Skip to main content

grafton_visca/command/
nd_filter.rs

1//! Neutral Density (ND) filter control commands for VISCA cameras.
2//!
3//! This module provides commands for controlling camera ND filter functionality,
4//! including mode selection, value setting, and auto ND control.
5//!
6//! # VISCA Compliance
7//! ND filter commands are vendor-specific extensions to the VISCA protocol.
8//!
9//! ## Vendor-Specific Commands
10//! - All ND filter commands - Sony FR7 specific
11//! - The FR7 supports variable ND filter (2 to 7 stops, continuously variable)
12
13use crate::{
14    command::{
15        bytes::{constants, ConstCommandBuilder},
16        encode::ViscaCommand,
17    },
18    error::Error,
19    timeout::CommandCategory,
20};
21
22/// ND filter mode for Sony FR7.
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
25#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
26#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
27#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS), ts(export))]
28pub enum NdFilterMode {
29    /// Preset mode - ND is set to discrete levels (user-defined presets).
30    Preset,
31    /// Variable mode - ND can be adjusted in fine steps.
32    Variable,
33}
34
35impl From<NdFilterMode> for u8 {
36    fn from(mode: NdFilterMode) -> u8 {
37        match mode {
38            NdFilterMode::Preset => 0x00,
39            NdFilterMode::Variable => 0x01,
40        }
41    }
42}
43
44/// Set the ND filter mode (preset or variable).
45///
46/// # Sony FR7 Specific
47/// Command: `8x 01 7E 04 52 0p FF`
48/// - p = 0 (Preset mode)
49/// - p = 1 (Variable mode)
50#[derive(Debug, Clone, Copy)]
51pub struct NdFilterModeCommand {
52    mode: NdFilterMode,
53}
54
55impl ViscaCommand for NdFilterModeCommand {
56    const MAX_SIZE: usize = 7;
57    const TIMEOUT_CATEGORY: CommandCategory = CommandCategory::Quick;
58
59    fn write_into(
60        &self,
61        camera_id: crate::camera_id::CameraId,
62        buffer: &mut [u8],
63    ) -> Result<usize, Error> {
64        ConstCommandBuilder::<7>::new()
65            .append(constants::nd_filter::CONTROL_PREFIX)
66            .push(u8::from(self.mode))
67            .with_camera_id(camera_id)
68            .terminate()
69            .build_into(buffer)
70    }
71}
72
73impl NdFilterModeCommand {
74    /// Create a new ND filter mode command.
75    pub fn new(mode: NdFilterMode) -> Self {
76        Self { mode }
77    }
78}
79
80/// Direct ND filter value command for variable mode.
81///
82/// # Sony FR7 Specific
83/// Command: `8x 01 7E 04 42 00 0p 0q FF`
84/// - Value 0x0000 = ND 1/4 (2 stops, minimum ND)
85/// - Value 0x0014 = ND 1/128 (7 stops, maximum density)
86/// - Linear scale for optical density (each increment ~0.5 stop)
87#[derive(Debug, Clone, Copy)]
88pub struct NdFilterValue {
89    value: u16,
90}
91
92impl ViscaCommand for NdFilterValue {
93    const MAX_SIZE: usize = 9;
94    const TIMEOUT_CATEGORY: CommandCategory = CommandCategory::Quick;
95
96    fn write_into(
97        &self,
98        camera_id: crate::camera_id::CameraId,
99        buffer: &mut [u8],
100    ) -> Result<usize, Error> {
101        ConstCommandBuilder::<9>::new()
102            .append(constants::nd_filter::DIRECT_PREFIX)
103            .push_nibble_pair(self.value)
104            .with_camera_id(camera_id)
105            .terminate()
106            .build_into(buffer)
107    }
108}
109
110impl NdFilterValue {
111    /// Create a new ND filter value command.
112    ///
113    /// # Arguments
114    /// * `value` - ND filter value (0x0000 to 0x0014)
115    pub fn new(value: u16) -> Result<Self, Error> {
116        if value > 0x0014 {
117            return Err(Error::ParameterOutOfRange {
118                parameter: "ND filter value",
119                value: value as i32,
120                min: 0,
121                max: 20,
122            });
123        }
124        Ok(Self { value })
125    }
126
127    /// Create from a stop value (2.0 to 7.0 stops).
128    pub fn from_stops(stops: f32) -> Result<Self, Error> {
129        if !(2.0..=7.0).contains(&stops) {
130            return Err(Error::ParameterOutOfRange {
131                parameter: "ND filter stops",
132                value: stops as i32,
133                min: 2,
134                max: 7,
135            });
136        }
137        // Convert stops to value: 2 stops = 0x0000, 7 stops = 0x0014
138        // Linear mapping: (stops - 2) * 4 = value
139        let value = ((stops - 2.0) * 4.0) as u16;
140        Ok(Self { value })
141    }
142}
143
144/// ND filter step adjustment direction.
145#[derive(Debug, Clone, Copy, PartialEq, Eq)]
146#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
147#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
148#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
149#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS), ts(export))]
150pub enum NdFilterStep {
151    /// Increase ND by one step (more density).
152    Up,
153    /// Decrease ND by one step (less density).
154    Down,
155}
156
157impl From<NdFilterStep> for u8 {
158    fn from(step: NdFilterStep) -> u8 {
159        match step {
160            NdFilterStep::Up => 0x02,
161            NdFilterStep::Down => 0x03,
162        }
163    }
164}
165
166/// ND filter step adjustment command.
167///
168/// # Sony FR7 Specific
169/// Command: `8x 01 7E 04 12 0p FF`
170/// - p = 02 (ND Filter Up - increase ND one step)
171/// - p = 03 (ND Filter Down - decrease ND one step)
172///   Works in Variable mode to bump ND in small increments.
173#[derive(Debug, Clone, Copy)]
174pub struct NdFilterStepCommand {
175    direction: NdFilterStep,
176}
177
178impl ViscaCommand for NdFilterStepCommand {
179    const MAX_SIZE: usize = 7;
180    const TIMEOUT_CATEGORY: CommandCategory = CommandCategory::Quick;
181
182    fn write_into(
183        &self,
184        camera_id: crate::camera_id::CameraId,
185        buffer: &mut [u8],
186    ) -> Result<usize, Error> {
187        ConstCommandBuilder::<7>::new()
188            .append(constants::nd_filter::MODE_PREFIX)
189            .push(u8::from(self.direction))
190            .with_camera_id(camera_id)
191            .terminate()
192            .build_into(buffer)
193    }
194}
195
196impl NdFilterStepCommand {
197    /// Create a new ND filter step command.
198    pub fn new(direction: NdFilterStep) -> Self {
199        Self { direction }
200    }
201}
202
203/// Auto ND filter control.
204///
205/// # Sony FR7 Specific
206/// Command: `8x 01 7E 04 53 0p FF`
207/// - p = 02 (Auto ND On)
208/// - p = 03 (Auto ND Off)
209///   When Auto ND is On, the camera automatically engages the ND filter
210///   to maintain exposure (like auto-iris, but using ND).
211#[derive(Debug, Clone, Copy)]
212pub struct AutoNdCommand {
213    enabled: bool,
214}
215
216impl AutoNdCommand {
217    /// Create a new auto ND command.
218    pub fn new(enabled: bool) -> Self {
219        Self { enabled }
220    }
221}
222
223impl ViscaCommand for AutoNdCommand {
224    const MAX_SIZE: usize = 7;
225    const TIMEOUT_CATEGORY: CommandCategory = CommandCategory::Quick;
226
227    fn write_into(
228        &self,
229        camera_id: crate::camera_id::CameraId,
230        buffer: &mut [u8],
231    ) -> Result<usize, Error> {
232        ConstCommandBuilder::<7>::new()
233            .append(constants::nd_filter::LEVEL_PREFIX)
234            .push(if self.enabled { 0x02 } else { 0x03 })
235            .with_camera_id(camera_id)
236            .terminate()
237            .build_into(buffer)
238    }
239}
240
241#[cfg(test)]
242#[allow(clippy::expect_used)]
243mod tests {
244    use super::*;
245
246    use crate::{command::bytes::VISCA_TERMINATOR, macros::test_utils::visca_test};
247
248    visca_test!(
249        NdFilterMode,
250        test_nd_filter_mode_preset,
251        NdFilterModeCommand::new(NdFilterMode::Preset),
252        &[0x81, 0x01, 0x7E, 0x04, 0x52, 0x00, VISCA_TERMINATOR]
253    );
254
255    visca_test!(
256        NdFilterMode,
257        test_nd_filter_mode_variable,
258        NdFilterModeCommand::new(NdFilterMode::Variable),
259        &[0x81, 0x01, 0x7E, 0x04, 0x52, 0x01, VISCA_TERMINATOR]
260    );
261
262    visca_test!(
263        NdFilterValue,
264        test_nd_filter_value_min,
265        NdFilterValue::new(0x0000).expect("Failed to create NdFilterValue with valid value"),
266        &[
267            0x81,
268            0x01,
269            0x7E,
270            0x04,
271            0x42,
272            0x00,
273            0x00,
274            0x00,
275            VISCA_TERMINATOR
276        ]
277    );
278
279    visca_test!(
280        NdFilterValue,
281        test_nd_filter_value_max,
282        NdFilterValue::new(0x0014).expect("Failed to create NdFilterValue with valid value"),
283        &[
284            0x81,
285            0x01,
286            0x7E,
287            0x04,
288            0x42,
289            0x00,
290            0x01,
291            0x04,
292            VISCA_TERMINATOR
293        ]
294    );
295
296    #[test]
297    fn test_nd_filter_value_out_of_range() {
298        assert!(NdFilterValue::new(0x0015).is_err());
299    }
300
301    #[test]
302    fn test_nd_filter_from_stops() {
303        let cmd = NdFilterValue::from_stops(2.0)
304            .expect("Failed to create NdFilterValue from valid stops");
305        assert_eq!(cmd.value, 0x0000);
306
307        let cmd = NdFilterValue::from_stops(7.0)
308            .expect("Failed to create NdFilterValue from valid stops");
309        assert_eq!(cmd.value, 0x0014);
310
311        let cmd = NdFilterValue::from_stops(4.5)
312            .expect("Failed to create NdFilterValue from valid stops");
313        assert_eq!(cmd.value, 0x000A); // (4.5 - 2) * 4 = 10
314
315        assert!(NdFilterValue::from_stops(1.5).is_err());
316        assert!(NdFilterValue::from_stops(8.0).is_err());
317    }
318
319    visca_test!(
320        NdFilterStep,
321        test_nd_filter_step_up,
322        NdFilterStepCommand::new(NdFilterStep::Up),
323        &[0x81, 0x01, 0x7E, 0x04, 0x12, 0x02, VISCA_TERMINATOR]
324    );
325
326    visca_test!(
327        NdFilterStep,
328        test_nd_filter_step_down,
329        NdFilterStepCommand::new(NdFilterStep::Down),
330        &[0x81, 0x01, 0x7E, 0x04, 0x12, 0x03, VISCA_TERMINATOR]
331    );
332
333    visca_test!(
334        AutoNdCommand,
335        test_auto_nd_on,
336        AutoNdCommand::new(true),
337        &[0x81, 0x01, 0x7E, 0x04, 0x53, 0x02, VISCA_TERMINATOR]
338    );
339
340    visca_test!(
341        AutoNdCommand,
342        test_auto_nd_off,
343        AutoNdCommand::new(false),
344        &[0x81, 0x01, 0x7E, 0x04, 0x53, 0x03, VISCA_TERMINATOR]
345    );
346}