valetudo-map-parser 0.1.9b65__tar.gz → 0.1.9b67__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.
Files changed (28) hide show
  1. {valetudo_map_parser-0.1.9b65 → valetudo_map_parser-0.1.9b67}/PKG-INFO +1 -1
  2. {valetudo_map_parser-0.1.9b65 → valetudo_map_parser-0.1.9b67}/SCR/valetudo_map_parser/config/drawable.py +87 -6
  3. {valetudo_map_parser-0.1.9b65 → valetudo_map_parser-0.1.9b67}/SCR/valetudo_map_parser/hypfer_handler.py +9 -3
  4. {valetudo_map_parser-0.1.9b65 → valetudo_map_parser-0.1.9b67}/SCR/valetudo_map_parser/rand256_handler.py +40 -4
  5. {valetudo_map_parser-0.1.9b65 → valetudo_map_parser-0.1.9b67}/pyproject.toml +1 -1
  6. valetudo_map_parser-0.1.9b65/SCR/valetudo_map_parser/config/rand25_parser.py +0 -412
  7. {valetudo_map_parser-0.1.9b65 → valetudo_map_parser-0.1.9b67}/LICENSE +0 -0
  8. {valetudo_map_parser-0.1.9b65 → valetudo_map_parser-0.1.9b67}/NOTICE.txt +0 -0
  9. {valetudo_map_parser-0.1.9b65 → valetudo_map_parser-0.1.9b67}/README.md +0 -0
  10. {valetudo_map_parser-0.1.9b65 → valetudo_map_parser-0.1.9b67}/SCR/valetudo_map_parser/__init__.py +0 -0
  11. {valetudo_map_parser-0.1.9b65 → valetudo_map_parser-0.1.9b67}/SCR/valetudo_map_parser/config/__init__.py +0 -0
  12. {valetudo_map_parser-0.1.9b65 → valetudo_map_parser-0.1.9b67}/SCR/valetudo_map_parser/config/async_utils.py +0 -0
  13. {valetudo_map_parser-0.1.9b65 → valetudo_map_parser-0.1.9b67}/SCR/valetudo_map_parser/config/auto_crop.py +0 -0
  14. {valetudo_map_parser-0.1.9b65 → valetudo_map_parser-0.1.9b67}/SCR/valetudo_map_parser/config/color_utils.py +0 -0
  15. {valetudo_map_parser-0.1.9b65 → valetudo_map_parser-0.1.9b67}/SCR/valetudo_map_parser/config/colors.py +0 -0
  16. {valetudo_map_parser-0.1.9b65 → valetudo_map_parser-0.1.9b67}/SCR/valetudo_map_parser/config/drawable_elements.py +0 -0
  17. {valetudo_map_parser-0.1.9b65 → valetudo_map_parser-0.1.9b67}/SCR/valetudo_map_parser/config/enhanced_drawable.py +0 -0
  18. {valetudo_map_parser-0.1.9b65 → valetudo_map_parser-0.1.9b67}/SCR/valetudo_map_parser/config/optimized_element_map.py +0 -0
  19. {valetudo_map_parser-0.1.9b65 → valetudo_map_parser-0.1.9b67}/SCR/valetudo_map_parser/config/rand256_parser.py +0 -0
  20. {valetudo_map_parser-0.1.9b65 → valetudo_map_parser-0.1.9b67}/SCR/valetudo_map_parser/config/shared.py +0 -0
  21. {valetudo_map_parser-0.1.9b65 → valetudo_map_parser-0.1.9b67}/SCR/valetudo_map_parser/config/types.py +0 -0
  22. {valetudo_map_parser-0.1.9b65 → valetudo_map_parser-0.1.9b67}/SCR/valetudo_map_parser/config/utils.py +0 -0
  23. {valetudo_map_parser-0.1.9b65 → valetudo_map_parser-0.1.9b67}/SCR/valetudo_map_parser/hypfer_draw.py +0 -0
  24. {valetudo_map_parser-0.1.9b65 → valetudo_map_parser-0.1.9b67}/SCR/valetudo_map_parser/hypfer_rooms_handler.py +0 -0
  25. {valetudo_map_parser-0.1.9b65 → valetudo_map_parser-0.1.9b67}/SCR/valetudo_map_parser/map_data.py +0 -0
  26. {valetudo_map_parser-0.1.9b65 → valetudo_map_parser-0.1.9b67}/SCR/valetudo_map_parser/py.typed +0 -0
  27. {valetudo_map_parser-0.1.9b65 → valetudo_map_parser-0.1.9b67}/SCR/valetudo_map_parser/reimg_draw.py +0 -0
  28. {valetudo_map_parser-0.1.9b65 → valetudo_map_parser-0.1.9b67}/SCR/valetudo_map_parser/rooms_handler.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: valetudo-map-parser
3
- Version: 0.1.9b65
3
+ Version: 0.1.9b67
4
4
  Summary: A Python library to parse Valetudo map data returning a PIL Image object.
5
5
  License: Apache-2.0
6
6
  Author: Sandro Cantarella
@@ -14,6 +14,7 @@ import logging
14
14
  import math
15
15
  import asyncio
16
16
  import inspect
17
+ import threading
17
18
 
18
19
  import numpy as np
19
20
  from PIL import ImageDraw, ImageFont
@@ -26,6 +27,72 @@ from .types import Color, NumpyArray, PilPNG, Point, Tuple, Union
26
27
  _LOGGER = logging.getLogger(__name__)
27
28
 
28
29
 
30
+ class ImageArrayPool:
31
+ """Thread-safe memory pool for reusing image arrays to reduce allocation overhead."""
32
+
33
+ def __init__(self, max_arrays_per_size: int = 3):
34
+ self._pools = {} # {(width, height): [array1, array2, ...]}
35
+ self._lock = threading.Lock()
36
+ self._max_arrays_per_size = max_arrays_per_size
37
+
38
+ def get_array(self, width: int, height: int, background_color: Color) -> NumpyArray:
39
+ """Get a reusable array or create a new one if none available."""
40
+ key = (width, height)
41
+
42
+ with self._lock:
43
+ if key in self._pools and self._pools[key]:
44
+ # Reuse existing array
45
+ array = self._pools[key].pop()
46
+ _LOGGER.debug("Reused array from pool for size %dx%d", width, height)
47
+ else:
48
+ # Create new array
49
+ array = np.empty((height, width, 4), dtype=np.uint8)
50
+ _LOGGER.debug("Created new array for size %dx%d", width, height)
51
+
52
+ # Fill with background color (outside lock for better performance)
53
+ array[:] = background_color
54
+ return array
55
+
56
+ def return_array(self, array: NumpyArray) -> None:
57
+ """Return an array to the pool for reuse."""
58
+ if array is None:
59
+ return
60
+
61
+ height, width = array.shape[:2]
62
+ key = (width, height)
63
+
64
+ with self._lock:
65
+ if key not in self._pools:
66
+ self._pools[key] = []
67
+
68
+ # Only keep up to max_arrays_per_size arrays per size
69
+ if len(self._pools[key]) < self._max_arrays_per_size:
70
+ self._pools[key].append(array)
71
+ _LOGGER.debug("Returned array to pool for size %dx%d (pool size: %d)",
72
+ width, height, len(self._pools[key]))
73
+ else:
74
+ _LOGGER.debug("Pool full for size %dx%d, discarding array", width, height)
75
+
76
+ def clear_pool(self) -> None:
77
+ """Clear all arrays from the pool."""
78
+ with self._lock:
79
+ total_arrays = sum(len(arrays) for arrays in self._pools.values())
80
+ self._pools.clear()
81
+ _LOGGER.debug("Cleared image array pool (%d arrays freed)", total_arrays)
82
+
83
+ def get_pool_stats(self) -> dict:
84
+ """Get statistics about the current pool state."""
85
+ with self._lock:
86
+ stats = {}
87
+ for (width, height), arrays in self._pools.items():
88
+ stats[f"{width}x{height}"] = len(arrays)
89
+ return stats
90
+
91
+
92
+ # Global shared pool instance for both Hypfer and Rand256 handlers
93
+ _image_pool = ImageArrayPool()
94
+
95
+
29
96
  class Drawable:
30
97
  """
31
98
  Collection of drawing utility functions for the image handlers.
@@ -45,13 +112,27 @@ class Drawable:
45
112
  async def create_empty_image(
46
113
  width: int, height: int, background_color: Color
47
114
  ) -> NumpyArray:
48
- """Create the empty background image NumPy array.
115
+ """Create the empty background image NumPy array using memory pool for better performance.
49
116
  Background color is specified as an RGBA tuple.
50
- Optimized: Uses np.empty + broadcast instead of np.full for better performance."""
51
- # Use np.empty + broadcast instead of np.full (avoids double initialization)
52
- img_array = np.empty((height, width, 4), dtype=np.uint8)
53
- img_array[:] = background_color # Broadcast color to all pixels efficiently
54
- return img_array
117
+ Optimized: Uses shared memory pool to reuse arrays and reduce allocation overhead."""
118
+ # Get array from shared pool (reuses memory when possible)
119
+ return _image_pool.get_array(width, height, background_color)
120
+
121
+ @staticmethod
122
+ def return_image_to_pool(image_array: NumpyArray) -> None:
123
+ """Return an image array to the memory pool for reuse.
124
+ Call this when you're done with an image array to enable memory reuse."""
125
+ _image_pool.return_array(image_array)
126
+
127
+ @staticmethod
128
+ def get_pool_stats() -> dict:
129
+ """Get statistics about the current memory pool state."""
130
+ return _image_pool.get_pool_stats()
131
+
132
+ @staticmethod
133
+ def clear_image_pool() -> None:
134
+ """Clear all arrays from the memory pool."""
135
+ _image_pool.clear_pool()
55
136
 
56
137
  @staticmethod
57
138
  async def from_json_to_image(
@@ -390,7 +390,9 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
390
390
  if self.check_zoom_and_aspect_ratio():
391
391
  # Convert to PIL for resizing
392
392
  pil_img = await AsyncPIL.async_fromarray(img_np_array, mode="RGBA")
393
- del img_np_array
393
+ # Return array to pool for reuse instead of just deleting
394
+ from .config.drawable import Drawable
395
+ Drawable.return_image_to_pool(img_np_array)
394
396
  resize_params = prepare_resize_params(self, pil_img, False)
395
397
  resized_image = await self.async_resize_images(resize_params)
396
398
 
@@ -405,13 +407,17 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
405
407
  if return_webp:
406
408
  # Convert directly from NumPy to WebP for better performance
407
409
  webp_bytes = await numpy_to_webp_bytes(img_np_array)
408
- del img_np_array
410
+ # Return array to pool for reuse instead of just deleting
411
+ from .config.drawable import Drawable
412
+ Drawable.return_image_to_pool(img_np_array)
409
413
  LOGGER.debug("%s: Frame Completed.", self.file_name)
410
414
  return webp_bytes
411
415
  else:
412
416
  # Convert to PIL Image (original behavior)
413
417
  pil_img = await AsyncPIL.async_fromarray(img_np_array, mode="RGBA")
414
- del img_np_array
418
+ # Return array to pool for reuse instead of just deleting
419
+ from .config.drawable import Drawable
420
+ Drawable.return_image_to_pool(img_np_array)
415
421
  LOGGER.debug("%s: Frame Completed.", self.file_name)
416
422
  return pil_img
417
423
  except (RuntimeError, RuntimeWarning) as e:
@@ -7,6 +7,7 @@ Version: 0.1.9.a6
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
+ import asyncio
10
11
  import logging
11
12
  import uuid
12
13
  from typing import Any
@@ -14,6 +15,7 @@ from typing import Any
14
15
  import numpy as np
15
16
  from PIL import Image
16
17
 
18
+ from .config.async_utils import AsyncNumPy, AsyncPIL, AsyncParallel
17
19
  from .config.auto_crop import AutoCrop
18
20
  from .config.drawable_elements import DrawableElement
19
21
  from .config.types import (
@@ -166,6 +168,14 @@ class ReImageHandler(BaseHandler, AutoCrop):
166
168
  self.json_id = str(uuid.uuid4()) # image id
167
169
  _LOGGER.info("Vacuum Data ID: %s", self.json_id)
168
170
 
171
+ # Prepare parallel data extraction tasks
172
+ data_tasks = []
173
+ data_tasks.append(self._prepare_zone_data(m_json))
174
+ data_tasks.append(self._prepare_path_data(m_json))
175
+
176
+ # Execute data preparation tasks in parallel
177
+ zone_data, path_data = await asyncio.gather(*data_tasks, return_exceptions=True)
178
+
169
179
  (
170
180
  img_np_array,
171
181
  robot_position,
@@ -192,12 +202,16 @@ class ReImageHandler(BaseHandler, AutoCrop):
192
202
  if return_webp:
193
203
  # Convert directly to WebP bytes for better performance
194
204
  webp_bytes = await numpy_to_webp_bytes(img_np_array)
195
- del img_np_array # free memory
205
+ # Return array to pool for reuse instead of just deleting
206
+ from .config.drawable import Drawable
207
+ Drawable.return_image_to_pool(img_np_array)
196
208
  return webp_bytes
197
209
  else:
198
- # Convert to PIL Image (original behavior)
199
- pil_img = Image.fromarray(img_np_array, mode="RGBA")
200
- del img_np_array # free memory
210
+ # Convert to PIL Image using async utilities
211
+ pil_img = await AsyncPIL.async_fromarray(img_np_array, mode="RGBA")
212
+ # Return array to pool for reuse instead of just deleting
213
+ from .config.drawable import Drawable
214
+ Drawable.return_image_to_pool(img_np_array)
201
215
  return await self._finalize_image(pil_img)
202
216
 
203
217
  except (RuntimeError, RuntimeWarning) as e:
@@ -673,3 +687,25 @@ class ReImageHandler(BaseHandler, AutoCrop):
673
687
  property_name=property_name,
674
688
  value=value,
675
689
  )
690
+
691
+ async def async_copy_array(self, original_array):
692
+ """Copy the array using async utilities."""
693
+ return await AsyncNumPy.async_copy(original_array)
694
+
695
+ async def _prepare_zone_data(self, m_json):
696
+ """Prepare zone data for parallel processing."""
697
+ await asyncio.sleep(0) # Yield control
698
+ try:
699
+ return self.data.find_zone_entities(m_json)
700
+ except (ValueError, KeyError):
701
+ return None
702
+
703
+ async def _prepare_path_data(self, m_json):
704
+ """Prepare path data for parallel processing."""
705
+ await asyncio.sleep(0) # Yield control
706
+ try:
707
+ return self.data.find_path_entities(m_json)
708
+ except (ValueError, KeyError):
709
+ return None
710
+
711
+
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "valetudo-map-parser"
3
- version = "0.1.9b65"
3
+ version = "0.1.9b67"
4
4
  description = "A Python library to parse Valetudo map data returning a PIL Image object."
5
5
  authors = ["Sandro Cantarella <gsca075@gmail.com>"]
6
6
  license = "Apache-2.0"
@@ -1,412 +0,0 @@
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
- import math
9
- import struct
10
- from enum import Enum
11
- from typing import Any, Callable, Dict, List, Optional, TypeVar
12
-
13
-
14
- _CallableT = TypeVar("_CallableT", bound=Callable[..., Any])
15
-
16
-
17
- def callback(func: _CallableT) -> _CallableT:
18
- """Annotation to mark method as safe to call from within the event loop."""
19
- setattr(func, "_hass_callback", True) # Attach a custom attribute to the function
20
- return func # Return the function without modifying its behavior
21
-
22
-
23
- # noinspection PyTypeChecker
24
- class RRMapParser:
25
- """Parse the map data from the Rand256 vacuum."""
26
-
27
- def __init__(self):
28
- self.map_data = None
29
-
30
- class Tools:
31
- """Tools for the RRMapParser."""
32
-
33
- DIMENSION_PIXELS = 1024
34
- DIMENSION_MM = 50 * 1024
35
-
36
- class Types(Enum):
37
- """Types of blocks in the RRMapParser."""
38
-
39
- CHARGER_LOCATION = 1
40
- IMAGE = 2
41
- PATH = 3
42
- GOTO_PATH = 4
43
- GOTO_PREDICTED_PATH = 5
44
- CURRENTLY_CLEANED_ZONES = 6
45
- GOTO_TARGET = 7
46
- ROBOT_POSITION = 8
47
- FORBIDDEN_ZONES = 9
48
- VIRTUAL_WALLS = 10
49
- CURRENTLY_CLEANED_BLOCKS = 11
50
- FORBIDDEN_MOP_ZONES = 12
51
- DIGEST = 1024
52
-
53
- @staticmethod
54
- def parse_block(
55
- buf: bytes,
56
- offset: int,
57
- result: Optional[Dict[int, Any]] = None,
58
- pixels: bool = False,
59
- ) -> Dict[int, Any]:
60
- """Parse a block of data from the map data."""
61
- result = result or {}
62
- if len(buf) <= offset:
63
- return result
64
-
65
- type_ = struct.unpack("<H", buf[offset : offset + 2])[0]
66
- hlength = struct.unpack("<H", buf[offset + 2 : offset + 4])[0]
67
- length = struct.unpack("<I", buf[offset + 4 : offset + 8])[0]
68
-
69
- if type_ in (
70
- RRMapParser.Types.ROBOT_POSITION.value,
71
- RRMapParser.Types.CHARGER_LOCATION.value,
72
- ):
73
- result[type_] = {
74
- "position": [
75
- int.from_bytes(buf[offset + 8 : offset + 10], byteorder="little"),
76
- int.from_bytes(buf[offset + 12 : offset + 14], byteorder="little"),
77
- ],
78
- "angle": (
79
- struct.unpack("<i", buf[offset + 16 : offset + 20])[0]
80
- if length >= 12
81
- else 0
82
- ),
83
- }
84
- elif type_ == RRMapParser.Types.IMAGE.value:
85
- RRMapParser._parse_image_block(buf, offset, length, hlength, result, pixels)
86
- elif type_ in (
87
- RRMapParser.Types.PATH.value,
88
- RRMapParser.Types.GOTO_PATH.value,
89
- RRMapParser.Types.GOTO_PREDICTED_PATH.value,
90
- ):
91
- result[type_] = RRMapParser._parse_path_block(buf, offset, length)
92
- elif type_ == RRMapParser.Types.GOTO_TARGET.value:
93
- result[type_] = {
94
- "position": [
95
- struct.unpack("<H", buf[offset + 8 : offset + 10])[0],
96
- struct.unpack("<H", buf[offset + 10 : offset + 12])[0],
97
- ]
98
- }
99
- elif type_ == RRMapParser.Types.CURRENTLY_CLEANED_ZONES.value:
100
- result[type_] = RRMapParser._parse_cleaned_zones(buf, offset, length)
101
- elif type_ in (
102
- RRMapParser.Types.FORBIDDEN_ZONES.value,
103
- RRMapParser.Types.FORBIDDEN_MOP_ZONES.value,
104
- RRMapParser.Types.VIRTUAL_WALLS.value,
105
- ):
106
- result[type_] = RRMapParser._parse_forbidden_zones(buf, offset, length)
107
- return RRMapParser.parse_block(buf, offset + length + hlength, result, pixels)
108
-
109
- @staticmethod
110
- def _parse_image_block(
111
- buf: bytes,
112
- offset: int,
113
- length: int,
114
- hlength: int,
115
- result: Dict[int, Any],
116
- pixels: bool,
117
- ) -> None:
118
- """Parse the image block of the map data."""
119
- g3offset = 4 if hlength > 24 else 0
120
- parameters = {
121
- "segments": {
122
- "count": (
123
- struct.unpack("<i", buf[offset + 8 : offset + 12])[0]
124
- if g3offset
125
- else 0
126
- ),
127
- "id": [],
128
- },
129
- "position": {
130
- "top": struct.unpack(
131
- "<i", buf[offset + 8 + g3offset : offset + 12 + g3offset]
132
- )[0],
133
- "left": struct.unpack(
134
- "<i", buf[offset + 12 + g3offset : offset + 16 + g3offset]
135
- )[0],
136
- },
137
- "dimensions": {
138
- "height": struct.unpack(
139
- "<i", buf[offset + 16 + g3offset : offset + 20 + g3offset]
140
- )[0],
141
- "width": struct.unpack(
142
- "<i", buf[offset + 20 + g3offset : offset + 24 + g3offset]
143
- )[0],
144
- },
145
- "pixels": {"floor": [], "walls": [], "segments": {}},
146
- }
147
- parameters["position"]["top"] = (
148
- RRMapParser.Tools.DIMENSION_PIXELS
149
- - parameters["position"]["top"]
150
- - parameters["dimensions"]["height"]
151
- )
152
- if (
153
- parameters["dimensions"]["height"] > 0
154
- and parameters["dimensions"]["width"] > 0
155
- ):
156
- for i in range(length):
157
- segment_type = (
158
- struct.unpack(
159
- "<B",
160
- buf[offset + 24 + g3offset + i : offset + 25 + g3offset + i],
161
- )[0]
162
- & 0x07
163
- )
164
- if segment_type == 0:
165
- continue
166
- if segment_type == 1 and pixels:
167
- parameters["pixels"]["walls"].append(i)
168
- else:
169
- s = (
170
- struct.unpack(
171
- "<B",
172
- buf[
173
- offset + 24 + g3offset + i : offset + 25 + g3offset + i
174
- ],
175
- )[0]
176
- >> 3
177
- )
178
- if s == 0 and pixels:
179
- parameters["pixels"]["floor"].append(i)
180
- elif s != 0:
181
- if s not in parameters["segments"]["id"]:
182
- parameters["segments"]["id"].append(s)
183
- parameters["segments"]["pixels_seg_" + str(s)] = []
184
- if pixels:
185
- parameters["segments"]["pixels_seg_" + str(s)].append(i)
186
- result[RRMapParser.Types.IMAGE.value] = parameters
187
-
188
- @staticmethod
189
- def _parse_path_block(buf: bytes, offset: int, length: int) -> Dict[str, Any]:
190
- """Parse a path block of the map data."""
191
- points = [
192
- [
193
- struct.unpack("<H", buf[offset + 20 + i : offset + 22 + i])[0],
194
- struct.unpack("<H", buf[offset + 22 + i : offset + 24 + i])[0],
195
- ]
196
- for i in range(0, length, 4)
197
- ]
198
- return {
199
- "current_angle": struct.unpack("<I", buf[offset + 16 : offset + 20])[0],
200
- "points": points,
201
- }
202
-
203
- @staticmethod
204
- def _parse_cleaned_zones(buf: bytes, offset: int, length: int) -> List[List[int]]:
205
- """Parse the cleaned zones block of the map data."""
206
- zone_count = struct.unpack("<I", buf[offset + 8 : offset + 12])[0]
207
- return (
208
- [
209
- [
210
- struct.unpack("<H", buf[offset + 12 + i : offset + 14 + i])[0],
211
- struct.unpack("<H", buf[offset + 14 + i : offset + 16 + i])[0],
212
- struct.unpack("<H", buf[offset + 16 + i : offset + 18 + i])[0],
213
- struct.unpack("<H", buf[offset + 18 + i : offset + 20 + i])[0],
214
- ]
215
- for i in range(0, length, 8)
216
- ]
217
- if zone_count > 0
218
- else []
219
- )
220
-
221
- @staticmethod
222
- def _parse_forbidden_zones(buf: bytes, offset: int, length: int) -> List[List[int]]:
223
- """Parse the forbidden zones block of the map data."""
224
- zone_count = struct.unpack("<I", buf[offset + 8 : offset + 12])[0]
225
- return (
226
- [
227
- [
228
- struct.unpack("<H", buf[offset + 12 + i : offset + 14 + i])[0],
229
- struct.unpack("<H", buf[offset + 14 + i : offset + 16 + i])[0],
230
- struct.unpack("<H", buf[offset + 16 + i : offset + 18 + i])[0],
231
- struct.unpack("<H", buf[offset + 18 + i : offset + 20 + i])[0],
232
- struct.unpack("<H", buf[offset + 20 + i : offset + 22 + i])[0],
233
- struct.unpack("<H", buf[offset + 22 + i : offset + 24 + i])[0],
234
- struct.unpack("<H", buf[offset + 24 + i : offset + 26 + i])[0],
235
- struct.unpack("<H", buf[offset + 26 + i : offset + 28 + i])[0],
236
- ]
237
- for i in range(0, length, 16)
238
- ]
239
- if zone_count > 0
240
- else []
241
- )
242
-
243
- @callback
244
- def parse(self, map_buf: bytes) -> Dict[str, Any]:
245
- """Parse the map data."""
246
- if map_buf[0:2] == b"rr":
247
- return {
248
- "header_length": struct.unpack("<H", map_buf[2:4])[0],
249
- "data_length": struct.unpack("<H", map_buf[4:6])[0],
250
- "version": {
251
- "major": struct.unpack("<H", map_buf[8:10])[0],
252
- "minor": struct.unpack("<H", map_buf[10:12])[0],
253
- },
254
- "map_index": struct.unpack("<H", map_buf[12:14])[0],
255
- "map_sequence": struct.unpack("<H", map_buf[16:18])[0],
256
- }
257
- return {}
258
-
259
- @callback
260
- def parse_rrm_data(
261
- self, map_buf: bytes, pixels: bool = False
262
- ) -> Optional[Dict[str, Any]]:
263
- """Parse the complete map data."""
264
- if not self.parse(map_buf).get("map_index"):
265
- return None
266
-
267
- parsed_map_data = {}
268
- blocks = self.parse_block(map_buf, 0x14, None, pixels)
269
-
270
- self._parse_image_data(parsed_map_data, blocks)
271
- self._parse_charger_data(parsed_map_data, blocks)
272
- self._parse_robot_data(parsed_map_data, blocks)
273
- self._parse_zones_data(parsed_map_data, blocks)
274
- self._parse_virtual_walls_data(parsed_map_data, blocks)
275
- self._parse_misc_data(parsed_map_data, blocks)
276
-
277
- return parsed_map_data
278
-
279
- @staticmethod
280
- def _parse_image_data(parsed_map_data: Dict[str, Any], blocks: Dict[int, Any]):
281
- """Parse image-related data."""
282
- if RRMapParser.Types.IMAGE.value in blocks:
283
- parsed_map_data["image"] = blocks[RRMapParser.Types.IMAGE.value]
284
- for item in [
285
- {"type": RRMapParser.Types.PATH.value, "path": "path"},
286
- {
287
- "type": RRMapParser.Types.GOTO_PREDICTED_PATH.value,
288
- "path": "goto_predicted_path",
289
- },
290
- ]:
291
- if item["type"] in blocks:
292
- parsed_map_data[item["path"]] = blocks[item["type"]]
293
- parsed_map_data[item["path"]]["points"] = [
294
- [point[0], RRMapParser.Tools.DIMENSION_MM - point[1]]
295
- for point in parsed_map_data[item["path"]]["points"]
296
- ]
297
- if len(parsed_map_data[item["path"]]["points"]) >= 2:
298
- parsed_map_data[item["path"]]["current_angle"] = math.degrees(
299
- math.atan2(
300
- parsed_map_data[item["path"]]["points"][-1][1]
301
- - parsed_map_data[item["path"]]["points"][-2][1],
302
- parsed_map_data[item["path"]]["points"][-1][0]
303
- - parsed_map_data[item["path"]]["points"][-2][0],
304
- )
305
- )
306
-
307
- @staticmethod
308
- def _parse_charger_data(parsed_map_data: Dict[str, Any], blocks: Dict[int, Any]):
309
- """Parse charger location data."""
310
- if RRMapParser.Types.CHARGER_LOCATION.value in blocks:
311
- charger = blocks[RRMapParser.Types.CHARGER_LOCATION.value]["position"]
312
- parsed_map_data["charger"] = charger
313
-
314
- @staticmethod
315
- def _parse_robot_data(parsed_map_data: Dict[str, Any], blocks: Dict[int, Any]):
316
- """Parse robot position data."""
317
- if RRMapParser.Types.ROBOT_POSITION.value in blocks:
318
- robot = blocks[RRMapParser.Types.ROBOT_POSITION.value]["position"]
319
- rob_angle = blocks[RRMapParser.Types.ROBOT_POSITION.value]["angle"]
320
- parsed_map_data["robot"] = robot
321
- parsed_map_data["robot_angle"] = rob_angle
322
-
323
- @staticmethod
324
- def _parse_zones_data(parsed_map_data: Dict[str, Any], blocks: Dict[int, Any]):
325
- """Parse zones and forbidden zones data."""
326
- if RRMapParser.Types.CURRENTLY_CLEANED_ZONES.value in blocks:
327
- parsed_map_data["currently_cleaned_zones"] = [
328
- [
329
- zone[0],
330
- RRMapParser.Tools.DIMENSION_MM - zone[1],
331
- zone[2],
332
- RRMapParser.Tools.DIMENSION_MM - zone[3],
333
- ]
334
- for zone in blocks[RRMapParser.Types.CURRENTLY_CLEANED_ZONES.value]
335
- ]
336
-
337
- if RRMapParser.Types.FORBIDDEN_ZONES.value in blocks:
338
- parsed_map_data["forbidden_zones"] = [
339
- [
340
- zone[0],
341
- RRMapParser.Tools.DIMENSION_MM - zone[1],
342
- zone[2],
343
- RRMapParser.Tools.DIMENSION_MM - zone[3],
344
- zone[4],
345
- RRMapParser.Tools.DIMENSION_MM - zone[5],
346
- zone[6],
347
- RRMapParser.Tools.DIMENSION_MM - zone[7],
348
- ]
349
- for zone in blocks[RRMapParser.Types.FORBIDDEN_ZONES.value]
350
- ]
351
-
352
- @staticmethod
353
- def _parse_virtual_walls_data(
354
- parsed_map_data: Dict[str, Any], blocks: Dict[int, Any]
355
- ):
356
- """Parse virtual walls data."""
357
- if RRMapParser.Types.VIRTUAL_WALLS.value in blocks:
358
- parsed_map_data["virtual_walls"] = [
359
- [
360
- wall[0],
361
- RRMapParser.Tools.DIMENSION_MM - wall[1],
362
- wall[2],
363
- RRMapParser.Tools.DIMENSION_MM - wall[3],
364
- ]
365
- for wall in blocks[RRMapParser.Types.VIRTUAL_WALLS.value]
366
- ]
367
-
368
- @staticmethod
369
- def _parse_misc_data(parsed_map_data: Dict[str, Any], blocks: Dict[int, Any]):
370
- """Parse miscellaneous data like cleaned blocks and mop zones."""
371
- if RRMapParser.Types.CURRENTLY_CLEANED_BLOCKS.value in blocks:
372
- parsed_map_data["currently_cleaned_blocks"] = blocks[
373
- RRMapParser.Types.CURRENTLY_CLEANED_BLOCKS.value
374
- ]
375
-
376
- if RRMapParser.Types.FORBIDDEN_MOP_ZONES.value in blocks:
377
- parsed_map_data["forbidden_mop_zones"] = [
378
- [
379
- zone[0],
380
- RRMapParser.Tools.DIMENSION_MM - zone[1],
381
- zone[2],
382
- RRMapParser.Tools.DIMENSION_MM - zone[3],
383
- zone[4],
384
- RRMapParser.Tools.DIMENSION_MM - zone[5],
385
- zone[6],
386
- RRMapParser.Tools.DIMENSION_MM - zone[7],
387
- ]
388
- for zone in blocks[RRMapParser.Types.FORBIDDEN_MOP_ZONES.value]
389
- ]
390
-
391
- if RRMapParser.Types.GOTO_TARGET.value in blocks:
392
- parsed_map_data["goto_target"] = blocks[
393
- RRMapParser.Types.GOTO_TARGET.value
394
- ]["position"]
395
-
396
- def parse_data(
397
- self, payload: Optional[bytes] = None, pixels: bool = False
398
- ) -> Optional[Dict[str, Any]]:
399
- """Get the map data from MQTT and return the json."""
400
- if payload:
401
- self.map_data = self.parse(payload)
402
- self.map_data.update(self.parse_rrm_data(payload, pixels) or {})
403
- return self.map_data
404
-
405
- def get_image(self) -> Dict[str, Any]:
406
- """Get the image data from the map data."""
407
- return self.map_data.get("image", {})
408
-
409
- @staticmethod
410
- def get_int32(data: bytes, address: int) -> int:
411
- """Get a 32-bit integer from the data."""
412
- return struct.unpack_from("<i", data, address)[0]