feat: Q10 (B01/ss07) map support — rooms + rendered map image#847
feat: Q10 (B01/ss07) map support — rooms + rendered map image#847tubededentifrice wants to merge 6 commits into
Conversation
Brings Q10 maps toward parity with V1 devices. Verified end-to-end against two physical Q10 (roborock.vacuum.ss07) robots. Protocol (reverse-engineered from live captures): - Requesting device state (dpRequestDps) makes the robot push its current map as a protocol-301 MAP_RESPONSE a few seconds later (firmware throttles to ~once per minute). - The "01 01" map packet carries a u32be map id, u16le grid width, and an LZ4-block-compressed occupancy grid followed by 47-byte room records (id + ascii name); room cells use value room_id*4. The payload is unencrypted, unlike the Q7 SCMap protobuf format. Changes: - roborock/map/b01_q10_map_parser.py: clean LZ4 block decoder + packet parser + renderer producing a PNG and MapData with room names. - roborock/devices/rpc/b01_q10_channel.py: request_map() triggers and awaits the MAP_RESPONSE push. - roborock/devices/traits/b01/q10/map.py: MapContentTrait (refresh/parse/image/ rooms), wired into Q10PropertiesApi. - cli: `map-image` and `rooms` now work for Q10 devices. - Tests + a synthetic (no-PII) map fixture. Map packet format documentation credit: the roborock-qseries-map-bridge project (GPL-3.0): https://github.com/v1b3c0d3x3r/roborock-qseries-map-bridge
Adds parsing for the Q10 "02 01" live position packet (delivered on the same protocol-301 channel as the map, only while the robot is moving). The packet format was reverse-engineered and validated against live ss07 captures (the 18-byte-header layout documented elsewhere did NOT match this firmware): - 10-byte header (sequence counter at byte 3, then a constant type/flag). - big-endian int16 (x, y) point pairs; this firmware sends the current position as a single point per packet rather than an accumulated path. - Confirmed live: as R1 traversed the corridor, the decoded x moved from -163 to +169 with y ~0. The full saved map packet (01 01) was checked too and does NOT carry the live path (identical across captures during a clean), so position comes from 02 01. - b01_q10_map_parser: parse_trace_packet() + Q10TracePacket/Q10Point. - b01_q10_channel: request_trace() (marker-filtered). - MapContentTrait.refresh_trace() exposes path + robot_position. - cli: `q10-position` (reports gracefully when the robot is idle). - Tests use a real captured position packet + a synthetic multi-point packet.
|
Added Q10 trace parsing for The tested firmware uses a 10-byte header followed by int16 Later captures from the same cleaning session showed multi-point packets containing the accumulated current-session path; the follow-up comment below covers that final framing and the added fixture. |
Live capture (R1 corridor run) disproved the earlier 'single current point per packet' assumption: the same session emitted packets of 1, then 3, then 15 points, each a strict superset. The robot accumulates the full session path server-side and returns it whole, so a client connecting mid-session still gets the complete trail (matching the app showing it after a cold launch). The parser already read all points; this corrects the docs and adds a real 15-point fixture + test, and clarifies that byte 3 is a session counter (tracks the device clean count) not a per-packet sequence.
|
Updated the trace handling with a real 15-point current-session path fixture. A live corridor run produced The parser now treats the packet body as int16 |
allenporter
left a comment
There was a problem hiding this comment.
Thank your pushing this forward!
My only high level issue is for Q10 we want to avoid faking a synchronous API where there is not one and keep things entirely asynchronous. WDYT?
|
@allenporter yeah I think that's the right call, and will avoid problems with that logic in the first place, let me refactor that a bit |
The Q10 has no synchronous get-map command. The previous MapContentTrait faked one: refresh()/refresh_trace() sent a dpRequestDps and blocked awaiting the next MAP_RESPONSE push with a timeout. That has no request/response correlation and fights the firmware's ~60-70s push throttle. Mirror the existing Q10 StatusTrait model instead: - MapContentTrait is now a push-only TraitUpdateListener. The Q10PropertiesApi subscribe loop routes protocol-301 MAP_RESPONSE packets to update_from_map_response(), which parses the payload, updates the cached fields and notifies listeners. - Drop request_map()/request_trace() and the trait's refresh()/refresh_trace(). - CLI map-image/rooms/q10-position now nudge the device with refresh() and wait on a map-trait update listener for the pushed data.
|
@allenporter Done, pushed the push-driven refactor. Lines up with how Q10 status already works. |
1ccc910 to
8797ede
Compare
Summary
Adds Q10 (
roborock.vacuum.ss07, B01 protocol) map support: requesting current state makes the robot push a protocol-301 map packet, which this PR can parse into a rendered map image, room list, and current-session path data.This branch is independent of #846 and is based on
main.Protocol Notes
dpRequestDpscauses the robot to publishMAP_RESPONSEpackets shortly afterward. Firmware also throttles these pushes.01 01marker. The parser reads the map id, grid width, compressed layout length, and an LZ4 block containing the occupancy grid plus room records.02 01marker. On the tested firmware these contain a 10-byte header followed by big-endian int16(x, y)point pairs for the current cleaning session path.SCMapprotobuf format.Changes
roborock/map/b01_q10_map_parser.pyfor Q10 map, room, LZ4, and trace parsing.request_map()andrequest_trace()helpers on the Q10 channel.MapContentTraitforrefresh(),parse_map_content(),refresh_trace(),image_content,map_data,rooms,path,robot_position, andraw_api_response.map-image,rooms, and Q10 position flows through the Q10 map trait.Testing
uv run pytest— 510 passed on this branchuv run pre-commit run --all-filesCredit
The initial
01 01/02 01packet layout reference came fromroborock-qseries-map-bridgeand the related Home Assistant community thread. The implementation here is separate and is covered by tests against synthetic or scrubbed fixtures.Follow-ups