wip
This commit is contained in:
parent
63c913a8e6
commit
1d7848119e
@ -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"
|
||||
|
@ -1 +1,2 @@
|
||||
partition_table = "partitions.csv"
|
||||
baudrate = 460800
|
87
src/animations/mod.rs
Normal file
87
src/animations/mod.rs
Normal file
@ -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<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(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();
|
||||
}
|
||||
}
|
@ -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<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();
|
||||
}
|
||||
}
|
||||
use super::{Coordinates, Rectangle, VirtualCoordinates};
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
enum TestShader {
|
||||
@ -168,11 +88,13 @@ impl<T: Surface> TestPattern<T> {
|
||||
}
|
||||
}
|
||||
|
||||
//const Animations: Namespace = Namespace("animations");
|
||||
|
||||
impl<T: Surface> Task for TestPattern<T> {
|
||||
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<T: Surface> Task for TestPattern<T> {
|
||||
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<T: Surface> Task for TestPattern<T> {
|
||||
fn stop(&mut self, _bus: &mut EventBus) {
|
||||
self.surface.clear_shader();
|
||||
}
|
||||
}
|
||||
}
|
@ -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<Box<dyn Shader>>,
|
||||
rect: Rectangle<Virtual>,
|
||||
opacity: u8
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct SurfaceUpdate {
|
||||
shader: Option<Option<Box<dyn Shader>>>,
|
||||
rect: Option<Rectangle<Virtual>>,
|
||||
@ -41,12 +36,6 @@ impl SurfaceUpdate {
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Box<dyn Shader> {
|
||||
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<UpdateQueue>,
|
||||
slot: usize
|
||||
@ -89,16 +77,15 @@ impl Surface for BufferedSurface {
|
||||
});
|
||||
}
|
||||
|
||||
fn set_shader(&mut self, shader: Box<dyn Shader>) {
|
||||
fn set_shader<T: Shader + 'static>(&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<Vec<SurfaceUpdate>>,
|
||||
damaged: AtomicBool
|
||||
@ -135,7 +122,6 @@ impl UpdateQueue {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ShaderChain {
|
||||
bindings: Vec<ShaderBinding>,
|
||||
updates: Arc<UpdateQueue>
|
||||
@ -154,31 +140,28 @@ impl ShaderChain {
|
||||
}
|
||||
|
||||
pub fn commit(&mut self) {
|
||||
let mut queue: Vec<SurfaceUpdate> = {
|
||||
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<SurfaceUpdate> = {
|
||||
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<Virtual>) -> Result<Self::Surface, Self::Error> {
|
||||
fn new_surface(&mut self, area: Rectangle<Virtual>) -> Result<BufferedSurface, ()> {
|
||||
let next_slot = self.bindings.len();
|
||||
self.bindings.push(ShaderBinding {
|
||||
opacity: 255,
|
||||
@ -193,12 +176,12 @@ impl Surfaces for ShaderChain {
|
||||
}
|
||||
|
||||
fn render_to<S: Sample>(&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<RwLock<ShaderChain>>
|
||||
pool: RefCell<ShaderChain>
|
||||
}
|
||||
|
||||
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 = <ShaderChain as Surfaces>::Surface;
|
||||
type Surface = BufferedSurface;
|
||||
fn new_surface(&mut self, area: crate::geometry::Rectangle<crate::geometry::Virtual>) -> Result<Self::Surface, Self::Error> {
|
||||
self.pool.write().unwrap().new_surface(area)
|
||||
self.pool.borrow_mut().new_surface(area)
|
||||
}
|
||||
|
||||
fn render_to<S: crate::render::Sample>(&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);
|
||||
}
|
||||
}
|
||||
|
||||
|
243
src/events.rs
243
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<u8>)
|
||||
RGB(Rgb<u8>),
|
||||
Vec(Vec<Variant>)
|
||||
}
|
||||
|
||||
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<u8>, RGB);
|
||||
impl_variant_type!(Vec<Variant>, 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<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))
|
||||
}
|
||||
|
||||
@ -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<Property>;
|
||||
}
|
||||
|
||||
#[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<V>(&self, value: V) -> $crate::Event where $crate::Variant: From<V> {
|
||||
$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<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: LinkedList<Property>
|
||||
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 {
|
||||
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<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)
|
||||
}
|
||||
}
|
||||
|
||||
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<Variant> {
|
||||
match self.get_key(key) {
|
||||
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>(&mut self, key: &'static str, value: V) -> bool where Variant: From<V> {
|
||||
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_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<Variant> {
|
||||
self.props.lock().unwrap().get(key)
|
||||
pub fn properties(&self) -> std::sync::MutexGuard<'_, Properties> {
|
||||
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
|
||||
);
|
101
src/inputs/circadian.rs
Normal file
101
src/inputs/circadian.rs
Normal file
@ -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<Utc> = 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);
|
||||
}
|
||||
}
|
||||
}
|
1
src/inputs/mod.rs
Normal file
1
src/inputs/mod.rs
Normal file
@ -0,0 +1 @@
|
||||
pub mod circadian;
|
@ -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<u8> {
|
||||
}
|
||||
|
||||
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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
42
src/main.rs
42
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::<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...");
|
||||
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);
|
||||
|
@ -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
143
src/platform/esp32/board.rs
Normal 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())
|
||||
])
|
||||
}
|
||||
}
|
3
src/platform/esp32/mod.rs
Normal file
3
src/platform/esp32/mod.rs
Normal file
@ -0,0 +1,3 @@
|
||||
pub mod board;
|
||||
pub mod wifi;
|
||||
pub mod mqtt;
|
186
src/platform/esp32/mqtt.rs
Normal file
186
src/platform/esp32/mqtt.rs
Normal 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, 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<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.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<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.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
|
||||
);
|
||||
}
|
104
src/platform/esp32/wifi.rs
Normal file
104
src/platform/esp32/wifi.rs
Normal 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, 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<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.push(SystemNS::NetworkOnline.new_property_change(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.push(SystemNS::NetworkOnline.new_property_change(false));
|
||||
}
|
||||
}
|
@ -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
|
||||
);
|
||||
}
|
@ -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<P: Pixbuf<Pixel=T::Color>, T: FastWrite> Output for StrideOutput<P, T> {
|
||||
|
||||
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,
|
||||
_ => ()
|
||||
}
|
||||
}
|
||||
|
@ -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<Rgb<u8>> + Fract8Ops {}
|
||||
impl<T> HardwarePixel for T where T: Send + Sync + Copy + AsMilliwatts + Default + From<Rgb<u8>> + Fract8Ops {}
|
||||
@ -23,19 +23,19 @@ pub trait Sample {
|
||||
fn sample(&mut self, rect: &Rectangle<Virtual>) -> impl PixelView<Pixel = Self::Pixel>;
|
||||
}
|
||||
|
||||
pub trait Shader: Send + Sync {
|
||||
pub trait Shader: Send {
|
||||
fn draw(&self, surface_coords: &VirtualCoordinates, frame: usize) -> Rgb<u8>;
|
||||
}
|
||||
|
||||
pub trait Surfaces: Send + Sync {
|
||||
pub trait Surfaces: Send {
|
||||
type Surface: Surface;
|
||||
type Error: Debug;
|
||||
fn new_surface(&mut self, area: Rectangle<Virtual>) -> Result<Self::Surface, Self::Error>;
|
||||
fn render_to<S: Sample>(&self, output: &mut S, frame: usize);
|
||||
}
|
||||
|
||||
pub trait Surface: Send + Sync {
|
||||
fn set_shader(&mut self, shader: Box<dyn Shader>);
|
||||
pub trait Surface: Send {
|
||||
fn set_shader<T: Shader + 'static>(&mut self, shader: T);
|
||||
fn clear_shader(&mut self);
|
||||
|
||||
fn set_rect(&mut self, rect: Rectangle<Virtual>);
|
||||
@ -74,7 +74,7 @@ impl<T: Output, S: Surfaces> Renderer<T, S> {
|
||||
impl<T: Output, S: Surfaces> Task for Renderer<T, S> {
|
||||
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<T: Output, S: Surfaces> Task for Renderer<T, S> {
|
||||
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
|
||||
);
|
||||
}
|
@ -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))
|
||||
}
|
||||
},
|
||||
_ => ()
|
||||
}
|
||||
}
|
||||
}*/
|
||||
}
|
||||
_ => ()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
use crate::prop_id;
|
10
src/task.rs
10
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::<Self>()
|
||||
}
|
||||
|
||||
fn properties(&self) -> LinkedList<Property> { 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<const TASK_COUNT: usize> Scheduler for FixedSizeScheduler<TASK_COUNT> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user