fix(ruview-swarm): honest NED metres in Remote ID, not WGS84 (ADR-159 A3)
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>
This commit is contained in:
parent
6b5fd3cf25
commit
8d9c5994db
|
|
@ -1,16 +1,38 @@
|
||||||
//! ASTM F3411 Remote ID broadcast (Basic ID + Location/Vector message).
|
//! 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 crate::types::DroneState;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
/// Remote ID broadcast state for one drone.
|
/// 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)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct RemoteIdBroadcast {
|
pub struct RemoteIdBroadcast {
|
||||||
pub uas_id: [u8; 20], // 20-byte UAS ID (ANSI/CTA-2063-A)
|
pub uas_id: [u8; 20], // 20-byte UAS ID (ANSI/CTA-2063-A)
|
||||||
|
/// Operator latitude (WGS84 degrees) — real geodetic position.
|
||||||
pub operator_lat: f64,
|
pub operator_lat: f64,
|
||||||
|
/// Operator longitude (WGS84 degrees) — real geodetic position.
|
||||||
pub operator_lon: f64,
|
pub operator_lon: f64,
|
||||||
pub drone_lat: f64,
|
/// Drone north offset in **metres** from the operator/takeoff datum (NED x).
|
||||||
pub drone_lon: f64,
|
/// 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 altitude_msl_m: f32,
|
||||||
pub speed_ms: f32,
|
pub speed_ms: f32,
|
||||||
pub heading_deg: f32,
|
pub heading_deg: f32,
|
||||||
|
|
@ -24,8 +46,8 @@ impl RemoteIdBroadcast {
|
||||||
uas_id,
|
uas_id,
|
||||||
operator_lat: 0.0,
|
operator_lat: 0.0,
|
||||||
operator_lon: 0.0,
|
operator_lon: 0.0,
|
||||||
drone_lat: 0.0,
|
drone_north_m: 0.0,
|
||||||
drone_lon: 0.0,
|
drone_east_m: 0.0,
|
||||||
altitude_msl_m: 0.0,
|
altitude_msl_m: 0.0,
|
||||||
speed_ms: 0.0,
|
speed_ms: 0.0,
|
||||||
heading_deg: 0.0,
|
heading_deg: 0.0,
|
||||||
|
|
@ -35,11 +57,15 @@ impl RemoteIdBroadcast {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update from a drone state and operator position.
|
/// 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)) {
|
pub fn update(&mut self, state: &DroneState, operator_pos: (f64, f64)) {
|
||||||
// Convert NED position to approximate lat/lon (placeholder — real impl uses WGS84).
|
// NED metres, stored as-is in metre-typed fields (no fabricated geodetic
|
||||||
// We store the NED metres as placeholder values here.
|
// coordinates). A future Location/Vector encoder must transform these
|
||||||
self.drone_lat = state.position.x; // placeholder: x ≈ north offset
|
// through a datum-anchored NED→WGS84 projection before broadcast.
|
||||||
self.drone_lon = state.position.y; // placeholder: y ≈ east offset
|
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.altitude_msl_m = state.altitude_agl_m as f32;
|
||||||
self.speed_ms = state.velocity.magnitude() as f32;
|
self.speed_ms = state.velocity.magnitude() as f32;
|
||||||
self.heading_deg = state.heading_rad.to_degrees() as f32;
|
self.heading_deg = state.heading_rad.to_degrees() as f32;
|
||||||
|
|
@ -80,4 +106,38 @@ mod tests {
|
||||||
let buf = rid.encode_basic_id();
|
let buf = rid.encode_basic_id();
|
||||||
assert_eq!(buf[2], 0xFF);
|
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.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue