easylink-mavlink 0.1.0__tar.gz
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.
- easylink_mavlink-0.1.0/PKG-INFO +56 -0
- easylink_mavlink-0.1.0/README.md +40 -0
- easylink_mavlink-0.1.0/easylink/__init__.py +26 -0
- easylink_mavlink-0.1.0/easylink/actions.py +219 -0
- easylink_mavlink-0.1.0/easylink/camera.py +71 -0
- easylink_mavlink-0.1.0/easylink/connection.py +91 -0
- easylink_mavlink-0.1.0/easylink/drone.py +211 -0
- easylink_mavlink-0.1.0/easylink/events.py +22 -0
- easylink_mavlink-0.1.0/easylink/exceptions.py +23 -0
- easylink_mavlink-0.1.0/easylink/firmware.py +76 -0
- easylink_mavlink-0.1.0/easylink/geofence.py +61 -0
- easylink_mavlink-0.1.0/easylink/mission.py +212 -0
- easylink_mavlink-0.1.0/easylink/navigation.py +100 -0
- easylink_mavlink-0.1.0/easylink/parameters.py +89 -0
- easylink_mavlink-0.1.0/easylink/safety.py +53 -0
- easylink_mavlink-0.1.0/easylink/telemetry.py +237 -0
- easylink_mavlink-0.1.0/easylink_mavlink.egg-info/PKG-INFO +56 -0
- easylink_mavlink-0.1.0/easylink_mavlink.egg-info/SOURCES.txt +21 -0
- easylink_mavlink-0.1.0/easylink_mavlink.egg-info/dependency_links.txt +1 -0
- easylink_mavlink-0.1.0/easylink_mavlink.egg-info/requires.txt +4 -0
- easylink_mavlink-0.1.0/easylink_mavlink.egg-info/top_level.txt +1 -0
- easylink_mavlink-0.1.0/pyproject.toml +30 -0
- easylink_mavlink-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: easylink-mavlink
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Lightweight, zero-friction Python library for MAVLink drone control (ArduPilot & PX4)
|
|
5
|
+
Author: EASYLink Contributors
|
|
6
|
+
Project-URL: Homepage, https://github.com/easylink/easylink
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Topic :: Scientific/Engineering
|
|
11
|
+
Requires-Python: >=3.8
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Requires-Dist: pymavlink>=2.4.41
|
|
14
|
+
Provides-Extra: dev
|
|
15
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
16
|
+
|
|
17
|
+
# PY-EASYLink (`easylink-mavlink`)
|
|
18
|
+
|
|
19
|
+
Lightweight, zero-friction Python library for MAVLink drone control (supporting both ArduPilot and PX4).
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install easylink-mavlink
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Quickstart
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from easylink import Drone
|
|
31
|
+
|
|
32
|
+
# Auto-discovers SITL simulator or hardware serial port
|
|
33
|
+
drone = Drone()
|
|
34
|
+
drone.connect()
|
|
35
|
+
|
|
36
|
+
drone.arm()
|
|
37
|
+
drone.takeoff(10.0)
|
|
38
|
+
|
|
39
|
+
pos = drone.position
|
|
40
|
+
print(f"Current Position: Lat={pos.lat}, Lon={pos.lon}, Alt={pos.alt}m")
|
|
41
|
+
|
|
42
|
+
drone.hover(5.0)
|
|
43
|
+
drone.home()
|
|
44
|
+
drone.disconnect()
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Features
|
|
48
|
+
|
|
49
|
+
- **Auto-Discovery**: Probes local SITL ports (`udp:127.0.0.1:14550`, etc.) and serial ports (`/dev/ttyAMA0`, `COM3`, etc.).
|
|
50
|
+
- **Dual Firmware Support**: Auto-detects ArduPilot vs PX4 from heartbeats.
|
|
51
|
+
- **Packet Dispatcher**: Solves packet-stealing and threading collisions cleanly.
|
|
52
|
+
- **Fluent Mission Builder**: `drone.mission.clear().add_takeoff(15).add_waypoint(lat, lon, alt).add_land().upload()`
|
|
53
|
+
|
|
54
|
+
## License
|
|
55
|
+
|
|
56
|
+
MIT
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# PY-EASYLink (`easylink-mavlink`)
|
|
2
|
+
|
|
3
|
+
Lightweight, zero-friction Python library for MAVLink drone control (supporting both ArduPilot and PX4).
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install easylink-mavlink
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quickstart
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from easylink import Drone
|
|
15
|
+
|
|
16
|
+
# Auto-discovers SITL simulator or hardware serial port
|
|
17
|
+
drone = Drone()
|
|
18
|
+
drone.connect()
|
|
19
|
+
|
|
20
|
+
drone.arm()
|
|
21
|
+
drone.takeoff(10.0)
|
|
22
|
+
|
|
23
|
+
pos = drone.position
|
|
24
|
+
print(f"Current Position: Lat={pos.lat}, Lon={pos.lon}, Alt={pos.alt}m")
|
|
25
|
+
|
|
26
|
+
drone.hover(5.0)
|
|
27
|
+
drone.home()
|
|
28
|
+
drone.disconnect()
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Features
|
|
32
|
+
|
|
33
|
+
- **Auto-Discovery**: Probes local SITL ports (`udp:127.0.0.1:14550`, etc.) and serial ports (`/dev/ttyAMA0`, `COM3`, etc.).
|
|
34
|
+
- **Dual Firmware Support**: Auto-detects ArduPilot vs PX4 from heartbeats.
|
|
35
|
+
- **Packet Dispatcher**: Solves packet-stealing and threading collisions cleanly.
|
|
36
|
+
- **Fluent Mission Builder**: `drone.mission.clear().add_takeoff(15).add_waypoint(lat, lon, alt).add_land().upload()`
|
|
37
|
+
|
|
38
|
+
## License
|
|
39
|
+
|
|
40
|
+
MIT
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from .drone import Drone
|
|
2
|
+
from .mission import MissionManager
|
|
3
|
+
from .telemetry import Position, Attitude, Battery, GPSInfo
|
|
4
|
+
from .exceptions import (
|
|
5
|
+
EASYLinkError,
|
|
6
|
+
ConnectionError,
|
|
7
|
+
ArmingError,
|
|
8
|
+
CommandTimeoutError,
|
|
9
|
+
ModeError,
|
|
10
|
+
PreFlightCheckError
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
'Drone',
|
|
15
|
+
'MissionManager',
|
|
16
|
+
'Position',
|
|
17
|
+
'Attitude',
|
|
18
|
+
'Battery',
|
|
19
|
+
'GPSInfo',
|
|
20
|
+
'EASYLinkError',
|
|
21
|
+
'ConnectionError',
|
|
22
|
+
'ArmingError',
|
|
23
|
+
'CommandTimeoutError',
|
|
24
|
+
'ModeError',
|
|
25
|
+
'PreFlightCheckError'
|
|
26
|
+
]
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from pymavlink import mavutil
|
|
3
|
+
from .exceptions import CommandTimeoutError, ArmingError
|
|
4
|
+
|
|
5
|
+
class ActionManager:
|
|
6
|
+
"""Handles vehicle actions such as arming, disarming, takeoff, landing, RTL, and kill."""
|
|
7
|
+
|
|
8
|
+
def __init__(self, connection_mgr, adapter_mgr):
|
|
9
|
+
self.conn_mgr = connection_mgr
|
|
10
|
+
self.adapter_mgr = adapter_mgr
|
|
11
|
+
|
|
12
|
+
@property
|
|
13
|
+
def master(self):
|
|
14
|
+
return self.conn_mgr.master
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def dispatcher(self):
|
|
18
|
+
drone_inst = getattr(self.conn_mgr, 'drone', None)
|
|
19
|
+
return drone_inst._telemetry if drone_inst else None
|
|
20
|
+
|
|
21
|
+
def arm(self, timeout: float = 15.0):
|
|
22
|
+
"""Arms vehicle motors. Swaps to STABILIZE mode first for pre-arm integrity checks, then guided mode."""
|
|
23
|
+
master = self.master
|
|
24
|
+
is_px4 = self.conn_mgr.autopilot_type == mavutil.mavlink.MAV_AUTOPILOT_PX4
|
|
25
|
+
|
|
26
|
+
# 1. Switch to initial arm-friendly mode
|
|
27
|
+
stabilize_mode = 'ALT_HOLD' if is_px4 else 'STABILIZE'
|
|
28
|
+
if master.mode_mapping() and stabilize_mode in master.mode_mapping():
|
|
29
|
+
mode_id = master.mode_mapping()[stabilize_mode]
|
|
30
|
+
master.mav.command_long_send(
|
|
31
|
+
self.conn_mgr.target_system,
|
|
32
|
+
self.conn_mgr.target_component,
|
|
33
|
+
mavutil.mavlink.MAV_CMD_DO_SET_MODE,
|
|
34
|
+
0,
|
|
35
|
+
mavutil.mavlink.MAV_MODE_FLAG_CUSTOM_MODE_ENABLED,
|
|
36
|
+
mode_id, 0, 0, 0, 0, 0
|
|
37
|
+
)
|
|
38
|
+
time.sleep(0.3)
|
|
39
|
+
|
|
40
|
+
# 2. Send Arm command
|
|
41
|
+
master.mav.command_long_send(
|
|
42
|
+
self.conn_mgr.target_system,
|
|
43
|
+
self.conn_mgr.target_component,
|
|
44
|
+
mavutil.mavlink.MAV_CMD_COMPONENT_ARM_DISARM,
|
|
45
|
+
0,
|
|
46
|
+
1, # 1 to arm
|
|
47
|
+
0, 0, 0, 0, 0, 0
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# 3. Await armed HEARTBEAT via dispatcher queue
|
|
51
|
+
disp = self.dispatcher
|
|
52
|
+
armed_ok = False
|
|
53
|
+
if disp:
|
|
54
|
+
msg = disp.wait_for_message(
|
|
55
|
+
'HEARTBEAT',
|
|
56
|
+
timeout=timeout,
|
|
57
|
+
filter_fn=lambda m: bool(m.base_mode & mavutil.mavlink.MAV_MODE_FLAG_SAFETY_ARMED)
|
|
58
|
+
)
|
|
59
|
+
if msg:
|
|
60
|
+
armed_ok = True
|
|
61
|
+
else:
|
|
62
|
+
start_time = time.time()
|
|
63
|
+
while time.time() - start_time < timeout:
|
|
64
|
+
msg = master.recv_match(type='HEARTBEAT', blocking=True, timeout=0.5)
|
|
65
|
+
if msg and (msg.base_mode & mavutil.mavlink.MAV_MODE_FLAG_SAFETY_ARMED):
|
|
66
|
+
armed_ok = True
|
|
67
|
+
break
|
|
68
|
+
|
|
69
|
+
if not armed_ok:
|
|
70
|
+
raise ArmingError("Arming request timed out or was rejected by the flight controller.")
|
|
71
|
+
|
|
72
|
+
# 4. Transition to GUIDED/OFFBOARD for navigation commands
|
|
73
|
+
time.sleep(0.3)
|
|
74
|
+
self.adapter_mgr.set_guided_mode(master)
|
|
75
|
+
return True
|
|
76
|
+
|
|
77
|
+
def disarm(self, timeout: float = 15.0):
|
|
78
|
+
"""Disarms vehicle motors safely."""
|
|
79
|
+
self.master.mav.command_long_send(
|
|
80
|
+
self.conn_mgr.target_system,
|
|
81
|
+
self.conn_mgr.target_component,
|
|
82
|
+
mavutil.mavlink.MAV_CMD_COMPONENT_ARM_DISARM,
|
|
83
|
+
0,
|
|
84
|
+
0, # 0 to disarm
|
|
85
|
+
0, 0, 0, 0, 0, 0
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
disp = self.dispatcher
|
|
89
|
+
if disp:
|
|
90
|
+
msg = disp.wait_for_message(
|
|
91
|
+
'HEARTBEAT',
|
|
92
|
+
timeout=timeout,
|
|
93
|
+
filter_fn=lambda m: not bool(m.base_mode & mavutil.mavlink.MAV_MODE_FLAG_SAFETY_ARMED)
|
|
94
|
+
)
|
|
95
|
+
if msg:
|
|
96
|
+
return True
|
|
97
|
+
else:
|
|
98
|
+
start_time = time.time()
|
|
99
|
+
while time.time() - start_time < timeout:
|
|
100
|
+
msg = self.master.recv_match(type='HEARTBEAT', blocking=True, timeout=0.5)
|
|
101
|
+
if msg and not (msg.base_mode & mavutil.mavlink.MAV_MODE_FLAG_SAFETY_ARMED):
|
|
102
|
+
return True
|
|
103
|
+
raise ArmingError("Disarming request timed out or was rejected by the flight controller.")
|
|
104
|
+
|
|
105
|
+
def takeoff(self, altitude: float, timeout: float = 30.0):
|
|
106
|
+
"""Commands drone takeoff to specified relative altitude in meters."""
|
|
107
|
+
self.adapter_mgr.set_guided_mode(self.master)
|
|
108
|
+
time.sleep(0.2)
|
|
109
|
+
|
|
110
|
+
self.master.mav.command_long_send(
|
|
111
|
+
self.conn_mgr.target_system,
|
|
112
|
+
self.conn_mgr.target_component,
|
|
113
|
+
mavutil.mavlink.MAV_CMD_NAV_TAKEOFF,
|
|
114
|
+
0,
|
|
115
|
+
0, 0, 0, 0, 0, 0,
|
|
116
|
+
altitude
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
disp = self.dispatcher
|
|
120
|
+
if disp:
|
|
121
|
+
ack = disp.wait_for_message(
|
|
122
|
+
'COMMAND_ACK',
|
|
123
|
+
timeout=3.0,
|
|
124
|
+
filter_fn=lambda m: getattr(m, 'command', None) == mavutil.mavlink.MAV_CMD_NAV_TAKEOFF
|
|
125
|
+
)
|
|
126
|
+
if ack and ack.result in [mavutil.mavlink.MAV_RESULT_TEMPORARILY_REJECTED, mavutil.mavlink.MAV_RESULT_DENIED]:
|
|
127
|
+
raise CommandTimeoutError(f"Takeoff command rejected by vehicle: result {ack.result}")
|
|
128
|
+
|
|
129
|
+
# Block until target altitude is reached (within 90% threshold)
|
|
130
|
+
start_time = time.time()
|
|
131
|
+
while time.time() - start_time < timeout:
|
|
132
|
+
if disp:
|
|
133
|
+
current_alt = disp.position.alt
|
|
134
|
+
if current_alt >= altitude * 0.90:
|
|
135
|
+
return True
|
|
136
|
+
time.sleep(0.2)
|
|
137
|
+
else:
|
|
138
|
+
msg = self.master.recv_match(type='GLOBAL_POSITION_INT', blocking=True, timeout=0.5)
|
|
139
|
+
if msg and (msg.relative_alt / 1000.0) >= altitude * 0.90:
|
|
140
|
+
return True
|
|
141
|
+
raise CommandTimeoutError("Takeoff command issued, but target altitude was not reached within timeout.")
|
|
142
|
+
|
|
143
|
+
def land(self, blocking: bool = True, timeout: float = 60.0):
|
|
144
|
+
"""Commands landing at current position. If blocking=True, waits until motors disarm on ground."""
|
|
145
|
+
self.master.mav.command_long_send(
|
|
146
|
+
self.conn_mgr.target_system,
|
|
147
|
+
self.conn_mgr.target_component,
|
|
148
|
+
mavutil.mavlink.MAV_CMD_NAV_LAND,
|
|
149
|
+
0,
|
|
150
|
+
0, 0, 0, 0, 0, 0, 0
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
if not blocking:
|
|
154
|
+
return True
|
|
155
|
+
|
|
156
|
+
# Blocking wait: Watch for armed state to drop (disarm on ground)
|
|
157
|
+
disp = self.dispatcher
|
|
158
|
+
start_time = time.time()
|
|
159
|
+
while time.time() - start_time < timeout:
|
|
160
|
+
if disp:
|
|
161
|
+
if not disp.is_armed:
|
|
162
|
+
return True
|
|
163
|
+
time.sleep(0.5)
|
|
164
|
+
else:
|
|
165
|
+
msg = self.master.recv_match(type='HEARTBEAT', blocking=True, timeout=0.5)
|
|
166
|
+
if msg and not bool(msg.base_mode & mavutil.mavlink.MAV_MODE_FLAG_SAFETY_ARMED):
|
|
167
|
+
return True
|
|
168
|
+
return True
|
|
169
|
+
|
|
170
|
+
def rtl(self, blocking: bool = True, timeout: float = 90.0):
|
|
171
|
+
"""Triggers Return-To-Launch (RTL). If blocking=True, waits until vehicle lands & disarms."""
|
|
172
|
+
if self.master.mode_mapping() and 'RTL' in self.master.mode_mapping():
|
|
173
|
+
mode_id = self.master.mode_mapping()['RTL']
|
|
174
|
+
self.master.mav.command_long_send(
|
|
175
|
+
self.conn_mgr.target_system,
|
|
176
|
+
self.conn_mgr.target_component,
|
|
177
|
+
mavutil.mavlink.MAV_CMD_DO_SET_MODE,
|
|
178
|
+
0,
|
|
179
|
+
mavutil.mavlink.MAV_MODE_FLAG_CUSTOM_MODE_ENABLED,
|
|
180
|
+
mode_id, 0, 0, 0, 0, 0
|
|
181
|
+
)
|
|
182
|
+
else:
|
|
183
|
+
self.master.mav.command_long_send(
|
|
184
|
+
self.conn_mgr.target_system,
|
|
185
|
+
self.conn_mgr.target_component,
|
|
186
|
+
mavutil.mavlink.MAV_CMD_NAV_RETURN_TO_LAUNCH,
|
|
187
|
+
0,
|
|
188
|
+
0, 0, 0, 0, 0, 0, 0
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
if not blocking:
|
|
192
|
+
return True
|
|
193
|
+
|
|
194
|
+
# Blocking wait: Watch for armed state to drop (vehicle returned, landed, and disarmed)
|
|
195
|
+
disp = self.dispatcher
|
|
196
|
+
start_time = time.time()
|
|
197
|
+
while time.time() - start_time < timeout:
|
|
198
|
+
if disp:
|
|
199
|
+
if not disp.is_armed:
|
|
200
|
+
return True
|
|
201
|
+
time.sleep(0.5)
|
|
202
|
+
else:
|
|
203
|
+
msg = self.master.recv_match(type='HEARTBEAT', blocking=True, timeout=0.5)
|
|
204
|
+
if msg and not bool(msg.base_mode & mavutil.mavlink.MAV_MODE_FLAG_SAFETY_ARMED):
|
|
205
|
+
return True
|
|
206
|
+
return True
|
|
207
|
+
|
|
208
|
+
def kill(self):
|
|
209
|
+
"""Emergency motor kill (disarms immediately regardless of flight state)."""
|
|
210
|
+
self.master.mav.command_long_send(
|
|
211
|
+
self.conn_mgr.target_system,
|
|
212
|
+
self.conn_mgr.target_component,
|
|
213
|
+
mavutil.mavlink.MAV_CMD_COMPONENT_ARM_DISARM,
|
|
214
|
+
0,
|
|
215
|
+
0, # Disarm
|
|
216
|
+
21196, # Magic number to force kill
|
|
217
|
+
0, 0, 0, 0, 0
|
|
218
|
+
)
|
|
219
|
+
return True
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from pymavlink import mavutil
|
|
2
|
+
|
|
3
|
+
class CameraManager:
|
|
4
|
+
"""Manages simple camera commands via MAVLink."""
|
|
5
|
+
|
|
6
|
+
def __init__(self, connection_mgr):
|
|
7
|
+
self.conn_mgr = connection_mgr
|
|
8
|
+
|
|
9
|
+
@property
|
|
10
|
+
def master(self):
|
|
11
|
+
return self.conn_mgr.master
|
|
12
|
+
|
|
13
|
+
def take_photo(self):
|
|
14
|
+
"""Sends command to trigger single camera photo capture."""
|
|
15
|
+
self.master.mav.command_long_send(
|
|
16
|
+
self.conn_mgr.target_system,
|
|
17
|
+
self.conn_mgr.target_component,
|
|
18
|
+
mavutil.mavlink.MAV_CMD_IMAGE_START_CAPTURE,
|
|
19
|
+
0,
|
|
20
|
+
0, # Reserved (0)
|
|
21
|
+
0, # Interval (0 for single image)
|
|
22
|
+
1, # Total number of images to capture (1)
|
|
23
|
+
0, 0, 0, 0
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
def start_video(self):
|
|
27
|
+
"""Sends command to start video capture."""
|
|
28
|
+
self.master.mav.command_long_send(
|
|
29
|
+
self.conn_mgr.target_system,
|
|
30
|
+
self.conn_mgr.target_component,
|
|
31
|
+
mavutil.mavlink.MAV_CMD_VIDEO_START_CAPTURE,
|
|
32
|
+
0,
|
|
33
|
+
0, # Camera ID
|
|
34
|
+
0, 0, 0, 0, 0, 0
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
def stop_video(self):
|
|
38
|
+
"""Sends command to stop video capture."""
|
|
39
|
+
self.master.mav.command_long_send(
|
|
40
|
+
self.conn_mgr.target_system,
|
|
41
|
+
self.conn_mgr.target_component,
|
|
42
|
+
mavutil.mavlink.MAV_CMD_VIDEO_STOP_CAPTURE,
|
|
43
|
+
0,
|
|
44
|
+
0, # Camera ID
|
|
45
|
+
0, 0, 0, 0, 0, 0
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class GimbalManager:
|
|
50
|
+
"""Manages camera gimbal angle controls."""
|
|
51
|
+
|
|
52
|
+
def __init__(self, connection_mgr):
|
|
53
|
+
self.conn_mgr = connection_mgr
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def master(self):
|
|
57
|
+
return self.conn_mgr.master
|
|
58
|
+
|
|
59
|
+
def set_pitch_yaw(self, pitch: float, yaw: float):
|
|
60
|
+
"""Sets pitch and yaw angles of camera gimbal mount."""
|
|
61
|
+
self.master.mav.command_long_send(
|
|
62
|
+
self.conn_mgr.target_system,
|
|
63
|
+
self.conn_mgr.target_component,
|
|
64
|
+
mavutil.mavlink.MAV_CMD_DO_MOUNT_CONTROL,
|
|
65
|
+
0,
|
|
66
|
+
pitch, # Pitch angle in degrees
|
|
67
|
+
0, # Roll (0)
|
|
68
|
+
yaw, # Yaw angle in degrees
|
|
69
|
+
0, 0, 0,
|
|
70
|
+
mavutil.mavlink.MAV_MOUNT_MODE_MAVLINK_TARGETING # Mount mode
|
|
71
|
+
)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import time
|
|
3
|
+
import logging
|
|
4
|
+
from pymavlink import mavutil
|
|
5
|
+
from .exceptions import ConnectionError
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
class ConnectionManager:
|
|
10
|
+
"""Manages connection establishment, port scanning/auto-discovery, and heartbeat listening."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, target: str = None):
|
|
13
|
+
self.target = target
|
|
14
|
+
self.drone = None
|
|
15
|
+
self.master = None
|
|
16
|
+
self.target_system = 1
|
|
17
|
+
self.target_component = 1
|
|
18
|
+
self.autopilot_type = None
|
|
19
|
+
self.is_connected = False
|
|
20
|
+
|
|
21
|
+
def _get_candidates(self):
|
|
22
|
+
"""Generates list of possible connection targets when no target is specified."""
|
|
23
|
+
if self.target:
|
|
24
|
+
return [self.target]
|
|
25
|
+
|
|
26
|
+
candidates = []
|
|
27
|
+
|
|
28
|
+
# 1. Simulators (SITL) defaults - high priority
|
|
29
|
+
candidates.append("udp:127.0.0.1:14550")
|
|
30
|
+
candidates.append("udp:127.0.0.1:14551")
|
|
31
|
+
candidates.append("tcp:127.0.0.1:5760")
|
|
32
|
+
candidates.append("tcp:127.0.0.1:5762")
|
|
33
|
+
|
|
34
|
+
# 2. Hardware Serial Port scanning
|
|
35
|
+
if sys.platform.startswith("win"):
|
|
36
|
+
# Scan common active Windows COM ports (prioritize COM3-COM8, COM1, COM2)
|
|
37
|
+
for i in [3, 4, 5, 6, 7, 8, 1, 2, 9, 10, 11, 12]:
|
|
38
|
+
candidates.append(f"COM{i}:57600")
|
|
39
|
+
candidates.append(f"COM{i}:115200")
|
|
40
|
+
else:
|
|
41
|
+
# Linux / macOS / Raspberry Pi
|
|
42
|
+
serial_devs = [
|
|
43
|
+
"/dev/ttyUSB0", "/dev/ttyUSB1",
|
|
44
|
+
"/dev/ttyACM0", "/dev/ttyACM1",
|
|
45
|
+
"/dev/ttyAMA0", "/dev/ttyAMA1",
|
|
46
|
+
"/dev/ttyS0"
|
|
47
|
+
]
|
|
48
|
+
for dev in serial_devs:
|
|
49
|
+
candidates.append(f"{dev}:57600")
|
|
50
|
+
candidates.append(f"{dev}:115200")
|
|
51
|
+
|
|
52
|
+
return candidates
|
|
53
|
+
|
|
54
|
+
def connect(self, timeout: float = 15.0):
|
|
55
|
+
candidates = self._get_candidates()
|
|
56
|
+
|
|
57
|
+
for candidate in candidates:
|
|
58
|
+
try:
|
|
59
|
+
# Attempt to open connection
|
|
60
|
+
self.master = mavutil.mavlink_connection(candidate, timeout=0.5)
|
|
61
|
+
|
|
62
|
+
# Check for heartbeat to confirm active vehicle
|
|
63
|
+
start_time = time.time()
|
|
64
|
+
probe_timeout = timeout if self.target else 0.5
|
|
65
|
+
|
|
66
|
+
while time.time() - start_time < probe_timeout:
|
|
67
|
+
msg = self.master.recv_match(type='HEARTBEAT', blocking=True, timeout=0.3)
|
|
68
|
+
if msg:
|
|
69
|
+
self.target_system = self.master.target_system
|
|
70
|
+
self.target_component = self.master.target_component
|
|
71
|
+
self.autopilot_type = msg.autopilot
|
|
72
|
+
self.is_connected = True
|
|
73
|
+
return
|
|
74
|
+
except Exception:
|
|
75
|
+
if self.master:
|
|
76
|
+
try:
|
|
77
|
+
self.master.close()
|
|
78
|
+
except Exception:
|
|
79
|
+
pass
|
|
80
|
+
self.master = None
|
|
81
|
+
|
|
82
|
+
raise ConnectionError("Failed to establish MAVLink connection. No active vehicle responded to probes.")
|
|
83
|
+
|
|
84
|
+
def disconnect(self):
|
|
85
|
+
if self.master:
|
|
86
|
+
try:
|
|
87
|
+
self.master.close()
|
|
88
|
+
except Exception:
|
|
89
|
+
pass
|
|
90
|
+
self.is_connected = False
|
|
91
|
+
self.master = None
|