Compare commits
18 Commits
3996e1b09f
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66c431f20c | ||
|
|
b332d75c7f | ||
|
|
6deb31836f | ||
|
|
13326a49d9 | ||
|
|
20b8f6d4db | ||
|
|
4dd5587b49 | ||
|
|
ae215ce252 | ||
|
|
a9abc2e05b | ||
|
|
ee133379f2 | ||
|
|
b718334392 | ||
|
|
c2b2517661 | ||
|
|
4451917154 | ||
|
|
086e194a1d | ||
|
|
4c788f7313 | ||
|
|
3d8adc0d52 | ||
|
|
20983a8c08 | ||
|
|
7521c87460 | ||
|
|
b43490e876 |
841
Cargo.lock
generated
841
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -9,3 +9,11 @@ edition = "2018"
|
||||
bluer = "0.13.1"
|
||||
tokio = {version = "1.15.0", features = ["full"]}
|
||||
futures = "0.3.19"
|
||||
rand = "0.8.5"
|
||||
rodio = "0.15.0"
|
||||
clap = { version = "3.1.6", features = ["derive"] }
|
||||
pattern_derive = { path = "pattern_derive" }
|
||||
|
||||
serde_derive = "1.0.136"
|
||||
serde = "1.0.136"
|
||||
serde_yaml = "0.8"
|
||||
|
||||
81
spectacle.yml
Normal file
81
spectacle.yml
Normal file
@@ -0,0 +1,81 @@
|
||||
colors:
|
||||
- &red
|
||||
red: 255
|
||||
green: 0
|
||||
blue: 0
|
||||
|
||||
devices:
|
||||
- name : toto
|
||||
mac_addresses :
|
||||
- C9:81:9C:BA:53:BC
|
||||
sequence:
|
||||
- pattern:
|
||||
type: Rain
|
||||
color: &blue
|
||||
red: 0
|
||||
green: 0
|
||||
blue: 255
|
||||
background_color: &yellow
|
||||
red: 0
|
||||
green: 255
|
||||
blue: 255
|
||||
stability: 10
|
||||
lines: 5
|
||||
period: 200
|
||||
- pattern:
|
||||
type: Blink
|
||||
fade:
|
||||
current_iteration: 10
|
||||
steps: 20
|
||||
begin_color: &black
|
||||
red: 0
|
||||
green: 0
|
||||
blue: 0
|
||||
end_color: &purple
|
||||
red: 255
|
||||
green: 0
|
||||
blue: 255
|
||||
max_iteration: 10
|
||||
period: 50
|
||||
- pattern:
|
||||
type: Fade
|
||||
steps: 20
|
||||
begin_color:
|
||||
red: 255
|
||||
green: 0
|
||||
blue: 0
|
||||
end_color: *blue
|
||||
period: 200
|
||||
- pattern:
|
||||
type: ColorWipe
|
||||
max_iteration: 25
|
||||
color: &green
|
||||
red: 0
|
||||
green: 255
|
||||
blue: 0
|
||||
period: 100
|
||||
- pattern:
|
||||
type: FillRandom
|
||||
color: *blue
|
||||
stability: 40
|
||||
period: 80
|
||||
- name: titi
|
||||
sequence:
|
||||
- pattern:
|
||||
type: StarsRandom
|
||||
color: *green
|
||||
ratio: 8
|
||||
steps: 20
|
||||
background_color:
|
||||
red: 0
|
||||
green: 255
|
||||
blue: 100
|
||||
period: 50
|
||||
- pattern:
|
||||
type: Rainbow
|
||||
max_iteration: 200
|
||||
period: 10
|
||||
- pattern:
|
||||
type: Scanner
|
||||
color: *blue
|
||||
period: 100
|
||||
13
src/args.rs
Normal file
13
src/args.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
use clap::Parser;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[clap(author, version, about, long_about = None)]
|
||||
pub struct Cli {
|
||||
/// file for light sequences
|
||||
#[clap(short, long)]
|
||||
pub spectacle_file: String,
|
||||
|
||||
/// musique file that will be played
|
||||
#[clap(short, long)]
|
||||
pub musique_file: String,
|
||||
}
|
||||
18
src/audio.rs
Normal file
18
src/audio.rs
Normal file
@@ -0,0 +1,18 @@
|
||||
use rodio::{Decoder, OutputStream, Sink};
|
||||
use std::fs::File;
|
||||
use std::io::BufReader;
|
||||
use std::path::Path;
|
||||
|
||||
pub fn play_sound(path: &Path) {
|
||||
println!("starting musique");
|
||||
let (_stream, stream_handle) = OutputStream::try_default().unwrap();
|
||||
let sink = Sink::try_new(&stream_handle).unwrap();
|
||||
|
||||
let file = BufReader::new(File::open(path).unwrap());
|
||||
let source = Some(Decoder::new(file).unwrap()).unwrap();
|
||||
sink.append(source);
|
||||
|
||||
// The sound plays in a separate thread. This call will block the current thread until the sink
|
||||
// has finished playing all its queued sounds.
|
||||
sink.sleep_until_end();
|
||||
}
|
||||
@@ -39,7 +39,6 @@ pub(crate) async fn bluetooth_scan(
|
||||
while let Some(evt) = discover.next().await {
|
||||
match evt {
|
||||
AdapterEvent::DeviceAdded(addr) => {
|
||||
println!("new device {}", addr);
|
||||
if already_scanned.contains(&addr) {
|
||||
continue;
|
||||
}
|
||||
@@ -49,7 +48,6 @@ pub(crate) async fn bluetooth_scan(
|
||||
Ok(service_found) => {
|
||||
if service_found {
|
||||
tx.send(device).await.unwrap();
|
||||
println!("found service in device {}", addr);
|
||||
}
|
||||
}
|
||||
Err(_) => continue,
|
||||
|
||||
24
src/config.rs
Normal file
24
src/config.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
use crate::pixel_color::PixelColor;
|
||||
use crate::runner::DeviceSequence;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use std::collections::HashSet;
|
||||
use std::fs;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Config<const N: usize> {
|
||||
devices: Vec<Device<N>>,
|
||||
colors: Vec<PixelColor>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct Device<const N: usize> {
|
||||
pub mac_addresses: Option<HashSet<String>>,
|
||||
name: Option<String>,
|
||||
pub sequence: DeviceSequence<N>,
|
||||
}
|
||||
|
||||
pub fn load_from_file<const N: usize>(file: &std::path::Path) -> Vec<Device<N>> {
|
||||
let file = fs::read_to_string(file).unwrap();
|
||||
let config: Config<N> = serde_yaml::from_str(&*file).unwrap();
|
||||
config.devices
|
||||
}
|
||||
178
src/main.rs
178
src/main.rs
@@ -1,10 +1,22 @@
|
||||
mod args;
|
||||
mod audio;
|
||||
mod bluetooth;
|
||||
mod config;
|
||||
mod patterns;
|
||||
mod pixel_color;
|
||||
mod runner;
|
||||
mod spectacle;
|
||||
|
||||
use crate::patterns::{ColorWipe, Fade, PixelColor, Rainbow};
|
||||
use crate::args::Cli;
|
||||
use crate::audio::play_sound;
|
||||
use crate::config::load_from_file;
|
||||
use crate::runner::Spectacle;
|
||||
use bluer::gatt::remote::Characteristic;
|
||||
use bluer::{Device, Error};
|
||||
use clap::Parser;
|
||||
use patterns::Strip;
|
||||
use std::time::Duration;
|
||||
use std::path::Path;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::sync::watch;
|
||||
|
||||
@@ -15,9 +27,15 @@ const BASE_STRIP_DATA: [u8; 3] = [0x00, 0x00, 0x01];
|
||||
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
async fn main() -> bluer::Result<()> {
|
||||
let adapter = bluetooth::create_session().await?;
|
||||
let command_line = Cli::parse();
|
||||
|
||||
let (tx_scan, mut rx_scan) = mpsc::channel(3);
|
||||
let file = Path::new(&command_line.spectacle_file);
|
||||
let config = load_from_file(file);
|
||||
let spectacle = Spectacle::from(config);
|
||||
let spectacle = Arc::new(Mutex::new(spectacle));
|
||||
|
||||
let adapter = bluetooth::create_session().await?;
|
||||
let (tx_scan, rx_scan) = mpsc::channel(3);
|
||||
|
||||
tokio::spawn(bluetooth::bluetooth_scan(
|
||||
tx_scan,
|
||||
@@ -25,110 +43,57 @@ async fn main() -> bluer::Result<()> {
|
||||
SERVICE_UUID,
|
||||
));
|
||||
|
||||
let (tx, rx) = watch::channel(Strip::<STRIP_SIZE>::default());
|
||||
tokio::spawn(connect_device(spectacle.clone(), rx_scan));
|
||||
|
||||
tokio::spawn(async move {
|
||||
println!("starting");
|
||||
let musique_file = command_line.musique_file;
|
||||
let musique = tokio::task::spawn_blocking(move || play_sound(Path::new(&musique_file)));
|
||||
let futures;
|
||||
{
|
||||
let mut spectacle = spectacle.lock().unwrap();
|
||||
futures = spectacle.run();
|
||||
}
|
||||
let mut joinhandles = vec![];
|
||||
for future in futures {
|
||||
joinhandles.push(tokio::spawn(future));
|
||||
}
|
||||
for joinhandle in joinhandles {
|
||||
joinhandle.await.unwrap();
|
||||
}
|
||||
musique.await.unwrap();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn connect_device(
|
||||
spectacle: Arc<Mutex<Spectacle<STRIP_SIZE>>>,
|
||||
mut rx_scan: mpsc::Receiver<Device>,
|
||||
) -> Result<(), Error> {
|
||||
while let Some(device) = rx_scan.recv().await {
|
||||
bluetooth::connect(&device, 3).await?;
|
||||
let char = bluetooth::get_char(&device, SERVICE_UUID, PIXEL_DATA_UUID).await?;
|
||||
if let Some(char) = char {
|
||||
let mut rx_device = rx.clone();
|
||||
let rx: Option<watch::Receiver<Strip<STRIP_SIZE>>>;
|
||||
{
|
||||
let spectacle = spectacle.lock().unwrap();
|
||||
rx = spectacle.get_channel(&device.address());
|
||||
}
|
||||
if let Some(mut rx) = rx {
|
||||
tokio::spawn(async move {
|
||||
println!("device connected : {}", &device.address());
|
||||
while rx_device.changed().await.is_ok() {
|
||||
let strip = *rx_device.borrow();
|
||||
while rx.changed().await.is_ok() {
|
||||
let strip = *rx.borrow();
|
||||
if write_strip(&strip, &char).await.is_err() {
|
||||
break;
|
||||
};
|
||||
}
|
||||
println!("device {} disconnected", &device.address());
|
||||
// drop(rx_device);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
bluer::Result::Ok(())
|
||||
});
|
||||
|
||||
let mut pixels = Vec::<PixelColor>::new();
|
||||
for _ in 0..STRIP_SIZE {
|
||||
pixels.push(PixelColor {
|
||||
red: 255,
|
||||
green: 0,
|
||||
blue: 0,
|
||||
})
|
||||
}
|
||||
|
||||
let mut _colorwipe = ColorWipe::<25> {
|
||||
current_iteration: 0,
|
||||
color: PixelColor {
|
||||
red: 0,
|
||||
green: 255,
|
||||
blue: 0,
|
||||
},
|
||||
infinite: true,
|
||||
};
|
||||
|
||||
let mut fade = Fade::<25> {
|
||||
current_iteration: 0,
|
||||
nbr_iterations: 25,
|
||||
begin_color: PixelColor {
|
||||
red: 255,
|
||||
green: 0,
|
||||
blue: 0,
|
||||
},
|
||||
end_color: PixelColor {
|
||||
red: 0,
|
||||
green: 0,
|
||||
blue: 255,
|
||||
},
|
||||
};
|
||||
|
||||
let mut pattern = Rainbow::<25> {
|
||||
current_iteration: 0,
|
||||
max_iteration: None,
|
||||
step: Some(10),
|
||||
};
|
||||
|
||||
let mut strip = fade.next().unwrap();
|
||||
tokio::time::sleep(Duration::from_secs(5)).await;
|
||||
println!("starting");
|
||||
while tx.send(strip).is_ok() {
|
||||
if let Some(value) = fade.next() {
|
||||
strip = value;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(1000)).await;
|
||||
}
|
||||
println!("starting rainbow");
|
||||
while tx.send(pattern.next().unwrap()).is_ok() {
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
}
|
||||
println!("error sending value");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// fn create_pattern_list() -> Vec<patterns>
|
||||
// async fn send_seq(char: &Characteristic) -> bluer::Result<()> {
|
||||
// println!(" Characteristic flags : {:?}, ", char.flags().await?);
|
||||
//
|
||||
// let mut strip_red : Strip<10> = Strip::default();
|
||||
// strip_red.strip.fill(PixelColor { red: 255, green: 0, blue: 0});
|
||||
//
|
||||
// let mut strip_green: Strip<10> = Strip::default();
|
||||
// strip_green.strip.fill(PixelColor { red: 0, green: 255, blue: 0});
|
||||
//
|
||||
// let pattern = RainbowPattern::<STRIP_SIZE> { current_iteration: 0,};
|
||||
//
|
||||
//
|
||||
// for strip in pattern {
|
||||
// write_strip(strip, char).await?;
|
||||
// sleep(Duration::from_millis(50)).await;
|
||||
// }
|
||||
//
|
||||
// Ok(())
|
||||
// }
|
||||
|
||||
pub async fn write_strip<const N: usize>(
|
||||
data: &Strip<N>,
|
||||
char: &Characteristic,
|
||||
@@ -138,36 +103,3 @@ pub async fn write_strip<const N: usize>(
|
||||
char.write(&*frame).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// async fn find_neopixel_service(device: &Device) -> bluer::Result<Option<Characteristic>> {
|
||||
// let addr = device.address();
|
||||
// let uuids = device.uuids().await?.unwrap_or_default();
|
||||
//
|
||||
// if uuids.contains(&SERVICE_UUID) {
|
||||
// println!("service neopixel found for device {}", &addr);
|
||||
// match connect(device, 3).await {
|
||||
// Ok(()) => {
|
||||
// println!("successefully connected");
|
||||
// }
|
||||
// Err(err) => {
|
||||
// println!("unable to connect, trying new device, {}", err);
|
||||
// return Err(err)
|
||||
// }
|
||||
// }
|
||||
// for service in device.services().await? {
|
||||
// if SERVICE_UUID == service.uuid().await? {
|
||||
// for char in service.characteristics().await? {
|
||||
// println!("uuid : {}", &char.uuid().await?);
|
||||
// if PIXEL_DATA_UUID == char.uuid().await? {
|
||||
//
|
||||
// return Ok(Some(char));
|
||||
// }
|
||||
//
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// }
|
||||
//
|
||||
// Ok(None)
|
||||
// }
|
||||
|
||||
344
src/patterns.rs
344
src/patterns.rs
@@ -1,27 +1,183 @@
|
||||
use std::fmt;
|
||||
mod blink;
|
||||
mod color_wipe;
|
||||
mod fade;
|
||||
mod fade_random;
|
||||
mod fade_unstable;
|
||||
mod fill_random;
|
||||
mod fill_unstable;
|
||||
mod rain;
|
||||
mod rainbow;
|
||||
mod ring;
|
||||
mod ring_scanner;
|
||||
mod scanner;
|
||||
mod stars_random;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct PixelColor {
|
||||
pub(crate) red: u8,
|
||||
pub(crate) green: u8,
|
||||
pub(crate) blue: u8,
|
||||
use crate::patterns::blink::Blink;
|
||||
use crate::patterns::color_wipe::ColorWipe;
|
||||
use crate::patterns::fade::Fade;
|
||||
use crate::patterns::fade_random::FadeRandom;
|
||||
use crate::patterns::fade_unstable::FadeUnstable;
|
||||
use crate::patterns::fill_random::FillRandom;
|
||||
use crate::patterns::fill_unstable::FillUnstable;
|
||||
use crate::patterns::rain::Rain;
|
||||
use crate::patterns::rainbow::Rainbow;
|
||||
use crate::patterns::ring::Ring;
|
||||
use crate::patterns::ring_scanner::RingScanner;
|
||||
use crate::patterns::scanner::Scanner;
|
||||
use crate::patterns::stars_random::StarsRandom;
|
||||
use crate::pixel_color::PixelColor;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use std::ops::{Index, IndexMut};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Eq, Hash, PartialEq, Copy, Clone)]
|
||||
#[serde(tag = "type")]
|
||||
pub(crate) enum Patterns<const N: usize> {
|
||||
Rainbow(Rainbow<N>),
|
||||
Fade(Fade<N>),
|
||||
ColorWipe(ColorWipe<N>),
|
||||
Scanner(Scanner<N>),
|
||||
FillRandom(FillRandom<N>),
|
||||
FillUnstable(FillUnstable<N>),
|
||||
Blink(Blink<N>),
|
||||
Rain(Rain<N>),
|
||||
FadeRandom(FadeRandom<N>),
|
||||
FadeUnstable(FadeUnstable<N>),
|
||||
Ring(Ring<N>),
|
||||
RingScanner(RingScanner<N>),
|
||||
StarsRandom(StarsRandom<N>),
|
||||
}
|
||||
|
||||
impl PixelColor {
|
||||
fn new() -> Self {
|
||||
Default::default()
|
||||
/// Pattern that have to be implemented
|
||||
///
|
||||
///
|
||||
///
|
||||
///
|
||||
pub trait Pattern: Iterator {
|
||||
type Strip;
|
||||
|
||||
fn init(&mut self) -> Option<Self::Strip> {
|
||||
None
|
||||
}
|
||||
fn is_last_iteration(&self) -> bool;
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! impl_pattern_init_background {
|
||||
($generic: tt) => {
|
||||
fn init(&mut self) -> Option<Self::Strip> {
|
||||
if let Some(color) = self.background_color {
|
||||
Some(Strip::<N>::new(color))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! impl_pattern_last_iteration {
|
||||
($name:ty) => {
|
||||
fn is_last_iteration(&self) -> bool {
|
||||
match self.max_iteration {
|
||||
None => false,
|
||||
Some(max_iter) => {
|
||||
if self.current_iteration >= max_iter {
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl<const N: usize> Iterator for Patterns<N> {
|
||||
type Item = Strip<N>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
match self {
|
||||
Patterns::Rainbow(p) => p.next(),
|
||||
Patterns::Fade(p) => p.next(),
|
||||
Patterns::ColorWipe(p) => p.next(),
|
||||
Patterns::Scanner(p) => p.next(),
|
||||
Patterns::FillRandom(p) => p.next(),
|
||||
Patterns::FillUnstable(p) => p.next(),
|
||||
Patterns::Blink(p) => p.next(),
|
||||
Patterns::Rain(p) => p.next(),
|
||||
Patterns::FadeRandom(p) => p.next(),
|
||||
Patterns::FadeUnstable(p) => p.next(),
|
||||
Patterns::Ring(p) => p.next(),
|
||||
Patterns::RingScanner(p) => p.next(),
|
||||
Patterns::StarsRandom(p) => p.next(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for PixelColor {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "[{},{},{}]", self.red, self.green, self.blue)
|
||||
impl<const N: usize> Pattern for Patterns<N> {
|
||||
type Strip = Strip<N>;
|
||||
|
||||
fn init(&mut self) -> Option<Self::Strip> {
|
||||
match self {
|
||||
Patterns::Rainbow(p) => p.init(),
|
||||
Patterns::Fade(p) => p.init(),
|
||||
Patterns::ColorWipe(p) => p.init(),
|
||||
Patterns::Scanner(p) => p.init(),
|
||||
Patterns::FillRandom(p) => p.init(),
|
||||
Patterns::FillUnstable(p) => p.init(),
|
||||
Patterns::Blink(p) => p.init(),
|
||||
Patterns::Rain(p) => p.init(),
|
||||
Patterns::FadeRandom(p) => p.init(),
|
||||
Patterns::FadeUnstable(p) => p.init(),
|
||||
Patterns::Ring(p) => p.init(),
|
||||
Patterns::RingScanner(p) => p.init(),
|
||||
Patterns::StarsRandom(p) => p.init(),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_last_iteration(&self) -> bool {
|
||||
match self {
|
||||
Patterns::Rainbow(p) => p.is_last_iteration(),
|
||||
Patterns::Fade(p) => p.is_last_iteration(),
|
||||
Patterns::ColorWipe(p) => p.is_last_iteration(),
|
||||
Patterns::Scanner(p) => p.is_last_iteration(),
|
||||
Patterns::FillRandom(p) => p.is_last_iteration(),
|
||||
Patterns::FillUnstable(p) => p.is_last_iteration(),
|
||||
Patterns::Blink(p) => p.is_last_iteration(),
|
||||
Patterns::Rain(p) => p.is_last_iteration(),
|
||||
Patterns::FadeRandom(p) => p.is_last_iteration(),
|
||||
Patterns::FadeUnstable(p) => p.is_last_iteration(),
|
||||
Patterns::Ring(p) => p.is_last_iteration(),
|
||||
Patterns::RingScanner(p) => p.is_last_iteration(),
|
||||
Patterns::StarsRandom(p) => p.is_last_iteration(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct Strip<const N: usize> {
|
||||
pub strip: [PixelColor; N],
|
||||
///
|
||||
/// a basic newtype based on a fix array size.
|
||||
///
|
||||
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
|
||||
pub struct Strip<const N: usize>([Option<PixelColor>; N]);
|
||||
|
||||
impl<const N: usize> Default for Strip<N> {
|
||||
fn default() -> Self {
|
||||
Strip([None; N])
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize> Index<usize> for Strip<N> {
|
||||
type Output = Option<PixelColor>;
|
||||
|
||||
fn index(&self, index: usize) -> &Self::Output {
|
||||
&self.0[index]
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize> IndexMut<usize> for Strip<N> {
|
||||
fn index_mut(&mut self, index: usize) -> &mut Option<PixelColor> {
|
||||
&mut self.0[index]
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize> Strip<N> {
|
||||
@@ -29,161 +185,19 @@ impl<const N: usize> Strip<N> {
|
||||
let mut data: Vec<u8> = vec![];
|
||||
|
||||
for i in 0..N {
|
||||
data.append(&mut vec![
|
||||
self.strip[i].green,
|
||||
self.strip[i].red,
|
||||
self.strip[i].blue,
|
||||
]);
|
||||
let color = self[i].unwrap_or(PixelColor::default());
|
||||
data.append(&mut vec![color.green, color.red, color.blue]);
|
||||
}
|
||||
data
|
||||
}
|
||||
|
||||
pub fn fill(&mut self, color: PixelColor) {
|
||||
self.strip = [color; N];
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize> Default for Strip<N> {
|
||||
fn default() -> Self {
|
||||
pub fn new(color: PixelColor) -> Strip<N> {
|
||||
Strip {
|
||||
strip: [PixelColor::new(); N],
|
||||
0: [Some(color); N],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Rainbow pattern /////////////////////////////////////////////////
|
||||
|
||||
pub struct Rainbow<const N: usize> {
|
||||
pub(crate) current_iteration: usize,
|
||||
pub(crate) max_iteration: Option<usize>,
|
||||
pub(crate) step: Option<usize>,
|
||||
}
|
||||
|
||||
impl<const N: usize> Iterator for Rainbow<N> {
|
||||
type Item = Strip<N>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if let Some(nbr_iteration) = self.max_iteration {
|
||||
if nbr_iteration == self.current_iteration {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
let mut strip = Strip::default();
|
||||
let step = self.step.unwrap_or(255 / N);
|
||||
for i in 0..N {
|
||||
let pos = (i * step + self.current_iteration) as u8;
|
||||
strip.strip[i] = wheel(pos)
|
||||
}
|
||||
self.current_iteration += 1;
|
||||
Some(strip)
|
||||
}
|
||||
}
|
||||
|
||||
fn wheel(index: u8) -> PixelColor {
|
||||
let pos = 255 - index;
|
||||
match pos {
|
||||
0..=85 => PixelColor {
|
||||
red: 255 - (pos * 3),
|
||||
green: 0,
|
||||
blue: pos * 3,
|
||||
},
|
||||
86..=170 => {
|
||||
let pos = pos - 85;
|
||||
PixelColor {
|
||||
red: 0,
|
||||
green: pos * 3,
|
||||
blue: 255 - (pos * 3),
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
let pos = pos - 170;
|
||||
PixelColor {
|
||||
red: pos * 3,
|
||||
green: 255 - (pos * 3),
|
||||
blue: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
/// Colorwipe pattern ////////////////////////////////////////////////////
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
pub struct ColorWipe<const N: usize> {
|
||||
pub(crate) current_iteration: usize,
|
||||
pub(crate) color: PixelColor,
|
||||
pub(crate) infinite: bool,
|
||||
}
|
||||
|
||||
impl<const N: usize> Iterator for ColorWipe<N> {
|
||||
type Item = Strip<N>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let mut strip = Strip::default();
|
||||
if self.infinite {
|
||||
self.current_iteration %= N;
|
||||
} else if self.current_iteration >= N {
|
||||
return None;
|
||||
}
|
||||
|
||||
for i in 0..self.current_iteration {
|
||||
strip.strip[i] = self.color;
|
||||
}
|
||||
self.current_iteration += 1;
|
||||
Some(strip)
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
/// fade pattern ////////////////////////////////////////////////////
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
pub struct Fade<const N: usize> {
|
||||
pub(crate) current_iteration: usize,
|
||||
pub(crate) nbr_iterations: usize,
|
||||
pub(crate) begin_color: PixelColor,
|
||||
pub(crate) end_color: PixelColor,
|
||||
}
|
||||
|
||||
fn fade_value(value_start: u8, value_end: u8, index: usize, nbr_iter: usize) -> u8 {
|
||||
((value_start as usize * (nbr_iter - index) + value_end as usize * index) / nbr_iter) as u8
|
||||
}
|
||||
|
||||
impl<const N: usize> Iterator for Fade<N> {
|
||||
type Item = Strip<N>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let mut strip = Strip::default();
|
||||
|
||||
if self.current_iteration >= self.nbr_iterations {
|
||||
return None;
|
||||
}
|
||||
|
||||
let red = fade_value(
|
||||
self.begin_color.red,
|
||||
self.end_color.red,
|
||||
self.current_iteration,
|
||||
self.nbr_iterations,
|
||||
);
|
||||
let green = fade_value(
|
||||
self.begin_color.green,
|
||||
self.end_color.green,
|
||||
self.current_iteration,
|
||||
self.nbr_iterations,
|
||||
);
|
||||
let blue = fade_value(
|
||||
self.begin_color.blue,
|
||||
self.end_color.blue,
|
||||
self.current_iteration,
|
||||
self.nbr_iterations,
|
||||
);
|
||||
|
||||
let current_color = PixelColor { red, green, blue };
|
||||
|
||||
strip.fill(current_color);
|
||||
|
||||
self.current_iteration += 1;
|
||||
Some(strip)
|
||||
}
|
||||
|
||||
pub fn fill(&mut self, color: PixelColor) {
|
||||
self.0.fill(Some(color));
|
||||
}
|
||||
}
|
||||
|
||||
40
src/patterns/blink.rs
Normal file
40
src/patterns/blink.rs
Normal file
@@ -0,0 +1,40 @@
|
||||
use crate::patterns::fade::Fade;
|
||||
use crate::patterns::Pattern;
|
||||
use crate::{impl_pattern_last_iteration, Strip};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// # Blink
|
||||
///
|
||||
/// fade from one color to an other then go back several times
|
||||
#[derive(Serialize, Deserialize, Debug, Eq, Hash, PartialEq, Copy, Clone)]
|
||||
pub struct Blink<const N: usize> {
|
||||
#[serde(default)]
|
||||
pub(crate) current_iteration: usize,
|
||||
pub(crate) max_iteration: Option<usize>,
|
||||
fade: Fade<N>,
|
||||
}
|
||||
|
||||
impl<const N: usize> Pattern for Blink<N> {
|
||||
type Strip = Strip<N>;
|
||||
impl_pattern_last_iteration!(ColorWipe);
|
||||
}
|
||||
|
||||
impl<const N: usize> Iterator for Blink<N> {
|
||||
type Item = Strip<N>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let value = self.fade.next();
|
||||
match value {
|
||||
None => {
|
||||
if self.is_last_iteration() {
|
||||
return None;
|
||||
}
|
||||
self.current_iteration += 1;
|
||||
self.fade.current_iteration = 0;
|
||||
std::mem::swap(&mut self.fade.begin_color, &mut self.fade.end_color);
|
||||
self.fade.next()
|
||||
}
|
||||
Some(value) => Some(value),
|
||||
}
|
||||
}
|
||||
}
|
||||
50
src/patterns/color_wipe.rs
Normal file
50
src/patterns/color_wipe.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
use crate::patterns::Pattern;
|
||||
use crate::pixel_color::PixelColor;
|
||||
use crate::{impl_pattern_init_background, impl_pattern_last_iteration, Strip};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// # Colorwipe pattern
|
||||
/// every pattern implement the iterator trait
|
||||
///
|
||||
/// This pattern fill the strip with a specific color, one by one.
|
||||
///
|
||||
/// ### Note
|
||||
///
|
||||
/// setting max_iteration to None lead the pattern to loop infinitely
|
||||
///
|
||||
#[derive(Serialize, Deserialize, Debug, Eq, Hash, PartialEq, Copy, Clone)]
|
||||
pub struct ColorWipe<const N: usize> {
|
||||
#[serde(default)]
|
||||
pub(crate) current_iteration: usize,
|
||||
pub(crate) max_iteration: Option<usize>,
|
||||
pub(crate) color: PixelColor,
|
||||
pub(crate) background_color: Option<PixelColor>,
|
||||
}
|
||||
|
||||
impl<const N: usize> Pattern for ColorWipe<N> {
|
||||
type Strip = Strip<N>;
|
||||
impl_pattern_init_background!(N);
|
||||
impl_pattern_last_iteration!(ColorWipe);
|
||||
}
|
||||
|
||||
impl<const N: usize> Iterator for ColorWipe<N> {
|
||||
type Item = Strip<N>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.is_last_iteration() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut strip = Strip::default();
|
||||
if let Some(c) = self.background_color {
|
||||
strip.fill(c);
|
||||
}
|
||||
let iteration = self.current_iteration % N;
|
||||
|
||||
for i in 0..iteration {
|
||||
strip[i] = Some(self.color);
|
||||
}
|
||||
self.current_iteration += 1;
|
||||
Some(strip)
|
||||
}
|
||||
}
|
||||
65
src/patterns/fade.rs
Normal file
65
src/patterns/fade.rs
Normal file
@@ -0,0 +1,65 @@
|
||||
use crate::patterns::Pattern;
|
||||
use crate::pixel_color::PixelColor;
|
||||
use crate::Strip;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// # fade pattern
|
||||
/// fade from one color to an other
|
||||
#[derive(Serialize, Deserialize, Debug, Eq, Hash, PartialEq, Copy, Clone)]
|
||||
pub struct Fade<const N: usize> {
|
||||
#[serde(default)]
|
||||
pub(crate) current_iteration: usize,
|
||||
pub(crate) steps: usize,
|
||||
pub(crate) begin_color: PixelColor,
|
||||
pub(crate) end_color: PixelColor,
|
||||
}
|
||||
|
||||
fn fade_value(value_start: u8, value_end: u8, index: usize, nbr_iter: usize) -> u8 {
|
||||
((value_start as usize * (nbr_iter - index) + value_end as usize * index) / nbr_iter) as u8
|
||||
}
|
||||
|
||||
impl<const N: usize> Pattern for Fade<N> {
|
||||
type Strip = Strip<N>;
|
||||
|
||||
fn is_last_iteration(&self) -> bool {
|
||||
self.current_iteration >= self.steps
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize> Iterator for Fade<N> {
|
||||
type Item = Strip<N>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let mut strip = Strip::default();
|
||||
|
||||
if self.is_last_iteration() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let red = fade_value(
|
||||
self.begin_color.red,
|
||||
self.end_color.red,
|
||||
self.current_iteration,
|
||||
self.steps,
|
||||
);
|
||||
let green = fade_value(
|
||||
self.begin_color.green,
|
||||
self.end_color.green,
|
||||
self.current_iteration,
|
||||
self.steps,
|
||||
);
|
||||
let blue = fade_value(
|
||||
self.begin_color.blue,
|
||||
self.end_color.blue,
|
||||
self.current_iteration,
|
||||
self.steps,
|
||||
);
|
||||
|
||||
let current_color = PixelColor { red, green, blue };
|
||||
|
||||
strip.fill(current_color);
|
||||
|
||||
self.current_iteration += 1;
|
||||
Some(strip)
|
||||
}
|
||||
}
|
||||
44
src/patterns/fade_random.rs
Normal file
44
src/patterns/fade_random.rs
Normal file
@@ -0,0 +1,44 @@
|
||||
use crate::patterns::fade::Fade;
|
||||
use crate::patterns::Pattern;
|
||||
use crate::{impl_pattern_last_iteration, Strip};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// # FadeRandom
|
||||
///
|
||||
/// fade from one color to an other with random variations applied to each pixel
|
||||
#[derive(Serialize, Deserialize, Debug, Eq, Hash, PartialEq, Copy, Clone)]
|
||||
pub struct FadeRandom<const N: usize> {
|
||||
#[serde(default)]
|
||||
pub(crate) current_iteration: usize,
|
||||
pub(crate) max_iteration: Option<usize>,
|
||||
pub(crate) stability: usize,
|
||||
fade: Fade<N>,
|
||||
}
|
||||
|
||||
impl<const N: usize> Pattern for FadeRandom<N> {
|
||||
type Strip = Strip<N>;
|
||||
impl_pattern_last_iteration!(ColorWipe);
|
||||
}
|
||||
|
||||
impl<const N: usize> Iterator for FadeRandom<N> {
|
||||
type Item = Strip<N>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let strip = self.fade.next();
|
||||
match strip {
|
||||
None => None,
|
||||
Some(mut fade) => {
|
||||
let rng = rand::thread_rng();
|
||||
for i in 0..N {
|
||||
match fade[i] {
|
||||
None => {}
|
||||
Some(pixel) => {
|
||||
fade[i] = Some(pixel.random_delta(self.stability, &mut rng.clone()))
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(fade)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
38
src/patterns/fade_unstable.rs
Normal file
38
src/patterns/fade_unstable.rs
Normal file
@@ -0,0 +1,38 @@
|
||||
use crate::patterns::fade::Fade;
|
||||
use crate::patterns::Pattern;
|
||||
use crate::{impl_pattern_last_iteration, Strip};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// # FadeUnstable
|
||||
///
|
||||
/// fade from one color to an other with random variations applied to the entire strip
|
||||
#[derive(Serialize, Deserialize, Debug, Eq, Hash, PartialEq, Copy, Clone)]
|
||||
pub struct FadeUnstable<const N: usize> {
|
||||
#[serde(default)]
|
||||
pub(crate) current_iteration: usize,
|
||||
pub(crate) max_iteration: Option<usize>,
|
||||
pub(crate) stability: usize,
|
||||
fade: Fade<N>,
|
||||
}
|
||||
|
||||
impl<const N: usize> Pattern for FadeUnstable<N> {
|
||||
type Strip = Strip<N>;
|
||||
impl_pattern_last_iteration!(ColorWipe);
|
||||
}
|
||||
|
||||
impl<const N: usize> Iterator for FadeUnstable<N> {
|
||||
type Item = Strip<N>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let strip = self.fade.next();
|
||||
match strip {
|
||||
None => None,
|
||||
Some(fade) => {
|
||||
let mut rng = rand::thread_rng();
|
||||
let pixel = fade[0].unwrap().random_delta(self.stability, &mut rng);
|
||||
|
||||
Some(Strip::new(pixel))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
40
src/patterns/fill_random.rs
Normal file
40
src/patterns/fill_random.rs
Normal file
@@ -0,0 +1,40 @@
|
||||
use crate::patterns::Pattern;
|
||||
use crate::pixel_color::PixelColor;
|
||||
use crate::{impl_pattern_last_iteration, Strip};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// # FillRandom
|
||||
///
|
||||
/// fill strip with color then apply random variation to each pixel
|
||||
#[derive(Serialize, Deserialize, Debug, Eq, Hash, PartialEq, Copy, Clone)]
|
||||
pub struct FillRandom<const N: usize> {
|
||||
pub(crate) color: PixelColor,
|
||||
pub(crate) stability: usize,
|
||||
pub(crate) max_iteration: Option<usize>,
|
||||
#[serde(default)]
|
||||
pub(crate) current_iteration: usize,
|
||||
}
|
||||
|
||||
impl<const N: usize> Pattern for FillRandom<N> {
|
||||
type Strip = Strip<N>;
|
||||
impl_pattern_last_iteration!(ColorWipe);
|
||||
}
|
||||
|
||||
impl<const N: usize> Iterator for FillRandom<N> {
|
||||
type Item = Strip<N>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.is_last_iteration() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut strip = Strip::<N>::default();
|
||||
let rng = rand::thread_rng();
|
||||
for i in 0..N {
|
||||
let c = self.color.random_delta(self.stability, &mut rng.clone());
|
||||
strip[i] = Some(c);
|
||||
}
|
||||
self.current_iteration += 1;
|
||||
Some(strip)
|
||||
}
|
||||
}
|
||||
36
src/patterns/fill_unstable.rs
Normal file
36
src/patterns/fill_unstable.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
use crate::patterns::Pattern;
|
||||
use crate::pixel_color::PixelColor;
|
||||
use crate::{impl_pattern_last_iteration, Strip};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// # FillUnstable
|
||||
///
|
||||
/// fill strip with color then apply same random variation to all pixel
|
||||
#[derive(Serialize, Deserialize, Debug, Eq, Hash, PartialEq, Copy, Clone)]
|
||||
pub struct FillUnstable<const N: usize> {
|
||||
pub(crate) color: PixelColor,
|
||||
pub(crate) stability: usize,
|
||||
pub(crate) max_iteration: Option<usize>,
|
||||
#[serde(default)]
|
||||
pub(crate) current_iteration: usize,
|
||||
}
|
||||
|
||||
impl<const N: usize> Pattern for FillUnstable<N> {
|
||||
type Strip = Strip<N>;
|
||||
impl_pattern_last_iteration!(FillUnstable);
|
||||
}
|
||||
|
||||
impl<const N: usize> Iterator for FillUnstable<N> {
|
||||
type Item = Strip<N>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.is_last_iteration() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut rng = rand::thread_rng();
|
||||
let unstable_color = self.color.random_delta(self.stability, &mut rng);
|
||||
self.current_iteration += 1;
|
||||
Some(Strip::<N>::new(unstable_color))
|
||||
}
|
||||
}
|
||||
70
src/patterns/rain.rs
Normal file
70
src/patterns/rain.rs
Normal file
@@ -0,0 +1,70 @@
|
||||
use crate::patterns::Pattern;
|
||||
use crate::pixel_color::PixelColor;
|
||||
use crate::{impl_pattern_last_iteration, Strip};
|
||||
use rand::Rng;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// # rain pattern
|
||||
/// randomly fall colors from top or bottom.
|
||||
///
|
||||
///
|
||||
/// # note
|
||||
///
|
||||
/// `stability` is the probability (between 0 an 1) that any led will light on.
|
||||
/// The probability of at least one led light on is `stability*number of lines`
|
||||
///
|
||||
/// this pattern works only for 5x5 square
|
||||
#[derive(Serialize, Deserialize, Debug, Eq, Hash, PartialEq, Copy, Clone)]
|
||||
pub struct Rain<const N: usize> {
|
||||
pub(crate) color: PixelColor,
|
||||
pub(crate) background_color: Option<PixelColor>,
|
||||
pub(crate) max_iteration: Option<usize>,
|
||||
#[serde(default)]
|
||||
pub(crate) current_iteration: usize,
|
||||
pub(crate) stability: usize,
|
||||
pub(crate) lines: usize,
|
||||
#[serde(skip)]
|
||||
drops: Strip<N>,
|
||||
}
|
||||
|
||||
impl<const N: usize> Pattern for Rain<N> {
|
||||
type Strip = Strip<N>;
|
||||
fn init(&mut self) -> Option<Self::Strip> {
|
||||
if let Some(color) = self.background_color {
|
||||
self.drops = Strip::<N>::new(color);
|
||||
Some(self.drops)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
impl_pattern_last_iteration!(ColorRain);
|
||||
}
|
||||
|
||||
impl<const N: usize> Iterator for Rain<N> {
|
||||
type Item = Strip<N>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let mut strip = Strip::<N>::default();
|
||||
|
||||
for i in 0..(N - self.lines) {
|
||||
strip[i + self.lines] = self.drops[i];
|
||||
}
|
||||
let mut rng = rand::thread_rng();
|
||||
for i in 0..self.lines {
|
||||
if rng.gen_bool(self.stability as f64 / 100 as f64) {
|
||||
strip[i] = Some(self.color);
|
||||
} else {
|
||||
let c = strip[i + self.lines];
|
||||
if self.background_color != c && c != Some(PixelColor::default()) {
|
||||
strip[i] = Some(c.unwrap() / 2);
|
||||
} else {
|
||||
strip[i] = self.background_color;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.drops = strip.clone();
|
||||
|
||||
self.current_iteration += 1;
|
||||
Some(strip)
|
||||
}
|
||||
}
|
||||
80
src/patterns/rainbow.rs
Normal file
80
src/patterns/rainbow.rs
Normal file
@@ -0,0 +1,80 @@
|
||||
use crate::patterns::Pattern;
|
||||
use crate::pixel_color::PixelColor;
|
||||
use crate::{impl_pattern_last_iteration, Strip};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// # Rainbow pattern
|
||||
///
|
||||
/// every pattern implement the iterator trait
|
||||
///
|
||||
/// This pattern display a moving rainbow over the strip
|
||||
///
|
||||
/// ### Note
|
||||
///
|
||||
/// If max iteration is let to None, it's an infinite pattern.
|
||||
///
|
||||
#[derive(Serialize, Deserialize, Debug, Eq, Hash, PartialEq, Copy, Clone)]
|
||||
pub struct Rainbow<const N: usize> {
|
||||
#[serde(default)]
|
||||
pub(crate) current_iteration: usize,
|
||||
pub(crate) max_iteration: Option<usize>,
|
||||
pub(crate) step: Option<usize>,
|
||||
}
|
||||
|
||||
impl<const N: usize> Pattern for Rainbow<N> {
|
||||
type Strip = Strip<N>;
|
||||
impl_pattern_last_iteration!(Rainbow);
|
||||
}
|
||||
|
||||
impl<const N: usize> Iterator for Rainbow<N> {
|
||||
type Item = Strip<N>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.is_last_iteration() {
|
||||
return None;
|
||||
}
|
||||
let mut strip = Strip::default();
|
||||
let step = self.step.unwrap_or(255 / N);
|
||||
for i in 0..N {
|
||||
let pos = (i * step + self.current_iteration) as u8;
|
||||
strip[i] = Some(wheel(pos))
|
||||
}
|
||||
self.current_iteration += 1;
|
||||
Some(strip)
|
||||
}
|
||||
}
|
||||
|
||||
/// compute **rgb** pixel color according to the **hsv** wheel
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `index`: position in the hsv wheel
|
||||
///
|
||||
/// returns: PixelColor
|
||||
///
|
||||
fn wheel(index: u8) -> PixelColor {
|
||||
let pos = 255 - index;
|
||||
match pos {
|
||||
0..=85 => PixelColor {
|
||||
red: 255 - (pos * 3),
|
||||
green: 0,
|
||||
blue: pos * 3,
|
||||
},
|
||||
86..=170 => {
|
||||
let pos = pos - 85;
|
||||
PixelColor {
|
||||
red: 0,
|
||||
green: pos * 3,
|
||||
blue: 255 - (pos * 3),
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
let pos = pos - 170;
|
||||
PixelColor {
|
||||
red: pos * 3,
|
||||
green: 255 - (pos * 3),
|
||||
blue: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
52
src/patterns/ring.rs
Normal file
52
src/patterns/ring.rs
Normal file
@@ -0,0 +1,52 @@
|
||||
use crate::patterns::Pattern;
|
||||
use crate::pixel_color::PixelColor;
|
||||
use crate::{impl_pattern_init_background, impl_pattern_last_iteration, Strip};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// # Ring
|
||||
///
|
||||
/// display a ring on the strip
|
||||
#[derive(Serialize, Deserialize, Debug, Eq, Hash, PartialEq, Copy, Clone)]
|
||||
pub struct Ring<const N: usize> {
|
||||
#[serde(default)]
|
||||
pub(crate) current_iteration: usize,
|
||||
pub(crate) max_iteration: Option<usize>,
|
||||
pub(crate) color: PixelColor,
|
||||
pub(crate) background_color: Option<PixelColor>,
|
||||
pub(crate) ring_size: usize,
|
||||
}
|
||||
|
||||
impl<const N: usize> Pattern for Ring<N> {
|
||||
type Strip = Strip<N>;
|
||||
impl_pattern_init_background!(N);
|
||||
impl_pattern_last_iteration!(Ring);
|
||||
}
|
||||
|
||||
impl<const N: usize> Iterator for Ring<N> {
|
||||
type Item = Strip<N>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.is_last_iteration() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut strip: Strip<N>;
|
||||
|
||||
if let Some(color) = self.background_color {
|
||||
strip = Strip::new(color);
|
||||
} else {
|
||||
strip = Strip::default();
|
||||
}
|
||||
|
||||
let nbr_iter = N / self.ring_size;
|
||||
let iter = self.current_iteration % nbr_iter;
|
||||
let first_index = iter * self.ring_size;
|
||||
let last_index = first_index + self.ring_size;
|
||||
|
||||
for i in first_index..last_index {
|
||||
strip[i] = Some(self.color);
|
||||
}
|
||||
self.current_iteration += 1;
|
||||
Some(strip)
|
||||
}
|
||||
}
|
||||
56
src/patterns/ring_scanner.rs
Normal file
56
src/patterns/ring_scanner.rs
Normal file
@@ -0,0 +1,56 @@
|
||||
use crate::patterns::Pattern;
|
||||
use crate::pixel_color::PixelColor;
|
||||
use crate::{impl_pattern_init_background, impl_pattern_last_iteration, Strip};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// # RingScanner
|
||||
///
|
||||
/// display a ring on the strip fading for the previous ones
|
||||
#[derive(Serialize, Deserialize, Debug, Eq, Hash, PartialEq, Copy, Clone)]
|
||||
pub struct RingScanner<const N: usize> {
|
||||
#[serde(default)]
|
||||
pub(crate) current_iteration: usize,
|
||||
pub(crate) max_iteration: Option<usize>,
|
||||
pub(crate) color: PixelColor,
|
||||
pub(crate) background_color: Option<PixelColor>,
|
||||
pub(crate) ring_size: usize,
|
||||
}
|
||||
|
||||
impl<const N: usize> Pattern for RingScanner<N> {
|
||||
type Strip = Strip<N>;
|
||||
impl_pattern_init_background!(N);
|
||||
impl_pattern_last_iteration!(ColorWipe);
|
||||
}
|
||||
|
||||
impl<const N: usize> Iterator for RingScanner<N> {
|
||||
type Item = Strip<N>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.is_last_iteration() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut strip: Strip<N>;
|
||||
|
||||
if let Some(color) = self.background_color {
|
||||
strip = Strip::new(color);
|
||||
} else {
|
||||
strip = Strip::default();
|
||||
}
|
||||
|
||||
let nbr_iter = N / self.ring_size;
|
||||
let iter = self.current_iteration % nbr_iter;
|
||||
|
||||
for j in 0..iter {
|
||||
let first_index = j * self.ring_size;
|
||||
let last_index = first_index + self.ring_size;
|
||||
let fade_gain = 2 * (iter - j);
|
||||
for i in first_index..last_index {
|
||||
strip[i] = Some(self.color / fade_gain as u8);
|
||||
}
|
||||
}
|
||||
|
||||
self.current_iteration += 1;
|
||||
Some(strip)
|
||||
}
|
||||
}
|
||||
59
src/patterns/scanner.rs
Normal file
59
src/patterns/scanner.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
use crate::patterns::Pattern;
|
||||
use crate::pixel_color::PixelColor;
|
||||
use crate::{impl_pattern_init_background, impl_pattern_last_iteration, Strip};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// # scanner pattern
|
||||
/// color one pixel with a color and leave a train of this color fading to black
|
||||
///
|
||||
/// # note
|
||||
///
|
||||
/// background_color work only for color not set by scanner pattern
|
||||
#[derive(Serialize, Deserialize, Debug, Eq, Hash, PartialEq, Copy, Clone)]
|
||||
pub struct Scanner<const N: usize> {
|
||||
#[serde(default)]
|
||||
pub(crate) current_iteration: usize,
|
||||
pub(crate) color: PixelColor,
|
||||
pub(crate) background_color: Option<PixelColor>,
|
||||
#[serde(default)]
|
||||
pub(crate) max_iteration: Option<usize>,
|
||||
}
|
||||
|
||||
impl<const N: usize> Pattern for Scanner<N> {
|
||||
type Strip = Strip<N>;
|
||||
impl_pattern_init_background!(N);
|
||||
impl_pattern_last_iteration!(ColorWipe);
|
||||
}
|
||||
|
||||
impl<const N: usize> Iterator for Scanner<N> {
|
||||
type Item = Strip<N>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.is_last_iteration() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut strip: Strip<N>;
|
||||
|
||||
if let Some(color) = self.background_color {
|
||||
strip = Strip::new(color);
|
||||
} else {
|
||||
strip = Strip::default();
|
||||
}
|
||||
|
||||
let current_led = self.current_iteration % N;
|
||||
let min_led;
|
||||
if current_led < 8 {
|
||||
min_led = 0;
|
||||
} else {
|
||||
min_led = current_led - 8;
|
||||
}
|
||||
let mut c = self.color;
|
||||
for i in min_led..current_led {
|
||||
strip[i] = Some(c);
|
||||
c = c / 2;
|
||||
}
|
||||
self.current_iteration += 1;
|
||||
Some(strip)
|
||||
}
|
||||
}
|
||||
83
src/patterns/stars_random.rs
Normal file
83
src/patterns/stars_random.rs
Normal file
@@ -0,0 +1,83 @@
|
||||
use crate::patterns::Pattern;
|
||||
use crate::pixel_color::PixelColor;
|
||||
use crate::{impl_pattern_last_iteration, Strip};
|
||||
use rand::Rng;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// # StarsRandom
|
||||
///
|
||||
/// fill strip with background color then color random pixel with other color fading to background color
|
||||
#[derive(Serialize, Deserialize, Debug, Eq, Hash, PartialEq, Copy, Clone)]
|
||||
pub struct StarsRandom<const N: usize> {
|
||||
pub(crate) color: PixelColor,
|
||||
pub(crate) ratio: usize,
|
||||
pub(crate) max_iteration: Option<usize>,
|
||||
pub(crate) background_color: Option<PixelColor>,
|
||||
pub(crate) steps: usize,
|
||||
#[serde(default)]
|
||||
pub(crate) current_iteration: usize,
|
||||
#[serde(skip)]
|
||||
strip: Strip<N>,
|
||||
#[serde(skip)]
|
||||
step: (i8, i8, i8),
|
||||
}
|
||||
|
||||
impl<const N: usize> Pattern for StarsRandom<N> {
|
||||
type Strip = Strip<N>;
|
||||
fn init(&mut self) -> Option<Self::Strip> {
|
||||
if let Some(color) = self.background_color {
|
||||
self.strip = Strip::<N>::new(color);
|
||||
let red = self.color.red as i16 - color.red as i16;
|
||||
let green = color.green as i16 - self.color.green as i16;
|
||||
let blue = self.color.blue as i16 - color.blue as i16;
|
||||
self.step = (
|
||||
-(red / self.steps as i16) as i8,
|
||||
-(green / self.steps as i16) as i8,
|
||||
-(blue / self.steps as i16) as i8,
|
||||
);
|
||||
Some(self.strip)
|
||||
} else {
|
||||
self.step = (
|
||||
-((self.color.red / self.steps as u8) as i8),
|
||||
-((self.color.green / self.steps as u8) as i8),
|
||||
-((self.color.blue / self.steps as u8) as i8),
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
impl_pattern_last_iteration!(StarsRandom);
|
||||
}
|
||||
|
||||
impl<const N: usize> Iterator for StarsRandom<N> {
|
||||
type Item = Strip<N>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.is_last_iteration() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut rng = rand::thread_rng();
|
||||
for i in 0..N {
|
||||
let current_color = self.strip[i];
|
||||
if rng.gen_bool(1.0 / self.ratio as f64) {
|
||||
self.strip[i] = Some(self.color);
|
||||
} else {
|
||||
let color = current_color.unwrap();
|
||||
let background = self.background_color.unwrap_or(PixelColor::default());
|
||||
let distance = PixelColor {
|
||||
red: self.step.0.abs() as u8,
|
||||
green: self.step.1.abs() as u8,
|
||||
blue: self.step.2.abs() as u8,
|
||||
};
|
||||
if color.is_closer(background, distance) {
|
||||
self.strip[i] = self.background_color;
|
||||
} else {
|
||||
self.strip[i] = Some(color.delta(self.step));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.current_iteration += 1;
|
||||
Some(self.strip)
|
||||
}
|
||||
}
|
||||
122
src/pixel_color.rs
Normal file
122
src/pixel_color.rs
Normal file
@@ -0,0 +1,122 @@
|
||||
use rand::prelude::ThreadRng;
|
||||
use rand::Rng;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use std::fmt;
|
||||
use std::ops::{Add, AddAssign, Div, Sub, SubAssign};
|
||||
|
||||
///
|
||||
/// a struct for an RGB color
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Default, Eq, Hash, PartialEq)]
|
||||
pub struct PixelColor {
|
||||
pub(crate) red: u8,
|
||||
pub(crate) green: u8,
|
||||
pub(crate) blue: u8,
|
||||
}
|
||||
|
||||
impl fmt::Display for PixelColor {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "[{},{},{}]", self.red, self.green, self.blue)
|
||||
}
|
||||
}
|
||||
|
||||
impl Div<u8> for PixelColor {
|
||||
type Output = PixelColor;
|
||||
|
||||
fn div(self, rhs: u8) -> Self::Output {
|
||||
PixelColor {
|
||||
red: self.red / rhs,
|
||||
green: self.green / rhs,
|
||||
blue: self.blue / rhs,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Add for PixelColor {
|
||||
type Output = Self;
|
||||
|
||||
fn add(self, rhs: Self) -> Self::Output {
|
||||
PixelColor {
|
||||
red: self.red.saturating_add(rhs.red),
|
||||
green: self.green.saturating_add(rhs.green),
|
||||
blue: self.blue.saturating_add(rhs.blue),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Sub<u8> for PixelColor {
|
||||
type Output = Self;
|
||||
|
||||
fn sub(self, rhs: u8) -> Self::Output {
|
||||
PixelColor {
|
||||
red: self.red.saturating_sub(rhs),
|
||||
green: self.green.saturating_sub(rhs),
|
||||
blue: self.blue.saturating_sub(rhs),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SubAssign for PixelColor {
|
||||
fn sub_assign(&mut self, rhs: Self) {
|
||||
self.red = self.red.saturating_sub(rhs.red);
|
||||
self.green = self.red.saturating_sub(rhs.green);
|
||||
self.blue = self.red.saturating_sub(rhs.blue);
|
||||
}
|
||||
}
|
||||
|
||||
impl AddAssign for PixelColor {
|
||||
fn add_assign(&mut self, rhs: Self) {
|
||||
self.red = self.red.saturating_add(rhs.red);
|
||||
self.green = self.red.saturating_add(rhs.green);
|
||||
self.blue = self.red.saturating_add(rhs.blue);
|
||||
}
|
||||
}
|
||||
|
||||
impl PixelColor {
|
||||
/// apply random delta to pixel and return computed value
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `pixel`: pixel to apply delta
|
||||
/// * `stability`: amplitude of delta between 0 and `stability`
|
||||
///
|
||||
/// returns: PixelColor
|
||||
///
|
||||
pub fn random_delta(&self, stability: usize, rng: &mut ThreadRng) -> PixelColor {
|
||||
let minus_interval = -(stability as i8);
|
||||
let max_interval = stability as i8;
|
||||
let red_delta = rng.gen_range(minus_interval..max_interval) as i8;
|
||||
let green_delta = rng.gen_range(minus_interval..max_interval) as i8;
|
||||
let blue_delta = rng.gen_range(minus_interval..max_interval) as i8;
|
||||
self.delta((red_delta, green_delta, blue_delta))
|
||||
}
|
||||
|
||||
pub fn delta(&self, delta: (i8, i8, i8)) -> PixelColor {
|
||||
let (red, green, blue) = delta;
|
||||
|
||||
let red = self.red as i16 + red as i16;
|
||||
let green = self.green as i16 + green as i16;
|
||||
let blue = self.blue as i16 + blue as i16;
|
||||
|
||||
let bound = |color: i16| {
|
||||
if color < 0 {
|
||||
0
|
||||
} else if color > 255 {
|
||||
255
|
||||
} else {
|
||||
color as u8
|
||||
}
|
||||
};
|
||||
|
||||
PixelColor {
|
||||
red: bound(red),
|
||||
green: bound(green),
|
||||
blue: bound(blue),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_closer(&self, pixel: PixelColor, distance: PixelColor) -> bool {
|
||||
(self.red as i16 - pixel.red as i16).abs() <= distance.red as i16
|
||||
&& (self.green as i16 - pixel.green as i16).abs() <= distance.green as i16
|
||||
&& (self.blue as i16 - pixel.blue as i16).abs() <= distance.blue as i16
|
||||
}
|
||||
}
|
||||
234
src/runner.rs
Normal file
234
src/runner.rs
Normal file
@@ -0,0 +1,234 @@
|
||||
use crate::config::Device;
|
||||
use crate::patterns::{Pattern, Patterns};
|
||||
use crate::pixel_color::PixelColor;
|
||||
use crate::Strip;
|
||||
use bluer::Address;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::future::Future;
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::watch::{Receiver, Sender};
|
||||
use tokio::time::Instant;
|
||||
|
||||
pub(crate) mod period {
|
||||
use serde::{de::Error as _, Deserialize, Deserializer};
|
||||
use std::time::Duration;
|
||||
|
||||
/// Custom deserializer for period from millisecond value
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `deserializer`:
|
||||
///
|
||||
/// returns: Result<Duration, <D as Deserializer>::Error>
|
||||
///
|
||||
pub fn deserialize<'de, D>(deserializer: D) -> Result<Duration, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
String::deserialize(deserializer)?
|
||||
.parse::<u64>()
|
||||
.and_then(|d| Ok(Duration::from_millis(d)))
|
||||
.map_err(|e| D::Error::custom(format!("{}", e)))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Eq, Hash, PartialEq, Copy, Clone)]
|
||||
pub struct Context<const N: usize> {
|
||||
pub(crate) pattern: Patterns<N>,
|
||||
#[serde(deserialize_with = "period::deserialize")]
|
||||
pub period: Duration,
|
||||
}
|
||||
|
||||
pub(crate) type DeviceSequence<const N: usize> = Vec<Context<N>>;
|
||||
|
||||
/// iterate over `runner` and stream strip data to `tx` channel
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `runner`: Sequence to stream
|
||||
/// * `tx`: channel to stream data to
|
||||
///
|
||||
/// returns: ()
|
||||
///
|
||||
async fn runner_loop<const N: usize>(runner: DeviceSequence<N>, tx: Sender<Strip<N>>) {
|
||||
let mut background = Strip::<N>::default();
|
||||
for mut context in runner {
|
||||
let mut strip = context.pattern.init().unwrap_or(background);
|
||||
let delay = context.period;
|
||||
let mut next_instant = Instant::now() + delay;
|
||||
while tx.send(strip).is_ok() {
|
||||
if let Some(value) = context.pattern.next() {
|
||||
strip = apply_mask(background, value);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
tokio::time::sleep_until(next_instant).await;
|
||||
next_instant += delay;
|
||||
}
|
||||
background = strip;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fn apply_mask<const N: usize>(previous_strip: Strip<N>, strip: Strip<{ N }>) -> Strip<N> {
|
||||
let mut new_strip = Strip::default();
|
||||
for i in 0..N {
|
||||
if let Some(color) = strip[i] {
|
||||
new_strip[i] = Some(color);
|
||||
} else {
|
||||
new_strip[i] = previous_strip[i];
|
||||
}
|
||||
}
|
||||
new_strip
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Spectacle<const N: usize> {
|
||||
runners: HashMap<DeviceSequence<N>, (Sender<Strip<N>>, Receiver<Strip<N>>)>,
|
||||
channels: HashMap<Address, Receiver<Strip<N>>>,
|
||||
default_runner: Option<(DeviceSequence<N>, Sender<Strip<N>>)>,
|
||||
default_receiver: Option<Receiver<Strip<N>>>,
|
||||
}
|
||||
|
||||
impl<const N: usize> Spectacle<N> {
|
||||
pub fn add(&mut self, mac_address: Address, runner: &DeviceSequence<N>) {
|
||||
if let Some((_, rx)) = self.runners.get(runner as &DeviceSequence<N>) {
|
||||
self.channels.insert(mac_address, rx.clone());
|
||||
} else {
|
||||
let (tx, rx) = tokio::sync::watch::channel(Strip::<N>::new(PixelColor {
|
||||
red: 0,
|
||||
green: 255,
|
||||
blue: 0,
|
||||
}));
|
||||
self.runners.insert(runner.clone(), (tx, rx.clone()));
|
||||
self.channels.insert(mac_address, rx.clone());
|
||||
}
|
||||
}
|
||||
|
||||
/// add default sequence for any device implementing **adafruit neopixel BLE**
|
||||
/// not specified in an other sequence.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `runner`: sequence to stream
|
||||
///
|
||||
/// returns: ()
|
||||
///
|
||||
pub fn add_default(&mut self, runner: DeviceSequence<N>) {
|
||||
let (tx, rx) = tokio::sync::watch::channel(Strip::<N>::new(PixelColor {
|
||||
red: 0,
|
||||
green: 0,
|
||||
blue: 255,
|
||||
}));
|
||||
self.default_runner = Some((runner, tx));
|
||||
self.default_receiver = Some(rx);
|
||||
}
|
||||
|
||||
/// Get channel associated with the mac address passed in parameters. If no channel found,
|
||||
/// give back the default channel if existing.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `mac_address`: mac address to look for
|
||||
///
|
||||
/// returns: Option<Receiver<Strip<{ N }>>>
|
||||
///
|
||||
pub fn get_channel(&self, mac_address: &Address) -> Option<Receiver<Strip<N>>> {
|
||||
let channel = self.channels.get(mac_address);
|
||||
match channel {
|
||||
None => {
|
||||
let d = &self.default_receiver;
|
||||
match d {
|
||||
None => None,
|
||||
Some(runner) => {
|
||||
println!("default rx");
|
||||
Some(runner.clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(channel) => {
|
||||
println!("rx found ");
|
||||
Some(channel.clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// start to stream configured sequences. Returns the joinhandle of task to wait for
|
||||
///
|
||||
///
|
||||
///
|
||||
/// returns: Vec<impl Future<Output = ()>>
|
||||
///
|
||||
pub fn run(&mut self) -> Vec<impl Future<Output = ()>> {
|
||||
let mut joinhandles = vec![];
|
||||
|
||||
if self.default_runner.is_some() {
|
||||
let d = std::mem::take(&mut self.default_runner).unwrap();
|
||||
joinhandles.push(runner_loop(d.0.clone(), d.1));
|
||||
println!("add default runner loop");
|
||||
}
|
||||
|
||||
for (runner, (tx, _)) in self.runners.drain().into_iter() {
|
||||
joinhandles.push(runner_loop(runner.clone(), tx));
|
||||
println!("add custom runner loop");
|
||||
}
|
||||
joinhandles
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize> From<Vec<Device<N>>> for Spectacle<N> {
|
||||
fn from(config: Vec<Device<N>>) -> Self {
|
||||
let mut spectacle: Spectacle<N> = Spectacle::default();
|
||||
for device in config {
|
||||
if let Some(mac_addresses) = device.mac_addresses {
|
||||
for mac_address in mac_addresses {
|
||||
let mac_address = Address::from_str(&*mac_address);
|
||||
match mac_address {
|
||||
Ok(address) => {
|
||||
spectacle.add(address, &device.sequence);
|
||||
}
|
||||
Err(_) => {
|
||||
println!("error reading mac address");
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
spectacle.add_default(device.sequence);
|
||||
}
|
||||
}
|
||||
spectacle
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn apply_mask() {
|
||||
let pixel_entry = PixelColor {
|
||||
red: 10,
|
||||
green: 10,
|
||||
blue: 10,
|
||||
};
|
||||
|
||||
let pixel_mask = PixelColor {
|
||||
red: 20,
|
||||
green: 20,
|
||||
blue: 20,
|
||||
};
|
||||
|
||||
let mut entry = Strip::<5>::new(pixel_entry);
|
||||
entry[0] = None;
|
||||
entry[1] = None;
|
||||
|
||||
let mut mask = Strip::<5>::new(pixel_mask);
|
||||
mask[0] = None;
|
||||
|
||||
let mut expected = entry.clone();
|
||||
expected[1] = Some(pixel_mask);
|
||||
assert_eq!(expected, super::apply_mask(mask, entry));
|
||||
}
|
||||
}
|
||||
1
src/spectacle.rs
Normal file
1
src/spectacle.rs
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
Reference in New Issue
Block a user