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 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,
|
||||
pub drone_lat: f64,
|
||||
pub drone_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,
|
||||
|
|
@ -24,8 +46,8 @@ impl RemoteIdBroadcast {
|
|||
uas_id,
|
||||
operator_lat: 0.0,
|
||||
operator_lon: 0.0,
|
||||
drone_lat: 0.0,
|
||||
drone_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,
|
||||
|
|
@ -35,11 +57,15 @@ impl RemoteIdBroadcast {
|
|||
}
|
||||
|
||||
/// 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)) {
|
||||
// Convert NED position to approximate lat/lon (placeholder — real impl uses WGS84).
|
||||
// We store the NED metres as placeholder values here.
|
||||
self.drone_lat = state.position.x; // placeholder: x ≈ north offset
|
||||
self.drone_lon = state.position.y; // placeholder: y ≈ east offset
|
||||
// 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;
|
||||
|
|
@ -80,4 +106,38 @@ mod tests {
|
|||
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.
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue