This commit is contained in:
2026-02-08 18:11:50 +02:00
commit cbfc8743f7
15 changed files with 1614 additions and 0 deletions
+241
View File
@@ -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
View File
@@ -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)
}
}