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
105/// Request codes written by the external source (UI/HMI).
106///
107/// These values are written to `command` in [`CommandInterfaceView`].
108#[repr(u16)]
109#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110pub enum CommandRequest {
111    /// No request. The external source should set this after the full handshake completes.
112    Idle = 10,
113    /// Request execution of the command specified by `command_code`.
114    Execute = 11,
115    /// Acknowledge that the command result has been read. Allows the interface to
116    /// return to [`CommandStatus::Idle`].
117    AckDone = 101,
118}
119
120/// View struct for mapping command interface signals to GlobalMemory fields.
121///
122/// Create one of these each scan cycle by borrowing the appropriate fields
123/// from your `GlobalMemory` struct, then pass it to [`CommandInterface::call`].
124///
125/// # Example
126///
127/// ```ignore
128/// fn cmd_view(gm: &mut GlobalMemory) -> CommandInterfaceView<'_> {
129///     CommandInterfaceView {
130///         command:        &mut gm.robot_command,
131///         command_code:   &mut gm.robot_command_code,
132///         command_arg_1:  &mut gm.robot_arg_1,
133///         command_arg_2:  &mut gm.robot_arg_2,
134///         command_status: &mut gm.robot_status,
135///         command_result: &mut gm.robot_result,
136///     }
137/// }
138/// ```
139pub struct CommandInterfaceView<'a> {
140    /// Request from external source ([`CommandRequest`] values stored as u16).
141    pub command: &'a mut u16,
142    /// Identifies the specific command to execute.
143    pub command_code: &'a mut u32,
144    /// First argument to the command. Meaning is defined per command code.
145    pub command_arg_1: &'a mut f64,
146    /// Second argument to the command. Meaning is defined per command code.
147    pub command_arg_2: &'a mut f64,
148    /// Current status of the interface ([`CommandStatus`] values stored as u16).
149    pub command_status: &'a mut u16,
150    /// Result value from the most recently completed command.
151    pub command_result: &'a mut f64,
152}
153
154/// Command Interface function block.
155///
156/// Manages the handshake protocol between an external command source and the
157/// control program. Call [`call`](Self::call) once per scan cycle. When a new
158/// command arrives, `call` returns `Some(command_code)`. The control program
159/// then processes the command and calls [`set_done`](Self::set_done) or
160/// [`set_error`](Self::set_error) when finished.
161///
162/// # Usage Pattern
163///
164/// ```ignore
165/// // In your control program struct:
166/// cmd: CommandInterface,
167///
168/// // In process_tick:
169/// let mut cmd_view = my_cmd_view(gm);
170/// if let Some(code) = self.cmd.call(&mut cmd_view) {
171///     match code {
172///         1 => { /* start something */ }
173///         2 => { /* do something else */ }
174///         _ => { self.cmd.set_error(&mut cmd_view, -1.0); }
175///     }
176/// }
177///
178/// // When async work completes later:
179/// if self.cmd.is_executing() && work_is_done {
180///     self.cmd.set_done(&mut cmd_view, result_value);
181/// }
182/// ```
183#[derive(Debug, Clone)]
184pub struct CommandInterface {
185    initialized: bool,
186    active_command: Option<u32>,
187}
188
189impl CommandInterface {
190    /// Creates a new command interface. Status will be set to [`CommandStatus::Idle`]
191    /// on the first call to [`call`](Self::call).
192    pub fn new() -> Self {
193        Self {
194            initialized: false,
195            active_command: None,
196        }
197    }
198
199    /// Call once per scan cycle.
200    ///
201    /// Returns `Some(command_code)` on the scan where a new `Execute` request is
202    /// detected while the interface is `Idle`. Returns `None` on all other scans.
203    ///
204    /// This method handles the full handshake lifecycle:
205    /// - **Idle + Execute** → transition to `Executing`, return the command code
206    /// - **Done + AckDone** → transition back to `Idle`
207    /// - **Error + AckDone** → transition back to `Idle`
208    pub fn call(&mut self, view: &mut CommandInterfaceView) -> Option<u32> {
209        if !self.initialized {
210            *view.command_status = CommandStatus::Idle as u16;
211            self.initialized = true;
212            return None;
213        }
214
215        let status = *view.command_status;
216        let command = *view.command;
217
218        if status == CommandStatus::Idle as u16 {
219            if command == CommandRequest::Execute as u16 {
220                self.active_command = Some(*view.command_code);
221                *view.command_status = CommandStatus::Executing as u16;
222                return self.active_command;
223            }
224        } else if status == CommandStatus::Done as u16 || status == CommandStatus::Error as u16 {
225            if command == CommandRequest::AckDone as u16 {
226                self.active_command = None;
227                *view.command_status = CommandStatus::Idle as u16;
228                *view.command = CommandRequest::Idle as u16;
229            }
230        }
231
232        None
233    }
234
235    /// Returns `true` if a command is currently being executed.
236    pub fn is_executing(&self) -> bool {
237        self.active_command.is_some()
238    }
239
240    /// Returns the command code currently being executed, or `None` if idle.
241    pub fn active_command(&self) -> Option<u32> {
242        self.active_command
243    }
244
245    /// Mark the current command as completed successfully.
246    ///
247    /// Sets `command_status` to [`CommandStatus::Done`] and writes `result` to
248    /// `command_result`. The interface will wait for the external source to send
249    /// [`CommandRequest::AckDone`] before returning to idle.
250    pub fn set_done(&mut self, view: &mut CommandInterfaceView, result: f64) {
251        *view.command_result = result;
252        *view.command_status = CommandStatus::Done as u16;
253    }
254
255    /// Mark the current command as failed.
256    ///
257    /// Sets `command_status` to [`CommandStatus::Error`]. Optionally write an
258    /// error code or description to `command_result` before calling this.
259    /// The interface will wait for [`CommandRequest::AckDone`] before returning
260    /// to idle.
261    pub fn set_error(&mut self, view: &mut CommandInterfaceView, error_result: f64) {
262        *view.command_result = error_result;
263        *view.command_status = CommandStatus::Error as u16;
264    }
265}
266
267impl Default for CommandInterface {
268    fn default() -> Self {
269        Self::new()
270    }
271}
272
273#[cfg(test)]
274mod tests {
275    use super::*;
276
277    fn make_view_fields() -> (u16, u32, f64, f64, u16, f64) {
278        (CommandRequest::Idle as u16, 0u32, 0.0f64, 0.0f64, 0u16, 0.0f64)
279    }
280
281    macro_rules! view {
282        ($cmd:expr, $code:expr, $a1:expr, $a2:expr, $status:expr, $result:expr) => {
283            CommandInterfaceView {
284                command: &mut $cmd,
285                command_code: &mut $code,
286                command_arg_1: &mut $a1,
287                command_arg_2: &mut $a2,
288                command_status: &mut $status,
289                command_result: &mut $result,
290            }
291        };
292    }
293
294    #[test]
295    fn test_initialization() {
296        let mut ci = CommandInterface::new();
297        let (mut cmd, mut code, mut a1, mut a2, mut status, mut result) = make_view_fields();
298        let mut v = view!(cmd, code, a1, a2, status, result);
299
300        assert_eq!(ci.call(&mut v), None);
301        assert_eq!(*v.command_status, CommandStatus::Idle as u16);
302        assert!(!ci.is_executing());
303    }
304
305    #[test]
306    fn test_full_handshake() {
307        let mut ci = CommandInterface::new();
308        let (mut cmd, mut code, mut a1, mut a2, mut status, mut result) = make_view_fields();
309
310        // Init scan
311        {
312            let mut v = view!(cmd, code, a1, a2, status, result);
313            ci.call(&mut v);
314        }
315        assert_eq!(status, CommandStatus::Idle as u16);
316
317        // External sends command
318        code = 42;
319        a1 = 3.14;
320        a2 = 2.71;
321        cmd = CommandRequest::Execute as u16;
322
323        // Scan picks it up
324        {
325            let mut v = view!(cmd, code, a1, a2, status, result);
326            assert_eq!(ci.call(&mut v), Some(42));
327        }
328        assert_eq!(status, CommandStatus::Executing as u16);
329        assert!(ci.is_executing());
330        assert_eq!(ci.active_command(), Some(42));
331
332        // Subsequent scan — still executing
333        {
334            let mut v = view!(cmd, code, a1, a2, status, result);
335            assert_eq!(ci.call(&mut v), None);
336        }
337
338        // Control program completes
339        {
340            let mut v = view!(cmd, code, a1, a2, status, result);
341            ci.set_done(&mut v, 99.0);
342        }
343        assert_eq!(status, CommandStatus::Done as u16);
344        assert_eq!(result, 99.0);
345
346        // External acknowledges
347        cmd = CommandRequest::AckDone as u16;
348        {
349            let mut v = view!(cmd, code, a1, a2, status, result);
350            assert_eq!(ci.call(&mut v), None);
351        }
352        assert_eq!(status, CommandStatus::Idle as u16);
353        assert_eq!(cmd, CommandRequest::Idle as u16);
354        assert!(!ci.is_executing());
355        assert_eq!(ci.active_command(), None);
356    }
357
358    #[test]
359    fn test_error_handshake() {
360        let mut ci = CommandInterface::new();
361        let (mut cmd, mut code, mut a1, mut a2, mut status, mut result) = make_view_fields();
362
363        // Init
364        {
365            let mut v = view!(cmd, code, a1, a2, status, result);
366            ci.call(&mut v);
367        }
368
369        // Send command
370        code = 7;
371        cmd = CommandRequest::Execute as u16;
372        {
373            let mut v = view!(cmd, code, a1, a2, status, result);
374            assert_eq!(ci.call(&mut v), Some(7));
375        }
376
377        // Command fails
378        {
379            let mut v = view!(cmd, code, a1, a2, status, result);
380            ci.set_error(&mut v, -1.0);
381        }
382        assert_eq!(status, CommandStatus::Error as u16);
383        assert_eq!(result, -1.0);
384
385        // External acknowledges the error
386        cmd = CommandRequest::AckDone as u16;
387        {
388            let mut v = view!(cmd, code, a1, a2, status, result);
389            ci.call(&mut v);
390        }
391        assert_eq!(status, CommandStatus::Idle as u16);
392        assert!(!ci.is_executing());
393    }
394
395    #[test]
396    fn test_ignores_exec_while_executing() {
397        let mut ci = CommandInterface::new();
398        let (mut cmd, mut code, mut a1, mut a2, mut status, mut result) = make_view_fields();
399
400        // Init
401        {
402            let mut v = view!(cmd, code, a1, a2, status, result);
403            ci.call(&mut v);
404        }
405
406        // First command
407        code = 1;
408        cmd = CommandRequest::Execute as u16;
409        {
410            let mut v = view!(cmd, code, a1, a2, status, result);
411            assert_eq!(ci.call(&mut v), Some(1));
412        }
413
414        // External tries to send another EXEC while still executing — ignored
415        code = 2;
416        {
417            let mut v = view!(cmd, code, a1, a2, status, result);
418            assert_eq!(ci.call(&mut v), None);
419        }
420        assert_eq!(status, CommandStatus::Executing as u16);
421        assert_eq!(ci.active_command(), Some(1));
422    }
423
424    #[test]
425    fn test_idle_command_ignored_when_idle() {
426        let mut ci = CommandInterface::new();
427        let (mut cmd, mut code, mut a1, mut a2, mut status, mut result) = make_view_fields();
428
429        // Init
430        {
431            let mut v = view!(cmd, code, a1, a2, status, result);
432            ci.call(&mut v);
433        }
434
435        // Command stays IDLE — nothing happens
436        {
437            let mut v = view!(cmd, code, a1, a2, status, result);
438            assert_eq!(ci.call(&mut v), None);
439        }
440        assert_eq!(status, CommandStatus::Idle as u16);
441    }
442}