diff --git a/components/lua/lua-5.3/ldebug.c b/components/lua/lua-5.3/ldebug.c index 7f02f78c..aee12c3f 100644 --- a/components/lua/lua-5.3/ldebug.c +++ b/components/lua/lua-5.3/ldebug.c @@ -132,10 +132,11 @@ static const char *upvalname (Proto *p, int uv) { static const char *findvararg (CallInfo *ci, int n, StkId *pos) { int nparams = getnumparams(clLvalue(ci->func)->p); - if (n >= cast_int(ci->u.l.base - ci->func) - nparams) + int nvararg = cast_int(ci->u.l.base - ci->func) - nparams; + if (n <= -nvararg) return NULL; /* no such vararg */ else { - *pos = ci->func + nparams + n; + *pos = ci->func + nparams - n; return "(*vararg)"; /* generic name for any vararg */ } } @@ -147,7 +148,7 @@ static const char *findlocal (lua_State *L, CallInfo *ci, int n, StkId base; if (isLua(ci)) { if (n < 0) /* access to vararg values? */ - return findvararg(ci, -n, pos); + return findvararg(ci, n, pos); else { base = ci->u.l.base; name = luaF_getlocalname(ci_func(ci)->p, n, currentpc(ci)); diff --git a/components/modules/CMakeLists.txt b/components/modules/CMakeLists.txt index 3719a39f..25279a26 100644 --- a/components/modules/CMakeLists.txt +++ b/components/modules/CMakeLists.txt @@ -8,6 +8,7 @@ set(module_srcs "dht.c" "encoder.c" "eromfs.c" + "espnow.c" "file.c" "gpio.c" "heaptrace.c" diff --git a/components/modules/Kconfig b/components/modules/Kconfig index 510c6fdc..2dd4af14 100644 --- a/components/modules/Kconfig +++ b/components/modules/Kconfig @@ -82,6 +82,12 @@ menu "NodeMCU modules" store the directory path as part of the filename just as SPIFFS does. + config NODEMCU_CMODULE_ESPNOW + bool "ESP-NOW module" + default "n" + help + Includes the espnow module. + config NODEMCU_CMODULE_ETH depends on IDF_TARGET_ESP32 select ETH_USE_ESP32_EMAC diff --git a/components/modules/espnow.c b/components/modules/espnow.c new file mode 100644 index 00000000..5507a9b3 --- /dev/null +++ b/components/modules/espnow.c @@ -0,0 +1,390 @@ +/* + * Copyright 2024 Dius Computing Pty Ltd. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the + * distribution. + * - Neither the name of the copyright holders nor the names of + * its contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @author Jade Mattsson + */ +#include "module.h" +#include "platform.h" +#include "ip_fmt.h" +#include "task/task.h" +#include "lauxlib.h" +#include "esp_now.h" +#include "esp_wifi.h" +#include "esp_err.h" + +#if ESP_NOW_ETH_ALEN != 6 +# error "MAC address length assumption broken" +#endif +#if ESP_NOW_MAX_DATA_LEN > 0xffff +# error "Update len field in received_packet_t" +#endif + +typedef struct { + uint8_t src[6]; + uint8_t dst[6]; + int16_t rssi; + uint16_t len; // ESP_NOW_MAX_DATA_LEN is currently 250 + char data[0]; +} received_packet_t; + +typedef struct { + uint8_t dst[6]; + esp_now_send_status_t status; +} sent_packet_t; + + +static task_handle_t espnow_task = 0; + +static int recv_ref = LUA_NOREF; +static int sent_ref = LUA_NOREF; + + +// --- Helper functions ----------------------------------- + +static int *cb_ref_for_event(const char *name) +{ + if (strcmp("receive", name) == 0) + return &recv_ref; + else if (strcmp("sent", name) == 0) + return &sent_ref; + else + return NULL; +} + +static int hexval(lua_State *L, char c) +{ + if (c >= '0' && c <= '9') + return c - '0'; + else if (c >= 'A' && c <= 'F') + return c - 'A' + 10; + else if (c >= 'a' && c <= 'f') + return c - 'a' + 10; + else + return luaL_error(L, "invalid hex digit '%c'", c); +} + +// TODO: share with wifi_sta.c +static bool parse_mac(const char *str, uint8_t out[6]) +{ + const char *fmts[] = { + "%hhx%hhx%hhx%hhx%hhx%hhx", + "%hhx:%hhx:%hhx:%hhx:%hhx:%hhx", + "%hhx-%hhx-%hhx-%hhx-%hhx-%hhx", + "%hhx %hhx %hhx %hhx %hhx %hhx", + NULL + }; + for (unsigned i = 0; fmts[i]; ++i) + { + if (sscanf (str, fmts[i], + &out[0], &out[1], &out[2], &out[3], &out[4], &out[5]) == 6) + return true; + } + return false; +} + + +static void on_receive(const esp_now_recv_info_t *info, const uint8_t *data, int len) +{ + if (len < 0) + return; // Don't do that. + + received_packet_t *p = malloc(sizeof(received_packet_t) + len); + if (!p) + { + NODE_ERR("out of memory\n"); + return; + } + memcpy(p->src, info->src_addr, sizeof(p->src)); + memcpy(p->dst, info->des_addr, sizeof(p->dst)); + p->rssi = info->rx_ctrl->rssi; + p->len = len; + memcpy(p->data, data, len); + + if (!task_post_high(espnow_task, (task_param_t)p)) + { + NODE_ERR("lost esp-now packet; task queue full\n"); + free(p); + } +} + + +static void on_sent(const uint8_t *mac, esp_now_send_status_t status) +{ + sent_packet_t *p = malloc(sizeof(sent_packet_t)); + if (!p) + { + NODE_ERR("out of memory\n"); + return; + } + memcpy(p->dst, mac, sizeof(p->dst)); + p->status = status; + + if (!task_post_medium(espnow_task, (task_param_t)p)) + { + NODE_ERR("lost esp-now packet; task queue full\n"); + free(p); + } +} + + +static void espnow_task_fn(task_param_t param, task_prio_t prio) +{ + lua_State* L = lua_getstate(); + int top = lua_gettop(L); + luaL_checkstack(L, 3, ""); + + if (prio == TASK_PRIORITY_HIGH) // received packet + { + received_packet_t *p = (received_packet_t *)param; + if (recv_ref != LUA_NOREF) + { + lua_rawgeti (L, LUA_REGISTRYINDEX, recv_ref); + lua_createtable(L, 0, 4); // src, dst, rssi, data + char mac[MAC_STR_SZ]; + macstr(mac, p->src); + lua_pushstring(L, mac); + lua_setfield(L, -2, "src"); + macstr(mac, p->dst); + lua_pushstring(L, mac); + lua_setfield(L, -2, "dst"); + lua_pushinteger(L, p->rssi); + lua_setfield(L, -2, "rssi"); + lua_pushlstring(L, p->data, p->len); + lua_setfield(L, -2, "data"); + luaL_pcallx(L, 1, 0); + } + free(p); + } + else if (prio == TASK_PRIORITY_MEDIUM) // sent + { + sent_packet_t *p = (sent_packet_t *)param; + if (sent_ref != LUA_NOREF) + { + lua_rawgeti (L, LUA_REGISTRYINDEX, sent_ref); + char dst[MAC_STR_SZ]; + macstr(dst, p->dst); + lua_pushstring(L, dst); + if (p->status == ESP_NOW_SEND_SUCCESS) + lua_pushinteger(L, 1); + else + lua_pushnil(L); + luaL_pcallx(L, 2, 0); + } + free(p); + } + + lua_settop(L, top); // restore original before exit +} + + +static void err_check(lua_State *L, esp_err_t err) +{ + if (err != ESP_OK) + luaL_error(L, "%s", esp_err_to_name(err)); +} + + +// --- Lua interface functions ----------------------------------- + +static int lespnow_start(lua_State *L) +{ + err_check(L, esp_now_init()); + err_check(L, esp_now_register_recv_cb(on_receive)); + err_check(L, esp_now_register_send_cb(on_sent)); + return 0; +} + +static int lespnow_stop(lua_State *L) +{ + err_check(L, esp_now_unregister_send_cb()); + err_check(L, esp_now_unregister_recv_cb()); + err_check(L, esp_now_deinit()); + return 0; +} + +static int lespnow_getversion(lua_State *L) +{ + uint32_t ver; + err_check(L, esp_now_get_version(&ver)); + lua_pushinteger(L, ver); + return 1; +} + +// espnow.on('sent' or 'received', cb) +// sent -> cb('ma:ca:dd:00:11:22', status) +// received -> cb({ src=, dst=, data=, rssi= }) +static int lespnow_on(lua_State *L) +{ + const char *evtname = luaL_checkstring(L, 1); + int *ref = cb_ref_for_event(evtname); + if (!ref) + return luaL_error(L, "unknown event type"); + + if (lua_isnoneornil(L, 2)) + { + if (*ref != LUA_NOREF) + { + luaL_unref(L, LUA_REGISTRYINDEX, *ref); + *ref = LUA_NOREF; + } + } + else if (lua_isfunction(L, 2)) + { + if (*ref != LUA_NOREF) + luaL_unref(L, LUA_REGISTRYINDEX, *ref); + lua_pushvalue(L, 2); + *ref = luaL_ref(L, LUA_REGISTRYINDEX); + } + else + return luaL_error(L, "expected function"); + return 0; +} + +// espnow.send('ma:ca:dd:00:11:22' or nil, str) +static int lespnow_send(lua_State *L) +{ + const char *mac = luaL_optstring(L, 1, NULL); + size_t payloadlen = 0; + const char *payload = luaL_checklstring(L, 2, &payloadlen); + + uint8_t peer_addr[6]; + if (mac && !parse_mac(mac, peer_addr)) + return luaL_error(L, "bad peer address"); + + err_check(L, esp_now_send( + mac ? peer_addr : NULL, (const uint8_t *)payload, payloadlen)); + return 0; +} + + +// espnow.addpeer('ma:ca:dd:00:11:22', { lmk=, channel=, encrypt= } +static int lespnow_addpeer(lua_State *L) +{ + esp_now_peer_info_t peer_info; + memset(&peer_info, 0, sizeof(peer_info)); + + const char *mac = luaL_checkstring(L, 1); + + wifi_mode_t mode = WIFI_MODE_NULL; + err_check(L, esp_wifi_get_mode(&mode)); + + switch (mode) + { + case WIFI_MODE_STA: peer_info.ifidx = WIFI_IF_STA; break; + case WIFI_MODE_APSTA: // fall-through + case WIFI_MODE_AP: peer_info.ifidx = WIFI_IF_AP; break; + default: return luaL_error(L, "No wifi interface found"); + } + + if (!parse_mac(mac, peer_info.peer_addr)) + return luaL_error(L, "bad peer address"); + + lua_settop(L, 2); // Discard excess parameters, to ensure we have space + if (lua_istable(L, 2)) + { + lua_getfield(L, 2, "encrypt"); + peer_info.encrypt = luaL_optint(L, -1, 0); + lua_pop(L, 1); + if (peer_info.encrypt) + { + lua_getfield(L, 2, "lmk"); + size_t lmklen = 0; + const char *lmkstr = luaL_checklstring(L, -1, &lmklen); + lua_pop(L, 1); + if (lmklen != 2*sizeof(peer_info.lmk)) + return luaL_error(L, "LMK must be %d hex digits", 2*ESP_NOW_KEY_LEN); + for (unsigned i = 0; i < sizeof(peer_info.lmk); ++i) + peer_info.lmk[i] = + (hexval(L, lmkstr[i*2]) << 4) + hexval(L, lmkstr[i*2 +1]); + } + + lua_getfield(L, 2, "channel"); + peer_info.channel = luaL_optint(L, -1, 0); + lua_pop(L, 1); + } + + err_check(L, esp_now_add_peer(&peer_info)); + return 0; +} + +// espnow.delpeer('ma:ca:dd:00:11:22') +static int lespnow_delpeer(lua_State *L) +{ + const char *mac = luaL_checkstring(L, 1); + uint8_t peer_addr[6]; + if (!parse_mac(mac, peer_addr)) + return luaL_error(L, "bad peer address"); + + err_check(L, esp_now_del_peer(peer_addr)); + return 0; +} + +static int lespnow_setpmk(lua_State *L) +{ + uint8_t pmk[ESP_NOW_KEY_LEN] = { 0, }; + size_t len = 0; + const char *str = luaL_checklstring(L, 1, &len); + if (len != sizeof(pmk) * 2) + return luaL_error(L, "PMK must be %d hex digits", 2*ESP_NOW_KEY_LEN); + for (unsigned i = 0; i < sizeof(pmk); ++i) + pmk[i] = (hexval(L, str[i*2]) << 4) + hexval(L, str[i*2 +1]); + + err_check(L, esp_now_set_pmk(pmk)); + return 0; +} + +static int lespnow_setwakewindow(lua_State *L) +{ + int n = luaL_checkint(L, 1); + if (n < 0 || n > 0xffff) + return luaL_error(L, "wake window out of bounds"); + err_check(L, esp_now_set_wake_window(n)); + return 0; +} + +static int lespnow_init(lua_State *L) +{ + espnow_task = task_get_id(espnow_task_fn); + return 0; +} + +LROT_BEGIN(espnow, NULL, 0) + LROT_FUNCENTRY( start, lespnow_start ) + LROT_FUNCENTRY( stop, lespnow_stop ) + LROT_FUNCENTRY( getversion, lespnow_getversion ) + LROT_FUNCENTRY( on, lespnow_on ) // 'receive', 'sent' + LROT_FUNCENTRY( send, lespnow_send ) + LROT_FUNCENTRY( addpeer, lespnow_addpeer ) + LROT_FUNCENTRY( delpeer, lespnow_delpeer ) + LROT_FUNCENTRY( setpmk, lespnow_setpmk ) + LROT_FUNCENTRY( setwakewindow, lespnow_setwakewindow ) +LROT_END(espnow, NULL, 0) + +NODEMCU_MODULE(ESPNOW, "espnow", espnow, lespnow_init); diff --git a/components/modules/struct.c b/components/modules/struct.c index 7a6ebb7c..0cf7414c 100644 --- a/components/modules/struct.c +++ b/components/modules/struct.c @@ -93,12 +93,14 @@ typedef struct Header { } Header; -static int getnum (const char **fmt, int df) { +static int getnum (lua_State *L, const char **fmt, int df) { if (!isdigit((unsigned char)**fmt)) /* no number? */ return df; /* return default value */ else { int a = 0; do { + if (a > (INT_MAX / 10) || a * 10 > (INT_MAX - (**fmt - '0'))) + luaL_error(L, "integral size overflow"); a = a*10 + *((*fmt)++) - '0'; } while (isdigit((unsigned char)**fmt)); return a; @@ -121,9 +123,9 @@ static size_t optsize (lua_State *L, char opt, const char **fmt) { case 'd': return sizeof(double); #endif case 'x': return 1; - case 'c': return getnum(fmt, 1); + case 'c': return getnum(L, fmt, 1); case 'i': case 'I': { - int sz = getnum(fmt, sizeof(int)); + int sz = getnum(L, fmt, sizeof(int)); if (sz > MAXINTSIZE) luaL_error(L, "integral size %d is larger than limit of %d", sz, MAXINTSIZE); @@ -156,7 +158,7 @@ static void controloptions (lua_State *L, int opt, const char **fmt, case '>': h->endian = BIG; return; case '<': h->endian = LITTLE; return; case '!': { - int a = getnum(fmt, MAXALIGN); + int a = getnum(L, fmt, MAXALIGN); if (!isp2(a)) luaL_error(L, "alignment %d is not a power of 2", a); h->align = a; diff --git a/components/modules/ws2812.c b/components/modules/ws2812.c index 76a84523..fd7e9492 100644 --- a/components/modules/ws2812.c +++ b/components/modules/ws2812.c @@ -11,6 +11,14 @@ #define SHIFT_LOGICAL 0 #define SHIFT_CIRCULAR 1 +// The default bit H & L durations in multiples of 100ns. +#define WS2812_DURATION_T0H 4 +#define WS2812_DURATION_T0L 7 +#define WS2812_DURATION_T1H 8 +#define WS2812_DURATION_T1L 6 +// The default reset duration in multiples of 100ns. +#define WS2812_DURATION_RESET 512 + typedef struct { int size; @@ -33,6 +41,7 @@ static void ws2812_cleanup( lua_State *L, int pop ) // ws2812.write({pin = 4, data = string.char(255, 0, 0, 255, 255, 255)}) first LED green, second LED white. static int ws2812_write( lua_State* L ) { + int type; int top = lua_gettop( L ); for (int stack = 1; stack <= top; stack++) { @@ -56,6 +65,106 @@ static int ws2812_write( lua_State* L ) int gpio_num = luaL_checkint( L, -1 ); lua_pop( L, 1 ); + // + // retrieve reset + // This is an optional parameter which defaults to WS2812_DURATION_RESET. + // + int reset = WS2812_DURATION_RESET; + type = lua_getfield( L, stack, "reset" ); + if (type!=LUA_TNIL ) + { + if (!lua_isnumber( L, -1 )) { + ws2812_cleanup( L, 1 ); + return luaL_argerror( L, stack, "invalid reset" ); + } + reset = luaL_checkint( L, -1 ); + if ((reset<0) || (reset>0xfffe)) { + ws2812_cleanup( L, 1 ); + return luaL_argerror( L, stack, "reset must be 0<=reset<=65534" ); + } + } + lua_pop( L, 1 ); + + // + // retrieve t0h + // This is an optional parameter which defaults to WS2812_DURATION_T0H. + // + int t0h = WS2812_DURATION_T0H; + type = lua_getfield( L, stack, "t0h" ); + if (type!=LUA_TNIL ) + { + if (!lua_isnumber( L, -1 )) { + ws2812_cleanup( L, 1 ); + return luaL_argerror( L, stack, "invalid t0h" ); + } + t0h = luaL_checkint( L, -1 ); + if ((t0h<1) || (t0h>0x7fff)) { + ws2812_cleanup( L, 1 ); + return luaL_argerror( L, stack, "t0h must be 1<=t0h<=32767" ); + } + } + lua_pop( L, 1 ); + + // + // retrieve t0l + // This is an optional parameter which defaults to WS2812_DURATION_T0L. + // + int t0l = WS2812_DURATION_T0L; + type = lua_getfield( L, stack, "t0l" ); + if (type!=LUA_TNIL ) + { + if (!lua_isnumber( L, -1 )) { + ws2812_cleanup( L, 1 ); + return luaL_argerror( L, stack, "invalid t0l" ); + } + t0l = luaL_checkint( L, -1 ); + if ((t0l<1) || (t0l>0x7fff)) { + ws2812_cleanup( L, 1 ); + return luaL_argerror( L, stack, "t0l must be 1<=t0l<=32767" ); + } + } + lua_pop( L, 1 ); + + // + // retrieve t1h + // This is an optional parameter which defaults to WS2812_DURATION_T1H. + // + int t1h = WS2812_DURATION_T1H; + type = lua_getfield( L, stack, "t1h" ); + if (type!=LUA_TNIL ) + { + if (!lua_isnumber( L, -1 )) { + ws2812_cleanup( L, 1 ); + return luaL_argerror( L, stack, "invalid t1h" ); + } + t1h = luaL_checkint( L, -1 ); + if ((t1h<1) || (t1h>0x7fff)) { + ws2812_cleanup( L, 1 ); + return luaL_argerror( L, stack, "t1h must be 1<=t1h<=32767" ); + } + } + lua_pop( L, 1 ); + + // + // retrieve t1l + // This is an optional parameter which defaults to WS2812_DURATION_T1L. + // + int t1l = WS2812_DURATION_T1L; + type = lua_getfield( L, stack, "t1l" ); + if (type!=LUA_TNIL ) + { + if (!lua_isnumber( L, -1 )) { + ws2812_cleanup( L, 1 ); + return luaL_argerror( L, stack, "invalid t1l" ); + } + t1l = luaL_checkint( L, -1 ); + if ((t1l<1) || (t1l>0x7fff)) { + ws2812_cleanup( L, 1 ); + return luaL_argerror( L, stack, "t1l must be 1<=t1l<=32767" ); + } + } + lua_pop( L, 1 ); + // // retrieve data // @@ -83,7 +192,7 @@ static int ws2812_write( lua_State* L ) lua_pop( L, 1 ); // prepare channel - if (platform_ws2812_setup( gpio_num, 1, (const uint8_t *)data, length ) != PLATFORM_OK) { + if (platform_ws2812_setup( gpio_num, reset, t0h, t0l, t1h, t1l, (const uint8_t *)data, length ) != PLATFORM_OK) { ws2812_cleanup( L, 0 ); return luaL_argerror( L, stack, "can't set up chain" ); } diff --git a/components/platform/include/platform.h b/components/platform/include/platform.h index 5c0929a4..2706a9de 100644 --- a/components/platform/include/platform.h +++ b/components/platform/include/platform.h @@ -209,7 +209,7 @@ int platform_dht_read( uint8_t gpio_num, uint8_t wakeup_ms, uint8_t *data ); // WS2812 platform interface void platform_ws2812_init( void ); -int platform_ws2812_setup( uint8_t gpio_num, uint8_t num_mem, const uint8_t *data, size_t len ); +int platform_ws2812_setup( uint8_t gpio_num, uint32_t reset, uint32_t bit0h, uint32_t bit0l, uint32_t bit1h, uint32_t bit1l, const uint8_t *data, size_t len ); int platform_ws2812_release( void ); int platform_ws2812_send( void ); diff --git a/components/platform/ws2812.c b/components/platform/ws2812.c index d00510cb..21c14a4c 100644 --- a/components/platform/ws2812.c +++ b/components/platform/ws2812.c @@ -32,112 +32,180 @@ #include "soc/periph_defs.h" #include "rom/gpio.h" // for gpio_matrix_out() #include "soc/gpio_periph.h" +#include "soc/rmt_reg.h" #undef WS2812_DEBUG -// If either of these fails, the reset logic in ws2812_sample_to_rmt will need revisiting. -_Static_assert(SOC_RMT_MEM_WORDS_PER_CHANNEL % 8 == 0, - "SOC_RMT_MEM_WORDS_PER_CHANNEL is assumed to be a multiple of 8"); -_Static_assert(SOC_RMT_MEM_WORDS_PER_CHANNEL >= 16, - "SOC_RMT_MEM_WORDS_PER_CHANNEL is assumed to be >= 16"); - // divider to generate 100ns base period from 80MHz APB clock #define WS2812_CLKDIV (100 * 80 /1000) -// bit H & L durations in multiples of 100ns -#define WS2812_DURATION_T0H 4 -#define WS2812_DURATION_T0L 7 -#define WS2812_DURATION_T1H 8 -#define WS2812_DURATION_T1L 6 -#define WS2812_DURATION_RESET (50000 / 100) - -// 0 bit in rmt encoding -const rmt_item32_t ws2812_rmt_bit0 = { - .level0 = 1, - .duration0 = WS2812_DURATION_T0H, - .level1 = 0, - .duration1 = WS2812_DURATION_T0L -}; -// 1 bit in rmt encoding -const rmt_item32_t ws2812_rmt_bit1 = { - .level0 = 1, - .duration0 = WS2812_DURATION_T1H, - .level1 = 0, - .duration1 = WS2812_DURATION_T1L -}; - -// This is one eighth of 512 * 100ns, ie in total a bit above the requisite 50us -const rmt_item32_t ws2812_rmt_reset = { .level0 = 0, .duration0 = 32, .level1 = 0, .duration1 = 32 }; // descriptor for a ws2812 chain typedef struct { bool valid; bool needs_reset; uint8_t gpio; + rmt_item32_t reset; + rmt_item32_t bits[2]; const uint8_t *data; size_t len; + uint8_t bitpos; } ws2812_chain_t; // chain descriptor array static ws2812_chain_t ws2812_chains[RMT_CHANNEL_MAX]; -#define MIN(a, b) ((a) < (b) ? (a) : (b)) - static void ws2812_sample_to_rmt(const void *src, rmt_item32_t *dest, size_t src_size, size_t wanted_num, size_t *translated_size, size_t *item_num) { - // Note: enabling these commented-out logs will ruin the timing so nothing - // will actually work when they're enabled. But I've kept them in as comments - // because they were useful in debugging the buffer management. - // ESP_DRAM_LOGW("ws2812", "ws2812_sample_to_rmt wanted=%u src_size=%u", wanted_num, src_size); + size_t cnt_in; + size_t cnt_out; + const uint8_t *pucData; + uint8_t ucData; + uint8_t ucBitPos; + esp_err_t tStatus; + void *pvContext; + ws2812_chain_t *ptContext; + uint8_t ucBit; - void *ctx; - rmt_translator_get_context(item_num, &ctx); - ws2812_chain_t *chain = (ws2812_chain_t *)ctx; + cnt_in = 0; + cnt_out = 0; + if( dest!=NULL && wanted_num>0 ) + { + tStatus = rmt_translator_get_context(item_num, &pvContext); + if( tStatus==ESP_OK ) + { + ptContext = (ws2812_chain_t *)pvContext; - size_t reset_num = 0; - if (chain->needs_reset) { - // Haven't sent reset yet + if( ptContext->needs_reset==true ) + { + dest[cnt_out++] = ptContext->reset; + ptContext->needs_reset = false; + } + if( src!=NULL && src_size>0 ) + { + ucBitPos = ptContext->bitpos; - // We split the reset into 8 even though it would fit in a single - // rmt_item32_t, simply so that dest stays 8-item aligned which means we - // don't have to worry about having to split a byte of src across multiple - // blocks (assuming the static asserts at the top of this file are true). - for (int i = 0; i < 8; i++) { - dest[i] = ws2812_rmt_reset; - } - dest += 8; - wanted_num -= 8; - reset_num = 8; - chain->needs_reset = false; - } + /* Each bit of the input data is converted into one RMT item. */ - // Now write the actual data from src - const uint8_t *data = (const uint8_t *)src; - size_t data_num = MIN(wanted_num, src_size * 8) / 8; - for (size_t idx = 0; idx < data_num; idx++) { - uint8_t byte = data[idx]; - for (uint8_t i = 0; i < 8; i++) { - dest[idx * 8 + i] = (byte & 0x80) ? ws2812_rmt_bit1 : ws2812_rmt_bit0; - byte <<= 1; + pucData = (const uint8_t*)src; + /* Get the current byte. */ + ucData = pucData[cnt_in] << ucBitPos; + + while( cnt_in> 7U; + /* Translate the bit to a WS2812 input code. */ + dest[cnt_out++] = ptContext->bits[ucBit]; + /* Move to the next bit. */ + ++ucBitPos; + if( ucBitPos<8U ) + { + ucData <<= 1; + } + else + { + ucBitPos = 0U; + ++cnt_in; + ucData = pucData[cnt_in]; + } + } + + ptContext->bitpos = ucBitPos; + } } } - - *translated_size = data_num; - *item_num = reset_num + data_num * 8; - // ESP_DRAM_LOGW("ws2812", "src bytes consumed: %u total rmt items: %u", *translated_size, *item_num); + *translated_size = cnt_in; + *item_num = cnt_out; } -int platform_ws2812_setup( uint8_t gpio_num, uint8_t num_mem, const uint8_t *data, size_t len ) +int platform_ws2812_setup( uint8_t gpio_num, uint32_t reset, uint32_t t0h, uint32_t t0l, uint32_t t1h, uint32_t t1l, const uint8_t *data, size_t len ) { int channel; - if ((channel = platform_rmt_allocate( num_mem, RMT_MODE_TX )) >= 0) { + if ((channel = platform_rmt_allocate( 1, RMT_MODE_TX )) >= 0) { ws2812_chain_t *chain = &(ws2812_chains[channel]); + rmt_item32_t tRmtItem; + uint32_t half; chain->valid = true; chain->gpio = gpio_num; chain->len = len; chain->data = data; - chain->needs_reset = true; + chain->bitpos = 0; + + // Send a reset if "reset" is not 0. + chain->needs_reset = (reset != 0); + + // Construct the RMT item for a reset. + tRmtItem.level0 = 0; + tRmtItem.level1 = 0; + // The reset duration must fit into one RMT item. This leaves 2*15 bit, + // which results in a maximum of 0xfffe . + if (reset>0xfffe) + { + reset = 0xfffe; + } + if (reset>0x7fff) + { + tRmtItem.duration0 = 0x7fff; + tRmtItem.duration1 = reset - 0x7fff; + } + else + { + half = reset >> 1U; + tRmtItem.duration0 = half; + tRmtItem.duration1 = reset - half; + } + chain->reset = tRmtItem; + + // Limit the bit times to the available 15 bits. + // The values must not be 0. + if( t0h==0 ) + { + t0h = 1; + } + else if( t0h>0x7fffU ) + { + t0h = 0x7fffU; + } + if( t0l==0 ) + { + t0l = 1; + } + else if( t0l>0x7fffU ) + { + t0l = 0x7fffU; + } + if( t1h==0 ) + { + t1h = 1; + } + else if( t1h>0x7fffU ) + { + t1h = 0x7fffU; + } + if( t1l==0 ) + { + t1l = 1; + } + else if( t1l>0x7fffU ) + { + t1l = 0x7fffU; + } + + // Construct the RMT item for a 0 bit. + tRmtItem.level0 = 1; + tRmtItem.duration0 = t0h; + tRmtItem.level1 = 0; + tRmtItem.duration1 = t0l; + chain->bits[0] = tRmtItem; + + // Construct the RMT item for a 1 bit. + tRmtItem.level0 = 1; + tRmtItem.duration0 = t1h; + tRmtItem.level1 = 0; + tRmtItem.duration1 = t1l; + chain->bits[1] = tRmtItem; #ifdef WS2812_DEBUG ESP_LOGI("ws2812", "Setup done for gpio %d on RMT channel %d", gpio_num, channel); @@ -210,6 +278,19 @@ int platform_ws2812_send( void ) } } + // Try to add all channels to a group. This moves the start of all RMT sequences closer + // together. +#if SOC_RMT_SUPPORT_TX_SYNCHRO + for (rmt_channel_t channel = 0; channel < RMT_CHANNEL_MAX && res == PLATFORM_OK; channel++) { + if (ws2812_chains[channel].valid) { + if (rmt_add_channel_to_group( channel ) != ESP_OK) { + res = PLATFORM_ERR; + break; + } + } + } +#endif + // start selected channels one by one for (rmt_channel_t channel = 0; channel < RMT_CHANNEL_MAX && res == PLATFORM_OK; channel++) { if (ws2812_chains[channel].valid) { @@ -229,6 +310,17 @@ int platform_ws2812_send( void ) } } +#if SOC_RMT_SUPPORT_TX_SYNCHRO + for (rmt_channel_t channel = 0; channel < RMT_CHANNEL_MAX; channel++) { + if (ws2812_chains[channel].valid) { + if (rmt_remove_channel_from_group( channel ) != ESP_OK) { + res = PLATFORM_ERR; + break; + } + } + } +#endif + return res; } diff --git a/docs/index.md b/docs/index.md index 5a2072a7..e6e0923d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,10 +1,12 @@ # NodeMCU Documentation -NodeMCU is an open source [Lua](https://www.lua.org/) based firmware for the [ESP32](http://espressif.com/en/products/hardware/esp32/overview) and [ESP8266 WiFi SOC from Espressif](http://espressif.com/en/products/esp8266/) and uses an on-module flash-based [SPIFFS](https://github.com/pellepl/spiffs) file system. NodeMCU is implemented in C and is layered on the [Espressif ESP-IDF](https://github.com/espressif/ESP-IDF). +NodeMCU is an open source [Lua](https://www.lua.org/) based firmware for the [ESP32](https://www.espressif.com/en/products/socs/esp32) and [ESP8266](https://www.espressif.com/en/products/socs/esp8266) WiFi SOCs from Espressif. It uses an on-module flash-based [SPIFFS](https://github.com/pellepl/spiffs) file system. NodeMCU is implemented in C and the ESP32 version is layered on the [Espressif ESP-IDF](https://github.com/espressif/ESP-IDF). The firmware was initially developed as is a companion project to the popular ESP8266-based [NodeMCU development modules](https://github.com/nodemcu/nodemcu-devkit-v1.0), but the project is now community-supported, and the firmware can now be run on _any_ ESP module. -Support for the new [ESP32 WiFi/BlueTooth SOC from Espressif](http://www.espressif.com/en/products/hardware/esp32/overview) is under way. +!!! important + + The NodeMCU [`release`](https://github.com/nodemcu/nodemcu-firmware/tree/release) and [`dev`](https://github.com/nodemcu/nodemcu-firmware/tree/dev) branches target the ESP8266. The [`dev-esp32`](https://github.com/nodemcu/nodemcu-firmware/tree/dev-esp32) branch targets the ESP32. ## Up-To-Date Documentation At the moment the only up-to-date documentation maintained by the current NodeMCU team is in English. It is part of the source code repository (`/docs` subfolder) and kept in sync with the code. diff --git a/docs/js/extra.js b/docs/js/extra.js index 2653f4a7..fd2a2870 100644 --- a/docs/js/extra.js +++ b/docs/js/extra.js @@ -27,7 +27,7 @@ var nodemcu = nodemcu || {}; } function isModulePage() { // if the breadcrumb contains 'Modules »' it must be an API page - return $("ul.wy-breadcrumbs li:contains('Modules »')").size() > 0; + return $("ul.wy-breadcrumbs li:contains('C Modules')").length > 0; } function createTocTableRow(func, intro) { // fragile attempt to auto-create the in-page anchor diff --git a/docs/modules/espnow.md b/docs/modules/espnow.md new file mode 100644 index 00000000..b491e1fb --- /dev/null +++ b/docs/modules/espnow.md @@ -0,0 +1,248 @@ +# ESP-NOW Module +| Since | Origin / Contributor | Maintainer | Source | +| :----- | :-------------------- | :---------- | :------ | +| 2024-03-07 | [DiUS](https://github.com/DiUS) [Jade Mattsson](https://github.com/jmattsson) |[Jade Mattsson](https://github.com/jmattsson) | [espnow.c](../../components/modules/espnow.c)| + +The `espnow` module provides an interface to Espressif's [ESP-NOW functionality](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/network/esp_now.html). To quote their documentation directly: + +"ESP-NOW is a kind of connectionless Wi-Fi communication protocol that is defined by Espressif. In ESP-NOW, application data is encapsulated in a vendor-specific action frame and then transmitted from one Wi-Fi device to another without connection." + +Packets can be sent to either individual peers, the whole list of defined peers, or broadcast to everyone in range. For non-broadcast packets, ESP-NOW provides optional encryption support to prevent eavesdropping. To use encryption, a "Primary Master Key" (PMK) should first be set. When registering a peer, a peer-specific "Local Master Key" (LMK) is then given, which is further encrypted by the PMK. All packets sent to that peer will be encrypted with the resulting key. + +To broadcast packets, a peer with address 'ff:ff:ff:ff:ff:ff' must first be registered. Broadcast packets do not support encryption, and attempting to enable encrypting when registering the broadcast peer will result in an error. + +ESP-NOW uses a WiFi vendor-specific action frame to transmit data, and as such it requires the WiFi stack to have been started before ESP-NOW packets can be sent and received. + + +## espnow.start + +Starts the ESP-NOW stack. While this may be called prior to `wifi.start()`, packet transmission and reception will not be possible until after the WiFi stack has been started. + +#### Syntax +```lua +espnow.start() +``` + +#### Parameters +None. + +#### Returns +`nil` + +An error will be raised if the ESP-NOW stack cannot be started. + + +## espnow.stop + +Stops the ESP-NOW stack. + +#### Syntax +```lua +espnow.stop() +``` + +#### Parameters +None. + +#### Returns +`nil` + +An error will be raised if the ESP-NOW stack cannot be stopped. + + +## espnow.getversion + +Returns the raw version number enum value. Currently, it is `1`. Might be useful for checking version compatibility in the future. + +#### Syntax +```lua +ver = espnow.getversion() +``` + +#### Parameters +None. + +#### Returns +An integer representing the ESP-NOW version. + + +## espnow.setpmk + +Sets the Primary Master Key (PMK). When using security, this should be done prior to adding any peers, as their LMKs will be encrypted by the current PMK. + +#### Syntax +```lua +espnow.setpmk(pmk) +``` +#### Parameters +`pmk` The Primary Master Key, given as a hex-encoding of a 16-byte key (i.e. the `pmk` should consist of 32 hex digits. + +#### Returns +`nil` + +An error will be raised if the PMK cannot be set. + +#### Example +```lua +espnow.setpmk('00112233445566778899aabbccddeeff') +``` + + +## espnow.setwakewindow + +Controls the wake window during which ESP-NOW listens. In most cases this should never need to be changed from the default. Refer to the Espressif documentation for further details. + +#### Syntax +```lua +espnow.setwakewindow(window) +``` + +#### Parameters +`window` An integer between 0 and 65535. + +#### Returns +`nil` + + +## espnow.addpeer + +Registers a peer MAC address. Optionally parameters for the peer may be included, such as encryption key. + +#### Syntax +```lua +espnow.addpeer(mac, options) +``` +#### Parameters +- `mac` The peer mac address, given as a string in `00:11:22:33:44:55` format (colons optional, and may also be replaced by '-' or ' '). +- `options` A table with with following entries: + - `channel` An integer indicating the WiFi channel to be used. The default is `0`, indicating that the current WiFi channel should be used. If non-zero, must match the current WiFi channel. + - `lmk` The LMK for the peer, if encryption is to be used. + - `encrypt` A non-zero integer to indicate encryption should be enabled. When set, makes `lmk` a required field. + +#### Returns +`nil` + +An error will be raised if a peer cannot be added, such as if the peer list if full, or the peer has already been added. + +#### Examples + +Adding a peer without encryption enabled. +```lua +espnow.addpeer('7c:df:a1:c1:4c:71') +``` + +Adding a peer with encryption enabled. Please use randomly generated keys instead of these easily guessable placeholders. +```lua +espnow.setpmk('ffeeddccbbaa99887766554433221100') +espnow.addpeer('7c:df:a1:c1:4c:71', { encrypt = 1, lmk = '00112233445566778899aabbccddeeff' }) +``` + +## espnow.delpeer + +Deletes a previously added peer from the internal peer list. + +#### Syntax +```lua +espnow.delpeer(mac) +``` + +#### Parameters +`mac` The MAC address of the peer to delete. + +#### Returns +`nil` + +Returns an error if the peer cannot be deleted. + + +## espnow.on + +Registers or unregisters callback handlers for the ESP-NOW events. + +There are two events available, `sent` which is issued in response to a packet send request and which reports the status of the send attempt, and 'receive' which is issued when an ESP-NOW packet is successfully received. + +Only a single callback function can be registered for each event. + +The callback function for the `sent` event is invoked with two parameters, the destination MAC address, and a `1`/`nil` to indicate whether the send was believed to be successful or not. + +The callback function for the `receive` event is invoked with a single parameter, a table with the following keys: + +- `src` The sender MAC address +- `dst` The destination MAC address (likely either the local MAC of the receiver, or the broadcast address) +- `rssi` The RSSI value from the packet, indicating signal strength between the two devices +- `data` The actual payload data, as a string. The string may contain binary data. + +#### Syntax +```lua +espnow.on(event, callbackfn) +``` + +#### Parameters +- `event` The event name, one of `sent` or `receive`. +- `callbackfn` The callback function to register, or `nil` to unregister the previously set callback function for the event. + +#### Returns +`nil` + +Raises an error if invalid arguments are given. + +#### Example +Registering callback handlers. +```lua +espnow.on('sent', + function(mac, success) print(mac, success and 'Yay!' or 'Noooo') end) +espnow.on('receive', + function(t) print(t.src, '->', t.dst, '@', t.rssi, ':', t.data) end) +``` + +Unregistering callback handlers. +```lua +espnow.on('sent') -- implicit nil +espnow.on('receive', nil) +``` + + +## espnow.send + +Attempts to send an ESP-NOW packet to one or more peers. + +In general it is strongly recommended to use the encryption functionality, as this ensures not only secrecy but also prevent unintentional interference between different users of ESP-NOW. + +If you do need to use broadcasts or multicasts, you should make sure to have a unique, recognisable marker at the start of the payload to make filtering out unwanted messages easy, both for you and other ESP-NOW users. + +#### Syntax +```lua +espnow.send(peer, data) +``` + +#### Parameters +- `peer` The peer MAC address to send to. Must have previously been added via `espnow.addpeer()`. If `peer` is given as `nil`, the packet is sent to all registered non-broadcast/multicast peers, and the `sent` callback is invoked for each of those peers. +- `data` A string of data to send. May contain binary bytes. Maximum supported length at the time of writing is 250 bytes. + +#### Returns +`nil`, but the `sent` callback is invoked with the status afterwards. + +Raises an error if the peer is not valid, or other fatal errors preventing a send attempt from even being made. The `sent` callback will not be invoked in this case. + +#### Example +Broadcasting a message to every single ESP-NOW device in range. +```lua +bcast='ff:ff:ff:ff:ff:ff' +espnow.addpeer(bcast) +espnow.send(bcast, '[NodeMCU] Hello, world!') +``` + +Sending a directed message to one specific ESP-NOW device. +```lua +peer='7c:df:a1:c1:4c:71' +espnow.addpeer(peer) +espnow.send(peer, 'Hello, you!') +``` + +Sending a message to all registered peers. +```lua +espnow.addpeer('7c:df:a1:c1:4c:71') +espnow.addpeer('7c:df:a1:c1:4c:47') +espnow.addpeer('7c:df:a1:c1:4f:12') +espnow.send(nil, 'Hello, peers!') +``` diff --git a/docs/modules/rtcmem.md b/docs/modules/rtcmem.md index bd4020dd..613feaa0 100644 --- a/docs/modules/rtcmem.md +++ b/docs/modules/rtcmem.md @@ -1,7 +1,7 @@ # RTC User Memory Module | Since | Origin / Contributor | Maintainer | Source | | :----- | :-------------------- | :---------- | :------ | -| 2015-06-25 | [DiUS](https://github.com/DiUS), [Johny Mattsson](https://github.com/jmattsson) | [PJSG](https://github.com/pjsg) | [rtcmem.c](../../app/modules/rtcmem.c)| +| 2015-06-25 | [DiUS](https://github.com/DiUS), [Jade Mattsson](https://github.com/jmattsson) | [PJSG](https://github.com/pjsg) | [rtcmem.c](../../components/modules/rtcmem.c)| The rtcmem module provides basic access to the RTC memory. diff --git a/docs/modules/ws2812.md b/docs/modules/ws2812.md index c408b096..c4ae1c4c 100644 --- a/docs/modules/ws2812.md +++ b/docs/modules/ws2812.md @@ -1,7 +1,7 @@ # WS2812 Module | Since | Origin / Contributor | Maintainer | Source | | :----- | :-------------------- | :---------- | :------ | -| 2015-02-05 | [Till Klocke](https://github.com/dereulenspiegel), [Thomas Soëte](https://github.com/Alkorin) | [Arnim Läuger](https://github.com/devsaurus) | [ws2812.c](../../components/modules/ws2812.c)| +| 2015-02-05 | [Till Klocke](https://github.com/dereulenspiegel), [Thomas Soëte](https://github.com/Alkorin), [Christoph Thelen](https://github.com/docbacardi) | [Arnim Läuger](https://github.com/devsaurus) | [ws2812.c](../../components/modules/ws2812.c)| ws2812 is a library to handle ws2812-like led strips. It works at least on WS2812, WS2812b, APA104, SK6812 (RGB or RGBW). @@ -22,6 +22,14 @@ Variable number of tables, each describing a single strip. Required elements are - `pin` IO index, see [GPIO Overview](gpio.md#gpio-overview) - `data` payload to be sent to one or more WS2812 like leds through GPIO2 +Optional elements are: + +- `reset` duration of the reset signal in multiples of 100ns. A duration of 0 generates no reset. The minimum possible value is 0. The maximum is 65534. The default value is 512 which generates a reset of 51.2us. +- `t0h` duration of the high period for a 0 code in multiples of 100ns. The minimum possible value is 1. The maximum is 32767. The default value is 4 which results in a high period of 400ns for each 0 code. +- `t0l` duration of the low period for a 0 code in multiples of 100ns. The minimum possible value is 1. The maximum is 32767. The default value is 7 which results in a low period of 700ns for each 0 code. +- `t1h` duration of the high period for a 1 code in multiples of 100ns. The minimum possible value is 1. The maximum is 32767. The default value is 8 which results in a high period of 800ns for each 1 code. +- `t1l` duration of the low period for a 1 code in multiples of 100ns. The minimum possible value is 1. The maximum is 32767. The default value is 6 which results in a low period of 600ns for each 1 code. + Payload type could be: - `string` representing bytes to send @@ -44,6 +52,10 @@ ws2812.write({pin = 4, data = string.char(255, 0, 0, 255, 0, 0)}, {pin = 14, data = string.char(0, 255, 0, 0, 255, 0)}) -- turn the two first RGB leds to green on the first strip and red on the second strip ``` +```lua +ws2812.write({pin = 8, reset = 800, t0h = 3, t0l = 9, t1h = 6, t1l = 6, data = string.char(1, 0, 0)}) -- turn the SK6812 GRB led on the ESP32-C3-DevKitM-1 to green +``` + # Buffer module For more advanced animations, it is useful to keep a "framebuffer" of the strip, interact with it and flush it to the strip. diff --git a/docs/requirements.txt b/docs/requirements.txt index ba6e4ea8..f26ede80 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,2 +1,2 @@ -mkdocs==1.2.2 -jinja2<3.1 +mkdocs>=1.5.3 +jinja2>=3.1.0 diff --git a/mkdocs.yml b/mkdocs.yml index ce5dfd87..8682c4c9 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -20,7 +20,7 @@ extra_css: extra_javascript: - js/extra.js -pages: +nav: - Overview: 'index.md' - Basics: - Building the firmware: 'build.md' @@ -48,6 +48,7 @@ pages: - 'dht': 'modules/dht.md' - 'encoder': 'modules/encoder.md' - 'eromfs': 'modules/eromfs.md' + - 'espnow': 'modules/espnow.md' - 'eth': 'modules/eth.md' - 'file': 'modules/file.md' - 'gpio': 'modules/gpio.md' @@ -66,6 +67,7 @@ pages: - 'pulsecnt': 'modules/pulsecnt.md' - 'qrcodegen': 'modules/qrcodegen.md' - 'rmt': 'modules/rmt.md' + - 'rtcmem': 'modules/rtcmem.md' - 'sdmmc': 'modules/sdmmc.md' - 'sigma delta': 'modules/sigma-delta.md' - 'sjson': 'modules/sjson.md'