From 1d7848119efe6356758e051f87f1fb2323c91f08 Mon Sep 17 00:00:00 2001 From: Victoria Fischer Date: Sun, 15 Dec 2024 15:18:12 +0100 Subject: [PATCH] wip --- Cargo.toml | 9 +- espflash.toml | 1 + src/animations/mod.rs | 87 +++++ src/{animations.rs => animations/test.rs} | 94 +---- src/buffers.rs | 87 ++--- src/events.rs | 243 +++++++++--- src/inputs/circadian.rs | 101 +++++ src/inputs/mod.rs | 1 + src/lib8/interpolate.rs | 33 +- src/main.rs | 42 +- 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 | 6 +- src/render.rs | 28 +- src/scenes.rs | 37 +- src/task.rs | 10 +- 20 files changed, 983 insertions(+), 696 deletions(-) create mode 100644 src/animations/mod.rs rename src/{animations.rs => animations/test.rs} (56%) 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 diff --git a/Cargo.toml b/Cargo.toml index 11385c2..917b8e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,11 +11,13 @@ name = "renderbug" harness = false # do not use the built in cargo test harness -> resolve rust-analyzer errors [profile.release] -opt-level = "s" +opt-level = 3 +lto = "on" [profile.dev] debug = true # Symbols are nice and they don't increase the size on Flash -opt-level = "z" +opt-level = "s" +lto = "on" [features] default = ["std", "esp-idf-svc/native", "rmt", "smart-leds"] @@ -48,6 +50,9 @@ embedded-graphics = { version = "0.8.1", optional = true, features = ["fixed_poi ansi_term = "0.12.1" num = "0.4.3" chrono = "0.4.38" +serde_json = "1.0.133" +ssd1306 = "0.9.0" +embedded-hal = "1.0.0" [build-dependencies] embuild = "0.32.0" diff --git a/espflash.toml b/espflash.toml index d22b92e..231558a 100644 --- a/espflash.toml +++ b/espflash.toml @@ -1 +1,2 @@ partition_table = "partitions.csv" +baudrate = 460800 \ No newline at end of file diff --git a/src/animations/mod.rs b/src/animations/mod.rs new file mode 100644 index 0000000..6dd4f31 --- /dev/null +++ b/src/animations/mod.rs @@ -0,0 +1,87 @@ +pub mod test; + +use palette::Hsv; + +use rgb::RGB8; + +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_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(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 56% rename from src/animations.rs rename to src/animations/test.rs index 45c2ba3..99e9d06 100644 --- a/src/animations.rs +++ b/src/animations/test.rs @@ -1,89 +1,9 @@ use palette::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, Namespace}, 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 { @@ -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/buffers.rs b/src/buffers.rs index b49db4f..a3b5f1b 100644 --- a/src/buffers.rs +++ b/src/buffers.rs @@ -1,25 +1,20 @@ -use crate::events::EventBus; use crate::geometry::*; use crate::lib8::interpolate::Fract8Ops; use crate::power::AsMilliwatts; use crate::render::{PixelView, Sample, Shader, Surface, Surfaces, HardwarePixel}; -use crate::task::Task; -use std::fmt::Debug; +use std::cell::RefCell; use std::ops::IndexMut; use std::sync::atomic::AtomicBool; -use std::sync::RwLock; use std::sync::{Arc, Mutex}; -#[derive(Debug)] struct ShaderBinding { shader: Option>, rect: Rectangle, opacity: u8 } -#[derive(Debug)] struct SurfaceUpdate { shader: Option>>, rect: Option>, @@ -41,12 +36,6 @@ impl SurfaceUpdate { } } -impl Debug for Box { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("Shader").finish() - } -} - impl Default for SurfaceUpdate { fn default() -> Self { SurfaceUpdate { @@ -58,7 +47,6 @@ impl Default for SurfaceUpdate { } } -#[derive(Debug)] pub struct BufferedSurface { updater: Arc, slot: usize @@ -89,16 +77,15 @@ impl Surface for BufferedSurface { }); } - fn set_shader(&mut self, shader: Box) { + fn set_shader(&mut self, shader: T) { self.updater.push(SurfaceUpdate { - shader: Some(Some(shader)), + shader: Some(Some(Box::new(shader))), slot: self.slot, ..Default::default() }); } } -#[derive(Debug)] struct UpdateQueue { pending: Mutex>, damaged: AtomicBool @@ -135,7 +122,6 @@ impl UpdateQueue { } } -#[derive(Debug)] pub struct ShaderChain { bindings: Vec, updates: Arc @@ -154,31 +140,28 @@ impl ShaderChain { } pub fn commit(&mut self) { - let mut queue: Vec = { - let mut updates = self.updates.pending.lock().unwrap(); - std::mem::take(updates.as_mut()) - }; - for update in queue.iter_mut() { - let target_slot = &mut self.bindings[update.slot]; - if let Some(shader) = update.shader.take() { - target_slot.shader = shader; - } - if let Some(opacity) = update.opacity.take() { - target_slot.opacity = opacity; - } - if let Some(rect) = update.rect.take() { - target_slot.rect = rect; + if self.is_dirty() { + let mut queue: Vec = { + let mut updates = self.updates.pending.lock().unwrap(); + std::mem::take(updates.as_mut()) + }; + for update in queue.iter_mut() { + let target_slot = &mut self.bindings[update.slot]; + if let Some(shader) = update.shader.take() { + target_slot.shader = shader; + } + if let Some(opacity) = update.opacity.take() { + target_slot.opacity = opacity; + } + if let Some(rect) = update.rect.take() { + target_slot.rect = rect; + } } + self.updates.damaged.store(false, std::sync::atomic::Ordering::Relaxed); } - self.updates.damaged.store(false, std::sync::atomic::Ordering::Relaxed); } -} -impl Surfaces for ShaderChain { - type Error = (); - type Surface = BufferedSurface; - - fn new_surface(&mut self, area: Rectangle) -> Result { + fn new_surface(&mut self, area: Rectangle) -> Result { let next_slot = self.bindings.len(); self.bindings.push(ShaderBinding { opacity: 255, @@ -193,12 +176,12 @@ impl Surfaces for ShaderChain { } fn render_to(&self, output: &mut S, frame: usize) { - for surface in self.bindings.iter() { + for surface in &self.bindings { let opacity = surface.opacity; if opacity > 0 { - let rect = surface.rect; - let mut sample = output.sample(&rect); if let Some(ref shader) = surface.shader { + let rect = surface.rect; + let mut sample = output.sample(&rect); while let Some((virt_coords, pixel)) = sample.next() { *pixel = pixel.blend8(shader.draw(&virt_coords, frame).into(), opacity); } @@ -208,37 +191,29 @@ impl Surfaces for ShaderChain { } } -#[derive(Clone)] pub struct BufferedSurfacePool { - pool: Arc> + pool: RefCell } impl BufferedSurfacePool { pub fn new() -> Self { BufferedSurfacePool { - pool: Arc::new(RwLock::new(ShaderChain::new())) + pool: RefCell::new(ShaderChain::new()) } } } impl Surfaces for BufferedSurfacePool { type Error = (); - type Surface = ::Surface; + type Surface = BufferedSurface; fn new_surface(&mut self, area: crate::geometry::Rectangle) -> Result { - self.pool.write().unwrap().new_surface(area) + self.pool.borrow_mut().new_surface(area) } fn render_to(&self, output: &mut S, frame: usize) { - self.pool.read().unwrap().render_to(output, frame); - } -} - - -impl Task for BufferedSurfacePool { - fn on_tick(&mut self, _bus: &mut EventBus) { - if self.pool.read().unwrap().is_dirty() { - self.pool.write().unwrap().commit(); - } + let mut b = self.pool.borrow_mut(); + b.commit(); + b.render_to(output, frame); } } diff --git a/src/events.rs b/src/events.rs index 290f5d2..67d46af 100644 --- a/src/events.rs +++ b/src/events.rs @@ -1,5 +1,5 @@ -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; @@ -13,7 +13,8 @@ pub enum Variant { BigInt(i64), Boolean(bool), String(String), - RGB(Rgb) + RGB(Rgb), + Vec(Vec) } macro_rules! impl_variant_type { @@ -28,7 +29,7 @@ macro_rules! impl_variant_type { fn into(self) -> $type { match self { Variant::$var_type(value) => value, - _ => panic!("Expected Variant::$var_type, but got {:?}", self) + _ => panic!(concat!("Expected Variant::", stringify!($var_type), "but got {:?}"), self) } } } @@ -44,6 +45,7 @@ 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 { @@ -51,29 +53,25 @@ impl<'a> From<&'a str> for Variant { } } -impl Display for Variant { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Variant::BigInt(i) => (i as &dyn Display).fmt(f), - Variant::BigUInt(i) => (i as &dyn Display).fmt(f), - Variant::Boolean(i) => (i as &dyn Display).fmt(f), - Variant::Byte(i) => (i as &dyn Display).fmt(f), - Variant::Int(i) => (i as &dyn Display).fmt(f), - Variant::String(i) => (i as &dyn Display).fmt(f), - Variant::UInt(i) => (i as &dyn Display).fmt(f), - Variant::SignedByte(b) => (b as &dyn Display).fmt(f), - Variant::RGB(rgb) => f.write_fmt(format_args!("[{}, {}, {}]", rgb.r, rgb.g, rgb.b)), - } - } -} - #[derive(Debug, Clone, PartialEq, Eq)] pub enum Event { ReadyToRock, 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 { @@ -81,7 +79,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)) } @@ -100,65 +98,200 @@ impl Event { #[derive(Debug, Clone, PartialEq, Eq)] pub struct Property { - key: &'static str, + key: PropertyKey, value: Variant } impl Property { - pub const fn new(key: &'static str, value: Variant) -> Self { + pub const fn new(key: PropertyKey, value: Variant) -> Self { Property { key, - value: value + 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<'_>) -> Result { + write!(f, "{}::{}", self.namespace.0, self.key.0) + } +} + +pub trait Namespace { + fn nskey() -> NamespaceKey; + fn properties() -> Vec; +} + +#[macro_export] +macro_rules! prop_id { + ($name:ident, $prop_name:ident) => { + $crate::PropertyID { + namespace: $crate::NamespaceKey(stringify!($name)), + key: $crate::PropertyKey(stringify!($prop_name)) + } + }; +} + +#[macro_export] +macro_rules! property_namespace { + ($name:ident, $($prop_name:ident => $value:expr),*) => { + pub enum $name { + $($prop_name),* + } + + impl $crate::Namespace for $name { + fn nskey() -> $crate::NamespaceKey { + $crate::NamespaceKey(stringify!($name)) + } + + fn properties() -> Vec<$crate::Property> { + vec![ + $($crate::Property::new(Self::$prop_name.key(), $crate::Variant::from($value))),* + ] + } + } + + impl $name { + pub const fn key(&self) -> $crate::PropertyKey { + match self { + $(Self::$prop_name => $crate::PropertyKey(stringify!($prop_name))),* + } + } + + pub const fn id(&self) -> $crate::PropertyID { + $crate::PropertyID { namespace: $crate::NamespaceKey(stringify!($name)), key: self.key() } + } + + pub fn new_property_change(&self, value: V) -> $crate::Event where $crate::Variant: From { + $crate::Event::PropertyChange(self.id(), $crate::Variant::from(value)) + } + } + + impl Into<$crate::PropertyID> for $name { + fn into(self) -> $crate::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: LinkedList + 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 { fn new() -> Self { Properties { - contents: LinkedList::new() + contents: Vec::new(), } } - fn get_key(&self, key: &'static str) -> Option<&Property> { - for next in self.contents.iter() { - if next.key == key { - return Some(next); + 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) } } - return None; + log::warn!("Unknown namespace {:?}", ns); + + 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); + 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) } } - return None; + log::warn!("Unknown namespace {:?}", ns); + + None } - pub fn get(&self, key: &'static str) -> Option { - match self.get_key(key) { + 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: &'static str, value: V) -> bool where Variant: From { + 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_key_mut(key) { - None => { - self.contents.push_front(Property::new(key, as_variant)); - return true - }, + 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; @@ -191,7 +324,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() @@ -205,7 +338,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..2c3024e --- /dev/null +++ b/src/inputs/circadian.rs @@ -0,0 +1,101 @@ +use chrono::{DateTime, Timelike, Utc}; + +use crate::{events::{Event, EventBus, Namespace, Variant}, lib8::interpolate::lerp8by8, prop_id, render::props::Output as OutputNS, task::Task, time::Periodically, PropertyID}; + +#[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.push(OutputNS::Brightness.new_property_change(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: PropertyID, value: &Variant, bus: &mut EventBus) { + match (key, value) { + (prop_id!(System, 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/interpolate.rs b/src/lib8/interpolate.rs index a338931..b23def6 100644 --- a/src/lib8/interpolate.rs +++ b/src/lib8/interpolate.rs @@ -12,12 +12,21 @@ pub trait Fract8Ops { impl Fract8Ops for u8 { fn scale8(self, scale: Fract8) -> Self { - // borrowed from FastLED - (self as u16 * (1 + scale as u16)).unsigned_shr(8) as u8 + match scale { + 0 => 0, + 255 => self, + _ => + // borrowed from FastLED + (self as u16 * (1 + scale as u16)).unsigned_shr(8) as u8 + } } fn blend8(self, other: Self, scale: Fract8) -> Self { - ((((self as u16).unsigned_shl(8).bitor(other as u16)) as u16).wrapping_add(other as u16 * scale as u16).wrapping_sub(self as u16 * scale as u16)).unsigned_shr(8) as u8 + match scale { + 0 => self, + 255 => other, + _ => ((((self as u16).unsigned_shl(8).bitor(other as u16)) as u16).wrapping_add(other as u16 * scale as u16).wrapping_sub(self as u16 * scale as u16)).unsigned_shr(8) as u8 + } } } @@ -31,13 +40,17 @@ impl Fract8Ops for Rgb { } fn blend8(self, other: Self, scale: Fract8) -> Self { - match (other.r, other.g, other.b) { - (0, 0, 0) => self, - _ => Rgb::new( - self.r.blend8(other.r, scale), - self.g.blend8(other.g, scale), - self.b.blend8(other.b, scale) - ) + match scale { + 0 => self, + 255 => other, + _ => match (other.r, other.g, other.b) { + (0, 0, 0) => self, + _ => Rgb::new( + self.r.blend8(other.r, scale), + self.g.blend8(other.g, scale), + self.b.blend8(other.b, scale) + ) + } } } } diff --git a/src/main.rs b/src/main.rs index cbc4d6b..bbe666c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,18 +8,24 @@ mod platform; mod animations; mod mappings; mod buffers; -mod events; mod scenes; +mod inputs; -use events::Event; -use rgb::Rgb; +#[macro_use] +mod events; + +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 +49,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 +62,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 +98,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..66ac728 --- /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, Namespace, Property, System as SystemNS, Variant}, prop_id, render::props::Output as OutputNS, scenes::props::Scenes as SceneNS, task::Task, PropertyID}; +use crate::platform::props::Board as BoardNS; + +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.push(props::MQTT::Online.new_property_change(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!(System, Online), 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.push(props::MQTT::Online.new_property_change(true)); + } + }, + (prop_id!(Output, 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..890085d --- /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, Variant, System as SystemNS}, 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.push(SystemNS::NetworkOnline.new_property_change(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.push(SystemNS::NetworkOnline.new_property_change(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..7f14b10 100644 --- a/src/platform/smart_leds_lib.rs +++ b/src/platform/smart_leds_lib.rs @@ -2,9 +2,9 @@ use smart_leds_trait::SmartLedsWrite; use crate::buffers::Pixbuf; use crate::events::Variant; -use crate::render::{HardwarePixel, Output, PixelView, Sample}; +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 core::fmt::Debug; @@ -57,7 +57,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!(Output, Brightness), Variant::Byte(new_brightness)) => self.brightness = *new_brightness, _ => () } } diff --git a/src/render.rs b/src/render.rs index f5285ba..b1a68da 100644 --- a/src/render.rs +++ b/src/render.rs @@ -1,13 +1,13 @@ use rgb::Rgb; - -use crate::events::{Event, EventBus}; -use crate::geometry::*; +use crate::events::*; +use crate::{geometry::*, PropertyID}; use crate::lib8::interpolate::Fract8Ops; use crate::power::AsMilliwatts; use crate::task::Task; use crate::time::Periodically; use running_average::RealTimeRunningAverage; use core::fmt::Debug; +use std::collections::LinkedList; pub trait HardwarePixel: Send + Sync + Copy + AsMilliwatts + Default + From> + Fract8Ops {} impl HardwarePixel for T where T: Send + Sync + Copy + AsMilliwatts + Default + From> + Fract8Ops {} @@ -23,19 +23,19 @@ pub trait Sample { fn sample(&mut self, rect: &Rectangle) -> impl PixelView; } -pub trait Shader: Send + Sync { +pub trait Shader: Send { fn draw(&self, surface_coords: &VirtualCoordinates, frame: usize) -> Rgb; } -pub trait Surfaces: Send + Sync { +pub trait Surfaces: Send { type Surface: Surface; type Error: Debug; fn new_surface(&mut self, area: Rectangle) -> Result; fn render_to(&self, output: &mut S, frame: usize); } -pub trait Surface: Send + Sync { - fn set_shader(&mut self, shader: Box); +pub trait Surface: Send { + fn set_shader(&mut self, shader: T); fn clear_shader(&mut self); fn set_rect(&mut self, rect: Rectangle); @@ -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: &crate::events::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.push(crate::render::props::Output::FPS.new_property_change(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..27cdc72 100644 --- a/src/scenes.rs +++ b/src/scenes.rs @@ -1,5 +1,3 @@ -use std::str::FromStr; - use crate::task::Task; use crate::events::*; @@ -78,34 +76,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.push(Scenes::Current.new_property_change(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.push(Scenes::All.new_property_change(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..41ff6b2 100644 --- a/src/task.rs +++ b/src/task.rs @@ -1,17 +1,20 @@ use core::fmt; +use std::collections::LinkedList; -use crate::events::{Event, EventBus, Variant}; +use crate::{events::{Event, EventBus, Namespace, Property, 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) {} fn name(&self) -> &'static str { core::any::type_name::() } + + fn properties(&self) -> LinkedList { LinkedList::new() } } #[derive(Debug, Clone)] @@ -80,7 +83,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), _ => () } }, @@ -141,7 +144,6 @@ impl Scheduler for FixedSizeScheduler { } } } - } }