feat: B01 grid-layer decomposition + Q10 vector overlay decoding#850
Conversation
Two standalone, device-agnostic map-parsing primitives with no dependencies on the rest of the package: - b01_grid_layers: decompose an occupancy grid into background/wall/ per-room floor layers, render each as a transparent PNG, and fit a GridCalibration (resolution/origin) from a known path via solve_calibration(). - b01_q10_overlays: decode Q10 restricted-zone blobs into no-go, no-mop, and virtual-wall overlays. Both are pure transforms (grid/bytes in, structured data out) and are fully covered by self-contained tests that build their inputs inline, so no binary fixtures are needed.
|
Thanks for decomposing the B01 grid and overlays. One data point in case it helps, on the no-mop type. On our Q10 (
i.e. drawing a "no-mop" area in the app and reading the DP back gives type byte (For us, virtual walls read back through a separate DP, This lines up with ioBroker's Q10 parser, which decodes the restricted-zone payload as Since this PR currently treats |
The restricted-zone DP (RESTRICTED_ZONE_UP) encodes no-mop as type 2 and the door-threshold rectangle as type 3; type 1 is a virtual wall (which in practice arrives on its own DP) and any other value, including 0, is a no-go zone. The previous reading mislabelled type 3 as no-mop. Add ZONE_TYPE_VIRTUAL_WALL (1) and ZONE_TYPE_THRESHOLD (3); set ZONE_TYPE_NO_MOP = 2. Confirmed on ss07 (firmware 03.11.24) read-backs and cross-checked against the ioBroker roborock Q10 parser. Reported and verified by @andrewlyeats.
|
Thanks @andrewlyeats — you're right, and I've corrected it in aa321fd (credited in the commit and code comments). I verified your read-back against the ioBroker roborock adapter's with virtual walls handled separately as Fix:
Added a test asserting the constant values and one that a no-mop (2) and a door-threshold (3) zone keep distinct types. The overlay-routing consumer in the stacked map-layers PR imports I'll still gladly take your raw |
|
Thank you @tubededentifrice 👍🏼 👍🏼 👍🏼 |
|
Thanks @tubededentifrice — glad it helped, and thanks for the credit. How these were produced: each blob below is the device's own Where these sit in the protocol: they're B01 data-points on the cloud-MQTT channel. The blobs above were issued by the device as its The base64 is verbatim; the hex is an annotated breakdown of those same bytes (separators, decoded 1 — drew one threshold (the app's "Set Threshold" tool, type 2 — drew a No-mop Zone + a Set Threshold (types 3 — drew a No-Go Zone + a No-mop Zone (types These decode cleanly through One heads-up that matters for #848: Fed to Two more in the same family, FWIW: zoned-clean rectangles ride If our working decode is handy as a reference (sometimes clearer than prose), it all ships in Thanks again — happy to capture more (a specific type, more vertices, an empty → set → clear sequence, …) if it'd help. |
Virtual walls (dpVirtualWallUp 57) use a different on-wire frame from the restricted-zone DPs: a bare [count] byte (no version, no per-record type/pad) then 8-byte (y, x) int16-BE records. Feeding such a blob to parse_zone_blob mis-frames it (leading 0x01 read as a version, the next coordinate byte as a record count), so virtual_walls silently came back empty and the wall overlay never rendered. Add parse_virtual_wall_blob (axes un-swapped to (x, y) so walls share the restricted-zone coordinate order), point load_overlays at it for DP 57, and correct the overlay module's docs that wrongly claimed parse_zone_blob handled DP 57. Tested against a real ss07 read-back from the PR Python-roborock#850 thread.
Virtual walls (dpVirtualWallUp 57) use a different on-wire frame from the restricted-zone DPs: a bare [count] byte (no version, no per-record type/pad) then 8-byte (y, x) int16-BE records. Feeding such a blob to parse_zone_blob mis-frames it (leading 0x01 read as a version, the next coordinate byte as a record count), so virtual_walls silently came back empty and the wall overlay never rendered. Add parse_virtual_wall_blob (axes un-swapped to (x, y) so walls share the restricted-zone coordinate order), point load_overlays at it for DP 57, and correct the overlay module's docs that wrongly claimed parse_zone_blob handled DP 57. Tested against a real ss07 read-back from the PR Python-roborock#850 thread.
|
Two coordinate follow-ups — including a correction to myself. As far as I can tell none of this affected the merge ( Correction: I said walls (DP 57) use a different coordinate order than the zones (DP 55) — that was wrong. On our captures they're the same order; the only DP-57-vs-DP-55 difference we see is the record framing ( The bit that might matter for We hit this exact transpose in our own renderer; caught it by drawing a zone in the app and predicting its grid cell — happy to share a worked before/after when you wire the overlays in. Apologies for the mix-up! |
Summary
Splits out the standalone, low-level map-parsing primitives from #848 so they can be reviewed in parallel, without waiting on #846 and #847. Both modules are pure transforms with no dependencies on the rest of the package, and their tests build all inputs inline (no binary fixtures).
This is branched directly off
main— independent of #846/#847/#848. Once it merges, #848 rebases on top and shrinks to the parts that genuinely depend on the Q10 map parser (calibration wiring, parser integration, CLI, Q7 reuse).Changes
roborock/map/b01_grid_layers.py— device-agnostic occupancy-grid decomposition: split a grid into background / wall / per-room floor layers (via a caller-suppliedclassifycallable), render any class or room as a transparent RGBA PNG, and fit aGridCalibration(resolution / origin / y-sign) from a known path withsolve_calibration().roborock/map/b01_q10_overlays.py— decode Q10 restricted-zone blobs into no-go, no-mop, and virtual-wall overlays.Testing
uv run pytest tests/map/test_b01_grid_layers.py tests/map/test_b01_q10_overlays.py— 12 passeduv run pre-commit run --files ...— ruff format, ruff, mypy all pass