Skip to content

Maploc: mapping & localization as a robotd-hosted subcrate β€” and why it never worked - #127

Open
apirrone wants to merge 19 commits into
mainfrom
maploc
Open

Maploc: mapping & localization as a robotd-hosted subcrate β€” and why it never worked#127
apirrone wants to merge 19 commits into
mainfrom
maploc

Conversation

@apirrone

Copy link
Copy Markdown
Contributor

Absorbs microduck_maploc_rs (already Rust) as the maploc/ subcrate β€” and, per the brief, spends most of its effort on why the results were "kinda ok but noisy" and relocalization never worked. Every fix below was found and verified offline against the prototype's own recorded .mdlg sessions, using a replay bench that ships with the crate.

The bugs, each with the measurement that exposed it

  1. Sensor-origin asymmetry. Maps were inked from the sensor world pose (10–15 cm ahead of the body, moving with head yaw), but MCL and brute-force relocalize scored beams from the body pose. Scan is now per-beam origin β†’ endpoint pairs; every consumer derives world beams identically. The shape also makes scans composable β€” frames at one body pose with different head yaws merge exactly.
  2. Off-map cherry-picking. Relocalize/MCL silently skipped out-of-bounds beams, so a wrong pose that threw 95% of its scan off the map was scored on the agreeing remainder β€” measured beating the true pose with mean residuals near zero from a tenth of the beams. Every beam now counts (off-map = full clamp).
  3. See-through degeneracy. Endpoints-only scoring let poses dump whole scans into dense wall blobs at ~0 residual. One mid-ray occupancy sample per beam breaks it.
  4. The ~9 cm map noise floor. Stop-and-scan inked every raw frame; walking-passer-by transients and the sensor's far-range noise tail set a floor above every downstream gate (loop closures gated at 10 cm never fired; the true pose scored worse than the 5 cm relocalize acceptance β€” the "never locks" mechanism, jointly with Fix two health-check defects slice 1 shippedΒ #2 accepting lies). New accumulator: a still window's frames vote per endpoint cell; beams confirmed by β‰₯3 distinct frames survive; ranges cap at 2 m; one wide composite comes out per stop.
  5. The correction that never reached tracking. The current submap's pose-graph node was created with no edge attached β€” invisible to the optimizer β€” and the post-closure tracked-pose correction rides that node. Closures moved every frozen anchor while live tracking sailed on uncorrected (the prototype wired it identically). Nodes now chain at open time; pinned by a synthetic walked-loop test.
  6. Over-closing / room-folding. 43 "closures" in one 5-minute session folded the two recorded rooms onto each other: aliased matches pass every local gate in blobby rooms. Three data-derived defenses: witnesses are the largest scans (β‰₯150 beams β€” the 12-beam scraps were vetoing the 6000-beam composites' consensus), a closure needs two agreeing strong witnesses, and its correction must be plausible for the odometry drift accumulable over the gap it spans. Also: coverage is measured against beams landing in cells the target submap actually observed.

Bench result on the recorded room: brute-force relocalize went 0/13 β†’ 6/20 probes within 30 cm/20Β° β€” every miss a narrow-wedge probe, every wide head-sweep composite a hit β€” and MCL stopped locking onto wrong clusters. The clear operational lesson, now quantified: relocalization wants a deliberate head sweep, which the robot can do on demand (follow-up).

Integration

  • [maploc] in robotd.toml, off by default (documented in deploy/robotd.toml): enabled, mode = "stop_and_scan" | "continuous" (the requested toggle), map_path, wipe_on_boot. Sessions autosave once a minute + on shutdown, restore on boot.
  • robotd hosts it on a niced (+10) worker thread: the control loop pays one try_send per tick; depth frames arrive via a reconnecting subscription to tofd's socket; frames go through the Posture-aware (lean-corrected) reprojection.
  • robot.map (API v12 β†’ v13): a robot.state-style subscription; map.frame notifications carry the trinary grid as base64 (tiny RFC 4648 module in the proto), map-frame pose, tracking flag, submap/loop counts, at 1 Hz while subscribed.
  • Monitor: the path panel becomes " map " when frames flow β€” walls in braille, free space a sparse stipple, the robot its yellow marker (magenta ? while searching) β€” and stays the odometry track otherwise. m puts either view over the whole terminal.
  • Trimmed from the satellite: TCP telemetry (later, per decision), rand/rayon (pinned xoshiro β€” relocalize runs replay bit-for-bit β€” and std::thread::scope), the fixed-mount projection.

Testing

  • Workspace green (46 suites), clippy clean. New: 15+ maploc unit tests including the synthetic walked-loop closure test, accumulator voting, session round-trip (format bumped to v2 for the Scan reshape), b64 RFC vectors, map render.
  • Replay bench: cargo run -p maploc --release --example replay -- <session.mdlg> β€” builds the map (PGM dump), measures window self-consistency, probes relocalization with truth-pose scores. The prototype's recorded sessions in ~/MISC/microduck_maploc-rs/sessions/ drive it.
  • Hardware: enable [maploc], robotctl monitor, walk stop-and-scan around a room and watch the map build live; restart robotd and see the session restore. CPU: watch top β€” the worker is niced, and the loop's hz in the monitor should not move.

Follow-ups noted, not included: head-sweep-on-relocalize (the robot pans its own head to build the wide composite that provably relocalizes), robot.look-style goal/navigation (planner + follower are ported and dormant), MCL boot relocalize wiring in robotd (the crate machinery is fixed and tested; the robotd flow wants the head sweep first).

πŸ€– Generated with Claude Code

apirrone and others added 4 commits August 21, 2026 16:19
The satellite was already Rust; this is integration plus the bug hunt
it never got, run offline against its own recorded sessions via the new
replay bench (examples/replay.rs + kinematics dev-dep). Found and fixed,
each with the measurement that exposed it:

- Sensor-origin asymmetry: maps were inked from the sensor pose but MCL
  and relocalize scored beams from the body pose β€” a 10-15 cm systematic
  disagreement on a head-mounted sensor. Scan is now per-beam
  origin→endpoint pairs; every consumer derives world beams identically.
  The pair shape also makes scans composable: frames at one body pose
  with different head yaws merge exactly.

- Off-map cherry-picking: relocalize and MCL skipped out-of-bounds
  beams, so a wrong pose that threw 95% of its scan off the map was
  scored on the agreeing remainder β€” measured beating the true pose
  with mean residuals near zero from a tenth of the beams. Every beam
  now counts (off-map = full clamp).

- See-through degeneracy: endpoints-only scoring let poses dump whole
  scans into dense wall blobs at ~0 residual. One mid-ray occupancy
  sample per beam breaks it.

- The map noise floor itself: stop-and-scan inked every raw frame, so
  walking-person transients and the sensor's far-range noise tail set a
  ~9 cm floor that no gate survived (loop closures gated at 10 cm never
  fired; the true pose scored above the 5 cm relocalize acceptance).
  New `accumulator`: a still window's frames vote per endpoint cell,
  beams confirmed by β‰₯3 distinct frames survive, ranges cap at 2 m, one
  wide composite comes out. Bench result on the recorded room: probe
  hit-rate 0/13 β†’ 6/20 pre-closure, and loop closures fire at all.

- Witness quality in the loop closer: verification picked scans spread
  across the buffer, so 12-beam scraps vetoed the consensus of
  6000-beam composites β€” and after that fix, a lone weak witness could
  still swear in a wrong edge that warped every anchor. Witnesses are
  now the largest scans, minimum 150 beams, no fallback.

Trimmed relative to the satellite: no TCP telemetry (the monitor will
read robotd's socket), no rand/rayon (pinned xoshiro β€” a relocalize run
replays bit-for-bit β€” and std::thread::scope), no fixed-mount model
(kinematics::tof::flatten projects through the live head FK and the
IMU-levelled frame, with the new per-frame sensor origin). Session
format bumped to v2 for the Scan reshape.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Struct-init over post-default assignment, the kernel index-loop lint
allowed once at crate level with its reasoning (the loops mirror the
matrix maths they implement), and the loop-closure tests pass a config
that admits their single 64-beam raycasts past the witness gate that is
tuned for the accumulator's composites.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`pipeline::Slam` assembles submaps + graph + loop closure + optimizer
behind the calls a host makes (observe_odom / tick / integrate / render
/ save), so robotd and the replay bench drive the same code instead of
two copies that drift. The bench now runs on it.

Fixed on the way, each measured in the bench:

- The current submap's graph node was added with no edge attached β€” an
  unconnected node is invisible to the optimizer, and the tracked-pose
  correction after a loop closure rides that node. Closures moved every
  frozen anchor while live tracking sailed on uncorrected; the prototype
  wired it the same way. Nodes are now chained at open time. Pinned by
  a synthetic walked-loop test: drift 0.27 m, closure fires, tracking
  ends within a quarter of it.

- Loop-closure coverage was measured against every valid beam; a 360Β°
  composite matched against a small submap parks most beams outside
  what that submap ever OBSERVED, and honest matches got vetoed as
  low-coverage. The denominator is now beams landing in observed cells.

- Over-closing folded the two recorded rooms onto each other (43
  "closures" in a 5-minute session): aliased matches in blobby rooms
  pass every local-quality gate. Two defenses, both from the data: a
  closure needs at least two strong witnesses in agreement (lone-witness
  closures were the wrong ones), and its correction must be plausible
  for the odometry drift accumulable over the gap it spans.

Bench on the recorded room: 1 conservative closure, brute-force
relocalize 6/20 probes within 30 cm/20Β° β€” every miss a narrow-wedge
probe, every wide head-sweep composite a hit β€” and MCL no longer locks
onto wrong clusters (streak gates hold it at "searching", which is the
correct answer to ambiguous data).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
[maploc] in robotd.toml (off by default β€” mapping is the most CPU-hungry
thing the robot can do): enabled, mode = stop_and_scan | continuous,
map_path, wipe_on_boot. When on, robotd spawns a niced (+10) worker
thread driving maploc::pipeline::Slam; the control loop's entire cost is
one try_send per tick (odometry pose, gravity, trunk height, head
joints, the moving flag), and depth frames arrive from a tokio task
subscribed to tofd's socket like any other client, reconnecting with
backoff. Frames go through the Posture-aware reprojection; stop-and-scan
mode routes them through the still-window accumulator so only vetted
composites ink the map. Sessions autosave once a minute and on shutdown,
and restore on boot.

robot.map (API v13) is a subscription on robotd's socket like
robot.state: the answer says whether this robot maps, then map.frame
notifications carry the rendered grid β€” trinary cells as base64 (a
small hand-rolled RFC 4648 module in the proto, so no client needs a
dependency to read a map) β€” plus the map-frame pose, tracking state and
submap/loop counts, at 1 Hz while anyone listens.

robotctl monitor subscribes on its existing robotd connection: the path
panel becomes " map " when frames flow (walls in braille, free space a
sparse stipple, the robot its yellow marker β€” or a magenta ? while the
pose is not to be trusted), and stays the odometry track otherwise.
`m` puts either one over the whole terminal.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 21, 2026

Copy link
Copy Markdown

Coverage

72.85% lines on this branch, against a floor of 72%.

Per-file
Filename                              Regions    Missed Regions     Cover   Functions  Missed Functions  Executed       Lines      Missed Lines     Cover    Branches   Missed Branches     Cover
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
btd/src/adv.rs                             79                 0   100.00%           9                 0   100.00%          42                 0   100.00%           0                 0         -
btd/src/bluez.rs                          422               422     0.00%          32                32     0.00%         279               279     0.00%           0                 0         -
btd/src/chorale.rs                        346               246    28.90%          23                13    43.48%         230               171    25.65%           0                 0         -
btd/src/framing.rs                        217                 7    96.77%          17                 1    94.12%          97                 5    94.85%           0                 0         -
btd/src/gatt.rs                             3                 0   100.00%           1                 0   100.00%           3                 0   100.00%           0                 0         -
btd/src/link.rs                            13                 0   100.00%           1                 0   100.00%          17                 0   100.00%           0                 0         -
btd/src/main.rs                            77                77     0.00%          11                11     0.00%          77                77     0.00%           0                 0         -
btd/src/pairing.rs                        149                 2    98.66%          12                 1    91.67%          81                 1    98.77%           0                 0         -
btd/src/route.rs                          291                26    91.07%          24                 0   100.00%         271                 9    96.68%           0                 0         -
btd/src/session.rs                       1118                71    93.65%          66                 5    92.42%         649                41    93.68%           0                 0         -
btd/src/upstream.rs                       297                43    85.52%          29                 8    72.41%         184                25    86.41%           0                 0         -
configd/src/bluez.rs                      441               441     0.00%          78                78     0.00%         297               297     0.00%           0                 0         -
configd/src/identity.rs                   111                 5    95.50%          12                 1    91.67%          52                 3    94.23%           0                 0         -
configd/src/main.rs                       494               494     0.00%          38                38     0.00%         338               338     0.00%           0                 0         -
configd/src/net.rs                        246                12    95.12%          38                 2    94.74%         181                 4    97.79%           0                 0         -
configd/src/nm.rs                         271               213    21.40%          43                39     9.30%         171               120    29.82%           0                 0         -
configd/src/pad.rs                        301                12    96.01%          33                 2    93.94%         186                 8    95.70%           0                 0         -
configd/src/power.rs                       30                30     0.00%           6                 6     0.00%          28                28     0.00%           0                 0         -
configd/src/store.rs                      350                25    92.86%          28                 2    92.86%         173                 9    94.80%           0                 0         -
configd/src/units.rs                      119               119     0.00%          18                18     0.00%          83                83     0.00%           0                 0         -
duck-control/src/bus.rs                   408               299    26.72%          33                21    36.36%         276               203    26.45%           0                 0         -
duck-control/src/fall.rs                  173                 0   100.00%          15                 0   100.00%         108                 0   100.00%           0                 0         -
duck-control/src/imu.rs                   370                 8    97.84%          20                 0   100.00%         195                 5    97.44%           0                 0         -
duck-control/src/io.rs                    127                19    85.04%          20                 5    75.00%         115                17    85.22%           0                 0         -
duck-control/src/model.rs                 113                 1    99.12%          14                 0   100.00%          68                 0   100.00%           0                 0         -
duck-control/src/obs.rs                   260                 3    98.85%          23                 1    95.65%         174                 5    97.13%           0                 0         -
duck-control/src/policy.rs                347               239    31.12%          41                24    41.46%         229               150    34.50%           0                 0         -
duck-control/src/safety.rs                410                12    97.07%          31                 2    93.55%         274                 6    97.81%           0                 0         -
duck-detect/src/bin/duck-bench.rs         306               306     0.00%          16                16     0.00%         170               170     0.00%           0                 0         -
duck-detect/src/lib.rs                    512                10    98.05%          24                 0   100.00%         274                 6    97.81%           0                 0         -
duck-detect/src/onnx.rs                    96                96     0.00%           5                 5     0.00%          52                52     0.00%           0                 0         -
duck-detect/src/rknn.rs                   267               237    11.24%          15                11    26.67%         216               195     9.72%           0                 0         -
duck-ipc-proto/src/lib.rs                2023               175    91.35%         135                15    88.89%        1448               103    92.89%           0                 0         -
duckctl/src/main.rs                      1900               825    56.58%         120                41    65.83%        1169               508    56.54%           0                 0         -
kinematics/src/hand.rs                    373                 2    99.46%          21                 2    90.48%         172                 2    98.84%           0                 0         -
kinematics/src/head.rs                    381                 9    97.64%          20                 0   100.00%         188                 4    97.87%           0                 0         -
kinematics/src/lib.rs                     229                11    95.20%          19                 2    89.47%         115                 7    93.91%           0                 0         -
kinematics/src/math.rs                    171                 0   100.00%          16                 0   100.00%          79                 0   100.00%           0                 0         -
kinematics/src/mjcf.rs                    225                32    85.78%          17                 1    94.12%         132                15    88.64%           0                 0         -
kinematics/src/tof.rs                     414                22    94.69%          16                 1    93.75%         226                15    93.36%           0                 0         -
maploc/src/accumulator.rs                 303                 6    98.02%          15                 1    93.33%         154                 5    96.75%           0                 0         -
maploc/src/follower.rs                    155                 5    96.77%          11                 1    90.91%          99                 7    92.93%           0                 0         -
maploc/src/global_render.rs               328                 6    98.17%           5                 0   100.00%         168                 3    98.21%           0                 0         -
maploc/src/grid.rs                        869                79    90.91%          44                 3    93.18%         512                33    93.55%           0                 0         -
maploc/src/loop_closer.rs                 630                18    97.14%          13                 0   100.00%         379                39    89.71%           0                 0         -
maploc/src/mapper.rs                      940                57    93.94%          35                 3    91.43%         692                54    92.20%           0                 0         -
maploc/src/mcl.rs                        1125               214    80.98%          45                12    73.33%         711               145    79.61%           0                 0         -
maploc/src/optimizer.rs                   405                 3    99.26%           9                 0   100.00%         239                 7    97.07%           0                 0         -
maploc/src/pipeline.rs                    475                45    90.53%          19                 3    84.21%         289                21    92.73%           0                 0         -
maploc/src/planner.rs                     501                47    90.62%          16                 0   100.00%         307                27    91.21%           0                 0         -
maploc/src/pose_graph.rs                  167                11    93.41%          15                 2    86.67%          92                 9    90.22%           0                 0         -
maploc/src/record.rs                      194                20    89.69%           7                 0   100.00%         115                 8    93.04%           0                 0         -
maploc/src/relocalize.rs                  524                13    97.52%          19                 1    94.74%         332                 8    97.59%           0                 0         -
maploc/src/replay.rs                      472                49    89.62%          17                 1    94.12%         276                53    80.80%           0                 0         -
maploc/src/rng.rs                         125                 0   100.00%          10                 0   100.00%          73                 0   100.00%           0                 0         -
maploc/src/scan_matcher.rs                413                 8    98.06%           7                 0   100.00%         228                 6    97.37%           0                 0         -
maploc/src/session.rs                     273                27    90.11%           7                 1    85.71%         151                13    91.39%           0                 0         -
maploc/src/submap.rs                      332                14    95.78%          22                 0   100.00%         173                 5    97.11%           0                 0         -
maploc/src/submap_manager.rs              335                 4    98.81%          22                 1    95.45%         210                 4    98.10%           0                 0         -
mediad/src/config.rs                       90                 5    94.44%           7                 1    85.71%          46                 4    91.30%           0                 0         -
mediad/src/detect.rs                      293               222    24.23%          13                 9    30.77%         200               145    27.50%           0                 0         -
mediad/src/exposure.rs                    412               161    60.92%          28                 8    71.43%         279               110    60.57%           0                 0         -
mediad/src/main.rs                        245               245     0.00%           7                 7     0.00%         158               158     0.00%           0                 0         -
mediad/src/pipeline.rs                    977               909     6.96%          62                56     9.68%         624               578     7.37%           0                 0         -
mediad/src/producer.rs                    161                33    79.50%          20                 5    75.00%         109                20    81.65%           0                 0         -
mediad/src/route.rs                       115                13    88.70%           8                 0   100.00%          86                 6    93.02%           0                 0         -
mediad/src/session.rs                     493                12    97.57%          30                 0   100.00%         290                 8    97.24%           0                 0         -
mediad/src/upstream.rs                    123                23    81.30%          10                 1    90.00%          82                17    79.27%           0                 0         -
mediad/src/web.rs                         112                26    76.79%          14                 4    71.43%          76                17    77.63%           0                 0         -
odometry/src/lib.rs                       321                 5    98.44%          22                 1    95.45%         192                 7    96.35%           0                 0         -
padd/src/main.rs                          446               446     0.00%           9                 9     0.00%         315               315     0.00%           0                 0         -
padd/src/tap.rs                           693               455    34.34%          42                25    40.48%         419               267    36.28%           0                 0         -
pet-detect/src/bin/detect.rs               61                61     0.00%           1                 1     0.00%          38                38     0.00%           0                 0         -
pet-detect/src/bin/features.rs             46                46     0.00%           2                 2     0.00%          20                20     0.00%           0                 0         -
pet-detect/src/lib.rs                     358               180    49.72%          27                13    51.85%         217               115    47.00%           0                 0         -
pet-detect/src/worker.rs                  293               293     0.00%          17                17     0.00%         223               223     0.00%           0                 0         -
robotctl/src/configure.rs                1721               345    79.95%          89                14    84.27%         850               216    74.59%           0                 0         -
robotctl/src/duck.rs                     1211               144    88.11%          63                 1    98.41%         604                47    92.22%           0                 0         -
robotctl/src/main.rs                     3097              1611    47.98%         178                86    51.69%        2219              1133    48.94%           0                 0         -
robotctl/src/monitor.rs                  4149               825    80.12%         220                42    80.91%        2602               520    80.02%           0                 0         -
robotctl/src/path_map.rs                  703                72    89.76%          40                 2    95.00%         389                46    88.17%           0                 0         -
robotctl/src/show.rs                      714                46    93.56%          31                 1    96.77%         509                25    95.09%           0                 0         -
robotd-params/src/lib.rs                  771                56    92.74%          65                 5    92.31%         596                39    93.46%           0                 0         -
robotd-params/src/registry.rs             179                10    94.41%          13                 2    84.62%         133                17    87.22%           0                 0         -
robotd/src/chorale.rs                    1366                30    97.80%          71                 4    94.37%         826                20    97.58%           0                 0         -
robotd/src/control.rs                     297               260    12.46%          17                12    29.41%         257               210    18.29%           0                 0         -
robotd/src/intents.rs                     379                60    84.17%          38                 6    84.21%         276                39    85.87%           0                 0         -
robotd/src/main.rs                       4394              1450    67.00%         191                50    73.82%        2748               968    64.77%           0                 0         -
robotd/src/maploc.rs                      566               566     0.00%          19                19     0.00%         395               395     0.00%           0                 0         -
robotd/src/soc.rs                          47                25    46.81%           4                 1    75.00%          30                15    50.00%           0                 0         -
robotd/src/sound.rs                       968               688    28.93%          50                33    34.00%         565               416    26.37%           0                 0         -
robotd/src/theremin.rs                    472                71    84.96%          32                 6    81.25%         271                38    85.98%           0                 0         -
sounds/src/chorale/beat.rs                403                16    96.03%          26                 1    96.15%         246                12    95.12%           0                 0         -
sounds/src/chorale/midi.rs               1047                80    92.36%          44                 2    95.45%         581                43    92.60%           0                 0         -
sounds/src/chorale/mod.rs                1423                57    95.99%          83                 3    96.39%         812                41    94.95%           0                 0         -
sounds/src/chorale/text.rs                700                60    91.43%          41                12    70.73%         376                17    95.48%           0                 0         -
sounds/src/lib.rs                         162                30    81.48%          14                 6    57.14%          84                18    78.57%           0                 0         -
sounds/src/main.rs                        463               463     0.00%          20                20     0.00%         240               240     0.00%           0                 0         -
sounds/src/personality.rs                 174                 0   100.00%           6                 0   100.00%          84                 0   100.00%           0                 0         -
sounds/src/rng.rs                         173                 0   100.00%          18                 0   100.00%          96                 0   100.00%           0                 0         -
sounds/src/stream.rs                      719                 7    99.03%          46                 1    97.83%         420                 6    98.57%           0                 0         -
sounds/src/synth.rs                       353                 7    98.02%          29                 0   100.00%         197                 3    98.48%           0                 0         -
sounds/src/voices.rs                      679                 2    99.71%          26                 0   100.00%         353                 2    99.43%           0                 0         -
test-support/src/lib.rs                   272                 2    99.26%          22                 0   100.00%         167                 0   100.00%           0                 0         -
tof/src/lib.rs                             88                 0   100.00%           9                 0   100.00%          54                 0   100.00%           0                 0         -
tof/src/main.rs                           478               438     8.37%          23                18    21.74%         303               281     7.26%           0                 0         -
tof/src/sensor.rs                         230               148    35.65%          22                15    31.82%         163               114    30.06%           0                 0         -
tof/src/status.rs                          76                 2    97.37%           7                 1    85.71%          58                 1    98.28%           0                 0         -
updater/src/config.rs                     428                24    94.39%          37                 4    89.19%         349                17    95.13%           0                 0         -
updater/src/engine.rs                    3202               436    86.38%         211                23    89.10%        2057               253    87.70%           0                 0         -
updater/src/faults.rs                      69                 7    89.86%           7                 0   100.00%          51                 0   100.00%           0                 0         -
updater/src/fsutil.rs                     104                30    71.15%          10                 6    40.00%          51                24    52.94%           0                 0         -
updater/src/hooks.rs                      454                15    96.70%          37                 2    94.59%         382                12    96.86%           0                 0         -
updater/src/ipc.rs                        793               235    70.37%          65                19    70.77%         509               133    73.87%           0                 0         -
updater/src/journal.rs                    832                78    90.62%          57                10    82.46%         483                53    89.03%           0                 0         -
updater/src/lib.rs                         43                13    69.77%           4                 0   100.00%          33                13    60.61%           0                 0         -
updater/src/main.rs                       568               240    57.75%          37                13    64.86%         420               154    63.33%           0                 0         -
updater/src/manifest.rs                   159                 6    96.23%          16                 0   100.00%         110                 1    99.09%           0                 0         -
updater/src/orphan.rs                     288                 6    97.92%          25                 0   100.00%         181                 3    98.34%           0                 0         -
updater/src/preflight.rs                  380                14    96.32%          46                 3    93.48%         282                 9    96.81%           0                 0         -
updater/src/reconcile.rs                  292                15    94.86%          25                 3    88.00%         172                 5    97.09%           0                 0         -
updater/src/robot.rs                      212                20    90.57%          31                 4    87.10%         128                10    92.19%           0                 0         -
updater/src/source/github.rs              474               169    64.35%          51                26    49.02%         304                99    67.43%           0                 0         -
updater/src/source/hf_hub.rs              136                72    47.06%          20                12    40.00%          81                41    49.38%           0                 0         -
updater/src/source/http.rs                319                82    74.29%          29                11    62.07%         260                88    66.15%           0                 0         -
updater/src/source/local.rs               346                37    89.31%          36                 9    75.00%         192                26    86.46%           0                 0         -
updater/src/source/mod.rs                  39                30    23.08%           4                 2    50.00%          30                24    20.00%           0                 0         -
updater/src/spawn.rs                       94                13    86.17%           5                 0   100.00%          49                 4    91.84%           0                 0         -
updater/src/store.rs                      593                41    93.09%          43                 7    83.72%         271                42    84.50%           0                 0         -
updater/src/transcript.rs                 518                42    91.89%          31                 3    90.32%         290                22    92.41%           0                 0         -
updater/src/verify.rs                     786                95    87.91%          55                18    67.27%         424                75    82.31%           0                 0         -
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
TOTAL                                   67888             17603    74.07%        4236              1130    73.32%       42306             11486    72.85%           0                 0         -

apirrone and others added 2 commits August 21, 2026 17:11
Field test one: enable maploc, stand the robot up, watch the monitor β€”
nothing. Three causes, three fixes:

- The still-window only integrated on the still→moving TRANSITION, so a
  robot standing still accumulated forever and never inked. Windows now
  flush every 3 s while the stand continues.

- Empty submaps froze on age β€” nine husks in the graph after a couple
  of minutes of standing, each a dead node dragging the optimizer.
  A submap with no content re-anchors at the current pose instead of
  freezing (TickOutcome::Reanchored; the pipeline moves its node and
  updates the inbound edge measurement to match).

- Nothing said what mapping was doing. map.frame now carries `windows`
  (integrated so far) and `still` (a window is accumulating right now);
  the panel caption reads "N windows Β· M submaps Β· K loops Β· scanning",
  so "no walls yet" and "no scans ever reached the map" stop looking
  identical. Window integration logs at info.

And per request: the odometry path draws in red β€” in the path view and
overlaid on the live map (same world frame until loop closures diverge
them, and worth seeing together even then).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Field test two: "0 windows Β· 71 submaps" β€” the 71 are husks restored
from the previous build's session, but the zero needs data, not another
theory. The worker now logs a status line every 5 s (odom samples,
frames received/kept, windows, the still verdict with the moving flag
and the 500 ms odometry deltas it was decided from, window size,
submaps), says when it connects to tofd, and no longer swallows the
connect error silently β€” that silence already cost an afternoon.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
apirrone and others added 11 commits August 21, 2026 17:32
Field test three, with the new status line: odom flowing, stillness
perfect, frames=0 β€” the depth feed never delivered a single frame. The
task was spawned on the control loop's runtime, which is deliberately
built time-only: no IO driver, so the UnixStream connect panicked the
task on its first poll, silently, inside a JoinHandle nobody reads.

The feed now runs on its own thread with its own current_thread runtime
built with enable_io β€” which is also simply better: tofd's socket I/O
and 15 Hz of JSON parsing never belonged on the control thread at all.
Niced like the worker.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Four field-test-two fixes:
 - the age rule only fires once the robot has moved 15 cm from the
   anchor β€” standing (or sitting) still accrues no drift to bound, and
   the old rule minted an identical submap every 8 s (24 -> 53 over one
   seated coffee break, all junk for the loop closer);
 - windows under 60 beams are discarded, not inked: a seated robot's
   3-second windows distilled to 2-27 beams of floor clutter;
 - a vetted window inks twice: one pass wrote log-odds 85 per wall cell
   and the wire frame calls a wall at 150, so a lap that stopped once
   per spot painted the whole walk invisibly β€” the 'scattered white
   points' were the rare twice-visited cells;
 - the map view's red path is now the tracked pose history, not raw
   odometry: after a session resume or a closure the two live in
   different frames, and the odometry path diverging from the robot
   marker reads as a bug when it is a frame.

Also: loop closures now log their correction, and a fruitless window
flush disarms the ripe timer instead of thrashing single-frame flushes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The moving flag fed maploc's stillness gate through
`twist_magnitude() > 0.0` β€” and a gamepad twist idles at a
not-quite-zero value below the walking threshold, so the flag stayed
latched true through an entire stop-and-scan lap: the robot stood,
odometry read zero, and mapping refused every stop (kept=124 frozen
for two minutes in the field-test journal). The policy already decides
standing versus walking; `moving` now follows that decision.

Also makes safeToRestart stop saying no to a robot that is only
holding a drifting stick.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…bench case

Three things, one commit because they are one design:

Mapper (maploc::mapper) β€” the mapping host loop moves out of robotd's
worker into the crate: stillness, window vetting, and a new
lost/relocalize state machine. Before a window inks it is scored against
the map at the tracked pose (relocalize::score_pose); a window the map
can judge but flatly contradicts flips tracking to lost β€” a kidnapped
robot's scans land in territory the map knows and disagree everywhere,
while a robot exploring a new room lands in cells the map cannot judge
and keeps mapping. While lost nothing inks, and every window becomes a
brute-force relocalize attempt (decimated to 256 beams β€” full composites
would cost seconds a try); an accepted pose snaps tracking back and
mapping resumes. The same watchdog heals a resumed session whose robot
moved while the daemon was down. robotd's worker is now a thin host:
channel in, log lines and map frames out.

Recorder (maploc::record, [maploc] record_dir) β€” .mdlg format v2:
everything the mapper consumes (odometry, gravity, trunk height, head,
moving/sitting, raw ToF zones) appended to a timestamped log, ~6 KB/s.

Ground-truth bench (examples/evaluate) β€” replays a v2 log through the
SAME Mapper against a tape-measured room (truth.toml, centimetres and
degrees): return-to-start error vs raw odometry, kidnap detection and
relocalization latency + pose error, map-vs-room wall statistics, PGMs
with the truth walls burned in. The protocol's kidnap marker is a SIT:
odometry cannot see a carry, but it cannot miss a sit β€” and the mapper
now refuses to map from sitting height anyway. room_lab.toml carries the
lab digitized from the CAD sketch; the start/kidnap poses are scaled off
the drawing and should be corrected with a tape measure.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replayed the first ground-truth recording (lap + sit-carry kidnap in the
tape-measured lab) through the bench until the kidnap recovered. Four
measured failure modes, four fixes:

 - The see-through clamp punished beams for hitting the wall they
   measured: a robot 10 cm from a divider had 4200 of 10800 beams
   'seeing through' it (midpoint of a short or grazing beam lands in
   the wall's own cells) β€” the false LOST at t=35 s. The watchdog is
   now endpoint-only (at a trusted pose there is no blob degeneracy to
   exploit); the search keeps the clamp but only for crossings well
   away from the beam's endpoint.
 - A young sparse map let a wrong relocalize basin score 0.022 and
   poisoned the whole run (return-to-start went 0.43 m / 95Β° while raw
   odometry was 0.14 m / 0.1Β°). A candidate now needs the NEXT window,
   carried by odometry, to confirm it β€” with a coverage floor, because
   a keyhole wall wedge aliases onto any wall at the same range
   (measured: 204/1680 kidnapped beams landed on old walls at residual
   0.005, 0.3 m from the truth).
 - A kidnapped stand vouched for itself: its first window painted the
   kidnapper's room and every later window 'agreed with the map'.
   Windows are now judged against a snapshot of the map from when the
   stand began, contradictions quarantine before they ink, and lost
   takes two consecutive contradicting windows.
 - Geometry cannot detect a carry through a 45Β° keyhole at all when
   the scene aliases or lands in unknown (measured: both). But the
   robot KNOWS it sat: sitting arms the lost machinery with 'I was not
   moved' pre-seeded as the candidate β€” an unmoved robot confirms in
   one window, a kidnapped one is refused by the coverage floor and
   falls through to the search.

On the recording: kidnap recovered 8.5 s after the stand, a loop
closure fired after recovery, the final sit-confirm found the returned
robot 0.2 m / 10Β° from its boot pose, and map-vs-room went from mean
0.57 m / p90 1.24 to mean 0.14 m / p90 0.28.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…cion can expire

The relocalization triggers are now every moment the robot stops being
able to vouch for its pose:

 - a SIT (it cannot feel a carry),
 - a FALL (it can be dragged and spun β€” new 'fallen' bit through
   OdomSample and the .mdlg flags byte),
 - a SESSION RESUME (it may have been moved, or booted in another room,
   while the daemon was off β€” previously the saved pose was trusted
   outright),
 - and the scan watchdog, for displacements the scans can prove.

All four arm the same machinery: tracking continues on odometry,
nothing inks, and each still window either CONFIRMS the carried
'I was not moved' hypothesis (one window when true β€” a reboot in place
costs ~3 s of paused mapping), REFUTES it (evidence of displacement:
suspicion hardens and the brute-force search takes over), or cannot
judge it. Soft suspicion that stays unjudgeable for 10 windows expires
and tracking resumes unverified at the odometry pose β€” without that
escape, a robot that sits facing an unmapped corner says 'searching'
forever; with evidence of displacement the escape never applies.

Bench regression on the recorded kidnap session: identical (recovery
8.5 s after the stand, map-vs-room mean 0.143 m).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…s a room

Two things the last field round asked for:

Head sweep while searching ([maploc] search_sweep, default on): a
single static 45Β° wedge aliases onto any wall at the same range β€”
measured, it is why kidnap confirmation needs a coverage floor at all β€”
and the bench scored 0-for-13 relocalizes on wedges vs 6-for-10 on
wide composites. While the mapper cannot vouch for its pose and the
robot stands, the control loop sweeps head yaw Β±0.9 rad over 6 s (slow
enough that every cell survives the accumulator's 3-frame vote); the
window accumulator already merges the pan into one ~150Β° composite via
per-beam origins. Only head yaw, only while searching; the command EMA
glides the takeover and the handback. The worker publishes searching
through an AtomicBool on the Host.

Monitor map render: confirmed floor is now a solid dark background
fill and walls fill their whole cell footprint in bright braille,
instead of one dot per wall cell over a 1-in-5 floor stipple β€” a lap
used to render as scattered stars; a room should read as a shape.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
robot.map_wipe (API v14): resets the mapping session in place β€” map,
pose graph, tracked pose and any suspicion β€” and deletes the saved
session file. Mapping starts fresh from wherever the robot stands; no
ssh, no daemon restart. The field workflow was
'sudo rm /var/lib/robot/maploc.session && systemctl restart robotd'
between every experiment, which is three moving parts too many for the
thing done most often.

The worker takes it as an event on its existing channel; the IPC side
reaches it through a OnceLock<Host> the control loop sets at spawn.
Refused (as an intent, not an error) when mapping is off.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…t map

Field test five's second kidnap 'failed to relocalize' for three
minutes: the stand press after the carry never reached robotd (no
'sit toggle direction=stand' in the journal, trunk kinematics frozen
at sit height for 200 s β€” likely a pad/BT drop while the robot was
carried out of range), so the controller stayed in Sitting and the
mapper β€” correctly β€” refused to map or relocalize from the floor. The
monitor said only 'searching', which reads as a relocalization failure
when it is a robot waiting to be stood up.

MapFrame gains an additive 'seated' flag (serde default, no API bump);
the map caption now says 'seated β€” stand the robot to map', and the 5 s
status log spells out sitting/fallen alongside moving.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Field test five, second carry, decoded from its recording: the 'I was
not moved' seed confirmed on a single static wedge (residual 0.010 at
the OLD pose β€” the new spot's wall matched the old spot's wall), and
half of that confirmation was free: the still window that flushes at
the sit describes the world BEFORE the carry and trivially agrees with
the pre-carry pose.

Two changes, both verified by replaying that recording:

 - the soft seed now needs TWO consecutive agreeing windows before
   tracking resumes on it (the search path already had two windows by
   construction). The head sweep keeps running between them, so the
   second window covers a different arc β€” independent evidence instead
   of the same wedge twice;
 - suspicion arms AFTER the window flush, so the pre-sit window inks
   as ordinary tracked data instead of counting as agreement #1.

On the recording: the seed no longer false-confirms; the search finds
the true post-carry pose, the next window confirms it, a loop closure
lands, and window-vs-room agreement drops from 0.40 m to 0.028 m.
Also folds the candidate check into a method β€” the four-parameter
closure was the construct two rustfmt versions disagree about (CI red).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The prototype panned the head 130-230 degrees at every mapping stop β€”
that width is where its map quality came from, and the port only swept
while searching: an ordinary stop kept whichever 45-degree wedge the
head happened to face and threw the rest of the stop away. A messy
office at ankle height needs every degree of that arc to read as more
than scattered blobs.

Same sweep, same param, same accumulator (per-beam origins were built
for the pan); the head comes back to the commanded pose as soon as the
robot moves.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
apirrone and others added 2 commits August 30, 2026 13:16
robot.map / robot.map_wipe renumber to API v17 (main took v13–v16 for
the theremin, the chorale, setMode and update.show). MaplocParams moves
into the robotd-params crate with registry entries, so
'robotctl configure' can edit the [maploc] section like any other.
The connection handler's three streams (state, map, chorale beacons)
all ride the pending-forever select helper. mediad's WebRTC route
permits robot.map β€” the transport btd's refusal always pointed at β€”
and btd itself still refuses both map calls.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nism

A high-effort review over the mapping code confirmed ten correctness
bugs; all fixed here, each pinned by an existing or new test:

 - continuous mode could arm 'lost' (sit/fall/resume) with no path that
   ever clears it: recovery now runs through the still-window machinery
   in both modes (test: continuous_mode_recovers_from_suspicion);
 - the two-witness independence gate cross-examined a scan against
   itself: double-inking stored duplicate RawScans. Weighted
   integration stores one scan per window, and the witness picker
   refuses byte-identical duplicates from old sessions;
 - a kidnap judged on 5-30% of beams was 'unjudgeable' and burned the
   give-up budget: refutation now needs only the watchdog's coverage
   floor, and a new Ambiguous verdict (judged, verdict between
   agreement and contradiction) keeps searching without spending the
   escape budget β€” the escape now fires only when the map truly had no
   opinion (test: a_contradicted_seed_never_resumes_unverified);
 - save-on-shutdown was dead code (the channel never closes: the tofd
   feed and the IPC handle hold senders forever): robotd's shutdown
   path now sends an explicit Shutdown event and waits for the ack;
 - a mid-stand loop closure left the pending window mixing pre- and
   post-correction frames: the accumulator drops them;
 - resume_at inked into a submap still anchored at the pre-carry pose
   (silently clipped): the manager ticks before the ink lands;
 - grid-untouched scans no longer occupy witness slots or make
   has_content() freeze inkless husks (integrate_ray reports whether
   it wrote anything);
 - the matcher's final score counted a stricter beam set than its own
   optimizer, deflating loop-closure acceptance: scoring now matches
   the GN set, and closure coverage judges by n_beams_observed;
 - MCL's see-through kernel gains relocalize's graze exemption and its
   own see_through_fp;
 - densest-bin tie-breaks were HashMap-order (per-process random):
   deterministic now, as the crate's replay contract requires.

Plus the review's efficiency findings: loop-closure witnesses decimate
to 512 beams before the coarse search (~20x fewer lookups per freeze),
the 1 Hz map publish reuses a cached render unless the ink changed
(the re-render grew with the map for a robot that was just walking),
and wrap_pi collapses from seven copies to pose_graph's one.

Ground-truth regression: kidnap recovery unchanged (8.5 s), pose error
0.46 -> 0.39 m, map-vs-room mean 0.143 -> 0.126 m, p90 0.28 -> 0.26 m.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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