From 2b897e56c7f5fa11a2da197946d69f7be349a80a Mon Sep 17 00:00:00 2001 From: Yury Popov Date: Mon, 26 Dec 2016 16:19:12 +0300 Subject: [PATCH] Cron module (#1674) --- app/include/user_modules.h | 1 + app/modules/cron.c | 233 +++++++++++++++++++++++++++++++++++++ docs/en/modules/cron.md | 129 ++++++++++++++++++++ mkdocs.yml | 1 + 4 files changed, 364 insertions(+) create mode 100644 app/modules/cron.c create mode 100644 docs/en/modules/cron.md diff --git a/app/include/user_modules.h b/app/include/user_modules.h index 990138b5..ffcad680 100644 --- a/app/include/user_modules.h +++ b/app/include/user_modules.h @@ -26,6 +26,7 @@ //#define LUA_USE_MODULES_BME280 //#define LUA_USE_MODULES_CJSON //#define LUA_USE_MODULES_COAP +//#define LUA_USE_MODULES_CRON //#define LUA_USE_MODULES_CRYPTO #define LUA_USE_MODULES_DHT //#define LUA_USE_MODULES_ENCODER diff --git a/app/modules/cron.c b/app/modules/cron.c new file mode 100644 index 00000000..0c8af2e8 --- /dev/null +++ b/app/modules/cron.c @@ -0,0 +1,233 @@ +// Cron module + +#include "module.h" +#include "lauxlib.h" +#include "lmem.h" +#include "user_interface.h" +#include "c_types.h" +#include "c_string.h" +#include "ets_sys.h" +#include "time.h" +#include "rtc/rtctime_internal.h" +#include "rtc/rtctime.h" +#include "stdlib.h" +#include "mem.h" + +struct cronent_desc { + uint64_t min; // Minutes repeat - bits 0-59 + uint32_t hour; // Hours repeat - bits 0-23 + uint32_t dom; // Day-of-month repeat - bits 0-30 + uint16_t mon; // Monthly repeat - bits 0-11 + uint8_t dow; // Day-of-week repeat - bits 0-6 +}; + +typedef struct cronent_ud { + struct cronent_desc desc; + int cb_ref; +} cronent_ud_t; + +static ETSTimer cron_timer; + +static int *cronent_list = 0; +static size_t cronent_count = 0; + +static uint64_t lcron_parsepart(lua_State *L, char *str, char **end, uint8_t min, uint8_t max) { + uint64_t res = 0; + if (str[0] == '*') { + uint32_t each = 1; + *end = str + 1; + if (str[1] == '/') { + each = strtol(str + 2, end, 10); + if (end != 0) + if (each == 0 || each >= max - min) { + return luaL_error(L, "invalid spec (each %d)", each); + } + } + for (int i = 0; i <= (max - min); i++) { + if (((min + i) % each) == 0) res |= (uint64_t)1 << i; + } + } else { + uint32_t val; + while (1) { + val = strtol(str, end, 10); + if (val < min || val > max) { + return luaL_error(L, "invalid spec (val %d out of range %d..%d)", val, min, max); + } + res |= (1 << (val - min)); + if (**end != ',') break; + str = *end + 1; + } + } + return res; +} + +static int lcron_parsedesc(lua_State *L, char *str, struct cronent_desc *desc) { + char *s = str; + desc->min = lcron_parsepart(L, s, &s, 0, 59); + if (*s != ' ') return luaL_error(L, "invalid spec (separator @%d)", s - str); + desc->hour = lcron_parsepart(L, s + 1, &s, 0, 23); + if (*s != ' ') return luaL_error(L, "invalid spec (separator @%d)", s - str); + desc->dow = lcron_parsepart(L, s + 1, &s, 0, 6); + if (*s != ' ') return luaL_error(L, "invalid spec (separator @%d)", s - str); + desc->dom = lcron_parsepart(L, s + 1, &s, 1, 31); + if (*s != ' ') return luaL_error(L, "invalid spec (separator @%d)", s - str); + desc->mon = lcron_parsepart(L, s + 1, &s, 1, 12); + if (*s != 0) return luaL_error(L, "invalid spec (trailing @%d)", s - str); + return 0; +} + +static int lcron_create(lua_State *L) { + // Check arguments + char *strdesc = (char*)luaL_checkstring(L, 1); + luaL_checkanyfunction(L, 2); + // Parse description + struct cronent_desc desc; + lcron_parsedesc(L, strdesc, &desc); + // Allocate userdata + cronent_ud_t *ud = lua_newuserdata(L, sizeof(cronent_ud_t)); + // Set metatable + luaL_getmetatable(L, "cron.entry"); + lua_setmetatable(L, -2); + // Set callback + lua_pushvalue(L, 2); + ud->cb_ref = luaL_ref(L, LUA_REGISTRYINDEX); + // Set entry + ud->desc = desc; + // Store entry + lua_pushvalue(L, -1); + cronent_list = os_realloc(cronent_list, sizeof(int) * (cronent_count + 1)); + cronent_list[cronent_count++] = luaL_ref(L, LUA_REGISTRYINDEX); + return 1; +} + +static size_t lcron_findindex(lua_State *L, cronent_ud_t *ud) { + cronent_ud_t *eud; + size_t i; + for (i = 0; i < cronent_count; i++) { + lua_rawgeti(L, LUA_REGISTRYINDEX, cronent_list[i]); + eud = lua_touserdata(L, 1); + lua_pop(L, 1); + if (eud == ud) break; + } + if (i == cronent_count) return -1; + return i; +} + +static int lcron_schedule(lua_State *L) { + cronent_ud_t *ud = luaL_checkudata(L, 1, "cron.entry"); + char *strdesc = (char*)luaL_checkstring(L, 2); + struct cronent_desc desc; + lcron_parsedesc(L, strdesc, &desc); + ud->desc = desc; + size_t i = lcron_findindex(L, ud); + if (i == -1) { + lua_pushvalue(L, 1); + cronent_list = os_realloc(cronent_list, sizeof(int) * (cronent_count + 1)); + cronent_list[cronent_count++] = lua_ref(L, LUA_REGISTRYINDEX); + } + return 0; +} + +static int lcron_handler(lua_State *L) { + cronent_ud_t *ud = luaL_checkudata(L, 1, "cron.entry"); + luaL_checkanyfunction(L, 2); + lua_pushvalue(L, 2); + luaL_unref(L, LUA_REGISTRYINDEX, ud->cb_ref); + ud->cb_ref = luaL_ref(L, LUA_REGISTRYINDEX); + return 0; +} + +static int lcron_unschedule(lua_State *L) { + cronent_ud_t *ud = luaL_checkudata(L, 1, "cron.entry"); + size_t i = lcron_findindex(L, ud); + if (i == -1) return 0; + luaL_unref(L, LUA_REGISTRYINDEX, cronent_list[i]); + memmove(cronent_list + i, cronent_list + i + 1, sizeof(int) * cronent_count - i - 1); + cronent_count--; + return 0; +} + +static int lcron_delete(lua_State *L) { + cronent_ud_t *ud = luaL_checkudata(L, 1, "cron.entry"); + lcron_unschedule(L); + luaL_unref(L, LUA_REGISTRYINDEX, ud->cb_ref); + return 0; +} + +static int lcron_reset(lua_State *L) { + for (size_t i = 0; i < cronent_count; i++) { + luaL_unref(L, LUA_REGISTRYINDEX, cronent_list[i]); + } + cronent_count = 0; + os_free(cronent_list); + cronent_list = 0; + return 0; +} + +static void cron_handle_time(uint8_t mon, uint8_t dom, uint8_t dow, uint8_t hour, uint8_t min) { + lua_State *L = lua_getstate(); + struct cronent_desc desc; + desc.mon = (uint16_t)1 << (mon - 1); + desc.dom = (uint32_t)1 << (dom - 1); + desc.dow = ( uint8_t)1 << dow; + desc.hour = (uint32_t)1 << hour; + desc.min = (uint64_t)1 << min; + for (size_t i = 0; i < cronent_count; i++) { + lua_rawgeti(L, LUA_REGISTRYINDEX, cronent_list[i]); + cronent_ud_t *ent = lua_touserdata(L, 1); + lua_pop(L, 1); + if ((ent->desc.mon & desc.mon ) == 0) continue; + if ((ent->desc.dom & desc.dom ) == 0) continue; + if ((ent->desc.dow & desc.dow ) == 0) continue; + if ((ent->desc.hour & desc.hour) == 0) continue; + if ((ent->desc.min & desc.min ) == 0) continue; + lua_rawgeti(L, LUA_REGISTRYINDEX, ent->cb_ref); + lua_rawgeti(L, LUA_REGISTRYINDEX, cronent_list[i]); + lua_call(L, 1, 0); + } +} + +static void cron_handle_tmr() { + struct rtc_timeval tv; + rtc_time_gettimeofday(&tv); + if (tv.tv_sec == 0) { // Wait for RTC time + ets_timer_arm_new(&cron_timer, 1000, 0, 1); + return; + } + time_t t = tv.tv_sec; + struct tm tm; + gmtime_r(&t, &tm); + uint32_t diff = 60000 - tm.tm_sec * 1000 - tv.tv_usec / 1000; + if (tm.tm_sec == 59) { + t++; + diff += 60000; + gmtime_r(&t, &tm); + } + ets_timer_arm_new(&cron_timer, diff, 0, 1); + cron_handle_time(tm.tm_mon + 1, tm.tm_mday, tm.tm_wday, tm.tm_hour, tm.tm_min); +} + +static const LUA_REG_TYPE cronent_map[] = { + { LSTRKEY( "schedule" ), LFUNCVAL( lcron_schedule ) }, + { LSTRKEY( "handler" ), LFUNCVAL( lcron_handler ) }, + { LSTRKEY( "unschedule" ), LFUNCVAL( lcron_unschedule ) }, + { LSTRKEY( "__gc" ), LFUNCVAL( lcron_delete ) }, + { LSTRKEY( "__index" ), LROVAL( cronent_map ) }, + { LNILKEY, LNILVAL } +}; + +static const LUA_REG_TYPE cron_map[] = { + { LSTRKEY( "schedule" ), LFUNCVAL( lcron_create ) }, + { LSTRKEY( "reset" ), LFUNCVAL( lcron_reset ) }, + { LNILKEY, LNILVAL } +}; + +int luaopen_cron( lua_State *L ) { + ets_timer_disarm(&cron_timer); + ets_timer_setfn(&cron_timer, cron_handle_tmr, 0); + ets_timer_arm_new(&cron_timer, 1000, 0, 1); + luaL_rometatable(L, "cron.entry", (void *)cronent_map); + return 0; +} + +NODEMCU_MODULE(CRON, "cron", cron_map, luaopen_cron); diff --git a/docs/en/modules/cron.md b/docs/en/modules/cron.md new file mode 100644 index 00000000..93798de4 --- /dev/null +++ b/docs/en/modules/cron.md @@ -0,0 +1,129 @@ +# Cron Module +| Since | Origin / Contributor | Maintainer | Source | +| :----- | :-------------------- | :---------- | :------ | +| 2016-12-18 | [PhoeniX](https://github.com/djphoenix) | [PhoeniX](https://github.com/djphoenix) | [cron.c](../../../app/modules/cron.c)| + +[Cron](https://en.wikipedia.org/wiki/Cron)-like scheduler module. + +!!! important + This module needs RTC time to operate correctly. Do not forget to include the [`rtctime`](rtctime.md) module. + +## cron.schedule() + +Creates a new schedule entry. + +#### Syntax +`cron.schedule(mask, callback)` + +#### Parameters +- `mask` - [crontab](https://en.wikipedia.org/wiki/Cron#Overview)-like string mask for schedule +- `callback` - callback `function(entry)` that is executed at the scheduled time + +#### Returns +`cron.entry` sub module + +#### Example + +```lua +cron.schedule("* * * * *", function(e) + print("Every minute") +end) + +cron.schedule("*/5 * * * *", function(e) + print("Every 5 minutes") +end) + +cron.schedule("0 */2 * * *", function(e) + print("Every 2 hours") +end) +``` + +## cron.reset() + +Removes all scheduled entries. + +#### Syntax +`cron.reset()` + +#### Parameters +none + +#### Returns +nil + +# cron.entry Module + +## cron.entry:handler() + +Sets a new handler for entry. + +#### Syntax +`handler(callback)` + +#### Parameters +- `callback` - callback `function(entry)` that is executed at the scheduled time + +#### Returns +nil + +#### Example + +```lua +ent = cron.schedule("* * * * *", function(e) + print("Every minute") +end) + +ent:handler(function(e) + print("New handler: Every minute") +end) +``` + +## cron.entry:schedule() + +Sets a new schedule mask. + +#### Syntax +`schedule(mask)` + +#### Parameters +- `mask` - [crontab](https://en.wikipedia.org/wiki/Cron#Overview)-like string mask for schedule + +#### Returns +none + +#### Example + +```lua +ent = cron.schedule("* * * * *", function(e) + print("Tick") +end) + +-- Every 5 minutes is really better! +ent:schedule("*/5 * * * *") +``` + +## cron.entry:unschedule() + +Disables schedule. + +Disabled schedules may be enabled again by calling [`:schedule(mask)`](cron.md#cronentryschedule). + +#### Syntax +`unschedule()` + +#### Parameters +none + +#### Returns +nil + +#### Example + +```lua +ent = cron.schedule("* * * * *", function(e) + print("Tick") +end) + +-- We don't need this anymore +ent:unschedule() +``` diff --git a/mkdocs.yml b/mkdocs.yml index 8b05b0d2..5b33e436 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -44,6 +44,7 @@ pages: - 'bmp085': 'en/modules/bmp085.md' - 'cjson': 'en/modules/cjson.md' - 'coap': 'en/modules/coap.md' + - 'cron': 'en/modules/cron.md' - 'crypto': 'en/modules/crypto.md' - 'dht': 'en/modules/dht.md' - 'encoder': 'en/modules/encoder.md'