valetudo-map-parser 0.1.4__py3-none-any.whl → 0.1.6__py3-none-any.whl

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.
@@ -1,8 +1,9 @@
1
1
  """Valetudo map parser.
2
- Version: 0.1.4"""
3
-
2
+ Version: 0.1.6"""
4
3
 
5
4
  from .hypfer_handler import HypferMapImageHandler
5
+ from .rand25_handler import ReImageHandler
6
+ from .config.rand25_parser import RRMapParser
6
7
  from .config.shared import CameraShared, CameraSharedManager
7
8
  from .config.colors import ColorsManagment
8
9
  from .config.drawable import Drawable
@@ -17,6 +18,7 @@ from .config.types import (
17
18
 
18
19
  __all__ = [
19
20
  "HypferMapImageHandler",
21
+ "ReImageHandler",
20
22
  "CameraShared",
21
23
  "CameraSharedManager",
22
24
  "ColorsManagment",
@@ -71,18 +71,18 @@ class AutoCrop:
71
71
  return trimmed_width, trimmed_height
72
72
 
73
73
  async def _async_auto_crop_data(self, tdata=None):
74
- """Load the auto crop data from the disk."""
75
-
76
- if not self.imh.auto_crop:
77
- trims_data = TrimCropData.from_dict(dict(tdata)).to_list()
78
- (
79
- self.imh.trim_left,
80
- self.imh.trim_up,
81
- self.imh.trim_right,
82
- self.imh.trim_down,
83
- ) = trims_data
84
- self._calculate_trimmed_dimensions()
85
- return trims_data
74
+ """Load the auto crop data from the Camera config."""
75
+ # todo: implement this method but from config data
76
+ # if not self.imh.auto_crop:
77
+ # trims_data = TrimCropData.from_dict(dict(tdata)).to_list()
78
+ # (
79
+ # self.imh.trim_left,
80
+ # self.imh.trim_up,
81
+ # self.imh.trim_right,
82
+ # self.imh.trim_down,
83
+ # ) = trims_data
84
+ # self._calculate_trimmed_dimensions()
85
+ # return trims_data
86
86
  return None
87
87
 
88
88
  def auto_crop_offset(self):
@@ -0,0 +1,398 @@
1
+ """
2
+ Version: v2024.08.2
3
+ - This parser is the python version of @rand256 valetudo_mapper.
4
+ - This class is extracting the vacuum binary map_data.
5
+ - Additional functions are to get in our image_handler the images datas.
6
+ """
7
+
8
+ from enum import Enum
9
+ import math
10
+ import struct
11
+ from typing import Dict, List, Optional, Callable, TypeVar, Any
12
+
13
+ _CallableT = TypeVar("_CallableT", bound=Callable[..., Any])
14
+
15
+
16
+ def callback(func: _CallableT) -> _CallableT:
17
+ """Annotation to mark method as safe to call from within the event loop."""
18
+ setattr(func, "_hass_callback", True) # Attach a custom attribute to the function
19
+ return func # Return the function without modifying its behavior
20
+
21
+
22
+ # noinspection PyTypeChecker
23
+ class RRMapParser:
24
+ """Parse the map data from the Rand256 vacuum."""
25
+
26
+ def __init__(self):
27
+ self.map_data = None
28
+
29
+ class Tools:
30
+ """Tools for the RRMapParser."""
31
+
32
+ DIMENSION_PIXELS = 1024
33
+ DIMENSION_MM = 50 * 1024
34
+
35
+ class Types(Enum):
36
+ """Types of blocks in the RRMapParser."""
37
+
38
+ CHARGER_LOCATION = 1
39
+ IMAGE = 2
40
+ PATH = 3
41
+ GOTO_PATH = 4
42
+ GOTO_PREDICTED_PATH = 5
43
+ CURRENTLY_CLEANED_ZONES = 6
44
+ GOTO_TARGET = 7
45
+ ROBOT_POSITION = 8
46
+ FORBIDDEN_ZONES = 9
47
+ VIRTUAL_WALLS = 10
48
+ CURRENTLY_CLEANED_BLOCKS = 11
49
+ FORBIDDEN_MOP_ZONES = 12
50
+ DIGEST = 1024
51
+
52
+ @staticmethod
53
+ def parse_block(
54
+ buf: bytes,
55
+ offset: int,
56
+ result: Optional[Dict[int, Any]] = None,
57
+ pixels: bool = False,
58
+ ) -> Dict[int, Any]:
59
+ """Parse a block of data from the map data."""
60
+ result = result or {}
61
+ if len(buf) <= offset:
62
+ return result
63
+
64
+ type_ = struct.unpack("<H", buf[offset : offset + 2])[0]
65
+ hlength = struct.unpack("<H", buf[offset + 2 : offset + 4])[0]
66
+ length = struct.unpack("<I", buf[offset + 4 : offset + 8])[0]
67
+
68
+ if type_ in (
69
+ RRMapParser.Types.ROBOT_POSITION.value,
70
+ RRMapParser.Types.CHARGER_LOCATION.value,
71
+ ):
72
+ result[type_] = {
73
+ "position": [
74
+ int.from_bytes(buf[offset + 8 : offset + 10], byteorder="little"),
75
+ int.from_bytes(buf[offset + 12 : offset + 14], byteorder="little"),
76
+ ],
77
+ "angle": (
78
+ struct.unpack("<i", buf[offset + 16 : offset + 20])[0]
79
+ if length >= 12
80
+ else 0
81
+ ),
82
+ }
83
+ elif type_ == RRMapParser.Types.IMAGE.value:
84
+ RRMapParser._parse_image_block(buf, offset, length, hlength, result, pixels)
85
+ elif type_ in (
86
+ RRMapParser.Types.PATH.value,
87
+ RRMapParser.Types.GOTO_PATH.value,
88
+ RRMapParser.Types.GOTO_PREDICTED_PATH.value,
89
+ ):
90
+ result[type_] = RRMapParser._parse_path_block(buf, offset, length)
91
+ elif type_ == RRMapParser.Types.GOTO_TARGET.value:
92
+ result[type_] = {
93
+ "position": [
94
+ struct.unpack("<H", buf[offset + 8 : offset + 10])[0],
95
+ struct.unpack("<H", buf[offset + 10 : offset + 12])[0],
96
+ ]
97
+ }
98
+ elif type_ == RRMapParser.Types.CURRENTLY_CLEANED_ZONES.value:
99
+ result[type_] = RRMapParser._parse_cleaned_zones(buf, offset, length)
100
+ elif type_ in (
101
+ RRMapParser.Types.FORBIDDEN_ZONES.value,
102
+ RRMapParser.Types.FORBIDDEN_MOP_ZONES.value,
103
+ RRMapParser.Types.VIRTUAL_WALLS.value,
104
+ ):
105
+ result[type_] = RRMapParser._parse_forbidden_zones(buf, offset, length)
106
+ return RRMapParser.parse_block(buf, offset + length + hlength, result, pixels)
107
+
108
+ @staticmethod
109
+ def _parse_image_block(
110
+ buf: bytes,
111
+ offset: int,
112
+ length: int,
113
+ hlength: int,
114
+ result: Dict[int, Any],
115
+ pixels: bool,
116
+ ) -> None:
117
+ """Parse the image block of the map data."""
118
+ g3offset = 4 if hlength > 24 else 0
119
+ parameters = {
120
+ "segments": {
121
+ "count": (
122
+ struct.unpack("<i", buf[offset + 8 : offset + 12])[0]
123
+ if g3offset
124
+ else 0
125
+ ),
126
+ "id": [],
127
+ },
128
+ "position": {
129
+ "top": struct.unpack(
130
+ "<i", buf[offset + 8 + g3offset : offset + 12 + g3offset]
131
+ )[0],
132
+ "left": struct.unpack(
133
+ "<i", buf[offset + 12 + g3offset : offset + 16 + g3offset]
134
+ )[0],
135
+ },
136
+ "dimensions": {
137
+ "height": struct.unpack(
138
+ "<i", buf[offset + 16 + g3offset : offset + 20 + g3offset]
139
+ )[0],
140
+ "width": struct.unpack(
141
+ "<i", buf[offset + 20 + g3offset : offset + 24 + g3offset]
142
+ )[0],
143
+ },
144
+ "pixels": {"floor": [], "walls": [], "segments": {}},
145
+ }
146
+ parameters["position"]["top"] = (
147
+ RRMapParser.Tools.DIMENSION_PIXELS
148
+ - parameters["position"]["top"]
149
+ - parameters["dimensions"]["height"]
150
+ )
151
+ if (
152
+ parameters["dimensions"]["height"] > 0
153
+ and parameters["dimensions"]["width"] > 0
154
+ ):
155
+ for i in range(length):
156
+ segment_type = (
157
+ struct.unpack(
158
+ "<B",
159
+ buf[offset + 24 + g3offset + i : offset + 25 + g3offset + i],
160
+ )[0]
161
+ & 0x07
162
+ )
163
+ if segment_type == 0:
164
+ continue
165
+ elif segment_type == 1 and pixels:
166
+ parameters["pixels"]["walls"].append(i)
167
+ else:
168
+ s = (
169
+ struct.unpack(
170
+ "<B",
171
+ buf[
172
+ offset + 24 + g3offset + i : offset + 25 + g3offset + i
173
+ ],
174
+ )[0]
175
+ >> 3
176
+ )
177
+ if s == 0 and pixels:
178
+ parameters["pixels"]["floor"].append(i)
179
+ elif s != 0:
180
+ if s not in parameters["segments"]["id"]:
181
+ parameters["segments"]["id"].append(s)
182
+ parameters["segments"]["pixels_seg_" + str(s)] = []
183
+ if pixels:
184
+ parameters["segments"]["pixels_seg_" + str(s)].append(i)
185
+ result[RRMapParser.Types.IMAGE.value] = parameters
186
+
187
+ @staticmethod
188
+ def _parse_path_block(buf: bytes, offset: int, length: int) -> Dict[str, Any]:
189
+ """Parse a path block of the map data."""
190
+ points = [
191
+ [
192
+ struct.unpack("<H", buf[offset + 20 + i : offset + 22 + i])[0],
193
+ struct.unpack("<H", buf[offset + 22 + i : offset + 24 + i])[0],
194
+ ]
195
+ for i in range(0, length, 4)
196
+ ]
197
+ return {
198
+ "current_angle": struct.unpack("<I", buf[offset + 16 : offset + 20])[0],
199
+ "points": points,
200
+ }
201
+
202
+ @staticmethod
203
+ def _parse_cleaned_zones(buf: bytes, offset: int, length: int) -> List[List[int]]:
204
+ """Parse the cleaned zones block of the map data."""
205
+ zone_count = struct.unpack("<I", buf[offset + 8 : offset + 12])[0]
206
+ return (
207
+ [
208
+ [
209
+ struct.unpack("<H", buf[offset + 12 + i : offset + 14 + i])[0],
210
+ struct.unpack("<H", buf[offset + 14 + i : offset + 16 + i])[0],
211
+ struct.unpack("<H", buf[offset + 16 + i : offset + 18 + i])[0],
212
+ struct.unpack("<H", buf[offset + 18 + i : offset + 20 + i])[0],
213
+ ]
214
+ for i in range(0, length, 8)
215
+ ]
216
+ if zone_count > 0
217
+ else []
218
+ )
219
+
220
+ @staticmethod
221
+ def _parse_forbidden_zones(buf: bytes, offset: int, length: int) -> List[List[int]]:
222
+ """Parse the forbidden zones block of the map data."""
223
+ zone_count = struct.unpack("<I", buf[offset + 8 : offset + 12])[0]
224
+ return (
225
+ [
226
+ [
227
+ struct.unpack("<H", buf[offset + 12 + i : offset + 14 + i])[0],
228
+ struct.unpack("<H", buf[offset + 14 + i : offset + 16 + i])[0],
229
+ struct.unpack("<H", buf[offset + 16 + i : offset + 18 + i])[0],
230
+ struct.unpack("<H", buf[offset + 18 + i : offset + 20 + i])[0],
231
+ struct.unpack("<H", buf[offset + 20 + i : offset + 22 + i])[0],
232
+ struct.unpack("<H", buf[offset + 22 + i : offset + 24 + i])[0],
233
+ struct.unpack("<H", buf[offset + 24 + i : offset + 26 + i])[0],
234
+ struct.unpack("<H", buf[offset + 26 + i : offset + 28 + i])[0],
235
+ ]
236
+ for i in range(0, length, 16)
237
+ ]
238
+ if zone_count > 0
239
+ else []
240
+ )
241
+
242
+ @callback
243
+ def parse(self, map_buf: bytes) -> Dict[str, Any]:
244
+ """Parse the map data."""
245
+ if map_buf[0:2] == b"rr":
246
+ return {
247
+ "header_length": struct.unpack("<H", map_buf[2:4])[0],
248
+ "data_length": struct.unpack("<H", map_buf[4:6])[0],
249
+ "version": {
250
+ "major": struct.unpack("<H", map_buf[8:10])[0],
251
+ "minor": struct.unpack("<H", map_buf[10:12])[0],
252
+ },
253
+ "map_index": struct.unpack("<H", map_buf[12:14])[0],
254
+ "map_sequence": struct.unpack("<H", map_buf[16:18])[0],
255
+ }
256
+ return {}
257
+
258
+ @callback
259
+ def parse_rrm_data(
260
+ self, map_buf: bytes, pixels: bool = False
261
+ ) -> Optional[Dict[str, Any]]:
262
+ """Parse the complete map data."""
263
+ if not self.parse(map_buf).get("map_index"):
264
+ return None
265
+
266
+ parsed_map_data = {}
267
+ blocks = self.parse_block(map_buf, 0x14, None, pixels)
268
+
269
+ if RRMapParser.Types.IMAGE.value in blocks:
270
+ parsed_map_data["image"] = blocks[RRMapParser.Types.IMAGE.value]
271
+ for item in [
272
+ {"type": RRMapParser.Types.PATH.value, "path": "path"},
273
+ {
274
+ "type": RRMapParser.Types.GOTO_PREDICTED_PATH.value,
275
+ "path": "goto_predicted_path",
276
+ },
277
+ ]:
278
+ if item["type"] in blocks:
279
+ parsed_map_data[item["path"]] = blocks[item["type"]]
280
+ parsed_map_data[item["path"]]["points"] = [
281
+ [point[0], RRMapParser.Tools.DIMENSION_MM - point[1]]
282
+ for point in parsed_map_data[item["path"]]["points"]
283
+ ]
284
+ if len(parsed_map_data[item["path"]]["points"]) >= 2:
285
+ parsed_map_data[item["path"]]["current_angle"] = math.degrees(
286
+ math.atan2(
287
+ parsed_map_data[item["path"]]["points"][-1][1]
288
+ - parsed_map_data[item["path"]]["points"][-2][1],
289
+ parsed_map_data[item["path"]]["points"][-1][0]
290
+ - parsed_map_data[item["path"]]["points"][-2][0],
291
+ )
292
+ )
293
+ if RRMapParser.Types.CHARGER_LOCATION.value in blocks:
294
+ charger = blocks[RRMapParser.Types.CHARGER_LOCATION.value]["position"]
295
+ # Assume no transformation needed here
296
+ parsed_map_data["charger"] = charger
297
+
298
+ if RRMapParser.Types.ROBOT_POSITION.value in blocks:
299
+ robot = blocks[RRMapParser.Types.ROBOT_POSITION.value]["position"]
300
+ rob_angle = blocks[RRMapParser.Types.ROBOT_POSITION.value]["angle"]
301
+ # Assume no transformation needed here
302
+ parsed_map_data["robot"] = robot
303
+ parsed_map_data["robot_angle"] = rob_angle
304
+
305
+ if RRMapParser.Types.GOTO_TARGET.value in blocks:
306
+ parsed_map_data["goto_target"] = blocks[
307
+ RRMapParser.Types.GOTO_TARGET.value
308
+ ]["position"]
309
+ # Assume no transformation needed here
310
+
311
+ if RRMapParser.Types.CURRENTLY_CLEANED_ZONES.value in blocks:
312
+ parsed_map_data["currently_cleaned_zones"] = blocks[
313
+ RRMapParser.Types.CURRENTLY_CLEANED_ZONES.value
314
+ ]
315
+ parsed_map_data["currently_cleaned_zones"] = [
316
+ [
317
+ zone[0],
318
+ RRMapParser.Tools.DIMENSION_MM - zone[1],
319
+ zone[2],
320
+ RRMapParser.Tools.DIMENSION_MM - zone[3],
321
+ ]
322
+ for zone in parsed_map_data["currently_cleaned_zones"]
323
+ ]
324
+
325
+ if RRMapParser.Types.FORBIDDEN_ZONES.value in blocks:
326
+ parsed_map_data["forbidden_zones"] = blocks[
327
+ RRMapParser.Types.FORBIDDEN_ZONES.value
328
+ ]
329
+ parsed_map_data["forbidden_zones"] = [
330
+ [
331
+ zone[0],
332
+ RRMapParser.Tools.DIMENSION_MM - zone[1],
333
+ zone[2],
334
+ RRMapParser.Tools.DIMENSION_MM - zone[3],
335
+ zone[4],
336
+ RRMapParser.Tools.DIMENSION_MM - zone[5],
337
+ zone[6],
338
+ RRMapParser.Tools.DIMENSION_MM - zone[7],
339
+ ]
340
+ for zone in parsed_map_data["forbidden_zones"]
341
+ ]
342
+
343
+ if RRMapParser.Types.VIRTUAL_WALLS.value in blocks:
344
+ parsed_map_data["virtual_walls"] = blocks[
345
+ RRMapParser.Types.VIRTUAL_WALLS.value
346
+ ]
347
+ parsed_map_data["virtual_walls"] = [
348
+ [
349
+ wall[0],
350
+ RRMapParser.Tools.DIMENSION_MM - wall[1],
351
+ wall[2],
352
+ RRMapParser.Tools.DIMENSION_MM - wall[3],
353
+ ]
354
+ for wall in parsed_map_data["virtual_walls"]
355
+ ]
356
+
357
+ if RRMapParser.Types.CURRENTLY_CLEANED_BLOCKS.value in blocks:
358
+ parsed_map_data["currently_cleaned_blocks"] = blocks[
359
+ RRMapParser.Types.CURRENTLY_CLEANED_BLOCKS.value
360
+ ]
361
+
362
+ if RRMapParser.Types.FORBIDDEN_MOP_ZONES.value in blocks:
363
+ parsed_map_data["forbidden_mop_zones"] = blocks[
364
+ RRMapParser.Types.FORBIDDEN_MOP_ZONES.value
365
+ ]
366
+ parsed_map_data["forbidden_mop_zones"] = [
367
+ [
368
+ zone[0],
369
+ RRMapParser.Tools.DIMENSION_MM - zone[1],
370
+ zone[2],
371
+ RRMapParser.Tools.DIMENSION_MM - zone[3],
372
+ zone[4],
373
+ RRMapParser.Tools.DIMENSION_MM - zone[5],
374
+ zone[6],
375
+ RRMapParser.Tools.DIMENSION_MM - zone[7],
376
+ ]
377
+ for zone in parsed_map_data["forbidden_mop_zones"]
378
+ ]
379
+
380
+ return parsed_map_data
381
+
382
+ def parse_data(
383
+ self, payload: Optional[bytes] = None, pixels: bool = False
384
+ ) -> Optional[Dict[str, Any]]:
385
+ """Get the map data from MQTT and return the json."""
386
+ if payload:
387
+ self.map_data = self.parse(payload)
388
+ self.map_data.update(self.parse_rrm_data(payload, pixels) or {})
389
+ return self.map_data
390
+
391
+ def get_image(self) -> Dict[str, Any]:
392
+ """Get the image data from the map data."""
393
+ return self.map_data.get("image", {})
394
+
395
+ @staticmethod
396
+ def get_int32(data: bytes, address: int) -> int:
397
+ """Get a 32-bit integer from the data."""
398
+ return struct.unpack_from("<i", data, address)[0]
@@ -66,10 +66,10 @@ class HypferMapImageHandler:
66
66
  self.max_frames = 1024
67
67
  self.zooming = False # zooming the image.
68
68
  self.svg_wait = False # SVG image creation wait.
69
- self.trim_down = None # memory stored trims calculated once.
70
- self.trim_left = None # memory stored trims calculated once.
71
- self.trim_right = None # memory stored trims calculated once.
72
- self.trim_up = None # memory stored trims calculated once.
69
+ self.trim_down = 0 # memory stored trims calculated once.
70
+ self.trim_left = 0 # memory stored trims calculated once.
71
+ self.trim_right = 0 # memory stored trims calculated once.
72
+ self.trim_up = 0 # memory stored trims calculated once.
73
73
  self.offset_top = self.shared.offset_top # offset top
74
74
  self.offset_bottom = self.shared.offset_down # offset bottom
75
75
  self.offset_left = self.shared.offset_left # offset left
@@ -3,12 +3,11 @@ Collections of Json and List routines
3
3
  ImageData is part of the Image_Handler
4
4
  used functions to search data in the json
5
5
  provided for the creation of the new camera frame
6
- Version: v2024.08.0
6
+ Version: v0.1.6
7
7
  """
8
8
 
9
9
  from __future__ import annotations
10
10
 
11
- from typing import Any
12
11
  import numpy as np
13
12
 
14
13
  from .config.types import (
@@ -186,7 +185,7 @@ class ImageData:
186
185
  if rand:
187
186
  x, y, _ = entry # Extract x and y coordinates
188
187
  max_x = max(max_x, x) # Update max x coordinate
189
- max_y = max(max_y, y) # Update max y coordinate
188
+ max_y = max(max_y, y + pixel_size) # Update max y coordinate
190
189
  min_x = min(min_x, x) # Update min x coordinate
191
190
  min_y = min(min_y, y) # Update min y coordinate
192
191
  else:
@@ -227,7 +226,7 @@ class ImageData:
227
226
  compressed_pixels = []
228
227
 
229
228
  tot_pixels = 0
230
- current_x, current_y, count = None, None, 0 # pylint: disable=unused-variable
229
+ current_x, current_y, count = None, None, 0
231
230
  for index in pixel_data:
232
231
  x = (index % image_width) + image_left
233
232
  y = ((image_height - 1) - (index // image_width)) + image_top