refactor: move firmware workspace under firmware

This commit is contained in:
Onur Özkan committed 2026-08-10 16:58:55 +03:00
1 parent 153b0bf262
commit f03fc119a4
222 files changed
+56 -13

No files matched your search

+339
View File
@@ -0,0 +1,339 @@
//! Portable board-support contract for Kaptan.
//!
//! This crate describes logical board identity, sensing capability and
//! outbound data-channel policy. It intentionally has no dependency on an MCU
//! family, peripheral access crate, RTIC, or a runtime framework.
#![no_std]
use kaptan_log::KaptanError;
pub use kaptan_log::Severity;
/// Physical IMU device selected by a board declaration.
///
/// This is immutable board topology, not a runtime-selectable sensor driver.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ImuDevice {
/// InvenSense MPU-6000 on the board's selected SPI route.
Mpu6000,
/// TDK ICM-42688-P on the board's selected SPI route.
Icm42688,
}
/// Fixed board-to-body IMU mounting transform.
///
/// Calibration remains a separate, safety-gated workflow. This declaration
/// only selects the compiled transform for the physical board route.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ImuMount {
/// Sensor axes already match the controller body axes.
BodyAligned,
/// Sensor is yawed 180° relative to the controller body axes.
Yaw180,
}
/// Logical outbound MAVLink serial endpoint selected by a board declaration.
///
/// The platform maps these logical names to its checked pin/DMA routes.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum MavlinkPort {
/// First supported board serial endpoint.
Serial1,
/// Second supported board serial endpoint.
Serial2,
}
/// The vertical-sensing data a board support package makes available.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[repr(u8)]
pub enum VerticalSensingKind {
/// No range or optical-flow data is supported.
None,
/// Range data is supported.
Range,
/// Coherent range and optical-flow data are supported.
RangeAndOpticalFlow,
}
/// MAVLink endpoint identity selected by one board support package.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct MavlinkEndpoint {
/// MAVLink system identifier.
pub system_id: u8,
/// MAVLink component identifier.
pub component_id: u8,
/// Immutable physical serial endpoint selected for this board image.
pub port: MavlinkPort,
/// UART line rate used by that physical endpoint.
pub baud: u32,
}
/// Fixed outbound data-channel rates driven by a platform service timer.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct DataChannelRates {
/// Frequency of the timer that calls the data-channel scheduler.
pub service_hz: usize,
/// Raw-IMU publication rate.
pub raw_imu_hz: usize,
/// Range publication rate. `None` means range is not published.
pub range_hz: Option<usize>,
/// Optical-flow publication rate. `None` means flow is not published.
pub optical_flow_hz: Option<usize>,
/// Link heartbeat publication rate.
pub heartbeat_hz: usize,
}
/// Telemetry-only physical-sensor and outbound-data policy.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct TelemetryProfile {
/// Immutable physical IMU driver selected by this board image.
pub imu: ImuDevice,
/// Immutable board-to-body transform applied to that IMU's samples.
pub imu_mount: ImuMount,
/// Supported physical vertical-sensing capability.
pub vertical_sensing: VerticalSensingKind,
/// Data-channel timer and publication rates.
pub data_channel: DataChannelRates,
/// Highest optional diagnostic severity forwarded to MAVLink.
///
/// Errors remain observable even when optional debug/info diagnostics are
/// filtered. This setting is compile-time board policy until the target
/// has the reviewed configuration activation path.
pub maximum_log_level: Severity,
}
/// Exclusive build-time image selection.
///
/// `ExternalControlOnly` remains representable for an explicit validation
/// rejection rather than silently falling back to telemetry.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum BoardProfile {
/// Physical-sensor telemetry image.
TelemetryOnly(TelemetryProfile),
/// Reserved and deliberately unimplemented production authority profile.
ExternalControlOnly,
}
/// Static board identity and exclusive build-time image configuration.
///
/// The selected platform trait determines the MCU family. Keeping a free-form
/// MCU string here would duplicate that type-level fact without validating it.
///
/// Board policy is compiled and non-deserializable. The current F722 platform
/// validates it against typestate-selected electrical routes before it touches
/// hardware. Target-side runtime configuration remains disabled until its
/// separate authenticated, journalled, disarmed activation contract exists.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct BoardConfig {
/// Stable board name used in build and diagnostic tooling.
pub name: &'static str,
/// MAVLink endpoint identity.
pub mavlink: MavlinkEndpoint,
/// Exactly one immutable image profile.
pub profile: BoardProfile,
}
impl BoardConfig {
/// Returns physical telemetry configuration only for a telemetry image.
pub const fn telemetry(self) -> Option<TelemetryProfile> {
match self.profile {
BoardProfile::TelemetryOnly(profile) => Some(profile),
BoardProfile::ExternalControlOnly => None,
}
}
/// Validates the selected exclusive profile.
pub const fn validate(self) -> Result<(), BoardConfigError> {
if self.mavlink.system_id == 0 || self.mavlink.component_id == 0 {
return Err(BoardConfigError::InvalidMavlinkEndpoint);
}
if self.mavlink.baud == 0 {
return Err(BoardConfigError::ZeroMavlinkBaud);
}
match self.profile {
BoardProfile::TelemetryOnly(profile) => Self::validate_telemetry(profile),
BoardProfile::ExternalControlOnly => {
// TODO(flight-config): replace this hard rejection only with a
// complete runtime and atomic configuration activation. Required
// inputs are vehicle geometry, calibrated sensors, actuator
// limits, authenticated MAVLink-v2 authority, persistent replay
// state, estimator/controller/allocator readiness, and a
// verified output watchdog.
Err(BoardConfigError::UnsupportedFlightProfile)
}
}
}
const fn validate_telemetry(profile: TelemetryProfile) -> Result<(), BoardConfigError> {
let rates = profile.data_channel;
if rates.service_hz == 0 {
return Err(BoardConfigError::ZeroServiceFrequency);
}
if rates.raw_imu_hz == 0 || rates.heartbeat_hz == 0 {
return Err(BoardConfigError::MissingRequiredDataChannel);
}
if rates.raw_imu_hz > rates.service_hz || rates.heartbeat_hz > rates.service_hz {
return Err(BoardConfigError::RateExceedsServiceFrequency);
}
let Some(mut scheduled_hz) = rates.raw_imu_hz.checked_add(rates.heartbeat_hz) else {
return Err(BoardConfigError::DataChannelOvercommitted);
};
if let Some(range_hz) = rates.range_hz {
if range_hz == 0 || range_hz > rates.service_hz {
return Err(BoardConfigError::RateExceedsServiceFrequency);
}
let Some(total) = scheduled_hz.checked_add(range_hz) else {
return Err(BoardConfigError::DataChannelOvercommitted);
};
scheduled_hz = total;
}
if let Some(flow_hz) = rates.optical_flow_hz {
if flow_hz == 0 || flow_hz > rates.service_hz {
return Err(BoardConfigError::RateExceedsServiceFrequency);
}
let Some(total) = scheduled_hz.checked_add(flow_hz) else {
return Err(BoardConfigError::DataChannelOvercommitted);
};
scheduled_hz = total;
}
if scheduled_hz > rates.service_hz {
return Err(BoardConfigError::DataChannelOvercommitted);
}
let vertical_schedule_is_valid = match profile.vertical_sensing {
VerticalSensingKind::None
if rates.range_hz.is_none() && rates.optical_flow_hz.is_none() =>
{
true
}
VerticalSensingKind::Range
if rates.range_hz.is_some() && rates.optical_flow_hz.is_none() =>
{
true
}
VerticalSensingKind::RangeAndOpticalFlow
if rates.range_hz.is_some() && rates.optical_flow_hz.is_some() =>
{
true
}
_ => false,
};
if !vertical_schedule_is_valid {
return Err(BoardConfigError::CapabilityDataChannelMismatch);
}
Ok(())
}
}
/// Invalid board-support configuration.
#[derive(Clone, Copy, Debug, Eq, PartialEq, KaptanError)]
pub enum BoardConfigError {
/// MAVLink system/component identifiers must be exact nonzero values.
InvalidMavlinkEndpoint,
/// A physical MAVLink endpoint needs a nonzero UART rate.
ZeroMavlinkBaud,
/// A data-channel schedule needs a nonzero service frequency.
ZeroServiceFrequency,
/// Raw IMU and heartbeat are mandatory on every flight board.
MissingRequiredDataChannel,
/// A configured stream is faster than its service timer.
RateExceedsServiceFrequency,
/// The schedule requests more than one frame per service tick.
DataChannelOvercommitted,
/// Declared vertical sensing and published streams disagree.
CapabilityDataChannelMismatch,
/// The selected operation profile has no complete runtime implementation.
UnsupportedFlightProfile,
}
#[cfg(test)]
mod tests {
use super::{
BoardConfig, BoardConfigError, BoardProfile, DataChannelRates, ImuDevice, ImuMount,
MavlinkEndpoint, MavlinkPort, TelemetryProfile, VerticalSensingKind,
};
use kaptan_log::Severity;
const BASE: BoardConfig = BoardConfig {
name: "test",
mavlink: MavlinkEndpoint {
system_id: 1,
component_id: 1,
port: MavlinkPort::Serial1,
baud: 115_200,
},
profile: BoardProfile::TelemetryOnly(TelemetryProfile {
imu: ImuDevice::Mpu6000,
imu_mount: ImuMount::BodyAligned,
vertical_sensing: VerticalSensingKind::RangeAndOpticalFlow,
data_channel: DataChannelRates {
service_hz: 100,
raw_imu_hz: 20,
range_hz: Some(20),
optical_flow_hz: Some(20),
heartbeat_hz: 2,
},
maximum_log_level: Severity::Info,
}),
};
#[test]
fn accepts_a_schedulable_range_and_flow_board() {
assert_eq!(BASE.validate(), Ok(()));
}
#[test]
fn rejects_streams_that_exceed_the_service_budget() {
let mut config = BASE;
let BoardProfile::TelemetryOnly(profile) = &mut config.profile else {
unreachable!();
};
profile.data_channel.optical_flow_hz = Some(80);
assert_eq!(
config.validate(),
Err(BoardConfigError::DataChannelOvercommitted)
);
}
#[test]
fn rejects_capability_and_data_channel_mismatch() {
let mut config = BASE;
let BoardProfile::TelemetryOnly(profile) = &mut config.profile else {
unreachable!();
};
profile.vertical_sensing = VerticalSensingKind::Range;
assert_eq!(
config.validate(),
Err(BoardConfigError::CapabilityDataChannelMismatch)
);
}
#[test]
fn rejects_external_control_until_its_runtime_exists() {
let mut config = BASE;
config.profile = BoardProfile::ExternalControlOnly;
assert_eq!(
config.validate(),
Err(BoardConfigError::UnsupportedFlightProfile)
);
}
#[test]
fn rejects_invalid_immutable_link_policy() {
let mut config = BASE;
config.mavlink.baud = 0;
assert_eq!(config.validate(), Err(BoardConfigError::ZeroMavlinkBaud));
let mut config = BASE;
config.mavlink.component_id = 0;
assert_eq!(
config.validate(),
Err(BoardConfigError::InvalidMavlinkEndpoint)
);
}
}