Skip to content

apps-sandbox: move to the float32 bootloader, add dotbot-next and a button calibration app - #424

Merged
geonnave merged 32 commits into
DotBots:mainfrom
geonnave:lh2-phase3
Sep 23, 2026
Merged

geonnave merged 32 commits into
DotBots:mainfrom
geonnave:lh2-phase3

Conversation

@geonnave

@geonnave geonnave commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

The DotBot-firmware piece of the LH2 calibration rework: the sandbox apps move to swarmit's new import library, a new calibrate app captures a calibration point from the robot's own button, and the bare-metal dotbot app takes the float32 calibration packet.

This PR supersedes #422 and carries its commits first (apps-sandbox/dotbot-next, the control-loop-free app on the fix sequence, its README, and the import library with the fix-sequence entry). #422 sat on the same import library this work regenerates, so it is folded in rather than merged ahead; its description and review stay readable there.

Changes since review. The sandbox apps follow the swarmit PR's interface change: the uplink-budget entry point is replaced by swarmit_get_min_tx_interval_us(), so cmse_implib.a is regenerated with that entry and every app relinks against it. Veneer addresses move, so an app built before this change must not run on the new bootloader. calibrate paces its log events on the minimum TX interval instead of on half the budget.

What is in it

  • cmse_implib.a regenerated from the swarmit companion PR, now carrying the fix-sequence, raw-counts and minimum TX interval entries (15 in all). Most veneer addresses move, so every sandbox app here is rebuilt against it, even the ones unchanged in source, and runs only on a bootloader from that same swarmit build. Old app binaries do not run on the new bootloader, and new ones do not run on the old.
  • apps-sandbox/calibrate, a button-triggered LH2 capture. Press the robot's button: three blinks on LED3, then a solid window in which it reads 25 raw-count samples per visible station through swarmit_localization_get_raw_counts(), then a verdict. The app checks the capture locally (every read valid, and the spread of each station's counts under a stillness threshold, so a finger pushing the chassis is refused with a slow pulse). A clean capture is sent three times over swarmit_log_data() under a 5-bit press counter, as [0xCB][press:5 | chunk:3][up to 13 nine-byte records] with a short last chunk, so the host needs no station count and deduplicates by press and chunk. swarmit_keep_alive() is never called inside the capture window, because it drains the counts the window is reading; the window is bounded at 800 ms, under the ~1 s watchdog. The events are paced at least one minimum TX interval apart (swarmit_get_min_tx_interval_us(), rounded up to the 10 ms tick, never under 20 ms), 1 s apart when not joined, so a burst never outruns mari's 32-entry TX queue: on tiny (30,260 us) four stations' 24 events take about 1 s. The three acknowledgement blinks no longer stretch over the whole send. The host side is PyDotBot's calibrate-lh2 collect; the two change together.
  • The watchdog feed is decided on elapsed ticks in calibrate and dotbot-next. Both used to call swarmit_keep_alive() when the tick number at service time was a multiple of 10, so a backlog of ticks serviced at once could step over the multiple. Ten skips in a row would let the watchdog reset the robot. They now feed it when at least 10 ticks have passed since the last feed. In dotbot-next that call is also what runs the position solve, so the position poll moves to the same rule, and so do its command timeout and advertisement.
  • apps/dotbot (bare metal) takes the float32 homography packet. The matrix sits one byte into the packet and the FPU cannot load a float from an unaligned address, so it is copied to an aligned local first, and the calibration index is bounded before the store.
  • dotbot-libs bumped to the float32 store and the decoder and timer fixes.

Lines changed

File + -
apps-sandbox/applications.emProject +48 -0
apps-sandbox/calibrate/main.c +384 -0
apps-sandbox/dotbot-next/README.md +106 -0
apps-sandbox/dotbot-next/main.c +377 -0
2 small files: apps/dotbot/main.c, dotbot-libs +5 -2
apps-sandbox/cmse_implib.a (binary, 860 bytes, 716 on main)
Total, 7 files +920 -2

Merge order

  1. DotBot-libs: bsp: carry LH2 homographies as float32 and fix the LH2 decoder and timer re-arm DotBot-libs#31
  2. swarmit: device: carry float32 LH2 calibrations with a site, and expose raw counts swarmit#165
  3. DotBot-firmware: this PR
  4. PyDotBot: dotbot: push float32 LH2 calibrations with a site gate, reframe, and capture from the button PyDotBot#296

The dotbot-libs submodule here points at the DotBot-libs PR's branch tip and is re-pointed at its merge commit once that lands; cmse_implib.a should be checked against the swarmit build that merges. No other gate: these can merge when reviewed.

Validation

SEGGER Embedded Studio 7.22, sandbox-dotbot-v3 Release, zero warnings for calibrate, dotbot-next and dotbot; make check-format clean on every file in this PR. On one DotBot v3 with one lighthouse, running the Debug bootloader and network core from the swarmit companion PR:

  • dotbot-next and dotbot ran 5-minute soaks with no reset at about 10 solves/s, and 10-15 minute windows each in a 4.4 h A/B against main with no unexpected reset and 98-100% STATUS delivery.
  • calibrate ran 11 minutes with no reset. Button presses at four corners of a 906 x 963 mm rectangle were each received and assembled by the host, even when only one of the three copies of a chunk arrived; the resulting calibration had 25 reads per point, a spread of at most 5 counts, and a 4-point residual of 2e-5 mm. A button capture matched a READY-path capture at the same spot to under 1 count.
  • After the watchdog-feed change: the rebuilt dotbot-next flashed over the air ran 5.5 minutes on one boot with no reset and a fresh position in every 30 s sample, then the rebuilt dotbot app went back on and came up Running.

Re-run after the code-review fixes, 2026-09-22 10:17-11:38, on one tethered DotBot v3 with one lighthouse and the gateway on schedule tiny, Debug bootloader and network core from swarmit #165 cable-flashed with a calibration: three fresh cable flashes each reported a live position 1.5-1.6 s after the flash tool returned, with no button press. In 15-minute windows (bootloader idle, dotbot idle, dotbot with move_raw bursts, dotbot-next idle, calibrate with simulated presses) there was no reset, no crash report, no absence from swarm status, no NODE_LEFT and no FCS error on the gateway link; STATUS delivery was 98.9-99.7% and adverts 97.7-99.6% of the 2/s the apps send, with no advert gap over 1.13 s; the advertised position matched STATUS to 1 mm and no invalid position was reported. calibrate with five simulated presses delivered all three copies of every chunk, the six events of each press within 0.4 s on tiny (budget 3304, so about 70 ms apart), and the host assembled every press. dotbot received 259 of 262 move_raw packets in the fully sampled windows, and dotbot-next published 9.99 fixes per second.

Re-run after the final review round, 2026-09-22 13:55-14:19, on the same tethered robot and gateway (schedule tiny): two cable flashes of the Debug bootloader and network core with a calibration each reported a fresh boot 2-3 s after the flash tool returned, device info v2 carrying the file's site and id. A dotbot build with a receive counter got 400 of 400 move_raw packets exactly once (100 at 4/s, then 300 at about 20/s), with STATUS flowing and no mari rejoin. A 15-minute window on dotbot had no reset and no NODE_LEFT, swarm status had updated within the last second at each of 58 samples, and 99.6% of the 2/s adverts arrived (largest gap 1.27 s). dotbot-next stopped its motors 539-720 ms after the last command (a 519 ms timeout checked every 200 ms) and advertised at 2.00/s. All ten sandbox apps and the bare dotbot app build with zero warnings; cmse_implib.a is unchanged (nm identical to a fresh bootloader build).

Final run on this branch's head, 2026-09-22 17:38-18:19, on one tethered DotBot v3 and a gateway DK on schedule tiny, with the Debug bootloader and network core from the swarmit PR's head cable-flashed and every app rebuilt against the committed cmse_implib.a (nm-identical to that bootloader build's): a stress build of dotbot-next sending 20 raw frames every 10 ms ran 5 minutes with move_raw at 25 Hz and no reset, then stayed up through three unjoined windows of 10 s to 15.8 min with its frame counter running and rejoined by itself each time; calibrate ran 2.5 minutes with no reset (its button capture was not exercised, with nobody at the bench to press the button); and the dotbot app from this head came up Running and joined, with adverts at 2/s. The minimum TX interval read 30,260 us on tiny and 0 while unjoined. The details are in the swarmit PR's validation section.

Not observed on hardware: calibrate's refusal when the chassis is pushed, and a press with no host collecting. The watchdog-feed change was checked by a soak, not by forcing a tick backlog.

The secure side gained swarmit_localization_get_fix(), which reports a
position together with the sequence of the solve it came from. Adding
an entry moves every veneer address, so all the applications here are
rebuilt against this blob whether or not they call the new function,
and a bot only runs them once its bootloader has been reflashed over
cable from the matching swarmit build.

AI-assisted: Claude Opus 5
The advertisement used to zero the accumulator, so counts belonged to
whoever read first and an estimator added later would see nothing. The
tick now drains the destructive hardware read into totals that are never
cleared, and each consumer takes a delta against its own cursor. Totals
are unsigned so the wrap at 2^32 subtracts correctly rather than being
undefined.

AI-assisted: Claude Opus 5
The LH2 calibration packet now carries the matrix as float32 in
millimetres and the calibrated advertisement byte is a full bitmask,
so the host's calibration packer has to move with this bump.

AI-assisted: Claude Opus 5
The matrix sits one byte into the packet and the driver reads it as
floats, which the FPU cannot load from an unaligned address, so it is
copied to an aligned local first.

AI-assisted: Claude Opus 5
The secure side gained swarmit_localization_get_raw_counts(). Adding an
entry moves most veneer addresses, so every application here is rebuilt
against this blob and runs only on a bootloader reflashed over cable
from the matching swarmit build.

AI-assisted: Claude Opus 5
Its log events, [0xCB][press:5 | chunk:3][up to 13 nine-byte records]
with a short last chunk, are decoded by PyDotBot's calibrate-lh2
collect; the two change together.

AI-assisted: Claude Opus 5
…lang-format versions agree

clang-format 15 (local) and 18 (CI) disagree on how to align
_drain[16] against the following aligned-attribute declaration;
only the attribute-after-declarator form is stable across both,
which is why the CI style check failed while a local check-format
passed clean.

AI-assisted: Claude Sonnet 5
@geonnave
geonnave merged commit 6f417db into DotBots:main Sep 23, 2026
32 checks passed
@geonnave
geonnave deleted the lh2-phase3 branch September 23, 2026 08:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant