rust_ethernet_ip/lib.rs
1//! EtherNet/IP client library for Allen-Bradley CompactLogix and ControlLogix PLCs.
2//!
3//! `rust-ethernet-ip` provides async Rust APIs for explicit EtherNet/IP and CIP
4//! tag operations, plus FFI surfaces used by the repository's .NET wrapper.
5//! The current released crate line is `1.2.0`.
6//!
7//! ## Highlights
8//!
9//! - Async client API via [`EipClient`]
10//! - Symbolic tag addressing, including program-scoped tags, array indexing, and nested UDT paths
11//! - Batch reads, writes, and mixed execution with [`BatchOperation`]
12//! - Route-path support for backplane and routed topologies via [`RoutePath`]
13//! - UDT discovery, schema export, diagnostics, subscriptions, and tag-group polling
14//!
15//! ## Quick Start
16//!
17//! ```no_run
18//! use rust_ethernet_ip::{EipClient, PlcValue};
19//!
20//! #[tokio::main]
21//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
22//! let mut client = EipClient::connect("192.168.1.100:44818").await?;
23//! let running = client.read_tag("Program:Main.MotorRunning").await?;
24//! client
25//! .write_tag("Program:Main.SetPoint", PlcValue::Dint(1500))
26//! .await?;
27//!
28//! println!("running={running:?}");
29//! Ok(())
30//! }
31//! ```
32//!
33//! Routed example:
34//!
35//! ```no_run
36//! use rust_ethernet_ip::{EipClient, RoutePath};
37//!
38//! #[tokio::main]
39//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
40//! let route = RoutePath::new().add_slot(0);
41//! let _client = EipClient::with_route_path("192.168.1.100:44818", route).await?;
42//! Ok(())
43//! }
44//! ```
45//!
46//! ## Known PLC/Firmware Limits
47//!
48//! Earlier release lines classified several direct write shapes as
49//! controller/firmware limitations. Hardware probes on 2026-07-02 through
50//! 2026-07-08 corrected that picture, and the fixes ship in this line.
51//! Real-hardware validation for the `1.2.0` release line (CompactLogix
52//! 5069-L330ERM fw38, full-coverage across Rust/C#/Python/C++) confirmed:
53//!
54//! - Standalone standard Logix `STRING` tags write directly using the standard
55//! `0x02A0`/`0x0FCE` structure encoding.
56//! - Scalar UDT array element members (DINT/REAL/BOOL/INT) write directly when
57//! the full member path is preserved.
58//! - `STRING` members inside UDTs and UDT array elements — including **custom
59//! string types** with a user-defined name/length (e.g. `Str82`, `Str400`) —
60//! write directly: the library discovers the target's real structure handle
61//! instead of assuming the built-in `0x0FCE`. A `0x2107` Read/Write Tag
62//! data-type mismatch here now indicates a genuine type mismatch, not a
63//! firmware block.
64//! - Strings/structures larger than one CIP packet are read and written via
65//! CIP Read/Write Tag Fragmented (`0x52`/`0x53`).
66//!
67//! Remaining limits: whole-UDT *array-element* writes as a single structure are
68//! not supported (update members individually), and `read_tag` returns custom
69//! string types as [`PlcValue::Udt`] — use `read_string_tag` when the tag is
70//! known to be a string. See `docs/STRING_HANDLING.md` for size limits per tag
71//! scope.
72
73#![deny(unused_must_use, unsafe_op_in_unsafe_fn)]
74#![cfg_attr(not(test), warn(clippy::print_stdout, clippy::dbg_macro))]
75
76use tokio::io::{AsyncRead, AsyncWrite};
77
78/// Trait for streams that can be used with EipClient
79///
80/// This trait combines the requirements for streams used with EtherNet/IP:
81/// - `AsyncRead`: Read data from the stream
82/// - `AsyncWrite`: Write data to the stream
83/// - `Unpin`: Required for async operations
84/// - `Send`: Required for cross-thread safety
85///
86/// Most tokio streams (like `TcpStream`, `UnixStream`, etc.) automatically
87/// implement this trait. You can also implement it for custom stream wrappers
88/// to add metrics, logging, or other functionality.
89///
90/// # Example
91///
92/// ```no_run
93/// use rust_ethernet_ip::EtherNetIpStream;
94/// use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
95///
96/// // Custom stream wrapper for metrics
97/// struct MetricsStream<S> {
98/// inner: S,
99/// bytes_read: u64,
100/// bytes_written: u64,
101/// }
102///
103/// // Most tokio streams automatically implement EtherNetIpStream
104/// // For example, TcpStream implements it:
105/// use tokio::net::TcpStream;
106/// // TcpStream: AsyncRead + AsyncWrite + Unpin + Send ✓
107/// // Therefore: TcpStream implements EtherNetIpStream ✓
108/// ```
109pub trait EtherNetIpStream: AsyncRead + AsyncWrite + Unpin + Send {}
110
111impl<S> EtherNetIpStream for S where S: AsyncRead + AsyncWrite + Unpin + Send {}
112
113pub mod batch;
114pub mod client;
115pub mod config; // Production-ready configuration management
116pub mod error;
117#[cfg(feature = "ffi")]
118pub mod ffi;
119pub mod fleet;
120pub mod monitoring; // Enterprise-grade monitoring and health checks
121pub mod plc_manager;
122pub(crate) mod protocol;
123pub mod route;
124pub mod schema;
125pub mod subscription;
126pub mod tag_group;
127pub mod tag_manager;
128pub mod tag_path;
129pub mod types;
130pub mod udt;
131pub mod version;
132
133// Re-export commonly used items
134pub use batch::{BatchConfig, BatchError, BatchOperation, BatchResult};
135pub use client::{Backoff, Client, ConnectionEvent, EipClient, RetryClient, RetryPolicy};
136#[expect(
137 deprecated,
138 reason = "CODEX-AQ intentionally re-exports ProductionConfig for 1.x compatibility"
139)]
140pub use config::ProductionConfig;
141pub use config::{
142 ConnectionConfig, LogFormat, LogLevel, LogRotationSchedule, LoggingConfig, MonitoringConfig,
143 PerformanceConfig, PlcSpecificConfig, SecurityConfig,
144};
145pub use error::{EtherNetIpError, Result};
146pub use fleet::{Fleet, FleetEvent};
147#[expect(
148 deprecated,
149 reason = "CODEX-AQ intentionally re-exports ProductionMonitor for 1.x compatibility"
150)]
151pub use monitoring::ProductionMonitor;
152pub use monitoring::{
153 ConnectionMetrics, DiagnosticsSnapshot, ErrorCategory, ErrorMetrics, HealthCheckMode,
154 HealthMetrics, HealthStatus, MonitoringMetrics, OperationMetrics, PerformanceMetrics,
155};
156#[expect(
157 deprecated,
158 reason = "CODEX-AQ intentionally re-exports PlcManager for 1.x compatibility"
159)]
160pub use plc_manager::PlcManager;
161pub use plc_manager::{PlcConfig, PlcConnection};
162pub use route::{RouteHop, RoutePath};
163pub use schema::{
164 SchemaCapabilities, SchemaDataType, SchemaExport, SchemaLibraryInfo, SchemaRoutePath,
165 SchemaScope, SchemaTag, SchemaTargetInfo, SchemaUdt, SchemaUdtMember,
166};
167#[expect(
168 deprecated,
169 reason = "CODEX-AQ intentionally re-exports SubscriptionManager for 1.x compatibility"
170)]
171pub use subscription::SubscriptionManager;
172#[expect(
173 deprecated,
174 reason = "CODEX-AQ intentionally re-exports RealTimeSubscriptionManager for 1.x compatibility"
175)]
176pub use subscription::SubscriptionManager as RealTimeSubscriptionManager;
177pub use subscription::{
178 SubscriptionOptions, SubscriptionOptions as RealTimeSubscriptionOptions, TagSubscription,
179 TagSubscription as RealTimeSubscription, TagSubscriptionEvent,
180};
181pub use tag_group::{
182 TagGroupConfig, TagGroupEvent, TagGroupEventKind, TagGroupFailureCategory,
183 TagGroupFailureDiagnostic, TagGroupSnapshot, TagGroupSubscription, TagGroupValueResult,
184};
185#[expect(
186 deprecated,
187 reason = "CODEX-AQ intentionally re-exports TagCache for 1.x compatibility"
188)]
189pub use tag_manager::TagCache;
190pub use tag_manager::{TagManager, TagMetadata, TagPermissions, TagScope};
191pub use tag_path::TagPath;
192pub use types::{PlcValue, UdtData};
193pub use udt::{TagAttributes, UdtDefinition, UdtMember, UdtTemplate};
194
195#[cfg(feature = "ffi")]
196pub(crate) use client::RUNTIME;
197
198/// Initialize tracing subscriber with environment-based filtering
199///
200/// This function sets up the tracing subscriber to use the `RUST_LOG` environment variable
201/// for log level filtering. If not called, tracing events will be ignored.
202///
203/// # Examples
204///
205/// ```no_run
206/// use rust_ethernet_ip::init_tracing;
207///
208/// // Initialize with default settings (reads RUST_LOG env var)
209/// init_tracing();
210///
211/// // Or set RUST_LOG before calling:
212/// // RUST_LOG=debug cargo run
213/// ```
214///
215/// # Log Levels
216///
217/// Set the `RUST_LOG` environment variable to control logging:
218/// - `RUST_LOG=trace` - Most verbose (all events)
219/// - `RUST_LOG=debug` - Debug information
220/// - `RUST_LOG=info` - Informational messages (default)
221/// - `RUST_LOG=warn` - Warnings only
222/// - `RUST_LOG=error` - Errors only
223/// - `RUST_LOG=rust_ethernet_ip=debug` - Debug for this crate only
224///
225/// # Panics
226///
227/// This function will panic if called more than once. Use `try_init_tracing()` for
228/// non-panicking initialization.
229pub fn init_tracing() {
230 use tracing_subscriber::EnvFilter;
231 use tracing_subscriber::fmt;
232
233 let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
234
235 let subscriber = fmt::Subscriber::builder()
236 .with_env_filter(filter)
237 .with_target(false) // Don't show module paths by default
238 .finish();
239
240 tracing::subscriber::set_global_default(subscriber).expect("Failed to set tracing subscriber");
241}
242
243/// Try to initialize tracing subscriber (non-panicking version)
244///
245/// Returns `Ok(())` if initialization was successful, or an error if a subscriber
246/// was already set.
247pub fn try_init_tracing() -> crate::error::Result<()> {
248 use tracing_subscriber::EnvFilter;
249 use tracing_subscriber::fmt;
250
251 let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
252
253 let subscriber = fmt::Subscriber::builder()
254 .with_env_filter(filter)
255 .with_target(false)
256 .finish();
257
258 tracing::subscriber::set_global_default(subscriber)
259 .map_err(|e| crate::error::EtherNetIpError::Other(e.to_string()))?;
260 Ok(())
261}