From 514c9defd6ef18925cc4ebec9648cfc8fb58cffc Mon Sep 17 00:00:00 2001 From: Torrie Fischer Date: Sun, 15 Dec 2024 19:27:27 +0100 Subject: [PATCH] properties: rewrite properties (whoops) --- Cargo.toml | 2 + src/animations/mod.rs | 86 ++++ src/{animations.rs => animations/test.rs} | 98 +---- src/events.rs | 165 ++------ src/inputs/circadian.rs | 103 +++++ src/inputs/mod.rs | 1 + src/lib8/mod.rs | 24 +- src/main.rs | 41 +- src/platform/esp32.rs | 453 ---------------------- src/platform/esp32/board.rs | 143 +++++++ src/platform/esp32/mod.rs | 3 + src/platform/esp32/mqtt.rs | 186 +++++++++ src/platform/esp32/wifi.rs | 104 +++++ src/platform/mod.rs | 11 +- src/platform/smart_leds_lib.rs | 9 +- src/properties.rs | 270 +++++++++++++ src/render.rs | 18 +- src/scenes.rs | 43 +- src/task.rs | 6 +- 19 files changed, 1051 insertions(+), 715 deletions(-) create mode 100644 src/animations/mod.rs rename src/{animations.rs => animations/test.rs} (55%) create mode 100644 src/inputs/circadian.rs create mode 100644 src/inputs/mod.rs delete mode 100644 src/platform/esp32.rs create mode 100644 src/platform/esp32/board.rs create mode 100644 src/platform/esp32/mod.rs create mode 100644 src/platform/esp32/mqtt.rs create mode 100644 src/platform/esp32/wifi.rs create mode 100644 src/properties.rs diff --git a/Cargo.toml b/Cargo.toml index 7c966be..0e7fef6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,6 +45,8 @@ smart-leds = { version = "0.4.0", optional = true } embedded-graphics = { version = "0.8.1", optional = true, features = ["fixed_point", "defmt"] } num = "0.4.3" chrono = "0.4.38" +serde_json = "1.0.133" +paste = "1.0.15" [build-dependencies] embuild = "0.32.0" diff --git a/src/animations/mod.rs b/src/animations/mod.rs new file mode 100644 index 0000000..1da9970 --- /dev/null +++ b/src/animations/mod.rs @@ -0,0 +1,86 @@ +pub mod test; + +use rgb::RGB8; +use crate::lib8::Hsv; + +use crate::events::EventBus; +use crate::geometry::*; +use crate::render::{Shader, Surface, Surfaces}; +use crate::task::Task; +use crate::lib8::{trig::{sin8, cos8}, noise::inoise8, IntoRgb8}; + +#[derive(Debug)] +pub struct IdleTask { + solid: T, + surface: T, + shimmer: T, +} + +#[derive(Debug)] +struct SolidShader {} + +impl Shader for SolidShader { + fn draw(&self, _coords: &VirtualCoordinates, frame: usize) -> RGB8 { + Hsv::new((frame % 255) as u8, 255, sin8((frame % 64) as u8)).into_rgb8() + } +} + +#[derive(Debug)] +struct ShimmerShader {} + +impl Shader for ShimmerShader { + fn draw(&self, coords: &VirtualCoordinates, frame: usize) -> RGB8 { + Hsv::new(((coords.x as usize).wrapping_add(frame / 3) % 255) as u8, coords.y, sin8(frame).max(75).min(64)).into_rgb8() + } +} + +#[derive(Debug)] +struct ThinkingShader {} + +impl Shader for ThinkingShader { + fn draw(&self, coords: &VirtualCoordinates, frame: usize) -> RGB8 { + //let noise_x = sin8(sin8((frame % 255) as u8).wrapping_add(coords.x)); + //let noise_y = cos8(cos8((frame % 255) as u8).wrapping_add(coords.y)); + let offset_x = sin8(frame.wrapping_add(coords.x as usize)); + let offset_y = cos8(frame.wrapping_add(coords.y as usize)); + let noise_x = offset_x / 2; + let noise_y = offset_y / 2; + //let noise_x = coords.x.wrapping_add(offset_x); + //let noise_y = coords.y.wrapping_add(offset_y); + Hsv::new( + inoise8(offset_x as i16, offset_y as i16), + 128_u8.saturating_add(inoise8(noise_y as i16, noise_x as i16)), + 255 + ).into_rgb8() + } +} + +impl IdleTask { + pub fn new>(surfaces: &mut S) -> Self { + IdleTask { + solid: surfaces.new_surface(Rectangle::everything()).unwrap(), + surface: surfaces.new_surface(Rectangle::everything()).unwrap(), + shimmer: surfaces.new_surface(Rectangle::everything()).unwrap(), + } + } +} + +impl Task for IdleTask { + fn name(&self) -> &'static str { "Idle" } + + fn start(&mut self, _bus: &mut EventBus) { + self.solid.set_shader(SolidShader {}); + self.surface.set_shader(ThinkingShader { }); + self.shimmer.set_shader(ShimmerShader { }); + + self.solid.set_opacity(64); + self.surface.set_opacity(128); + self.shimmer.set_opacity(64); + } + + fn stop(&mut self, _bus: &mut EventBus) { + self.solid.clear_shader(); + self.surface.clear_shader(); + self.shimmer.clear_shader(); + } +} \ No newline at end of file diff --git a/src/animations.rs b/src/animations/test.rs similarity index 55% rename from src/animations.rs rename to src/animations/test.rs index 45c2ba3..9e7466d 100644 --- a/src/animations.rs +++ b/src/animations/test.rs @@ -1,89 +1,9 @@ -use palette::Hsv; - +use crate::lib8::Hsv; use rgb::RGB8; -use crate::events::{Event, EventBus}; -use crate::time::Periodically; -use crate::geometry::*; -use crate::render::{Shader, Surface, Surfaces}; -use crate::task::Task; -use crate::lib8::{trig::{sin8, cos8}, interpolate::scale8, noise::inoise8, IntoRgb8}; +use crate::{events::{Event, EventBus}, lib8::{interpolate::scale8, trig::{cos8, sin8}, IntoRgb8}, render::{Shader, Surface}, task::Task, time::Periodically}; -#[derive(Debug)] -pub struct IdleTask { - solid: T, - surface: T, - shimmer: T, -} - -#[derive(Debug)] -struct SolidShader {} - -impl Shader for SolidShader { - fn draw(&self, _coords: &VirtualCoordinates, frame: usize) -> RGB8 { - Hsv::new_srgb((frame % 255) as u8, 255, sin8((frame % 64) as u8)).into_rgb8() - } -} - -#[derive(Debug)] -struct ShimmerShader {} - -impl Shader for ShimmerShader { - fn draw(&self, coords: &VirtualCoordinates, frame: usize) -> RGB8 { - Hsv::new_srgb(((coords.x as usize).wrapping_add(frame / 3) % 255) as u8, coords.y, sin8(frame).max(75).min(64)).into_rgb8() - } -} - -#[derive(Debug)] -struct ThinkingShader {} - -impl Shader for ThinkingShader { - fn draw(&self, coords: &VirtualCoordinates, frame: usize) -> RGB8 { - //let noise_x = sin8(sin8((frame % 255) as u8).wrapping_add(coords.x)); - //let noise_y = cos8(cos8((frame % 255) as u8).wrapping_add(coords.y)); - let offset_x = sin8(frame.wrapping_add(coords.x as usize)); - let offset_y = cos8(frame.wrapping_add(coords.y as usize)); - let noise_x = offset_x / 2; - let noise_y = offset_y / 2; - //let noise_x = coords.x.wrapping_add(offset_x); - //let noise_y = coords.y.wrapping_add(offset_y); - Hsv::new_srgb( - inoise8(offset_x as i16, offset_y as i16), - 128_u8.saturating_add(inoise8(noise_y as i16, noise_x as i16)), - 255 - ).into_rgb8() - } -} - -impl IdleTask { - pub fn new>(surfaces: &mut S) -> Self { - IdleTask { - solid: surfaces.new_surface(Rectangle::everything()).unwrap(), - surface: surfaces.new_surface(Rectangle::everything()).unwrap(), - shimmer: surfaces.new_surface(Rectangle::everything()).unwrap(), - } - } -} - -impl Task for IdleTask { - fn name(&self) -> &'static str { "Idle" } - - fn start(&mut self, _bus: &mut EventBus) { - self.solid.set_shader(Box::new(SolidShader { })); - self.surface.set_shader(Box::new(ThinkingShader { })); - self.shimmer.set_shader(Box::new(ShimmerShader { })); - - self.solid.set_opacity(64); - self.surface.set_opacity(128); - self.shimmer.set_opacity(64); - } - - fn stop(&mut self, _bus: &mut EventBus) { - self.solid.clear_shader(); - self.surface.clear_shader(); - self.shimmer.clear_shader(); - } -} +use super::{Coordinates, Rectangle, VirtualCoordinates}; #[derive(Clone, Copy, Debug)] enum TestShader { @@ -128,7 +48,7 @@ impl Shader for TestShader { Self::Blue => RGB8::new(0, 0, 255), Self::White => RGB8::new(255, 255, 255), Self::RGB => RGB8::new(coords.x, coords.y, 255 - (coords.x / 2 + coords.y / 2)), - Self::HSV => Hsv::new_srgb(coords.x, coords.y, 255).into_rgb8(), + Self::HSV => Hsv::new(coords.x, coords.y, 255).into_rgb8(), Self::Outline => match (coords.x, coords.y) { (0, 0) => RGB8::new(255, 255, 255), (0, _) => RGB8::new(255, 0, 0), @@ -168,11 +88,13 @@ impl TestPattern { } } +//const Animations: Namespace = Namespace("animations"); + impl Task for TestPattern { fn name(&self) -> &'static str { "TestPattern" } fn start(&mut self, _bus: &mut EventBus) { - self.surface.set_shader(Box::new(self.pattern)); + self.surface.set_shader(self.pattern); } fn on_tick(&mut self, bus: &mut EventBus) { @@ -180,8 +102,8 @@ impl Task for TestPattern { self.pattern = self.pattern.next(); log::info!("Test pattern: {:?}", self.pattern); self.frame = 0; - self.surface.set_shader(Box::new(self.pattern)); - bus.push(Event::new_property_change("animations.test.pattern", format!("{:?}", self.pattern))); + self.surface.set_shader(self.pattern); + //bus.push(Animations.new_property_change( "test.pattern", format!("{:?}", self.pattern))); }); self.stepper.run(|| { self.frame = self.frame.wrapping_add(1); @@ -203,4 +125,4 @@ impl Task for TestPattern { fn stop(&mut self, _bus: &mut EventBus) { self.surface.clear_shader(); } -} +} \ No newline at end of file diff --git a/src/events.rs b/src/events.rs index 5ac1812..c6e2c08 100644 --- a/src/events.rs +++ b/src/events.rs @@ -1,57 +1,7 @@ -use core::fmt::{Debug, Display}; -use std::{collections::{LinkedList, VecDeque}, sync::{Arc, Mutex}}; +use core::fmt::Debug; +use std::{collections::VecDeque, fmt::{Display, Result}, sync::{Arc, Mutex}}; -use rgb::Rgb; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum Variant { - SignedByte(i8), - Byte(u8), - UInt(u32), - Int(i32), - BigUInt(u64), - BigInt(i64), - Boolean(bool), - String(String), - RGB(Rgb), - Vec(Vec) -} - -macro_rules! impl_variant_type { - ($type:ty, $var_type:tt) => { - impl From<$type> for Variant { - fn from(value: $type) -> Self { - Variant::$var_type(value) - } - } - - impl Into<$type> for Variant { - fn into(self) -> $type { - match self { - Variant::$var_type(value) => value, - _ => panic!(concat!("Expected Variant::", stringify!($var_type), "but got {:?}"), self) - } - } - } - }; -} - -impl_variant_type!(u8, Byte); -impl_variant_type!(i8, SignedByte); -impl_variant_type!(u32, UInt); -impl_variant_type!(i32, Int); -impl_variant_type!(i64, BigInt); -impl_variant_type!(u64, BigUInt); -impl_variant_type!(bool, Boolean); -impl_variant_type!(String, String); -impl_variant_type!(Rgb, RGB); -impl_variant_type!(Vec, Vec); - -impl<'a> From<&'a str> for Variant { - fn from(value: &'a str) -> Self { - Variant::String(value.to_owned()) - } -} +use crate::{properties::{Properties, PropertyID, Variant}, property_namespace}; #[derive(Debug, Clone, PartialEq, Eq)] pub enum Event { @@ -59,7 +9,19 @@ pub enum Event { Tick, StartThing(&'static str), StopThing(&'static str), - PropertyChange(&'static str, Variant) + PropertyChange(PropertyID, Variant) +} + +impl Display for Event { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result { + match self { + Self::ReadyToRock => f.write_str("ReadyToRock"), + Self::Tick => f.write_str("Tick"), + Self::StartThing(name) => write!(f, "Start {}", *name), + Self::StopThing(name) => write!(f, "Stop {}", *name), + Self::PropertyChange(id, value) => write!(f, "{} -> {:?}", id, value) + } + } } impl Event { @@ -67,7 +29,7 @@ impl Event { Event::Tick } - pub fn new_property_change(key: &'static str, data: T) -> Self where Variant: From { + pub fn new_property_change(key: PropertyID, data: T) -> Self where Variant: From { Event::PropertyChange(key, Variant::from(data)) } @@ -84,79 +46,6 @@ impl Event { } } -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Property { - key: &'static str, - value: Variant -} - -impl Property { - pub const fn new(key: &'static str, value: Variant) -> Self { - Property { - key, - value: value - } - } -} - -#[derive(Debug, Clone)] -pub struct Properties { - contents: LinkedList -} - -impl Properties { - fn new() -> Self { - Properties { - contents: LinkedList::new() - } - } - - fn get_key(&self, key: &'static str) -> Option<&Property> { - for next in self.contents.iter() { - if next.key == key { - return Some(next); - } - } - - return None; - } - - fn get_key_mut(&mut self, key: &'static str) -> Option<&mut Property> { - for next in self.contents.iter_mut() { - if next.key == key { - return Some(next); - } - } - - return None; - } - - pub fn get(&self, key: &'static str) -> Option { - match self.get_key(key) { - None => None, - Some(v) => Some(v.value.clone()) - } - } - - pub fn set(&mut self, key: &'static str, value: V) -> bool where Variant: From { - let as_variant: Variant = value.into(); - match self.get_key_mut(key) { - None => { - self.contents.push_front(Property::new(key, as_variant)); - return true - }, - Some(found_key) => { - if found_key.value != as_variant { - found_key.value = as_variant; - return true - } - } - } - - return false - } -} - #[derive(Clone)] pub struct EventBus { pending: Arc>>, @@ -177,7 +66,7 @@ impl EventBus { match next { Event::PropertyChange(key, value) => { if self.props.lock().unwrap().set(key, value.clone()) { - log::trace!("prop-update key={} value={:?}", key, value); + log::trace!("prop-update key={:?} value={:?}", key, value); Event::PropertyChange(key, value) } else { Event::new_tick() @@ -191,7 +80,19 @@ impl EventBus { self.pending.lock().unwrap().push_back(event); } - pub fn property(&self, key: &'static str) -> Option { - self.props.lock().unwrap().get(key) + pub fn properties(&self) -> std::sync::MutexGuard<'_, Properties> { + self.props.lock().unwrap() } -} \ No newline at end of file + + pub fn set_property, V: Into>(&mut self, key: K, value: V) { + self.push(Event::new_property_change(key.into(), value.into())); + } +} + +property_namespace!( + System, + NetworkOnline => false, + IP => "", + Gateway => "", + TimeSync => false +); \ No newline at end of file diff --git a/src/inputs/circadian.rs b/src/inputs/circadian.rs new file mode 100644 index 0000000..9ba2bf6 --- /dev/null +++ b/src/inputs/circadian.rs @@ -0,0 +1,103 @@ +use chrono::{DateTime, Timelike, Utc}; + +use crate::{events::{Event, EventBus}, lib8::interpolate::lerp8by8, prop_id, properties::{PropertyID, Variant}, render::props::Output as OutputNS, task::Task, time::Periodically}; +use crate::events::System as SystemNS; +use paste::paste; + +#[derive(Debug, Clone)] +struct ScheduleEntry { + hour: u8, + brightness: u8 +} + +pub struct CircadianRhythm { + time_check: Periodically, + schedule: [ScheduleEntry;10] +} + +impl CircadianRhythm { + pub fn new() -> Self { + CircadianRhythm { + time_check: Periodically::new_every_n_seconds(60), + schedule: [ + ScheduleEntry { hour: 0, brightness: 0 }, + ScheduleEntry { hour: 5, brightness: 0 }, + ScheduleEntry { hour: 6, brightness: 10 }, + ScheduleEntry { hour: 7, brightness: 20 }, + ScheduleEntry { hour: 8, brightness: 80 }, + ScheduleEntry { hour: 11, brightness: 120 }, + ScheduleEntry { hour: 18, brightness: 200 }, + ScheduleEntry { hour: 19, brightness: 255 }, + ScheduleEntry { hour: 22, brightness: 120 }, + ScheduleEntry { hour: 23, brightness: 5 } + ] + } + } + + fn update_brightness(&self, bus: &mut EventBus) { + let now: DateTime = std::time::SystemTime::now().into(); + let next_brightness = self.brightness_for_time(now.hour() as u8, now.minute() as u8); + bus.set_property(OutputNS::Brightness, next_brightness); + } + + fn brightness_for_time(&self, hour: u8, minute: u8) -> u8 { + let mut start = self.schedule.last().unwrap(); + let mut end = self.schedule.first().unwrap(); + for cur in self.schedule.iter() { + if cur.hour <= hour { + start = cur; + } else { + end = cur; + break; + } + } + + log::info!("hour={:?} minute={:?} start={:?} end={:?}", hour, minute, start, end); + + let mut adjusted_end = end.clone(); + if start.hour > end.hour { + adjusted_end.hour += 24; + } + + let start_time = (start.hour as u16).wrapping_mul(60); + let end_time = (end.hour as u16).wrapping_mul(60); + let now_time = (hour as u16).wrapping_mul(60).wrapping_add(minute as u16); + + let duration = end_time - start_time; + let cur_duration = now_time - start_time; + + let frac = map_range(cur_duration.into(), 0, duration.into(), 0, 255) as u8; + + lerp8by8(start.brightness, end.brightness, frac) + } +} + +fn map_range(x: u16, in_min: u16, in_max: u16, out_min: u16, out_max: u16) -> u16 { + let run = in_max - in_min; + if run == 0 { + return 0; + } + let rise = out_max - out_min; + let delta = x - in_min; + return (delta.wrapping_mul(rise)) / run + out_min; +} + + +impl Task for CircadianRhythm { + fn on_ready(&mut self, bus: &mut EventBus) { + self.update_brightness(bus); + } + + fn on_property_change(&mut self, key: PropertyID, value: &Variant, bus: &mut EventBus) { + match (key, value) { + (prop_id!(SystemNS::TimeSync), Variant::Boolean(true)) => self.update_brightness(bus), + _ => () + } + } + + fn on_tick(&mut self, bus: &mut EventBus) { + if self.time_check.tick() { + self.update_brightness(bus); + } + } +} diff --git a/src/inputs/mod.rs b/src/inputs/mod.rs new file mode 100644 index 0000000..87fdfc2 --- /dev/null +++ b/src/inputs/mod.rs @@ -0,0 +1 @@ +pub mod circadian; \ No newline at end of file diff --git a/src/lib8/mod.rs b/src/lib8/mod.rs index 81a7a28..b4ba701 100644 --- a/src/lib8/mod.rs +++ b/src/lib8/mod.rs @@ -2,8 +2,6 @@ pub mod interpolate; pub mod noise; pub mod trig; -use palette::encoding::srgb::Srgb; - use rgb::Rgb; use crate::lib8::interpolate::scale8; @@ -18,7 +16,23 @@ impl IntoRgb8 for Rgb { } } -impl IntoRgb8 for palette::Hsv { +pub struct Hsv { + pub hue: u8, + pub saturation: u8, + pub value: u8 +} + +impl Hsv { + pub fn new(hue: u8, saturation: u8, value: u8) -> Self { + Hsv { + hue, + saturation, + value + } + } +} + +impl IntoRgb8 for Hsv { //TODO: Borrowed from FastLED fn into_rgb8(self) -> Rgb { const HSV_SECTION_3: u8 = 0x40; @@ -27,7 +41,7 @@ impl IntoRgb8 for palette::Hsv { return Rgb::new(self.value, self.value, self.value) } - let mock_hue = scale8(191, self.hue.into_inner()); + let mock_hue = scale8(191, self.hue); let value: u8 = self.value; let saturation: u8 = self.saturation; let invsat: u8 = 255 - saturation; @@ -52,4 +66,4 @@ impl IntoRgb8 for palette::Hsv { _ => Rgb::new(rampup_adj_with_floor, brightness_floor, rampdown_adj_with_floor) } } -} +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index cbc4d6b..5d1882e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,18 +8,23 @@ mod platform; mod animations; mod mappings; mod buffers; -mod events; mod scenes; +mod inputs; +mod events; +mod properties; -use events::Event; -use rgb::Rgb; +use events::*; +use inputs::circadian::CircadianRhythm; +use platform::esp32::mqtt::props::MQTT; +use render::props::Output; use scenes::Sequencer; use crate::events::EventBus; -use crate::platform::{DefaultBoard, Board}; +use crate::platform::{DefaultBoard, Board, props::Board as BoardNS}; use crate::task::{FixedSizeScheduler, Scheduler}; use crate::render::{Surfaces, Renderer}; use crate::geometry::Rectangle; +use crate::scenes::props::Scenes as SceneNS; fn main() { let mut board: DefaultBoard = Board::take(); @@ -43,10 +48,11 @@ fn main() { log::info!("🌌 Creating animations"); let mut animations = FixedSizeScheduler::new([ Box::new(animations::IdleTask::new(&mut surfaces)), - Box::new(animations::TestPattern::new(surfaces.new_surface(Rectangle::everything()).unwrap())), + Box::new(animations::test::TestPattern::new(surfaces.new_surface(Rectangle::everything()).unwrap())), ]); let mut inputs = FixedSizeScheduler::new([ + Box::new(CircadianRhythm::new()), Box::new(Sequencer::new()), ]); @@ -55,18 +61,27 @@ fn main() { log::info!("🚌 Starting event bus"); let mut bus = EventBus::new(); - bus.push(Event::new_property_change("colors.primary", Rgb::new(255, 128, 128))); - bus.push(Event::new_property_change("system.board.chip_id", DefaultBoard::chip_id())); - bus.push(Event::new_property_change("system.network.online", false)); + { + let mut props = bus.properties(); + props.add_namespace::(); + props.add_namespace::(); + props.add_namespace::(); + props.add_namespace::(); + props.add_namespace::(); + + props.set(BoardNS::ChipID, DefaultBoard::chip_id()); + + log::info!("System properties:"); + log::info!("{}", props); + } log::info!("Priming events..."); let initial_tasks = [ "Renderer", "renderbug::scenes::Sequencer", - "renderbug::buffers::BufferedSurfacePool", - "renderbug::platform::esp32::WifiTask", - "renderbug::platform::esp32::MqttTask", - "renderbug::platform::esp32::CircadianRhythm" + "renderbug::platform::esp32::wifi::WifiTask", + "renderbug::platform::esp32::mqtt::MqttTask", + "renderbug::inputs::circadian::CircadianRhythm" ]; for task_name in initial_tasks { bus.push(Event::new_start_thing(task_name)); @@ -82,7 +97,7 @@ fn main() { Event::ReadyToRock => { log::info!("🚀 Ready to rock and roll"); } - _ => log::info!("⚡ Event: {:?}", next_event) + _ => log::info!("⚡ Event: {}", next_event) } inputs.tick(&next_event, &mut bus); animations.tick(&next_event, &mut bus); diff --git a/src/platform/esp32.rs b/src/platform/esp32.rs deleted file mode 100644 index 5b50c3e..0000000 --- a/src/platform/esp32.rs +++ /dev/null @@ -1,453 +0,0 @@ -use std::fmt::Debug; -use std::thread::JoinHandle; - -use chrono::DateTime; -use chrono::Timelike; -use chrono::Utc; - -use esp_idf_svc::eventloop::{EspSubscription, EspSystemEventLoop, System}; -use esp_idf_svc::hal::gpio::Pins; -use esp_idf_svc::hal::modem::Modem; -use esp_idf_svc::hal::prelude::Peripherals; -use esp_idf_svc::hal::rmt::RMT; -use esp_idf_svc::hal::task::thread::ThreadSpawnConfiguration; -use esp_idf_svc::mqtt::client::EspMqttClient; -use esp_idf_svc::mqtt::client::MqttClientConfiguration; -use esp_idf_svc::netif::IpEvent; -use esp_idf_svc::nvs::{EspDefaultNvsPartition, EspNvsPartition, NvsDefault}; -use esp_idf_svc::sntp::EspSntp; -use esp_idf_svc::sntp::SyncStatus; -use esp_idf_svc::sys::esp_efuse_mac_get_default; -use esp_idf_svc::wifi::{AuthMethod, ClientConfiguration, Configuration, EspWifi, WifiEvent}; -use rgb::Rgb; - -use super::smart_leds_lib::rmt::FastWs2812Esp32Rmt; -use super::smart_leds_lib::StrideOutput; -use super::Board; - -use crate::buffers::BufferedSurfacePool; -use crate::buffers::Pixbuf; -use crate::events::Event; -use crate::events::EventBus; -use crate::events::Variant; -use crate::lib8::interpolate::lerp8by8; -use crate::mappings::StrideMapping; -use crate::task::FixedSizeScheduler; -use crate::task::Task; -use crate::time::Periodically; - -pub struct Esp32Board { - sys_loop: EspSystemEventLoop, - modem: Option, - pins: Option, - rmt: Option, - surfaces: BufferedSurfacePool, -} - -impl Board for Esp32Board { - type Output = StrideOutput<[Rgb; 310], FastWs2812Esp32Rmt<'static>>; - type Surfaces = BufferedSurfacePool; - type Scheduler = FixedSizeScheduler<4>; - - fn chip_id() -> u64 { - let mut chip_id: [u8; 8] = [0; 8]; - unsafe { - esp_efuse_mac_get_default(&mut chip_id as *mut u8); - } - return u64::from_be_bytes(chip_id); - } - - fn take() -> Self { - // It is necessary to call this function once. Otherwise some patches to the runtime - // implemented by esp-idf-sys might not link properly. See https://github.com/esp-rs/esp-idf-template/issues/71 - esp_idf_svc::sys::link_patches(); - - // Bind the log crate to the ESP Logging facilities - esp_idf_svc::log::EspLogger::initialize_default(); - - let peripherals = Peripherals::take().unwrap(); - let sys_loop = EspSystemEventLoop::take().unwrap(); - - Esp32Board { - modem: Some(peripherals.modem), - sys_loop: sys_loop, - surfaces: BufferedSurfacePool::new(), - pins: Some(peripherals.pins), - rmt: Some(peripherals.rmt) - } - } - - fn output(&mut self) -> Self::Output { - - log::info!("Setting up output for chip ID {:x?}", Self::chip_id()); - - const POWER_VOLTS : u32 = 5; - const POWER_MA : u32 = 500; - const MAX_POWER_MW : u32 = POWER_VOLTS * POWER_MA; - - let pins = self.pins.take().unwrap(); - let rmt = self.rmt.take().unwrap(); - ThreadSpawnConfiguration { - pin_to_core: Some(esp_idf_svc::hal::cpu::Core::Core1), - ..Default::default() - }.set().unwrap(); - // Wifi driver creates too many interrupts on core0, so we need to use RMT on core1. - // But the implementation spawns a thread based on the core the driver was created in, - // so we create the driver in another thread briefly. - // Fun stuff. - let output = match Self::chip_id().to_be_bytes() { // panel test board - [72, 202, 67, 89, 145, 204, 0, 0] => { - StrideOutput::new( - Pixbuf::new(), - StrideMapping::new_panel(), - std::thread::spawn(move || { FastWs2812Esp32Rmt::new(rmt.channel0, pins.gpio5).unwrap() }).join().unwrap(), - MAX_POWER_MW - ) - }, - [0x8C, 0xAA, 0xB5, 0x83, 0x5f, 0x74, 0x0, 0x0] => { //ponderjar - StrideOutput::new( - Pixbuf::new(), - StrideMapping::new_jar(), - std::thread::spawn(move || { FastWs2812Esp32Rmt::new(rmt.channel0, pins.gpio14).unwrap() }).join().unwrap(), - MAX_POWER_MW - ) - }, - [0x4a, 0xca, 0x43, 0x59, 0x85, 0x58, 0x0, 0x0] => { // Albus the tree - StrideOutput::new( - Pixbuf::new(), - StrideMapping::new_albus(), - std::thread::spawn(move || { FastWs2812Esp32Rmt::new(rmt.channel0, pins.gpio5).unwrap() }).join().unwrap(), - MAX_POWER_MW - ) - }, - [0x48, 0xca, 0x43, 0x59, 0x9d, 0x48, 0x0, 0x0] => { // kitchen cabinets - StrideOutput::new( - Pixbuf::new(), - StrideMapping::new_fairylights(), - std::thread::spawn(move || { FastWs2812Esp32Rmt::new(rmt.channel0, pins.gpio5).unwrap() }).join().unwrap(), - MAX_POWER_MW - ) - }, - [0x48, 0xca, 0x43, 0x59, 0x9e, 0xdc, 0x0, 0x0] => { // front window - StrideOutput::new( - Pixbuf::new(), - StrideMapping::new_fairylights(), - std::thread::spawn(move || { FastWs2812Esp32Rmt::new(rmt.channel0, pins.gpio5).unwrap() }).join().unwrap(), - MAX_POWER_MW - ) - }, - [0xfc, 0xf5, 0xc4, 0x05, 0xb8, 0x30, 0x0, 0x0] => { // cyberplague - StrideOutput::new( - Pixbuf::new(), - StrideMapping::new_cyberplague(), - std::thread::spawn(move || { FastWs2812Esp32Rmt::new(rmt.channel0, pins.gpio13).unwrap() }).join().unwrap(), - MAX_POWER_MW - ) - }, - _ => { - StrideOutput::new( - Pixbuf::new(), - StrideMapping::new(), - std::thread::spawn(move || { FastWs2812Esp32Rmt::new(rmt.channel0, pins.gpio5).unwrap() }).join().unwrap(), - MAX_POWER_MW - ) - } - }; - ThreadSpawnConfiguration { - ..Default::default() - }.set().unwrap(); - - output - } - - fn surfaces(&mut self) -> Self::Surfaces { - self.surfaces.clone() - } - - fn system_tasks(&mut self) -> Self::Scheduler { - let nvs = EspDefaultNvsPartition::take().unwrap(); - FixedSizeScheduler::new([ - Box::new(WifiTask::new(self.modem.take().unwrap(), self.sys_loop.clone(), &nvs)), - Box::new(CircadianRhythm::new()), - Box::new(MqttTask::new()), - Box::new(self.surfaces.clone()) - ]) - } -} - -#[derive(Debug, Clone)] -struct ScheduleEntry { - hour: u8, - brightness: u8 -} - -struct CircadianRhythm { - time_check: Periodically, - schedule: [ScheduleEntry;10] -} - -impl CircadianRhythm { - fn new() -> Self { - CircadianRhythm { - time_check: Periodically::new_every_n_seconds(60), - schedule: [ - ScheduleEntry { hour: 0, brightness: 0 }, - ScheduleEntry { hour: 5, brightness: 0 }, - ScheduleEntry { hour: 6, brightness: 10 }, - ScheduleEntry { hour: 7, brightness: 20 }, - ScheduleEntry { hour: 8, brightness: 80 }, - ScheduleEntry { hour: 11, brightness: 120 }, - ScheduleEntry { hour: 18, brightness: 200 }, - ScheduleEntry { hour: 19, brightness: 255 }, - ScheduleEntry { hour: 22, brightness: 120 }, - ScheduleEntry { hour: 23, brightness: 5 } - ] - } - } - - fn update_brightness(&self, bus: &mut EventBus) { - let now: DateTime = std::time::SystemTime::now().into(); - let next_brightness = self.brightness_for_time(now.hour() as u8, now.minute() as u8); - bus.push(Event::new_property_change("output.brightness", next_brightness)); - } - - fn brightness_for_time(&self, hour: u8, minute: u8) -> u8 { - let mut start = self.schedule.last().unwrap(); - let mut end = self.schedule.first().unwrap(); - for cur in self.schedule.iter() { - if cur.hour <= hour { - start = cur; - } else { - end = cur; - break; - } - } - - log::info!("hour={:?} minute={:?} start={:?} end={:?}", hour, minute, start, end); - - let mut adjusted_end = end.clone(); - if start.hour > end.hour { - adjusted_end.hour += 24; - } - - let start_time = (start.hour as u16).wrapping_mul(60); - let end_time = (end.hour as u16).wrapping_mul(60); - let now_time = (hour as u16).wrapping_mul(60).wrapping_add(minute as u16); - - let duration = end_time - start_time; - let cur_duration = now_time - start_time; - - let frac = map_range(cur_duration.into(), 0, duration.into(), 0, 255) as u8; - - lerp8by8(start.brightness, end.brightness, frac) - } -} - -fn map_range(x: u16, in_min: u16, in_max: u16, out_min: u16, out_max: u16) -> u16 { - let run = in_max - in_min; - if run == 0 { - return 0; - } - let rise = out_max - out_min; - let delta = x - in_min; - return (delta * rise) / run + out_min; -} - - -impl Task for CircadianRhythm { - fn on_ready(&mut self, bus: &mut EventBus) { - self.update_brightness(bus); - } - - fn on_property_change(&mut self, key: &'static str, value: &Variant, bus: &mut EventBus) { - match (key, value) { - ("system.time.synchronized", Variant::Boolean(true)) => self.update_brightness(bus), - _ => () - } - } - - fn on_tick(&mut self, bus: &mut EventBus) { - if self.time_check.tick() { - self.update_brightness(bus); - } - } -} - -struct MqttTask { - client: Option>, - conn_thread: Option>, -} - -impl MqttTask { - fn new() -> Self { - MqttTask { - conn_thread: None, - client: None, - } - } - - fn start_mqtt(&mut self, bus: &EventBus) { - log::info!("Starting MQTT"); - let chip_id: u64 = bus.property("system.board.chip_id").unwrap().into(); - let (client, mut conn) = EspMqttClient::new( - "mqtt://10.0.0.2:1883", - &MqttClientConfiguration { - client_id: Some(&format!("{:X}", chip_id)), - ..Default::default() - } - ).unwrap(); - log::info!("Connected!"); - - self.conn_thread = Some(std::thread::Builder::new() - .stack_size(6000) - .spawn(move || { - conn.next().unwrap(); - }).unwrap()); - self.client = Some(client); - } - - fn topic_prefix(bus: &EventBus) -> String { - let chip_id: u64 = bus.property("system.board.chip_id").unwrap().into(); - - format!("homeassistant-test/renderbug/{:X}", chip_id) - } -} - -impl Task for MqttTask { - fn start(&mut self, bus: &mut EventBus) { - bus.push(Event::new_property_change("mqtt.online", false)); - } - - fn on_property_change(&mut self, key: &'static str, value: &Variant, bus: &mut EventBus) { - match (key, value) { - ("system.network.online", Variant::Boolean(true)) => { - log::info!("Registering with MQTT"); - - self.start_mqtt(bus); - - if let Some(ref mut client) = self.client { - client.enqueue( - Self::topic_prefix(bus).as_str(), - esp_idf_svc::mqtt::client::QoS::AtLeastOnce, - false, - "hello, world".as_bytes() - ).unwrap(); - log::info!("MQTT should be online!"); - - bus.push(Event::new_property_change("mqtt.online", true)); - } - }, - (name, value) => { - if let Some(ref mut client) = self.client { - let prefix = Self::topic_prefix(bus); - - client.enqueue( - format!("{}/properties/{}", prefix, name).as_str(), - esp_idf_svc::mqtt::client::QoS::AtLeastOnce, - false, - format!("{}", value).as_bytes() - ).unwrap(); - } - } - } - } -} - -impl Debug for WifiTask { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("WifiTask").finish() - } -} - -struct WifiTask { - wifi: EspWifi<'static>, - ntp: EspSntp<'static>, - connection_check: Periodically, - sys_loop: EspSystemEventLoop, - wifi_sub: Option>, - ip_sub: Option>, -} - -impl WifiTask { - fn new(modem: Modem, sys_loop: EspSystemEventLoop, nvs: &EspNvsPartition) -> Self { - log::info!("Installing wifi driver"); - let wifi = EspWifi::new( - modem, - sys_loop.clone(), - Some(nvs.clone()) - ).unwrap(); - - WifiTask { - wifi, - ntp: EspSntp::new_default().unwrap(), - connection_check: Periodically::new_every_n_seconds(1), - sys_loop, - wifi_sub: None, - ip_sub: None, - } - } - - fn connect(&mut self) { - log::info!("Connecting wifi"); - let wifi_config = Configuration::Client(ClientConfiguration { - ssid: "The Frequency".try_into().unwrap(), - bssid: None, - auth_method: AuthMethod::WPA2Personal, - password: "thepasswordkenneth".try_into().unwrap(), - channel: None, - ..Default::default() - }); - self.wifi.set_configuration(&wifi_config).unwrap(); - self.wifi.start().unwrap(); - self.wifi.connect().unwrap(); - } - - fn disconnect(&mut self) { - log::info!("Disconnecting wifi"); - self.wifi.disconnect().unwrap(); - self.wifi.stop().unwrap(); - } -} - -impl Task for WifiTask { - fn start(&mut self, bus: &mut EventBus) { - log::info!("Starting wifi!"); - let mut wifi_bus = bus.clone(); - self.wifi_sub = Some(self.sys_loop.subscribe::( move |evt| { - log::debug!("wifi event {:?}", evt); - wifi_bus.push(Event::new_property_change("system.network.online", false)); - }).unwrap()); - let mut ip_bus = bus.clone(); - self.ip_sub = Some(self.sys_loop.subscribe::(move |evt| { - log::debug!("ip event {:?}", evt); - match evt { - IpEvent::DhcpIpAssigned(addr) => { - ip_bus.push(Event::new_property_change("system.network.ip", addr.ip().to_string())); - ip_bus.push(Event::new_property_change("system.network.gateway", addr.gateway().to_string())); - ip_bus.push(Event::new_property_change("system.network.online", true)); - }, - _ => () - } - }).unwrap()); - self.connect(); - } - - fn on_tick(&mut self, bus: &mut EventBus) { - if self.connection_check.tick() { - if bus.property("system.network.online").unwrap() == Variant::Boolean(true) { - match self.ntp.get_sync_status() { - SyncStatus::Completed => bus.push(Event::new_property_change("system.time.synchronized", true)), - _ => bus.push(Event::new_property_change("system.time.synchronized", false)) - } - } - } - } - - fn stop(&mut self, bus: &mut EventBus) { - log::info!("Stopping wifi"); - self.wifi_sub.take().unwrap(); - self.ip_sub.take().unwrap(); - self.disconnect(); - bus.push(Event::new_property_change("system.network.online", false)); - } -} diff --git a/src/platform/esp32/board.rs b/src/platform/esp32/board.rs new file mode 100644 index 0000000..f2e61d4 --- /dev/null +++ b/src/platform/esp32/board.rs @@ -0,0 +1,143 @@ +use esp_idf_svc::{eventloop::EspSystemEventLoop, hal::{gpio::Pins, modem::Modem, prelude::Peripherals, rmt::RMT, task::thread::ThreadSpawnConfiguration}, nvs::EspDefaultNvsPartition, sys::esp_efuse_mac_get_default}; +use rgb::Rgb; + +use crate::{buffers::{BufferedSurfacePool, Pixbuf}, mappings::StrideMapping, platform::{smart_leds_lib::{rmt::FastWs2812Esp32Rmt, StrideOutput}, Board}, task::FixedSizeScheduler}; + +use super::{mqtt::MqttTask, wifi::WifiTask}; + +pub struct Esp32Board { + sys_loop: EspSystemEventLoop, + modem: Option, + pins: Option, + rmt: Option, + surfaces: Option, +} + +impl Board for Esp32Board { + type Output = StrideOutput<[Rgb; 310], FastWs2812Esp32Rmt<'static>>; + type Surfaces = BufferedSurfacePool; + type Scheduler = FixedSizeScheduler<2>; + + fn chip_id() -> u64 { + let mut chip_id: [u8; 8] = [0; 8]; + unsafe { + esp_efuse_mac_get_default(&mut chip_id as *mut u8); + } + return u64::from_be_bytes(chip_id); + } + + fn take() -> Self { + // It is necessary to call this function once. Otherwise some patches to the runtime + // implemented by esp-idf-sys might not link properly. See https://github.com/esp-rs/esp-idf-template/issues/71 + esp_idf_svc::sys::link_patches(); + + // Bind the log crate to the ESP Logging facilities + esp_idf_svc::log::EspLogger::initialize_default(); + + let peripherals = Peripherals::take().unwrap(); + let sys_loop = EspSystemEventLoop::take().unwrap(); + + Esp32Board { + modem: Some(peripherals.modem), + sys_loop: sys_loop, + surfaces: Some(BufferedSurfacePool::new()), + pins: Some(peripherals.pins), + rmt: Some(peripherals.rmt) + } + } + + fn output(&mut self) -> Self::Output { + + log::info!("Setting up output for chip ID {:x?}", Self::chip_id()); + + const POWER_VOLTS : u32 = 5; + const POWER_MA : u32 = 500; + const MAX_POWER_MW : u32 = POWER_VOLTS * POWER_MA; + + let pins = self.pins.take().unwrap(); + let rmt = self.rmt.take().unwrap(); + ThreadSpawnConfiguration { + pin_to_core: Some(esp_idf_svc::hal::cpu::Core::Core1), + ..Default::default() + }.set().unwrap(); + // Wifi driver creates too many interrupts on core0, so we need to use RMT on core1. + // But the implementation spawns a thread based on the core the driver was created in, + // so we create the driver in another thread briefly. + // Fun stuff. + let output = match Self::chip_id().to_be_bytes() { // panel test board + [72, 202, 67, 89, 145, 204, 0, 0] => { + StrideOutput::new( + Pixbuf::new(), + StrideMapping::new_panel(), + std::thread::spawn(move || { FastWs2812Esp32Rmt::new(rmt.channel0, pins.gpio5).unwrap() }).join().unwrap(), + MAX_POWER_MW + ) + }, + [0x8C, 0xAA, 0xB5, 0x83, 0x5f, 0x74, 0x0, 0x0] => { //ponderjar + StrideOutput::new( + Pixbuf::new(), + StrideMapping::new_jar(), + std::thread::spawn(move || { FastWs2812Esp32Rmt::new(rmt.channel0, pins.gpio14).unwrap() }).join().unwrap(), + MAX_POWER_MW + ) + }, + [0x4a, 0xca, 0x43, 0x59, 0x85, 0x58, 0x0, 0x0] => { // Albus the tree + StrideOutput::new( + Pixbuf::new(), + StrideMapping::new_albus(), + std::thread::spawn(move || { FastWs2812Esp32Rmt::new(rmt.channel0, pins.gpio5).unwrap() }).join().unwrap(), + POWER_VOLTS * 2_400 + ) + }, + [0x48, 0xca, 0x43, 0x59, 0x9d, 0x48, 0x0, 0x0] => { // kitchen cabinets + StrideOutput::new( + Pixbuf::new(), + StrideMapping::new_fairylights(), + std::thread::spawn(move || { FastWs2812Esp32Rmt::new(rmt.channel0, pins.gpio5).unwrap() }).join().unwrap(), + MAX_POWER_MW + ) + }, + [0x48, 0xca, 0x43, 0x59, 0x9e, 0xdc, 0x0, 0x0] => { // front window + StrideOutput::new( + Pixbuf::new(), + StrideMapping::new_fairylights(), + std::thread::spawn(move || { FastWs2812Esp32Rmt::new(rmt.channel0, pins.gpio5).unwrap() }).join().unwrap(), + MAX_POWER_MW + ) + }, + [0xfc, 0xf5, 0xc4, 0x05, 0xb8, 0x30, 0x0, 0x0] => { // cyberplague + StrideOutput::new( + Pixbuf::new(), + StrideMapping::new_cyberplague(), + std::thread::spawn(move || { FastWs2812Esp32Rmt::new(rmt.channel0, pins.gpio13).unwrap() }).join().unwrap(), + MAX_POWER_MW + ) + }, + _ => { + StrideOutput::new( + Pixbuf::new(), + StrideMapping::new(), + std::thread::spawn(move || { FastWs2812Esp32Rmt::new(rmt.channel0, pins.gpio5).unwrap() }).join().unwrap(), + MAX_POWER_MW + ) + } + }; + ThreadSpawnConfiguration { + ..Default::default() + }.set().unwrap(); + + output + } + + fn surfaces(&mut self) -> Self::Surfaces { + self.surfaces.take().unwrap() + } + + fn system_tasks(&mut self) -> Self::Scheduler { + let nvs = EspDefaultNvsPartition::take().unwrap(); + FixedSizeScheduler::new([ + Box::new(WifiTask::new(self.modem.take().unwrap(), self.sys_loop.clone(), &nvs)), + Box::new(MqttTask::new()) + ]) + } +} \ No newline at end of file diff --git a/src/platform/esp32/mod.rs b/src/platform/esp32/mod.rs new file mode 100644 index 0000000..00cbccc --- /dev/null +++ b/src/platform/esp32/mod.rs @@ -0,0 +1,3 @@ +pub mod board; +pub mod wifi; +pub mod mqtt; \ No newline at end of file diff --git a/src/platform/esp32/mqtt.rs b/src/platform/esp32/mqtt.rs new file mode 100644 index 0000000..800dc0c --- /dev/null +++ b/src/platform/esp32/mqtt.rs @@ -0,0 +1,186 @@ +use std::{collections::LinkedList, thread::JoinHandle}; + +use esp_idf_svc::mqtt::client::{EspMqttClient, MqttClientConfiguration}; +use serde_json::{json, Value}; + +use crate::{events::{Event, EventBus, System as SystemNS}, prop_id, properties::{PropertyID, Variant}, render::props::Output as OutputNS, scenes::props::Scenes as SceneNS, task::Task}; +use crate::platform::props::Board as BoardNS; +use paste::paste; + +struct HADevice { + prefix: String, + unique_id: String +} + +impl HADevice { + fn new(component: &str, chip_id: u64, name: &str) -> Self { + HADevice { + // eg: homeassistant/sensor/0BADCOFFEE/fps + unique_id: format!("{:X}-{}", chip_id, name), + prefix: format!("homeassistant/{}/renderbug-rs/{:X}-{}", component, chip_id, name) + } + } + + fn topic(&self, name: &str) -> String { + format!("{}/{}", self.prefix, name) + } + + fn registration(&self) -> Value { + json!({ + "~": self.prefix, + "stat_t": "~/state", + "unique_id": self.unique_id, + "dev": { + "name": "Renderbug-rs ESP32", + "mdl": "Renderbug-rs ESP32", + "sw": "", + "mf": "Phong Robotics", + "ids": [self.unique_id] + } + }) + } +} + +struct HAScene { + prefix: String, + unique_id: String +} + +impl HAScene { + fn new(chip_id: u64, name: &str) -> Self { + HAScene { + // eg: homeassistant/sensor/0BADCOFFEE/fps + unique_id: format!("{:X}-{}", chip_id, name), + prefix: format!("homeassistant/scene/renderbug-rs/{:X}-{}", chip_id, name) + } + } + + fn topic(&self, name: &str) -> String { + format!("{}/{}", self.prefix, name) + } + + fn registration(&self) -> Value { + json!({ + "~": self.prefix, + "stat_t": "~/state", + "cmd_t": "~/command", + "unique_id": self.unique_id, + "payload_on": "on", + "dev": { + "name": "Renderbug-rs ESP32", + "mdl": "Renderbug-rs ESP32", + "sw": "", + "mf": "Phong Robotics", + "ids": [self.unique_id] + } + }) + } +} + +pub struct MqttTask { + client: Option>, + conn_thread: Option>, + fps_sensor: Option +} + +impl MqttTask { + pub fn new() -> Self { + MqttTask { + conn_thread: None, + client: None, + fps_sensor: None + } + } + + fn start_mqtt(&mut self, bus: &EventBus) { + log::info!("Starting MQTT"); + let chip_id: u64 = bus.properties().get(BoardNS::ChipID).unwrap().into(); + + self.fps_sensor = Some(HADevice::new("sensor", chip_id, "output-fps")); + + let (client, mut conn) = EspMqttClient::new( + "mqtt://10.0.0.2:1883", + &MqttClientConfiguration { + client_id: Some(&format!("{:X}", chip_id)), + ..Default::default() + } + ).unwrap(); + log::info!("Connected!"); + + self.conn_thread = Some(std::thread::Builder::new() + .stack_size(6000) + .spawn(move || { + conn.next().unwrap(); + }).unwrap()); + self.client = Some(client); + } +} + +impl Task for MqttTask { + fn start(&mut self, bus: &mut EventBus) { + bus.set_property(props::MQTT::Online, false); + + let chip_id = bus.properties().get(BoardNS::ChipID).unwrap().into(); + self.fps_sensor = Some(HADevice::new("sensor", chip_id, "fps")); + } + + fn on_property_change(&mut self, key: PropertyID, value: &Variant, bus: &mut EventBus) { + match (key, value) { + (prop_id!(SystemNS::NetworkOnline), Variant::Boolean(true)) => { + log::info!("Registering with MQTT"); + + let chip_id = bus.properties().get(BoardNS::ChipID).unwrap().into(); + + self.start_mqtt(bus); + + if let Some(ref mut client) = self.client { + if let Some(ref sensor) = self.fps_sensor { + client.enqueue( + &sensor.topic("config"), + esp_idf_svc::mqtt::client::QoS::AtLeastOnce, + false, + sensor.registration().to_string().as_bytes() + ).unwrap(); + } + + let scenes: Vec = bus.properties().get(SceneNS::All).unwrap().into(); + for scene in scenes.iter() { + let scene_name: String = scene.clone().into(); + let scene_device = HAScene::new(chip_id, scene_name.as_str()); + client.enqueue( + &scene_device.topic("config"), + esp_idf_svc::mqtt::client::QoS::AtLeastOnce, + false, + scene_device.registration().to_string().as_bytes() + ).unwrap(); + } + log::info!("MQTT should be online!"); + + bus.set_property(props::MQTT::Online, true); + } + }, + (prop_id!(OutputNS::FPS), Variant::UInt(fps)) => { + if let Some(ref mut client) = self.client { + if let Some(ref sensor) = self.fps_sensor { + client.enqueue( + &sensor.topic("state"), + esp_idf_svc::mqtt::client::QoS::AtLeastOnce, + false, + json!(fps).to_string().as_bytes() + ).unwrap(); + } + } + }, + _ => () + } + } +} + +pub mod props { + use crate::property_namespace; + + property_namespace!( + MQTT, + Online => true + ); +} \ No newline at end of file diff --git a/src/platform/esp32/wifi.rs b/src/platform/esp32/wifi.rs new file mode 100644 index 0000000..64ae1f3 --- /dev/null +++ b/src/platform/esp32/wifi.rs @@ -0,0 +1,104 @@ +use esp_idf_svc::{eventloop::{EspSubscription, EspSystemEventLoop, System}, hal::modem::Modem, netif::IpEvent, nvs::{EspNvsPartition, NvsDefault}, sntp::{EspSntp, SyncStatus}, wifi::{AuthMethod, ClientConfiguration, Configuration, EspWifi, WifiEvent}}; + +use crate::{events::{EventBus, System as SystemNS}, properties::Variant, task::Task, time::Periodically}; + +use std::fmt::Debug; + +pub struct WifiTask { + wifi: EspWifi<'static>, + ntp: EspSntp<'static>, + connection_check: Periodically, + sys_loop: EspSystemEventLoop, + wifi_sub: Option>, + ip_sub: Option>, +} + +impl Debug for WifiTask { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("WifiTask").finish() + } +} + +impl WifiTask { + pub fn new(modem: Modem, sys_loop: EspSystemEventLoop, nvs: &EspNvsPartition) -> Self { + log::info!("Installing wifi driver"); + let wifi = EspWifi::new( + modem, + sys_loop.clone(), + Some(nvs.clone()) + ).unwrap(); + + WifiTask { + wifi, + ntp: EspSntp::new_default().unwrap(), + connection_check: Periodically::new_every_n_seconds(1), + sys_loop, + wifi_sub: None, + ip_sub: None, + } + } + + fn connect(&mut self) { + log::info!("Connecting wifi"); + let wifi_config = Configuration::Client(ClientConfiguration { + ssid: "The Frequency".try_into().unwrap(), + bssid: None, + auth_method: AuthMethod::WPA2Personal, + password: "thepasswordkenneth".try_into().unwrap(), + channel: None, + ..Default::default() + }); + self.wifi.set_configuration(&wifi_config).unwrap(); + self.wifi.start().unwrap(); + self.wifi.connect().unwrap(); + } + + fn disconnect(&mut self) { + log::info!("Disconnecting wifi"); + self.wifi.disconnect().unwrap(); + self.wifi.stop().unwrap(); + } +} + +impl Task for WifiTask { + fn start(&mut self, bus: &mut EventBus) { + log::info!("Starting wifi!"); + let mut wifi_bus = bus.clone(); + self.wifi_sub = Some(self.sys_loop.subscribe::( move |evt| { + log::debug!("wifi event {:?}", evt); + wifi_bus.set_property(SystemNS::NetworkOnline,false); + }).unwrap()); + let mut ip_bus = bus.clone(); + self.ip_sub = Some(self.sys_loop.subscribe::(move |evt| { + log::debug!("ip event {:?}", evt); + match evt { + IpEvent::DhcpIpAssigned(addr) => { + ip_bus.set_property(SystemNS::IP, addr.ip().to_string()); + ip_bus.set_property(SystemNS::Gateway, addr.gateway().to_string()); + ip_bus.set_property(SystemNS::NetworkOnline, true); + }, + _ => () + } + }).unwrap()); + self.connect(); + } + + fn on_tick(&mut self, bus: &mut EventBus) { + if self.connection_check.tick() { + if bus.properties().get(SystemNS::NetworkOnline).unwrap() == Variant::Boolean(true) { + match self.ntp.get_sync_status() { + SyncStatus::Completed => bus.set_property(SystemNS::TimeSync, true), + _ => bus.set_property(SystemNS::TimeSync, false) + } + } + } + } + + fn stop(&mut self, bus: &mut EventBus) { + log::info!("Stopping wifi"); + self.wifi_sub.take().unwrap(); + self.ip_sub.take().unwrap(); + self.disconnect(); + bus.set_property(SystemNS::NetworkOnline, false); + } +} diff --git a/src/platform/mod.rs b/src/platform/mod.rs index af2413b..162be19 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -6,7 +6,7 @@ pub mod smart_leds_lib; pub mod esp32; -pub type DefaultBoard = esp32::Esp32Board; +pub type DefaultBoard = esp32::board::Esp32Board; use crate::render::{Output, Surfaces}; use crate::task::Scheduler; @@ -21,4 +21,13 @@ pub trait Board { fn surfaces(&mut self) -> Self::Surfaces; fn system_tasks(&mut self) -> Self::Scheduler; fn chip_id() -> u64; +} + +pub mod props { + use crate::property_namespace; + + property_namespace!( + Board, + ChipID => 0_u64 + ); } \ No newline at end of file diff --git a/src/platform/smart_leds_lib.rs b/src/platform/smart_leds_lib.rs index 7b5d326..d1602f6 100644 --- a/src/platform/smart_leds_lib.rs +++ b/src/platform/smart_leds_lib.rs @@ -1,11 +1,12 @@ use smart_leds_trait::SmartLedsWrite; use crate::buffers::Pixbuf; -use crate::events::Variant; -use crate::render::{HardwarePixel, Output, PixelView, Sample}; +use crate::properties::Variant; +use crate::render::{HardwarePixel, Output, PixelView, Sample, props::Output as OutputNS}; use crate::power::brightness_for_mw; -use crate::geometry::*; +use crate::{geometry::*, prop_id}; use crate::mappings::*; +use paste::paste; use core::fmt::Debug; @@ -57,7 +58,7 @@ impl, T: FastWrite> Output for StrideOutput { fn on_event(&mut self, event: &crate::events::Event) { match event { - crate::events::Event::PropertyChange("output.brightness", Variant::Byte(new_brightness)) => self.brightness = *new_brightness, + crate::events::Event::PropertyChange(prop_id!(OutputNS::Brightness), Variant::Byte(new_brightness)) => self.brightness = *new_brightness, _ => () } } diff --git a/src/properties.rs b/src/properties.rs new file mode 100644 index 0000000..e27f16e --- /dev/null +++ b/src/properties.rs @@ -0,0 +1,270 @@ +use std::fmt::Display; + +use rgb::Rgb; +pub use paste::paste; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Variant { + SignedByte(i8), + Byte(u8), + UInt(u32), + Int(i32), + BigUInt(u64), + BigInt(i64), + Boolean(bool), + String(String), + RGB(Rgb), + Vec(Vec) +} + +macro_rules! impl_variant_type { + ($type:ty, $var_type:tt) => { + impl From<$type> for Variant { + fn from(value: $type) -> Self { + Variant::$var_type(value) + } + } + + impl Into<$type> for Variant { + fn into(self) -> $type { + match self { + Variant::$var_type(value) => value, + _ => panic!(concat!("Expected Variant::", stringify!($var_type), "but got {:?}"), self) + } + } + } + }; +} + +impl_variant_type!(u8, Byte); +impl_variant_type!(i8, SignedByte); +impl_variant_type!(u32, UInt); +impl_variant_type!(i32, Int); +impl_variant_type!(i64, BigInt); +impl_variant_type!(u64, BigUInt); +impl_variant_type!(bool, Boolean); +impl_variant_type!(String, String); +impl_variant_type!(Rgb, RGB); +impl_variant_type!(Vec, Vec); + +impl<'a> From<&'a str> for Variant { + fn from(value: &'a str) -> Self { + Variant::String(value.to_owned()) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Property { + key: PropertyKey, + value: Variant +} + +impl Property { + pub const fn new(key: PropertyKey, value: Variant) -> Self { + Property { + key, + value, + } + } +} + +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub struct NamespaceKey(pub &'static str); + +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub struct PropertyKey(pub &'static str); + +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub struct PropertyID { + pub namespace: NamespaceKey, + pub key: PropertyKey +} + +impl Display for PropertyID { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}::{}", self.namespace.0, self.key.0) + } +} + +pub trait Namespace { + fn nskey() -> NamespaceKey; + fn properties() -> Vec; +} + +#[macro_export] +macro_rules! prop_id { + ($head:ident $(:: $tail:tt)+) => { + prop_id!( $head :: ; $($tail),* ) + }; + ($($namespace:ident ::)+ ; $key:ident ) => { + $crate::properties::PropertyID { + namespace: $($namespace ::)+ NAMESPACE_KEY, + key: paste! { $($namespace ::)+ [] } + } + }; + ($($namespace:ident ::)+ ; $head:ident , $($tail:ident),+) => { + prop_id!( $($namespace ::)* $head :: ; $($tail),* ) + } +} + +#[macro_export] +macro_rules! property_namespace { + ($name:ident, $($prop_name:ident => $value:expr),*) => { + pub enum $name { + $($prop_name),* + } + + use paste::paste; + + impl crate::properties::Namespace for $name { + fn nskey() -> $crate::properties::NamespaceKey { + Self::NAMESPACE_KEY + } + + fn properties() -> Vec<$crate::properties::Property> { + vec![ + $($crate::properties::Property::new(paste! {Self::[]}, $crate::properties::Variant::from($value)),)* + ] + } + } + + impl $name { + pub const NAMESPACE_KEY: $crate::properties::NamespaceKey = $crate::properties::NamespaceKey(stringify!($name)); + + $(paste! { pub const []: crate::properties::PropertyKey = $crate::properties::PropertyKey(stringify!($prop_name)); })* + + pub const fn key(&self) -> $crate::properties::PropertyKey { + match self { + $(Self::$prop_name => $crate::properties::PropertyKey(stringify!($prop_name))),* + } + } + + pub const fn id(&self) -> $crate::properties::PropertyID { + $crate::properties::PropertyID { namespace: Self::NAMESPACE_KEY, key: self.key() } + } + } + + impl Into<$crate::properties::PropertyID> for $name { + fn into(self) -> $crate::properties::PropertyID { + self.id() + } + } + }; +} + +#[derive(Debug, Clone)] +pub struct NamespaceProps { + props: Vec, + key: NamespaceKey +} + +impl NamespaceProps { + fn new() -> Self { + NamespaceProps { + props: NS::properties(), + key: NS::nskey() + } + } + + fn get_key(&self, key: &PropertyKey) -> Option<&Property> { + for next in self.props.iter() { + if next.key == *key { + return Some(next); + } + } + + log::warn!("Unknown key {:?}", key); + + return None; + } + + fn get_key_mut(&mut self, key: &PropertyKey) -> Option<&mut Property> { + for next in self.props.iter_mut() { + if next.key == *key { + return Some(next); + } + } + + log::warn!("Unknown key {:?}", key); + + return None; + } + +} + +#[derive(Debug, Clone)] +pub struct Properties { + contents: Vec +} + +impl Display for Properties { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for ns in self.contents.iter() { + write!(f, "{}\n", ns.key.0).unwrap(); + for prop in ns.props.iter() { + write!(f, "\t{} = {:?}\n", prop.key.0, prop.value).unwrap(); + } + } + Result::Ok(()) + } +} + +impl Properties { + pub fn new() -> Self { + Properties { + contents: Vec::new(), + } + } + + pub fn add_namespace(&mut self) { + self.contents.push(NamespaceProps::new::()); + } + + fn get_namespace(&self, ns: &NamespaceKey) -> Option<&NamespaceProps> { + for nsprops in self.contents.iter() { + if nsprops.key == *ns { + return Some(nsprops) + } + } + + log::warn!("Unknown namespace {:?}", ns); + + None + } + + fn get_namespace_mut(&mut self, ns: &NamespaceKey) -> Option<&mut NamespaceProps> { + for nsprops in self.contents.iter_mut() { + if nsprops.key == *ns { + return Some(nsprops) + } + } + + log::warn!("Unknown namespace {:?}", ns); + + None + } + + pub fn get>(&self, key: T) -> Option { + let as_id = key.into(); + match self.get_namespace(&as_id.namespace).unwrap().get_key(&as_id.key) { + None => None, + Some(v) => Some(v.value.clone()) + } + } + + pub fn set>(&mut self, key: T, value: V) -> bool where Variant: From { + let as_id = key.into(); + let as_variant: Variant = value.into(); + match self.get_namespace_mut(&as_id.namespace).unwrap().get_key_mut(&as_id.key) { + None => (), + Some(found_key) => { + if found_key.value != as_variant { + found_key.value = as_variant; + return true + } + } + } + + return false + } +} \ No newline at end of file diff --git a/src/render.rs b/src/render.rs index 727ddf9..6ea286e 100644 --- a/src/render.rs +++ b/src/render.rs @@ -1,6 +1,6 @@ use rgb::Rgb; - -use crate::events::{Event, EventBus}; +use crate::events::*; +use crate::properties::*; use crate::geometry::*; use crate::lib8::interpolate::Fract8Ops; use crate::power::AsMilliwatts; @@ -74,7 +74,7 @@ impl Renderer { impl Task for Renderer { fn name(&self) -> &'static str { "Renderer" } - fn on_property_change(&mut self, key: &'static str, value: &crate::events::Variant, _bus: &mut EventBus) { + fn on_property_change(&mut self, key: PropertyID, value: &Variant, _bus: &mut EventBus) { self.output.on_event(&Event::new_property_change(key, value.clone())); } @@ -90,7 +90,17 @@ impl Task for Renderer { self.fps.insert((self.frame - self.frame_count) as u32); self.frame_count = self.frame; let fps = self.fps.measurement(); - bus.push(Event::new_property_change("output.fps", fps.rate() as u32)); + bus.set_property(crate::render::props::Output::FPS, fps.rate() as u32); }); } +} + +pub mod props { + use crate::property_namespace; + + property_namespace!( + Output, + FPS => 0, + Brightness => 0 + ); } \ No newline at end of file diff --git a/src/scenes.rs b/src/scenes.rs index 40ba4e3..51544bc 100644 --- a/src/scenes.rs +++ b/src/scenes.rs @@ -1,7 +1,7 @@ -use std::str::FromStr; - use crate::task::Task; use crate::events::*; +use crate::properties::*; +use paste::paste; #[derive(Debug, PartialEq, Eq, Clone)] struct Scene { @@ -13,7 +13,7 @@ struct Scene { #[derive(Debug, PartialEq, Eq, Clone)] enum Trigger { Startup, - PropertyEquals(&'static str, Variant) + PropertyEquals(PropertyID, Variant) } pub struct Sequencer { @@ -34,7 +34,7 @@ impl Sequencer { Scene { name: "Online", patterns: vec!["Idle"], - trigger: Trigger::PropertyEquals("system.network.online", Variant::Boolean(true)) + trigger: Trigger::PropertyEquals(prop_id!(System::NetworkOnline), Variant::Boolean(true)) } ] } @@ -78,34 +78,53 @@ impl Sequencer { } } +pub mod props { + use crate::property_namespace; + + property_namespace!( + Scenes, + Current => "", + All => Vec::new() + ); +} + +use crate::scenes::props::Scenes; + impl Task for Sequencer { fn start(&mut self, bus: &mut EventBus) { log::info!("Starting sequencer!!!"); let startup_scene = self.scenes.iter().filter(|i| { i.trigger == Trigger::Startup }).next().unwrap(); - bus.push(Event::new_property_change("scenes.current", startup_scene.name)); + bus.set_property(Scenes::Current, startup_scene.name); + let mut scene_list = Vec::new(); + for scene in self.scenes.iter() { + scene_list.push(Variant::String(scene.name.to_string())); + } + + bus.set_property(Scenes::All, scene_list); } - fn on_property_change(&mut self, key: &'static str, value: &Variant, bus: &mut EventBus) { + fn on_property_change(&mut self, key: PropertyID, value: &Variant, bus: &mut EventBus) { match (key, value) { - ("scenes.current", Variant::String(scene_name)) => { + (prop_id!(Scenes::Current), Variant::String(ref scene_name)) => { log::info!("Applying scene"); self.apply_scene(scene_name, bus); }, (key, value) => { - for scene in self.scenes.iter() { + /*for scene in self.scenes.iter() { match scene.trigger { Trigger::PropertyEquals(trigger_key, ref trigger_value) => { if trigger_key == key && trigger_value == value { log::info!("Triggering scene {}", scene.name); - bus.push(Event::new_property_change("scenes.current", scene.name)) + bus.push(Scenes::Current.new_property_change(scene.name)) } }, _ => () } - } + }*/ } - _ => () } } -} \ No newline at end of file +} + +use crate::prop_id; \ No newline at end of file diff --git a/src/task.rs b/src/task.rs index 87e2361..40f5aea 100644 --- a/src/task.rs +++ b/src/task.rs @@ -1,11 +1,11 @@ use core::fmt; -use crate::events::{Event, EventBus, Variant}; +use crate::{events::{Event, EventBus}, properties::{Variant, PropertyID}}; pub trait Task: Send { fn on_ready(&mut self, bus: &mut EventBus) {} fn on_tick(&mut self, bus: &mut EventBus) {} - fn on_property_change(&mut self, key: &'static str, value: &Variant, bus: &mut EventBus) {} + fn on_property_change(&mut self, key: PropertyID, value: &Variant, bus: &mut EventBus) {} fn start(&mut self, bus: &mut EventBus) {} fn stop(&mut self, bus: &mut EventBus) {} @@ -80,7 +80,7 @@ impl ScheduledTask { match event { Event::Tick => self.task.on_tick(bus), Event::ReadyToRock => self.task.on_ready(bus), - Event::PropertyChange(key, value) => self.task.on_property_change(key, value, bus), + Event::PropertyChange(key, value) => self.task.on_property_change(key.clone(), value, bus), _ => () } },