Skip to main content

grafton_visca/command/
focus.rs

1//! Focus control commands for VISCA cameras.
2//!
3//! This module provides commands for controlling camera focus functionality,
4//! including auto/manual modes, directional focus, and direct position control.
5//!
6//! # VISCA Compliance
7//! Most commands in this module are part of the baseline VISCA specification.
8//!
9//! ## Vendor-Specific Commands
10//! - `FocusLock` - PtzOptics specific
11//! - `PushAF` - Sony FR7 specific
12
13use grafton_visca_macros::ViscaEnum;
14
15use crate::{
16    command::encode::ViscaCommand,
17    error::Error,
18    timeout::CommandCategory,
19    types::{FocusPosition, SpeedLevel},
20    visca_command,
21};
22
23/// Focus mode setting.
24#[derive(Debug, Clone, Copy, PartialEq, Eq, ViscaEnum)]
25#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
26#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
27#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
28#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS), ts(export))]
29pub enum FocusMode {
30    /// Automatic focus mode.
31    Auto = 0x02,
32    /// Manual focus mode.
33    Manual = 0x03,
34}
35
36/// Focus range setting.
37#[derive(Debug, Clone, Copy, PartialEq, Eq, ViscaEnum)]
38#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
39#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
40#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
41#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS), ts(export))]
42pub enum FocusRange {
43    /// Normal focus range.
44    Normal = 0x00,
45    /// 10x focus range.
46    Range10x = 0x01,
47    /// 4.3x focus range.
48    Range4_3x = 0x02,
49    /// 2.1x focus range.
50    Range2_1x = 0x03,
51    /// 1x focus range.
52    Range1x = 0x04,
53    /// 0.35x focus range.
54    Range0_35x = 0x05,
55}
56
57crate::visca_range_type! {
58    /// Variable focus speed.
59    ///
60    /// Valid range: 0 to 7 where 0 is the slowest and 7 is the fastest.
61    FocusSpeed: u8 {
62        min: 0,
63        max: 7
64    }
65}
66
67impl From<SpeedLevel> for FocusSpeed {
68    fn from(level: SpeedLevel) -> Self {
69        Self(level.to_focus_speed())
70    }
71}
72
73/// Focus control commands.
74///
75/// Provides various ways to control camera focus.
76#[derive(Debug, Copy, Clone)]
77pub enum Focus {
78    /// Stop any focus movement.
79    Stop,
80    /// Move focus far at standard speed.
81    Far,
82    /// Move focus near at standard speed.
83    Near,
84    /// Move focus far at variable speed.
85    FarWithSpeed(FocusSpeed),
86    /// Move focus near at variable speed.
87    NearWithSpeed(FocusSpeed),
88    /// Set focus to specific position.
89    Position(FocusPosition),
90    /// Enable auto focus mode.
91    Auto,
92    /// Enable manual focus mode.
93    Manual,
94    /// Trigger one-push auto focus (focus once then return to manual).
95    OnePushTrigger,
96    /// Set focus to infinity.
97    Infinity,
98    /// Toggle between auto and manual focus modes.
99    ///
100    /// **Vendor-Specific**: PTZOptics cameras.
101    Toggle,
102    /// Snap focus (one-push AF while in manual mode).
103    ///
104    /// Triggers a single autofocus operation, then returns to manual focus mode.
105    /// **Vendor-Specific**: Some vendor/firmware command references document
106    /// this opcode, but built-in profiles only expose it when their profile
107    /// metadata reports one-push focus support.
108    Snap,
109}
110
111impl ViscaCommand for Focus {
112    const MAX_SIZE: usize = 9;
113    const TIMEOUT_CATEGORY: CommandCategory = CommandCategory::Movement;
114
115    fn write_into(
116        &self,
117        camera_id: crate::camera_id::CameraId,
118        buffer: &mut [u8],
119    ) -> Result<usize, Error> {
120        use crate::command::bytes::{constants, ConstCommandBuilder};
121
122        match self {
123            Self::Stop | Self::Far | Self::Near => {
124                // Demonstrate type-state pattern usage for Stop command
125                if matches!(self, Self::Stop) {
126                    // Use the new type-state API
127                    let builder = ConstCommandBuilder::<6>::new()
128                        .append(constants::focus::MOVEMENT_PREFIX)
129                        .push(0x00)
130                        .with_camera_id(camera_id)
131                        .terminate();
132
133                    // Now we can access bytes only after termination
134                    builder.build_into(buffer)
135                } else {
136                    // Use legacy API for other commands
137                    let mut builder = ConstCommandBuilder::<6>::new();
138                    builder.append_mut(constants::focus::MOVEMENT_PREFIX);
139                    builder.push_mut(match self {
140                        Self::Far => 0x02,
141                        Self::Near => 0x03,
142                        _ => unreachable!(),
143                    });
144                    builder.with_camera_id_mut(camera_id);
145                    builder.terminate().build_into(buffer)
146                }
147            }
148            Self::FarWithSpeed(_) | Self::NearWithSpeed(_) => {
149                let mut builder = ConstCommandBuilder::<6>::new();
150                builder.append_mut(constants::focus::MOVEMENT_PREFIX);
151                builder.push_mut(match self {
152                    Self::FarWithSpeed(s) => 0x20 | s.value(),
153                    Self::NearWithSpeed(s) => 0x30 | s.value(),
154                    _ => unreachable!(),
155                });
156                builder.with_camera_id_mut(camera_id);
157                builder.terminate().build_into(buffer)
158            }
159            Self::Position(position) => {
160                let builder = ConstCommandBuilder::<9>::new()
161                    .append(constants::focus::POSITION_PREFIX)
162                    .push_visca_u16(position.value())
163                    .with_camera_id(camera_id)
164                    .terminate();
165                builder.build_into(buffer)
166            }
167            Self::Auto | Self::Manual | Self::Toggle | Self::Snap => {
168                let mut builder = ConstCommandBuilder::<6>::new();
169                builder.append_mut(constants::focus::MODE_PREFIX);
170                builder.push_mut(match self {
171                    Self::Auto => 0x02,
172                    Self::Manual => 0x03,
173                    Self::Snap => 0x04,
174                    Self::Toggle => 0x10,
175                    _ => unreachable!(),
176                });
177                builder.with_camera_id_mut(camera_id);
178                builder.terminate().build_into(buffer)
179            }
180            Self::OnePushTrigger | Self::Infinity => {
181                let mut builder = ConstCommandBuilder::<6>::new();
182                builder.append_mut(constants::focus::ONE_PUSH_PREFIX);
183                builder.push_mut(match self {
184                    Self::OnePushTrigger => 0x01,
185                    Self::Infinity => 0x02,
186                    _ => unreachable!(),
187                });
188                builder.with_camera_id_mut(camera_id);
189                builder.terminate().build_into(buffer)
190            }
191        }
192    }
193}
194
195/// Focus Zone selection (baseline VISCA).
196///
197/// Determines which area of the image the camera uses for auto focus.
198#[derive(Debug, Copy, Clone, PartialEq, Eq, ViscaEnum)]
199#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
200#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
201#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
202#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS), ts(export))]
203pub enum FocusZone {
204    /// Focus on the top area of the image.
205    Top = 0x00,
206    /// Focus on the center area of the image (default).
207    Center = 0x01,
208    /// Focus on the bottom area of the image.
209    Bottom = 0x02,
210}
211
212visca_command! {
213        /// Command to set the focus zone.
214    pub struct FocusZoneCommand {
215        zone: FocusZone,
216    };
217    prefix = [0x01, 0x04, 0xAA];
218    param = match *zone {
219        FocusZone::Top => 0x00u8,
220        FocusZone::Center => 0x01u8,
221        FocusZone::Bottom => 0x02u8,
222    };
223    max_param_size = 1;
224    category = CommandCategory::Quick;
225}
226
227impl FocusZoneCommand {
228    /// Create a new focus zone command.
229    pub fn new(zone: FocusZone) -> Self {
230        Self { zone }
231    }
232}
233
234/// Auto Focus Sensitivity levels.
235///
236/// Controls how responsive the auto focus system is to changes in the scene.
237#[derive(Debug, Copy, Clone, PartialEq, Eq, ViscaEnum)]
238#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
239#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
240#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
241#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS), ts(export))]
242pub enum AutoFocusSensitivity {
243    /// Low sensitivity - slower focus response, more stable in changing scenes.
244    Low = 0x00,
245    /// Normal sensitivity - balanced focus response (default).
246    Normal = 0x01,
247    /// High sensitivity - quick focus response to scene changes.
248    High = 0x02,
249}
250
251visca_command! {
252        /// Command to set auto focus sensitivity.
253    pub struct AutoFocusSensitivityCommand {
254        sensitivity: AutoFocusSensitivity,
255    };
256    prefix = [0x01, 0x04, 0x58];
257    param = match *sensitivity {
258        AutoFocusSensitivity::High => 0x02u8,
259        AutoFocusSensitivity::Normal => 0x01u8,
260        AutoFocusSensitivity::Low => 0x00u8,
261    };
262    max_param_size = 1;
263    category = CommandCategory::Quick;
264}
265
266impl AutoFocusSensitivityCommand {
267    /// Create a new auto focus sensitivity command.
268    pub fn new(sensitivity: AutoFocusSensitivity) -> Self {
269        Self { sensitivity }
270    }
271}
272
273visca_command! {
274        /// Command to set the focus near limit.
275    ///
276    /// Sets the minimum focus distance to prevent the camera from
277    /// focusing on objects too close to the lens.
278    pub struct FocusNearLimitCommand {
279        position: FocusPosition,
280    };
281    prefix = [0x01, 0x04, 0x28];
282    param = {
283        let value = position.value();
284        [
285            ((value >> 12) & 0x0F) as u8,
286            ((value >> 8) & 0x0F) as u8,
287            ((value >> 4) & 0x0F) as u8,
288            (value & 0x0F) as u8,
289        ]
290    };
291    max_param_size = 4;
292    category = CommandCategory::Quick;
293}
294
295impl FocusNearLimitCommand {
296    /// Create a new focus near limit command.
297    pub fn new(position: FocusPosition) -> Self {
298        Self { position }
299    }
300}
301
302/// Focus Lock command.
303///
304/// Controls whether the camera locks focus at the current position.
305///
306/// **Vendor-Specific**: This command is specific to PtzOptics cameras.
307#[derive(Debug, Copy, Clone)]
308#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
309#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
310#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
311#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS), ts(export))]
312pub enum FocusLock {
313    /// Enable focus lock
314    On,
315    /// Disable focus lock
316    Off,
317}
318
319impl ViscaCommand for FocusLock {
320    const MAX_SIZE: usize = 6;
321    const TIMEOUT_CATEGORY: CommandCategory = CommandCategory::Quick;
322
323    fn write_into(
324        &self,
325        camera_id: crate::camera_id::CameraId,
326        buffer: &mut [u8],
327    ) -> Result<usize, Error> {
328        use crate::command::bytes::ConstCommandBuilder;
329
330        let builder = match self {
331            Self::On => ConstCommandBuilder::<6>::new()
332                .append(crate::command::bytes::constants::focus::LOCK_PREFIX)
333                .push(0x02),
334            Self::Off => ConstCommandBuilder::<6>::new()
335                .append(crate::command::bytes::constants::focus::LOCK_PREFIX)
336                .push(0x03),
337        };
338
339        builder
340            .with_camera_id(camera_id)
341            .terminate()
342            .build_into(buffer)
343    }
344}
345
346/// Push AF command.
347///
348/// Controls the Push Auto Focus feature which temporarily activates
349/// auto focus when pressed.
350///
351/// **Vendor-Specific**: This command is specific to Sony FR7 cameras.
352#[derive(Debug, Copy, Clone)]
353#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
354#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
355#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
356#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS), ts(export))]
357pub enum PushAF {
358    /// Press Push AF button (activate temporary auto focus)
359    Press,
360    /// Release Push AF button
361    Release,
362}
363
364impl ViscaCommand for PushAF {
365    const MAX_SIZE: usize = 8;
366    const TIMEOUT_CATEGORY: CommandCategory = CommandCategory::Quick;
367
368    fn write_into(
369        &self,
370        camera_id: crate::camera_id::CameraId,
371        buffer: &mut [u8],
372    ) -> Result<usize, Error> {
373        use crate::command::bytes::{constants, ConstCommandBuilder};
374
375        let mut builder = ConstCommandBuilder::<8>::new();
376        builder.append_mut(constants::focus::PUSH_AF_PREFIX);
377        builder.push_mut(match self {
378            Self::Press => 0x01,
379            Self::Release => 0x00,
380        });
381        builder
382            .with_camera_id(camera_id)
383            .terminate()
384            .build_into(buffer)
385    }
386}
387
388#[cfg(test)]
389#[allow(clippy::panic)]
390mod tests {
391    use super::*;
392    use crate::command::bytes::VISCA_TERMINATOR;
393    use crate::command::encode::ViscaCommand;
394    use crate::macros::test_utils::visca_test;
395    use crate::timeout::CommandTimeout;
396
397    visca_test!(
398        Focus,
399        test_focus_command_stop,
400        Focus::Stop,
401        &[0x81, 0x01, 0x04, 0x08, 0x00, VISCA_TERMINATOR]
402    );
403
404    visca_test!(
405        Focus,
406        test_focus_command_far_standard,
407        Focus::Far,
408        &[0x81, 0x01, 0x04, 0x08, 0x02, VISCA_TERMINATOR]
409    );
410
411    visca_test!(
412        Focus,
413        test_focus_command_near_standard,
414        Focus::Near,
415        &[0x81, 0x01, 0x04, 0x08, 0x03, VISCA_TERMINATOR]
416    );
417
418    #[test]
419    fn test_focus_command_far_variable() {
420        // Valid speeds
421        for speed_val in 0..=7 {
422            let speed = FocusSpeed::new(speed_val)
423                .unwrap_or_else(|e| panic!("Test assertion failed: {e:?}"));
424            let cmd = Focus::FarWithSpeed(speed);
425            assert_eq!(
426                cmd.to_bytes(crate::camera_id::CameraId::CAMERA_1)
427                    .map(|b| b.to_vec())
428                    .unwrap_or_else(|e| panic!("Test assertion failed: {e:?}")),
429                vec![0x81, 0x01, 0x04, 0x08, 0x20 | speed_val, VISCA_TERMINATOR]
430            );
431        }
432    }
433
434    #[test]
435    fn test_focus_command_near_variable() {
436        // Valid speeds
437        for speed_val in 0..=7 {
438            let speed = FocusSpeed::new(speed_val)
439                .unwrap_or_else(|e| panic!("Test assertion failed: {e:?}"));
440            let cmd = Focus::NearWithSpeed(speed);
441            assert_eq!(
442                cmd.to_bytes(crate::camera_id::CameraId::CAMERA_1)
443                    .map(|b| b.to_vec())
444                    .unwrap_or_else(|e| panic!("Test assertion failed: {e:?}")),
445                vec![0x81, 0x01, 0x04, 0x08, 0x30 | speed_val, VISCA_TERMINATOR]
446            );
447        }
448    }
449
450    #[test]
451    fn test_focus_speed_validation() {
452        // Valid speeds
453        assert!(FocusSpeed::new(0).is_ok());
454        assert!(FocusSpeed::new(7).is_ok());
455
456        // Invalid speeds
457        assert!(matches!(
458            FocusSpeed::new(8),
459            Err(Error::InvalidParameter { .. })
460        ));
461    }
462
463    #[test]
464    fn test_focus_command_position() {
465        let cmd = Focus::Position(FocusPosition::new(0x1234));
466        assert_eq!(
467            cmd.to_bytes(crate::camera_id::CameraId::CAMERA_1)
468                .map(|b| b.to_vec())
469                .unwrap_or_else(|e| panic!("Test assertion failed: {e:?}")),
470            vec![
471                0x81,
472                0x01,
473                0x04,
474                0x48,
475                0x01,
476                0x02,
477                0x03,
478                0x04,
479                VISCA_TERMINATOR
480            ]
481        );
482
483        let cmd = Focus::Position(FocusPosition::new(0xF000));
484        assert_eq!(
485            cmd.to_bytes(crate::camera_id::CameraId::CAMERA_1)
486                .map(|b| b.to_vec())
487                .unwrap_or_else(|e| panic!("Test assertion failed: {e:?}")),
488            vec![
489                0x81,
490                0x01,
491                0x04,
492                0x48,
493                0x0F,
494                0x00,
495                0x00,
496                0x00,
497                VISCA_TERMINATOR
498            ]
499        );
500    }
501
502    visca_test!(
503        Focus,
504        test_focus_command_auto,
505        Focus::Auto,
506        &[0x81, 0x01, 0x04, 0x38, 0x02, VISCA_TERMINATOR]
507    );
508
509    visca_test!(
510        Focus,
511        test_focus_command_manual,
512        Focus::Manual,
513        &[0x81, 0x01, 0x04, 0x38, 0x03, VISCA_TERMINATOR]
514    );
515
516    visca_test!(
517        Focus,
518        test_focus_command_toggle,
519        Focus::Toggle,
520        &[0x81, 0x01, 0x04, 0x38, 0x10, VISCA_TERMINATOR]
521    );
522
523    visca_test!(
524        Focus,
525        test_focus_command_snap,
526        Focus::Snap,
527        &[0x81, 0x01, 0x04, 0x38, 0x04, VISCA_TERMINATOR]
528    );
529
530    visca_test!(
531        Focus,
532        test_focus_command_one_push_trigger,
533        Focus::OnePushTrigger,
534        &[0x81, 0x01, 0x04, 0x18, 0x01, VISCA_TERMINATOR]
535    );
536
537    visca_test!(
538        Focus,
539        test_focus_command_infinity,
540        Focus::Infinity,
541        &[0x81, 0x01, 0x04, 0x18, 0x02, VISCA_TERMINATOR]
542    );
543
544    #[test]
545    fn test_focus_zone_command() {
546        let cmd = FocusZoneCommand {
547            zone: FocusZone::Top,
548        };
549        assert_eq!(
550            cmd.to_bytes(crate::camera_id::CameraId::CAMERA_1)
551                .map(|b| b.to_vec())
552                .unwrap_or_else(|e| panic!("Test assertion failed: {e:?}")),
553            vec![0x81, 0x01, 0x04, 0xAA, 0x00, VISCA_TERMINATOR]
554        );
555
556        let cmd = FocusZoneCommand {
557            zone: FocusZone::Center,
558        };
559        assert_eq!(
560            cmd.to_bytes(crate::camera_id::CameraId::CAMERA_1)
561                .map(|b| b.to_vec())
562                .unwrap_or_else(|e| panic!("Test assertion failed: {e:?}")),
563            vec![0x81, 0x01, 0x04, 0xAA, 0x01, VISCA_TERMINATOR]
564        );
565
566        let cmd = FocusZoneCommand {
567            zone: FocusZone::Bottom,
568        };
569        assert_eq!(
570            cmd.to_bytes(crate::camera_id::CameraId::CAMERA_1)
571                .map(|b| b.to_vec())
572                .unwrap_or_else(|e| panic!("Test assertion failed: {e:?}")),
573            vec![0x81, 0x01, 0x04, 0xAA, 0x02, VISCA_TERMINATOR]
574        );
575    }
576
577    #[test]
578    fn test_auto_focus_sensitivity_command() {
579        let cmd = AutoFocusSensitivityCommand {
580            sensitivity: AutoFocusSensitivity::High,
581        };
582        assert_eq!(
583            cmd.to_bytes(crate::camera_id::CameraId::CAMERA_1)
584                .map(|b| b.to_vec())
585                .unwrap_or_else(|e| panic!("Test assertion failed: {e:?}")),
586            vec![0x81, 0x01, 0x04, 0x58, 0x02, VISCA_TERMINATOR]
587        );
588
589        let cmd = AutoFocusSensitivityCommand {
590            sensitivity: AutoFocusSensitivity::Normal,
591        };
592        assert_eq!(
593            cmd.to_bytes(crate::camera_id::CameraId::CAMERA_1)
594                .map(|b| b.to_vec())
595                .unwrap_or_else(|e| panic!("Test assertion failed: {e:?}")),
596            vec![0x81, 0x01, 0x04, 0x58, 0x01, VISCA_TERMINATOR]
597        );
598
599        let cmd = AutoFocusSensitivityCommand {
600            sensitivity: AutoFocusSensitivity::Low,
601        };
602        assert_eq!(
603            cmd.to_bytes(crate::camera_id::CameraId::CAMERA_1)
604                .map(|b| b.to_vec())
605                .unwrap_or_else(|e| panic!("Test assertion failed: {e:?}")),
606            vec![0x81, 0x01, 0x04, 0x58, 0x00, VISCA_TERMINATOR]
607        );
608    }
609
610    #[test]
611    fn test_focus_near_limit_command() {
612        let cmd = FocusNearLimitCommand {
613            position: FocusPosition::new(0x1234),
614        };
615        assert_eq!(
616            cmd.to_bytes(crate::camera_id::CameraId::CAMERA_1)
617                .map(|b| b.to_vec())
618                .unwrap_or_else(|e| panic!("Test assertion failed: {e:?}")),
619            vec![
620                0x81,
621                0x01,
622                0x04,
623                0x28,
624                0x01,
625                0x02,
626                0x03,
627                0x04,
628                VISCA_TERMINATOR
629            ]
630        );
631
632        let cmd = FocusNearLimitCommand {
633            position: FocusPosition::new(0x1000),
634        };
635        assert_eq!(
636            cmd.to_bytes(crate::camera_id::CameraId::CAMERA_1)
637                .map(|b| b.to_vec())
638                .unwrap_or_else(|e| panic!("Test assertion failed: {e:?}")),
639            vec![
640                0x81,
641                0x01,
642                0x04,
643                0x28,
644                0x01,
645                0x00,
646                0x00,
647                0x00,
648                VISCA_TERMINATOR
649            ]
650        );
651    }
652
653    #[test]
654    fn test_command_categories() {
655        assert_eq!(Focus::Stop.timeout_class(), CommandCategory::Movement);
656        assert_eq!(Focus::Auto.timeout_class(), CommandCategory::Movement);
657        assert_eq!(
658            FocusZoneCommand {
659                zone: FocusZone::Top
660            }
661            .timeout_class(),
662            CommandCategory::Quick
663        );
664        assert_eq!(
665            AutoFocusSensitivityCommand {
666                sensitivity: AutoFocusSensitivity::High
667            }
668            .timeout_class(),
669            CommandCategory::Quick
670        );
671        assert_eq!(
672            FocusNearLimitCommand {
673                position: FocusPosition::new(0x1000)
674            }
675            .timeout_class(),
676            CommandCategory::Quick
677        );
678    }
679}