rust_ethernet_ip/
lib.rs

1// lib.rs - Rust EtherNet/IP Driver Library with Comprehensive Documentation
2// =========================================================================
3//
4// # Rust EtherNet/IP Driver Library
5//
6// A high-performance, production-ready EtherNet/IP communication library for
7// Allen-Bradley CompactLogix and ControlLogix PLCs, written in pure Rust with C FFI exports.
8//
9// ## Overview
10//
11// This library provides a complete implementation of the EtherNet/IP protocol
12// and Common Industrial Protocol (CIP) for communicating with Allen-Bradley
13// CompactLogix and ControlLogix series PLCs. It offers both native Rust APIs and C-compatible
14// FFI exports for integration with other programming languages.
15//
16// ## Architecture
17//
18// ```text
19// ┌─────────────────────────────────────────────────────────────────────────────────┐
20// │                              Application Layer                                  │
21// │  ┌─────────────┐  ┌─────────────────────────────────────────────────────────┐  │
22// │  │    Rust     │  │                    C# Ecosystem                         │  │
23// │  │   Native    │  │  ┌─────────────┐  ┌─────────────┐  ┌─────────────────┐  │  │
24// │  │             │  │  │     WPF     │  │  WinForms   │  │   ASP.NET Core  │  │  │
25// │  │             │  │  │  Desktop    │  │  Desktop    │  │    Web API      │  │  │
26// │  │             │  │  └─────────────┘  └─────────────┘  └─────────┬───────┘  │  │
27// │  │             │  │                                               │           │  │
28// │  │             │  │                                    ┌─────────┴───────┐  │  │
29// │  │             │  │                                    │  TypeScript +   │  │  │
30// │  │             │  │                                    │  React Frontend │  │  │
31// │  │             │  │                                    │  (HTTP/REST)    │  │  │
32// │  │             │  │                                    └─────────────────┘  │  │
33// │  └─────────────┘  └─────────────────────────────────────────────────────────┘  │
34// └─────────────────────┬─────────────────────────────────────────────────────────┘
35//                       │
36// ┌─────────────────────┴─────────────────────────────────────────────────────────┐
37// │                           C# FFI Wrapper                                      │
38// │  • 22 exported functions for all data types                                   │
39// │  • Type-safe C# API with comprehensive error handling                         │
40// │  • Cross-platform support (Windows, Linux, macOS)                            │
41// └─────────────────────┬─────────────────────────────────────────────────────────┘
42//                       │
43// ┌─────────────────────┴─────────────────────────────────────────────────────────┐
44// │                         Core Rust Library                                     │
45// │  ┌─────────────────────────────────────────────────────────────────────────┐  │
46// │  │                           EipClient                                     │  │  
47// │  │  • Connection Management & Session Handling                            │  │
48// │  │  • Advanced Tag Operations & Program-Scoped Tag Support                │  │
49// │  │  • Complete Data Type Support (13 Allen-Bradley types)                 │  │
50// │  │  • Advanced Tag Path Parsing (arrays, bits, UDTs, strings)             │  │
51// │  └─────────────────────────────────────────────────────────────────────────┘  │
52// │  ┌─────────────────────────────────────────────────────────────────────────┐  │
53// │  │                    Protocol Implementation                              │  │
54// │  │  • EtherNet/IP Encapsulation Protocol                                  │  │
55// │  │  • CIP (Common Industrial Protocol)                                    │  │
56// │  │  • Symbolic Tag Addressing with Advanced Parsing                       │  │
57// │  │  • Comprehensive CIP Error Code Mapping                                │  │
58// │  └─────────────────────────────────────────────────────────────────────────┘  │
59// │  ┌─────────────────────────────────────────────────────────────────────────┐  │
60// │  │                        Network Layer                                    │  │
61// │  │  • TCP Socket Management with Connection Pooling                       │  │
62// │  │  • Async I/O with Tokio Runtime                                        │  │
63// │  │  • Robust Error Handling & Network Resilience                          │  │
64// │  │  • Session Management & Automatic Reconnection                         │  │
65// │  └─────────────────────────────────────────────────────────────────────────┘  │
66// └─────────────────────────────────────────────────────────────────────────────────┘
67// ```
68//
69// ## Integration Paths
70//
71// ### 🦀 **Native Rust Applications**
72// Direct library usage with full async support and zero-overhead abstractions.
73// Perfect for high-performance applications and embedded systems.
74//
75// ### 🖥️ **Desktop Applications (C#)**
76// - **WPF**: Modern desktop applications with MVVM architecture
77// - **WinForms**: Traditional Windows applications with familiar UI patterns
78// - Uses C# FFI wrapper for seamless integration
79//
80// ### 🌐 **Web Applications**
81// - **ASP.NET Core Web API**: RESTful backend service
82// - **TypeScript + React Frontend**: Modern web dashboard via HTTP/REST API
83// - **Scalable Architecture**: Backend handles PLC communication, frontend provides UI
84//
85// ### 🔧 **System Integration**
86// - **C/C++ Applications**: Direct FFI integration
87// - **Other .NET Languages**: VB.NET, F#, etc. via C# wrapper
88// - **Microservices**: ASP.NET Core API as a service component
89//
90// ## Features
91//
92// ### Core Capabilities
93// - **High Performance**: 1,500+ read operations per second, 800+ write operations per second
94// - **Complete Data Types**: All Allen-Bradley native data types with type-safe operations
95// - **Advanced Tag Addressing**: Program-scoped, arrays, bits, UDTs, strings
96// - **Async I/O**: Built on Tokio for excellent concurrency and performance
97// - **Error Handling**: Comprehensive CIP error code mapping and reporting
98// - **Memory Safe**: Zero-copy operations where possible, proper resource cleanup
99//
100// ### Supported PLCs
101// - **CompactLogix L1x, L2x, L3x, L4x, L5x series** (Primary focus)
102// - **ControlLogix L6x, L7x, L8x series** (Full support)
103// - Optimized for PC applications (Windows, Linux, macOS)
104//
105// ### Advanced Tag Addressing
106// - **Program-scoped tags**: `Program:MainProgram.Tag1`
107// - **Array element access**: `MyArray[5]`, `MyArray[1,2,3]`
108// - **Bit-level operations**: `MyDINT.15` (access individual bits)
109// - **UDT member access**: `MyUDT.Member1.SubMember`
110// - **String operations**: `MyString.LEN`, `MyString.DATA[5]`
111// - **Complex nested paths**: `Program:Production.Lines[2].Stations[5].Motor.Status.15`
112//
113// ### Complete Data Type Support
114// - **BOOL**: Boolean values
115// - **SINT, INT, DINT, LINT**: Signed integers (8, 16, 32, 64-bit)
116// - **USINT, UINT, UDINT, ULINT**: Unsigned integers (8, 16, 32, 64-bit)
117// - **REAL, LREAL**: Floating point (32, 64-bit IEEE 754)
118// - **STRING**: Variable-length strings
119// - **UDT**: User Defined Types with full nesting support
120//
121// ### Protocol Support
122// - **EtherNet/IP**: Complete encapsulation protocol implementation
123// - **CIP**: Common Industrial Protocol for tag operations
124// - **Symbolic Addressing**: Direct tag name resolution with advanced parsing
125// - **Session Management**: Proper registration/unregistration sequences
126//
127// ### Integration Options
128// - **Native Rust**: Direct library usage with full async support
129// - **C# Desktop Applications**: WPF and WinForms via C# FFI wrapper
130// - **Web Applications**: ASP.NET Core API + TypeScript/React frontend
131// - **C/C++ Integration**: Direct FFI functions for system integration
132// - **Cross-Platform**: Windows, Linux, macOS support
133//
134// ## Performance Characteristics
135//
136// Benchmarked on typical industrial hardware:
137//
138// | Operation | Performance | Notes |
139// |-----------|-------------|-------|
140// | Read BOOL | 1,500+ ops/sec | Single tag operations |
141// | Read DINT | 1,400+ ops/sec | 32-bit integer tags |
142// | Read REAL | 1,300+ ops/sec | Floating point tags |
143// | Write BOOL | 800+ ops/sec | Single tag operations |
144// | Write DINT | 750+ ops/sec | 32-bit integer tags |
145// | Write REAL | 700+ ops/sec | Floating point tags |
146// | Connection | <1 second | Initial session setup |
147// | Tag Path Parsing | 10,000+ ops/sec | Advanced addressing |
148//
149// ## Security Considerations
150//
151// - **No Authentication**: EtherNet/IP protocol has limited built-in security
152// - **Network Level**: Implement firewall rules and network segmentation
153// - **PLC Protection**: Use PLC safety locks and access controls
154// - **Data Validation**: Always validate data before writing to PLCs
155//
156// ## Thread Safety
157//
158// The `EipClient` struct is **NOT** thread-safe. For multi-threaded applications:
159// - Use one client per thread, OR
160// - Implement external synchronization (Mutex/RwLock), OR
161// - Use a connection pool pattern
162//
163// ## Memory Usage
164//
165// - **Per Connection**: ~8KB base memory footprint
166// - **Network Buffers**: ~2KB per active connection
167// - **Tag Cache**: Minimal (tag names only when needed)
168// - **Total Typical**: <10MB for most applications
169//
170// ## Error Handling Philosophy
171//
172// This library follows Rust's error handling principles:
173// - All fallible operations return `Result<T, EtherNetIpError>`
174// - Errors are propagated rather than panicking
175// - Detailed error messages with CIP status code mapping
176// - Network errors are distinguished from protocol errors
177//
178// ## Examples
179//
180// See the `examples/` directory for comprehensive usage examples, including:
181// - Advanced tag addressing demonstrations
182// - Complete data type showcase
183// - Real-world industrial automation scenarios
184//
185// ## Changelog
186//
187// ### v0.4.0 (January 2025)
188// - Complete data type support for all Allen-Bradley types
189// - Advanced tag path parsing (program-scoped, arrays, bits, UDTs)
190// - Enhanced error handling and documentation
191// - Comprehensive test coverage (30+ unit tests)
192// - Production-ready stability and performance
193//
194// =========================================================================
195
196use tokio::net::TcpStream;
197use tokio::io::{AsyncReadExt, AsyncWriteExt};
198use tokio::time::{timeout, Duration, Instant};
199use std::collections::HashMap;
200use tokio::runtime::Runtime;
201use tokio::sync::Mutex;
202use lazy_static::lazy_static;
203use crate::udt::UdtManager;
204use std::sync::Arc;
205use std::net::SocketAddr;
206use std::sync::atomic::AtomicBool;
207
208pub mod version;
209pub mod plc_manager;
210pub mod tag_manager;
211pub mod tag_path;
212pub mod udt;
213pub mod error;
214pub mod ffi;
215pub mod subscription;
216pub mod python;  // Add Python module
217
218// Re-export commonly used items
219pub use plc_manager::{PlcManager, PlcConfig, PlcConnection};
220pub use tag_manager::{TagManager, TagCache, TagMetadata, TagScope, TagPermissions};
221pub use tag_path::TagPath;
222pub use udt::{UdtDefinition, UdtMember};
223pub use error::{EtherNetIpError, Result};
224pub use subscription::{TagSubscription, SubscriptionOptions, SubscriptionManager};
225
226// Static runtime and client management for FFI
227lazy_static! {
228    /// Global Tokio runtime for handling async operations in FFI context
229    static ref RUNTIME: Runtime = Runtime::new().unwrap();
230    
231    /// Global storage for EipClient instances, indexed by client ID
232    static ref CLIENTS: Mutex<HashMap<i32, EipClient>> = Mutex::new(HashMap::new());
233    
234    /// Counter for generating unique client IDs
235    static ref NEXT_ID: Mutex<i32> = Mutex::new(1);
236}
237
238// =========================================================================
239// BATCH OPERATIONS DATA STRUCTURES
240// =========================================================================
241
242/// Represents a single operation in a batch request
243/// 
244/// This enum defines the different types of operations that can be
245/// performed in a batch. Each operation specifies whether it's a read
246/// or write operation and includes the necessary parameters.
247#[derive(Debug, Clone)]
248pub enum BatchOperation {
249    /// Read operation for a specific tag
250    /// 
251    /// # Fields
252    /// 
253    /// * `tag_name` - The name of the tag to read
254    Read { tag_name: String },
255    
256    /// Write operation for a specific tag with a value
257    /// 
258    /// # Fields
259    /// 
260    /// * `tag_name` - The name of the tag to write
261    /// * `value` - The value to write to the tag
262    Write { tag_name: String, value: PlcValue },
263}
264
265/// Result of a single operation in a batch request
266/// 
267/// This structure contains the result of executing a single batch operation,
268/// including success/failure status and the actual data or error information.
269#[derive(Debug, Clone)]
270pub struct BatchResult {
271    /// The original operation that was executed
272    pub operation: BatchOperation,
273    
274    /// The result of the operation
275    pub result: std::result::Result<Option<PlcValue>, BatchError>,
276    
277    /// Execution time for this specific operation (in microseconds)
278    pub execution_time_us: u64,
279}
280
281/// Specific error types that can occur during batch operations
282/// 
283/// This enum provides detailed error information for batch operations,
284/// allowing for better error handling and diagnostics.
285#[derive(Debug, Clone)]
286pub enum BatchError {
287    /// Tag was not found in the PLC
288    TagNotFound(String),
289    
290    /// Data type mismatch between expected and actual
291    DataTypeMismatch { expected: String, actual: String },
292    
293    /// Network communication error
294    NetworkError(String),
295    
296    /// CIP protocol error with status code
297    CipError { status: u8, message: String },
298    
299    /// Tag name parsing error
300    TagPathError(String),
301    
302    /// Value serialization/deserialization error
303    SerializationError(String),
304    
305    /// Operation timeout
306    Timeout,
307    
308    /// Generic error for unexpected issues
309    Other(String),
310}
311
312impl std::fmt::Display for BatchError {
313    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
314        match self {
315            BatchError::TagNotFound(tag) => write!(f, "Tag not found: {}", tag),
316            BatchError::DataTypeMismatch { expected, actual } => {
317                write!(f, "Data type mismatch: expected {}, got {}", expected, actual)
318            }
319            BatchError::NetworkError(msg) => write!(f, "Network error: {}", msg),
320            BatchError::CipError { status, message } => {
321                write!(f, "CIP error (0x{:02X}): {}", status, message)
322            }
323            BatchError::TagPathError(msg) => write!(f, "Tag path error: {}", msg),
324            BatchError::SerializationError(msg) => write!(f, "Serialization error: {}", msg),
325            BatchError::Timeout => write!(f, "Operation timeout"),
326            BatchError::Other(msg) => write!(f, "Error: {}", msg),
327        }
328    }
329}
330
331impl std::error::Error for BatchError {}
332
333/// Configuration for batch operations
334/// 
335/// This structure controls the behavior and performance characteristics
336/// of batch read/write operations. Proper tuning can significantly
337/// improve throughput for applications that need to process many tags.
338#[derive(Debug, Clone)]
339pub struct BatchConfig {
340    /// Maximum number of operations to include in a single CIP packet
341    /// 
342    /// Larger values improve performance but may exceed PLC packet size limits.
343    /// Typical range: 10-50 operations per packet.
344    pub max_operations_per_packet: usize,
345    
346    /// Maximum packet size in bytes for batch operations
347    /// 
348    /// Should not exceed the PLC's maximum packet size capability.
349    /// Typical values: 504 bytes (default), up to 4000 bytes for modern PLCs.
350    pub max_packet_size: usize,
351    
352    /// Timeout for individual batch packets (in milliseconds)
353    /// 
354    /// This is per-packet timeout, not per-operation.
355    /// Typical range: 1000-5000 milliseconds.
356    pub packet_timeout_ms: u64,
357    
358    /// Whether to continue processing other operations if one fails
359    /// 
360    /// If true, failed operations are reported but don't stop the batch.
361    /// If false, the first error stops the entire batch processing.
362    pub continue_on_error: bool,
363    
364    /// Whether to optimize packet packing by grouping similar operations
365    /// 
366    /// If true, reads and writes are grouped separately for better performance.
367    /// If false, operations are processed in the order provided.
368    pub optimize_packet_packing: bool,
369}
370
371impl Default for BatchConfig {
372    fn default() -> Self {
373        Self {
374            max_operations_per_packet: 20,
375            max_packet_size: 504,  // Conservative default for maximum compatibility
376            packet_timeout_ms: 3000,
377            continue_on_error: true,
378            optimize_packet_packing: true,
379        }
380    }
381}
382
383/// Connected session information for Class 3 explicit messaging
384/// 
385/// Allen-Bradley PLCs often require connected sessions for certain operations
386/// like STRING writes. This structure maintains the connection state.
387#[derive(Debug, Clone)]
388pub struct ConnectedSession {
389    /// Connection ID assigned by the PLC
390    pub connection_id: u32,
391    
392    /// Our connection ID (originator -> target)
393    pub o_to_t_connection_id: u32,
394    
395    /// PLC's connection ID (target -> originator)  
396    pub t_to_o_connection_id: u32,
397    
398    /// Connection serial number for this session
399    pub connection_serial: u16,
400    
401    /// Originator vendor ID (our vendor ID)
402    pub originator_vendor_id: u16,
403    
404    /// Originator serial number (our serial number)
405    pub originator_serial: u32,
406    
407    /// Connection timeout multiplier
408    pub timeout_multiplier: u8,
409    
410    /// Requested Packet Interval (RPI) in microseconds
411    pub rpi: u32,
412    
413    /// Connection parameters for O->T direction
414    pub o_to_t_params: ConnectionParameters,
415    
416    /// Connection parameters for T->O direction  
417    pub t_to_o_params: ConnectionParameters,
418    
419    /// Timestamp when connection was established
420    pub established_at: Instant,
421    
422    /// Whether this connection is currently active
423    pub is_active: bool,
424    
425    /// Sequence counter for connected messages (increments with each message)
426    pub sequence_count: u16,
427}
428
429/// Connection parameters for EtherNet/IP connections
430#[derive(Debug, Clone)]
431pub struct ConnectionParameters {
432    /// Connection size in bytes
433    pub size: u16,
434    
435    /// Connection type (0x02 = Point-to-point, 0x01 = Multicast)
436    pub connection_type: u8,
437    
438    /// Priority (0x00 = Low, 0x01 = High, 0x02 = Scheduled, 0x03 = Urgent)
439    pub priority: u8,
440    
441    /// Variable size flag
442    pub variable_size: bool,
443}
444
445impl Default for ConnectionParameters {
446    fn default() -> Self {
447        Self {
448            size: 500,              // 500 bytes default
449            connection_type: 0x02,  // Point-to-point
450            priority: 0x01,         // High priority
451            variable_size: false,
452        }
453    }
454}
455
456impl ConnectedSession {
457    /// Creates a new connected session with default parameters
458    pub fn new(connection_serial: u16) -> Self {
459        Self {
460            connection_id: 0,
461            o_to_t_connection_id: 0,
462            t_to_o_connection_id: 0,
463            connection_serial,
464            originator_vendor_id: 0x1337,  // Custom vendor ID
465            originator_serial: 0x12345678,  // Custom serial number
466            timeout_multiplier: 0x05,       // 32 seconds timeout
467            rpi: 100000,                    // 100ms RPI
468            o_to_t_params: ConnectionParameters::default(),
469            t_to_o_params: ConnectionParameters::default(),
470            established_at: Instant::now(),
471            is_active: false,
472            sequence_count: 0,
473        }
474    }
475    
476    /// Creates a connected session with alternative parameters for different PLCs
477    pub fn with_config(connection_serial: u16, config_id: u8) -> Self {
478        let mut session = Self::new(connection_serial);
479        
480        match config_id {
481            1 => {
482                // Config 1: Conservative Allen-Bradley parameters
483                session.timeout_multiplier = 0x07;  // 256 seconds timeout
484                session.rpi = 200000;  // 200ms RPI (slower)
485                session.o_to_t_params.size = 504;   // Standard packet size
486                session.t_to_o_params.size = 504;
487                session.o_to_t_params.priority = 0x00;  // Low priority
488                session.t_to_o_params.priority = 0x00;
489                println!("🔧 [CONFIG 1] Conservative: 504 bytes, 200ms RPI, low priority");
490            },
491            2 => {
492                // Config 2: Compact parameters 
493                session.timeout_multiplier = 0x03;  // 8 seconds timeout
494                session.rpi = 50000;   // 50ms RPI (faster)
495                session.o_to_t_params.size = 256;   // Smaller packet size
496                session.t_to_o_params.size = 256;
497                session.o_to_t_params.priority = 0x02;  // Scheduled priority
498                session.t_to_o_params.priority = 0x02;
499                println!("🔧 [CONFIG 2] Compact: 256 bytes, 50ms RPI, scheduled priority");
500            },
501            3 => {
502                // Config 3: Minimal parameters
503                session.timeout_multiplier = 0x01;  // 4 seconds timeout
504                session.rpi = 1000000; // 1000ms RPI (very slow)
505                session.o_to_t_params.size = 128;   // Very small packets
506                session.t_to_o_params.size = 128;
507                session.o_to_t_params.priority = 0x03;  // Urgent priority
508                session.t_to_o_params.priority = 0x03;
509                println!("🔧 [CONFIG 3] Minimal: 128 bytes, 1000ms RPI, urgent priority");
510            },
511            4 => {
512                // Config 4: Standard Rockwell parameters (from documentation)
513                session.timeout_multiplier = 0x05;  // 32 seconds timeout
514                session.rpi = 100000;  // 100ms RPI
515                session.o_to_t_params.size = 500;   // Standard size
516                session.t_to_o_params.size = 500;
517                session.o_to_t_params.connection_type = 0x01;  // Multicast
518                session.t_to_o_params.connection_type = 0x01;
519                session.originator_vendor_id = 0x001D;  // Rockwell vendor ID
520                println!("🔧 [CONFIG 4] Rockwell standard: 500 bytes, 100ms RPI, multicast, Rockwell vendor");
521            },
522            5 => {
523                // Config 5: Large buffer parameters
524                session.timeout_multiplier = 0x0A;  // Very long timeout
525                session.rpi = 500000;  // 500ms RPI
526                session.o_to_t_params.size = 1024;  // Large packets
527                session.t_to_o_params.size = 1024;
528                session.o_to_t_params.variable_size = true;  // Variable size
529                session.t_to_o_params.variable_size = true;
530                println!("🔧 [CONFIG 5] Large buffer: 1024 bytes, 500ms RPI, variable size");
531            },
532            _ => {
533                // Default config
534                println!("🔧 [CONFIG 0] Default parameters");
535            }
536        }
537        
538        session
539    }
540}
541
542/// Represents the different data types supported by Allen-Bradley PLCs
543/// 
544/// These correspond to the CIP data type codes used in EtherNet/IP
545/// communication. Each variant maps to a specific 16-bit type identifier
546/// that the PLC uses to describe tag data.
547/// 
548/// # Supported Data Types
549/// 
550/// ## Integer Types
551/// - **SINT**: 8-bit signed integer (-128 to 127)
552/// - **INT**: 16-bit signed integer (-32,768 to 32,767)
553/// - **DINT**: 32-bit signed integer (-2,147,483,648 to 2,147,483,647)
554/// - **LINT**: 64-bit signed integer (-9,223,372,036,854,775,808 to 9,223,372,036,854,775,807)
555/// 
556/// ## Unsigned Integer Types
557/// - **USINT**: 8-bit unsigned integer (0 to 255)
558/// - **UINT**: 16-bit unsigned integer (0 to 65,535)
559/// - **UDINT**: 32-bit unsigned integer (0 to 4,294,967,295)
560/// - **ULINT**: 64-bit unsigned integer (0 to 18,446,744,073,709,551,615)
561/// 
562/// ## Floating Point Types
563/// - **REAL**: 32-bit IEEE 754 float (±1.18 × 10^-38 to ±3.40 × 10^38)
564/// - **LREAL**: 64-bit IEEE 754 double (±2.23 × 10^-308 to ±1.80 × 10^308)
565/// 
566/// ## Other Types
567/// - **BOOL**: Boolean value (true/false)
568/// - **STRING**: Variable-length string
569/// - **UDT**: User Defined Type (structured data)
570#[derive(Debug, Clone, PartialEq)]
571pub enum PlcValue {
572    /// Boolean value (single bit)
573    /// 
574    /// Maps to CIP type 0x00C1. In CompactLogix PLCs, BOOL tags
575    /// are stored as single bits but transmitted as bytes over the network.
576    Bool(bool),
577    
578    /// 8-bit signed integer (-128 to 127)
579    /// 
580    /// Maps to CIP type 0x00C2. Used for small numeric values,
581    /// status codes, and compact data storage.
582    Sint(i8),
583    
584    /// 16-bit signed integer (-32,768 to 32,767)
585    /// 
586    /// Maps to CIP type 0x00C3. Common for analog input/output values,
587    /// counters, and medium-range numeric data.
588    Int(i16),
589    
590    /// 32-bit signed integer (-2,147,483,648 to 2,147,483,647)
591    /// 
592    /// Maps to CIP type 0x00C4. This is the most common integer type
593    /// in Allen-Bradley PLCs, used for counters, setpoints, and numeric values.
594    Dint(i32),
595    
596    /// 64-bit signed integer (-9,223,372,036,854,775,808 to 9,223,372,036,854,775,807)
597    /// 
598    /// Maps to CIP type 0x00C5. Used for large counters, timestamps,
599    /// and high-precision calculations.
600    Lint(i64),
601    
602    /// 8-bit unsigned integer (0 to 255)
603    /// 
604    /// Maps to CIP type 0x00C6. Used for byte data, small counters,
605    /// and status flags.
606    Usint(u8),
607    
608    /// 16-bit unsigned integer (0 to 65,535)
609    /// 
610    /// Maps to CIP type 0x00C7. Common for analog values, port numbers,
611    /// and medium-range unsigned data.
612    Uint(u16),
613    
614    /// 32-bit unsigned integer (0 to 4,294,967,295)
615    /// 
616    /// Maps to CIP type 0x00C8. Used for large counters, memory addresses,
617    /// and unsigned calculations.
618    Udint(u32),
619    
620    /// 64-bit unsigned integer (0 to 18,446,744,073,709,551,615)
621    /// 
622    /// Maps to CIP type 0x00C9. Used for very large counters, timestamps,
623    /// and high-precision unsigned calculations.
624    Ulint(u64),
625    
626    /// 32-bit IEEE 754 floating point number
627    /// 
628    /// Maps to CIP type 0x00CA. Used for analog values, calculations,
629    /// and any data requiring decimal precision.
630    /// Range: ±1.18 × 10^-38 to ±3.40 × 10^38
631    Real(f32),
632    
633    /// 64-bit IEEE 754 floating point number
634    /// 
635    /// Maps to CIP type 0x00CB. Used for high-precision calculations,
636    /// scientific data, and extended-range floating point values.
637    /// Range: ±2.23 × 10^-308 to ±1.80 × 10^308
638    Lreal(f64),
639    
640    /// String value
641    /// 
642    /// Maps to CIP type 0x00DA. Variable-length string data
643    /// commonly used for product names, status messages, and text data.
644    String(String),
645    
646    /// User Defined Type instance
647    /// 
648    /// Maps to CIP type 0x00A0. Structured data type containing
649    /// multiple members of different types.
650    Udt(HashMap<String, PlcValue>),
651}
652
653impl PlcValue {
654    /// Converts the PLC value to its byte representation for network transmission
655    /// 
656    /// This function handles the little-endian byte encoding required by
657    /// the EtherNet/IP protocol. Each data type has specific encoding rules:
658    /// 
659    /// - BOOL: Single byte (0x00 = false, 0xFF = true)
660    /// - SINT: Single signed byte
661    /// - INT: 2 bytes in little-endian format
662    /// - DINT: 4 bytes in little-endian format
663    /// - LINT: 8 bytes in little-endian format
664    /// - USINT: Single unsigned byte
665    /// - UINT: 2 bytes in little-endian format
666    /// - UDINT: 4 bytes in little-endian format
667    /// - ULINT: 8 bytes in little-endian format
668    /// - REAL: 4 bytes IEEE 754 little-endian format
669    /// - LREAL: 8 bytes IEEE 754 little-endian format
670    /// 
671    /// # Returns
672    /// 
673    /// A vector of bytes ready for transmission to the PLC
674    pub fn to_bytes(&self) -> Vec<u8> {
675        match self {
676            PlcValue::Bool(val) => vec![if *val { 0xFF } else { 0x00 }],
677            PlcValue::Sint(val) => val.to_le_bytes().to_vec(),
678            PlcValue::Int(val) => val.to_le_bytes().to_vec(),
679            PlcValue::Dint(val) => val.to_le_bytes().to_vec(),
680            PlcValue::Lint(val) => val.to_le_bytes().to_vec(),
681            PlcValue::Usint(val) => val.to_le_bytes().to_vec(),
682            PlcValue::Uint(val) => val.to_le_bytes().to_vec(),
683            PlcValue::Udint(val) => val.to_le_bytes().to_vec(),
684            PlcValue::Ulint(val) => val.to_le_bytes().to_vec(),
685            PlcValue::Real(val) => val.to_le_bytes().to_vec(),
686            PlcValue::Lreal(val) => val.to_le_bytes().to_vec(),
687            PlcValue::String(val) => {
688                // Try minimal approach - just length + data without padding
689                // Testing if the PLC accepts a simpler format
690                
691                let mut bytes = Vec::new();
692                
693                // Length field (4 bytes as DINT) - number of characters currently used
694                let length = val.len().min(82) as u32;
695                bytes.extend_from_slice(&length.to_le_bytes());
696                
697                // String data - just the actual characters, no padding
698                let string_bytes = val.as_bytes();
699                let data_len = string_bytes.len().min(82);
700                bytes.extend_from_slice(&string_bytes[..data_len]);
701                
702                bytes
703            }
704            PlcValue::Udt(_) => {
705                // UDT serialization is handled by the UdtManager
706                vec![]
707            }
708        }
709    }
710    
711    /// Returns the CIP data type code for this value
712    /// 
713    /// These codes are defined by the CIP specification and must match
714    /// exactly what the PLC expects for each data type.
715    /// 
716    /// # Returns
717    /// 
718    /// The 16-bit CIP type code for this value type
719    pub fn get_data_type(&self) -> u16 {
720        match self {
721            PlcValue::Bool(_) => 0x00C1,  // BOOL
722            PlcValue::Sint(_) => 0x00C2,  // SINT (signed char)
723            PlcValue::Int(_) => 0x00C3,   // INT (short)
724            PlcValue::Dint(_) => 0x00C4,  // DINT (int)
725            PlcValue::Lint(_) => 0x00C5,  // LINT (long long)
726            PlcValue::Usint(_) => 0x00C6, // USINT (unsigned char)
727            PlcValue::Uint(_) => 0x00C7,  // UINT (unsigned short)
728            PlcValue::Udint(_) => 0x00C8, // UDINT (unsigned int)
729            PlcValue::Ulint(_) => 0x00C9, // ULINT (unsigned long long)
730            PlcValue::Real(_) => 0x00CA,  // REAL (float)
731            PlcValue::Lreal(_) => 0x00CB, // LREAL (double)
732            PlcValue::String(_) => 0x02A0, // Allen-Bradley STRING type (matches PLC read responses)
733            PlcValue::Udt(_) => 0x00A0,   // UDT placeholder
734        }
735    }
736}
737
738/// High-performance EtherNet/IP client for PLC communication
739/// 
740/// This struct provides the core functionality for communicating with Allen-Bradley
741/// PLCs using the EtherNet/IP protocol. It handles connection management, session
742/// registration, and tag operations.
743/// 
744/// # Thread Safety
745/// 
746/// The `EipClient` is **NOT** thread-safe. For multi-threaded applications:
747/// 
748/// ```rust,no_run
749/// use std::sync::Arc;
750/// use tokio::sync::Mutex;
751/// use rust_ethernet_ip::EipClient;
752/// 
753/// #[tokio::main]
754/// async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
755///     // Create a thread-safe wrapper
756///     let client = Arc::new(Mutex::new(EipClient::connect("192.168.1.100:44818").await?));
757/// 
758///     // Use in multiple threads
759///     let client_clone = client.clone();
760///     tokio::spawn(async move {
761///         let mut client = client_clone.lock().await;
762///         let _ = client.read_tag("Tag1").await?;
763///         Ok::<(), Box<dyn std::error::Error + Send + Sync>>(())
764///     });
765///     Ok(())
766/// }
767/// ```
768/// 
769/// # Performance Characteristics
770/// 
771/// | Operation | Latency | Throughput | Memory |
772/// |-----------|---------|------------|---------|
773/// | Connect | 100-500ms | N/A | ~8KB |
774/// | Read Tag | 1-5ms | 1,500+ ops/sec | ~2KB |
775/// | Write Tag | 2-10ms | 600+ ops/sec | ~2KB |
776/// | Batch Read | 5-20ms | 2,000+ ops/sec | ~4KB |
777/// 
778/// # Error Handling
779/// 
780/// All operations return `Result<T, EtherNetIpError>`. Common errors include:
781/// 
782/// ```rust,no_run
783/// use rust_ethernet_ip::{EipClient, EtherNetIpError};
784/// 
785/// #[tokio::main]
786/// async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
787///     let mut client = EipClient::connect("192.168.1.100:44818").await?;
788///     match client.read_tag("Tag1").await {
789///         Ok(value) => println!("Tag value: {:?}", value),
790///         Err(EtherNetIpError::Protocol(_)) => println!("Tag does not exist"),
791///         Err(EtherNetIpError::Connection(_)) => println!("Lost connection to PLC"),
792///         Err(EtherNetIpError::Timeout(_)) => println!("Operation timed out"),
793///         Err(e) => println!("Other error: {}", e),
794///     }
795///     Ok(())
796/// }
797/// ```
798/// 
799/// # Examples
800/// 
801/// Basic usage:
802/// ```rust,no_run
803/// use rust_ethernet_ip::{EipClient, PlcValue};
804/// 
805/// #[tokio::main]
806/// async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
807///     let mut client = EipClient::connect("192.168.1.100:44818").await?;
808/// 
809///     // Read a boolean tag
810///     let motor_running = client.read_tag("MotorRunning").await?;
811/// 
812///     // Write an integer tag
813///     client.write_tag("SetPoint", PlcValue::Dint(1500)).await?;
814/// 
815///     // Read multiple tags in sequence
816///     let tag1 = client.read_tag("Tag1").await?;
817///     let tag2 = client.read_tag("Tag2").await?;
818///     let tag3 = client.read_tag("Tag3").await?;
819///     Ok(())
820/// }
821/// ```
822/// 
823/// Advanced usage with error recovery:
824/// ```rust
825/// use rust_ethernet_ip::{EipClient, PlcValue, EtherNetIpError};
826/// use tokio::time::Duration;
827/// 
828/// async fn read_with_retry(client: &mut EipClient, tag: &str, retries: u32) -> Result<PlcValue, EtherNetIpError> {
829///     for attempt in 0..retries {
830///         match client.read_tag(tag).await {
831///             Ok(value) => return Ok(value),
832///             Err(EtherNetIpError::Connection(_)) => {
833///                 if attempt < retries - 1 {
834///                     tokio::time::sleep(Duration::from_secs(1)).await;
835///                     continue;
836///                 }
837///             }
838///             Err(e) => return Err(e),
839///         }
840///     }
841///     Err(EtherNetIpError::Protocol("Max retries exceeded".to_string()))
842/// }
843/// ```
844#[derive(Debug, Clone)]
845pub struct EipClient {
846    /// TCP stream for network communication
847    stream: Arc<Mutex<TcpStream>>,
848    /// Session handle for the connection
849    session_handle: u32,
850    /// Connection ID for the session
851    _connection_id: u32,
852    /// Tag manager for handling tag operations
853    tag_manager: Arc<Mutex<TagManager>>,
854    /// UDT manager for handling UDT operations
855    udt_manager: Arc<Mutex<UdtManager>>,
856    /// Whether the client is connected
857    _connected: Arc<AtomicBool>,
858    /// Maximum packet size for communication
859    max_packet_size: u32,
860    /// Last activity timestamp
861    last_activity: Arc<Mutex<Instant>>,
862    /// Session timeout duration
863    _session_timeout: Duration,
864    /// Configuration for batch operations
865    batch_config: BatchConfig,
866    /// Connected session management for Class 3 operations
867    connected_sessions: Arc<Mutex<HashMap<String, ConnectedSession>>>,
868    /// Connection sequence counter
869    connection_sequence: Arc<Mutex<u32>>,
870    /// Active tag subscriptions
871    subscriptions: Arc<Mutex<Vec<TagSubscription>>>,
872}
873
874impl EipClient {
875    pub async fn new(addr: &str) -> Result<Self> {
876        let addr = addr.parse::<SocketAddr>()
877            .map_err(|e| EtherNetIpError::Protocol(format!("Invalid address format: {}", e)))?;
878        let stream = TcpStream::connect(addr).await?;
879        let mut client = Self {
880            stream: Arc::new(Mutex::new(stream)),
881            session_handle: 0,
882            _connection_id: 0,
883            tag_manager: Arc::new(Mutex::new(TagManager::new())),
884            udt_manager: Arc::new(Mutex::new(UdtManager::new())),
885            _connected: Arc::new(AtomicBool::new(false)),
886            max_packet_size: 4000,
887            last_activity: Arc::new(Mutex::new(Instant::now())),
888            _session_timeout: Duration::from_secs(120),
889            batch_config: BatchConfig::default(),
890            connected_sessions: Arc::new(Mutex::new(HashMap::new())),
891            connection_sequence: Arc::new(Mutex::new(1)),
892            subscriptions: Arc::new(Mutex::new(Vec::new())),
893        };
894        client.register_session().await?;
895        Ok(client)
896    }
897
898    /// Public async connect function for EipClient
899    pub async fn connect(addr: &str) -> Result<Self> {
900        Self::new(addr).await
901    }
902    
903    /// Registers an EtherNet/IP session with the PLC
904    /// 
905    /// This is an internal function that implements the EtherNet/IP session
906    /// registration protocol. It sends a Register Session command and
907    /// processes the response to extract the session handle.
908    /// 
909    /// # Protocol Details
910    /// 
911    /// The Register Session command consists of:
912    /// - EtherNet/IP Encapsulation Header (24 bytes)
913    /// - Registration Data (4 bytes: protocol version + options)
914    /// 
915    /// The PLC responds with:
916    /// - Same header format with assigned session handle
917    /// - Status code indicating success/failure
918    /// 
919    /// # Errors
920    /// 
921    /// - Network timeout or disconnection
922    /// - Invalid response format
923    /// - PLC rejection (status code non-zero)
924    async fn register_session(&mut self) -> crate::error::Result<()> {
925        println!("🔌 [DEBUG] Starting session registration...");
926        let packet: [u8; 28] = [
927            0x65, 0x00,             // Command: Register Session (0x0065)
928            0x04, 0x00,             // Length: 4 bytes
929            0x00, 0x00, 0x00, 0x00, // Session Handle: 0 (will be assigned)
930            0x00, 0x00, 0x00, 0x00, // Status: 0
931            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Sender Context (8 bytes)
932            0x00, 0x00, 0x00, 0x00, // Options: 0
933            0x01, 0x00,             // Protocol Version: 1
934            0x00, 0x00,             // Option Flags: 0
935        ];
936
937        println!("📤 [DEBUG] Sending Register Session packet: {:02X?}", packet);
938        self.stream.lock().await.write_all(&packet).await
939            .map_err(|e| {
940                println!("❌ [DEBUG] Failed to send Register Session packet: {}", e);
941                EtherNetIpError::Io(e)
942            })?;
943
944        let mut buf = [0u8; 1024];
945        println!("⏳ [DEBUG] Waiting for Register Session response...");
946        let n = match timeout(Duration::from_secs(5), self.stream.lock().await.read(&mut buf)).await {
947            Ok(Ok(n)) => {
948                println!("📥 [DEBUG] Received {} bytes in response", n);
949                n
950            },
951            Ok(Err(e)) => {
952                println!("❌ [DEBUG] Error reading response: {}", e);
953                return Err(EtherNetIpError::Io(e));
954            },
955            Err(_) => {
956                println!("⏰ [DEBUG] Timeout waiting for response");
957                return Err(EtherNetIpError::Timeout(Duration::from_secs(5)));
958            },
959        };
960
961        if n < 28 {
962            println!("❌ [DEBUG] Response too short: {} bytes (expected 28)", n);
963            return Err(EtherNetIpError::Protocol("Response too short".to_string()));
964        }
965
966        // Extract session handle from response
967        self.session_handle = u32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]);
968        println!("🔑 [DEBUG] Session handle: 0x{:08X}", self.session_handle);
969        
970        // Check status
971        let status = u32::from_le_bytes([buf[8], buf[9], buf[10], buf[11]]);
972        println!("📊 [DEBUG] Status code: 0x{:08X}", status);
973        
974        if status != 0 {
975            println!("❌ [DEBUG] Session registration failed with status: 0x{:08X}", status);
976            return Err(EtherNetIpError::Protocol(format!("Session registration failed with status: 0x{:08X}", status)));
977        }
978
979        println!("✅ [DEBUG] Session registration successful");
980        Ok(())
981    }
982    
983    /// Sets the maximum packet size for communication
984    pub fn set_max_packet_size(&mut self, size: u32) {
985        self.max_packet_size = size.min(4000);
986    }
987    
988    /// Discovers all tags in the PLC
989    pub async fn discover_tags(&mut self) -> crate::error::Result<()> {
990        let response = self.send_cip_request(&self.build_list_tags_request()).await?;
991        let tags = self.tag_manager.lock().await.parse_tag_list(&response)?;
992        let tag_manager = self.tag_manager.lock().await;
993        let mut cache = tag_manager.cache.write().unwrap();
994        for (name, metadata) in tags {
995            cache.insert(name, metadata);
996        }
997        Ok(())
998    }
999    
1000    /// Gets metadata for a tag
1001    pub async fn get_tag_metadata(&self, tag_name: &str) -> Option<TagMetadata> {
1002        let tag_manager = self.tag_manager.lock().await;
1003        let cache = tag_manager.cache.read().unwrap();
1004        let result = cache.get(tag_name).cloned();
1005        result
1006    }
1007    
1008    /// Reads a tag value from the PLC
1009    /// 
1010    /// This function performs a CIP read request for the specified tag.
1011    /// The tag's data type is automatically determined from the PLC's response.
1012    /// 
1013    /// # Arguments
1014    /// 
1015    /// * `tag_name` - The name of the tag to read
1016    /// 
1017    /// # Returns
1018    /// 
1019    /// The tag's value as a `PlcValue` enum
1020    /// 
1021    /// # Examples
1022    /// 
1023    /// ```rust,no_run
1024    /// use rust_ethernet_ip::{EipClient, PlcValue};
1025    /// 
1026    /// #[tokio::main]
1027    /// async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1028    ///     let mut client = EipClient::connect("192.168.1.100:44818").await?;
1029    ///     
1030    ///     // Read different data types
1031    ///     let bool_val = client.read_tag("MotorRunning").await?;
1032    ///     let int_val = client.read_tag("Counter").await?;
1033    ///     let real_val = client.read_tag("Temperature").await?;
1034    ///     
1035    ///     // Handle the result
1036    ///     match bool_val {
1037    ///         PlcValue::Bool(true) => println!("Motor is running"),
1038    ///         PlcValue::Bool(false) => println!("Motor is stopped"),
1039    ///         _ => println!("Unexpected data type"),
1040    ///     }
1041    ///     Ok(())
1042    /// }
1043    /// ```
1044    /// 
1045    /// # Performance
1046    /// 
1047    /// - Latency: 1-5ms typical
1048    /// - Throughput: 1,500+ ops/sec
1049    /// - Network: 1 request/response cycle
1050    /// 
1051    /// # Error Handling
1052    /// 
1053    /// Common errors:
1054    /// - `Protocol`: Tag doesn't exist or invalid format
1055    /// - `Connection`: Lost connection to PLC
1056    /// - `Timeout`: Operation timed out
1057    pub async fn read_tag(&mut self, tag_name: &str) -> crate::error::Result<PlcValue> {
1058        self.validate_session().await?;
1059        // Check if we have metadata for this tag
1060        if let Some(metadata) = self.get_tag_metadata(tag_name).await {
1061            // Handle UDT tags
1062            if metadata.data_type == 0x00A0 {
1063                let data = self.read_tag_raw(tag_name).await?;
1064                return self.udt_manager.lock().await.parse_udt_instance(tag_name, &data);
1065            }
1066        }
1067
1068        // Standard tag reading
1069        let response = self.send_cip_request(&self.build_read_request(tag_name)).await?;
1070        let cip_data = self.extract_cip_from_response(&response)?;
1071        self.parse_cip_response(&cip_data)
1072    }
1073    
1074    /// Writes a value to a PLC tag
1075    /// 
1076    /// This method automatically determines the best communication method based on the data type:
1077    /// - STRING values use unconnected explicit messaging with proper AB STRING format
1078    /// - Other data types use standard unconnected messaging
1079    /// 
1080    /// # Arguments
1081    /// 
1082    /// * `tag_name` - The name of the tag to write to
1083    /// * `value` - The value to write
1084    /// 
1085    /// # Example
1086    /// 
1087    /// ```no_run
1088    /// # async fn example() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1089    /// # let mut client = rust_ethernet_ip::EipClient::connect("192.168.1.100:44818").await?;
1090    /// use rust_ethernet_ip::PlcValue;
1091    /// 
1092    /// client.write_tag("Counter", PlcValue::Dint(42)).await?;
1093    /// client.write_tag("Message", PlcValue::String("Hello PLC".to_string())).await?;
1094    /// # Ok(())
1095    /// # }
1096    /// ```
1097    pub async fn write_tag(&mut self, tag_name: &str, value: PlcValue) -> crate::error::Result<()> {
1098        println!("📝 Writing '{}' to tag '{}'", 
1099                 match &value {
1100                     PlcValue::String(s) => format!("\"{}\"", s),
1101                     _ => format!("{:?}", value),
1102                 }, 
1103                 tag_name);
1104        
1105        // Use specialized AB STRING format for STRING writes (required for proper Allen-Bradley STRING handling)
1106        // All data types including strings now use the standard write path
1107        // The PlcValue::to_bytes() method handles the correct format for each type
1108        
1109        // Use standard unconnected messaging for other data types
1110        let cip_request = self.build_write_request(tag_name, &value)?;
1111        
1112        let response = self.send_cip_request(&cip_request).await?;
1113        
1114        // Check write response for errors - need to extract CIP response first
1115        let cip_response = self.extract_cip_from_response(&response)?;
1116        
1117        if cip_response.len() < 3 {
1118            return Err(EtherNetIpError::Protocol("Write response too short".to_string()));
1119        }
1120        
1121        let service_reply = cip_response[0];  // Should be 0xCD (0x4D + 0x80) for Write Tag reply
1122        let general_status = cip_response[2]; // CIP status code
1123        
1124        println!("🔧 [DEBUG] Write response - Service: 0x{:02X}, Status: 0x{:02X}", service_reply, general_status);
1125        
1126        if general_status != 0x00 {
1127            let error_msg = self.get_cip_error_message(general_status);
1128            println!("❌ [WRITE] CIP Error: {} (0x{:02X})", error_msg, general_status);
1129            return Err(EtherNetIpError::Protocol(format!("CIP Error 0x{:02X}: {}", general_status, error_msg)));
1130        }
1131        
1132        println!("✅ Write operation completed successfully");
1133        Ok(())
1134    }
1135
1136    /// Builds a write request specifically for Allen-Bradley string format 
1137    fn _build_ab_string_write_request(&self, tag_name: &str, value: &PlcValue) -> crate::error::Result<Vec<u8>> {
1138        if let PlcValue::String(string_value) = value {
1139            println!("🔧 [DEBUG] Building correct Allen-Bradley string write request for tag: '{}'", tag_name);
1140            
1141            let mut cip_request = Vec::new();
1142            
1143            // Service: Write Tag Service (0x4D)
1144            cip_request.push(0x4D);
1145            
1146            // Request Path Size (in words)
1147            let tag_bytes = tag_name.as_bytes();
1148            let path_len = if tag_bytes.len() % 2 == 0 { 
1149                tag_bytes.len() + 2 
1150            } else { 
1151                tag_bytes.len() + 3 
1152            } / 2;
1153            cip_request.push(path_len as u8);
1154            
1155            // Request Path
1156            cip_request.push(0x91); // ANSI Extended Symbol
1157            cip_request.push(tag_bytes.len() as u8);
1158            cip_request.extend_from_slice(tag_bytes);
1159            
1160            // Pad to word boundary if needed
1161            if tag_bytes.len() % 2 != 0 {
1162                cip_request.push(0x00);
1163            }
1164            
1165            // Data Type: Allen-Bradley STRING (0x02A0)
1166            cip_request.extend_from_slice(&[0xA0, 0x02]);
1167            
1168            // Element Count (always 1 for single string)
1169            cip_request.extend_from_slice(&[0x01, 0x00]);
1170            
1171            // Build the correct AB STRING structure
1172            let string_bytes = string_value.as_bytes();
1173            let max_len: u16 = 82; // Standard AB STRING max length
1174            let current_len = string_bytes.len().min(max_len as usize) as u16;
1175            
1176            // AB STRING structure:
1177            // - Len (2 bytes) - number of characters used
1178            cip_request.extend_from_slice(&current_len.to_le_bytes());
1179            
1180            // - MaxLen (2 bytes) - maximum characters allowed (typically 82)  
1181            cip_request.extend_from_slice(&max_len.to_le_bytes());
1182            
1183            // - Data[MaxLen] (82 bytes) - the character array, zero-padded
1184            let mut data_array = vec![0u8; max_len as usize];
1185            data_array[..current_len as usize].copy_from_slice(&string_bytes[..current_len as usize]);
1186            cip_request.extend_from_slice(&data_array);
1187            
1188            println!("🔧 [DEBUG] Built correct AB string write request ({} bytes): len={}, maxlen={}, data_len={}", 
1189                     cip_request.len(), current_len, max_len, string_bytes.len());
1190            println!("🔧 [DEBUG] First 32 bytes: {:02X?}", &cip_request[..std::cmp::min(32, cip_request.len())]);
1191            
1192            Ok(cip_request)
1193        } else {
1194            Err(EtherNetIpError::Protocol("Expected string value for Allen-Bradley string write".to_string()))
1195        }
1196    }
1197
1198    /// Builds a CIP Write Tag Service request
1199    /// 
1200    /// This creates the CIP packet for writing a value to a tag.
1201    /// The request includes the service code, tag path, data type, and value.
1202    fn build_write_request(&self, tag_name: &str, value: &PlcValue) -> crate::error::Result<Vec<u8>> {
1203        println!("🔧 [DEBUG] Building write request for tag: '{}'", tag_name);
1204        
1205        // Use Connected Explicit Messaging for consistency
1206        let mut cip_request = Vec::new();
1207        
1208        // Service: Write Tag Service (0x4D)
1209        cip_request.push(0x4D);
1210        
1211        // Request Path Size (in words)
1212        let tag_bytes = tag_name.as_bytes();
1213        let path_len = if tag_bytes.len() % 2 == 0 { 
1214            tag_bytes.len() + 2 
1215        } else { 
1216            tag_bytes.len() + 3 
1217        };
1218        cip_request.push((path_len / 2) as u8);
1219        
1220        // Request Path: ANSI Extended Symbol Segment for tag name
1221        cip_request.push(0x91); // ANSI Extended Symbol Segment
1222        cip_request.push(tag_bytes.len() as u8); // Tag name length
1223        cip_request.extend_from_slice(tag_bytes); // Tag name
1224        
1225        // Pad to even length if necessary
1226        if tag_bytes.len() % 2 != 0 {
1227            cip_request.push(0x00);
1228        }
1229        
1230        // Add data type and element count
1231        let data_type = value.get_data_type();
1232        let value_bytes = value.to_bytes();
1233        
1234        cip_request.extend_from_slice(&data_type.to_le_bytes()); // Data type
1235        cip_request.extend_from_slice(&[0x01, 0x00]); // Element count: 1
1236        cip_request.extend_from_slice(&value_bytes); // Value data
1237        
1238        println!("🔧 [DEBUG] Built CIP write request ({} bytes): {:02X?}", 
1239                 cip_request.len(), cip_request);
1240        Ok(cip_request)
1241    }
1242
1243    /// Builds a raw write request with pre-serialized data
1244    fn build_write_request_raw(&self, tag_name: &str, data: &[u8]) -> crate::error::Result<Vec<u8>> {
1245        let mut request = Vec::new();
1246        
1247        // Write Tag Service
1248        request.push(0x4D);
1249        request.push(0x00);
1250        
1251        // Build tag path  
1252        let tag_path = self.build_tag_path(tag_name);
1253        request.extend(tag_path);
1254        
1255        // Add raw data
1256        request.extend(data);
1257        
1258        Ok(request)
1259    }
1260
1261    /// Builds the CIP tag path for a given tag name
1262    /// 
1263    /// This function converts a human-readable tag name into the binary
1264    /// path format required by the CIP protocol. The path consists of
1265    /// segments that describe how to navigate to the tag in the PLC's
1266    /// tag database.
1267    /// 
1268    /// # Arguments
1269    /// 
1270    /// * `tag_name` - The tag name to convert to a path
1271    /// 
1272    /// # Returns
1273    /// 
1274    /// A vector of bytes representing the CIP path
1275    fn build_tag_path(&self, tag_name: &str) -> Vec<u8> {
1276        // Use simple tag path for now
1277        self.build_simple_tag_path(tag_name)
1278    }
1279    
1280    /// Builds a simple tag path for basic tag names (fallback method)
1281    fn build_simple_tag_path(&self, tag_name: &str) -> Vec<u8> {
1282        let mut path = Vec::new();
1283        let tag_bytes = tag_name.as_bytes();
1284        
1285        // ANSI Extended Symbol Segment
1286        path.push(0x91);
1287        path.push(tag_bytes.len() as u8);
1288        path.extend_from_slice(tag_bytes);
1289        
1290        // Pad to even length if necessary
1291        if (tag_bytes.len() + 1) % 2 != 0 {
1292            path.push(0x00);
1293        }
1294        
1295        path
1296    }
1297
1298    /// Serializes a PlcValue into bytes for transmission
1299    #[allow(dead_code)]
1300    fn serialize_value(&self, value: &PlcValue) -> crate::error::Result<Vec<u8>> {
1301        let mut data = Vec::new();
1302        
1303        match value {
1304            PlcValue::Bool(v) => {
1305                data.extend(&0x00C1u16.to_le_bytes()); // Data type
1306                data.push(if *v { 0xFF } else { 0x00 });
1307            }
1308            PlcValue::Sint(v) => {
1309                data.extend(&0x00C2u16.to_le_bytes()); // Data type
1310                data.extend(&v.to_le_bytes());
1311            }
1312            PlcValue::Int(v) => {
1313                data.extend(&0x00C3u16.to_le_bytes()); // Data type
1314                data.extend(&v.to_le_bytes());
1315            }
1316            PlcValue::Dint(v) => {
1317                data.extend(&0x00C4u16.to_le_bytes()); // Data type
1318                data.extend(&v.to_le_bytes());
1319            }
1320            PlcValue::Lint(v) => {
1321                data.extend(&0x00C5u16.to_le_bytes()); // Data type
1322                data.extend(&v.to_le_bytes());
1323            }
1324            PlcValue::Usint(v) => {
1325                data.extend(&0x00C6u16.to_le_bytes()); // Data type
1326                data.extend(&v.to_le_bytes());
1327            }
1328            PlcValue::Uint(v) => {
1329                data.extend(&0x00C7u16.to_le_bytes()); // Data type
1330                data.extend(&v.to_le_bytes());
1331            }
1332            PlcValue::Udint(v) => {
1333                data.extend(&0x00C8u16.to_le_bytes()); // Data type
1334                data.extend(&v.to_le_bytes());
1335            }
1336            PlcValue::Ulint(v) => {
1337                data.extend(&0x00C9u16.to_le_bytes()); // Data type
1338                data.extend(&v.to_le_bytes());
1339            }
1340            PlcValue::Real(v) => {
1341                data.extend(&0x00CAu16.to_le_bytes()); // Data type
1342                data.extend(&v.to_le_bytes());
1343            }
1344            PlcValue::Lreal(v) => {
1345                data.extend(&0x00CBu16.to_le_bytes()); // Data type
1346                data.extend(&v.to_le_bytes());
1347            }
1348            PlcValue::String(v) => {
1349                data.extend(&0x00CEu16.to_le_bytes()); // Data type - correct Allen-Bradley STRING CIP type
1350                
1351                // Length field (4 bytes as DINT) - number of characters currently used
1352                let length = v.len().min(82) as u32;
1353                data.extend_from_slice(&length.to_le_bytes());
1354                
1355                // String data - the actual characters (no MaxLen field)
1356                let string_bytes = v.as_bytes();
1357                let data_len = string_bytes.len().min(82);
1358                data.extend_from_slice(&string_bytes[..data_len]);
1359                
1360                // Padding to make total data area exactly 82 bytes after length
1361                let remaining_chars = 82 - data_len;
1362                data.extend(vec![0u8; remaining_chars]);
1363            }
1364            PlcValue::Udt(_) => {
1365                // UDT serialization is handled by the UdtManager
1366                // For now, just add placeholder data
1367                data.extend(&0x00A0u16.to_le_bytes()); // UDT type code
1368            }
1369        }
1370        
1371        Ok(data)
1372    }
1373
1374    pub fn build_list_tags_request(&self) -> Vec<u8> {
1375        println!("🔧 [DEBUG] Building list tags request");
1376        
1377        // Use Connected Explicit Messaging for consistency
1378        let mut cip_request = Vec::new();
1379        
1380        // Service: List All Tags Service (0x55)
1381        cip_request.push(0x55);
1382        
1383        // Request Path Size (in words) - 3 words = 6 bytes  
1384        cip_request.push(0x03);
1385        
1386        // Request Path: Class 0x6B (Symbol Object), Instance 1
1387        cip_request.push(0x20); // Class segment identifier
1388        cip_request.push(0x6B); // Symbol Object Class
1389        cip_request.push(0x24); // Instance segment identifier
1390        cip_request.push(0x01); // Instance 1
1391        cip_request.push(0x01); // Attribute segment identifier  
1392        cip_request.push(0x00); // Attribute 0 (tag list)
1393        
1394        println!("🔧 [DEBUG] Built CIP list tags request ({} bytes): {:02X?}", 
1395                 cip_request.len(), cip_request);
1396        
1397        cip_request
1398    }
1399
1400    /// Gets a human-readable error message for a CIP status code
1401    /// 
1402    /// # Arguments
1403    /// 
1404    /// * `status` - The CIP status code to look up
1405    /// 
1406    /// # Returns
1407    /// 
1408    /// A string describing the error
1409    fn get_cip_error_message(&self, status: u8) -> String {
1410        match status {
1411            0x00 => "Success".to_string(),
1412            0x01 => "Connection failure".to_string(),
1413            0x02 => "Resource unavailable".to_string(),
1414            0x03 => "Invalid parameter value".to_string(),
1415            0x04 => "Path segment error".to_string(),
1416            0x05 => "Path destination unknown".to_string(),
1417            0x06 => "Partial transfer".to_string(),
1418            0x07 => "Connection lost".to_string(),
1419            0x08 => "Service not supported".to_string(),
1420            0x09 => "Invalid attribute value".to_string(),
1421            0x0A => "Attribute list error".to_string(),
1422            0x0B => "Already in requested mode/state".to_string(),
1423            0x0C => "Object state conflict".to_string(),
1424            0x0D => "Object already exists".to_string(),
1425            0x0E => "Attribute not settable".to_string(),
1426            0x0F => "Privilege violation".to_string(),
1427            0x10 => "Device state conflict".to_string(),
1428            0x11 => "Reply data too large".to_string(),
1429            0x12 => "Fragmentation of a primitive value".to_string(),
1430            0x13 => "Not enough data".to_string(),
1431            0x14 => "Attribute not supported".to_string(),
1432            0x15 => "Too much data".to_string(),
1433            0x16 => "Object does not exist".to_string(),
1434            0x17 => "Service fragmentation sequence not in progress".to_string(),
1435            0x18 => "No stored attribute data".to_string(),
1436            0x19 => "Store operation failure".to_string(),
1437            0x1A => "Routing failure, request packet too large".to_string(),
1438            0x1B => "Routing failure, response packet too large".to_string(),
1439            0x1C => "Missing attribute list entry data".to_string(),
1440            0x1D => "Invalid attribute value list".to_string(),
1441            0x1E => "Embedded service error".to_string(),
1442            0x1F => "Vendor specific error".to_string(),
1443            0x20 => "Invalid parameter".to_string(),
1444            0x21 => "Write-once value or medium already written".to_string(),
1445            0x22 => "Invalid reply received".to_string(),
1446            0x23 => "Buffer overflow".to_string(),
1447            0x24 => "Invalid message format".to_string(),
1448            0x25 => "Key failure in path".to_string(),
1449            0x26 => "Path size invalid".to_string(),
1450            0x27 => "Unexpected attribute in list".to_string(),
1451            0x28 => "Invalid member ID".to_string(),
1452            0x29 => "Member not settable".to_string(),
1453            0x2A => "Group 2 only server general failure".to_string(),
1454            0x2B => "Unknown Modbus error".to_string(),
1455            0x2C => "Attribute not gettable".to_string(),
1456            _ => format!("Unknown CIP error code: 0x{:02X}", status)
1457        }
1458    }
1459
1460    async fn validate_session(&mut self) -> crate::error::Result<()> {
1461        let time_since_activity = self.last_activity.lock().await.elapsed();
1462        
1463        // Send keep-alive if it's been more than 30 seconds since last activity
1464        if time_since_activity > Duration::from_secs(30) {
1465            self.send_keep_alive().await?;
1466        }
1467        
1468        Ok(())
1469    }
1470
1471    async fn send_keep_alive(&mut self) -> crate::error::Result<()> {
1472        let packet = vec![
1473            0x6F, 0x00, // Command: SendRRData
1474            0x00, 0x00, // Length: 0
1475        ];
1476        
1477        let mut stream = self.stream.lock().await;
1478        stream.write_all(&packet).await?;
1479        *self.last_activity.lock().await = Instant::now();
1480        Ok(())
1481    }
1482
1483    /// Checks the health of the connection
1484    pub async fn check_health(&self) -> bool {
1485        // Check if we have a valid session handle and recent activity
1486        self.session_handle != 0 && self.last_activity.lock().await.elapsed() < Duration::from_secs(150)
1487    }
1488    
1489    /// Performs a more thorough health check by actually communicating with the PLC
1490    pub async fn check_health_detailed(&mut self) -> crate::error::Result<bool> {
1491        if self.session_handle == 0 {
1492            return Ok(false);
1493        }
1494        
1495        // Try sending a lightweight keep-alive command
1496        match self.send_keep_alive().await {
1497            Ok(()) => Ok(true),
1498            Err(_) => {
1499                // If keep-alive fails, try re-registering the session
1500                match self.register_session().await {
1501                    Ok(()) => Ok(true),
1502                    Err(_) => Ok(false),
1503                }
1504            }
1505        }
1506    }
1507
1508    /// Reads raw data from a tag
1509    async fn read_tag_raw(&mut self, tag_name: &str) -> crate::error::Result<Vec<u8>> {
1510        let response = self.send_cip_request(&self.build_read_request(tag_name)).await?;
1511        Ok(self.extract_cip_from_response(&response)?)
1512    }
1513
1514    /// Writes raw data to a tag
1515    #[allow(dead_code)]
1516    async fn write_tag_raw(&mut self, tag_name: &str, data: &[u8]) -> crate::error::Result<()> {
1517        let request = self.build_write_request_raw(tag_name, data)?;
1518        let response = self.send_cip_request(&request).await?;
1519        
1520        // Check write response for errors
1521        let cip_response = self.extract_cip_from_response(&response)?;
1522        
1523        if cip_response.len() < 3 {
1524            return Err(EtherNetIpError::Protocol("Write response too short".to_string()));
1525        }
1526        
1527        let service_reply = cip_response[0];  // Should be 0xCD (0x4D + 0x80) for Write Tag reply
1528        let general_status = cip_response[2]; // CIP status code
1529        
1530        println!("🔧 [DEBUG] Write response - Service: 0x{:02X}, Status: 0x{:02X}", service_reply, general_status);
1531        
1532        if general_status != 0x00 {
1533            let error_msg = self.get_cip_error_message(general_status);
1534            println!("❌ [WRITE] CIP Error: {} (0x{:02X})", error_msg, general_status);
1535            return Err(EtherNetIpError::Protocol(format!("CIP Error 0x{:02X}: {}", general_status, error_msg)));
1536        }
1537        
1538        println!("✅ Write completed successfully");
1539        Ok(())
1540    }
1541
1542    /// Sends a CIP request wrapped in EtherNet/IP SendRRData command
1543    pub async fn send_cip_request(&self, cip_request: &[u8]) -> Result<Vec<u8>> {
1544        println!("🔧 [DEBUG] Sending CIP request ({} bytes): {:02X?}", cip_request.len(), cip_request);
1545        
1546        // Calculate total packet size
1547        let cip_data_size = cip_request.len();
1548        let total_data_len = 4 + 2 + 2 + 8 + cip_data_size; // Interface + Timeout + Count + Items + CIP
1549        
1550        let mut packet = Vec::new();
1551        
1552        // EtherNet/IP header (24 bytes)
1553        packet.extend_from_slice(&[0x6F, 0x00]); // Command: Send RR Data (0x006F)
1554        packet.extend_from_slice(&(total_data_len as u16).to_le_bytes()); // Length
1555        packet.extend_from_slice(&self.session_handle.to_le_bytes()); // Session handle
1556        packet.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); // Status
1557        packet.extend_from_slice(&[0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]); // Context
1558        packet.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); // Options
1559        
1560        // CPF (Common Packet Format) data
1561        packet.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); // Interface handle
1562        packet.extend_from_slice(&[0x05, 0x00]); // Timeout (5 seconds)
1563        packet.extend_from_slice(&[0x02, 0x00]); // Item count: 2
1564        
1565        // Item 1: Null Address Item (0x0000)
1566        packet.extend_from_slice(&[0x00, 0x00]); // Type: Null Address
1567        packet.extend_from_slice(&[0x00, 0x00]); // Length: 0
1568        
1569        // Item 2: Unconnected Data Item (0x00B2)
1570        packet.extend_from_slice(&[0xB2, 0x00]); // Type: Unconnected Data
1571        packet.extend_from_slice(&(cip_data_size as u16).to_le_bytes()); // Length
1572        
1573        // Add CIP request data
1574        packet.extend_from_slice(cip_request);
1575        
1576        println!("🔧 [DEBUG] Built packet ({} bytes): {:02X?}", packet.len(), &packet[..std::cmp::min(64, packet.len())]);
1577        
1578        // Send packet with timeout
1579        let mut stream = self.stream.lock().await;
1580        stream.write_all(&packet).await
1581            .map_err(|e| EtherNetIpError::Io(e))?;
1582        
1583        // Read response header with timeout
1584        let mut header = [0u8; 24];
1585        match timeout(Duration::from_secs(10), stream.read_exact(&mut header)).await {
1586            Ok(Ok(_)) => {},
1587            Ok(Err(e)) => return Err(EtherNetIpError::Io(e)),
1588            Err(_) => return Err(EtherNetIpError::Timeout(Duration::from_secs(10))),
1589        }
1590        
1591        // Check EtherNet/IP command status
1592        let cmd_status = u32::from_le_bytes([header[8], header[9], header[10], header[11]]);
1593        if cmd_status != 0 {
1594            return Err(EtherNetIpError::Protocol(format!("EIP Command failed. Status: 0x{:08X}", cmd_status)));
1595        }
1596        
1597        // Parse response length
1598        let response_length = u16::from_le_bytes([header[2], header[3]]) as usize;
1599        if response_length == 0 {
1600            return Ok(Vec::new());
1601        }
1602        
1603        // Read response data with timeout
1604        let mut response_data = vec![0u8; response_length];
1605        match timeout(Duration::from_secs(10), stream.read_exact(&mut response_data)).await {
1606            Ok(Ok(_)) => {},
1607            Ok(Err(e)) => return Err(EtherNetIpError::Io(e)),
1608            Err(_) => return Err(EtherNetIpError::Timeout(Duration::from_secs(10))),
1609        }
1610        
1611        // Update last activity time
1612        *self.last_activity.lock().await = Instant::now();
1613        
1614        println!("🔧 [DEBUG] Received response ({} bytes): {:02X?}", 
1615                 response_data.len(), &response_data[..std::cmp::min(32, response_data.len())]);
1616        
1617        Ok(response_data)
1618    }
1619
1620    /// Extracts CIP data from EtherNet/IP response packet
1621    fn extract_cip_from_response(&self, response: &[u8]) -> crate::error::Result<Vec<u8>> {
1622        println!("🔧 [DEBUG] Extracting CIP from response ({} bytes): {:02X?}", 
1623                 response.len(), &response[..std::cmp::min(32, response.len())]);
1624        
1625        // Parse CPF (Common Packet Format) structure directly from response data
1626        // Response format: [Interface(4)] [Timeout(2)] [ItemCount(2)] [Items...]
1627        
1628        if response.len() < 8 {
1629            return Err(EtherNetIpError::Protocol("Response too short for CPF header".to_string()));
1630        }
1631
1632        // Skip interface handle (4 bytes) and timeout (2 bytes)
1633        let mut pos = 6;
1634        
1635        // Read item count
1636        let item_count = u16::from_le_bytes([response[pos], response[pos+1]]);
1637        pos += 2;
1638        println!("🔧 [DEBUG] CPF item count: {}", item_count);
1639
1640        // Process items
1641        for i in 0..item_count {
1642            if pos + 4 > response.len() {
1643                return Err(EtherNetIpError::Protocol("Response truncated while parsing items".to_string()));
1644            }
1645
1646            let item_type = u16::from_le_bytes([response[pos], response[pos + 1]]);
1647            let item_length = u16::from_le_bytes([response[pos + 2], response[pos + 3]]) as usize;
1648            pos += 4; // Skip item header
1649
1650            println!("🔧 [DEBUG] Item {}: type=0x{:04X}, length={}", i, item_type, item_length);
1651
1652            if item_type == 0x00B2 { // Unconnected Data Item
1653                if pos + item_length > response.len() {
1654                    return Err(EtherNetIpError::Protocol("Data item truncated".to_string()));
1655                }
1656
1657                let cip_data = response[pos..pos + item_length].to_vec();
1658                println!("🔧 [DEBUG] Found Unconnected Data Item, extracted CIP data ({} bytes)", cip_data.len());
1659                println!("🔧 [DEBUG] CIP data bytes: {:02X?}", &cip_data[..std::cmp::min(16, cip_data.len())]);
1660                return Ok(cip_data);
1661            } else {
1662                // Skip this item's data
1663                pos += item_length;
1664            }
1665        }
1666
1667        Err(EtherNetIpError::Protocol("No Unconnected Data Item (0x00B2) found in response".to_string()))
1668    }
1669
1670    /// Parses CIP response and converts to PlcValue
1671    fn parse_cip_response(&self, cip_response: &[u8]) -> crate::error::Result<PlcValue> {
1672        println!("🔧 [DEBUG] Parsing CIP response ({} bytes): {:02X?}", cip_response.len(), cip_response);
1673        
1674        if cip_response.len() < 2 {
1675            return Err(EtherNetIpError::Protocol("CIP response too short".to_string()));
1676        }
1677
1678        let service_reply = cip_response[0];    // Should be 0xCC (0x4C + 0x80) for Read Tag reply
1679        let general_status = cip_response[2];   // CIP status code
1680        
1681        println!("🔧 [DEBUG] Service reply: 0x{:02X}, Status: 0x{:02X}", 
1682                 service_reply, general_status);
1683
1684        // Check for CIP errors  
1685        if general_status != 0x00 {
1686            let error_msg = self.get_cip_error_message(general_status);
1687            println!("🔧 [DEBUG] CIP Error - Status: 0x{:02X}, Message: {}", general_status, error_msg);
1688            return Err(EtherNetIpError::Protocol(format!("CIP Error {}: {}", general_status, error_msg)));
1689        }
1690
1691        // For read operations, parse the returned data
1692        if service_reply == 0xCC { // Read Tag reply
1693            if cip_response.len() < 6 {
1694                return Err(EtherNetIpError::Protocol("Read response too short for data".to_string()));
1695            }
1696            
1697            let data_type = u16::from_le_bytes([cip_response[4], cip_response[5]]);
1698            let value_data = &cip_response[6..];
1699            
1700            println!("🔧 [DEBUG] Data type: 0x{:04X}, Value data ({} bytes): {:02X?}", 
1701                     data_type, value_data.len(), value_data);
1702            
1703            // Parse based on data type
1704            match data_type {
1705                0x00C1 => { // BOOL
1706                    if value_data.is_empty() {
1707                        return Err(EtherNetIpError::Protocol("No data for BOOL value".to_string()));
1708                    }
1709                    let value = value_data[0] != 0;
1710                    println!("🔧 [DEBUG] Parsed BOOL: {}", value);
1711                    Ok(PlcValue::Bool(value))
1712                }
1713                0x00C2 => { // SINT
1714                    if value_data.is_empty() {
1715                        return Err(EtherNetIpError::Protocol("No data for SINT value".to_string()));
1716                    }
1717                    let value = value_data[0] as i8;
1718                    println!("🔧 [DEBUG] Parsed SINT: {}", value);
1719                    Ok(PlcValue::Sint(value))
1720                }
1721                0x00C3 => { // INT
1722                    if value_data.len() < 2 {
1723                        return Err(EtherNetIpError::Protocol("Insufficient data for INT value".to_string()));
1724                    }
1725                    let value = i16::from_le_bytes([value_data[0], value_data[1]]);
1726                    println!("🔧 [DEBUG] Parsed INT: {}", value);
1727                    Ok(PlcValue::Int(value))
1728                }
1729                0x00C4 => { // DINT
1730                    if value_data.len() < 4 {
1731                        return Err(EtherNetIpError::Protocol("Insufficient data for DINT value".to_string()));
1732                    }
1733                    let value = i32::from_le_bytes([
1734                        value_data[0], value_data[1], 
1735                        value_data[2], value_data[3]
1736                    ]);
1737                    println!("🔧 [DEBUG] Parsed DINT: {}", value);
1738                    Ok(PlcValue::Dint(value))
1739                }
1740                0x00CA => { // REAL
1741                    if value_data.len() < 4 {
1742                        return Err(EtherNetIpError::Protocol("Insufficient data for REAL value".to_string()));
1743                    }
1744                    let value = f32::from_le_bytes([
1745                        value_data[0], value_data[1],
1746                        value_data[2], value_data[3]
1747                    ]);
1748                    println!("🔧 [DEBUG] Parsed REAL: {}", value);
1749                    Ok(PlcValue::Real(value))
1750                }
1751                0x00DA => { // STRING
1752                    if value_data.is_empty() {
1753                        return Ok(PlcValue::String(String::new()));
1754                    }
1755                    let length = value_data[0] as usize;
1756                    if value_data.len() < 1 + length {
1757                        return Err(EtherNetIpError::Protocol("Insufficient data for STRING value".to_string()));
1758                    }
1759                    let string_data = &value_data[1..1 + length];
1760                    let value = String::from_utf8_lossy(string_data).to_string();
1761                    println!("🔧 [DEBUG] Parsed STRING: '{}'", value);
1762                    Ok(PlcValue::String(value))
1763                }
1764                0x02A0 => { // Alternative STRING type (Allen-Bradley specific)
1765                    if value_data.len() < 7 {
1766                        return Err(EtherNetIpError::Protocol("Insufficient data for alternative STRING value".to_string()));
1767                    }
1768                    
1769                    // For this format, the string data starts directly at position 6
1770                    // We need to find the null terminator or use the full remaining length
1771                    let string_start = 6;
1772                    let string_data = &value_data[string_start..];
1773                    
1774                    // Find null terminator or use full length
1775                    let string_end = string_data.iter().position(|&b| b == 0).unwrap_or(string_data.len());
1776                    let string_bytes = &string_data[..string_end];
1777                    
1778                    let value = String::from_utf8_lossy(string_bytes).to_string();
1779                    println!("🔧 [DEBUG] Parsed alternative STRING (0x02A0): '{}'", value);
1780                    Ok(PlcValue::String(value))
1781                }
1782                _ => {
1783                    println!("🔧 [DEBUG] Unknown data type: 0x{:04X}", data_type);
1784                    Err(EtherNetIpError::Protocol(format!("Unsupported data type: 0x{:04X}", data_type)))
1785                }
1786            }
1787        } else if service_reply == 0xCD { // Write Tag reply - no data to parse
1788            println!("🔧 [DEBUG] Write operation successful");
1789            Ok(PlcValue::Bool(true)) // Indicate success
1790        } else {
1791            Err(EtherNetIpError::Protocol(format!("Unknown service reply: 0x{:02X}", service_reply)))
1792        }
1793    }
1794
1795    /// Unregisters the EtherNet/IP session with the PLC
1796    pub async fn unregister_session(&mut self) -> crate::error::Result<()> {
1797        println!("🔌 Unregistering session and cleaning up connections...");
1798        
1799        // Close all connected sessions first
1800        let _ = self.close_all_connected_sessions().await;
1801        
1802        let mut packet = Vec::new();
1803        
1804        // EtherNet/IP header
1805        packet.extend_from_slice(&[0x66, 0x00]); // Command: Unregister Session
1806        packet.extend_from_slice(&[0x04, 0x00]); // Length: 4 bytes
1807        packet.extend_from_slice(&self.session_handle.to_le_bytes()); // Session handle
1808        packet.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); // Status
1809        packet.extend_from_slice(&[0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]); // Sender context
1810        packet.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); // Options
1811        
1812        // Protocol version for unregister session
1813        packet.extend_from_slice(&[0x01, 0x00, 0x00, 0x00]); // Protocol version 1
1814        
1815        self.stream.lock().await.write_all(&packet).await.map_err(EtherNetIpError::Io)?;
1816        
1817        println!("✅ Session unregistered and all connections closed");
1818        Ok(())
1819    }
1820
1821    /// Builds a CIP Read Tag Service request
1822    fn build_read_request(&self, tag_name: &str) -> Vec<u8> {
1823        println!("🔧 [DEBUG] Building read request for tag: '{}'", tag_name);
1824        
1825        // Use Connected Explicit Messaging for better compatibility
1826        // This is simpler and more widely supported across different PLC types
1827        let mut cip_request = Vec::new();
1828        
1829        // Service: Read Tag Service (0x4C)
1830        cip_request.push(0x4C);
1831        
1832        // Request Path Size (in words)
1833        let tag_bytes = tag_name.as_bytes();
1834        let path_len = if tag_bytes.len() % 2 == 0 { 
1835            tag_bytes.len() + 2 
1836        } else { 
1837            tag_bytes.len() + 3 
1838        };
1839        cip_request.push((path_len / 2) as u8);
1840        
1841        // Request Path: ANSI Extended Symbol Segment for tag name
1842        cip_request.push(0x91); // ANSI Extended Symbol Segment
1843        cip_request.push(tag_bytes.len() as u8); // Tag name length
1844        cip_request.extend_from_slice(tag_bytes); // Tag name
1845        
1846        // Pad to even length if necessary
1847        if tag_bytes.len() % 2 != 0 {
1848            cip_request.push(0x00);
1849        }
1850        
1851        // Element count (little-endian)
1852        cip_request.extend_from_slice(&[0x01, 0x00]); // Read 1 element
1853        
1854        println!("🔧 [DEBUG] Built CIP read request ({} bytes): {:02X?}", 
1855                 cip_request.len(), cip_request);
1856        
1857        cip_request
1858    }
1859
1860    // =========================================================================
1861    // BATCH OPERATIONS IMPLEMENTATION
1862    // =========================================================================
1863
1864    /// Executes a batch of read and write operations
1865    /// 
1866    /// This is the main entry point for batch operations. It takes a slice of
1867    /// `BatchOperation` items and executes them efficiently by grouping them
1868    /// into optimal CIP packets based on the current `BatchConfig`.
1869    /// 
1870    /// # Arguments
1871    /// 
1872    /// * `operations` - A slice of operations to execute
1873    /// 
1874    /// # Returns
1875    /// 
1876    /// A vector of `BatchResult` items, one for each input operation.
1877    /// Results are returned in the same order as the input operations.
1878    /// 
1879    /// # Performance
1880    /// 
1881    /// - **Throughput**: 5,000-15,000+ operations/second (vs 1,500 individual)
1882    /// - **Latency**: 5-20ms per batch (vs 1-3ms per individual operation)
1883    /// - **Network efficiency**: 1-5 packets vs N packets for N operations
1884    /// 
1885    /// # Examples
1886    /// 
1887    /// ```rust,no_run
1888    /// use rust_ethernet_ip::{EipClient, BatchOperation, PlcValue};
1889    /// 
1890    /// #[tokio::main]
1891    /// async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1892    ///     let mut client = EipClient::connect("192.168.1.100:44818").await?;
1893    ///     
1894    ///     let operations = vec![
1895    ///         BatchOperation::Read { tag_name: "Motor1_Speed".to_string() },
1896    ///         BatchOperation::Read { tag_name: "Motor2_Speed".to_string() },
1897    ///         BatchOperation::Write { 
1898    ///             tag_name: "SetPoint".to_string(), 
1899    ///             value: PlcValue::Dint(1500) 
1900    ///         },
1901    ///     ];
1902    ///     
1903    ///     let results = client.execute_batch(&operations).await?;
1904    ///     
1905    ///     for result in results {
1906    ///         match result.result {
1907    ///             Ok(Some(value)) => println!("Read value: {:?}", value),
1908    ///             Ok(None) => println!("Write successful"),
1909    ///             Err(e) => println!("Operation failed: {}", e),
1910    ///         }
1911    ///     }
1912    ///     
1913    ///     Ok(())
1914    /// }
1915    /// ```
1916    pub async fn execute_batch(&mut self, operations: &[BatchOperation]) -> crate::error::Result<Vec<BatchResult>> {
1917        if operations.is_empty() {
1918            return Ok(Vec::new());
1919        }
1920
1921        let start_time = Instant::now();
1922        println!("🚀 [BATCH] Starting batch execution with {} operations", operations.len());
1923
1924        // Group operations based on configuration
1925        let operation_groups = if self.batch_config.optimize_packet_packing {
1926            self.optimize_operation_groups(operations)
1927        } else {
1928            self.sequential_operation_groups(operations)
1929        };
1930
1931        let mut all_results = Vec::with_capacity(operations.len());
1932
1933        // Execute each group
1934        for (group_index, group) in operation_groups.iter().enumerate() {
1935            println!("🔧 [BATCH] Processing group {} with {} operations", group_index + 1, group.len());
1936            
1937            match self.execute_operation_group(group).await {
1938                Ok(mut group_results) => {
1939                    all_results.append(&mut group_results);
1940                }
1941                Err(e) => {
1942                    if !self.batch_config.continue_on_error {
1943                        return Err(e);
1944                    }
1945                    
1946                    // Create error results for this group
1947                    for op in group {
1948                        let error_result = BatchResult {
1949                            operation: op.clone(),
1950                            result: Err(BatchError::NetworkError(e.to_string())),
1951                            execution_time_us: 0,
1952                        };
1953                        all_results.push(error_result);
1954                    }
1955                }
1956            }
1957        }
1958
1959        let total_time = start_time.elapsed();
1960        println!("✅ [BATCH] Completed batch execution in {:?} - {} operations processed", 
1961                 total_time, all_results.len());
1962
1963        Ok(all_results)
1964    }
1965
1966    /// Reads multiple tags in a single batch operation
1967    /// 
1968    /// This is a convenience method for read-only batch operations.
1969    /// It's optimized for reading many tags at once.
1970    /// 
1971    /// # Arguments
1972    /// 
1973    /// * `tag_names` - A slice of tag names to read
1974    /// 
1975    /// # Returns
1976    /// 
1977    /// A vector of tuples containing (tag_name, result) pairs
1978    /// 
1979    /// # Examples
1980    /// 
1981    /// ```rust,no_run
1982    /// use rust_ethernet_ip::EipClient;
1983    /// 
1984    /// #[tokio::main]
1985    /// async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1986    ///     let mut client = EipClient::connect("192.168.1.100:44818").await?;
1987    ///     
1988    ///     let tags = ["Motor1_Speed", "Motor2_Speed", "Temperature", "Pressure"];
1989    ///     let results = client.read_tags_batch(&tags).await?;
1990    ///     
1991    ///     for (tag_name, result) in results {
1992    ///         match result {
1993    ///             Ok(value) => println!("{}: {:?}", tag_name, value),
1994    ///             Err(e) => println!("{}: Error - {}", tag_name, e),
1995    ///         }
1996    ///     }
1997    ///     
1998    ///     Ok(())
1999    /// }
2000    /// ```
2001    pub async fn read_tags_batch(&mut self, tag_names: &[&str]) -> crate::error::Result<Vec<(String, std::result::Result<PlcValue, BatchError>)>> {
2002        let operations: Vec<BatchOperation> = tag_names
2003            .iter()
2004            .map(|&name| BatchOperation::Read { tag_name: name.to_string() })
2005            .collect();
2006
2007        let results = self.execute_batch(&operations).await?;
2008        
2009        Ok(results.into_iter().map(|result| {
2010            let tag_name = match &result.operation {
2011                BatchOperation::Read { tag_name } => tag_name.clone(),
2012                _ => unreachable!("Should only have read operations"),
2013            };
2014            
2015            let value_result = match result.result {
2016                Ok(Some(value)) => Ok(value),
2017                Ok(None) => Err(BatchError::Other("Unexpected None result for read operation".to_string())),
2018                Err(e) => Err(e),
2019            };
2020            
2021            (tag_name, value_result)
2022        }).collect())
2023    }
2024
2025    /// Writes multiple tag values in a single batch operation
2026    /// 
2027    /// This is a convenience method for write-only batch operations.
2028    /// It's optimized for writing many values at once.
2029    /// 
2030    /// # Arguments
2031    /// 
2032    /// * `tag_values` - A slice of (tag_name, value) tuples to write
2033    /// 
2034    /// # Returns
2035    /// 
2036    /// A vector of tuples containing (tag_name, result) pairs
2037    /// 
2038    /// # Examples
2039    /// 
2040    /// ```rust,no_run
2041    /// use rust_ethernet_ip::{EipClient, PlcValue};
2042    /// 
2043    /// #[tokio::main]
2044    /// async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
2045    ///     let mut client = EipClient::connect("192.168.1.100:44818").await?;
2046    ///     
2047    ///     let writes = vec![
2048    ///         ("SetPoint1", PlcValue::Bool(true)),
2049    ///         ("SetPoint2", PlcValue::Dint(2000)),
2050    ///         ("EnableFlag", PlcValue::Bool(true)),
2051    ///     ];
2052    ///     
2053    ///     let results = client.write_tags_batch(&writes).await?;
2054    ///     
2055    ///     for (tag_name, result) in results {
2056    ///         match result {
2057    ///             Ok(_) => println!("{}: Write successful", tag_name),
2058    ///             Err(e) => println!("{}: Write failed - {}", tag_name, e),
2059    ///         }
2060    ///     }
2061    ///     
2062    ///     Ok(())
2063    /// }
2064    /// ```
2065    pub async fn write_tags_batch(&mut self, tag_values: &[(&str, PlcValue)]) -> crate::error::Result<Vec<(String, std::result::Result<(), BatchError>)>> {
2066        let operations: Vec<BatchOperation> = tag_values
2067            .iter()
2068            .map(|(name, value)| BatchOperation::Write { 
2069                tag_name: name.to_string(), 
2070                value: value.clone() 
2071            })
2072            .collect();
2073
2074        let results = self.execute_batch(&operations).await?;
2075        
2076        Ok(results.into_iter().map(|result| {
2077            let tag_name = match &result.operation {
2078                BatchOperation::Write { tag_name, .. } => tag_name.clone(),
2079                _ => unreachable!("Should only have write operations"),
2080            };
2081            
2082            let write_result = match result.result {
2083                Ok(None) => Ok(()),
2084                Ok(Some(_)) => Err(BatchError::Other("Unexpected value result for write operation".to_string())),
2085                Err(e) => Err(e),
2086            };
2087            
2088            (tag_name, write_result)
2089        }).collect())
2090    }
2091
2092    /// Configures batch operation settings
2093    /// 
2094    /// This method allows fine-tuning of batch operation behavior,
2095    /// including performance optimizations and error handling.
2096    /// 
2097    /// # Arguments
2098    /// 
2099    /// * `config` - The new batch configuration to use
2100    /// 
2101    /// # Examples
2102    /// 
2103    /// ```rust,no_run
2104    /// use rust_ethernet_ip::{EipClient, BatchConfig};
2105    /// 
2106    /// #[tokio::main]
2107    /// async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
2108    ///     let mut client = EipClient::connect("192.168.1.100:44818").await?;
2109    ///     
2110    ///     let config = BatchConfig {
2111    ///         max_operations_per_packet: 50,
2112    ///         max_packet_size: 1500,
2113    ///         packet_timeout_ms: 5000,
2114    ///         continue_on_error: false,
2115    ///         optimize_packet_packing: true,
2116    ///     };
2117    ///     
2118    ///     client.configure_batch_operations(config);
2119    ///     
2120    ///     Ok(())
2121    /// }
2122    /// ```
2123    pub fn configure_batch_operations(&mut self, config: BatchConfig) {
2124        self.batch_config = config;
2125        println!("🔧 [BATCH] Updated batch configuration: max_ops={}, max_size={}, timeout={}ms", 
2126                 self.batch_config.max_operations_per_packet,
2127                 self.batch_config.max_packet_size,
2128                 self.batch_config.packet_timeout_ms);
2129    }
2130
2131    /// Gets current batch operation configuration
2132    pub fn get_batch_config(&self) -> &BatchConfig {
2133        &self.batch_config
2134    }
2135
2136    // =========================================================================
2137    // INTERNAL BATCH OPERATION HELPERS
2138    // =========================================================================
2139
2140    /// Groups operations optimally for batch processing
2141    fn optimize_operation_groups(&self, operations: &[BatchOperation]) -> Vec<Vec<BatchOperation>> {
2142        let mut groups = Vec::new();
2143        let mut reads = Vec::new();
2144        let mut writes = Vec::new();
2145
2146        // Separate reads and writes
2147        for op in operations {
2148            match op {
2149                BatchOperation::Read { .. } => reads.push(op.clone()),
2150                BatchOperation::Write { .. } => writes.push(op.clone()),
2151            }
2152        }
2153
2154        // Group reads
2155        for chunk in reads.chunks(self.batch_config.max_operations_per_packet) {
2156            groups.push(chunk.to_vec());
2157        }
2158
2159        // Group writes
2160        for chunk in writes.chunks(self.batch_config.max_operations_per_packet) {
2161            groups.push(chunk.to_vec());
2162        }
2163
2164        groups
2165    }
2166
2167    /// Groups operations sequentially (preserves order)
2168    fn sequential_operation_groups(&self, operations: &[BatchOperation]) -> Vec<Vec<BatchOperation>> {
2169        operations
2170            .chunks(self.batch_config.max_operations_per_packet)
2171            .map(|chunk| chunk.to_vec())
2172            .collect()
2173    }
2174
2175    /// Executes a single group of operations as a CIP Multiple Service Packet
2176    async fn execute_operation_group(&mut self, operations: &[BatchOperation]) -> crate::error::Result<Vec<BatchResult>> {
2177        let start_time = Instant::now();
2178        let mut results = Vec::with_capacity(operations.len());
2179
2180        // Build Multiple Service Packet request
2181        let cip_request = self.build_multiple_service_packet(operations)?;
2182        
2183        // Send request and get response
2184        let response = self.send_cip_request(&cip_request).await?;
2185        
2186        // Parse response and create results
2187        let parsed_results = self.parse_multiple_service_response(&response, operations)?;
2188        
2189        let execution_time = start_time.elapsed();
2190        
2191        // Create BatchResult objects
2192        for (i, operation) in operations.iter().enumerate() {
2193            let op_execution_time = execution_time.as_micros() as u64 / operations.len() as u64;
2194            
2195            let result = if i < parsed_results.len() {
2196                match &parsed_results[i] {
2197                    Ok(value) => Ok(value.clone()),
2198                    Err(e) => Err(e.clone()),
2199                }
2200            } else {
2201                Err(BatchError::Other("Missing result from response".to_string()))
2202            };
2203            
2204            results.push(BatchResult {
2205                operation: operation.clone(),
2206                result,
2207                execution_time_us: op_execution_time,
2208            });
2209        }
2210
2211        Ok(results)
2212    }
2213
2214    /// Builds a CIP Multiple Service Packet request
2215    fn build_multiple_service_packet(&self, operations: &[BatchOperation]) -> crate::error::Result<Vec<u8>> {
2216        let mut packet = Vec::new();
2217
2218        // Multiple Service Packet service code
2219        packet.push(0x0A);
2220
2221        // Request path (2 bytes for class 0x02, instance 1)
2222        packet.push(0x02); // Path size in words
2223        packet.push(0x20); // Class segment
2224        packet.push(0x02); // Class 0x02 (Message Router)
2225        packet.push(0x24); // Instance segment  
2226        packet.push(0x01); // Instance 1
2227
2228        // Number of services
2229        packet.extend_from_slice(&(operations.len() as u16).to_le_bytes());
2230
2231        // Calculate offset table
2232        let mut service_requests = Vec::new();
2233        let mut current_offset = 2 + (operations.len() * 2); // Start after offset table
2234
2235        for operation in operations {
2236            // Build individual service request
2237            let service_request = match operation {
2238                BatchOperation::Read { tag_name } => {
2239                    self.build_read_request(tag_name)
2240                }
2241                BatchOperation::Write { tag_name, value } => {
2242                    self.build_write_request(tag_name, value)?
2243                }
2244            };
2245
2246            service_requests.push(service_request);
2247        }
2248
2249        // Add offset table
2250        for service_request in &service_requests {
2251            packet.extend_from_slice(&(current_offset as u16).to_le_bytes());
2252            current_offset += service_request.len();
2253        }
2254
2255        // Add service requests
2256        for service_request in service_requests {
2257            packet.extend_from_slice(&service_request);
2258        }
2259
2260        println!("🔧 [BATCH] Built Multiple Service Packet ({} bytes, {} services)", 
2261                 packet.len(), operations.len());
2262
2263        Ok(packet)
2264    }
2265
2266    /// Parses a Multiple Service Packet response
2267    fn parse_multiple_service_response(&self, response: &[u8], operations: &[BatchOperation]) -> crate::error::Result<Vec<std::result::Result<Option<PlcValue>, BatchError>>> {
2268        if response.len() < 6 {
2269            return Err(crate::error::EtherNetIpError::Protocol("Response too short for Multiple Service Packet".to_string()));
2270        }
2271
2272        let mut results = Vec::new();
2273
2274        println!("🔧 [DEBUG] Raw Multiple Service Response ({} bytes): {:02X?}", response.len(), response);
2275
2276        // First, extract the CIP data from the EtherNet/IP response
2277        let cip_data = match self.extract_cip_from_response(response) {
2278            Ok(data) => data,
2279            Err(e) => {
2280                println!("🔧 [DEBUG] Failed to extract CIP data: {}", e);
2281                return Err(e);
2282            }
2283        };
2284
2285        println!("🔧 [DEBUG] Extracted CIP data ({} bytes): {:02X?}", cip_data.len(), cip_data);
2286
2287        if cip_data.len() < 6 {
2288            return Err(crate::error::EtherNetIpError::Protocol("CIP data too short for Multiple Service Response".to_string()));
2289        }
2290
2291        // Parse Multiple Service Response header from CIP data:
2292        // [0] = Service Code (0x8A)
2293        // [1] = Reserved (0x00)  
2294        // [2] = General Status (0x00 for success)
2295        // [3] = Additional Status Size (0x00)
2296        // [4-5] = Number of replies (little endian)
2297        
2298        let service_code = cip_data[0];
2299        let general_status = cip_data[2];
2300        let num_replies = u16::from_le_bytes([cip_data[4], cip_data[5]]) as usize;
2301
2302        println!("🔧 [DEBUG] Multiple Service Response: service=0x{:02X}, status=0x{:02X}, replies={}", 
2303                service_code, general_status, num_replies);
2304
2305        if general_status != 0x00 {
2306            return Err(crate::error::EtherNetIpError::Protocol(format!("Multiple Service Response error: 0x{:02X}", general_status)));
2307        }
2308
2309        if num_replies != operations.len() {
2310            return Err(crate::error::EtherNetIpError::Protocol(format!("Reply count mismatch: expected {}, got {}", operations.len(), num_replies)));
2311        }
2312
2313        // Read reply offsets (each is 2 bytes, little endian)
2314        let mut reply_offsets = Vec::new();
2315        let mut offset = 6; // Skip header
2316        
2317        for _i in 0..num_replies {
2318            if offset + 2 > cip_data.len() {
2319                return Err(crate::error::EtherNetIpError::Protocol("CIP data too short for reply offsets".to_string()));
2320            }
2321            let reply_offset = u16::from_le_bytes([cip_data[offset], cip_data[offset + 1]]) as usize;
2322            reply_offsets.push(reply_offset);
2323            offset += 2;
2324        }
2325
2326        println!("🔧 [DEBUG] Reply offsets: {:?}", reply_offsets);
2327
2328        // The reply data starts after all the offsets
2329        let reply_base_offset = 6 + (num_replies * 2);
2330        
2331        println!("🔧 [DEBUG] Reply base offset: {}", reply_base_offset);
2332
2333        // Parse each reply
2334        for (i, &reply_offset) in reply_offsets.iter().enumerate() {
2335            // Reply offset is relative to position 4 (after service code, reserved, status, additional status size)
2336            let reply_start = 4 + reply_offset;
2337            
2338            if reply_start >= cip_data.len() {
2339                results.push(Err(BatchError::Other("Reply offset beyond CIP data".to_string())));
2340                continue;
2341            }
2342
2343            // Calculate reply end position
2344            let reply_end = if i + 1 < reply_offsets.len() {
2345                // Not the last reply - use next reply's offset as boundary
2346                4 + reply_offsets[i + 1]
2347            } else {
2348                // Last reply - goes to end of CIP data
2349                cip_data.len()
2350            };
2351
2352            if reply_end > cip_data.len() || reply_start >= reply_end {
2353                results.push(Err(BatchError::Other("Invalid reply boundaries".to_string())));
2354                continue;
2355            }
2356
2357            let reply_data = &cip_data[reply_start..reply_end];
2358            
2359            println!("🔧 [DEBUG] Reply {} at offset {}: start={}, end={}, len={}", 
2360                    i, reply_offset, reply_start, reply_end, reply_data.len());
2361            println!("🔧 [DEBUG] Reply {} data: {:02X?}", i, reply_data);
2362
2363            let result = self.parse_individual_reply(reply_data, &operations[i]);
2364            results.push(result);
2365        }
2366
2367        Ok(results)
2368    }
2369
2370    /// Parses an individual service reply within a Multiple Service Packet response
2371    fn parse_individual_reply(&self, reply_data: &[u8], operation: &BatchOperation) -> std::result::Result<Option<PlcValue>, BatchError> {
2372        if reply_data.len() < 4 {
2373            return Err(BatchError::SerializationError("Reply too short".to_string()));
2374        }
2375
2376        println!("🔧 [DEBUG] Parsing individual reply ({} bytes): {:02X?}", reply_data.len(), reply_data);
2377
2378        // Each individual reply in Multiple Service Response has the same format as standalone CIP response:
2379        // [0] = Service Code (0xCC for read response, 0xCD for write response)
2380        // [1] = Reserved (0x00)
2381        // [2] = General Status (0x00 for success)
2382        // [3] = Additional Status Size (0x00)
2383        // [4..] = Response data (for reads) or empty (for writes)
2384
2385        let service_code = reply_data[0];
2386        let general_status = reply_data[2];
2387
2388        println!("🔧 [DEBUG] Service code: 0x{:02X}, Status: 0x{:02X}", service_code, general_status);
2389
2390        if general_status != 0x00 {
2391            let error_msg = self.get_cip_error_message(general_status);
2392            return Err(BatchError::CipError { 
2393                status: general_status, 
2394                message: error_msg 
2395            });
2396        }
2397
2398        match operation {
2399            BatchOperation::Write { .. } => {
2400                // Write operations return no data on success
2401                Ok(None)
2402            }
2403            BatchOperation::Read { .. } => {
2404                // Read operations return data starting at offset 4
2405                if reply_data.len() < 6 {
2406                    return Err(BatchError::SerializationError("Read reply too short for data".to_string()));
2407                }
2408
2409                // Parse the data directly (skip the 4-byte header)
2410                // Data format: [type_low, type_high, value_bytes...]
2411                let data = &reply_data[4..];
2412                println!("🔧 [DEBUG] Parsing data ({} bytes): {:02X?}", data.len(), data);
2413                
2414                if data.len() < 2 {
2415                    return Err(BatchError::SerializationError("Data too short for type".to_string()));
2416                }
2417
2418                let data_type = u16::from_le_bytes([data[0], data[1]]);
2419                let value_data = &data[2..];
2420                
2421                println!("🔧 [DEBUG] Data type: 0x{:04X}, Value data ({} bytes): {:02X?}", data_type, value_data.len(), value_data);
2422
2423                // Parse based on data type
2424                match data_type {
2425                    0x00C1 => {
2426                        // BOOL
2427                        if value_data.is_empty() {
2428                            return Err(BatchError::SerializationError("Missing BOOL value".to_string()));
2429                        }
2430                        Ok(Some(PlcValue::Bool(value_data[0] != 0)))
2431                    }
2432                    0x00C2 => {
2433                        // SINT
2434                        if value_data.is_empty() {
2435                            return Err(BatchError::SerializationError("Missing SINT value".to_string()));
2436                        }
2437                        Ok(Some(PlcValue::Sint(value_data[0] as i8)))
2438                    }
2439                    0x00C3 => {
2440                        // INT
2441                        if value_data.len() < 2 {
2442                            return Err(BatchError::SerializationError("Missing INT value".to_string()));
2443                        }
2444                        let value = i16::from_le_bytes([value_data[0], value_data[1]]);
2445                        Ok(Some(PlcValue::Int(value)))
2446                    }
2447                    0x00C4 => {
2448                        // DINT
2449                        if value_data.len() < 4 {
2450                            return Err(BatchError::SerializationError("Missing DINT value".to_string()));
2451                        }
2452                        let value = i32::from_le_bytes([value_data[0], value_data[1], value_data[2], value_data[3]]);
2453                        println!("🔧 [DEBUG] Parsed DINT: {}", value);
2454                        Ok(Some(PlcValue::Dint(value)))
2455                    }
2456                    0x00C5 => {
2457                        // LINT
2458                        if value_data.len() < 8 {
2459                            return Err(BatchError::SerializationError("Missing LINT value".to_string()));
2460                        }
2461                        let value = i64::from_le_bytes([
2462                            value_data[0], value_data[1], value_data[2], value_data[3],
2463                            value_data[4], value_data[5], value_data[6], value_data[7]
2464                        ]);
2465                        Ok(Some(PlcValue::Lint(value)))
2466                    }
2467                    0x00C6 => {
2468                        // USINT
2469                        if value_data.is_empty() {
2470                            return Err(BatchError::SerializationError("Missing USINT value".to_string()));
2471                        }
2472                        Ok(Some(PlcValue::Usint(value_data[0])))
2473                    }
2474                    0x00C7 => {
2475                        // UINT
2476                        if value_data.len() < 2 {
2477                            return Err(BatchError::SerializationError("Missing UINT value".to_string()));
2478                        }
2479                        let value = u16::from_le_bytes([value_data[0], value_data[1]]);
2480                        Ok(Some(PlcValue::Uint(value)))
2481                    }
2482                    0x00C8 => {
2483                        // UDINT
2484                        if value_data.len() < 4 {
2485                            return Err(BatchError::SerializationError("Missing UDINT value".to_string()));
2486                        }
2487                        let value = u32::from_le_bytes([value_data[0], value_data[1], value_data[2], value_data[3]]);
2488                        Ok(Some(PlcValue::Udint(value)))
2489                    }
2490                    0x00C9 => {
2491                        // ULINT
2492                        if value_data.len() < 8 {
2493                            return Err(BatchError::SerializationError("Missing ULINT value".to_string()));
2494                        }
2495                        let value = u64::from_le_bytes([
2496                            value_data[0], value_data[1], value_data[2], value_data[3],
2497                            value_data[4], value_data[5], value_data[6], value_data[7]
2498                        ]);
2499                        Ok(Some(PlcValue::Ulint(value)))
2500                    }
2501                    0x00CA => {
2502                        // REAL
2503                        if value_data.len() < 4 {
2504                            return Err(BatchError::SerializationError("Missing REAL value".to_string()));
2505                        }
2506                        let bytes = [value_data[0], value_data[1], value_data[2], value_data[3]];
2507                        let value = f32::from_le_bytes(bytes);
2508                        println!("🔧 [DEBUG] Parsed REAL: {}", value);
2509                        Ok(Some(PlcValue::Real(value)))
2510                    }
2511                    0x00CB => {
2512                        // LREAL
2513                        if value_data.len() < 8 {
2514                            return Err(BatchError::SerializationError("Missing LREAL value".to_string()));
2515                        }
2516                        let bytes = [
2517                            value_data[0], value_data[1], value_data[2], value_data[3],
2518                            value_data[4], value_data[5], value_data[6], value_data[7]
2519                        ];
2520                        let value = f64::from_le_bytes(bytes);
2521                        Ok(Some(PlcValue::Lreal(value)))
2522                    }
2523                    0x00DA => {
2524                        // STRING
2525                        if value_data.is_empty() {
2526                            return Ok(Some(PlcValue::String(String::new())));
2527                        }
2528                        let length = value_data[0] as usize;
2529                        if value_data.len() < 1 + length {
2530                            return Err(BatchError::SerializationError("Insufficient data for STRING value".to_string()));
2531                        }
2532                        let string_data = &value_data[1..1 + length];
2533                        let value = String::from_utf8_lossy(string_data).to_string();
2534                        println!("🔧 [DEBUG] Parsed STRING: '{}'", value);
2535                        Ok(Some(PlcValue::String(value)))
2536                    }
2537                    0x02A0 => {
2538                        // Alternative STRING type (Allen-Bradley specific) for batch operations
2539                        if value_data.len() < 7 {
2540                            return Err(BatchError::SerializationError("Insufficient data for alternative STRING value".to_string()));
2541                        }
2542                        
2543                        // For this format, the string data starts directly at position 6
2544                        // We need to find the null terminator or use the full remaining length
2545                        let string_start = 6;
2546                        let string_data = &value_data[string_start..];
2547                        
2548                        // Find null terminator or use full length
2549                        let string_end = string_data.iter().position(|&b| b == 0).unwrap_or(string_data.len());
2550                        let string_bytes = &string_data[..string_end];
2551                        
2552                        let value = String::from_utf8_lossy(string_bytes).to_string();
2553                        println!("🔧 [DEBUG] Parsed alternative STRING (0x02A0): '{}'", value);
2554                        Ok(Some(PlcValue::String(value)))
2555                    }
2556                    _ => {
2557                        Err(BatchError::SerializationError(format!("Unsupported data type: 0x{:04X}", data_type)))
2558                    }
2559                }
2560            }
2561        }
2562    }
2563
2564    /// Writes a string value using Allen-Bradley UDT component access
2565    /// This writes to TestString.LEN and TestString.DATA separately
2566    pub async fn write_ab_string_components(&mut self, tag_name: &str, value: &str) -> crate::error::Result<()> {
2567        println!("🔧 [AB STRING] Writing string '{}' to tag '{}' using component access", value, tag_name);
2568        
2569        let string_bytes = value.as_bytes();
2570        let string_len = string_bytes.len() as i32;
2571        
2572        // Step 1: Write the length to TestString.LEN
2573        let len_tag = format!("{}.LEN", tag_name);
2574        println!("   📝 Step 1: Writing length {} to {}", string_len, len_tag);
2575        
2576        match self.write_tag(&len_tag, PlcValue::Dint(string_len)).await {
2577            Ok(_) => println!("   ✅ Length written successfully"),
2578            Err(e) => {
2579                println!("   ❌ Length write failed: {}", e);
2580                return Err(e);
2581            }
2582        }
2583        
2584        // Step 2: Write the string data to TestString.DATA using array access
2585        println!("   📝 Step 2: Writing string data to {}.DATA", tag_name);
2586        
2587        // We need to write each character individually to the DATA array
2588        for (i, &byte) in string_bytes.iter().enumerate() {
2589            let data_element = format!("{}.DATA[{}]", tag_name, i);
2590            match self.write_tag(&data_element, PlcValue::Sint(byte as i8)).await {
2591                Ok(_) => print!("."),
2592                Err(e) => {
2593                    println!("\n   ❌ Failed to write byte {} to position {}: {}", byte, i, e);
2594                    return Err(e);
2595                }
2596            }
2597        }
2598        
2599        // Step 3: Clear remaining bytes (null terminate)
2600        if string_bytes.len() < 82 {
2601            let null_element = format!("{}.DATA[{}]", tag_name, string_bytes.len());
2602            match self.write_tag(&null_element, PlcValue::Sint(0)).await {
2603                Ok(_) => println!("\n   ✅ String null-terminated successfully"),
2604                Err(e) => println!("\n   ⚠️ Could not null-terminate: {}", e),
2605            }
2606        }
2607        
2608        println!("   🎉 AB STRING component write completed!");
2609        Ok(())
2610    }
2611    
2612    /// Writes a string using a single UDT write with proper AB STRING format
2613    pub async fn write_ab_string_udt(&mut self, tag_name: &str, value: &str) -> crate::error::Result<()> {
2614        println!("🔧 [AB STRING UDT] Writing string '{}' to tag '{}' as UDT", value, tag_name);
2615        
2616        let string_bytes = value.as_bytes();
2617        if string_bytes.len() > 82 {
2618            return Err(EtherNetIpError::Protocol("String too long for Allen-Bradley STRING (max 82 chars)".to_string()));
2619        }
2620        
2621        // Build a CIP request that writes the complete AB STRING structure
2622        let mut cip_request = Vec::new();
2623        
2624        // Service: Write Tag Service (0x4D)
2625        cip_request.push(0x4D);
2626        
2627        // Request Path
2628        let tag_path = self.build_tag_path(tag_name);
2629        cip_request.push((tag_path.len() / 2) as u8); // Path size in words
2630        cip_request.extend_from_slice(&tag_path);
2631        
2632        // Data Type: Allen-Bradley STRING (0x02A0) - but write as UDT components
2633        cip_request.extend_from_slice(&[0xA0, 0x00]); // UDT type
2634        cip_request.extend_from_slice(&[0x01, 0x00]); // Element count
2635        
2636        // AB STRING UDT structure:
2637        // - DINT .LEN (4 bytes)
2638        // - SINT .DATA[82] (82 bytes)
2639        
2640        // Write .LEN field (current string length)
2641        let len = string_bytes.len() as u32;
2642        cip_request.extend_from_slice(&len.to_le_bytes());
2643        
2644        // Write .DATA field (82 bytes total)
2645        cip_request.extend_from_slice(string_bytes); // Actual string data
2646        
2647        // Pad with zeros to reach 82 bytes
2648        let padding_needed = 82 - string_bytes.len();
2649        cip_request.extend_from_slice(&vec![0u8; padding_needed]);
2650        
2651        println!("   📦 Built UDT write request: {} bytes total", cip_request.len());
2652        
2653        let response = self.send_cip_request(&cip_request).await?;
2654        
2655        if response.len() >= 3 {
2656            let general_status = response[2];
2657            if general_status == 0x00 {
2658                println!("   ✅ AB STRING UDT write successful!");
2659                Ok(())
2660            } else {
2661                let error_msg = self.get_cip_error_message(general_status);
2662                Err(EtherNetIpError::Protocol(format!("AB STRING UDT write failed - CIP Error 0x{:02X}: {}", general_status, error_msg)))
2663            }
2664        } else {
2665            Err(EtherNetIpError::Protocol("Invalid AB STRING UDT write response".to_string()))
2666        }
2667    }
2668    
2669    /// Establishes a Class 3 connected session for STRING operations
2670    /// 
2671    /// Connected sessions are required for certain operations like STRING writes
2672    /// in Allen-Bradley PLCs. This implements the Forward Open CIP service.
2673    /// Will try multiple connection parameter configurations until one succeeds.
2674    async fn establish_connected_session(&mut self, session_name: &str) -> crate::error::Result<ConnectedSession> {
2675        println!("🔗 [CONNECTED] Establishing connected session: '{}'", session_name);
2676        println!("🔗 [CONNECTED] Will try multiple parameter configurations...");
2677        
2678        // Generate unique connection parameters
2679        *self.connection_sequence.lock().await += 1;
2680        let connection_serial = (*self.connection_sequence.lock().await & 0xFFFF) as u16;
2681        
2682        // Try different configurations until one works
2683        for config_id in 0..=5 {
2684            println!("\n🔧 [ATTEMPT {}] Trying configuration {}:", config_id + 1, config_id);
2685            
2686            let mut session = if config_id == 0 {
2687                ConnectedSession::new(connection_serial)
2688            } else {
2689                ConnectedSession::with_config(connection_serial, config_id)
2690            };
2691            
2692            // Generate unique connection IDs for this attempt
2693            session.o_to_t_connection_id = 0x20000000 + *self.connection_sequence.lock().await + (config_id as u32 * 0x1000);
2694            session.t_to_o_connection_id = 0x30000000 + *self.connection_sequence.lock().await + (config_id as u32 * 0x1000);
2695            
2696            // Build Forward Open request with this configuration
2697            let forward_open_request = self.build_forward_open_request(&session)?;
2698            
2699            println!("🔗 [ATTEMPT {}] Sending Forward Open request ({} bytes)", config_id + 1, forward_open_request.len());
2700            
2701            // Send Forward Open request
2702            match self.send_cip_request(&forward_open_request).await {
2703                Ok(response) => {
2704                    // Try to parse the response - DON'T clone, modify the session directly!
2705                    match self.parse_forward_open_response(&mut session, &response) {
2706                        Ok(()) => {
2707                            // Success! Store the session and return
2708                            println!("✅ [SUCCESS] Configuration {} worked!", config_id);
2709                            println!("   Connection ID: 0x{:08X}", session.connection_id);
2710                            println!("   O->T ID: 0x{:08X}", session.o_to_t_connection_id);
2711                            println!("   T->O ID: 0x{:08X}", session.t_to_o_connection_id);
2712                            println!("   Using Connection ID: 0x{:08X} for messaging", session.connection_id);
2713                            
2714                            session.is_active = true;
2715                            let mut sessions = self.connected_sessions.lock().await;
2716                            sessions.insert(session_name.to_string(), session.clone());
2717                            return Ok(session);
2718                        },
2719                        Err(e) => {
2720                            println!("❌ [ATTEMPT {}] Configuration {} failed: {}", config_id + 1, config_id, e);
2721                            
2722                            // If it's a specific status error, log it
2723                            if e.to_string().contains("status: 0x") {
2724                                println!("   Status indicates: parameter incompatibility or resource conflict");
2725                            }
2726                        }
2727                    }
2728                },
2729                Err(e) => {
2730                    println!("❌ [ATTEMPT {}] Network error with config {}: {}", config_id + 1, config_id, e);
2731                }
2732            }
2733            
2734            // Small delay between attempts
2735            tokio::time::sleep(std::time::Duration::from_millis(100)).await;
2736        }
2737        
2738        // If we get here, all configurations failed
2739        Err(EtherNetIpError::Protocol(
2740            "All connection parameter configurations failed. PLC may not support connected messaging or has reached connection limits.".to_string()
2741        ))
2742    }
2743    
2744    /// Builds a Forward Open CIP request for establishing connected sessions
2745    fn build_forward_open_request(&self, session: &ConnectedSession) -> crate::error::Result<Vec<u8>> {
2746        let mut request = Vec::new();
2747        
2748        // CIP Forward Open Service (0x54)
2749        request.push(0x54);
2750        
2751        // Request path length (Connection Manager object)
2752        request.push(0x02); // 2 words
2753        
2754        // Class ID: Connection Manager (0x06)
2755        request.push(0x20); // Logical Class segment
2756        request.push(0x06);
2757        
2758        // Instance ID: Connection Manager instance (0x01)
2759        request.push(0x24); // Logical Instance segment
2760        request.push(0x01);
2761        
2762        // Forward Open parameters
2763        
2764        // Connection Timeout Ticks (1 byte) + Timeout multiplier (1 byte)
2765        request.push(0x0A); // Timeout ticks (10)
2766        request.push(session.timeout_multiplier);
2767        
2768        // Originator -> Target Connection ID (4 bytes, little-endian)
2769        request.extend_from_slice(&session.o_to_t_connection_id.to_le_bytes());
2770        
2771        // Target -> Originator Connection ID (4 bytes, little-endian)
2772        request.extend_from_slice(&session.t_to_o_connection_id.to_le_bytes());
2773        
2774        // Connection Serial Number (2 bytes, little-endian)
2775        request.extend_from_slice(&session.connection_serial.to_le_bytes());
2776        
2777        // Originator Vendor ID (2 bytes, little-endian)
2778        request.extend_from_slice(&session.originator_vendor_id.to_le_bytes());
2779        
2780        // Originator Serial Number (4 bytes, little-endian)
2781        request.extend_from_slice(&session.originator_serial.to_le_bytes());
2782        
2783        // Connection Timeout Multiplier (1 byte) - repeated for target
2784        request.push(session.timeout_multiplier);
2785        
2786        // Reserved bytes (3 bytes)
2787        request.extend_from_slice(&[0x00, 0x00, 0x00]);
2788        
2789        // Originator -> Target RPI (4 bytes, little-endian, microseconds)
2790        request.extend_from_slice(&session.rpi.to_le_bytes());
2791        
2792        // Originator -> Target connection parameters (4 bytes)
2793        let o_to_t_params = self.encode_connection_parameters(&session.o_to_t_params);
2794        request.extend_from_slice(&o_to_t_params.to_le_bytes());
2795        
2796        // Target -> Originator RPI (4 bytes, little-endian, microseconds)
2797        request.extend_from_slice(&session.rpi.to_le_bytes());
2798        
2799        // Target -> Originator connection parameters (4 bytes)
2800        let t_to_o_params = self.encode_connection_parameters(&session.t_to_o_params);
2801        request.extend_from_slice(&t_to_o_params.to_le_bytes());
2802        
2803        // Transport type/trigger (1 byte) - Class 3, Application triggered
2804        request.push(0xA3);
2805        
2806        // Connection Path Size (1 byte)
2807        request.push(0x02); // 2 words for Message Router path
2808        
2809        // Connection Path - Target the Message Router
2810        request.push(0x20); // Logical Class segment
2811        request.push(0x02); // Message Router class (0x02)
2812        request.push(0x24); // Logical Instance segment
2813        request.push(0x01); // Message Router instance (0x01)
2814        
2815        Ok(request)
2816    }
2817    
2818    /// Encodes connection parameters into a 32-bit value
2819    fn encode_connection_parameters(&self, params: &ConnectionParameters) -> u32 {
2820        let mut encoded = 0u32;
2821        
2822        // Connection size (bits 0-15)
2823        encoded |= params.size as u32;
2824        
2825        // Variable flag (bit 25)
2826        if params.variable_size {
2827            encoded |= 1 << 25;
2828        }
2829        
2830        // Connection type (bits 29-30)
2831        encoded |= (params.connection_type as u32) << 29;
2832        
2833        // Priority (bits 26-27)
2834        encoded |= (params.priority as u32) << 26;
2835        
2836        encoded
2837    }
2838    
2839    /// Parses Forward Open response and updates session with connection info
2840    fn parse_forward_open_response(&self, session: &mut ConnectedSession, response: &[u8]) -> crate::error::Result<()> {
2841        if response.len() < 2 {
2842            return Err(EtherNetIpError::Protocol("Forward Open response too short".to_string()));
2843        }
2844        
2845        let service = response[0];
2846        let status = response[1];
2847        
2848        // Check if this is a Forward Open Reply (0xD4)
2849        if service != 0xD4 {
2850            return Err(EtherNetIpError::Protocol(format!("Unexpected service in Forward Open response: 0x{:02X}", service)));
2851        }
2852        
2853        // Check status
2854        if status != 0x00 {
2855            let error_msg = match status {
2856                0x01 => "Connection failure - Resource unavailable or already exists",
2857                0x02 => "Invalid parameter - Connection parameters rejected",
2858                0x03 => "Connection timeout - PLC did not respond in time",
2859                0x04 => "Connection limit exceeded - Too many connections",
2860                0x08 => "Invalid service - Forward Open not supported",
2861                0x0C => "Invalid attribute - Connection parameters invalid",
2862                0x13 => "Path destination unknown - Target object not found",
2863                0x26 => "Invalid parameter value - RPI or size out of range",
2864                _ => &format!("Unknown status: 0x{:02X}", status),
2865            };
2866            return Err(EtherNetIpError::Protocol(format!("Forward Open failed with status 0x{:02X}: {}", status, error_msg)));
2867        }
2868        
2869        // Parse successful response
2870        if response.len() < 16 {
2871            return Err(EtherNetIpError::Protocol("Forward Open response data too short".to_string()));
2872        }
2873        
2874        // CRITICAL FIX: The Forward Open response contains the actual connection IDs assigned by the PLC
2875        // Use the IDs returned by the PLC, not our requested ones
2876        let actual_o_to_t_id = u32::from_le_bytes([response[2], response[3], response[4], response[5]]);
2877        let actual_t_to_o_id = u32::from_le_bytes([response[6], response[7], response[8], response[9]]);
2878        
2879        // Update session with the actual assigned connection IDs
2880        session.o_to_t_connection_id = actual_o_to_t_id;
2881        session.t_to_o_connection_id = actual_t_to_o_id;
2882        session.connection_id = actual_o_to_t_id;  // Use O->T as the primary connection ID
2883        
2884        println!("✅ [FORWARD OPEN] Success!");
2885        println!("   O->T Connection ID: 0x{:08X} (PLC assigned)", session.o_to_t_connection_id);
2886        println!("   T->O Connection ID: 0x{:08X} (PLC assigned)", session.t_to_o_connection_id);
2887        println!("   Using Connection ID: 0x{:08X} for messaging", session.connection_id);
2888        
2889        Ok(())
2890    }
2891    
2892    /// Writes a string using connected explicit messaging
2893    pub async fn write_string_connected(&mut self, tag_name: &str, value: &str) -> crate::error::Result<()> {
2894        let session_name = format!("string_write_{}", tag_name);
2895        let mut sessions = self.connected_sessions.lock().await;
2896        
2897        if !sessions.contains_key(&session_name) {
2898            drop(sessions); // Release the lock before calling establish_connected_session
2899            self.establish_connected_session(&session_name).await?;
2900            sessions = self.connected_sessions.lock().await;
2901        }
2902        
2903        let session = sessions.get(&session_name).unwrap().clone();
2904        let request = self.build_connected_string_write_request(tag_name, value, &session)?;
2905        
2906        drop(sessions); // Release the lock before sending the request
2907        let response = self.send_connected_cip_request(&request, &session, &session_name).await?;
2908        
2909        // Check if write was successful
2910        if response.len() >= 2 {
2911            let status = response[1];
2912            if status == 0x00 {
2913                Ok(())
2914            } else {
2915                let error_msg = self.get_cip_error_message(status);
2916                Err(EtherNetIpError::Protocol(format!("CIP Error 0x{:02X}: {}", status, error_msg)))
2917            }
2918        } else {
2919            Err(EtherNetIpError::Protocol("Invalid connected string write response".to_string()))
2920        }
2921    }
2922    
2923    /// Builds a string write request for connected messaging
2924    fn build_connected_string_write_request(&self, tag_name: &str, value: &str, _session: &ConnectedSession) -> crate::error::Result<Vec<u8>> {
2925        let mut request = Vec::new();
2926        
2927        // For connected messaging, use direct CIP Write service
2928        // The connection is already established, so we can send the request directly
2929        
2930        // CIP Write Service Code
2931        request.push(0x4D);
2932        
2933        // Tag path - use simple ANSI format for connected messaging  
2934        let tag_bytes = tag_name.as_bytes();
2935        let path_size_words = (2 + tag_bytes.len() + 1) / 2; // +1 for potential padding, /2 for word count
2936        request.push(path_size_words as u8);
2937        
2938        request.push(0x91); // ANSI symbol segment
2939        request.push(tag_bytes.len() as u8); // Length of tag name
2940        request.extend_from_slice(tag_bytes);
2941        
2942        // Add padding byte if needed to make path even length
2943        if (2 + tag_bytes.len()) % 2 != 0 {
2944            request.push(0x00);
2945        }
2946        
2947        // Data type for AB STRING
2948        request.extend_from_slice(&[0xCE, 0x0F]); // AB STRING data type (4046)
2949        
2950        // Number of elements (always 1 for a single string)
2951        request.extend_from_slice(&[0x01, 0x00]);
2952        
2953        // Build the AB STRING structure payload
2954        let string_bytes = value.as_bytes();
2955        let max_len: u16 = 82; // Standard AB STRING max length
2956        let current_len = string_bytes.len().min(max_len as usize) as u16;
2957        
2958        // STRING structure:
2959        // - Len (2 bytes) - number of characters used
2960        request.extend_from_slice(&current_len.to_le_bytes());
2961        
2962        // - MaxLen (2 bytes) - maximum characters allowed (typically 82)  
2963        request.extend_from_slice(&max_len.to_le_bytes());
2964        
2965        // - Data[MaxLen] (82 bytes) - the character array, zero-padded
2966        let mut data_array = vec![0u8; max_len as usize];
2967        data_array[..current_len as usize].copy_from_slice(&string_bytes[..current_len as usize]);
2968        request.extend_from_slice(&data_array);
2969        
2970        println!("🔧 [DEBUG] Built connected string write request ({} bytes) for '{}' = '{}' (len={}, maxlen={})", 
2971                 request.len(), tag_name, value, current_len, max_len);
2972        println!("🔧 [DEBUG] Request: {:02X?}", request);
2973        
2974        Ok(request)
2975    }
2976    
2977    /// Sends a CIP request using connected messaging
2978    async fn send_connected_cip_request(&mut self, cip_request: &[u8], session: &ConnectedSession, session_name: &str) -> crate::error::Result<Vec<u8>> {
2979        println!("🔗 [CONNECTED] Sending connected CIP request ({} bytes) using T->O connection ID 0x{:08X}", 
2980                 cip_request.len(), session.t_to_o_connection_id);
2981        
2982        // Build EtherNet/IP header for connected data (Send RR Data)
2983        let mut packet = Vec::new();
2984        
2985        // EtherNet/IP Header
2986        packet.extend_from_slice(&[0x6F, 0x00]); // Command: Send RR Data (0x006F) - correct for connected messaging
2987        packet.extend_from_slice(&[0x00, 0x00]); // Length (fill in later)
2988        packet.extend_from_slice(&self.session_handle.to_le_bytes()); // Session handle
2989        packet.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); // Status
2990        packet.extend_from_slice(&[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]); // Context
2991        packet.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); // Options
2992        
2993        // CPF (Common Packet Format) data starts here
2994        let cpf_start = packet.len();
2995        
2996        // Interface handle (4 bytes)
2997        packet.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]);
2998        
2999        // Timeout (2 bytes) - 5 seconds
3000        packet.extend_from_slice(&[0x05, 0x00]);
3001        
3002        // Item count (2 bytes) - 2 items: Address + Data
3003        packet.extend_from_slice(&[0x02, 0x00]);
3004        
3005        // Item 1: Connected Address Item (specifies which connection to use)
3006        packet.extend_from_slice(&[0xA1, 0x00]); // Type: Connected Address Item (0x00A1)
3007        packet.extend_from_slice(&[0x04, 0x00]); // Length: 4 bytes
3008        // Use T->O connection ID (Target to Originator) for addressing
3009        packet.extend_from_slice(&session.t_to_o_connection_id.to_le_bytes());
3010        
3011        // Item 2: Connected Data Item (contains the CIP request + sequence)
3012        packet.extend_from_slice(&[0xB1, 0x00]); // Type: Connected Data Item (0x00B1)
3013        let data_length = cip_request.len() + 2; // +2 for sequence count
3014        packet.extend_from_slice(&(data_length as u16).to_le_bytes()); // Length
3015        
3016        // Clone session_name and session before acquiring the lock
3017        let session_name_clone = session_name.to_string();
3018        let _session_clone = session.clone();
3019        
3020        // Get the current session mutably to increment sequence counter
3021        let mut sessions = self.connected_sessions.lock().await;
3022        let current_sequence = if let Some(session_mut) = sessions.get_mut(&session_name_clone) {
3023            session_mut.sequence_count += 1;
3024            session_mut.sequence_count
3025        } else {
3026            1 // Fallback if session not found
3027        };
3028        
3029        // Drop the lock before sending the request
3030        drop(sessions);
3031        
3032        // Sequence count (2 bytes) - incremental counter for this connection
3033        packet.extend_from_slice(&current_sequence.to_le_bytes());
3034        
3035        // CIP request data
3036        packet.extend_from_slice(cip_request);
3037        
3038        // Update packet length in header (total CPF data size)
3039        let cpf_length = packet.len() - cpf_start;
3040        packet[2..4].copy_from_slice(&(cpf_length as u16).to_le_bytes());
3041        
3042        println!("🔗 [CONNECTED] Sending packet ({} bytes) with sequence {}", packet.len(), current_sequence);
3043        
3044        // Send packet
3045        let mut stream = self.stream.lock().await;
3046        stream.write_all(&packet).await
3047            .map_err(|e| EtherNetIpError::Io(e))?;
3048        
3049        // Read response header
3050        let mut header = [0u8; 24];
3051        stream.read_exact(&mut header).await
3052            .map_err(|e| EtherNetIpError::Io(e))?;
3053        
3054        // Check EtherNet/IP command status
3055        let cmd_status = u32::from_le_bytes([header[8], header[9], header[10], header[11]]);
3056        if cmd_status != 0 {
3057            return Err(EtherNetIpError::Protocol(format!("Connected message failed with status: 0x{:08X}", cmd_status)));
3058        }
3059        
3060        // Read response data
3061        let response_length = u16::from_le_bytes([header[2], header[3]]) as usize;
3062        let mut response_data = vec![0u8; response_length];
3063        stream.read_exact(&mut response_data).await
3064            .map_err(|e| EtherNetIpError::Io(e))?;
3065        
3066        let mut last_activity = self.last_activity.lock().await;
3067        *last_activity = Instant::now();
3068        
3069        println!("🔗 [CONNECTED] Received response ({} bytes)", response_data.len());
3070        
3071        // Extract connected CIP response
3072        self.extract_connected_cip_from_response(&response_data)
3073    }
3074    
3075    /// Extracts CIP data from connected response
3076    fn extract_connected_cip_from_response(&self, response: &[u8]) -> crate::error::Result<Vec<u8>> {
3077        println!("🔗 [CONNECTED] Extracting CIP from connected response ({} bytes): {:02X?}", 
3078                 response.len(), response);
3079        
3080        if response.len() < 12 {
3081            return Err(EtherNetIpError::Protocol("Connected response too short for CPF header".to_string()));
3082        }
3083        
3084        // Parse CPF (Common Packet Format) structure
3085        // [0-3]: Interface handle
3086        // [4-5]: Timeout
3087        // [6-7]: Item count
3088        let item_count = u16::from_le_bytes([response[6], response[7]]) as usize;
3089        println!("🔗 [CONNECTED] CPF item count: {}", item_count);
3090        
3091        let mut pos = 8; // Start after CPF header
3092        
3093        // Look for Connected Data Item (0x00B1)
3094        for _i in 0..item_count {
3095            if pos + 4 > response.len() {
3096                return Err(EtherNetIpError::Protocol("Response truncated while parsing items".to_string()));
3097            }
3098            
3099            let item_type = u16::from_le_bytes([response[pos], response[pos + 1]]);
3100            let item_length = u16::from_le_bytes([response[pos + 2], response[pos + 3]]) as usize;
3101            pos += 4; // Skip item header
3102            
3103            println!("🔗 [CONNECTED] Found item: type=0x{:04X}, length={}", item_type, item_length);
3104            
3105            if item_type == 0x00B1 { // Connected Data Item
3106                if pos + item_length > response.len() {
3107                    return Err(EtherNetIpError::Protocol("Connected data item truncated".to_string()));
3108                }
3109                
3110                // Connected Data Item contains [sequence_count(2)][cip_data]
3111                if item_length < 2 {
3112                    return Err(EtherNetIpError::Protocol("Connected data item too short for sequence".to_string()));
3113                }
3114                
3115                let sequence_count = u16::from_le_bytes([response[pos], response[pos + 1]]);
3116                println!("🔗 [CONNECTED] Sequence count: {}", sequence_count);
3117                
3118                // Extract CIP data (skip 2-byte sequence count)
3119                let cip_data = response[pos + 2..pos + item_length].to_vec();
3120                println!("🔗 [CONNECTED] Extracted CIP data ({} bytes): {:02X?}", 
3121                         cip_data.len(), cip_data);
3122                
3123                return Ok(cip_data);
3124            } else {
3125                // Skip this item's data
3126                pos += item_length;
3127            }
3128        }
3129        
3130        Err(EtherNetIpError::Protocol("Connected Data Item (0x00B1) not found in response".to_string()))
3131    }
3132    
3133    /// Closes a specific connected session
3134    async fn close_connected_session(&mut self, session_name: &str) -> crate::error::Result<()> {
3135        if let Some(session) = self.connected_sessions.lock().await.get(session_name) {
3136            let session = session.clone(); // Clone to avoid borrowing issues
3137            
3138            // Build Forward Close request
3139            let forward_close_request = self.build_forward_close_request(&session)?;
3140            
3141            // Send Forward Close request
3142            let _response = self.send_cip_request(&forward_close_request).await?;
3143            
3144            println!("🔗 [CONNECTED] Session '{}' closed successfully", session_name);
3145        }
3146        
3147        // Remove session from our tracking
3148        let mut sessions = self.connected_sessions.lock().await;
3149        sessions.remove(session_name);
3150        
3151        Ok(())
3152    }
3153
3154    /// Builds a Forward Close CIP request for terminating connected sessions
3155    fn build_forward_close_request(&self, session: &ConnectedSession) -> crate::error::Result<Vec<u8>> {
3156        let mut request = Vec::new();
3157        
3158        // CIP Forward Close Service (0x4E)
3159        request.push(0x4E);
3160        
3161        // Request path length (Connection Manager object)
3162        request.push(0x02); // 2 words
3163        
3164        // Class ID: Connection Manager (0x06)
3165        request.push(0x20); // Logical Class segment
3166        request.push(0x06);
3167        
3168        // Instance ID: Connection Manager instance (0x01)  
3169        request.push(0x24); // Logical Instance segment
3170        request.push(0x01);
3171        
3172        // Forward Close parameters
3173        
3174        // Connection Timeout Ticks (1 byte) + Timeout multiplier (1 byte)
3175        request.push(0x0A); // Timeout ticks (10)
3176        request.push(session.timeout_multiplier);
3177        
3178        // Connection Serial Number (2 bytes, little-endian)
3179        request.extend_from_slice(&session.connection_serial.to_le_bytes());
3180        
3181        // Originator Vendor ID (2 bytes, little-endian)
3182        request.extend_from_slice(&session.originator_vendor_id.to_le_bytes());
3183        
3184        // Originator Serial Number (4 bytes, little-endian)
3185        request.extend_from_slice(&session.originator_serial.to_le_bytes());
3186        
3187        // Connection Path Size (1 byte)
3188        request.push(0x02); // 2 words for Message Router path
3189        
3190        // Connection Path - Target the Message Router
3191        request.push(0x20); // Logical Class segment
3192        request.push(0x02); // Message Router class (0x02)
3193        request.push(0x24); // Logical Instance segment  
3194        request.push(0x01); // Message Router instance (0x01)
3195        
3196        Ok(request)
3197    }
3198     
3199    /// Closes all connected sessions (called during disconnect)
3200    async fn close_all_connected_sessions(&mut self) -> crate::error::Result<()> {
3201        let session_names: Vec<String> = self.connected_sessions.lock().await.keys().cloned().collect();
3202        
3203        for session_name in session_names {
3204            let _ = self.close_connected_session(&session_name).await; // Ignore errors during cleanup
3205        }
3206        
3207        Ok(())
3208    }
3209
3210    /// Writes a string using unconnected explicit messaging with proper AB STRING format
3211    /// 
3212    /// This method uses standard unconnected messaging instead of connected messaging
3213    /// and implements the proper Allen-Bradley STRING structure as described in the
3214    /// provided information about Len, MaxLen, and Data[82] format.
3215    pub async fn write_string_unconnected(&mut self, tag_name: &str, value: &str) -> crate::error::Result<()> {
3216        println!("📝 [UNCONNECTED] Writing string '{}' to tag '{}' using unconnected messaging", value, tag_name);
3217        
3218        self.validate_session().await?;
3219        
3220        let string_bytes = value.as_bytes();
3221        if string_bytes.len() > 82 {
3222            return Err(EtherNetIpError::Protocol("String too long for Allen-Bradley STRING (max 82 chars)".to_string()));
3223        }
3224        
3225        // Build the CIP request with proper AB STRING structure
3226        let mut cip_request = Vec::new();
3227        
3228        // Service: Write Tag Service (0x4D)
3229        cip_request.push(0x4D);
3230        
3231        // Request Path Size (in words)
3232        let tag_bytes = tag_name.as_bytes();
3233        let path_len = if tag_bytes.len() % 2 == 0 { 
3234            tag_bytes.len() + 2 
3235        } else { 
3236            tag_bytes.len() + 3 
3237        } / 2;
3238        cip_request.push(path_len as u8);
3239        
3240        // Request Path: ANSI Extended Symbol Segment for tag name
3241        cip_request.push(0x91); // ANSI Extended Symbol Segment
3242        cip_request.push(tag_bytes.len() as u8); // Tag name length
3243        cip_request.extend_from_slice(tag_bytes); // Tag name
3244        
3245        // Pad to even length if necessary
3246        if tag_bytes.len() % 2 != 0 {
3247            cip_request.push(0x00);
3248        }
3249        
3250        // For write operations, we don't include data type and element count
3251        // The PLC infers the data type from the tag definition
3252        
3253        // Build Allen-Bradley STRING structure based on what we see in read responses:
3254        // Looking at read response: [CE, 0F, 01, 00, 00, 00, 31, 00, ...]
3255        // Structure appears to be:
3256        // - Some header/identifier (2 bytes): 0xCE, 0x0F  
3257        // - Length (2 bytes): number of characters
3258        // - MaxLength or padding (2 bytes): 0x00, 0x00
3259        // - Data array (variable length, null terminated)
3260        
3261        let current_len = string_bytes.len().min(82) as u16;
3262        
3263        // Build the correct Allen-Bradley STRING structure to match what the PLC expects
3264        // Analysis of read response: [CE, 0F, 01, 00, 00, 00, 31, 00, 00, 00, ...]
3265        // Structure appears to be:
3266        // - Header (2 bytes): 0xCE, 0x0F (Allen-Bradley STRING identifier)
3267        // - Length (4 bytes, DINT): Number of characters currently used  
3268        // - Data (variable): Character data followed by padding to complete the structure
3269        
3270        let current_len = string_bytes.len().min(82) as u32;
3271        
3272        // AB STRING header/identifier - this appears to be required
3273        cip_request.extend_from_slice(&[0xCE, 0x0F]);
3274        
3275        // Length (4 bytes) - number of characters used as DINT
3276        cip_request.extend_from_slice(&current_len.to_le_bytes());
3277        
3278        // Data bytes - the actual string content
3279        cip_request.extend_from_slice(&string_bytes[..current_len as usize]);
3280        
3281        // Add padding if the total structure needs to be a specific size
3282        // Based on reads, it looks like there might be additional padding after the data
3283        
3284        println!("🔧 [DEBUG] Built Allen-Bradley STRING write request ({} bytes) for '{}' = '{}' (len={})", 
3285                 cip_request.len(), tag_name, value, current_len);
3286        println!("🔧 [DEBUG] Request structure: Service=0x4D, Path={} bytes, Header=0xCE0F, Len={} (4 bytes), Data", 
3287                 path_len * 2, current_len);
3288        
3289        // Send the request using standard unconnected messaging
3290        let response = self.send_cip_request(&cip_request).await?;
3291        
3292        // Extract CIP response from EtherNet/IP wrapper
3293        let cip_response = self.extract_cip_from_response(&response)?;
3294        
3295        // Check if write was successful - use correct CIP response format
3296        if cip_response.len() >= 3 {
3297            let service_reply = cip_response[0];  // Should be 0xCD (0x4D + 0x80) for Write Tag reply
3298            let _additional_status_size = cip_response[1]; // Additional status size (usually 0)
3299            let status = cip_response[2];         // CIP status code at position 2
3300            
3301            println!("🔧 [DEBUG] Write response - Service: 0x{:02X}, Status: 0x{:02X}", service_reply, status);
3302            
3303            if status == 0x00 {
3304                println!("✅ [UNCONNECTED] String write completed successfully");
3305                Ok(())
3306            } else {
3307                let error_msg = self.get_cip_error_message(status);
3308                println!("❌ [UNCONNECTED] String write failed: {} (0x{:02X})", error_msg, status);
3309                Err(EtherNetIpError::Protocol(format!("CIP Error 0x{:02X}: {}", status, error_msg)))
3310            }
3311        } else {
3312            Err(EtherNetIpError::Protocol("Invalid unconnected string write response - too short".to_string()))
3313        }
3314    }
3315
3316    /// Write a string value to a PLC tag using unconnected messaging
3317    /// 
3318    /// # Arguments
3319    /// 
3320    /// * `tag_name` - The name of the tag to write to
3321    /// * `value` - The string value to write (max 82 characters)
3322    /// 
3323    /// # Returns
3324    /// 
3325    /// * `Ok(())` if the write was successful
3326    /// * `Err(EtherNetIpError)` if the write failed
3327    /// 
3328    /// # Errors
3329    /// 
3330    /// * `StringTooLong` - If the string is longer than 82 characters
3331    /// * `InvalidString` - If the string contains invalid characters
3332    /// * `TagNotFound` - If the tag doesn't exist
3333    /// * `WriteError` - If the write operation fails
3334    pub async fn write_string(&mut self, tag_name: &str, value: &str) -> crate::error::Result<()> {
3335        // Validate string length
3336        if value.len() > 82 {
3337            return Err(crate::error::EtherNetIpError::StringTooLong {
3338                max_length: 82,
3339                actual_length: value.len(),
3340            });
3341        }
3342
3343        // Validate string content (ASCII only)
3344        if !value.is_ascii() {
3345            return Err(crate::error::EtherNetIpError::InvalidString {
3346                reason: "String contains non-ASCII characters".to_string(),
3347            });
3348        }
3349
3350        // Build the string write request
3351        let request = self.build_string_write_request(tag_name, value)?;
3352        
3353        // Send the request and get the response
3354        let response = self.send_cip_request(&request).await?;
3355        
3356        // Parse the response
3357        let cip_response = self.extract_cip_from_response(&response)?;
3358        
3359        // Check for errors in the response
3360        if cip_response.len() < 2 {
3361            return Err(crate::error::EtherNetIpError::InvalidResponse {
3362                reason: "Response too short".to_string(),
3363            });
3364        }
3365
3366        let status = cip_response[0];
3367        if status != 0 {
3368            return Err(crate::error::EtherNetIpError::WriteError {
3369                status,
3370                message: self.get_cip_error_message(status),
3371            });
3372        }
3373
3374        Ok(())
3375    }
3376
3377    /// Build a string write request packet
3378    fn build_string_write_request(&self, tag_name: &str, value: &str) -> crate::error::Result<Vec<u8>> {
3379        let mut request = Vec::new();
3380        
3381        // CIP Write Service (0x4D)
3382        request.push(0x4D);
3383        
3384        // Tag path
3385        let tag_path = self.build_tag_path(tag_name);
3386        request.extend_from_slice(&tag_path);
3387        
3388        // AB STRING data structure
3389        request.extend_from_slice(&(value.len() as u16).to_le_bytes()); // Len
3390        request.extend_from_slice(&82u16.to_le_bytes());                // MaxLen
3391        
3392        // Data[82] with padding
3393        let mut data = [0u8; 82];
3394        let bytes = value.as_bytes();
3395        data[..bytes.len()].copy_from_slice(bytes);
3396        request.extend_from_slice(&data);
3397        
3398        Ok(request)
3399    }
3400
3401    /// Subscribes to a tag for real-time updates
3402    pub async fn subscribe_to_tag(&self, tag_path: &str, options: SubscriptionOptions) -> Result<()> {
3403        let mut subscriptions = self.subscriptions.lock().await;
3404        let subscription = TagSubscription::new(tag_path.to_string(), options);
3405        subscriptions.push(subscription);
3406        drop(subscriptions); // Release the lock before starting the monitoring thread
3407        
3408        let tag_path = tag_path.to_string();
3409        let mut client = self.clone();
3410        tokio::spawn(async move {
3411            loop {
3412                match client.read_tag(&tag_path).await {
3413                    Ok(value) => {
3414                        if let Err(e) = client.update_subscription(&tag_path, &value).await {
3415                            eprintln!("Error updating subscription: {}", e);
3416                            break;
3417                        }
3418                    }
3419                    Err(e) => {
3420                        eprintln!("Error reading tag {}: {}", tag_path, e);
3421                        break;
3422                    }
3423                }
3424                tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
3425            }
3426        });
3427        Ok(())
3428    }
3429
3430    pub async fn subscribe_to_tags(&self, tags: &[(&str, SubscriptionOptions)]) -> Result<()> {
3431        for (tag_name, options) in tags {
3432            self.subscribe_to_tag(tag_name, options.clone()).await?;
3433        }
3434        Ok(())
3435    }
3436
3437    async fn update_subscription(&self, tag_name: &str, value: &PlcValue) -> Result<()> {
3438        let subscriptions = self.subscriptions.lock().await;
3439        for subscription in subscriptions.iter() {
3440            if subscription.tag_path == tag_name && subscription.is_active() {
3441                subscription.update_value(value).await?;
3442            }
3443        }
3444        Ok(())
3445    }
3446
3447    async fn _get_connected_session(&mut self, session_name: &str) -> crate::error::Result<ConnectedSession> {
3448        // First check if we already have a session
3449        {
3450            let sessions = self.connected_sessions.lock().await;
3451            if let Some(session) = sessions.get(session_name) {
3452                return Ok(session.clone());
3453            }
3454        }
3455
3456        // If we don't have a session, establish a new one
3457        let session = self.establish_connected_session(session_name).await?;
3458        
3459        // Store the new session
3460        let mut sessions = self.connected_sessions.lock().await;
3461        sessions.insert(session_name.to_string(), session.clone());
3462        
3463        Ok(session)
3464    }
3465}
3466
3467/*
3468===============================================================================
3469END OF LIBRARY DOCUMENTATION
3470
3471This file provides a complete, production-ready EtherNet/IP communication
3472library for Allen-Bradley PLCs. The library includes:
3473
3474- Native Rust API with async support
3475- C FFI exports for cross-language integration  
3476- Comprehensive error handling and validation
3477- Detailed documentation and examples
3478- Performance optimizations
3479- Memory safety guarantees
3480
3481For usage examples, see the main.rs file or the C# integration samples.
3482
3483For technical details about the EtherNet/IP protocol implementation,
3484refer to the inline documentation above.
3485
3486Version: 1.0.0
3487Compatible with: CompactLogix L1x-L5x series PLCs
3488License: As specified in Cargo.toml
3489===============================================================================_
3490*/