Added networking stack (mostly copied from pico-weather) and added NTP sync
ci/woodpecker/push/woodpecker Pipeline failed

This commit is contained in:
2026-09-07 18:01:31 -04:00
parent b9195b33bc
commit a0b8906d63
4 changed files with 383 additions and 17 deletions
+309
View File
@@ -0,0 +1,309 @@
package main
import (
"context"
"errors"
"fmt"
"net"
"net/netip"
"syscall"
"time"
"github.com/soypat/cyw43439"
"github.com/soypat/lneto"
"github.com/soypat/lneto/ethernet"
"github.com/soypat/lneto/x/xnet"
)
const (
pollTime = 5 * time.Millisecond
protoTimeout = 5 * time.Second
protoRetries = 3
tcpBufsize = 2048
tcpPacketQueueSize = 4
tcpConnPoolSize = 5
tcpEstablishedTimeout = 4 * time.Second
tcpCloseTimeout = protoTimeout
)
var nanotime = func() int64 {
return time.Now().UnixNano()
}
// CywAdapter bridges the cyw43439 driver to the lneto interface requirements.
type CywAdapter struct {
dev *cyw43439.Device
rxBuf [1514]byte
rxLen int
hasPkt bool
}
func NewCywAdapter(dev *cyw43439.Device) *CywAdapter {
adapter := &CywAdapter{dev: dev}
dev.RecvEthHandle(func(pkt []byte) error {
if !adapter.hasPkt && len(pkt) <= len(adapter.rxBuf) {
copy(adapter.rxBuf[:], pkt)
adapter.rxLen = len(pkt)
adapter.hasPkt = true
}
return nil
})
return adapter
}
// In CywAdapter:
func (a *CywAdapter) PollHardware() error {
_, err := a.dev.TryPoll()
return err
}
func (a *CywAdapter) SendEth(frame []byte) error {
return a.dev.SendEth(frame)
}
func (a *CywAdapter) RecvEth(dst []byte) (int, error) {
if !a.hasPkt {
return 0, nil
}
n := copy(dst, a.rxBuf[:a.rxLen])
a.hasPkt = false
a.rxLen = 0
return n, nil
}
func (a *CywAdapter) HardwareAddress6() ([6]byte, error) {
return a.dev.HardwareAddr6()
}
func (a *CywAdapter) MaxFrameLength() (int, error) {
return 1514, nil
}
type NetworkStack struct {
adapter *CywAdapter
stack *xnet.StackAsync
gostack xnet.StackGo
localIP netip.Addr
}
func InitNetwork(ssid, pass string) (*NetworkStack, error) {
println("Initializing CYW43439 Wi-Fi hardware...")
dev := cyw43439.NewPicoWDevice()
cfg := cyw43439.DefaultWifiConfig()
if err := dev.Init(cfg); err != nil {
return nil, fmt.Errorf("device init failed: %w", err)
}
println("Associating with SSID:", ssid)
if err := dev.JoinWPA2(ssid, pass); err != nil {
return nil, fmt.Errorf("wifi association failed: %w", err)
}
println("Wi-Fi associated.")
adapter := NewCywAdapter(dev)
hwaddr, err := adapter.HardwareAddress6()
if err != nil {
return nil, fmt.Errorf("read MAC: %w", err)
}
framelen, err := adapter.MaxFrameLength()
if err != nil {
return nil, fmt.Errorf("max frame len: %w", err)
}
stack := &xnet.StackAsync{}
err = stack.Reset(xnet.StackConfig{
Hostname: "pico2w-bme280",
RandSeed: time.Now().UnixNano(),
MaxActiveTCPPorts: 2,
MTU: uint16(framelen - ethernet.MaxOverheadSize),
HardwareAddress: hwaddr,
})
if err != nil {
return nil, fmt.Errorf("stack config reset: %w", err)
}
// Start background frame pump
ctx := context.Background()
go stackLoop(ctx, stack, adapter)
println("Acquiring IP via DHCP...")
rstack := stack.StackRetrying(stackBackoff)
results, err := rstack.DoDHCPv4([4]byte{}, protoTimeout, protoRetries)
if err != nil {
return nil, fmt.Errorf("DHCP failed: %w", err)
}
err = stack.AssimilateDHCPResults(results)
if err != nil {
return nil, fmt.Errorf("assimilate DHCP failed: %w", err)
}
println("Resolving router MAC...")
gateway, err := rstack.DoResolveHardwareAddress6(results.Router, protoTimeout, protoRetries)
if err != nil {
return nil, fmt.Errorf("resolving router MAC failed: %w", err)
}
stack.SetGatewayHardwareAddr(gateway)
localIP := netip.AddrFrom4(results.AssignedAddr4)
println("DHCP lease assigned! IP:", localIP.String())
gostack := stack.StackBlocking(stackBackoff).StackGo(xnet.StackGoConfig{
ListenerPoolConfig: xnet.TCPPoolConfig{
PoolSize: tcpConnPoolSize,
QueueSize: tcpPacketQueueSize,
TxBufSize: tcpBufsize,
RxBufSize: tcpBufsize,
NanoTime: nanotime,
EstablishedTimeout: tcpEstablishedTimeout,
ClosingTimeout: tcpCloseTimeout,
NewBackoff: func() lneto.BackoffStrategy { return tcpBackoff },
},
})
return &NetworkStack{
adapter: adapter,
stack: stack,
gostack: gostack,
localIP: localIP,
}, nil
}
func (ns *NetworkStack) DialTCP(ctx context.Context, hostIP string, port uint16) (net.Conn, error) {
rIP, err := netip.ParseAddr(hostIP)
if err != nil {
return nil, fmt.Errorf("invalid host IPv4: %w", err)
}
laddr := net.TCPAddrFromAddrPort(netip.AddrPortFrom(ns.localIP, 0))
raddr := net.TCPAddrFromAddrPort(netip.AddrPortFrom(rIP, port))
const sockstream = 0x1
c, err := ns.gostack.Socket(ctx, "tcp", syscall.AF_INET, sockstream, laddr, raddr)
if err != nil {
return nil, fmt.Errorf("socket dial: %w", err)
}
conn, ok := c.(net.Conn)
if !ok {
return nil, errors.New("socket did not return a stream connection")
}
return conn, nil
}
func stackLoop(ctx context.Context, stack *xnet.StackAsync, adapter *CywAdapter) {
frameLength, _ := adapter.MaxFrameLength()
buf := make([]byte, frameLength)
for ctx.Err() == nil {
// 1. Pump the CYW43439 hardware over SPI to trigger RecvEthHandle
_ = adapter.PollHardware()
// 2. Ingress: read from adapter into lneto
nread, err := adapter.RecvEth(buf[:])
if err != nil {
println("recv err:", err.Error())
} else if nread > 0 {
err = stack.IngressEthernet(buf[:nread])
if err != nil && err != lneto.ErrPacketDrop {
println("ingress err:", err.Error())
}
}
// 3. Egress: send out any frames generated by lneto
nwrite, err := stack.EgressEthernet(buf[:])
if err != nil {
println("egress err:", err.Error())
} else if nwrite > 0 {
if err := adapter.SendEth(buf[:nwrite]); err != nil {
println("send eth err:", err.Error())
}
}
if nwrite == 0 && nread == 0 {
time.Sleep(pollTime)
}
}
}
func stackBackoff(consecutiveBackoffs uint) time.Duration {
if consecutiveBackoffs < 10 {
return time.Millisecond
}
return 10 * time.Millisecond
}
func tcpBackoff(consecutiveBackoffs uint) time.Duration {
const (
minWait = uint32(time.Microsecond)
maxWait = 5 * uint32(time.Millisecond)
maxShift = 22
)
shifted := minWait << min(consecutiveBackoffs, maxShift)
wait := min(shifted, maxWait)
return time.Duration(wait)
}
// FetchNTP requests the current time from an NTP server IPv4 address over UDP (port 123).
func (ns *NetworkStack) FetchNTP(serverIP string) (time.Time, error) {
ip, err := netip.ParseAddr(serverIP)
if err != nil || !ip.Is4() {
return time.Time{}, fmt.Errorf("invalid IPv4 address: %w", err)
}
ctx, cancel := context.WithTimeout(context.Background(), protoTimeout)
defer cancel()
laddr := net.UDPAddrFromAddrPort(netip.AddrPortFrom(ns.localIP, 0))
raddr := net.UDPAddrFromAddrPort(netip.AddrPortFrom(ip, 123))
const sockDgram = 0x2
c, err := ns.gostack.Socket(ctx, "udp", syscall.AF_INET, sockDgram, laddr, raddr)
if err != nil {
return time.Time{}, fmt.Errorf("udp socket dial: %w", err)
}
conn, ok := c.(net.Conn)
if !ok {
return time.Time{}, errors.New("socket did not return a net.Conn")
}
defer conn.Close()
_ = conn.SetDeadline(time.Now().Add(protoTimeout))
// Standard NTP request packet (48 bytes, leap indicator 0, version 4, mode 3 client = 0x23)
req := make([]byte, 48)
req[0] = 0x23
if _, err := conn.Write(req); err != nil {
return time.Time{}, fmt.Errorf("ntp write error: %w", err)
}
resp := make([]byte, 48)
n, err := conn.Read(resp)
if err != nil {
return time.Time{}, fmt.Errorf("ntp read error: %w", err)
}
if n < 48 {
return time.Time{}, fmt.Errorf("ntp response too short: %d bytes", n)
}
// Transmit timestamp seconds are at bytes 40-43 (big-endian)
secs := uint32(resp[40])<<24 | uint32(resp[41])<<16 | uint32(resp[42])<<8 | uint32(resp[43])
fraction := uint32(resp[44])<<24 | uint32(resp[45])<<16 | uint32(resp[46])<<8 | uint32(resp[47])
// NTP epoch begins Jan 1, 1900. Unix epoch begins Jan 1, 1970.
// Difference is 2,208,988,800 seconds (70 years).
const ntpEpochOffset = 2208988800
if secs < ntpEpochOffset {
return time.Time{}, errors.New("invalid NTP timestamp received")
}
unixSecs := int64(secs - ntpEpochOffset)
nanos := (int64(fraction) * 1e9) >> 32
return time.Unix(unixSecs, nanos), nil
}