fix(provision): per-device state keyed by serial port path, so a different board inherits the previous board's node_id
Summary
provision.py keys its per-device state file by serial port path only, with no board
identity, so provisioning a different board on a port previously used by another board
silently offers the earlier board's settings — including node_id, which is the fusion cohort
key.
provision.py:126-129:
def _state_path_for(port: str, state_dir: str) -> str:
safe = port.replace("/", "_").replace(":", "_").replace("\\", "_")
return os.path.join(state_dir, f"{safe}.json")
Port names are reused aggressively — on macOS a second board plugged into the same hub slot
commonly enumerates as the same /dev/cu.usbmodemXXXX.
To be fair to the current design: the merge behaviour is documented (provision.py:22),
--reset is offered for recycled boards (:31, :362), and the merged values are printed
before flashing (:443). This is a footgun rather than a hidden failure. But the printed
values look identical whether they were chosen by the operator or inherited from a different
physical board, so there is nothing to notice.
Why now
Hit three times while provisioning three ESP32-S3 boards in one session. Each new board was
offered the previous board's node_id. Without --reset every board would have been
provisioned as the same node id, which on a multistatic deployment silently corrupts the
cohort rather than failing loudly.
Reproduction
provision.py --port <PORT> --node-id 2 ...with board A.- Unplug A, plug board B into the same port (same device path).
python3 provision.py --port <PORT> --state→ prints board A'snode_id,target_portandchannel.- Provisioning B without
--resetwrites those values to B.
Done when
State is bound to board identity, so a different board on the same port does not inherit.
Work items
- Key the state file by chip identity —
esptool read-mac/ chip id — instead of, or in addition to, the port path. The script already talks to the chip. - When a port's stored identity does not match the connected board, do not merge silently:
either ignore the stale state or require
--reset. - Mark inherited values distinctly in the pre-flash summary (
:443), so "inherited from a previous board" is visually different from "you passed this".
Note
--force-partial is marked deprecated "since #391/#574 … the script now merges with prior
state by default", which is the change that introduced this. Worth checking those for prior
discussion.
Source: ruvnet/RuView