FiWiControl/docs/power-control-and-inventory.md

14 KiB
Raw Blame History

Power control, discovery, and INI inventory (FiWiControl)

Packages: fiwicontrol.lab (discovery + INI inventory), fiwicontrol.power (Power, AcronamePower, MonsoonPower, CLI wrappers).
Code: src/fiwicontrol/lab/, src/fiwicontrol/power/.

This document describes what must be installed on remote rigs (e.g. a Raspberry Pi 5) when you use remote discovery or --verify-inventory with mode = remote in your INI. It also explains how to edit the lab INI and how to verify a new install end-to-end.


Lab INI file reference (configs/*.ini)

Power inventory files are normal INI files (Python configparser). FiWiControl reads [site], every [node.<id>], and every [inventory.host.<name>]. Other sections or keys are ignored unless you add tooling that reads them.

Copy configs/clubhouse.ini as a template, or start from the minimal skeleton at the end of this section.

[site] (optional)

Key Meaning
name Human-readable site label (printed by --verify-inventory).

[node.<id>] — SSH targets for mode = remote

<id> is the section suffix (e.g. raspberry_pi5 in [node.raspberry_pi5]). Remote inventory rows reference this id with node = <id>.

Key Required Meaning
ipaddr yes Address passed to ssh_node (passwordless root@<ipaddr>). Use 127.0.0.1 only for documentation symmetry; mode = local does not use SSH.
sshtype no (default ssh) Must be ssh for standard OpenSSH automation.
label, description no Comments for humans; not used by discovery.

Every remote [inventory.host.*] must set node = <id> where <id> matches a [node.<id>] section.

[inventory.host.<name>] — expected USB lab devices

<name> is a short label (e.g. localhost, pi5). Each section describes one machine whose live discovery is compared to your expectations.

Key Required Meaning
mode no (default local) local — run python3 -m fiwicontrol.power --discovery-json on the workstation that runs the command. remote — run the same over SSH using [node.<id>].
node required if mode = remote Must match [node.<id>] section id (suffix after node.).
acroname no (default empty) Comma-separated list of StemClass:downstream_usb_ports. Each entry is one expected Acroname / BrainStem module (by stem class and downstream USB port count). Order does not matter; duplicates are allowed (multiset match vs discovery). Example: USBHub2x4:4, USBHub3p:8.
monsoon_count no (default 0) Exact number of Monsoon-class USB serial devices discovery must find on that host.

Finding stem names and port counts: run python3 -m fiwicontrol.power --discovery-json on the host (or python3 -m fiwicontrol.power --list-power-devices with -c pointing at your draft INI) and read each Acroname rows stem_class and downstream_usb_ports. Those strings and integers must match your acroname entries.

Empty hardware: acroname may be left blank and monsoon_count = 0 if that host has no power/USB inventory to track.

Minimal example

[site]
name = MyLab

[node.pi]
ipaddr = 192.168.1.50
sshtype = ssh

[inventory.host.workstation]
mode = local
acroname =
monsoon_count = 0

[inventory.host.pi]
mode = remote
node = pi
acroname = USBHub2x4:4
monsoon_count = 0

At least one [inventory.host.*] section is required; otherwise loading the INI fails.


What has to run on the remote (e.g. Raspberry Pi 5)

Inventory verification and remote discovery call SSH like:

python3 -m fiwicontrol.power --discovery-json

on the remote host. That requires:

  1. Python 3.11+ on the Pi (same as this repo).
  2. fiwicontrol importable on the Pi — typically:
    • editable install from a checkout:
      cd /path/to/FiWiControl && python3 -m pip install -e ".[power]"
    • or install your wheel / package however you deploy, as long as
      python3 -c "import fiwicontrol.power" succeeds.
  3. Optional extras for hardware discovery (same as operator workstation):
    • brainstem — Acroname / BrainStem USB enumeration.
    • pyserial — Monsoon (and similar) USB-serial enumeration. Install with pip install 'fiwicontrol[power]' or pip install -e ".[power]" from the repo root.
  4. Non-interactive SSH — automation uses root@<host> with passwordless keys (see docs/node-control-asyncio-design.md).

If fiwicontrol is only on PYTHONPATH (no install), ensure the remote shell used for SSH has that path set (e.g. in ~/.profile / systemd unit), or prefer pip install -e . on the Pi so python3 -m fiwicontrol.power resolves reliably.

First-time provisioning (clone on the Pi, pip install -e '.[power]' from your workstation over SSH, PEP 668, smoke tests): see docs/install.md“Bring up systems” and “Remote host setup”.

List attached power/USB devices on every inventory host (this machine + each mode=remote SSH target; addresses in each section header):

python3 -m fiwicontrol.power --list-power-devices
# or: python3 -m fiwicontrol.power -c configs/clubhouse.ini --list-power-devices

Sanity on the Pi (after install):

python3 -c "import fiwicontrol.power, brainstem, serial; print('imports ok')"
python3 -m fiwicontrol.power --discovery-json

The second command should print JSON with top-level keys acroname and monsoon (possibly empty lists if nothing is plugged in). Errors such as acroname_error / monsoon_error in that JSON usually mean a missing dependency or no access to USB devices.

Monsoon note: the HVPM often shows up in lsusb as 2ab9:0001 but does not create a /dev/ttyACM* (upstream tools use libusb, not a serial TTY). FiWiControl counts Monsoon for discovery/inventory using pyserial when a TTY exists, and on Linux also matches the same VID/PID under /sys/bus/usb/devices, so monsoon can still reflect lsusb even when list_ports is empty.

Example: healthy discovery on a Raspberry Pi (lab rig)

Run on the Pi (same machine where USB is cabled). lsusb should show the Monsoon line; --discovery-json should still list monsoon even without ttyACM:

$ lsusb | grep -i 2ab9
Bus 005 Device 002: ID 2ab9:0001 Monsoon Solutions Inc. Mobile Device Power Monitor
python3 -m fiwicontrol.power --discovery-json

Example output (shape is what matters; serial numbers and counts will match your hardware):

{
  "acroname": [
    {
      "transport": "USB",
      "serial_number": 882238458,
      "module_address": 0,
      "model_id": 19,
      "model_name": "USBHub3p",
      "model_description": "Programmable 8+1 port USB 3.0 Hub: Default module address is 6.",
      "stem_class": "USBHub3p",
      "downstream_usb_ports": 8,
      "hub_port_entities": 12
    },
    {
      "transport": "USB",
      "serial_number": 3346268699,
      "module_address": 0,
      "model_id": 17,
      "model_name": "USBHub2x4",
      "model_description": "Programmable 4 port USB Hub: Default module address is 6.",
      "stem_class": "USBHub2x4",
      "downstream_usb_ports": 4,
      "hub_port_entities": 6
    },
    {
      "transport": "USB",
      "serial_number": 1960815024,
      "module_address": 0,
      "model_id": 19,
      "model_name": "USBHub3p",
      "model_description": "Programmable 8+1 port USB 3.0 Hub: Default module address is 6.",
      "stem_class": "USBHub3p",
      "downstream_usb_ports": 8,
      "hub_port_entities": 12
    }
  ],
  "monsoon": [
    {
      "device": "/dev/bus/usb/5/2",
      "serial_number": "35004",
      "vid": 10937,
      "pid": 1,
      "manufacturer": null,
      "product": null,
      "hwid": "sysfs:5-2"
    }
  ]
}

How to read this:

Field / pattern Meaning
acroname One object per BrainStem module (hubs/stems). Use stem_class + downstream_usb_ports for acroname = … in the INI (Stem:ports multiset).
monsoon One object per Monsoon-class USB device FiWiControl detected. device may be /dev/bus/usb/<bus>/<dev> (Linux sysfs path) when there is no serial TTY — that is normal for HVPM.
vid: 10937 Decimal for 0x2AB9 (Monsoon vendor ID). pid: 1 is 0x0001.
hwid: sysfs:5-2 Sysfs device directory name under /sys/bus/usb/devices/ (debugging / correlation with lsusb bus topology).
Empty monsoon with Monsoon in lsusb Old FiWiControl build (no sysfs fallback), or Monsoon not actually on this hosts USB tree.
acroname_error / monsoon_error Missing brainstem / pyserial, or enumeration failure — read the string value.

Use this JSON to set [inventory.host.*] acroname and monsoon_count, then run --verify-inventory.

Example: --verify-inventory (success)

From your workstation (repo root, passwordless ssh root@<pi-ip> working, Pi and Fedora have fiwicontrol[power] installed and USB as expected):

cd ~/Code/FiWiControl
python3 -m fiwicontrol.power --verify-inventory
# or: python3 -m fiwicontrol.power -c configs/clubhouse.ini --verify-inventory
echo "exit code: $?"

Successful transcript (the Loaded inventory path is your clone; OK: plus exit 0 confirm everything matched):

Loaded inventory from /home/rjmcmahon/Code/FiWiControl/configs/clubhouse.ini
Site: ClubHouse
  host 'localhost': mode=local acroname=[('USBHub2x4', 4)] monsoon_count=0
  host 'pi5': mode=remote acroname=[('USBHub3p', 8), ('USBHub3p', 8), ('USBHub2x4', 4)] monsoon_count=1
OK: discovery matches INI for all inventory.host sections.

$ echo $?
0

Exit code 0 means every [inventory.host.*] row matched live discovery (local workstation + SSH to each mode=remote node).

If something is wrong, the command prints MISMATCH: on stderr and one or more lines such as [pi5] Acroname multiset mismatch or Monsoon count mismatch, then exits 1. Fix acroname / monsoon_count in the INI to match --list-power-devices / --discovery-json, or fix USB/hardware until discovery matches the INI.

SSH noise before JSON: if ssh prints errors (e.g. Bad owner or permissions on /etc/ssh/ssh_config.d/20-systemd-ssh-proxy.conf), OpenSSH can put that on stdout and break remote discovery. On Fedora, sudo chmod 644 and correct ownership (root:root) on that file (and any other bad drop-ins) so ssh -o BatchMode=yes root@<pi> true prints nothing before the remote command runs.


Verification checklist (after install)

Run these in order on your workstation (repo root, pip install -e ".[power]" for anything that touches USB discovery). Fix each step before moving on.

  1. Package imports (workstation)
    python3 -c "import fiwicontrol.commands, fiwicontrol.power; print('ok')"

  2. Local USB discovery (workstation)
    python3 -m fiwicontrol.power --discovery-json — expect JSON with acroname / monsoon (or *_error keys if dependencies/USB access are wrong).

  3. SSH to the rig (non-interactive)
    ssh -o BatchMode=yes root@<RIG_IP> true — exit 0, no password prompt.

  4. Remote Python + fiwicontrol (rig)
    ssh -o BatchMode=yes root@<RIG_IP> 'python3 -c "import fiwicontrol.power; print(\"ok\")"'

  5. Remote discovery (rig)
    Same SSH, run python3 -m fiwicontrol.power --discovery-json and confirm JSON shape (as in step 2).

  6. Automated remote dependency test (workstation)
    export FIWI_REMOTE_IP=<RIG_IP> if not using the default, then pytest -q tests/test_remote_power_dependencies.py.

  7. SSH command path (workstation)
    FIWI_REMOTE_IP=<RIG_IP> pytest -q tests/test_node_control.py — exercises ssh_node / rexec against root@<ip>.

  8. INI matches your lab (workstation)
    Edit configs/*.ini (see Lab INI file reference above). Then:
    python3 -m fiwicontrol.power --list-power-devices (optional -c path/to.ini; default configs/default.ini) — confirms discovery per host with headers showing addresses.
    python3 -m fiwicontrol.power --verify-inventory — same -c rules; must print OK: discovery matches INI… (exit 0). If you see MISMATCH, adjust acroname / monsoon_count or fix hardware/cabling.

  9. Optional strict pytest (workstation)
    FIWI_VERIFY_POWER_INI=1 pytest -q tests/test_inventory_verify_live.py — opt-in live test that compares discovery to configs/clubhouse.ini (edit the test or your INI if your lab uses a different file).

Environment variables (see docs/install.md): FIWI_REMOTE_IP, FIWI_REMOTE_PYTHON (if python3 on the rig is not the interpreter pip used), FIWI_REMOTE_PIP_FLAGS (PEP 668 on the Pi).


Local vs remote in configs/*.ini

  • [inventory.host.<name>] with mode = local — discovery runs on the machine where you invoke python -m fiwicontrol.power --verify-inventory ... (or --list-power-devices).
  • mode = remote with node = <id> — resolves [node.<id>] (e.g. ipaddr) and runs python3 -m fiwicontrol.power --discovery-json over SSH on that host as root.

See configs/clubhouse.ini for a full example and inline comments.


Commands (recap)

Compare INI to live discovery (local + remote rows):

cd ~/Code/FiWiControl
python3 -m pip install -e ".[power]"
python3 -m fiwicontrol.power --verify-inventory

Full inventory match including hardware (pytest, opt-in):

FIWI_VERIFY_POWER_INI=1 pytest -q tests/test_inventory_verify_live.py

Check only that the remote has imports + python -m working (pytest, needs SSH to FIWI_REMOTE_IP):

pytest -q tests/test_remote_power_dependencies.py

Acroname and concurrency

BrainStem calls are blocking and the device is effectively serialized over one USB link. Use a single lock (or one owning coroutine) per connected hub if multiple async tasks might touch it. See discussion in the codebase / review notes if you add multi-port async drivers.