Skip to main content

rust_ethernet_ip/
lib.rs

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