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
|
||||
signals []SignalInfo
|
||||
configJS []byte // cached JSON config message
|
||||
configSeq uint64 // incremented on every UpdateConfig call
|
||||
|
||||
clients map[*wsClient]bool
|
||||
register chan *wsClient
|
||||
@@ -97,16 +98,26 @@ type Hub struct {
|
||||
broadcastCh chan []byte // all sends go through Run() to avoid races on c.send
|
||||
|
||||
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.
|
||||
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),
|
||||
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),
|
||||
timeSigCalib: make(map[string]float64),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,6 +134,7 @@ func (h *Hub) UpdateConfig(sigs []SignalInfo) {
|
||||
h.mu.Lock()
|
||||
h.signals = sigs
|
||||
h.configJS = msg
|
||||
h.configSeq++
|
||||
h.mu.Unlock()
|
||||
|
||||
h.broadcast(msg)
|
||||
@@ -256,10 +268,11 @@ type dataMsg struct {
|
||||
//
|
||||
// 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.
|
||||
// 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 embedded TimeSignal value (uint32 microseconds) is used as the timing
|
||||
// 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.
|
||||
//
|
||||
// 2. Scalar signal (NumElements == 1):
|
||||
@@ -273,32 +286,83 @@ func (h *Hub) buildDataMessage(batch []DataSample, sigs []SignalInfo) []byte {
|
||||
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)
|
||||
|
||||
for _, sig := range sigs {
|
||||
n := sig.NumElements()
|
||||
isTemporal := n > 1 && sig.TimeMode != 0
|
||||
isTemporal := n > 1 && (sig.TimeMode == TimeModeFirstSample || sig.TimeMode == TimeModeLastSample)
|
||||
|
||||
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)
|
||||
// Resolve time signal (scalar that gives the anchor time in microseconds).
|
||||
hasTimeSig := sig.TimeSignalIdx != NoTimeSignal && int(sig.TimeSignalIdx) < len(sigs)
|
||||
var timeSigName string
|
||||
if hasTimeSig {
|
||||
timeSigName = sigs[sig.TimeSignalIdx].Name
|
||||
}
|
||||
|
||||
dt := 0.0
|
||||
if sig.SamplingRate > 0 {
|
||||
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 {
|
||||
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
|
||||
|
||||
// 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++ {
|
||||
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])
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user