test(e2e-chain): orchestrator + end-to-end fixes (Task 7)

Full-chain orchestrator (run_chain_e2e.sh) now runs the starter set green
(3 pass). Fixes found bringing the chain up end-to-end:

- client: gorilla/websocket cannot survive a read deadline (next ReadMessage
  panics "repeated read on failed connection"); replace the poll-with-deadline
  loop with a background reader goroutine + mutex-guarded state.
- orchestrator: guard env.sh's unbound LD_LIBRARY_PATH under set -u.
- scenarios/gen_data: centralize NUM_ROWS/ROW_DT and enforce sine freq to be a
  multiple of the buffer fundamental (LOOP_HZ=5 Hz) so the looped FileReader
  buffer is a seamless waveform; align starter freqs (5/5/10 Hz).
- gen_cfg: FileReader allows exactly one consuming Function, so route tapped
  (oracle=fed/both) sources through the DDB (ReaderGAM->DDB, then StreamGAM and
  TapGAM both read DDB) instead of a second FileReader consumer.
- validate_waveform: fidelity gates correctness (bit-exact / within one quant
  level); sine shape becomes a gross frequency-sanity gate (corr>=0.5) plus a
  tracked corr/nRMSE quality metric, since per-sample wall-clock calibration
  (Phase-A) and FULL_ARRAY packed timestamps (Phase-A4) are still pending.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Martino Ferrari
2026-06-25 13:11:35 +02:00
parent d4fa84d482
commit f256a4def5
6 changed files with 400 additions and 40 deletions
+17 -2
View File
@@ -80,6 +80,16 @@ QUANT_TYPES = {"none", "uint8", "int8", "uint16", "int16"}
QUANT_LEVELS = {"uint8": 255, "int8": 254, "uint16": 65535, "int16": 65534}
TIME_MODES = {"PacketTime", "FullArray", "FirstSample", "LastSample"}
# Producer buffer geometry — the single source of truth shared with gen_data.py.
# The MARTe FileReader loops this finite buffer (EOF=Rewind), so the streamed
# signal is only a continuous waveform if the buffer holds an integer number of
# periods. The buffer fundamental LOOP_HZ = 1/(NUM_ROWS*ROW_DT) is therefore the
# smallest sine frequency that loops seamlessly; every sine freq must be a
# positive integer multiple of it or the analytic shape oracle is invalid.
NUM_ROWS = 200 # producer cycles written to the FileReader input
ROW_DT = 1.0e-3 # seconds per producer cycle (row); 1 kHz producer
LOOP_HZ = 1.0 / (NUM_ROWS * ROW_DT) # 5.0 Hz buffer fundamental
def _sig(name, type, elements=1, time_mode="PacketTime", time_signal=None,
sampling_rate=None, quant="none", range_min=None, range_max=None,
@@ -135,6 +145,11 @@ def validate_scenario(s):
if sig["time_mode"] in ("FirstSample", "LastSample"):
if not sig["sampling_rate"] or sig["sampling_rate"] <= 0:
errs.append(f"{sig['name']}: First/LastSample needs sampling_rate>0")
if sig["formula"] == "sine" and sig.get("freq"):
ratio = sig["freq"] / LOOP_HZ
if abs(ratio - round(ratio)) > 1e-9 or round(ratio) < 1:
errs.append(f"{sig['name']}: sine freq {sig['freq']} must be a "
f"positive multiple of LOOP_HZ={LOOP_HZ} (seamless loop)")
return errs
@@ -151,7 +166,7 @@ SCENARIOS = [
"multicast_group": None,
"signals": [
_sig("Counter", "uint32", 1, formula="counter"),
_sig("Sine", "float32", 1, formula="sine", freq=2.0, unit="V"),
_sig("Sine", "float32", 1, formula="sine", freq=5.0, unit="V"),
],
}],
"oracle": "analytic",
@@ -190,7 +205,7 @@ SCENARIOS = [
"signals": [
_sig("Sine", "float32", 1, quant="uint16",
range_min=-5.0, range_max=5.0, formula="sine",
freq=3.0, unit="V"),
freq=10.0, unit="V"),
],
}],
"oracle": "analytic",