From 10e378394861b8261eac4366658dad069aa12f63 Mon Sep 17 00:00:00 2001 From: Yiheng Cao <65160922+Crispy-fried-chicken@users.noreply.github.com> Date: Tue, 6 Feb 2024 08:27:00 +0800 Subject: [PATCH 1/7] Fix potential integer overflow in getnum and fix the negation overflow in lua (#3634) --- components/lua/lua-5.3/ldebug.c | 7 ++++--- components/modules/struct.c | 10 ++++++---- 2 files changed, 10 insertions(+), 7 deletions(-) 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/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; From 14cdff107fd812d10b2496990ded1b1e572319f0 Mon Sep 17 00:00:00 2001 From: Christoph Thelen Date: Wed, 7 Feb 2024 23:56:17 +0100 Subject: [PATCH 2/7] Update rmt translator (#3629) * Choose the number of RMT buffers in the ws2812 module. The number of buffers required for optimal operation should be selected by the ws2812 module, not the caller. * Add parameters for RGB LED bit times. This patch adds compatibility for different RGB LEDS besides the WS2812. ESP evaluation boards like the ESP32-C3-DevKitM-1 use an SK68XXMINI-HS RGB LED which does not respond to the timings of this module. The patch adds optional parameters for the bit timings to the write function. If the new parameters are not supplied, the old values are used. An example for the SK68XXMINI-HS is provided in the documentation. * Remove restrictions from RTM translator. The old RMT translator was not able to split the bits of the source data into the size requested by the RMT transmitter. Either all 8 bits of an input byte were translated or none. The new routine removes the restriction by delivering exactly the requested amount of data to the transmitter, which results in a more balanced buffering of translated data under load. * Add a parameter for the RGB LED reset time. This patch introduces a new optional parameter for the reset time in the RGB LED communication. The default is 51.2 microseconds. A value of 0 sends no reset signal, which allows a small optimisation for consecutive write commands. Please note that the reset time of the old code should be 50 microseconds, as the define WS2812_DURATION_RESET suggested. Due to the restrictions of the old RMT translator routine, it was slightly increased to 51.2 microseconds. This patch keeps the value of 51.2 microseconds to be as compatible as possible. * Minimize the time drift between RMT channels. Place all RMT channels in a group to minimize the time drift between the signals. Please note that this feature is not available on all platforms. * Fix the description of the SK6812 LED in the example code. The SK6812 expects the data for the green LED first, then red and finally blue. It should be described as a GRB LED. --- components/modules/ws2812.c | 111 +++++++++++- components/platform/include/platform.h | 2 +- components/platform/ws2812.c | 230 +++++++++++++++++-------- docs/modules/ws2812.md | 14 +- 4 files changed, 285 insertions(+), 72 deletions(-) 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/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. From 4bc22d0c3af648022f6599b03bf05c3eef97d0d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20St=C3=B6r?= Date: Sun, 18 Feb 2024 22:14:27 +0100 Subject: [PATCH 3/7] Bump MkDocs to latest --- docs/js/extra.js | 2 +- docs/requirements.txt | 4 ++-- mkdocs.yml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) 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/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..f2be2f15 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' From 10d0a26f87d137c51eb6821d2452476fb7a623b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20St=C3=B6r?= Date: Sun, 18 Feb 2024 22:14:50 +0100 Subject: [PATCH 4/7] Add note about NodeMCU branches --- docs/index.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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. From bd6b70ee61158708aa266a74c7c38d791064cc66 Mon Sep 17 00:00:00 2001 From: Jade Mattsson Date: Wed, 27 Mar 2024 13:51:31 +1100 Subject: [PATCH 5/7] Add missing doc link to rtcmem module. As pointed out by Marcel, thank you! --- docs/modules/rtcmem.md | 2 +- mkdocs.yml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) 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/mkdocs.yml b/mkdocs.yml index f2be2f15..801e3847 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -66,6 +66,7 @@ nav: - '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' From 830522ac334e6fc9d9fb1952ad06879c9eb7097b Mon Sep 17 00:00:00 2001 From: Jade Mattsson Date: Thu, 7 Mar 2024 17:50:31 +1100 Subject: [PATCH 6/7] Added espnow module with documentation. --- components/modules/CMakeLists.txt | 1 + components/modules/Kconfig | 6 + components/modules/espnow.c | 390 ++++++++++++++++++++++++++++++ docs/modules/espnow.md | 247 +++++++++++++++++++ mkdocs.yml | 1 + 5 files changed, 645 insertions(+) create mode 100644 components/modules/espnow.c create mode 100644 docs/modules/espnow.md diff --git a/components/modules/CMakeLists.txt b/components/modules/CMakeLists.txt index c5e77a2a..fa765429 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 7741e37b..95e16d28 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/docs/modules/espnow.md b/docs/modules/espnow.md new file mode 100644 index 00000000..c572f9b8 --- /dev/null +++ b/docs/modules/espnow.md @@ -0,0 +1,247 @@ +# ESP-NOW Module +| Since | Origin / Contributor | Maintainer | Source | +| :----- | :-------------------- | :---------- | :------ | +| 2024-03-07 | [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/mkdocs.yml b/mkdocs.yml index 801e3847..8682c4c9 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -48,6 +48,7 @@ nav: - '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' From bc3b31fee5b92acbd63589e26d811f3f9bb9687f Mon Sep 17 00:00:00 2001 From: Jade Mattsson Date: Fri, 8 Mar 2024 16:08:17 +1100 Subject: [PATCH 7/7] Updated docs after review. --- docs/modules/espnow.md | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/docs/modules/espnow.md b/docs/modules/espnow.md index c572f9b8..b491e1fb 100644 --- a/docs/modules/espnow.md +++ b/docs/modules/espnow.md @@ -1,7 +1,7 @@ # ESP-NOW Module | Since | Origin / Contributor | Maintainer | Source | | :----- | :-------------------- | :---------- | :------ | -| 2024-03-07 | [Jade Mattsson](https://github.com/jmattsson) |[Jade Mattsson](https://github.com/jmattsson) | [espnow.c](../../components/modules/espnow.c)| +| 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: @@ -113,8 +113,8 @@ Registers a peer MAC address. Optionally parameters for the peer may be included 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: +- `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. @@ -166,10 +166,11 @@ 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. + +- `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 @@ -177,8 +178,8 @@ 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. +- `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` @@ -215,8 +216,8 @@ 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. +- `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.