diff --git a/examples/wifinina/ntpclient/main.go b/examples/wifinina/ntpclient/main.go new file mode 100644 index 0000000..37d401b --- /dev/null +++ b/examples/wifinina/ntpclient/main.go @@ -0,0 +1,166 @@ +// This is an example of using the wifinina driver to implement a NTP client. +// It creates a UDP connection to request the current time and parse the +// response from a NTP server. +package main + +import ( + "errors" + "fmt" + "machine" + "runtime" + "time" + + "tinygo.org/x/drivers/net" + "tinygo.org/x/drivers/wifinina" +) + +// access point info +const ssid = "" +const pass = "" + +// IP address of the server aka "hub". Replace with your own info. +const ntpHost = "129.6.15.29" + +const NTP_PACKET_SIZE = 48 + +var ( + + // this is the ESP chip that has the WIFININA firmware flashed on it + // these are the default pins for the Arduino Nano33 IoT. + adaptor = wifinina.Device{ + SPI: machine.NINA_SPI, + CS: machine.NINA_CS, + ACK: machine.NINA_ACK, + GPIO0: machine.NINA_GPIO0, + RESET: machine.NINA_RESETN, + } + + b = make([]byte, NTP_PACKET_SIZE) + + console = machine.UART0 +) + +func main() { + + // Init esp32 + // Configure SPI for 8Mhz, Mode 0, MSB First + machine.NINA_SPI.Configure(machine.SPIConfig{ + Frequency: 8 * 1e6, + MOSI: machine.NINA_MOSI, + MISO: machine.NINA_MISO, + SCK: machine.NINA_SCK, + }) + adaptor.Configure() + + // connect to access point + connectToAP() + + // now make UDP connection + ip := net.ParseIP(ntpHost) + raddr := &net.UDPAddr{IP: ip, Port: 123} + laddr := &net.UDPAddr{Port: 2390} + conn, err := net.DialUDP("udp", laddr, raddr) + if err != nil { + for { + time.Sleep(time.Second) + println(err) + } + } + + for { + // send data + println("Requesting NTP time...") + t, err := getCurrentTime(conn) + if err != nil { + message("Error getting current time: %v", err) + } else { + message("NTP time: %v", t) + } + runtime.AdjustTimeOffset(-1 * int64(time.Since(t))) + for i := 0; i < 10; i++ { + message("Current time: %v", time.Now()) + time.Sleep(1 * time.Second) + } + } + + // Right now this code is never reached. Need a way to trigger it... + println("Disconnecting UDP...") + conn.Close() + println("Done.") +} + +func getCurrentTime(conn *net.UDPSerialConn) (time.Time, error) { + if err := sendNTPpacket(conn); err != nil { + return time.Time{}, err + } + clearBuffer() + for now := time.Now(); time.Since(now) < time.Second; { + time.Sleep(5 * time.Millisecond) + if n, err := conn.Read(b); err != nil { + return time.Time{}, fmt.Errorf("error reading UDP packet: %w", err) + } else if n == 0 { + continue // no packet received yet + } else if n != NTP_PACKET_SIZE { + if n != NTP_PACKET_SIZE { + return time.Time{}, fmt.Errorf("expected NTP packet size of %d: %d", NTP_PACKET_SIZE, n) + } + } + return parseNTPpacket(), nil + } + return time.Time{}, errors.New("no packet received after 1 second") +} + +func sendNTPpacket(conn *net.UDPSerialConn) error { + clearBuffer() + b[0] = 0b11100011 // LI, Version, Mode + b[1] = 0 // Stratum, or type of clock + b[2] = 6 // Polling Interval + b[3] = 0xEC // Peer Clock Precision + // 8 bytes of zero for Root Delay & Root Dispersion + b[12] = 49 + b[13] = 0x4E + b[14] = 49 + b[15] = 52 + if _, err := conn.Write(b); err != nil { + return err + } + return nil +} + +func parseNTPpacket() time.Time { + // the timestamp starts at byte 40 of the received packet and is four bytes, + // this is NTP time (seconds since Jan 1 1900): + t := uint32(b[40])<<24 | uint32(b[41])<<16 | uint32(b[42])<<8 | uint32(b[43]) + const seventyYears = 2208988800 + return time.Unix(int64(t-seventyYears), 0) +} + +func clearBuffer() { + for i := range b { + b[i] = 0 + } +} + +// connect to access point +func connectToAP() { + time.Sleep(2 * time.Second) + message("Connecting to " + ssid) + adaptor.SetPassphrase(ssid, pass) + for st, _ := adaptor.GetConnectionStatus(); st != wifinina.StatusConnected; { + message("Connection status: " + st.String()) + time.Sleep(1 * time.Second) + st, _ = adaptor.GetConnectionStatus() + } + message("Connected.") + time.Sleep(2 * time.Second) + ip, _, _, err := adaptor.GetIP() + for ; err != nil; ip, _, _, err = adaptor.GetIP() { + message(err.Error()) + time.Sleep(1 * time.Second) + } + message(ip.String()) +} + +func message(format string, args ...interface{}) { + println(fmt.Sprintf(format, args...), "\r") +} diff --git a/examples/wifinina/udpstation/main.go b/examples/wifinina/udpstation/main.go new file mode 100644 index 0000000..ee7ede5 --- /dev/null +++ b/examples/wifinina/udpstation/main.go @@ -0,0 +1,100 @@ +// This is a sensor station that uses a ESP32 running nina-fw over SPI. +// It creates a UDP connection you can use to get info to/from your computer via the microcontroller. +// +// In other words: +// Your computer <--> UART0 <--> MCU <--> SPI <--> ESP32 +// +package main + +import ( + "machine" + "strconv" + "time" + + "tinygo.org/x/drivers/net" + "tinygo.org/x/drivers/wifinina" +) + +// access point info +const ssid = "" +const pass = "" + +// IP address of the server aka "hub". Replace with your own info. +const hubIP = "" + +// these are the default pins for the Arduino Nano33 IoT. +// change these to connect to a different UART or pins for the ESP8266/ESP32 +var ( + + // this is the ESP chip that has the WIFININA firmware flashed on it + // these are the default pins for the Arduino Nano33 IoT. + adaptor = &wifinina.Device{ + SPI: machine.NINA_SPI, + CS: machine.NINA_CS, + ACK: machine.NINA_ACK, + GPIO0: machine.NINA_GPIO0, + RESET: machine.NINA_RESETN, + } +) + +func main() { + + // Init esp8266/esp32 + // Configure SPI for 8Mhz, Mode 0, MSB First + machine.NINA_SPI.Configure(machine.SPIConfig{ + Frequency: 8 * 1e6, + MOSI: machine.NINA_MOSI, + MISO: machine.NINA_MISO, + SCK: machine.NINA_SCK, + }) + adaptor.Configure() + + // connect to access point + connectToAP() + + // now make UDP connection + ip := net.ParseIP(hubIP) + raddr := &net.UDPAddr{IP: ip, Port: 2222} + laddr := &net.UDPAddr{Port: 2222} + + println("Dialing UDP connection...") + conn, _ := net.DialUDP("udp", laddr, raddr) + + for { + // send data + println("Sending data...") + for i := 0; i < 25; i++ { + conn.Write([]byte("hello " + strconv.Itoa(i) + "\r\n")) + } + time.Sleep(1000 * time.Millisecond) + } + + // Right now this code is never reached. Need a way to trigger it... + println("Disconnecting UDP...") + conn.Close() + println("Done.") +} + +// connect to access point +func connectToAP() { + time.Sleep(2 * time.Second) + message("Connecting to " + ssid) + adaptor.SetPassphrase(ssid, pass) + for st, _ := adaptor.GetConnectionStatus(); st != wifinina.StatusConnected; { + message("Connection status: " + st.String()) + time.Sleep(1 * time.Second) + st, _ = adaptor.GetConnectionStatus() + } + message("Connected.") + time.Sleep(2 * time.Second) + ip, _, _, err := adaptor.GetIP() + for ; err != nil; ip, _, _, err = adaptor.GetIP() { + message(err.Error()) + time.Sleep(1 * time.Second) + } + message(ip.String()) +} + +func message(msg string) { + println(msg, "\r") +} diff --git a/wifinina/tcp.go b/wifinina/tcp.go index 9a7d32e..d5c0c06 100644 --- a/wifinina/tcp.go +++ b/wifinina/tcp.go @@ -20,6 +20,10 @@ type Driver struct { dev *Device sock uint8 readBuf readBuffer + + proto uint8 + ip uint32 + port uint16 } type readBuffer struct { @@ -43,6 +47,8 @@ func (drv *Driver) ConnectSSLSocket(addr, portStr string) error { func (drv *Driver) connectSocket(addr, portStr string, mode uint8) error { + drv.proto, drv.ip, drv.port = mode, 0, 0 + // convert port to uint16 p64, err := strconv.ParseUint(portStr, 10, 16) if err != nil { @@ -90,8 +96,56 @@ func (drv *Driver) connectSocket(addr, portStr string, mode uint8) error { return ErrConnectionTimeout } -func (drv *Driver) ConnectUDPSocket(addr, sport, lport string) error { - return ErrNotImplemented +func convertPort(portStr string) (uint16, error) { + p64, err := strconv.ParseUint(portStr, 10, 16) + if err != nil { + return 0, fmt.Errorf("could not convert port to uint16: %w", err) + } + return uint16(p64), nil +} + +func (drv *Driver) ConnectUDPSocket(addr, portStr, lportStr string) (err error) { + + drv.proto, drv.ip, drv.port = ProtoModeUDP, 0, 0 + + // convert remote port to uint16 + if drv.port, err = convertPort(portStr); err != nil { + return err + } + + // convert local port to uint16 + var lport uint16 + if lport, err = convertPort(lportStr); err != nil { + return err + } + + // look up the hostname if necessary; if an IP address was specified, the + // same will be returned. Otherwise, an IPv4 for the hostname is returned. + ipAddr, err := drv.dev.GetHostByName(addr) + if err != nil { + return err + } + drv.ip = ipAddr.AsUint32() + + // check to see if socket is already set; if so, stop it + // TODO: we can probably have more than one socket at once right? + if drv.sock != NoSocketAvail { + if err := drv.stop(); err != nil { + return err + } + } + + // get a socket from the device + if drv.sock, err = drv.dev.GetSocket(); err != nil { + return err + } + + // start listening for UDP packets on the local port + if err := drv.dev.StartServer(lport, drv.sock, drv.proto); err != nil { + return err + } + + return nil } func (drv *Driver) DisconnectSocket() error { @@ -114,16 +168,31 @@ func (drv *Driver) Write(b []byte) (n int, err error) { if len(b) == 0 { return 0, ErrNoData } - written, err := drv.dev.SendData(b, drv.sock) - if err != nil { - return 0, err - } - if written == 0 { - return 0, ErrDataNotWritten - } - if sent, _ := drv.dev.CheckDataSent(drv.sock); !sent { - return 0, ErrCheckDataError + if drv.proto == ProtoModeUDP { + if err := drv.dev.StartClient(drv.ip, drv.port, drv.sock, drv.proto); err != nil { + return 0, fmt.Errorf("error in startClient: %w", err) + } + if _, err := drv.dev.InsertDataBuf(b, drv.sock); err != nil { + return 0, fmt.Errorf("error in insertDataBuf: %w", err) + } + if _, err := drv.dev.SendUDPData(drv.sock); err != nil { + return 0, fmt.Errorf("error in sendUDPData: %w", err) + } + return len(b), nil + } else { + written, err := drv.dev.SendData(b, drv.sock) + if err != nil { + return 0, err + } + if written == 0 { + return 0, ErrDataNotWritten + } + if sent, _ := drv.dev.CheckDataSent(drv.sock); !sent { + return 0, ErrCheckDataError + } + return len(b), nil } + return len(b), nil } diff --git a/wifinina/wifinina.go b/wifinina/wifinina.go index adfb32d..b82ac32 100644 --- a/wifinina/wifinina.go +++ b/wifinina/wifinina.go @@ -375,6 +375,50 @@ func (d *Device) StopClient(sock uint8) error { return err } +func (d *Device) StartServer(port uint16, sock uint8, mode uint8) error { + if err := d.waitForSlaveSelect(); err != nil { + d.spiSlaveDeselect() + return err + } + l := d.sendCmd(CmdStartServerTCP, 3) + l += d.sendParam16(port, false) + l += d.sendParam8(sock, false) + l += d.sendParam8(mode, true) + d.addPadding(l) + d.spiSlaveDeselect() + _, err := d.waitRspCmd1(CmdStartClientTCP) + return err +} + +// InsertDataBuf adds data to the buffer used for sending UDP data +func (d *Device) InsertDataBuf(buf []byte, sock uint8) (bool, error) { + if err := d.waitForSlaveSelect(); err != nil { + d.spiSlaveDeselect() + return false, err + } + l := d.sendCmd(CmdInsertDataBuf, 2) + l += d.sendParamBuf([]byte{sock}, false) + l += d.sendParamBuf(buf, true) + d.addPadding(l) + d.spiSlaveDeselect() + n, err := d.getUint8(d.waitRspCmd1(CmdInsertDataBuf)) + return n == 1, err +} + +// SendUDPData sends the data previously added to the UDP buffer +func (d *Device) SendUDPData(sock uint8) (bool, error) { + if err := d.waitForSlaveSelect(); err != nil { + d.spiSlaveDeselect() + return false, err + } + l := d.sendCmd(CmdSendDataUDP, 1) + l += d.sendParam8(sock, true) + d.addPadding(l) + d.spiSlaveDeselect() + n, err := d.getUint8(d.waitRspCmd1(CmdSendDataUDP)) + return n == 1, err +} + // ---------- /client methods (should this be a separate struct?) ------------ /*