python-roborock 7.11.1__tar.gz → 7.12.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-7.11.1 → python_roborock-7.12.1}/.gitignore +3 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/PKG-INFO +1 -1
- {python_roborock-7.11.1 → python_roborock-7.12.1}/pyproject.toml +5 -1
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/data/v1/v1_clean_modes.py +31 -4
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/traits/v1/status.py +3 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/map/b01_q10_map_parser.py +181 -28
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/map/b01_q10_render.py +4 -4
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/protocols/b01_q10_protocol.py +14 -10
- {python_roborock-7.11.1 → python_roborock-7.12.1}/LICENSE +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/README.md +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/__init__.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/broadcast_protocol.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/callbacks.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/cli.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/const.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/data/__init__.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/data/b01_q10/__init__.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/data/b01_q10/b01_q10_code_mappings.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/data/b01_q10/b01_q10_containers.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/data/b01_q7/__init__.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/data/b01_q7/b01_q7_code_mappings.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/data/b01_q7/b01_q7_containers.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/data/code_mappings.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/data/containers.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/data/dyad/__init__.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/data/dyad/dyad_code_mappings.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/data/dyad/dyad_containers.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/data/mower/__init__.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/data/mower/mower_code_mappings.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/data/mower/mower_containers.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/data/v1/__init__.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/data/v1/v1_code_mappings.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/data/v1/v1_containers.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/data/zeo/__init__.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/data/zeo/zeo_code_mappings.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/data/zeo/zeo_containers.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/device_features.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/README.md +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/__init__.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/cache.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/device.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/device_manager.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/file_cache.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/rpc/__init__.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/rpc/a01_channel.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/rpc/b01_q10_channel.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/rpc/b01_q7_channel.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/rpc/v1_channel.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/traits/__init__.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/traits/a01/__init__.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/traits/a01/device_feature.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/traits/b01/__init__.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/traits/b01/q10/__init__.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/traits/b01/q10/button_light.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/traits/b01/q10/child_lock.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/traits/b01/q10/clean_history.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/traits/b01/q10/command.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/traits/b01/q10/common.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/traits/b01/q10/consumable.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/traits/b01/q10/do_not_disturb.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/traits/b01/q10/dust_collection.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/traits/b01/q10/goto.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/traits/b01/q10/map.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/traits/b01/q10/maps.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/traits/b01/q10/network_info.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/traits/b01/q10/remote.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/traits/b01/q10/status.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/traits/b01/q10/vacuum.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/traits/b01/q10/volume.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/traits/b01/q7/__init__.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/traits/b01/q7/clean_summary.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/traits/b01/q7/map.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/traits/b01/q7/map_content.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/traits/common.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/traits/traits_mixin.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/traits/v1/__init__.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/traits/v1/child_lock.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/traits/v1/clean_summary.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/traits/v1/command.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/traits/v1/common.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/traits/v1/consumeable.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/traits/v1/device_features.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/traits/v1/do_not_disturb.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/traits/v1/dust_collection_mode.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/traits/v1/flow_led_status.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/traits/v1/home.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/traits/v1/led_status.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/traits/v1/map_content.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/traits/v1/maps.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/traits/v1/mop_dryer.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/traits/v1/network_info.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/traits/v1/obstacle_photos.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/traits/v1/rooms.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/traits/v1/routines.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/traits/v1/smart_wash_params.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/traits/v1/valley_electricity_timer.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/traits/v1/volume.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/traits/v1/wash_towel_mode.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/transport/__init__.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/transport/channel.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/transport/local_channel.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/transport/mqtt_channel.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/diagnostics.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/exceptions.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/map/__init__.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/map/b01_grid_layers.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/map/b01_map_parser.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/map/b01_q10_overlays.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/map/map_parser.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/map/proto/__init__.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/map/proto/b01_scmap.proto +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/map/proto/b01_scmap_pb2.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/map/room_colors.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/mqtt/__init__.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/mqtt/health_manager.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/mqtt/roborock_session.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/mqtt/session.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/protocol.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/protocols/__init__.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/protocols/a01_protocol.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/protocols/b01_q7_protocol.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/protocols/v1_protocol.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/py.typed +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/roborock_message.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/roborock_typing.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/testing/__init__.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/testing/a01_simulator.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/testing/b01_q10_simulator.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/testing/channel.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/testing/cloud.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/testing/simulator.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/testing/v1_simulator.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/util.py +0 -0
- {python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/web_api.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: python-roborock
|
|
3
|
-
Version: 7.
|
|
3
|
+
Version: 7.12.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 = "7.
|
|
3
|
+
version = "7.12.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"
|
|
@@ -67,6 +67,8 @@ dev = [
|
|
|
67
67
|
"syrupy>=4.9.1,<6",
|
|
68
68
|
"pdoc>=15.0.4,<17",
|
|
69
69
|
"pytest-cov>=7.0.0",
|
|
70
|
+
"zmqtt>=0.2.0,<0.3.0",
|
|
71
|
+
"pytest-benchmark>=5.1.0",
|
|
70
72
|
]
|
|
71
73
|
|
|
72
74
|
[tool.hatch.build.targets.sdist]
|
|
@@ -146,6 +148,8 @@ module = ["roborock.map.proto.*"]
|
|
|
146
148
|
ignore_errors = true
|
|
147
149
|
|
|
148
150
|
[tool.pytest.ini_options]
|
|
151
|
+
addopts = "-m 'not mqtt_broker'"
|
|
152
|
+
markers = ["mqtt_broker: tests requiring a real MQTT broker"]
|
|
149
153
|
asyncio_mode = "auto"
|
|
150
154
|
asyncio_default_fixture_loop_scope = "function"
|
|
151
155
|
timeout = 30
|
|
@@ -57,9 +57,10 @@ class WaterModes(RoborockModeEnum):
|
|
|
57
57
|
SMART_MODE = ("smart_mode", 209)
|
|
58
58
|
PURE_WATER_FLOW_START = ("slight", 221)
|
|
59
59
|
PURE_WATER_FLOW_SMALL = ("low", 225)
|
|
60
|
+
PURE_WATER_FLOW_GENTLE = ("gentle", 230)
|
|
60
61
|
PURE_WATER_FLOW_MIDDLE = ("medium", 235)
|
|
61
|
-
PURE_WATER_FLOW_LARGE = ("moderate",
|
|
62
|
-
PURE_WATER_SUPER_BEGIN = ("high",
|
|
62
|
+
PURE_WATER_FLOW_LARGE = ("moderate", 240)
|
|
63
|
+
PURE_WATER_SUPER_BEGIN = ("high", 245)
|
|
63
64
|
PURE_WATER_FLOW_END = ("extreme", 250)
|
|
64
65
|
|
|
65
66
|
|
|
@@ -91,12 +92,25 @@ WATER_SLIDE_MODE_MAPPING: dict[int, WaterModes] = {
|
|
|
91
92
|
200: WaterModes.OFF,
|
|
92
93
|
221: WaterModes.PURE_WATER_FLOW_START,
|
|
93
94
|
225: WaterModes.PURE_WATER_FLOW_SMALL,
|
|
95
|
+
230: WaterModes.PURE_WATER_FLOW_GENTLE,
|
|
94
96
|
235: WaterModes.PURE_WATER_FLOW_MIDDLE,
|
|
95
|
-
|
|
96
|
-
|
|
97
|
+
240: WaterModes.PURE_WATER_FLOW_LARGE,
|
|
98
|
+
245: WaterModes.PURE_WATER_SUPER_BEGIN,
|
|
97
99
|
250: WaterModes.PURE_WATER_FLOW_END,
|
|
98
100
|
}
|
|
99
101
|
|
|
102
|
+
# Water slide devices report the raw slider position (1-30) as 220 + position.
|
|
103
|
+
# The app groups positions into named ranges, keyed here by their last position.
|
|
104
|
+
_WATER_SLIDE_RANGES: tuple[tuple[int, WaterModes], ...] = (
|
|
105
|
+
(3, WaterModes.PURE_WATER_FLOW_START),
|
|
106
|
+
(7, WaterModes.PURE_WATER_FLOW_SMALL),
|
|
107
|
+
(12, WaterModes.PURE_WATER_FLOW_GENTLE),
|
|
108
|
+
(17, WaterModes.PURE_WATER_FLOW_MIDDLE),
|
|
109
|
+
(22, WaterModes.PURE_WATER_FLOW_LARGE),
|
|
110
|
+
(27, WaterModes.PURE_WATER_SUPER_BEGIN),
|
|
111
|
+
(30, WaterModes.PURE_WATER_FLOW_END),
|
|
112
|
+
)
|
|
113
|
+
|
|
100
114
|
ModeEnumT = TypeVar("ModeEnumT", bound=RoborockModeEnum)
|
|
101
115
|
|
|
102
116
|
|
|
@@ -183,6 +197,19 @@ def get_water_modes(features: DeviceFeatures) -> list[WaterModes]:
|
|
|
183
197
|
return supported_modes
|
|
184
198
|
|
|
185
199
|
|
|
200
|
+
def get_water_slide_mode_name(code: int) -> str | None:
|
|
201
|
+
"""Get the app's range name for a water slide code, or None if out of range."""
|
|
202
|
+
if code == 200:
|
|
203
|
+
return WaterModes.OFF.value
|
|
204
|
+
position = code - 220
|
|
205
|
+
if position < 1:
|
|
206
|
+
return None
|
|
207
|
+
for last_position, mode in _WATER_SLIDE_RANGES:
|
|
208
|
+
if position <= last_position:
|
|
209
|
+
return mode.value
|
|
210
|
+
return None
|
|
211
|
+
|
|
212
|
+
|
|
186
213
|
def get_water_mode_mapping(features: DeviceFeatures) -> dict[int, str]:
|
|
187
214
|
"""Get water mode mapping by supported feature set.
|
|
188
215
|
|
|
@@ -15,6 +15,7 @@ from roborock import (
|
|
|
15
15
|
get_current_cleaning_mode,
|
|
16
16
|
get_water_mode_mapping,
|
|
17
17
|
get_water_modes,
|
|
18
|
+
get_water_slide_mode_name,
|
|
18
19
|
resolve_cleaning_mode,
|
|
19
20
|
)
|
|
20
21
|
from roborock.devices.traits.common import DpsDataConverter, TraitUpdateListener
|
|
@@ -103,6 +104,8 @@ class StatusTrait(StatusV2, common.V1TraitMixin, TraitUpdateListener):
|
|
|
103
104
|
def water_mode_name(self) -> str | None:
|
|
104
105
|
if self.water_box_mode is None:
|
|
105
106
|
return None
|
|
107
|
+
if self._device_features_trait.is_water_slide_mode_supported:
|
|
108
|
+
return get_water_slide_mode_name(self.water_box_mode)
|
|
106
109
|
return self.water_mode_mapping.get(self.water_box_mode)
|
|
107
110
|
|
|
108
111
|
@property
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"""Parser for Roborock Q10 (B01/ss07) map packets.
|
|
2
2
|
|
|
3
|
-
Q10 devices deliver map data as
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Q10 devices deliver map data as protocol-301 ``MAP_RESPONSE`` pushes. Current
|
|
4
|
+
maps follow a read-only status request, while saved-map and clean-record detail
|
|
5
|
+
packets follow their respective ``select`` requests. Unlike the Q7 ``SCMap``
|
|
6
|
+
protobuf format, the Q10 uses a custom, unencrypted binary packet:
|
|
6
7
|
|
|
7
8
|
- ``01 01`` marker, then a ``u32be`` map id (bytes 2-5) and two consecutive
|
|
8
9
|
``u16be`` dimensions: grid width (bytes 7-8) and grid height (bytes 9-10).
|
|
@@ -22,6 +23,7 @@ https://github.com/v1b3c0d3x3r/roborock-qseries-map-bridge
|
|
|
22
23
|
import io
|
|
23
24
|
import math
|
|
24
25
|
import statistics
|
|
26
|
+
import struct
|
|
25
27
|
from dataclasses import dataclass, field, replace
|
|
26
28
|
|
|
27
29
|
from PIL import Image
|
|
@@ -30,6 +32,7 @@ from vacuum_map_parser_base.config.image_config import ImageConfig
|
|
|
30
32
|
from vacuum_map_parser_base.map_data import ImageData, MapData, Point
|
|
31
33
|
|
|
32
34
|
from roborock.data.b01_q10.b01_q10_containers import Q10RoborockPoint
|
|
35
|
+
from roborock.data.code_mappings import RoborockEnum
|
|
33
36
|
from roborock.data.containers import RoborockBase
|
|
34
37
|
from roborock.exceptions import RoborockException
|
|
35
38
|
|
|
@@ -67,9 +70,6 @@ def classify_q10_cell(value: int) -> str:
|
|
|
67
70
|
return LAYER_FLOOR
|
|
68
71
|
|
|
69
72
|
|
|
70
|
-
MAP_PACKET_MARKER = b"\x01\x01"
|
|
71
|
-
TRACE_PACKET_MARKER = b"\x02\x01"
|
|
72
|
-
|
|
73
73
|
_MAP_ID_OFFSET = 2
|
|
74
74
|
# Width and height are two consecutive u16be fields. An earlier revision read the
|
|
75
75
|
# width as u16le at offset 8; that high byte is actually the height's high byte,
|
|
@@ -84,6 +84,7 @@ _LAYOUT_COMPRESSED_OFFSET = 29
|
|
|
84
84
|
_ROOM_RECORD_LENGTH = 47
|
|
85
85
|
_ROOM_NAME_LENGTH_OFFSET = 26
|
|
86
86
|
_MAX_ROOMS = 32
|
|
87
|
+
_MAX_GRID_CELLS = 16_000_000
|
|
87
88
|
# Sanity bound for the erase-zone vector section's vertices-per-polygon field.
|
|
88
89
|
_MAX_ERASE_ZONE_VERTICES = 16
|
|
89
90
|
|
|
@@ -195,10 +196,34 @@ class Q10HeaderCalibration:
|
|
|
195
196
|
)
|
|
196
197
|
|
|
197
198
|
|
|
199
|
+
class Q10MapPacketKind(RoborockEnum):
|
|
200
|
+
"""Semantic kind identified by a Q10 map packet's two-byte marker."""
|
|
201
|
+
|
|
202
|
+
unknown = -1
|
|
203
|
+
CURRENT = 1
|
|
204
|
+
TRACE = 2
|
|
205
|
+
CLEAN_RECORD_DETAIL = 3
|
|
206
|
+
SAVED_MAP_DETAIL = 4
|
|
207
|
+
|
|
208
|
+
@property
|
|
209
|
+
def marker(self) -> bytes:
|
|
210
|
+
"""Return the two-byte wire marker for this packet kind."""
|
|
211
|
+
return b"" if self is self.unknown else bytes((self.value, 1))
|
|
212
|
+
|
|
213
|
+
@classmethod
|
|
214
|
+
def from_payload(cls, payload: bytes) -> "Q10MapPacketKind | None":
|
|
215
|
+
"""Return the recognized kind for a payload marker."""
|
|
216
|
+
if len(payload) < 2 or payload[1] != 1:
|
|
217
|
+
return None
|
|
218
|
+
kind = cls(payload[0])
|
|
219
|
+
return None if kind is cls.unknown else kind
|
|
220
|
+
|
|
221
|
+
|
|
198
222
|
@dataclass
|
|
199
223
|
class Q10MapPacket:
|
|
200
|
-
"""Decoded contents of a Q10
|
|
224
|
+
"""Decoded contents of a Q10 current or archived map packet."""
|
|
201
225
|
|
|
226
|
+
kind: Q10MapPacketKind
|
|
202
227
|
map_id: int
|
|
203
228
|
width: int
|
|
204
229
|
height: int
|
|
@@ -271,6 +296,33 @@ class Q10TracePacket:
|
|
|
271
296
|
return self.points[-1] if self.points else None
|
|
272
297
|
|
|
273
298
|
|
|
299
|
+
@dataclass
|
|
300
|
+
class Q10HistoricalTracePacket:
|
|
301
|
+
"""Cleaning path embedded in a Q10 ``03 01`` clean-record detail packet.
|
|
302
|
+
|
|
303
|
+
This is a different wire layout from the live ``02 01`` trace. Its header
|
|
304
|
+
carries a 16-bit format version, a 32-bit opaque value, a 32-bit
|
|
305
|
+
point count, a signed heading, and a zero reserved word. Points use the same
|
|
306
|
+
signed big-endian ``(x, y)`` coordinate pairs as the live trace.
|
|
307
|
+
"""
|
|
308
|
+
|
|
309
|
+
points: list[Q10Point] = field(default_factory=list)
|
|
310
|
+
heading: int = 0
|
|
311
|
+
|
|
312
|
+
@property
|
|
313
|
+
def robot_position(self) -> Q10Point | None:
|
|
314
|
+
"""The final recorded position, if the historical path is non-empty."""
|
|
315
|
+
return self.points[-1] if self.points else None
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
@dataclass
|
|
319
|
+
class Q10CleanRecordDetail:
|
|
320
|
+
"""A clean-record response containing a map and its recorded path."""
|
|
321
|
+
|
|
322
|
+
map: Q10MapPacket
|
|
323
|
+
trace: Q10HistoricalTracePacket | None = None
|
|
324
|
+
|
|
325
|
+
|
|
274
326
|
# Trace packet (``02 01``): a 14-byte header followed by big-endian int16 (x, y)
|
|
275
327
|
# point pairs forming the accumulated session path. Header layout confirmed
|
|
276
328
|
# against live ss07 captures and cross-checked by @andrewlyeats:
|
|
@@ -280,18 +332,26 @@ class Q10TracePacket:
|
|
|
280
332
|
# - bytes 10-11: the 0201 SLAM heading (s16be degrees; 0 = +x, +90 = +y,
|
|
281
333
|
# +-180 = -x, -90 = -y) -- the robot's current orientation.
|
|
282
334
|
# - bytes 12-13: a constant (0x0000).
|
|
283
|
-
# - byte 14 onward:
|
|
335
|
+
# - byte 14 onward: exactly ``point_count`` path points.
|
|
284
336
|
# An earlier revision used a 10-byte header, which folded the heading word into
|
|
285
337
|
# a phantom leading point ``(heading, 0)`` -- that is the "stray point" the
|
|
286
338
|
# heuristic below was papering over, and why the count read "one high". The
|
|
287
|
-
# parser
|
|
288
|
-
#
|
|
339
|
+
# parser requires the declared point count to match the complete body, so a
|
|
340
|
+
# truncated or extended tail cannot be silently interpreted as path data.
|
|
289
341
|
# NOTE: the format documented by roborock-qseries-map-bridge (18-byte header)
|
|
290
342
|
# did not match this firmware -- this 14-byte layout is what the device sent.
|
|
291
343
|
_TRACE_HEADER_LENGTH = 14
|
|
292
344
|
_TRACE_SEQUENCE_OFFSET = 3
|
|
345
|
+
_TRACE_POINT_COUNT_OFFSET = 8
|
|
293
346
|
_TRACE_HEADING_OFFSET = 10
|
|
294
347
|
|
|
348
|
+
_HISTORICAL_TRACE_HEADER_LENGTH = 14
|
|
349
|
+
_HISTORICAL_TRACE_PREFIX_LENGTH = 1
|
|
350
|
+
_HISTORICAL_TRACE_VERSION = 1
|
|
351
|
+
_HISTORICAL_TRACE_POINT_COUNT_OFFSET = 6
|
|
352
|
+
_HISTORICAL_TRACE_HEADING_OFFSET = 10
|
|
353
|
+
_HISTORICAL_TRACE_RESERVED_OFFSET = 12
|
|
354
|
+
|
|
295
355
|
# Some cleans still prepend a single near-origin sentinel as the first real
|
|
296
356
|
# point (e.g. ~(5, 76) / (-3, 0) when the path proper starts near (-1700, -800));
|
|
297
357
|
# it skews the rendered start/bounding box and any path-based calibration. (This
|
|
@@ -306,12 +366,12 @@ _STRAY_POINT_STEP_RATIO = 20
|
|
|
306
366
|
|
|
307
367
|
def is_map_packet(payload: bytes) -> bool:
|
|
308
368
|
"""Return True if the payload is a Q10 full-map (``01 01``) packet."""
|
|
309
|
-
return payload
|
|
369
|
+
return Q10MapPacketKind.from_payload(payload) is Q10MapPacketKind.CURRENT
|
|
310
370
|
|
|
311
371
|
|
|
312
372
|
def is_trace_packet(payload: bytes) -> bool:
|
|
313
373
|
"""Return True if the payload is a Q10 live trace (``02 01``) packet."""
|
|
314
|
-
return payload
|
|
374
|
+
return Q10MapPacketKind.from_payload(payload) is Q10MapPacketKind.TRACE
|
|
315
375
|
|
|
316
376
|
|
|
317
377
|
def parse_trace_packet(payload: bytes) -> Q10TracePacket:
|
|
@@ -323,6 +383,9 @@ def parse_trace_packet(payload: bytes) -> Q10TracePacket:
|
|
|
323
383
|
body = payload[_TRACE_HEADER_LENGTH:]
|
|
324
384
|
if len(body) % 4:
|
|
325
385
|
raise RoborockException("Q10 trace points are not 4-byte (x, y) pairs")
|
|
386
|
+
declared_point_count = int.from_bytes(payload[_TRACE_POINT_COUNT_OFFSET : _TRACE_POINT_COUNT_OFFSET + 2], "big")
|
|
387
|
+
if declared_point_count != len(body) // 4:
|
|
388
|
+
raise RoborockException("Q10 trace point count does not match its payload")
|
|
326
389
|
|
|
327
390
|
heading = int.from_bytes(payload[_TRACE_HEADING_OFFSET : _TRACE_HEADING_OFFSET + 2], "big", signed=True)
|
|
328
391
|
points = [
|
|
@@ -353,11 +416,13 @@ def _drop_stray_leading_point(points: list[Q10Point]) -> list[Q10Point]:
|
|
|
353
416
|
return points
|
|
354
417
|
|
|
355
418
|
|
|
356
|
-
def lz4_block_decompress(data: bytes) -> bytes:
|
|
419
|
+
def lz4_block_decompress(data: bytes, max_output_size: int) -> bytes:
|
|
357
420
|
"""Decompress a raw LZ4 *block* (no frame header).
|
|
358
421
|
|
|
359
422
|
The Q10 map grid is stored as a single LZ4 block. This implements the
|
|
360
|
-
standard LZ4 block format so we don't add a native dependency.
|
|
423
|
+
standard LZ4 block format so we don't add a native dependency. Expansion beyond
|
|
424
|
+
``max_output_size`` is rejected before
|
|
425
|
+
allocating the excess output.
|
|
361
426
|
"""
|
|
362
427
|
index = 0
|
|
363
428
|
output = bytearray()
|
|
@@ -385,6 +450,8 @@ def lz4_block_decompress(data: bytes) -> bytes:
|
|
|
385
450
|
end = index + literal_length
|
|
386
451
|
if end > len(data):
|
|
387
452
|
raise RoborockException("Truncated LZ4 block while reading literals")
|
|
453
|
+
if len(output) + literal_length > max_output_size:
|
|
454
|
+
raise RoborockException("LZ4 block exceeds maximum output size")
|
|
388
455
|
output.extend(data[index:end])
|
|
389
456
|
index = end
|
|
390
457
|
|
|
@@ -399,6 +466,8 @@ def lz4_block_decompress(data: bytes) -> bytes:
|
|
|
399
466
|
raise RoborockException("Invalid LZ4 back-reference offset")
|
|
400
467
|
|
|
401
468
|
match_length = read_length(token & 0x0F) + 4
|
|
469
|
+
if len(output) + match_length > max_output_size:
|
|
470
|
+
raise RoborockException("LZ4 block exceeds maximum output size")
|
|
402
471
|
for _ in range(match_length):
|
|
403
472
|
output.append(output[-offset])
|
|
404
473
|
|
|
@@ -463,8 +532,24 @@ def _parse_rooms(room_data: bytes, grid: bytes) -> list[Q10Room]:
|
|
|
463
532
|
|
|
464
533
|
|
|
465
534
|
def parse_map_packet(payload: bytes) -> Q10MapPacket:
|
|
466
|
-
"""Parse
|
|
467
|
-
|
|
535
|
+
"""Parse the raster and shared metadata of a current or archived map."""
|
|
536
|
+
packet, _, _ = _parse_map_layout(payload)
|
|
537
|
+
return packet
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def parse_clean_record_detail(payload: bytes) -> Q10CleanRecordDetail:
|
|
541
|
+
"""Parse a clean-record response into its map and optional recorded path."""
|
|
542
|
+
if Q10MapPacketKind.from_payload(payload) is not Q10MapPacketKind.CLEAN_RECORD_DETAIL:
|
|
543
|
+
raise RoborockException("Payload is not a Q10 clean-record detail packet")
|
|
544
|
+
packet, tail, trace_offset = _parse_map_layout(payload)
|
|
545
|
+
trace = _parse_clean_record_trace(tail, trace_offset) if trace_offset is not None else None
|
|
546
|
+
return Q10CleanRecordDetail(map=packet, trace=trace)
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def _parse_map_layout(payload: bytes) -> tuple[Q10MapPacket, bytes, int | None]:
|
|
550
|
+
"""Decode shared map fields and return the tail and historical path offset."""
|
|
551
|
+
kind = Q10MapPacketKind.from_payload(payload)
|
|
552
|
+
if len(payload) < _LAYOUT_COMPRESSED_OFFSET or kind is None or kind is Q10MapPacketKind.TRACE:
|
|
468
553
|
raise RoborockException("Payload is not a Q10 map packet")
|
|
469
554
|
|
|
470
555
|
map_id = int.from_bytes(payload[_MAP_ID_OFFSET : _MAP_ID_OFFSET + 4], "big")
|
|
@@ -472,6 +557,8 @@ def parse_map_packet(payload: bytes) -> Q10MapPacket:
|
|
|
472
557
|
height = int.from_bytes(payload[_HEIGHT_OFFSET : _HEIGHT_OFFSET + 2], "big")
|
|
473
558
|
if width <= 0:
|
|
474
559
|
raise RoborockException("Q10 map packet has invalid width")
|
|
560
|
+
if height > 0 and width * height > _MAX_GRID_CELLS:
|
|
561
|
+
raise RoborockException("Q10 map packet dimensions exceed the supported grid size")
|
|
475
562
|
|
|
476
563
|
compressed_length = int.from_bytes(
|
|
477
564
|
payload[_COMPRESSED_LAYOUT_LENGTH_OFFSET : _COMPRESSED_LAYOUT_LENGTH_OFFSET + 2], "big"
|
|
@@ -480,7 +567,10 @@ def parse_map_packet(payload: bytes) -> Q10MapPacket:
|
|
|
480
567
|
if compressed_length <= 0 or layout_end > len(payload):
|
|
481
568
|
raise RoborockException("Q10 map packet has invalid layout block length")
|
|
482
569
|
|
|
483
|
-
decoded = lz4_block_decompress(
|
|
570
|
+
decoded = lz4_block_decompress(
|
|
571
|
+
payload[_LAYOUT_COMPRESSED_OFFSET:layout_end],
|
|
572
|
+
max_output_size=_MAX_GRID_CELLS + 2 + _MAX_ROOMS * _ROOM_RECORD_LENGTH,
|
|
573
|
+
)
|
|
484
574
|
# Prefer the header height; fall back to inference if it doesn't line up
|
|
485
575
|
# (e.g. older captures/fixtures that don't populate the height field).
|
|
486
576
|
split = _split_with_dims(decoded, width, height) if height > 0 else None
|
|
@@ -491,9 +581,10 @@ def parse_map_packet(payload: bytes) -> Q10MapPacket:
|
|
|
491
581
|
rooms = _parse_rooms(room_data, grid)
|
|
492
582
|
tail = payload[layout_end:]
|
|
493
583
|
erase_zones = _parse_erase_zones(tail)
|
|
494
|
-
carpet_mask =
|
|
584
|
+
carpet_mask, carpet_end = _parse_carpet_block(tail, width, height)
|
|
495
585
|
header_calibration = _parse_header_calibration(payload)
|
|
496
|
-
|
|
586
|
+
packet = Q10MapPacket(
|
|
587
|
+
kind=kind,
|
|
497
588
|
map_id=map_id,
|
|
498
589
|
width=width,
|
|
499
590
|
height=height,
|
|
@@ -503,6 +594,7 @@ def parse_map_packet(payload: bytes) -> Q10MapPacket:
|
|
|
503
594
|
header_calibration=header_calibration,
|
|
504
595
|
carpet_mask=carpet_mask,
|
|
505
596
|
)
|
|
597
|
+
return packet, tail, carpet_end
|
|
506
598
|
|
|
507
599
|
|
|
508
600
|
def _parse_header_calibration(payload: bytes) -> Q10HeaderCalibration | None:
|
|
@@ -574,7 +666,18 @@ def _carpet_offset(tail: bytes) -> int:
|
|
|
574
666
|
return 2 + count * vertices_per * 4
|
|
575
667
|
|
|
576
668
|
|
|
577
|
-
def
|
|
669
|
+
def _erase_section_end(tail: bytes) -> int:
|
|
670
|
+
"""Return the end of a complete, structurally valid erase section."""
|
|
671
|
+
if len(tail) < 2:
|
|
672
|
+
return 0
|
|
673
|
+
count, vertices_per = tail[0], tail[1]
|
|
674
|
+
if count and not 1 <= vertices_per <= _MAX_ERASE_ZONE_VERTICES:
|
|
675
|
+
return 0
|
|
676
|
+
end = _carpet_offset(tail)
|
|
677
|
+
return end if end <= len(tail) else 0
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
def _parse_carpet_block(tail: bytes, width: int, height: int) -> tuple[bytes | None, int | None]:
|
|
578
681
|
"""Decode the carpet mask that follows the erase section in the packet tail.
|
|
579
682
|
|
|
580
683
|
Framing matches the main grid block: ``[u32 uncompressed_len]``
|
|
@@ -583,23 +686,73 @@ def _parse_carpet_mask(tail: bytes, width: int, height: int) -> bytes | None:
|
|
|
583
686
|
non-zero cell is carpet (the value is the carpet kind). Confirmed byte-exact
|
|
584
687
|
on live ss07 captures (R1 / RDC), where ``uncompressed_len == width*height``.
|
|
585
688
|
|
|
586
|
-
Returns the decompressed mask
|
|
587
|
-
not line up
|
|
588
|
-
|
|
689
|
+
Returns the decompressed mask and its end offset. Both are ``None`` if the
|
|
690
|
+
section is absent or does not line up. The end offset is used to anchor
|
|
691
|
+
optional later sections without scanning arbitrary trailing bytes.
|
|
589
692
|
"""
|
|
590
|
-
offset =
|
|
693
|
+
offset = _erase_section_end(tail)
|
|
694
|
+
if offset == 0:
|
|
695
|
+
return None, None
|
|
591
696
|
if offset + 6 > len(tail):
|
|
592
|
-
return None
|
|
697
|
+
return None, None
|
|
593
698
|
uncompressed_len = int.from_bytes(tail[offset : offset + 4], "big")
|
|
594
699
|
compressed_len = int.from_bytes(tail[offset + 4 : offset + 6], "big")
|
|
595
700
|
block_end = offset + 6 + compressed_len
|
|
596
701
|
if uncompressed_len != width * height or compressed_len <= 0 or block_end > len(tail):
|
|
597
|
-
return None
|
|
702
|
+
return None, None
|
|
598
703
|
try:
|
|
599
|
-
mask = lz4_block_decompress(tail[offset + 6 : block_end])
|
|
704
|
+
mask = lz4_block_decompress(tail[offset + 6 : block_end], max_output_size=width * height)
|
|
600
705
|
except RoborockException:
|
|
706
|
+
return None, None
|
|
707
|
+
if len(mask) != width * height:
|
|
708
|
+
return None, None
|
|
709
|
+
return mask, block_end
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
def _parse_clean_record_trace(
|
|
713
|
+
tail: bytes,
|
|
714
|
+
offset: int,
|
|
715
|
+
) -> Q10HistoricalTracePacket | None:
|
|
716
|
+
"""Decode the bounded historical path following a ``03 01`` carpet block.
|
|
717
|
+
|
|
718
|
+
The header and declared point count were validated against a physical ss07
|
|
719
|
+
clean-record response and its point bytes match captured prefixes of the
|
|
720
|
+
corresponding live trace exactly. One observed zero byte precedes the path;
|
|
721
|
+
its meaning is unknown, so a non-zero value makes the entire section opaque.
|
|
722
|
+
Any unsupported version, non-zero reserved word, or truncated point table is
|
|
723
|
+
likewise left completely opaque. Bytes after the declared points are
|
|
724
|
+
deliberately not consumed: the observed 12-byte suffix appears structured,
|
|
725
|
+
but there is not enough controlled evidence to name or decode it safely.
|
|
726
|
+
"""
|
|
727
|
+
if offset >= len(tail) or tail[offset] != 0:
|
|
728
|
+
return None
|
|
729
|
+
offset += _HISTORICAL_TRACE_PREFIX_LENGTH
|
|
730
|
+
header_end = offset + _HISTORICAL_TRACE_HEADER_LENGTH
|
|
731
|
+
if header_end > len(tail):
|
|
601
732
|
return None
|
|
602
|
-
|
|
733
|
+
version = int.from_bytes(tail[offset : offset + 2], "big")
|
|
734
|
+
reserved = int.from_bytes(
|
|
735
|
+
tail[offset + _HISTORICAL_TRACE_RESERVED_OFFSET : offset + _HISTORICAL_TRACE_RESERVED_OFFSET + 2],
|
|
736
|
+
"big",
|
|
737
|
+
)
|
|
738
|
+
if version != _HISTORICAL_TRACE_VERSION or reserved != 0:
|
|
739
|
+
return None
|
|
740
|
+
point_count = int.from_bytes(
|
|
741
|
+
tail[offset + _HISTORICAL_TRACE_POINT_COUNT_OFFSET : offset + _HISTORICAL_TRACE_POINT_COUNT_OFFSET + 4],
|
|
742
|
+
"big",
|
|
743
|
+
)
|
|
744
|
+
points_end = header_end + point_count * 4
|
|
745
|
+
if points_end > len(tail):
|
|
746
|
+
return None
|
|
747
|
+
coordinates = struct.iter_unpack(">hh", memoryview(tail)[header_end:points_end])
|
|
748
|
+
return Q10HistoricalTracePacket(
|
|
749
|
+
points=_drop_stray_leading_point([Q10Point(x=x, y=y) for x, y in coordinates]),
|
|
750
|
+
heading=int.from_bytes(
|
|
751
|
+
tail[offset + _HISTORICAL_TRACE_HEADING_OFFSET : offset + _HISTORICAL_TRACE_HEADING_OFFSET + 2],
|
|
752
|
+
"big",
|
|
753
|
+
signed=True,
|
|
754
|
+
),
|
|
755
|
+
)
|
|
603
756
|
|
|
604
757
|
|
|
605
758
|
def erased_packet(packet: "Q10MapPacket", cells: set[int]) -> "Q10MapPacket":
|
|
@@ -34,6 +34,7 @@ from .b01_q10_map_parser import (
|
|
|
34
34
|
B01Q10MapParser,
|
|
35
35
|
B01Q10MapParserConfig,
|
|
36
36
|
Q10EraseZone,
|
|
37
|
+
Q10HistoricalTracePacket,
|
|
37
38
|
Q10MapPacket,
|
|
38
39
|
Q10TracePacket,
|
|
39
40
|
erased_packet,
|
|
@@ -62,7 +63,6 @@ _MIN_CALIBRATION_POINTS = 20
|
|
|
62
63
|
# a much shorter path suffices to confirm it (early in a clean, not just a dense
|
|
63
64
|
# one). See :func:`solve_calibration_with_origin`.
|
|
64
65
|
_MIN_HEADER_CALIBRATION_POINTS = 4
|
|
65
|
-
|
|
66
66
|
_Q10_DRAWABLE_TYPES = {
|
|
67
67
|
Drawable.CHARGER,
|
|
68
68
|
Drawable.NO_GO_AREAS,
|
|
@@ -84,7 +84,7 @@ class Q10MapOverlays:
|
|
|
84
84
|
|
|
85
85
|
def render_q10_map(
|
|
86
86
|
packet: Q10MapPacket,
|
|
87
|
-
trace: Q10TracePacket | None,
|
|
87
|
+
trace: Q10TracePacket | Q10HistoricalTracePacket | None,
|
|
88
88
|
overlays: Q10MapOverlays,
|
|
89
89
|
*,
|
|
90
90
|
config: B01Q10MapParserConfig,
|
|
@@ -133,7 +133,7 @@ def render_q10_map(
|
|
|
133
133
|
|
|
134
134
|
def solve_q10_calibration(
|
|
135
135
|
packet: Q10MapPacket,
|
|
136
|
-
trace: Q10TracePacket | None,
|
|
136
|
+
trace: Q10TracePacket | Q10HistoricalTracePacket | None,
|
|
137
137
|
) -> GridCalibration | None:
|
|
138
138
|
"""Derive world-to-pixel calibration from a map and its current trace.
|
|
139
139
|
|
|
@@ -232,7 +232,7 @@ def _erased_cells(
|
|
|
232
232
|
def _place_trace(
|
|
233
233
|
map_data: MapData,
|
|
234
234
|
calibration: GridCalibration,
|
|
235
|
-
trace: Q10TracePacket,
|
|
235
|
+
trace: Q10TracePacket | Q10HistoricalTracePacket,
|
|
236
236
|
*,
|
|
237
237
|
charger_heading: int | None = None,
|
|
238
238
|
) -> None:
|
|
@@ -11,10 +11,11 @@ from roborock.data.b01_q10.b01_q10_code_mappings import B01_Q10_DP
|
|
|
11
11
|
from roborock.data.b01_q10.b01_q10_containers import Q10RoborockPoint
|
|
12
12
|
from roborock.exceptions import RoborockException
|
|
13
13
|
from roborock.map.b01_q10_map_parser import (
|
|
14
|
+
Q10CleanRecordDetail,
|
|
14
15
|
Q10MapPacket,
|
|
16
|
+
Q10MapPacketKind,
|
|
15
17
|
Q10TracePacket,
|
|
16
|
-
|
|
17
|
-
is_trace_packet,
|
|
18
|
+
parse_clean_record_detail,
|
|
18
19
|
parse_map_packet,
|
|
19
20
|
parse_trace_packet,
|
|
20
21
|
)
|
|
@@ -155,24 +156,27 @@ class Q10DpsUpdate:
|
|
|
155
156
|
# A single decoded message from a Q10 device: a DPS status update, a full map
|
|
156
157
|
# packet, or a live cleaning-path (trace) packet. Map/trace packets arrive as
|
|
157
158
|
# protocol-301 ``MAP_RESPONSE`` pushes; everything else is a DPS update.
|
|
158
|
-
Q10Message = Q10DpsUpdate | Q10MapPacket | Q10TracePacket
|
|
159
|
+
Q10Message = Q10DpsUpdate | Q10MapPacket | Q10TracePacket | Q10CleanRecordDetail
|
|
159
160
|
|
|
160
161
|
|
|
161
162
|
def decode_message(message: RoborockMessage) -> Q10Message | None:
|
|
162
163
|
"""Decode a pushed Q10 ``RoborockMessage`` into a typed message.
|
|
163
164
|
|
|
164
|
-
``MAP_RESPONSE`` (protocol 301) payloads carry
|
|
165
|
-
trace (``02 01``)
|
|
166
|
-
``
|
|
167
|
-
protocol is treated as a DPS status update.
|
|
165
|
+
``MAP_RESPONSE`` (protocol 301) payloads carry binary current-map (``01
|
|
166
|
+
01``), trace (``02 01``), clean-record detail (``03 01``), or saved-map
|
|
167
|
+
detail (``04 01``) packets. Any other marker is unrecognized and yields
|
|
168
|
+
``None``. Every other protocol is treated as a DPS status update.
|
|
168
169
|
|
|
169
170
|
Raises ``RoborockException`` if a recognized payload fails to parse.
|
|
170
171
|
"""
|
|
171
172
|
if message.protocol == RoborockMessageProtocol.MAP_RESPONSE:
|
|
172
173
|
payload = message.payload or b""
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
if is_trace_packet(payload):
|
|
174
|
+
kind = Q10MapPacketKind.from_payload(payload)
|
|
175
|
+
if kind is Q10MapPacketKind.TRACE:
|
|
176
176
|
return parse_trace_packet(payload)
|
|
177
|
+
if kind is Q10MapPacketKind.CLEAN_RECORD_DETAIL:
|
|
178
|
+
return parse_clean_record_detail(payload)
|
|
179
|
+
if kind is not None:
|
|
180
|
+
return parse_map_packet(payload)
|
|
177
181
|
return None
|
|
178
182
|
return Q10DpsUpdate(dps=decode_rpc_response(message))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/data/b01_q10/b01_q10_code_mappings.py
RENAMED
|
File without changes
|
{python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/data/b01_q10/b01_q10_containers.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/data/b01_q7/b01_q7_code_mappings.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/data/mower/mower_code_mappings.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/traits/a01/device_feature.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/traits/b01/q10/__init__.py
RENAMED
|
File without changes
|
{python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/traits/b01/q10/button_light.py
RENAMED
|
File without changes
|
{python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/traits/b01/q10/child_lock.py
RENAMED
|
File without changes
|
{python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/traits/b01/q10/clean_history.py
RENAMED
|
File without changes
|
{python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/traits/b01/q10/command.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/traits/b01/q10/consumable.py
RENAMED
|
File without changes
|
{python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/traits/b01/q10/do_not_disturb.py
RENAMED
|
File without changes
|
{python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/traits/b01/q10/dust_collection.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/traits/b01/q10/network_info.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/traits/b01/q7/__init__.py
RENAMED
|
File without changes
|
{python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/traits/b01/q7/clean_summary.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/traits/b01/q7/map_content.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/traits/v1/clean_summary.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/traits/v1/device_features.py
RENAMED
|
File without changes
|
{python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/traits/v1/do_not_disturb.py
RENAMED
|
File without changes
|
{python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/traits/v1/dust_collection_mode.py
RENAMED
|
File without changes
|
{python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/traits/v1/flow_led_status.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/traits/v1/network_info.py
RENAMED
|
File without changes
|
{python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/traits/v1/obstacle_photos.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/traits/v1/smart_wash_params.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/traits/v1/wash_towel_mode.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/transport/local_channel.py
RENAMED
|
File without changes
|
{python_roborock-7.11.1 → python_roborock-7.12.1}/roborock/devices/transport/mqtt_channel.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|