diff --git a/backend/src/tcp/mod.rs b/backend/src/tcp/mod.rs index dfeacb1..93a0d90 100644 --- a/backend/src/tcp/mod.rs +++ b/backend/src/tcp/mod.rs @@ -1,4 +1,7 @@ -use std::net::{Ipv4Addr, SocketAddrV4}; +use std::{ + net::{Ipv4Addr, SocketAddrV4}, + time::{SystemTime, UNIX_EPOCH}, +}; use color_eyre::eyre::{self}; use packets::{Color, Packet, State}; @@ -62,7 +65,18 @@ pub async fn receive_device_update_events(state: AppState) -> eyre::Result<()> { .await?; } Mode::Sun => { - Packet::save_send(&mut socket, &mut buf, Packet::State(State::Sun)).await?; + let timestamp = + SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() % (60 * 60 * 24); + + Packet::save_send( + &mut socket, + &mut buf, + Packet::State(State::Sun { + sunrise: timestamp + 30, + sunset: timestamp + 150, + }), + ) + .await?; } Mode::Off => { Packet::save_send(&mut socket, &mut buf, Packet::State(State::Off)).await?; diff --git a/backend/src/tcp/registration.rs b/backend/src/tcp/registration.rs index 38f1b05..6b2b613 100644 --- a/backend/src/tcp/registration.rs +++ b/backend/src/tcp/registration.rs @@ -84,7 +84,18 @@ async fn handle_registration( .await?; } Mode::Sun => { - Packet::save_send(socket, &mut buf, Packet::State(State::Sun)).await?; + let timestamp = + SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() % (60 * 60 * 24); + + Packet::save_send( + socket, + &mut buf, + Packet::State(State::Sun { + sunrise: timestamp + 30, + sunset: timestamp + 150, + }), + ) + .await?; } Mode::Off => {} } diff --git a/firmware/Cargo.lock b/firmware/Cargo.lock index 4b63b95..c60b724 100644 --- a/firmware/Cargo.lock +++ b/firmware/Cargo.lock @@ -1050,6 +1050,7 @@ dependencies = [ "critical-section", "dotenvy", "embassy-executor", + "embassy-futures", "embassy-net", "embassy-sync 0.8.0", "embassy-time", diff --git a/firmware/Cargo.toml b/firmware/Cargo.toml index 5560713..ed74ec2 100644 --- a/firmware/Cargo.toml +++ b/firmware/Cargo.toml @@ -40,6 +40,8 @@ esp-println = { version = "0.17.0", features = ["esp32", "log-04"] } embassy-executor = { version = "0.10.0", features = ["log"] } embassy-sync = "0.8.0" embassy-time = { version = "0.5.0", features = ["log"] } +embassy-futures = { version = "0.1.2" } + esp-radio = { version = "0.18.0", features = ["esp32", "log-04", "unstable", "wifi-eap"] } smoltcp = { version = "0.13.0", default-features = false, features = [ "log", diff --git a/firmware/src/main.rs b/firmware/src/main.rs index 802633f..bcd5c19 100644 --- a/firmware/src/main.rs +++ b/firmware/src/main.rs @@ -6,8 +6,10 @@ holding buffers for the duration of a data transfer." )] +use embassy_futures::select::{Either, select}; use embassy_sync::blocking_mutex::raw::NoopRawMutex; use embassy_sync::channel::{Channel, Receiver, Sender}; +use embassy_time::{Duration, Timer}; use esp_hal::gpio::AnyPin; use esp_hal::mcpwm::operator::{PwmActions, PwmPinConfig, PwmUpdateMethod, UpdateAction}; use esp_hal::mcpwm::timer::PwmWorkingMode; @@ -27,11 +29,13 @@ use static_cell::StaticCell; use crate::clock::CLOCK; use crate::communication::{RegisterEventChannel, listen_register_events, listen_updates}; +use crate::sun::calculate_current_color; extern crate alloc; pub mod clock; pub mod communication; +pub mod sun; pub mod tcp; pub mod wifi; @@ -87,6 +91,9 @@ async fn main(spawner: Spawner) { static COLOR_EVENT_CHANNEL: StaticCell = StaticCell::new(); let color_event_channel = COLOR_EVENT_CHANNEL.init(Channel::new()); + static SUN_EVENT_CHANNEL: StaticCell = StaticCell::new(); + let sun_event_channel = SUN_EVENT_CHANNEL.init(Channel::new()); + spawner.spawn( listen_register_events( stack, @@ -97,7 +104,14 @@ async fn main(spawner: Spawner) { ); spawner.spawn(listen_updates(stack, event_channel.sender()).unwrap()); - spawner.spawn(handle_event(event_channel.receiver(), color_event_channel.sender()).unwrap()); + spawner.spawn( + handle_event( + event_channel.receiver(), + color_event_channel.sender(), + sun_event_channel.sender(), + ) + .unwrap(), + ); spawner.spawn( handle_color( @@ -111,6 +125,7 @@ async fn main(spawner: Spawner) { ) .unwrap(), ); + spawner.spawn(handle_sun(color_event_channel.sender(), sun_event_channel.receiver()).unwrap()); } pub type UpdateEventChannel = Channel; @@ -121,6 +136,7 @@ pub type UpdateEventReceiver = Receiver<'static, NoopRawMutex, Event, 1>; pub async fn handle_event( event_receiver: UpdateEventReceiver, color_event_sender: UpdateColorEventSender, + sun_event_receiver: SunEventSender, ) { loop { let event = event_receiver.receive().await; @@ -130,22 +146,114 @@ pub async fn handle_event( Event::TimeSync(time) => CLOCK.lock().await.update(time), Event::StateChange(state) => match state { State::Off => { - color_event_sender.send(Color(0, 0, 0)).await; + sun_event_receiver.send(None).await; + color_event_sender.send(FullColor::new(0, 0, 0, 0)).await; } State::Color(color) => { - color_event_sender.send(color).await; + sun_event_receiver.send(None).await; + color_event_sender.send(FullColor::from_color(color)).await; } - State::Sun => { - info!("sun change received"); + State::Sun { sunrise, sunset } => { + sun_event_receiver.send(Some((sunrise, sunset))).await; } }, } } } -pub type UpdateColorEventChannel = Channel; -pub type UpdateColorEventSender = Sender<'static, NoopRawMutex, Color, 1>; -pub type UpdateColorEventReceiver = Receiver<'static, NoopRawMutex, Color, 1>; +pub type SunEvent = Option<(u64, u64)>; + +pub type SunEventChannel = Channel; +pub type SunEventSender = Sender<'static, NoopRawMutex, SunEvent, 1>; +pub type SunEventReceiver = Receiver<'static, NoopRawMutex, SunEvent, 1>; + +#[embassy_executor::task] +pub async fn handle_sun( + color_event_sender: UpdateColorEventSender, + sun_event_receiver: SunEventReceiver, +) { + let mut enabled = false; + let mut sunrise = 0; + let mut sunset = 0; + + loop { + let result = select( + sun_event_receiver.receive(), + Timer::after(Duration::from_millis(5000)), + ) + .await; + + match result { + Either::First(None) => enabled = false, + Either::First(Some((new_sunrise, new_sunset))) => { + enabled = true; + sunrise = new_sunrise; + sunset = new_sunset; + } + Either::Second(_) => { + if !enabled { + continue; + } + + let current_time = CLOCK.lock().await.now(); + let new_color = calculate_current_color(sunrise, sunset, current_time); + color_event_sender.send(new_color).await; + } + } + } +} + +// TODO: swap to use f32 +#[derive(Debug, Clone, Default, PartialEq)] +pub struct FullColor { + pub r: u8, + pub g: u8, + pub b: u8, + pub w: u8, +} + +impl FullColor { + pub const fn new(r: u8, g: u8, b: u8, w: u8) -> Self { + Self { r, g, b, w } + } + + pub fn from_color(color: Color) -> Self { + let white_out = color.0.min(color.1).min(color.2); + let red_out = color.0 - white_out; + let green_out = color.1 - white_out; + let blue_out = color.2 - white_out; + + Self::new(red_out, green_out, blue_out, white_out) + } + + pub fn with_brightness(&self, brightness: f32) -> Self { + let b = brightness.clamp(0.0, 1.0); + + FullColor { + r: (self.r as f32 * b) as u8, + g: (self.g as f32 * b) as u8, + b: (self.b as f32 * b) as u8, + w: (self.w as f32 * b) as u8, + } + } + + fn lerp(color_1: &FullColor, color_2: &FullColor, percent: f32) -> FullColor { + let t = percent.clamp(0.0, 1.0); + + let lerp_u8 = |a: u8, b: u8| -> u8 { (a as f32 + (b as f32 - a as f32) * t) as u8 }; + + FullColor { + r: lerp_u8(color_1.r, color_2.r), + g: lerp_u8(color_1.g, color_2.g), + b: lerp_u8(color_1.b, color_2.b), + w: lerp_u8(color_1.w, color_2.w), + } + } +} + +pub type UpdateColorEventChannel = Channel; +pub type UpdateColorEventSender = Sender<'static, NoopRawMutex, FullColor, 1>; +pub type UpdateColorEventReceiver = Receiver<'static, NoopRawMutex, FullColor, 1>; #[embassy_executor::task] pub async fn handle_color( @@ -187,18 +295,23 @@ pub async fn handle_color( mcpwm0.timer2.start(timer_clock_cfg); mcpwm1.timer0.start(timer_clock_cfg); + let mut prev_color = FullColor::default(); + loop { let color = color_event_receiver.receive().await; + if color == prev_color { + continue; + } + info!("received color_event: {:?}", color); - let white_out = color.0.min(color.1).min(color.2); - let red_out = color.0 - white_out; - let green_out = color.1 - white_out; - let blue_out = color.2 - white_out; + //TODO: do human brightness conversion + + pwm_red_pin.set_timestamp((color.r) as u16); + pwm_green_pin.set_timestamp((color.g) as u16); + pwm_blue_pin.set_timestamp((color.b) as u16); + pwm_white_pin.set_timestamp((color.w) as u16); - pwm_red_pin.set_timestamp((red_out) as u16); - pwm_green_pin.set_timestamp((green_out) as u16); - pwm_blue_pin.set_timestamp((blue_out) as u16); - pwm_white_pin.set_timestamp((white_out) as u16); + prev_color = color; } } diff --git a/firmware/src/sun.rs b/firmware/src/sun.rs new file mode 100644 index 0000000..fe7f025 --- /dev/null +++ b/firmware/src/sun.rs @@ -0,0 +1,70 @@ +use crate::FullColor; + +const KEYFRAME_MAP: &[Keyframe] = &[ + Keyframe::new(0.00, 255, 40, 0, 0, 0.05), + Keyframe::new(0.05, 255, 120, 20, 40, 0.20), + Keyframe::new(0.10, 255, 180, 120, 180, 0.60), + Keyframe::new(0.50, 20, 20, 20, 255, 1.00), + Keyframe::new(0.90, 255, 180, 120, 180, 0.60), + Keyframe::new(0.95, 255, 80, 0, 20, 0.20), + Keyframe::new(1.00, 0, 0, 5, 0, 0.01), +]; + +struct Keyframe { + percent: f32, + color: FullColor, + brightness: f32, +} + +impl Keyframe { + const fn new(percent: f32, r: u8, g: u8, b: u8, w: u8, brightness: f32) -> Self { + Self { + percent, + color: FullColor::new(r, g, b, w), + brightness, + } + } +} + +fn find_closest_keyframes(percent: f32) -> (&'static Keyframe, &'static Keyframe) { + let mut closest = (&KEYFRAME_MAP[0], &KEYFRAME_MAP[KEYFRAME_MAP.len() - 1]); + + for i in 1..KEYFRAME_MAP.len() { + let keyframe = &KEYFRAME_MAP[i]; + if percent >= keyframe.percent + && (percent - keyframe.percent).abs() < (percent - closest.0.percent).abs() + { + closest.0 = keyframe; + } + if percent <= keyframe.percent + && (percent - keyframe.percent).abs() < (percent - closest.1.percent).abs() + { + closest.1 = keyframe; + } + } + + closest +} + +pub fn calculate_current_color(sun_rise: u64, sun_set: u64, current_time: u64) -> FullColor { + let current_normalized = current_time % (60 * 60 * 24); + + let percent = (current_normalized as f32 - sun_rise as f32) / (sun_set - sun_rise) as f32; + + if percent < 0.0 || percent > 1.0 { + return FullColor::default(); + } + + let (left, right) = find_closest_keyframes(percent); + if left.percent == right.percent { + return left.color.with_brightness(left.brightness); + } + + let lerp_percent = (percent - left.percent) / (right.percent - left.percent); + + FullColor::lerp( + &left.color.with_brightness(left.brightness), + &right.color.with_brightness(right.brightness), + lerp_percent, + ) +} diff --git a/lib/packets/src/lib.rs b/lib/packets/src/lib.rs index d440c9b..b2750a6 100644 --- a/lib/packets/src/lib.rs +++ b/lib/packets/src/lib.rs @@ -42,7 +42,7 @@ impl Color { pub enum State { Off, Color(Color), - Sun, + Sun { sunrise: u64, sunset: u64 }, } #[derive(Debug, Error)] @@ -156,9 +156,13 @@ impl<'a> Packet<'a> { 4 } - Packet::State(State::Sun) => { + Packet::State(State::Sun { sunrise, sunset }) => { Self::write_u8(buf, 0, 6); - 1 + + Self::write_u64(buf, 1, *sunrise); + Self::write_u64(buf, 9, *sunset); + + 17 } }; @@ -215,7 +219,12 @@ impl<'a> Packet<'a> { Ok(Packet::State(State::Color(Color(red, green, blue)))) } - 6 => Ok(Packet::State(State::Sun)), + 6 => { + let sunrise = Self::read_u64(buf, 1); + let sunset = Self::read_u64(buf, 9); + + Ok(Packet::State(State::Sun { sunrise, sunset })) + } _ => Err(PacketError::UnknownType), } }