From 3ea65d92712fe8a86fb98d175ad3be00d555545e Mon Sep 17 00:00:00 2001 From: Robert McMahon Date: Fri, 3 Apr 2026 14:54:36 -0700 Subject: [PATCH] check_concentrator: panel order, Power totals line; README + post-commit hook. - Port table: sort by panel; Power: Total W / mA (per port follows) as sole header; dash alignment; fiber_map panel column unchanged. - Add README.md with git hook setup; githooks/post-commit (opt-in push + remote pull); scripts/install-git-hooks.sh sets core.hooksPath. Made-with: Cursor --- README.md | 45 +++++++++++++++++ githooks/post-commit | 46 ++++++++++++++++++ scripts/install-git-hooks.sh | 15 ++++++ tests/check_concentrator.py | 93 ++++++++++++++++++++++++++---------- 4 files changed, 173 insertions(+), 26 deletions(-) create mode 100644 README.md create mode 100755 githooks/post-commit create mode 100755 scripts/install-git-hooks.sh diff --git a/README.md b/README.md new file mode 100644 index 0000000..a79b46f --- /dev/null +++ b/README.md @@ -0,0 +1,45 @@ +# FiWiManager + +Fi-Wi USB power-control hubs, `fiber_map.json`, SSH remotes, and related tooling. Deeper detail lives under [`docs/`](docs/) (for example [`docs/fiwi-cli.md`](docs/fiwi-cli.md) and [`docs/fiwi-design.md`](docs/fiwi-design.md)). + +## Git hooks: post-commit push and remote pull + +You can use a **post-commit** hook that, after each commit, runs **`git push`** for the current branch to **`origin`**, then **SSH** to another machine (e.g. a lab Pi) and runs **`git pull`** in a clone there. + +### Install once per clone + +```bash +./scripts/install-git-hooks.sh +``` + +That sets **`core.hooksPath`** to **`githooks/`** so Git uses the tracked hook in this repository. + +### Enable (opt-in) + +The hook **does nothing** unless you set: + +```bash +export FIWI_POST_COMMIT_SYNC=1 +export FIWI_POST_COMMIT_REMOTE='user@host' +export FIWI_POST_COMMIT_REMOTE_PATH='~/Code/FiWiManager' # optional; default shown +``` + +Put those in your shell profile if you want them every session. + +| Variable | Meaning | +|----------|---------| +| `FIWI_POST_COMMIT_SYNC` | Must be `1` for the hook to run. | +| `FIWI_POST_COMMIT_REMOTE` | SSH target for `git pull` (e.g. `rjmcmahon@192.168.1.39`). **Required** when `SYNC=1`; if empty, the hook skips **both** push and pull and prints a message. | +| `FIWI_POST_COMMIT_REMOTE_PATH` | Directory of the FiWiManager clone on that host. Paths starting with `~/` are turned into `$HOME/…` on the remote. | + +**Skip once:** `FIWI_POST_COMMIT_SYNC=0 git commit …` + +**SSH:** the hook uses `ssh -o BatchMode=yes`, so it expects non-interactive auth (keys). + +**Disable** hooks from this repo’s `githooks/` directory: + +```bash +git config --unset core.hooksPath +``` + +See also comments at the top of [`githooks/post-commit`](githooks/post-commit). diff --git a/githooks/post-commit b/githooks/post-commit new file mode 100755 index 0000000..ad2e75d --- /dev/null +++ b/githooks/post-commit @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# Post-commit hook: push this repo, then optionally `git pull` on another machine over SSH. +# +# Opt-in (all must be set for the hook to do anything): +# export FIWI_POST_COMMIT_SYNC=1 +# export FIWI_POST_COMMIT_REMOTE='user@host' # SSH target for the Pi / lab box +# export FIWI_POST_COMMIT_REMOTE_PATH='~/Code/FiWiManager' # repo root on that host +# +# Skip once: FIWI_POST_COMMIT_SYNC=0 git commit ... +# Disable hook: git config --unset core.hooksPath (or point hooksPath elsewhere) +# +# One-time setup in this clone: +# ./scripts/install-git-hooks.sh + +set -euo pipefail + +if [[ "${FIWI_POST_COMMIT_SYNC:-}" != "1" ]]; then + exit 0 +fi + +if [[ -z "${FIWI_POST_COMMIT_REMOTE:-}" ]]; then + echo "post-commit: FIWI_POST_COMMIT_SYNC=1 but FIWI_POST_COMMIT_REMOTE is empty; skipping." >&2 + exit 0 +fi + +root=$(git rev-parse --show-toplevel) +cd "$root" + +branch=$(git rev-parse --abbrev-ref HEAD) +if [[ "$branch" == "HEAD" ]]; then + echo "post-commit: detached HEAD; skipping push." >&2 + exit 0 +fi + +echo "post-commit: git push origin $branch" >&2 +git push origin "$branch" + +rpath=${FIWI_POST_COMMIT_REMOTE_PATH:-~/Code/FiWiManager} +echo "post-commit: ssh $FIWI_POST_COMMIT_REMOTE git pull in $rpath" >&2 +if [[ "$rpath" == ~/* ]]; then + _tail=${rpath#~/} + ssh -o BatchMode=yes "$FIWI_POST_COMMIT_REMOTE" bash -lc "cd \"\$HOME/$_tail\" && git pull" +else + _q=$(printf '%q' "$rpath") + ssh -o BatchMode=yes "$FIWI_POST_COMMIT_REMOTE" bash -lc "cd ${_q} && git pull" +fi diff --git a/scripts/install-git-hooks.sh b/scripts/install-git-hooks.sh new file mode 100755 index 0000000..455bdd9 --- /dev/null +++ b/scripts/install-git-hooks.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +# Point this repository at tracked hooks under githooks/ (post-commit push + optional remote pull). +set -euo pipefail +root=$(git rev-parse --show-toplevel 2>/dev/null) || { + echo "Run from inside a FiWiManager git clone." >&2 + exit 1 +} +cd "$root" +git config core.hooksPath githooks +echo "Set core.hooksPath=githooks in this repo." +echo "" +echo "To sync after each commit (when FIWI_POST_COMMIT_SYNC=1), add to your shell profile:" +echo " export FIWI_POST_COMMIT_SYNC=1" +echo " export FIWI_POST_COMMIT_REMOTE='user@host'" +echo " export FIWI_POST_COMMIT_REMOTE_PATH='~/Code/FiWiManager'" diff --git a/tests/check_concentrator.py b/tests/check_concentrator.py index 6dfff27..c806f9f 100644 --- a/tests/check_concentrator.py +++ b/tests/check_concentrator.py @@ -4,8 +4,10 @@ Smoke check: :class:`fiwi.concentrator.FiWiConcentrator` plus consolidated USB h metrics tables covering **this machine** and each configured **remote**. The consolidated hub table has **Location** (``local`` or remote IP/hostname, plus **tty** names when known) and **USB** (Bus/Device, VID:PID, product from sysfs). The per-port table starts with **Panel** (``fiber_ports`` -key / rack position when ``hub``, ``port``, and optional ``ssh`` match this run), then power metrics -and **Location** (no USB column). +key / rack position when ``hub``, ``port``, and optional ``ssh`` match this run), rows sorted by panel +number; a **Power: Total … W / … mA (per port follows)** line heads the section, then the column header +and rows. +(no USB column). Adnacom PCIe catalog last. Requires BrainStem locally for hub enumeration on this host. Remotes use SSH ``show_hostcards`` @@ -20,7 +22,7 @@ and ``port-metrics-json``; the remote tree must include that command (same revis ``--powercycle`` runs a destructive self-test on **local** USB hubs and each host in ``FIWI_CALIBRATE_REMOTES`` / merged hub hosts: all ports OFF (verify), then all ON (verify), -then prints the **per-port power** table (snapshot after the test). +then prints the port power table (snapshot after the test). With pytest:: @@ -263,6 +265,23 @@ def _panel_cell( return lookup.get((sk, h1, port_0), "—") +def _panel_sort_tuple(pnl: str, hub_table_idx: int, port_0: int) -> tuple: + """ + Sort per-port rows by patch panel label: numeric ``fiber_ports`` keys first (min if comma-list), + then other labels, unmapped (—) last; tie-break hub # and port. + """ + s = (pnl or "").strip() + if not s or s == "—": + return (2, 0, hub_table_idx, port_0) + nums: list[int] = [] + for part in re.split(r"[\s,]+", s): + if part.isdigit(): + nums.append(int(part)) + if nums: + return (0, min(nums), hub_table_idx, port_0) + return (1, s, hub_table_idx, port_0) + + def _merged_hub_hosts() -> list[str]: from fiwi.ssh import SshNodeConfig @@ -672,17 +691,14 @@ def _print_per_port_power_table( c: FiWiConcentrator, hub_rows: list[ConsolidatedHubRow], base_rc: int, - *, - title: str | None = None, ) -> int: """ - Per-port power, current (mA), voltage (V), and Location — same hub order as consolidated rows. + ``Power: Total …`` line, then per-port current (mA), voltage (V), and Location (sorted by panel). Returns ``base_rc`` OR’d with failure if any ``port-metrics-json`` remote call fails. """ rc = base_rc - section_title = title or "Per-port power (consolidated hub order)" if not hub_rows: - print(" (no hubs — skipping per-port power table.)", flush=True) + print(" (no hubs — skipping port power table.)", flush=True) print(flush=True) return rc @@ -707,11 +723,12 @@ def _print_per_port_power_table( remote_cache[ssh_target] = {} return remote_cache[ssh_target] - print(section_title, flush=True) - print("-" * len(_PER_PORT_PWR_HDR), flush=True) - print(_PER_PORT_PWR_HDR, flush=True) - print("-" * len(_PER_PORT_PWR_HDR), flush=True) - + # Collect rows; sum current (mA) and power (W) where metrics allow. + lines_out: list[tuple[tuple, str]] = [] + total_ma = 0.0 + n_ma = 0 + total_power_w = 0.0 + n_power = 0 for row in hub_rows: sn_key = _norm_hub_serial(row.serial) if row.ssh_target is None: @@ -721,6 +738,8 @@ def _print_per_port_power_table( for port in range(row.n_ports): m = by_port.get(port) + cur_f: float | None = None + v_f: float | None = None if m is None: pwr, ma_s, v_s = "?", "—", "—" else: @@ -730,7 +749,10 @@ def _print_per_port_power_table( ma_s = "—" else: try: - ma_s = f"{float(cur):.2f}" + cur_f = float(cur) + ma_s = f"{cur_f:.2f}" + total_ma += cur_f + n_ma += 1 except (TypeError, ValueError): ma_s = "—" vv = m.get("voltage_v") @@ -738,15 +760,39 @@ def _print_per_port_power_table( v_s = "—" else: try: - v_s = f"{float(vv):.3f}" + v_f = float(vv) + v_s = f"{v_f:.3f}" except (TypeError, ValueError): v_s = "—" + if cur_f is not None and v_f is not None: + total_power_w += v_f * (cur_f / 1000.0) + n_power += 1 pnl = _panel_cell(panel_lookup, hub_rows, row, port) - print( + st = _panel_sort_tuple(pnl, row.idx, port) + line = ( f"{pnl:<8} | {row.idx:<4} | {row.serial:<12} | {port:<3} | {pwr:<5} | " - f"{ma_s:>8} | {v_s:>8} | {_location_cell(row)}", - flush=True, + f"{ma_s:>8} | {v_s:>8} | {_location_cell(row)}" ) + lines_out.append((st, line)) + + if n_ma and n_power: + summary = ( + f"Power: Total {total_power_w:.3f} W / {total_ma:.2f} mA (per port follows)" + ) + elif n_ma: + summary = f"Power: Total — W / {total_ma:.2f} mA (per port follows)" + else: + summary = "Power: — (per port follows)" + + print(summary, flush=True) + print("-" * len(_PER_PORT_PWR_HDR), flush=True) + + print(_PER_PORT_PWR_HDR, flush=True) + print("-" * len(_PER_PORT_PWR_HDR), flush=True) + + lines_out.sort(key=lambda x: x[0]) + for _sk, line in lines_out: + print(line, flush=True) print(flush=True) @@ -766,7 +812,7 @@ def _print_per_port_power_table( def _print_usb_hub_tables(c: FiWiConcentrator, hub_rows: list[ConsolidatedHubRow], base_rc: int) -> int: - """Print consolidated hub table and per-port power / current / voltage table. Returns updated rc.""" + """Print consolidated hub table and port power / current / voltage table. Returns updated rc.""" rc = base_rc print("USB power-control hubs (consolidated)", flush=True) print("-" * len(_CONSOLIDATED_HUB_HDR), flush=True) @@ -838,7 +884,7 @@ def _parse_args() -> argparse.Namespace: action="store_true", help=( "Self-test: all downstream ports OFF then ON on local hubs and each merged " - "remote hub host; verifies via port metrics; then print per-port power table " + "remote hub host; verifies via port metrics; then print port power table " "(disrupts USB power)." ), ) @@ -868,12 +914,7 @@ def main() -> int: hub_rows, brc = _build_consolidated_hub_rows(c) remote_fail = remote_fail or brc hub_rows = _enrich_hub_rows_usb(hub_rows) - remote_fail = _print_per_port_power_table( - c, - hub_rows, - remote_fail, - title="Per-port power (after power-cycle test)", - ) + remote_fail = _print_per_port_power_table(c, hub_rows, remote_fail) else: remote_fail = _print_consolidated_report(c) except AssertionError as exc: