common/fixture.rs
1//! # Fixture Management Module
2//!
3//! This module provides the core structures and logic for managing lighting fixtures,
4//! their DMX-Channels, colors, properties, and global registries.
5//!
6//! ## Key Components
7//! - [`Fixture`]: Represents an active lighting fixture instance assigned to a universe.
8//! - [`FixtureType`]: Template defining the channel layout and properties of a fixture type.
9//! - [`calculate_dmx_values`]: Generates the final DMX-Channel buffer across all universes.
10
11mod color;
12mod channel;
13mod fixture_type;
14
15pub use fixture_type::FixtureType;
16pub use crate::fixture::channel::{ChannelError, ChannelParameter, PropertyType};
17pub use color::ColorPropertyType;
18
19use std::sync::{LazyLock, RwLock};
20use std::collections::HashMap;
21use serde::{Deserialize, Serialize};
22use color::Color;
23use crate::fixture::FixtureError::InvalidFixtureType;
24use crate::fixture::channel::{Channel, SimplePropertyType};
25
26/// The fundamental numeric data type used to represent raw DMX-Channel values internally.
27///
28/// **Scaling note:** Must match the bit-width requirements of [`MAX_FINE_DEGREES`]
29pub type ChannelValue = u32;
30
31/// The maximum number of fine-tuning degrees (resolution layers) supported per channel.
32///
33/// **Core Scaling Constant:** Defines the maximum precision depth per property
34/// (e.g., Coarse + Fine + Ultra + Uber = 4 degrees). Changing this constant dictates
35/// that [`ChannelValue`], [`SignedChannelValue`], and [`FloatChannelValue`] must be adjusted
36/// in tandem to provide sufficient byte-width and mantissa precision.
37pub const MAX_FINE_DEGREES: usize = 4;
38
39/// A signed integer type matching the scale of [`ChannelValue`], used for calculations and offsets.
40type SignedChannelValue = i64;
41
42/// A floating-point type matching the scale of [`ChannelValue`], used for mathematical and color transformations.
43pub type FloatChannelValue = f64;
44
45/// The index type used to address individual DMX-Channels within a universe.
46pub type ChannelIndex = u16;
47
48/// The maximum number of DMX-Channels per universe (DMX512 standard).
49pub const MAX_CHANNEL: ChannelIndex = 512;
50
51/// The index type used to uniquely identify and address distinct DMX universes.
52pub type UniverseIndex = usize;
53
54/// A composite identifier representing a specific channel within a designated universe.
55///
56/// Combines a [`ChannelIndex`] and a [`UniverseIndex`] to pinpoint a unique DMX endpoint.
57pub type ChannelInUniverse = (ChannelIndex, UniverseIndex);
58
59/// Global registry storing all registered fixture types mapped by their unique name.
60static FIXTURE_TYPE_LIST: LazyLock<RwLock<HashMap<String, FixtureType>>> =
61 LazyLock::new(|| RwLock::new(HashMap::new()));
62
63
64/// A single fixture instance with its current property values and DMX-Channels.
65///
66/// Created from a [`FixtureType`] template. Each property maps to one or more
67/// DMX-Channels based on its configured [`ChannelParameter`] layout.
68#[derive(Clone)]
69pub struct Fixture {
70 fixture_type: String,
71 color: Option<Color>,
72 properties: HashMap<SimplePropertyType, Channel>,
73 start_channel: ChannelIndex,
74 universe: usize,
75 name: String,
76}
77
78
79impl Fixture {
80 /// Creates a new fixture instance.
81 ///
82 /// Allocates DMX-Channels based on the given [`FixtureType`] template,
83 /// offset by `start_channel`, and returns the instantiated [`Fixture`].
84 ///
85 /// # Arguments
86 ///
87 /// * `fixture_type_name` - Name of a previously registered [`FixtureType`]
88 /// * `start_channel` - DMX-Channel offset within the universe
89 /// * `universe` - DMX universe index (0-based)
90 /// * `name` - Unique name for this fixture instance
91 ///
92 /// # Errors
93 ///
94 /// * [`InvalidFixtureType`] – if `fixture_type_name` is not registered
95 pub fn new(
96 fixture_type_name: String,
97 start_channel: ChannelIndex,
98 universe: usize,
99 name: String,
100 ) -> Result<Fixture, FixtureError> {
101
102 if start_channel > MAX_CHANNEL {
103 return Err(FixtureError::ChannelError(ChannelError::ChannelOutOfRange));
104 }
105
106 let list = FIXTURE_TYPE_LIST.read().unwrap();
107
108 let fixture_type = list.get(fixture_type_name.as_str())
109 .ok_or(InvalidFixtureType(fixture_type_name.clone()))?;
110
111 let color = fixture_type
112 .color
113 .as_ref()
114 .map(|c| Color::new(c, start_channel, universe));
115
116 let properties = fixture_type
117 .properties
118 .iter()
119 .map(|(property_type, channel)| {
120 let default_value = Channel::get_default_value(property_type.clone());
121 let channel = Channel::new(channel.clone(), default_value, start_channel, universe);
122 Ok((property_type.clone(), channel))
123 })
124 .collect::<Result<HashMap<SimplePropertyType, Channel>, ChannelError>>()?;
125
126 Ok(Self {
127 color: color.clone(),
128 fixture_type: fixture_type.name.clone(),
129 properties: properties.clone(),
130 start_channel,
131 universe,
132 name: name.clone(),
133 })
134 }
135
136 /// Returns an iterator over all properties and their channels.
137 pub fn iter_over_properties(&self) -> impl Iterator<Item = (PropertyType, &Channel)> {
138 let simple_iter = self.properties.iter()
139 .map(|(simple_property, channel)| (PropertyType::Simple(simple_property.clone()), channel));
140
141 let color_iter = self.color
142 .as_ref()
143 .into_iter()
144 .flat_map(|color| color.get_channels_as_iter());
145
146 simple_iter.chain(color_iter)
147
148 }
149
150 /// Returns a mutable iterator over all properties and their channels.
151 fn iter_mut_over_properties(&mut self) -> impl Iterator<Item = (PropertyType, &mut Channel)> {
152 let simple_iter = self.properties.iter_mut()
153 .map(|(simple_property, channel)| (PropertyType::Simple(simple_property.clone()), channel));
154
155 let color_iter = self.color
156 .as_mut()
157 .into_iter()
158 .flat_map(|color| color.get_channels_as_iter_mut());
159
160 simple_iter.chain(color_iter)
161
162 }
163
164 /// Moves the fixture to a new starting channel and universe.
165 ///
166 /// # Arguments
167 ///
168 /// * `new_channel` - New starting DMX-Channel index
169 /// * `new_universe` - New target DMX universe index (0-based)
170 ///
171 /// # Errors
172 ///
173 /// * [`ChannelOutOfRange`](ChannelError::ChannelOutOfRange) – if the new channel range is out of bounds
174 pub fn move_to_channel(&mut self, new_channel: ChannelIndex, new_universe: usize) -> Result<(), FixtureError> {
175
176 if new_channel > MAX_CHANNEL {
177 return Err(FixtureError::ChannelError(ChannelError::ChannelOutOfRange));
178 }
179
180 let old_start = (self.start_channel, self.universe);
181 let new_start = (new_channel, new_universe);
182
183 self.iter_mut_over_properties().try_for_each(|(_, channel)| {
184 channel.move_channels(old_start, new_start);
185
186 Ok::<(), ChannelError>(())
187 }).map_err(FixtureError::from)?;
188
189 self.start_channel = new_channel;
190 self.universe = new_universe;
191
192 Ok(())
193 }
194
195 /// Sets the value of a property on the fixture.
196 ///
197 /// # Arguments
198 ///
199 /// * `property_type` - Property type to update (Simple or Color)
200 /// * `value` - 32-bit channel value to set
201 ///
202 /// # Errors
203 ///
204 /// * [`FixtureError::MissingProperty`] – if the fixture does not have this property
205 pub fn set(&mut self, property_type: PropertyType, value: ChannelValue) -> Result<(), FixtureError> {
206 match property_type {
207 PropertyType::Simple(property_type) => {
208 let property =
209 self
210 .properties
211 .get_mut(&property_type)
212 .ok_or(FixtureError::MissingProperty(PropertyType::Simple(
213 property_type,
214 )))?;
215
216 property.value = value;
217 }
218
219 PropertyType::Color(property_type) => {
220 if let Some(color) = &mut self.color {
221 color.set(property_type, value);
222 } else {
223 return Err(FixtureError::MissingProperty(PropertyType::Color(
224 property_type,
225 )));
226 }
227 }
228 }
229
230 Ok(())
231 }
232
233 fn get_channel_values(&self) -> Vec<(ChannelInUniverse, u8)> {
234 let mut output: Vec<(ChannelInUniverse, u8)> =
235 self.properties.iter()
236 .flat_map(|(_, channel)| channel.get_all_values())
237 .collect();
238
239 if let Some(color) = self.color.as_ref() {
240 output.append(&mut color.get_values())
241 }
242
243 output
244 }
245
246 /// Returns the name of the [`FixtureType`] this fixture was created from.
247 pub fn get_fixture_type(&self) -> String {
248 self.fixture_type.clone()
249 }
250
251 /// Returns the name of this fixture.
252 pub fn get_name(&self) -> &str {
253 &self.name
254 }
255}
256
257/// Errors that can occur when managing fixtures and fixture types.
258#[derive(Debug, Serialize, Deserialize, Clone)]
259pub enum FixtureError {
260 /// The given property name does not match any known [`SimplePropertyType`] or [`ColorPropertyType`].
261 InvalidPropertyType(String),
262 /// A fixture type mixes incompatible color models (e.g. RGB and HSV).
263 MultipleColorOutputTypes(String),
264 /// A fixture with this name is already registered.
265 FixtureNameAlreadyInUse(String),
266 /// A fixture type with this name is already registered.
267 FixtureTypeNameAlreadyInUse(String),
268 /// No fixture type with this name is registered.
269 InvalidFixtureType(String),
270 /// No fixture with this name is registered.
271 InvalidFixture(String),
272 /// The fixture does not have the requested property.
273 MissingProperty(PropertyType),
274 /// The DMX configuration and the fixture registry have drifted out of sync,
275 /// indicating an internal engine inconsistency.
276 DmxStateDesync,
277 /// A DMX-Channel operation failed.
278 ChannelError(ChannelError),
279}
280
281impl From<ChannelError> for FixtureError {
282 fn from(e: ChannelError) -> Self {
283 FixtureError::ChannelError(e)
284 }
285}
286
287/// Collects values from all registered fixtures via their channel and color properties.
288///
289/// Returns one array per universe, where each index corresponds to a DMX
290/// channel and the value is the 8-bit DMX level.
291///
292/// # Arguments
293///
294/// * `universe_count` - Total number of universes to calculate values for
295/// * `fixture_list` - Slice of active fixtures to process
296///
297/// # Panics
298///
299/// Panics if a fixture has a channel that exceeds [`MAX_CHANNEL`]. WHO THE FUCK GOT THE IDEA THAT DMX_UNIVERSES SHOULD
300/// HAVE 512 !!!!! 512 Channels? Why?!?!?! Just because of 1 Bit we have to use u16 instead of u8! WHY!?!?!?!
301pub fn calculate_dmx_values(universe_count: usize, fixture_list: &[Fixture]) -> Vec<[u8; MAX_CHANNEL as usize]> {
302
303 let mut output = vec![[0u8; MAX_CHANNEL as usize]; universe_count];
304
305 fixture_list.iter().for_each(|fixture| {
306 let fixture_type = fixture.get_fixture_type();
307 let fixture_name = fixture.get_name();
308
309 fixture
310 .get_channel_values()
311 .iter()
312 .for_each(|(channel_in_universe, value)| {
313 let channel_index = channel_in_universe.0;
314 let universe_index = channel_in_universe.1;
315
316 if universe_index < universe_count {
317 *output
318 .get_mut(universe_index)
319 .unwrap()
320 .get_mut(channel_index as usize)
321 .ok_or(ChannelError::ChannelOutOfRange)
322 .unwrap_or_else(|_| {
323 panic!(
324 "Fixture \"{}\" of type {} has a channel that is out of bounds",
325 fixture_name, fixture_type
326 )
327 }) = *value;
328 }
329 });
330 });
331
332 output
333}