From b904255bf547bdc80573592bf239cf2aa2af1476 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20=C3=96zkan?= Date: Sat, 1 Aug 2026 20:54:03 +0300 Subject: [PATCH] libs/kaptan-board: define portable board configuration --- libs/kaptan-board/Cargo.toml | 12 +++ libs/kaptan-board/src/lib.rs | 183 +++++++++++++++++++++++++++++++++++ 2 files changed, 195 insertions(+) create mode 100644 libs/kaptan-board/Cargo.toml create mode 100644 libs/kaptan-board/src/lib.rs diff --git a/libs/kaptan-board/Cargo.toml b/libs/kaptan-board/Cargo.toml new file mode 100644 index 0000000..27523a2 --- /dev/null +++ b/libs/kaptan-board/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "kaptan-board" +edition.workspace = true +license.workspace = true +repository.workspace = true +rust-version.workspace = true +version.workspace = true + +[lints] +workspace = true + +[dependencies] diff --git a/libs/kaptan-board/src/lib.rs b/libs/kaptan-board/src/lib.rs new file mode 100644 index 0000000..5406f9e --- /dev/null +++ b/libs/kaptan-board/src/lib.rs @@ -0,0 +1,183 @@ +//! 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] + +/// 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, +} + +/// 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, + /// Optical-flow publication rate. `None` means flow is not published. + pub optical_flow_hz: Option, + /// Link heartbeat publication rate. + pub heartbeat_hz: usize, +} + +/// Static board identity, capability, and data-channel 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. +#[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, + /// Supported vertical-sensing capability. + pub vertical_sensing: VerticalSensingKind, + /// Data-channel timer and publication rates. + pub data_channel: DataChannelRates, +} + +impl BoardConfig { + /// Validates rate bounds and the one-frame-per-service-tick budget. + pub const fn validate(self) -> Result<(), BoardConfigError> { + let rates = self.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); + } + + match self.vertical_sensing { + VerticalSensingKind::None + if rates.range_hz.is_none() && rates.optical_flow_hz.is_none() => + { + Ok(()) + } + VerticalSensingKind::Range + if rates.range_hz.is_some() && rates.optical_flow_hz.is_none() => + { + Ok(()) + } + VerticalSensingKind::RangeAndOpticalFlow + if rates.range_hz.is_some() && rates.optical_flow_hz.is_some() => + { + Ok(()) + } + _ => Err(BoardConfigError::CapabilityDataChannelMismatch), + } + } +} + +/// Invalid board-support configuration. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum BoardConfigError { + /// 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, +} + +#[cfg(test)] +mod tests { + use super::{ + BoardConfig, BoardConfigError, DataChannelRates, MavlinkEndpoint, VerticalSensingKind, + }; + + const BASE: BoardConfig = BoardConfig { + name: "test", + mavlink: MavlinkEndpoint { + system_id: 1, + component_id: 1, + }, + 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, + }, + }; + + #[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; + config.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; + config.vertical_sensing = VerticalSensingKind::Range; + + assert_eq!( + config.validate(), + Err(BoardConfigError::CapabilityDataChannelMismatch) + ); + } +}