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.
Files changed (44) hide show
  1. {dronecot-2.3.7 → dronecot-2.3.8}/CHANGELOG.md +7 -0
  2. {dronecot-2.3.7/dronecot.egg-info → dronecot-2.3.8}/PKG-INFO +1 -1
  3. {dronecot-2.3.7 → dronecot-2.3.8/dronecot.egg-info}/PKG-INFO +1 -1
  4. {dronecot-2.3.7 → dronecot-2.3.8}/dronecot.egg-info/SOURCES.txt +1 -0
  5. dronecot-2.3.8/src/dronecot/VERSION +1 -0
  6. {dronecot-2.3.7 → dronecot-2.3.8}/src/dronecot/classes.py +56 -0
  7. dronecot-2.3.8/tests/test_serial_crlf.py +95 -0
  8. dronecot-2.3.7/src/dronecot/VERSION +0 -1
  9. {dronecot-2.3.7 → dronecot-2.3.8}/LICENSE +0 -0
  10. {dronecot-2.3.7 → dronecot-2.3.8}/MANIFEST.in +0 -0
  11. {dronecot-2.3.7 → dronecot-2.3.8}/README.md +0 -0
  12. {dronecot-2.3.7 → dronecot-2.3.8}/dronecot.egg-info/dependency_links.txt +0 -0
  13. {dronecot-2.3.7 → dronecot-2.3.8}/dronecot.egg-info/entry_points.txt +0 -0
  14. {dronecot-2.3.7 → dronecot-2.3.8}/dronecot.egg-info/requires.txt +0 -0
  15. {dronecot-2.3.7 → dronecot-2.3.8}/dronecot.egg-info/top_level.txt +0 -0
  16. {dronecot-2.3.7 → dronecot-2.3.8}/setup.cfg +0 -0
  17. {dronecot-2.3.7 → dronecot-2.3.8}/setup.py +0 -0
  18. {dronecot-2.3.7 → dronecot-2.3.8}/src/dronecot/__init__.py +0 -0
  19. {dronecot-2.3.7 → dronecot-2.3.8}/src/dronecot/ble_capture.py +0 -0
  20. {dronecot-2.3.7 → dronecot-2.3.8}/src/dronecot/ble_parse.py +0 -0
  21. {dronecot-2.3.7 → dronecot-2.3.8}/src/dronecot/bluez_capture.py +0 -0
  22. {dronecot-2.3.7 → dronecot-2.3.8}/src/dronecot/commands.py +0 -0
  23. {dronecot-2.3.7 → dronecot-2.3.8}/src/dronecot/constants.py +0 -0
  24. {dronecot-2.3.7 → dronecot-2.3.8}/src/dronecot/dji_exceptions.py +0 -0
  25. {dronecot-2.3.7 → dronecot-2.3.8}/src/dronecot/dji_functions.py +0 -0
  26. {dronecot-2.3.7 → dronecot-2.3.8}/src/dronecot/dji_text_parser.py +0 -0
  27. {dronecot-2.3.7 → dronecot-2.3.8}/src/dronecot/functions.py +0 -0
  28. {dronecot-2.3.7 → dronecot-2.3.8}/src/dronecot/odid.py +0 -0
  29. {dronecot-2.3.7 → dronecot-2.3.8}/src/dronecot/open_drone_id.py +0 -0
  30. {dronecot-2.3.7 → dronecot-2.3.8}/src/dronecot/rid_normalize.py +0 -0
  31. {dronecot-2.3.7 → dronecot-2.3.8}/src/dronecot/rid_track.py +0 -0
  32. {dronecot-2.3.7 → dronecot-2.3.8}/src/dronecot/udp_rid.py +0 -0
  33. {dronecot-2.3.7 → dronecot-2.3.8}/src/dronecot/wifi_capture.py +0 -0
  34. {dronecot-2.3.7 → dronecot-2.3.8}/src/dronecot/wifi_parse.py +0 -0
  35. {dronecot-2.3.7 → dronecot-2.3.8}/tests/test_adsb_vehicle.py +0 -0
  36. {dronecot-2.3.7 → dronecot-2.3.8}/tests/test_bluez_capture.py +0 -0
  37. {dronecot-2.3.7 → dronecot-2.3.8}/tests/test_dji_listener.py +0 -0
  38. {dronecot-2.3.7 → dronecot-2.3.8}/tests/test_functions.py +0 -0
  39. {dronecot-2.3.7 → dronecot-2.3.8}/tests/test_mavlink_identity.py +0 -0
  40. {dronecot-2.3.7 → dronecot-2.3.8}/tests/test_rid_normalize.py +0 -0
  41. {dronecot-2.3.7 → dronecot-2.3.8}/tests/test_rid_track.py +0 -0
  42. {dronecot-2.3.7 → dronecot-2.3.8}/tests/test_sensor_detail.py +0 -0
  43. {dronecot-2.3.7 → dronecot-2.3.8}/tests/test_status.py +0 -0
  44. {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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dronecot
3
- Version: 2.3.7
3
+ Version: 2.3.8
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.8
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.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