python-roborock 6.1.0__tar.gz → 6.2.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python_roborock-6.1.0 → python_roborock-6.2.1}/PKG-INFO +1 -1
- {python_roborock-6.1.0 → python_roborock-6.2.1}/pyproject.toml +1 -1
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/devices/traits/b01/q10/map.py +19 -11
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/devices/traits/common.py +6 -2
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/map/b01_q10_map_parser.py +63 -25
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/map/b01_q10_overlays.py +6 -5
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/map/b01_q10_render.py +126 -77
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/map/map_parser.py +96 -7
- python_roborock-6.2.1/roborock/map/room_colors.py +59 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/.gitignore +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/LICENSE +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/README.md +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/__init__.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/broadcast_protocol.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/callbacks.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/cli.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/const.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/data/__init__.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/data/b01_q10/__init__.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/data/b01_q10/b01_q10_code_mappings.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/data/b01_q10/b01_q10_containers.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/data/b01_q7/__init__.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/data/b01_q7/b01_q7_code_mappings.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/data/b01_q7/b01_q7_containers.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/data/code_mappings.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/data/containers.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/data/dyad/__init__.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/data/dyad/dyad_code_mappings.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/data/dyad/dyad_containers.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/data/mower/__init__.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/data/mower/mower_containers.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/data/v1/__init__.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/data/v1/v1_clean_modes.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/data/v1/v1_code_mappings.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/data/v1/v1_containers.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/data/zeo/__init__.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/data/zeo/zeo_code_mappings.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/data/zeo/zeo_containers.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/device_features.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/devices/README.md +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/devices/__init__.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/devices/cache.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/devices/device.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/devices/device_manager.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/devices/file_cache.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/devices/rpc/__init__.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/devices/rpc/a01_channel.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/devices/rpc/b01_q10_channel.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/devices/rpc/b01_q7_channel.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/devices/rpc/v1_channel.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/devices/traits/__init__.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/devices/traits/a01/__init__.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/devices/traits/b01/__init__.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/devices/traits/b01/q10/__init__.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/devices/traits/b01/q10/button_light.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/devices/traits/b01/q10/child_lock.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/devices/traits/b01/q10/clean_history.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/devices/traits/b01/q10/command.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/devices/traits/b01/q10/common.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/devices/traits/b01/q10/consumable.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/devices/traits/b01/q10/do_not_disturb.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/devices/traits/b01/q10/dust_collection.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/devices/traits/b01/q10/network_info.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/devices/traits/b01/q10/remote.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/devices/traits/b01/q10/status.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/devices/traits/b01/q10/vacuum.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/devices/traits/b01/q10/volume.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/devices/traits/b01/q7/__init__.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/devices/traits/b01/q7/clean_summary.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/devices/traits/b01/q7/map.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/devices/traits/b01/q7/map_content.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/devices/traits/traits_mixin.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/devices/traits/v1/__init__.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/devices/traits/v1/child_lock.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/devices/traits/v1/clean_summary.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/devices/traits/v1/command.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/devices/traits/v1/common.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/devices/traits/v1/consumeable.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/devices/traits/v1/device_features.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/devices/traits/v1/do_not_disturb.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/devices/traits/v1/dust_collection_mode.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/devices/traits/v1/flow_led_status.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/devices/traits/v1/home.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/devices/traits/v1/led_status.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/devices/traits/v1/map_content.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/devices/traits/v1/maps.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/devices/traits/v1/network_info.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/devices/traits/v1/obstacle_photos.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/devices/traits/v1/rooms.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/devices/traits/v1/routines.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/devices/traits/v1/smart_wash_params.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/devices/traits/v1/status.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/devices/traits/v1/valley_electricity_timer.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/devices/traits/v1/volume.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/devices/traits/v1/wash_towel_mode.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/devices/transport/__init__.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/devices/transport/channel.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/devices/transport/local_channel.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/devices/transport/mqtt_channel.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/diagnostics.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/exceptions.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/map/__init__.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/map/b01_grid_layers.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/map/b01_map_parser.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/map/proto/__init__.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/map/proto/b01_scmap.proto +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/map/proto/b01_scmap_pb2.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/mqtt/__init__.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/mqtt/health_manager.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/mqtt/roborock_session.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/mqtt/session.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/protocol.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/protocols/__init__.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/protocols/a01_protocol.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/protocols/b01_q10_protocol.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/protocols/b01_q7_protocol.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/protocols/v1_protocol.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/py.typed +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/roborock_message.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/roborock_typing.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/testing/__init__.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/testing/b01_q10_simulator.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/testing/channel.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/testing/cloud.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/testing/simulator.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/testing/v1_simulator.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/util.py +0 -0
- {python_roborock-6.1.0 → python_roborock-6.2.1}/roborock/web_api.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-roborock
|
|
3
|
-
Version: 6.1
|
|
3
|
+
Version: 6.2.1
|
|
4
4
|
Summary: A package to control Roborock vacuums.
|
|
5
5
|
Project-URL: Repository, https://github.com/python-roborock/python-roborock
|
|
6
6
|
Project-URL: Documentation, https://python-roborock.readthedocs.io/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "python-roborock"
|
|
3
|
-
version = "6.1
|
|
3
|
+
version = "6.2.1"
|
|
4
4
|
description = "A package to control Roborock vacuums."
|
|
5
5
|
authors = [{ name = "humbertogontijo", email = "humbertogontijo@users.noreply.github.com" }, {name="Lash-L"}, {name="allenporter"}]
|
|
6
6
|
requires-python = ">=3.11, <4"
|
|
@@ -4,12 +4,12 @@ Map-related state arrives on three independent streams:
|
|
|
4
4
|
|
|
5
5
|
* map packets are decoded from map-protocol responses;
|
|
6
6
|
* trace packets are decoded from trace-protocol responses;
|
|
7
|
-
* restricted zones
|
|
7
|
+
* restricted zones, virtual walls and dock state arrive as ordinary DPS values.
|
|
8
8
|
|
|
9
|
-
``MapDpsTrait`` owns the low-level DPS read model.
|
|
10
|
-
|
|
11
|
-
functions in :mod:`roborock.map.b01_q10_render`. The high-level
|
|
12
|
-
the latest value from each source and one replace-whole
|
|
9
|
+
``MapDpsTrait`` owns the low-level map-specific DPS read model.
|
|
10
|
+
``MapContentTrait`` combines that state with the latest map/trace packets
|
|
11
|
+
through the pure functions in :mod:`roborock.map.b01_q10_render`. The high-level
|
|
12
|
+
trait keeps only the latest value from each source and one replace-whole image;
|
|
13
13
|
calibration, path placement and overlay placement remain inside the renderer.
|
|
14
14
|
"""
|
|
15
15
|
|
|
@@ -18,7 +18,7 @@ from dataclasses import dataclass, field
|
|
|
18
18
|
from typing import Any
|
|
19
19
|
|
|
20
20
|
from roborock.data import RoborockBase
|
|
21
|
-
from roborock.data.b01_q10.b01_q10_code_mappings import B01_Q10_DP
|
|
21
|
+
from roborock.data.b01_q10.b01_q10_code_mappings import B01_Q10_DP, YXDeviceState
|
|
22
22
|
from roborock.devices.traits.common import DpsDataConverter, TraitUpdateListener
|
|
23
23
|
from roborock.exceptions import RoborockException
|
|
24
24
|
from roborock.map.b01_q10_map_parser import (
|
|
@@ -34,12 +34,14 @@ from roborock.map.b01_q10_render import Q10MapOverlays, render_q10_map
|
|
|
34
34
|
from .common import UpdatableTrait
|
|
35
35
|
|
|
36
36
|
_LOGGER = logging.getLogger(__name__)
|
|
37
|
+
_DOCKED_STATES = {YXDeviceState.CHARGING, YXDeviceState.EMPTYING_THE_BIN}
|
|
37
38
|
|
|
38
39
|
|
|
39
40
|
@dataclass
|
|
40
41
|
class MapDps(RoborockBase):
|
|
41
42
|
"""Low-level map values delivered in the Q10 DPS stream."""
|
|
42
43
|
|
|
44
|
+
status: YXDeviceState | None = field(default=None, metadata={"dps": B01_Q10_DP.STATUS})
|
|
43
45
|
restricted_zone_up: str | None = field(default=None, metadata={"dps": B01_Q10_DP.RESTRICTED_ZONE_UP})
|
|
44
46
|
virtual_wall_up: str | None = field(default=None, metadata={"dps": B01_Q10_DP.VIRTUAL_WALL_UP})
|
|
45
47
|
|
|
@@ -59,8 +61,13 @@ class MapDpsTrait(MapDps, UpdatableTrait):
|
|
|
59
61
|
"""Overlays decoded once from the latest relevant DPS update."""
|
|
60
62
|
return self._overlays
|
|
61
63
|
|
|
64
|
+
@property
|
|
65
|
+
def robot_at_dock(self) -> bool:
|
|
66
|
+
"""Whether status places the idle robot at the saved dock."""
|
|
67
|
+
return self.status in _DOCKED_STATES
|
|
68
|
+
|
|
62
69
|
def update_from_dps(self, decoded_dps: dict[B01_Q10_DP, Any]) -> None:
|
|
63
|
-
"""
|
|
70
|
+
"""Update one coherent snapshot of the DPS inputs used by the map."""
|
|
64
71
|
if not self._CONVERTER.update_from_dps(self, decoded_dps):
|
|
65
72
|
return
|
|
66
73
|
self._overlays = Q10MapOverlays(
|
|
@@ -73,8 +80,8 @@ class MapDpsTrait(MapDps, UpdatableTrait):
|
|
|
73
80
|
class MapContentTrait(TraitUpdateListener):
|
|
74
81
|
"""High-level composed Q10 map view.
|
|
75
82
|
|
|
76
|
-
The latest map and trace packets are combined with the injected
|
|
77
|
-
|
|
83
|
+
The latest map and trace packets are combined with the injected map DPS
|
|
84
|
+
whenever any source changes.
|
|
78
85
|
"""
|
|
79
86
|
|
|
80
87
|
def __init__(
|
|
@@ -129,7 +136,7 @@ class MapContentTrait(TraitUpdateListener):
|
|
|
129
136
|
self._notify_update()
|
|
130
137
|
|
|
131
138
|
def _map_dps_updated(self) -> None:
|
|
132
|
-
"""Render after the low-level DPS source changes."""
|
|
139
|
+
"""Render after the low-level map DPS source changes."""
|
|
133
140
|
if self._map_packet is None:
|
|
134
141
|
return
|
|
135
142
|
self._render()
|
|
@@ -142,9 +149,10 @@ class MapContentTrait(TraitUpdateListener):
|
|
|
142
149
|
try:
|
|
143
150
|
self._image_content = render_q10_map(
|
|
144
151
|
self._map_packet,
|
|
145
|
-
self._trace_packet,
|
|
152
|
+
self._trace_packet if not self._map_dps.robot_at_dock else None,
|
|
146
153
|
self._map_dps.overlays,
|
|
147
154
|
config=self._config,
|
|
155
|
+
robot_at_dock=self._map_dps.robot_at_dock,
|
|
148
156
|
)
|
|
149
157
|
except RoborockException as ex:
|
|
150
158
|
_LOGGER.debug("Failed to render Q10 map packet: %s", ex)
|
|
@@ -105,10 +105,14 @@ class DpsDataConverter(Generic[TDps]):
|
|
|
105
105
|
decoded_dps: The decoded DPS data to convert.
|
|
106
106
|
|
|
107
107
|
Returns:
|
|
108
|
-
True if any values
|
|
108
|
+
True if any values changed, False otherwise.
|
|
109
109
|
"""
|
|
110
110
|
conversions = RoborockBase.convert_dict(self._dps_type_map, decoded_dps)
|
|
111
|
+
changed = False
|
|
111
112
|
for dps_id, value in conversions.items():
|
|
112
113
|
field_name = self._dps_field_map[dps_id]
|
|
114
|
+
if getattr(target, field_name) == value:
|
|
115
|
+
continue
|
|
113
116
|
setattr(target, field_name, value)
|
|
114
|
-
|
|
117
|
+
changed = True
|
|
118
|
+
return changed
|
|
@@ -19,15 +19,15 @@ documentation that informed this clean-room implementation comes from the
|
|
|
19
19
|
https://github.com/v1b3c0d3x3r/roborock-qseries-map-bridge
|
|
20
20
|
"""
|
|
21
21
|
|
|
22
|
-
import colorsys
|
|
23
22
|
import io
|
|
24
23
|
import math
|
|
25
24
|
import statistics
|
|
26
25
|
from dataclasses import dataclass, field, replace
|
|
27
26
|
|
|
28
27
|
from PIL import Image
|
|
28
|
+
from vacuum_map_parser_base.config.color import ColorsPalette, SupportedColor
|
|
29
29
|
from vacuum_map_parser_base.config.image_config import ImageConfig
|
|
30
|
-
from vacuum_map_parser_base.map_data import ImageData, MapData
|
|
30
|
+
from vacuum_map_parser_base.map_data import ImageData, MapData, Point
|
|
31
31
|
|
|
32
32
|
from roborock.exceptions import RoborockException
|
|
33
33
|
|
|
@@ -40,6 +40,7 @@ from .b01_grid_layers import (
|
|
|
40
40
|
decompose_grid,
|
|
41
41
|
)
|
|
42
42
|
from .map_parser import ParsedMapData
|
|
43
|
+
from .room_colors import adjacency_aware_room_colors
|
|
43
44
|
|
|
44
45
|
_MAP_FILE_FORMAT = "PNG"
|
|
45
46
|
|
|
@@ -93,15 +94,18 @@ _MAX_ERASE_ZONE_VERTICES = 16
|
|
|
93
94
|
# 50 mm/px, so dividing by 10 yields the origin in grid pixels -- the (ox, oy)
|
|
94
95
|
# that solve_calibration otherwise recovers by sliding the path.
|
|
95
96
|
# - 15-16 resolution (u16be): reads 5 (= 0.05 m/px = 50 mm/px) universally.
|
|
96
|
-
# - 17-18 charger x, 19-20 charger y (s16be,
|
|
97
|
+
# - 17-18 charger x, 19-20 charger y (s16be, absolute map decipixels),
|
|
98
|
+
# 21-22 charger phi. The official app's device-point transform confirms these
|
|
99
|
+
# coordinates are already in the map-array frame and must not receive x_min /
|
|
100
|
+
# y_min a second time.
|
|
97
101
|
_ORIGIN_X_OFFSET = 11
|
|
98
102
|
_ORIGIN_Y_OFFSET = 13
|
|
99
103
|
_HEADER_RESOLUTION_OFFSET = 15
|
|
100
104
|
_CHARGER_X_OFFSET = 17
|
|
101
105
|
_CHARGER_Y_OFFSET = 19
|
|
102
106
|
_CHARGER_PHI_OFFSET = 21
|
|
103
|
-
# The header origin
|
|
104
|
-
#
|
|
107
|
+
# The header origin and charger fields both divide by 10 to reach grid pixels,
|
|
108
|
+
# although they use different coordinate frames as documented above.
|
|
105
109
|
_HEADER_UNITS_PER_PIXEL = 10
|
|
106
110
|
|
|
107
111
|
# Grid cell values >= this are walls / borders rather than room segments.
|
|
@@ -129,7 +133,8 @@ class Q10EraseZone:
|
|
|
129
133
|
|
|
130
134
|
These are the app's *Erase* tool rectangles -- regions the user marked to be
|
|
131
135
|
removed from the map (e.g. phantom floor the lidar mapped through windows).
|
|
132
|
-
Coordinates are world units
|
|
136
|
+
Coordinates are 5 mm world units, the same frame as restriction zones.
|
|
137
|
+
Trace points use a separate 2.5 mm scale.
|
|
133
138
|
|
|
134
139
|
Confirmed by a controlled diff: removing the two erase zones on a live device
|
|
135
140
|
dropped this section's count from 2 to 0 while the grid and the trailing
|
|
@@ -148,9 +153,11 @@ class Q10HeaderCalibration:
|
|
|
148
153
|
straight from the map packet -- no cleaning path / fit required, so it works
|
|
149
154
|
docked or pre-clean. See :meth:`origin_pixels`.
|
|
150
155
|
|
|
151
|
-
``origin_x`` / ``origin_y``
|
|
152
|
-
|
|
153
|
-
|
|
156
|
+
``origin_x`` / ``origin_y`` are in 5 mm units and define the world-coordinate
|
|
157
|
+
origin. The charger coordinates are absolute decipixels in the map-array
|
|
158
|
+
frame, so they are divided by 10 without applying that origin again.
|
|
159
|
+
``resolution`` is the raw header field (5 == 50 mm/px). ``charger_phi`` is
|
|
160
|
+
the raw dock heading field. Reported and verified by @andrewlyeats (ss07).
|
|
154
161
|
"""
|
|
155
162
|
|
|
156
163
|
origin_x: int
|
|
@@ -176,6 +183,15 @@ class Q10HeaderCalibration:
|
|
|
176
183
|
return None
|
|
177
184
|
return (self.origin_x / _HEADER_UNITS_PER_PIXEL, self.origin_y / _HEADER_UNITS_PER_PIXEL)
|
|
178
185
|
|
|
186
|
+
def charger_pixels(self) -> tuple[float, float] | None:
|
|
187
|
+
"""Return the saved dock in absolute map-array pixel coordinates."""
|
|
188
|
+
if (self.charger_x == 0 and self.charger_y == 0) or self.charger_x == -1 or self.charger_y == -1:
|
|
189
|
+
return None
|
|
190
|
+
return (
|
|
191
|
+
self.charger_x / _HEADER_UNITS_PER_PIXEL,
|
|
192
|
+
self.charger_y / _HEADER_UNITS_PER_PIXEL,
|
|
193
|
+
)
|
|
194
|
+
|
|
179
195
|
|
|
180
196
|
@dataclass
|
|
181
197
|
class Q10MapPacket:
|
|
@@ -639,7 +655,12 @@ class B01Q10MapParser:
|
|
|
639
655
|
width=packet.width,
|
|
640
656
|
image_config=ImageConfig(scale=self._config.map_scale),
|
|
641
657
|
data=image,
|
|
642
|
-
|
|
658
|
+
# ImageDimensions uses V1's bottom-up map convention before
|
|
659
|
+
# projecting into the top-down PNG. Q10 points are already
|
|
660
|
+
# top-down grid pixels, so this adapter cancels that standard flip
|
|
661
|
+
# and lets Q10 use the shared V1 ImageGenerator without moving
|
|
662
|
+
# overlays vertically.
|
|
663
|
+
img_transformation=lambda p: Point(p.x, packet.height - p.y - 1, p.a),
|
|
643
664
|
)
|
|
644
665
|
room_names = {room.id: room.name for room in packet.rooms}
|
|
645
666
|
if room_names:
|
|
@@ -655,12 +676,12 @@ class B01Q10MapParser:
|
|
|
655
676
|
return ParsedMapData(image_content=image_bytes.getvalue(), map_data=map_data)
|
|
656
677
|
|
|
657
678
|
def _render(self, packet: Q10MapPacket) -> Image.Image:
|
|
658
|
-
"""Render the Q10 grid
|
|
659
|
-
palette = _build_palette(packet.grid)
|
|
660
|
-
|
|
679
|
+
"""Render the Q10 grid with the V1 map palette."""
|
|
680
|
+
palette = _build_palette(packet.grid, packet.width)
|
|
681
|
+
rgba = bytearray()
|
|
661
682
|
for value in packet.grid:
|
|
662
|
-
|
|
663
|
-
img = Image.frombytes("
|
|
683
|
+
rgba.extend(palette[value])
|
|
684
|
+
img = Image.frombytes("RGBA", (packet.width, packet.height), bytes(rgba))
|
|
664
685
|
# The ss07 grid is stored top-down (row 0 = top of the home), so it is
|
|
665
686
|
# rendered as-is -- unlike the V1/Q7 convention, no vertical flip.
|
|
666
687
|
scale = self._config.map_scale
|
|
@@ -669,15 +690,32 @@ class B01Q10MapParser:
|
|
|
669
690
|
return img
|
|
670
691
|
|
|
671
692
|
|
|
672
|
-
def
|
|
673
|
-
"""
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
693
|
+
def _opaque(color: tuple[int, ...]) -> tuple[int, int, int, int]:
|
|
694
|
+
"""Return a palette color as RGBA."""
|
|
695
|
+
return (color[0], color[1], color[2], color[3] if len(color) == 4 else 255)
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
def _build_palette(grid: bytes, width: int) -> list[tuple[int, int, int, int]]:
|
|
699
|
+
"""Map Q10 cells onto the same colors used by the V1 map renderer."""
|
|
700
|
+
|
|
701
|
+
def room_id(value: int) -> int | None:
|
|
702
|
+
return max(1, value // 4) if 0 < value < _WALL_THRESHOLD else None
|
|
703
|
+
|
|
704
|
+
colors = ColorsPalette()
|
|
705
|
+
room_colors = adjacency_aware_room_colors(
|
|
706
|
+
grid,
|
|
707
|
+
width,
|
|
708
|
+
colors,
|
|
709
|
+
room_id,
|
|
710
|
+
)
|
|
711
|
+
outside = (0, 0, 0, 0)
|
|
712
|
+
palette = [outside] * 256
|
|
713
|
+
for value in {value for value in grid if 0 < value < _WALL_THRESHOLD}:
|
|
714
|
+
palette[value] = _opaque(room_colors[max(1, value // 4)])
|
|
715
|
+
wall = _opaque(colors.get_color(SupportedColor.GREY_WALL))
|
|
680
716
|
for value in range(_WALL_THRESHOLD, 256):
|
|
681
|
-
palette[value] =
|
|
682
|
-
palette[
|
|
717
|
+
palette[value] = wall
|
|
718
|
+
palette[_UNSEGMENTED_FLOOR_VALUE] = _opaque(colors.get_color(SupportedColor.MAP_INSIDE))
|
|
719
|
+
palette[_BACKGROUND_VALUE] = outside
|
|
720
|
+
palette[0] = outside
|
|
683
721
|
return palette
|
|
@@ -19,11 +19,12 @@ the next, a coordinate, as a record count). The coordinate order matches the
|
|
|
19
19
|
zones (first wire word = x), confirmed against the app. Provenance and the
|
|
20
20
|
byte-level breakdown are in PR #850's review thread.
|
|
21
21
|
|
|
22
|
-
Coordinates are in
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
threshold, anything else -- incl. 0 -- a no-go zone); it
|
|
26
|
-
so callers can route polygons to the right ``MapData``
|
|
22
|
+
Coordinates are in 5 mm device world units, the same space as erase polygons.
|
|
23
|
+
Cleaning trace points use a separate 2.5 mm scale, so callers must not project
|
|
24
|
+
both through the same resolution. ``type`` distinguishes the restriction kind
|
|
25
|
+
(2 = no-mop, 3 = door threshold, anything else -- incl. 0 -- a no-go zone); it
|
|
26
|
+
is preserved verbatim so callers can route polygons to the right ``MapData``
|
|
27
|
+
layer.
|
|
27
28
|
"""
|
|
28
29
|
|
|
29
30
|
import base64
|
|
@@ -18,7 +18,8 @@ import math
|
|
|
18
18
|
from collections.abc import Sequence
|
|
19
19
|
from dataclasses import dataclass
|
|
20
20
|
|
|
21
|
-
from
|
|
21
|
+
from vacuum_map_parser_base.config.drawable import Drawable
|
|
22
|
+
from vacuum_map_parser_base.config.size import Size, Sizes
|
|
22
23
|
from vacuum_map_parser_base.map_data import Area, MapData, Path, Point, Wall
|
|
23
24
|
|
|
24
25
|
from roborock.exceptions import RoborockException
|
|
@@ -38,6 +39,7 @@ from .b01_q10_map_parser import (
|
|
|
38
39
|
erased_packet,
|
|
39
40
|
)
|
|
40
41
|
from .b01_q10_overlays import ZONE_TYPE_NO_GO, ZONE_TYPE_NO_MOP, Q10Zone
|
|
42
|
+
from .map_parser import DEFAULT_DRAWABLES, MapParserConfig, _create_image_generator
|
|
41
43
|
|
|
42
44
|
# Path-units-per-pixel candidates for calibration. A dense ss07 path lands a
|
|
43
45
|
# best fit of 20.0 around the header origin -- ground-truthed June 2026 on the
|
|
@@ -49,6 +51,10 @@ from .b01_q10_overlays import ZONE_TYPE_NO_GO, ZONE_TYPE_NO_MOP, Q10Zone
|
|
|
49
51
|
# reach 20 (it railed at the bound), biasing the fit. A dense cleaning path
|
|
50
52
|
# selects the best fit within this bracket.
|
|
51
53
|
_Q10_RESOLUTIONS = [step * 0.5 for step in range(24, 53)] # 12.0 .. 26.0
|
|
54
|
+
# The header resolution is expressed in centimetres per grid pixel, while
|
|
55
|
+
# erase/restriction vectors use 5 mm units: one header unit therefore equals
|
|
56
|
+
# two vector units. Trace points use a separate 2.5 mm coordinate scale.
|
|
57
|
+
_Q10_VECTOR_UNITS_PER_HEADER_RESOLUTION_UNIT = 2.0
|
|
52
58
|
# A path needs enough shape to constrain a full (origin + resolution) fit; a few
|
|
53
59
|
# points cannot.
|
|
54
60
|
_MIN_CALIBRATION_POINTS = 20
|
|
@@ -57,6 +63,16 @@ _MIN_CALIBRATION_POINTS = 20
|
|
|
57
63
|
# one). See :func:`solve_calibration_with_origin`.
|
|
58
64
|
_MIN_HEADER_CALIBRATION_POINTS = 4
|
|
59
65
|
|
|
66
|
+
_Q10_DRAWABLE_TYPES = {
|
|
67
|
+
Drawable.CHARGER,
|
|
68
|
+
Drawable.NO_GO_AREAS,
|
|
69
|
+
Drawable.NO_MOPPING_AREAS,
|
|
70
|
+
Drawable.PATH,
|
|
71
|
+
Drawable.VACUUM_POSITION,
|
|
72
|
+
Drawable.VIRTUAL_WALLS,
|
|
73
|
+
}
|
|
74
|
+
_Q10_DRAWABLES = [drawable for drawable in DEFAULT_DRAWABLES if drawable in _Q10_DRAWABLE_TYPES]
|
|
75
|
+
|
|
60
76
|
|
|
61
77
|
@dataclass(frozen=True)
|
|
62
78
|
class Q10MapOverlays:
|
|
@@ -72,21 +88,22 @@ def render_q10_map(
|
|
|
72
88
|
overlays: Q10MapOverlays,
|
|
73
89
|
*,
|
|
74
90
|
config: B01Q10MapParserConfig,
|
|
91
|
+
robot_at_dock: bool = False,
|
|
75
92
|
) -> bytes:
|
|
76
93
|
"""Compose the latest map, trace and DPS inputs into one PNG image.
|
|
77
94
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
:class:`RoborockException` if map rendering fails.
|
|
95
|
+
Separate transforms are derived for 2.5 mm trace points and 5 mm
|
|
96
|
+
erase/restriction vectors. Erase zones are blanked out of the raster, while
|
|
97
|
+
available trace and DPS overlays are projected and drawn in pixel space.
|
|
98
|
+
Raises :class:`RoborockException` if map rendering fails.
|
|
83
99
|
"""
|
|
84
100
|
parser = B01Q10MapParser(config)
|
|
85
|
-
|
|
101
|
+
trace_calibration = solve_q10_calibration(packet, trace)
|
|
102
|
+
vector_calibration = _vector_calibration(packet, trace_calibration)
|
|
86
103
|
|
|
87
104
|
render_packet = packet
|
|
88
|
-
if
|
|
89
|
-
cells = _erased_cells(packet.layers, packet.erase_zones,
|
|
105
|
+
if vector_calibration is not None:
|
|
106
|
+
cells = _erased_cells(packet.layers, packet.erase_zones, vector_calibration)
|
|
90
107
|
if cells:
|
|
91
108
|
# Blank the erase-zone cells before parsing the raster so phantom
|
|
92
109
|
# areas disappear (as the app shows).
|
|
@@ -97,10 +114,19 @@ def render_q10_map(
|
|
|
97
114
|
raise RoborockException("Failed to render Q10 map image")
|
|
98
115
|
map_data = parsed.map_data
|
|
99
116
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
117
|
+
has_drawables = False
|
|
118
|
+
if trace_calibration is not None and trace is not None:
|
|
119
|
+
charger_heading = packet.header_calibration.charger_phi if packet.header_calibration is not None else None
|
|
120
|
+
_place_trace(map_data, trace_calibration, trace, charger_heading=charger_heading)
|
|
121
|
+
has_drawables = True
|
|
122
|
+
has_drawables = _place_charger_from_header(map_data, packet) or has_drawables
|
|
123
|
+
if robot_at_dock:
|
|
124
|
+
has_drawables = _place_docked_robot(map_data) or has_drawables
|
|
125
|
+
if vector_calibration is not None:
|
|
126
|
+
_place_overlays(map_data, vector_calibration, overlays)
|
|
127
|
+
has_drawables = has_drawables or bool(map_data.no_go_areas or map_data.no_mopping_areas or map_data.walls)
|
|
128
|
+
if has_drawables:
|
|
129
|
+
return _draw_map_content(map_data, config=config)
|
|
104
130
|
|
|
105
131
|
return parsed.image_content
|
|
106
132
|
|
|
@@ -137,6 +163,41 @@ def _calibration_from_header(
|
|
|
137
163
|
return solve_calibration_with_origin(packet.layers, points, origin, resolutions=_Q10_RESOLUTIONS)
|
|
138
164
|
|
|
139
165
|
|
|
166
|
+
def _calibration_from_header_metadata(
|
|
167
|
+
packet: Q10MapPacket,
|
|
168
|
+
*,
|
|
169
|
+
y_sign: int = 1,
|
|
170
|
+
) -> GridCalibration | None:
|
|
171
|
+
"""Build the vector transform directly from a usable map header."""
|
|
172
|
+
header = packet.header_calibration
|
|
173
|
+
if header is None or header.resolution <= 0 or (origin := header.origin_pixels()) is None:
|
|
174
|
+
return None
|
|
175
|
+
return GridCalibration(
|
|
176
|
+
resolution=header.resolution * _Q10_VECTOR_UNITS_PER_HEADER_RESOLUTION_UNIT,
|
|
177
|
+
origin_x=origin[0],
|
|
178
|
+
origin_y=origin[1],
|
|
179
|
+
y_sign=y_sign,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _vector_calibration(
|
|
184
|
+
packet: Q10MapPacket,
|
|
185
|
+
trace_calibration: GridCalibration | None,
|
|
186
|
+
) -> GridCalibration | None:
|
|
187
|
+
"""Derive the 5 mm erase/restriction-vector transform."""
|
|
188
|
+
y_sign = trace_calibration.y_sign if trace_calibration is not None else 1
|
|
189
|
+
if calibration := _calibration_from_header_metadata(packet, y_sign=y_sign):
|
|
190
|
+
return calibration
|
|
191
|
+
if trace_calibration is None:
|
|
192
|
+
return None
|
|
193
|
+
return GridCalibration(
|
|
194
|
+
resolution=trace_calibration.resolution / 2,
|
|
195
|
+
origin_x=trace_calibration.origin_x,
|
|
196
|
+
origin_y=trace_calibration.origin_y,
|
|
197
|
+
y_sign=trace_calibration.y_sign,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
|
|
140
201
|
def _calibration_from_fit(layers: GridLayers, points: list[tuple[float, float]]) -> GridCalibration | None:
|
|
141
202
|
"""Full origin + resolution fit; needs a reasonably dense path."""
|
|
142
203
|
if len(points) < _MIN_CALIBRATION_POINTS:
|
|
@@ -170,6 +231,8 @@ def _place_trace(
|
|
|
170
231
|
map_data: MapData,
|
|
171
232
|
calibration: GridCalibration,
|
|
172
233
|
trace: Q10TracePacket,
|
|
234
|
+
*,
|
|
235
|
+
charger_heading: int | None = None,
|
|
173
236
|
) -> None:
|
|
174
237
|
"""Project trace path, position, heading and charger into pixel space.
|
|
175
238
|
|
|
@@ -181,11 +244,48 @@ def _place_trace(
|
|
|
181
244
|
robot_position = trace.robot_position
|
|
182
245
|
if robot_position is not None:
|
|
183
246
|
px, py = calibration.world_to_pixel(robot_position.x, robot_position.y)
|
|
184
|
-
#
|
|
185
|
-
#
|
|
186
|
-
map_data.vacuum_position = Point(px, py,
|
|
247
|
+
# The shared V1 marker expects a map-space heading (+Y is up), not the
|
|
248
|
+
# top-down PNG angle previously used by Q10's custom marker.
|
|
249
|
+
map_data.vacuum_position = Point(px, py, calibration.y_sign * trace.heading)
|
|
187
250
|
if pixels:
|
|
188
|
-
map_data.charger =
|
|
251
|
+
map_data.charger = Point(
|
|
252
|
+
pixels[0].x,
|
|
253
|
+
pixels[0].y,
|
|
254
|
+
calibration.y_sign * charger_heading if charger_heading is not None else None,
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _place_charger_from_header(
|
|
259
|
+
map_data: MapData,
|
|
260
|
+
packet: Q10MapPacket,
|
|
261
|
+
) -> bool:
|
|
262
|
+
"""Place the saved dock using its absolute header pixel coordinates."""
|
|
263
|
+
header = packet.header_calibration
|
|
264
|
+
if header is None or (position := header.charger_pixels()) is None:
|
|
265
|
+
return False
|
|
266
|
+
map_data.charger = Point(*position, header.charger_phi)
|
|
267
|
+
return True
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _place_docked_robot(map_data: MapData) -> bool:
|
|
271
|
+
"""Place a charging robot immediately in front of the saved dock.
|
|
272
|
+
|
|
273
|
+
A zero-point idle trace has no robot coordinates. The dock heading does,
|
|
274
|
+
however, identify its outward-facing side. Offset the robot by the shared
|
|
275
|
+
unscaled V1 charger radius so the two standard glyphs meet without one
|
|
276
|
+
covering the other, and preserve the saved dock heading.
|
|
277
|
+
"""
|
|
278
|
+
charger = map_data.charger
|
|
279
|
+
if charger is None or charger.a is None:
|
|
280
|
+
return False
|
|
281
|
+
angle = math.radians(charger.a)
|
|
282
|
+
offset = Sizes.SIZES[Size.CHARGER_RADIUS]
|
|
283
|
+
map_data.vacuum_position = Point(
|
|
284
|
+
charger.x + offset * math.cos(angle),
|
|
285
|
+
charger.y - offset * math.sin(angle),
|
|
286
|
+
charger.a,
|
|
287
|
+
)
|
|
288
|
+
return True
|
|
189
289
|
|
|
190
290
|
|
|
191
291
|
def _place_overlays(
|
|
@@ -217,69 +317,18 @@ def _place_overlays(
|
|
|
217
317
|
|
|
218
318
|
|
|
219
319
|
def _draw_map_content(
|
|
220
|
-
image_content: bytes,
|
|
221
320
|
map_data: MapData,
|
|
222
321
|
*,
|
|
223
322
|
config: B01Q10MapParserConfig,
|
|
224
|
-
line_color: tuple[int, int, int, int] = (235, 64, 52, 255),
|
|
225
|
-
position_color: tuple[int, int, int, int] = (255, 211, 0, 255),
|
|
226
323
|
) -> bytes:
|
|
227
|
-
"""Draw
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
# Erase zones are applied to the raster itself (cells blanked), so they are
|
|
237
|
-
# not drawn here -- the base image already reflects them.
|
|
238
|
-
|
|
239
|
-
# No-go (blue) and no-mop (magenta) zones beneath the path.
|
|
240
|
-
for areas, fill, outline in (
|
|
241
|
-
(map_data.no_go_areas or [], (0, 120, 255, 70), (0, 80, 200, 255)),
|
|
242
|
-
(map_data.no_mopping_areas or [], (255, 0, 200, 70), (200, 0, 160, 255)),
|
|
243
|
-
):
|
|
244
|
-
for area in areas:
|
|
245
|
-
polygon = [
|
|
246
|
-
(area.x0 * scale, area.y0 * scale),
|
|
247
|
-
(area.x1 * scale, area.y1 * scale),
|
|
248
|
-
(area.x2 * scale, area.y2 * scale),
|
|
249
|
-
(area.x3 * scale, area.y3 * scale),
|
|
250
|
-
]
|
|
251
|
-
draw.polygon(polygon, fill=fill, outline=outline)
|
|
252
|
-
|
|
253
|
-
# Virtual walls (line segments, not polygons) drawn over the zones.
|
|
254
|
-
for wall in map_data.walls or []:
|
|
255
|
-
draw.line(
|
|
256
|
-
[(wall.x0 * scale, wall.y0 * scale), (wall.x1 * scale, wall.y1 * scale)],
|
|
257
|
-
fill=(255, 64, 64, 255),
|
|
258
|
-
width=max(2, scale),
|
|
259
|
-
)
|
|
260
|
-
|
|
261
|
-
for path in map_data.path.path if map_data.path else []:
|
|
262
|
-
if len(path) >= 2:
|
|
263
|
-
draw.line([to_image(point) for point in path], fill=line_color, width=max(1, scale // 2))
|
|
264
|
-
if map_data.charger is not None:
|
|
265
|
-
dx, dy = to_image(map_data.charger)
|
|
266
|
-
draw.ellipse([dx - scale, dy - scale, dx + scale, dy + scale], outline=(40, 200, 40, 255), width=2)
|
|
267
|
-
robot_position = map_data.vacuum_position
|
|
268
|
-
if robot_position is not None:
|
|
269
|
-
cx, cy = to_image(robot_position)
|
|
270
|
-
radius = scale
|
|
271
|
-
draw.ellipse([cx - radius, cy - radius, cx + radius, cy + radius], fill=position_color)
|
|
272
|
-
robot_heading = robot_position.a
|
|
273
|
-
if robot_heading is not None:
|
|
274
|
-
# Heading was projected into image coordinates alongside the robot
|
|
275
|
-
# position, so no calibration state is needed during drawing.
|
|
276
|
-
angle = math.radians(robot_heading)
|
|
277
|
-
tick = 4 * radius
|
|
278
|
-
draw.line(
|
|
279
|
-
[cx, cy, cx + math.cos(angle) * tick, cy + math.sin(angle) * tick],
|
|
280
|
-
fill=position_color,
|
|
281
|
-
width=max(1, scale // 2),
|
|
282
|
-
)
|
|
324
|
+
"""Draw Q10 content with the shared V1 image generator."""
|
|
325
|
+
if map_data.image is None:
|
|
326
|
+
raise RoborockException("Failed to render Q10 map image")
|
|
327
|
+
generator = _create_image_generator(
|
|
328
|
+
MapParserConfig(map_scale=config.map_scale),
|
|
329
|
+
drawables=_Q10_DRAWABLES,
|
|
330
|
+
)
|
|
331
|
+
generator.draw_map(map_data)
|
|
283
332
|
buffer = io.BytesIO()
|
|
284
|
-
|
|
333
|
+
map_data.image.data.save(buffer, format="PNG")
|
|
285
334
|
return buffer.getvalue()
|