const CORDIC_ATAN: [i16; 15] = [
8192, 4836, 2555, 1297, 651, 326, 163, 81, 41, 20, 10, 5, 3, 1, 1,
];
const X_INIT: i32 = 5_094_520;
#[inline]
fn cordic_sin(phase: u16) -> u8 {
let mut negate = false;
let mut angle: i32 = phase as i32;
if angle >= 32768 {
negate = true;
angle -= 32768;
}
if angle > 16384 {
angle = 32768 - angle;
}
let mut x: i32 = X_INIT;
let mut y: i32 = 0;
for i in 0..15 {
let d: i32 = if angle >= 0 { 1 } else { -1 };
let x_next = x - d * (y >> i);
let y_next = y + d * (x >> i);
angle -= d * CORDIC_ATAN[i] as i32;
x = x_next;
y = y_next;
}
if negate {
y = -y;
}
((y >> 16) + 128).clamp(0, 255) as u8
}
pub struct Breath {
phase: u16,
phase_step: u16,
interval_ms: u32,
}
impl Breath {
#[track_caller]
pub fn new(cycle_ms: u32, interval_ms: u32) -> Self {
assert!(cycle_ms > 0, "cycle_ms must be > 0");
assert!(interval_ms > 0, "interval_ms must be > 0");
let step = ((65536u64 * interval_ms as u64) / cycle_ms as u64) as u16;
Self {
phase: 0,
phase_step: step.max(1),
interval_ms,
}
}
#[inline]
pub fn next(&mut self) -> u8 {
let brightness = cordic_sin(self.phase);
self.phase = self.phase.wrapping_add(self.phase_step);
brightness
}
#[inline]
pub fn reset(&mut self) {
self.phase = 0;
}
}
impl core::fmt::Debug for Breath {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Breath")
.field("phase", &self.phase)
.field("phase_step", &self.phase_step)
.field("interval_ms", &self.interval_ms)
.finish()
}
}
#[cfg(feature = "defmt")]
impl defmt::Format for Breath {
fn format(&self, fmt: defmt::Formatter) {
defmt::write!(
fmt,
"Breath {{ phase: {}, step: {}, interval_ms: {} }}",
self.phase,
self.phase_step,
self.interval_ms
)
}
}
use crate::PolarityMode;
use crate::pwm::{GammaCorrection, GammaMap, PwmLed};
use embedded_hal::pwm::SetDutyCycle;
pub struct BreathLed<P: SetDutyCycle, G: GammaMap = GammaCorrection> {
led: PwmLed<P, G>,
breath: Breath,
}
impl<P: SetDutyCycle, G: GammaMap> BreathLed<P, G> {
pub fn new(
pin: P,
gamma: G,
polarity: PolarityMode,
cycle_ms: u32,
interval_ms: u32,
) -> Result<Self, P::Error> {
let led = PwmLed::new(pin, gamma, polarity)?;
let breath = Breath::new(cycle_ms, interval_ms);
Ok(Self { led, breath })
}
#[inline]
pub fn reset_breath(&mut self) {
self.breath.reset();
}
#[inline]
pub async fn breathe(&mut self) -> Result<(), P::Error> {
let brightness = self.breath.next();
self.led.set_brightness(brightness)?;
embassy_time::Timer::after_millis(self.breath.interval_ms as u64).await;
Ok(())
}
#[inline]
pub fn led(&self) -> &PwmLed<P, G> {
&self.led
}
#[inline]
pub fn release(self) -> PwmLed<P, G> {
self.led
}
}
impl<P: SetDutyCycle, G: GammaMap> core::fmt::Debug for BreathLed<P, G> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("BreathLed")
.field("led", &self.led)
.field("breath", &self.breath)
.finish()
}
}
#[cfg(feature = "defmt")]
impl<P: SetDutyCycle, G: GammaMap> defmt::Format for BreathLed<P, G> {
fn format(&self, fmt: defmt::Formatter) {
defmt::write!(
fmt,
"BreathLed {{ led: {}, breath: {} }}",
self.led,
self.breath
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cordic_sin_range() {
for phase in (0..=65535u16).step_by(137) {
let _v = cordic_sin(phase);
}
}
#[test]
fn cordic_sin_endpoints() {
let v0 = cordic_sin(0);
assert!(v0 >= 126 && v0 <= 130, "sin(0) = {}", v0);
let v90 = cordic_sin(16384);
assert!(v90 >= 253, "sin(π/2) = {}", v90);
let v180 = cordic_sin(32768);
assert!(v180 >= 126 && v180 <= 130, "sin(π) = {}", v180);
let v270 = cordic_sin(49152);
assert!(v270 <= 2, "sin(3π/2) = {}", v270);
}
#[test]
fn cordic_sin_symmetry() {
for phase in (0..=16384u16).step_by(257) {
let a = cordic_sin(phase);
let b = cordic_sin(32768 - phase);
let sym_a = (a as i16 - 128).unsigned_abs();
let sym_b = (b as i16 - 128).unsigned_abs();
let diff = (sym_a as i16 - sym_b as i16).unsigned_abs();
assert!(
diff <= 2,
"symmetry broken at phase={}: a={}, b={}",
phase,
a,
b
);
}
}
#[test]
fn cordic_sin_monotonic_rising() {
let mut prev = cordic_sin(0);
for phase in 1..=16384u16 {
let val = cordic_sin(phase);
assert!(
val >= prev,
"non-monotonic at phase={}: {} -> {}",
phase,
prev,
val
);
prev = val;
}
}
#[test]
fn breath_new_does_not_panic() {
let _b = Breath::new(1000, 50);
}
#[test]
fn breath_next_returns_valid_range() {
let mut b = Breath::new(1000, 50);
for _ in 0..1000 {
let _v = b.next();
}
}
#[test]
fn breath_next_advances() {
let mut b = Breath::new(65536, 16384);
let v1 = b.next();
let v2 = b.next();
let diff = (v1 as i16 - v2 as i16).unsigned_abs();
assert!(diff > 0, "phase didn't advance: {}→{}", v1, v2);
}
#[test]
fn breath_reset() {
let mut b = Breath::new(65536, 16384); for _ in 0..10 {
b.next();
}
b.reset();
let v = b.next();
assert!(
(126..=130).contains(&v),
"after reset got {v}, expected ~128"
);
}
#[test]
fn breath_full_cycle_reaches_min_and_max() {
let mut b = Breath::new(65536, 256);
let mut min = 255u8;
let mut max = 0u8;
for _ in 0..300 {
let v = b.next();
min = min.min(v);
max = max.max(v);
}
assert!(min <= 2, "min should be near 0, got {}", min);
assert!(max >= 253, "max should be near 255, got {}", max);
}
#[test]
#[should_panic(expected = "cycle_ms must be > 0")]
fn breath_new_cycle_zero_panics() {
Breath::new(0, 50);
}
#[test]
#[should_panic(expected = "interval_ms must be > 0")]
fn breath_new_interval_zero_panics() {
Breath::new(1000, 0);
}
mod breath_led_tests {
use super::*;
use crate::PolarityMode;
use crate::pwm::GammaCorrection;
use embedded_hal_mock::eh1::pwm::Mock as PwmMock;
use embedded_hal_mock::eh1::pwm::Transaction as PwmTrans;
const MAX_DUTY: u16 = 1000;
#[test]
fn breath_led_new_starts_off() {
let e = [
PwmTrans::max_duty_cycle(MAX_DUTY),
PwmTrans::set_duty_cycle(0),
];
let led = BreathLed::new(
PwmMock::new(&e),
GammaCorrection::Linear,
PolarityMode::ActiveHigh,
10_000,
50,
)
.unwrap();
assert!(led.led().is_off());
led.release().release().done();
}
#[test]
fn breath_led_reset_breath() {
let e = [
PwmTrans::max_duty_cycle(MAX_DUTY),
PwmTrans::set_duty_cycle(0), ];
let mut led = BreathLed::new(
PwmMock::new(&e),
GammaCorrection::Linear,
PolarityMode::ActiveHigh,
10_000,
50,
)
.unwrap();
led.reset_breath();
led.release().release().done();
}
}
}