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.
@@ -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}")