Initial commit

This commit is contained in:
Austen Adler 2021-07-31 14:29:21 -04:00
commit fb5da5fa82
19 changed files with 1026 additions and 0 deletions

4
.gitignore vendored Normal file
View File

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

17
Cargo.toml Normal file
View File

@ -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"

10
Makefile Normal file
View File

@ -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

10
README.adoc Normal file
View File

@ -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
----

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 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}"

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

60
src/color.rs Normal file
View File

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

48
src/main.rs Normal file
View File

@ -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::<Message>();
thread::spawn(move || {
let mut strip = LEDStrip::new(3);
strip.strip_loop(&rx);
println!("Dead therad");
});
console_ui_loop(&tx);
}

330
src/pattern.rs Normal file
View File

@ -0,0 +1,330 @@
use crate::color::{self, RAINBOW, RGB};
pub trait Pattern: std::fmt::Debug {
fn init(&mut self, lights_buf: &mut Vec<RGB>, num_lights: u16) -> Result<(), ()>;
fn step(&mut self, lights_buf: &mut Vec<RGB>) -> 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<RGB>) -> 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<RGB>, 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<RGB>) -> bool {
let ret = !self.has_run;
self.has_run = true;
ret
}
fn init(&mut self, lights_buf: &mut Vec<RGB>, 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<RGB>) -> bool {
lights_buf.rotate_right(1);
true
}
fn init(&mut self, lights_buf: &mut Vec<RGB>, 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::<Vec<RGB>>();
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<RGB>) -> 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<RGB>, 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<RGB>) -> 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<RGB>, 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<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() {}
}

111
src/strip.rs Normal file
View File

@ -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<dyn Pattern + Send>),
SetNumLights(u16),
}
#[allow(clippy::module_name_repetitions)]
pub struct LEDStrip {
pub adapter: Box<dyn WS28xxAdapter>,
pub num_lights: u16,
pub pattern: Box<dyn Pattern>,
pub lights_buf: Vec<RGB>,
}
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::<Vec<(u8, u8, u8)>>();
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<Message>) {
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;
}
}
}
}

111
src/ui.rs Normal file
View File

@ -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<strip::Message>) {
loop {
if let Err(msg) = parse_cmd(tx, &get_line("Command (cfqs)")) {
println!("Command error: {}", msg);
}
}
}
fn parse_cmd(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(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(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(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(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::<u16>()
.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<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(tx: &Sender<strip::Message>, pat: Box<dyn Pattern + Send>) -> 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()
}