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/// Test Information System (TIS) producer helpers — conveniences over the
222/// `tis.*` IPC commands, e.g. attaching chart region bands to a cycle.
223pub mod tis;
224
225/// Subscription helper for `ams.asset_updated.<location>` broadcasts.
226/// Control programs use this to drive EL3356 SDO writes (and similar)
227/// when a load cell asset is registered, recalibrated, or retired.
228pub mod asset_watch;
229pub use asset_watch::{AssetUpdate, AssetWatch, AssetWatchStatus, AssetWatchTrigger};
230
231/// EtherCAT utilities (SDO client, etc.).
232pub mod ethercat;
233
234/// CiA 402 motion control: axis abstraction, traits, and types.
235pub mod motion;
236
237/// Shared memory utilities for external modules.
238pub mod shm;
239
240/// Lightweight process diagnostics (FD count, RSS).
241pub mod diagnostics;
242
243/// Banner Engineering device helpers (WLS15 IO-Link light strip, etc.).
244pub mod banner;
245
246/// Fixed-length string type for shared memory variables.
247pub mod fixed_string;
248pub use fixed_string::FixedString;
249
250/// Zero-phase Butterworth low-pass filtering for captured signal traces.
251pub mod butterworth;
252
253// ============================================================================
254// Core Framework
255// ============================================================================
256
257/// Marker trait for generated GlobalMemory structs.
258///
259/// This trait is implemented by the auto-generated `GlobalMemory` struct
260/// that represents the shared memory layout. It serves as a marker for
261/// type safety in the control framework.
262///
263/// You don't need to implement this trait yourself - it's automatically
264/// implemented by the code generator.
265pub trait AutoCoreMemory {}
266
267/// Trait for detecting changes in memory structures.
268pub trait ChangeTracker {
269    /// Compare self with a previous state and return a list of changed fields.
270    /// Returns a vector of (field_name, new_value).
271    fn get_changes(&self, prev: &Self) -> Vec<(&'static str, serde_json::Value)>;
272
273    /// Unpack bit-mapped variables from their source words.
274    /// Called automatically after reading shared memory, before `process_tick`.
275    /// Auto-generated by codegen when bit-mapped variables exist; default is no-op.
276    fn unpack_bits(&mut self) {}
277
278    /// Pack bit-mapped variables back into their source words.
279    /// Called automatically after `process_tick`, before writing shared memory.
280    /// Only packs sources where at least one mapped bool changed since `pre_tick`.
281    /// Auto-generated by codegen when bit-mapped variables exist; default is no-op.
282    fn pack_bits(&mut self, _pre_tick: &Self) {}
283}
284
285/// A neutral, typed value for a single GlobalMemory field.
286///
287/// This is the lingua franca for *generic* GM field access ([`GmAccess`]): it
288/// lets tooling read/write GM fields by name or id without knowing the concrete
289/// generated struct. Values are widened to the broadest type per category (all
290/// integer widths as `i64`, all floats as `f64`).
291#[derive(Clone, Debug, PartialEq)]
292pub enum FieldValue {
293    /// A boolean field.
294    Bool(bool),
295    /// Any integer field, widened to `i64`.
296    Int(i64),
297    /// Any float field, widened to `f64`.
298    Float(f64),
299    /// A fixed-string field, as an owned `String`.
300    Text(String),
301}
302
303impl FieldValue {
304    /// Interpret as a boolean (non-zero integers are `true`).
305    pub fn as_bool(&self) -> Option<bool> {
306        match self {
307            FieldValue::Bool(b) => Some(*b),
308            FieldValue::Int(i) => Some(*i != 0),
309            _ => None,
310        }
311    }
312    /// Interpret as an `i64` (floats truncate, bools map to 0/1).
313    pub fn as_i64(&self) -> Option<i64> {
314        match self {
315            FieldValue::Int(i) => Some(*i),
316            FieldValue::Float(f) => Some(*f as i64),
317            FieldValue::Bool(b) => Some(*b as i64),
318            _ => None,
319        }
320    }
321    /// Interpret as an `f64` (ints/bools promote).
322    pub fn as_f64(&self) -> Option<f64> {
323        match self {
324            FieldValue::Float(f) => Some(*f),
325            FieldValue::Int(i) => Some(*i as f64),
326            FieldValue::Bool(b) => Some(if *b { 1.0 } else { 0.0 }),
327            _ => None,
328        }
329    }
330    /// Borrow as text, if this is a `Text` value.
331    pub fn as_str(&self) -> Option<&str> {
332        match self {
333            FieldValue::Text(s) => Some(s.as_str()),
334            _ => None,
335        }
336    }
337}
338
339/// Generic, module-agnostic access to `GlobalMemory` fields by name or id.
340///
341/// This is the inverse of the [`ChangeTracker`] field table, and like it, it is
342/// **auto-generated by the code generator** — you don't implement it by hand.
343/// It exists so that *any* consumer — the IDE, debuggers, name-keyed IPC, the
344/// autocore-seq sequencer — can read and write GM fields generically, without
345/// the framework knowing about those consumers. `field_id` is resolved once
346/// (off the hot path); `read_field` / `write_field` are O(1).
347pub trait GmAccess {
348    /// Resolve a field name to a stable id. `None` if there is no such field.
349    fn field_id(name: &str) -> Option<u32>
350    where
351        Self: Sized;
352    /// Read a field by id. `None` if the id is out of range.
353    fn read_field(&self, id: u32) -> Option<FieldValue>;
354    /// Write a field by id. Returns `false` if the id is unknown or the value
355    /// is type-incompatible (e.g. text into a numeric field).
356    fn write_field(&mut self, id: u32, value: &FieldValue) -> bool;
357}
358
359/// Per-tick context passed to the control program by the framework.
360///
361/// `TickContext` bundles all per-cycle data into a single struct so that the
362/// [`ControlProgram::process_tick`] signature stays stable as new fields are
363/// added in the future (e.g., delta time, diagnostics).
364///
365/// The framework constructs a fresh `TickContext` each cycle, calls
366/// [`CommandClient::poll`] before handing it to the program, and writes
367/// the memory back to shared memory after `process_tick` returns.
368pub struct TickContext<'a, M> {
369    /// Mutable reference to the local shared memory copy.
370    pub gm: &'a mut M,
371    /// IPC command client for communicating with external modules.
372    pub client: &'a mut CommandClient,
373    /// Current cycle number (starts at 1, increments each tick).
374    pub cycle: u64,
375}
376
377/// The trait that defines a control program's logic.
378///
379/// Implement this trait to create your control program. The associated `Memory`
380/// type should be the generated `GlobalMemory` struct from your project.
381///
382/// # Memory Type Requirements
383///
384/// The `Memory` type must implement `Copy` to allow efficient synchronization
385/// between shared memory and local buffers. This is automatically satisfied
386/// by the generated `GlobalMemory` struct.
387///
388/// # Lifecycle
389///
390/// 1. `initialize` is called once at startup
391/// 2. `process_tick` is called repeatedly in the control loop with a
392///    [`TickContext`] that provides shared memory, the IPC client, and the
393///    current cycle number.
394///
395/// # Example
396///
397/// ```ignore
398/// use autocore_std::{ControlProgram, TickContext};
399///
400/// mod gm;
401/// use gm::GlobalMemory;
402///
403/// pub struct MyController {
404///     cycle_counter: u64,
405/// }
406///
407/// impl MyController {
408///     pub fn new() -> Self {
409///         Self { cycle_counter: 0 }
410///     }
411/// }
412///
413/// impl ControlProgram for MyController {
414///     type Memory = GlobalMemory;
415///
416///     fn initialize(&mut self, mem: &mut GlobalMemory) {
417///         // Set initial output states
418///         mem.outputs.ready = true;
419///         log::info!("Controller initialized");
420///     }
421///
422///     fn process_tick(&mut self, ctx: &mut TickContext<Self::Memory>) {
423///         self.cycle_counter = ctx.cycle;
424///
425///         // Your control logic here
426///         if ctx.gm.inputs.start && !ctx.gm.inputs.estop {
427///             ctx.gm.outputs.running = true;
428///         }
429///     }
430/// }
431/// ```
432/// Compatibility constants for the GM segment a control program was generated
433/// against. Code-generated `gm.rs` implements this for `GlobalMemory`, so every
434/// control program carries the fingerprint of the exact layout it was built
435/// for. The runner checks these against the server's SHM header before reading
436/// a single byte of the variable region (see [`ControlRunner::run`]).
437pub trait GmCompat {
438    /// Fingerprint of the GM layout (must equal the server's SHM header hash).
439    const LAYOUT_HASH: u64;
440    /// Total GM segment size in bytes, including the header.
441    const TOTAL_SIZE: usize;
442}
443
444pub trait ControlProgram {
445    /// The shared memory structure type (usually the generated `GlobalMemory`).
446    ///
447    /// Must implement `Copy` to allow efficient memory synchronization, and
448    /// `GmCompat` so the runner can verify it matches the live segment.
449    type Memory: Copy + ChangeTracker + GmCompat;
450
451    /// Called once when the control program starts.
452    ///
453    /// Use this to initialize output states, reset counters, or perform
454    /// any one-time setup. The default implementation does nothing.
455    ///
456    /// # Arguments
457    ///
458    /// * `mem` - Mutable reference to the shared memory. Changes are written
459    ///           back to shared memory after this method returns.
460    fn initialize(&mut self, _mem: &mut Self::Memory) {}
461
462    /// The main control loop - called once per scan cycle.
463    ///
464    /// This is where your control logic lives. Read inputs from `ctx.gm`,
465    /// perform calculations, and write outputs back to `ctx.gm`. Use
466    /// `ctx.client` for IPC commands and `ctx.cycle` for the current cycle
467    /// number.
468    ///
469    /// The framework calls [`CommandClient::poll`] before each invocation,
470    /// so incoming responses are already buffered when your code runs.
471    ///
472    /// # Arguments
473    ///
474    /// * `ctx` - A [`TickContext`] containing the local shared memory copy,
475    ///           the IPC command client, and the current cycle number.
476    ///
477    /// # Timing
478    ///
479    /// This method should complete within the scan cycle time. Long-running
480    /// operations will cause cycle overruns.
481    fn process_tick(&mut self, ctx: &mut TickContext<Self::Memory>);
482}
483
484/// Configuration for the [`ControlRunner`].
485///
486/// Specifies connection parameters, shared memory names, and logging settings.
487/// Use [`Default::default()`] for typical configurations.
488///
489/// # Example
490///
491/// ```
492/// use autocore_std::RunnerConfig;
493/// use log::LevelFilter;
494///
495/// let config = RunnerConfig {
496///     server_host: "192.168.1.100".to_string(),
497///     module_name: "my_controller".to_string(),
498///     shm_name: "my_project_shm".to_string(),
499///     tick_signal_name: "tick".to_string(),
500///     busy_signal_name: Some("busy".to_string()),
501///     log_level: LevelFilter::Debug,
502///     ..Default::default()
503/// };
504/// ```
505#[derive(Debug, Clone)]
506pub struct RunnerConfig {
507    /// Server host address (default: "127.0.0.1")
508    pub server_host: String,
509    /// WebSocket port for commands (default: 11969)
510    pub ws_port: u16,
511    /// Module name for identification (default: "control")
512    pub module_name: String,
513    /// Shared memory segment name (must match server configuration)
514    pub shm_name: String,
515    /// Name of the tick signal in shared memory (triggers each scan cycle)
516    pub tick_signal_name: String,
517    /// Optional name of the busy signal (set when cycle completes)
518    pub busy_signal_name: Option<String>,
519    /// Minimum log level to send to the server (default: Info)
520    pub log_level: LevelFilter,
521    /// UDP port for sending logs to the server (default: 39101)
522    pub log_udp_port: u16,
523}
524
525/// Default WebSocket port for autocore-server
526pub const DEFAULT_WS_PORT: u16 = 11969;
527
528impl Default for RunnerConfig {
529    fn default() -> Self {
530        Self {
531            server_host: "127.0.0.1".to_string(),
532            ws_port: DEFAULT_WS_PORT,
533            module_name: "control".to_string(),
534            shm_name: "autocore_cyclic".to_string(),
535            tick_signal_name: "tick".to_string(),
536            busy_signal_name: None,
537            log_level: LevelFilter::Info,
538            log_udp_port: logger::DEFAULT_LOG_UDP_PORT,
539        }
540    }
541}
542
543
544/// The main execution engine for control programs.
545///
546/// `ControlRunner` handles all the infrastructure required to run a control program:
547///
548/// - Reading memory layout from the server's layout file
549/// - Opening and mapping shared memory
550/// - Setting up synchronization signals
551/// - Running the real-time control loop
552/// - Sending log messages to the server
553///
554/// # Usage
555///
556/// ```ignore
557/// use autocore_std::{ControlRunner, RunnerConfig};
558///
559/// let config = RunnerConfig {
560///     shm_name: "my_project_shm".to_string(),
561///     tick_signal_name: "tick".to_string(),
562///     ..Default::default()
563/// };
564///
565/// ControlRunner::new(MyProgram::new())
566///     .config(config)
567///     .run()?;  // Blocks forever
568/// ```
569///
570/// # Control Loop
571///
572/// The runner executes a synchronous control loop:
573///
574/// 1. **Wait** - Blocks until the tick signal is set by the server
575/// 2. **Read** - Copies shared memory to a local buffer (acquire barrier)
576/// 3. **Execute** - Calls your `process_tick` method
577/// 4. **Write** - Copies local buffer back to shared memory (release barrier)
578/// 5. **Signal** - Sets the busy signal (if configured) to indicate completion
579///
580/// This ensures your code always sees a consistent snapshot of the data
581/// and that your writes are atomically visible to other processes.
582pub struct ControlRunner<P: ControlProgram> {
583    config: RunnerConfig,
584    program: P,
585    /// Build-time version of the *control crate* (not autocore-std). Set by
586    /// the `autocore_main!` macro from the control crate's
587    /// `CARGO_PKG_VERSION`, so it advances automatically with every
588    /// `acctl push control` bump. Surfaced via `--print-compat` for the
589    /// server to read and display. Defaults to "unknown" for callers built
590    /// against an older macro that doesn't set it (backwards compatible).
591    version: &'static str,
592}
593
594impl<P: ControlProgram> ControlRunner<P> {
595    /// Creates a new runner for the given control program.
596    ///
597    /// Uses default configuration. Call [`.config()`](Self::config) to customize.
598    ///
599    /// # Arguments
600    ///
601    /// * `program` - Your control program instance
602    ///
603    /// # Example
604    ///
605    /// ```ignore
606    /// let runner = ControlRunner::new(MyProgram::new());
607    /// ```
608    pub fn new(program: P) -> Self {
609        Self {
610            config: RunnerConfig::default(),
611            program,
612            version: "unknown",
613        }
614    }
615
616    /// Sets the control program's build version. Called by `autocore_main!`
617    /// with the control crate's `CARGO_PKG_VERSION`; not normally set by
618    /// hand. Reported in `--print-compat` output.
619    pub fn version(mut self, version: &'static str) -> Self {
620        self.version = version;
621        self
622    }
623
624    /// Sets the configuration for this runner.
625    ///
626    /// # Arguments
627    ///
628    /// * `config` - The configuration to use
629    ///
630    /// # Example
631    ///
632    /// ```ignore
633    /// ControlRunner::new(MyProgram::new())
634    ///     .config(RunnerConfig {
635    ///         shm_name: "custom_shm".to_string(),
636    ///         ..Default::default()
637    ///     })
638    ///     .run()?;
639    /// ```
640    pub fn config(mut self, config: RunnerConfig) -> Self {
641        self.config = config;
642        self
643    }
644
645    /// Starts the control loop.
646    ///
647    /// This method blocks indefinitely, running the control loop until
648    /// an error occurs or the process is terminated.
649    ///
650    /// # Returns
651    ///
652    /// Returns `Ok(())` only if the loop exits cleanly (which typically
653    /// doesn't happen). Returns an error if:
654    ///
655    /// - IPC connection fails
656    /// - Shared memory cannot be opened
657    /// - Signal offsets cannot be found
658    /// - A critical error occurs during execution
659    ///
660    /// # Example
661    ///
662    /// ```ignore
663    /// fn main() -> anyhow::Result<()> {
664    ///     ControlRunner::new(MyProgram::new())
665    ///         .config(config)
666    ///         .run()
667    /// }
668    /// ```
669    pub fn run(mut self) -> Result<()> {
670        // Fast path: report this binary's build-time compatibility info as JSON
671        // and exit. `acctl push control` uses this to compare the freshly built
672        // control program against the running server without attaching.
673        if std::env::args().any(|a| a == "--print-compat") {
674            println!("{}", serde_json::json!({
675                "format_version": mechutil::shm_header::SHM_FORMAT_VERSION,
676                "shm_abi_version": mechutil::SHM_ABI_VERSION,
677                "mechutil_version": mechutil::VERSION,
678                "layout_hash": format!("{:#018x}", <P::Memory as GmCompat>::LAYOUT_HASH),
679                "total_size": <P::Memory as GmCompat>::TOTAL_SIZE,
680                // Build version of the control crate. Consumers that predate
681                // this field simply ignore it (backwards compatible).
682                "version": self.version,
683            }));
684            return Ok(());
685        }
686
687        // Initialize UDP logger FIRST (before any log statements)
688        if let Err(e) = logger::init_udp_logger(
689            &self.config.server_host,
690            self.config.log_udp_port,
691            self.config.log_level,
692            "control",
693        ) {
694            eprintln!("Warning: Failed to initialize UDP logger: {}", e);
695            // Continue anyway - logging will just go nowhere
696        }
697
698        // Multi-threaded runtime so spawned WS read/write tasks can run
699        // alongside the synchronous control loop.
700        let rt = tokio::runtime::Builder::new_multi_thread()
701            .worker_threads(2)
702            .enable_all()
703            .build()?;
704
705        rt.block_on(async {
706            log::info!("AutoCore Control Runner Starting...");
707
708            // 1. Connect to server via WebSocket and get layout
709            let ws_url = format!("ws://{}:{}/ws/", self.config.server_host, self.config.ws_port);
710            log::info!("Connecting to server at {}", ws_url);
711
712            let (ws_stream, _) = connect_async(&ws_url).await
713                .map_err(|e| anyhow!("Failed to connect to server at {}: {}", ws_url, e))?;
714
715            let (mut write, mut read) = ws_stream.split();
716
717            // Send gm.get_layout request
718            let request = CommandMessage::request("gm.get_layout", serde_json::Value::Null);
719            let transaction_id = request.transaction_id;
720            let request_json = serde_json::to_string(&request)?;
721
722            write.send(Message::Text(request_json)).await
723                .map_err(|e| anyhow!("Failed to send layout request: {}", e))?;
724
725            // Wait for response with matching transaction_id
726            let timeout = Duration::from_secs(10);
727            let start = std::time::Instant::now();
728            let mut layout: Option<HashMap<String, serde_json::Value>> = None;
729
730            while start.elapsed() < timeout {
731                match tokio::time::timeout(Duration::from_secs(1), read.next()).await {
732                    Ok(Some(Ok(Message::Text(text)))) => {
733                        if let Ok(response) = serde_json::from_str::<CommandMessage>(&text) {
734                            if response.transaction_id == transaction_id {
735                                if !response.success {
736                                    return Err(anyhow!("Server error: {}", response.error_message));
737                                }
738                                layout = Some(serde_json::from_value(response.data)?);
739                                break;
740                            }
741                            // Skip broadcasts and other messages
742                            if response.message_type == MessageType::Broadcast {
743                                continue;
744                            }
745                        }
746                    }
747                    Ok(Some(Ok(_))) => continue,
748                    Ok(Some(Err(e))) => return Err(anyhow!("WebSocket error: {}", e)),
749                    Ok(None) => return Err(anyhow!("Server closed connection")),
750                    Err(_) => continue, // Timeout on single read, keep trying
751                }
752            }
753
754            let layout = layout.ok_or_else(|| anyhow!("Timeout waiting for layout response"))?;
755            log::info!("Layout received with {} entries.", layout.len());
756
757            // Set up channels and background tasks for shared WebSocket access.
758            // This allows both the control loop (gm.write) and CommandClient (IPC
759            // commands) to share the write half, while routing incoming responses
760            // to the CommandClient.
761            let (ws_write_tx, mut ws_write_rx) = tokio::sync::mpsc::unbounded_channel::<String>();
762            let (response_tx, response_rx) = tokio::sync::mpsc::unbounded_channel::<CommandMessage>();
763
764            // Background task: WS write loop
765            // Reads serialized messages from ws_write_rx and sends them over the WebSocket.
766            tokio::spawn(async move {
767                while let Some(msg_json) = ws_write_rx.recv().await {
768                    if let Err(e) = write.send(Message::Text(msg_json)).await {
769                        log::error!("WebSocket write error: {}", e);
770                        break;
771                    }
772                }
773            });
774
775            // Background task: WS read loop
776            // Reads all incoming WebSocket messages. Routes Response and
777            // Broadcast messages to response_tx; CommandClient dispatches
778            // them onward (responses by transaction_id, broadcasts by
779            // topic into per-topic buffers that subscribers drain via
780            // `take_broadcasts`).
781            tokio::spawn(async move {
782                while let Some(result) = read.next().await {
783                    match result {
784                        Ok(Message::Text(text)) => {
785                            if let Ok(msg) = serde_json::from_str::<CommandMessage>(&text) {
786                                if matches!(
787                                    msg.message_type,
788                                    MessageType::Response | MessageType::Broadcast,
789                                ) {
790                                    if response_tx.send(msg).is_err() {
791                                        break; // receiver dropped
792                                    }
793                                }
794                                // Other message types are ignored
795                            }
796                        }
797                        Ok(Message::Close(_)) => {
798                            log::info!("WebSocket closed by server");
799                            break;
800                        }
801                        Err(e) => {
802                            log::error!("WebSocket read error: {}", e);
803                            break;
804                        }
805                        _ => {} // Ping/Pong/Binary - ignore
806                    }
807                }
808            });
809
810            // Construct CommandClient — owned by the runner, passed to the
811            // program via TickContext each cycle.
812            let mut command_client = CommandClient::new(ws_write_tx.clone(), response_rx);
813
814            // 2. Find Signal Offsets
815            let tick_offset = self.find_offset(&layout, &self.config.tick_signal_name)?;
816            let busy_offset = if let Some(name) = &self.config.busy_signal_name {
817                Some(self.find_offset(&layout, name)?)
818            } else {
819                None
820            };
821
822            // 4. Open Shared Memory
823            let shmem = ShmemConf::new().os_id(&self.config.shm_name).open()?;
824            let base_ptr = shmem.as_ptr();
825            log::info!("Shared Memory '{}' mapped.", self.config.shm_name);
826
827            // 4b. Validate the segment header BEFORE trusting any of its bytes.
828            // The server writes a self-describing header at offset 0; if the
829            // layout/ABI this program was built against doesn't match the live
830            // segment, refuse to run rather than read/write mismapped memory.
831            {
832                use mechutil::shm_header::{ShmHeader, SHM_HEADER_SIZE, SHM_MAGIC, SHM_FORMAT_VERSION};
833                if shmem.len() < SHM_HEADER_SIZE {
834                    anyhow::bail!(
835                        "GM segment '{}' is {} bytes, smaller than the {}-byte header — \
836                         server is too old or the segment is corrupt.",
837                        self.config.shm_name, shmem.len(), SHM_HEADER_SIZE);
838                }
839                let header: ShmHeader =
840                    unsafe { std::ptr::read_volatile(base_ptr as *const ShmHeader) };
841                if header.magic != SHM_MAGIC {
842                    anyhow::bail!(
843                        "GM segment '{}' has bad magic 0x{:016x} (expected 0x{:016x}) — \
844                         not an autocore segment, or an incompatible server.",
845                        self.config.shm_name, header.magic, SHM_MAGIC);
846                }
847                if header.format_version != SHM_FORMAT_VERSION {
848                    anyhow::bail!(
849                        "INCOMPATIBLE GM segment: header format v{} but this control program \
850                         expects v{}. Rebuild server and control against the same mechutil.",
851                        header.format_version, SHM_FORMAT_VERSION);
852                }
853                if header.shm_abi_version != mechutil::SHM_ABI_VERSION {
854                    anyhow::bail!(
855                        "INCOMPATIBLE GM segment: server SHM ABI v{} but this control program was \
856                         built against mechutil ABI v{} ({}). Rebuild the control program against \
857                         the server's mechutil version.",
858                        header.shm_abi_version, mechutil::SHM_ABI_VERSION, mechutil::VERSION);
859                }
860                if header.layout_hash != <P::Memory as GmCompat>::LAYOUT_HASH {
861                    anyhow::bail!(
862                        "INCOMPATIBLE GM layout: server segment hash 0x{:016x} != control hash \
863                         0x{:016x}. The server is serving a stale layout — RESTART autocore-server \
864                         to apply the pushed control program.",
865                        header.layout_hash, <P::Memory as GmCompat>::LAYOUT_HASH);
866                }
867                if header.total_size as usize != <P::Memory as GmCompat>::TOTAL_SIZE {
868                    anyhow::bail!(
869                        "INCOMPATIBLE GM size: server segment {} bytes != control {} bytes — \
870                         rebuild/restart required.",
871                        header.total_size, <P::Memory as GmCompat>::TOTAL_SIZE);
872                }
873                log::info!(
874                    "GM compatibility OK (layout 0x{:016x}, abi {}, {} bytes).",
875                    header.layout_hash, header.shm_abi_version, header.total_size);
876            }
877
878            // Wait for the server to finish applying initial values before we
879            // read SHM. Without this, our startup read can race with the
880            // server's initial-value writes; we would then clobber any
881            // initials when we write local_mem back below.
882            if let Some(ready_info) = layout.get("__ready__") {
883                let ready_offset = ready_info.get("offset")
884                    .and_then(|v| v.as_u64())
885                    .ok_or_else(|| anyhow!("__ready__ layout entry has no offset"))? as usize;
886                let ready_ptr = unsafe { base_ptr.add(ready_offset) as *const u32 };
887                let start = std::time::Instant::now();
888                let timeout = Duration::from_secs(10);
889                loop {
890                    let val = unsafe { std::ptr::read_volatile(ready_ptr) };
891                    if val == 1 {
892                        fence(Ordering::Acquire);
893                        log::info!("Server ready flag observed after {:?}", start.elapsed());
894                        break;
895                    }
896                    if start.elapsed() > timeout {
897                        log::warn!("Timed out waiting for server ready flag; proceeding anyway (initials may be zeroed)");
898                        break;
899                    }
900                    std::thread::sleep(Duration::from_millis(5));
901                }
902            } else {
903                log::warn!("No __ready__ flag in layout; server may predate ready-flag protocol. Initial values may race.");
904            }
905
906            // 5. Setup Pointers
907            // SAFETY: header validated above — the variable region begins at
908            // SHM_HEADER_SIZE and matches the generated GlobalMemory struct.
909            let gm = unsafe {
910                &mut *(base_ptr.add(mechutil::shm_header::SHM_HEADER_SIZE) as *mut P::Memory)
911            };
912
913            // Get tick event from shared memory
914            log::info!("Setting up tick event at offset {} (base_ptr: {:p})", tick_offset, base_ptr);
915            let (tick_event, _) = unsafe {
916                Event::from_existing(base_ptr.add(tick_offset))
917            }.map_err(|e| anyhow!("Failed to open tick event: {:?}", e))?;
918            log::info!("Tick event ready");
919
920            // Busy signal event (optional)
921            let busy_event = busy_offset.map(|offset| {
922                unsafe { Event::from_existing(base_ptr.add(offset)) }
923                    .map(|(event, _)| event)
924                    .ok()
925            }).flatten();
926
927            // 6. Initialize local memory buffer and user program
928            // We use a local copy for the control loop to ensure:
929            // - Consistent snapshot of inputs at start of cycle
930            // - Atomic commit of outputs at end of cycle
931            // - Proper memory barriers for cross-process visibility
932            let mut local_mem: P::Memory = unsafe { std::ptr::read_volatile(gm) };
933            let mut prev_mem: P::Memory = local_mem; // Snapshot for change detection
934
935            fence(Ordering::Acquire); // Ensure we see all prior writes from other processes
936
937            self.program.initialize(&mut local_mem);
938
939            // Write back any changes from initialize
940            fence(Ordering::Release);
941            unsafe { std::ptr::write_volatile(gm, local_mem) };
942
943            // Set up signal handler for graceful shutdown
944            let running = Arc::new(AtomicBool::new(true));
945            let r = running.clone();
946            
947            // Only set handler if not already set
948            if let Err(e) = ctrlc::set_handler(move || {
949                r.store(false, Ordering::SeqCst);
950            }) {
951                log::warn!("Failed to set signal handler: {}", e);
952            }
953
954            log::info!("Entering Control Loop - waiting for first tick...");
955            let mut cycle_count: u64 = 0;
956            let mut consecutive_timeouts: u32 = 0;
957
958            while running.load(Ordering::SeqCst) {
959                // Wait for Tick - Event-based synchronization
960                // Use a timeout (1s) to allow checking the running flag periodically
961                match tick_event.wait(Timeout::Val(Duration::from_secs(1))) {
962                    Ok(_) => {
963                        consecutive_timeouts = 0;
964                    },
965                    Err(e) => {
966                        // Check for timeout
967                        let err_str = format!("{:?}", e);
968                        if err_str.contains("Timeout") {
969                            consecutive_timeouts += 1;
970                            if consecutive_timeouts == 10 {
971                                log::error!(
972                                    "TICK STALL: {} consecutive timeouts! cycle={} pending={} responses={} fds={} rss_kb={}",
973                                    consecutive_timeouts,
974                                    cycle_count,
975                                    command_client.pending_count(),
976                                    command_client.response_count(),
977                                    diagnostics::count_open_fds(),
978                                    diagnostics::get_rss_kb(),
979                                );
980                            }
981                            if consecutive_timeouts > 10 && consecutive_timeouts % 60 == 0 {
982                                log::error!(
983                                    "TICK STALL continues: {} consecutive timeouts, cycle={}",
984                                    consecutive_timeouts,
985                                    cycle_count,
986                                );
987                            }
988                            continue;
989                        }
990                        return Err(anyhow!("Tick wait failed: {:?}", e));
991                    }
992                }
993
994                if !running.load(Ordering::SeqCst) {
995                    log::info!("Shutdown signal received, exiting control loop.");
996                    break;
997                }
998
999                cycle_count += 1;
1000                if cycle_count == 1 {
1001                    log::info!("First tick received!");
1002                }
1003
1004                // // Periodic diagnostics (every 30s at 100 Hz)
1005                // if cycle_count % 3000 == 0 {
1006                //     log::info!(
1007                //         "DIAG cycle={} pending={} responses={} fds={} rss_kb={}",
1008                //         cycle_count,
1009                //         command_client.pending_count(),
1010                //         command_client.response_count(),
1011                //         diagnostics::count_open_fds(),
1012                //         diagnostics::get_rss_kb(),
1013                //     );
1014                // }
1015
1016                // === INPUT PHASE ===
1017                // Read all variables from shared memory into local buffer.
1018                // This gives us a consistent snapshot of inputs for this cycle.
1019                // Acquire fence ensures we see all writes from other processes (server, modules).
1020                local_mem = unsafe { std::ptr::read_volatile(gm) };
1021                
1022                // Update prev_mem before execution to track changes made IN THIS CYCLE
1023                // Actually, we want to know what changed in SHM relative to what we last knew,
1024                // OR what WE changed relative to what we read?
1025                // The user wants "writes on shared variables" to be broadcast.
1026                // Typically outputs.
1027                // If inputs changed (from other source), broadcasting them again is fine too.
1028                // Let's capture state BEFORE execution (which is what we just read from SHM).
1029                prev_mem = local_mem;
1030
1031                fence(Ordering::Acquire);
1032
1033                // Unpack bit-mapped variables from their source words.
1034                local_mem.unpack_bits();
1035
1036                // Snapshot after unpack — used by pack_bits to detect which
1037                // bools the control program actually changed.
1038                let pre_tick = local_mem;
1039
1040                // === EXECUTE PHASE ===
1041                // Poll IPC responses so they are available during process_tick.
1042                command_client.poll();
1043
1044                // Execute user logic on the local copy.
1045                // All reads/writes during process_tick operate on local_mem.
1046                let mut ctx = TickContext {
1047                    gm: &mut local_mem,
1048                    client: &mut command_client,
1049                    cycle: cycle_count,
1050                };
1051                self.program.process_tick(&mut ctx);
1052
1053                // === OUTPUT PHASE ===
1054                // Pack bit-mapped variables back into their source words,
1055                // but only for sources where a mapped bool actually changed.
1056                local_mem.pack_bits(&pre_tick);
1057
1058                // Write all variables from local buffer back to shared memory.
1059                // Release fence ensures our writes are visible to other processes.
1060                fence(Ordering::Release);
1061                unsafe { std::ptr::write_volatile(gm, local_mem) };
1062
1063                // === CHANGE DETECTION & NOTIFICATION ===
1064                let changes = local_mem.get_changes(&prev_mem);
1065                if !changes.is_empty() {
1066                    // Construct bulk write message
1067                    let mut data_map = serde_json::Map::new();
1068                    for (key, val) in changes {
1069                        data_map.insert(key.to_string(), val);
1070                    }
1071                    
1072                    let msg = CommandMessage::request("gm.write", serde_json::Value::Object(data_map));
1073                    let msg_json = serde_json::to_string(&msg).unwrap_or_default();
1074
1075                    // Send via the shared write channel (non-blocking)
1076                    if let Err(e) = ws_write_tx.send(msg_json) {
1077                        log::error!("Failed to send updates: {}", e);
1078                    }
1079                }
1080
1081                // Signal Busy/Done event
1082                if let Some(ref busy_ev) = busy_event {
1083                    let _ = busy_ev.set(EventState::Signaled);
1084                }
1085            }
1086
1087            Ok(())
1088        })
1089    }
1090
1091    fn find_offset(&self, layout: &HashMap<String, serde_json::Value>, name: &str) -> Result<usize> {
1092        let info = layout.get(name).ok_or_else(|| anyhow!("Signal '{}' not found in layout", name))?;
1093        info.get("offset")
1094            .and_then(|v| v.as_u64())
1095            .map(|v| v as usize)
1096            .ok_or_else(|| anyhow!("Invalid offset for '{}'", name))
1097    }
1098}
1099
1100/// Generates the standard `main` function for a control program.
1101///
1102/// This macro reduces boilerplate by creating a properly configured `main`
1103/// function that initializes and runs your control program.
1104///
1105/// # Arguments
1106///
1107/// * `$prog_type` - The type of your control program (must implement [`ControlProgram`])
1108/// * `$shm_name` - The shared memory segment name (string literal)
1109/// * `$tick_signal` - The tick signal name in shared memory (string literal)
1110///
1111/// # Example
1112///
1113/// ```ignore
1114/// mod gm;
1115/// use gm::GlobalMemory;
1116///
1117/// pub struct MyProgram;
1118///
1119/// impl MyProgram {
1120///     pub fn new() -> Self { Self }
1121/// }
1122///
1123/// impl autocore_std::ControlProgram for MyProgram {
1124///     type Memory = GlobalMemory;
1125///
1126///     fn process_tick(&mut self, ctx: &mut autocore_std::TickContext<Self::Memory>) {
1127///         // Your logic here
1128///     }
1129/// }
1130///
1131/// // This generates the main function
1132/// autocore_std::autocore_main!(MyProgram, "my_project_shm", "tick");
1133/// ```
1134///
1135/// # Generated Code
1136///
1137/// The macro expands to:
1138///
1139/// ```ignore
1140/// fn main() -> anyhow::Result<()> {
1141///     let config = autocore_std::RunnerConfig {
1142///         server_host: "127.0.0.1".to_string(),
1143///         ws_port: autocore_std::DEFAULT_WS_PORT,
1144///         module_name: "control".to_string(),
1145///         shm_name: "my_project_shm".to_string(),
1146///         tick_signal_name: "tick".to_string(),
1147///         busy_signal_name: None,
1148///         log_level: log::LevelFilter::Info,
1149///         log_udp_port: autocore_std::logger::DEFAULT_LOG_UDP_PORT,
1150///     };
1151///
1152///     autocore_std::ControlRunner::new(MyProgram::new())
1153///         .config(config)
1154///         .run()
1155/// }
1156/// ```
1157#[macro_export]
1158macro_rules! autocore_main {
1159    ($prog_type:ty, $shm_name:expr, $tick_signal:expr) => {
1160        fn main() -> anyhow::Result<()> {
1161            let config = autocore_std::RunnerConfig {
1162                server_host: "127.0.0.1".to_string(),
1163                ws_port: autocore_std::DEFAULT_WS_PORT,
1164                module_name: "control".to_string(),
1165                shm_name: $shm_name.to_string(),
1166                tick_signal_name: $tick_signal.to_string(),
1167                busy_signal_name: None,
1168                log_level: log::LevelFilter::Info,
1169                log_udp_port: autocore_std::logger::DEFAULT_LOG_UDP_PORT,
1170            };
1171
1172            // `env!("CARGO_PKG_VERSION")` expands in the CONTROL crate (where
1173            // this macro is invoked), so it captures the control program's own
1174            // version — which `acctl push control` auto-bumps — not
1175            // autocore-std's. Surfaced via `--print-compat` for display.
1176            autocore_std::ControlRunner::new(<$prog_type>::new())
1177                .config(config)
1178                .version(env!("CARGO_PKG_VERSION"))
1179                .run()
1180        }
1181    };
1182}
1183