dronecot 2.3.7__tar.gz → 2.3.9__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.9}/CHANGELOG.md +7 -0
- {dronecot-2.3.7/dronecot.egg-info → dronecot-2.3.9}/PKG-INFO +1 -1
- {dronecot-2.3.7 → dronecot-2.3.9/dronecot.egg-info}/PKG-INFO +1 -1
- {dronecot-2.3.7 → dronecot-2.3.9}/dronecot.egg-info/SOURCES.txt +1 -0
- dronecot-2.3.9/src/dronecot/VERSION +1 -0
- {dronecot-2.3.7 → dronecot-2.3.9}/src/dronecot/classes.py +70 -5
- dronecot-2.3.9/tests/test_serial_crlf.py +95 -0
- {dronecot-2.3.7 → dronecot-2.3.9}/tests/test_status.py +19 -0
- dronecot-2.3.7/src/dronecot/VERSION +0 -1
- {dronecot-2.3.7 → dronecot-2.3.9}/LICENSE +0 -0
- {dronecot-2.3.7 → dronecot-2.3.9}/MANIFEST.in +0 -0
- {dronecot-2.3.7 → dronecot-2.3.9}/README.md +0 -0
- {dronecot-2.3.7 → dronecot-2.3.9}/dronecot.egg-info/dependency_links.txt +0 -0
- {dronecot-2.3.7 → dronecot-2.3.9}/dronecot.egg-info/entry_points.txt +0 -0
- {dronecot-2.3.7 → dronecot-2.3.9}/dronecot.egg-info/requires.txt +0 -0
- {dronecot-2.3.7 → dronecot-2.3.9}/dronecot.egg-info/top_level.txt +0 -0
- {dronecot-2.3.7 → dronecot-2.3.9}/setup.cfg +0 -0
- {dronecot-2.3.7 → dronecot-2.3.9}/setup.py +0 -0
- {dronecot-2.3.7 → dronecot-2.3.9}/src/dronecot/__init__.py +0 -0
- {dronecot-2.3.7 → dronecot-2.3.9}/src/dronecot/ble_capture.py +0 -0
- {dronecot-2.3.7 → dronecot-2.3.9}/src/dronecot/ble_parse.py +0 -0
- {dronecot-2.3.7 → dronecot-2.3.9}/src/dronecot/bluez_capture.py +0 -0
- {dronecot-2.3.7 → dronecot-2.3.9}/src/dronecot/commands.py +0 -0
- {dronecot-2.3.7 → dronecot-2.3.9}/src/dronecot/constants.py +0 -0
- {dronecot-2.3.7 → dronecot-2.3.9}/src/dronecot/dji_exceptions.py +0 -0
- {dronecot-2.3.7 → dronecot-2.3.9}/src/dronecot/dji_functions.py +0 -0
- {dronecot-2.3.7 → dronecot-2.3.9}/src/dronecot/dji_text_parser.py +0 -0
- {dronecot-2.3.7 → dronecot-2.3.9}/src/dronecot/functions.py +0 -0
- {dronecot-2.3.7 → dronecot-2.3.9}/src/dronecot/odid.py +0 -0
- {dronecot-2.3.7 → dronecot-2.3.9}/src/dronecot/open_drone_id.py +0 -0
- {dronecot-2.3.7 → dronecot-2.3.9}/src/dronecot/rid_normalize.py +0 -0
- {dronecot-2.3.7 → dronecot-2.3.9}/src/dronecot/rid_track.py +0 -0
- {dronecot-2.3.7 → dronecot-2.3.9}/src/dronecot/udp_rid.py +0 -0
- {dronecot-2.3.7 → dronecot-2.3.9}/src/dronecot/wifi_capture.py +0 -0
- {dronecot-2.3.7 → dronecot-2.3.9}/src/dronecot/wifi_parse.py +0 -0
- {dronecot-2.3.7 → dronecot-2.3.9}/tests/test_adsb_vehicle.py +0 -0
- {dronecot-2.3.7 → dronecot-2.3.9}/tests/test_bluez_capture.py +0 -0
- {dronecot-2.3.7 → dronecot-2.3.9}/tests/test_dji_listener.py +0 -0
- {dronecot-2.3.7 → dronecot-2.3.9}/tests/test_functions.py +0 -0
- {dronecot-2.3.7 → dronecot-2.3.9}/tests/test_mavlink_identity.py +0 -0
- {dronecot-2.3.7 → dronecot-2.3.9}/tests/test_rid_normalize.py +0 -0
- {dronecot-2.3.7 → dronecot-2.3.9}/tests/test_rid_track.py +0 -0
- {dronecot-2.3.7 → dronecot-2.3.9}/tests/test_sensor_detail.py +0 -0
- {dronecot-2.3.7 → dronecot-2.3.9}/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.9
|
|
@@ -75,11 +75,61 @@ class _NoStatus:
|
|
|
75
75
|
_StatusWriter = getattr(pytak, "StatusWriter", None)
|
|
76
76
|
|
|
77
77
|
|
|
78
|
-
|
|
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
|
+
|
|
121
|
+
def make_status(app_name: str, version: str, path: Optional[str] = None):
|
|
79
122
|
"""Return a status writer, or a no-op if this pytak has none."""
|
|
80
123
|
if _StatusWriter is None:
|
|
81
124
|
return _NoStatus()
|
|
82
|
-
return _StatusWriter(app_name, version=version)
|
|
125
|
+
return _StatusWriter(app_name, path=path, version=version)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def make_worker_status(config):
|
|
129
|
+
"""Build the status writer for one independently managed instance."""
|
|
130
|
+
app_name = str(config.get("STATUS_APP", "dronecot") or "dronecot")
|
|
131
|
+
path = str(config.get("STATUS_PATH", "") or "") or None
|
|
132
|
+
return make_status(app_name, dronecot.__version__, path=path)
|
|
83
133
|
|
|
84
134
|
|
|
85
135
|
class SerialWorker(pytak.QueueWorker):
|
|
@@ -122,6 +172,18 @@ class SerialWorker(pytak.QueueWorker):
|
|
|
122
172
|
|
|
123
173
|
return serial_port, baud_rate
|
|
124
174
|
|
|
175
|
+
def _serial_crlf_normalization_enabled(self) -> bool:
|
|
176
|
+
"""Return whether broken console-style CRLF expansion is expected."""
|
|
177
|
+
value = self.config.get("SERIAL_CRLF_NORMALIZE", "0")
|
|
178
|
+
return str(value).strip().lower() in {"1", "true", "yes", "on"}
|
|
179
|
+
|
|
180
|
+
def _configure_serial_connection(self, master) -> None:
|
|
181
|
+
"""Install optional byte-stream compatibility filters on *master*."""
|
|
182
|
+
if not self._serial_crlf_normalization_enabled():
|
|
183
|
+
return
|
|
184
|
+
master.recv = _CRLFNormalizingReceiver(master.recv)
|
|
185
|
+
self._logger.info("Normalizing CRLF-expanded MAVLink serial input")
|
|
186
|
+
|
|
125
187
|
@staticmethod
|
|
126
188
|
def _id_or_mac_to_mac(id_or_mac) -> Optional[str]:
|
|
127
189
|
"""Render a MAVLink OpenDroneID ``id_or_mac`` field as an advertiser MAC.
|
|
@@ -228,6 +290,7 @@ class SerialWorker(pytak.QueueWorker):
|
|
|
228
290
|
master = await asyncio.to_thread(
|
|
229
291
|
mavutil.mavlink_connection, interface, baudrate
|
|
230
292
|
)
|
|
293
|
+
self._configure_serial_connection(master)
|
|
231
294
|
await asyncio.to_thread(master.wait_heartbeat, timeout=20)
|
|
232
295
|
self._logger.info("MAVLink heartbeat received")
|
|
233
296
|
except SerialException as exc:
|
|
@@ -776,7 +839,7 @@ class RIDWorker(pytak.QueueWorker):
|
|
|
776
839
|
# every RF source (Wi-Fi, BLE, BlueZ, serial MAVLink, MQTT, UDP) funnels
|
|
777
840
|
# through here. Instrumenting the capture workers instead would give N
|
|
778
841
|
# writers racing on one path, and each would see only its own slice.
|
|
779
|
-
self.status =
|
|
842
|
+
self.status = make_worker_status(config)
|
|
780
843
|
|
|
781
844
|
def _tracked(self) -> int:
|
|
782
845
|
"""Number of transmitters the aggregator is currently holding."""
|
|
@@ -934,10 +997,12 @@ class RXMockWorker(pytak.RXWorker):
|
|
|
934
997
|
class DJIWorker(pytak.QueueWorker):
|
|
935
998
|
"""Process DJI Drone ID data from a net queue and emit CoT events."""
|
|
936
999
|
|
|
937
|
-
def __init__(
|
|
1000
|
+
def __init__(
|
|
1001
|
+
self, tx_queue: asyncio.Queue, config, net_queue: asyncio.Queue
|
|
1002
|
+
) -> None:
|
|
938
1003
|
super().__init__(tx_queue, config)
|
|
939
1004
|
self.net_queue = net_queue
|
|
940
|
-
self.status =
|
|
1005
|
+
self.status = make_worker_status(config)
|
|
941
1006
|
|
|
942
1007
|
async def handle_data(self, data) -> None:
|
|
943
1008
|
if not data:
|
|
@@ -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()
|
|
@@ -330,3 +330,22 @@ class TestStatusDegradesVisibly:
|
|
|
330
330
|
if classes._StatusWriter is None:
|
|
331
331
|
pytest.skip("installed pytak has no StatusWriter")
|
|
332
332
|
assert not isinstance(classes.make_status("x", "0"), classes._NoStatus)
|
|
333
|
+
|
|
334
|
+
def test_instance_status_uses_configured_app_and_path(self, monkeypatch, tmp_path):
|
|
335
|
+
captured = {}
|
|
336
|
+
|
|
337
|
+
def fake_writer(app_name, *, path=None, version=None):
|
|
338
|
+
captured.update(app_name=app_name, path=path, version=version)
|
|
339
|
+
return object()
|
|
340
|
+
|
|
341
|
+
monkeypatch.setattr(classes, "_StatusWriter", fake_writer)
|
|
342
|
+
path = str(tmp_path / "status.json")
|
|
343
|
+
classes.make_worker_status(
|
|
344
|
+
{"STATUS_APP": "dronecot-dronescout", "STATUS_PATH": path}
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
assert captured == {
|
|
348
|
+
"app_name": "dronecot-dronescout",
|
|
349
|
+
"path": path,
|
|
350
|
+
"version": classes.dronecot.__version__,
|
|
351
|
+
}
|
|
@@ -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
|