dronecot 2.3.0__tar.gz → 2.3.1__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 (40) hide show
  1. {dronecot-2.3.0 → dronecot-2.3.1}/CHANGELOG.md +23 -0
  2. {dronecot-2.3.0/dronecot.egg-info → dronecot-2.3.1}/PKG-INFO +1 -1
  3. {dronecot-2.3.0 → dronecot-2.3.1/dronecot.egg-info}/PKG-INFO +1 -1
  4. dronecot-2.3.1/src/dronecot/VERSION +1 -0
  5. {dronecot-2.3.0 → dronecot-2.3.1}/src/dronecot/functions.py +73 -8
  6. {dronecot-2.3.0 → dronecot-2.3.1}/src/dronecot/rid_track.py +15 -2
  7. {dronecot-2.3.0 → dronecot-2.3.1}/tests/test_functions.py +20 -8
  8. {dronecot-2.3.0 → dronecot-2.3.1}/tests/test_rid_track.py +99 -0
  9. dronecot-2.3.0/src/dronecot/VERSION +0 -1
  10. {dronecot-2.3.0 → dronecot-2.3.1}/LICENSE +0 -0
  11. {dronecot-2.3.0 → dronecot-2.3.1}/MANIFEST.in +0 -0
  12. {dronecot-2.3.0 → dronecot-2.3.1}/README.md +0 -0
  13. {dronecot-2.3.0 → dronecot-2.3.1}/dronecot.egg-info/SOURCES.txt +0 -0
  14. {dronecot-2.3.0 → dronecot-2.3.1}/dronecot.egg-info/dependency_links.txt +0 -0
  15. {dronecot-2.3.0 → dronecot-2.3.1}/dronecot.egg-info/entry_points.txt +0 -0
  16. {dronecot-2.3.0 → dronecot-2.3.1}/dronecot.egg-info/requires.txt +0 -0
  17. {dronecot-2.3.0 → dronecot-2.3.1}/dronecot.egg-info/top_level.txt +0 -0
  18. {dronecot-2.3.0 → dronecot-2.3.1}/setup.cfg +0 -0
  19. {dronecot-2.3.0 → dronecot-2.3.1}/setup.py +0 -0
  20. {dronecot-2.3.0 → dronecot-2.3.1}/src/dronecot/__init__.py +0 -0
  21. {dronecot-2.3.0 → dronecot-2.3.1}/src/dronecot/ble_capture.py +0 -0
  22. {dronecot-2.3.0 → dronecot-2.3.1}/src/dronecot/ble_parse.py +0 -0
  23. {dronecot-2.3.0 → dronecot-2.3.1}/src/dronecot/bluez_capture.py +0 -0
  24. {dronecot-2.3.0 → dronecot-2.3.1}/src/dronecot/classes.py +0 -0
  25. {dronecot-2.3.0 → dronecot-2.3.1}/src/dronecot/commands.py +0 -0
  26. {dronecot-2.3.0 → dronecot-2.3.1}/src/dronecot/constants.py +0 -0
  27. {dronecot-2.3.0 → dronecot-2.3.1}/src/dronecot/dji_exceptions.py +0 -0
  28. {dronecot-2.3.0 → dronecot-2.3.1}/src/dronecot/dji_functions.py +0 -0
  29. {dronecot-2.3.0 → dronecot-2.3.1}/src/dronecot/dji_text_parser.py +0 -0
  30. {dronecot-2.3.0 → dronecot-2.3.1}/src/dronecot/odid.py +0 -0
  31. {dronecot-2.3.0 → dronecot-2.3.1}/src/dronecot/open_drone_id.py +0 -0
  32. {dronecot-2.3.0 → dronecot-2.3.1}/src/dronecot/rid_normalize.py +0 -0
  33. {dronecot-2.3.0 → dronecot-2.3.1}/src/dronecot/udp_rid.py +0 -0
  34. {dronecot-2.3.0 → dronecot-2.3.1}/src/dronecot/wifi_capture.py +0 -0
  35. {dronecot-2.3.0 → dronecot-2.3.1}/src/dronecot/wifi_parse.py +0 -0
  36. {dronecot-2.3.0 → dronecot-2.3.1}/tests/test_adsb_vehicle.py +0 -0
  37. {dronecot-2.3.0 → dronecot-2.3.1}/tests/test_bluez_capture.py +0 -0
  38. {dronecot-2.3.0 → dronecot-2.3.1}/tests/test_rid_normalize.py +0 -0
  39. {dronecot-2.3.0 → dronecot-2.3.1}/tests/test_sensor_detail.py +0 -0
  40. {dronecot-2.3.0 → dronecot-2.3.1}/tests/test_wifi_parse.py +0 -0
@@ -1,3 +1,26 @@
1
+ ## DroneCOT 2.3.1
2
+
3
+ - **Fix: CoT `ce`/`le` reported ODID accuracy ENUM CODES as if they were metres.**
4
+ ASTM F3411 accuracy fields are enum codes; CoT `<point ce= le=>` are metres.
5
+ Emitting the raw code claimed precision the aircraft never reported, and the
6
+ worst case inverted the meaning entirely: code `0` means *"unknown / >= 18.52
7
+ km"* but rendered as `ce="0"` — a perfect fix. Observed live on a DroneBeacon
8
+ DB120: `HorizAccuracy=9` (ODID *< 30 m*) was drawn on the map at **9 m**, and
9
+ `BaroAccuracy=0` (unknown) as **zero error**. Codes now decode to metres via
10
+ tables mirroring `decodeHorizontalAccuracy()`/`decodeVerticalAccuracy()` in
11
+ opendroneid-core-c; unknown and reserved codes emit the CoT unknown sentinel
12
+ rather than a fabricated number. Four existing tests asserted the old raw
13
+ codes and have been corrected.
14
+ - **Fix: absent altitude rendered as sea level.** `<height value>` defaulted to
15
+ `0`; an unknown altitude now uses the CoT unknown sentinel like `hae`/`ce`/`le`.
16
+ - **Fix: MAC-less feeds shared one placeholder UID.** Serial/MAVLink receivers
17
+ (DroneScout Bridge, SiK) report no MAC, so a Location- or System-only message
18
+ had neither MAC nor serial and rendered as `Unknown-BasicID_0`, colliding
19
+ across every such source. The UID now falls back to `FEED-<sensor_id>`.
20
+ Measured on a live DroneScout Bridge: 57 correctly identified events alongside
21
+ 3 placeholders. Aggregation is deliberately NOT keyed on the feed — one
22
+ receiver reports many aircraft, so that would merge distinct drones.
23
+
1
24
  ## DroneCOT 2.3.0
2
25
 
3
26
  Correctness release for single-message Remote ID. A BLE legacy transmitter fits
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dronecot
3
- Version: 2.3.0
3
+ Version: 2.3.1
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.0
3
+ Version: 2.3.1
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>
@@ -0,0 +1 @@
1
+ 2.3.1
@@ -190,6 +190,60 @@ def _cot_event_with_detail(
190
190
  )
191
191
 
192
192
 
193
+ # ASTM F3411 / Open Drone ID accuracy fields are ENUM CODES, not metres. CoT
194
+ # <point ce=/le=> are metres. Emitting the raw code claims a precision the
195
+ # aircraft never reported -- and the worst case inverts the meaning entirely:
196
+ # code 0 means "unknown / >= 18.52 km" but rendered as ce="0", i.e. a PERFECT
197
+ # fix. Observed live: a DroneBeacon reporting HorizAccuracy=9 (<30 m) was drawn
198
+ # on the map at 9 m, and BaroAccuracy=0 (unknown) as zero error.
199
+ #
200
+ # Values mirror decodeHorizontalAccuracy()/decodeVerticalAccuracy() in
201
+ # opendroneid-core-c. Each code is an upper bound ("less than X"), so using X is
202
+ # the conservative reading.
203
+ CE_LE_UNKNOWN = "9999999.0"
204
+
205
+ ODID_HORIZ_ACCURACY_M = {
206
+ 1: 18520.0, # 10 NM
207
+ 2: 7408.0, # 4 NM
208
+ 3: 3704.0, # 2 NM
209
+ 4: 1852.0, # 1 NM
210
+ 5: 926.0, # 0.5 NM
211
+ 6: 555.6, # 0.3 NM
212
+ 7: 185.2, # 0.1 NM
213
+ 8: 92.6, # 0.05 NM
214
+ 9: 30.0,
215
+ 10: 10.0,
216
+ 11: 3.0,
217
+ 12: 1.0,
218
+ }
219
+
220
+ ODID_VERT_ACCURACY_M = {1: 150.0, 2: 45.0, 3: 25.0, 4: 10.0, 5: 3.0, 6: 1.0}
221
+
222
+
223
+ def _odid_accuracy_m(value, table) -> str:
224
+ """Map an ODID accuracy enum code to metres for CoT ce/le.
225
+
226
+ Returns the CoT unknown sentinel for code 0 (explicitly "unknown"), for any
227
+ unlisted/reserved code, and for a missing value -- never a fabricated 0.
228
+ """
229
+ if value is None:
230
+ return CE_LE_UNKNOWN
231
+ try:
232
+ code = int(value)
233
+ except (TypeError, ValueError):
234
+ return CE_LE_UNKNOWN
235
+ metres = table.get(code)
236
+ return CE_LE_UNKNOWN if metres is None else str(metres)
237
+
238
+
239
+ def _odid_horiz_ce(data: dict) -> str:
240
+ return _odid_accuracy_m(data.get("HorizAccuracy"), ODID_HORIZ_ACCURACY_M)
241
+
242
+
243
+ def _odid_vert_le(data: dict) -> str:
244
+ return _odid_accuracy_m(data.get("VertAccuracy"), ODID_VERT_ACCURACY_M)
245
+
246
+
193
247
  def _rid_identity(data: dict) -> Tuple[str, Optional[str]]:
194
248
  """Resolve a stable UAS identifier and the advertiser MAC for CoT UIDs.
195
249
 
@@ -208,7 +262,16 @@ def _rid_identity(data: dict) -> Tuple[str, Optional[str]]:
208
262
  uasid = data.get("BasicID") or data.get("BasicID_0")
209
263
  uasid = str(uasid).strip() if uasid else ""
210
264
  if not uasid:
211
- uasid = f"MAC-{mac.replace(':', '').upper()}" if mac else "Unknown-BasicID_0"
265
+ if mac:
266
+ uasid = f"MAC-{mac.replace(':', '').upper()}"
267
+ else:
268
+ # Neither serial nor MAC: serial/MAVLink receivers report no MAC at
269
+ # all. Fall back to the feed that delivered it so two receivers do
270
+ # not collide, rather than the shared Unknown-BasicID_0 placeholder
271
+ # that put every such aircraft on one track.
272
+ feed = src_data.get("sensor_id") or src_data.get("sensor ID")
273
+ feed = str(feed).strip() if feed else ""
274
+ uasid = f"FEED-{feed}" if feed else "Unknown-BasicID_0"
212
275
 
213
276
  return uasid, mac
214
277
 
@@ -298,8 +361,8 @@ def rid_op_to_cot_xml( # NOQA pylint: disable=too-many-locals,too-many-branches
298
361
  stale=cot_stale,
299
362
  lat=lat,
300
363
  lon=lon,
301
- ce=str(data.get("HorizAccuracy", "9999999.0")),
302
- le=str(data.get("VertAccuracy", "9999999.0")),
364
+ ce=_odid_horiz_ce(data),
365
+ le=_odid_vert_le(data),
303
366
  hae=str(data.get("OperatorAltitudeGeo", "9999999.0")),
304
367
  detail=detail,
305
368
  config=config,
@@ -365,7 +428,9 @@ def rid_uas_to_cot_xml( # NOQA pylint: disable=too-many-locals,too-many-branche
365
428
  track.set("course", str(data.get("Direction", 0)))
366
429
 
367
430
  height: ET.Element = ET.Element("height")
368
- height.set("value", str(data.get("AltitudeGeo", 0)))
431
+ # Never default to 0: an absent altitude is unknown, not sea level. Matches
432
+ # the CoT unknown sentinel used for hae/ce/le above.
433
+ height.set("value", str(data.get("AltitudeGeo", CE_LE_UNKNOWN)))
369
434
 
370
435
  # link: ET.Element = ET.Element("link")
371
436
  # link.set("uid", op_uid)
@@ -490,8 +555,8 @@ def rid_uas_to_cot_xml( # NOQA pylint: disable=too-many-locals,too-many-branche
490
555
  stale=cot_stale,
491
556
  lat=lat,
492
557
  lon=lon,
493
- ce=str(data.get("HorizAccuracy", "9999999.0")),
494
- le=str(data.get("VertAccuracy", "9999999.0")),
558
+ ce=_odid_horiz_ce(data),
559
+ le=_odid_vert_le(data),
495
560
  hae=str(data.get("AltitudeGeo", "9999999.0")),
496
561
  detail=detail,
497
562
  config=config,
@@ -572,8 +637,8 @@ def sensor_status_to_cot( # NOQA pylint: disable=too-many-locals,too-many-branc
572
637
  stale=cot_stale,
573
638
  lat=lat,
574
639
  lon=lon,
575
- ce=str(data.get("HorizAccuracy", "9999999.0")),
576
- le=str(data.get("VertAccuracy", "9999999.0")),
640
+ ce=_odid_horiz_ce(data),
641
+ le=_odid_vert_le(data),
577
642
  hae=hae,
578
643
  detail=detail,
579
644
  config=config,
@@ -94,8 +94,21 @@ def _is_empty(value: Any) -> bool:
94
94
  def track_key(rid: dict) -> Optional[Tuple[str, str]]:
95
95
  """Return a ``(kind, value)`` aggregation key for a normalized RID dict.
96
96
 
97
- Returns ``None`` when the record identifies no transmitter at all, in which
98
- case it cannot be safely merged with anything and should pass through.
97
+ Keyed on the advertiser MAC, falling back to the transmitter's own serial.
98
+
99
+ Deliberately NOT keyed on the feed/sensor when neither is present. It is
100
+ tempting -- a MAC-less serial receiver looks like a single stream -- but a
101
+ DroneScout-style receiver reports MANY aircraft over one port, so feeding
102
+ them all into one key would merge distinct drones into a single track: the
103
+ exact bug this module exists to fix, reintroduced by another route. It also
104
+ splits rather than merges, because a BasicID message would key on the serial
105
+ while a Location message from the same aircraft keyed on the feed.
106
+
107
+ So a record with neither MAC nor serial passes through unaggregated, and
108
+ only its rendered UID falls back to the feed (see functions._rid_identity)
109
+ so that unidentified contacts from different receivers stay distinct.
110
+
111
+ Returns ``None`` when the record identifies no transmitter at all.
99
112
  """
100
113
  meta = rid.get(_META_KEY) or {}
101
114
  mac = meta.get("MAC address")
@@ -73,8 +73,11 @@ class FunctionsTestCase(unittest.TestCase):
73
73
  self.assertIsNotNone(point)
74
74
  self.assertEqual(point.get("lat"), "37.7599")
75
75
  self.assertEqual(point.get("lon"), "-122.4977")
76
- self.assertEqual(point.get("ce"), "12")
77
- self.assertEqual(point.get("le"), "5")
76
+ # ODID accuracy codes, decoded to METRES for CoT: HorizAccuracy 12 is
77
+ # ODID_HOR_ACC_1_METER and VertAccuracy 5 is ODID_VER_ACC_3_METER. These
78
+ # previously asserted the raw codes "12"/"5", which claimed 12 m / 5 m.
79
+ self.assertEqual(point.get("ce"), "1.0")
80
+ self.assertEqual(point.get("le"), "3.0")
78
81
  self.assertEqual(point.get("hae"), "28.0")
79
82
 
80
83
  detail = cot_xml.find("detail")
@@ -113,8 +116,11 @@ class FunctionsTestCase(unittest.TestCase):
113
116
  self.assertIsNotNone(point)
114
117
  self.assertEqual(point.get("lat"), "37.7599")
115
118
  self.assertEqual(point.get("lon"), "-122.4977")
116
- self.assertEqual(point.get("ce"), "12")
117
- self.assertEqual(point.get("le"), "5")
119
+ # ODID accuracy codes, decoded to METRES for CoT: HorizAccuracy 12 is
120
+ # ODID_HOR_ACC_1_METER and VertAccuracy 5 is ODID_VER_ACC_3_METER. These
121
+ # previously asserted the raw codes "12"/"5", which claimed 12 m / 5 m.
122
+ self.assertEqual(point.get("ce"), "1.0")
123
+ self.assertEqual(point.get("le"), "3.0")
118
124
  self.assertEqual(point.get("hae"), "28.0")
119
125
 
120
126
  detail = cot_xml.find("detail")
@@ -153,8 +159,11 @@ class FunctionsTestCase(unittest.TestCase):
153
159
  self.assertIsNotNone(point)
154
160
  self.assertEqual(point.get("lat"), "37.7599")
155
161
  self.assertEqual(point.get("lon"), "-122.4977")
156
- self.assertEqual(point.get("ce"), "12")
157
- self.assertEqual(point.get("le"), "5")
162
+ # ODID accuracy codes, decoded to METRES for CoT: HorizAccuracy 12 is
163
+ # ODID_HOR_ACC_1_METER and VertAccuracy 5 is ODID_VER_ACC_3_METER. These
164
+ # previously asserted the raw codes "12"/"5", which claimed 12 m / 5 m.
165
+ self.assertEqual(point.get("ce"), "1.0")
166
+ self.assertEqual(point.get("le"), "3.0")
158
167
  self.assertEqual(point.get("hae"), "28.0")
159
168
 
160
169
  detail = cot_xml.find("detail")
@@ -193,8 +202,11 @@ class FunctionsTestCase(unittest.TestCase):
193
202
  self.assertIsNotNone(point)
194
203
  self.assertEqual(point.get("lat"), "37.7599")
195
204
  self.assertEqual(point.get("lon"), "-122.4977")
196
- self.assertEqual(point.get("ce"), "12")
197
- self.assertEqual(point.get("le"), "5")
205
+ # ODID accuracy codes, decoded to METRES for CoT: HorizAccuracy 12 is
206
+ # ODID_HOR_ACC_1_METER and VertAccuracy 5 is ODID_VER_ACC_3_METER. These
207
+ # previously asserted the raw codes "12"/"5", which claimed 12 m / 5 m.
208
+ self.assertEqual(point.get("ce"), "1.0")
209
+ self.assertEqual(point.get("le"), "3.0")
198
210
  self.assertEqual(point.get("hae"), "28.0")
199
211
 
200
212
  detail = cot_xml.find("detail")
@@ -267,6 +267,56 @@ class TestFieldPadding(unittest.TestCase):
267
267
  self.assertEqual(event.get("uid"), "RID.SHORTSN.uas")
268
268
 
269
269
 
270
+ class TestMaclessSources(unittest.TestCase):
271
+ """Serial/MAVLink receivers report no MAC at all.
272
+
273
+ Measured on a live DroneScout Bridge: 57 correctly identified events
274
+ alongside 3 rendered under the shared ``Unknown-BasicID_0`` placeholder.
275
+
276
+ These records must NOT be aggregated on the feed -- one receiver reports
277
+ many aircraft, so a feed key would merge distinct drones. Instead they pass
278
+ through, and only the rendered UID falls back to the feed so contacts from
279
+ different receivers stay distinct.
280
+ """
281
+
282
+ @staticmethod
283
+ def _serial_rid(msg, sensor="dronescout"):
284
+ """Normalize a message the way a serial worker does: NO MAC address."""
285
+ return rid_normalize.bytes_to_rid_dict(
286
+ msg, {"sensor_id": sensor, "type": "MAVLink"}
287
+ )
288
+
289
+ def test_macless_without_serial_uses_feed_not_placeholder(self):
290
+ """The fix: never render under the shared placeholder UID."""
291
+ rid = self._serial_rid(_system_message(51.5, -0.12))
292
+ uid, mac = functions._rid_identity(rid)
293
+ self.assertIsNone(mac)
294
+ self.assertEqual(uid, "FEED-dronescout")
295
+ self.assertNotEqual(uid, "Unknown-BasicID_0")
296
+
297
+ def test_different_receivers_stay_distinct(self):
298
+ a = functions._rid_identity(self._serial_rid(_system_message(1.0, 2.0), "rx-a"))[0]
299
+ b = functions._rid_identity(self._serial_rid(_system_message(3.0, 4.0), "rx-b"))[0]
300
+ self.assertNotEqual(a, b)
301
+
302
+ def test_serial_wins_over_feed_in_uid(self):
303
+ rid = self._serial_rid(_basic_id_message("SERIAL9"))
304
+ self.assertEqual(functions._rid_identity(rid)[0], "SERIAL9")
305
+
306
+ def test_feed_is_NOT_used_as_an_aggregation_key(self):
307
+ """Guard against merging distinct aircraft from one multi-drone feed."""
308
+ rid = self._serial_rid(_location_message(51.5, -0.12))
309
+ self.assertIsNone(
310
+ rid_track.track_key(rid),
311
+ "a MAC-less, serial-less record must not be keyed on its feed",
312
+ )
313
+
314
+ def test_mac_still_wins_when_present(self):
315
+ rid = _rid(_location_message(1.0, 2.0), "AA:BB:CC:DD:EE:FF")
316
+ rid["data"]["sensor_id"] = "dronescout"
317
+ self.assertEqual(rid_track.track_key(rid)[0], "mac")
318
+
319
+
270
320
  class TestRidIdentity(unittest.TestCase):
271
321
  def test_missing_basic_id_falls_back_to_mac(self):
272
322
  rid = _rid(_location_message(1.0, 2.0), "DF:72:11:D2:6B:95")
@@ -300,3 +350,52 @@ class TestRidIdentity(unittest.TestCase):
300
350
 
301
351
  if __name__ == "__main__":
302
352
  unittest.main()
353
+
354
+
355
+ class TestAccuracyEnums(unittest.TestCase):
356
+ """ODID accuracy fields are enum CODES; CoT ce/le are METRES.
357
+
358
+ Emitting the raw code claims precision the aircraft never reported. Worst
359
+ case inverts the meaning: code 0 means "unknown / >= 18.52 km" but rendered
360
+ as ce="0" -- a perfect fix. Observed live on a DroneBeacon: HorizAccuracy=9
361
+ (<30 m) drawn at 9 m, BaroAccuracy=0 (unknown) as zero error.
362
+ """
363
+
364
+ def test_horizontal_codes_map_to_metres(self):
365
+ # Mirrors decodeHorizontalAccuracy() in opendroneid-core-c.
366
+ for code, metres in ((9, "30.0"), (10, "10.0"), (11, "3.0"), (12, "1.0"),
367
+ (1, "18520.0"), (6, "555.6")):
368
+ self.assertEqual(functions._odid_horiz_ce({"HorizAccuracy": code}), metres)
369
+
370
+ def test_vertical_codes_map_to_metres(self):
371
+ for code, metres in ((1, "150.0"), (2, "45.0"), (4, "10.0"), (6, "1.0")):
372
+ self.assertEqual(functions._odid_vert_le({"VertAccuracy": code}), metres)
373
+
374
+ def test_unknown_code_zero_is_not_perfect_precision(self):
375
+ """The dangerous inversion: 0 means UNKNOWN, never zero error."""
376
+ self.assertEqual(functions._odid_horiz_ce({"HorizAccuracy": 0}),
377
+ functions.CE_LE_UNKNOWN)
378
+ self.assertEqual(functions._odid_vert_le({"VertAccuracy": 0}),
379
+ functions.CE_LE_UNKNOWN)
380
+
381
+ def test_missing_or_reserved_codes_are_unknown(self):
382
+ for probe in ({}, {"HorizAccuracy": None}, {"HorizAccuracy": 15},
383
+ {"HorizAccuracy": "junk"}):
384
+ self.assertEqual(functions._odid_horiz_ce(probe), functions.CE_LE_UNKNOWN)
385
+
386
+ def test_end_to_end_cot_uses_metres_not_the_code(self):
387
+ pack = (bytes([0xF0, ODID_MESSAGE_SIZE, 2])
388
+ + _basic_id_message("SERIAL1") + _location_message(37.7, -122.4))
389
+ rid = rid_normalize.bytes_to_rid_dict(pack, {"MAC address": "AA:BB:CC:DD:EE:FF"})
390
+ rid["HorizAccuracy"] = 9 # ODID: < 30 m
391
+ rid["VertAccuracy"] = 2 # ODID: < 45 m
392
+ point = functions.rid_uas_to_cot_xml(rid, {}).find("point")
393
+ self.assertEqual(point.get("ce"), "30.0")
394
+ self.assertEqual(point.get("le"), "45.0")
395
+
396
+ def test_absent_altitude_is_not_rendered_as_sea_level(self):
397
+ rid = _rid(_location_message(1.0, 2.0), "AA:BB:CC:DD:EE:FF")
398
+ rid.pop("AltitudeGeo", None)
399
+ event = functions.rid_uas_to_cot_xml(rid, {})
400
+ height = event.find(".//height")
401
+ self.assertEqual(height.get("value"), functions.CE_LE_UNKNOWN)
@@ -1 +0,0 @@
1
- 2.3.0
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes