libs/kaptan-board: define portable board configuration
This commit is contained in:
1 parent
5623ebd1f3
commit
b904255bf5
2 files changed
+195
No files matched your search
@@ -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]
|
||||
@@ -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<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,
|
||||
}
|
||||
|
||||
/// 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)
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user