Panel calibrate UX, check_concentrator extras, power summary AC hint

- Panel calibrate: Ctrl-C saves map during patch panel; skip/quit aliases (s/skip/., q/quit/exit); clearer prompts and help.
- PCIe prompt: s/skip keeps metadata; note Ctrl-C in calibrate.
- check_concentrator: --panel-calibrate, --inrush and options; compact Power line with 120V/220V AC amp hint from total W.
- docs/brainstem-sdk.md: find How_To_Build.txt examples.

Made-with: Cursor
This commit is contained in:
Robert McMahon 2026-04-03 16:33:44 -07:00
parent a488dc2ed0
commit 93cd346a07
5 changed files with 417 additions and 11 deletions

View File

@ -36,7 +36,13 @@ That installs udev rules (typically requires `sudo`) and adds the current user t
## Build C/C++ examples from source ## 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 Acronames steps. In general: Sample projects live under `api/examples/c_cpp/`. Each example includes `How_To_Build.txt` or `How_to_Build.txt` with Acronames 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. 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). 2. Copy the **`lib`** directory from the SDK into the example tree where the readme says (headers and `BrainStem2` shared library).

View File

@ -321,6 +321,7 @@ def main() -> int:
" fiwi.py panel calibrate [merge] [<N>] [--ssh user@host] …\n" " fiwi.py panel calibrate [merge] [<N>] [--ssh user@host] …\n"
" calibrate: local hub ports first, then each --ssh host, calibrate_remotes in JSON, and/or\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" " 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" " 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 <n> is 1…slots.\n" " Calibrate starts by setting patch_panel.slots (front-panel positions); panel <n> is 1…slots.\n"
" Use power fiber-port for arbitrary fiber ids beyond the panel if needed.\n" " Use power fiber-port for arbitrary fiber ids beyond the panel if needed.\n"

View File

@ -937,7 +937,8 @@ class FiWiConcentrator:
have = PatchPanel.from_map_blob(doc.get("patch_panel")) have = PatchPanel.from_map_blob(doc.get("patch_panel"))
print( print(
"\n--- Patch panel (front-panel positions) ---\n" "\n--- Patch panel (front-panel positions) ---\n"
f"Fiber map keys 1…N refer to these panel positions (power/status: panel <N>).\n", f"Fiber map keys 1…N refer to these panel positions (power/status: panel <N>).\n"
"Ctrl-C saves fiber_map.json and exits (patch panel may be unchanged).\n",
flush=True, flush=True,
) )
if have is not None: if have is not None:
@ -1018,7 +1019,16 @@ class FiWiConcentrator:
doc = {"fiber_ports": {}} doc = {"fiber_ports": {}}
doc = fm.ensure_fiber_map_document(doc) doc = fm.ensure_fiber_map_document(doc)
try:
self._prompt_patch_panel(doc) 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) self._write_fiber_map_document(doc)
seen_h = set() seen_h = set()
@ -1208,8 +1218,10 @@ class FiWiConcentrator:
"After ON (~2s): fiwi snapshots wireless interfaces on that host (sysfs + lspci/iw) — " "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 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" "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" "Each step: ON → wlan snapshot → fiber id — "
"Port stays ON through optional PCIe prompts, then powers OFF. Ctrl-C anytime saves fiber_map.json and exits.\n" "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" "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: 16 = known Adnacom H3 card, then SFP 14 " "After each fiber id you can pick PCIe by number: 16 = known Adnacom H3 card, then SFP 14 "
"(no paste), or m=manual / c=clear / Enter=keep. Edit fiber_map.json anytime (see example).", "(no paste), or m=manual / c=clear / Enter=keep. Edit fiber_map.json anytime (see example).",
@ -1342,7 +1354,7 @@ class FiWiConcentrator:
) )
try: try:
line = input( 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() ).strip()
except EOFError: except EOFError:
print( print(
@ -1361,11 +1373,11 @@ class FiWiConcentrator:
raise SystemExit(130) raise SystemExit(130)
low = line.lower() low = line.lower()
if low == "q": if low in ("q", "quit", "exit"):
if step_powered: if step_powered:
self._calibrate_step_power_off(ssh_host, hub_1, port_0) self._calibrate_step_power_off(ssh_host, hub_1, port_0)
break break
if low == "s" or not line: if low in ("s", "skip", ".") or not line:
if step_powered: if step_powered:
self._calibrate_step_power_off(ssh_host, hub_1, port_0) self._calibrate_step_power_off(ssh_host, hub_1, port_0)
continue continue

View File

@ -305,10 +305,12 @@ def prompt_pcie_metadata_for_calibrate(existing_pcie):
print_catalog_menu() print_catalog_menu()
try: try:
r = input( r = input(
" PCIe? [Enter=keep, 16=card from list, m=manual, c=clear]: " " PCIe? [Enter/s/skip=keep, 16=card, m=manual, c=clear, Ctrl-C=save & exit calibrate]: "
).strip().lower() ).strip().lower()
except EOFError: except EOFError:
return ("keep", None) return ("keep", None)
if r in ("s", "skip"):
return ("keep", None)
if r in ("c", "clear"): if r in ("c", "clear"):
return ("clear", None) return ("clear", None)
if r in ("m", "manual"): if r in ("m", "manual"):

View File

@ -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
python tests/check_concentrator.py --config uax24 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``). ``--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), ``FIWI_CALIBRATE_REMOTES`` / merged hub hosts: all ports OFF (verify), then all ON (verify),
then prints the port power table (snapshot after the test). 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:: With pytest::
FIWI_CONFIG=uax24 pytest tests/check_concentrator.py FIWI_CONFIG=uax24 pytest tests/check_concentrator.py
@ -850,8 +864,15 @@ def _print_per_port_power_table(
lines_out.append((st, line)) lines_out.append((st, line))
if n_ma and n_power: if n_ma and n_power:
# Literal "120V/220V=…A/…A" suffix (labels, not f-string slash parsing).
summary = ( 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: elif n_ma:
summary = ( summary = (
@ -928,6 +949,177 @@ def _print_pcie_catalog_section() -> None:
print(flush=True) 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: def _print_consolidated_report(c: FiWiConcentrator) -> int:
_print_ssh_and_hosts_summary() _print_ssh_and_hosts_summary()
hub_rows, remote_rc = _build_consolidated_hub_rows(c) hub_rows, remote_rc = _build_consolidated_hub_rows(c)
@ -944,7 +1136,10 @@ def test_concentrator() -> None:
def _parse_args() -> argparse.Namespace: def _parse_args() -> argparse.Namespace:
p = argparse.ArgumentParser( 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, formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=( epilog=(
"PROFILE selects <repo>/config/<PROFILE>.ini (e.g. uax24, uax4, default).\n" "PROFILE selects <repo>/config/<PROFILE>.ini (e.g. uax24, uax4, default).\n"
@ -966,9 +1161,184 @@ def _parse_args() -> argparse.Namespace:
"(disrupts USB power)." "(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() 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: def main() -> int:
try: try:
os.chdir(_ROOT) os.chdir(_ROOT)
@ -980,6 +1350,19 @@ def main() -> int:
os.environ["FIWI_CONFIG"] = args.config.strip() os.environ["FIWI_CONFIG"] = args.config.strip()
label = os.environ.get("FIWI_CONFIG", "default (config/default.ini if present)") 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) _print_banner(label)
c = None c = None
@ -995,6 +1378,8 @@ def main() -> int:
remote_fail = _print_per_port_power_table(c, hub_rows, remote_fail) remote_fail = _print_per_port_power_table(c, hub_rows, remote_fail)
else: else:
remote_fail = _print_consolidated_report(c) 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: except AssertionError as exc:
print(f"FAIL: {exc}", file=sys.stderr) print(f"FAIL: {exc}", file=sys.stderr)
return 1 return 1