Skip to content

Commit 2cd641c

Browse files
refactor: cache one composed Q10 map image
1 parent f22e88c commit 2cd641c

3 files changed

Lines changed: 21 additions & 61 deletions

File tree

roborock/cli.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -745,10 +745,9 @@ async def q10_map_with_path(ctx, device_id: str, output_file: str):
745745
if not got_path:
746746
click.echo("No live path available (the robot only reports its path while cleaning).")
747747
return
748-
try:
749-
image = map_trait.render_path_on_map()
750-
except RoborockException as err:
751-
click.echo(f"Could not render path on map: {err}")
748+
image = map_trait.image_content
749+
if image is None:
750+
click.echo("No map image content available.")
752751
return
753752
with open(output_file, "wb") as f:
754753
f.write(image)

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

Lines changed: 9 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
``MapDpsTrait`` owns the low-level DPS read model. ``MapContentTrait`` depends
1010
on it and combines that state with the latest map/trace packets through the pure
1111
functions in :mod:`roborock.map.b01_q10_render`. The high-level trait keeps only
12-
one grouped source snapshot and one replace-whole rendered result; calibration,
12+
one grouped source snapshot and one replace-whole rendered image; calibration,
1313
path placement and overlay placement are not independently mutable trait state.
1414
"""
1515

@@ -28,7 +28,7 @@
2828
Q10TracePacket,
2929
)
3030
from roborock.map.b01_q10_overlays import Q10Zone, parse_virtual_wall_blob, parse_zone_blob
31-
from roborock.map.b01_q10_render import Q10MapOverlays, Q10MapRender, draw_path_on_map, render_q10_map
31+
from roborock.map.b01_q10_render import Q10MapOverlays, render_q10_map
3232

3333
from .common import UpdatableTrait
3434

@@ -66,7 +66,7 @@ class MapContentTrait(TraitUpdateListener):
6666
6767
Map and trace packet updates replace :attr:`_source`; DPS updates are owned
6868
by the injected :class:`MapDpsTrait`. Rendering always produces one new
69-
:class:`Q10MapRender`, keeping the externally visible fields consistent.
69+
image, keeping the externally visible fields consistent.
7070
"""
7171

7272
def __init__(
@@ -79,13 +79,13 @@ def __init__(
7979
self._config = map_parser_config or B01Q10MapParserConfig()
8080
self._map_dps = map_dps or MapDpsTrait()
8181
self._source = Q10MapSource()
82-
self._render: Q10MapRender | None = None
82+
self._image_content: bytes | None = None
8383
self._map_dps.add_update_listener(self._map_dps_updated)
8484

8585
@property
8686
def image_content(self) -> bytes | None:
87-
"""The rendered base map PNG, if a map has been pushed."""
88-
return self._render.image_content if self._render else None
87+
"""The composed map PNG, if a map has been pushed."""
88+
return self._image_content
8989

9090
@property
9191
def rooms(self) -> list[Q10Room]:
@@ -128,7 +128,7 @@ def update_from_map_packet(self, packet: Q10MapPacket) -> None:
128128
if render is None:
129129
return
130130
self._source = source
131-
self._render = render
131+
self._image_content = render
132132
self._notify_update()
133133

134134
def update_from_trace_packet(self, packet: Q10TracePacket) -> None:
@@ -138,29 +138,13 @@ def update_from_trace_packet(self, packet: Q10TracePacket) -> None:
138138
self._rebuild()
139139
self._notify_update()
140140

141-
def render_path_on_map(
142-
self,
143-
*,
144-
line_color: tuple[int, int, int, int] = (235, 64, 52, 255),
145-
position_color: tuple[int, int, int, int] = (255, 211, 0, 255),
146-
) -> bytes:
147-
"""Return a PNG with the path, robot, zones and walls drawn."""
148-
if self._render is None:
149-
raise RoborockException("No map available; no map has been pushed yet")
150-
return draw_path_on_map(
151-
self._render,
152-
config=self._config,
153-
line_color=line_color,
154-
position_color=position_color,
155-
)
156-
157141
def _map_dps_updated(self) -> None:
158142
"""Recompose placed overlays after the low-level DPS state changes."""
159143
if self._source.map_packet is not None:
160144
self._rebuild()
161145
self._notify_update()
162146

163-
def _compose(self, source: Q10MapSource) -> Q10MapRender | None:
147+
def _compose(self, source: Q10MapSource) -> bytes | None:
164148
"""Compose a source snapshot, preserving the previous result on error."""
165149
if source.map_packet is None:
166150
return None
@@ -179,4 +163,4 @@ def _rebuild(self) -> None:
179163
"""Replace the derived render from the current source snapshot."""
180164
render = self._compose(self._source)
181165
if render is not None:
182-
self._render = render
166+
self._image_content = render

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

Lines changed: 9 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
from roborock.data.b01_q10.b01_q10_code_mappings import B01_Q10_DP
2323
from roborock.devices.traits.b01.q10 import Q10PropertiesApi, create
2424
from roborock.devices.traits.b01.q10.map import MapContentTrait, MapDpsTrait
25-
from roborock.exceptions import RoborockException
2625
from roborock.map.b01_grid_layers import GridCalibration
2726
from roborock.map.b01_q10_map_parser import (
2827
Q10HeaderCalibration,
@@ -232,48 +231,25 @@ def test_trace_update_projects_short_path_using_header() -> None:
232231
trait = MapContentTrait()
233232
packet = replace(parse_map_packet(FIXTURE.read_bytes()), header_calibration=_USABLE_HEADER)
234233
trait.update_from_map_packet(packet)
234+
base = trait.image_content
235+
assert base is not None
235236
true = GridCalibration(resolution=20.0, origin_x=0.0, origin_y=5.0, y_sign=1)
236237
trait.update_from_trace_packet(Q10TracePacket(points=_floor_world_points(packet, true, 6)))
237238
assert len(trait.path) < 20 # far too short for the full origin+resolution fit
238239

239-
assert trait.render_path_on_map()[:8] == b"\x89PNG\r\n\x1a\n"
240+
assert trait.image_content is not None
241+
assert trait.image_content != base
240242

241243

242244
def test_short_trace_without_header_cannot_be_projected() -> None:
243245
"""Without a header origin a short trace cannot be placed on the map."""
244246
packet = parse_map_packet(FIXTURE.read_bytes())
245247
trait = MapContentTrait()
246248
trait.update_from_map_packet(packet) # the fixture header is a keepalive frame
249+
base = trait.image_content
247250
true = GridCalibration(resolution=10.0, origin_x=0.0, origin_y=5.0, y_sign=1)
248251
trait.update_from_trace_packet(Q10TracePacket(points=_floor_world_points(packet, true, 6)))
249-
with pytest.raises(RoborockException, match="No calibration available"):
250-
trait.render_path_on_map()
251-
252-
253-
def test_render_path_on_map_requires_map() -> None:
254-
trait = MapContentTrait()
255-
with pytest.raises(RoborockException, match="No map available"):
256-
trait.render_path_on_map()
257-
258-
259-
def test_render_path_on_map_uses_derived_content() -> None:
260-
"""The renderer draws the already-derived map content as a PNG."""
261-
trait = MapContentTrait()
262-
packet = replace(parse_map_packet(FIXTURE.read_bytes()), header_calibration=_USABLE_HEADER)
263-
trait.update_from_map_packet(packet)
264-
true = GridCalibration(resolution=20.0, origin_x=0.0, origin_y=5.0, y_sign=1)
265-
trait.update_from_trace_packet(Q10TracePacket(points=_floor_world_points(packet, true, 6)))
266-
267-
png = trait.render_path_on_map()
268-
269-
assert png[:8] == b"\x89PNG\r\n\x1a\n"
270-
271-
272-
def test_render_path_on_map_without_path_cannot_calibrate() -> None:
273-
"""A map but no cleaning path -> no calibration -> a clear error."""
274-
trait = _trait_with_map()
275-
with pytest.raises(RoborockException, match="No calibration available"):
276-
trait.render_path_on_map()
252+
assert trait.image_content == base
277253

278254

279255
# --- Overlays ----------------------------------------------------------------
@@ -287,7 +263,8 @@ def test_load_overlays_places_zones_after_calibration() -> None:
287263
trait.update_from_map_packet(packet)
288264
true = GridCalibration(resolution=20.0, origin_x=0.0, origin_y=5.0, y_sign=1)
289265
trait.update_from_trace_packet(Q10TracePacket(points=_floor_world_points(packet, true, 6)))
290-
before = trait.render_path_on_map()
266+
before = trait.image_content
267+
assert before is not None
291268

292269
def rect(zone_type: int, corners: list[tuple[int, int]]) -> bytes:
293270
out = bytes([zone_type, len(corners)])
@@ -299,7 +276,7 @@ def rect(zone_type: int, corners: list[tuple[int, int]]) -> bytes:
299276
map_dps.update_from_dps({B01_Q10_DP.RESTRICTED_ZONE_UP: base64.b64encode(blob).decode()})
300277

301278
assert len(trait.zones) == 1
302-
assert trait.render_path_on_map() != before
279+
assert trait.image_content != before
303280

304281

305282
def test_load_overlays_partial_update_keeps_existing_zones() -> None:

0 commit comments

Comments
 (0)