From c9ca2f42574238fb4af5373d1625df25e91c6fa6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20=C3=96zkan?= Date: Sat, 1 Aug 2026 20:55:02 +0300 Subject: [PATCH] workspace: replace runtime with F722 platform --- Cargo.lock | 81 +++++++------ Cargo.toml | 6 +- libs/kaptan-runtime/Cargo.toml | 17 --- libs/kaptan-runtime/src/imu.rs | 148 ---------------------- libs/kaptan-runtime/src/lib.rs | 20 --- libs/kaptan-runtime/src/mailbox.rs | 189 ----------------------------- libs/kaptan-runtime/src/range.rs | 165 ------------------------- 7 files changed, 50 insertions(+), 576 deletions(-) delete mode 100644 libs/kaptan-runtime/Cargo.toml delete mode 100644 libs/kaptan-runtime/src/imu.rs delete mode 100644 libs/kaptan-runtime/src/lib.rs delete mode 100644 libs/kaptan-runtime/src/mailbox.rs delete mode 100644 libs/kaptan-runtime/src/range.rs diff --git a/Cargo.lock b/Cargo.lock index c8b320e..8c65758 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -139,6 +139,21 @@ dependencies = [ name = "kaptan-actuator" version = "0.1.0" +[[package]] +name = "kaptan-board" +version = "0.1.0" + +[[package]] +name = "kaptan-data-channel" +version = "0.1.0" +dependencies = [ + "kaptan-board", + "kaptan-dma", + "kaptan-mavlink", + "kaptan-sensor", + "kaptan-time", +] + [[package]] name = "kaptan-dma" version = "0.1.0" @@ -153,6 +168,14 @@ dependencies = [ "kaptan-time", ] +[[package]] +name = "kaptan-geprc-f722-v2" +version = "0.1.0" +dependencies = [ + "kaptan-board", + "platform-stm32f7", +] + [[package]] name = "kaptan-i2c" version = "0.1.0" @@ -161,18 +184,8 @@ version = "0.1.0" name = "kaptan-kakute-f7-mini-v3" version = "0.1.0" dependencies = [ - "cortex-m", - "cortex-m-rt", - "kaptan-actuator", - "kaptan-dma", - "kaptan-drivers", - "kaptan-mavlink", - "kaptan-runtime", - "kaptan-spi", - "kaptan-stm32f7", - "panic-halt", - "rtic", - "stm32f7", + "kaptan-board", + "platform-stm32f7", ] [[package]] @@ -188,17 +201,6 @@ dependencies = [ "mavlink-core", ] -[[package]] -name = "kaptan-runtime" -version = "0.1.0" -dependencies = [ - "kaptan-drivers", - "kaptan-i2c", - "kaptan-sensor", - "kaptan-spi", - "kaptan-time", -] - [[package]] name = "kaptan-sensor" version = "0.1.0" @@ -210,18 +212,6 @@ dependencies = [ name = "kaptan-spi" version = "0.1.0" -[[package]] -name = "kaptan-stm32f7" -version = "0.1.0" -dependencies = [ - "cortex-m", - "kaptan-dma", - "kaptan-i2c", - "kaptan-spi", - "kaptan-time", - "stm32f7", -] - [[package]] name = "kaptan-time" version = "0.1.0" @@ -316,6 +306,27 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a513e167849a384b7f9b746e517604398518590a9142f4846a32e3c2a4de7b11" +[[package]] +name = "platform-stm32f7" +version = "0.1.0" +dependencies = [ + "cortex-m", + "cortex-m-rt", + "kaptan-actuator", + "kaptan-board", + "kaptan-data-channel", + "kaptan-dma", + "kaptan-drivers", + "kaptan-i2c", + "kaptan-mavlink", + "kaptan-sensor", + "kaptan-spi", + "kaptan-time", + "panic-halt", + "rtic", + "stm32f7", +] + [[package]] name = "portable-atomic" version = "1.14.0" diff --git a/Cargo.toml b/Cargo.toml index 9d22a2b..68f1666 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,17 +1,19 @@ [workspace] members = [ + "boards/geprc-f722-v2", "boards/kakute-f7-mini-v3", "libs/kaptan-actuator", + "libs/kaptan-board", + "libs/kaptan-data-channel", "libs/kaptan-dma", "libs/kaptan-drivers", "libs/kaptan-i2c", "libs/kaptan-mavlink", - "libs/kaptan-runtime", "libs/kaptan-sensor", "libs/kaptan-spi", - "libs/kaptan-stm32f7", "libs/kaptan-time", "libs/kaptan-tx", + "platforms/stm32f7", ] resolver = "2" diff --git a/libs/kaptan-runtime/Cargo.toml b/libs/kaptan-runtime/Cargo.toml deleted file mode 100644 index 072715b..0000000 --- a/libs/kaptan-runtime/Cargo.toml +++ /dev/null @@ -1,17 +0,0 @@ -[package] -name = "kaptan-runtime" -edition.workspace = true -license.workspace = true -repository.workspace = true -rust-version.workspace = true -version.workspace = true - -[lints] -workspace = true - -[dependencies] -kaptan-drivers = { path = "../kaptan-drivers" } -kaptan-i2c = { path = "../kaptan-i2c" } -kaptan-sensor = { path = "../kaptan-sensor" } -kaptan-spi = { path = "../kaptan-spi" } -kaptan-time = { path = "../kaptan-time" } diff --git a/libs/kaptan-runtime/src/imu.rs b/libs/kaptan-runtime/src/imu.rs deleted file mode 100644 index 307ee28..0000000 --- a/libs/kaptan-runtime/src/imu.rs +++ /dev/null @@ -1,148 +0,0 @@ -//! Single-owner MPU acquisition service. - -use kaptan_drivers::mpu6000::{Error as Mpu6000Error, Mpu6000, Mpu6000Event}; -use kaptan_sensor::imu::ImuSnapshot; -use kaptan_spi::{SpiCompletion, SpiDevice}; -use kaptan_time::MonotonicClock; - -/// One observable result from the IMU worker. -#[derive(Debug)] -pub enum ImuEvent { - /// No accepted sample or terminal fault was produced. - Idle, - /// MPU configuration completed and the DRDY route may be enabled. - Initialized, - /// A coherent IMU observation is ready for telemetry or control. - Sample(ImuSnapshot), - /// The selected sensor path entered a terminal fault state. - Fault(Mpu6000Error), -} - -/// Owns MPU state, its clock, and sample acceptance. -/// -/// Exactly one worker invokes this service. Hardware ISRs only report deferred -/// DRDY, timer, and SPI-DMA terminal events to that worker. -pub struct ImuService { - clock: Clock, - imu: Mpu6000, -} - -impl ImuService { - /// Creates an inactive acquisition service. - pub const fn new(clock: Clock, imu: Mpu6000) -> Self { - Self { clock, imu } - } -} - -impl ImuService -where - Clock: MonotonicClock, - Bus: SpiDevice, -{ - /// Submits the asynchronous sensor reset transaction. - pub fn begin(&mut self) -> Result<(), Mpu6000Error> { - self.imu.begin_initialization() - } - - /// Advances the reset-settling state on a board periodic event. - pub fn on_periodic_tick(&mut self) -> ImuEvent { - if self.imu.is_ready() { - return match self.imu.request_sample(self.clock.now()) { - Ok(()) | Err(Mpu6000Error::NotReady) => ImuEvent::Idle, - Err(error) => ImuEvent::Fault(error), - }; - } - - let _ = self.clock.now(); - match self.imu.advance_initialization() { - Ok(()) => ImuEvent::Idle, - Err(error) => ImuEvent::Fault(error), - } - } - - /// Handles one coalesced data-ready event. - pub fn on_data_ready(&mut self) -> ImuEvent { - if !self.imu.is_ready() { - return ImuEvent::Idle; - } - - match self.imu.request_sample(self.clock.now()) { - Ok(()) | Err(Mpu6000Error::NotReady) => ImuEvent::Idle, - Err(error) => ImuEvent::Fault(error), - } - } - - /// Advances the MPU transaction state from a DMA terminal result. - pub fn on_transfer_complete(&mut self, completion: SpiCompletion) -> ImuEvent { - match self.imu.on_transfer_complete(completion) { - Mpu6000Event::NotPending | Mpu6000Event::Initializing => ImuEvent::Idle, - Mpu6000Event::Initialized => ImuEvent::Initialized, - Mpu6000Event::Sample(sample) => ImuEvent::Sample(sample), - Mpu6000Event::Fault(error) => ImuEvent::Fault(error), - } - } -} - -#[cfg(test)] -mod tests { - use super::{ImuEvent, ImuService}; - use kaptan_drivers::mpu6000::{Mpu6000, Mpu6000Config}; - use kaptan_spi::{SpiCompletion, SpiDevice}; - use kaptan_time::{MonotonicClock, Timestamp}; - - struct Clock; - - impl MonotonicClock for Clock { - fn now(&mut self) -> Timestamp { - Timestamp::from_microseconds(42) - } - } - - struct Bus { - response: [u8; 15], - } - - impl SpiDevice for Bus { - type StartError = (); - - fn start_transfer(&mut self, _: &[u8], receive: &mut [u8]) -> Result<(), Self::StartError> { - receive.copy_from_slice(&self.response[..receive.len()]); - Ok(()) - } - - fn complete_transfer(&mut self, _: SpiCompletion) {} - } - - #[test] - fn accepts_a_sample_only_after_initialization() { - let mut response = [0; 15]; - response[1] = 0x68; - let mut service = ImuService::new( - Clock, - Mpu6000::new(Bus { response }, Mpu6000Config::default()), - ); - - service.begin().unwrap(); - assert!(matches!(service.on_data_ready(), ImuEvent::Idle)); - assert!(matches!( - service.on_transfer_complete(SpiCompletion::Complete), - ImuEvent::Idle - )); - assert!(matches!(service.on_periodic_tick(), ImuEvent::Idle)); - for _ in 0..9 { - assert!(matches!( - service.on_transfer_complete(SpiCompletion::Complete), - ImuEvent::Idle - )); - } - assert!(matches!( - service.on_transfer_complete(SpiCompletion::Complete), - ImuEvent::Initialized - )); - assert!(matches!(service.on_periodic_tick(), ImuEvent::Idle)); - assert!(matches!( - service.on_transfer_complete(SpiCompletion::Complete), - ImuEvent::Sample(_) - )); - } -} diff --git a/libs/kaptan-runtime/src/lib.rs b/libs/kaptan-runtime/src/lib.rs deleted file mode 100644 index 6c70b17..0000000 --- a/libs/kaptan-runtime/src/lib.rs +++ /dev/null @@ -1,20 +0,0 @@ -//! RTIC- and PAC-independent flight services. -//! -//! Acquisition services and board-owned output workers have separate owners. -//! Boards connect hardware interrupts to their dedicated workers through -//! bounded deferred events; they must not place unrelated work behind one -//! priority-ceiling resource. - -#![no_std] - -/// Single-owner IMU acquisition service. -pub mod imu; -/// Lock-free worker handoff primitives. -pub mod mailbox; -/// Single-owner LIDAR-Lite range acquisition service. -pub mod range; - -pub use imu::{ImuEvent, ImuService}; -pub use mailbox::LatestImuMailbox; -pub use mailbox::LatestRangeMailbox; -pub use range::{RangeEvent, RangeService}; diff --git a/libs/kaptan-runtime/src/mailbox.rs b/libs/kaptan-runtime/src/mailbox.rs deleted file mode 100644 index 749593d..0000000 --- a/libs/kaptan-runtime/src/mailbox.rs +++ /dev/null @@ -1,189 +0,0 @@ -//! Lock-free single-producer, single-consumer latest-value mailboxes. - -use core::{ - marker::PhantomData, - sync::atomic::{AtomicU32, Ordering}, -}; - -use kaptan_sensor::{imu::ImuSnapshot, range::RangeSnapshot}; -use kaptan_time::Timestamp; - -/// Fixed-width snapshot representation for [`LatestMailbox`]. -/// -/// The representation consists only of atomic words. Implementations must -/// encode and decode every field without loss so an unread older observation -/// can be safely replaced by a newer one. -pub trait MailboxValue: Copy { - /// Converts a snapshot into its atomic-word representation. - fn into_words(self) -> [u32; WORDS]; - - /// Reconstructs a snapshot from its atomic-word representation. - fn from_words(words: [u32; WORDS]) -> Self; -} - -/// Lock-free latest-value SPSC mailbox. -/// -/// Publishing a newer value intentionally replaces an unread older value: the -/// output path is best-effort and must never delay acquisition. The sequence -/// counter is a seqlock; odd values indicate a writer and even values identify -/// a stable snapshot. Exactly one producer and one consumer may use an -/// instance concurrently. Sequence operations are sequentially consistent so -/// a reader cannot validate an old even sequence around concurrently written -/// atomic payload words; the payload words themselves need only be relaxed. -pub struct LatestMailbox { - sequence: AtomicU32, - words: [AtomicU32; WORDS], - value: PhantomData, -} - -impl, const WORDS: usize> LatestMailbox { - /// Creates an empty mailbox. - pub const fn new() -> Self { - Self { - sequence: AtomicU32::new(0), - words: [const { AtomicU32::new(0) }; WORDS], - value: PhantomData, - } - } - - /// Publishes a snapshot, replacing any unread older snapshot. - pub fn publish(&self, value: Value) { - let sequence = self.sequence.fetch_add(1, Ordering::SeqCst); - debug_assert_eq!(sequence & 1, 0, "SPSC producer must not reenter"); - - for (destination, word) in self.words.iter().zip(value.into_words()) { - destination.store(word, Ordering::Relaxed); - } - - self.sequence - .store(sequence.wrapping_add(2), Ordering::SeqCst); - } - - /// Returns the newest stable snapshot different from `observed_sequence`. - pub fn take_newer(&self, observed_sequence: &mut u32) -> Option { - let before = self.sequence.load(Ordering::SeqCst); - if before == *observed_sequence || before & 1 != 0 { - return None; - } - - let mut words = [0; WORDS]; - for (destination, source) in words.iter_mut().zip(&self.words) { - *destination = source.load(Ordering::Relaxed); - } - - if self.sequence.load(Ordering::SeqCst) != before { - return None; - } - - *observed_sequence = before; - Some(Value::from_words(words)) - } -} - -impl, const WORDS: usize> Default for LatestMailbox { - fn default() -> Self { - Self::new() - } -} - -impl MailboxValue<9> for ImuSnapshot { - fn into_words(self) -> [u32; 9] { - let timestamp = self.timestamp.as_microseconds(); - [ - timestamp as u32, - (timestamp >> 32) as u32, - self.acceleration_milligravity[0] as u16 as u32, - self.acceleration_milligravity[1] as u16 as u32, - self.acceleration_milligravity[2] as u16 as u32, - self.angular_rate_milliradians_per_second[0] as u16 as u32, - self.angular_rate_milliradians_per_second[1] as u16 as u32, - self.angular_rate_milliradians_per_second[2] as u16 as u32, - self.temperature_centidegrees_celsius as u16 as u32, - ] - } - - fn from_words(words: [u32; 9]) -> Self { - ImuSnapshot::new( - timestamp_from_words(words[0], words[1]), - [ - words[2] as u16 as i16, - words[3] as u16 as i16, - words[4] as u16 as i16, - ], - [ - words[5] as u16 as i16, - words[6] as u16 as i16, - words[7] as u16 as i16, - ], - words[8] as u16 as i16, - ) - } -} - -impl MailboxValue<3> for RangeSnapshot { - fn into_words(self) -> [u32; 3] { - let timestamp = self.timestamp.as_microseconds(); - [ - timestamp as u32, - (timestamp >> 32) as u32, - self.distance_millimetres, - ] - } - - fn from_words(words: [u32; 3]) -> Self { - RangeSnapshot::new(timestamp_from_words(words[0], words[1]), words[2]) - } -} - -/// Latest-value mailbox for IMU telemetry snapshots. -pub type LatestImuMailbox = LatestMailbox; -/// Latest-value mailbox for range telemetry snapshots. -pub type LatestRangeMailbox = LatestMailbox; - -const fn timestamp_from_words(low: u32, high: u32) -> Timestamp { - Timestamp::from_microseconds((low as u64) | ((high as u64) << 32)) -} - -#[cfg(test)] -mod tests { - use super::{LatestImuMailbox, LatestRangeMailbox}; - use kaptan_sensor::{imu::ImuSnapshot, range::RangeSnapshot}; - use kaptan_time::Timestamp; - - #[test] - fn publishes_only_the_newest_unread_imu_sample() { - let mailbox = LatestImuMailbox::new(); - let mut observed = 0; - mailbox.publish(ImuSnapshot::new( - Timestamp::from_microseconds(1), - [1; 3], - [2; 3], - 3, - )); - mailbox.publish(ImuSnapshot::new( - Timestamp::from_microseconds(2), - [4; 3], - [5; 3], - 6, - )); - - let sample = mailbox.take_newer(&mut observed).unwrap(); - assert_eq!(sample.timestamp, Timestamp::from_microseconds(2)); - assert_eq!(sample.acceleration_milligravity, [4; 3]); - assert!(mailbox.take_newer(&mut observed).is_none()); - } - - #[test] - fn publishes_only_the_newest_unread_range_sample() { - let mailbox = LatestRangeMailbox::new(); - let mut observed = 0; - mailbox.publish(RangeSnapshot::new(Timestamp::from_microseconds(1), 100)); - mailbox.publish(RangeSnapshot::new(Timestamp::from_microseconds(2), 200)); - - assert_eq!( - mailbox.take_newer(&mut observed), - Some(RangeSnapshot::new(Timestamp::from_microseconds(2), 200)) - ); - assert!(mailbox.take_newer(&mut observed).is_none()); - } -} diff --git a/libs/kaptan-runtime/src/range.rs b/libs/kaptan-runtime/src/range.rs deleted file mode 100644 index 137f1dd..0000000 --- a/libs/kaptan-runtime/src/range.rs +++ /dev/null @@ -1,165 +0,0 @@ -//! Single-owner LIDAR-Lite range acquisition service. - -use kaptan_drivers::lidar_lite_v3::{ - AcquisitionMode, Error as LidarError, LidarLiteV3, LidarLiteV3Event, StartupError, -}; -use kaptan_i2c::{I2cBus, I2cCompletion}; -use kaptan_sensor::range::RangeSnapshot; -use kaptan_time::MonotonicClock; - -/// One observable result from the range worker. -#[derive(Debug)] -pub enum RangeEvent { - /// No observation or terminal fault was produced. - Idle, - /// One coherent range observation is ready for publication. - Measurement(RangeSnapshot), - /// The selected sensor path entered a terminal fault state. - Fault(LidarError), -} - -/// Owns LIDAR-Lite state, its clock, and measurement acceptance. -pub struct RangeService { - clock: Clock, - lidar: LidarLiteV3, - mode: AcquisitionMode, -} - -impl RangeService { - /// Creates an inactive range acquisition service. - pub const fn new(clock: Clock, lidar: LidarLiteV3, mode: AcquisitionMode) -> Self { - Self { clock, lidar, mode } - } - - /// Completes the LIDAR-Lite startup interval selected by the board. - pub fn finish_startup(&mut self) -> Result<(), StartupError> { - self.lidar.finish_startup() - } -} - -impl RangeService { - /// Starts a new measurement, or polls one already in progress. - pub fn on_periodic_tick(&mut self) -> RangeEvent { - let now = self.clock.now(); - let result = if self.lidar.is_ready() { - self.lidar.request_measurement(now, self.mode) - } else { - self.lidar.poll_measurement(now) - }; - match result { - Ok(()) | Err(LidarError::NotReady) => RangeEvent::Idle, - Err(error) => RangeEvent::Fault(error), - } - } - - /// Advances the LIDAR-Lite transaction state from an I²C terminal event. - pub fn on_transaction_complete( - &mut self, - completion: I2cCompletion, - ) -> RangeEvent { - match self.lidar.on_transaction_complete(completion) { - LidarLiteV3Event::Measurement(sample) => RangeEvent::Measurement(sample), - LidarLiteV3Event::Fault(error) => RangeEvent::Fault(error), - _ => RangeEvent::Idle, - } - } - - /// Acknowledges a board-controller interrupt and advances the acquisition. - /// - /// The closure keeps the controller-specific interrupt handling at the - /// board boundary while preserving this service as the sole owner of the - /// LIDAR state machine. - pub fn on_controller_interrupt(&mut self, acknowledge: F) -> RangeEvent - where - F: FnOnce(&mut Bus) -> I2cCompletion, - { - let completion = self.lidar.acknowledge_with(acknowledge); - self.on_transaction_complete(completion) - } -} - -#[cfg(test)] -mod tests { - use super::{RangeEvent, RangeService}; - use kaptan_drivers::lidar_lite_v3::LidarLiteV3; - use kaptan_i2c::{I2cBus, I2cCompletion}; - use kaptan_time::{MonotonicClock, Timestamp}; - - struct Clock; - - impl MonotonicClock for Clock { - fn now(&mut self) -> Timestamp { - Timestamp::from_microseconds(123) - } - } - - struct Bus { - response: [u8; 2], - } - - impl I2cBus for Bus { - type StartError = (); - - fn start_write(&mut self, _: u8, _: &[u8]) -> Result<(), Self::StartError> { - Ok(()) - } - - fn start_read(&mut self, _: u8, _: usize) -> Result<(), Self::StartError> { - Ok(()) - } - - fn complete_transaction(&mut self, completion: I2cCompletion, read: Option<&mut [u8]>) { - if completion == I2cCompletion::Complete - && let Some(read) = read - { - read.copy_from_slice(&self.response[..read.len()]); - } - } - - fn cancel_transaction(&mut self) {} - } - - #[test] - fn produces_a_measurement_after_stop_separated_i2c_transactions() { - let mut service = RangeService::new( - Clock, - LidarLiteV3::new(Bus { response: [0; 2] }), - kaptan_drivers::lidar_lite_v3::AcquisitionMode::WithReceiverBiasCorrection, - ); - service.finish_startup().unwrap(); - - assert!(matches!(service.on_periodic_tick(), RangeEvent::Idle)); - assert!(matches!( - service.on_transaction_complete(I2cCompletion::Complete), - RangeEvent::Idle - )); - assert!(matches!(service.on_periodic_tick(), RangeEvent::Idle)); - assert!(matches!( - service.on_controller_interrupt(|bus| { - bus.response = [1 << 5, 0]; - I2cCompletion::Complete - }), - RangeEvent::Idle - )); - assert!(matches!(service.on_periodic_tick(), RangeEvent::Idle)); - assert!(matches!( - service.on_controller_interrupt(|_| I2cCompletion::Complete), - RangeEvent::Idle - )); - assert!(matches!(service.on_periodic_tick(), RangeEvent::Idle)); - assert!(matches!( - service.on_controller_interrupt(|bus| { - bus.response = [0x01, 0x23]; - I2cCompletion::Complete - }), - RangeEvent::Idle - )); - assert!(matches!(service.on_periodic_tick(), RangeEvent::Idle)); - assert!(matches!( - service.on_controller_interrupt(|_| I2cCompletion::Complete), - RangeEvent::Measurement(sample) - if sample.timestamp == Timestamp::from_microseconds(123) - && sample.distance_millimetres == 2_910 - )); - } -}