Skip to main content

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}