Fix WebUI rolling-window and buffer-wrap display bugs
Replace fragile zoomGuard boolean with userInteracting flag so that programmatic scale updates (rolling window, resize, fit) never lock p.xRange. Only genuine mouse drag or scroll-wheel events on the uPlot canvas set userInteracting=true and allow onZoom to freeze the view. Also move stale-xRange detection out of the needsRedraw gate so that a plot whose circular buffer has scrolled past a frozen zoom range automatically returns to rolling-window mode every frame, fixing the second bug where data disappeared as the buffer wrapped. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+79
-15
@@ -90,6 +90,7 @@ type Hub struct {
|
|||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
signals []SignalInfo
|
signals []SignalInfo
|
||||||
configJS []byte // cached JSON config message
|
configJS []byte // cached JSON config message
|
||||||
|
configSeq uint64 // incremented on every UpdateConfig call
|
||||||
|
|
||||||
clients map[*wsClient]bool
|
clients map[*wsClient]bool
|
||||||
register chan *wsClient
|
register chan *wsClient
|
||||||
@@ -97,16 +98,26 @@ type Hub struct {
|
|||||||
broadcastCh chan []byte // all sends go through Run() to avoid races on c.send
|
broadcastCh chan []byte // all sends go through Run() to avoid races on c.send
|
||||||
|
|
||||||
dataCh chan DataSample // incoming samples from UDP goroutine
|
dataCh chan DataSample // incoming samples from UDP goroutine
|
||||||
|
|
||||||
|
// Time-signal calibration: only accessed from the Run() goroutine.
|
||||||
|
// For temporal-array signals (TimeMode=FirstSample/LastSample), the
|
||||||
|
// MARTe2 Time signal value (uint32 microseconds from start) is used as
|
||||||
|
// the timing anchor. We calibrate a per-signal wall-clock offset once
|
||||||
|
// on the first data packet so that subsequent packets are stamped purely
|
||||||
|
// from the embedded timer value → perfect continuity, no jitter gaps.
|
||||||
|
timeSigCalib map[string]float64 // key=time-signal name, value=wallTime-timerSecs offset
|
||||||
|
configSeqAtCalib uint64 // configSeq value when timeSigCalib was last reset
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHub creates an initialised Hub.
|
// NewHub creates an initialised Hub.
|
||||||
func NewHub() *Hub {
|
func NewHub() *Hub {
|
||||||
return &Hub{
|
return &Hub{
|
||||||
clients: make(map[*wsClient]bool),
|
clients: make(map[*wsClient]bool),
|
||||||
register: make(chan *wsClient, 8),
|
register: make(chan *wsClient, 8),
|
||||||
unregister: make(chan *wsClient, 8),
|
unregister: make(chan *wsClient, 8),
|
||||||
broadcastCh: make(chan []byte, 64),
|
broadcastCh: make(chan []byte, 64),
|
||||||
dataCh: make(chan DataSample, 256),
|
dataCh: make(chan DataSample, 256),
|
||||||
|
timeSigCalib: make(map[string]float64),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,6 +134,7 @@ func (h *Hub) UpdateConfig(sigs []SignalInfo) {
|
|||||||
h.mu.Lock()
|
h.mu.Lock()
|
||||||
h.signals = sigs
|
h.signals = sigs
|
||||||
h.configJS = msg
|
h.configJS = msg
|
||||||
|
h.configSeq++
|
||||||
h.mu.Unlock()
|
h.mu.Unlock()
|
||||||
|
|
||||||
h.broadcast(msg)
|
h.broadcast(msg)
|
||||||
@@ -256,10 +268,11 @@ type dataMsg struct {
|
|||||||
//
|
//
|
||||||
// Three cases are handled:
|
// Three cases are handled:
|
||||||
//
|
//
|
||||||
// 1. Temporal array (NumElements > 1, TimeMode != PacketTime):
|
// 1. Temporal array (NumElements > 1, TimeMode == FirstSample or LastSample):
|
||||||
// The N samples in each packet represent a contiguous time burst at SamplingRate.
|
// 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
|
// The embedded TimeSignal value (uint32 microseconds) is used as the timing
|
||||||
// samples are reconstructed as t[k] = wallT - (N-1-k)/SamplingRate.
|
// anchor for each burst. A wall-clock offset is calibrated once on the first
|
||||||
|
// packet so that abs-time stays consistent with other signals.
|
||||||
// The full expanded stream is decimated to maxBatchPoints if needed.
|
// The full expanded stream is decimated to maxBatchPoints if needed.
|
||||||
//
|
//
|
||||||
// 2. Scalar signal (NumElements == 1):
|
// 2. Scalar signal (NumElements == 1):
|
||||||
@@ -273,32 +286,83 @@ func (h *Hub) buildDataMessage(batch []DataSample, sigs []SignalInfo) []byte {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset time-signal calibration whenever the config has changed.
|
||||||
|
h.mu.RLock()
|
||||||
|
seq := h.configSeq
|
||||||
|
h.mu.RUnlock()
|
||||||
|
if seq != h.configSeqAtCalib {
|
||||||
|
h.configSeqAtCalib = seq
|
||||||
|
h.timeSigCalib = make(map[string]float64)
|
||||||
|
}
|
||||||
|
|
||||||
out := make(map[string]sigData, len(sigs)*2)
|
out := make(map[string]sigData, len(sigs)*2)
|
||||||
|
|
||||||
for _, sig := range sigs {
|
for _, sig := range sigs {
|
||||||
n := sig.NumElements()
|
n := sig.NumElements()
|
||||||
isTemporal := n > 1 && sig.TimeMode != 0
|
isTemporal := n > 1 && (sig.TimeMode == TimeModeFirstSample || sig.TimeMode == TimeModeLastSample)
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case isTemporal:
|
case isTemporal:
|
||||||
// Expand each packet's N samples into individual time-stamped points.
|
// Resolve time signal (scalar that gives the anchor time in microseconds).
|
||||||
allT := make([]float64, 0, len(batch)*n)
|
hasTimeSig := sig.TimeSignalIdx != NoTimeSignal && int(sig.TimeSignalIdx) < len(sigs)
|
||||||
allV := make([]float64, 0, len(batch)*n)
|
var timeSigName string
|
||||||
|
if hasTimeSig {
|
||||||
|
timeSigName = sigs[sig.TimeSignalIdx].Name
|
||||||
|
}
|
||||||
|
|
||||||
dt := 0.0
|
dt := 0.0
|
||||||
if sig.SamplingRate > 0 {
|
if sig.SamplingRate > 0 {
|
||||||
dt = 1.0 / sig.SamplingRate
|
dt = 1.0 / sig.SamplingRate
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
|
||||||
for _, s := range batch {
|
for _, s := range batch {
|
||||||
vals, ok := s.Values[sig.Name]
|
vals, ok := s.Values[sig.Name]
|
||||||
if !ok || len(vals) < n {
|
if !ok || len(vals) < n {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// wallT ≈ arrival time of the packet ≈ timestamp of the last sample.
|
|
||||||
wallT := float64(s.WallTime.UnixNano()) / 1e9
|
// Compute the anchor timestamp for this burst.
|
||||||
|
// Prefer the embedded time-signal value (microseconds) so that
|
||||||
|
// consecutive bursts are perfectly contiguous regardless of jitter.
|
||||||
|
var anchorTime float64
|
||||||
|
anchorIsFirstSample := (sig.TimeMode == TimeModeFirstSample)
|
||||||
|
|
||||||
|
if hasTimeSig {
|
||||||
|
tVals, tOk := s.Values[timeSigName]
|
||||||
|
if tOk && len(tVals) >= 1 {
|
||||||
|
// Time signal is uint32 microseconds from system start.
|
||||||
|
timerS := tVals[0] * 1e-6
|
||||||
|
wallT := float64(s.WallTime.UnixNano()) / 1e9
|
||||||
|
|
||||||
|
// Calibrate the wall-clock offset once per session so that
|
||||||
|
// anchor times can be expressed as absolute Unix timestamps.
|
||||||
|
if _, exists := h.timeSigCalib[timeSigName]; !exists {
|
||||||
|
h.timeSigCalib[timeSigName] = wallT - timerS
|
||||||
|
}
|
||||||
|
anchorTime = h.timeSigCalib[timeSigName] + timerS
|
||||||
|
} else {
|
||||||
|
// Time signal missing in this packet – fall back to wall clock.
|
||||||
|
anchorTime = float64(s.WallTime.UnixNano()) / 1e9
|
||||||
|
anchorIsFirstSample = false // wallT = last sample
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No time signal configured – use wall arrival as last-sample anchor.
|
||||||
|
anchorTime = float64(s.WallTime.UnixNano()) / 1e9
|
||||||
|
anchorIsFirstSample = false
|
||||||
|
}
|
||||||
|
|
||||||
for k := 0; k < n; k++ {
|
for k := 0; k < n; k++ {
|
||||||
allT = append(allT, wallT-float64(n-1-k)*dt)
|
var t float64
|
||||||
|
if anchorIsFirstSample {
|
||||||
|
t = anchorTime + float64(k)*dt
|
||||||
|
} else {
|
||||||
|
t = anchorTime - float64(n-1-k)*dt
|
||||||
|
}
|
||||||
|
allT = append(allT, t)
|
||||||
allV = append(allV, vals[k])
|
allV = append(allV, vals[k])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,15 +19,21 @@ const (
|
|||||||
PktConnect uint8 = 3
|
PktConnect uint8 = 3
|
||||||
PktDisconnect uint8 = 4
|
PktDisconnect uint8 = 4
|
||||||
|
|
||||||
HeaderSize = 17
|
HeaderSize = 17
|
||||||
SigDescSize = 136
|
SigDescSize = 136
|
||||||
NoTimeSignal = uint32(0xFFFFFFFF)
|
NoTimeSignal = uint32(0xFFFFFFFF)
|
||||||
|
|
||||||
QuantNone uint8 = 0
|
QuantNone uint8 = 0
|
||||||
QuantUint8 uint8 = 1
|
QuantUint8 uint8 = 1
|
||||||
QuantInt8 uint8 = 2
|
QuantInt8 uint8 = 2
|
||||||
QuantUint16 uint8 = 3
|
QuantUint16 uint8 = 3
|
||||||
QuantInt16 uint8 = 4
|
QuantInt16 uint8 = 4
|
||||||
|
|
||||||
|
// TimeMode values – must match UDPStreamerTimeMode enum in UDPStreamer.h
|
||||||
|
TimeModePacket uint8 = 0 // use wall-clock packet arrival time
|
||||||
|
TimeModeFullArray uint8 = 1 // TimeSignal has same N elements; not expanded here
|
||||||
|
TimeModeFirstSample uint8 = 2 // TimeSignal scalar = time of element [0]
|
||||||
|
TimeModeLastSample uint8 = 3 // TimeSignal scalar = time of element [N-1]
|
||||||
)
|
)
|
||||||
|
|
||||||
// ─── Packet header (17 bytes, little-endian, packed) ─────────────────────────
|
// ─── Packet header (17 bytes, little-endian, packed) ─────────────────────────
|
||||||
|
|||||||
+1204
-747
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user