WebUI: per-signal vscale toolbar, active-signal highlighting, zoom fix

- Per-signal, per-plot vertical scale state (sigVScale keyed by plotId:signalKey)
  so the same signal in two plots has fully independent vscale config
- Active signal redrawn on top of all series with 2× line width for clear
  visual identification; badge click toggles selection and opens/closes the
  embedded vscale toolbar (click same badge again to deselect)
- Vscale configurator moved from floating popup to a slim toolbar strip
  anchored inside the plot card, with an × close button
- Trigger dropdown shows one entry per array signal with [0…N-1] label;
  opening it shows an index-picker dialog to choose the element
- Zoom resampling: when server returns no data for a zoomed range, fall
  back to the local circular buffer instead of returning empty arrays

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Martino Ferrari
2026-05-27 15:42:00 +02:00
parent b15e637f14
commit 3dd0d863fa
13 changed files with 1508 additions and 231 deletions
+14 -8
View File
@@ -107,6 +107,7 @@ func (u *UDPClient) runSession() error {
reassembler := NewReassembler(2 * time.Second)
buf := make([]byte, readBufSize)
var currentSigs []SignalInfo
var currentPublishMode uint8
for {
conn.SetReadDeadline(time.Now().Add(silenceTimeout))
@@ -142,13 +143,14 @@ func (u *UDPClient) runSession() error {
switch hdr.Type {
case PktConfig:
sigs, err := ParseConfig(complete)
sigs, pm, err := ParseConfig(complete)
if err != nil {
log.Printf("[%s] udp: parse config: %v", u.sourceID, err)
continue
}
currentSigs = sigs
log.Printf("[%s] udp: received CONFIG (%d signals)", u.sourceID, len(sigs))
currentPublishMode = pm
log.Printf("[%s] udp: received CONFIG (%d signals, publishMode=%d)", u.sourceID, len(sigs), pm)
u.hub.SetSourceState(u.sourceID, "connected")
u.hub.UpdateConfigForSource(u.sourceID, sigs)
@@ -156,12 +158,14 @@ func (u *UDPClient) runSession() error {
if len(currentSigs) == 0 {
continue
}
sample, err := ParseData(complete, currentSigs, arrivalTime)
samples, err := ParseData(complete, currentSigs, currentPublishMode, arrivalTime)
if err != nil {
log.Printf("[%s] udp: parse data: %v", u.sourceID, err)
continue
}
u.hub.PushDataForSource(u.sourceID, sample)
for _, s := range samples {
u.hub.PushDataForSource(u.sourceID, s)
}
case PktACK:
log.Printf("[%s] udp: received ACK (counter=%d)", u.sourceID, hdr.Counter)
@@ -224,11 +228,11 @@ func (u *UDPClient) runMulticastSession() error {
return err
}
}
currentSigs, err := ParseConfig(cfgPayload)
currentSigs, currentPublishMode, err := ParseConfig(cfgPayload)
if err != nil {
return err
}
log.Printf("[%s] tcp: received CONFIG (%d signals)", u.sourceID, len(currentSigs))
log.Printf("[%s] tcp: received CONFIG (%d signals, publishMode=%d)", u.sourceID, len(currentSigs), currentPublishMode)
u.hub.SetSourceState(u.sourceID, "connected")
u.hub.UpdateConfigForSource(u.sourceID, currentSigs)
@@ -318,12 +322,14 @@ func (u *UDPClient) runMulticastSession() error {
if len(currentSigs) == 0 {
continue
}
sample, parseErr := ParseData(complete, currentSigs, arrivalTime)
samples, parseErr := ParseData(complete, currentSigs, currentPublishMode, arrivalTime)
if parseErr != nil {
log.Printf("[%s] multicast: parse data: %v", u.sourceID, parseErr)
continue
}
u.hub.PushDataForSource(u.sourceID, sample)
for _, s := range samples {
u.hub.PushDataForSource(u.sourceID, s)
}
}
select {