initial
This commit is contained in:
+241
@@ -0,0 +1,241 @@
|
||||
// This file was automatically generated.
|
||||
|
||||
#![allow(unused)]
|
||||
|
||||
macro_rules! assert {
|
||||
($($x:tt)*) => {
|
||||
{
|
||||
#[cfg(not(feature = "defmt"))]
|
||||
::core::assert!($($x)*);
|
||||
#[cfg(feature = "defmt")]
|
||||
::defmt::assert!($($x)*);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! assert_eq {
|
||||
($($x:tt)*) => {
|
||||
{
|
||||
#[cfg(not(feature = "defmt"))]
|
||||
::core::assert_eq!($($x)*);
|
||||
#[cfg(feature = "defmt")]
|
||||
::defmt::assert_eq!($($x)*);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! assert_ne {
|
||||
($($x:tt)*) => {
|
||||
{
|
||||
#[cfg(not(feature = "defmt"))]
|
||||
::core::assert_ne!($($x)*);
|
||||
#[cfg(feature = "defmt")]
|
||||
::defmt::assert_ne!($($x)*);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! debug_assert {
|
||||
($($x:tt)*) => {
|
||||
{
|
||||
#[cfg(not(feature = "defmt"))]
|
||||
::core::debug_assert!($($x)*);
|
||||
#[cfg(feature = "defmt")]
|
||||
::defmt::debug_assert!($($x)*);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! debug_assert_eq {
|
||||
($($x:tt)*) => {
|
||||
{
|
||||
#[cfg(not(feature = "defmt"))]
|
||||
::core::debug_assert_eq!($($x)*);
|
||||
#[cfg(feature = "defmt")]
|
||||
::defmt::debug_assert_eq!($($x)*);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! debug_assert_ne {
|
||||
($($x:tt)*) => {
|
||||
{
|
||||
#[cfg(not(feature = "defmt"))]
|
||||
::core::debug_assert_ne!($($x)*);
|
||||
#[cfg(feature = "defmt")]
|
||||
::defmt::debug_assert_ne!($($x)*);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! todo {
|
||||
($($x:tt)*) => {
|
||||
{
|
||||
#[cfg(not(feature = "defmt"))]
|
||||
::core::todo!($($x)*);
|
||||
#[cfg(feature = "defmt")]
|
||||
::defmt::todo!($($x)*);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "defmt"))]
|
||||
macro_rules! unreachable {
|
||||
($($x:tt)*) => {
|
||||
::core::unreachable!($($x)*)
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(feature = "defmt")]
|
||||
macro_rules! unreachable {
|
||||
($($x:tt)*) => {
|
||||
::defmt::unreachable!($($x)*)
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! panic {
|
||||
($($x:tt)*) => {
|
||||
{
|
||||
#[cfg(not(feature = "defmt"))]
|
||||
::core::panic!($($x)*);
|
||||
#[cfg(feature = "defmt")]
|
||||
::defmt::panic!($($x)*);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! trace {
|
||||
($s:literal $(, $x:expr)* $(,)?) => {
|
||||
{
|
||||
#[cfg(feature = "defmt")]
|
||||
::defmt::trace!($s $(, $x)*);
|
||||
#[cfg(feature="defmt")]
|
||||
let _ = ($( & $x ),*);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! debug {
|
||||
($s:literal $(, $x:expr)* $(,)?) => {
|
||||
{
|
||||
#[cfg(feature = "defmt")]
|
||||
::defmt::debug!($s $(, $x)*);
|
||||
#[cfg(not(feature="defmt"))]
|
||||
let _ = ($( & $x ),*);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! info {
|
||||
($s:literal $(, $x:expr)* $(,)?) => {
|
||||
{
|
||||
#[cfg(feature = "defmt")]
|
||||
::defmt::info!($s $(, $x)*);
|
||||
#[cfg(not(feature="defmt"))]
|
||||
let _ = ($( & $x ),*);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! _warn {
|
||||
($s:literal $(, $x:expr)* $(,)?) => {
|
||||
{
|
||||
#[cfg(feature = "defmt")]
|
||||
::defmt::warn!($s $(, $x)*);
|
||||
#[cfg(not(feature="defmt"))]
|
||||
let _ = ($( & $x ),*);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! error {
|
||||
($s:literal $(, $x:expr)* $(,)?) => {
|
||||
{
|
||||
#[cfg(feature = "defmt")]
|
||||
::defmt::error!($s $(, $x)*);
|
||||
#[cfg(not(feature="defmt"))]
|
||||
let _ = ($( & $x ),*);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(feature = "defmt")]
|
||||
macro_rules! unwrap {
|
||||
($($x:tt)*) => {
|
||||
::defmt::unwrap!($($x)*)
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "defmt"))]
|
||||
macro_rules! unwrap {
|
||||
($arg:expr) => {
|
||||
match $crate::fmt::Try::into_result($arg) {
|
||||
::core::result::Result::Ok(t) => t,
|
||||
::core::result::Result::Err(_) => {
|
||||
::core::panic!();
|
||||
}
|
||||
}
|
||||
};
|
||||
($arg:expr, $($msg:expr),+ $(,)? ) => {
|
||||
match $crate::fmt::Try::into_result($arg) {
|
||||
::core::result::Result::Ok(t) => t,
|
||||
::core::result::Result::Err(_) => {
|
||||
::core::panic!();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
||||
pub struct NoneError;
|
||||
|
||||
pub trait Try {
|
||||
type Ok;
|
||||
type Error;
|
||||
fn into_result(self) -> Result<Self::Ok, Self::Error>;
|
||||
}
|
||||
|
||||
impl<T> Try for Option<T> {
|
||||
type Ok = T;
|
||||
type Error = NoneError;
|
||||
|
||||
#[inline]
|
||||
fn into_result(self) -> Result<T, NoneError> {
|
||||
self.ok_or(NoneError)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, E> Try for Result<T, E> {
|
||||
type Ok = T;
|
||||
type Error = E;
|
||||
|
||||
#[inline]
|
||||
fn into_result(self) -> Self {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct Bytes<'a>(pub &'a [u8]);
|
||||
|
||||
#[cfg(feature = "defmt")]
|
||||
impl defmt::Format for Bytes<'_> {
|
||||
fn format(&self, fmt: defmt::Formatter) {
|
||||
defmt::write!(fmt, "{:02x}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) use _warn as warn;
|
||||
pub(crate) use assert;
|
||||
pub(crate) use assert_eq;
|
||||
pub(crate) use assert_ne;
|
||||
pub(crate) use debug;
|
||||
pub(crate) use debug_assert;
|
||||
pub(crate) use debug_assert_eq;
|
||||
pub(crate) use debug_assert_ne;
|
||||
pub(crate) use error;
|
||||
pub(crate) use info;
|
||||
pub(crate) use panic;
|
||||
pub(crate) use todo;
|
||||
pub(crate) use trace;
|
||||
pub(crate) use unreachable;
|
||||
pub(crate) use unwrap;
|
||||
+482
@@ -0,0 +1,482 @@
|
||||
#![no_std]
|
||||
#![no_main]
|
||||
|
||||
mod fmt;
|
||||
|
||||
use core::mem;
|
||||
use core::ops::{AddAssign, SubAssign};
|
||||
use defmt::Format;
|
||||
#[cfg(not(feature = "defmt"))]
|
||||
use panic_halt as _;
|
||||
#[cfg(feature = "defmt")]
|
||||
use {defmt_rtt as _, panic_probe as _};
|
||||
#[cfg(feature = "defmt")]
|
||||
use defmt::info;
|
||||
|
||||
use embassy_executor::Spawner;
|
||||
use embassy_futures::join::{join3, join4};
|
||||
use embassy_nrf::gpio::{AnyPin, Input, Level, Output, OutputDrive, Pull};
|
||||
use embassy_nrf::pwm::{DutyCycle, Prescaler, SimplePwm};
|
||||
use embassy_nrf::Peri;
|
||||
use embassy_sync::blocking_mutex::raw::ThreadModeRawMutex;
|
||||
use embassy_sync::mutex::Mutex;
|
||||
use embassy_sync::signal::Signal;
|
||||
use embassy_time::{with_timeout, Duration, Timer};
|
||||
// use nrf_softdevice::{raw, Softdevice};
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Format)]
|
||||
enum SysAction {
|
||||
PresentenceOn,
|
||||
PresentenceOff,
|
||||
ConstantOn,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Format)]
|
||||
enum TextLightType {
|
||||
White,
|
||||
ConstantColor,
|
||||
Hue
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Format)]
|
||||
enum HeartLightType {
|
||||
ConstantColor,
|
||||
Hue,
|
||||
HeartBeat
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Format)]
|
||||
pub enum TouchAction {
|
||||
Tap,
|
||||
DoubleTap,
|
||||
TripleTap,
|
||||
Hold,
|
||||
}
|
||||
|
||||
static TOUCH_SIGNAL: Signal<ThreadModeRawMutex, TouchAction> = Signal::new();
|
||||
static RADAR_SIGNAL: Signal<ThreadModeRawMutex, u8> = Signal::new();
|
||||
static TEXT_ANIMATION_SIGNAL: Signal<ThreadModeRawMutex, TextLightType> = Signal::new();
|
||||
static HEART_ANIMATION_SIGNAL: Signal<ThreadModeRawMutex, HeartLightType> = Signal::new();
|
||||
static SYS_ACTION: Mutex<ThreadModeRawMutex, SysAction> = Mutex::new(SysAction::PresentenceOn);
|
||||
static TEXT_ANIMATION: Mutex<ThreadModeRawMutex, TextLightType> = Mutex::new(TextLightType::Hue);
|
||||
static HEART_ANIMATION: Mutex<ThreadModeRawMutex, HeartLightType> = Mutex::new(HeartLightType::HeartBeat);
|
||||
|
||||
#[embassy_executor::main]
|
||||
async fn main(spawner: Spawner) {
|
||||
// let config = nrf_softdevice::Config {
|
||||
// clock: Some(raw::nrf_clock_lf_cfg_t {
|
||||
// source: raw::NRF_CLOCK_LF_SRC_RC as u8,
|
||||
// rc_ctiv: 16,
|
||||
// rc_temp_ctiv: 2,
|
||||
// accuracy: raw::NRF_CLOCK_LF_ACCURACY_500_PPM as u8,
|
||||
// }),
|
||||
// conn_gap: Some(raw::ble_gap_conn_cfg_t {
|
||||
// conn_count: 6,
|
||||
// event_length: 24,
|
||||
// }),
|
||||
// conn_gatt: Some(raw::ble_gatt_conn_cfg_t { att_mtu: 256 }),
|
||||
// gatts_attr_tab_size: Some(raw::ble_gatts_cfg_attr_tab_size_t {
|
||||
// attr_tab_size: raw::BLE_GATTS_ATTR_TAB_SIZE_DEFAULT,
|
||||
// }),
|
||||
// gap_role_count: Some(raw::ble_gap_cfg_role_count_t {
|
||||
// adv_set_count: 1,
|
||||
// periph_role_count: 3,
|
||||
// central_role_count: 3,
|
||||
// central_sec_count: 0,
|
||||
// _bitfield_1: raw::ble_gap_cfg_role_count_t::new_bitfield_1(0),
|
||||
// }),
|
||||
// gap_device_name: Some(raw::ble_gap_cfg_device_name_t {
|
||||
// p_value: b"HelloRust" as *const u8 as _,
|
||||
// current_len: 9,
|
||||
// max_len: 9,
|
||||
// write_perm: unsafe { mem::zeroed() },
|
||||
// _bitfield_1: raw::ble_gap_cfg_device_name_t::new_bitfield_1(raw::BLE_GATTS_VLOC_STACK as u8),
|
||||
// }),
|
||||
// ..Default::default()
|
||||
// };
|
||||
//
|
||||
// let _sd = Softdevice::enable(&config);
|
||||
|
||||
let p = embassy_nrf::init(Default::default());
|
||||
let pwm_text = SimplePwm::new_3ch(p.PWM0, p.P1_15, p.P1_13, p.P1_11, &Default::default());
|
||||
let pwm_heart = SimplePwm::new_3ch(p.PWM2, p.P0_31, p.P0_29, p.P0_02, &Default::default());
|
||||
spawner.spawn(radar_task()).expect("failed to spawn radar task");
|
||||
spawner.spawn(actions_task()).expect("failed to spawn actions task");
|
||||
|
||||
join4(touch_button(p.P0_20.into()), moving_radar(p.P0_22.into()), heartbeat_task(pwm_heart), light_task(pwm_text)).await;
|
||||
}
|
||||
|
||||
async fn touch_button(pin: Peri<'static, AnyPin>) {
|
||||
let mut button = Input::new(pin, Pull::Down);
|
||||
|
||||
loop {
|
||||
info!("Wait for button");
|
||||
button.wait_for_high().await;
|
||||
let mut tap_count = 0;
|
||||
let mut is_hold = false;
|
||||
|
||||
info!("Wait for low 1");
|
||||
match with_timeout(Duration::from_millis(400), button.wait_for_low()).await {
|
||||
Err(_) => {
|
||||
info!("Look holding");
|
||||
is_hold = true;
|
||||
TOUCH_SIGNAL.signal(TouchAction::Hold);
|
||||
button.wait_for_low().await;
|
||||
}
|
||||
Ok(_) => {
|
||||
tap_count = 1;
|
||||
|
||||
info!("Wait for next tap ({})", tap_count);
|
||||
loop {
|
||||
match with_timeout(Duration::from_millis(300), button.wait_for_high()).await {
|
||||
Ok(_) => {
|
||||
info!("Wait for tap release ({})", tap_count);
|
||||
button.wait_for_low().await;
|
||||
tap_count += 1;
|
||||
if tap_count >= 3 { break; }
|
||||
}
|
||||
Err(_) => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Dispatch the action if it wasn't a hold
|
||||
if !is_hold {
|
||||
match tap_count {
|
||||
1 => TOUCH_SIGNAL.signal(TouchAction::Tap),
|
||||
2 => TOUCH_SIGNAL.signal(TouchAction::DoubleTap),
|
||||
3 => TOUCH_SIGNAL.signal(TouchAction::TripleTap),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// Debounce / Cool-down
|
||||
Timer::after_millis(100).await;
|
||||
}
|
||||
}
|
||||
|
||||
#[embassy_executor::task]
|
||||
async fn actions_task() {
|
||||
loop {
|
||||
let signal = TOUCH_SIGNAL.wait().await;
|
||||
info!("Action: {:?}", signal);
|
||||
match signal {
|
||||
// constant on vs presentence
|
||||
TouchAction::Tap => {
|
||||
let mut action = SYS_ACTION.lock().await;
|
||||
match *action {
|
||||
SysAction::PresentenceOn |
|
||||
SysAction::PresentenceOff => {
|
||||
*action = SysAction::ConstantOn;
|
||||
info!("Constant light");
|
||||
}
|
||||
SysAction::ConstantOn => {
|
||||
*action = SysAction::PresentenceOn;
|
||||
info!("Light by presentence");
|
||||
}
|
||||
}
|
||||
info!("Light mode {:?}", *action);
|
||||
}
|
||||
// change text animation
|
||||
TouchAction::DoubleTap => {
|
||||
let mut current = TEXT_ANIMATION.lock().await;
|
||||
let next = match *current {
|
||||
TextLightType::ConstantColor => {
|
||||
TextLightType::White
|
||||
}
|
||||
TextLightType::Hue => {
|
||||
TextLightType::ConstantColor
|
||||
}
|
||||
TextLightType::White => {
|
||||
TextLightType::Hue
|
||||
}
|
||||
};
|
||||
*current = next;
|
||||
info!("Set text to {:?}", next);
|
||||
TEXT_ANIMATION_SIGNAL.signal(next);
|
||||
}
|
||||
// change anchor animation
|
||||
TouchAction::TripleTap => {
|
||||
let mut current = HEART_ANIMATION.lock().await;
|
||||
let next = match *current {
|
||||
HeartLightType::ConstantColor => {
|
||||
HeartLightType::Hue
|
||||
}
|
||||
HeartLightType::Hue => {
|
||||
HeartLightType::HeartBeat
|
||||
}
|
||||
HeartLightType::HeartBeat => {
|
||||
HeartLightType::ConstantColor
|
||||
}
|
||||
};
|
||||
info!("Set heart to {:?}", next);
|
||||
*current = next;
|
||||
HEART_ANIMATION_SIGNAL.signal(next);
|
||||
}
|
||||
// enter pairing mode
|
||||
TouchAction::Hold => {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[embassy_executor::task]
|
||||
async fn radar_task() {
|
||||
const MAX_TIME: usize = 24;
|
||||
let mut time = MAX_TIME;
|
||||
loop {
|
||||
let action = {
|
||||
let current = SYS_ACTION.lock().await;
|
||||
*current
|
||||
};
|
||||
if action != SysAction::ConstantOn {
|
||||
if let Err(_) = with_timeout(Duration::from_secs(1), RADAR_SIGNAL.wait()).await {
|
||||
time = time.saturating_sub(1);
|
||||
if time == 0 {
|
||||
let mut action = SYS_ACTION.lock().await;
|
||||
if *action == SysAction::PresentenceOn {
|
||||
*action = SysAction::PresentenceOff;
|
||||
info!("Off light due to no presentence");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if time == 0 {
|
||||
let mut action = SYS_ACTION.lock().await;
|
||||
if *action == SysAction::PresentenceOff {
|
||||
*action = SysAction::PresentenceOn;
|
||||
info!("On light due to presentence");
|
||||
}
|
||||
}
|
||||
time = MAX_TIME;
|
||||
}
|
||||
} else {
|
||||
time = MAX_TIME;
|
||||
Timer::after_millis(500).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn moving_radar(pin: Peri<'static, AnyPin>) {
|
||||
let mut radar = Input::new(pin, Pull::Down);
|
||||
loop {
|
||||
info!("Waiting for radar");
|
||||
radar.wait_for_high().await;
|
||||
RADAR_SIGNAL.signal(0);
|
||||
while let Err(_) = with_timeout(Duration::from_secs(1), radar.wait_for_low()).await {
|
||||
info!("Moving!!");
|
||||
RADAR_SIGNAL.signal(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn light_task(mut pwm: SimplePwm<'static>) {
|
||||
info!("Starting light task");
|
||||
const MAX_DUTY: u16 = 32767;
|
||||
|
||||
pwm.set_prescaler(Prescaler::Div1);
|
||||
pwm.set_max_duty(MAX_DUTY);
|
||||
pwm.set_ch1_drive(OutputDrive::Standard);
|
||||
pwm.set_ch2_drive(OutputDrive::Standard);
|
||||
pwm.set_ch3_drive(OutputDrive::Standard);
|
||||
pwm.set_all_duties([DutyCycle::normal(0), DutyCycle::normal(0), DutyCycle::normal(0), DutyCycle::normal(0)]);
|
||||
|
||||
let mut intensity = 0i32;
|
||||
let mut hue: u16 = 0;
|
||||
let mut animation_type = TextLightType::Hue;
|
||||
|
||||
loop {
|
||||
let action = {
|
||||
let guard = SYS_ACTION.lock().await;
|
||||
guard.clone()
|
||||
};
|
||||
match action {
|
||||
SysAction::PresentenceOn if intensity < 100 => {
|
||||
intensity.add_assign(1);
|
||||
info!("Increase intensity to {}", intensity);
|
||||
},
|
||||
SysAction::PresentenceOff if intensity > 0 => {
|
||||
intensity.sub_assign(1);
|
||||
info!("Decrease intensity to {}", intensity);
|
||||
},
|
||||
_ => {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(val) = with_timeout(Duration::from_millis(10), TEXT_ANIMATION_SIGNAL.wait()).await {
|
||||
animation_type = val;
|
||||
}
|
||||
match animation_type {
|
||||
TextLightType::Hue => {
|
||||
let (r, g, b) = hue_to_rgb(hue, MAX_DUTY);
|
||||
|
||||
pwm.set_all_duties([
|
||||
DutyCycle::normal(((r as i32) * intensity / 100) as u16),
|
||||
DutyCycle::normal(((g as i32) * intensity / 100) as u16),
|
||||
DutyCycle::normal(((b as i32) * intensity / 100) as u16),
|
||||
DutyCycle::normal(0), // Ch4 unused
|
||||
]);
|
||||
|
||||
hue = (hue + 1) % 1536;
|
||||
}
|
||||
TextLightType::White => {
|
||||
let r = 12288; // Lowered significantly
|
||||
let g = 16384;
|
||||
let b = 24576;
|
||||
|
||||
// Set them once. No loop, no flicker.
|
||||
pwm.set_all_duties([
|
||||
DutyCycle::normal(((r) * intensity / 100) as u16),
|
||||
DutyCycle::normal(((g) * intensity / 100) as u16),
|
||||
DutyCycle::normal(((b) * intensity / 100) as u16),
|
||||
DutyCycle::normal(0),
|
||||
]);
|
||||
}
|
||||
TextLightType::ConstantColor => {
|
||||
let (r, g, b) = hue_to_rgb(hue, MAX_DUTY);
|
||||
|
||||
pwm.set_all_duties([
|
||||
DutyCycle::normal(((r as i32) * intensity / 100) as u16),
|
||||
DutyCycle::normal(((g as i32) * intensity / 100) as u16),
|
||||
DutyCycle::normal(((b as i32) * intensity / 100) as u16),
|
||||
DutyCycle::normal(0), // Ch4 unused
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn hue_to_rgb(hue: u16, max: u16) -> (u16, u16, u16) {
|
||||
let sector = (hue / 256) as u8;
|
||||
let rising = (hue % 256) as u32 * max as u32 / 255;
|
||||
let falling = max as u32 - rising;
|
||||
|
||||
let (r, g, b) = match sector {
|
||||
0 => (max as u32, rising, 0), // Red to Yellow
|
||||
1 => (falling, max as u32, 0), // Yellow to Green
|
||||
2 => (0, max as u32, rising), // Green to Cyan
|
||||
3 => (0, falling, max as u32), // Cyan to Blue
|
||||
4 => (rising, 0, max as u32), // Blue to Magenta
|
||||
_ => (max as u32, 0, falling), // Magenta to Red
|
||||
};
|
||||
|
||||
(r as u16, g as u16, b as u16)
|
||||
}
|
||||
|
||||
async fn heartbeat_task(mut pwm: SimplePwm<'static>) {
|
||||
const MAX: u16 = 16384;
|
||||
const MIN: u16 = 2048;
|
||||
pwm.set_max_duty(MAX);
|
||||
pwm.set_prescaler(Prescaler::Div1);
|
||||
|
||||
|
||||
let mut intensity = 0i32;
|
||||
let mut animation_type = HeartLightType::HeartBeat;
|
||||
let mut hue: u16 = 0;
|
||||
let mut heart_beat = 0;
|
||||
|
||||
loop {
|
||||
let action = {
|
||||
let guard = SYS_ACTION.lock().await;
|
||||
guard.clone()
|
||||
};
|
||||
match action {
|
||||
SysAction::PresentenceOn if intensity < 100 => {
|
||||
intensity.add_assign(1);
|
||||
info!("Increase intensity to {}", intensity);
|
||||
},
|
||||
SysAction::PresentenceOff if intensity > 0 => {
|
||||
intensity.sub_assign(1);
|
||||
info!("Decrease intensity to {}", intensity);
|
||||
},
|
||||
_ => {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(val) = with_timeout(Duration::from_millis(10), HEART_ANIMATION_SIGNAL.wait()).await {
|
||||
animation_type = val;
|
||||
}
|
||||
|
||||
match animation_type {
|
||||
HeartLightType::ConstantColor => {
|
||||
let (r, g, b) = hue_to_rgb(hue, MAX);
|
||||
|
||||
pwm.set_all_duties([
|
||||
DutyCycle::normal(((r as i32) * intensity / 100) as u16),
|
||||
DutyCycle::normal(((g as i32) * intensity / 100) as u16),
|
||||
DutyCycle::normal(((b as i32) * intensity / 100) as u16),
|
||||
DutyCycle::normal(0), // Ch4 unused
|
||||
]);
|
||||
}
|
||||
HeartLightType::Hue => {
|
||||
let (r, g, b) = hue_to_rgb(hue, MAX);
|
||||
|
||||
pwm.set_all_duties([
|
||||
DutyCycle::normal(((r as i32) * intensity / 100) as u16),
|
||||
DutyCycle::normal(((g as i32) * intensity / 100) as u16),
|
||||
DutyCycle::normal(((b as i32) * intensity / 100) as u16),
|
||||
DutyCycle::normal(0), // Ch4 unused
|
||||
]);
|
||||
|
||||
hue = (hue + 1) % 1536;
|
||||
}
|
||||
HeartLightType::HeartBeat => {
|
||||
let beat = get_heartbeat(heart_beat);
|
||||
|
||||
pwm.set_all_duties([
|
||||
DutyCycle::normal(((beat as i32) * intensity / 100) as u16),
|
||||
DutyCycle::normal(0),
|
||||
DutyCycle::normal(0),
|
||||
DutyCycle::normal(0), // Ch4 unused
|
||||
]);
|
||||
|
||||
heart_beat += 10;
|
||||
if heart_beat >= 32768 {
|
||||
heart_beat = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const BPM: u32 = 24;
|
||||
const MIN_GLOW: u32 = 2048;
|
||||
const MAX_VAL: u32 = 32768;
|
||||
const PERIOD_MS: u32 = 60_000 / BPM;
|
||||
|
||||
/// Heartbeat logic optimized for MCU (no floats, precalculated constants)
|
||||
pub fn get_heartbeat(sys_time_ms: u32) -> u16 {
|
||||
let time_tick = (((sys_time_ms % PERIOD_MS) as u64 * MAX_VAL as u64) / PERIOD_MS as u64) as u32;
|
||||
|
||||
// 1. Calculate the pulse intensity (0..MAX_VAL)
|
||||
let pulse = if time_tick > 2000 && time_tick < 7000 {
|
||||
scale_pulse(time_tick, 2000, 4000, 7000, MAX_VAL)
|
||||
} else if time_tick > 8500 && time_tick < 12500 {
|
||||
scale_pulse(time_tick, 8500, 10000, 12500, MAX_VAL / 2)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
// 2. Square it for that organic "ease-in" feel
|
||||
let eased_pulse = (pulse * pulse) >> 15;
|
||||
|
||||
// 3. Map the 0..MAX_VAL pulse into the MIN_GLOW..MAX_VAL range
|
||||
// Formula: MIN + (eased_pulse * (MAX - MIN) / MAX)
|
||||
let range = MAX_VAL - MIN_GLOW;
|
||||
let final_val = MIN_GLOW + ((eased_pulse * range) >> 15);
|
||||
|
||||
final_val as u16
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn scale_pulse(t: u32, start: u32, peak: u32, end: u32, max: u32) -> u32 {
|
||||
if t <= peak {
|
||||
((t - start) * max) / (peak - start)
|
||||
} else {
|
||||
((end - t) * max) / (end - peak)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user