Merge single ODID messages per transmitter; fix NUL bytes in CoT UIDs — v2.3.0 - #7
Conversation
A BLE legacy transmitter fits only ONE 25-byte ODID message per advertisement and rotates through message types, so the serial, the UAS position and the operator location arrive in different frames. DroneCOT rendered each frame independently, with three consequences in the field: * CoT XML could contain NUL bytes and fail to parse. ODID pads its fixed-width ASCII fields with NUL and clean_SN()/clean_string() stripped only whitespace, so any UAS with a serial shorter than the 20-byte field emitted a UID like RID.SHORTSN\0\0\0....uas — 82 NUL bytes in the document, illegal in XML 1.0. Affected every source; hid because every committed fixture uses a full-width 20-char serial. * Every drone collapsed onto one track. A Location message has a position but no serial, so it rendered as RID.Unknown-BasicID_0.uas for all aircraft in range. The UID now falls back to the advertiser MAC so distinct transmitters stay distinct. * BasicID and System messages were dropped, having no position. Adds rid_track.ODIDAggregator, keyed on the advertiser MAC (ASTM F3411 requires it to be stable for a flight session), TTL-expired and hard-capped to bound memory. Wired into RIDWorker.handle_data(), the one choke point every worker feeds, so Wi-Fi, BLE, serial, MQTT and UDP all benefit. Full 0xF message packs pass through unchanged. Also fixes rid_op_to_cot_xml() reading the MAC from the wrong nesting level, and replaces deprecated datetime.utcfromtimestamp(). 16 new tests, including a real ASTM F3411 advertisement captured off the air from a BlueMark DroneBeacon DB120 — the existing fixtures are all message packs or CUAS blobs and never exercised the single-message path. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0197da7dhvcPoHxYKamrYqyM
6a9e619 to
82b1b00
Compare
Validated against a live BlueMark DroneBeacon DB120 over 234 real BLE
advertisements captured through the BlueZ backend. The transmitter sent
BasicID, Location and System as SEPARATE advertisements and no 0xF
message packs at all, confirming the single-message rotation this work
exists to handle:
without aggregation 57/234 (24%) rendered; real serial NEVER appeared
-- every event used the MAC fallback UID
with aggregation 230/234 (98%) rendered, all under the correct
serial RID.1787F04BM24010011195.uas
Two fixes the live run exposed:
* RID_TRACK_ID_GRACE (default 5s). Rendering a Location before its
BasicID arrives emits CoT under a MAC-derived UID and then switches to
the real serial, so ONE aircraft became TWO TAK markers -- observed
directly: both RID.MAC-DF7211D26B95.uas and
RID.1787F04BM24010011195.uas for the same DB120. A position-only track
is now held briefly while waiting for the serial; after the grace
period it renders anyway, because an unidentified drone still needs to
be on the map. Set 0 to render immediately.
* ODIDAggregator.__bool__. __len__ made a freshly-constructed aggregator
FALSY, so the natural `if aggregator:` skipped aggregation until the
first track existed -- which never happens, because tracks are only
created by aggregating. Truthiness now means "there is an aggregator",
not "it is holding something". This silently defeated a comparison
harness before it was caught.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0197da7dhvcPoHxYKamrYqyM
Validated against live hardware ✅The "not yet verified" caveat in the description is now resolved. A BlueMark DroneBeacon DB120 was transmitting in range, and I captured 234 real BLE advertisements through the BlueZ backend (#8) and replayed the same frames through both code paths. The transmitter sent
So on real traffic the fix takes a source that produced no correctly-identified track at all and turns it into a clean one, while raising usable CoT yield from 24% to 98%. Two fixes the live run exposed (2174fdf)
Sample CoT off the wire, end to end: 61 tests pass, zero warnings. |
Why
A BLE legacy Remote ID transmitter fits only one 25-byte ODID message per advertisement, so it rotates through message types — the serial (BasicID), the aircraft position (Location) and the operator location (System) each arrive in a different frame. DroneCOT rendered every frame independently. Three things went wrong in the field.
1. CoT XML could contain NUL bytes and fail to parse
ODID pads its fixed-width ASCII fields (20-byte serial, 23-byte text) with NUL, and
clean_SN()/clean_string()stripped only whitespace. Any UAS whose serial was shorter than the field produced:NUL is illegal in XML 1.0, so TAK would reject the event. This affected every source (Wi-Fi, BLE, serial, MQTT). It stayed hidden because every committed fixture happens to use a full-width 20-character serial.
2. Every drone collapsed onto a single track
A Location message carries a position but no serial, so it rendered with the constant UID
RID.Unknown-BasicID_0.uas— every aircraft in range shared one track. The UID now falls back to the advertiser MAC (MAC-<addr>).3. BasicID and System messages were silently dropped
They have no position of their own, so
rid_uas_to_cot_xml()returnedNone.What changed
rid_track.ODIDAggregator— merges successive single ODID messages per transmitter, keyed on the advertiser MAC (ASTM F3411 requires it to stay constant for a flight session, and it is the only field present on every message type). TTL-expired and hard-capped so the table can't grow without bound.RIDWorker.handle_data()— the single choke point every worker feeds vianet_queue— so Wi-Fi, BLE, serial, MQTT and UDP all benefit from one call site. Sensor-status beacons bypass it.0xFmessage packs pass through unchanged; they already carry everything in one frame.rid_op_to_cot_xml()readingdata["MAC address"]when wireless captures put it indata["data"], so theop-<mac>UID never actually used a MAC.datetime.utcfromtimestamp().Config
RID_TRACK_TTL1200disables aggregation.RID_TRACK_MAX512Tests
38 pass, zero warnings (
-W error::DeprecationWarning). 16 new, including a real ASTM F3411 advertisement captured off the air withbtmonfrom a BlueMark DroneBeacon DB120 — the existing fixtures are all message packs or CUAS blobs and never exercised the single-message path at all.Key regressions covered: two MACs stay two tracks; a lone Location yields a MAC-derived UID rather than
Unknown-BasicID_0; short serials produce CoT that reparses; TTL and max-tracks bound the table; packs are unchanged.Not yet verified
Aggregation is validated against synthetic and captured frames, but not yet against a live multi-drone flight. Worth a HIL run on the existing Sniffle/DS101 boxes before this goes to the fleet apt repo.
🤖 Generated with Claude Code
https://claude.ai/code/session_0197da7dhvcPoHxYKamrYqyM