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.1`.
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.1` 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#![deny(missing_docs)]
75#![cfg_attr(not(test), warn(clippy::print_stdout, clippy::dbg_macro))]
76
77use tokio::io::{AsyncRead, AsyncWrite};
78
79/// Trait for streams that can be used with EipClient
80///
81/// This trait combines the requirements for streams used with EtherNet/IP:
82/// - `AsyncRead`: Read data from the stream
83/// - `AsyncWrite`: Write data to the stream
84/// - `Unpin`: Required for async operations
85/// - `Send`: Required for cross-thread safety
86///
87/// Most tokio streams (like `TcpStream`, `UnixStream`, etc.) automatically
88/// implement this trait. You can also implement it for custom stream wrappers
89/// to add metrics, logging, or other functionality.
90///
91/// # Example
92///
93/// ```no_run
94/// use rust_ethernet_ip::EtherNetIpStream;
95/// use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
96///
97/// // Custom stream wrapper for metrics
98/// struct MetricsStream<S> {
99/// inner: S,
100/// bytes_read: u64,
101/// bytes_written: u64,
102/// }
103///
104/// // Most tokio streams automatically implement EtherNetIpStream
105/// // For example, TcpStream implements it:
106/// use tokio::net::TcpStream;
107/// // TcpStream: AsyncRead + AsyncWrite + Unpin + Send ✓
108/// // Therefore: TcpStream implements EtherNetIpStream ✓
109/// ```
110pub trait EtherNetIpStream: AsyncRead + AsyncWrite + Unpin + Send {}
111
112impl<S> EtherNetIpStream for S where S: AsyncRead + AsyncWrite + Unpin + Send {}
113
114/// Batch operation types and packet-grouping configuration.
115pub mod batch;
116/// Async EtherNet/IP clients and high-level tag operations.
117pub mod client;
118/// Legacy production configuration models.
119pub mod config; // Production-ready configuration management
120/// Error and result types.
121pub mod error;
122#[cfg(feature = "ffi")]
123/// Stable C ABI used by the C/C++, C#, and Python bindings.
124pub mod ffi;
125/// Multi-controller client collection and event forwarding.
126pub mod fleet;
127/// Connection, operation, performance, error, and health metrics.
128pub mod monitoring; // Enterprise-grade monitoring and health checks
129/// Legacy PLC connection manager.
130pub mod plc_manager;
131pub(crate) mod protocol;
132/// Ordered CIP routing paths for backplane and network hops.
133pub mod route;
134/// Portable controller tag and UDT schema export models.
135pub mod schema;
136/// Legacy single-tag subscription primitives.
137pub mod subscription;
138/// Named tag-group polling and subscription models.
139pub mod tag_group;
140/// Tag metadata discovery and caching.
141pub mod tag_manager;
142/// Structured symbolic Logix tag paths.
143pub mod tag_path;
144/// Values exchanged with Logix tags.
145pub mod types;
146/// User-defined type metadata and conversion helpers.
147pub mod udt;
148/// Library, ABI, capability, and build version metadata.
149pub mod version;
150
151// Re-export commonly used items
152pub use batch::{BatchConfig, BatchError, BatchOperation, BatchResult};
153pub use client::{Backoff, Client, ConnectionEvent, EipClient, RetryClient, RetryPolicy};
154#[expect(
155 deprecated,
156 reason = "CODEX-AQ intentionally re-exports ProductionConfig for 1.x compatibility"
157)]
158pub use config::ProductionConfig;
159pub use config::{
160 ConnectionConfig, LogFormat, LogLevel, LogRotationSchedule, LoggingConfig, MonitoringConfig,
161 PerformanceConfig, PlcSpecificConfig, SecurityConfig,
162};
163pub use error::{EtherNetIpError, Result};
164pub use fleet::{Fleet, FleetEvent};
165#[expect(
166 deprecated,
167 reason = "CODEX-AQ intentionally re-exports ProductionMonitor for 1.x compatibility"
168)]
169pub use monitoring::ProductionMonitor;
170pub use monitoring::{
171 ConnectionMetrics, DiagnosticsSnapshot, ErrorCategory, ErrorMetrics, HealthCheckMode,
172 HealthMetrics, HealthStatus, MonitoringMetrics, OperationMetrics, PerformanceMetrics,
173 SchemaCacheMetrics,
174};
175#[expect(
176 deprecated,
177 reason = "CODEX-AQ intentionally re-exports PlcManager for 1.x compatibility"
178)]
179pub use plc_manager::PlcManager;
180pub use plc_manager::{PlcConfig, PlcConnection};
181pub use route::{RouteHop, RoutePath};
182pub use schema::{
183 SchemaCapabilities, SchemaDataType, SchemaExport, SchemaLibraryInfo, SchemaRoutePath,
184 SchemaScope, SchemaTag, SchemaTargetInfo, SchemaUdt, SchemaUdtMember,
185};
186#[expect(
187 deprecated,
188 reason = "CODEX-AQ intentionally re-exports SubscriptionManager for 1.x compatibility"
189)]
190pub use subscription::SubscriptionManager;
191#[expect(
192 deprecated,
193 reason = "CODEX-AQ intentionally re-exports RealTimeSubscriptionManager for 1.x compatibility"
194)]
195pub use subscription::SubscriptionManager as RealTimeSubscriptionManager;
196pub use subscription::{
197 SubscriptionOptions, SubscriptionOptions as RealTimeSubscriptionOptions, TagSubscription,
198 TagSubscription as RealTimeSubscription, TagSubscriptionEvent,
199};
200pub use tag_group::{
201 TagGroupConfig, TagGroupEvent, TagGroupEventKind, TagGroupFailureCategory,
202 TagGroupFailureDiagnostic, TagGroupSnapshot, TagGroupSubscription, TagGroupValueResult,
203};
204#[expect(
205 deprecated,
206 reason = "CODEX-AQ intentionally re-exports TagCache for 1.x compatibility"
207)]
208pub use tag_manager::TagCache;
209pub use tag_manager::{TagManager, TagMetadata, TagPermissions, TagScope};
210pub use tag_path::TagPath;
211pub use types::{PlcValue, UdtData};
212pub use udt::{TagAttributes, UdtDefinition, UdtMember, UdtTemplate};
213
214#[cfg(feature = "ffi")]
215pub(crate) use client::RUNTIME;
216
217/// Initialize tracing subscriber with environment-based filtering
218///
219/// This function sets up the tracing subscriber to use the `RUST_LOG` environment variable
220/// for log level filtering. If not called, tracing events will be ignored.
221///
222/// # Examples
223///
224/// ```no_run
225/// use rust_ethernet_ip::init_tracing;
226///
227/// // Initialize with default settings (reads RUST_LOG env var)
228/// init_tracing();
229///
230/// // Or set RUST_LOG before calling:
231/// // RUST_LOG=debug cargo run
232/// ```
233///
234/// # Log Levels
235///
236/// Set the `RUST_LOG` environment variable to control logging:
237/// - `RUST_LOG=trace` - Most verbose (all events)
238/// - `RUST_LOG=debug` - Debug information
239/// - `RUST_LOG=info` - Informational messages (default)
240/// - `RUST_LOG=warn` - Warnings only
241/// - `RUST_LOG=error` - Errors only
242/// - `RUST_LOG=rust_ethernet_ip=debug` - Debug for this crate only
243///
244/// # Panics
245///
246/// This function will panic if called more than once. Use `try_init_tracing()` for
247/// non-panicking initialization.
248pub fn init_tracing() {
249 use tracing_subscriber::EnvFilter;
250 use tracing_subscriber::fmt;
251
252 let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
253
254 let subscriber = fmt::Subscriber::builder()
255 .with_env_filter(filter)
256 .with_target(false) // Don't show module paths by default
257 .finish();
258
259 tracing::subscriber::set_global_default(subscriber).expect("Failed to set tracing subscriber");
260}
261
262/// Try to initialize tracing subscriber (non-panicking version)
263///
264/// Returns `Ok(())` if initialization was successful, or an error if a subscriber
265/// was already set.
266pub fn try_init_tracing() -> crate::error::Result<()> {
267 use tracing_subscriber::EnvFilter;
268 use tracing_subscriber::fmt;
269
270 let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
271
272 let subscriber = fmt::Subscriber::builder()
273 .with_env_filter(filter)
274 .with_target(false)
275 .finish();
276
277 tracing::subscriber::set_global_default(subscriber)
278 .map_err(|e| crate::error::EtherNetIpError::Other(e.to_string()))?;
279 Ok(())
280}