wshub: add setCalibration/reloadConfig frames and config acks
Wire five new WebSocket frames into hub.go: setCalibration (client→hub), calibration (hub→client broadcast), reloadConfig (client→hub), configSaved and configReloaded (hub→client acks with ok/path/error). Extend hubCmd with cal field, add buildCalibrationMsg and buildConfigAckMsg builders, send calibration on client connect, and run Save/Reload on separate goroutines to avoid blocking the Run() select loop. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
dfd257cfd9
commit
ffe7cb1cc5
@@ -107,6 +107,27 @@ func (c *wsClient) readPump() {
|
||||
case c.hub.commandCh <- hubCmd{op: "wsSaveSources"}:
|
||||
default:
|
||||
}
|
||||
case "setCalibration":
|
||||
source, _ := env["source"].(string)
|
||||
signal, _ := env["signal"].(string)
|
||||
scale, hasScale := env["scale"].(float64)
|
||||
if !hasScale {
|
||||
scale = 1
|
||||
}
|
||||
offset, _ := env["offset"].(float64)
|
||||
unit, _ := env["unit"].(string)
|
||||
select {
|
||||
case c.hub.commandCh <- hubCmd{op: "wsSetCalibration", cal: CalConfig{
|
||||
Source: source, Signal: signal,
|
||||
Scale: scale, Offset: offset, Unit: unit,
|
||||
}}:
|
||||
default:
|
||||
}
|
||||
case "reloadConfig":
|
||||
select {
|
||||
case c.hub.commandCh <- hubCmd{op: "wsReloadConfig"}:
|
||||
default:
|
||||
}
|
||||
case "setMonotonic":
|
||||
enabled, _ := env["enabled"].(bool)
|
||||
select {
|
||||
@@ -212,7 +233,8 @@ type taggedSample struct {
|
||||
// hubCmd carries a command to the Run() goroutine.
|
||||
type hubCmd struct {
|
||||
op string // "addSource","removeSource","setSourceState","updateConfig",
|
||||
// "wsAddSource","wsRemoveSource","wsSaveSources"
|
||||
// "wsAddSource","wsRemoveSource","wsSaveSources",
|
||||
// "wsSetCalibration","wsReloadConfig"
|
||||
sourceID string
|
||||
label string
|
||||
addr string
|
||||
@@ -220,7 +242,8 @@ type hubCmd struct {
|
||||
sigs []udpsprotocol.SignalInfo
|
||||
multicastGroup string
|
||||
dataPort int
|
||||
enabled bool // "setMonotonic" toggle
|
||||
enabled bool // "setMonotonic" toggle
|
||||
cal CalConfig // "wsSetCalibration" payload
|
||||
}
|
||||
|
||||
// Hub is the central broker between UDP clients and WebSocket clients.
|
||||
@@ -475,6 +498,26 @@ func buildSourcesMsg(sm map[string]*sourceHubState) []byte {
|
||||
return msg
|
||||
}
|
||||
|
||||
// buildCalibrationMsg serialises the calibration table as a "calibration"
|
||||
// message. It is its own frame rather than a field on "sources" because the
|
||||
// C++ BroadcastSources serialises into a fixed 4096-byte buffer that a
|
||||
// calibration table would overflow.
|
||||
func buildCalibrationMsg(t *calTable) []byte {
|
||||
list := t.List() // never nil: the SPA replaces its table wholesale on receipt
|
||||
msg, _ := json.Marshal(map[string]any{"type": "calibration", "cal": list})
|
||||
return msg
|
||||
}
|
||||
|
||||
// buildConfigAckMsg serialises a configSaved / configReloaded acknowledgement.
|
||||
func buildConfigAckMsg(msgType, path string, err error) []byte {
|
||||
m := map[string]any{"type": msgType, "ok": err == nil, "path": path}
|
||||
if err != nil {
|
||||
m["error"] = err.Error()
|
||||
}
|
||||
msg, _ := json.Marshal(m)
|
||||
return msg
|
||||
}
|
||||
|
||||
// Run is the hub's main goroutine. Must be started with go hub.Run().
|
||||
func (h *Hub) Run() {
|
||||
ticker := time.NewTicker(time.Second / 30)
|
||||
@@ -522,6 +565,11 @@ func (h *Hub) Run() {
|
||||
case c.send <- wsMessage{websocket.TextMessage, monoMsg}:
|
||||
default:
|
||||
}
|
||||
calMsg := buildCalibrationMsg(h.cal)
|
||||
select {
|
||||
case c.send <- wsMessage{websocket.TextMessage, calMsg}:
|
||||
default:
|
||||
}
|
||||
// Notify the application layer so it can replay any persistent state
|
||||
// (e.g., MARTe2 connection status, forced/traced signals).
|
||||
h.onClientConnectMu.RLock()
|
||||
@@ -647,10 +695,44 @@ func (h *Hub) Run() {
|
||||
|
||||
case "wsSaveSources":
|
||||
if h.sm != nil {
|
||||
if err := h.sm.Save(); err != nil {
|
||||
log.Printf("hub: save sources: %v", err)
|
||||
}
|
||||
// Save writes to disk; run it off the Run() goroutine so a
|
||||
// slow filesystem can never stall the hub loop.
|
||||
go func(sm *SourceManager) {
|
||||
err := sm.Save()
|
||||
if err != nil {
|
||||
log.Printf("hub: save config: %v", err)
|
||||
}
|
||||
h.broadcast(buildConfigAckMsg("configSaved", sm.Path(), err))
|
||||
}(h.sm)
|
||||
}
|
||||
|
||||
case "wsSetCalibration":
|
||||
if h.cal.Set(cmd.cal) {
|
||||
h.broadcast(buildCalibrationMsg(h.cal))
|
||||
} else {
|
||||
// No broadcast: the offending client reverts to the last
|
||||
// value it was sent.
|
||||
log.Printf("hub: rejected calibration %q/%q (scale=%v offset=%v)",
|
||||
cmd.cal.Source, cmd.cal.Signal, cmd.cal.Scale, cmd.cal.Offset)
|
||||
}
|
||||
|
||||
case "wsReloadConfig":
|
||||
if h.sm != nil {
|
||||
// Reload calls sm.Add(), which sends on commandCh; from the
|
||||
// Run() goroutine that send would hit the non-blocking
|
||||
// default and be dropped, so it must run elsewhere.
|
||||
go func(sm *SourceManager) {
|
||||
err := sm.Reload()
|
||||
if err != nil {
|
||||
log.Printf("hub: reload config: %v", err)
|
||||
}
|
||||
h.broadcast(buildConfigAckMsg("configReloaded", sm.Path(), err))
|
||||
if err == nil {
|
||||
h.broadcast(buildCalibrationMsg(h.cal))
|
||||
}
|
||||
}(h.sm)
|
||||
}
|
||||
|
||||
case "setMonotonic":
|
||||
h.monotonicTS = cmd.enabled
|
||||
monoMsg, _ := json.Marshal(map[string]any{"type": "monotonicState", "enabled": h.monotonicTS})
|
||||
|
||||
Reference in New Issue
Block a user