From 620542a72287fb4a2e312c5d9d664eff9117d176 Mon Sep 17 00:00:00 2001 From: Martino Ferrari Date: Tue, 19 May 2026 14:14:22 +0200 Subject: [PATCH] Implemented backend hr resolution data, splitted test gam --- Client/WebUI/hub.go | 154 ++++++++++- Client/WebUI/main.go | 1 + Client/WebUI/ringbuf.go | 88 +++++++ Client/WebUI/static/app.js | 239 +++++++++++++++++- Makefile.inc | 3 +- .../DataSources/UDPStreamer/Makefile.inc | 3 +- Source/Components/GAMs/Makefile.gcc | 25 ++ Source/Components/GAMs/Makefile.inc | 38 +++ .../Components/GAMs/SineArrayGAM/Makefile.gcc | 25 ++ .../Components/GAMs/SineArrayGAM/Makefile.inc | 52 ++++ .../SineArrayGAM}/SineArrayGAM.cpp | 0 .../SineArrayGAM}/SineArrayGAM.h | 0 Test/MARTeApp/run.sh | 17 +- 13 files changed, 610 insertions(+), 35 deletions(-) create mode 100644 Client/WebUI/ringbuf.go create mode 100644 Source/Components/GAMs/Makefile.gcc create mode 100644 Source/Components/GAMs/Makefile.inc create mode 100644 Source/Components/GAMs/SineArrayGAM/Makefile.gcc create mode 100644 Source/Components/GAMs/SineArrayGAM/Makefile.inc rename Source/Components/{DataSources/UDPStreamer => GAMs/SineArrayGAM}/SineArrayGAM.cpp (100%) rename Source/Components/{DataSources/UDPStreamer => GAMs/SineArrayGAM}/SineArrayGAM.h (100%) diff --git a/Client/WebUI/hub.go b/Client/WebUI/hub.go index 1a3a751..72af7e2 100644 --- a/Client/WebUI/hub.go +++ b/Client/WebUI/hub.go @@ -5,6 +5,9 @@ import ( "log" "math" "net/http" + "strconv" + "strings" + "sync" "time" "github.com/gorilla/websocket" @@ -137,7 +140,8 @@ type hubCmd struct { } // Hub is the central broker between UDP clients and WebSocket clients. -// All map state is accessed exclusively from the Run() goroutine. +// All map state is accessed exclusively from the Run() goroutine, except +// ringsMu/rings which are also read by HTTP handler goroutines. type Hub struct { clients map[*wsClient]bool register chan *wsClient @@ -147,6 +151,11 @@ type Hub struct { commandCh chan hubCmd sm *SourceManager // set after construction; used for WS-initiated source changes + + // Ring buffers for hi-res zoom data. + // ringsMu protects the map structure; each sigRing has its own RWMutex for data. + ringsMu sync.RWMutex + rings map[string]*sigRing // "sourceId:signalKey" → ring } // NewHub creates an initialised Hub. @@ -158,6 +167,75 @@ func NewHub() *Hub { broadcastCh: make(chan []byte, 64), dataCh: make(chan taggedSample, 256), commandCh: make(chan hubCmd, 64), + rings: make(map[string]*sigRing), + } +} + +// getRing returns the ring buffer for a fully-prefixed signal key, or nil. +func (h *Hub) getRing(key string) *sigRing { + h.ringsMu.RLock() + rb := h.rings[key] + h.ringsMu.RUnlock() + return rb +} + +// HandleZoom serves GET /api/zoom?t0=...&t1=...&n=...&signals=key1,key2 +// It reads from the ring buffers (safe for concurrent access) and returns +// LTTB-decimated signal data for the requested time range. +func (h *Hub) HandleZoom(w http.ResponseWriter, r *http.Request) { + q := r.URL.Query() + t0, err0 := strconv.ParseFloat(q.Get("t0"), 64) + t1, err1 := strconv.ParseFloat(q.Get("t1"), 64) + if err0 != nil || err1 != nil || t1 <= t0 { + http.Error(w, "invalid t0/t1", http.StatusBadRequest) + return + } + // n=0 (or explicit "0") means no LTTB decimation — return all ring data in range. + // n omitted / invalid → default 2400 (display quality). + var n int + if nStr := q.Get("n"); nStr == "" { + n = 2400 + } else { + n, _ = strconv.Atoi(nStr) + if n <= 0 { + n = 1 << 30 // no decimation + } else if n < 10 { + n = 2400 + } + } + + keys := strings.Split(q.Get("signals"), ",") + + // Collect ring references under a brief RLock. + h.ringsMu.RLock() + refs := make(map[string]*sigRing, len(keys)) + for _, k := range keys { + k = strings.TrimSpace(k) + if k == "" { + continue + } + if rb, ok := h.rings[k]; ok { + refs[k] = rb + } + } + h.ringsMu.RUnlock() + + result := make(map[string]sigData, len(refs)) + for k, rb := range refs { + rt, rv := rb.slice(t0, t1) + if len(rt) == 0 { + continue + } + dt, dv := lttbDecimate(rt, rv, n) + result[k] = sigData{T: dt, V: dv} + } + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(map[string]any{ + "type": "zoom", + "signals": result, + }); err != nil { + log.Printf("hub: zoom encode: %v", err) } } @@ -294,6 +372,14 @@ func (h *Hub) Run() { case "removeSource": delete(sourcesMap, cmd.sourceID) delete(pending, cmd.sourceID) + pfxDel := cmd.sourceID + ":" + h.ringsMu.Lock() + for k := range h.rings { + if strings.HasPrefix(k, pfxDel) { + delete(h.rings, k) + } + } + h.ringsMu.Unlock() rebuildSources() case "setSourceState": @@ -309,7 +395,7 @@ func (h *Hub) Run() { } src.signals = cmd.sigs src.configSeq++ - cfgMsg, err := json.Marshal(map[string]interface{}{ + cfgMsg, err := json.Marshal(map[string]any{ "type": "config", "sourceId": cmd.sourceID, "signals": cmd.sigs, @@ -320,6 +406,28 @@ func (h *Hub) Run() { } src.configJS = cfgMsg h.broadcast(cfgMsg) + // Rebuild ring buffers for this source. + pfxUpd := cmd.sourceID + ":" + h.ringsMu.Lock() + for k := range h.rings { + if strings.HasPrefix(k, pfxUpd) { + delete(h.rings, k) + } + } + for _, sig := range cmd.sigs { + ne := sig.NumElements() + isTemporal := ne > 1 && (sig.TimeMode == TimeModeFirstSample || sig.TimeMode == TimeModeLastSample) + if isTemporal { + h.rings[pfxUpd+sig.Name] = newSigRing(ringCapTemporal) + } else if ne == 1 { + h.rings[pfxUpd+sig.Name] = newSigRing(ringCapScalar) + } else { + for i := 0; i < ne; i++ { + h.rings[pfxUpd+arrayKey(sig.Name, i)] = newSigRing(ringCapScalar) + } + } + } + h.ringsMu.Unlock() case "wsAddSource": if h.sm != nil { @@ -364,17 +472,25 @@ func (h *Hub) Run() { // ─── Data serialisation ─────────────────────────────────────────────────────── -// maxScalarPoints caps scalar/spatial-array signals per 30 Hz tick. -// At typical cycle rates (≤10 kHz) a tick accumulates at most ~333 samples, -// so this cap is almost never hit. -const maxScalarPoints = 2000 +// maxPushPoints is the LTTB target for data pushed over WebSocket per 30 Hz tick. +// At 2 k pts/tick × 30 Hz = 60 k pts/s; the 600 k frontend buffer covers ~10 s. +// Kept deliberately low so the rolling window shows plenty of history even for +// multi-MHz signals — zoom resolution comes from the ring buffer instead. +const maxPushPoints = 2_000 -// maxTemporalPoints caps temporal-array (packed-burst) signals per 30 Hz tick. -// Raised significantly vs scalars because temporal arrays carry high-frequency -// waveforms: at 5 Msps / 5 kHz update rate a tick produces ~167 k samples; -// sending 20 k points limits the wire to ~320 KB/ch/tick while giving a -// minimum visible Δt of ≈ 1.6 µs (vs ≈16 µs with the old 2 k cap). -const maxTemporalPoints = 20000 +// maxRingPoints is the LTTB target written into the ring buffer per tick. +// At 5 Msps / 5 kHz packet rate ≈ 167 k raw samples/tick → LTTB to 20 k → +// min Δt ≈ 33 ms / 20 k ≈ 1.65 µs, sufficient for sub-10 µs zoom resolution. +const maxRingPoints = 20_000 + +// ringCapTemporal is the ring buffer capacity for temporal-array signals. +// At 20 k pts/tick × 30 Hz = 600 k pts/s → 6 M cap gives ~10 s of hi-res +// history — the same temporal coverage as the frontend push buffer. +const ringCapTemporal = 6_000_000 + +// ringCapScalar is the ring buffer capacity for scalar / spatial-array signals. +// At ≤10 kHz → ~333 pts/tick × 30 Hz ≈ 10 k pts/s → ~10 s of history. +const ringCapScalar = 100_000 // lttbDecimate reduces (tIn, vIn) to at most threshold representative points // using the Largest-Triangle-Three-Buckets algorithm. @@ -510,7 +626,13 @@ func (h *Hub) buildDataMessageForSource(src *sourceHubState, batch []DataSample) } } - decimT, decimV := lttbDecimate(allT, allV, maxTemporalPoints) + // Write hi-res LTTB data to ring for on-demand zoom queries. + ringT, ringV := lttbDecimate(allT, allV, maxRingPoints) + if rb := h.getRing(pfx + sig.Name); rb != nil { + rb.write(ringT, ringV) + } + // Decimate further for WebSocket push (rolling window). + decimT, decimV := lttbDecimate(allT, allV, maxPushPoints) out[pfx+sig.Name] = sigData{T: decimT, V: decimV} case n == 1: @@ -524,6 +646,9 @@ func (h *Hub) buildDataMessageForSource(src *sourceHubState, batch []DataSample) ts = append(ts, float64(s.WallTime.UnixNano())/1e9) vs = append(vs, vals[0]) } + if rb := h.getRing(pfx + sig.Name); rb != nil { + rb.write(ts, vs) + } out[pfx+sig.Name] = sigData{T: ts, V: vs} default: @@ -540,6 +665,9 @@ func (h *Hub) buildDataMessageForSource(src *sourceHubState, batch []DataSample) ts = append(ts, float64(s.WallTime.UnixNano())/1e9) vs = append(vs, vals[i]) } + if rb := h.getRing(key); rb != nil { + rb.write(ts, vs) + } out[key] = sigData{T: ts, V: vs} } } diff --git a/Client/WebUI/main.go b/Client/WebUI/main.go index 71205b3..d5dd470 100644 --- a/Client/WebUI/main.go +++ b/Client/WebUI/main.go @@ -53,6 +53,7 @@ func main() { } http.Handle("/", http.FileServer(http.FS(sub))) http.HandleFunc("/ws", hub.HandleWebSocket) + http.HandleFunc("/api/zoom", hub.HandleZoom) http.HandleFunc("/version", func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, version) }) diff --git a/Client/WebUI/ringbuf.go b/Client/WebUI/ringbuf.go new file mode 100644 index 0000000..fc9a691 --- /dev/null +++ b/Client/WebUI/ringbuf.go @@ -0,0 +1,88 @@ +package main + +import "sync" + +// sigRing is a fixed-capacity circular buffer storing (time, value) pairs. +// Writes come from the Hub.Run() goroutine; reads come from HTTP handler goroutines. +// The embedded RWMutex protects concurrent access. +type sigRing struct { + mu sync.RWMutex + t, v []float64 + cap int + head, size int // next write position; current fill +} + +func newSigRing(capacity int) *sigRing { + return &sigRing{ + t: make([]float64, capacity), + v: make([]float64, capacity), + cap: capacity, + } +} + +// write appends (tArr[i], vArr[i]) pairs, overwriting oldest entries when full. +func (rb *sigRing) write(tArr, vArr []float64) { + rb.mu.Lock() + defer rb.mu.Unlock() + for i := 0; i < len(tArr); i++ { + rb.t[rb.head] = tArr[i] + rb.v[rb.head] = vArr[i] + rb.head = (rb.head + 1) % rb.cap + if rb.size < rb.cap { + rb.size++ + } + } +} + +// slice returns copies of all (t, v) pairs whose timestamp falls in [t0, t1]. +// The returned slices are safe to use after the call without holding any lock. +func (rb *sigRing) slice(t0, t1 float64) ([]float64, []float64) { + rb.mu.RLock() + defer rb.mu.RUnlock() + + if rb.size == 0 { + return nil, nil + } + start := 0 + if rb.size == rb.cap { + start = rb.head + } + physAt := func(k int) int { return (start + k) % rb.cap } + + // Binary search for t0 + lo, hi := 0, rb.size + for lo < hi { + m := (lo + hi) >> 1 + if rb.t[physAt(m)] < t0 { + lo = m + 1 + } else { + hi = m + } + } + kStart := lo + + // Binary search for t1 + lo, hi = kStart, rb.size + for lo < hi { + m := (lo + hi) >> 1 + if rb.t[physAt(m)] <= t1 { + lo = m + 1 + } else { + hi = m + } + } + kEnd := lo + + n := kEnd - kStart + if n <= 0 { + return nil, nil + } + outT := make([]float64, n) + outV := make([]float64, n) + for i := 0; i < n; i++ { + p := physAt(kStart + i) + outT[i] = rb.t[p] + outV[i] = rb.v[p] + } + return outT, outV +} diff --git a/Client/WebUI/static/app.js b/Client/WebUI/static/app.js index f2bddb4..9be44cf 100644 --- a/Client/WebUI/static/app.js +++ b/Client/WebUI/static/app.js @@ -64,6 +64,11 @@ let zoomGuard = false; // Zoom history for Back button (global since plots are zoom-synced) const zoomHistory = []; +// zoomData: hi-res data fetched from /api/zoom, keyed by plot id. +// Each entry: { signals: { key: {t:Float64Array, v:Float64Array} }, t0, t1 } +const zoomData = {}; +let _zoomFetchTimer = null; + // Cursors A/B — stored in x-axis units of the current mode: // live mode → Unix seconds // trig mode → relative seconds from trigger @@ -248,6 +253,46 @@ function finaliseTriggerCapture() { plots.forEach(p => { p.xRange = null; p.needsRedraw = true; }); if (trig.mode === 'normal' && !trig.stopped) setTimeout(() => { if (trig.enabled && trig.mode === 'normal' && !trig.stopped) trigArm(); }, 200); + + // Upgrade the snapshot in the background with full-resolution ring data. + // The push buffer has min Δt ≈ 16 µs; the ring provides ≈ 1.65 µs. + upgradeTriggerSnapshot(t0, t1, trig.trigTime); +} + +// Fetches full-resolution ring data for the trigger window and replaces the +// push-buffer entries in trig.snapshot, then re-renders all plots. +async function upgradeTriggerSnapshot(t0, t1, capturedTrigTime) { + // Small delay so the ring receives the last post-trigger samples + // (ring write period ≈ 33 ms; 120 ms gives ≥ 3 ticks of margin). + await new Promise(r => setTimeout(r, 120)); + // Abort if a new trigger has fired since we started. + if (!trig.snapshot || trig.trigTime !== capturedTrigTime) return; + + const keys = Object.keys(buffers); + if (!keys.length) return; + + const url = `/api/zoom?t0=${t0}&t1=${t1}&n=0&signals=${keys.join(',')}`; + let ringSignals; + try { + const resp = await fetch(url); + if (!resp.ok) return; + const json = await resp.json(); + ringSignals = json && json.signals; + } catch (e) { + console.warn('trigger snapshot ring upgrade failed:', e); + return; + } + + // Abort if the snapshot was replaced while we were fetching. + if (!trig.snapshot || trig.trigTime !== capturedTrigTime) return; + + let updated = false; + Object.entries(ringSignals || {}).forEach(([key, sd]) => { + if (!sd || !sd.t || !sd.t.length) return; + trig.snapshot[key] = { t: Float64Array.from(sd.t), v: Float64Array.from(sd.v) }; + updated = true; + }); + if (updated) plots.forEach(p => { p.needsRedraw = true; }); } function trigArm() { // Do NOT clear trig.snapshot here — keep the last waveform and trigger marker @@ -431,6 +476,81 @@ function lttb(t, v, threshold) { return { t: outT, v: outV }; } +/* ════════════════════════════════════════════════════════════════ + Hybrid zoom: hi-res data fetched from /api/zoom on demand + ════════════════════════════════════════════════════════════════ */ +function cancelZoomFetch() { + if (_zoomFetchTimer !== null) { clearTimeout(_zoomFetchTimer); _zoomFetchTimer = null; } +} +function scheduleZoomFetch(t0, t1) { + cancelZoomFetch(); + _zoomFetchTimer = setTimeout(() => { _zoomFetchTimer = null; doZoomFetch(t0, t1); }, 150); +} + +async function doZoomFetch(t0, t1) { + // Collect all signal keys across all plots; each key encodes sourceId as prefix. + const allKeys = new Set(); + plots.forEach(p => p.traces.forEach(k => allKeys.add(k))); + if (allKeys.size === 0) return; + + const targetPts = Math.max(400, (plots.find(p => p.uplot)?.uplot?.width || 600) * 2); + const url = `/api/zoom?t0=${t0}&t1=${t1}&n=${targetPts}&signals=${[...allKeys].join(',')}`; + let fetched; + try { + const resp = await fetch(url); + if (!resp.ok) return; + fetched = await resp.json(); + } catch (e) { console.warn('zoom fetch:', e); return; } + + const sigs = fetched && fetched.signals; + if (!sigs) return; + + // Convert arrays from JSON to Float64Array and store per-plot. + const merged = {}; + Object.entries(sigs).forEach(([k, sd]) => { + if (!sd || !sd.t || !sd.v) return; + merged[k] = { t: Float64Array.from(sd.t), v: Float64Array.from(sd.v) }; + }); + + plots.forEach(p => { + // Only store if this plot's range still matches what we fetched. + if (!p.xRange || Math.abs(p.xRange[0] - t0) > 1e-9 || Math.abs(p.xRange[1] - t1) > 1e-9) return; + const plotSigs = {}; + p.traces.forEach(k => { if (merged[k]) plotSigs[k] = merged[k]; }); + if (Object.keys(plotSigs).length > 0) { + zoomData[p.id] = { signals: plotSigs, t0, t1 }; + p.needsRedraw = true; + } + }); +} + +// Build uPlot data arrays from server-fetched hi-res signal data. +function buildDataFromFetched(p, fetchedSignals, targetPts) { + let masterKey = p.traces[0], masterCount = -1, masterRate = -1; + for (const key of p.traces) { + const sd = fetchedSignals[key]; + if (!sd || !sd.t || !sd.t.length) continue; + const rate = getKeySamplingRate(key); + if (rate > masterRate || (rate === masterRate && sd.t.length > masterCount)) { + masterRate = rate; masterCount = sd.t.length; masterKey = key; + } + } + const masterSd = fetchedSignals[masterKey]; + if (!masterSd || !masterSd.t.length) + return [new Float64Array(0), ...p.traces.map(() => new Float64Array(0))]; + + const dec = lttb(masterSd.t, masterSd.v, targetPts); + const sharedT = dec.t; + const yArrays = []; + for (const key of p.traces) { + if (key === masterKey) { yArrays.push(dec.v); continue; } + const sd = fetchedSignals[key]; + if (!sd || !sd.t.length) { yArrays.push(new Float64Array(sharedT.length)); continue; } + yArrays.push(resampleLinear(sd.t, sd.v, sharedT)); + } + return [sharedT, ...yArrays]; +} + /* ════════════════════════════════════════════════════════════════ uPlot helpers ════════════════════════════════════════════════════════════════ */ @@ -903,6 +1023,14 @@ function buildLiveData(p) { // raises the effective sample cap and reveals full resolution. const targetPts = Math.max(LTTB_MIN, ((p.uplot ? p.uplot.width : p.div.clientWidth) || 600) * 2); + // When zoomed, prefer server-fetched hi-res data if it covers this exact range. + if (p.xRange) { + const zd = zoomData[p.id]; + if (zd && Math.abs(zd.t0 - t0) < 1e-9 && Math.abs(zd.t1 - t1) < 1e-9) { + return buildDataFromFetched(p, zd.signals, targetPts); + } + } + // Slice all traces once; pick the master time grid using configured samplingRate // as the primary criterion (unambiguous, independent of buffer fill / trace order). // Fall back to raw sample count for signals without a configured rate. @@ -1019,6 +1147,9 @@ function onZoom(sourcePlotId, min, max) { // Mark all plots dirty (re-slice data to the new range for full resolution) plots.forEach(p => { p.needsRedraw = true; }); + + // Schedule hi-res fetch from ring buffers. + scheduleZoomFetch(min, max); } // Undo last zoom/pan action @@ -1027,6 +1158,10 @@ function zoomBack() { const prev = zoomHistory.pop(); if (!zoomHistory.length) document.getElementById('btn-zoom-back').style.display = 'none'; + // Discard stale zoom data regardless of direction. + Object.keys(zoomData).forEach(k => delete zoomData[k]); + cancelZoomFetch(); + if (prev === null) { // Was at auto/rolling state before the zoom resetZoom(); @@ -1038,11 +1173,14 @@ function zoomBack() { p.needsRedraw = true; }); zoomGuard = false; + scheduleZoomFetch(prev[0], prev[1]); } } // Reset to auto/rolling window (clears all zoom) function resetZoom() { + Object.keys(zoomData).forEach(k => delete zoomData[k]); + cancelZoomFetch(); syncLocked = false; document.getElementById('btn-sync-resume').style.display = 'none'; if (trig.enabled && trig.snapshot) { @@ -1097,6 +1235,7 @@ function zoomFit() { p.needsRedraw = true; }); zoomGuard = false; + scheduleZoomFetch(gMin, gMax); } // Auto = return to rolling window / full trigger window @@ -1464,34 +1603,112 @@ function buildLayoutMenu() { } /* ════════════════════════════════════════════════════════════════ - Export CSV (all plots) + Export CSV (all plots) — fetches full-resolution data from ring ════════════════════════════════════════════════════════════════ */ -function exportAllCSV() { +async function exportAllCSV() { + const btn = document.getElementById('btn-csv-all'); + if (btn.disabled) return; + const inTrigMode = trig.enabled && trig.snapshot !== null; - // Collect unique signal keys across all plots (preserving order) + + // Collect unique signal keys across all plots (preserving order). const keys = []; plots.forEach(p => p.traces.forEach(k => { if (!keys.includes(k)) keys.push(k); })); if (!keys.length) return; + // Determine time range to export. + let t0, t1, relOffset = 0; + if (inTrigMode) { + // Export the full trigger window around the trigger event. + t0 = trig.trigTime - trigPreSec(); + t1 = trig.trigTime + trigPostSec(); + relOffset = trig.trigTime; + } else { + // Use the current zoom range if active, else the rolling window. + const refPlot = plots.find(p => p.xRange); + if (refPlot) { + [t0, t1] = refPlot.xRange; + } else { + let plotNow = -Infinity; + keys.forEach(k => { + const buf = buffers[k]; + 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; + t0 = plotNow - windowSec; + t1 = plotNow; + } + } + + // Show loading state. + const origLabel = btn.textContent; + btn.textContent = '⏳ Downloading…'; + btn.disabled = true; + + // Fetch full-resolution ring data (n=0 → no LTTB decimation). + const url = `/api/zoom?t0=${t0}&t1=${t1}&n=0&signals=${keys.join(',')}`; + let ringSignals = null; + try { + const resp = await fetch(url); + if (resp.ok) { + const json = await resp.json(); + ringSignals = json && json.signals; + } + } catch (e) { + console.warn('CSV export: ring fetch failed, falling back to push buffer', e); + } finally { + btn.textContent = origLabel; + btn.disabled = false; + } + + // Build per-signal time/value arrays. + // Priority: ring buffer (full res) → trigger snapshot → push buffer. const slices = keys.map(key => { + const rd = ringSignals && ringSignals[key]; + if (rd && rd.t && rd.t.length > 0) { + const t = rd.t, v = rd.v; + if (inTrigMode) { + return { t: Array.from(t).map(ts => ts - relOffset), v: Array.from(v) }; + } + return { t: Array.from(t), v: Array.from(v) }; + } + // Fallback: push buffer or trigger snapshot. if (inTrigMode) { const raw = trig.snapshot[key] || { t: new Float64Array(0), v: new Float64Array(0) }; - return { t: Array.from(raw.t).map(ts => ts - trig.trigTime), v: Array.from(raw.v) }; + return { t: Array.from(raw.t).map(ts => ts - relOffset), v: Array.from(raw.v) }; } const buf = buffers[key]; if (!buf) return { t: [], v: [] }; - const sl = getBufferSlice(buf); + const sl = getBufferSliceRange(buf, t0, t1); return { t: Array.from(sl.t), v: Array.from(sl.v) }; }); - const allT = new Set(); slices.forEach(s => s.t.forEach(t => allT.add(t))); + // Merge all timestamps and build aligned rows. + const allT = new Set(); + slices.forEach(s => s.t.forEach(t => allT.add(t))); const sortedT = Array.from(allT).sort((a, b) => a - b); - const lookups = slices.map(s => { const m = new Map(); s.t.forEach((t, i) => m.set(t, s.v[i])); return m; }); - const hdr = [(inTrigMode ? 'time_rel_s' : 'time_s'), ...keys].join(','); - const rows = sortedT.map(t => [t.toFixed(9), ...lookups.map(lk => lk.has(t) ? lk.get(t) : '')].join(',')); + if (!sortedT.length) return; + + const lookups = slices.map(s => { + const m = new Map(); + s.t.forEach((t, i) => m.set(t, s.v[i])); + return m; + }); + + // Strip "sourceId:" prefix from column headers for readability. + const displayKeys = keys.map(k => (k.includes(':') ? k.split(':').slice(1).join(':') : k)); + const hdr = [(inTrigMode ? 'time_rel_s' : 'time_s'), ...displayKeys].join(','); + const rows = sortedT.map(t => + [t.toFixed(9), ...lookups.map(lk => (lk.has(t) ? lk.get(t) : ''))].join(',') + ); const blob = new Blob([hdr + '\n' + rows.join('\n')], { type: 'text/csv' }); const a = document.createElement('a'); - a.href = URL.createObjectURL(blob); a.download = 'signals_' + Date.now() + '.csv'; - a.click(); URL.revokeObjectURL(a.href); + a.href = URL.createObjectURL(blob); + a.download = 'signals_' + Date.now() + '.csv'; + a.click(); + URL.revokeObjectURL(a.href); } /* ════════════════════════════════════════════════════════════════ diff --git a/Makefile.inc b/Makefile.inc index 0d92581..89f15ff 100644 --- a/Makefile.inc +++ b/Makefile.inc @@ -27,8 +27,7 @@ #If SPB is directly exported as an environment variable it will also be evaluated as part of the subprojects SPB, thus #potentially overriding its value #Main target subprojects. May be overridden by shell definition. -SPBM?=Source/Components/DataSources.x -# Source/Components/GAMs.x \ +SPBM?=Source/Components/DataSources.x Source/Components/GAMs.x diff --git a/Source/Components/DataSources/UDPStreamer/Makefile.inc b/Source/Components/DataSources/UDPStreamer/Makefile.inc index 4590af1..bda52a9 100644 --- a/Source/Components/DataSources/UDPStreamer/Makefile.inc +++ b/Source/Components/DataSources/UDPStreamer/Makefile.inc @@ -22,8 +22,7 @@ # ############################################################# -OBJSX = UDPStreamer.x \ - SineArrayGAM.x +OBJSX = UDPStreamer.x PACKAGE=Components/DataSources ROOT_DIR=../../../../ diff --git a/Source/Components/GAMs/Makefile.gcc b/Source/Components/GAMs/Makefile.gcc new file mode 100644 index 0000000..a9c3668 --- /dev/null +++ b/Source/Components/GAMs/Makefile.gcc @@ -0,0 +1,25 @@ +############################################################# +# +# Copyright 2015 F4E | European Joint Undertaking for ITER +# and the Development of Fusion Energy ('Fusion for Energy') +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# http://ec.europa.eu/idabc/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# +############################################################# + +include Makefile.inc diff --git a/Source/Components/GAMs/Makefile.inc b/Source/Components/GAMs/Makefile.inc new file mode 100644 index 0000000..e43196d --- /dev/null +++ b/Source/Components/GAMs/Makefile.inc @@ -0,0 +1,38 @@ +############################################################# +# +# Copyright 2015 F4E | European Joint Undertaking for ITER +# and the Development of Fusion Energy ('Fusion for Energy') +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# http://ec.europa.eu/idabc/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# +############################################################# + +OBJSX= + +SPB = SineArrayGAM.x + +PACKAGE=Components +ROOT_DIR=../../.. +MAKEDEFAULTDIR=$(MARTe2_DIR)/MakeDefaults + +include $(MAKEDEFAULTDIR)/MakeStdLibDefs.$(TARGET) + +all: $(OBJS) $(SUBPROJ) + echo $(OBJS) + +include $(MAKEDEFAULTDIR)/MakeStdLibRules.$(TARGET) diff --git a/Source/Components/GAMs/SineArrayGAM/Makefile.gcc b/Source/Components/GAMs/SineArrayGAM/Makefile.gcc new file mode 100644 index 0000000..a9c3668 --- /dev/null +++ b/Source/Components/GAMs/SineArrayGAM/Makefile.gcc @@ -0,0 +1,25 @@ +############################################################# +# +# Copyright 2015 F4E | European Joint Undertaking for ITER +# and the Development of Fusion Energy ('Fusion for Energy') +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# http://ec.europa.eu/idabc/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# +############################################################# + +include Makefile.inc diff --git a/Source/Components/GAMs/SineArrayGAM/Makefile.inc b/Source/Components/GAMs/SineArrayGAM/Makefile.inc new file mode 100644 index 0000000..e2cdc60 --- /dev/null +++ b/Source/Components/GAMs/SineArrayGAM/Makefile.inc @@ -0,0 +1,52 @@ +############################################################# +# +# Copyright 2015 F4E | European Joint Undertaking for ITER +# and the Development of Fusion Energy ('Fusion for Energy') +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# http://ec.europa.eu/idabc/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# +############################################################# + +OBJSX = SineArrayGAM.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)/SineArrayGAM$(LIBEXT) \ + $(BUILD_DIR)/SineArrayGAM$(DLLEXT) + echo $(OBJS) + +-include depends.$(TARGET) + +include $(MAKEDEFAULTDIR)/MakeStdLibRules.$(TARGET) diff --git a/Source/Components/DataSources/UDPStreamer/SineArrayGAM.cpp b/Source/Components/GAMs/SineArrayGAM/SineArrayGAM.cpp similarity index 100% rename from Source/Components/DataSources/UDPStreamer/SineArrayGAM.cpp rename to Source/Components/GAMs/SineArrayGAM/SineArrayGAM.cpp diff --git a/Source/Components/DataSources/UDPStreamer/SineArrayGAM.h b/Source/Components/GAMs/SineArrayGAM/SineArrayGAM.h similarity index 100% rename from Source/Components/DataSources/UDPStreamer/SineArrayGAM.h rename to Source/Components/GAMs/SineArrayGAM/SineArrayGAM.h diff --git a/Test/MARTeApp/run.sh b/Test/MARTeApp/run.sh index 7633fe6..6dad3ae 100755 --- a/Test/MARTeApp/run.sh +++ b/Test/MARTeApp/run.sh @@ -48,16 +48,24 @@ if [ -z "${MARTe2_DIR}" ]; then exit 1 fi -# ── Build UDPStreamer ───────────────────────────────────────────────────────── +# ── Build components ───────────────────────────────────────────────────────── TARGET=x86-linux UDPSTREAMER_SRC="${REPO_ROOT}/Source/Components/DataSources/UDPStreamer" UDPSTREAMER_LIB="${REPO_ROOT}/Build/${TARGET}/Components/DataSources/UDPStreamer" +SINEARRAYGAM_SRC="${REPO_ROOT}/Source/Components/GAMs/SineArrayGAM" +SINEARRAYGAM_LIB="${REPO_ROOT}/Build/${TARGET}/Components/GAMs/SineArrayGAM" echo "==> Building UDPStreamer (TARGET=${TARGET})..." make -C "${UDPSTREAMER_SRC}" \ -f Makefile.gcc \ TARGET="${TARGET}" \ 2>&1 | tail -5 + +echo "==> Building SineArrayGAM (TARGET=${TARGET})..." +make -C "${SINEARRAYGAM_SRC}" \ + -f Makefile.gcc \ + TARGET="${TARGET}" \ + 2>&1 | tail -5 echo "==> Build done." # ── Build WebUI binary (if requested and not already built) ────────────────── @@ -74,6 +82,7 @@ COMP="${MARTe2_Components_DIR}/Build/${TARGET}/Components" export LD_LIBRARY_PATH="\ ${UDPSTREAMER_LIB}:\ +${SINEARRAYGAM_LIB}:\ ${MARTe2_DIR}/Build/${TARGET}/Core:\ ${COMP}/DataSources/LinuxTimer:\ ${COMP}/DataSources/LoggerDataSource:\ @@ -96,12 +105,6 @@ for cls in WaveformSin WaveformChirp WaveformPointsDef; do fi done -# SineArrayGAM is bundled inside UDPStreamer.so; create a symlink so MARTe2 -# can dlopen("SineArrayGAM.so") before UDPStreamer has been registered. -SINE_LINK="${UDPSTREAMER_LIB}/SineArrayGAM.so" -if [ ! -e "${SINE_LINK}" ]; then - ln -sf "${UDPSTREAMER_LIB}/UDPStreamer.so" "${SINE_LINK}" -fi # ── Optionally start WebUI ──────────────────────────────────────────────────── if [ "${START_WEBUI}" -eq 1 ]; then