mrtautopilot 5__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.
- mrtautopilot-5/PKG-INFO +96 -0
- mrtautopilot-5/README.md +87 -0
- mrtautopilot-5/mrtautopilot/__init__.py +3 -0
- mrtautopilot-5/mrtautopilot/autopilot.py +510 -0
- mrtautopilot-5/mrtautopilot.egg-info/PKG-INFO +96 -0
- mrtautopilot-5/mrtautopilot.egg-info/SOURCES.txt +9 -0
- mrtautopilot-5/mrtautopilot.egg-info/dependency_links.txt +1 -0
- mrtautopilot-5/mrtautopilot.egg-info/requires.txt +1 -0
- mrtautopilot-5/mrtautopilot.egg-info/top_level.txt +1 -0
- mrtautopilot-5/pyproject.toml +11 -0
- mrtautopilot-5/setup.cfg +4 -0
mrtautopilot-5/PKG-INFO
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: mrtautopilot
|
|
3
|
+
Version: 5
|
|
4
|
+
Summary: Communicate with the MRT Autopilot Mavlink API
|
|
5
|
+
Maintainer-email: Nathan Knotts <nathan@magothyrt.com>
|
|
6
|
+
Requires-Python: >=3.8
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: mrtmavlink>=26.0
|
|
9
|
+
|
|
10
|
+
# MRT Autopilot Python3 API
|
|
11
|
+
|
|
12
|
+
This repository contains the Python3 API for the MRT Autopilot.
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
pip3 install git+https://github.com/magothy/mrtautopilot-py3@v5
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
Example usage:
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
#!/usr/bin/env python3
|
|
26
|
+
|
|
27
|
+
import logging
|
|
28
|
+
import selectors
|
|
29
|
+
|
|
30
|
+
import mrtautopilot
|
|
31
|
+
|
|
32
|
+
# example for send_protobuf_proxy
|
|
33
|
+
from mrtproto import Magothy_pb2
|
|
34
|
+
PROTO_ID = 0x5433 # defined by protocol
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def main():
|
|
38
|
+
mavlink = mrtautopilot.MavlinkThread()
|
|
39
|
+
|
|
40
|
+
proto = Magothy_pb2.VehicleData()
|
|
41
|
+
proto.position.latitude_deg = 38.3
|
|
42
|
+
proto.position.longitude_deg = -77.1
|
|
43
|
+
|
|
44
|
+
def got_low_bandwidth():
|
|
45
|
+
d: mrtautopilot.LowBandwidth = mavlink.low_bandwidth_queue.get_nowait()
|
|
46
|
+
logging.info(f"Got Vehicle Data: {d.latitude_deg}, {d.longitude_deg}")
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
sel = selectors.DefaultSelector()
|
|
50
|
+
sel.register(
|
|
51
|
+
mavlink.low_bandwidth_queue_fileobj,
|
|
52
|
+
selectors.EVENT_READ,
|
|
53
|
+
got_low_bandwidth,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
count = 0
|
|
57
|
+
mavlink.start()
|
|
58
|
+
while True:
|
|
59
|
+
mavlink.send_heartbeat()
|
|
60
|
+
|
|
61
|
+
count += 1
|
|
62
|
+
|
|
63
|
+
if count == 5:
|
|
64
|
+
mavlink.send_protobuf_proxy(PROTO_ID, proto.SerializeToString())
|
|
65
|
+
|
|
66
|
+
if count == 10:
|
|
67
|
+
mavlink.set_motor_enablement(True)
|
|
68
|
+
mavlink.send_waypoint(38.3, -77.1, 3.0)
|
|
69
|
+
|
|
70
|
+
if count == 20:
|
|
71
|
+
mavlink.set_motor_enablement(False)
|
|
72
|
+
mavlink.send_autopilot_stop()
|
|
73
|
+
|
|
74
|
+
for key, mask in sel.select():
|
|
75
|
+
key.data()
|
|
76
|
+
|
|
77
|
+
finally:
|
|
78
|
+
mavlink.stop()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def setup_logging():
|
|
82
|
+
logging.basicConfig(
|
|
83
|
+
format="%(asctime)s.%(msecs)03d | %(levelname)8s | %(message)s",
|
|
84
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
85
|
+
)
|
|
86
|
+
logger = logging.getLogger()
|
|
87
|
+
logger.setLevel(logging.INFO)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
if __name__ == "__main__":
|
|
91
|
+
setup_logging()
|
|
92
|
+
try:
|
|
93
|
+
main()
|
|
94
|
+
except KeyboardInterrupt:
|
|
95
|
+
pass
|
|
96
|
+
```
|
mrtautopilot-5/README.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# MRT Autopilot Python3 API
|
|
2
|
+
|
|
3
|
+
This repository contains the Python3 API for the MRT Autopilot.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
pip3 install git+https://github.com/magothy/mrtautopilot-py3@v5
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
Example usage:
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
#!/usr/bin/env python3
|
|
17
|
+
|
|
18
|
+
import logging
|
|
19
|
+
import selectors
|
|
20
|
+
|
|
21
|
+
import mrtautopilot
|
|
22
|
+
|
|
23
|
+
# example for send_protobuf_proxy
|
|
24
|
+
from mrtproto import Magothy_pb2
|
|
25
|
+
PROTO_ID = 0x5433 # defined by protocol
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def main():
|
|
29
|
+
mavlink = mrtautopilot.MavlinkThread()
|
|
30
|
+
|
|
31
|
+
proto = Magothy_pb2.VehicleData()
|
|
32
|
+
proto.position.latitude_deg = 38.3
|
|
33
|
+
proto.position.longitude_deg = -77.1
|
|
34
|
+
|
|
35
|
+
def got_low_bandwidth():
|
|
36
|
+
d: mrtautopilot.LowBandwidth = mavlink.low_bandwidth_queue.get_nowait()
|
|
37
|
+
logging.info(f"Got Vehicle Data: {d.latitude_deg}, {d.longitude_deg}")
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
sel = selectors.DefaultSelector()
|
|
41
|
+
sel.register(
|
|
42
|
+
mavlink.low_bandwidth_queue_fileobj,
|
|
43
|
+
selectors.EVENT_READ,
|
|
44
|
+
got_low_bandwidth,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
count = 0
|
|
48
|
+
mavlink.start()
|
|
49
|
+
while True:
|
|
50
|
+
mavlink.send_heartbeat()
|
|
51
|
+
|
|
52
|
+
count += 1
|
|
53
|
+
|
|
54
|
+
if count == 5:
|
|
55
|
+
mavlink.send_protobuf_proxy(PROTO_ID, proto.SerializeToString())
|
|
56
|
+
|
|
57
|
+
if count == 10:
|
|
58
|
+
mavlink.set_motor_enablement(True)
|
|
59
|
+
mavlink.send_waypoint(38.3, -77.1, 3.0)
|
|
60
|
+
|
|
61
|
+
if count == 20:
|
|
62
|
+
mavlink.set_motor_enablement(False)
|
|
63
|
+
mavlink.send_autopilot_stop()
|
|
64
|
+
|
|
65
|
+
for key, mask in sel.select():
|
|
66
|
+
key.data()
|
|
67
|
+
|
|
68
|
+
finally:
|
|
69
|
+
mavlink.stop()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def setup_logging():
|
|
73
|
+
logging.basicConfig(
|
|
74
|
+
format="%(asctime)s.%(msecs)03d | %(levelname)8s | %(message)s",
|
|
75
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
76
|
+
)
|
|
77
|
+
logger = logging.getLogger()
|
|
78
|
+
logger.setLevel(logging.INFO)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
if __name__ == "__main__":
|
|
82
|
+
setup_logging()
|
|
83
|
+
try:
|
|
84
|
+
main()
|
|
85
|
+
except KeyboardInterrupt:
|
|
86
|
+
pass
|
|
87
|
+
```
|
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import threading
|
|
3
|
+
import multiprocessing
|
|
4
|
+
import socket
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
import struct
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from typing import List, Union, Tuple
|
|
11
|
+
|
|
12
|
+
import mrtmavlink
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class HealthItem:
|
|
17
|
+
health_id: str
|
|
18
|
+
status_id: int
|
|
19
|
+
description: str
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
HEALTH_ITEMS = [
|
|
23
|
+
HealthItem(
|
|
24
|
+
health_id="health-monitor-status",
|
|
25
|
+
status_id=0x10000000,
|
|
26
|
+
description="Health Monitor is not properly configured",
|
|
27
|
+
),
|
|
28
|
+
HealthItem(
|
|
29
|
+
health_id="geo-fence", status_id=0x100000, description="Geofence is breached"
|
|
30
|
+
),
|
|
31
|
+
HealthItem(
|
|
32
|
+
health_id="mcu-timeout",
|
|
33
|
+
status_id=0x8000,
|
|
34
|
+
description="MCU Data Is Missing or Stale",
|
|
35
|
+
),
|
|
36
|
+
HealthItem(
|
|
37
|
+
health_id="gps-timeout",
|
|
38
|
+
status_id=0x20,
|
|
39
|
+
description="GPS Data Is Missing or Stale",
|
|
40
|
+
),
|
|
41
|
+
HealthItem(
|
|
42
|
+
health_id="ahrs-timeout",
|
|
43
|
+
status_id=0x200000,
|
|
44
|
+
description="AHRS Data Is Missing or Stale",
|
|
45
|
+
),
|
|
46
|
+
HealthItem(
|
|
47
|
+
health_id="depth-timeout",
|
|
48
|
+
status_id=0x400000,
|
|
49
|
+
description="Altimeter Data Is Missing or Stale",
|
|
50
|
+
),
|
|
51
|
+
HealthItem(
|
|
52
|
+
health_id="joystick-timeout",
|
|
53
|
+
status_id=0x10000,
|
|
54
|
+
description="Joystick Data Is Missing or Stale",
|
|
55
|
+
),
|
|
56
|
+
HealthItem(
|
|
57
|
+
health_id="low-fuel",
|
|
58
|
+
status_id=0x2000000,
|
|
59
|
+
description="Low Fuel or Battery",
|
|
60
|
+
),
|
|
61
|
+
HealthItem(
|
|
62
|
+
health_id="oc-timeout",
|
|
63
|
+
status_id=0x8000000,
|
|
64
|
+
description="Operator Console Communication Timeout",
|
|
65
|
+
),
|
|
66
|
+
HealthItem(
|
|
67
|
+
health_id="low-disk-space", status_id=0x1000000, description="Low Disk Space"
|
|
68
|
+
),
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class HealthResponseType(Enum):
|
|
73
|
+
Ignore = 0
|
|
74
|
+
Halt = 1
|
|
75
|
+
Loiter = 2
|
|
76
|
+
GoRally = 3
|
|
77
|
+
GoFirst = 4
|
|
78
|
+
GoLast = 5
|
|
79
|
+
GoLaunch = 6
|
|
80
|
+
Custom = 7
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass
|
|
84
|
+
class HealthResponse:
|
|
85
|
+
item: HealthItem
|
|
86
|
+
response: HealthResponseType
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class MagothyCustomMainMode(Enum):
|
|
90
|
+
MANUAL = 1
|
|
91
|
+
GUIDED = 2
|
|
92
|
+
SIMPLE = 3
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class MagothyCustomSubModeGuided(Enum):
|
|
96
|
+
UNSET = 0
|
|
97
|
+
READY = 1
|
|
98
|
+
MISSION = 2
|
|
99
|
+
LOITER = 3
|
|
100
|
+
UNHEALTHY_MISSION = 4
|
|
101
|
+
MISSION_PLANNING = 5
|
|
102
|
+
UNHEALTHY_MISSION_PLANNING = 6
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class MagothyCustomSubModeManualBitmask(Enum):
|
|
106
|
+
STABILIZED_SURGE = 0x01
|
|
107
|
+
STABILIZED_SWAY = 0x02
|
|
108
|
+
STABILIZED_DEPTH_HEAVE = 0x04
|
|
109
|
+
STABILIZED_ALTITUDE_HEAVE = 0x08
|
|
110
|
+
STABILIZED_ROLL = 0x10
|
|
111
|
+
STABILIZED_PITCH = 0x20
|
|
112
|
+
STABILIZED_YAW = 0x40
|
|
113
|
+
STABILIZED_DEGRADED = 0x80
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class AutopilotMode(Enum):
|
|
117
|
+
Unknown = 0
|
|
118
|
+
Standby = 1
|
|
119
|
+
Manual = 2
|
|
120
|
+
HealthyMission = 3
|
|
121
|
+
UnhealthyMission = 4
|
|
122
|
+
Loiter = 5
|
|
123
|
+
MissionPlanning = 6
|
|
124
|
+
UnhealthyMissionPlanning = 7
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@dataclass
|
|
128
|
+
class LowBandwidth:
|
|
129
|
+
def __init__(
|
|
130
|
+
self,
|
|
131
|
+
msg: mrtmavlink.MAVLink_magothy_low_bandwidth_message,
|
|
132
|
+
health_items: List[HealthItem],
|
|
133
|
+
):
|
|
134
|
+
self.main_mode = MagothyCustomMainMode((msg.custom_mode >> 16) & 0xFF)
|
|
135
|
+
self.sub_mode = MagothyCustomSubModeGuided((msg.custom_mode >> 24) & 0xFF)
|
|
136
|
+
|
|
137
|
+
self.autopilot_mode = AutopilotMode.Unknown
|
|
138
|
+
if self.main_mode == MagothyCustomMainMode.MANUAL:
|
|
139
|
+
self.autopilot_mode = AutopilotMode.Manual
|
|
140
|
+
elif self.main_mode == MagothyCustomMainMode.GUIDED:
|
|
141
|
+
if self.sub_mode == MagothyCustomSubModeGuided.READY:
|
|
142
|
+
self.autopilot_mode = AutopilotMode.Standby
|
|
143
|
+
elif self.sub_mode == MagothyCustomSubModeGuided.MISSION:
|
|
144
|
+
self.autopilot_mode = AutopilotMode.HealthyMission
|
|
145
|
+
elif self.sub_mode == MagothyCustomSubModeGuided.UNHEALTHY_MISSION:
|
|
146
|
+
self.autopilot_mode = AutopilotMode.UnhealthyMission
|
|
147
|
+
elif self.sub_mode == MagothyCustomSubModeGuided.LOITER:
|
|
148
|
+
self.autopilot_mode = AutopilotMode.Loiter
|
|
149
|
+
elif self.sub_mode == MagothyCustomSubModeGuided.MISSION_PLANNING:
|
|
150
|
+
self.autopilot_mode = AutopilotMode.MissionPlanning
|
|
151
|
+
elif self.sub_mode == MagothyCustomSubModeGuided.UNHEALTHY_MISSION_PLANNING:
|
|
152
|
+
self.autopilot_mode = AutopilotMode.UnhealthyMissionPlanning
|
|
153
|
+
else:
|
|
154
|
+
logging.debug(f"Unknown sub mode: {self.sub_mode}")
|
|
155
|
+
else:
|
|
156
|
+
logging.debug(f"Unknown main mode: {self.main_mode}")
|
|
157
|
+
|
|
158
|
+
self.latitude_deg = msg.lat / 1e7 if msg.lat != 0x7FFFFFFF else None
|
|
159
|
+
self.longitude_deg = msg.lon / 1e7 if msg.lon != 0x7FFFFFFF else None
|
|
160
|
+
self.battery_voltage_V = (
|
|
161
|
+
msg.voltage_battery / 1000 if msg.voltage_battery != 0xFFFF else None
|
|
162
|
+
)
|
|
163
|
+
self.battery_current_A = (
|
|
164
|
+
msg.current_battery / 100 if msg.current_battery >= 0 else None
|
|
165
|
+
)
|
|
166
|
+
self.battery_soc = msg.battery_remaining if msg.battery_remaining >= 0 else None
|
|
167
|
+
self.mission_item_seq = msg.mission_seq if msg.mission_seq != 0xFF else None
|
|
168
|
+
self.speed_mps = msg.speed / 100 if msg.speed != 0xFFFF else None
|
|
169
|
+
self.course_deg = msg.course / 100 if msg.course != 0xFFFF else None
|
|
170
|
+
self.heading_deg = msg.heading / 100 if msg.heading != 0xFFFF else None
|
|
171
|
+
self.num_satellites = msg.satellites_visible
|
|
172
|
+
self.target_speed_mps = (
|
|
173
|
+
msg.desired_speed / 100 if msg.desired_speed != 0xFFFF else None
|
|
174
|
+
)
|
|
175
|
+
self.target_course_deg = (
|
|
176
|
+
msg.desired_course / 100 if msg.desired_course != 0xFFFF else None
|
|
177
|
+
)
|
|
178
|
+
self.is_position_independent = msg.is_position_independent == 1
|
|
179
|
+
self.position_error_m = (
|
|
180
|
+
msg.position_error / 100 if msg.position_error != 0xFFFF else None
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
self.enabled_health_items = []
|
|
184
|
+
self.triggered_health_items = []
|
|
185
|
+
for item in health_items:
|
|
186
|
+
if msg.onboard_control_sensors_health & item.status_id == 0:
|
|
187
|
+
self.triggered_health_items.append(item)
|
|
188
|
+
|
|
189
|
+
self.fault_response = None
|
|
190
|
+
if self.main_mode == MagothyCustomMainMode.GUIDED and self.sub_mode in [
|
|
191
|
+
MagothyCustomSubModeGuided.UNHEALTHY_MISSION,
|
|
192
|
+
MagothyCustomSubModeGuided.UNHEALTHY_MISSION_PLANNING,
|
|
193
|
+
]:
|
|
194
|
+
details = (msg.custom_mode >> 8) & 0xFF
|
|
195
|
+
|
|
196
|
+
response_type = HealthResponseType((details >> 5) & 0x07)
|
|
197
|
+
status_id = 1 << (details & 0x1F)
|
|
198
|
+
|
|
199
|
+
for item in health_items:
|
|
200
|
+
if status_id == item.status_id:
|
|
201
|
+
self.fault_response = HealthResponse(item, response_type)
|
|
202
|
+
break
|
|
203
|
+
|
|
204
|
+
assert self.fault_response is not None
|
|
205
|
+
|
|
206
|
+
autopilot_mode: AutopilotMode
|
|
207
|
+
main_mode: MagothyCustomMainMode
|
|
208
|
+
sub_mode: MagothyCustomSubModeGuided
|
|
209
|
+
latitude_deg: Union[float, None]
|
|
210
|
+
longitude_deg: Union[float, None]
|
|
211
|
+
battery_voltage_V: Union[float, None]
|
|
212
|
+
battery_current_A: Union[float, None]
|
|
213
|
+
battery_soc: Union[int, None]
|
|
214
|
+
mission_item_seq: Union[int, None]
|
|
215
|
+
speed_mps: Union[float, None]
|
|
216
|
+
course_deg: Union[float, None]
|
|
217
|
+
heading_deg: Union[float, None]
|
|
218
|
+
num_satellites: int
|
|
219
|
+
target_speed_mps: Union[float, None]
|
|
220
|
+
target_course_deg: Union[float, None]
|
|
221
|
+
is_position_independent: bool
|
|
222
|
+
position_error_m: Union[float, None]
|
|
223
|
+
triggered_health_items: List[HealthItem]
|
|
224
|
+
fault_response: Union[HealthResponse, None]
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
class MavlinkThread:
|
|
228
|
+
def __init__(
|
|
229
|
+
self,
|
|
230
|
+
bind_address: str = "0.0.0.0",
|
|
231
|
+
remote_address: str = "127.0.0.1",
|
|
232
|
+
remote_port: int = 14551,
|
|
233
|
+
multicast_group: str = "",
|
|
234
|
+
multicast_port: int = 14550,
|
|
235
|
+
):
|
|
236
|
+
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
237
|
+
self.sock.setblocking(False)
|
|
238
|
+
|
|
239
|
+
if multicast_group:
|
|
240
|
+
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
241
|
+
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
|
|
242
|
+
self.sock.bind((bind_address, multicast_port))
|
|
243
|
+
|
|
244
|
+
mreq = struct.pack(
|
|
245
|
+
"4sl", socket.inet_aton(multicast_group), socket.INADDR_ANY
|
|
246
|
+
)
|
|
247
|
+
self.sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
|
|
248
|
+
else:
|
|
249
|
+
self.sock.bind((bind_address, 0))
|
|
250
|
+
|
|
251
|
+
self.system_id: Union[int, None] = None
|
|
252
|
+
|
|
253
|
+
self.remote_address = remote_address
|
|
254
|
+
self.remote_port = remote_port
|
|
255
|
+
self.remote_address_list = [remote_address]
|
|
256
|
+
if remote_address == "127.0.0.1":
|
|
257
|
+
self.remote_address_list.extend(
|
|
258
|
+
socket.gethostbyname_ex(socket.gethostname())[2]
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
self.is_started = False
|
|
262
|
+
self.is_started_cv = threading.Condition()
|
|
263
|
+
self.conn = mrtmavlink.MAVLink(self, 254, mrtmavlink.MAV_COMP_ID_SYSTEM_CONTROL)
|
|
264
|
+
|
|
265
|
+
self.is_done = False
|
|
266
|
+
self.thread = threading.Thread(target=self._thread, name="MrtMavlink")
|
|
267
|
+
|
|
268
|
+
self.low_bandwidth_queue: multiprocessing.Queue[LowBandwidth] = (
|
|
269
|
+
multiprocessing.Queue(maxsize=5)
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
self.low_bandwidth_queue_fileobj: int = (
|
|
273
|
+
self.low_bandwidth_queue._reader.fileno() # type: ignore
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
def write(self, data: bytes):
|
|
277
|
+
self.sock.sendto(data, (self.remote_address, self.remote_port))
|
|
278
|
+
|
|
279
|
+
def __enter__(self):
|
|
280
|
+
self.start()
|
|
281
|
+
return self
|
|
282
|
+
|
|
283
|
+
def __exit__(self, _type, _value, _traceback): # type: ignore
|
|
284
|
+
self.stop()
|
|
285
|
+
|
|
286
|
+
def start(self):
|
|
287
|
+
self.thread.start()
|
|
288
|
+
|
|
289
|
+
with self.is_started_cv:
|
|
290
|
+
while not self.is_started:
|
|
291
|
+
self.is_started_cv.wait()
|
|
292
|
+
|
|
293
|
+
return self.thread
|
|
294
|
+
|
|
295
|
+
def stop(self):
|
|
296
|
+
self.is_done = True
|
|
297
|
+
self.thread.join()
|
|
298
|
+
|
|
299
|
+
def connection_made(self, transport: asyncio.DatagramTransport):
|
|
300
|
+
pass
|
|
301
|
+
|
|
302
|
+
def datagram_received(self, data: bytes, addr: Tuple[str, int]):
|
|
303
|
+
messages = self.conn.parse_buffer(data)
|
|
304
|
+
if addr[0] not in self.remote_address_list:
|
|
305
|
+
return
|
|
306
|
+
|
|
307
|
+
if addr[1] != self.remote_port:
|
|
308
|
+
self.remote_port = addr[1]
|
|
309
|
+
logging.info(f"Setting Remote Port to {self.remote_port}")
|
|
310
|
+
|
|
311
|
+
if messages:
|
|
312
|
+
for msg in messages:
|
|
313
|
+
self._msg_callback(msg)
|
|
314
|
+
|
|
315
|
+
def _msg_callback(self, msg: mrtmavlink.MAVLink_message):
|
|
316
|
+
logging.debug(f"Received: {msg.msgname}, addr")
|
|
317
|
+
|
|
318
|
+
sys_id = msg.get_srcSystem()
|
|
319
|
+
if self.system_id != sys_id:
|
|
320
|
+
self.system_id = msg.get_srcSystem()
|
|
321
|
+
logging.info(f"Setting System ID to {sys_id}")
|
|
322
|
+
|
|
323
|
+
if msg.get_msgId() == mrtmavlink.MAVLINK_MSG_ID_MAGOTHY_LOW_BANDWIDTH:
|
|
324
|
+
data = LowBandwidth(msg, HEALTH_ITEMS) # type: ignore
|
|
325
|
+
|
|
326
|
+
if not self.low_bandwidth_queue.full():
|
|
327
|
+
self.low_bandwidth_queue.put_nowait(data)
|
|
328
|
+
|
|
329
|
+
def send_heartbeat(self):
|
|
330
|
+
self.loop.call_soon_threadsafe(lambda: self._send_heartbeat())
|
|
331
|
+
|
|
332
|
+
def _send_heartbeat(self):
|
|
333
|
+
logging.info("Sending Heartbeat")
|
|
334
|
+
self.conn.heartbeat_send(
|
|
335
|
+
mrtmavlink.MAV_TYPE_GCS, mrtmavlink.MAV_AUTOPILOT_INVALID, 0, 0, 0
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
def set_parameter(
|
|
339
|
+
self,
|
|
340
|
+
id: str,
|
|
341
|
+
value: float,
|
|
342
|
+
typ: int,
|
|
343
|
+
component: int = mrtmavlink.MAV_COMP_ID_AUTOPILOT1,
|
|
344
|
+
):
|
|
345
|
+
self.loop.call_soon_threadsafe(
|
|
346
|
+
lambda: self._set_parameter(id, value, typ, component)
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
def _set_parameter(self, id: str, value: float, typ: int, component: int):
|
|
350
|
+
assert len(id) <= 16
|
|
351
|
+
if not self.system_id:
|
|
352
|
+
logging.info("Failed to set parameter, system_id not set")
|
|
353
|
+
return
|
|
354
|
+
|
|
355
|
+
logging.info(f"Setting Param {id} to {value}")
|
|
356
|
+
|
|
357
|
+
if typ == mrtmavlink.MAV_PARAM_TYPE_UINT8:
|
|
358
|
+
val_bytes = struct.pack("BBBB", int(value), 0, 0, 0)
|
|
359
|
+
elif typ == mrtmavlink.MAV_PARAM_TYPE_INT8:
|
|
360
|
+
val_bytes = struct.pack("bBBB", int(value), 0, 0, 0)
|
|
361
|
+
elif typ == mrtmavlink.MAV_PARAM_TYPE_UINT16:
|
|
362
|
+
val_bytes = struct.pack("<HBB", int(value), 0, 0)
|
|
363
|
+
elif typ == mrtmavlink.MAV_PARAM_TYPE_INT16:
|
|
364
|
+
val_bytes = struct.pack("<hBB", int(value), 0, 0)
|
|
365
|
+
elif typ == mrtmavlink.MAV_PARAM_TYPE_UINT32:
|
|
366
|
+
val_bytes = struct.pack("<I", int(value))
|
|
367
|
+
elif typ == mrtmavlink.MAV_PARAM_TYPE_INT32:
|
|
368
|
+
val_bytes = struct.pack("<i", int(value))
|
|
369
|
+
elif typ == mrtmavlink.MAV_PARAM_TYPE_REAL32:
|
|
370
|
+
val_bytes = struct.pack("<f", value)
|
|
371
|
+
else:
|
|
372
|
+
assert False, "unsupported type"
|
|
373
|
+
|
|
374
|
+
self.conn.param_set_send(
|
|
375
|
+
self.system_id,
|
|
376
|
+
component,
|
|
377
|
+
id.encode("utf-8"),
|
|
378
|
+
struct.unpack("f", val_bytes)[0],
|
|
379
|
+
typ,
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
def set_motor_enablement(self, enable: bool):
|
|
383
|
+
logging.info(f"Setting Motor Enablement to {enable}")
|
|
384
|
+
self.set_parameter(
|
|
385
|
+
"MOTOR_ENABLE", 1 if enable else 0, mrtmavlink.MAV_PARAM_TYPE_UINT8
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
async def _run(self):
|
|
389
|
+
with self.is_started_cv:
|
|
390
|
+
self.is_started = True
|
|
391
|
+
self.is_started_cv.notify()
|
|
392
|
+
|
|
393
|
+
self.loop = asyncio.get_running_loop()
|
|
394
|
+
await self.loop.create_datagram_endpoint(lambda: self, sock=self.sock) # type: ignore
|
|
395
|
+
|
|
396
|
+
while not self.is_done:
|
|
397
|
+
logging.debug("Sending system time")
|
|
398
|
+
self._send_system_time()
|
|
399
|
+
await asyncio.sleep(1.0)
|
|
400
|
+
|
|
401
|
+
def _thread(self):
|
|
402
|
+
asyncio.run(self._run())
|
|
403
|
+
|
|
404
|
+
def _send_system_time(self):
|
|
405
|
+
self.conn.system_time_send(int(time.time() * 1e6), 0)
|
|
406
|
+
|
|
407
|
+
def send_autopilot_stop(self):
|
|
408
|
+
self.loop.call_soon_threadsafe(lambda: self._send_autopilot_stop())
|
|
409
|
+
|
|
410
|
+
def _send_autopilot_stop(self):
|
|
411
|
+
if not self.system_id:
|
|
412
|
+
logging.warn("System ID not set, not sending STOP")
|
|
413
|
+
return
|
|
414
|
+
logging.info("Sending STOP")
|
|
415
|
+
|
|
416
|
+
# custom_mode is a uint32
|
|
417
|
+
# param2 is a float
|
|
418
|
+
# convert custom_mode to a float via struct.unpack
|
|
419
|
+
custom_mode = bytearray(
|
|
420
|
+
[
|
|
421
|
+
0,
|
|
422
|
+
0,
|
|
423
|
+
MagothyCustomMainMode.GUIDED.value,
|
|
424
|
+
MagothyCustomSubModeGuided.READY.value,
|
|
425
|
+
]
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
self.conn.command_long_send(
|
|
429
|
+
self.system_id, # target_system
|
|
430
|
+
mrtmavlink.MAV_COMP_ID_AUTOPILOT1, # target_component
|
|
431
|
+
mrtmavlink.MAV_CMD_DO_SET_MODE, # command
|
|
432
|
+
0, # confirmation
|
|
433
|
+
0, # param1
|
|
434
|
+
struct.unpack("f", custom_mode)[0], # param2 (custom_mode)
|
|
435
|
+
0, # param3
|
|
436
|
+
0, # param4
|
|
437
|
+
0, # param5
|
|
438
|
+
0, # param6
|
|
439
|
+
0, # param7
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
def send_waypoint(self, lat_deg: float, lon_deg: float, speed_mps: float):
|
|
443
|
+
self.loop.call_soon_threadsafe(
|
|
444
|
+
lambda: self._send_waypoint(lat_deg, lon_deg, speed_mps)
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
def _send_waypoint(self, lat_deg: float, lon_deg: float, speed_mps: float):
|
|
448
|
+
if not self.system_id:
|
|
449
|
+
logging.warn("System ID not set, not sending waypoint")
|
|
450
|
+
return
|
|
451
|
+
logging.info(
|
|
452
|
+
f"Sending Waypoint to lat {lat_deg}°, lon {lon_deg}°, speed {speed_mps} m/s"
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
self.conn.command_long_send(
|
|
456
|
+
self.system_id, # target_system
|
|
457
|
+
mrtmavlink.MAV_COMP_ID_AUTOPILOT1, # target_component
|
|
458
|
+
mrtmavlink.MAV_CMD_WAYPOINT_USER_1, # command
|
|
459
|
+
0, # confirmation
|
|
460
|
+
0, # param1
|
|
461
|
+
speed_mps, # param2 (speed_mps)
|
|
462
|
+
0, # param3
|
|
463
|
+
0, # param4
|
|
464
|
+
lat_deg * 1e7, # param5 (latitude_deg * 1e7)
|
|
465
|
+
lon_deg * 1e7, # param6 (longitude_deg * 1e7)
|
|
466
|
+
0, # param7
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
def send_loiter(self, speed_mps: float, radius_m: float, duration_s: float):
|
|
470
|
+
self.loop.call_soon_threadsafe(
|
|
471
|
+
lambda: self._send_loiter(speed_mps, radius_m, duration_s)
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
def _send_loiter(self, speed_mps: float, radius_m: float, duration_s: float):
|
|
475
|
+
if not self.system_id:
|
|
476
|
+
logging.warn("System ID not set, not sending loiter")
|
|
477
|
+
return
|
|
478
|
+
logging.info(
|
|
479
|
+
f"Sending Loiter: speed {speed_mps} m/s, radius {radius_m} m, duration {duration_s} s"
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
self.conn.command_long_send(
|
|
483
|
+
self.system_id, # target_system
|
|
484
|
+
mrtmavlink.MAV_COMP_ID_AUTOPILOT1, # target_component
|
|
485
|
+
mrtmavlink.MAV_CMD_WAYPOINT_USER_4, # command
|
|
486
|
+
0, # confirmation
|
|
487
|
+
duration_s, # param1 - duration (seconds)
|
|
488
|
+
speed_mps, # param2 - speed (m/s)
|
|
489
|
+
radius_m, # param3 - radiums (m)
|
|
490
|
+
0, # param4 - unused
|
|
491
|
+
float("nan"), # param5 (latitude_deg * 1e7)
|
|
492
|
+
float("nan"), # param6 (longitude_deg * 1e7)
|
|
493
|
+
0, # param7 - unused
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
def send_protobuf_proxy(self, proto_id: int, buf: bytes):
|
|
497
|
+
self.loop.call_soon_threadsafe(lambda: self._send_protobuf_proxy(proto_id, buf))
|
|
498
|
+
|
|
499
|
+
def _send_protobuf_proxy(self, proto_id: int, buf: bytes):
|
|
500
|
+
MAX_BUF_LEN = 251
|
|
501
|
+
buf_len = len(buf)
|
|
502
|
+
|
|
503
|
+
assert buf_len <= MAX_BUF_LEN
|
|
504
|
+
logging.info(f"Sending Protobuf Proxy with ID {proto_id} and len {buf_len}")
|
|
505
|
+
|
|
506
|
+
pad_len = MAX_BUF_LEN - buf_len
|
|
507
|
+
buf += b"\0" * pad_len
|
|
508
|
+
assert len(buf) == MAX_BUF_LEN
|
|
509
|
+
|
|
510
|
+
self.conn.magothy_protobuf_proxy_send(proto_id, False, buf_len, buf)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: mrtautopilot
|
|
3
|
+
Version: 5
|
|
4
|
+
Summary: Communicate with the MRT Autopilot Mavlink API
|
|
5
|
+
Maintainer-email: Nathan Knotts <nathan@magothyrt.com>
|
|
6
|
+
Requires-Python: >=3.8
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: mrtmavlink>=26.0
|
|
9
|
+
|
|
10
|
+
# MRT Autopilot Python3 API
|
|
11
|
+
|
|
12
|
+
This repository contains the Python3 API for the MRT Autopilot.
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
pip3 install git+https://github.com/magothy/mrtautopilot-py3@v5
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
Example usage:
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
#!/usr/bin/env python3
|
|
26
|
+
|
|
27
|
+
import logging
|
|
28
|
+
import selectors
|
|
29
|
+
|
|
30
|
+
import mrtautopilot
|
|
31
|
+
|
|
32
|
+
# example for send_protobuf_proxy
|
|
33
|
+
from mrtproto import Magothy_pb2
|
|
34
|
+
PROTO_ID = 0x5433 # defined by protocol
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def main():
|
|
38
|
+
mavlink = mrtautopilot.MavlinkThread()
|
|
39
|
+
|
|
40
|
+
proto = Magothy_pb2.VehicleData()
|
|
41
|
+
proto.position.latitude_deg = 38.3
|
|
42
|
+
proto.position.longitude_deg = -77.1
|
|
43
|
+
|
|
44
|
+
def got_low_bandwidth():
|
|
45
|
+
d: mrtautopilot.LowBandwidth = mavlink.low_bandwidth_queue.get_nowait()
|
|
46
|
+
logging.info(f"Got Vehicle Data: {d.latitude_deg}, {d.longitude_deg}")
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
sel = selectors.DefaultSelector()
|
|
50
|
+
sel.register(
|
|
51
|
+
mavlink.low_bandwidth_queue_fileobj,
|
|
52
|
+
selectors.EVENT_READ,
|
|
53
|
+
got_low_bandwidth,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
count = 0
|
|
57
|
+
mavlink.start()
|
|
58
|
+
while True:
|
|
59
|
+
mavlink.send_heartbeat()
|
|
60
|
+
|
|
61
|
+
count += 1
|
|
62
|
+
|
|
63
|
+
if count == 5:
|
|
64
|
+
mavlink.send_protobuf_proxy(PROTO_ID, proto.SerializeToString())
|
|
65
|
+
|
|
66
|
+
if count == 10:
|
|
67
|
+
mavlink.set_motor_enablement(True)
|
|
68
|
+
mavlink.send_waypoint(38.3, -77.1, 3.0)
|
|
69
|
+
|
|
70
|
+
if count == 20:
|
|
71
|
+
mavlink.set_motor_enablement(False)
|
|
72
|
+
mavlink.send_autopilot_stop()
|
|
73
|
+
|
|
74
|
+
for key, mask in sel.select():
|
|
75
|
+
key.data()
|
|
76
|
+
|
|
77
|
+
finally:
|
|
78
|
+
mavlink.stop()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def setup_logging():
|
|
82
|
+
logging.basicConfig(
|
|
83
|
+
format="%(asctime)s.%(msecs)03d | %(levelname)8s | %(message)s",
|
|
84
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
85
|
+
)
|
|
86
|
+
logger = logging.getLogger()
|
|
87
|
+
logger.setLevel(logging.INFO)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
if __name__ == "__main__":
|
|
91
|
+
setup_logging()
|
|
92
|
+
try:
|
|
93
|
+
main()
|
|
94
|
+
except KeyboardInterrupt:
|
|
95
|
+
pass
|
|
96
|
+
```
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
mrtautopilot/__init__.py
|
|
4
|
+
mrtautopilot/autopilot.py
|
|
5
|
+
mrtautopilot.egg-info/PKG-INFO
|
|
6
|
+
mrtautopilot.egg-info/SOURCES.txt
|
|
7
|
+
mrtautopilot.egg-info/dependency_links.txt
|
|
8
|
+
mrtautopilot.egg-info/requires.txt
|
|
9
|
+
mrtautopilot.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
mrtmavlink>=26.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
mrtautopilot
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "mrtautopilot"
|
|
3
|
+
description = "Communicate with the MRT Autopilot Mavlink API"
|
|
4
|
+
dynamic = ["version"]
|
|
5
|
+
requires-python = ">= 3.8"
|
|
6
|
+
dependencies = ["mrtmavlink >= 26.0"]
|
|
7
|
+
maintainers = [{ name = "Nathan Knotts", email = "nathan@magothyrt.com" }]
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
|
|
10
|
+
[tool.setuptools.dynamic]
|
|
11
|
+
version = { attr = "mrtautopilot.__version__" }
|
mrtautopilot-5/setup.cfg
ADDED