Skip to main content

grafton_visca/command/
menu.rs

1//! Menu control commands for camera on-screen display (OSD) menu navigation.
2//!
3//! This module provides VISCA commands for controlling the camera's built-in menu system,
4//! allowing remote navigation and configuration. These commands are particularly useful
5//! for Sony FR7 and other cameras with comprehensive on-screen menus.
6
7use crate::{command::bytes::ConstCommandBuilder, timeout::CommandCategory, visca_command};
8
9visca_command! {
10    /// Menu display control command.
11    ///
12    /// Toggles the camera's on-screen menu display on or off.
13    ///
14    /// VISCA format: `81 01 06 06 0p FF` where p = 2 (On) or 3 (Off)
15    pub struct SetMenuDisplay {
16        on: bool,
17    };
18    prefix = [0x01, 0x06, 0x06];
19    param = if *on { 0x02 } else { 0x03 };
20    max_param_size = 1;
21    category = CommandCategory::Quick;
22}
23
24impl SetMenuDisplay {
25    /// Create a new menu display command.
26    pub fn new(on: bool) -> Self {
27        Self { on }
28    }
29}
30
31/// Menu navigation direction.
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
34#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
35#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
36#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS), ts(export))]
37pub enum MenuDirection {
38    /// Move cursor up
39    Up,
40    /// Move cursor down
41    Down,
42    /// Move cursor left
43    Left,
44    /// Move cursor right
45    Right,
46}
47
48// Manual implementation for MenuNavigate due to complex direction mapping
49impl crate::command::ViscaCommand for MenuNavigate {
50    const MAX_SIZE: usize = 9;
51    const TIMEOUT_CATEGORY: CommandCategory = CommandCategory::Quick;
52
53    fn write_into(
54        &self,
55        camera_id: crate::camera_id::CameraId,
56        buffer: &mut [u8],
57    ) -> Result<usize, crate::Error> {
58        let mut builder = ConstCommandBuilder::<9>::new();
59        builder.push_mut(camera_id.to_address_byte());
60        builder.append_mut(&[0x01, 0x06, 0x01, 0x0E, 0x0E]);
61        match self.direction {
62            MenuDirection::Up => builder.append_mut(&[0x03, 0x01]),
63            MenuDirection::Down => builder.append_mut(&[0x03, 0x02]),
64            MenuDirection::Left => builder.append_mut(&[0x01, 0x03]),
65            MenuDirection::Right => builder.append_mut(&[0x02, 0x03]),
66        };
67        builder.terminate().build_into(buffer)
68    }
69}
70
71/// Menu navigation command for cursor movement.
72///
73/// Moves the menu cursor in the specified direction.
74///
75/// VISCA format: `81 01 06 01 VV WW XX YY FF` where:
76/// - VV = Pan speed (0x0E for menu)
77/// - WW = Tilt speed (0x0E for menu)
78/// - XX YY = Direction codes
79#[derive(Debug, Clone, Copy)]
80pub struct MenuNavigate {
81    direction: MenuDirection,
82}
83
84impl MenuNavigate {
85    /// Create a new menu navigation command.
86    pub fn new(direction: MenuDirection) -> Self {
87        Self { direction }
88    }
89}
90
91/// Menu action type.
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
93#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
94#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
95#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
96#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS), ts(export))]
97pub enum MenuAction {
98    /// Select/Enter the current menu item
99    Select,
100    /// Cancel/Back to previous menu
101    Cancel,
102}
103
104impl From<MenuAction> for u8 {
105    fn from(action: MenuAction) -> u8 {
106        match action {
107            MenuAction::Select => 0x05,
108            MenuAction::Cancel => 0x04,
109        }
110    }
111}
112
113visca_command! {
114    /// Menu action command for select/cancel operations.
115    ///
116    /// Performs menu selection (Enter) or cancellation (Back) actions.
117    ///
118    /// VISCA format: `81 01 06 06 0p FF` where p = 5 (Select) or 4 (Cancel)
119    pub struct PerformMenuAction {
120        action: MenuAction,
121    };
122    prefix = [0x01, 0x06, 0x06];
123    param = u8::from(*action);
124    max_param_size = 1;
125    category = CommandCategory::Quick;
126}
127
128impl PerformMenuAction {
129    /// Create a new menu action command.
130    pub fn new(action: MenuAction) -> Self {
131        Self { action }
132    }
133}
134
135/// Direct menu control command for Sony FR7.
136///
137/// Provides direct control over the FR7's advanced menu system using
138/// manufacturer-specific codes for button presses and dial turns.
139///
140/// VISCA format: `81 01 7E 04 72 pp qq FF`
141#[derive(Debug, Copy, Clone)]
142pub struct DirectMenuControl {
143    /// The control1 parameter.
144    /// First control byte (pp)
145    pub control1: u8,
146    /// The control2 parameter.
147    /// Second control byte (qq)
148    pub control2: u8,
149}
150
151impl crate::command::ViscaCommand for DirectMenuControl {
152    const MAX_SIZE: usize = 8;
153    const TIMEOUT_CATEGORY: CommandCategory = CommandCategory::Quick;
154
155    fn write_into(
156        &self,
157        camera_id: crate::camera_id::CameraId,
158        buffer: &mut [u8],
159    ) -> Result<usize, crate::Error> {
160        let mut builder = ConstCommandBuilder::<8>::new();
161        builder.push_mut(camera_id.to_address_byte());
162        builder.append_mut(&[0x01, 0x7E, 0x04, 0x72]);
163        builder.push_mut(self.control1);
164        builder.push_mut(self.control2);
165        builder.terminate().build_into(buffer)
166    }
167}
168
169impl DirectMenuControl {
170    /// Create a new direct menu control command.
171    pub fn new(control1: u8, control2: u8) -> Self {
172        Self { control1, control2 }
173    }
174
175    /// Menu open/close toggle (FR7).
176    pub fn open_close() -> Self {
177        Self::new(0x00, 0x01)
178    }
179}
180
181#[cfg(test)]
182#[allow(clippy::expect_used, clippy::unwrap_used)]
183mod tests {
184    use super::*;
185
186    use crate::{command::bytes::VISCA_TERMINATOR, macros::test_utils::visca_test};
187
188    visca_test!(
189        SetMenuDisplay,
190        test_menu_display_on,
191        SetMenuDisplay::new(true),
192        &[0x81, 0x01, 0x06, 0x06, 0x02, VISCA_TERMINATOR]
193    );
194
195    visca_test!(
196        SetMenuDisplay,
197        test_menu_display_off,
198        SetMenuDisplay::new(false),
199        &[0x81, 0x01, 0x06, 0x06, 0x03, VISCA_TERMINATOR]
200    );
201
202    visca_test!(
203        MenuNavigate,
204        test_menu_navigate_up,
205        MenuNavigate::new(MenuDirection::Up),
206        &[
207            0x81,
208            0x01,
209            0x06,
210            0x01,
211            0x0E,
212            0x0E,
213            0x03,
214            0x01,
215            VISCA_TERMINATOR
216        ]
217    );
218
219    visca_test!(
220        MenuNavigate,
221        test_menu_navigate_down,
222        MenuNavigate::new(MenuDirection::Down),
223        &[
224            0x81,
225            0x01,
226            0x06,
227            0x01,
228            0x0E,
229            0x0E,
230            0x03,
231            0x02,
232            VISCA_TERMINATOR
233        ]
234    );
235
236    visca_test!(
237        MenuNavigate,
238        test_menu_navigate_left,
239        MenuNavigate::new(MenuDirection::Left),
240        &[
241            0x81,
242            0x01,
243            0x06,
244            0x01,
245            0x0E,
246            0x0E,
247            0x01,
248            0x03,
249            VISCA_TERMINATOR
250        ]
251    );
252
253    visca_test!(
254        MenuNavigate,
255        test_menu_navigate_right,
256        MenuNavigate::new(MenuDirection::Right),
257        &[
258            0x81,
259            0x01,
260            0x06,
261            0x01,
262            0x0E,
263            0x0E,
264            0x02,
265            0x03,
266            VISCA_TERMINATOR
267        ]
268    );
269
270    visca_test!(
271        PerformMenuAction,
272        test_menu_select,
273        PerformMenuAction::new(MenuAction::Select),
274        &[0x81, 0x01, 0x06, 0x06, 0x05, VISCA_TERMINATOR]
275    );
276
277    visca_test!(
278        PerformMenuAction,
279        test_menu_cancel,
280        PerformMenuAction::new(MenuAction::Cancel),
281        &[0x81, 0x01, 0x06, 0x06, 0x04, VISCA_TERMINATOR]
282    );
283
284    visca_test!(
285        DirectMenuControl,
286        test_direct_menu_control,
287        DirectMenuControl::new(0x00, 0x01),
288        &[0x81, 0x01, 0x7E, 0x04, 0x72, 0x00, 0x01, VISCA_TERMINATOR]
289    );
290
291    visca_test!(
292        DirectMenuControl,
293        test_direct_menu_open_close,
294        DirectMenuControl::open_close(),
295        &[0x81, 0x01, 0x7E, 0x04, 0x72, 0x00, 0x01, VISCA_TERMINATOR]
296    );
297}