Compare commits
109 Commits
master
...
liveview-r
Author | SHA1 | Date | |
---|---|---|---|
|
058a076414 | ||
|
dd978179df | ||
|
f65d45260e | ||
|
5ff96febac | ||
|
2496e03033 | ||
|
d4b3651dbc | ||
|
cb9fe3b9e3 | ||
|
529610a7ff | ||
|
4cb3ce0bff | ||
|
ccc0b3d883 | ||
|
ea1b9017f1 | ||
|
e271fb8277 | ||
|
b49326f5dc | ||
|
9032aba60a | ||
|
a11168c8f1 | ||
|
016bc590d1 | ||
|
3bdc909933 | ||
|
ab33695994 | ||
|
ac7132659c | ||
|
ad45bc3431 | ||
|
84241963a2 | ||
|
d989a1183f | ||
|
60687e11fa | ||
|
aa96d81fa3 | ||
|
6cb9883a7c | ||
|
7c609be435 | ||
|
cee8a92c87 | ||
|
1ad1a4d315 | ||
|
d7beb73329 | ||
|
f1e11c1bc2 | ||
|
bc4a6dfeac | ||
|
0c652a1c19 | ||
|
76886622ec | ||
|
663fd4feca | ||
|
e9a68b0997 | ||
|
b9c906ae96 | ||
|
2fed645d22 | ||
|
e4e69f4d51 | ||
|
4781506ff9 | ||
|
97d523a149 | ||
|
1c6eefc004 | ||
|
7a43ae7431 | ||
|
0a13264096 | ||
|
7d2737d1d1 | ||
|
0c84fda192 | ||
|
1eb6f0f51a | ||
|
914dfa23d7 | ||
|
c27f78208d | ||
|
9f565cdafb | ||
|
11fe309b34 | ||
|
a8b3a4cce5 | ||
|
8a730a40d5 | ||
|
c89ca20295 | ||
|
9abeb4492a | ||
|
db45a14e84 | ||
|
3f02d476dc | ||
|
4c8ae8c9f6 | ||
|
c30583a9cc | ||
|
2d13b8aca5 | ||
|
e86d8cfd6f | ||
|
d621a338aa | ||
|
15cb6a512a | ||
|
5c23f9226c | ||
|
baa9b79863 | ||
|
f0e6aeb89f | ||
|
72b26d9e74 | ||
|
0fdfc2155c | ||
|
a958fee74c | ||
|
632f49f6a9 | ||
|
993a8ad1c7 | ||
|
b1f9829532 | ||
|
4aa5fe8df4 | ||
|
45c732a0d2 | ||
|
d5624e7475 | ||
|
c41eb8691a | ||
|
33b5975f93 | ||
|
3a81585d51 | ||
|
bdf6122993 | ||
|
ac3507b774 | ||
|
35c9de16c2 | ||
|
fc5403388f | ||
|
b5b4bf3b41 | ||
|
c295839a9a | ||
|
4251b18465 | ||
|
81bb9a2de0 | ||
|
65aa41d780 | ||
|
47114718a1 | ||
|
8a7453d12c | ||
|
690aaf4d40 | ||
|
d13432e35d | ||
|
0787c19b90 | ||
|
d97e4b761c | ||
|
9ed83f0b96 | ||
|
b9b503d61b | ||
|
dad2401023 | ||
|
04bc783c09 | ||
|
afb57a17ff | ||
|
a6ee7ad6bc | ||
|
22a53fd735 | ||
|
c40173f2ca | ||
|
49f7774d66 | ||
|
489adc1af1 | ||
|
537fec8a4d | ||
|
1734dbb5c9 | ||
|
5b5343dc9f | ||
|
3b0eb7fc1c | ||
|
efeb2d56dc | ||
|
86ef9ba35c | ||
|
829e9d97aa |
@ -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
1
.cargo/config.toml
Symbolic link
@ -0,0 +1 @@
|
||||
../config.toml
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,4 +1,6 @@
|
||||
/.env
|
||||
/debug/
|
||||
/target/
|
||||
/Cargo.lock
|
||||
**/*.rs.bk
|
||||
/node_modules
|
||||
|
35
Cargo.toml
35
Cargo.toml
@ -3,17 +3,30 @@ name = "aw-lights"
|
||||
version = "0.1.0"
|
||||
authors = ["Austen Adler <agadler@austenadler.com>"]
|
||||
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]
|
||||
rppal = "0.7"
|
||||
ws2818-rgb-led-spi-driver = { path = "lib-ws2818-rgb-led-spi-driver/" }
|
||||
serde = {version = "1.0", features = ["derive"]}
|
||||
actix-web = {version = "3", default_features = false}
|
||||
rust-embed="6.0.0"
|
||||
hex = "0.4.3"
|
||||
serde_json = "1"
|
||||
actix-web-static-files = "3.0"
|
||||
rppal = "0.14"
|
||||
ws2818-rgb-led-spi-driver = { path = "./lib-ws2818-rgb-led-spi-driver" }
|
||||
common = { path = "./common" }
|
||||
webui = { path = "./webui" }
|
||||
mqtt = { path = "./mqtt" }
|
||||
dotenv = "0.15.0"
|
||||
clap = { version = "4.3.0", features = ["derive", "env"] }
|
||||
tracing = "0.1.37"
|
||||
env_logger = "0.10.0"
|
||||
|
||||
[build-dependencies]
|
||||
actix-web-static-files = "3.0"
|
||||
[profile.release]
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
|
45
Makefile
45
Makefile
@ -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:
|
||||
cargo build --target=armv7-unknown-linux-gnueabihf
|
||||
# TARGET = arm-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
|
||||
arm-linux-gnueabihf-strip ./target/armv7-unknown-linux-gnueabihf/debug/aw-lights
|
||||
scp ./target/armv7-unknown-linux-gnueabihf/debug/aw-lights pi:aw-lights-bin
|
||||
scp ./target/$(TARGET)/debug/$(PROJECT_NAME) pi@$(HOST):$(PROJECT_NAME)-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
|
||||
ssh pi ./aw-lights
|
||||
ssh pi@192.168.1.82 ./$(PROJECT_NAME)-bin
|
||||
|
37
README.adoc
37
README.adoc
@ -1,7 +1,9 @@
|
||||
# aw-lights
|
||||
= aw-lights
|
||||
|
||||
== Setup
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
# Install packages
|
||||
xbps-install fake-hwclock
|
||||
ln -s /etc/sv/fake-hwclock/ /var/service/
|
||||
|
||||
@ -11,3 +13,34 @@ echo 'dtparam=spi=on' >>/boot/config.txt
|
||||
# Allow big buf for SPI
|
||||
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
252
aw-lights_0.1.0.bb
Normal 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
|
12
build.rs
12
build.rs
@ -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
14
common/Cargo.toml
Normal 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
340
common/src/cava.rs
Normal 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)
|
||||
}
|
||||
}
|
@ -1,9 +1,8 @@
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::num::ParseIntError;
|
||||
use std::str::FromStr;
|
||||
use crate::pattern::{PatternError, PatternResult};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{iter, num::ParseIntError, 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);
|
||||
impl Rgb {
|
||||
pub const fn to_tuple(self) -> (u8, u8, u8) {
|
||||
@ -12,6 +11,9 @@ impl Rgb {
|
||||
pub fn to_float_tuple(self) -> (f32, f32, f32) {
|
||||
(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) {
|
||||
// (
|
||||
// GAMMA_CORRECT[self.0 as usize],
|
||||
@ -37,6 +39,7 @@ impl FromStr for Rgb {
|
||||
.map_err(|_| ())
|
||||
.and_then(|v| {
|
||||
Ok(Self(
|
||||
#[allow(clippy::get_first)]
|
||||
*v.get(0).ok_or(())?,
|
||||
*v.get(1).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,
|
||||
// 223, 225, 228, 231, 233, 236, 239, 241, 244, 247, 249, 252, 255,
|
||||
// ];
|
||||
|
||||
pub const RAINBOW: [Rgb; 7] = [
|
||||
Rgb(255, 0, 0), // R
|
||||
Rgb(255, 128, 0), // O
|
||||
@ -81,17 +85,34 @@ pub const RAINBOW: [Rgb; 7] = [
|
||||
Rgb(148, 0, 211), // V
|
||||
];
|
||||
|
||||
/// Merges a color by some factor
|
||||
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();
|
||||
/// Stretch
|
||||
///
|
||||
/// ```
|
||||
/// 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
|
||||
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)
|
||||
from.iter().flat_map(move |&x| {
|
||||
// The number of copies of this this segment
|
||||
// Can be either count, or count+1, depending on
|
||||
let segment_count = (count).saturating_add(if additional_remainder > 0 {
|
||||
additional_remainder = additional_remainder.saturating_sub(1);
|
||||
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`
|
||||
@ -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 mut ret: Vec<Rgb> = vec![];
|
||||
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
|
||||
}
|
||||
|
||||
pub fn min_with_factor(at_least: u16, factor: u16) -> Result<u16, ()> {
|
||||
Ok(at_least
|
||||
.checked_sub(1)
|
||||
.ok_or(())?
|
||||
/// Returns the minimum number that is evenly divisible by `factor`
|
||||
///
|
||||
/// ```rust
|
||||
/// 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)
|
||||
.checked_add(1)
|
||||
.ok_or(())?
|
||||
.ok_or(PatternError::ArithmeticError)?
|
||||
.checked_mul(factor)
|
||||
.ok_or(())?)
|
||||
.ok_or(PatternError::ArithmeticError)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@ -130,3 +159,42 @@ mod tests {
|
||||
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)
|
||||
}
|
||||
}
|
@ -1,23 +1,22 @@
|
||||
use crate::pattern::PatternError;
|
||||
use core::any::Any;
|
||||
use std::fmt;
|
||||
use std::io;
|
||||
use std::{fmt, io};
|
||||
|
||||
pub type ProgramResult<T> = Result<T, ProgramError>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Message {
|
||||
Error(ProgramError),
|
||||
Terminated,
|
||||
String(String),
|
||||
InputPrompt(String),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ProgramError {
|
||||
General(String),
|
||||
UiError(String),
|
||||
Boxed(Box<dyn Any + Send>),
|
||||
IoError(io::Error),
|
||||
PatternError(PatternError),
|
||||
}
|
||||
|
||||
impl From<PatternError> for ProgramError {
|
||||
fn from(e: PatternError) -> Self {
|
||||
Self::PatternError(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for ProgramError {
|
||||
@ -41,10 +40,11 @@ impl From<Box<dyn Any + Send>> for ProgramError {
|
||||
impl fmt::Display for ProgramError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
|
||||
match self {
|
||||
Self::General(s) => write!(f, "{}", s),
|
||||
Self::UiError(s) => write!(f, "Critial UI error: {}", s),
|
||||
Self::Boxed(s) => write!(f, "{:?}", s),
|
||||
Self::IoError(e) => write!(f, "{:?}", e),
|
||||
Self::General(s) => write!(f, "{s}"),
|
||||
Self::UiError(s) => write!(f, "Critial UI error: {s}"),
|
||||
Self::Boxed(s) => write!(f, "{s:?}"),
|
||||
Self::IoError(e) => write!(f, "{e:?}"),
|
||||
Self::PatternError(e) => write!(f, "{e:?}"),
|
||||
}
|
||||
}
|
||||
}
|
35
common/src/final_ring.rs
Normal file
35
common/src/final_ring.rs
Normal 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
107
common/src/lib.rs
Normal 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
245
common/src/pattern.rs
Normal 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() {}
|
||||
// }
|
103
common/src/pattern/car_rainbow.rs
Normal file
103
common/src/pattern/car_rainbow.rs
Normal 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))
|
||||
}
|
||||
}
|
@ -1,8 +1,37 @@
|
||||
use super::Pattern;
|
||||
use super::{ColorIterator, FormRender, InputRender, Pattern, PatternError, PatternResult};
|
||||
use crate::color::{self, Rgb};
|
||||
use std::collections::vec_deque;
|
||||
use serde::{Deserialize, Serialize};
|
||||
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)]
|
||||
pub struct Collide {
|
||||
num_lights: u16,
|
||||
@ -18,13 +47,18 @@ pub struct Collide {
|
||||
increase_offset: bool,
|
||||
lights_buf: VecDeque<Rgb>,
|
||||
}
|
||||
impl Default for Collide {
|
||||
fn default() -> Self {
|
||||
Self::new(&CollideParams::default())
|
||||
}
|
||||
}
|
||||
impl Collide {
|
||||
pub fn new(left_color: Rgb, right_color: Rgb, conjoined_color: Rgb) -> Self {
|
||||
pub fn new(params: &CollideParams) -> Self {
|
||||
Self {
|
||||
num_lights: 0,
|
||||
left_color,
|
||||
right_color,
|
||||
conjoined_color,
|
||||
left_color: params.left_color,
|
||||
right_color: params.right_color,
|
||||
conjoined_color: params.conjoined_color,
|
||||
step: 0,
|
||||
step_max: 0,
|
||||
conjoined_bounds: (0, 0),
|
||||
@ -37,7 +71,10 @@ impl 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
|
||||
// Set the left and right color
|
||||
let colors =
|
||||
@ -51,7 +88,7 @@ impl Pattern for Collide {
|
||||
*self
|
||||
.lights_buf
|
||||
.get_mut(usize::from(self.previous_offset))
|
||||
.ok_or(())? = color::BLACK;
|
||||
.ok_or(PatternError::Index)? = color::BLACK;
|
||||
if self.previous_offset
|
||||
!= self
|
||||
.num_lights
|
||||
@ -65,13 +102,13 @@ impl Pattern for Collide {
|
||||
.saturating_sub(1)
|
||||
.saturating_sub(self.previous_offset),
|
||||
))
|
||||
.ok_or(())? = color::BLACK;
|
||||
.ok_or(PatternError::Index)? = color::BLACK;
|
||||
}
|
||||
// Set the color of the current offset
|
||||
*self
|
||||
.lights_buf
|
||||
.get_mut(usize::from(self.offset))
|
||||
.ok_or(())? = colors.0;
|
||||
.ok_or(PatternError::Index)? = colors.0;
|
||||
if self.offset
|
||||
!= self
|
||||
.num_lights
|
||||
@ -85,7 +122,7 @@ impl Pattern for Collide {
|
||||
.saturating_sub(1)
|
||||
.saturating_sub(self.offset),
|
||||
))
|
||||
.ok_or(())? = colors.1;
|
||||
.ok_or(PatternError::Index)? = colors.1;
|
||||
}
|
||||
|
||||
self.previous_offset = self.offset;
|
||||
@ -106,7 +143,7 @@ impl Pattern for Collide {
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
fn init(&mut self, num_lights: u16) -> Result<(), ()> {
|
||||
fn init(&mut self, num_lights: u16) -> PatternResult<()> {
|
||||
// Reset changing parameters
|
||||
self.step = 0;
|
||||
self.offset = 0;
|
||||
@ -114,7 +151,7 @@ impl Pattern for Collide {
|
||||
self.num_lights = num_lights;
|
||||
self.increase_offset = true;
|
||||
if self.num_lights < 3 {
|
||||
return Err(());
|
||||
return Err(PatternError::LightCount);
|
||||
}
|
||||
self.lights_buf = VecDeque::from(vec![color::BLACK; self.num_lights.into()]);
|
||||
if self.num_lights.rem_euclid(2) == 0 {
|
||||
@ -149,7 +186,7 @@ impl Pattern for Collide {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_strip(&self) -> vec_deque::Iter<Rgb> {
|
||||
self.lights_buf.iter()
|
||||
fn get_strip(&self) -> ColorIterator {
|
||||
Box::new(self.lights_buf.iter())
|
||||
}
|
||||
}
|
132
common/src/pattern/custom_visualizer.rs
Normal file
132
common/src/pattern/custom_visualizer.rs
Normal 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(())
|
||||
}
|
||||
}
|
84
common/src/pattern/fade.rs
Normal file
84
common/src/pattern/fade.rs
Normal 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())
|
||||
}
|
||||
}
|
96
common/src/pattern/flashing.rs
Normal file
96
common/src/pattern/flashing.rs
Normal 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())
|
||||
}
|
||||
}
|
77
common/src/pattern/moving_pixel.rs
Normal file
77
common/src/pattern/moving_pixel.rs
Normal 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())
|
||||
}
|
||||
}
|
119
common/src/pattern/moving_rainbow.rs
Normal file
119
common/src/pattern/moving_rainbow.rs
Normal 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())
|
||||
}
|
||||
}
|
||||
}
|
@ -1,8 +1,35 @@
|
||||
use super::Pattern;
|
||||
use super::{ColorIterator, FormRender, InputRender, Pattern, PatternError, PatternResult};
|
||||
use crate::color::{self, Rgb};
|
||||
use std::collections::vec_deque;
|
||||
use std::collections::VecDeque;
|
||||
use std::iter;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{collections::VecDeque, 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)]
|
||||
pub struct Orb {
|
||||
@ -25,14 +52,21 @@ pub struct Orb {
|
||||
/// Direction of the orb. This can switch if `bounces` is true
|
||||
direction: bool,
|
||||
}
|
||||
impl Default for Orb {
|
||||
fn default() -> Self {
|
||||
Self::new(&OrbParams::default())
|
||||
}
|
||||
}
|
||||
impl Orb {
|
||||
pub fn new(color: Rgb, center_width: u8, backoff_width: u8) -> Self {
|
||||
pub fn new(params: &OrbParams) -> Self {
|
||||
Self {
|
||||
lights_buf: VecDeque::new(),
|
||||
color,
|
||||
center_width,
|
||||
backoff_width,
|
||||
total_width: center_width.saturating_add(backoff_width.saturating_mul(2)),
|
||||
color: params.color,
|
||||
center_width: params.center_width,
|
||||
backoff_width: params.backoff_width,
|
||||
total_width: params
|
||||
.center_width
|
||||
.saturating_add(params.backoff_width.saturating_mul(2)),
|
||||
bounces: false,
|
||||
step: 0,
|
||||
step_max: 0,
|
||||
@ -41,7 +75,10 @@ impl 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 we don't bounce, then just wrap and we're done
|
||||
self.lights_buf.rotate_right(1);
|
||||
@ -64,9 +101,9 @@ impl Pattern for Orb {
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
fn init(&mut self, num_lights: u16) -> Result<(), ()> {
|
||||
fn init(&mut self, num_lights: u16) -> PatternResult<()> {
|
||||
if num_lights < 1 {
|
||||
return Err(());
|
||||
return Err(PatternError::LightCount);
|
||||
}
|
||||
self.step = 0;
|
||||
let other_color = color::BLACK;
|
||||
@ -82,15 +119,13 @@ impl Pattern for Orb {
|
||||
)
|
||||
.collect();
|
||||
|
||||
self.step_max = self
|
||||
.lights_buf
|
||||
.len()
|
||||
.checked_sub(self.total_width.into())
|
||||
.unwrap_or_else(|| self.lights_buf.len());
|
||||
let len = self.lights_buf.len();
|
||||
|
||||
self.step_max = len.checked_sub(self.total_width.into()).unwrap_or(len);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
fn get_strip(&self) -> vec_deque::Iter<Rgb> {
|
||||
self.lights_buf.iter()
|
||||
fn get_strip(&self) -> ColorIterator {
|
||||
Box::new(self.lights_buf.iter())
|
||||
}
|
||||
}
|
112
common/src/pattern/slide.rs
Normal file
112
common/src/pattern/slide.rs
Normal 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
|
||||
}
|
66
common/src/pattern/solid.rs
Normal file
66
common/src/pattern/solid.rs
Normal 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())
|
||||
}
|
||||
}
|
92
common/src/pattern/visualizer.rs
Normal file
92
common/src/pattern/visualizer.rs
Normal 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
48
common/src/strip.rs
Normal 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
29
config.toml
Normal 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
3
copy-code.sh
Executable 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
24
entr.sh
@ -2,13 +2,25 @@ CMD="$(cat <<'EOF'
|
||||
set -euo pipefail
|
||||
HEIGHT="$(($(tput lines) - 1))"
|
||||
clear
|
||||
for i in check fmt build clippy; do
|
||||
echo "+ cargo "${i}""
|
||||
cargo --color=always "${i}" |& head -n "${HEIGHT}"
|
||||
done
|
||||
|
||||
function cargo_cmd() {
|
||||
cargo --color=always "${1}" |& head -n "${HEIGHT}"
|
||||
}
|
||||
|
||||
(
|
||||
cd ./webui/liveview-rust/js/
|
||||
npm run-script build
|
||||
) &
|
||||
|
||||
cargo_cmd fmt
|
||||
cargo_cmd check &
|
||||
wait
|
||||
cargo_cmd build
|
||||
cargo_cmd clippy
|
||||
EOF
|
||||
)"
|
||||
{
|
||||
fd -tf -ers
|
||||
printf "%s\n" "Cargo.toml"
|
||||
fd -tf -ers -ehtml
|
||||
fd -tf liveview-dev.js
|
||||
fd -tf Cargo.toml
|
||||
} | entr bash -c "${CMD}"
|
||||
|
8
env.dist
Normal file
8
env.dist
Normal 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=
|
17
homeassistant-mqtt-discovery/Cargo.toml
Normal file
17
homeassistant-mqtt-discovery/Cargo.toml
Normal 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"]
|
67
homeassistant-mqtt-discovery/sample-file-small.txt
Normal file
67
homeassistant-mqtt-discovery/sample-file-small.txt
Normal 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 %}
|
1201
homeassistant-mqtt-discovery/sample-file.txt
Normal file
1201
homeassistant-mqtt-discovery/sample-file.txt
Normal file
File diff suppressed because it is too large
Load Diff
323
homeassistant-mqtt-discovery/src/bin/codegen.rs
Normal file
323
homeassistant-mqtt-discovery/src/bin/codegen.rs
Normal 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()
|
||||
}
|
3515
homeassistant-mqtt-discovery/src/integrations.rs
Normal file
3515
homeassistant-mqtt-discovery/src/integrations.rs
Normal file
File diff suppressed because it is too large
Load Diff
148
homeassistant-mqtt-discovery/src/lib.rs
Normal file
148
homeassistant-mqtt-discovery/src/lib.rs
Normal 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>,
|
||||
}
|
31
homeassistant-mqtt-discovery/src/light.rs
Normal file
31
homeassistant-mqtt-discovery/src/light.rs
Normal 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,
|
||||
// }
|
223
homeassistant-mqtt-discovery/src/tests.rs
Normal file
223
homeassistant-mqtt-discovery/src/tests.rs
Normal 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
|
||||
})
|
||||
);
|
||||
}
|
@ -1,8 +1,7 @@
|
||||
//! Generic Hardware Abstraction Layer, no_std-compatible.
|
||||
|
||||
use crate::encoding::encode_rgb_slice;
|
||||
use alloc::boxed::Box;
|
||||
use alloc::string::String;
|
||||
use alloc::{boxed::Box, string::String};
|
||||
|
||||
/// SPI-device abstraction.
|
||||
pub trait HardwareDev {
|
||||
@ -35,7 +34,7 @@ pub trait Ws28xxAdapter {
|
||||
/// 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`].
|
||||
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(|_| {
|
||||
format!(
|
||||
"Failed to send {} bytes via the specified hardware device. If you use SPI on Linux Perhaps your SPI buffer is too small!\
|
||||
|
@ -1,12 +1,15 @@
|
||||
//! Adapter for SPI-dev on Linux-systems. This requires std.
|
||||
|
||||
use crate::adapter_gen::{HardwareDev, Ws28xxAdapter, Ws28xxGenAdapter};
|
||||
use crate::timings::PI_SPI_HZ;
|
||||
use alloc::boxed::Box;
|
||||
use alloc::string::{String, ToString};
|
||||
use crate::{
|
||||
adapter_gen::{HardwareDev, Ws28xxAdapter, Ws28xxGenAdapter},
|
||||
timings::PI_SPI_HZ,
|
||||
};
|
||||
use alloc::{
|
||||
boxed::Box,
|
||||
string::{String, ToString},
|
||||
};
|
||||
use spidev::{SpiModeFlags, Spidev, SpidevOptions};
|
||||
use std::io;
|
||||
use std::io::Write;
|
||||
use std::{io, io::Write};
|
||||
|
||||
/// Wrapper around Spidev.
|
||||
struct SpiHwAdapterDev(Spidev);
|
||||
@ -14,7 +17,7 @@ struct SpiHwAdapterDev(Spidev);
|
||||
// Implement Hardwareabstraction for device.
|
||||
impl HardwareDev for SpiHwAdapterDev {
|
||||
fn write_all(&mut self, encoded_data: &[u8]) -> Result<(), String> {
|
||||
self.0.write_all(&encoded_data)
|
||||
self.0.write_all(encoded_data)
|
||||
.map_err(|_| {
|
||||
format!(
|
||||
"Failed to send {} bytes via SPI. Perhaps your SPI buffer is too small!\
|
||||
|
5
lunatic-webui/.cargo/config.toml
Normal file
5
lunatic-webui/.cargo/config.toml
Normal file
@ -0,0 +1,5 @@
|
||||
[build]
|
||||
target = "wasm32-wasi"
|
||||
|
||||
[target.wasm32-wasi]
|
||||
runner = "lunatic"
|
11
lunatic-webui/Cargo.toml
Normal file
11
lunatic-webui/Cargo.toml
Normal 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
9
lunatic-webui/index.html
Normal 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
50
lunatic-webui/src/main.rs
Normal 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
14
mqtt/Cargo.toml
Normal 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
430
mqtt/src/lib.rs
Normal 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
3
package-lock.json
generated
@ -1,3 +0,0 @@
|
||||
{
|
||||
"lockfileVersion": 1
|
||||
}
|
@ -1,2 +1,2 @@
|
||||
# unstable_features = true
|
||||
# imports_granularity = "Crate"
|
||||
unstable_features = true
|
||||
imports_granularity = "Crate"
|
||||
|
123
src/main.rs
123
src/main.rs
@ -2,19 +2,19 @@
|
||||
// #![allow(dead_code, unused_imports)]
|
||||
// Enable clippy 'hard mode'
|
||||
#![warn(clippy::all, clippy::pedantic, clippy::nursery)]
|
||||
// Intended behavior (10_f64 as i32)
|
||||
// #![allow(clippy::cast_possible_truncation)]
|
||||
#![allow(
|
||||
// Cannot be fixed
|
||||
clippy::multiple_crate_versions,
|
||||
// Intentional code
|
||||
clippy::map_err_ignore,
|
||||
// "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
|
||||
clippy::implicit_return,
|
||||
// Missing docs is fine
|
||||
clippy::missing_docs_in_private_items,
|
||||
// Many redeclerations that are just pub used
|
||||
clippy::module_name_repetitions,
|
||||
)]
|
||||
// Restriction lints
|
||||
#![warn(
|
||||
@ -28,78 +28,97 @@
|
||||
)]
|
||||
// See https://rust-lang.github.io/rust-clippy/master/index.html for more lints
|
||||
|
||||
mod color;
|
||||
mod errors;
|
||||
mod pattern;
|
||||
mod strip;
|
||||
mod ui;
|
||||
mod webui;
|
||||
use errors::{ProgramError, ProgramResult};
|
||||
use std::io;
|
||||
use std::io::Write;
|
||||
use std::sync::mpsc::{channel, Sender};
|
||||
use std::thread;
|
||||
use clap::Parser;
|
||||
use common::{
|
||||
error::{ProgramError, ProgramResult},
|
||||
strip::Message,
|
||||
};
|
||||
use std::{
|
||||
sync::mpsc::{channel, Sender},
|
||||
thread,
|
||||
time::Duration,
|
||||
};
|
||||
use strip::LedStrip;
|
||||
use ui::console_ui_loop;
|
||||
use tracing::{error, info};
|
||||
|
||||
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
|
||||
let (strip_tx, strip_rx) = channel::<strip::Message>();
|
||||
let (console_strip_tx, webui_strip_tx) = (strip_tx.clone(), strip_tx);
|
||||
let (strip_tx, strip_rx) = channel::<common::strip::Message>();
|
||||
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<()> {
|
||||
let mut strip = LedStrip::new(strip::Config {
|
||||
// I have 89 right now, but start off with 20
|
||||
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)
|
||||
let mut strip = LedStrip::new(config_clone)?;
|
||||
strip.strip_loop(message_tx, &state_tx, &strip_rx, strip_terminated_tx)
|
||||
});
|
||||
|
||||
make_child(message_tx.clone(), move |message_tx| -> ProgramResult<()> {
|
||||
console_ui_loop(message_tx, &console_strip_tx)
|
||||
});
|
||||
// Webui user-interface
|
||||
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<()> {
|
||||
webui::start(message_tx.clone(), webui_strip_tx).map_err(ProgramError::IoError)
|
||||
});
|
||||
// Mqtt user-interface
|
||||
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;
|
||||
loop {
|
||||
std::mem::drop(message_tx);
|
||||
|
||||
'ret: loop {
|
||||
match message_rx.recv() {
|
||||
Ok(errors::Message::String(s)) => println!("\r{}", s),
|
||||
Ok(errors::Message::Error(e)) => println!("\rError!! {:?}", e),
|
||||
Ok(errors::Message::Terminated) => {
|
||||
panic!("A thread terminated")
|
||||
Ok(common::Message::String(s)) => info!(s),
|
||||
Ok(common::Message::Error(e)) => error!("{e:?}"),
|
||||
Ok(common::Message::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) => {
|
||||
return Err(ProgramError::General(format!(
|
||||
"All transmitters hung up! {:?}",
|
||||
e
|
||||
break 'ret Err(ProgramError::General(format!(
|
||||
"All transmitters hung up! {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
|
||||
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) {
|
||||
Ok(()) => message_tx.send(errors::Message::Terminated),
|
||||
Err(e) => message_tx.send(errors::Message::Error(e)),
|
||||
Ok(()) => message_tx.send(common::Message::Terminated),
|
||||
Err(e) => message_tx.send(common::Message::Error(e)),
|
||||
});
|
||||
}
|
||||
|
@ -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() {}
|
||||
// }
|
@ -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()
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
284
src/strip.rs
284
src/strip.rs
@ -1,78 +1,105 @@
|
||||
use crate::color;
|
||||
use crate::errors;
|
||||
use crate::errors::ProgramError;
|
||||
use crate::pattern::{self, Pattern};
|
||||
use std::cmp;
|
||||
use std::ops::Add;
|
||||
use std::process;
|
||||
use std::sync::mpsc::{Receiver, Sender};
|
||||
use std::time::{Duration, Instant};
|
||||
use ws2818_rgb_led_spi_driver::adapter_gen::Ws28xxAdapter;
|
||||
use ws2818_rgb_led_spi_driver::adapter_spi::Ws28xxSpiAdapter;
|
||||
|
||||
/// Maximum number of lights allowed
|
||||
const MAX_NUM_LIGHTS: u16 = 128;
|
||||
/// Default time per tick
|
||||
pub const DEFAULT_TICK_TIME_MS: u64 = 50;
|
||||
/// Minimum time per tick before strip breaks
|
||||
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,
|
||||
}
|
||||
use common::{
|
||||
color,
|
||||
error::ProgramError,
|
||||
pattern::{self, ColorIterator, Pattern},
|
||||
strip::Message,
|
||||
Config, MAX_NUM_LIGHTS, MIN_TICK_TIME,
|
||||
};
|
||||
use std::{
|
||||
cmp,
|
||||
ops::Add,
|
||||
str::FromStr,
|
||||
sync::mpsc::{Receiver, Sender},
|
||||
thread,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tracing::{error, info};
|
||||
use ws2818_rgb_led_spi_driver::{adapter_gen::Ws28xxAdapter, adapter_spi::Ws28xxSpiAdapter};
|
||||
|
||||
#[allow(clippy::module_name_repetitions)]
|
||||
pub struct LedStrip {
|
||||
pub adapter: Box<dyn Ws28xxAdapter>,
|
||||
pub config: Config,
|
||||
pub pattern: Box<dyn Pattern>,
|
||||
pub pattern: Box<dyn Pattern + Send + Sync>,
|
||||
pub state: common::strip::State,
|
||||
}
|
||||
|
||||
impl LedStrip {
|
||||
pub fn new(config: Config) -> Result<Self, ProgramError> {
|
||||
let adapter = Box::new(
|
||||
Ws28xxSpiAdapter::new("/dev/spidev0.0")
|
||||
.map_err(|_| "Cannot start device /dev/spidev0.0!")?,
|
||||
Ws28xxSpiAdapter::new(&config.serial_interface)
|
||||
.map_err(|_| format!("Cannot start device {}!", config.serial_interface))?,
|
||||
);
|
||||
let pattern = Box::new(pattern::Solid::new(color::BLACK));
|
||||
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 {
|
||||
adapter,
|
||||
pattern,
|
||||
config,
|
||||
pattern,
|
||||
state,
|
||||
};
|
||||
ret.set_num_lights(num_lights);
|
||||
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
/// Writes a buffer from the given pattern
|
||||
fn write_buf_from_pattern(&mut self) -> Result<(), ProgramError> {
|
||||
let global_brightness_max = self.config.global_brightness_max;
|
||||
let data = vec![color::BLACK]
|
||||
.iter()
|
||||
.cycle()
|
||||
.take(self.config.shift_lights.into())
|
||||
.chain(
|
||||
self.pattern
|
||||
.get_strip()
|
||||
// .as_slice()
|
||||
.take(self.config.num_lights.into()),
|
||||
)
|
||||
|
||||
let pattern_iterator: ColorIterator = if self.config.mirrored_lights {
|
||||
if self.config.reverse_mirror {
|
||||
Box::new(
|
||||
self.pattern
|
||||
.get_strip()
|
||||
.chain(self.pattern.get_strip().rev()),
|
||||
)
|
||||
} 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(|(r, g, b)| {
|
||||
(
|
||||
@ -89,69 +116,147 @@ impl LedStrip {
|
||||
|
||||
fn set_num_lights(&mut self, num_lights: u16) {
|
||||
if num_lights > MAX_NUM_LIGHTS {
|
||||
println!(
|
||||
"Cannot set lights to {} as it exceeds max of {}",
|
||||
num_lights, MAX_NUM_LIGHTS
|
||||
);
|
||||
error!("Cannot set lights to {num_lights} as it exceeds max of {MAX_NUM_LIGHTS}");
|
||||
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;
|
||||
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(
|
||||
&mut self,
|
||||
message_tx: &Sender<errors::Message>,
|
||||
message_tx: &Sender<common::Message>,
|
||||
state_tx: &Sender<common::strip::State>,
|
||||
rx: &Receiver<Message>,
|
||||
strip_terminated: Sender<()>,
|
||||
) -> Result<(), ProgramError> {
|
||||
let mut exit = false;
|
||||
let _ = state_tx.send(self.state.clone());
|
||||
|
||||
loop {
|
||||
let target_time = Instant::now().add(Duration::from_millis(self.config.tick_time_ms));
|
||||
|
||||
if let Ok(message) = rx.try_recv() {
|
||||
match message {
|
||||
Message::ClearLights => {
|
||||
let mut pat = Box::new(pattern::Solid::new(color::BLACK));
|
||||
if pat.init(self.config.num_lights).is_ok() {
|
||||
let mut pat = Box::new(pattern::Solid::new(&pattern::SolidParams {
|
||||
color: color::BLACK,
|
||||
}));
|
||||
if pat.init(self.pattern_num_lights()).is_ok() {
|
||||
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 {
|
||||
let _ = message_tx.send(errors::Message::String(format!(
|
||||
"Clearing light strip: {:?}",
|
||||
pat
|
||||
let _result = message_tx.send(common::Message::String(format!(
|
||||
"Clearing light strip: {pat:?}"
|
||||
)));
|
||||
}
|
||||
}
|
||||
Message::ChangePattern(pat) => {
|
||||
let mut pat = pat;
|
||||
if pat.init(self.config.num_lights).is_ok() {
|
||||
Message::ChangePattern(mut pat) => match pat.init(self.pattern_num_lights()) {
|
||||
Ok(()) => {
|
||||
if let Err(e) = self.pattern.cleanup() {
|
||||
error!("Error cleaning up old pattern: {e:?}");
|
||||
}
|
||||
|
||||
self.pattern = pat;
|
||||
} else {
|
||||
let _ = message_tx.send(errors::Message::String(format!(
|
||||
"Error initializing pattern: {:?}",
|
||||
pat
|
||||
info!("Changed pattern");
|
||||
self.state.on = true;
|
||||
self.state.pattern = self.pattern.get_name().to_string();
|
||||
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) => {
|
||||
self.set_num_lights(num_lights);
|
||||
}
|
||||
Message::SetTickTime(tick_time_ms) => {
|
||||
if tick_time_ms < MIN_TICK_TIME {
|
||||
let _ = message_tx.send(errors::Message::String(format!(
|
||||
"Error with tick time: {}",
|
||||
tick_time_ms
|
||||
let _result = message_tx.send(common::Message::String(format!(
|
||||
"Error with tick time: {tick_time_ms}"
|
||||
)));
|
||||
}
|
||||
self.config.tick_time_ms = tick_time_ms;
|
||||
info!("Updated tick time to {}", self.config.tick_time_ms);
|
||||
}
|
||||
Message::Quit => {
|
||||
exit = true;
|
||||
let mut pat = pattern::Solid::new(color::BLACK);
|
||||
if pat.init(self.config.num_lights).is_ok() {
|
||||
let mut pat = pattern::Solid::new(&pattern::SolidParams {
|
||||
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);
|
||||
} 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",
|
||||
)));
|
||||
}
|
||||
@ -168,17 +273,28 @@ impl LedStrip {
|
||||
}
|
||||
|
||||
if exit {
|
||||
let _ = message_tx.send(errors::Message::String(String::from(
|
||||
let _result = message_tx.send(common::Message::String(String::from(
|
||||
"Exiting as requested",
|
||||
)));
|
||||
process::exit(0);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
loop {
|
||||
if Instant::now() >= target_time {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Wait out the rest of the time
|
||||
thread::sleep(target_time.saturating_duration_since(Instant::now()));
|
||||
|
||||
// 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(())
|
||||
}
|
||||
}
|
||||
|
59
src/ui.rs
59
src/ui.rs
@ -1,26 +1,26 @@
|
||||
use crate::color::Rgb;
|
||||
use crate::errors::{self, ProgramError, ProgramResult};
|
||||
use crate::pattern::{self, Pattern};
|
||||
use crate::strip;
|
||||
use std::io;
|
||||
use std::io::Write;
|
||||
use std::sync::mpsc::Sender;
|
||||
use common::{
|
||||
color::Rgb,
|
||||
error::{self, ProgramError, ProgramResult},
|
||||
pattern::{self, Pattern},
|
||||
strip,
|
||||
};
|
||||
use std::{io, io::Write, sync::mpsc::Sender};
|
||||
|
||||
pub fn console_ui_loop(
|
||||
message_tx: &Sender<errors::Message>,
|
||||
message_tx: &Sender<common::Message>,
|
||||
strip_tx: &Sender<strip::Message>,
|
||||
) -> ProgramResult<()> {
|
||||
loop {
|
||||
let line = get_line(message_tx, "Command (cfqs)")?;
|
||||
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> {
|
||||
match s
|
||||
.split(char::is_whitespace)
|
||||
.split_ascii_whitespace()
|
||||
.collect::<Vec<&str>>()
|
||||
.as_slice()
|
||||
{
|
||||
@ -33,14 +33,14 @@ fn parse_cmd(strip_tx: &Sender<strip::Message>, s: &str) -> Result<(), String> {
|
||||
b.parse::<u8>()
|
||||
.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] => {
|
||||
let color_value = c
|
||||
.parse::<u8>()
|
||||
.map_err(|_| String::from("Could not parse color"))?;
|
||||
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] => {
|
||||
let color = Rgb(
|
||||
@ -51,50 +51,49 @@ fn parse_cmd(strip_tx: &Sender<strip::Message>, s: &str) -> Result<(), String> {
|
||||
b.parse::<u8>()
|
||||
.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] => {
|
||||
let color_value = c
|
||||
.parse::<u8>()
|
||||
.map_err(|_| String::from("Could not parse color"))?;
|
||||
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] => {
|
||||
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] => {
|
||||
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] => {
|
||||
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] => {
|
||||
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] => {
|
||||
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"))?;
|
||||
change_pattern(strip_tx, Box::new(pattern::MovingRainbow::new(width, ["t", "T"].contains(f), shift)))
|
||||
let skip = s.parse::<u8>().map_err(|_| String::from("Skip could not be parsed"))?;
|
||||
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] => {
|
||||
let left = parse_color(r1, g1, b1)?;
|
||||
let right = parse_color(r2, g2, b2)?;
|
||||
let combined = parse_color(r3, g3, b3)?;
|
||||
change_pattern(strip_tx, Box::new(pattern::Collide::new(left, right, combined)))
|
||||
let left_color = parse_color(r1, g1, b1)?;
|
||||
let right_color = parse_color(r2, g2, b2)?;
|
||||
let conjoined_color = parse_color(r3, g3, b3)?;
|
||||
change_pattern(strip_tx, Box::new(pattern::Collide::new(&pattern::CollideParams {left_color, right_color, conjoined_color})))
|
||||
}
|
||||
["x"] => strip_tx
|
||||
.send(strip::Message::ClearLights)
|
||||
.map_err(|e| e.to_string()),
|
||||
["q"] => {
|
||||
strip_tx.send(strip::Message::Quit).map_err(|e| e.to_string())?;
|
||||
// TODO
|
||||
panic!("i");
|
||||
panic!("Quitting");
|
||||
},
|
||||
["s", n] => strip_tx
|
||||
.send(strip::Message::SetNumLights(
|
||||
@ -124,15 +123,15 @@ fn parse_color(r: &str, g: &str, b: &str) -> Result<Rgb, String> {
|
||||
|
||||
fn change_pattern(
|
||||
strip_tx: &Sender<strip::Message>,
|
||||
pat: Box<dyn Pattern + Send>,
|
||||
pat: Box<dyn Pattern + Send + Sync>,
|
||||
) -> Result<(), String> {
|
||||
strip_tx
|
||||
.send(strip::Message::ChangePattern(pat))
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
fn get_line(message_tx: &Sender<errors::Message>, prompt: &str) -> ProgramResult<String> {
|
||||
let _ = message_tx.send(errors::Message::InputPrompt(String::from(prompt)));
|
||||
fn get_line(message_tx: &Sender<common::Message>, prompt: &str) -> ProgramResult<String> {
|
||||
let _drop = message_tx.send(common::Message::InputPrompt(String::from(prompt)));
|
||||
std::io::stdout()
|
||||
.flush()
|
||||
.map_err(|_| ProgramError::UiError(String::from("Could not flush stdout")))?;
|
||||
|
69
src/webui.rs
69
src/webui.rs
@ -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
962
web/package-lock.json
generated
@ -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
18
webui/Cargo.toml
Normal 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
26
webui/build.rs
Normal 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
12
webui/liveview-rust/.github/FUNDING.yml
vendored
Normal 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
5
webui/liveview-rust/.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
/target
|
||||
**/*.rs.bk
|
||||
Cargo.lock
|
||||
**/node_modules
|
||||
/js/dist
|
11
webui/liveview-rust/CHANGELOG.md
Normal file
11
webui/liveview-rust/CHANGELOG.md
Normal 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
|
18
webui/liveview-rust/Cargo.toml
Normal file
18
webui/liveview-rust/Cargo.toml
Normal 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"] }
|
21
webui/liveview-rust/LICENSE
Normal file
21
webui/liveview-rust/LICENSE
Normal 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.
|
23
webui/liveview-rust/README.md
Normal file
23
webui/liveview-rust/README.md
Normal 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
|
26
webui/liveview-rust/build.rs
Normal file
26
webui/liveview-rust/build.rs
Normal 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");
|
||||
}
|
||||
}
|
182
webui/liveview-rust/js/liveview-dev.js
Normal file
182
webui/liveview-rust/js/liveview-dev.js
Normal 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();
|
2
webui/liveview-rust/js/liveview.js
Normal file
2
webui/liveview-rust/js/liveview.js
Normal file
File diff suppressed because one or more lines are too long
24
webui/liveview-rust/js/liveview.js.LICENSE.txt
Normal file
24
webui/liveview-rust/js/liveview.js.LICENSE.txt
Normal 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
2312
webui/liveview-rust/js/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
29
webui/liveview-rust/js/package.json
Normal file
29
webui/liveview-rust/js/package.json
Normal 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"
|
||||
}
|
||||
}
|
11
webui/liveview-rust/js/webpack.config.js
Normal file
11
webui/liveview-rust/js/webpack.config.js
Normal 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',
|
||||
};
|
800
webui/liveview-rust/js/yarn.lock
Normal file
800
webui/liveview-rust/js/yarn.lock
Normal 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"
|
18
webui/liveview-rust/src/lib.rs
Normal file
18
webui/liveview-rust/src/lib.rs
Normal 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);
|
||||
}
|
||||
}
|
44
webui/liveview-rust/src/live_view.rs
Normal file
44
webui/liveview-rust/src/live_view.rs
Normal 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);
|
||||
}
|
||||
}
|
118
webui/liveview-rust/src/socket.rs
Normal file
118
webui/liveview-rust/src/socket.rs
Normal 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
6
webui/package-lock.json
generated
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "webui",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
172
webui/src/lib.rs
Normal file
172
webui/src/lib.rs
Normal 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
28
webui/src/template.rs
Normal 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
30
webui/templates/app.html
Normal 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>
|
15
webui/templates/control.html
Normal file
15
webui/templates/control.html
Normal 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
427
webui/templates/css/normalize.css
vendored
Normal 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
418
webui/templates/css/skeleton.css
vendored
Normal 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) {}
|
0
web/.gitignore → webui/web/.gitignore
vendored
0
web/.gitignore → webui/web/.gitignore
vendored
4158
webui/web/package-lock.json
generated
Normal file
4158
webui/web/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -11,6 +11,8 @@
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-commonjs": "^17.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-plugin-css-only": "^3.1.0",
|
||||
"rollup-plugin-livereload": "^2.0.0",
|
||||
@ -19,12 +21,12 @@
|
||||
"svelte": "^3.0.0",
|
||||
"svelte-check": "^2.0.0",
|
||||
"svelte-preprocess": "^4.0.0",
|
||||
"@rollup/plugin-typescript": "^8.0.0",
|
||||
"typescript": "^4.0.0",
|
||||
"tslib": "^2.0.0",
|
||||
"@tsconfig/svelte": "^2.0.0"
|
||||
"typescript": "^4.0.0",
|
||||
"webpack-cli": "^4.10.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"sirv-cli": "^1.0.0"
|
||||
"sirv-cli": "^1.0.0",
|
||||
"webpack": "^5.74.0"
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
@ -14,6 +14,7 @@
|
||||
}
|
||||
}
|
||||
</script>
|
||||
hi
|
||||
{#each value as value}
|
||||
<input type="color" bind:value={value} />
|
||||
{/each}
|
@ -32,6 +32,11 @@
|
||||
{name: "width", type: "number", label: "Width", 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];
|
||||
|
||||
@ -87,8 +92,10 @@
|
||||
{#each selectedPattern.formElements as fe}
|
||||
<label for={fe.name}>{fe.label}</label>
|
||||
{#if fe.type === "colors"}
|
||||
colors
|
||||
<Colors bind:value={fe.value}/>
|
||||
{:else}
|
||||
others
|
||||
<input type={fe.type} name={fe.name} on:input={(e) => fe.value = e.target.value} />
|
||||
{/if}
|
||||
{/each}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user