mirror of
https://github.com/soypat/lneto.git
synced 2026-07-26 10:38:47 +00:00
Lay out device/stack interfaces - Netdev/Netlink (#92)
* good code today * add netdev.Runner * round off APIs more * better interface method documentation * improve DHCP netstack API * add pico w netdev example * remove temp file * keep thinking about this. this is hard :/ * fix ci * small fixer * working dhcp * fix rebase API mismatches * begin adding espradio example * keep working on espradio, icmp not working * fix icmp by fixing dhcp
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
module espradio-netdev
|
||||
|
||||
go 1.25.7
|
||||
|
||||
// These replace directives point to local checkouts during development.
|
||||
// Remove them when using as a standalone program with published releases.
|
||||
replace github.com/soypat/lneto => ../../../.
|
||||
|
||||
replace tinygo.org/x/espradio => ../../../../espradio
|
||||
|
||||
require (
|
||||
github.com/soypat/lneto v0.1.1-0.20260425023453-aa77403a2b32
|
||||
tinygo.org/x/espradio v0.1.0
|
||||
)
|
||||
@@ -0,0 +1,213 @@
|
||||
// Example showing how to use the ESP32 WiFi radio through lneto's netdev
|
||||
// package. This mirrors the picow-netdev example and demonstrates the
|
||||
// standardised DevEthernet+Netlink interface that works across hardware targets.
|
||||
//
|
||||
// Build and flash:
|
||||
//
|
||||
// tinygo flash -target xiao-esp32c3 \
|
||||
// -ldflags="-X main.ssid=YourSSID -X main.password=YourPassword" \
|
||||
// -monitor ./examples/esp32-netdev
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"net/netip"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/soypat/lneto"
|
||||
"github.com/soypat/lneto/x/netdev"
|
||||
"github.com/soypat/lneto/x/xnet"
|
||||
"tinygo.org/x/espradio"
|
||||
)
|
||||
|
||||
var (
|
||||
//go:embed wifi.credentials
|
||||
credentials string
|
||||
// remove windows CRLF "\r\n" and trailing newline.
|
||||
credentialsNormalized = strings.TrimSuffix(strings.ReplaceAll(credentials, "\r\n", "\n"), "\n")
|
||||
|
||||
globConnectParams espradio.STAConfig
|
||||
|
||||
poolCfg = xnet.TCPPoolConfig{
|
||||
PoolSize: 4,
|
||||
QueueSize: 4,
|
||||
TxBufSize: 2048,
|
||||
RxBufSize: 512,
|
||||
NewBackoff: func() lneto.BackoffStrategy { return backoff },
|
||||
NanoTime: func() int64 { return time.Now().UnixNano() },
|
||||
EstablishedTimeout: 5 * time.Second,
|
||||
ClosingTimeout: 3 * time.Second,
|
||||
}
|
||||
)
|
||||
|
||||
func main() {
|
||||
time.Sleep(time.Second)
|
||||
ssid, password, ok := strings.Cut(credentialsNormalized, "\n")
|
||||
if !ok {
|
||||
fail("must write newline separated ssid/password in wifi.credentials", nil)
|
||||
}
|
||||
globConnectParams.SSID = ssid
|
||||
globConnectParams.Password = password
|
||||
var dev EspDev
|
||||
dev.radioConfig = espradio.Config{Logging: espradio.LogLevelError}
|
||||
|
||||
// LinkConnect runs Enable+Start+Connect+StartNetDev. It must complete
|
||||
// before HardwareAddr6 is called because the MAC is only readable after
|
||||
// Enable() initialises the WiFi hardware.
|
||||
err := dev.LinkConnect(globConnectParams)
|
||||
failIfErr("wifi connect", err)
|
||||
|
||||
hw, err := dev.HardwareAddr6()
|
||||
failIfErr("hardware addr", err)
|
||||
|
||||
var stack xnet.Netstack
|
||||
err = stack.Reset(xnet.StackConfig{
|
||||
RandSeed: time.Now().UnixNano() | 1,
|
||||
Hostname: "esp32-lneto",
|
||||
MaxActiveTCPPorts: 4,
|
||||
MaxActiveUDPPorts: 4,
|
||||
ICMPQueueLimit: 1,
|
||||
MTU: 1500,
|
||||
PassivePeers: 4,
|
||||
HardwareAddress: hw,
|
||||
}, backoff, poolCfg)
|
||||
failIfErr("stack reset", err)
|
||||
err = stack.EnableICMP(true)
|
||||
failIfErr("icmp enable", err)
|
||||
dev.LinkNotify(userNotify)
|
||||
var iface netdev.Interface[espradio.STAConfig]
|
||||
err = iface.Init(&dev, &dev, netdev.InterfaceConfig{})
|
||||
failIfErr("init iface", err)
|
||||
|
||||
var runner netdev.Runner[espradio.STAConfig]
|
||||
go func() {
|
||||
if err := runner.Run(context.Background(), iface, &stack, backoff); err != nil {
|
||||
failIfErr("runner", err)
|
||||
}
|
||||
}()
|
||||
|
||||
assigned, gatewayRt, subnetBits, err := stack.EnableDHCP(context.Background(), true, netip.Addr{})
|
||||
failIfErr("enable dhcp", err)
|
||||
println("assigned=", assigned.String(), "gateway=", gatewayRt.String(), "subnet=", subnetBits)
|
||||
|
||||
select {}
|
||||
}
|
||||
|
||||
// compile-time interface checks.
|
||||
var _ lneto.BackoffStrategy = backoff
|
||||
var _ netdev.Stack = (*xnet.Netstack)(nil)
|
||||
var _ netdev.DevEthernet = (*EspDev)(nil)
|
||||
var _ netdev.Netlink[espradio.STAConfig] = (*EspDev)(nil)
|
||||
|
||||
func backoff(consecutiveBackoffs uint) time.Duration {
|
||||
return 5 * time.Millisecond
|
||||
}
|
||||
|
||||
func userNotify(connected bool) (retries int, reconnectParams espradio.STAConfig) {
|
||||
if !connected {
|
||||
return 1, globConnectParams
|
||||
}
|
||||
return 0, espradio.STAConfig{}
|
||||
}
|
||||
|
||||
// EspDev adapts [espradio.NetDev] to [netdev.DevEthernet] and wraps the
|
||||
// ESP32 WiFi bring-up sequence (Enable/Start/Connect/StartNetDev) as [netdev.Netlink].
|
||||
//
|
||||
// The same struct implements both interfaces so a single pointer can be passed
|
||||
// to [netdev.Interface.Init] for both the netlink and device arguments, matching
|
||||
// the pattern used by the picow-netdev example.
|
||||
type EspDev struct {
|
||||
nd *espradio.NetDev
|
||||
radioConfig espradio.Config
|
||||
notifyCb netdev.NotifyCallback[espradio.STAConfig]
|
||||
}
|
||||
|
||||
// LinkConnect implements [netdev.Netlink].
|
||||
// Runs Enable→Start→Connect→StartNetDev. nd is nil until this returns successfully.
|
||||
func (d *EspDev) LinkConnect(cfg espradio.STAConfig) error {
|
||||
if err := espradio.Enable(d.radioConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := espradio.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := espradio.Connect(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
nd, err := espradio.StartNetDev()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.nd = nd
|
||||
return nil
|
||||
}
|
||||
|
||||
// LinkDisconnect implements [netdev.Netlink].
|
||||
func (d *EspDev) LinkDisconnect() {}
|
||||
|
||||
// LinkNotify implements [netdev.Netlink].
|
||||
func (d *EspDev) LinkNotify(cb netdev.NotifyCallback[espradio.STAConfig]) {
|
||||
d.notifyCb = cb
|
||||
}
|
||||
|
||||
// HardwareAddr6 implements [netdev.DevEthernet].
|
||||
func (d *EspDev) HardwareAddr6() ([6]byte, error) {
|
||||
return d.nd.HardwareAddr6()
|
||||
}
|
||||
|
||||
// SendOffsetEthFrame implements [netdev.DevEthernet].
|
||||
// ESP32 has no frame prefix offset, so buf is passed directly to [espradio.NetDev.SendEthFrame].
|
||||
func (d *EspDev) SendOffsetEthFrame(buf []byte) error {
|
||||
return d.nd.SendEthFrame(buf)
|
||||
}
|
||||
|
||||
// SetEthRecvHandler implements [netdev.DevEthernet].
|
||||
// Adapts the error-less netdev handler signature to espradio's error-returning one.
|
||||
func (d *EspDev) SetEthRecvHandler(handler func(rxEthframe []byte)) {
|
||||
if handler == nil {
|
||||
d.nd.SetEthRecvHandler(nil)
|
||||
return
|
||||
}
|
||||
d.nd.SetEthRecvHandler(func(pkt []byte) error {
|
||||
handler(pkt)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// EthPoll implements [netdev.DevEthernet].
|
||||
//
|
||||
// espradio.NetDev.EthPoll both pops a frame from the C ring buffer into buf
|
||||
// AND synchronously calls the registered rxHandler with that frame. Returning
|
||||
// the non-zero byte count here as well would trigger the Runner's "device uses
|
||||
// both paths" guard. We therefore drain the ring (calling the handler) and
|
||||
// return (0, 0, err) so the Runner sees only the handler-based receive path.
|
||||
func (d *EspDev) EthPoll(buf []byte) (ethFrameOff, ethernetBytes int, err error) {
|
||||
_, err = d.nd.EthPoll(buf)
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
// MaxFrameSizeAndOffset implements [netdev.DevEthernet].
|
||||
// ESP32 transmits frames with no prefix offset.
|
||||
func (d *EspDev) MaxFrameSizeAndOffset() (maxFrameSize int, frameOff int) {
|
||||
return d.nd.MaxFrameSize(), 0
|
||||
}
|
||||
|
||||
func failIfErr(msg string, err error) {
|
||||
if err != nil {
|
||||
fail(msg, err)
|
||||
}
|
||||
println(msg, "PASS")
|
||||
}
|
||||
|
||||
func fail(msg string, err error) {
|
||||
var errstr string
|
||||
if err != nil {
|
||||
errstr = err.Error()
|
||||
}
|
||||
for {
|
||||
println("FAIL:", msg, errstr)
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
module piconetdev
|
||||
|
||||
go 1.25.7
|
||||
|
||||
require github.com/soypat/cyw43439 v0.1.1
|
||||
|
||||
require (
|
||||
github.com/soypat/lneto v0.1.0 // indirect
|
||||
github.com/soypat/seqs v0.0.0-20250124201400-0d65bc7c1710 // indirect
|
||||
github.com/tinygo-org/pio v0.2.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa // indirect
|
||||
)
|
||||
|
||||
// This is an example taken grom github.com/soypat/lneto
|
||||
// Remove this replace directive when using as own program.
|
||||
replace github.com/soypat/lneto => ../../../.
|
||||
@@ -0,0 +1,10 @@
|
||||
github.com/soypat/cyw43439 v0.1.1 h1:vcaTiVzfuz3keK7lJpVxStZ6tV8HCw7Ugzsh1k4mneE=
|
||||
github.com/soypat/cyw43439 v0.1.1/go.mod h1:R2uSILRwSPmcmmKy5Z0FtK4ypgiPf5YqK+F+IKmXqxc=
|
||||
github.com/soypat/lneto v0.1.0 h1:VAHCJ33hvC3wDqhM0Vm7w0k6vwNsOCAsQ8XTrXJpS7I=
|
||||
github.com/soypat/lneto v0.1.0/go.mod h1:g/8Lk+hIsMZydyWDJjK2YfsCuG6jA5mWCO6U+4S7w1U=
|
||||
github.com/soypat/seqs v0.0.0-20250124201400-0d65bc7c1710 h1:Y9fBuiR/urFY/m76+SAZTxk2xAOS2n85f+H1CugajeA=
|
||||
github.com/soypat/seqs v0.0.0-20250124201400-0d65bc7c1710/go.mod h1:oCVCNGCHMKoBj97Zp9znLbQ1nHxpkmOY9X+UAGzOxc8=
|
||||
github.com/tinygo-org/pio v0.2.0 h1:vo3xa6xDZ2rVtxrks/KcTZHF3qq4lyWOntvEvl2pOhU=
|
||||
github.com/tinygo-org/pio v0.2.0/go.mod h1:LU7Dw00NJ+N86QkeTGjMLNkYcEYMor6wTDpTCu0EaH8=
|
||||
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa h1:ELnwvuAXPNtPk1TJRuGkI9fDTwym6AYBu0qzT8AcHdI=
|
||||
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
|
||||
@@ -0,0 +1,177 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"net"
|
||||
"net/netip"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/soypat/cyw43439"
|
||||
"github.com/soypat/lneto"
|
||||
"github.com/soypat/lneto/x/netdev"
|
||||
"github.com/soypat/lneto/x/xnet"
|
||||
)
|
||||
|
||||
var (
|
||||
//go:embed wifi.credentials
|
||||
credentials string
|
||||
// remove windows CRLF "\r\n" and trailing newline.
|
||||
credentialsNormalized = strings.TrimSuffix(strings.ReplaceAll(credentials, "\r\n", "\n"), "\n")
|
||||
globConnectParams ConnectParams
|
||||
poolCfg = xnet.TCPPoolConfig{
|
||||
PoolSize: 5,
|
||||
QueueSize: 5,
|
||||
TxBufSize: 2048,
|
||||
RxBufSize: 2048,
|
||||
NewBackoff: func() lneto.BackoffStrategy { return backoff },
|
||||
NanoTime: func() int64 { return time.Now().UnixNano() },
|
||||
EstablishedTimeout: 5 * time.Second,
|
||||
ClosingTimeout: 3 * time.Second,
|
||||
}
|
||||
)
|
||||
|
||||
func main() {
|
||||
time.Sleep(time.Second)
|
||||
ssid, password, ok := strings.Cut(credentialsNormalized, "\n")
|
||||
if !ok {
|
||||
fail("must write newline separated ssid/password in wifi.credentials", nil)
|
||||
}
|
||||
globConnectParams.SSID = ssid
|
||||
globConnectParams.Passphrase = password
|
||||
|
||||
dev := Netdev{
|
||||
dev: cyw43439.NewPicoWDevice(),
|
||||
}
|
||||
err := dev.dev.Init(cyw43439.DefaultWifiConfig())
|
||||
failIfErr("init cyw43439", err)
|
||||
hw, _ := dev.HardwareAddr6()
|
||||
var stack xnet.Netstack
|
||||
err = stack.Reset(xnet.StackConfig{
|
||||
RandSeed: time.Now().UnixNano() | 1,
|
||||
Hostname: "lneto-pico",
|
||||
MaxActiveTCPPorts: 4,
|
||||
MaxActiveUDPPorts: 4,
|
||||
ICMPQueueLimit: 1,
|
||||
MTU: 1500,
|
||||
HardwareAddress: hw,
|
||||
}, backoff, poolCfg)
|
||||
failIfErr("stack reset", err)
|
||||
err = stack.EnableICMP(true)
|
||||
failIfErr("icmp enable", err)
|
||||
err = dev.LinkConnect(globConnectParams)
|
||||
failIfErr("wifi join", err)
|
||||
|
||||
var iface netdev.Interface[ConnectParams]
|
||||
var runner netdev.Runner[ConnectParams]
|
||||
dev.LinkNotify(userNotify)
|
||||
err = iface.Init(&dev, &dev, netdev.InterfaceConfig{})
|
||||
failIfErr("init iface", err)
|
||||
|
||||
go func() {
|
||||
if err := runner.Run(context.Background(), iface, &stack, backoff); err != nil {
|
||||
failIfErr("runner", err)
|
||||
}
|
||||
}()
|
||||
assigned, gatewayRt, subnetBits, err := stack.EnableDHCP(context.Background(), true, netip.Addr{})
|
||||
failIfErr("enable dhcp", err)
|
||||
println("assigned=", assigned.String(), "gateway=", gatewayRt.String(), "subnet", subnetBits)
|
||||
select {}
|
||||
}
|
||||
|
||||
// compile-time guarantee of interface implementation.
|
||||
var _ lneto.BackoffStrategy = backoff
|
||||
var _ netdev.Stack = (*xnet.Netstack)(nil)
|
||||
|
||||
func backoff(consecutiveBackoffs uint) (sleepOrFlag time.Duration) {
|
||||
return 5 * time.Millisecond
|
||||
}
|
||||
|
||||
func userNotify(connected bool) (retries int, connectParams ConnectParams) {
|
||||
if !connected {
|
||||
return 1, globConnectParams
|
||||
}
|
||||
return 0, ConnectParams{}
|
||||
}
|
||||
|
||||
var _ netdev.DevEthernet = (*Netdev)(nil)
|
||||
var _ netdev.Netlink[ConnectParams] = (*Netdev)(nil)
|
||||
|
||||
type Netdev struct {
|
||||
dev *cyw43439.Device
|
||||
notifyCb netdev.NotifyCallback[ConnectParams]
|
||||
}
|
||||
|
||||
type ConnectParams struct {
|
||||
SSID string
|
||||
cyw43439.JoinOptions
|
||||
}
|
||||
|
||||
func (nl *Netdev) Netflags() (flags net.Flags) {
|
||||
flags |= net.FlagUp
|
||||
if nl.dev.IsLinkUp() {
|
||||
flags |= net.FlagRunning
|
||||
}
|
||||
return flags
|
||||
}
|
||||
|
||||
// LinkConnect implements [netdev.Netlink].
|
||||
func (nl *Netdev) LinkConnect(connectParams ConnectParams) error {
|
||||
return nl.dev.Join(connectParams.SSID, connectParams.JoinOptions)
|
||||
}
|
||||
|
||||
// LinkDisconnect implements [netdev.Netlink].
|
||||
func (nl *Netdev) LinkDisconnect() {
|
||||
// Not implemented by cyw43439 package.
|
||||
}
|
||||
|
||||
// LinkNotify implements [netdev.Netlink].
|
||||
func (nl *Netdev) LinkNotify(cb netdev.NotifyCallback[ConnectParams]) {
|
||||
nl.notifyCb = cb
|
||||
}
|
||||
|
||||
// HardwareAddr6 implements [netdev.DevEthernet].
|
||||
func (d *Netdev) HardwareAddr6() ([6]byte, error) {
|
||||
return d.dev.HardwareAddr6()
|
||||
}
|
||||
|
||||
// SendEthFrameOffset implements [netdev.DevEthernet].
|
||||
func (d *Netdev) SendOffsetEthFrame(offsetTxEthFrame []byte) error {
|
||||
return d.dev.SendEth(offsetTxEthFrame)
|
||||
}
|
||||
|
||||
// SetRecvHandler implements [netdev.DevEthernet].
|
||||
func (d *Netdev) SetEthRecvHandler(handler func(rxEthframe []byte)) {
|
||||
d.dev.RecvEthHandle(func(pkt []byte) error {
|
||||
handler(pkt)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// EthPoll implements [netdev.DevEthernet].
|
||||
func (d *Netdev) EthPoll(buf []byte) (ethFrameOff, ethernetBytes int, err error) {
|
||||
_, err = d.dev.PollOne()
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
// MaxFrameSizeAndOffset implements [netdev.DevEthernet].
|
||||
func (d *Netdev) MaxFrameSizeAndOffset() (maxFrameSize int, frameOff int) {
|
||||
return cyw43439.MaxFrameSize, 0
|
||||
}
|
||||
func failIfErr(msg string, err error) {
|
||||
if err != nil {
|
||||
fail(msg, err)
|
||||
}
|
||||
println("PASS", msg)
|
||||
}
|
||||
func fail(msg string, err error) {
|
||||
var errstr string
|
||||
if err != nil {
|
||||
errstr = err.Error()
|
||||
}
|
||||
for {
|
||||
println(msg, errstr)
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user