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.
@@ -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,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,3 @@
1
+ __version__ = "5"
2
+
3
+ from .autopilot import *
@@ -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
+ 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__" }
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+