Skip to content

12 — Where the Map Comes From

Scope. The provider's own segmentation and pose, normalized into one brand-neutral shape: per-room bbox, name and area, plus the moving overlays — robot and dock anchors, current room, path, hazards. Everything is a float 0..1 of the rendered image with a top-left origin.

What a map's stored state holds once it arrives is 11 — A Map's Stored State.

Two brands feed this through declarations, not through branches in core:

brand map_state_source.backend pose notes
Eufy storage inmem_pixel_pose the fork's .storage file, memory-primary off its in-memory map; pixel pose needs separately-loaded geometry to normalize against
Roborock memory parsed_mapdata a parser MapData located by defensive search; pose already in the rendered frame, plus a re-decode of the raw blob's segment layer the parser discards

1. No backend is a default

map_state_source.live_pose.backend is inmem_pixel_pose or parsed_mapdata. An undeclared or unrecognised value is reported as itselfunknown_pose_backend:<name|undeclared> — and never falls through to a reader.

The rejected version is the one that shipped: the accessor was the Eufy fork reader wearing a generic name, so anything undeclared got the fork's treatment. That collapsed "this brand declares no pose shape" into "this brand has no position", and the two are indistinguishable to every consumer downstream. Roborock's position sat live on the parsed map the card was already rendering while the accessor answered not_configured — the stall capture drew rooms with no robot dot, and the pose ring banked rows with no anchors.

A third brand stays a declaration. It never becomes a branch here.

The gate is checked at the second door too

_apply_inmem_pose_to_result tests live_cfg["backend"] == POSE_BACKEND_INMEM_PIXELS, not merely that a live_pose dict exists. Testing for the dict was sufficient while exactly one brand declared the block, and stops being sufficient the moment a second does — a parsed_mapdata brand would drive the fork's attribute search over an unrelated provider's internals, on the event loop, inside the dashboard snapshot, finding nothing and then paying for a full structure dump to say so, on every refresh.

The test that pins this was green while the gate was removed. The method wraps its body in except Exception by design, so a probe that raises is swallowed. It was rewritten to record the call and assert nothing was called — and only ablation caught the difference.

Re-verified 2026-08-23, including the two sibling tests that still use a raising stub. Those probe async_get_map_live_pose, whose dispatch has no try/except around it, so a raise there does propagate and they bite as written — confirmed by ablating each backend gate in turn. Recorded because the distinction is the useful part: record-the-call and raise-in-a-stub are not interchangeable styles, and which one is correct depends entirely on whether the seam under test swallows. A raising stub in a never-raises seam proves nothing and looks identical to a passing test.


2. Absence: hold the map, drop the movement

A transient absent read holds the last-known-good map for up to six hours, and the hold strips the moving fields: current_room to None, robot_anchor to None, path to empty.

Two alternatives were rejected here, and the second is the interesting one:

  • Dropping the map on any absent read. A Roborock cloud map entity going unavailable when the vacuum idles or docks makes the introspector report no_parsed_map — which would throw away a perfectly good map for the ordinary act of finishing a run.
  • Flagging staleness and leaving the moving fields intact, which is what shipped first. A docked robot then kept attributing to wherever it was last seen, for the whole six-hour window, at whatever cadence the consumer polled. The stale flag existed and nothing downstream read it.

A hold is display-only. Geometry survives a dropout; claims about where the robot is do not.

The terminal reasons that never hold — live_map_absent, store_version_mismatch, not_configured, no_device, and any unknown backend — are the ones where holding would serve a map that is not this vacuum's, or not this map. Everything else is transient.

The cache key carries map_id, and that is load-bearing

The .storage mtime cache hits on (mtime, presence gate, map_id). The fork's file is per-vacuum, not per-map, so its mtime does not vary by which map is being asked about. Key on mtime alone and two different maps queried close together legitimately share one — the second silently inherits the first map's geometry. The hold path carries the same guard, for the same reason.


3. Normalization, and where clamping lies

normalize_rendered defaults to clamping out-of-grid points. Only the pose and anchor callers opt into rejection.

For those callers a clamped point is indistinguishable from a legitimate point at the edge, so clamping silently reports "off the map" as "at the wall". It stops there: the companion current-room lookup is handed the RAW pixel, and current_room_for_pixel bounds-checks the derived raster index, so an off-grid anchor yields None rather than the room owning that wall. The observed failure is the pair disagreeing — a confident edge position beside no room at all. For room geometry the opposite holds: a room whose bbox slightly exceeds the grid is still that room, and rejecting it loses a real room.

The same rule is implemented twice, once per brand path, deliberately.

The Eufy outline offset is a measurement

The room_outline → main-grid offset sign is not derived; it was measured, and it is the single source shared by per-room extraction, the current-room lookup and saved-zone filing.

On a stored map whose outline origin sits 105 cells from the map origin, the shipped sign overlays the segmentation floor onto the rendered floor at ~93%. The other plausible reading of origin + index*res gives under 1% — rooms land nowhere near their pixels. One source, not per-call-site copies, because two copies of a measured sign is two chances to invert it.

Eufy only. Roborock has no separate outline frame, and its zero offset is itself a measurement rather than an absence.


4. Two decoders would be one too many

The in-memory Eufy map is base64-encoded so the existing .storage decoders consume it unchanged. The alternative was a second, object-shaped decoder — and two decoders of one map is the failure this codebase keeps re-learning.

It returns None, not a partial dict, when room_pixels is not bytes or when any of the four _MAPDATA_REQUIRED_GEOMETRY_FIELDSwidth, height, room_outline_width, room_outline_height — is missing. A partial dict looks usable because it carries the raster, and then the geometry step hard-requires the four fields it lacks. resolution is not in that set: a MapData without one converts fine and the geometry step falls back to 5.

The in-memory content version hashes the raster bytes plus the origin and resolution, unlike the sibling render path which hashes the raster alone. The raster does not say where its pixels sit: a re-map to the same room shapes at a new origin is a real content change that a raster-only hash reads as a cache hit.

Roborock's raw re-decode is emitted under Eufy's format name, with the image block's top/left carried as raw_top/raw_left and deliberately not applied — the parser's own projection already carries that correction, and applying it again double-corrects. The values are emitted rather than dropped because they were measured, on hardware, at 281/245: about 12 metres of offset if anyone ever applies them by mistake.

The shared format name is not sloppiness — it is what lets Roborock ride the same frontend decoder instead of shipping a second one.


5. Finding the data at all

Both brands locate their map by searching a provider's internals, and both searches are narrower than they look.

Roborock duck-types for .rooms and .image, not for a generic bbox-like collection. The generic predicate also matches no-go areas, walls and zones — all bbox-shaped — so the search would happily return a hazard layer as "the rooms", and the room list would be silently wrong rather than empty.

The Eufy pose holder is matched on its attributes existing, not on the robot pixel currently being a coordinate pair. The fork nulls that pixel while the robot is docked, so value-matching misses a docked robot entirely — which is exactly the case the live read exists to serve. A non-pair robot pixel with a valid dock resolves the robot to the dock.


6. Common wrong assumptions

assumption actually
the catch-all room sentinel is one constant with one meaning there are two, with different values and different bit widths, and neither is derivable from the other
functions marked pure and "never raises" are safe on the event loop purity is not loop-safety. This unit contains an O(width × height) pure function
the present_requires_live_map_image gate means the live map is working it tests presence in the state machine. An unavailable entity still has a State object and passes
an mtime cache hit re-applies the current pose, so a moving robot is never frozen true of the return value, false of the cache — the hit branch mutates a shallow copy
the multi-vacuum cross-contamination guard is closed the device_id parameter exists and works; no production call site passes it
the render format room_pixels_v1 is Eufy-specific, or the legacy eufy_room_pixels_v1 key is dead it is the card's brand-agnostic decoder name (both brands emit it), renamed to drop the brand word; the coordinator still dual-accepts the legacy eufy_room_pixels_v1 for one release so a stored config isn't stranded
the render-data candidate walk tries every root until one yields a usable render it short-circuits on a partial success — an absent-marker dict is not None

Registries

00b-invariants.mdIN rules and their consequences. 00c-replicas.mdRN sets, where one rule has more than one copy.