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 +}