"""Fi-Wi framework CLI: argv dispatch, --ssh, fiber-map forwarding.""" import json import os import sys from fiwi.harness import FiWiHarness from fiwi.brainstem_loader import load_brainstem from fiwi.ssh_dispatch import dispatch_fiber_mapped_ssh_if_needed from fiwi.ssh_node import SshNode from fiwi import usb_probe as usb def _strip_async_from_argv(argv: list[str]) -> list[str]: """``--async`` → ``FIWI_REMOTE_DEFER=1`` (deferred remote calls return handles / Tasks).""" out = [] for a in argv: if a == "--async": os.environ["FIWI_REMOTE_DEFER"] = "1" continue out.append(a) return out def _parse_panel_calibrate_argv(args): """ ``panel calibrate [merge] [N] [--ssh user@host] …`` Returns ``(merge, limit, calibrate_ssh_hosts)``. """ merge = False limit = None hosts = [] i = 0 while i < len(args): a = args[i] low = a.lower() if low == "merge": merge = True i += 1 continue if low == "--ssh": if i + 1 >= len(args): print("panel calibrate: --ssh requires user@host", file=sys.stderr, flush=True) sys.exit(2) hosts.append(args[i + 1].strip()) i += 2 continue if a.isdigit(): limit = int(a) i += 1 continue print(f"panel calibrate: unknown argument {a!r}", file=sys.stderr, flush=True) sys.exit(2) return merge, limit, hosts def main() -> int: sys.argv[:] = [sys.argv[0]] + _strip_async_from_argv(sys.argv[1:]) argv = sys.argv[1:] if len(argv) >= 2 and argv[0] in ("--ssh", "--remote"): remote_host = argv[1] rest = argv[2:] if not rest: print( "Usage: fiwi.py --ssh user@host [args...]\n" " Example: fiwi.py --ssh pi@192.168.1.39 discover\n" " If brainstem is in a Pi venv: copy remote_ssh.env.example → remote_ssh.env next to\n" " this script on the PC where you run --ssh (paths in the file are on the Pi).\n" " Or export FIWI_REMOTE_PYTHON / FIWI_REMOTE_SCRIPT.\n" " On the Pi: pip install -r requirements.txt in that venv; udev 24ff.", file=sys.stderr, flush=True, ) return 2 return SshNode.parse(remote_host).invoke(rest, defer=False) rc_ssh_map = dispatch_fiber_mapped_ssh_if_needed(argv) if rc_ssh_map is not None: return rc_ssh_map os.write(2, b"fiwi: start\n") try: load_brainstem() except Exception as exc: print(f"fiwi: failed to import brainstem: {exc}", file=sys.stderr, flush=True) if isinstance(exc, ImportError): print( " If this text came from `fiwi.py --ssh …`: the remote used system python3 by default.\n" " On your PC export FIWI_REMOTE_PYTHON to the Pi venv’s python and\n" " FIWI_REMOTE_SCRIPT to that fiwi.py (absolute paths on the Pi).", file=sys.stderr, flush=True, ) return 1 harness = FiWiHarness() try: cmd = sys.argv[1].lower() if len(sys.argv) > 1 else "status" target = sys.argv[2] if len(sys.argv) > 2 else "all" if cmd == "status": harness.status(target) elif cmd == "calibrate-ports-json": if not harness.hubs and not harness.connect(): print("[]", flush=True) else: pairs = harness._ordered_downstream_ports() print(json.dumps([[h, p] for h, p in pairs]), flush=True) elif cmd == "lsusb-lines-json": print(json.dumps(usb.lsusb_lines()), flush=True) elif cmd == "wlan-info-json": from fiwi.ieee80211_dev import discover_wireless_for_map print(json.dumps(discover_wireless_for_map()), flush=True) elif cmd == "discover": harness.discover() elif cmd == "power": if len(sys.argv) < 5 or sys.argv[2].lower() != "fiber-port": print( "Usage: fiwi.py power fiber-port on|off\n" " Uses fiber_map.json; per-entry ssh / host+user forwards to that host (see help).", file=sys.stderr, flush=True, ) return 2 try: fp_n = int(sys.argv[3]) except ValueError: print("fiber_port_id must be an integer.", file=sys.stderr, flush=True) return 2 mode = sys.argv[4].lower() if mode not in ("on", "off"): print("Last argument must be on or off.", file=sys.stderr, flush=True) return 2 harness.fiber_power(mode, fp_n) elif cmd == "fiber": if len(sys.argv) < 3: print( "Usage: fiwi.py fiber status\n" " fiwi.py fiber chip [save]\n" " status — hub.port, Route, power, and saved previews from fiber_map.json\n" " chip — local lsusb diff only (not used for SSH-mapped / PCIe-fiber paths)", file=sys.stderr, flush=True, ) return 2 sub = sys.argv[2].lower() if sub == "status": harness.fiber_map_status() elif sub == "chip": if len(sys.argv) < 4: print( "Usage: fiwi.py fiber chip [save]", file=sys.stderr, flush=True, ) return 2 try: chip_fp = int(sys.argv[3]) except ValueError: print("fiber_port_id must be an integer.", file=sys.stderr, flush=True) return 2 save_chip = len(sys.argv) >= 5 and sys.argv[4].lower() == "save" harness.fiber_chip(chip_fp, save=save_chip) else: print(f"Unknown fiber subcommand: {sub!r}", file=sys.stderr, flush=True) return 2 elif cmd == "panel": if len(sys.argv) < 3: print( "Usage: fiwi.py panel status\n" " fiwi.py panel on|off \n" " fiwi.py panel reboot|reboot-force \n" " 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" " 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" " Preset: fiber_map.rpi20.json → fiber_map.json for 8+8+4 → fiber ports 1–20.", file=sys.stderr, flush=True, ) return 2 sub = sys.argv[2].lower() if sub == "status": harness.panel_status() elif sub == "calibrate": args = sys.argv[3:] merge, limit, cal_hosts = _parse_panel_calibrate_argv(args) harness.panel_calibrate(merge=merge, limit=limit, calibrate_ssh_hosts=cal_hosts) elif sub in ("on", "off"): if len(sys.argv) < 4: print( f"Usage: fiwi.py panel {sub} <1-N> (N = patch_panel.slots in fiber_map, default 24)", file=sys.stderr, flush=True, ) return 2 harness.panel_power(sub, int(sys.argv[3])) elif sub == "reboot": if len(sys.argv) < 4: print( "Usage: fiwi.py panel reboot <1-N> (N from fiber_map patch_panel.slots)", file=sys.stderr, flush=True, ) return 2 harness.panel_reboot(int(sys.argv[3]), skip_empty=True) elif sub == "reboot-force": if len(sys.argv) < 4: print( "Usage: fiwi.py panel reboot-force <1-N> (N from fiber_map patch_panel.slots)", file=sys.stderr, flush=True, ) return 2 harness.panel_reboot(int(sys.argv[3]), skip_empty=False) else: print(f"Unknown panel subcommand: {sub!r}", file=sys.stderr, flush=True) return 2 elif cmd in ("on", "off"): if not harness.power(cmd, target): return 1 elif cmd in ("reboot", "reboot-force"): harness.reboot(target, skip_empty=(cmd == "reboot")) elif cmd == "setup": harness.setup_udev() elif cmd == "verify": harness.verify() elif cmd in ("help", "-h", "--help"): print( "Fi-Wi test framework — CLI\n" "Usage: fiwi.py [--async] [target]\n" " --async set FIWI_REMOTE_DEFER: deferred calls spawn ssh child processes immediately;\n" " panel calibrate overlaps them (join via handle.result(); no Python threads).\n" " Or set FIWI_REMOTE_DEFER=1 / remote_ssh.env (see remote_ssh.env.example).\n" " discover — list hubs (serial, port count); no port I/O\n" " status [target] — default command; target like all, 1.3, all.2\n" " fiber status — fiber_ports + power (local or per-entry ssh / host+user)\n" " fiber chip [save] — local lsusb probe (SSH-mapped ports: wlan/pcie from calibrate, not fiber chip)\n" " wlan-info-json — machine-readable wireless NIC snapshot (sysfs + lspci/iw); used by panel calibrate over SSH\n" " power fiber-port on|off — power by fiber key (ssh forward if map says so)\n" " panel status — rack positions 1…N (N = patch_panel.slots, default 24)\n" " panel calibrate … — set patch panel size first, then USB hub walk → fiber_map.json\n" " panel on|off|reboot|reboot-force \n" " on|off [target] reboot|reboot-force [target] setup verify\n" "\n" "Remote (hubs on another host — no local brainstem needed):\n" " fiwi.py --ssh user@host discover\n" " remote_ssh.env next to fiwi.py (see remote_ssh.env.example) or env vars:\n" " FIWI_REMOTE_PYTHON remote interpreter (default python3)\n" " FIWI_REMOTE_SCRIPT remote script path (default /usr/local/bin/fiwi.py)\n" " FIWI_SSH_OPTS e.g. '-o BatchMode=yes'\n" " FIWI_CALIBRATE_REMOTES optional comma-separated user@host for panel calibrate (no --ssh needed)\n" " Pi: venv + requirements.txt; udev 24ff.\n" "\n" "fiber_map.json fiber_ports entries may set ssh routing (hubs on another machine):\n" ' "ssh": "user@host" or "remote": "…" or "host": "ip", "user": "pi"\n' " On the SSH destination, the same fiber id should be local (omit ssh) so commands are not re-forwarded.\n" ' Optional "pcie": { bus, switch, slot, adapter_port, sfp_serial, board_serial, … } — calibrate can fill via 1–6+SFP.\n' "\n" "Hybrid calibrate: put {\"calibrate_remotes\": [\"pi@ip\"]} in fiber_map.json or pass --ssh per host;\n" " order is all local downstream ports, then each remote’s ports (see calibrate-ports-json on the Pi)." ) else: print(f"Unknown command: {cmd!r}", file=sys.stderr, flush=True) print( "Try: --ssh user@host … | discover | calibrate-ports-json | wlan-info-json | status | fiber | power | panel | … | help", file=sys.stderr, flush=True, ) return 2 finally: harness.disconnect() return 0