STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
t7_integration.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2"""LabJack T7 end to end, hermetically: t7_bridge -> calibration_service -> calibrated tables.
3
4Runs the real binaries against a stand-in Elodin that relays what t7_bridge publishes to
5calibration_service, the way elodin-db's subscriptions do, and records what calibration_service
6publishes back. Everything happens in a temporary directory with its own config and calibration
7store, on OS-assigned ports: nothing here touches hardware, the live calibration store, or a
8running stack (unlike test_integration.sh — see daq-server/CLAUDE.md).
9
10 1. chain t7_bridge --fake streams a PT (4-20 mA), a TC, an RTD (LJTick-Resistance) and an LC. calibration_service must
11 publish psi / °C / kg in the ranges the fake signals sit in, take Zero All and a
12 tare on the T7 inputs, and file them as microvolt channels / under T7<n>_Cal.AIN<a>.
13 2. cubic Packets injected directly, at exact voltages: two capture points per input put
14 2.5 V at 500 psi and 10 mV at 250 kg, and the fit survives a service restart.
15 3. restart t7_bridge --fake-state: kill -9 one bridge (its stand-in T7 keeps streaming, as a
16 real one does), start another; it must stop the leftover stream, reconfigure and
17 stream. A clean SIGTERM must leave the device idle.
18 4. mixed (needs elodin-db; skipped without it) The real database, an ESP32 PT board
19 (board_simulator -> daq_bridge) streaming, calibration_service subscribed, and only
20 THEN t7_bridge registering its tables. Elodin does not deliver a table that did not
21 exist at subscribe time, so the T7 tables must be re-subscribed — while ESP32 packets
22 keep arriving. Both boards' calibrated tables must be recorded, and the run exports.
23
24Usage (ctest runs it with both paths):
25 t7_integration.py --bridge build/bin/t7_bridge --cal build/bin/calibration_service
26"""
27
28from __future__ import annotations
29
30import argparse
31import json
32import os
33import shutil
34import socket
35import struct
36import subprocess
37import sys
38import tempfile
39import threading
40import time
41from pathlib import Path
42
43T7 = 0x26
44SLOT = 1 # board_id 81
45CJC_OFFSET = 0x0F
46
47CONFIG = """
48[database]
49host = "127.0.0.1"
50port = {port}
51
52[boards.labjack_1]
53type = "T7"
54ip = "192.168.2.81"
55board_id = 81
56scan_rate_hz = 100.0
57
58[[boards.labjack_1.channels]]
59ain = 1
60kind = "PT"
61role = "T7 PT"
62signal = "4-20mA"
63shunt_ohms = 250.0
64full_scale = 1000.0
65
66[[boards.labjack_1.channels]]
67ain = 2
68kind = "TC"
69tc_type = "K"
70role = "T7 TC"
71
72[[boards.labjack_1.channels]]
73ain = 3
74kind = "RTD"
75role = "T7 RTD"
76r0_ohms = 1000.0
77ljtr_ohms = 1000.0
78
79[[boards.labjack_1.channels]]
80ain = 4
81kind = "LC"
82role = "T7 LC"
83differential = true
84range = 0.1
85full_scale = 500.0
86sensitivity_mv_per_v = 2.0
87excitation_v = 10.0
88{extra}
89"""
90
91failures: list[str] = []
92
93
94def check(ok: bool, what: str) -> None:
95 print((" ok " if ok else " FAIL ") + what, flush=True)
96 if not ok:
97 failures.append(what)
98
99
100def frame(hi: int, lo: int, body: bytes) -> bytes:
101 """ElodinClient framing: u32 len (body + 4) | ty | id hi | id lo | req | body."""
102 return struct.pack("<IBBBB", 4 + len(body), 1, hi, lo, 0) + body
103
104
105def raw_body(lo: int, value: float, ms: int = 0) -> bytes:
106 return struct.pack("<QB3xfIB", time.time_ns(), lo & 0x1F, value, ms, 0)
107
108
110 """Accepts any number of clients. A T7 raw packet from one is relayed to all the others
111 (elodin-db fanning a table out to its subscribers); every T7 calibrated packet is recorded."""
112
113 def __init__(self) -> None:
114 self.srv = socket.socket()
115 self.srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
116 self.srv.bind(("127.0.0.1", 0))
117 self.srv.listen()
118 self.port = self.srv.getsockname()[1]
119 self.clients: list[socket.socket] = []
120 self.lock = threading.Lock()
121 self.calibrated: list[tuple[int, float, int, int]] = [] # (lo, value, code, status)
122 self.raw_seen: set[int] = set()
123 self.raw_count = 0
124 threading.Thread(target=self._accept, daemon=True).start()
125
126 def _accept(self) -> None:
127 while True:
128 try:
129 c, _ = self.srv.accept()
130 except OSError:
131 return
132 with self.lock:
133 self.clients.append(c)
134 threading.Thread(target=self._read, args=(c,), daemon=True).start()
135
136 def _read(self, c: socket.socket) -> None:
137 buf = b""
138 while True:
139 try:
140 d = c.recv(65536)
141 except OSError:
142 break
143 if not d:
144 break
145 buf += d
146 while len(buf) >= 8:
147 (ln,) = struct.unpack_from("<I", buf)
148 if len(buf) < 4 + ln:
149 break
150 pkt, buf = buf[: 4 + ln], buf[4 + ln :]
151 hi, lo, body = pkt[5], pkt[6], pkt[8:]
152 if hi != T7 or len(body) != 21:
153 continue
154 if (lo & 0x1F) >= 0x10:
155 _, _, value, code, status = struct.unpack("<QB3xfiB", body)
156 with self.lock:
157 self.calibrated.append((lo, value, code, status))
158 else:
159 with self.lock:
160 self.raw_seen.add(lo)
161 self.raw_count += 1
162 others = [o for o in self.clients if o is not c]
163 for o in others:
164 try:
165 o.sendall(pkt)
166 except OSError:
167 pass
168 with self.lock:
169 if c in self.clients:
170 self.clients.remove(c)
171
172 def send_all(self, data: bytes) -> None:
173 with self.lock:
174 targets = list(self.clients)
175 for c in targets:
176 try:
177 c.sendall(data)
178 except OSError:
179 pass
180
181 def command(self, cmd: int, uid: int, ref: float) -> None:
182 """A CalibrationCommand [0x46,0x00]: ts | cmd @8 | pad | uid (u16 LE) @10 | ref f32 @12."""
183 self.send_all(frame(0x46, 0x00, struct.pack("<QBBHf", time.time_ns(), cmd, 0, uid, ref)))
184
185 def latest(self, ain: int, n: int = 20) -> list[tuple[int, float, int, int]]:
186 lo = (SLOT - 1) * 0x20 + 0x10 + ain
187 with self.lock:
188 return [x for x in self.calibrated if x[0] == lo][-n:]
189
190 def wait_clients(self, n: int, timeout: float = 10.0) -> bool:
191 end = time.time() + timeout
192 while time.time() < end:
193 with self.lock:
194 if len(self.clients) >= n:
195 return True
196 time.sleep(0.05)
197 return False
198
199 def close(self) -> None:
200 self.srv.close()
201 with self.lock:
202 for c in self.clients:
203 c.close()
204
205
206def start_cal(cal: str, work: Path, port: int) -> subprocess.Popen:
207 return subprocess.Popen(
208 [cal, "--config", "config/config.toml", "--elodin-port", str(port), "--adjustments", "adj.json"],
209 cwd=work, stdout=open(work / "cal.log", "a"), stderr=subprocess.STDOUT)
210
211
212def stop(p: subprocess.Popen) -> None:
213 p.terminate()
214 try:
215 p.wait(timeout=10)
216 except subprocess.TimeoutExpired:
217 p.kill()
218
219
220def setup(root: Path, port: int, extra: str = "") -> Path:
221 (root / "scripts/calibration/calibrations").mkdir(parents=True)
222 (root / "config").mkdir()
223 (root / "config/config.toml").write_text(CONFIG.format(port=port, extra=extra))
224 return root
225
226
227def store(work: Path, name: str) -> dict:
228 try:
229 return json.loads((work / "scripts/calibration/calibrations" / name).read_text())
230 except (OSError, json.JSONDecodeError):
231 return {}
232
233
234def values(rows) -> list[float]:
235 return [r[1] for r in rows]
236
237
238def scenario_chain(bridge: str, cal: str, root: Path) -> None:
239 print("=== chain: t7_bridge --fake -> calibration_service ===", flush=True)
240 db = FakeElodin()
241 work = setup(root / "chain", db.port)
242 calp = start_cal(cal, work, db.port)
243 check(db.wait_clients(1), "calibration_service connects")
244 br = subprocess.Popen([bridge, "--config", str(work / "config/config.toml"), "--fake"],
245 stdout=open(work / "bridge.log", "w"), stderr=subprocess.STDOUT)
246 try:
247 check(db.wait_clients(2), "t7_bridge connects")
248 time.sleep(4.0)
249 base = (SLOT - 1) * 0x20
250 check({base + 1, base + 2, base + 3, base + 4, base + 14, base + CJC_OFFSET} <= db.raw_seen,
251 "bridge publishes AIN1/2/3/4, AIN14 and the cold junction (0x0F) raw")
252 check(not any(lo & 0x1F >= 0x10 for lo in db.raw_seen),
253 "bridge publishes no calibrated table (that is calibration_service's)")
254
255 # The --fake signals: PT 30 % of full scale ±3 %, LC 20 % of rated ±2 %, TC 60 ±10 °C.
256 pt, tc, lc = db.latest(1), db.latest(2), db.latest(4)
257 check(len(pt) > 0 and all(260 <= v <= 340 for v in values(pt)) and all(r[3] == 1 for r in pt),
258 f"PT 4-20 mA -> psi, physics (default model): {values(pt)[-1:] if pt else 'none'}")
259 check(len(tc) > 0 and all(48 <= v <= 72 for v in values(tc)),
260 f"TC volts + cold junction -> °C: {values(tc)[-1:] if tc else 'none'}")
261 check(len(lc) > 0 and all(85 <= v <= 115 for v in values(lc)),
262 f"LC volts -> kg: {values(lc)[-1:] if lc else 'none'}")
263 # RTD: a Pt1000 on an LJTick-Resistance-1k, faked at -150 ±20 °C through the divider.
264 rtd = db.latest(3)
265 check(len(rtd) > 0 and all(-171 <= v <= -129 for v in values(rtd)) and all(r[3] == 1 for r in rtd),
266 f"RTD divider volts -> ohms -> °C (IEC 60751): {values(rtd)[-1:] if rtd else 'none'}")
267 # 4-20 mA across 250 ohm: psi = (V - 1) / 4 * 1000. The PT output is smoothed (EMA), so
268 # the code and the value agree to within the fake signal's few psi of wobble.
269 check(len(pt) > 0 and abs((pt[-1][2] / 1e6 - 1.0) / 4.0 * 1000.0 - pt[-1][1]) < 15.0,
270 f"calibrated table carries the µV code it was converted from: "
271 f"{(pt[-1][2], round(pt[-1][1], 1)) if pt else 'none'}")
272
273 db.command(0, 0, 0.0) # Zero All
274 time.sleep(0.5)
275 db.command(8, 8104, 0.0) # tare the T7 load cell
276 time.sleep(1.0)
277 finally:
278 stop(br)
279 stop(calp)
280 db.close()
281 cs = store(work, "cubic_calibration.json").get("cubic_state", {})
282 check(set(cs) == {"8101", "8104"}, f"store holds the T7 PT and LC by uid: {sorted(cs)}")
283 check(all(cs[u].get("rawUnit") == "uV" for u in cs), "filed as microvolt channels")
284 check(cs.get("8101", {}).get("logicalCh") == 129 and cs.get("8104", {}).get("logicalCh") == 132,
285 "on logical channels 128 + ain, clear of the ESP32 boards")
286 check(all(cs[u].get("numPoints") == 1 for u in cs), "Zero All captured a point on each")
287 tares = store(work, "lc_tare.json").get("tares", [])
288 check(len(tares) == 1 and tares[0].get("entity") == "T71_Cal.AIN4" and 85 <= tares[0].get("offset_kg", 0) <= 115,
289 f"tare filed under the T7 entity at the load it held: {tares}")
290
291
292def scenario_cubic(cal: str, root: Path) -> None:
293 print("=== cubic: captured points on T7 inputs ===", flush=True)
294 models = '\n[calibration_model_labjack_1]\n"T7 PT" = "cubic"\n"T7 LC" = "cubic"\n'
295 signal = {1: 2.5 * 0.4, 2: 0.005259, 4: 0.005} # PT in volts across the 250 ohm shunt
296 stop_stream = threading.Event()
297
298 def stream(db: FakeElodin) -> None:
299 k = 0
300 while not stop_stream.is_set():
301 pkts = frame(T7, CJC_OFFSET, raw_body(CJC_OFFSET, 22.0, k * 10))
302 for ain, v in list(signal.items()):
303 pkts += frame(T7, (SLOT - 1) * 0x20 + ain, raw_body(ain, v, k * 10))
304 db.send_all(pkts)
305 k += 1
306 time.sleep(0.01)
307
308 def run(db: FakeElodin, work: Path, steps) -> None:
309 calp = start_cal(cal, work, db.port)
310 try:
311 check(db.wait_clients(1), "calibration_service connects")
312 stop_stream.clear()
313 t = threading.Thread(target=stream, args=(db,), daemon=True)
314 t.start()
315 steps(db)
316 stop_stream.set()
317 t.join()
318 finally:
319 stop(calp)
320
321 db = FakeElodin()
322 work = setup(root / "cubic", db.port, models)
323
324 def captures(db: FakeElodin) -> None:
325 time.sleep(2.0)
326 pt = db.latest(1)
327 check(len(pt) > 0 and pt[-1][1] == 0.0 and pt[-1][3] == 0,
328 "an uncalibrated cubic T7 PT reads 0 with cal_status 0, like an ESP32 one")
329 # 4-20 mA across 250 ohm: 1 V is 0 psi, 5 V is 1000 psi.
330 signal[1], signal[4] = 1.0, 0.0
331 time.sleep(1.5)
332 db.command(5, 8101, 0.0)
333 db.command(5, 8104, 0.0)
334 time.sleep(0.3)
335 signal[1], signal[4] = 5.0, 0.020
336 time.sleep(1.5)
337 db.command(5, 8101, 1000.0)
338 db.command(5, 8104, 500.0)
339 time.sleep(0.3)
340 signal[1], signal[4] = 3.0, 0.010
341 time.sleep(2.0)
342 pt, lc, tc = db.latest(1, 5), db.latest(4, 5), db.latest(2, 5)
343 check(len(pt) > 0 and abs(pt[-1][1] - 500.0) < 1.0 and pt[-1][3] == 1,
344 f"PT captured at 1 V->0, 5 V->1000 reads 3 V as 500 psi: {values(pt)[-1:]}")
345 check(len(lc) > 0 and abs(lc[-1][1] - 250.0) < 0.5 and lc[-1][3] == 1,
346 f"LC captured at 0->0, 20 mV->500 reads 10 mV as 250 kg: {values(lc)[-1:]}")
347 check(len(tc) > 0 and abs(tc[-1][1] - 150.0) < 0.5,
348 f"TC at a 22 °C junction reads 150 °C: {values(tc)[-1:]}")
349
350 run(db, work, captures)
351 cs = store(work, "cubic_calibration.json").get("cubic_state", {})
352 check([round(p["adc"]) for p in cs.get("8101", {}).get("points", [])] == [1_000_000, 5_000_000],
353 "the PT's points are stored in µV")
354
355 def after_restart(db: FakeElodin) -> None:
356 time.sleep(2.0)
357 pt = db.latest(1, 5)
358 check(len(pt) > 0 and abs(pt[-1][1] - 500.0) < 1.0 and pt[-1][3] == 1,
359 f"the fit survives a calibration_service restart: {values(pt)[-1:]}")
360
361 run(db, work, after_restart)
362 db.close()
363
364
365def free_port(kind: int = socket.SOCK_STREAM) -> int:
366 s = socket.socket(socket.AF_INET, kind)
367 s.bind(("127.0.0.1", 0))
368 port = s.getsockname()[1]
369 s.close()
370 return port
371
372
373def find_elodin_db(given: str | None) -> str | None:
374 for cand in (given, os.path.expanduser("~/.cargo/bin/elodin-db"), shutil.which("elodin-db")):
375 if cand and os.path.isfile(cand) and os.access(cand, os.X_OK):
376 return cand
377 return None
378
379
380MIXED_EXTRA = """
381[network]
382bind_ip = "127.0.0.1"
383sensor_port = {udp}
384
385# heartbeat_service owns the server heartbeat, so daq_bridge sends none (no broadcast here).
386[heartbeat_service]
387enabled = true
388
389[boards.pt_board]
390type = "PT"
391pt_type = "0-5V ratiometric"
392ip = "127.0.0.21"
393board_id = 21
394enabled = true
395voltage_reference = 1
396active_connectors = [1, 2]
397
398[sensor_roles_pt_board]
399"ESP PT" = 1
400
401[calibration_model_pt_board]
402"ESP PT" = "physics"
403"""
404
405
406def export_rows(elodin: str, db: Path, out: Path, component: str) -> int:
407 """Rows elodin-db holds for one component, via its own CSV export."""
408 subprocess.run([elodin, "export", str(db), "-o", str(out), "--format", "csv", "--flatten",
409 "--pattern", component], capture_output=True, check=False)
410 files = list(out.glob(f"{component}*.csv"))
411 if not files:
412 return 0
413 return max(0, len(files[0].read_text().splitlines()) - 1)
414
415
416def scenario_mixed(bridge: str, cal: str, daq_bridge: str, simulator: str, elodin: str,
417 root: Path) -> None:
418 print("=== mixed: real elodin-db, ESP32 streaming, T7 registering late ===", flush=True)
419 port, udp = free_port(), free_port(socket.SOCK_DGRAM)
420 work = setup(root / "mixed", port, MIXED_EXTRA.format(udp=udp))
421 cfg = str(work / "config/config.toml")
422 db_dir = work / "db"
423 procs: list[subprocess.Popen] = []
424
425 def spawn(args, log, cwd=work):
426 p = subprocess.Popen(args, cwd=cwd, stdout=open(work / log, "w"), stderr=subprocess.STDOUT)
427 procs.append(p)
428 return p
429
430 try:
431 spawn([elodin, "run", f"127.0.0.1:{port}", str(db_dir)], "elodin.log")
432 time.sleep(2.0)
433 spawn([daq_bridge, cfg], "daq_bridge.log")
434 spawn([sys.executable, simulator, "--config", cfg, "--port", str(udp), "--only-type", "PT",
435 "--skip-startup", "--sensor-hz", "50", "--duration", "30"], "simulator.log")
436 time.sleep(2.0)
437 procs.append(start_cal(cal, work, port))
438 time.sleep(4.0) # ESP32 data flowing and calibrated before the T7 exists
439 spawn([bridge, "--config", cfg, "--fake"], "bridge.log")
440 time.sleep(14.0)
441 finally:
442 for p in reversed(procs):
443 stop(p)
444 out = work / "export"
445 esp = export_rows(elodin, db_dir, out, "PT1_Cal.CH1.pressure_psi")
446 t7_raw = export_rows(elodin, db_dir, out, "T71.AIN1.volts")
447 t7_cal = export_rows(elodin, db_dir, out, "T71_Cal.AIN1.pressure_psi")
448 cjc = export_rows(elodin, db_dir, out, "T71.CJC.temperature_c")
449 check(esp > 100, f"ESP32 PT calibrated rows recorded: {esp}")
450 check(t7_raw > 500 and cjc > 500, f"T7 raw and cold-junction rows recorded: {t7_raw}, {cjc}")
451 check(t7_cal > 300,
452 f"T7 calibrated rows recorded though the T7 registered after the subscribe: {t7_cal}")
453
454
455def state(path: Path) -> dict[str, str]:
456 """The stand-in T7's state file: 'streaming 1', 'stops 0', ... as a dict."""
457 out: dict[str, str] = {}
458 try:
459 for line in path.read_text().splitlines():
460 k, _, v = line.partition(" ")
461 if k != "reg":
462 out[k] = v
463 except OSError:
464 pass
465 return out
466
467
468def scenario_restart(bridge: str, root: Path) -> None:
469 """A T7 keeps streaming after the bridge that started it dies, and refuses configuration
470 until that stream is stopped. --fake-state makes the stand-in T7 a file that outlives the
471 process, so kill -9 leaves it streaming exactly as a server torn down mid-run leaves a real
472 one, and the next bridge has to take it over."""
473 print("=== restart: kill -9 the bridge, start another on the same T7 ===", flush=True)
474 db = FakeElodin()
475 work = setup(root / "restart", db.port)
476 devdir = work / "t7state"
477 devdir.mkdir()
478 unit = devdir / "boards.labjack_1.state"
479 cfg = str(work / "config/config.toml")
480
481 def spawn(log: str) -> subprocess.Popen:
482 return subprocess.Popen([bridge, "--config", cfg, "--fake", "--fake-state", str(devdir)],
483 stdout=open(work / log, "w"), stderr=subprocess.STDOUT)
484
485 def flowing(after: int, timeout: float = 8.0) -> bool:
486 end = time.time() + timeout
487 while time.time() < end:
488 with db.lock:
489 if db.raw_count > after + 50:
490 return True
491 time.sleep(0.1)
492 return False
493
494 def count() -> int:
495 with db.lock:
496 return db.raw_count
497
498 procs: list[subprocess.Popen] = []
499 try:
500 a = spawn("bridge_a.log")
501 procs.append(a)
502 check(flowing(0), "first bridge streams")
503 a.kill() # SIGKILL: no stop reaches the device
504 a.wait()
505 st = state(unit)
506 check(st.get("streaming") == "1" and st.get("stops") == "0",
507 f"killed with -9, the T7 is left streaming to nobody ({st})")
508
509 n = count()
510 b = spawn("bridge_b.log")
511 procs.append(b)
512 check(flowing(n), "the next bridge streams from the same T7")
513 log_b = (work / "bridge_b.log").read_text(errors="replace")
514 check("stopped a stream already running" in log_b,
515 "and says it stopped the stream the killed one left")
516 check("retrying" not in log_b, "without a failed start first")
517 st = state(unit)
518 check(st.get("starts") == "2" and st.get("stops") == "1" and st.get("streaming") == "1",
519 f"one stop, a second start ({st})")
520
521 b.terminate() # SIGTERM: a clean shutdown stops the stream
522 b.wait(timeout=10)
523 check(state(unit).get("streaming") == "0", "a clean shutdown leaves the T7 idle")
524
525 n = count()
526 c = spawn("bridge_c.log")
527 procs.append(c)
528 check(flowing(n), "a bridge after a clean shutdown streams")
529 check("stopped a stream already running" not in
530 (work / "bridge_c.log").read_text(errors="replace"),
531 "and finds nothing to stop")
532 finally:
533 for p in procs:
534 if p.poll() is None:
535 stop(p)
536 db.close()
537
538
539def main() -> int:
540 ap = argparse.ArgumentParser()
541 ap.add_argument("--bridge", required=True)
542 ap.add_argument("--cal", required=True)
543 ap.add_argument("--daq-bridge", help="daq_bridge, for the mixed (real elodin-db) scenario")
544 ap.add_argument("--simulator", help="sim/board_simulator.py, for the mixed scenario")
545 ap.add_argument("--elodin-db", help="elodin-db binary (default: ~/.cargo/bin, then PATH)")
546 a = ap.parse_args()
547 bridge, cal = os.path.abspath(a.bridge), os.path.abspath(a.cal)
548 with tempfile.TemporaryDirectory(prefix="t7_integration_") as d:
549 root = Path(d)
550 scenario_chain(bridge, cal, root)
551 scenario_cubic(cal, root)
552 scenario_restart(bridge, root)
553 elodin = find_elodin_db(a.elodin_db)
554 if elodin and a.daq_bridge and a.simulator:
555 scenario_mixed(bridge, cal, os.path.abspath(a.daq_bridge), os.path.abspath(a.simulator),
556 elodin, root)
557 else:
558 print("=== mixed: SKIPPED (needs elodin-db, --daq-bridge and --simulator) ===", flush=True)
559 if failures:
560 for log in sorted(root.glob("*/*.log")):
561 print(f"--- {log.relative_to(root)} (tail) ---")
562 print("\n".join(log.read_text(errors="replace").splitlines()[-15:]))
563 print("FAILED: " + str(len(failures)) if failures else "PASSED", flush=True)
564 return 1 if failures else 0
565
566
567if __name__ == "__main__":
568 sys.exit(main())
#define max(a, b)
Definition Arduino.h:43
list[tuple[int, float, int, int]] latest(self, int ain, int n=20)
None command(self, int cmd, int uid, float ref)
bool wait_clients(self, int n, float timeout=10.0)
None send_all(self, bytes data)
None _read(self, socket.socket c)
void setup()
Definition main.cpp:571
static ActuatorControllerState state
Definition main.cpp:57
static void start(gnss_frame_parser_t *p, uint8_t byte)
Definition gnss_frame.c:45
size_type count(const key_type &key) const
Definition json.hpp:19264
Definition run.py:1
dict store(Path work, str name)
None scenario_cubic(str cal, Path root)
list[float] values(rows)
int free_port(int kind=socket.SOCK_STREAM)
str|None find_elodin_db(str|None given)
int export_rows(str elodin, Path db, Path out, str component)
None scenario_restart(str bridge, Path root)
None scenario_chain(str bridge, str cal, Path root)
subprocess.Popen start_cal(str cal, Path work, int port)
bytes frame(int hi, int lo, bytes body)
None scenario_mixed(str bridge, str cal, str daq_bridge, str simulator, str elodin, Path root)
bytes raw_body(int lo, float value, int ms=0)
None check(bool ok, str what)
None stop(subprocess.Popen p)
TOML_NODISCARD_CTOR time() noexcept=default