refactor(config): generate scalar validation boundaries
This commit is contained in:
1 parent
afafd126d7
commit
8134235f48
29 files changed
+1008
-480
No files matched your search
@@ -26,6 +26,19 @@ pub enum CborError {
|
||||
InvalidProfile(ConfigViolation),
|
||||
}
|
||||
|
||||
/// Failure while encoding one complete candidate snapshot.
|
||||
///
|
||||
/// The candidate is validated before the first CBOR item is written, so a
|
||||
/// caller can never export a locally valid but relationally invalid edit.
|
||||
#[derive(Debug)]
|
||||
pub enum CborEncodeError<E> {
|
||||
/// The complete candidate did not validate against immutable board
|
||||
/// capabilities.
|
||||
InvalidProfile(ConfigViolation),
|
||||
/// The caller-owned CBOR writer rejected the encoded snapshot.
|
||||
Encode(minicbor::encode::Error<E>),
|
||||
}
|
||||
|
||||
/// Decodes exactly one bounded current record and validates it against immutable
|
||||
/// board capabilities before returning a candidate to its caller.
|
||||
pub fn decode_candidate(
|
||||
@@ -37,31 +50,50 @@ pub fn decode_candidate(
|
||||
}
|
||||
let candidate = decode_current(bytes)?;
|
||||
candidate
|
||||
.validate_against(capabilities)
|
||||
.validate(capabilities)
|
||||
.map_err(CborError::InvalidProfile)?;
|
||||
Ok(candidate)
|
||||
}
|
||||
|
||||
/// Encodes one complete current snapshot to a caller-owned CBOR writer.
|
||||
/// Validates and encodes one complete current snapshot to a caller-owned CBOR
|
||||
/// writer.
|
||||
///
|
||||
/// Validation happens before creating the encoder, ensuring a failed
|
||||
/// cross-field or capability check emits no partial CBOR record.
|
||||
pub fn encode_candidate<W>(
|
||||
candidate: CandidateProfile,
|
||||
capabilities: &VehicleCapabilities,
|
||||
writer: W,
|
||||
) -> Result<(), minicbor::encode::Error<W::Error>>
|
||||
) -> Result<(), CborEncodeError<W::Error>>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
candidate
|
||||
.validate(capabilities)
|
||||
.map_err(CborEncodeError::InvalidProfile)?;
|
||||
let mut encoder = Encoder::new(writer);
|
||||
encoder.map(CandidateProfile::FIELD_COUNT as u64 + 2)?;
|
||||
encoder.u16(FORMAT_KEY)?.u16(FORMAT_VERSION)?;
|
||||
encoder.u16(SCHEMA_KEY)?.bytes(&SCHEMA_ID)?;
|
||||
encoder
|
||||
.map(CandidateProfile::FIELD_COUNT as u64 + 2)
|
||||
.map_err(CborEncodeError::Encode)?;
|
||||
encoder
|
||||
.u16(FORMAT_KEY)
|
||||
.and_then(|encoder| encoder.u16(FORMAT_VERSION))
|
||||
.map_err(CborEncodeError::Encode)?;
|
||||
encoder
|
||||
.u16(SCHEMA_KEY)
|
||||
.and_then(|encoder| encoder.bytes(&SCHEMA_ID))
|
||||
.map_err(CborEncodeError::Encode)?;
|
||||
for descriptor in CandidateProfile::descriptors() {
|
||||
encoder.u16(descriptor.id.get())?;
|
||||
encoder
|
||||
.u16(descriptor.id.get())
|
||||
.map_err(CborEncodeError::Encode)?;
|
||||
encode_value(
|
||||
&mut encoder,
|
||||
candidate
|
||||
.field_value(descriptor.id)
|
||||
.expect("descriptor field"),
|
||||
)?;
|
||||
)
|
||||
.map_err(CborEncodeError::Encode)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -178,7 +210,7 @@ mod tests {
|
||||
fn round_trip_and_trailing_rejection_are_exact() {
|
||||
let mut writer = minicbor::encode::write::Cursor::new([0_u8; MAXIMUM_CANDIDATE_BYTES]);
|
||||
let capabilities = VehicleCapabilities::quad_x();
|
||||
encode_candidate(CandidateProfile::default(), &mut writer).unwrap();
|
||||
encode_candidate(CandidateProfile::default(), &capabilities, &mut writer).unwrap();
|
||||
let length = writer.position();
|
||||
let bytes = writer.into_inner();
|
||||
assert!(decode_candidate(&bytes[..length], &capabilities).is_ok());
|
||||
@@ -190,6 +222,23 @@ mod tests {
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encoding_rejects_a_cross_field_invalid_transaction_before_writing() {
|
||||
let capabilities = VehicleCapabilities::quad_x();
|
||||
let mut candidate = CandidateProfile::default();
|
||||
candidate
|
||||
.set_field(kaptan_config::FieldId::new(2), ConfigValue::U64(50_001))
|
||||
.expect("the scalar is locally valid");
|
||||
let mut writer = minicbor::encode::write::Cursor::new([0_u8; MAXIMUM_CANDIDATE_BYTES]);
|
||||
|
||||
assert!(matches!(
|
||||
encode_candidate(candidate, &capabilities, &mut writer),
|
||||
Err(CborEncodeError::InvalidProfile(violation))
|
||||
if violation.rule() == ValidationRule::Inconsistent
|
||||
));
|
||||
assert_eq!(writer.position(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_wrong_format_and_schema_before_candidate_conversion() {
|
||||
let capabilities = VehicleCapabilities::quad_x();
|
||||
@@ -259,6 +308,46 @@ mod tests {
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cbor_scalar_write_preserves_the_generated_field_and_rule() {
|
||||
let capabilities = VehicleCapabilities::quad_x();
|
||||
let mass = CandidateProfile::descriptors()
|
||||
.iter()
|
||||
.find(|descriptor| descriptor.display_key == "thrust.mass_kg")
|
||||
.expect("declared mass descriptor");
|
||||
let mut writer = minicbor::encode::write::Cursor::new([0_u8; MAXIMUM_CANDIDATE_BYTES]);
|
||||
let mut encoder = Encoder::new(&mut writer);
|
||||
encoder
|
||||
.map(CandidateProfile::FIELD_COUNT as u64 + 2)
|
||||
.unwrap();
|
||||
encoder
|
||||
.u16(FORMAT_KEY)
|
||||
.unwrap()
|
||||
.u16(FORMAT_VERSION)
|
||||
.unwrap();
|
||||
encoder.u16(SCHEMA_KEY).unwrap().bytes(&SCHEMA_ID).unwrap();
|
||||
let candidate = CandidateProfile::default();
|
||||
for descriptor in CandidateProfile::descriptors() {
|
||||
encoder.u16(descriptor.id.get()).unwrap();
|
||||
let value = if descriptor.id == mass.id {
|
||||
ConfigValue::F32(0.009)
|
||||
} else {
|
||||
candidate.field_value(descriptor.id).unwrap()
|
||||
};
|
||||
encode_value(&mut encoder, value).unwrap();
|
||||
}
|
||||
let length = writer.position();
|
||||
let bytes = writer.into_inner();
|
||||
|
||||
assert!(matches!(
|
||||
decode_candidate(&bytes[..length], &capabilities),
|
||||
Err(CborError::InvalidProfile(issue))
|
||||
if issue.primary() == Some(mass.id)
|
||||
&& issue.related_field().is_none()
|
||||
&& issue.rule() == ValidationRule::BelowMinimum
|
||||
));
|
||||
}
|
||||
|
||||
fn encoded_test_record<F>(
|
||||
format_version: u16,
|
||||
schema_id: [u8; 16],
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
pub mod cbor;
|
||||
pub mod profile;
|
||||
|
||||
pub use cbor::{CborError, MAXIMUM_CANDIDATE_BYTES, decode_candidate, encode_candidate};
|
||||
pub use cbor::{
|
||||
CborEncodeError, CborError, MAXIMUM_CANDIDATE_BYTES, decode_candidate, encode_candidate,
|
||||
};
|
||||
pub use profile::{CandidateProfile, FORMAT_VERSION, VehicleCapabilities, VehicleProfile};
|
||||
|
||||
/// Exact identifier of the one unreleased Quad-X profile schema.
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
//! Private vehicle candidate and whole-profile validation boundary.
|
||||
|
||||
use kaptan_config::{
|
||||
ConfigModel, ConfigValue, ConfigViolation, FieldDescriptor, FieldId, FieldWriteError,
|
||||
ValidationRule,
|
||||
ConfigAggregate, ConfigModel, ConfigValidate, ConfigValue, ConfigViolation, FieldDescriptor,
|
||||
FieldId, FieldWriteError, ValidateFields, ValidationRule,
|
||||
};
|
||||
|
||||
use super::{
|
||||
@@ -24,41 +24,39 @@ pub const FORMAT_VERSION: u16 = 1;
|
||||
///
|
||||
/// Fields are private outside this crate. Operators, CBOR import, and MAVLink
|
||||
/// transport all use the same descriptor/mutation and full-validation path.
|
||||
#[derive(Clone, Copy)]
|
||||
#[derive(Clone, Copy, ConfigValidate)]
|
||||
pub struct CandidateProfile {
|
||||
#[config(section)]
|
||||
pub(super) safety: SafetyCandidate,
|
||||
#[config(section)]
|
||||
pub(super) timing: TimingCandidate,
|
||||
#[config(section)]
|
||||
pub(super) aiding: AidingCandidate,
|
||||
#[config(section)]
|
||||
pub(super) control: ControlCandidate,
|
||||
#[config(section)]
|
||||
pub(super) thrust: ThrustCandidate,
|
||||
#[config(section)]
|
||||
pub(super) rate_pid: PidCandidate,
|
||||
#[config(section)]
|
||||
pub(super) actuator_limits: ActuatorLimitsCandidate,
|
||||
#[config(section)]
|
||||
pub(super) observability: ObservabilityCandidate,
|
||||
}
|
||||
|
||||
impl Default for CandidateProfile {
|
||||
fn default() -> Self {
|
||||
registry::default_candidate()
|
||||
<Self as ConfigAggregate>::default_aggregate()
|
||||
.expect("compiled configuration defaults must validate")
|
||||
}
|
||||
}
|
||||
|
||||
impl CandidateProfile {
|
||||
/// Number of operator-editable fields in the derived descriptor registry.
|
||||
pub const FIELD_COUNT: usize = registry::FIELD_COUNT;
|
||||
|
||||
/// Returns the complete transport-neutral descriptor registry.
|
||||
pub const fn descriptors() -> &'static [FieldDescriptor; Self::FIELD_COUNT] {
|
||||
registry::descriptors()
|
||||
}
|
||||
|
||||
/// Validates this candidate against immutable board capabilities.
|
||||
pub fn validate_against(
|
||||
&self,
|
||||
capabilities: &VehicleCapabilities,
|
||||
) -> Result<VehicleProfile, ConfigViolation> {
|
||||
self.validate_complete(capabilities)
|
||||
}
|
||||
|
||||
fn from_active(active: &VehicleProfile) -> Self {
|
||||
let safety = active.safety();
|
||||
let timing = active.timing();
|
||||
@@ -118,19 +116,8 @@ impl CandidateProfile {
|
||||
&self,
|
||||
capabilities: &VehicleCapabilities,
|
||||
) -> Result<VehicleProfile, ConfigViolation> {
|
||||
if capabilities.actuator_count() != 4 {
|
||||
return Err(ConfigViolation::global(
|
||||
ValidationRule::UnsupportedCapability,
|
||||
));
|
||||
}
|
||||
self.safety.validate()?;
|
||||
self.timing.validate()?;
|
||||
self.aiding.validate()?;
|
||||
self.control.validate()?;
|
||||
self.thrust.validate()?;
|
||||
self.rate_pid.validate()?;
|
||||
self.actuator_limits.validate()?;
|
||||
self.observability.validate()?;
|
||||
self.validate_fields()?;
|
||||
self.validate_capabilities(capabilities)?;
|
||||
Ok(VehicleProfile {
|
||||
safety: SafetySettings {
|
||||
prearm_dwell_microseconds: self.safety.prearm_dwell_microseconds,
|
||||
@@ -179,6 +166,18 @@ impl CandidateProfile {
|
||||
mixer: capabilities.mixer(),
|
||||
})
|
||||
}
|
||||
|
||||
fn validate_capabilities(
|
||||
&self,
|
||||
capabilities: &VehicleCapabilities,
|
||||
) -> Result<(), ConfigViolation> {
|
||||
if capabilities.actuator_count() != 4 {
|
||||
return Err(ConfigViolation::global(
|
||||
ValidationRule::UnsupportedCapability,
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl ConfigModel for CandidateProfile {
|
||||
@@ -191,19 +190,19 @@ impl ConfigModel for CandidateProfile {
|
||||
}
|
||||
|
||||
fn field_count() -> u16 {
|
||||
u16::try_from(registry::FIELD_COUNT).expect("field registry exceeds the protocol limit")
|
||||
u16::try_from(Self::FIELD_COUNT).expect("field registry exceeds the protocol limit")
|
||||
}
|
||||
|
||||
fn field_value(&self, id: FieldId) -> Option<ConfigValue> {
|
||||
registry::field_value(self, id)
|
||||
<Self as ConfigAggregate>::field_value(self, id)
|
||||
}
|
||||
|
||||
fn active_field_value(active: &Self::Active, id: FieldId) -> Option<ConfigValue> {
|
||||
registry::field_value(&Self::from_active(active), id)
|
||||
<Self as ConfigAggregate>::field_value(&Self::from_active(active), id)
|
||||
}
|
||||
|
||||
fn set_field(&mut self, id: FieldId, value: ConfigValue) -> Result<(), FieldWriteError> {
|
||||
registry::set_field(self, id, value)
|
||||
<Self as ConfigAggregate>::set_field(self, id, value)
|
||||
}
|
||||
|
||||
fn validate(&self, context: &Self::Context) -> Result<Self::Active, ConfigViolation> {
|
||||
@@ -213,13 +212,21 @@ impl ConfigModel for CandidateProfile {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use kaptan_config::{ConfigModel, ConfigValue, FieldWriteError, ValidationRule};
|
||||
use kaptan_config::{ConfigModel, ConfigValue, FieldId, FieldWriteError, ValidationRule};
|
||||
|
||||
use super::{CandidateProfile, VehicleCapabilities};
|
||||
use crate::profile::ids;
|
||||
|
||||
const CAPABILITIES: VehicleCapabilities = VehicleCapabilities::quad_x();
|
||||
|
||||
fn descriptor_id(display_key: &str) -> FieldId {
|
||||
CandidateProfile::descriptors()
|
||||
.iter()
|
||||
.find(|descriptor| descriptor.display_key == display_key)
|
||||
.expect("declared field descriptor")
|
||||
.id
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_descriptor_has_a_readable_default_value() {
|
||||
let candidate = CandidateProfile::default();
|
||||
@@ -293,13 +300,19 @@ mod tests {
|
||||
fn observability_filter_is_persisted_restart_only_and_cannot_hide_errors() {
|
||||
let mut candidate = CandidateProfile::default();
|
||||
candidate
|
||||
.set_field(ids::OBSERVABILITY_MAXIMUM_LOG_LEVEL, ConfigValue::U32(3))
|
||||
.set_field(
|
||||
descriptor_id("observability.maximum_log_level"),
|
||||
ConfigValue::U32(3),
|
||||
)
|
||||
.unwrap();
|
||||
let active = candidate.validate(&CAPABILITIES).unwrap();
|
||||
assert_eq!(active.observability().maximum_log_level(), 3);
|
||||
assert_eq!(active.observability().maximum_log_level(), 3);
|
||||
candidate
|
||||
.set_field(ids::OBSERVABILITY_MAXIMUM_LOG_LEVEL, ConfigValue::U32(5))
|
||||
.set_field(
|
||||
descriptor_id("observability.maximum_log_level"),
|
||||
ConfigValue::U32(5),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
candidate
|
||||
@@ -311,7 +324,7 @@ mod tests {
|
||||
);
|
||||
assert!(matches!(
|
||||
candidate.set_field(
|
||||
ids::OBSERVABILITY_MAXIMUM_LOG_LEVEL,
|
||||
descriptor_id("observability.maximum_log_level"),
|
||||
ConfigValue::U32(2),
|
||||
),
|
||||
Err(FieldWriteError::Violation(issue))
|
||||
@@ -324,7 +337,10 @@ mod tests {
|
||||
for raw in [3, 4, 5, 6, 7] {
|
||||
let mut candidate = CandidateProfile::default();
|
||||
candidate
|
||||
.set_field(ids::OBSERVABILITY_MAXIMUM_LOG_LEVEL, ConfigValue::U32(raw))
|
||||
.set_field(
|
||||
descriptor_id("observability.maximum_log_level"),
|
||||
ConfigValue::U32(raw),
|
||||
)
|
||||
.expect("descriptor range accepts the stable severity");
|
||||
assert_eq!(
|
||||
candidate
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use kaptan_config::{ConfigValidate, ConfigViolation, ValidateFields, ValidationRule};
|
||||
use kaptan_config::{ConfigValidate, ConfigViolation, ValidationRule};
|
||||
|
||||
use super::ids;
|
||||
|
||||
@@ -24,12 +24,6 @@ pub(super) struct AidingCandidate {
|
||||
pub(super) velocity_variance_m2_per_second_squared: f32,
|
||||
}
|
||||
|
||||
impl AidingCandidate {
|
||||
pub(super) fn validate(self) -> Result<(), ConfigViolation> {
|
||||
self.validate_fields()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, ConfigValidate)]
|
||||
pub(super) struct ControlCandidate {
|
||||
#[config(
|
||||
@@ -78,13 +72,8 @@ pub(super) struct ControlCandidate {
|
||||
)]
|
||||
pub(super) maximum_downward_acceleration_mps2: f32,
|
||||
}
|
||||
impl ControlCandidate {
|
||||
pub(super) fn validate(self) -> Result<(), ConfigViolation> {
|
||||
self.validate_fields()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, ConfigValidate)]
|
||||
#[config(post = Self::validate_thrust)]
|
||||
pub(super) struct ThrustCandidate {
|
||||
#[config(
|
||||
id = 13,
|
||||
@@ -115,8 +104,7 @@ pub(super) struct ThrustCandidate {
|
||||
pub(super) maximum_tilt_tangent: f32,
|
||||
}
|
||||
impl ThrustCandidate {
|
||||
pub(super) fn validate(self) -> Result<(), ConfigViolation> {
|
||||
self.validate_fields()?;
|
||||
fn validate_thrust(&self) -> Result<(), ConfigViolation> {
|
||||
if self.maximum_collective_thrust_newtons <= self.mass_kg * 9.806_65 {
|
||||
return Err(ConfigViolation::related(
|
||||
ids::MAXIMUM_COLLECTIVE_THRUST_NEWTONS,
|
||||
@@ -129,6 +117,7 @@ impl ThrustCandidate {
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, ConfigValidate)]
|
||||
#[config(post = Self::validate_pid)]
|
||||
pub(super) struct PidCandidate {
|
||||
#[config(
|
||||
id = 16,
|
||||
@@ -177,8 +166,7 @@ pub(super) struct PidCandidate {
|
||||
pub(super) integral_limit: f32,
|
||||
}
|
||||
impl PidCandidate {
|
||||
pub(super) fn validate(self) -> Result<(), ConfigViolation> {
|
||||
self.validate_fields()?;
|
||||
fn validate_pid(&self) -> Result<(), ConfigViolation> {
|
||||
if self.integral_limit > self.output_limit {
|
||||
return Err(ConfigViolation::related(
|
||||
ids::RATE_PID_INTEGRAL_LIMIT,
|
||||
@@ -191,6 +179,7 @@ impl PidCandidate {
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, ConfigValidate)]
|
||||
#[config(post = Self::validate_actuator_limits)]
|
||||
pub(super) struct ActuatorLimitsCandidate {
|
||||
#[config(
|
||||
id = 21,
|
||||
@@ -221,8 +210,7 @@ pub(super) struct ActuatorLimitsCandidate {
|
||||
pub(super) slew_per_second: f32,
|
||||
}
|
||||
impl ActuatorLimitsCandidate {
|
||||
pub(super) fn validate(self) -> Result<(), ConfigViolation> {
|
||||
self.validate_fields()?;
|
||||
fn validate_actuator_limits(&self) -> Result<(), ConfigViolation> {
|
||||
if self.minimum >= self.maximum {
|
||||
return Err(ConfigViolation::related(
|
||||
ids::ACTUATOR_MINIMUM,
|
||||
|
||||
@@ -1,41 +1,20 @@
|
||||
//! Stable field identities for the current vehicle configuration schema.
|
||||
//! Symbolic projections used by relation checks and focused tests.
|
||||
//!
|
||||
//! These constants never define an ID. Each projects the metadata generated
|
||||
//! from its owning `#[config(id = ...)]` declaration.
|
||||
|
||||
use super::{
|
||||
control::{
|
||||
ActuatorLimitsCandidate, AidingCandidate, ControlCandidate, PidCandidate, ThrustCandidate,
|
||||
},
|
||||
observability::ObservabilityCandidate,
|
||||
safety::SafetyCandidate,
|
||||
timing::TimingCandidate,
|
||||
};
|
||||
use kaptan_config::FieldId;
|
||||
|
||||
// These symbolic names are projections of derive-generated metadata. No field
|
||||
// identifier is handwritten outside the field declaration itself.
|
||||
pub(super) const PREARM_DWELL_MICROSECONDS: FieldId = SafetyCandidate::DESCRIPTORS[0].id;
|
||||
use super::{
|
||||
control::{ActuatorLimitsCandidate, PidCandidate, ThrustCandidate},
|
||||
timing::TimingCandidate,
|
||||
};
|
||||
|
||||
pub(super) const MAXIMUM_IMU_INTERVAL_MICROSECONDS: FieldId = TimingCandidate::DESCRIPTORS[0].id;
|
||||
pub(super) const IMU_FRESHNESS_MICROSECONDS: FieldId = TimingCandidate::DESCRIPTORS[1].id;
|
||||
pub(super) const LOCAL_STATE_FRESHNESS_MICROSECONDS: FieldId = TimingCandidate::DESCRIPTORS[2].id;
|
||||
pub(super) const COMMAND_LINK_FRESHNESS_MICROSECONDS: FieldId = TimingCandidate::DESCRIPTORS[3].id;
|
||||
pub(super) const POSITION_VARIANCE_M2: FieldId = AidingCandidate::DESCRIPTORS[0].id;
|
||||
pub(super) const VELOCITY_VARIANCE_M2_PER_SECOND_SQUARED: FieldId =
|
||||
AidingCandidate::DESCRIPTORS[1].id;
|
||||
pub(super) const POSITION_GAIN_PER_SECOND_SQUARED: FieldId = ControlCandidate::DESCRIPTORS[0].id;
|
||||
pub(super) const VELOCITY_GAIN_PER_SECOND: FieldId = ControlCandidate::DESCRIPTORS[1].id;
|
||||
pub(super) const MAXIMUM_HORIZONTAL_ACCELERATION_MPS2: FieldId =
|
||||
ControlCandidate::DESCRIPTORS[2].id;
|
||||
pub(super) const MAXIMUM_UPWARD_ACCELERATION_MPS2: FieldId = ControlCandidate::DESCRIPTORS[3].id;
|
||||
pub(super) const MAXIMUM_DOWNWARD_ACCELERATION_MPS2: FieldId = ControlCandidate::DESCRIPTORS[4].id;
|
||||
pub(super) const MASS_KG: FieldId = ThrustCandidate::DESCRIPTORS[0].id;
|
||||
pub(super) const MAXIMUM_COLLECTIVE_THRUST_NEWTONS: FieldId = ThrustCandidate::DESCRIPTORS[1].id;
|
||||
pub(super) const MAXIMUM_TILT_TANGENT: FieldId = ThrustCandidate::DESCRIPTORS[2].id;
|
||||
pub(super) const RATE_PID_PROPORTIONAL: FieldId = PidCandidate::DESCRIPTORS[0].id;
|
||||
pub(super) const RATE_PID_INTEGRAL: FieldId = PidCandidate::DESCRIPTORS[1].id;
|
||||
pub(super) const RATE_PID_DERIVATIVE: FieldId = PidCandidate::DESCRIPTORS[2].id;
|
||||
pub(super) const RATE_PID_OUTPUT_LIMIT: FieldId = PidCandidate::DESCRIPTORS[3].id;
|
||||
pub(super) const RATE_PID_INTEGRAL_LIMIT: FieldId = PidCandidate::DESCRIPTORS[4].id;
|
||||
pub(super) const ACTUATOR_MINIMUM: FieldId = ActuatorLimitsCandidate::DESCRIPTORS[0].id;
|
||||
pub(super) const ACTUATOR_MAXIMUM: FieldId = ActuatorLimitsCandidate::DESCRIPTORS[1].id;
|
||||
pub(super) const ACTUATOR_SLEW_PER_SECOND: FieldId = ActuatorLimitsCandidate::DESCRIPTORS[2].id;
|
||||
pub(super) const OBSERVABILITY_MAXIMUM_LOG_LEVEL: FieldId =
|
||||
ObservabilityCandidate::DESCRIPTORS[0].id;
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Persisted, restart-only runtime-observability policy.
|
||||
|
||||
use kaptan_config::{ConfigValidate, ConfigViolation, ValidateFields};
|
||||
use kaptan_config::ConfigValidate;
|
||||
|
||||
/// Candidate-only observability policy. It deliberately controls only optional
|
||||
/// detail; error-level records are never suppressible configuration fields.
|
||||
@@ -16,12 +16,6 @@ pub(super) struct ObservabilityCandidate {
|
||||
pub(super) maximum_log_level: u32,
|
||||
}
|
||||
|
||||
impl ObservabilityCandidate {
|
||||
pub(super) fn validate(self) -> Result<(), ConfigViolation> {
|
||||
self.validate_fields()
|
||||
}
|
||||
}
|
||||
|
||||
/// Immutable boot-active best-effort observability policy.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct ObservabilitySettings {
|
||||
|
||||
@@ -1,35 +1,33 @@
|
||||
//! Current field registry and local candidate mutation mapping.
|
||||
//! Aggregate descriptor registry for the generated vehicle sections.
|
||||
//!
|
||||
//! Every descriptor is generated from its owning scalar field. This ordered
|
||||
//! projection is intentionally exhaustive: `FIELD_COUNT` comes from the root
|
||||
//! aggregate, so adding an attributed field without extending this view fails
|
||||
//! at compile time instead of silently omitting it from CBOR or transport.
|
||||
|
||||
use kaptan_config::{
|
||||
ConfigValue, ConfigViolation, FieldDescriptor, FieldId, FieldWriteError, ValidationRule,
|
||||
ValueKind,
|
||||
};
|
||||
use kaptan_config::FieldDescriptor;
|
||||
|
||||
use super::{
|
||||
candidate::CandidateProfile,
|
||||
control::{
|
||||
ActuatorLimitsCandidate, AidingCandidate, ControlCandidate, PidCandidate, ThrustCandidate,
|
||||
},
|
||||
ids,
|
||||
observability::ObservabilityCandidate,
|
||||
safety::SafetyCandidate,
|
||||
timing::TimingCandidate,
|
||||
};
|
||||
|
||||
/// Number of operator-editable fields in the current SITL Quad-X profile.
|
||||
/// Number of operator-editable fields in the current vehicle profile.
|
||||
///
|
||||
/// Allocation topology is immutable board capability data, not an operator
|
||||
/// parameter. It requires a separate physical-airframe safety workflow.
|
||||
pub(super) const FIELD_COUNT: usize = SafetyCandidate::DESCRIPTORS.len()
|
||||
+ TimingCandidate::DESCRIPTORS.len()
|
||||
+ AidingCandidate::DESCRIPTORS.len()
|
||||
+ ControlCandidate::DESCRIPTORS.len()
|
||||
+ ThrustCandidate::DESCRIPTORS.len()
|
||||
+ PidCandidate::DESCRIPTORS.len()
|
||||
+ ActuatorLimitsCandidate::DESCRIPTORS.len()
|
||||
+ ObservabilityCandidate::DESCRIPTORS.len();
|
||||
/// The registry is an ordered view of metadata generated from field attributes.
|
||||
/// It owns no IDs, bounds, defaults, or display text.
|
||||
/// Allocation topology remains immutable board capability data, not an
|
||||
/// operator parameter.
|
||||
pub(super) const FIELD_COUNT: usize = CandidateProfile::FIELD_COUNT;
|
||||
|
||||
/// Ordered view of metadata generated from field attributes.
|
||||
///
|
||||
/// This has no independently-maintained defaults, bounds, values, or setter
|
||||
/// logic. The length is tied to `CandidateProfile::FIELD_COUNT` as a
|
||||
/// compile-time completeness guard.
|
||||
const FIELDS: [FieldDescriptor; FIELD_COUNT] = [
|
||||
SafetyCandidate::DESCRIPTORS[0],
|
||||
TimingCandidate::DESCRIPTORS[0],
|
||||
@@ -77,332 +75,6 @@ pub(super) const fn descriptors() -> &'static [FieldDescriptor; FIELD_COUNT] {
|
||||
&FIELDS
|
||||
}
|
||||
|
||||
/// Constructs the vehicle candidate entirely from derive-generated defaults.
|
||||
pub(super) fn default_candidate() -> CandidateProfile {
|
||||
CandidateProfile {
|
||||
safety: SafetyCandidate {
|
||||
prearm_dwell_microseconds: default_u64(ids::PREARM_DWELL_MICROSECONDS),
|
||||
},
|
||||
timing: TimingCandidate {
|
||||
maximum_imu_interval_microseconds: default_u64(ids::MAXIMUM_IMU_INTERVAL_MICROSECONDS),
|
||||
imu_freshness_microseconds: default_u64(ids::IMU_FRESHNESS_MICROSECONDS),
|
||||
local_state_freshness_microseconds: default_u64(
|
||||
ids::LOCAL_STATE_FRESHNESS_MICROSECONDS,
|
||||
),
|
||||
command_link_freshness_microseconds: default_u64(
|
||||
ids::COMMAND_LINK_FRESHNESS_MICROSECONDS,
|
||||
),
|
||||
},
|
||||
aiding: AidingCandidate {
|
||||
position_variance_m2: default_f32(ids::POSITION_VARIANCE_M2),
|
||||
velocity_variance_m2_per_second_squared: default_f32(
|
||||
ids::VELOCITY_VARIANCE_M2_PER_SECOND_SQUARED,
|
||||
),
|
||||
},
|
||||
control: ControlCandidate {
|
||||
position_gain_per_second_squared: default_f32(ids::POSITION_GAIN_PER_SECOND_SQUARED),
|
||||
velocity_gain_per_second: default_f32(ids::VELOCITY_GAIN_PER_SECOND),
|
||||
maximum_horizontal_acceleration_mps2: default_f32(
|
||||
ids::MAXIMUM_HORIZONTAL_ACCELERATION_MPS2,
|
||||
),
|
||||
maximum_upward_acceleration_mps2: default_f32(ids::MAXIMUM_UPWARD_ACCELERATION_MPS2),
|
||||
maximum_downward_acceleration_mps2: default_f32(
|
||||
ids::MAXIMUM_DOWNWARD_ACCELERATION_MPS2,
|
||||
),
|
||||
},
|
||||
thrust: ThrustCandidate {
|
||||
mass_kg: default_f32(ids::MASS_KG),
|
||||
maximum_collective_thrust_newtons: default_f32(ids::MAXIMUM_COLLECTIVE_THRUST_NEWTONS),
|
||||
maximum_tilt_tangent: default_f32(ids::MAXIMUM_TILT_TANGENT),
|
||||
},
|
||||
rate_pid: PidCandidate {
|
||||
proportional: default_f32(ids::RATE_PID_PROPORTIONAL),
|
||||
integral: default_f32(ids::RATE_PID_INTEGRAL),
|
||||
derivative: default_f32(ids::RATE_PID_DERIVATIVE),
|
||||
output_limit: default_f32(ids::RATE_PID_OUTPUT_LIMIT),
|
||||
integral_limit: default_f32(ids::RATE_PID_INTEGRAL_LIMIT),
|
||||
},
|
||||
actuator_limits: ActuatorLimitsCandidate {
|
||||
minimum: default_f32(ids::ACTUATOR_MINIMUM),
|
||||
maximum: default_f32(ids::ACTUATOR_MAXIMUM),
|
||||
slew_per_second: default_f32(ids::ACTUATOR_SLEW_PER_SECOND),
|
||||
},
|
||||
observability: ObservabilityCandidate {
|
||||
maximum_log_level: default_u32(ids::OBSERVABILITY_MAXIMUM_LOG_LEVEL),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn descriptor(index: u16) -> Option<&'static FieldDescriptor> {
|
||||
FIELDS.get(usize::from(index))
|
||||
}
|
||||
|
||||
pub(super) fn field_value(candidate: &CandidateProfile, id: FieldId) -> Option<ConfigValue> {
|
||||
Some(match id {
|
||||
ids::PREARM_DWELL_MICROSECONDS => {
|
||||
ConfigValue::U64(candidate.safety.prearm_dwell_microseconds)
|
||||
}
|
||||
ids::MAXIMUM_IMU_INTERVAL_MICROSECONDS => {
|
||||
ConfigValue::U64(candidate.timing.maximum_imu_interval_microseconds)
|
||||
}
|
||||
ids::IMU_FRESHNESS_MICROSECONDS => {
|
||||
ConfigValue::U64(candidate.timing.imu_freshness_microseconds)
|
||||
}
|
||||
ids::LOCAL_STATE_FRESHNESS_MICROSECONDS => {
|
||||
ConfigValue::U64(candidate.timing.local_state_freshness_microseconds)
|
||||
}
|
||||
ids::COMMAND_LINK_FRESHNESS_MICROSECONDS => {
|
||||
ConfigValue::U64(candidate.timing.command_link_freshness_microseconds)
|
||||
}
|
||||
ids::POSITION_VARIANCE_M2 => ConfigValue::F32(candidate.aiding.position_variance_m2),
|
||||
ids::VELOCITY_VARIANCE_M2_PER_SECOND_SQUARED => {
|
||||
ConfigValue::F32(candidate.aiding.velocity_variance_m2_per_second_squared)
|
||||
}
|
||||
ids::POSITION_GAIN_PER_SECOND_SQUARED => {
|
||||
ConfigValue::F32(candidate.control.position_gain_per_second_squared)
|
||||
}
|
||||
ids::VELOCITY_GAIN_PER_SECOND => {
|
||||
ConfigValue::F32(candidate.control.velocity_gain_per_second)
|
||||
}
|
||||
ids::MAXIMUM_HORIZONTAL_ACCELERATION_MPS2 => {
|
||||
ConfigValue::F32(candidate.control.maximum_horizontal_acceleration_mps2)
|
||||
}
|
||||
ids::MAXIMUM_UPWARD_ACCELERATION_MPS2 => {
|
||||
ConfigValue::F32(candidate.control.maximum_upward_acceleration_mps2)
|
||||
}
|
||||
ids::MAXIMUM_DOWNWARD_ACCELERATION_MPS2 => {
|
||||
ConfigValue::F32(candidate.control.maximum_downward_acceleration_mps2)
|
||||
}
|
||||
ids::MASS_KG => ConfigValue::F32(candidate.thrust.mass_kg),
|
||||
ids::MAXIMUM_COLLECTIVE_THRUST_NEWTONS => {
|
||||
ConfigValue::F32(candidate.thrust.maximum_collective_thrust_newtons)
|
||||
}
|
||||
ids::MAXIMUM_TILT_TANGENT => ConfigValue::F32(candidate.thrust.maximum_tilt_tangent),
|
||||
ids::RATE_PID_PROPORTIONAL => ConfigValue::F32(candidate.rate_pid.proportional),
|
||||
ids::RATE_PID_INTEGRAL => ConfigValue::F32(candidate.rate_pid.integral),
|
||||
ids::RATE_PID_DERIVATIVE => ConfigValue::F32(candidate.rate_pid.derivative),
|
||||
ids::RATE_PID_OUTPUT_LIMIT => ConfigValue::F32(candidate.rate_pid.output_limit),
|
||||
ids::RATE_PID_INTEGRAL_LIMIT => ConfigValue::F32(candidate.rate_pid.integral_limit),
|
||||
ids::ACTUATOR_MINIMUM => ConfigValue::F32(candidate.actuator_limits.minimum),
|
||||
ids::ACTUATOR_MAXIMUM => ConfigValue::F32(candidate.actuator_limits.maximum),
|
||||
ids::ACTUATOR_SLEW_PER_SECOND => {
|
||||
ConfigValue::F32(candidate.actuator_limits.slew_per_second)
|
||||
}
|
||||
ids::OBSERVABILITY_MAXIMUM_LOG_LEVEL => {
|
||||
ConfigValue::U32(candidate.observability.maximum_log_level)
|
||||
}
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn set_field(
|
||||
candidate: &mut CandidateProfile,
|
||||
id: FieldId,
|
||||
value: ConfigValue,
|
||||
) -> Result<(), FieldWriteError> {
|
||||
let value = match descriptor_for(id) {
|
||||
Some(descriptor) => match descriptor.kind {
|
||||
ValueKind::U64 => ConfigValue::U64(validate_u64(id, value, descriptor)?),
|
||||
ValueKind::U32 => ConfigValue::U32(validate_u32(id, value, descriptor)?),
|
||||
ValueKind::F32 => ConfigValue::F32(validate_f32(id, value, descriptor)?),
|
||||
kind => {
|
||||
return Err(FieldWriteError::WrongType {
|
||||
field: id,
|
||||
expected: kind,
|
||||
actual: value.kind(),
|
||||
});
|
||||
}
|
||||
},
|
||||
None => return Err(FieldWriteError::UnknownField(id)),
|
||||
};
|
||||
match value {
|
||||
ConfigValue::U64(value) => match id {
|
||||
ids::PREARM_DWELL_MICROSECONDS => candidate.safety.prearm_dwell_microseconds = value,
|
||||
ids::MAXIMUM_IMU_INTERVAL_MICROSECONDS => {
|
||||
candidate.timing.maximum_imu_interval_microseconds = value
|
||||
}
|
||||
ids::IMU_FRESHNESS_MICROSECONDS => candidate.timing.imu_freshness_microseconds = value,
|
||||
ids::LOCAL_STATE_FRESHNESS_MICROSECONDS => {
|
||||
candidate.timing.local_state_freshness_microseconds = value
|
||||
}
|
||||
ids::COMMAND_LINK_FRESHNESS_MICROSECONDS => {
|
||||
candidate.timing.command_link_freshness_microseconds = value
|
||||
}
|
||||
_ => return Err(FieldWriteError::UnknownField(id)),
|
||||
},
|
||||
ConfigValue::U32(value) => match id {
|
||||
ids::OBSERVABILITY_MAXIMUM_LOG_LEVEL => {
|
||||
candidate.observability.maximum_log_level = value
|
||||
}
|
||||
_ => return Err(FieldWriteError::UnknownField(id)),
|
||||
},
|
||||
ConfigValue::F32(value) => match id {
|
||||
ids::POSITION_VARIANCE_M2 => candidate.aiding.position_variance_m2 = value,
|
||||
ids::VELOCITY_VARIANCE_M2_PER_SECOND_SQUARED => {
|
||||
candidate.aiding.velocity_variance_m2_per_second_squared = value
|
||||
}
|
||||
ids::POSITION_GAIN_PER_SECOND_SQUARED => {
|
||||
candidate.control.position_gain_per_second_squared = value
|
||||
}
|
||||
ids::VELOCITY_GAIN_PER_SECOND => candidate.control.velocity_gain_per_second = value,
|
||||
ids::MAXIMUM_HORIZONTAL_ACCELERATION_MPS2 => {
|
||||
candidate.control.maximum_horizontal_acceleration_mps2 = value
|
||||
}
|
||||
ids::MAXIMUM_UPWARD_ACCELERATION_MPS2 => {
|
||||
candidate.control.maximum_upward_acceleration_mps2 = value
|
||||
}
|
||||
ids::MAXIMUM_DOWNWARD_ACCELERATION_MPS2 => {
|
||||
candidate.control.maximum_downward_acceleration_mps2 = value
|
||||
}
|
||||
ids::MASS_KG => candidate.thrust.mass_kg = value,
|
||||
ids::MAXIMUM_COLLECTIVE_THRUST_NEWTONS => {
|
||||
candidate.thrust.maximum_collective_thrust_newtons = value
|
||||
}
|
||||
ids::MAXIMUM_TILT_TANGENT => candidate.thrust.maximum_tilt_tangent = value,
|
||||
ids::RATE_PID_PROPORTIONAL => candidate.rate_pid.proportional = value,
|
||||
ids::RATE_PID_INTEGRAL => candidate.rate_pid.integral = value,
|
||||
ids::RATE_PID_DERIVATIVE => candidate.rate_pid.derivative = value,
|
||||
ids::RATE_PID_OUTPUT_LIMIT => candidate.rate_pid.output_limit = value,
|
||||
ids::RATE_PID_INTEGRAL_LIMIT => candidate.rate_pid.integral_limit = value,
|
||||
ids::ACTUATOR_MINIMUM => candidate.actuator_limits.minimum = value,
|
||||
ids::ACTUATOR_MAXIMUM => candidate.actuator_limits.maximum = value,
|
||||
ids::ACTUATOR_SLEW_PER_SECOND => candidate.actuator_limits.slew_per_second = value,
|
||||
_ => return Err(FieldWriteError::UnknownField(id)),
|
||||
},
|
||||
ConfigValue::Bool(_) | ConfigValue::I32(_) | ConfigValue::F64(_) => {
|
||||
return Err(FieldWriteError::WrongType {
|
||||
field: id,
|
||||
expected: descriptor_for(id).expect("checked descriptor").kind,
|
||||
actual: value.kind(),
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_u32(
|
||||
id: FieldId,
|
||||
value: ConfigValue,
|
||||
descriptor: &FieldDescriptor,
|
||||
) -> Result<u32, FieldWriteError> {
|
||||
let ConfigValue::U32(value) = value else {
|
||||
return Err(FieldWriteError::WrongType {
|
||||
field: id,
|
||||
expected: ValueKind::U32,
|
||||
actual: value.kind(),
|
||||
});
|
||||
};
|
||||
let Some(ConfigValue::U32(minimum)) = descriptor.minimum else {
|
||||
unreachable!()
|
||||
};
|
||||
let Some(ConfigValue::U32(maximum)) = descriptor.maximum else {
|
||||
unreachable!()
|
||||
};
|
||||
if value < minimum {
|
||||
return Err(FieldWriteError::Violation(ConfigViolation::field(
|
||||
id,
|
||||
ValidationRule::BelowMinimum,
|
||||
)));
|
||||
}
|
||||
if value > maximum {
|
||||
return Err(FieldWriteError::Violation(ConfigViolation::field(
|
||||
id,
|
||||
ValidationRule::AboveMaximum,
|
||||
)));
|
||||
}
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
fn descriptor_for(id: FieldId) -> Option<&'static FieldDescriptor> {
|
||||
FIELDS.iter().find(|descriptor| descriptor.id == id)
|
||||
}
|
||||
|
||||
fn default_u32(id: FieldId) -> u32 {
|
||||
match descriptor_for(id).expect("generated descriptor").default {
|
||||
ConfigValue::U32(value) => value,
|
||||
_ => unreachable!("generated descriptor type"),
|
||||
}
|
||||
}
|
||||
|
||||
fn default_u64(id: FieldId) -> u64 {
|
||||
match descriptor_for(id).expect("generated descriptor").default {
|
||||
ConfigValue::U64(value) => value,
|
||||
_ => unreachable!("generated descriptor type"),
|
||||
}
|
||||
}
|
||||
|
||||
fn default_f32(id: FieldId) -> f32 {
|
||||
match descriptor_for(id).expect("generated descriptor").default {
|
||||
ConfigValue::F32(value) => value,
|
||||
_ => unreachable!("generated descriptor type"),
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_u64(
|
||||
id: FieldId,
|
||||
value: ConfigValue,
|
||||
descriptor: &FieldDescriptor,
|
||||
) -> Result<u64, FieldWriteError> {
|
||||
let ConfigValue::U64(value) = value else {
|
||||
return Err(FieldWriteError::WrongType {
|
||||
field: id,
|
||||
expected: ValueKind::U64,
|
||||
actual: value.kind(),
|
||||
});
|
||||
};
|
||||
let Some(ConfigValue::U64(minimum)) = descriptor.minimum else {
|
||||
unreachable!()
|
||||
};
|
||||
let Some(ConfigValue::U64(maximum)) = descriptor.maximum else {
|
||||
unreachable!()
|
||||
};
|
||||
if value < minimum {
|
||||
return Err(FieldWriteError::Violation(ConfigViolation::field(
|
||||
id,
|
||||
ValidationRule::BelowMinimum,
|
||||
)));
|
||||
}
|
||||
if value > maximum {
|
||||
return Err(FieldWriteError::Violation(ConfigViolation::field(
|
||||
id,
|
||||
ValidationRule::AboveMaximum,
|
||||
)));
|
||||
}
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
fn validate_f32(
|
||||
id: FieldId,
|
||||
value: ConfigValue,
|
||||
descriptor: &FieldDescriptor,
|
||||
) -> Result<f32, FieldWriteError> {
|
||||
let ConfigValue::F32(value) = value else {
|
||||
return Err(FieldWriteError::WrongType {
|
||||
field: id,
|
||||
expected: ValueKind::F32,
|
||||
actual: value.kind(),
|
||||
});
|
||||
};
|
||||
if !value.is_finite() {
|
||||
return Err(FieldWriteError::non_finite(id));
|
||||
}
|
||||
let Some(ConfigValue::F32(minimum)) = descriptor.minimum else {
|
||||
unreachable!()
|
||||
};
|
||||
let Some(ConfigValue::F32(maximum)) = descriptor.maximum else {
|
||||
unreachable!()
|
||||
};
|
||||
if value < minimum {
|
||||
return Err(FieldWriteError::Violation(ConfigViolation::field(
|
||||
id,
|
||||
ValidationRule::BelowMinimum,
|
||||
)));
|
||||
}
|
||||
if value > maximum {
|
||||
return Err(FieldWriteError::Violation(ConfigViolation::field(
|
||||
id,
|
||||
ValidationRule::AboveMaximum,
|
||||
)));
|
||||
}
|
||||
Ok(value)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
use kaptan_config::{ConfigValidate, ConfigViolation, ValidateFields};
|
||||
use kaptan_config::ConfigValidate;
|
||||
|
||||
#[derive(Clone, Copy, ConfigValidate)]
|
||||
pub(super) struct SafetyCandidate {
|
||||
@@ -12,9 +12,3 @@ pub(super) struct SafetyCandidate {
|
||||
)]
|
||||
pub(super) prearm_dwell_microseconds: u64,
|
||||
}
|
||||
|
||||
impl SafetyCandidate {
|
||||
pub(super) fn validate(self) -> Result<(), ConfigViolation> {
|
||||
self.validate_fields()
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
use kaptan_config::{ConfigValidate, ConfigViolation, ValidateFields, ValidationRule};
|
||||
use kaptan_config::{ConfigValidate, ConfigViolation, ValidationRule};
|
||||
|
||||
use super::ids;
|
||||
|
||||
#[derive(Clone, Copy, ConfigValidate)]
|
||||
#[config(post = Self::validate_timing)]
|
||||
pub(super) struct TimingCandidate {
|
||||
#[config(
|
||||
id = 2,
|
||||
@@ -43,8 +44,7 @@ pub(super) struct TimingCandidate {
|
||||
}
|
||||
|
||||
impl TimingCandidate {
|
||||
pub(super) fn validate(self) -> Result<(), ConfigViolation> {
|
||||
self.validate_fields()?;
|
||||
fn validate_timing(&self) -> Result<(), ConfigViolation> {
|
||||
if self.maximum_imu_interval_microseconds > self.imu_freshness_microseconds {
|
||||
return Err(ConfigViolation::related(
|
||||
ids::MAXIMUM_IMU_INTERVAL_MICROSECONDS,
|
||||
|
||||
Reference in new issue
Block a user