diff --git a/examples/net/ntpclient/main.go b/examples/net/ntpclient/main.go index 9ea079ddb..d37026b1b 100644 --- a/examples/net/ntpclient/main.go +++ b/examples/net/ntpclient/main.go @@ -3,7 +3,7 @@ // It creates a UDP connection to request the current time and parse the // response from a NTP server. The system time is set to NTP time. -//go:build ninafw || wioterminal || challenger_rp2040 || comboat_fw +//go:build ninafw || wioterminal || challenger_rp2040 || comboat_fw || arduino_uno_r4_wifi package main diff --git a/examples/net/socket/main.go b/examples/net/socket/main.go index 2e867fd53..b8af9d9d8 100644 --- a/examples/net/socket/main.go +++ b/examples/net/socket/main.go @@ -4,7 +4,7 @@ // // nc -lk 8080 -//go:build ninafw || wioterminal || challenger_rp2040 || comboat_fw +//go:build ninafw || wioterminal || challenger_rp2040 || comboat_fw || arduino_uno_r4_wifi package main diff --git a/examples/net/tcpclient/main.go b/examples/net/tcpclient/main.go index 24f07750c..ddff73c05 100644 --- a/examples/net/tcpclient/main.go +++ b/examples/net/tcpclient/main.go @@ -5,7 +5,7 @@ // // nc -lk 8080 -//go:build ninafw || wioterminal || challenger_rp2040 || comboat_fw +//go:build ninafw || wioterminal || challenger_rp2040 || comboat_fw || arduino_uno_r4_wifi package main diff --git a/examples/net/tcpecho/main.go b/examples/net/tcpecho/main.go index 274dc04d2..ca58c8d17 100644 --- a/examples/net/tcpecho/main.go +++ b/examples/net/tcpecho/main.go @@ -7,7 +7,7 @@ // // $ nc 10.0.0.2 8080 copy ; cmp file copy -//go:build ninafw || wioterminal +//go:build ninafw || wioterminal || arduino_uno_r4_wifi package main diff --git a/examples/net/tlsclient/main.go b/examples/net/tlsclient/main.go index adef22005..4db4478dc 100644 --- a/examples/net/tlsclient/main.go +++ b/examples/net/tlsclient/main.go @@ -5,7 +5,7 @@ // // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security -//go:build ninafw || wioterminal || comboat_fw +//go:build ninafw || wioterminal || comboat_fw || arduino_uno_r4_wifi package main diff --git a/examples/net/webclient/main.go b/examples/net/webclient/main.go index 6b287839d..8a8fc5d09 100644 --- a/examples/net/webclient/main.go +++ b/examples/net/webclient/main.go @@ -17,7 +17,7 @@ // } // --------------------------------------------------------------------------- -//go:build ninafw || wioterminal || comboat_fw +//go:build ninafw || wioterminal || comboat_fw || arduino_uno_r4_wifi package main diff --git a/netlink/probe/unor4wifi.go b/netlink/probe/unor4wifi.go new file mode 100644 index 000000000..0f87846be --- /dev/null +++ b/netlink/probe/unor4wifi.go @@ -0,0 +1,27 @@ +//go:build arduino_uno_r4_wifi + +package probe + +import ( + "machine" + + "tinygo.org/x/drivers/netdev" + "tinygo.org/x/drivers/netlink" + "tinygo.org/x/drivers/unor4wifi" +) + +func Probe() (netlink.Netlinker, netdev.Netdever) { + + cfg := unor4wifi.Config{ + // UART3 is the SCI1 link to the ESP32-S3 AT interface. + Uart: machine.UART3, + Tx: machine.UART3_TX_PIN, + Rx: machine.UART3_RX_PIN, + BaudRate: 115200, + } + + dev := unor4wifi.New(&cfg) + netdev.UseNetdev(dev) + + return dev, dev +} diff --git a/smoketest.sh b/smoketest.sh index b9a5636ba..542a8fa70 100755 --- a/smoketest.sh +++ b/smoketest.sh @@ -194,3 +194,6 @@ tinygo build -size short -o ./build/test.hex -target=wioterminal -stack-size 8kb # network examples (comboat) tinygo build -size short -o ./build/test.hex -target=elecrow-rp2040 -stack-size 8kb ./examples/net/tlsclient/ tinygo build -size short -o ./build/test.hex -target=elecrow-rp2350 -stack-size 8kb ./examples/net/ntpclient/ +# network examples (unor4wifi) +tinygo build -size short -o ./build/test.hex -target=arduino-uno-r4-wifi -stack-size 2kb ./examples/net/tcpclient/ +tinygo build -size short -o ./build/test.hex -target=arduino-uno-r4-wifi -stack-size 2kb ./examples/net/ntpclient/ diff --git a/unor4wifi/README.md b/unor4wifi/README.md new file mode 100644 index 000000000..d14aa21b4 --- /dev/null +++ b/unor4wifi/README.md @@ -0,0 +1,75 @@ +# unor4wifi + +WiFi network driver for the Arduino UNO R4 WiFi. + +The board has a Renesas RA4M1 main MCU and an ESP32-S3 WiFi module. The +ESP32-S3 runs the Arduino [uno-r4-wifi-usb-bridge][bridge] firmware. The +RA4M1 talks to it over UART (SCI1, `machine.UART3`) with an AT style +protocol. This driver speaks that protocol and implements the +[netlink](../netlink) and [netdev](../netdev) interfaces, so the standard +`net` package works. + +Keep the stock bridge firmware on the ESP32-S3. Do not flash the ESP32-S3. + +## Supported + +- WiFi station mode (`ConnectModeSTA`) with WPA2 or open networks +- WiFi access point mode (`ConnectModeAP`) +- TCP client and TCP server +- UDP +- DNS lookup + +## Not supported + +- TLS offload in the ESP32-S3. Use the Go `crypto/tls` package instead. +- Socket options (`SetSockOpt` returns `netdev.ErrNotSupported`). +- BLE. The bridge has HCI pass-through commands, but this driver does not + use them. + +## Usage + +```go +import ( + "tinygo.org/x/drivers/netlink" + "tinygo.org/x/drivers/netlink/probe" +) + +link, _ := probe.Probe() +err := link.NetConnect(&netlink.ConnectParams{ + Ssid: "my-ssid", + Passphrase: "my-passphrase", +}) +``` + +Then use `net.Dial`, `net.Listen` and so on. + +The RA4M1 has 256 KB of flash. The `net/http` package does not fit. +Use the `net` package directly, or a small client such as the `ntpclient` +or `tcpclient` examples. + +Build with `-target=arduino-uno-r4-wifi -stack-size 2kb`. The RA4M1 has +32 KB of RAM and each goroutine gets a stack of the given size, so a larger +stack size leaves too little heap. The network examples that fit in flash +(`socket`, `tcpclient`, `tcpecho`, `ntpclient`, `tlsclient`, `webclient`) +carry the `arduino_uno_r4_wifi` build tag. + +Do not use `io.Copy` with a `net.TCPConn` on this board. It allocates a +32 KB buffer. Use a `Read`/`Write` loop with a small buffer instead. + +## Protocol + +Commands and response formats are defined in the bridge firmware: + +- [commands.h][commands] +- [cmds_wifi_netif.h][netif] (TCP client and server) +- [cmds_wifi_udp.h][udp] + +Text responses look like `+CMD:\r\nOK\r\n`. Binary responses from +`+CLIENTRECEIVE` and `+UDPREAD` look like `+CMD:|OK\r\n`. +Payload for `+CLIENTSEND` and `+UDPWRITE` follows the command line as raw +bytes. + +[bridge]: https://github.com/arduino/uno-r4-wifi-usb-bridge +[commands]: https://github.com/arduino/uno-r4-wifi-usb-bridge/blob/main/UNOR4USBBridge/commands.h +[netif]: https://github.com/arduino/uno-r4-wifi-usb-bridge/blob/main/UNOR4USBBridge/cmds_wifi_netif.h +[udp]: https://github.com/arduino/uno-r4-wifi-usb-bridge/blob/main/UNOR4USBBridge/cmds_wifi_udp.h diff --git a/unor4wifi/debug.go b/unor4wifi/debug.go new file mode 100644 index 000000000..7e42405c6 --- /dev/null +++ b/unor4wifi/debug.go @@ -0,0 +1,16 @@ +package unor4wifi + +type debug uint8 + +const ( + debugBasic debug = 1 << iota // show fw version, mac addr, etc + debugNetdev // show netdev entry points + debugCmd // show AT cmds + + debugOff = 0 + debugAll = debugBasic | debugNetdev | debugCmd +) + +func debugging(want debug) bool { + return (_debug & want) != 0 +} diff --git a/unor4wifi/modem.go b/unor4wifi/modem.go new file mode 100644 index 000000000..4a7178642 --- /dev/null +++ b/unor4wifi/modem.go @@ -0,0 +1,225 @@ +package unor4wifi + +import ( + "errors" + "fmt" + "machine" + "runtime" + "strconv" + "strings" + "time" +) + +var ( + errModemError = errors.New("unor4wifi: modem returned ERROR") + errModemTimeout = errors.New("unor4wifi: modem response timeout") + errModemParse = errors.New("unor4wifi: modem response parse error") +) + +const ( + tokenOK = "OK" + tokenError = "ERROR" +) + +// modem is the AT transport to the ESP32-S3 bridge firmware. +// The protocol is defined in uno-r4-wifi-usb-bridge/UNOR4USBBridge/commands.h. +// Responses have the form "+CMD:\r\nOK\r\n" or "ERROR\r\n". +// Binary responses have the form "+CMD:|OK\r\n". +type modem struct { + uart *machine.UART + timeout time.Duration + line []byte +} + +func (m *modem) readByte(deadline time.Time) (byte, error) { + for m.uart.Buffered() == 0 { + if time.Now().After(deadline) { + return 0, errModemTimeout + } + runtime.Gosched() + } + return m.uart.ReadByte() +} + +// readLine reads a line and strips the trailing CR LF. +func (m *modem) readLine(deadline time.Time) (string, error) { + m.line = m.line[:0] + for { + c, err := m.readByte(deadline) + if err != nil { + return "", err + } + if c == '\n' { + n := len(m.line) + if n > 0 && m.line[n-1] == '\r' { + n-- + } + return string(m.line[:n]), nil + } + m.line = append(m.line, c) + } +} + +func (m *modem) writeString(s string) { + m.uart.Write([]byte(s)) +} + +func (m *modem) flushInput() { + for m.uart.Buffered() > 0 { + m.uart.ReadByte() + } +} + +// waitResult reads response lines until OK or ERROR. +// Data lines are collected into a single string. Multiple data lines are +// joined with "\r\n". +func (m *modem) waitResult(deadline time.Time) (string, error) { + var sb strings.Builder + first := true + for { + line, err := m.readLine(deadline) + if err != nil { + return sb.String(), err + } + if debugging(debugCmd) { + fmt.Printf(" [<--] %s\r\n", line) + } + switch { + case line == tokenOK: + return strings.TrimSpace(sb.String()), nil + case line == tokenError: + return strings.TrimSpace(sb.String()), errModemError + case first && len(line) > 0 && line[0] == '+': + if i := strings.IndexAny(line, ":="); i >= 0 { + line = line[i+1:] + } else { + line = "" + } + sb.WriteString(strings.TrimLeft(line, " ")) + first = false + case first && line == "": + // Skip empty lines before the prompt. + default: + if !first { + sb.WriteString("\r\n") + } + sb.WriteString(line) + first = false + } + } +} + +// exec sends a command and returns the response data. +func (m *modem) exec(cmd string) (string, error) { + return m.execTimeout(cmd, m.timeout) +} + +func (m *modem) execTimeout(cmd string, timeout time.Duration) (string, error) { + m.flushInput() + if debugging(debugCmd) { + fmt.Printf(" [-->] %s\r\n", cmd) + } + m.writeString(cmd) + m.writeString("\r\n") + return m.waitResult(time.Now().Add(timeout)) +} + +// execInt sends a command and parses the response as an integer. +func (m *modem) execInt(cmd string) (int, error) { + res, err := m.exec(cmd) + if err != nil { + return 0, err + } + n, err := strconv.Atoi(res) + if err != nil { + return 0, errModemParse + } + return n, nil +} + +// execSend sends a command followed by raw payload bytes and waits for OK. +func (m *modem) execSend(cmd string, payload []byte) error { + m.flushInput() + if debugging(debugCmd) { + fmt.Printf(" [-->] %s, len(payload): %d\r\n", cmd, len(payload)) + } + m.writeString(cmd) + m.writeString("\r\n") + m.uart.Write(payload) + _, err := m.waitResult(time.Now().Add(m.timeout)) + return err +} + +// execSized sends a command whose response carries binary data in the form +// "+CMD:|". The bytes are stored in dst. Returns the count. +func (m *modem) execSized(cmd string, dst []byte) (int, error) { + m.flushInput() + if debugging(debugCmd) { + fmt.Printf(" [-->] %s\r\n", cmd) + } + m.writeString(cmd) + m.writeString("\r\n") + deadline := time.Now().Add(m.timeout) + + // Find the prompt. A bare ERROR line can arrive instead. + for { + c, err := m.readByte(deadline) + if err != nil { + return 0, err + } + if c == '+' { + break + } + if c == 'E' { + line, err := m.readLine(deadline) + if err != nil { + return 0, err + } + if "E"+line == tokenError { + return 0, errModemError + } + return 0, errModemParse + } + } + // Skip the command name up to ':'. + for { + c, err := m.readByte(deadline) + if err != nil { + return 0, err + } + if c == ':' || c == '=' { + break + } + } + // Read the decimal length up to '|'. + size := 0 + for { + c, err := m.readByte(deadline) + if err != nil { + return 0, err + } + if c == '|' { + break + } + if c < '0' || c > '9' { + if c == ' ' { + continue + } + return 0, errModemParse + } + size = size*10 + int(c-'0') + } + n := 0 + for i := 0; i < size; i++ { + c, err := m.readByte(deadline) + if err != nil { + return n, err + } + if i < len(dst) { + dst[i] = c + n++ + } + } + _, err := m.waitResult(deadline) + return n, err +} diff --git a/unor4wifi/unor4wifi.go b/unor4wifi/unor4wifi.go new file mode 100644 index 000000000..68af0dbfe --- /dev/null +++ b/unor4wifi/unor4wifi.go @@ -0,0 +1,732 @@ +// Package unor4wifi implements a WiFi network driver for the Arduino UNO R4 +// WiFi. The on-board ESP32-S3 runs the Arduino "uno-r4-wifi-usb-bridge" +// firmware. This driver talks to that firmware over UART with the same AT +// style protocol that the Arduino WiFiS3 library uses. +// +// Supported: STA and AP mode, TCP client, TCP server, UDP, DNS lookup. +// Not supported: TLS offload (use Go TLS), socket options. +// +// References: +// https://github.com/arduino/uno-r4-wifi-usb-bridge/blob/main/UNOR4USBBridge/commands.h +// https://github.com/arduino/ArduinoCore-renesas/tree/main/libraries/WiFiS3/src +package unor4wifi // import "tinygo.org/x/drivers/unor4wifi" + +import ( + "fmt" + "io" + "machine" + "net" + "net/netip" + "strconv" + "strings" + "sync" + "time" + + "tinygo.org/x/drivers" + "tinygo.org/x/drivers/netdev" + "tinygo.org/x/drivers/netlink" +) + +var _debug debug = debugBasic + +//var _debug debug = debugBasic | debugNetdev +//var _debug debug = debugBasic | debugNetdev | debugCmd + +var ( + driverName = "TinyGo UNO R4 WiFi (ESP32-S3 bridge) network device driver (unor4wifi)" +) + +const ( + defaultBaudRate = 115200 + defaultTimeout = 10 * time.Second + maxSockets = 8 + // The bridge RX path is a UART, so keep transfers small. + maxChunk = 1024 + // Poll interval while a Recv or Accept waits for data. + pollInterval = 5 * time.Millisecond + // TCP connect timeout passed to the firmware. + connectTimeout = 10 * time.Second + + // WiFi status values, see WiFiS3/src/WiFiTypes.h. + wlIdleStatus = 0 + wlConnected = 3 + wlConnectFailed = 4 + wlConnectionLost = 5 + wlDisconnected = 6 + wlAPListening = 7 + + // ip_type values, see commands.h. + ipAddr = 0 + gatewayAddr = 1 + netmaskAddr = 2 + dns1Addr = 3 + + modeSTA = 1 + modeAP = 2 +) + +type Config struct { + Uart *machine.UART + Tx machine.Pin + Rx machine.Pin + BaudRate uint32 + // Timeout for one modem transaction. Default 10 s. + Timeout time.Duration +} + +type socket struct { + protocol int + // Socket number inside the ESP32 firmware, -1 when not yet allocated. + modemSock int + server bool + laddr netip.AddrPort + raddr netip.AddrPort +} + +type device struct { + cfg *Config + modem modem + mu sync.Mutex + notifyCb func(netlink.Event) + + params *netlink.ConnectParams + netConnected bool + mac net.HardwareAddr + ip netip.Addr + gateway netip.Addr + netmask netip.Addr + dns netip.Addr + + sockets [maxSockets]*socket + rxbuf [maxChunk]byte +} + +// New returns a new driver instance. The UART is configured in NetConnect. +func New(cfg *Config) *device { + d := &device{cfg: cfg} + d.modem.timeout = cfg.Timeout + if d.modem.timeout == 0 { + d.modem.timeout = defaultTimeout + } + return d +} + +// escapeArg escapes the characters that the bridge argument parser treats +// as special. See uno-r4-wifi-usb-bridge/UNOR4USBBridge/parser.cpp, +// ATParser::parse, state Argument. +func escapeArg(s string) string { + if !strings.ContainsAny(s, `\",`) { + return s + } + var sb strings.Builder + for i := 0; i < len(s); i++ { + c := s[i] + if c == '\\' || c == '"' || c == ',' { + sb.WriteByte('\\') + } + sb.WriteByte(c) + } + return sb.String() +} + +func (d *device) setupUART() { + baud := d.cfg.BaudRate + if baud == 0 { + baud = defaultBaudRate + } + d.modem.uart = d.cfg.Uart + d.modem.uart.Configure(machine.UARTConfig{ + BaudRate: baud, + TX: d.cfg.Tx, + RX: d.cfg.Rx, + }) +} + +// reset clears all firmware sockets and WiFi state. Retries until the +// firmware answers. +func (d *device) reset() error { + var err error + for i := 0; i < 10; i++ { + _, err = d.modem.execTimeout("AT+SOFTRESETWIFI", 500*time.Millisecond) + if err == nil { + return nil + } + } + return err +} + +func (d *device) firmwareVersion() string { + res, err := d.modem.exec("AT+FWVERSION?") + if err != nil { + return "unknown" + } + return res +} + +func (d *device) readMAC(cmd string) { + res, err := d.modem.exec(cmd) + if err != nil { + return + } + if mac, err := net.ParseMAC(res); err == nil { + d.mac = mac + } +} + +func (d *device) readAddr(cmd string) netip.Addr { + res, err := d.modem.exec(cmd) + if err != nil { + return netip.Addr{} + } + addr, _ := netip.ParseAddr(strings.TrimSpace(res)) + return addr +} + +func (d *device) status() int { + n, err := d.modem.execInt("AT+GETSTATUS?") + if err != nil { + return wlIdleStatus + } + return n +} + +func (d *device) connectSTA() error { + p := d.params + timeout := p.ConnectTimeout + if timeout == 0 { + timeout = netlink.DefaultConnectTimeout + } + + if _, err := d.modem.exec("AT+WIFIMODE=" + strconv.Itoa(modeSTA)); err != nil { + return err + } + if p.Hostname != "" { + d.modem.exec("AT+HOSTNAME=" + escapeArg(p.Hostname)) + } + + cmd := "AT+BEGINSTA=" + escapeArg(p.Ssid) + if p.Passphrase != "" { + cmd += "," + escapeArg(p.Passphrase) + } + if _, err := d.modem.exec(cmd); err != nil { + return netlink.ErrConnectFailed + } + + deadline := time.Now().Add(timeout) + for time.Now().Before(deadline) { + switch d.status() { + case wlConnected: + // The bridge reports WL_CONNECTED before DHCP completes. + d.ip = d.readAddr("AT+IPSTA=" + strconv.Itoa(ipAddr)) + if !d.ip.IsValid() || d.ip.IsUnspecified() { + break + } + d.gateway = d.readAddr("AT+IPSTA=" + strconv.Itoa(gatewayAddr)) + d.netmask = d.readAddr("AT+IPSTA=" + strconv.Itoa(netmaskAddr)) + d.dns = d.readAddr("AT+IPSTA=" + strconv.Itoa(dns1Addr)) + d.readMAC("AT+MACSTA?") + return nil + case wlConnectFailed: + return netlink.ErrConnectFailed + } + time.Sleep(100 * time.Millisecond) + } + return netlink.ErrConnectTimeout +} + +func (d *device) connectAP() error { + p := d.params + if _, err := d.modem.exec("AT+WIFIMODE=" + strconv.Itoa(modeAP)); err != nil { + return err + } + // Channel 1 is the WiFiS3 default. + cmd := "AT+BEGINSOFTAP=" + escapeArg(p.Ssid) + "," + escapeArg(p.Passphrase) + ",1" + n, err := d.modem.execInt(cmd) + if err != nil || n != wlAPListening { + return netlink.ErrConnectFailed + } + d.ip = d.readAddr("AT+IPSOFTAP") + d.gateway = d.ip + d.readMAC("AT+MACSOFTAP?") + return nil +} + +func (d *device) NetConnect(params *netlink.ConnectParams) error { + d.mu.Lock() + defer d.mu.Unlock() + + if d.netConnected { + return netlink.ErrConnected + } + if len(params.Ssid) == 0 { + return netlink.ErrMissingSSID + } + if params.ConnectMode == netlink.ConnectModeSTA && + params.AuthType != netlink.AuthTypeOpen && len(params.Passphrase) < 8 { + return netlink.ErrShortPassphrase + } + d.params = params + + d.setupUART() + + if debugging(debugBasic) { + fmt.Printf("\r\n") + fmt.Printf("%s\r\n\r\n", driverName) + fmt.Printf("Driver version : %s\r\n", drivers.Version) + } + + if err := d.reset(); err != nil { + return err + } + if debugging(debugBasic) { + fmt.Printf("Bridge firmware version : %s\r\n", d.firmwareVersion()) + } + + var err error + switch params.ConnectMode { + case netlink.ConnectModeSTA: + if debugging(debugBasic) { + fmt.Printf("Connecting to Wifi SSID '%s'...", params.Ssid) + } + err = d.connectSTA() + case netlink.ConnectModeAP: + if debugging(debugBasic) { + fmt.Printf("Starting Wifi AP as SSID '%s'...", params.Ssid) + } + err = d.connectAP() + default: + return netlink.ErrConnectModeNoGood + } + if err != nil { + if debugging(debugBasic) { + fmt.Printf("FAILED (%s)\r\n", err.Error()) + } + return err + } + if debugging(debugBasic) { + fmt.Printf("CONNECTED\r\n") + fmt.Printf("\r\n") + fmt.Printf("MAC address : %s\r\n", d.mac.String()) + fmt.Printf("IP address : %s\r\n", d.ip) + fmt.Printf("Gateway : %s\r\n", d.gateway) + if d.dns.IsValid() { + fmt.Printf("DNS server : %s\r\n", d.dns) + } + fmt.Printf("\r\n") + } + + d.netConnected = true + if d.notifyCb != nil { + d.notifyCb(netlink.EventNetUp) + } + return nil +} + +func (d *device) NetDisconnect() { + d.mu.Lock() + defer d.mu.Unlock() + + if !d.netConnected { + return + } + if d.params.ConnectMode == netlink.ConnectModeAP { + d.modem.exec("AT+DISCONNECTSOFTAP") + } else { + d.modem.exec("AT+DISCONNECT") + } + d.netConnected = false + + if debugging(debugBasic) { + fmt.Printf("\r\nDisconnected from Wifi SSID '%s'\r\n\r\n", d.params.Ssid) + } + + if d.notifyCb != nil { + d.notifyCb(netlink.EventNetDown) + } +} + +func (d *device) NetNotify(cb func(netlink.Event)) { + d.notifyCb = cb +} + +func (d *device) GetHardwareAddr() (net.HardwareAddr, error) { + if debugging(debugNetdev) { + fmt.Printf("[GetHardwareAddr]\r\n") + } + + return d.mac, nil +} + +func (d *device) GetHostByName(name string) (netip.Addr, error) { + if debugging(debugNetdev) { + fmt.Printf("[GetHostByName] name: %s\r\n", name) + } + + if ip, err := netip.ParseAddr(name); err == nil { + return ip, nil + } + + d.mu.Lock() + defer d.mu.Unlock() + + res, err := d.modem.exec("AT+HOSTBYNAME=" + name) + if err != nil { + return netip.Addr{}, netdev.ErrHostUnknown + } + ip, err := netip.ParseAddr(strings.TrimSpace(res)) + if err != nil { + return netip.Addr{}, netdev.ErrHostUnknown + } + return ip, nil +} + +func (d *device) Addr() (netip.Addr, error) { + if debugging(debugNetdev) { + fmt.Printf("[GetIPAddr]\r\n") + } + + return d.ip, nil +} + +func (d *device) getSocket(sockfd int) (*socket, error) { + if sockfd < 0 || sockfd >= maxSockets || d.sockets[sockfd] == nil { + return nil, netdev.ErrInvalidSocketFd + } + return d.sockets[sockfd], nil +} + +func (d *device) newSocket(protocol int, modemSock int) (int, *socket, error) { + for fd := range d.sockets { + if d.sockets[fd] == nil { + s := &socket{protocol: protocol, modemSock: modemSock} + d.sockets[fd] = s + return fd, s, nil + } + } + return -1, nil, netdev.ErrNoMoreSockets +} + +func (d *device) Socket(domain int, stype int, protocol int) (int, error) { + if debugging(debugNetdev) { + fmt.Printf("[Socket] domain: %d, type: %d, protocol: %d\r\n", + domain, stype, protocol) + } + + if domain != netdev.AF_INET { + return -1, netdev.ErrFamilyNotSupported + } + switch { + case protocol == netdev.IPPROTO_TCP && stype == netdev.SOCK_STREAM: + case protocol == netdev.IPPROTO_UDP && stype == netdev.SOCK_DGRAM: + default: + return -1, netdev.ErrProtocolNotSupported + } + + d.mu.Lock() + defer d.mu.Unlock() + + fd, _, err := d.newSocket(protocol, -1) + return fd, err +} + +// udpBegin opens the firmware UDP socket on the local port, once. +func (d *device) udpBegin(s *socket) error { + if s.modemSock >= 0 { + return nil + } + n, err := d.modem.execInt("AT+UDPBEGIN=" + strconv.Itoa(int(s.laddr.Port()))) + if err != nil { + return err + } + s.modemSock = n + return nil +} + +func (d *device) Bind(sockfd int, ip netip.AddrPort) error { + if debugging(debugNetdev) { + fmt.Printf("[Bind] sockfd: %d, addr: %s:%d\r\n", sockfd, ip.Addr(), ip.Port()) + } + + d.mu.Lock() + defer d.mu.Unlock() + + s, err := d.getSocket(sockfd) + if err != nil { + return err + } + s.laddr = ip + if s.protocol == netdev.IPPROTO_UDP { + return d.udpBegin(s) + } + return nil +} + +func (d *device) Connect(sockfd int, host string, ip netip.AddrPort) error { + if debugging(debugNetdev) { + fmt.Printf("[Connect] sockfd: %d, host: %s, addr: %s\r\n", sockfd, host, ip) + } + + d.mu.Lock() + defer d.mu.Unlock() + + s, err := d.getSocket(sockfd) + if err != nil { + return err + } + s.raddr = ip + + if s.protocol == netdev.IPPROTO_UDP { + return d.udpBegin(s) + } + + if s.modemSock < 0 { + n, err := d.modem.execInt("AT+BEGINCLIENT") + if err != nil { + return err + } + s.modemSock = n + } + + // The firmware resolves names and dotted addresses through the same + // path, so always use +CLIENTCONNECT like the Arduino WiFiS3 library. + if host == "" { + host = ip.Addr().String() + } + cmd := "AT+CLIENTCONNECT=" + strconv.Itoa(s.modemSock) + "," + host + "," + + strconv.Itoa(int(ip.Port())) + "," + strconv.Itoa(int(connectTimeout/time.Millisecond)) + if _, err := d.modem.execTimeout(cmd, connectTimeout+d.modem.timeout); err != nil { + return netlink.ErrConnectFailed + } + return nil +} + +func (d *device) Listen(sockfd int, backlog int) error { + if debugging(debugNetdev) { + fmt.Printf("[Listen] sockfd: %d\r\n", sockfd) + } + + d.mu.Lock() + defer d.mu.Unlock() + + s, err := d.getSocket(sockfd) + if err != nil { + return err + } + if s.protocol != netdev.IPPROTO_TCP { + return netdev.ErrProtocolNotSupported + } + n, err := d.modem.execInt("AT+BEGINSERVER=" + strconv.Itoa(int(s.laddr.Port()))) + if err != nil { + return err + } + s.modemSock = n + s.server = true + return nil +} + +func (d *device) remoteAddr(sock string) netip.AddrPort { + ipStr, err := d.modem.exec("AT+REMOTEIP=" + sock) + if err != nil { + return netip.AddrPort{} + } + port, err := d.modem.execInt("AT+REMOTEPORT=" + sock) + if err != nil { + return netip.AddrPort{} + } + ip, _ := netip.ParseAddr(strings.TrimSpace(ipStr)) + return netip.AddrPortFrom(ip, uint16(port)) +} + +func (d *device) Accept(sockfd int) (int, netip.AddrPort, error) { + if debugging(debugNetdev) { + fmt.Printf("[Accept] sockfd: %d\r\n", sockfd) + } + + for { + d.mu.Lock() + s, err := d.getSocket(sockfd) + if err != nil { + d.mu.Unlock() + return -1, netip.AddrPort{}, err + } + if !s.server { + d.mu.Unlock() + return -1, netip.AddrPort{}, netdev.ErrNotSupported + } + n, err := d.modem.execInt("AT+SERVERACCEPT=" + strconv.Itoa(s.modemSock)) + if err == nil && n >= 0 { + fd, cs, err := d.newSocket(netdev.IPPROTO_TCP, n) + if err != nil { + d.mu.Unlock() + return -1, netip.AddrPort{}, err + } + cs.laddr = s.laddr + cs.raddr = d.remoteAddr(strconv.Itoa(n)) + d.mu.Unlock() + return fd, cs.raddr, nil + } + d.mu.Unlock() + time.Sleep(pollInterval) + } +} + +func (d *device) sendTCP(s *socket, buf []byte) (int, error) { + sock := strconv.Itoa(s.modemSock) + sent := 0 + for sent < len(buf) { + end := sent + maxChunk + if end > len(buf) { + end = len(buf) + } + chunk := buf[sent:end] + cmd := "AT+CLIENTSEND=" + sock + "," + strconv.Itoa(len(chunk)) + if err := d.modem.execSend(cmd, chunk); err != nil { + return sent, err + } + sent += len(chunk) + } + return sent, nil +} + +func (d *device) sendUDP(s *socket, buf []byte) (int, error) { + sock := strconv.Itoa(s.modemSock) + cmd := "AT+UDPBEGINPACKETIP=" + sock + "," + + strconv.Itoa(int(s.raddr.Port())) + "," + s.raddr.Addr().String() + if _, err := d.modem.exec(cmd); err != nil { + return 0, err + } + cmd = "AT+UDPWRITE=" + sock + "," + strconv.Itoa(len(buf)) + if err := d.modem.execSend(cmd, buf); err != nil { + return 0, err + } + if _, err := d.modem.exec("AT+UDPENDPACKET=" + sock); err != nil { + return 0, err + } + return len(buf), nil +} + +func (d *device) Send(sockfd int, buf []byte, flags int, deadline time.Time) (int, error) { + if debugging(debugNetdev) { + fmt.Printf("[Send] sockfd: %d, len(buf): %d, flags: %d\r\n", + sockfd, len(buf), flags) + } + + d.mu.Lock() + defer d.mu.Unlock() + + s, err := d.getSocket(sockfd) + if err != nil { + return 0, err + } + if s.modemSock < 0 { + return 0, netdev.ErrInvalidSocketFd + } + if s.protocol == netdev.IPPROTO_UDP { + return d.sendUDP(s, buf) + } + return d.sendTCP(s, buf) +} + +// recvOnce performs one receive attempt. Returns 0, nil when no data is +// available yet, and io.EOF when the connection is closed. +func (d *device) recvOnce(s *socket, buf []byte) (int, error) { + sock := strconv.Itoa(s.modemSock) + want := len(buf) + if want > maxChunk { + want = maxChunk + } + + if s.protocol == netdev.IPPROTO_UDP { + n, err := d.modem.execInt("AT+UDPPARSE=" + sock) + if err != nil { + return 0, err + } + if n == 0 { + return 0, nil + } + if n < want { + want = n + } + return d.modem.execSized("AT+UDPREAD="+sock+","+strconv.Itoa(want), buf) + } + + n, err := d.modem.execSized("AT+CLIENTRECEIVE="+sock+","+strconv.Itoa(want), buf) + if err != nil { + return 0, err + } + if n > 0 { + return n, nil + } + connected, err := d.modem.execInt("AT+CLIENTCONNECTED=" + sock) + if err != nil || connected == 0 { + return 0, io.EOF + } + return 0, nil +} + +func (d *device) Recv(sockfd int, buf []byte, flags int, deadline time.Time) (int, error) { + if debugging(debugNetdev) { + fmt.Printf("[Recv] sockfd: %d, len(buf): %d, flags: %d\r\n", + sockfd, len(buf), flags) + } + + for { + d.mu.Lock() + s, err := d.getSocket(sockfd) + if err != nil { + d.mu.Unlock() + return 0, err + } + if s.modemSock < 0 { + d.mu.Unlock() + return 0, netdev.ErrInvalidSocketFd + } + n, err := d.recvOnce(s, buf) + d.mu.Unlock() + if err != nil || n > 0 { + return n, err + } + if !deadline.IsZero() && time.Now().After(deadline) { + return 0, netdev.ErrTimeout + } + time.Sleep(pollInterval) + } +} + +func (d *device) Close(sockfd int) error { + if debugging(debugNetdev) { + fmt.Printf("[Close] sockfd: %d\r\n", sockfd) + } + + d.mu.Lock() + defer d.mu.Unlock() + + s, err := d.getSocket(sockfd) + if err != nil { + return err + } + if s.modemSock >= 0 { + sock := strconv.Itoa(s.modemSock) + switch { + case s.protocol == netdev.IPPROTO_UDP: + _, err = d.modem.exec("AT+UDPSTOP=" + sock) + case s.server: + _, err = d.modem.exec("AT+SERVEREND=" + sock) + default: + _, err = d.modem.exec("AT+CLIENTCLOSE=" + sock) + } + } + d.sockets[sockfd] = nil + if err != nil { + return netdev.ErrClosingSocket + } + return nil +} + +func (d *device) SetSockOpt(sockfd int, level int, opt int, value interface{}) error { + if debugging(debugNetdev) { + fmt.Printf("[SetSockOpt] sockfd: %d\r\n", sockfd) + } + + return netdev.ErrNotSupported +}