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/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" diff --git a/espnow.go b/espnow.go index c0f0f08..7e8426e 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,8 +240,9 @@ 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 { + if handler == nil && manager == nil { return } @@ -256,24 +258,36 @@ 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 { + if handler == nil && manager == 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 +} 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) + } +}