use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering};
fn store_f64(atom: &AtomicU64, val: f64) {
atom.store(val.to_bits(), Ordering::Relaxed);
}
fn load_f64(atom: &AtomicU64) -> f64 {
f64::from_bits(atom.load(Ordering::Relaxed))
}
struct Slot {
target: AtomicU64,
current: AtomicU64,
rate: AtomicU64,
active: AtomicBool,
}
impl Slot {
fn new() -> Self {
Self {
target: AtomicU64::new(0f64.to_bits()),
current: AtomicU64::new(0f64.to_bits()),
rate: AtomicU64::new(0f64.to_bits()),
active: AtomicBool::new(false),
}
}
}
impl Default for MidiDmxStore {
fn default() -> Self {
Self::new()
}
}
pub struct MidiDmxStore {
slots: Vec<Slot>,
channel_to_slot: HashMap<(u16, u16), usize>,
slot_to_fixture: Vec<(String, String)>,
dim_rates: HashMap<u16, AtomicU64>,
generation: AtomicU32,
interpolating: AtomicBool,
}
impl MidiDmxStore {
pub fn new() -> Self {
Self {
slots: Vec::new(),
channel_to_slot: HashMap::new(),
slot_to_fixture: Vec::new(),
dim_rates: HashMap::new(),
generation: AtomicU32::new(0),
interpolating: AtomicBool::new(false),
}
}
pub fn register_slot(
&mut self,
universe: u16,
dmx_channel: u16,
fixture_name: &str,
channel_name: &str,
) -> usize {
let slot_index = self.slots.len();
self.slots.push(Slot::new());
self.channel_to_slot
.insert((universe, dmx_channel), slot_index);
self.slot_to_fixture
.push((fixture_name.to_string(), channel_name.to_string()));
slot_index
}
pub fn register_universe(&mut self, universe_id: u16) {
self.dim_rates
.entry(universe_id)
.or_insert_with(|| AtomicU64::new(1.0f64.to_bits()));
}
pub fn write(&self, universe: u16, channel: u16, value: u8, dim: bool) {
if let Some(&slot_index) = self.channel_to_slot.get(&(universe, channel)) {
let slot = &self.slots[slot_index];
let value_f64 = f64::from(value);
store_f64(&slot.target, value_f64);
if dim {
let current = load_f64(&slot.current);
let dim_rate = self.dim_rates.get(&universe).map(load_f64).unwrap_or(1.0);
let rate = if dim_rate > f64::EPSILON {
(value_f64 - current) / dim_rate
} else {
0.0 };
store_f64(&slot.rate, rate);
} else {
store_f64(&slot.rate, 0.0); }
slot.active.store(true, Ordering::Release);
self.generation.fetch_add(1, Ordering::Release);
}
}
pub fn set_dim_rate(&self, universe: u16, rate_ticks: f64) {
if let Some(dim_rate) = self.dim_rates.get(&universe) {
store_f64(dim_rate, rate_ticks);
}
}
pub fn tick(&self) -> bool {
let mut any_changed = false;
let mut still_interpolating = false;
for slot in &self.slots {
if !slot.active.load(Ordering::Acquire) {
continue;
}
let current = load_f64(&slot.current);
let target = load_f64(&slot.target);
let rate = load_f64(&slot.rate);
let new_current = if rate > f64::EPSILON {
(current + rate).min(target)
} else if rate < -f64::EPSILON {
(current + rate).max(target)
} else {
target };
if (new_current - current).abs() > f64::EPSILON {
any_changed = true;
}
if (new_current - target).abs() > f64::EPSILON {
still_interpolating = true;
}
store_f64(&slot.current, new_current);
}
self.interpolating
.store(still_interpolating, Ordering::Relaxed);
if any_changed {
self.generation.fetch_add(1, Ordering::Release);
}
any_changed
}
pub fn generation(&self) -> u32 {
self.generation.load(Ordering::Acquire)
}
#[cfg(test)]
pub fn has_active_slots(&self) -> bool {
self.slots.iter().any(|s| s.active.load(Ordering::Relaxed))
}
pub fn iter_active(&self) -> impl Iterator<Item = (usize, f64)> + '_ {
self.slots.iter().enumerate().filter_map(|(i, slot)| {
if slot.active.load(Ordering::Acquire) {
let current = load_f64(&slot.current);
Some((i, current / 255.0))
} else {
None
}
})
}
pub fn fixture_info(&self, slot_index: usize) -> &(String, String) {
&self.slot_to_fixture[slot_index]
}
pub fn lookup(&self, universe: u16, channel: u16) -> Option<usize> {
self.channel_to_slot.get(&(universe, channel)).copied()
}
pub fn clear(&self) {
for slot in &self.slots {
store_f64(&slot.current, 0.0);
store_f64(&slot.target, 0.0);
store_f64(&slot.rate, 0.0);
slot.active.store(false, Ordering::Release);
}
self.generation.fetch_add(1, Ordering::Release);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_store() -> MidiDmxStore {
let mut store = MidiDmxStore::new();
store.register_slot(1, 10, "wash1", "dimmer");
store.register_slot(1, 11, "wash1", "red");
store.register_slot(1, 12, "wash1", "green");
store.register_slot(1, 13, "wash1", "blue");
store.register_universe(1);
store
}
#[test]
fn test_write_and_read_instant() {
let store = create_test_store();
store.write(1, 10, 200, false);
store.tick();
let values: Vec<(usize, f64)> = store.iter_active().collect();
assert_eq!(values.len(), 1);
assert_eq!(values[0].0, 0); assert!((values[0].1 - 200.0 / 255.0).abs() < 0.01);
}
#[test]
fn test_write_and_read_dimmed() {
let store = create_test_store();
store.set_dim_rate(1, 44.0);
store.write(1, 10, 220, true);
for _ in 0..22 {
store.tick();
}
let values: Vec<(usize, f64)> = store.iter_active().collect();
assert_eq!(values.len(), 1);
let normalized = values[0].1;
assert!(
normalized > 0.3 && normalized < 0.6,
"expected roughly halfway, got {}",
normalized
);
for _ in 0..22 {
store.tick();
}
let values: Vec<(usize, f64)> = store.iter_active().collect();
let normalized = values[0].1;
assert!(
(normalized - 220.0 / 255.0).abs() < 0.02,
"expected ~0.863, got {}",
normalized
);
}
#[test]
fn test_dim_rate_change() {
let store = create_test_store();
store.set_dim_rate(1, 44.0);
store.write(1, 10, 200, true);
for _ in 0..10 {
store.tick();
}
store.set_dim_rate(1, 88.0);
store.write(1, 11, 100, true);
for _ in 0..34 {
store.tick();
}
let values: Vec<(usize, f64)> = store.iter_active().collect();
assert_eq!(values.len(), 2);
}
#[test]
fn test_clear_resets_slots() {
let store = create_test_store();
store.write(1, 10, 200, false);
store.write(1, 11, 100, false);
store.tick();
assert!(store.has_active_slots());
store.clear();
assert!(!store.has_active_slots());
assert_eq!(store.iter_active().count(), 0);
}
#[test]
fn test_unmapped_channel_returns_none() {
let store = create_test_store();
assert!(store.lookup(1, 99).is_none());
assert!(store.lookup(2, 10).is_none());
assert!(store.lookup(1, 10).is_some());
}
#[test]
fn test_generation_increments_on_write() {
let store = create_test_store();
let gen0 = store.generation();
store.write(1, 10, 100, false);
let gen1 = store.generation();
assert!(gen1 > gen0);
store.write(1, 11, 50, false);
let gen2 = store.generation();
assert!(gen2 > gen1);
}
#[test]
fn test_generation_increments_on_tick_change() {
let store = create_test_store();
store.set_dim_rate(1, 10.0);
store.write(1, 10, 200, true);
let gen_before = store.generation();
let changed = store.tick();
let gen_after = store.generation();
assert!(changed);
assert!(gen_after > gen_before);
}
#[test]
fn test_generation_increments_on_clear() {
let store = create_test_store();
store.write(1, 10, 100, false);
let gen_before = store.generation();
store.clear();
let gen_after = store.generation();
assert!(gen_after > gen_before);
}
#[test]
fn test_fixture_info() {
let store = create_test_store();
let (fixture, channel) = store.fixture_info(0);
assert_eq!(fixture, "wash1");
assert_eq!(channel, "dimmer");
let (fixture, channel) = store.fixture_info(2);
assert_eq!(fixture, "wash1");
assert_eq!(channel, "green");
}
#[test]
fn test_tick_no_active_slots_returns_false() {
let store = create_test_store();
assert!(!store.tick());
}
#[test]
fn test_tick_converged_slots_returns_false() {
let store = create_test_store();
store.write(1, 10, 100, false);
store.tick();
assert!(!store.tick());
}
#[test]
fn test_write_to_unmapped_channel_is_noop() {
let store = create_test_store();
let gen_before = store.generation();
store.write(2, 99, 255, false);
assert_eq!(store.generation(), gen_before);
assert!(!store.has_active_slots());
}
#[test]
fn test_default_trait() {
let store = MidiDmxStore::default();
assert_eq!(store.iter_active().count(), 0);
assert_eq!(store.generation(), 0);
}
#[test]
fn test_multiple_universes() {
let mut store = MidiDmxStore::new();
store.register_slot(1, 1, "fixture_a", "dimmer");
store.register_slot(2, 1, "fixture_b", "dimmer");
store.register_universe(1);
store.register_universe(2);
store.write(1, 1, 100, false);
store.write(2, 1, 200, false);
store.tick();
let values: Vec<(usize, f64)> = store.iter_active().collect();
assert_eq!(values.len(), 2);
}
#[test]
fn test_instant_write_zero_dim_rate() {
let store = create_test_store();
store.set_dim_rate(1, 0.0);
store.write(1, 10, 128, true); store.tick();
let values: Vec<(usize, f64)> = store.iter_active().collect();
assert_eq!(values.len(), 1);
assert!((values[0].1 - 128.0 / 255.0).abs() < 0.01);
}
#[test]
fn test_dim_down() {
let store = create_test_store();
store.set_dim_rate(1, 44.0);
store.write(1, 10, 200, false);
store.tick();
store.write(1, 10, 50, true);
for _ in 0..22 {
store.tick();
}
let values: Vec<(usize, f64)> = store.iter_active().collect();
let normalized = values[0].1;
assert!(normalized > 50.0 / 255.0 - 0.05);
assert!(normalized < 200.0 / 255.0 + 0.05);
}
#[test]
fn test_lookup_returns_correct_slot_index() {
let store = create_test_store();
assert_eq!(store.lookup(1, 10), Some(0));
assert_eq!(store.lookup(1, 11), Some(1));
assert_eq!(store.lookup(1, 12), Some(2));
assert_eq!(store.lookup(1, 13), Some(3));
}
}