From 647307a2f28d49fe9d1386952bd226a99fcea8e8 Mon Sep 17 00:00:00 2001 From: Geovane Fedrecheski Date: Fri, 2 Oct 2026 14:16:01 +0200 Subject: [PATCH 1/5] dotbot/calibration: push and bake stations by mask, each with its rectangle Breaking, cross-repo: the messages and the config page (magic SWRP, 872 bytes) only match a swarmit bootloader and net core that store stations by mask, and swarmit's Python must accept mask messages before it relays them. Robots below device info 4 are refused with a reflash. AI-assisted: Claude Opus 5.5 --- dotbot/calibration/lighthouse2.py | 31 ++++++------ dotbot/calibration/push.py | 2 +- dotbot/calibration/simulated.py | 2 +- dotbot/firmware/flash.py | 46 +++++++++++------- dotbot/tests/lh2_wire_fixture.py | 10 ++-- dotbot/tests/test_calibration_lighthouse2.py | 47 ++++++++++++++++--- .../tests/test_calibration_multi_station.py | 40 ++++++++++++++++ dotbot/tests/test_calibration_push.py | 4 +- dotbot/tests/test_calibration_session.py | 4 +- dotbot/tests/test_calibration_spin.py | 2 +- dotbot/tests/test_device.py | 28 ++++++----- 11 files changed, 152 insertions(+), 64 deletions(-) diff --git a/dotbot/calibration/lighthouse2.py b/dotbot/calibration/lighthouse2.py index 6954f95c..8114ccca 100644 --- a/dotbot/calibration/lighthouse2.py +++ b/dotbot/calibration/lighthouse2.py @@ -379,8 +379,10 @@ def reprojection_residual_mm( # --- Wire ------------------------------------------------------------------- # One calibration message per station, `swrmt_lh2_calibration_data_t` in the -# swarmit netcore: count, index, the matrix, then the site fields every -# message of a push repeats. swarmit's `helpers.py` packs the same bytes; the +# swarmit netcore: the station mask, the station's index, its matrix and its +# rectangle, then the site name and calibration id every message of a push +# repeats. The net core commits once every station of the mask has arrived, +# keyed by mask and id. swarmit's `helpers.py` packs the same bytes; the # fixture test in each repo pins them. LH2_SITE_NAME_BYTES = 16 LH2_CALIBRATION_ID_BYTES = 8 @@ -483,11 +485,10 @@ def station_label(index: int) -> str: def pushable_stations(calibration: Calibration) -> list[StationSolution]: - """The stations in index order, refused when a robot would mis-key them. + """The stations in index order, refused when a robot could not hold them. - The receiver stores a matrix at its index and takes the count as "slots - 0 to count - 1 are valid", so a gap in the numbering would leave a slot - the count claims and nothing filled. + A robot stores each station in the slot of its index, 0 to 15, so any + set of indices in that range will do. """ ordered = sorted(calibration.stations, key=lambda s: s.index) if not ordered: @@ -497,30 +498,30 @@ def pushable_stations(calibration: Calibration) -> list[StationSolution]: f"{len(ordered)} stations exceeds the LH2 limit " f"({LH2_BASESTATION_COUNT_MAX})" ) - if [s.index for s in ordered] != list(range(len(ordered))): - got = ", ".join(str(s.index) for s in ordered) + outside = [s.index for s in ordered if not 0 <= s.index < LH2_BASESTATION_COUNT_MAX] + if outside: raise ValueError( - f"stations must be numbered from zero without gaps to be pushed, got {got}" + f"station index {outside} is outside 0 to {LH2_BASESTATION_COUNT_MAX - 1}" ) return ordered def site_fields_as_bytes(calibration: Calibration) -> bytes: - """valid_mm, site name and id: the 40 bytes after every matrix.""" - return ( - valid_mm_as_bytes(calibration.valid_mm) - + site_name_as_bytes(calibration.site.name) - + calibration_id_as_bytes(pushed_id(calibration)) + """Site name and id: the 24 bytes that close every message and the page.""" + return site_name_as_bytes(calibration.site.name) + calibration_id_as_bytes( + pushed_id(calibration) ) def calibration_messages(calibration: Calibration) -> list[bytes]: """One 84-byte calibration message per station, in index order.""" stations = pushable_stations(calibration) + mask = station_mask(calibration) site_fields = site_fields_as_bytes(calibration) return [ - struct.pack(" dict[str, Any]: self.pushed[-1][:LH2_CALIBRATION_MESSAGE_BYTES] ) info = SimpleNamespace( - info_version=3, lh2_site_name=site, lh2_calibration_id=calibration_id + info_version=4, lh2_site_name=site, lh2_calibration_id=calibration_id ) return {self.device: SimpleNamespace(info_gen=1, info=info)} diff --git a/dotbot/firmware/flash.py b/dotbot/firmware/flash.py index 4fa34417..bd8b3f4a 100644 --- a/dotbot/firmware/flash.py +++ b/dotbot/firmware/flash.py @@ -52,22 +52,27 @@ # swarmit's `swarmit_config_t` (SWARMIT_CONFIG_MAGIC_VALUE). Each value must # match its firmware's. CONFIG_MAGIC_BY_ROLE = { - "dotbot-v3": 0x5753524F, + "dotbot-v3": 0x57535250, "gateway": 0x5753524D, } CONFIG_MANIFEST_NAME = "config-manifest.json" # swarmit_config_t, little-endian and packed: magic, has_net_id, net_id, -# homography_count, sixteen 3x3 float32 matrices, valid_mm[4], site_name[16], -# calibration_id[8]. Every byte the calibration does not set is 0xFF, the -# erase state the firmware reads as "absent". +# station_mask, sixteen 3x3 float32 matrices by slot, sixteen valid_mm[4] by +# slot, site_name[16], calibration_id[8]; 872 bytes. Every byte the +# calibration does not set is 0xFF, the erase state the firmware reads as +# "absent"; slots outside the mask are ignored. LH2_MATRIX_BYTES = 3 * 3 * 4 +LH2_VALID_MM_BYTES = 4 * 4 LH2_MAX_HOMOGRAPHIES = 16 -SWARMIT_CONFIG_COUNT_OFFSET = 12 -SWARMIT_CONFIG_MATRICES_OFFSET = SWARMIT_CONFIG_COUNT_OFFSET + 4 -SWARMIT_CONFIG_SITE_OFFSET = ( +SWARMIT_CONFIG_MASK_OFFSET = 12 +SWARMIT_CONFIG_MATRICES_OFFSET = SWARMIT_CONFIG_MASK_OFFSET + 4 +SWARMIT_CONFIG_VALID_MM_OFFSET = ( SWARMIT_CONFIG_MATRICES_OFFSET + LH2_MAX_HOMOGRAPHIES * LH2_MATRIX_BYTES ) -SWARMIT_CONFIG_SITE_BYTES = 4 * 4 + 16 + 8 +SWARMIT_CONFIG_SITE_OFFSET = ( + SWARMIT_CONFIG_VALID_MM_OFFSET + LH2_MAX_HOMOGRAPHIES * LH2_VALID_MM_BYTES +) +SWARMIT_CONFIG_SITE_BYTES = 16 + 8 SWARMIT_CONFIG_BYTES = SWARMIT_CONFIG_SITE_OFFSET + SWARMIT_CONFIG_SITE_BYTES # Application images are linked after the bootloader. APP_FLASH_BASE_ADDR = 0x00010000 @@ -163,17 +168,15 @@ def make_config_hex_path( def load_calibration_file(path: Path): - """Read a schema 3 calibration file, refused when a robot could not take it.""" + """Read a calibration file, refused when a robot could not take it.""" from dotbot.calibration.lighthouse2 import ( - pushable_stations, + calibration_messages, read_calibration_file, - site_fields_as_bytes, ) try: calibration = read_calibration_file(Path(path)) - pushable_stations(calibration) - site_fields_as_bytes(calibration) + calibration_messages(calibration) except (OSError, ValueError) as exc: raise click.ClickException( f"Cannot use calibration file {path}: {exc}" @@ -187,22 +190,28 @@ def swarmit_config_page(net_id_value: int, calibration=None) -> bytes: homography_as_float32, pushable_stations, site_fields_as_bytes, + station_mask, + valid_mm_as_bytes, ) page = bytearray(b"\xff" * SWARMIT_CONFIG_BYTES) - page[:SWARMIT_CONFIG_COUNT_OFFSET] = struct.pack( + page[:SWARMIT_CONFIG_MASK_OFFSET] = struct.pack( " ButtonCapture ) -def _info(version=3, site="", calibration_id=""): +def _info(version=4, site="", calibration_id=""): """A robot's device info as swarmit decodes it.""" return SimpleNamespace( info_version=version, lh2_site_name=site, lh2_calibration_id=calibration_id @@ -115,7 +115,7 @@ def send_lh2_calibration(self, payload: bytes, devices=None) -> None: for addr, info in self.infos.items(): if devices is not None and addr not in devices: continue - if info is not None and info.info_version >= 3: + if info is not None and info.info_version >= 4: info.lh2_site_name, info.lh2_calibration_id = message_site( payload[:LH2_CALIBRATION_MESSAGE_BYTES] ) diff --git a/dotbot/tests/test_calibration_spin.py b/dotbot/tests/test_calibration_spin.py index 11d8e039..14b6878e 100644 --- a/dotbot/tests/test_calibration_spin.py +++ b/dotbot/tests/test_calibration_spin.py @@ -359,7 +359,7 @@ def test_a_spin_push_waits_for_the_robots_to_leave_the_app(monkeypatch, lab): sent = [] fleet.send_lh2_calibration = lambda payload, devices=None: sent.append(devices) for node in fleet.nodes.values(): - node.info.info_version = 3 + node.info.info_version = 4 node.info.lh2_site_name = "c405" node.info.lh2_calibration_id = "" node.info_gen = 1 diff --git a/dotbot/tests/test_device.py b/dotbot/tests/test_device.py index 769d0903..59fda2cd 100644 --- a/dotbot/tests/test_device.py +++ b/dotbot/tests/test_device.py @@ -532,13 +532,13 @@ def test_the_sandbox_page_without_a_calibration_is_erased_past_the_net_id(): from dotbot.firmware.flash import swarmit_config_page page = swarmit_config_page(0x1234) - assert len(page) == 632 - assert page[:12] == bytes.fromhex("4f525357" "01000000" "34120000") - assert page[12:] == b"\xff" * 620 + assert len(page) == 872 + assert page[:12] == bytes.fromhex("50525357" "01000000" "34120000") + assert page[12:] == b"\xff" * 860 def test_the_sandbox_page_with_a_calibration_is_pinned(tmp_path): - """swarmit_config_t, 632 bytes, for the fixture file shared with swarmit.""" + """swarmit_config_t, 872 bytes, for the fixture file shared with swarmit.""" import hashlib from dotbot.firmware.flash import load_calibration_file, swarmit_config_page @@ -548,22 +548,26 @@ def test_the_sandbox_page_with_a_calibration_is_pinned(tmp_path): path.write_text(FIXTURE_TOML, encoding="utf-8") page = swarmit_config_page(0x1234, load_calibration_file(path)) - assert len(page) == 632 + assert len(page) == 872 assert page[:64].hex() == ( - "4f525357010000003412000002000000" + "50525357010000003412000003000000" "cd6cbe44cdcc18c2cd2c7d449a992742" "9a79bf443313774488855a3e7c61b2bd" "0000803f0008b9c40000484100603845" ) assert hashlib.sha256(page).hexdigest() == ( - "b1d492d3b8021d16976e5f49a984374655fada7df62e13b35bf9e4e351d6e530" + "deda154c7d1991dc043e38a7cac5b4461ffd381d0d16fa53a2d79e045348880f" ) - # Station 1 in slot 1, the fourteen unused slots erased, then the site - # fields exactly as the calibration message carries them. + # Station 1 in slot 1, the fourteen unused slots erased; each station's + # rectangle in its slot, the rest erased; then the site name and id + # exactly as the calibration message carries them. message = [bytes.fromhex(h) for h in MESSAGE_HEX] assert page[16 + 36 : 16 + 72] == message[1][8:44] assert page[88:592] == b"\xff" * 504 - assert page[592:632] == message[0][44:84] + assert page[592:608] == message[0][44:60] + assert page[608:624] == message[1][44:60] + assert page[624:848] == b"\xff" * 224 + assert page[848:872] == message[0][60:84] def test_the_sandbox_page_has_a_slot_per_station_the_calibration_allows(): @@ -598,9 +602,9 @@ def test_the_manifest_cache_misses_on_the_old_magic(tmp_path): from dotbot.firmware.flash import build_manifest_payload, manifest_matches payload = build_manifest_payload(tmp_path / "c.hex", "dotbot-v3", "local", "1234") - assert payload["magic"] == "0x5753524F" + assert payload["magic"] == "0x57535250" assert manifest_matches(payload, "dotbot-v3", "local", "1234") - for old in ("0x5753524E", "0x5753524D"): + for old in ("0x5753524F", "0x5753524E", "0x5753524D"): payload["magic"] = old assert not manifest_matches(payload, "dotbot-v3", "local", "1234") gateway = build_manifest_payload(tmp_path / "g.hex", "gateway", "local", "1234") From 1a05d9856acc38c4714e931d105360113210b581 Mon Sep 17 00:00:00 2001 From: Geovane Fedrecheski Date: Fri, 2 Oct 2026 14:16:01 +0200 Subject: [PATCH 2/5] dotbot/cli: name stations by index and channel in push, add push --dry-run AI-assisted: Claude Opus 5.5 --- dotbot/cli/swarm_lh2.py | 46 +++++++++++++++++++++++++-- dotbot/tests/test_calibration_push.py | 19 +++++++++++ 2 files changed, 62 insertions(+), 3 deletions(-) diff --git a/dotbot/cli/swarm_lh2.py b/dotbot/cli/swarm_lh2.py index 43e0c359..df5f0c74 100644 --- a/dotbot/cli/swarm_lh2.py +++ b/dotbot/cli/swarm_lh2.py @@ -754,8 +754,16 @@ def _collect_spin( "report another one." ), ) +@click.option( + "--dry-run", + is_flag=True, + help=( + "Print the messages the push would send, one per station, and send " + "nothing; needs no swarm connection." + ), +) @click.pass_context -def _push(ctx, calibration, conn, swarm_id, site_name, site_changed): +def _push(ctx, calibration, conn, swarm_id, site_name, site_changed, dry_run): from dotbot.calibration.lighthouse2 import ( read_calibration_file, resolve_calibration_path, @@ -767,17 +775,45 @@ def _push(ctx, calibration, conn, swarm_id, site_name, site_changed): loaded = read_calibration_file(path) except ValueError as exc: raise click.ClickException(str(exc)) from exc + if dry_run: + _print_messages(loaded) + return client = _swarmit_client(ctx, conn, swarm_id) with client: _gated_push(client, loaded, site_changed=site_changed, devices=_devices(ctx)) +def _print_messages(calibration): + """What a push of `calibration` sends, message by message.""" + from dotbot.calibration.lighthouse2 import ( + calibration_messages, + station_label, + station_mask, + ) + + try: + messages = calibration_messages(calibration) + except ValueError as exc: + raise click.ClickException(str(exc)) from exc + stations = sorted(calibration.stations, key=lambda s: s.index) + click.echo( + f"Would send {len(messages)} message(s), {sum(map(len, messages))} B: " + f"id {calibration.id8}, site {calibration.site.name}, station mask " + f"0x{station_mask(calibration):04X}" + ) + for station, message in zip(stations, messages): + click.echo( + f" {station_label(station.index)}, rectangle {list(station.valid_mm)}: " + f"{message.hex()}" + ) + + def _gated_push(client, calibration, site_changed=False, devices=None, stop=False): """Check the robots' device info, push, and print the stale-id worklist. With `stop`, the named `devices` still in their app are stopped first. """ - from dotbot.calibration.lighthouse2 import calibration_payload + from dotbot.calibration.lighthouse2 import calibration_payload, station_label from dotbot.calibration.push import ( PushRefused, gate_push, @@ -810,8 +846,12 @@ def _gated_push(client, calibration, site_changed=False, devices=None, stop=Fals f"{len(check.other_site)} robot(s) move to site " f"{calibration.site.name} (--site-changed)." ) + stations = ", ".join( + station_label(s.index) + for s in sorted(calibration.stations, key=lambda s: s.index) + ) click.echo( - f"Sending {len(calibration.stations)} calibration matrix/matrices " + f"Sending {stations} " f"({len(payload)} B, id {calibration.id8}, site {calibration.site.name}) " f"to the swarm; {len(check.stale)} robot(s) hold another id..." ) diff --git a/dotbot/tests/test_calibration_push.py b/dotbot/tests/test_calibration_push.py index b5635001..94f28e65 100644 --- a/dotbot/tests/test_calibration_push.py +++ b/dotbot/tests/test_calibration_push.py @@ -368,3 +368,22 @@ def client(ctx, conn, swarm_id): ) assert seen == {"no_server": True} assert fleet.pushed_to == [["A"]] + + +def test_a_dry_run_prints_the_messages_and_needs_no_swarm( + monkeypatch, calibration_file +): + def no_client(*_args, **_kwargs): + raise AssertionError("a dry run opens no swarm connection") + + monkeypatch.setattr(swarm_lh2, "_swarmit_client", no_client) + result = CliRunner().invoke( + swarm_lh2.cmd, + ["push", str(calibration_file), "--dry-run"], + obj={"config": LAB}, + ) + assert result.exit_code == 0, result.output + assert "Would send 2 message(s), 168 B" in result.output + assert "station mask 0x0003" in result.output + for hexed in MESSAGE_HEX: + assert hexed in result.output From 6df086f6ae0fff88fe6eeb4e8c5ac2a0c97b38a8 Mon Sep 17 00:00:00 2001 From: Geovane Fedrecheski Date: Fri, 2 Oct 2026 14:16:01 +0200 Subject: [PATCH 3/5] dotbot/console-web: read lh2_station_mask, take device info 4 as current AI-assisted: Claude Opus 5.5 --- dotbot/console-web/dev/fake_swarmit_server.py | 20 ++++++++++--------- dotbot/console-web/src/fleetViews.test.tsx | 2 +- dotbot/console-web/src/localization.test.ts | 2 +- dotbot/console-web/src/localization.ts | 2 +- dotbot/console-web/src/pushTarget.test.tsx | 2 +- dotbot/console-web/src/types.ts | 3 ++- 6 files changed, 17 insertions(+), 14 deletions(-) diff --git a/dotbot/console-web/dev/fake_swarmit_server.py b/dotbot/console-web/dev/fake_swarmit_server.py index f961d2ba..aa54e305 100644 --- a/dotbot/console-web/dev/fake_swarmit_server.py +++ b/dotbot/console-web/dev/fake_swarmit_server.py @@ -174,17 +174,19 @@ def device_info(addr: str) -> dict: seed = _seed(addr) name, digest, size = IMAGES[seed % len(IMAGES)] # A minority are uncalibrated, which is the state an operator acts on. - homographies = 0 if seed % 9 == 0 else (2 if seed % 5 == 0 else 1) - flags = LH2_FLAG_VALID | LH2_FLAG_FROM_FLASH if homographies else 0 - noun = "basestation" if homographies == 1 else "basestations" + mask = 0 if seed % 9 == 0 else (0b101 if seed % 5 == 0 else 0b1) + flags = LH2_FLAG_VALID | LH2_FLAG_FROM_FLASH if mask else 0 + stations = [i for i in range(16) if mask >> i & 1] summary = ( "uncalibrated" - if not homographies - else f"{homographies} {noun} (valid, from flash)" + if not mask + else f"station{'s' if len(stations) > 1 else ''} " + f"{', '.join(map(str, stations))} (channel{'s' if len(stations) > 1 else ''} " + f"{', '.join(str(i + 1) for i in stations)}; valid, from flash)" ) return { # A few run sandbox firmware too old for this host's calibrations. - "info_version": 2 if seed % 7 == 0 else 3, + "info_version": 3 if seed % 7 == 0 else 4, "info_gen": 4, "boot_count": 2 + seed % 30, "uptime_s": 60 + seed % 9000, @@ -198,10 +200,10 @@ def device_info(addr: str) -> dict: "image_digest": digest, "image_name": name, "image_version": "", - "lh2_homography_count": homographies, + "lh2_station_mask": mask, "lh2_flags": flags, - "lh2_site_name": FAKE_SITE if homographies else "", - "lh2_calibration_id": FAKE_CALIBRATION_ID if homographies else "", + "lh2_site_name": FAKE_SITE if mask else "", + "lh2_calibration_id": FAKE_CALIBRATION_ID if mask else "", "lh2_summary": summary, "raw": "8f0104" + f"{seed:08x}" * 4, } diff --git a/dotbot/console-web/src/fleetViews.test.tsx b/dotbot/console-web/src/fleetViews.test.tsx index 9186e61e..efd5d8de 100644 --- a/dotbot/console-web/src/fleetViews.test.tsx +++ b/dotbot/console-web/src/fleetViews.test.tsx @@ -30,7 +30,7 @@ const site: Site = { }; const info = (over: Partial = {}): SwarmitDeviceInfo => ({ - info_version: 3, + info_version: 4, bl_version: "1.25.0", net_version: "1.25.0", boot_count: 3, diff --git a/dotbot/console-web/src/localization.test.ts b/dotbot/console-web/src/localization.test.ts index 7b83a269..8293d15a 100644 --- a/dotbot/console-web/src/localization.test.ts +++ b/dotbot/console-web/src/localization.test.ts @@ -56,7 +56,7 @@ const bot = (id: string, calibrationId?: string): UnifiedBot => calibrationId === undefined ? null : ({ - info: { info_version: 3, lh2_calibration_id: calibrationId }, + info: { info_version: 4, lh2_calibration_id: calibrationId }, } as unknown as SwarmitNode), }) as UnifiedBot; diff --git a/dotbot/console-web/src/localization.ts b/dotbot/console-web/src/localization.ts index 0d05627a..e04bbafa 100644 --- a/dotbot/console-web/src/localization.ts +++ b/dotbot/console-web/src/localization.ts @@ -39,7 +39,7 @@ export interface Coverage { } /** The device-info version of firmware that takes this host's calibrations; older needs a reflash. */ -export const DEVICE_INFO_VERSION_MIN = 3; +export const DEVICE_INFO_VERSION_MIN = 4; /** Who carries `savedId`, from the calibration id each robot's device info reports. */ export function calibrationCoverage(bots: UnifiedBot[], savedId: string): Coverage { diff --git a/dotbot/console-web/src/pushTarget.test.tsx b/dotbot/console-web/src/pushTarget.test.tsx index ed1140ba..a747f7a9 100644 --- a/dotbot/console-web/src/pushTarget.test.tsx +++ b/dotbot/console-web/src/pushTarget.test.tsx @@ -76,7 +76,7 @@ const bot = (id: string, calibrationId: string): UnifiedBot => ({ id, swarmit: { - info: { info_version: 3, lh2_calibration_id: calibrationId }, + info: { info_version: 4, lh2_calibration_id: calibrationId }, } as unknown as SwarmitNode, }) as UnifiedBot; diff --git a/dotbot/console-web/src/types.ts b/dotbot/console-web/src/types.ts index dac6b4a1..2d43c334 100644 --- a/dotbot/console-web/src/types.ts +++ b/dotbot/console-web/src/types.ts @@ -266,7 +266,8 @@ export interface SwarmitDeviceInfo { image_name: string; image_version: string; image_digest: string; - lh2_homography_count?: number; + // Bit i set: the bot holds station i's homography (channel i + 1). + lh2_station_mask?: number; lh2_flags?: number; // Device info v2: the site and calibration id the bot holds, "" for none. lh2_site_name?: string; From 1ba82eff3a567884a64bdcc1da1b5b8e7efdb6bf Mon Sep 17 00:00:00 2001 From: Geovane Fedrecheski Date: Fri, 2 Oct 2026 14:48:58 +0200 Subject: [PATCH 4/5] dotbot/tests: pin the wire fixture to stations 2 and 8 with rectangles of their own AI-assisted: Claude Opus 5.5 --- dotbot/tests/lh2_wire_fixture.py | 49 +++++++++++--------- dotbot/tests/test_calibration_lighthouse2.py | 11 +++-- dotbot/tests/test_calibration_push.py | 10 ++-- dotbot/tests/test_calibration_spin.py | 2 +- dotbot/tests/test_controller.py | 10 +++- dotbot/tests/test_device.py | 28 ++++++----- 6 files changed, 59 insertions(+), 51 deletions(-) diff --git a/dotbot/tests/lh2_wire_fixture.py b/dotbot/tests/lh2_wire_fixture.py index 7cd998be..2bf658d6 100644 --- a/dotbot/tests/lh2_wire_fixture.py +++ b/dotbot/tests/lh2_wire_fixture.py @@ -7,14 +7,14 @@ message bytes: the two packers must agree, so keep the copies identical. """ -FIXTURE_ID = "80285c9b7db82732" +FIXTURE_ID = "9b12f56f622f0fb6" FIXTURE_TOML = """\ schema_version = 4 [metadata] created_at = "2026-09-10T09:12:00Z" -id = "80285c9b7db82732" +id = "9b12f56f622f0fb6" robot = "dotbot-v3" tag = "arena-relay" @@ -31,46 +31,48 @@ points_mm = [[47.0, 18.5], [1953.0, 18.5], [47.0, 1981.5], [1953.0, 1981.5]] captured_at = "2026-09-10T09:10:41Z" samples = [ - { station = 0, point = 0, count1 = [41290, 41291], count2 = [51728, 51727] }, - { station = 0, point = 1, count1 = [40877, 40878], count2 = [51102, 51101] }, - { station = 0, point = 2, count1 = [43166, 43167], count2 = [75717, 75716] }, - { station = 0, point = 3, count1 = [42810, 42811], count2 = [75104, 75103] }, + { station = 2, point = 0, count1 = [41290, 41291], count2 = [51728, 51727] }, + { station = 2, point = 1, count1 = [40877, 40878], count2 = [51102, 51101] }, + { station = 2, point = 2, count1 = [43166, 43167], count2 = [75717, 75716] }, + { station = 2, point = 3, count1 = [42810, 42811], count2 = [75104, 75103] }, ] [[station]] -index = 0 +index = 2 solved_from = "direct" points = 4 residual_mm = 0.0 -valid_mm = [0, 0, 3330, 4000] +valid_mm = [0, 0, 2500, 4000] homography = [[1523.4, -38.2, 1012.7], [41.9, 1531.8, 988.3], [0.2134, -0.0871, 1.0]] [[station]] -index = 1 +index = 8 solved_from = "direct" points = 4 residual_mm = 0.0 -valid_mm = [0, 0, 3330, 4000] +valid_mm = [800, 0, 3330, 4000] homography = [[-1480.25, 12.5, 2950.0], [-20.75, 1502.125, 1010.5], [-0.1875, 0.0625, 1.0]] """ -# One 84-byte swrmt_lh2_calibration_data_t per station: station mask 0x3. +# One 84-byte swrmt_lh2_calibration_data_t per station: stations 2 and 8 +# (channels 3 and 9), station mask 0x0104, each with its own rectangle, so a +# packer that keys by position or writes the union into every message fails. MESSAGE_HEX = [ ( - "0300000000000000cd6cbe44cdcc18c2cd2c7d449a9927429a79bf4433137744" - "88855a3e7c61b2bd0000803f0000000000000000020d0000a00f000063343035" - "2d6172656e6100000000000080285c9b7db82732" + "0401000002000000cd6cbe44cdcc18c2cd2c7d449a9927429a79bf4433137744" + "88855a3e7c61b2bd0000803f0000000000000000c4090000a00f000063343035" + "2d6172656e610000000000009b12f56f622f0fb6" ), ( - "03000000010000000008b9c400004841006038450000a6c100c4bb4400a07c44" - "000040be0000803d0000803f0000000000000000020d0000a00f000063343035" - "2d6172656e6100000000000080285c9b7db82732" + "04010000080000000008b9c400004841006038450000a6c100c4bb4400a07c44" + "000040be0000803d0000803f2003000000000000020d0000a00f000063343035" + "2d6172656e610000000000009b12f56f622f0fb6" ), ] # The same file without [site] and [validity], so each reader's defaults reach # the wire: site "default" and valid_mm [0, 0, 10000, 10000]. -DEFAULTS_FIXTURE_ID = "4e0d138b62b57aa5" +DEFAULTS_FIXTURE_ID = "87453fc957d6f0ef" DEFAULTS_FIXTURE_TOML = ( FIXTURE_TOML.replace( @@ -79,19 +81,20 @@ "", ) .replace("[validity]\nvalid_mm = [0, 0, 3330, 4000]\n\n", "") - .replace("valid_mm = [0, 0, 3330, 4000]", "valid_mm = [0, 0, 10000, 10000]") + .replace("valid_mm = [0, 0, 2500, 4000]", "valid_mm = [0, 0, 10000, 10000]") + .replace("valid_mm = [800, 0, 3330, 4000]", "valid_mm = [0, 0, 10000, 10000]") .replace(f'id = "{FIXTURE_ID}"', f'id = "{DEFAULTS_FIXTURE_ID}"') ) DEFAULTS_MESSAGE_HEX = [ ( - "0300000000000000cd6cbe44cdcc18c2cd2c7d449a9927429a79bf4433137744" + "0401000002000000cd6cbe44cdcc18c2cd2c7d449a9927429a79bf4433137744" "88855a3e7c61b2bd0000803f0000000000000000102700001027000064656661" - "756c740000000000000000004e0d138b62b57aa5" + "756c7400000000000000000087453fc957d6f0ef" ), ( - "03000000010000000008b9c400004841006038450000a6c100c4bb4400a07c44" + "04010000080000000008b9c400004841006038450000a6c100c4bb4400a07c44" "000040be0000803d0000803f0000000000000000102700001027000064656661" - "756c740000000000000000004e0d138b62b57aa5" + "756c7400000000000000000087453fc957d6f0ef" ), ] diff --git a/dotbot/tests/test_calibration_lighthouse2.py b/dotbot/tests/test_calibration_lighthouse2.py index 158ffb93..d76e6163 100644 --- a/dotbot/tests/test_calibration_lighthouse2.py +++ b/dotbot/tests/test_calibration_lighthouse2.py @@ -730,16 +730,17 @@ def test_a_message_carries_the_matrix_as_float32_and_the_site_fields(tmp_path): calibration = _wire_fixture(tmp_path) message = calibration_messages(calibration)[1] mask, index = struct.unpack_from(" Date: Sun, 4 Oct 2026 03:59:08 +0200 Subject: [PATCH 5/5] dotbot: read the advertised LH2 station mask in 16 bits Breaking wire change, landing with DotBot-libs' 16-bit calibrated field: every advertisement field after the mask moves by one byte and the vendored dotbot_control.wasm is rebuilt at ABI 3 from that change. With the one-byte mask, a calibration naming station 8 or above read every sandbox robot as missing it and resent homographies on each advert. AI-assisted: Claude Opus 5.5 --- dotbot/dotbot_simulator.py | 10 +++++----- dotbot/models.py | 2 +- dotbot/protocol.py | 6 +++--- dotbot/sim/core.py | 4 ++-- dotbot/sim/dotbot_control.json | 6 +++--- dotbot/sim/dotbot_control.wasm | Bin 41053 -> 43583 bytes dotbot/tests/test_controller.py | 13 ++++--------- dotbot/tests/test_dotbot_simulator.py | 3 ++- dotbot/tests/test_protocol.py | 4 ++-- dotbot/tests/test_sim_core.py | 17 ++++++++++------- 10 files changed, 32 insertions(+), 33 deletions(-) diff --git a/dotbot/dotbot_simulator.py b/dotbot/dotbot_simulator.py index efc93f7e..efcb9c44 100644 --- a/dotbot/dotbot_simulator.py +++ b/dotbot/dotbot_simulator.py @@ -61,8 +61,8 @@ # How far apart `--robots N` puts a generated fleet FLEET_PITCH_MM = 200 # The stations a simulated robot holds when neither its world file nor the -# controller's calibration says: all eight -CALIBRATED_ALL = 0xFF +# controller's calibration says: all sixteen +CALIBRATED_ALL = 0xFFFF # The heading of every robot of a generated fleet: up (-y), 0 facing +y FLEET_FACING = 180 @@ -113,7 +113,7 @@ class SimulatedDotBotSettings(BaseModel): estimator starts out tracking; without one the robot faces +y and starts with no heading, as a real robot does from boot. `calibrated` is the bitmask of stations the robot holds a homography for; without one it - holds the stations of the controller's calibration, all eight when none + holds the stations of the controller's calibration, all sixteen when none is loaded. """ @@ -522,7 +522,7 @@ def __init__( ] calibrated = CALIBRATED_ALL if calibrated is None else calibrated self._calibrated = [ - (calibrated if s.calibrated is None else s.calibrated) & 0xFF + (calibrated if s.calibrated is None else s.calibrated) & 0xFFFF for s in settings ] self._dotbot_modes = [s.network_mode for s in settings] @@ -725,7 +725,7 @@ def _gru_residual(self, index: int, model, report): def _advertise(self, index: int, packet: bytearray): """Send robot `index`'s advertisement, as its firmware encoded it.""" # The calibration bitmask is the node's, not the control core's - packet[1] = self._calibrated[index] + packet[1:3] = self._calibrated[index].to_bytes(2, "little") self._counts[:, index] = 0 frame = Frame(header=self._headers[index], packet=Packet.from_bytes(packet)) if self._dotbot_modes[index] == SimulatedNetworkMode.MARI: diff --git a/dotbot/models.py b/dotbot/models.py index 219b6f74..ff3c8d37 100644 --- a/dotbot/models.py +++ b/dotbot/models.py @@ -684,7 +684,7 @@ class DotBotModel(BaseModel): axle_position: Optional[DotBotLH2Position] = None # the robot's own estimate # Where the robot has been, oldest first trail: List[Union[DotBotLH2Position, DotBotGPSPosition]] = [] - calibrated: int = 0x00 # Bitmask: first lighthouse = 0x01, second lighthouse = 0x02 + calibrated: int = 0x00 # bit i: the robot holds station i's homography battery: float = 3.0 # Voltage in Volts diff --git a/dotbot/protocol.py b/dotbot/protocol.py index cd9be916..c30f1cd3 100644 --- a/dotbot/protocol.py +++ b/dotbot/protocol.py @@ -25,7 +25,7 @@ # The advertised `calibrated` bitmask of a robot that holds no per-station # homographies at all, such as one running a sandbox app. -CALIBRATED_NOT_APPLICABLE = 0xFF +CALIBRATED_NOT_APPLICABLE = 0xFFFF class PayloadType(IntEnum): @@ -114,7 +114,7 @@ class PayloadDotBotAdvertisement(Payload): metadata: list[PayloadFieldMetadata] = dataclasses.field( default_factory=lambda: [ - PayloadFieldMetadata(name="calibrated", disp="cal."), + PayloadFieldMetadata(name="calibrated", disp="cal.", length=2), PayloadFieldMetadata(name="direction", disp="dir.", length=2, signed=True), PayloadFieldMetadata(name="pos_x", disp="x", length=4), PayloadFieldMetadata(name="pos_y", disp="y", length=4), @@ -140,7 +140,7 @@ class PayloadDotBotAdvertisement(Payload): ] ) - calibrated: int = 0x00 # Bitmask: first lighthouse = 0x01, second lighthouse = 0x02 + calibrated: int = 0x00 # bit i: the robot holds station i's homography direction: int = 0xFFFF pos_x: int = 0xFFFFFFFF pos_y: int = 0xFFFFFFFF diff --git a/dotbot/sim/core.py b/dotbot/sim/core.py index 3baafcc3..e9c121bf 100644 --- a/dotbot/sim/core.py +++ b/dotbot/sim/core.py @@ -21,7 +21,7 @@ import numpy as np -ABI_VERSION = 2 +ABI_VERSION = 3 WASM_PATH = Path(__file__).with_name("dotbot_control.wasm") MANIFEST_PATH = Path(__file__).with_name("dotbot_control.json") @@ -81,7 +81,7 @@ ("max_speed_10mm", "u1"), ] ) -ADVERTISEMENT_BYTES = 42 +ADVERTISEMENT_BYTES = 43 # fleet_geometry_t: drv/geometry.h as the core was built, mm and degrees GEOMETRY = np.dtype( [ diff --git a/dotbot/sim/dotbot_control.json b/dotbot/sim/dotbot_control.json index af6528d3..c2a38030 100644 --- a/dotbot/sim/dotbot_control.json +++ b/dotbot/sim/dotbot_control.json @@ -1,8 +1,8 @@ { "file": "dotbot_control.wasm", "source": "https://github.com/DotBots/DotBot-libs", - "commit": "48cc06bff499db56be1373cd3bf5c852084adddd", + "commit": "01976927d5a6e47e9774875547b0d0ad0b5680d7", "build": "wasm/fetch-wasi-sdk.sh && make wasm (wasi-sdk 34.0)", - "abi_version": 2, - "sha256": "53df5c73421442d85b306514d403e968d32289a0e823ed2a12825fb2800a5177" + "abi_version": 3, + "sha256": "e602b8bbbf7ebadbb099b1c217429f179b8903a0d0abcd5f6a92793a667220e9" } diff --git a/dotbot/sim/dotbot_control.wasm b/dotbot/sim/dotbot_control.wasm index 5be0d427b9e6096244c2978c195bd3089e78f80d..6464f5bf14cb87f06e2fef59be24101b47b48b73 100755 GIT binary patch delta 6812 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 O@3h6=%DUgLivAk}eKfuR diff --git a/dotbot/tests/test_controller.py b/dotbot/tests/test_controller.py index d3295d37..88fba246 100644 --- a/dotbot/tests/test_controller.py +++ b/dotbot/tests/test_controller.py @@ -1597,17 +1597,12 @@ async def test_a_resend_that_fails_drops_the_command(controller, clock): def _old_calibration_settings(tmp_path, **kwargs): - """Settings loading the wire fixture, made 2026-09-10, by path, with its - stations renumbered 0 and 1: a robot advertises the stations it holds in - one byte, stations 0 to 7.""" + """Settings loading the wire fixture, made 2026-09-10, by path: stations + 2 and 8.""" from dotbot.tests.lh2_wire_fixture import FIXTURE_TOML path = tmp_path / "calibration.toml" - path.write_text( - FIXTURE_TOML.replace("index = 2\n", "index = 0\n").replace( - "index = 8\n", "index = 1\n" - ) - ) + path.write_text(FIXTURE_TOML) return ControllerSettings( port="/dev/null", baudrate=115200, @@ -1654,7 +1649,7 @@ def test_robots_holding_stations_the_calibration_does_not_solve_are_warned_once( _old_calibration_settings(tmp_path, lh2_calibration_max_age_days=0) ) solved = sorted(station.index for station in controller.lh2_calibration) - first, second = sorted(set(range(8)) - set(solved))[:2] + first, second = sorted(set(range(16)) - set(solved))[-2:] held = sum(1 << index for index in solved) with capture_logs() as logs: for bot in (BOT, BOT + 1, BOT + 2): diff --git a/dotbot/tests/test_dotbot_simulator.py b/dotbot/tests/test_dotbot_simulator.py index e35f6a55..e93f243f 100644 --- a/dotbot/tests/test_dotbot_simulator.py +++ b/dotbot/tests/test_dotbot_simulator.py @@ -562,7 +562,8 @@ def test_a_robot_given_a_heading_starts_tracking_it_where_it_was_put(tmp_path): @pytest.mark.parametrize( - "calibrated, expected", [(None, [0xFF, 0x05]), (0b11, [0b11, 0x05])] + "calibrated, expected", + [(None, [0xFFFF, 0x05]), (0b11, [0b11, 0x05]), (0x0104, [0x0104, 0x05])], ) def test_a_robot_holds_the_controllers_stations_unless_its_file_says( calibrated, expected diff --git a/dotbot/tests/test_protocol.py b/dotbot/tests/test_protocol.py index 0d9ca5de..47e4b810 100644 --- a/dotbot/tests/test_protocol.py +++ b/dotbot/tests/test_protocol.py @@ -981,7 +981,7 @@ def test_advertisement_waypoint_report(): report=True, ) data = advert.to_bytes() - assert len(data) == 41 + assert len(data) == 42 parsed = PayloadDotBotAdvertisement().from_bytes(data) assert parsed.has_report assert (parsed.waypoint_idx, parsed.waypoints_status, parsed.waypoints_reason) == ( @@ -993,7 +993,7 @@ def test_advertisement_waypoint_report(): assert (parsed.axle_x, parsed.axle_y) == (1234, 567) legacy = PayloadDotBotAdvertisement(direction=-90, waypoint_idx=1).to_bytes() - assert len(legacy) == 33 + assert len(legacy) == 34 parsed = PayloadDotBotAdvertisement().from_bytes(legacy) assert not parsed.has_report assert parsed.waypoint_idx == 1 and parsed.batch_id == 0 diff --git a/dotbot/tests/test_sim_core.py b/dotbot/tests/test_sim_core.py index 31319399..f89d4790 100644 --- a/dotbot/tests/test_sim_core.py +++ b/dotbot/tests/test_sim_core.py @@ -179,10 +179,10 @@ def test_the_advertisement_starts_the_encoder_deltas_over(): second = core.advertisement(0, 2900) assert len(first) == control.ADVERTISEMENT_BYTES # Type, calibration, direction, x, y, battery, duties, mode, then the deltas - assert int.from_bytes(first[17:21], "little", signed=True) == 7 - assert int.from_bytes(first[21:25], "little", signed=True) == -3 - assert int.from_bytes(second[17:25], "little") == 0 - assert int.from_bytes(first[12:14], "little") == 2900 + assert int.from_bytes(first[18:22], "little", signed=True) == 7 + assert int.from_bytes(first[22:26], "little", signed=True) == -3 + assert int.from_bytes(second[18:26], "little") == 0 + assert int.from_bytes(first[13:15], "little") == 2900 def test_a_seeded_robot_tracks_the_pose_it_is_given(): @@ -206,7 +206,10 @@ def test_the_batched_advertisements_are_those_the_step_asked_for(): indices, packets = core.advertisements(np.array([3000, 3001, 3002, 3003])) assert indices.tolist() == [1, 3] assert packets.shape == (2, control.ADVERTISEMENT_BYTES) - assert [int.from_bytes(p[12:14], "little") for p in packets] == [3001, 3003] - assert [int.from_bytes(p[17:21], "little") for p in packets] == [11, 13] + assert [int.from_bytes(p[13:15], "little") for p in packets] == [3001, 3003] + assert [int.from_bytes(p[18:22], "little") for p in packets] == [11, 13] empty_indices, empty_packets = core.advertisements(np.zeros(4)) - assert empty_indices.size == 0 and empty_packets.shape == (0, 42) + assert empty_indices.size == 0 and empty_packets.shape == ( + 0, + control.ADVERTISEMENT_BYTES, + )