From 558d13d521dbb87ccba6f0164e9f5cb29c3041bc Mon Sep 17 00:00:00 2001 From: Martino Ferrari Date: Sun, 1 Nov 2026 09:39:25 +0100 Subject: [PATCH] tools: a protocol mock of the MCC E-1608, with its unverified commands listed Co-Authored-By: Claude Sonnet 4.6 --- tools/mock_e1608.py | 400 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 400 insertions(+) create mode 100755 tools/mock_e1608.py diff --git a/tools/mock_e1608.py b/tools/mock_e1608.py new file mode 100755 index 0000000..d63a340 --- /dev/null +++ b/tools/mock_e1608.py @@ -0,0 +1,400 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: 2026 Martino Ferrari +"""A protocol mock of the Measurement Computing E-1608 Ethernet DAQ, for exercising udpscope +without a board. It is the *contract* the `e1608` source is verified against until a board is +attached, so its layouts are only as good as the two references they were taken from: + + * MARTe2 `E1608DataSource` (~/Projects/marte2_projects/MCC_E1608_DataSource), the primary, and + * the Go `uopi/internal/datasource/mcc` implementation, the cross-check. + + ./tools/mock_e1608.py --port 54211 [--code N] [--drop-after S] [--overrun-after S] [--log F] + +Serves: UDP `D` discovery and `C` connection-code handshake on --port; the TCP command channel on +--port; the TCP scan stream on --port+1 (it starts when the one filler byte follows the scan-start +command). Frame: 0xDB, cmd, frame id, status, count LE16, payload, checksum (the whole frame sums +to 0xFF mod 256). Reply: cmd | 0x80, same id. + +UNVERIFIED -- every item below rests on a single reference, or the references disagree. They are +the first places to look when a real board misbehaves: + + * 0x31 counter reset: Go only (no payload, no reply payload). + * Pacer: MARTe2 sends round(80e6 / (scan_rate * channels) - 1), the *aggregate* rate; Go sends the + passed frequency. This mock follows MARTe2. + * The scan-start options byte (trigger): none 0, rising 4, falling 8, high 12, low 16 -- MARTe2 only. + * Handshake status 2 ("code ignored") is accepted by MARTe2 and refused by Go. The mock never sends it. + * The first frame id: MARTe2 pre-increments (first frame is 1), Go starts at 0. The mock echoes it. + * The maximum frame size (MARTe2: 1024 in total; Go: 1024 of payload). The mock enforces neither. + * Agreed by both references (not in doubt): the filler byte after scan start, the scan port being + command port + 1, DIFF wire channel = channel + 8. + +Switches: --drop-after S closes both connections once, S seconds after the first command connection +(logs DROP); --overrun-after S raises the overrun bit and stops the stream S seconds after a scan +starts, once (logs OVERRUN); --code N is the connection code the board accepts (anything else gets +status 1). +""" + +import argparse +import socket +import struct +import sys +import threading +import time + +import numpy as np + +CLOCK = 80e6 +PRODUCT_ID = 0x012F +FS = [10.0, 5.0, 2.0, 1.0] # full scale per range code BIP10V, BIP5V, BIP2V, BIP1V + + +def f32(x): + return struct.unpack("= 8 + ch = wire - 8 if diff else wire + fs = FS[rng] + dc = min(0.5 * (ch + 1), 0.8 * fs) + v = np.clip(dc + 0.1 * np.sin(2.0 * np.pi * 5.0 * t), -0.98 * fs, 0.98 * fs) + code = v * 32768.0 / fs + 32768.0 + slope, icpt = (DF_CAL if diff else SE_CAL)[rng] + return np.clip(np.rint((code - icpt) / slope), 0, 65535) + + # --- commands --------------------------------------------------------------------------- + def handle(self, cmd, p): + """Returns (status, reply payload). Status 1 protocol, 2 parameter.""" + if cmd == 0x00: + return 0, bytes([self.din()]) + if cmd == 0x02: + return 0, bytes([self.dout]) + if cmd == 0x03: + if len(p) < 1: + return 2, b"" + self.dout = p[0] + self.log("DOUT 0x%02x" % p[0]) + return 0, b"" + if cmd == 0x05: + if len(p) < 1: + return 2, b"" + self.dconf = p[0] + self.log("DCONF 0x%02x" % p[0]) + return 0, b"" + if cmd == 0x10: + return 0, struct.pack(" 8 or len(p) != 1 + 2 * n: + return 2, b"" + q = [(p[1 + 2 * i], p[2 + 2 * i]) for i in range(n)] + if any(r > 3 for _, r in q): + return 2, b"" + self.queue = q + self.log("QUEUE " + " ".join("%d:%d" % e for e in q)) + return 0, b"" + if cmd == 0x20: + return 0, struct.pack(" 1: + return 2, b"" + ch, code = struct.unpack(" 0 else 1000.0 * nq # external clock: 1 kS/s each + rate = agg / nq + t0 = time.monotonic() + sent = 0 + ov_at = None + if self.args.overrun_after > 0 and not self.overrun_done: + ov_at = t0 + self.args.overrun_after + self.log("STREAM rate=%.1f channels=%d" % (rate, nq)) + try: + while self.stream_gen == gen: + now = time.monotonic() + if ov_at is not None and now >= ov_at: + with self.lock: + if self.stream_gen == gen: + self.overrun = True + self.overrun_done = True + self.streaming = False + self.log("OVERRUN") + return + due = min(int((now - t0) * rate) - sent, 65536) + if due > 0: + t = np.arange(sent, sent + due) / rate + cols = [self.raw(w, r, t) for (w, r) in queue] + conn.sendall(np.stack(cols, axis=1).astype("= 5: + (code,) = struct.unpack(" 0: + threading.Timer(self.args.drop_after, self.drop).start() + try: + while True: + b = conn.recv(1) + if not b: + break + if b[0] == 0x00: + self.start_stream() # the filler byte after scan start + continue + if b[0] != 0xDB: + continue + rest = recv_exact(conn, 5) + if rest is None: + break + cmd, fid, _status, count = struct.unpack("