Added espnow module with documentation.

This commit is contained in:
Jade Mattsson 2024-03-07 17:50:31 +11:00 committed by Jade Mattsson
parent bd6b70ee61
commit 830522ac33
5 changed files with 645 additions and 0 deletions

View File

@ -8,6 +8,7 @@ set(module_srcs
"dht.c"
"encoder.c"
"eromfs.c"
"espnow.c"
"file.c"
"gpio.c"
"heaptrace.c"

View File

@ -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

390
components/modules/espnow.c Normal file
View File

@ -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 <jmattsson@dius.com.au>
*/
#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);

247
docs/modules/espnow.md Normal file
View File

@ -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!')
```

View File

@ -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'