diff --git a/Client/WebUI/hub.go b/Client/WebUI/hub.go index 72af7e2..5e9edde 100644 --- a/Client/WebUI/hub.go +++ b/Client/WebUI/hub.go @@ -156,6 +156,9 @@ type Hub struct { // ringsMu protects the map structure; each sigRing has its own RWMutex for data. ringsMu sync.RWMutex rings map[string]*sigRing // "sourceId:signalKey" → ring + + statsMu sync.RWMutex + statsMap map[string]*SourceStat } // NewHub creates an initialised Hub. @@ -168,6 +171,7 @@ func NewHub() *Hub { dataCh: make(chan taggedSample, 256), commandCh: make(chan hubCmd, 64), rings: make(map[string]*sigRing), + statsMap: make(map[string]*SourceStat), } } @@ -321,6 +325,9 @@ func (h *Hub) Run() { ticker := time.NewTicker(time.Second / 30) defer ticker.Stop() + statsTicker := time.NewTicker(time.Second) + defer statsTicker.Stop() + sourcesMap := make(map[string]*sourceHubState) var sourcesMsg []byte @@ -367,6 +374,9 @@ func (h *Hub) Run() { connState: "connecting", timeSigCalib: make(map[string]float64), } + h.statsMu.Lock() + h.statsMap[cmd.sourceID] = &SourceStat{} + h.statsMu.Unlock() rebuildSources() case "removeSource": @@ -380,6 +390,9 @@ func (h *Hub) Run() { } } h.ringsMu.Unlock() + h.statsMu.Lock() + delete(h.statsMap, cmd.sourceID) + h.statsMu.Unlock() rebuildSources() case "setSourceState": @@ -416,7 +429,7 @@ func (h *Hub) Run() { } for _, sig := range cmd.sigs { ne := sig.NumElements() - isTemporal := ne > 1 && (sig.TimeMode == TimeModeFirstSample || sig.TimeMode == TimeModeLastSample) + isTemporal := ne > 1 && sig.TimeMode != TimeModePacket if isTemporal { h.rings[pfxUpd+sig.Name] = newSigRing(ringCapTemporal) } else if ne == 1 { @@ -466,6 +479,18 @@ func (h *Hub) Run() { h.broadcast(msg) } } + + case <-statsTicker.C: + h.statsMu.RLock() + snap := make(map[string]StatInfo, len(h.statsMap)) + for id, st := range h.statsMap { + snap[id] = st.Snapshot() + } + h.statsMu.RUnlock() + if len(snap) > 0 { + msg, _ := json.Marshal(map[string]any{"type": "stats", "sources": snap}) + h.broadcast(msg) + } } } } @@ -574,14 +599,19 @@ func (h *Hub) buildDataMessageForSource(src *sourceHubState, batch []DataSample) for _, sig := range sigs { n := sig.NumElements() - isTemporal := n > 1 && (sig.TimeMode == TimeModeFirstSample || sig.TimeMode == TimeModeLastSample) switch { - case isTemporal: + case n > 1 && (sig.TimeMode == TimeModeFirstSample || sig.TimeMode == TimeModeLastSample): hasTimeSig := sig.TimeSignalIdx != NoTimeSignal && int(sig.TimeSignalIdx) < len(sigs) var timeSigName string + // uint64 signals carry nanoseconds (HRT); everything else is assumed microseconds. + timerToSec := 1e-6 if hasTimeSig { - timeSigName = sigs[sig.TimeSignalIdx].Name + ts := sigs[sig.TimeSignalIdx] + timeSigName = ts.Name + if ts.TypeCode == 6 { // uint64 → nanoseconds + timerToSec = 1e-9 + } } dt := 0.0 if sig.SamplingRate > 0 { @@ -600,7 +630,7 @@ func (h *Hub) buildDataMessageForSource(src *sourceHubState, batch []DataSample) if hasTimeSig { tVals, tOk := s.Values[timeSigName] if tOk && len(tVals) >= 1 { - timerS := tVals[0] * 1e-6 + timerS := tVals[0] * timerToSec wallT := float64(s.WallTime.UnixNano()) / 1e9 if _, exists := src.timeSigCalib[timeSigName]; !exists { src.timeSigCalib[timeSigName] = wallT - timerS @@ -635,6 +665,58 @@ func (h *Hub) buildDataMessageForSource(src *sourceHubState, batch []DataSample) decimT, decimV := lttbDecimate(allT, allV, maxPushPoints) out[pfx+sig.Name] = sigData{T: decimT, V: decimV} + case n > 1 && sig.TimeMode == TimeModeFullArray: + // The time signal has the same N elements as the data signal. + // Each element pair (timeSig[k], dataSig[k]) is one (t, v) sample. + hasTimeSig := sig.TimeSignalIdx != NoTimeSignal && int(sig.TimeSignalIdx) < len(sigs) + var timeSigName string + timerToSec := 1e-6 + if hasTimeSig { + ts := sigs[sig.TimeSignalIdx] + timeSigName = ts.Name + if ts.TypeCode == 6 { // uint64 → nanoseconds + timerToSec = 1e-9 + } + } + 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 + } + if hasTimeSig { + tVals, tOk := s.Values[timeSigName] + if tOk && len(tVals) >= n { + // Calibrate once: map timer ticks to wall-clock seconds. + if _, exists := src.timeSigCalib[timeSigName]; !exists { + wallT := float64(s.WallTime.UnixNano()) / 1e9 + src.timeSigCalib[timeSigName] = wallT - tVals[0]*timerToSec + } + calib := src.timeSigCalib[timeSigName] + for k := 0; k < n; k++ { + allT = append(allT, calib+tVals[k]*timerToSec) + allV = append(allV, vals[k]) + } + continue + } + } + // Fallback: stamp all elements with packet arrival time. + wallT := float64(s.WallTime.UnixNano()) / 1e9 + for k := 0; k < n; k++ { + allT = append(allT, wallT) + allV = append(allV, vals[k]) + } + } + + ringT, ringV := lttbDecimate(allT, allV, maxRingPoints) + if rb := h.getRing(pfx + sig.Name); rb != nil { + rb.write(ringT, ringV) + } + decimT, decimV := lttbDecimate(allT, allV, maxPushPoints) + out[pfx+sig.Name] = sigData{T: decimT, V: decimV} + case n == 1: ts := make([]float64, 0, len(batch)) vs := make([]float64, 0, len(batch)) @@ -681,6 +763,16 @@ func (h *Hub) buildDataMessageForSource(src *sourceHubState, batch []DataSample) return result } +// RecordDataFragment is called by UDPClient for every incoming DATA datagram. +func (h *Hub) RecordDataFragment(sourceID string, counter uint32, nBytes int, arrivalNs int64, complete bool) { + h.statsMu.RLock() + st := h.statsMap[sourceID] + h.statsMu.RUnlock() + if st != nil { + st.RecordFragment(counter, nBytes, arrivalNs, complete) + } +} + // arrayKey returns the buffer key for element i of an array signal. func arrayKey(name string, i int) string { return name + "[" + itoa(i) + "]" diff --git a/Client/WebUI/static/app.js b/Client/WebUI/static/app.js index 9be44cf..98c189b 100644 --- a/Client/WebUI/static/app.js +++ b/Client/WebUI/static/app.js @@ -111,6 +111,7 @@ function connectWS() { if (msg.type === 'sources') onSources(msg); else if (msg.type === 'config') onConfig(msg); else if (msg.type === 'data') onData(msg); + else if (msg.type === 'stats') onStats(msg); }; } @@ -745,6 +746,26 @@ function drawCursorLines(u, p) { drawLine(cursors.tB, 'rgba(249,226,175,0.85)', 'B'); } +// Compute the rolling-window anchor ("newest common timestamp") for a plot. +// Returns the min-of-max timestamp across all sources contributing traces to p, +// so no source shows a blank right edge. Falls back to Date.now()/1000 if no data. +function computePlotNow(p) { + const sourceNewest = {}; + p.traces.forEach(key => { + const colon = key.indexOf(':'); + if (colon < 0) return; + const srcId = key.slice(0, colon); + const buf = buffers[key]; + if (!buf || buf.size === 0) return; + const t = buf.t[(buf.head - 1 + buf.cap) % buf.cap]; + if (sourceNewest[srcId] === undefined || t > sourceNewest[srcId]) sourceNewest[srcId] = t; + }); + const srcVals = Object.values(sourceNewest); + let now = srcVals.length > 0 ? Math.min(...srcVals) : -Infinity; + if (!isFinite(now)) now = Date.now() / 1000; + return now; +} + // Build uPlot opts for a given plot object function makeUPlotOpts(p, inTrigMode) { const seriesArr = [{}]; // time (index 0) @@ -774,11 +795,16 @@ function makeUPlotOpts(p, inTrigMode) { }, select: { show: true }, scales: { - x: { - time: false, auto: false, - min: p.xRange ? p.xRange[0] : 0, - max: p.xRange ? p.xRange[1] : windowSec - }, + x: (() => { + let xMin, xMax; + if (p.xRange) { + xMin = p.xRange[0]; xMax = p.xRange[1]; + } else { + const now = computePlotNow(p); + xMin = now - windowSec; xMax = now; + } + return { time: false, auto: false, min: xMin, max: xMax }; + })(), y: { auto: true }, }, series: seriesArr, @@ -1005,16 +1031,8 @@ function resampleLinear(tSrc, vSrc, tDst) { function buildLiveData(p) { if (p.traces.length === 0) return [new Float64Array(0)]; - // plotNow = newest timestamp across ALL traces - let plotNow = -Infinity; - p.traces.forEach(key => { - const buf = buffers[key]; - if (buf && buf.size > 0) { - const t = buf.t[(buf.head - 1 + buf.cap) % buf.cap]; - if (t > plotNow) plotNow = t; - } - }); - if (!isFinite(plotNow)) plotNow = Date.now() / 1000; + // plotNow = min(newest per source) so no source shows a blank right edge. + const plotNow = computePlotNow(p); const t0 = p.xRange ? p.xRange[0] : plotNow - windowSec; const t1 = p.xRange ? p.xRange[1] : plotNow; @@ -1564,7 +1582,10 @@ function applyLayout(cls) { // Recreate all uPlot instances once the CSS grid has sized the cells. // This also updates axis visibility (which axes show labels depends on grid position). requestAnimationFrame(() => { - plots.forEach(p => createUPlot(p)); + plots.forEach(p => { + createUPlot(p); + p.needsRedraw = true; // force immediate data+scale refresh so x/y ranges are preserved + }); setTimeout(() => plots.forEach(p => { if (p.uplot) p.uplot.setSize({ width: p.div.clientWidth, height: p.div.clientHeight }); }), 60); @@ -1888,16 +1909,8 @@ function renderDirtyPlots() { // Zoomed: re-apply so scale is correct after setData p.uplot.setScale('x', { min: p.xRange[0], max: p.xRange[1] }); } else { - // Rolling window: compute plotNow fresh each frame (same anchor as buildLiveData) - // and set the scale immediately after setData for correct alignment. - let plotNow = -Infinity; - p.traces.forEach(key => { - const buf = buffers[key]; - if (!buf || buf.size === 0) return; - const t = buf.t[(buf.head - 1 + buf.cap) % buf.cap]; - if (t > plotNow) plotNow = t; - }); - if (!isFinite(plotNow)) plotNow = Date.now() / 1000; + // Rolling window: use same anchor as buildLiveData (min-of-max per source). + const plotNow = computePlotNow(p); p.uplot.setScale('x', { min: plotNow - windowSec, max: plotNow }); } zoomGuard = false; @@ -1965,6 +1978,7 @@ function onSources(msg) { } }); buildSidebar(); + if (statsOpen) _refreshStatsSelector(); } function addSourceWS(label, addr) { @@ -2110,6 +2124,139 @@ function initSignalMenu() { document.addEventListener('keydown', e => { if (e.key === 'Escape') hideSignalMenu(); }); } +/* ════════════════════════════════════════════════════════════════ + Source Statistics panel + ════════════════════════════════════════════════════════════════ */ +const sourceStats = {}; // sourceId → StatInfo (from backend 1 Hz message) +let statsOpen = false; +let statsSelectedSrc = null; // currently displayed source id + +function onStats(msg) { + const incoming = msg.sources || {}; + Object.keys(incoming).forEach(id => { sourceStats[id] = incoming[id]; }); + if (statsOpen) renderStats(); +} + +// Rebuild the source selector options; preserve selection when possible. +function _refreshStatsSelector() { + const sel = document.getElementById('stats-source-sel'); + if (!sel) return; + const prev = statsSelectedSrc; + sel.innerHTML = ''; + const srcs = Object.values(sourcesMap); + srcs.forEach(src => { + const opt = document.createElement('option'); + opt.value = src.id; + opt.textContent = src.label || src.id; + sel.appendChild(opt); + }); + // Restore previous selection or default to first + if (prev && sourcesMap[prev]) { + sel.value = prev; + } else if (srcs.length > 0) { + sel.value = srcs[0].id; + } + statsSelectedSrc = sel.value || null; +} + +// Frontend latency for one source: wallNow − newest calibrated buffer timestamp. +function sourceLatencyMs(srcId) { + const prefix = srcId + ':'; + const wallNow = Date.now() / 1000; + let best = null; + Object.keys(buffers).forEach(key => { + if (!key.startsWith(prefix)) return; + const buf = buffers[key]; + if (!buf || buf.size === 0) return; + const newest = buf.t[(buf.head - 1 + buf.cap) % buf.cap]; + const lag = (wallNow - newest) * 1000; + if (best === null || lag < best) best = lag; + }); + return best; +} + +function _fmtMs(v) { return v != null && isFinite(v) ? v.toFixed(2) + ' ms' : '—'; } +function _fmtHz(v) { return v != null && isFinite(v) && v > 0 ? v.toFixed(2) + ' Hz' : '—'; } +function _fmtKB(v) { return v != null && isFinite(v) ? (v / 1024).toFixed(2) + ' KB' : '—'; } + +function _statsKV(label, value, cls) { + return `
+ * +TimeArrayGAM1 = {
+ * Class = TimeArrayGAM
+ * SamplingRate = 1000000.0 // Sample rate in Hz (must match data signal)
+ * Anchor = FirstSample // FirstSample (default) or LastSample
+ * InputSignals = {
+ * Time = { DataSource = DDB; Type = uint32 }
+ * }
+ * OutputSignals = {
+ * TimeArray = { DataSource = DDB; Type = uint32; NumberOfElements = 1000 }
+ * }
+ * }
+ *
+ *
+ * Exactly one uint32 input signal and one uint32 output array are required.
+ */
+
+#ifndef TIMEARRAYGAM_H_
+#define TIMEARRAYGAM_H_
+
+#include "CompilerTypes.h"
+#include "GAM.h"
+
+namespace MARTe {
+
+class TimeArrayGAM : public GAM {
+public:
+ CLASS_REGISTER_DECLARATION()
+
+ TimeArrayGAM();
+ virtual ~TimeArrayGAM();
+
+ virtual bool Initialise(StructuredDataI &data);
+ virtual bool Setup();
+ virtual bool Execute();
+
+private:
+ float64 samplingRate; /**< Sample rate [Hz] */
+ bool anchorIsFirst; /**< true = FirstSample anchor, false = LastSample */
+ uint32 nElements; /**< Number of output elements */
+ uint32 *inputTime; /**< Pointer to scalar input (microseconds, uint32 from LinuxTimer) */
+ uint64 *outputBuf; /**< Pointer to output array (nanoseconds, uint64) */
+};
+
+} /* namespace MARTe */
+
+#endif /* TIMEARRAYGAM_H_ */
diff --git a/Test/MARTeApp/TestApp.cfg b/Test/MARTeApp/TestApp.cfg
index 17a53fc..bea93d6 100644
--- a/Test/MARTeApp/TestApp.cfg
+++ b/Test/MARTeApp/TestApp.cfg
@@ -1,20 +1,27 @@
/**
* Test MARTe2 application for UDPStreamer DataSource.
*
- * Generates scalar and high-frequency packed signals and streams them via UDPStreamer.
- * Connect with the WebUI client (Client/WebUI) to visualise the signals.
+ * Three independent RT threads demonstrate all four UDPStreamer time modes:
*
- * Signals produced (scalar, 10 kHz):
- * Counter – uint32 cycle counter from LinuxTimer
- * Time – uint32 time in microseconds from LinuxTimer
- * Sine1 – float32, 1 Hz sine, amplitude 10, quantised to uint16 on wire
- * Sine2 – float32, 0.3 Hz sine, amplitude 5, raw float32 on wire
+ * Thread1 – "Streamer" port 44500 (scalar signals, PacketTime)
+ * Counter – uint32 cycle counter
+ * Time – uint32 time in microseconds (LinuxTimer, 1 kHz)
+ * Sine1 – float32, 1 Hz, quantised to uint16 on wire (PacketTime)
+ * Sine2 – float32, 0.3 Hz, raw float32 on wire (PacketTime)
*
- * Signals produced (packed temporal arrays, 10 kHz × 1000 samples = 10 MSps):
- * Ch1 – float32[1000], 1 kHz sine, amplitude 1.0
- * Ch2 – float32[1000], 1 kHz sine, amplitude 0.5, phase π/2
- * Both channels use TimeMode=FirstSample with Time as the anchor and
- * SamplingRate=10000000 so the WebUI reconstructs the per-sample timestamps.
+ * Thread2 – "FastStreamer" port 44501 (packed arrays, FirstSample + LastSample)
+ * Time – uint32 scalar anchor (5 kHz LinuxTimer)
+ * Ch1 – float32[1000], 1 kHz sine (TimeMode = FirstSample)
+ * Ch2 – float32[1000], 10 kHz sine (TimeMode = LastSample)
+ * Both channels use Time as the anchor and SamplingRate = 5 000 000 Hz so
+ * the WebUI reconstructs the 200 ns per-sample timestamps.
+ *
+ * Thread3 – "FullArrStreamer" port 44502 (packed arrays, FullArray)
+ * TimeArray – uint64[1000] per-sample timestamps in ns generated by TimeArrayGAM
+ * Ch3 – float32[1000], 3 kHz sine (TimeMode = FullArray)
+ * Ch4 – float32[1000], 500 Hz sine (TimeMode = FullArray)
+ * The WebUI uses the explicit timestamp for each sample rather than
+ * reconstructing them from a scalar anchor.
*/
$TestApp = {
Class = RealTimeApplication
@@ -46,6 +53,8 @@ $TestApp = {
}
}
}
+
+ // ── 5 kHz fast timer for packed-array threads ────────────────────────
+FastTimerGAM = {
Class = IOGAM
InputSignals = {
@@ -61,7 +70,6 @@ $TestApp = {
Type = uint32
}
}
-
}
// ── 1 Hz sinusoidal signal ───────────────────────────────────────────
@@ -106,14 +114,14 @@ $TestApp = {
}
}
- // ── 1 kHz sine burst – channel 1 (1000 samples/packet at 10 MSps) ──────
+ // ── 1 kHz sine burst – channel 1 (FirstSample anchor) ───────────────
+SineGAM3 = {
Class = SineArrayGAM
Frequency = 1000.0
Amplitude = 1.0
Phase = 0.0
Offset = 0.0
- SamplingRate = 1000000.0
+ SamplingRate = 5000000.0
OutputSignals = {
Ch1 = {
DataSource = DDB2
@@ -124,14 +132,14 @@ $TestApp = {
}
}
- // ── 1 kHz sine burst – channel 2 (phase-shifted by π/2) ──────────────
+ // ── 10 kHz sine burst – channel 2 (LastSample anchor) ───────────────
+SineGAM4 = {
Class = SineArrayGAM
Frequency = 10000.0
Amplitude = 0.5
Phase = 1.5708
Offset = 0.0
- SamplingRate = 1000000.0
+ SamplingRate = 5000000.0
OutputSignals = {
Ch2 = {
DataSource = DDB2
@@ -142,7 +150,7 @@ $TestApp = {
}
}
- // ── Route signals into UDPStreamer ────────────────────────────────────
+ // ── Route scalar signals → Streamer ──────────────────────────────────
+StreamerGAM = {
Class = IOGAM
InputSignals = {
@@ -182,6 +190,8 @@ $TestApp = {
}
}
}
+
+ // ── Route packed arrays → FastStreamer (FirstSample + LastSample) ────
+FastStreamerGAM = {
Class = IOGAM
InputSignals = {
@@ -221,21 +231,146 @@ $TestApp = {
}
}
}
+
+ // ── 3 kHz sine burst – channel 3 (FullArray anchor) ─────────────────
+ +SineGAM5 = {
+ Class = SineArrayGAM
+ Frequency = 3000.0
+ Amplitude = 2.0
+ Phase = 0.0
+ Offset = 0.0
+ SamplingRate = 5000000.0
+ OutputSignals = {
+ Ch3 = {
+ DataSource = DDB3
+ Type = float32
+ NumberOfDimensions = 1
+ NumberOfElements = 1000
+ }
+ }
+ }
+
+ // ── 500 Hz sine burst – channel 4 (FullArray anchor) ─────────────────
+ +SineGAM6 = {
+ Class = SineArrayGAM
+ Frequency = 500.0
+ Amplitude = 3.0
+ Phase = 0.7854
+ Offset = 0.0
+ SamplingRate = 5000000.0
+ OutputSignals = {
+ Ch4 = {
+ DataSource = DDB3
+ Type = float32
+ NumberOfDimensions = 1
+ NumberOfElements = 1000
+ }
+ }
+ }
+
+ // ── Build per-sample time array for FullArray channels ────────────────
+ // TimeArrayGAM expands the scalar LinuxTimer Time (uint32, µs) into a
+ // uint64[1000] array in nanoseconds where element[k] = Time_ns + k * period_ns.
+ // Using ns preserves sub-µs resolution at sampling rates > 1 MHz.
+ +TimeArrayGAM1 = {
+ Class = TimeArrayGAM
+ SamplingRate = 5000000.0
+ Anchor = FirstSample
+ InputSignals = {
+ Time = {
+ DataSource = DDB3
+ Type = uint32
+ }
+ }
+ OutputSignals = {
+ TimeArray = {
+ DataSource = DDB3
+ Type = uint64
+ NumberOfDimensions = 1
+ NumberOfElements = 1000
+ }
+ }
+ }
+
+ // ── Fast timer for FullArray thread ───────────────────────────────────
+ +FullArrTimerGAM = {
+ Class = IOGAM
+ InputSignals = {
+ Time = {
+ Frequency = 5000
+ DataSource = FullArrTimer
+ Type = uint32
+ }
+ }
+ OutputSignals = {
+ Time = {
+ DataSource = DDB3
+ Type = uint32
+ }
+ }
+ }
+
+ // ── Route FullArray channels → FullArrStreamer ─────────────────────
+ +FullArrStreamerGAM = {
+ Class = IOGAM
+ InputSignals = {
+ TimeArray = {
+ DataSource = DDB3
+ Type = uint64
+ NumberOfDimensions = 1
+ NumberOfElements = 1000
+ }
+ Ch3 = {
+ DataSource = DDB3
+ Type = float32
+ NumberOfDimensions = 1
+ NumberOfElements = 1000
+ }
+ Ch4 = {
+ DataSource = DDB3
+ Type = float32
+ NumberOfDimensions = 1
+ NumberOfElements = 1000
+ }
+ }
+ OutputSignals = {
+ TimeArray = {
+ DataSource = FullArrStreamer
+ Type = uint64
+ NumberOfDimensions = 1
+ NumberOfElements = 1000
+ }
+ Ch3 = {
+ DataSource = FullArrStreamer
+ Type = float32
+ NumberOfDimensions = 1
+ NumberOfElements = 1000
+ }
+ Ch4 = {
+ DataSource = FullArrStreamer
+ Type = float32
+ NumberOfDimensions = 1
+ NumberOfElements = 1000
+ }
+ }
+ }
}
+Data = {
Class = ReferenceContainer
DefaultDataSource = DDB
- // ── Inter-GAM data buffer ────────────────────────────────────────────
+DDB = {
Class = GAMDataSource
}
+DDB2 = {
Class = GAMDataSource
}
+ +DDB3 = {
+ Class = GAMDataSource
+ }
- // ── Real-time clock / trigger source ─────────────────────────────────
+ // ── 1 kHz real-time clock (Thread1) ──────────────────────────────────
+Timer = {
Class = LinuxTimer
SleepNature = "Default"
@@ -248,6 +383,8 @@ $TestApp = {
}
}
}
+
+ // ── 5 kHz fast timer (Thread2 – FirstSample / LastSample) ────────────
+FastTimer = {
Class = LinuxTimer
SleepNature = "Default"
@@ -261,7 +398,21 @@ $TestApp = {
}
}
- // ── UDP Streamer DataSource ──────────────────────────────────────────
+ // ── 5 kHz fast timer (Thread3 – FullArray) ───────────────────────────
+ +FullArrTimer = {
+ Class = LinuxTimer
+ SleepNature = "Default"
+ Signals = {
+ Counter = {
+ Type = uint32
+ }
+ Time = {
+ Type = uint32
+ }
+ }
+ }
+
+ // ── Streamer: scalar signals, PacketTime (port 44500) ─────────────────
+Streamer = {
Class = UDPStreamer
Port = 44500
@@ -289,6 +440,19 @@ $TestApp = {
}
}
}
+
+ // ── FastStreamer: packed arrays, FirstSample + LastSample (port 44501)
+ //
+ // Ch1 uses TimeMode = FirstSample:
+ // Time is the timestamp of sample [0]; later samples are extrapolated
+ // forward: t[k] = Time + k / SamplingRate.
+ //
+ // Ch2 uses TimeMode = LastSample:
+ // Time is the timestamp of sample [N-1]; earlier samples are
+ // extrapolated backward: t[k] = Time - (N-1-k) / SamplingRate.
+ //
+ // Both modes produce identical wall-clock placements for a fixed-rate
+ // signal and are shown here side-by-side for comparison.
+FastStreamer = {
Class = UDPStreamer
Port = 44501
@@ -312,14 +476,49 @@ $TestApp = {
Unit = "V"
NumberOfDimensions = 1
NumberOfElements = 1000
- TimeMode = FirstSample
+ TimeMode = LastSample
TimeSignal = Time
SamplingRate = 5000000.0
}
}
}
- // ── Timing statistics ────────────────────────────────────────────────
+ // ── FullArrStreamer: packed arrays, FullArray (port 44502) ─────────────
+ //
+ // TimeMode = FullArray: the TimeSignal (TimeArray) has the same
+ // NumberOfElements as the data channel. Each sample pair
+ // (TimeArray[k], Ch3[k]) provides its own independent timestamp.
+ // This mode handles non-uniform sampling and explicit per-sample clocks.
+ +FullArrStreamer = {
+ Class = UDPStreamer
+ Port = 44502
+ MaxPayloadSize = 1400
+ Signals = {
+ TimeArray = {
+ Type = uint64
+ Unit = "ns"
+ NumberOfDimensions = 1
+ NumberOfElements = 1000
+ }
+ Ch3 = {
+ Type = float32
+ Unit = "V"
+ NumberOfDimensions = 1
+ NumberOfElements = 1000
+ TimeMode = FullArray
+ TimeSignal = TimeArray
+ }
+ Ch4 = {
+ Type = float32
+ Unit = "V"
+ NumberOfDimensions = 1
+ NumberOfElements = 1000
+ TimeMode = FullArray
+ TimeSignal = TimeArray
+ }
+ }
+ }
+
+Timings = {
Class = TimingDataSource
}
@@ -331,16 +530,24 @@ $TestApp = {
Class = RealTimeState
+Threads = {
Class = ReferenceContainer
+ // Thread1: scalar signals at 1 kHz
+Thread1 = {
Class = RealTimeThread
CPUs = 0x1
Functions = {TimerGAM SineGAM1 SineGAM2 StreamerGAM}
}
+ // Thread2: packed arrays at 5 kHz (FirstSample + LastSample)
+Thread2 = {
Class = RealTimeThread
CPUs = 0x2
Functions = {FastTimerGAM SineGAM3 SineGAM4 FastStreamerGAM}
}
+ // Thread3: packed arrays at 5 kHz (FullArray with explicit timestamps)
+ +Thread3 = {
+ Class = RealTimeThread
+ CPUs = 0x4
+ Functions = {FullArrTimerGAM SineGAM5 SineGAM6 TimeArrayGAM1 FullArrStreamerGAM}
+ }
}
}
}
diff --git a/Test/MARTeApp/run.sh b/Test/MARTeApp/run.sh
index 6dad3ae..854241d 100755
--- a/Test/MARTeApp/run.sh
+++ b/Test/MARTeApp/run.sh
@@ -6,7 +6,10 @@
# ./run.sh --webui # also start the WebUI Go client in the background
# ./run.sh --help # show this message
#
-# The WebUI is started with --source "TestApp@127.0.0.1:44500".
+# The WebUI is started with three sources:
+# Streamer @ 127.0.0.1:44500 (scalar signals, PacketTime, 1 kHz)
+# FastStreamer @ 127.0.0.1:44501 (packed arrays, FirstSample/LastSample, 5 kHz)
+# FullArrStreamer @ 127.0.0.1:44502 (packed arrays, FullArray, 5 kHz)
# Additional sources and a persistent source list file (--sources-file) can
# be configured directly in the WebUI or by editing this script.
#
@@ -28,7 +31,7 @@ for arg in "$@"; do
case "$arg" in
--webui) START_WEBUI=1 ;;
--help|-h)
- sed -n '2,12p' "$0"
+ sed -n '2,15p' "$0"
exit 0
;;
esac
@@ -54,6 +57,8 @@ UDPSTREAMER_SRC="${REPO_ROOT}/Source/Components/DataSources/UDPStreamer"
UDPSTREAMER_LIB="${REPO_ROOT}/Build/${TARGET}/Components/DataSources/UDPStreamer"
SINEARRAYGAM_SRC="${REPO_ROOT}/Source/Components/GAMs/SineArrayGAM"
SINEARRAYGAM_LIB="${REPO_ROOT}/Build/${TARGET}/Components/GAMs/SineArrayGAM"
+TIMEARRAYGAM_SRC="${REPO_ROOT}/Source/Components/GAMs/TimeArrayGAM"
+TIMEARRAYGAM_LIB="${REPO_ROOT}/Build/${TARGET}/Components/GAMs/TimeArrayGAM"
echo "==> Building UDPStreamer (TARGET=${TARGET})..."
make -C "${UDPSTREAMER_SRC}" \
@@ -66,6 +71,12 @@ make -C "${SINEARRAYGAM_SRC}" \
-f Makefile.gcc \
TARGET="${TARGET}" \
2>&1 | tail -5
+
+echo "==> Building TimeArrayGAM (TARGET=${TARGET})..."
+make -C "${TIMEARRAYGAM_SRC}" \
+ -f Makefile.gcc \
+ TARGET="${TARGET}" \
+ 2>&1 | tail -5
echo "==> Build done."
# ── Build WebUI binary (if requested and not already built) ──────────────────
@@ -83,6 +94,7 @@ COMP="${MARTe2_Components_DIR}/Build/${TARGET}/Components"
export LD_LIBRARY_PATH="\
${UDPSTREAMER_LIB}:\
${SINEARRAYGAM_LIB}:\
+${TIMEARRAYGAM_LIB}:\
${MARTe2_DIR}/Build/${TARGET}/Core:\
${COMP}/DataSources/LinuxTimer:\
${COMP}/DataSources/LoggerDataSource:\
@@ -112,6 +124,7 @@ if [ "${START_WEBUI}" -eq 1 ]; then
"${WEBUI_BIN}" \
--source "Streamer@127.0.0.1:44500" \
--source "FastStreamer@127.0.0.1:44501" \
+ --source "FullArrStreamer@127.0.0.1:44502" \
--listen :8080 &
WEBUI_PID=$!
echo "==> WebUI PID ${WEBUI_PID}"
@@ -126,7 +139,10 @@ if [ ! -x "${MARTE_APP}" ]; then
exit 1
fi
-echo "==> Starting MARTe2 application (state=Running, 100 Hz)..."
+echo "==> Starting MARTe2 application (state=Running)..."
+echo "==> Thread1: scalar signals 1 kHz → port 44500"
+echo "==> Thread2: packed arrays 5 kHz → port 44501 (FirstSample / LastSample)"
+echo "==> Thread3: FullArray arrays 5 kHz → port 44502 (FullArray)"
echo "==> Press Ctrl+C to stop."
echo ""