1515import hashlib
1616import io
1717import zlib
18- from dataclasses import dataclass , field
18+ from dataclasses import dataclass
1919
2020from Crypto .Cipher import AES
2121from Crypto .Util .Padding import pad , unpad
2525from vacuum_map_parser_base .map_data import ImageData , MapData
2626
2727from roborock .exceptions import RoborockException
28- from roborock .map .proto .b01_scmap_pb2 import ( # type: ignore[attr-defined]
29- DevicePointInfo ,
30- MapBoundaryInfo ,
31- MapExtInfo ,
32- MapHeadInfo ,
33- RobotMap ,
34- RoomDataInfo ,
35- )
28+ from roborock .map .proto .b01_scmap_pb2 import RobotMap # type: ignore[attr-defined]
3629
3730from .map_parser import ParsedMapData
3831
3932_B64_CHARS = set (b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=" )
4033_MAP_FILE_FORMAT = "PNG"
4134
4235
43- @dataclass (frozen = True )
44- class _ScPoint :
45- x : float | None = None
46- y : float | None = None
47-
48-
49- @dataclass (frozen = True )
50- class _ScMapBoundaryInfo :
51- map_md5 : str | None = None
52- v_min_x : int | None = None
53- v_max_x : int | None = None
54- v_min_y : int | None = None
55- v_max_y : int | None = None
56-
57-
58- @dataclass (frozen = True )
59- class _ScMapExtInfo :
60- task_begin_date : int | None = None
61- map_upload_date : int | None = None
62- map_valid : int | None = None
63- radian : int | None = None
64- force : int | None = None
65- clean_path : int | None = None
66- boundary_info : _ScMapBoundaryInfo | None = None
67- map_version : int | None = None
68- map_value_type : int | None = None
69-
70-
71- @dataclass (frozen = True )
72- class _ScMapHead :
73- map_head_id : int | None = None
74- size_x : int | None = None
75- size_y : int | None = None
76- min_x : float | None = None
77- min_y : float | None = None
78- max_x : float | None = None
79- max_y : float | None = None
80- resolution : float | None = None
81-
82-
83- @dataclass (frozen = True )
84- class _ScRoomData :
85- room_id : int | None = None
86- room_name : str | None = None
87- room_type_id : int | None = None
88- material_id : int | None = None
89- clean_state : int | None = None
90- room_clean : int | None = None
91- room_clean_index : int | None = None
92- room_name_post : _ScPoint | None = None
93- color_id : int | None = None
94- floor_direction : int | None = None
95- global_seq : int | None = None
96-
97-
98- @dataclass (frozen = True )
99- class _ScMapPayload :
100- map_type : int | None = None
101- map_ext_info : _ScMapExtInfo | None = None
102- map_head : _ScMapHead | None = None
103- map_data : bytes | None = None
104- room_data_info : tuple [_ScRoomData , ...] = field (default_factory = tuple )
105-
106-
10736@dataclass
10837class B01MapParserConfig :
10938 """Configuration for the B01/Q7 map parser."""
@@ -121,9 +50,9 @@ def __init__(self, config: B01MapParserConfig | None = None) -> None:
12150 def parse (self , raw_payload : bytes , * , serial : str , model : str ) -> ParsedMapData :
12251 """Parse a raw MAP_RESPONSE payload and return a PNG + MapData."""
12352 inflated = _decode_b01_map_payload (raw_payload , serial = serial , model = model )
124- scmap = _parse_scmap_payload (inflated )
125- size_x , size_y , grid = _extract_grid (scmap )
126- room_names = _extract_room_names (scmap . room_data_info )
53+ parsed = _parse_scmap_payload (inflated )
54+ size_x , size_y , grid = _extract_grid (parsed )
55+ room_names = _extract_room_names (parsed )
12756
12857 image = _render_occupancy_image (grid , size_x = size_x , size_y = size_y , scale = self ._config .map_scale )
12958
@@ -203,108 +132,37 @@ def _decode_map_data_bytes(value: bytes) -> bytes:
203132 return value
204133
205134
206- def _parse_sc_point (parsed : DevicePointInfo ) -> _ScPoint :
207- return _ScPoint (
208- x = parsed .x if parsed .HasField ("x" ) else None ,
209- y = parsed .y if parsed .HasField ("y" ) else None ,
210- )
211-
212-
213- def _parse_sc_map_boundary_info (parsed : MapBoundaryInfo ) -> _ScMapBoundaryInfo :
214- return _ScMapBoundaryInfo (
215- map_md5 = parsed .mapMd5 if parsed .HasField ("mapMd5" ) else None ,
216- v_min_x = parsed .vMinX if parsed .HasField ("vMinX" ) else None ,
217- v_max_x = parsed .vMaxX if parsed .HasField ("vMaxX" ) else None ,
218- v_min_y = parsed .vMinY if parsed .HasField ("vMinY" ) else None ,
219- v_max_y = parsed .vMaxY if parsed .HasField ("vMaxY" ) else None ,
220- )
221-
222-
223- def _parse_sc_map_ext_info (parsed : MapExtInfo ) -> _ScMapExtInfo :
224- return _ScMapExtInfo (
225- task_begin_date = parsed .taskBeginDate if parsed .HasField ("taskBeginDate" ) else None ,
226- map_upload_date = parsed .mapUploadDate if parsed .HasField ("mapUploadDate" ) else None ,
227- map_valid = parsed .mapValid if parsed .HasField ("mapValid" ) else None ,
228- radian = parsed .radian if parsed .HasField ("radian" ) else None ,
229- force = parsed .force if parsed .HasField ("force" ) else None ,
230- clean_path = parsed .cleanPath if parsed .HasField ("cleanPath" ) else None ,
231- boundary_info = _parse_sc_map_boundary_info (parsed .boudaryInfo ) if parsed .HasField ("boudaryInfo" ) else None ,
232- map_version = parsed .mapVersion if parsed .HasField ("mapVersion" ) else None ,
233- map_value_type = parsed .mapValueType if parsed .HasField ("mapValueType" ) else None ,
234- )
235-
236-
237- def _parse_sc_map_head (parsed : MapHeadInfo ) -> _ScMapHead :
238- return _ScMapHead (
239- map_head_id = parsed .mapHeadId if parsed .HasField ("mapHeadId" ) else None ,
240- size_x = parsed .sizeX if parsed .HasField ("sizeX" ) else None ,
241- size_y = parsed .sizeY if parsed .HasField ("sizeY" ) else None ,
242- min_x = parsed .minX if parsed .HasField ("minX" ) else None ,
243- min_y = parsed .minY if parsed .HasField ("minY" ) else None ,
244- max_x = parsed .maxX if parsed .HasField ("maxX" ) else None ,
245- max_y = parsed .maxY if parsed .HasField ("maxY" ) else None ,
246- resolution = parsed .resolution if parsed .HasField ("resolution" ) else None ,
247- )
248-
249-
250- def _parse_sc_room_data (parsed : RoomDataInfo ) -> _ScRoomData :
251- return _ScRoomData (
252- room_id = parsed .roomId if parsed .HasField ("roomId" ) else None ,
253- room_name = parsed .roomName if parsed .HasField ("roomName" ) else None ,
254- room_type_id = parsed .roomTypeId if parsed .HasField ("roomTypeId" ) else None ,
255- material_id = parsed .meterialId if parsed .HasField ("meterialId" ) else None ,
256- clean_state = parsed .cleanState if parsed .HasField ("cleanState" ) else None ,
257- room_clean = parsed .roomClean if parsed .HasField ("roomClean" ) else None ,
258- room_clean_index = parsed .roomCleanIndex if parsed .HasField ("roomCleanIndex" ) else None ,
259- room_name_post = _parse_sc_point (parsed .roomNamePost ) if parsed .HasField ("roomNamePost" ) else None ,
260- color_id = parsed .colorId if parsed .HasField ("colorId" ) else None ,
261- floor_direction = parsed .floor_direction if parsed .HasField ("floor_direction" ) else None ,
262- global_seq = parsed .global_seq if parsed .HasField ("global_seq" ) else None ,
263- )
264-
265-
266- def _parse_scmap_payload (payload : bytes ) -> _ScMapPayload :
267- """Parse inflated SCMap bytes into typed map metadata."""
135+ def _parse_scmap_payload (payload : bytes ) -> RobotMap :
136+ """Parse inflated SCMap bytes into a generated protobuf message."""
268137 parsed = RobotMap ()
269138 _parse_proto (payload , parsed , context = "B01 SCMap" )
270-
271- map_data = None
272- if parsed .HasField ("mapData" ):
273- if not parsed .mapData .HasField ("mapData" ):
274- raise RoborockException ("B01 map payload missing mapData" )
275- map_data = _decode_map_data_bytes (parsed .mapData .mapData )
276-
277- return _ScMapPayload (
278- map_type = parsed .mapType if parsed .HasField ("mapType" ) else None ,
279- map_ext_info = _parse_sc_map_ext_info (parsed .mapExtInfo ) if parsed .HasField ("mapExtInfo" ) else None ,
280- map_head = _parse_sc_map_head (parsed .mapHead ) if parsed .HasField ("mapHead" ) else None ,
281- map_data = map_data ,
282- room_data_info = tuple (_parse_sc_room_data (room ) for room in parsed .roomDataInfo ),
283- )
139+ return parsed
284140
285141
286- def _extract_grid (scmap : _ScMapPayload ) -> tuple [int , int , bytes ]:
287- if scmap . map_head is None or scmap . map_data is None :
142+ def _extract_grid (parsed : RobotMap ) -> tuple [int , int , bytes ]:
143+ if not parsed . HasField ( "mapHead" ) or not parsed . HasField ( "mapData" ) :
288144 raise RoborockException ("Failed to parse B01 map header/grid" )
289145
290- size_x = scmap . map_head . size_x or 0
291- size_y = scmap . map_head . size_y or 0
292- if not size_x or not size_y or not scmap . map_data :
146+ size_x = parsed . mapHead . sizeX if parsed . mapHead . HasField ( "sizeX" ) else 0
147+ size_y = parsed . mapHead . sizeY if parsed . mapHead . HasField ( "sizeY" ) else 0
148+ if not size_x or not size_y or not parsed . mapData . HasField ( "mapData" ) :
293149 raise RoborockException ("Failed to parse B01 map header/grid" )
294150
151+ map_data = _decode_map_data_bytes (parsed .mapData .mapData )
295152 expected_len = size_x * size_y
296- if len (scmap . map_data ) < expected_len :
153+ if len (map_data ) < expected_len :
297154 raise RoborockException ("B01 map data shorter than expected dimensions" )
298155
299- return size_x , size_y , scmap . map_data [:expected_len ]
156+ return size_x , size_y , map_data [:expected_len ]
300157
301158
302- def _extract_room_names (rooms : tuple [ _ScRoomData , ...] ) -> dict [int , str ]:
159+ def _extract_room_names (parsed : RobotMap ) -> dict [int , str ]:
303160 # Expose room id/name mapping without inventing room geometry/polygons.
304161 room_names : dict [int , str ] = {}
305- for room in rooms :
306- if room .room_id is not None :
307- room_names [room .room_id ] = room .room_name or f"Room { room .room_id } "
162+ for room in parsed .roomDataInfo :
163+ if room .HasField ("roomId" ):
164+ room_id = room .roomId
165+ room_names [room_id ] = room .roomName if room .HasField ("roomName" ) else f"Room { room_id } "
308166 return room_names
309167
310168
0 commit comments