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.
Files changed (44) hide show
  1. {dronecot-2.3.7 → dronecot-2.3.9}/CHANGELOG.md +7 -0
  2. {dronecot-2.3.7/dronecot.egg-info → dronecot-2.3.9}/PKG-INFO +1 -1
  3. {dronecot-2.3.7 → dronecot-2.3.9/dronecot.egg-info}/PKG-INFO +1 -1
  4. {dronecot-2.3.7 → dronecot-2.3.9}/dronecot.egg-info/SOURCES.txt +1 -0
  5. dronecot-2.3.9/src/dronecot/VERSION +1 -0
  6. {dronecot-2.3.7 → dronecot-2.3.9}/src/dronecot/classes.py +70 -5
  7. dronecot-2.3.9/tests/test_serial_crlf.py +95 -0
  8. {dronecot-2.3.7 → dronecot-2.3.9}/tests/test_status.py +19 -0
  9. dronecot-2.3.7/src/dronecot/VERSION +0 -1
  10. {dronecot-2.3.7 → dronecot-2.3.9}/LICENSE +0 -0
  11. {dronecot-2.3.7 → dronecot-2.3.9}/MANIFEST.in +0 -0
  12. {dronecot-2.3.7 → dronecot-2.3.9}/README.md +0 -0
  13. {dronecot-2.3.7 → dronecot-2.3.9}/dronecot.egg-info/dependency_links.txt +0 -0
  14. {dronecot-2.3.7 → dronecot-2.3.9}/dronecot.egg-info/entry_points.txt +0 -0
  15. {dronecot-2.3.7 → dronecot-2.3.9}/dronecot.egg-info/requires.txt +0 -0
  16. {dronecot-2.3.7 → dronecot-2.3.9}/dronecot.egg-info/top_level.txt +0 -0
  17. {dronecot-2.3.7 → dronecot-2.3.9}/setup.cfg +0 -0
  18. {dronecot-2.3.7 → dronecot-2.3.9}/setup.py +0 -0
  19. {dronecot-2.3.7 → dronecot-2.3.9}/src/dronecot/__init__.py +0 -0
  20. {dronecot-2.3.7 → dronecot-2.3.9}/src/dronecot/ble_capture.py +0 -0
  21. {dronecot-2.3.7 → dronecot-2.3.9}/src/dronecot/ble_parse.py +0 -0
  22. {dronecot-2.3.7 → dronecot-2.3.9}/src/dronecot/bluez_capture.py +0 -0
  23. {dronecot-2.3.7 → dronecot-2.3.9}/src/dronecot/commands.py +0 -0
  24. {dronecot-2.3.7 → dronecot-2.3.9}/src/dronecot/constants.py +0 -0
  25. {dronecot-2.3.7 → dronecot-2.3.9}/src/dronecot/dji_exceptions.py +0 -0
  26. {dronecot-2.3.7 → dronecot-2.3.9}/src/dronecot/dji_functions.py +0 -0
  27. {dronecot-2.3.7 → dronecot-2.3.9}/src/dronecot/dji_text_parser.py +0 -0
  28. {dronecot-2.3.7 → dronecot-2.3.9}/src/dronecot/functions.py +0 -0
  29. {dronecot-2.3.7 → dronecot-2.3.9}/src/dronecot/odid.py +0 -0
  30. {dronecot-2.3.7 → dronecot-2.3.9}/src/dronecot/open_drone_id.py +0 -0
  31. {dronecot-2.3.7 → dronecot-2.3.9}/src/dronecot/rid_normalize.py +0 -0
  32. {dronecot-2.3.7 → dronecot-2.3.9}/src/dronecot/rid_track.py +0 -0
  33. {dronecot-2.3.7 → dronecot-2.3.9}/src/dronecot/udp_rid.py +0 -0
  34. {dronecot-2.3.7 → dronecot-2.3.9}/src/dronecot/wifi_capture.py +0 -0
  35. {dronecot-2.3.7 → dronecot-2.3.9}/src/dronecot/wifi_parse.py +0 -0
  36. {dronecot-2.3.7 → dronecot-2.3.9}/tests/test_adsb_vehicle.py +0 -0
  37. {dronecot-2.3.7 → dronecot-2.3.9}/tests/test_bluez_capture.py +0 -0
  38. {dronecot-2.3.7 → dronecot-2.3.9}/tests/test_dji_listener.py +0 -0
  39. {dronecot-2.3.7 → dronecot-2.3.9}/tests/test_functions.py +0 -0
  40. {dronecot-2.3.7 → dronecot-2.3.9}/tests/test_mavlink_identity.py +0 -0
  41. {dronecot-2.3.7 → dronecot-2.3.9}/tests/test_rid_normalize.py +0 -0
  42. {dronecot-2.3.7 → dronecot-2.3.9}/tests/test_rid_track.py +0 -0
  43. {dronecot-2.3.7 → dronecot-2.3.9}/tests/test_sensor_detail.py +0 -0
  44. {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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dronecot
3
- Version: 2.3.7
3
+ Version: 2.3.9
4
4
  Summary: Display Drones in Team Awareness Kit (TAK).
5
5
  Home-page: https://github.com/snstac/dronecot
6
6
  Maintainer: Greg Albrecht <oss@undef.net>
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dronecot
3
- Version: 2.3.7
3
+ Version: 2.3.9
4
4
  Summary: Display Drones in Team Awareness Kit (TAK).
5
5
  Home-page: https://github.com/snstac/dronecot
6
6
  Maintainer: Greg Albrecht <oss@undef.net>
@@ -37,5 +37,6 @@ tests/test_mavlink_identity.py
37
37
  tests/test_rid_normalize.py
38
38
  tests/test_rid_track.py
39
39
  tests/test_sensor_detail.py
40
+ tests/test_serial_crlf.py
40
41
  tests/test_status.py
41
42
  tests/test_wifi_parse.py
@@ -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
- def make_status(app_name: str, version: str):
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 = make_status("dronecot", dronecot.__version__)
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__(self, tx_queue: asyncio.Queue, config, net_queue: asyncio.Queue) -> None:
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 = make_status("dronecot", dronecot.__version__)
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