From 625f2207dd1d02b934e5dc992a8b5d1eee0794f1 Mon Sep 17 00:00:00 2001 From: Matthew Hiles Date: Wed, 6 May 2026 13:48:48 -0400 Subject: [PATCH 1/2] Add ESP-NOW wrappers --- error.go | 23 +++++ espnow.c | 51 ++++++++++ espnow.go | 279 +++++++++++++++++++++++++++++++++++++++++++++++++++++ espradio.h | 9 ++ 4 files changed, 362 insertions(+) create mode 100644 espnow.c create mode 100644 espnow.go diff --git a/error.go b/error.go index dbb1380..f254ba7 100644 --- a/error.go +++ b/error.go @@ -21,6 +21,29 @@ func (e Error) Error() string { return "espradio: unknown flash error" case e >= C.ESP_ERR_MESH_BASE: return "espradio: unknown mesh error" + case e >= C.ESP_ERR_ESPNOW_BASE: + switch e { + case C.ESP_ERR_ESPNOW_NOT_INIT: + return "espradio: esp-now not initialized" + case C.ESP_ERR_ESPNOW_ARG: + return "espradio: esp-now invalid argument" + case C.ESP_ERR_ESPNOW_NO_MEM: + return "espradio: esp-now out of memory" + case C.ESP_ERR_ESPNOW_FULL: + return "espradio: esp-now peer list full" + case C.ESP_ERR_ESPNOW_NOT_FOUND: + return "espradio: esp-now peer not found" + case C.ESP_ERR_ESPNOW_INTERNAL: + return "espradio: esp-now internal error" + case C.ESP_ERR_ESPNOW_EXIST: + return "espradio: esp-now peer already exists" + case C.ESP_ERR_ESPNOW_IF: + return "espradio: esp-now interface mismatch" + case C.ESP_ERR_ESPNOW_CHAN: + return "espradio: esp-now channel mismatch" + default: + return "espradio: esp-now error " + strconv.FormatInt(int64(int32(e)), 10) + } case e >= C.ESP_ERR_WIFI_BASE: code := int32(e) switch code { diff --git a/espnow.c b/espnow.c new file mode 100644 index 0000000..4d1abf7 --- /dev/null +++ b/espnow.c @@ -0,0 +1,51 @@ +#include "espradio.h" + +static void espradio_esp_now_recv_cb(const esp_now_recv_info_t *info, const uint8_t *data, int data_len) { + const uint8_t *src_addr = NULL; + const uint8_t *dest_addr = NULL; + int rssi = 0; + uint8_t channel = 0; + uint8_t secondary_channel = 0; + int noise_floor = 0; + uint32_t timestamp = 0; + + if (info != NULL) { + src_addr = info->src_addr; + dest_addr = info->des_addr; + if (info->rx_ctrl != NULL) { + rssi = info->rx_ctrl->rssi; + channel = info->rx_ctrl->channel; + secondary_channel = info->rx_ctrl->secondary_channel; + noise_floor = info->rx_ctrl->noise_floor; + timestamp = info->rx_ctrl->timestamp; + } + } + + espradio_on_esp_now_recv(src_addr, dest_addr, rssi, channel, secondary_channel, noise_floor, timestamp, data, data_len); +} + +static void espradio_esp_now_send_cb(const esp_now_send_info_t *tx_info, esp_now_send_status_t status) { + const uint8_t *dest_addr = NULL; + const uint8_t *src_addr = NULL; + wifi_interface_t ifidx = WIFI_IF_STA; + wifi_phy_rate_t rate = 0; + wifi_tx_status_t tx_status = WIFI_SEND_FAIL; + + if (tx_info != NULL) { + dest_addr = tx_info->des_addr; + src_addr = tx_info->src_addr; + ifidx = tx_info->ifidx; + rate = tx_info->rate; + tx_status = tx_info->tx_status; + } + + espradio_on_esp_now_send(dest_addr, src_addr, ifidx, rate, tx_status, status); +} + +esp_err_t espradio_esp_now_register_recv_cb(void) { + return esp_now_register_recv_cb(espradio_esp_now_recv_cb); +} + +esp_err_t espradio_esp_now_register_send_cb(void) { + return esp_now_register_send_cb(espradio_esp_now_send_cb); +} diff --git a/espnow.go b/espnow.go new file mode 100644 index 0000000..8836c4b --- /dev/null +++ b/espnow.go @@ -0,0 +1,279 @@ +//go:build esp32c3 || esp32c3_qemu_target || esp32s3 + +package espradio + +/* +#cgo CFLAGS: -Iblobs/include +#cgo CFLAGS: -Iblobs/include/local +#cgo CFLAGS: -Iblobs/headers +#cgo CFLAGS: -DCONFIG_SOC_WIFI_NAN_SUPPORT=0 +#cgo CFLAGS: -DESPRADIO_PHY_PATCH_ROMFUNCS=0 +#cgo CFLAGS: -fno-short-enums + +#include "espradio.h" +*/ +import "C" + +import ( + "sync" + "unsafe" +) + +const ( + ESPNowAddressLength = C.ESP_NOW_ETH_ALEN + ESPNowKeyLength = C.ESP_NOW_KEY_LEN + ESPNowMaxDataLength = C.ESP_NOW_MAX_DATA_LEN +) + +type WiFiInterface uint8 + +const ( + WiFiInterfaceSTA WiFiInterface = C.WIFI_IF_STA + WiFiInterfaceAP WiFiInterface = C.WIFI_IF_AP +) + +type ESPNowSendStatus uint8 + +const ( + ESPNowSendSuccess ESPNowSendStatus = C.ESP_NOW_SEND_SUCCESS + ESPNowSendFail ESPNowSendStatus = C.ESP_NOW_SEND_FAIL +) + +type ESPNowPeer struct { + Address [ESPNowAddressLength]byte + Key [ESPNowKeyLength]byte + Channel uint8 + If WiFiInterface + Encrypt bool +} + +type ESPNowPeerCount struct { + Total int + Encrypted int +} + +type ESPNowReceive struct { + SourceAddress [ESPNowAddressLength]byte + DestinationAddress [ESPNowAddressLength]byte + RSSI int8 + Channel uint8 + SecondaryChannel uint8 + NoiseFloor int8 + Timestamp uint32 + Data []byte +} + +type ESPNowSendReport struct { + DestinationAddress [ESPNowAddressLength]byte + SourceAddress [ESPNowAddressLength]byte + If WiFiInterface + Rate uint32 + TxStatus ESPNowSendStatus + Status ESPNowSendStatus +} + +var ( + espNowMu sync.RWMutex + espNowRecvHandler func(ESPNowReceive) + espNowSendHandler func(ESPNowSendReport) +) + +// ESPNowInit initializes the ESP-NOW subsystem and registers callback trampolines. +func ESPNowInit() error { + if code := C.esp_now_init(); code != C.ESP_OK { + return makeError(code) + } + if code := C.espradio_esp_now_register_recv_cb(); code != C.ESP_OK { + _ = C.esp_now_deinit() + return makeError(code) + } + if code := C.espradio_esp_now_register_send_cb(); code != C.ESP_OK { + _ = C.esp_now_unregister_recv_cb() + _ = C.esp_now_deinit() + return makeError(code) + } + return nil +} + +// ESPNowDeinit deinitializes ESP-NOW and unregisters any callback trampolines. +func ESPNowDeinit() error { + if code := C.esp_now_unregister_recv_cb(); code != C.ESP_OK { + return makeError(code) + } + if code := C.esp_now_unregister_send_cb(); code != C.ESP_OK { + return makeError(code) + } + if code := C.esp_now_deinit(); code != C.ESP_OK { + return makeError(code) + } + return nil +} + +// ESPNowVersion returns the underlying ESP-NOW version reported by the SDK. +func ESPNowVersion() (uint32, error) { + var version C.uint32_t + if code := C.esp_now_get_version(&version); code != C.ESP_OK { + return 0, makeError(code) + } + return uint32(version), nil +} + +// ESPNowSetPrimaryMasterKey configures the 16-byte PMK used to encrypt LMKs. +func ESPNowSetPrimaryMasterKey(key [ESPNowKeyLength]byte) error { + return makeError(C.esp_now_set_pmk((*C.uint8_t)(unsafe.Pointer(&key[0])))) +} + +// ESPNowSetReceiveHandler installs the Go callback for incoming ESP-NOW packets. +func ESPNowSetReceiveHandler(handler func(ESPNowReceive)) { + espNowMu.Lock() + espNowRecvHandler = handler + espNowMu.Unlock() +} + +// ESPNowSetSendHandler installs the Go callback for ESP-NOW send completion reports. +func ESPNowSetSendHandler(handler func(ESPNowSendReport)) { + espNowMu.Lock() + espNowSendHandler = handler + espNowMu.Unlock() +} + +// ESPNowSend sends a packet to one peer, or to all peers when peer is nil. +func ESPNowSend(peer *[ESPNowAddressLength]byte, data []byte) error { + var peerPtr *C.uint8_t + if peer != nil { + peerPtr = (*C.uint8_t)(unsafe.Pointer(&peer[0])) + } + var dataPtr *C.uint8_t + if len(data) > 0 { + dataPtr = (*C.uint8_t)(unsafe.Pointer(&data[0])) + } + return makeError(C.esp_now_send(peerPtr, dataPtr, C.size_t(len(data)))) +} + +// ESPNowAddPeer adds a peer to the SDK-maintained peer table. +func ESPNowAddPeer(peer ESPNowPeer) error { + cpeer := cESPNowPeer(peer) + return makeError(C.esp_now_add_peer(&cpeer)) +} + +// ESPNowDeletePeer removes a peer from the SDK-maintained peer table. +func ESPNowDeletePeer(addr [ESPNowAddressLength]byte) error { + return makeError(C.esp_now_del_peer((*C.uint8_t)(unsafe.Pointer(&addr[0])))) +} + +// ESPNowModifyPeer updates an existing peer record. +func ESPNowModifyPeer(peer ESPNowPeer) error { + cpeer := cESPNowPeer(peer) + return makeError(C.esp_now_mod_peer(&cpeer)) +} + +// ESPNowGetPeer looks up a peer by MAC address. +func ESPNowGetPeer(addr [ESPNowAddressLength]byte) (ESPNowPeer, error) { + var cpeer C.esp_now_peer_info_t + if code := C.esp_now_get_peer((*C.uint8_t)(unsafe.Pointer(&addr[0])), &cpeer); code != C.ESP_OK { + return ESPNowPeer{}, makeError(code) + } + return goESPNowPeer(cpeer), nil +} + +// ESPNowFetchPeer fetches the next peer from the peer table. +func ESPNowFetchPeer(fromHead bool) (ESPNowPeer, error) { + var cpeer C.esp_now_peer_info_t + if code := C.esp_now_fetch_peer(C.bool(fromHead), &cpeer); code != C.ESP_OK { + return ESPNowPeer{}, makeError(code) + } + return goESPNowPeer(cpeer), nil +} + +// ESPNowPeerExists reports whether a peer exists in the peer table. +func ESPNowPeerExists(addr [ESPNowAddressLength]byte) bool { + return bool(C.esp_now_is_peer_exist((*C.uint8_t)(unsafe.Pointer(&addr[0])))) +} + +// ESPNowGetPeerCount returns the total and encrypted peer counts. +func ESPNowGetPeerCount() (ESPNowPeerCount, error) { + var counts C.esp_now_peer_num_t + if code := C.esp_now_get_peer_num(&counts); code != C.ESP_OK { + return ESPNowPeerCount{}, makeError(code) + } + return ESPNowPeerCount{ + Total: int(counts.total_num), + Encrypted: int(counts.encrypt_num), + }, nil +} + +func cESPNowPeer(peer ESPNowPeer) C.esp_now_peer_info_t { + var cpeer C.esp_now_peer_info_t + copy(cArrayToBytes((*C.uint8_t)(unsafe.Pointer(&cpeer.peer_addr[0])), ESPNowAddressLength), peer.Address[:]) + copy(cArrayToBytes((*C.uint8_t)(unsafe.Pointer(&cpeer.lmk[0])), ESPNowKeyLength), peer.Key[:]) + cpeer.channel = C.uint8_t(peer.Channel) + cpeer.ifidx = C.wifi_interface_t(peer.If) + cpeer.encrypt = C.bool(peer.Encrypt) + cpeer.priv = nil + return cpeer +} + +func goESPNowPeer(peer C.esp_now_peer_info_t) ESPNowPeer { + var out ESPNowPeer + copy(out.Address[:], cArrayToBytes((*C.uint8_t)(unsafe.Pointer(&peer.peer_addr[0])), ESPNowAddressLength)) + copy(out.Key[:], cArrayToBytes((*C.uint8_t)(unsafe.Pointer(&peer.lmk[0])), ESPNowKeyLength)) + out.Channel = uint8(peer.channel) + out.If = WiFiInterface(peer.ifidx) + out.Encrypt = bool(peer.encrypt) + return out +} + +func cArrayToBytes(ptr *C.uint8_t, n int) []byte { + return unsafe.Slice((*byte)(unsafe.Pointer(ptr)), n) +} + +func copyMAC(ptr *C.uint8_t) [ESPNowAddressLength]byte { + var out [ESPNowAddressLength]byte + if ptr != nil { + copy(out[:], cArrayToBytes(ptr, ESPNowAddressLength)) + } + return out +} + +//export espradio_on_esp_now_recv +func espradio_on_esp_now_recv(srcAddr, destAddr *C.uint8_t, rssi C.int, channel, secondaryChannel C.uint8_t, noiseFloor C.int, timestamp C.uint32_t, data *C.uint8_t, dataLen C.int) { + espNowMu.RLock() + handler := espNowRecvHandler + espNowMu.RUnlock() + if handler == nil { + return + } + + event := ESPNowReceive{ + SourceAddress: copyMAC(srcAddr), + DestinationAddress: copyMAC(destAddr), + RSSI: int8(rssi), + Channel: uint8(channel), + SecondaryChannel: uint8(secondaryChannel), + NoiseFloor: int8(noiseFloor), + Timestamp: uint32(timestamp), + } + if data != nil && dataLen > 0 { + event.Data = C.GoBytes(unsafe.Pointer(data), dataLen) + } + handler(event) +} + +//export espradio_on_esp_now_send +func espradio_on_esp_now_send(destAddr, srcAddr *C.uint8_t, ifidx C.wifi_interface_t, rate C.wifi_phy_rate_t, txStatus C.wifi_tx_status_t, status C.esp_now_send_status_t) { + espNowMu.RLock() + handler := espNowSendHandler + espNowMu.RUnlock() + if handler == nil { + return + } + + handler(ESPNowSendReport{ + DestinationAddress: copyMAC(destAddr), + SourceAddress: copyMAC(srcAddr), + If: WiFiInterface(ifidx), + Rate: uint32(rate), + TxStatus: ESPNowSendStatus(txStatus), + Status: ESPNowSendStatus(status), + }) +} diff --git a/espradio.h b/espradio.h index 265f6bc..1287af0 100644 --- a/espradio.h +++ b/espradio.h @@ -47,6 +47,8 @@ uint32_t espradio_sniff_count(void); esp_err_t espradio_ap_set_config(const char *ssid, int ssid_len, const char *pwd, int pwd_len, uint8_t channel, int auth_open); +esp_err_t espradio_esp_now_register_recv_cb(void); +esp_err_t espradio_esp_now_register_send_cb(void); extern esp_err_t esp_wifi_connect_internal(void); /* ===== netif (netif.c) ===== */ @@ -77,6 +79,13 @@ extern void espradio_hal_wifi_rtc_disable_iso_go(void); extern void espradio_hal_reset_wifi_mac_go(void); extern int espradio_hal_read_mac_go(unsigned char *mac, unsigned int iftype); extern void espradio_on_wifi_event(int32_t eventID, void *data); +extern void espradio_on_esp_now_recv(const uint8_t *src_addr, const uint8_t *dest_addr, + int rssi, uint8_t channel, uint8_t secondary_channel, + int noise_floor, uint32_t timestamp, + const uint8_t *data, int data_len); +extern void espradio_on_esp_now_send(const uint8_t *dest_addr, const uint8_t *src_addr, + wifi_interface_t ifidx, wifi_phy_rate_t rate, + wifi_tx_status_t tx_status, esp_now_send_status_t status); /* ===== chip-specific → linker (implemented in esp32c3/ or esp32s3/ *.c) ===== */ extern void esp_phy_enable(esp_phy_modem_t modem); From 8c712f6013ee38b50528ceb8bc2786239d96e556 Mon Sep 17 00:00:00 2001 From: Matthew Hiles Date: Wed, 6 May 2026 13:59:38 -0400 Subject: [PATCH 2/2] address compile error --- Makefile | 8 +++++--- espnow.c | 10 ++++++++++ espnow.go | 4 ++-- espradio.h | 2 ++ 4 files changed, 19 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index f90e2a0..848a901 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,10 @@ +CGO_CFLAGS_ALLOW_PATTERN = -fno-short-enums + fmt-check: test -z "$(shell gofmt -l .)" unit-test: - tinygo test -target=esp32c3-qemu.json ./... + CGO_CFLAGS_ALLOW='$(CGO_CFLAGS_ALLOW_PATTERN)' tinygo test -target=esp32c3-qemu.json ./... update: update-esp-wifi rm -rf blobs/headers @@ -22,8 +24,8 @@ smoke-test: rm -rf build/* @for example in ./examples/*/; do \ for target in xiao-esp32c3 xiao-esp32s3; do \ - echo "tinygo build -target=$$target -size short -o build/$$(basename $$example) $$example"; \ - tinygo build -target=$$target -size short -o build/$$(basename $$example) $$example || exit 1; \ + echo "CGO_CFLAGS_ALLOW='$(CGO_CFLAGS_ALLOW_PATTERN)' tinygo build -target=$$target -size short -o build/$$(basename $$example) $$example"; \ + CGO_CFLAGS_ALLOW='$(CGO_CFLAGS_ALLOW_PATTERN)' tinygo build -target=$$target -size short -o build/$$(basename $$example) $$example || exit 1; \ done; \ done diff --git a/espnow.c b/espnow.c index 4d1abf7..b4d6610 100644 --- a/espnow.c +++ b/espnow.c @@ -49,3 +49,13 @@ esp_err_t espradio_esp_now_register_recv_cb(void) { esp_err_t espradio_esp_now_register_send_cb(void) { return esp_now_register_send_cb(espradio_esp_now_send_cb); } + +esp_err_t espradio_esp_now_fetch_peer(int from_head, esp_now_peer_info_t *peer) { + return esp_now_fetch_peer(from_head != 0, peer); +} + +void espradio_esp_now_peer_set_encrypt(esp_now_peer_info_t *peer, int encrypt) { + if (peer != NULL) { + peer->encrypt = encrypt != 0; + } +} diff --git a/espnow.go b/espnow.go index 8836c4b..c0f0f08 100644 --- a/espnow.go +++ b/espnow.go @@ -179,7 +179,7 @@ func ESPNowGetPeer(addr [ESPNowAddressLength]byte) (ESPNowPeer, error) { // ESPNowFetchPeer fetches the next peer from the peer table. func ESPNowFetchPeer(fromHead bool) (ESPNowPeer, error) { var cpeer C.esp_now_peer_info_t - if code := C.esp_now_fetch_peer(C.bool(fromHead), &cpeer); code != C.ESP_OK { + if code := C.espradio_esp_now_fetch_peer(C.int(boolToInt(fromHead)), &cpeer); code != C.ESP_OK { return ESPNowPeer{}, makeError(code) } return goESPNowPeer(cpeer), nil @@ -208,7 +208,7 @@ func cESPNowPeer(peer ESPNowPeer) C.esp_now_peer_info_t { copy(cArrayToBytes((*C.uint8_t)(unsafe.Pointer(&cpeer.lmk[0])), ESPNowKeyLength), peer.Key[:]) cpeer.channel = C.uint8_t(peer.Channel) cpeer.ifidx = C.wifi_interface_t(peer.If) - cpeer.encrypt = C.bool(peer.Encrypt) + C.espradio_esp_now_peer_set_encrypt(&cpeer, C.int(boolToInt(peer.Encrypt))) cpeer.priv = nil return cpeer } diff --git a/espradio.h b/espradio.h index 1287af0..03efc27 100644 --- a/espradio.h +++ b/espradio.h @@ -49,6 +49,8 @@ esp_err_t espradio_ap_set_config(const char *ssid, int ssid_len, uint8_t channel, int auth_open); esp_err_t espradio_esp_now_register_recv_cb(void); esp_err_t espradio_esp_now_register_send_cb(void); +esp_err_t espradio_esp_now_fetch_peer(int from_head, esp_now_peer_info_t *peer); +void espradio_esp_now_peer_set_encrypt(esp_now_peer_info_t *peer, int encrypt); extern esp_err_t esp_wifi_connect_internal(void); /* ===== netif (netif.c) ===== */