Improved uo and added timerarraygam for testing

This commit is contained in:
Martino Ferrari
2026-05-20 00:15:38 +02:00
parent 620542a722
commit 62545503c4
13 changed files with 960 additions and 58 deletions
+97 -5
View File
@@ -156,6 +156,9 @@ type Hub struct {
// ringsMu protects the map structure; each sigRing has its own RWMutex for data. // ringsMu protects the map structure; each sigRing has its own RWMutex for data.
ringsMu sync.RWMutex ringsMu sync.RWMutex
rings map[string]*sigRing // "sourceId:signalKey" → ring rings map[string]*sigRing // "sourceId:signalKey" → ring
statsMu sync.RWMutex
statsMap map[string]*SourceStat
} }
// NewHub creates an initialised Hub. // NewHub creates an initialised Hub.
@@ -168,6 +171,7 @@ func NewHub() *Hub {
dataCh: make(chan taggedSample, 256), dataCh: make(chan taggedSample, 256),
commandCh: make(chan hubCmd, 64), commandCh: make(chan hubCmd, 64),
rings: make(map[string]*sigRing), rings: make(map[string]*sigRing),
statsMap: make(map[string]*SourceStat),
} }
} }
@@ -321,6 +325,9 @@ func (h *Hub) Run() {
ticker := time.NewTicker(time.Second / 30) ticker := time.NewTicker(time.Second / 30)
defer ticker.Stop() defer ticker.Stop()
statsTicker := time.NewTicker(time.Second)
defer statsTicker.Stop()
sourcesMap := make(map[string]*sourceHubState) sourcesMap := make(map[string]*sourceHubState)
var sourcesMsg []byte var sourcesMsg []byte
@@ -367,6 +374,9 @@ func (h *Hub) Run() {
connState: "connecting", connState: "connecting",
timeSigCalib: make(map[string]float64), timeSigCalib: make(map[string]float64),
} }
h.statsMu.Lock()
h.statsMap[cmd.sourceID] = &SourceStat{}
h.statsMu.Unlock()
rebuildSources() rebuildSources()
case "removeSource": case "removeSource":
@@ -380,6 +390,9 @@ func (h *Hub) Run() {
} }
} }
h.ringsMu.Unlock() h.ringsMu.Unlock()
h.statsMu.Lock()
delete(h.statsMap, cmd.sourceID)
h.statsMu.Unlock()
rebuildSources() rebuildSources()
case "setSourceState": case "setSourceState":
@@ -416,7 +429,7 @@ func (h *Hub) Run() {
} }
for _, sig := range cmd.sigs { for _, sig := range cmd.sigs {
ne := sig.NumElements() ne := sig.NumElements()
isTemporal := ne > 1 && (sig.TimeMode == TimeModeFirstSample || sig.TimeMode == TimeModeLastSample) isTemporal := ne > 1 && sig.TimeMode != TimeModePacket
if isTemporal { if isTemporal {
h.rings[pfxUpd+sig.Name] = newSigRing(ringCapTemporal) h.rings[pfxUpd+sig.Name] = newSigRing(ringCapTemporal)
} else if ne == 1 { } else if ne == 1 {
@@ -466,6 +479,18 @@ func (h *Hub) Run() {
h.broadcast(msg) 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 { for _, sig := range sigs {
n := sig.NumElements() n := sig.NumElements()
isTemporal := n > 1 && (sig.TimeMode == TimeModeFirstSample || sig.TimeMode == TimeModeLastSample)
switch { switch {
case isTemporal: case n > 1 && (sig.TimeMode == TimeModeFirstSample || sig.TimeMode == TimeModeLastSample):
hasTimeSig := sig.TimeSignalIdx != NoTimeSignal && int(sig.TimeSignalIdx) < len(sigs) hasTimeSig := sig.TimeSignalIdx != NoTimeSignal && int(sig.TimeSignalIdx) < len(sigs)
var timeSigName string var timeSigName string
// uint64 signals carry nanoseconds (HRT); everything else is assumed microseconds.
timerToSec := 1e-6
if hasTimeSig { 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 dt := 0.0
if sig.SamplingRate > 0 { if sig.SamplingRate > 0 {
@@ -600,7 +630,7 @@ func (h *Hub) buildDataMessageForSource(src *sourceHubState, batch []DataSample)
if hasTimeSig { if hasTimeSig {
tVals, tOk := s.Values[timeSigName] tVals, tOk := s.Values[timeSigName]
if tOk && len(tVals) >= 1 { if tOk && len(tVals) >= 1 {
timerS := tVals[0] * 1e-6 timerS := tVals[0] * timerToSec
wallT := float64(s.WallTime.UnixNano()) / 1e9 wallT := float64(s.WallTime.UnixNano()) / 1e9
if _, exists := src.timeSigCalib[timeSigName]; !exists { if _, exists := src.timeSigCalib[timeSigName]; !exists {
src.timeSigCalib[timeSigName] = wallT - timerS src.timeSigCalib[timeSigName] = wallT - timerS
@@ -635,6 +665,58 @@ func (h *Hub) buildDataMessageForSource(src *sourceHubState, batch []DataSample)
decimT, decimV := lttbDecimate(allT, allV, maxPushPoints) decimT, decimV := lttbDecimate(allT, allV, maxPushPoints)
out[pfx+sig.Name] = sigData{T: decimT, V: decimV} 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: case n == 1:
ts := make([]float64, 0, len(batch)) ts := make([]float64, 0, len(batch))
vs := 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 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. // arrayKey returns the buffer key for element i of an array signal.
func arrayKey(name string, i int) string { func arrayKey(name string, i int) string {
return name + "[" + itoa(i) + "]" return name + "[" + itoa(i) + "]"
+179 -26
View File
@@ -111,6 +111,7 @@ function connectWS() {
if (msg.type === 'sources') onSources(msg); if (msg.type === 'sources') onSources(msg);
else if (msg.type === 'config') onConfig(msg); else if (msg.type === 'config') onConfig(msg);
else if (msg.type === 'data') onData(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'); 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 // Build uPlot opts for a given plot object
function makeUPlotOpts(p, inTrigMode) { function makeUPlotOpts(p, inTrigMode) {
const seriesArr = [{}]; // time (index 0) const seriesArr = [{}]; // time (index 0)
@@ -774,11 +795,16 @@ function makeUPlotOpts(p, inTrigMode) {
}, },
select: { show: true }, select: { show: true },
scales: { scales: {
x: { x: (() => {
time: false, auto: false, let xMin, xMax;
min: p.xRange ? p.xRange[0] : 0, if (p.xRange) {
max: p.xRange ? p.xRange[1] : windowSec 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 }, y: { auto: true },
}, },
series: seriesArr, series: seriesArr,
@@ -1005,16 +1031,8 @@ function resampleLinear(tSrc, vSrc, tDst) {
function buildLiveData(p) { function buildLiveData(p) {
if (p.traces.length === 0) return [new Float64Array(0)]; if (p.traces.length === 0) return [new Float64Array(0)];
// plotNow = newest timestamp across ALL traces // plotNow = min(newest per source) so no source shows a blank right edge.
let plotNow = -Infinity; const plotNow = computePlotNow(p);
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;
const t0 = p.xRange ? p.xRange[0] : plotNow - windowSec; const t0 = p.xRange ? p.xRange[0] : plotNow - windowSec;
const t1 = p.xRange ? p.xRange[1] : plotNow; 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. // 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). // This also updates axis visibility (which axes show labels depends on grid position).
requestAnimationFrame(() => { 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 => { setTimeout(() => plots.forEach(p => {
if (p.uplot) p.uplot.setSize({ width: p.div.clientWidth, height: p.div.clientHeight }); if (p.uplot) p.uplot.setSize({ width: p.div.clientWidth, height: p.div.clientHeight });
}), 60); }), 60);
@@ -1888,16 +1909,8 @@ function renderDirtyPlots() {
// Zoomed: re-apply so scale is correct after setData // Zoomed: re-apply so scale is correct after setData
p.uplot.setScale('x', { min: p.xRange[0], max: p.xRange[1] }); p.uplot.setScale('x', { min: p.xRange[0], max: p.xRange[1] });
} else { } else {
// Rolling window: compute plotNow fresh each frame (same anchor as buildLiveData) // Rolling window: use same anchor as buildLiveData (min-of-max per source).
// and set the scale immediately after setData for correct alignment. const plotNow = computePlotNow(p);
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;
p.uplot.setScale('x', { min: plotNow - windowSec, max: plotNow }); p.uplot.setScale('x', { min: plotNow - windowSec, max: plotNow });
} }
zoomGuard = false; zoomGuard = false;
@@ -1965,6 +1978,7 @@ function onSources(msg) {
} }
}); });
buildSidebar(); buildSidebar();
if (statsOpen) _refreshStatsSelector();
} }
function addSourceWS(label, addr) { function addSourceWS(label, addr) {
@@ -2110,6 +2124,139 @@ function initSignalMenu() {
document.addEventListener('keydown', e => { if (e.key === 'Escape') hideSignalMenu(); }); 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 `<div class="stats-kv"><span class="stats-k">${label}</span><span class="stats-v${cls ? ' ' + cls : ''}">${value}</span></div>`;
}
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 `<div class="hist-bar" style="height:${pct}%" title="${c} samples"></div>`;
}).join('');
return `<div class="stats-hist">
<div class="hist-bars">${bars}</div>
<div class="hist-labels"><span>${si.cycleHistMin.toFixed(3)}</span><span>${si.cycleHistMax.toFixed(3)} ms</span></div>
</div>`;
}
function renderStats() {
const body = document.getElementById('stats-body');
if (!body) return;
const src = statsSelectedSrc ? sourcesMap[statsSelectedSrc] : null;
if (!src) {
body.innerHTML = '<span class="stats-empty">No source selected</span>';
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 = `
<div class="stats-section">
<div class="stats-section-label">Connection</div>
<div class="stats-row">
${_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')}
</div>
</div>
<hr class="stats-sep">
<div class="stats-section">
<div class="stats-section-label">Cycle rate</div>
<div class="stats-row">
${_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) : '—')}
</div>
</div>
<hr class="stats-sep">
<div class="stats-section stats-section-grow">
<div class="stats-section-label">Cycle time histogram</div>
<div class="stats-row" style="margin-bottom:6px">
${_statsKV('avg', si ? _fmtMs(si.cycleAvgMs) : '—')}
${_statsKV('σ', si ? _fmtMs(si.cycleStdMs) : '—')}
${_statsKV('min', si ? _fmtMs(si.cycleMinMs) : '—')}
${_statsKV('max', si ? _fmtMs(si.cycleMaxMs) : '—')}
</div>
${si ? _histHTML(si) : '<span class="stats-empty">No data yet</span>'}
</div>`;
}
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 Init
════════════════════════════════════════════════════════════════ */ ════════════════════════════════════════════════════════════════ */
@@ -2118,6 +2265,12 @@ applyLayout('l1x1');
buildSidebar(); // show "Add Source" section even before WS connection buildSidebar(); // show "Add Source" section even before WS connection
initSignalMenu(); initSignalMenu();
document.getElementById('btn-csv-all').addEventListener('click', exportAllCSV); 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(); connectWS();
requestAnimationFrame(renderDirtyPlots); requestAnimationFrame(renderDirtyPlots);
fetch('/version').then(r => r.text()).then(v => { fetch('/version').then(r => r.text()).then(v => {
+10
View File
@@ -97,12 +97,22 @@
<div id="plot-grid" class="l1x1"></div> <div id="plot-grid" class="l1x1"></div>
</div> </div>
</div> </div>
<!-- ── Stats panel ─────────────────────────────────────────────── -->
<div id="stats-panel">
<div id="stats-panel-hdr">
<span class="stats-hdr-label">Source Statistics</span>
<select id="stats-source-sel" class="stats-source-sel"></select>
<button id="btn-stats-close"></button>
</div>
<div id="stats-body"></div>
</div>
<!-- ── Status bar ─────────────────────────────────────────────── --> <!-- ── Status bar ─────────────────────────────────────────────── -->
<div id="statusbar"> <div id="statusbar">
<div id="sb-left"> <div id="sb-left">
<div id="status-led"></div> <div id="status-led"></div>
<span id="status-text">Disconnected</span> <span id="status-text">Disconnected</span>
<span id="sb-tsage"></span> <span id="sb-tsage"></span>
<button id="btn-stats" class="ctrl-btn" style="height:16px;padding:0 7px;font-size:10px;line-height:1">📊 Stats</button>
</div> </div>
<span id="build-version"></span> <span id="build-version"></span>
</div> </div>
+64
View File
@@ -377,6 +377,70 @@ input[type=range].trig-range::-webkit-slider-thumb {
.save-src-btn { color:var(--green); } .save-src-btn { color:var(--green); }
.save-src-btn:hover { background:rgba(166,227,161,0.1); border-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 ──────────────────────────────────────────────── */
#empty-state { #empty-state {
position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); position:absolute; top:50%; left:50%; transform:translate(-50%,-50%);
+155
View File
@@ -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"`
}
+3
View File
@@ -110,6 +110,9 @@ func (u *UDPClient) runSession() error {
copy(payload, buf[HeaderSize:n]) copy(payload, buf[HeaderSize:n])
complete, ok := reassembler.AddFragment(hdr, payload) complete, ok := reassembler.AddFragment(hdr, payload)
if hdr.Type == PktData {
u.hub.RecordDataFragment(u.sourceID, hdr.Counter, n, arrivalTime.UnixNano(), ok)
}
if !ok { if !ok {
continue continue
} }
+2 -1
View File
@@ -24,7 +24,8 @@
OBJSX= OBJSX=
SPB = SineArrayGAM.x SPB = SineArrayGAM.x \
TimeArrayGAM.x
PACKAGE=Components PACKAGE=Components
ROOT_DIR=../../.. ROOT_DIR=../../..
@@ -0,0 +1 @@
include 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)
@@ -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<uint32 *>(GetInputSignalMemory(0u));
outputBuf = reinterpret_cast<uint64 *>(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<uint32>(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<uint64>(1000000000.0 / samplingRate + 0.5);
/* Input is uint32 microseconds (LinuxTimer); convert to nanoseconds. */
uint64 anchorNs = static_cast<uint64>(*inputTime) * 1000u;
if (anchorIsFirst) {
/* out[k] = anchorNs + k * periodNs */
for (uint32 k = 0u; k < nElements; k++) {
outputBuf[k] = anchorNs + static_cast<uint64>(k) * periodNs;
}
}
else {
/* out[k] = anchorNs - (N-1-k) * periodNs */
for (uint32 k = 0u; k < nElements; k++) {
outputBuf[k] = anchorNs - static_cast<uint64>(nElements - 1u - k) * periodNs;
}
}
return true;
}
CLASS_REGISTER(TimeArrayGAM, "1.0")
} /* namespace MARTe */
@@ -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:
* <pre>
* +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 }
* }
* }
* </pre>
*
* 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_ */
+230 -23
View File
@@ -1,20 +1,27 @@
/** /**
* Test MARTe2 application for UDPStreamer DataSource. * Test MARTe2 application for UDPStreamer DataSource.
* *
* Generates scalar and high-frequency packed signals and streams them via UDPStreamer. * Three independent RT threads demonstrate all four UDPStreamer time modes:
* Connect with the WebUI client (Client/WebUI) to visualise the signals.
* *
* Signals produced (scalar, 10 kHz): * Thread1 "Streamer" port 44500 (scalar signals, PacketTime)
* Counter uint32 cycle counter from LinuxTimer * Counter uint32 cycle counter
* Time uint32 time in microseconds from LinuxTimer * Time uint32 time in microseconds (LinuxTimer, 1 kHz)
* Sine1 float32, 1 Hz sine, amplitude 10, quantised to uint16 on wire * Sine1 float32, 1 Hz, quantised to uint16 on wire (PacketTime)
* Sine2 float32, 0.3 Hz sine, amplitude 5, raw float32 on wire * Sine2 float32, 0.3 Hz, raw float32 on wire (PacketTime)
* *
* Signals produced (packed temporal arrays, 10 kHz × 1000 samples = 10 MSps): * Thread2 "FastStreamer" port 44501 (packed arrays, FirstSample + LastSample)
* Ch1 float32[1000], 1 kHz sine, amplitude 1.0 * Time uint32 scalar anchor (5 kHz LinuxTimer)
* Ch2 float32[1000], 1 kHz sine, amplitude 0.5, phase π/2 * Ch1 float32[1000], 1 kHz sine (TimeMode = FirstSample)
* Both channels use TimeMode=FirstSample with Time as the anchor and * Ch2 float32[1000], 10 kHz sine (TimeMode = LastSample)
* SamplingRate=10000000 so the WebUI reconstructs the per-sample timestamps. * 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 = { $TestApp = {
Class = RealTimeApplication Class = RealTimeApplication
@@ -46,6 +53,8 @@ $TestApp = {
} }
} }
} }
// ── 5 kHz fast timer for packed-array threads ────────────────────────
+FastTimerGAM = { +FastTimerGAM = {
Class = IOGAM Class = IOGAM
InputSignals = { InputSignals = {
@@ -61,7 +70,6 @@ $TestApp = {
Type = uint32 Type = uint32
} }
} }
} }
// ── 1 Hz sinusoidal signal ─────────────────────────────────────────── // ── 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 = { +SineGAM3 = {
Class = SineArrayGAM Class = SineArrayGAM
Frequency = 1000.0 Frequency = 1000.0
Amplitude = 1.0 Amplitude = 1.0
Phase = 0.0 Phase = 0.0
Offset = 0.0 Offset = 0.0
SamplingRate = 1000000.0 SamplingRate = 5000000.0
OutputSignals = { OutputSignals = {
Ch1 = { Ch1 = {
DataSource = DDB2 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 = { +SineGAM4 = {
Class = SineArrayGAM Class = SineArrayGAM
Frequency = 10000.0 Frequency = 10000.0
Amplitude = 0.5 Amplitude = 0.5
Phase = 1.5708 Phase = 1.5708
Offset = 0.0 Offset = 0.0
SamplingRate = 1000000.0 SamplingRate = 5000000.0
OutputSignals = { OutputSignals = {
Ch2 = { Ch2 = {
DataSource = DDB2 DataSource = DDB2
@@ -142,7 +150,7 @@ $TestApp = {
} }
} }
// ── Route signals into UDPStreamer ──────────────────────────────────── // ── Route scalar signals → Streamer ──────────────────────────────────
+StreamerGAM = { +StreamerGAM = {
Class = IOGAM Class = IOGAM
InputSignals = { InputSignals = {
@@ -182,6 +190,8 @@ $TestApp = {
} }
} }
} }
// ── Route packed arrays → FastStreamer (FirstSample + LastSample) ────
+FastStreamerGAM = { +FastStreamerGAM = {
Class = IOGAM Class = IOGAM
InputSignals = { 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 = { +Data = {
Class = ReferenceContainer Class = ReferenceContainer
DefaultDataSource = DDB DefaultDataSource = DDB
// ── Inter-GAM data buffer ────────────────────────────────────────────
+DDB = { +DDB = {
Class = GAMDataSource Class = GAMDataSource
} }
+DDB2 = { +DDB2 = {
Class = GAMDataSource Class = GAMDataSource
} }
+DDB3 = {
Class = GAMDataSource
}
// ── Real-time clock / trigger source ───────────────────────────────── // ── 1 kHz real-time clock (Thread1) ──────────────────────────────────
+Timer = { +Timer = {
Class = LinuxTimer Class = LinuxTimer
SleepNature = "Default" SleepNature = "Default"
@@ -248,6 +383,8 @@ $TestApp = {
} }
} }
} }
// ── 5 kHz fast timer (Thread2 FirstSample / LastSample) ────────────
+FastTimer = { +FastTimer = {
Class = LinuxTimer Class = LinuxTimer
SleepNature = "Default" 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 = { +Streamer = {
Class = UDPStreamer Class = UDPStreamer
Port = 44500 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 = { +FastStreamer = {
Class = UDPStreamer Class = UDPStreamer
Port = 44501 Port = 44501
@@ -312,14 +476,49 @@ $TestApp = {
Unit = "V" Unit = "V"
NumberOfDimensions = 1 NumberOfDimensions = 1
NumberOfElements = 1000 NumberOfElements = 1000
TimeMode = FirstSample TimeMode = LastSample
TimeSignal = Time TimeSignal = Time
SamplingRate = 5000000.0 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 = { +Timings = {
Class = TimingDataSource Class = TimingDataSource
} }
@@ -331,16 +530,24 @@ $TestApp = {
Class = RealTimeState Class = RealTimeState
+Threads = { +Threads = {
Class = ReferenceContainer Class = ReferenceContainer
// Thread1: scalar signals at 1 kHz
+Thread1 = { +Thread1 = {
Class = RealTimeThread Class = RealTimeThread
CPUs = 0x1 CPUs = 0x1
Functions = {TimerGAM SineGAM1 SineGAM2 StreamerGAM} Functions = {TimerGAM SineGAM1 SineGAM2 StreamerGAM}
} }
// Thread2: packed arrays at 5 kHz (FirstSample + LastSample)
+Thread2 = { +Thread2 = {
Class = RealTimeThread Class = RealTimeThread
CPUs = 0x2 CPUs = 0x2
Functions = {FastTimerGAM SineGAM3 SineGAM4 FastStreamerGAM} 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}
}
} }
} }
} }
+19 -3
View File
@@ -6,7 +6,10 @@
# ./run.sh --webui # also start the WebUI Go client in the background # ./run.sh --webui # also start the WebUI Go client in the background
# ./run.sh --help # show this message # ./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 # Additional sources and a persistent source list file (--sources-file) can
# be configured directly in the WebUI or by editing this script. # be configured directly in the WebUI or by editing this script.
# #
@@ -28,7 +31,7 @@ for arg in "$@"; do
case "$arg" in case "$arg" in
--webui) START_WEBUI=1 ;; --webui) START_WEBUI=1 ;;
--help|-h) --help|-h)
sed -n '2,12p' "$0" sed -n '2,15p' "$0"
exit 0 exit 0
;; ;;
esac esac
@@ -54,6 +57,8 @@ UDPSTREAMER_SRC="${REPO_ROOT}/Source/Components/DataSources/UDPStreamer"
UDPSTREAMER_LIB="${REPO_ROOT}/Build/${TARGET}/Components/DataSources/UDPStreamer" UDPSTREAMER_LIB="${REPO_ROOT}/Build/${TARGET}/Components/DataSources/UDPStreamer"
SINEARRAYGAM_SRC="${REPO_ROOT}/Source/Components/GAMs/SineArrayGAM" SINEARRAYGAM_SRC="${REPO_ROOT}/Source/Components/GAMs/SineArrayGAM"
SINEARRAYGAM_LIB="${REPO_ROOT}/Build/${TARGET}/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})..." echo "==> Building UDPStreamer (TARGET=${TARGET})..."
make -C "${UDPSTREAMER_SRC}" \ make -C "${UDPSTREAMER_SRC}" \
@@ -66,6 +71,12 @@ make -C "${SINEARRAYGAM_SRC}" \
-f Makefile.gcc \ -f Makefile.gcc \
TARGET="${TARGET}" \ TARGET="${TARGET}" \
2>&1 | tail -5 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." echo "==> Build done."
# ── Build WebUI binary (if requested and not already built) ────────────────── # ── Build WebUI binary (if requested and not already built) ──────────────────
@@ -83,6 +94,7 @@ COMP="${MARTe2_Components_DIR}/Build/${TARGET}/Components"
export LD_LIBRARY_PATH="\ export LD_LIBRARY_PATH="\
${UDPSTREAMER_LIB}:\ ${UDPSTREAMER_LIB}:\
${SINEARRAYGAM_LIB}:\ ${SINEARRAYGAM_LIB}:\
${TIMEARRAYGAM_LIB}:\
${MARTe2_DIR}/Build/${TARGET}/Core:\ ${MARTe2_DIR}/Build/${TARGET}/Core:\
${COMP}/DataSources/LinuxTimer:\ ${COMP}/DataSources/LinuxTimer:\
${COMP}/DataSources/LoggerDataSource:\ ${COMP}/DataSources/LoggerDataSource:\
@@ -112,6 +124,7 @@ if [ "${START_WEBUI}" -eq 1 ]; then
"${WEBUI_BIN}" \ "${WEBUI_BIN}" \
--source "Streamer@127.0.0.1:44500" \ --source "Streamer@127.0.0.1:44500" \
--source "FastStreamer@127.0.0.1:44501" \ --source "FastStreamer@127.0.0.1:44501" \
--source "FullArrStreamer@127.0.0.1:44502" \
--listen :8080 & --listen :8080 &
WEBUI_PID=$! WEBUI_PID=$!
echo "==> WebUI PID ${WEBUI_PID}" echo "==> WebUI PID ${WEBUI_PID}"
@@ -126,7 +139,10 @@ if [ ! -x "${MARTE_APP}" ]; then
exit 1 exit 1
fi 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 "==> Press Ctrl+C to stop."
echo "" echo ""