Skip to main content

common/fixture/
channel.rs

1use std::fmt;
2use std::fmt::{Display, Formatter};
3use std::hash::Hash;
4use arrayvec::ArrayVec;
5use serde::{Deserialize, Serialize};
6use crate::fixture::{
7    ChannelInUniverse, ChannelIndex, ChannelValue, FixtureError, UniverseIndex, MAX_CHANNEL, MAX_FINE_DEGREES
8};
9use crate::fixture::color::ColorPropertyType;
10
11/// A single DMX-Channel with an optional fine channel for 16-bit control.
12#[derive(Clone)]
13pub struct Channel {
14    pub(crate) value: ChannelValue,
15    channel: ChannelParameter,
16}
17
18impl Channel {
19    /// Creates a new [`Channel`] and shifts its channel indices to the correct absolute position.
20    ///
21    /// The provided `channel_numbers` are assumed to be zero-based (relative to the fixture).
22    /// This method automatically shifts them using `device_channel` as the new starting address.
23    ///
24    /// # Arguments
25    ///
26    /// * `channel_numbers` - A [`ChannelParameter`] containing the relative coarse and fine channels.
27    /// * `default_value`   - The initial 16-bit value for this channel.
28    /// * `device_channel`  - The absolute DMX start address of the device within its universe.
29    /// * `device_universe` - The absoulte DMX start universe of the device
30    pub(crate) fn new(
31        channel_numbers: ChannelParameter,
32        default_value: ChannelValue,
33        device_channel: ChannelIndex,
34        device_universe: UniverseIndex,
35    ) -> Self {
36
37        let mut channel = channel_numbers.clone();
38        channel.move_indices((0,0), (device_channel, device_universe));
39
40        Channel {
41            value: default_value,
42            channel,
43        }
44    }
45
46    //TODO Add the option to have some fixtures go over Universe-Borders
47    /// Safely calculates a new absolute DMX address for a single channel when a fixture is moved.
48    ///
49    /// It determines the relative distance of the channel from the `old_start` address
50    /// and reapplies this relative position to the `new_start` address.
51    ///
52    /// # Arguments
53    ///
54    /// * `channel`   - The current absolute channel and universe index to be moved.
55    /// * `old_start` - The previous DMX start address and universe) of the fixture.
56    /// * `new_start` - The new DMX start address and universe) of the fixture.
57    fn move_single_channel(
58        channel: ChannelInUniverse, old_start: ChannelInUniverse, new_start: ChannelInUniverse
59    ) -> ChannelInUniverse {
60        let max_c = MAX_CHANNEL as usize;
61
62        let abs_channel = channel.1 * max_c + channel.0 as usize;
63        let abs_old_start = old_start.1 * max_c + old_start.0 as usize;
64        let abs_new_start = new_start.1 * max_c + new_start.0 as usize;
65
66        let relative_pos = abs_channel.checked_sub(abs_old_start)
67            .expect("CRITICAL: A channel was before the fixture's start channel!");
68        let new_abs_channel = abs_new_start + relative_pos;
69
70        let new_universe = new_abs_channel / max_c;
71        let new_channel = (new_abs_channel % max_c) as ChannelIndex;
72
73        (new_channel, new_universe)
74    }
75
76    /// Shifts all associated channel indices (coarse and fine) to a new DMX start address.
77    ///
78    /// # Arguments
79    ///
80    /// * `old_start` - The previous DMX start address and universe of the fixture.
81    /// * `new_start` - The new DMX start address and universe to shift the channels to.
82    pub(super) fn move_channels(&mut self, old_start: ChannelInUniverse, new_start: ChannelInUniverse) {
83        self.channel.move_indices(old_start, new_start);
84    }
85
86
87    /// Returns the coarse DMX output value as `Vec<(ChannelInUniverse, 8-bit value)>` . If fine, ultra, uber, ... channels
88    /// exist, then they are also part of the Return-Value
89    pub(super) fn get_all_values(&self) -> Vec<(ChannelInUniverse, u8)> {
90        let bytes = self.value.to_be_bytes();
91
92        self.channel.get_channel_indices()
93            .iter()
94            .zip(bytes)
95            .map(|(&channel, byte)| (channel, byte))
96            .collect()
97    }
98
99    /// Returns a copy of the internal array containing all configured channel and universe indices.
100    pub fn get_channel_indices(&self) -> ArrayVec<ChannelInUniverse, MAX_FINE_DEGREES> {
101        self.channel.get_channel_indices()
102    }
103
104    /// Determines the default startup value for a given `SimplePropertyType`.
105    ///
106    /// To prevent fixtures from moving wildly on startup, spatial attributes like `Pan`
107    /// and `Tilt` are initialized to their center positions (`ChannelValue::MAX / 2`).
108    /// All other simple properties default to `0`.
109    ///
110    /// # Arguments
111    ///
112    /// * `property_type` - The [`SimplePropertyType`] for which to determine the default value.
113    pub(super) fn get_default_value(property_type: SimplePropertyType) -> ChannelValue {
114        match property_type {
115            SimplePropertyType::Pan => ChannelValue::MAX / 2,
116            SimplePropertyType::Tilt => ChannelValue::MAX / 2,
117            _ => 0,
118        }
119    }
120}
121
122
123/// Represents a DMX-Channel parameter, managing the base channel and its fine-degree channels.
124/// It uses an `ArrayVec` to store the coarse channel alongside optional fine channels up to [`MAX_FINE_DEGREES`].
125#[derive(Clone, Debug, Serialize, Deserialize)]
126pub struct ChannelParameter {
127    channel: ArrayVec<ChannelInUniverse, MAX_FINE_DEGREES>
128}
129
130impl ChannelParameter {
131    /// Initializes a new `ChannelParameter` with a single, coarse channel index.
132    ///
133    /// # Arguments
134    ///
135    /// * `channel_index` - The base (coarse) channel index to initialize the parameter with.
136    /// * `universe_index` - The base (coarse) universe index to initialize the parameter with.
137    pub fn new(channel_index: ChannelIndex, universe_index: UniverseIndex) -> Self {
138        let mut channel = ArrayVec::new();
139        channel.push((channel_index, universe_index));
140        Self {
141            channel,
142        }
143    }
144
145    /// Adds a fine-degree channel to the parameter.
146    ///
147    /// The fine channels must be added in sequential order. The length of the internal
148    /// channel array dictates which fine degree is expected next.
149    ///
150    /// # Arguments
151    ///
152    /// * `fine_degree` - The degree level of the fine channel (e.g., 1 for fine, 2 for ultra-fine).
153    /// * `fine_index`  - The specific DMX-Channel and -Universe index for this fine degree.
154    ///
155    /// # Errors
156    ///
157    /// * [`FineDegreeOutOfRange`]((ChannelError::FineDegreeOutOfRange) - If the requested degree exceeds `MAX_FINE_DEGREES`.
158    /// * [`FineDegreeExists`](ChannelError::FineDegreeExists) - If a channel for this fine degree has already been added.
159    /// * [`FineDegreeTooHigh`](ChannelError::FineDegreeTooHigh) - If you attempt to add a higher fine degree before adding the intermediate ones.
160    pub fn add_fine(
161        &mut self, fine_degree: usize, fine_channel_index: ChannelIndex, fine_universe_index: UniverseIndex
162    ) -> Result<(), ChannelError> {
163        if fine_degree > MAX_FINE_DEGREES {
164            return Err(ChannelError::FineDegreeOutOfRange(fine_degree));
165        }
166
167        let required_len = fine_degree;
168
169        if self.channel.len() == required_len {
170            self.channel.push((fine_channel_index, fine_universe_index));
171            Ok(())
172        } else if self.channel.len() > required_len {
173            Err(ChannelError::FineDegreeExists(fine_degree))
174        } else {
175            Err(ChannelError::FineDegreeTooHigh(fine_degree))
176        }
177    }
178
179    /// Shifts all managed channel indices to a new starting address.
180    ///
181    /// The method calculates the relative position of each channel based on the `old_start`
182    /// and updates them relative to the `new_start`.
183    ///
184    /// # Arguments
185    ///
186    /// * `old_start` - The current DMX start address and universe used as the baseline for the shift.
187    /// * `new_start` - The target DMX start address and universe to move the indices to.
188    fn move_indices(&mut self, old_start: ChannelInUniverse, new_start: ChannelInUniverse) {
189        let channels = &mut self.channel;
190
191        for channel_index in channels.iter_mut() {
192            *channel_index = Channel::move_single_channel(*channel_index, old_start, new_start);
193        }
194    }
195
196    /// Returns a cloned `ArrayVec` containing all absolute channel and universe indices (coarse and fine)
197    /// currently held by this parameter.
198    pub(super) fn get_channel_indices(&self) -> ArrayVec<ChannelInUniverse, MAX_FINE_DEGREES> {
199        self.channel.clone()
200    }
201}
202
203
204/// A single configurable property of a lighting fixture.
205///
206/// Each variant corresponds to one DMX-controllable attribute.
207/// For color-related properties see [`ColorPropertyType`].
208///
209/// # Variants
210///
211/// * **Dimmer** – Fixture brightness.
212/// * **Strobe** – Strobe rate or shutter pulse speed.
213/// * **Shutter** – Mechanical shutter (open/close).
214/// * **Zoom** – Beam width.
215/// * **Focus** – Beam sharpness.
216/// * **Frost** – Diffusion/frost effect intensity.
217/// * **Prism** – Enables or selects a prism.
218/// * **PrismRotation** – Continuous prism rotation speed/direction.
219/// * **PrismIndexation** – Discrete prism index position.
220/// * **GoboRotation** – Absolute gobo rotation angle.
221/// * **GoboRotationSpeed** – Continuous gobo rotation speed.
222/// * **GoboWheelRotation** – Gobo wheel slot selection/rotation.
223/// * **GoboWheelRotationSpeed** – Gobo wheel continuous rotation speed.
224/// * **Pan** – Horizontal head movement.
225/// * **Tilt** – Vertical head movement.
226/// * **FogIntensity** – Fog output amount.
227/// * **FogFanSpeed** – Fan speed for fog dispersion.
228/// * **UV** – UV-LED intensity.
229/// * **Speed** – Global effect or macro speed.
230/// * **Other(String)** – Any manufacturer-specific or unsupported property.
231#[derive(Debug, Hash, Eq, PartialEq, Clone, Serialize, Deserialize)]
232pub enum SimplePropertyType {
233    Dimmer,
234    Strobe,
235    Zoom,
236    Focus,
237    Frost,
238    Prism,
239    PrismRotation,
240    PrismIndexation,
241    GoboRotation,
242    GoboRotationSpeed,
243    GoboWheelRotation,
244    GoboWheelRotationSpeed,
245    Pan,
246    Tilt,
247    FogIntensity,
248    FogFanSpeed,
249    Shutter,
250    UV,
251    Speed,
252    Other(String),
253}
254
255/// A fixture property, either a simple single-channel attribute or a color.
256///
257/// * **Simple([`SimplePropertyType`])** – Any non-color property such as dimmer, pan, gobo, etc.
258/// * **Color([`ColorPropertyType`])** – A color channel (RGB, CMY, or HSV).
259#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
260pub enum PropertyType {
261    Simple(SimplePropertyType),
262    Color(ColorPropertyType),
263}
264
265impl PropertyType {
266
267    /// Parses a raw string slice into a strongly typed `PropertyType`.
268    ///
269    /// It sequentially attempts to match the input string against known `ColorPropertyType`
270    /// variants first, and then falls back to `SimplePropertyType` variants.
271    ///
272    /// # Arguments
273    ///
274    /// * `property_type` - A string slice representing the name of the property to parse.
275    ///
276    /// # Errors
277    ///
278    /// Returns a [`InvalidPropertyType-Error`](FixtureError::InvalidPropertyType) if the string matches neither
279    /// a color nor a simple property.
280    pub fn from_str(property_type: &str) -> Result<PropertyType, FixtureError> {
281        if let Ok(property_type) = ColorPropertyType::from_string(property_type) {
282            Ok(PropertyType::Color(property_type))
283        } else if let Ok(property_type) = SimplePropertyType::from_string(property_type) {
284            Ok(PropertyType::Simple(property_type))
285        } else {
286            Err(FixtureError::InvalidPropertyType(property_type.to_string()))
287        }
288    }
289}
290
291impl Display for PropertyType {
292    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
293        match self {
294            PropertyType::Simple(property) => write!(f, "{}", property),
295            PropertyType::Color(property) => write!(f, "{}", property),
296        }
297    }
298}
299
300impl SimplePropertyType {
301    /// Attempts to parse a string identifier into a `SimplePropertyType`.
302    ///
303    /// Custom or manufacturer-specific properties can be parsed if they are
304    /// prefixed with `other_` (e.g., `other_gobo_shake`).
305    ///
306    /// # Arguments
307    ///
308    /// * `s` - The raw string identifier to parse into a simple property.
309    ///
310    /// # Errors
311    ///
312    /// Returns a [`InvalidPropertyType-Error`](FixtureError::InvalidPropertyType) if the string does not match
313    /// any known simple property and lacks the `other_` prefix.
314    fn from_string(s: &str) -> Result<SimplePropertyType, FixtureError> {
315        match s {
316            "dimmer" => Ok(SimplePropertyType::Dimmer),
317            "strobe" => Ok(SimplePropertyType::Strobe),
318            "zoom" => Ok(SimplePropertyType::Zoom),
319            "focus" => Ok(SimplePropertyType::Focus),
320            "frost" => Ok(SimplePropertyType::Frost),
321            "prism" => Ok(SimplePropertyType::Prism),
322            "prism-rotation" => Ok(SimplePropertyType::PrismRotation),
323            "prism-index" => Ok(SimplePropertyType::PrismIndexation),
324            "gobo" => Ok(SimplePropertyType::GoboRotation),
325            "gobo-rotation" => Ok(SimplePropertyType::GoboRotationSpeed),
326            "gobo-wheel-rotation" => Ok(SimplePropertyType::GoboWheelRotation),
327            "gobo-wheel-speed" => Ok(SimplePropertyType::GoboWheelRotationSpeed),
328            "pan" => Ok(SimplePropertyType::Pan),
329            "tilt" => Ok(SimplePropertyType::Tilt),
330            "fog-intensity" => Ok(SimplePropertyType::FogIntensity),
331            "fog-fan-speed" => Ok(SimplePropertyType::FogFanSpeed),
332            "shutter" => Ok(SimplePropertyType::Shutter),
333            "uv" => Ok(SimplePropertyType::UV),
334            "speed" => Ok(SimplePropertyType::Speed),
335            _ => {
336                if let Some(suffix) = s.strip_prefix("other_") {
337                    Ok(SimplePropertyType::Other(suffix.to_string()))
338                } else {
339                    Err(FixtureError::InvalidPropertyType(s.to_string()))
340                }
341            }
342        }
343    }
344}
345
346impl Display for SimplePropertyType {
347    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
348        match self {
349            SimplePropertyType::Dimmer => write!(f, "dimmer"),
350            SimplePropertyType::Strobe => write!(f, "strobe"),
351            SimplePropertyType::Zoom => write!(f, "zoom"),
352            SimplePropertyType::Focus => write!(f, "focus"),
353            SimplePropertyType::Frost => write!(f, "frost"),
354            SimplePropertyType::Prism => write!(f, "prism"),
355            SimplePropertyType::PrismRotation => write!(f, "prism-rotation"),
356            SimplePropertyType::PrismIndexation => write!(f, "prism-index"),
357            SimplePropertyType::GoboRotation => write!(f, "gobo"),
358            SimplePropertyType::GoboRotationSpeed => write!(f, "gobo-rotation"),
359            SimplePropertyType::GoboWheelRotation => write!(f, "gobo-wheel-rotation"),
360            SimplePropertyType::GoboWheelRotationSpeed => write!(f, "gobo-wheel-speed"),
361            SimplePropertyType::Pan => write!(f, "pan"),
362            SimplePropertyType::Tilt => write!(f, "tilt"),
363            SimplePropertyType::FogIntensity => write!(f, "fog-intensity"),
364            SimplePropertyType::FogFanSpeed => write!(f, "fog-fan-speed"),
365            SimplePropertyType::Shutter => write!(f, "shutter"),
366            SimplePropertyType::UV => write!(f, "uv"),
367            SimplePropertyType::Speed => write!(f, "speed"),
368            SimplePropertyType::Other(s) => write!(f, "{}", s),
369
370        }
371    }
372}
373
374/// Errors that can occur when reserving or accessing DMX-Channels.
375#[derive(Debug, Clone, Serialize, Deserialize)]
376pub enum ChannelError {
377    /// The channel number exceeds [`MAX_CHANNEL`].
378    ChannelOutOfRange,
379    /// The universe index exceeds the configured universe count.
380    UniverseOutOfRange,
381    /// The channel is already reserved by the named fixture.
382    ChannelAlreadyInUse(String),
383    /// The channel has the same fine-degree multiple times
384    FineDegreeExists(usize),
385    /// The channel has too high fine-degrees, while lower fine-degrees don't exist
386    FineDegreeTooHigh(usize),
387    /// The channel has to many fine-channels
388    FineDegreeOutOfRange(usize),
389}