Skip to main content

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}