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