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