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