Files
MARTe_IO_Components/Client/WebUI/hub.go
T
Martino Ferrari e3389f932b FIrs
2026-05-15 17:42:14 +02:00

384 lines
9.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"encoding/json"
"log"
"net/http"
"sync"
"time"
"github.com/gorilla/websocket"
)
// ─── WebSocket client ─────────────────────────────────────────────────────────
type wsClient struct {
hub *Hub
conn *websocket.Conn
send chan []byte
}
func (c *wsClient) writePump() {
pingTicker := time.NewTicker(30 * time.Second)
defer func() {
pingTicker.Stop()
c.conn.Close()
}()
for {
select {
case msg, ok := <-c.send:
if !ok {
c.conn.WriteMessage(websocket.CloseMessage, []byte{})
return
}
if err := c.conn.WriteMessage(websocket.TextMessage, msg); err != nil {
return
}
case <-pingTicker.C:
if err := c.conn.WriteControl(websocket.PingMessage, []byte{},
time.Now().Add(10*time.Second)); err != nil {
return
}
}
}
}
func (c *wsClient) readPump() {
defer func() {
c.hub.unregister <- c
c.conn.Close()
}()
c.conn.SetReadLimit(64 * 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 {
_, msg, err := c.conn.ReadMessage()
if err != nil {
break
}
// Handle client messages (ping, setWindow, etc.) currently just log.
var env map[string]interface{}
if json.Unmarshal(msg, &env) == nil {
if t, ok := env["type"].(string); ok {
switch t {
case "ping":
resp, _ := json.Marshal(map[string]string{"type": "pong"})
select {
case c.send <- resp:
default:
}
}
}
}
c.conn.SetReadDeadline(time.Now().Add(60 * time.Second))
}
}
// ─── Hub ─────────────────────────────────────────────────────────────────────
var upgrader = websocket.Upgrader{
ReadBufferSize: 4096,
WriteBufferSize: 64 * 1024,
CheckOrigin: func(r *http.Request) bool { return true },
}
// Hub is the central data broker between the UDP client and WebSocket clients.
type Hub struct {
mu sync.RWMutex
signals []SignalInfo
configJS []byte // cached JSON config message
clients map[*wsClient]bool
register chan *wsClient
unregister chan *wsClient
broadcastCh chan []byte // all sends go through Run() to avoid races on c.send
dataCh chan DataSample // incoming samples from UDP goroutine
}
// NewHub creates an initialised Hub.
func NewHub() *Hub {
return &Hub{
clients: make(map[*wsClient]bool),
register: make(chan *wsClient, 8),
unregister: make(chan *wsClient, 8),
broadcastCh: make(chan []byte, 64),
dataCh: make(chan DataSample, 256),
}
}
// UpdateConfig stores a new signal config and broadcasts it to all WS clients.
func (h *Hub) UpdateConfig(sigs []SignalInfo) {
msg, err := json.Marshal(map[string]interface{}{
"type": "config",
"signals": sigs,
})
if err != nil {
log.Printf("hub: marshal config: %v", err)
return
}
h.mu.Lock()
h.signals = sigs
h.configJS = msg
h.mu.Unlock()
h.broadcast(msg)
}
// PushData enqueues a data sample for broadcasting to WebSocket clients.
func (h *Hub) PushData(s DataSample) {
select {
case h.dataCh <- s:
default:
// Drop if buffer full to avoid blocking the UDP goroutine.
}
}
// broadcast enqueues a message for delivery to all WebSocket clients.
// All actual sends happen inside Run() to avoid concurrent access to c.send.
func (h *Hub) broadcast(msg []byte) {
select {
case h.broadcastCh <- msg:
default:
// Drop if the broadcast queue is full.
}
}
// HandleWebSocket upgrades an HTTP request to a WebSocket connection.
func (h *Hub) HandleWebSocket(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Printf("ws upgrade: %v", err)
return
}
c := &wsClient{
hub: h,
conn: conn,
send: make(chan []byte, 64),
}
h.register <- c
go c.writePump()
go c.readPump()
}
// Run is the hub's main goroutine. It must be started with go hub.Run().
func (h *Hub) Run() {
// Batch data at ≤30 Hz.
ticker := time.NewTicker(time.Second / 30)
defer ticker.Stop()
// Accumulate samples between ticks.
pending := make([]DataSample, 0, 64)
for {
select {
case c := <-h.register:
h.mu.Lock()
h.clients[c] = true
cfg := h.configJS
h.mu.Unlock()
// Send current config immediately if we have one.
if cfg != nil {
select {
case c.send <- cfg:
default:
}
}
case c := <-h.unregister:
h.mu.Lock()
if _, ok := h.clients[c]; ok {
delete(h.clients, c)
close(c.send)
}
h.mu.Unlock()
case msg := <-h.broadcastCh:
h.mu.RLock()
for c := range h.clients {
select {
case c.send <- msg:
default:
}
}
h.mu.RUnlock()
case s := <-h.dataCh:
pending = append(pending, s)
case <-ticker.C:
if len(pending) == 0 {
continue
}
h.mu.RLock()
sigs := h.signals
noClients := len(h.clients) == 0
h.mu.RUnlock()
if noClients || len(sigs) == 0 {
pending = pending[:0]
continue
}
msg := h.buildDataMessage(pending, sigs)
pending = pending[:0]
if msg != nil {
h.broadcast(msg)
}
}
}
}
// maxBatchPoints is the maximum number of points sent per signal per 30 Hz display tick.
// For temporal-array signals (e.g. 1 MSps packed as 1000 samples/packet), the expanded
// sample stream is decimated to this limit before transmission to WebSocket clients.
const maxBatchPoints = 2000
// sigData carries one signal's worth of time+value pairs in a single batch message.
type sigData struct {
T []float64 `json:"t"` // unix seconds
V []float64 `json:"v"` // physical values
}
// dataMsg is the JSON envelope for batched data sent to WebSocket clients.
// Each signal carries its own time axis so that temporal arrays (packed sample
// bursts with a known sampling rate) and scalar signals can coexist cleanly.
type dataMsg struct {
Type string `json:"type"`
Signals map[string]sigData `json:"signals"`
}
// buildDataMessage merges a batch of DataSamples into one JSON message.
//
// Three cases are handled:
//
// 1. Temporal array (NumElements > 1, TimeMode != PacketTime):
// The N samples in each packet represent a contiguous time burst at SamplingRate.
// Wall arrival time is treated as the timestamp of the last sample; earlier
// samples are reconstructed as t[k] = wallT - (N-1-k)/SamplingRate.
// The full expanded stream is decimated to maxBatchPoints if needed.
//
// 2. Scalar signal (NumElements == 1):
// One {t, v} pair per packet wall arrival time used as timestamp.
//
// 3. Spatial / PacketTime array (NumElements > 1, TimeMode == 0):
// Each element is tracked as a separate stream keyed "sig[i]", with wall
// arrival time as the shared timestamp.
func (h *Hub) buildDataMessage(batch []DataSample, sigs []SignalInfo) []byte {
if len(batch) == 0 {
return nil
}
out := make(map[string]sigData, len(sigs)*2)
for _, sig := range sigs {
n := sig.NumElements()
isTemporal := n > 1 && sig.TimeMode != 0
switch {
case isTemporal:
// Expand each packet's N samples into individual time-stamped points.
allT := make([]float64, 0, len(batch)*n)
allV := make([]float64, 0, len(batch)*n)
dt := 0.0
if sig.SamplingRate > 0 {
dt = 1.0 / sig.SamplingRate
}
for _, s := range batch {
vals, ok := s.Values[sig.Name]
if !ok || len(vals) < n {
continue
}
// wallT ≈ arrival time of the packet ≈ timestamp of the last sample.
wallT := float64(s.WallTime.UnixNano()) / 1e9
for k := 0; k < n; k++ {
allT = append(allT, wallT-float64(n-1-k)*dt)
allV = append(allV, vals[k])
}
}
// Decimate to maxBatchPoints if necessary.
step := 1
if len(allT) > maxBatchPoints {
step = len(allT) / maxBatchPoints
if step < 1 {
step = 1
}
}
decimT := make([]float64, 0, len(allT)/step+1)
decimV := make([]float64, 0, len(allV)/step+1)
for i := 0; i < len(allT); i += step {
decimT = append(decimT, allT[i])
decimV = append(decimV, allV[i])
}
out[sig.Name] = sigData{T: decimT, V: decimV}
case n == 1:
// Scalar signal: one sample per packet.
ts := make([]float64, 0, len(batch))
vs := make([]float64, 0, len(batch))
for _, s := range batch {
vals, ok := s.Values[sig.Name]
if !ok || len(vals) < 1 {
continue
}
ts = append(ts, float64(s.WallTime.UnixNano())/1e9)
vs = append(vs, vals[0])
}
out[sig.Name] = sigData{T: ts, V: vs}
default:
// Spatial / PacketTime array: one stream per element, keyed "sig[i]".
for i := 0; i < n; i++ {
key := arrayKey(sig.Name, i)
ts := make([]float64, 0, len(batch))
vs := make([]float64, 0, len(batch))
for _, s := range batch {
vals, ok := s.Values[sig.Name]
if !ok || len(vals) <= i {
continue
}
ts = append(ts, float64(s.WallTime.UnixNano())/1e9)
vs = append(vs, vals[i])
}
out[key] = sigData{T: ts, V: vs}
}
}
}
result, err := json.Marshal(dataMsg{
Type: "data",
Signals: out,
})
if err != nil {
log.Printf("hub: marshal data: %v", err)
return nil
}
return result
}
// arrayKey returns the buffer key for element i of an array signal.
func arrayKey(name string, i int) string {
return name + "[" + itoa(i) + "]"
}
func itoa(n int) string {
if n == 0 {
return "0"
}
buf := [20]byte{}
pos := len(buf)
for n > 0 {
pos--
buf[pos] = byte('0' + n%10)
n /= 10
}
return string(buf[pos:])
}