Skip to content

Commit db86533

Browse files
authored
fix: request Q10 maps without starting cleaning (#933)
1 parent bedad13 commit db86533

3 files changed

Lines changed: 27 additions & 96 deletions

File tree

roborock/cli.py

Lines changed: 6 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -594,9 +594,9 @@ async def maps(ctx, device_id: str):
594594
await _display_v1_trait(context, device_id, lambda v1: v1.maps)
595595

596596

597-
# The Q10 publishes its map asynchronously after a dpMultiMap list/get request.
598-
# Firmware throttles pushes to ~once per 60-70s, so rapid re-requests may not be
599-
# answered immediately. This bounds how long a one-shot CLI command waits.
597+
# The Q10 publishes its current map asynchronously after a REQUEST_DPS. Firmware
598+
# throttles pushes to ~once per 60-70s, so rapid re-requests may not be answered
599+
# immediately. This bounds how long a one-shot CLI command waits.
600600
_Q10_MAP_PUSH_TIMEOUT = 30.0
601601

602602

@@ -609,10 +609,9 @@ async def _await_q10_map_push(
609609
) -> bool:
610610
"""Request Q10 map content and wait for usable map-trait state.
611611
612-
A Q10 needs a saved-map ID before it can request content. The map list and
613-
content have independent refresh schedules, so the list is requested only
614-
when no ID is stored. The content then arrives as a later ``MAP_RESPONSE``
615-
and is published through the standard trait update interface.
612+
The read-only ``REQUEST_DPS`` request returns immediately; current map
613+
content arrives as a later ``MAP_RESPONSE`` and is published through the
614+
standard trait update interface.
616615
"""
617616
loop = asyncio.get_running_loop()
618617
updated: asyncio.Future[None] = loop.create_future()
@@ -624,20 +623,6 @@ def on_update() -> None:
624623
unsub = properties.map.add_update_listener(on_update)
625624
try:
626625
async with asyncio.timeout(timeout):
627-
if properties.maps.current_map_id is None:
628-
map_list_updated: asyncio.Future[None] = loop.create_future()
629-
630-
def on_map_list_update() -> None:
631-
if properties.maps.current_map_id is not None and not map_list_updated.done():
632-
map_list_updated.set_result(None)
633-
634-
unsub_maps = properties.maps.add_update_listener(on_map_list_update)
635-
try:
636-
await properties.maps.refresh()
637-
if properties.maps.current_map_id is None:
638-
await map_list_updated
639-
finally:
640-
unsub_maps()
641626
await properties.map.refresh()
642627
await updated
643628
return True

roborock/devices/traits/b01/q10/map.py

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@
77
* restricted zones, virtual walls and dock state arrive as ordinary DPS values.
88
99
``MapDpsTrait`` owns the low-level map-specific DPS read model.
10-
``MapContentTrait`` uses a stored ID from ``MapsTrait`` only when it requests
11-
content. It combines the latest map and trace packets with the map DPS state
12-
through the pure functions in :mod:`roborock.map.b01_q10_render`. Map-list
13-
updates do not refresh content.
10+
``MapContentTrait`` requests a current-map push through ``REQUEST_DPS`` and
11+
combines the latest map and trace packets with the map DPS state through the
12+
pure functions in :mod:`roborock.map.b01_q10_render`. Saved-map list/detail
13+
operations remain on ``MapsTrait``.
1414
"""
1515

1616
import logging
@@ -107,20 +107,13 @@ def __init__(
107107
self._map_dps.add_update_listener(self._map_dps_updated)
108108

109109
async def refresh(self) -> None:
110-
"""Request content for the first map in the latest saved-map list."""
111-
if (map_id := self._maps.current_map_id) is None:
112-
raise RoborockException("Cannot request Q10 map content before the map list is available")
113-
# Map lists and map content can change at different times. Reuse the
114-
# stored ID so a content refresh does not also refresh the list.
115-
await self._command.send(
116-
B01_Q10_DP.COMMON,
117-
{
118-
str(B01_Q10_DP.MULTI_MAP.code): {
119-
"op": "get",
120-
"id": map_id,
121-
}
122-
},
123-
)
110+
"""Request a safe asynchronous current-map/status push.
111+
112+
Some ss07 firmware treats ``dpMultiMap op:get`` as an active
113+
cleaning/relocation command. ``REQUEST_DPS`` is the device's read-only
114+
current-map request and does not depend on a saved-map ID.
115+
"""
116+
await self._command.send(B01_Q10_DP.REQUEST_DPS, params={})
124117

125118
@property
126119
def image_content(self) -> bytes | None:

tests/devices/traits/b01/q10/test_map.py

Lines changed: 10 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -140,34 +140,6 @@ async def refresh_map() -> None:
140140
self.map.refresh = refresh_map # type: ignore[method-assign]
141141

142142

143-
class _FakeQ10PropertiesWithoutMapId:
144-
def __init__(self) -> None:
145-
command = cast(CommandTrait, Mock(spec=CommandTrait))
146-
self.maps = MapsTrait(command)
147-
self.map = MapContentTrait(MapDpsTrait(), self.maps, command)
148-
self.maps_refresh_count = 0
149-
self.map_refresh_count = 0
150-
151-
async def refresh_maps() -> None:
152-
self.maps_refresh_count += 1
153-
self.maps.update_from_dps(
154-
{
155-
B01_Q10_DP.MULTI_MAP: {
156-
"data": [{"id": "12345"}],
157-
"op": "list",
158-
"result": 1,
159-
}
160-
}
161-
)
162-
163-
async def refresh_map() -> None:
164-
self.map_refresh_count += 1
165-
self.map.update_from_trace_packet(parse_trace_packet(TRACE_SESSION_FIXTURE.read_bytes()))
166-
167-
self.maps.refresh = refresh_maps # type: ignore[method-assign]
168-
self.map.refresh = refresh_map # type: ignore[method-assign]
169-
170-
171143
async def test_await_q10_map_push_waits_for_fresh_update() -> None:
172144
"""A cached trace alone is not treated as a successful new map push."""
173145
properties = _FakeQ10Properties()
@@ -196,21 +168,6 @@ async def test_await_q10_map_push_returns_true_after_update() -> None:
196168
assert len(properties.map.path) == 14
197169

198170

199-
async def test_await_q10_map_push_requests_map_list_only_on_first_use() -> None:
200-
"""Content gets the list first only when no stored map ID is available."""
201-
properties = _FakeQ10PropertiesWithoutMapId()
202-
203-
got_trace = await _await_q10_map_push(
204-
cast(Q10PropertiesApi, properties),
205-
lambda: bool(properties.map.path),
206-
timeout=0.01,
207-
)
208-
209-
assert got_trace is True
210-
assert properties.maps_refresh_count == 1
211-
assert properties.map_refresh_count == 1
212-
213-
214171
async def test_await_q10_map_push_can_fall_back_to_cached_map_on_timeout() -> None:
215172
properties = _FakeQ10Properties()
216173
properties.map.update_from_map_packet(parse_map_packet(FIXTURE.read_bytes()))
@@ -286,7 +243,7 @@ async def test_subscribe_loop_routes_trace_push(
286243
assert q10_api.map.robot_position is not None
287244

288245

289-
async def test_map_list_and_content_refresh_are_independent(
246+
async def test_map_list_and_current_content_refresh_are_independent(
290247
q10_api: Q10PropertiesApi,
291248
mock_channel: FakeB01Q10Channel,
292249
message_queue: asyncio.Queue[Q10Message],
@@ -320,15 +277,7 @@ async def test_map_list_and_content_refresh_are_independent(
320277

321278
await q10_api.map.refresh()
322279

323-
assert mock_channel.published_commands[1] == (
324-
B01_Q10_DP.COMMON,
325-
{
326-
str(B01_Q10_DP.MULTI_MAP.code): {
327-
"op": "get",
328-
"id": "12345",
329-
}
330-
},
331-
)
280+
assert mock_channel.published_commands[1] == (B01_Q10_DP.REQUEST_DPS, {})
332281
assert q10_api.maps.current_map_id == "12345"
333282

334283

@@ -355,10 +304,14 @@ async def test_empty_map_list_does_not_request_content(
355304
assert mock_channel.published_commands == []
356305

357306

358-
async def test_map_content_refresh_requires_stored_map_id(q10_api: Q10PropertiesApi) -> None:
359-
"""Content cannot be requested until the map list supplies an ID."""
360-
with pytest.raises(RoborockException, match="map list is available"):
361-
await q10_api.map.refresh()
307+
async def test_map_content_refresh_does_not_require_stored_map_id(
308+
q10_api: Q10PropertiesApi,
309+
mock_channel: FakeB01Q10Channel,
310+
) -> None:
311+
"""Current-map refresh is read-only and independent of saved-map state."""
312+
await q10_api.map.refresh()
313+
314+
assert mock_channel.published_commands == [(B01_Q10_DP.REQUEST_DPS, {})]
362315

363316

364317
async def test_map_content_refresh_requests_are_not_rate_limited(q10_api: Q10PropertiesApi) -> None:

0 commit comments

Comments
 (0)