From 0ec887c9d81687059e616c0713248c585d5e2e40 Mon Sep 17 00:00:00 2001 From: Scott Feldman Date: Mon, 14 Nov 2022 13:24:15 -0800 Subject: [PATCH] wifinina: add support for http server (#480) * wifinina: add support for http server This adds http.ListenAndServe() implementation for wifinina. Included is an example webserver, basically copied from the rtl8720dn/webserver. Tested on Arduino Nano RP2040 Connect. Also tried testing on Arduino Nano33 IoT but test panics with "out of memory". There's only 32K of SRAM on the Nano33, vs 264K on the rp2040 Connect. --- examples/wifinina/webserver/main.go | 209 ++++++++++++++++++ wifinina/http.go | 323 ++++++++++++++++++++++++++++ 2 files changed, 532 insertions(+) create mode 100644 examples/wifinina/webserver/main.go create mode 100644 wifinina/http.go diff --git a/examples/wifinina/webserver/main.go b/examples/wifinina/webserver/main.go new file mode 100644 index 0000000..f8bf65e --- /dev/null +++ b/examples/wifinina/webserver/main.go @@ -0,0 +1,209 @@ +package main + +import ( + "fmt" + "machine" + "strconv" + "time" + + "tinygo.org/x/drivers/net/http" + "tinygo.org/x/drivers/wifinina" +) + +// You can override the settings with the init() in another source code: +// +// func init() { +// ssid = "your-ssid" +// pass = "your-password" +// } +// +// Or use -ldflags option on tinygo command to set at compile-time: +// +// tinygo flash ... -ldflags '-X "main.ssid=xxx" -X "main.pass=xxx"' ... +// + +var ( + ssid string + pass string +) + +var led = machine.LED + +func main() { + led.Configure(machine.PinConfig{Mode: machine.PinOutput}) + + err := run() + for err != nil { + fmt.Printf("error: %s\r\n", err.Error()) + time.Sleep(5 * time.Second) + } +} + +func run() error { + + spi := machine.NINA_SPI + spi.Configure(machine.SPIConfig{ + Frequency: 8 * 1e6, + SDO: machine.NINA_SDO, + SDI: machine.NINA_SDI, + SCK: machine.NINA_SCK, + }) + + adaptor := wifinina.New(spi, + machine.NINA_CS, + machine.NINA_ACK, + machine.NINA_GPIO0, + machine.NINA_RESETN) + adaptor.Configure() + + time.Sleep(2 * time.Second) + println("Connecting to " + ssid) + err := adaptor.ConnectToAccessPoint(ssid, pass, 10*time.Second) + if err != nil { + return err + } + + println("Connected.") + + time.Sleep(2 * time.Second) + ip, subnet, gateway, err := adaptor.GetIP() + if err != nil { + return err + } + + fmt.Printf("IP Address : %s\r\n", ip) + fmt.Printf("Mask : %s\r\n", subnet) + fmt.Printf("Gateway : %s\r\n", gateway) + + http.UseDriver(adaptor) + + http.HandleFunc("/", root) + http.HandleFunc("/hello", hello) + http.HandleFunc("/cnt", cnt) + http.HandleFunc("/6", sixlines) + http.HandleFunc("/off", LED_OFF) + http.HandleFunc("/on", LED_ON) + + return http.ListenAndServe(":80", nil) +} + +func root(w http.ResponseWriter, r *http.Request) { + access := 1 + + cookie, err := r.Cookie("access") + if err != nil { + if err == http.ErrNoCookie { + cookie = &http.Cookie{ + Name: "access", + Value: "1", + } + } else { + http.Error(w, fmt.Sprintf("%s", err.Error()), http.StatusBadRequest) + return + } + } else { + v, err := strconv.ParseInt(cookie.Value, 10, 0) + if err != nil { + http.Error(w, fmt.Sprintf("invalid cookie.Value : %s", cookie.Value), http.StatusBadRequest) + return + } + cookie.Value = fmt.Sprintf("%d", v+1) + access = int(v) + 1 + } + http.SetCookie(w, cookie) + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` + + + TinyGo HTTP Server + + + +
TinyGo HTTP Server
+ +

+ access: %d +

+ + /hello
+ /6
+ +

+ LED
+ /on
+ /off
+

+ + +

+ /cnt
+ cnt:
+ incrCnt()
+

+ + +
+

+ + + `, access) +} + +func sixlines(w http.ResponseWriter, r *http.Request) { + // https://fukuno.jig.jp/3267 + fmt.Fprint(w, ``) +} + +func LED_ON(w http.ResponseWriter, r *http.Request) { + led.High() + w.Header().Set(`Content-Type`, `text/plain; charset=UTF-8`) + fmt.Fprintf(w, "led.High()") +} + +func LED_OFF(w http.ResponseWriter, r *http.Request) { + led.Low() + w.Header().Set(`Content-Type`, `text/plain; charset=UTF-8`) + fmt.Fprintf(w, "led.Low()") +} + +func hello(w http.ResponseWriter, r *http.Request) { + w.Header().Set(`Content-Type`, `text/plain; charset=UTF-8`) + fmt.Fprintf(w, "hello") +} + +var counter int + +func cnt(w http.ResponseWriter, r *http.Request) { + r.ParseForm() + if r.Method == "POST" { + c := r.Form.Get("cnt") + if c != "" { + i64, _ := strconv.ParseInt(c, 0, 0) + counter = int(i64) + } + } + + w.Header().Set(`Content-Type`, `application/json`) + fmt.Fprintf(w, `{"cnt": %d}`, counter) +} + +func message(msg string) { + println(msg, "\r") +} diff --git a/wifinina/http.go b/wifinina/http.go new file mode 100644 index 0000000..e1358a0 --- /dev/null +++ b/wifinina/http.go @@ -0,0 +1,323 @@ +package wifinina + +import ( + "bufio" + "bytes" + "fmt" + "io" + "strconv" + "strings" + "time" + + "tinygo.org/x/drivers/net/http" +) + +func (d *Device) ListenAndServe(addr string, handler http.Handler) error { + + if handler == nil { + handler = http.DefaultServeMux + } + + server := newServer(d, handler) + + if err := server.listen(addr); err != nil { + return err + } + + for { + client, err := server.accept() + if err != nil { + return err + } + + if err := client.handleHTTP(); err != nil { + return err + } + + if err = client.stop(); err != nil { + return err + } + } + + return nil +} + +// Server stuff + +type server struct { + device *Device + handler http.Handler + sock uint8 + clients map[uint8]*client // keyed by client sock +} + +func newServer(device *Device, handler http.Handler) *server { + return &server{ + device: device, + handler: handler, + sock: NoSocketAvail, + clients: make(map[uint8]*client), + } +} + +func portFromAddr(addr string) (uint16, error) { + // ignore anything before ':' in address + i := strings.LastIndex(addr, ":") + if i < 0 { + return 0, fmt.Errorf("Missing ':' in address") + } + v, err := strconv.ParseUint(addr[i+1:], 10, 16) + if err != nil { + return 0, fmt.Errorf("Parsing address err: %s", err) + } + return uint16(v), nil +} + +func (s *server) listen(addr string) error { + port, err := portFromAddr(addr) + if err != nil { + return fmt.Errorf("Getting port err: %s", err) + } + + s.sock, err = s.device.GetSocket() + if err != nil { + return fmt.Errorf("Getting socket err: %s", err) + } + if s.sock == NoSocketAvail { + return fmt.Errorf("No socket available") + } + + return s.device.StartServer(port, s.sock, ProtoModeTCP) +} + +func (s *server) availServer(sock uint8) (uint8, error) { + d := s.device + + d.mu.Lock() + defer d.mu.Unlock() + + if err := d.waitForChipSelect(); err != nil { + d.spiChipDeselect() + return NoSocketAvail, fmt.Errorf("Wait for CS: %s", err) + } + + l := d.sendCmd(CmdAvailDataTCP, 1) + l += d.sendParam8(sock, true) + d.addPadding(l) + d.spiChipDeselect() + _, err := d.waitRspCmd1(CmdAvailDataTCP) + if err != nil { + return NoSocketAvail, fmt.Errorf("Wait for Rsp: %s", err) + } + newsock, err := d.getUint16(2, err) + if err != nil { + return NoSocketAvail, fmt.Errorf("getUint16: %s", err) + } + return uint8(newsock >> 8), nil +} + +func (s *server) accept() (*client, error) { + + for { + sock, err := s.availServer(s.sock) + if err != nil { + return nil, fmt.Errorf("accept: %w", err) + } + + if sock == NoSocketAvail { + continue + } + + if client, ok := s.clients[sock]; ok { + return client, nil + } + + client := newClient(s, sock) + s.clients[sock] = client + + return client, nil + } +} + +// client stuff + +type client struct { + server *server + device *Device + sock uint8 + + // HTTP request + req *http.Request + reqBuf bytes.Buffer + readBuf [256]byte + + // HTTP response + res bytes.Buffer + resHdr http.Header + resBuf bytes.Buffer + statusCode int +} + +func newClient(server *server, sock uint8) *client { + return &client{ + server: server, + device: server.device, + sock: sock, + } +} + +// client implements http.ResponseWriter interface + +func (c *client) Header() http.Header { + return c.resHdr +} + +func (c *client) Write(b []byte) (int, error) { + return c.resBuf.Write(b) +} + +func (c *client) WriteHeader(statusCode int) { + c.statusCode = statusCode +} + +func (c *client) status() uint8 { + d := c.device + + d.mu.Lock() + defer d.mu.Unlock() + + if err := d.waitForChipSelect(); err != nil { + d.spiChipDeselect() + return 0 + } + + l := d.sendCmd(CmdGetClientStateTCP, 1) + l += d.sendParam8(c.sock, true) + d.addPadding(l) + d.spiChipDeselect() + _, err := d.waitRspCmd1(CmdGetClientStateTCP) + if err != nil { + return 0 + } + status, err := d.getUint8(1, err) + if err != nil { + return 0 + } + return status +} + +func (c *client) stop() error { + if err := c.device.StopClient(c.sock); err != nil { + return err + } + + // Wait max 5 secs for the connection to close + for i := 0; i < 50 && c.status() != TCPStateClosed; i++ { + time.Sleep(100 * time.Millisecond) + } + + if c.status() != TCPStateClosed { + return fmt.Errorf("stop failed, client status %x", c.status()) + } + + return nil +} + +func (c *client) handleHTTP() error { + + c.reqBuf.Reset() + end := -1 + + // read the request + + start := time.Now() + for { + + // TODO use Server.ReadTimeout + if time.Since(start) > 1*time.Second { + return fmt.Errorf("ReadTimeout") + } + + n, err := c.device.GetDataBuf(c.sock, c.readBuf[:]) + if err != nil { + return fmt.Errorf("GetDataBuf: %s", err) + } + if n == 0 { + time.Sleep(1 * time.Millisecond) + continue + } + + c.reqBuf.Write(c.readBuf[:n]) + bytesSoFar := c.reqBuf.Bytes() + + if end == -1 { + + // search for blank line marking end-of-header + end = bytes.Index(bytesSoFar, []byte("\r\n\r\n")) + if end == -1 { + continue + } + + // found end-of-header; parse header + end += len([]byte("\r\n\r\n")) + bufio := bufio.NewReader(bytes.NewReader(bytesSoFar[:end])) + c.req, err = http.ReadRequest(bufio) + if err != nil { + return err + } + } + + v := c.req.Header.Get("Content-Length") + if v == "" { + // no body; we're done reading request + break + } + + length, _ := strconv.Atoi(v) + if end+length == len(bytesSoFar) { + // got the whole body + body := bytes.NewReader(bytesSoFar[end:]) + c.req.Body = io.NopCloser(body) + break + } + + // continue reading request... + } + + // build the response + + c.statusCode = 200 + + c.resHdr = http.Header{} + c.resHdr.Add(`Content-Type`, `text/html; charset=UTF-8`) + c.resHdr.Add(`Connection`, `close`) + + c.resBuf.Reset() + c.server.handler.ServeHTTP(c, c.req) + + c.resHdr.Add(`Content-Length`, fmt.Sprintf("%d", c.resBuf.Len())) + + c.res.Reset() + fmt.Fprintf(&c.res, "HTTP/1.1 %d %s\r\n", c.statusCode, + http.StatusText(c.statusCode)) + if err := c.resHdr.Write(&c.res); err != nil { + return err + } + c.res.WriteByte(byte('\n')) + c.res.Write(c.resBuf.Bytes()) + + // send the response + + written, err := c.device.SendData(c.res.Bytes(), c.sock) + if err != nil { + return err + } + if written == 0 { + return ErrDataNotWritten + } + if sent, _ := c.device.CheckDataSent(c.sock); !sent { + return ErrCheckDataError + } + + return nil +}