391 lines
10 KiB
C
391 lines
10 KiB
C
|
/*
|
||
|
* 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);
|