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,583 @@
1
+ # petal_leafsdk/mission_step_executor.py
2
+
3
+ import numpy as np
4
+ import time
5
+ import json
6
+ import traceback
7
+ import uuid
8
+ from typing import Dict, Any, List, Optional, Tuple, Union, Sequence, Literal, Type
9
+ from abc import ABC, abstractmethod
10
+ from dataclasses import dataclass
11
+
12
+ from pymavlink import mavutil
13
+ from pymavlink.dialects.v20 import droneleaf_mav_msgs as leafMAV
14
+
15
+ from petal_app_manager.proxies.redis import RedisProxy
16
+ from petal_app_manager.proxies.external import MavLinkExternalProxy
17
+
18
+ from petal_leafsdk.redis_helpers import setup_redis_subscriptions, unsetup_redis_subscriptions
19
+ from petal_leafsdk.mavlink_helpers import setup_mavlink_subscriptions, unsetup_mavlink_subscriptions
20
+
21
+ from leafsdk.core.mission.trajectory import WaypointTrajectory #, TrajectorySampler
22
+ from leafsdk import logger
23
+ from leafsdk.utils.logstyle import LogIcons
24
+ from leafsdk.core.mission.mission_step import (
25
+ Tuple3D, MissionStep, _GotoBase, GotoGPSWaypoint, GotoLocalPosition,
26
+ YawAbsolute, GotoRelative, YawRelative, Takeoff, Wait, Land
27
+ )
28
+
29
+
30
+ @dataclass
31
+ class StepState:
32
+ completed: bool = False
33
+ paused: bool = False
34
+ canceled: bool = False
35
+ exec_count: int = 0
36
+
37
+ def reset(self):
38
+ self.completed = False
39
+ self.paused = False
40
+ self.canceled = False
41
+ self.exec_count = 0
42
+
43
+
44
+ @dataclass
45
+ class StepMemory:
46
+ yaw_offset: float = 0.0
47
+ waypoint_offset: Tuple3D = (0.0, 0.0, 0.0)
48
+
49
+
50
+ class MissionStepExecutor(ABC):
51
+ def __init__(self, step: MissionStep):
52
+ self.state = StepState() # Holds the current state of the step
53
+ self.setpoint_offset = StepMemory()
54
+ self.step: MissionStep = step
55
+ self.output = True # Indicates the logical output of the step (mostly used for conditional steps)
56
+
57
+ def handler_setpoint(msg: mavutil.mavlink.MAVLink_message) -> bool:
58
+ self.setpoint_offset = StepMemory(yaw_offset=msg.yaw, waypoint_offset=(msg.x, msg.y, msg.z))
59
+ logger.debug(f"{LogIcons.SUCCESS} Received setpoint position: {self.setpoint_offset}")
60
+ return True
61
+
62
+ self._handler_setpoint = handler_setpoint
63
+
64
+ def reset(self):
65
+ """Reset the executor state for re-execution."""
66
+ self.state.reset()
67
+
68
+ def first_exec(self) -> bool:
69
+ """Returns True if this is the first execution of the step logic."""
70
+ return self.state.exec_count == 0
71
+
72
+ def description(self) -> str:
73
+ """Returns a string description of the step for logging purposes."""
74
+ return self.step.description()
75
+
76
+ def is_pausable(self) -> bool:
77
+ """Returns True if the step is pausable."""
78
+ return self.step.is_pausable()
79
+
80
+ def is_cancelable(self) -> bool:
81
+ """Returns True if the step can be cancelled."""
82
+ return self.step.is_cancelable()
83
+
84
+ def log_info(self):
85
+ """Log information about the step."""
86
+ self.step.log_info()
87
+
88
+ @abstractmethod
89
+ def execute_step_logic(self, mav_proxy: MavLinkExternalProxy = None, redis_proxy: RedisProxy = None):
90
+ """Execute the logic for the mission step - this is called repeatedly until the step is completed."""
91
+ pass
92
+
93
+ def setup(self, mav_proxy: MavLinkExternalProxy = None, redis_proxy: RedisProxy = None):
94
+ """Setup any resources needed for the step prior to mission plan execution."""
95
+ self.reset()
96
+
97
+ def start(self, mav_proxy: MavLinkExternalProxy = None, redis_proxy: RedisProxy = None):
98
+ """Execute one time operations at the start of the step."""
99
+ pass
100
+
101
+ def terminate(self, mav_proxy: MavLinkExternalProxy = None, redis_proxy: RedisProxy = None):
102
+ """Execute one time operations at the end of the step."""
103
+ pass
104
+
105
+ def execute_step(self, mav_proxy: MavLinkExternalProxy = None, redis_proxy: RedisProxy = None) -> Tuple[bool, bool]:
106
+ # Check cancellation before executing
107
+ if self.state.canceled or self.state.paused:
108
+ return self.output, self.state.completed
109
+
110
+ if self.first_exec():
111
+ self.start(mav_proxy=mav_proxy, redis_proxy=redis_proxy)
112
+ self.log_info()
113
+ self.state.exec_count += 1
114
+ elif not self.state.completed:
115
+ self.execute_step_logic(mav_proxy=mav_proxy, redis_proxy=redis_proxy)
116
+ self.state.exec_count += 1
117
+ if self.state.completed:
118
+ self.terminate(mav_proxy=mav_proxy, redis_proxy=redis_proxy)
119
+ logger.info(f"{LogIcons.SUCCESS} Done: {self.description()} completed!")
120
+
121
+ return self.output, self.state.completed
122
+
123
+ def pause(self, mission_id, mav_proxy: MavLinkExternalProxy = None, redis_proxy: RedisProxy = None):
124
+ """Pause the step if it is pausable."""
125
+ if self.is_pausable() and not self.state.canceled:
126
+ self.state.paused = True
127
+ self.stop_trajectory(redis_proxy)
128
+ logger.info(f"{LogIcons.PAUSE} Step paused: {self.description()}")
129
+ else:
130
+ logger.warning(f"{LogIcons.WARNING} Step cannot be paused: {self.description()}")
131
+
132
+ def stop_trajectory(self, redis_proxy):
133
+ redis_proxy.publish(
134
+ channel="/traj_sys/clear_queue_and_abort_current_trajectory_ori",
135
+ message=json.dumps({"payload": 1}) #TODO this number is used in FC as boolean not int, better to pass True or false
136
+ )
137
+ redis_proxy.publish(
138
+ channel="/traj_sys/generate_stop_traj_on_xyz_plane_from_states_by_deceleration",
139
+ message=json.dumps({"payload": self.step.average_deceleration})
140
+ )
141
+
142
+ def resume(self, mav_proxy: MavLinkExternalProxy = None, redis_proxy: RedisProxy = None):
143
+ """Resume the step if it was paused."""
144
+ if self.state.paused and not self.state.canceled:
145
+ self.state.paused = False
146
+ logger.info(f"{LogIcons.RUN} Step resumed: {self.description()}")
147
+ else:
148
+ logger.warning(f"{LogIcons.WARNING} Step is not paused or has been canceled, cannot resume: {self.description()}")
149
+
150
+ def cancel(self, mav_proxy: MavLinkExternalProxy = None, redis_proxy: RedisProxy = None):
151
+ """Cancel the step."""
152
+ self.state.canceled = True
153
+ self.terminate(mav_proxy=mav_proxy, redis_proxy=redis_proxy)
154
+ logger.info(f"{LogIcons.CANCEL} Step canceled: {self.description()}")
155
+
156
+
157
+ class _GotoExecutor(MissionStepExecutor):
158
+ def __init__(self, step: MissionStep):
159
+ super().__init__(step=step)
160
+
161
+ # ---- Internal state ----
162
+ self.yaw_offset = 0.0 # Default yaw offset
163
+ self.waypoint_offset = [0.0, 0.0, 0.0] # Default position offset
164
+ self.trajectory = None
165
+ self.uuid_str = str(uuid.uuid4())
166
+ self.queued_traj_ids: Dict[str, bool] = {} # trajectories waiting for completion, value indicates if completed
167
+ self.current_traj_segment: int = 0 # index of segment being processed
168
+
169
+ def _handle_notify_trajectory_completed(self, channel: str, message: str) -> None:
170
+ """
171
+ Handle notification messages for trajectory completion.
172
+ This function is triggered asynchronously by the Redis subscriber.
173
+
174
+ It parses the message, extracts the trajectory_id, and stores it
175
+ in an internal queue or list for later processing. This function
176
+ must not block.
177
+
178
+ Parameters
179
+ ----------
180
+ channel : str
181
+ The Redis channel from which the message was received.
182
+ message : str
183
+ The message content, expected to be a JSON string with trajectory details.
184
+ """
185
+ logger.info(f"{LogIcons.SUCCESS} Received notification on {channel}: {message}")
186
+
187
+ try:
188
+ command_data = json.loads(message)
189
+ traj_id = command_data.get("trajectory_id")
190
+
191
+ if traj_id:
192
+ self.queued_traj_ids[traj_id] = True
193
+ logger.info(f"{LogIcons.SUCCESS} Trajectory completed: {traj_id}")
194
+ else:
195
+ logger.warning(f"{LogIcons.WARNING} Received notification without trajectory_id: {message}")
196
+ except Exception as e:
197
+ logger.error(f"{LogIcons.ERROR} Error parsing completion notification: {e}")
198
+
199
+ def start(self, mav_proxy: MavLinkExternalProxy = None, redis_proxy: RedisProxy = None):
200
+ setup_mavlink_subscriptions(
201
+ key=str(leafMAV.MAVLINK_MSG_ID_LEAF_SETPOINT_OFFSET),
202
+ callback=self._handler_setpoint,
203
+ duplicate_filter_interval=0.7,
204
+ mav_proxy=mav_proxy
205
+ )
206
+ setup_redis_subscriptions(pattern="/petal-leafsdk/notify_trajectory_completed", callback=self._handle_notify_trajectory_completed, redis_proxy=redis_proxy)
207
+ try:
208
+ logger.info(f"{LogIcons.WARNING} setpoint data: {self.__class__.__name__}: {self.setpoint_offset}")
209
+
210
+ # Compute trajectory once
211
+ self.pos_traj_ids, self.pos_traj_json, self.yaw_traj_ids, self.yaw_traj_json = self._compute_trajectory(
212
+ self.step.waypoints,
213
+ self.step.yaws_deg,
214
+ self.step.speed,
215
+ self.step.yaw_speed,
216
+ home=self.setpoint_offset.waypoint_offset,
217
+ home_yaw=self.setpoint_offset.yaw_offset,
218
+ cartesian=self.step.cartesian,
219
+ )
220
+
221
+ except Exception as e:
222
+ logger.error(f"{LogIcons.ERROR} Error computing trajectory: {e}")
223
+ logger.error(traceback.format_exc())
224
+ raise e
225
+
226
+ def execute_step_logic(self, mav_proxy: MavLinkExternalProxy = None, redis_proxy: RedisProxy = None):
227
+ """
228
+ Execute mission step logic for sequential trajectory publishing.
229
+ This function is designed to be called periodically (non-blocking).
230
+ """
231
+ total_segments = len(self.pos_traj_json)
232
+
233
+ # If there are no uncompleted trajectories, publish next segment
234
+ if all(list(self.queued_traj_ids.values())) and self.current_traj_segment < total_segments:
235
+ self._publish_trajectory_segment(
236
+ idx=self.current_traj_segment,
237
+ pos_traj_seg=self.pos_traj_json[self.current_traj_segment],
238
+ yaw_traj_seg=self.yaw_traj_json[self.current_traj_segment],
239
+ pos_traj_id=self.pos_traj_ids[self.current_traj_segment],
240
+ yaw_traj_id=self.yaw_traj_ids[self.current_traj_segment],
241
+ redis_proxy=redis_proxy
242
+ )
243
+ self.current_traj_segment += 1
244
+ elif all(list(self.queued_traj_ids.values())) and self.current_traj_segment >= total_segments:
245
+ self.state.completed = True
246
+
247
+ def terminate(self, mav_proxy: MavLinkExternalProxy = None, redis_proxy: RedisProxy = None):
248
+ unsetup_mavlink_subscriptions(
249
+ key=str(leafMAV.MAVLINK_MSG_ID_LEAF_SETPOINT_OFFSET),
250
+ callback=self._handler_setpoint,
251
+ mav_proxy=mav_proxy
252
+ )
253
+ unsetup_redis_subscriptions(pattern="/petal-leafsdk/notify_trajectory_completed", redis_proxy=redis_proxy)
254
+
255
+ def _compute_trajectory( # TODO make all parameters either degree or radians
256
+ self,
257
+ waypoints: Sequence[Tuple[float, float, float]],
258
+ yaws_deg: Sequence[float],
259
+ speed: Sequence[float],
260
+ yaw_speed: Union[Sequence[float], Literal["sync"]],
261
+ home: Tuple[float, float, float],
262
+ home_yaw: float,
263
+ cartesian: bool,
264
+ is_yaw_relative: Optional[bool] = False,
265
+ ) -> Tuple[List[str], List[Optional[str]], List[str], List[Optional[str]]]:
266
+ """
267
+ Compute the trajectory for the given waypoints and yaws.
268
+ This function generates trajectory JSON files for each segment
269
+ and returns their identifiers.
270
+
271
+ Parameters
272
+ ----------
273
+ waypoints : Sequence[Tuple[float, float, float]]
274
+ List of waypoints as (lat, lon, alt) or (x, y, z).
275
+ yaws_deg : Sequence[float]
276
+ List of yaw commands in degrees at each waypoint.
277
+ speed : Sequence[float]
278
+ List of speeds (m/s) for each segment.
279
+ yaw_speed : Sequence[float] or str
280
+ List of yaw speeds (deg/s) for each segment, or 'sync' to match position trajectory.
281
+ home : Tuple[float, float, float]
282
+ Home position reference (lat, lon, alt) or (x, y, z).
283
+ home_yaw : float
284
+ Home yaw reference in radians.
285
+ cartesian : bool
286
+ If True, waypoints are in Cartesian coordinates; if False, GPS coordinates.
287
+ is_yaw_relative : bool
288
+ If True, interpret yaws as relative changes; otherwise absolute.
289
+
290
+ Returns
291
+ -------
292
+ pos_traj_ids : List[str]
293
+ List of position trajectory segment identifiers.
294
+ pos_traj_json : List[Optional[str]]
295
+ List of position trajectory JSON strings (None if static).
296
+ yaw_traj_ids : List[str]
297
+ List of yaw trajectory segment identifiers.
298
+ yaw_traj_json : List[Optional[str]]
299
+ List of yaw trajectory JSON strings (None if static).
300
+ """
301
+
302
+ # Create trajectory json files for each segment based on the waypoints and yaws
303
+ self.trajectory = WaypointTrajectory(
304
+ waypoints=waypoints,
305
+ yaws_deg=yaws_deg,
306
+ speed=speed,
307
+ yaw_speed=yaw_speed,
308
+ home=home,
309
+ home_yaw=home_yaw,
310
+ cartesian=cartesian,
311
+ is_yaw_relative=is_yaw_relative
312
+ )
313
+
314
+ pos_traj_ids, pos_traj_json = self.trajectory.build_pos_polynomial_trajectory_json(self.uuid_str)
315
+ yaw_traj_ids, yaw_traj_json = self.trajectory.build_yaw_polynomial_trajectory_json(self.uuid_str)
316
+
317
+ return pos_traj_ids, pos_traj_json, yaw_traj_ids, yaw_traj_json
318
+
319
+ def _publish_trajectory_segment(
320
+ self,
321
+ idx: int,
322
+ pos_traj_seg: str,
323
+ yaw_traj_seg: str,
324
+ pos_traj_id: str,
325
+ yaw_traj_id: str,
326
+ redis_proxy: RedisProxy = None
327
+ ) -> None:
328
+ """
329
+ Publish a single trajectory segment (position and/or yaw) to Redis.
330
+
331
+ This function does not block or wait for completion. Completion is
332
+ handled asynchronously via `_handle_notify_trajectory_completed`.
333
+
334
+ Parameters
335
+ ----------
336
+ idx : int
337
+ Segment index (0-based).
338
+ pos_traj_seg : str or None
339
+ JSON string for the position trajectory segment, or None if static.
340
+ yaw_traj_seg : str or None
341
+ JSON string for the yaw trajectory segment, or None if static.
342
+ pos_traj_id : str or None
343
+ Identifier for the position trajectory segment.
344
+ yaw_traj_id : str or None
345
+ Identifier for the yaw trajectory segment.
346
+ """
347
+ try:
348
+ if redis_proxy is None:
349
+ logger.warning(f"{LogIcons.WARNING} Redis proxy not available, skipping trajectory publication")
350
+ return
351
+
352
+ # Skip publishing if both are None
353
+ if pos_traj_seg is None and yaw_traj_seg is None:
354
+ logger.warning(
355
+ f"{LogIcons.WARNING} Both position and yaw trajectory segments are static, "
356
+ f"skipping publication for segment {idx+1}"
357
+ )
358
+ return
359
+
360
+ # Publish position trajectory
361
+ if pos_traj_seg is not None:
362
+ redis_proxy.publish(
363
+ channel="/traj_sys/queue_traj_primitive_pos",
364
+ message=pos_traj_seg,
365
+ )
366
+ self.queued_traj_ids[pos_traj_id] = False
367
+ logger.info(f"{LogIcons.SUCCESS} Position trajectory segment {idx+1} published to Redis successfully")
368
+
369
+ # Publish yaw trajectory
370
+ if yaw_traj_seg is not None:
371
+ redis_proxy.publish(
372
+ channel="/traj_sys/queue_traj_primitive_ori",
373
+ message=yaw_traj_seg,
374
+ )
375
+ self.queued_traj_ids[yaw_traj_id] = False
376
+ logger.info(f"{LogIcons.SUCCESS} Yaw trajectory segment {idx+1} published to Redis successfully")
377
+
378
+ except Exception as e:
379
+ logger.error(f"{LogIcons.ERROR} Error publishing trajectory segment {idx+1}: {e}")
380
+
381
+
382
+ class GotoGPSWaypointExecutor(_GotoExecutor):
383
+ def __init__(self, step: GotoGPSWaypoint):
384
+ super().__init__(step=step)
385
+
386
+
387
+ class GotoLocalPositionExecutor(_GotoExecutor):
388
+ def __init__(self, step: GotoLocalPosition):
389
+ super().__init__(step=step)
390
+
391
+
392
+ class GotoRelativeExecutor(_GotoExecutor):
393
+ def __init__(self, step: GotoRelative):
394
+ super().__init__(step=step)
395
+
396
+ def start(self, mav_proxy: MavLinkExternalProxy = None, redis_proxy: RedisProxy = None):
397
+ setup_redis_subscriptions(pattern="/petal-leafsdk/notify_trajectory_completed", callback=self._handle_notify_trajectory_completed, redis_proxy=redis_proxy)
398
+ try:
399
+ # Cumulative sum the relative points to get absolute waypoints
400
+ waypoints = np.cumsum(self.step.waypoints, axis=0) + self.setpoint_offset.waypoint_offset
401
+
402
+ logger.debug(f"{LogIcons.WARNING} Using waypoint offset: {self.setpoint_offset.waypoint_offset} and yaw offset: {self.setpoint_offset.yaw_offset}")
403
+
404
+ # Compute trajectory once
405
+ self.pos_traj_ids, self.pos_traj_json, self.yaw_traj_ids, self.yaw_traj_json = self._compute_trajectory(
406
+ waypoints,
407
+ self.step.yaws_deg,
408
+ self.step.speed,
409
+ self.step.yaw_speed,
410
+ home=self.setpoint_offset.waypoint_offset,
411
+ home_yaw=self.setpoint_offset.yaw_offset,
412
+ cartesian=self.step.cartesian,
413
+ is_yaw_relative=True,
414
+ )
415
+
416
+ except Exception as e:
417
+ logger.error(f"{LogIcons.ERROR} Error computing trajectory: {e}")
418
+ logger.error(traceback.format_exc())
419
+ raise e
420
+
421
+
422
+ class YawAbsoluteExecutor(_GotoExecutor):
423
+ def __init__(self, step: YawAbsolute):
424
+ super().__init__(step=step)
425
+
426
+ def start(self, mav_proxy: MavLinkExternalProxy = None, redis_proxy: RedisProxy = None):
427
+ setup_redis_subscriptions(pattern="/petal-leafsdk/notify_trajectory_completed", callback=self._handle_notify_trajectory_completed, redis_proxy=redis_proxy)
428
+ try:
429
+ logger.debug(f"{LogIcons.WARNING} Using waypoint offset: {self.setpoint_offset.waypoint_offset} and yaw offset: {self.setpoint_offset.yaw_offset}")
430
+
431
+ waypoints = self.step.waypoints + np.asarray(self.setpoint_offset.waypoint_offset)
432
+
433
+ # Compute trajectory once
434
+ self.pos_traj_ids, self.pos_traj_json, self.yaw_traj_ids, self.yaw_traj_json = self._compute_trajectory(
435
+ waypoints,
436
+ self.step.yaws_deg,
437
+ self.step.speed,
438
+ self.step.yaw_speed,
439
+ home=self.setpoint_offset.waypoint_offset,
440
+ home_yaw=self.setpoint_offset.yaw_offset,
441
+ cartesian=self.step.cartesian,
442
+ )
443
+
444
+ except Exception as e:
445
+ logger.error(f"{LogIcons.ERROR} Error computing trajectory: {e}")
446
+ logger.error(traceback.format_exc())
447
+ raise e
448
+
449
+
450
+ class YawRelativeExecutor(_GotoExecutor):
451
+ def __init__(self, step: YawRelative):
452
+ super().__init__(step=step)
453
+
454
+ def start(self, mav_proxy: MavLinkExternalProxy = None, redis_proxy: RedisProxy = None):
455
+ setup_redis_subscriptions(pattern="/petal-leafsdk/notify_trajectory_completed", callback=self._handle_notify_trajectory_completed, redis_proxy=redis_proxy)
456
+ try:
457
+ logger.debug(f"{LogIcons.WARNING} Using waypoint offset: {self.setpoint_offset.waypoint_offset} and yaw offset: {self.setpoint_offset.yaw_offset}")
458
+
459
+ waypoints = self.step.waypoints + np.asarray(self.setpoint_offset.waypoint_offset)
460
+ # Compute trajectory once
461
+ self.pos_traj_ids, self.pos_traj_json, self.yaw_traj_ids, self.yaw_traj_json = self._compute_trajectory(
462
+ waypoints,
463
+ self.step.yaws_deg,
464
+ self.step.speed,
465
+ self.step.yaw_speed,
466
+ home=self.setpoint_offset.waypoint_offset,
467
+ home_yaw=self.setpoint_offset.yaw_offset,
468
+ cartesian=self.step.cartesian,
469
+ is_yaw_relative=True
470
+ )
471
+
472
+ except Exception as e:
473
+ logger.error(f"{LogIcons.ERROR} Error computing trajectory: {e}")
474
+ logger.error(traceback.format_exc())
475
+ raise e
476
+
477
+
478
+ class TakeoffExecutor(MissionStepExecutor):
479
+ def __init__(self, step: Takeoff):
480
+ super().__init__(step=step)
481
+
482
+ def setup(self, mav_proxy: MavLinkExternalProxy = None, redis_proxy: RedisProxy = None):
483
+ super().setup(mav_proxy=mav_proxy, redis_proxy=redis_proxy)
484
+
485
+ def start(self, mav_proxy: MavLinkExternalProxy = None, redis_proxy: RedisProxy = None):
486
+ if mav_proxy is not None:
487
+ msg = leafMAV.MAVLink_leaf_do_takeoff_message(
488
+ target_system=mav_proxy.target_system,
489
+ altitude=self.step.alt
490
+ )
491
+ mav_proxy.send(key='mav', msg=msg, burst_count=4, burst_interval=0.1)
492
+ else:
493
+ logger.warning(f"{LogIcons.WARNING} MavLinkExternalProxy is not provided, cannot send takeoff message.")
494
+
495
+ def execute_step_logic(self, mav_proxy: MavLinkExternalProxy = None, redis_proxy: RedisProxy = None) -> None:
496
+ logger.info(f"{LogIcons.SUCCESS} setpoint data: {self.__class__.__name__}: {self.setpoint_offset}")
497
+ logger.info(f"{LogIcons.WARNING} Takeoff with waypoint offset: {self.setpoint_offset.waypoint_offset} and yaw offset: {self.setpoint_offset.yaw_offset}")
498
+ self.state.completed = True
499
+
500
+
501
+ class WaitExecutor(MissionStepExecutor):
502
+ def __init__(self, step: Wait):
503
+ super().__init__(step=step)
504
+
505
+ def start(self, mav_proxy: MavLinkExternalProxy = None, redis_proxy: RedisProxy = None):
506
+ self.tick = time.time()
507
+
508
+ def execute_step_logic(self, mav_proxy: MavLinkExternalProxy = None, redis_proxy: RedisProxy = None):
509
+ elapsed_time = time.time() - self.tick
510
+ if elapsed_time >= self.step.duration:
511
+ self.state.completed = True
512
+
513
+
514
+ class LandExecutor(MissionStepExecutor):
515
+ def __init__(self, step: Land):
516
+ super().__init__(step=step)
517
+ self._landed = False
518
+
519
+ def _handle_notify_trajectory_completed(self, channel: str, message: str) -> None:
520
+ """
521
+ Handle notification messages for landing trajectory completion.
522
+
523
+ Parameters
524
+ ----------
525
+ channel : str
526
+ The Redis channel from which the message was received.
527
+ message : str
528
+ The message content, expected to be a JSON string with trajectory details.
529
+ """
530
+ logger.info(f"{LogIcons.SUCCESS} Received notification on {channel}: {message}")
531
+
532
+ try:
533
+ command_data = json.loads(message)
534
+ traj_id = command_data.get("trajectory_id")
535
+
536
+ if "land" in traj_id:
537
+ # For Land step, we can directly mark completed on any trajectory completion
538
+ self._landed = True
539
+ logger.info(f"{LogIcons.SUCCESS} Trajectory completed: {traj_id}")
540
+ except Exception as e:
541
+ logger.error(f"{LogIcons.ERROR} Error parsing completion notification: {e}")
542
+
543
+ def start(self, mav_proxy: MavLinkExternalProxy = None, redis_proxy: RedisProxy = None):
544
+ super().start(mav_proxy=mav_proxy, redis_proxy=redis_proxy)
545
+ setup_redis_subscriptions(pattern="/petal-leafsdk/notify_trajectory_completed", callback=self._handle_notify_trajectory_completed, redis_proxy=redis_proxy)
546
+ if mav_proxy is not None:
547
+ msg = leafMAV.MAVLink_leaf_do_land_message(
548
+ target_system=mav_proxy.target_system,
549
+ )
550
+ mav_proxy.send(key='mav', msg=msg, burst_count=4, burst_interval=0.1)
551
+ else:
552
+ logger.warning(f"{LogIcons.WARNING} MavLinkExternalProxy is not provided, cannot send land message.")
553
+
554
+ def execute_step_logic(self, mav_proxy: MavLinkExternalProxy = None, redis_proxy: RedisProxy = None) -> None:
555
+ if self._landed:
556
+ self.state.completed = True
557
+
558
+ def terminate(self, mav_proxy: MavLinkExternalProxy = None, redis_proxy: RedisProxy = None):
559
+ super().terminate(mav_proxy=mav_proxy, redis_proxy=redis_proxy)
560
+ unsetup_redis_subscriptions(pattern="/petal-leafsdk/notify_trajectory_completed", redis_proxy=redis_proxy)
561
+ self._landed = False
562
+
563
+
564
+ def get_mission_step_executor(step: MissionStep) -> MissionStepExecutor:
565
+ """Factory method to get the appropriate MissionStepExecutor for a given MissionStep."""
566
+ if isinstance(step, GotoGPSWaypoint):
567
+ return GotoGPSWaypointExecutor(step=step)
568
+ elif isinstance(step, GotoLocalPosition):
569
+ return GotoLocalPositionExecutor(step=step)
570
+ elif isinstance(step, GotoRelative):
571
+ return GotoRelativeExecutor(step=step)
572
+ elif isinstance(step, YawAbsolute):
573
+ return YawAbsoluteExecutor(step=step)
574
+ elif isinstance(step, YawRelative):
575
+ return YawRelativeExecutor(step=step)
576
+ elif isinstance(step, Takeoff):
577
+ return TakeoffExecutor(step=step)
578
+ elif isinstance(step, Wait):
579
+ return WaitExecutor(step=step)
580
+ elif isinstance(step, Land):
581
+ return LandExecutor(step=step)
582
+ else:
583
+ raise ValueError(f"Unsupported MissionStep type: {type(step)}")