pymammotion 0.4.0a2__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 +5 -4
- pymammotion/aliyun/client.py +235 -0
- pymammotion/aliyun/cloud_gateway.py +312 -64
- 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 +7 -9
- pymammotion/bluetooth/ble_message.py +10 -14
- pymammotion/const.py +3 -0
- pymammotion/data/model/__init__.py +1 -2
- pymammotion/data/model/device.py +95 -27
- pymammotion/data/model/device_config.py +4 -4
- pymammotion/data/model/device_info.py +35 -0
- pymammotion/data/model/device_limits.py +10 -10
- 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 +2 -2
- pymammotion/data/model/hash_list.py +370 -57
- pymammotion/data/model/location.py +4 -4
- pymammotion/data/model/mowing_modes.py +17 -1
- pymammotion/data/model/raw_data.py +2 -10
- pymammotion/data/model/region_data.py +10 -11
- pymammotion/data/model/report_info.py +31 -5
- 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 +27 -6
- pymammotion/homeassistant/__init__.py +3 -0
- pymammotion/homeassistant/mower_api.py +484 -0
- pymammotion/homeassistant/rtk_api.py +54 -0
- pymammotion/http/encryption.py +5 -6
- pymammotion/http/http.py +574 -28
- 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 +129 -4
- 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 +30 -1
- 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 +17 -23
- pymammotion/mammotion/commands/messages/ota.py +18 -18
- pymammotion/mammotion/commands/messages/system.py +32 -49
- pymammotion/mammotion/commands/messages/video.py +15 -16
- pymammotion/mammotion/devices/__init__.py +27 -3
- pymammotion/mammotion/devices/base.py +40 -131
- pymammotion/mammotion/devices/mammotion.py +436 -201
- pymammotion/mammotion/devices/mammotion_bluetooth.py +57 -47
- pymammotion/mammotion/devices/mammotion_cloud.py +134 -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 +93 -52
- 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 +29 -5
- pymammotion/utility/datatype_converter.py +13 -12
- pymammotion/utility/device_config.py +522 -130
- pymammotion/utility/device_type.py +218 -21
- pymammotion/utility/map.py +238 -51
- pymammotion/utility/mur_mur_hash.py +159 -0
- {pymammotion-0.4.0a2.dist-info → pymammotion-0.5.51.dist-info}/METADATA +26 -31
- pymammotion-0.5.51.dist-info/RECORD +152 -0
- {pymammotion-0.4.0a2.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 -129
- 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 -664
- pymammotion/proto/mctrl_ota.py +0 -48
- pymammotion/proto/mctrl_pept.py +0 -41
- pymammotion/proto/mctrl_sys.py +0 -574
- pymammotion-0.4.0a2.dist-info/RECORD +0 -131
- /pymammotion/http/{_init_.py → __init__.py} +0 -0
- {pymammotion-0.4.0a2.dist-info → pymammotion-0.5.51.dist-info/licenses}/LICENSE +0 -0
|
@@ -2,8 +2,13 @@ from dataclasses import dataclass, field
|
|
|
2
2
|
from enum import IntEnum
|
|
3
3
|
|
|
4
4
|
from mashumaro.mixins.orjson import DataClassORJSONMixin
|
|
5
|
+
from shapely import Point
|
|
5
6
|
|
|
6
|
-
from pymammotion.
|
|
7
|
+
from pymammotion.data.model.generate_geojson import GeoJSONCollection, generate_geojson
|
|
8
|
+
from pymammotion.data.model.location import Dock, LocationPoint
|
|
9
|
+
from pymammotion.proto import NavGetCommDataAck, NavGetHashListAck, SvgMessageAckT
|
|
10
|
+
from pymammotion.utility.map import CoordinateConverter
|
|
11
|
+
from pymammotion.utility.mur_mur_hash import MurMurHashUtil
|
|
7
12
|
|
|
8
13
|
|
|
9
14
|
class PathType(IntEnum):
|
|
@@ -12,25 +17,175 @@ class PathType(IntEnum):
|
|
|
12
17
|
AREA = 0
|
|
13
18
|
OBSTACLE = 1
|
|
14
19
|
PATH = 2
|
|
20
|
+
LINE = 10
|
|
15
21
|
DUMP = 12
|
|
16
22
|
SVG = 13
|
|
17
23
|
|
|
18
24
|
|
|
25
|
+
@dataclass
|
|
26
|
+
class CommDataCouple:
|
|
27
|
+
x: float = 0.0
|
|
28
|
+
y: float = 0.0
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class AreaLabelName(DataClassORJSONMixin):
|
|
33
|
+
label: str = ""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class NavNameTime(DataClassORJSONMixin):
|
|
38
|
+
name: str = ""
|
|
39
|
+
create_time: int = 0
|
|
40
|
+
modify_time: int = 0
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class NavGetCommData(DataClassORJSONMixin):
|
|
45
|
+
pver: int = 0
|
|
46
|
+
sub_cmd: int = 0
|
|
47
|
+
result: int = 0
|
|
48
|
+
action: int = 0
|
|
49
|
+
type: int = 0
|
|
50
|
+
hash: int = 0
|
|
51
|
+
paternal_hash_a: int = 0
|
|
52
|
+
paternal_hash_b: int = 0
|
|
53
|
+
total_frame: int = 0
|
|
54
|
+
current_frame: int = 0
|
|
55
|
+
data_hash: int = 0
|
|
56
|
+
data_len: int = 0
|
|
57
|
+
data_couple: list["CommDataCouple"] = field(default_factory=list)
|
|
58
|
+
reserved: str = ""
|
|
59
|
+
name_time: NavNameTime = field(default_factory=NavNameTime)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
class MowPathPacket(DataClassORJSONMixin):
|
|
64
|
+
path_hash: int = 0
|
|
65
|
+
path_type: int = 0
|
|
66
|
+
path_total: int = 0
|
|
67
|
+
path_cur: int = 0
|
|
68
|
+
zone_hash: int = 0
|
|
69
|
+
data_couple: list["CommDataCouple"] = field(default_factory=list)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass
|
|
73
|
+
class MowPath(DataClassORJSONMixin):
|
|
74
|
+
pver: int = 0
|
|
75
|
+
sub_cmd: int = 0
|
|
76
|
+
result: int = 0
|
|
77
|
+
area: int = 0
|
|
78
|
+
time: int = 0
|
|
79
|
+
total_frame: int = 0
|
|
80
|
+
current_frame: int = 0
|
|
81
|
+
total_path_num: int = 0
|
|
82
|
+
valid_path_num: int = 0
|
|
83
|
+
data_hash: int = 0
|
|
84
|
+
transaction_id: int = 0
|
|
85
|
+
reserved: list[int] = field(default_factory=list)
|
|
86
|
+
data_len: int = 0
|
|
87
|
+
path_packets: list[MowPathPacket] = field(default_factory=list)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@dataclass
|
|
91
|
+
class SvgMessageData(DataClassORJSONMixin):
|
|
92
|
+
x_move: float = 0.0
|
|
93
|
+
y_move: float = 0.0
|
|
94
|
+
scale: float = 0.0
|
|
95
|
+
rotate: float = 0.0
|
|
96
|
+
base_width_m: float = 0.0
|
|
97
|
+
base_width_pix: int = 0
|
|
98
|
+
base_height_m: float = 0.0
|
|
99
|
+
base_height_pix: int = 0
|
|
100
|
+
data_count: int = 0
|
|
101
|
+
hide_svg: bool = False
|
|
102
|
+
name_count: int = 0
|
|
103
|
+
svg_file_name: str = ""
|
|
104
|
+
svg_file_data: str = ""
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@dataclass
|
|
108
|
+
class SvgMessage(DataClassORJSONMixin):
|
|
109
|
+
pver: int = 0
|
|
110
|
+
sub_cmd: int = 0
|
|
111
|
+
total_frame: int = 0
|
|
112
|
+
current_frame: int = 0
|
|
113
|
+
data_hash: int = 0
|
|
114
|
+
paternal_hash_a: int = 0
|
|
115
|
+
type: int = 0
|
|
116
|
+
result: int = 0
|
|
117
|
+
svg_message: "SvgMessageData" = field(default_factory=SvgMessageData)
|
|
118
|
+
|
|
119
|
+
|
|
19
120
|
@dataclass
|
|
20
121
|
class FrameList(DataClassORJSONMixin):
|
|
21
|
-
total_frame: int
|
|
22
|
-
|
|
122
|
+
total_frame: int = 0
|
|
123
|
+
sub_cmd: int = 0
|
|
124
|
+
data: list[NavGetCommData | SvgMessage] = field(default_factory=list)
|
|
23
125
|
|
|
24
126
|
|
|
25
127
|
@dataclass
|
|
26
|
-
class
|
|
128
|
+
class Plan(DataClassORJSONMixin):
|
|
129
|
+
pver: int = 0
|
|
130
|
+
sub_cmd: int = 2
|
|
131
|
+
area: int = 0
|
|
132
|
+
work_time: int = 0
|
|
133
|
+
version: str = ""
|
|
134
|
+
id: str = ""
|
|
135
|
+
user_id: str = ""
|
|
136
|
+
device_id: str = ""
|
|
137
|
+
plan_id: str = ""
|
|
138
|
+
task_id: str = ""
|
|
139
|
+
job_id: str = ""
|
|
140
|
+
start_time: str = ""
|
|
141
|
+
end_time: str = ""
|
|
142
|
+
week: int = 0
|
|
143
|
+
knife_height: int = 0
|
|
144
|
+
model: int = 0
|
|
145
|
+
edge_mode: int = 0
|
|
146
|
+
required_time: int = 0
|
|
147
|
+
route_angle: int = 0
|
|
148
|
+
route_model: int = 0
|
|
149
|
+
route_spacing: int = 0
|
|
150
|
+
ultrasonic_barrier: int = 0
|
|
151
|
+
total_plan_num: int = 0
|
|
152
|
+
plan_index: int = 0
|
|
153
|
+
result: int = 0
|
|
154
|
+
speed: float = 0.0
|
|
155
|
+
task_name: str = ""
|
|
156
|
+
job_name: str = ""
|
|
157
|
+
zone_hashs: list[int] = field(default_factory=list)
|
|
158
|
+
reserved: str = ""
|
|
159
|
+
start_date: str = ""
|
|
160
|
+
end_date: str = ""
|
|
161
|
+
trigger_type: int = 0
|
|
162
|
+
day: int = 0
|
|
163
|
+
weeks: list[int] = field(default_factory=list)
|
|
164
|
+
remained_seconds: int = 0
|
|
165
|
+
toward_mode: int = 0
|
|
166
|
+
toward_included_angle: int = 0
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@dataclass(eq=False, repr=False)
|
|
170
|
+
class NavGetHashListData(DataClassORJSONMixin):
|
|
27
171
|
"""Dataclass for NavGetHashListData."""
|
|
28
172
|
|
|
173
|
+
pver: int = 0
|
|
174
|
+
sub_cmd: int = 0
|
|
175
|
+
total_frame: int = 0
|
|
176
|
+
current_frame: int = 0
|
|
177
|
+
data_hash: int = 0
|
|
178
|
+
hash_len: int = 0
|
|
179
|
+
reserved: str = ""
|
|
180
|
+
result: int = 0
|
|
181
|
+
data_couple: list[int] = field(default_factory=list)
|
|
182
|
+
|
|
29
183
|
|
|
30
184
|
@dataclass
|
|
31
185
|
class RootHashList(DataClassORJSONMixin):
|
|
32
186
|
total_frame: int = 0
|
|
33
|
-
|
|
187
|
+
sub_cmd: int = 0
|
|
188
|
+
data: list[NavGetHashListData] = field(default_factory=list)
|
|
34
189
|
|
|
35
190
|
|
|
36
191
|
@dataclass
|
|
@@ -48,93 +203,214 @@ class HashList(DataClassORJSONMixin):
|
|
|
48
203
|
hashlist for all our hashIDs for verification
|
|
49
204
|
"""
|
|
50
205
|
|
|
51
|
-
|
|
52
|
-
area: dict = field(default_factory=dict) # type 0
|
|
53
|
-
path: dict = field(default_factory=dict) # type 2
|
|
54
|
-
obstacle: dict = field(default_factory=dict) # type 1
|
|
55
|
-
dump: dict = field(default_factory=dict) # type 12?
|
|
56
|
-
svg: dict = field(default_factory=dict) # type 13
|
|
206
|
+
root_hash_lists: list[RootHashList] = field(default_factory=list)
|
|
207
|
+
area: dict[int, FrameList] = field(default_factory=dict) # type 0
|
|
208
|
+
path: dict[int, FrameList] = field(default_factory=dict) # type 2
|
|
209
|
+
obstacle: dict[int, FrameList] = field(default_factory=dict) # type 1
|
|
210
|
+
dump: dict[int, FrameList] = field(default_factory=dict) # type 12? / sub cmd 4
|
|
211
|
+
svg: dict[int, FrameList] = field(default_factory=dict) # type 13
|
|
212
|
+
line: dict[int, FrameList] = field(default_factory=dict) # type 10 possibly breakpoint? / sub cmd 3
|
|
213
|
+
plan: dict[str, Plan] = field(default_factory=dict)
|
|
57
214
|
area_name: list[AreaHashNameList] = field(default_factory=list)
|
|
215
|
+
current_mow_path: dict[int, MowPath] = field(default_factory=dict)
|
|
216
|
+
generated_geojson: GeoJSONCollection = field(default_factory=dict)
|
|
58
217
|
|
|
59
|
-
def update_hash_lists(self, hashlist: list[int]) -> None:
|
|
218
|
+
def update_hash_lists(self, hashlist: list[int], bol_hash: str | None = None) -> None:
|
|
219
|
+
if bol_hash:
|
|
220
|
+
self.invalidate_maps(int(bol_hash))
|
|
60
221
|
self.area = {hash_id: frames for hash_id, frames in self.area.items() if hash_id in hashlist}
|
|
61
222
|
self.path = {hash_id: frames for hash_id, frames in self.path.items() if hash_id in hashlist}
|
|
62
223
|
self.obstacle = {hash_id: frames for hash_id, frames in self.obstacle.items() if hash_id in hashlist}
|
|
63
224
|
self.dump = {hash_id: frames for hash_id, frames in self.dump.items() if hash_id in hashlist}
|
|
64
225
|
self.svg = {hash_id: frames for hash_id, frames in self.svg.items() if hash_id in hashlist}
|
|
65
226
|
|
|
227
|
+
area_hashes = list(self.area.keys())
|
|
228
|
+
for hash_id, plan_task in self.plan.copy().items():
|
|
229
|
+
for item in plan_task.zone_hashs:
|
|
230
|
+
if item not in area_hashes:
|
|
231
|
+
self.plan.pop(hash_id)
|
|
232
|
+
break
|
|
233
|
+
|
|
234
|
+
self.area_name = [
|
|
235
|
+
area_item
|
|
236
|
+
for area_item in self.area_name
|
|
237
|
+
if area_item.hash in self.area.keys() or area_item.hash in hashlist
|
|
238
|
+
]
|
|
239
|
+
|
|
66
240
|
@property
|
|
67
241
|
def hashlist(self) -> list[int]:
|
|
68
|
-
if
|
|
242
|
+
if not self.root_hash_lists:
|
|
69
243
|
return []
|
|
70
|
-
|
|
244
|
+
# Combine data_couple from all RootHashLists
|
|
245
|
+
return [i for root_list in self.root_hash_lists for obj in root_list.data for i in obj.data_couple]
|
|
71
246
|
|
|
72
247
|
@property
|
|
73
|
-
def
|
|
248
|
+
def area_root_hashlist(self) -> list[int]:
|
|
249
|
+
if not self.root_hash_lists:
|
|
250
|
+
return []
|
|
251
|
+
# Combine data_couple from all RootHashLists
|
|
74
252
|
return [
|
|
75
253
|
i
|
|
76
|
-
for
|
|
254
|
+
for root_list in self.root_hash_lists
|
|
255
|
+
for obj in root_list.data
|
|
77
256
|
for i in obj.data_couple
|
|
78
|
-
if
|
|
79
|
-
not in set(self.area.keys()).union(
|
|
80
|
-
self.path.keys(), self.obstacle.keys(), self.dump.keys(), self.svg.keys()
|
|
81
|
-
)
|
|
257
|
+
if root_list.sub_cmd == 0
|
|
82
258
|
]
|
|
83
259
|
|
|
84
|
-
def
|
|
85
|
-
|
|
260
|
+
def missing_hashlist(self, sub_cmd: int = 0) -> list[int]:
|
|
261
|
+
"""Return missing hashlist."""
|
|
262
|
+
all_hash_ids = set(self.area.keys()).union(
|
|
263
|
+
self.path.keys(), self.obstacle.keys(), self.dump.keys(), self.svg.keys()
|
|
264
|
+
)
|
|
265
|
+
if sub_cmd == 3:
|
|
266
|
+
all_hash_ids = set(self.line.keys())
|
|
267
|
+
return [
|
|
268
|
+
i
|
|
269
|
+
for root_list in self.root_hash_lists
|
|
270
|
+
for obj in root_list.data
|
|
271
|
+
if root_list.sub_cmd == sub_cmd
|
|
272
|
+
for i in obj.data_couple
|
|
273
|
+
if i not in all_hash_ids
|
|
274
|
+
]
|
|
86
275
|
|
|
87
|
-
|
|
276
|
+
def missing_root_hash_frame(self, hash_list: NavGetHashListAck) -> list[int]:
|
|
277
|
+
"""Return missing root hash frame."""
|
|
278
|
+
target_root_list = next(
|
|
279
|
+
(
|
|
280
|
+
rhl
|
|
281
|
+
for rhl in self.root_hash_lists
|
|
282
|
+
if rhl.total_frame == hash_list.total_frame and rhl.sub_cmd == hash_list.sub_cmd
|
|
283
|
+
),
|
|
284
|
+
None,
|
|
285
|
+
)
|
|
286
|
+
if target_root_list is None:
|
|
287
|
+
return []
|
|
288
|
+
|
|
289
|
+
return self.find_missing_frames(target_root_list)
|
|
290
|
+
|
|
291
|
+
def update_root_hash_list(self, hash_list: NavGetHashListData) -> None:
|
|
292
|
+
target_root_list = next(
|
|
293
|
+
(
|
|
294
|
+
rhl
|
|
295
|
+
for rhl in self.root_hash_lists
|
|
296
|
+
if rhl.total_frame == hash_list.total_frame and rhl.sub_cmd == hash_list.sub_cmd
|
|
297
|
+
),
|
|
298
|
+
None,
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
if target_root_list is None:
|
|
302
|
+
# Create new RootHashList if none exists for this total_frame
|
|
303
|
+
new_root_list = RootHashList(total_frame=hash_list.total_frame, sub_cmd=hash_list.sub_cmd, data=[hash_list])
|
|
304
|
+
self.root_hash_lists.append(new_root_list)
|
|
305
|
+
return
|
|
306
|
+
|
|
307
|
+
for index, obj in enumerate(target_root_list.data):
|
|
88
308
|
if obj.current_frame == hash_list.current_frame:
|
|
89
309
|
# Replace the item if current_frame matches
|
|
90
|
-
|
|
310
|
+
target_root_list.data[index] = hash_list
|
|
91
311
|
return
|
|
92
312
|
|
|
93
313
|
# If no match was found, append the new item
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
def missing_hash_frame(self):
|
|
97
|
-
|
|
314
|
+
target_root_list.data.append(hash_list)
|
|
315
|
+
|
|
316
|
+
def missing_hash_frame(self, hash_ack: NavGetHashListAck) -> list[int]:
|
|
317
|
+
"""Returns a combined list of all missing frames across all RootHashLists."""
|
|
318
|
+
missing_frames = []
|
|
319
|
+
filtered_lists = [rl for rl in self.root_hash_lists if rl.sub_cmd == hash_ack.sub_cmd]
|
|
320
|
+
for root_list in filtered_lists:
|
|
321
|
+
missing = self.find_missing_frames(root_list)
|
|
322
|
+
if missing:
|
|
323
|
+
missing_frames.extend(missing)
|
|
324
|
+
return missing_frames
|
|
98
325
|
|
|
99
326
|
def missing_frame(self, hash_data: NavGetCommDataAck | SvgMessageAckT) -> list[int]:
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if hash_data.type == PathType.OBSTACLE:
|
|
104
|
-
return self._find_missing_frames(self.obstacle.get(hash_data.hash))
|
|
327
|
+
frame_list = self._get_frame_list_by_type_and_hash(hash_data)
|
|
328
|
+
return self.find_missing_frames(frame_list)
|
|
105
329
|
|
|
106
|
-
|
|
107
|
-
|
|
330
|
+
def _get_frame_list_by_type_and_hash(self, hash_data: NavGetCommDataAck | SvgMessageAckT) -> FrameList | None:
|
|
331
|
+
"""Get the appropriate FrameList based on hash_data type and hash."""
|
|
332
|
+
path_type_mapping = self._get_path_type_mapping()
|
|
333
|
+
target_dict = path_type_mapping.get(hash_data.type)
|
|
108
334
|
|
|
109
|
-
if
|
|
110
|
-
return
|
|
335
|
+
if target_dict is None:
|
|
336
|
+
return None
|
|
111
337
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
338
|
+
# Handle SvgMessage with data_hash attribute
|
|
339
|
+
if isinstance(hash_data, SvgMessageAckT):
|
|
340
|
+
return target_dict.get(hash_data.data_hash)
|
|
341
|
+
|
|
342
|
+
# Handle NavGetCommDataAck with hash attribute
|
|
343
|
+
return target_dict.get(hash_data.hash)
|
|
344
|
+
|
|
345
|
+
def update_plan(self, plan: Plan) -> None:
|
|
346
|
+
if plan.total_plan_num != 0:
|
|
347
|
+
self.plan[plan.plan_id] = plan
|
|
348
|
+
|
|
349
|
+
def _get_path_type_mapping(self) -> dict[int, dict[int, FrameList]]:
|
|
350
|
+
"""Return mapping of PathType to corresponding hash dictionary."""
|
|
351
|
+
return {
|
|
352
|
+
PathType.AREA: self.area,
|
|
353
|
+
PathType.OBSTACLE: self.obstacle,
|
|
354
|
+
PathType.PATH: self.path,
|
|
355
|
+
PathType.LINE: self.line,
|
|
356
|
+
PathType.DUMP: self.dump,
|
|
357
|
+
PathType.SVG: self.svg,
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
def update(self, hash_data: NavGetCommData | SvgMessage) -> bool:
|
|
116
361
|
"""Update the map data."""
|
|
117
|
-
|
|
362
|
+
|
|
363
|
+
if hash_data.type == PathType.AREA and isinstance(hash_data, NavGetCommData):
|
|
118
364
|
existing_name = next((area for area in self.area_name if area.hash == hash_data.hash), None)
|
|
119
365
|
if not existing_name:
|
|
120
|
-
name = f"area {len(self.area_name)+1}"
|
|
366
|
+
name = f"area {len(self.area_name)+1}"
|
|
121
367
|
self.area_name.append(AreaHashNameList(name=name, hash=hash_data.hash))
|
|
122
|
-
|
|
368
|
+
result = self._add_hash_data(self.area, hash_data)
|
|
369
|
+
self.update_hash_lists(self.hashlist)
|
|
370
|
+
return result
|
|
371
|
+
|
|
372
|
+
path_type_mapping = self._get_path_type_mapping()
|
|
373
|
+
target_dict = path_type_mapping.get(hash_data.type)
|
|
123
374
|
|
|
124
|
-
if
|
|
125
|
-
return self._add_hash_data(
|
|
375
|
+
if target_dict is not None:
|
|
376
|
+
return self._add_hash_data(target_dict, hash_data)
|
|
126
377
|
|
|
127
|
-
|
|
128
|
-
|
|
378
|
+
return False
|
|
379
|
+
|
|
380
|
+
def find_missing_mow_path_frames(self) -> list[int]:
|
|
381
|
+
"""Find missing frames in current_mow_path based on total_frame."""
|
|
382
|
+
if not self.current_mow_path:
|
|
383
|
+
return []
|
|
129
384
|
|
|
130
|
-
|
|
131
|
-
|
|
385
|
+
# Get total_frame from any MowPath object (they should all have the same total_frame)
|
|
386
|
+
total_frame = next(iter(self.current_mow_path.values())).total_frame
|
|
132
387
|
|
|
133
|
-
if
|
|
134
|
-
return
|
|
388
|
+
if total_frame == 0:
|
|
389
|
+
return []
|
|
390
|
+
|
|
391
|
+
if total_frame == len(self.current_mow_path):
|
|
392
|
+
return []
|
|
393
|
+
|
|
394
|
+
# Generate list of expected frame numbers (1 to total_frame)
|
|
395
|
+
expected_frames = set(range(1, total_frame + 1))
|
|
396
|
+
|
|
397
|
+
# Get current frame numbers from dictionary keys
|
|
398
|
+
current_frames = set(self.current_mow_path.keys())
|
|
399
|
+
|
|
400
|
+
# Return sorted list of missing frames
|
|
401
|
+
missing_frames = sorted(expected_frames - current_frames)
|
|
402
|
+
return missing_frames
|
|
403
|
+
|
|
404
|
+
def update_mow_path(self, path: MowPath) -> None:
|
|
405
|
+
"""Update the current_mow_path with the latest MowPath data."""
|
|
406
|
+
# TODO check if we need to clear the current_mow_path first
|
|
407
|
+
self.current_mow_path[path.current_frame] = path
|
|
135
408
|
|
|
136
409
|
@staticmethod
|
|
137
|
-
def
|
|
410
|
+
def find_missing_frames(frame_list: FrameList | RootHashList | None) -> list[int]:
|
|
411
|
+
if frame_list is None:
|
|
412
|
+
return []
|
|
413
|
+
|
|
138
414
|
if frame_list.total_frame == len(frame_list.data):
|
|
139
415
|
return []
|
|
140
416
|
number_list = list(range(1, frame_list.total_frame + 1))
|
|
@@ -144,22 +420,59 @@ class HashList(DataClassORJSONMixin):
|
|
|
144
420
|
return missing_numbers
|
|
145
421
|
|
|
146
422
|
@staticmethod
|
|
147
|
-
def _add_hash_data(hash_dict: dict, hash_data:
|
|
148
|
-
if isinstance(hash_data,
|
|
149
|
-
if hash_dict.get(hash_data.data_hash) is None:
|
|
423
|
+
def _add_hash_data(hash_dict: dict[int, FrameList], hash_data: NavGetCommData | SvgMessage) -> bool:
|
|
424
|
+
if isinstance(hash_data, SvgMessage):
|
|
425
|
+
if hash_dict.get(hash_data.data_hash, None) is None:
|
|
150
426
|
hash_dict[hash_data.data_hash] = FrameList(total_frame=hash_data.total_frame, data=[hash_data])
|
|
151
427
|
return True
|
|
152
428
|
|
|
153
429
|
if hash_data not in hash_dict[hash_data.data_hash].data:
|
|
430
|
+
exists = next(
|
|
431
|
+
(
|
|
432
|
+
rhl
|
|
433
|
+
for rhl in hash_dict[hash_data.data_hash].data
|
|
434
|
+
if rhl.current_frame == hash_data.current_frame
|
|
435
|
+
),
|
|
436
|
+
None,
|
|
437
|
+
)
|
|
438
|
+
if exists:
|
|
439
|
+
return True
|
|
154
440
|
hash_dict[hash_data.data_hash].data.append(hash_data)
|
|
155
441
|
return True
|
|
156
442
|
return False
|
|
157
443
|
|
|
158
|
-
if hash_dict.get(hash_data.hash) is None:
|
|
444
|
+
if hash_dict.get(hash_data.hash, None) is None:
|
|
159
445
|
hash_dict[hash_data.hash] = FrameList(total_frame=hash_data.total_frame, data=[hash_data])
|
|
160
446
|
return True
|
|
161
447
|
|
|
162
448
|
if hash_data not in hash_dict[hash_data.hash].data:
|
|
449
|
+
exists = next(
|
|
450
|
+
(rhl for rhl in hash_dict[hash_data.hash].data if rhl.current_frame == hash_data.current_frame),
|
|
451
|
+
None,
|
|
452
|
+
)
|
|
453
|
+
if exists:
|
|
454
|
+
return True
|
|
163
455
|
hash_dict[hash_data.hash].data.append(hash_data)
|
|
164
456
|
return True
|
|
165
457
|
return False
|
|
458
|
+
|
|
459
|
+
def invalidate_maps(self, bol_hash: int) -> None:
|
|
460
|
+
if MurMurHashUtil.hash_unsigned_list(self.area_root_hashlist) != bol_hash:
|
|
461
|
+
self.root_hash_lists = []
|
|
462
|
+
|
|
463
|
+
def generate_geojson(self, rtk: LocationPoint, dock: Dock) -> GeoJSONCollection:
|
|
464
|
+
"""Generate geojson from frames."""
|
|
465
|
+
coordinator_converter = CoordinateConverter(rtk.latitude, rtk.longitude)
|
|
466
|
+
RTK_real_loc = coordinator_converter.enu_to_lla(0, 0)
|
|
467
|
+
|
|
468
|
+
dock_location = coordinator_converter.enu_to_lla(dock.latitude, dock.longitude)
|
|
469
|
+
dock_rotation = coordinator_converter.get_transform_yaw_with_yaw(dock.rotation) + 180
|
|
470
|
+
|
|
471
|
+
self.generated_geojson = generate_geojson(
|
|
472
|
+
self,
|
|
473
|
+
Point(RTK_real_loc.latitude, RTK_real_loc.longitude),
|
|
474
|
+
Point(dock_location.latitude, dock_location.longitude),
|
|
475
|
+
int(dock_rotation),
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
return self.generated_geojson
|
|
@@ -6,7 +6,7 @@ from mashumaro.mixins.orjson import DataClassORJSONMixin
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
@dataclass
|
|
9
|
-
class
|
|
9
|
+
class LocationPoint(DataClassORJSONMixin):
|
|
10
10
|
"""Returns a lat long."""
|
|
11
11
|
|
|
12
12
|
latitude: float = 0.0
|
|
@@ -18,7 +18,7 @@ class Point(DataClassORJSONMixin):
|
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
@dataclass
|
|
21
|
-
class Dock(
|
|
21
|
+
class Dock(LocationPoint):
|
|
22
22
|
"""Stores robot dock position."""
|
|
23
23
|
|
|
24
24
|
rotation: int = 0
|
|
@@ -28,8 +28,8 @@ class Dock(Point):
|
|
|
28
28
|
class Location(DataClassORJSONMixin):
|
|
29
29
|
"""Stores/retrieves RTK GPS data."""
|
|
30
30
|
|
|
31
|
-
device:
|
|
32
|
-
RTK:
|
|
31
|
+
device: LocationPoint = field(default_factory=LocationPoint)
|
|
32
|
+
RTK: LocationPoint = field(default_factory=LocationPoint)
|
|
33
33
|
dock: Dock = field(default_factory=Dock)
|
|
34
34
|
position_type: int = 0
|
|
35
35
|
orientation: int = 0 # 360 degree rotation +-
|
|
@@ -10,6 +10,14 @@ class CuttingMode(IntEnum):
|
|
|
10
10
|
no_grid = 3
|
|
11
11
|
|
|
12
12
|
|
|
13
|
+
class CuttingSpeedMode(IntEnum):
|
|
14
|
+
"""speed"""
|
|
15
|
+
|
|
16
|
+
normal = 0
|
|
17
|
+
slow = 1
|
|
18
|
+
fast = 2
|
|
19
|
+
|
|
20
|
+
|
|
13
21
|
class BorderPatrolMode(IntEnum):
|
|
14
22
|
""""""
|
|
15
23
|
|
|
@@ -44,13 +52,21 @@ class TraversalMode(IntEnum):
|
|
|
44
52
|
follow_perimeter = 1
|
|
45
53
|
|
|
46
54
|
|
|
47
|
-
class
|
|
55
|
+
class TurningMode(IntEnum):
|
|
56
|
+
"""Turning mode on corners."""
|
|
57
|
+
|
|
58
|
+
zero_turn = 0
|
|
59
|
+
multipoint = 1
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class DetectionStrategy(IntEnum):
|
|
48
63
|
"""Matches up with ultra_wave."""
|
|
49
64
|
|
|
50
65
|
direct_touch = 0
|
|
51
66
|
slow_touch = 1
|
|
52
67
|
less_touch = 2
|
|
53
68
|
no_touch = 10 # luba 2 yuka only or possibly value of 10
|
|
69
|
+
sensitive = 11 # x series
|
|
54
70
|
|
|
55
71
|
|
|
56
72
|
class PathAngleSetting(IntEnum):
|
|
@@ -1,21 +1,13 @@
|
|
|
1
1
|
from dataclasses import dataclass, field
|
|
2
|
-
from typing import Optional
|
|
3
2
|
|
|
4
3
|
from mashumaro.mixins.orjson import DataClassORJSONMixin
|
|
5
4
|
|
|
6
|
-
from pymammotion.proto
|
|
7
|
-
from pymammotion.proto.luba_msg import LubaMsg
|
|
8
|
-
from pymammotion.proto.luba_mul import SocMul
|
|
9
|
-
from pymammotion.proto.mctrl_driver import MctlDriver
|
|
10
|
-
from pymammotion.proto.mctrl_nav import MctlNav
|
|
11
|
-
from pymammotion.proto.mctrl_ota import MctlOta
|
|
12
|
-
from pymammotion.proto.mctrl_pept import MctlPept
|
|
13
|
-
from pymammotion.proto.mctrl_sys import MctlSys
|
|
5
|
+
from pymammotion.proto import DevNet, LubaMsg, MctlDriver, MctlNav, MctlOta, MctlPept, MctlSys, SocMul
|
|
14
6
|
|
|
15
7
|
|
|
16
8
|
@dataclass
|
|
17
9
|
class RawMowerData:
|
|
18
|
-
raw:
|
|
10
|
+
raw: LubaMsg | None = field(default_factory=LubaMsg)
|
|
19
11
|
|
|
20
12
|
@classmethod
|
|
21
13
|
def from_raw(cls, raw: dict) -> "RawMowerData":
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
from dataclasses import dataclass
|
|
2
|
-
from typing import Optional
|
|
3
2
|
|
|
4
3
|
from mashumaro.mixins.orjson import DataClassORJSONMixin
|
|
5
4
|
|
|
@@ -7,14 +6,14 @@ from mashumaro.mixins.orjson import DataClassORJSONMixin
|
|
|
7
6
|
@dataclass
|
|
8
7
|
class RegionData(DataClassORJSONMixin):
|
|
9
8
|
def __init__(self) -> None:
|
|
10
|
-
self.hash:
|
|
9
|
+
self.hash: int = 0
|
|
11
10
|
self.action: int = 0
|
|
12
11
|
self.current_frame: int = 0
|
|
13
|
-
self.data_hash:
|
|
12
|
+
self.data_hash: int = 0
|
|
14
13
|
self.data_len: int = 0
|
|
15
|
-
self.p_hash_a:
|
|
16
|
-
self.p_hash_b:
|
|
17
|
-
self.path:
|
|
14
|
+
self.p_hash_a: int = 0
|
|
15
|
+
self.p_hash_b: int = 0
|
|
16
|
+
self.path: list[list[float]] | None = None
|
|
18
17
|
self.pver: int = 0
|
|
19
18
|
self.result: int = 0
|
|
20
19
|
self.sub_cmd: int = 0
|
|
@@ -72,31 +71,31 @@ class RegionData(DataClassORJSONMixin):
|
|
|
72
71
|
def set_current_frame(self, current_frame: int) -> None:
|
|
73
72
|
self.current_frame = current_frame
|
|
74
73
|
|
|
75
|
-
def get_path(self) ->
|
|
74
|
+
def get_path(self) -> list[list[float]] | None:
|
|
76
75
|
return self.path
|
|
77
76
|
|
|
78
77
|
def set_path(self, path: list[list[float]]) -> None:
|
|
79
78
|
self.path = path
|
|
80
79
|
|
|
81
|
-
def get_hash(self) ->
|
|
80
|
+
def get_hash(self) -> int | None:
|
|
82
81
|
return self.hash
|
|
83
82
|
|
|
84
83
|
def set_data_hash(self, data_hash: int) -> None:
|
|
85
84
|
self.data_hash = data_hash
|
|
86
85
|
|
|
87
|
-
def get_data_hash(self) ->
|
|
86
|
+
def get_data_hash(self) -> int | None:
|
|
88
87
|
return self.data_hash
|
|
89
88
|
|
|
90
89
|
def set_p_hash_a(self, p_hash_a: int) -> None:
|
|
91
90
|
self.p_hash_a = p_hash_a
|
|
92
91
|
|
|
93
|
-
def get_p_hash_a(self) ->
|
|
92
|
+
def get_p_hash_a(self) -> int | None:
|
|
94
93
|
return self.p_hash_a
|
|
95
94
|
|
|
96
95
|
def set_p_hash_b(self, p_hash_b: int) -> None:
|
|
97
96
|
self.p_hash_b = p_hash_b
|
|
98
97
|
|
|
99
|
-
def get_p_hash_b(self) ->
|
|
98
|
+
def get_p_hash_b(self) -> int | None:
|
|
100
99
|
return self.p_hash_b
|
|
101
100
|
|
|
102
101
|
def __str__(self) -> str:
|