14 KiB
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 row’s 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:
- Python 3.11+ on the Pi (same as this repo).
fiwicontrolimportable 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.
- editable install from a checkout:
- Optional extras for hardware discovery (same as operator workstation):
brainstem— Acroname / BrainStem USB enumeration.pyserial— Monsoon (and similar) USB-serial enumeration. Install withpip install 'fiwicontrol[power]'orpip install -e ".[power]"from the repo root.
- Non-interactive SSH — automation uses
root@<host>with passwordless keys (seedocs/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 host’s 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.
-
Package imports (workstation)
python3 -c "import fiwicontrol.commands, fiwicontrol.power; print('ok')" -
Local USB discovery (workstation)
python3 -m fiwicontrol.power --discovery-json— expect JSON withacroname/monsoon(or*_errorkeys if dependencies/USB access are wrong). -
SSH to the rig (non-interactive)
ssh -o BatchMode=yes root@<RIG_IP> true— exit 0, no password prompt. -
Remote Python + fiwicontrol (rig)
ssh -o BatchMode=yes root@<RIG_IP> 'python3 -c "import fiwicontrol.power; print(\"ok\")"' -
Remote discovery (rig)
Same SSH, runpython3 -m fiwicontrol.power --discovery-jsonand confirm JSON shape (as in step 2). -
Automated remote dependency test (workstation)
export FIWI_REMOTE_IP=<RIG_IP>if not using the default, thenpytest -q tests/test_remote_power_dependencies.py. -
SSH command path (workstation)
FIWI_REMOTE_IP=<RIG_IP> pytest -q tests/test_node_control.py— exercisesssh_node/rexecagainstroot@<ip>. -
INI matches your lab (workstation)
Editconfigs/*.ini(see Lab INI file reference above). Then:
python3 -m fiwicontrol.power --list-power-devices(optional-c path/to.ini; defaultconfigs/default.ini) — confirms discovery per host with headers showing addresses.
python3 -m fiwicontrol.power --verify-inventory— same-crules; must printOK: discovery matches INI…(exit 0). If you seeMISMATCH, adjustacroname/monsoon_countor fix hardware/cabling. -
Optional strict pytest (workstation)
FIWI_VERIFY_POWER_INI=1 pytest -q tests/test_inventory_verify_live.py— opt-in live test that compares discovery toconfigs/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>]withmode = local— discovery runs on the machine where you invokepython -m fiwicontrol.power --verify-inventory ...(or--list-power-devices).mode = remotewithnode = <id>— resolves[node.<id>](e.g.ipaddr) and runspython3 -m fiwicontrol.power --discovery-jsonover SSH on that host asroot.
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.