@@ -24,6 +24,14 @@ class Types(Enum):
2424 VIRTUAL_WALLS = 10
2525 CURRENTLY_CLEANED_BLOCKS = 11
2626 FORBIDDEN_MOP_ZONES = 12
27+ OBSTACLES = 13
28+ IGNORED_OBSTACLES = 14
29+ OBSTACLES_WITH_PHOTO = 15
30+ IGNORED_OBSTACLES_WITH_PHOTO = 16
31+ CARPET_MAP = 17
32+ MOP_PATH = 18
33+ NO_CARPET_AREAS = 19
34+ DIGEST = 1024
2735
2836 class Tools :
2937 """Tools for coordinate transformations."""
@@ -33,6 +41,7 @@ class Tools:
3341
3442 def __init__ (self ):
3543 """Initialize the parser."""
44+ self .is_valid = False
3645 self .map_data : Dict [str , Any ] = {}
3746
3847 # Xiaomi/Roborock style byte extraction methods
@@ -67,6 +76,61 @@ def _get_int32_signed(data: bytes, address: int) -> int:
6776 value = RRMapParser ._get_int32 (data , address )
6877 return value if value < 0x80000000 else value - 0x100000000
6978
79+ @staticmethod
80+ def _parse_carpet_map (data : bytes ) -> set [int ]:
81+ carpet_map = set ()
82+
83+ for i , v in enumerate (data ):
84+ if v :
85+ carpet_map .add (i )
86+ return carpet_map
87+
88+ @staticmethod
89+ def _parse_area (header : bytes , data : bytes ) -> list :
90+ area_pairs = RRMapParser ._get_int16 (header , 0x08 )
91+ areas = []
92+ for area_start in range (0 , area_pairs * 16 , 16 ):
93+ x0 = RRMapParser ._get_int16 (data , area_start + 0 )
94+ y0 = RRMapParser ._get_int16 (data , area_start + 2 )
95+ x1 = RRMapParser ._get_int16 (data , area_start + 4 )
96+ y1 = RRMapParser ._get_int16 (data , area_start + 6 )
97+ x2 = RRMapParser ._get_int16 (data , area_start + 8 )
98+ y2 = RRMapParser ._get_int16 (data , area_start + 10 )
99+ x3 = RRMapParser ._get_int16 (data , area_start + 12 )
100+ y3 = RRMapParser ._get_int16 (data , area_start + 14 )
101+ areas .append (
102+ [
103+ x0 ,
104+ RRMapParser .Tools .DIMENSION_MM - y0 ,
105+ x1 ,
106+ RRMapParser .Tools .DIMENSION_MM - y1 ,
107+ x2 ,
108+ RRMapParser .Tools .DIMENSION_MM - y2 ,
109+ x3 ,
110+ RRMapParser .Tools .DIMENSION_MM - y3 ,
111+ ]
112+ )
113+ return areas
114+
115+ @staticmethod
116+ def _parse_zones (data : bytes , header : bytes ) -> list :
117+ zone_pairs = RRMapParser ._get_int16 (header , 0x08 )
118+ zones = []
119+ for zone_start in range (0 , zone_pairs * 8 , 8 ):
120+ x0 = RRMapParser ._get_int16 (data , zone_start + 0 )
121+ y0 = RRMapParser ._get_int16 (data , zone_start + 2 )
122+ x1 = RRMapParser ._get_int16 (data , zone_start + 4 )
123+ y1 = RRMapParser ._get_int16 (data , zone_start + 6 )
124+ zones .append (
125+ [
126+ x0 ,
127+ RRMapParser .Tools .DIMENSION_MM - y0 ,
128+ x1 ,
129+ RRMapParser .Tools .DIMENSION_MM - y1 ,
130+ ]
131+ )
132+ return zones
133+
70134 @staticmethod
71135 def _parse_object_position (block_data_length : int , data : bytes ) -> Dict [str , Any ]:
72136 """Parse object position using Xiaomi method."""
@@ -82,6 +146,19 @@ def _parse_object_position(block_data_length: int, data: bytes) -> Dict[str, Any
82146 angle = raw_angle
83147 return {"position" : [x , y ], "angle" : angle }
84148
149+
150+ @staticmethod
151+ def _parse_walls (data : bytes , header : bytes ) -> list :
152+ wall_pairs = RRMapParser ._get_int16 (header , 0x08 )
153+ walls = []
154+ for wall_start in range (0 , wall_pairs * 8 , 8 ):
155+ x0 = RRMapParser ._get_int16 (data , wall_start + 0 )
156+ y0 = RRMapParser ._get_int16 (data , wall_start + 2 )
157+ x1 = RRMapParser ._get_int16 (data , wall_start + 4 )
158+ y1 = RRMapParser ._get_int16 (data , wall_start + 6 )
159+ walls .append ([x0 , RRMapParser .Tools .DIMENSION_MM - y0 , x1 , RRMapParser .Tools .DIMENSION_MM - y1 ])
160+ return walls
161+
85162 @staticmethod
86163 def _parse_path_block (buf : bytes , offset : int , length : int ) -> Dict [str , Any ]:
87164 """Parse path block using EXACT same method as working parser."""
@@ -127,59 +204,45 @@ def parse(self, map_buf: bytes) -> Dict[str, Any]:
127204 return {}
128205
129206 def parse_blocks (self , raw : bytes , pixels : bool = True ) -> Dict [int , Any ]:
130- """Parse all blocks using Xiaomi method."""
131207 blocks = {}
132208 map_header_length = self ._get_int16 (raw , 0x02 )
133209 block_start_position = map_header_length
134-
135210 while block_start_position < len (raw ):
136211 try :
137- # Parse block header using Xiaomi method
138212 block_header_length = self ._get_int16 (raw , block_start_position + 0x02 )
139213 header = self ._get_bytes (raw , block_start_position , block_header_length )
140214 block_type = self ._get_int16 (header , 0x00 )
141215 block_data_length = self ._get_int32 (header , 0x04 )
142216 block_data_start = block_start_position + block_header_length
143217 data = self ._get_bytes (raw , block_data_start , block_data_length )
144-
145- # Parse different block types
146- if block_type == self .Types .ROBOT_POSITION .value :
147- blocks [block_type ] = self ._parse_object_position (
148- block_data_length , data
149- )
150- elif block_type == self .Types .CHARGER_LOCATION .value :
151- blocks [block_type ] = self ._parse_object_position (
152- block_data_length , data
153- )
154- elif block_type == self .Types .PATH .value :
155- blocks [block_type ] = self ._parse_path_block (
156- raw , block_start_position , block_data_length
157- )
158- elif block_type == self .Types .GOTO_PREDICTED_PATH .value :
159- blocks [block_type ] = self ._parse_path_block (
160- raw , block_start_position , block_data_length
161- )
162- elif block_type == self .Types .GOTO_TARGET .value :
163- blocks [block_type ] = {"position" : self ._parse_goto_target (data )}
164- elif block_type == self .Types .IMAGE .value :
165- # Get header length for Gen1/Gen3 detection
166- header_length = self ._get_int8 (header , 2 )
167- blocks [block_type ] = self ._parse_image_block (
168- raw ,
169- block_start_position ,
170- block_data_length ,
171- header_length ,
172- pixels ,
173- )
174-
175- # Move to next block using Xiaomi method
176- block_start_position = (
177- block_start_position + block_data_length + self ._get_int8 (header , 2 )
178- )
179-
218+ match block_type :
219+ case self .Types .DIGEST .value :
220+ self .is_valid = True
221+ case self .Types .ROBOT_POSITION .value | self .Types .CHARGER_LOCATION .value :
222+ blocks [block_type ] = self ._parse_object_position (block_data_length , data )
223+ case self .Types .PATH .value | self .Types .GOTO_PREDICTED_PATH .value :
224+ blocks [block_type ] = self ._parse_path_block (raw , block_start_position , block_data_length )
225+ case self .Types .CURRENTLY_CLEANED_ZONES .value :
226+ blocks [block_type ] = {"zones" : self ._parse_zones (data , header )}
227+ case self .Types .FORBIDDEN_ZONES .value :
228+ blocks [block_type ] = {"forbidden_zones" : self ._parse_area (header , data )}
229+ case self .Types .FORBIDDEN_MOP_ZONES .value :
230+ blocks [block_type ] = {"forbidden_mop_zones" : self ._parse_area (header , data )}
231+ case self .Types .GOTO_TARGET .value :
232+ blocks [block_type ] = {"position" : self ._parse_goto_target (data )}
233+ case self .Types .VIRTUAL_WALLS .value :
234+ blocks [block_type ] = {"virtual_walls" : self ._parse_walls (data , header )}
235+ case self .Types .CARPET_MAP .value :
236+ data = RRMapParser ._get_bytes (raw , block_data_start , block_data_length )
237+ blocks [block_type ] = {"carpet_map" : self ._parse_carpet_map (data )}
238+ case self .Types .IMAGE .value :
239+ header_length = self ._get_int8 (header , 2 )
240+ blocks [block_type ] = self ._parse_image_block (
241+ raw , block_start_position , block_data_length , header_length , pixels )
242+
243+ block_start_position = block_start_position + block_data_length + self ._get_int8 (header , 2 )
180244 except (struct .error , IndexError ):
181245 break
182-
183246 return blocks
184247
185248 def _parse_image_block (
@@ -365,8 +428,32 @@ def parse_rrm_data(
365428 ]
366429
367430 # Add missing fields to match expected JSON format
368- parsed_map_data ["forbidden_zones" ] = []
369- parsed_map_data ["virtual_walls" ] = []
431+ parsed_map_data ["currently_cleaned_zones" ] = (
432+ blocks [self .Types .CURRENTLY_CLEANED_ZONES .value ]["zones" ]
433+ if self .Types .CURRENTLY_CLEANED_ZONES .value in blocks
434+ else []
435+ )
436+ parsed_map_data ["forbidden_zones" ] = (
437+ blocks [self .Types .FORBIDDEN_ZONES .value ]["forbidden_zones" ]
438+ if self .Types .FORBIDDEN_ZONES .value in blocks
439+ else []
440+ )
441+ parsed_map_data ["forbidden_mop_zones" ] = (
442+ blocks [self .Types .FORBIDDEN_MOP_ZONES .value ]["forbidden_mop_zones" ]
443+ if self .Types .FORBIDDEN_MOP_ZONES .value in blocks
444+ else []
445+ )
446+ parsed_map_data ["virtual_walls" ] = (
447+ blocks [self .Types .VIRTUAL_WALLS .value ]["virtual_walls" ]
448+ if self .Types .VIRTUAL_WALLS .value in blocks
449+ else []
450+ )
451+ parsed_map_data ["carpet_areas" ] = (
452+ blocks [self .Types .CARPET_MAP .value ]["carpet_map" ]
453+ if self .Types .CARPET_MAP .value in blocks
454+ else []
455+ )
456+ parsed_map_data ["is_valid" ] = self .is_valid
370457
371458 return parsed_map_data
372459
@@ -388,8 +475,3 @@ def parse_data(
388475 except (struct .error , IndexError , ValueError ):
389476 return None
390477 return self .map_data
391-
392- @staticmethod
393- def get_int32 (data : bytes , address : int ) -> int :
394- """Get a 32-bit integer from the data - kept for compatibility."""
395- return struct .unpack_from ("<i" , data , address )[0 ]
0 commit comments