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}