Compare commits

..

No commits in common. "master-old" and "master" have entirely different histories.

49 changed files with 3108 additions and 132 deletions

12
.cargo/config Normal file
View File

@ -0,0 +1,12 @@
# 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"

View File

@ -1,10 +1,19 @@
[package]
name = "aw-lights"
version = "0.1.0"
authors = ["root"]
authors = ["Austen Adler <agadler@austenadler.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
build = "build.rs"
[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"
[build-dependencies]
actix-web-static-files = "3.0"

11
Makefile Normal file
View File

@ -0,0 +1,11 @@
.PHONY: build deploy run
build:
cargo build --target=armv7-unknown-linux-gnueabihf
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
run: deploy
ssh pi ./aw-lights

13
README.adoc Normal file
View File

@ -0,0 +1,13 @@
# aw-lights
----
# Install packages
xbps-install fake-hwclock
ln -s /etc/sv/fake-hwclock/ /var/service/
# Enable SPI
echo 'dtparam=spi=on' >>/boot/config.txt
# Allow big buf for SPI
echo 'options spidev bufsiz=65536' > /etc/modprobe.d/spidev.conf
----

12
build.rs Normal file
View File

@ -0,0 +1,12 @@
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
entr.sh Executable file
View File

@ -0,0 +1,14 @@
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
EOF
)"
{
fd -tf -ers
printf "%s\n" "Cargo.toml"
} | entr bash -c "${CMD}"

1
html/index.html Normal file
View File

@ -0,0 +1 @@
hi

View File

@ -0,0 +1,4 @@
/debug/
/target/
/Cargo.lock
**/*.rs.bk

View File

@ -0,0 +1,27 @@
[package]
name = "ws2818-rgb-led-spi-driver"
description = "Simple, stripped down, educational, no_std-compatible driver for WS28XX (WS2811/12) RGB LEDs. Uses SPI device for timing/clock, and works definitely on Linux/Raspberry Pi."
version = "2.0.0"
authors = ["Philipp Schuster <phip1611@gmail.com>", "Austen Adler <>"]
edition = "2018"
exclude = [
"examples",
".travis.yml",
]
keywords = ["spi", "ws2811", "ws2812", "ws2818", "neopixel"]
categories = ["hardware-support", "no-std"]
readme = "README.md"
license = "MIT"
homepage = "https://github.com/phip1611/ws2818-rgb-led-spi-driver"
repository = "https://github.com/phip1611/ws2818-rgb-led-spi-driver"
documentation = "https://docs.rs/ws2818-rgb-led-spi-driver/"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
# by default this crate needs "std" and uses "spidev" to access SPI device on Linux
default = ["adapter_spidev"]
adapter_spidev = ["spidev"]
[dependencies]
spidev = { version = "0.4.1", optional = true }

View File

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

View File

@ -0,0 +1 @@
This library is almost verbatim from https://github.com/phip1611/ws2818-rgb-led-spi-driver/ with some cleanup.

View File

@ -0,0 +1,68 @@
//! Generic Hardware Abstraction Layer, no_std-compatible.
use crate::encoding::encode_rgb_slice;
use alloc::boxed::Box;
use alloc::string::String;
/// SPI-device abstraction.
pub trait HardwareDev {
fn write_all(&mut self, encoded_data: &[u8]) -> Result<(), String>;
}
pub trait Ws28xxAdapter {
/// Returns a reference to the hardware device.
/// This function only needs to be implemented once in the generic adapter.
fn get_hw_dev(&mut self) -> &mut Box<dyn HardwareDev>;
/// Encodes RGB values and write them via the hardware device to the LEDs. The length of the vector
/// is the number of LEDs you want to write to. *Note* that if you have performance critical
/// applications (like you need a signal on the LEDS on a given time) it's a better idea
/// to encode the data earlier by yourself using `crate::encoding`-module and calling
/// `Ws28xxAdapter::write_encoded_rgb`. Otherwise and if your device is slow the encoding
/// could cost a few microseconds to milliseconds - depending on your amount of data and machine.
fn write_rgb(&mut self, rgb_data: &[(u8, u8, u8)]) -> Result<(), String> {
let encoded_data = encode_rgb_slice(rgb_data);
self.write_encoded_rgb(&encoded_data)
}
/// Clears all LEDs. Sets each to (0, 0, 0).
// fn clear(&mut self, num_leds: usize) {
// let data = vec![(0, 0, 0); num_leds];
// self.write_rgb(&data).expect("Critical data write error!");
// }
/// Directly writes encoded RGB values via hardware device to the LEDs. This method and the encoded data
/// 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)
.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!\
Check https://www.raspberrypi.org/forums/viewtopic.php?p=309582#p309582 for example.",
encoded_data.len()
)}
)
}
}
/// Platform agnostic (generic) adapter that connects your application via your specified
/// hardware interface to your Ws28xx LEDs. *Handle this as something like an abstract class
/// for concrete implementations!* This works in `#[no-std]`-environments.
pub struct Ws28xxGenAdapter {
hw: Box<dyn HardwareDev>,
}
impl Ws28xxGenAdapter {
/// Constructor that stores the hardware device in the adapter.
pub fn new(hw: Box<dyn HardwareDev>) -> Self {
Self { hw }
}
}
// Implement the getter for the hardware device.
impl Ws28xxAdapter for Ws28xxGenAdapter {
fn get_hw_dev(&mut self) -> &mut Box<dyn HardwareDev> {
&mut self.hw
}
}

View File

@ -0,0 +1,80 @@
//! 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 spidev::{SpiModeFlags, Spidev, SpidevOptions};
use std::io;
use std::io::Write;
/// Wrapper around Spidev.
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)
.map_err(|_| {
format!(
"Failed to send {} bytes via SPI. Perhaps your SPI buffer is too small!\
Check https://www.raspberrypi.org/forums/viewtopic.php?p=309582#p309582 for example.",
encoded_data.len()
)
})
}
}
impl SpiHwAdapterDev {
/// Connects your application with the SPI-device of your device.
/// This uses the `spidev`-crate. Returns a new adapter object
/// for the Ws28xx LEDs.
///
/// * `dev` - Device name. Probably "/dev/spidev0.0" if available.
///
/// Fails if connection to SPI can't be established.
pub fn new(dev: &str) -> io::Result<Self> {
let mut spi = Spidev::open(dev)?;
let options = SpidevOptions::new()
.bits_per_word(8)
// According to https://www.raspberrypi.org/documentation/hardware/raspberrypi/spi/README.md
.max_speed_hz(PI_SPI_HZ)
.mode(SpiModeFlags::SPI_MODE_0)
.build();
spi.configure(&options)?;
Ok(Self(spi))
}
}
/// Adapter that connects your application via SPI to your Ws28xx LEDs.
/// This requires an SPI device on your machine. This doesn't work
/// with `#[no-std]`.
pub struct Ws28xxSpiAdapter {
gen: Ws28xxGenAdapter,
}
impl Ws28xxSpiAdapter {
/// Connects your application with the SPI-device of your device.
/// This uses the `spidev`-crate. Returns a new adapter object
/// for the Ws28xx LEDs.
///
/// * `dev` - Device name. Probably "/dev/spidev0.0" if available.
///
/// Fails if connection to SPI can't be established.
pub fn new(dev: &str) -> Result<Self, String> {
let spi = SpiHwAdapterDev::new(dev).map_err(|err| err.to_string())?;
let spi = Box::from(spi);
let gen = Ws28xxGenAdapter::new(spi);
Ok(Self { gen })
}
}
impl Ws28xxAdapter for Ws28xxSpiAdapter {
fn get_hw_dev(&mut self) -> &mut Box<dyn HardwareDev> {
// forward to generic adapter
// todo this is not the best code design because this requires
// each sub adapter (like a sub class in OOP) to implement
// this manually..
self.gen.get_hw_dev()
}
}

View File

@ -0,0 +1,48 @@
use crate::timings::encoding::{
SPI_BYTES_PER_DATA_BIT, WS2812_LOGICAL_ONE_BYTES, WS2812_LOGICAL_ZERO_BYTES,
};
use alloc::vec::Vec;
const COLORS: usize = 3;
const BITS_PER_COLOR: usize = 8;
/// The number of bytes that must be send over SPI to transfer the data of a single RGB pixel.
pub const SPI_BYTES_PER_RGB_PIXEL: usize = COLORS * BITS_PER_COLOR * SPI_BYTES_PER_DATA_BIT;
/// Encodes RGB-Values to the bytes that must be transferred via SPI MOSI.
/// These SPI bytes represent the logical zeros and ones for WS2818.
/// This counts in the constraints that come from [`crate::timings`]-module.
/// Due to the specification the data is send this way:
/// G7..G0,R7..R0,B7..B0
///
/// The resulting is [`SPI_BYTES_PER_RGB_PIXEL`] bytes long.
pub fn encode_rgb(r: u8, g: u8, b: u8) -> [u8; SPI_BYTES_PER_RGB_PIXEL] {
let mut spi_bytes: [u8; SPI_BYTES_PER_RGB_PIXEL] = [0; SPI_BYTES_PER_RGB_PIXEL];
let mut spi_bytes_i = 0;
let mut grb = [g, r, b]; // order specified by specification
for color_bits in grb.iter_mut() {
for _ in 0..8 {
// for each bit of our color; starting with most significant
// we encode now one color bit in two spi bytes (for proper timings along with our frequency)
if 0b10000000 & *color_bits == 0 {
spi_bytes[spi_bytes_i] = WS2812_LOGICAL_ZERO_BYTES[0];
spi_bytes[spi_bytes_i + 1] = WS2812_LOGICAL_ZERO_BYTES[1];
} else {
spi_bytes[spi_bytes_i] = WS2812_LOGICAL_ONE_BYTES[0];
spi_bytes[spi_bytes_i + 1] = WS2812_LOGICAL_ONE_BYTES[1];
}
*color_bits <<= 1;
spi_bytes_i += 2; // update array index;
}
}
debug_assert_eq!(spi_bytes_i, SPI_BYTES_PER_RGB_PIXEL);
spi_bytes
}
/// Encodes multiple RGB values in a slice. Uses [`encode_rgb`] for each value.
pub fn encode_rgb_slice(data: &[(u8, u8, u8)]) -> Vec<u8> {
let mut bytes = vec![];
data.iter()
.for_each(|rgb| bytes.extend_from_slice(&encode_rgb(rgb.0, rgb.1, rgb.2)));
bytes
}

View File

@ -0,0 +1,14 @@
#![no_std]
#[cfg(feature = "adapter_spidev")]
extern crate std;
#[macro_use]
extern crate alloc;
pub mod adapter_gen; // generic [no_std] hardware abstraction
#[cfg(feature = "adapter_spidev")]
pub mod adapter_spi; // specific [std]-implementation
pub mod encoding;
pub mod timings;

View File

@ -0,0 +1,48 @@
/// SPI Frequency
pub const PI_SPI_HZ: u32 = 15_600_000;
// 15.6 Mhz, see https://www.raspberrypi.org/documentation/hardware/raspberrypi/spi/README.md
// this means 1 / 15_600_000 * 1E9 ns/cycle => 64ns / cycle => 15.6 MBit/s
//
// See data sheet: https://cdn-shop.adafruit.com/datasheets/WS2812.pdf
//
// Timings of WS2818:
//
// pub const _T0H_NS: u64 = 350; // +-150ns tolerance
// pub const _T0L_NS: u64 = 800; // +-150ns tolerance
// pub const _T1H_NS: u64 = 700; // +-150ns tolerance
// pub const _T1L_NS: u64 = 600; // +-150ns tolerance
// pub const _TRESET: u64 = 50_000; // >50 µs
//
// One Wire Protocol on WS2812 requires the
// - "logical 0 Bit" to be:
// - T0H_NS +-150ns to be high
// - T0L_NS +-150ns to be low (most of the time; at the end)
// - "logical 1 Bit" to be:
// - T1H_NS +-150ns to be high (most of the time; at the beginning)
// - T1L_NS +-150ns to be low
//
// T0H_NS = 350ns +-150ns => 1_1111 ( 5 bits * 64ns per bit ~ 320ns)
// T0L_NS = 800ns +-150ns => 000_0000_0000 (11 bits * 64ns per bit ~ 704ns)
//
// T1H_NS = 700ns +-150ns => 1_1111_1111 (9 bits * 64ns per bit ~ 576ns)
// T1L_NS = 600ns +-150ns => 000_0000 (7 bits * 64ns per bit ~ 448ns)
//
// => !! we encode one data bit in two SPI byte for the proper timings !!
pub mod encoding {
/// How many SPI bytes must be send for a single data bit.
/// This number of bytes result in one logical zero or one
/// on WS2818 LED.
pub const SPI_BYTES_PER_DATA_BIT: usize = 2;
/// See code comments above where this value comes from!
/// These are the bits to send via SPI MOSI that represent a logical 0
/// on WS2812 RGB LED interface. Frequency + length results in the proper timings.
pub const WS2812_LOGICAL_ZERO_BYTES: [u8; SPI_BYTES_PER_DATA_BIT] = [0b1111_1000, 0b0000_0000];
/// See code comments above where this value comes from!
/// These are the bits to send via SPI MOSI that represent a logical 1
/// on WS2812 RGB LED interface. Frequency + length results in the proper timings.
pub const WS2812_LOGICAL_ONE_BYTES: [u8; SPI_BYTES_PER_DATA_BIT] = [0b1111_1111, 0b1000_0000];
}

3
package-lock.json generated Normal file
View File

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

2
rustfmt.toml Normal file
View File

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

132
src/color.rs Normal file
View File

@ -0,0 +1,132 @@
use serde::Deserialize;
use serde::Serialize;
use std::num::ParseIntError;
use std::str::FromStr;
#[derive(Copy, Clone, Debug, PartialEq, Deserialize, Serialize)]
pub struct Rgb(pub u8, pub u8, pub u8);
impl Rgb {
pub const fn to_tuple(self) -> (u8, u8, u8) {
(self.0, self.1, self.2)
}
pub fn to_float_tuple(self) -> (f32, f32, f32) {
(f32::from(self.0), f32::from(self.1), f32::from(self.2))
}
// pub fn to_gamma_corrected_tuple(&self) -> (u8, u8, u8) {
// (
// GAMMA_CORRECT[self.0 as usize],
// GAMMA_CORRECT[self.1 as usize],
// GAMMA_CORRECT[self.2 as usize],
// )
// }
}
impl FromStr for Rgb {
type Err = ParseIntError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let [r, g, b] = s.split(' ').collect::<Vec<&str>>().as_slice() {
return Ok(Self(r.parse::<u8>()?, g.parse::<u8>()?, b.parse::<u8>()?));
}
hex::decode(
s.trim_start_matches('#')
.trim_start_matches("0x")
.trim_start_matches("0X"),
)
.map_err(|_| ())
.and_then(|v| {
Ok(Self(
*v.get(0).ok_or(())?,
*v.get(1).ok_or(())?,
*v.get(2).ok_or(())?,
))
})
.or_else(|_| {
// TODO: Return a proper error here
let color_value = s.parse::<u8>()?;
Ok(Self(color_value, color_value, color_value))
})
}
}
#[allow(dead_code)]
pub const BLACK: Rgb = Rgb(0, 0, 0);
#[allow(dead_code)]
pub const WHITE: Rgb = Rgb(255, 255, 255);
// Corrections:
// R: 0x10, G: 0x08, B: 0x00
// const GAMMA_CORRECT: [u8; 256] = [
// 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1,
// 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 5, 5, 5,
// 5, 6, 6, 6, 6, 7, 7, 7, 7, 8, 8, 8, 9, 9, 9, 10, 10, 10, 11, 11, 11, 12, 12, 13, 13, 13, 14,
// 14, 15, 15, 16, 16, 17, 17, 18, 18, 19, 19, 20, 20, 21, 21, 22, 22, 23, 24, 24, 25, 25, 26, 27,
// 27, 28, 29, 29, 30, 31, 32, 32, 33, 34, 35, 35, 36, 37, 38, 39, 39, 40, 41, 42, 43, 44, 45, 46,
// 47, 48, 49, 50, 50, 51, 52, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 66, 67, 68, 69, 70, 72,
// 73, 74, 75, 77, 78, 79, 81, 82, 83, 85, 86, 87, 89, 90, 92, 93, 95, 96, 98, 99, 101, 102, 104,
// 105, 107, 109, 110, 112, 114, 115, 117, 119, 120, 122, 124, 126, 127, 129, 131, 133, 135, 137,
// 138, 140, 142, 144, 146, 148, 150, 152, 154, 156, 158, 160, 162, 164, 167, 169, 171, 173, 175,
// 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
Rgb(255, 255, 0), // Y
Rgb(0, 255, 0), // G
Rgb(0, 0, 255), // B
Rgb(75, 0, 130), // I
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();
// 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)
}
/// Builds a color ramp of length `length` of the (exclusive) bounds of `from_color` to `to_color`
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
}
pub fn min_with_factor(at_least: u16, factor: u16) -> Result<u16, ()> {
Ok(at_least
.checked_sub(1)
.ok_or(())?
.div_euclid(factor)
.checked_add(1)
.ok_or(())?
.checked_mul(factor)
.ok_or(())?)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_to_tuple() {
assert_eq!(Rgb(213, 14, 0).to_tuple(), (213, 14, 0));
assert_eq!(WHITE.to_tuple(), (255, 255, 255));
assert_eq!(BLACK.to_tuple(), (0, 0, 0));
}
#[test]
fn test_black() {
// Most important because this will blank out the strip
assert_eq!(BLACK, Rgb(0, 0, 0));
}
}

50
src/errors.rs Normal file
View File

@ -0,0 +1,50 @@
use core::any::Any;
use std::fmt;
use std::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),
}
impl From<String> for ProgramError {
fn from(e: String) -> Self {
Self::General(e)
}
}
impl From<&str> for ProgramError {
fn from(e: &str) -> Self {
Self::General(e.to_string())
}
}
impl From<Box<dyn Any + Send>> for ProgramError {
fn from(e: Box<dyn Any + Send>) -> Self {
Self::Boxed(e)
}
}
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),
}
}
}

View File

@ -1,16 +1,105 @@
mod wire_protocol;
use wire_protocol::{LedPanel, RGB};
// Easy mode
// #![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,
// This is fine
clippy::implicit_return,
// Missing docs is fine
clippy::missing_docs_in_private_items,
)]
// Restriction lints
#![warn(
clippy::else_if_without_else,
// Obviously bad
clippy::indexing_slicing,
clippy::integer_arithmetic,
clippy::integer_division,
clippy::expect_used,
clippy::unwrap_used,
)]
// See https://rust-lang.github.io/rust-clippy/master/index.html for more lints
fn main() {
let mut panel = LedPanel::new(5);
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 strip::LedStrip;
use ui::console_ui_loop;
let colors: Vec<RGB> = vec![
RGB(0x00, 0x00, 0x00),
RGB(0x00, 0x00, 0x0F),
RGB(0x00, 0x00, 0xFF),
RGB(0x00, 0x00, 0x00),
RGB(0x00, 0x00, 0x00),
];
fn main() -> ProgramResult<()> {
// 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);
panel.set_leds(&colors);
let (message_tx, message_rx) = channel::<errors::Message>();
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)
});
make_child(message_tx.clone(), move |message_tx| -> ProgramResult<()> {
console_ui_loop(message_tx, &console_strip_tx)
});
make_child(message_tx, move |message_tx| -> ProgramResult<()> {
webui::start(message_tx.clone(), webui_strip_tx).map_err(ProgramError::IoError)
});
let mut input_prompt: Option<String> = None;
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(errors::Message::InputPrompt(i)) => input_prompt = Some(i),
Err(e) => {
return 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)
where
F: FnOnce(&Sender<errors::Message>) -> ProgramResult<()> + std::marker::Send,
{
thread::spawn(move || match f(&message_tx) {
Ok(()) => message_tx.send(errors::Message::Terminated),
Err(e) => message_tx.send(errors::Message::Error(e)),
});
}

89
src/pattern.rs Normal file
View File

@ -0,0 +1,89 @@
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() {}
// }

155
src/pattern/collide.rs Normal file
View File

@ -0,0 +1,155 @@
use super::Pattern;
use crate::color::{self, Rgb};
use std::collections::vec_deque;
use std::collections::VecDeque;
#[derive(Clone, Debug)]
pub struct Collide {
num_lights: u16,
left_color: Rgb,
right_color: Rgb,
conjoined_color: Rgb,
step: u16,
step_max: u16,
conjoined_bounds: (u16, u16),
offset_max: u16,
offset: u16,
previous_offset: u16,
increase_offset: bool,
lights_buf: VecDeque<Rgb>,
}
impl Collide {
pub fn new(left_color: Rgb, right_color: Rgb, conjoined_color: Rgb) -> Self {
Self {
num_lights: 0,
left_color,
right_color,
conjoined_color,
step: 0,
step_max: 0,
conjoined_bounds: (0, 0),
offset_max: 0,
previous_offset: 0,
offset: 0,
increase_offset: true,
lights_buf: VecDeque::new(),
}
}
}
impl Pattern for Collide {
fn step(&mut self) -> Result<bool, ()> {
// TODO: Better range storage
// Set the left and right color
let colors =
if ((self.conjoined_bounds.0)..=(self.conjoined_bounds.1)).contains(&(self.step)) {
(self.conjoined_color, self.conjoined_color)
} else {
(self.left_color, self.right_color)
};
// Turn off the previous LED
*self
.lights_buf
.get_mut(usize::from(self.previous_offset))
.ok_or(())? = color::BLACK;
if self.previous_offset
!= self
.num_lights
.saturating_sub(1)
.saturating_sub(self.previous_offset)
{
*self
.lights_buf
.get_mut(usize::from(
self.num_lights
.saturating_sub(1)
.saturating_sub(self.previous_offset),
))
.ok_or(())? = color::BLACK;
}
// Set the color of the current offset
*self
.lights_buf
.get_mut(usize::from(self.offset))
.ok_or(())? = colors.0;
if self.offset
!= self
.num_lights
.saturating_sub(1)
.saturating_sub(self.offset)
{
*self
.lights_buf
.get_mut(usize::from(
self.num_lights
.saturating_sub(1)
.saturating_sub(self.offset),
))
.ok_or(())? = colors.1;
}
self.previous_offset = self.offset;
if self.offset == 0 {
self.increase_offset = true;
self.offset = self.offset.saturating_add(1);
} else if self.offset == self.offset_max {
self.increase_offset = false;
self.offset = self.offset.saturating_sub(1);
} else if self.increase_offset {
self.offset = self.offset.saturating_add(1);
// The previous condition was false, so subtract
} else {
self.offset = self.offset.saturating_sub(1);
}
self.step = self.step.saturating_add(1).rem_euclid(self.step_max);
Ok(true)
}
fn init(&mut self, num_lights: u16) -> Result<(), ()> {
// Reset changing parameters
self.step = 0;
self.offset = 0;
self.previous_offset = 0;
self.num_lights = num_lights;
self.increase_offset = true;
if self.num_lights < 3 {
return Err(());
}
self.lights_buf = VecDeque::from(vec![color::BLACK; self.num_lights.into()]);
if self.num_lights.rem_euclid(2) == 0 {
self.conjoined_bounds = (
self.num_lights
.div_euclid(2)
.saturating_add(1_u16.saturating_sub(1)),
self.num_lights
.saturating_add(1)
.saturating_mul(3)
.div_euclid(2)
.saturating_sub(4),
);
} else {
self.conjoined_bounds = (
self.num_lights
.div_euclid(2)
.saturating_add(1_u16.saturating_sub(1)),
self.num_lights
.saturating_add(1)
.saturating_mul(3)
.div_euclid(2)
.saturating_sub(3),
);
}
self.offset_max = self.num_lights.saturating_sub(1).div_euclid(2);
self.step_max = self
.num_lights
.saturating_sub(1)
.div_euclid(2)
.saturating_mul(4);
Ok(())
}
fn get_strip(&self) -> vec_deque::Iter<Rgb> {
self.lights_buf.iter()
}
}

57
src/pattern/fade.rs Normal file
View File

@ -0,0 +1,57 @@
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()
}
}

61
src/pattern/flashing.rs Normal file
View File

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

View File

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

View File

@ -0,0 +1,77 @@
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()
}
}

96
src/pattern/orb.rs Normal file
View File

@ -0,0 +1,96 @@
use super::Pattern;
use crate::color::{self, Rgb};
use std::collections::vec_deque;
use std::collections::VecDeque;
use std::iter;
#[derive(Clone, Debug)]
pub struct Orb {
/// Buffer to manage the lights
lights_buf: VecDeque<Rgb>,
/// The color of the orb
color: Rgb,
/// The width of the center of the orb
center_width: u8,
/// The width of each side's backoff (fadeout)
backoff_width: u8,
/// The total width of the orb, equal to center_width + 2*backoff_width
total_width: u8,
/// True if the orb should bounce from left to right, otherwise it will wrap around
bounces: bool,
/// The state of the program
step: usize,
/// The maximum number of steps for a given direction. The orb will turn around after this many steps
step_max: usize,
/// Direction of the orb. This can switch if `bounces` is true
direction: bool,
}
impl Orb {
pub fn new(color: Rgb, center_width: u8, backoff_width: u8) -> Self {
Self {
lights_buf: VecDeque::new(),
color,
center_width,
backoff_width,
total_width: center_width.saturating_add(backoff_width.saturating_mul(2)),
bounces: false,
step: 0,
step_max: 0,
direction: true,
}
}
}
impl Pattern for Orb {
fn step(&mut self) -> Result<bool, ()> {
if !self.bounces {
// If we don't bounce, then just wrap and we're done
self.lights_buf.rotate_right(1);
return Ok(true);
}
if self.direction {
self.lights_buf.rotate_right(1);
self.step = self.step.saturating_add(1);
} else {
self.lights_buf.rotate_left(1);
self.step = self.step.saturating_sub(1);
}
if self.step == self.step_max || self.step == 0 {
self.direction = !self.direction;
// } else if self.lights_buf - self.step == self.total_width {
// self.direction = true;
}
Ok(true)
}
fn init(&mut self, num_lights: u16) -> Result<(), ()> {
if num_lights < 1 {
return Err(());
}
self.step = 0;
let other_color = color::BLACK;
let ramp = color::build_ramp(other_color, self.color, self.backoff_width.into());
self.lights_buf = iter::empty::<Rgb>()
.chain(ramp.clone().into_iter())
.chain(iter::repeat(self.color).take(self.center_width.into()))
.chain(ramp.into_iter().rev())
.chain(
iter::repeat(other_color)
.take(num_lights.saturating_sub(self.total_width.into()).into()),
)
.collect();
self.step_max = self
.lights_buf
.len()
.checked_sub(self.total_width.into())
.unwrap_or_else(|| self.lights_buf.len());
Ok(())
}
fn get_strip(&self) -> vec_deque::Iter<Rgb> {
self.lights_buf.iter()
}
}

39
src/pattern/solid.rs Normal file
View File

@ -0,0 +1,39 @@
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()
}
}

184
src/strip.rs Normal file
View File

@ -0,0 +1,184 @@
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,
}
#[allow(clippy::module_name_repetitions)]
pub struct LedStrip {
pub adapter: Box<dyn Ws28xxAdapter>,
pub config: Config,
pub pattern: Box<dyn Pattern>,
}
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!")?,
);
let pattern = Box::new(pattern::Solid::new(color::BLACK));
let num_lights = config.num_lights;
let mut ret = Self {
adapter,
pattern,
config,
};
ret.set_num_lights(num_lights);
Ok(ret)
}
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()),
)
.map(|c| c.to_tuple())
.map(|(r, g, b)| {
(
cmp::min(r, global_brightness_max),
cmp::min(g, global_brightness_max),
cmp::min(b, global_brightness_max),
)
})
// TODO: Do not re-collect as u8s
.collect::<Vec<(u8, u8, u8)>>();
self.adapter.write_rgb(data.as_slice())?;
Ok(())
}
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
);
return;
}
if self.pattern.init(num_lights).is_ok() {
self.config.num_lights = num_lights;
}
}
pub fn strip_loop(
&mut self,
message_tx: &Sender<errors::Message>,
rx: &Receiver<Message>,
) -> Result<(), ProgramError> {
let mut exit = false;
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() {
self.pattern = pat;
} else {
let _ = message_tx.send(errors::Message::String(format!(
"Clearing light strip: {:?}",
pat
)));
}
}
Message::ChangePattern(pat) => {
let mut pat = pat;
if pat.init(self.config.num_lights).is_ok() {
self.pattern = pat;
} else {
let _ = message_tx.send(errors::Message::String(format!(
"Error initializing pattern: {:?}",
pat
)));
}
}
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
)));
}
self.config.tick_time_ms = tick_time_ms;
}
Message::Quit => {
exit = true;
let mut pat = pattern::Solid::new(color::BLACK);
if pat.init(self.config.num_lights).is_ok() {
self.pattern = Box::new(pat);
} else {
let _ = message_tx.send(errors::Message::String(String::from(
"Could not construct clear pattern",
)));
}
}
}
}
if self
.pattern
.step()
.map_err(|_| ProgramError::General(String::from("Pattern step failure")))?
{
self.write_buf_from_pattern()?;
}
if exit {
let _ = message_tx.send(errors::Message::String(String::from(
"Exiting as requested",
)));
process::exit(0);
}
loop {
if Instant::now() >= target_time {
break;
}
}
}
}
}

144
src/ui.rs Normal file
View File

@ -0,0 +1,144 @@
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;
pub fn console_ui_loop(
message_tx: &Sender<errors::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);
}
}
}
fn parse_cmd(strip_tx: &Sender<strip::Message>, s: &str) -> Result<(), String> {
match s
.split(char::is_whitespace)
.collect::<Vec<&str>>()
.as_slice()
{
["f", r, g, b] => {
let color = Rgb(
r.parse::<u8>()
.map_err(|_| String::from("Red could not be parsed"))?,
g.parse::<u8>()
.map_err(|_| String::from("Green could not be parsed"))?,
b.parse::<u8>()
.map_err(|_| String::from("Blue could not be parsed"))?,
);
change_pattern(strip_tx, Box::new(pattern::Fade::new(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)))
}
["m", r, g, b] => {
let color = Rgb(
r.parse::<u8>()
.map_err(|_| String::from("Red could not be parsed"))?,
g.parse::<u8>()
.map_err(|_| String::from("Green could not be parsed"))?,
b.parse::<u8>()
.map_err(|_| String::from("Blue could not be parsed"))?,
);
change_pattern(strip_tx, Box::new(pattern::MovingPixel::new(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)))
},
["c", r, g, b] => {
let color = parse_color(r, g, b)?;
change_pattern(strip_tx, Box::new(pattern::Solid::new(color)))
}
["c", c] => {
let color = parse_color(c, c, c)?;
change_pattern(strip_tx, Box::new(pattern::Solid::new(color)))
},
["r"] => change_pattern(strip_tx, Box::new(pattern::MovingRainbow::new(4, true, 0))),
["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)))
},
["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)))
},
["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)))
},
["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)))
}
["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");
},
["s", n] => strip_tx
.send(strip::Message::SetNumLights(
n.parse::<u16>()
.map_err(|_| String::from("Could not parse light count"))?,
))
.map_err(|e| e.to_string()),
["t", n] => strip_tx.send(strip::Message::SetTickTime(
n.parse::<u64>()
.map_err(|_| String::from("Could not parse light count"))?,
))
.map_err(|e| e.to_string()),
_ => Err(String::from("Unknown command. Available commands: solidColor movingRainbow Bouncing Fade clear(X) Setnumlights setTicktime")),
}
}
fn parse_color(r: &str, g: &str, b: &str) -> Result<Rgb, String> {
Ok(Rgb(
r.parse::<u8>()
.map_err(|_| String::from("Red could not be parsed"))?,
g.parse::<u8>()
.map_err(|_| String::from("Green could not be parsed"))?,
b.parse::<u8>()
.map_err(|_| String::from("Blue could not be parsed"))?,
))
}
fn change_pattern(
strip_tx: &Sender<strip::Message>,
pat: Box<dyn Pattern + Send>,
) -> 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)));
std::io::stdout()
.flush()
.map_err(|_| ProgramError::UiError(String::from("Could not flush stdout")))?;
let mut input_text = String::new();
io::stdin()
.read_line(&mut input_text)
.map_err(|_| ProgramError::UiError(String::from("Could not read from stdin")))?;
Ok(input_text.trim().to_string())
}

69
src/webui.rs Normal file
View File

@ -0,0 +1,69 @@
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
}

View File

@ -1,117 +0,0 @@
use rppal::spi::{Bus, Mode, SlaveSelect, Spi};
pub struct LedPanel {
/// stores [g, r, b] for each led (as opposed to the normal RGB)
buffer: Vec<u8>,
spi: Spi,
num_leds: u32,
}
/// Stores color as a tuple of (Red, Green, Blue)
pub struct RGB(pub u8, pub u8, pub u8);
impl LedPanel {
pub fn new(num_leds: u32) -> LedPanel {
let bus = Bus::Spi0;
let mode = Mode::Mode0;
let slave = SlaveSelect::Ss0;
let clock_speed = 2_400_000;
let buffer = Vec::new();
LedPanel {
buffer,
spi: Spi::new(bus, slave, clock_speed, mode).unwrap(),
num_leds,
}
}
fn write(&mut self) {
let output = self
.buffer
.drain(..)
.flat_map(|val| LedPanel::to_ws2812b_bytes(val).to_vec())
.collect::<Vec<u8>>();
self.spi.write(&output).unwrap();
}
pub fn set_leds(&mut self, hex_codes: &[RGB]) {
hex_codes.iter().for_each(|hex_code| {
// Swapping here from RGB to the GRB expected by the LED panel
self.buffer
.extend_from_slice(&[hex_code.1, hex_code.0, hex_code.2]);
});
self.write();
}
// Turns all LEDs off and clears buffer
pub fn clear_all_leds(&mut self) {
self.buffer.clear();
let mut clear_codes = vec![0; (self.num_leds * 3) as usize];
self.buffer.append(&mut clear_codes);
self.write();
}
/**
* Maps one byte to three bytes per the WS2821B protocol
* 1 -> 110
* 0 -> 100
*/
pub fn to_ws2812b_bytes(byte: u8) -> [u8; 3] {
let mut ret: u32 = 0b0;
println!("Adding byte {:#b}", byte);
// Each bit in byte
// for i in (0..8).rev() // Correct order
for i in 0..8 {
// Reverse order
println!(" Pushing bit {:} ({:})", (u32::from(byte) >> i) & 0b1, i);
println!(" Now {:#b}", ret);
// 0b -> 0b00
ret <<= 2;
// 0b00 -> 0b1<bit>
println!(" Now {:#b}", ret);
ret |= 0b10 | ((u32::from(byte) >> i) & 0b1);
println!(" Now {:#b}", ret);
// 0b1<bit> -> 0b1<bit>0
ret <<= 1;
println!(" Now {:#b}", ret);
}
let bytes: [u8; 4] = ret.to_be_bytes();
[bytes[1], bytes[2], bytes[3]]
}
}
#[cfg(test)]
mod tests {
use super::LedPanel;
#[test]
fn convert_0() {
assert_eq!(
vec![0b10010010, 0b01001001, 0b00100100],
LedPanel::to_ws2812b_bytes(0b00000000)
);
}
#[test]
fn convert_number() {
// assert_eq!(vec![0b10011010, 0b01101001, 0b10110100], LedPanel::to_ws2812b_bytes(0b01010110)); // Regular
assert_eq!(
vec![0b10011011, 0b01001101, 0b00110100],
LedPanel::to_ws2812b_bytes(0b01010110)
); // Reversed
}
#[test]
fn convert_max() {
assert_eq!(
vec![0b11011011, 0b01101101, 0b10110110],
LedPanel::to_ws2812b_bytes(0b11111111)
);
}
}

4
web/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
/node_modules/
/public/build/
.DS_Store

3
web/.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"recommendations": ["svelte.svelte-vscode"]
}

105
web/README.md Normal file
View File

@ -0,0 +1,105 @@
*Looking for a shareable component template? Go here --> [sveltejs/component-template](https://github.com/sveltejs/component-template)*
---
# svelte app
This is a project template for [Svelte](https://svelte.dev) apps. It lives at https://github.com/sveltejs/template.
To create a new project based on this template using [degit](https://github.com/Rich-Harris/degit):
```bash
npx degit sveltejs/template svelte-app
cd svelte-app
```
*Note that you will need to have [Node.js](https://nodejs.org) installed.*
## Get started
Install the dependencies...
```bash
cd svelte-app
npm install
```
...then start [Rollup](https://rollupjs.org):
```bash
npm run dev
```
Navigate to [localhost:5000](http://localhost:5000). You should see your app running. Edit a component file in `src`, save it, and reload the page to see your changes.
By default, the server will only respond to requests from localhost. To allow connections from other computers, edit the `sirv` commands in package.json to include the option `--host 0.0.0.0`.
If you're using [Visual Studio Code](https://code.visualstudio.com/) we recommend installing the official extension [Svelte for VS Code](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode). If you are using other editors you may need to install a plugin in order to get syntax highlighting and intellisense.
## Building and running in production mode
To create an optimised version of the app:
```bash
npm run build
```
You can run the newly built app with `npm run start`. This uses [sirv](https://github.com/lukeed/sirv), which is included in your package.json's `dependencies` so that the app will work when you deploy to platforms like [Heroku](https://heroku.com).
## Single-page app mode
By default, sirv will only respond to requests that match files in `public`. This is to maximise compatibility with static fileservers, allowing you to deploy your app anywhere.
If you're building a single-page app (SPA) with multiple routes, sirv needs to be able to respond to requests for *any* path. You can make it so by editing the `"start"` command in package.json:
```js
"start": "sirv public --single"
```
## Using TypeScript
This template comes with a script to set up a TypeScript development environment, you can run it immediately after cloning the template with:
```bash
node scripts/setupTypeScript.js
```
Or remove the script via:
```bash
rm scripts/setupTypeScript.js
```
## Deploying to the web
### With [Vercel](https://vercel.com)
Install `vercel` if you haven't already:
```bash
npm install -g vercel
```
Then, from within your project folder:
```bash
cd public
vercel deploy --name my-project
```
### With [surge](https://surge.sh/)
Install `surge` if you haven't already:
```bash
npm install -g surge
```
Then, from within your project folder:
```bash
npm run build
surge public my-project.surge.sh
```

962
web/package-lock.json generated Normal file
View File

@ -0,0 +1,962 @@
{
"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
}
}
}

30
web/package.json Normal file
View File

@ -0,0 +1,30 @@
{
"name": "svelte-app",
"version": "1.0.0",
"private": true,
"scripts": {
"build": "rollup -c",
"dev": "rollup -c -w",
"start": "sirv public --no-clear",
"check": "svelte-check --tsconfig ./tsconfig.json"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^17.0.0",
"@rollup/plugin-node-resolve": "^11.0.0",
"rollup": "^2.3.4",
"rollup-plugin-css-only": "^3.1.0",
"rollup-plugin-livereload": "^2.0.0",
"rollup-plugin-svelte": "^7.0.0",
"rollup-plugin-terser": "^7.0.0",
"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"
},
"dependencies": {
"sirv-cli": "^1.0.0"
}
}

BIN
web/public/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

63
web/public/global.css Normal file
View File

@ -0,0 +1,63 @@
html, body {
position: relative;
width: 100%;
height: 100%;
}
body {
color: #333;
margin: 0;
padding: 8px;
box-sizing: border-box;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
}
a {
color: rgb(0,100,200);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
a:visited {
color: rgb(0,80,160);
}
label {
display: block;
}
input, button, select, textarea {
font-family: inherit;
font-size: inherit;
-webkit-padding: 0.4em 0;
padding: 0.4em;
margin: 0 0 0.5em 0;
box-sizing: border-box;
border: 1px solid #ccc;
border-radius: 2px;
}
input:disabled {
color: #ccc;
}
button {
color: #333;
background-color: #f4f4f4;
outline: none;
}
button:disabled {
color: #999;
}
button:not(:disabled):active {
background-color: #ddd;
}
button:focus {
border-color: #666;
}

18
web/public/index.html Normal file
View File

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width,initial-scale=1'>
<title>Svelte app</title>
<link rel='icon' type='image/png' href='/favicon.png'>
<link rel='stylesheet' href='/global.css'>
<link rel='stylesheet' href='/build/bundle.css'>
<script defer src='/build/bundle.js'></script>
</head>
<body>
</body>
</html>

83
web/rollup.config.js Normal file
View File

@ -0,0 +1,83 @@
import svelte from 'rollup-plugin-svelte';
import commonjs from '@rollup/plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
import livereload from 'rollup-plugin-livereload';
import { terser } from 'rollup-plugin-terser';
import sveltePreprocess from 'svelte-preprocess';
import typescript from '@rollup/plugin-typescript';
import css from 'rollup-plugin-css-only';
const production = !process.env.ROLLUP_WATCH;
function serve() {
let server;
function toExit() {
if (server) server.kill(0);
}
return {
writeBundle() {
if (server) return;
server = require('child_process').spawn('npm', ['run', 'start', '--', '--dev'], {
stdio: ['ignore', 'inherit', 'inherit'],
shell: true
});
process.on('SIGTERM', toExit);
process.on('exit', toExit);
}
};
}
export default {
input: 'src/main.ts',
output: {
sourcemap: true,
format: 'iife',
name: 'app',
file: 'public/build/bundle.js'
},
plugins: [
svelte({
preprocess: sveltePreprocess({ sourceMap: !production }),
compilerOptions: {
// enable run-time checks when not in production
dev: !production
}
}),
// we'll extract any component CSS out into
// a separate file - better for performance
css({ output: 'bundle.css' }),
// If you have external dependencies installed from
// npm, you'll most likely need these plugins. In
// some cases you'll need additional configuration -
// consult the documentation for details:
// https://github.com/rollup/plugins/tree/master/packages/commonjs
resolve({
browser: true,
dedupe: ['svelte']
}),
commonjs(),
typescript({
sourceMap: !production,
inlineSources: !production
}),
// In dev mode, call `npm run start` once
// the bundle has been generated
!production && serve(),
// Watch the `public` directory and refresh the
// browser on changes when not in production
!production && livereload('public'),
// If we're building for production (npm run build
// instead of npm run dev), minify
production && terser()
],
watch: {
clearScreen: true
}
};

10
web/src/App.svelte Normal file
View File

@ -0,0 +1,10 @@
<script lang="ts">
export let name: string = "Light Control";
import Form from "./Form.svelte";
function handleOnSubmit() {
console.log("Submitted");
}
</script>
<Form on:submit={handleOnSubmit}></Form>

21
web/src/Colors.svelte Normal file
View File

@ -0,0 +1,21 @@
<script lang="ts">
// From: https://css-tricks.com/snippets/javascript/random-hex-color/
function randomColor() {
return "#" + Math.floor(Math.random()*16777215).toString(16);
}
export let value = [randomColor()];
function newColor() {
value = [...value, randomColor()];
}
function removeColor() {
if(newColor.length > 1) {
value = value.slice(0, -1);
}
}
</script>
{#each value as value}
<input type="color" bind:value={value} />
{/each}
<button type="button" on:click={newColor} href="#">+</button>
<button type="button" on:click={removeColor} href="#">-</button>

96
web/src/Form.svelte Normal file
View File

@ -0,0 +1,96 @@
<script lang="ts">
import Colors from "./Colors.svelte";
let url = "/api/setcolor";
let possiblePatterns = [
{name: "Solid", text: "Solid", formElements: [
{name: "color", type: "color", label: "Color", value: "#000000"},
]},
{name: "Collide", text: "Collide", formElements: [
{name: "left_color", type: "color", label: "Left Color", value: "#000000"},
{name: "right_color", type: "color", label: "Right Color", value: "#000000"},
{name: "conjoined_color", type: "color", label: "Conjoined Color", value: "#000000"},
]},
{name: "Fade", text: "Fade", formElements: [
{name: "color", type: "color", label: "Color", value: "#000000"},
]},
{name: "MovingPixel", text: "MovingPixel", formElements: [
{name: "color", type: "color", label: "Color", value: "#000000"},
]},
{name: "MovingRainbow", text: "MovingRainbow", formElements: [
{name: "width", type: "number", label: "Width", value: 4},
{name: "forward", type: "checkbox", label: "More Forward?", value: ""},
{name: "skip", type: "number", label: "# to skip", value: ""},
]},
{name: "Orb", text: "Orb", formElements: [
{name: "color", type: "color", label: "Color", value: "#000000"},
{name: "center_width", type: "number", label: "Center Width", value: "1"},
{name: "backoff_width", type: "number", label: "Backoff Width", value: "0"},
]},
{name: "Flashing", text: "Flashing", formElements: [
{name: "color", type: "colors", label: "Color", value: []},
{name: "width", type: "number", label: "Width", value: 10},
{name: "tick_rate", type: "number", label: "Tick Rate", value: 10},
]},
];
let selectedPattern = possiblePatterns[0];
function hexToRgb(hex) {
// Adapted from https://stackoverflow.com/a/5624139
var result = /^#?([a-f\d]{1,2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
console.log(hex);
result.splice(0, 1);
return result ? result.map(r => parseInt(r, 16)) : null;
}
function getFormData() {
var ret = {};
ret[selectedPattern.name] = selectedPattern.formElements.map(x => {
switch(x.type) {
case "color":
return hexToRgb(x.value);
break;
case "number":
return parseInt(x.value);
break;
case "checkbox":
return x.value === "on";
break;
case "colors":
console.log(x);
return x.value.map(hexToRgb);
break;
default:
return x.value;
}
});
return ret;
}
function handleSubmit() {
const response = fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(getFormData())
}).then(r => console.log(response));
}
</script>
<form on:submit|preventDefault={handleSubmit}>
<select bind:value={selectedPattern}>
{#each possiblePatterns as p}
<option value={p}>{p.text}</option>
{/each}
</select>
{#each selectedPattern.formElements as fe}
<label for={fe.name}>{fe.label}</label>
{#if fe.type === "colors"}
<Colors bind:value={fe.value}/>
{:else}
<input type={fe.type} name={fe.name} on:input={(e) => fe.value = e.target.value} />
{/if}
{/each}
<button type="submit">Submit</button>
</form>

1
web/src/Input.svelte Normal file
View File

@ -0,0 +1 @@

1
web/src/global.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="svelte" />

10
web/src/main.ts Normal file
View File

@ -0,0 +1,10 @@
import App from './App.svelte';
const app = new App({
target: document.body,
props: {
name: 'world'
}
});
export default app;

6
web/tsconfig.json Normal file
View File

@ -0,0 +1,6 @@
{
"extends": "@tsconfig/svelte/tsconfig.json",
"include": ["src/**/*"],
"exclude": ["node_modules/*", "__sapper__/*", "public/*"]
}