#1755·RuView

fix(provision): per-device state keyed by serial port path, so a different board inherits the previous board's node_id

Author: proffesor-for-testingCreated Aug 31, 2026Updated Sep 16, 2026
Labelsbugfirmware

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

  1. provision.py --port <PORT> --node-id 2 ... with board A.
  2. Unplug A, plug board B into the same port (same device path).
  3. python3 provision.py --port <PORT> --state → prints board A's node_id, target_port and channel.
  4. Provisioning B without --reset writes 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.