From 625f2207dd1d02b934e5dc992a8b5d1eee0794f1 Mon Sep 17 00:00:00 2001 From: Matthew Hiles Date: Wed, 6 May 2026 13:48:48 -0400 Subject: [PATCH 1/5] 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/5] 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) ===== */ From e9140b1c3607dc54080a5401c3b8ce22251dc77e Mon Sep 17 00:00:00 2001 From: Matthew Hiles Date: Wed, 6 May 2026 14:57:47 -0400 Subject: [PATCH 3/5] add go-idiomatic io.PacketConn interface for ESPNow + a streaming adapter --- doc.go | 70 +++++- espnow.go | 32 ++- espnow_packetconn.go | 580 +++++++++++++++++++++++++++++++++++++++++++ espnow_stream.go | 103 ++++++++ 4 files changed, 770 insertions(+), 15 deletions(-) create mode 100644 espnow_packetconn.go create mode 100644 espnow_stream.go diff --git a/doc.go b/doc.go index c6c2cf1..3125806 100644 --- a/doc.go +++ b/doc.go @@ -1,4 +1,68 @@ -// Package espradio provides support for the ESP32-S2 and ESP32-S3 microcontrollers. -// It is based on the ESP-IDF framework and provides access to Wi-Fi and Bluetooth. -// The package is designed to be used with the TinyGo compiler. +// Package espradio provides wireless support for Espressif microcontrollers +// when used with the TinyGo compiler. +// +// The package wraps the Espressif Wi-Fi blobs and exposes both lower-level +// 1:1 bindings and higher-level Go APIs for common use cases such as: +// +// - station and soft-AP Wi-Fi +// - scanning +// - raw Ethernet/netdev integration +// - ESP-NOW datagram communication +// +// ESP-NOW can be used directly through the thin wrapper functions, or through +// the higher-level managed API returned by NewESPNow. The managed API maps each +// remote peer to a Peer that implements net.PacketConn. +// +// For applications that prefer a stream-like API over packet-oriented reads and +// writes, Peer can also be wrapped with PeerStream via peer.Stream(). +// +// Example usage: +// +// func main() { +// if err := espradio.Enable(espradio.Config{}); err != nil { +// panic(err) +// } +// if err := espradio.Start(); err != nil { +// panic(err) +// } +// +// now, err := espradio.NewESPNow(espradio.ESPNowConfig{}) +// if err != nil { +// panic(err) +// } +// defer now.Close() +// +// peer, err := now.AddPeer(espradio.PeerConfig{ +// Address: espradio.ESPNowAddr{0x24, 0x6f, 0x28, 0xaa, 0xbb, 0xcc}, +// If: espradio.WiFiInterfaceSTA, +// }) +// if err != nil { +// panic(err) +// } +// defer peer.Close() +// +// if _, err := peer.WriteTo([]byte("hello"), nil); err != nil { +// panic(err) +// } +// +// buf := make([]byte, espradio.ESPNowMaxDataLength) +// n, addr, err := peer.ReadFrom(buf) +// if err != nil { +// panic(err) +// } +// println("received", n, "bytes from", addr.String()) +// +// stream := peer.Stream() +// if _, err := stream.Write([]byte("streamed payload")); err != nil { +// panic(err) +// } +// +// broadcast, err := now.Broadcast() +// if err != nil { +// panic(err) +// } +// if _, err := broadcast.WriteTo([]byte("announcement"), nil); err != nil { +// panic(err) +// } +// } package espradio // import "tinygo.org/x/espradio" diff --git a/espnow.go b/espnow.go index c0f0f08..e468f45 100644 --- a/espnow.go +++ b/espnow.go @@ -73,9 +73,10 @@ type ESPNowSendReport struct { } var ( - espNowMu sync.RWMutex - espNowRecvHandler func(ESPNowReceive) - espNowSendHandler func(ESPNowSendReport) + espNowMu sync.RWMutex + espNowRecvHandler func(ESPNowReceive) + espNowSendHandler func(ESPNowSendReport) + activeManagedESPNow *ESPNow ) // ESPNowInit initializes the ESP-NOW subsystem and registers callback trampolines. @@ -239,10 +240,8 @@ func copyMAC(ptr *C.uint8_t) [ESPNowAddressLength]byte { 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 + manager := activeManagedESPNow espNowMu.RUnlock() - if handler == nil { - return - } event := ESPNowReceive{ SourceAddress: copyMAC(srcAddr), @@ -256,24 +255,33 @@ func espradio_on_esp_now_recv(srcAddr, destAddr *C.uint8_t, rssi C.int, channel, if data != nil && dataLen > 0 { event.Data = C.GoBytes(unsafe.Pointer(data), dataLen) } - handler(event) + if handler != nil { + handler(event) + } + if manager != nil { + manager.handleReceive(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 + manager := activeManagedESPNow espNowMu.RUnlock() - if handler == nil { - return - } - handler(ESPNowSendReport{ + report := ESPNowSendReport{ DestinationAddress: copyMAC(destAddr), SourceAddress: copyMAC(srcAddr), If: WiFiInterface(ifidx), Rate: uint32(rate), TxStatus: ESPNowSendStatus(txStatus), Status: ESPNowSendStatus(status), - }) + } + if handler != nil { + handler(report) + } + if manager != nil { + manager.handleSend(report) + } } diff --git a/espnow_packetconn.go b/espnow_packetconn.go new file mode 100644 index 0000000..0538a57 --- /dev/null +++ b/espnow_packetconn.go @@ -0,0 +1,580 @@ +//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 ( + "errors" + "fmt" + "io" + "net" + "os" + "sync" + "time" + "unsafe" +) + +const espNowPeerQueueDepth = 8 + +var ( + // ErrESPNowClosed reports use of a closed ESP-NOW manager or peer. + ErrESPNowClosed = net.ErrClosed + // ErrESPNowPacketTooLarge reports an attempt to send a payload larger than + // the maximum supported ESP-NOW payload size. + ErrESPNowPacketTooLarge = errors.New("espradio: esp-now payload too large") + // ErrESPNowPeerActive reports that a second managed ESP-NOW instance was + // created while another one is still active. + ErrESPNowPeerActive = errors.New("espradio: esp-now manager already active") + // ErrESPNowPeerMismatch reports that WriteTo was asked to send to an address + // other than the peer's configured remote address. + ErrESPNowPeerMismatch = errors.New("espradio: packet destination does not match peer") + // ErrESPNowAddrType reports that a net.Addr value is not an ESPNowAddr. + ErrESPNowAddrType = errors.New("espradio: expected ESP-NOW address") +) + +// ESPNowAddr is a 6-byte ESP-NOW MAC address and implements net.Addr. +// +// The Network method returns "espnow". The String method formats the address +// as lower-case hexadecimal separated by colons. +type ESPNowAddr [ESPNowAddressLength]byte + +func (a ESPNowAddr) Network() string { return "espnow" } + +func (a ESPNowAddr) String() string { + return fmt.Sprintf("%02x:%02x:%02x:%02x:%02x:%02x", a[0], a[1], a[2], a[3], a[4], a[5]) +} + +func (a ESPNowAddr) IsBroadcast() bool { + return a == ESPNowBroadcastAddr +} + +// ESPNowBroadcastAddr is the broadcast destination FF:FF:FF:FF:FF:FF. +var ESPNowBroadcastAddr = ESPNowAddr{0xff, 0xff, 0xff, 0xff, 0xff, 0xff} + +// ESPNowConfig configures a managed ESP-NOW instance created by NewESPNow. +// +// If PrimaryMasterKey is non-nil, it is installed before any peers are added. +type ESPNowConfig struct { + PrimaryMasterKey *[ESPNowKeyLength]byte +} + +// PeerConfig describes one remote ESP-NOW peer. +// +// Address is required. If If is zero, station mode is used by default. +// Channel may be zero to use the current Wi-Fi channel. For encrypted peers, +// set Encrypt and provide a 16-byte Key. +type PeerConfig struct { + Address ESPNowAddr + Key *[ESPNowKeyLength]byte + Channel uint8 + If WiFiInterface + Encrypt bool +} + +// ESPNow is a higher-level managed ESP-NOW wrapper. +// +// It owns the underlying global ESP-NOW initialization, maintains the SDK peer +// table for peers added through AddPeer, and routes incoming packets into Peer +// instances that implement net.PacketConn. +// +// Only one managed ESPNow instance may be active at a time. +type ESPNow struct { + mu sync.RWMutex + closed bool + peers map[ESPNowAddr]*Peer + broadcastPeer *Peer +} + +// Peer represents one remote ESP-NOW destination and implements net.PacketConn. +// +// The abstraction is packet-oriented, not stream-oriented: each WriteTo sends +// one ESP-NOW frame, and each ReadFrom returns at most one received frame. +// Incoming packets are routed by source address, so a peer created for address +// A receives packets whose source MAC is A. +type Peer struct { + now *ESPNow + + mu sync.RWMutex + addr ESPNowAddr + iface WiFiInterface + localAddr ESPNowAddr + closed bool + readDeadline time.Time + writeDeadline time.Time + rx chan espNowInboundPacket + done chan struct{} +} + +type espNowInboundPacket struct { + src ESPNowAddr + dst ESPNowAddr + data []byte +} + +var _ net.PacketConn = (*Peer)(nil) + +// NewESPNow initializes ESP-NOW and returns a managed wrapper. +// +// The caller must have already enabled and started Wi-Fi before creating the +// managed instance. Close must be called when finished to release the global +// ESP-NOW state. +// +// Only one managed ESPNow instance may exist at a time; attempting to create a +// second one returns ErrESPNowPeerActive. +func NewESPNow(cfg ESPNowConfig) (*ESPNow, error) { + if err := ESPNowInit(); err != nil { + return nil, err + } + if cfg.PrimaryMasterKey != nil { + if err := ESPNowSetPrimaryMasterKey(*cfg.PrimaryMasterKey); err != nil { + _ = ESPNowDeinit() + return nil, err + } + } + + now := &ESPNow{ + peers: make(map[ESPNowAddr]*Peer), + } + + espNowMu.Lock() + defer espNowMu.Unlock() + if activeManagedESPNow != nil { + _ = ESPNowDeinit() + return nil, ErrESPNowPeerActive + } + activeManagedESPNow = now + + return now, nil +} + +// Close deinitializes the managed ESP-NOW instance and closes all peers created +// through it. +// +// Close is idempotent. After Close, all peer operations return ErrESPNowClosed. +func (n *ESPNow) Close() error { + n.mu.Lock() + if n.closed { + n.mu.Unlock() + return nil + } + n.closed = true + peers := make([]*Peer, 0, len(n.peers)) + for _, peer := range n.peers { + peers = append(peers, peer) + } + n.peers = nil + n.broadcastPeer = nil + n.mu.Unlock() + + for _, peer := range peers { + peer.closeLocal() + } + + espNowMu.Lock() + if activeManagedESPNow == n { + activeManagedESPNow = nil + } + espNowMu.Unlock() + + return ESPNowDeinit() +} + +// AddPeer adds a remote peer to the SDK peer table and returns a Peer that +// implements net.PacketConn. +// +// If the peer already exists in this managed instance, the existing Peer is +// returned. Broadcast may be added either by calling Broadcast or by passing +// ESPNowBroadcastAddr as the peer address. +func (n *ESPNow) AddPeer(cfg PeerConfig) (*Peer, error) { + if cfg.Address.IsBroadcast() { + return n.broadcast() + } + if cfg.If == 0 { + cfg.If = WiFiInterfaceSTA + } + n.mu.Lock() + defer n.mu.Unlock() + if n.closed { + return nil, ErrESPNowClosed + } + if peer := n.peers[cfg.Address]; peer != nil { + return peer, nil + } + + raw := ESPNowPeer{ + Address: [ESPNowAddressLength]byte(cfg.Address), + Channel: cfg.Channel, + If: cfg.If, + Encrypt: cfg.Encrypt, + } + if cfg.Key != nil { + raw.Key = *cfg.Key + } + if err := ESPNowAddPeer(raw); err != nil { + return nil, err + } + + peer, err := n.newPeer(cfg.Address, cfg.If) + if err != nil { + _ = ESPNowDeletePeer(raw.Address) + return nil, err + } + n.peers[cfg.Address] = peer + return peer, nil +} + +// Peer looks up a previously added peer by MAC address. +func (n *ESPNow) Peer(addr ESPNowAddr) (*Peer, bool) { + n.mu.RLock() + defer n.mu.RUnlock() + peer, ok := n.peers[addr] + return peer, ok +} + +// Broadcast returns a Peer bound to the broadcast address +// FF:FF:FF:FF:FF:FF. +// +// Writes sent through the returned peer are broadcast. Reads from it receive +// incoming packets whose destination address is broadcast, regardless of which +// source peer sent them. +func (n *ESPNow) Broadcast() (*Peer, error) { + return n.broadcast() +} + +func (n *ESPNow) broadcast() (*Peer, error) { + n.mu.Lock() + defer n.mu.Unlock() + if n.closed { + return nil, ErrESPNowClosed + } + if n.broadcastPeer != nil { + return n.broadcastPeer, nil + } + + raw := ESPNowPeer{ + Address: [ESPNowAddressLength]byte(ESPNowBroadcastAddr), + If: WiFiInterfaceSTA, + } + if !ESPNowPeerExists(raw.Address) { + if err := ESPNowAddPeer(raw); err != nil { + return nil, err + } + } + + peer, err := n.newPeer(ESPNowBroadcastAddr, WiFiInterfaceSTA) + if err != nil { + return nil, err + } + n.peers[ESPNowBroadcastAddr] = peer + n.broadcastPeer = peer + return peer, nil +} + +func (n *ESPNow) newPeer(addr ESPNowAddr, iface WiFiInterface) (*Peer, error) { + local, err := currentESPNowMAC(iface) + if err != nil { + return nil, err + } + return &Peer{ + now: n, + addr: addr, + iface: iface, + localAddr: local, + rx: make(chan espNowInboundPacket, espNowPeerQueueDepth), + done: make(chan struct{}), + }, nil +} + +func (n *ESPNow) handleReceive(event ESPNowReceive) { + src := ESPNowAddr(event.SourceAddress) + dst := ESPNowAddr(event.DestinationAddress) + + n.mu.RLock() + if n.closed { + n.mu.RUnlock() + return + } + peer := n.peers[src] + broadcast := n.broadcastPeer + n.mu.RUnlock() + + if peer != nil { + peer.enqueue(espNowInboundPacket{ + src: src, + dst: dst, + data: append([]byte(nil), event.Data...), + }) + } + if dst.IsBroadcast() && broadcast != nil && broadcast != peer { + broadcast.enqueue(espNowInboundPacket{ + src: src, + dst: dst, + data: append([]byte(nil), event.Data...), + }) + } +} + +func (n *ESPNow) handleSend(ESPNowSendReport) { +} + +// Addr returns the peer's configured remote address. +func (p *Peer) Addr() ESPNowAddr { + p.mu.RLock() + defer p.mu.RUnlock() + return p.addr +} + +// LocalESPNowAddr returns the local MAC address used by this peer's interface. +func (p *Peer) LocalESPNowAddr() ESPNowAddr { + p.mu.RLock() + defer p.mu.RUnlock() + return p.localAddr +} + +// Send sends one payload to the peer's configured remote address. +// +// It is a convenience wrapper around WriteTo(payload, nil). +func (p *Peer) Send(payload []byte) (int, error) { + return p.WriteTo(payload, p.addr) +} + +// ReadPacket allocates a maximum-size ESP-NOW buffer, reads one packet, and +// returns the payload and sender address. +func (p *Peer) ReadPacket() ([]byte, net.Addr, error) { + buf := make([]byte, ESPNowMaxDataLength) + n, addr, err := p.ReadFrom(buf) + if err != nil { + return nil, addr, err + } + return buf[:n], addr, nil +} + +// ReadFrom waits for one received packet for this peer. +// +// The returned address is the sender's ESP-NOW address. Deadlines are honored +// using SetReadDeadline or SetDeadline. If the peer is closed while ReadFrom is +// blocked, it returns ErrESPNowClosed. +// +// If buf is smaller than the received packet, the payload is truncated and +// ReadFrom returns io.ErrShortBuffer. +func (p *Peer) ReadFrom(buf []byte) (int, net.Addr, error) { + if err := p.checkClosed(); err != nil { + return 0, nil, err + } + + deadline := p.getReadDeadline() + var timer <-chan time.Time + if !deadline.IsZero() { + d := time.Until(deadline) + if d <= 0 { + return 0, nil, os.ErrDeadlineExceeded + } + timer = time.After(d) + } + + select { + case pkt, ok := <-p.rx: + if ok { + if len(pkt.data) > len(buf) { + copy(buf, pkt.data[:len(buf)]) + return len(buf), pkt.src, io.ErrShortBuffer + } + copy(buf, pkt.data) + return len(pkt.data), pkt.src, nil + } + return 0, nil, ErrESPNowClosed + case <-p.done: + return 0, nil, ErrESPNowClosed + case <-timer: + return 0, nil, os.ErrDeadlineExceeded + } +} + +// WriteTo sends one ESP-NOW payload. +// +// The destination address must be nil or equal to the peer's configured remote +// address. Each successful call sends exactly one ESP-NOW frame. Payloads +// larger than ESPNowMaxDataLength return ErrESPNowPacketTooLarge. +// +// Write deadlines are checked before the frame is queued into the SDK. +func (p *Peer) WriteTo(payload []byte, addr net.Addr) (int, error) { + if err := p.checkClosed(); err != nil { + return 0, err + } + if len(payload) > ESPNowMaxDataLength { + return 0, ErrESPNowPacketTooLarge + } + if deadline := p.getWriteDeadline(); !deadline.IsZero() && time.Now().After(deadline) { + return 0, os.ErrDeadlineExceeded + } + + dest, err := p.resolveWriteAddr(addr) + if err != nil { + return 0, err + } + + raw := [ESPNowAddressLength]byte(dest) + if err := ESPNowSend(&raw, payload); err != nil { + return 0, err + } + return len(payload), nil +} + +// Close closes the peer and removes it from the SDK peer table. +// +// Close is idempotent. Closing a peer does not close the owning ESPNow +// instance, but any blocked reads on the peer are unblocked with +// ErrESPNowClosed. +func (p *Peer) Close() error { + p.mu.Lock() + if p.closed { + p.mu.Unlock() + return nil + } + p.closed = true + p.mu.Unlock() + + p.now.mu.Lock() + if p.now.peers != nil { + delete(p.now.peers, p.addr) + if p.now.broadcastPeer == p { + p.now.broadcastPeer = nil + } + } + p.now.mu.Unlock() + + p.closeLocal() + return ESPNowDeletePeer([ESPNowAddressLength]byte(p.addr)) +} + +// LocalAddr returns the local address used by this peer and satisfies +// net.PacketConn. +func (p *Peer) LocalAddr() net.Addr { + p.mu.RLock() + defer p.mu.RUnlock() + return p.localAddr +} + +// SetDeadline sets both the read and write deadlines for the peer. +func (p *Peer) SetDeadline(t time.Time) error { + p.mu.Lock() + defer p.mu.Unlock() + p.readDeadline = t + p.writeDeadline = t + return nil +} + +// SetReadDeadline sets the deadline for future ReadFrom calls. +func (p *Peer) SetReadDeadline(t time.Time) error { + p.mu.Lock() + defer p.mu.Unlock() + p.readDeadline = t + return nil +} + +// SetWriteDeadline sets the deadline checked by future WriteTo calls. +func (p *Peer) SetWriteDeadline(t time.Time) error { + p.mu.Lock() + defer p.mu.Unlock() + p.writeDeadline = t + return nil +} + +func (p *Peer) enqueue(pkt espNowInboundPacket) { + p.mu.RLock() + if p.closed { + p.mu.RUnlock() + return + } + ch := p.rx + p.mu.RUnlock() + + select { + case ch <- pkt: + default: + } +} + +func (p *Peer) closeLocal() { + p.mu.Lock() + if p.closed { + p.mu.Unlock() + return + } + p.closed = true + done := p.done + p.mu.Unlock() + close(done) +} + +func (p *Peer) checkClosed() error { + p.mu.RLock() + defer p.mu.RUnlock() + if p.closed { + return ErrESPNowClosed + } + return nil +} + +func (p *Peer) getReadDeadline() time.Time { + p.mu.RLock() + defer p.mu.RUnlock() + return p.readDeadline +} + +func (p *Peer) getWriteDeadline() time.Time { + p.mu.RLock() + defer p.mu.RUnlock() + return p.writeDeadline +} + +func (p *Peer) resolveWriteAddr(addr net.Addr) (ESPNowAddr, error) { + p.mu.RLock() + defer p.mu.RUnlock() + + if addr == nil { + return p.addr, nil + } + dest, err := parseESPNowAddr(addr) + if err != nil { + return ESPNowAddr{}, err + } + if dest != p.addr { + return ESPNowAddr{}, ErrESPNowPeerMismatch + } + return dest, nil +} + +func parseESPNowAddr(addr net.Addr) (ESPNowAddr, error) { + switch v := addr.(type) { + case ESPNowAddr: + return v, nil + case *ESPNowAddr: + if v == nil { + return ESPNowAddr{}, ErrESPNowAddrType + } + return *v, nil + default: + return ESPNowAddr{}, ErrESPNowAddrType + } +} + +func currentESPNowMAC(iface WiFiInterface) (ESPNowAddr, error) { + var mac ESPNowAddr + code := C.esp_wifi_get_mac(C.wifi_interface_t(iface), (*C.uint8_t)(unsafe.Pointer(&mac[0]))) + if code != C.ESP_OK { + return ESPNowAddr{}, makeError(code) + } + return mac, nil +} diff --git a/espnow_stream.go b/espnow_stream.go new file mode 100644 index 0000000..eed33ee --- /dev/null +++ b/espnow_stream.go @@ -0,0 +1,103 @@ +//go:build esp32c3 || esp32c3_qemu_target || esp32s3 + +package espradio + +import ( + "bytes" + "io" + "sync" +) + +// PeerStream adapts a packet-oriented Peer into a stream-like io.ReadWriter. +// +// Read buffers received ESP-NOW packets internally and presents them as a +// continuous byte stream. Packet boundaries are not preserved: one Read may +// return data from part of a packet, a whole packet, or multiple packets. +// +// Write splits large writes into multiple ESP-NOW packets as needed. Each +// packet is sent in order using the wrapped peer. +// +// This adapter is intentionally lossy with respect to packet framing. Use Peer +// directly when application-level message boundaries matter. +type PeerStream struct { + peer *Peer + + readMu sync.Mutex + writeMu sync.Mutex + readBuf bytes.Buffer +} + +var _ io.ReadWriter = (*PeerStream)(nil) + +// NewPeerStream returns a stream-like adapter around a packet-oriented Peer. +func NewPeerStream(peer *Peer) *PeerStream { + return &PeerStream{peer: peer} +} + +// Stream returns a stream-like adapter for the peer. +// +// The returned adapter buffers incoming packets and fragments large writes into +// multiple ESP-NOW frames. Packet boundaries are not preserved. +func (p *Peer) Stream() *PeerStream { + return NewPeerStream(p) +} + +// Read reads from the adapter's internal buffer, refilling it from received +// ESP-NOW packets as needed. +// +// Unlike bytes.Buffer.Read, this method does not surface io.EOF merely because +// the current internal buffer is empty. Instead it blocks waiting for the next +// packet from the wrapped peer. Errors returned by the wrapped peer, including +// deadline and close errors, are returned directly. +func (s *PeerStream) Read(p []byte) (int, error) { + if len(p) == 0 { + return 0, nil + } + + s.readMu.Lock() + defer s.readMu.Unlock() + + for s.readBuf.Len() == 0 { + pkt, _, err := s.peer.ReadPacket() + if err != nil { + return 0, err + } + if len(pkt) == 0 { + continue + } + _, _ = s.readBuf.Write(pkt) + } + + n, err := s.readBuf.Read(p) + if err == io.EOF { + return n, nil + } + return n, err +} + +// Write writes a byte stream to the wrapped peer. +// +// Large writes are split into multiple ESP-NOW packets of at most +// ESPNowMaxDataLength bytes each and sent sequentially. +func (s *PeerStream) Write(p []byte) (int, error) { + if len(p) == 0 { + return 0, nil + } + + s.writeMu.Lock() + defer s.writeMu.Unlock() + + written := 0 + for written < len(p) { + end := written + ESPNowMaxDataLength + if end > len(p) { + end = len(p) + } + n, err := s.peer.Send(p[written:end]) + written += n + if err != nil { + return written, err + } + } + return written, nil +} From 96e4b1b96f2a000444838527645556ec6e3bf5a3 Mon Sep 17 00:00:00 2001 From: Matthew Hiles Date: Fri, 29 May 2026 10:09:41 -0400 Subject: [PATCH 4/5] bound error range --- error.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/error.go b/error.go index f254ba7..49125f1 100644 --- a/error.go +++ b/error.go @@ -21,7 +21,7 @@ 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: + case e >= C.ESP_ERR_ESPNOW_BASE && e <= C.ESP_ERR_ESPNOW_CHAN: switch e { case C.ESP_ERR_ESPNOW_NOT_INIT: return "espradio: esp-now not initialized" From 818e89875faf56eb43bf1bfd2e1ba2e0150378af Mon Sep 17 00:00:00 2001 From: Matthew Hiles Date: Tue, 2 Jun 2026 14:32:16 -0400 Subject: [PATCH 5/5] add examples/espnow/main.go --- examples/espnow/main.go | 94 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 examples/espnow/main.go diff --git a/examples/espnow/main.go b/examples/espnow/main.go new file mode 100644 index 0000000..0c28c3f --- /dev/null +++ b/examples/espnow/main.go @@ -0,0 +1,94 @@ +// This example demonstrates ESP-NOW packet and stream-style communication using +// the managed espradio ESPNow API. +// +// Set peerAddress to the ESP-NOW MAC address of the remote device before +// flashing. For two devices running this example, each device should use the +// other device's printed local ESP-NOW address as peerAddress. +// +// tinygo flash -target xiao-esp32c3 -monitor ./examples/espnow +package main + +import ( + "errors" + "os" + "time" + + "tinygo.org/x/espradio" +) + +var peerAddress = espradio.ESPNowAddr{0x24, 0x6f, 0x28, 0xaa, 0xbb, 0xcc} + +func main() { + time.Sleep(time.Second) + + println("initializing radio...") + if err := espradio.Enable(espradio.Config{}); err != nil { + failure("could not enable radio: " + err.Error()) + } + + println("starting radio...") + if err := espradio.Start(); err != nil { + failure("could not start radio: " + err.Error()) + } + + now, err := espradio.NewESPNow(espradio.ESPNowConfig{}) + if err != nil { + failure("could not initialize ESP-NOW: " + err.Error()) + } + defer now.Close() + + peer, err := now.AddPeer(espradio.PeerConfig{ + Address: peerAddress, + If: espradio.WiFiInterfaceSTA, + }) + if err != nil { + failure("could not add peer: " + err.Error()) + } + defer peer.Close() + + println("local ESP-NOW address:", peer.LocalESPNowAddr().String()) + println("peer ESP-NOW address:", peer.Addr().String()) + + if _, err := peer.WriteTo([]byte("hello"), nil); err != nil { + failure("could not send packet: " + err.Error()) + } + println("sent packet") + + if err := peer.SetReadDeadline(time.Now().Add(5 * time.Second)); err != nil { + failure("could not set read deadline: " + err.Error()) + } + buf := make([]byte, espradio.ESPNowMaxDataLength) + n, addr, err := peer.ReadFrom(buf) + if err != nil { + if errors.Is(err, os.ErrDeadlineExceeded) { + println("no reply received before deadline") + } else { + failure("could not read packet: " + err.Error()) + } + } else { + println("received", n, "bytes from", addr.String()) + println("payload:", string(buf[:n])) + } + + stream := peer.Stream() + if _, err := stream.Write([]byte("streamed payload")); err != nil { + failure("could not send stream payload: " + err.Error()) + } + println("sent stream payload") + + broadcast, err := now.Broadcast() + if err != nil { + failure("could not add broadcast peer: " + err.Error()) + } + if _, err := broadcast.WriteTo([]byte("announcement"), nil); err != nil { + failure("could not send broadcast: " + err.Error()) + } + println("sent broadcast announcement") +} + +func failure(msg string) { + for { + println("failure:", msg) + time.Sleep(time.Second) + } +}