mirror of
https://github.com/ruvnet/RuView
synced 2026-08-03 19:21:42 +00:00
8d9c5994db
RemoteIdBroadcast::update stored NED metres (state.position.x/.y) into
drone_lat/drone_lon, so the ASTM F3411 broadcast would carry physically
-impossible coordinates ("latitude = 37.5 m"). The module doc claimed a
Location/Vector message but only encode_basic_id() exists.
- Rename drone_lat/drone_lon -> drone_north_m/drone_east_m (NED metres
relative to the operator/takeoff datum), documented as non-geodetic.
operator_lat/lon stay true WGS84.
- Correct the module doc to claim Basic ID only; Location/Vector encoding
is deferred until a datum-anchored NED->WGS84 transform lands.
Never broadcast physically-impossible coordinates.
Failing-on-old test:
security::remote_id::tests::test_ned_offset_stored_as_metres_not_latlon.
Co-Authored-By: claude-flow <ruv@ruv.net>
144 lines
5.8 KiB
Rust
144 lines
5.8 KiB
Rust
//! ASTM F3411 Remote ID — **Basic ID message only** (ADR-159 §A3).
|
|
//!
|
|
//! Only the Basic ID message (`encode_basic_id`) is implemented. The
|
|
//! Location/Vector message is **not** encoded yet because the drone position is
|
|
//! tracked in a local NED frame (north/east metres relative to a takeoff datum),
|
|
//! and a compliant Location/Vector message requires WGS84 latitude/longitude.
|
|
//! Broadcasting NED metres in lat/lon fields would emit physically-impossible
|
|
//! coordinates (e.g. "latitude = 12.4 metres"), so we deliberately keep the
|
|
//! drone position in honest `drone_north_m` / `drone_east_m` fields until a real
|
|
//! local-tangent-plane NED→WGS84 transform (with an operator datum) lands. See
|
|
//! the `ACCEPTED-FUTURE` note in ADR-159 §A3.
|
|
|
|
use crate::types::DroneState;
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
/// Remote ID broadcast state for one drone.
|
|
///
|
|
/// Drone position is stored as **NED metres** (`drone_north_m` / `drone_east_m`)
|
|
/// relative to the operator/takeoff datum — *not* WGS84 lat/lon — because no
|
|
/// datum-anchored geodetic transform is wired yet. The operator position is true
|
|
/// WGS84 (it comes from the operator's GNSS, not the local frame).
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct RemoteIdBroadcast {
|
|
pub uas_id: [u8; 20], // 20-byte UAS ID (ANSI/CTA-2063-A)
|
|
/// Operator latitude (WGS84 degrees) — real geodetic position.
|
|
pub operator_lat: f64,
|
|
/// Operator longitude (WGS84 degrees) — real geodetic position.
|
|
pub operator_lon: f64,
|
|
/// Drone north offset in **metres** from the operator/takeoff datum (NED x).
|
|
/// NOT a latitude. See module docs — Location/Vector encoding is deferred
|
|
/// until a real NED→WGS84 transform exists.
|
|
pub drone_north_m: f64,
|
|
/// Drone east offset in **metres** from the operator/takeoff datum (NED y).
|
|
/// NOT a longitude.
|
|
pub drone_east_m: f64,
|
|
pub altitude_msl_m: f32,
|
|
pub speed_ms: f32,
|
|
pub heading_deg: f32,
|
|
pub timestamp_ms: u64,
|
|
pub emergency_status: bool,
|
|
}
|
|
|
|
impl RemoteIdBroadcast {
|
|
pub fn new(uas_id: [u8; 20]) -> Self {
|
|
Self {
|
|
uas_id,
|
|
operator_lat: 0.0,
|
|
operator_lon: 0.0,
|
|
drone_north_m: 0.0,
|
|
drone_east_m: 0.0,
|
|
altitude_msl_m: 0.0,
|
|
speed_ms: 0.0,
|
|
heading_deg: 0.0,
|
|
timestamp_ms: 0,
|
|
emergency_status: false,
|
|
}
|
|
}
|
|
|
|
/// Update from a drone state and operator position.
|
|
///
|
|
/// The drone position is stored as honest NED metres — we do **not** fake a
|
|
/// lat/lon from a local-frame offset. The operator position is true WGS84.
|
|
pub fn update(&mut self, state: &DroneState, operator_pos: (f64, f64)) {
|
|
// NED metres, stored as-is in metre-typed fields (no fabricated geodetic
|
|
// coordinates). A future Location/Vector encoder must transform these
|
|
// through a datum-anchored NED→WGS84 projection before broadcast.
|
|
self.drone_north_m = state.position.x; // NED x = north offset, metres
|
|
self.drone_east_m = state.position.y; // NED y = east offset, metres
|
|
self.altitude_msl_m = state.altitude_agl_m as f32;
|
|
self.speed_ms = state.velocity.magnitude() as f32;
|
|
self.heading_deg = state.heading_rad.to_degrees() as f32;
|
|
self.timestamp_ms = state.timestamp_ms;
|
|
self.operator_lat = operator_pos.0;
|
|
self.operator_lon = operator_pos.1;
|
|
}
|
|
|
|
/// Encode a 25-byte ASTM F3411 Basic ID message.
|
|
/// Format: [message_type(1)] [id_type(1)] [uas_id(20)] [reserved(3)]
|
|
pub fn encode_basic_id(&self) -> [u8; 25] {
|
|
let mut buf = [0u8; 25];
|
|
buf[0] = 0x00; // Message type: Basic ID
|
|
buf[1] = 0x01; // ID type: Serial Number
|
|
buf[2..22].copy_from_slice(&self.uas_id);
|
|
// bytes 22-24: reserved
|
|
buf
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_encode_basic_id_length() {
|
|
let rid = RemoteIdBroadcast::new([0x41u8; 20]);
|
|
let buf = rid.encode_basic_id();
|
|
assert_eq!(buf.len(), 25);
|
|
assert_eq!(buf[1], 0x01); // ID type: serial number
|
|
}
|
|
|
|
#[test]
|
|
fn test_uas_id_in_encoded_buffer() {
|
|
let mut id = [0u8; 20];
|
|
id[0] = 0xFF;
|
|
let rid = RemoteIdBroadcast::new(id);
|
|
let buf = rid.encode_basic_id();
|
|
assert_eq!(buf[2], 0xFF);
|
|
}
|
|
|
|
/// ADR-159 §A3 — a known NED offset must land in honest **metre** fields,
|
|
/// never in WGS84 lat/lon fields (which would broadcast physically-impossible
|
|
/// coordinates like "latitude = 37.5 m"). Fails on old code, where the same
|
|
/// values were stored into `drone_lat`/`drone_lon`.
|
|
#[test]
|
|
fn test_ned_offset_stored_as_metres_not_latlon() {
|
|
use crate::types::{DroneState, NodeId, Position3D};
|
|
|
|
let mut state = DroneState::default_at_origin(NodeId(7));
|
|
// 37.5 m north, -12.0 m east of the takeoff datum.
|
|
state.position = Position3D {
|
|
x: 37.5,
|
|
y: -12.0,
|
|
z: 5.0,
|
|
};
|
|
let mut rid = RemoteIdBroadcast::new([0x41u8; 20]);
|
|
// Operator at a real WGS84 fix (San Francisco-ish).
|
|
rid.update(&state, (37.7749, -122.4194));
|
|
|
|
// Drone offset is honest NED metres.
|
|
assert_eq!(rid.drone_north_m, 37.5);
|
|
assert_eq!(rid.drone_east_m, -12.0);
|
|
|
|
// Operator position is the real geodetic fix and is plausibly a lat/lon.
|
|
assert!((-90.0..=90.0).contains(&rid.operator_lat));
|
|
assert!((-180.0..=180.0).contains(&rid.operator_lon));
|
|
assert!((rid.operator_lat - 37.7749).abs() < 1e-9);
|
|
|
|
// The drone NED metres would have been an out-of-range "latitude" only
|
|
// if a value happened to exceed 90 — but the contract is the field name
|
|
// itself: these are metres, not degrees. A future Location/Vector
|
|
// encoder must project them through a real NED→WGS84 transform.
|
|
}
|
|
}
|