Skip to main content

autocore_std/iface/
command_interface.rs

1//! Command Interface protocol.
2//!
3//! A handshake-based command protocol for receiving commands from external sources
4//! (UI, HMI, other modules) via shared memory. Based on CANopen/EtherCAT state
5//! machine conventions.
6//!
7//! # Protocol
8//!
9//! The external source (e.g. HMI) and the control program communicate through
10//! six shared memory fields, managed via a [`CommandInterfaceView`]:
11//!
12//! | Field | Direction | Description |
13//! |-------|-----------|-------------|
14//! | `command` | External → Control | Request code ([`CommandRequest`] values) |
15//! | `command_code` | External → Control | Identifies the specific command |
16//! | `command_arg_1` | External → Control | First argument (meaning defined per command code) |
17//! | `command_arg_2` | External → Control | Second argument |
18//! | `command_status` | Control → External | Current status ([`CommandStatus`] values) |
19//! | `command_result` | Control → External | Result value from completed command |
20//!
21//! # Handshake Sequence
22//!
23//! ```text
24//! External                          Control Program
25//! ────────                          ───────────────
26//! 1. Write command_code, arg_1, arg_2
27//! 2. Set command = EXEC (11)
28//!                                   3. Sees EXEC → status = EXECUTING (20)
29//!                                      call() returns Some(command_code)
30//!                                   4. Process the command...
31//!                                   5. set_done() → status = DONE (100)
32//! 6. Read command_result
33//! 7. Set command = ACKDONE (101)
34//!                                   8. Sees ACKDONE → status = IDLE (10)
35//! ```
36//!
37//! # Example
38//!
39//! ```
40//! use autocore_std::iface::{CommandInterface, CommandInterfaceView, CommandStatus, CommandRequest};
41//!
42//! // These would normally be GlobalMemory fields
43//! let mut command: u16 = CommandRequest::Idle.as_u16();
44//! let mut command_code: u32 = 0;
45//! let mut arg_1: f64 = 0.0;
46//! let mut arg_2: f64 = 0.0;
47//! let mut status: u16 = 0;
48//! let mut result: f64 = 0.0;
49//!
50//! let mut cmd = CommandInterface::new();
51//!
52//! // First scan — initializes to IDLE
53//! let mut view = CommandInterfaceView {
54//!     command: &mut command,
55//!     command_code: &mut command_code,
56//!     command_arg_1: &mut arg_1,
57//!     command_arg_2: &mut arg_2,
58//!     command_status: &mut status,
59//!     command_result: &mut result,
60//! };
61//! assert_eq!(cmd.call(&mut view), None);
62//! assert_eq!(*view.command_status, CommandStatus::Idle.as_u16());
63//!
64//! // External source sends a command
65//! *view.command_code = 42;
66//! *view.command_arg_1 = 3.14;
67//! *view.command = CommandRequest::Execute.as_u16();
68//!
69//! // Next scan — command interface picks it up
70//! assert_eq!(cmd.call(&mut view), Some(42));
71//! assert_eq!(*view.command_status, CommandStatus::Executing.as_u16());
72//!
73//! // Subsequent scans — still executing, call() returns None
74//! assert_eq!(cmd.call(&mut view), None);
75//!
76//! // Control program finishes the command
77//! cmd.set_done(&mut view, 99.0);
78//! assert_eq!(*view.command_status, CommandStatus::Done.as_u16());
79//! assert_eq!(*view.command_result, 99.0);
80//!
81//! // External source acknowledges
82//! *view.command = CommandRequest::AckDone.as_u16();
83//! assert_eq!(cmd.call(&mut view), None);
84//! assert_eq!(*view.command_status, CommandStatus::Idle.as_u16());
85//! ```
86
87/// Status of the command interface, written by the control program.
88///
89/// These values are written to `command_status` in [`CommandInterfaceView`].
90#[repr(u16)]
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92pub enum CommandStatus {
93    /// Initial state before first scan.
94    Init = 1,
95    /// Ready to accept a new command.
96    Idle = 10,
97    /// A command is currently being processed.
98    Executing = 20,
99    /// Command completed successfully. Result is available in `command_result`.
100    Done = 100,
101    /// Command failed. The control program may place an error code in `command_result`.
102    Error = 900,
103}
104
105impl CommandStatus {
106    /// Convert to the underlying u16 wire value.
107    pub fn as_u16(self) -> u16 {
108        self as u16
109    }
110
111    /// Try to convert a raw u16 from shared memory into a `CommandStatus`.
112    /// Returns `None` if the value does not match any known variant.
113    pub fn from_u16(val: u16) -> Option<Self> {
114        match val {
115            1 => Some(Self::Init),
116            10 => Some(Self::Idle),
117            20 => Some(Self::Executing),
118            100 => Some(Self::Done),
119            900 => Some(Self::Error),
120            _ => None,
121        }
122    }
123}
124
125/// Request codes written by the external source (UI/HMI).
126///
127/// These values are written to `command` in [`CommandInterfaceView`].
128#[repr(u16)]
129#[derive(Debug, Clone, Copy, PartialEq, Eq)]
130pub enum CommandRequest {
131    /// No request. The external source should set this after the full handshake completes.
132    Idle = 10,
133    /// Request execution of the command specified by `command_code`.
134    Execute = 11,
135    /// Acknowledge that the command result has been read. Allows the interface to
136    /// return to [`CommandStatus::Idle`].
137    AckDone = 101,
138}
139
140impl CommandRequest {
141    /// Convert to the underlying u16 wire value.
142    pub fn as_u16(self) -> u16 {
143        self as u16
144    }
145
146    /// Try to convert a raw u16 from shared memory into a `CommandRequest`.
147    /// Returns `None` if the value does not match any known variant.
148    pub fn from_u16(val: u16) -> Option<Self> {
149        match val {
150            10 => Some(Self::Idle),
151            11 => Some(Self::Execute),
152            101 => Some(Self::AckDone),
153            _ => None,
154        }
155    }
156}
157
158/// View struct for mapping command interface signals to GlobalMemory fields.
159///
160/// Create one of these each scan cycle by borrowing the appropriate fields
161/// from your `GlobalMemory` struct, then pass it to [`CommandInterface::call`].
162///
163/// # Example
164///
165/// ```ignore
166/// fn cmd_view(gm: &mut GlobalMemory) -> CommandInterfaceView<'_> {
167///     CommandInterfaceView {
168///         command:        &mut gm.robot_command,
169///         command_code:   &mut gm.robot_command_code,
170///         command_arg_1:  &mut gm.robot_arg_1,
171///         command_arg_2:  &mut gm.robot_arg_2,
172///         command_status: &mut gm.robot_status,
173///         command_result: &mut gm.robot_result,
174///     }
175/// }
176/// ```
177pub struct CommandInterfaceView<'a> {
178    /// Request from external source ([`CommandRequest`] values stored as u16).
179    pub command: &'a mut u16,
180    /// Identifies the specific command to execute.
181    pub command_code: &'a mut u32,
182    /// First argument to the command. Meaning is defined per command code.
183    pub command_arg_1: &'a mut f64,
184    /// Second argument to the command. Meaning is defined per command code.
185    pub command_arg_2: &'a mut f64,
186    /// Current status of the interface ([`CommandStatus`] values stored as u16).
187    pub command_status: &'a mut u16,
188    /// Result value from the most recently completed command.
189    pub command_result: &'a mut f64,
190}
191
192/// Command Interface function block.
193///
194/// Manages the handshake protocol between an external command source and the
195/// control program. Call [`call`](Self::call) once per scan cycle. When a new
196/// command arrives, `call` returns `Some(command_code)`. The control program
197/// then processes the command and calls [`set_done`](Self::set_done) or
198/// [`set_error`](Self::set_error) when finished.
199///
200/// # Usage Pattern
201///
202/// ```ignore
203/// // In your control program struct:
204/// cmd: CommandInterface,
205///
206/// // In process_tick:
207/// let mut cmd_view = my_cmd_view(gm);
208/// if let Some(code) = self.cmd.call(&mut cmd_view) {
209///     match code {
210///         1 => { /* start something */ }
211///         2 => { /* do something else */ }
212///         _ => { self.cmd.set_error(&mut cmd_view, -1.0); }
213///     }
214/// }
215///
216/// // When async work completes later:
217/// if self.cmd.is_executing() && work_is_done {
218///     self.cmd.set_done(&mut cmd_view, result_value);
219/// }
220/// ```
221#[derive(Debug, Clone)]
222pub struct CommandInterface {
223    initialized: bool,
224    active_command: Option<u32>,
225}
226
227impl CommandInterface {
228    /// Creates a new command interface. Status will be set to [`CommandStatus::Idle`]
229    /// on the first call to [`call`](Self::call).
230    pub fn new() -> Self {
231        Self {
232            initialized: false,
233            active_command: None,
234        }
235    }
236
237    /// Call once per scan cycle.
238    ///
239    /// Returns `Some(command_code)` on the scan where a new `Execute` request is
240    /// detected while the interface is `Idle`. Returns `None` on all other scans.
241    ///
242    /// This method handles the full handshake lifecycle:
243    /// - **Idle + Execute** → transition to `Executing`, return the command code
244    /// - **Done + AckDone** → transition back to `Idle`
245    /// - **Error + AckDone** → transition back to `Idle`
246    pub fn call(&mut self, view: &mut CommandInterfaceView) -> Option<u32> {
247        if !self.initialized {
248            *view.command_status = CommandStatus::Idle.as_u16();
249            self.initialized = true;
250            return None;
251        }
252
253        let status = *view.command_status;
254        let command = *view.command;
255
256        if status == CommandStatus::Idle.as_u16() {
257            if command == CommandRequest::Execute.as_u16() {
258                self.active_command = Some(*view.command_code);
259                *view.command_status = CommandStatus::Executing.as_u16();
260                return self.active_command;
261            }
262        } else if status == CommandStatus::Done.as_u16() || status == CommandStatus::Error.as_u16() {
263            if command == CommandRequest::AckDone.as_u16() {
264                self.active_command = None;
265                *view.command_status = CommandStatus::Idle.as_u16();
266                *view.command = CommandRequest::Idle.as_u16();
267            }
268        }
269
270        None
271    }
272
273    /// Returns `true` if a command is currently being executed.
274    pub fn is_executing(&self) -> bool {
275        self.active_command.is_some()
276    }
277
278    /// Returns the command code currently being executed, or `None` if idle.
279    pub fn active_command(&self) -> Option<u32> {
280        self.active_command
281    }
282
283    /// Mark the current command as completed successfully.
284    ///
285    /// Sets `command_status` to [`CommandStatus::Done`] and writes `result` to
286    /// `command_result`. The interface will wait for the external source to send
287    /// [`CommandRequest::AckDone`] before returning to idle.
288    pub fn set_done(&mut self, view: &mut CommandInterfaceView, result: f64) {
289        *view.command_result = result;
290        *view.command_status = CommandStatus::Done.as_u16();
291    }
292
293    /// Mark the current command as failed.
294    ///
295    /// Sets `command_status` to [`CommandStatus::Error`]. Optionally write an
296    /// error code or description to `command_result` before calling this.
297    /// The interface will wait for [`CommandRequest::AckDone`] before returning
298    /// to idle.
299    pub fn set_error(&mut self, view: &mut CommandInterfaceView, error_result: f64) {
300        *view.command_result = error_result;
301        *view.command_status = CommandStatus::Error.as_u16();
302    }
303}
304
305impl Default for CommandInterface {
306    fn default() -> Self {
307        Self::new()
308    }
309}
310
311#[cfg(test)]
312mod tests {
313    use super::*;
314
315    fn make_view_fields() -> (u16, u32, f64, f64, u16, f64) {
316        (CommandRequest::Idle.as_u16(), 0u32, 0.0f64, 0.0f64, 0u16, 0.0f64)
317    }
318
319    macro_rules! view {
320        ($cmd:expr, $code:expr, $a1:expr, $a2:expr, $status:expr, $result:expr) => {
321            CommandInterfaceView {
322                command: &mut $cmd,
323                command_code: &mut $code,
324                command_arg_1: &mut $a1,
325                command_arg_2: &mut $a2,
326                command_status: &mut $status,
327                command_result: &mut $result,
328            }
329        };
330    }
331
332    #[test]
333    fn test_initialization() {
334        let mut ci = CommandInterface::new();
335        let (mut cmd, mut code, mut a1, mut a2, mut status, mut result) = make_view_fields();
336        let mut v = view!(cmd, code, a1, a2, status, result);
337
338        assert_eq!(ci.call(&mut v), None);
339        assert_eq!(*v.command_status, CommandStatus::Idle.as_u16());
340        assert!(!ci.is_executing());
341    }
342
343    #[test]
344    fn test_full_handshake() {
345        let mut ci = CommandInterface::new();
346        let (mut cmd, mut code, mut a1, mut a2, mut status, mut result) = make_view_fields();
347
348        // Init scan
349        {
350            let mut v = view!(cmd, code, a1, a2, status, result);
351            ci.call(&mut v);
352        }
353        assert_eq!(status, CommandStatus::Idle.as_u16());
354
355        // External sends command
356        code = 42;
357        a1 = 3.14;
358        a2 = 2.71;
359        cmd = CommandRequest::Execute.as_u16();
360
361        // Scan picks it up
362        {
363            let mut v = view!(cmd, code, a1, a2, status, result);
364            assert_eq!(ci.call(&mut v), Some(42));
365        }
366        assert_eq!(status, CommandStatus::Executing.as_u16());
367        assert!(ci.is_executing());
368        assert_eq!(ci.active_command(), Some(42));
369
370        // Subsequent scan — still executing
371        {
372            let mut v = view!(cmd, code, a1, a2, status, result);
373            assert_eq!(ci.call(&mut v), None);
374        }
375
376        // Control program completes
377        {
378            let mut v = view!(cmd, code, a1, a2, status, result);
379            ci.set_done(&mut v, 99.0);
380        }
381        assert_eq!(status, CommandStatus::Done.as_u16());
382        assert_eq!(result, 99.0);
383
384        // External acknowledges
385        cmd = CommandRequest::AckDone.as_u16();
386        {
387            let mut v = view!(cmd, code, a1, a2, status, result);
388            assert_eq!(ci.call(&mut v), None);
389        }
390        assert_eq!(status, CommandStatus::Idle.as_u16());
391        assert_eq!(cmd, CommandRequest::Idle.as_u16());
392        assert!(!ci.is_executing());
393        assert_eq!(ci.active_command(), None);
394    }
395
396    #[test]
397    fn test_error_handshake() {
398        let mut ci = CommandInterface::new();
399        let (mut cmd, mut code, mut a1, mut a2, mut status, mut result) = make_view_fields();
400
401        // Init
402        {
403            let mut v = view!(cmd, code, a1, a2, status, result);
404            ci.call(&mut v);
405        }
406
407        // Send command
408        code = 7;
409        cmd = CommandRequest::Execute.as_u16();
410        {
411            let mut v = view!(cmd, code, a1, a2, status, result);
412            assert_eq!(ci.call(&mut v), Some(7));
413        }
414
415        // Command fails
416        {
417            let mut v = view!(cmd, code, a1, a2, status, result);
418            ci.set_error(&mut v, -1.0);
419        }
420        assert_eq!(status, CommandStatus::Error.as_u16());
421        assert_eq!(result, -1.0);
422
423        // External acknowledges the error
424        cmd = CommandRequest::AckDone.as_u16();
425        {
426            let mut v = view!(cmd, code, a1, a2, status, result);
427            ci.call(&mut v);
428        }
429        assert_eq!(status, CommandStatus::Idle.as_u16());
430        assert!(!ci.is_executing());
431    }
432
433    #[test]
434    fn test_ignores_exec_while_executing() {
435        let mut ci = CommandInterface::new();
436        let (mut cmd, mut code, mut a1, mut a2, mut status, mut result) = make_view_fields();
437
438        // Init
439        {
440            let mut v = view!(cmd, code, a1, a2, status, result);
441            ci.call(&mut v);
442        }
443
444        // First command
445        code = 1;
446        cmd = CommandRequest::Execute.as_u16();
447        {
448            let mut v = view!(cmd, code, a1, a2, status, result);
449            assert_eq!(ci.call(&mut v), Some(1));
450        }
451
452        // External tries to send another EXEC while still executing — ignored
453        code = 2;
454        {
455            let mut v = view!(cmd, code, a1, a2, status, result);
456            assert_eq!(ci.call(&mut v), None);
457        }
458        assert_eq!(status, CommandStatus::Executing.as_u16());
459        assert_eq!(ci.active_command(), Some(1));
460    }
461
462    #[test]
463    fn test_idle_command_ignored_when_idle() {
464        let mut ci = CommandInterface::new();
465        let (mut cmd, mut code, mut a1, mut a2, mut status, mut result) = make_view_fields();
466
467        // Init
468        {
469            let mut v = view!(cmd, code, a1, a2, status, result);
470            ci.call(&mut v);
471        }
472
473        // Command stays IDLE — nothing happens
474        {
475            let mut v = view!(cmd, code, a1, a2, status, result);
476            assert_eq!(ci.call(&mut v), None);
477        }
478        assert_eq!(status, CommandStatus::Idle.as_u16());
479    }
480}