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
petal_leafsdk/plugin.py
ADDED
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
# petal-leafsdk/plugin.py
|
|
2
|
+
# Petal plugin for Leaf SDK integration and mission planning
|
|
3
|
+
|
|
4
|
+
import time
|
|
5
|
+
from . import logger
|
|
6
|
+
|
|
7
|
+
from typing import Any, Dict
|
|
8
|
+
import asyncio, httpx
|
|
9
|
+
import traceback
|
|
10
|
+
|
|
11
|
+
from pymavlink.dialects.v20 import droneleaf_mav_msgs as leafMAV
|
|
12
|
+
from pymavlink import mavutil
|
|
13
|
+
|
|
14
|
+
from petal_app_manager.plugins.base import Petal
|
|
15
|
+
from petal_app_manager.plugins.decorators import http_action
|
|
16
|
+
from petal_app_manager.proxies import (
|
|
17
|
+
MQTTProxy,
|
|
18
|
+
MavLinkExternalProxy,
|
|
19
|
+
RedisProxy
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
from petal_leafsdk.data_model import CancelMissionRequest, MissionGraph, ProgressUpdateSubscription, SafeReturnPlanRequestAddress
|
|
23
|
+
from petal_leafsdk.mission_plan_executor import MissionPlanExecutor, MissionState
|
|
24
|
+
|
|
25
|
+
# Mission imports
|
|
26
|
+
from leafsdk.core.mission.mission_clock import MissionClock
|
|
27
|
+
from leafsdk.core.mission.mission_plan import MissionPlan
|
|
28
|
+
from leafsdk.utils.logstyle import LogIcons
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class PetalMissionPlanner(Petal):
|
|
32
|
+
name = "petal-mission-planner"
|
|
33
|
+
version = "v0.1.0"
|
|
34
|
+
use_mqtt_proxy = True # Enable MQTT-aware startup
|
|
35
|
+
|
|
36
|
+
def startup(self):
|
|
37
|
+
super().startup()
|
|
38
|
+
self.running = False
|
|
39
|
+
self.mission_ready = False
|
|
40
|
+
self.mission_clock = None
|
|
41
|
+
self.mission_plan = None
|
|
42
|
+
self.subscriber_address = None # For progress updates
|
|
43
|
+
self.safe_return_waypoint_request_address = None # For safe return waypoint requests
|
|
44
|
+
self.redis_proxy: RedisProxy = self._proxies.get("redis") # Get the Redis proxy instance
|
|
45
|
+
self.mqtt_proxy: MQTTProxy = self._proxies.get("mqtt") # Get the MQTT proxy instance
|
|
46
|
+
self.mqtt_subscription_id = None
|
|
47
|
+
|
|
48
|
+
self._setup_mavlink_handlers()
|
|
49
|
+
self._init_mavlink_and_mission()
|
|
50
|
+
self.mission_executor = MissionPlanExecutor(self.mission_plan, self.mavlink_proxy, self.redis_proxy)
|
|
51
|
+
|
|
52
|
+
self.loop = asyncio.get_event_loop() # store loop reference
|
|
53
|
+
|
|
54
|
+
def _setup_mavlink_handlers(self):
|
|
55
|
+
self._mavlink_handlers = {}
|
|
56
|
+
|
|
57
|
+
def _handler_mavlink_mission_run_ack(msg: mavutil.mavlink.MAVLink_message) -> bool:
|
|
58
|
+
"""Called when the FC acknowledges mission start."""
|
|
59
|
+
if self.running:
|
|
60
|
+
logger.warning(f"{LogIcons.WARNING} Mission already running — ignoring duplicate run ACK.")
|
|
61
|
+
return True
|
|
62
|
+
elif not self.mission_ready:
|
|
63
|
+
logger.warning(f"{LogIcons.WARNING} There is no mission ready to start.")
|
|
64
|
+
return True
|
|
65
|
+
try:
|
|
66
|
+
logger.info(f"{LogIcons.SUCCESS} Received MAVLink mission run acknowledgment from flight controller.")
|
|
67
|
+
self.running = True
|
|
68
|
+
|
|
69
|
+
# Schedule on stored event loop safely, even from another thread
|
|
70
|
+
self.loop.call_soon_threadsafe(lambda: asyncio.create_task(self.mission_loop()))
|
|
71
|
+
except Exception as e:
|
|
72
|
+
logger.error(f"{LogIcons.ERROR} Failed to start mission loop: {e}, trace: {traceback.format_exc()}")
|
|
73
|
+
|
|
74
|
+
return True
|
|
75
|
+
|
|
76
|
+
def _handler_mavlink_qgc_control_cmd(msg: mavutil.mavlink.MAVLink_message) -> bool:
|
|
77
|
+
control_cmd_name = leafMAV.enums['LEAF_CONTROL_COMMAND'][msg.cmd].name
|
|
78
|
+
logger.info(f"{LogIcons.SUCCESS} Received QGC control command: {control_cmd_name}")
|
|
79
|
+
|
|
80
|
+
if control_cmd_name == "LEAF_CONTROL_PAUSE":
|
|
81
|
+
self.pause()
|
|
82
|
+
elif control_cmd_name == "LEAF_CONTROL_RESUME":
|
|
83
|
+
self.resume()
|
|
84
|
+
elif control_cmd_name == "LEAF_CONTROL_CANCEL":
|
|
85
|
+
self.cancel(action="HOVER")
|
|
86
|
+
|
|
87
|
+
return True
|
|
88
|
+
|
|
89
|
+
def _handler_mavlink_ack_mission_abort(msg: mavutil.mavlink.MAVLink_message) -> bool:
|
|
90
|
+
"""Handle abort acknowledgment."""
|
|
91
|
+
logger.info(f"{LogIcons.SUCCESS} Received MAVLink mission abort acknowledgment from flight controller.")
|
|
92
|
+
if self.running:
|
|
93
|
+
self.abort()
|
|
94
|
+
else:
|
|
95
|
+
logger.info(f"{LogIcons.RUN} No active mission — resetting mission plan.")
|
|
96
|
+
self._reset_mission()
|
|
97
|
+
|
|
98
|
+
return True
|
|
99
|
+
|
|
100
|
+
def _handler_mavlink_ack_mission_resume(msg: mavutil.mavlink.MAVLink_message) -> bool:
|
|
101
|
+
"""Handle resume acknowledgment."""
|
|
102
|
+
logger.info(f"{LogIcons.SUCCESS} Received MAVLink mission resume acknowledgment from flight controller.")
|
|
103
|
+
return True
|
|
104
|
+
|
|
105
|
+
self._mavlink_handlers[str(leafMAV.MAVLINK_MSG_ID_LEAF_ACK_MISSION_RUN)] = _handler_mavlink_mission_run_ack
|
|
106
|
+
self._mavlink_handlers[str(leafMAV.MAVLINK_MSG_ID_LEAF_QGC_CONTROL_CMD)] = _handler_mavlink_qgc_control_cmd
|
|
107
|
+
self._mavlink_handlers[str(leafMAV.MAVLINK_MSG_ID_LEAF_ACK_MISSION_ABORT)] = _handler_mavlink_ack_mission_abort
|
|
108
|
+
self._mavlink_handlers[str(leafMAV.MAVLINK_MSG_ID_LEAF_ACK_MISSION_RESUME)] = _handler_mavlink_ack_mission_resume
|
|
109
|
+
|
|
110
|
+
def _init_mavlink_and_mission(self):
|
|
111
|
+
logger.info(f"{LogIcons.RUN} Initializing MAVLink connection via proxy...")
|
|
112
|
+
# Use the external MAVLink proxy if available
|
|
113
|
+
self.mavlink_proxy: MavLinkExternalProxy = self._proxies.get("ext_mavlink")
|
|
114
|
+
self.mission_clock = MissionClock(rate_hz=50)
|
|
115
|
+
self.mission_plan = MissionPlan(name="main")
|
|
116
|
+
logger.info(f"{LogIcons.SUCCESS} MAVLink initialized and mission plan ready.")
|
|
117
|
+
|
|
118
|
+
for key, handler in self._mavlink_handlers.items():
|
|
119
|
+
self.mavlink_proxy.register_handler(
|
|
120
|
+
key=key,
|
|
121
|
+
fn=handler,
|
|
122
|
+
duplicate_filter_interval=0.7
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
async def async_startup(self):
|
|
126
|
+
"""
|
|
127
|
+
Called after startup to handle async operations like MQTT subscriptions.
|
|
128
|
+
|
|
129
|
+
Note: The MQTT-aware startup logic (organization ID monitoring, event loop setup)
|
|
130
|
+
is handled by the main application's _mqtt_aware_petal_startup function.
|
|
131
|
+
This method will be called by that function after organization ID is available.
|
|
132
|
+
"""
|
|
133
|
+
# This method is intentionally simple - the main app handles:
|
|
134
|
+
# 1. Setting self._loop
|
|
135
|
+
# 2. Waiting for organization ID
|
|
136
|
+
# 3. Calling self._setup_mqtt_topics() when ready
|
|
137
|
+
# 4. Starting organization ID monitoring if needed
|
|
138
|
+
|
|
139
|
+
logger.info("{LogIcons.RUN} Performing MQTT-aware async startup...")
|
|
140
|
+
pass
|
|
141
|
+
|
|
142
|
+
async def _setup_mqtt_topics(self):
|
|
143
|
+
logger.info(f"{LogIcons.RUN} Setting up MQTT topics...")
|
|
144
|
+
await self._mqtt_subscribe_to_mission_plan()
|
|
145
|
+
logger.info(f"{LogIcons.SUCCESS} All MQTT topics active")
|
|
146
|
+
|
|
147
|
+
async def run_mission(self, mission_graph: dict):
|
|
148
|
+
try:
|
|
149
|
+
self.mission_executor.reset()
|
|
150
|
+
self.mission_executor.load_plan(mission_graph)
|
|
151
|
+
logger.info(f"{LogIcons.RUN} Loading mission: {self.mission_plan.name}")
|
|
152
|
+
self.mission_ready = self.mission_executor.prepare()
|
|
153
|
+
|
|
154
|
+
if self.mission_ready:
|
|
155
|
+
logger.info(f"{LogIcons.SUCCESS} Mission '{self.mission_plan.name}' loaded and ready.")
|
|
156
|
+
# Send mission run message
|
|
157
|
+
msg = leafMAV.MAVLink_leaf_do_mission_run_message(
|
|
158
|
+
target_system=self.mavlink_proxy.target_system,
|
|
159
|
+
mission_id=self.mission_plan.id.encode("ascii"),
|
|
160
|
+
forced=0
|
|
161
|
+
)
|
|
162
|
+
self.mavlink_proxy.send(key='mav', msg=msg, burst_count=4, burst_interval=0.1)
|
|
163
|
+
# Wait for MAVLink ACK (non-blocking)
|
|
164
|
+
logger.info(f"{LogIcons.RUN} Sent mission run request — awaiting ACK to start execution...")
|
|
165
|
+
else:
|
|
166
|
+
logger.error(f"{LogIcons.ERROR} Mission '{self.mission_plan.name}' failed to prepare.")
|
|
167
|
+
return
|
|
168
|
+
|
|
169
|
+
except Exception as e:
|
|
170
|
+
logger.error(f"{LogIcons.ERROR} Mission run request error: {e}")
|
|
171
|
+
logger.error(traceback.format_exc())
|
|
172
|
+
|
|
173
|
+
async def _wait_for_mission_ack(self, timeout: float = 20.0):
|
|
174
|
+
start = time.monotonic()
|
|
175
|
+
while not self.running and (time.monotonic() - start < timeout):
|
|
176
|
+
await asyncio.sleep(0.1)
|
|
177
|
+
|
|
178
|
+
async def mission_loop(self):
|
|
179
|
+
"""Main mission execution loop — started when ACK is received."""
|
|
180
|
+
logger.info(f"{LogIcons.RUN} Mission loop started.")
|
|
181
|
+
try:
|
|
182
|
+
while self.running:
|
|
183
|
+
self.mission_clock.tick()
|
|
184
|
+
status = self.mission_executor.run_step()
|
|
185
|
+
self.running = status["state"] in (str(MissionState.RUNNING), str(MissionState.PAUSED))
|
|
186
|
+
|
|
187
|
+
if status["step_completed"]:
|
|
188
|
+
await self.publish_status_update(status)
|
|
189
|
+
|
|
190
|
+
await self.mission_clock.tock(blocking=False)
|
|
191
|
+
self.mission_ready = False
|
|
192
|
+
except Exception as e:
|
|
193
|
+
self.cancel(action="HOVER")
|
|
194
|
+
logger.error(f"{LogIcons.ERROR} Mission execution error: {e}, trace: {traceback.format_exc()}")
|
|
195
|
+
|
|
196
|
+
logger.info(f"{LogIcons.SUCCESS} Mission completed with final status: {status['state'].name}")
|
|
197
|
+
|
|
198
|
+
# Send mission done message when loop exits
|
|
199
|
+
msg = leafMAV.MAVLink_leaf_done_mission_run_message(
|
|
200
|
+
target_system=self.mavlink_proxy.target_system,
|
|
201
|
+
mission_id=self.mission_plan.id.encode("ascii"),
|
|
202
|
+
)
|
|
203
|
+
self.mavlink_proxy.send(key='mav', msg=msg, burst_count=4, burst_interval=0.1)
|
|
204
|
+
|
|
205
|
+
async def publish_status_update(self, status: dict):
|
|
206
|
+
if self.subscriber_address:
|
|
207
|
+
try:
|
|
208
|
+
async with httpx.AsyncClient(timeout=5.0) as client:
|
|
209
|
+
await client.post(
|
|
210
|
+
self.subscriber_address,
|
|
211
|
+
json=status,
|
|
212
|
+
headers={"Content-Type": "application/json"},
|
|
213
|
+
)
|
|
214
|
+
except Exception as e:
|
|
215
|
+
logger.warning(f"{LogIcons.WARNING} Failed to report progress: {e}")
|
|
216
|
+
|
|
217
|
+
def pause(self):
|
|
218
|
+
if self.mission_plan is None:
|
|
219
|
+
logger.warning(f"{LogIcons.WARNING} No mission plan available to pause")
|
|
220
|
+
|
|
221
|
+
try:
|
|
222
|
+
self.mission_executor.pause()
|
|
223
|
+
except Exception as e:
|
|
224
|
+
logger.error(f"{LogIcons.ERROR} Failed to pause mission: {e}, trace: {traceback.format_exc()}")
|
|
225
|
+
|
|
226
|
+
def resume(self):
|
|
227
|
+
if self.mission_plan is None:
|
|
228
|
+
logger.warning(f"{LogIcons.WARNING} No mission plan available to resume")
|
|
229
|
+
|
|
230
|
+
try:
|
|
231
|
+
self.mission_executor.resume()
|
|
232
|
+
except Exception as e:
|
|
233
|
+
logger.error(f"{LogIcons.ERROR} Failed to resume mission: {e}")
|
|
234
|
+
|
|
235
|
+
def cancel(self, action: str = "NONE"):
|
|
236
|
+
if self.mission_plan is None:
|
|
237
|
+
logger.warning(f"{LogIcons.WARNING} No mission plan available to cancel")
|
|
238
|
+
|
|
239
|
+
try:
|
|
240
|
+
self.mission_executor.cancel(action)
|
|
241
|
+
except Exception as e:
|
|
242
|
+
logger.error(f"{LogIcons.ERROR} Failed to cancel mission: {e}")
|
|
243
|
+
|
|
244
|
+
def abort(self):
|
|
245
|
+
if self.mission_plan is None:
|
|
246
|
+
logger.warning(f"{LogIcons.WARNING} No mission plan available to abort")
|
|
247
|
+
|
|
248
|
+
try:
|
|
249
|
+
self.mission_executor.abort()
|
|
250
|
+
except Exception as e:
|
|
251
|
+
logger.error(f"{LogIcons.ERROR} Failed to abort mission: {e}")
|
|
252
|
+
|
|
253
|
+
def _reset_mission(self):
|
|
254
|
+
"""Safely clear mission state."""
|
|
255
|
+
self.running = False
|
|
256
|
+
self.mission_ready = False
|
|
257
|
+
self.mission_executor.reset()
|
|
258
|
+
logger.info(f"{LogIcons.SUCCESS} Mission state reset successfully.")
|
|
259
|
+
|
|
260
|
+
@http_action(
|
|
261
|
+
method="POST",
|
|
262
|
+
path="/mission/plan",
|
|
263
|
+
description="Receives a mission graph and runs it in the background",
|
|
264
|
+
summary="Execute Mission Plan",
|
|
265
|
+
tags=["mission"]
|
|
266
|
+
)
|
|
267
|
+
async def receive_mission(self, data: MissionGraph):
|
|
268
|
+
if self.running or self.mission_ready:
|
|
269
|
+
logger.warning(f"{LogIcons.WARNING} A mission is already loaded.")
|
|
270
|
+
return {"status": f"{LogIcons.WARNING} A mission is already loaded", "error": "Mission in progress"}
|
|
271
|
+
|
|
272
|
+
mission_graph = data.model_dump(by_alias=True)
|
|
273
|
+
|
|
274
|
+
# ✅ Run in background (non-blocking)
|
|
275
|
+
asyncio.create_task(self.run_mission(mission_graph))
|
|
276
|
+
|
|
277
|
+
return {"status": "⏳ Mission is running in background"}
|
|
278
|
+
|
|
279
|
+
@http_action(
|
|
280
|
+
method="POST",
|
|
281
|
+
path="/mission/subscribe_to_progress_updates",
|
|
282
|
+
description="Receives an address where mission progress updates will be posted"
|
|
283
|
+
)
|
|
284
|
+
async def subscribe_to_progress_updates(self, data: ProgressUpdateSubscription):
|
|
285
|
+
self.subscriber_address = data.address.rstrip("/") # remove trailing slash if present
|
|
286
|
+
logger.info(f"{LogIcons.SUCCESS} Subscribed to mission progress updates at: {self.subscriber_address}")
|
|
287
|
+
return {"status": f"{LogIcons.SUCCESS} Subscribed"}
|
|
288
|
+
|
|
289
|
+
# Not used in the current implementation, but can be used to set a safe return waypoint request address
|
|
290
|
+
@http_action(
|
|
291
|
+
method="POST",
|
|
292
|
+
path="/mission/set_safe_return_plan_request_address",
|
|
293
|
+
description="Sets the address for safe return plan requests"
|
|
294
|
+
)
|
|
295
|
+
async def set_safe_return_plan_request_address(self, data: SafeReturnPlanRequestAddress):
|
|
296
|
+
self.safe_return_plan_request_address = data.address.rstrip("/") # remove trailing slash if present
|
|
297
|
+
logger.info(f"{LogIcons.SUCCESS} Set safe return plan request address to: {self.safe_return_plan_request_address}")
|
|
298
|
+
return {"status": f"{LogIcons.SUCCESS} Safe return plan request address set"}
|
|
299
|
+
|
|
300
|
+
# Not used in the current implementation, but can be used to handle safe return plan requests
|
|
301
|
+
@http_action(
|
|
302
|
+
method="GET",
|
|
303
|
+
path="/mission/safe_return_plan_request",
|
|
304
|
+
description="Receives a safe return plan request and feeds it to the mission planner"
|
|
305
|
+
)
|
|
306
|
+
async def safe_return_plan_request(self):
|
|
307
|
+
if not self.safe_return_plan_request_address:
|
|
308
|
+
logger.warning(f"{LogIcons.WARNING} No safe return plan request address set")
|
|
309
|
+
return {"status": f"{LogIcons.WARNING} No safe return plan request address set", "error": "Address not initialized"}
|
|
310
|
+
|
|
311
|
+
pass
|
|
312
|
+
|
|
313
|
+
@http_action(
|
|
314
|
+
method="POST",
|
|
315
|
+
path="/mission/pause",
|
|
316
|
+
description="Pauses the currently running mission"
|
|
317
|
+
)
|
|
318
|
+
async def pause_mission(self):
|
|
319
|
+
if self.mission_plan is None:
|
|
320
|
+
logger.warning(f"{LogIcons.WARNING} No mission plan available to pause")
|
|
321
|
+
return {"status": f"{LogIcons.WARNING} No mission plan available", "error": "Mission plan not initialized"}
|
|
322
|
+
|
|
323
|
+
try:
|
|
324
|
+
self.mission_executor.pause()
|
|
325
|
+
|
|
326
|
+
return {"status": f"{LogIcons.PAUSE} Mission pause command received successfully!"}
|
|
327
|
+
except Exception as e:
|
|
328
|
+
logger.error(f"{LogIcons.ERROR} Failed to pause mission: {e}")
|
|
329
|
+
return {"status": f"{LogIcons.ERROR} Failed to pause mission", "error": str(e)}
|
|
330
|
+
|
|
331
|
+
@http_action(
|
|
332
|
+
method="POST",
|
|
333
|
+
path="/mission/resume",
|
|
334
|
+
description="Resumes a paused mission"
|
|
335
|
+
)
|
|
336
|
+
async def resume_mission(self):
|
|
337
|
+
if self.mission_plan is None:
|
|
338
|
+
logger.warning(f"{LogIcons.WARNING} No mission plan available to resume")
|
|
339
|
+
return {"status": f"{LogIcons.WARNING} No mission plan available", "error": "Mission plan not initialized"}
|
|
340
|
+
|
|
341
|
+
try:
|
|
342
|
+
self.mission_executor.resume()
|
|
343
|
+
|
|
344
|
+
return {"status": f"{LogIcons.RESUME} Mission resume command received successfully!"}
|
|
345
|
+
except Exception as e:
|
|
346
|
+
logger.error(f"{LogIcons.ERROR} Failed to resume mission: {e}")
|
|
347
|
+
return {"status": f"{LogIcons.ERROR} Failed to resume mission", "error": str(e)}
|
|
348
|
+
|
|
349
|
+
@http_action(
|
|
350
|
+
method="POST",
|
|
351
|
+
path="/mission/cancel",
|
|
352
|
+
description="Cancels the currently running mission"
|
|
353
|
+
)
|
|
354
|
+
async def cancel_mission(self, data: CancelMissionRequest):
|
|
355
|
+
if self.mission_plan is None:
|
|
356
|
+
logger.warning(f"{LogIcons.WARNING} No mission plan available to cancel")
|
|
357
|
+
return {"status": f"{LogIcons.WARNING} No mission plan available", "error": "Mission plan not initialized"}
|
|
358
|
+
|
|
359
|
+
try:
|
|
360
|
+
self.mission_executor.cancel(data.action)
|
|
361
|
+
|
|
362
|
+
return {"status": f"{LogIcons.CANCEL} Mission cancel command received successfully!"}
|
|
363
|
+
except Exception as e:
|
|
364
|
+
logger.error(f"{LogIcons.ERROR} Failed to cancel mission: {e}")
|
|
365
|
+
return {"status": f"{LogIcons.ERROR} Failed to cancel mission", "error": str(e)}
|
|
366
|
+
|
|
367
|
+
async def _mqtt_subscribe_to_mission_plan(self):
|
|
368
|
+
if self.mqtt_proxy is None:
|
|
369
|
+
logger.warning(f"{LogIcons.WARNING} MQTT proxy not available. MQTT functionalities will be disabled.")
|
|
370
|
+
return
|
|
371
|
+
self.mqtt_subscription_id = self.mqtt_proxy.register_handler(self._mqtt_command_handler_master)
|
|
372
|
+
logger.info(f"{LogIcons.SUCCESS} registered MQTT command handler with subscription ID: {self.mqtt_subscription_id}")
|
|
373
|
+
|
|
374
|
+
async def _mqtt_command_handler_master(self, topic: str, payload: Dict[str, Any]):
|
|
375
|
+
command = payload.get('command')
|
|
376
|
+
message_id = payload.get('messageId')
|
|
377
|
+
|
|
378
|
+
if command is None or message_id is None:
|
|
379
|
+
logger.warning(f"{LogIcons.WARNING} Invalid command payload received via MQTT.")
|
|
380
|
+
return
|
|
381
|
+
|
|
382
|
+
if command == "petal-leafsdk/mission_plan":
|
|
383
|
+
data_recvd: MissionGraph = payload.get("payload")["mission_plan_json"]
|
|
384
|
+
asyncio.create_task(self._mqtt_command_handler_mission_plan(message_id, data_recvd))
|
|
385
|
+
|
|
386
|
+
async def _mqtt_command_handler_mission_plan(self, msg_id: str, data: Dict[str, Any]):
|
|
387
|
+
if self.running or self.mission_ready:
|
|
388
|
+
logger.warning(f"{LogIcons.WARNING} A mission is already loaded.")
|
|
389
|
+
await self.mqtt_proxy.publish_message({
|
|
390
|
+
'messageId': msg_id,
|
|
391
|
+
'status': 'error',
|
|
392
|
+
'error': 'A mission is already loaded'
|
|
393
|
+
})
|
|
394
|
+
return
|
|
395
|
+
|
|
396
|
+
logger.info(f"{LogIcons.SUCCESS} Received mission plan via MQTT.")
|
|
397
|
+
data_model = MissionGraph.model_validate(data, by_alias=True)
|
|
398
|
+
mission_graph = data_model.model_dump(by_alias=True)
|
|
399
|
+
|
|
400
|
+
# ✅ Run in background (non-blocking)
|
|
401
|
+
asyncio.create_task(self.run_mission(mission_graph))
|
|
402
|
+
|
|
403
|
+
# Send response
|
|
404
|
+
await self.mqtt_proxy.publish_message({
|
|
405
|
+
'messageId': msg_id,
|
|
406
|
+
'status': 'success',
|
|
407
|
+
'result': 'Command executed successfully'
|
|
408
|
+
})
|
|
409
|
+
logger.info(f"{LogIcons.SUCCESS} MQTT command response sent.")
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# leafsdk/utils/redis_helpers.py
|
|
2
|
+
# Redis helper functions for LeafSDK
|
|
3
|
+
|
|
4
|
+
from leafsdk import logger
|
|
5
|
+
from leafsdk.utils.logstyle import LogIcons
|
|
6
|
+
from petal_app_manager.proxies.redis import RedisProxy
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
def setup_redis_subscriptions(pattern: str, callback: callable, redis_proxy: Optional[RedisProxy] = None):
|
|
10
|
+
"""Setup Redis subscriptions - call this after object creation if using Redis"""
|
|
11
|
+
if redis_proxy is None:
|
|
12
|
+
logger.warning(f"{LogIcons.WARNING} Redis proxy not provided, skipping Redis subscriptions")
|
|
13
|
+
return
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
# Subscribe to a general broadcast channel
|
|
17
|
+
redis_proxy.register_pattern_channel_callback(channel=pattern, callback=callback)
|
|
18
|
+
|
|
19
|
+
logger.info(f"{LogIcons.SUCCESS} Redis subscriptions set up successfully to {pattern}.")
|
|
20
|
+
except Exception as e:
|
|
21
|
+
logger.error(f"{LogIcons.ERROR} Failed to set up Redis subscriptions: {e}")
|
|
22
|
+
|
|
23
|
+
def unsetup_redis_subscriptions(pattern: str, redis_proxy: Optional[RedisProxy] = None):
|
|
24
|
+
"""Unsubscribe from Redis channels - call this when the step is no longer needed"""
|
|
25
|
+
if redis_proxy is None:
|
|
26
|
+
logger.warning(f"{LogIcons.WARNING} Redis proxy not provided, skipping Redis unsubscriptions")
|
|
27
|
+
return
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
redis_proxy.unregister_pattern_channel_callback(channel=pattern)
|
|
31
|
+
logger.info(f"{LogIcons.SUCCESS} Redis subscriptions to {pattern} have been removed.")
|
|
32
|
+
except Exception as e:
|
|
33
|
+
logger.error(f"{LogIcons.ERROR} Failed to remove Redis subscriptions: {e}")
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: petal-leafsdk
|
|
3
|
+
Version: 0.2.4
|
|
4
|
+
Summary: SDK for mission planning
|
|
5
|
+
Author-Email: Khalil Al Handawi <khalil.alhandawi@droneleaf.io>, Suleyman Yildirim <suleyman.yildirim@droneleaf.io>
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Requires-Dist: petal-app-manager>=0.1.26
|
|
9
|
+
Requires-Dist: LeafSDK<0.3.3,>=0.3.2
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# petal-hello-world
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
petal_leafsdk-0.2.4.dist-info/METADATA,sha256=MC0dOqseZDv50HevTa0Gqr1L6E5E-StcBJAz4Ssn4z0,383
|
|
2
|
+
petal_leafsdk-0.2.4.dist-info/WHEEL,sha256=tsUv_t7BDeJeRHaSrczbGeuK-TtDpGsWi_JfpzD255I,90
|
|
3
|
+
petal_leafsdk-0.2.4.dist-info/entry_points.txt,sha256=fYAqfL1mH5znPmRSqSfY_MrjR21WhjTGLQb7ZYS2BT0,110
|
|
4
|
+
petal_leafsdk/__init__.py,sha256=dDZJMO6wrldo0ANuAjfphZUw-1f2mfep_1K6ykQZkNk,425
|
|
5
|
+
petal_leafsdk/data_model.py,sha256=ScZOK4haGqYvJPHrQZIbfqeLDUvoCUvWPDvaIg46m9w,9035
|
|
6
|
+
petal_leafsdk/mavlink_helpers.py,sha256=EUYogmf0ND_BLgL8VPeZw2rOc5H2raZqOphIhllq_hA,2755
|
|
7
|
+
petal_leafsdk/mission_plan_executor.py,sha256=KrBtoncEg3NhL_muc5BCiPleetTKVGP1qO8-Ap1ApZU,17135
|
|
8
|
+
petal_leafsdk/mission_step_executor.py,sha256=CFuMdOtgbPReOOp_7ybL6LweRh3qp8zkWlKR-v1nNSA,28205
|
|
9
|
+
petal_leafsdk/plugin.py,sha256=YUNyDS2_6QAt7bnq4Tj3DUnKSD-BcDDixXMCH1QJnEo,18412
|
|
10
|
+
petal_leafsdk/redis_helpers.py,sha256=dfloq4ltMn3LGafTPQ9TnlpoFETALtOlhbeWrtc_aP8,1550
|
|
11
|
+
petal_leafsdk-0.2.4.dist-info/RECORD,,
|