Implemented backend hr resolution data, splitted test gam
This commit is contained in:
+141
-13
@@ -5,6 +5,9 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"math"
|
"math"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
@@ -137,7 +140,8 @@ type hubCmd struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Hub is the central broker between UDP clients and WebSocket clients.
|
// 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 {
|
type Hub struct {
|
||||||
clients map[*wsClient]bool
|
clients map[*wsClient]bool
|
||||||
register chan *wsClient
|
register chan *wsClient
|
||||||
@@ -147,6 +151,11 @@ type Hub struct {
|
|||||||
commandCh chan hubCmd
|
commandCh chan hubCmd
|
||||||
|
|
||||||
sm *SourceManager // set after construction; used for WS-initiated source changes
|
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.
|
// NewHub creates an initialised Hub.
|
||||||
@@ -158,6 +167,75 @@ func NewHub() *Hub {
|
|||||||
broadcastCh: make(chan []byte, 64),
|
broadcastCh: make(chan []byte, 64),
|
||||||
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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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":
|
case "removeSource":
|
||||||
delete(sourcesMap, cmd.sourceID)
|
delete(sourcesMap, cmd.sourceID)
|
||||||
delete(pending, 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()
|
rebuildSources()
|
||||||
|
|
||||||
case "setSourceState":
|
case "setSourceState":
|
||||||
@@ -309,7 +395,7 @@ func (h *Hub) Run() {
|
|||||||
}
|
}
|
||||||
src.signals = cmd.sigs
|
src.signals = cmd.sigs
|
||||||
src.configSeq++
|
src.configSeq++
|
||||||
cfgMsg, err := json.Marshal(map[string]interface{}{
|
cfgMsg, err := json.Marshal(map[string]any{
|
||||||
"type": "config",
|
"type": "config",
|
||||||
"sourceId": cmd.sourceID,
|
"sourceId": cmd.sourceID,
|
||||||
"signals": cmd.sigs,
|
"signals": cmd.sigs,
|
||||||
@@ -320,6 +406,28 @@ func (h *Hub) Run() {
|
|||||||
}
|
}
|
||||||
src.configJS = cfgMsg
|
src.configJS = cfgMsg
|
||||||
h.broadcast(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":
|
case "wsAddSource":
|
||||||
if h.sm != nil {
|
if h.sm != nil {
|
||||||
@@ -364,17 +472,25 @@ func (h *Hub) Run() {
|
|||||||
|
|
||||||
// ─── Data serialisation ───────────────────────────────────────────────────────
|
// ─── Data serialisation ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
// maxScalarPoints caps scalar/spatial-array signals per 30 Hz tick.
|
// maxPushPoints is the LTTB target for data pushed over WebSocket per 30 Hz tick.
|
||||||
// At typical cycle rates (≤10 kHz) a tick accumulates at most ~333 samples,
|
// At 2 k pts/tick × 30 Hz = 60 k pts/s; the 600 k frontend buffer covers ~10 s.
|
||||||
// so this cap is almost never hit.
|
// Kept deliberately low so the rolling window shows plenty of history even for
|
||||||
const maxScalarPoints = 2000
|
// 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.
|
// maxRingPoints is the LTTB target written into the ring buffer per tick.
|
||||||
// Raised significantly vs scalars because temporal arrays carry high-frequency
|
// At 5 Msps / 5 kHz packet rate ≈ 167 k raw samples/tick → LTTB to 20 k →
|
||||||
// waveforms: at 5 Msps / 5 kHz update rate a tick produces ~167 k samples;
|
// min Δt ≈ 33 ms / 20 k ≈ 1.65 µs, sufficient for sub-10 µs zoom resolution.
|
||||||
// sending 20 k points limits the wire to ~320 KB/ch/tick while giving a
|
const maxRingPoints = 20_000
|
||||||
// minimum visible Δt of ≈ 1.6 µs (vs ≈16 µs with the old 2 k cap).
|
|
||||||
const maxTemporalPoints = 20000
|
// 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
|
// lttbDecimate reduces (tIn, vIn) to at most threshold representative points
|
||||||
// using the Largest-Triangle-Three-Buckets algorithm.
|
// 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}
|
out[pfx+sig.Name] = sigData{T: decimT, V: decimV}
|
||||||
|
|
||||||
case n == 1:
|
case n == 1:
|
||||||
@@ -524,6 +646,9 @@ func (h *Hub) buildDataMessageForSource(src *sourceHubState, batch []DataSample)
|
|||||||
ts = append(ts, float64(s.WallTime.UnixNano())/1e9)
|
ts = append(ts, float64(s.WallTime.UnixNano())/1e9)
|
||||||
vs = append(vs, vals[0])
|
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}
|
out[pfx+sig.Name] = sigData{T: ts, V: vs}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -540,6 +665,9 @@ func (h *Hub) buildDataMessageForSource(src *sourceHubState, batch []DataSample)
|
|||||||
ts = append(ts, float64(s.WallTime.UnixNano())/1e9)
|
ts = append(ts, float64(s.WallTime.UnixNano())/1e9)
|
||||||
vs = append(vs, vals[i])
|
vs = append(vs, vals[i])
|
||||||
}
|
}
|
||||||
|
if rb := h.getRing(key); rb != nil {
|
||||||
|
rb.write(ts, vs)
|
||||||
|
}
|
||||||
out[key] = sigData{T: ts, V: vs}
|
out[key] = sigData{T: ts, V: vs}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
http.Handle("/", http.FileServer(http.FS(sub)))
|
http.Handle("/", http.FileServer(http.FS(sub)))
|
||||||
http.HandleFunc("/ws", hub.HandleWebSocket)
|
http.HandleFunc("/ws", hub.HandleWebSocket)
|
||||||
|
http.HandleFunc("/api/zoom", hub.HandleZoom)
|
||||||
http.HandleFunc("/version", func(w http.ResponseWriter, r *http.Request) {
|
http.HandleFunc("/version", func(w http.ResponseWriter, r *http.Request) {
|
||||||
fmt.Fprint(w, version)
|
fmt.Fprint(w, version)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
+228
-11
@@ -64,6 +64,11 @@ let zoomGuard = false;
|
|||||||
// Zoom history for Back button (global since plots are zoom-synced)
|
// Zoom history for Back button (global since plots are zoom-synced)
|
||||||
const zoomHistory = [];
|
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:
|
// Cursors A/B — stored in x-axis units of the current mode:
|
||||||
// live mode → Unix seconds
|
// live mode → Unix seconds
|
||||||
// trig mode → relative seconds from trigger
|
// trig mode → relative seconds from trigger
|
||||||
@@ -248,6 +253,46 @@ function finaliseTriggerCapture() {
|
|||||||
plots.forEach(p => { p.xRange = null; p.needsRedraw = true; });
|
plots.forEach(p => { p.xRange = null; p.needsRedraw = true; });
|
||||||
if (trig.mode === 'normal' && !trig.stopped)
|
if (trig.mode === 'normal' && !trig.stopped)
|
||||||
setTimeout(() => { if (trig.enabled && trig.mode === 'normal' && !trig.stopped) trigArm(); }, 200);
|
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() {
|
function trigArm() {
|
||||||
// Do NOT clear trig.snapshot here — keep the last waveform and trigger marker
|
// 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 };
|
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
|
uPlot helpers
|
||||||
════════════════════════════════════════════════════════════════ */
|
════════════════════════════════════════════════════════════════ */
|
||||||
@@ -903,6 +1023,14 @@ function buildLiveData(p) {
|
|||||||
// raises the effective sample cap and reveals full resolution.
|
// 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);
|
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
|
// Slice all traces once; pick the master time grid using configured samplingRate
|
||||||
// as the primary criterion (unambiguous, independent of buffer fill / trace order).
|
// as the primary criterion (unambiguous, independent of buffer fill / trace order).
|
||||||
// Fall back to raw sample count for signals without a configured rate.
|
// 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)
|
// Mark all plots dirty (re-slice data to the new range for full resolution)
|
||||||
plots.forEach(p => { p.needsRedraw = true; });
|
plots.forEach(p => { p.needsRedraw = true; });
|
||||||
|
|
||||||
|
// Schedule hi-res fetch from ring buffers.
|
||||||
|
scheduleZoomFetch(min, max);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Undo last zoom/pan action
|
// Undo last zoom/pan action
|
||||||
@@ -1027,6 +1158,10 @@ function zoomBack() {
|
|||||||
const prev = zoomHistory.pop();
|
const prev = zoomHistory.pop();
|
||||||
if (!zoomHistory.length) document.getElementById('btn-zoom-back').style.display = 'none';
|
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) {
|
if (prev === null) {
|
||||||
// Was at auto/rolling state before the zoom
|
// Was at auto/rolling state before the zoom
|
||||||
resetZoom();
|
resetZoom();
|
||||||
@@ -1038,11 +1173,14 @@ function zoomBack() {
|
|||||||
p.needsRedraw = true;
|
p.needsRedraw = true;
|
||||||
});
|
});
|
||||||
zoomGuard = false;
|
zoomGuard = false;
|
||||||
|
scheduleZoomFetch(prev[0], prev[1]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset to auto/rolling window (clears all zoom)
|
// Reset to auto/rolling window (clears all zoom)
|
||||||
function resetZoom() {
|
function resetZoom() {
|
||||||
|
Object.keys(zoomData).forEach(k => delete zoomData[k]);
|
||||||
|
cancelZoomFetch();
|
||||||
syncLocked = false;
|
syncLocked = false;
|
||||||
document.getElementById('btn-sync-resume').style.display = 'none';
|
document.getElementById('btn-sync-resume').style.display = 'none';
|
||||||
if (trig.enabled && trig.snapshot) {
|
if (trig.enabled && trig.snapshot) {
|
||||||
@@ -1097,6 +1235,7 @@ function zoomFit() {
|
|||||||
p.needsRedraw = true;
|
p.needsRedraw = true;
|
||||||
});
|
});
|
||||||
zoomGuard = false;
|
zoomGuard = false;
|
||||||
|
scheduleZoomFetch(gMin, gMax);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto = return to rolling window / full trigger window
|
// 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;
|
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 = [];
|
const keys = [];
|
||||||
plots.forEach(p => p.traces.forEach(k => { if (!keys.includes(k)) keys.push(k); }));
|
plots.forEach(p => p.traces.forEach(k => { if (!keys.includes(k)) keys.push(k); }));
|
||||||
if (!keys.length) return;
|
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 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) {
|
if (inTrigMode) {
|
||||||
const raw = trig.snapshot[key] || { t: new Float64Array(0), v: new Float64Array(0) };
|
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 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) };
|
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 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; });
|
if (!sortedT.length) return;
|
||||||
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(','));
|
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 blob = new Blob([hdr + '\n' + rows.join('\n')], { type: 'text/csv' });
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.href = URL.createObjectURL(blob); a.download = 'signals_' + Date.now() + '.csv';
|
a.href = URL.createObjectURL(blob);
|
||||||
a.click(); URL.revokeObjectURL(a.href);
|
a.download = 'signals_' + Date.now() + '.csv';
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(a.href);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ════════════════════════════════════════════════════════════════
|
/* ════════════════════════════════════════════════════════════════
|
||||||
|
|||||||
+1
-2
@@ -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
|
#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
|
#potentially overriding its value
|
||||||
#Main target subprojects. May be overridden by shell definition.
|
#Main target subprojects. May be overridden by shell definition.
|
||||||
SPBM?=Source/Components/DataSources.x
|
SPBM?=Source/Components/DataSources.x Source/Components/GAMs.x
|
||||||
# Source/Components/GAMs.x \
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -22,8 +22,7 @@
|
|||||||
#
|
#
|
||||||
#############################################################
|
#############################################################
|
||||||
|
|
||||||
OBJSX = UDPStreamer.x \
|
OBJSX = UDPStreamer.x
|
||||||
SineArrayGAM.x
|
|
||||||
|
|
||||||
PACKAGE=Components/DataSources
|
PACKAGE=Components/DataSources
|
||||||
ROOT_DIR=../../../../
|
ROOT_DIR=../../../../
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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)
|
||||||
@@ -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
|
||||||
@@ -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)
|
||||||
+10
-7
@@ -48,16 +48,24 @@ if [ -z "${MARTe2_DIR}" ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ── Build UDPStreamer ─────────────────────────────────────────────────────────
|
# ── Build components ─────────────────────────────────────────────────────────
|
||||||
TARGET=x86-linux
|
TARGET=x86-linux
|
||||||
UDPSTREAMER_SRC="${REPO_ROOT}/Source/Components/DataSources/UDPStreamer"
|
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_LIB="${REPO_ROOT}/Build/${TARGET}/Components/GAMs/SineArrayGAM"
|
||||||
|
|
||||||
echo "==> Building UDPStreamer (TARGET=${TARGET})..."
|
echo "==> Building UDPStreamer (TARGET=${TARGET})..."
|
||||||
make -C "${UDPSTREAMER_SRC}" \
|
make -C "${UDPSTREAMER_SRC}" \
|
||||||
-f Makefile.gcc \
|
-f Makefile.gcc \
|
||||||
TARGET="${TARGET}" \
|
TARGET="${TARGET}" \
|
||||||
2>&1 | tail -5
|
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."
|
echo "==> Build done."
|
||||||
|
|
||||||
# ── Build WebUI binary (if requested and not already built) ──────────────────
|
# ── Build WebUI binary (if requested and not already built) ──────────────────
|
||||||
@@ -74,6 +82,7 @@ COMP="${MARTe2_Components_DIR}/Build/${TARGET}/Components"
|
|||||||
|
|
||||||
export LD_LIBRARY_PATH="\
|
export LD_LIBRARY_PATH="\
|
||||||
${UDPSTREAMER_LIB}:\
|
${UDPSTREAMER_LIB}:\
|
||||||
|
${SINEARRAYGAM_LIB}:\
|
||||||
${MARTe2_DIR}/Build/${TARGET}/Core:\
|
${MARTe2_DIR}/Build/${TARGET}/Core:\
|
||||||
${COMP}/DataSources/LinuxTimer:\
|
${COMP}/DataSources/LinuxTimer:\
|
||||||
${COMP}/DataSources/LoggerDataSource:\
|
${COMP}/DataSources/LoggerDataSource:\
|
||||||
@@ -96,12 +105,6 @@ for cls in WaveformSin WaveformChirp WaveformPointsDef; do
|
|||||||
fi
|
fi
|
||||||
done
|
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 ────────────────────────────────────────────────────
|
# ── Optionally start WebUI ────────────────────────────────────────────────────
|
||||||
if [ "${START_WEBUI}" -eq 1 ]; then
|
if [ "${START_WEBUI}" -eq 1 ]; then
|
||||||
|
|||||||
Reference in New Issue
Block a user