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}