dronecot 2.3.1__tar.gz → 2.3.2__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 (41) hide show
  1. {dronecot-2.3.1/dronecot.egg-info → dronecot-2.3.2}/PKG-INFO +1 -1
  2. {dronecot-2.3.1 → dronecot-2.3.2/dronecot.egg-info}/PKG-INFO +1 -1
  3. {dronecot-2.3.1 → dronecot-2.3.2}/dronecot.egg-info/SOURCES.txt +1 -0
  4. dronecot-2.3.2/src/dronecot/VERSION +1 -0
  5. {dronecot-2.3.1 → dronecot-2.3.2}/src/dronecot/classes.py +65 -6
  6. {dronecot-2.3.1 → dronecot-2.3.2}/src/dronecot/functions.py +9 -1
  7. dronecot-2.3.2/tests/test_mavlink_identity.py +145 -0
  8. dronecot-2.3.1/src/dronecot/VERSION +0 -1
  9. {dronecot-2.3.1 → dronecot-2.3.2}/CHANGELOG.md +0 -0
  10. {dronecot-2.3.1 → dronecot-2.3.2}/LICENSE +0 -0
  11. {dronecot-2.3.1 → dronecot-2.3.2}/MANIFEST.in +0 -0
  12. {dronecot-2.3.1 → dronecot-2.3.2}/README.md +0 -0
  13. {dronecot-2.3.1 → dronecot-2.3.2}/dronecot.egg-info/dependency_links.txt +0 -0
  14. {dronecot-2.3.1 → dronecot-2.3.2}/dronecot.egg-info/entry_points.txt +0 -0
  15. {dronecot-2.3.1 → dronecot-2.3.2}/dronecot.egg-info/requires.txt +0 -0
  16. {dronecot-2.3.1 → dronecot-2.3.2}/dronecot.egg-info/top_level.txt +0 -0
  17. {dronecot-2.3.1 → dronecot-2.3.2}/setup.cfg +0 -0
  18. {dronecot-2.3.1 → dronecot-2.3.2}/setup.py +0 -0
  19. {dronecot-2.3.1 → dronecot-2.3.2}/src/dronecot/__init__.py +0 -0
  20. {dronecot-2.3.1 → dronecot-2.3.2}/src/dronecot/ble_capture.py +0 -0
  21. {dronecot-2.3.1 → dronecot-2.3.2}/src/dronecot/ble_parse.py +0 -0
  22. {dronecot-2.3.1 → dronecot-2.3.2}/src/dronecot/bluez_capture.py +0 -0
  23. {dronecot-2.3.1 → dronecot-2.3.2}/src/dronecot/commands.py +0 -0
  24. {dronecot-2.3.1 → dronecot-2.3.2}/src/dronecot/constants.py +0 -0
  25. {dronecot-2.3.1 → dronecot-2.3.2}/src/dronecot/dji_exceptions.py +0 -0
  26. {dronecot-2.3.1 → dronecot-2.3.2}/src/dronecot/dji_functions.py +0 -0
  27. {dronecot-2.3.1 → dronecot-2.3.2}/src/dronecot/dji_text_parser.py +0 -0
  28. {dronecot-2.3.1 → dronecot-2.3.2}/src/dronecot/odid.py +0 -0
  29. {dronecot-2.3.1 → dronecot-2.3.2}/src/dronecot/open_drone_id.py +0 -0
  30. {dronecot-2.3.1 → dronecot-2.3.2}/src/dronecot/rid_normalize.py +0 -0
  31. {dronecot-2.3.1 → dronecot-2.3.2}/src/dronecot/rid_track.py +0 -0
  32. {dronecot-2.3.1 → dronecot-2.3.2}/src/dronecot/udp_rid.py +0 -0
  33. {dronecot-2.3.1 → dronecot-2.3.2}/src/dronecot/wifi_capture.py +0 -0
  34. {dronecot-2.3.1 → dronecot-2.3.2}/src/dronecot/wifi_parse.py +0 -0
  35. {dronecot-2.3.1 → dronecot-2.3.2}/tests/test_adsb_vehicle.py +0 -0
  36. {dronecot-2.3.1 → dronecot-2.3.2}/tests/test_bluez_capture.py +0 -0
  37. {dronecot-2.3.1 → dronecot-2.3.2}/tests/test_functions.py +0 -0
  38. {dronecot-2.3.1 → dronecot-2.3.2}/tests/test_rid_normalize.py +0 -0
  39. {dronecot-2.3.1 → dronecot-2.3.2}/tests/test_rid_track.py +0 -0
  40. {dronecot-2.3.1 → dronecot-2.3.2}/tests/test_sensor_detail.py +0 -0
  41. {dronecot-2.3.1 → dronecot-2.3.2}/tests/test_wifi_parse.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dronecot
3
- Version: 2.3.1
3
+ Version: 2.3.2
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.1
3
+ Version: 2.3.2
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>
@@ -32,6 +32,7 @@ src/dronecot/wifi_parse.py
32
32
  tests/test_adsb_vehicle.py
33
33
  tests/test_bluez_capture.py
34
34
  tests/test_functions.py
35
+ tests/test_mavlink_identity.py
35
36
  tests/test_rid_normalize.py
36
37
  tests/test_rid_track.py
37
38
  tests/test_sensor_detail.py
@@ -0,0 +1 @@
1
+ 2.3.2
@@ -82,13 +82,64 @@ class SerialWorker(pytak.QueueWorker):
82
82
 
83
83
  return serial_port, baud_rate
84
84
 
85
- def _mavlink_pack_to_parse_payload_schema(self, messages, pack_size) -> dict:
85
+ @staticmethod
86
+ def _id_or_mac_to_mac(id_or_mac) -> Optional[str]:
87
+ """Render a MAVLink OpenDroneID ``id_or_mac`` field as an advertiser MAC.
88
+
89
+ ODID carries a 20-byte ``id_or_mac`` naming the TRANSMITTER, and it is the
90
+ only per-aircraft grouping key a MAVLink serial feed offers -- unlike a
91
+ BLE or Wi-Fi capture there is no radio MAC in the metadata.
92
+
93
+ Measured on a DroneScout DS110: populated on 58 of 58 packs, as
94
+ db:13:81:94:13:55 followed by 14 zero bytes.
95
+
96
+ Only a MAC-shaped value (6 significant bytes then padding) is returned.
97
+ The field may instead carry an opaque ID string, and calling that a MAC
98
+ would put a wrong-looking value in the CoT UID; such feeds keep the
99
+ previous behaviour until there is real hardware to test against.
100
+ """
101
+ if id_or_mac is None:
102
+ return None
103
+ raw = bytes(bytearray(id_or_mac))
104
+ if not any(raw) or len(raw) < 6 or any(raw[6:]):
105
+ return None
106
+ return ":".join("%02X" % b for b in raw[:6])
107
+
108
+ def _mavlink_meta(self, id_or_mac=None, payload_type: Optional[str] = None) -> dict:
109
+ """Sensor metadata for a MAVLink-sourced record.
110
+
111
+ The wireless path builds this via rid_normalize.bytes_to_rid_dict; the
112
+ MAVLink path used to skip it entirely and hand up a record with no
113
+ ``data`` sub-dict at all. Two consequences, both seen on a live box:
114
+
115
+ * no transmitter identity, so a pack carrying only System/OperatorID
116
+ and no BasicID had nothing to aggregate on and rendered as
117
+ op-Unknown-BasicID_0 -- every operator in range on ONE track;
118
+ * no sensor_id, so CoT was attributed to the default
119
+ dronecot_<hostname> rather than the configured SENSOR_ID, making it
120
+ impossible to tell which receiver saw a contact.
121
+ """
122
+ data = dronecot.rid_normalize.uas_meta_defaults(self.config)
123
+ if payload_type:
124
+ data["type"] = payload_type
125
+ mac = self._id_or_mac_to_mac(id_or_mac)
126
+ if mac:
127
+ data["MAC address"] = mac
128
+ return data
129
+
130
+ def _mavlink_pack_to_parse_payload_schema(
131
+ self, messages, pack_size, id_or_mac=None
132
+ ) -> dict:
86
133
  """Convert MAVLink OPEN_DRONE_ID_MESSAGE_PACK to parse_payload-like schema."""
87
134
  parsed = dronecot.odid.message_pack_to_dict(messages, pack_size)
88
- return dronecot.rid_normalize.odid_parsed_to_rid_dict(parsed)
135
+ rid = dronecot.rid_normalize.odid_parsed_to_rid_dict(parsed)
136
+ rid["data"] = self._mavlink_meta(
137
+ id_or_mac, payload_type="MAVLink OPEN_DRONE_ID_MESSAGE_PACK"
138
+ )
139
+ return rid
89
140
 
90
141
  @staticmethod
91
- def _adsb_vehicle_to_rid_dict(msg) -> dict:
142
+ def _adsb_vehicle_to_rid_dict(msg, meta: Optional[dict] = None) -> dict:
92
143
  """Convert a MAVLink ADSB_VEHICLE to a RIDWorker dict.
93
144
 
94
145
  The DroneScout Bridge (and similar Remote ID receivers) can emit detected
@@ -113,7 +164,7 @@ class SerialWorker(pytak.QueueWorker):
113
164
  "SpeedHorizontal": getattr(msg, "hor_velocity", 0) / 100.0, # cm/s -> m/s
114
165
  "SpeedVertical": getattr(msg, "ver_velocity", 0) / 100.0,
115
166
  "Direction": getattr(msg, "heading", 0) / 100.0, # cdeg -> deg
116
- "data": {"type": "MAVLink ADSB_VEHICLE"},
167
+ "data": dict(meta) if meta else {"type": "MAVLink ADSB_VEHICLE"},
117
168
  }
118
169
 
119
170
  async def run(self, _=-1) -> None:
@@ -180,11 +231,19 @@ class SerialWorker(pytak.QueueWorker):
180
231
  msg_type = msg.get_type()
181
232
  if msg_type == "OPEN_DRONE_ID_MESSAGE_PACK":
182
233
  parsed_payload = self._mavlink_pack_to_parse_payload_schema(
183
- msg.messages, msg.msg_pack_size
234
+ msg.messages,
235
+ msg.msg_pack_size,
236
+ getattr(msg, "id_or_mac", None),
184
237
  )
185
238
  await self.put_queue(parsed_payload)
186
239
  elif msg_type == "ADSB_VEHICLE":
187
- rid = self._adsb_vehicle_to_rid_dict(msg)
240
+ rid = self._adsb_vehicle_to_rid_dict(
241
+ msg,
242
+ self._mavlink_meta(
243
+ getattr(msg, "id_or_mac", None),
244
+ payload_type="MAVLink ADSB_VEHICLE",
245
+ ),
246
+ )
188
247
  if rid:
189
248
  await self.put_queue(rid)
190
249
  elif msg_type == "HEARTBEAT":
@@ -269,7 +269,15 @@ def _rid_identity(data: dict) -> Tuple[str, Optional[str]]:
269
269
  # all. Fall back to the feed that delivered it so two receivers do
270
270
  # not collide, rather than the shared Unknown-BasicID_0 placeholder
271
271
  # that put every such aircraft on one track.
272
- feed = src_data.get("sensor_id") or src_data.get("sensor ID")
272
+ # Checked at BOTH levels, for the same reason the MAC is: MQTT and
273
+ # serial feeds put this at the top level. Looking only in the
274
+ # sub-dict meant this rung silently never fired for them.
275
+ feed = (
276
+ src_data.get("sensor_id")
277
+ or src_data.get("sensor ID")
278
+ or data.get("sensor_id")
279
+ or data.get("sensor ID")
280
+ )
273
281
  feed = str(feed).strip() if feed else ""
274
282
  uasid = f"FEED-{feed}" if feed else "Unknown-BasicID_0"
275
283
 
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env python3
2
+ # Copyright Sensors & Signals LLC https://www.snstac.com/
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ """Transmitter identity on the MAVLink serial feed (DroneScout Bridge).
5
+
6
+ Regression tests for operator tracks collapsing onto op-Unknown-BasicID_0.
7
+
8
+ Measured on a live box (aryaos-4f11, DroneScout DS110, dronecot 2.3.1), 30s of
9
+ CoT on the Mesh SA group, attributed by source IP:
10
+
11
+ op-Unknown-BasicID_0 x62 <- every operator on ONE track
12
+ RID.1787F04BM24010011195.uas x54 correct
13
+ op-1787F04BM24010011195 x54 correct
14
+
15
+ The aircraft UID resolved but the operator UID did not. Cause: the MAVLink path
16
+ called rid_normalize.odid_parsed_to_rid_dict directly and attached no ``data``
17
+ metadata at all, unlike the wireless path (bytes_to_rid_dict). With no MAC and no
18
+ sensor_id, a pack carrying only System/OperatorID had no transmitter identity to
19
+ key on, so functions._rid_identity fell all the way to Unknown-BasicID_0.
20
+
21
+ MAVLink OpenDroneID carries a 20-byte ``id_or_mac`` naming the transmitter. The
22
+ DS110 populates it on every pack -- measured 58 of 58, as
23
+ db:13:81:94:13:55 + 14 zero bytes -- and it is the only per-aircraft grouping key
24
+ a serial feed offers.
25
+ """
26
+
27
+ import types
28
+ import unittest
29
+ from unittest import mock
30
+
31
+ from dronecot.classes import SerialWorker
32
+ from dronecot.functions import _rid_identity
33
+
34
+ # Exactly what the DS110 sent, including the 14 bytes of padding.
35
+ DS110_ID_OR_MAC = bytes.fromhex("db13819413550000000000000000000000000000")
36
+ DS110_MAC = "DB:13:81:94:13:55"
37
+
38
+
39
+ def _worker(sensor_id="dronescout"):
40
+ """A SerialWorker with just enough state for the metadata helpers."""
41
+ w = SerialWorker.__new__(SerialWorker)
42
+ w.config = {"SENSOR_ID": sensor_id}
43
+ return w
44
+
45
+
46
+ class IdOrMacTestCase(unittest.TestCase):
47
+ def test_real_ds110_value_renders_as_mac(self):
48
+ self.assertEqual(SerialWorker._id_or_mac_to_mac(DS110_ID_OR_MAC), DS110_MAC)
49
+
50
+ def test_all_zeros_is_no_identity(self):
51
+ self.assertIsNone(SerialWorker._id_or_mac_to_mac(bytes(20)))
52
+
53
+ def test_absent_is_no_identity(self):
54
+ self.assertIsNone(SerialWorker._id_or_mac_to_mac(None))
55
+
56
+ def test_opaque_id_is_not_claimed_to_be_a_mac(self):
57
+ """Payload past byte 6 means this is an ID string, not a MAC.
58
+
59
+ Rendering it as one would put a wrong-looking value in the CoT UID, so it
60
+ is left alone rather than guessed at.
61
+ """
62
+ self.assertIsNone(SerialWorker._id_or_mac_to_mac(b"SERIAL12345678901234"))
63
+
64
+ def test_accepts_a_bare_six_byte_field(self):
65
+ self.assertEqual(
66
+ SerialWorker._id_or_mac_to_mac(bytes.fromhex("db13819413 55".replace(" ", ""))),
67
+ DS110_MAC,
68
+ )
69
+
70
+
71
+ class MavlinkMetaTestCase(unittest.TestCase):
72
+ def test_meta_carries_identity_and_configured_sensor_id(self):
73
+ meta = _worker()._mavlink_meta(DS110_ID_OR_MAC, payload_type="pack")
74
+ self.assertEqual(meta["MAC address"], DS110_MAC)
75
+ # Previously absent, so CoT was attributed to dronecot_<hostname> and you
76
+ # could not tell which receiver saw a contact.
77
+ self.assertEqual(meta["sensor_id"], "dronescout")
78
+ self.assertEqual(meta["type"], "pack")
79
+
80
+ def test_no_identity_still_yields_sensor_id(self):
81
+ meta = _worker()._mavlink_meta(bytes(20))
82
+ self.assertNotIn("MAC address", meta)
83
+ self.assertEqual(meta["sensor_id"], "dronescout")
84
+
85
+
86
+ class PackIdentityTestCase(unittest.TestCase):
87
+ """The actual regression: a pack with no BasicID must still identify itself."""
88
+
89
+ def _pack(self, parsed):
90
+ with mock.patch("dronecot.odid.message_pack_to_dict", return_value=parsed):
91
+ return _worker()._mavlink_pack_to_parse_payload_schema(
92
+ b"\x00" * 25, 1, DS110_ID_OR_MAC
93
+ )
94
+
95
+ def test_pack_without_basicid_gets_a_distinct_uid(self):
96
+ rid = self._pack({"OperatorLatitude": 37.7, "OperatorLongitude": -122.4})
97
+ uasid, mac = _rid_identity(rid)
98
+ self.assertEqual(mac, DS110_MAC)
99
+ # The bug: this was "Unknown-BasicID_0" for every transmitter in range.
100
+ self.assertNotEqual(uasid, "Unknown-BasicID_0")
101
+ self.assertEqual(uasid, "MAC-DB1381941355")
102
+
103
+ def test_pack_with_basicid_prefers_the_real_serial(self):
104
+ rid = self._pack({"UASID": "1787F04BM24010011195"})
105
+ uasid, mac = _rid_identity(rid)
106
+ self.assertEqual(uasid, "1787F04BM24010011195")
107
+ self.assertEqual(mac, DS110_MAC)
108
+
109
+ def test_two_transmitters_do_not_collapse(self):
110
+ """The DS110 reports many aircraft over one port; they must stay distinct."""
111
+ other = bytes.fromhex("aabbccddeeff" + "00" * 14)
112
+ with mock.patch("dronecot.odid.message_pack_to_dict", return_value={}):
113
+ a = _worker()._mavlink_pack_to_parse_payload_schema(
114
+ b"\x00" * 25, 1, DS110_ID_OR_MAC
115
+ )
116
+ b = _worker()._mavlink_pack_to_parse_payload_schema(
117
+ b"\x00" * 25, 1, other
118
+ )
119
+ self.assertNotEqual(_rid_identity(a)[0], _rid_identity(b)[0])
120
+
121
+
122
+ class SensorIdFallbackTestCase(unittest.TestCase):
123
+ """_rid_identity looked for sensor_id only in data['data'].
124
+
125
+ The MAC was already checked at both levels because "MQTT and serial feeds put
126
+ it at the top level" -- the same is true of sensor_id, so that rung of the
127
+ fallback silently never fired for those feeds.
128
+ """
129
+
130
+ def test_top_level_sensor_id_is_used(self):
131
+ uasid, mac = _rid_identity({"sensor_id": "dronescout"})
132
+ self.assertIsNone(mac)
133
+ self.assertEqual(uasid, "FEED-dronescout")
134
+
135
+ def test_nested_sensor_id_still_works(self):
136
+ uasid, _ = _rid_identity({"data": {"sensor_id": "dronescout"}})
137
+ self.assertEqual(uasid, "FEED-dronescout")
138
+
139
+ def test_nothing_at_all_is_still_the_last_resort(self):
140
+ uasid, _ = _rid_identity({})
141
+ self.assertEqual(uasid, "Unknown-BasicID_0")
142
+
143
+
144
+ if __name__ == "__main__":
145
+ unittest.main()
@@ -1 +0,0 @@
1
- 2.3.1
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes