You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Q-series / B01 Q10 map and live trace packets decoded via HA runtime bridge #827
No credentials, tokens, map IDs, device identifiers, private captures, or household calibration are included.
I am sharing this so it can be reviewed, corrected, and hopefully folded into python-roborock / Home Assistant properly instead of depending on HA runtime internals from a custom bridge.
Thanks for sharing this — really useful writeup. I ended up decoding the same Q10 (B01, roborock.vacuum.ss07) map stream independently for a standalone CLI (andrewlyeats/roborock-q10-cli), reading the spontaneous protocol-301 frames over MQTT directly (outside Home Assistant). Same 0101/0201 split and LZ4 layout on our end, so I'm just adding what we found in case any of it is useful for an upstream parser.
Grid width: in our captures we didn't get a reliable width from the header bytes, so we ended up recovering it from the decompressed grid instead — picking the row stride at which walls stay continuous. Grid dimensions vary per home, so a header-derived width may be map-size-dependent; sharing in case it helps.
Lining the path up with the grid: in our case, path points mapped onto grid pixels with a fixed origin and ~20 mm/px, with the grid's row axis inverted relative to the path's x-axis — roughly col = (y + Cy) // 20, row = (Cx − x) // 20, where Cx/Cy are dock-anchored constants that differ per install. That registration landed ~99.8% of our path points on floor pixels and stayed stable as long as the dock didn't move and the map wasn't reset. Not claiming it's the general solution — just what worked for our single map.
Coordinates are in 0.5 mm units (×2 to reach path-frame mm). (We weren't able to set these — the write seems to ride the MQTT input topic.)
Availability we observed: the 0101 grid comes through even while docked; the 0201 path/position only during an active clean.
Full byte-level notes and a working offline decoder are in DESIGN_NOTES.md and decode_map.py if helpful. Glad to help with a python-roborock PR if that'd be useful.
Following up on my earlier comment — I need to correct a few things I got wrong, and add a more useful update now that we've taken the work further.
Corrections to my earlier comment (apologies — these were wrong or have since been superseded):
Coordinate units — I said "0.5 mm units"; that was off by 10×. Ground-truthed against the app's default 3.3 ft (~1006 mm) zone, which decodes to ~200 units: the binary zone/wall coords are ~5 mm/unit, the 0201 path is ~2.5 mm/unit, and the grid is ~50 mm/px.
Grid width — I overcomplicated it. It's a clean header read: width and height are consecutive u16be at [7:9] and [9:11] of the 01 01 frame (this is the read that went into feat: Q10 (B01/ss07) map support — rooms + rendered map image #847). My earlier "recover from the decompressed row-stride" was a workaround I no longer need.
Georeference — the divisor is right but I mislabeled the unit.path_unit // 20 = pixel holds, but that 20 is 20 path-units/px (≈50 mm/px), not "20 mm/px".
Setting zones/walls — I said we couldn't; we since cracked it. Restricted zones (DP 54) and virtual walls (DP 56) are settable from our own MQTT client via the string-key COMMON(101) envelope ({"101":{"54": <base64 blob>}}), live round-trip validated (write → read-back → restore, byte-exact). It does not require the app/MITM path.
The actual update — live map and live pose without an active clean, via the DP-110 heartbeat:
My earlier claim that the 0201 path "only comes through during an active clean" is wrong — but the exact behavior is worth stating precisely. The app gets the live map by polling DP-110 (COMMON(101){"110":1}, what we label HEARTBEAT) roughly every 5 s while the map screen is open. Two different things come back on those polls:
The 0101 occupancy grid streams in response to the heartbeat at any time, including while the robot is just docked and idle — it's the app's live-map keepalive.
The 0201 path/position (with a live heading field) streams outside an active clean during manual-control sessions — i.e. while the robot is being driven by hand (remote-control mode). It is not emitted while the robot sits docked and idle: we checked that case directly (docked, idle, never entered remote mode, sustained heartbeats) and got only the 0101 grid, no0201 pose. So it isn't "any time" — but it is definitely not clean-only, since manual teleop (not a clean) is enough to get live pose.
We replicated this from a plain MQTT client (no app, no HA runtime): sending the same {"101":{"110":1}} heartbeat on an interval streams the live 0101 grid to our connection while docked, and the live 0201 pose+heading while the robot is driven by hand — all outside any clean. So a live map (and, during manual control, live position) looks achievable in python-roborock directly: today the B01 path appears to drop the spontaneous 301 frames and never sends the heartbeat, so the stream is never requested.
Happy to share captures. With the recent Q10 map work landing (#847 merged, #846/#848 in progress), the one piece this adds is eliciting that live map/pose stream on demand, without a clean: sending the DP-110 heartbeat gets the 0101 grid while docked (and the 0201 pose during manual control), instead of waiting on the spontaneous frames a clean produces. Glad to fold that heartbeat-poll trick into that effort if it's useful, rather than spin up a separate PR. And thanks again for the original writeup — the HA-runtime-bridge framing is what got us comparing notes.
I've got a Roborock Q7 Series B01 now. Please let me know if there's anything I can help with to get this integrated. No HA user, but fluent with coding + Claude.
Activity
andrewlyeats commentedon Jun 14, 2026
Thanks for sharing this — really useful writeup. I ended up decoding the same Q10 (B01,
roborock.vacuum.ss07) map stream independently for a standalone CLI (andrewlyeats/roborock-q10-cli), reading the spontaneous protocol-301 frames over MQTT directly (outside Home Assistant). Same0101/0201split and LZ4 layout on our end, so I'm just adding what we found in case any of it is useful for an upstream parser.Grid width: in our captures we didn't get a reliable width from the header bytes, so we ended up recovering it from the decompressed grid instead — picking the row stride at which walls stay continuous. Grid dimensions vary per home, so a header-derived width may be map-size-dependent; sharing in case it helps.
Lining the path up with the grid: in our case, path points mapped onto grid pixels with a fixed origin and ~20 mm/px, with the grid's row axis inverted relative to the path's x-axis — roughly
col = (y + Cy) // 20,row = (Cx − x) // 20, whereCx/Cyare dock-anchored constants that differ per install. That registration landed ~99.8% of our path points on floor pixels and stayed stable as long as the dock didn't move and the map wasn't reset. Not claiming it's the general solution — just what worked for our single map.Grid pixel values we saw:
pixel // 4 == room_id;243= outside/unmapped,249= wall.Spatial DPs (walls / zones / carpets): we were also able to decode these from the robot's
*_UPoutput DPs, in case they're useful later:[count:u8]+ per-wall(y1, x1, y2, x2)BE int16 — (y, x) order.[0x01][count:u8]+ per-zone[type:u8][nverts:u8=4]+ 4 ×(x, y)BE int16.vertexs).Availability we observed: the
0101grid comes through even while docked; the0201path/position only during an active clean.Full byte-level notes and a working offline decoder are in DESIGN_NOTES.md and
decode_map.pyif helpful. Glad to help with a python-roborock PR if that'd be useful.andrewlyeats commentedon Jun 23, 2026
Following up on my earlier comment — I need to correct a few things I got wrong, and add a more useful update now that we've taken the work further.
Corrections to my earlier comment (apologies — these were wrong or have since been superseded):
0201path is ~2.5 mm/unit, and the grid is ~50 mm/px.u16beat[7:9]and[9:11]of the01 01frame (this is the read that went into feat: Q10 (B01/ss07) map support — rooms + rendered map image #847). My earlier "recover from the decompressed row-stride" was a workaround I no longer need.path_unit // 20 = pixelholds, but that20is 20 path-units/px (≈50 mm/px), not "20 mm/px".COMMON(101)envelope ({"101":{"54": <base64 blob>}}), live round-trip validated (write → read-back → restore, byte-exact). It does not require the app/MITM path.DESIGN_NOTES.mdlink is gone — that content moved intoPROTOCOL.mdandFRAME_ANATOMY.md.The actual update — live map and live pose without an active clean, via the DP-110 heartbeat:
My earlier claim that the
0201path "only comes through during an active clean" is wrong — but the exact behavior is worth stating precisely. The app gets the live map by polling DP-110 (COMMON(101){"110":1}, what we labelHEARTBEAT) roughly every 5 s while the map screen is open. Two different things come back on those polls:0101occupancy grid streams in response to the heartbeat at any time, including while the robot is just docked and idle — it's the app's live-map keepalive.0201path/position (with a live heading field) streams outside an active clean during manual-control sessions — i.e. while the robot is being driven by hand (remote-control mode). It is not emitted while the robot sits docked and idle: we checked that case directly (docked, idle, never entered remote mode, sustained heartbeats) and got only the0101grid, no0201pose. So it isn't "any time" — but it is definitely not clean-only, since manual teleop (not a clean) is enough to get live pose.We replicated this from a plain MQTT client (no app, no HA runtime): sending the same
{"101":{"110":1}}heartbeat on an interval streams the live0101grid to our connection while docked, and the live0201pose+heading while the robot is driven by hand — all outside any clean. So a live map (and, during manual control, live position) looks achievable in python-roborock directly: today the B01 path appears to drop the spontaneous 301 frames and never sends the heartbeat, so the stream is never requested.Happy to share captures. With the recent Q10 map work landing (#847 merged, #846/#848 in progress), the one piece this adds is eliciting that live map/pose stream on demand, without a clean: sending the DP-110 heartbeat gets the
0101grid while docked (and the0201pose during manual control), instead of waiting on the spontaneous frames a clean produces. Glad to fold that heartbeat-poll trick into that effort if it's useful, rather than spin up a separate PR. And thanks again for the original writeup — the HA-runtime-bridge framing is what got us comparing notes.andig commentedon Aug 4, 2026
I've got a Roborock Q7 Series B01 now. Please let me know if there's anything I can help with to get this integrated. No HA user, but fluent with coding + Claude.