"""Exercise real SDK instances for 1-4 recorded inputs, without UART/hardware.""" import argparse import itertools import json import os import sys from functools import partial from pathlib import Path from unittest.mock import patch sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "serial")) from finger_api import FingerSensorBank def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--recording", required=True, type=Path) parser.add_argument("--scratch", required=True, type=Path) parser.add_argument("--calibration", type=Path, help="explicit calibration used for all offline test sources") args = parser.parse_args() scratch = args.scratch.resolve() scratch.mkdir(parents=True, exist_ok=True) os.environ["NUMBA_CACHE_DIR"] = str(scratch / "numba") import cv2 import numpy as np import orisys from orisys.finger import FingerRuntime print(f"SDK: {orisys.__file__}", flush=True) capture = cv2.VideoCapture(str(args.recording.resolve())) frames = [] try: for _ in range(12): ok, frame = capture.read() if not ok: raise RuntimeError("recording must contain at least 12 frames") frames.append(frame) finally: capture.release() height, width = frames[0].shape[:2] sources = [] calibrations = [] for sensor_id in range(1, 5): path = scratch / f"sensor-{sensor_id}.avi" writer = cv2.VideoWriter(str(path), cv2.VideoWriter_fourcc(*"MJPG"), 30, (width, height)) try: if not writer.isOpened(): raise RuntimeError("cannot create test recording") for frame in frames: writer.write(np.roll(frame, sensor_id, axis=1)) finally: writer.release() sources.append(str(path)) if args.calibration: calibration = scratch / f"sensor-{sensor_id}-calibration.json" calibration.write_bytes(args.calibration.read_bytes()) calibrations.append(str(calibration)) # Keep the SDK implementation intact; redirect only its generated UV cache. open_runtime = partial(FingerRuntime.open, map_cache=str(scratch / "uv-map.npz")) with patch.object(FingerRuntime, "open", side_effect=open_runtime): for count in range(1, 5): bank = FingerSensorBank() try: for index, source in enumerate(sources[:count]): existing = dict(bank.runtimes) bank.add(source, cal_path=calibrations[index] if calibrations else None) assert all(bank.runtimes[i] is runtime for i, runtime in existing.items()) assert len(bank.read_all()) == len(bank.runtimes) if calibrations: for sensor_id, runtime in bank.runtimes.items(): calibration = runtime.tactile.raw_sensor.calibration assert calibration is not None assert calibration.source_path.resolve() == Path(calibrations[sensor_id - 1]) assert runtime.tactile.force_units == ("N", "N", "N") runtimes = list(bank.runtimes.values()) for left, right in itertools.combinations(runtimes, 2): assert left.capture is not right.capture assert left.tactile.raw_sensor is not right.tactile.raw_sensor assert not np.shares_memory(left.tactile.flow, right.tactile.flow) for _ in range(3): readings = bank.read_all() assert [r.sensor_id for r in readings] == list(range(1, count + 1)) assert all(np.isfinite([r.normal, r.shear_x, r.shear_y]).all() for r in readings) print(f"PASS {count} sources: incremental add/read, independent states, 3 final rounds", flush=True) finally: bank.close() assert all(runtime.capture is None for runtime in runtimes) setup = scratch / "sensors.json" setup.write_text(json.dumps({"sensors": [ {"source": source, **({"calibration": calibrations[i]} if calibrations else {})} for i, source in enumerate(sources) ]}), encoding="utf-8") bank = FingerSensorBank() try: assert bank.add_setup(setup) == (1, 2, 3, 4) assert len(bank.read_all()) == 4 bad_calibration = scratch / "invalid-calibration.json" bad_calibration.write_text("{}", encoding="utf-8") finally: bank.close() bank = FingerSensorBank() try: bank.add(sources[0]) first = bank.runtimes[1] try: bank.add(sources[1], cal_path=str(bad_calibration)) except ValueError: pass else: raise AssertionError("invalid explicit calibration was accepted") assert bank.runtimes == {1: first} assert len(bank.read_all()) == 1 finally: bank.close() print("PASS 4-source JSON setup; invalid calibration rejected without fallback", flush=True) return 0 if __name__ == "__main__": raise SystemExit(main())