properties: rewrite properties (whoops)

This commit is contained in:
Torrie Fischer 2024-12-15 19:27:27 +01:00
parent 3a49e7e390
commit 514c9defd6
19 changed files with 1051 additions and 715 deletions

View File

@ -45,6 +45,8 @@ smart-leds = { version = "0.4.0", optional = true }
embedded-graphics = { version = "0.8.1", optional = true, features = ["fixed_point", "defmt"] } embedded-graphics = { version = "0.8.1", optional = true, features = ["fixed_point", "defmt"] }
num = "0.4.3" num = "0.4.3"
chrono = "0.4.38" chrono = "0.4.38"
serde_json = "1.0.133"
paste = "1.0.15"
[build-dependencies] [build-dependencies]
embuild = "0.32.0" embuild = "0.32.0"

86
src/animations/mod.rs Normal file
View File

@ -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<T: Surface> {
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<T: Surface> IdleTask<T> {
pub fn new<S: Surfaces<Surface = T>>(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<T: Surface> Task for IdleTask<T> {
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();
}
}

View File

@ -1,89 +1,9 @@
use palette::Hsv; use crate::lib8::Hsv;
use rgb::RGB8; use rgb::RGB8;
use crate::events::{Event, EventBus}; use crate::{events::{Event, EventBus}, lib8::{interpolate::scale8, trig::{cos8, sin8}, IntoRgb8}, render::{Shader, Surface}, task::Task, time::Periodically};
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};
#[derive(Debug)] use super::{Coordinates, Rectangle, VirtualCoordinates};
pub struct IdleTask<T: Surface> {
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<T: Surface> IdleTask<T> {
pub fn new<S: Surfaces<Surface = T>>(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<T: Surface> Task for IdleTask<T> {
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();
}
}
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
enum TestShader { enum TestShader {
@ -128,7 +48,7 @@ impl Shader for TestShader {
Self::Blue => RGB8::new(0, 0, 255), Self::Blue => RGB8::new(0, 0, 255),
Self::White => RGB8::new(255, 255, 255), Self::White => RGB8::new(255, 255, 255),
Self::RGB => RGB8::new(coords.x, coords.y, 255 - (coords.x / 2 + coords.y / 2)), 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) { Self::Outline => match (coords.x, coords.y) {
(0, 0) => RGB8::new(255, 255, 255), (0, 0) => RGB8::new(255, 255, 255),
(0, _) => RGB8::new(255, 0, 0), (0, _) => RGB8::new(255, 0, 0),
@ -168,11 +88,13 @@ impl<T: Surface> TestPattern<T> {
} }
} }
//const Animations: Namespace = Namespace("animations");
impl<T: Surface> Task for TestPattern<T> { impl<T: Surface> Task for TestPattern<T> {
fn name(&self) -> &'static str { "TestPattern" } fn name(&self) -> &'static str { "TestPattern" }
fn start(&mut self, _bus: &mut EventBus) { 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) { fn on_tick(&mut self, bus: &mut EventBus) {
@ -180,8 +102,8 @@ impl<T: Surface> Task for TestPattern<T> {
self.pattern = self.pattern.next(); self.pattern = self.pattern.next();
log::info!("Test pattern: {:?}", self.pattern); log::info!("Test pattern: {:?}", self.pattern);
self.frame = 0; self.frame = 0;
self.surface.set_shader(Box::new(self.pattern)); self.surface.set_shader(self.pattern);
bus.push(Event::new_property_change("animations.test.pattern", format!("{:?}", self.pattern))); //bus.push(Animations.new_property_change( "test.pattern", format!("{:?}", self.pattern)));
}); });
self.stepper.run(|| { self.stepper.run(|| {
self.frame = self.frame.wrapping_add(1); self.frame = self.frame.wrapping_add(1);
@ -203,4 +125,4 @@ impl<T: Surface> Task for TestPattern<T> {
fn stop(&mut self, _bus: &mut EventBus) { fn stop(&mut self, _bus: &mut EventBus) {
self.surface.clear_shader(); self.surface.clear_shader();
} }
} }

View File

@ -1,57 +1,7 @@
use core::fmt::{Debug, Display}; use core::fmt::Debug;
use std::{collections::{LinkedList, VecDeque}, sync::{Arc, Mutex}}; use std::{collections::VecDeque, fmt::{Display, Result}, sync::{Arc, Mutex}};
use rgb::Rgb; use crate::{properties::{Properties, PropertyID, Variant}, property_namespace};
#[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<u8>),
Vec(Vec<Variant>)
}
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<u8>, RGB);
impl_variant_type!(Vec<Variant>, Vec);
impl<'a> From<&'a str> for Variant {
fn from(value: &'a str) -> Self {
Variant::String(value.to_owned())
}
}
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub enum Event { pub enum Event {
@ -59,7 +9,19 @@ pub enum Event {
Tick, Tick,
StartThing(&'static str), StartThing(&'static str),
StopThing(&'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 { impl Event {
@ -67,7 +29,7 @@ impl Event {
Event::Tick Event::Tick
} }
pub fn new_property_change<T>(key: &'static str, data: T) -> Self where Variant: From<T> { pub fn new_property_change<T>(key: PropertyID, data: T) -> Self where Variant: From<T> {
Event::PropertyChange(key, Variant::from(data)) 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<Property>
}
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<Variant> {
match self.get_key(key) {
None => None,
Some(v) => Some(v.value.clone())
}
}
pub fn set<V>(&mut self, key: &'static str, value: V) -> bool where Variant: From<V> {
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)] #[derive(Clone)]
pub struct EventBus { pub struct EventBus {
pending: Arc<Mutex<VecDeque<Event>>>, pending: Arc<Mutex<VecDeque<Event>>>,
@ -177,7 +66,7 @@ impl EventBus {
match next { match next {
Event::PropertyChange(key, value) => { Event::PropertyChange(key, value) => {
if self.props.lock().unwrap().set(key, value.clone()) { 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) Event::PropertyChange(key, value)
} else { } else {
Event::new_tick() Event::new_tick()
@ -191,7 +80,19 @@ impl EventBus {
self.pending.lock().unwrap().push_back(event); self.pending.lock().unwrap().push_back(event);
} }
pub fn property(&self, key: &'static str) -> Option<Variant> { pub fn properties(&self) -> std::sync::MutexGuard<'_, Properties> {
self.props.lock().unwrap().get(key) self.props.lock().unwrap()
} }
}
pub fn set_property<K: Into<PropertyID>, V: Into<Variant>>(&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
);

103
src/inputs/circadian.rs Normal file
View File

@ -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<Utc> = 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);
}
}
}

1
src/inputs/mod.rs Normal file
View File

@ -0,0 +1 @@
pub mod circadian;

View File

@ -2,8 +2,6 @@ pub mod interpolate;
pub mod noise; pub mod noise;
pub mod trig; pub mod trig;
use palette::encoding::srgb::Srgb;
use rgb::Rgb; use rgb::Rgb;
use crate::lib8::interpolate::scale8; use crate::lib8::interpolate::scale8;
@ -18,7 +16,23 @@ impl IntoRgb8 for Rgb<u8> {
} }
} }
impl IntoRgb8 for palette::Hsv<Srgb, u8> { 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 //TODO: Borrowed from FastLED
fn into_rgb8(self) -> Rgb<u8> { fn into_rgb8(self) -> Rgb<u8> {
const HSV_SECTION_3: u8 = 0x40; const HSV_SECTION_3: u8 = 0x40;
@ -27,7 +41,7 @@ impl IntoRgb8 for palette::Hsv<Srgb, u8> {
return Rgb::new(self.value, self.value, self.value) 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 value: u8 = self.value;
let saturation: u8 = self.saturation; let saturation: u8 = self.saturation;
let invsat: u8 = 255 - saturation; let invsat: u8 = 255 - saturation;
@ -52,4 +66,4 @@ impl IntoRgb8 for palette::Hsv<Srgb, u8> {
_ => Rgb::new(rampup_adj_with_floor, brightness_floor, rampdown_adj_with_floor) _ => Rgb::new(rampup_adj_with_floor, brightness_floor, rampdown_adj_with_floor)
} }
} }
} }

View File

@ -8,18 +8,23 @@ mod platform;
mod animations; mod animations;
mod mappings; mod mappings;
mod buffers; mod buffers;
mod events;
mod scenes; mod scenes;
mod inputs;
mod events;
mod properties;
use events::Event; use events::*;
use rgb::Rgb; use inputs::circadian::CircadianRhythm;
use platform::esp32::mqtt::props::MQTT;
use render::props::Output;
use scenes::Sequencer; use scenes::Sequencer;
use crate::events::EventBus; 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::task::{FixedSizeScheduler, Scheduler};
use crate::render::{Surfaces, Renderer}; use crate::render::{Surfaces, Renderer};
use crate::geometry::Rectangle; use crate::geometry::Rectangle;
use crate::scenes::props::Scenes as SceneNS;
fn main() { fn main() {
let mut board: DefaultBoard = Board::take(); let mut board: DefaultBoard = Board::take();
@ -43,10 +48,11 @@ fn main() {
log::info!("🌌 Creating animations"); log::info!("🌌 Creating animations");
let mut animations = FixedSizeScheduler::new([ let mut animations = FixedSizeScheduler::new([
Box::new(animations::IdleTask::new(&mut surfaces)), 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([ let mut inputs = FixedSizeScheduler::new([
Box::new(CircadianRhythm::new()),
Box::new(Sequencer::new()), Box::new(Sequencer::new()),
]); ]);
@ -55,18 +61,27 @@ fn main() {
log::info!("🚌 Starting event bus"); log::info!("🚌 Starting event bus");
let mut bus = EventBus::new(); 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())); let mut props = bus.properties();
bus.push(Event::new_property_change("system.network.online", false)); props.add_namespace::<System>();
props.add_namespace::<BoardNS>();
props.add_namespace::<Output>();
props.add_namespace::<SceneNS>();
props.add_namespace::<MQTT>();
props.set(BoardNS::ChipID, DefaultBoard::chip_id());
log::info!("System properties:");
log::info!("{}", props);
}
log::info!("Priming events..."); log::info!("Priming events...");
let initial_tasks = [ let initial_tasks = [
"Renderer", "Renderer",
"renderbug::scenes::Sequencer", "renderbug::scenes::Sequencer",
"renderbug::buffers::BufferedSurfacePool", "renderbug::platform::esp32::wifi::WifiTask",
"renderbug::platform::esp32::WifiTask", "renderbug::platform::esp32::mqtt::MqttTask",
"renderbug::platform::esp32::MqttTask", "renderbug::inputs::circadian::CircadianRhythm"
"renderbug::platform::esp32::CircadianRhythm"
]; ];
for task_name in initial_tasks { for task_name in initial_tasks {
bus.push(Event::new_start_thing(task_name)); bus.push(Event::new_start_thing(task_name));
@ -82,7 +97,7 @@ fn main() {
Event::ReadyToRock => { Event::ReadyToRock => {
log::info!("🚀 Ready to rock and roll"); log::info!("🚀 Ready to rock and roll");
} }
_ => log::info!("⚡ Event: {:?}", next_event) _ => log::info!("⚡ Event: {}", next_event)
} }
inputs.tick(&next_event, &mut bus); inputs.tick(&next_event, &mut bus);
animations.tick(&next_event, &mut bus); animations.tick(&next_event, &mut bus);

View File

@ -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<Modem>,
pins: Option<Pins>,
rmt: Option<RMT>,
surfaces: BufferedSurfacePool,
}
impl Board for Esp32Board {
type Output = StrideOutput<[Rgb<u8>; 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<Utc> = 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<EspMqttClient<'static>>,
conn_thread: Option<JoinHandle<()>>,
}
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<EspSubscription<'static, System>>,
ip_sub: Option<EspSubscription<'static, System>>,
}
impl WifiTask {
fn new(modem: Modem, sys_loop: EspSystemEventLoop, nvs: &EspNvsPartition<NvsDefault>) -> 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::<WifiEvent, _>( 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::<IpEvent, _>(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));
}
}

143
src/platform/esp32/board.rs Normal file
View File

@ -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<Modem>,
pins: Option<Pins>,
rmt: Option<RMT>,
surfaces: Option<BufferedSurfacePool>,
}
impl Board for Esp32Board {
type Output = StrideOutput<[Rgb<u8>; 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())
])
}
}

View File

@ -0,0 +1,3 @@
pub mod board;
pub mod wifi;
pub mod mqtt;

186
src/platform/esp32/mqtt.rs Normal file
View File

@ -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<EspMqttClient<'static>>,
conn_thread: Option<JoinHandle<()>>,
fps_sensor: Option<HADevice>
}
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<Variant> = 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
);
}

104
src/platform/esp32/wifi.rs Normal file
View File

@ -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<EspSubscription<'static, System>>,
ip_sub: Option<EspSubscription<'static, System>>,
}
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<NvsDefault>) -> 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::<WifiEvent, _>( 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::<IpEvent, _>(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);
}
}

View File

@ -6,7 +6,7 @@ pub mod smart_leds_lib;
pub mod esp32; pub mod esp32;
pub type DefaultBoard = esp32::Esp32Board; pub type DefaultBoard = esp32::board::Esp32Board;
use crate::render::{Output, Surfaces}; use crate::render::{Output, Surfaces};
use crate::task::Scheduler; use crate::task::Scheduler;
@ -21,4 +21,13 @@ pub trait Board {
fn surfaces(&mut self) -> Self::Surfaces; fn surfaces(&mut self) -> Self::Surfaces;
fn system_tasks(&mut self) -> Self::Scheduler; fn system_tasks(&mut self) -> Self::Scheduler;
fn chip_id() -> u64; fn chip_id() -> u64;
}
pub mod props {
use crate::property_namespace;
property_namespace!(
Board,
ChipID => 0_u64
);
} }

View File

@ -1,11 +1,12 @@
use smart_leds_trait::SmartLedsWrite; use smart_leds_trait::SmartLedsWrite;
use crate::buffers::Pixbuf; use crate::buffers::Pixbuf;
use crate::events::Variant; use crate::properties::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::power::brightness_for_mw;
use crate::geometry::*; use crate::{geometry::*, prop_id};
use crate::mappings::*; use crate::mappings::*;
use paste::paste;
use core::fmt::Debug; use core::fmt::Debug;
@ -57,7 +58,7 @@ impl<P: Pixbuf<Pixel=T::Color>, T: FastWrite> Output for StrideOutput<P, T> {
fn on_event(&mut self, event: &crate::events::Event) { fn on_event(&mut self, event: &crate::events::Event) {
match 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,
_ => () _ => ()
} }
} }

270
src/properties.rs Normal file
View File

@ -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<u8>),
Vec(Vec<Variant>)
}
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<u8>, RGB);
impl_variant_type!(Vec<Variant>, 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<Property>;
}
#[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 ::)+ [<KEY_ $key>] }
}
};
($($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::[<KEY_ $prop_name>]}, $crate::properties::Variant::from($value)),)*
]
}
}
impl $name {
pub const NAMESPACE_KEY: $crate::properties::NamespaceKey = $crate::properties::NamespaceKey(stringify!($name));
$(paste! { pub const [<KEY_ $prop_name>]: 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<Property>,
key: NamespaceKey
}
impl NamespaceProps {
fn new<NS: Namespace>() -> 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<NamespaceProps>
}
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<T: Namespace>(&mut self) {
self.contents.push(NamespaceProps::new::<T>());
}
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<T: Into<PropertyID>>(&self, key: T) -> Option<Variant> {
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<V, T: Into<PropertyID>>(&mut self, key: T, value: V) -> bool where Variant: From<V> {
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
}
}

View File

@ -1,6 +1,6 @@
use rgb::Rgb; use rgb::Rgb;
use crate::events::*;
use crate::events::{Event, EventBus}; use crate::properties::*;
use crate::geometry::*; use crate::geometry::*;
use crate::lib8::interpolate::Fract8Ops; use crate::lib8::interpolate::Fract8Ops;
use crate::power::AsMilliwatts; use crate::power::AsMilliwatts;
@ -74,7 +74,7 @@ impl<T: Output, S: Surfaces> Renderer<T, S> {
impl<T: Output, S: Surfaces> Task for Renderer<T, S> { impl<T: Output, S: Surfaces> Task for Renderer<T, S> {
fn name(&self) -> &'static str { "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())); self.output.on_event(&Event::new_property_change(key, value.clone()));
} }
@ -90,7 +90,17 @@ impl<T: Output, S: Surfaces> Task for Renderer<T, S> {
self.fps.insert((self.frame - self.frame_count) as u32); self.fps.insert((self.frame - self.frame_count) as u32);
self.frame_count = self.frame; self.frame_count = self.frame;
let fps = self.fps.measurement(); 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
);
} }

View File

@ -1,7 +1,7 @@
use std::str::FromStr;
use crate::task::Task; use crate::task::Task;
use crate::events::*; use crate::events::*;
use crate::properties::*;
use paste::paste;
#[derive(Debug, PartialEq, Eq, Clone)] #[derive(Debug, PartialEq, Eq, Clone)]
struct Scene { struct Scene {
@ -13,7 +13,7 @@ struct Scene {
#[derive(Debug, PartialEq, Eq, Clone)] #[derive(Debug, PartialEq, Eq, Clone)]
enum Trigger { enum Trigger {
Startup, Startup,
PropertyEquals(&'static str, Variant) PropertyEquals(PropertyID, Variant)
} }
pub struct Sequencer { pub struct Sequencer {
@ -34,7 +34,7 @@ impl Sequencer {
Scene { Scene {
name: "Online", name: "Online",
patterns: vec!["Idle"], 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 { impl Task for Sequencer {
fn start(&mut self, bus: &mut EventBus) { fn start(&mut self, bus: &mut EventBus) {
log::info!("Starting sequencer!!!"); log::info!("Starting sequencer!!!");
let startup_scene = self.scenes.iter().filter(|i| { i.trigger == Trigger::Startup }).next().unwrap(); 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) { match (key, value) {
("scenes.current", Variant::String(scene_name)) => { (prop_id!(Scenes::Current), Variant::String(ref scene_name)) => {
log::info!("Applying scene"); log::info!("Applying scene");
self.apply_scene(scene_name, bus); self.apply_scene(scene_name, bus);
}, },
(key, value) => { (key, value) => {
for scene in self.scenes.iter() { /*for scene in self.scenes.iter() {
match scene.trigger { match scene.trigger {
Trigger::PropertyEquals(trigger_key, ref trigger_value) => { Trigger::PropertyEquals(trigger_key, ref trigger_value) => {
if trigger_key == key && trigger_value == value { if trigger_key == key && trigger_value == value {
log::info!("Triggering scene {}", scene.name); 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))
} }
}, },
_ => () _ => ()
} }
} }*/
} }
_ => ()
} }
} }
} }
use crate::prop_id;

View File

@ -1,11 +1,11 @@
use core::fmt; use core::fmt;
use crate::events::{Event, EventBus, Variant}; use crate::{events::{Event, EventBus}, properties::{Variant, PropertyID}};
pub trait Task: Send { pub trait Task: Send {
fn on_ready(&mut self, bus: &mut EventBus) {} fn on_ready(&mut self, bus: &mut EventBus) {}
fn on_tick(&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 start(&mut self, bus: &mut EventBus) {}
fn stop(&mut self, bus: &mut EventBus) {} fn stop(&mut self, bus: &mut EventBus) {}
@ -80,7 +80,7 @@ impl ScheduledTask {
match event { match event {
Event::Tick => self.task.on_tick(bus), Event::Tick => self.task.on_tick(bus),
Event::ReadyToRock => self.task.on_ready(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),
_ => () _ => ()
} }
}, },