pymammotion 0.2.62__py3-none-any.whl → 0.5.51__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.
Potentially problematic release.
This version of pymammotion might be problematic. Click here for more details.
- pymammotion/__init__.py +9 -6
- pymammotion/aliyun/client.py +235 -0
- pymammotion/aliyun/cloud_gateway.py +320 -69
- pymammotion/aliyun/model/aep_response.py +1 -2
- pymammotion/aliyun/model/dev_by_account_response.py +170 -23
- pymammotion/aliyun/model/login_by_oauth_response.py +2 -3
- pymammotion/aliyun/model/regions_response.py +3 -3
- pymammotion/aliyun/model/session_by_authcode_response.py +2 -2
- pymammotion/aliyun/model/thing_response.py +12 -0
- pymammotion/aliyun/regions.py +62 -0
- pymammotion/aliyun/tea/core.py +297 -0
- pymammotion/bluetooth/ble.py +11 -15
- pymammotion/bluetooth/ble_message.py +389 -106
- pymammotion/bluetooth/model/atomic_integer.py +54 -0
- pymammotion/const.py +3 -0
- pymammotion/data/model/__init__.py +1 -2
- pymammotion/data/model/device.py +92 -240
- pymammotion/data/model/device_config.py +10 -24
- pymammotion/data/model/device_info.py +35 -0
- pymammotion/data/model/device_limits.py +49 -0
- pymammotion/data/model/enums.py +12 -2
- pymammotion/data/model/errors.py +12 -0
- pymammotion/data/model/events.py +14 -0
- pymammotion/data/model/generate_geojson.py +521 -0
- pymammotion/data/model/generate_route_information.py +3 -4
- pymammotion/data/model/hash_list.py +384 -48
- pymammotion/data/model/location.py +4 -4
- pymammotion/data/model/mowing_modes.py +24 -1
- pymammotion/data/model/raw_data.py +215 -0
- pymammotion/data/model/region_data.py +10 -11
- pymammotion/data/model/report_info.py +62 -6
- pymammotion/data/model/work.py +27 -0
- pymammotion/data/mower_state_manager.py +316 -0
- pymammotion/data/mqtt/event.py +73 -28
- pymammotion/data/mqtt/mammotion_properties.py +257 -0
- pymammotion/data/mqtt/properties.py +93 -78
- pymammotion/data/mqtt/status.py +18 -17
- pymammotion/event/event.py +32 -8
- pymammotion/homeassistant/__init__.py +3 -0
- pymammotion/homeassistant/mower_api.py +484 -0
- pymammotion/homeassistant/rtk_api.py +54 -0
- pymammotion/http/__init__.py +0 -0
- pymammotion/http/encryption.py +220 -0
- pymammotion/http/http.py +652 -44
- pymammotion/http/model/__init__.py +0 -0
- pymammotion/{aliyun/model/stream_subscription_response.py → http/model/camera_stream.py} +14 -2
- pymammotion/http/model/http.py +160 -9
- pymammotion/http/model/response_factory.py +61 -0
- pymammotion/http/model/rtk.py +16 -0
- pymammotion/mammotion/commands/abstract_message.py +7 -5
- pymammotion/mammotion/commands/mammotion_command.py +32 -3
- pymammotion/mammotion/commands/messages/basestation.py +43 -0
- pymammotion/mammotion/commands/messages/driver.py +61 -29
- pymammotion/mammotion/commands/messages/media.py +68 -15
- pymammotion/mammotion/commands/messages/navigation.py +61 -25
- pymammotion/mammotion/commands/messages/network.py +93 -100
- pymammotion/mammotion/commands/messages/ota.py +18 -18
- pymammotion/mammotion/commands/messages/system.py +97 -72
- pymammotion/mammotion/commands/messages/video.py +17 -12
- pymammotion/mammotion/devices/__init__.py +27 -3
- pymammotion/mammotion/devices/base.py +50 -127
- pymammotion/mammotion/devices/mammotion.py +447 -212
- pymammotion/mammotion/devices/mammotion_bluetooth.py +105 -60
- pymammotion/mammotion/devices/mammotion_cloud.py +157 -105
- pymammotion/mammotion/devices/mammotion_mower_ble.py +49 -0
- pymammotion/mammotion/devices/mammotion_mower_cloud.py +39 -0
- pymammotion/mammotion/devices/managers/managers.py +81 -0
- pymammotion/mammotion/devices/mower_device.py +124 -0
- pymammotion/mammotion/devices/mower_manager.py +107 -0
- pymammotion/mammotion/devices/rtk_ble.py +89 -0
- pymammotion/mammotion/devices/rtk_cloud.py +113 -0
- pymammotion/mammotion/devices/rtk_device.py +50 -0
- pymammotion/mammotion/devices/rtk_manager.py +122 -0
- pymammotion/mqtt/__init__.py +2 -1
- pymammotion/mqtt/aliyun_mqtt.py +232 -0
- pymammotion/mqtt/linkkit/__init__.py +5 -0
- pymammotion/mqtt/linkkit/h2client.py +585 -0
- pymammotion/mqtt/linkkit/linkkit.py +3023 -0
- pymammotion/mqtt/mammotion_mqtt.py +176 -169
- pymammotion/mqtt/mqtt_models.py +66 -0
- pymammotion/proto/__init__.py +4839 -4
- pymammotion/proto/basestation.proto +8 -0
- pymammotion/proto/basestation_pb2.py +11 -9
- pymammotion/proto/basestation_pb2.pyi +16 -2
- pymammotion/proto/dev_net.proto +79 -55
- pymammotion/proto/dev_net_pb2.py +60 -56
- pymammotion/proto/dev_net_pb2.pyi +49 -6
- pymammotion/proto/luba_msg.proto +2 -1
- pymammotion/proto/luba_msg_pb2.py +6 -6
- pymammotion/proto/luba_msg_pb2.pyi +1 -0
- pymammotion/proto/luba_mul.proto +62 -1
- pymammotion/proto/luba_mul_pb2.py +38 -22
- pymammotion/proto/luba_mul_pb2.pyi +94 -7
- pymammotion/proto/mctrl_driver.proto +44 -4
- pymammotion/proto/mctrl_driver_pb2.py +26 -14
- pymammotion/proto/mctrl_driver_pb2.pyi +66 -11
- pymammotion/proto/mctrl_nav.proto +97 -51
- pymammotion/proto/mctrl_nav_pb2.py +75 -67
- pymammotion/proto/mctrl_nav_pb2.pyi +142 -56
- pymammotion/proto/mctrl_ota.proto +40 -2
- pymammotion/proto/mctrl_ota_pb2.py +23 -13
- pymammotion/proto/mctrl_ota_pb2.pyi +67 -4
- pymammotion/proto/mctrl_pept.proto +8 -3
- pymammotion/proto/mctrl_pept_pb2.py +8 -6
- pymammotion/proto/mctrl_pept_pb2.pyi +14 -6
- pymammotion/proto/mctrl_sys.proto +325 -86
- pymammotion/proto/mctrl_sys_pb2.py +162 -98
- pymammotion/proto/mctrl_sys_pb2.pyi +451 -25
- pymammotion/proto/message_pool.py +3 -0
- pymammotion/proto/py.typed +0 -0
- pymammotion/utility/constant/device_constant.py +65 -21
- pymammotion/utility/datatype_converter.py +13 -12
- pymammotion/utility/device_config.py +755 -0
- pymammotion/utility/device_type.py +218 -21
- pymammotion/utility/map.py +238 -51
- pymammotion/utility/mur_mur_hash.py +159 -0
- {pymammotion-0.2.62.dist-info → pymammotion-0.5.51.dist-info}/METADATA +27 -31
- pymammotion-0.5.51.dist-info/RECORD +152 -0
- {pymammotion-0.2.62.dist-info → pymammotion-0.5.51.dist-info}/WHEEL +1 -1
- pymammotion/aliyun/cloud_service.py +0 -65
- pymammotion/data/model/plan.py +0 -58
- pymammotion/data/state_manager.py +0 -130
- pymammotion/proto/basestation.py +0 -59
- pymammotion/proto/common.py +0 -12
- pymammotion/proto/dev_net.py +0 -381
- pymammotion/proto/luba_msg.py +0 -81
- pymammotion/proto/luba_mul.py +0 -76
- pymammotion/proto/mctrl_driver.py +0 -100
- pymammotion/proto/mctrl_nav.py +0 -660
- pymammotion/proto/mctrl_ota.py +0 -48
- pymammotion/proto/mctrl_pept.py +0 -41
- pymammotion/proto/mctrl_sys.py +0 -574
- pymammotion-0.2.62.dist-info/RECORD +0 -125
- /pymammotion/{http/_init_.py → bluetooth/model/__init__.py} +0 -0
- {pymammotion-0.2.62.dist-info → pymammotion-0.5.51.dist-info/licenses}/LICENSE +0 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
|
|
3
|
+
from mashumaro.mixins.orjson import DataClassORJSONMixin
|
|
4
|
+
|
|
5
|
+
from pymammotion.http.model.http import ErrorInfo
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class DeviceErrors(DataClassORJSONMixin):
|
|
10
|
+
err_code_list: list[int] = field(default_factory=list)
|
|
11
|
+
err_code_list_time: list[int] = field(default_factory=list)
|
|
12
|
+
error_codes: dict[str, ErrorInfo] = field(default_factory=dict)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
|
|
3
|
+
from mashumaro.mixins.orjson import DataClassORJSONMixin
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class WorkTaskEvent(DataClassORJSONMixin):
|
|
8
|
+
hash_area_map: dict[int, int] = field(default_factory=dict)
|
|
9
|
+
ids: list[int] = field(default_factory=list)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class Events(DataClassORJSONMixin):
|
|
14
|
+
work_tasks_event: WorkTaskEvent = field(default_factory=WorkTaskEvent)
|
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import math
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from shapely.geometry import Point
|
|
6
|
+
|
|
7
|
+
from pymammotion import logger
|
|
8
|
+
from pymammotion.data.model.hash_list import (
|
|
9
|
+
AreaHashNameList,
|
|
10
|
+
CommDataCouple,
|
|
11
|
+
FrameList,
|
|
12
|
+
HashList,
|
|
13
|
+
MowPath,
|
|
14
|
+
MowPathPacket,
|
|
15
|
+
NavGetCommData,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
# Constants
|
|
19
|
+
DEFAULT_X_OFFSET: int = 0
|
|
20
|
+
DEFAULT_Y_OFFSET: int = 0
|
|
21
|
+
GEOMETRY_TYPES: list[str] = ["Polygon", "Polygon", "LineString", "Point"]
|
|
22
|
+
MAP_OBJECT_TYPES: list[str] = ["area", "path", "obstacle", "dump"]
|
|
23
|
+
|
|
24
|
+
image_path = "/local/community/ha-mammotion-map/dist/assets/map/"
|
|
25
|
+
|
|
26
|
+
RTK_IMAGE = {
|
|
27
|
+
"iconImage": "map_icon_base_station_rtk.webp",
|
|
28
|
+
"iconSize": [30, 30],
|
|
29
|
+
"iconAnchor": [15, 30],
|
|
30
|
+
"iconUrl": f"{image_path}map_icon_base_station_rtk.webp",
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
DOCK_IMAGE = {
|
|
34
|
+
"iconImage": "icon_map_recharge.webp",
|
|
35
|
+
"iconSize": [30, 30],
|
|
36
|
+
"iconAnchor": [15, 15],
|
|
37
|
+
"iconUrl": f"{image_path}icon_map_recharge.webp",
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
#############################
|
|
41
|
+
# STYLE CONFIGURATION
|
|
42
|
+
#############################
|
|
43
|
+
|
|
44
|
+
DOCK_STYLE = {"color": "lightgray", "fill": "lightgray", "weight": 2, "opacity": 1.0, "fillOpacity": 0.7, "radius": 10}
|
|
45
|
+
|
|
46
|
+
RTK_STYLE = {"color": "purple", "fill": "purple", "weight": 2, "opacity": 1.0, "fillOpacity": 0.7, "radius": 7}
|
|
47
|
+
|
|
48
|
+
AREA_STYLE = {
|
|
49
|
+
"color": "green",
|
|
50
|
+
"fill": "darkgreen",
|
|
51
|
+
"weight": 3,
|
|
52
|
+
"opacity": 0.8,
|
|
53
|
+
"fillOpacity": 0.3,
|
|
54
|
+
"dashArray": "",
|
|
55
|
+
"lineCap": "round",
|
|
56
|
+
"lineJoin": "round",
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
OBSTACLE_STYLE = {
|
|
60
|
+
"color": "#FF4D00",
|
|
61
|
+
"fill": "darkorange",
|
|
62
|
+
"weight": 2,
|
|
63
|
+
"opacity": 0.9,
|
|
64
|
+
"fillOpacity": 0.4,
|
|
65
|
+
"dashArray": "",
|
|
66
|
+
"lineCap": "round",
|
|
67
|
+
"lineJoin": "round",
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
PATH_STYLE = {
|
|
71
|
+
"color": "#ffffff",
|
|
72
|
+
"weight": 8,
|
|
73
|
+
"opacity": 1.0,
|
|
74
|
+
"zIndex": -1,
|
|
75
|
+
"dashArray": "",
|
|
76
|
+
"lineCap": "round",
|
|
77
|
+
"lineJoin": "round",
|
|
78
|
+
"road_center_color": "#696969",
|
|
79
|
+
"road_center_dash": "8, 8",
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
POINT_STYLE = {"color": "blue", "fill": "lightblue", "weight": 2, "opacity": 1.0, "fillOpacity": 0.7, "radius": 5}
|
|
83
|
+
|
|
84
|
+
geojson_metadata = {"name": "Lawn Areas", "description": "Generated from Mammotion diagnostics data"}
|
|
85
|
+
|
|
86
|
+
# Map type IDs
|
|
87
|
+
TYPE_MOWING_ZONE: int = 0
|
|
88
|
+
TYPE_OBSTACLE: int = 1
|
|
89
|
+
TYPE_PATH: int = 2
|
|
90
|
+
TYPE_MOW_PATH: int = 3
|
|
91
|
+
|
|
92
|
+
# Coordinate conversion constants
|
|
93
|
+
METERS_PER_DEGREE_LON: int = 111320
|
|
94
|
+
METERS_PER_DEGREE_LAT: int = 111320
|
|
95
|
+
|
|
96
|
+
# Type aliases
|
|
97
|
+
Coordinate = tuple[float, float]
|
|
98
|
+
CoordinateList = list[list[float]]
|
|
99
|
+
GeoJSONFeature = dict[str, Any]
|
|
100
|
+
GeoJSONCollection = dict[str, Any]
|
|
101
|
+
LocationDict = dict[str, Coordinate]
|
|
102
|
+
|
|
103
|
+
# Path to mammotion integration diagnostics file
|
|
104
|
+
x_offset: int = DEFAULT_X_OFFSET
|
|
105
|
+
y_offset: int = DEFAULT_Y_OFFSET
|
|
106
|
+
geometry_types: list[str] = GEOMETRY_TYPES
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def is_overlapping(p: Point, placed_points: list[Point], min_distance: float = 0.00005) -> bool:
|
|
110
|
+
"""Check if point p is too close to any previously placed label."""
|
|
111
|
+
return any(p.distance(existing) < min_distance for existing in placed_points)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def apply_meter_offsets(lon: float, lat: float, lon_offset: float, lat_offset: float) -> list[float]:
|
|
115
|
+
"""Apply meter-based offsets to coordinates (in degrees)"""
|
|
116
|
+
new_lon = lon + (lon_offset / (111320 * math.cos(math.radians(lat))))
|
|
117
|
+
new_lat = lat + (lat_offset / 111320)
|
|
118
|
+
return [new_lon, new_lat]
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def generate_geojson(
|
|
122
|
+
hash_list: HashList, rtk_location: Point, dock_location: Point, dock_rotation: int
|
|
123
|
+
) -> GeoJSONCollection:
|
|
124
|
+
"""Generate GeoJSON from hash list data.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
hash_list: HashList object containing map data
|
|
128
|
+
rtk_location: Tuple of (longitude, latitude) for rtk position
|
|
129
|
+
:param hash_list:
|
|
130
|
+
:param rtk_location:
|
|
131
|
+
:param dock_rotation:
|
|
132
|
+
:param dock_location:
|
|
133
|
+
|
|
134
|
+
"""
|
|
135
|
+
area_names = _build_area_name_lookup(hash_list.area_name)
|
|
136
|
+
|
|
137
|
+
geo_json: GeoJSONCollection = {"type": "FeatureCollection", "name": "Lawn Areas", "features": []}
|
|
138
|
+
_add_rtk_and_dock(rtk_location, dock_location, dock_rotation, geo_json)
|
|
139
|
+
total_frames = _process_map_objects(hash_list, rtk_location, area_names, geo_json)
|
|
140
|
+
|
|
141
|
+
# _save_geojson(geo_json)
|
|
142
|
+
logger.debug("GeoJson complete. Total Frames processed:", total_frames)
|
|
143
|
+
return geo_json
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _add_rtk_and_dock(
|
|
147
|
+
rtk_location: Point, dock_location: Point, dock_rotation: int, geo_json: GeoJSONCollection
|
|
148
|
+
) -> None:
|
|
149
|
+
geo_json["features"].append(
|
|
150
|
+
{
|
|
151
|
+
"type": "Feature",
|
|
152
|
+
"properties": {
|
|
153
|
+
"title": "RTK Base",
|
|
154
|
+
"Name": "RTK Base",
|
|
155
|
+
"description": "RTK Base Station location",
|
|
156
|
+
"type_name": "station",
|
|
157
|
+
**RTK_STYLE,
|
|
158
|
+
**RTK_IMAGE,
|
|
159
|
+
},
|
|
160
|
+
"geometry": {"type": "Point", "coordinates": [rtk_location.y, rtk_location.x]},
|
|
161
|
+
}
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
geo_json["features"].append(
|
|
165
|
+
{
|
|
166
|
+
"type": "Feature",
|
|
167
|
+
"properties": {
|
|
168
|
+
"title": "Dock",
|
|
169
|
+
"Name": "Dock",
|
|
170
|
+
"description": "Charging dock location",
|
|
171
|
+
"type_name": "station",
|
|
172
|
+
"rotation": dock_rotation,
|
|
173
|
+
**DOCK_STYLE,
|
|
174
|
+
**DOCK_IMAGE,
|
|
175
|
+
},
|
|
176
|
+
"geometry": {"type": "Point", "coordinates": [dock_location.y, dock_location.x]},
|
|
177
|
+
}
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _build_area_name_lookup(area_names: list[AreaHashNameList]) -> dict[int, str]:
|
|
182
|
+
"""Build a hash lookup table for area names.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
area_names: List of AreaHashNameList objects
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
Dictionary mapping hash to area name
|
|
189
|
+
|
|
190
|
+
"""
|
|
191
|
+
return {item.hash: item.name for item in area_names}
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _process_map_objects(
|
|
195
|
+
hash_list: HashList, rtk_location: Point, area_names: dict[int, str], geo_json: GeoJSONCollection
|
|
196
|
+
) -> int:
|
|
197
|
+
"""Process all map objects and add them to GeoJSON.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
hash_list: HashList object containing map data
|
|
201
|
+
rtk_location: Tuple of (longitude, latitude) for rtk position
|
|
202
|
+
area_names: Dictionary mapping hash to area name
|
|
203
|
+
geo_json: GeoJSON collection to add features to
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
Total number of frames processed
|
|
207
|
+
|
|
208
|
+
"""
|
|
209
|
+
total_frames = 0
|
|
210
|
+
|
|
211
|
+
# Map type names to their corresponding dictionaries in HashList
|
|
212
|
+
type_mapping: dict[str, dict[int, FrameList]] = {
|
|
213
|
+
"area": hash_list.area,
|
|
214
|
+
"path": hash_list.path,
|
|
215
|
+
"obstacle": hash_list.obstacle,
|
|
216
|
+
"dump": hash_list.dump,
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
for type_name, map_objects in type_mapping.items():
|
|
220
|
+
for hash_key, frame_list in map_objects.items():
|
|
221
|
+
logger.debug(hash_key, type_name)
|
|
222
|
+
|
|
223
|
+
if not _validate_frame_list(frame_list, hash_key, area_names):
|
|
224
|
+
continue
|
|
225
|
+
|
|
226
|
+
local_coords = _collect_frame_coordinates(frame_list)
|
|
227
|
+
total_frames += len(frame_list.data)
|
|
228
|
+
|
|
229
|
+
lonlat_coords = _convert_to_lonlat_coords(local_coords, rtk_location)
|
|
230
|
+
length, area = map_object_stats(local_coords)
|
|
231
|
+
|
|
232
|
+
feature = _create_feature(hash_key, frame_list, type_name, lonlat_coords, length, area)
|
|
233
|
+
if feature:
|
|
234
|
+
geo_json["features"].append(feature)
|
|
235
|
+
|
|
236
|
+
for key, path_packet_list in hash_list.current_mow_path.items():
|
|
237
|
+
local_coords = _collect_mow_frame_coordinates(path_packet_list)
|
|
238
|
+
total_frames += len(path_packet_list.path_packets)
|
|
239
|
+
|
|
240
|
+
lonlat_coords = _convert_to_lonlat_coords(local_coords, rtk_location)
|
|
241
|
+
length, area = map_object_stats(local_coords)
|
|
242
|
+
|
|
243
|
+
feature = _create_mow_path_feature(key, path_packet_list, lonlat_coords, length, area)
|
|
244
|
+
if feature:
|
|
245
|
+
geo_json["features"].append(feature)
|
|
246
|
+
|
|
247
|
+
return total_frames
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _validate_frame_list(frame_list: FrameList, hash_key: int, area_names: dict[int, str]) -> bool:
|
|
251
|
+
"""Validate that frame list has complete frame data.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
frame_list: FrameList object to validate
|
|
255
|
+
hash_key: Hash key for the area
|
|
256
|
+
area_names: Dictionary mapping hash to area name
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
True if valid, False otherwise
|
|
260
|
+
|
|
261
|
+
"""
|
|
262
|
+
if len(frame_list.data) != frame_list.total_frame:
|
|
263
|
+
area_name = area_names.get(hash_key, "Unknown")
|
|
264
|
+
logger.debug(f"Error: full coord data not available for area: '{area_name}' - '{hash_key}'")
|
|
265
|
+
return False
|
|
266
|
+
return True
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _collect_frame_coordinates(frame_list: FrameList) -> list[CommDataCouple]:
|
|
270
|
+
"""Collect coordinates from all frames in a FrameList.
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
frame_list: FrameList containing frame data
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
List of coordinate dictionaries with 'x' and 'y' keys
|
|
277
|
+
|
|
278
|
+
"""
|
|
279
|
+
local_coords: list[CommDataCouple] = []
|
|
280
|
+
for frame in frame_list.data:
|
|
281
|
+
if isinstance(frame, NavGetCommData):
|
|
282
|
+
local_coords.extend(frame.data_couple)
|
|
283
|
+
# TODO svg message needs different transform
|
|
284
|
+
# elif isinstance(frame, SvgMessage):
|
|
285
|
+
# local_coords.extend(frame.)
|
|
286
|
+
return local_coords
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _collect_mow_frame_coordinates(frame_list: MowPath) -> list[CommDataCouple]:
|
|
290
|
+
"""Collect coordinates from all frames in a FrameList."""
|
|
291
|
+
local_coords: list[CommDataCouple] = []
|
|
292
|
+
for frame in frame_list.path_packets:
|
|
293
|
+
local_coords.extend(frame.data_couple)
|
|
294
|
+
return local_coords
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _convert_to_lonlat_coords(local_coords: list[CommDataCouple], rtk_location: Point) -> CoordinateList:
|
|
298
|
+
"""Convert local x,y coordinates to lon,lat coordinates.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
local_coords: List of coordinate dictionaries with 'x' and 'y' keys
|
|
302
|
+
rtk_location: Tuple of (longitude, latitude) for rtk position
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
List of [longitude, latitude] coordinate pairs
|
|
306
|
+
|
|
307
|
+
"""
|
|
308
|
+
lonlat_coords: CoordinateList = [
|
|
309
|
+
list(lon_lat_delta(rtk_location, xy.x + x_offset, xy.y + y_offset)) for xy in local_coords
|
|
310
|
+
]
|
|
311
|
+
lonlat_coords.reverse() # GeoJSON polygons go clockwise
|
|
312
|
+
return lonlat_coords
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _create_feature(
|
|
316
|
+
hash_key: int, frame_list: FrameList, type_name: str, lonlat_coords: CoordinateList, length: float, area: float
|
|
317
|
+
) -> GeoJSONFeature | None:
|
|
318
|
+
"""Create a GeoJSON feature from frame list data.
|
|
319
|
+
|
|
320
|
+
Args:
|
|
321
|
+
hash_key: Hash identifier for the feature
|
|
322
|
+
frame_list: FrameList containing frame data
|
|
323
|
+
type_name: Type name of the map object
|
|
324
|
+
lonlat_coords: List of [longitude, latitude] coordinate pairs
|
|
325
|
+
length: Calculated length of the feature
|
|
326
|
+
area: Calculated area of the feature
|
|
327
|
+
|
|
328
|
+
Returns:
|
|
329
|
+
GeoJSON feature dictionary or None if invalid
|
|
330
|
+
|
|
331
|
+
"""
|
|
332
|
+
first_frame = frame_list.data[0]
|
|
333
|
+
type_id = first_frame.type
|
|
334
|
+
object_name = ""
|
|
335
|
+
if isinstance(first_frame, NavGetCommData):
|
|
336
|
+
object_name = first_frame.name_time.name
|
|
337
|
+
|
|
338
|
+
properties = _create_feature_properties(hash_key, type_id, type_name, first_frame, length, area, object_name)
|
|
339
|
+
geometry = _create_feature_geometry(type_id, lonlat_coords, properties)
|
|
340
|
+
|
|
341
|
+
if geometry is None:
|
|
342
|
+
return None
|
|
343
|
+
|
|
344
|
+
return {"type": "Feature", "properties": properties, "geometry": geometry}
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _create_mow_path_feature(
|
|
348
|
+
key: int, path_packet_list: MowPath, lonlat_coords: CoordinateList, length: float, area: float
|
|
349
|
+
) -> GeoJSONFeature | None:
|
|
350
|
+
first_frame = path_packet_list.path_packets[0]
|
|
351
|
+
|
|
352
|
+
properties = _create_feature_mow_path_properties(key, first_frame, length, path_packet_list.area)
|
|
353
|
+
geometry = _create_feature_geometry(3, lonlat_coords, properties)
|
|
354
|
+
|
|
355
|
+
if geometry is None:
|
|
356
|
+
return None
|
|
357
|
+
|
|
358
|
+
return {"type": "Feature", "properties": properties, "geometry": geometry}
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _create_feature_mow_path_properties(
|
|
362
|
+
hash_key: int, first_frame: MowPathPacket, length: float, area: float
|
|
363
|
+
) -> dict[str, Any]:
|
|
364
|
+
"""Create properties dictionary for GeoJSON feature."""
|
|
365
|
+
return {
|
|
366
|
+
"hash": hash_key,
|
|
367
|
+
"type_name": "mow_path",
|
|
368
|
+
"zone_hash": first_frame.zone_hash,
|
|
369
|
+
"length": length,
|
|
370
|
+
"area": area,
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def _create_feature_properties(
|
|
375
|
+
hash_key: int, type_id: int, type_name: str, first_frame: Any, length: float, area: float, object_name: str = ""
|
|
376
|
+
) -> dict[str, Any]:
|
|
377
|
+
"""Create properties dictionary for GeoJSON feature.
|
|
378
|
+
|
|
379
|
+
Args:
|
|
380
|
+
hash_key: Hash identifier
|
|
381
|
+
object_name: Name of the object
|
|
382
|
+
type_id: Type ID of the feature
|
|
383
|
+
type_name: Type name of the feature
|
|
384
|
+
first_frame: First frame from the FrameList
|
|
385
|
+
length: Calculated length
|
|
386
|
+
area: Calculated area
|
|
387
|
+
|
|
388
|
+
Returns:
|
|
389
|
+
Properties dictionary
|
|
390
|
+
|
|
391
|
+
"""
|
|
392
|
+
return {
|
|
393
|
+
"hash": hash_key,
|
|
394
|
+
"title": object_name,
|
|
395
|
+
"Name": object_name,
|
|
396
|
+
"description": "description <b>test</b>",
|
|
397
|
+
"type_id": type_id,
|
|
398
|
+
"type_name": type_name,
|
|
399
|
+
"parent_hash_a": first_frame.paternal_hash_a,
|
|
400
|
+
"parent_hash_b": first_frame.paternal_hash_b,
|
|
401
|
+
"length": length,
|
|
402
|
+
"area": area,
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def _create_feature_geometry(
|
|
407
|
+
type_id: int, lonlat_coords: CoordinateList, properties: dict[str, Any]
|
|
408
|
+
) -> dict[str, Any] | None:
|
|
409
|
+
"""Create geometry dictionary for GeoJSON feature based on type.
|
|
410
|
+
|
|
411
|
+
Args:
|
|
412
|
+
type_id: Type ID determining geometry type
|
|
413
|
+
lonlat_coords: List of [longitude, latitude] coordinate pairs
|
|
414
|
+
properties: Properties dictionary to modify with style information
|
|
415
|
+
|
|
416
|
+
Returns:
|
|
417
|
+
Geometry dictionary or None if invalid type
|
|
418
|
+
|
|
419
|
+
"""
|
|
420
|
+
if type_id == TYPE_MOWING_ZONE:
|
|
421
|
+
properties.update(AREA_STYLE)
|
|
422
|
+
return {"type": "Polygon", "coordinates": [lonlat_coords]}
|
|
423
|
+
elif type_id == TYPE_OBSTACLE:
|
|
424
|
+
properties.update(OBSTACLE_STYLE)
|
|
425
|
+
return {"type": "Polygon", "coordinates": [lonlat_coords]}
|
|
426
|
+
elif type_id == TYPE_PATH and len(lonlat_coords) > 1:
|
|
427
|
+
properties.update(PATH_STYLE)
|
|
428
|
+
return {"type": "LineString", "coordinates": lonlat_coords}
|
|
429
|
+
elif type_id == TYPE_MOW_PATH and len(lonlat_coords) > 1:
|
|
430
|
+
properties["color"] = "green"
|
|
431
|
+
return {"type": "LineString", "coordinates": lonlat_coords}
|
|
432
|
+
else:
|
|
433
|
+
return None # Point (ignore)
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def _save_geojson(geoJSON: GeoJSONCollection) -> None:
|
|
437
|
+
"""Save GeoJSON data to file.
|
|
438
|
+
|
|
439
|
+
Args:
|
|
440
|
+
geoJSON: GeoJSON collection to save
|
|
441
|
+
|
|
442
|
+
"""
|
|
443
|
+
with open("areas.json", "w") as json_file:
|
|
444
|
+
json.dump(geoJSON, json_file, indent=2)
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def lon_lat_delta(rtk: Point, x: float, y: float) -> Coordinate:
|
|
448
|
+
"""Add delta (in meters) to lon/lat, return new lon/lat.
|
|
449
|
+
|
|
450
|
+
Args:
|
|
451
|
+
lon: Longitude in degrees
|
|
452
|
+
lat: Latitude in degrees
|
|
453
|
+
x: X offset in meters
|
|
454
|
+
y: Y offset in meters
|
|
455
|
+
|
|
456
|
+
Returns:
|
|
457
|
+
Tuple of (new_longitude, new_latitude)
|
|
458
|
+
|
|
459
|
+
"""
|
|
460
|
+
new_lon = rtk.y + (x / (METERS_PER_DEGREE_LON * math.cos(math.radians(rtk.x))))
|
|
461
|
+
new_lat = rtk.x + (y / METERS_PER_DEGREE_LAT)
|
|
462
|
+
return new_lon, new_lat
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def map_object_stats(coords: list[CommDataCouple]) -> Coordinate:
|
|
466
|
+
"""Calculate length and area statistics for map object coordinates.
|
|
467
|
+
|
|
468
|
+
Args:
|
|
469
|
+
coords: List of coordinate dictionaries with 'x' and 'y' keys
|
|
470
|
+
|
|
471
|
+
Returns:
|
|
472
|
+
Tuple of (length, area) in meters and square meters
|
|
473
|
+
|
|
474
|
+
"""
|
|
475
|
+
# Point Object
|
|
476
|
+
if len(coords) < 2:
|
|
477
|
+
return 0.0, 0.0
|
|
478
|
+
|
|
479
|
+
def distance(p1: CommDataCouple, p2: CommDataCouple) -> float:
|
|
480
|
+
"""Calculate Euclidean distance between two points."""
|
|
481
|
+
return math.sqrt((p2.x - p1.x) ** 2 + (p2.y - p1.y) ** 2)
|
|
482
|
+
|
|
483
|
+
length = sum(distance(coords[i], coords[i + 1]) for i in range(len(coords) - 1))
|
|
484
|
+
|
|
485
|
+
# Open line
|
|
486
|
+
if coords[0] != coords[-1]:
|
|
487
|
+
return length, 0.0
|
|
488
|
+
|
|
489
|
+
# Closed Polygon - Calculate area using shoelace formula
|
|
490
|
+
area = 0.5 * abs(sum(coords[i].x * coords[i + 1].y - coords[i + 1].x * coords[i].y for i in range(len(coords) - 1)))
|
|
491
|
+
|
|
492
|
+
return length, area
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def is_point_in_polygon(x: float, y: float, poly: list[list[float]]) -> bool:
|
|
496
|
+
"""Test if a point is inside a polygon using ray casting algorithm.
|
|
497
|
+
|
|
498
|
+
Args:
|
|
499
|
+
x: X coordinate of the point
|
|
500
|
+
y: Y coordinate of the point
|
|
501
|
+
poly: Polygon as list of [x, y] coordinate pairs
|
|
502
|
+
|
|
503
|
+
Returns:
|
|
504
|
+
True if point is inside polygon, False otherwise
|
|
505
|
+
|
|
506
|
+
"""
|
|
507
|
+
return (
|
|
508
|
+
sum(
|
|
509
|
+
(y > poly[i][1]) != (y > poly[(i + 1) % len(poly)][1])
|
|
510
|
+
and (
|
|
511
|
+
x
|
|
512
|
+
< (poly[(i + 1) % len(poly)][0] - poly[i][0])
|
|
513
|
+
* (y - poly[i][1])
|
|
514
|
+
/ (poly[(i + 1) % len(poly)][1] - poly[i][1])
|
|
515
|
+
+ poly[i][0]
|
|
516
|
+
)
|
|
517
|
+
for i in range(len(poly))
|
|
518
|
+
)
|
|
519
|
+
% 2
|
|
520
|
+
== 1
|
|
521
|
+
)
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
1
2
|
import logging
|
|
2
|
-
from dataclasses import dataclass
|
|
3
|
-
from typing import List
|
|
4
3
|
|
|
5
4
|
logger = logging.getLogger(__name__)
|
|
6
5
|
|
|
@@ -9,8 +8,8 @@ logger = logging.getLogger(__name__)
|
|
|
9
8
|
class GenerateRouteInformation:
|
|
10
9
|
"""Creates a model for generating route information and mowing plan before starting a job."""
|
|
11
10
|
|
|
12
|
-
one_hashs: list[int] = list
|
|
13
|
-
job_mode: int =
|
|
11
|
+
one_hashs: list[int] = field(default_factory=list)
|
|
12
|
+
job_mode: int = 4 # taskMode
|
|
14
13
|
job_version: int = 0
|
|
15
14
|
job_id: int = 0
|
|
16
15
|
speed: float = 0.3
|