rust_ethernet_ip/lib.rs
1// lib.rs - Rust EtherNet/IP Driver Library with Comprehensive Documentation
2// =========================================================================
3//
4// # Rust EtherNet/IP Driver Library v0.6.2
5//
6// A high-performance, production-ready EtherNet/IP communication library for
7// Allen-Bradley CompactLogix and ControlLogix PLCs, written in pure Rust with
8// comprehensive C# language bindings.
9//
10// ## Overview
11//
12// This library provides a complete implementation of the EtherNet/IP protocol
13// and Common Industrial Protocol (CIP) for communicating with Allen-Bradley
14// CompactLogix and ControlLogix series PLCs. It offers native Rust APIs, comprehensive
15// language bindings, and production-ready features for enterprise deployment.
16//
17// ## Architecture
18//
19// ```text
20// ┌─────────────────────────────────────────────────────────────────────────────────┐
21// │ Application Layer │
22// │ ┌─────────────┐ ┌─────────────────────────────────────────────────────────┐ │
23// │ │ Rust │ │ C# Ecosystem │ │
24// │ │ Native │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ │ │
25// │ │ │ │ │ WPF │ │ WinForms │ │ ASP.NET Core │ │ │
26// │ │ │ │ │ Desktop │ │ Desktop │ │ Web API │ │ │
27// │ │ │ │ └─────────────┘ └─────────────┘ └─────────┬───────┘ │ │
28// │ │ │ │ │ │ │
29// │ │ │ │ ┌─────────┴───────┐ │ │
30// │ │ │ │ │ TypeScript + │ │ │
31// │ │ │ │ │ React Frontend │ │ │
32// │ │ │ │ │ (HTTP/REST) │ │ │
33// │ │ │ │ └─────────────────┘ │ │
34// │ └─────────────┘ └─────────────────────────────────────────────────────────┘ │
35// └─────────────────────┬─────────────────────────────────────────────────────────┘
36// │
37// ┌─────────────────────┴─────────────────────────────────────────────────────────┐
38// │ Language Wrappers │
39// │ ┌─────────────┐ │
40// │ │ C# FFI │ │
41// │ │ Wrapper │ │
42// │ │ │ │
43// │ │ • 22 funcs │ │
44// │ │ • Type-safe │ │
45// │ │ • Cross-plat│ │
46// │ └─────────────┘ │
47// └─────────────────────┬─────────────────────────────────────────────────────────┘
48// │
49// ┌─────────────────────┴─────────────────────────────────────────────────────────┐
50// │ Core Rust Library │
51// │ ┌─────────────────────────────────────────────────────────────────────────┐ │
52// │ │ EipClient │ │
53// │ │ • Connection Management & Session Handling │ │
54// │ │ • Advanced Tag Operations & Program-Scoped Tag Support │ │
55// │ │ • Complete Data Type Support (13 Allen-Bradley types) │ │
56// │ │ • Advanced Tag Path Parsing (arrays, bits, UDTs, strings) │ │
57// │ │ • Real-Time Subscriptions with Event-Driven Notifications │ │
58// │ │ • High-Performance Batch Operations (2,000+ ops/sec) │ │
59// │ └─────────────────────────────────────────────────────────────────────────┘ │
60// │ ┌─────────────────────────────────────────────────────────────────────────┐ │
61// │ │ Protocol Implementation │ │
62// │ │ • EtherNet/IP Encapsulation Protocol │ │
63// │ │ • CIP (Common Industrial Protocol) │ │
64// │ │ • Symbolic Tag Addressing with Advanced Parsing │ │
65// │ │ • Comprehensive CIP Error Code Mapping │ │
66// │ └─────────────────────────────────────────────────────────────────────────┘ │
67// │ ┌─────────────────────────────────────────────────────────────────────────┐ │
68// │ │ Network Layer │ │
69// │ │ • TCP Socket Management with Connection Pooling │ │
70// │ │ • Async I/O with Tokio Runtime │ │
71// │ │ • Robust Error Handling & Network Resilience │ │
72// │ │ • Session Management & Automatic Reconnection │ │
73// │ └─────────────────────────────────────────────────────────────────────────┘ │
74// └─────────────────────────────────────────────────────────────────────────────────┘
75// ```
76//
77// ## Integration Paths
78//
79// ### 🦀 **Native Rust Applications**
80// Direct library usage with full async support and zero-overhead abstractions.
81// Perfect for high-performance applications and embedded systems.
82//
83// ### 🖥️ **Desktop Applications (C#)**
84// - **WPF**: Modern desktop applications with MVVM architecture
85// - **WinForms**: Traditional Windows applications with familiar UI patterns
86// - Uses C# FFI wrapper for seamless integration
87//
88// ### 🌐 **Web Applications**
89// - **ASP.NET Core Web API**: RESTful backend service
90// - **Scalable Architecture**: Backend handles PLC communication, frontend provides UI
91//
92// ### 🔧 **System Integration**
93// - **C/C++ Applications**: Direct FFI integration
94// - **Other .NET Languages**: VB.NET, F#, etc. via C# wrapper
95// - **Microservices**: ASP.NET Core API as a service component
96//
97// ## Features
98//
99// ### Core Capabilities
100// - **High Performance**: 2,000+ operations per second with batch operations
101// - **Real-Time Subscriptions**: Event-driven notifications with 1ms-10s intervals
102// - **Complete Data Types**: All Allen-Bradley native data types with type-safe operations
103// - **Advanced Tag Addressing**: Program-scoped, arrays, bits, UDTs, strings
104// - **Batch Operations**: High-performance multi-tag read/write with 2,000+ ops/sec
105// - **Async I/O**: Built on Tokio for excellent concurrency and performance
106// - **Error Handling**: Comprehensive CIP error code mapping and reporting
107// - **Memory Safe**: Zero-copy operations where possible, proper resource cleanup
108// - **Production Ready**: Enterprise-grade monitoring, health checks, and configuration
109//
110// ### Supported PLCs
111// - **CompactLogix L1x, L2x, L3x, L4x, L5x series** (Primary focus)
112// - **ControlLogix L6x, L7x, L8x series** (Full support)
113// - Optimized for PC applications (Windows, Linux, macOS)
114//
115// ### Advanced Tag Addressing
116// - **Program-scoped tags**: `Program:MainProgram.Tag1`
117// - **Array element access**: `MyArray[5]`, `MyArray[1,2,3]`
118// - **Bit-level operations**: `MyDINT.15` (access individual bits)
119// - **UDT member access**: `MyUDT.Member1.SubMember`
120// - **String operations**: `MyString.LEN`, `MyString.DATA[5]`
121// - **Complex nested paths**: `Program:Production.Lines[2].Stations[5].Motor.Status.15`
122//
123// ### Complete Data Type Support
124// - **BOOL**: Boolean values
125// - **SINT, INT, DINT, LINT**: Signed integers (8, 16, 32, 64-bit)
126// - **USINT, UINT, UDINT, ULINT**: Unsigned integers (8, 16, 32, 64-bit)
127// - **REAL, LREAL**: Floating point (32, 64-bit IEEE 754)
128// - **STRING**: Variable-length strings
129// - **UDT**: User Defined Types with full nesting support
130//
131// ### Protocol Support
132// - **EtherNet/IP**: Complete encapsulation protocol implementation
133// - **CIP**: Common Industrial Protocol for tag operations
134// - **Symbolic Addressing**: Direct tag name resolution with advanced parsing
135// - **Session Management**: Proper registration/unregistration sequences
136//
137// ### Integration Options
138// - **Native Rust**: Direct library usage with full async support
139// - **C# Desktop Applications**: WPF and WinForms via C# FFI wrapper
140// - **Web Applications**: ASP.NET Core API + TypeScript/React/Vue frontend
141// - **C/C++ Integration**: Direct FFI functions for system integration
142// - **Cross-Platform**: Windows, Linux, macOS support
143//
144// ## Performance Characteristics
145//
146// Benchmarked on typical industrial hardware:
147//
148// | Operation | Performance | Notes |
149// |-----------|-------------|-------|
150// | Read BOOL | 1,500+ ops/sec | Single tag operations |
151// | Read DINT | 1,400+ ops/sec | 32-bit integer tags |
152// | Read REAL | 1,300+ ops/sec | Floating point tags |
153// | Write BOOL | 800+ ops/sec | Single tag operations |
154// | Write DINT | 750+ ops/sec | 32-bit integer tags |
155// | Write REAL | 700+ ops/sec | Floating point tags |
156// | **Batch Read** | **2,000+ ops/sec** | **Multi-tag operations** |
157// | **Batch Write** | **1,500+ ops/sec** | **Multi-tag operations** |
158// | **Real-Time Subscriptions** | **1ms-10s intervals** | **Event-driven** |
159// | Connection | <1 second | Initial session setup |
160// | Tag Path Parsing | 10,000+ ops/sec | Advanced addressing |
161//
162// ## Security Considerations
163//
164// - **No Authentication**: EtherNet/IP protocol has limited built-in security
165// - **Network Level**: Implement firewall rules and network segmentation
166// - **PLC Protection**: Use PLC safety locks and access controls
167// - **Data Validation**: Always validate data before writing to PLCs
168//
169// ## Thread Safety
170//
171// The `EipClient` struct is **NOT** thread-safe. For multi-threaded applications:
172// - Use one client per thread, OR
173// - Implement external synchronization (Mutex/RwLock), OR
174// - Use a connection pool pattern
175//
176// ## Memory Usage
177//
178// - **Per Connection**: ~8KB base memory footprint
179// - **Network Buffers**: ~2KB per active connection
180// - **Tag Cache**: Minimal (tag names only when needed)
181// - **Total Typical**: <10MB for most applications
182//
183// ## Error Handling Philosophy
184//
185// This library follows Rust's error handling principles:
186// - All fallible operations return `Result<T, EtherNetIpError>`
187// - Errors are propagated rather than panicking
188// - Detailed error messages with CIP status code mapping
189// - Network errors are distinguished from protocol errors
190//
191// ## Known Limitations
192//
193// The following operations are not supported due to PLC firmware restrictions.
194// These limitations are inherent to the Allen-Bradley PLC firmware and cannot be
195// bypassed at the library level.
196//
197// ### STRING Tag Writing
198//
199// **Cannot write directly to STRING tags** (e.g., `gTest_STRING`).
200//
201// **Root Cause:** PLC firmware limitation (CIP Error 0x2107). The PLC rejects
202// direct write operations to STRING tags, regardless of the communication method used.
203//
204// **What Works:**
205// - Reading STRING tags: `gTest_STRING` (read successfully)
206// - Reading STRING members in UDTs: `gTestUDT.Member5_String` (read successfully)
207//
208// **What Doesn't Work:**
209// - Writing simple STRING tags: `gTest_STRING` (write fails - PLC limitation)
210// - Writing program-scoped STRING tags: `Program:TestProgram.gTest_STRING` (write fails)
211// - Writing STRING members in UDTs directly: `gTestUDT.Member5_String` (write fails)
212//
213// **Workaround for STRING Members in UDTs:**
214// If the STRING is part of a UDT structure, read the entire UDT, modify the STRING
215// member in memory, then write the entire UDT back. For standalone STRING tags,
216// there is no workaround at the communication library level.
217//
218// ### UDT Array Element Member Writing
219//
220// **Cannot write directly to members of UDT array elements** (e.g., `gTestUDT_Array[0].Member1_DINT`).
221//
222// **Root Cause:** PLC firmware limitation (CIP Error 0x2107). The PLC does not
223// support direct write operations to individual members within UDT array elements.
224//
225// **What Works:**
226// - Reading UDT array element members: `gTestUDT_Array[0].Member1_DINT` (read successfully)
227// - Writing entire UDT array elements: `gTestUDT_Array[0]` (write full UDT structure)
228// - Writing UDT members (non-array): `gTestUDT.Member1_DINT` (write individual members)
229// - Writing simple array elements: `gArray[5]` (write elements of simple arrays)
230//
231// **What Doesn't Work:**
232// - Writing UDT array element members: `gTestUDT_Array[0].Member1_DINT` (write fails)
233// - Writing program-scoped UDT array element members: `Program:TestProgram.gTestUDT_Array[0].Member1_DINT` (write fails)
234//
235// **Workaround:**
236// Use a read-modify-write pattern: Read the entire UDT array element, modify the
237// member in memory, then write the entire UDT array element back.
238//
239// **Important Notes:**
240// - These limitations are PLC firmware restrictions, not library bugs
241// - The library correctly implements the EtherNet/IP and CIP protocols
242// - All read operations work correctly for all tag types
243// - Workarounds are available for UDT array element members and STRING members in UDTs
244//
245// ## Examples
246//
247// See the `examples/` directory for comprehensive usage examples, including:
248// - Advanced tag addressing demonstrations
249// - Complete data type showcase
250// - Real-world industrial automation scenarios
251// - Professional HMI/SCADA dashboard
252// - Multi-language integration examples (C#)
253//
254// ## Changelog
255//
256// ### v0.6.2 (January 2026) - **CURRENT**
257// - **NEW: Stream Injection API** - `connect_with_stream()` for custom TCP transport
258// - Enables wrapping streams for metrics/observability (bytes in/out)
259// - Supports custom socket options (keepalive, timeouts, bind local address)
260// - Allows reusing pre-established tunnels/connections
261// - Supports in-memory streams for deterministic testing
262// - **NEW: Test Configuration** - Environment variable support for PLC testing
263// - `TEST_PLC_ADDRESS` - Set PLC IP address for tests
264// - `TEST_PLC_SLOT` - Set CPU slot number
265// - `SKIP_PLC_TESTS` - Skip PLC-dependent tests
266// - **FIXED: Nested UDT Member Access** - Fixed reading nested UDT members from array elements
267// - Correctly handles paths like `Cell_NestData[90].PartData.Member`
268// - Fixed array element detection to use TagPath::parse() for complex paths
269// - Now correctly builds full CIP paths instead of using array workaround
270
271// ### v0.6.1 (January 2026)
272// - **Repository Cleanup**: Removed Go and Python wrappers to focus on Rust library and C# integration
273// - **Streamlined Examples**: Focused on Microsoft stack (WinForms, WPF, ASP.NET) and Rust native examples
274
275// ### v0.6.0 (January 2026)
276// - **NEW: Generic UDT Format** - `UdtData` struct with `symbol_id` and raw bytes
277// - Works with any UDT without requiring prior knowledge of member structure
278// - Enables parsing UDT members using UDT definitions when needed
279// - Supports reading and writing UDTs generically
280// - **NEW: Library Health** - All 31 unit tests passing, production-ready core
281// - **NEW: Comprehensive Examples** - All examples updated for new UDT API
282// - **NEW: Integration Tests** - All tests updated for new UDT format
283// - Enhanced UDT documentation with usage examples
284// - Improved code quality and consistency
285
286// ### v0.5.5 (December 2025)
287// - **NEW: Array Element Access** - Full read/write support for array elements
288// - **NEW: Array Element Writing** - Write individual array elements with automatic array modification
289// - **NEW: BOOL Array Support** - Automatic DWORD bit extraction for BOOL arrays
290
291// ### v0.5.4 (October 2025)
292// - **NEW: UDT Definition Discovery from PLC** - Automatic UDT structure detection
293// - **NEW: Enhanced Tag Discovery** - Full attribute support with permissions and scope
294// - **NEW: Packet Size Negotiation** - Dynamic negotiation with firmware 20+
295// - **NEW: Route Path Support** - Slot configuration and multi-hop routing
296// - **NEW: CIP Service 0x03** - Get Attribute List implementation
297// - **NEW: CIP Service 0x4C** - Read Tag Fragmented for large data
298// - **NEW: UDT Template Management** - Caching and parsing of UDT templates
299// - **NEW: Tag Attributes API** - Comprehensive tag metadata discovery
300// - **NEW: Program-Scoped Tag Discovery** - Discover tags within specific programs
301// - **NEW: Route Path API** - Support for remote racks and complex topologies
302// - **NEW: Cache Management** - Clear and manage UDT/tag caches
303// - **NEW: Comprehensive Unit Tests** - 15+ new test cases for UDT discovery
304// - **NEW: UDT Discovery Demo** - Complete example showcasing new features
305// - **NEW: Enhanced FFI Functions** - 3 new C# wrapper functions
306// - Enhanced error handling for UDT operations
307// - Improved performance with packet size optimization
308// - Production-ready UDT support for industrial applications
309
310// ### v0.5.3 (January 2025)
311// - Enhanced safety documentation for all FFI functions
312// - Comprehensive clippy optimizations and code quality improvements
313// - Improved memory management and connection pool handling
314// - Enhanced C# wrapper stability
315// - Production-ready code quality with 0 warnings
316//
317// ### v0.5.0 (January 2025)
318// - Professional HMI/SCADA production dashboard
319// - Enterprise-grade monitoring and health checks
320// - Production-ready configuration management
321// - Comprehensive metrics collection and reporting
322// - Enhanced error handling and recovery mechanisms
323//
324// ### v0.4.0 (January 2025)
325// - Real-time subscriptions with event-driven notifications
326// - High-performance batch operations (2,000+ ops/sec)
327// - Complete data type support for all Allen-Bradley types
328// - Advanced tag path parsing (program-scoped, arrays, bits, UDTs)
329// - Enhanced error handling and documentation
330// - Comprehensive test coverage (47+ tests)
331// - Production-ready stability and performance
332//
333// =========================================================================
334
335use crate::udt::UdtManager;
336use lazy_static::lazy_static;
337use std::collections::HashMap;
338use std::net::SocketAddr;
339use std::sync::atomic::AtomicBool;
340use std::sync::Arc;
341use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
342use tokio::net::TcpStream;
343
344/// Trait for streams that can be used with EipClient
345///
346/// This trait combines the requirements for streams used with EtherNet/IP:
347/// - `AsyncRead`: Read data from the stream
348/// - `AsyncWrite`: Write data to the stream
349/// - `Unpin`: Required for async operations
350/// - `Send`: Required for cross-thread safety
351///
352/// Most tokio streams (like `TcpStream`, `UnixStream`, etc.) automatically
353/// implement this trait. You can also implement it for custom stream wrappers
354/// to add metrics, logging, or other functionality.
355///
356/// # Example
357///
358/// ```no_run
359/// use rust_ethernet_ip::EtherNetIpStream;
360/// use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
361///
362/// // Custom stream wrapper for metrics
363/// struct MetricsStream<S> {
364/// inner: S,
365/// bytes_read: u64,
366/// bytes_written: u64,
367/// }
368///
369/// // Most tokio streams automatically implement EtherNetIpStream
370/// // For example, TcpStream implements it:
371/// use tokio::net::TcpStream;
372/// // TcpStream: AsyncRead + AsyncWrite + Unpin + Send ✓
373/// // Therefore: TcpStream implements EtherNetIpStream ✓
374/// ```
375pub trait EtherNetIpStream: AsyncRead + AsyncWrite + Unpin + Send {}
376
377impl<S> EtherNetIpStream for S where S: AsyncRead + AsyncWrite + Unpin + Send {}
378use tokio::runtime::Runtime;
379use tokio::sync::Mutex;
380use tokio::time::{timeout, Duration, Instant};
381
382pub mod config; // Production-ready configuration management
383pub mod error;
384pub mod ffi;
385pub mod monitoring; // Enterprise-grade monitoring and health checks
386pub mod plc_manager;
387pub mod subscription;
388pub mod tag_manager;
389pub mod tag_path;
390pub mod tag_subscription; // Real-time subscription management
391pub mod udt;
392pub mod version;
393
394// Re-export commonly used items
395pub use config::{
396 ConnectionConfig, LoggingConfig, MonitoringConfig, PerformanceConfig, PlcSpecificConfig,
397 ProductionConfig, SecurityConfig,
398};
399pub use error::{EtherNetIpError, Result};
400pub use monitoring::{
401 ConnectionMetrics, ErrorMetrics, HealthMetrics, HealthStatus, MonitoringMetrics,
402 OperationMetrics, PerformanceMetrics, ProductionMonitor,
403};
404pub use plc_manager::{PlcConfig, PlcConnection, PlcManager};
405pub use subscription::{SubscriptionManager, SubscriptionOptions, TagSubscription};
406pub use tag_manager::{TagCache, TagManager, TagMetadata, TagPermissions, TagScope};
407pub use tag_path::TagPath;
408pub use tag_subscription::{
409 SubscriptionManager as RealTimeSubscriptionManager,
410 SubscriptionOptions as RealTimeSubscriptionOptions, TagSubscription as RealTimeSubscription,
411};
412pub use udt::{TagAttributes, UdtDefinition, UdtMember, UdtTemplate};
413
414/// Initialize tracing subscriber with environment-based filtering
415///
416/// This function sets up the tracing subscriber to use the `RUST_LOG` environment variable
417/// for log level filtering. If not called, tracing events will be ignored.
418///
419/// # Examples
420///
421/// ```no_run
422/// use rust_ethernet_ip::init_tracing;
423///
424/// // Initialize with default settings (reads RUST_LOG env var)
425/// init_tracing();
426///
427/// // Or set RUST_LOG before calling:
428/// // RUST_LOG=debug cargo run
429/// ```
430///
431/// # Log Levels
432///
433/// Set the `RUST_LOG` environment variable to control logging:
434/// - `RUST_LOG=trace` - Most verbose (all events)
435/// - `RUST_LOG=debug` - Debug information
436/// - `RUST_LOG=info` - Informational messages (default)
437/// - `RUST_LOG=warn` - Warnings only
438/// - `RUST_LOG=error` - Errors only
439/// - `RUST_LOG=rust_ethernet_ip=debug` - Debug for this crate only
440///
441/// # Panics
442///
443/// This function will panic if called more than once. Use `try_init_tracing()` for
444/// non-panicking initialization.
445pub fn init_tracing() {
446 use tracing_subscriber::fmt;
447 use tracing_subscriber::EnvFilter;
448
449 let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
450
451 let subscriber = fmt::Subscriber::builder()
452 .with_env_filter(filter)
453 .with_target(false) // Don't show module paths by default
454 .finish();
455
456 tracing::subscriber::set_global_default(subscriber).expect("Failed to set tracing subscriber");
457}
458
459/// Try to initialize tracing subscriber (non-panicking version)
460///
461/// Returns `Ok(())` if initialization was successful, or an error if a subscriber
462/// was already set.
463pub fn try_init_tracing() -> std::result::Result<(), Box<dyn std::error::Error>> {
464 use tracing_subscriber::fmt;
465 use tracing_subscriber::EnvFilter;
466
467 let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
468
469 let subscriber = fmt::Subscriber::builder()
470 .with_env_filter(filter)
471 .with_target(false)
472 .finish();
473
474 tracing::subscriber::set_global_default(subscriber)
475 .map_err(|e| Box::new(e) as Box<dyn std::error::Error>)?;
476 Ok(())
477}
478
479/// Route path for PLC communication
480#[derive(Debug, Clone)]
481pub struct RoutePath {
482 pub slots: Vec<u8>,
483 pub ports: Vec<u8>,
484 pub addresses: Vec<String>,
485}
486
487impl RoutePath {
488 /// Creates a new route path
489 pub fn new() -> Self {
490 Self {
491 slots: Vec::new(),
492 ports: Vec::new(),
493 addresses: Vec::new(),
494 }
495 }
496
497 /// Adds a backplane slot to the route
498 pub fn add_slot(mut self, slot: u8) -> Self {
499 self.slots.push(slot);
500 self
501 }
502
503 /// Adds a network port to the route
504 pub fn add_port(mut self, port: u8) -> Self {
505 self.ports.push(port);
506 self
507 }
508
509 /// Adds a network address to the route
510 pub fn add_address(mut self, address: String) -> Self {
511 self.addresses.push(address);
512 self
513 }
514
515 /// Builds CIP route path bytes
516 ///
517 /// Reference: EtherNetIP_Connection_Paths_and_Routing.md, Port Segment Encoding
518 /// According to the examples: Port 1 (backplane), Slot X = [0x01, X]
519 /// The 0x01 byte encodes both "Port Segment (8-bit link)" AND "Port 1 (backplane)"
520 /// Examples from documentation:
521 /// - Slot 0: `01 00`
522 /// - Slot 1: `01 01`
523 /// - Slot 2: `01 02`
524 pub fn to_cip_bytes(&self) -> Vec<u8> {
525 let mut path = Vec::new();
526
527 // Add backplane slots
528 // Reference: EtherNetIP_Connection_Paths_and_Routing.md, Backplane Port Segment Examples
529 // Format: [0x01, slot] where:
530 // - 0x01 = Port Segment (8-bit link) for Port 1 (backplane)
531 // - slot = Slot number (0-255)
532 // Examples: Slot 0 = [0x01, 0x00], Slot 1 = [0x01, 0x01], etc.
533 for &slot in &self.slots {
534 path.push(0x01); // Port Segment (8-bit link) for Port 1 (backplane)
535 path.push(slot); // Slot number
536 }
537
538 // Add network hops
539 for (i, address) in self.addresses.iter().enumerate() {
540 if i < self.ports.len() {
541 path.push(self.ports[i]); // Port number
542 } else {
543 path.push(0x01); // Default port
544 }
545
546 // Parse IP address and add to path
547 if let Ok(ip) = address.parse::<std::net::Ipv4Addr>() {
548 let octets = ip.octets();
549 path.extend_from_slice(&octets);
550 }
551 }
552
553 path
554 }
555}
556
557impl Default for RoutePath {
558 fn default() -> Self {
559 Self::new()
560 }
561}
562
563// Static runtime and client management for FFI
564lazy_static! {
565 /// Global Tokio runtime for handling async operations in FFI context
566 static ref RUNTIME: Runtime = Runtime::new().unwrap();
567
568 /// Global storage for EipClient instances, indexed by client ID
569 static ref CLIENTS: Mutex<HashMap<i32, EipClient>> = Mutex::new(HashMap::new());
570
571 /// Counter for generating unique client IDs
572 static ref NEXT_ID: Mutex<i32> = Mutex::new(1);
573}
574
575// =========================================================================
576// BATCH OPERATIONS DATA STRUCTURES
577// =========================================================================
578
579/// Represents a single operation in a batch request
580///
581/// This enum defines the different types of operations that can be
582/// performed in a batch. Each operation specifies whether it's a read
583/// or write operation and includes the necessary parameters.
584#[derive(Debug, Clone)]
585pub enum BatchOperation {
586 /// Read operation for a specific tag
587 ///
588 /// # Fields
589 ///
590 /// * `tag_name` - The name of the tag to read
591 Read { tag_name: String },
592
593 /// Write operation for a specific tag with a value
594 ///
595 /// # Fields
596 ///
597 /// * `tag_name` - The name of the tag to write
598 /// * `value` - The value to write to the tag
599 Write { tag_name: String, value: PlcValue },
600}
601
602/// Result of a single operation in a batch request
603///
604/// This structure contains the result of executing a single batch operation,
605/// including success/failure status and the actual data or error information.
606#[derive(Debug, Clone)]
607pub struct BatchResult {
608 /// The original operation that was executed
609 pub operation: BatchOperation,
610
611 /// The result of the operation
612 pub result: std::result::Result<Option<PlcValue>, BatchError>,
613
614 /// Execution time for this specific operation (in microseconds)
615 pub execution_time_us: u64,
616}
617
618/// Specific error types that can occur during batch operations
619///
620/// This enum provides detailed error information for batch operations,
621/// allowing for better error handling and diagnostics.
622#[derive(Debug, Clone)]
623pub enum BatchError {
624 /// Tag was not found in the PLC
625 TagNotFound(String),
626
627 /// Data type mismatch between expected and actual
628 DataTypeMismatch { expected: String, actual: String },
629
630 /// Network communication error
631 NetworkError(String),
632
633 /// CIP protocol error with status code
634 CipError { status: u8, message: String },
635
636 /// Tag name parsing error
637 TagPathError(String),
638
639 /// Value serialization/deserialization error
640 SerializationError(String),
641
642 /// Operation timeout
643 Timeout,
644
645 /// Generic error for unexpected issues
646 Other(String),
647}
648
649impl std::fmt::Display for BatchError {
650 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
651 match self {
652 BatchError::TagNotFound(tag) => write!(f, "Tag not found: {tag}"),
653 BatchError::DataTypeMismatch { expected, actual } => {
654 write!(f, "Data type mismatch: expected {expected}, got {actual}")
655 }
656 BatchError::NetworkError(msg) => write!(f, "Network error: {msg}"),
657 BatchError::CipError { status, message } => {
658 write!(f, "CIP error (0x{status:02X}): {message}")
659 }
660 BatchError::TagPathError(msg) => write!(f, "Tag path error: {msg}"),
661 BatchError::SerializationError(msg) => write!(f, "Serialization error: {msg}"),
662 BatchError::Timeout => write!(f, "Operation timeout"),
663 BatchError::Other(msg) => write!(f, "Error: {msg}"),
664 }
665 }
666}
667
668impl std::error::Error for BatchError {}
669
670/// Configuration for batch operations
671///
672/// This structure controls the behavior and performance characteristics
673/// of batch read/write operations. Proper tuning can significantly
674/// improve throughput for applications that need to process many tags.
675#[derive(Debug, Clone)]
676pub struct BatchConfig {
677 /// Maximum number of operations to include in a single CIP packet
678 ///
679 /// Larger values improve performance but may exceed PLC packet size limits.
680 /// Typical range: 10-50 operations per packet.
681 pub max_operations_per_packet: usize,
682
683 /// Maximum packet size in bytes for batch operations
684 ///
685 /// Should not exceed the PLC's maximum packet size capability.
686 /// Typical values: 504 bytes (default), up to 4000 bytes for modern PLCs.
687 pub max_packet_size: usize,
688
689 /// Timeout for individual batch packets (in milliseconds)
690 ///
691 /// This is per-packet timeout, not per-operation.
692 /// Typical range: 1000-5000 milliseconds.
693 pub packet_timeout_ms: u64,
694
695 /// Whether to continue processing other operations if one fails
696 ///
697 /// If true, failed operations are reported but don't stop the batch.
698 /// If false, the first error stops the entire batch processing.
699 pub continue_on_error: bool,
700
701 /// Whether to optimize packet packing by grouping similar operations
702 ///
703 /// If true, reads and writes are grouped separately for better performance.
704 /// If false, operations are processed in the order provided.
705 pub optimize_packet_packing: bool,
706}
707
708impl Default for BatchConfig {
709 fn default() -> Self {
710 Self {
711 max_operations_per_packet: 20,
712 max_packet_size: 504, // Conservative default for maximum compatibility
713 packet_timeout_ms: 3000,
714 continue_on_error: true,
715 optimize_packet_packing: true,
716 }
717 }
718}
719
720/// Connected session information for Class 3 explicit messaging
721///
722/// Allen-Bradley PLCs often require connected sessions for certain operations
723/// like STRING writes. This structure maintains the connection state.
724#[derive(Debug, Clone)]
725pub struct ConnectedSession {
726 /// Connection ID assigned by the PLC
727 pub connection_id: u32,
728
729 /// Our connection ID (originator -> target)
730 pub o_to_t_connection_id: u32,
731
732 /// PLC's connection ID (target -> originator)
733 pub t_to_o_connection_id: u32,
734
735 /// Connection serial number for this session
736 pub connection_serial: u16,
737
738 /// Originator vendor ID (our vendor ID)
739 pub originator_vendor_id: u16,
740
741 /// Originator serial number (our serial number)
742 pub originator_serial: u32,
743
744 /// Connection timeout multiplier
745 pub timeout_multiplier: u8,
746
747 /// Requested Packet Interval (RPI) in microseconds
748 pub rpi: u32,
749
750 /// Connection parameters for O->T direction
751 pub o_to_t_params: ConnectionParameters,
752
753 /// Connection parameters for T->O direction
754 pub t_to_o_params: ConnectionParameters,
755
756 /// Timestamp when connection was established
757 pub established_at: Instant,
758
759 /// Whether this connection is currently active
760 pub is_active: bool,
761
762 /// Sequence counter for connected messages (increments with each message)
763 pub sequence_count: u16,
764}
765
766/// Connection parameters for EtherNet/IP connections
767#[derive(Debug, Clone)]
768pub struct ConnectionParameters {
769 /// Connection size in bytes
770 pub size: u16,
771
772 /// Connection type (0x02 = Point-to-point, 0x01 = Multicast)
773 pub connection_type: u8,
774
775 /// Priority (0x00 = Low, 0x01 = High, 0x02 = Scheduled, 0x03 = Urgent)
776 pub priority: u8,
777
778 /// Variable size flag
779 pub variable_size: bool,
780}
781
782impl Default for ConnectionParameters {
783 fn default() -> Self {
784 Self {
785 size: 500, // 500 bytes default
786 connection_type: 0x02, // Point-to-point
787 priority: 0x01, // High priority
788 variable_size: false,
789 }
790 }
791}
792
793impl ConnectedSession {
794 /// Creates a new connected session with default parameters
795 pub fn new(connection_serial: u16) -> Self {
796 Self {
797 connection_id: 0,
798 o_to_t_connection_id: 0,
799 t_to_o_connection_id: 0,
800 connection_serial,
801 originator_vendor_id: 0x1337, // Custom vendor ID
802 originator_serial: 0x1234_5678, // Custom serial number
803 timeout_multiplier: 0x05, // 32 seconds timeout
804 rpi: 100_000, // 100ms RPI
805 o_to_t_params: ConnectionParameters::default(),
806 t_to_o_params: ConnectionParameters::default(),
807 established_at: Instant::now(),
808 is_active: false,
809 sequence_count: 0,
810 }
811 }
812
813 /// Creates a connected session with alternative parameters for different PLCs
814 pub fn with_config(connection_serial: u16, config_id: u8) -> Self {
815 let mut session = Self::new(connection_serial);
816
817 match config_id {
818 1 => {
819 // Config 1: Conservative Allen-Bradley parameters
820 session.timeout_multiplier = 0x07; // 256 seconds timeout
821 session.rpi = 200_000; // 200ms RPI (slower)
822 session.o_to_t_params.size = 504; // Standard packet size
823 session.t_to_o_params.size = 504;
824 session.o_to_t_params.priority = 0x00; // Low priority
825 session.t_to_o_params.priority = 0x00;
826 tracing::debug!("CONFIG 1: Conservative: 504 bytes, 200ms RPI, low priority");
827 }
828 2 => {
829 // Config 2: Compact parameters
830 session.timeout_multiplier = 0x03; // 8 seconds timeout
831 session.rpi = 50000; // 50ms RPI (faster)
832 session.o_to_t_params.size = 256; // Smaller packet size
833 session.t_to_o_params.size = 256;
834 session.o_to_t_params.priority = 0x02; // Scheduled priority
835 session.t_to_o_params.priority = 0x02;
836 tracing::debug!("CONFIG 2: Compact: 256 bytes, 50ms RPI, scheduled priority");
837 }
838 3 => {
839 // Config 3: Minimal parameters
840 session.timeout_multiplier = 0x01; // 4 seconds timeout
841 session.rpi = 1_000_000; // 1000ms RPI (very slow)
842 session.o_to_t_params.size = 128; // Very small packets
843 session.t_to_o_params.size = 128;
844 session.o_to_t_params.priority = 0x03; // Urgent priority
845 session.t_to_o_params.priority = 0x03;
846 tracing::debug!("CONFIG 3: Minimal: 128 bytes, 1000ms RPI, urgent priority");
847 }
848 4 => {
849 // Config 4: Standard Rockwell parameters (from documentation)
850 session.timeout_multiplier = 0x05; // 32 seconds timeout
851 session.rpi = 100_000; // 100ms RPI
852 session.o_to_t_params.size = 500; // Standard size
853 session.t_to_o_params.size = 500;
854 session.o_to_t_params.connection_type = 0x01; // Multicast
855 session.t_to_o_params.connection_type = 0x01;
856 session.originator_vendor_id = 0x001D; // Rockwell vendor ID
857 tracing::debug!(
858 "CONFIG 4: Rockwell standard: 500 bytes, 100ms RPI, multicast, Rockwell vendor"
859 );
860 }
861 5 => {
862 // Config 5: Large buffer parameters
863 session.timeout_multiplier = 0x0A; // Very long timeout
864 session.rpi = 500_000; // 500ms RPI
865 session.o_to_t_params.size = 1024; // Large packets
866 session.t_to_o_params.size = 1024;
867 session.o_to_t_params.variable_size = true; // Variable size
868 session.t_to_o_params.variable_size = true;
869 tracing::debug!("CONFIG 5: Large buffer: 1024 bytes, 500ms RPI, variable size");
870 }
871 _ => {
872 // Default config
873 tracing::debug!("CONFIG 0: Default parameters");
874 }
875 }
876
877 session
878 }
879}
880
881/// Represents the different data types supported by Allen-Bradley PLCs
882///
883/// These correspond to the CIP data type codes used in EtherNet/IP
884/// communication. Each variant maps to a specific 16-bit type identifier
885/// that the PLC uses to describe tag data.
886///
887/// # Supported Data Types
888///
889/// ## Integer Types
890/// - **SINT**: 8-bit signed integer (-128 to 127)
891/// - **INT**: 16-bit signed integer (-32,768 to 32,767)
892/// - **DINT**: 32-bit signed integer (-2,147,483,648 to 2,147,483,647)
893/// - **LINT**: 64-bit signed integer (-9,223,372,036,854,775,808 to 9,223,372,036,854,775,807)
894///
895/// ## Unsigned Integer Types
896/// - **USINT**: 8-bit unsigned integer (0 to 255)
897/// - **UINT**: 16-bit unsigned integer (0 to 65,535)
898/// - **UDINT**: 32-bit unsigned integer (0 to 4,294,967,295)
899/// - **ULINT**: 64-bit unsigned integer (0 to 18,446,744,073,709,551,615)
900///
901/// ## Floating Point Types
902/// - **REAL**: 32-bit IEEE 754 float (±1.18 × 10^-38 to ±3.40 × 10^38)
903/// - **LREAL**: 64-bit IEEE 754 double (±2.23 × 10^-308 to ±1.80 × 10^308)
904///
905/// ## Other Types
906/// - **BOOL**: Boolean value (true/false)
907/// - **STRING**: Variable-length string
908/// - **UDT**: User Defined Type (structured data)
909///
910/// Represents raw UDT (User Defined Type) data
911///
912/// This structure stores UDT data in a generic format that works for any UDT
913/// without requiring knowledge of member names. The `symbol_id` (template instance ID)
914/// is required for writing UDTs back to the PLC, and the raw bytes can be parsed
915/// later when the UDT definition is available.
916///
917/// # Usage
918///
919/// To write a UDT, you typically need to read it first to get the `symbol_id`.
920/// While it's technically possible to calculate the symbol_id, it's much safer
921/// to enforce a read of the UDT before writing to it.
922#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
923pub struct UdtData {
924 /// The template instance ID (symbol_id) from the PLC
925 /// This is required for writing UDTs back to the PLC
926 pub symbol_id: i32,
927 /// Raw UDT data bytes
928 /// This can be parsed into member values when the UDT definition is known
929 pub data: Vec<u8>,
930}
931
932#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
933pub enum PlcValue {
934 /// Boolean value (single bit)
935 ///
936 /// Maps to CIP type 0x00C1. In CompactLogix PLCs, BOOL tags
937 /// are stored as single bits but transmitted as bytes over the network.
938 Bool(bool),
939
940 /// 8-bit signed integer (-128 to 127)
941 ///
942 /// Maps to CIP type 0x00C2. Used for small numeric values,
943 /// status codes, and compact data storage.
944 Sint(i8),
945
946 /// 16-bit signed integer (-32,768 to 32,767)
947 ///
948 /// Maps to CIP type 0x00C3. Common for analog input/output values,
949 /// counters, and medium-range numeric data.
950 Int(i16),
951
952 /// 32-bit signed integer (-2,147,483,648 to 2,147,483,647)
953 ///
954 /// Maps to CIP type 0x00C4. This is the most common integer type
955 /// in Allen-Bradley PLCs, used for counters, setpoints, and numeric values.
956 Dint(i32),
957
958 /// 64-bit signed integer (-9,223,372,036,854,775,808 to 9,223,372,036,854,775,807)
959 ///
960 /// Maps to CIP type 0x00C5. Used for large counters, timestamps,
961 /// and high-precision calculations.
962 Lint(i64),
963
964 /// 8-bit unsigned integer (0 to 255)
965 ///
966 /// Maps to CIP type 0x00C6. Used for byte data, small counters,
967 /// and status flags.
968 Usint(u8),
969
970 /// 16-bit unsigned integer (0 to 65,535)
971 ///
972 /// Maps to CIP type 0x00C7. Common for analog values, port numbers,
973 /// and medium-range unsigned data.
974 Uint(u16),
975
976 /// 32-bit unsigned integer (0 to 4,294,967,295)
977 ///
978 /// Maps to CIP type 0x00C8. Used for large counters, memory addresses,
979 /// and unsigned calculations.
980 Udint(u32),
981
982 /// 64-bit unsigned integer (0 to 18,446,744,073,709,551,615)
983 ///
984 /// Maps to CIP type 0x00C9. Used for very large counters, timestamps,
985 /// and high-precision unsigned calculations.
986 Ulint(u64),
987
988 /// 32-bit IEEE 754 floating point number
989 ///
990 /// Maps to CIP type 0x00CA. Used for analog values, calculations,
991 /// and any data requiring decimal precision.
992 /// Range: ±1.18 × 10^-38 to ±3.40 × 10^38
993 Real(f32),
994
995 /// 64-bit IEEE 754 floating point number
996 ///
997 /// Maps to CIP type 0x00CB. Used for high-precision calculations,
998 /// scientific data, and extended-range floating point values.
999 /// Range: ±2.23 × 10^-308 to ±1.80 × 10^308
1000 Lreal(f64),
1001
1002 /// String value
1003 ///
1004 /// Maps to CIP type 0x00DA. Variable-length string data
1005 /// commonly used for product names, status messages, and text data.
1006 String(String),
1007
1008 /// User Defined Type instance
1009 ///
1010 /// Maps to CIP type 0x00A0. Structured data type containing
1011 /// multiple members of different types.
1012 ///
1013 /// **v0.6.0**: Uses `UdtData` which stores the symbol_id (template instance ID)
1014 /// and raw bytes. This generic format works for any UDT without requiring
1015 /// knowledge of member names ahead of time. The raw bytes can be parsed
1016 /// into member values when the UDT definition is available using `UdtData::parse()`.
1017 ///
1018 /// # Example
1019 ///
1020 /// ```rust,no_run
1021 /// # async fn example() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1022 /// # let mut client = rust_ethernet_ip::EipClient::connect("192.168.1.100:44818").await?;
1023 /// use rust_ethernet_ip::PlcValue;
1024 /// let value = client.read_tag("MyUDT").await?;
1025 /// if let PlcValue::Udt(udt_data) = value {
1026 /// let udt_def = client.get_udt_definition("MyUDT").await?;
1027 /// // Convert UdtDefinition to UserDefinedType
1028 /// let mut user_def = rust_ethernet_ip::udt::UserDefinedType::new(udt_def.name.clone());
1029 /// for member in &udt_def.members {
1030 /// user_def.add_member(member.clone());
1031 /// }
1032 /// let members = udt_data.parse(&user_def)?;
1033 /// // Access members via HashMap
1034 /// }
1035 /// # Ok(())
1036 /// # }
1037 /// ```
1038 Udt(UdtData),
1039}
1040
1041impl UdtData {
1042 /// Parses the raw UDT data into a HashMap of member values using the UDT definition
1043 ///
1044 /// **v0.6.0**: This method converts the generic `UdtData` format into a structured
1045 /// HashMap of member names to values. This requires a UDT definition to know the
1046 /// structure of the data.
1047 ///
1048 /// Use `EipClient::get_udt_definition()` to obtain the definition from the PLC first.
1049 ///
1050 /// # Arguments
1051 ///
1052 /// * `definition` - The UDT definition containing member information (offsets, types, etc.)
1053 ///
1054 /// # Returns
1055 ///
1056 /// A HashMap mapping member names to their parsed `PlcValue` values
1057 ///
1058 /// # Example
1059 ///
1060 /// ```rust,no_run
1061 /// # async fn example() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1062 /// # let mut client = rust_ethernet_ip::EipClient::connect("192.168.1.100:44818").await?;
1063 /// use rust_ethernet_ip::PlcValue;
1064 /// let udt_value = client.read_tag("MyUDT").await?;
1065 /// if let PlcValue::Udt(udt_data) = udt_value {
1066 /// let udt_def = client.get_udt_definition("MyUDT").await?;
1067 /// // Convert UdtDefinition to UserDefinedType
1068 /// let mut user_def = rust_ethernet_ip::udt::UserDefinedType::new(udt_def.name.clone());
1069 /// for member in &udt_def.members {
1070 /// user_def.add_member(member.clone());
1071 /// }
1072 /// let members = udt_data.parse(&user_def)?;
1073 ///
1074 /// if let Some(PlcValue::Dint(value)) = members.get("Member1") {
1075 /// println!("Member1 value: {}", value);
1076 /// }
1077 /// }
1078 /// # Ok(())
1079 /// # }
1080 /// ```
1081 pub fn parse(
1082 &self,
1083 definition: &crate::udt::UserDefinedType,
1084 ) -> crate::error::Result<HashMap<String, PlcValue>> {
1085 definition.to_hash_map(&self.data)
1086 }
1087
1088 /// Creates UdtData from a HashMap of member values and a UDT definition
1089 ///
1090 /// **v0.6.0**: This method serializes member values back into raw bytes according
1091 /// to the UDT definition. This is useful when you need to modify UDT members and
1092 /// write them back to the PLC.
1093 ///
1094 /// # Arguments
1095 ///
1096 /// * `members` - HashMap of member names to `PlcValue` values
1097 /// * `definition` - The UDT definition containing member information (offsets, types, etc.)
1098 /// * `symbol_id` - The template instance ID (symbol_id) for this UDT. Typically obtained
1099 /// by reading the UDT first.
1100 ///
1101 /// # Returns
1102 ///
1103 /// `UdtData` containing the serialized bytes and symbol_id, ready to be written back
1104 ///
1105 /// # Example
1106 ///
1107 /// ```rust,no_run
1108 /// # async fn example() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1109 /// # let mut client = rust_ethernet_ip::EipClient::connect("192.168.1.100:44818").await?;
1110 /// use rust_ethernet_ip::{PlcValue, UdtData};
1111 /// // Read existing UDT to get symbol_id
1112 /// let udt_value = client.read_tag("MyUDT").await?;
1113 /// let udt_def = client.get_udt_definition("MyUDT").await?;
1114 ///
1115 /// if let PlcValue::Udt(mut udt_data) = udt_value {
1116 /// // Convert UdtDefinition to UserDefinedType
1117 /// let mut user_def = rust_ethernet_ip::udt::UserDefinedType::new(udt_def.name.clone());
1118 /// for member in &udt_def.members {
1119 /// user_def.add_member(member.clone());
1120 /// }
1121 /// // Parse to modify members
1122 /// let mut members = udt_data.parse(&user_def)?;
1123 /// members.insert("Member1".to_string(), PlcValue::Dint(42));
1124 ///
1125 /// // Serialize back to UdtData
1126 /// let modified_udt = UdtData::from_hash_map(&members, &user_def, udt_data.symbol_id)?;
1127 /// client.write_tag("MyUDT", PlcValue::Udt(modified_udt)).await?;
1128 /// }
1129 /// # Ok(())
1130 /// # }
1131 /// ```
1132 pub fn from_hash_map(
1133 members: &HashMap<String, PlcValue>,
1134 definition: &crate::udt::UserDefinedType,
1135 symbol_id: i32,
1136 ) -> crate::error::Result<Self> {
1137 let data = definition.from_hash_map(members)?;
1138 Ok(UdtData { symbol_id, data })
1139 }
1140}
1141
1142impl PlcValue {
1143 /// Converts the PLC value to its byte representation for network transmission
1144 ///
1145 /// This function handles the little-endian byte encoding required by
1146 /// the EtherNet/IP protocol. Each data type has specific encoding rules:
1147 ///
1148 /// - BOOL: Single byte (0x00 = false, 0xFF = true)
1149 /// - SINT: Single signed byte
1150 /// - INT: 2 bytes in little-endian format
1151 /// - DINT: 4 bytes in little-endian format
1152 /// - LINT: 8 bytes in little-endian format
1153 /// - USINT: Single unsigned byte
1154 /// - UINT: 2 bytes in little-endian format
1155 /// - UDINT: 4 bytes in little-endian format
1156 /// - ULINT: 8 bytes in little-endian format
1157 /// - REAL: 4 bytes IEEE 754 little-endian format
1158 /// - LREAL: 8 bytes IEEE 754 little-endian format
1159 ///
1160 /// # Returns
1161 ///
1162 /// A vector of bytes ready for transmission to the PLC
1163 pub fn to_bytes(&self) -> Vec<u8> {
1164 match self {
1165 PlcValue::Bool(val) => vec![if *val { 0xFF } else { 0x00 }],
1166 PlcValue::Sint(val) => val.to_le_bytes().to_vec(),
1167 PlcValue::Int(val) => val.to_le_bytes().to_vec(),
1168 PlcValue::Dint(val) => val.to_le_bytes().to_vec(),
1169 PlcValue::Lint(val) => val.to_le_bytes().to_vec(),
1170 PlcValue::Usint(val) => val.to_le_bytes().to_vec(),
1171 PlcValue::Uint(val) => val.to_le_bytes().to_vec(),
1172 PlcValue::Udint(val) => val.to_le_bytes().to_vec(),
1173 PlcValue::Ulint(val) => val.to_le_bytes().to_vec(),
1174 PlcValue::Real(val) => val.to_le_bytes().to_vec(),
1175 PlcValue::Lreal(val) => val.to_le_bytes().to_vec(),
1176 PlcValue::String(val) => {
1177 // Try minimal approach - just length + data without padding
1178 // Testing if the PLC accepts a simpler format
1179
1180 let mut bytes = Vec::new();
1181
1182 // Length field (4 bytes as DINT) - number of characters currently used
1183 let length = val.len().min(82) as u32;
1184 bytes.extend_from_slice(&length.to_le_bytes());
1185
1186 // String data - just the actual characters, no padding
1187 let string_bytes = val.as_bytes();
1188 let data_len = string_bytes.len().min(82);
1189 bytes.extend_from_slice(&string_bytes[..data_len]);
1190
1191 bytes
1192 }
1193 PlcValue::Udt(udt_data) => {
1194 // Return the raw UDT data bytes
1195 udt_data.data.clone()
1196 }
1197 }
1198 }
1199
1200 /// Returns the CIP data type code for this value
1201 ///
1202 /// These codes are defined by the CIP specification and must match
1203 /// exactly what the PLC expects for each data type.
1204 ///
1205 /// # Returns
1206 ///
1207 /// The 16-bit CIP type code for this value type
1208 pub fn get_data_type(&self) -> u16 {
1209 match self {
1210 PlcValue::Bool(_) => 0x00C1, // BOOL
1211 PlcValue::Sint(_) => 0x00C2, // SINT (signed char)
1212 PlcValue::Int(_) => 0x00C3, // INT (short)
1213 PlcValue::Dint(_) => 0x00C4, // DINT (int)
1214 PlcValue::Lint(_) => 0x00C5, // LINT (long long)
1215 PlcValue::Usint(_) => 0x00C6, // USINT (unsigned char)
1216 PlcValue::Uint(_) => 0x00C7, // UINT (unsigned short)
1217 PlcValue::Udint(_) => 0x00C8, // UDINT (unsigned int)
1218 PlcValue::Ulint(_) => 0x00C9, // ULINT (unsigned long long)
1219 PlcValue::Real(_) => 0x00CA, // REAL (float)
1220 PlcValue::Lreal(_) => 0x00CB, // LREAL (double)
1221 PlcValue::String(_) => 0x00CE, // Allen-Bradley STRING type
1222 PlcValue::Udt(_) => 0x00A0, // UDT placeholder
1223 }
1224 }
1225}
1226
1227/// High-performance EtherNet/IP client for PLC communication
1228///
1229/// This struct provides the core functionality for communicating with Allen-Bradley
1230/// PLCs using the EtherNet/IP protocol. It handles connection management, session
1231/// registration, and tag operations.
1232///
1233/// # Thread Safety
1234///
1235/// The `EipClient` is **NOT** thread-safe. For multi-threaded applications:
1236///
1237/// ```rust,no_run
1238/// use std::sync::Arc;
1239/// use tokio::sync::Mutex;
1240/// use rust_ethernet_ip::EipClient;
1241///
1242/// #[tokio::main]
1243/// async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1244/// // Create a thread-safe wrapper
1245/// let client = Arc::new(Mutex::new(EipClient::connect("192.168.1.100:44818").await?));
1246///
1247/// // Use in multiple threads
1248/// let client_clone = client.clone();
1249/// tokio::spawn(async move {
1250/// let mut client = client_clone.lock().await;
1251/// let _ = client.read_tag("Tag1").await?;
1252/// Ok::<(), Box<dyn std::error::Error + Send + Sync>>(())
1253/// });
1254/// Ok(())
1255/// }
1256/// ```
1257///
1258/// # Performance Characteristics
1259///
1260/// | Operation | Latency | Throughput | Memory |
1261/// |-----------|---------|------------|---------|
1262/// | Connect | 100-500ms | N/A | ~8KB |
1263/// | Read Tag | 1-5ms | 1,500+ ops/sec | ~2KB |
1264/// | Write Tag | 2-10ms | 600+ ops/sec | ~2KB |
1265/// | Batch Read | 5-20ms | 2,000+ ops/sec | ~4KB |
1266///
1267/// # Known Limitations
1268///
1269/// The following operations are **not supported** due to PLC firmware limitations:
1270///
1271/// ## UDT Array Element Member Writes
1272///
1273/// **Cannot write directly to UDT array element members** (e.g., `gTestUDT_Array[0].Member1_DINT`).
1274/// This is a PLC firmware limitation, not a library bug. The PLC returns CIP Error 0x2107
1275/// (Vendor Specific Error) when attempting to write to such paths.
1276///
1277/// ## STRING Tags and STRING Members in UDTs
1278///
1279/// **Cannot write directly to STRING tags or STRING members in UDTs**.
1280/// This is a PLC firmware limitation (CIP Error 0x2107). Both simple STRING tags
1281/// (e.g., `gTest_STRING`) and STRING members within UDTs (e.g., `gTestUDT.Member5_String`)
1282/// cannot be written directly. STRING values must be written as part of the entire UDT
1283/// structure, not as individual tags or members.
1284///
1285/// **What works:**
1286/// - ✅ Reading UDT array element members: `gTestUDT_Array[0].Member1_DINT` (read)
1287/// - ✅ Writing entire UDT array elements: `gTestUDT_Array[0]` (write full UDT)
1288/// - ✅ Writing UDT members (non-STRING): `gTestUDT.Member1_DINT` (write DINT/REAL/BOOL/INT members)
1289/// - ✅ Writing array elements: `gArray[5]` (write element of simple array)
1290/// - ✅ Reading STRING tags: `gTest_STRING` (read)
1291/// - ✅ Reading STRING members in UDTs: `gTestUDT.Member5_String` (read)
1292///
1293/// **What doesn't work:**
1294/// - ❌ Writing UDT array element members: `gTestUDT_Array[0].Member1_DINT` (write)
1295/// - ❌ Writing program-scoped UDT array element members: `Program:TestProgram.gTestUDT_Array[0].Member1_DINT` (write)
1296/// - ❌ Writing simple STRING tags: `gTest_STRING` (write) - PLC limitation
1297/// - ❌ Writing program-scoped STRING tags: `Program:TestProgram.gTest_STRING` (write) - PLC limitation
1298/// - ❌ Writing STRING members in UDTs: `gTestUDT.Member5_String` (write) - must write entire UDT
1299/// - ❌ Writing program-scoped STRING members: `Program:TestProgram.gTestUDT.Member5_String` (write) - must write entire UDT
1300///
1301/// **Workaround:**
1302/// To modify a UDT array element member, read the entire UDT array element, modify the member
1303/// in memory, then write the entire UDT array element back:
1304///
1305/// ```rust,no_run
1306/// # async fn example() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1307/// # let mut client = rust_ethernet_ip::EipClient::connect("192.168.1.100:44818").await?;
1308/// use rust_ethernet_ip::{PlcValue, UdtData};
1309///
1310/// // Read the entire UDT array element
1311/// let udt_value = client.read_tag("gTestUDT_Array[0]").await?;
1312/// if let PlcValue::Udt(mut udt_data) = udt_value {
1313/// let udt_def = client.get_udt_definition("gTestUDT_Array").await?;
1314/// // Convert UdtDefinition to UserDefinedType
1315/// let mut user_def = rust_ethernet_ip::udt::UserDefinedType::new(udt_def.name.clone());
1316/// for member in &udt_def.members {
1317/// user_def.add_member(member.clone());
1318/// }
1319/// let mut members = udt_data.parse(&user_def)?;
1320///
1321/// // Modify the member
1322/// members.insert("Member1_DINT".to_string(), PlcValue::Dint(100));
1323///
1324/// // Write the entire UDT array element back
1325/// let modified_udt = UdtData::from_hash_map(&members, &user_def, udt_data.symbol_id)?;
1326/// client.write_tag("gTestUDT_Array[0]", PlcValue::Udt(modified_udt)).await?;
1327/// }
1328/// # Ok(())
1329/// # }
1330/// ```
1331///
1332/// # Error Handling
1333///
1334/// All operations return `Result<T, EtherNetIpError>`. Common errors include:
1335///
1336/// ```rust,no_run
1337/// use rust_ethernet_ip::{EipClient, EtherNetIpError};
1338///
1339/// #[tokio::main]
1340/// async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1341/// let mut client = EipClient::connect("192.168.1.100:44818").await?;
1342/// match client.read_tag("Tag1").await {
1343/// Ok(value) => println!("Tag value: {:?}", value),
1344/// Err(EtherNetIpError::Protocol(_)) => println!("Tag does not exist"),
1345/// Err(EtherNetIpError::Connection(_)) => println!("Lost connection to PLC"),
1346/// Err(EtherNetIpError::Timeout(_)) => println!("Operation timed out"),
1347/// Err(e) => println!("Other error: {}", e),
1348/// }
1349/// Ok(())
1350/// }
1351/// ```
1352///
1353/// # Examples
1354///
1355/// Basic usage:
1356/// ```rust,no_run
1357/// use rust_ethernet_ip::{EipClient, PlcValue};
1358///
1359/// #[tokio::main]
1360/// async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1361/// let mut client = EipClient::connect("192.168.1.100:44818").await?;
1362///
1363/// // Read a boolean tag
1364/// let motor_running = client.read_tag("MotorRunning").await?;
1365///
1366/// // Write an integer tag
1367/// client.write_tag("SetPoint", PlcValue::Dint(1500)).await?;
1368///
1369/// // Read multiple tags in sequence
1370/// let tag1 = client.read_tag("Tag1").await?;
1371/// let tag2 = client.read_tag("Tag2").await?;
1372/// let tag3 = client.read_tag("Tag3").await?;
1373/// Ok(())
1374/// }
1375/// ```
1376///
1377/// Advanced usage with error recovery:
1378/// ```rust
1379/// use rust_ethernet_ip::{EipClient, PlcValue, EtherNetIpError};
1380/// use tokio::time::Duration;
1381///
1382/// async fn read_with_retry(client: &mut EipClient, tag: &str, retries: u32) -> Result<PlcValue, EtherNetIpError> {
1383/// for attempt in 0..retries {
1384/// match client.read_tag(tag).await {
1385/// Ok(value) => return Ok(value),
1386/// Err(EtherNetIpError::Connection(_)) => {
1387/// if attempt < retries - 1 {
1388/// tokio::time::sleep(Duration::from_secs(1)).await;
1389/// continue;
1390/// }
1391/// return Err(EtherNetIpError::Protocol("Max retries exceeded".to_string()));
1392/// }
1393/// Err(e) => return Err(e),
1394/// }
1395/// }
1396/// Err(EtherNetIpError::Protocol("Max retries exceeded".to_string()))
1397/// }
1398/// ```
1399#[derive(Clone)]
1400pub struct EipClient {
1401 /// Stream for network communication (supports any AsyncRead + AsyncWrite + Unpin stream)
1402 stream: Arc<Mutex<Box<dyn EtherNetIpStream>>>,
1403 /// Session handle for the connection
1404 session_handle: u32,
1405 /// Connection ID for the session
1406 _connection_id: u32,
1407 /// Tag manager for handling tag operations
1408 tag_manager: Arc<Mutex<TagManager>>,
1409 /// UDT manager for handling UDT operations
1410 udt_manager: Arc<Mutex<UdtManager>>,
1411 /// Route path for PLC communication
1412 route_path: Option<RoutePath>,
1413 /// Whether the client is connected
1414 _connected: Arc<AtomicBool>,
1415 /// Maximum packet size for communication
1416 max_packet_size: u32,
1417 /// Last activity timestamp
1418 last_activity: Arc<Mutex<Instant>>,
1419 /// Session timeout duration
1420 _session_timeout: Duration,
1421 /// Configuration for batch operations
1422 batch_config: BatchConfig,
1423 /// Connected session management for Class 3 operations
1424 connected_sessions: Arc<Mutex<HashMap<String, ConnectedSession>>>,
1425 /// Connection sequence counter
1426 connection_sequence: Arc<Mutex<u32>>,
1427 /// Active tag subscriptions
1428 subscriptions: Arc<Mutex<Vec<TagSubscription>>>,
1429}
1430
1431impl std::fmt::Debug for EipClient {
1432 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1433 f.debug_struct("EipClient")
1434 .field("session_handle", &self.session_handle)
1435 .field("route_path", &self.route_path)
1436 .field("max_packet_size", &self.max_packet_size)
1437 .field("_session_timeout", &self._session_timeout)
1438 .field("batch_config", &self.batch_config)
1439 .field("stream", &"<stream>")
1440 .field("tag_manager", &"<tag_manager>")
1441 .field("udt_manager", &"<udt_manager>")
1442 .field("connected_sessions", &"<connected_sessions>")
1443 .field("subscriptions", &"<subscriptions>")
1444 .finish()
1445 }
1446}
1447
1448impl EipClient {
1449 /// Internal constructor that initializes an EipClient from any stream
1450 /// that implements AsyncRead + AsyncWrite + Unpin + Send
1451 async fn from_stream<S>(stream: S) -> Result<Self>
1452 where
1453 S: EtherNetIpStream + 'static,
1454 {
1455 let mut client = Self {
1456 stream: Arc::new(Mutex::new(Box::new(stream))),
1457 session_handle: 0,
1458 _connection_id: 0,
1459 tag_manager: Arc::new(Mutex::new(TagManager::new())),
1460 udt_manager: Arc::new(Mutex::new(UdtManager::new())),
1461 route_path: None,
1462 _connected: Arc::new(AtomicBool::new(false)),
1463 max_packet_size: 4000,
1464 last_activity: Arc::new(Mutex::new(Instant::now())),
1465 _session_timeout: Duration::from_secs(120),
1466 batch_config: BatchConfig::default(),
1467 connected_sessions: Arc::new(Mutex::new(HashMap::new())),
1468 connection_sequence: Arc::new(Mutex::new(1)),
1469 subscriptions: Arc::new(Mutex::new(Vec::new())),
1470 };
1471 client.register_session().await?;
1472 client.negotiate_packet_size().await?;
1473 Ok(client)
1474 }
1475
1476 pub async fn new(addr: &str) -> Result<Self> {
1477 let addr = addr
1478 .parse::<SocketAddr>()
1479 .map_err(|e| EtherNetIpError::Protocol(format!("Invalid address format: {e}")))?;
1480 let stream = TcpStream::connect(addr).await?;
1481 Self::from_stream(stream).await
1482 }
1483
1484 /// Public async connect function for `EipClient`
1485 pub async fn connect(addr: &str) -> Result<Self> {
1486 Self::new(addr).await
1487 }
1488
1489 /// Registers an EtherNet/IP session with the PLC
1490 ///
1491 /// This is an internal function that implements the EtherNet/IP session
1492 /// registration protocol. It sends a Register Session command and
1493 /// processes the response to extract the session handle.
1494 ///
1495 /// # Protocol Details
1496 ///
1497 /// The Register Session command consists of:
1498 /// - EtherNet/IP Encapsulation Header (24 bytes)
1499 /// - Registration Data (4 bytes: protocol version + options)
1500 ///
1501 /// The PLC responds with:
1502 /// - Same header format with assigned session handle
1503 /// - Status code indicating success/failure
1504 ///
1505 /// # Errors
1506 ///
1507 /// - Network timeout or disconnection
1508 /// - Invalid response format
1509 /// - PLC rejection (status code non-zero)
1510 async fn register_session(&mut self) -> crate::error::Result<()> {
1511 tracing::debug!("Starting session registration...");
1512 let packet: [u8; 28] = [
1513 0x65, 0x00, // Command: Register Session (0x0065)
1514 0x04, 0x00, // Length: 4 bytes
1515 0x00, 0x00, 0x00, 0x00, // Session Handle: 0 (will be assigned)
1516 0x00, 0x00, 0x00, 0x00, // Status: 0
1517 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Sender Context (8 bytes)
1518 0x00, 0x00, 0x00, 0x00, // Options: 0
1519 0x01, 0x00, // Protocol Version: 1
1520 0x00, 0x00, // Option Flags: 0
1521 ];
1522
1523 tracing::trace!("Sending Register Session packet: {:02X?}", packet);
1524 self.stream
1525 .lock()
1526 .await
1527 .write_all(&packet)
1528 .await
1529 .map_err(|e| {
1530 tracing::error!("Failed to send Register Session packet: {}", e);
1531 EtherNetIpError::Io(e)
1532 })?;
1533
1534 let mut buf = [0u8; 1024];
1535 tracing::debug!("Waiting for Register Session response...");
1536 let n = match timeout(
1537 Duration::from_secs(5),
1538 self.stream.lock().await.read(&mut buf),
1539 )
1540 .await
1541 {
1542 Ok(Ok(n)) => {
1543 tracing::trace!("Received {} bytes in response", n);
1544 n
1545 }
1546 Ok(Err(e)) => {
1547 tracing::error!("Error reading response: {}", e);
1548 return Err(EtherNetIpError::Io(e));
1549 }
1550 Err(_) => {
1551 tracing::warn!("Timeout waiting for response");
1552 return Err(EtherNetIpError::Timeout(Duration::from_secs(5)));
1553 }
1554 };
1555
1556 if n < 28 {
1557 tracing::error!("Response too short: {} bytes (expected 28)", n);
1558 return Err(EtherNetIpError::Protocol("Response too short".to_string()));
1559 }
1560
1561 // Extract session handle from response
1562 self.session_handle = u32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]);
1563 tracing::debug!("Session handle: 0x{:08X}", self.session_handle);
1564
1565 // Check status
1566 let status = u32::from_le_bytes([buf[8], buf[9], buf[10], buf[11]]);
1567 tracing::trace!("Status code: 0x{:08X}", status);
1568
1569 if status != 0 {
1570 tracing::error!("Session registration failed with status: 0x{:08X}", status);
1571 return Err(EtherNetIpError::Protocol(format!(
1572 "Session registration failed with status: 0x{status:08X}"
1573 )));
1574 }
1575
1576 tracing::info!("Session registration successful");
1577 Ok(())
1578 }
1579
1580 /// Sets the maximum packet size for communication
1581 pub fn set_max_packet_size(&mut self, size: u32) {
1582 self.max_packet_size = size.min(4000);
1583 }
1584
1585 /// Discovers all tags in the PLC (including hierarchical UDT members)
1586 pub async fn discover_tags(&mut self) -> crate::error::Result<()> {
1587 let response = self
1588 .send_cip_request(&self.build_list_tags_request())
1589 .await?;
1590
1591 // Extract CIP data from response and check for errors
1592 let cip_data = self.extract_cip_from_response(&response)?;
1593
1594 // Check for CIP errors before parsing
1595 if let Err(e) = self.check_cip_error(&cip_data) {
1596 return Err(crate::error::EtherNetIpError::Protocol(format!(
1597 "Tag discovery failed: {}. Some PLCs may not support tag discovery. Try reading tags directly by name.",
1598 e
1599 )));
1600 }
1601
1602 let tags = {
1603 let tag_manager = self.tag_manager.lock().await;
1604 tag_manager.parse_tag_list(&cip_data)?
1605 };
1606
1607 tracing::debug!("Initial tag discovery found {} tags", tags.len());
1608
1609 // Perform recursive drill-down discovery (similar to TypeScript implementation)
1610 let hierarchical_tags = {
1611 let tag_manager = self.tag_manager.lock().await;
1612 tag_manager.drill_down_tags(&tags).await?
1613 };
1614
1615 tracing::debug!(
1616 "After drill-down: {} total tags discovered",
1617 hierarchical_tags.len()
1618 );
1619
1620 {
1621 let tag_manager = self.tag_manager.lock().await;
1622 let mut cache = tag_manager.cache.write().unwrap();
1623 for (name, metadata) in hierarchical_tags {
1624 cache.insert(name, metadata);
1625 }
1626 }
1627 Ok(())
1628 }
1629
1630 /// Discovers UDT members for a specific structure
1631 pub async fn discover_udt_members(
1632 &mut self,
1633 udt_name: &str,
1634 ) -> crate::error::Result<Vec<(String, TagMetadata)>> {
1635 // Build CIP request to get UDT definition
1636 let cip_request = {
1637 let tag_manager = self.tag_manager.lock().await;
1638 tag_manager.build_udt_definition_request(udt_name)?
1639 };
1640
1641 // Send the request
1642 let response = self.send_cip_request(&cip_request).await?;
1643
1644 // Parse the UDT definition from response
1645 let definition = {
1646 let tag_manager = self.tag_manager.lock().await;
1647 tag_manager.parse_udt_definition_response(&response, udt_name)?
1648 };
1649
1650 // Cache the definition
1651 {
1652 let tag_manager = self.tag_manager.lock().await;
1653 let mut definitions = tag_manager.udt_definitions.write().unwrap();
1654 definitions.insert(udt_name.to_string(), definition.clone());
1655 }
1656
1657 // Create member metadata
1658 let mut members = Vec::new();
1659 for member in &definition.members {
1660 let member_name = member.name.clone();
1661 let full_name = format!("{}.{}", udt_name, member_name);
1662
1663 let metadata = TagMetadata {
1664 data_type: member.data_type,
1665 scope: TagScope::Controller,
1666 permissions: TagPermissions {
1667 readable: true,
1668 writable: true,
1669 },
1670 is_array: false,
1671 dimensions: Vec::new(),
1672 last_access: std::time::Instant::now(),
1673 size: member.size,
1674 array_info: None,
1675 last_updated: std::time::Instant::now(),
1676 };
1677
1678 members.push((full_name, metadata));
1679 }
1680
1681 Ok(members)
1682 }
1683
1684 /// Gets cached UDT definition
1685 pub async fn get_udt_definition_cached(&self, udt_name: &str) -> Option<UdtDefinition> {
1686 let tag_manager = self.tag_manager.lock().await;
1687 tag_manager.get_udt_definition_cached(udt_name)
1688 }
1689
1690 /// Lists all cached UDT definitions
1691 pub async fn list_udt_definitions(&self) -> Vec<String> {
1692 let tag_manager = self.tag_manager.lock().await;
1693 tag_manager.list_udt_definitions()
1694 }
1695
1696 /// Discovers all tags with full attributes
1697 /// This method queries the PLC for all available tags and their detailed attributes
1698 pub async fn discover_tags_detailed(&mut self) -> crate::error::Result<Vec<TagAttributes>> {
1699 // Build CIP request for tag list with attributes
1700 let request = self.build_tag_list_request()?;
1701 let response = self.send_cip_request(&request).await?;
1702
1703 // Extract CIP data from response and check for errors
1704 let cip_data = self.extract_cip_from_response(&response)?;
1705
1706 // Check for CIP errors before parsing
1707 if let Err(e) = self.check_cip_error(&cip_data) {
1708 return Err(crate::error::EtherNetIpError::Protocol(format!(
1709 "Tag discovery failed: {}. Some PLCs may not support tag discovery. Try reading tags directly by name.",
1710 e
1711 )));
1712 }
1713
1714 // Parse response with all attributes
1715 self.parse_tag_list_response(&cip_data)
1716 }
1717
1718 /// Discovers program-scoped tags
1719 /// This method discovers tags within a specific program scope
1720 pub async fn discover_program_tags(
1721 &mut self,
1722 program_name: &str,
1723 ) -> crate::error::Result<Vec<TagAttributes>> {
1724 // Build CIP request for program-scoped tag list
1725 let request = self.build_program_tag_list_request(program_name)?;
1726 let response = self.send_cip_request(&request).await?;
1727
1728 // Extract CIP data from response and check for errors
1729 let cip_data = self.extract_cip_from_response(&response)?;
1730
1731 // Check for CIP errors before parsing
1732 if let Err(e) = self.check_cip_error(&cip_data) {
1733 return Err(crate::error::EtherNetIpError::Protocol(format!(
1734 "Program tag discovery failed for '{}': {}. Some PLCs may not support tag discovery. Try reading tags directly by name.",
1735 program_name, e
1736 )));
1737 }
1738
1739 // Parse response
1740 self.parse_tag_list_response(&cip_data)
1741 }
1742
1743 /// Lists all cached tag attributes
1744 pub async fn list_cached_tag_attributes(&self) -> Vec<String> {
1745 self.udt_manager.lock().await.list_tag_attributes()
1746 }
1747
1748 /// Clears all caches (UDT definitions, templates, tag attributes)
1749 pub async fn clear_caches(&mut self) {
1750 self.udt_manager.lock().await.clear_cache();
1751 }
1752
1753 /// Creates a new client with a specific route path
1754 pub async fn with_route_path(addr: &str, route: RoutePath) -> crate::error::Result<Self> {
1755 let mut client = Self::new(addr).await?;
1756 client.set_route_path(route);
1757 Ok(client)
1758 }
1759
1760 /// Connect to a PLC using a custom stream
1761 ///
1762 /// This method allows you to provide your own stream implementation, enabling:
1763 /// - Wrapping streams for metrics/observability (bytes in/out)
1764 /// - Applying custom socket options (keepalive, timeouts, bind local address)
1765 /// - Reusing pre-established tunnels/connections
1766 /// - Using in-memory streams for deterministic testing
1767 ///
1768 /// # Arguments
1769 ///
1770 /// * `stream` - Any stream that implements `AsyncRead + AsyncWrite + Unpin + Send`
1771 ///
1772 /// # Example
1773 ///
1774 /// ```no_run
1775 /// use rust_ethernet_ip::EipClient;
1776 /// use std::net::SocketAddr;
1777 /// use tokio::net::TcpStream;
1778 ///
1779 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1780 /// // Create a custom stream with socket options
1781 /// let addr: SocketAddr = "192.168.1.100:44818".parse()?;
1782 /// let stream = TcpStream::connect(addr).await?;
1783 /// stream.set_nodelay(true)?;
1784 ///
1785 /// // Connect using the custom stream
1786 /// let client = EipClient::connect_with_stream(stream, None).await?;
1787 /// # Ok(())
1788 /// # }
1789 /// ```
1790 pub async fn connect_with_stream<S>(stream: S, route: Option<RoutePath>) -> Result<Self>
1791 where
1792 S: EtherNetIpStream + 'static,
1793 {
1794 let mut client = Self::from_stream(stream).await?;
1795 if let Some(route) = route {
1796 client.set_route_path(route);
1797 }
1798 Ok(client)
1799 }
1800
1801 /// Sets the route path for the client
1802 pub fn set_route_path(&mut self, route: RoutePath) {
1803 self.route_path = Some(route);
1804 }
1805
1806 /// Gets the current route path
1807 pub fn get_route_path(&self) -> Option<&RoutePath> {
1808 self.route_path.as_ref()
1809 }
1810
1811 /// Removes the route path (uses direct connection)
1812 pub fn clear_route_path(&mut self) {
1813 self.route_path = None;
1814 }
1815
1816 /// Gets metadata for a tag
1817 pub async fn get_tag_metadata(&self, tag_name: &str) -> Option<TagMetadata> {
1818 let tag_manager = self.tag_manager.lock().await;
1819 let cache = tag_manager.cache.read().unwrap();
1820 let result = cache.get(tag_name).cloned();
1821 result
1822 }
1823
1824 /// Reads a tag value from the PLC
1825 ///
1826 /// This function performs a CIP read request for the specified tag.
1827 /// The tag's data type is automatically determined from the PLC's response.
1828 ///
1829 /// **v0.6.0**: For UDT tags, this returns `PlcValue::Udt(UdtData)` with `symbol_id`
1830 /// and raw bytes. Use `UdtData::parse()` with a UDT definition to access members.
1831 ///
1832 /// # Arguments
1833 ///
1834 /// * `tag_name` - The name of the tag to read
1835 ///
1836 /// # Returns
1837 ///
1838 /// The tag's value as a `PlcValue` enum. For UDTs, this is `PlcValue::Udt(UdtData)`.
1839 ///
1840 /// # Examples
1841 ///
1842 /// ```rust,no_run
1843 /// use rust_ethernet_ip::{EipClient, PlcValue};
1844 ///
1845 /// #[tokio::main]
1846 /// async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1847 /// let mut client = EipClient::connect("192.168.1.100:44818").await?;
1848 ///
1849 /// // Read different data types
1850 /// let bool_val = client.read_tag("MotorRunning").await?;
1851 /// let int_val = client.read_tag("Counter").await?;
1852 /// let real_val = client.read_tag("Temperature").await?;
1853 ///
1854 /// // Read a UDT (v0.6.0: returns UdtData)
1855 /// let udt_val = client.read_tag("MyUDT").await?;
1856 /// if let PlcValue::Udt(udt_data) = udt_val {
1857 /// let udt_def = client.get_udt_definition("MyUDT").await?;
1858 /// // Convert UdtDefinition to UserDefinedType
1859 /// let mut user_def = rust_ethernet_ip::udt::UserDefinedType::new(udt_def.name.clone());
1860 /// for member in &udt_def.members {
1861 /// user_def.add_member(member.clone());
1862 /// }
1863 /// let members = udt_data.parse(&user_def)?;
1864 /// println!("UDT has {} members", members.len());
1865 /// }
1866 ///
1867 /// // Handle the result
1868 /// match bool_val {
1869 /// PlcValue::Bool(true) => println!("Motor is running"),
1870 /// PlcValue::Bool(false) => println!("Motor is stopped"),
1871 /// _ => println!("Unexpected data type"),
1872 /// }
1873 /// Ok(())
1874 /// }
1875 /// ```
1876 ///
1877 /// # Performance
1878 ///
1879 /// - Latency: 1-5ms typical
1880 /// - Throughput: 1,500+ ops/sec
1881 /// - Network: 1 request/response cycle
1882 ///
1883 /// # Error Handling
1884 ///
1885 /// Common errors:
1886 /// - `Protocol`: Tag doesn't exist or invalid format
1887 /// - `Connection`: Lost connection to PLC
1888 /// - `Timeout`: Operation timed out
1889 pub async fn read_tag(&mut self, tag_name: &str) -> crate::error::Result<PlcValue> {
1890 self.validate_session().await?;
1891
1892 // Check if this is a simple array element access (e.g., "ArrayName[0]")
1893 // BUT NOT if it has member access after (e.g., "ArrayName[0].Member")
1894 // Complex paths like "gTestUDT_Array[0].Member1_DINT" should use TagPath::parse()
1895 if let Some((base_name, index)) = self.parse_array_element_access(tag_name) {
1896 // Only use workaround if there's no member access after the array brackets
1897 // Find the FIRST [ and ] pair to check for member access after it
1898 if let Some(bracket_start) = tag_name.find('[') {
1899 if let Some(bracket_end_rel) = tag_name[bracket_start..].find(']') {
1900 let bracket_end_abs = bracket_start + bracket_end_rel;
1901 let after_bracket = &tag_name[bracket_end_abs + 1..];
1902 tracing::debug!(
1903 "Array element detected for '{}': base='{}', index={}, after_bracket='{}'",
1904 tag_name,
1905 base_name,
1906 index,
1907 after_bracket
1908 );
1909 // If there's a dot after the bracket, it's a member access - use TagPath::parse() instead
1910 if !after_bracket.starts_with('.') {
1911 tracing::debug!(
1912 "Detected simple array element access: {}[{}], using workaround",
1913 base_name,
1914 index
1915 );
1916 return self.read_array_element_workaround(&base_name, index).await;
1917 } else {
1918 tracing::debug!(
1919 "Array element '{}[{}]' has member access after bracket ('{}'), using TagPath::parse()",
1920 base_name,
1921 index,
1922 after_bracket
1923 );
1924 }
1925 }
1926 }
1927 }
1928
1929 // For complex paths (with member access, nested arrays, etc.), use TagPath::parse()
1930 // This handles paths like "gTestUDT_Array[0].Member1_DINT" correctly
1931 // Standard tag reading uses build_read_request which uses TagPath::parse()
1932 let response = self
1933 .send_cip_request(&self.build_read_request(tag_name))
1934 .await?;
1935 let cip_data = self.extract_cip_from_response(&response)?;
1936 self.parse_cip_response(&cip_data)
1937 }
1938
1939 /// Reads a single bit from a tag (e.g. a DINT used as a status word).
1940 ///
1941 /// Equivalent to `read_tag(&format!("{}.{}", tag_base, bit_index))` for bit paths.
1942 /// `bit_index` must be in 0..32 (Allen-Bradley DINT bits).
1943 ///
1944 /// # Example
1945 ///
1946 /// ```ignore
1947 /// let bit_5 = client.read_bit("StatusWord", 5).await?;
1948 /// ```
1949 pub async fn read_bit(&mut self, tag_base: &str, bit_index: u8) -> crate::error::Result<bool> {
1950 if bit_index >= 32 {
1951 return Err(crate::error::EtherNetIpError::Protocol(
1952 "bit_index must be 0..32 for DINT bit access".to_string(),
1953 ));
1954 }
1955 let path = format!("{}.{}", tag_base, bit_index);
1956 match self.read_tag(&path).await? {
1957 PlcValue::Bool(b) => Ok(b),
1958 PlcValue::Dint(n) => {
1959 // Some PLCs/simulators return the full DINT for bit paths; extract the bit
1960 Ok((n >> bit_index) & 1 != 0)
1961 }
1962 other => Err(crate::error::EtherNetIpError::DataTypeMismatch {
1963 expected: "BOOL or DINT".to_string(),
1964 actual: format!("{:?}", other),
1965 }),
1966 }
1967 }
1968
1969 /// Writes a single bit to a tag (e.g. a DINT used as a control word).
1970 ///
1971 /// Equivalent to `write_tag(&format!("{}.{}", tag_base, bit_index), PlcValue::Bool(value))`.
1972 /// `bit_index` must be in 0..32.
1973 ///
1974 /// # Example
1975 ///
1976 /// ```ignore
1977 /// client.write_bit("ControlWord", 3, true).await?;
1978 /// ```
1979 pub async fn write_bit(
1980 &mut self,
1981 tag_base: &str,
1982 bit_index: u8,
1983 value: bool,
1984 ) -> crate::error::Result<()> {
1985 if bit_index >= 32 {
1986 return Err(crate::error::EtherNetIpError::Protocol(
1987 "bit_index must be 0..32 for DINT bit access".to_string(),
1988 ));
1989 }
1990 let path = format!("{}.{}", tag_base, bit_index);
1991 self.write_tag(&path, PlcValue::Bool(value)).await
1992 }
1993
1994 /// Parses array element access syntax (e.g., "ArrayName[0]") and returns (base_name, index)
1995 fn parse_array_element_access(&self, tag_name: &str) -> Option<(String, u32)> {
1996 // Look for array bracket notation
1997 if let Some(bracket_pos) = tag_name.rfind('[') {
1998 if let Some(close_bracket_pos) = tag_name.rfind(']') {
1999 if close_bracket_pos > bracket_pos {
2000 let base_name = tag_name[..bracket_pos].to_string();
2001 let index_str = &tag_name[bracket_pos + 1..close_bracket_pos];
2002 if let Ok(index) = index_str.parse::<u32>() {
2003 // Make sure there are no more brackets after this (multi-dimensional arrays not supported yet)
2004 if !tag_name[..bracket_pos].contains('[') {
2005 return Some((base_name, index));
2006 }
2007 }
2008 }
2009 }
2010 }
2011 None
2012 }
2013
2014 /// Reads a single array element using proper CIP element addressing
2015 ///
2016 /// This method uses element addressing (0x28/0x29/0x2A segments) in the Request Path
2017 /// to read directly from the specified array index, eliminating the need to read
2018 /// the entire array.
2019 ///
2020 /// Reference: 1756-PM020, Pages 603-611, 815-837 (Array Element Access Examples)
2021 ///
2022 /// # Arguments
2023 ///
2024 /// * `base_array_name` - Base name of the array (e.g., "MyArray" for "MyArray[5]")
2025 /// * `index` - Element index to read (0-based)
2026 async fn read_array_element_workaround(
2027 &mut self,
2028 base_array_name: &str,
2029 index: u32,
2030 ) -> crate::error::Result<PlcValue> {
2031 tracing::debug!(
2032 "Reading array element '{}[{}]' using element addressing",
2033 base_array_name,
2034 index
2035 );
2036
2037 // First, detect if it's a BOOL array by reading with count=1 to check data type
2038 let test_response = self
2039 .send_cip_request(&self.build_read_request_with_count(base_array_name, 1))
2040 .await?;
2041 let test_cip_data = self.extract_cip_from_response(&test_response)?;
2042
2043 // Check for errors in test read
2044 self.check_cip_error(&test_cip_data)?;
2045
2046 // Check if it's a BOOL array (data type 0x00D3 = DWORD)
2047 if test_cip_data.len() >= 6 {
2048 let test_data_type = u16::from_le_bytes([test_cip_data[4], test_cip_data[5]]);
2049 if test_data_type == 0x00D3 {
2050 // BOOL array - use special workaround to extract the bit
2051 return self
2052 .read_bool_array_element_workaround(base_array_name, index)
2053 .await;
2054 }
2055 }
2056
2057 // Use element addressing to read directly from the specified index
2058 // Reference: 1756-PM020, Pages 815-837 (Reading Array Element - Full Message)
2059 let request = self.build_read_array_request(base_array_name, index, 1);
2060
2061 let response = self.send_cip_request(&request).await?;
2062 let cip_data = self.extract_cip_from_response(&response)?;
2063
2064 // Check for errors (including extended errors)
2065 self.check_cip_error(&cip_data)?;
2066
2067 // Parse response - should be consistent format now
2068 // Reference: 1756-PM020, Page 828-837 (Response format)
2069 self.parse_cip_response(&cip_data)
2070 }
2071
2072 /// Special workaround for BOOL arrays: reads DWORD and extracts the specific bit
2073 ///
2074 /// Reference: 1756-PM020, Page 797-811 (BOOL Array Access)
2075 async fn read_bool_array_element_workaround(
2076 &mut self,
2077 base_array_name: &str,
2078 index: u32,
2079 ) -> crate::error::Result<PlcValue> {
2080 tracing::debug!(
2081 "BOOL array detected - reading DWORD and extracting bit [{}]",
2082 index
2083 );
2084
2085 // Read just 1 element (the DWORD containing 32 BOOLs)
2086 // Reference: 1756-PM020, Page 797-811
2087 let response = self
2088 .send_cip_request(&self.build_read_request_with_count(base_array_name, 1))
2089 .await?;
2090 let cip_data = self.extract_cip_from_response(&response)?;
2091
2092 // Parse the response
2093 if cip_data.len() < 6 {
2094 return Err(EtherNetIpError::Protocol(
2095 "BOOL array response too short".to_string(),
2096 ));
2097 }
2098
2099 // Check for errors (including extended errors)
2100 self.check_cip_error(&cip_data)?;
2101
2102 let service_reply = cip_data[0];
2103 if service_reply != 0xCC {
2104 return Err(EtherNetIpError::Protocol(format!(
2105 "Unexpected service reply: 0x{service_reply:02X}"
2106 )));
2107 }
2108
2109 let data_type = u16::from_le_bytes([cip_data[4], cip_data[5]]);
2110
2111 // Check response format - might have element count or just data
2112 // Reference: 1756-PM020, Page 828-837 (Response format)
2113 let value_data = if cip_data.len() >= 8 && data_type == 0x00D3 {
2114 // Check if there's an element count field (bytes 6-7)
2115 // For BOOL arrays with count=1, we should get just the DWORD data
2116 if cip_data.len() >= 12 {
2117 // Has element count field
2118 &cip_data[8..]
2119 } else if cip_data.len() >= 10 {
2120 // No element count, data starts at byte 6
2121 &cip_data[6..]
2122 } else {
2123 return Err(EtherNetIpError::Protocol(
2124 "BOOL array response too short for data".to_string(),
2125 ));
2126 }
2127 } else {
2128 // Standard format with element count
2129 if cip_data.len() < 8 {
2130 return Err(EtherNetIpError::Protocol(
2131 "BOOL array response too short".to_string(),
2132 ));
2133 }
2134 &cip_data[8..]
2135 };
2136
2137 // For BOOL arrays, the data is a DWORD (4 bytes) containing 32 BOOLs
2138 if value_data.len() < 4 {
2139 return Err(EtherNetIpError::Protocol(format!(
2140 "BOOL array data too short: need 4 bytes (DWORD), got {} bytes",
2141 value_data.len()
2142 )));
2143 }
2144
2145 let dword_value =
2146 u32::from_le_bytes([value_data[0], value_data[1], value_data[2], value_data[3]]);
2147
2148 // Extract the specific bit
2149 // Each DWORD contains 32 BOOLs (bits 0-31)
2150 let bit_index = (index % 32) as u8;
2151 let bool_value = (dword_value >> bit_index) & 1 != 0;
2152
2153 Ok(PlcValue::Bool(bool_value))
2154 }
2155
2156 /// Helper function to read large arrays in chunks to avoid PLC response size limits
2157 ///
2158 /// This method uses element addressing to read specific ranges of array elements,
2159 /// allowing efficient reading of large arrays without reading from element 0 each time.
2160 ///
2161 /// Reference: 1756-PM020, Pages 276-315 (Read Tag Fragmented Service), 840-851 (Reading Multiple Array Elements)
2162 async fn read_array_in_chunks(
2163 &mut self,
2164 base_array_name: &str,
2165 data_type: u16,
2166 start_index: u32,
2167 target_element_count: u32,
2168 ) -> crate::error::Result<Vec<u8>> {
2169 // Determine element size and safe chunk size
2170 let element_size = match data_type {
2171 0x00C1 => 1, // BOOL
2172 0x00C2 => 1, // SINT
2173 0x00C3 => 2, // INT
2174 0x00C4 => 4, // DINT
2175 0x00C5 => 8, // LINT
2176 0x00C6 => 1, // USINT
2177 0x00C7 => 2, // UINT
2178 0x00C8 => 4, // UDINT
2179 0x00C9 => 8, // ULINT
2180 0x00CA => 4, // REAL
2181 0x00CB => 8, // LREAL
2182 _ => {
2183 return Err(EtherNetIpError::Protocol(format!(
2184 "Unsupported array data type for chunked reading: 0x{:04X}",
2185 data_type
2186 )));
2187 }
2188 };
2189
2190 // Read in chunks - use 8 elements per chunk for 4-byte types to stay under 38-byte limit
2191 // For smaller types, we can read more elements per chunk
2192 let elements_per_chunk = match element_size {
2193 1 => 30, // 1-byte types: 30 elements = 30 bytes + 8 header = 38 bytes
2194 2 => 15, // 2-byte types: 15 elements = 30 bytes + 8 header = 38 bytes
2195 4 => 8, // 4-byte types: 8 elements = 32 bytes + 8 header = 40 bytes (may truncate to 38)
2196 8 => 4, // 8-byte types: 4 elements = 32 bytes + 8 header = 40 bytes
2197 _ => 8,
2198 };
2199
2200 let end_index = start_index
2201 .checked_add(target_element_count)
2202 .ok_or_else(|| EtherNetIpError::Protocol("Array range overflow".to_string()))?;
2203
2204 let mut all_data = Vec::new();
2205 let mut next_chunk_start = start_index;
2206
2207 tracing::debug!(
2208 "Reading array '{}' in chunks: {} elements per chunk, target: {} elements",
2209 base_array_name,
2210 elements_per_chunk,
2211 target_element_count
2212 );
2213
2214 while next_chunk_start < end_index {
2215 // Use element addressing to read specific range starting from next_chunk_start
2216 // Reference: 1756-PM020, Pages 840-851 (Reading Multiple Array Elements)
2217 let chunk_end = (next_chunk_start + elements_per_chunk as u32).min(end_index);
2218 let chunk_size = (chunk_end - next_chunk_start) as u16;
2219
2220 tracing::trace!(
2221 "Reading chunk: elements {} to {} ({} elements) using element addressing",
2222 next_chunk_start,
2223 chunk_end - 1,
2224 chunk_size
2225 );
2226
2227 // Use element addressing to read this specific range
2228 // Reference: 1756-PM020, Pages 840-851 (Reading Multiple Array Elements)
2229 let response = self
2230 .send_cip_request(&self.build_read_array_request(
2231 base_array_name,
2232 next_chunk_start,
2233 chunk_size,
2234 ))
2235 .await?;
2236 let cip_data = self.extract_cip_from_response(&response)?;
2237
2238 if cip_data.len() < 8 {
2239 // Response too short - might be an error or empty response
2240 // Check if it's a CIP error response
2241 if cip_data.len() >= 3 {
2242 let general_status = cip_data[2];
2243 if general_status != 0x00 {
2244 let error_msg = self.get_cip_error_message(general_status);
2245 return Err(EtherNetIpError::Protocol(format!(
2246 "CIP Error {} when reading chunk (elements {} to {}): {}",
2247 general_status,
2248 next_chunk_start,
2249 chunk_end - 1,
2250 error_msg
2251 )));
2252 }
2253 }
2254 return Err(EtherNetIpError::Protocol(format!(
2255 "Chunk response too short: got {} bytes, expected at least 8 (requested {} elements starting at {})",
2256 cip_data.len(), chunk_size, next_chunk_start
2257 )));
2258 }
2259
2260 // Check for CIP errors in the response
2261 if cip_data.len() >= 3 {
2262 let general_status = cip_data[2];
2263 if general_status != 0x00 {
2264 let error_msg = self.get_cip_error_message(general_status);
2265 return Err(EtherNetIpError::Protocol(format!(
2266 "CIP Error {} when reading chunk (elements {} to {}): {}",
2267 general_status,
2268 next_chunk_start,
2269 chunk_end - 1,
2270 error_msg
2271 )));
2272 }
2273 }
2274
2275 // Check service reply
2276 if !cip_data.is_empty() && cip_data[0] != 0xCC {
2277 return Err(EtherNetIpError::Protocol(format!(
2278 "Unexpected service reply in chunk: 0x{:02X} (expected 0xCC)",
2279 cip_data[0]
2280 )));
2281 }
2282
2283 if cip_data.len() < 6 {
2284 return Err(EtherNetIpError::Protocol(format!(
2285 "Chunk response too short for data type: got {} bytes, expected at least 6",
2286 cip_data.len()
2287 )));
2288 }
2289
2290 let chunk_data_type = u16::from_le_bytes([cip_data[4], cip_data[5]]);
2291 if chunk_data_type != data_type {
2292 return Err(EtherNetIpError::Protocol(format!(
2293 "Data type mismatch in chunk: expected 0x{:04X}, got 0x{:04X}",
2294 data_type, chunk_data_type
2295 )));
2296 }
2297
2298 // Parse response data - with element addressing, response contains the requested range
2299 // Reference: 1756-PM020, Page 828-837 (Response format)
2300 let value_data_start = if cip_data.len() >= 8 {
2301 // Standard format: [service][reserved][status][status_size][data_type(2)][element_count(2)][data...]
2302 8
2303 } else {
2304 6
2305 };
2306
2307 let chunk_value_data = &cip_data[value_data_start..];
2308 let chunk_complete_bytes = (chunk_value_data.len() / element_size) * element_size;
2309 let chunk_data = &chunk_value_data[..chunk_complete_bytes];
2310
2311 // With element addressing, the response directly contains the requested range
2312 // No need to extract a portion - use all the data we received
2313 if !chunk_data.is_empty() {
2314 all_data.extend_from_slice(chunk_data);
2315 let elements_received = chunk_data.len() / element_size;
2316 next_chunk_start += elements_received as u32;
2317
2318 tracing::trace!(
2319 "Chunk read: {} elements ({} bytes) starting at index {}, total so far: {} elements",
2320 elements_received,
2321 chunk_data.len(),
2322 next_chunk_start - elements_received as u32,
2323 all_data.len() / element_size
2324 );
2325
2326 // Continue reading if we haven't reached our target yet
2327 if next_chunk_start >= end_index {
2328 tracing::trace!(
2329 "Reached target element count ({}), stopping chunked read",
2330 target_element_count
2331 );
2332 break;
2333 }
2334 } else {
2335 // No data received, we're done
2336 break;
2337 }
2338 }
2339
2340 let final_element_count = all_data.len() / element_size;
2341 tracing::debug!(
2342 "Chunked read complete: {} total elements ({} bytes), target was {} elements",
2343 final_element_count,
2344 all_data.len(),
2345 target_element_count
2346 );
2347
2348 if final_element_count < target_element_count as usize {
2349 return Err(EtherNetIpError::Protocol(format!(
2350 "Incomplete array read: requested {} elements, received {}",
2351 target_element_count, final_element_count
2352 )));
2353 }
2354
2355 Ok(all_data)
2356 }
2357
2358 fn array_element_size(data_type: u16) -> Option<usize> {
2359 match data_type {
2360 0x00C1 => Some(1), // BOOL
2361 0x00C2 => Some(1), // SINT
2362 0x00C3 => Some(2), // INT
2363 0x00C4 => Some(4), // DINT
2364 0x00C5 => Some(8), // LINT
2365 0x00C6 => Some(1), // USINT
2366 0x00C7 => Some(2), // UINT
2367 0x00C8 => Some(4), // UDINT
2368 0x00C9 => Some(8), // ULINT
2369 0x00CA => Some(4), // REAL
2370 0x00CB => Some(8), // LREAL
2371 _ => None,
2372 }
2373 }
2374
2375 fn decode_array_bytes(
2376 &self,
2377 data_type: u16,
2378 bytes: &[u8],
2379 ) -> crate::error::Result<Vec<PlcValue>> {
2380 let Some(element_size) = Self::array_element_size(data_type) else {
2381 return Err(EtherNetIpError::Protocol(format!(
2382 "Unsupported data type for array decoding: 0x{:04X}",
2383 data_type
2384 )));
2385 };
2386
2387 if bytes.len() % element_size != 0 {
2388 return Err(EtherNetIpError::Protocol(format!(
2389 "Array payload length {} is not aligned to element size {}",
2390 bytes.len(),
2391 element_size
2392 )));
2393 }
2394
2395 let mut values = Vec::with_capacity(bytes.len() / element_size);
2396 for chunk in bytes.chunks_exact(element_size) {
2397 let value = match data_type {
2398 0x00C1 => PlcValue::Bool(chunk[0] != 0),
2399 0x00C2 => PlcValue::Sint(chunk[0] as i8),
2400 0x00C3 => PlcValue::Int(i16::from_le_bytes([chunk[0], chunk[1]])),
2401 0x00C4 => {
2402 PlcValue::Dint(i32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
2403 }
2404 0x00C5 => PlcValue::Lint(i64::from_le_bytes([
2405 chunk[0], chunk[1], chunk[2], chunk[3], chunk[4], chunk[5], chunk[6], chunk[7],
2406 ])),
2407 0x00C6 => PlcValue::Usint(chunk[0]),
2408 0x00C7 => PlcValue::Uint(u16::from_le_bytes([chunk[0], chunk[1]])),
2409 0x00C8 => {
2410 PlcValue::Udint(u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
2411 }
2412 0x00C9 => PlcValue::Ulint(u64::from_le_bytes([
2413 chunk[0], chunk[1], chunk[2], chunk[3], chunk[4], chunk[5], chunk[6], chunk[7],
2414 ])),
2415 0x00CA => {
2416 PlcValue::Real(f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
2417 }
2418 0x00CB => PlcValue::Lreal(f64::from_le_bytes([
2419 chunk[0], chunk[1], chunk[2], chunk[3], chunk[4], chunk[5], chunk[6], chunk[7],
2420 ])),
2421 _ => unreachable!("validated by array_element_size"),
2422 };
2423 values.push(value);
2424 }
2425
2426 Ok(values)
2427 }
2428
2429 /// Read a range of elements from a basic-type PLC array.
2430 ///
2431 /// This method reads arrays in chunks under the hood to avoid PLC packet-size limits.
2432 /// It supports basic CIP scalar types:
2433 /// BOOL, SINT, INT, DINT, LINT, USINT, UINT, UDINT, ULINT, REAL, LREAL.
2434 ///
2435 /// # Arguments
2436 ///
2437 /// * `base_array_name` - Base array tag name without index (e.g., `"MyDintArray"`)
2438 /// * `start_index` - Starting element index
2439 /// * `element_count` - Number of elements to read
2440 ///
2441 /// # Returns
2442 ///
2443 /// A `Vec<PlcValue>` with one element per requested array entry.
2444 pub async fn read_array_range(
2445 &mut self,
2446 base_array_name: &str,
2447 start_index: u32,
2448 element_count: u32,
2449 ) -> crate::error::Result<Vec<PlcValue>> {
2450 if element_count == 0 {
2451 return Ok(Vec::new());
2452 }
2453
2454 let probe_response = self
2455 .send_cip_request(&self.build_read_array_request(base_array_name, start_index, 1))
2456 .await?;
2457 let probe_cip = self.extract_cip_from_response(&probe_response)?;
2458 self.check_cip_error(&probe_cip)?;
2459
2460 if probe_cip.len() < 6 {
2461 return Err(EtherNetIpError::Protocol(
2462 "Array probe response too short".to_string(),
2463 ));
2464 }
2465
2466 let data_type = u16::from_le_bytes([probe_cip[4], probe_cip[5]]);
2467 let raw = self
2468 .read_array_in_chunks(base_array_name, data_type, start_index, element_count)
2469 .await?;
2470 let values = self.decode_array_bytes(data_type, &raw)?;
2471
2472 if values.len() != element_count as usize {
2473 return Err(EtherNetIpError::Protocol(format!(
2474 "Array read count mismatch: requested {}, got {}",
2475 element_count,
2476 values.len()
2477 )));
2478 }
2479
2480 Ok(values)
2481 }
2482
2483 /// Writes to a single array element using direct element addressing
2484 ///
2485 /// This method uses element addressing (0x28/0x29/0x2A segments) in the Request Path
2486 /// to write directly to the specified array index, eliminating the need to read
2487 /// the entire array.
2488 ///
2489 /// Reference: 1756-PM020, Pages 855-867 (Writing to Array Element)
2490 ///
2491 /// # Arguments
2492 ///
2493 /// * `base_array_name` - Base name of the array (e.g., "MyArray" for "MyArray[10]")
2494 /// * `index` - Element index to write (0-based)
2495 /// * `value` - The value to write
2496 async fn write_array_element_workaround(
2497 &mut self,
2498 base_array_name: &str,
2499 index: u32,
2500 value: PlcValue,
2501 ) -> crate::error::Result<()> {
2502 tracing::debug!(
2503 "Writing to array element '{}[{}]' using element addressing",
2504 base_array_name,
2505 index
2506 );
2507
2508 // First, detect if it's a BOOL array by reading with count=1
2509 let test_response = self
2510 .send_cip_request(&self.build_read_request_with_count(base_array_name, 1))
2511 .await?;
2512 let test_cip_data = self.extract_cip_from_response(&test_response)?;
2513
2514 // Check for errors in the test read response
2515 if test_cip_data.len() < 3 {
2516 return Err(EtherNetIpError::Protocol(
2517 "Test read response too short".to_string(),
2518 ));
2519 }
2520
2521 // Check for errors in test read (including extended errors)
2522 if let Err(e) = self.check_cip_error(&test_cip_data) {
2523 return Err(EtherNetIpError::Protocol(format!(
2524 "Cannot write to array element: Test read failed: {}",
2525 e
2526 )));
2527 }
2528
2529 // Check if we have enough data to determine the data type
2530 if test_cip_data.len() < 6 {
2531 return Err(EtherNetIpError::Protocol(
2532 "Test read response too short to determine data type".to_string(),
2533 ));
2534 }
2535
2536 let test_data_type = u16::from_le_bytes([test_cip_data[4], test_cip_data[5]]);
2537
2538 // If it's a BOOL array (0x00D3 = DWORD), handle it specially
2539 if test_data_type == 0x00D3 {
2540 return self
2541 .write_bool_array_element_workaround(base_array_name, index, value)
2542 .await;
2543 }
2544
2545 // Get the data type and convert value to bytes
2546 let data_type = test_data_type;
2547 let value_bytes = value.to_bytes();
2548
2549 // Use element addressing to write directly to the specified index
2550 // Reference: 1756-PM020, Pages 855-867
2551 let request = self.build_write_array_request_with_index(
2552 base_array_name,
2553 index,
2554 1, // Write 1 element
2555 data_type,
2556 &value_bytes,
2557 )?;
2558
2559 let response = self.send_cip_request(&request).await?;
2560 let cip_data = self.extract_cip_from_response(&response)?;
2561
2562 // Check for errors (including extended errors)
2563 self.check_cip_error(&cip_data)?;
2564
2565 tracing::info!("Array element write completed successfully");
2566 Ok(())
2567 }
2568
2569 /// Special workaround for BOOL arrays: reads DWORD, modifies bit, writes back.
2570 ///
2571 /// Note: This is a read-modify-write operation. Callers must ensure exclusive
2572 /// access to the client for the entire duration (the `&mut self` requirement
2573 /// provides this guarantee in safe Rust; FFI callers are protected by the global mutex).
2574 ///
2575 /// Reference: 1756-PM020, Page 797-811 (BOOL Array Access)
2576 async fn write_bool_array_element_workaround(
2577 &mut self,
2578 base_array_name: &str,
2579 index: u32,
2580 value: PlcValue,
2581 ) -> crate::error::Result<()> {
2582 tracing::debug!(
2583 "BOOL array element write - reading DWORD, modifying bit [{}], writing back",
2584 index
2585 );
2586
2587 // Read the DWORD
2588 let response = self
2589 .send_cip_request(&self.build_read_request_with_count(base_array_name, 1))
2590 .await?;
2591 let cip_data = self.extract_cip_from_response(&response)?;
2592
2593 // BOOL array response format: [0]=service, [1]=reserved, [2]=status, [3]=additional_status_size,
2594 // [4-5]=data_type, [6-9]=data (DWORD, 4 bytes)
2595 // Minimum size is 10 bytes (no element count field when count=1)
2596 if cip_data.len() < 10 {
2597 return Err(EtherNetIpError::Protocol(
2598 "BOOL array response too short".to_string(),
2599 ));
2600 }
2601
2602 // Check for errors (including extended errors)
2603 self.check_cip_error(&cip_data)?;
2604
2605 let service_reply = cip_data[0];
2606 if service_reply != 0xCC {
2607 return Err(EtherNetIpError::Protocol(format!(
2608 "Unexpected service reply: 0x{service_reply:02X}"
2609 )));
2610 }
2611
2612 let data_type = u16::from_le_bytes([cip_data[4], cip_data[5]]);
2613
2614 // Extract DWORD data (4 bytes)
2615 // For BOOL arrays with count=1, data starts at byte 6 (no element count field)
2616 let value_data = if cip_data.len() >= 10 {
2617 &cip_data[6..10]
2618 } else {
2619 return Err(EtherNetIpError::Protocol(
2620 "BOOL array data too short".to_string(),
2621 ));
2622 };
2623
2624 // Get the boolean value
2625 let bool_value = match value {
2626 PlcValue::Bool(b) => b,
2627 _ => {
2628 return Err(EtherNetIpError::Protocol(
2629 "Expected BOOL value for BOOL array element".to_string(),
2630 ))
2631 }
2632 };
2633
2634 // Modify the DWORD
2635 let mut dword_value =
2636 u32::from_le_bytes([value_data[0], value_data[1], value_data[2], value_data[3]]);
2637
2638 let bit_index = (index % 32) as u8;
2639 if bool_value {
2640 dword_value |= 1u32 << bit_index;
2641 } else {
2642 dword_value &= !(1u32 << bit_index);
2643 }
2644
2645 tracing::trace!(
2646 "Modified BOOL[{}] in DWORD: 0x{:08X} -> 0x{:08X} (bit {} = {})",
2647 index,
2648 u32::from_le_bytes([value_data[0], value_data[1], value_data[2], value_data[3]]),
2649 dword_value,
2650 bit_index,
2651 bool_value
2652 );
2653
2654 // Write the DWORD back
2655 let write_request = self.build_write_request_with_data(
2656 base_array_name,
2657 data_type,
2658 1,
2659 &dword_value.to_le_bytes(),
2660 )?;
2661 let write_response = self.send_cip_request(&write_request).await?;
2662 let write_cip_data = self.extract_cip_from_response(&write_response)?;
2663
2664 // Check for errors (including extended errors)
2665 self.check_cip_error(&write_cip_data)?;
2666
2667 tracing::info!("BOOL array element write completed successfully");
2668 Ok(())
2669 }
2670
2671 /// Builds a write request for an entire array (legacy method - writes from element 0)
2672 ///
2673 /// Reference: 1756-PM020, Page 318-357 (Write Tag Service)
2674 #[allow(dead_code)]
2675 fn build_write_array_request(
2676 &self,
2677 tag_name: &str,
2678 data_type: u16,
2679 element_count: u16,
2680 data: &[u8],
2681 ) -> crate::error::Result<Vec<u8>> {
2682 let mut cip_request = Vec::new();
2683
2684 // Service: Write Tag Service (0x4D)
2685 // Reference: 1756-PM020, Page 318
2686 cip_request.push(0x4D);
2687
2688 // Build the path
2689 let path = self.build_tag_path(tag_name);
2690 cip_request.push((path.len() / 2) as u8);
2691 cip_request.extend_from_slice(&path);
2692
2693 // Data type and element count
2694 // Reference: 1756-PM020, Page 335-337 (Request Data format)
2695 cip_request.extend_from_slice(&data_type.to_le_bytes());
2696 cip_request.extend_from_slice(&element_count.to_le_bytes());
2697
2698 // Array data
2699 cip_request.extend_from_slice(data);
2700
2701 Ok(cip_request)
2702 }
2703
2704 /// Builds a CIP Write Tag Service request for array elements with element addressing
2705 ///
2706 /// This method uses proper CIP element addressing (0x28/0x29/0x2A segments) in the
2707 /// Request Path to write to specific array elements or ranges.
2708 ///
2709 /// Reference: 1756-PM020, Pages 603-611, 855-867 (Writing to Array Element)
2710 ///
2711 /// # Arguments
2712 ///
2713 /// * `base_array_name` - Base name of the array (e.g., "MyArray" for "MyArray[10]")
2714 /// * `start_index` - Starting element index (0-based)
2715 /// * `element_count` - Number of elements to write
2716 /// * `data_type` - CIP data type code (e.g., 0x00C4 for DINT)
2717 /// * `data` - Raw bytes of the data to write
2718 ///
2719 /// # Example
2720 ///
2721 /// Writing value 0x12345678 to element 10 of array "MyArray":
2722 /// ```
2723 /// # async fn example() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
2724 /// # let mut client = rust_ethernet_ip::EipClient::connect("192.168.1.100:44818").await?;
2725 /// let data = 0x12345678u32.to_le_bytes();
2726 /// let request = client.build_write_array_request_with_index(
2727 /// "MyArray", 10, 1, 0x00C4, &data
2728 /// )?;
2729 /// # Ok(())
2730 /// # }
2731 /// ```
2732 #[cfg_attr(not(test), allow(dead_code))]
2733 pub fn build_write_array_request_with_index(
2734 &self,
2735 base_array_name: &str,
2736 start_index: u32,
2737 element_count: u16,
2738 data_type: u16,
2739 data: &[u8],
2740 ) -> crate::error::Result<Vec<u8>> {
2741 let mut cip_request = Vec::new();
2742
2743 // Service: Write Tag Service (0x4D)
2744 // Reference: 1756-PM020, Page 318
2745 cip_request.push(0x4D);
2746
2747 // Build base tag path (symbolic segment)
2748 // Reference: 1756-PM020, Page 894-909
2749 let mut full_path = self.build_base_tag_path(base_array_name);
2750
2751 // Add element addressing segment
2752 // Reference: 1756-PM020, Pages 603-611, 870-890
2753 full_path.extend_from_slice(&self.build_element_id_segment(start_index));
2754
2755 // Ensure path is word-aligned
2756 if full_path.len() % 2 != 0 {
2757 full_path.push(0x00);
2758 }
2759
2760 // Path size (in words)
2761 let path_size = (full_path.len() / 2) as u8;
2762 cip_request.push(path_size);
2763 cip_request.extend_from_slice(&full_path);
2764
2765 // Request Data: Data type, element count, and data
2766 // Reference: 1756-PM020, Page 855-867 (Writing to Array Element - Full Message)
2767 cip_request.extend_from_slice(&data_type.to_le_bytes());
2768 cip_request.extend_from_slice(&element_count.to_le_bytes());
2769 cip_request.extend_from_slice(data);
2770
2771 Ok(cip_request)
2772 }
2773
2774 /// Builds a write request with raw data
2775 fn build_write_request_with_data(
2776 &self,
2777 tag_name: &str,
2778 data_type: u16,
2779 element_count: u16,
2780 data: &[u8],
2781 ) -> crate::error::Result<Vec<u8>> {
2782 let mut cip_request = Vec::new();
2783
2784 // Service: Write Tag Service (0x4D)
2785 cip_request.push(0x4D);
2786
2787 // Build the path
2788 let path = self.build_tag_path(tag_name);
2789 cip_request.push((path.len() / 2) as u8);
2790 cip_request.extend_from_slice(&path);
2791
2792 // Data type and element count
2793 cip_request.extend_from_slice(&data_type.to_le_bytes());
2794 cip_request.extend_from_slice(&element_count.to_le_bytes());
2795
2796 // Data
2797 cip_request.extend_from_slice(data);
2798
2799 Ok(cip_request)
2800 }
2801
2802 /// Reads a UDT with advanced chunked reading to handle large structures
2803 ///
2804 /// **v0.6.0**: Returns `PlcValue::Udt(UdtData)` with `symbol_id` and raw bytes.
2805 /// Use `UdtData::parse()` with a UDT definition to access individual members.
2806 ///
2807 /// This method uses multiple strategies to handle large UDTs that exceed
2808 /// the maximum packet size, including intelligent chunking and member discovery.
2809 ///
2810 /// # Arguments
2811 ///
2812 /// * `tag_name` - The name of the UDT tag to read
2813 ///
2814 /// # Returns
2815 ///
2816 /// `PlcValue::Udt(UdtData)` containing the symbol_id and raw data bytes
2817 ///
2818 /// # Example
2819 ///
2820 /// ```no_run
2821 /// # async fn example() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
2822 /// # let mut client = rust_ethernet_ip::EipClient::connect("192.168.1.100:44818").await?;
2823 /// let udt_value = client.read_udt_chunked("Part_Data").await?;
2824 /// if let rust_ethernet_ip::PlcValue::Udt(udt_data) = udt_value {
2825 /// println!("UDT symbol_id: {}, data size: {} bytes", udt_data.symbol_id, udt_data.data.len());
2826 /// // Parse members if needed
2827 /// let udt_def = client.get_udt_definition("Part_Data").await?;
2828 /// // Convert UdtDefinition to UserDefinedType
2829 /// let mut user_def = rust_ethernet_ip::udt::UserDefinedType::new(udt_def.name.clone());
2830 /// for member in &udt_def.members {
2831 /// user_def.add_member(member.clone());
2832 /// }
2833 /// let members = udt_data.parse(&user_def)?;
2834 /// }
2835 /// # Ok(())
2836 /// # }
2837 /// ```
2838 pub async fn read_udt_chunked(&mut self, tag_name: &str) -> crate::error::Result<PlcValue> {
2839 self.validate_session().await?;
2840
2841 tracing::debug!("[CHUNKED] Starting advanced UDT reading for: {}", tag_name);
2842
2843 // Strategy 1: Try normal read first
2844 match self.read_tag(tag_name).await {
2845 Ok(value) => {
2846 tracing::debug!("[CHUNKED] Normal read successful");
2847 return Ok(value);
2848 }
2849 Err(crate::error::EtherNetIpError::Protocol(msg))
2850 if msg.contains("Partial transfer") =>
2851 {
2852 tracing::debug!("[CHUNKED] Partial transfer detected, using advanced chunking");
2853 }
2854 Err(e) => {
2855 tracing::warn!("[CHUNKED] Normal read failed: {}", e);
2856 return Err(e);
2857 }
2858 }
2859
2860 // Strategy 2: Advanced chunked reading with multiple approaches
2861 self.read_udt_advanced_chunked(tag_name).await
2862 }
2863
2864 /// Advanced chunked UDT reading with multiple strategies
2865 async fn read_udt_advanced_chunked(
2866 &mut self,
2867 tag_name: &str,
2868 ) -> crate::error::Result<PlcValue> {
2869 tracing::debug!("[ADVANCED] Using multiple strategies for large UDT");
2870
2871 // Strategy A: Try different chunk sizes
2872 let chunk_sizes = vec![512, 256, 128, 64, 32, 16, 8, 4];
2873
2874 for chunk_size in chunk_sizes {
2875 tracing::trace!("[ADVANCED] Trying chunk size: {}", chunk_size);
2876
2877 match self.read_udt_with_chunk_size(tag_name, chunk_size).await {
2878 Ok(udt_value) => {
2879 tracing::debug!("[ADVANCED] Success with chunk size {}", chunk_size);
2880 return Ok(udt_value);
2881 }
2882 Err(e) => {
2883 tracing::trace!("[ADVANCED] Chunk size {} failed: {}", chunk_size, e);
2884 continue;
2885 }
2886 }
2887 }
2888
2889 // Strategy B: Try member-by-member discovery
2890 tracing::debug!("[ADVANCED] Trying member-by-member discovery");
2891 match self.read_udt_member_discovery(tag_name).await {
2892 Ok(udt_value) => {
2893 tracing::debug!("[ADVANCED] Member discovery successful");
2894 return Ok(udt_value);
2895 }
2896 Err(e) => {
2897 tracing::warn!("[ADVANCED] Member discovery failed: {}", e);
2898 }
2899 }
2900
2901 // Strategy C: Try progressive reading
2902 tracing::debug!("[ADVANCED] Trying progressive reading");
2903 match self.read_udt_progressive(tag_name).await {
2904 Ok(udt_value) => {
2905 tracing::debug!("[ADVANCED] Progressive reading successful");
2906 return Ok(udt_value);
2907 }
2908 Err(e) => {
2909 tracing::warn!("[ADVANCED] Progressive reading failed: {}", e);
2910 }
2911 }
2912
2913 // Strategy D: Fallback - try to get at least the symbol_id
2914 tracing::warn!("[ADVANCED] All strategies failed, using fallback");
2915 // Try to get tag attributes for symbol_id
2916 let symbol_id = self
2917 .get_tag_attributes(tag_name)
2918 .await
2919 .ok()
2920 .and_then(|attr| attr.template_instance_id)
2921 .unwrap_or(0) as i32;
2922
2923 // Return empty UDT data with error indication
2924 Ok(PlcValue::Udt(UdtData {
2925 symbol_id,
2926 data: vec![], // Empty data indicates read failure
2927 }))
2928 }
2929
2930 /// Try reading UDT with specific chunk size
2931 async fn read_udt_with_chunk_size(
2932 &mut self,
2933 tag_name: &str,
2934 mut chunk_size: usize,
2935 ) -> crate::error::Result<PlcValue> {
2936 let mut all_data = Vec::new();
2937 let mut offset = 0;
2938 let mut consecutive_failures = 0;
2939 const MAX_FAILURES: usize = 3;
2940
2941 loop {
2942 match self
2943 .read_udt_chunk_advanced(tag_name, offset, chunk_size)
2944 .await
2945 {
2946 Ok(chunk_data) => {
2947 if chunk_data.is_empty() {
2948 break; // No more data
2949 }
2950
2951 all_data.extend_from_slice(&chunk_data);
2952 offset += chunk_data.len();
2953 consecutive_failures = 0;
2954
2955 tracing::trace!(
2956 "[CHUNK] Read {} bytes at offset {}, total: {}",
2957 chunk_data.len(),
2958 offset - chunk_data.len(),
2959 all_data.len()
2960 );
2961
2962 // If we got less data than requested, we might be done
2963 if chunk_data.len() < chunk_size {
2964 break;
2965 }
2966 }
2967 Err(e) => {
2968 consecutive_failures += 1;
2969 tracing::warn!(
2970 "[CHUNK] Chunk read failed (attempt {}): {}",
2971 consecutive_failures,
2972 e
2973 );
2974
2975 if consecutive_failures >= MAX_FAILURES {
2976 break;
2977 }
2978
2979 // Try smaller chunk by reducing size and continuing
2980 if chunk_size > 4 {
2981 chunk_size /= 2;
2982 continue;
2983 }
2984 }
2985 }
2986 }
2987
2988 if all_data.is_empty() {
2989 return Err(crate::error::EtherNetIpError::Protocol(
2990 "No data read from UDT".to_string(),
2991 ));
2992 }
2993
2994 tracing::debug!("[CHUNK] Total data collected: {} bytes", all_data.len());
2995
2996 // Get symbol_id from tag attributes
2997 let symbol_id = self
2998 .get_tag_attributes(tag_name)
2999 .await
3000 .ok()
3001 .and_then(|attr| attr.template_instance_id)
3002 .unwrap_or(0) as i32;
3003
3004 // Return raw UDT data (generic approach - no parsing)
3005 Ok(PlcValue::Udt(UdtData {
3006 symbol_id,
3007 data: all_data,
3008 }))
3009 }
3010
3011 /// Advanced chunk reading with better error handling
3012 async fn read_udt_chunk_advanced(
3013 &mut self,
3014 tag_name: &str,
3015 offset: usize,
3016 size: usize,
3017 ) -> crate::error::Result<Vec<u8>> {
3018 // Build a more sophisticated read request
3019 let mut request = Vec::new();
3020
3021 // Service: Read Tag (0x4C)
3022 request.push(0x4C);
3023
3024 // Use TagPath::parse() to correctly handle complex paths like Cell_NestData[90].PartData
3025 let tag_path = self.build_tag_path(tag_name);
3026
3027 // Path size (in words)
3028 let path_size = (tag_path.len() / 2) as u8;
3029 request.push(path_size);
3030
3031 // Path: use properly parsed tag path
3032 request.extend_from_slice(&tag_path);
3033
3034 // For UDTs, we need to use a different approach than array indexing
3035 // Try to read as raw data with offset
3036 if offset > 0 {
3037 // Use element path for offset
3038 request.push(0x28); // Element symbol
3039 request.push(0x02); // 2 bytes for offset
3040 request.extend_from_slice(&(offset as u16).to_le_bytes());
3041 }
3042
3043 // Element count
3044 request.push(0x28); // Element count symbol
3045 request.push(0x02); // 2 bytes for count
3046 request.extend_from_slice(&(size as u16).to_le_bytes());
3047
3048 // Data type - try as raw bytes first
3049 request.push(0x00);
3050 request.push(0x01);
3051
3052 // Send the request
3053 let response = self.send_cip_request(&request).await?;
3054 let cip_data = self.extract_cip_from_response(&response)?;
3055
3056 // Parse the response
3057 if cip_data.len() < 2 {
3058 return Ok(Vec::new()); // No data
3059 }
3060
3061 let _data_type = u16::from_le_bytes([cip_data[0], cip_data[1]]);
3062 let data = &cip_data[2..];
3063
3064 Ok(data.to_vec())
3065 }
3066
3067 /// Try to read UDT as raw data with symbol_id
3068 ///
3069 /// This is a generic approach that works for any UDT without requiring
3070 /// knowledge of member names. It reads the raw bytes and gets the
3071 /// symbol_id (template instance ID) from tag attributes.
3072 async fn read_udt_member_discovery(
3073 &mut self,
3074 tag_name: &str,
3075 ) -> crate::error::Result<PlcValue> {
3076 tracing::debug!("[DISCOVERY] Reading UDT as raw data for: {}", tag_name);
3077
3078 // Get tag attributes to retrieve symbol_id (template_instance_id)
3079 let attributes = self.get_tag_attributes(tag_name).await?;
3080
3081 let symbol_id = attributes.template_instance_id.ok_or_else(|| {
3082 crate::error::EtherNetIpError::Protocol(
3083 "UDT template instance ID not found in tag attributes".to_string(),
3084 )
3085 })?;
3086
3087 // Read raw UDT data
3088 let raw_data = self.read_tag_raw(tag_name).await?;
3089
3090 tracing::debug!(
3091 "[DISCOVERY] Read {} bytes of UDT data with symbol_id: {}",
3092 raw_data.len(),
3093 symbol_id
3094 );
3095
3096 Ok(PlcValue::Udt(UdtData {
3097 symbol_id: symbol_id as i32,
3098 data: raw_data,
3099 }))
3100 }
3101
3102 /// Progressive reading - try to read UDT in progressively smaller chunks
3103 async fn read_udt_progressive(&mut self, tag_name: &str) -> crate::error::Result<PlcValue> {
3104 tracing::debug!("[PROGRESSIVE] Starting progressive reading");
3105
3106 // Start with a small chunk and gradually increase
3107 let mut chunk_size = 4;
3108 let mut all_data = Vec::new();
3109 let mut offset = 0;
3110
3111 while chunk_size <= 512 {
3112 match self
3113 .read_udt_chunk_advanced(tag_name, offset, chunk_size)
3114 .await
3115 {
3116 Ok(chunk_data) => {
3117 if chunk_data.is_empty() {
3118 break;
3119 }
3120
3121 all_data.extend_from_slice(&chunk_data);
3122 offset += chunk_data.len();
3123
3124 tracing::trace!(
3125 "[PROGRESSIVE] Read {} bytes with chunk size {}",
3126 chunk_data.len(),
3127 chunk_size
3128 );
3129
3130 // If we got the full chunk, try a larger one next time
3131 if chunk_data.len() == chunk_size {
3132 chunk_size = (chunk_size * 2).min(512);
3133 }
3134 }
3135 Err(_) => {
3136 // Reduce chunk size and try again
3137 chunk_size /= 2;
3138 if chunk_size < 4 {
3139 break;
3140 }
3141 }
3142 }
3143 }
3144
3145 if all_data.is_empty() {
3146 return Err(crate::error::EtherNetIpError::Protocol(
3147 "Progressive reading failed".to_string(),
3148 ));
3149 }
3150
3151 tracing::debug!("[PROGRESSIVE] Collected {} bytes total", all_data.len());
3152
3153 // Get symbol_id from tag attributes
3154 let symbol_id = self
3155 .get_tag_attributes(tag_name)
3156 .await
3157 .ok()
3158 .and_then(|attr| attr.template_instance_id)
3159 .unwrap_or(0) as i32;
3160
3161 // Return raw UDT data (generic approach - no parsing)
3162 Ok(PlcValue::Udt(UdtData {
3163 symbol_id,
3164 data: all_data,
3165 }))
3166 }
3167
3168 /// Reads a UDT in chunks to handle large structures
3169 #[allow(dead_code)]
3170 async fn read_udt_in_chunks(&mut self, tag_name: &str) -> crate::error::Result<PlcValue> {
3171 const MAX_CHUNK_SIZE: usize = 1000; // Conservative chunk size
3172 let mut all_data = Vec::new();
3173 let mut offset = 0;
3174 let mut chunk_size = MAX_CHUNK_SIZE;
3175
3176 loop {
3177 // Try to read a chunk
3178 match self.read_udt_chunk(tag_name, offset, chunk_size).await {
3179 Ok(chunk_data) => {
3180 all_data.extend_from_slice(&chunk_data);
3181 offset += chunk_data.len();
3182
3183 // If we got less data than requested, we're done
3184 if chunk_data.len() < chunk_size {
3185 break;
3186 }
3187 }
3188 Err(crate::error::EtherNetIpError::Protocol(msg))
3189 if msg.contains("Partial transfer") =>
3190 {
3191 // Reduce chunk size and try again
3192 chunk_size /= 2;
3193 if chunk_size < 100 {
3194 return Err(crate::error::EtherNetIpError::Protocol(
3195 "UDT too large even for smallest chunk size".to_string(),
3196 ));
3197 }
3198 continue;
3199 }
3200 Err(e) => return Err(e),
3201 }
3202 }
3203
3204 // Get symbol_id from tag attributes
3205 let symbol_id = self
3206 .get_tag_attributes(tag_name)
3207 .await
3208 .ok()
3209 .and_then(|attr| attr.template_instance_id)
3210 .unwrap_or(0) as i32;
3211
3212 // Return raw UDT data (generic approach - no parsing)
3213 Ok(PlcValue::Udt(UdtData {
3214 symbol_id,
3215 data: all_data,
3216 }))
3217 }
3218
3219 /// Reads a specific chunk of a UDT
3220 #[allow(dead_code)]
3221 async fn read_udt_chunk(
3222 &mut self,
3223 tag_name: &str,
3224 offset: usize,
3225 size: usize,
3226 ) -> crate::error::Result<Vec<u8>> {
3227 // Build a read request for a specific range
3228 let mut request = Vec::new();
3229
3230 // Service: Read Tag (0x4C)
3231 request.push(0x4C);
3232
3233 // Path size (in words) - tag name + array index
3234 let path_size = 2 + (tag_name.len() + 1) / 2; // Round up for word alignment
3235 request.push(path_size as u8);
3236
3237 // Path: tag name
3238 request.extend_from_slice(tag_name.as_bytes());
3239 if tag_name.len() % 2 != 0 {
3240 request.push(0); // Pad to word boundary
3241 }
3242
3243 // Array index for chunk reading
3244 request.push(0x28); // Array index symbol
3245 request.push(0x02); // 2 bytes for index
3246 request.extend_from_slice(&(offset as u16).to_le_bytes());
3247
3248 // Element count
3249 request.push(0x28); // Element count symbol
3250 request.push(0x02); // 2 bytes for count
3251 request.extend_from_slice(&(size as u16).to_le_bytes());
3252
3253 // Data type (assume DINT for raw data)
3254 request.push(0x00);
3255 request.push(0x01);
3256
3257 // Send the request
3258 let response = self.send_cip_request(&request).await?;
3259 let cip_data = self.extract_cip_from_response(&response)?;
3260
3261 // Parse the response to get raw data
3262 if cip_data.len() < 2 {
3263 return Err(crate::error::EtherNetIpError::Protocol(
3264 "Response too short".to_string(),
3265 ));
3266 }
3267
3268 let _data_type = u16::from_le_bytes([cip_data[0], cip_data[1]]);
3269 let data = &cip_data[2..];
3270
3271 Ok(data.to_vec())
3272 }
3273
3274 /// Reads a specific UDT member by offset
3275 ///
3276 /// This method reads a specific member of a UDT by calculating its offset
3277 /// and reading only that portion of the UDT.
3278 ///
3279 /// # Arguments
3280 ///
3281 /// * `udt_name` - The name of the UDT tag
3282 /// * `member_offset` - The byte offset of the member in the UDT
3283 /// * `member_size` - The size of the member in bytes
3284 /// * `data_type` - The data type of the member (0x00C1 for BOOL, 0x00CA for REAL, etc.)
3285 ///
3286 /// # Example
3287 ///
3288 /// ```no_run
3289 /// # async fn example() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
3290 /// # let mut client = rust_ethernet_ip::EipClient::connect("192.168.1.100:44818").await?;
3291 /// let member_value = client.read_udt_member_by_offset("MyUDT", 0, 1, 0x00C1).await?;
3292 /// println!("Member value: {:?}", member_value);
3293 /// # Ok(())
3294 /// # }
3295 /// ```
3296 pub async fn read_udt_member_by_offset(
3297 &mut self,
3298 udt_name: &str,
3299 member_offset: usize,
3300 member_size: usize,
3301 data_type: u16,
3302 ) -> crate::error::Result<PlcValue> {
3303 self.validate_session().await?;
3304
3305 // Read the UDT data
3306 let udt_data = self.read_tag_raw(udt_name).await?;
3307
3308 // Extract the member data
3309 if member_offset + member_size > udt_data.len() {
3310 return Err(crate::error::EtherNetIpError::Protocol(format!(
3311 "Member data incomplete: offset {} + size {} > UDT size {}",
3312 member_offset,
3313 member_size,
3314 udt_data.len()
3315 )));
3316 }
3317
3318 let member_data = &udt_data[member_offset..member_offset + member_size];
3319
3320 // Parse the member value using the data type
3321 let member = crate::udt::UdtMember {
3322 name: "temp".to_string(),
3323 data_type,
3324 offset: member_offset as u32,
3325 size: member_size as u32,
3326 };
3327
3328 let udt = crate::udt::UserDefinedType::new(udt_name.to_string());
3329 udt.parse_member_value(&member, member_data)
3330 }
3331
3332 /// Writes a specific UDT member by offset
3333 ///
3334 /// This method writes a specific member of a UDT by calculating its offset
3335 /// and writing only that portion of the UDT.
3336 ///
3337 /// # Arguments
3338 ///
3339 /// * `udt_name` - The name of the UDT tag
3340 /// * `member_offset` - The byte offset of the member in the UDT
3341 /// * `member_size` - The size of the member in bytes
3342 /// * `data_type` - The data type of the member (0x00C1 for BOOL, 0x00CA for REAL, etc.)
3343 /// * `value` - The value to write
3344 ///
3345 /// # Example
3346 ///
3347 /// ```no_run
3348 /// # async fn example() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
3349 /// # use rust_ethernet_ip::PlcValue;
3350 /// # let mut client = rust_ethernet_ip::EipClient::connect("192.168.1.100:44818").await?;
3351 /// client.write_udt_member_by_offset("MyUDT", 0, 1, 0x00C1, PlcValue::Bool(true)).await?;
3352 /// # Ok(())
3353 /// # }
3354 /// ```
3355 pub async fn write_udt_member_by_offset(
3356 &mut self,
3357 udt_name: &str,
3358 member_offset: usize,
3359 member_size: usize,
3360 data_type: u16,
3361 value: PlcValue,
3362 ) -> crate::error::Result<()> {
3363 self.validate_session().await?;
3364
3365 // Read the current UDT data
3366 let mut udt_data = self.read_tag_raw(udt_name).await?;
3367
3368 // Check bounds
3369 if member_offset + member_size > udt_data.len() {
3370 return Err(crate::error::EtherNetIpError::Protocol(format!(
3371 "Member data incomplete: offset {} + size {} > UDT size {}",
3372 member_offset,
3373 member_size,
3374 udt_data.len()
3375 )));
3376 }
3377
3378 // Serialize the value
3379 let member = crate::udt::UdtMember {
3380 name: "temp".to_string(),
3381 data_type,
3382 offset: member_offset as u32,
3383 size: member_size as u32,
3384 };
3385
3386 let udt = crate::udt::UserDefinedType::new(udt_name.to_string());
3387 let member_data = udt.serialize_member_value(&member, &value)?;
3388
3389 // Update the UDT data
3390 let end_offset = member_offset + member_data.len();
3391 if end_offset <= udt_data.len() {
3392 udt_data[member_offset..end_offset].copy_from_slice(&member_data);
3393 } else {
3394 return Err(crate::error::EtherNetIpError::Protocol(format!(
3395 "Member data exceeds UDT size: {} > {}",
3396 end_offset,
3397 udt_data.len()
3398 )));
3399 }
3400
3401 // Write the updated UDT data back
3402 self.write_tag_raw(udt_name, &udt_data).await
3403 }
3404
3405 /// Gets UDT definition from the PLC
3406 /// This method queries the PLC for the UDT structure and caches it for future use
3407 pub async fn get_udt_definition(
3408 &mut self,
3409 udt_name: &str,
3410 ) -> crate::error::Result<UdtDefinition> {
3411 // Check cache first
3412 if let Some(cached) = self.udt_manager.lock().await.get_definition(udt_name) {
3413 return Ok(cached.clone());
3414 }
3415
3416 // Get tag attributes to find template ID
3417 let attributes = self.get_tag_attributes(udt_name).await?;
3418
3419 // If this is not a UDT, return error
3420 if attributes.data_type != 0x00A0 {
3421 return Err(crate::error::EtherNetIpError::Protocol(format!(
3422 "Tag '{}' is not a UDT (type: {})",
3423 udt_name, attributes.data_type_name
3424 )));
3425 }
3426
3427 // Get template instance ID
3428 let template_id = attributes.template_instance_id.ok_or_else(|| {
3429 crate::error::EtherNetIpError::Protocol(
3430 "UDT template instance ID not found".to_string(),
3431 )
3432 })?;
3433
3434 // Read UDT template
3435 let template_data = self.read_udt_template(template_id).await?;
3436
3437 // Parse template
3438 let template = self
3439 .udt_manager
3440 .lock()
3441 .await
3442 .parse_udt_template(template_id, &template_data)?;
3443
3444 // Convert template to definition
3445 let definition = UdtDefinition {
3446 name: udt_name.to_string(),
3447 members: template.members,
3448 };
3449
3450 // Cache the definition
3451 self.udt_manager
3452 .lock()
3453 .await
3454 .add_definition(definition.clone());
3455
3456 Ok(definition)
3457 }
3458
3459 /// Gets tag attributes (type, size, dimensions, scope) from the PLC.
3460 ///
3461 /// Use this to introspect a tag before reading or writing: discover data type,
3462 /// size in bytes, array dimensions, and scope (controller vs program). Results
3463 /// are cached per tag for the lifetime of the client.
3464 ///
3465 /// # Example
3466 ///
3467 /// ```ignore
3468 /// let attrs = client.get_tag_attributes("MyTag").await?;
3469 /// println!("Type: {}, size: {} bytes", attrs.data_type_name, attrs.size);
3470 /// if !attrs.dimensions.is_empty() {
3471 /// println!("Array dimensions: {:?}", attrs.dimensions);
3472 /// }
3473 /// ```
3474 ///
3475 pub async fn get_tag_attributes(
3476 &mut self,
3477 tag_name: &str,
3478 ) -> crate::error::Result<TagAttributes> {
3479 // Check cache first
3480 if let Some(cached) = self.udt_manager.lock().await.get_tag_attributes(tag_name) {
3481 return Ok(cached.clone());
3482 }
3483
3484 // Build CIP request for Get Attribute List (Service 0x03)
3485 let request = self.build_get_attributes_request(tag_name)?;
3486
3487 // Send request and get response
3488 let response = self.send_cip_request(&request).await?;
3489
3490 // Parse response
3491 let attributes = self.parse_attributes_response(tag_name, &response)?;
3492
3493 // Cache the attributes
3494 self.udt_manager
3495 .lock()
3496 .await
3497 .add_tag_attributes(attributes.clone());
3498
3499 Ok(attributes)
3500 }
3501
3502 /// Reads UDT template data from the PLC
3503 async fn read_udt_template(&mut self, template_id: u32) -> crate::error::Result<Vec<u8>> {
3504 // Build CIP request for Read Tag Fragmented (Service 0x4C)
3505 let request = self.build_read_template_request(template_id)?;
3506
3507 // Send request and get response
3508 let response = self.send_cip_request(&request).await?;
3509
3510 // Parse response and extract template data
3511 self.parse_template_response(&response)
3512 }
3513
3514 /// Builds CIP request for Get Attribute List (Service 0x03)
3515 fn build_get_attributes_request(&self, tag_name: &str) -> crate::error::Result<Vec<u8>> {
3516 let mut request = Vec::new();
3517
3518 // Service: Get Attribute List (0x03)
3519 request.push(0x03);
3520
3521 // Path: Tag name (ANSI extended symbolic segment)
3522 let tag_bytes = tag_name.as_bytes();
3523 request.push(0x91); // ANSI extended symbolic segment
3524 request.push(tag_bytes.len() as u8);
3525 request.extend_from_slice(tag_bytes);
3526
3527 // Attribute count
3528 request.extend_from_slice(&[0x02, 0x00]); // 2 attributes
3529
3530 // Attribute 1: Data Type (0x01)
3531 request.extend_from_slice(&[0x01, 0x00]);
3532
3533 // Attribute 2: Template Instance ID (0x02)
3534 request.extend_from_slice(&[0x02, 0x00]);
3535
3536 Ok(request)
3537 }
3538
3539 /// Builds CIP request for Read Tag Fragmented (Service 0x4C)
3540 fn build_read_template_request(&self, template_id: u32) -> crate::error::Result<Vec<u8>> {
3541 let mut request = Vec::new();
3542
3543 // Service: Read Tag Fragmented (0x4C)
3544 request.push(0x4C);
3545
3546 // Path: Template instance
3547 request.push(0x20); // Class ID
3548 request.extend_from_slice(&[0x02, 0x00]); // Class 0x02 (Data Type)
3549 request.push(0x24); // Instance ID
3550 request.extend_from_slice(&template_id.to_le_bytes());
3551
3552 // Offset and size (read entire template)
3553 request.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); // Offset 0
3554 request.extend_from_slice(&[0xFF, 0xFF, 0x00, 0x00]); // Size (max)
3555
3556 Ok(request)
3557 }
3558
3559 /// Parses attributes response from CIP
3560 fn parse_attributes_response(
3561 &self,
3562 tag_name: &str,
3563 response: &[u8],
3564 ) -> crate::error::Result<TagAttributes> {
3565 if response.len() < 8 {
3566 return Err(crate::error::EtherNetIpError::Protocol(
3567 "Attributes response too short".to_string(),
3568 ));
3569 }
3570
3571 let mut offset = 0;
3572
3573 // Parse data type
3574 let data_type = u16::from_le_bytes([response[offset], response[offset + 1]]);
3575 offset += 2;
3576
3577 // Parse size
3578 let size = u32::from_le_bytes([
3579 response[offset],
3580 response[offset + 1],
3581 response[offset + 2],
3582 response[offset + 3],
3583 ]);
3584 offset += 4;
3585
3586 // Parse template instance ID (if present)
3587 let template_instance_id = if response.len() > offset + 4 {
3588 Some(u32::from_le_bytes([
3589 response[offset],
3590 response[offset + 1],
3591 response[offset + 2],
3592 response[offset + 3],
3593 ]))
3594 } else {
3595 None
3596 };
3597
3598 // Create attributes
3599 let attributes = TagAttributes {
3600 name: tag_name.to_string(),
3601 data_type,
3602 data_type_name: self.get_data_type_name(data_type),
3603 dimensions: Vec::new(), // Would need additional parsing
3604 permissions: udt::TagPermissions::ReadWrite, // Default assumption
3605 scope: if tag_name.contains(':') {
3606 let parts: Vec<&str> = tag_name.split(':').collect();
3607 if parts.len() >= 2 {
3608 udt::TagScope::Program(parts[0].to_string())
3609 } else {
3610 udt::TagScope::Controller
3611 }
3612 } else {
3613 udt::TagScope::Controller
3614 },
3615 template_instance_id,
3616 size,
3617 };
3618
3619 Ok(attributes)
3620 }
3621
3622 /// Parses template response from CIP
3623 fn parse_template_response(&self, response: &[u8]) -> crate::error::Result<Vec<u8>> {
3624 if response.len() < 4 {
3625 return Err(crate::error::EtherNetIpError::Protocol(
3626 "Template response too short".to_string(),
3627 ));
3628 }
3629
3630 // Skip CIP header and return data portion
3631 let data_start = 4; // Skip status and other header bytes
3632 Ok(response[data_start..].to_vec())
3633 }
3634
3635 /// Gets human-readable data type name
3636 fn get_data_type_name(&self, data_type: u16) -> String {
3637 match data_type {
3638 0x00C1 => "BOOL".to_string(),
3639 0x00C2 => "SINT".to_string(),
3640 0x00C3 => "INT".to_string(),
3641 0x00C4 => "DINT".to_string(),
3642 0x00C5 => "LINT".to_string(),
3643 0x00C6 => "USINT".to_string(),
3644 0x00C7 => "UINT".to_string(),
3645 0x00C8 => "UDINT".to_string(),
3646 0x00C9 => "ULINT".to_string(),
3647 0x00CA => "REAL".to_string(),
3648 0x00CB => "LREAL".to_string(),
3649 0x00CE => "STRING".to_string(),
3650 0x00A0 => "UDT".to_string(),
3651 _ => format!("UNKNOWN(0x{:04X})", data_type),
3652 }
3653 }
3654
3655 /// Builds CIP request for tag list discovery
3656 fn build_tag_list_request(&self) -> crate::error::Result<Vec<u8>> {
3657 let mut request = Vec::new();
3658
3659 // Service: Get Instance Attribute List (0x55)
3660 request.push(0x55);
3661
3662 // Path: Symbol Object (Class 0x6B)
3663 request.push(0x20); // Class ID
3664 request.extend_from_slice(&[0x6B, 0x00]); // Class 0x6B (Symbol Object)
3665 request.push(0x25); // Instance ID (0x25 = all instances)
3666 request.extend_from_slice(&[0x00, 0x00]);
3667
3668 // Attribute count
3669 request.extend_from_slice(&[0x02, 0x00]); // 2 attributes
3670
3671 // Attribute 1: Symbol Name (0x01)
3672 request.extend_from_slice(&[0x01, 0x00]);
3673
3674 // Attribute 2: Data Type (0x02)
3675 request.extend_from_slice(&[0x02, 0x00]);
3676
3677 Ok(request)
3678 }
3679
3680 /// Builds CIP request for program-scoped tag list discovery
3681 fn build_program_tag_list_request(&self, _program_name: &str) -> crate::error::Result<Vec<u8>> {
3682 let mut request = Vec::new();
3683
3684 // Service: Get Instance Attribute List (0x55)
3685 request.push(0x55);
3686
3687 // Path: Program Object (Class 0x6C)
3688 request.push(0x20); // Class ID
3689 request.extend_from_slice(&[0x6C, 0x00]); // Class 0x6C (Program Object)
3690 request.push(0x24); // Instance ID
3691 request.extend_from_slice(&[0x00, 0x00]); // Would need to resolve program name to ID
3692
3693 // Attribute count
3694 request.extend_from_slice(&[0x02, 0x00]); // 2 attributes
3695
3696 // Attribute 1: Symbol Name (0x01)
3697 request.extend_from_slice(&[0x01, 0x00]);
3698
3699 // Attribute 2: Data Type (0x02)
3700 request.extend_from_slice(&[0x02, 0x00]);
3701
3702 Ok(request)
3703 }
3704
3705 /// Parses tag list response from CIP
3706 fn parse_tag_list_response(&self, response: &[u8]) -> crate::error::Result<Vec<TagAttributes>> {
3707 if response.len() < 4 {
3708 return Err(crate::error::EtherNetIpError::Protocol(
3709 "Tag list response too short".to_string(),
3710 ));
3711 }
3712
3713 let mut offset = 0;
3714 let mut tags = Vec::new();
3715
3716 // Skip CIP header
3717 offset += 4;
3718
3719 // Parse each tag entry
3720 while offset < response.len() {
3721 if offset + 8 > response.len() {
3722 break; // Not enough data for another tag
3723 }
3724
3725 // Parse tag name length
3726 let name_length = u16::from_le_bytes([response[offset], response[offset + 1]]) as usize;
3727 offset += 2;
3728
3729 if offset
3730 .checked_add(name_length)
3731 .map_or(true, |end| end > response.len())
3732 {
3733 break; // Not enough data for tag name
3734 }
3735
3736 // Parse tag name
3737 let name_bytes = &response[offset..offset + name_length];
3738 let tag_name = String::from_utf8_lossy(name_bytes).to_string();
3739 offset += name_length;
3740
3741 // Align to 4-byte boundary
3742 offset = (offset + 3) & !3;
3743
3744 if offset + 2 > response.len() {
3745 break; // Not enough data for data type
3746 }
3747
3748 // Parse data type
3749 let data_type = u16::from_le_bytes([response[offset], response[offset + 1]]);
3750 offset += 2;
3751
3752 // Create tag attributes
3753 let attributes = TagAttributes {
3754 name: tag_name,
3755 data_type,
3756 data_type_name: self.get_data_type_name(data_type),
3757 dimensions: Vec::new(), // Would need additional parsing
3758 permissions: udt::TagPermissions::ReadWrite, // Default assumption
3759 scope: udt::TagScope::Controller, // Default assumption
3760 template_instance_id: if data_type == 0x00A0 { Some(0) } else { None },
3761 size: 0, // Would need additional parsing
3762 };
3763
3764 tags.push(attributes);
3765 }
3766
3767 Ok(tags)
3768 }
3769
3770 /// Negotiates packet size with the PLC
3771 /// This method queries the PLC for its maximum supported packet size
3772 /// and updates the client's configuration accordingly
3773 async fn negotiate_packet_size(&mut self) -> crate::error::Result<()> {
3774 // Build CIP request for Get Attribute List (Service 0x03)
3775 // Query the Message Router object (Class 0x02, Instance 1) for max packet size
3776 let mut request = vec![
3777 0x03, // Service: Get Attribute List
3778 0x02, // Path size: 2 words (4 bytes)
3779 0x20, 0x02, // 8-bit class segment: Class 0x02 (Message Router)
3780 0x24, 0x01, // 8-bit instance segment: Instance 1
3781 ];
3782 // Attribute count
3783 request.extend_from_slice(&[0x01, 0x00]); // 1 attribute
3784 // Attribute: Max Packet Size (attribute 4 on the Message Router)
3785 request.extend_from_slice(&[0x04, 0x00]);
3786
3787 // Send request and extract CIP from CPF response
3788 let response = self.send_cip_request(&request).await?;
3789 let cip_data = self.extract_cip_from_response(&response)?;
3790
3791 // CIP response format: [Service Reply][Reserved][Status][AddtlStatusSize][...data...]
3792 // For Get Attribute List reply: after the 4-byte CIP header, we get:
3793 // [AttrCount(2)] [AttrID(2)] [Status(2)] [Value(2)]
3794 // The attribute value for max packet size is a UINT (2 bytes)
3795 if cip_data.len() >= 12 && cip_data[2] == 0x00 {
3796 // Skip CIP header (4 bytes) + attr count (2) + attr id (2) + attr status (2) = 10
3797 let max_packet_size = u16::from_le_bytes([cip_data[10], cip_data[11]]) as u32;
3798
3799 // Update client's max packet size (with reasonable limits)
3800 self.max_packet_size = max_packet_size.clamp(504, 4000);
3801 tracing::debug!("Negotiated packet size: {} bytes", self.max_packet_size);
3802 } else {
3803 // If negotiation fails, use default size
3804 self.max_packet_size = 4000;
3805 tracing::debug!("Using default packet size: {} bytes", self.max_packet_size);
3806 }
3807
3808 Ok(())
3809 }
3810
3811 /// Writes a value to a PLC tag
3812 ///
3813 /// This method automatically determines the best communication method based on the data type:
3814 /// - STRING values use unconnected explicit messaging with proper AB STRING format
3815 /// - Other data types use standard unconnected messaging
3816 ///
3817 /// **v0.6.0**: For UDT tags, pass `PlcValue::Udt(UdtData)`. The `symbol_id` must be set
3818 /// (typically obtained by reading the UDT first). If `symbol_id` is 0, the method will
3819 /// attempt to read tag attributes to get the symbol_id automatically.
3820 ///
3821 /// # Arguments
3822 ///
3823 /// * `tag_name` - The name of the tag to write to
3824 /// * `value` - The value to write. For UDTs, use `PlcValue::Udt(UdtData)`.
3825 ///
3826 /// # Example
3827 ///
3828 /// ```no_run
3829 /// # async fn example() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
3830 /// # let mut client = rust_ethernet_ip::EipClient::connect("192.168.1.100:44818").await?;
3831 /// use rust_ethernet_ip::{PlcValue, UdtData};
3832 ///
3833 /// // Write simple types
3834 /// client.write_tag("Counter", PlcValue::Dint(42)).await?;
3835 /// client.write_tag("Message", PlcValue::String("Hello PLC".to_string())).await?;
3836 ///
3837 /// // Write UDT (v0.6.0: read first to get symbol_id, then modify and write)
3838 /// let udt_value = client.read_tag("MyUDT").await?;
3839 /// if let PlcValue::Udt(mut udt_data) = udt_value {
3840 /// let udt_def = client.get_udt_definition("MyUDT").await?;
3841 /// // Convert UdtDefinition to UserDefinedType
3842 /// let mut user_def = rust_ethernet_ip::udt::UserDefinedType::new(udt_def.name.clone());
3843 /// for member in &udt_def.members {
3844 /// user_def.add_member(member.clone());
3845 /// }
3846 /// let mut members = udt_data.parse(&user_def)?;
3847 /// members.insert("Member1".to_string(), PlcValue::Dint(100));
3848 /// let modified_udt = UdtData::from_hash_map(&members, &user_def, udt_data.symbol_id)?;
3849 /// client.write_tag("MyUDT", PlcValue::Udt(modified_udt)).await?;
3850 /// }
3851 /// # Ok(())
3852 /// # }
3853 /// ```
3854 pub async fn write_tag(&mut self, tag_name: &str, value: PlcValue) -> crate::error::Result<()> {
3855 tracing::debug!(
3856 "Writing '{}' to tag '{}'",
3857 match &value {
3858 PlcValue::String(s) => format!("\"{s}\""),
3859 _ => format!("{value:?}"),
3860 },
3861 tag_name
3862 );
3863
3864 // For UDT writes, ensure we have a valid symbol_id
3865 // As noted by the contributor: "to write a UDT, you typically need to read it first to get the symbol_id"
3866 let value = if let PlcValue::Udt(udt_data) = &value {
3867 if udt_data.symbol_id == 0 {
3868 tracing::debug!("[UDT WRITE] symbol_id is 0, reading tag to get symbol_id");
3869 // Read tag attributes to get symbol_id
3870 let attributes = self.get_tag_attributes(tag_name).await?;
3871 let symbol_id = attributes.template_instance_id.ok_or_else(|| {
3872 crate::error::EtherNetIpError::Protocol(
3873 "UDT template instance ID not found. Cannot write UDT without symbol_id."
3874 .to_string(),
3875 )
3876 })? as i32;
3877
3878 // Create new UdtData with the correct symbol_id
3879 PlcValue::Udt(UdtData {
3880 symbol_id,
3881 data: udt_data.data.clone(),
3882 })
3883 } else {
3884 value
3885 }
3886 } else {
3887 value
3888 };
3889
3890 // Check if this is array element access (e.g., "ArrayName[0]")
3891 if let Some((base_name, index)) = self.parse_array_element_access(tag_name) {
3892 tracing::debug!(
3893 "Detected array element write: {}[{}], using workaround",
3894 base_name,
3895 index
3896 );
3897 return self
3898 .write_array_element_workaround(&base_name, index, value)
3899 .await;
3900 }
3901
3902 // Use specialized AB STRING format for STRING writes (required for proper Allen-Bradley STRING handling)
3903 // All data types including strings now use the standard write path
3904 // The PlcValue::to_bytes() method handles the correct format for each type
3905
3906 // Use standard unconnected messaging for other data types
3907 let cip_request = self.build_write_request(tag_name, &value)?;
3908
3909 let response = self.send_cip_request(&cip_request).await?;
3910
3911 // Check write response for errors - need to extract CIP response first
3912 let cip_response = self.extract_cip_from_response(&response)?;
3913
3914 if cip_response.len() < 3 {
3915 return Err(EtherNetIpError::Protocol(
3916 "Write response too short".to_string(),
3917 ));
3918 }
3919
3920 let service_reply = cip_response[0]; // Should be 0xCD (0x4D + 0x80) for Write Tag reply
3921 let general_status = cip_response[2]; // CIP status code
3922
3923 tracing::trace!(
3924 "Write response - Service: 0x{:02X}, Status: 0x{:02X}",
3925 service_reply,
3926 general_status
3927 );
3928
3929 // Check for errors (including extended errors)
3930 if let Err(e) = self.check_cip_error(&cip_response) {
3931 tracing::error!("[WRITE] CIP Error: {}", e);
3932 return Err(e);
3933 }
3934
3935 tracing::info!("Write operation completed successfully");
3936 Ok(())
3937 }
3938
3939 /// Builds a write request specifically for Allen-Bradley string format
3940 fn _build_ab_string_write_request(
3941 &self,
3942 tag_name: &str,
3943 value: &PlcValue,
3944 ) -> crate::error::Result<Vec<u8>> {
3945 if let PlcValue::String(string_value) = value {
3946 tracing::debug!(
3947 "Building correct Allen-Bradley string write request for tag: '{}'",
3948 tag_name
3949 );
3950
3951 let mut cip_request = Vec::new();
3952
3953 // Service: Write Tag Service (0x4D)
3954 cip_request.push(0x4D);
3955
3956 // Request Path Size (in words)
3957 let tag_bytes = tag_name.as_bytes();
3958 let path_len = if tag_bytes.len() % 2 == 0 {
3959 tag_bytes.len() + 2
3960 } else {
3961 tag_bytes.len() + 3
3962 } / 2;
3963 cip_request.push(path_len as u8);
3964
3965 // Request Path
3966 cip_request.push(0x91); // ANSI Extended Symbol
3967 cip_request.push(tag_bytes.len() as u8);
3968 cip_request.extend_from_slice(tag_bytes);
3969
3970 // Pad to word boundary if needed
3971 if tag_bytes.len() % 2 != 0 {
3972 cip_request.push(0x00);
3973 }
3974
3975 // Data Type: Allen-Bradley STRING (0x02A0)
3976 cip_request.extend_from_slice(&[0xA0, 0x02]);
3977
3978 // Element Count (always 1 for single string)
3979 cip_request.extend_from_slice(&[0x01, 0x00]);
3980
3981 // Build the correct AB STRING structure
3982 let string_bytes = string_value.as_bytes();
3983 let max_len: u16 = 82; // Standard AB STRING max length
3984 let current_len = string_bytes.len().min(max_len as usize) as u16;
3985
3986 // AB STRING structure:
3987 // - Len (2 bytes) - number of characters used
3988 cip_request.extend_from_slice(¤t_len.to_le_bytes());
3989
3990 // - MaxLen (2 bytes) - maximum characters allowed (typically 82)
3991 cip_request.extend_from_slice(&max_len.to_le_bytes());
3992
3993 // - Data[MaxLen] (82 bytes) - the character array, zero-padded
3994 let mut data_array = vec![0u8; max_len as usize];
3995 data_array[..current_len as usize]
3996 .copy_from_slice(&string_bytes[..current_len as usize]);
3997 cip_request.extend_from_slice(&data_array);
3998
3999 tracing::trace!(
4000 "Built correct AB string write request ({} bytes): len={}, maxlen={}, data_len={}",
4001 cip_request.len(),
4002 current_len,
4003 max_len,
4004 string_bytes.len()
4005 );
4006 tracing::trace!(
4007 "First 32 bytes: {:02X?}",
4008 &cip_request[..std::cmp::min(32, cip_request.len())]
4009 );
4010
4011 Ok(cip_request)
4012 } else {
4013 Err(EtherNetIpError::Protocol(
4014 "Expected string value for Allen-Bradley string write".to_string(),
4015 ))
4016 }
4017 }
4018
4019 /// Builds a CIP Write Tag Service request
4020 ///
4021 /// This creates the CIP packet for writing a value to a tag.
4022 /// The request includes the service code, tag path, data type, and value.
4023 ///
4024 /// For UDT writes, the data type must be Structure Tag Type (0x02A0 + Structure Handle).
4025 /// The Structure Handle is the template_instance_id (symbol_id) from Template Attribute 1.
4026 ///
4027 /// Reference: 1756-PM020, Page 1080 (UDT Data Layout Considerations)
4028 fn build_write_request(
4029 &self,
4030 tag_name: &str,
4031 value: &PlcValue,
4032 ) -> crate::error::Result<Vec<u8>> {
4033 tracing::debug!("Building write request for tag: '{}'", tag_name);
4034
4035 // Use Connected Explicit Messaging for consistency
4036 let mut cip_request = Vec::new();
4037
4038 // Service: Write Tag Service (0x4D)
4039 cip_request.push(0x4D);
4040
4041 // Use the same path building logic as read operations
4042 let path = self.build_tag_path(tag_name);
4043
4044 // Request Path Size (in words)
4045 cip_request.push((path.len() / 2) as u8);
4046
4047 // Request Path: Use the same path building as read operations
4048 cip_request.extend_from_slice(&path);
4049
4050 // Add data type and element count
4051 // For UDTs, use Structure Tag Type (0x02A0 + Structure Handle) per 1756-PM020, Page 1080
4052 let data_type = if let PlcValue::Udt(udt_data) = value {
4053 // Structure Tag Type = 0x02A0 + Structure Handle (template_instance_id)
4054 // Reference: 1756-PM020, Page 1080 (UDT Data Layout Considerations)
4055 0x02A0u16.wrapping_add(udt_data.symbol_id as u16)
4056 } else {
4057 value.get_data_type()
4058 };
4059 let value_bytes = value.to_bytes();
4060
4061 cip_request.extend_from_slice(&data_type.to_le_bytes()); // Data type
4062 cip_request.extend_from_slice(&[0x01, 0x00]); // Element count: 1
4063 cip_request.extend_from_slice(&value_bytes); // Value data
4064
4065 tracing::trace!(
4066 "Built CIP write request ({} bytes): {:02X?}",
4067 cip_request.len(),
4068 cip_request
4069 );
4070 Ok(cip_request)
4071 }
4072
4073 /// Builds a raw write request with pre-serialized data
4074 fn build_write_request_raw(
4075 &self,
4076 tag_name: &str,
4077 data: &[u8],
4078 ) -> crate::error::Result<Vec<u8>> {
4079 let mut request = Vec::new();
4080
4081 // Write Tag Service
4082 request.push(0x4D);
4083 request.push(0x00);
4084
4085 // Build tag path
4086 let tag_path = self.build_tag_path(tag_name);
4087 request.extend(tag_path);
4088
4089 // Add raw data
4090 request.extend(data);
4091
4092 Ok(request)
4093 }
4094
4095 /// Serializes a `PlcValue` into bytes for transmission
4096 #[allow(dead_code)]
4097 fn serialize_value(&self, value: &PlcValue) -> crate::error::Result<Vec<u8>> {
4098 let mut data = Vec::new();
4099
4100 match value {
4101 PlcValue::Bool(v) => {
4102 data.extend(&0x00C1u16.to_le_bytes()); // Data type
4103 data.push(if *v { 0xFF } else { 0x00 });
4104 }
4105 PlcValue::Sint(v) => {
4106 data.extend(&0x00C2u16.to_le_bytes()); // Data type
4107 data.extend(&v.to_le_bytes());
4108 }
4109 PlcValue::Int(v) => {
4110 data.extend(&0x00C3u16.to_le_bytes()); // Data type
4111 data.extend(&v.to_le_bytes());
4112 }
4113 PlcValue::Dint(v) => {
4114 data.extend(&0x00C4u16.to_le_bytes()); // Data type
4115 data.extend(&v.to_le_bytes());
4116 }
4117 PlcValue::Lint(v) => {
4118 data.extend(&0x00C5u16.to_le_bytes()); // Data type
4119 data.extend(&v.to_le_bytes());
4120 }
4121 PlcValue::Usint(v) => {
4122 data.extend(&0x00C6u16.to_le_bytes()); // Data type
4123 data.extend(&v.to_le_bytes());
4124 }
4125 PlcValue::Uint(v) => {
4126 data.extend(&0x00C7u16.to_le_bytes()); // Data type
4127 data.extend(&v.to_le_bytes());
4128 }
4129 PlcValue::Udint(v) => {
4130 data.extend(&0x00C8u16.to_le_bytes()); // Data type
4131 data.extend(&v.to_le_bytes());
4132 }
4133 PlcValue::Ulint(v) => {
4134 data.extend(&0x00C9u16.to_le_bytes()); // Data type
4135 data.extend(&v.to_le_bytes());
4136 }
4137 PlcValue::Real(v) => {
4138 data.extend(&0x00CAu16.to_le_bytes()); // Data type
4139 data.extend(&v.to_le_bytes());
4140 }
4141 PlcValue::Lreal(v) => {
4142 data.extend(&0x00CBu16.to_le_bytes()); // Data type
4143 data.extend(&v.to_le_bytes());
4144 }
4145 PlcValue::String(v) => {
4146 data.extend(&0x00CEu16.to_le_bytes()); // Data type - correct Allen-Bradley STRING CIP type
4147
4148 // Length field (4 bytes as DINT) - number of characters currently used
4149 let length = v.len().min(82) as u32;
4150 data.extend_from_slice(&length.to_le_bytes());
4151
4152 // String data - the actual characters (no MaxLen field)
4153 let string_bytes = v.as_bytes();
4154 let data_len = string_bytes.len().min(82);
4155 data.extend_from_slice(&string_bytes[..data_len]);
4156
4157 // Padding to make total data area exactly 82 bytes after length
4158 let remaining_chars = 82 - data_len;
4159 data.extend(vec![0u8; remaining_chars]);
4160 }
4161 PlcValue::Udt(_) => {
4162 // UDT serialization is handled by the UdtManager
4163 // For now, just add placeholder data
4164 data.extend(&0x00A0u16.to_le_bytes()); // UDT type code
4165 }
4166 }
4167
4168 Ok(data)
4169 }
4170
4171 pub fn build_list_tags_request(&self) -> Vec<u8> {
4172 tracing::debug!("Building list tags request");
4173
4174 // Build path array for Symbol Object Class (0x6B)
4175 let path_array = vec![
4176 // Class segment: Symbol Object Class (0x6B)
4177 0x20, // Class segment identifier
4178 0x6B, // Symbol Object Class
4179 // Instance segment: Start at Instance 0
4180 0x25, // Instance segment identifier with 0x00
4181 0x00, 0x00, 0x00,
4182 ];
4183
4184 // Request data: 2 Attributes - Attribute 1 and Attribute 2
4185 let request_data = vec![0x02, 0x00, 0x01, 0x00, 0x02, 0x00];
4186
4187 // Build CIP Message Router request
4188 let mut cip_request = Vec::new();
4189
4190 // Service: Get Instance Attribute List (0x55)
4191 cip_request.push(0x55);
4192
4193 // Request Path Size (in words)
4194 cip_request.push((path_array.len() / 2) as u8);
4195
4196 // Request Path
4197 cip_request.extend_from_slice(&path_array);
4198
4199 // Request Data
4200 cip_request.extend_from_slice(&request_data);
4201
4202 tracing::trace!(
4203 "Built CIP list tags request ({} bytes): {:02X?}",
4204 cip_request.len(),
4205 cip_request
4206 );
4207
4208 cip_request
4209 }
4210
4211 /// Gets a human-readable error message for a CIP status code
4212 ///
4213 /// # Arguments
4214 ///
4215 /// * `status` - The CIP status code to look up
4216 ///
4217 /// # Returns
4218 ///
4219 /// A string describing the error
4220 /// Parses extended CIP error codes from response data
4221 ///
4222 /// When general_status is 0xFF, the error code is in the additional status field.
4223 /// Format: [0]=service, [1]=reserved, [2]=0xFF, [3]=additional_status_size (words), [4-5]=extended_error_code
4224 fn parse_extended_error(&self, cip_data: &[u8]) -> crate::error::Result<String> {
4225 if cip_data.len() < 6 {
4226 return Err(EtherNetIpError::Protocol(
4227 "Extended error response too short".to_string(),
4228 ));
4229 }
4230
4231 let additional_status_size = cip_data[3] as usize; // Size in words
4232 if additional_status_size == 0 || cip_data.len() < 4 + (additional_status_size * 2) {
4233 return Ok("Extended error (no additional status)".to_string());
4234 }
4235
4236 // Extended error code is in the additional status field (2 bytes)
4237 // Try both little-endian and big-endian interpretations
4238 let extended_error_code_le = u16::from_le_bytes([cip_data[4], cip_data[5]]);
4239 let extended_error_code_be = u16::from_be_bytes([cip_data[4], cip_data[5]]);
4240
4241 // Map extended error codes (these are the same as regular error codes but in extended format)
4242 // Try little-endian first (standard CIP format)
4243 let error_msg = match extended_error_code_le {
4244 0x0001 => "Connection failure (extended)".to_string(),
4245 0x0002 => "Resource unavailable (extended)".to_string(),
4246 0x0003 => "Invalid parameter value (extended)".to_string(),
4247 0x0004 => "Path segment error (extended)".to_string(),
4248 0x0005 => "Path destination unknown (extended)".to_string(),
4249 0x0006 => "Partial transfer (extended)".to_string(),
4250 0x0007 => "Connection lost (extended)".to_string(),
4251 0x0008 => "Service not supported (extended)".to_string(),
4252 0x0009 => "Invalid attribute value (extended)".to_string(),
4253 0x000A => "Attribute list error (extended)".to_string(),
4254 0x000B => "Already in requested mode/state (extended)".to_string(),
4255 0x000C => "Object state conflict (extended)".to_string(),
4256 0x000D => "Object already exists (extended)".to_string(),
4257 0x000E => "Attribute not settable (extended)".to_string(),
4258 0x000F => "Privilege violation (extended)".to_string(),
4259 0x0010 => "Device state conflict (extended)".to_string(),
4260 0x0011 => "Reply data too large (extended)".to_string(),
4261 0x0012 => "Fragmentation of a primitive value (extended)".to_string(),
4262 0x0013 => "Not enough data (extended)".to_string(),
4263 0x0014 => "Attribute not supported (extended)".to_string(),
4264 0x0015 => "Too much data (extended)".to_string(),
4265 0x0016 => "Object does not exist (extended)".to_string(),
4266 0x0017 => "Service fragmentation sequence not in progress (extended)".to_string(),
4267 0x0018 => "No stored attribute data (extended)".to_string(),
4268 0x0019 => "Store operation failure (extended)".to_string(),
4269 0x001A => "Routing failure, request packet too large (extended)".to_string(),
4270 0x001B => "Routing failure, response packet too large (extended)".to_string(),
4271 0x001C => "Missing attribute list entry data (extended)".to_string(),
4272 0x001D => "Invalid attribute value list (extended)".to_string(),
4273 0x001E => "Embedded service error (extended)".to_string(),
4274 0x001F => "Vendor specific error (extended)".to_string(),
4275 0x0020 => "Invalid parameter (extended)".to_string(),
4276 0x0021 => "Write-once value or medium already written (extended)".to_string(),
4277 0x0022 => "Invalid reply received (extended)".to_string(),
4278 0x0023 => "Buffer overflow (extended)".to_string(),
4279 0x0024 => "Invalid message format (extended)".to_string(),
4280 0x0025 => "Key failure in path (extended)".to_string(),
4281 0x0026 => "Path size invalid (extended)".to_string(),
4282 0x0027 => "Unexpected attribute in list (extended)".to_string(),
4283 0x0028 => "Invalid member ID (extended)".to_string(),
4284 0x0029 => "Member not settable (extended)".to_string(),
4285 0x002A => "Group 2 only server general failure (extended)".to_string(),
4286 0x002B => "Unknown Modbus error (extended)".to_string(),
4287 0x002C => "Attribute not gettable (extended)".to_string(),
4288 // Try big-endian interpretation if little-endian doesn't match
4289 _ => {
4290 // Try big-endian interpretation
4291 match extended_error_code_be {
4292 0x0001 => "Connection failure (extended, BE)".to_string(),
4293 0x0002 => "Resource unavailable (extended, BE)".to_string(),
4294 0x0003 => "Invalid parameter value (extended, BE)".to_string(),
4295 0x0004 => "Path segment error (extended, BE)".to_string(),
4296 0x0005 => "Path destination unknown (extended, BE)".to_string(),
4297 0x0006 => "Partial transfer (extended, BE)".to_string(),
4298 0x0007 => "Connection lost (extended, BE)".to_string(),
4299 0x0008 => "Service not supported (extended, BE)".to_string(),
4300 0x0009 => "Invalid attribute value (extended, BE)".to_string(),
4301 0x000A => "Attribute list error (extended, BE)".to_string(),
4302 0x000B => "Already in requested mode/state (extended, BE)".to_string(),
4303 0x000C => "Object state conflict (extended, BE)".to_string(),
4304 0x000D => "Object already exists (extended, BE)".to_string(),
4305 0x000E => "Attribute not settable (extended, BE)".to_string(),
4306 0x000F => "Privilege violation (extended, BE)".to_string(),
4307 0x0010 => "Device state conflict (extended, BE)".to_string(),
4308 0x0011 => "Reply data too large (extended, BE)".to_string(),
4309 0x0012 => "Fragmentation of a primitive value (extended, BE)".to_string(),
4310 0x0013 => "Not enough data (extended, BE)".to_string(),
4311 0x0014 => "Attribute not supported (extended, BE)".to_string(),
4312 0x0015 => "Too much data (extended, BE)".to_string(),
4313 0x0016 => "Object does not exist (extended, BE)".to_string(),
4314 0x0017 => "Service fragmentation sequence not in progress (extended, BE)".to_string(),
4315 0x0018 => "No stored attribute data (extended, BE)".to_string(),
4316 0x0019 => "Store operation failure (extended, BE)".to_string(),
4317 0x001A => "Routing failure, request packet too large (extended, BE)".to_string(),
4318 0x001B => "Routing failure, response packet too large (extended, BE)".to_string(),
4319 0x001C => "Missing attribute list entry data (extended, BE)".to_string(),
4320 0x001D => "Invalid attribute value list (extended, BE)".to_string(),
4321 0x001E => "Embedded service error (extended, BE)".to_string(),
4322 0x001F => "Vendor specific error (extended, BE)".to_string(),
4323 0x0020 => "Invalid parameter (extended, BE)".to_string(),
4324 0x0021 => "Write-once value or medium already written (extended, BE)".to_string(),
4325 0x0022 => "Invalid reply received (extended, BE)".to_string(),
4326 0x0023 => "Buffer overflow (extended, BE)".to_string(),
4327 0x0024 => "Invalid message format (extended, BE)".to_string(),
4328 0x0025 => "Key failure in path (extended, BE)".to_string(),
4329 0x0026 => "Path size invalid (extended, BE)".to_string(),
4330 0x0027 => "Unexpected attribute in list (extended, BE)".to_string(),
4331 0x0028 => "Invalid member ID (extended, BE)".to_string(),
4332 0x0029 => "Member not settable (extended, BE)".to_string(),
4333 0x002A => "Group 2 only server general failure (extended, BE)".to_string(),
4334 0x002B => "Unknown Modbus error (extended, BE)".to_string(),
4335 0x002C => "Attribute not gettable (extended, BE)".to_string(),
4336 // Check if it's a vendor-specific or composite error
4337 _ if extended_error_code_le == 0x2107 || extended_error_code_be == 0x2107 => {
4338 // 0x2107 might be a composite error or vendor-specific
4339 // Bytes are [0x07, 0x21] - could be error 0x07 (Connection lost) with additional info 0x21
4340 // Or could be a vendor-specific extended error
4341 format!(
4342 "Vendor-specific or composite extended error: 0x{extended_error_code_le:04X} (LE) / 0x{extended_error_code_be:04X} (BE). Raw bytes: [0x{:02X}, 0x{:02X}]. This may indicate the PLC does not support writing to UDT array element members directly.",
4343 cip_data[4], cip_data[5]
4344 )
4345 }
4346 _ => format!(
4347 "Unknown extended CIP error code: 0x{extended_error_code_le:04X} (LE) / 0x{extended_error_code_be:04X} (BE). Raw bytes: [0x{:02X}, 0x{:02X}]",
4348 cip_data[4], cip_data[5]
4349 ),
4350 }
4351 }
4352 };
4353
4354 Ok(error_msg)
4355 }
4356
4357 /// Checks CIP response for errors, including extended error codes
4358 /// Returns Ok(()) if no error, Err with error message if error found
4359 fn check_cip_error(&self, cip_data: &[u8]) -> crate::error::Result<()> {
4360 if cip_data.len() < 3 {
4361 return Err(EtherNetIpError::Protocol(
4362 "CIP response too short for status check".to_string(),
4363 ));
4364 }
4365
4366 let general_status = cip_data[2];
4367
4368 if general_status == 0x00 {
4369 // Success
4370 return Ok(());
4371 }
4372
4373 // Check for extended error (0xFF indicates extended error code)
4374 if general_status == 0xFF {
4375 let error_msg = self.parse_extended_error(cip_data)?;
4376 return Err(EtherNetIpError::Protocol(format!(
4377 "CIP Extended Error: {error_msg}"
4378 )));
4379 }
4380
4381 // Regular error code
4382 let error_msg = self.get_cip_error_message(general_status);
4383 Err(EtherNetIpError::Protocol(format!(
4384 "CIP Error 0x{general_status:02X}: {error_msg}"
4385 )))
4386 }
4387
4388 fn get_cip_error_message(&self, status: u8) -> String {
4389 match status {
4390 0x00 => "Success".to_string(),
4391 0x01 => "Connection failure".to_string(),
4392 0x02 => "Resource unavailable".to_string(),
4393 0x03 => "Invalid parameter value".to_string(),
4394 0x04 => "Path segment error".to_string(),
4395 0x05 => "Path destination unknown".to_string(),
4396 0x06 => "Partial transfer".to_string(),
4397 0x07 => "Connection lost".to_string(),
4398 0x08 => "Service not supported".to_string(),
4399 0x09 => "Invalid attribute value".to_string(),
4400 0x0A => "Attribute list error".to_string(),
4401 0x0B => "Already in requested mode/state".to_string(),
4402 0x0C => "Object state conflict".to_string(),
4403 0x0D => "Object already exists".to_string(),
4404 0x0E => "Attribute not settable".to_string(),
4405 0x0F => "Privilege violation".to_string(),
4406 0x10 => "Device state conflict".to_string(),
4407 0x11 => "Reply data too large".to_string(),
4408 0x12 => "Fragmentation of a primitive value".to_string(),
4409 0x13 => "Not enough data".to_string(),
4410 0x14 => "Attribute not supported".to_string(),
4411 0x15 => "Too much data".to_string(),
4412 0x16 => "Object does not exist".to_string(),
4413 0x17 => "Service fragmentation sequence not in progress".to_string(),
4414 0x18 => "No stored attribute data".to_string(),
4415 0x19 => "Store operation failure".to_string(),
4416 0x1A => "Routing failure, request packet too large".to_string(),
4417 0x1B => "Routing failure, response packet too large".to_string(),
4418 0x1C => "Missing attribute list entry data".to_string(),
4419 0x1D => "Invalid attribute value list".to_string(),
4420 0x1E => "Embedded service error".to_string(),
4421 0x1F => "Vendor specific error".to_string(),
4422 0x20 => "Invalid parameter".to_string(),
4423 0x21 => "Write-once value or medium already written".to_string(),
4424 0x22 => "Invalid reply received".to_string(),
4425 0x23 => "Buffer overflow".to_string(),
4426 0x24 => "Invalid message format".to_string(),
4427 0x25 => "Key failure in path".to_string(),
4428 0x26 => "Path size invalid".to_string(),
4429 0x27 => "Unexpected attribute in list".to_string(),
4430 0x28 => "Invalid member ID".to_string(),
4431 0x29 => "Member not settable".to_string(),
4432 0x2A => "Group 2 only server general failure".to_string(),
4433 0x2B => "Unknown Modbus error".to_string(),
4434 0x2C => "Attribute not gettable".to_string(),
4435 _ => format!("Unknown CIP error code: 0x{status:02X}"),
4436 }
4437 }
4438
4439 async fn validate_session(&mut self) -> crate::error::Result<()> {
4440 let time_since_activity = self.last_activity.lock().await.elapsed();
4441
4442 // Send keep-alive if it's been more than 30 seconds since last activity
4443 if time_since_activity > Duration::from_secs(30) {
4444 self.send_keep_alive().await?;
4445 }
4446
4447 Ok(())
4448 }
4449
4450 async fn send_keep_alive(&mut self) -> crate::error::Result<()> {
4451 // Send NOP command (0x0000) — a valid 24-byte EtherNet/IP packet
4452 // that keeps the TCP connection alive without affecting session state.
4453 // NOP requires no response, so we don't read one.
4454 let packet = vec![0u8; 24];
4455 // Command: NOP (0x0000) — already zero
4456 // Length: 0 — already zero
4457 // Session handle, status, context, options — all zero for NOP
4458
4459 let mut stream = self.stream.lock().await;
4460 stream.write_all(&packet).await?;
4461 *self.last_activity.lock().await = Instant::now();
4462 Ok(())
4463 }
4464
4465 /// Quick connection health check (no I/O).
4466 ///
4467 /// Returns `true` if the session handle is valid and there has been activity
4468 /// within the last 150 seconds. Use this for cheap periodic checks; for a
4469 /// definitive check that the PLC is still responding, use [`check_health_detailed`](Self::check_health_detailed).
4470 pub async fn check_health(&self) -> bool {
4471 self.session_handle != 0
4472 && self.last_activity.lock().await.elapsed() < Duration::from_secs(150)
4473 }
4474
4475 /// Verifies the connection by sending a keep-alive (and re-registering if needed).
4476 ///
4477 /// Use this when you need to confirm the PLC is still reachable (e.g. after
4478 /// a long idle or before a critical operation). On failure, consider
4479 /// reconnecting; check [`EtherNetIpError::is_retriable`](crate::error::EtherNetIpError::is_retriable) on errors.
4480 pub async fn check_health_detailed(&mut self) -> crate::error::Result<bool> {
4481 if self.session_handle == 0 {
4482 return Ok(false);
4483 }
4484
4485 // Try sending a lightweight keep-alive command
4486 match self.send_keep_alive().await {
4487 Ok(()) => Ok(true),
4488 Err(_) => {
4489 // If keep-alive fails, try re-registering the session
4490 match self.register_session().await {
4491 Ok(()) => Ok(true),
4492 Err(_) => Ok(false),
4493 }
4494 }
4495 }
4496 }
4497
4498 /// Reads raw data from a tag
4499 async fn read_tag_raw(&mut self, tag_name: &str) -> crate::error::Result<Vec<u8>> {
4500 let response = self
4501 .send_cip_request(&self.build_read_request(tag_name))
4502 .await?;
4503 self.extract_cip_from_response(&response)
4504 }
4505
4506 /// Writes raw data to a tag
4507 #[allow(dead_code)]
4508 async fn write_tag_raw(&mut self, tag_name: &str, data: &[u8]) -> crate::error::Result<()> {
4509 let request = self.build_write_request_raw(tag_name, data)?;
4510 let response = self.send_cip_request(&request).await?;
4511
4512 // Check write response for errors
4513 let cip_response = self.extract_cip_from_response(&response)?;
4514
4515 if cip_response.len() < 3 {
4516 return Err(EtherNetIpError::Protocol(
4517 "Write response too short".to_string(),
4518 ));
4519 }
4520
4521 let service_reply = cip_response[0]; // Should be 0xCD (0x4D + 0x80) for Write Tag reply
4522 let general_status = cip_response[2]; // CIP status code
4523
4524 tracing::trace!(
4525 "Write response - Service: 0x{:02X}, Status: 0x{:02X}",
4526 service_reply,
4527 general_status
4528 );
4529
4530 // Check for errors (including extended errors)
4531 if let Err(e) = self.check_cip_error(&cip_response) {
4532 tracing::error!("[WRITE] CIP Error: {}", e);
4533 return Err(e);
4534 }
4535
4536 tracing::info!("Write completed successfully");
4537 Ok(())
4538 }
4539
4540 /// Builds an Unconnected Send message wrapping a CIP request
4541 ///
4542 /// Reference: EtherNetIP_Connection_Paths_and_Routing.md
4543 /// The route path goes at the END of the Unconnected Send message, NOT in the CIP service request.
4544 ///
4545 /// Structure:
4546 /// - Service: 0x52 (Unconnected Send)
4547 /// - Request Path: Connection Manager (Class 0x06, Instance 1)
4548 /// - Priority/Time Tick: 0x0A
4549 /// - Timeout Ticks: 0xF0
4550 /// - Embedded Message Length
4551 /// - Embedded CIP Message (Read Tag, Write Tag, etc.) ← NO route path here
4552 /// - Pad byte (if message length is odd)
4553 /// - Route Path Size
4554 /// - Reserved byte
4555 /// - Route Path ← Route path goes HERE
4556 fn build_unconnected_send(&self, embedded_message: &[u8]) -> Vec<u8> {
4557 let mut ucmm = vec![
4558 // Service: Unconnected Send (0x52)
4559 0x52, // Request Path Size: 2 words (4 bytes) for Connection Manager
4560 0x02,
4561 // Request Path: Connection Manager (Class 0x06, Instance 1)
4562 0x20, // Logical Class segment
4563 0x06, // Class 0x06 (Connection Manager)
4564 0x24, // Logical Instance segment
4565 0x01, // Instance 1
4566 // Priority/Time Tick: 0x0A
4567 0x0A, // Timeout Ticks: 0xF0 (240 ticks)
4568 0xF0,
4569 ];
4570
4571 // Embedded message length (16-bit, little-endian)
4572 let msg_len = embedded_message.len() as u16;
4573 ucmm.extend_from_slice(&msg_len.to_le_bytes());
4574
4575 // The actual CIP message (Read Tag, Write Tag, etc.) - NO route path here!
4576 ucmm.extend_from_slice(embedded_message);
4577
4578 // Pad byte if message length is odd
4579 if embedded_message.len() % 2 == 1 {
4580 ucmm.push(0x00);
4581 }
4582
4583 // Route Path Size (in 16-bit words)
4584 // Get route path if configured
4585 let route_path_bytes = if let Some(route_path) = &self.route_path {
4586 route_path.to_cip_bytes()
4587 } else {
4588 Vec::new()
4589 };
4590
4591 let route_path_words = if route_path_bytes.is_empty() {
4592 0
4593 } else {
4594 (route_path_bytes.len() / 2) as u8
4595 };
4596 ucmm.push(route_path_words);
4597
4598 // Reserved byte
4599 ucmm.push(0x00);
4600
4601 // Route Path - THIS IS WHERE [0x01, slot] GOES
4602 if !route_path_bytes.is_empty() {
4603 tracing::trace!(
4604 "Adding route path to Unconnected Send: {:02X?} ({} bytes, {} words)",
4605 route_path_bytes,
4606 route_path_bytes.len(),
4607 route_path_words
4608 );
4609 ucmm.extend_from_slice(&route_path_bytes);
4610 }
4611
4612 ucmm
4613 }
4614
4615 /// Sends a CIP request wrapped in EtherNet/IP SendRRData command
4616 pub async fn send_cip_request(&self, cip_request: &[u8]) -> Result<Vec<u8>> {
4617 tracing::trace!(
4618 "Sending CIP request ({} bytes): {:02X?}",
4619 cip_request.len(),
4620 cip_request
4621 );
4622
4623 // Build Unconnected Send message wrapping the CIP request
4624 // Route path goes at the END of Unconnected Send, NOT in the CIP request
4625 let ucmm_message = self.build_unconnected_send(cip_request);
4626
4627 tracing::trace!(
4628 "Unconnected Send message ({} bytes): {:02X?}",
4629 ucmm_message.len(),
4630 &ucmm_message[..std::cmp::min(64, ucmm_message.len())]
4631 );
4632
4633 // Calculate total packet size
4634 let ucmm_data_size = ucmm_message.len();
4635 let total_data_len = 4 + 2 + 2 + 8 + ucmm_data_size; // Interface + Timeout + Count + Items + UCMM
4636
4637 let mut packet = Vec::new();
4638
4639 // EtherNet/IP header (24 bytes)
4640 packet.extend_from_slice(&[0x6F, 0x00]); // Command: Send RR Data (0x006F)
4641 packet.extend_from_slice(&(total_data_len as u16).to_le_bytes()); // Length
4642 packet.extend_from_slice(&self.session_handle.to_le_bytes()); // Session handle
4643 packet.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); // Status
4644 packet.extend_from_slice(&[0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]); // Context
4645 packet.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); // Options
4646
4647 // CPF (Common Packet Format) data
4648 packet.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); // Interface handle
4649 packet.extend_from_slice(&[0x05, 0x00]); // Timeout (5 seconds)
4650 packet.extend_from_slice(&[0x02, 0x00]); // Item count: 2
4651
4652 // Item 1: Null Address Item (0x0000)
4653 packet.extend_from_slice(&[0x00, 0x00]); // Type: Null Address
4654 packet.extend_from_slice(&[0x00, 0x00]); // Length: 0
4655
4656 // Item 2: Unconnected Data Item (0x00B2)
4657 packet.extend_from_slice(&[0xB2, 0x00]); // Type: Unconnected Data
4658 packet.extend_from_slice(&(ucmm_data_size as u16).to_le_bytes()); // Length
4659
4660 // Add Unconnected Send message (which contains the CIP request + route path)
4661 packet.extend_from_slice(&ucmm_message);
4662
4663 tracing::trace!(
4664 "Built packet ({} bytes): {:02X?}",
4665 packet.len(),
4666 &packet[..std::cmp::min(64, packet.len())]
4667 );
4668
4669 // Send packet with timeout
4670 let mut stream = self.stream.lock().await;
4671 stream
4672 .write_all(&packet)
4673 .await
4674 .map_err(EtherNetIpError::Io)?;
4675
4676 // Read response header with timeout
4677 let mut header = [0u8; 24];
4678 match timeout(Duration::from_secs(10), stream.read_exact(&mut header)).await {
4679 Ok(Ok(_)) => {}
4680 Ok(Err(e)) => return Err(EtherNetIpError::Io(e)),
4681 Err(_) => return Err(EtherNetIpError::Timeout(Duration::from_secs(10))),
4682 }
4683
4684 // Check EtherNet/IP command status
4685 let cmd_status = u32::from_le_bytes([header[8], header[9], header[10], header[11]]);
4686 if cmd_status != 0 {
4687 return Err(EtherNetIpError::Protocol(format!(
4688 "EIP Command failed. Status: 0x{cmd_status:08X}"
4689 )));
4690 }
4691
4692 // Parse response length
4693 let response_length = u16::from_le_bytes([header[2], header[3]]) as usize;
4694 if response_length == 0 {
4695 return Ok(Vec::new());
4696 }
4697
4698 // Read response data with timeout
4699 let mut response_data = vec![0u8; response_length];
4700 match timeout(
4701 Duration::from_secs(10),
4702 stream.read_exact(&mut response_data),
4703 )
4704 .await
4705 {
4706 Ok(Ok(_)) => {}
4707 Ok(Err(e)) => return Err(EtherNetIpError::Io(e)),
4708 Err(_) => return Err(EtherNetIpError::Timeout(Duration::from_secs(10))),
4709 }
4710
4711 // Update last activity time
4712 *self.last_activity.lock().await = Instant::now();
4713
4714 tracing::trace!(
4715 "Received response ({} bytes): {:02X?}",
4716 response_data.len(),
4717 &response_data[..std::cmp::min(32, response_data.len())]
4718 );
4719
4720 Ok(response_data)
4721 }
4722
4723 /// Extracts CIP data from EtherNet/IP response packet
4724 fn extract_cip_from_response(&self, response: &[u8]) -> crate::error::Result<Vec<u8>> {
4725 tracing::trace!(
4726 "Extracting CIP from response ({} bytes): {:02X?}",
4727 response.len(),
4728 &response[..std::cmp::min(32, response.len())]
4729 );
4730
4731 // Parse CPF (Common Packet Format) structure directly from response data
4732 // Response format: [Interface(4)] [Timeout(2)] [ItemCount(2)] [Items...]
4733
4734 if response.len() < 8 {
4735 return Err(EtherNetIpError::Protocol(
4736 "Response too short for CPF header".to_string(),
4737 ));
4738 }
4739
4740 // Skip interface handle (4 bytes) and timeout (2 bytes)
4741 let mut pos = 6;
4742
4743 // Read item count
4744 let item_count = u16::from_le_bytes([response[pos], response[pos + 1]]);
4745 pos += 2;
4746 tracing::trace!("CPF item count: {}", item_count);
4747
4748 // Process items
4749 for i in 0..item_count {
4750 if pos + 4 > response.len() {
4751 return Err(EtherNetIpError::Protocol(
4752 "Response truncated while parsing items".to_string(),
4753 ));
4754 }
4755
4756 let item_type = u16::from_le_bytes([response[pos], response[pos + 1]]);
4757 let item_length = u16::from_le_bytes([response[pos + 2], response[pos + 3]]) as usize;
4758 pos += 4; // Skip item header
4759
4760 tracing::trace!(
4761 "Item {}: type=0x{:04X}, length={}",
4762 i,
4763 item_type,
4764 item_length
4765 );
4766
4767 if pos
4768 .checked_add(item_length)
4769 .map_or(true, |end| end > response.len())
4770 {
4771 return Err(EtherNetIpError::Protocol("Data item truncated".to_string()));
4772 }
4773
4774 if item_type == 0x00B2 {
4775 // Unconnected Data Item
4776 let cip_data = response[pos..pos + item_length].to_vec();
4777 tracing::trace!(
4778 "Found Unconnected Data Item, extracted CIP data ({} bytes)",
4779 cip_data.len()
4780 );
4781 tracing::trace!(
4782 "CIP data bytes: {:02X?}",
4783 &cip_data[..std::cmp::min(16, cip_data.len())]
4784 );
4785 return Ok(cip_data);
4786 } else {
4787 // Skip this item's data
4788 pos += item_length;
4789 }
4790 }
4791
4792 Err(EtherNetIpError::Protocol(
4793 "No Unconnected Data Item (0x00B2) found in response".to_string(),
4794 ))
4795 }
4796
4797 /// Parses CIP response and converts to `PlcValue`
4798 fn parse_cip_response(&self, cip_response: &[u8]) -> crate::error::Result<PlcValue> {
4799 tracing::trace!(
4800 "Parsing CIP response ({} bytes): {:02X?}",
4801 cip_response.len(),
4802 cip_response
4803 );
4804
4805 if cip_response.len() < 4 {
4806 return Err(EtherNetIpError::Protocol(
4807 "CIP response too short".to_string(),
4808 ));
4809 }
4810
4811 let service_reply = cip_response[0]; // Should be 0xCC (0x4C + 0x80) for Read Tag reply
4812 let general_status = cip_response[2]; // CIP status code
4813
4814 tracing::trace!(
4815 "Service reply: 0x{:02X}, Status: 0x{:02X}",
4816 service_reply,
4817 general_status
4818 );
4819
4820 // Check for CIP errors (including extended errors)
4821 if let Err(e) = self.check_cip_error(cip_response) {
4822 tracing::error!("CIP Error: {}", e);
4823 return Err(e);
4824 }
4825
4826 // For read operations, parse the returned data
4827 if service_reply == 0xCC {
4828 // Read Tag reply
4829 if cip_response.len() < 6 {
4830 return Err(EtherNetIpError::Protocol(
4831 "Read response too short for data".to_string(),
4832 ));
4833 }
4834
4835 let data_type = u16::from_le_bytes([cip_response[4], cip_response[5]]);
4836 let value_data = &cip_response[6..];
4837
4838 tracing::trace!(
4839 "Data type: 0x{:04X}, Value data ({} bytes): {:02X?}",
4840 data_type,
4841 value_data.len(),
4842 value_data
4843 );
4844
4845 // Parse based on data type
4846 match data_type {
4847 0x00C1 => {
4848 // BOOL
4849 if value_data.is_empty() {
4850 return Err(EtherNetIpError::Protocol(
4851 "No data for BOOL value".to_string(),
4852 ));
4853 }
4854 let value = value_data[0] != 0;
4855 tracing::trace!("Parsed BOOL: {}", value);
4856 Ok(PlcValue::Bool(value))
4857 }
4858 0x00C2 => {
4859 // SINT
4860 if value_data.is_empty() {
4861 return Err(EtherNetIpError::Protocol(
4862 "No data for SINT value".to_string(),
4863 ));
4864 }
4865 let value = value_data[0] as i8;
4866 tracing::trace!("Parsed SINT: {}", value);
4867 Ok(PlcValue::Sint(value))
4868 }
4869 0x00C3 => {
4870 // INT
4871 if value_data.len() < 2 {
4872 return Err(EtherNetIpError::Protocol(
4873 "Insufficient data for INT value".to_string(),
4874 ));
4875 }
4876 let value = i16::from_le_bytes([value_data[0], value_data[1]]);
4877 tracing::trace!("Parsed INT: {}", value);
4878 Ok(PlcValue::Int(value))
4879 }
4880 0x00C4 => {
4881 // DINT
4882 if value_data.len() < 4 {
4883 return Err(EtherNetIpError::Protocol(
4884 "Insufficient data for DINT value".to_string(),
4885 ));
4886 }
4887 let value = i32::from_le_bytes([
4888 value_data[0],
4889 value_data[1],
4890 value_data[2],
4891 value_data[3],
4892 ]);
4893 tracing::trace!("Parsed DINT: {}", value);
4894 Ok(PlcValue::Dint(value))
4895 }
4896 0x00C5 => {
4897 // LINT (64-bit signed integer)
4898 if value_data.len() < 8 {
4899 return Err(EtherNetIpError::Protocol(
4900 "Insufficient data for LINT value".to_string(),
4901 ));
4902 }
4903 let value = i64::from_le_bytes([
4904 value_data[0],
4905 value_data[1],
4906 value_data[2],
4907 value_data[3],
4908 value_data[4],
4909 value_data[5],
4910 value_data[6],
4911 value_data[7],
4912 ]);
4913 tracing::trace!("Parsed LINT: {}", value);
4914 Ok(PlcValue::Lint(value))
4915 }
4916 0x00C6 => {
4917 // USINT (8-bit unsigned integer)
4918 if value_data.is_empty() {
4919 return Err(EtherNetIpError::Protocol(
4920 "No data for USINT value".to_string(),
4921 ));
4922 }
4923 let value = value_data[0];
4924 tracing::trace!("Parsed USINT: {}", value);
4925 Ok(PlcValue::Usint(value))
4926 }
4927 0x00C7 => {
4928 // UINT (16-bit unsigned integer)
4929 if value_data.len() < 2 {
4930 return Err(EtherNetIpError::Protocol(
4931 "Insufficient data for UINT value".to_string(),
4932 ));
4933 }
4934 let value = u16::from_le_bytes([value_data[0], value_data[1]]);
4935 tracing::trace!("Parsed UINT: {}", value);
4936 Ok(PlcValue::Uint(value))
4937 }
4938 0x00C8 => {
4939 // UDINT (32-bit unsigned integer)
4940 if value_data.len() < 4 {
4941 return Err(EtherNetIpError::Protocol(
4942 "Insufficient data for UDINT value".to_string(),
4943 ));
4944 }
4945 let value = u32::from_le_bytes([
4946 value_data[0],
4947 value_data[1],
4948 value_data[2],
4949 value_data[3],
4950 ]);
4951 tracing::trace!("Parsed UDINT: {}", value);
4952 Ok(PlcValue::Udint(value))
4953 }
4954 0x00C9 => {
4955 // ULINT (64-bit unsigned integer)
4956 if value_data.len() < 8 {
4957 return Err(EtherNetIpError::Protocol(
4958 "Insufficient data for ULINT value".to_string(),
4959 ));
4960 }
4961 let value = u64::from_le_bytes([
4962 value_data[0],
4963 value_data[1],
4964 value_data[2],
4965 value_data[3],
4966 value_data[4],
4967 value_data[5],
4968 value_data[6],
4969 value_data[7],
4970 ]);
4971 tracing::trace!("Parsed ULINT: {}", value);
4972 Ok(PlcValue::Ulint(value))
4973 }
4974 0x00CA => {
4975 // REAL
4976 if value_data.len() < 4 {
4977 return Err(EtherNetIpError::Protocol(
4978 "Insufficient data for REAL value".to_string(),
4979 ));
4980 }
4981 let value = f32::from_le_bytes([
4982 value_data[0],
4983 value_data[1],
4984 value_data[2],
4985 value_data[3],
4986 ]);
4987 tracing::trace!("Parsed REAL: {}", value);
4988 Ok(PlcValue::Real(value))
4989 }
4990 0x00CB => {
4991 // LREAL (64-bit float)
4992 if value_data.len() < 8 {
4993 return Err(EtherNetIpError::Protocol(
4994 "Insufficient data for LREAL value".to_string(),
4995 ));
4996 }
4997 let value = f64::from_le_bytes([
4998 value_data[0],
4999 value_data[1],
5000 value_data[2],
5001 value_data[3],
5002 value_data[4],
5003 value_data[5],
5004 value_data[6],
5005 value_data[7],
5006 ]);
5007 tracing::trace!("Parsed LREAL: {}", value);
5008 Ok(PlcValue::Lreal(value))
5009 }
5010 0x00CE => {
5011 // Allen-Bradley STRING type (0x00CE)
5012 // STRING format: 4-byte length (DINT) followed by string data (up to 82 bytes)
5013 if value_data.len() < 4 {
5014 return Err(EtherNetIpError::Protocol(
5015 "Insufficient data for STRING length field".to_string(),
5016 ));
5017 }
5018 let length = u32::from_le_bytes([
5019 value_data[0],
5020 value_data[1],
5021 value_data[2],
5022 value_data[3],
5023 ]) as usize;
5024
5025 if value_data.len() < 4 || value_data.len() - 4 < length {
5026 return Err(EtherNetIpError::Protocol(format!(
5027 "Insufficient data for STRING value: need {} bytes, have {} bytes",
5028 4 + length,
5029 value_data.len()
5030 )));
5031 }
5032 let string_data = &value_data[4..4 + length];
5033 let value = String::from_utf8_lossy(string_data).to_string();
5034 tracing::trace!(
5035 "Parsed STRING (0x00CE): length={}, value='{}'",
5036 length,
5037 value
5038 );
5039 Ok(PlcValue::String(value))
5040 }
5041 0x00DA => {
5042 // Alternative STRING format (0x00DA) - single byte length
5043 if value_data.is_empty() {
5044 return Ok(PlcValue::String(String::new()));
5045 }
5046 let length = value_data[0] as usize;
5047 if value_data.len() < 1 + length {
5048 return Err(EtherNetIpError::Protocol(
5049 "Insufficient data for STRING value".to_string(),
5050 ));
5051 }
5052 let string_data = &value_data[1..1 + length];
5053 let value = String::from_utf8_lossy(string_data).to_string();
5054 tracing::trace!("Parsed STRING (0x00DA): '{}'", value);
5055 Ok(PlcValue::String(value))
5056 }
5057 0x02A0 => {
5058 // Allen-Bradley UDT type (0x02A0)
5059 // Note: symbol_id not available in parse_cip_response context
5060 // For proper UDT handling with symbol_id, use read_tag() which gets tag attributes
5061 tracing::trace!(
5062 "Detected UDT structure (0x02A0) with {} bytes",
5063 value_data.len()
5064 );
5065 Ok(PlcValue::Udt(UdtData {
5066 symbol_id: 0, // Not available in this context
5067 data: value_data.to_vec(),
5068 }))
5069 }
5070 0x00D3 => {
5071 // ULINT (64-bit unsigned integer) - sometimes returned for BOOL arrays
5072 // BOOL arrays in Allen-Bradley are stored as DWORD arrays (32 bits per DWORD)
5073 // The PLC may return 4 bytes (DWORD) for BOOL arrays
5074 if value_data.len() >= 4 {
5075 // Parse as DWORD (4 bytes) - BOOL arrays are often returned as DWORD
5076 let dword_value = u32::from_le_bytes([
5077 value_data[0],
5078 value_data[1],
5079 value_data[2],
5080 value_data[3],
5081 ]);
5082 tracing::trace!(
5083 "Parsed 0x00D3 as DWORD (BOOL array): {} (0x{:08X})",
5084 dword_value,
5085 dword_value
5086 );
5087 // Return as UDINT (DWORD) - this represents the first 32 BOOLs
5088 Ok(PlcValue::Udint(dword_value))
5089 } else if value_data.len() >= 8 {
5090 // If we have 8 bytes, parse as ULINT
5091 let value = u64::from_le_bytes([
5092 value_data[0],
5093 value_data[1],
5094 value_data[2],
5095 value_data[3],
5096 value_data[4],
5097 value_data[5],
5098 value_data[6],
5099 value_data[7],
5100 ]);
5101 tracing::trace!("Parsed ULINT: {}", value);
5102 Ok(PlcValue::Ulint(value))
5103 } else {
5104 Err(EtherNetIpError::Protocol(
5105 "Insufficient data for ULINT/DWORD value".to_string(),
5106 ))
5107 }
5108 }
5109 0x00A0 => {
5110 // UDT (User Defined Type)
5111 // Note: symbol_id will be 0 here since we don't have tag context
5112 // For proper UDT handling with symbol_id, use read_tag() which
5113 // gets tag attributes first
5114 tracing::trace!(
5115 "Parsed UDT ({} bytes) - note: symbol_id not available in this context",
5116 value_data.len()
5117 );
5118 Ok(PlcValue::Udt(UdtData {
5119 symbol_id: 0, // Will need to be set by caller if available
5120 data: value_data.to_vec(),
5121 }))
5122 }
5123 _ => {
5124 tracing::warn!("Unknown data type: 0x{:04X}", data_type);
5125 Err(EtherNetIpError::Protocol(format!(
5126 "Unsupported data type: 0x{data_type:04X}"
5127 )))
5128 }
5129 }
5130 } else if service_reply == 0xCD {
5131 // Write Tag reply - no data to parse
5132 tracing::debug!("Write operation successful");
5133 Ok(PlcValue::Bool(true)) // Indicate success
5134 } else {
5135 Err(EtherNetIpError::Protocol(format!(
5136 "Unknown service reply: 0x{service_reply:02X}"
5137 )))
5138 }
5139 }
5140
5141 /// Unregisters the EtherNet/IP session with the PLC
5142 pub async fn unregister_session(&mut self) -> crate::error::Result<()> {
5143 tracing::info!("Unregistering session and cleaning up connections...");
5144
5145 // Close all connected sessions first
5146 let _ = self.close_all_connected_sessions().await;
5147
5148 let mut packet = Vec::new();
5149
5150 // EtherNet/IP encapsulation header (24 bytes, no command-specific data)
5151 packet.extend_from_slice(&[0x66, 0x00]); // Command: Unregister Session
5152 packet.extend_from_slice(&[0x00, 0x00]); // Length: 0 (no data payload)
5153 packet.extend_from_slice(&self.session_handle.to_le_bytes()); // Session handle
5154 packet.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); // Status
5155 packet.extend_from_slice(&[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]); // Sender context
5156 packet.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); // Options
5157
5158 self.stream
5159 .lock()
5160 .await
5161 .write_all(&packet)
5162 .await
5163 .map_err(EtherNetIpError::Io)?;
5164
5165 tracing::info!("Session unregistered and all connections closed");
5166 Ok(())
5167 }
5168
5169 /// Builds a CIP Read Tag Service request
5170 fn build_read_request(&self, tag_name: &str) -> Vec<u8> {
5171 self.build_read_request_with_count(tag_name, 1)
5172 }
5173
5174 /// Builds a CIP Read Tag Service request with specified element count
5175 ///
5176 /// Reference: 1756-PM020, Page 220-252 (Read Tag Service)
5177 fn build_read_request_with_count(&self, tag_name: &str, element_count: u16) -> Vec<u8> {
5178 tracing::debug!(
5179 "Building read request for tag: '{}' with count: {}",
5180 tag_name,
5181 element_count
5182 );
5183
5184 let mut cip_request = Vec::new();
5185
5186 // Service: Read Tag Service (0x4C)
5187 // Reference: 1756-PM020, Page 220
5188 cip_request.push(0x4C);
5189
5190 // Build the path based on tag name format
5191 let path = self.build_tag_path(tag_name);
5192
5193 // Request Path Size (in words)
5194 let path_size_words = (path.len() / 2) as u8;
5195 tracing::debug!(
5196 "Path size calculation: {} bytes / 2 = {} words for tag '{}'",
5197 path.len(),
5198 path_size_words,
5199 tag_name
5200 );
5201 cip_request.push(path_size_words);
5202
5203 // Request Path
5204 cip_request.extend_from_slice(&path);
5205
5206 // Element count (little-endian)
5207 // Reference: 1756-PM020, Page 241 (Request Data: Number of elements to read)
5208 cip_request.extend_from_slice(&element_count.to_le_bytes());
5209
5210 tracing::debug!(
5211 "Built CIP read request ({} bytes) for tag '{}': {:02X?}",
5212 cip_request.len(),
5213 tag_name,
5214 cip_request
5215 );
5216 tracing::debug!(
5217 "Path bytes ({} bytes, {} words) for tag '{}': {:02X?}",
5218 path.len(),
5219 path_size_words,
5220 tag_name,
5221 path
5222 );
5223
5224 cip_request
5225 }
5226
5227 /// Builds an Element ID segment for array element addressing
5228 ///
5229 /// Reference: 1756-PM020, Pages 603-611, 870-890 (Element ID Segment Size Selection)
5230 ///
5231 /// Element ID segments use different sizes based on index value:
5232 /// - 0-255: 8-bit Element ID (0x28 + 1 byte value)
5233 /// - 256-65535: 16-bit Element ID (0x29 0x00 + 2 bytes low, high)
5234 /// - 65536+: 32-bit Element ID (0x2A 0x00 + 4 bytes lowest to highest)
5235 #[cfg_attr(not(test), allow(dead_code))]
5236 pub fn build_element_id_segment(&self, index: u32) -> Vec<u8> {
5237 let mut segment = Vec::new();
5238
5239 if index <= 255 {
5240 // 8-bit Element ID: 0x28 + index (2 bytes total)
5241 // Reference: 1756-PM020, Page 607, Example 1
5242 segment.push(0x28);
5243 segment.push(index as u8);
5244 } else if index <= 65535 {
5245 // 16-bit Element ID: 0x29, 0x00, low_byte, high_byte (4 bytes total)
5246 // Reference: 1756-PM020, Page 666-684, Example 3
5247 segment.push(0x29);
5248 segment.push(0x00); // Padding byte
5249 segment.extend_from_slice(&(index as u16).to_le_bytes());
5250 } else {
5251 // 32-bit Element ID: 0x2A, 0x00, byte0, byte1, byte2, byte3 (6 bytes total)
5252 // Reference: 1756-PM020, Page 144-146 (Element ID Segments table)
5253 segment.push(0x2A);
5254 segment.push(0x00); // Padding byte
5255 segment.extend_from_slice(&index.to_le_bytes());
5256 }
5257
5258 segment
5259 }
5260
5261 /// Builds base tag path without array element addressing
5262 ///
5263 /// Extracts the base tag name from array notation (e.g., "MyArray[5]" -> "MyArray")
5264 /// Reference: 1756-PM020, Page 894-909 (ANSI Extended Symbol Segment Construction)
5265 #[cfg_attr(not(test), allow(dead_code))]
5266 pub fn build_base_tag_path(&self, tag_name: &str) -> Vec<u8> {
5267 // Parse tag path but strip array indices
5268 match TagPath::parse(tag_name) {
5269 Ok(path) => {
5270 // If it's an array path, get just the base
5271 let base_path = match &path {
5272 TagPath::Array { base_path, .. } => base_path.as_ref(),
5273 _ => &path,
5274 };
5275 base_path.to_cip_path().unwrap_or_else(|_| {
5276 // Fallback: simple symbol segment
5277 // Reference: 1756-PM020, Page 894-909
5278 let mut path = Vec::new();
5279 path.push(0x91); // ANSI Extended Symbol Segment
5280 let name_bytes = tag_name.as_bytes();
5281 path.push(name_bytes.len() as u8);
5282 path.extend_from_slice(name_bytes);
5283 // Pad to word boundary if odd length
5284 if path.len() % 2 != 0 {
5285 path.push(0x00);
5286 }
5287 path
5288 })
5289 }
5290 Err(_) => {
5291 // Fallback: simple symbol segment
5292 let mut path = Vec::new();
5293 path.push(0x91); // ANSI Extended Symbol Segment
5294 let name_bytes = tag_name.as_bytes();
5295 path.push(name_bytes.len() as u8);
5296 path.extend_from_slice(name_bytes);
5297 // Pad to word boundary if odd length
5298 if path.len() % 2 != 0 {
5299 path.push(0x00);
5300 }
5301 path
5302 }
5303 }
5304 }
5305
5306 /// Builds a CIP Read Tag Service request for array elements with element addressing
5307 ///
5308 /// This method uses proper CIP element addressing (0x28/0x29/0x2A segments) in the
5309 /// Request Path to read specific array elements or ranges.
5310 ///
5311 /// Reference: 1756-PM020, Pages 603-611, 815-851 (Array Element Addressing Examples)
5312 ///
5313 /// # Arguments
5314 ///
5315 /// * `base_array_name` - Base name of the array (e.g., "MyArray" for "MyArray[10]")
5316 /// * `start_index` - Starting element index (0-based)
5317 /// * `element_count` - Number of elements to read
5318 ///
5319 /// # Example
5320 ///
5321 /// Reading elements 10-14 of array "MyArray" (5 elements):
5322 /// ```
5323 /// # async fn example() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
5324 /// # let mut client = rust_ethernet_ip::EipClient::connect("192.168.1.100:44818").await?;
5325 /// let request = client.build_read_array_request("MyArray", 10, 5);
5326 /// # Ok(())
5327 /// # }
5328 /// ```
5329 ///
5330 /// This generates:
5331 /// - Request Path: [0x91] "MyArray" [0x28] [0x0A] (element 10)
5332 /// - Request Data: [0x05 0x00] (5 elements)
5333 #[cfg_attr(not(test), allow(dead_code))]
5334 pub fn build_read_array_request(
5335 &self,
5336 base_array_name: &str,
5337 start_index: u32,
5338 element_count: u16,
5339 ) -> Vec<u8> {
5340 let mut cip_request = Vec::new();
5341
5342 // Service: Read Tag Service (0x4C)
5343 // Reference: 1756-PM020, Page 220
5344 cip_request.push(0x4C);
5345
5346 // Build base tag path (symbolic segment)
5347 // Reference: 1756-PM020, Page 894-909
5348 // NOTE: Route path does NOT go here - it goes at the end of Unconnected Send message
5349 // Reference: EtherNetIP_Connection_Paths_and_Routing.md
5350 let mut full_path = self.build_base_tag_path(base_array_name);
5351
5352 tracing::trace!(
5353 "build_read_array_request: base_path for '{}' = {:02X?} ({} bytes)",
5354 base_array_name,
5355 full_path,
5356 full_path.len()
5357 );
5358
5359 // Add element addressing segment
5360 // Reference: 1756-PM020, Pages 603-611, 870-890
5361 let element_segment = self.build_element_id_segment(start_index);
5362 tracing::trace!(
5363 "build_read_array_request: element_segment for index {} = {:02X?} ({} bytes)",
5364 start_index,
5365 element_segment,
5366 element_segment.len()
5367 );
5368 full_path.extend_from_slice(&element_segment);
5369
5370 // Ensure path is word-aligned
5371 if full_path.len() % 2 != 0 {
5372 full_path.push(0x00);
5373 }
5374
5375 // Path size (in words)
5376 let path_size = (full_path.len() / 2) as u8;
5377 cip_request.push(path_size);
5378 cip_request.extend_from_slice(&full_path);
5379
5380 // Request Data: Element count (NOT in path, but in Request Data)
5381 // Reference: 1756-PM020, Page 840-851 (Reading Multiple Array Elements)
5382 cip_request.extend_from_slice(&element_count.to_le_bytes());
5383
5384 tracing::trace!(
5385 "build_read_array_request: final request = {:02X?} ({} bytes), path_size = {} words ({} bytes)",
5386 cip_request, cip_request.len(), path_size, full_path.len()
5387 );
5388
5389 cip_request
5390 }
5391
5392 /// Builds the correct path for a tag name
5393 /// Uses TagPath parser to properly handle arrays, bits, UDTs, etc.
5394 ///
5395 /// For ControlLogix, prepends the route path (backplane routing) if configured.
5396 /// Reference: EtherNetIP_Connection_Paths_and_Routing.md
5397 fn build_tag_path(&self, tag_name: &str) -> Vec<u8> {
5398 // Build the application path (tag name)
5399 // NOTE: Route path does NOT go here - it goes at the end of Unconnected Send message
5400 // Reference: EtherNetIP_Connection_Paths_and_Routing.md
5401 let app_path = match TagPath::parse(tag_name) {
5402 Ok(tag_path) => {
5403 tracing::debug!("Parsed tag path for '{}': {:?}", tag_name, tag_path);
5404 // Generate CIP path using the proper parser
5405 match tag_path.to_cip_path() {
5406 Ok(path) => {
5407 tracing::debug!(
5408 "TagPath generated {} bytes ({} words) for '{}': {:02X?}",
5409 path.len(),
5410 path.len() / 2,
5411 tag_name,
5412 path
5413 );
5414 path
5415 }
5416 Err(e) => {
5417 tracing::warn!("TagPath.to_cip_path() failed for '{}': {}", tag_name, e);
5418 // Fallback to old method if parsing fails
5419 self.build_simple_tag_path_legacy(tag_name)
5420 }
5421 }
5422 }
5423 Err(e) => {
5424 tracing::warn!("TagPath::parse() failed for '{}': {}", tag_name, e);
5425 // Fallback to old method if parsing fails
5426 self.build_simple_tag_path_legacy(tag_name)
5427 }
5428 };
5429
5430 app_path
5431 }
5432
5433 /// Builds a simple tag path (no program prefix) - legacy method for fallback
5434 fn build_simple_tag_path_legacy(&self, tag_name: &str) -> Vec<u8> {
5435 let mut path = Vec::new();
5436 path.push(0x91); // ANSI Extended Symbol Segment
5437 path.push(tag_name.len() as u8);
5438 path.extend_from_slice(tag_name.as_bytes());
5439
5440 // Pad to even length if necessary
5441 if tag_name.len() % 2 != 0 {
5442 path.push(0x00);
5443 }
5444
5445 path
5446 }
5447
5448 // =========================================================================
5449 // BATCH OPERATIONS IMPLEMENTATION
5450 // =========================================================================
5451
5452 /// Executes a batch of read and write operations
5453 ///
5454 /// This is the main entry point for batch operations. It takes a slice of
5455 /// `BatchOperation` items and executes them efficiently by grouping them
5456 /// into optimal CIP packets based on the current `BatchConfig`.
5457 ///
5458 /// # Arguments
5459 ///
5460 /// * `operations` - A slice of operations to execute
5461 ///
5462 /// # Returns
5463 ///
5464 /// A vector of `BatchResult` items, one for each input operation.
5465 /// Results are returned in the same order as the input operations.
5466 ///
5467 /// # Performance
5468 ///
5469 /// - **Throughput**: 5,000-15,000+ operations/second (vs 1,500 individual)
5470 /// - **Latency**: 5-20ms per batch (vs 1-3ms per individual operation)
5471 /// - **Network efficiency**: 1-5 packets vs N packets for N operations
5472 ///
5473 /// # Examples
5474 ///
5475 /// ```rust,no_run
5476 /// use rust_ethernet_ip::{EipClient, BatchOperation, PlcValue};
5477 ///
5478 /// #[tokio::main]
5479 /// async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
5480 /// let mut client = EipClient::connect("192.168.1.100:44818").await?;
5481 ///
5482 /// let operations = vec![
5483 /// BatchOperation::Read { tag_name: "Motor1_Speed".to_string() },
5484 /// BatchOperation::Read { tag_name: "Motor2_Speed".to_string() },
5485 /// BatchOperation::Write {
5486 /// tag_name: "SetPoint".to_string(),
5487 /// value: PlcValue::Dint(1500)
5488 /// },
5489 /// ];
5490 ///
5491 /// let results = client.execute_batch(&operations).await?;
5492 ///
5493 /// for result in results {
5494 /// match result.result {
5495 /// Ok(Some(value)) => println!("Read value: {:?}", value),
5496 /// Ok(None) => println!("Write successful"),
5497 /// Err(e) => println!("Operation failed: {}", e),
5498 /// }
5499 /// }
5500 ///
5501 /// Ok(())
5502 /// }
5503 /// ```
5504 pub async fn execute_batch(
5505 &mut self,
5506 operations: &[BatchOperation],
5507 ) -> crate::error::Result<Vec<BatchResult>> {
5508 if operations.is_empty() {
5509 return Ok(Vec::new());
5510 }
5511
5512 let start_time = Instant::now();
5513 tracing::debug!(
5514 "[BATCH] Starting batch execution with {} operations",
5515 operations.len()
5516 );
5517
5518 // Group operations based on configuration
5519 let operation_groups = if self.batch_config.optimize_packet_packing {
5520 self.optimize_operation_groups(operations)
5521 } else {
5522 self.sequential_operation_groups(operations)
5523 };
5524
5525 let mut all_results = Vec::with_capacity(operations.len());
5526
5527 // Execute each group
5528 for (group_index, group) in operation_groups.iter().enumerate() {
5529 tracing::debug!(
5530 "[BATCH] Processing group {} with {} operations",
5531 group_index + 1,
5532 group.len()
5533 );
5534
5535 match self.execute_operation_group(group).await {
5536 Ok(mut group_results) => {
5537 all_results.append(&mut group_results);
5538 }
5539 Err(e) => {
5540 if !self.batch_config.continue_on_error {
5541 return Err(e);
5542 }
5543
5544 // Create error results for this group
5545 for op in group {
5546 let error_result = BatchResult {
5547 operation: op.clone(),
5548 result: Err(BatchError::NetworkError(e.to_string())),
5549 execution_time_us: 0,
5550 };
5551 all_results.push(error_result);
5552 }
5553 }
5554 }
5555 }
5556
5557 let total_time = start_time.elapsed();
5558 tracing::info!(
5559 "[BATCH] Completed batch execution in {:?} - {} operations processed",
5560 total_time,
5561 all_results.len()
5562 );
5563
5564 Ok(all_results)
5565 }
5566
5567 /// Reads multiple tags in a single batch operation
5568 ///
5569 /// This is a convenience method for read-only batch operations.
5570 /// It's optimized for reading many tags at once.
5571 ///
5572 /// # Arguments
5573 ///
5574 /// * `tag_names` - A slice of tag names to read
5575 ///
5576 /// # Returns
5577 ///
5578 /// A vector of tuples containing `(tag_name, result)` pairs
5579 ///
5580 /// # Examples
5581 ///
5582 /// ```rust,no_run
5583 /// use rust_ethernet_ip::EipClient;
5584 ///
5585 /// #[tokio::main]
5586 /// async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
5587 /// let mut client = EipClient::connect("192.168.1.100:44818").await?;
5588 ///
5589 /// let tags = ["Motor1_Speed", "Motor2_Speed", "Temperature", "Pressure"];
5590 /// let results = client.read_tags_batch(&tags).await?;
5591 ///
5592 /// for (tag_name, result) in results {
5593 /// match result {
5594 /// Ok(value) => println!("{}: {:?}", tag_name, value),
5595 /// Err(e) => println!("{}: Error - {}", tag_name, e),
5596 /// }
5597 /// }
5598 ///
5599 /// Ok(())
5600 /// }
5601 /// ```
5602 pub async fn read_tags_batch(
5603 &mut self,
5604 tag_names: &[&str],
5605 ) -> crate::error::Result<Vec<(String, std::result::Result<PlcValue, BatchError>)>> {
5606 let operations: Vec<BatchOperation> = tag_names
5607 .iter()
5608 .map(|&name| BatchOperation::Read {
5609 tag_name: name.to_string(),
5610 })
5611 .collect();
5612
5613 let results = self.execute_batch(&operations).await?;
5614
5615 Ok(results
5616 .into_iter()
5617 .map(|result| {
5618 let tag_name = match &result.operation {
5619 BatchOperation::Read { tag_name } => tag_name.clone(),
5620 BatchOperation::Write { .. } => {
5621 unreachable!("Should only have read operations")
5622 }
5623 };
5624
5625 let value_result = match result.result {
5626 Ok(Some(value)) => Ok(value),
5627 Ok(None) => Err(BatchError::Other(
5628 "Unexpected None result for read operation".to_string(),
5629 )),
5630 Err(e) => Err(e),
5631 };
5632
5633 (tag_name, value_result)
5634 })
5635 .collect())
5636 }
5637
5638 /// Writes multiple tag values in a single batch operation
5639 ///
5640 /// This is a convenience method for write-only batch operations.
5641 /// It's optimized for writing many values at once.
5642 ///
5643 /// # Arguments
5644 ///
5645 /// * `tag_values` - A slice of `(tag_name, value)` tuples to write
5646 ///
5647 /// # Returns
5648 ///
5649 /// A vector of tuples containing `(tag_name, result)` pairs
5650 ///
5651 /// # Examples
5652 ///
5653 /// ```rust,no_run
5654 /// use rust_ethernet_ip::{EipClient, PlcValue};
5655 ///
5656 /// #[tokio::main]
5657 /// async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
5658 /// let mut client = EipClient::connect("192.168.1.100:44818").await?;
5659 ///
5660 /// let writes = vec![
5661 /// ("SetPoint1", PlcValue::Bool(true)),
5662 /// ("SetPoint2", PlcValue::Dint(2000)),
5663 /// ("EnableFlag", PlcValue::Bool(true)),
5664 /// ];
5665 ///
5666 /// let results = client.write_tags_batch(&writes).await?;
5667 ///
5668 /// for (tag_name, result) in results {
5669 /// match result {
5670 /// Ok(_) => println!("{}: Write successful", tag_name),
5671 /// Err(e) => println!("{}: Write failed - {}", tag_name, e),
5672 /// }
5673 /// }
5674 ///
5675 /// Ok(())
5676 /// }
5677 /// ```
5678 pub async fn write_tags_batch(
5679 &mut self,
5680 tag_values: &[(&str, PlcValue)],
5681 ) -> crate::error::Result<Vec<(String, std::result::Result<(), BatchError>)>> {
5682 let operations: Vec<BatchOperation> = tag_values
5683 .iter()
5684 .map(|(name, value)| BatchOperation::Write {
5685 tag_name: name.to_string(),
5686 value: value.clone(),
5687 })
5688 .collect();
5689
5690 let results = self.execute_batch(&operations).await?;
5691
5692 Ok(results
5693 .into_iter()
5694 .map(|result| {
5695 let tag_name = match &result.operation {
5696 BatchOperation::Write { tag_name, .. } => tag_name.clone(),
5697 BatchOperation::Read { .. } => {
5698 unreachable!("Should only have write operations")
5699 }
5700 };
5701
5702 let write_result = match result.result {
5703 Ok(None) => Ok(()),
5704 Ok(Some(_)) => Err(BatchError::Other(
5705 "Unexpected value result for write operation".to_string(),
5706 )),
5707 Err(e) => Err(e),
5708 };
5709
5710 (tag_name, write_result)
5711 })
5712 .collect())
5713 }
5714
5715 /// Configures batch operation settings
5716 ///
5717 /// This method allows fine-tuning of batch operation behavior,
5718 /// including performance optimizations and error handling.
5719 ///
5720 /// # Arguments
5721 ///
5722 /// * `config` - The new batch configuration to use
5723 ///
5724 /// # Examples
5725 ///
5726 /// ```rust,no_run
5727 /// use rust_ethernet_ip::{EipClient, BatchConfig};
5728 ///
5729 /// #[tokio::main]
5730 /// async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
5731 /// let mut client = EipClient::connect("192.168.1.100:44818").await?;
5732 ///
5733 /// let config = BatchConfig {
5734 /// max_operations_per_packet: 50,
5735 /// max_packet_size: 1500,
5736 /// packet_timeout_ms: 5000,
5737 /// continue_on_error: false,
5738 /// optimize_packet_packing: true,
5739 /// };
5740 ///
5741 /// client.configure_batch_operations(config);
5742 ///
5743 /// Ok(())
5744 /// }
5745 /// ```
5746 pub fn configure_batch_operations(&mut self, config: BatchConfig) {
5747 self.batch_config = config;
5748 tracing::debug!(
5749 "[BATCH] Updated batch configuration: max_ops={}, max_size={}, timeout={}ms",
5750 self.batch_config.max_operations_per_packet,
5751 self.batch_config.max_packet_size,
5752 self.batch_config.packet_timeout_ms
5753 );
5754 }
5755
5756 /// Gets current batch operation configuration
5757 pub fn get_batch_config(&self) -> &BatchConfig {
5758 &self.batch_config
5759 }
5760
5761 // =========================================================================
5762 // INTERNAL BATCH OPERATION HELPERS
5763 // =========================================================================
5764
5765 /// Groups operations optimally for batch processing
5766 fn optimize_operation_groups(&self, operations: &[BatchOperation]) -> Vec<Vec<BatchOperation>> {
5767 let mut groups = Vec::new();
5768 let mut reads = Vec::new();
5769 let mut writes = Vec::new();
5770
5771 // Separate reads and writes
5772 for op in operations {
5773 match op {
5774 BatchOperation::Read { .. } => reads.push(op.clone()),
5775 BatchOperation::Write { .. } => writes.push(op.clone()),
5776 }
5777 }
5778
5779 // Group reads
5780 for chunk in reads.chunks(self.batch_config.max_operations_per_packet) {
5781 groups.push(chunk.to_vec());
5782 }
5783
5784 // Group writes
5785 for chunk in writes.chunks(self.batch_config.max_operations_per_packet) {
5786 groups.push(chunk.to_vec());
5787 }
5788
5789 groups
5790 }
5791
5792 /// Groups operations sequentially (preserves order)
5793 fn sequential_operation_groups(
5794 &self,
5795 operations: &[BatchOperation],
5796 ) -> Vec<Vec<BatchOperation>> {
5797 operations
5798 .chunks(self.batch_config.max_operations_per_packet)
5799 .map(|chunk| chunk.to_vec())
5800 .collect()
5801 }
5802
5803 /// Executes a single group of operations as a CIP Multiple Service Packet
5804 async fn execute_operation_group(
5805 &mut self,
5806 operations: &[BatchOperation],
5807 ) -> crate::error::Result<Vec<BatchResult>> {
5808 let start_time = Instant::now();
5809 let mut results = Vec::with_capacity(operations.len());
5810
5811 // Build Multiple Service Packet request
5812 let cip_request = self.build_multiple_service_packet(operations)?;
5813
5814 // Send request and get response
5815 let response = self.send_cip_request(&cip_request).await?;
5816
5817 // Parse response and create results
5818 let parsed_results = self.parse_multiple_service_response(&response, operations)?;
5819
5820 let execution_time = start_time.elapsed();
5821
5822 // Create BatchResult objects
5823 for (i, operation) in operations.iter().enumerate() {
5824 let op_execution_time = execution_time.as_micros() as u64 / operations.len() as u64;
5825
5826 let result = if i < parsed_results.len() {
5827 match &parsed_results[i] {
5828 Ok(value) => Ok(value.clone()),
5829 Err(e) => Err(e.clone()),
5830 }
5831 } else {
5832 Err(BatchError::Other(
5833 "Missing result from response".to_string(),
5834 ))
5835 };
5836
5837 results.push(BatchResult {
5838 operation: operation.clone(),
5839 result,
5840 execution_time_us: op_execution_time,
5841 });
5842 }
5843
5844 Ok(results)
5845 }
5846
5847 /// Builds a CIP Multiple Service Packet request
5848 fn build_multiple_service_packet(
5849 &self,
5850 operations: &[BatchOperation],
5851 ) -> crate::error::Result<Vec<u8>> {
5852 let mut packet = Vec::with_capacity(8 + (operations.len() * 2));
5853
5854 // Multiple Service Packet service code
5855 packet.push(0x0A);
5856
5857 // Request path (2 bytes for class 0x02, instance 1)
5858 packet.push(0x02); // Path size in words
5859 packet.push(0x20); // Class segment
5860 packet.push(0x02); // Class 0x02 (Message Router)
5861 packet.push(0x24); // Instance segment
5862 packet.push(0x01); // Instance 1
5863
5864 // Number of services
5865 packet.extend_from_slice(&(operations.len() as u16).to_le_bytes());
5866
5867 // Calculate offset table
5868 let mut service_requests = Vec::with_capacity(operations.len());
5869 let mut current_offset = 2 + (operations.len() * 2); // Start after offset table
5870
5871 for operation in operations {
5872 // Build individual service request
5873 let service_request = match operation {
5874 BatchOperation::Read { tag_name } => self.build_read_request(tag_name),
5875 BatchOperation::Write { tag_name, value } => {
5876 self.build_write_request(tag_name, value)?
5877 }
5878 };
5879
5880 service_requests.push(service_request);
5881 }
5882
5883 // Add offset table
5884 for service_request in &service_requests {
5885 packet.extend_from_slice(&(current_offset as u16).to_le_bytes());
5886 current_offset += service_request.len();
5887 }
5888
5889 // Add service requests
5890 for service_request in service_requests {
5891 packet.extend_from_slice(&service_request);
5892 }
5893
5894 tracing::trace!(
5895 "[BATCH] Built Multiple Service Packet ({} bytes, {} services)",
5896 packet.len(),
5897 operations.len()
5898 );
5899
5900 Ok(packet)
5901 }
5902
5903 /// Parses a Multiple Service Packet response
5904 fn parse_multiple_service_response(
5905 &self,
5906 response: &[u8],
5907 operations: &[BatchOperation],
5908 ) -> crate::error::Result<Vec<std::result::Result<Option<PlcValue>, BatchError>>> {
5909 if response.len() < 6 {
5910 return Err(crate::error::EtherNetIpError::Protocol(
5911 "Response too short for Multiple Service Packet".to_string(),
5912 ));
5913 }
5914
5915 let mut results = Vec::new();
5916
5917 tracing::trace!(
5918 "Raw Multiple Service Response ({} bytes): {:02X?}",
5919 response.len(),
5920 response
5921 );
5922
5923 // First, extract the CIP data from the EtherNet/IP response
5924 let cip_data = match self.extract_cip_from_response(response) {
5925 Ok(data) => data,
5926 Err(e) => {
5927 tracing::error!("Failed to extract CIP data: {}", e);
5928 return Err(e);
5929 }
5930 };
5931
5932 tracing::trace!(
5933 "Extracted CIP data ({} bytes): {:02X?}",
5934 cip_data.len(),
5935 cip_data
5936 );
5937
5938 if cip_data.len() < 6 {
5939 return Err(crate::error::EtherNetIpError::Protocol(
5940 "CIP data too short for Multiple Service Response".to_string(),
5941 ));
5942 }
5943
5944 // Parse Multiple Service Response header from CIP data:
5945 // [0] = Service Code (0x8A)
5946 // [1] = Reserved (0x00)
5947 // [2] = General Status (0x00 for success)
5948 // [3] = Additional Status Size (0x00)
5949 // [4-5] = Number of replies (little endian)
5950
5951 let service_code = cip_data[0];
5952 let general_status = cip_data[2];
5953 let num_replies = u16::from_le_bytes([cip_data[4], cip_data[5]]) as usize;
5954
5955 tracing::debug!(
5956 "Multiple Service Response: service=0x{:02X}, status=0x{:02X}, replies={}",
5957 service_code,
5958 general_status,
5959 num_replies
5960 );
5961
5962 if general_status != 0x00 {
5963 return Err(crate::error::EtherNetIpError::Protocol(format!(
5964 "Multiple Service Response error: 0x{general_status:02X}"
5965 )));
5966 }
5967
5968 if num_replies != operations.len() {
5969 return Err(crate::error::EtherNetIpError::Protocol(format!(
5970 "Reply count mismatch: expected {}, got {}",
5971 operations.len(),
5972 num_replies
5973 )));
5974 }
5975
5976 // Read reply offsets (each is 2 bytes, little endian)
5977 let mut reply_offsets = Vec::new();
5978 let mut offset = 6; // Skip header
5979
5980 for _i in 0..num_replies {
5981 if offset + 2 > cip_data.len() {
5982 return Err(crate::error::EtherNetIpError::Protocol(
5983 "CIP data too short for reply offsets".to_string(),
5984 ));
5985 }
5986 let reply_offset =
5987 u16::from_le_bytes([cip_data[offset], cip_data[offset + 1]]) as usize;
5988 reply_offsets.push(reply_offset);
5989 offset += 2;
5990 }
5991
5992 tracing::trace!("Reply offsets: {:?}", reply_offsets);
5993
5994 // The reply data starts after all the offsets
5995 let reply_base_offset = 6 + (num_replies * 2);
5996
5997 tracing::trace!("Reply base offset: {}", reply_base_offset);
5998
5999 // Parse each reply
6000 for (i, &reply_offset) in reply_offsets.iter().enumerate() {
6001 // Reply offset is relative to position 4 (after service code, reserved, status, additional status size)
6002 let reply_start = 4 + reply_offset;
6003
6004 if reply_start >= cip_data.len() {
6005 results.push(Err(BatchError::Other(
6006 "Reply offset beyond CIP data".to_string(),
6007 )));
6008 continue;
6009 }
6010
6011 // Calculate reply end position
6012 let reply_end = if i + 1 < reply_offsets.len() {
6013 // Not the last reply - use next reply's offset as boundary
6014 4 + reply_offsets[i + 1]
6015 } else {
6016 // Last reply - goes to end of CIP data
6017 cip_data.len()
6018 };
6019
6020 if reply_end > cip_data.len() || reply_start >= reply_end {
6021 results.push(Err(BatchError::Other(
6022 "Invalid reply boundaries".to_string(),
6023 )));
6024 continue;
6025 }
6026
6027 let reply_data = &cip_data[reply_start..reply_end];
6028
6029 tracing::trace!(
6030 "Reply {} at offset {}: start={}, end={}, len={}",
6031 i,
6032 reply_offset,
6033 reply_start,
6034 reply_end,
6035 reply_data.len()
6036 );
6037 tracing::trace!("Reply {} data: {:02X?}", i, reply_data);
6038
6039 let result = self.parse_individual_reply(reply_data, &operations[i]);
6040 results.push(result);
6041 }
6042
6043 Ok(results)
6044 }
6045
6046 /// Parses an individual service reply within a Multiple Service Packet response
6047 fn parse_individual_reply(
6048 &self,
6049 reply_data: &[u8],
6050 operation: &BatchOperation,
6051 ) -> std::result::Result<Option<PlcValue>, BatchError> {
6052 if reply_data.len() < 4 {
6053 return Err(BatchError::SerializationError(
6054 "Reply too short".to_string(),
6055 ));
6056 }
6057
6058 tracing::trace!(
6059 "Parsing individual reply ({} bytes): {:02X?}",
6060 reply_data.len(),
6061 reply_data
6062 );
6063
6064 // Each individual reply in Multiple Service Response has the same format as standalone CIP response:
6065 // [0] = Service Code (0xCC for read response, 0xCD for write response)
6066 // [1] = Reserved (0x00)
6067 // [2] = General Status (0x00 for success)
6068 // [3] = Additional Status Size (0x00)
6069 // [4..] = Response data (for reads) or empty (for writes)
6070
6071 let service_code = reply_data[0];
6072 let general_status = reply_data[2];
6073
6074 tracing::trace!(
6075 "Service code: 0x{:02X}, Status: 0x{:02X}",
6076 service_code,
6077 general_status
6078 );
6079
6080 if general_status != 0x00 {
6081 let error_msg = self.get_cip_error_message(general_status);
6082 return Err(BatchError::CipError {
6083 status: general_status,
6084 message: error_msg,
6085 });
6086 }
6087
6088 match operation {
6089 BatchOperation::Write { .. } => {
6090 // Write operations return no data on success
6091 Ok(None)
6092 }
6093 BatchOperation::Read { .. } => {
6094 // Read operations return data starting at offset 4
6095 if reply_data.len() < 6 {
6096 return Err(BatchError::SerializationError(
6097 "Read reply too short for data".to_string(),
6098 ));
6099 }
6100
6101 // Parse the data directly (skip the 4-byte header)
6102 // Data format: [type_low, type_high, value_bytes...]
6103 let data = &reply_data[4..];
6104 tracing::trace!("Parsing data ({} bytes): {:02X?}", data.len(), data);
6105
6106 if data.len() < 2 {
6107 return Err(BatchError::SerializationError(
6108 "Data too short for type".to_string(),
6109 ));
6110 }
6111
6112 let data_type = u16::from_le_bytes([data[0], data[1]]);
6113 let value_data = &data[2..];
6114
6115 tracing::trace!(
6116 "Data type: 0x{:04X}, Value data ({} bytes): {:02X?}",
6117 data_type,
6118 value_data.len(),
6119 value_data
6120 );
6121
6122 // Parse based on data type
6123 match data_type {
6124 0x00C1 => {
6125 // BOOL
6126 if value_data.is_empty() {
6127 return Err(BatchError::SerializationError(
6128 "Missing BOOL value".to_string(),
6129 ));
6130 }
6131 Ok(Some(PlcValue::Bool(value_data[0] != 0)))
6132 }
6133 0x00C2 => {
6134 // SINT
6135 if value_data.is_empty() {
6136 return Err(BatchError::SerializationError(
6137 "Missing SINT value".to_string(),
6138 ));
6139 }
6140 Ok(Some(PlcValue::Sint(value_data[0] as i8)))
6141 }
6142 0x00C3 => {
6143 // INT
6144 if value_data.len() < 2 {
6145 return Err(BatchError::SerializationError(
6146 "Missing INT value".to_string(),
6147 ));
6148 }
6149 let value = i16::from_le_bytes([value_data[0], value_data[1]]);
6150 Ok(Some(PlcValue::Int(value)))
6151 }
6152 0x00C4 => {
6153 // DINT
6154 if value_data.len() < 4 {
6155 return Err(BatchError::SerializationError(
6156 "Missing DINT value".to_string(),
6157 ));
6158 }
6159 let value = i32::from_le_bytes([
6160 value_data[0],
6161 value_data[1],
6162 value_data[2],
6163 value_data[3],
6164 ]);
6165 tracing::trace!("Parsed DINT: {}", value);
6166 Ok(Some(PlcValue::Dint(value)))
6167 }
6168 0x00C5 => {
6169 // LINT
6170 if value_data.len() < 8 {
6171 return Err(BatchError::SerializationError(
6172 "Missing LINT value".to_string(),
6173 ));
6174 }
6175 let value = i64::from_le_bytes([
6176 value_data[0],
6177 value_data[1],
6178 value_data[2],
6179 value_data[3],
6180 value_data[4],
6181 value_data[5],
6182 value_data[6],
6183 value_data[7],
6184 ]);
6185 Ok(Some(PlcValue::Lint(value)))
6186 }
6187 0x00C6 => {
6188 // USINT
6189 if value_data.is_empty() {
6190 return Err(BatchError::SerializationError(
6191 "Missing USINT value".to_string(),
6192 ));
6193 }
6194 Ok(Some(PlcValue::Usint(value_data[0])))
6195 }
6196 0x00C7 => {
6197 // UINT
6198 if value_data.len() < 2 {
6199 return Err(BatchError::SerializationError(
6200 "Missing UINT value".to_string(),
6201 ));
6202 }
6203 let value = u16::from_le_bytes([value_data[0], value_data[1]]);
6204 Ok(Some(PlcValue::Uint(value)))
6205 }
6206 0x00C8 => {
6207 // UDINT
6208 if value_data.len() < 4 {
6209 return Err(BatchError::SerializationError(
6210 "Missing UDINT value".to_string(),
6211 ));
6212 }
6213 let value = u32::from_le_bytes([
6214 value_data[0],
6215 value_data[1],
6216 value_data[2],
6217 value_data[3],
6218 ]);
6219 Ok(Some(PlcValue::Udint(value)))
6220 }
6221 0x00C9 => {
6222 // ULINT
6223 if value_data.len() < 8 {
6224 return Err(BatchError::SerializationError(
6225 "Missing ULINT value".to_string(),
6226 ));
6227 }
6228 let value = u64::from_le_bytes([
6229 value_data[0],
6230 value_data[1],
6231 value_data[2],
6232 value_data[3],
6233 value_data[4],
6234 value_data[5],
6235 value_data[6],
6236 value_data[7],
6237 ]);
6238 Ok(Some(PlcValue::Ulint(value)))
6239 }
6240 0x00CA => {
6241 // REAL
6242 if value_data.len() < 4 {
6243 return Err(BatchError::SerializationError(
6244 "Missing REAL value".to_string(),
6245 ));
6246 }
6247 let bytes = [value_data[0], value_data[1], value_data[2], value_data[3]];
6248 let value = f32::from_le_bytes(bytes);
6249 tracing::trace!("Parsed REAL: {}", value);
6250 Ok(Some(PlcValue::Real(value)))
6251 }
6252 0x00CB => {
6253 // LREAL
6254 if value_data.len() < 8 {
6255 return Err(BatchError::SerializationError(
6256 "Missing LREAL value".to_string(),
6257 ));
6258 }
6259 let bytes = [
6260 value_data[0],
6261 value_data[1],
6262 value_data[2],
6263 value_data[3],
6264 value_data[4],
6265 value_data[5],
6266 value_data[6],
6267 value_data[7],
6268 ];
6269 let value = f64::from_le_bytes(bytes);
6270 Ok(Some(PlcValue::Lreal(value)))
6271 }
6272 0x00DA => {
6273 // STRING
6274 if value_data.is_empty() {
6275 return Ok(Some(PlcValue::String(String::new())));
6276 }
6277 let length = value_data[0] as usize;
6278 if value_data.len() < 1 + length {
6279 return Err(BatchError::SerializationError(
6280 "Insufficient data for STRING value".to_string(),
6281 ));
6282 }
6283 let string_data = &value_data[1..1 + length];
6284 let value = String::from_utf8_lossy(string_data).to_string();
6285 tracing::trace!("Parsed STRING: '{}'", value);
6286 Ok(Some(PlcValue::String(value)))
6287 }
6288 0x02A0 => {
6289 // Allen-Bradley UDT type (0x02A0) for batch operations
6290 // Note: symbol_id not available in batch read context
6291 tracing::trace!(
6292 "Detected UDT structure (0x02A0) with {} bytes",
6293 value_data.len()
6294 );
6295 Ok(Some(PlcValue::Udt(UdtData {
6296 symbol_id: 0, // Not available in batch context
6297 data: value_data.to_vec(),
6298 })))
6299 }
6300 _ => Err(BatchError::SerializationError(format!(
6301 "Unsupported data type: 0x{data_type:04X}"
6302 ))),
6303 }
6304 }
6305 }
6306 }
6307
6308 /// Writes a string value using Allen-Bradley UDT component access
6309 /// This writes to TestString.LEN and TestString.DATA separately
6310 pub async fn write_ab_string_components(
6311 &mut self,
6312 tag_name: &str,
6313 value: &str,
6314 ) -> crate::error::Result<()> {
6315 tracing::debug!(
6316 "[AB STRING] Writing string '{}' to tag '{}' using component access",
6317 value,
6318 tag_name
6319 );
6320
6321 let string_bytes = value.as_bytes();
6322 let string_len = string_bytes.len() as i32;
6323
6324 // Step 1: Write the length to TestString.LEN
6325 let len_tag = format!("{tag_name}.LEN");
6326 tracing::debug!("Step 1: Writing length {} to {}", string_len, len_tag);
6327
6328 match self.write_tag(&len_tag, PlcValue::Dint(string_len)).await {
6329 Ok(_) => tracing::debug!("Length written successfully"),
6330 Err(e) => {
6331 tracing::error!("Length write failed: {}", e);
6332 return Err(e);
6333 }
6334 }
6335
6336 // Step 2: Write the string data to TestString.DATA using array access
6337 tracing::debug!("Step 2: Writing string data to {}.DATA", tag_name);
6338
6339 // We need to write each character individually to the DATA array
6340 for (i, &byte) in string_bytes.iter().enumerate() {
6341 let data_element = format!("{tag_name}.DATA[{i}]");
6342 match self
6343 .write_tag(&data_element, PlcValue::Sint(byte as i8))
6344 .await
6345 {
6346 Ok(_) => print!("."),
6347 Err(e) => {
6348 tracing::error!("Failed to write byte {} to position {}: {}", byte, i, e);
6349 return Err(e);
6350 }
6351 }
6352 }
6353
6354 // Step 3: Clear remaining bytes (null terminate)
6355 if string_bytes.len() < 82 {
6356 let null_element = format!("{}.DATA[{}]", tag_name, string_bytes.len());
6357 match self.write_tag(&null_element, PlcValue::Sint(0)).await {
6358 Ok(_) => tracing::debug!("String null-terminated successfully"),
6359 Err(e) => tracing::warn!("Could not null-terminate: {}", e),
6360 }
6361 }
6362
6363 tracing::info!("AB STRING component write completed!");
6364 Ok(())
6365 }
6366
6367 /// Writes a string using a single UDT write with proper AB STRING format
6368 pub async fn write_ab_string_udt(
6369 &mut self,
6370 tag_name: &str,
6371 value: &str,
6372 ) -> crate::error::Result<()> {
6373 tracing::debug!(
6374 "[AB STRING UDT] Writing string '{}' to tag '{}' as UDT",
6375 value,
6376 tag_name
6377 );
6378
6379 let string_bytes = value.as_bytes();
6380 if string_bytes.len() > 82 {
6381 return Err(EtherNetIpError::Protocol(
6382 "String too long for Allen-Bradley STRING (max 82 chars)".to_string(),
6383 ));
6384 }
6385
6386 // Build a CIP request that writes the complete AB STRING structure
6387 let mut cip_request = Vec::new();
6388
6389 // Service: Write Tag Service (0x4D)
6390 cip_request.push(0x4D);
6391
6392 // Request Path
6393 let tag_path = self.build_tag_path(tag_name);
6394 cip_request.push((tag_path.len() / 2) as u8); // Path size in words
6395 cip_request.extend_from_slice(&tag_path);
6396
6397 // Data Type: Allen-Bradley STRING (0x02A0) - but write as UDT components
6398 cip_request.extend_from_slice(&[0xA0, 0x00]); // UDT type
6399 cip_request.extend_from_slice(&[0x01, 0x00]); // Element count
6400
6401 // AB STRING UDT structure:
6402 // - DINT .LEN (4 bytes)
6403 // - SINT .DATA[82] (82 bytes)
6404
6405 // Write .LEN field (current string length)
6406 let len = string_bytes.len() as u32;
6407 cip_request.extend_from_slice(&len.to_le_bytes());
6408
6409 // Write .DATA field (82 bytes total)
6410 cip_request.extend_from_slice(string_bytes); // Actual string data
6411
6412 // Pad with zeros to reach 82 bytes
6413 let padding_needed = 82 - string_bytes.len();
6414 cip_request.extend_from_slice(&vec![0u8; padding_needed]);
6415
6416 tracing::trace!("Built UDT write request: {} bytes total", cip_request.len());
6417
6418 let response = self.send_cip_request(&cip_request).await?;
6419
6420 if response.len() >= 3 {
6421 let general_status = response[2];
6422 if general_status == 0x00 {
6423 tracing::info!("AB STRING UDT write successful!");
6424 Ok(())
6425 } else {
6426 let error_msg = self.get_cip_error_message(general_status);
6427 Err(EtherNetIpError::Protocol(format!(
6428 "AB STRING UDT write failed - CIP Error 0x{general_status:02X}: {error_msg}"
6429 )))
6430 }
6431 } else {
6432 Err(EtherNetIpError::Protocol(
6433 "Invalid AB STRING UDT write response".to_string(),
6434 ))
6435 }
6436 }
6437
6438 /// Establishes a Class 3 connected session for STRING operations
6439 ///
6440 /// Connected sessions are required for certain operations like STRING writes
6441 /// in Allen-Bradley PLCs. This implements the Forward Open CIP service.
6442 /// Will try multiple connection parameter configurations until one succeeds.
6443 async fn establish_connected_session(
6444 &mut self,
6445 session_name: &str,
6446 ) -> crate::error::Result<ConnectedSession> {
6447 tracing::debug!(
6448 "[CONNECTED] Establishing connected session: '{}'",
6449 session_name
6450 );
6451 tracing::debug!("[CONNECTED] Will try multiple parameter configurations...");
6452
6453 // Generate unique connection parameters
6454 *self.connection_sequence.lock().await += 1;
6455 let connection_serial = (*self.connection_sequence.lock().await & 0xFFFF) as u16;
6456
6457 // Try different configurations until one works
6458 for config_id in 0..=5 {
6459 tracing::debug!(
6460 "[ATTEMPT {}] Trying configuration {}:",
6461 config_id + 1,
6462 config_id
6463 );
6464
6465 let mut session = if config_id == 0 {
6466 ConnectedSession::new(connection_serial)
6467 } else {
6468 ConnectedSession::with_config(connection_serial, config_id)
6469 };
6470
6471 // Generate unique connection IDs for this attempt
6472 session.o_to_t_connection_id =
6473 0x2000_0000 + *self.connection_sequence.lock().await + (config_id as u32 * 0x1000);
6474 session.t_to_o_connection_id =
6475 0x3000_0000 + *self.connection_sequence.lock().await + (config_id as u32 * 0x1000);
6476
6477 // Build Forward Open request with this configuration
6478 let forward_open_request = self.build_forward_open_request(&session)?;
6479
6480 tracing::debug!(
6481 "[ATTEMPT {}] Sending Forward Open request ({} bytes)",
6482 config_id + 1,
6483 forward_open_request.len()
6484 );
6485
6486 // Send Forward Open request
6487 match self.send_cip_request(&forward_open_request).await {
6488 Ok(response) => {
6489 // Try to parse the response - DON'T clone, modify the session directly!
6490 match self.parse_forward_open_response(&mut session, &response) {
6491 Ok(()) => {
6492 // Success! Store the session and return
6493 tracing::info!("[SUCCESS] Configuration {} worked!", config_id);
6494 tracing::debug!("Connection ID: 0x{:08X}", session.connection_id);
6495 tracing::debug!("O->T ID: 0x{:08X}", session.o_to_t_connection_id);
6496 tracing::debug!("T->O ID: 0x{:08X}", session.t_to_o_connection_id);
6497 tracing::debug!(
6498 "Using Connection ID: 0x{:08X} for messaging",
6499 session.connection_id
6500 );
6501
6502 session.is_active = true;
6503 let mut sessions = self.connected_sessions.lock().await;
6504 sessions.insert(session_name.to_string(), session.clone());
6505 return Ok(session);
6506 }
6507 Err(e) => {
6508 tracing::warn!(
6509 "[ATTEMPT {}] Configuration {} failed: {}",
6510 config_id + 1,
6511 config_id,
6512 e
6513 );
6514
6515 // If it's a specific status error, log it
6516 if e.to_string().contains("status: 0x") {
6517 tracing::debug!("Status indicates: parameter incompatibility or resource conflict");
6518 }
6519 }
6520 }
6521 }
6522 Err(e) => {
6523 tracing::warn!(
6524 "[ATTEMPT {}] Network error with config {}: {}",
6525 config_id + 1,
6526 config_id,
6527 e
6528 );
6529 }
6530 }
6531
6532 // Small delay between attempts
6533 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
6534 }
6535
6536 // If we get here, all configurations failed
6537 Err(EtherNetIpError::Protocol(
6538 "All connection parameter configurations failed. PLC may not support connected messaging or has reached connection limits.".to_string()
6539 ))
6540 }
6541
6542 /// Builds a Forward Open CIP request for establishing connected sessions
6543 fn build_forward_open_request(
6544 &self,
6545 session: &ConnectedSession,
6546 ) -> crate::error::Result<Vec<u8>> {
6547 let mut request = Vec::with_capacity(50);
6548
6549 // CIP Forward Open Service (0x54)
6550 request.push(0x54);
6551
6552 // Request path length (Connection Manager object)
6553 request.push(0x02); // 2 words
6554
6555 // Class ID: Connection Manager (0x06)
6556 request.push(0x20); // Logical Class segment
6557 request.push(0x06);
6558
6559 // Instance ID: Connection Manager instance (0x01)
6560 request.push(0x24); // Logical Instance segment
6561 request.push(0x01);
6562
6563 // Forward Open parameters
6564
6565 // Connection Timeout Ticks (1 byte) + Timeout multiplier (1 byte)
6566 request.push(0x0A); // Timeout ticks (10)
6567 request.push(session.timeout_multiplier);
6568
6569 // Originator -> Target Connection ID (4 bytes, little-endian)
6570 request.extend_from_slice(&session.o_to_t_connection_id.to_le_bytes());
6571
6572 // Target -> Originator Connection ID (4 bytes, little-endian)
6573 request.extend_from_slice(&session.t_to_o_connection_id.to_le_bytes());
6574
6575 // Connection Serial Number (2 bytes, little-endian)
6576 request.extend_from_slice(&session.connection_serial.to_le_bytes());
6577
6578 // Originator Vendor ID (2 bytes, little-endian)
6579 request.extend_from_slice(&session.originator_vendor_id.to_le_bytes());
6580
6581 // Originator Serial Number (4 bytes, little-endian)
6582 request.extend_from_slice(&session.originator_serial.to_le_bytes());
6583
6584 // Connection Timeout Multiplier (1 byte) - repeated for target
6585 request.push(session.timeout_multiplier);
6586
6587 // Reserved bytes (3 bytes)
6588 request.extend_from_slice(&[0x00, 0x00, 0x00]);
6589
6590 // Originator -> Target RPI (4 bytes, little-endian, microseconds)
6591 request.extend_from_slice(&session.rpi.to_le_bytes());
6592
6593 // Originator -> Target connection parameters (4 bytes)
6594 let o_to_t_params = self.encode_connection_parameters(&session.o_to_t_params);
6595 request.extend_from_slice(&o_to_t_params.to_le_bytes());
6596
6597 // Target -> Originator RPI (4 bytes, little-endian, microseconds)
6598 request.extend_from_slice(&session.rpi.to_le_bytes());
6599
6600 // Target -> Originator connection parameters (4 bytes)
6601 let t_to_o_params = self.encode_connection_parameters(&session.t_to_o_params);
6602 request.extend_from_slice(&t_to_o_params.to_le_bytes());
6603
6604 // Transport type/trigger (1 byte) - Class 3, Application triggered
6605 request.push(0xA3);
6606
6607 // Connection Path Size (1 byte)
6608 request.push(0x02); // 2 words for Message Router path
6609
6610 // Connection Path - Target the Message Router
6611 request.push(0x20); // Logical Class segment
6612 request.push(0x02); // Message Router class (0x02)
6613 request.push(0x24); // Logical Instance segment
6614 request.push(0x01); // Message Router instance (0x01)
6615
6616 Ok(request)
6617 }
6618
6619 /// Encodes connection parameters into a 32-bit value
6620 fn encode_connection_parameters(&self, params: &ConnectionParameters) -> u32 {
6621 let mut encoded = 0u32;
6622
6623 // Connection size (bits 0-15)
6624 encoded |= params.size as u32;
6625
6626 // Variable flag (bit 25)
6627 if params.variable_size {
6628 encoded |= 1 << 25;
6629 }
6630
6631 // Connection type (bits 29-30)
6632 encoded |= (params.connection_type as u32) << 29;
6633
6634 // Priority (bits 26-27)
6635 encoded |= (params.priority as u32) << 26;
6636
6637 encoded
6638 }
6639
6640 /// Parses Forward Open response and updates session with connection info
6641 fn parse_forward_open_response(
6642 &self,
6643 session: &mut ConnectedSession,
6644 response: &[u8],
6645 ) -> crate::error::Result<()> {
6646 if response.len() < 2 {
6647 return Err(EtherNetIpError::Protocol(
6648 "Forward Open response too short".to_string(),
6649 ));
6650 }
6651
6652 let service = response[0];
6653 let status = response[1];
6654
6655 // Check if this is a Forward Open Reply (0xD4)
6656 if service != 0xD4 {
6657 return Err(EtherNetIpError::Protocol(format!(
6658 "Unexpected service in Forward Open response: 0x{service:02X}"
6659 )));
6660 }
6661
6662 // Check status
6663 if status != 0x00 {
6664 let error_msg = match status {
6665 0x01 => "Connection failure - Resource unavailable or already exists",
6666 0x02 => "Invalid parameter - Connection parameters rejected",
6667 0x03 => "Connection timeout - PLC did not respond in time",
6668 0x04 => "Connection limit exceeded - Too many connections",
6669 0x08 => "Invalid service - Forward Open not supported",
6670 0x0C => "Invalid attribute - Connection parameters invalid",
6671 0x13 => "Path destination unknown - Target object not found",
6672 0x26 => "Invalid parameter value - RPI or size out of range",
6673 _ => &format!("Unknown status: 0x{status:02X}"),
6674 };
6675 return Err(EtherNetIpError::Protocol(format!(
6676 "Forward Open failed with status 0x{status:02X}: {error_msg}"
6677 )));
6678 }
6679
6680 // Parse successful response
6681 if response.len() < 16 {
6682 return Err(EtherNetIpError::Protocol(
6683 "Forward Open response data too short".to_string(),
6684 ));
6685 }
6686
6687 // CRITICAL FIX: The Forward Open response contains the actual connection IDs assigned by the PLC
6688 // Use the IDs returned by the PLC, not our requested ones
6689 let actual_o_to_t_id =
6690 u32::from_le_bytes([response[2], response[3], response[4], response[5]]);
6691 let actual_t_to_o_id =
6692 u32::from_le_bytes([response[6], response[7], response[8], response[9]]);
6693
6694 // Update session with the actual assigned connection IDs
6695 session.o_to_t_connection_id = actual_o_to_t_id;
6696 session.t_to_o_connection_id = actual_t_to_o_id;
6697 session.connection_id = actual_o_to_t_id; // Use O->T as the primary connection ID
6698
6699 tracing::info!("[FORWARD OPEN] Success!");
6700 tracing::debug!(
6701 "O->T Connection ID: 0x{:08X} (PLC assigned)",
6702 session.o_to_t_connection_id
6703 );
6704 tracing::debug!(
6705 "T->O Connection ID: 0x{:08X} (PLC assigned)",
6706 session.t_to_o_connection_id
6707 );
6708 tracing::debug!(
6709 "Using Connection ID: 0x{:08X} for messaging",
6710 session.connection_id
6711 );
6712
6713 Ok(())
6714 }
6715
6716 /// Writes a string using connected explicit messaging
6717 pub async fn write_string_connected(
6718 &mut self,
6719 tag_name: &str,
6720 value: &str,
6721 ) -> crate::error::Result<()> {
6722 let session_name = format!("string_write_{tag_name}");
6723 let mut sessions = self.connected_sessions.lock().await;
6724
6725 if !sessions.contains_key(&session_name) {
6726 drop(sessions); // Release the lock before calling establish_connected_session
6727 self.establish_connected_session(&session_name).await?;
6728 sessions = self.connected_sessions.lock().await;
6729 }
6730
6731 let session = sessions.get(&session_name).unwrap().clone();
6732 let request = self.build_connected_string_write_request(tag_name, value, &session)?;
6733
6734 drop(sessions); // Release the lock before sending the request
6735 let response = self
6736 .send_connected_cip_request(&request, &session, &session_name)
6737 .await?;
6738
6739 // Check if write was successful
6740 if response.len() >= 2 {
6741 let status = response[1];
6742 if status == 0x00 {
6743 Ok(())
6744 } else {
6745 let error_msg = self.get_cip_error_message(status);
6746 Err(EtherNetIpError::Protocol(format!(
6747 "CIP Error 0x{status:02X}: {error_msg}"
6748 )))
6749 }
6750 } else {
6751 Err(EtherNetIpError::Protocol(
6752 "Invalid connected string write response".to_string(),
6753 ))
6754 }
6755 }
6756
6757 /// Builds a string write request for connected messaging
6758 fn build_connected_string_write_request(
6759 &self,
6760 tag_name: &str,
6761 value: &str,
6762 _session: &ConnectedSession,
6763 ) -> crate::error::Result<Vec<u8>> {
6764 let mut request = Vec::new();
6765
6766 // For connected messaging, use direct CIP Write service
6767 // The connection is already established, so we can send the request directly
6768
6769 // CIP Write Service Code
6770 request.push(0x4D);
6771
6772 // Tag path - use simple ANSI format for connected messaging
6773 let tag_bytes = tag_name.as_bytes();
6774 let path_size_words = (2 + tag_bytes.len() + 1) / 2; // +1 for potential padding, /2 for word count
6775 request.push(path_size_words as u8);
6776
6777 request.push(0x91); // ANSI symbol segment
6778 request.push(tag_bytes.len() as u8); // Length of tag name
6779 request.extend_from_slice(tag_bytes);
6780
6781 // Add padding byte if needed to make path even length
6782 if (2 + tag_bytes.len()) % 2 != 0 {
6783 request.push(0x00);
6784 }
6785
6786 // Data type for AB STRING
6787 request.extend_from_slice(&[0xCE, 0x0F]); // AB STRING data type (4046)
6788
6789 // Number of elements (always 1 for a single string)
6790 request.extend_from_slice(&[0x01, 0x00]);
6791
6792 // Build the AB STRING structure payload
6793 let string_bytes = value.as_bytes();
6794 let max_len: u16 = 82; // Standard AB STRING max length
6795 let current_len = string_bytes.len().min(max_len as usize) as u16;
6796
6797 // STRING structure:
6798 // - Len (2 bytes) - number of characters used
6799 request.extend_from_slice(¤t_len.to_le_bytes());
6800
6801 // - MaxLen (2 bytes) - maximum characters allowed (typically 82)
6802 request.extend_from_slice(&max_len.to_le_bytes());
6803
6804 // - Data[MaxLen] (82 bytes) - the character array, zero-padded
6805 let mut data_array = vec![0u8; max_len as usize];
6806 data_array[..current_len as usize].copy_from_slice(&string_bytes[..current_len as usize]);
6807 request.extend_from_slice(&data_array);
6808
6809 tracing::trace!(
6810 "Built connected string write request ({} bytes) for '{}' = '{}' (len={}, maxlen={})",
6811 request.len(),
6812 tag_name,
6813 value,
6814 current_len,
6815 max_len
6816 );
6817 tracing::trace!("Request: {:02X?}", request);
6818
6819 Ok(request)
6820 }
6821
6822 /// Sends a CIP request using connected messaging
6823 async fn send_connected_cip_request(
6824 &mut self,
6825 cip_request: &[u8],
6826 session: &ConnectedSession,
6827 session_name: &str,
6828 ) -> crate::error::Result<Vec<u8>> {
6829 tracing::debug!(
6830 "[CONNECTED] Sending connected CIP request ({} bytes) using T->O connection ID 0x{:08X}",
6831 cip_request.len(), session.t_to_o_connection_id
6832 );
6833
6834 // Build EtherNet/IP header for connected data (Send RR Data)
6835 let mut packet = Vec::new();
6836
6837 // EtherNet/IP Header
6838 packet.extend_from_slice(&[0x6F, 0x00]); // Command: Send RR Data (0x006F) - correct for connected messaging
6839 packet.extend_from_slice(&[0x00, 0x00]); // Length (fill in later)
6840 packet.extend_from_slice(&self.session_handle.to_le_bytes()); // Session handle
6841 packet.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); // Status
6842 packet.extend_from_slice(&[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]); // Context
6843 packet.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); // Options
6844
6845 // CPF (Common Packet Format) data starts here
6846 let cpf_start = packet.len();
6847
6848 // Interface handle (4 bytes)
6849 packet.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]);
6850
6851 // Timeout (2 bytes) - 5 seconds
6852 packet.extend_from_slice(&[0x05, 0x00]);
6853
6854 // Item count (2 bytes) - 2 items: Address + Data
6855 packet.extend_from_slice(&[0x02, 0x00]);
6856
6857 // Item 1: Connected Address Item (specifies which connection to use)
6858 packet.extend_from_slice(&[0xA1, 0x00]); // Type: Connected Address Item (0x00A1)
6859 packet.extend_from_slice(&[0x04, 0x00]); // Length: 4 bytes
6860 // Use T->O connection ID (Target to Originator) for addressing
6861 packet.extend_from_slice(&session.t_to_o_connection_id.to_le_bytes());
6862
6863 // Item 2: Connected Data Item (contains the CIP request + sequence)
6864 packet.extend_from_slice(&[0xB1, 0x00]); // Type: Connected Data Item (0x00B1)
6865 let data_length = cip_request.len() + 2; // +2 for sequence count
6866 packet.extend_from_slice(&(data_length as u16).to_le_bytes()); // Length
6867
6868 // Clone session_name and session before acquiring the lock
6869 let session_name_clone = session_name.to_string();
6870 let _session_clone = session.clone();
6871
6872 // Get the current session mutably to increment sequence counter
6873 let mut sessions = self.connected_sessions.lock().await;
6874 let current_sequence = if let Some(session_mut) = sessions.get_mut(&session_name_clone) {
6875 session_mut.sequence_count += 1;
6876 session_mut.sequence_count
6877 } else {
6878 1 // Fallback if session not found
6879 };
6880
6881 // Drop the lock before sending the request
6882 drop(sessions);
6883
6884 // Sequence count (2 bytes) - incremental counter for this connection
6885 packet.extend_from_slice(¤t_sequence.to_le_bytes());
6886
6887 // CIP request data
6888 packet.extend_from_slice(cip_request);
6889
6890 // Update packet length in header (total CPF data size)
6891 let cpf_length = packet.len() - cpf_start;
6892 packet[2..4].copy_from_slice(&(cpf_length as u16).to_le_bytes());
6893
6894 tracing::trace!(
6895 "[CONNECTED] Sending packet ({} bytes) with sequence {}",
6896 packet.len(),
6897 current_sequence
6898 );
6899
6900 // Send packet
6901 let mut stream = self.stream.lock().await;
6902 stream
6903 .write_all(&packet)
6904 .await
6905 .map_err(EtherNetIpError::Io)?;
6906
6907 // Read response header
6908 let mut header = [0u8; 24];
6909 stream
6910 .read_exact(&mut header)
6911 .await
6912 .map_err(EtherNetIpError::Io)?;
6913
6914 // Check EtherNet/IP command status
6915 let cmd_status = u32::from_le_bytes([header[8], header[9], header[10], header[11]]);
6916 if cmd_status != 0 {
6917 return Err(EtherNetIpError::Protocol(format!(
6918 "Connected message failed with status: 0x{cmd_status:08X}"
6919 )));
6920 }
6921
6922 // Read response data
6923 let response_length = u16::from_le_bytes([header[2], header[3]]) as usize;
6924 let mut response_data = vec![0u8; response_length];
6925 stream
6926 .read_exact(&mut response_data)
6927 .await
6928 .map_err(EtherNetIpError::Io)?;
6929
6930 let mut last_activity = self.last_activity.lock().await;
6931 *last_activity = Instant::now();
6932
6933 tracing::trace!(
6934 "[CONNECTED] Received response ({} bytes)",
6935 response_data.len()
6936 );
6937
6938 // Extract connected CIP response
6939 self.extract_connected_cip_from_response(&response_data)
6940 }
6941
6942 /// Extracts CIP data from connected response
6943 fn extract_connected_cip_from_response(
6944 &self,
6945 response: &[u8],
6946 ) -> crate::error::Result<Vec<u8>> {
6947 tracing::trace!(
6948 "[CONNECTED] Extracting CIP from connected response ({} bytes): {:02X?}",
6949 response.len(),
6950 response
6951 );
6952
6953 if response.len() < 12 {
6954 return Err(EtherNetIpError::Protocol(
6955 "Connected response too short for CPF header".to_string(),
6956 ));
6957 }
6958
6959 // Parse CPF (Common Packet Format) structure
6960 // [0-3]: Interface handle
6961 // [4-5]: Timeout
6962 // [6-7]: Item count
6963 let item_count = u16::from_le_bytes([response[6], response[7]]) as usize;
6964 tracing::trace!("[CONNECTED] CPF item count: {}", item_count);
6965
6966 let mut pos = 8; // Start after CPF header
6967
6968 // Look for Connected Data Item (0x00B1)
6969 for _i in 0..item_count {
6970 if pos + 4 > response.len() {
6971 return Err(EtherNetIpError::Protocol(
6972 "Response truncated while parsing items".to_string(),
6973 ));
6974 }
6975
6976 let item_type = u16::from_le_bytes([response[pos], response[pos + 1]]);
6977 let item_length = u16::from_le_bytes([response[pos + 2], response[pos + 3]]) as usize;
6978 pos += 4; // Skip item header
6979
6980 tracing::trace!(
6981 "[CONNECTED] Found item: type=0x{:04X}, length={}",
6982 item_type,
6983 item_length
6984 );
6985
6986 if pos
6987 .checked_add(item_length)
6988 .map_or(true, |end| end > response.len())
6989 {
6990 return Err(EtherNetIpError::Protocol(
6991 "Connected data item truncated".to_string(),
6992 ));
6993 }
6994
6995 if item_type == 0x00B1 {
6996 // Connected Data Item
6997 // Connected Data Item contains [sequence_count(2)][cip_data]
6998 if item_length < 2 {
6999 return Err(EtherNetIpError::Protocol(
7000 "Connected data item too short for sequence".to_string(),
7001 ));
7002 }
7003
7004 let sequence_count = u16::from_le_bytes([response[pos], response[pos + 1]]);
7005 tracing::trace!("[CONNECTED] Sequence count: {}", sequence_count);
7006
7007 // Extract CIP data (skip 2-byte sequence count)
7008 let cip_data = response[pos + 2..pos + item_length].to_vec();
7009 tracing::trace!(
7010 "[CONNECTED] Extracted CIP data ({} bytes): {:02X?}",
7011 cip_data.len(),
7012 cip_data
7013 );
7014
7015 return Ok(cip_data);
7016 } else {
7017 // Skip this item's data
7018 pos += item_length;
7019 }
7020 }
7021
7022 Err(EtherNetIpError::Protocol(
7023 "Connected Data Item (0x00B1) not found in response".to_string(),
7024 ))
7025 }
7026
7027 /// Closes a specific connected session
7028 async fn close_connected_session(&mut self, session_name: &str) -> crate::error::Result<()> {
7029 if let Some(session) = self.connected_sessions.lock().await.get(session_name) {
7030 let session = session.clone(); // Clone to avoid borrowing issues
7031
7032 // Build Forward Close request
7033 let forward_close_request = self.build_forward_close_request(&session)?;
7034
7035 // Send Forward Close request
7036 let _response = self.send_cip_request(&forward_close_request).await?;
7037
7038 tracing::info!("[CONNECTED] Session '{}' closed successfully", session_name);
7039 }
7040
7041 // Remove session from our tracking
7042 let mut sessions = self.connected_sessions.lock().await;
7043 sessions.remove(session_name);
7044
7045 Ok(())
7046 }
7047
7048 /// Builds a Forward Close CIP request for terminating connected sessions
7049 fn build_forward_close_request(
7050 &self,
7051 session: &ConnectedSession,
7052 ) -> crate::error::Result<Vec<u8>> {
7053 let mut request = Vec::with_capacity(21);
7054
7055 // CIP Forward Close Service (0x4E)
7056 request.push(0x4E);
7057
7058 // Request path length (Connection Manager object)
7059 request.push(0x02); // 2 words
7060
7061 // Class ID: Connection Manager (0x06)
7062 request.push(0x20); // Logical Class segment
7063 request.push(0x06);
7064
7065 // Instance ID: Connection Manager instance (0x01)
7066 request.push(0x24); // Logical Instance segment
7067 request.push(0x01);
7068
7069 // Forward Close parameters
7070
7071 // Connection Timeout Ticks (1 byte) + Timeout multiplier (1 byte)
7072 request.push(0x0A); // Timeout ticks (10)
7073 request.push(session.timeout_multiplier);
7074
7075 // Connection Serial Number (2 bytes, little-endian)
7076 request.extend_from_slice(&session.connection_serial.to_le_bytes());
7077
7078 // Originator Vendor ID (2 bytes, little-endian)
7079 request.extend_from_slice(&session.originator_vendor_id.to_le_bytes());
7080
7081 // Originator Serial Number (4 bytes, little-endian)
7082 request.extend_from_slice(&session.originator_serial.to_le_bytes());
7083
7084 // Connection Path Size (1 byte)
7085 request.push(0x02); // 2 words for Message Router path
7086
7087 // Connection Path - Target the Message Router
7088 request.push(0x20); // Logical Class segment
7089 request.push(0x02); // Message Router class (0x02)
7090 request.push(0x24); // Logical Instance segment
7091 request.push(0x01); // Message Router instance (0x01)
7092
7093 Ok(request)
7094 }
7095
7096 /// Closes all connected sessions (called during disconnect)
7097 async fn close_all_connected_sessions(&mut self) -> crate::error::Result<()> {
7098 let session_names: Vec<String> = self
7099 .connected_sessions
7100 .lock()
7101 .await
7102 .keys()
7103 .cloned()
7104 .collect();
7105
7106 for session_name in session_names {
7107 let _ = self.close_connected_session(&session_name).await; // Ignore errors during cleanup
7108 }
7109
7110 Ok(())
7111 }
7112
7113 /// Writes a string using unconnected explicit messaging with proper AB STRING format
7114 ///
7115 /// This method uses standard unconnected messaging instead of connected messaging
7116 /// and implements the proper Allen-Bradley STRING structure as described in the
7117 /// provided information about `Len`, `MaxLen`, and `Data[82]` format.
7118 pub async fn write_string_unconnected(
7119 &mut self,
7120 tag_name: &str,
7121 value: &str,
7122 ) -> crate::error::Result<()> {
7123 tracing::debug!(
7124 "[UNCONNECTED] Writing string '{}' to tag '{}' using unconnected messaging",
7125 value,
7126 tag_name
7127 );
7128
7129 self.validate_session().await?;
7130
7131 let string_bytes = value.as_bytes();
7132 if string_bytes.len() > 82 {
7133 return Err(EtherNetIpError::Protocol(
7134 "String too long for Allen-Bradley STRING (max 82 chars)".to_string(),
7135 ));
7136 }
7137
7138 // Build the CIP request with proper AB STRING structure
7139 let mut cip_request = Vec::new();
7140
7141 // Service: Write Tag Service (0x4D)
7142 cip_request.push(0x4D);
7143
7144 // Request Path Size (in words)
7145 let tag_bytes = tag_name.as_bytes();
7146 let path_len = if tag_bytes.len() % 2 == 0 {
7147 tag_bytes.len() + 2
7148 } else {
7149 tag_bytes.len() + 3
7150 } / 2;
7151 cip_request.push(path_len as u8);
7152
7153 // Request Path: ANSI Extended Symbol Segment for tag name
7154 cip_request.push(0x91); // ANSI Extended Symbol Segment
7155 cip_request.push(tag_bytes.len() as u8); // Tag name length
7156 cip_request.extend_from_slice(tag_bytes); // Tag name
7157
7158 // Pad to even length if necessary
7159 if tag_bytes.len() % 2 != 0 {
7160 cip_request.push(0x00);
7161 }
7162
7163 // For write operations, we don't include data type and element count
7164 // The PLC infers the data type from the tag definition
7165
7166 // Build Allen-Bradley STRING structure based on what we see in read responses:
7167 // Looking at read response: [CE, 0F, 01, 00, 00, 00, 31, 00, ...]
7168 // Structure appears to be:
7169 // - Some header/identifier (2 bytes): 0xCE, 0x0F
7170 // - Length (2 bytes): number of characters
7171 // - MaxLength or padding (2 bytes): 0x00, 0x00
7172 // - Data array (variable length, null terminated)
7173
7174 let _current_len = string_bytes.len().min(82) as u16;
7175
7176 // Build the correct Allen-Bradley STRING structure to match what the PLC expects
7177 // Analysis of read response: [CE, 0F, 01, 00, 00, 00, 31, 00, 00, 00, ...]
7178 // Structure appears to be:
7179 // - Header (2 bytes): 0xCE, 0x0F (Allen-Bradley STRING identifier)
7180 // - Length (4 bytes, DINT): Number of characters currently used
7181 // - Data (variable): Character data followed by padding to complete the structure
7182
7183 let current_len = string_bytes.len().min(82) as u32;
7184
7185 // AB STRING header/identifier - this appears to be required
7186 cip_request.extend_from_slice(&[0xCE, 0x0F]);
7187
7188 // Length (4 bytes) - number of characters used as DINT
7189 cip_request.extend_from_slice(¤t_len.to_le_bytes());
7190
7191 // Data bytes - the actual string content
7192 cip_request.extend_from_slice(&string_bytes[..current_len as usize]);
7193
7194 // Add padding if the total structure needs to be a specific size
7195 // Based on reads, it looks like there might be additional padding after the data
7196
7197 tracing::trace!(
7198 "Built Allen-Bradley STRING write request ({} bytes) for '{}' = '{}' (len={})",
7199 cip_request.len(),
7200 tag_name,
7201 value,
7202 current_len
7203 );
7204 tracing::trace!(
7205 "Request structure: Service=0x4D, Path={} bytes, Header=0xCE0F, Len={} (4 bytes), Data",
7206 path_len * 2,
7207 current_len
7208 );
7209
7210 // Send the request using standard unconnected messaging
7211 let response = self.send_cip_request(&cip_request).await?;
7212
7213 // Extract CIP response from EtherNet/IP wrapper
7214 let cip_response = self.extract_cip_from_response(&response)?;
7215
7216 // Check if write was successful - use correct CIP response format
7217 if cip_response.len() >= 3 {
7218 let service_reply = cip_response[0]; // Should be 0xCD (0x4D + 0x80) for Write Tag reply
7219 let _additional_status_size = cip_response[1]; // Additional status size (usually 0)
7220 let status = cip_response[2]; // CIP status code at position 2
7221
7222 tracing::trace!(
7223 "Write response - Service: 0x{:02X}, Status: 0x{:02X}",
7224 service_reply,
7225 status
7226 );
7227
7228 if status == 0x00 {
7229 tracing::info!("[UNCONNECTED] String write completed successfully");
7230 Ok(())
7231 } else {
7232 let error_msg = self.get_cip_error_message(status);
7233 tracing::error!(
7234 "[UNCONNECTED] String write failed: {} (0x{:02X})",
7235 error_msg,
7236 status
7237 );
7238 Err(EtherNetIpError::Protocol(format!(
7239 "CIP Error 0x{status:02X}: {error_msg}"
7240 )))
7241 }
7242 } else {
7243 Err(EtherNetIpError::Protocol(
7244 "Invalid unconnected string write response - too short".to_string(),
7245 ))
7246 }
7247 }
7248
7249 /// Write a string value to a PLC tag using unconnected messaging
7250 ///
7251 /// # Arguments
7252 ///
7253 /// * `tag_name` - The name of the tag to write to
7254 /// * `value` - The string value to write (max 82 characters)
7255 ///
7256 /// # Returns
7257 ///
7258 /// * `Ok(())` if the write was successful
7259 /// * `Err(EtherNetIpError)` if the write failed
7260 ///
7261 /// # Errors
7262 ///
7263 /// * `StringTooLong` - If the string is longer than 82 characters
7264 /// * `InvalidString` - If the string contains invalid characters
7265 /// * `TagNotFound` - If the tag doesn't exist
7266 /// * `WriteError` - If the write operation fails
7267 pub async fn write_string(&mut self, tag_name: &str, value: &str) -> crate::error::Result<()> {
7268 // Validate string length
7269 if value.len() > 82 {
7270 return Err(crate::error::EtherNetIpError::StringTooLong {
7271 max_length: 82,
7272 actual_length: value.len(),
7273 });
7274 }
7275
7276 // Validate string content (ASCII only)
7277 if !value.is_ascii() {
7278 return Err(crate::error::EtherNetIpError::InvalidString {
7279 reason: "String contains non-ASCII characters".to_string(),
7280 });
7281 }
7282
7283 // Build the string write request
7284 let request = self.build_string_write_request(tag_name, value)?;
7285
7286 // Send the request and get the response
7287 let response = self.send_cip_request(&request).await?;
7288
7289 // Parse the response
7290 let cip_response = self.extract_cip_from_response(&response)?;
7291
7292 // Check for errors in the response
7293 if cip_response.len() < 2 {
7294 return Err(crate::error::EtherNetIpError::InvalidResponse {
7295 reason: "Response too short".to_string(),
7296 });
7297 }
7298
7299 let status = cip_response[0];
7300 if status != 0 {
7301 return Err(crate::error::EtherNetIpError::WriteError {
7302 status,
7303 message: self.get_cip_error_message(status),
7304 });
7305 }
7306
7307 Ok(())
7308 }
7309
7310 /// Build a string write request packet
7311 fn build_string_write_request(
7312 &self,
7313 tag_name: &str,
7314 value: &str,
7315 ) -> crate::error::Result<Vec<u8>> {
7316 let mut request = Vec::new();
7317
7318 // CIP Write Service (0x4D)
7319 request.push(0x4D);
7320
7321 // Tag path
7322 let tag_path = self.build_tag_path(tag_name);
7323 request.extend_from_slice(&tag_path);
7324
7325 // AB STRING data structure
7326 request.extend_from_slice(&(value.len() as u16).to_le_bytes()); // Len
7327 request.extend_from_slice(&82u16.to_le_bytes()); // MaxLen
7328
7329 // Data[82] with padding
7330 let mut data = [0u8; 82];
7331 let bytes = value.as_bytes();
7332 data[..bytes.len()].copy_from_slice(bytes);
7333 request.extend_from_slice(&data);
7334
7335 Ok(request)
7336 }
7337
7338 /// Subscribes to a tag for real-time updates.
7339 ///
7340 /// The returned [`TagSubscription`] can be used to:
7341 /// - [`wait_for_update()`](TagSubscription::wait_for_update) for the next value
7342 /// - [`get_last_value()`](TagSubscription::get_last_value) for the latest cached value
7343 /// - [`into_stream()`](TagSubscription::into_stream) for an async `Stream` of updates
7344 ///
7345 /// The background poll loop uses [`SubscriptionOptions::update_rate`] (milliseconds) between reads.
7346 pub async fn subscribe_to_tag(
7347 &self,
7348 tag_path: &str,
7349 options: SubscriptionOptions,
7350 ) -> Result<TagSubscription> {
7351 let mut subscriptions = self.subscriptions.lock().await;
7352 let subscription = TagSubscription::new(tag_path.to_string(), options.clone());
7353 let update_rate_ms = options.update_rate;
7354 subscriptions.push(subscription.clone());
7355 drop(subscriptions);
7356
7357 let tag_path = tag_path.to_string();
7358 let mut client = self.clone();
7359 tokio::spawn(async move {
7360 let interval = std::time::Duration::from_millis(update_rate_ms as u64);
7361 loop {
7362 match client.read_tag(&tag_path).await {
7363 Ok(value) => {
7364 if let Err(e) = client.update_subscription(&tag_path, &value).await {
7365 tracing::error!("Error updating subscription: {}", e);
7366 break;
7367 }
7368 }
7369 Err(e) => {
7370 tracing::error!("Error reading tag {}: {}", tag_path, e);
7371 break;
7372 }
7373 }
7374 tokio::time::sleep(interval).await;
7375 }
7376 });
7377 Ok(subscription)
7378 }
7379
7380 /// Subscribes to multiple tags. Returns one [`TagSubscription`] per tag in order.
7381 pub async fn subscribe_to_tags(
7382 &self,
7383 tags: &[(&str, SubscriptionOptions)],
7384 ) -> Result<Vec<TagSubscription>> {
7385 let mut subs = Vec::with_capacity(tags.len());
7386 for (tag_name, options) in tags {
7387 subs.push(self.subscribe_to_tag(tag_name, options.clone()).await?);
7388 }
7389 Ok(subs)
7390 }
7391
7392 async fn update_subscription(&self, tag_name: &str, value: &PlcValue) -> Result<()> {
7393 let subscriptions = self.subscriptions.lock().await;
7394 for subscription in subscriptions.iter() {
7395 if subscription.tag_path == tag_name && subscription.is_active() {
7396 subscription.update_value(value).await?;
7397 }
7398 }
7399 Ok(())
7400 }
7401
7402 async fn _get_connected_session(
7403 &mut self,
7404 session_name: &str,
7405 ) -> crate::error::Result<ConnectedSession> {
7406 // First check if we already have a session
7407 {
7408 let sessions = self.connected_sessions.lock().await;
7409 if let Some(session) = sessions.get(session_name) {
7410 return Ok(session.clone());
7411 }
7412 }
7413
7414 // If we don't have a session, establish a new one
7415 let session = self.establish_connected_session(session_name).await?;
7416
7417 // Store the new session
7418 let mut sessions = self.connected_sessions.lock().await;
7419 sessions.insert(session_name.to_string(), session.clone());
7420
7421 Ok(session)
7422 }
7423
7424 /// Enhanced UDT structure parser - tries multiple parsing strategies
7425 #[allow(dead_code)]
7426 fn parse_udt_structure(&self, data: &[u8]) -> crate::error::Result<PlcValue> {
7427 tracing::debug!("Parsing UDT structure with {} bytes", data.len());
7428
7429 // Strategy 1: Try to parse as TestTagUDT structure (DINT, DINT, REAL)
7430 if data.len() >= 12 {
7431 let _offset = 0;
7432
7433 // Try different byte alignments and interpretations
7434 for alignment in 0..4 {
7435 if alignment + 12 <= data.len() {
7436 let aligned_data = &data[alignment..];
7437
7438 // Parse first DINT
7439 if aligned_data.len() >= 4 {
7440 let dint1_bytes = [
7441 aligned_data[0],
7442 aligned_data[1],
7443 aligned_data[2],
7444 aligned_data[3],
7445 ];
7446 let dint1_value = i32::from_le_bytes(dint1_bytes);
7447
7448 // Parse second DINT
7449 if aligned_data.len() >= 8 {
7450 let dint2_bytes = [
7451 aligned_data[4],
7452 aligned_data[5],
7453 aligned_data[6],
7454 aligned_data[7],
7455 ];
7456 let dint2_value = i32::from_le_bytes(dint2_bytes);
7457
7458 // Parse REAL
7459 if aligned_data.len() >= 12 {
7460 let real_bytes = [
7461 aligned_data[8],
7462 aligned_data[9],
7463 aligned_data[10],
7464 aligned_data[11],
7465 ];
7466 let real_value = f32::from_le_bytes(real_bytes);
7467
7468 tracing::trace!(
7469 "Alignment {}: DINT1={}, DINT2={}, REAL={}",
7470 alignment,
7471 dint1_value,
7472 dint2_value,
7473 real_value
7474 );
7475
7476 // Check if this looks like reasonable values
7477 if self.is_reasonable_udt_values(
7478 dint1_value,
7479 dint2_value,
7480 real_value,
7481 ) {
7482 // Legacy parsing - return raw data with symbol_id=0
7483 // Note: These methods are deprecated in favor of generic UdtData approach
7484 tracing::debug!(
7485 "Found reasonable UDT values at alignment {}",
7486 alignment
7487 );
7488 return Ok(PlcValue::Udt(UdtData {
7489 symbol_id: 0, // Not available in this context
7490 data: data.to_vec(),
7491 }));
7492 }
7493 }
7494 }
7495 }
7496 }
7497 }
7498 }
7499
7500 // Strategy 2: Try to parse as simple packed structure
7501 if data.len() >= 4 {
7502 // Try different interpretations of the data
7503 let interpretations = vec![
7504 ("DINT_at_start", 0, 4),
7505 ("DINT_at_end", data.len().saturating_sub(4), data.len()),
7506 ("DINT_middle", data.len() / 2, data.len() / 2 + 4),
7507 ];
7508
7509 for (name, start, end) in interpretations {
7510 if end <= data.len() && end > start {
7511 let bytes = &data[start..end];
7512 if bytes.len() == 4 {
7513 let dint_value =
7514 i32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
7515 tracing::trace!("{}: DINT = {}", name, dint_value);
7516
7517 if self.is_reasonable_value(dint_value) {
7518 // Legacy parsing - return raw data with symbol_id=0
7519 return Ok(PlcValue::Udt(UdtData {
7520 symbol_id: 0, // Not available in this context
7521 data: data.to_vec(),
7522 }));
7523 }
7524 }
7525 }
7526 }
7527 }
7528
7529 Err(crate::error::EtherNetIpError::Protocol(
7530 "Could not parse UDT structure".to_string(),
7531 ))
7532 }
7533
7534 /// Simple UDT parser fallback
7535 /// Note: This is a legacy method. New code should use generic UdtData approach.
7536 #[allow(dead_code)]
7537 fn parse_udt_simple(&self, data: &[u8]) -> crate::error::Result<PlcValue> {
7538 // Legacy parsing - return raw data with symbol_id=0
7539 Ok(PlcValue::Udt(UdtData {
7540 symbol_id: 0, // Not available in this context
7541 data: data.to_vec(),
7542 }))
7543 }
7544
7545 /// Check if UDT values look reasonable
7546 #[allow(dead_code)]
7547 fn is_reasonable_udt_values(&self, dint1: i32, dint2: i32, real: f32) -> bool {
7548 // Check for reasonable ranges
7549 let dint1_reasonable = (-1000..=1000).contains(&dint1);
7550 let dint2_reasonable = (-1000..=1000).contains(&dint2);
7551 let real_reasonable = (-1000.0..=1000.0).contains(&real) && real.is_finite();
7552
7553 tracing::trace!(
7554 "Reasonableness check: DINT1={} ({}), DINT2={} ({}), REAL={} ({})",
7555 dint1,
7556 dint1_reasonable,
7557 dint2,
7558 dint2_reasonable,
7559 real,
7560 real_reasonable
7561 );
7562
7563 dint1_reasonable && dint2_reasonable && real_reasonable
7564 }
7565
7566 /// Check if a single value looks reasonable
7567 #[allow(dead_code)]
7568 fn is_reasonable_value(&self, value: i32) -> bool {
7569 (-1000..=1000).contains(&value)
7570 }
7571}
7572
7573/*
7574===============================================================================
7575END OF LIBRARY DOCUMENTATION
7576
7577This file provides a complete, production-ready EtherNet/IP communication
7578library for Allen-Bradley PLCs. The library includes:
7579
7580- Native Rust API with async support
7581- C FFI exports for cross-language integration
7582- Comprehensive error handling and validation
7583- Detailed documentation and examples
7584- Performance optimizations
7585- Memory safety guarantees
7586
7587For usage examples, see the main.rs file or the C# integration samples.
7588
7589For technical details about the EtherNet/IP protocol implementation,
7590refer to the inline documentation above.
7591
7592Version: 1.0.0
7593Compatible with: CompactLogix L1x-L5x series PLCs
7594License: As specified in Cargo.toml
7595===============================================================================_
7596*/