diff --git a/components/modules/Kconfig b/components/modules/Kconfig index 4309295c..dc1ca7a9 100644 --- a/components/modules/Kconfig +++ b/components/modules/Kconfig @@ -111,6 +111,12 @@ config LUA_MODULE_STRUCT Includes the struct module. This module provides [un]packing of raw byte strings into Lua values and vice versa. +config LUA_MODULE_TMR + bool "Timer module" + default "y" + help + Includes the timer module (recommended). + config LUA_MODULE_WIFI bool "WiFi module" default "y" diff --git a/components/modules/tmr.c b/components/modules/tmr.c new file mode 100755 index 00000000..d611ccb5 --- /dev/null +++ b/components/modules/tmr.c @@ -0,0 +1,266 @@ + +#include "module.h" +#include "lauxlib.h" +#include +#include "task/task.h" + +#include "freertos/FreeRTOS.h" +#include "freertos/timers.h" + +#define TIMER_MODE_OFF 3 +#define TIMER_MODE_SINGLE 0 +#define TIMER_MODE_SEMI 2 +#define TIMER_MODE_AUTO 1 +#define TIMER_IDLE_FLAG (1<<7) + +#define STRINGIFY_VAL(x) #x +#define STRINGIFY(x) STRINGIFY_VAL(x) + +// assuming system_timer_reinit() has *not* been called +#define MAX_TIMEOUT_DEF 6870947 //SDK 1.5.3 limit (0x68D7A3) + +static const uint32 MAX_TIMEOUT=MAX_TIMEOUT_DEF; +static const char* MAX_TIMEOUT_ERR_STR = "Range: 1-"STRINGIFY(MAX_TIMEOUT_DEF); + +typedef struct{ + TimerHandle_t timer; + int32_t cb_ref, self_ref; + uint32_t interval; + uint8_t mode; +} tmr_struct_t; +typedef tmr_struct_t *tmr_t; + +static task_handle_t alarm_task_id; + +static void timer_callback(TimerHandle_t xTimer) +{ + tmr_t tmr = (tmr_t)pvTimerGetTimerID(xTimer); + + task_post_high(alarm_task_id, (task_param_t)tmr); +} + +static void alarm_timer_task(task_param_t param, task_prio_t prio) +{ + tmr_t tmr = (tmr_t)param; + lua_State* L = lua_getstate(); + if (tmr->cb_ref == LUA_NOREF) + return; + lua_rawgeti(L, LUA_REGISTRYINDEX, tmr->cb_ref); + lua_rawgeti(L, LUA_REGISTRYINDEX, tmr->self_ref); + // if the timer was set to single run we clean up after it + if (tmr->mode == TIMER_MODE_SINGLE) { + luaL_unref(L, LUA_REGISTRYINDEX, tmr->cb_ref); + tmr->cb_ref = LUA_NOREF; + tmr->mode = TIMER_MODE_OFF; + } else if (tmr->mode == TIMER_MODE_SEMI) { + tmr->mode |= TIMER_IDLE_FLAG; + } + if (tmr->mode != TIMER_MODE_AUTO && tmr->self_ref != LUA_REFNIL) { + luaL_unref(L, LUA_REGISTRYINDEX, tmr->self_ref); + tmr->self_ref = LUA_NOREF; + } + lua_call(L, 1, 0); +} + +static tmr_t tmr_get( lua_State *L, int stack ) +{ + return (tmr_t)luaL_checkudata(L, stack, "tmr.timer"); +} + +// Lua: tmr.register( self, interval, mode, function ) +static int tmr_register(lua_State* L) +{ + int stack = 0; + + tmr_t tmr = tmr_get(L, ++stack); + + uint32_t interval = luaL_checkinteger(L, ++stack); + luaL_argcheck(L, interval > 0 && interval <= MAX_TIMEOUT, stack, MAX_TIMEOUT_ERR_STR); + + uint8_t mode = luaL_checkinteger(L, ++stack); + luaL_argcheck(L, mode == TIMER_MODE_SINGLE || mode == TIMER_MODE_SEMI || mode == TIMER_MODE_AUTO, stack, "Invalid mode"); + + ++stack; + luaL_argcheck(L, lua_type(L, stack) == LUA_TFUNCTION || lua_type(L, stack) == LUA_TLIGHTFUNCTION, stack, "Must be function"); + + if (tmr->timer) { + // delete previous timer since mode change could be requested here + xTimerDelete(tmr->timer, portMAX_DELAY); + } + + //get the lua function reference + luaL_unref(L, LUA_REGISTRYINDEX, tmr->cb_ref); + lua_pushvalue(L, stack); + tmr->cb_ref = luaL_ref(L, LUA_REGISTRYINDEX); + tmr->mode = mode | TIMER_IDLE_FLAG; + tmr->interval = interval; + tmr->timer = xTimerCreate("tmr", + pdMS_TO_TICKS(interval), + mode == TIMER_MODE_AUTO ? pdTRUE : pdFALSE, + (void *)tmr, + timer_callback); + + if (!tmr->timer) { + luaL_error(L, "FreeRTOS timer creation failed"); + } + + return 0; +} + +// Lua: tmr.start( self ) +static int tmr_start(lua_State* L) +{ + tmr_t tmr = tmr_get(L, 1); + + if (tmr->self_ref == LUA_NOREF) { + lua_pushvalue(L, 1); + tmr->self_ref = luaL_ref(L, LUA_REGISTRYINDEX); + } + + if (!tmr->timer || tmr->cb_ref == LUA_NOREF) { + luaL_error(L, "timer not registered"); + } + + // we return false if the timer is not idle + if(!(tmr->mode & TIMER_IDLE_FLAG)) { + lua_pushboolean(L, 0); + } else { + tmr->mode &= ~TIMER_IDLE_FLAG; + if (xTimerStart(tmr->timer, portMAX_DELAY) != pdPASS) { + luaL_error(L, "timer start failed"); + } + lua_pushboolean(L, 1); + } + return 1; +} + +// Lua: tmr.alarm( self, interval, repeat, function ) +static int tmr_alarm(lua_State* L){ + tmr_register(L); + return tmr_start(L); +} + +// Lua: tmr.stop( id / ref ) +static int tmr_stop(lua_State* L) +{ + tmr_t tmr = tmr_get(L, 1); + + luaL_unref(L, LUA_REGISTRYINDEX, tmr->self_ref); + tmr->self_ref = LUA_NOREF; + + //we return false if the timer is idle (of not registered) + if(!(tmr->mode & TIMER_IDLE_FLAG) && tmr->mode != TIMER_MODE_OFF){ + tmr->mode |= TIMER_IDLE_FLAG; + xTimerStop(tmr->timer, portMAX_DELAY); + lua_pushboolean(L, 1); + } else { + lua_pushboolean(L, 0); + } + return 1; +} + +// Lua: tmr.unregister( self ) +static int tmr_unregister(lua_State* L){ + tmr_t tmr = tmr_get(L, 1); + + if (tmr->self_ref != LUA_REFNIL) { + luaL_unref(L, LUA_REGISTRYINDEX, tmr->self_ref); + tmr->self_ref = LUA_NOREF; + } + + if(!(tmr->mode & TIMER_IDLE_FLAG) && tmr->mode != TIMER_MODE_OFF) { + if (tmr->timer) { + xTimerStop(tmr->timer, portMAX_DELAY); + } + } + if (tmr->timer) { + xTimerDelete(tmr->timer, portMAX_DELAY); + } + tmr->timer = NULL; + luaL_unref(L, LUA_REGISTRYINDEX, tmr->cb_ref); + tmr->cb_ref = LUA_NOREF; + tmr->mode = TIMER_MODE_OFF; + return 0; +} + +// Lua: tmr.interval( self, interval ) +static int tmr_interval(lua_State* L) +{ + tmr_t tmr = tmr_get(L, 1); + + uint32_t interval = luaL_checkinteger(L, 2); + luaL_argcheck(L, interval > 0 && interval <= MAX_TIMEOUT, 2, MAX_TIMEOUT_ERR_STR); + if (tmr->mode != TIMER_MODE_OFF) { + tmr->interval = interval; + if (!(tmr->mode & TIMER_IDLE_FLAG)) { + xTimerStop(tmr->timer, portMAX_DELAY); + if (xTimerChangePeriod(tmr->timer, tmr->interval, portMAX_DELAY) != pdPASS) { + luaL_error(L, "cannot change period"); + } + // stop again since xTimerChangePeriod will re-start the timer + xTimerStop(tmr->timer, portMAX_DELAY); + } + } + return 0; +} + +// Lua: tmr.state( self, state ) +static int tmr_state(lua_State* L) +{ + tmr_t tmr = tmr_get(L, 1); + + if (tmr->mode == TIMER_MODE_OFF) { + lua_pushnil(L); + return 1; + } + lua_pushboolean(L, (tmr->mode & TIMER_IDLE_FLAG) == 0); + lua_pushinteger(L, tmr->mode & (~TIMER_IDLE_FLAG)); + return 2; +} + +// Lua: tmr.create() +static int tmr_create( lua_State *L ) { + tmr_t ud = (tmr_t)lua_newuserdata(L, sizeof(tmr_struct_t)); + if (!ud) return luaL_error(L, "not enough memory"); + luaL_getmetatable(L, "tmr.timer"); + lua_setmetatable(L, -2); + ud->cb_ref = LUA_NOREF; + ud->self_ref = LUA_NOREF; + ud->mode = TIMER_MODE_OFF; + ud->timer = NULL; + return 1; +} + + +// Module function map + +static const LUA_REG_TYPE tmr_dyn_map[] = { + { LSTRKEY( "register" ), LFUNCVAL( tmr_register ) }, + { LSTRKEY( "alarm" ), LFUNCVAL( tmr_alarm ) }, + { LSTRKEY( "start" ), LFUNCVAL( tmr_start ) }, + { LSTRKEY( "stop" ), LFUNCVAL( tmr_stop ) }, + { LSTRKEY( "unregister" ), LFUNCVAL( tmr_unregister ) }, + { LSTRKEY( "interval" ), LFUNCVAL( tmr_interval) }, + { LSTRKEY( "state" ), LFUNCVAL( tmr_state ) }, + { LSTRKEY( "__gc" ), LFUNCVAL( tmr_unregister ) }, + { LSTRKEY( "__index" ), LROVAL( tmr_dyn_map ) }, + { LNILKEY, LNILVAL } +}; + +static const LUA_REG_TYPE tmr_map[] = { + { LSTRKEY( "create" ), LFUNCVAL( tmr_create ) }, + { LSTRKEY( "ALARM_SINGLE" ), LNUMVAL( TIMER_MODE_SINGLE ) }, + { LSTRKEY( "ALARM_SEMI" ), LNUMVAL( TIMER_MODE_SEMI ) }, + { LSTRKEY( "ALARM_AUTO" ), LNUMVAL( TIMER_MODE_AUTO ) }, + { LNILKEY, LNILVAL } +}; + +static int luaopen_tmr( lua_State *L ){ + luaL_rometatable(L, "tmr.timer", (void *)tmr_dyn_map); + + alarm_task_id = task_get_id(alarm_timer_task); + + return 0; +} + +NODEMCU_MODULE(TMR, "tmr", tmr_map, luaopen_tmr); diff --git a/docs/en/modules/tmr.md b/docs/en/modules/tmr.md new file mode 100644 index 00000000..d6539a87 --- /dev/null +++ b/docs/en/modules/tmr.md @@ -0,0 +1,207 @@ +# Timer Module +| Since | Origin / Contributor | Maintainer | Source | +| :----- | :-------------------- | :---------- | :------ | +| 2014-12-12 | [Zeroday](https://github.com/funshine) | [dnc40085](https://github.com/dnc40085) | [tmr.c](../../../app/modules/tmr.c)| + +The tmr module allows access to simple timers. It is aimed at setting up regularly occurring tasks and timing out operations. + +What the tmr module is *not* however, is a time keeping module. While all timeouts are expressed in milliseconds, the accuracy is limited and compounding errors would lead to rather inaccurate time keeping. Consider using the [rtctime](rtctime.md) module for "wall clock" time. + +## tmr.create() + +Creates a dynamic timer object. + +#### Parameters +none + +#### Returns +`timer` object + +#### Example +```lua +local mytimer = tmr.create() + +-- oo calling +mytimer:register(5000, tmr.ALARM_SINGLE, function (t) print("expired") end) +mytimer:start() +mytimer = nil +``` + +# tmr Object +## tmr.obj:alarm() + +This is a convenience function combining [`tmr.obj:register()`](#tmrobjregister) and [`tmr.obj:start()`](#tmrobjstart) into a single call. + +To free up the resources with this timer when done using it, call [`tmr.obj:unregister()`](#tmrobjunregister) on it. For one-shot timers this is not necessary, unless they were stopped before they expired. + +#### Syntax +`mytmr:alarm(interval_ms, mode, func())` + +#### Parameters +- `interval_ms` timer interval in milliseconds. Maximum value is 6870947 (1:54:30.947). +- `mode` timer mode: + - `tmr.ALARM_SINGLE` a one-shot alarm (and no need to call [`tmr.unregister()`](#tmrunregister)) + - `tmr.ALARM_SEMI` manually repeating alarm (call [`tmr.start()`](#tmrstart) to restart) + - `tmr.ALARM_AUTO` automatically repeating alarm +- `func(timer)` callback function which is invoked with the timer object as an argument + +#### Returns +`true` if the timer was started, `false` on error + +#### Example +```lua +if not tmr.create():alarm(5000, tmr.ALARM_SINGLE, function() + print("hey there") +end) +then + print("whoopsie") +end +``` +#### See also +- [`tmr.create()`](#tmrcreate) +- [`tmr.obj:register()`](#tmrobjregister) +- [`tmr.obj:start()`](#tmrobjstart) +- [`tmr.obj:unregister()`](#tmrobjunregister) + +## tmr.obj:interval() + +Changes a registered timer's expiry interval. + +#### Syntax +`mytmr:interval(interval_ms)` + +#### Parameters +- `interval_ms` new timer interval in milliseconds. Maximum value is 6870947 (1:54:30.947). + +#### Returns +`nil` + +#### Example +```lua +mytimer = tmr.create() +mytimer:register(10000, tmr.ALARM_AUTO, function() print("hey there") end) +mytimer:interval(3000) -- actually, 3 seconds is better! +mytimer:start() +``` + +## tmr.obj:register() + +Configures a timer and registers the callback function to call on expiry. + +To free up the resources with this timer when done using it, call [`tmr.obj:unregister()`](#tmrobjunregister) on it. For one-shot timers this is not necessary, unless they were stopped before they expired. + +#### Syntax +`mytmr:register(interval_ms, mode, func())` + +#### Parameters +- `interval_ms` timer interval in milliseconds. Maximum value is 6870947 (1:54:30.947). +- `mode` timer mode: + - `tmr.ALARM_SINGLE` a one-shot alarm (and no need to call [`tmr.unregister()`](#tmrunregister)) + - `tmr.ALARM_SEMI` manually repeating alarm (call [`tmr.start()`](#tmrunregister) to restart) + - `tmr.ALARM_AUTO` automatically repeating alarm +- `func(timer)` callback function which is invoked with the timer object as an argument + +Note that registering does *not* start the alarm. + +#### Returns +`nil` + +#### Example +```lua +mytimer = tmr.create() +mytimer:register(5000, tmr.ALARM_SINGLE, function() print("hey there") end) +mytimer:start() +``` +#### See also +- [`tmr.create()`](#tmrcreate) +- [`tmr.obj:alarm()`](#tmrobjalarm) + +## tmr.obj:start() + +Starts or restarts a previously configured timer. + +#### Syntax +`mytmr:start()` + +#### Parameters +none + +#### Returns +`true` if the timer was started, `false` on error + +#### Example +```lua +mytimer = tmr.create() +mytimer:register(5000, tmr.ALARM_SINGLE, function() print("hey there") end) +if not mytimer:start() then print("uh oh") end +``` +#### See also +- [`tmr.create()`](#tmrcreate) +- [`tmr.obj:register()`](#tmrobjregister) +- [`tmr.obj:stop()`](#tmrobjstop) +- [`tmr.obj:unregister()`](#tmrobjunregister) + +## tmr.obj:state() + +Checks the state of a timer. + +#### Syntax +`mytmr:state()` + +#### Parameters +none + +#### Returns +(bool, int) or `nil` + +If the specified timer is registered, returns whether it is currently started and its mode. If the timer is not registered, `nil` is returned. + +#### Example +```lua +mytimer = tmr.create() +print(mytimer:state()) -- nil +mytimer:register(5000, tmr.ALARM_SINGLE, function() print("hey there") end) +running, mode = mytimer:state() +print("running: " .. tostring(running) .. ", mode: " .. mode) -- running: false, mode: 0 +``` + +## tmr.obj:stop() + +Stops a running timer, but does *not* unregister it. A stopped timer can be restarted with [`tmr.obj:start()`](#tmrobjstart). + +#### Syntax +`mytmr:stop()` + +#### Parameters +none + +#### Returns +`true` if the timer was stopped, `false` on error + +#### Example +```lua +mytimer = tmr.create() +if not mytimer:stop() then print("timer not stopped, not registered?") end +``` +#### See also +- [`tmr.obj:register()`](#tmrobjregister) +- [`tmr.obj:stop()`](#tmrobjstop) +- [`tmr.obj:unregister()`](#tmrobjunregister) + +## tmr.obj:unregister() + +Stops the timer (if running) and unregisters the associated callback. + +This isn't necessary for one-shot timers (`tmr.ALARM_SINGLE`), as those automatically unregister themselves when fired. + +#### Syntax +`mytmr:unregister()` + +#### Parameters +none + +#### Returns +`nil` + +#### See also +[`tmr.obj:register()`](#tmrobjregister) diff --git a/mkdocs.yml b/mkdocs.yml index eba9c9fd..5a10cfd9 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -37,6 +37,7 @@ pages: - 'node': 'en/modules/node.md' - 'sigma delta': 'en/modules/sigma-delta.md' - 'struct': 'en/modules/struct.md' + - 'tmr': 'en/modules/tmr.md' - 'uart': 'en/modules/uart.md' - 'wifi': 'en/modules/wifi.md'