Compare commits

147 Commits

Author SHA1 Message Date
514c9defd6 properties: rewrite properties (whoops) 2024-12-15 19:27:27 +01:00
3a49e7e390 buffers: refactor locking structure for higher fps and automatic surface commit 2024-12-15 17:58:28 +01:00
9c773f0335 buffers: drop Debug impls 2024-12-15 17:57:43 +01:00
9aad55f369 cargo: drop unused deps 2024-12-15 17:54:28 +01:00
7b03c06c29 render: drop Sync from some traits 2024-12-15 17:53:42 +01:00
e0643e3fad events: add Vec to variants, drop Display impl 2024-12-15 17:52:17 +01:00
708106ab95 events: fix variant panic message 2024-12-15 17:51:36 +01:00
2afbc713ce mappings: update albus 2024-12-15 17:49:13 +01:00
b21cdef8b0 lib8: interpolate: optimize some edge cases for blending 2024-12-15 17:48:20 +01:00
3329ad7b89 espflash: use higher baud 2024-12-15 17:46:07 +01:00
63c913a8e6 platform: esp32: mqtt: set client id based on chip id 2024-12-14 15:32:52 +01:00
cdc82cdbf9 events: reimplement From and To impls with macros 2024-12-14 15:32:25 +01:00
01fdc11552 buffers: cleanup now dead sharedsurface impl 2024-12-14 15:32:07 +01:00
a0d524b825 task: rewrite event handling as a step towards event-based subscriptions 2024-12-14 14:53:24 +01:00
f9a8b32d3e platform: smart_leds: build++ 2024-12-14 14:52:09 +01:00
473aff9aa3 platform: embedded_graphics: clone-- 2024-12-14 14:51:42 +01:00
5c2fa005c7 render: only update fps counter when we need it 2024-12-14 14:51:05 +01:00
e9bbdd13c2 render: make surfaces own rectangles, to drop a clone 2024-12-14 14:49:45 +01:00
f180171ee7 events: implement property events and state management 2024-12-14 14:47:23 +01:00
da2a8f5bfc task: remove a copy operation 2024-12-14 14:46:16 +01:00
d09c82c3fc mappings: add a map for albus the tree 2024-12-14 14:44:53 +01:00
c9496e3dc3 mappings: drop a rect clone when we only need the x coord for a linear map 2024-12-14 14:44:13 +01:00
bbdb3d7404 platform: smart_leds: drop a clone we dont need 2024-12-14 14:43:18 +01:00
a237bb7dc8 platform: esp32: drop buggy and slow i2s implementation 2024-12-14 14:41:26 +01:00
3a850105a8 espflash: bump partition size 2024-12-14 14:41:00 +01:00
7d44c11c78 time: clone-- 2024-12-14 14:15:28 +01:00
457ae73c50 main: emojify boot output 2024-12-14 11:11:38 +01:00
8bb46b6f78 platform: smart_leds: warning-- 2024-12-14 11:10:40 +01:00
4b256bc0d9 platform: esp32: wifi: drop event subscriptions on stop 2024-12-14 11:10:15 +01:00
2af9918239 platform: esp32: move output creation into output() method 2024-12-14 11:09:34 +01:00
bfdf7c3230 mappings: debug-- 2024-12-14 11:08:00 +01:00
b6e34111ff platform: add chip_id function to board api 2024-12-14 11:07:34 +01:00
e57ceeb149 todo: update 2024-12-13 01:00:23 +01:00
e0491fafe8 platform: esp32: fix crash when hours wrap around 2024-12-13 01:00:23 +01:00
bf4ef46699 platform: esp32: create cyberplague mask map 2024-12-13 01:00:23 +01:00
42fc0b0c62 platform: esp32: first implementation of mqtt client 2024-12-13 01:00:17 +01:00
3c3952a8a9 platform: esp32: implement a circadian rhythm event source 2024-12-13 00:59:00 +01:00
2f8b94ae61 platform: esp32: document some more chip ids 2024-12-13 00:57:51 +01:00
d7f312ffe4 events: implement a first attempt at an eventing system 2024-12-13 00:56:50 +01:00
9a749c40a1 mappings: add more chip IDs 2024-12-13 00:52:37 +01:00
272bc49eaa lib8: interpolate: add lerp8by8 and map8 functions 2024-12-13 00:47:16 +01:00
b468eb8533 vscode: add cargo tasks 2024-12-13 00:46:09 +01:00
b20c562b27 platform: esp32: first attempt at an i2s implementation, and fix rmt flickering with thread magic 2024-12-06 18:24:22 +01:00
ea75d7a2ee main: logs++ 2024-12-06 18:23:43 +01:00
202876b42f platform: smart_leds: move StrideSampler into mappings module 2024-12-06 15:01:13 +01:00
16c0c9524d buffers: verbose-- 2024-12-02 22:53:51 +01:00
e2e608048d clippy++ 2024-12-02 19:39:06 +01:00
6cafdcfa45 render: remove getters from Surface, reimplement buffer sharing with Send+Sync 2024-12-02 19:36:17 +01:00
f789f6ded9 task: move Scheduler into a trait, define static-sized scheduler implementation, expose as a system task runner in platform 2024-12-02 19:32:18 +01:00
132d7c0a33 render: move Error type into associated types 2024-12-01 21:28:34 +01:00
5c10dcbd7d task: require Tasks to have Send 2024-12-01 21:26:38 +01:00
06a527a552 render: require more rendering primitives to have Sync and/or Send 2024-12-01 21:26:03 +01:00
15308ecd76 buffers: require Send for Pixmaps 2024-12-01 21:23:07 +01:00
0884c704b0 platform: remove Peripherals param from board trait 2024-12-01 21:21:50 +01:00
59c3307c27 geometry: make rectangle::new const 2024-12-01 21:09:56 +01:00
28c9d61926 cargo: clean up some deps 2024-12-01 21:09:01 +01:00
d23321d2ec mappings: pass a raw array for creating strides instead of a bigger, slower vec 2024-12-01 21:08:26 +01:00
d72f4c9a85 task: core++ 2024-12-01 21:07:27 +01:00
d6d4d5b76b task: use a faster enum instead of fancy heap allocations for task states 2024-12-01 21:07:09 +01:00
9af4af6269 time: use core 2024-12-01 21:05:15 +01:00
36aead9762 src: use core instead of std in some places, aiming for no_std compat 2024-12-01 16:17:51 +01:00
0f73b42818 mappings: clippy++ 2024-11-30 16:06:24 +01:00
3af9ad408e platform: rewrite the platform-specific display creation traits into a new Board trait that also supports tasks 2024-11-30 16:06:17 +01:00
57e660cbb6 time: add a tick() function to Periodically to avoid needing Fn 2024-11-30 16:05:01 +01:00
94567b9b60 buffers: move Pixmap out of smart_leds and into buffers 2024-11-29 19:00:07 +01:00
4000e1d0e7 lib8: Rgb8Blend is no longer used 2024-11-29 18:59:31 +01:00
815d1417e0 buffers: fix threads 2024-11-29 18:59:00 +01:00
6fe5fdcc1a animations: clean up casts for future 16 bit coord support 2024-11-29 18:58:27 +01:00
198aa0ebd0 cleanup++ 2024-11-29 18:13:38 +01:00
bd2f2edebb render: effectively rename Display to Output, push remaining common code into Renderer task 2024-11-29 18:13:22 +01:00
5ca062adbd animations: build++ 2024-11-29 16:40:49 +01:00
a002b72567 animations: build++ 2024-11-29 10:58:35 +01:00
26a8924bc8 clippy++ 2024-11-29 10:58:17 +01:00
9289a829be mappings: split coord mapping from pixel mapping 2024-11-29 10:57:33 +01:00
d28c2a1a4c render: merge Framed trait into Display 2024-11-29 00:14:06 +01:00
2f9b99c2b0 geometry: cut down on <> noise with associated types 2024-11-29 00:10:52 +01:00
5488f85792 warnings-- 2024-11-28 19:38:44 +01:00
b1b088eab6 noise: warnings-- 2024-11-28 19:36:44 +01:00
992a8b2568 render: s/RGB8/Rgb<u8>/ 2024-11-28 19:30:10 +01:00
04f5575ba6 platform: smart_leds: simplify trait hierarchy and push pixel type conversion even closer towards the hardware 2024-11-24 23:53:43 +01:00
a76d040ff6 interpolate: pub Fract8 2024-11-24 23:51:18 +01:00
30ed741349 buffers: update threaded implementations 2024-11-24 23:51:03 +01:00
3783bc60b2 animations: rewrite animations with improved surface capabilities 2024-11-24 21:55:24 +01:00
d17baa754f lib8: trig: style++ 2024-11-24 21:54:20 +01:00
2c7d1d2888 mappings: split out ponderjar/panel layouts into separate new functions 2024-11-24 21:53:27 +01:00
54e7506865 mappings: rewrite pixel maps to use the stronger geometry primitives and coordinate space APIs 2024-11-24 19:09:20 +01:00
e8f6a90d0f geometry: style++ 2024-11-24 19:04:35 +01:00
60bcf4faa3 geometry: implement distance API for points 2024-11-24 18:59:16 +01:00
450fbbf9c9 geometry: simplify coord traits, add sized, clone, copy, etc 2024-11-24 18:58:36 +01:00
35ccf20142 lib8: trig: rewrite sin8/cos8 to use traits, for easy modular math with usize 2024-11-24 18:56:47 +01:00
64c28b89fe lib8: interpolate: implement blend8 API 2024-11-24 18:55:54 +01:00
facb95c363 lib8: noise: use wrapping shifts to fix sampling errors 2024-11-24 18:55:11 +01:00
859177a65c lib8: fix hsv2rgb implementation by prescaling the hue 2024-11-24 18:54:25 +01:00
be8aa0248d mappings: default to max 24 strides 2024-11-24 18:52:56 +01:00
b5e1b114f6 render: add opacity to surfaces 2024-11-24 18:51:51 +01:00
43c3344418 sdkconfig: use 240mhz cpu 2024-11-24 18:33:03 +01:00
23fcf1f35f geometry: s/assert/debug_assert/ 2024-11-23 15:01:24 +01:00
a7b681a046 geometry: implement width/height/left/top/right/bottom operations for rectangles 2024-11-23 15:01:02 +01:00
ff610fa6aa platform: smart_leds: use 310 pixels instead of 300 2024-11-23 14:52:36 +01:00
d3a7f8a3e9 cargo: add esp32s3 as a target, configure espflash to use --no-stub 2024-11-23 14:52:04 +01:00
56443c638c lib8: interpolate: drop unused casts 2024-11-23 14:51:40 +01:00
5fed51fbd1 lib8: interpolate: provide a trait to handle fract8 operations 2024-11-23 14:51:27 +01:00
9dff0119a4 render: add a frame parameter to all shaders 2024-11-23 14:50:28 +01:00
73c3ced3d7 render: add api for setting the rectangle on a surface 2024-11-22 15:54:06 +01:00
7b6cf42e4f geometry: require Ord for coordinates, to implement asserts in rectangles 2024-11-22 15:50:49 +01:00
e651608ecc buffers: split out concrete surface and pixbuf implementations into a buffers module 2024-11-18 23:48:20 +01:00
821924cddb cargo: remove embassy from default features 2024-11-18 23:45:05 +01:00
5de628f3e0 platform: smart_leds: rearchitect pixbufs to use color types that are closer to the hardware 2024-11-18 17:21:58 +01:00
e4b8863513 lib8: use Rgb<u8> instead of RGB8 2024-11-18 17:21:10 +01:00
c53a9e27ae power: implement asmilliwatts for sized arrays 2024-11-16 16:25:32 +01:00
cc8377484e platform: smart_leds: warning-- 2024-11-16 12:41:10 +01:00
1b126a3221 cargo: add ansi_term and num 2024-11-16 12:28:21 +01:00
b504ce298b lib8: implement more fastled 8 bit math as lib8 submodules 2024-11-16 12:28:01 +01:00
7d8d59b508 platform: smart_leds: format++ 2024-11-16 12:16:51 +01:00
e3b5a9bde4 platform: smart_leds: handle frame render error 2024-11-16 12:15:42 +01:00
e51ac02dc6 render: implement surface mapping api 2024-11-16 12:13:49 +01:00
10f0eaa75e platform: smart_leds: move pixbuf size into type args 2024-11-16 12:11:34 +01:00
d6e6f1c554 platform: smart_leds: blank display on every frame 2024-11-16 12:06:46 +01:00
24f0a336e6 platform: smart_leds: no need to keep total_mw around when we can calculate on the fly 2024-11-16 12:06:00 +01:00
3517428d54 mappings: implement some debug traits for viewing pixbufs 2024-11-16 12:02:11 +01:00
291affc992 mappings: first implementation of surface mapping and pixel selection engine 2024-11-16 11:49:10 +01:00
61b28179d1 lib8: implement scale8 from fastled 2024-11-16 11:48:11 +01:00
386b454fb7 TODO: add planned layer and surface operations 2024-11-16 11:44:12 +01:00
a64a449da4 lib8: change saturating_add to use by-value instead of by-ref 2024-11-16 11:43:38 +01:00
52434577cd render: make surfacepool cloneable 2024-11-16 11:40:08 +01:00
3a8dd89828 lib8: rewrite hsv2rgb implementation based on fastled 2024-11-16 11:39:28 +01:00
67f1c995ce animations: add other two sides for outline pattern 2024-11-16 11:37:57 +01:00
e329a56c90 geometry: rewrite Coordinates trait into a concrete struct, pass coords by reference to shaders, and add a Rectangle type 2024-11-16 11:37:24 +01:00
a23b2e8e94 power: implement AsMilliwatts for slices 2024-11-14 19:43:14 +01:00
7e90dd5a22 animations: set blank shaders on task start 2024-11-02 15:22:49 +01:00
dbfc79046e render: implement a generic Renderer task that can run any display 2024-11-02 15:22:37 +01:00
873954d596 task: derive Debug 2024-11-02 15:21:30 +01:00
c096d83ab3 render: derive Debug 2024-11-02 15:21:07 +01:00
1b08bc5f52 time: derive Debug 2024-11-02 15:18:41 +01:00
b6b3376fb4 task: provide default impl for name() 2024-11-02 15:18:15 +01:00
faae4b40c7 animations: derive Debug 2024-11-02 15:17:35 +01:00
577a17e4e4 animations: testpattern: extend update time to 10 seconds 2024-11-02 13:11:45 +01:00
8662eb0db7 main: rewrite display creation code to use type aliases and fewer cfg branches 2024-10-30 21:56:03 +01:00
0a28f9f5c6 platform: smart-leds: implement layer blending 2024-10-30 21:55:38 +01:00
b71a66146c animations: implement a test animation, split out to animations module 2024-10-30 21:55:15 +01:00
18287783d4 lib8: implement layer blending 2024-10-30 21:54:27 +01:00
6fc4cb224f platform: embedded: build++ 2024-10-30 21:51:43 +01:00
84b5abce09 platform: move platform lib bits into platform module 2024-10-30 20:54:49 +01:00
1668db2c85 lib8: drop custom RGB8 struct for rgb crate 2024-10-30 20:48:00 +01:00
b7995423d7 cargo: also hide thread api behind thread cfg 2024-10-30 19:59:19 +01:00
f803d8fe93 build: configure different displays based on cargo configs 2024-10-30 19:52:02 +01:00
739d7c2e6d TODO: add todo 2024-10-30 19:50:17 +01:00
37 changed files with 3048 additions and 715 deletions

View File

@ -1,16 +1,22 @@
[build]
target = "xtensa-esp32-espidf"
target = "xtensa-esp32s3-espidf"
[target.xtensa-esp32s3-espidf]
linker = "ldproxy"
runner = "espflash flash --monitor --no-stub" # Select this runner for espflash v3.x.x
rustflags = [ "--cfg", "espidf_time64"] # Extending time_t for ESP IDF 5: https://github.com/esp-rs/rust/issues/110
rust-env = {MCU = "esp32s3"}
[target.xtensa-esp32-espidf]
linker = "ldproxy"
runner = "espflash flash --monitor" # Select this runner for espflash v3.x.x
runner = "espflash flash --monitor --no-stub" # Select this runner for espflash v3.x.x
rustflags = [ "--cfg", "espidf_time64"] # Extending time_t for ESP IDF 5: https://github.com/esp-rs/rust/issues/110
rust-env = {MCU = "esp32"}
[unstable]
build-std = ["std", "panic_abort"]
[env]
MCU="esp32"
# Note: this variable is not used by the pio builder (`cargo build --features pio`)
ESP_IDF_VERSION = "v5.2.2"

29
.vscode/tasks.json vendored Normal file
View File

@ -0,0 +1,29 @@
{
"version": "2.0.0",
"tasks": [
{
"type": "cargo",
"command": "build",
"problemMatcher": [
"$rustc"
],
"group": {
"kind": "build",
"isDefault": true
},
"label": "rust: cargo build"
},
{
"type": "cargo",
"command": "espflash flash --no-stub --monitor",
"problemMatcher": [
"$rustc"
],
"group": {
"kind": "build",
"isDefault": false
},
"label": "rust: cargo espflash"
}
]
}

View File

@ -18,29 +18,35 @@ debug = true # Symbols are nice and they don't increase the size on Flash
opt-level = "z"
[features]
default = ["std", "embassy", "esp-idf-svc/native"]
default = ["std", "esp-idf-svc/native", "rmt", "smart-leds"]
embedded-graphics = ["dep:embedded-graphics", "ws2812-esp32-rmt-driver/embedded-graphics-core"]
smart-leds = ["dep:smart-leds", "dep:smart-leds-trait", "ws2812-esp32-rmt-driver/smart-leds-trait"]
spi = ["dep:ws2812-spi"]
rmt = ["dep:ws2812-esp32-rmt-driver"]
pio = ["esp-idf-svc/pio"]
std = ["alloc", "esp-idf-svc/binstart", "esp-idf-svc/std"]
alloc = ["esp-idf-svc/alloc"]
nightly = ["esp-idf-svc/nightly"]
experimental = ["esp-idf-svc/experimental"]
embassy = ["esp-idf-svc/embassy-sync", "esp-idf-svc/critical-section", "esp-idf-svc/embassy-time-driver"]
[dependencies]
log = { version = "0.4", default-features = false }
esp-idf-svc = { version = "0.49", default-features = false }
ws2812-esp32-rmt-driver = { version = "*", features = ["embedded-graphics-core", "smart-leds-trait"]}
embedded-graphics = { version = "0.8.1", features = ["fixed_point", "defmt"] }
hsv = "0.1.1"
palette = { version = "0.7.6" }
embedded-canvas = "0.3.1"
embassy-executor = "0.6.0"
running-average = "0.1.0"
ws2812-spi = "0.5.0"
smart-leds-trait = "0.3.0"
rgb = "0.8.50"
smart-leds = "0.4.0"
ws2812-esp32-rmt-driver = { version = "*", optional = true }
ws2812-spi = { version = "0.5.0", optional = true }
smart-leds-trait = { version = "0.3.0", optional = true }
smart-leds = { version = "0.4.0", optional = true }
embedded-graphics = { version = "0.8.1", optional = true, features = ["fixed_point", "defmt"] }
num = "0.4.3"
chrono = "0.4.38"
serde_json = "1.0.133"
paste = "1.0.15"
[build-dependencies]
embuild = "0.32.0"

2
espflash.toml Normal file
View File

@ -0,0 +1,2 @@
partition_table = "partitions.csv"
baudrate = 460800

5
partitions.csv Normal file
View File

@ -0,0 +1,5 @@
# Name, Type, SubType, Offset, Size, Flags
# Note: if you have increased the bootloader size, make sure to update the offsets to avoid overlap
nvs, data, nvs, , 0x6000,
phy_init, data, phy, , 0x1000,
factory, app, factory, , 3M,
1 # Name, Type, SubType, Offset, Size, Flags
2 # Note: if you have increased the bootloader size, make sure to update the offsets to avoid overlap
3 nvs, data, nvs, , 0x6000,
4 phy_init, data, phy, , 0x1000,
5 factory, app, factory, , 3M,

View File

@ -3,9 +3,15 @@ CONFIG_ESP_MAIN_TASK_STACK_SIZE=8000
# Use this to set FreeRTOS kernel tick frequency to 1000 Hz (100 Hz by default).
# This allows to use 1 ms granularity for thread sleeps (10 ms by default).
#CONFIG_FREERTOS_HZ=1000
CONFIG_FREERTOS_HZ=1000
# Workaround for https://github.com/espressif/esp-idf/issues/7631
#CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=n
#CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_FULL=n
CONFIG_ESP32_XTAL_FREQ_26=y
#CONFIG_ESP32_XTAL_FREQ_26=y
CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_40=
CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_80=
CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_160=
CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240=y
CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ=240

18
src/TODO.md Normal file
View File

@ -0,0 +1,18 @@
[x] cfg macros
[ ] warnings
[x] rgb crate
[x] Layer blending
[x] Refactor idle pattern into test pattern
[x] Wifi
[ ] JSON surface map loading
[ ] Weather
[x] Circadian Rhythm
[x] NTP
[ ] Config to only start a subset of tasks on startup
[ ] Serial CLI
[ ] Surface blending API
[ ] Layer blending equations
[ ] Surface rotation
[ ] esp8266 port
[ ] event system
[ ] threaded schedulers

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();
}
}

128
src/animations/test.rs Normal file
View File

@ -0,0 +1,128 @@
use crate::lib8::Hsv;
use rgb::RGB8;
use crate::{events::{Event, EventBus}, lib8::{interpolate::scale8, trig::{cos8, sin8}, IntoRgb8}, render::{Shader, Surface}, task::Task, time::Periodically};
use super::{Coordinates, Rectangle, VirtualCoordinates};
#[derive(Clone, Copy, Debug)]
enum TestShader {
Red,
Green,
Blue,
White,
RGB,
HSV,
Outline,
SweepX,
SweepY,
SinX,
SinY,
Metaballs
}
impl TestShader {
pub fn next(self) -> Self {
match self {
Self::Red => Self::Green,
Self::Green => Self::Blue,
Self::Blue => Self::White,
Self::White => Self::RGB,
Self::RGB => Self::HSV,
Self::HSV => Self::Outline,
Self::Outline => Self::SweepX,
Self::SweepX => Self::SweepY,
Self::SweepY => Self::SinX,
Self::SinX => Self::SinY,
Self::SinY => Self::Metaballs,
Self::Metaballs => Self::Red
}
}
}
impl Shader for TestShader {
fn draw(&self, coords: &VirtualCoordinates, frame: usize) -> RGB8 {
match self {
Self::Red => RGB8::new(255, 0, 0),
Self::Green => RGB8::new(0, 255, 0),
Self::Blue => RGB8::new(0, 0, 255),
Self::White => RGB8::new(255, 255, 255),
Self::RGB => RGB8::new(coords.x, coords.y, 255 - (coords.x / 2 + coords.y / 2)),
Self::HSV => Hsv::new(coords.x, coords.y, 255).into_rgb8(),
Self::Outline => match (coords.x, coords.y) {
(0, 0) => RGB8::new(255, 255, 255),
(0, _) => RGB8::new(255, 0, 0),
(_, 0) => RGB8::new(0, 255, 0),
(255, _) => RGB8::new(0, 0, 255),
(_, 255) => RGB8::new(0, 0, 255),
_ => RGB8::new(0, 0, 0)
},
Self::SweepX => RGB8::new(255, 0, 0),
Self::SweepY => RGB8::new(255, 0, 0),
Self::SinX => RGB8::new(sin8(coords.x.wrapping_add((frame % 255) as u8)), 0, 0),
Self::SinY => RGB8::new(sin8(coords.y.wrapping_add((frame % 255) as u8)), 0, 0),
Self::Metaballs => {
let ball_center = Coordinates::new(
scale8(64, sin8(frame)) + 128,
scale8(64, cos8(frame)) + 128
);
let dist = 255 - coords.distance_to(&ball_center);
RGB8::new(dist, 0, 0)
}
}
}
}
#[derive(Debug)]
pub struct TestPattern<T: Surface> {
surface: T,
updater: Periodically,
stepper: Periodically,
pattern: TestShader,
frame: u8
}
impl<T: Surface> TestPattern<T> {
pub fn new(surface: T) -> Self {
TestPattern { surface, updater: Periodically::new_every_n_seconds(10), stepper: Periodically::new_every_n_ms(60), pattern: TestShader::HSV, frame: 0 }
}
}
//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(self.pattern);
}
fn on_tick(&mut self, bus: &mut EventBus) {
self.updater.run(|| {
self.pattern = self.pattern.next();
log::info!("Test pattern: {:?}", self.pattern);
self.frame = 0;
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);
self.surface.set_opacity(sin8(self.frame));
self.surface.set_rect( match self.pattern {
TestShader::SweepX => Rectangle::new(
Coordinates::new(self.frame, 0),
Coordinates::new(self.frame, 255)
),
TestShader::SweepY => Rectangle::new(
Coordinates::new(0, self.frame),
Coordinates::new(255, self.frame)
),
_ => Rectangle::everything()
});
});
}
fn stop(&mut self, _bus: &mut EventBus) {
self.surface.clear_shader();
}
}

245
src/buffers.rs Normal file
View File

@ -0,0 +1,245 @@
use crate::geometry::*;
use crate::lib8::interpolate::Fract8Ops;
use crate::power::AsMilliwatts;
use crate::render::{PixelView, Sample, Shader, Surface, Surfaces, HardwarePixel};
use std::cell::RefCell;
use std::ops::IndexMut;
use std::sync::atomic::AtomicBool;
use std::sync::{Arc, Mutex};
struct ShaderBinding {
shader: Option<Box<dyn Shader>>,
rect: Rectangle<Virtual>,
opacity: u8
}
struct SurfaceUpdate {
shader: Option<Option<Box<dyn Shader>>>,
rect: Option<Rectangle<Virtual>>,
opacity: Option<u8>,
slot: usize
}
impl SurfaceUpdate {
fn merge(&mut self, mut other: Self) {
if other.shader.is_some() {
self.shader = other.shader.take()
}
if other.rect.is_some() {
self.rect = other.rect.take()
}
if other.opacity.is_some() {
self.opacity = other.opacity.take()
}
}
}
impl Default for SurfaceUpdate {
fn default() -> Self {
SurfaceUpdate {
shader: None,
rect: None,
opacity: None,
slot: usize::MAX
}
}
}
pub struct BufferedSurface {
updater: Arc<UpdateQueue>,
slot: usize
}
impl Surface for BufferedSurface {
fn clear_shader(&mut self) {
self.updater.push(SurfaceUpdate {
shader: None,
slot: self.slot,
..Default::default()
});
}
fn set_opacity(&mut self, opacity: u8) {
self.updater.push(SurfaceUpdate {
opacity: Some(opacity),
slot: self.slot,
..Default::default()
});
}
fn set_rect(&mut self, rect: Rectangle<Virtual>) {
self.updater.push(SurfaceUpdate {
rect: Some(rect),
slot: self.slot,
..Default::default()
});
}
fn set_shader<T: Shader + 'static>(&mut self, shader: T) {
self.updater.push(SurfaceUpdate {
shader: Some(Some(Box::new(shader))),
slot: self.slot,
..Default::default()
});
}
}
struct UpdateQueue {
pending: Mutex<Vec<SurfaceUpdate>>,
damaged: AtomicBool
}
impl UpdateQueue {
fn new() -> Self {
UpdateQueue {
pending: Mutex::new(Vec::new()),
damaged: AtomicBool::new(false)
}
}
fn push(&self, update: SurfaceUpdate) {
let mut locked = self.pending.lock().unwrap();
let mut existing_slot = None;
for existing in locked.iter_mut() {
if existing.slot == update.slot {
existing_slot = Some(existing);
break
}
}
match existing_slot {
Some(tgt) => {
log::debug!("Updating existing shader update");
tgt.merge(update);
}
_ => {
log::debug!("Pushing new shader update");
locked.push(update);
self.damaged.store(true, std::sync::atomic::Ordering::Relaxed);
}
}
}
}
pub struct ShaderChain {
bindings: Vec<ShaderBinding>,
updates: Arc<UpdateQueue>
}
impl ShaderChain {
pub fn new() -> Self {
ShaderChain {
bindings: Vec::new(),
updates: Arc::new(UpdateQueue::new())
}
}
pub fn is_dirty(&self) -> bool {
self.updates.damaged.load(std::sync::atomic::Ordering::Relaxed)
}
pub fn commit(&mut self) {
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);
}
}
fn new_surface(&mut self, area: Rectangle<Virtual>) -> Result<BufferedSurface, ()> {
let next_slot = self.bindings.len();
self.bindings.push(ShaderBinding {
opacity: 255,
shader: None,
rect: area
});
Ok(BufferedSurface {
updater: Arc::clone(&self.updates),
slot: next_slot
})
}
fn render_to<S: Sample>(&self, output: &mut S, frame: usize) {
for surface in &self.bindings {
let opacity = surface.opacity;
if opacity > 0 {
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);
}
}
}
}
}
}
pub struct BufferedSurfacePool {
pool: RefCell<ShaderChain>
}
impl BufferedSurfacePool {
pub fn new() -> Self {
BufferedSurfacePool {
pool: RefCell::new(ShaderChain::new())
}
}
}
impl Surfaces for BufferedSurfacePool {
type Error = ();
type Surface = BufferedSurface;
fn new_surface(&mut self, area: crate::geometry::Rectangle<crate::geometry::Virtual>) -> Result<Self::Surface, Self::Error> {
self.pool.borrow_mut().new_surface(area)
}
fn render_to<S: crate::render::Sample>(&self, output: &mut S, frame: usize) {
let mut b = self.pool.borrow_mut();
b.commit();
b.render_to(output, frame);
}
}
pub trait Pixbuf: AsMilliwatts + IndexMut<usize, Output=Self::Pixel> + Send {
type Pixel: HardwarePixel;
fn new() -> Self;
fn blank(&mut self);
fn iter_with_brightness(&self, brightness: u8) -> impl Iterator<Item = Self::Pixel> + Send;
fn pixel_count(&self) -> usize;
}
impl<T: HardwarePixel, const PIXEL_NUM: usize> Pixbuf for [T; PIXEL_NUM] {
type Pixel = T;
fn new() -> Self {
[T::default(); PIXEL_NUM]
}
fn pixel_count(&self) -> usize {
self.len()
}
fn blank(&mut self) {
self.fill(T::default())
}
fn iter_with_brightness(&self, brightness: u8) -> impl Iterator<Item=T> + Send {
self.iter().map(move |x| { x.scale8(brightness)})
}
}

View File

@ -1,126 +0,0 @@
use embedded_graphics::{
prelude::*,
pixelcolor::Rgb888,
primitives::Rectangle
};
use embedded_graphics::pixelcolor::RgbColor;
use ws2812_esp32_rmt_driver::lib_embedded_graphics::{Ws2812DrawTarget, LedPixelShape};
use running_average::RealTimeRunningAverage;
use std::io;
use crate::power;
use crate::power::AsMilliwatts;
use crate::lib8::*;
use crate::render::*;
use crate::time::Periodically;
use crate::task::Task;
use crate::geometry::*;
impl<T: RgbColor> AsMilliwatts for T {
fn as_milliwatts(&self) -> u32 {
const RED_MW : u32 = 16 * 5; //< 16mA @ 5v = 80mW
const GREEN_MW : u32 = 11 * 5; //< 11mA @ 5v = 55mW
const BLUE_MW : u32 = 15 * 5; //< 15mA @ 5v = 75mW
const DARK_MW : u32 = 1 * 5; //< 1mA @ 5v = 5mW
let red = (self.r() as u32 * RED_MW).wrapping_shr(8);
let green = (self.g() as u32 * GREEN_MW).wrapping_shr(8);
let blue = (self.b() as u32 * BLUE_MW).wrapping_shr(8);
return red + green + blue + DARK_MW;
}
}
pub struct EmbeddedDisplay<T, S>
where
T: DrawTarget,
S: Surface {
surfaces : SurfacePool<S>,
target: T,
total_mw: u32,
max_mw: u32,
fps: RealTimeRunningAverage<u32>,
frame: u32,
fps_display: Periodically
}
impl<T: LedPixelShape, S: Surface> Task for EmbeddedDisplay<Ws2812DrawTarget<'_, T>, S> {
fn start(&mut self) {
self.target.set_brightness(0);
}
fn name(&self) -> &'static str { "Renderer" }
fn tick(&mut self) {
self.start_frame();
self.render_frame();
self.end_frame();
}
}
impl<T, S> EmbeddedDisplay<T, S>
where
T: DrawTarget,
S: Surface {
pub fn new(target: T, max_mw: u32) -> Self {
EmbeddedDisplay {
surfaces: SurfacePool::new(),
target: target,
max_mw: max_mw,
total_mw: 0,
fps: RealTimeRunningAverage::default(),
frame: 0,
fps_display: Periodically::new_every_n_seconds(5)
}
}
}
impl<T, S> Surfaces<S> for EmbeddedDisplay<T, S>
where
T: DrawTarget,
S: Surface {
fn new_surface(&mut self) -> Result<S, io::Error> {
self.surfaces.new_surface()
}
}
impl<T: LedPixelShape, S: Surface> Display<S> for EmbeddedDisplay<Ws2812DrawTarget<'_, T>, S> {
fn start_frame(&mut self) {
self.total_mw = 0;
self.frame = self.frame.wrapping_add(1);
}
fn end_frame(&mut self) {
let brightness = power::brightness_for_mw(self.total_mw, 255, self.max_mw);
self.target.set_brightness(brightness);
self.target.flush().unwrap();
self.fps.insert(1);
self.fps_display.run(|| {
log::info!("FPS: {} frame={} brightness={} mw={}", self.fps.measurement(), self.frame, brightness, self.total_mw);
});
}
fn render_frame(&mut self) {
let size = T::size();
let xStride: u8 = 255 / (size.width as u8);
let yStride: u8 = 255 / (size.height as u8);
let area = Rectangle::new(Point::new(0, 0), size);
self.target.draw_iter(
area.points()
.map(|pos| {
let virtCoords = VirtualCoordinates::new(pos.x as u8 * xStride, pos.y as u8 * yStride);
let mut pixel = RGB8::new(0, 0, 0);
for surface in self.surfaces.iter() {
surface.with_shader(|shader| {
pixel = shader.draw(virtCoords.clone());
})
}
self.total_mw += pixel.as_milliwatts();
return Pixel(pos, Rgb888::new(pixel.red, pixel.green, pixel.blue));
})
).unwrap();
}
}

98
src/events.rs Normal file
View File

@ -0,0 +1,98 @@
use core::fmt::Debug;
use std::{collections::VecDeque, fmt::{Display, Result}, sync::{Arc, Mutex}};
use crate::{properties::{Properties, PropertyID, Variant}, property_namespace};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Event {
ReadyToRock,
Tick,
StartThing(&'static str),
StopThing(&'static str),
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 {
pub const fn new_tick() -> Self {
Event::Tick
}
pub fn new_property_change<T>(key: PropertyID, data: T) -> Self where Variant: From<T> {
Event::PropertyChange(key, Variant::from(data))
}
pub const fn new_ready_to_rock() -> Self {
Event::ReadyToRock
}
pub const fn new_start_thing(name: &'static str) -> Self {
Event::StartThing(name)
}
pub const fn new_stop_thing(name: &'static str) -> Self {
Event::StopThing(name)
}
}
#[derive(Clone)]
pub struct EventBus {
pending: Arc<Mutex<VecDeque<Event>>>,
props: Arc<Mutex<Properties>>
}
impl EventBus {
pub fn new() -> Self {
EventBus {
pending: Arc::new(Mutex::new(VecDeque::with_capacity(32))),
props: Arc::new(Mutex::new(Properties::new()))
}
}
pub fn next(&mut self) -> Event {
let next = self.pending.lock().unwrap().pop_front().unwrap_or(Event::new_tick());
match next {
Event::PropertyChange(key, value) => {
if self.props.lock().unwrap().set(key, value.clone()) {
log::trace!("prop-update key={:?} value={:?}", key, value);
Event::PropertyChange(key, value)
} else {
Event::new_tick()
}
},
_ => next
}
}
pub fn push(&mut self, event: Event) {
self.pending.lock().unwrap().push_back(event);
}
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
);

View File

@ -1,53 +1,140 @@
use std::marker::PhantomData;
use core::fmt::{Debug, Formatter};
use core::ops::{Mul, Sub, Add};
use num::{One, pow, integer::Roots};
use core::cmp::{min, max};
pub trait CoordinateSpace {}
pub trait CoordinateOp: PartialOrd + PartialEq + Sub + Clone + Mul + Copy + One + Add + Eq + Debug where
Self: Sub<Output=Self> {
const MIN: Self;
const MAX: Self;
fn distance(x1: Self, y1: Self, x2: Self, y2: Self) -> Self;
}
pub trait Coordinates<T, S: CoordinateSpace> {
fn x(&self) -> T;
fn y(&self) -> T;
fn new(x: T, y: T) -> Self;
pub trait CoordinateSpace {
type Data: CoordinateOp;
}
const MAX: T;
const MIN: T;
#[derive(PartialEq, Eq, Clone, Copy, PartialOrd, Ord)]
pub struct Coordinates<S: CoordinateSpace> {
pub x: S::Data,
pub y: S::Data,
}
impl<S: CoordinateSpace> Debug for Coordinates<S> where S::Data: Debug {
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
f.debug_tuple("@")
.field(&self.x)
.field(&self.y)
.finish()
}
}
impl CoordinateOp for u8 {
const MIN: u8 = 0;
const MAX: u8 = 255;
fn distance(x1: Self, y1: Self, x2: Self, y2: Self) -> Self {
(max(x2, x1) - min(x2, x1)).saturating_add(max(y2, y1) - min(y2, y1))
}
}
impl CoordinateOp for u16 {
const MIN: u16 = u16::MIN;
const MAX: u16 = u16::MAX;
fn distance(x1: Self, y1: Self, x2: Self, y2: Self) -> Self {
(pow(x2 - x1, 2) + pow(y2 - y1, 2)).sqrt()
}
}
impl CoordinateOp for usize {
const MIN: usize = usize::MIN;
const MAX: usize = usize::MAX;
fn distance(x1: Self, y1: Self, x2: Self, y2: Self) -> Self {
(pow(x2 - x1, 2) + pow(y2 - y1, 2)).sqrt()
}
}
impl<S: CoordinateSpace> Coordinates<S> {
pub const fn new(x: S::Data, y: S::Data) -> Self {
Self {
x,
y
}
}
const fn top_left() -> Self {
Self::new(S::Data::MIN, S::Data::MIN)
}
const fn top_right() -> Self {
Self::new(S::Data::MAX, S::Data::MIN)
}
const fn bottom_left() -> Self {
Self::new(S::Data::MIN, S::Data::MAX)
}
const fn bottom_right() -> Self {
Self::new(S::Data::MAX, S::Data::MAX)
}
pub fn distance_to(&self, other: &Self) -> S::Data {
S::Data::distance(self.x, other.x, self.y, other.y)
}
}
#[derive(PartialEq, Debug, Copy, Clone)]
pub struct Virtual {}
impl CoordinateSpace for Virtual {}
#[derive(PartialEq, Debug, Copy, Clone)]
pub struct Physical {}
impl CoordinateSpace for Physical {}
#[derive(PartialEq, Debug, Copy, Clone)]
pub struct Coord8<S: CoordinateSpace> {
x: u8,
y: u8,
space: PhantomData<S>
impl CoordinateSpace for Virtual {
type Data = u8;
}
pub type VirtualCoordinates = Coord8<Virtual>;
pub type PhysicalCoordinates = Coord8<Physical>;
pub type VirtualCoordinates = Coordinates<Virtual>;
impl<S> Coordinates<u8, S> for Coord8<S>
where
S: CoordinateSpace {
fn new(x: u8, y: u8) -> Self {
#[derive(PartialEq, Eq, Copy, Clone, Debug, PartialOrd)]
pub struct Rectangle<Space: CoordinateSpace> {
pub top_left: Coordinates<Space>,
pub bottom_right: Coordinates<Space>
}
impl<Space: CoordinateSpace> Rectangle<Space> {
pub const fn new(top_left: Coordinates<Space>, bottom_right: Coordinates<Space>) -> Self {
Self {
x: x,
y: y,
space: PhantomData
top_left,
bottom_right
}
}
fn x(&self) -> u8 {
self.x
pub const fn everything() -> Self {
Self {
top_left: Coordinates::<Space>::top_left(),
bottom_right: Coordinates::<Space>::bottom_right()
}
}
fn y(&self) -> u8 {
self.y
pub fn width(&self) -> Space::Data {
self.bottom_right.x - self.top_left.x
}
const MAX: u8 = 255;
const MIN: u8 = 255;
pub fn height(&self) -> Space::Data {
self.bottom_right.y - self.top_left.y
}
pub const fn left(&self) -> Space::Data {
self.top_left.x
}
pub const fn top(&self) -> Space::Data {
self.top_left.y
}
pub const fn right (&self) -> Space::Data {
self.bottom_right.x
}
pub const fn bottom(&self) -> Space::Data {
self.bottom_right.y
}
}

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

@ -1,72 +0,0 @@
use palette::convert::FromColorUnclamped;
use palette::encoding::srgb::Srgb;
use palette::Hsv;
use embedded_graphics::pixelcolor::RgbColor;
use embedded_graphics::pixelcolor::PixelColor;
use embedded_graphics::pixelcolor::raw::RawU8;
#[derive(PartialEq, Debug, Copy, Clone)]
pub struct RGB8 {
pub red: u8,
pub green: u8,
pub blue: u8
}
impl RGB8 {
pub const fn new(red : u8, green : u8, blue : u8) -> Self {
Self {
red: red,
green: green,
blue: blue
}
}
}
impl RgbColor for RGB8 {
fn r(&self) -> u8 { self.red }
fn g(&self) -> u8 { self.green }
fn b(&self) -> u8 { self.blue }
const MAX_R: u8 = 255;
const MAX_G: u8 = 255;
const MAX_B: u8 = 255;
const BLACK: Self = Self::new(0, 0, 0);
const WHITE: Self = Self::new(255, 255, 255);
const RED: Self = Self::new(255, 0, 0);
const GREEN: Self = Self::new(0, 255, 0);
const BLUE: Self = Self::new(0, 0, 255);
const YELLOW: Self = Self::new(255, 0, 0);
const CYAN: Self = Self::new(0, 255, 0);
const MAGENTA: Self = Self::new(0, 0, 255);
}
impl PixelColor for RGB8 {
type Raw = RawU8;
}
impl FromColorUnclamped<Hsv<Srgb, u8>> for RGB8 {
fn from_color_unclamped(hsv: Hsv<Srgb, u8>) -> RGB8 {
if hsv.saturation == 0 {
return RGB8::new(hsv.value, hsv.value, hsv.value);
}
let region = hsv.hue.into_inner() / 43;
let remainder = (hsv.hue.into_inner() - (region * 43)) * 6;
let p = hsv.value.wrapping_mul(255 - hsv.saturation).wrapping_shr(8);
let q = (hsv.value.wrapping_mul(255 - ((hsv.saturation.wrapping_mul(remainder)).wrapping_shr(8)))).wrapping_shr(8);
let t = (hsv.value.wrapping_mul(255 - ((hsv.saturation.wrapping_mul(255 - remainder)).wrapping_shr(8)))).wrapping_shr(8);
match region {
0 => RGB8::new(hsv.value, t, p),
1 => RGB8::new(q, hsv.value, p),
2 => RGB8::new(p, hsv.value, t),
3 => RGB8::new(p, q, hsv.value),
4 => RGB8::new(t, p, hsv.value),
_ => RGB8::new(hsv.value, p, q)
}
}
}

130
src/lib8/interpolate.rs Normal file
View File

@ -0,0 +1,130 @@
use num::PrimInt;
use core::ops::BitOr;
use rgb::Rgb;
pub type Fract8 = u8;
pub trait Fract8Ops {
fn scale8(self, scale: Fract8) -> Self;
fn blend8(self, other: Self, scale: Fract8) -> Self;
}
impl Fract8Ops for u8 {
fn scale8(self, scale: Fract8) -> Self {
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 {
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
}
}
}
impl Fract8Ops for Rgb<u8> {
fn scale8(self, scale: Fract8) -> Self {
Rgb::new(
self.r.scale8(scale),
self.g.scale8(scale),
self.b.scale8(scale)
)
}
fn blend8(self, other: Self, scale: Fract8) -> Self {
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)
)
}
}
}
}
pub fn scale8<T: Fract8Ops>(i: T, scale: Fract8) -> T {
i.scale8(scale)
}
pub fn avg7(i: i8, j: i8) -> i8 {
i.unsigned_shr(1).wrapping_add(j.unsigned_shr(1)).wrapping_add(i & 0x1)
}
pub fn grad8(hash: u8, x: i8, y: i8) -> i8 {
let mut u: i8;
let mut v: i8;
if hash & 4 != 0 {
u = y; v = x;
} else {
u = x; v = y;
}
if hash & 1 != 0 {
u = u.wrapping_neg();
}
if hash & 2 != 0 {
v = v.wrapping_neg();
}
return avg7(u, v);
}
pub fn lerp7by8(a: i8, b: i8, frac: u8) -> i8 {
if b > a {
let delta: u8 = b.wrapping_sub(a) as u8;
let scaled: u8 = scale8(delta, frac);
return a.wrapping_add(scaled as i8);
} else {
let delta: u8 = a.wrapping_sub(b) as u8;
let scaled: u8 = scale8(delta, frac);
return a.wrapping_sub(scaled as i8);
}
}
pub fn lerp8by8(a: u8, b: u8, frac: u8) -> u8 {
if b > a {
let delta = b - a;
let scaled = scale8(delta, frac);
return a + scaled;
} else {
let delta = a - b;
let scaled = scale8(delta, frac);
return a - scaled;
}
}
pub fn map8(x: u8, range_start: u8, range_end: u8) -> u8 {
let range_width = range_end - range_start;
let mut out = scale8(x, range_width);
out += range_start;
return out;
}
pub fn ease8InOutQuad(i: u8) -> u8 {
let j = if i & 0x80 != 0 {
255 - i
} else {
i
};
let jj = scale8(j, j);
let jj2 = jj.unsigned_shl(1);
if i & 0x80 == 0 {
return jj2
} else {
return 255 - jj2;
}
}

69
src/lib8/mod.rs Normal file
View File

@ -0,0 +1,69 @@
pub mod interpolate;
pub mod noise;
pub mod trig;
use rgb::Rgb;
use crate::lib8::interpolate::scale8;
pub trait IntoRgb8 {
fn into_rgb8(self) -> Rgb<u8>;
}
impl IntoRgb8 for Rgb<u8> {
fn into_rgb8(self) -> Rgb<u8> {
self
}
}
pub struct Hsv {
pub hue: u8,
pub saturation: u8,
pub value: u8
}
impl Hsv {
pub fn new(hue: u8, saturation: u8, value: u8) -> Self {
Hsv {
hue,
saturation,
value
}
}
}
impl IntoRgb8 for Hsv {
//TODO: Borrowed from FastLED
fn into_rgb8(self) -> Rgb<u8> {
const HSV_SECTION_3: u8 = 0x40;
if self.saturation == 0 {
return Rgb::new(self.value, self.value, self.value)
}
let mock_hue = scale8(191, self.hue);
let value: u8 = self.value;
let saturation: u8 = self.saturation;
let invsat: u8 = 255 - saturation;
let brightness_floor: u8 = (value as u16 * invsat as u16 / 256) as u8;
let color_amplitude: u8 = value - brightness_floor;
let section: u8 = mock_hue / HSV_SECTION_3;
let offset: u8 = mock_hue % HSV_SECTION_3;
let rampup: u8 = offset;
let rampdown: u8 = (HSV_SECTION_3 - 1) - offset;
let rampup_amp_adj: u8 = (rampup as u16 * color_amplitude as u16 / 64) as u8;
let rampdown_amp_adj: u8 = (rampdown as u16 * color_amplitude as u16 / 64) as u8;
let rampup_adj_with_floor: u8 = rampup_amp_adj.saturating_add(brightness_floor);
let rampdown_adj_with_floor: u8 = rampdown_amp_adj.saturating_add(brightness_floor);
match section {
1 => Rgb::new(brightness_floor, rampdown_adj_with_floor, rampup_adj_with_floor),
0 => Rgb::new(rampdown_adj_with_floor, rampup_adj_with_floor, brightness_floor),
_ => Rgb::new(rampup_adj_with_floor, brightness_floor, rampdown_adj_with_floor)
}
}
}

57
src/lib8/noise.rs Normal file
View File

@ -0,0 +1,57 @@
use crate::lib8::interpolate::*;
const NOISE_CUBE: [u8; 257] = [
151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7, 225,
140, 36, 103, 30, 69, 142, 8, 99, 37, 240, 21, 10, 23, 190, 6, 148,
247, 120, 234, 75, 0, 26, 197, 62, 94, 252, 219, 203, 117, 35, 11, 32,
57, 177, 33, 88, 237, 149, 56, 87, 174, 20, 125, 136, 171, 168, 68, 175,
74, 165, 71, 134, 139, 48, 27, 166, 77, 146, 158, 231, 83, 111, 229, 122,
60, 211, 133, 230, 220, 105, 92, 41, 55, 46, 245, 40, 244, 102, 143, 54,
65, 25, 63, 161, 1, 216, 80, 73, 209, 76, 132, 187, 208, 89, 18, 169,
200, 196, 135, 130, 116, 188, 159, 86, 164, 100, 109, 198, 173, 186, 3, 64,
52, 217, 226, 250, 124, 123, 5, 202, 38, 147, 118, 126, 255, 82, 85, 212,
207, 206, 59, 227, 47, 16, 58, 17, 182, 189, 28, 42, 223, 183, 170, 213,
119, 248, 152, 2, 44, 154, 163, 70, 221, 153, 101, 155, 167, 43, 172, 9,
129, 22, 39, 253, 19, 98, 108, 110, 79, 113, 224, 232, 178, 185, 112, 104,
218, 246, 97, 228, 251, 34, 242, 193, 238, 210, 144, 12, 191, 179, 162, 241,
81, 51, 145, 235, 249, 14, 239, 107, 49, 192, 214, 31, 181, 199, 106, 157,
184, 84, 204, 176, 115, 121, 50, 45, 127, 4, 150, 254, 138, 236, 205, 93,
222, 114, 67, 29, 24, 72, 243, 141, 128, 195, 78, 66, 215, 61, 156, 180,
151];
const fn get_cube(x: u8) -> u8 {
NOISE_CUBE[x as usize]
}
fn inoise8_raw(x_src: u16, y_src: u16) -> i8 {
let x = x_src.wrapping_shr(8) as u8;
let y = y_src.wrapping_shr(8) as u8;
let a = get_cube(x) + y;
let aa = get_cube(a);
let ab = get_cube(a+1);
let b = get_cube(x+1) + y;
let ba = get_cube(b);
let bb = get_cube(b+1);
let mut u = x_src as u8;
let mut v = y_src as u8;
let xx = ((x_src as u8).wrapping_shr(1) & 0x7f) as i8;
let yy = ((y_src as u8).wrapping_shr(1) & 0x7f) as i8;
let n = 0x80u8 as i8;
u = ease8InOutQuad(u);
v = ease8InOutQuad(v);
let x1 = lerp7by8(grad8(get_cube(aa), xx, yy), grad8(get_cube(ba), xx.wrapping_sub(n), yy), u);
let x2 = lerp7by8(grad8(get_cube(ab), xx, yy.wrapping_sub(n)), grad8(get_cube(bb), xx.wrapping_sub(n), yy.wrapping_sub(n)), u);
return lerp7by8(x1, x2, v);
}
pub fn inoise8(x: i16, y: i16) -> u8 {
let mut n = inoise8_raw(x as u16, y as u16);
n = n.wrapping_add(64);
return (n as u8).saturating_add(n as u8);
}

57
src/lib8/trig.rs Normal file
View File

@ -0,0 +1,57 @@
use num::PrimInt;
const B_M16_INTERLEAVE: [u8; 8] = [0, 49, 49, 41, 90, 27, 117, 10];
pub trait Trig8 {
fn sin8(self) -> u8;
fn cos8(self) -> u8;
}
impl Trig8 for u8 {
fn sin8(self) -> u8 {
let mut offset: u8 = self;
if self & 0x40 != 0 {
offset = 255 - offset;
}
offset &= 0x3f;
let mut secoffset: u8 = offset & 0x0f;
if self & 0x40 != 0 {
secoffset += 1;
}
let section: u8 = offset.unsigned_shr(4);
let s2: u8 = section * 2;
let b: u8 = B_M16_INTERLEAVE[s2 as usize];
let m16: u8 = B_M16_INTERLEAVE[s2 as usize + 1];
let mx: u8 = m16.wrapping_mul(secoffset).unsigned_shr(4);
let mut y: i8 = mx as i8 + b as i8;
if self & 0x80 != 0 {
y = -y;
}
y = y.wrapping_add(128u8 as i8);
return y as u8;
}
fn cos8(self) -> u8 {
sin8(self.wrapping_add(64))
}
}
impl Trig8 for usize {
fn sin8(self) -> u8 {
((self % 255) as u8).sin8()
}
fn cos8(self) -> u8 {
((self % 255) as u8).cos8()
}
}
pub fn sin8<T: Trig8>(theta: T) -> u8 {
theta.sin8()
}
pub fn cos8<T: Trig8>(theta: T) -> u8 {
theta.cos8()
}

View File

@ -1,87 +1,107 @@
use palette::Hsv;
use palette::convert::IntoColorUnclamped;
use ws2812_esp32_rmt_driver::lib_smart_leds::Ws2812Esp32Rmt;
use ws2812_esp32_rmt_driver::lib_embedded_graphics::Ws2812DrawTarget;
mod power;
mod lib8;
mod render;
mod task;
mod time;
mod geometry;
mod embedded_graphics_lib;
mod smart_leds_lib;
mod platform;
mod animations;
mod mappings;
mod buffers;
mod scenes;
mod inputs;
mod events;
mod properties;
use crate::time::Periodically;
use crate::geometry::{Coordinates, VirtualCoordinates};
use crate::render::{Shader, Surfaces, Surface, SimpleSurface};
use crate::task::Task;
use crate::platform::{DisplayInit, PonderjarTarget, SPIDisplay};
use events::*;
use inputs::circadian::CircadianRhythm;
use platform::esp32::mqtt::props::MQTT;
use render::props::Output;
use scenes::Sequencer;
struct IdleTask<T: Surface> {
frame: u8,
surface: T,
updater: Periodically
}
struct IdleShader {
frame: u8
}
impl Shader for IdleShader {
fn draw(&self, coords: VirtualCoordinates) -> lib8::RGB8 {
Hsv::new_srgb(self.frame.wrapping_add(coords.x()).wrapping_add(coords.y()), 255, 255).into_color_unclamped()
}
}
impl<T: Surface> IdleTask<T> {
fn new(surface: T) -> Self {
IdleTask {
frame: 0,
surface: surface,
updater: Periodically::new_every_n_ms(16)
}
}
}
impl<T: Surface> Task for IdleTask<T> {
fn name(&self) -> &'static str { "Idle" }
fn tick(&mut self) {
self.updater.run(|| {
self.frame = self.frame.wrapping_add(1);
self.surface.set_shader(Box::new(IdleShader { frame: self.frame }));
})
}
fn stop(&mut self) {
self.surface.clear_shader();
}
}
use crate::events::EventBus;
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() {
// 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();
let mut board: DefaultBoard = Board::take();
// Bind the log crate to the ESP Logging facilities
esp_idf_svc::log::EspLogger::initialize_default();
log::info!("🐛 Booting Renderbug!");
log::info!("Setting up display");
//let mut display = SPIDisplay::new_display::<SimpleSurface>();
//let mut display = PonderjarTarget::new_display::<SimpleSurface>();
let mut display = Ws2812Esp32Rmt::new_display::<SimpleSurface>();
log::info!("📡 Board {}", core::any::type_name_of_val(&board));
log::info!("Creating runner");
let mut runner = task::Scheduler::new(vec![
Box::new(IdleTask::new(display.new_surface().unwrap())),
Box::new(display),
log::info!("⚙️ Creating tasks");
let mut system = board.system_tasks();
log::info!("⏰ System scheduler: {}", core::any::type_name_of_val(&system));
log::info!("💡 Creating output");
let output = board.output();
log::info!("Output: {}", core::any::type_name_of_val(&output));
log::info!("🎨 Preparing surfaces");
let mut surfaces = board.surfaces();
log::info!("Surface implementation: {}", core::any::type_name_of_val(&output));
log::info!("🌌 Creating animations");
let mut animations = FixedSizeScheduler::new([
Box::new(animations::IdleTask::new(&mut surfaces)),
Box::new(animations::test::TestPattern::new(surfaces.new_surface(Rectangle::everything()).unwrap())),
]);
log::info!("Ready to rock and roll");
let mut inputs = FixedSizeScheduler::new([
Box::new(CircadianRhythm::new()),
Box::new(Sequencer::new()),
]);
let mut renderer = FixedSizeScheduler::new([Box::new(Renderer::new(output, surfaces))]);
log::info!("🚌 Starting event bus");
let mut bus = EventBus::new();
{
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::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));
log::info!("+ {}", task_name);
}
bus.push(Event::new_ready_to_rock());
log::info!("🚀 Launching...");
loop {
runner.tick();
let next_event = bus.next();
match next_event {
events::Event::Tick => (),
Event::ReadyToRock => {
log::info!("🚀 Ready to rock and roll");
}
_ => log::info!("⚡ Event: {}", next_event)
}
inputs.tick(&next_event, &mut bus);
animations.tick(&next_event, &mut bus);
system.tick(&next_event, &mut bus);
renderer.tick(&next_event, &mut bus);
}
}

366
src/mappings.rs Normal file
View File

@ -0,0 +1,366 @@
use crate::buffers::Pixbuf;
use crate::geometry::*;
use crate::lib8::interpolate::scale8;
use crate::render::PixelView;
use core::cmp::{max, min};
use core::fmt::{Formatter, Debug};
pub trait CoordinateView<'a>: Debug {
type Space: CoordinateSpace;
fn next(&mut self) -> Option<(Coordinates<Virtual>, Coordinates<Self::Space>)>;
}
pub trait Select<'a> {
type Space: CoordinateSpace;
type View: CoordinateView<'a>;
fn select(&'a self, rect: &Rectangle<Virtual>) -> Self::View;
}
#[derive(Debug)]
pub struct LinearCoordView {
max_x: u8,
idx: usize,
}
pub struct LinearSpace {}
impl CoordinateSpace for LinearSpace {
type Data = usize;
}
pub type LinearCoords = Coordinates<LinearSpace>;
impl<'a> CoordinateView<'a> for LinearCoordView {
type Space = LinearSpace;
fn next(&mut self) -> Option<(VirtualCoordinates, LinearCoords)> {
if self.idx as u8 == self.max_x {
None
} else {
let virt = VirtualCoordinates::new(self.idx as u8, 0); // FIXME: scale8
let phys = LinearCoords::new(
self.idx as usize,
0
);
self.idx += 1;
return Some((virt, phys))
}
}
}
pub struct LinearPixelMapping {
}
impl LinearPixelMapping {
pub fn new() -> Self {
Self {}
}
}
impl<'a> Select<'a> for LinearPixelMapping {
type Space = LinearSpace;
type View = LinearCoordView;
fn select(&'a self, rect: &Rectangle<Virtual>) -> Self::View {
LinearCoordView {
max_x: rect.bottom_right.x,
idx: 0,
}
}
}
#[derive(Default, Clone, Copy, Debug, PartialEq, Eq)]
pub struct Stride {
pub length: u8,
pub x: u8,
pub y: u8,
pub reverse: bool,
pub physical_idx: usize
}
impl Stride {
pub const fn pixel_idx_for_offset(&self, offset: u8) -> usize {
if self.reverse {
self.physical_idx + (self.length + self.y - 1 - offset) as usize
} else {
self.physical_idx + offset as usize
}
}
}
#[derive(Debug)]
pub struct StrideMapping<const STRIDE_NUM: usize = 24> {
pub strides: [Stride; STRIDE_NUM],
pub pixel_count: usize,
pub size: Rectangle<StrideSpace>
}
impl<const STRIDE_NUM: usize> StrideMapping<STRIDE_NUM> {
pub fn new() -> Self {
Self::from_json(&[
(0, 0, 255, false)
])
}
pub fn new_fairylights() -> Self {
Self::from_json(&[
(0, 0, 50, false)
])
}
pub fn new_cyberplague() -> Self {
Self::from_json(&[
(0, 6, 6, false),
(1, 6, 6, true),
(2, 6, 6, false),
(3, 4, 9, true),
(4, 4, 14, false),
(5, 0, 17, true),
(6, 2, 12, false),
(7, 0, 18, true),
(8, 4, 14, false),
(9, 5, 9, true),
(10, 4, 7, false),
(11, 5, 6, true),
(12, 5, 6, false)
])
}
pub fn new_jar() -> Self {
Self::from_json(&[
(0, 0, 17, false),
(1, 0, 17, false),
(2, 0, 17, false),
(3, 0, 17, false),
(4, 0, 16, false),
(5, 0, 17, false),
(6, 0, 17, false),
(7, 0, 17, false),
(8, 0, 17, false),
(9, 0, 17, false),
(10, 0, 17, false),
(11, 0, 17, false),
(12, 0, 18, false),
(13, 0, 17, false),
(14, 0, 18, false),
(15, 0, 17, false),
(16, 0, 17, false),
(17, 0, 17, false)
])
}
pub fn new_panel() -> Self {
Self::from_json(&[
(0, 0, 16, false),
(1, 0, 16, true),
(2, 0, 16, false),
(3, 0, 16, true),
(4, 0, 16, false),
(5, 0, 16, true),
(6, 0, 16, false),
(7, 0, 16, true),
(8, 0, 16, false),
(9, 0, 16, true),
(10, 0, 16, false),
(11, 0, 16, true),
(12, 0, 16, false),
(13, 0, 16, true),
(14, 0, 16, false),
(15, 0, 16, true),
])
}
pub fn new_albus() -> Self {
Self::from_json(&[
(0, 0, 29, false),
(1, 0, 20, false),
(2, 0, 22, false),
(3, 0, 19, false),
(4, 0, 12, false),
(5, 0, 14, false),
(6, 0, 16, false),
(7, 0, 19, false),
])
}
pub fn from_json(stride_json: &[(u8, u8, u8, bool)]) -> Self {
let mut strides = [Stride::default(); STRIDE_NUM];
let stride_count = stride_json.len();
let mut physical_idx = 0;
let mut size: Option<Rectangle<StrideSpace>> = None;
for stride_idx in 0..stride_count {
let json_data = stride_json[stride_idx];
let x = json_data.0;
let y = json_data.1;
let length = json_data.2;
let reverse = json_data.3;
strides[stride_idx] = Stride {
length,
x,
y,
reverse,
physical_idx
};
physical_idx += length as usize;
size = Some(match size.take() {
None => Rectangle::new(
Coordinates::new(x, y),
Coordinates::new(x, y + length - 1),
),
Some(s) => Rectangle::new(
Coordinates::new(
min(s.top_left.x, x),
min(s.top_left.y, y)
),
Coordinates::new(
max(s.bottom_right.x, x),
max(s.bottom_right.y, y + length - 1)
)
)
});
log::info!("stride={:?} size={:?}", strides[stride_idx], size);
}
let s = size.take().unwrap();
log::info!("size={:?}", s);
Self {
strides,
pixel_count: physical_idx,
size: s
}
}
}
impl<'a> Select<'a> for StrideMapping {
type Space = StrideSpace;
type View = StrideView<'a>;
fn select(&'a self, rect: &Rectangle<Virtual>) -> Self::View {
StrideView::new(self, rect)
}
}
#[derive(Debug, Clone, Copy)]
pub struct StrideSpace {}
impl CoordinateSpace for StrideSpace {
type Data = u8;
}
pub type StrideCoords = Coordinates<StrideSpace>;
pub struct StrideView<'a> {
pub map: &'a StrideMapping,
range: Rectangle<StrideSpace>,
cur: StrideCoords,
step_size: VirtualCoordinates
}
impl<'a> Debug for StrideView<'a> {
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
f.debug_struct("StrideView")
.field("range", &self.range)
.field("step", &self.step_size)
.field("cur", &self.cur).finish()
}
}
impl<'a> StrideView<'a> {
fn new(map: &'a StrideMapping, rect: &Rectangle<Virtual>) -> Self {
// Zero-index shape of the pixel picking area
let range: Rectangle<StrideSpace> = Rectangle::new(
Coordinates::new(
scale8(map.size.width() as u8, rect.top_left.x) + map.size.left() as u8,
scale8(map.size.height() as u8, rect.top_left.y) + map.size.top() as u8
),
Coordinates::new(
scale8(map.size.width() as u8, rect.bottom_right.x) + map.size.left() as u8,
scale8(map.size.height() as u8, rect.bottom_right.y) + map.size.top() as u8
)
);
//log::info!("rect={:?} map.size={:?} range={:?}", rect, map.size, range);
debug_assert!(
range.bottom_right.x <= map.size.width() as u8 &&
range.bottom_right.y <= map.size.height() as u8,
"the range for this view is out of bounds range={:?} rect={:?}, map_size={:?}",
range,
rect,
(map.size.width(), map.size.height())
);
let step_size = VirtualCoordinates::new(
u8::MAX / core::cmp::max(1, range.width()),
u8::MAX / core::cmp::max(1, range.height())
//scale8(255, std::cmp::max(1, range.bottom_right.x - range.top_left.x)),
//scale8(255, std::cmp::max(1, range.bottom_right.y - range.top_left.y))
);
debug_assert_ne!(step_size.x, 0);
debug_assert_ne!(step_size.y, 0);
return Self {
map,
range,
step_size,
cur: range.top_left
};
}
}
impl<'a> CoordinateView<'a> for StrideView<'a> {
type Space = StrideSpace;
fn next(&mut self) -> Option<(VirtualCoordinates, StrideCoords)> {
// Keep scanning until we reach the far right of the range
while self.cur.x <= self.range.bottom_right.x {
debug_assert!((self.cur.x as usize) < self.map.strides.len(), "stride out of bounds {:?}", self);
let cur_stride: &Stride = &self.map.strides[self.cur.x as usize];
// Skip ahead to the top of the current stride if we are starting from higher above.
if self.cur.y < cur_stride.y {
self.cur.y = cur_stride.y;
}
// If we are at the bottom of our rectangle, or our stride, go to the next stride.
if self.cur.y > self.range.bottom_right.y || self.cur.y > cur_stride.y + cur_stride.length - 1 {
self.cur.x += 1;
self.cur.y = self.range.top_left.y;
continue;
}
// By now, we must be safely somewhere inside our current stride
debug_assert!(self.cur.y <= cur_stride.y + cur_stride.length - 1, "coords={:?} out of bounds for stride={:?} view={:?}", self.cur, cur_stride, self);
// Move to the next coord and return
let physical_coords = self.cur;
self.cur.y += 1;
let virtual_coords = VirtualCoordinates::new(
(physical_coords.x as u8).saturating_mul(self.step_size.x),
(physical_coords.y as u8).saturating_mul(self.step_size.y)
);
return Some((virtual_coords, physical_coords));
}
None
}
}
pub struct StrideSampler<'a, P: Pixbuf> {
pixbuf: &'a mut P,
selection: StrideView<'a>
}
impl<'a, P: Pixbuf> StrideSampler<'a, P> {
pub fn new(pixbuf: &'a mut P, selection: StrideView<'a>) -> Self {
StrideSampler {
pixbuf,
selection
}
}
}
impl<'a, P: Pixbuf> PixelView for StrideSampler<'a, P> {
type Pixel = P::Pixel;
fn next(&mut self) -> Option<(Coordinates<Virtual>, &mut Self::Pixel)> {
if let Some((virt, coords)) = self.selection.next() {
let idx = self.selection.map.strides[coords.x as usize].pixel_idx_for_offset(coords.y);
Some((virt, &mut self.pixbuf[idx]))
} else {
None
}
}
}

View File

@ -1,102 +0,0 @@
use esp_idf_svc::hal::{
prelude::*,
gpio::AnyIOPin,
spi::{
config::{Config, DriverConfig},
Dma,
SpiBusDriver,
SpiDriver,
}
};
use ws2812_spi::Ws2812;
use ws2812_esp32_rmt_driver::lib_smart_leds::Ws2812Esp32Rmt;
use ws2812_esp32_rmt_driver::lib_embedded_graphics::{LedPixelShape, Ws2812DrawTarget};
use embedded_graphics::prelude::{Size, Point};
use crate::smart_leds_lib::SmartLedDisplay;
use crate::embedded_graphics_lib::EmbeddedDisplay;
use crate::render::{Surface, Display};
use crate::task::Task;
pub trait DisplayInit {
fn new_display<S: Surface>() -> impl Display<S> + Task;
}
impl DisplayInit for Ws2812Esp32Rmt<'_> {
fn new_display<S: Surface>() -> impl Display<S> + Task {
let peripherals = Peripherals::take().unwrap();
let led_pin = peripherals.pins.gpio14;
let channel = peripherals.rmt.channel0;
const POWER_VOLTS : u32 = 5;
const POWER_MA : u32 = 500;
const MAX_POWER_MW : u32 = POWER_VOLTS * POWER_MA;
let target = Self::new(channel, led_pin).unwrap();
return SmartLedDisplay::new(target, MAX_POWER_MW);
}
}
impl<Shape: LedPixelShape> DisplayInit for Ws2812DrawTarget<'_, Shape> {
fn new_display<S: Surface>() -> impl Display<S> + Task {
let peripherals = Peripherals::take().unwrap();
let led_pin = peripherals.pins.gpio14;
let channel = peripherals.rmt.channel0;
const POWER_VOLTS : u32 = 5;
const POWER_MA : u32 = 500;
const MAX_POWER_MW : u32 = POWER_VOLTS * POWER_MA;
let target = Self::new(channel, led_pin).unwrap();
return EmbeddedDisplay::<Self, S>::new(target, MAX_POWER_MW);
}
}
pub struct SPIDisplay {}
impl DisplayInit for SPIDisplay {
fn new_display<S: Surface>() -> impl Display<S> + Task {
let peripherals = Peripherals::take().unwrap();
let driver = SpiDriver::new_without_sclk(
peripherals.spi2,
peripherals.pins.gpio14,
Option::<AnyIOPin>::None,
&DriverConfig::new().dma(Dma::Auto(512))
).unwrap();
let cfg = Config::new().baudrate(3_200.kHz().into());
let spi = SpiBusDriver::new(driver, &cfg).unwrap();
const POWER_VOLTS : u32 = 5;
const POWER_MA : u32 = 500;
const MAX_POWER_MW : u32 = POWER_VOLTS * POWER_MA;
let target = Ws2812::new(spi);
return SmartLedDisplay::new(target, MAX_POWER_MW)
}
}
pub struct PonderjarMatrix {}
impl LedPixelShape for PonderjarMatrix {
fn size() -> Size {
Size::new(17, 17)
}
fn pixel_index(point: Point) -> Option<usize> {
if (0..Self::size().width as i32).contains(&point.x) && (0..Self::size().height as i32).contains(&point.y) {
if point.y % 2 == 0 {
Some((point.y as u32 * Self::size().width as u32 + point.x as u32).try_into().unwrap())
} else {
Some((point.y as u32 * Self::size().width as u32 - point.x as u32).try_into().unwrap())
}
} else {
None
}
}
}
pub type PonderjarTarget<'a> = Ws2812DrawTarget<'a, PonderjarMatrix>;

View File

@ -0,0 +1,145 @@
use embedded_graphics::{
prelude::*,
pixelcolor::Rgb888,
primitives::Rectangle
};
use rgb::RGB8;
use ws2812_esp32_rmt_driver::lib_embedded_graphics::{Ws2812DrawTarget, LedPixelShape};
use esp_idf_svc::hal::prelude::Peripherals;
use std::io;
use std::fmt::{Formatter, Debug};
use crate::power;
use crate::power::AsMilliwatts;
use crate::lib8::*;
use crate::render::*;
use crate::geometry::*;
use crate::platform::DisplayInit;
pub struct EmbeddedDisplay<T, S>
where
T: DrawTarget,
S: Surface {
surfaces : SurfacePool<S>,
target: T,
total_mw: u32,
max_mw: u32,
}
impl<T: DrawTarget, S: Surface> Debug for EmbeddedDisplay<T, S> {
fn fmt(&self, f: &mut Formatter) -> Result<(), std::fmt::Error> {
f.debug_struct("EmbeddedDisplay")
.field("total_mw", &self.total_mw)
.field("surfaces", &self.surfaces)
.finish()
}
}
trait TargetInit<T: DrawTarget> {
fn setup_target(mut target: T) -> T { target }
}
impl<T, S> EmbeddedDisplay<T, S>
where
Self: TargetInit<T>,
T: DrawTarget,
S: Surface {
pub fn new(target: T, max_mw: u32) -> Self {
EmbeddedDisplay {
surfaces: SurfacePool::new(),
target: Self::setup_target(target),
max_mw: max_mw,
total_mw: 0,
}
}
}
impl<T: LedPixelShape, S: Surface> TargetInit<Ws2812DrawTarget<'_, T>> for EmbeddedDisplay<Ws2812DrawTarget<'_, T>, S> {
fn setup_target(mut target: Ws2812DrawTarget<'_, T>) -> Ws2812DrawTarget<'_, T> {
target.set_brightness(0);
target
}
}
impl<T, S> Surfaces<S> for EmbeddedDisplay<T, S>
where
T: DrawTarget,
S: Surface {
fn new_surface(&mut self) -> Result<S, io::Error> {
self.surfaces.new_surface()
}
}
impl<T: LedPixelShape, S: Surface> Display<S> for EmbeddedDisplay<Ws2812DrawTarget<'_, T>, S> {
fn start_frame(&mut self) {
self.total_mw = 0;
}
fn end_frame(&mut self) {
let brightness = power::brightness_for_mw(self.total_mw, 255, self.max_mw);
self.target.set_brightness(brightness);
self.target.flush().unwrap();
}
fn render_frame(&mut self) {
let size = T::size();
let xStride: u8 = 255 / (size.width as u8);
let yStride: u8 = 255 / (size.height as u8);
let area = Rectangle::new(Point::new(0, 0), size);
self.target.draw_iter(
area.points()
.map(|pos| {
let virtCoords = VirtualCoordinates::new(pos.x as u8 * xStride, pos.y as u8 * yStride);
let mut pixel = RGB8::new(0, 0, 0);
for surface in self.surfaces.iter() {
surface.with_shader(|shader| {
pixel = pixel.saturating_add(shader.draw(virtCoords));
})
}
self.total_mw += pixel.as_milliwatts();
return Pixel(pos, Rgb888::new(pixel.r, pixel.g, pixel.b));
})
).unwrap();
}
}
impl<Shape: LedPixelShape> DisplayInit for Ws2812DrawTarget<'_, Shape> {
fn new_display<S: Surface>() -> impl Display<S> {
let peripherals = Peripherals::take().unwrap();
let led_pin = peripherals.pins.gpio14;
let channel = peripherals.rmt.channel0;
const POWER_VOLTS : u32 = 5;
const POWER_MA : u32 = 500;
const MAX_POWER_MW : u32 = POWER_VOLTS * POWER_MA;
let target = Self::new(channel, led_pin).unwrap();
return EmbeddedDisplay::<Self, S>::new(target, MAX_POWER_MW);
}
}
pub struct PonderjarMatrix {}
impl LedPixelShape for PonderjarMatrix {
fn size() -> Size {
Size::new(17, 17)
}
fn pixel_index(point: Point) -> Option<usize> {
if (0..Self::size().width as i32).contains(&point.x) && (0..Self::size().height as i32).contains(&point.y) {
if point.y % 2 == 0 {
Some((point.y as u32 * Self::size().width as u32 + point.x as u32).try_into().unwrap())
} else {
Some((point.y as u32 * Self::size().width as u32 - point.x as u32).try_into().unwrap())
}
} else {
None
}
}
}
pub type PonderjarTarget<'a> = Ws2812DrawTarget<'a, PonderjarMatrix>;

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);
}
}

33
src/platform/mod.rs Normal file
View File

@ -0,0 +1,33 @@
#[cfg(feature="embedded-graphics")]
pub mod embedded_graphics_lib;
#[cfg(feature="smart-leds")]
pub mod smart_leds_lib;
pub mod esp32;
pub type DefaultBoard = esp32::board::Esp32Board;
use crate::render::{Output, Surfaces};
use crate::task::Scheduler;
pub trait Board {
type Output: Output;
type Surfaces: Surfaces;
type Scheduler: Scheduler;
fn take() -> Self;
fn output(&mut self) -> Self::Output;
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
);
}

View File

@ -0,0 +1,167 @@
use smart_leds_trait::SmartLedsWrite;
use crate::buffers::Pixbuf;
use crate::properties::Variant;
use crate::render::{HardwarePixel, Output, PixelView, Sample, props::Output as OutputNS};
use crate::power::brightness_for_mw;
use crate::{geometry::*, prop_id};
use crate::mappings::*;
use paste::paste;
use core::fmt::Debug;
impl<P: Pixbuf, T: FastWrite> Debug for StrideOutput<P, T> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("StrideOutput").finish()
}
}
pub struct StrideOutput<P: Pixbuf, T: FastWrite> {
pixbuf: P,
stride_map: StrideMapping,
target: T,
max_mw: u32,
brightness: u8
}
impl<P: Pixbuf, T: FastWrite> StrideOutput<P, T> {
pub fn new(pixbuf: P, stride_map: StrideMapping, target: T, max_mw: u32) -> Self {
assert!(stride_map.pixel_count <= pixbuf.pixel_count(), "map needs {} pixels, I only have PIXEL_NUM={}", stride_map.pixel_count, pixbuf.pixel_count());
StrideOutput {
pixbuf,
stride_map,
target,
max_mw,
brightness: 255
}
}
}
impl<P: Pixbuf, T: FastWrite> Sample for StrideOutput<P, T> {
type Pixel = P::Pixel;
fn sample(&mut self, rect: &Rectangle<Virtual>) -> impl PixelView<Pixel = Self::Pixel> {
StrideSampler::new(&mut self.pixbuf,self.stride_map.select(rect))
}
}
impl<P: Pixbuf<Pixel=T::Color>, T: FastWrite> Output for StrideOutput<P, T> {
fn blank(&mut self) {
self.pixbuf.blank();
}
fn commit(&mut self) {
let b = brightness_for_mw(self.pixbuf.as_milliwatts(), self.brightness, self.max_mw);
if self.target.fast_write(self.pixbuf.iter_with_brightness(b)).is_err() {
panic!("Could not write frame!");
};
}
fn on_event(&mut self, event: &crate::events::Event) {
match event {
crate::events::Event::PropertyChange(prop_id!(OutputNS::Brightness), Variant::Byte(new_brightness)) => self.brightness = *new_brightness,
_ => ()
}
}
}
pub trait FastWrite: Send {
type Target: SmartLedsWrite;
type Color: HardwarePixel;
type Error;
fn fast_write<T, I>(&mut self, iterator: T) -> Result<(), Self::Error> where
T: IntoIterator<Item = I>,
I: Into<Self::Color>,
<T as IntoIterator>::IntoIter: Send;
}
#[cfg(feature="rmt")]
pub mod rmt {
use ws2812_esp32_rmt_driver::driver::color::LedPixelColorGrb24;
use smart_leds::SmartLedsWrite;
use rgb::Rgb;
use ws2812_esp32_rmt_driver::LedPixelEsp32Rmt;
use super::FastWrite;
pub type FastWs2812Esp32Rmt<'a> = LedPixelEsp32Rmt<'a, Rgb<u8>, LedPixelColorGrb24>;
impl FastWrite for FastWs2812Esp32Rmt<'_> {
type Color = <Self as SmartLedsWrite>::Color;
type Error = <Self as SmartLedsWrite>::Error;
type Target = Self;
fn fast_write<T, I>(&mut self, iterator: T) -> Result<(), Self::Error> where
T: IntoIterator<Item = I>,
I: Into<Self::Color>,
<T as IntoIterator>::IntoIter: Send {
self.write_nocopy(iterator)
}
}
}
#[cfg(feature="spi")]
pub mod spi {
use smart_leds::SmartLedsWrite;
use ws2812_spi::prerendered::Ws2812;
use crate::render::{Display, Surface};
use crate::platform::smart_leds_lib::SmartLedDisplay;
use crate::DisplayInit;
use super::FastWrite;
use esp_idf_svc::hal::{
prelude::*,
gpio::AnyIOPin,
spi::{
config::{Config, DriverConfig},
Dma,
SpiBusDriver,
SpiDriver,
}
};
impl<'a> FastWrite for Ws2812<'_, SpiBusDriver<'a, SpiDriver<'a>>> {
type Color = <Self as SmartLedsWrite>::Color;
type Error = <Self as SmartLedsWrite>::Error;
type Target = Self;
fn fast_write<T, I>(&mut self, iterator: T) -> Result<(), Self::Error> where
T: IntoIterator<Item = I>,
I: Into<Self::Color>,
<T as IntoIterator>::IntoIter: Send {
let resp = self.write(iterator);
if let Err(e) = resp {
panic!("Could not write SPI frame! {:?}", e)
} else {
resp
}
}
}
static mut STATIC_BUFFER: [u8; 400 * 12] = [0; 400 * 12];
pub struct SPIDisplay {}
impl DisplayInit for SPIDisplay {
fn new_display<S: Surface>() -> impl Display<S> {
let peripherals = Peripherals::take().unwrap();
let driver = SpiDriver::new_without_sclk(
peripherals.spi2,
peripherals.pins.gpio5,
Option::<AnyIOPin>::None,
&DriverConfig::new().dma(Dma::Auto(4092))
).unwrap();
let cfg = Config::new().baudrate(3.MHz().into());
let spi = SpiBusDriver::new(driver, &cfg).unwrap();
const POWER_VOLTS : u32 = 5;
const POWER_MA : u32 = 500;
const MAX_POWER_MW : u32 = POWER_VOLTS * POWER_MA;
unsafe {
let target = Ws2812::new(spi, &mut STATIC_BUFFER);
return SmartLedDisplay::<Ws2812<SpiBusDriver<'_, SpiDriver<'_>>>, S, 310>::new(target, MAX_POWER_MW);
}
}
}
}

View File

@ -1,7 +1,37 @@
use rgb::RGB8;
pub trait AsMilliwatts {
fn as_milliwatts(&self) -> u32;
}
impl AsMilliwatts for RGB8 {
fn as_milliwatts(&self) -> u32 {
const RED_MW : u32 = 16 * 5; //< 16mA @ 5v = 80mW
const GREEN_MW : u32 = 11 * 5; //< 11mA @ 5v = 55mW
const BLUE_MW : u32 = 15 * 5; //< 15mA @ 5v = 75mW
const DARK_MW : u32 = 5; //< 1mA @ 5v = 5mW
let red = (self.r as u32 * RED_MW).wrapping_shr(8);
let green = (self.g as u32 * GREEN_MW).wrapping_shr(8);
let blue = (self.b as u32 * BLUE_MW).wrapping_shr(8);
return red + green + blue + DARK_MW;
}
}
impl<T> AsMilliwatts for [T] where T: AsMilliwatts {
fn as_milliwatts(&self) -> u32 {
self.iter().map(|p| { p.as_milliwatts() }).sum()
}
}
impl<T, const S: usize> AsMilliwatts for [T; S] where T: AsMilliwatts {
fn as_milliwatts(&self) -> u32 {
self.iter().map(|p| { p.as_milliwatts() }).sum()
}
}
pub fn brightness_for_mw(total_mw : u32, target : u8, max_power: u32) -> u8 {
let target32 = target as u32;
let requested_mw = (total_mw * target32) / 256;

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,127 +1,106 @@
use std::rc::Rc;
use std::cell::RefCell;
use std::sync::{Arc, Mutex};
use std::io;
use crate::lib8::RGB8;
use rgb::Rgb;
use crate::events::*;
use crate::properties::*;
use crate::geometry::*;
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;
pub trait Shader: Send {
fn draw(&self, surface_coords: VirtualCoordinates) -> RGB8;
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 {}
pub trait PixelView {
type Pixel: HardwarePixel;
fn next(&mut self) -> Option<(Coordinates<Virtual>, &mut Self::Pixel)>;
}
pub trait Surface: Default + Clone {
fn with_shader<F: FnMut(&dyn Shader)>(&self, f: F);
fn set_shader(&mut self, shader: Box<dyn Shader>);
pub trait Sample {
type Pixel: HardwarePixel;
fn sample(&mut self, rect: &Rectangle<Virtual>) -> impl PixelView<Pixel = Self::Pixel>;
}
pub trait Shader: Send {
fn draw(&self, surface_coords: &VirtualCoordinates, frame: usize) -> Rgb<u8>;
}
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 {
fn set_shader<T: Shader + 'static>(&mut self, shader: T);
fn clear_shader(&mut self);
fn set_rect(&mut self, rect: Rectangle<Virtual>);
fn set_opacity(&mut self, opacity: u8);
}
pub trait Surfaces<T: Surface> {
fn new_surface(&mut self) -> Result<T, io::Error>;
pub trait Output: Sample + Send {
fn on_event(&mut self, event: &Event);
fn blank(&mut self);
fn commit(&mut self);
}
pub trait Display<T: Surface>: Surfaces<T> {
fn start_frame(&mut self) {}
fn end_frame(&mut self) {}
fn render_frame(&mut self);
#[derive(Debug)]
pub struct Renderer<T: Output, S: Surfaces> {
output: T,
surfaces: S,
fps: RealTimeRunningAverage<u32>,
fps_display: Periodically,
frame: usize,
frame_count: usize
}
pub struct ShaderBinding {
shader: Option<Box<dyn Shader>>,
opacity: u8,
}
#[derive(Clone)]
pub struct BoundSurface<T> {
pub binding: T
}
pub type SharedSurface = BoundSurface<Arc<Mutex<ShaderBinding>>>;
pub type SimpleSurface = BoundSurface<Rc<RefCell<ShaderBinding>>>;
impl Default for BoundSurface<Arc<Mutex<ShaderBinding>>> {
fn default() -> Self {
impl<T: Output, S: Surfaces> Renderer<T, S> {
pub fn new(output: T, surfaces: S) -> Self {
Self {
binding: Arc::new(Mutex::new(ShaderBinding {
shader: None,
opacity: 255,
})),
output,
surfaces: surfaces,
fps: RealTimeRunningAverage::default(),
fps_display: Periodically::new_every_n_seconds(5),
frame: 0,
frame_count: 0
}
}
}
impl Default for BoundSurface<Rc<RefCell<ShaderBinding>>>{
fn default() -> Self {
Self {
binding: Rc::new(RefCell::new(ShaderBinding {
shader: None,
opacity: 255,
})),
}
impl<T: Output, S: Surfaces> Task for Renderer<T, S> {
fn name(&self) -> &'static str { "Renderer" }
fn on_property_change(&mut self, key: PropertyID, value: &Variant, _bus: &mut EventBus) {
self.output.on_event(&Event::new_property_change(key, value.clone()));
}
fn on_tick(&mut self, bus: &mut EventBus) {
self.output.blank();
self.surfaces.render_to(&mut self.output, self.frame);
self.output.commit();
self.frame += 1;
self.fps_display.run(|| {
self.fps.insert((self.frame - self.frame_count) as u32);
self.frame_count = self.frame;
let fps = self.fps.measurement();
bus.set_property(crate::render::props::Output::FPS, fps.rate() as u32);
});
}
}
impl Surface for BoundSurface<Rc<RefCell<ShaderBinding>>> {
fn with_shader<F: FnMut(&dyn Shader)>(&self, mut f: F) {
if let Some(ref shader) = self.binding.borrow().shader {
f(shader.as_ref());
}
}
pub mod props {
use crate::property_namespace;
fn set_shader(&mut self, shader: Box<dyn Shader>) {
self.binding.borrow_mut().shader = Some(shader);
}
fn clear_shader(&mut self) {
self.binding.borrow_mut().shader = None;
}
fn set_opacity(&mut self, opacity: u8) {
self.binding.borrow_mut().opacity = opacity;
}
}
impl Surface for BoundSurface<Arc<Mutex<ShaderBinding>>> {
fn with_shader<F: FnMut(&dyn Shader)>(&self, mut f: F) {
if let Some(ref shader) = self.binding.lock().unwrap().shader {
f(shader.as_ref());
}
}
fn set_shader(&mut self, shader: Box<dyn Shader>) {
self.binding.lock().unwrap().shader = Some(shader);
}
fn clear_shader(&mut self) {
self.binding.lock().unwrap().shader = None;
}
fn set_opacity(&mut self, opacity: u8) {
self.binding.lock().unwrap().opacity = opacity;
}
}
pub struct SurfacePool<S: Surface + Default> {
surfaces: Vec<S>
}
impl<S: Surface + Default> SurfacePool<S> {
pub const fn new() -> Self {
Self {
surfaces: Vec::new()
}
}
pub fn iter(&self) -> std::slice::Iter<S> {
self.surfaces.iter()
}
}
impl<S: Surface + Default + Clone> Surfaces<S> for SurfacePool<S> {
fn new_surface(&mut self) -> Result<S, io::Error> {
let surface = S::default();
self.surfaces.push(surface.clone());
return Ok(surface);
}
}
property_namespace!(
Output,
FPS => 0,
Brightness => 0
);
}

130
src/scenes.rs Normal file
View File

@ -0,0 +1,130 @@
use crate::task::Task;
use crate::events::*;
use crate::properties::*;
use paste::paste;
#[derive(Debug, PartialEq, Eq, Clone)]
struct Scene {
name: &'static str,
patterns: Vec<&'static str>,
trigger: Trigger
}
#[derive(Debug, PartialEq, Eq, Clone)]
enum Trigger {
Startup,
PropertyEquals(PropertyID, Variant)
}
pub struct Sequencer {
scenes: Vec<Scene>,
cur_scene: String
}
impl Sequencer {
pub fn new() -> Self {
Sequencer {
cur_scene: String::new(),
scenes: vec![
Scene {
name: "Start",
patterns: vec!["Idle"],
trigger: Trigger::Startup
},
Scene {
name: "Online",
patterns: vec!["Idle"],
trigger: Trigger::PropertyEquals(prop_id!(System::NetworkOnline), Variant::Boolean(true))
}
]
}
}
fn get_scene(&self, name: &String) -> Option<&Scene> {
for scene in self.scenes.iter() {
if scene.name == name {
return Some(scene);
}
}
return None;
}
fn apply_scene(&mut self, name: &String, bus: &mut EventBus) {
if let Some(dst_tasks) = self.get_scene(name) {
if let Some(src_tasks) = self.get_scene(&self.cur_scene) {
let stop_queue = src_tasks.patterns.iter().filter(|i| !dst_tasks.patterns.contains(i));
let start_queue = dst_tasks.patterns.iter().filter(|i| !src_tasks.patterns.contains(i));
log::info!("Switching scene from {} to {}", self.cur_scene, name);
for stop in stop_queue {
bus.push(Event::new_stop_thing(stop));
}
for start in start_queue {
bus.push(Event::new_start_thing(start));
}
} else {
log::info!("Starting new scene {}", name);
log::info!("start={:?}", dst_tasks.patterns);
for start in dst_tasks.patterns.iter() {
bus.push(Event::new_start_thing(start));
}
}
self.cur_scene = name.clone();
} else {
panic!("Could not apply scene {:?} scenes={:?}", name, self.scenes);
}
}
}
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.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: PropertyID, value: &Variant, bus: &mut EventBus) {
match (key, value) {
(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() {
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(Scenes::Current.new_property_change(scene.name))
}
},
_ => ()
}
}*/
}
}
}
}
use crate::prop_id;

View File

@ -1,91 +0,0 @@
use smart_leds_trait::SmartLedsWrite;
use running_average::RealTimeRunningAverage;
use crate::render::{Surface, SurfacePool, Display, Surfaces};
use crate::task::Task;
use crate::power;
use crate::time::Periodically;
use crate::lib8::RGB8;
use crate::geometry::*;
use crate::power::AsMilliwatts;
use smart_leds::brightness;
use std::io;
use rgb::Rgb;
pub struct SmartLedDisplay<T: SmartLedsWrite<Color = Rgb<u8>>, S: Surface> {
surfaces : SurfacePool<S>,
target: T,
fps: RealTimeRunningAverage<u32>,
frame: u32,
fps_display: Periodically,
pixbuf: [T::Color; 255],
total_mw: u32,
max_mw: u32
}
impl<T: SmartLedsWrite<Color = Rgb<u8>>, S: Surface> SmartLedDisplay<T, S> {
pub fn new(target: T, max_mw: u32) -> Self {
SmartLedDisplay {
surfaces: SurfacePool::new(),
target: target,
max_mw: max_mw,
total_mw: 0,
fps: RealTimeRunningAverage::default(),
frame: 0,
fps_display: Periodically::new_every_n_seconds(5),
pixbuf: [Rgb::new(0, 0, 0); 255]
}
}
}
impl<T: SmartLedsWrite<Color = Rgb<u8>>, S: Surface> Task for SmartLedDisplay<T, S> {
fn name(&self) -> &'static str { "Renderer" }
fn tick(&mut self) {
self.start_frame();
self.render_frame();
self.end_frame();
}
}
impl<T, S> Surfaces<S> for SmartLedDisplay<T, S>
where
T: SmartLedsWrite<Color = Rgb<u8>>,
S: Surface {
fn new_surface(&mut self) -> Result<S, io::Error> {
self.surfaces.new_surface()
}
}
impl<T: SmartLedsWrite<Color = Rgb<u8>>, S: Surface> Display<S> for SmartLedDisplay<T, S> {
fn start_frame(&mut self) {
self.frame = self.frame.wrapping_add(1);
self.total_mw = 0;
}
fn end_frame(&mut self) {
self.fps.insert(1);
let b = power::brightness_for_mw(self.total_mw, 255, self.max_mw);
self.fps_display.run(|| {
log::info!("FPS: {} frame={} brightness={} mw={}", self.fps.measurement(), self.frame, b, self.total_mw);
});
self.target.write(brightness(self.pixbuf.iter().cloned(), b));
}
fn render_frame(&mut self) {
for x in 0..self.pixbuf.len() {
let virtCoords = VirtualCoordinates::new(x as u8, 0);
let mut pixel = RGB8::new(0, 0, 0);
for surface in self.surfaces.iter() {
surface.with_shader(|shader| {
pixel = shader.draw(virtCoords.clone());
})
}
self.total_mw += pixel.as_milliwatts();
self.pixbuf[x] = Rgb::new(pixel.red, pixel.green, pixel.blue);
};
}
}

View File

@ -1,91 +1,33 @@
use std::fmt;
use core::fmt;
pub trait Task {
fn tick(&mut self) {}
fn start(&mut self) {}
fn stop(&mut self) {}
fn name(&self) -> &'static str;
}
use crate::{events::{Event, EventBus}, properties::{Variant, PropertyID}};
trait ScheduledState: std::fmt::Debug {
fn start(self: Box<Self>) -> Box<dyn ScheduledState>;
fn stop(self: Box<Self>) -> Box<dyn ScheduledState>;
fn tick(self: Box<Self>, task: &mut dyn Task) -> Box<dyn ScheduledState>;
}
#[derive(Debug)]
struct Starting {}
impl ScheduledState for Starting {
fn start(self: Box<Self>) -> Box<dyn ScheduledState> {
self
}
fn stop(self: Box<Self>) -> Box<dyn ScheduledState> {
Box::new(Stopped {})
}
fn tick(self: Box<Self>, task: &mut dyn Task) -> Box<dyn ScheduledState> {
task.start();
Box::new(Running{})
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: 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>()
}
}
#[derive(Debug)]
struct Running {}
impl ScheduledState for Running {
fn start(self: Box<Self>) -> Box<dyn ScheduledState> {
self
}
fn stop(self: Box<Self>) -> Box<dyn ScheduledState> {
Box::new(Stopping {})
}
fn tick(self: Box<Self>, task: &mut dyn Task) -> Box<dyn ScheduledState> {
task.tick();
self
}
}
#[derive(Debug)]
struct Stopping {}
impl ScheduledState for Stopping {
fn start(self: Box<Self>) -> Box<dyn ScheduledState> {
Box::new(Running {})
}
fn stop(self: Box<Self>) -> Box<dyn ScheduledState> {
self
}
fn tick(self: Box<Self>, task: &mut dyn Task) -> Box<dyn ScheduledState> {
task.stop();
Box::new(Stopped {})
}
}
#[derive(Debug)]
struct Stopped {}
impl ScheduledState for Stopped {
fn start(self: Box<Self>) -> Box<dyn ScheduledState> {
Box::new(Starting {})
}
fn stop(self: Box<Self>) -> Box<dyn ScheduledState> {
self
}
fn tick(self: Box<Self>, _task: &mut dyn Task) -> Box<dyn ScheduledState> {
self
}
#[derive(Debug, Clone)]
enum ScheduledState {
Stopped,
Start,
Running,
Stop
}
struct ScheduledTask {
state: Option<Box<dyn ScheduledState>>,
state: ScheduledState,
task: Box<dyn Task>,
}
impl std::fmt::Debug for ScheduledTask {
impl core::fmt::Debug for ScheduledTask {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ScheduledTask")
.field("task", &self.task.name())
@ -97,49 +39,112 @@ impl std::fmt::Debug for ScheduledTask {
impl ScheduledTask {
fn new(task: Box<dyn Task>) -> Self {
ScheduledTask {
state: Some(Box::new(Starting{})),
state: ScheduledState::Stopped,
task: task,
}
}
fn start(&mut self) {
if let Some(s) = self.state.take() {
self.state = Some(s.start());
match self.state {
ScheduledState::Stopped => self.state = ScheduledState::Start,
ScheduledState::Stop => self.state = ScheduledState::Running,
_ => ()
}
}
fn stop(&mut self) {
if let Some(s) = self.state.take() {
self.state = Some(s.stop());
match self.state {
ScheduledState::Running => self.state = ScheduledState::Stop,
ScheduledState::Start => self.state = ScheduledState::Stopped,
_ => ()
}
}
fn tick(&mut self) {
if let Some(s) = self.state.take() {
self.state = Some(s.tick(self.task.as_mut()));
fn tick(&mut self, event: &Event, bus: &mut EventBus) {
match self.state {
ScheduledState::Start => {
log::info!("Starting task {}", self.task.name());
self.task.start(bus);
self.state = ScheduledState::Running
},
ScheduledState::Stop => {
log::info!("Stopping task {}", self.task.name());
self.task.stop(bus);
self.state = ScheduledState::Stopped
},
_ => ()
};
match self.state {
ScheduledState::Running => {
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.clone(), value, bus),
_ => ()
}
},
_ => ()
}
}
}
pub struct Scheduler {
tasks: Vec<ScheduledTask>,
#[derive(Debug)]
pub struct FixedSizeScheduler<const TASK_COUNT: usize> {
tasks: [ScheduledTask; TASK_COUNT],
}
impl Scheduler {
pub fn new(tasks: Vec<Box<dyn Task>>) -> Self {
let mut scheduled = Vec::new();
impl<const TASK_COUNT: usize> FixedSizeScheduler<TASK_COUNT> {
pub fn new(tasks: [Box<dyn Task>; TASK_COUNT]) -> Self {
let mut scheduled: [ScheduledTask; TASK_COUNT] = unsafe { std::mem::MaybeUninit::zeroed().assume_init() };
let mut idx = 0;
for task in tasks {
log::info!("Scheduling task {:?}", task.name());
scheduled.push(ScheduledTask::new(task));
log::info!("Scheduling task {}", task.name());
let slot = &mut scheduled[idx];
unsafe { std::ptr::write(slot, ScheduledTask::new(task)) };
idx += 1;
}
Scheduler {
FixedSizeScheduler {
tasks: scheduled
}
}
pub fn tick(&mut self) {
for task in &mut self.tasks {
task.tick();
fn find_task(&mut self, name: &str) -> Option<&mut ScheduledTask> {
for slot in &mut self.tasks {
if slot.task.name() == name {
return Some(slot);
}
}
None
}
}
impl<const TASK_COUNT: usize> Scheduler for FixedSizeScheduler<TASK_COUNT> {
fn tick(&mut self, event: &Event, bus: &mut EventBus) {
match event {
Event::StartThing(task_name) => {
if let Some(slot) = self.find_task(task_name) {
log::debug!("Starting {}", task_name);
slot.start();
}
},
Event::StopThing(task_name) => {
if let Some(slot) = self.find_task(task_name) {
log::debug!("Stopping {}", task_name);
slot.stop();
}
},
_ => {
for slot in &mut self.tasks {
slot.tick(event, bus);
}
}
}
}
}
pub trait Scheduler {
fn tick(&mut self, event: &Event, bus: &mut EventBus);
}

View File

@ -1,5 +1,7 @@
use std::time::{Instant, Duration};
use core::time::Duration;
use std::time::Instant;
#[derive(Debug)]
pub struct Periodically {
last_run: Instant,
duration: Duration
@ -22,9 +24,17 @@ impl Periodically {
}
pub fn run<F>(&mut self, f: F) where F: FnOnce() {
if self.last_run.elapsed() >= self.duration {
if self.tick() {
f();
}
}
pub fn tick(&mut self) -> bool {
if self.last_run.elapsed() >= self.duration {
self.last_run = Instant::now();
true
} else {
false
}
}
}