From fb5da5fa82073b73b97784a5605e2ff0482fc622 Mon Sep 17 00:00:00 2001 From: Austen Adler Date: Sat, 31 Jul 2021 14:29:21 -0400 Subject: [PATCH] Initial commit --- .gitignore | 4 + Cargo.toml | 17 + Makefile | 10 + README.adoc | 10 + entr.sh | 14 + lib-ws2818-rgb-led-spi-driver/.gitignore | 4 + lib-ws2818-rgb-led-spi-driver/Cargo.toml | 27 ++ lib-ws2818-rgb-led-spi-driver/LICENSE | 21 ++ lib-ws2818-rgb-led-spi-driver/README.adoc | 1 + .../src/adapter_gen.rs | 68 ++++ .../src/adapter_spi.rs | 80 +++++ lib-ws2818-rgb-led-spi-driver/src/encoding.rs | 48 +++ lib-ws2818-rgb-led-spi-driver/src/lib.rs | 14 + lib-ws2818-rgb-led-spi-driver/src/timings.rs | 48 +++ src/color.rs | 60 ++++ src/main.rs | 48 +++ src/pattern.rs | 330 ++++++++++++++++++ src/strip.rs | 111 ++++++ src/ui.rs | 111 ++++++ 19 files changed, 1026 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 Makefile create mode 100644 README.adoc create mode 100755 entr.sh create mode 100644 lib-ws2818-rgb-led-spi-driver/.gitignore create mode 100644 lib-ws2818-rgb-led-spi-driver/Cargo.toml create mode 100644 lib-ws2818-rgb-led-spi-driver/LICENSE create mode 100644 lib-ws2818-rgb-led-spi-driver/README.adoc create mode 100644 lib-ws2818-rgb-led-spi-driver/src/adapter_gen.rs create mode 100644 lib-ws2818-rgb-led-spi-driver/src/adapter_spi.rs create mode 100644 lib-ws2818-rgb-led-spi-driver/src/encoding.rs create mode 100644 lib-ws2818-rgb-led-spi-driver/src/lib.rs create mode 100644 lib-ws2818-rgb-led-spi-driver/src/timings.rs create mode 100644 src/color.rs create mode 100644 src/main.rs create mode 100644 src/pattern.rs create mode 100644 src/strip.rs create mode 100644 src/ui.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d389eb4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/debug/ +/target/ +/Cargo.lock +**/*.rs.bk diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..9315aab --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "aw-lights" +version = "0.1.0" +authors = ["root"] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +rppal = "0.7" +ws2818-rgb-led-spi-driver = { path = "lib-ws2818-rgb-led-spi-driver/" } +# ws2818-rgb-led-spi-driver = { path = "ws2818-rgb-led-spi-driver/" } +# ws2818-rgb-led-spi-driver = { path = "/home/pi/tt/" } +# ws2818-rgb-led-spi-driver = { path = "/home/pi/ws2818-rgb-led-spi-driver/" } + +[target.armv7-unknown-linux-gnueabihf] +linker = "armv7-unknown-linux-gnueabihf" diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f028df2 --- /dev/null +++ b/Makefile @@ -0,0 +1,10 @@ +.PHONY: build deploy run + +build: + cargo build --target=armv7-unknown-linux-gnueabihf + +deploy: build + scp ./target/armv7-unknown-linux-gnueabihf/debug/aw-lights pi: + +run: deploy + ssh pi ./aw-lights diff --git a/README.adoc b/README.adoc new file mode 100644 index 0000000..3eb62d9 --- /dev/null +++ b/README.adoc @@ -0,0 +1,10 @@ +# 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 +---- diff --git a/entr.sh b/entr.sh new file mode 100755 index 0000000..e6206b5 --- /dev/null +++ b/entr.sh @@ -0,0 +1,14 @@ +CMD="$(cat <<'EOF' +set -euo pipefail +HEIGHT="$(($(tput lines) - 1))" +clear +for i in 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}" diff --git a/lib-ws2818-rgb-led-spi-driver/.gitignore b/lib-ws2818-rgb-led-spi-driver/.gitignore new file mode 100644 index 0000000..d389eb4 --- /dev/null +++ b/lib-ws2818-rgb-led-spi-driver/.gitignore @@ -0,0 +1,4 @@ +/debug/ +/target/ +/Cargo.lock +**/*.rs.bk diff --git a/lib-ws2818-rgb-led-spi-driver/Cargo.toml b/lib-ws2818-rgb-led-spi-driver/Cargo.toml new file mode 100644 index 0000000..3937140 --- /dev/null +++ b/lib-ws2818-rgb-led-spi-driver/Cargo.toml @@ -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 ", "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 } diff --git a/lib-ws2818-rgb-led-spi-driver/LICENSE b/lib-ws2818-rgb-led-spi-driver/LICENSE new file mode 100644 index 0000000..e354433 --- /dev/null +++ b/lib-ws2818-rgb-led-spi-driver/LICENSE @@ -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. diff --git a/lib-ws2818-rgb-led-spi-driver/README.adoc b/lib-ws2818-rgb-led-spi-driver/README.adoc new file mode 100644 index 0000000..f93aa9f --- /dev/null +++ b/lib-ws2818-rgb-led-spi-driver/README.adoc @@ -0,0 +1 @@ +This library is almost verbatim from https://github.com/phip1611/ws2818-rgb-led-spi-driver/ with some cleanup. diff --git a/lib-ws2818-rgb-led-spi-driver/src/adapter_gen.rs b/lib-ws2818-rgb-led-spi-driver/src/adapter_gen.rs new file mode 100644 index 0000000..232bf15 --- /dev/null +++ b/lib-ws2818-rgb-led-spi-driver/src/adapter_gen.rs @@ -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; + + /// 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, +} + +impl WS28xxGenAdapter { + /// Constructor that stores the hardware device in the adapter. + pub fn new(hw: Box) -> Self { + Self { hw } + } +} + +// Implement the getter for the hardware device. +impl WS28xxAdapter for WS28xxGenAdapter { + fn get_hw_dev(&mut self) -> &mut Box { + &mut self.hw + } +} diff --git a/lib-ws2818-rgb-led-spi-driver/src/adapter_spi.rs b/lib-ws2818-rgb-led-spi-driver/src/adapter_spi.rs new file mode 100644 index 0000000..aec5754 --- /dev/null +++ b/lib-ws2818-rgb-led-spi-driver/src/adapter_spi.rs @@ -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 { + 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 { + 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 { + // 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() + } +} diff --git a/lib-ws2818-rgb-led-spi-driver/src/encoding.rs b/lib-ws2818-rgb-led-spi-driver/src/encoding.rs new file mode 100644 index 0000000..805b948 --- /dev/null +++ b/lib-ws2818-rgb-led-spi-driver/src/encoding.rs @@ -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 { + let mut bytes = vec![]; + data.iter() + .for_each(|rgb| bytes.extend_from_slice(&encode_rgb(rgb.0, rgb.1, rgb.2))); + bytes +} diff --git a/lib-ws2818-rgb-led-spi-driver/src/lib.rs b/lib-ws2818-rgb-led-spi-driver/src/lib.rs new file mode 100644 index 0000000..7e28a4a --- /dev/null +++ b/lib-ws2818-rgb-led-spi-driver/src/lib.rs @@ -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; diff --git a/lib-ws2818-rgb-led-spi-driver/src/timings.rs b/lib-ws2818-rgb-led-spi-driver/src/timings.rs new file mode 100644 index 0000000..278d813 --- /dev/null +++ b/lib-ws2818-rgb-led-spi-driver/src/timings.rs @@ -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]; +} diff --git a/src/color.rs b/src/color.rs new file mode 100644 index 0000000..8b40489 --- /dev/null +++ b/src/color.rs @@ -0,0 +1,60 @@ +#[derive(Copy, Clone, Debug, PartialEq)] +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_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], + // ) + // } +} +#[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 +]; + +#[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)); + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..95d75d4 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,48 @@ +// 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, + // 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, + // Expect prints a message when there is a critical error, so this is valid + // clippy::expect_used, + clippy::unwrap_used, +)] +// See https://rust-lang.github.io/rust-clippy/master/index.html for more lints + +mod color; +mod pattern; +mod strip; +mod ui; +use std::sync::mpsc::channel; +use std::thread; +use strip::{LEDStrip, Message}; +use ui::console_ui_loop; + +fn main() { + let (tx, rx) = channel::(); + thread::spawn(move || { + let mut strip = LEDStrip::new(3); + strip.strip_loop(&rx); + println!("Dead therad"); + }); + + console_ui_loop(&tx); +} diff --git a/src/pattern.rs b/src/pattern.rs new file mode 100644 index 0000000..1b3492f --- /dev/null +++ b/src/pattern.rs @@ -0,0 +1,330 @@ +use crate::color::{self, RAINBOW, RGB}; +pub trait Pattern: std::fmt::Debug { + fn init(&mut self, lights_buf: &mut Vec, num_lights: u16) -> Result<(), ()>; + fn step(&mut self, lights_buf: &mut Vec) -> bool; +} + +#[derive(Clone, Debug)] +pub struct MovingPixel { + color: RGB, + num_lights: u16, + step: u16, +} +impl MovingPixel { + pub const fn new(color: RGB) -> Self { + Self { + color, + step: 0, + num_lights: 1, + } + } +} +impl Pattern for MovingPixel { + fn step(&mut self, lights_buf: &mut Vec) -> bool { + let len = self.num_lights; + lights_buf.swap( + self.step.rem_euclid(len).into(), + self.step.saturating_add(1).saturating_mul(3).into(), + ); + self.step = self.step.wrapping_add(1); + true + } + fn init(&mut self, lights_buf: &mut Vec, 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 + *lights_buf = vec![color::BLACK; num_lights.into()]; + lights_buf[0] = self.color; + Ok(()) + } +} + +#[derive(Clone, Debug)] +pub struct Solid { + color: RGB, + has_run: bool, +} +impl Solid { + pub const fn new(color: RGB) -> Self { + Self { + color, + has_run: false, + } + } +} +impl Pattern for Solid { + fn step(&mut self, _lights_buf: &mut Vec) -> bool { + let ret = !self.has_run; + self.has_run = true; + ret + } + fn init(&mut self, lights_buf: &mut Vec, num_lights: u16) -> Result<(), ()> { + if num_lights < 1 { + return Err(()); + } + self.has_run = false; + *lights_buf = vec![self.color; num_lights.into()]; + Ok(()) + } +} + +#[derive(Clone, Debug)] +pub struct MovingRainbow {} +impl MovingRainbow { + pub const fn new() -> Self { + Self {} + } +} +impl Pattern for MovingRainbow { + fn step(&mut self, lights_buf: &mut Vec) -> bool { + lights_buf.rotate_right(1); + true + } + fn init(&mut self, lights_buf: &mut Vec, num_lights: u16) -> Result<(), ()> { + if num_lights < 1 { + return Err(()); + } + *lights_buf = RAINBOW + .iter() + .cycle() + .take( + num_lights + .saturating_sub(1) + .div_euclid(7) + .saturating_add(1) + .saturating_mul(7) + .into(), + ) + .copied() + .collect::>(); + Ok(()) + } +} + +#[derive(Clone, Debug)] +pub struct Fade { + color: RGB, + step: u8, + direction: bool, + num_lights: u16, +} +impl Fade { + pub const fn new(color: RGB) -> Self { + Self { + color, + step: 0, + direction: true, + num_lights: 1, + } + } +} +impl Pattern for Fade { + fn step(&mut self, lights_buf: &mut Vec) -> 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); + } + *lights_buf = vec![RGB(self.step, self.step, self.step); self.num_lights.into()]; + true + } + fn init(&mut self, lights_buf: &mut Vec, num_lights: u16) -> Result<(), ()> { + if num_lights < 1 { + return Err(()); + } + self.step = 0; + self.direction = true; + self.num_lights = num_lights; + *lights_buf = vec![color::BLACK; self.num_lights.into()]; + Ok(()) + } +} + +#[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, +} +impl Collide { + pub const 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, + } + } +} +impl Pattern for Collide { + fn step(&mut self, lights_buf: &mut Vec) -> 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) + }; + // let colors = match self.step { + // // If we are in the conjoined bounds region, these will be the same color + // // (self.conjoined_bounds.0)..=(self.conjoined_bounds.1) => (self.conjoined_color, self.conjoined_color), + // l..=r => (self.conjoined_color, self.conjoined_color), + // // If + // _ => (self.left_color, self.right_color), + // }; + + // Turn off the previous LED + lights_buf[usize::from(self.previous_offset)] = color::BLACK; + if self.previous_offset + != self + .num_lights + .saturating_sub(1) + .saturating_sub(self.previous_offset) + { + lights_buf[usize::from( + self.num_lights + .saturating_sub(1) + .saturating_sub(self.previous_offset), + )] = color::BLACK; + } + // Set the color of the current offset + lights_buf[usize::from(self.offset)] = colors.0; + if self.offset + != self + .num_lights + .saturating_sub(1) + .saturating_sub(self.offset) + { + lights_buf[usize::from( + self.num_lights + .saturating_sub(1) + .saturating_sub(self.offset), + )] = 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); + + true + } + fn init(&mut self, lights_buf: &mut Vec, 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(()); + } + *lights_buf = 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(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + const NUM_LIGHTS: u16 = 10; + fn test_strip() -> Vec { + 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() {} +} diff --git a/src/strip.rs b/src/strip.rs new file mode 100644 index 0000000..220e05a --- /dev/null +++ b/src/strip.rs @@ -0,0 +1,111 @@ +use crate::color::{self, RGB}; +use crate::pattern::{self, Pattern}; +use std::ops::Add; +use std::sync::mpsc::Receiver; +use std::time::{Duration, Instant}; +use ws2818_rgb_led_spi_driver::adapter_gen::WS28xxAdapter; +use ws2818_rgb_led_spi_driver::adapter_spi::WS28xxSpiAdapter; + +const MAX_NUM_LIGHTS: u16 = 32; +const TICK_TIME: u64 = 400; + +pub enum Message { + ClearLights, + ChangePattern(Box), + SetNumLights(u16), +} + +#[allow(clippy::module_name_repetitions)] +pub struct LEDStrip { + pub adapter: Box, + pub num_lights: u16, + pub pattern: Box, + pub lights_buf: Vec, +} + +impl LEDStrip { + pub fn new(num_lights: u16) -> Self { + let adapter = Box::new( + WS28xxSpiAdapter::new("/dev/spidev0.0").expect("Cannot locate device /dev/spidev0.0!"), + ); + // let pattern = Pattern::default(); + let pattern = Box::new(pattern::Solid::new(color::BLACK)); + let lights_buf = vec![color::BLACK; num_lights.into()]; + let mut ret = Self { + adapter, + pattern, + lights_buf, + num_lights, + }; + ret.set_num_lights(num_lights); + ret + } + + fn write_buf(&mut self) { + let data = self + .lights_buf + .as_slice() + .iter() + .take(self.num_lights.into()) + .map(|c| c.to_tuple()) + .collect::>(); + self.adapter + .write_rgb(data.as_slice()) + .expect("TODO: Critical data write error!"); + } + + 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(&mut self.lights_buf, num_lights).is_ok() { + self.num_lights = num_lights; + } + } + + pub fn strip_loop(&mut self, rx: &Receiver) { + loop { + let target_time = Instant::now().add(Duration::from_millis(TICK_TIME)); + + if let Ok(message) = rx.try_recv() { + match message { + Message::ClearLights => { + let mut pat = Box::new(pattern::Solid::new(color::BLACK)); + if pat.init(&mut self.lights_buf, self.num_lights).is_ok() { + self.pattern = pat; + } else { + println!("Clearing light strip: {:?}", pat); + } + } + Message::ChangePattern(pat) => { + let mut pat = pat; + if pat.init(&mut self.lights_buf, self.num_lights).is_ok() { + self.pattern = pat; + } else { + println!("Error with pattern: {:?}", pat); + } + } + Message::SetNumLights(num_lights) => { + self.set_num_lights(num_lights); + } + } + } + + if self.pattern.step(&mut self.lights_buf) { + self.write_buf(); + } + + // let mut num_iter = 0; + loop { + if Instant::now() >= target_time { + break; + } + // num_iter += 1; + } + } + } +} diff --git a/src/ui.rs b/src/ui.rs new file mode 100644 index 0000000..e723414 --- /dev/null +++ b/src/ui.rs @@ -0,0 +1,111 @@ +use crate::color::RGB; +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(tx: &Sender) { + loop { + if let Err(msg) = parse_cmd(tx, &get_line("Command (cfqs)")) { + println!("Command error: {}", msg); + } + } +} + +fn parse_cmd(tx: &Sender, s: &str) -> Result<(), String> { + match s + .split(char::is_whitespace) + .collect::>() + .as_slice() + { + ["f", r, g, b] => { + let color = RGB( + r.parse::() + .map_err(|_| String::from("Red could not be parsed"))?, + g.parse::() + .map_err(|_| String::from("Green could not be parsed"))?, + b.parse::() + .map_err(|_| String::from("Blue could not be parsed"))?, + ); + change_pattern(tx, Box::new(pattern::Fade::new(color))) + } + ["f", c] => { + let color_value = c + .parse::() + .map_err(|_| String::from("Could not parse color"))?; + let color = RGB(color_value, color_value, color_value); + change_pattern(tx, Box::new(pattern::Fade::new(color))) + } + ["m", r, g, b] => { + let color = RGB( + r.parse::() + .map_err(|_| String::from("Red could not be parsed"))?, + g.parse::() + .map_err(|_| String::from("Green could not be parsed"))?, + b.parse::() + .map_err(|_| String::from("Blue could not be parsed"))?, + ); + change_pattern(tx, Box::new(pattern::MovingPixel::new(color))) + } + ["m", c] => { + let color_value = c + .parse::() + .map_err(|_| String::from("Could not parse color"))?; + let color = RGB(color_value, color_value, color_value); + change_pattern(tx, Box::new(pattern::MovingPixel::new(color))) + }, + ["c", r, g, b] => { + let color = parse_color(r, g, b)?; + change_pattern(tx, Box::new(pattern::Solid::new(color))) + } + ["c", c] => { + let color = parse_color(c, c, c)?; + change_pattern(tx, Box::new(pattern::Solid::new(color))) + }, + ["r"] =>change_pattern(tx, Box::new(pattern::MovingRainbow::new())), + ["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(tx, Box::new(pattern::Collide::new(left, right, combined))) + } + ["x"] => tx + .send(strip::Message::ClearLights) + .map_err(|e| e.to_string()), + ["q"] => Ok(()), + ["s", n] => tx + .send(strip::Message::SetNumLights( + n.parse::() + .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")), + } +} + +fn parse_color(r: &str, g: &str, b: &str) -> Result { + Ok(RGB( + r.parse::() + .map_err(|_| String::from("Red could not be parsed"))?, + g.parse::() + .map_err(|_| String::from("Green could not be parsed"))?, + b.parse::() + .map_err(|_| String::from("Blue could not be parsed"))?, + )) +} + +fn change_pattern(tx: &Sender, pat: Box) -> Result<(), String> { + tx.send(strip::Message::ChangePattern(pat)) + .map_err(|e| e.to_string()) +} + +fn get_line(prompt: &str) -> String { + print!("{}: ", prompt); + std::io::stdout().flush().expect("Could not flush stdout"); + let mut input_text = String::new(); + io::stdin() + .read_line(&mut input_text) + .expect("Could not read from stdin"); + input_text.trim().to_string() +}