From 07a80388578630a81d887e74c7cee89f3fb8f358 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20=C3=96zkan?= Date: Sun, 26 Jul 2026 00:36:45 +0300 Subject: [PATCH] kaptan-actuator: improve config abstractions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Onur Özkan --- .../src/actuator/tim3_dshot.rs | 7 +- .../src/actuator/tim3_pwm.rs | 7 +- boards/kakute-f7-mini-v3/src/board.rs | 2 +- libs/kaptan-actuator/src/dshot.rs | 47 ++++++- libs/kaptan-actuator/src/lib.rs | 24 +++- libs/kaptan-actuator/src/protocol.rs | 132 ------------------ libs/kaptan-actuator/src/pwm.rs | 63 ++++++++- 7 files changed, 136 insertions(+), 146 deletions(-) delete mode 100644 libs/kaptan-actuator/src/protocol.rs diff --git a/boards/kakute-f7-mini-v3/src/actuator/tim3_dshot.rs b/boards/kakute-f7-mini-v3/src/actuator/tim3_dshot.rs index 7bede81..e0cd7b6 100644 --- a/boards/kakute-f7-mini-v3/src/actuator/tim3_dshot.rs +++ b/boards/kakute-f7-mini-v3/src/actuator/tim3_dshot.rs @@ -1,8 +1,7 @@ use core::sync::atomic::{AtomicBool, Ordering}; -use kaptan_actuator::{ - DshotConfig, - dshot::{ArmSequence, FRAME_SYMBOLS, FrameValue, Throttle, Timing, symbols}, +use kaptan_actuator::dshot::{ + ArmSequence, Config, FRAME_SYMBOLS, FrameValue, Throttle, Timing, symbols, }; use stm32f7::stm32f722; @@ -34,7 +33,7 @@ impl Tim3Dshot { rcc: &stm32f722::RCC, timer: stm32f722::TIM3, scheduler: stm32f722::TIM5, - config: DshotConfig, + config: Config, ) -> Self { let rate = config.rate(); let dshot_timer_hz = rate.timer_base_hz(); diff --git a/boards/kakute-f7-mini-v3/src/actuator/tim3_pwm.rs b/boards/kakute-f7-mini-v3/src/actuator/tim3_pwm.rs index d32a1bf..3968eca 100644 --- a/boards/kakute-f7-mini-v3/src/actuator/tim3_pwm.rs +++ b/boards/kakute-f7-mini-v3/src/actuator/tim3_pwm.rs @@ -1,9 +1,6 @@ use core::sync::atomic::{AtomicU16, Ordering}; -use kaptan_actuator::{ - PwmConfig, - pwm::{ArmSequence, FrameValue, PulseWidth}, -}; +use kaptan_actuator::pwm::{ArmSequence, Config, FrameValue, PulseWidth}; use stm32f7::stm32f722; const APB1_TIMER_CLOCK_HZ: u32 = 108_000_000; @@ -28,7 +25,7 @@ pub(super) struct Tim3Pwm { } impl Tim3Pwm { - pub(super) fn new(rcc: &stm32f722::RCC, timer: stm32f722::TIM3, config: PwmConfig) -> Self { + pub(super) fn new(rcc: &stm32f722::RCC, timer: stm32f722::TIM3, config: Config) -> Self { let period_us = match config.frame_rate().period_microseconds() { Some(period) => period, None => unreachable!(), diff --git a/boards/kakute-f7-mini-v3/src/board.rs b/boards/kakute-f7-mini-v3/src/board.rs index 7f2dcc0..978897f 100644 --- a/boards/kakute-f7-mini-v3/src/board.rs +++ b/boards/kakute-f7-mini-v3/src/board.rs @@ -14,7 +14,7 @@ pub const USART1_CLOCK_HZ: u32 = 108_000_000; pub const USART1_BAUD: u32 = 115_200; /// ESC protocol selected for this board's M1–M4 output route. pub const MOTOR_PROTOCOL: MotorProtocol = MotorProtocol::Dshot( - match kaptan_actuator::DshotConfig::new( + match kaptan_actuator::dshot::Config::new( kaptan_actuator::dshot::Rate::Dshot600, match kaptan_actuator::dshot::Throttle::new(300) { Some(throttle) => throttle, diff --git a/libs/kaptan-actuator/src/dshot.rs b/libs/kaptan-actuator/src/dshot.rs index 87957fa..7f72121 100644 --- a/libs/kaptan-actuator/src/dshot.rs +++ b/libs/kaptan-actuator/src/dshot.rs @@ -1,11 +1,48 @@ //! DShot framing, timing, and arming policy. +use crate::ArmingDuration; use core::sync::atomic::{AtomicBool, AtomicU16, Ordering}; pub const MIN_THROTTLE: u16 = 48; pub const MOTOR_STOP: u16 = 0; pub const FRAME_SYMBOLS: usize = 18; +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct Config { + rate: Rate, + armed_idle_throttle: Throttle, + arming_duration: ArmingDuration, +} + +impl Config { + pub const fn new( + rate: Rate, + armed_idle_throttle: Throttle, + arming_duration: ArmingDuration, + ) -> Option { + if rate.frames_for_millis(arming_duration.milliseconds()) == 0 { + return None; + } + Some(Self { + rate, + armed_idle_throttle, + arming_duration, + }) + } + + pub const fn rate(self) -> Rate { + self.rate + } + + pub const fn armed_idle_throttle(self) -> Throttle { + self.armed_idle_throttle + } + + pub const fn arming_duration(self) -> ArmingDuration { + self.arming_duration + } +} + #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum Rate { Dshot150, @@ -186,7 +223,8 @@ pub const fn symbols(packet: u16, zero_high: u16, one_high: u16) -> [u16; FRAME_ #[cfg(test)] mod tests { - use super::{ArmSequence, FrameValue, Rate, Throttle, Timing, packet, symbols}; + use super::{ArmSequence, Config, FrameValue, Rate, Throttle, Timing, packet, symbols}; + use crate::ArmingDuration; #[test] fn packet_matches_standard_dshot_vectors() { @@ -219,4 +257,11 @@ mod tests { assert!(Timing::from_timer_clock(rate.timer_base_hz(), rate).is_some()); } } + + #[test] + fn requires_an_arming_interval_with_frames() { + let duration = ArmingDuration::from_millis(2_000).unwrap(); + let throttle = Throttle::new(300).unwrap(); + assert!(Config::new(Rate::Dshot600, throttle, duration).is_some()); + } } diff --git a/libs/kaptan-actuator/src/lib.rs b/libs/kaptan-actuator/src/lib.rs index 0e2b7c1..069c221 100644 --- a/libs/kaptan-actuator/src/lib.rs +++ b/libs/kaptan-actuator/src/lib.rs @@ -3,10 +3,30 @@ #![no_std] pub mod dshot; -mod protocol; pub mod pwm; -pub use protocol::{ArmingDuration, DshotConfig, MotorProtocol, PwmConfig}; +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct ArmingDuration(u16); + +impl ArmingDuration { + pub const fn from_millis(milliseconds: u16) -> Option { + if milliseconds != 0 && milliseconds <= 8_000 { + Some(Self(milliseconds)) + } else { + None + } + } + + pub const fn milliseconds(self) -> u16 { + self.0 + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum MotorProtocol { + Pwm(pwm::Config), + Dshot(dshot::Config), +} /// Flight-control and MAVLink boundary for motor outputs. pub trait MotorOutput { diff --git a/libs/kaptan-actuator/src/protocol.rs b/libs/kaptan-actuator/src/protocol.rs deleted file mode 100644 index 51a358e..0000000 --- a/libs/kaptan-actuator/src/protocol.rs +++ /dev/null @@ -1,132 +0,0 @@ -use crate::{dshot, pwm}; - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub struct ArmingDuration(u16); - -impl ArmingDuration { - pub const fn from_millis(milliseconds: u16) -> Option { - if milliseconds != 0 && milliseconds <= 8_000 { - Some(Self(milliseconds)) - } else { - None - } - } - - pub const fn milliseconds(self) -> u16 { - self.0 - } -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub struct PwmConfig { - frame_rate: pwm::Rate, - minimum_pulse: pwm::PulseWidth, - armed_idle_pulse: pwm::PulseWidth, - arming_duration: ArmingDuration, -} - -impl PwmConfig { - pub const fn new( - frame_rate: pwm::Rate, - minimum_pulse: pwm::PulseWidth, - armed_idle_pulse: pwm::PulseWidth, - arming_duration: ArmingDuration, - ) -> Option { - match frame_rate.period_microseconds() { - Some(period) - if minimum_pulse.microseconds() <= armed_idle_pulse.microseconds() - && armed_idle_pulse.microseconds() <= period => - { - Some(Self { - frame_rate, - minimum_pulse, - armed_idle_pulse, - arming_duration, - }) - } - _ => None, - } - } - - pub const fn frame_rate(self) -> pwm::Rate { - self.frame_rate - } - pub const fn minimum_pulse(self) -> pwm::PulseWidth { - self.minimum_pulse - } - pub const fn armed_idle_pulse(self) -> pwm::PulseWidth { - self.armed_idle_pulse - } - pub const fn arming_duration(self) -> ArmingDuration { - self.arming_duration - } -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub struct DshotConfig { - rate: dshot::Rate, - armed_idle_throttle: dshot::Throttle, - arming_duration: ArmingDuration, -} - -impl DshotConfig { - pub const fn new( - rate: dshot::Rate, - armed_idle_throttle: dshot::Throttle, - arming_duration: ArmingDuration, - ) -> Option { - if rate.frames_for_millis(arming_duration.milliseconds()) == 0 { - return None; - } - Some(Self { - rate, - armed_idle_throttle, - arming_duration, - }) - } - - pub const fn rate(self) -> dshot::Rate { - self.rate - } - pub const fn armed_idle_throttle(self) -> dshot::Throttle { - self.armed_idle_throttle - } - pub const fn arming_duration(self) -> ArmingDuration { - self.arming_duration - } -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum MotorProtocol { - Pwm(PwmConfig), - Dshot(DshotConfig), -} - -#[cfg(test)] -mod tests { - use super::{ArmingDuration, DshotConfig, MotorProtocol, PwmConfig}; - use crate::{dshot, pwm}; - - const ARMING: ArmingDuration = match ArmingDuration::from_millis(2_000) { - Some(duration) => duration, - None => panic!("test duration must be valid"), - }; - - #[test] - fn pwm_configuration_requires_an_exact_frame_and_ordered_pulses() { - let rate = pwm::Rate::new(400).unwrap(); - let minimum = pwm::PulseWidth::new(1_000).unwrap(); - let idle = pwm::PulseWidth::new(1_100).unwrap(); - assert!(PwmConfig::new(rate, minimum, idle, ARMING).is_some()); - assert!(PwmConfig::new(pwm::Rate::new(333).unwrap(), minimum, idle, ARMING).is_none()); - assert!(PwmConfig::new(rate, idle, minimum, ARMING).is_none()); - } - - #[test] - fn motor_protocol_contains_only_validated_policies() { - let idle = dshot::Throttle::new(300).unwrap(); - let config = DshotConfig::new(dshot::Rate::Dshot600, idle, ARMING).unwrap(); - assert_eq!(MotorProtocol::Dshot(config), MotorProtocol::Dshot(config)); - assert!(ArmingDuration::from_millis(0).is_none()); - } -} diff --git a/libs/kaptan-actuator/src/pwm.rs b/libs/kaptan-actuator/src/pwm.rs index d3d2b52..c27f5c7 100644 --- a/libs/kaptan-actuator/src/pwm.rs +++ b/libs/kaptan-actuator/src/pwm.rs @@ -1,7 +1,56 @@ //! Conventional PWM framing and arming policy. +use crate::ArmingDuration; use core::sync::atomic::{AtomicBool, AtomicU16, Ordering}; +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct Config { + frame_rate: Rate, + minimum_pulse: PulseWidth, + armed_idle_pulse: PulseWidth, + arming_duration: ArmingDuration, +} + +impl Config { + pub const fn new( + frame_rate: Rate, + minimum_pulse: PulseWidth, + armed_idle_pulse: PulseWidth, + arming_duration: ArmingDuration, + ) -> Option { + match frame_rate.period_microseconds() { + Some(period) + if minimum_pulse.microseconds() <= armed_idle_pulse.microseconds() + && armed_idle_pulse.microseconds() <= period => + { + Some(Self { + frame_rate, + minimum_pulse, + armed_idle_pulse, + arming_duration, + }) + } + _ => None, + } + } + + pub const fn frame_rate(self) -> Rate { + self.frame_rate + } + + pub const fn minimum_pulse(self) -> PulseWidth { + self.minimum_pulse + } + + pub const fn armed_idle_pulse(self) -> PulseWidth { + self.armed_idle_pulse + } + + pub const fn arming_duration(self) -> ArmingDuration { + self.arming_duration + } +} + #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct Rate(u16); @@ -114,7 +163,8 @@ impl ArmSequence { #[cfg(test)] mod tests { - use super::{ArmSequence, FrameValue, PulseWidth, Rate}; + use super::{ArmSequence, Config, FrameValue, PulseWidth, Rate}; + use crate::ArmingDuration; #[test] fn rate_describes_an_exact_400_hertz_frame() { @@ -134,4 +184,15 @@ mod tests { sequence.disarm(); assert_eq!(sequence.next_frame(), None); } + + #[test] + fn requires_an_exact_frame_and_ordered_pulses() { + let duration = ArmingDuration::from_millis(2_000).unwrap(); + let rate = Rate::new(400).unwrap(); + let minimum = PulseWidth::new(1_000).unwrap(); + let idle = PulseWidth::new(1_100).unwrap(); + assert!(Config::new(rate, minimum, idle, duration).is_some()); + assert!(Config::new(Rate::new(333).unwrap(), minimum, idle, duration).is_none()); + assert!(Config::new(rate, idle, minimum, duration).is_none()); + } }