From 62545503c44bf2c288bf1a3d56e38195133dbee1 Mon Sep 17 00:00:00 2001 From: Martino Ferrari Date: Wed, 20 May 2026 00:15:38 +0200 Subject: [PATCH] Improved uo and added timerarraygam for testing --- Client/WebUI/hub.go | 102 ++++++- Client/WebUI/static/app.js | 205 ++++++++++++-- Client/WebUI/static/index.html | 10 + Client/WebUI/static/style.css | 64 +++++ Client/WebUI/stats.go | 155 +++++++++++ Client/WebUI/udpclient.go | 3 + Source/Components/GAMs/Makefile.inc | 3 +- .../Components/GAMs/TimeArrayGAM/Makefile.gcc | 1 + .../Components/GAMs/TimeArrayGAM/Makefile.inc | 28 ++ .../GAMs/TimeArrayGAM/TimeArrayGAM.cpp | 108 ++++++++ .../GAMs/TimeArrayGAM/TimeArrayGAM.h | 64 +++++ Test/MARTeApp/TestApp.cfg | 253 ++++++++++++++++-- Test/MARTeApp/run.sh | 22 +- 13 files changed, 960 insertions(+), 58 deletions(-) create mode 100644 Client/WebUI/stats.go create mode 100644 Source/Components/GAMs/TimeArrayGAM/Makefile.gcc create mode 100644 Source/Components/GAMs/TimeArrayGAM/Makefile.inc create mode 100644 Source/Components/GAMs/TimeArrayGAM/TimeArrayGAM.cpp create mode 100644 Source/Components/GAMs/TimeArrayGAM/TimeArrayGAM.h 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 `
${label}${value}
`; +} + +function _histHTML(si) { + if (!si.cycleHist || !si.cycleHist.length) return ''; + const maxC = Math.max(...si.cycleHist, 1); + const bars = si.cycleHist.map(c => { + const pct = Math.max(Math.round((c / maxC) * 100), 1); + return `
`; + }).join(''); + return `
+
${bars}
+
${si.cycleHistMin.toFixed(3)}${si.cycleHistMax.toFixed(3)} ms
+
`; +} + +function renderStats() { + const body = document.getElementById('stats-body'); + if (!body) return; + + const src = statsSelectedSrc ? sourcesMap[statsSelectedSrc] : null; + if (!src) { + body.innerHTML = 'No source selected'; + return; + } + + const si = sourceStats[src.id]; + const latMs = sourceLatencyMs(src.id); + const lossColor = si && si.totalLost > 0 ? 'warn' : 'ok'; + const lossText = si ? `${si.totalLost} / ${si.totalReceived}` : '—'; + + body.innerHTML = ` +
+ +
+ ${_statsKV('Address', src.addr)} + ${_statsKV('Latency', _fmtMs(latMs))} + ${_statsKV('Lost / Rx', lossText, lossColor)} + ${_statsKV('Loss %', si && si.totalReceived > 0 ? (si.totalLost / si.totalReceived * 100).toFixed(2) + ' %' : '—', si && si.totalLost > 0 ? 'warn' : 'ok')} +
+
+
+
+ +
+ ${_statsKV('avg', si ? _fmtHz(si.rateHz) : '—')} + ${_statsKV('± σ', si ? _fmtHz(si.rateStdHz) : '—')} + ${_statsKV('Pkts / cycle', si ? si.fragsPerCycle.toFixed(1) : '—')} + ${_statsKV('KB / cycle', si ? _fmtKB(si.bytesPerCycle) : '—')} +
+
+
+
+ +
+ ${_statsKV('avg', si ? _fmtMs(si.cycleAvgMs) : '—')} + ${_statsKV('σ', si ? _fmtMs(si.cycleStdMs) : '—')} + ${_statsKV('min', si ? _fmtMs(si.cycleMinMs) : '—')} + ${_statsKV('max', si ? _fmtMs(si.cycleMaxMs) : '—')} +
+ ${si ? _histHTML(si) : 'No data yet'} +
`; +} + +function toggleStats() { + statsOpen = !statsOpen; + document.getElementById('stats-panel').classList.toggle('open', statsOpen); + document.getElementById('btn-stats').classList.toggle('active', statsOpen); + if (statsOpen) { + _refreshStatsSelector(); + renderStats(); + } +} + +// Refresh latency and stats every second while panel is open. +setInterval(() => { if (statsOpen) renderStats(); }, 1000); + /* ════════════════════════════════════════════════════════════════ Init ════════════════════════════════════════════════════════════════ */ @@ -2118,6 +2265,12 @@ applyLayout('l1x1'); buildSidebar(); // show "Add Source" section even before WS connection initSignalMenu(); document.getElementById('btn-csv-all').addEventListener('click', exportAllCSV); +document.getElementById('btn-stats').addEventListener('click', toggleStats); +document.getElementById('btn-stats-close').addEventListener('click', toggleStats); +document.getElementById('stats-source-sel').addEventListener('change', e => { + statsSelectedSrc = e.target.value || null; + renderStats(); +}); connectWS(); requestAnimationFrame(renderDirtyPlots); fetch('/version').then(r => r.text()).then(v => { diff --git a/Client/WebUI/static/index.html b/Client/WebUI/static/index.html index d006df3..f7d6709 100644 --- a/Client/WebUI/static/index.html +++ b/Client/WebUI/static/index.html @@ -97,12 +97,22 @@
+ +
+
+ Source Statistics + + +
+
+
Disconnected +
diff --git a/Client/WebUI/static/style.css b/Client/WebUI/static/style.css index 6f742b0..d5bc57d 100644 --- a/Client/WebUI/static/style.css +++ b/Client/WebUI/static/style.css @@ -377,6 +377,70 @@ input[type=range].trig-range::-webkit-slider-thumb { .save-src-btn { color:var(--green); } .save-src-btn:hover { background:rgba(166,227,161,0.1); border-color:var(--green); } +/* ── Stats panel ─────────────────────────────────────────────── */ +#stats-panel { + position:fixed; bottom:var(--statusbar-h); left:0; right:0; + height:0; overflow:hidden; + background:var(--mantle); border-top:2px solid var(--surface0); + z-index:89; /* below topbar / sidebar */ + transition:height var(--transition); +} +#stats-panel.open { height:290px; } +#stats-panel-hdr { + display:flex; align-items:center; gap:8px; + padding:4px 10px; border-bottom:1px solid var(--surface0); flex-shrink:0; + font-size:11px; font-weight:700; color:var(--subtext1); letter-spacing:0.5px; text-transform:uppercase; +} +.stats-hdr-label { flex-shrink:0; } +.stats-source-sel { + flex:1; min-width:0; + background:var(--surface0); border:1px solid var(--surface1); border-radius:var(--radius); + color:var(--text); font-size:11px; padding:1px 5px; cursor:pointer; +} +.stats-source-sel:focus { outline:none; border-color:var(--accent); } +#btn-stats-close { + background:none; border:none; color:var(--overlay0); flex-shrink:0; + cursor:pointer; font-size:14px; line-height:1; padding:2px; + transition:color var(--transition); +} +#btn-stats-close:hover { color:var(--red); } +#stats-body { + overflow-x:hidden; overflow-y:auto; + display:flex; flex-direction:column; gap:0; + padding:8px 18px; + height:calc(290px - 30px); box-sizing:border-box; +} +.stats-section { + display:flex; flex-direction:column; gap:5px; padding:4px 0; +} +.stats-section-grow { flex:1; } +.stats-empty { font-size:11px; color:var(--overlay0); } +.stats-section-label { + font-size:9px; color:var(--overlay0); text-transform:uppercase; letter-spacing:0.6px; +} +.stats-row { display:flex; gap:16px; flex-wrap:wrap; align-items:flex-end; } +.stats-kv { display:flex; flex-direction:column; gap:1px; min-width:70px; } +.stats-k { font-size:9px; color:var(--overlay0); text-transform:uppercase; letter-spacing:0.5px; } +.stats-v { font-size:12px; color:var(--text); font-family:monospace; font-weight:600; } +.stats-v.warn { color:var(--yellow); } +.stats-v.ok { color:var(--green); } +.stats-sep { border:none; border-top:1px solid var(--surface0); margin:2px 0; } +/* Histogram */ +.stats-hist { width:100%; } +.hist-bars { + display:flex; align-items:flex-end; gap:1px; height:72px; + background:var(--crust); border-radius:3px; padding:2px 3px; +} +.hist-bar { + flex:1; background:var(--accent); border-radius:1px 1px 0 0; min-height:1px; + opacity:0.65; transition:opacity 0.1s; +} +.hist-bar:hover { opacity:1; } +.hist-labels { + display:flex; justify-content:space-between; + font-size:9px; color:var(--overlay0); font-family:monospace; margin-top:2px; +} + /* ── Empty state ──────────────────────────────────────────────── */ #empty-state { position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); diff --git a/Client/WebUI/stats.go b/Client/WebUI/stats.go new file mode 100644 index 0000000..a594c68 --- /dev/null +++ b/Client/WebUI/stats.go @@ -0,0 +1,155 @@ +package main + +import ( + "math" + "sync" +) + +const statRingSize = 512 + +// SourceStat accumulates per-source UDP performance metrics. +// Thread-safe; RecordFragment is called from the UDPClient goroutine. +type SourceStat struct { + mu sync.Mutex + + seenFirst bool + lastCounter uint32 + TotalRx uint64 + TotalLost uint64 + + // Cycle-time ring (seconds between consecutive DATA completions) + ctRing [statRingSize]float64 + ctHead int + ctFull bool + lastRxNs int64 + + // Per-cycle accumulators (reset after each DATA completion) + fragCount int + byteCount int + + fragRing [statRingSize]int + byteRing [statRingSize]int +} + +// RecordFragment is called for every UDP datagram of a DATA packet. +// complete: this fragment completed the DATA reassembly. +// nBytes: raw datagram size (header+payload). +func (s *SourceStat) RecordFragment(counter uint32, nBytes int, arrivalNs int64, complete bool) { + s.mu.Lock() + defer s.mu.Unlock() + + s.fragCount++ + s.byteCount += nBytes + + if !complete { + return + } + + s.TotalRx++ + if s.seenFirst { + if delta := counter - s.lastCounter; delta > 1 { + s.TotalLost += uint64(delta - 1) + } + } else { + s.seenFirst = true + } + s.lastCounter = counter + + if s.lastRxNs != 0 { + idx := s.ctHead + s.ctRing[idx] = float64(arrivalNs-s.lastRxNs) * 1e-9 + s.fragRing[idx] = s.fragCount + s.byteRing[idx] = s.byteCount + s.ctHead = (s.ctHead + 1) % statRingSize + if s.ctHead == 0 { + s.ctFull = true + } + } + s.lastRxNs = arrivalNs + s.fragCount = 0 + s.byteCount = 0 +} + +// Snapshot computes and returns a StatInfo for broadcast. +func (s *SourceStat) Snapshot() StatInfo { + s.mu.Lock() + defer s.mu.Unlock() + + n := s.ctHead + if s.ctFull { + n = statRingSize + } + + si := StatInfo{TotalReceived: s.TotalRx, TotalLost: s.TotalLost} + if n == 0 { + return si + } + + sum, sumSq := 0.0, 0.0 + minV, maxV := math.MaxFloat64, 0.0 + fragSum, byteSum := 0, 0 + for i := 0; i < n; i++ { + v := s.ctRing[i] + sum += v + sumSq += v * v + if v < minV { + minV = v + } + if v > maxV { + maxV = v + } + fragSum += s.fragRing[i] + byteSum += s.byteRing[i] + } + avg := sum / float64(n) + variance := sumSq/float64(n) - avg*avg + if variance < 0 { + variance = 0 + } + stdv := math.Sqrt(variance) + + si.CycleAvgMs = avg * 1e3 + si.CycleStdMs = stdv * 1e3 + si.CycleMinMs = minV * 1e3 + si.CycleMaxMs = maxV * 1e3 + si.RateHz = 1.0 / avg + si.RateStdHz = stdv / (avg * avg) + si.FragsPerCycle = float64(fragSum) / float64(n) + si.BytesPerCycle = float64(byteSum) / float64(n) + + const nBins = 20 + si.CycleHistMin = minV * 1e3 + si.CycleHistMax = maxV * 1e3 + si.CycleHist = make([]int, nBins) + span := maxV - minV + for i := 0; i < n; i++ { + var bin int + if span > 0 { + bin = int((s.ctRing[i] - minV) / span * float64(nBins)) + if bin >= nBins { + bin = nBins - 1 + } + } else { + bin = nBins / 2 + } + si.CycleHist[bin]++ + } + return si +} + +// StatInfo is the JSON snapshot for one source sent to the frontend. +type StatInfo struct { + TotalReceived uint64 `json:"totalReceived"` + TotalLost uint64 `json:"totalLost"` + RateHz float64 `json:"rateHz"` + RateStdHz float64 `json:"rateStdHz"` + FragsPerCycle float64 `json:"fragsPerCycle"` + BytesPerCycle float64 `json:"bytesPerCycle"` + CycleAvgMs float64 `json:"cycleAvgMs"` + CycleStdMs float64 `json:"cycleStdMs"` + CycleMinMs float64 `json:"cycleMinMs"` + CycleMaxMs float64 `json:"cycleMaxMs"` + CycleHist []int `json:"cycleHist"` + CycleHistMin float64 `json:"cycleHistMin"` + CycleHistMax float64 `json:"cycleHistMax"` +} diff --git a/Client/WebUI/udpclient.go b/Client/WebUI/udpclient.go index de31cf0..4eb05bc 100644 --- a/Client/WebUI/udpclient.go +++ b/Client/WebUI/udpclient.go @@ -110,6 +110,9 @@ func (u *UDPClient) runSession() error { copy(payload, buf[HeaderSize:n]) complete, ok := reassembler.AddFragment(hdr, payload) + if hdr.Type == PktData { + u.hub.RecordDataFragment(u.sourceID, hdr.Counter, n, arrivalTime.UnixNano(), ok) + } if !ok { continue } diff --git a/Source/Components/GAMs/Makefile.inc b/Source/Components/GAMs/Makefile.inc index e43196d..f7f9455 100644 --- a/Source/Components/GAMs/Makefile.inc +++ b/Source/Components/GAMs/Makefile.inc @@ -24,7 +24,8 @@ OBJSX= -SPB = SineArrayGAM.x +SPB = SineArrayGAM.x \ + TimeArrayGAM.x PACKAGE=Components ROOT_DIR=../../.. diff --git a/Source/Components/GAMs/TimeArrayGAM/Makefile.gcc b/Source/Components/GAMs/TimeArrayGAM/Makefile.gcc new file mode 100644 index 0000000..ab19097 --- /dev/null +++ b/Source/Components/GAMs/TimeArrayGAM/Makefile.gcc @@ -0,0 +1 @@ +include Makefile.inc diff --git a/Source/Components/GAMs/TimeArrayGAM/Makefile.inc b/Source/Components/GAMs/TimeArrayGAM/Makefile.inc new file mode 100644 index 0000000..a4766c3 --- /dev/null +++ b/Source/Components/GAMs/TimeArrayGAM/Makefile.inc @@ -0,0 +1,28 @@ +OBJSX = TimeArrayGAM.x + +PACKAGE=Components/GAMs +ROOT_DIR=../../../../ +MAKEDEFAULTDIR=$(MARTe2_DIR)/MakeDefaults + +include $(MAKEDEFAULTDIR)/MakeStdLibDefs.$(TARGET) + +INCLUDES += -I. +INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L0Types +INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L1Portability +INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L2Objects +INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L3Streams +INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L4Messages +INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L4Configuration +INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L5GAMs +INCLUDES += -I$(MARTe2_DIR)/Source/Core/Scheduler/L1Portability +INCLUDES += -I$(MARTe2_DIR)/Source/Core/Scheduler/L3Services +INCLUDES += -I$(MARTe2_DIR)/Source/Core/Scheduler/L4Messages + +all: $(OBJS) \ + $(BUILD_DIR)/TimeArrayGAM$(LIBEXT) \ + $(BUILD_DIR)/TimeArrayGAM$(DLLEXT) + echo $(OBJS) + +-include depends.$(TARGET) + +include $(MAKEDEFAULTDIR)/MakeStdLibRules.$(TARGET) diff --git a/Source/Components/GAMs/TimeArrayGAM/TimeArrayGAM.cpp b/Source/Components/GAMs/TimeArrayGAM/TimeArrayGAM.cpp new file mode 100644 index 0000000..bc475c6 --- /dev/null +++ b/Source/Components/GAMs/TimeArrayGAM/TimeArrayGAM.cpp @@ -0,0 +1,108 @@ +/** + * @file TimeArrayGAM.cpp + * @brief Source file for class TimeArrayGAM + * @date 19/05/2026 + * @author Martino Ferrari + */ + +#define DLL_API + +#include "AdvancedErrorManagement.h" +#include "TimeArrayGAM.h" + +namespace MARTe { + +TimeArrayGAM::TimeArrayGAM() : + GAM(), + samplingRate(1000000.0), + anchorIsFirst(true), + nElements(0u), + inputTime(NULL_PTR(uint32 *)), + outputBuf(NULL_PTR(uint64 *)) { +} + +TimeArrayGAM::~TimeArrayGAM() { +} + +bool TimeArrayGAM::Initialise(StructuredDataI &data) { + bool ok = GAM::Initialise(data); + if (ok) { + if (!data.Read("SamplingRate", samplingRate) || samplingRate <= 0.0) { + REPORT_ERROR(ErrorManagement::InitialisationError, + "TimeArrayGAM: SamplingRate > 0 is required."); + ok = false; + } + } + if (ok) { + StreamString anchor; + (void) data.Read("Anchor", anchor); + if (anchor.Size() == 0u || anchor == "FirstSample") { + anchorIsFirst = true; /* default */ + } + else if (anchor == "LastSample") { + anchorIsFirst = false; + } + else { + REPORT_ERROR(ErrorManagement::InitialisationError, + "TimeArrayGAM: Anchor must be 'FirstSample' or 'LastSample'."); + ok = false; + } + } + return ok; +} + +bool TimeArrayGAM::Setup() { + bool ok = (GetNumberOfInputSignals() == 1u) && (GetNumberOfOutputSignals() == 1u); + if (!ok) { + REPORT_ERROR(ErrorManagement::InitialisationError, + "TimeArrayGAM: exactly one input and one output signal are required."); + return false; + } + + inputTime = reinterpret_cast(GetInputSignalMemory(0u)); + outputBuf = reinterpret_cast(GetOutputSignalMemory(0u)); + ok = (inputTime != NULL_PTR(uint32 *)) && (outputBuf != NULL_PTR(uint64 *)); + if (!ok) { + REPORT_ERROR(ErrorManagement::InitialisationError, + "TimeArrayGAM: failed to resolve signal memory."); + return false; + } + + uint32 sz = 0u; + ok = GetSignalByteSize(OutputSignals, 0u, sz); + if (ok) { + nElements = sz / static_cast(sizeof(uint32)); + ok = (nElements > 0u); + } + if (!ok) { + REPORT_ERROR(ErrorManagement::InitialisationError, + "TimeArrayGAM: output signal must be a non-empty uint32 array."); + } + return ok; +} + +bool TimeArrayGAM::Execute() { + /* Period in nanoseconds — uint64 preserves sub-microsecond resolution + * even at sampling rates > 1 MHz where the µs period would be < 1. */ + uint64 periodNs = static_cast(1000000000.0 / samplingRate + 0.5); + /* Input is uint32 microseconds (LinuxTimer); convert to nanoseconds. */ + uint64 anchorNs = static_cast(*inputTime) * 1000u; + + if (anchorIsFirst) { + /* out[k] = anchorNs + k * periodNs */ + for (uint32 k = 0u; k < nElements; k++) { + outputBuf[k] = anchorNs + static_cast(k) * periodNs; + } + } + else { + /* out[k] = anchorNs - (N-1-k) * periodNs */ + for (uint32 k = 0u; k < nElements; k++) { + outputBuf[k] = anchorNs - static_cast(nElements - 1u - k) * periodNs; + } + } + return true; +} + +CLASS_REGISTER(TimeArrayGAM, "1.0") + +} /* namespace MARTe */ diff --git a/Source/Components/GAMs/TimeArrayGAM/TimeArrayGAM.h b/Source/Components/GAMs/TimeArrayGAM/TimeArrayGAM.h new file mode 100644 index 0000000..0fcb5d8 --- /dev/null +++ b/Source/Components/GAMs/TimeArrayGAM/TimeArrayGAM.h @@ -0,0 +1,64 @@ +/** + * @file TimeArrayGAM.h + * @brief GAM that expands a scalar timer value into a per-sample uint32 time array. + * @date 19/05/2026 + * @author Martino Ferrari + * + * @details Each Execute() call reads one uint32 scalar input (time in microseconds, + * e.g. from LinuxTimer) and fills a uint32[N] output array where element[k] holds + * the reconstructed timestamp of sample k: + * + * Anchor = FirstSample: out[k] = input + k * period_us + * Anchor = LastSample: out[k] = input - (N-1-k) * period_us + * + * The resulting time array is suitable as the TimeSignal for a UDPStreamer signal + * configured with TimeMode = FullArray, providing exact per-sample timestamps. + * + * Configuration: + *
+ * +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 ""