autocore_std/lib.rs
1//! # AutoCore Standard Library
2//!
3//! The standard library for writing AutoCore control programs. This crate provides
4//! everything you need to build real-time control applications that integrate with
5//! the AutoCore server ecosystem.
6//!
7//! ## Overview
8//!
9//! AutoCore control programs run as separate processes that communicate with the
10//! autocore-server via shared memory and IPC. This library handles all the low-level
11//! details, allowing you to focus on your control logic.
12//!
13//! ```text
14//! ┌─────────────────────────┐ ┌─────────────────────────┐
15//! │ autocore-server │ │ Your Control Program │
16//! │ │ │ │
17//! │ ┌─────────────────┐ │ │ ┌─────────────────┐ │
18//! │ │ Shared Memory │◄───┼─────┼──│ ControlRunner │ │
19//! │ │ (GlobalMemory) │ │ │ │ │ │
20//! │ └─────────────────┘ │ │ │ ┌─────────────┐ │ │
21//! │ │ │ │ │ Your Logic │ │ │
22//! │ ┌─────────────────┐ │ │ │ └─────────────┘ │ │
23//! │ │ Tick Signal │────┼─────┼──│ │ │
24//! │ └─────────────────┘ │ │ └─────────────────┘ │
25//! └─────────────────────────┘ └─────────────────────────┘
26//! ```
27//!
28//! ## Quick Start
29//!
30//! 1. Create a new control project using `acctl`:
31//! ```bash
32//! acctl clone <server-ip> <project-name>
33//! ```
34//!
35//! 2. Implement the [`ControlProgram`] trait:
36//! ```ignore
37//! use autocore_std::ControlProgram;
38//! use autocore_std::fb::RTrig;
39//!
40//! // GlobalMemory is generated from your project.json
41//! mod gm;
42//! use gm::GlobalMemory;
43//!
44//! pub struct MyProgram {
45//! start_button: RTrig,
46//! }
47//!
48//! impl MyProgram {
49//! pub fn new() -> Self {
50//! Self {
51//! start_button: RTrig::new(),
52//! }
53//! }
54//! }
55//!
56//! impl ControlProgram for MyProgram {
57//! type Memory = GlobalMemory;
58//!
59//! fn process_tick(&mut self, mem: &mut GlobalMemory, _cycle: u64) {
60//! // Detect rising edge on start button
61//! if self.start_button.call(mem.inputs.start_button) {
62//! mem.outputs.motor_running = true;
63//! autocore_std::log::info!("Motor started!");
64//! }
65//! }
66//! }
67//! ```
68//!
69//! 3. Use the [`autocore_main!`] macro for the entry point:
70//! ```ignore
71//! autocore_std::autocore_main!(MyProgram, "my_project_shm", "tick");
72//! ```
73//!
74//! ## Function Blocks (IEC 61131-3 Inspired)
75//!
76//! This library includes standard function blocks commonly used in PLC programming:
77//!
78//! - [`fb::RTrig`] - Rising edge detector (false→true transition)
79//! - [`fb::FTrig`] - Falling edge detector (true→false transition)
80//! - [`fb::Ton`] - Timer On Delay (output after delay)
81//! - [`fb::BitResetOnDelay`] - Resets a boolean after it has been true for a duration
82//! - [`fb::SimpleTimer`] - Simple one-shot timer (NOT IEC 61131-3, for imperative use)
83//! - [`fb::StateMachine`] - State machine helper with automatic timer management
84//! - [`fb::RunningAverage`] - Accumulates values and computes their arithmetic mean
85//! - [`fb::Beeper`] - Audible beeper controller with configurable beep sequences
86//! - [`fb::Heartbeat`] - Monitors a remote heartbeat counter for connection loss
87//!
88//! ### Example: Edge Detection
89//!
90//! ```
91//! use autocore_std::fb::RTrig;
92//!
93//! let mut trigger = RTrig::new();
94//!
95//! // First call with false - no edge
96//! assert_eq!(trigger.call(false), false);
97//!
98//! // Rising edge detected!
99//! assert_eq!(trigger.call(true), true);
100//!
101//! // Still true, but no edge (already high)
102//! assert_eq!(trigger.call(true), false);
103//!
104//! // Back to false
105//! assert_eq!(trigger.call(false), false);
106//!
107//! // Another rising edge
108//! assert_eq!(trigger.call(true), true);
109//! ```
110//!
111//! ### Example: Timer
112//!
113//! ```
114//! use autocore_std::fb::Ton;
115//! use std::time::Duration;
116//!
117//! let mut timer = Ton::new();
118//! let delay = Duration::from_millis(100);
119//!
120//! // Timer not enabled - output is false
121//! assert_eq!(timer.call(false, delay), false);
122//!
123//! // Enable timer - starts counting
124//! assert_eq!(timer.call(true, delay), false);
125//!
126//! // Still counting...
127//! std::thread::sleep(Duration::from_millis(50));
128//! assert_eq!(timer.call(true, delay), false);
129//! assert!(timer.et < delay); // Elapsed time < preset
130//!
131//! // After delay elapsed
132//! std::thread::sleep(Duration::from_millis(60));
133//! assert_eq!(timer.call(true, delay), true); // Output is now true!
134//! ```
135//!
136//! ## Logging
137//!
138//! Control programs can send log messages to the autocore-server for display in the
139//! web console. Logging is handled automatically when using [`ControlRunner`].
140//!
141//! ```ignore
142//! use autocore_std::log;
143//!
144//! log::trace!("Detailed trace message");
145//! log::debug!("Debug information");
146//! log::info!("Normal operation message");
147//! log::warn!("Warning condition detected");
148//! log::error!("Error occurred!");
149//! ```
150//!
151//! See the [`logger`] module for advanced configuration.
152//!
153//! ## Memory Synchronization
154//!
155//! The [`ControlRunner`] handles all shared memory synchronization automatically:
156//!
157//! 1. **Wait for tick** - Blocks until the server signals a new cycle
158//! 2. **Read inputs** - Copies shared memory to local buffer (atomic snapshot)
159//! 3. **Execute logic** - Your `process_tick` runs on the local buffer
160//! 4. **Write outputs** - Copies local buffer back to shared memory
161//!
162//! This ensures your control logic always sees a consistent view of the data,
163//! even when other processes are modifying shared memory.
164
165#![warn(missing_docs)]
166#![warn(rustdoc::missing_crate_level_docs)]
167#![doc(html_root_url = "https://docs.rs/autocore-std/3.3.0")]
168
169use anyhow::{anyhow, Result};
170use futures_util::{SinkExt, StreamExt};
171use log::LevelFilter;
172use mechutil::ipc::{CommandMessage, MessageType};
173use raw_sync::events::{Event, EventInit, EventState};
174use raw_sync::Timeout;
175use shared_memory::ShmemConf;
176use std::collections::HashMap;
177use std::sync::atomic::{fence, Ordering, AtomicBool};
178use std::sync::Arc;
179use std::time::Duration;
180use tokio_tungstenite::{connect_async, tungstenite::Message};
181
182/// UDP logger for sending log messages to autocore-server.
183///
184/// This module provides a non-blocking logger implementation that sends log messages
185/// via UDP to the autocore-server. Messages are batched and sent asynchronously to
186/// avoid impacting the control loop timing.
187///
188/// # Example
189///
190/// ```ignore
191/// use autocore_std::logger;
192/// use log::LevelFilter;
193///
194/// // Initialize the logger (done automatically by ControlRunner)
195/// logger::init_udp_logger("127.0.0.1", 39101, LevelFilter::Info, "control")?;
196///
197/// // Now you can use the log macros
198/// log::info!("System initialized");
199/// ```
200pub mod logger;
201
202// Re-export log crate for convenience - control programs can use autocore_std::log::info!() etc.
203pub use log;
204
205/// Function blocks for control programs (IEC 61131-3 inspired).
206pub mod fb;
207
208/// Interface protocols for communication between control programs and external sources.
209pub mod iface;
210
211// ============================================================================
212// Core Framework
213// ============================================================================
214
215/// Marker trait for generated GlobalMemory structs.
216///
217/// This trait is implemented by the auto-generated `GlobalMemory` struct
218/// that represents the shared memory layout. It serves as a marker for
219/// type safety in the control framework.
220///
221/// You don't need to implement this trait yourself - it's automatically
222/// implemented by the code generator.
223pub trait AutoCoreMemory {}
224
225/// Trait for detecting changes in memory structures.
226pub trait ChangeTracker {
227 /// Compare self with a previous state and return a list of changed fields.
228 /// Returns a vector of (field_name, new_value).
229 fn get_changes(&self, prev: &Self) -> Vec<(&'static str, serde_json::Value)>;
230}
231
232/// The trait that defines a control program's logic.
233///
234/// Implement this trait to create your control program. The associated `Memory`
235/// type should be the generated `GlobalMemory` struct from your project.
236///
237/// # Memory Type Requirements
238///
239/// The `Memory` type must implement `Copy` to allow efficient synchronization
240/// between shared memory and local buffers. This is automatically satisfied
241/// by the generated `GlobalMemory` struct.
242///
243/// # Lifecycle
244///
245/// 1. `initialize` is called once at startup
246/// 2. `process_tick` is called repeatedly in the control loop
247///
248/// # Example
249///
250/// ```ignore
251/// use autocore_std::ControlProgram;
252///
253/// mod gm;
254/// use gm::GlobalMemory;
255///
256/// pub struct MyController {
257/// cycle_counter: u64,
258/// }
259///
260/// impl MyController {
261/// pub fn new() -> Self {
262/// Self { cycle_counter: 0 }
263/// }
264/// }
265///
266/// impl ControlProgram for MyController {
267/// type Memory = GlobalMemory;
268///
269/// fn initialize(&mut self, mem: &mut GlobalMemory) {
270/// // Set initial output states
271/// mem.outputs.ready = true;
272/// log::info!("Controller initialized");
273/// }
274///
275/// fn process_tick(&mut self, mem: &mut GlobalMemory, cycle: u64) {
276/// self.cycle_counter = cycle;
277///
278/// // Your control logic here
279/// if mem.inputs.start && !mem.inputs.estop {
280/// mem.outputs.running = true;
281/// }
282/// }
283/// }
284/// ```
285pub trait ControlProgram {
286 /// The shared memory structure type (usually the generated `GlobalMemory`).
287 ///
288 /// Must implement `Copy` to allow efficient memory synchronization.
289 type Memory: Copy + ChangeTracker;
290
291 /// Called once when the control program starts.
292 ///
293 /// Use this to initialize output states, reset counters, or perform
294 /// any one-time setup. The default implementation does nothing.
295 ///
296 /// # Arguments
297 ///
298 /// * `mem` - Mutable reference to the shared memory. Changes are written
299 /// back to shared memory after this method returns.
300 fn initialize(&mut self, _mem: &mut Self::Memory) {}
301
302 /// The main control loop - called once per scan cycle.
303 ///
304 /// This is where your control logic lives. Read inputs from `mem`,
305 /// perform calculations, and write outputs back to `mem`.
306 ///
307 /// # Arguments
308 ///
309 /// * `mem` - Mutable reference to a local copy of the shared memory.
310 /// Changes made here are written back to shared memory after
311 /// this method returns.
312 /// * `cycle` - The current cycle number (increments each tick, starting at 1).
313 ///
314 /// # Timing
315 ///
316 /// This method should complete within the scan cycle time. Long-running
317 /// operations will cause cycle overruns.
318 fn process_tick(&mut self, mem: &mut Self::Memory, cycle: u64);
319}
320
321/// Configuration for the [`ControlRunner`].
322///
323/// Specifies connection parameters, shared memory names, and logging settings.
324/// Use [`Default::default()`] for typical configurations.
325///
326/// # Example
327///
328/// ```
329/// use autocore_std::RunnerConfig;
330/// use log::LevelFilter;
331///
332/// let config = RunnerConfig {
333/// server_host: "192.168.1.100".to_string(),
334/// module_name: "my_controller".to_string(),
335/// shm_name: "my_project_shm".to_string(),
336/// tick_signal_name: "tick".to_string(),
337/// busy_signal_name: Some("busy".to_string()),
338/// log_level: LevelFilter::Debug,
339/// ..Default::default()
340/// };
341/// ```
342#[derive(Debug, Clone)]
343pub struct RunnerConfig {
344 /// Server host address (default: "127.0.0.1")
345 pub server_host: String,
346 /// WebSocket port for commands (default: 11969)
347 pub ws_port: u16,
348 /// Module name for identification (default: "control")
349 pub module_name: String,
350 /// Shared memory segment name (must match server configuration)
351 pub shm_name: String,
352 /// Name of the tick signal in shared memory (triggers each scan cycle)
353 pub tick_signal_name: String,
354 /// Optional name of the busy signal (set when cycle completes)
355 pub busy_signal_name: Option<String>,
356 /// Minimum log level to send to the server (default: Info)
357 pub log_level: LevelFilter,
358 /// UDP port for sending logs to the server (default: 39101)
359 pub log_udp_port: u16,
360}
361
362/// Default WebSocket port for autocore-server
363pub const DEFAULT_WS_PORT: u16 = 11969;
364
365impl Default for RunnerConfig {
366 fn default() -> Self {
367 Self {
368 server_host: "127.0.0.1".to_string(),
369 ws_port: DEFAULT_WS_PORT,
370 module_name: "control".to_string(),
371 shm_name: "autocore_cyclic".to_string(),
372 tick_signal_name: "tick".to_string(),
373 busy_signal_name: None,
374 log_level: LevelFilter::Info,
375 log_udp_port: logger::DEFAULT_LOG_UDP_PORT,
376 }
377 }
378}
379
380
381/// The main execution engine for control programs.
382///
383/// `ControlRunner` handles all the infrastructure required to run a control program:
384///
385/// - Reading memory layout from the server's layout file
386/// - Opening and mapping shared memory
387/// - Setting up synchronization signals
388/// - Running the real-time control loop
389/// - Sending log messages to the server
390///
391/// # Usage
392///
393/// ```ignore
394/// use autocore_std::{ControlRunner, RunnerConfig};
395///
396/// let config = RunnerConfig {
397/// shm_name: "my_project_shm".to_string(),
398/// tick_signal_name: "tick".to_string(),
399/// ..Default::default()
400/// };
401///
402/// ControlRunner::new(MyProgram::new())
403/// .config(config)
404/// .run()?; // Blocks forever
405/// ```
406///
407/// # Control Loop
408///
409/// The runner executes a synchronous control loop:
410///
411/// 1. **Wait** - Blocks until the tick signal is set by the server
412/// 2. **Read** - Copies shared memory to a local buffer (acquire barrier)
413/// 3. **Execute** - Calls your `process_tick` method
414/// 4. **Write** - Copies local buffer back to shared memory (release barrier)
415/// 5. **Signal** - Sets the busy signal (if configured) to indicate completion
416///
417/// This ensures your code always sees a consistent snapshot of the data
418/// and that your writes are atomically visible to other processes.
419pub struct ControlRunner<P: ControlProgram> {
420 config: RunnerConfig,
421 program: P,
422}
423
424impl<P: ControlProgram> ControlRunner<P> {
425 /// Creates a new runner for the given control program.
426 ///
427 /// Uses default configuration. Call [`.config()`](Self::config) to customize.
428 ///
429 /// # Arguments
430 ///
431 /// * `program` - Your control program instance
432 ///
433 /// # Example
434 ///
435 /// ```ignore
436 /// let runner = ControlRunner::new(MyProgram::new());
437 /// ```
438 pub fn new(program: P) -> Self {
439 Self {
440 config: RunnerConfig::default(),
441 program,
442 }
443 }
444
445 /// Sets the configuration for this runner.
446 ///
447 /// # Arguments
448 ///
449 /// * `config` - The configuration to use
450 ///
451 /// # Example
452 ///
453 /// ```ignore
454 /// ControlRunner::new(MyProgram::new())
455 /// .config(RunnerConfig {
456 /// shm_name: "custom_shm".to_string(),
457 /// ..Default::default()
458 /// })
459 /// .run()?;
460 /// ```
461 pub fn config(mut self, config: RunnerConfig) -> Self {
462 self.config = config;
463 self
464 }
465
466 /// Starts the control loop.
467 ///
468 /// This method blocks indefinitely, running the control loop until
469 /// an error occurs or the process is terminated.
470 ///
471 /// # Returns
472 ///
473 /// Returns `Ok(())` only if the loop exits cleanly (which typically
474 /// doesn't happen). Returns an error if:
475 ///
476 /// - IPC connection fails
477 /// - Shared memory cannot be opened
478 /// - Signal offsets cannot be found
479 /// - A critical error occurs during execution
480 ///
481 /// # Example
482 ///
483 /// ```ignore
484 /// fn main() -> anyhow::Result<()> {
485 /// ControlRunner::new(MyProgram::new())
486 /// .config(config)
487 /// .run()
488 /// }
489 /// ```
490 pub fn run(mut self) -> Result<()> {
491 // Initialize UDP logger FIRST (before any log statements)
492 if let Err(e) = logger::init_udp_logger(
493 &self.config.server_host,
494 self.config.log_udp_port,
495 self.config.log_level,
496 "control",
497 ) {
498 eprintln!("Warning: Failed to initialize UDP logger: {}", e);
499 // Continue anyway - logging will just go nowhere
500 }
501
502 // We use a dedicated runtime for the setup phase
503 let rt = tokio::runtime::Builder::new_current_thread()
504 .enable_all()
505 .build()?;
506
507 rt.block_on(async {
508 log::info!("AutoCore Control Runner Starting...");
509
510 // 1. Connect to server via WebSocket and get layout
511 let ws_url = format!("ws://{}:{}/ws/", self.config.server_host, self.config.ws_port);
512 log::info!("Connecting to server at {}", ws_url);
513
514 let (ws_stream, _) = connect_async(&ws_url).await
515 .map_err(|e| anyhow!("Failed to connect to server at {}: {}", ws_url, e))?;
516
517 let (mut write, mut read) = ws_stream.split();
518
519 // Send gm.get_layout request
520 let request = CommandMessage::request("gm.get_layout", serde_json::Value::Null);
521 let transaction_id = request.transaction_id;
522 let request_json = serde_json::to_string(&request)?;
523
524 write.send(Message::Text(request_json)).await
525 .map_err(|e| anyhow!("Failed to send layout request: {}", e))?;
526
527 // Wait for response with matching transaction_id
528 let timeout = Duration::from_secs(10);
529 let start = std::time::Instant::now();
530 let mut layout: Option<HashMap<String, serde_json::Value>> = None;
531
532 while start.elapsed() < timeout {
533 match tokio::time::timeout(Duration::from_secs(1), read.next()).await {
534 Ok(Some(Ok(Message::Text(text)))) => {
535 if let Ok(response) = serde_json::from_str::<CommandMessage>(&text) {
536 if response.transaction_id == transaction_id {
537 if !response.success {
538 return Err(anyhow!("Server error: {}", response.error_message));
539 }
540 layout = Some(serde_json::from_value(response.data)?);
541 break;
542 }
543 // Skip broadcasts and other messages
544 if response.message_type == MessageType::Broadcast {
545 continue;
546 }
547 }
548 }
549 Ok(Some(Ok(_))) => continue,
550 Ok(Some(Err(e))) => return Err(anyhow!("WebSocket error: {}", e)),
551 Ok(None) => return Err(anyhow!("Server closed connection")),
552 Err(_) => continue, // Timeout on single read, keep trying
553 }
554 }
555
556 let layout = layout.ok_or_else(|| anyhow!("Timeout waiting for layout response"))?;
557 log::info!("Layout received with {} entries.", layout.len());
558
559 // We keep the WebSocket open for sending updates
560 // let _ = write.close().await;
561
562 // 2. Find Signal Offsets
563 let tick_offset = self.find_offset(&layout, &self.config.tick_signal_name)?;
564 let busy_offset = if let Some(name) = &self.config.busy_signal_name {
565 Some(self.find_offset(&layout, name)?)
566 } else {
567 None
568 };
569
570 // 4. Open Shared Memory
571 let shmem = ShmemConf::new().os_id(&self.config.shm_name).open()?;
572 let base_ptr = shmem.as_ptr();
573 log::info!("Shared Memory '{}' mapped.", self.config.shm_name);
574
575 // 5. Setup Pointers
576 // SAFETY: We trust the server's layout matches the generated GlobalMemory struct.
577 let gm = unsafe { &mut *(base_ptr as *mut P::Memory) };
578
579 // Get tick event from shared memory
580 log::info!("Setting up tick event at offset {} (base_ptr: {:p})", tick_offset, base_ptr);
581 let (tick_event, _) = unsafe {
582 Event::from_existing(base_ptr.add(tick_offset))
583 }.map_err(|e| anyhow!("Failed to open tick event: {:?}", e))?;
584 log::info!("Tick event ready");
585
586 // Busy signal event (optional)
587 let busy_event = busy_offset.map(|offset| {
588 unsafe { Event::from_existing(base_ptr.add(offset)) }
589 .map(|(event, _)| event)
590 .ok()
591 }).flatten();
592
593 // 6. Initialize local memory buffer and user program
594 // We use a local copy for the control loop to ensure:
595 // - Consistent snapshot of inputs at start of cycle
596 // - Atomic commit of outputs at end of cycle
597 // - Proper memory barriers for cross-process visibility
598 let mut local_mem: P::Memory = unsafe { std::ptr::read_volatile(gm) };
599 let mut prev_mem: P::Memory = local_mem; // Snapshot for change detection
600
601 fence(Ordering::Acquire); // Ensure we see all prior writes from other processes
602
603 self.program.initialize(&mut local_mem);
604
605 // Write back any changes from initialize
606 fence(Ordering::Release);
607 unsafe { std::ptr::write_volatile(gm, local_mem) };
608
609 // Set up signal handler for graceful shutdown
610 let running = Arc::new(AtomicBool::new(true));
611 let r = running.clone();
612
613 // Only set handler if not already set
614 if let Err(e) = ctrlc::set_handler(move || {
615 r.store(false, Ordering::SeqCst);
616 }) {
617 log::warn!("Failed to set signal handler: {}", e);
618 }
619
620 log::info!("Entering Control Loop - waiting for first tick...");
621 let mut cycle_count: u64 = 0;
622
623 while running.load(Ordering::SeqCst) {
624 // Wait for Tick - Event-based synchronization
625 // Use a timeout (1s) to allow checking the running flag periodically
626 match tick_event.wait(Timeout::Val(Duration::from_secs(1))) {
627 Ok(_) => {},
628 Err(e) => {
629 // Check for timeout
630 let err_str = format!("{:?}", e);
631 if err_str.contains("Timeout") {
632 continue;
633 }
634 return Err(anyhow!("Tick wait failed: {:?}", e));
635 }
636 }
637
638 if !running.load(Ordering::SeqCst) {
639 log::info!("Shutdown signal received, exiting control loop.");
640 break;
641 }
642
643 cycle_count += 1;
644 if cycle_count == 1 {
645 log::info!("First tick received!");
646 }
647
648 // === INPUT PHASE ===
649 // Read all variables from shared memory into local buffer.
650 // This gives us a consistent snapshot of inputs for this cycle.
651 // Acquire fence ensures we see all writes from other processes (server, modules).
652 local_mem = unsafe { std::ptr::read_volatile(gm) };
653
654 // Update prev_mem before execution to track changes made IN THIS CYCLE
655 // Actually, we want to know what changed in SHM relative to what we last knew,
656 // OR what WE changed relative to what we read?
657 // The user wants "writes on shared variables" to be broadcast.
658 // Typically outputs.
659 // If inputs changed (from other source), broadcasting them again is fine too.
660 // Let's capture state BEFORE execution (which is what we just read from SHM).
661 prev_mem = local_mem;
662
663 fence(Ordering::Acquire);
664
665 // === EXECUTE PHASE ===
666 // Execute user logic on the local copy.
667 // All reads/writes during process_tick operate on local_mem.
668 self.program.process_tick(&mut local_mem, cycle_count);
669
670 // === OUTPUT PHASE ===
671 // Write all variables from local buffer back to shared memory.
672 // Release fence ensures our writes are visible to other processes.
673 fence(Ordering::Release);
674 unsafe { std::ptr::write_volatile(gm, local_mem) };
675
676 // === CHANGE DETECTION & NOTIFICATION ===
677 let changes = local_mem.get_changes(&prev_mem);
678 if !changes.is_empty() {
679 // Construct bulk write message
680 let mut data_map = serde_json::Map::new();
681 for (key, val) in changes {
682 data_map.insert(key.to_string(), val);
683 }
684
685 let msg = CommandMessage::request("gm.write", serde_json::Value::Object(data_map));
686 let msg_json = serde_json::to_string(&msg).unwrap_or_default();
687
688 // Send via WebSocket (fire and forget, don't block)
689 // Note: WebSocket send is async. We are in block_on.
690 // We can await it. If it takes too long, it might delay the cycle.
691 // Ideally we should spawn this? But spawn requires 'static or Arc.
692 // For now, let's await with a very short timeout or just await.
693 // write is Sink.
694 if let Err(e) = write.send(Message::Text(msg_json)).await {
695 log::error!("Failed to send updates: {}", e);
696 }
697 }
698
699 // Signal Busy/Done event
700 if let Some(ref busy_ev) = busy_event {
701 let _ = busy_ev.set(EventState::Signaled);
702 }
703 }
704
705 Ok(())
706 })
707 }
708
709 fn find_offset(&self, layout: &HashMap<String, serde_json::Value>, name: &str) -> Result<usize> {
710 let info = layout.get(name).ok_or_else(|| anyhow!("Signal '{}' not found in layout", name))?;
711 info.get("offset")
712 .and_then(|v| v.as_u64())
713 .map(|v| v as usize)
714 .ok_or_else(|| anyhow!("Invalid offset for '{}'", name))
715 }
716}
717
718/// Generates the standard `main` function for a control program.
719///
720/// This macro reduces boilerplate by creating a properly configured `main`
721/// function that initializes and runs your control program.
722///
723/// # Arguments
724///
725/// * `$prog_type` - The type of your control program (must implement [`ControlProgram`])
726/// * `$shm_name` - The shared memory segment name (string literal)
727/// * `$tick_signal` - The tick signal name in shared memory (string literal)
728///
729/// # Example
730///
731/// ```ignore
732/// mod gm;
733/// use gm::GlobalMemory;
734///
735/// pub struct MyProgram;
736///
737/// impl MyProgram {
738/// pub fn new() -> Self { Self }
739/// }
740///
741/// impl autocore_std::ControlProgram for MyProgram {
742/// type Memory = GlobalMemory;
743///
744/// fn process_tick(&mut self, mem: &mut GlobalMemory, _cycle: u64) {
745/// // Your logic here
746/// }
747/// }
748///
749/// // This generates the main function
750/// autocore_std::autocore_main!(MyProgram, "my_project_shm", "tick");
751/// ```
752///
753/// # Generated Code
754///
755/// The macro expands to:
756///
757/// ```ignore
758/// fn main() -> anyhow::Result<()> {
759/// let config = autocore_std::RunnerConfig {
760/// server_host: "127.0.0.1".to_string(),
761/// ws_port: autocore_std::DEFAULT_WS_PORT,
762/// module_name: "control".to_string(),
763/// shm_name: "my_project_shm".to_string(),
764/// tick_signal_name: "tick".to_string(),
765/// busy_signal_name: None,
766/// log_level: log::LevelFilter::Info,
767/// log_udp_port: autocore_std::logger::DEFAULT_LOG_UDP_PORT,
768/// };
769///
770/// autocore_std::ControlRunner::new(MyProgram::new())
771/// .config(config)
772/// .run()
773/// }
774/// ```
775#[macro_export]
776macro_rules! autocore_main {
777 ($prog_type:ty, $shm_name:expr, $tick_signal:expr) => {
778 fn main() -> anyhow::Result<()> {
779 let config = autocore_std::RunnerConfig {
780 server_host: "127.0.0.1".to_string(),
781 ws_port: autocore_std::DEFAULT_WS_PORT,
782 module_name: "control".to_string(),
783 shm_name: $shm_name.to_string(),
784 tick_signal_name: $tick_signal.to_string(),
785 busy_signal_name: None,
786 log_level: log::LevelFilter::Info,
787 log_udp_port: autocore_std::logger::DEFAULT_LOG_UDP_PORT,
788 };
789
790 autocore_std::ControlRunner::new(<$prog_type>::new())
791 .config(config)
792 .run()
793 }
794 };
795}
796