Skip to main content

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// ============================================================================
209// Core Framework
210// ============================================================================
211
212/// Marker trait for generated GlobalMemory structs.
213///
214/// This trait is implemented by the auto-generated `GlobalMemory` struct
215/// that represents the shared memory layout. It serves as a marker for
216/// type safety in the control framework.
217///
218/// You don't need to implement this trait yourself - it's automatically
219/// implemented by the code generator.
220pub trait AutoCoreMemory {}
221
222/// Trait for detecting changes in memory structures.
223pub trait ChangeTracker {
224    /// Compare self with a previous state and return a list of changed fields.
225    /// Returns a vector of (field_name, new_value).
226    fn get_changes(&self, prev: &Self) -> Vec<(&'static str, serde_json::Value)>;
227}
228
229/// The trait that defines a control program's logic.
230///
231/// Implement this trait to create your control program. The associated `Memory`
232/// type should be the generated `GlobalMemory` struct from your project.
233///
234/// # Memory Type Requirements
235///
236/// The `Memory` type must implement `Copy` to allow efficient synchronization
237/// between shared memory and local buffers. This is automatically satisfied
238/// by the generated `GlobalMemory` struct.
239///
240/// # Lifecycle
241///
242/// 1. `initialize` is called once at startup
243/// 2. `process_tick` is called repeatedly in the control loop
244///
245/// # Example
246///
247/// ```ignore
248/// use autocore_std::ControlProgram;
249///
250/// mod gm;
251/// use gm::GlobalMemory;
252///
253/// pub struct MyController {
254///     cycle_counter: u64,
255/// }
256///
257/// impl MyController {
258///     pub fn new() -> Self {
259///         Self { cycle_counter: 0 }
260///     }
261/// }
262///
263/// impl ControlProgram for MyController {
264///     type Memory = GlobalMemory;
265///
266///     fn initialize(&mut self, mem: &mut GlobalMemory) {
267///         // Set initial output states
268///         mem.outputs.ready = true;
269///         log::info!("Controller initialized");
270///     }
271///
272///     fn process_tick(&mut self, mem: &mut GlobalMemory, cycle: u64) {
273///         self.cycle_counter = cycle;
274///
275///         // Your control logic here
276///         if mem.inputs.start && !mem.inputs.estop {
277///             mem.outputs.running = true;
278///         }
279///     }
280/// }
281/// ```
282pub trait ControlProgram {
283    /// The shared memory structure type (usually the generated `GlobalMemory`).
284    ///
285    /// Must implement `Copy` to allow efficient memory synchronization.
286    type Memory: Copy + ChangeTracker;
287
288    /// Called once when the control program starts.
289    ///
290    /// Use this to initialize output states, reset counters, or perform
291    /// any one-time setup. The default implementation does nothing.
292    ///
293    /// # Arguments
294    ///
295    /// * `mem` - Mutable reference to the shared memory. Changes are written
296    ///           back to shared memory after this method returns.
297    fn initialize(&mut self, _mem: &mut Self::Memory) {}
298
299    /// The main control loop - called once per scan cycle.
300    ///
301    /// This is where your control logic lives. Read inputs from `mem`,
302    /// perform calculations, and write outputs back to `mem`.
303    ///
304    /// # Arguments
305    ///
306    /// * `mem` - Mutable reference to a local copy of the shared memory.
307    ///           Changes made here are written back to shared memory after
308    ///           this method returns.
309    /// * `cycle` - The current cycle number (increments each tick, starting at 1).
310    ///
311    /// # Timing
312    ///
313    /// This method should complete within the scan cycle time. Long-running
314    /// operations will cause cycle overruns.
315    fn process_tick(&mut self, mem: &mut Self::Memory, cycle: u64);
316}
317
318/// Configuration for the [`ControlRunner`].
319///
320/// Specifies connection parameters, shared memory names, and logging settings.
321/// Use [`Default::default()`] for typical configurations.
322///
323/// # Example
324///
325/// ```
326/// use autocore_std::RunnerConfig;
327/// use log::LevelFilter;
328///
329/// let config = RunnerConfig {
330///     server_host: "192.168.1.100".to_string(),
331///     module_name: "my_controller".to_string(),
332///     shm_name: "my_project_shm".to_string(),
333///     tick_signal_name: "tick".to_string(),
334///     busy_signal_name: Some("busy".to_string()),
335///     log_level: LevelFilter::Debug,
336///     ..Default::default()
337/// };
338/// ```
339#[derive(Debug, Clone)]
340pub struct RunnerConfig {
341    /// Server host address (default: "127.0.0.1")
342    pub server_host: String,
343    /// WebSocket port for commands (default: 11969)
344    pub ws_port: u16,
345    /// Module name for identification (default: "control")
346    pub module_name: String,
347    /// Shared memory segment name (must match server configuration)
348    pub shm_name: String,
349    /// Name of the tick signal in shared memory (triggers each scan cycle)
350    pub tick_signal_name: String,
351    /// Optional name of the busy signal (set when cycle completes)
352    pub busy_signal_name: Option<String>,
353    /// Minimum log level to send to the server (default: Info)
354    pub log_level: LevelFilter,
355    /// UDP port for sending logs to the server (default: 39101)
356    pub log_udp_port: u16,
357}
358
359/// Default WebSocket port for autocore-server
360pub const DEFAULT_WS_PORT: u16 = 11969;
361
362impl Default for RunnerConfig {
363    fn default() -> Self {
364        Self {
365            server_host: "127.0.0.1".to_string(),
366            ws_port: DEFAULT_WS_PORT,
367            module_name: "control".to_string(),
368            shm_name: "autocore_cyclic".to_string(),
369            tick_signal_name: "tick".to_string(),
370            busy_signal_name: None,
371            log_level: LevelFilter::Info,
372            log_udp_port: logger::DEFAULT_LOG_UDP_PORT,
373        }
374    }
375}
376
377
378/// The main execution engine for control programs.
379///
380/// `ControlRunner` handles all the infrastructure required to run a control program:
381///
382/// - Reading memory layout from the server's layout file
383/// - Opening and mapping shared memory
384/// - Setting up synchronization signals
385/// - Running the real-time control loop
386/// - Sending log messages to the server
387///
388/// # Usage
389///
390/// ```ignore
391/// use autocore_std::{ControlRunner, RunnerConfig};
392///
393/// let config = RunnerConfig {
394///     shm_name: "my_project_shm".to_string(),
395///     tick_signal_name: "tick".to_string(),
396///     ..Default::default()
397/// };
398///
399/// ControlRunner::new(MyProgram::new())
400///     .config(config)
401///     .run()?;  // Blocks forever
402/// ```
403///
404/// # Control Loop
405///
406/// The runner executes a synchronous control loop:
407///
408/// 1. **Wait** - Blocks until the tick signal is set by the server
409/// 2. **Read** - Copies shared memory to a local buffer (acquire barrier)
410/// 3. **Execute** - Calls your `process_tick` method
411/// 4. **Write** - Copies local buffer back to shared memory (release barrier)
412/// 5. **Signal** - Sets the busy signal (if configured) to indicate completion
413///
414/// This ensures your code always sees a consistent snapshot of the data
415/// and that your writes are atomically visible to other processes.
416pub struct ControlRunner<P: ControlProgram> {
417    config: RunnerConfig,
418    program: P,
419}
420
421impl<P: ControlProgram> ControlRunner<P> {
422    /// Creates a new runner for the given control program.
423    ///
424    /// Uses default configuration. Call [`.config()`](Self::config) to customize.
425    ///
426    /// # Arguments
427    ///
428    /// * `program` - Your control program instance
429    ///
430    /// # Example
431    ///
432    /// ```ignore
433    /// let runner = ControlRunner::new(MyProgram::new());
434    /// ```
435    pub fn new(program: P) -> Self {
436        Self {
437            config: RunnerConfig::default(),
438            program,
439        }
440    }
441
442    /// Sets the configuration for this runner.
443    ///
444    /// # Arguments
445    ///
446    /// * `config` - The configuration to use
447    ///
448    /// # Example
449    ///
450    /// ```ignore
451    /// ControlRunner::new(MyProgram::new())
452    ///     .config(RunnerConfig {
453    ///         shm_name: "custom_shm".to_string(),
454    ///         ..Default::default()
455    ///     })
456    ///     .run()?;
457    /// ```
458    pub fn config(mut self, config: RunnerConfig) -> Self {
459        self.config = config;
460        self
461    }
462
463    /// Starts the control loop.
464    ///
465    /// This method blocks indefinitely, running the control loop until
466    /// an error occurs or the process is terminated.
467    ///
468    /// # Returns
469    ///
470    /// Returns `Ok(())` only if the loop exits cleanly (which typically
471    /// doesn't happen). Returns an error if:
472    ///
473    /// - IPC connection fails
474    /// - Shared memory cannot be opened
475    /// - Signal offsets cannot be found
476    /// - A critical error occurs during execution
477    ///
478    /// # Example
479    ///
480    /// ```ignore
481    /// fn main() -> anyhow::Result<()> {
482    ///     ControlRunner::new(MyProgram::new())
483    ///         .config(config)
484    ///         .run()
485    /// }
486    /// ```
487    pub fn run(mut self) -> Result<()> {
488        // Initialize UDP logger FIRST (before any log statements)
489        if let Err(e) = logger::init_udp_logger(
490            &self.config.server_host,
491            self.config.log_udp_port,
492            self.config.log_level,
493            "control",
494        ) {
495            eprintln!("Warning: Failed to initialize UDP logger: {}", e);
496            // Continue anyway - logging will just go nowhere
497        }
498
499        // We use a dedicated runtime for the setup phase
500        let rt = tokio::runtime::Builder::new_current_thread()
501            .enable_all()
502            .build()?;
503
504        rt.block_on(async {
505            log::info!("AutoCore Control Runner Starting...");
506
507            // 1. Connect to server via WebSocket and get layout
508            let ws_url = format!("ws://{}:{}/ws/", self.config.server_host, self.config.ws_port);
509            log::info!("Connecting to server at {}", ws_url);
510
511            let (ws_stream, _) = connect_async(&ws_url).await
512                .map_err(|e| anyhow!("Failed to connect to server at {}: {}", ws_url, e))?;
513
514            let (mut write, mut read) = ws_stream.split();
515
516            // Send gm.get_layout request
517            let request = CommandMessage::request("gm.get_layout", serde_json::Value::Null);
518            let transaction_id = request.transaction_id;
519            let request_json = serde_json::to_string(&request)?;
520
521            write.send(Message::Text(request_json)).await
522                .map_err(|e| anyhow!("Failed to send layout request: {}", e))?;
523
524            // Wait for response with matching transaction_id
525            let timeout = Duration::from_secs(10);
526            let start = std::time::Instant::now();
527            let mut layout: Option<HashMap<String, serde_json::Value>> = None;
528
529            while start.elapsed() < timeout {
530                match tokio::time::timeout(Duration::from_secs(1), read.next()).await {
531                    Ok(Some(Ok(Message::Text(text)))) => {
532                        if let Ok(response) = serde_json::from_str::<CommandMessage>(&text) {
533                            if response.transaction_id == transaction_id {
534                                if !response.success {
535                                    return Err(anyhow!("Server error: {}", response.error_message));
536                                }
537                                layout = Some(serde_json::from_value(response.data)?);
538                                break;
539                            }
540                            // Skip broadcasts and other messages
541                            if response.message_type == MessageType::Broadcast {
542                                continue;
543                            }
544                        }
545                    }
546                    Ok(Some(Ok(_))) => continue,
547                    Ok(Some(Err(e))) => return Err(anyhow!("WebSocket error: {}", e)),
548                    Ok(None) => return Err(anyhow!("Server closed connection")),
549                    Err(_) => continue, // Timeout on single read, keep trying
550                }
551            }
552
553            let layout = layout.ok_or_else(|| anyhow!("Timeout waiting for layout response"))?;
554            log::info!("Layout received with {} entries.", layout.len());
555
556            // We keep the WebSocket open for sending updates
557            // let _ = write.close().await;
558
559            // 2. Find Signal Offsets
560            let tick_offset = self.find_offset(&layout, &self.config.tick_signal_name)?;
561            let busy_offset = if let Some(name) = &self.config.busy_signal_name {
562                Some(self.find_offset(&layout, name)?)
563            } else {
564                None
565            };
566
567            // 4. Open Shared Memory
568            let shmem = ShmemConf::new().os_id(&self.config.shm_name).open()?;
569            let base_ptr = shmem.as_ptr();
570            log::info!("Shared Memory '{}' mapped.", self.config.shm_name);
571
572            // 5. Setup Pointers
573            // SAFETY: We trust the server's layout matches the generated GlobalMemory struct.
574            let gm = unsafe { &mut *(base_ptr as *mut P::Memory) };
575
576            // Get tick event from shared memory
577            log::info!("Setting up tick event at offset {} (base_ptr: {:p})", tick_offset, base_ptr);
578            let (tick_event, _) = unsafe {
579                Event::from_existing(base_ptr.add(tick_offset))
580            }.map_err(|e| anyhow!("Failed to open tick event: {:?}", e))?;
581            log::info!("Tick event ready");
582
583            // Busy signal event (optional)
584            let busy_event = busy_offset.map(|offset| {
585                unsafe { Event::from_existing(base_ptr.add(offset)) }
586                    .map(|(event, _)| event)
587                    .ok()
588            }).flatten();
589
590            // 6. Initialize local memory buffer and user program
591            // We use a local copy for the control loop to ensure:
592            // - Consistent snapshot of inputs at start of cycle
593            // - Atomic commit of outputs at end of cycle
594            // - Proper memory barriers for cross-process visibility
595            let mut local_mem: P::Memory = unsafe { std::ptr::read_volatile(gm) };
596            let mut prev_mem: P::Memory = local_mem; // Snapshot for change detection
597
598            fence(Ordering::Acquire); // Ensure we see all prior writes from other processes
599
600            self.program.initialize(&mut local_mem);
601
602            // Write back any changes from initialize
603            fence(Ordering::Release);
604            unsafe { std::ptr::write_volatile(gm, local_mem) };
605
606            // Set up signal handler for graceful shutdown
607            let running = Arc::new(AtomicBool::new(true));
608            let r = running.clone();
609            
610            // Only set handler if not already set
611            if let Err(e) = ctrlc::set_handler(move || {
612                r.store(false, Ordering::SeqCst);
613            }) {
614                log::warn!("Failed to set signal handler: {}", e);
615            }
616
617            log::info!("Entering Control Loop - waiting for first tick...");
618            let mut cycle_count: u64 = 0;
619
620            while running.load(Ordering::SeqCst) {
621                // Wait for Tick - Event-based synchronization
622                // Use a timeout (1s) to allow checking the running flag periodically
623                match tick_event.wait(Timeout::Val(Duration::from_secs(1))) {
624                    Ok(_) => {},
625                    Err(e) => {
626                        // Check for timeout
627                        let err_str = format!("{:?}", e);
628                        if err_str.contains("Timeout") {
629                            continue;
630                        }
631                        return Err(anyhow!("Tick wait failed: {:?}", e));
632                    }
633                }
634
635                if !running.load(Ordering::SeqCst) {
636                    log::info!("Shutdown signal received, exiting control loop.");
637                    break;
638                }
639
640                cycle_count += 1;
641                if cycle_count == 1 {
642                    log::info!("First tick received!");
643                }
644
645                // === INPUT PHASE ===
646                // Read all variables from shared memory into local buffer.
647                // This gives us a consistent snapshot of inputs for this cycle.
648                // Acquire fence ensures we see all writes from other processes (server, modules).
649                local_mem = unsafe { std::ptr::read_volatile(gm) };
650                
651                // Update prev_mem before execution to track changes made IN THIS CYCLE
652                // Actually, we want to know what changed in SHM relative to what we last knew,
653                // OR what WE changed relative to what we read?
654                // The user wants "writes on shared variables" to be broadcast.
655                // Typically outputs.
656                // If inputs changed (from other source), broadcasting them again is fine too.
657                // Let's capture state BEFORE execution (which is what we just read from SHM).
658                prev_mem = local_mem;
659
660                fence(Ordering::Acquire);
661
662                // === EXECUTE PHASE ===
663                // Execute user logic on the local copy.
664                // All reads/writes during process_tick operate on local_mem.
665                self.program.process_tick(&mut local_mem, cycle_count);
666
667                // === OUTPUT PHASE ===
668                // Write all variables from local buffer back to shared memory.
669                // Release fence ensures our writes are visible to other processes.
670                fence(Ordering::Release);
671                unsafe { std::ptr::write_volatile(gm, local_mem) };
672
673                // === CHANGE DETECTION & NOTIFICATION ===
674                let changes = local_mem.get_changes(&prev_mem);
675                if !changes.is_empty() {
676                    // Construct bulk write message
677                    let mut data_map = serde_json::Map::new();
678                    for (key, val) in changes {
679                        data_map.insert(key.to_string(), val);
680                    }
681                    
682                    let msg = CommandMessage::request("gm.write", serde_json::Value::Object(data_map));
683                    let msg_json = serde_json::to_string(&msg).unwrap_or_default();
684                    
685                    // Send via WebSocket (fire and forget, don't block)
686                    // Note: WebSocket send is async. We are in block_on.
687                    // We can await it. If it takes too long, it might delay the cycle.
688                    // Ideally we should spawn this? But spawn requires 'static or Arc.
689                    // For now, let's await with a very short timeout or just await.
690                    // write is Sink.
691                    if let Err(e) = write.send(Message::Text(msg_json)).await {
692                        log::error!("Failed to send updates: {}", e);
693                    }
694                }
695
696                // Signal Busy/Done event
697                if let Some(ref busy_ev) = busy_event {
698                    let _ = busy_ev.set(EventState::Signaled);
699                }
700            }
701
702            Ok(())
703        })
704    }
705
706    fn find_offset(&self, layout: &HashMap<String, serde_json::Value>, name: &str) -> Result<usize> {
707        let info = layout.get(name).ok_or_else(|| anyhow!("Signal '{}' not found in layout", name))?;
708        info.get("offset")
709            .and_then(|v| v.as_u64())
710            .map(|v| v as usize)
711            .ok_or_else(|| anyhow!("Invalid offset for '{}'", name))
712    }
713}
714
715/// Generates the standard `main` function for a control program.
716///
717/// This macro reduces boilerplate by creating a properly configured `main`
718/// function that initializes and runs your control program.
719///
720/// # Arguments
721///
722/// * `$prog_type` - The type of your control program (must implement [`ControlProgram`])
723/// * `$shm_name` - The shared memory segment name (string literal)
724/// * `$tick_signal` - The tick signal name in shared memory (string literal)
725///
726/// # Example
727///
728/// ```ignore
729/// mod gm;
730/// use gm::GlobalMemory;
731///
732/// pub struct MyProgram;
733///
734/// impl MyProgram {
735///     pub fn new() -> Self { Self }
736/// }
737///
738/// impl autocore_std::ControlProgram for MyProgram {
739///     type Memory = GlobalMemory;
740///
741///     fn process_tick(&mut self, mem: &mut GlobalMemory, _cycle: u64) {
742///         // Your logic here
743///     }
744/// }
745///
746/// // This generates the main function
747/// autocore_std::autocore_main!(MyProgram, "my_project_shm", "tick");
748/// ```
749///
750/// # Generated Code
751///
752/// The macro expands to:
753///
754/// ```ignore
755/// fn main() -> anyhow::Result<()> {
756///     let config = autocore_std::RunnerConfig {
757///         server_host: "127.0.0.1".to_string(),
758///         ws_port: autocore_std::DEFAULT_WS_PORT,
759///         module_name: "control".to_string(),
760///         shm_name: "my_project_shm".to_string(),
761///         tick_signal_name: "tick".to_string(),
762///         busy_signal_name: None,
763///         log_level: log::LevelFilter::Info,
764///         log_udp_port: autocore_std::logger::DEFAULT_LOG_UDP_PORT,
765///     };
766///
767///     autocore_std::ControlRunner::new(MyProgram::new())
768///         .config(config)
769///         .run()
770/// }
771/// ```
772#[macro_export]
773macro_rules! autocore_main {
774    ($prog_type:ty, $shm_name:expr, $tick_signal:expr) => {
775        fn main() -> anyhow::Result<()> {
776            let config = autocore_std::RunnerConfig {
777                server_host: "127.0.0.1".to_string(),
778                ws_port: autocore_std::DEFAULT_WS_PORT,
779                module_name: "control".to_string(),
780                shm_name: $shm_name.to_string(),
781                tick_signal_name: $tick_signal.to_string(),
782                busy_signal_name: None,
783                log_level: log::LevelFilter::Info,
784                log_udp_port: autocore_std::logger::DEFAULT_LOG_UDP_PORT,
785            };
786
787            autocore_std::ControlRunner::new(<$prog_type>::new())
788                .config(config)
789                .run()
790        }
791    };
792}
793