mirror of
https://github.com/tinygo-org/drivers.git
synced 2026-08-06 16:03:41 +00:00
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:
@@ -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")
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user