1use crate::{
14 command::{
15 bytes::{constants, ConstCommandBuilder},
16 encode::ViscaCommand,
17 },
18 error::Error,
19 timeout::CommandCategory,
20};
21
22#[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,
31 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#[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 pub fn new(mode: NdFilterMode) -> Self {
76 Self { mode }
77 }
78}
79
80#[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 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 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 let value = ((stops - 2.0) * 4.0) as u16;
140 Ok(Self { value })
141 }
142}
143
144#[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 Up,
153 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#[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 pub fn new(direction: NdFilterStep) -> Self {
199 Self { direction }
200 }
201}
202
203#[derive(Debug, Clone, Copy)]
212pub struct AutoNdCommand {
213 enabled: bool,
214}
215
216impl AutoNdCommand {
217 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); 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}