use crate::brightness::{self, Brightness};
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_raw(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, &atan) in CORDIC_ATAN.iter().enumerate() {
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 * atan as i32;
x = x_next;
y = y_next;
}
if negate {
y = -y;
}
((y >> 16) + 128).clamp(0, 255) as u8
}
#[inline]
fn cordic_sin(phase: u16) -> Brightness {
let raw8 = cordic_sin_raw(phase);
brightness::from_u32_clamped(raw8 as u32 * brightness::MAX_BRIGHTNESS / 255)
}
pub struct Breath {
phase: u16,
phase_step: u16,
interval: embassy_time::Duration,
}
impl Breath {
#[track_caller]
pub fn new(cycle: embassy_time::Duration, interval: embassy_time::Duration) -> Self {
let cycle_ms = cycle.as_millis();
let interval_ms = interval.as_millis();
assert!(cycle_ms > 0, "cycle duration must be > 0 ms");
assert!(interval_ms > 0, "interval duration must be > 0 ms");
let step = ((65536u64 * interval_ms) / cycle_ms) as u16;
Self {
phase: 0,
phase_step: step.max(1),
interval,
}
}
#[inline]
#[allow(clippy::should_implement_trait)]
pub fn next(&mut self) -> Brightness {
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.as_millis())
.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.as_millis()
)
}
}
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: embassy_time::Duration,
interval: embassy_time::Duration,
) -> Result<Self, P::Error> {
let led = PwmLed::new(pin, gamma, polarity)?;
let breath = Breath::new(cycle, interval);
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(self.breath.interval).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>
where
G: core::fmt::Debug,
{
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>
where
G: defmt::Format,
{
fn format(&self, fmt: defmt::Formatter) {
defmt::write!(
fmt,
"BreathLed {{ led: {}, breath: {} }}",
self.led,
self.breath
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use embassy_time::Duration;
#[test]
fn cordic_sin_range() {
for phase in (0..=65535u16).step_by(137) {
let v = cordic_sin(phase);
let vu = brightness::to_u32(v);
assert!(
vu <= brightness::MAX_BRIGHTNESS,
"cordic_sin({phase}) = {vu}, exceeds max {}",
brightness::MAX_BRIGHTNESS
);
}
}
#[test]
fn cordic_sin_endpoints() {
let max = brightness::MAX_BRIGHTNESS;
let mid = max / 2;
let v0 = brightness::to_u32(cordic_sin(0));
let half_range = if max == 255 { 2 } else { 32 };
assert!(
v0.abs_diff(mid) <= half_range,
"sin(0) = {v0}, expected ~{mid}"
);
let v90 = brightness::to_u32(cordic_sin(16384));
assert!(
v90 >= max.saturating_sub(2),
"sin(π/2) = {v90}, expected ~{max}"
);
let v180 = brightness::to_u32(cordic_sin(32768));
assert!(
v180.abs_diff(mid) <= half_range,
"sin(π) = {v180}, expected ~{mid}"
);
let v270 = brightness::to_u32(cordic_sin(49152));
let near_min = if max == 255 { 2 } else { 33 };
assert!(v270 <= near_min, "sin(3π/2) = {v270}, expected ≤{near_min}");
}
#[test]
fn cordic_sin_symmetry() {
let mid = brightness::MAX_BRIGHTNESS / 2;
for phase in (0..=16384u16).step_by(257) {
let a = brightness::to_u32(cordic_sin(phase));
let b = brightness::to_u32(cordic_sin(32768 - phase));
let sym_a = a.abs_diff(mid);
let sym_b = b.abs_diff(mid);
let diff = sym_a.abs_diff(sym_b);
let tolerance = if brightness::MAX_BRIGHTNESS == 255 {
2
} else {
33
};
assert!(
diff <= tolerance,
"symmetry broken at phase={phase}: a={a}, b={b}"
);
}
}
#[test]
fn cordic_sin_monotonic_rising() {
let mut prev = brightness::to_u32(cordic_sin(0));
for phase in 1..=16384u16 {
let val = brightness::to_u32(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(Duration::from_millis(1000), Duration::from_millis(50));
}
#[test]
fn breath_next_returns_valid_range() {
let mut b = Breath::new(Duration::from_millis(1000), Duration::from_millis(50));
for _ in 0..1000 {
let v = brightness::to_u32(b.next());
assert!(
v <= brightness::MAX_BRIGHTNESS,
"breath.next() = {v}, exceeds max"
);
}
}
#[test]
fn breath_next_advances() {
let mut b = Breath::new(Duration::from_millis(65536), Duration::from_millis(16384));
let v1 = brightness::to_u32(b.next());
let v2 = brightness::to_u32(b.next());
let diff = v1.abs_diff(v2);
assert!(diff > 0, "phase didn't advance: {v1}→{v2}");
}
#[test]
fn breath_reset() {
let mut b = Breath::new(Duration::from_millis(65536), Duration::from_millis(16384)); for _ in 0..10 {
b.next();
}
b.reset();
let v = brightness::to_u32(b.next());
let mid = brightness::MAX_BRIGHTNESS / 2;
let tolerance = if brightness::MAX_BRIGHTNESS == 255 {
2
} else {
32
};
assert!(
v.abs_diff(mid) <= tolerance * 2,
"after reset got {v}, expected ~{mid}"
);
}
#[test]
fn breath_full_cycle_reaches_min_and_max() {
let mut b = Breath::new(Duration::from_millis(65536), Duration::from_millis(256));
let mut min = brightness::MAX_BRIGHTNESS;
let mut max = 0u32;
for _ in 0..300 {
let v = brightness::to_u32(b.next());
min = min.min(v);
max = max.max(v);
}
let near_min = if brightness::MAX_BRIGHTNESS == 255 {
2
} else {
33
};
assert!(min <= near_min, "min should be near 0, got {min}");
assert!(
max >= brightness::MAX_BRIGHTNESS.saturating_sub(2),
"max should be near {}, got {max}",
brightness::MAX_BRIGHTNESS
);
}
#[test]
#[should_panic(expected = "cycle duration must be > 0 ms")]
fn breath_new_cycle_zero_panics() {
Breath::new(Duration::from_millis(0), Duration::from_millis(50));
}
#[test]
#[should_panic(expected = "interval duration must be > 0 ms")]
fn breath_new_interval_zero_panics() {
Breath::new(Duration::from_millis(1000), Duration::from_millis(0));
}
mod breath_led_tests {
use super::*;
use crate::PolarityMode;
use crate::pwm::GammaCorrection;
use embassy_time::Duration;
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,
Duration::from_millis(10_000),
Duration::from_millis(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,
Duration::from_millis(10_000),
Duration::from_millis(50),
)
.unwrap();
led.reset_breath();
led.release().release().done();
}
}
}