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, TickContext};
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, ctx: &mut TickContext<Self::Memory>) {
60//!            // Detect rising edge on start button
61//!            if self.start_button.call(ctx.gm.inputs.start_button) {
62//!                ctx.gm.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// Re-export `paste` so the exported view macros (e.g. `wls15_run_mode_view!`,
206// `el3356_view!`) can reference it via `$crate::paste` and work in any consumer
207// crate without that crate having to depend on `paste` itself.
208#[doc(hidden)]
209pub use paste;
210
211/// Function blocks for control programs (IEC 61131-3 inspired).
212pub mod fb;
213
214/// Interface protocols for communication between control programs and external sources.
215pub mod iface;
216
217/// Client for sending IPC commands to external modules via WebSocket.
218pub mod command_client;
219pub use command_client::CommandClient;
220
221/// Subscription helper for `ams.asset_updated.<location>` broadcasts.
222/// Control programs use this to drive EL3356 SDO writes (and similar)
223/// when a load cell asset is registered, recalibrated, or retired.
224pub mod asset_watch;
225pub use asset_watch::{AssetUpdate, AssetWatch, AssetWatchStatus, AssetWatchTrigger};
226
227/// EtherCAT utilities (SDO client, etc.).
228pub mod ethercat;
229
230/// CiA 402 motion control: axis abstraction, traits, and types.
231pub mod motion;
232
233/// Shared memory utilities for external modules.
234pub mod shm;
235
236/// Lightweight process diagnostics (FD count, RSS).
237pub mod diagnostics;
238
239/// Banner Engineering device helpers (WLS15 IO-Link light strip, etc.).
240pub mod banner;
241
242/// Fixed-length string type for shared memory variables.
243pub mod fixed_string;
244pub use fixed_string::FixedString;
245
246// ============================================================================
247// Core Framework
248// ============================================================================
249
250/// Marker trait for generated GlobalMemory structs.
251///
252/// This trait is implemented by the auto-generated `GlobalMemory` struct
253/// that represents the shared memory layout. It serves as a marker for
254/// type safety in the control framework.
255///
256/// You don't need to implement this trait yourself - it's automatically
257/// implemented by the code generator.
258pub trait AutoCoreMemory {}
259
260/// Trait for detecting changes in memory structures.
261pub trait ChangeTracker {
262    /// Compare self with a previous state and return a list of changed fields.
263    /// Returns a vector of (field_name, new_value).
264    fn get_changes(&self, prev: &Self) -> Vec<(&'static str, serde_json::Value)>;
265
266    /// Unpack bit-mapped variables from their source words.
267    /// Called automatically after reading shared memory, before `process_tick`.
268    /// Auto-generated by codegen when bit-mapped variables exist; default is no-op.
269    fn unpack_bits(&mut self) {}
270
271    /// Pack bit-mapped variables back into their source words.
272    /// Called automatically after `process_tick`, before writing shared memory.
273    /// Only packs sources where at least one mapped bool changed since `pre_tick`.
274    /// Auto-generated by codegen when bit-mapped variables exist; default is no-op.
275    fn pack_bits(&mut self, _pre_tick: &Self) {}
276}
277
278/// Per-tick context passed to the control program by the framework.
279///
280/// `TickContext` bundles all per-cycle data into a single struct so that the
281/// [`ControlProgram::process_tick`] signature stays stable as new fields are
282/// added in the future (e.g., delta time, diagnostics).
283///
284/// The framework constructs a fresh `TickContext` each cycle, calls
285/// [`CommandClient::poll`] before handing it to the program, and writes
286/// the memory back to shared memory after `process_tick` returns.
287pub struct TickContext<'a, M> {
288    /// Mutable reference to the local shared memory copy.
289    pub gm: &'a mut M,
290    /// IPC command client for communicating with external modules.
291    pub client: &'a mut CommandClient,
292    /// Current cycle number (starts at 1, increments each tick).
293    pub cycle: u64,
294}
295
296/// The trait that defines a control program's logic.
297///
298/// Implement this trait to create your control program. The associated `Memory`
299/// type should be the generated `GlobalMemory` struct from your project.
300///
301/// # Memory Type Requirements
302///
303/// The `Memory` type must implement `Copy` to allow efficient synchronization
304/// between shared memory and local buffers. This is automatically satisfied
305/// by the generated `GlobalMemory` struct.
306///
307/// # Lifecycle
308///
309/// 1. `initialize` is called once at startup
310/// 2. `process_tick` is called repeatedly in the control loop with a
311///    [`TickContext`] that provides shared memory, the IPC client, and the
312///    current cycle number.
313///
314/// # Example
315///
316/// ```ignore
317/// use autocore_std::{ControlProgram, TickContext};
318///
319/// mod gm;
320/// use gm::GlobalMemory;
321///
322/// pub struct MyController {
323///     cycle_counter: u64,
324/// }
325///
326/// impl MyController {
327///     pub fn new() -> Self {
328///         Self { cycle_counter: 0 }
329///     }
330/// }
331///
332/// impl ControlProgram for MyController {
333///     type Memory = GlobalMemory;
334///
335///     fn initialize(&mut self, mem: &mut GlobalMemory) {
336///         // Set initial output states
337///         mem.outputs.ready = true;
338///         log::info!("Controller initialized");
339///     }
340///
341///     fn process_tick(&mut self, ctx: &mut TickContext<Self::Memory>) {
342///         self.cycle_counter = ctx.cycle;
343///
344///         // Your control logic here
345///         if ctx.gm.inputs.start && !ctx.gm.inputs.estop {
346///             ctx.gm.outputs.running = true;
347///         }
348///     }
349/// }
350/// ```
351pub trait ControlProgram {
352    /// The shared memory structure type (usually the generated `GlobalMemory`).
353    ///
354    /// Must implement `Copy` to allow efficient memory synchronization.
355    type Memory: Copy + ChangeTracker;
356
357    /// Called once when the control program starts.
358    ///
359    /// Use this to initialize output states, reset counters, or perform
360    /// any one-time setup. The default implementation does nothing.
361    ///
362    /// # Arguments
363    ///
364    /// * `mem` - Mutable reference to the shared memory. Changes are written
365    ///           back to shared memory after this method returns.
366    fn initialize(&mut self, _mem: &mut Self::Memory) {}
367
368    /// The main control loop - called once per scan cycle.
369    ///
370    /// This is where your control logic lives. Read inputs from `ctx.gm`,
371    /// perform calculations, and write outputs back to `ctx.gm`. Use
372    /// `ctx.client` for IPC commands and `ctx.cycle` for the current cycle
373    /// number.
374    ///
375    /// The framework calls [`CommandClient::poll`] before each invocation,
376    /// so incoming responses are already buffered when your code runs.
377    ///
378    /// # Arguments
379    ///
380    /// * `ctx` - A [`TickContext`] containing the local shared memory copy,
381    ///           the IPC command client, and the current cycle number.
382    ///
383    /// # Timing
384    ///
385    /// This method should complete within the scan cycle time. Long-running
386    /// operations will cause cycle overruns.
387    fn process_tick(&mut self, ctx: &mut TickContext<Self::Memory>);
388}
389
390/// Configuration for the [`ControlRunner`].
391///
392/// Specifies connection parameters, shared memory names, and logging settings.
393/// Use [`Default::default()`] for typical configurations.
394///
395/// # Example
396///
397/// ```
398/// use autocore_std::RunnerConfig;
399/// use log::LevelFilter;
400///
401/// let config = RunnerConfig {
402///     server_host: "192.168.1.100".to_string(),
403///     module_name: "my_controller".to_string(),
404///     shm_name: "my_project_shm".to_string(),
405///     tick_signal_name: "tick".to_string(),
406///     busy_signal_name: Some("busy".to_string()),
407///     log_level: LevelFilter::Debug,
408///     ..Default::default()
409/// };
410/// ```
411#[derive(Debug, Clone)]
412pub struct RunnerConfig {
413    /// Server host address (default: "127.0.0.1")
414    pub server_host: String,
415    /// WebSocket port for commands (default: 11969)
416    pub ws_port: u16,
417    /// Module name for identification (default: "control")
418    pub module_name: String,
419    /// Shared memory segment name (must match server configuration)
420    pub shm_name: String,
421    /// Name of the tick signal in shared memory (triggers each scan cycle)
422    pub tick_signal_name: String,
423    /// Optional name of the busy signal (set when cycle completes)
424    pub busy_signal_name: Option<String>,
425    /// Minimum log level to send to the server (default: Info)
426    pub log_level: LevelFilter,
427    /// UDP port for sending logs to the server (default: 39101)
428    pub log_udp_port: u16,
429}
430
431/// Default WebSocket port for autocore-server
432pub const DEFAULT_WS_PORT: u16 = 11969;
433
434impl Default for RunnerConfig {
435    fn default() -> Self {
436        Self {
437            server_host: "127.0.0.1".to_string(),
438            ws_port: DEFAULT_WS_PORT,
439            module_name: "control".to_string(),
440            shm_name: "autocore_cyclic".to_string(),
441            tick_signal_name: "tick".to_string(),
442            busy_signal_name: None,
443            log_level: LevelFilter::Info,
444            log_udp_port: logger::DEFAULT_LOG_UDP_PORT,
445        }
446    }
447}
448
449
450/// The main execution engine for control programs.
451///
452/// `ControlRunner` handles all the infrastructure required to run a control program:
453///
454/// - Reading memory layout from the server's layout file
455/// - Opening and mapping shared memory
456/// - Setting up synchronization signals
457/// - Running the real-time control loop
458/// - Sending log messages to the server
459///
460/// # Usage
461///
462/// ```ignore
463/// use autocore_std::{ControlRunner, RunnerConfig};
464///
465/// let config = RunnerConfig {
466///     shm_name: "my_project_shm".to_string(),
467///     tick_signal_name: "tick".to_string(),
468///     ..Default::default()
469/// };
470///
471/// ControlRunner::new(MyProgram::new())
472///     .config(config)
473///     .run()?;  // Blocks forever
474/// ```
475///
476/// # Control Loop
477///
478/// The runner executes a synchronous control loop:
479///
480/// 1. **Wait** - Blocks until the tick signal is set by the server
481/// 2. **Read** - Copies shared memory to a local buffer (acquire barrier)
482/// 3. **Execute** - Calls your `process_tick` method
483/// 4. **Write** - Copies local buffer back to shared memory (release barrier)
484/// 5. **Signal** - Sets the busy signal (if configured) to indicate completion
485///
486/// This ensures your code always sees a consistent snapshot of the data
487/// and that your writes are atomically visible to other processes.
488pub struct ControlRunner<P: ControlProgram> {
489    config: RunnerConfig,
490    program: P,
491}
492
493impl<P: ControlProgram> ControlRunner<P> {
494    /// Creates a new runner for the given control program.
495    ///
496    /// Uses default configuration. Call [`.config()`](Self::config) to customize.
497    ///
498    /// # Arguments
499    ///
500    /// * `program` - Your control program instance
501    ///
502    /// # Example
503    ///
504    /// ```ignore
505    /// let runner = ControlRunner::new(MyProgram::new());
506    /// ```
507    pub fn new(program: P) -> Self {
508        Self {
509            config: RunnerConfig::default(),
510            program,
511        }
512    }
513
514    /// Sets the configuration for this runner.
515    ///
516    /// # Arguments
517    ///
518    /// * `config` - The configuration to use
519    ///
520    /// # Example
521    ///
522    /// ```ignore
523    /// ControlRunner::new(MyProgram::new())
524    ///     .config(RunnerConfig {
525    ///         shm_name: "custom_shm".to_string(),
526    ///         ..Default::default()
527    ///     })
528    ///     .run()?;
529    /// ```
530    pub fn config(mut self, config: RunnerConfig) -> Self {
531        self.config = config;
532        self
533    }
534
535    /// Starts the control loop.
536    ///
537    /// This method blocks indefinitely, running the control loop until
538    /// an error occurs or the process is terminated.
539    ///
540    /// # Returns
541    ///
542    /// Returns `Ok(())` only if the loop exits cleanly (which typically
543    /// doesn't happen). Returns an error if:
544    ///
545    /// - IPC connection fails
546    /// - Shared memory cannot be opened
547    /// - Signal offsets cannot be found
548    /// - A critical error occurs during execution
549    ///
550    /// # Example
551    ///
552    /// ```ignore
553    /// fn main() -> anyhow::Result<()> {
554    ///     ControlRunner::new(MyProgram::new())
555    ///         .config(config)
556    ///         .run()
557    /// }
558    /// ```
559    pub fn run(mut self) -> Result<()> {
560        // Initialize UDP logger FIRST (before any log statements)
561        if let Err(e) = logger::init_udp_logger(
562            &self.config.server_host,
563            self.config.log_udp_port,
564            self.config.log_level,
565            "control",
566        ) {
567            eprintln!("Warning: Failed to initialize UDP logger: {}", e);
568            // Continue anyway - logging will just go nowhere
569        }
570
571        // Multi-threaded runtime so spawned WS read/write tasks can run
572        // alongside the synchronous control loop.
573        let rt = tokio::runtime::Builder::new_multi_thread()
574            .worker_threads(2)
575            .enable_all()
576            .build()?;
577
578        rt.block_on(async {
579            log::info!("AutoCore Control Runner Starting...");
580
581            // 1. Connect to server via WebSocket and get layout
582            let ws_url = format!("ws://{}:{}/ws/", self.config.server_host, self.config.ws_port);
583            log::info!("Connecting to server at {}", ws_url);
584
585            let (ws_stream, _) = connect_async(&ws_url).await
586                .map_err(|e| anyhow!("Failed to connect to server at {}: {}", ws_url, e))?;
587
588            let (mut write, mut read) = ws_stream.split();
589
590            // Send gm.get_layout request
591            let request = CommandMessage::request("gm.get_layout", serde_json::Value::Null);
592            let transaction_id = request.transaction_id;
593            let request_json = serde_json::to_string(&request)?;
594
595            write.send(Message::Text(request_json)).await
596                .map_err(|e| anyhow!("Failed to send layout request: {}", e))?;
597
598            // Wait for response with matching transaction_id
599            let timeout = Duration::from_secs(10);
600            let start = std::time::Instant::now();
601            let mut layout: Option<HashMap<String, serde_json::Value>> = None;
602
603            while start.elapsed() < timeout {
604                match tokio::time::timeout(Duration::from_secs(1), read.next()).await {
605                    Ok(Some(Ok(Message::Text(text)))) => {
606                        if let Ok(response) = serde_json::from_str::<CommandMessage>(&text) {
607                            if response.transaction_id == transaction_id {
608                                if !response.success {
609                                    return Err(anyhow!("Server error: {}", response.error_message));
610                                }
611                                layout = Some(serde_json::from_value(response.data)?);
612                                break;
613                            }
614                            // Skip broadcasts and other messages
615                            if response.message_type == MessageType::Broadcast {
616                                continue;
617                            }
618                        }
619                    }
620                    Ok(Some(Ok(_))) => continue,
621                    Ok(Some(Err(e))) => return Err(anyhow!("WebSocket error: {}", e)),
622                    Ok(None) => return Err(anyhow!("Server closed connection")),
623                    Err(_) => continue, // Timeout on single read, keep trying
624                }
625            }
626
627            let layout = layout.ok_or_else(|| anyhow!("Timeout waiting for layout response"))?;
628            log::info!("Layout received with {} entries.", layout.len());
629
630            // Set up channels and background tasks for shared WebSocket access.
631            // This allows both the control loop (gm.write) and CommandClient (IPC
632            // commands) to share the write half, while routing incoming responses
633            // to the CommandClient.
634            let (ws_write_tx, mut ws_write_rx) = tokio::sync::mpsc::unbounded_channel::<String>();
635            let (response_tx, response_rx) = tokio::sync::mpsc::unbounded_channel::<CommandMessage>();
636
637            // Background task: WS write loop
638            // Reads serialized messages from ws_write_rx and sends them over the WebSocket.
639            tokio::spawn(async move {
640                while let Some(msg_json) = ws_write_rx.recv().await {
641                    if let Err(e) = write.send(Message::Text(msg_json)).await {
642                        log::error!("WebSocket write error: {}", e);
643                        break;
644                    }
645                }
646            });
647
648            // Background task: WS read loop
649            // Reads all incoming WebSocket messages. Routes Response and
650            // Broadcast messages to response_tx; CommandClient dispatches
651            // them onward (responses by transaction_id, broadcasts by
652            // topic into per-topic buffers that subscribers drain via
653            // `take_broadcasts`).
654            tokio::spawn(async move {
655                while let Some(result) = read.next().await {
656                    match result {
657                        Ok(Message::Text(text)) => {
658                            if let Ok(msg) = serde_json::from_str::<CommandMessage>(&text) {
659                                if matches!(
660                                    msg.message_type,
661                                    MessageType::Response | MessageType::Broadcast,
662                                ) {
663                                    if response_tx.send(msg).is_err() {
664                                        break; // receiver dropped
665                                    }
666                                }
667                                // Other message types are ignored
668                            }
669                        }
670                        Ok(Message::Close(_)) => {
671                            log::info!("WebSocket closed by server");
672                            break;
673                        }
674                        Err(e) => {
675                            log::error!("WebSocket read error: {}", e);
676                            break;
677                        }
678                        _ => {} // Ping/Pong/Binary - ignore
679                    }
680                }
681            });
682
683            // Construct CommandClient — owned by the runner, passed to the
684            // program via TickContext each cycle.
685            let mut command_client = CommandClient::new(ws_write_tx.clone(), response_rx);
686
687            // 2. Find Signal Offsets
688            let tick_offset = self.find_offset(&layout, &self.config.tick_signal_name)?;
689            let busy_offset = if let Some(name) = &self.config.busy_signal_name {
690                Some(self.find_offset(&layout, name)?)
691            } else {
692                None
693            };
694
695            // 4. Open Shared Memory
696            let shmem = ShmemConf::new().os_id(&self.config.shm_name).open()?;
697            let base_ptr = shmem.as_ptr();
698            log::info!("Shared Memory '{}' mapped.", self.config.shm_name);
699
700            // Wait for the server to finish applying initial values before we
701            // read SHM. Without this, our startup read can race with the
702            // server's initial-value writes; we would then clobber any
703            // initials when we write local_mem back below.
704            if let Some(ready_info) = layout.get("__ready__") {
705                let ready_offset = ready_info.get("offset")
706                    .and_then(|v| v.as_u64())
707                    .ok_or_else(|| anyhow!("__ready__ layout entry has no offset"))? as usize;
708                let ready_ptr = unsafe { base_ptr.add(ready_offset) as *const u32 };
709                let start = std::time::Instant::now();
710                let timeout = Duration::from_secs(10);
711                loop {
712                    let val = unsafe { std::ptr::read_volatile(ready_ptr) };
713                    if val == 1 {
714                        fence(Ordering::Acquire);
715                        log::info!("Server ready flag observed after {:?}", start.elapsed());
716                        break;
717                    }
718                    if start.elapsed() > timeout {
719                        log::warn!("Timed out waiting for server ready flag; proceeding anyway (initials may be zeroed)");
720                        break;
721                    }
722                    std::thread::sleep(Duration::from_millis(5));
723                }
724            } else {
725                log::warn!("No __ready__ flag in layout; server may predate ready-flag protocol. Initial values may race.");
726            }
727
728            // 5. Setup Pointers
729            // SAFETY: We trust the server's layout matches the generated GlobalMemory struct.
730            let gm = unsafe { &mut *(base_ptr as *mut P::Memory) };
731
732            // Get tick event from shared memory
733            log::info!("Setting up tick event at offset {} (base_ptr: {:p})", tick_offset, base_ptr);
734            let (tick_event, _) = unsafe {
735                Event::from_existing(base_ptr.add(tick_offset))
736            }.map_err(|e| anyhow!("Failed to open tick event: {:?}", e))?;
737            log::info!("Tick event ready");
738
739            // Busy signal event (optional)
740            let busy_event = busy_offset.map(|offset| {
741                unsafe { Event::from_existing(base_ptr.add(offset)) }
742                    .map(|(event, _)| event)
743                    .ok()
744            }).flatten();
745
746            // 6. Initialize local memory buffer and user program
747            // We use a local copy for the control loop to ensure:
748            // - Consistent snapshot of inputs at start of cycle
749            // - Atomic commit of outputs at end of cycle
750            // - Proper memory barriers for cross-process visibility
751            let mut local_mem: P::Memory = unsafe { std::ptr::read_volatile(gm) };
752            let mut prev_mem: P::Memory = local_mem; // Snapshot for change detection
753
754            fence(Ordering::Acquire); // Ensure we see all prior writes from other processes
755
756            self.program.initialize(&mut local_mem);
757
758            // Write back any changes from initialize
759            fence(Ordering::Release);
760            unsafe { std::ptr::write_volatile(gm, local_mem) };
761
762            // Set up signal handler for graceful shutdown
763            let running = Arc::new(AtomicBool::new(true));
764            let r = running.clone();
765            
766            // Only set handler if not already set
767            if let Err(e) = ctrlc::set_handler(move || {
768                r.store(false, Ordering::SeqCst);
769            }) {
770                log::warn!("Failed to set signal handler: {}", e);
771            }
772
773            log::info!("Entering Control Loop - waiting for first tick...");
774            let mut cycle_count: u64 = 0;
775            let mut consecutive_timeouts: u32 = 0;
776
777            while running.load(Ordering::SeqCst) {
778                // Wait for Tick - Event-based synchronization
779                // Use a timeout (1s) to allow checking the running flag periodically
780                match tick_event.wait(Timeout::Val(Duration::from_secs(1))) {
781                    Ok(_) => {
782                        consecutive_timeouts = 0;
783                    },
784                    Err(e) => {
785                        // Check for timeout
786                        let err_str = format!("{:?}", e);
787                        if err_str.contains("Timeout") {
788                            consecutive_timeouts += 1;
789                            if consecutive_timeouts == 10 {
790                                log::error!(
791                                    "TICK STALL: {} consecutive timeouts! cycle={} pending={} responses={} fds={} rss_kb={}",
792                                    consecutive_timeouts,
793                                    cycle_count,
794                                    command_client.pending_count(),
795                                    command_client.response_count(),
796                                    diagnostics::count_open_fds(),
797                                    diagnostics::get_rss_kb(),
798                                );
799                            }
800                            if consecutive_timeouts > 10 && consecutive_timeouts % 60 == 0 {
801                                log::error!(
802                                    "TICK STALL continues: {} consecutive timeouts, cycle={}",
803                                    consecutive_timeouts,
804                                    cycle_count,
805                                );
806                            }
807                            continue;
808                        }
809                        return Err(anyhow!("Tick wait failed: {:?}", e));
810                    }
811                }
812
813                if !running.load(Ordering::SeqCst) {
814                    log::info!("Shutdown signal received, exiting control loop.");
815                    break;
816                }
817
818                cycle_count += 1;
819                if cycle_count == 1 {
820                    log::info!("First tick received!");
821                }
822
823                // // Periodic diagnostics (every 30s at 100 Hz)
824                // if cycle_count % 3000 == 0 {
825                //     log::info!(
826                //         "DIAG cycle={} pending={} responses={} fds={} rss_kb={}",
827                //         cycle_count,
828                //         command_client.pending_count(),
829                //         command_client.response_count(),
830                //         diagnostics::count_open_fds(),
831                //         diagnostics::get_rss_kb(),
832                //     );
833                // }
834
835                // === INPUT PHASE ===
836                // Read all variables from shared memory into local buffer.
837                // This gives us a consistent snapshot of inputs for this cycle.
838                // Acquire fence ensures we see all writes from other processes (server, modules).
839                local_mem = unsafe { std::ptr::read_volatile(gm) };
840                
841                // Update prev_mem before execution to track changes made IN THIS CYCLE
842                // Actually, we want to know what changed in SHM relative to what we last knew,
843                // OR what WE changed relative to what we read?
844                // The user wants "writes on shared variables" to be broadcast.
845                // Typically outputs.
846                // If inputs changed (from other source), broadcasting them again is fine too.
847                // Let's capture state BEFORE execution (which is what we just read from SHM).
848                prev_mem = local_mem;
849
850                fence(Ordering::Acquire);
851
852                // Unpack bit-mapped variables from their source words.
853                local_mem.unpack_bits();
854
855                // Snapshot after unpack — used by pack_bits to detect which
856                // bools the control program actually changed.
857                let pre_tick = local_mem;
858
859                // === EXECUTE PHASE ===
860                // Poll IPC responses so they are available during process_tick.
861                command_client.poll();
862
863                // Execute user logic on the local copy.
864                // All reads/writes during process_tick operate on local_mem.
865                let mut ctx = TickContext {
866                    gm: &mut local_mem,
867                    client: &mut command_client,
868                    cycle: cycle_count,
869                };
870                self.program.process_tick(&mut ctx);
871
872                // === OUTPUT PHASE ===
873                // Pack bit-mapped variables back into their source words,
874                // but only for sources where a mapped bool actually changed.
875                local_mem.pack_bits(&pre_tick);
876
877                // Write all variables from local buffer back to shared memory.
878                // Release fence ensures our writes are visible to other processes.
879                fence(Ordering::Release);
880                unsafe { std::ptr::write_volatile(gm, local_mem) };
881
882                // === CHANGE DETECTION & NOTIFICATION ===
883                let changes = local_mem.get_changes(&prev_mem);
884                if !changes.is_empty() {
885                    // Construct bulk write message
886                    let mut data_map = serde_json::Map::new();
887                    for (key, val) in changes {
888                        data_map.insert(key.to_string(), val);
889                    }
890                    
891                    let msg = CommandMessage::request("gm.write", serde_json::Value::Object(data_map));
892                    let msg_json = serde_json::to_string(&msg).unwrap_or_default();
893
894                    // Send via the shared write channel (non-blocking)
895                    if let Err(e) = ws_write_tx.send(msg_json) {
896                        log::error!("Failed to send updates: {}", e);
897                    }
898                }
899
900                // Signal Busy/Done event
901                if let Some(ref busy_ev) = busy_event {
902                    let _ = busy_ev.set(EventState::Signaled);
903                }
904            }
905
906            Ok(())
907        })
908    }
909
910    fn find_offset(&self, layout: &HashMap<String, serde_json::Value>, name: &str) -> Result<usize> {
911        let info = layout.get(name).ok_or_else(|| anyhow!("Signal '{}' not found in layout", name))?;
912        info.get("offset")
913            .and_then(|v| v.as_u64())
914            .map(|v| v as usize)
915            .ok_or_else(|| anyhow!("Invalid offset for '{}'", name))
916    }
917}
918
919/// Generates the standard `main` function for a control program.
920///
921/// This macro reduces boilerplate by creating a properly configured `main`
922/// function that initializes and runs your control program.
923///
924/// # Arguments
925///
926/// * `$prog_type` - The type of your control program (must implement [`ControlProgram`])
927/// * `$shm_name` - The shared memory segment name (string literal)
928/// * `$tick_signal` - The tick signal name in shared memory (string literal)
929///
930/// # Example
931///
932/// ```ignore
933/// mod gm;
934/// use gm::GlobalMemory;
935///
936/// pub struct MyProgram;
937///
938/// impl MyProgram {
939///     pub fn new() -> Self { Self }
940/// }
941///
942/// impl autocore_std::ControlProgram for MyProgram {
943///     type Memory = GlobalMemory;
944///
945///     fn process_tick(&mut self, ctx: &mut autocore_std::TickContext<Self::Memory>) {
946///         // Your logic here
947///     }
948/// }
949///
950/// // This generates the main function
951/// autocore_std::autocore_main!(MyProgram, "my_project_shm", "tick");
952/// ```
953///
954/// # Generated Code
955///
956/// The macro expands to:
957///
958/// ```ignore
959/// fn main() -> anyhow::Result<()> {
960///     let config = autocore_std::RunnerConfig {
961///         server_host: "127.0.0.1".to_string(),
962///         ws_port: autocore_std::DEFAULT_WS_PORT,
963///         module_name: "control".to_string(),
964///         shm_name: "my_project_shm".to_string(),
965///         tick_signal_name: "tick".to_string(),
966///         busy_signal_name: None,
967///         log_level: log::LevelFilter::Info,
968///         log_udp_port: autocore_std::logger::DEFAULT_LOG_UDP_PORT,
969///     };
970///
971///     autocore_std::ControlRunner::new(MyProgram::new())
972///         .config(config)
973///         .run()
974/// }
975/// ```
976#[macro_export]
977macro_rules! autocore_main {
978    ($prog_type:ty, $shm_name:expr, $tick_signal:expr) => {
979        fn main() -> anyhow::Result<()> {
980            let config = autocore_std::RunnerConfig {
981                server_host: "127.0.0.1".to_string(),
982                ws_port: autocore_std::DEFAULT_WS_PORT,
983                module_name: "control".to_string(),
984                shm_name: $shm_name.to_string(),
985                tick_signal_name: $tick_signal.to_string(),
986                busy_signal_name: None,
987                log_level: log::LevelFilter::Info,
988                log_udp_port: autocore_std::logger::DEFAULT_LOG_UDP_PORT,
989            };
990
991            autocore_std::ControlRunner::new(<$prog_type>::new())
992                .config(config)
993                .run()
994        }
995    };
996}
997