navigation: add fail-closed SITL estimator
This commit is contained in:
1 parent
c19cf74314
commit
b25a14f224
4 files changed
+4796
No files matched your search
@@ -0,0 +1,23 @@
|
||||
[package]
|
||||
name = "kaptan-navigation"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
rust-version.workspace = true
|
||||
version.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[features]
|
||||
default = []
|
||||
# Enables the provisional raw-rate estimator used only by the internal SITL
|
||||
# executable. It is not part of a board build.
|
||||
sitl = ["dep:kaptan-sensor"]
|
||||
[dependencies]
|
||||
kaptan-math = { path = "../kaptan-math" }
|
||||
kaptan-sensor = { path = "../kaptan-sensor", optional = true }
|
||||
kaptan-time = { path = "../kaptan-time" }
|
||||
|
||||
[dev-dependencies]
|
||||
kaptan-sensor = { path = "../kaptan-sensor" }
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,41 @@
|
||||
//! Navigation observation contracts and a SITL-only raw-rate ESKF.
|
||||
//!
|
||||
//! Navigation owns the conversion from driver integer observations into SI
|
||||
//! `f32` values. Board and platform crates must not manufacture navigation
|
||||
//! observations or duplicate their frame/calibration metadata.
|
||||
|
||||
#![no_std]
|
||||
|
||||
#[cfg(all(feature = "sitl", target_os = "none"))]
|
||||
compile_error!(
|
||||
"kaptan-navigation's raw-rate ESKF is restricted to the SITL executable; \
|
||||
it is not available to a board target"
|
||||
);
|
||||
|
||||
#[cfg(any(test, feature = "sitl"))]
|
||||
mod eskf;
|
||||
mod observation;
|
||||
|
||||
pub use observation::{
|
||||
AccelerationFrd, AngularRateFrd, CalibrationRevision, Covariance2, FrameId,
|
||||
LocalPositionVelocityObservation, ObservationContext, ObservationSequence,
|
||||
OpticalFlowObservation, PositionFrd, PositionNed, RangeObservation, ScalarVariance,
|
||||
SensorExtrinsics, SourceId, Validity, VelocityNed,
|
||||
};
|
||||
|
||||
/// Internal-SITL API for the provisional raw-rate ESKF.
|
||||
///
|
||||
/// It supports the host simulator only. It cannot compile for a bare-metal
|
||||
/// target and must not be used as a flight-estimator API.
|
||||
#[cfg(any(test, feature = "sitl"))]
|
||||
pub mod sitl {
|
||||
pub use crate::eskf::{
|
||||
Eskf, EstimatorCapabilities, EstimatorConfig, EstimatorHealth, FusionError, FusionResult,
|
||||
InertialEskf, IntegrityConfig, LocalTerrainEskf, NominalState, PropagationError,
|
||||
SourceStatus, StationaryInitializer,
|
||||
};
|
||||
pub use crate::observation::{ImuNoise, ImuObservation};
|
||||
}
|
||||
|
||||
/// Standard gravity in metres per second squared.
|
||||
pub const GRAVITY_MPS2: f32 = 9.806_65;
|
||||
@@ -0,0 +1,540 @@
|
||||
//! Typed SI observations and their calibration/frame provenance.
|
||||
|
||||
use kaptan_math::{Frd, Ned, Sensor, UnitQuaternion, Vector2, Vector3};
|
||||
#[cfg(any(test, feature = "sitl"))]
|
||||
use kaptan_sensor::imu::ImuSnapshot;
|
||||
#[cfg(any(test, feature = "sitl"))]
|
||||
use kaptan_sensor::{optical_flow::OpticalFlowSnapshot, range::RangeSnapshot};
|
||||
use kaptan_time::Timestamp;
|
||||
|
||||
#[cfg(any(test, feature = "sitl"))]
|
||||
use crate::GRAVITY_MPS2;
|
||||
|
||||
/// Origin of one observation stream, allocated by a static board configuration.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct SourceId(u8);
|
||||
impl SourceId {
|
||||
/// Creates a stable source identifier.
|
||||
pub const fn new(value: u8) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
/// Returns the diagnostic identifier.
|
||||
pub const fn get(self) -> u8 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Physical coordinate frame identifier in the active calibration set.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct FrameId(u8);
|
||||
impl FrameId {
|
||||
/// Aircraft FRD body frame.
|
||||
pub const BODY_FRD: Self = Self(0);
|
||||
/// Active launch-local navigation frame with north/east/down axes.
|
||||
pub const LOCAL_NED: Self = Self(1);
|
||||
/// Creates a configured sensor frame identifier.
|
||||
pub const fn new(value: u8) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
/// Returns the diagnostic identifier.
|
||||
pub const fn get(self) -> u8 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Monotonic revision of the calibration/extrinsics used for an observation.
|
||||
///
|
||||
/// TODO(configuration): this is a provenance token, not a calibration store.
|
||||
/// The future configuration service must atomically install sensor mounting,
|
||||
/// scale/bias calibration, uncertainty, and this revision while navigation is
|
||||
/// stopped, then require reinitialization before admitting observations from
|
||||
/// the new revision.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct CalibrationRevision(u32);
|
||||
impl CalibrationRevision {
|
||||
/// Creates a calibration revision token.
|
||||
pub const fn new(value: u32) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
/// Returns its value for diagnostics.
|
||||
pub const fn get(self) -> u32 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Producer sequence number used to detect replayed or dropped observations.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct ObservationSequence(u32);
|
||||
impl ObservationSequence {
|
||||
/// Creates a sequence token.
|
||||
pub const fn new(value: u32) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
/// Returns the token value.
|
||||
pub const fn get(self) -> u32 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Driver validity state preserved through the navigation boundary.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum Validity {
|
||||
/// The producer accepted this observation.
|
||||
Valid,
|
||||
/// The producer or adapter rejected it.
|
||||
Invalid,
|
||||
}
|
||||
|
||||
/// Common provenance for every navigation observation.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct ObservationContext {
|
||||
/// Sensor measurement time synchronized into the controller monotonic domain.
|
||||
pub measurement_time: Timestamp,
|
||||
/// Controller receipt time, never compared with an external time directly.
|
||||
pub receipt_time: Timestamp,
|
||||
/// Statically configured source.
|
||||
pub source: SourceId,
|
||||
/// Frame of the value.
|
||||
pub frame: FrameId,
|
||||
/// Calibration/extrinsics revision.
|
||||
pub calibration: CalibrationRevision,
|
||||
/// Device quality, 0 through 255.
|
||||
pub quality: u8,
|
||||
/// Device/adapter validity.
|
||||
pub validity: Validity,
|
||||
/// Source sequence number.
|
||||
pub sequence: ObservationSequence,
|
||||
}
|
||||
|
||||
impl ObservationContext {
|
||||
/// Creates provenance shared by a concrete observation value.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub const fn new(
|
||||
measurement_time: Timestamp,
|
||||
receipt_time: Timestamp,
|
||||
source: SourceId,
|
||||
frame: FrameId,
|
||||
calibration: CalibrationRevision,
|
||||
quality: u8,
|
||||
validity: Validity,
|
||||
sequence: ObservationSequence,
|
||||
) -> Self {
|
||||
Self {
|
||||
measurement_time,
|
||||
receipt_time,
|
||||
source,
|
||||
frame,
|
||||
calibration,
|
||||
quality,
|
||||
validity,
|
||||
sequence,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Positive finite scalar measurement variance in squared SI units.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct ScalarVariance(f32);
|
||||
impl ScalarVariance {
|
||||
/// Validates a configured or reported scalar variance.
|
||||
pub fn new(value: f32) -> Option<Self> {
|
||||
if value.is_finite() && value > 0.0 {
|
||||
Some(Self(value))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
/// Returns the squared-SI value.
|
||||
pub const fn get(self) -> f32 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Symmetric two-dimensional covariance in squared SI units.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Covariance2 {
|
||||
/// xx variance.
|
||||
pub xx: ScalarVariance,
|
||||
/// xy covariance.
|
||||
pub xy: f32,
|
||||
/// yy variance.
|
||||
pub yy: ScalarVariance,
|
||||
}
|
||||
impl Covariance2 {
|
||||
/// Validates finite covariance and a positive determinant.
|
||||
pub fn new(xx: ScalarVariance, xy: f32, yy: ScalarVariance) -> Option<Self> {
|
||||
let determinant = xx.get() * yy.get() - xy * xy;
|
||||
if xy.is_finite() && determinant.is_finite() && determinant > 0.0 {
|
||||
Some(Self { xx, xy, yy })
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// NED position in metres.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct PositionNed(Vector3<Ned>);
|
||||
impl PositionNed {
|
||||
/// Creates a position in metres.
|
||||
pub const fn metres(north: f32, east: f32, down: f32) -> Self {
|
||||
Self(Vector3::new(north, east, down))
|
||||
}
|
||||
/// Returns its NED metre vector.
|
||||
pub const fn vector(self) -> Vector3<Ned> {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// NED velocity in metres per second.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct VelocityNed(Vector3<Ned>);
|
||||
impl VelocityNed {
|
||||
/// Creates velocity in metres per second.
|
||||
pub const fn metres_per_second(north: f32, east: f32, down: f32) -> Self {
|
||||
Self(Vector3::new(north, east, down))
|
||||
}
|
||||
/// Returns its NED vector.
|
||||
pub const fn vector(self) -> Vector3<Ned> {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Complete position and velocity aiding in the active local-NED frame.
|
||||
///
|
||||
/// Position and velocity share one source timestamp and sequence because the
|
||||
/// producer measures the same vehicle state. Their diagonal variances remain
|
||||
/// explicit rather than being hidden in a simulator or transport adapter.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct LocalPositionVelocityObservation {
|
||||
/// Required source/time/frame provenance.
|
||||
pub context: ObservationContext,
|
||||
/// Absolute local-NED position in metres.
|
||||
pub position: PositionNed,
|
||||
/// Absolute local-NED velocity in metres per second.
|
||||
pub velocity: VelocityNed,
|
||||
/// Per-axis position measurement variance in m².
|
||||
pub position_variance: ScalarVariance,
|
||||
/// Per-axis velocity measurement variance in (m/s)².
|
||||
pub velocity_variance: ScalarVariance,
|
||||
}
|
||||
|
||||
/// Body FRD position offset in metres.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct PositionFrd(Vector3<Frd>);
|
||||
impl PositionFrd {
|
||||
/// Creates a body-frame offset in metres.
|
||||
pub const fn metres(forward: f32, right: f32, down: f32) -> Self {
|
||||
Self(Vector3::new(forward, right, down))
|
||||
}
|
||||
/// Returns its body vector.
|
||||
pub const fn vector(self) -> Vector3<Frd> {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Body FRD specific force in metres per second squared.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct AccelerationFrd(Vector3<Frd>);
|
||||
impl AccelerationFrd {
|
||||
/// Creates specific force in metres per second squared.
|
||||
pub const fn metres_per_second_squared(x: f32, y: f32, z: f32) -> Self {
|
||||
Self(Vector3::new(x, y, z))
|
||||
}
|
||||
/// Returns the body vector.
|
||||
pub const fn vector(self) -> Vector3<Frd> {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Body FRD angular rate in radians per second.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct AngularRateFrd(Vector3<Frd>);
|
||||
impl AngularRateFrd {
|
||||
/// Creates angular rate in radians per second.
|
||||
pub const fn radians_per_second(x: f32, y: f32, z: f32) -> Self {
|
||||
Self(Vector3::new(x, y, z))
|
||||
}
|
||||
/// Returns the body vector.
|
||||
pub const fn vector(self) -> Vector3<Frd> {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Configured IMU white-noise variances in SI squared units.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
#[cfg(any(test, feature = "sitl"))]
|
||||
pub struct ImuNoise {
|
||||
/// Gyro rate variance, (rad/s)^2.
|
||||
pub gyro_rate: ScalarVariance,
|
||||
/// Accelerometer specific-force variance, (m/s²)^2.
|
||||
pub acceleration: ScalarVariance,
|
||||
}
|
||||
|
||||
/// IMU observation expressed in the calibrated aircraft FRD frame.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
#[cfg(any(test, feature = "sitl"))]
|
||||
pub struct ImuObservation {
|
||||
/// Required observation provenance.
|
||||
pub context: ObservationContext,
|
||||
/// Specific force in m/s².
|
||||
pub acceleration: AccelerationFrd,
|
||||
/// Angular rate in rad/s.
|
||||
pub angular_rate: AngularRateFrd,
|
||||
/// Explicit configured uncertainty.
|
||||
pub noise: ImuNoise,
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "sitl"))]
|
||||
impl ImuObservation {
|
||||
/// Converts the existing integer IMU contract to SI exactly at the
|
||||
/// navigation boundary. The board has already aligned this snapshot to FRD.
|
||||
pub fn from_snapshot(
|
||||
snapshot: ImuSnapshot,
|
||||
mut context: ObservationContext,
|
||||
noise: ImuNoise,
|
||||
) -> Self {
|
||||
context.measurement_time = snapshot.timestamp;
|
||||
Self {
|
||||
context,
|
||||
acceleration: AccelerationFrd::metres_per_second_squared(
|
||||
f32::from(snapshot.acceleration_milligravity[0]) * GRAVITY_MPS2 / 1_000.0,
|
||||
f32::from(snapshot.acceleration_milligravity[1]) * GRAVITY_MPS2 / 1_000.0,
|
||||
f32::from(snapshot.acceleration_milligravity[2]) * GRAVITY_MPS2 / 1_000.0,
|
||||
),
|
||||
angular_rate: AngularRateFrd::radians_per_second(
|
||||
f32::from(snapshot.angular_rate_milliradians_per_second[0]) / 1_000.0,
|
||||
f32::from(snapshot.angular_rate_milliradians_per_second[1]) / 1_000.0,
|
||||
f32::from(snapshot.angular_rate_milliradians_per_second[2]) / 1_000.0,
|
||||
),
|
||||
noise,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Range observation in metres along the sensor's positive z axis.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct RangeObservation {
|
||||
/// Required observation provenance.
|
||||
pub context: ObservationContext,
|
||||
/// Ray distance in metres.
|
||||
pub metres: f32,
|
||||
/// Explicit measurement variance in m².
|
||||
pub variance: ScalarVariance,
|
||||
}
|
||||
|
||||
impl RangeObservation {
|
||||
/// Converts a driver range value to metres at the navigation boundary.
|
||||
#[cfg(any(test, feature = "sitl"))]
|
||||
pub fn from_snapshot(
|
||||
snapshot: RangeSnapshot,
|
||||
mut context: ObservationContext,
|
||||
variance: ScalarVariance,
|
||||
) -> Self {
|
||||
context.measurement_time = snapshot.timestamp;
|
||||
Self {
|
||||
context,
|
||||
metres: snapshot.distance_millimetres as f32 / 1_000.0,
|
||||
variance,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Ground-referenced XY velocity reported in a named sensor frame.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct OpticalFlowObservation {
|
||||
/// Required observation provenance.
|
||||
pub context: ObservationContext,
|
||||
/// Sensor-frame x/y velocity in m/s.
|
||||
pub velocity: Vector2<Sensor>,
|
||||
/// Ground distance in metres; retained for plausibility/diagnostics.
|
||||
pub ground_distance_metres: f32,
|
||||
/// Explicit XY measurement covariance in (m/s)².
|
||||
pub covariance: Covariance2,
|
||||
}
|
||||
|
||||
impl OpticalFlowObservation {
|
||||
/// Converts the existing MTF-01-independent driver value into SI.
|
||||
#[cfg(any(test, feature = "sitl"))]
|
||||
pub fn from_snapshot(
|
||||
snapshot: OpticalFlowSnapshot,
|
||||
mut context: ObservationContext,
|
||||
covariance: Covariance2,
|
||||
) -> Self {
|
||||
context.measurement_time = snapshot.timestamp;
|
||||
context.quality = snapshot.quality;
|
||||
Self {
|
||||
context,
|
||||
velocity: Vector2::new(
|
||||
snapshot.velocity_millimetres_per_second[0] as f32 / 1_000.0,
|
||||
snapshot.velocity_millimetres_per_second[1] as f32 / 1_000.0,
|
||||
),
|
||||
ground_distance_metres: snapshot.ground_distance_millimetres as f32 / 1_000.0,
|
||||
covariance,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Static calibrated transform from a sensor frame into aircraft FRD.
|
||||
///
|
||||
/// TODO(configuration): this remains a build-time value until the
|
||||
/// configuration service can stage the full sensor calibration bundle. Do not
|
||||
/// expose setters for individual fields: source identity, frame, calibration
|
||||
/// revision, rotation, and lever arm must change together and only while the
|
||||
/// estimator and actuators are inhibited.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct SensorExtrinsics {
|
||||
/// Statically configured source expected for this physical device.
|
||||
pub source: SourceId,
|
||||
/// Frame ID expected in every observation from this device.
|
||||
pub sensor_frame: FrameId,
|
||||
/// Calibration/extrinsics revision represented by this transform.
|
||||
pub calibration: CalibrationRevision,
|
||||
/// Rotation mapping sensor vectors into FRD.
|
||||
pub sensor_to_body: UnitQuaternion<Sensor, Frd>,
|
||||
/// Sensor origin relative to aircraft origin, in FRD metres.
|
||||
pub position_body: PositionFrd,
|
||||
}
|
||||
|
||||
impl SensorExtrinsics {
|
||||
/// Maps a sensor vector into FRD at the sole sensor-calibration boundary.
|
||||
pub fn sensor_to_body_vector(self, value: Vector3<Sensor>) -> Vector3<Frd> {
|
||||
self.sensor_to_body.rotate(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use kaptan_sensor::{optical_flow::OpticalFlowSnapshot, range::RangeSnapshot};
|
||||
|
||||
fn context(frame: FrameId) -> ObservationContext {
|
||||
ObservationContext::new(
|
||||
Timestamp::from_microseconds(10),
|
||||
Timestamp::from_microseconds(12),
|
||||
SourceId::new(2),
|
||||
frame,
|
||||
CalibrationRevision::new(3),
|
||||
250,
|
||||
Validity::Valid,
|
||||
ObservationSequence::new(9),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn imu_snapshot_is_si() {
|
||||
let noise = ImuNoise {
|
||||
gyro_rate: ScalarVariance::new(0.01).unwrap(),
|
||||
acceleration: ScalarVariance::new(0.02).unwrap(),
|
||||
};
|
||||
let sample = ImuObservation::from_snapshot(
|
||||
ImuSnapshot::new(
|
||||
Timestamp::from_microseconds(10),
|
||||
[1_000, 0, -1_000],
|
||||
[1_000, 0, 0],
|
||||
0,
|
||||
),
|
||||
context(FrameId::BODY_FRD),
|
||||
noise,
|
||||
);
|
||||
assert!((sample.acceleration.vector().x() - GRAVITY_MPS2).abs() < 1.0e-5);
|
||||
assert!((sample.angular_rate.vector().x() - 1.0).abs() < 1.0e-6);
|
||||
assert_eq!(sample.context.calibration, CalibrationRevision::new(3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn covariance_validation() {
|
||||
let variance = ScalarVariance::new(1.0).unwrap();
|
||||
assert!(Covariance2::new(variance, 1.0, variance).is_none());
|
||||
let large = ScalarVariance::new(f32::MAX).unwrap();
|
||||
assert!(Covariance2::new(large, 0.0, large).is_none());
|
||||
assert!(Covariance2::new(variance, f32::NAN, variance).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provenance_validation() {
|
||||
assert_eq!(SourceId::new(7).get(), 7);
|
||||
assert_eq!(FrameId::new(8).get(), 8);
|
||||
assert_eq!(CalibrationRevision::new(9).get(), 9);
|
||||
assert_eq!(ObservationSequence::new(10).get(), 10);
|
||||
assert!(ScalarVariance::new(0.0).is_none());
|
||||
assert!(ScalarVariance::new(-1.0).is_none());
|
||||
assert!(ScalarVariance::new(f32::NAN).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshots_convert_at_boundary() {
|
||||
let variance = ScalarVariance::new(0.25).unwrap();
|
||||
let range = RangeObservation::from_snapshot(
|
||||
RangeSnapshot::new(Timestamp::from_microseconds(20), 1_234),
|
||||
context(FrameId::new(4)),
|
||||
variance,
|
||||
);
|
||||
assert_eq!(
|
||||
range.context.measurement_time,
|
||||
Timestamp::from_microseconds(20)
|
||||
);
|
||||
assert!((range.metres - 1.234).abs() < 1.0e-6);
|
||||
|
||||
let covariance = Covariance2::new(variance, 0.01, variance).unwrap();
|
||||
let flow = OpticalFlowObservation::from_snapshot(
|
||||
OpticalFlowSnapshot::new(Timestamp::from_microseconds(21), [-500, 250], 2_500, 42),
|
||||
context(FrameId::new(5)),
|
||||
covariance,
|
||||
);
|
||||
assert_eq!(
|
||||
flow.context.measurement_time,
|
||||
Timestamp::from_microseconds(21)
|
||||
);
|
||||
assert_eq!(flow.context.quality, 42);
|
||||
assert_eq!((flow.velocity.x(), flow.velocity.y()), (-0.5, 0.25));
|
||||
assert_eq!(flow.ground_distance_metres, 2.5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn observation_frames() {
|
||||
let noise = ImuNoise {
|
||||
gyro_rate: ScalarVariance::new(0.01).unwrap(),
|
||||
acceleration: ScalarVariance::new(0.02).unwrap(),
|
||||
};
|
||||
let sample = ImuObservation::from_snapshot(
|
||||
ImuSnapshot::new(Timestamp::from_microseconds(10), [0; 3], [0; 3], 0),
|
||||
context(FrameId::BODY_FRD),
|
||||
noise,
|
||||
);
|
||||
assert_eq!(
|
||||
sample.context.measurement_time,
|
||||
Timestamp::from_microseconds(10)
|
||||
);
|
||||
assert_eq!(PositionNed::metres(1.0, 2.0, 3.0).vector().z(), 3.0);
|
||||
assert_eq!(
|
||||
VelocityNed::metres_per_second(1.0, 2.0, 3.0).vector().y(),
|
||||
2.0
|
||||
);
|
||||
assert_eq!(PositionFrd::metres(1.0, 2.0, 3.0).vector().x(), 1.0);
|
||||
assert_eq!(
|
||||
AccelerationFrd::metres_per_second_squared(1.0, 2.0, 3.0)
|
||||
.vector()
|
||||
.y(),
|
||||
2.0
|
||||
);
|
||||
assert_eq!(
|
||||
AngularRateFrd::radians_per_second(1.0, 2.0, 3.0)
|
||||
.vector()
|
||||
.z(),
|
||||
3.0
|
||||
);
|
||||
|
||||
let extrinsics = SensorExtrinsics {
|
||||
source: SourceId::new(2),
|
||||
sensor_frame: FrameId::new(5),
|
||||
calibration: CalibrationRevision::new(3),
|
||||
sensor_to_body: UnitQuaternion::identity(),
|
||||
position_body: PositionFrd::metres(0.0, 0.0, 0.0),
|
||||
};
|
||||
assert_eq!(
|
||||
extrinsics.sensor_to_body_vector(Vector3::new(1.0, 2.0, 3.0)),
|
||||
Vector3::new(1.0, 2.0, 3.0)
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user