use ola::DmxBuffer;
use parking_lot::RwLock;
use spin_sleep;
use std::sync::atomic::{AtomicU16, Ordering};
use std::sync::mpsc::Sender;
use std::sync::Arc;
use std::thread::{self, JoinHandle};
use std::time::{Duration, Instant};
use tracing::error;
use crate::config;
use crate::playsync::CancelHandle;
use super::engine::DmxMessage;
const UNIVERSE_SIZE: usize = 512;
pub(crate) const TARGET_HZ: f64 = 44.0;
pub(crate) struct Universe {
current: Arc<RwLock<Vec<f64>>>,
target: Arc<RwLock<Vec<f64>>>,
rates: Arc<RwLock<Vec<f64>>>,
global_dim_rate: RwLock<f64>,
max_channels: Arc<AtomicU16>,
config: config::Universe,
cancel_handle: CancelHandle,
ola_sender: Sender<DmxMessage>,
last_effect_values: RwLock<Vec<u8>>,
}
impl Universe {
pub(super) fn new(
config: config::Universe,
cancel_handle: CancelHandle,
ola_sender: Sender<DmxMessage>,
) -> Universe {
Universe {
rates: Arc::new(RwLock::new(vec![0.0; UNIVERSE_SIZE])),
current: Arc::new(RwLock::new(vec![0.0; UNIVERSE_SIZE])),
target: Arc::new(RwLock::new(vec![0.0; UNIVERSE_SIZE])),
global_dim_rate: RwLock::new(1.0),
max_channels: Arc::new(AtomicU16::new(0)),
config,
cancel_handle,
ola_sender,
last_effect_values: RwLock::new(vec![0; UNIVERSE_SIZE]),
}
}
#[cfg(test)]
pub fn get_dim_speed(&self) -> f64 {
*self.global_dim_rate.read()
}
#[cfg(test)]
pub fn get_target_value(&self, channel_index: usize) -> f64 {
self.target.read()[channel_index]
}
pub fn update_dim_speed(&self, dim_rate: Duration) {
let mut global_dim_rate = self.global_dim_rate.write();
if dim_rate.is_zero() {
*global_dim_rate = 1.0
} else {
*global_dim_rate = dim_rate.as_secs_f64() * TARGET_HZ
}
}
pub fn update_channel_data(&self, channel: u16, value: u8, dim: bool) {
let channel_index = if channel > 0 {
usize::from(channel - 1) } else {
0 };
let value = f64::from(value);
self.target.write()[channel_index] = value;
self.rates.write()[channel_index] = if dim {
(value - self.current.read()[channel_index]) / *self.global_dim_rate.read()
} else {
0.0
};
let _ =
self.max_channels
.fetch_update(Ordering::SeqCst, Ordering::SeqCst, |current_channel| {
if channel >= current_channel {
return Some(channel + 1);
}
None
});
}
pub fn clear_effect_cache(&self) {
self.last_effect_values.write().fill(0);
}
pub fn update_effect_commands(&self, commands: Vec<(u16, u8)>) {
let mut last = self.last_effect_values.write();
let mut target = self.target.write();
let mut rates = self.rates.write();
for (channel, value) in commands {
let idx = if channel > 0 {
usize::from(channel - 1)
} else {
0
};
if last[idx] != value {
last[idx] = value;
target[idx] = f64::from(value);
rates[idx] = 0.0;
let _ = self.max_channels.fetch_update(
Ordering::SeqCst,
Ordering::SeqCst,
|current_channel| {
if channel >= current_channel {
Some(channel + 1)
} else {
None
}
},
);
}
}
}
pub fn start_thread(&self) -> JoinHandle<()> {
let rates = self.rates.clone();
let current = self.current.clone();
let target = self.target.clone();
let max_channels = self.max_channels.clone();
let cancel_handle = self.cancel_handle.clone();
let universe = u32::from(self.config.universe());
let ola_sender = self.ola_sender.clone();
thread::spawn(move || {
let mut last_time = Instant::now();
let tick_duration = Duration::from_secs(1).div_f64(TARGET_HZ);
let mut buffer = DmxBuffer::new();
loop {
if cancel_handle.is_cancelled() {
return;
}
if Universe::approach_target(&rates, ¤t, &target, &max_channels, &mut buffer)
{
if let Err(e) = ola_sender.send(DmxMessage {
universe,
buffer: buffer.clone(),
}) {
error!(
err = e.to_string(),
"Error sending DMX packet to universe {}", universe
);
}
}
last_time += tick_duration;
spin_sleep::sleep(last_time - Instant::now());
}
})
}
fn approach_target(
rates: &Arc<RwLock<Vec<f64>>>,
current: &Arc<RwLock<Vec<f64>>>,
target: &Arc<RwLock<Vec<f64>>>,
max_channels: &Arc<AtomicU16>,
buffer: &mut DmxBuffer,
) -> bool {
let target = target.read();
let rates = rates.read();
let mut current = current.write();
let mut changed = false;
for i in 0..usize::from(max_channels.load(Ordering::Relaxed)) {
if (current[i] - target[i]).abs() > f64::EPSILON {
changed = true;
if rates[i] > 0.0 {
current[i] = (current[i] + rates[i]).min(target[i])
} else if rates[i] == 0.0 {
current[i] = target[i]
} else {
current[i] = (current[i] + rates[i]).max(target[i])
}
buffer.set_channel(
i,
current[i].min(u8::MAX.into()).max(u8::MIN.into()).round() as u8,
);
}
}
changed
}
}
#[cfg(test)]
mod test {
use std::{
error::Error,
sync::mpsc::{self, Receiver},
thread,
time::Duration,
};
use ola::DmxBuffer;
use crate::{
config,
dmx::{engine::DmxMessage, universe::TARGET_HZ},
playsync::CancelHandle,
};
use super::Universe;
fn new_universe() -> (Universe, Receiver<DmxMessage>) {
let (sender, receiver) = mpsc::channel();
(
Universe::new(
config::Universe::new(1, "universe".to_string()),
CancelHandle::new(),
sender,
),
receiver,
)
}
#[test]
fn test_thread() -> Result<(), Box<dyn Error>> {
let (universe, receiver) = new_universe();
let receiver_handle = thread::spawn(move || receiver.recv());
let handle = universe.start_thread();
universe.update_channel_data(0, 0, false);
universe.update_channel_data(1, 50, false);
let result = receiver_handle
.join()
.map_err(|_| "Error waiting for join".to_string())??;
assert_eq!([50u8, 0u8], result.buffer.as_slice()[0..2]);
universe.cancel_handle.cancel();
handle
.join()
.map_err(|_| "Error waiting for join".to_string())?;
Ok(())
}
#[test]
fn test_no_dimming() {
let (universe, _) = new_universe();
universe.update_channel_data(1, 0, true);
universe.update_channel_data(1, 50, true);
universe.update_channel_data(2, 100, true);
universe.update_channel_data(3, 150, true);
universe.update_channel_data(4, 200, true);
let mut buffer = DmxBuffer::new();
Universe::approach_target(
&universe.rates,
&universe.current,
&universe.target,
&universe.max_channels,
&mut buffer,
);
assert_eq!([50u8, 100u8, 150u8, 200u8, 0u8], buffer.as_slice()[0..5]);
}
#[test]
fn test_ignore_dimming() {
let (universe, _) = new_universe();
universe.update_dim_speed(Duration::from_secs(2));
universe.update_channel_data(1, 0, false);
universe.update_channel_data(2, 50, false);
universe.update_channel_data(3, 100, false);
universe.update_channel_data(4, 150, false);
universe.update_channel_data(5, 200, false);
let mut buffer = DmxBuffer::new();
Universe::approach_target(
&universe.rates,
&universe.current,
&universe.target,
&universe.max_channels,
&mut buffer,
);
assert_eq!([0u8, 50u8, 100u8, 150u8, 200u8], buffer.as_slice()[0..5]);
universe.update_channel_data(2, 50u8, false);
universe.update_channel_data(3, 200u8, false);
universe.update_channel_data(4, 0, false);
Universe::approach_target(
&universe.rates,
&universe.current,
&universe.target,
&universe.max_channels,
&mut buffer,
);
assert_eq!([0u8, 50u8, 200u8, 0u8, 200u8], buffer.as_slice()[0..5]);
}
#[test]
fn test_dimming_over_two_seconds() {
let (universe, _) = new_universe();
universe.update_dim_speed(Duration::from_secs(2));
universe.update_channel_data(1, 0, true);
universe.update_channel_data(2, 50, true);
universe.update_channel_data(3, 100, true);
universe.update_channel_data(4, 150, true);
universe.update_channel_data(5, 200, true);
let mut buffer = DmxBuffer::new();
for _ in 0..(TARGET_HZ as usize) {
assert!(Universe::approach_target(
&universe.rates,
&universe.current,
&universe.target,
&universe.max_channels,
&mut buffer,
))
}
assert_eq!([0u8, 25u8, 50u8, 75u8, 100u8], buffer.as_slice()[0..5]);
for _ in 0..(TARGET_HZ as usize) {
assert!(Universe::approach_target(
&universe.rates,
&universe.current,
&universe.target,
&universe.max_channels,
&mut buffer,
))
}
assert_eq!([0u8, 50u8, 100u8, 150u8, 200u8], buffer.as_slice()[0..5]);
for _ in 0..(TARGET_HZ as usize) {
assert!(!Universe::approach_target(
&universe.rates,
&universe.current,
&universe.target,
&universe.max_channels,
&mut buffer,
))
}
assert_eq!([0u8, 50u8, 100u8, 150u8, 200u8], buffer.as_slice()[0..5]);
}
#[test]
fn test_separate_dimming() {
let (universe, _) = new_universe();
universe.update_dim_speed(Duration::from_secs(1));
universe.update_channel_data(1, 100, true);
let mut buffer = DmxBuffer::new();
let _ = Universe::approach_target(
&universe.rates,
&universe.current,
&universe.target,
&universe.max_channels,
&mut buffer,
);
universe.update_dim_speed(Duration::from_secs(2));
universe.update_channel_data(1, 100, true);
for _ in 0..(TARGET_HZ as usize) {
assert!(Universe::approach_target(
&universe.rates,
&universe.current,
&universe.target,
&universe.max_channels,
&mut buffer,
))
}
assert!(buffer.as_slice()[0] >= 50 && buffer.as_slice()[0] <= 52);
for _ in 0..(TARGET_HZ as usize) {
assert!(Universe::approach_target(
&universe.rates,
&universe.current,
&universe.target,
&universe.max_channels,
&mut buffer,
))
}
assert_eq!([100u8], buffer.as_slice()[0..1]);
}
#[test]
fn test_dimming_override() {
let (universe, _) = new_universe();
universe.update_dim_speed(Duration::from_secs(1));
universe.update_channel_data(1, 100, true);
let mut buffer = DmxBuffer::new();
for _ in 0..((TARGET_HZ / 2.0) as usize) {
assert!(Universe::approach_target(
&universe.rates,
&universe.current,
&universe.target,
&universe.max_channels,
&mut buffer,
))
}
assert_eq!([50u8], buffer.as_slice()[0..1]);
universe.update_dim_speed(Duration::from_secs(2));
universe.update_channel_data(1, 100, true);
for _ in 0..(TARGET_HZ as usize) {
assert!(Universe::approach_target(
&universe.rates,
&universe.current,
&universe.target,
&universe.max_channels,
&mut buffer,
))
}
assert_eq!([75u8], buffer.as_slice()[0..1]);
for _ in 0..(TARGET_HZ as usize) {
assert!(Universe::approach_target(
&universe.rates,
&universe.current,
&universe.target,
&universe.max_channels,
&mut buffer,
))
}
assert_eq!([100u8], buffer.as_slice()[0..1]);
}
#[test]
fn test_update_dim_speed_zero_duration() {
let (universe, _) = new_universe();
universe.update_dim_speed(Duration::ZERO);
assert_eq!(universe.get_dim_speed(), 1.0);
}
#[test]
fn test_update_dim_speed_one_second() {
let (universe, _) = new_universe();
universe.update_dim_speed(Duration::from_secs(1));
assert_eq!(universe.get_dim_speed(), TARGET_HZ);
}
#[test]
fn test_update_channel_data_channel_zero() {
let (universe, _) = new_universe();
universe.update_channel_data(0, 100, false);
assert_eq!(universe.get_target_value(0), 100.0);
}
#[test]
fn test_update_effect_commands_channel_zero() {
let (universe, _) = new_universe();
let commands = vec![(0u16, 128u8)];
universe.update_effect_commands(commands);
assert_eq!(universe.get_target_value(0), 128.0);
}
#[test]
fn test_update_effect_commands_basic() {
let (universe, _) = new_universe();
let commands = vec![(1u16, 200u8), (2, 100), (3, 50)];
universe.update_effect_commands(commands);
assert_eq!(universe.get_target_value(0), 200.0);
assert_eq!(universe.get_target_value(1), 100.0);
assert_eq!(universe.get_target_value(2), 50.0);
}
#[test]
fn test_update_effect_commands_deduplicates() {
let (universe, _) = new_universe();
let commands = vec![(1u16, 200u8), (2, 100)];
universe.update_effect_commands(commands);
let commands = vec![(1u16, 200u8), (2, 100)];
universe.update_effect_commands(commands);
assert_eq!(universe.get_target_value(0), 200.0);
assert_eq!(universe.get_target_value(1), 100.0);
}
#[test]
fn test_clear_effect_cache() {
let (universe, _) = new_universe();
universe.update_effect_commands(vec![(1u16, 200u8)]);
assert_eq!(universe.get_target_value(0), 200.0);
universe.clear_effect_cache();
universe.update_effect_commands(vec![(1u16, 200u8)]);
assert_eq!(universe.get_target_value(0), 200.0);
}
#[test]
fn test_approach_target_no_change_returns_false() {
let (universe, _) = new_universe();
universe.update_channel_data(1, 0, false);
let mut buffer = DmxBuffer::new();
Universe::approach_target(
&universe.rates,
&universe.current,
&universe.target,
&universe.max_channels,
&mut buffer,
);
let changed = Universe::approach_target(
&universe.rates,
&universe.current,
&universe.target,
&universe.max_channels,
&mut buffer,
);
assert!(!changed);
}
#[test]
fn test_dimming_down_from_higher_to_lower() {
let (universe, _) = new_universe();
universe.update_dim_speed(Duration::from_secs(1));
universe.update_channel_data(1, 200, true);
let mut buffer = DmxBuffer::new();
for _ in 0..(TARGET_HZ as usize) {
assert!(Universe::approach_target(
&universe.rates,
&universe.current,
&universe.target,
&universe.max_channels,
&mut buffer,
))
}
assert_eq!([200u8], buffer.as_slice()[0..1]);
universe.update_channel_data(1, 100, true);
for _ in 0..((TARGET_HZ / 2.0) as usize) {
assert!(Universe::approach_target(
&universe.rates,
&universe.current,
&universe.target,
&universe.max_channels,
&mut buffer,
))
}
let value = buffer.as_slice()[0];
assert!(
(148..=152).contains(&value),
"Expected ~150 when dimming down from 200 to 100, got {}",
value
);
for _ in 0..((TARGET_HZ / 2.0) as usize) {
assert!(Universe::approach_target(
&universe.rates,
&universe.current,
&universe.target,
&universe.max_channels,
&mut buffer,
))
}
assert_eq!([100u8], buffer.as_slice()[0..1]);
}
}