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}