Skip to content

Commit

Permalink
use net/x/websocket for tinygo (for now)
Browse files Browse the repository at this point in the history
  • Loading branch information
scottfeldman committed Dec 11, 2024
1 parent f8fe441 commit 46675ed
Show file tree
Hide file tree
Showing 4 changed files with 228 additions and 0 deletions.
122 changes: 122 additions & 0 deletions pkg/device/ws-client-tinygo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
//go:build tinygo

package device

import (
"net/http"
"net/url"
"time"

"golang.org/x/net/websocket"
)

func newConfig(wsUrl *url.URL, user, passwd string) (*websocket.Config, error) {

// Set the origin to match the WebSocket server’s scheme and host
origin := &url.URL{Scheme: "http", Host: wsUrl.Host}
if wsUrl.Scheme == "wss" {
origin.Scheme = "https"
}

// Configure the websocket
config, err := websocket.NewConfig(wsUrl.String(), origin.String())
if err != nil {
return nil, err
}

// If valid user, set the basic auth header for the request
if user != "" {
req, err := http.NewRequest("GET", wsUrl.String(), nil)
if err != nil {
return nil, err
}
req.SetBasicAuth(user, passwd)
config.Header = req.Header
}

return config, nil
}

func wsDial(url *url.URL, user, passwd string) {
cfg, err := newConfig(url, user, passwd)
if err != nil {
LogError("Configuring websocket", "err", err)
return
}

for {
// Dial the websocket
conn, err := websocket.DialConfig(cfg)
if err == nil {
// Service the client websocket
wsClient(conn)
} else {
LogError("Dialing", "url", url, "err", err)
}

// Try again in a second
time.Sleep(time.Second)
}
}

func wsClient(conn *websocket.Conn) {
defer conn.Close()

var link = &wsLink{conn: conn}
var ann = announcement{
Id: root.Id,
Model: root.Model,
Name: root.Name,
DeployParams: root.DeployParams,
}
var pkt = &Packet{
Dst: ann.Id,
Path: "/announce",
}

pkt.Marshal(&ann)

// Send announcement
LogInfo("Sending announcement", "pkt", pkt)
err := link.Send(pkt)
if err != nil {
LogError("Sending", "err", err)
return
}

// Receive welcome within 1 sec
pkt, err = link.receiveTimeout(time.Second)
if err != nil {
LogError("Receiving", "err", err)
return
}

LogInfo("Reply from announcement", "pkt", pkt)
if pkt.Path != "/welcome" {
LogError("Not welcomed, got", "path", pkt.Path)
return
}

LogInfo("Adding Uplink")
uplinksAdd(link)

// Send /state packets to all devices
devicesSendState(link)

// Route incoming packets down to the destination device. Stop and
// disconnect on EOF.

LogInfo("Receiving packets")
for {
pkt, err := link.receivePoll()
if err != nil {
LogError("Receiving packet", "err", err)
break
}
LogInfo("Route packet DOWN", "pkt", pkt)
deviceRouteDown(pkt.Dst, pkt)
}

LogInfo("Removing Uplink")
uplinksRemove(link)
}
5 changes: 5 additions & 0 deletions pkg/device/ws-client.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
//go:build !tinygo

// TODO get gorilla/websocket working on tinygo. Currently hit:
// ../../../go/pkg/mod/github.com/gorilla/[email protected]/client.go:18:2: package net/http/httptrace is not in std (/root/...

package device

import (
Expand Down
96 changes: 96 additions & 0 deletions pkg/device/ws-tinygo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
//go:build tinygo

package device

import (
"encoding/json"
"fmt"
"html/template"
"net"
"sync"
"time"

"golang.org/x/net/websocket"
)

type wsLink struct {
conn *websocket.Conn
sync.Mutex
lastRecv time.Time
lastSend time.Time
}

type announcement struct {
Id string
Model string
Name string
DeployParams template.HTML
}

func (l *wsLink) Send(pkt *Packet) error {
data, err := json.Marshal(pkt)
if err != nil {
return fmt.Errorf("Marshal error: %w", err)
}
l.Lock()
defer l.Unlock()
if err := websocket.Message.Send(l.conn, string(data)); err != nil {
return fmt.Errorf("Send error: %w", err)
}
l.lastSend = time.Now()
return nil
}

func (l *wsLink) Close() {
l.conn.Close()
}

func (l *wsLink) receive() (*Packet, error) {
var data []byte
var pkt Packet

if err := websocket.Message.Receive(l.conn, &data); err != nil {
return nil, err
}
l.lastRecv = time.Now()
if err := json.Unmarshal(data, &pkt); err != nil {
LogError("Unmarshal Error", "data", string(data))
return nil, fmt.Errorf("Unmarshalling error: %w", err)
}
return &pkt, nil
}

func (l *wsLink) receiveTimeout(timeout time.Duration) (*Packet, error) {
l.conn.SetReadDeadline(time.Now().Add(timeout))
pkt, err := l.receive()
l.conn.SetReadDeadline(time.Time{})
return pkt, err
}

var pingDuration = 4 * time.Second
var pingTimeout = 2*pingDuration + time.Second

func (l *wsLink) receivePoll() (*Packet, error) {
for {
if time.Since(l.lastSend) >= pingDuration {
if err := l.Send(&Packet{Path: "/ping"}); err != nil {
return nil, err
}
}
pkt, err := l.receiveTimeout(time.Second)
if err == nil {
if pkt.Path == "/ping" {
continue
}
return pkt, nil
}
if netErr, ok := err.(*net.OpError); ok && netErr.Timeout() {
if time.Since(l.lastRecv) > pingTimeout {
return nil, err
}
continue
}
return nil, err
}
return nil, nil
}
5 changes: 5 additions & 0 deletions pkg/device/ws.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
//go:build !tinygo

// TODO get gorilla/websocket working on tinygo. Currently hit:
// ../../../go/pkg/mod/github.com/gorilla/[email protected]/client.go:18:2: package net/http/httptrace is not in std (/root/...

package device

import (
Expand Down

0 comments on commit 46675ed

Please sign in to comment.