Skip to main content

grafton_visca/command/
resolution.rs

1//! Resolution mode helpers and utilities.
2//!
3//! This module provides helper functions for interpreting resolution mode values
4//! returned by cameras in response to resolution inquiry commands.
5
6/// Common video resolution modes for Ptz cameras.
7///
8/// Note: These mappings are based on common PtzOptics camera patterns.
9/// Actual mappings may vary by camera model and firmware version.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
12#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
13#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
14#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS), ts(export))]
15pub enum ResolutionMode {
16    /// 1920x1080 @ 60fps
17    FullHD60,
18    /// 1920x1080 @ 30fps
19    FullHD30,
20    /// 1920x1080 @ 25fps
21    FullHD25,
22    /// 1280x720 @ 60fps
23    HD60,
24    /// 1280x720 @ 30fps
25    HD30,
26    /// 1280x720 @ 25fps
27    HD25,
28    /// Unknown or camera-specific mode
29    Unknown(u8),
30}
31
32impl ResolutionMode {
33    /// Convert a raw resolution mode byte to a ResolutionMode enum.
34    ///
35    /// # Arguments
36    /// * `mode` - The raw mode byte from the camera
37    ///
38    /// # Returns
39    /// The corresponding ResolutionMode variant
40    pub fn from_byte(mode: u8) -> Self {
41        match mode {
42            0x00 => ResolutionMode::FullHD60,
43            0x01 => ResolutionMode::FullHD30,
44            0x02 => ResolutionMode::FullHD25,
45            0x03 => ResolutionMode::HD60,
46            0x04 => ResolutionMode::HD30,
47            0x05 => ResolutionMode::HD25,
48            _ => ResolutionMode::Unknown(mode),
49        }
50    }
51
52    /// Get a human-readable description of the resolution mode.
53    pub fn description(&self) -> &'static str {
54        match self {
55            ResolutionMode::FullHD60 => "1080p60 (1920x1080 @ 60fps)",
56            ResolutionMode::FullHD30 => "1080p30 (1920x1080 @ 30fps)",
57            ResolutionMode::FullHD25 => "1080p25 (1920x1080 @ 25fps)",
58            ResolutionMode::HD60 => "720p60 (1280x720 @ 60fps)",
59            ResolutionMode::HD30 => "720p30 (1280x720 @ 30fps)",
60            ResolutionMode::HD25 => "720p25 (1280x720 @ 25fps)",
61            ResolutionMode::Unknown(_) => "Unknown resolution mode",
62        }
63    }
64
65    /// Get the width in pixels for this resolution mode.
66    pub fn width(&self) -> Option<u32> {
67        match self {
68            ResolutionMode::FullHD60 | ResolutionMode::FullHD30 | ResolutionMode::FullHD25 => {
69                Some(1920)
70            }
71            ResolutionMode::HD60 | ResolutionMode::HD30 | ResolutionMode::HD25 => Some(1280),
72            ResolutionMode::Unknown(_) => None,
73        }
74    }
75
76    /// Get the height in pixels for this resolution mode.
77    pub fn height(&self) -> Option<u32> {
78        match self {
79            ResolutionMode::FullHD60 | ResolutionMode::FullHD30 | ResolutionMode::FullHD25 => {
80                Some(1080)
81            }
82            ResolutionMode::HD60 | ResolutionMode::HD30 | ResolutionMode::HD25 => Some(720),
83            ResolutionMode::Unknown(_) => None,
84        }
85    }
86
87    /// Get the frame rate for this resolution mode.
88    pub fn frame_rate(&self) -> Option<f32> {
89        match self {
90            ResolutionMode::FullHD60 | ResolutionMode::HD60 => Some(60.0),
91            ResolutionMode::FullHD30 | ResolutionMode::HD30 => Some(30.0),
92            ResolutionMode::FullHD25 | ResolutionMode::HD25 => Some(25.0),
93            ResolutionMode::Unknown(_) => None,
94        }
95    }
96}
97
98/// Picture effect modes for Ptz cameras.
99///
100/// These effects modify the camera's video output for artistic or functional purposes.
101#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
103#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
104#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
105#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS), ts(export))]
106pub enum PictureEffectMode {
107    /// Normal operation (no effect).
108    Off,
109    /// Negative image effect.
110    ///
111    /// Not validated for built-in profiles; `PictureEffectCommand` rejects this
112    /// named mode unless a future profile adds source-backed value mapping.
113    Negative,
114    /// Black and white effect.
115    BlackAndWhite,
116    /// Sepia tone effect.
117    ///
118    /// Not validated for built-in profiles; `PictureEffectCommand` rejects this
119    /// named mode unless a future profile adds source-backed value mapping.
120    Sepia,
121    /// Sketch effect.
122    ///
123    /// Not validated for built-in profiles; `PictureEffectCommand` rejects this
124    /// named mode unless a future profile adds source-backed value mapping.
125    Sketch,
126    /// Emboss effect.
127    ///
128    /// Not validated for built-in profiles; `PictureEffectCommand` rejects this
129    /// named mode unless a future profile adds source-backed value mapping.
130    Emboss,
131    /// Mosaic effect.
132    ///
133    /// Not validated for built-in profiles; `PictureEffectCommand` rejects this
134    /// named mode unless a future profile adds source-backed value mapping.
135    Mosaic,
136    /// Unknown or camera-specific effect.
137    Unknown(u8),
138}
139
140impl PictureEffectMode {
141    /// Convert a raw picture effect byte to a PictureEffectMode enum.
142    ///
143    /// # Arguments
144    /// * `effect` - The raw effect byte from the camera
145    ///
146    /// # Returns
147    /// The corresponding PictureEffectMode variant
148    pub fn from_byte(effect: u8) -> Self {
149        match effect {
150            0x00 | 0x02 => PictureEffectMode::Off,
151            0x01 => PictureEffectMode::Negative,
152            0x03 => PictureEffectMode::Sepia,
153            0x04 => PictureEffectMode::BlackAndWhite,
154            0x05 => PictureEffectMode::Emboss,
155            0x06 => PictureEffectMode::Mosaic,
156            _ => PictureEffectMode::Unknown(effect),
157        }
158    }
159
160    /// Get a human-readable description of the picture effect.
161    pub fn description(&self) -> &'static str {
162        match self {
163            PictureEffectMode::Off => "Off (normal)",
164            PictureEffectMode::Negative => "Negative",
165            PictureEffectMode::BlackAndWhite => "Black & White",
166            PictureEffectMode::Sepia => "Sepia",
167            PictureEffectMode::Sketch => "Sketch",
168            PictureEffectMode::Emboss => "Emboss",
169            PictureEffectMode::Mosaic => "Mosaic",
170            PictureEffectMode::Unknown(_) => "Unknown picture effect",
171        }
172    }
173
174    /// Get the raw byte value for this picture effect mode.
175    pub fn as_byte(&self) -> u8 {
176        match self {
177            PictureEffectMode::Off => 0x00,
178            PictureEffectMode::Negative => 0x01,
179            PictureEffectMode::BlackAndWhite => 0x04,
180            PictureEffectMode::Sepia => 0x03,
181            PictureEffectMode::Sketch => 0x04,
182            PictureEffectMode::Emboss => 0x05,
183            PictureEffectMode::Mosaic => 0x06,
184            PictureEffectMode::Unknown(value) => *value,
185        }
186    }
187}
188
189/// ND filter positions for cameras with neutral density filters (Sony FR7).
190///
191/// ND filters reduce light entering the camera without affecting color balance.
192#[derive(Debug, Clone, Copy, PartialEq, Eq)]
193pub enum NdFilterPosition {
194    /// Clear (no filter applied).
195    Clear,
196    /// 1/4 ND (2 stops reduction).
197    OneQuarter,
198    /// 1/8 ND (3 stops reduction).
199    OneEighth,
200    /// 1/16 ND (4 stops reduction).
201    OneSixteenth,
202    /// 1/32 ND (5 stops reduction).
203    OneThirtySecond,
204    /// 1/64 ND (6 stops reduction).
205    OneSixtyFourth,
206    /// Unknown or camera-specific ND filter setting.
207    Unknown(u8),
208}
209
210impl NdFilterPosition {
211    /// Convert a raw ND filter position byte to an NdFilterPosition enum.
212    ///
213    /// # Arguments
214    /// * `position` - The raw position byte from the camera
215    ///
216    /// # Returns
217    /// The corresponding NdFilterPosition variant
218    pub fn from_byte(position: u8) -> Self {
219        match position {
220            0x00 => NdFilterPosition::Clear,
221            0x01 => NdFilterPosition::OneQuarter,
222            0x02 => NdFilterPosition::OneEighth,
223            0x03 => NdFilterPosition::OneSixteenth,
224            0x04 => NdFilterPosition::OneThirtySecond,
225            0x05 => NdFilterPosition::OneSixtyFourth,
226            _ => NdFilterPosition::Unknown(position),
227        }
228    }
229
230    /// Get a human-readable description of the ND filter position.
231    pub fn description(&self) -> &'static str {
232        match self {
233            NdFilterPosition::Clear => "Clear (no filter)",
234            NdFilterPosition::OneQuarter => "1/4 ND",
235            NdFilterPosition::OneEighth => "1/8 ND",
236            NdFilterPosition::OneSixteenth => "1/16 ND",
237            NdFilterPosition::OneThirtySecond => "1/32 ND",
238            NdFilterPosition::OneSixtyFourth => "1/64 ND",
239            NdFilterPosition::Unknown(_) => "Unknown ND filter position",
240        }
241    }
242
243    /// Get the raw byte value for this ND filter position.
244    pub fn as_byte(&self) -> u8 {
245        match self {
246            NdFilterPosition::Clear => 0x00,
247            NdFilterPosition::OneQuarter => 0x01,
248            NdFilterPosition::OneEighth => 0x02,
249            NdFilterPosition::OneSixteenth => 0x03,
250            NdFilterPosition::OneThirtySecond => 0x04,
251            NdFilterPosition::OneSixtyFourth => 0x05,
252            NdFilterPosition::Unknown(value) => *value,
253        }
254    }
255
256    /// Get the light reduction factor as a rational number (numerator, denominator).
257    /// Returns None for Clear or Unknown positions.
258    pub fn reduction_factor(&self) -> Option<(u32, u32)> {
259        match self {
260            NdFilterPosition::Clear => Some((1, 1)),
261            NdFilterPosition::OneQuarter => Some((1, 4)),
262            NdFilterPosition::OneEighth => Some((1, 8)),
263            NdFilterPosition::OneSixteenth => Some((1, 16)),
264            NdFilterPosition::OneThirtySecond => Some((1, 32)),
265            NdFilterPosition::OneSixtyFourth => Some((1, 64)),
266            NdFilterPosition::Unknown(_) => None,
267        }
268    }
269
270    /// Get the number of f-stops of light reduction.
271    /// Returns None for Unknown positions.
272    pub fn stops_reduction(&self) -> Option<f32> {
273        match self {
274            NdFilterPosition::Clear => Some(0.0),
275            NdFilterPosition::OneQuarter => Some(2.0),
276            NdFilterPosition::OneEighth => Some(3.0),
277            NdFilterPosition::OneSixteenth => Some(4.0),
278            NdFilterPosition::OneThirtySecond => Some(5.0),
279            NdFilterPosition::OneSixtyFourth => Some(6.0),
280            NdFilterPosition::Unknown(_) => None,
281        }
282    }
283}
284
285#[cfg(test)]
286mod tests {
287    use super::*;
288
289    #[test]
290    fn test_resolution_mode_conversion() {
291        assert_eq!(ResolutionMode::from_byte(0x00), ResolutionMode::FullHD60);
292        assert_eq!(ResolutionMode::from_byte(0x01), ResolutionMode::FullHD30);
293        assert_eq!(ResolutionMode::from_byte(0x04), ResolutionMode::HD30);
294        assert_eq!(
295            ResolutionMode::from_byte(0xFF),
296            ResolutionMode::Unknown(0xFF)
297        );
298    }
299
300    #[test]
301    fn test_resolution_properties() {
302        let mode = ResolutionMode::FullHD60;
303        assert_eq!(mode.width(), Some(1920));
304        assert_eq!(mode.height(), Some(1080));
305        assert_eq!(mode.frame_rate(), Some(60.0));
306        assert_eq!(mode.description(), "1080p60 (1920x1080 @ 60fps)");
307
308        let unknown = ResolutionMode::Unknown(0x99);
309        assert_eq!(unknown.width(), None);
310        assert_eq!(unknown.height(), None);
311        assert_eq!(unknown.frame_rate(), None);
312    }
313
314    #[test]
315    fn test_nd_filter_descriptions() {
316        assert_eq!(NdFilterPosition::Clear.description(), "Clear (no filter)");
317        assert_eq!(NdFilterPosition::OneQuarter.description(), "1/4 ND");
318        assert_eq!(NdFilterPosition::OneSixtyFourth.description(), "1/64 ND");
319        assert_eq!(
320            NdFilterPosition::Unknown(0xFF).description(),
321            "Unknown ND filter position"
322        );
323    }
324
325    #[test]
326    fn test_picture_effect_descriptions() {
327        assert_eq!(PictureEffectMode::Off.description(), "Off (normal)");
328        assert_eq!(PictureEffectMode::Negative.description(), "Negative");
329        assert_eq!(
330            PictureEffectMode::BlackAndWhite.description(),
331            "Black & White"
332        );
333        assert_eq!(PictureEffectMode::from_byte(0x02), PictureEffectMode::Off);
334        assert_eq!(
335            PictureEffectMode::from_byte(0x04),
336            PictureEffectMode::BlackAndWhite
337        );
338        assert_eq!(PictureEffectMode::BlackAndWhite.as_byte(), 0x04);
339        assert_eq!(
340            PictureEffectMode::Unknown(0xFF).description(),
341            "Unknown picture effect"
342        );
343    }
344}