No plots yet
Click + Add Plot then drag signals from the sidebar.
diff --git a/Client/WebUI/hub.go b/Client/WebUI/hub.go index e1263ff..cddde21 100644 --- a/Client/WebUI/hub.go +++ b/Client/WebUI/hub.go @@ -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]) } } diff --git a/Client/WebUI/protocol.go b/Client/WebUI/protocol.go index 6159ac5..a364482 100644 --- a/Client/WebUI/protocol.go +++ b/Client/WebUI/protocol.go @@ -19,15 +19,21 @@ const ( PktConnect uint8 = 3 PktDisconnect uint8 = 4 - HeaderSize = 17 - SigDescSize = 136 - NoTimeSignal = uint32(0xFFFFFFFF) + HeaderSize = 17 + SigDescSize = 136 + NoTimeSignal = uint32(0xFFFFFFFF) QuantNone uint8 = 0 QuantUint8 uint8 = 1 QuantInt8 uint8 = 2 QuantUint16 uint8 = 3 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) ───────────────────────── diff --git a/Client/WebUI/static/index.html b/Client/WebUI/static/index.html index dfcc19a..6fb33ae 100644 --- a/Client/WebUI/static/index.html +++ b/Client/WebUI/static/index.html @@ -4,583 +4,569 @@