diff --git a/docs/brainstem-sdk.md b/docs/brainstem-sdk.md index 39c7db5..1b6bc19 100644 --- a/docs/brainstem-sdk.md +++ b/docs/brainstem-sdk.md @@ -36,7 +36,13 @@ That installs udev rules (typically requires `sudo`) and adds the current user t ## Build C/C++ examples from source -Sample projects live under `api/examples/c_cpp/`. Each example includes `How_To_Build.txt` or `How_to_Build.txt` with Acroname’s steps. In general: +Sample projects live under `api/examples/c_cpp/`. Each example includes `How_To_Build.txt` or `How_to_Build.txt` with Acroname’s steps (note the full filename — `find -name How` will not match). From the directory that contains `api/`: + +```bash +find api/examples/c_cpp -maxdepth 2 -iname 'how*build*.txt' -print +``` + +Example: `api/examples/c_cpp/BrainStem2-Cpp-Example/How_To_Build.txt`. In general: 1. Open that file in the example directory you care about. 2. Copy the **`lib`** directory from the SDK into the example tree where the readme says (headers and `BrainStem2` shared library). diff --git a/fiwi/cli.py b/fiwi/cli.py index 3385890..951adf1 100644 --- a/fiwi/cli.py +++ b/fiwi/cli.py @@ -321,6 +321,7 @@ def main() -> int: " fiwi.py panel calibrate [merge] [] [--ssh user@host] …\n" " calibrate: local hub ports first, then each --ssh host, calibrate_remotes in JSON, and/or\n" " FIWI_CALIBRATE_REMOTES in remote_ssh.env (comma-separated) for one-command hybrid.\n" + " Per port: s/skip/. skip · q/quit/exit stop · Ctrl-C save map & exit.\n" " merge / N as before; remote steps set \"ssh\" on new fiber_ports entries.\n" " Calibrate starts by setting patch_panel.slots (front-panel positions); panel is 1…slots.\n" " Use power fiber-port for arbitrary fiber ids beyond the panel if needed.\n" diff --git a/fiwi/concentrator.py b/fiwi/concentrator.py index 083479b..9d0f11b 100644 --- a/fiwi/concentrator.py +++ b/fiwi/concentrator.py @@ -937,7 +937,8 @@ class FiWiConcentrator: have = PatchPanel.from_map_blob(doc.get("patch_panel")) print( "\n--- Patch panel (front-panel positions) ---\n" - f"Fiber map keys 1…N refer to these panel positions (power/status: panel ).\n", + f"Fiber map keys 1…N refer to these panel positions (power/status: panel ).\n" + "Ctrl-C saves fiber_map.json and exits (patch panel may be unchanged).\n", flush=True, ) if have is not None: @@ -1018,7 +1019,16 @@ class FiWiConcentrator: doc = {"fiber_ports": {}} doc = fm.ensure_fiber_map_document(doc) - self._prompt_patch_panel(doc) + try: + self._prompt_patch_panel(doc) + except KeyboardInterrupt: + print( + "\n*** Calibrate interrupted (Ctrl-C) during patch panel; " + "writing fiber_map.json (unchanged or partial). ***\n", + flush=True, + ) + self._write_fiber_map_document(doc) + raise SystemExit(130) self._write_fiber_map_document(doc) seen_h = set() @@ -1208,8 +1218,10 @@ class FiWiConcentrator: "After ON (~2s): fiwi snapshots wireless interfaces on that host (sysfs + lspci/iw) — " "local and SSH — for chip/interface in the map (no external fiwi script).\n" "Local steps: lsusb OFF→ON may also suggest a USB downstream device; USB lsusb is not used on SSH hosts.\n" - "Each step: ON → wlan snapshot → fiber id, s=skip, q=quit.\n" - "Port stays ON through optional PCIe prompts, then powers OFF. Ctrl-C anytime saves fiber_map.json and exits.\n" + "Each step: ON → wlan snapshot → fiber id — " + "s / skip / . = skip this port, q / quit / exit = stop calibrate.\n" + "Port stays ON through optional PCIe prompts, then powers OFF. " + "Ctrl-C saves fiber_map.json and exits (also during patch panel or PCIe prompts).\n" "Remote rows: ssh + hub.port + wlan + pcie (usb_id/chip_type from lsusb are cleared; chip_type may come from wlan).\n" "After each fiber id you can pick PCIe by number: 1–6 = known Adnacom H3 card, then SFP 1–4 " "(no paste), or m=manual / c=clear / Enter=keep. Edit fiber_map.json anytime (see example).", @@ -1342,7 +1354,7 @@ class FiWiConcentrator: ) try: line = input( - "Which fiber port id? [s=skip q=quit, Ctrl-C=save map & exit]: " + "Which fiber port id? [s/skip/.=skip q/quit/exit=done Ctrl-C=save & exit]: " ).strip() except EOFError: print( @@ -1361,11 +1373,11 @@ class FiWiConcentrator: raise SystemExit(130) low = line.lower() - if low == "q": + if low in ("q", "quit", "exit"): if step_powered: self._calibrate_step_power_off(ssh_host, hub_1, port_0) break - if low == "s" or not line: + if low in ("s", "skip", ".") or not line: if step_powered: self._calibrate_step_power_off(ssh_host, hub_1, port_0) continue diff --git a/fiwi/fiber_map_io.py b/fiwi/fiber_map_io.py index 0a84412..167e55d 100644 --- a/fiwi/fiber_map_io.py +++ b/fiwi/fiber_map_io.py @@ -305,10 +305,12 @@ def prompt_pcie_metadata_for_calibrate(existing_pcie): print_catalog_menu() try: r = input( - " PCIe? [Enter=keep, 1–6=card from list, m=manual, c=clear]: " + " PCIe? [Enter/s/skip=keep, 1–6=card, m=manual, c=clear, Ctrl-C=save & exit calibrate]: " ).strip().lower() except EOFError: return ("keep", None) + if r in ("s", "skip"): + return ("keep", None) if r in ("c", "clear"): return ("clear", None) if r in ("m", "manual"): diff --git a/tests/check_concentrator.py b/tests/check_concentrator.py index 8b6c9a7..23f3d64 100755 --- a/tests/check_concentrator.py +++ b/tests/check_concentrator.py @@ -18,6 +18,10 @@ and ``port-metrics-json``; the remote tree must include that command (same revis python tests/check_concentrator.py python tests/check_concentrator.py --config uax24 + python tests/check_concentrator.py --inrush 1 0 + python tests/check_concentrator.py --inrush 1 0 --inrush-host-sample --inrush-json + python tests/check_concentrator.py --panel-calibrate + python tests/check_concentrator.py --panel-calibrate --calibrate-merge --calibrate-ssh pi@192.168.1.39 ``--config`` matches ``FIWI_CONFIG`` (profile or absolute ``*.ini``). @@ -25,6 +29,16 @@ and ``port-metrics-json``; the remote tree must include that command (same revis ``FIWI_CALIBRATE_REMOTES`` / merged hub hosts: all ports OFF (verify), then all ON (verify), then prints the port power table (snapshot after the test). +``--inrush HUB PORT`` (after the report, or after ``--powercycle``) runs the same USB inrush probe as +``tests/check_inrush.py`` on **local** hubs only (1-based hub index, 0-based port). Use +``--inrush-host-sample`` for host-side polling; default is on-hub Reflex scratchpad. + +``--panel-calibrate`` runs the interactive **fiber map** workflow (patch panel size, USB port walk, +wlan + lspci snapshot per step → ``chip_type`` / ``wlan`` / ``hub``+``port`` in ``fiber_map.json``). +Same as ``python3 fiwi.py panel calibrate``. Use ``--calibrate-merge``, ``--calibrate-limit N``, +and repeatable ``--calibrate-ssh user@host``. Needs a TTY; cannot combine with ``--powercycle`` or +``--inrush``. + With pytest:: FIWI_CONFIG=uax24 pytest tests/check_concentrator.py @@ -850,8 +864,15 @@ def _print_per_port_power_table( lines_out.append((st, line)) if n_ma and n_power: + # Literal "120V/220V=…A/…A" suffix (labels, not f-string slash parsing). summary = ( - f"Power({n_on}): Total {total_power_w:.3f} W / {total_ma:.2f} mA (per port follows)" + "Power({n}): Total {tw:.3f} W / {tm:.2f} mA (120V/220V={a120:.2f}A/{a220:.2f}A)" + ).format( + n=n_on, + tw=total_power_w, + tm=total_ma, + a120=total_power_w / 120.0, + a220=total_power_w / 220.0, ) elif n_ma: summary = ( @@ -928,6 +949,177 @@ def _print_pcie_catalog_section() -> None: print(flush=True) +def _prepend_tests_dir_to_syspath() -> None: + td = os.path.join(_ROOT, "tests") + if td not in sys.path: + sys.path.insert(0, td) + + +def _run_inrush_probe(c: FiWiConcentrator, args: argparse.Namespace) -> int: + """Run :mod:`check_inrush` measurement after the hub report; **local** hubs only.""" + assert args.inrush is not None + hub_1, port_0 = int(args.inrush[0]), int(args.inrush[1]) + if hub_1 < 1: + print("check_concentrator --inrush: hub must be >= 1", file=sys.stderr, flush=True) + return 2 + if port_0 < 0: + print("check_concentrator --inrush: port must be >= 0", file=sys.stderr, flush=True) + return 2 + + _prepend_tests_dir_to_syspath() + import check_inrush as ci # noqa: E402 + + if not c.hubs: + if not c.connect(): + print( + "check_concentrator --inrush: no local USB power-control hubs connected.", + file=sys.stderr, + flush=True, + ) + return 1 + hi = hub_1 - 1 + if hi < 0 or hi >= len(c.hubs): + print( + f"check_concentrator --inrush: hub {hub_1} invalid (have {len(c.hubs)} hub(s)).", + file=sys.stderr, + flush=True, + ) + return 1 + stem = c.hubs[hi] + n = c._port_count(stem) + if port_0 < 0 or port_0 >= n: + print( + f"check_concentrator --inrush: port {port_0} out of range for hub {hub_1} (0..{n - 1}).", + file=sys.stderr, + flush=True, + ) + return 1 + + if not args.inrush_host_sample and port_0 != 0: + print( + "check_concentrator --inrush: on-hub reflex/inrush.reflex monitors port 0 only; " + f"you asked for port {port_0}. Recompile Reflex or use --inrush-host-sample.", + file=sys.stderr, + flush=True, + ) + + off_s = max(args.inrush_off_ms / 1000.0, 0.0) + sample_s = max(args.inrush_sample_ms / 1000.0, 0.01) + interval_s = max(args.inrush_interval_ms / 1000.0, 0.0005) + power_cycle = not args.inrush_no_power_cycle + + if args.inrush_host_sample: + out = ci._measure_inrush_host( + c, + hi, + port_0, + off_s=off_s, + sample_s=sample_s, + interval_s=interval_s, + threshold_ma=args.inrush_threshold_ma, + power_cycle=power_cycle, + ) + else: + out = ci._measure_inrush_on_hub( + c, + hi, + port_0, + off_s=off_s, + sample_s=sample_s, + power_cycle=power_cycle, + store_index=args.inrush_map_store_index, + map_slot=args.inrush_map_slot, + pointer_index=args.inrush_pointer_index, + rearm_map=not args.inrush_no_rearm_map, + ) + + if out.get("error") == "rearm_map_failed": + print( + "check_concentrator --inrush: could not re-arm map (store slotDisable/Enable failed). " + "Load reflex/inrush.map into --inrush-map-store-index / --inrush-map-slot, " + "or try --inrush-no-rearm-map.", + file=sys.stderr, + flush=True, + ) + return 1 + if out.get("error") == "scratchpad_read_failed": + print( + "check_concentrator --inrush: scratchpad read failed (pointer offset / hub type). " + "Confirm inrush.map is loaded, mapEnable ran, and pointer index matches Reflex.", + file=sys.stderr, + flush=True, + ) + return 1 + + print(flush=True) + print("--- USB inrush (--inrush) ---", flush=True) + print(flush=True) + + if args.inrush_json: + print(json.dumps(out), flush=True) + else: + ptr_idx = args.inrush_pointer_index + if out.get("mode") == "on-hub": + print( + f"Hub {out['hub']} port {out['port']} serial {out['serial']}", + flush=True, + ) + print(f" mode: on-hub reflex (scratchpad via pointer {ptr_idx})", flush=True) + print( + f" peak current: {out['peak_ma']} mA (peak_ua={out['peak_ua']})", + flush=True, + ) + print( + f" hub ticks: {out['ticks']} @ {out['sample_period_us']} µs → " + f"~{out['observation_us']} µs on timer grid", + flush=True, + ) + print( + f" > {ci._REFLEX_THRESHOLD_MA} mA (reflex): ~{out['above_threshold_us']} µs " + f"({out['above_threshold_ticks']} ticks)", + flush=True, + ) + print( + f" host wait: {out['sample_window_ms']} ms power_cycled: {out['power_cycled']} " + f"store[{out['map_store_index']}] slot {out['map_slot']}", + flush=True, + ) + if out["ticks"] == 0 and out.get("sample_period_us", 0) == 0: + print( + " ! ticks and sample_period_us are 0 — map likely not running or wrong pointer.", + file=sys.stderr, + flush=True, + ) + else: + fat = out["first_above_threshold_ms"] + fat_s = f"{fat} ms" if fat is not None else "n/a" + print( + f"Hub {out['hub']} port {out['port']} serial {out['serial']}", + flush=True, + ) + print(" mode: host-sample (getPortCurrent loop)", flush=True) + print( + f" peak current: {out['peak_ma']} mA " + f"(when max first rose: {out['peak_time_ms']} ms)", + flush=True, + ) + print( + f" first > {out['above_threshold_ma']} mA: {fat_s}", + flush=True, + ) + print( + f" > {out['above_threshold_ma']} mA for ~{out['above_threshold_ms']} ms " + f"({out['sample_count']} samples @ {out['interval_ms']} ms; wall {out['elapsed_ms']} ms)", + flush=True, + ) + print( + f" nominal window: {out['sample_window_ms']} ms power_cycled: {out['power_cycled']}", + flush=True, + ) + print(flush=True) + return 0 + + def _print_consolidated_report(c: FiWiConcentrator) -> int: _print_ssh_and_hosts_summary() hub_rows, remote_rc = _build_consolidated_hub_rows(c) @@ -944,7 +1136,10 @@ def test_concentrator() -> None: def _parse_args() -> argparse.Namespace: p = argparse.ArgumentParser( - description="Check FiWiConcentrator: consolidated local + remote USB hub table.", + description=( + "FiWiConcentrator check: consolidated USB hub table, optional inrush probe, " + "or interactive panel calibrate (fiber_map.json — hub/port, chip_type, patch panel)." + ), formatter_class=argparse.RawDescriptionHelpFormatter, epilog=( "PROFILE selects /config/.ini (e.g. uax24, uax4, default).\n" @@ -966,9 +1161,184 @@ def _parse_args() -> argparse.Namespace: "(disrupts USB power)." ), ) + p.add_argument( + "--inrush", + nargs=2, + type=int, + metavar=("HUB", "PORT"), + help=( + "After the report (or after --powercycle), run USB inrush on this **local** hub " + "(1-based) and downstream port (0-based). Same behavior as tests/check_inrush.py." + ), + ) + ir = p.add_argument_group("inrush options (only with --inrush)") + ir.add_argument( + "--inrush-host-sample", + action="store_true", + help="Poll getPortCurrent on the host instead of on-hub Reflex scratchpad", + ) + ir.add_argument( + "--inrush-json", + action="store_true", + help="Print one JSON object for the inrush result", + ) + ir.add_argument( + "--inrush-off-ms", + type=float, + default=250.0, + help="ms downstream port off before re-enable when power-cycling (default 250)", + ) + ir.add_argument( + "--inrush-sample-ms", + type=float, + default=500.0, + help="Host wait after re-arm / power-on while hub samples (default 500)", + ) + ir.add_argument( + "--inrush-interval-ms", + type=float, + default=1.0, + help="(host-sample) ms between getPortCurrent reads (default 1)", + ) + ir.add_argument( + "--inrush-threshold-ma", + type=float, + default=50.0, + help="(host-sample) threshold mA for above-threshold tally (default 50)", + ) + ir.add_argument( + "--inrush-no-power-cycle", + action="store_true", + help="Do not disable/enable the downstream port before measure", + ) + ir.add_argument( + "--inrush-map-store-index", + type=int, + default=1, + help="stem.store index for slotDisable/Enable (default 1)", + ) + ir.add_argument( + "--inrush-map-slot", + type=int, + default=0, + help="Store slot where inrush.map is loaded (default 0)", + ) + ir.add_argument( + "--inrush-pointer-index", + type=int, + default=0, + help="stem.pointer index for Reflex scratchpad (default 0)", + ) + ir.add_argument( + "--inrush-no-rearm-map", + action="store_true", + help="Do not slotDisable/slotEnable before measure (on-hub mode)", + ) + p.add_argument( + "--panel-calibrate", + action="store_true", + help=( + "Interactive fiber_map.json build: patch panel slots, walk each USB downstream port " + "(one powered at a time), capture wlan+lspci for chip_type, map hub.port → panel id. " + "Same as 'python3 fiwi.py panel calibrate'. Requires a TTY; not with --powercycle/--inrush." + ), + ) + cal = p.add_argument_group("panel calibrate (only with --panel-calibrate)") + cal.add_argument( + "--calibrate-merge", + action="store_true", + help="Merge into existing fiber_map.json instead of clearing fiber_ports", + ) + cal.add_argument( + "--calibrate-limit", + type=int, + default=None, + metavar="N", + help="Stop after the first N USB calibrate steps (hub.port walks)", + ) + cal.add_argument( + "--calibrate-ssh", + action="append", + default=None, + metavar="USER@HOST", + help="Remote hub host for calibrate steps (repeat for several); also uses calibrate_remotes / env", + ) return p.parse_args() +def _calibrate_flags_without_panel(args: argparse.Namespace) -> bool: + if args.panel_calibrate: + return False + if args.calibrate_merge: + return True + if args.calibrate_limit is not None: + return True + if args.calibrate_ssh: + return True + return False + + +def _main_panel_calibrate(args: argparse.Namespace, label: str) -> int: + """Run :meth:`FiWiConcentrator.panel_calibrate` (writes ``fiber_map.json``).""" + if args.powercycle or args.inrush is not None: + print( + "check_concentrator: --panel-calibrate cannot be combined with --powercycle or --inrush.", + file=sys.stderr, + flush=True, + ) + return 2 + if not sys.stdin.isatty(): + print( + "check_concentrator: --panel-calibrate needs an interactive TTY (e.g. ssh -t host).", + file=sys.stderr, + flush=True, + ) + return 2 + + _ensure_paths_configured() + from fiwi.brainstem_loader import load_brainstem + + print(_rule("="), flush=True) + print(f"Fi-Wi panel calibrate → fiber_map.json [config: {label}]", flush=True) + print(_rule("="), flush=True) + print(flush=True) + print( + "Maps each USB hub downstream port to a patch-panel fiber id; records wlan snapshot " + "(sysfs + lspci/iw) as chip_type / wlan, optional PCIe metadata.\n" + "Keys: s / skip / . = skip port · q / quit / exit = stop · Ctrl-C = save fiber_map.json & exit.\n" + "Equivalent to: python3 fiwi.py panel calibrate " + + ("merge " if args.calibrate_merge else "") + + (f"{args.calibrate_limit} " if args.calibrate_limit is not None else "") + + " ".join(f"--ssh {h}" for h in (args.calibrate_ssh or [])) + + "\n", + flush=True, + ) + + c = None + try: + load_brainstem() + c = _instantiate_concentrator() + c.panel_calibrate( + merge=args.calibrate_merge, + limit=args.calibrate_limit, + calibrate_ssh_hosts=list(args.calibrate_ssh or []), + ) + except Exception as exc: + print(f"FAIL: {exc}", file=sys.stderr, flush=True) + return 1 + finally: + if c is not None: + try: + c.disconnect() + except Exception: + pass + + print(_rule("="), flush=True) + print(f"Panel calibrate finished [config: {label}]", flush=True) + print(_rule("="), flush=True) + return 0 + + def main() -> int: try: os.chdir(_ROOT) @@ -980,6 +1350,19 @@ def main() -> int: os.environ["FIWI_CONFIG"] = args.config.strip() label = os.environ.get("FIWI_CONFIG", "default (config/default.ini if present)") + + if _calibrate_flags_without_panel(args): + print( + "check_concentrator: --calibrate-merge / --calibrate-limit / --calibrate-ssh " + "require --panel-calibrate.", + file=sys.stderr, + flush=True, + ) + return 2 + + if args.panel_calibrate: + return _main_panel_calibrate(args, label) + _print_banner(label) c = None @@ -995,6 +1378,8 @@ def main() -> int: remote_fail = _print_per_port_power_table(c, hub_rows, remote_fail) else: remote_fail = _print_consolidated_report(c) + if args.inrush is not None: + remote_fail = remote_fail or _run_inrush_probe(c, args) except AssertionError as exc: print(f"FAIL: {exc}", file=sys.stderr) return 1