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.
This commit is contained in:
Scott Feldman
2022-11-14 13:24:15 -08:00
committed by GitHub
parent c5dbe18be1
commit 0ec887c9d8
2 changed files with 532 additions and 0 deletions
+209
View File
@@ -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, `
<html>
<head>
<title>TinyGo HTTP Server</title>
<script language="javascript" type="text/javascript">
var counter = 0
function ledOn() { fetch("/on"); }
function ledOff() { fetch("/off"); }
function fetchCnt() { fetch("/cnt").then(response => response.json()).then(json => { counter = json.cnt; cnt.innerHTML = counter; }); }
function incrCnt() { counter = counter + 1; fetch("/cnt?cnt=" + counter, { method: 'POST' }).then(response => response.json()).then(json => { counter = json.cnt; cnt.innerHTML = counter; }); }
function setCnt() { fetch("/cnt", {
method: "POST",
body: "cnt=" + document.getElementsByName("cnt")[0].value,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
}).then(response => response.json()).then(json => { counter = json.cnt; cnt.innerHTML = counter; }); return false; }
function onLoad() { fetchCnt(); }
</script>
</head>
<body onLoad="onLoad()">
<h5>TinyGo HTTP Server</h5>
<p>
access: %d
</p>
<a href="/hello">/hello</a><br>
<a href="/6">/6</a><br>
<p>
LED<br>
<a href="javascript:ledOn();">/on</a><br>
<a href="javascript:ledOff();">/off</a><br>
</p>
<p>
<a href="/cnt">/cnt</a><br>
cnt: <span id="cnt"></span><br>
<a href="javascript:incrCnt()">incrCnt()</a><br>
<form id="form1" style="display: inline" onSubmit="return setCnt()">
<input type="text" name="cnt">
<input type="button" value="set cnt", onClick="setCnt()">
</form>
</p>
</body>
</html>
`, access)
}
func sixlines(w http.ResponseWriter, r *http.Request) {
// https://fukuno.jig.jp/3267
fmt.Fprint(w, `<body onload='onkeydown=e=>K=parseInt(e.key[5]||6,28)/3-8;Z=X=[B=A=12];Y=_=>`+
`{for(C=[q=c=i=4];f=i--*K;c-=!Z[h+(K+6?p+K:C[i]=p*A-(p/9|0)*145)])p=B[i];for(c?0:K+6?h+=K:B=C;`+
`i=K=q--;f+=Z[A+p])X[p=h+B[q]]=1;h+=A;if(f|B)for(Z=X,X=[l=228],B=[[-7,-20,6,h=17,-9,3,3][t=++t%7]-4,0,1,t-6?-A:2];l--;)`+
`for(l%A?l-=l%A*!Z[l]:(P++,c=l+=A);--c>A;)Z[c]=Z[c-A];for(S="";i<240;S+=X[i]|(X[i]=Z[i]|=++i%A<2|i>228)?i%A?"■":"■<br>":" ");`+
`D.innerHTML=S+P;setTimeout(Y,i-P)};Y(h=K=t=P=0)'id=D>`)
}
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")
}
+323
View File
@@ -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
}