dronecot 2.2.3__tar.gz → 2.2.5__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 (36) hide show
  1. {dronecot-2.2.3/dronecot.egg-info → dronecot-2.2.5}/PKG-INFO +1 -1
  2. {dronecot-2.2.3 → dronecot-2.2.5/dronecot.egg-info}/PKG-INFO +1 -1
  3. {dronecot-2.2.3 → dronecot-2.2.5}/dronecot.egg-info/SOURCES.txt +3 -1
  4. dronecot-2.2.5/src/dronecot/VERSION +1 -0
  5. {dronecot-2.2.3 → dronecot-2.2.5}/src/dronecot/__init__.py +2 -0
  6. {dronecot-2.2.3 → dronecot-2.2.5}/src/dronecot/constants.py +5 -0
  7. {dronecot-2.2.3 → dronecot-2.2.5}/src/dronecot/functions.py +28 -1
  8. {dronecot-2.2.3 → dronecot-2.2.5}/src/dronecot/wifi_parse.py +17 -36
  9. dronecot-2.2.5/tests/test_sensor_detail.py +54 -0
  10. dronecot-2.2.5/tests/test_wifi_parse.py +53 -0
  11. dronecot-2.2.3/src/dronecot/VERSION +0 -1
  12. {dronecot-2.2.3 → dronecot-2.2.5}/CHANGELOG.md +0 -0
  13. {dronecot-2.2.3 → dronecot-2.2.5}/LICENSE +0 -0
  14. {dronecot-2.2.3 → dronecot-2.2.5}/MANIFEST.in +0 -0
  15. {dronecot-2.2.3 → dronecot-2.2.5}/README.md +0 -0
  16. {dronecot-2.2.3 → dronecot-2.2.5}/dronecot.egg-info/dependency_links.txt +0 -0
  17. {dronecot-2.2.3 → dronecot-2.2.5}/dronecot.egg-info/entry_points.txt +0 -0
  18. {dronecot-2.2.3 → dronecot-2.2.5}/dronecot.egg-info/requires.txt +0 -0
  19. {dronecot-2.2.3 → dronecot-2.2.5}/dronecot.egg-info/top_level.txt +0 -0
  20. {dronecot-2.2.3 → dronecot-2.2.5}/setup.cfg +0 -0
  21. {dronecot-2.2.3 → dronecot-2.2.5}/setup.py +0 -0
  22. {dronecot-2.2.3 → dronecot-2.2.5}/src/dronecot/ble_capture.py +0 -0
  23. {dronecot-2.2.3 → dronecot-2.2.5}/src/dronecot/ble_parse.py +0 -0
  24. {dronecot-2.2.3 → dronecot-2.2.5}/src/dronecot/classes.py +0 -0
  25. {dronecot-2.2.3 → dronecot-2.2.5}/src/dronecot/commands.py +0 -0
  26. {dronecot-2.2.3 → dronecot-2.2.5}/src/dronecot/dji_exceptions.py +0 -0
  27. {dronecot-2.2.3 → dronecot-2.2.5}/src/dronecot/dji_functions.py +0 -0
  28. {dronecot-2.2.3 → dronecot-2.2.5}/src/dronecot/dji_text_parser.py +0 -0
  29. {dronecot-2.2.3 → dronecot-2.2.5}/src/dronecot/odid.py +0 -0
  30. {dronecot-2.2.3 → dronecot-2.2.5}/src/dronecot/open_drone_id.py +0 -0
  31. {dronecot-2.2.3 → dronecot-2.2.5}/src/dronecot/rid_normalize.py +0 -0
  32. {dronecot-2.2.3 → dronecot-2.2.5}/src/dronecot/udp_rid.py +0 -0
  33. {dronecot-2.2.3 → dronecot-2.2.5}/src/dronecot/wifi_capture.py +0 -0
  34. {dronecot-2.2.3 → dronecot-2.2.5}/tests/test_adsb_vehicle.py +0 -0
  35. {dronecot-2.2.3 → dronecot-2.2.5}/tests/test_functions.py +0 -0
  36. {dronecot-2.2.3 → dronecot-2.2.5}/tests/test_rid_normalize.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dronecot
3
- Version: 2.2.3
3
+ Version: 2.2.5
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.2.3
3
+ Version: 2.2.5
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>
@@ -29,4 +29,6 @@ src/dronecot/wifi_capture.py
29
29
  src/dronecot/wifi_parse.py
30
30
  tests/test_adsb_vehicle.py
31
31
  tests/test_functions.py
32
- tests/test_rid_normalize.py
32
+ tests/test_rid_normalize.py
33
+ tests/test_sensor_detail.py
34
+ tests/test_wifi_parse.py
@@ -0,0 +1 @@
1
+ 2.2.5
@@ -32,6 +32,8 @@ from .constants import ( # NOQA
32
32
  DEFAULT_GPS_INFO_CMD,
33
33
  DEFAULT_SENSOR_ID,
34
34
  DEFAULT_SENSOR_PAYLOAD_TYPE,
35
+ DEFAULT_SENSOR_MODEL,
36
+ DEFAULT_SENSOR_TYPE,
35
37
  DEFAULT_SENSOR_COT_TYPE,
36
38
  DEFAULT_OP_COT_TYPE,
37
39
  DEFAULT_HOME_COT_TYPE,
@@ -41,6 +41,11 @@ DEFAULT_SENSOR_ID: str = f"dronecot_{hostname}"
41
41
 
42
42
  DEFAULT_SENSOR_PAYLOAD_TYPE: str = "Unknown-Sensor-Payload-Type"
43
43
 
44
+ # Receiver hardware model (e.g. "Atheros AR9271") and detection method /
45
+ # capability (e.g. "Wi-Fi Open Drone ID"). Set via SENSOR_MODEL / SENSOR_TYPE.
46
+ DEFAULT_SENSOR_MODEL: str = "Unknown-Sensor"
47
+ DEFAULT_SENSOR_TYPE: str = "Remote ID"
48
+
44
49
  DEFAULT_SENSOR_COT_TYPE: str = "a-f-G-E-S-E"
45
50
  DEFAULT_OP_COT_TYPE: str = "a-n-G"
46
51
  DEFAULT_UAS_COT_TYPE: str = "a-n-A-M-H-Q"
@@ -74,6 +74,19 @@ def _pacific_timestamp() -> str:
74
74
  return datetime.now(tzinfo).strftime("%Y-%m-%d %H:%M:%S %Z")
75
75
 
76
76
 
77
+ def _rf_band(channel) -> str:
78
+ """Map a Wi-Fi channel number to its RF band label (for SIGINT detail)."""
79
+ try:
80
+ ch = int(channel)
81
+ except (TypeError, ValueError):
82
+ return ""
83
+ if 1 <= ch <= 14:
84
+ return "2.4 GHz"
85
+ if 32 <= ch <= 196:
86
+ return "5 GHz"
87
+ return ""
88
+
89
+
77
90
  def _dji_feed_uses_text(config, feed_url: str, parsed) -> bool:
78
91
  """Return True if the configured DJI feed uses AntSDR text CSV format."""
79
92
  feed_format = str(config.get("FEED_FORMAT", "")).lower()
@@ -341,10 +354,19 @@ def rid_uas_to_cot_xml( # NOQA pylint: disable=too-many-locals,too-many-branche
341
354
  sensor_type = str(src_data.get("type", dronecot.DEFAULT_SENSOR_PAYLOAD_TYPE))
342
355
  rssi = str(src_data.get("RSSI"))
343
356
 
357
+ # Receiver / SIGINT detail: what hardware saw this, how, and on what RF band.
358
+ sensor_model = str(config.get("SENSOR_MODEL", dronecot.DEFAULT_SENSOR_MODEL))
359
+ sensor_method = str(config.get("SENSOR_TYPE", dronecot.DEFAULT_SENSOR_TYPE))
360
+ channel = src_data.get("channel")
361
+ band = _rf_band(channel)
362
+
344
363
  cuas: ET.Element = ET.Element("__cuas")
345
364
  cuas.set("sensor_id", sensor_id)
365
+ cuas.set("sensor_model", sensor_model)
366
+ cuas.set("sensor_method", sensor_method)
346
367
  cuas.set("rssi", rssi)
347
- cuas.set("channel", str(src_data.get("channel")))
368
+ cuas.set("channel", str(channel))
369
+ cuas.set("band", band)
348
370
  cuas.set("timestamp", str(src_data.get("timestamp")))
349
371
  cuas.set("mac_address", mac_address_text)
350
372
  cuas.set("type", sensor_type)
@@ -374,6 +396,11 @@ def rid_uas_to_cot_xml( # NOQA pylint: disable=too-many-locals,too-many-branche
374
396
  else:
375
397
  callsign = f"{uasid[0:4]}...{uasid[-4:]}"
376
398
 
399
+ if rssi and rssi not in ("None", ""):
400
+ remarks_fields.append(f"RSSI: {rssi} dBm")
401
+ if channel is not None:
402
+ remarks_fields.append(f"Channel: {channel}{f' ({band})' if band else ''}")
403
+ remarks_fields.append(f"Receiver: {sensor_model} — {sensor_method}")
377
404
  remarks_fields.append(f"Sensor: {sensor_id}")
378
405
 
379
406
  contact: ET.Element = ET.Element("contact")
@@ -157,9 +157,16 @@ def extract_odid_from_dot11(frame_bytes: bytes) -> Optional[Tuple[bytes, Dict[st
157
157
 
158
158
 
159
159
  def extract_odid_from_scapy_packet(packet) -> Optional[Tuple[bytes, Dict[str, Any]]]:
160
- """Extract ODID from a Scapy packet (Dot11 layer)."""
160
+ """Extract ODID from a Scapy packet (Dot11 layer).
161
+
162
+ Delegates to the byte-level ``extract_odid_from_dot11`` parser on the
163
+ serialized Dot11 frame. This avoids a class of Scapy field-type bugs — e.g.
164
+ ``Dot11EltVendorSpecific.oui`` is an int, so ``bytes(elt.oui)`` builds a huge
165
+ zero buffer instead of the 3 OUI bytes and the ASTM vendor element never
166
+ matches — and keeps Wi-Fi Beacon and NAN decoding on one proven code path.
167
+ """
161
168
  try:
162
- from scapy.layers.dot11 import Dot11, Dot11Beacon, Dot11EltVendorSpecific, RadioTap # noqa
169
+ from scapy.layers.dot11 import Dot11, RadioTap # noqa
163
170
  except ImportError:
164
171
  return None
165
172
 
@@ -167,40 +174,14 @@ def extract_odid_from_scapy_packet(packet) -> Optional[Tuple[bytes, Dict[str, An
167
174
  return None
168
175
 
169
176
  dot11 = packet.getlayer(Dot11)
170
- meta: Dict[str, Any] = {"MAC address": dot11.addr2 or dot11.addr3 or ""}
177
+ result = extract_odid_from_dot11(bytes(dot11))
178
+ if not result:
179
+ return None
171
180
 
172
- if packet.haslayer(RadioTap) and hasattr(packet[RadioTap], "dBm_AntSignal"):
173
- rssi = packet[RadioTap].dBm_AntSignal
181
+ pack, meta = result
182
+ # Enrich with Scapy-only signal info when present.
183
+ if packet.haslayer(RadioTap):
184
+ rssi = getattr(packet[RadioTap], "dBm_AntSignal", None)
174
185
  if rssi is not None:
175
186
  meta["RSSI"] = int(rssi)
176
-
177
- subtype = dot11.subtype
178
-
179
- if subtype == 8: # beacon
180
- elt = packet.getlayer(Dot11EltVendorSpecific)
181
- while elt:
182
- if hasattr(elt, "oui") and hasattr(elt, "info"):
183
- oui = bytes(elt.oui) if not isinstance(elt.oui, bytes) else elt.oui
184
- info = bytes(elt.info) if elt.info else b""
185
- ie_data = oui + info
186
- pack = parse_beacon_vendor_ie(ie_data)
187
- if pack:
188
- meta["type"] = "WiFi beacon"
189
- if dot11.IDinfo:
190
- meta["channel"] = int(getattr(dot11.IDinfo, "channel", 0) or 0)
191
- return pack, meta
192
- elt = elt.payload.getlayer(Dot11EltVendorSpecific)
193
- elif subtype == 13: # action
194
- raw = bytes(packet[Dot11].payload) if dot11.payload else b""
195
- if not raw and packet.haslayer("Raw"):
196
- raw = bytes(packet["Raw"].load)
197
- frame = bytes(dot11.build()) + raw if raw else bytes(packet.build())
198
- # Rebuild full frame for NAN parser
199
- full = bytes(packet.build())
200
- result = extract_odid_from_dot11(full)
201
- if result:
202
- pack, nan_meta = result
203
- meta.update(nan_meta)
204
- return pack, meta
205
-
206
- return None
187
+ return pack, meta
@@ -0,0 +1,54 @@
1
+ """Tests for the enriched sensor / SIGINT detail in the RID CoT payload."""
2
+
3
+ import xml.etree.ElementTree as ET
4
+
5
+ from dronecot.functions import _rf_band, rid_uas_to_cot_xml
6
+
7
+
8
+ def test_rf_band():
9
+ assert _rf_band(6) == "2.4 GHz"
10
+ assert _rf_band("6") == "2.4 GHz"
11
+ assert _rf_band(44) == "5 GHz"
12
+ assert _rf_band(None) == ""
13
+ assert _rf_band("nope") == ""
14
+
15
+
16
+ def _rid_data():
17
+ return {
18
+ "Latitude": 37.76,
19
+ "Longitude": -122.49,
20
+ "BasicID": "1787F04BM24010011195",
21
+ "SpeedHorizontal": 0,
22
+ "Direction": 0,
23
+ "AltitudeGeo": 10.0,
24
+ "data": {
25
+ "MAC address": "AA:BB:CC:DD:EE:FF",
26
+ "RSSI": -42,
27
+ "channel": 6,
28
+ "sensor_id": "wifi-rid",
29
+ },
30
+ }
31
+
32
+
33
+ def test_rid_cot_carries_sensor_detail():
34
+ config = {
35
+ "SENSOR_MODEL": "Atheros AR9271",
36
+ "SENSOR_TYPE": "Wi-Fi Open Drone ID",
37
+ }
38
+ event = rid_uas_to_cot_xml(_rid_data(), config)
39
+ assert event is not None
40
+ xml = ET.tostring(event, encoding="unicode")
41
+
42
+ cuas = event.find(".//__cuas")
43
+ assert cuas is not None
44
+ assert cuas.get("sensor_model") == "Atheros AR9271"
45
+ assert cuas.get("sensor_method") == "Wi-Fi Open Drone ID"
46
+ assert cuas.get("band") == "2.4 GHz"
47
+ assert cuas.get("rssi") == "-42"
48
+ assert cuas.get("channel") == "6"
49
+
50
+ remarks = event.find(".//remarks")
51
+ assert remarks is not None and remarks.text
52
+ assert "RSSI: -42 dBm" in remarks.text
53
+ assert "2.4 GHz" in remarks.text
54
+ assert "Atheros AR9271" in remarks.text
@@ -0,0 +1,53 @@
1
+ """Tests for Wi-Fi Open Drone ID frame parsing.
2
+
3
+ Regression for the Scapy beacon path: ``Dot11EltVendorSpecific.oui`` is an int,
4
+ so the old ``bytes(elt.oui)`` built a huge zero buffer and the ASTM vendor
5
+ element never matched — ``extract_odid_from_scapy_packet`` decoded nothing even
6
+ though ``extract_odid_from_dot11`` decoded the same frame.
7
+ """
8
+
9
+ import struct
10
+
11
+ import pytest
12
+
13
+ from dronecot import wifi_parse
14
+
15
+
16
+ def _astm_beacon_frame(pack_body: bytes) -> bytes:
17
+ """Build a raw 802.11 beacon carrying an ASTM ODID vendor IE."""
18
+ fc = struct.pack("<H", 0x0080) # mgmt, subtype 8 (beacon)
19
+ dur = b"\x00\x00"
20
+ dst = b"\xff\xff\xff\xff\xff\xff"
21
+ src = b"\xaa\xbb\xcc\xdd\xee\xff"
22
+ bssid = src
23
+ seq = b"\x00\x00"
24
+ fixed = b"\x00" * 8 + b"\x64\x00" + b"\x00\x00" # timestamp + interval + caps
25
+ # Vendor IE: OUI FA:0B:BC, type 0x0D, msg_counter 0x01, then the pack body.
26
+ ie_payload = wifi_parse.ASTM_WIFI_OUI + bytes([0x0D, 0x01]) + pack_body
27
+ ie = bytes([0xDD, len(ie_payload)]) + ie_payload
28
+ return fc + dur + dst + src + bssid + seq + fixed + ie
29
+
30
+
31
+ def test_extract_odid_from_dot11_beacon():
32
+ body = bytes(range(25))
33
+ frame = _astm_beacon_frame(body)
34
+ result = wifi_parse.extract_odid_from_dot11(frame)
35
+ assert result is not None
36
+ pack, meta = result
37
+ assert pack == body
38
+ assert meta.get("type") == "WiFi beacon"
39
+
40
+
41
+ def test_extract_odid_from_scapy_packet_matches_raw():
42
+ """The Scapy path must decode the same beacon the byte parser does."""
43
+ scapy_dot11 = pytest.importorskip("scapy.layers.dot11")
44
+ body = bytes(range(25))
45
+ frame = _astm_beacon_frame(body)
46
+ pkt = scapy_dot11.Dot11(frame)
47
+
48
+ raw = wifi_parse.extract_odid_from_dot11(frame)
49
+ viascapy = wifi_parse.extract_odid_from_scapy_packet(pkt)
50
+
51
+ assert raw is not None
52
+ assert viascapy is not None, "scapy path failed on a frame the byte parser decoded"
53
+ assert viascapy[0] == raw[0] == body
@@ -1 +0,0 @@
1
- 2.2.3
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes