#!/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("