Tools
Flashing, fleet, chaos, soak, and HIL tooling for the remote and machine nodes.
Every Python tool here runs from one environment, described by
pyproject.toml and pinned by uv.lock.
First-time setup
These tools use uv, a Python package and
environment manager.
It replaces pip and venv: you never create or activate a virtualenv by hand,
and everyone gets the identical dependency versions recorded in uv.lock.
1. Install uv
curl -LsSf https://astral.sh/uv/install.sh | sh
The installer puts uv in ~/.local/bin.
Open a new shell afterwards, or source ~/.local/bin/env, then check it:
uv --version
macOS users can use brew install uv instead; Windows and other install methods
are in the uv installation docs.
2. Create the environment
cd tools
uv sync
This creates tools/.venv and installs the locked dependencies into it —
one environment for every tool here, flashing through HIL.
It takes a few seconds and needs no further configuration.
You do not need Python installed already — uv fetches an interpreter if the
system one is older than 3.11.
Running a tool
Prefix any command with uv run.
There is no virtualenv to activate.
cd tools
uv run python flash_station.py --selftest
uv run python pstop_test_remote.py --port 8890
uv run esptool version
uv run re-checks the lock before each command, so a dependency added by a
teammate is installed automatically on your next run.
The shell tools need no prefix.
They re-enter this environment through uv run for each call, so they always
get the locked esptool:
tools/flash_pstop.sh --remote
Flashing a unit
flash_pstop.sh provisions one device over USB from a staged image directory;
flash_station.py runs the unattended production loop.
Both flash dev-profile images and need a staged image in
tools/production_image/ (remote) or tools/production_image_machn/
(machine), which carries secrets and is git-ignored.
See the header comment in each script for the full argument list.
pstop_secure.py signs, provisions and re-flashes units of the secure profiles
(secure-fe, the default, and secure); see
secure_boot_guide.md.
HIL suite
The rig tests live in hil/.
Run them through hil/run.sh, which strips PYTHONPATH — a sourced ROS
environment otherwise puts broken plugins on it:
cd tools/hil
./run.sh # everything, power cycles included
./run.sh -m 'not power' # skip the power-cycle tests
./run.sh test_00_rig.py # rig self-check after (re)wiring
Safety traceability linter
safety_lint/ parses the safety documents, checks the requirement, function,
and evidence mappings, and owns the generated coverage numbers in
docs/safety/requirements_traceability.md.
It is stdlib-only but runs from this environment like everything else here.
It resolves the safety documents against the repository root it is checked into,
so the working directory does not matter:
cd tools
uv run python -m safety_lint --check # CI's gate
uv run python -m safety_lint --write # refresh coverage
Tests
pyproject.toml holds the only pytest configuration. The default run is the
suites that need no hardware, so it passes on any checkout:
cd tools
uv run pytest # linter self-test + the flashing tests; no bench needed
The rig tests are opt-in, because their fixtures fail rather than skip when no relay board is attached:
tools/hil/run.sh # the HIL suite; needs the bench (see the [HIL guide](hil.md))
The flat scripts are deliberately outside that: several take a positional
integer or exit at import, so they are not collectible, and the repo-root
test/ ladders plus the pstop_*_test.py harnesses are bash-
and script-driven. See
testing.md for those.
Flashing-tool regression tests
tools/test/ covers flash_station.py and flash_pstop.sh against
test/fake_esptool.py, a shim that stands in for the real esptool, and
pstop_secure.py against espefuse's virtual eFuses.
No hardware is needed; it never opens a port.
cd tools
uv run pytest test/
This is part of the default uv run pytest collection above, so a plain run
from tools/ already includes it.
The one exception: soak_per_remote.py in ROS mode
soak_per_remote.py reads remote state from either the chip's HTTP
state.json or a ROS 2 topic.
The HTTP source is stdlib-only and runs under uv run like everything else.
The ROS source imports rclpy and protective_stop_msg lazily, and those come
from a sourced ROS workspace rather than PyPI, so the uv environment cannot
provide them:
source /opt/ros/$ROS_DISTRO/setup.bash
source <workspace>/install/setup.bash
python3 tools/soak_per_remote.py ... # system python, not uv run
Adding a dependency
cd tools
uv add <package>
uv add edits pyproject.toml and updates uv.lock.
Commit both.