Skip to content

Host tools

Everything under tools/ runs on the host. The shell scripts are written for macOS's bash 3.2; the Python needs only the standard library, except the exchange runner, whose two peers need Reticulum.

Tool What it does
test.sh Every check that does not need a board. What CI runs. See Testing.
dfu-flash.sh The cargo run runner: ELF → layout check → DFU package → serial DFU. Bootstraps adafruit-nrfutil into tools/.venv/ if needed.
uf2-flash.sh The alternative runner: ELF → UF2 → the bootloader's drive, for hosts where the drive works. Reads the family id from the board's CURRENT.UF2.
uf2conv.py A UF2 writer from the format specification, with tests.
verify_flash.py Compares the vector table in the board's CURRENT.UF2 against a built ELF and says MATCH or MISMATCH.
air_exchange.py Two RNodes on the bench exchanging packets through Reticulum: a size under 254 bytes and one over, each way, with every log kept. See Testing.
layout.py Reads memory.x for the other tools: tools/layout.py memory.x --field FLASH.origin.
check_layout.py Validates a built ELF (and optionally its UF2) against memory.x. See Memory layout.
crate_version.py Reads the version out of Cargo.toml.
package.sh Builds every image and lays out the files that go on a release under dist/.
release_notes.sh Writes the body of a release to stdout.
test_*.py Tests for the above, run by test.sh with unittest.

dfu-flash.sh

tools/dfu-flash.sh target/thumbv7em-none-eabihf/release/rnode

Flashing goes over the bootloader's SLIP-framed serial DFU (Nordic's older protocol, not Secure DFU, which is why it needs Adafruit's tool). The script checks the image against memory.x, finds the board, performs a 1200-baud touch if the board is running an oxinode image, waits for the bootloader, sends the package, and then resets the host's cached line setting on the port so the next thing that opens it does not perform another touch.

With one board attached it finds the port itself. With two it refuses to guess, because a second Base Duo running someone else's firmware enumerates on the same glob and flashing it would overwrite that firmware.

Variable
OXINODE_DFU_PORT use this serial port instead of searching
OXINODE_DFU_PORT_GLOB the glob to search (default /dev/cu.usbmodem*)
OXINODE_DFU_IN_DFU 1/0 to assert the board is already in its bootloader instead of detecting it (needed on a host that cannot mount the UF2 drive)
OXINODE_DFU_BOOTLOADER 1/0 to answer the bootloader probe directly
OXINODE_DFU_RETRY_DELAY seconds between retries (default 2)
OXINODE_DFU_SETTLE seconds to wait after a flash before reopening the port to reset the cached baud rate (default 3)

tools/test_dfu_flash.py exercises the script end to end without a board through those variables, which is why test.sh runs it after the firmware build: it needs a real linked ELF to push through.

air_exchange.py

python3.11 tools/air_exchange.py --a /dev/cu.usbmodem101 --b /dev/cu.usbmodem3101

Runs the exchange that says whether two RNodes agree on the air: for each size (200 and 400 bytes on the air by default, either side of the split at 254), a packet from the first to the second and one back, each through a Reticulum instance of its own with one RNodeInterface, so what goes on the air is what rnsd sends. The sender and the receiver compare hashes, the size on the air is asserted from the packet Reticulum built, and the receiver reports the signal the packet came with. Every oxinode's defmt log is captured from its log port for the whole run, so each frame's header byte and sequence nibble and every join are in the notes; a stock RNode has no log port, and its side is Reticulum's log.

The peers run under whichever Python has Reticulum: this one if it does, otherwise the one rnsd on the PATH runs under. Everything goes to target/air-exchange/<time>/: summary.txt, a Reticulum log per peer per exchange, and <name>.defmt.log per oxinode.

Option
--a, --b the two KISS ports
--a-name, --b-name what to call them in the logs (default A, B)
--a-log, --b-log a log port, or none; on macOS an oxinode's is derived from its KISS port (...1 to ...3)
--elf the ELF the oxinodes were built from (default the release rnode)
--sizes sizes on the air, comma separated (default 200,400)
--frequency, --bandwidth, --txpower, --spreadingfactor, --codingrate the radio configuration, given to both; --frequency 2478000000 --bandwidth 812500 --txpower 11 is the 2.4 GHz path, with both antennas on the u.FL
--timeout seconds to wait for an RNode to come up, or a packet to arrive (default 30)
--out where the logs go

Exit status is zero only when every exchange delivered the bytes that were sent. tools/test_air_exchange.py pins what it claims about Reticulum's framing: the overhead that makes a 400-byte packet 400 bytes on the air, the plan, and the config that keeps the two instances apart.

check_layout.py

tools/check_layout.py target/thumbv7em-none-eabihf/release/rnode --uf2 target/thumbv7em-none-eabihf/release/rnode.uf2

Checks that every loadable segment lies inside FLASH, that RAM usage lies inside RAM, that the initial stack pointer points into RAM, and that the reset vector points into FLASH with the Thumb bit set. With --uf2, also that the UF2's blocks cover the image at the right addresses. Its tests feed it deliberately broken images and assert that each is rejected.

package.sh and release_notes.sh

tools/package.sh                     # version from Cargo.toml, marked -dev
tools/package.sh --version v0.1.0    # must match Cargo.toml's version
tools/release_notes.sh v0.1.0

See Releasing.