Compare commits

...

109 Commits

Author SHA1 Message Date
Austen Adler
058a076414 Working app 2024-04-21 21:44:09 -04:00
Austen Adler
dd978179df Connect to HA 2024-04-21 16:52:12 -04:00
Austen Adler
f65d45260e Get many parts working 2024-04-21 13:16:26 -04:00
Austen Adler
5ff96febac Properly deserialize 2024-04-13 12:16:44 -04:00
Austen Adler
2496e03033 Add json incoming for light 2024-04-13 12:10:34 -04:00
Austen Adler
d4b3651dbc Use light type 2024-04-13 12:02:44 -04:00
Austen Adler
cb9fe3b9e3 Add more rigorous tests 2024-04-13 11:48:34 -04:00
Austen Adler
529610a7ff Add test 2024-04-13 11:43:52 -04:00
Austen Adler
4cb3ce0bff Add new and default for functions 2024-04-13 11:37:33 -04:00
Austen Adler
ccc0b3d883 Fix tests 2024-04-13 11:15:25 -04:00
Austen Adler
ea1b9017f1 Fix tests 2024-04-13 11:14:47 -04:00
Austen Adler
e271fb8277 Update schemas 2024-04-13 11:04:22 -04:00
Austen Adler
b49326f5dc Start work on subscriptions 2024-04-13 10:43:33 -04:00
Austen Adler
9032aba60a Start work on mqtt 2024-04-12 23:41:35 -04:00
Austen Adler
a11168c8f1 Fixed formatting 2024-04-12 22:46:38 -04:00
Austen Adler
016bc590d1 Fix scoping 2024-04-12 22:31:16 -04:00
Austen Adler
3bdc909933 Add tests 2024-04-12 22:28:12 -04:00
Austen Adler
ab33695994 Improvements 2024-04-12 22:07:40 -04:00
Austen Adler
ac7132659c Almost complete with initial migration 2024-04-08 23:50:37 -04:00
Austen Adler
ad45bc3431 Initial cleanup 2024-04-08 23:37:34 -04:00
Austen Adler
84241963a2 Initial import 2024-04-08 23:34:46 -04:00
Austen Adler
d989a1183f Fixes 2024-04-08 23:34:43 -04:00
Austen Adler
60687e11fa Changes 2024-04-08 23:15:56 -04:00
Austen Adler
aa96d81fa3 Get mqtt working 2024-04-07 20:10:08 -04:00
Austen Adler
6cb9883a7c Start work on mqtt 2024-04-07 17:25:15 -04:00
Austen Adler
7c609be435 Config changes 2024-04-07 17:15:09 -04:00
Austen Adler
cee8a92c87 Update cargo config file 2024-04-07 16:51:34 -04:00
Austen Adler
1ad1a4d315 Add custom visualizer 2023-12-10 13:24:31 -05:00
Austen Adler
d7beb73329 Update strip termination logic 2023-06-10 23:01:10 -04:00
Austen Adler
f1e11c1bc2 Update default moving rainbow parameters 2023-06-10 22:56:31 -04:00
Austen Adler
bc4a6dfeac Move message out of error 2023-06-04 00:24:41 -04:00
Austen Adler
0c652a1c19 Replace log message 2023-06-03 23:41:21 -04:00
Austen Adler
76886622ec Use tracing logger 2023-06-03 23:37:09 -04:00
Austen Adler
663fd4feca Properly quit on ctrl+c 2023-06-03 21:12:06 -04:00
Austen Adler
e9a68b0997 Fix strip and add reverse 2023-06-03 16:24:35 -04:00
Austen Adler
b9c906ae96 Allow splitting lights in half down the middle 2023-06-03 14:39:02 -04:00
Austen Adler
2fed645d22 Allow pattern initialization 2023-05-28 18:47:45 -04:00
Austen Adler
e4e69f4d51 Start work on car rainbow 2023-05-28 17:53:11 -04:00
Austen Adler
4781506ff9 Use strum for constructor 2023-05-28 17:46:02 -04:00
Austen Adler
97d523a149 Use strum 2023-05-28 17:43:07 -04:00
Austen Adler
1c6eefc004 Improve logging 2023-05-28 17:31:34 -04:00
Austen Adler
7a43ae7431 Use clap to parse arguments 2023-05-28 17:23:55 -04:00
Austen Adler
0a13264096 Update makefile and consts 2023-05-28 17:10:33 -04:00
Austen Adler
7d2737d1d1 Start on lunatic-webui 2023-05-28 14:45:39 -04:00
Austen Adler
0c84fda192 Log number of lights 2023-02-20 16:58:36 -05:00
Austen Adler
1eb6f0f51a Add env.dist 2023-02-20 16:57:11 -05:00
Austen Adler
914dfa23d7 Add yarn.lock 2023-02-20 16:57:04 -05:00
Austen Adler
c27f78208d Do not deploy on make 2023-02-20 16:56:56 -05:00
Austen Adler
9f565cdafb Initialize number of lights from environment 2023-02-20 16:56:52 -05:00
Austen Adler
11fe309b34 Update visualizer for crashes 2023-02-20 16:08:51 -05:00
Austen Adler
a8b3a4cce5 Fix warnings 2022-12-29 13:38:12 -05:00
Austen Adler
8a730a40d5 Make audio visualizer rainbow 2022-12-06 22:06:59 -05:00
Austen Adler
c89ca20295 Fix cava 2022-12-06 20:51:51 -05:00
Austen Adler
9abeb4492a Fix cava 2022-12-03 12:17:21 -05:00
Austen Adler
db45a14e84 More changes 2022-11-22 21:09:12 -05:00
Austen Adler
3f02d476dc More changes 2022-11-22 21:08:59 -05:00
Austen Adler
4c8ae8c9f6 Start work on cava 2022-11-21 11:48:45 -05:00
Austen Adler
c30583a9cc Call cleanup before assigning a new pattern 2022-11-17 16:28:41 -05:00
Austen Adler
2d13b8aca5 Start work on visualizer pattern 2022-11-17 16:23:26 -05:00
Austen Adler
e86d8cfd6f Webui updates 2022-11-17 15:58:55 -05:00
Austen Adler
d621a338aa Update rppal to build on musl target 2022-11-17 15:58:34 -05:00
Austen Adler
15cb6a512a Add slide; fix clippy warnings 2022-11-17 15:58:11 -05:00
Austen Adler
5c23f9226c Add bitbakefile I will never use 2022-10-25 18:08:06 -04:00
Austen Adler
baa9b79863 Update package.json 2022-07-25 22:30:09 -04:00
Austen Adler
f0e6aeb89f Remove webui cargo.lock 2022-07-25 22:29:54 -04:00
Austen Adler
72b26d9e74 Update Makefile 2022-07-25 22:29:32 -04:00
Austen Adler
0fdfc2155c Update .cargo/conf 2022-07-25 22:29:24 -04:00
Austen Adler
a958fee74c Update makefile 2021-10-16 07:36:15 -04:00
Austen Adler
632f49f6a9 Add copy-code.sh 2021-10-16 07:36:02 -04:00
Austen Adler
993a8ad1c7 Run rustfmt with new formatter 2021-10-12 17:15:34 -04:00
Austen Adler
b1f9829532 Fix clippy warnings 2021-10-12 17:15:24 -04:00
Austen Adler
4aa5fe8df4 Update entr.sh 2021-10-12 17:15:14 -04:00
Austen Adler
45c732a0d2 Rename errors to error 2021-10-12 17:14:44 -04:00
Austen Adler
d5624e7475 Cleanup cargo.toml 2021-10-12 17:12:24 -04:00
Austen Adler
c41eb8691a Split webui into separate package 2021-10-12 17:11:44 -04:00
Your Name
33b5975f93 Use Box<dyn Iterator> as return type of functions 2021-10-11 00:11:44 +01:00
Your Name
3a81585d51 Handle rainbow with splitting in ui 2021-10-11 00:11:27 +01:00
Your Name
bdf6122993 Rename into_pattern to to_pattern 2021-10-11 00:10:30 +01:00
Your Name
ac3507b774 Add ability to split rainbow 2021-10-11 00:08:36 +01:00
Your Name
35c9de16c2 Fix liveviewjs checkbox 2021-10-11 00:07:29 +01:00
Your Name
fc5403388f Use unstable Crate rustfmt option 2021-10-10 16:43:30 +01:00
Your Name
b5b4bf3b41 Use thread::sleep for better efficency 2021-10-10 16:43:08 +01:00
Your Name
c295839a9a Merge 2021-10-10 15:56:42 +01:00
Austen Adler
4251b18465 Remove extra package-lock.json 2021-08-28 01:07:46 -04:00
Austen Adler
81bb9a2de0 Fix Fade pattern 2021-08-28 01:07:36 -04:00
Austen Adler
65aa41d780 Add rust-form-multi to liveview-dev 2021-08-22 14:18:16 -04:00
Austen Adler
47114718a1 Allow multi form elements 2021-08-22 14:17:59 -04:00
Austen Adler
8a7453d12c Return newly rendered form on submit 2021-08-22 13:57:55 -04:00
Austen Adler
690aaf4d40 Format webui 2021-08-22 12:11:42 -04:00
Austen Adler
d13432e35d Clean up build scripts to work on non-nightly rust versions 2021-08-22 12:11:36 -04:00
Austen Adler
0787c19b90 Remove local reference in package.json 2021-08-22 12:09:17 -04:00
Austen Adler
d97e4b761c Send data to strip and add css 2021-08-22 12:08:49 -04:00
Austen Adler
9ed83f0b96 Misc improvements 2021-08-22 00:14:50 -04:00
Austen Adler
b9b503d61b Try fixing blank form field bug 2021-08-22 00:14:45 -04:00
Austen Adler
dad2401023 Get form submission working 2021-08-21 21:45:59 -04:00
Austen Adler
04bc783c09 Serialize form as json 2021-08-21 17:22:10 -04:00
Austen Adler
afb57a17ff Fix html rendering 2021-08-21 17:22:03 -04:00
Austen Adler
a6ee7ad6bc Update build.rs for new liveview npm install 2021-08-21 17:21:50 -04:00
Austen Adler
22a53fd735 Fix cargo version 2021-08-21 17:15:26 -04:00
Austen Adler
c40173f2ca Ignore dist 2021-08-21 16:28:14 -04:00
Austen Adler
49f7774d66 Fix more clippy warnings 2021-08-21 16:27:57 -04:00
Austen Adler
489adc1af1 Clean up clippy warnings 2021-08-21 15:51:10 -04:00
Austen Adler
537fec8a4d Implement working form manipulation 2021-08-21 15:24:21 -04:00
Austen Adler
1734dbb5c9 Complete pattern refactor 2021-08-21 14:11:19 -04:00
Austen Adler
5b5343dc9f Continue work on rust-liveview 2021-08-21 14:02:08 -04:00
Austen Adler
3b0eb7fc1c Make work 2021-08-18 21:12:43 -04:00
Austen Adler
efeb2d56dc Test out liveview rust features 2021-08-18 10:03:10 -04:00
Austen Adler
86ef9ba35c Add liveview-rust 2021-08-18 10:02:40 -04:00
Austen Adler
829e9d97aa Add base files for liveview-rs 2021-08-16 09:53:54 -04:00
106 changed files with 19176 additions and 1708 deletions

View File

@ -1,12 +0,0 @@
# For cross compilation
[target.aarch64-unknown-linux-musl]
linker = "aarch64-linux-gnu-gcc"
[target.armv7-unknown-linux-musleabi]
linker = "arm-linux-gnueabi-gcc"
[target.arm-unknown-linux-musleabi]
linker = "arm-linux-gnueabi-gcc"
[target.armv7-unknown-linux-gnueabihf]
linker = "arm-linux-gnueabihf-gcc"

1
.cargo/config.toml Symbolic link
View File

@ -0,0 +1 @@
../config.toml

2
.gitignore vendored
View File

@ -1,4 +1,6 @@
/.env
/debug/ /debug/
/target/ /target/
/Cargo.lock /Cargo.lock
**/*.rs.bk **/*.rs.bk
/node_modules

View File

@ -3,17 +3,30 @@ name = "aw-lights"
version = "0.1.0" version = "0.1.0"
authors = ["Austen Adler <agadler@austenadler.com>"] authors = ["Austen Adler <agadler@austenadler.com>"]
edition = "2018" edition = "2018"
build = "build.rs" repository = "https://gitea.austen-wares.com/stonewareslord/aw-lights.git"
# build = "build.rs"
[workspace]
members = [
".",
"webui",
"mqtt",
"common",
# "lunatic-webui",
"homeassistant-mqtt-discovery",
]
[dependencies] [dependencies]
rppal = "0.7" rppal = "0.14"
ws2818-rgb-led-spi-driver = { path = "lib-ws2818-rgb-led-spi-driver/" } ws2818-rgb-led-spi-driver = { path = "./lib-ws2818-rgb-led-spi-driver" }
serde = {version = "1.0", features = ["derive"]} common = { path = "./common" }
actix-web = {version = "3", default_features = false} webui = { path = "./webui" }
rust-embed="6.0.0" mqtt = { path = "./mqtt" }
hex = "0.4.3" dotenv = "0.15.0"
serde_json = "1" clap = { version = "4.3.0", features = ["derive", "env"] }
actix-web-static-files = "3.0" tracing = "0.1.37"
env_logger = "0.10.0"
[build-dependencies] [profile.release]
actix-web-static-files = "3.0" lto = true
codegen-units = 1

View File

@ -1,11 +1,44 @@
.PHONY: build deploy run # Old target:
# armv7-unknown-linux-gnueabihf
# New target:
# arm-unknown-linux-musleabihf
# armv7-unknown-linux-musleabihf
build: # TARGET = arm-unknown-linux-gnueabihf
cargo build --target=armv7-unknown-linux-gnueabihf # TARGET = armv7-unknown-linux-gnueabihf
# TARGET = arm-unknown-linux-gnueabi
# TARGET = armv7-unknown-linux-gnueabi
TARGET = arm-unknown-linux-musleabihf
# TARGET = armv7-unknown-linux-musleabihf
# TARGET = arm-unknown-linux-musleabi
# TARGET = armv7-unknown-linux-musleabi
HOST = raspberrypi
# HOST = 192.168.1.82
# HOST = raspberrypi
PROJECT_NAME = aw-lights
.PHONY: build release
deploy-release: build-release
scp ./target/$(TARGET)/release/$(PROJECT_NAME) pi@$(HOST):$(PROJECT_NAME)-bin
build-release:
cargo build --release --target=$(TARGET)
du -sh ./target/$(TARGET)/release/$(PROJECT_NAME) ||:
arm-linux-musleabihf-strip ./target/$(TARGET)/release/$(PROJECT_NAME)
du -sh ./target/$(TARGET)/release/$(PROJECT_NAME) ||:
deploy: build deploy: build
arm-linux-gnueabihf-strip ./target/armv7-unknown-linux-gnueabihf/debug/aw-lights scp ./target/$(TARGET)/debug/$(PROJECT_NAME) pi@$(HOST):$(PROJECT_NAME)-bin
scp ./target/armv7-unknown-linux-gnueabihf/debug/aw-lights pi:aw-lights-bin
build:
cargo build --target=$(TARGET)
du -sh ./target/$(TARGET)/debug/$(PROJECT_NAME) ||:
arm-linux-musleabihf-strip ./target/$(TARGET)/debug/$(PROJECT_NAME)
du -sh ./target/$(TARGET)/release/$(PROJECT_NAME) ||:
run: deploy run: deploy
ssh pi ./aw-lights ssh pi@192.168.1.82 ./$(PROJECT_NAME)-bin

View File

@ -1,7 +1,9 @@
# aw-lights = aw-lights
== Setup
[source,bash]
---- ----
# Install packages
xbps-install fake-hwclock xbps-install fake-hwclock
ln -s /etc/sv/fake-hwclock/ /var/service/ ln -s /etc/sv/fake-hwclock/ /var/service/
@ -11,3 +13,34 @@ echo 'dtparam=spi=on' >>/boot/config.txt
# Allow big buf for SPI # Allow big buf for SPI
echo 'options spidev bufsiz=65536' > /etc/modprobe.d/spidev.conf echo 'options spidev bufsiz=65536' > /etc/modprobe.d/spidev.conf
---- ----
Set CPU clock
* RPI3
+
[title='/boot/config.txt']
----
core_freq=250
----
* RPI4
+
[title='/boot/config.txt']
----
core_freq=500
core_freq_min=500
----
== Cross Compiling
----
xbps-install -y cross-armv7l-linux-gnueabihf
rustup target add armv7-unknown-linux-gnueabihf
make deploy
----
== Notes
----
Car: 102
House: 89; skip 14
----

252
aw-lights_0.1.0.bb Normal file
View File

@ -0,0 +1,252 @@
# Auto-Generated by cargo-bitbake 0.3.16
#
inherit cargo
# If this is git based prefer versioned ones if they exist
# DEFAULT_PREFERENCE = "-1"
# how to get aw-lights could be as easy as but default to a git checkout:
# SRC_URI += "crate://crates.io/aw-lights/0.1.0"
SRC_URI += "gaw:stonewareslord/aw-lights;nobranch=1;branch=liveview-rust-implementation"
SRCREV = "baa9b798631d341399ca299e0fe6f5a6a925d346"
S = "${WORKDIR}/git"
CARGO_SRC_DIR = ""
PV:append = ".AUTOINC+baa9b79863"
# please note if you have entries that do not begin with crate://
# you must change them to how that package can be fetched
SRC_URI += " \
crate://crates.io/actix-codec/0.3.0 \
crate://crates.io/actix-connect/2.0.0 \
crate://crates.io/actix-http/2.2.2 \
crate://crates.io/actix-macros/0.1.3 \
crate://crates.io/actix-router/0.2.7 \
crate://crates.io/actix-rt/1.1.1 \
crate://crates.io/actix-server/1.0.4 \
crate://crates.io/actix-service/1.0.6 \
crate://crates.io/actix-testing/1.0.1 \
crate://crates.io/actix-threadpool/0.3.3 \
crate://crates.io/actix-tls/2.0.0 \
crate://crates.io/actix-utils/2.0.0 \
crate://crates.io/actix-web-actors/3.0.0 \
crate://crates.io/actix-web-codegen/0.4.0 \
crate://crates.io/actix-web/3.3.3 \
crate://crates.io/actix/0.10.0 \
crate://crates.io/actix_derive/0.5.0 \
crate://crates.io/adler/1.0.2 \
crate://crates.io/ahash/0.7.6 \
crate://crates.io/aho-corasick/0.7.15 \
crate://crates.io/alloc-no-stdlib/2.0.3 \
crate://crates.io/alloc-stdlib/0.2.1 \
crate://crates.io/arrayvec/0.5.2 \
crate://crates.io/askama/0.10.5 \
crate://crates.io/askama_derive/0.10.5 \
crate://crates.io/askama_escape/0.10.3 \
crate://crates.io/askama_shared/0.11.2 \
crate://crates.io/async-trait/0.1.56 \
crate://crates.io/autocfg/1.1.0 \
crate://crates.io/awc/2.0.3 \
crate://crates.io/base-x/0.2.11 \
crate://crates.io/base64/0.13.0 \
crate://crates.io/bitflags/1.3.2 \
crate://crates.io/bitvec/0.19.6 \
crate://crates.io/block-buffer/0.9.0 \
crate://crates.io/brotli-decompressor/2.3.2 \
crate://crates.io/brotli/3.3.4 \
crate://crates.io/bumpalo/3.10.0 \
crate://crates.io/byteorder/1.4.3 \
crate://crates.io/bytes/0.5.6 \
crate://crates.io/bytes/1.2.0 \
crate://crates.io/bytestring/1.1.0 \
crate://crates.io/cc/1.0.73 \
crate://crates.io/cfg-if/0.1.10 \
crate://crates.io/cfg-if/1.0.0 \
crate://crates.io/const_fn/0.4.9 \
crate://crates.io/convert_case/0.4.0 \
crate://crates.io/cookie/0.14.4 \
crate://crates.io/copyless/0.1.5 \
crate://crates.io/cpufeatures/0.2.2 \
crate://crates.io/crc32fast/1.3.2 \
crate://crates.io/crossbeam-channel/0.4.4 \
crate://crates.io/crossbeam-utils/0.7.2 \
crate://crates.io/derive_more/0.99.17 \
crate://crates.io/digest/0.9.0 \
crate://crates.io/discard/1.0.4 \
crate://crates.io/either/1.7.0 \
crate://crates.io/encoding_rs/0.8.31 \
crate://crates.io/enum-as-inner/0.3.4 \
crate://crates.io/flate2/1.0.24 \
crate://crates.io/fnv/1.0.7 \
crate://crates.io/form_urlencoded/1.0.1 \
crate://crates.io/fuchsia-zircon-sys/0.3.3 \
crate://crates.io/fuchsia-zircon/0.3.3 \
crate://crates.io/funty/1.1.0 \
crate://crates.io/futures-channel/0.3.21 \
crate://crates.io/futures-core/0.3.21 \
crate://crates.io/futures-io/0.3.21 \
crate://crates.io/futures-macro/0.3.21 \
crate://crates.io/futures-sink/0.3.21 \
crate://crates.io/futures-task/0.3.21 \
crate://crates.io/futures-util/0.3.21 \
crate://crates.io/futures/0.3.21 \
crate://crates.io/fxhash/0.2.1 \
crate://crates.io/generic-array/0.14.5 \
crate://crates.io/getrandom/0.1.16 \
crate://crates.io/getrandom/0.2.7 \
crate://crates.io/h2/0.2.7 \
crate://crates.io/hashbrown/0.11.2 \
crate://crates.io/hashbrown/0.12.3 \
crate://crates.io/heck/0.4.0 \
crate://crates.io/hermit-abi/0.1.19 \
crate://crates.io/hex/0.4.3 \
crate://crates.io/hostname/0.3.1 \
crate://crates.io/http/0.2.8 \
crate://crates.io/httparse/1.7.1 \
crate://crates.io/humansize/1.1.1 \
crate://crates.io/idna/0.2.3 \
crate://crates.io/indexmap/1.9.1 \
crate://crates.io/instant/0.1.12 \
crate://crates.io/iovec/0.1.4 \
crate://crates.io/ipconfig/0.2.2 \
crate://crates.io/itoa/0.4.8 \
crate://crates.io/itoa/1.0.2 \
crate://crates.io/kernel32-sys/0.2.2 \
crate://crates.io/language-tags/0.2.2 \
crate://crates.io/lazy_static/1.4.0 \
crate://crates.io/lexical-core/0.7.6 \
crate://crates.io/libc/0.2.126 \
crate://crates.io/linked-hash-map/0.5.6 \
crate://crates.io/lock_api/0.4.7 \
crate://crates.io/log/0.4.17 \
crate://crates.io/lru-cache/0.1.2 \
crate://crates.io/match_cfg/0.1.0 \
crate://crates.io/matches/0.1.9 \
crate://crates.io/maybe-uninit/2.0.0 \
crate://crates.io/memchr/2.3.4 \
crate://crates.io/mime/0.3.16 \
crate://crates.io/miniz_oxide/0.5.3 \
crate://crates.io/mio-uds/0.6.8 \
crate://crates.io/mio/0.6.23 \
crate://crates.io/miow/0.2.2 \
crate://crates.io/net2/0.2.37 \
crate://crates.io/nix/0.14.1 \
crate://crates.io/nom/6.2.1 \
crate://crates.io/num-traits/0.2.15 \
crate://crates.io/num_cpus/1.13.1 \
crate://crates.io/once_cell/1.13.0 \
crate://crates.io/opaque-debug/0.3.0 \
crate://crates.io/parking_lot/0.11.2 \
crate://crates.io/parking_lot_core/0.8.5 \
crate://crates.io/percent-encoding/2.1.0 \
crate://crates.io/pin-project-internal/0.4.30 \
crate://crates.io/pin-project-internal/1.0.11 \
crate://crates.io/pin-project-lite/0.1.12 \
crate://crates.io/pin-project-lite/0.2.9 \
crate://crates.io/pin-project/0.4.30 \
crate://crates.io/pin-project/1.0.11 \
crate://crates.io/pin-utils/0.1.0 \
crate://crates.io/ppv-lite86/0.2.16 \
crate://crates.io/proc-macro-hack/0.5.19 \
crate://crates.io/proc-macro2/1.0.41 \
crate://crates.io/quick-error/1.2.3 \
crate://crates.io/quote/1.0.20 \
crate://crates.io/radium/0.5.3 \
crate://crates.io/rand/0.7.3 \
crate://crates.io/rand_chacha/0.2.2 \
crate://crates.io/rand_core/0.5.1 \
crate://crates.io/rand_hc/0.2.0 \
crate://crates.io/redox_syscall/0.2.15 \
crate://crates.io/regex-syntax/0.6.27 \
crate://crates.io/regex/1.4.6 \
crate://crates.io/resolv-conf/0.7.0 \
crate://crates.io/rppal/0.7.1 \
crate://crates.io/rust-embed-impl/6.2.0 \
crate://crates.io/rust-embed-utils/7.2.0 \
crate://crates.io/rust-embed/6.4.0 \
crate://crates.io/rustc_version/0.2.3 \
crate://crates.io/rustc_version/0.4.0 \
crate://crates.io/ryu/1.0.10 \
crate://crates.io/same-file/1.0.6 \
crate://crates.io/scopeguard/1.1.0 \
crate://crates.io/semver-parser/0.7.0 \
crate://crates.io/semver/0.9.0 \
crate://crates.io/semver/1.0.12 \
crate://crates.io/serde/1.0.140 \
crate://crates.io/serde_derive/1.0.140 \
crate://crates.io/serde_json/1.0.82 \
crate://crates.io/serde_urlencoded/0.7.1 \
crate://crates.io/sha-1/0.9.8 \
crate://crates.io/sha1/0.6.1 \
crate://crates.io/sha1_smol/1.0.0 \
crate://crates.io/sha2/0.9.9 \
crate://crates.io/signal-hook-registry/1.4.0 \
crate://crates.io/slab/0.4.7 \
crate://crates.io/smallvec/1.9.0 \
crate://crates.io/socket2/0.3.19 \
crate://crates.io/spidev/0.4.1 \
crate://crates.io/standback/0.2.17 \
crate://crates.io/static_assertions/1.1.0 \
crate://crates.io/stdweb-derive/0.5.3 \
crate://crates.io/stdweb-internal-macros/0.2.9 \
crate://crates.io/stdweb-internal-runtime/0.1.5 \
crate://crates.io/stdweb/0.4.20 \
crate://crates.io/syn/1.0.98 \
crate://crates.io/tap/1.0.1 \
crate://crates.io/thiserror-impl/1.0.31 \
crate://crates.io/thiserror/1.0.31 \
crate://crates.io/threadpool/1.8.1 \
crate://crates.io/time-macros-impl/0.1.2 \
crate://crates.io/time-macros/0.1.1 \
crate://crates.io/time/0.2.27 \
crate://crates.io/tinyvec/1.6.0 \
crate://crates.io/tinyvec_macros/0.1.0 \
crate://crates.io/tokio-util/0.3.1 \
crate://crates.io/tokio/0.2.25 \
crate://crates.io/toml/0.5.9 \
crate://crates.io/tracing-core/0.1.28 \
crate://crates.io/tracing-futures/0.2.5 \
crate://crates.io/tracing/0.1.35 \
crate://crates.io/trust-dns-proto/0.19.7 \
crate://crates.io/trust-dns-resolver/0.19.7 \
crate://crates.io/typenum/1.15.0 \
crate://crates.io/unicode-bidi/0.3.8 \
crate://crates.io/unicode-ident/1.0.2 \
crate://crates.io/unicode-normalization/0.1.21 \
crate://crates.io/url/2.2.2 \
crate://crates.io/version_check/0.9.4 \
crate://crates.io/void/1.0.2 \
crate://crates.io/walkdir/2.3.2 \
crate://crates.io/wasi/0.11.0+wasi-snapshot-preview1 \
crate://crates.io/wasi/0.9.0+wasi-snapshot-preview1 \
crate://crates.io/wasm-bindgen-backend/0.2.81 \
crate://crates.io/wasm-bindgen-macro-support/0.2.81 \
crate://crates.io/wasm-bindgen-macro/0.2.81 \
crate://crates.io/wasm-bindgen-shared/0.2.81 \
crate://crates.io/wasm-bindgen/0.2.81 \
crate://crates.io/widestring/0.4.3 \
crate://crates.io/winapi-build/0.1.1 \
crate://crates.io/winapi-i686-pc-windows-gnu/0.4.0 \
crate://crates.io/winapi-util/0.1.5 \
crate://crates.io/winapi-x86_64-pc-windows-gnu/0.4.0 \
crate://crates.io/winapi/0.2.8 \
crate://crates.io/winapi/0.3.9 \
crate://crates.io/winreg/0.6.2 \
crate://crates.io/ws2_32-sys/0.2.1 \
crate://crates.io/wyz/0.2.0 \
"
# FIXME: update generateme with the real MD5 of the license file
LIC_FILES_CHKSUM = " \
"
SUMMARY = "aw-lights"
HOMEPAGE = "https://gitea.austen-wares.com/stonewareslord/aw-lights.git"
LICENSE = "CLOSED"
# includes this file if it exists but does not fail
# this is useful for anything you may want to override from
# what cargo-bitbake generates.
include aw-lights-${PV}.inc
include aw-lights.inc

View File

@ -1,12 +0,0 @@
use actix_web_static_files::NpmBuild;
fn main() {
NpmBuild::new("./web")
.install()
.unwrap()
.run("build")
.unwrap()
.target("./web/public")
.to_resource_dir()
.build()
.unwrap();
}

14
common/Cargo.toml Normal file
View File

@ -0,0 +1,14 @@
[package]
name = "common"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = {version = "1.0", features = ["derive"]}
hex = "0.4.3"
serde_json = "1"
parking_lot = "0.12"
crossbeam-channel = "0.5.6"
strum = { version = "0.24.1", features = ["derive"] }
tracing = "0.1.37"
clap = { version = "4.5.4", features = ["derive", "env"] }

340
common/src/cava.rs Normal file
View File

@ -0,0 +1,340 @@
use crossbeam_channel::{select, unbounded, Sender};
use std::{
fs::File,
io::Write,
path::{Path, PathBuf},
process::{Child, Command, Stdio},
sync::Arc,
thread,
time::Duration,
};
use tracing::{error, info};
use crate::{
final_ring::FinalRing,
pattern::{PatternError, PatternResult},
};
#[derive(Debug)]
pub struct Cava {
terminate_tx: Sender<()>,
// cava_process: Child,
// num_bars: u16,
final_ring: Arc<FinalRing>,
}
impl Cava {
fn spawn_cava<P: AsRef<Path>>(config_file: P) -> PatternResult<Child> {
// Start cava
let mut cava_process = Command::new("cava")
// DISPLAY cannot be set
.env_remove("DISPLAY")
.arg("-p")
.arg(config_file.as_ref())
.stderr(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.map_err(|e| PatternError::CommandNotFound(e.to_string()))?;
info!("New cava process spawned");
// Send cava stderr to our stderr
// TODO: Can we just remove the stderr(piped) above?
let mut stderr_reader = cava_process.stderr.take().unwrap();
let _stderr_thread = thread::spawn(move || {
std::io::copy(&mut stderr_reader, &mut std::io::stderr()).unwrap()
});
Ok(cava_process)
}
pub fn new(num_bars: u16) -> PatternResult<Self> {
info!("Making config file");
let config_file = Self::write_cava_config_file(num_bars as usize * 2)?;
info!("Starting cava");
let mut cava_process = Self::spawn_cava(&config_file)?;
// TODO: * 2 because it's a u16
let final_ring = Arc::new(FinalRing::new(num_bars as usize * 2 * 2));
let f = final_ring.clone();
// Make a channel to trigger termination of cava
let (terminate_tx, terminate_rx) = unbounded();
let _reader_thread = thread::spawn(move || {
loop {
// Start reading from cava
// let f_clone = f.clone();
let reader = cava_process.stdout.take().unwrap();
let (reader_error_tx, reader_error_rx) = unbounded();
let f_clone = f.clone();
let _reader_thread = thread::spawn(move || {
// The reader should never end so this will be an error
let _ = f_clone.read_loop(reader);
reader_error_tx.send(()).unwrap();
});
// Wait for either the reader to fail, cava to crash, or a termination request
let should_terminate = select! {
recv(terminate_rx) -> _ => {
// Stop, because they want us to terminate
true
}
recv(reader_error_rx) -> _ => {
error!("Cava reader crashed. Will attempt a restart");
false
}
};
// We cannot kill a thread. Instead, kill the cava process and expect the threads to die
info!("Terminating cava process");
if let Err(e) = cava_process.kill() {
error!("Error trying to kill cava process: {e:?}");
}
if should_terminate {
// We need to stop. Don't attempt to start a new cava
break;
} else {
// Wait a second before continuing
std::thread::sleep(Duration::from_secs(1));
// Try spinning up a new cava instance
cava_process = match Self::spawn_cava(&config_file) {
Ok(c) => c,
Err(e) => {
error!("Could not spawn a new cava process: {e:?}");
break;
}
}
}
}
});
Ok(Self {
terminate_tx,
// cava_process,
final_ring,
})
}
pub fn terminate(self) {
self.terminate_tx.send(()).unwrap();
}
pub fn get_latest_reading(&self) -> Vec<u16> {
self.final_ring
.get_last_bytes()
.array_chunks()
.map(|ac| u16::from_le_bytes(*ac))
.collect()
}
fn write_cava_config_file(num_bars: usize) -> PatternResult<PathBuf> {
// TODO: Fix config file location
let config_file = PathBuf::from("/tmp/config_file.cava");
let mut f = File::create(&config_file)?;
f.write_all(
format!(
r#"## Configuration file for CAVA. Default values are commented out. Use either ';' or '#' for commenting.
[general]
# Smoothing mode. Can be 'normal', 'scientific' or 'waves'. DEPRECATED as of 0.6.0
; mode = normal
# Accepts only non-negative values.
; framerate = 60
# 'autosens' will attempt to decrease sensitivity if the bars peak. 1 = on, 0 = off
# new as of 0.6.0 autosens of low values (dynamic range)
# 'overshoot' allows bars to overshoot (in % of terminal height) without initiating autosens. DEPRECATED as of 0.6.0
; autosens = 1
; overshoot = 20
# Manual sensitivity in %. If autosens is enabled, this will only be the initial value.
# 200 means double height. Accepts only non-negative values.
; sensitivity = 100
# The number of bars (0-200). 0 sets it to auto (fill up console).
# Bars' width and space between bars in number of characters.
bars = {num_bars}
; bar_width = 2
; bar_spacing = 1
# bar_height is only used for output in "noritake" format
; bar_height = 32
# For SDL width and space between bars is in pixels, defaults are:
; bar_width = 20
; bar_spacing = 5
# Lower and higher cutoff frequencies for lowest and highest bars
# the bandwidth of the visualizer.
# Note: there is a minimum total bandwidth of 43Mhz x number of bars.
# Cava will automatically increase the higher cutoff if a too low band is specified.
; lower_cutoff_freq = 50
; higher_cutoff_freq = 10000
# Seconds with no input before cava goes to sleep mode. Cava will not perform FFT or drawing and
# only check for input once per second. Cava will wake up once input is detected. 0 = disable.
; sleep_timer = 0
[input]
# Audio capturing method. Possible methods are: 'pulse', 'alsa', 'fifo', 'sndio' or 'shmem'
# Defaults to 'pulse', 'alsa' or 'fifo', in that order, dependent on what support cava was built with.
#
# All input methods uses the same config variable 'source'
# to define where it should get the audio.
#
# For pulseaudio 'source' will be the source. Default: 'auto', which uses the monitor source of the default sink
# (all pulseaudio sinks(outputs) have 'monitor' sources(inputs) associated with them).
#
# For alsa 'source' will be the capture device.
# For fifo 'source' will be the path to fifo-file.
# For shmem 'source' will be /squeezelite-AA:BB:CC:DD:EE:FF where 'AA:BB:CC:DD:EE:FF' will be squeezelite's MAC address
; method = pulse
; source = auto
; method = alsa
; source = hw:Loopback,1
; method = fifo
; source = /tmp/mpd.fifo
; sample_rate = 44100
; sample_bits = 16
; method = shmem
; source = /squeezelite-AA:BB:CC:DD:EE:FF
; method = portaudio
; source = auto
[output]
# Output method. Can be 'ncurses', 'noncurses', 'raw', 'noritake' or 'sdl'.
# 'noncurses' uses a custom framebuffer technique and prints only changes
# from frame to frame in the terminal. 'ncurses' is default if supported.
#
# 'raw' is an 8 or 16 bit (configurable via the 'bit_format' option) data
# stream of the bar heights that can be used to send to other applications.
# 'raw' defaults to 200 bars, which can be adjusted in the 'bars' option above.
#
# 'noritake' outputs a bitmap in the format expected by a Noritake VFD display
# in graphic mode. It only support the 3000 series graphical VFDs for now.
#
# 'sdl' uses the Simple DirectMedia Layer to render in a graphical context.
method = raw
# Visual channels. Can be 'stereo' or 'mono'.
# 'stereo' mirrors both channels with low frequencies in center.
# 'mono' outputs left to right lowest to highest frequencies.
# 'mono_option' set mono to either take input from 'left', 'right' or 'average'.
# set 'reverse' to 1 to display frequencies the other way around.
; channels = stereo
; mono_option = average
; reverse = 0
# Raw output target. A fifo will be created if target does not exist.
; raw_target = /dev/stdout
# Raw data format. Can be 'binary' or 'ascii'.
; data_format = binary
# Binary bit format, can be '8bit' (0-255) or '16bit' (0-65530).
; bit_format = 16bit
# Ascii max value. In 'ascii' mode range will run from 0 to value specified here
; ascii_max_range = 1000
# Ascii delimiters. In ascii format each bar and frame is separated by a delimiters.
# Use decimal value in ascii table (i.e. 59 = ';' and 10 = '\n' (line feed)).
; bar_delimiter = 59
; frame_delimiter = 10
# sdl window size and position. -1,-1 is centered.
; sdl_width = 1000
; sdl_height = 500
; sdl_x = -1
; sdl_y= -1
[color]
# Colors can be one of seven predefined: black, blue, cyan, green, magenta, red, white, yellow.
# Or defined by hex code '#xxxxxx' (hex code must be within ''). User defined colors requires
# ncurses output method and a terminal that can change color definitions such as Gnome-terminal or rxvt.
# if supported, ncurses mode will be forced on if user defined colors are used.
# default is to keep current terminal color
; background = default
; foreground = default
# SDL only support hex code colors, these are the default:
; background = '#111111'
; foreground = '#33cccc'
# Gradient mode, only hex defined colors (and thereby ncurses mode) are supported,
# background must also be defined in hex or remain commented out. 1 = on, 0 = off.
# You can define as many as 8 different colors. They range from bottom to top of screen
; gradient = 0
; gradient_count = 8
; gradient_color_1 = '#59cc33'
; gradient_color_2 = '#80cc33'
; gradient_color_3 = '#a6cc33'
; gradient_color_4 = '#cccc33'
; gradient_color_5 = '#cca633'
; gradient_color_6 = '#cc8033'
; gradient_color_7 = '#cc5933'
; gradient_color_8 = '#cc3333'
[smoothing]
# Percentage value for integral smoothing. Takes values from 0 - 100.
# Higher values means smoother, but less precise. 0 to disable.
# DEPRECATED as of 0.8.0, use noise_reduction instead
; integral = 77
# Disables or enables the so-called "Monstercat smoothing" with or without "waves". Set to 0 to disable.
; monstercat = 0
; waves = 0
# Set gravity percentage for "drop off". Higher values means bars will drop faster.
# Accepts only non-negative values. 50 means half gravity, 200 means double. Set to 0 to disable "drop off".
# DEPRECATED as of 0.8.0, use noise_reduction instead
; gravity = 100
# In bar height, bars that would have been lower that this will not be drawn.
# DEPRECATED as of 0.8.0
; ignore = 0
# Noise reduction, float 0 - 1. default 0.77
# the raw visualization is very noisy, this factor adjusts the integral and gravity filters to keep the signal smooth
# 1 will be very slow and smooth, 0 will be fast but noisy.
; noise_reduction = 0.77
[eq]
# This one is tricky. You can have as much keys as you want.
# Remember to uncomment more then one key! More keys = more precision.
# Look at readme.md on github for further explanations and examples.
# DEPRECATED as of 0.8.0 can be brought back by popular request, open issue at:
# https://github.com/karlstav/cava
; 1 = 1 # bass
; 2 = 1
; 3 = 1 # midtone
; 4 = 1
; 5 = 1 # treble"#
)
.as_bytes(),
)?;
f.flush()?;
Ok(config_file)
}
}

View File

@ -1,9 +1,8 @@
use serde::Deserialize; use crate::pattern::{PatternError, PatternResult};
use serde::Serialize; use serde::{Deserialize, Serialize};
use std::num::ParseIntError; use std::{iter, num::ParseIntError, str::FromStr};
use std::str::FromStr;
#[derive(Copy, Clone, Debug, PartialEq, Deserialize, Serialize)] #[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Default)]
pub struct Rgb(pub u8, pub u8, pub u8); pub struct Rgb(pub u8, pub u8, pub u8);
impl Rgb { impl Rgb {
pub const fn to_tuple(self) -> (u8, u8, u8) { pub const fn to_tuple(self) -> (u8, u8, u8) {
@ -12,6 +11,9 @@ impl Rgb {
pub fn to_float_tuple(self) -> (f32, f32, f32) { pub fn to_float_tuple(self) -> (f32, f32, f32) {
(f32::from(self.0), f32::from(self.1), f32::from(self.2)) (f32::from(self.0), f32::from(self.1), f32::from(self.2))
} }
pub fn to_hex_str(self) -> String {
format!("#{:02x}{:02x}{:02x}", self.0, self.1, self.2)
}
// pub fn to_gamma_corrected_tuple(&self) -> (u8, u8, u8) { // pub fn to_gamma_corrected_tuple(&self) -> (u8, u8, u8) {
// ( // (
// GAMMA_CORRECT[self.0 as usize], // GAMMA_CORRECT[self.0 as usize],
@ -37,6 +39,7 @@ impl FromStr for Rgb {
.map_err(|_| ()) .map_err(|_| ())
.and_then(|v| { .and_then(|v| {
Ok(Self( Ok(Self(
#[allow(clippy::get_first)]
*v.get(0).ok_or(())?, *v.get(0).ok_or(())?,
*v.get(1).ok_or(())?, *v.get(1).ok_or(())?,
*v.get(2).ok_or(())?, *v.get(2).ok_or(())?,
@ -71,6 +74,7 @@ pub const WHITE: Rgb = Rgb(255, 255, 255);
// 177, 180, 182, 184, 186, 189, 191, 193, 196, 198, 200, 203, 205, 208, 210, 213, 215, 218, 220, // 177, 180, 182, 184, 186, 189, 191, 193, 196, 198, 200, 203, 205, 208, 210, 213, 215, 218, 220,
// 223, 225, 228, 231, 233, 236, 239, 241, 244, 247, 249, 252, 255, // 223, 225, 228, 231, 233, 236, 239, 241, 244, 247, 249, 252, 255,
// ]; // ];
pub const RAINBOW: [Rgb; 7] = [ pub const RAINBOW: [Rgb; 7] = [
Rgb(255, 0, 0), // R Rgb(255, 0, 0), // R
Rgb(255, 128, 0), // O Rgb(255, 128, 0), // O
@ -81,17 +85,34 @@ pub const RAINBOW: [Rgb; 7] = [
Rgb(148, 0, 211), // V Rgb(148, 0, 211), // V
]; ];
/// Merges a color by some factor /// Stretch
pub fn merge_colors(from_color: Rgb, to_color: Rgb, factor: f32) -> Rgb { ///
let (from_r, from_g, from_b) = from_color.to_float_tuple(); /// ```
let (to_r, to_g, to_b) = to_color.to_float_tuple(); /// use common::color::{stretch, Rgb};
/// let from_array = [Rgb(0,0,0), Rgb(1,1,1), Rgb(2,2,2)];
///
/// assert_eq!(stretch(&from_array[..], 1).collect::<Vec<Rgb>>(), [Rgb(0,0,0)]);
/// assert_eq!(stretch(&from_array[..], 3).collect::<Vec<Rgb>>(), [Rgb(0,0,0), Rgb(1,1,1), Rgb(2,2,2)]);
/// assert_eq!(stretch(&from_array[..], 4).collect::<Vec<Rgb>>(), [Rgb(0,0,0), Rgb(0,0,0), Rgb(1,1,1), Rgb(2,2,2)]);
/// assert_eq!(stretch(&from_array[..], 5).collect::<Vec<Rgb>>(), [Rgb(0,0,0), Rgb(0,0,0), Rgb(1,1,1), Rgb(1,1,1), Rgb(2,2,2)]);
/// assert_eq!(stretch(&from_array[..], 6).collect::<Vec<Rgb>>(), [Rgb(0,0,0), Rgb(0,0,0), Rgb(1,1,1), Rgb(1,1,1), Rgb(2,2,2), Rgb(2,2,2)]);
/// assert_eq!(stretch(&from_array[..], 7).collect::<Vec<Rgb>>(), [Rgb(0,0,0), Rgb(0,0,0), Rgb(0,0,0), Rgb(1,1,1), Rgb(1,1,1), Rgb(2,2,2), Rgb(2,2,2)]);
/// ```
pub fn stretch(from: &[Rgb], to_size: usize) -> impl Iterator<Item = Rgb> + std::fmt::Debug + '_ {
let count: usize = to_size / from.len();
let mut additional_remainder = to_size.rem_euclid(from.len());
// TODO: Do not use as u8 from.iter().flat_map(move |&x| {
let r = (to_r - from_r).mul_add(factor, from_r) as u8; // The number of copies of this this segment
let g = (to_g - from_g).mul_add(factor, from_g) as u8; // Can be either count, or count+1, depending on
let b = (to_b - from_b).mul_add(factor, from_b) as u8; let segment_count = (count).saturating_add(if additional_remainder > 0 {
additional_remainder = additional_remainder.saturating_sub(1);
Rgb(r, g, b) 1
} else {
0
});
iter::repeat(x).take(segment_count)
})
} }
/// Builds a color ramp of length `length` of the (exclusive) bounds of `from_color` to `to_color` /// Builds a color ramp of length `length` of the (exclusive) bounds of `from_color` to `to_color`
@ -99,20 +120,28 @@ pub fn build_ramp(from_color: Rgb, to_color: Rgb, length: usize) -> Vec<Rgb> {
let offset = 1.0_f32 / (length as f32 + 1.0_f32); let offset = 1.0_f32 / (length as f32 + 1.0_f32);
let mut ret: Vec<Rgb> = vec![]; let mut ret: Vec<Rgb> = vec![];
for step in 1..=length { for step in 1..=length {
ret.push(merge_colors(from_color, to_color, offset * step as f32)); ret.push(from_color.fade_to(to_color, offset * step as f32));
} }
ret ret
} }
pub fn min_with_factor(at_least: u16, factor: u16) -> Result<u16, ()> { /// Returns the minimum number that is evenly divisible by `factor`
Ok(at_least ///
.checked_sub(1) /// ```rust
.ok_or(())? /// use common::color::min_with_factor;
/// assert_eq!(min_with_factor(0, 20).unwrap(), 20);
/// assert_eq!(min_with_factor(10, 20).unwrap(), 20);
/// assert_eq!(min_with_factor(20, 20).unwrap(), 20);
/// assert_eq!(min_with_factor(21, 20).unwrap(), 40);
/// ```
pub fn min_with_factor(at_least: u16, factor: u16) -> PatternResult<u16> {
at_least
.saturating_sub(1)
.div_euclid(factor) .div_euclid(factor)
.checked_add(1) .checked_add(1)
.ok_or(())? .ok_or(PatternError::ArithmeticError)?
.checked_mul(factor) .checked_mul(factor)
.ok_or(())?) .ok_or(PatternError::ArithmeticError)
} }
#[cfg(test)] #[cfg(test)]
@ -130,3 +159,42 @@ mod tests {
assert_eq!(BLACK, Rgb(0, 0, 0)); assert_eq!(BLACK, Rgb(0, 0, 0));
} }
} }
pub trait Gradient<T> {
fn fade_to(&self, other: Self, factor: T) -> Self;
}
impl Gradient<i8> for Rgb {
fn fade_to(&self, to_color: Rgb, factor: i8) -> Rgb {
let factor = (factor as f32) / (i8::MAX as f32);
self.fade_to(to_color, factor)
}
}
impl Gradient<u8> for Rgb {
fn fade_to(&self, to_color: Rgb, factor: u8) -> Rgb {
let factor = (factor as f32) / (u8::MAX as f32);
self.fade_to(to_color, factor)
}
}
impl Gradient<u16> for Rgb {
fn fade_to(&self, to_color: Rgb, factor: u16) -> Rgb {
let factor = (factor as f32) / (u16::MAX as f32);
self.fade_to(to_color, factor)
}
}
impl Gradient<f32> for Rgb {
fn fade_to(&self, to_color: Rgb, factor: f32) -> Rgb {
let (from_r, from_g, from_b) = self.to_float_tuple();
let (to_r, to_g, to_b) = to_color.to_float_tuple();
// TODO: Do not use as u8
let r = (to_r - from_r).mul_add(factor, from_r) as u8;
let g = (to_g - from_g).mul_add(factor, from_g) as u8;
let b = (to_b - from_b).mul_add(factor, from_b) as u8;
Rgb(r, g, b)
}
}

View File

@ -1,23 +1,22 @@
use crate::pattern::PatternError;
use core::any::Any; use core::any::Any;
use std::fmt; use std::{fmt, io};
use std::io;
pub type ProgramResult<T> = Result<T, ProgramError>; pub type ProgramResult<T> = Result<T, ProgramError>;
#[derive(Debug)]
pub enum Message {
Error(ProgramError),
Terminated,
String(String),
InputPrompt(String),
}
#[derive(Debug)] #[derive(Debug)]
pub enum ProgramError { pub enum ProgramError {
General(String), General(String),
UiError(String), UiError(String),
Boxed(Box<dyn Any + Send>), Boxed(Box<dyn Any + Send>),
IoError(io::Error), IoError(io::Error),
PatternError(PatternError),
}
impl From<PatternError> for ProgramError {
fn from(e: PatternError) -> Self {
Self::PatternError(e)
}
} }
impl From<String> for ProgramError { impl From<String> for ProgramError {
@ -41,10 +40,11 @@ impl From<Box<dyn Any + Send>> for ProgramError {
impl fmt::Display for ProgramError { impl fmt::Display for ProgramError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
match self { match self {
Self::General(s) => write!(f, "{}", s), Self::General(s) => write!(f, "{s}"),
Self::UiError(s) => write!(f, "Critial UI error: {}", s), Self::UiError(s) => write!(f, "Critial UI error: {s}"),
Self::Boxed(s) => write!(f, "{:?}", s), Self::Boxed(s) => write!(f, "{s:?}"),
Self::IoError(e) => write!(f, "{:?}", e), Self::IoError(e) => write!(f, "{e:?}"),
Self::PatternError(e) => write!(f, "{e:?}"),
} }
} }
} }

35
common/src/final_ring.rs Normal file
View File

@ -0,0 +1,35 @@
use parking_lot::Mutex;
use std::io::{Read, Result};
use tracing::{error, info};
#[derive(Debug)]
pub struct FinalRing {
inner: Mutex<Vec<u8>>,
size: usize,
}
impl FinalRing {
pub fn new(size: usize) -> Self {
info!("Initializing FinalRing with size: {size}");
Self {
size,
inner: Mutex::new(vec![0; size]),
}
}
pub fn get_last_bytes(&self) -> Vec<u8> {
self.inner.lock().to_vec()
}
pub fn read_loop<R: Read>(&self, mut read: R) -> Result<()> {
let mut buf = vec![0; self.size];
loop {
if let Err(e) = read.read_exact(&mut buf) {
error!("Error reading: {e:?}");
break Err(e);
}
std::mem::swap(&mut *self.inner.lock(), &mut buf);
}
}
}

107
common/src/lib.rs Normal file
View File

@ -0,0 +1,107 @@
#![feature(array_chunks)]
use clap::{Args, Parser};
use error::ProgramError;
mod cava;
pub mod color;
pub mod error;
mod final_ring;
pub mod pattern;
pub mod strip;
/// Maximum number of lights allowed
pub const MAX_NUM_LIGHTS: u16 = 200;
/// Default time per tick
pub const DEFAULT_TICK_TIME_MS: u64 = 10;
/// Minimum time per tick before strip breaks
pub const MIN_TICK_TIME: u64 = 10;
#[derive(Debug)]
pub enum Message {
Error(ProgramError),
Terminated,
String(String),
// InputPrompt(String),
}
#[derive(Debug, Clone, Parser)]
pub struct Config {
/// Number of lights
#[clap(short, long, env, help = "Number of lights in the strip", value_parser = clap::value_parser!(u16).range(1..=(MAX_NUM_LIGHTS as i64)))]
pub num_lights: u16,
/// Number of lights to skip
#[clap(
short,
long,
env,
default_value_t = 0,
help = "Number of lights to skip in the beginning"
)]
pub skip_lights: u16,
/// Global brightness multiplier
#[clap(
long,
env,
default_value_t = 255,
help = "The max brightness (clamped)"
)]
pub global_brightness_max: u8,
/// Time per tick
#[clap(long, env, default_value_t = DEFAULT_TICK_TIME_MS, help = "Tick time in milliseconds", value_parser = clap::value_parser!(u64).range(MIN_TICK_TIME..))]
pub tick_time_ms: u64,
/// The default adapter
#[clap(
long,
env,
default_value = "/dev/spidev0.0",
help = "The serial interface"
)]
pub serial_interface: String,
/// The initial pattern
#[clap(short, env, long, help = "The name of the initial pattern")]
pub initial_pattern: Option<String>,
#[clap(env, long, help = "The light strip should be mirrored down the middle")]
pub mirrored_lights: bool,
#[clap(
long,
env,
help = "If mirrored_lights is true, reverse the mirror direction"
)]
pub reverse_mirror: bool,
#[clap(short, long, env, help = "Reverse all patterns")]
pub reverse: bool,
#[clap(flatten)]
pub mqtt: MqttConfig,
}
#[derive(Debug, Args, Clone)]
pub struct MqttConfig {
#[clap(long, env, help = "MQTT broker host")]
pub mqtt_broker: String,
#[clap(long, env, help = "MQTT broker port", default_value = "1883")]
pub mqtt_port: u16,
#[clap(long, env, help = "MQTT device id", value_parser = mqtt_topic_segment)]
pub mqtt_id: String,
#[clap(long, env, help = "Discovery prefix", default_value = "homeassistant", value_parser = mqtt_topic_segment)]
pub mqtt_discovery_prefix: String,
#[clap(long, env, help = "MQTT username", requires_all = ["mqtt_password"])]
pub mqtt_username: Option<String>,
#[clap(long, env, help = "MQTT user password")]
pub mqtt_password: Option<String>,
}
fn mqtt_topic_segment(s: &str) -> Result<String, &'static str> {
if s.is_empty() {
Err("MQTT topic segments must not be empty")
} else if s.contains('/') {
Err("MQTT topic segments cannot contain '/'")
} else {
Ok(s.to_string())
}
}

245
common/src/pattern.rs Normal file
View File

@ -0,0 +1,245 @@
use crate::color::Rgb;
use serde::{Deserialize, Serialize};
pub mod car_rainbow;
pub mod collide;
pub mod custom_visualizer;
pub mod fade;
pub mod flashing;
pub mod moving_pixel;
pub mod moving_rainbow;
pub mod orb;
pub mod slide;
pub mod solid;
pub mod visualizer;
pub use car_rainbow::{CarRainbow, CarRainbowParams};
pub use collide::{Collide, CollideParams};
pub use custom_visualizer::{CustomVisualizer, CustomVisualizerParams};
pub use fade::{Fade, FadeParams};
pub use flashing::{Flashing, FlashingParams};
pub use moving_pixel::{MovingPixel, MovingPixelParams};
pub use moving_rainbow::{MovingRainbow, MovingRainbowParams};
pub use orb::{Orb, OrbParams};
pub use slide::{Slide, SlideParams};
pub use solid::{Solid, SolidParams};
pub use visualizer::{Visualizer, VisualizerParams};
pub type ColorIterator<'a> = Box<dyn DoubleEndedIterator<Item = &'a Rgb> + 'a>;
pub type PatternResult<T> = Result<T, PatternError>;
#[derive(Debug)]
pub enum PatternError {
ArithmeticError,
Index,
LightCount,
CommandNotFound(String),
IoError(std::io::Error),
InternalError,
}
impl From<std::io::Error> for PatternError {
fn from(e: std::io::Error) -> Self {
Self::IoError(e)
}
}
pub trait FormRender {
fn render(&self) -> String;
}
pub trait InputRender {
fn render(&self, name: &str, multi_index: Option<usize>) -> String;
}
impl InputRender for bool {
fn render(&self, name: &str, multi_index: Option<usize>) -> String {
format!(
r#"<label for="{name}">{name}</label><input type="checkbox" name="{name}" {}{}/>"#,
if *self { " checked" } else { "" },
if let Some(i) = multi_index {
format!(r#" name="{name}-{i}" rust-form-multi="{name}""#)
} else {
format!(r#" name="{name}""#)
},
name = name
)
}
}
impl InputRender for Rgb {
fn render(&self, name: &str, multi_index: Option<usize>) -> String {
format!(
r#"<label for="{name}">{name}</label><input type="color" value="{}" name="{name}" {}/>"#,
self.to_hex_str(),
if let Some(i) = multi_index {
format!(r#" name="{name}-{i}" rust-form-multi="{name}""#)
} else {
format!(r#" name="{name}""#)
},
name = name
)
}
}
impl InputRender for Vec<Rgb> {
fn render(&self, name: &str, _multi_index: Option<usize>) -> String {
self.iter()
// .chain(iter::once(&Rgb::default()))
.enumerate()
.fold(String::new(), |acc, (i, c)| {
acc + &c.render(name, Some(i)) + "\n"
})
}
}
impl InputRender for u8 {
fn render(&self, name: &str, multi_index: Option<usize>) -> String {
format!(
r#"<label for="{name}">{name}</label><input type="number" max="255" min="0" step="1" value="{}" name="{name}" {}/>"#,
self,
if let Some(i) = multi_index {
format!(r#" name="{name}-{i}" rust-form-multi="{name}""#)
} else {
format!(r#" name="{name}""#)
},
name = name
)
}
}
impl InputRender for u16 {
fn render(&self, name: &str, multi_index: Option<usize>) -> String {
format!(
r#"<label for="{name}">{name}</label><input type="number" max="65535" min="0" step="1" value="{}" name="{name}" {}/>"#,
self,
if let Some(i) = multi_index {
format!(r#" name="{name}-{i}" rust-form-multi="{name}""#)
} else {
format!(r#" name="{name}""#)
},
name = name
)
}
}
#[derive(Serialize, Deserialize, Clone, Debug, strum::Display, strum::EnumString)]
pub enum Parameters {
Collide(CollideParams),
Slide(SlideParams),
Fade(FadeParams),
MovingPixel(MovingPixelParams),
MovingRainbow(MovingRainbowParams),
CarRainbow(CarRainbowParams),
Orb(OrbParams),
Solid(SolidParams),
Visualizer(VisualizerParams),
CustomVisualizer(CustomVisualizerParams),
Flashing(FlashingParams),
}
impl Default for Parameters {
fn default() -> Self {
Self::MovingRainbow(Default::default())
}
}
impl FormRender for Parameters {
fn render(&self) -> String {
match self {
Self::Collide(ref p) => p.render(),
Self::Slide(ref p) => p.render(),
Self::Fade(ref p) => p.render(),
Self::MovingPixel(ref p) => p.render(),
Self::MovingRainbow(ref p) => p.render(),
Self::CarRainbow(ref p) => p.render(),
Self::Orb(ref p) => p.render(),
Self::Solid(ref p) => p.render(),
Self::Visualizer(ref p) => p.render(),
Self::CustomVisualizer(ref p) => p.render(),
Self::Flashing(ref p) => p.render(),
}
}
}
impl Parameters {
pub const fn get_names() -> &'static [&'static str] {
&[
"Solid",
"CustomVisualizer",
"Visualizer",
"Collide",
"Slide",
"Fade",
"MovingPixel",
"MovingRainbow",
"CarRainbow",
"Orb",
"Flashing",
]
}
pub fn to_pattern(&self) -> Box<dyn Pattern + Send + Sync> {
match self {
Self::Collide(ref p) => Box::new(Collide::new(p)),
Self::Slide(ref p) => Box::new(Slide::new(p)),
Self::Fade(ref p) => Box::new(Fade::new(p)),
Self::MovingPixel(ref p) => Box::new(MovingPixel::new(p)),
Self::MovingRainbow(ref p) => Box::new(MovingRainbow::new(p)),
Self::CarRainbow(ref p) => Box::new(CarRainbow::new(p)),
Self::Orb(ref p) => Box::new(Orb::new(p)),
Self::Solid(ref p) => Box::new(Solid::new(p)),
Self::Visualizer(ref p) => Box::new(Visualizer::new(p)),
Self::CustomVisualizer(ref p) => Box::new(CustomVisualizer::new(p)),
Self::Flashing(ref p) => Box::new(Flashing::new(p)),
}
}
}
pub trait Pattern: std::fmt::Debug + Send + Sync {
fn get_name(&self) -> &'static str;
fn init(&mut self, num_lights: u16) -> PatternResult<()>;
fn step(&mut self) -> PatternResult<bool>;
fn get_strip(&self) -> ColorIterator;
fn cleanup(&mut self) -> PatternResult<()> {
Ok(())
}
}
// #[cfg(test)]
// mod tests {
// use super::*;
// const NUM_LIGHTS: u16 = 10;
// fn test_strip() -> Vec<Rgb> {
// vec![color::BLACK; NUM_LIGHTS.into()]
// }
// #[test]
// fn moving_pixel() {
// let color = Rgb(123, 152, 89);
// let mut pat = MovingPixel::new(color.clone());
// let mut strip = test_strip();
// assert!(pat.init(&mut strip, NUM_LIGHTS).is_ok());
// // One is my color
// assert_eq!(strip.iter().filter(|c| **c == color).count(), 1);
// // The rest are off
// assert_eq!(
// strip.iter().filter(|c| **c == color::BLACK).count(),
// (NUM_LIGHTS - 1).into()
// );
// pat.step(&mut strip);
// // One is my color
// assert_eq!(strip.iter().filter(|c| **c == color).count(), 1);
// // The rest are off
// assert_eq!(
// strip.iter().filter(|c| **c == color::BLACK).count(),
// (NUM_LIGHTS - 1).into()
// );
// }
// #[test]
// fn solid() {}
// #[test]
// fn moving_rainbow() {}
// #[test]
// fn fade() {}
// #[test]
// fn collide() {}
// }

View File

@ -0,0 +1,103 @@
use super::{ColorIterator, FormRender, InputRender, Pattern, PatternError, PatternResult};
use crate::color::{self, Rgb, RAINBOW};
use serde::{Deserialize, Serialize};
use std::{collections::VecDeque, convert::TryFrom, iter};
// use tracing::info;
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct CarRainbowParams {
pub width: u8,
pub skip: u8,
}
impl Default for CarRainbowParams {
fn default() -> Self {
Self { width: 8, skip: 4 }
}
}
impl FormRender for CarRainbowParams {
fn render(&self) -> String {
[
self.width.render("width", None),
self.skip.render("skip", None),
]
.concat()
}
}
#[derive(Clone, Debug)]
pub struct CarRainbow {
lights_buf: VecDeque<Rgb>,
/// The index to split, if Rainbow from the inside out
split_index: usize,
skip: u8,
width: u8,
}
impl Default for CarRainbow {
fn default() -> Self {
Self::new(&CarRainbowParams::default())
}
}
impl CarRainbow {
pub fn new(params: &CarRainbowParams) -> Self {
Self {
lights_buf: VecDeque::new(),
skip: params.skip,
width: params.width,
split_index: 0,
}
}
}
impl Pattern for CarRainbow {
fn get_name(&self) -> &'static str {
"CarRainbow"
}
fn step(&mut self) -> PatternResult<bool> {
self.lights_buf.rotate_left(1);
// TODO: Not sure if we should go forward or backwards
// if self.forward {
// self.lights_buf.rotate_left(1);
// } else {
// self.lights_buf.rotate_right(1);
// }
Ok(true)
}
fn init(&mut self, num_lights: u16) -> PatternResult<()> {
if !(1..=255).contains(&num_lights) {
return Err(PatternError::LightCount);
}
if self.width < 1 {
return Err(PatternError::LightCount);
}
// (width + skip) * RAINBOW.len()
let length_factor = u16::from(self.width)
.checked_add(self.skip.into())
.ok_or(PatternError::ArithmeticError)?
.saturating_mul(u16::try_from(RAINBOW.len()).or(Err(PatternError::ArithmeticError))?);
// The length of the buffer
// Always a factor of length_factor
let buf_length = color::min_with_factor(num_lights, length_factor)?;
self.split_index = (num_lights / 2_u16) as usize;
self.lights_buf = RAINBOW
.iter()
.flat_map(|&x| {
iter::repeat(x)
.take(self.width.into())
.chain(iter::repeat(color::BLACK).take(self.skip.into()))
})
.cycle()
.take(buf_length.into())
.collect();
Ok(())
}
fn get_strip(&self) -> ColorIterator {
let forward_lights = self.lights_buf.iter().take(self.split_index);
let backward_lights = self.lights_buf.iter().take(self.split_index).rev();
Box::new(forward_lights.chain(backward_lights))
}
}

View File

@ -1,8 +1,37 @@
use super::Pattern; use super::{ColorIterator, FormRender, InputRender, Pattern, PatternError, PatternResult};
use crate::color::{self, Rgb}; use crate::color::{self, Rgb};
use std::collections::vec_deque; use serde::{Deserialize, Serialize};
use std::collections::VecDeque; use std::collections::VecDeque;
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct CollideParams {
pub left_color: Rgb,
pub right_color: Rgb,
pub conjoined_color: Rgb,
}
impl Default for CollideParams {
fn default() -> Self {
// The classic red/blue/purple
Self {
left_color: Rgb(255, 0, 0),
right_color: Rgb(0, 0, 255),
conjoined_color: Rgb(255, 0, 255),
}
}
}
impl FormRender for CollideParams {
fn render(&self) -> String {
[
self.left_color.render("left_color", None),
self.right_color.render("right_color", None),
self.conjoined_color.render("conjoined_color", None),
]
.concat()
}
}
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Collide { pub struct Collide {
num_lights: u16, num_lights: u16,
@ -18,13 +47,18 @@ pub struct Collide {
increase_offset: bool, increase_offset: bool,
lights_buf: VecDeque<Rgb>, lights_buf: VecDeque<Rgb>,
} }
impl Default for Collide {
fn default() -> Self {
Self::new(&CollideParams::default())
}
}
impl Collide { impl Collide {
pub fn new(left_color: Rgb, right_color: Rgb, conjoined_color: Rgb) -> Self { pub fn new(params: &CollideParams) -> Self {
Self { Self {
num_lights: 0, num_lights: 0,
left_color, left_color: params.left_color,
right_color, right_color: params.right_color,
conjoined_color, conjoined_color: params.conjoined_color,
step: 0, step: 0,
step_max: 0, step_max: 0,
conjoined_bounds: (0, 0), conjoined_bounds: (0, 0),
@ -37,7 +71,10 @@ impl Collide {
} }
} }
impl Pattern for Collide { impl Pattern for Collide {
fn step(&mut self) -> Result<bool, ()> { fn get_name(&self) -> &'static str {
"Collide"
}
fn step(&mut self) -> PatternResult<bool> {
// TODO: Better range storage // TODO: Better range storage
// Set the left and right color // Set the left and right color
let colors = let colors =
@ -51,7 +88,7 @@ impl Pattern for Collide {
*self *self
.lights_buf .lights_buf
.get_mut(usize::from(self.previous_offset)) .get_mut(usize::from(self.previous_offset))
.ok_or(())? = color::BLACK; .ok_or(PatternError::Index)? = color::BLACK;
if self.previous_offset if self.previous_offset
!= self != self
.num_lights .num_lights
@ -65,13 +102,13 @@ impl Pattern for Collide {
.saturating_sub(1) .saturating_sub(1)
.saturating_sub(self.previous_offset), .saturating_sub(self.previous_offset),
)) ))
.ok_or(())? = color::BLACK; .ok_or(PatternError::Index)? = color::BLACK;
} }
// Set the color of the current offset // Set the color of the current offset
*self *self
.lights_buf .lights_buf
.get_mut(usize::from(self.offset)) .get_mut(usize::from(self.offset))
.ok_or(())? = colors.0; .ok_or(PatternError::Index)? = colors.0;
if self.offset if self.offset
!= self != self
.num_lights .num_lights
@ -85,7 +122,7 @@ impl Pattern for Collide {
.saturating_sub(1) .saturating_sub(1)
.saturating_sub(self.offset), .saturating_sub(self.offset),
)) ))
.ok_or(())? = colors.1; .ok_or(PatternError::Index)? = colors.1;
} }
self.previous_offset = self.offset; self.previous_offset = self.offset;
@ -106,7 +143,7 @@ impl Pattern for Collide {
Ok(true) Ok(true)
} }
fn init(&mut self, num_lights: u16) -> Result<(), ()> { fn init(&mut self, num_lights: u16) -> PatternResult<()> {
// Reset changing parameters // Reset changing parameters
self.step = 0; self.step = 0;
self.offset = 0; self.offset = 0;
@ -114,7 +151,7 @@ impl Pattern for Collide {
self.num_lights = num_lights; self.num_lights = num_lights;
self.increase_offset = true; self.increase_offset = true;
if self.num_lights < 3 { if self.num_lights < 3 {
return Err(()); return Err(PatternError::LightCount);
} }
self.lights_buf = VecDeque::from(vec![color::BLACK; self.num_lights.into()]); self.lights_buf = VecDeque::from(vec![color::BLACK; self.num_lights.into()]);
if self.num_lights.rem_euclid(2) == 0 { if self.num_lights.rem_euclid(2) == 0 {
@ -149,7 +186,7 @@ impl Pattern for Collide {
Ok(()) Ok(())
} }
fn get_strip(&self) -> vec_deque::Iter<Rgb> { fn get_strip(&self) -> ColorIterator {
self.lights_buf.iter() Box::new(self.lights_buf.iter())
} }
} }

View File

@ -0,0 +1,132 @@
use std::collections::VecDeque;
use tracing::info;
use super::{ColorIterator, FormRender, InputRender, Pattern, PatternError, PatternResult};
use crate::{
cava::Cava,
color::{self, Rgb, BLACK},
};
use color::Gradient;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct CustomVisualizerParams {
pub left_color: Rgb,
pub right_color: Rgb,
pub repeat: u16,
}
impl Default for CustomVisualizerParams {
fn default() -> Self {
// The classic red/blue/purple
Self {
left_color: Rgb(255, 0, 0),
right_color: Rgb(0, 0, 255),
repeat: 0,
}
}
}
impl FormRender for CustomVisualizerParams {
fn render(&self) -> String {
[
self.left_color.render("left_color", None),
self.right_color.render("right_color", None),
self.repeat.render("repeat", None),
]
.concat()
}
}
#[derive(Debug)]
pub struct CustomVisualizer {
lights_buf: VecDeque<Rgb>,
lights_buf_max: VecDeque<Rgb>,
left_color: Rgb,
right_color: Rgb,
repeat: u16,
cava: Option<Cava>,
}
impl Default for CustomVisualizer {
fn default() -> Self {
Self::new(&CustomVisualizerParams::default())
}
}
impl CustomVisualizer {
pub fn new(
CustomVisualizerParams {
left_color,
right_color,
repeat,
}: &CustomVisualizerParams,
) -> Self {
Self {
lights_buf: VecDeque::new(),
lights_buf_max: VecDeque::new(),
left_color: *left_color,
right_color: *right_color,
repeat: *repeat,
cava: None,
}
}
}
impl Pattern for CustomVisualizer {
fn get_name(&self) -> &'static str {
"CustomVisualizer"
}
fn step(&mut self) -> PatternResult<bool> {
if let Some(c) = &self.cava {
let reading = c
.get_latest_reading()
.into_iter()
.zip(self.lights_buf_max.iter())
.map(|(amount, to_color)| BLACK.fade_to(*to_color, amount))
.collect();
let changed = self.lights_buf != reading;
if changed {
self.lights_buf = reading;
}
Ok(changed)
} else {
Err(PatternError::InternalError)
}
}
fn init(&mut self, num_lights: u16) -> PatternResult<()> {
if num_lights < 2 {
return Err(PatternError::LightCount);
}
self.cava = Some(Cava::new(num_lights)?);
let whole_buf = std::iter::repeat([self.left_color, self.right_color])
.take(self.repeat as usize + 1)
.flatten()
.collect::<Vec<_>>();
self.lights_buf_max = color::stretch(&whole_buf, num_lights as usize).collect();
info!(
"CustomVisualizer got lights buf max: {:?}",
self.lights_buf_max
);
Ok(())
}
fn get_strip(&self) -> ColorIterator {
Box::new(self.lights_buf.iter())
}
fn cleanup(&mut self) -> PatternResult<()> {
if let Some(c) = self.cava.take() {
Cava::terminate(c);
}
Ok(())
}
}

View File

@ -0,0 +1,84 @@
use super::{ColorIterator, FormRender, InputRender, Pattern, PatternError, PatternResult};
use crate::color::{self, Gradient, Rgb};
use serde::{Deserialize, Serialize};
use std::{collections::VecDeque, iter};
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct FadeParams {
pub color: Rgb,
}
impl Default for FadeParams {
fn default() -> Self {
Self {
color: color::WHITE,
}
}
}
impl FormRender for FadeParams {
fn render(&self) -> String {
self.color.render("color", None)
}
}
#[derive(Clone, Debug)]
pub struct Fade {
color: Rgb,
step: u8,
direction: bool,
num_lights: u16,
lights_buf: VecDeque<Rgb>,
}
impl Default for Fade {
fn default() -> Self {
Self::new(&FadeParams::default())
}
}
impl Fade {
pub fn new(params: &FadeParams) -> Self {
Self {
color: params.color,
step: 0,
direction: true,
num_lights: 1,
lights_buf: VecDeque::new(),
}
}
}
impl Pattern for Fade {
fn get_name(&self) -> &'static str {
"Fade"
}
fn step(&mut self) -> PatternResult<bool> {
if self.direction {
if self.step == 254 {
self.direction = !self.direction;
}
self.step = self.step.saturating_add(1);
} else {
if self.step == 1 {
self.direction = !self.direction;
}
self.step = self.step.saturating_sub(1);
}
self.lights_buf = iter::repeat(color::BLACK.fade_to(self.color, self.step))
.take(self.num_lights.into())
.collect();
Ok(true)
}
fn init(&mut self, num_lights: u16) -> PatternResult<()> {
if num_lights < 1 {
return Err(PatternError::LightCount);
}
self.step = 0;
self.direction = true;
self.num_lights = num_lights;
self.lights_buf = VecDeque::from(vec![color::BLACK; self.num_lights.into()]);
Ok(())
}
fn get_strip(&self) -> ColorIterator {
Box::new(self.lights_buf.iter())
}
}

View File

@ -0,0 +1,96 @@
use super::{ColorIterator, FormRender, InputRender, Pattern, PatternError, PatternResult};
use crate::color::{self, Rgb};
use serde::{Deserialize, Serialize};
use std::{collections::VecDeque, iter};
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct FlashingParams {
pub colors: Vec<Rgb>,
pub width: u8,
pub tick_rate: u16,
}
impl Default for FlashingParams {
fn default() -> Self {
Self {
// Red and blue flashing
colors: vec![Rgb(255, 0, 0), Rgb(0, 0, 255)],
width: 8,
tick_rate: 10,
}
}
}
impl FormRender for FlashingParams {
fn render(&self) -> String {
[
self.colors.render("colors", None),
self.width.render("width", None),
self.tick_rate.render("tick_rate", None),
]
.concat()
}
}
#[derive(Clone, Debug)]
pub struct Flashing {
lights_buf: VecDeque<Rgb>,
width: u8,
step: u16,
tick_rate: u16,
colors: Vec<Rgb>,
}
impl Default for Flashing {
fn default() -> Self {
Self::new(&FlashingParams::default())
}
}
impl Flashing {
pub fn new(params: &FlashingParams) -> Self {
Self {
colors: params.colors.clone(),
tick_rate: params.tick_rate,
step: 0,
width: params.width,
lights_buf: VecDeque::new(),
}
}
}
impl Pattern for Flashing {
fn get_name(&self) -> &'static str {
"Flashing"
}
fn step(&mut self) -> PatternResult<bool> {
self.step = self.step.wrapping_add(1).rem_euclid(self.tick_rate);
if self.step != 0 {
return Ok(false);
}
self.lights_buf.rotate_right(self.width.into());
Ok(true)
}
fn init(&mut self, num_lights: u16) -> PatternResult<()> {
if num_lights < 1 {
return Err(PatternError::LightCount);
}
let length_factor = num_lights.saturating_mul(u16::from(self.width));
let buf_length = color::min_with_factor(num_lights, length_factor)?;
self.step = 0;
self.lights_buf = self
.colors
.iter()
.flat_map(|&x| iter::repeat(x).take(self.width.into()))
.cycle()
.take(buf_length.into())
.collect();
Ok(())
}
fn get_strip(&self) -> ColorIterator {
Box::new(self.lights_buf.iter())
}
}

View File

@ -0,0 +1,77 @@
use super::{ColorIterator, FormRender, InputRender, Pattern, PatternError, PatternResult};
use crate::color::{self, Rgb};
use serde::{Deserialize, Serialize};
use std::collections::VecDeque;
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct MovingPixelParams {
pub color: Rgb,
}
impl Default for MovingPixelParams {
fn default() -> Self {
Self {
color: color::WHITE,
}
}
}
impl FormRender for MovingPixelParams {
fn render(&self) -> String {
self.color.render("color", None)
}
}
#[derive(Clone, Debug)]
pub struct MovingPixel {
color: Rgb,
num_lights: u16,
step: u16,
lights_buf: VecDeque<Rgb>,
}
impl Default for MovingPixel {
fn default() -> Self {
Self::new(&MovingPixelParams::default())
}
}
impl MovingPixel {
pub fn new(params: &MovingPixelParams) -> Self {
Self {
color: params.color,
step: 0,
// TODO: Better initialization
num_lights: 1,
lights_buf: VecDeque::new(),
}
}
}
impl Pattern for MovingPixel {
fn get_name(&self) -> &'static str {
"MovingPixel"
}
fn step(&mut self) -> PatternResult<bool> {
let len = self.num_lights;
self.lights_buf.swap(
self.step.rem_euclid(len).into(),
self.step.saturating_add(1).rem_euclid(len).into(),
);
self.step = self.step.wrapping_add(1);
Ok(true)
}
fn init(&mut self, num_lights: u16) -> PatternResult<()> {
if num_lights < 1 {
return Err(PatternError::LightCount);
}
self.step = 0;
self.num_lights = num_lights;
// Set the strip to black except for one pixel
self.lights_buf = VecDeque::from(vec![color::BLACK; num_lights.into()]);
*self.lights_buf.get_mut(0).ok_or(PatternError::Index)? = self.color;
Ok(())
}
fn get_strip(&self) -> ColorIterator {
Box::new(self.lights_buf.iter())
}
}

View File

@ -0,0 +1,119 @@
use super::{ColorIterator, FormRender, InputRender, Pattern, PatternError, PatternResult};
use crate::color::{self, Rgb, RAINBOW};
use serde::{Deserialize, Serialize};
use std::{collections::VecDeque, convert::TryFrom, iter};
use tracing::info;
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct MovingRainbowParams {
pub width: u8,
pub forward: bool,
pub skip: u8,
pub fromcenter: bool,
}
impl Default for MovingRainbowParams {
fn default() -> Self {
Self {
width: 16,
forward: true,
skip: 0,
fromcenter: false,
}
}
}
impl FormRender for MovingRainbowParams {
fn render(&self) -> String {
[
self.width.render("width", None),
self.forward.render("forward", None),
self.skip.render("skip", None),
self.fromcenter.render("fromcenter", None),
]
.concat()
}
}
#[derive(Clone, Debug)]
pub struct MovingRainbow {
lights_buf: VecDeque<Rgb>,
/// The index to split, if moving from the inside out
split_index: usize,
skip: u8,
width: u8,
forward: bool,
fromcenter: bool,
}
impl Default for MovingRainbow {
fn default() -> Self {
Self::new(&MovingRainbowParams::default())
}
}
impl MovingRainbow {
pub fn new(params: &MovingRainbowParams) -> Self {
Self {
lights_buf: VecDeque::new(),
skip: params.skip,
width: params.width,
forward: params.forward,
fromcenter: params.fromcenter,
split_index: 0,
}
}
}
impl Pattern for MovingRainbow {
fn get_name(&self) -> &'static str {
"MovingRainbow"
}
fn step(&mut self) -> PatternResult<bool> {
if self.forward {
self.lights_buf.rotate_left(1);
} else {
self.lights_buf.rotate_right(1);
}
Ok(true)
}
fn init(&mut self, num_lights: u16) -> PatternResult<()> {
info!("Initializing with num_lights: {num_lights}");
if !(1..=255).contains(&num_lights) {
return Err(PatternError::LightCount);
}
if self.width < 1 {
return Err(PatternError::LightCount);
}
// (width + skip) * RAINBOW.len()
let length_factor = u16::from(self.width)
.checked_add(self.skip.into())
.ok_or(PatternError::ArithmeticError)?
.saturating_mul(u16::try_from(RAINBOW.len()).or(Err(PatternError::ArithmeticError))?);
// The length of the buffer
// Always a factor of length_factor
let buf_length = color::min_with_factor(num_lights, length_factor)?;
self.split_index = (num_lights / 2_u16) as usize;
self.lights_buf = RAINBOW
.iter()
.flat_map(|&x| {
iter::repeat(x)
.take(self.width.into())
.chain(iter::repeat(color::BLACK).take(self.skip.into()))
})
.cycle()
.take(buf_length.into())
.collect();
Ok(())
}
fn get_strip(&self) -> ColorIterator {
if self.fromcenter {
let tmp_iter = self.lights_buf.iter().take(self.split_index);
let tmp_iter2 = self.lights_buf.iter().take(self.split_index).rev();
Box::new(tmp_iter.chain(tmp_iter2))
} else {
Box::new(self.lights_buf.iter())
}
}
}

View File

@ -1,8 +1,35 @@
use super::Pattern; use super::{ColorIterator, FormRender, InputRender, Pattern, PatternError, PatternResult};
use crate::color::{self, Rgb}; use crate::color::{self, Rgb};
use std::collections::vec_deque; use serde::{Deserialize, Serialize};
use std::collections::VecDeque; use std::{collections::VecDeque, iter};
use std::iter;
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct OrbParams {
pub color: Rgb,
pub center_width: u8,
pub backoff_width: u8,
}
impl Default for OrbParams {
fn default() -> Self {
Self {
color: color::WHITE,
center_width: 8,
backoff_width: 4,
}
}
}
impl FormRender for OrbParams {
fn render(&self) -> String {
[
self.color.render("color", None),
self.center_width.render("center_width", None),
self.backoff_width.render("backoff_width", None),
]
.concat()
}
}
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Orb { pub struct Orb {
@ -25,14 +52,21 @@ pub struct Orb {
/// Direction of the orb. This can switch if `bounces` is true /// Direction of the orb. This can switch if `bounces` is true
direction: bool, direction: bool,
} }
impl Default for Orb {
fn default() -> Self {
Self::new(&OrbParams::default())
}
}
impl Orb { impl Orb {
pub fn new(color: Rgb, center_width: u8, backoff_width: u8) -> Self { pub fn new(params: &OrbParams) -> Self {
Self { Self {
lights_buf: VecDeque::new(), lights_buf: VecDeque::new(),
color, color: params.color,
center_width, center_width: params.center_width,
backoff_width, backoff_width: params.backoff_width,
total_width: center_width.saturating_add(backoff_width.saturating_mul(2)), total_width: params
.center_width
.saturating_add(params.backoff_width.saturating_mul(2)),
bounces: false, bounces: false,
step: 0, step: 0,
step_max: 0, step_max: 0,
@ -41,7 +75,10 @@ impl Orb {
} }
} }
impl Pattern for Orb { impl Pattern for Orb {
fn step(&mut self) -> Result<bool, ()> { fn get_name(&self) -> &'static str {
"Orb"
}
fn step(&mut self) -> PatternResult<bool> {
if !self.bounces { if !self.bounces {
// If we don't bounce, then just wrap and we're done // If we don't bounce, then just wrap and we're done
self.lights_buf.rotate_right(1); self.lights_buf.rotate_right(1);
@ -64,9 +101,9 @@ impl Pattern for Orb {
Ok(true) Ok(true)
} }
fn init(&mut self, num_lights: u16) -> Result<(), ()> { fn init(&mut self, num_lights: u16) -> PatternResult<()> {
if num_lights < 1 { if num_lights < 1 {
return Err(()); return Err(PatternError::LightCount);
} }
self.step = 0; self.step = 0;
let other_color = color::BLACK; let other_color = color::BLACK;
@ -82,15 +119,13 @@ impl Pattern for Orb {
) )
.collect(); .collect();
self.step_max = self let len = self.lights_buf.len();
.lights_buf
.len() self.step_max = len.checked_sub(self.total_width.into()).unwrap_or(len);
.checked_sub(self.total_width.into())
.unwrap_or_else(|| self.lights_buf.len());
Ok(()) Ok(())
} }
fn get_strip(&self) -> vec_deque::Iter<Rgb> { fn get_strip(&self) -> ColorIterator {
self.lights_buf.iter() Box::new(self.lights_buf.iter())
} }
} }

112
common/src/pattern/slide.rs Normal file
View File

@ -0,0 +1,112 @@
use super::{ColorIterator, FormRender, InputRender, Pattern, PatternError, PatternResult};
use crate::color::{self, Rgb};
use serde::{Deserialize, Serialize};
use std::{collections::VecDeque, iter};
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct SlideParams {
pub colors: Vec<Rgb>,
pub width: u8,
pub height: f32,
pub speed: u8,
}
impl Default for SlideParams {
fn default() -> Self {
Self {
// Red and blue Slide
colors: vec![Rgb(255, 0, 0), Rgb(0, 0, 255)],
width: 8,
height: 5.0f32,
speed: 10,
}
}
}
impl FormRender for SlideParams {
fn render(&self) -> String {
[
self.colors.render("colors", None),
self.width.render("width", None),
self.width.render("height", None),
self.speed.render("speed", None),
]
.concat()
}
}
#[derive(Clone, Debug)]
pub struct Slide {
lights_buf: VecDeque<Rgb>,
width: u8,
height: f32,
step: usize,
speed: u8,
colors: Vec<Rgb>,
}
impl Default for Slide {
fn default() -> Self {
Self::new(&SlideParams::default())
}
}
impl Slide {
pub fn new(params: &SlideParams) -> Self {
Self {
colors: params.colors.clone(),
speed: params.speed,
step: 0,
width: params.width,
height: params.height,
lights_buf: VecDeque::new(),
}
}
}
impl Pattern for Slide {
fn get_name(&self) -> &'static str {
"Slide"
}
fn step(&mut self) -> PatternResult<bool> {
self.step = self.step.wrapping_add(1);
if self.step.rem_euclid(self.speed as usize) != 0 {
return Ok(false);
}
match get_shift_amount(self.height, self.step) {
i if i > 0 => self.lights_buf.rotate_right(i as usize),
i if i < 0 => self.lights_buf.rotate_left(-i as usize),
_ => return Ok(false),
}
// self.lights_buf.rotate_right(((self.height as f32)*((self.step as f32)/10 ).sin()) as isize);
Ok(true)
}
fn init(&mut self, num_lights: u16) -> PatternResult<()> {
if num_lights < 1 {
return Err(PatternError::LightCount);
}
let length_factor = num_lights.saturating_mul(u16::from(self.width));
let buf_length = color::min_with_factor(num_lights, length_factor)?;
self.step = 0;
self.lights_buf = self
.colors
.iter()
.flat_map(|&x| iter::repeat(x).take(self.width.into()))
.cycle()
.take(buf_length.into())
.collect();
Ok(())
}
fn get_strip(&self) -> ColorIterator {
Box::new(self.lights_buf.iter())
}
}
fn get_shift_amount(wave_height: f32, x: usize) -> isize {
((wave_height) * ((x as f32) / 10.0f32).sin()) as isize
}

View File

@ -0,0 +1,66 @@
use super::{ColorIterator, FormRender, InputRender, Pattern, PatternError, PatternResult};
use crate::color::{self, Rgb};
use serde::{Deserialize, Serialize};
use std::collections::VecDeque;
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct SolidParams {
pub color: Rgb,
}
impl Default for SolidParams {
fn default() -> Self {
Self {
color: color::WHITE,
}
}
}
impl FormRender for SolidParams {
fn render(&self) -> String {
self.color.render("color", None)
}
}
#[derive(Clone, Debug)]
pub struct Solid {
color: Rgb,
has_run: bool,
lights_buf: VecDeque<Rgb>,
}
impl Default for Solid {
fn default() -> Self {
Self::new(&SolidParams::default())
}
}
impl Solid {
pub fn new(params: &SolidParams) -> Self {
Self {
color: params.color,
has_run: false,
lights_buf: VecDeque::new(),
}
}
}
impl Pattern for Solid {
fn get_name(&self) -> &'static str {
"Solid"
}
fn step(&mut self) -> PatternResult<bool> {
let ret = !self.has_run;
self.has_run = true;
Ok(ret)
}
fn init(&mut self, num_lights: u16) -> PatternResult<()> {
if num_lights < 1 {
return Err(PatternError::LightCount);
}
self.has_run = false;
self.lights_buf = VecDeque::from(vec![self.color; num_lights.into()]);
Ok(())
}
fn get_strip(&self) -> ColorIterator {
Box::new(self.lights_buf.iter())
}
}

View File

@ -0,0 +1,92 @@
use std::collections::VecDeque;
use tracing::info;
use super::{ColorIterator, FormRender, Pattern, PatternError, PatternResult};
use crate::{
cava::Cava,
color::{self, Rgb, BLACK, RAINBOW},
};
use color::Gradient;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
pub struct VisualizerParams {}
impl FormRender for VisualizerParams {
fn render(&self) -> String {
String::from("")
}
}
#[derive(Debug)]
pub struct Visualizer {
lights_buf: VecDeque<Rgb>,
lights_buf_max: VecDeque<Rgb>,
cava: Option<Cava>,
}
impl Default for Visualizer {
fn default() -> Self {
Self::new(&VisualizerParams::default())
}
}
impl Visualizer {
pub fn new(_params: &VisualizerParams) -> Self {
Self {
lights_buf: VecDeque::new(),
lights_buf_max: VecDeque::new(),
cava: None,
}
}
}
impl Pattern for Visualizer {
fn get_name(&self) -> &'static str {
"Visualizer"
}
fn step(&mut self) -> PatternResult<bool> {
if let Some(c) = &self.cava {
let reading = c
.get_latest_reading()
.into_iter()
.zip(self.lights_buf_max.iter())
.map(|(amount, to_color)| BLACK.fade_to(*to_color, amount))
.collect();
let changed = self.lights_buf != reading;
if changed {
self.lights_buf = reading;
}
Ok(changed)
} else {
Err(PatternError::InternalError)
}
}
fn init(&mut self, num_lights: u16) -> PatternResult<()> {
if num_lights < 2 {
return Err(PatternError::LightCount);
}
self.cava = Some(Cava::new(num_lights)?);
self.lights_buf_max = color::stretch(&RAINBOW[..], num_lights as usize).collect();
info!("Visualizer got lights buf max: {:?}", self.lights_buf_max);
Ok(())
}
fn get_strip(&self) -> ColorIterator {
Box::new(self.lights_buf.iter())
}
fn cleanup(&mut self) -> PatternResult<()> {
if let Some(c) = self.cava.take() {
Cava::terminate(c);
}
Ok(())
}
}

48
common/src/strip.rs Normal file
View File

@ -0,0 +1,48 @@
use std::fmt::Debug;
use crate::{
color::{self, Rgb},
pattern::Pattern,
};
#[derive(Debug)]
pub enum Message {
ClearLights,
TurnOn(Option<String>),
ChangePattern(Box<dyn Pattern + Send + Sync>),
SetNumLights(u16),
SetTickTime(u64),
Quit,
}
/// The state of the strip
#[derive(Clone, Debug)]
pub struct State {
pub on: bool,
pub pattern: String,
// brightnes: u8,
pub color: Rgb,
// Off,
// Pattern
}
// impl Default for State {
// fn default() -> Self {
// Self {
// on: false,
// pattern: None,
// color: color::BLACK,
// }
// }
// }
// impl Debug for Message {
// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// match self {
// Message::ClearLights => write!(f, "Message::ClearLights"),
// Message::ChangePattern(_) => write!(f, "Message::ChangePattern(_)"),
// Message::SetNumLights(n) => write!(f, "Message::SetNumLights({n})"),
// Message::SetTickTime(n) => write!(f, "Message::SetTickTime({n})"),
// Message::Quit => write!(f, "Message::Quit"),
// }
// }
// }

29
config.toml Normal file
View File

@ -0,0 +1,29 @@
# ARMv6
[target.arm-unknown-linux-gnueabi]
linker = "arm-linux-gnueabi-gcc"
[target.arm-unknown-linux-gnueabihf]
linker = "arm-linux-gnueabihf-gcc"
[target.arm-unknown-linux-musleabi]
linker = "arm-linux-gnueabi-gcc"
[target.arm-unknown-linux-musleabihf]
linker = "arm-linux-musleabihf-gcc"
# ARMv7
[target.armv7-unknown-linux-gnueabi]
linker = "arm-linux-gnueabi-gcc"
[target.armv7-unknown-linux-gnueabihf]
linker = "arm-linux-gnueabihf-gcc"
[target.armv7-unknown-linux-musleabi]
linker = "arm-linux-gnueabi-gcc"
[target.armv7-unknown-linux-musleabihf]
linker = "arm-linux-gnueabihf-gcc"
# AARCH64
[target.aarch64-unknown-linux-musl]
linker = "aarch64-linux-gnu-gcc"

3
copy-code.sh Executable file
View File

@ -0,0 +1,3 @@
#!/bin/bash
set -euxo pipefail
rsync -ha --info=progress2 --no-i-r --exclude='/.git' --filter='dir-merge,- .gitignore' ./ pi:aw-lights/

24
entr.sh
View File

@ -2,13 +2,25 @@ CMD="$(cat <<'EOF'
set -euo pipefail set -euo pipefail
HEIGHT="$(($(tput lines) - 1))" HEIGHT="$(($(tput lines) - 1))"
clear clear
for i in check fmt build clippy; do
echo "+ cargo "${i}"" function cargo_cmd() {
cargo --color=always "${i}" |& head -n "${HEIGHT}" cargo --color=always "${1}" |& head -n "${HEIGHT}"
done }
(
cd ./webui/liveview-rust/js/
npm run-script build
) &
cargo_cmd fmt
cargo_cmd check &
wait
cargo_cmd build
cargo_cmd clippy
EOF EOF
)" )"
{ {
fd -tf -ers fd -tf -ers -ehtml
printf "%s\n" "Cargo.toml" fd -tf liveview-dev.js
fd -tf Cargo.toml
} | entr bash -c "${CMD}" } | entr bash -c "${CMD}"

8
env.dist Normal file
View File

@ -0,0 +1,8 @@
NUM_LIGHTS=89
MQTT_BROKER=127.0.0.1
MQTT_ID=pilights00
# MQTT_PORT=1883
# MQTT_DISCOVERY_PREFIX=homeassistant
# MQTT_USERNAME=
# MQTT_PASSWORD=

View File

@ -0,0 +1,17 @@
[package]
name = "homeassistant-mqtt-discovery"
version = "0.1.0"
edition = "2021"
[dependencies]
either = { version = "1.10.0", features = ["serde"] }
nom = "7.1.3"
serde = { version = "1.0.197", features = ["derive"], optional = true }
[features]
default = ["serde"]
serde = ["dep:serde", "either/serde"]
[dev-dependencies]
serde_json = "1.0.115"
# std = ["serde/std"]

View File

@ -0,0 +1,67 @@
---
title: "MQTT Light"
description: "Instructions on how to setup MQTT lights using default schema within Home Assistant."
ha_category:
- Light
ha_iot_class: Configurable
ha_release: 0.8
ha_domain: mqtt
---
The `mqtt` light platform lets you control your MQTT enabled lights through one of the supported message schemas, `default`, `json` or `template`.
## Comparison of light MQTT schemas
| Function | [`default`](#default-schema) | [`json`](#json-schema) | [`template`](#template-schema) |
| ----------------- | ---------------------------- | ---------------------- | ------------------------------ |
| Brightness | ✔ | ✔ | ✔ |
| Color mode | ✔ | ✔ | ✘ |
| Color temperature | ✔ | ✔ | ✔ |
| Effects | ✔ | ✔ | ✔ |
| Flashing | ✘ | ✔ | ✔ |
| HS Color | ✔ | ✔ | ✔ |
| RGB Color | ✔ | ✔ | ✔ |
| RGBW Color | ✔ | ✔ | ✘ |
| RGBWW Color | ✔ | ✔ | ✘ |
| Transitions | ✘ | ✔ | ✔ |
| White | ✔ | ✔ | ✘ |
| XY Color | ✔ | ✔ | ✘ |
## Default schema
The `mqtt` light platform with default schema lets you control your MQTT enabled lights. It supports setting brightness, color temperature, effects, on/off, RGB colors, XY colors and white.
## Default schema - Configuration
In an ideal scenario, the MQTT device will have a state topic to publish state changes. If these messages are published with a `RETAIN` flag, the MQTT light will receive an instant state update after subscription and will start with the correct state. Otherwise, the initial state of the switch will be `unknown`. A MQTT device can reset the current state to `unknown` using a `None` payload.
When a state topic is not available, the light will work in optimistic mode. In this mode, the light will immediately change state after every command. Otherwise, the light will wait for state confirmation from the device (message from `state_topic`). The initial state is set to `False` / `off` in optimistic mode.
Optimistic mode can be forced, even if the `state_topic` is available. Try to enable it, if experiencing incorrect light operation.
Home Assistant internally assumes that a light's state corresponds to a defined `color_mode`.
The state of MQTT lights with default schema and support for both color and color temperature will set the `color_mode` according to the last received valid color or color temperature. Optionally, a `color_mode_state_topic` can be configured for explicit control of the `color_mode`.
```yaml
# Example configuration.yaml entry
mqtt:
- light:
command_topic: "office/rgb1/light/switch"
```
{% configuration %}
availability:
description: A list of MQTT topics subscribed to receive availability (online/offline) updates. Must not be used together with `availability_topic`.
Range for Hue: 0° .. 360°, Range of Saturation: 0..100.
Note: Brightness is sent separately in the `brightness_command_topic`."
required: false
type: list
keys:
payload_available:
description: The payload that represents the available state.
Range for Hue: 0° .. 360°, Range of Saturation: 0..100.
Note: Brightness is sent separately in the `brightness_command_topic`."
required: false
type: string
default: online
{% endconfiguration %}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,323 @@
// Terrible code generator for
// Clone this: https://github.com/home-assistant/core/tree/dev/homeassistant/components/mqtt
// Then run:
// cargo run -- ~/cloned-path/home-assistant.io/source/_integrations/*.mqtt.* > output.rs
#[allow(unused_imports)]
use nom::{
branch::alt,
bytes::complete::{tag, take_till, take_till1, take_until},
character::complete::anychar,
character::complete::newline,
character::complete::{alphanumeric1, line_ending, multispace0},
combinator::{eof, map, map_res, peek, rest},
multi::count,
multi::many1,
sequence::{delimited, pair, preceded, terminated, tuple},
IResult,
};
use nom::{character::complete::space0, combinator::map_opt, multi::many_till};
use std::{
path::{Path, PathBuf},
rc::Rc,
};
const START_MARKER: &str = "{% configuration %}";
const END_MARKER: &str = "{% endconfiguration %}";
const DOCUMENT_WHITESPACE: &str = " ";
// const KNOWN_STRUCTS: RefCell<HashMap<>> = ;
fn main() {
// println!("");
for file in std::env::args().skip(1) {
let p = PathBuf::from(file);
println!(
"mod {} {{",
p.file_name()
.expect("Not a filename")
.to_string_lossy()
.split_once(".")
.expect("No file extension")
.0
);
parse_file(p);
println!("}}");
}
//
// parse_file("sample-file.txt");
// parse_file("sample-file.txt");
}
fn parse_file(f: impl AsRef<Path>) {
let input = std::fs::read_to_string(f).expect("Input not be read");
let i = input.as_str();
// Find the start marker
let (i, _) = many_till(parse_line, terminated(tag(START_MARKER), line_ending))(i)
.expect("No start marker found");
// Parse each line
let (i, output) =
OutputStruct::parse(i, "Outer".to_string(), 0).expect("Could not parse output");
// Until the end marker
let (_i, _) = parse_end_line(i).expect("No end marker found");
// eprintln!("{i:#?}");
for o in output.format() {
println!("{o}");
}
}
#[derive(Debug, Hash, PartialEq, Eq)]
struct OutputStruct {
suggested_name: String,
fields: Vec<OutputField>,
}
impl OutputStruct {
fn format(&self) -> Vec<String> {
let mut ret = vec![];
ret.push(r#"#[derive(Debug, PartialEq, Eq, Clone)]"#.to_string());
ret.push(r#"#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]"#.to_string());
ret.push(format!(
"struct {} {{",
capitalize_first_letter(&self.suggested_name)
));
ret.append(&mut self.fields.iter().flat_map(|f| f.format()).collect());
ret.push("}".to_string());
ret.push("".to_string());
for sub_struct in self.fields.iter().flat_map(|f| f.sub_struct.as_ref()) {
ret.append(&mut sub_struct.format());
}
ret
}
fn parse(i: &str, suggested_name: String, indent: usize) -> IResult<&str, Self> {
map(many1(OutputField::parse(indent)), |fields| Self {
fields,
suggested_name: suggested_name.to_string(),
})(i)
}
}
#[derive(Debug, Hash, PartialEq, Eq)]
struct OutputField {
label: String,
default: Option<String>,
description: Option<String>,
required: Option<bool>,
data_type: Option<DataType>,
sub_struct: Option<Rc<OutputStruct>>,
}
impl OutputField {
fn format(&self) -> Vec<String> {
// First the comment lines
let mut ret = self
.description
.as_ref()
.map(|d| d.lines().map(|l| format!("/// {l}")).collect::<Vec<_>>())
.unwrap_or_default();
let mut ty = self
.data_type
.as_ref()
.map(|data_type| {
data_type.format(
self.sub_struct
.as_deref()
.map(|ss| capitalize_first_letter(ss.suggested_name.as_str()))
.as_deref(),
)
})
.unwrap_or_else(|| "UNKNOWN".to_string());
if !self.required.unwrap_or(false) {
// This is not a required field
ty = format!("Option<{ty}>");
// Let serde know
ret.push(
r#"#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]"#
.to_string(),
);
}
let label: &str = self.label.as_ref();
ret.push(format!("pub {label}: {ty},"));
ret.push("".to_string());
ret.iter().map(|l| format!(" {l}")).collect::<Vec<_>>()
}
fn parse<'a>(indent: usize) -> impl FnMut(&'a str) -> IResult<&'a str, Self> {
move |mut i: &str| {
let (new_i, label) = map(
delimited(
count(tag(DOCUMENT_WHITESPACE), indent),
take_till1(|c: char| c.is_whitespace() || c == ':'),
tag(":\n"),
),
|l: &str| l.to_string(),
)(i)?;
i = new_i;
let mut ret = Self {
label: label.clone(),
default: None,
description: None,
required: None,
data_type: None,
sub_struct: None,
};
while let Ok((new_i, line_type)) = LineType::parse(indent + 1)(i) {
i = new_i;
match line_type {
LineType::Default(default) => ret.default = Some(default),
LineType::Description(description) => ret.description = Some(description),
LineType::Required(required) => ret.required = Some(required),
LineType::Type(data_type) => ret.data_type = Some(data_type),
LineType::Extra(extra) => {
ret.description.as_mut().map(|d| {
d.push('\n');
d.push_str(&extra);
d
});
}
LineType::Keys => {
let (new_i, sub_struct) =
OutputStruct::parse(i, label.clone(), indent + 2)?;
i = new_i;
ret.sub_struct = Some(Rc::new(sub_struct));
}
}
}
Ok((i, ret))
}
}
}
#[derive(Debug, Hash, PartialEq, Eq)]
enum LineType {
// Label(String),
Default(String),
Description(String),
Required(bool),
Type(DataType),
Extra(String),
Keys,
}
impl LineType {
fn parse<'a>(indent: usize) -> impl FnMut(&'a str) -> IResult<&'a str, Self> {
move |i| preceded(count(tag(DOCUMENT_WHITESPACE), indent), Self::parse_type)(i)
}
fn parse_type(i: &str) -> IResult<&str, Self> {
let (i, line) = parse_line(i)?;
let matched_type: IResult<&str, Self> = map_opt(
tuple((
take_till1(|c: char| c.is_whitespace() || c == ':'),
pair(tag(":"), space0),
take_till(|c: char| c == '\n' || c == '\r'),
)),
|(field, _, value): (&str, _, _)| {
let value = value.to_string();
Some(match field {
"default" => Self::Default(value),
"description" => Self::Description(value.replace('"', "")),
"keys" => Self::Keys,
"required" => Self::Required(value == "true"),
"type" => Self::Type(DataType::parse(&value).expect("Invalid type found").1),
_ => return None,
})
},
)(line);
match matched_type {
Ok((_, result)) => Ok((i, result)),
Err(_) => Ok((i, Self::Extra(line.to_string()))),
}
}
}
#[derive(Debug, Hash, PartialEq, Eq)]
enum DataType {
List,
String,
Float,
StringList,
Map,
Template,
Integer,
Boolean,
Icon,
Unknown,
}
impl DataType {
fn parse(i: &str) -> IResult<&str, Self> {
map_opt(rest, |i| {
Some(match i {
"list" => Self::List,
"float" => Self::Float,
"string" => Self::String,
"map" => Self::Map,
"template" => Self::Template,
"integer" => Self::Integer,
"[list]" | "[string, list]" | "[list, string]" => Self::StringList,
"boolean" => Self::Boolean,
"icon" => Self::Icon,
"device_class" => Self::Unknown,
_ => return None,
})
})(i)
}
fn format(&self, type_name: Option<&str>) -> String {
let type_name = type_name.unwrap_or("_");
match self {
Self::List => format!("Vec<{type_name}>"),
Self::Map => type_name.to_string(),
Self::Unknown => String::from("_"),
Self::String => String::from("String"),
Self::StringList => String::from("Vec<String>"),
Self::Template => String::from("Template"),
Self::Integer => String::from("usize"),
Self::Float => String::from("f32"),
Self::Boolean => String::from("bool"),
Self::Icon => String::from("Icon"),
}
}
}
fn parse_end_line(i: &str) -> IResult<&str, ()> {
map(terminated(tag(END_MARKER), alt((eof, line_ending))), |_| ())(i)
}
fn parse_line(i: &str) -> IResult<&str, &str> {
terminated(
take_till(|c| c == '\n' || c == '\r'),
alt((eof, line_ending)),
)(i)
}
fn capitalize_first_letter(s: impl AsRef<str>) -> String {
let mut cs = s.as_ref().chars();
let first = cs.next().expect("Name is empty");
first.to_uppercase().collect::<String>() + cs.as_str()
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,148 @@
pub mod integrations;
#[cfg(test)]
mod tests;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
// pub mod light;
// TODO: Templates
pub type Template = String;
// TODO: fill with https://developers.home-assistant.io/docs/core/entity/#generic-properties
#[derive(Debug, PartialEq, Eq, Clone, Default)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
// #[cfg_attr(feature = "serde", serde(flatten))]
pub struct Common {
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub unique_id: Option<String>,
/// Used instead of `name` for automatic generation of `entity_id`
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub object_id: Option<String>,
/// The [category](https://developers.home-assistant.io/docs/core/entity#generic-properties) of the entity.
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub entity_category: Option<String>,
/// The maximum QoS level to be used when receiving and publishing messages.
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub qos: Option<usize>,
/// Flag which defines if the entity should be enabled when first added.
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub enabled_by_default: Option<bool>,
/// The encoding of the payloads received and published messages. Set to `` to disable decoding of incoming payload.
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub encoding: Option<String>,
/// Defines a [template](/docs/configuration/templating/#using-templates-with-the-mqtt-integration) to extract device's availability from the `availability_topic`. To determine the devices's availability result of this template will be compared to `payload_available` and `payload_not_available`.
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub availability_template: Option<Template>,
/// When `availability` is configured, this controls the conditions needed to set the entity to `available`. Valid entries are `all`, `any`, and `latest`. If set to `all`, `payload_available` must be received on all configured availability topics before the entity is marked as online. If set to `any`, `payload_available` must be received on at least one configured availability topic before the entity is marked as online. If set to `latest`, the last `payload_available` or `payload_not_available` received on any configured availability topic controls the availability.
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub availability_mode: Option<AvailabilityMode>,
/// A list of MQTT topics subscribed to receive availability (online/offline) updates. Must not be used together with `availability_topic`.
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub availability: Option<Availability>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub device: Option<Device>,
}
#[derive(Debug, PartialEq, Eq, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
pub enum AvailabilityMode {
All,
Any,
Latest,
}
impl Default for AvailabilityMode {
fn default() -> Self {
Self::Latest
}
}
#[derive(Debug, PartialEq, Eq, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub enum Availability {
/// A list of MQTT topics subscribed to receive availability (online/offline) updates. Must not be used together with `availability_topic`.
Availability(CustomAvailability),
/// The MQTT topic subscribed to receive availability (online/offline) updates. Must not be used together with `availability`.
AvailabilityTopic(String),
}
#[derive(Debug, PartialEq, Eq, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub struct CustomAvailability {
/// The payload that represents the available state.
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub payload_available: Option<String>,
/// The payload that represents the unavailable state.
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub payload_not_available: Option<String>,
/// An MQTT topic subscribed to receive availability (online/offline) updates.
pub topic: String,
/// Defines a [template](/docs/configuration/templating/#using-templates-with-the-mqtt-integration) to extract device's availability from the `topic`. To determine the devices's availability result of this template will be compared to `payload_available` and `payload_not_available`.
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub value_template: Option<Template>,
}
#[derive(Debug, PartialEq, Eq, Clone, Default)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
/// Information about the device this light is a part of to tie it into the device registry. Only works when unique_id is set. At least one of identifiers or connections must be present to identify the device.
pub struct Device {
/// A link to the webpage that can manage the configuration of this device. Can be either an http://, https:// or an internal homeassistant:// URL.
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub configuration_url: Option<String>,
/// A list of connections of the device to the outside world as a list of tuples [connection_type, connection_identifier]. For example the MAC address of a network interface: "connections": [["mac", "02:5b:26:a8:dc:12"]].
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub connections: Option<Vec<String>>,
/// The hardware version of the device.
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub hw_version: Option<String>,
/// A list of IDs that uniquely identify the device. For example a serial number.
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub identifiers: Option<Vec<String>>,
/// The manufacturer of the device.
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub manufacturer: Option<String>,
/// The model of the device.
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub model: Option<String>,
/// The name of the device.
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub name: Option<String>,
/// The serial number of the device.
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub serial_number: Option<String>,
/// Suggest an area if the device isn't in one yet.
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub suggested_area: Option<String>,
/// The firmware version of the device.
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub sw_version: Option<String>,
/// Identifier of a device that routes messages between this device and Home Assistant. Examples of such devices are hubs, or parent devices of a sub-device. This is used to show device topology in Home Assistant.
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub via_device: Option<String>,
}

View File

@ -0,0 +1,31 @@
use crate::{Common, Device, Template};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Discovery {
#[cfg_attr(feature = "serde", serde(flatten))]
common: Common,
}
// {
// "brightness": 255,
// "color_mode": "rgb",
// "color_temp": 155,
// "color": {
// "r": 255,
// "g": 180,
// "b": 200,
// "c": 100,
// "w": 50,
// "x": 0.406,
// "y": 0.301,
// "h": 344.0,
// "s": 29.412
// },
// "effect": "colorloop",
// "state": "ON",
// "transition": 2,
// }

View File

@ -0,0 +1,223 @@
use crate::integrations::{
binary_sensor,
light::{self, ColorMode, IncomingColor, JsonIncoming},
};
use super::*;
use serde_json::{from_str, Value};
#[test]
fn device() {
let test_data = r#"{
"identifiers": [
"01ad"
],
"name": "Garden"
}"#;
let expected = Device {
name: Some("Garden".to_string()),
identifiers: Some(vec!["01ad".to_string()]),
..Default::default()
};
assert_eq!(expected, serde_json::from_str::<Device>(test_data).unwrap(),);
assert_eq!(
serde_json::from_str::<Value>(&serde_json::to_string(&expected).unwrap()).unwrap(),
serde_json::from_str::<Value>(test_data).unwrap()
);
}
#[test]
fn light_json_incoming() {
let test_data = r#"{
"brightness": 255,
"color_mode": "rgb",
"color_temp": 155,
"color": {
"r": 255,
"g": 180,
"b": 200,
"c": 100,
"w": 50,
"x": 0.406,
"y": 0.301,
"h": 344.0,
"s": 29.412
},
"effect": "colorloop",
"state": "ON",
"transition": 2
}"#;
let expected = JsonIncoming {
brightness: Some(255),
color_mode: Some(ColorMode::Rgb),
color_temp: Some(155),
color: Some(IncomingColor {
r: Some(255),
g: Some(180),
b: Some(200),
c: Some(100),
w: Some(50),
x: Some(0.406),
y: Some(0.301),
h: Some(344.0),
s: Some(29.412),
}),
effect: Some(String::from("colorloop")),
state: Some(String::from("ON")),
transition: Some(2),
};
assert_eq!(
expected,
serde_json::from_str::<JsonIncoming>(test_data).unwrap(),
);
assert_eq!(
serde_json::from_str::<Value>(&serde_json::to_string(&expected).unwrap()).unwrap(),
serde_json::from_str::<Value>(test_data).unwrap()
);
}
#[test]
fn light_default() {
let test_data = r#"{
"name": "Office Light RGB",
"state_topic": "office/rgb1/light/status",
"command_topic": "office/rgb1/light/switch",
"brightness_state_topic": "office/rgb1/brightness/status",
"brightness_command_topic": "office/rgb1/brightness/set",
"rgb_state_topic": "office/rgb1/rgb/status",
"rgb_command_topic": "office/rgb1/rgb/set",
"state_value_template": "{{ value_json.state }}",
"brightness_value_template": "{{ value_json.brightness }}",
"rgb_value_template": "{{ value_json.rgb | join(',') }}",
"qos": 0,
"payload_on": "ON",
"payload_off": "OFF",
"optimistic": false
}"#;
let expected = light::DefaultDiscovery {
common: Common {
qos: Some(0),
..Common::default()
},
brightness_command_topic: Some("office/rgb1/brightness/set".into()),
brightness_state_topic: Some("office/rgb1/brightness/status".into()),
brightness_value_template: Some("{{ value_json.brightness }}".into()),
name: Some("Office Light RGB".into()),
optimistic: Some(false),
payload_off: Some("OFF".into()),
payload_on: Some("ON".into()),
rgb_command_topic: Some("office/rgb1/rgb/set".into()),
rgb_state_topic: Some("office/rgb1/rgb/status".into()),
rgb_value_template: Some("{{ value_json.rgb | join(',') }}".into()),
state_topic: Some("office/rgb1/light/status".into()),
state_value_template: Some("{{ value_json.state }}".into()),
..light::DefaultDiscovery::new("office/rgb1/light/switch")
};
assert_eq!(
expected,
serde_json::from_str::<light::DefaultDiscovery>(test_data).unwrap(),
);
assert_eq!(
serde_json::from_str::<Value>(&serde_json::to_string(&expected).unwrap()).unwrap(),
serde_json::from_str::<Value>(test_data).unwrap()
);
}
#[test]
fn light_json() {
let test_data = r#"{"schema":"json","name":"mqtt_json_light_1","state_topic":"home/rgb1","command_topic":"home/rgb1/set","brightness":true,"supported_color_modes":["rgb"]}"#;
let expected = light::JsonDiscovery {
common: Common {
..Common::default()
},
name: Some("mqtt_json_light_1".into()),
state_topic: Some("home/rgb1".into()),
brightness: Some(true),
supported_color_modes: Some(vec![ColorMode::Rgb]),
..light::JsonDiscovery::new("home/rgb1/set")
};
assert_eq!(
expected,
serde_json::from_str::<light::JsonDiscovery>(test_data).unwrap(),
);
assert_eq!(
serde_json::from_str::<Value>(&serde_json::to_string(&expected).unwrap()).unwrap(),
serde_json::from_str::<Value>(test_data).unwrap()
);
}
#[test]
fn binary_sensor() {
let test_data = r#"{
"device_class": "motion",
"state_topic": "homeassistant/binary_sensor/garden/state",
"unique_id": "motion01ad",
"device": {
"identifiers": [
"01ad"
],
"name": "Garden"
}
}"#;
let expected = binary_sensor::Discovery {
common: Common {
device: Some(Device {
name: Some("Garden".to_string()),
identifiers: Some(vec!["01ad".to_string()]),
..Default::default()
}),
unique_id: Some("motion01ad".to_string()),
..Common::default()
},
device_class: Some(binary_sensor::DeviceClass::Motion),
..binary_sensor::Discovery::new("homeassistant/binary_sensor/garden/state")
};
assert_eq!(
expected,
serde_json::from_str::<binary_sensor::Discovery>(test_data).unwrap(),
);
assert_eq!(
serde_json::from_str::<Value>(&serde_json::to_string(&expected).unwrap()).unwrap(),
serde_json::from_str::<Value>(test_data).unwrap()
);
}
#[test]
fn test_availability() {
let test_data = r#"{
"availability_topic": "asdf"
}"#;
assert_eq!(
from_str::<Availability>(test_data).unwrap(),
Availability::AvailabilityTopic(String::from("asdf"))
);
assert_eq!(
from_str::<Availability>(
r#"{
"availability": {
"payload_available": "yes",
"payload_not_available": "no",
"topic": "asdf"
}
}"#
)
.unwrap(),
Availability::Availability(CustomAvailability {
payload_available: Some(String::from("yes")),
payload_not_available: Some(String::from("no")),
topic: String::from("asdf"),
value_template: None
})
);
}

View File

@ -1,8 +1,7 @@
//! Generic Hardware Abstraction Layer, no_std-compatible. //! Generic Hardware Abstraction Layer, no_std-compatible.
use crate::encoding::encode_rgb_slice; use crate::encoding::encode_rgb_slice;
use alloc::boxed::Box; use alloc::{boxed::Box, string::String};
use alloc::string::String;
/// SPI-device abstraction. /// SPI-device abstraction.
pub trait HardwareDev { pub trait HardwareDev {
@ -35,7 +34,7 @@ pub trait Ws28xxAdapter {
/// must fulfill the restrictions given by [`crate::timings`] and [`crate::encoding`] if the hardware /// must fulfill the restrictions given by [`crate::timings`] and [`crate::encoding`] if the hardware
/// device uses the specified frequency in [`crate::timings::PI_SPI_HZ`]. /// device uses the specified frequency in [`crate::timings::PI_SPI_HZ`].
fn write_encoded_rgb(&mut self, encoded_data: &[u8]) -> Result<(), String> { fn write_encoded_rgb(&mut self, encoded_data: &[u8]) -> Result<(), String> {
self.get_hw_dev().write_all(&encoded_data) self.get_hw_dev().write_all(encoded_data)
.map_err(|_| { .map_err(|_| {
format!( format!(
"Failed to send {} bytes via the specified hardware device. If you use SPI on Linux Perhaps your SPI buffer is too small!\ "Failed to send {} bytes via the specified hardware device. If you use SPI on Linux Perhaps your SPI buffer is too small!\

View File

@ -1,12 +1,15 @@
//! Adapter for SPI-dev on Linux-systems. This requires std. //! Adapter for SPI-dev on Linux-systems. This requires std.
use crate::adapter_gen::{HardwareDev, Ws28xxAdapter, Ws28xxGenAdapter}; use crate::{
use crate::timings::PI_SPI_HZ; adapter_gen::{HardwareDev, Ws28xxAdapter, Ws28xxGenAdapter},
use alloc::boxed::Box; timings::PI_SPI_HZ,
use alloc::string::{String, ToString}; };
use alloc::{
boxed::Box,
string::{String, ToString},
};
use spidev::{SpiModeFlags, Spidev, SpidevOptions}; use spidev::{SpiModeFlags, Spidev, SpidevOptions};
use std::io; use std::{io, io::Write};
use std::io::Write;
/// Wrapper around Spidev. /// Wrapper around Spidev.
struct SpiHwAdapterDev(Spidev); struct SpiHwAdapterDev(Spidev);
@ -14,7 +17,7 @@ struct SpiHwAdapterDev(Spidev);
// Implement Hardwareabstraction for device. // Implement Hardwareabstraction for device.
impl HardwareDev for SpiHwAdapterDev { impl HardwareDev for SpiHwAdapterDev {
fn write_all(&mut self, encoded_data: &[u8]) -> Result<(), String> { fn write_all(&mut self, encoded_data: &[u8]) -> Result<(), String> {
self.0.write_all(&encoded_data) self.0.write_all(encoded_data)
.map_err(|_| { .map_err(|_| {
format!( format!(
"Failed to send {} bytes via SPI. Perhaps your SPI buffer is too small!\ "Failed to send {} bytes via SPI. Perhaps your SPI buffer is too small!\

View File

@ -0,0 +1,5 @@
[build]
target = "wasm32-wasi"
[target.wasm32-wasi]
runner = "lunatic"

11
lunatic-webui/Cargo.toml Normal file
View File

@ -0,0 +1,11 @@
[package]
name = "lunatic-webui"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
serde = { version = "1.0.160", features = ["derive"] }
submillisecond = "0.3.0"
submillisecond-live-view = "0.4.0"

9
lunatic-webui/index.html Normal file
View File

@ -0,0 +1,9 @@
<html>
<head>
<title>LiveView Counter</title>
<link rel="stylesheet" href="/static/counter.css" />
</head>
<body>
<div id="app"></div>
</body>
</html>

50
lunatic-webui/src/main.rs Normal file
View File

@ -0,0 +1,50 @@
use serde::{Deserialize, Serialize};
use submillisecond::{router, static_router, Application};
use submillisecond_live_view::prelude::*;
fn main() -> std::io::Result<()> {
Application::new(router! {
"/" => Counter::handler("index.html", "#app")
"/static" => static_router!("./static")
})
.serve("127.0.0.1:3000")
}
#[derive(Clone, Serialize, Deserialize)]
struct Counter {
count: i32,
}
impl LiveView for Counter {
type Events = (Increment, Decrement);
fn mount(_uri: Uri, _socket: Option<Socket>) -> Self {
Self { count: 0 }
}
fn render(&self) -> Rendered {
html! {
button @click=(Increment) {"Increment"}
button @click=(Decrement) {"Decrement"}
p { "Count is " (self.count) }
}
}
}
#[derive(Deserialize)]
struct Increment {}
impl LiveViewEvent<Increment> for Counter {
fn handle(state: &mut Self, _event: Increment) {
state.count += 1
}
}
#[derive(Deserialize)]
struct Decrement {}
impl LiveViewEvent<Decrement> for Counter {
fn handle(state: &mut Self, _event: Decrement) {
state.count -= 1
}
}

14
mqtt/Cargo.toml Normal file
View File

@ -0,0 +1,14 @@
[package]
name = "mqtt"
version = "0.1.0"
edition = "2021"
[dependencies]
common = { path = "../common" }
rumqttc = "0.24.0"
serde = { version = "1.0.197", features = ["derive"] }
serde_json = "1.0.115"
tracing = "0.1.37"
homeassistant-mqtt-discovery = { path = "../homeassistant-mqtt-discovery/" }
clap = { version = "4.5.4", features = ["derive"] }
anyhow = "1.0.82"

430
mqtt/src/lib.rs Normal file
View File

@ -0,0 +1,430 @@
#![feature(let_chains)]
use anyhow::{Context as _, Result};
use common::{
color::{self, Gradient, Rgb},
error::ProgramError,
pattern::{MovingRainbow, Parameters, Solid, SolidParams},
MqttConfig,
};
use homeassistant_mqtt_discovery::{
integrations::{
homeassistant,
light::{self, IncomingColor, JsonIncoming},
},
Common,
};
use rumqttc::{Client, Connection, Event, Incoming, MqttOptions, Packet, Publish, QoS};
use std::{
sync::mpsc::{channel, Receiver},
thread,
time::Duration,
};
use tracing::{error, info, warn};
use common::{error::ProgramResult, strip};
use std::sync::mpsc::Sender;
const MQTT_PREFIX: &str = "aw_lights";
const FRIENDLY_NAME: &str = "AW Lights Light";
pub struct MqttBuilder {
topics: Topics,
config: MqttConfig,
}
impl MqttBuilder {
pub fn new(config: MqttConfig) -> Self {
let topics = Topics::new(&config);
Self { config, topics }
}
pub fn start(
self,
strip_tx: Sender<strip::Message>,
state_rx: Receiver<common::strip::State>,
) -> ProgramResult<()> {
let (client, connection) = self.create_conection();
let mut mqtt = Mqtt {
topics: self.topics,
config: self.config,
client,
};
mqtt.start(connection, strip_tx, state_rx)
}
fn create_conection(&self) -> (Client, Connection) {
info!("Creating mqtt client");
let mut mqttoptions = MqttOptions::new(
&self.config.mqtt_id,
&self.config.mqtt_broker,
self.config.mqtt_port,
);
mqttoptions.set_keep_alive(Duration::from_secs(5));
if let Some(mqtt_username) = self.config.mqtt_username.as_ref()
&& let Some(mqtt_password) = self.config.mqtt_password.as_ref()
{
info!("Using authentication with mqtt");
mqttoptions.set_credentials(mqtt_username, mqtt_password);
}
Client::new(mqttoptions, 10)
}
}
pub struct Mqtt {
topics: Topics,
config: MqttConfig,
client: rumqttc::Client,
// connection: rumqttc::Connection,
}
impl Mqtt {
// pub fn new(config: MqttConfig) -> Self {
// let topics = Topics::new(&config);
// Self {
// config,
// topics,
// // client,
// // connection,
// }
// }
pub fn start(
&mut self,
mut connection: Connection,
strip_tx: Sender<strip::Message>,
state_rx: Receiver<common::strip::State>,
) -> ProgramResult<()> {
info!("Starting mqtt");
self.init().map_err(|e| ProgramError::Boxed(Box::new(e)))?;
let (internal_tx, internal_rx) = channel::<InternalMessage>();
let internal_tx2 = internal_tx.clone();
thread::spawn(move || {
// Iterate to poll the eventloop for connection progress
for (_i, message) in connection.iter().enumerate() {
// info!("Notification #{i} = {:?}", message);
match message {
Ok(Event::Incoming(Incoming::Publish(p))) => {
let _ = internal_tx2.send(InternalMessage::InboundMqttPacket(p));
// info!(
// "Got publish notification. Trying to deserialize: {:#?}",
// serde_json::from_slice::<light::JsonIncoming>(&p.payload)
// )
}
Ok(Event::Outgoing(_))
| Ok(Event::Incoming(Packet::PingResp))
| Ok(Event::Incoming(Incoming::SubAck(_)))
| Ok(Event::Incoming(Incoming::ConnAck(_)))
| Ok(Event::Incoming(Incoming::PubAck(_)))
| Ok(Event::Incoming(Incoming::PubRec(_)))
| Ok(Event::Incoming(Packet::PingReq)) => {}
Ok(m) => info!("Got unhandled message: {m:?}"),
Err(e) => {
error!("Connection error to mqtt: {e:?}")
}
}
}
let _ = internal_tx2.send(InternalMessage::MqttDied);
});
thread::spawn(move || {
while let Ok(p) = state_rx.recv() {
let _ = internal_tx.send(InternalMessage::OutboundStatePacket(p));
}
});
while let Ok(msg) = internal_rx.recv() {
match msg {
InternalMessage::InboundMqttPacket(p) => {
if let Err(e) = self.handle_incoming_message(&strip_tx, p) {
info!("Got error: {e:?}");
}
}
InternalMessage::OutboundStatePacket(p) => {
let state_msg = self.gen_state_message(p);
info!("Sending state message: {:?}", state_msg);
// Send initial autodiscovery
if let Err(e) = self
.client
.publish(
&self.topics.state_topic,
QoS::AtLeastOnce,
false,
serde_json::to_vec(&state_msg).unwrap(),
)
.context("Sending state change")
{
error!("{e:?}");
}
}
InternalMessage::MqttDied => todo!(),
}
}
info!("Done with mqtt");
Ok(())
}
fn handle_incoming_message(
&self,
strip_tx: &Sender<strip::Message>,
publish: Publish,
) -> Result<()> {
if publish.topic == self.topics.command_topic {
info!("Got command topic");
let command = serde_json::from_slice::<light::JsonIncoming>(&publish.payload)
.context("Deserializing command message")?;
let translated_command = build_strip_tx_msg(&command)
.context("Translating command to internal strip_tx message")?;
info!("Setting light to state: {:?}", translated_command);
strip_tx
.send(translated_command)
.context("Sending command to strip_tx")?;
} else if publish.topic == homeassistant::HOMEASSISTANT_TOPIC {
if &publish.payload == homeassistant::STATUS_ONLINE {
info!("Homeassistant is online");
self.send_discovery()?;
} else if &publish.payload == homeassistant::STATUS_OFFLINE {
warn!("Homeassistant is offline");
} else {
anyhow::bail!("Homeassistant status topic {:?} unknown", &publish.payload);
}
} else {
anyhow::bail!("Incoming message has unknown topic: {:?}", publish.topic);
}
Ok(())
}
/// Called after the initial connection to mqtt
fn init(&mut self) -> Result<()> {
// let topics = Topics::new(self.config);
info!("Subscribing to homeassistant");
// Check if homeassistant is starting or not
self.client
.subscribe(homeassistant::HOMEASSISTANT_TOPIC, QoS::AtMostOnce)
.context("Subscribing to homeassistant status topic")?;
// Check for commands
self.client
.subscribe(&self.topics.command_topic, QoS::AtMostOnce)
.context("Subscribing to command topic")?;
self.send_discovery()?;
Ok(())
}
fn send_discovery(&self) -> Result<()> {
let discovery = self.gen_discovery_message();
info!(
"Sending discovery_message: {:?} (topic: {:?})",
discovery, self.topics.autodiscovery
);
// Send initial autodiscovery
self.client
.publish(
&self.topics.autodiscovery,
QoS::AtLeastOnce,
false,
serde_json::to_vec(&discovery).unwrap(),
)
.context("Sending initial autodiscovery")?;
Ok(())
}
/// Generates a discovery message
fn gen_discovery_message(&self) -> light::JsonDiscovery {
// "<discovery_prefix>/device_trigger/[<node_id>/]<object_id>/config",
// homeassistant/device_trigger/0x90fd9ffffedf1266/action_arrow_left_click/config
light::JsonDiscovery {
common: Common {
unique_id: Some(self.config.mqtt_id.to_string()),
..Common::default()
},
name: Some(FRIENDLY_NAME.to_string()),
effect_list: Some(
Parameters::get_names()
.iter()
.map(|s| s.to_string())
.collect(),
),
brightness: Some(false),
effect: Some(true),
supported_color_modes: Some(vec![light::ColorMode::Rgb]),
state_topic: Some(self.topics.state_topic.clone()),
..light::JsonDiscovery::new(self.topics.command_topic.clone())
}
}
fn gen_state_message(&self, state: strip::State) -> light::JsonIncoming {
// "<discovery_prefix>/device_trigger/[<node_id>/]<object_id>/config",
// homeassistant/device_trigger/0x90fd9ffffedf1266/action_arrow_left_click/config
light::JsonIncoming {
brightness: None,
color_mode: None,
color_temp: None,
color: Some(IncomingColor {
r: Some(state.color.0 as usize),
g: Some(state.color.1 as usize),
b: Some(state.color.2 as usize),
..IncomingColor::default()
}),
effect: Some(state.pattern),
state: Some(
(if state.on {
light::STATUS_DEFAULT_LIGHT_ON
} else {
light::STATUS_DEFAULT_LIGHT_OFF
})
.to_string(),
),
// transition: None,
..light::JsonIncoming::default()
}
}
}
fn build_strip_tx_msg(command: &JsonIncoming) -> Option<strip::Message> {
use strip::Message;
info!("Got incoming command: {command:?}");
if let Some(state) = &command.state
&& state == light::STATUS_DEFAULT_LIGHT_OFF
{
return Some(Message::ClearLights);
}
// if let Some(effect) = &command.effect
// && effect == "Rainbow"
// {
// return Some(Message::ChangePattern(Box::new(MovingRainbow::default())));
// }
if command.effect.is_none()
&& let Some(color) = &command.color
&& let Some(r) = color.r
&& let Some(g) = color.g
&& let Some(b) = color.b
{
return Some(Message::ChangePattern(Box::new(Solid::new(&SolidParams {
color: Rgb(r as u8, g as u8, b as u8),
}))));
}
if let Some(brightness) = &command.brightness {
let brightness = *brightness as u8;
return Some(Message::ChangePattern(Box::new(Solid::new(&SolidParams {
color: color::BLACK.fade_to(color::WHITE, brightness),
}))));
}
if let Some(state) = &command.state
&& state == light::STATUS_DEFAULT_LIGHT_ON
{
return Some(Message::TurnOn(
command.effect.as_ref().map(|s| s.to_string()),
));
}
error!("Not able to parse input as a command: {command:?}");
None
}
struct Topics {
autodiscovery: String,
state_topic: String,
command_topic: String,
}
impl Topics {
pub fn new(config: &MqttConfig) -> Self {
let mqtt_id = &config.mqtt_id;
Self {
autodiscovery: format!("{}/light/{mqtt_id}/config", config.mqtt_discovery_prefix),
state_topic: format!("{MQTT_PREFIX}/{mqtt_id}/state"),
command_topic: format!("{MQTT_PREFIX}/{mqtt_id}/set"),
}
}
}
enum InternalMessage {
InboundMqttPacket(Publish),
OutboundStatePacket(strip::State),
MqttDied,
}
// unique_id: bedroom_switch
// name: "Bedroom Switch"
// state_topic: "home/bedroom/switch1"
// command_topic: "home/bedroom/switch1/set"
// availability:
// - topic: "home/bedroom/switch1/available"
// payload_on: "ON"
// payload_off: "OFF"
// state_on: "ON"
// state_off: "OFF"
// optimistic: false
// qos: 0
// retain: true
// https://github.com/smrtnt/Open-Home-Automation/blob/master/ha_mqtt_rgbw_light_with_discovery/ha_mqtt_rgbw_light_with_discovery.ino
// On connect:
// JsonObject& root = staticJsonBuffer.createObject();
// root["name"] = FRIENDLY_NAME;
// root["platform"] = "mqtt_json";
// root["state_topic"] = MQTT_STATE_TOPIC;
// root["command_topic"] = MQTT_COMMAND_TOPIC;
// root["brightness"] = true;
// root["rgb"] = true;
// root["white_value"] = true;
// root["color_temp"] = true;
// root["effect"] = true;
// root["effect_list"] = EFFECT_LIST;
// On update
// cmd = CMD_NOT_DEFINED;
// DynamicJsonBuffer dynamicJsonBuffer;
// JsonObject& root = dynamicJsonBuffer.createObject();
// root["state"] = bulb.getState() ? MQTT_STATE_ON_PAYLOAD : MQTT_STATE_OFF_PAYLOAD;
// root["brightness"] = bulb.getBrightness();
// JsonObject& color = root.createNestedObject("color");
// color["r"] = bulb.getColor().red;
// color["g"] = bulb.getColor().green;
// color["b"] = bulb.getColor().blue;
// root["white_value"] = bulb.getColor().white;
// root["color_temp"] = bulb.getColorTemperature();
// Status topic (/status)
// "alive" or "dead"
//#define MQTT_STATE_TOPIC_TEMPLATE "%s/rgbw/state"
// #define MQTT_COMMAND_TOPIC_TEMPLATE "%s/rgbw/set"
// #define MQTT_STATUS_TOPIC_TEMPLATE "%s/rgbw/status" // MQTT connection: alive/dead
// #define MQTT_HOME_ASSISTANT_DISCOVERY_PREFIX "homeassistant"
// #define MQTT_STATE_ON_PAYLOAD "ON"
// #define MQTT_STATE_OFF_PAYLOAD "OFF"

3
package-lock.json generated
View File

@ -1,3 +0,0 @@
{
"lockfileVersion": 1
}

View File

@ -1,2 +1,2 @@
# unstable_features = true unstable_features = true
# imports_granularity = "Crate" imports_granularity = "Crate"

View File

@ -2,19 +2,19 @@
// #![allow(dead_code, unused_imports)] // #![allow(dead_code, unused_imports)]
// Enable clippy 'hard mode' // Enable clippy 'hard mode'
#![warn(clippy::all, clippy::pedantic, clippy::nursery)] #![warn(clippy::all, clippy::pedantic, clippy::nursery)]
// Intended behavior (10_f64 as i32)
// #![allow(clippy::cast_possible_truncation)]
#![allow( #![allow(
// Cannot be fixed // Cannot be fixed
clippy::multiple_crate_versions, clippy::multiple_crate_versions,
// Intentional code // Intentional code
clippy::map_err_ignore, clippy::map_err_ignore,
// "as f32" used frequently in this project // "as f32" used frequently in this project
clippy::cast_precision_loss, clippy::cast_precision_loss, clippy::cast_possible_truncation, clippy::cast_sign_loss,
// This is fine // This is fine
clippy::implicit_return, clippy::implicit_return,
// Missing docs is fine // Missing docs is fine
clippy::missing_docs_in_private_items, clippy::missing_docs_in_private_items,
// Many redeclerations that are just pub used
clippy::module_name_repetitions,
)] )]
// Restriction lints // Restriction lints
#![warn( #![warn(
@ -28,78 +28,97 @@
)] )]
// See https://rust-lang.github.io/rust-clippy/master/index.html for more lints // See https://rust-lang.github.io/rust-clippy/master/index.html for more lints
mod color;
mod errors;
mod pattern;
mod strip; mod strip;
mod ui; use clap::Parser;
mod webui; use common::{
use errors::{ProgramError, ProgramResult}; error::{ProgramError, ProgramResult},
use std::io; strip::Message,
use std::io::Write; };
use std::sync::mpsc::{channel, Sender}; use std::{
use std::thread; sync::mpsc::{channel, Sender},
thread,
time::Duration,
};
use strip::LedStrip; use strip::LedStrip;
use ui::console_ui_loop; use tracing::{error, info};
fn main() -> ProgramResult<()> { fn main() -> ProgramResult<()> {
// Initialize any config
dotenv::dotenv().ok();
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
// Use clap to parse the configuration
let config = common::Config::parse();
// Strip control transmitter and receiver // Strip control transmitter and receiver
let (strip_tx, strip_rx) = channel::<strip::Message>(); let (strip_tx, strip_rx) = channel::<common::strip::Message>();
let (console_strip_tx, webui_strip_tx) = (strip_tx.clone(), strip_tx); let webui_strip_tx = strip_tx.clone();
let mqtt_strip_tx = strip_tx.clone();
let (message_tx, message_rx) = channel::<errors::Message>(); let (message_tx, message_rx) = channel::<common::Message>();
let (strip_terminated_tx, strip_terminated_rx) = channel::<()>();
let (state_tx, state_rx) = channel::<common::strip::State>();
// The strip itself
let config_clone = config.clone();
make_child(message_tx.clone(), move |message_tx| -> ProgramResult<()> { make_child(message_tx.clone(), move |message_tx| -> ProgramResult<()> {
let mut strip = LedStrip::new(strip::Config { let mut strip = LedStrip::new(config_clone)?;
// I have 89 right now, but start off with 20 strip.strip_loop(message_tx, &state_tx, &strip_rx, strip_terminated_tx)
num_lights: 89,
// Skip 14 lights
shift_lights: 14,
// Scaling factor (scale 0..255)
global_brightness_max: 255,
tick_time_ms: strip::DEFAULT_TICK_TIME_MS,
})?;
strip.strip_loop(message_tx, &strip_rx)
}); });
make_child(message_tx.clone(), move |message_tx| -> ProgramResult<()> { // Webui user-interface
console_ui_loop(message_tx, &console_strip_tx) make_child(
}); message_tx.clone(),
move |_message_tx| -> ProgramResult<()> {
webui::start(webui_strip_tx).map_err(ProgramError::IoError)
},
);
make_child(message_tx, move |message_tx| -> ProgramResult<()> { // Mqtt user-interface
webui::start(message_tx.clone(), webui_strip_tx).map_err(ProgramError::IoError) make_child(
}); message_tx.clone(),
move |_message_tx| -> ProgramResult<()> {
mqtt::MqttBuilder::new(config.mqtt.clone()).start(mqtt_strip_tx, state_rx)
// mqtt::start(mqtt_strip_tx, config.mqtt.clone())
},
);
let mut input_prompt: Option<String> = None; std::mem::drop(message_tx);
loop {
'ret: loop {
match message_rx.recv() { match message_rx.recv() {
Ok(errors::Message::String(s)) => println!("\r{}", s), Ok(common::Message::String(s)) => info!(s),
Ok(errors::Message::Error(e)) => println!("\rError!! {:?}", e), Ok(common::Message::Error(e)) => error!("{e:?}"),
Ok(errors::Message::Terminated) => { Ok(common::Message::Terminated) => {
panic!("A thread terminated") info!("Exiting due to Terminated signal");
// First, try to turn the strip off
let _ = strip_tx.send(Message::Quit);
// Wait a bit to give the strip a chance to exit
if let Err(e) = strip_terminated_rx.recv_timeout(Duration::from_secs(3)) {
error!("Strip could not be terminated {e:?}");
}
break 'ret Ok(());
} }
Ok(errors::Message::InputPrompt(i)) => input_prompt = Some(i),
Err(e) => { Err(e) => {
return Err(ProgramError::General(format!( break 'ret Err(ProgramError::General(format!(
"All transmitters hung up! {:?}", "All transmitters hung up! {e:?}"
e
))) )))
} }
} }
if let Some(ref s) = input_prompt {
print!("{}: ", s);
// We do not care if we can't flush
let _ = io::stdout().flush();
}
} }
} }
fn make_child<F: 'static>(message_tx: Sender<errors::Message>, f: F) fn make_child<F>(message_tx: Sender<common::Message>, f: F)
where where
F: FnOnce(&Sender<errors::Message>) -> ProgramResult<()> + std::marker::Send, F: FnOnce(&Sender<common::Message>) -> ProgramResult<()> + std::marker::Send + 'static,
{ {
thread::spawn(move || match f(&message_tx) { thread::spawn(move || match f(&message_tx) {
Ok(()) => message_tx.send(errors::Message::Terminated), Ok(()) => message_tx.send(common::Message::Terminated),
Err(e) => message_tx.send(errors::Message::Error(e)), Err(e) => message_tx.send(common::Message::Error(e)),
}); });
} }

View File

@ -1,89 +0,0 @@
use crate::color::Rgb;
use serde::{Deserialize, Serialize};
use std::collections::vec_deque;
pub mod collide;
pub mod fade;
pub mod flashing;
pub mod moving_pixel;
pub mod moving_rainbow;
pub mod orb;
pub mod solid;
pub use collide::Collide;
pub use fade::Fade;
pub use flashing::Flashing;
pub use moving_pixel::MovingPixel;
pub use moving_rainbow::MovingRainbow;
pub use orb::Orb;
pub use solid::Solid;
#[derive(Serialize, Deserialize, Debug)]
pub enum Parameters {
Collide(Rgb, Rgb, Rgb),
Fade((Rgb,)),
MovingPixel((Rgb,)),
MovingRainbow(u8, bool, u8),
Orb(Rgb, u8, u8),
Solid((Rgb,)),
Flashing(Vec<Rgb>, u8, u16),
}
impl Parameters {
pub fn into_pattern(self) -> Box<dyn Pattern + Send + Sync> {
match self {
Self::Collide(l, r, c) => Box::new(Collide::new(l, r, c)),
Self::Fade((c,)) => Box::new(Fade::new(c)),
Self::MovingPixel((c,)) => Box::new(MovingPixel::new(c)),
Self::MovingRainbow(w, f, s) => Box::new(MovingRainbow::new(w, f, s)),
Self::Orb(c, x, y) => Box::new(Orb::new(c, x, y)),
Self::Solid((c,)) => Box::new(Solid::new(c)),
Self::Flashing(cs, w, r) => Box::new(Flashing::new(cs, w, r)),
}
}
}
pub trait Pattern: std::fmt::Debug + Send + Sync {
fn init(&mut self, num_lights: u16) -> Result<(), ()>;
fn step(&mut self) -> Result<bool, ()>;
fn get_strip(&self) -> vec_deque::Iter<Rgb>;
}
// #[cfg(test)]
// mod tests {
// use super::*;
// const NUM_LIGHTS: u16 = 10;
// fn test_strip() -> Vec<Rgb> {
// vec![color::BLACK; NUM_LIGHTS.into()]
// }
// #[test]
// fn moving_pixel() {
// let color = Rgb(123, 152, 89);
// let mut pat = MovingPixel::new(color.clone());
// let mut strip = test_strip();
// assert!(pat.init(&mut strip, NUM_LIGHTS).is_ok());
// // One is my color
// assert_eq!(strip.iter().filter(|c| **c == color).count(), 1);
// // The rest are off
// assert_eq!(
// strip.iter().filter(|c| **c == color::BLACK).count(),
// (NUM_LIGHTS - 1).into()
// );
// pat.step(&mut strip);
// // One is my color
// assert_eq!(strip.iter().filter(|c| **c == color).count(), 1);
// // The rest are off
// assert_eq!(
// strip.iter().filter(|c| **c == color::BLACK).count(),
// (NUM_LIGHTS - 1).into()
// );
// }
// #[test]
// fn solid() {}
// #[test]
// fn moving_rainbow() {}
// #[test]
// fn fade() {}
// #[test]
// fn collide() {}
// }

View File

@ -1,57 +0,0 @@
use super::Pattern;
use crate::color::{self, Rgb};
use std::collections::vec_deque;
use std::collections::VecDeque;
#[derive(Clone, Debug)]
pub struct Fade {
color: Rgb,
step: u8,
direction: bool,
num_lights: u16,
lights_buf: VecDeque<Rgb>,
}
impl Fade {
pub fn new(color: Rgb) -> Self {
Self {
color,
step: 0,
direction: true,
num_lights: 1,
lights_buf: VecDeque::new(),
}
}
}
impl Pattern for Fade {
fn step(&mut self) -> Result<bool, ()> {
if self.direction {
if self.step == 254 {
self.direction = !self.direction;
}
self.step = self.step.saturating_add(1);
} else {
if self.step == 1 {
self.direction = !self.direction;
}
self.step = self.step.saturating_sub(1);
}
self.lights_buf = VecDeque::from(vec![
Rgb(self.step, self.step, self.step);
self.num_lights.into()
]);
Ok(true)
}
fn init(&mut self, num_lights: u16) -> Result<(), ()> {
if num_lights < 1 {
return Err(());
}
self.step = 0;
self.direction = true;
self.num_lights = num_lights;
self.lights_buf = VecDeque::from(vec![color::BLACK; self.num_lights.into()]);
Ok(())
}
fn get_strip(&self) -> vec_deque::Iter<Rgb> {
self.lights_buf.iter()
}
}

View File

@ -1,61 +0,0 @@
use super::Pattern;
use crate::color::{self, Rgb};
use std::{
collections::{vec_deque, VecDeque},
iter,
};
#[derive(Clone, Debug)]
pub struct Flashing {
lights_buf: VecDeque<Rgb>,
width: u8,
step: u16,
tick_rate: u16,
colors: Vec<Rgb>,
}
impl Flashing {
pub fn new(colors: Vec<Rgb>, width: u8, tick_rate: u16) -> Self {
Self {
colors,
tick_rate,
step: 0,
width,
lights_buf: VecDeque::new(),
}
}
}
impl Pattern for Flashing {
fn step(&mut self) -> Result<bool, ()> {
self.step = self.step.wrapping_add(1).rem_euclid(self.tick_rate);
if self.step != 0 {
return Ok(false);
}
self.lights_buf.rotate_right(self.width.into());
Ok(true)
}
fn init(&mut self, num_lights: u16) -> Result<(), ()> {
if num_lights < 1 {
return Err(());
}
let length_factor = num_lights.saturating_mul(u16::from(self.width));
let buf_length = color::min_with_factor(num_lights, length_factor)?;
self.step = 0;
self.lights_buf = self
.colors
.iter()
.flat_map(|&x| iter::repeat(x).take(self.width.into()))
.cycle()
.take(buf_length.into())
.collect();
Ok(())
}
fn get_strip(&self) -> vec_deque::Iter<Rgb> {
self.lights_buf.iter()
}
}

View File

@ -1,50 +0,0 @@
use super::Pattern;
use crate::color::{self, Rgb};
use std::collections::vec_deque;
use std::collections::VecDeque;
#[derive(Clone, Debug)]
pub struct MovingPixel {
color: Rgb,
num_lights: u16,
step: u16,
lights_buf: VecDeque<Rgb>,
}
impl MovingPixel {
pub fn new(color: Rgb) -> Self {
Self {
color,
step: 0,
// TODO: Better initialization
num_lights: 1,
lights_buf: VecDeque::new(),
}
}
}
impl Pattern for MovingPixel {
fn step(&mut self) -> Result<bool, ()> {
let len = self.num_lights;
self.lights_buf.swap(
self.step.rem_euclid(len).into(),
self.step.saturating_add(1).rem_euclid(len).into(),
);
self.step = self.step.wrapping_add(1);
Ok(true)
}
fn init(&mut self, num_lights: u16) -> Result<(), ()> {
if num_lights < 1 {
return Err(());
}
self.step = 0;
self.num_lights = num_lights;
// Set the strip to black except for one pixel
self.lights_buf = VecDeque::from(vec![color::BLACK; num_lights.into()]);
*self.lights_buf.get_mut(0).ok_or(())? = self.color;
Ok(())
}
fn get_strip(&self) -> vec_deque::Iter<Rgb> {
self.lights_buf.iter()
}
}

View File

@ -1,77 +0,0 @@
use super::Pattern;
use crate::color::{self, Rgb, RAINBOW};
use std::collections::{vec_deque, VecDeque};
use std::convert::TryFrom;
use std::iter;
#[derive(Clone, Debug)]
pub struct MovingRainbow {
lights_buf: VecDeque<Rgb>,
skip: u8,
width: u8,
forward: bool,
}
impl MovingRainbow {
pub fn new(width: u8, forward: bool, skip: u8) -> Self {
Self {
lights_buf: VecDeque::new(),
skip,
width,
forward,
}
}
}
impl Pattern for MovingRainbow {
fn step(&mut self) -> Result<bool, ()> {
if self.forward {
self.lights_buf.rotate_left(1);
} else {
self.lights_buf.rotate_right(1);
}
Ok(true)
}
fn init(&mut self, num_lights: u16) -> Result<(), ()> {
if !(1..=255).contains(&num_lights) {
return Err(());
}
if self.width < 1 {
return Err(());
}
// (width + skip) * RAINBOW.len()
let length_factor = u16::from(self.width)
.checked_add(self.skip.into())
.ok_or(())?
.saturating_mul(u16::try_from(RAINBOW.len()).or(Err(()))?);
// The length of the buffer
// Always a factor of length_factor
let buf_length = color::min_with_factor(num_lights, length_factor.into())?;
println!(
"Got buf length: {} with #lights {} and length factor {} ({})",
buf_length, num_lights, length_factor, (self.width+self.skip) as usize *RAINBOW.len()
);
// num_lights
// .checked_sub(1)
// .ok_or(())?
// .div_euclid(length_factor)
// .checked_add(1)
// .ok_or(())?
// .saturating_mul(length_factor);
self.lights_buf = RAINBOW
.iter()
.flat_map(|&x| {
iter::repeat(x)
.take(self.width.into())
.chain(iter::repeat(color::BLACK).take(self.skip.into()))
})
.cycle()
.take(buf_length.into())
.collect();
Ok(())
}
fn get_strip(&self) -> vec_deque::Iter<Rgb> {
self.lights_buf.iter()
}
}

View File

@ -1,39 +0,0 @@
use super::Pattern;
use crate::color::Rgb;
use std::collections::vec_deque;
use std::collections::VecDeque;
#[derive(Clone, Debug)]
pub struct Solid {
color: Rgb,
has_run: bool,
lights_buf: VecDeque<Rgb>,
}
impl Solid {
pub fn new(color: Rgb) -> Self {
Self {
color,
has_run: false,
lights_buf: VecDeque::new(),
}
}
}
impl Pattern for Solid {
fn step(&mut self) -> Result<bool, ()> {
let ret = !self.has_run;
self.has_run = true;
Ok(ret)
}
fn init(&mut self, num_lights: u16) -> Result<(), ()> {
if num_lights < 1 {
return Err(());
}
self.has_run = false;
self.lights_buf = VecDeque::from(vec![self.color; num_lights.into()]);
Ok(())
}
fn get_strip(&self) -> vec_deque::Iter<Rgb> {
self.lights_buf.iter()
}
}

View File

@ -1,78 +1,105 @@
use crate::color; use common::{
use crate::errors; color,
use crate::errors::ProgramError; error::ProgramError,
use crate::pattern::{self, Pattern}; pattern::{self, ColorIterator, Pattern},
use std::cmp; strip::Message,
use std::ops::Add; Config, MAX_NUM_LIGHTS, MIN_TICK_TIME,
use std::process; };
use std::sync::mpsc::{Receiver, Sender}; use std::{
use std::time::{Duration, Instant}; cmp,
use ws2818_rgb_led_spi_driver::adapter_gen::Ws28xxAdapter; ops::Add,
use ws2818_rgb_led_spi_driver::adapter_spi::Ws28xxSpiAdapter; str::FromStr,
sync::mpsc::{Receiver, Sender},
/// Maximum number of lights allowed thread,
const MAX_NUM_LIGHTS: u16 = 128; time::{Duration, Instant},
/// Default time per tick };
pub const DEFAULT_TICK_TIME_MS: u64 = 50; use tracing::{error, info};
/// Minimum time per tick before strip breaks use ws2818_rgb_led_spi_driver::{adapter_gen::Ws28xxAdapter, adapter_spi::Ws28xxSpiAdapter};
const MIN_TICK_TIME: u64 = 10;
#[derive(Debug, Clone)]
pub struct Config {
/// Number of lights
pub num_lights: u16,
/// Number of lights to skip
pub shift_lights: u16,
/// Global brightness multiplier
pub global_brightness_max: u8,
/// Time per tick
pub tick_time_ms: u64,
}
pub enum Message {
ClearLights,
ChangePattern(Box<dyn Pattern + Send>),
SetNumLights(u16),
SetTickTime(u64),
Quit,
}
#[allow(clippy::module_name_repetitions)] #[allow(clippy::module_name_repetitions)]
pub struct LedStrip { pub struct LedStrip {
pub adapter: Box<dyn Ws28xxAdapter>, pub adapter: Box<dyn Ws28xxAdapter>,
pub config: Config, pub config: Config,
pub pattern: Box<dyn Pattern>, pub pattern: Box<dyn Pattern + Send + Sync>,
pub state: common::strip::State,
} }
impl LedStrip { impl LedStrip {
pub fn new(config: Config) -> Result<Self, ProgramError> { pub fn new(config: Config) -> Result<Self, ProgramError> {
let adapter = Box::new( let adapter = Box::new(
Ws28xxSpiAdapter::new("/dev/spidev0.0") Ws28xxSpiAdapter::new(&config.serial_interface)
.map_err(|_| "Cannot start device /dev/spidev0.0!")?, .map_err(|_| format!("Cannot start device {}!", config.serial_interface))?,
); );
let pattern = Box::new(pattern::Solid::new(color::BLACK));
let num_lights = config.num_lights; let num_lights = config.num_lights;
// Initialize the pattern if they requested one; if there are any issues, initialize with black instead
let pattern = config
.initial_pattern
.as_ref()
.and_then(|ip| {
pattern::Parameters::from_str(ip)
.as_ref()
.map(pattern::Parameters::to_pattern)
.map_err(|e| error!("Could not initialize with requested pattern {ip}: {e:?}"))
.ok()
})
.unwrap_or_else(|| {
info!("Using default black pattern");
Box::new(pattern::Solid::new(&pattern::SolidParams {
color: color::BLACK,
}))
});
let state = common::strip::State {
on: true,
pattern: pattern.get_name().to_string(),
// brightnes: u8,
color: color::WHITE,
// Off,
// Pattern
};
let mut ret = Self { let mut ret = Self {
adapter, adapter,
pattern,
config, config,
pattern,
state,
}; };
ret.set_num_lights(num_lights); ret.set_num_lights(num_lights);
Ok(ret) Ok(ret)
} }
/// Writes a buffer from the given pattern
fn write_buf_from_pattern(&mut self) -> Result<(), ProgramError> { fn write_buf_from_pattern(&mut self) -> Result<(), ProgramError> {
let global_brightness_max = self.config.global_brightness_max; let global_brightness_max = self.config.global_brightness_max;
let data = vec![color::BLACK]
.iter() let pattern_iterator: ColorIterator = if self.config.mirrored_lights {
.cycle() if self.config.reverse_mirror {
.take(self.config.shift_lights.into()) Box::new(
.chain( self.pattern
self.pattern .get_strip()
.get_strip() .chain(self.pattern.get_strip().rev()),
// .as_slice() )
.take(self.config.num_lights.into()), } else {
) Box::new(self.pattern.get_strip().chain(self.pattern.get_strip()))
}
} else {
Box::new(self.pattern.get_strip())
};
let pattern_iterator = if self.config.reverse {
Box::new(pattern_iterator.rev())
} else {
pattern_iterator
};
let data = std::iter::repeat(&color::BLACK)
// Disable the first skip_lights
.take(self.config.skip_lights.into())
// Then, take whatever the patttern came up with
.chain(pattern_iterator.take(self.config.num_lights.into()))
// Only take the length of the light strip
.map(|c| c.to_tuple()) .map(|c| c.to_tuple())
.map(|(r, g, b)| { .map(|(r, g, b)| {
( (
@ -89,69 +116,147 @@ impl LedStrip {
fn set_num_lights(&mut self, num_lights: u16) { fn set_num_lights(&mut self, num_lights: u16) {
if num_lights > MAX_NUM_LIGHTS { if num_lights > MAX_NUM_LIGHTS {
println!( error!("Cannot set lights to {num_lights} as it exceeds max of {MAX_NUM_LIGHTS}");
"Cannot set lights to {} as it exceeds max of {}",
num_lights, MAX_NUM_LIGHTS
);
return; return;
} }
if self.pattern.init(num_lights).is_ok() { if let Err(e) = self.pattern.cleanup() {
error!("Error cleaning up old pattern: {e:?}");
}
if self
.pattern
.init(if self.config.mirrored_lights {
self.config.num_lights / 2
} else {
self.config.num_lights
})
.is_ok()
{
self.config.num_lights = num_lights; self.config.num_lights = num_lights;
info!("Updated tick time to {}", self.config.tick_time_ms);
} else {
error!("Could not initialize pattern with new num_lights value {num_lights}");
} }
} }
/// Gets the number of lights the pattern needs to concern itself with
///
/// Will be half of num_lights if mirrored_lights is true
pub fn pattern_num_lights(&self) -> u16 {
if self.config.mirrored_lights {
self.config.num_lights / 2
} else {
self.config.num_lights
}
}
// pub fn set_state(&self) {
// // let mut state = common::strip::State::default();
// }
pub fn strip_loop( pub fn strip_loop(
&mut self, &mut self,
message_tx: &Sender<errors::Message>, message_tx: &Sender<common::Message>,
state_tx: &Sender<common::strip::State>,
rx: &Receiver<Message>, rx: &Receiver<Message>,
strip_terminated: Sender<()>,
) -> Result<(), ProgramError> { ) -> Result<(), ProgramError> {
let mut exit = false; let mut exit = false;
let _ = state_tx.send(self.state.clone());
loop { loop {
let target_time = Instant::now().add(Duration::from_millis(self.config.tick_time_ms)); let target_time = Instant::now().add(Duration::from_millis(self.config.tick_time_ms));
if let Ok(message) = rx.try_recv() { if let Ok(message) = rx.try_recv() {
match message { match message {
Message::ClearLights => { Message::ClearLights => {
let mut pat = Box::new(pattern::Solid::new(color::BLACK)); let mut pat = Box::new(pattern::Solid::new(&pattern::SolidParams {
if pat.init(self.config.num_lights).is_ok() { color: color::BLACK,
}));
if pat.init(self.pattern_num_lights()).is_ok() {
self.pattern = pat; self.pattern = pat;
self.state.pattern = self.pattern.get_name().to_string();
self.state.on = false;
let _ = state_tx.send(self.state.clone());
info!("Cleared lights");
} else { } else {
let _ = message_tx.send(errors::Message::String(format!( let _result = message_tx.send(common::Message::String(format!(
"Clearing light strip: {:?}", "Clearing light strip: {pat:?}"
pat
))); )));
} }
} }
Message::ChangePattern(pat) => { Message::ChangePattern(mut pat) => match pat.init(self.pattern_num_lights()) {
let mut pat = pat; Ok(()) => {
if pat.init(self.config.num_lights).is_ok() { if let Err(e) = self.pattern.cleanup() {
error!("Error cleaning up old pattern: {e:?}");
}
self.pattern = pat; self.pattern = pat;
} else { info!("Changed pattern");
let _ = message_tx.send(errors::Message::String(format!( self.state.on = true;
"Error initializing pattern: {:?}", self.state.pattern = self.pattern.get_name().to_string();
pat let _ = state_tx.send(self.state.clone());
}
Err(e) => {
let _result = message_tx.send(common::Message::String(format!(
"Error initializing pattern {pat:?}: {e:?}",
))); )));
} }
},
Message::TurnOn(pattern_name) => {
// It's already on
if self.state.on {
continue;
}
if let Err(e) = self.pattern.cleanup() {
error!("Error cleaning up old pattern: {e:?}");
}
// Parameters::from_str(pattern_name)
let mut pat = pattern_name
.and_then(|p| pattern::Parameters::from_str(&p).ok())
.map(|p| p.to_pattern())
.unwrap_or_else(|| {
self.state.color = color::WHITE;
Box::new(pattern::Solid::new(&pattern::SolidParams {
color: color::WHITE,
}))
});
if pat.init(self.pattern_num_lights()).is_ok() {
self.pattern = pat;
self.state.pattern = self.pattern.get_name().to_string();
self.state.on = true;
let _ = state_tx.send(self.state.clone());
info!("Turned on");
}
} }
Message::SetNumLights(num_lights) => { Message::SetNumLights(num_lights) => {
self.set_num_lights(num_lights); self.set_num_lights(num_lights);
} }
Message::SetTickTime(tick_time_ms) => { Message::SetTickTime(tick_time_ms) => {
if tick_time_ms < MIN_TICK_TIME { if tick_time_ms < MIN_TICK_TIME {
let _ = message_tx.send(errors::Message::String(format!( let _result = message_tx.send(common::Message::String(format!(
"Error with tick time: {}", "Error with tick time: {tick_time_ms}"
tick_time_ms
))); )));
} }
self.config.tick_time_ms = tick_time_ms; self.config.tick_time_ms = tick_time_ms;
info!("Updated tick time to {}", self.config.tick_time_ms);
} }
Message::Quit => { Message::Quit => {
exit = true; exit = true;
let mut pat = pattern::Solid::new(color::BLACK); let mut pat = pattern::Solid::new(&pattern::SolidParams {
if pat.init(self.config.num_lights).is_ok() { color: color::BLACK,
});
if pat.init(self.pattern_num_lights()).is_ok() {
if let Err(e) = self.pattern.cleanup() {
error!("Error cleaning up old pattern: {e:?}");
}
self.pattern = Box::new(pat); self.pattern = Box::new(pat);
} else { } else {
let _ = message_tx.send(errors::Message::String(String::from( let _result = message_tx.send(common::Message::String(String::from(
"Could not construct clear pattern", "Could not construct clear pattern",
))); )));
} }
@ -168,17 +273,28 @@ impl LedStrip {
} }
if exit { if exit {
let _ = message_tx.send(errors::Message::String(String::from( let _result = message_tx.send(common::Message::String(String::from(
"Exiting as requested", "Exiting as requested",
))); )));
process::exit(0);
break;
} }
loop { // Wait out the rest of the time
if Instant::now() >= target_time { thread::sleep(target_time.saturating_duration_since(Instant::now()));
break;
} // Required if clock is not set up properly
} // loop {
// if Instant::now() >= target_time {
// break;
// }
// }
} }
let _ = strip_terminated.send(());
info!("Strip thread ended successfully");
Ok(())
} }
} }

View File

@ -1,26 +1,26 @@
use crate::color::Rgb; use common::{
use crate::errors::{self, ProgramError, ProgramResult}; color::Rgb,
use crate::pattern::{self, Pattern}; error::{self, ProgramError, ProgramResult},
use crate::strip; pattern::{self, Pattern},
use std::io; strip,
use std::io::Write; };
use std::sync::mpsc::Sender; use std::{io, io::Write, sync::mpsc::Sender};
pub fn console_ui_loop( pub fn console_ui_loop(
message_tx: &Sender<errors::Message>, message_tx: &Sender<common::Message>,
strip_tx: &Sender<strip::Message>, strip_tx: &Sender<strip::Message>,
) -> ProgramResult<()> { ) -> ProgramResult<()> {
loop { loop {
let line = get_line(message_tx, "Command (cfqs)")?; let line = get_line(message_tx, "Command (cfqs)")?;
if let Err(msg) = parse_cmd(strip_tx, &line) { if let Err(msg) = parse_cmd(strip_tx, &line) {
println!("Command error: {}", msg); error!("Command error: {msg}");
} }
} }
} }
fn parse_cmd(strip_tx: &Sender<strip::Message>, s: &str) -> Result<(), String> { fn parse_cmd(strip_tx: &Sender<strip::Message>, s: &str) -> Result<(), String> {
match s match s
.split(char::is_whitespace) .split_ascii_whitespace()
.collect::<Vec<&str>>() .collect::<Vec<&str>>()
.as_slice() .as_slice()
{ {
@ -33,14 +33,14 @@ fn parse_cmd(strip_tx: &Sender<strip::Message>, s: &str) -> Result<(), String> {
b.parse::<u8>() b.parse::<u8>()
.map_err(|_| String::from("Blue could not be parsed"))?, .map_err(|_| String::from("Blue could not be parsed"))?,
); );
change_pattern(strip_tx, Box::new(pattern::Fade::new(color))) change_pattern(strip_tx, Box::new(pattern::Fade::new(&pattern::FadeParams {color})))
} }
["f", c] => { ["f", c] => {
let color_value = c let color_value = c
.parse::<u8>() .parse::<u8>()
.map_err(|_| String::from("Could not parse color"))?; .map_err(|_| String::from("Could not parse color"))?;
let color = Rgb(color_value, color_value, color_value); let color = Rgb(color_value, color_value, color_value);
change_pattern(strip_tx, Box::new(pattern::Fade::new(color))) change_pattern(strip_tx, Box::new(pattern::Fade::new(&pattern::FadeParams {color})))
} }
["m", r, g, b] => { ["m", r, g, b] => {
let color = Rgb( let color = Rgb(
@ -51,50 +51,49 @@ fn parse_cmd(strip_tx: &Sender<strip::Message>, s: &str) -> Result<(), String> {
b.parse::<u8>() b.parse::<u8>()
.map_err(|_| String::from("Blue could not be parsed"))?, .map_err(|_| String::from("Blue could not be parsed"))?,
); );
change_pattern(strip_tx, Box::new(pattern::MovingPixel::new(color))) change_pattern(strip_tx, Box::new(pattern::MovingPixel::new(&pattern::MovingPixelParams {color})))
} }
["m", c] => { ["m", c] => {
let color_value = c let color_value = c
.parse::<u8>() .parse::<u8>()
.map_err(|_| String::from("Could not parse color"))?; .map_err(|_| String::from("Could not parse color"))?;
let color = Rgb(color_value, color_value, color_value); let color = Rgb(color_value, color_value, color_value);
change_pattern(strip_tx, Box::new(pattern::MovingPixel::new(color))) change_pattern(strip_tx, Box::new(pattern::MovingPixel::new(&pattern::MovingPixelParams {color})))
}, },
["c", r, g, b] => { ["c", r, g, b] => {
let color = parse_color(r, g, b)?; let color = parse_color(r, g, b)?;
change_pattern(strip_tx, Box::new(pattern::Solid::new(color))) change_pattern(strip_tx, Box::new(pattern::Solid::new(&pattern::SolidParams {color})))
} }
["c", c] => { ["c", c] => {
let color = parse_color(c, c, c)?; let color = parse_color(c, c, c)?;
change_pattern(strip_tx, Box::new(pattern::Solid::new(color))) change_pattern(strip_tx, Box::new(pattern::Solid::new(&pattern::SolidParams {color})))
}, },
["r"] => change_pattern(strip_tx, Box::new(pattern::MovingRainbow::new(4, true, 0))), ["r"] => change_pattern(strip_tx, Box::new(pattern::MovingRainbow::new(&pattern::MovingRainbowParams {width: 4, forward: true, skip: 0, fromcenter: false}))),
["r", w] => { ["r", w] => {
let width = w.parse::<u8>().map_err(|_| String::from("Width could not be parsed"))?; let width = w.parse::<u8>().map_err(|_| String::from("Width could not be parsed"))?;
change_pattern(strip_tx, Box::new(pattern::MovingRainbow::new(width, true, 0))) change_pattern(strip_tx, Box::new(pattern::MovingRainbow::new(&pattern::MovingRainbowParams {width, forward: true, skip: 0, fromcenter: false})))
}, },
["r", w, f] => { ["r", w, f] => {
let width = w.parse::<u8>().map_err(|_| String::from("Width could not be parsed"))?; let width = w.parse::<u8>().map_err(|_| String::from("Width could not be parsed"))?;
change_pattern(strip_tx, Box::new(pattern::MovingRainbow::new(width, ["t", "T"].contains(f), 0))) change_pattern(strip_tx, Box::new(pattern::MovingRainbow::new(&pattern::MovingRainbowParams {width, forward: ["t", "T"].contains(f), skip: 0, fromcenter: false})))
}, },
["r", w, f, s] => { ["r", w, f, s] => {
let width = w.parse::<u8>().map_err(|_| String::from("Width could not be parsed"))?; let width = w.parse::<u8>().map_err(|_| String::from("Width could not be parsed"))?;
let shift = s.parse::<u8>().map_err(|_| String::from("Shift could not be parsed"))?; let skip = s.parse::<u8>().map_err(|_| String::from("Skip could not be parsed"))?;
change_pattern(strip_tx, Box::new(pattern::MovingRainbow::new(width, ["t", "T"].contains(f), shift))) change_pattern(strip_tx, Box::new(pattern::MovingRainbow::new(&pattern::MovingRainbowParams {width, forward: ["t", "T"].contains(f), skip, fromcenter: false})))
}, },
["b", r1, g1, b1, r2, g2, b2, r3, g3, b3] => { ["b", r1, g1, b1, r2, g2, b2, r3, g3, b3] => {
let left = parse_color(r1, g1, b1)?; let left_color = parse_color(r1, g1, b1)?;
let right = parse_color(r2, g2, b2)?; let right_color = parse_color(r2, g2, b2)?;
let combined = parse_color(r3, g3, b3)?; let conjoined_color = parse_color(r3, g3, b3)?;
change_pattern(strip_tx, Box::new(pattern::Collide::new(left, right, combined))) change_pattern(strip_tx, Box::new(pattern::Collide::new(&pattern::CollideParams {left_color, right_color, conjoined_color})))
} }
["x"] => strip_tx ["x"] => strip_tx
.send(strip::Message::ClearLights) .send(strip::Message::ClearLights)
.map_err(|e| e.to_string()), .map_err(|e| e.to_string()),
["q"] => { ["q"] => {
strip_tx.send(strip::Message::Quit).map_err(|e| e.to_string())?; strip_tx.send(strip::Message::Quit).map_err(|e| e.to_string())?;
// TODO panic!("Quitting");
panic!("i");
}, },
["s", n] => strip_tx ["s", n] => strip_tx
.send(strip::Message::SetNumLights( .send(strip::Message::SetNumLights(
@ -124,15 +123,15 @@ fn parse_color(r: &str, g: &str, b: &str) -> Result<Rgb, String> {
fn change_pattern( fn change_pattern(
strip_tx: &Sender<strip::Message>, strip_tx: &Sender<strip::Message>,
pat: Box<dyn Pattern + Send>, pat: Box<dyn Pattern + Send + Sync>,
) -> Result<(), String> { ) -> Result<(), String> {
strip_tx strip_tx
.send(strip::Message::ChangePattern(pat)) .send(strip::Message::ChangePattern(pat))
.map_err(|e| e.to_string()) .map_err(|e| e.to_string())
} }
fn get_line(message_tx: &Sender<errors::Message>, prompt: &str) -> ProgramResult<String> { fn get_line(message_tx: &Sender<common::Message>, prompt: &str) -> ProgramResult<String> {
let _ = message_tx.send(errors::Message::InputPrompt(String::from(prompt))); let _drop = message_tx.send(common::Message::InputPrompt(String::from(prompt)));
std::io::stdout() std::io::stdout()
.flush() .flush()
.map_err(|_| ProgramError::UiError(String::from("Could not flush stdout")))?; .map_err(|_| ProgramError::UiError(String::from("Could not flush stdout")))?;

View File

@ -1,69 +0,0 @@
use crate::errors;
use crate::pattern;
use crate::strip;
use actix_web::{
error::{JsonPayloadError, UrlencodedError},
post, web,
web::JsonConfig,
App, HttpServer, Responder, Result,
};
use actix_web_static_files::ResourceFiles;
use std::io;
use std::sync::{mpsc::Sender, Arc, Mutex};
include!(concat!(env!("OUT_DIR"), "/generated.rs"));
struct AppState {
strip_tx: Arc<Mutex<Sender<strip::Message>>>,
}
#[post("/setcolor")]
async fn set_color_json(
data: web::Data<AppState>,
params: web::Json<pattern::Parameters>,
) -> Result<impl Responder> {
println!("Got params: {:?}", params);
data.strip_tx
.lock()
.map_err(|_| io::Error::new(io::ErrorKind::Other, "Failed to get a lock"))?
.send(strip::Message::ChangePattern(params.0.into_pattern()))
.map_err(|_| io::Error::new(io::ErrorKind::Other, "Failed to send to channel"))?;
Ok("Success")
}
#[actix_web::main]
pub async fn start(
message_tx: Sender<errors::Message>,
strip_tx: Sender<strip::Message>,
) -> std::io::Result<()> {
let _ = message_tx.send(errors::Message::String(String::from("Starting webui")));
HttpServer::new(move || {
let generated = generate();
App::new()
.data(AppState {
strip_tx: Arc::new(Mutex::new(strip_tx.clone())),
})
.service(
web::scope("/api")
.app_data(
JsonConfig::default().error_handler(|err: JsonPayloadError, _req| {
// let _ = message_tx.send(errors::Message::String(format!("JSON error: {:?}", err)));
println!("JSON error: {:?}", err);
err.into()
}),
)
.app_data(web::FormConfig::default().error_handler(
|err: UrlencodedError, _req| {
println!("{:?}", err);
err.into()
},
))
.service(set_color_json),
)
.service(ResourceFiles::new("/", generated))
})
.bind(("0.0.0.0", 8080))?
.run()
.await
}

962
web/package-lock.json generated
View File

@ -1,962 +0,0 @@
{
"name": "svelte-app",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@babel/code-frame": {
"version": "7.14.5",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz",
"integrity": "sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw==",
"dev": true,
"requires": {
"@babel/highlight": "^7.14.5"
}
},
"@babel/helper-validator-identifier": {
"version": "7.14.9",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.9.tgz",
"integrity": "sha512-pQYxPY0UP6IHISRitNe8bsijHex4TWZXi2HwKVsjPiltzlhse2znVcm9Ace510VT1kxIHjGJCZZQBX2gJDbo0g==",
"dev": true
},
"@babel/highlight": {
"version": "7.14.5",
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz",
"integrity": "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==",
"dev": true,
"requires": {
"@babel/helper-validator-identifier": "^7.14.5",
"chalk": "^2.0.0",
"js-tokens": "^4.0.0"
}
},
"@polka/url": {
"version": "1.0.0-next.15",
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.15.tgz",
"integrity": "sha512-15spi3V28QdevleWBNXE4pIls3nFZmBbUGrW9IVPwiQczuSb9n76TCB4bsk8TSel+I1OkHEdPhu5QKMfY6rQHA=="
},
"@rollup/plugin-commonjs": {
"version": "17.1.0",
"resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-17.1.0.tgz",
"integrity": "sha512-PoMdXCw0ZyvjpCMT5aV4nkL0QywxP29sODQsSGeDpr/oI49Qq9tRtAsb/LbYbDzFlOydVEqHmmZWFtXJEAX9ew==",
"dev": true,
"requires": {
"@rollup/pluginutils": "^3.1.0",
"commondir": "^1.0.1",
"estree-walker": "^2.0.1",
"glob": "^7.1.6",
"is-reference": "^1.2.1",
"magic-string": "^0.25.7",
"resolve": "^1.17.0"
}
},
"@rollup/plugin-node-resolve": {
"version": "11.2.1",
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-11.2.1.tgz",
"integrity": "sha512-yc2n43jcqVyGE2sqV5/YCmocy9ArjVAP/BeXyTtADTBBX6V0e5UMqwO8CdQ0kzjb6zu5P1qMzsScCMRvE9OlVg==",
"dev": true,
"requires": {
"@rollup/pluginutils": "^3.1.0",
"@types/resolve": "1.17.1",
"builtin-modules": "^3.1.0",
"deepmerge": "^4.2.2",
"is-module": "^1.0.0",
"resolve": "^1.19.0"
}
},
"@rollup/plugin-typescript": {
"version": "8.2.5",
"resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-8.2.5.tgz",
"integrity": "sha512-QL/LvDol/PAGB2O0S7/+q2HpSUNodpw7z6nGn9BfoVCPOZ0r4EALrojFU29Bkoi2Hr2jgTocTejJ5GGWZfOxbQ==",
"dev": true,
"requires": {
"@rollup/pluginutils": "^3.1.0",
"resolve": "^1.17.0"
}
},
"@rollup/pluginutils": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz",
"integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==",
"dev": true,
"requires": {
"@types/estree": "0.0.39",
"estree-walker": "^1.0.1",
"picomatch": "^2.2.2"
},
"dependencies": {
"estree-walker": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz",
"integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==",
"dev": true
}
}
},
"@tsconfig/svelte": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@tsconfig/svelte/-/svelte-2.0.1.tgz",
"integrity": "sha512-aqkICXbM1oX5FfgZd2qSSAGdyo/NRxjWCamxoyi3T8iVQnzGge19HhDYzZ6NrVOW7bhcWNSq9XexWFtMzbB24A==",
"dev": true
},
"@types/estree": {
"version": "0.0.39",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz",
"integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==",
"dev": true
},
"@types/node": {
"version": "16.4.13",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.4.13.tgz",
"integrity": "sha512-bLL69sKtd25w7p1nvg9pigE4gtKVpGTPojBFLMkGHXuUgap2sLqQt2qUnqmVCDfzGUL0DRNZP+1prIZJbMeAXg==",
"dev": true
},
"@types/pug": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.5.tgz",
"integrity": "sha512-LOnASQoeNZMkzexRuyqcBBDZ6rS+rQxUMkmj5A0PkhhiSZivLIuz6Hxyr1mkGoEZEkk66faROmpMi4fFkrKsBA==",
"dev": true
},
"@types/resolve": {
"version": "1.17.1",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz",
"integrity": "sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/sass": {
"version": "1.16.1",
"resolved": "https://registry.npmjs.org/@types/sass/-/sass-1.16.1.tgz",
"integrity": "sha512-iZUcRrGuz/Tbg3loODpW7vrQJkUtpY2fFSf4ELqqkApcS2TkZ1msk7ie8iZPB86lDOP8QOTTmuvWjc5S0R9OjQ==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"dev": true,
"requires": {
"color-convert": "^1.9.0"
}
},
"anymatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz",
"integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==",
"dev": true,
"requires": {
"normalize-path": "^3.0.0",
"picomatch": "^2.0.4"
}
},
"balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true
},
"binary-extensions": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
"dev": true
},
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"braces": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
"dev": true,
"requires": {
"fill-range": "^7.0.1"
}
},
"buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"dev": true
},
"builtin-modules": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.2.0.tgz",
"integrity": "sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA==",
"dev": true
},
"callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
"dev": true
},
"chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
"dev": true,
"requires": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
"supports-color": "^5.3.0"
}
},
"chokidar": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz",
"integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==",
"dev": true,
"requires": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
"fsevents": "~2.3.2",
"glob-parent": "~5.1.2",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
"normalize-path": "~3.0.0",
"readdirp": "~3.6.0"
}
},
"color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"dev": true,
"requires": {
"color-name": "1.1.3"
}
},
"color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
"dev": true
},
"commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"dev": true
},
"commondir": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
"integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=",
"dev": true
},
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
"dev": true
},
"console-clear": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/console-clear/-/console-clear-1.1.1.tgz",
"integrity": "sha512-pMD+MVR538ipqkG5JXeOEbKWS5um1H4LUUccUQG68qpeqBYbzYy79Gh55jkd2TtPdRfUaLWdv6LPP//5Zt0aPQ=="
},
"deepmerge": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz",
"integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==",
"dev": true
},
"detect-indent": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz",
"integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==",
"dev": true
},
"escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
"dev": true
},
"estree-walker": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"dev": true
},
"fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
"dev": true,
"requires": {
"to-regex-range": "^5.0.1"
}
},
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
"dev": true
},
"fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"optional": true
},
"function-bind": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
"dev": true
},
"get-port": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/get-port/-/get-port-3.2.0.tgz",
"integrity": "sha1-3Xzn3hh8Bsi/NTeWrHHgmfCYDrw="
},
"glob": {
"version": "7.1.7",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz",
"integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==",
"dev": true,
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
},
"glob-parent": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"requires": {
"is-glob": "^4.0.1"
}
},
"has": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
"dev": true,
"requires": {
"function-bind": "^1.1.1"
}
},
"has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
"dev": true
},
"import-fresh": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
"integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
"dev": true,
"requires": {
"parent-module": "^1.0.0",
"resolve-from": "^4.0.0"
}
},
"inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
"dev": true,
"requires": {
"once": "^1.3.0",
"wrappy": "1"
}
},
"inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true
},
"is-binary-path": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
"dev": true,
"requires": {
"binary-extensions": "^2.0.0"
}
},
"is-core-module": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.5.0.tgz",
"integrity": "sha512-TXCMSDsEHMEEZ6eCA8rwRDbLu55MRGmrctljsBX/2v1d9/GzqHOxW5c5oPSgrUt2vBFXebu9rGqckXGPWOlYpg==",
"dev": true,
"requires": {
"has": "^1.0.3"
}
},
"is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
"dev": true
},
"is-glob": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz",
"integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==",
"dev": true,
"requires": {
"is-extglob": "^2.1.1"
}
},
"is-module": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz",
"integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=",
"dev": true
},
"is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true
},
"is-reference": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz",
"integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==",
"dev": true,
"requires": {
"@types/estree": "*"
}
},
"jest-worker": {
"version": "26.6.2",
"resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz",
"integrity": "sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==",
"dev": true,
"requires": {
"@types/node": "*",
"merge-stream": "^2.0.0",
"supports-color": "^7.0.0"
},
"dependencies": {
"has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true
},
"supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"requires": {
"has-flag": "^4.0.0"
}
}
}
},
"js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true
},
"kleur": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
"integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="
},
"livereload": {
"version": "0.9.3",
"resolved": "https://registry.npmjs.org/livereload/-/livereload-0.9.3.tgz",
"integrity": "sha512-q7Z71n3i4X0R9xthAryBdNGVGAO2R5X+/xXpmKeuPMrteg+W2U8VusTKV3YiJbXZwKsOlFlHe+go6uSNjfxrZw==",
"dev": true,
"requires": {
"chokidar": "^3.5.0",
"livereload-js": "^3.3.1",
"opts": ">= 1.2.0",
"ws": "^7.4.3"
}
},
"livereload-js": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/livereload-js/-/livereload-js-3.3.2.tgz",
"integrity": "sha512-w677WnINxFkuixAoUEXOStewzLYGI76XVag+0JWMMEyjJQKs0ibWZMxkTlB96Lm3EjZ7IeOxVziBEbtxVQqQZA==",
"dev": true
},
"local-access": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/local-access/-/local-access-1.1.0.tgz",
"integrity": "sha512-XfegD5pyTAfb+GY6chk283Ox5z8WexG56OvM06RWLpAc/UHozO8X6xAxEkIitZOtsSMM1Yr3DkHgW5W+onLhCw=="
},
"magic-string": {
"version": "0.25.7",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz",
"integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==",
"dev": true,
"requires": {
"sourcemap-codec": "^1.4.4"
}
},
"merge-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
"integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
"dev": true
},
"mime": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz",
"integrity": "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg=="
},
"min-indent": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
"integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",
"dev": true
},
"minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"dev": true,
"requires": {
"brace-expansion": "^1.1.7"
}
},
"minimist": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
"dev": true
},
"mri": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/mri/-/mri-1.1.6.tgz",
"integrity": "sha512-oi1b3MfbyGa7FJMP9GmLTttni5JoICpYBRlq+x5V16fZbLsnL9N3wFqqIm/nIG43FjUFkFh9Epzp/kzUGUnJxQ=="
},
"normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
"dev": true
},
"once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"dev": true,
"requires": {
"wrappy": "1"
}
},
"opts": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/opts/-/opts-2.0.2.tgz",
"integrity": "sha512-k41FwbcLnlgnFh69f4qdUfvDQ+5vaSDnVPFI/y5XuhKRq97EnVVneO9F1ESVCdiVu4fCS2L8usX3mU331hB7pg==",
"dev": true
},
"parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
"integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
"dev": true,
"requires": {
"callsites": "^3.0.0"
}
},
"path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
"dev": true
},
"path-parse": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true
},
"picomatch": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz",
"integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==",
"dev": true
},
"randombytes": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
"integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
"dev": true,
"requires": {
"safe-buffer": "^5.1.0"
}
},
"readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"dev": true,
"requires": {
"picomatch": "^2.2.1"
}
},
"require-relative": {
"version": "0.8.7",
"resolved": "https://registry.npmjs.org/require-relative/-/require-relative-0.8.7.tgz",
"integrity": "sha1-eZlTn8ngR6N5KPoZb44VY9q9Nt4=",
"dev": true
},
"resolve": {
"version": "1.20.0",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz",
"integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==",
"dev": true,
"requires": {
"is-core-module": "^2.2.0",
"path-parse": "^1.0.6"
}
},
"resolve-from": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
"dev": true
},
"rollup": {
"version": "2.56.2",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.56.2.tgz",
"integrity": "sha512-s8H00ZsRi29M2/lGdm1u8DJpJ9ML8SUOpVVBd33XNeEeL3NVaTiUcSBHzBdF3eAyR0l7VSpsuoVUGrRHq7aPwQ==",
"dev": true,
"requires": {
"fsevents": "~2.3.2"
}
},
"rollup-plugin-css-only": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/rollup-plugin-css-only/-/rollup-plugin-css-only-3.1.0.tgz",
"integrity": "sha512-TYMOE5uoD76vpj+RTkQLzC9cQtbnJNktHPB507FzRWBVaofg7KhIqq1kGbcVOadARSozWF883Ho9KpSPKH8gqA==",
"dev": true,
"requires": {
"@rollup/pluginutils": "4"
},
"dependencies": {
"@rollup/pluginutils": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.1.1.tgz",
"integrity": "sha512-clDjivHqWGXi7u+0d2r2sBi4Ie6VLEAzWMIkvJLnDmxoOhBYOTfzGbOQBA32THHm11/LiJbd01tJUpJsbshSWQ==",
"dev": true,
"requires": {
"estree-walker": "^2.0.1",
"picomatch": "^2.2.2"
}
}
}
},
"rollup-plugin-livereload": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/rollup-plugin-livereload/-/rollup-plugin-livereload-2.0.5.tgz",
"integrity": "sha512-vqQZ/UQowTW7VoiKEM5ouNW90wE5/GZLfdWuR0ELxyKOJUIaj+uismPZZaICU4DnWPVjnpCDDxEqwU7pcKY/PA==",
"dev": true,
"requires": {
"livereload": "^0.9.1"
}
},
"rollup-plugin-svelte": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/rollup-plugin-svelte/-/rollup-plugin-svelte-7.1.0.tgz",
"integrity": "sha512-vopCUq3G+25sKjwF5VilIbiY6KCuMNHP1PFvx2Vr3REBNMDllKHFZN2B9jwwC+MqNc3UPKkjXnceLPEjTjXGXg==",
"dev": true,
"requires": {
"require-relative": "^0.8.7",
"rollup-pluginutils": "^2.8.2"
}
},
"rollup-plugin-terser": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz",
"integrity": "sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.10.4",
"jest-worker": "^26.2.1",
"serialize-javascript": "^4.0.0",
"terser": "^5.0.0"
}
},
"rollup-pluginutils": {
"version": "2.8.2",
"resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz",
"integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==",
"dev": true,
"requires": {
"estree-walker": "^0.6.1"
},
"dependencies": {
"estree-walker": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz",
"integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==",
"dev": true
}
}
},
"sade": {
"version": "1.7.4",
"resolved": "https://registry.npmjs.org/sade/-/sade-1.7.4.tgz",
"integrity": "sha512-y5yauMD93rX840MwUJr7C1ysLFBgMspsdTo4UVrDg3fXDvtwOyIqykhVAAm6fk/3au77773itJStObgK+LKaiA==",
"requires": {
"mri": "^1.1.0"
}
},
"safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"dev": true
},
"semiver": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/semiver/-/semiver-1.1.0.tgz",
"integrity": "sha512-QNI2ChmuioGC1/xjyYwyZYADILWyW6AmS1UH6gDj/SFUUUS4MBAWs/7mxnkRPc/F4iHezDP+O8t0dO8WHiEOdg=="
},
"serialize-javascript": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz",
"integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==",
"dev": true,
"requires": {
"randombytes": "^2.1.0"
}
},
"sirv": {
"version": "1.0.12",
"resolved": "https://registry.npmjs.org/sirv/-/sirv-1.0.12.tgz",
"integrity": "sha512-+jQoCxndz7L2tqQL4ZyzfDhky0W/4ZJip3XoOuxyQWnAwMxindLl3Xv1qT4x1YX/re0leShvTm8Uk0kQspGhBg==",
"requires": {
"@polka/url": "^1.0.0-next.15",
"mime": "^2.3.1",
"totalist": "^1.0.0"
}
},
"sirv-cli": {
"version": "1.0.12",
"resolved": "https://registry.npmjs.org/sirv-cli/-/sirv-cli-1.0.12.tgz",
"integrity": "sha512-Rs5PvF3a48zuLmrl8vcqVv9xF/WWPES19QawVkpdzqx7vD5SMZS07+ece1gK4umbslXN43YeIksYtQM5csgIzQ==",
"requires": {
"console-clear": "^1.1.0",
"get-port": "^3.2.0",
"kleur": "^3.0.0",
"local-access": "^1.0.1",
"sade": "^1.6.0",
"semiver": "^1.0.0",
"sirv": "^1.0.12",
"tinydate": "^1.0.0"
}
},
"source-map": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",
"integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==",
"dev": true
},
"source-map-support": {
"version": "0.5.19",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz",
"integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==",
"dev": true,
"requires": {
"buffer-from": "^1.0.0",
"source-map": "^0.6.0"
},
"dependencies": {
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true
}
}
},
"sourcemap-codec": {
"version": "1.4.8",
"resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
"integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==",
"dev": true
},
"strip-indent": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
"integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==",
"dev": true,
"requires": {
"min-indent": "^1.0.0"
}
},
"supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"dev": true,
"requires": {
"has-flag": "^3.0.0"
}
},
"svelte": {
"version": "3.42.1",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-3.42.1.tgz",
"integrity": "sha512-XtExLd2JAU3T7M2g/DkO3UNj/3n1WdTXrfL63OZ5nZq7nAqd9wQw+lR4Pv/wkVbrWbAIPfLDX47UjFdmnY+YtQ==",
"dev": true
},
"svelte-check": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-2.2.4.tgz",
"integrity": "sha512-eGEuZ3UEanOhlpQhICLjKejDxcZ9uYJlGnBGKAPW7uugolaBE6HpEBIiKFZN/TMRFFHQUURgGvsVn8/HJUBfeQ==",
"dev": true,
"requires": {
"chalk": "^4.0.0",
"chokidar": "^3.4.1",
"glob": "^7.1.6",
"import-fresh": "^3.2.1",
"minimist": "^1.2.5",
"sade": "^1.7.4",
"source-map": "^0.7.3",
"svelte-preprocess": "^4.0.0",
"typescript": "*"
},
"dependencies": {
"ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"requires": {
"color-convert": "^2.0.1"
}
},
"chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
}
},
"color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"requires": {
"color-name": "~1.1.4"
}
},
"color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
"has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true
},
"supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"requires": {
"has-flag": "^4.0.0"
}
}
}
},
"svelte-preprocess": {
"version": "4.7.4",
"resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-4.7.4.tgz",
"integrity": "sha512-mDAmaltQl6e5zU2VEtoWEf7eLTfuOTGr9zt+BpA3AGHo8MIhKiNSPE9OLTCTOMgj0vj/uL9QBbaNmpG4G1CgIA==",
"dev": true,
"requires": {
"@types/pug": "^2.0.4",
"@types/sass": "^1.16.0",
"detect-indent": "^6.0.0",
"strip-indent": "^3.0.0"
}
},
"terser": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.7.1.tgz",
"integrity": "sha512-b3e+d5JbHAe/JSjwsC3Zn55wsBIM7AsHLjKxT31kGCldgbpFePaFo+PiddtO6uwRZWRw7sPXmAN8dTW61xmnSg==",
"dev": true,
"requires": {
"commander": "^2.20.0",
"source-map": "~0.7.2",
"source-map-support": "~0.5.19"
}
},
"tinydate": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/tinydate/-/tinydate-1.3.0.tgz",
"integrity": "sha512-7cR8rLy2QhYHpsBDBVYnnWXm8uRTr38RoZakFSW7Bs7PzfMPNZthuMLkwqZv7MTu8lhQ91cOFYS5a7iFj2oR3w=="
},
"to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
"requires": {
"is-number": "^7.0.0"
}
},
"totalist": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/totalist/-/totalist-1.1.0.tgz",
"integrity": "sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g=="
},
"tslib": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
"dev": true
},
"typescript": {
"version": "4.3.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz",
"integrity": "sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==",
"dev": true
},
"wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
"dev": true
},
"ws": {
"version": "7.5.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.3.tgz",
"integrity": "sha512-kQ/dHIzuLrS6Je9+uv81ueZomEwH0qVYstcAQ4/Z93K8zeko9gtAbttJWzoC5ukqXY1PpoouV3+VSOqEAFt5wg==",
"dev": true
}
}
}

18
webui/Cargo.toml Normal file
View File

@ -0,0 +1,18 @@
[package]
name = "webui"
version = "0.1.0"
authors = ["Austen Adler <agadler@austenadler.com>"]
edition = "2018"
build = "build.rs"
[dependencies]
serde = {version = "1.0", features = ["derive"]}
actix = "0.10.0"
actix-web = {version = "3.3.2", default_features = false}
actix-web-actors = "3.0.0"
rust-embed="6.0.0"
serde_json = "1"
live-view = { path = "./liveview-rust/" }
askama = "0.10.5"
common = { path = "../common" }
tracing = "0.1.37"

26
webui/build.rs Normal file
View File

@ -0,0 +1,26 @@
use std::process::Command;
fn main() {
let profile = std::env::var("PROFILE").unwrap();
let success = Command::new("npm")
.current_dir("./liveview-rust/js")
.args(["run-script", "build"])
.env(
"NODE_ENV",
match profile.as_str() {
"release" => "production",
"debug" => "development",
r => panic!("Unknown release type: {}", r),
},
)
.spawn()
.unwrap()
.wait()
.unwrap()
.success();
if !success {
panic!("Npm build failed");
}
}

12
webui/liveview-rust/.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1,12 @@
# These are supported funding model platforms
github: [njaremko]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

5
webui/liveview-rust/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
/target
**/*.rs.bk
Cargo.lock
**/node_modules
/js/dist

View File

@ -0,0 +1,11 @@
# 0.0.7
- Update bundled JS (Probably breaking this out shortly)
# 0.0.6
- Add support for `keydown`, `mouseover`, `mouseout` events
# 0.0.5
- Update dependencies to modern versions

View File

@ -0,0 +1,18 @@
[package]
name = "live-view"
version = "0.0.8"
authors = ["Nathan Jaremko <nathan@jaremko.ca>"]
edition = "2018"
description = "A Live View implementation for Actix Web"
license = "MIT"
repository = "https://github.com/njaremko/liveview-rust"
[dependencies]
# actix-web = "4.0.0-beta.5"
actix-web = "3.3.2"
actix-web-actors = "3.0.0"
actix = "0.10.0"
serde = { version = "^1.0", features = ["derive"] }
serde_json = "^1.0"
askama = "^0.10"
hashbrown = { version = "^0.11", features = ["serde"] }

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 Nathan Jaremko
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,23 @@
# liveview-rust
PoC of LiveView in rust - "Never write javascript again"
[![Version](https://img.shields.io/crates/v/live-view.svg)](https://crates.io/crates/live-view)
[![Documentation](https://docs.rs/live-view/badge.svg)](https://docs.rs/live-view/)
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/njaremko/live-view/master/LICENSE)
This was inspired by the [Phoenix Live View](https://github.com/phoenixframework/phoenix_live_view) project.
An example of how to use this library can be found [here.](https://github.com/njaremko/liveview-rust-example)
We follow a similar model, with the only difference being that we send the full html on each render
and let the client calculate the diff, instead of sending only diffs to client and letting them apply the change.
What works?
- We click, text input, and submit events are send to server and template is re-rendered and sent to client, then morphdom
applies the change to the dom.
- Build on Actix-Web at the moment, potentially working with Rocket at some point as well.
Whats left?
- Testing framework
- HTML diffs on server side (not nessesary for PoC)
- Write some macros to make implementation nicer

View File

@ -0,0 +1,26 @@
use std::process::Command;
fn main() {
let profile = std::env::var("PROFILE").unwrap();
let success = Command::new("npm")
.current_dir("./js")
.args(["run-script", "build"])
.env(
"NODE_ENV",
match profile.as_str() {
"release" => "production",
"debug" => "development",
r => panic!("Unknown release type: {}", r),
},
)
.spawn()
.expect("Could not spawn npm")
.wait()
.unwrap()
.success();
if !success {
panic!("Npm build failed");
}
}

View File

@ -0,0 +1,182 @@
import morphdom from "morphdom";
const $ = require('jquery');
window.morphdom = morphdom;
window.$ = $;
let conn = null;
let content = document.getElementById('content');
function connect() {
disconnect();
let wsUri = (window.location.protocol === 'https:' && 'wss://' || 'ws://') + window.location.host + '/ws/';
conn = new WebSocket(wsUri);
console.log('Connecting...');
conn.onopen = function () {
console.log('Connected.');
// document.forms[0].submit();
};
conn.onmessage = function (e) {
let new_content = document.createElement('div');
new_content.setAttribute('id', 'content');
new_content.innerHTML = e.data;
morphdom(content, new_content, {
onBeforeElUpdated: function (fromEl, toEl) {
if (toEl.tagName === 'INPUT') {
toEl.value = fromEl.value;
}
},
});
// attach();
};
conn.onclose = function () {
console.log('Disconnected.');
conn = null;
};
}
function disconnect() {
if (conn != null) {
log('Disconnecting...');
conn.close();
conn = null;
}
}
function send_event(kind, event, data = null) {
let json = JSON.stringify({
"kind": kind,
"event": event,
"data": data,
});
console.log(json);
conn.send(json);
}
function hexToRgb(v) {
// Adapted from https://stackoverflow.com/a/5624139
var result = /^#?([a-f\d]{1,2})([a-f\d]{2})([a-f\d]{2})$/i.exec(v);
if(result) {
result.splice(0, 1);
v = result.map(r => parseInt(r, 16));
}
return v;
}
function getFormData(form) {
// let ret = {};
return Array.from(form.elements).reduce((acc, e) => {
var encodedValue;
switch(e.type) {
case "color":
encodedValue = hexToRgb(e.value);
break;
case "number":
encodedValue = parseInt(e.value);
break;
case "checkbox":
encodedValue = e.checked;
break;
default:
break;
}
let multiName = e.getAttribute("rust-form-multi");
// This is a multivalue
if (multiName) {
if (acc[multiName]) {
// Push to existing array
acc[multiName].push(encodedValue);
} else {
// This is the first element in the multi-array
acc[multiName] = [encodedValue];
}
} else {
// This is a regular value
acc[e.name] = encodedValue;
}
return acc;
}, {});
// return ret;
}
const CLICK_EVENT = 'click';
const SUBMIT_EVENT = 'submit';
const INPUT_EVENT = 'input';
const KEYDOWN_EVENT = 'keydown';
const MOUSEOVER_EVENT = 'mouseover';
const MOUSEOUT_EVENT = 'mouseout';
function attach() {
let clickElems = document.querySelectorAll('[rust-click]');
for (let i = 0; i < clickElems.length; i++) {
clickElems[i].addEventListener(CLICK_EVENT, function (e) {
e.preventDefault();
let val = clickElems[i].getAttribute('rust-click');
send_event(CLICK_EVENT, val);
});
}
let submitElems = document.querySelectorAll('[rust-submit]');
for (let i = 0; i < submitElems.length; i++) {
submitElems[i].addEventListener(SUBMIT_EVENT, function (e) {
console.log("Preventing");
e.preventDefault();
// Form serialization
// let form = $(this).serialize();
// JSON serialization
let data = {};
data[document.getElementById("template-name").value] = getFormData(this);
let form = JSON.stringify(data);
console.log("serialized form", form);
let event = submitElems[i].getAttribute('rust-submit');
send_event(SUBMIT_EVENT, event, form);
});
}
let inputElems = document.querySelectorAll('[rust-input]');
for (let i = 0; i < inputElems.length; i++) {
inputElems[i].addEventListener(INPUT_EVENT, function (e) {
let event = inputElems[i].getAttribute('rust-input');
let val = $(this).val();
send_event(INPUT_EVENT, event, val);
});
}
let keydownElems = document.querySelectorAll('[rust-keydown]');
for (let i = 0; i < keydownElems.length; i++) {
keydownElems[i].addEventListener(KEYDOWN_EVENT, function (e) {
let event = keydownElems[i].getAttribute('rust-keydown');
let val = $(this).val();
send_event(KEYDOWN_EVENT, event, val);
});
}
let mouseoverElems = document.querySelectorAll('[rust-mouseover]');
for (let i = 0; i < mouseoverElems.length; i++) {
mouseoverElems[i].addEventListener(MOUSEOVER_EVENT, function (e) {
let event = mouseoverElems[i].getAttribute('rust-mouseover');
let val = $(this).val();
send_event(MOUSEOVER_EVENT, event, val);
});
}
let mouseoutElems = document.querySelectorAll('[rust-mouseout]');
for (let i = 0; i < mouseoutElems.length; i++) {
mouseoutElems[i].addEventListener(MOUSEOUT_EVENT, function (e) {
let event = mouseoutElems[i].getAttribute('rust-mouseout');
let val = $(this).val();
send_event(MOUSEOUT_EVENT, event, val);
});
}
}
connect();
attach();

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,24 @@
/*!
* Sizzle CSS Selector Engine v2.3.6
* https://sizzlejs.com/
*
* Copyright JS Foundation and other contributors
* Released under the MIT license
* https://js.foundation/
*
* Date: 2021-02-16
*/
/*!
* jQuery JavaScript Library v3.6.0
* https://jquery.com/
*
* Includes Sizzle.js
* https://sizzlejs.com/
*
* Copyright OpenJS Foundation and other contributors
* Released under the MIT license
* https://jquery.org/license
*
* Date: 2021-03-02T17:08Z
*/

2312
webui/liveview-rust/js/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,29 @@
{
"name": "liveview-rust",
"version": "0.0.8",
"description": "PoC of LiveView in rust - \"Never write javascript again\"",
"private": false,
"main": "webpack.config.js",
"scripts": {
"build": "webpack",
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/njaremko/liveview-rust.git"
},
"author": "Nathan Jaremko",
"license": "MIT",
"bugs": {
"url": "https://github.com/njaremko/liveview-rust/issues"
},
"homepage": "https://github.com/njaremko/liveview-rust#readme",
"dependencies": {
"jquery": "^3.5.0",
"morphdom": "git+https://github.com/austenadler/morphdom#fix/input-value-type-change"
},
"devDependencies": {
"webpack": "^5.28.0",
"webpack-cli": "^4.6.0"
}
}

View File

@ -0,0 +1,11 @@
const path = require('path');
module.exports = {
mode: 'production',
entry: './liveview-dev.js',
output: {
filename: './liveview.js',
// path: path.resolve(__dirname, 'static/js')
},
devtool: 'eval-source-map',
};

View File

@ -0,0 +1,800 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@discoveryjs/json-ext@^0.5.0":
"integrity" "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw=="
"resolved" "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz"
"version" "0.5.7"
"@jridgewell/gen-mapping@^0.3.0":
"integrity" "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A=="
"resolved" "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz"
"version" "0.3.2"
dependencies:
"@jridgewell/set-array" "^1.0.1"
"@jridgewell/sourcemap-codec" "^1.4.10"
"@jridgewell/trace-mapping" "^0.3.9"
"@jridgewell/resolve-uri@^3.0.3":
"integrity" "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w=="
"resolved" "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz"
"version" "3.1.0"
"@jridgewell/set-array@^1.0.1":
"integrity" "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw=="
"resolved" "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz"
"version" "1.1.2"
"@jridgewell/source-map@^0.3.2":
"integrity" "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw=="
"resolved" "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz"
"version" "0.3.2"
dependencies:
"@jridgewell/gen-mapping" "^0.3.0"
"@jridgewell/trace-mapping" "^0.3.9"
"@jridgewell/sourcemap-codec@^1.4.10":
"integrity" "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw=="
"resolved" "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz"
"version" "1.4.14"
"@jridgewell/trace-mapping@^0.3.7", "@jridgewell/trace-mapping@^0.3.9":
"integrity" "sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ=="
"resolved" "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz"
"version" "0.3.14"
dependencies:
"@jridgewell/resolve-uri" "^3.0.3"
"@jridgewell/sourcemap-codec" "^1.4.10"
"@types/eslint-scope@^3.7.3":
"integrity" "sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA=="
"resolved" "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.4.tgz"
"version" "3.7.4"
dependencies:
"@types/eslint" "*"
"@types/estree" "*"
"@types/eslint@*":
"integrity" "sha512-dhsC09y1gpJWnK+Ff4SGvCuSnk9DaU0BJZSzOwa6GVSg65XtTugLBITDAAzRU5duGBoXBHpdR/9jHGxJjNflJQ=="
"resolved" "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.5.tgz"
"version" "8.4.5"
dependencies:
"@types/estree" "*"
"@types/json-schema" "*"
"@types/estree@*", "@types/estree@^0.0.51":
"integrity" "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ=="
"resolved" "https://registry.npmjs.org/@types/estree/-/estree-0.0.51.tgz"
"version" "0.0.51"
"@types/json-schema@*", "@types/json-schema@^7.0.8":
"integrity" "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ=="
"resolved" "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz"
"version" "7.0.11"
"@types/node@*":
"integrity" "sha512-z+2vB6yDt1fNwKOeGbckpmirO+VBDuQqecXkgeIqDlaOtmKn6hPR/viQ8cxCfqLU4fTlvM3+YjM367TukWdxpg=="
"resolved" "https://registry.npmjs.org/@types/node/-/node-18.6.1.tgz"
"version" "18.6.1"
"@webassemblyjs/ast@1.11.1":
"integrity" "sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw=="
"resolved" "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz"
"version" "1.11.1"
dependencies:
"@webassemblyjs/helper-numbers" "1.11.1"
"@webassemblyjs/helper-wasm-bytecode" "1.11.1"
"@webassemblyjs/floating-point-hex-parser@1.11.1":
"integrity" "sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ=="
"resolved" "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz"
"version" "1.11.1"
"@webassemblyjs/helper-api-error@1.11.1":
"integrity" "sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg=="
"resolved" "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz"
"version" "1.11.1"
"@webassemblyjs/helper-buffer@1.11.1":
"integrity" "sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA=="
"resolved" "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz"
"version" "1.11.1"
"@webassemblyjs/helper-numbers@1.11.1":
"integrity" "sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ=="
"resolved" "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz"
"version" "1.11.1"
dependencies:
"@webassemblyjs/floating-point-hex-parser" "1.11.1"
"@webassemblyjs/helper-api-error" "1.11.1"
"@xtuc/long" "4.2.2"
"@webassemblyjs/helper-wasm-bytecode@1.11.1":
"integrity" "sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q=="
"resolved" "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz"
"version" "1.11.1"
"@webassemblyjs/helper-wasm-section@1.11.1":
"integrity" "sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg=="
"resolved" "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz"
"version" "1.11.1"
dependencies:
"@webassemblyjs/ast" "1.11.1"
"@webassemblyjs/helper-buffer" "1.11.1"
"@webassemblyjs/helper-wasm-bytecode" "1.11.1"
"@webassemblyjs/wasm-gen" "1.11.1"
"@webassemblyjs/ieee754@1.11.1":
"integrity" "sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ=="
"resolved" "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz"
"version" "1.11.1"
dependencies:
"@xtuc/ieee754" "^1.2.0"
"@webassemblyjs/leb128@1.11.1":
"integrity" "sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw=="
"resolved" "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.1.tgz"
"version" "1.11.1"
dependencies:
"@xtuc/long" "4.2.2"
"@webassemblyjs/utf8@1.11.1":
"integrity" "sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ=="
"resolved" "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.1.tgz"
"version" "1.11.1"
"@webassemblyjs/wasm-edit@1.11.1":
"integrity" "sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA=="
"resolved" "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz"
"version" "1.11.1"
dependencies:
"@webassemblyjs/ast" "1.11.1"
"@webassemblyjs/helper-buffer" "1.11.1"
"@webassemblyjs/helper-wasm-bytecode" "1.11.1"
"@webassemblyjs/helper-wasm-section" "1.11.1"
"@webassemblyjs/wasm-gen" "1.11.1"
"@webassemblyjs/wasm-opt" "1.11.1"
"@webassemblyjs/wasm-parser" "1.11.1"
"@webassemblyjs/wast-printer" "1.11.1"
"@webassemblyjs/wasm-gen@1.11.1":
"integrity" "sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA=="
"resolved" "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz"
"version" "1.11.1"
dependencies:
"@webassemblyjs/ast" "1.11.1"
"@webassemblyjs/helper-wasm-bytecode" "1.11.1"
"@webassemblyjs/ieee754" "1.11.1"
"@webassemblyjs/leb128" "1.11.1"
"@webassemblyjs/utf8" "1.11.1"
"@webassemblyjs/wasm-opt@1.11.1":
"integrity" "sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw=="
"resolved" "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz"
"version" "1.11.1"
dependencies:
"@webassemblyjs/ast" "1.11.1"
"@webassemblyjs/helper-buffer" "1.11.1"
"@webassemblyjs/wasm-gen" "1.11.1"
"@webassemblyjs/wasm-parser" "1.11.1"
"@webassemblyjs/wasm-parser@1.11.1":
"integrity" "sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA=="
"resolved" "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz"
"version" "1.11.1"
dependencies:
"@webassemblyjs/ast" "1.11.1"
"@webassemblyjs/helper-api-error" "1.11.1"
"@webassemblyjs/helper-wasm-bytecode" "1.11.1"
"@webassemblyjs/ieee754" "1.11.1"
"@webassemblyjs/leb128" "1.11.1"
"@webassemblyjs/utf8" "1.11.1"
"@webassemblyjs/wast-printer@1.11.1":
"integrity" "sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg=="
"resolved" "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz"
"version" "1.11.1"
dependencies:
"@webassemblyjs/ast" "1.11.1"
"@xtuc/long" "4.2.2"
"@webpack-cli/configtest@^1.2.0":
"integrity" "sha512-4FB8Tj6xyVkyqjj1OaTqCjXYULB9FMkqQ8yGrZjRDrYh0nOE+7Lhs45WioWQQMV+ceFlE368Ukhe6xdvJM9Egg=="
"resolved" "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-1.2.0.tgz"
"version" "1.2.0"
"@webpack-cli/info@^1.5.0":
"integrity" "sha512-e8tSXZpw2hPl2uMJY6fsMswaok5FdlGNRTktvFk2sD8RjH0hE2+XistawJx1vmKteh4NmGmNUrp+Tb2w+udPcQ=="
"resolved" "https://registry.npmjs.org/@webpack-cli/info/-/info-1.5.0.tgz"
"version" "1.5.0"
dependencies:
"envinfo" "^7.7.3"
"@webpack-cli/serve@^1.7.0":
"integrity" "sha512-oxnCNGj88fL+xzV+dacXs44HcDwf1ovs3AuEzvP7mqXw7fQntqIhQ1BRmynh4qEKQSSSRSWVyXRjmTbZIX9V2Q=="
"resolved" "https://registry.npmjs.org/@webpack-cli/serve/-/serve-1.7.0.tgz"
"version" "1.7.0"
"@xtuc/ieee754@^1.2.0":
"integrity" "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA=="
"resolved" "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz"
"version" "1.2.0"
"@xtuc/long@4.2.2":
"integrity" "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ=="
"resolved" "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz"
"version" "4.2.2"
"acorn-import-assertions@^1.7.6":
"integrity" "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw=="
"resolved" "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz"
"version" "1.8.0"
"acorn@^8", "acorn@^8.5.0", "acorn@^8.7.1":
"integrity" "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w=="
"resolved" "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz"
"version" "8.8.0"
"ajv-keywords@^3.5.2":
"integrity" "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ=="
"resolved" "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz"
"version" "3.5.2"
"ajv@^6.12.5", "ajv@^6.9.1":
"integrity" "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="
"resolved" "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz"
"version" "6.12.6"
dependencies:
"fast-deep-equal" "^3.1.1"
"fast-json-stable-stringify" "^2.0.0"
"json-schema-traverse" "^0.4.1"
"uri-js" "^4.2.2"
"browserslist@^4.14.5", "browserslist@>= 4.21.0":
"integrity" "sha512-MonuOgAtUB46uP5CezYbRaYKBNt2LxP0yX+Pmj4LkcDFGkn9Cbpi83d9sCjwQDErXsIJSzY5oKGDbgOlF/LPAA=="
"resolved" "https://registry.npmjs.org/browserslist/-/browserslist-4.21.2.tgz"
"version" "4.21.2"
dependencies:
"caniuse-lite" "^1.0.30001366"
"electron-to-chromium" "^1.4.188"
"node-releases" "^2.0.6"
"update-browserslist-db" "^1.0.4"
"buffer-from@^1.0.0":
"integrity" "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="
"resolved" "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz"
"version" "1.1.2"
"caniuse-lite@^1.0.30001366":
"integrity" "sha512-3PDmaP56wz/qz7G508xzjx8C+MC2qEm4SYhSEzC9IBROo+dGXFWRuaXkWti0A9tuI00g+toiriVqxtWMgl350g=="
"resolved" "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001370.tgz"
"version" "1.0.30001370"
"chrome-trace-event@^1.0.2":
"integrity" "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg=="
"resolved" "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz"
"version" "1.0.3"
"clone-deep@^4.0.1":
"integrity" "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ=="
"resolved" "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz"
"version" "4.0.1"
dependencies:
"is-plain-object" "^2.0.4"
"kind-of" "^6.0.2"
"shallow-clone" "^3.0.0"
"colorette@^2.0.14":
"integrity" "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ=="
"resolved" "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz"
"version" "2.0.19"
"commander@^2.20.0":
"integrity" "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
"resolved" "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz"
"version" "2.20.3"
"commander@^7.0.0":
"integrity" "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="
"resolved" "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz"
"version" "7.2.0"
"cross-spawn@^7.0.3":
"integrity" "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w=="
"resolved" "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz"
"version" "7.0.3"
dependencies:
"path-key" "^3.1.0"
"shebang-command" "^2.0.0"
"which" "^2.0.1"
"electron-to-chromium@^1.4.188":
"integrity" "sha512-WIGME0Cs7oob3mxsJwHbeWkH0tYkIE/sjkJ8ML2BYmuRcjhRl/q5kVDXG7W9LOOKwzPU5M0LBlXRq9rlSgnNlg=="
"resolved" "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.199.tgz"
"version" "1.4.199"
"enhanced-resolve@^5.10.0":
"integrity" "sha512-T0yTFjdpldGY8PmuXXR0PyQ1ufZpEGiHVrp7zHKB7jdR4qlmZHhONVM5AQOAWXuF/w3dnHbEQVrNptJgt7F+cQ=="
"resolved" "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.10.0.tgz"
"version" "5.10.0"
dependencies:
"graceful-fs" "^4.2.4"
"tapable" "^2.2.0"
"envinfo@^7.7.3":
"integrity" "sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw=="
"resolved" "https://registry.npmjs.org/envinfo/-/envinfo-7.8.1.tgz"
"version" "7.8.1"
"es-module-lexer@^0.9.0":
"integrity" "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ=="
"resolved" "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz"
"version" "0.9.3"
"escalade@^3.1.1":
"integrity" "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw=="
"resolved" "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz"
"version" "3.1.1"
"eslint-scope@5.1.1":
"integrity" "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw=="
"resolved" "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz"
"version" "5.1.1"
dependencies:
"esrecurse" "^4.3.0"
"estraverse" "^4.1.1"
"esrecurse@^4.3.0":
"integrity" "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="
"resolved" "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz"
"version" "4.3.0"
dependencies:
"estraverse" "^5.2.0"
"estraverse@^4.1.1":
"integrity" "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw=="
"resolved" "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz"
"version" "4.3.0"
"estraverse@^5.2.0":
"integrity" "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="
"resolved" "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz"
"version" "5.3.0"
"events@^3.2.0":
"integrity" "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="
"resolved" "https://registry.npmjs.org/events/-/events-3.3.0.tgz"
"version" "3.3.0"
"fast-deep-equal@^3.1.1":
"integrity" "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
"resolved" "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz"
"version" "3.1.3"
"fast-json-stable-stringify@^2.0.0":
"integrity" "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="
"resolved" "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz"
"version" "2.1.0"
"fastest-levenshtein@^1.0.12":
"integrity" "sha512-tFfWHjnuUfKE186Tfgr+jtaFc0mZTApEgKDOeyN+FwOqRkO/zK/3h1AiRd8u8CY53owL3CUmGr/oI9p/RdyLTA=="
"resolved" "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.14.tgz"
"version" "1.0.14"
"find-up@^4.0.0":
"integrity" "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="
"resolved" "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz"
"version" "4.1.0"
dependencies:
"locate-path" "^5.0.0"
"path-exists" "^4.0.0"
"function-bind@^1.1.1":
"integrity" "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
"resolved" "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz"
"version" "1.1.1"
"glob-to-regexp@^0.4.1":
"integrity" "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="
"resolved" "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz"
"version" "0.4.1"
"graceful-fs@^4.1.2", "graceful-fs@^4.2.4", "graceful-fs@^4.2.9":
"integrity" "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA=="
"resolved" "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz"
"version" "4.2.10"
"has-flag@^4.0.0":
"integrity" "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
"resolved" "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz"
"version" "4.0.0"
"has@^1.0.3":
"integrity" "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw=="
"resolved" "https://registry.npmjs.org/has/-/has-1.0.3.tgz"
"version" "1.0.3"
dependencies:
"function-bind" "^1.1.1"
"import-local@^3.0.2":
"integrity" "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg=="
"resolved" "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz"
"version" "3.1.0"
dependencies:
"pkg-dir" "^4.2.0"
"resolve-cwd" "^3.0.0"
"interpret@^2.2.0":
"integrity" "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw=="
"resolved" "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz"
"version" "2.2.0"
"is-core-module@^2.9.0":
"integrity" "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A=="
"resolved" "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz"
"version" "2.9.0"
dependencies:
"has" "^1.0.3"
"is-plain-object@^2.0.4":
"integrity" "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og=="
"resolved" "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz"
"version" "2.0.4"
dependencies:
"isobject" "^3.0.1"
"isexe@^2.0.0":
"integrity" "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
"resolved" "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz"
"version" "2.0.0"
"isobject@^3.0.1":
"integrity" "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg=="
"resolved" "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz"
"version" "3.0.1"
"jest-worker@^27.4.5":
"integrity" "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg=="
"resolved" "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz"
"version" "27.5.1"
dependencies:
"@types/node" "*"
"merge-stream" "^2.0.0"
"supports-color" "^8.0.0"
"jquery@^3.5.0":
"integrity" "sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw=="
"resolved" "https://registry.npmjs.org/jquery/-/jquery-3.6.0.tgz"
"version" "3.6.0"
"json-parse-even-better-errors@^2.3.1":
"integrity" "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="
"resolved" "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz"
"version" "2.3.1"
"json-schema-traverse@^0.4.1":
"integrity" "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
"resolved" "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz"
"version" "0.4.1"
"kind-of@^6.0.2":
"integrity" "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="
"resolved" "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz"
"version" "6.0.3"
"loader-runner@^4.2.0":
"integrity" "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg=="
"resolved" "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz"
"version" "4.3.0"
"locate-path@^5.0.0":
"integrity" "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="
"resolved" "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz"
"version" "5.0.0"
dependencies:
"p-locate" "^4.1.0"
"merge-stream@^2.0.0":
"integrity" "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="
"resolved" "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz"
"version" "2.0.0"
"mime-db@1.52.0":
"integrity" "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="
"resolved" "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz"
"version" "1.52.0"
"mime-types@^2.1.27":
"integrity" "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="
"resolved" "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz"
"version" "2.1.35"
dependencies:
"mime-db" "1.52.0"
"morphdom@git+https://github.com/austenadler/morphdom#fix/input-value-type-change":
"resolved" "git+ssh://git@github.com/austenadler/morphdom.git#f314930c694b45e980cbb68c29916efa5e1340e8"
"version" "2.6.1"
"neo-async@^2.6.2":
"integrity" "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="
"resolved" "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz"
"version" "2.6.2"
"node-releases@^2.0.6":
"integrity" "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg=="
"resolved" "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz"
"version" "2.0.6"
"p-limit@^2.2.0":
"integrity" "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="
"resolved" "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz"
"version" "2.3.0"
dependencies:
"p-try" "^2.0.0"
"p-locate@^4.1.0":
"integrity" "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="
"resolved" "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz"
"version" "4.1.0"
dependencies:
"p-limit" "^2.2.0"
"p-try@^2.0.0":
"integrity" "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="
"resolved" "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz"
"version" "2.2.0"
"path-exists@^4.0.0":
"integrity" "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="
"resolved" "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz"
"version" "4.0.0"
"path-key@^3.1.0":
"integrity" "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="
"resolved" "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz"
"version" "3.1.1"
"path-parse@^1.0.7":
"integrity" "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
"resolved" "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz"
"version" "1.0.7"
"picocolors@^1.0.0":
"integrity" "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="
"resolved" "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz"
"version" "1.0.0"
"pkg-dir@^4.2.0":
"integrity" "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ=="
"resolved" "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz"
"version" "4.2.0"
dependencies:
"find-up" "^4.0.0"
"punycode@^2.1.0":
"integrity" "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
"resolved" "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz"
"version" "2.1.1"
"randombytes@^2.1.0":
"integrity" "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ=="
"resolved" "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz"
"version" "2.1.0"
dependencies:
"safe-buffer" "^5.1.0"
"rechoir@^0.7.0":
"integrity" "sha512-/njmZ8s1wVeR6pjTZ+0nCnv8SpZNRMT2D1RLOJQESlYFDBvwpTA4KWJpZ+sBJ4+vhjILRcK7JIFdGCdxEAAitg=="
"resolved" "https://registry.npmjs.org/rechoir/-/rechoir-0.7.1.tgz"
"version" "0.7.1"
dependencies:
"resolve" "^1.9.0"
"resolve-cwd@^3.0.0":
"integrity" "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg=="
"resolved" "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz"
"version" "3.0.0"
dependencies:
"resolve-from" "^5.0.0"
"resolve-from@^5.0.0":
"integrity" "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="
"resolved" "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz"
"version" "5.0.0"
"resolve@^1.9.0":
"integrity" "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw=="
"resolved" "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz"
"version" "1.22.1"
dependencies:
"is-core-module" "^2.9.0"
"path-parse" "^1.0.7"
"supports-preserve-symlinks-flag" "^1.0.0"
"safe-buffer@^5.1.0":
"integrity" "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
"resolved" "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz"
"version" "5.2.1"
"schema-utils@^3.1.0", "schema-utils@^3.1.1":
"integrity" "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw=="
"resolved" "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz"
"version" "3.1.1"
dependencies:
"@types/json-schema" "^7.0.8"
"ajv" "^6.12.5"
"ajv-keywords" "^3.5.2"
"serialize-javascript@^6.0.0":
"integrity" "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag=="
"resolved" "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz"
"version" "6.0.0"
dependencies:
"randombytes" "^2.1.0"
"shallow-clone@^3.0.0":
"integrity" "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA=="
"resolved" "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz"
"version" "3.0.1"
dependencies:
"kind-of" "^6.0.2"
"shebang-command@^2.0.0":
"integrity" "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="
"resolved" "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz"
"version" "2.0.0"
dependencies:
"shebang-regex" "^3.0.0"
"shebang-regex@^3.0.0":
"integrity" "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="
"resolved" "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz"
"version" "3.0.0"
"source-map-support@~0.5.20":
"integrity" "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="
"resolved" "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz"
"version" "0.5.21"
dependencies:
"buffer-from" "^1.0.0"
"source-map" "^0.6.0"
"source-map@^0.6.0":
"integrity" "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
"resolved" "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz"
"version" "0.6.1"
"supports-color@^8.0.0":
"integrity" "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="
"resolved" "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz"
"version" "8.1.1"
dependencies:
"has-flag" "^4.0.0"
"supports-preserve-symlinks-flag@^1.0.0":
"integrity" "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="
"resolved" "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz"
"version" "1.0.0"
"tapable@^2.1.1", "tapable@^2.2.0":
"integrity" "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ=="
"resolved" "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz"
"version" "2.2.1"
"terser-webpack-plugin@^5.1.3":
"integrity" "sha512-Fx60G5HNYknNTNQnzQ1VePRuu89ZVYWfjRAeT5rITuCY/1b08s49e5kSQwHDirKZWuoKOBRFS98EUUoZ9kLEwQ=="
"resolved" "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.3.tgz"
"version" "5.3.3"
dependencies:
"@jridgewell/trace-mapping" "^0.3.7"
"jest-worker" "^27.4.5"
"schema-utils" "^3.1.1"
"serialize-javascript" "^6.0.0"
"terser" "^5.7.2"
"terser@^5.7.2":
"integrity" "sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA=="
"resolved" "https://registry.npmjs.org/terser/-/terser-5.14.2.tgz"
"version" "5.14.2"
dependencies:
"@jridgewell/source-map" "^0.3.2"
"acorn" "^8.5.0"
"commander" "^2.20.0"
"source-map-support" "~0.5.20"
"update-browserslist-db@^1.0.4":
"integrity" "sha512-dteFFpCyvuDdr9S/ff1ISkKt/9YZxKjI9WlRR99c180GaztJtRa/fn18FdxGVKVsnPY7/a/FDN68mcvUmP4U7Q=="
"resolved" "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.5.tgz"
"version" "1.0.5"
dependencies:
"escalade" "^3.1.1"
"picocolors" "^1.0.0"
"uri-js@^4.2.2":
"integrity" "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="
"resolved" "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz"
"version" "4.4.1"
dependencies:
"punycode" "^2.1.0"
"watchpack@^2.4.0":
"integrity" "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg=="
"resolved" "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz"
"version" "2.4.0"
dependencies:
"glob-to-regexp" "^0.4.1"
"graceful-fs" "^4.1.2"
"webpack-cli@^4.6.0", "webpack-cli@4.x.x":
"integrity" "sha512-NLhDfH/h4O6UOy+0LSso42xvYypClINuMNBVVzX4vX98TmTaTUxwRbXdhucbFMd2qLaCTcLq/PdYrvi8onw90w=="
"resolved" "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.10.0.tgz"
"version" "4.10.0"
dependencies:
"@discoveryjs/json-ext" "^0.5.0"
"@webpack-cli/configtest" "^1.2.0"
"@webpack-cli/info" "^1.5.0"
"@webpack-cli/serve" "^1.7.0"
"colorette" "^2.0.14"
"commander" "^7.0.0"
"cross-spawn" "^7.0.3"
"fastest-levenshtein" "^1.0.12"
"import-local" "^3.0.2"
"interpret" "^2.2.0"
"rechoir" "^0.7.0"
"webpack-merge" "^5.7.3"
"webpack-merge@^5.7.3":
"integrity" "sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q=="
"resolved" "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.8.0.tgz"
"version" "5.8.0"
dependencies:
"clone-deep" "^4.0.1"
"wildcard" "^2.0.0"
"webpack-sources@^3.2.3":
"integrity" "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w=="
"resolved" "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz"
"version" "3.2.3"
"webpack@^5.1.0", "webpack@^5.28.0", "webpack@4.x.x || 5.x.x":
"integrity" "sha512-A2InDwnhhGN4LYctJj6M1JEaGL7Luj6LOmyBHjcI8529cm5p6VXiTIW2sn6ffvEAKmveLzvu4jrihwXtPojlAA=="
"resolved" "https://registry.npmjs.org/webpack/-/webpack-5.74.0.tgz"
"version" "5.74.0"
dependencies:
"@types/eslint-scope" "^3.7.3"
"@types/estree" "^0.0.51"
"@webassemblyjs/ast" "1.11.1"
"@webassemblyjs/wasm-edit" "1.11.1"
"@webassemblyjs/wasm-parser" "1.11.1"
"acorn" "^8.7.1"
"acorn-import-assertions" "^1.7.6"
"browserslist" "^4.14.5"
"chrome-trace-event" "^1.0.2"
"enhanced-resolve" "^5.10.0"
"es-module-lexer" "^0.9.0"
"eslint-scope" "5.1.1"
"events" "^3.2.0"
"glob-to-regexp" "^0.4.1"
"graceful-fs" "^4.2.9"
"json-parse-even-better-errors" "^2.3.1"
"loader-runner" "^4.2.0"
"mime-types" "^2.1.27"
"neo-async" "^2.6.2"
"schema-utils" "^3.1.0"
"tapable" "^2.1.1"
"terser-webpack-plugin" "^5.1.3"
"watchpack" "^2.4.0"
"webpack-sources" "^3.2.3"
"which@^2.0.1":
"integrity" "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="
"resolved" "https://registry.npmjs.org/which/-/which-2.0.2.tgz"
"version" "2.0.2"
dependencies:
"isexe" "^2.0.0"
"wildcard@^2.0.0":
"integrity" "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw=="
"resolved" "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz"
"version" "2.0.0"

View File

@ -0,0 +1,18 @@
#[macro_use]
extern crate serde;
mod live_view;
mod socket;
pub use crate::live_view::*;
pub use socket::*;
pub type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
}

View File

@ -0,0 +1,44 @@
use crate::{socket::Event, Result};
use hashbrown::HashMap;
pub trait Template: Sized + 'static + Clone + Unpin {
fn render(&self) -> Result<String>;
}
pub type EventHandler<State> = fn(&Event, &mut State) -> Option<String>;
#[derive(Default)]
pub struct LiveView<State: Template> {
pub(crate) click: HashMap<String, EventHandler<State>>,
pub(crate) submit: HashMap<String, EventHandler<State>>,
pub(crate) input: HashMap<String, EventHandler<State>>,
pub(crate) keydown: HashMap<String, EventHandler<State>>,
pub(crate) mouseover: HashMap<String, EventHandler<State>>,
pub(crate) mouseout: HashMap<String, EventHandler<State>>,
}
impl<State: Template> LiveView<State> {
pub fn on_click(&mut self, event: &str, func: EventHandler<State>) {
self.click.insert(event.into(), func);
}
pub fn on_submit(&mut self, event: &str, func: EventHandler<State>) {
self.submit.insert(event.into(), func);
}
pub fn on_input(&mut self, event: &str, func: EventHandler<State>) {
self.input.insert(event.into(), func);
}
pub fn on_keydown(&mut self, event: &str, func: EventHandler<State>) {
self.keydown.insert(event.into(), func);
}
pub fn on_mouseover(&mut self, event: &str, func: EventHandler<State>) {
self.mouseover.insert(event.into(), func);
}
pub fn on_mouseout(&mut self, event: &str, func: EventHandler<State>) {
self.mouseout.insert(event.into(), func);
}
}

View File

@ -0,0 +1,118 @@
use crate::live_view::{LiveView, Template};
use actix::prelude::*;
use actix_web_actors::{ws, ws::WebsocketContext};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Event {
pub kind: String,
pub event: String,
pub data: Option<String>,
pub target: Option<String>,
}
pub struct StateSocket<State: Template> {
pub state: State,
pub live_view: LiveView<State>,
}
impl<State: Template> Actor for StateSocket<State> {
type Context = WebsocketContext<Self>;
}
/// Handler for ws::Message message
impl<State: Template> StreamHandler<Result<ws::Message, ws::ProtocolError>> for StateSocket<State> {
fn handle(&mut self, msg: Result<ws::Message, ws::ProtocolError>, ctx: &mut Self::Context) {
match msg {
Ok(ws::Message::Ping(msg)) => ctx.pong(&msg),
Ok(ws::Message::Text(text)) => {
let parsed: Event = serde_json::from_str(&text).unwrap();
match parsed.kind.as_ref() {
"click" => click_handler(self, ctx, parsed),
"keydown" => keydown_handler(self, ctx, parsed),
"input" => input_handler(self, ctx, parsed),
"mouseover" => mouseover_handler(self, ctx, parsed),
"mouseout" => mouseout_handler(self, ctx, parsed),
"submit" => submit_handler(self, ctx, parsed),
_ => {}
}
}
Ok(ws::Message::Binary(bin)) => dbg!(ctx.binary(bin)),
Ok(ws::Message::Close(_)) => {
ctx.stop();
}
_ => (),
}
}
}
fn click_handler<State: Template>(
socket: &mut StateSocket<State>,
ctx: &mut WebsocketContext<StateSocket<State>>,
event: Event,
) {
if let Some(f) = socket.live_view.click.get_mut(&event.event) {
if let Some(rendered) = f(&event, &mut socket.state) {
ctx.text(rendered);
}
}
}
fn submit_handler<State: Template>(
socket: &mut StateSocket<State>,
ctx: &mut WebsocketContext<StateSocket<State>>,
event: Event,
) {
if let Some(f) = socket.live_view.submit.get_mut(&event.event) {
if let Some(rendered) = f(&event, &mut socket.state) {
ctx.text(rendered);
}
}
}
fn input_handler<State: Template>(
socket: &mut StateSocket<State>,
ctx: &mut WebsocketContext<StateSocket<State>>,
event: Event,
) {
if let Some(f) = socket.live_view.input.get_mut(&event.event) {
if let Some(rendered) = f(&event, &mut socket.state) {
ctx.text(rendered);
}
}
}
fn mouseover_handler<State: Template>(
socket: &mut StateSocket<State>,
ctx: &mut WebsocketContext<StateSocket<State>>,
event: Event,
) {
if let Some(f) = socket.live_view.mouseover.get_mut(&event.event) {
if let Some(rendered) = f(&event, &mut socket.state) {
ctx.text(rendered);
}
}
}
fn mouseout_handler<State: Template>(
socket: &mut StateSocket<State>,
ctx: &mut WebsocketContext<StateSocket<State>>,
event: Event,
) {
if let Some(f) = socket.live_view.mouseout.get_mut(&event.event) {
if let Some(rendered) = f(&event, &mut socket.state) {
ctx.text(rendered);
}
}
}
fn keydown_handler<State: Template>(
socket: &mut StateSocket<State>,
ctx: &mut WebsocketContext<StateSocket<State>>,
event: Event,
) {
if let Some(f) = socket.live_view.keydown.get_mut(&event.event) {
if let Some(rendered) = f(&event, &mut socket.state) {
ctx.text(rendered);
}
}
}

6
webui/package-lock.json generated Normal file
View File

@ -0,0 +1,6 @@
{
"name": "webui",
"lockfileVersion": 2,
"requires": true,
"packages": {}
}

172
webui/src/lib.rs Normal file
View File

@ -0,0 +1,172 @@
mod template;
use actix_web::HttpResponse;
use actix_web_actors::ws;
use common::{pattern, strip};
use live_view::{LiveView, StateSocket, Template};
use std::str::FromStr;
use template::{AppTemplate, ControlTemplate};
use tracing::{error, info};
use actix_web::{
error::{ErrorInternalServerError, JsonPayloadError, UrlencodedError},
get, post, web,
web::JsonConfig,
App, HttpRequest, HttpServer, Responder, Result,
};
use std::{
io,
sync::{mpsc::Sender, Arc, Mutex},
};
struct AppState {
strip_tx: Arc<Mutex<Sender<strip::Message>>>,
}
#[post("/setcolor")]
async fn set_color_json(
data: web::Data<AppState>,
params: web::Json<pattern::Parameters>,
) -> Result<impl Responder> {
info!("Got params: {params:?}");
data.strip_tx
.lock()
.map_err(|_| io::Error::new(io::ErrorKind::Other, "Failed to get a lock"))?
.send(strip::Message::ChangePattern(params.0.to_pattern()))
.map_err(|_| io::Error::new(io::ErrorKind::Other, "Failed to send to channel"))?;
Ok("Success")
}
#[actix_web::main]
pub async fn start(strip_tx: Sender<strip::Message>) -> std::io::Result<()> {
info!("Starting webui");
HttpServer::new(move || {
App::new()
.data(AppState {
strip_tx: Arc::new(Mutex::new(strip_tx.clone())),
})
.service(
web::scope("/api")
.app_data(
JsonConfig::default().error_handler(|err: JsonPayloadError, _req| {
error!("JSON error: {err:?}");
err.into()
}),
)
.app_data(web::FormConfig::default().error_handler(
|err: UrlencodedError, _req| {
error!("{err:?}");
err.into()
},
))
.service(set_color_json),
)
.service(web::resource("/ws/").route(web::get().to(start_socket)))
.service(initial_load)
})
.bind(("0.0.0.0", 8080))?
.run()
.await
}
#[get("/")]
async fn initial_load(data: web::Data<AppState>, _req: HttpRequest) -> impl Responder {
let state = AppTemplate {
body: ControlTemplate {
strip_tx: Some(data.strip_tx.clone()),
..ControlTemplate::default()
}
.render()
.map_err(|e| {
error!("Internal error e found: {e:?}");
ErrorInternalServerError(e)
})?,
// ..AppTemplate::default()
};
state
.render()
.map(|b| HttpResponse::Ok().body(b))
.map_err(ErrorInternalServerError)
}
async fn start_socket(
data: web::Data<AppState>,
req: HttpRequest,
stream: web::Payload,
) -> impl Responder {
let mut live_view: LiveView<ControlTemplate> = LiveView::default();
live_view.on_input("change-template", |event, state| {
info!("Got change template event: {event:?}");
let template_name = event.data.as_ref()?;
let params = pattern::Parameters::from_str(template_name).ok()?;
state.parameters = params;
state
.render()
.map(|s| {
info!("{s}");
s
})
.map_err(|e| {
format!("Error rendering state: {e:?}");
e
})
.ok()
});
live_view.on_submit("form", |event, state| {
// info!("(submit) Form submit: {:?}", event);
info!(
"Current value - {:?}",
serde_json::to_string(&state.parameters)
);
info!("Form data: {:?}", event.data);
let p: pattern::Parameters = serde_json::from_str(event.data.as_ref()?)
.map_err(|e| {
error!("Error parsing: {e:?}");
e
})
.ok()?;
state.parameters = p;
info!("Set state parameters to: {:?}", state.parameters);
state
.strip_tx
.as_ref()?
.lock()
.map_err(|_| {
info!(
"{:?}",
io::Error::new(io::ErrorKind::Other, "Failed to get a lock")
)
})
.ok()?
.send(strip::Message::ChangePattern(state.parameters.to_pattern()))
.map_err(|_| {
info!(
"{:?}",
io::Error::new(io::ErrorKind::Other, "Failed to send to channel")
)
})
.ok()?;
state
.render()
.map_err(|e| {
format!("Error rendering state: {e:?}");
e
})
.ok()
});
let actor = StateSocket {
state: ControlTemplate {
strip_tx: Some(data.strip_tx.clone()),
..ControlTemplate::default()
},
live_view,
};
ws::start(actor, &req, stream)
}

28
webui/src/template.rs Normal file
View File

@ -0,0 +1,28 @@
use crate::{
pattern::{FormRender, Parameters},
strip,
};
use std::sync::{mpsc::Sender, Arc, Mutex};
#[derive(askama::Template, Clone, Debug, Default)]
#[template(path = "app.html", escape = "none")]
pub struct AppTemplate {
pub body: String,
}
impl live_view::Template for AppTemplate {
fn render(&self) -> Result<String, Box<dyn std::error::Error>> {
Ok(<Self as askama::Template>::render(self)?)
}
}
#[derive(askama::Template, Clone, Debug, Default)]
#[template(path = "control.html", escape = "none")]
pub struct ControlTemplate {
pub strip_tx: Option<Arc<Mutex<Sender<strip::Message>>>>,
pub parameters: Parameters,
}
impl live_view::Template for ControlTemplate {
fn render(&self) -> Result<String, Box<dyn std::error::Error>> {
Ok(<Self as askama::Template>::render(self)?)
}
}

30
webui/templates/app.html Normal file
View File

@ -0,0 +1,30 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="description" content="Remote light control for aw-lights">
<meta name="author" content="Austen Adler <austenadler.com>">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- <link rel="icon" type="image/png" href="images/favicon.png"> -->
<title>Light Control</title>
<style type="text/css">
{% include "css/normalize.css" %}
</style>
<style type="text/css">
{% include "css/skeleton.css" %}
</style>
</head>
<body>
<div class="container">
<div id="content">
{{ body }}
</div>
</div>
<script>
{% include "../liveview-rust/js/dist/liveview.js" %}
</script>
</body>
</html>

View File

@ -0,0 +1,15 @@
<!-- TODO: When refreshing, the selected element does not change -->
<!-- TODO: Do not use .to_string() for comparison -->
<select id="template-name" rust-input="change-template">
{%- let selected_name = parameters.to_string() -%}
{% for name in Parameters::get_names() -%}
<option value="{{ name }}"
{%- if selected_name == name.to_string() %} selected="selected"{% endif -%}
>{{ name }}</option>
{% endfor -%}
</select>
<form rust-submit="form">
{{ parameters.render() }}
<button type="submit">Submit</button>
</form>

427
webui/templates/css/normalize.css vendored Normal file
View File

@ -0,0 +1,427 @@
/*! normalize.css v3.0.2 | MIT License | git.io/normalize */
/**
* 1. Set default font family to sans-serif.
* 2. Prevent iOS text size adjust after orientation change, without disabling
* user zoom.
*/
html {
font-family: sans-serif; /* 1 */
-ms-text-size-adjust: 100%; /* 2 */
-webkit-text-size-adjust: 100%; /* 2 */
}
/**
* Remove default margin.
*/
body {
margin: 0;
}
/* HTML5 display definitions
========================================================================== */
/**
* Correct `block` display not defined for any HTML5 element in IE 8/9.
* Correct `block` display not defined for `details` or `summary` in IE 10/11
* and Firefox.
* Correct `block` display not defined for `main` in IE 11.
*/
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
main,
menu,
nav,
section,
summary {
display: block;
}
/**
* 1. Correct `inline-block` display not defined in IE 8/9.
* 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.
*/
audio,
canvas,
progress,
video {
display: inline-block; /* 1 */
vertical-align: baseline; /* 2 */
}
/**
* Prevent modern browsers from displaying `audio` without controls.
* Remove excess height in iOS 5 devices.
*/
audio:not([controls]) {
display: none;
height: 0;
}
/**
* Address `[hidden]` styling not present in IE 8/9/10.
* Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22.
*/
[hidden],
template {
display: none;
}
/* Links
========================================================================== */
/**
* Remove the gray background color from active links in IE 10.
*/
a {
background-color: transparent;
}
/**
* Improve readability when focused and also mouse hovered in all browsers.
*/
a:active,
a:hover {
outline: 0;
}
/* Text-level semantics
========================================================================== */
/**
* Address styling not present in IE 8/9/10/11, Safari, and Chrome.
*/
abbr[title] {
border-bottom: 1px dotted;
}
/**
* Address style set to `bolder` in Firefox 4+, Safari, and Chrome.
*/
b,
strong {
font-weight: bold;
}
/**
* Address styling not present in Safari and Chrome.
*/
dfn {
font-style: italic;
}
/**
* Address variable `h1` font-size and margin within `section` and `article`
* contexts in Firefox 4+, Safari, and Chrome.
*/
h1 {
font-size: 2em;
margin: 0.67em 0;
}
/**
* Address styling not present in IE 8/9.
*/
mark {
background: #ff0;
color: #000;
}
/**
* Address inconsistent and variable font size in all browsers.
*/
small {
font-size: 80%;
}
/**
* Prevent `sub` and `sup` affecting `line-height` in all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sup {
top: -0.5em;
}
sub {
bottom: -0.25em;
}
/* Embedded content
========================================================================== */
/**
* Remove border when inside `a` element in IE 8/9/10.
*/
img {
border: 0;
}
/**
* Correct overflow not hidden in IE 9/10/11.
*/
svg:not(:root) {
overflow: hidden;
}
/* Grouping content
========================================================================== */
/**
* Address margin not present in IE 8/9 and Safari.
*/
figure {
margin: 1em 40px;
}
/**
* Address differences between Firefox and other browsers.
*/
hr {
-moz-box-sizing: content-box;
box-sizing: content-box;
height: 0;
}
/**
* Contain overflow in all browsers.
*/
pre {
overflow: auto;
}
/**
* Address odd `em`-unit font size rendering in all browsers.
*/
code,
kbd,
pre,
samp {
font-family: monospace, monospace;
font-size: 1em;
}
/* Forms
========================================================================== */
/**
* Known limitation: by default, Chrome and Safari on OS X allow very limited
* styling of `select`, unless a `border` property is set.
*/
/**
* 1. Correct color not being inherited.
* Known issue: affects color of disabled elements.
* 2. Correct font properties not being inherited.
* 3. Address margins set differently in Firefox 4+, Safari, and Chrome.
*/
button,
input,
optgroup,
select,
textarea {
color: inherit; /* 1 */
font: inherit; /* 2 */
margin: 0; /* 3 */
}
/**
* Address `overflow` set to `hidden` in IE 8/9/10/11.
*/
button {
overflow: visible;
}
/**
* Address inconsistent `text-transform` inheritance for `button` and `select`.
* All other form control elements do not inherit `text-transform` values.
* Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.
* Correct `select` style inheritance in Firefox.
*/
button,
select {
text-transform: none;
}
/**
* 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
* and `video` controls.
* 2. Correct inability to style clickable `input` types in iOS.
* 3. Improve usability and consistency of cursor style between image-type
* `input` and others.
*/
button,
html input[type="button"], /* 1 */
input[type="reset"],
input[type="submit"] {
-webkit-appearance: button; /* 2 */
cursor: pointer; /* 3 */
}
/**
* Re-set default cursor for disabled elements.
*/
button[disabled],
html input[disabled] {
cursor: default;
}
/**
* Remove inner padding and border in Firefox 4+.
*/
button::-moz-focus-inner,
input::-moz-focus-inner {
border: 0;
padding: 0;
}
/**
* Address Firefox 4+ setting `line-height` on `input` using `!important` in
* the UA stylesheet.
*/
input {
line-height: normal;
}
/**
* It's recommended that you don't attempt to style these elements.
* Firefox's implementation doesn't respect box-sizing, padding, or width.
*
* 1. Address box sizing set to `content-box` in IE 8/9/10.
* 2. Remove excess padding in IE 8/9/10.
*/
input[type="checkbox"],
input[type="radio"] {
box-sizing: border-box; /* 1 */
padding: 0; /* 2 */
}
/**
* Fix the cursor style for Chrome's increment/decrement buttons. For certain
* `font-size` values of the `input`, it causes the cursor style of the
* decrement button to change from `default` to `text`.
*/
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
height: auto;
}
/**
* 1. Address `appearance` set to `searchfield` in Safari and Chrome.
* 2. Address `box-sizing` set to `border-box` in Safari and Chrome
* (include `-moz` to future-proof).
*/
input[type="search"] {
-webkit-appearance: textfield; /* 1 */
-moz-box-sizing: content-box;
-webkit-box-sizing: content-box; /* 2 */
box-sizing: content-box;
}
/**
* Remove inner padding and search cancel button in Safari and Chrome on OS X.
* Safari (but not Chrome) clips the cancel button when the search input has
* padding (and `textfield` appearance).
*/
input[type="search"]::-webkit-search-cancel-button,
input[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* Define consistent border, margin, and padding.
*/
fieldset {
border: 1px solid #c0c0c0;
margin: 0 2px;
padding: 0.35em 0.625em 0.75em;
}
/**
* 1. Correct `color` not being inherited in IE 8/9/10/11.
* 2. Remove padding so people aren't caught out if they zero out fieldsets.
*/
legend {
border: 0; /* 1 */
padding: 0; /* 2 */
}
/**
* Remove default vertical scrollbar in IE 8/9/10/11.
*/
textarea {
overflow: auto;
}
/**
* Don't inherit the `font-weight` (applied by a rule above).
* NOTE: the default cannot safely be changed in Chrome and Safari on OS X.
*/
optgroup {
font-weight: bold;
}
/* Tables
========================================================================== */
/**
* Remove most spacing between table cells.
*/
table {
border-collapse: collapse;
border-spacing: 0;
}
td,
th {
padding: 0;
}

418
webui/templates/css/skeleton.css vendored Normal file
View File

@ -0,0 +1,418 @@
/*
* Skeleton V2.0.4
* Copyright 2014, Dave Gamache
* www.getskeleton.com
* Free to use under the MIT license.
* http://www.opensource.org/licenses/mit-license.php
* 12/29/2014
*/
/* Table of contents
- Grid
- Base Styles
- Typography
- Links
- Buttons
- Forms
- Lists
- Code
- Tables
- Spacing
- Utilities
- Clearing
- Media Queries
*/
/* Grid
*/
.container {
position: relative;
width: 100%;
max-width: 960px;
margin: 0 auto;
padding: 0 20px;
box-sizing: border-box; }
.column,
.columns {
width: 100%;
float: left;
box-sizing: border-box; }
/* For devices larger than 400px */
@media (min-width: 400px) {
.container {
width: 85%;
padding: 0; }
}
/* For devices larger than 550px */
@media (min-width: 550px) {
.container {
width: 80%; }
.column,
.columns {
margin-left: 4%; }
.column:first-child,
.columns:first-child {
margin-left: 0; }
.one.column,
.one.columns { width: 4.66666666667%; }
.two.columns { width: 13.3333333333%; }
.three.columns { width: 22%; }
.four.columns { width: 30.6666666667%; }
.five.columns { width: 39.3333333333%; }
.six.columns { width: 48%; }
.seven.columns { width: 56.6666666667%; }
.eight.columns { width: 65.3333333333%; }
.nine.columns { width: 74.0%; }
.ten.columns { width: 82.6666666667%; }
.eleven.columns { width: 91.3333333333%; }
.twelve.columns { width: 100%; margin-left: 0; }
.one-third.column { width: 30.6666666667%; }
.two-thirds.column { width: 65.3333333333%; }
.one-half.column { width: 48%; }
/* Offsets */
.offset-by-one.column,
.offset-by-one.columns { margin-left: 8.66666666667%; }
.offset-by-two.column,
.offset-by-two.columns { margin-left: 17.3333333333%; }
.offset-by-three.column,
.offset-by-three.columns { margin-left: 26%; }
.offset-by-four.column,
.offset-by-four.columns { margin-left: 34.6666666667%; }
.offset-by-five.column,
.offset-by-five.columns { margin-left: 43.3333333333%; }
.offset-by-six.column,
.offset-by-six.columns { margin-left: 52%; }
.offset-by-seven.column,
.offset-by-seven.columns { margin-left: 60.6666666667%; }
.offset-by-eight.column,
.offset-by-eight.columns { margin-left: 69.3333333333%; }
.offset-by-nine.column,
.offset-by-nine.columns { margin-left: 78.0%; }
.offset-by-ten.column,
.offset-by-ten.columns { margin-left: 86.6666666667%; }
.offset-by-eleven.column,
.offset-by-eleven.columns { margin-left: 95.3333333333%; }
.offset-by-one-third.column,
.offset-by-one-third.columns { margin-left: 34.6666666667%; }
.offset-by-two-thirds.column,
.offset-by-two-thirds.columns { margin-left: 69.3333333333%; }
.offset-by-one-half.column,
.offset-by-one-half.columns { margin-left: 52%; }
}
/* Base Styles
*/
/* NOTE
html is set to 62.5% so that all the REM measurements throughout Skeleton
are based on 10px sizing. So basically 1.5rem = 15px :) */
html {
font-size: 62.5%; }
body {
font-size: 1.5em; /* currently ems cause chrome bug misinterpreting rems on body element */
line-height: 1.6;
font-weight: 400;
font-family: "Raleway", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif;
color: #222; }
/* Typography
*/
h1, h2, h3, h4, h5, h6 {
margin-top: 0;
margin-bottom: 2rem;
font-weight: 300; }
h1 { font-size: 4.0rem; line-height: 1.2; letter-spacing: -.1rem;}
h2 { font-size: 3.6rem; line-height: 1.25; letter-spacing: -.1rem; }
h3 { font-size: 3.0rem; line-height: 1.3; letter-spacing: -.1rem; }
h4 { font-size: 2.4rem; line-height: 1.35; letter-spacing: -.08rem; }
h5 { font-size: 1.8rem; line-height: 1.5; letter-spacing: -.05rem; }
h6 { font-size: 1.5rem; line-height: 1.6; letter-spacing: 0; }
/* Larger than phablet */
@media (min-width: 550px) {
h1 { font-size: 5.0rem; }
h2 { font-size: 4.2rem; }
h3 { font-size: 3.6rem; }
h4 { font-size: 3.0rem; }
h5 { font-size: 2.4rem; }
h6 { font-size: 1.5rem; }
}
p {
margin-top: 0; }
/* Links
*/
a {
color: #1EAEDB; }
a:hover {
color: #0FA0CE; }
/* Buttons
*/
.button,
button,
input[type="submit"],
input[type="reset"],
input[type="button"] {
display: inline-block;
height: 38px;
padding: 0 30px;
color: #555;
text-align: center;
font-size: 11px;
font-weight: 600;
line-height: 38px;
letter-spacing: .1rem;
text-transform: uppercase;
text-decoration: none;
white-space: nowrap;
background-color: transparent;
border-radius: 4px;
border: 1px solid #bbb;
cursor: pointer;
box-sizing: border-box; }
.button:hover,
button:hover,
input[type="submit"]:hover,
input[type="reset"]:hover,
input[type="button"]:hover,
.button:focus,
button:focus,
input[type="submit"]:focus,
input[type="reset"]:focus,
input[type="button"]:focus {
color: #333;
border-color: #888;
outline: 0; }
.button.button-primary,
button.button-primary,
input[type="submit"].button-primary,
input[type="reset"].button-primary,
input[type="button"].button-primary {
color: #FFF;
background-color: #33C3F0;
border-color: #33C3F0; }
.button.button-primary:hover,
button.button-primary:hover,
input[type="submit"].button-primary:hover,
input[type="reset"].button-primary:hover,
input[type="button"].button-primary:hover,
.button.button-primary:focus,
button.button-primary:focus,
input[type="submit"].button-primary:focus,
input[type="reset"].button-primary:focus,
input[type="button"].button-primary:focus {
color: #FFF;
background-color: #1EAEDB;
border-color: #1EAEDB; }
/* Forms
*/
input[type="email"],
input[type="number"],
input[type="search"],
input[type="text"],
input[type="tel"],
input[type="url"],
input[type="password"],
textarea,
select {
height: 38px;
padding: 6px 10px; /* The 6px vertically centers text on FF, ignored by Webkit */
background-color: #fff;
border: 1px solid #D1D1D1;
border-radius: 4px;
box-shadow: none;
box-sizing: border-box; }
/* Removes awkward default styles on some inputs for iOS */
input[type="email"],
input[type="number"],
input[type="search"],
input[type="text"],
input[type="tel"],
input[type="url"],
input[type="password"],
textarea {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none; }
textarea {
min-height: 65px;
padding-top: 6px;
padding-bottom: 6px; }
input[type="email"]:focus,
input[type="number"]:focus,
input[type="search"]:focus,
input[type="text"]:focus,
input[type="tel"]:focus,
input[type="url"]:focus,
input[type="password"]:focus,
textarea:focus,
select:focus {
border: 1px solid #33C3F0;
outline: 0; }
label,
legend {
display: block;
margin-bottom: .5rem;
font-weight: 600; }
fieldset {
padding: 0;
border-width: 0; }
input[type="checkbox"],
input[type="radio"] {
display: inline; }
label > .label-body {
display: inline-block;
margin-left: .5rem;
font-weight: normal; }
/* Lists
*/
ul {
list-style: circle inside; }
ol {
list-style: decimal inside; }
ol, ul {
padding-left: 0;
margin-top: 0; }
ul ul,
ul ol,
ol ol,
ol ul {
margin: 1.5rem 0 1.5rem 3rem;
font-size: 90%; }
li {
margin-bottom: 1rem; }
/* Code
*/
code {
padding: .2rem .5rem;
margin: 0 .2rem;
font-size: 90%;
white-space: nowrap;
background: #F1F1F1;
border: 1px solid #E1E1E1;
border-radius: 4px; }
pre > code {
display: block;
padding: 1rem 1.5rem;
white-space: pre; }
/* Tables
*/
th,
td {
padding: 12px 15px;
text-align: left;
border-bottom: 1px solid #E1E1E1; }
th:first-child,
td:first-child {
padding-left: 0; }
th:last-child,
td:last-child {
padding-right: 0; }
/* Spacing
*/
button,
.button {
margin-bottom: 1rem; }
input,
textarea,
select,
fieldset {
margin-bottom: 1.5rem; }
pre,
blockquote,
dl,
figure,
table,
p,
ul,
ol,
form {
margin-bottom: 2.5rem; }
/* Utilities
*/
.u-full-width {
width: 100%;
box-sizing: border-box; }
.u-max-full-width {
max-width: 100%;
box-sizing: border-box; }
.u-pull-right {
float: right; }
.u-pull-left {
float: left; }
/* Misc
*/
hr {
margin-top: 3rem;
margin-bottom: 3.5rem;
border-width: 0;
border-top: 1px solid #E1E1E1; }
/* Clearing
*/
/* Self Clearing Goodness */
.container:after,
.row:after,
.u-cf {
content: "";
display: table;
clear: both; }
/* Media Queries
*/
/*
Note: The best way to structure the use of media queries is to create the queries
near the relevant code. For example, if you wanted to change the styles for buttons
on small devices, paste the mobile query code up in the buttons section and style it
there.
*/
/* Larger than mobile */
@media (min-width: 400px) {}
/* Larger than phablet (also point when grid becomes active) */
@media (min-width: 550px) {}
/* Larger than tablet */
@media (min-width: 750px) {}
/* Larger than desktop */
@media (min-width: 1000px) {}
/* Larger than Desktop HD */
@media (min-width: 1200px) {}

4158
webui/web/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -11,6 +11,8 @@
"devDependencies": { "devDependencies": {
"@rollup/plugin-commonjs": "^17.0.0", "@rollup/plugin-commonjs": "^17.0.0",
"@rollup/plugin-node-resolve": "^11.0.0", "@rollup/plugin-node-resolve": "^11.0.0",
"@rollup/plugin-typescript": "^8.0.0",
"@tsconfig/svelte": "^2.0.0",
"rollup": "^2.3.4", "rollup": "^2.3.4",
"rollup-plugin-css-only": "^3.1.0", "rollup-plugin-css-only": "^3.1.0",
"rollup-plugin-livereload": "^2.0.0", "rollup-plugin-livereload": "^2.0.0",
@ -19,12 +21,12 @@
"svelte": "^3.0.0", "svelte": "^3.0.0",
"svelte-check": "^2.0.0", "svelte-check": "^2.0.0",
"svelte-preprocess": "^4.0.0", "svelte-preprocess": "^4.0.0",
"@rollup/plugin-typescript": "^8.0.0",
"typescript": "^4.0.0",
"tslib": "^2.0.0", "tslib": "^2.0.0",
"@tsconfig/svelte": "^2.0.0" "typescript": "^4.0.0",
"webpack-cli": "^4.10.0"
}, },
"dependencies": { "dependencies": {
"sirv-cli": "^1.0.0" "sirv-cli": "^1.0.0",
"webpack": "^5.74.0"
} }
} }

View File

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@ -14,6 +14,7 @@
} }
} }
</script> </script>
hi
{#each value as value} {#each value as value}
<input type="color" bind:value={value} /> <input type="color" bind:value={value} />
{/each} {/each}

View File

@ -32,6 +32,11 @@
{name: "width", type: "number", label: "Width", value: 10}, {name: "width", type: "number", label: "Width", value: 10},
{name: "tick_rate", type: "number", label: "Tick Rate", value: 10}, {name: "tick_rate", type: "number", label: "Tick Rate", value: 10},
]}, ]},
{name: "Slide", text: "Slide", formElements: [
{name: "color", type: "colors", label: "Color", value: []},
{name: "width", type: "number", label: "Width", value: 10},
{name: "height", type: "number", label: "Height", value: 10},
]},
]; ];
let selectedPattern = possiblePatterns[0]; let selectedPattern = possiblePatterns[0];
@ -87,8 +92,10 @@
{#each selectedPattern.formElements as fe} {#each selectedPattern.formElements as fe}
<label for={fe.name}>{fe.label}</label> <label for={fe.name}>{fe.label}</label>
{#if fe.type === "colors"} {#if fe.type === "colors"}
colors
<Colors bind:value={fe.value}/> <Colors bind:value={fe.value}/>
{:else} {:else}
others
<input type={fe.type} name={fe.name} on:input={(e) => fe.value = e.target.value} /> <input type={fe.type} name={fe.name} on:input={(e) => fe.value = e.target.value} />
{/if} {/if}
{/each} {/each}

Some files were not shown because too many files have changed in this diff Show More