petal-leafsdk 0.2.4__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.
- petal_leafsdk/__init__.py +11 -0
- petal_leafsdk/data_model.py +290 -0
- petal_leafsdk/mavlink_helpers.py +71 -0
- petal_leafsdk/mission_plan_executor.py +434 -0
- petal_leafsdk/mission_step_executor.py +583 -0
- petal_leafsdk/plugin.py +409 -0
- petal_leafsdk/redis_helpers.py +33 -0
- petal_leafsdk-0.2.4.dist-info/METADATA +12 -0
- petal_leafsdk-0.2.4.dist-info/RECORD +11 -0
- petal_leafsdk-0.2.4.dist-info/WHEEL +4 -0
- petal_leafsdk-0.2.4.dist-info/entry_points.txt +7 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from importlib.metadata import PackageNotFoundError, version as _pkg_version
|
|
3
|
+
|
|
4
|
+
logger = logging.getLogger(__name__)
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
# ⚠️ Use the *distribution* name (what you put in pyproject.toml), not necessarily the import name
|
|
8
|
+
__version__ = _pkg_version("petal-leafsdk")
|
|
9
|
+
except PackageNotFoundError:
|
|
10
|
+
# Useful during local development before install; pick what you prefer here
|
|
11
|
+
__version__ = "0.0.0"
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
# petal_leafsdk/data_model.py
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
from typing import Any, List, Optional, Literal, Union, Tuple, Sequence, Dict
|
|
5
|
+
from enum import Enum
|
|
6
|
+
|
|
7
|
+
Tuple3D = Tuple[float, float, float] # exact length 3
|
|
8
|
+
|
|
9
|
+
# Mission schema
|
|
10
|
+
class TakeoffParams(BaseModel):
|
|
11
|
+
alt: float
|
|
12
|
+
|
|
13
|
+
class GotoLocalPositionParams(BaseModel):
|
|
14
|
+
waypoints: Union[Tuple3D, Sequence[Tuple3D]]
|
|
15
|
+
yaws_deg: Optional[Union[float, Sequence[float]]] = 0.0
|
|
16
|
+
speed: Optional[Union[float, Sequence[float]]] = 0.2
|
|
17
|
+
yaw_speed: Optional[Union[float, Sequence[float], Literal["sync"]]] = "sync"
|
|
18
|
+
is_pausable: bool = True
|
|
19
|
+
average_deceleration: float = 0.5 # m2/s
|
|
20
|
+
|
|
21
|
+
class GotoGPSWaypointParams(BaseModel):
|
|
22
|
+
waypoints: Union[Tuple3D, Sequence[Tuple3D]]
|
|
23
|
+
yaws_deg: Optional[Union[float, Sequence[float]]] = 0.0
|
|
24
|
+
speed: Optional[Union[float, Sequence[float]]] = 0.2
|
|
25
|
+
yaw_speed: Optional[Union[float, Sequence[float], Literal["sync"]]] = "sync"
|
|
26
|
+
is_pausable: bool = True
|
|
27
|
+
average_deceleration: float = 0.5 # m2/s
|
|
28
|
+
|
|
29
|
+
class GotoRelativeParams(BaseModel):
|
|
30
|
+
waypoints: Union[Tuple3D, Sequence[Tuple3D]]
|
|
31
|
+
yaws_deg: Optional[Union[float, Sequence[float]]] = 0.0
|
|
32
|
+
speed: Optional[Union[float, Sequence[float]]] = 0.2
|
|
33
|
+
yaw_speed: Optional[Union[float, Sequence[float], Literal["sync"]]] = "sync"
|
|
34
|
+
is_pausable: bool = True
|
|
35
|
+
average_deceleration: float = 0.5 # m2/s
|
|
36
|
+
|
|
37
|
+
class YawAbsoluteParams(BaseModel):
|
|
38
|
+
yaws_deg: Union[float, Sequence[float]]
|
|
39
|
+
yaw_speed: Optional[Union[float, Sequence[float]]] = 30.0
|
|
40
|
+
is_pausable: bool = True
|
|
41
|
+
average_deceleration: float = 0.5 # m2/s
|
|
42
|
+
|
|
43
|
+
class YawRelativeParams(BaseModel):
|
|
44
|
+
yaws_deg: Union[float, Sequence[float]]
|
|
45
|
+
yaw_speed: Optional[Union[float, Sequence[float]]] = 30.0
|
|
46
|
+
is_pausable: bool = True
|
|
47
|
+
average_deceleration: float = 0.5 # m2/s
|
|
48
|
+
|
|
49
|
+
class WaitParams(BaseModel):
|
|
50
|
+
duration: float
|
|
51
|
+
|
|
52
|
+
class TakeoffNode(BaseModel):
|
|
53
|
+
name: str
|
|
54
|
+
type: Literal["Takeoff"]
|
|
55
|
+
params: TakeoffParams
|
|
56
|
+
|
|
57
|
+
class GotoLocalPositionNode(BaseModel):
|
|
58
|
+
name: str
|
|
59
|
+
type: Literal["GotoLocalPosition"]
|
|
60
|
+
params: GotoLocalPositionParams
|
|
61
|
+
|
|
62
|
+
class GotoGPSWaypointNode(BaseModel):
|
|
63
|
+
name: str
|
|
64
|
+
type: Literal["GotoGPSWaypoint"]
|
|
65
|
+
params: GotoGPSWaypointParams
|
|
66
|
+
|
|
67
|
+
class GotoRelativeNode(BaseModel):
|
|
68
|
+
name: str
|
|
69
|
+
type: Literal["GotoRelative"]
|
|
70
|
+
params: GotoRelativeParams
|
|
71
|
+
|
|
72
|
+
class YawAbsoluteNode(BaseModel):
|
|
73
|
+
name: str
|
|
74
|
+
type: Literal["YawAbsolute"]
|
|
75
|
+
params: YawAbsoluteParams
|
|
76
|
+
|
|
77
|
+
class YawRelativeNode(BaseModel):
|
|
78
|
+
name: str
|
|
79
|
+
type: Literal["YawRelative"]
|
|
80
|
+
params: YawRelativeParams
|
|
81
|
+
|
|
82
|
+
class WaitNode(BaseModel):
|
|
83
|
+
name: str
|
|
84
|
+
type: Literal["Wait"]
|
|
85
|
+
params: WaitParams
|
|
86
|
+
|
|
87
|
+
class LandNode(BaseModel):
|
|
88
|
+
name: str
|
|
89
|
+
type: Literal["Land"]
|
|
90
|
+
params: Optional[dict] = None
|
|
91
|
+
|
|
92
|
+
class Edge(BaseModel):
|
|
93
|
+
from_: str = Field(..., alias="from")
|
|
94
|
+
to: str
|
|
95
|
+
condition: Optional[str] = None
|
|
96
|
+
|
|
97
|
+
model_config = {"populate_by_name": True}
|
|
98
|
+
|
|
99
|
+
Node = Union[TakeoffNode, GotoLocalPositionNode, GotoGPSWaypointNode, GotoRelativeNode, YawAbsoluteNode, YawRelativeNode, WaitNode, LandNode]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class MissionStepProgress(BaseModel):
|
|
103
|
+
completed_mission_step_id: str
|
|
104
|
+
completed_mission_step_description: Optional[str] = ""
|
|
105
|
+
next_mission_step_id: str
|
|
106
|
+
next_mission_step_description: Optional[str] = ""
|
|
107
|
+
|
|
108
|
+
class ProgressUpdateSubscription(BaseModel):
|
|
109
|
+
address: str # e.g., http://localhost:5000/WHMS/v1/update_step_progress
|
|
110
|
+
|
|
111
|
+
class SafeReturnPlanRequestAddress(BaseModel):
|
|
112
|
+
address: str # e.g., http://localhost:5000/WHMS/v1/safe_return_plan_request
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class JoystickMode(Enum):
|
|
116
|
+
DISABLED = "disabled"
|
|
117
|
+
ENABLED = "enabled"
|
|
118
|
+
ENABLED_ON_PAUSE = "enabled_on_pause"
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class MissionConfig:
|
|
122
|
+
joystick_mode: JoystickMode = JoystickMode.ENABLED
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class MissionGraph(BaseModel):
|
|
126
|
+
id: str = Field(..., description="Unique identifier for the mission", example="main")
|
|
127
|
+
joystick_mode: JoystickMode = JoystickMode.ENABLED
|
|
128
|
+
nodes: List[Node] = Field(..., description="List of mission steps/nodes")
|
|
129
|
+
edges: List[Edge] = Field(..., description="Connections between mission steps/nodes")
|
|
130
|
+
|
|
131
|
+
model_config = {
|
|
132
|
+
"json_schema_extra": {
|
|
133
|
+
"example": {
|
|
134
|
+
"id": "main",
|
|
135
|
+
"nodes": [
|
|
136
|
+
{
|
|
137
|
+
"name": "Takeoff",
|
|
138
|
+
"type": "Takeoff",
|
|
139
|
+
"params": {
|
|
140
|
+
"alt": 1
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
"name": "Wait 1",
|
|
145
|
+
"type": "Wait",
|
|
146
|
+
"params": {
|
|
147
|
+
"duration": 2
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
"name": "GotoLocalWaypoint 1",
|
|
152
|
+
"type": "GotoLocalPosition",
|
|
153
|
+
"params": {
|
|
154
|
+
"waypoints": [
|
|
155
|
+
[
|
|
156
|
+
0.5,
|
|
157
|
+
0.0,
|
|
158
|
+
1.0
|
|
159
|
+
]
|
|
160
|
+
],
|
|
161
|
+
"yaws_deg": [
|
|
162
|
+
0.0
|
|
163
|
+
],
|
|
164
|
+
"speed": [
|
|
165
|
+
0.2
|
|
166
|
+
],
|
|
167
|
+
"yaw_speed": [
|
|
168
|
+
30.0
|
|
169
|
+
]
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
"name": "GotoLocalWaypoint 2",
|
|
174
|
+
"type": "GotoLocalPosition",
|
|
175
|
+
"params": {
|
|
176
|
+
"waypoints": [
|
|
177
|
+
[
|
|
178
|
+
0.5,
|
|
179
|
+
0.5,
|
|
180
|
+
1.0
|
|
181
|
+
],
|
|
182
|
+
[
|
|
183
|
+
0.0,
|
|
184
|
+
0.0,
|
|
185
|
+
1.0
|
|
186
|
+
]
|
|
187
|
+
],
|
|
188
|
+
"yaws_deg": [
|
|
189
|
+
0.0,
|
|
190
|
+
0.0
|
|
191
|
+
],
|
|
192
|
+
"speed": [
|
|
193
|
+
0.2,
|
|
194
|
+
0.2
|
|
195
|
+
],
|
|
196
|
+
"yaw_speed": [
|
|
197
|
+
30.0,
|
|
198
|
+
30.0
|
|
199
|
+
]
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
"name": "GotoLocalWaypoint 3",
|
|
204
|
+
"type": "GotoLocalPosition",
|
|
205
|
+
"params": {
|
|
206
|
+
"waypoints": [
|
|
207
|
+
[
|
|
208
|
+
0.0,
|
|
209
|
+
0.5,
|
|
210
|
+
1.0
|
|
211
|
+
],
|
|
212
|
+
[
|
|
213
|
+
0.5,
|
|
214
|
+
0.5,
|
|
215
|
+
1.0
|
|
216
|
+
],
|
|
217
|
+
[
|
|
218
|
+
0.5,
|
|
219
|
+
0.0,
|
|
220
|
+
1.0
|
|
221
|
+
]
|
|
222
|
+
],
|
|
223
|
+
"yaws_deg": [
|
|
224
|
+
0.0,
|
|
225
|
+
10.0,
|
|
226
|
+
20.0
|
|
227
|
+
],
|
|
228
|
+
"speed": [
|
|
229
|
+
0.2,
|
|
230
|
+
0.3,
|
|
231
|
+
0.4
|
|
232
|
+
],
|
|
233
|
+
"yaw_speed": [
|
|
234
|
+
10.0,
|
|
235
|
+
20.0,
|
|
236
|
+
20.0
|
|
237
|
+
]
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
"name": "Wait 2",
|
|
242
|
+
"type": "Wait",
|
|
243
|
+
"params": {
|
|
244
|
+
"duration": 2
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
"name": "Land",
|
|
249
|
+
"type": "Land",
|
|
250
|
+
"params": {}
|
|
251
|
+
}
|
|
252
|
+
],
|
|
253
|
+
"edges": [
|
|
254
|
+
{
|
|
255
|
+
"from": "Takeoff",
|
|
256
|
+
"to": "Wait 1",
|
|
257
|
+
"condition": None
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
"from": "Wait 1",
|
|
261
|
+
"to": "GotoLocalWaypoint 1",
|
|
262
|
+
"condition": None
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
"from": "GotoLocalWaypoint 1",
|
|
266
|
+
"to": "GotoLocalWaypoint 2",
|
|
267
|
+
"condition": None
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
"from": "GotoLocalWaypoint 2",
|
|
271
|
+
"to": "GotoLocalWaypoint 3",
|
|
272
|
+
"condition": None
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
"from": "GotoLocalWaypoint 3",
|
|
276
|
+
"to": "Wait 2",
|
|
277
|
+
"condition": None
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
"from": "Wait 2",
|
|
281
|
+
"to": "Land",
|
|
282
|
+
"condition": None
|
|
283
|
+
}
|
|
284
|
+
]
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
class CancelMissionRequest(BaseModel):
|
|
290
|
+
action: Optional[Literal["NONE", "HOVER", "RETURN_TO_HOME", "LAND_IMMEDIATELY"]] = "HOVER"
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# leafsdk/utils/mavlink_helpers.py
|
|
2
|
+
# MAVLink helper functions for LeafSDK
|
|
3
|
+
|
|
4
|
+
import sys, time
|
|
5
|
+
import os
|
|
6
|
+
from pymavlink.dialects.v20 import droneleaf_mav_msgs as leafMAV
|
|
7
|
+
from petal_app_manager.proxies.external import MavLinkExternalProxy
|
|
8
|
+
from leafsdk import logger
|
|
9
|
+
from typing import Optional, Union
|
|
10
|
+
from leafsdk.utils.logstyle import LogIcons
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_mav_msg_name_from_id(msg_id: int) -> Union[str, int]:
|
|
14
|
+
"""
|
|
15
|
+
Get MAVLink message name from its ID.
|
|
16
|
+
"""
|
|
17
|
+
try:
|
|
18
|
+
msg_name = leafMAV.mavlink_map[msg_id].name
|
|
19
|
+
return msg_name
|
|
20
|
+
except KeyError:
|
|
21
|
+
logger.warning(f"{LogIcons.WARNING} Unknown MAVLink message ID: {msg_id}")
|
|
22
|
+
return msg_id
|
|
23
|
+
|
|
24
|
+
def parse_heartbeat(msg):
|
|
25
|
+
"""
|
|
26
|
+
Parse heartbeat message and return system status info.
|
|
27
|
+
"""
|
|
28
|
+
if msg.get_type() != "HEARTBEAT":
|
|
29
|
+
logger.warning("Expected HEARTBEAT message, got something else.")
|
|
30
|
+
return None
|
|
31
|
+
|
|
32
|
+
status = {
|
|
33
|
+
"type": msg.type,
|
|
34
|
+
"autopilot": msg.autopilot,
|
|
35
|
+
"base_mode": msg.base_mode,
|
|
36
|
+
"custom_mode": msg.custom_mode,
|
|
37
|
+
"system_status": msg.system_status,
|
|
38
|
+
"mavlink_version": msg.mavlink_version,
|
|
39
|
+
}
|
|
40
|
+
logger.debug(f"Parsed heartbeat: {status}")
|
|
41
|
+
return status
|
|
42
|
+
|
|
43
|
+
def setup_mavlink_subscriptions(key: str, callback: callable, mav_proxy: Optional[MavLinkExternalProxy] = None, duplicate_filter_interval: Optional[float] = 0):
|
|
44
|
+
"""Setup MAVLink subscriptions - call this after object creation if using MAVLink"""
|
|
45
|
+
if mav_proxy is None:
|
|
46
|
+
logger.warning(f"{LogIcons.WARNING} MAVLink proxy not provided, skipping MAVLink subscriptions")
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
# Subscribe to a general broadcast channel
|
|
51
|
+
mav_proxy.register_handler(
|
|
52
|
+
key=key,
|
|
53
|
+
fn=callback,
|
|
54
|
+
duplicate_filter_interval=duplicate_filter_interval
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
logger.info(f"{LogIcons.SUCCESS} MAVLink subscriptions set up successfully to {get_mav_msg_name_from_id(int(key))}.")
|
|
58
|
+
except Exception as e:
|
|
59
|
+
logger.error(f"{LogIcons.ERROR} Failed to set up MAVLink subscriptions: {e}")
|
|
60
|
+
|
|
61
|
+
def unsetup_mavlink_subscriptions(key: str, callback: callable, mav_proxy: Optional[MavLinkExternalProxy] = None):
|
|
62
|
+
"""Unsubscribe from MAVLink channels - call this when the step is no longer needed"""
|
|
63
|
+
if mav_proxy is None:
|
|
64
|
+
logger.warning(f"{LogIcons.WARNING} MAVLink proxy not provided, skipping MAVLink unsubscriptions")
|
|
65
|
+
return
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
mav_proxy.unregister_handler(key=key, fn=callback)
|
|
69
|
+
logger.info(f"{LogIcons.SUCCESS} MAVLink subscriptions to {get_mav_msg_name_from_id(int(key))} have been removed.")
|
|
70
|
+
except Exception as e:
|
|
71
|
+
logger.error(f"{LogIcons.ERROR} Failed to remove MAVLink subscriptions: {e}")
|