Added go webui for minimal c dependencies

This commit is contained in:
Martino Ferrari
2026-05-19 16:39:12 +02:00
parent 242b1c4406
commit d967098ead
10 changed files with 2379 additions and 0 deletions
+237
View File
@@ -0,0 +1,237 @@
package main
import (
"embed"
"encoding/json"
"flag"
"io/fs"
"log"
"net/http"
"sync"
"time"
"github.com/gorilla/websocket"
)
//go:embed static
var staticFiles embed.FS
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool { return true },
}
// ---------------------------------------------------------------------------
// Wire message types
// ---------------------------------------------------------------------------
type InMsg struct {
Type string `json:"type"`
Data json.RawMessage `json:"data,omitempty"`
}
type ConnectData struct {
Host string `json:"host"`
Port int `json:"port"`
UDPPort int `json:"udp_port"`
LogPort int `json:"log_port"`
}
type CmdData struct {
Cmd string `json:"cmd"`
}
// ---------------------------------------------------------------------------
// Hub fan-out WS messages to all connected browsers
// ---------------------------------------------------------------------------
type Hub struct {
mu sync.RWMutex
clients map[*WSClient]struct{}
marte *MarteClient
}
func newHub() *Hub {
return &Hub{clients: make(map[*WSClient]struct{})}
}
func (h *Hub) add(c *WSClient) {
h.mu.Lock()
h.clients[c] = struct{}{}
h.mu.Unlock()
}
func (h *Hub) remove(c *WSClient) {
h.mu.Lock()
delete(h.clients, c)
h.mu.Unlock()
}
func (h *Hub) broadcast(msg []byte) {
h.mu.RLock()
defer h.mu.RUnlock()
for c := range h.clients {
select {
case c.send <- msg:
default:
}
}
}
// ---------------------------------------------------------------------------
// WSClient one browser tab
// ---------------------------------------------------------------------------
type WSClient struct {
hub *Hub
conn *websocket.Conn
send chan []byte
}
func (c *WSClient) writePump() {
ticker := time.NewTicker(30 * time.Second)
defer func() {
ticker.Stop()
c.conn.Close()
}()
for {
select {
case msg, ok := <-c.send:
c.conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
if !ok {
c.conn.WriteMessage(websocket.CloseMessage, []byte{})
return
}
if err := c.conn.WriteMessage(websocket.TextMessage, msg); err != nil {
return
}
case <-ticker.C:
c.conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil {
return
}
}
}
}
func (c *WSClient) readPump() {
defer func() {
c.hub.remove(c)
c.conn.Close()
}()
c.conn.SetReadLimit(256 * 1024)
c.conn.SetReadDeadline(time.Now().Add(60 * time.Second))
c.conn.SetPongHandler(func(string) error {
c.conn.SetReadDeadline(time.Now().Add(60 * time.Second))
return nil
})
for {
_, raw, err := c.conn.ReadMessage()
if err != nil {
break
}
c.conn.SetReadDeadline(time.Now().Add(60 * time.Second))
var in InMsg
if err := json.Unmarshal(raw, &in); err != nil {
continue
}
c.handleIncoming(in)
}
}
func (c *WSClient) handleIncoming(in InMsg) {
m := c.hub.marte
switch in.Type {
case "connect":
var d ConnectData
if err := json.Unmarshal(in.Data, &d); err != nil {
return
}
if d.Host == "" {
d.Host = "127.0.0.1"
}
if d.Port == 0 {
d.Port = 8080
}
if d.UDPPort == 0 {
d.UDPPort = 8081
}
if d.LogPort == 0 {
d.LogPort = 8082
}
m.Connect(d.Host, d.Port, d.UDPPort, d.LogPort)
case "disconnect":
m.Disconnect()
case "cmd":
var d CmdData
if err := json.Unmarshal(in.Data, &d); err != nil {
return
}
m.SendCommand(d.Cmd)
}
}
// ---------------------------------------------------------------------------
// HTTP handler
// ---------------------------------------------------------------------------
func wsHandler(hub *Hub, w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println("upgrade:", err)
return
}
c := &WSClient{hub: hub, conn: conn, send: make(chan []byte, 256)}
hub.add(c)
// Send current connection status to newly joined client.
if hub.marte.IsConnected() {
b, _ := json.Marshal(map[string]any{"type": "connected"})
c.send <- b
// Also push current signal list if already discovered.
hub.marte.SendCachedState(c)
}
go c.writePump()
c.readPump() // blocks until client disconnects
}
// ---------------------------------------------------------------------------
// main
// ---------------------------------------------------------------------------
func main() {
addr := flag.String("addr", ":7777", "HTTP listen address")
flag.Parse()
hub := newHub()
hub.marte = newMarteClient(hub)
sub, err := fs.Sub(staticFiles, "static")
if err != nil {
log.Fatal(err)
}
fileServer := http.FileServer(http.FS(sub))
http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
wsHandler(hub, w, r)
})
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// Serve index.html for the root path explicitly.
if r.URL.Path == "/" {
data, err := staticFiles.ReadFile("static/index.html")
if err != nil {
http.Error(w, "not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write(data)
return
}
fileServer.ServeHTTP(w, r)
})
log.Printf("MARTe2 Web Debug Client listening on %s", *addr)
log.Fatal(http.ListenAndServe(*addr, nil))
}