Improved uo and added timerarraygam for testing
This commit is contained in:
+97
-5
@@ -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
@@ -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 => {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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%);
|
||||||
|
|||||||
@@ -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"`
|
||||||
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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 ""
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user