dronecot 2.3.7__tar.gz → 2.3.8__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.
- {dronecot-2.3.7 → dronecot-2.3.8}/CHANGELOG.md +7 -0
- {dronecot-2.3.7/dronecot.egg-info → dronecot-2.3.8}/PKG-INFO +1 -1
- {dronecot-2.3.7 → dronecot-2.3.8/dronecot.egg-info}/PKG-INFO +1 -1
- {dronecot-2.3.7 → dronecot-2.3.8}/dronecot.egg-info/SOURCES.txt +1 -0
- dronecot-2.3.8/src/dronecot/VERSION +1 -0
- {dronecot-2.3.7 → dronecot-2.3.8}/src/dronecot/classes.py +56 -0
- dronecot-2.3.8/tests/test_serial_crlf.py +95 -0
- dronecot-2.3.7/src/dronecot/VERSION +0 -1
- {dronecot-2.3.7 → dronecot-2.3.8}/LICENSE +0 -0
- {dronecot-2.3.7 → dronecot-2.3.8}/MANIFEST.in +0 -0
- {dronecot-2.3.7 → dronecot-2.3.8}/README.md +0 -0
- {dronecot-2.3.7 → dronecot-2.3.8}/dronecot.egg-info/dependency_links.txt +0 -0
- {dronecot-2.3.7 → dronecot-2.3.8}/dronecot.egg-info/entry_points.txt +0 -0
- {dronecot-2.3.7 → dronecot-2.3.8}/dronecot.egg-info/requires.txt +0 -0
- {dronecot-2.3.7 → dronecot-2.3.8}/dronecot.egg-info/top_level.txt +0 -0
- {dronecot-2.3.7 → dronecot-2.3.8}/setup.cfg +0 -0
- {dronecot-2.3.7 → dronecot-2.3.8}/setup.py +0 -0
- {dronecot-2.3.7 → dronecot-2.3.8}/src/dronecot/__init__.py +0 -0
- {dronecot-2.3.7 → dronecot-2.3.8}/src/dronecot/ble_capture.py +0 -0
- {dronecot-2.3.7 → dronecot-2.3.8}/src/dronecot/ble_parse.py +0 -0
- {dronecot-2.3.7 → dronecot-2.3.8}/src/dronecot/bluez_capture.py +0 -0
- {dronecot-2.3.7 → dronecot-2.3.8}/src/dronecot/commands.py +0 -0
- {dronecot-2.3.7 → dronecot-2.3.8}/src/dronecot/constants.py +0 -0
- {dronecot-2.3.7 → dronecot-2.3.8}/src/dronecot/dji_exceptions.py +0 -0
- {dronecot-2.3.7 → dronecot-2.3.8}/src/dronecot/dji_functions.py +0 -0
- {dronecot-2.3.7 → dronecot-2.3.8}/src/dronecot/dji_text_parser.py +0 -0
- {dronecot-2.3.7 → dronecot-2.3.8}/src/dronecot/functions.py +0 -0
- {dronecot-2.3.7 → dronecot-2.3.8}/src/dronecot/odid.py +0 -0
- {dronecot-2.3.7 → dronecot-2.3.8}/src/dronecot/open_drone_id.py +0 -0
- {dronecot-2.3.7 → dronecot-2.3.8}/src/dronecot/rid_normalize.py +0 -0
- {dronecot-2.3.7 → dronecot-2.3.8}/src/dronecot/rid_track.py +0 -0
- {dronecot-2.3.7 → dronecot-2.3.8}/src/dronecot/udp_rid.py +0 -0
- {dronecot-2.3.7 → dronecot-2.3.8}/src/dronecot/wifi_capture.py +0 -0
- {dronecot-2.3.7 → dronecot-2.3.8}/src/dronecot/wifi_parse.py +0 -0
- {dronecot-2.3.7 → dronecot-2.3.8}/tests/test_adsb_vehicle.py +0 -0
- {dronecot-2.3.7 → dronecot-2.3.8}/tests/test_bluez_capture.py +0 -0
- {dronecot-2.3.7 → dronecot-2.3.8}/tests/test_dji_listener.py +0 -0
- {dronecot-2.3.7 → dronecot-2.3.8}/tests/test_functions.py +0 -0
- {dronecot-2.3.7 → dronecot-2.3.8}/tests/test_mavlink_identity.py +0 -0
- {dronecot-2.3.7 → dronecot-2.3.8}/tests/test_rid_normalize.py +0 -0
- {dronecot-2.3.7 → dronecot-2.3.8}/tests/test_rid_track.py +0 -0
- {dronecot-2.3.7 → dronecot-2.3.8}/tests/test_sensor_detail.py +0 -0
- {dronecot-2.3.7 → dronecot-2.3.8}/tests/test_status.py +0 -0
- {dronecot-2.3.7 → dronecot-2.3.8}/tests/test_wifi_parse.py +0 -0
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
## DroneCOT 2.3.8
|
|
2
|
+
|
|
3
|
+
- Add opt-in `SERIAL_CRLF_NORMALIZE` support for receivers that send binary
|
|
4
|
+
MAVLink through an ESP console stream configured to expand LF bytes to CRLF.
|
|
5
|
+
The streaming normalizer reverses expansion across serial read boundaries so
|
|
6
|
+
pymavlink can validate and decode frames that were previously discarded.
|
|
7
|
+
|
|
1
8
|
## DroneCOT 2.3.7
|
|
2
9
|
|
|
3
10
|
- Reload systemd's unit metadata during Debian package configuration so an
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
2.3.8
|
|
@@ -75,6 +75,49 @@ class _NoStatus:
|
|
|
75
75
|
_StatusWriter = getattr(pytak, "StatusWriter", None)
|
|
76
76
|
|
|
77
77
|
|
|
78
|
+
class _CRLFNormalizer:
|
|
79
|
+
"""Reverse an LF-to-CRLF transformation on a binary byte stream.
|
|
80
|
+
|
|
81
|
+
Some ESP32 USB serial firmware sends MAVLink through a console-oriented
|
|
82
|
+
VFS stream configured to turn every LF byte into CRLF. That conversion
|
|
83
|
+
also touches LF bytes inside MAVLink headers, payloads and checksums, so
|
|
84
|
+
pymavlink rejects the affected frames. Keep a trailing CR between reads
|
|
85
|
+
so a CRLF pair split across pyserial chunks is still reversed exactly.
|
|
86
|
+
|
|
87
|
+
This is deliberately opt-in. Applying it to a compliant binary stream
|
|
88
|
+
would collapse a legitimate CRLF byte sequence and corrupt that frame.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
def __init__(self):
|
|
92
|
+
self._pending_cr = False
|
|
93
|
+
|
|
94
|
+
def feed(self, data: bytes) -> bytes:
|
|
95
|
+
"""Return *data* with transmit-side CRLF expansion reversed."""
|
|
96
|
+
if not data:
|
|
97
|
+
return b""
|
|
98
|
+
|
|
99
|
+
if self._pending_cr:
|
|
100
|
+
data = b"\r" + data
|
|
101
|
+
self._pending_cr = False
|
|
102
|
+
|
|
103
|
+
if data.endswith(b"\r"):
|
|
104
|
+
data = data[:-1]
|
|
105
|
+
self._pending_cr = True
|
|
106
|
+
|
|
107
|
+
return data.replace(b"\r\n", b"\n")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class _CRLFNormalizingReceiver:
|
|
111
|
+
"""Callable wrapper around a pymavlink connection's ``recv`` method."""
|
|
112
|
+
|
|
113
|
+
def __init__(self, receiver):
|
|
114
|
+
self._receiver = receiver
|
|
115
|
+
self._normalizer = _CRLFNormalizer()
|
|
116
|
+
|
|
117
|
+
def __call__(self, size=None) -> bytes:
|
|
118
|
+
return self._normalizer.feed(self._receiver(size))
|
|
119
|
+
|
|
120
|
+
|
|
78
121
|
def make_status(app_name: str, version: str):
|
|
79
122
|
"""Return a status writer, or a no-op if this pytak has none."""
|
|
80
123
|
if _StatusWriter is None:
|
|
@@ -122,6 +165,18 @@ class SerialWorker(pytak.QueueWorker):
|
|
|
122
165
|
|
|
123
166
|
return serial_port, baud_rate
|
|
124
167
|
|
|
168
|
+
def _serial_crlf_normalization_enabled(self) -> bool:
|
|
169
|
+
"""Return whether broken console-style CRLF expansion is expected."""
|
|
170
|
+
value = self.config.get("SERIAL_CRLF_NORMALIZE", "0")
|
|
171
|
+
return str(value).strip().lower() in {"1", "true", "yes", "on"}
|
|
172
|
+
|
|
173
|
+
def _configure_serial_connection(self, master) -> None:
|
|
174
|
+
"""Install optional byte-stream compatibility filters on *master*."""
|
|
175
|
+
if not self._serial_crlf_normalization_enabled():
|
|
176
|
+
return
|
|
177
|
+
master.recv = _CRLFNormalizingReceiver(master.recv)
|
|
178
|
+
self._logger.info("Normalizing CRLF-expanded MAVLink serial input")
|
|
179
|
+
|
|
125
180
|
@staticmethod
|
|
126
181
|
def _id_or_mac_to_mac(id_or_mac) -> Optional[str]:
|
|
127
182
|
"""Render a MAVLink OpenDroneID ``id_or_mac`` field as an advertiser MAC.
|
|
@@ -228,6 +283,7 @@ class SerialWorker(pytak.QueueWorker):
|
|
|
228
283
|
master = await asyncio.to_thread(
|
|
229
284
|
mavutil.mavlink_connection, interface, baudrate
|
|
230
285
|
)
|
|
286
|
+
self._configure_serial_connection(master)
|
|
231
287
|
await asyncio.to_thread(master.wait_heartbeat, timeout=20)
|
|
232
288
|
self._logger.info("MAVLink heartbeat received")
|
|
233
289
|
except SerialException as exc:
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
"""Regression tests for CRLF-expanded binary MAVLink serial receivers."""
|
|
5
|
+
|
|
6
|
+
import unittest
|
|
7
|
+
|
|
8
|
+
from pymavlink.dialects.v20 import common as mavlink
|
|
9
|
+
|
|
10
|
+
from dronecot.classes import (
|
|
11
|
+
SerialWorker,
|
|
12
|
+
_CRLFNormalizer,
|
|
13
|
+
_CRLFNormalizingReceiver,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CRLFNormalizerTest(unittest.TestCase):
|
|
18
|
+
"""Exercise the streaming inverse of ESP console newline expansion."""
|
|
19
|
+
|
|
20
|
+
def test_reverses_expansion_across_every_chunk_boundary(self):
|
|
21
|
+
original = b"header\nbody\r\ntail\x00\nend"
|
|
22
|
+
expanded = original.replace(b"\n", b"\r\n")
|
|
23
|
+
|
|
24
|
+
for split in range(len(expanded) + 1):
|
|
25
|
+
normalizer = _CRLFNormalizer()
|
|
26
|
+
actual = normalizer.feed(expanded[:split])
|
|
27
|
+
actual += normalizer.feed(expanded[split:])
|
|
28
|
+
self.assertEqual(actual, original, f"split={split}")
|
|
29
|
+
|
|
30
|
+
def test_preserves_a_cr_not_followed_by_lf(self):
|
|
31
|
+
normalizer = _CRLFNormalizer()
|
|
32
|
+
self.assertEqual(normalizer.feed(b"one\r"), b"one")
|
|
33
|
+
self.assertEqual(normalizer.feed(b"two"), b"\rtwo")
|
|
34
|
+
|
|
35
|
+
def test_receiver_wrapper_preserves_state_between_reads(self):
|
|
36
|
+
chunks = iter((b"abc\r", b"\ndef", b""))
|
|
37
|
+
receiver = _CRLFNormalizingReceiver(lambda _size: next(chunks))
|
|
38
|
+
self.assertEqual(receiver(4), b"abc")
|
|
39
|
+
self.assertEqual(receiver(4), b"\ndef")
|
|
40
|
+
self.assertEqual(receiver(4), b"")
|
|
41
|
+
|
|
42
|
+
def test_recovers_checksum_valid_mavlink_frame(self):
|
|
43
|
+
encoder = mavlink.MAVLink(None, srcSystem=254, srcComponent=236)
|
|
44
|
+
# custom_mode=10 guarantees an LF byte inside the binary payload.
|
|
45
|
+
packet = encoder.heartbeat_encode(34, 8, 0, 10, 4, 3).pack(encoder)
|
|
46
|
+
self.assertIn(b"\n", packet)
|
|
47
|
+
expanded = (packet + b"\n").replace(b"\n", b"\r\n")
|
|
48
|
+
|
|
49
|
+
normalizer = _CRLFNormalizer()
|
|
50
|
+
recovered = b"".join(normalizer.feed(bytes((byte,))) for byte in expanded)
|
|
51
|
+
self.assertEqual(recovered, packet + b"\n")
|
|
52
|
+
|
|
53
|
+
parser = mavlink.MAVLink(None)
|
|
54
|
+
parser.robust_parsing = True
|
|
55
|
+
messages = []
|
|
56
|
+
for byte in recovered:
|
|
57
|
+
message = parser.parse_char(bytes((byte,)))
|
|
58
|
+
if message is not None and message.get_type() != "BAD_DATA":
|
|
59
|
+
messages.append(message)
|
|
60
|
+
|
|
61
|
+
self.assertEqual([message.get_type() for message in messages], ["HEARTBEAT"])
|
|
62
|
+
self.assertEqual(messages[0].custom_mode, 10)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class SerialWorkerCRLFConfigTest(unittest.TestCase):
|
|
66
|
+
"""Keep the compatibility filter opt-in for standards-compliant feeds."""
|
|
67
|
+
|
|
68
|
+
@staticmethod
|
|
69
|
+
def _worker(value=None):
|
|
70
|
+
worker = SerialWorker.__new__(SerialWorker)
|
|
71
|
+
worker.config = {}
|
|
72
|
+
if value is not None:
|
|
73
|
+
worker.config["SERIAL_CRLF_NORMALIZE"] = value
|
|
74
|
+
return worker
|
|
75
|
+
|
|
76
|
+
def test_disabled_by_default(self):
|
|
77
|
+
self.assertFalse(self._worker()._serial_crlf_normalization_enabled())
|
|
78
|
+
|
|
79
|
+
def test_accepts_common_true_values(self):
|
|
80
|
+
for value in ("1", "true", "TRUE", "yes", "on", 1, True):
|
|
81
|
+
with self.subTest(value=value):
|
|
82
|
+
self.assertTrue(
|
|
83
|
+
self._worker(value)._serial_crlf_normalization_enabled()
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
def test_rejects_false_values(self):
|
|
87
|
+
for value in ("0", "false", "no", "off", "", 0, False):
|
|
88
|
+
with self.subTest(value=value):
|
|
89
|
+
self.assertFalse(
|
|
90
|
+
self._worker(value)._serial_crlf_normalization_enabled()
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
if __name__ == "__main__":
|
|
95
|
+
unittest.main()
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
2.3.7
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|