Initial commit
This commit is contained in:
commit
fb5da5fa82
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
/debug/
|
||||||
|
/target/
|
||||||
|
/Cargo.lock
|
||||||
|
**/*.rs.bk
|
17
Cargo.toml
Normal file
17
Cargo.toml
Normal 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
10
Makefile
Normal 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
10
README.adoc
Normal 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
14
entr.sh
Executable 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}"
|
4
lib-ws2818-rgb-led-spi-driver/.gitignore
vendored
Normal file
4
lib-ws2818-rgb-led-spi-driver/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
/debug/
|
||||||
|
/target/
|
||||||
|
/Cargo.lock
|
||||||
|
**/*.rs.bk
|
27
lib-ws2818-rgb-led-spi-driver/Cargo.toml
Normal file
27
lib-ws2818-rgb-led-spi-driver/Cargo.toml
Normal 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 }
|
21
lib-ws2818-rgb-led-spi-driver/LICENSE
Normal file
21
lib-ws2818-rgb-led-spi-driver/LICENSE
Normal 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.
|
1
lib-ws2818-rgb-led-spi-driver/README.adoc
Normal file
1
lib-ws2818-rgb-led-spi-driver/README.adoc
Normal file
@ -0,0 +1 @@
|
|||||||
|
This library is almost verbatim from https://github.com/phip1611/ws2818-rgb-led-spi-driver/ with some cleanup.
|
68
lib-ws2818-rgb-led-spi-driver/src/adapter_gen.rs
Normal file
68
lib-ws2818-rgb-led-spi-driver/src/adapter_gen.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
80
lib-ws2818-rgb-led-spi-driver/src/adapter_spi.rs
Normal file
80
lib-ws2818-rgb-led-spi-driver/src/adapter_spi.rs
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
48
lib-ws2818-rgb-led-spi-driver/src/encoding.rs
Normal file
48
lib-ws2818-rgb-led-spi-driver/src/encoding.rs
Normal 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
|
||||||
|
}
|
14
lib-ws2818-rgb-led-spi-driver/src/lib.rs
Normal file
14
lib-ws2818-rgb-led-spi-driver/src/lib.rs
Normal 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;
|
48
lib-ws2818-rgb-led-spi-driver/src/timings.rs
Normal file
48
lib-ws2818-rgb-led-spi-driver/src/timings.rs
Normal 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
60
src/color.rs
Normal 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
48
src/main.rs
Normal 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
330
src/pattern.rs
Normal 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
111
src/strip.rs
Normal 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
111
src/ui.rs
Normal 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()
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user