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,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)}")
|