1use crate::EtherNetIpStream;
2use crate::batch::{BatchConfig, BatchOperation};
3use crate::error::{EtherNetIpError, Result};
4use crate::protocol::cip::{
5 CipRequest, CipResponse, MULTIPLE_SERVICE_PACKET, READ_TAG, SendDataRequest, WRITE_TAG,
6};
7use crate::protocol::encap::{EncapsulationHeader, REGISTER_SESSION, UNREGISTER_SESSION};
8use crate::protocol::values;
9use crate::protocol::{Decode, Encode};
10use crate::route::RoutePath;
11use crate::subscription::TagSubscription;
12use crate::tag_group::TagGroupConfig;
13use crate::tag_manager::{TagManager, TagMetadata, TagPermissions, TagScope};
14use crate::types::{PlcValue, UdtData};
15use crate::udt::{TagAttributes, UdtDefinition, UdtManager};
16use crate::{TagPath, udt};
17use bytes::BytesMut;
18use std::collections::HashMap;
19use std::net::SocketAddr;
20#[cfg(feature = "ffi")]
21use std::sync::LazyLock;
22use std::sync::atomic::{AtomicBool, AtomicU8, AtomicU32, AtomicU64, Ordering};
23use std::sync::{Arc, Mutex as StdMutex};
24use std::time::{SystemTime, UNIX_EPOCH};
25use tokio::io::{AsyncReadExt, AsyncWriteExt};
26use tokio::net::TcpStream;
27#[cfg(feature = "ffi")]
28use tokio::runtime::Runtime;
29use tokio::sync::Mutex;
30use tokio::time::{Duration, Instant, timeout};
31
32mod actor;
33mod batch_exec;
34mod diagnostics;
35mod schema_export;
36mod service_layer;
37mod string;
38mod subscriptions;
39
40pub use actor::{Backoff, Client, ConnectionEvent, RetryClient, RetryPolicy};
41
42const READ_TAG_FRAGMENTED: u8 = 0x52;
43const WRITE_TAG_FRAGMENTED: u8 = 0x53;
44const READ_TAG_FRAGMENTED_REPLY: u8 = 0xD2;
45const WRITE_TAG_FRAGMENTED_REPLY: u8 = 0xD3;
46const CIP_STATUS_SUCCESS: u8 = 0x00;
47const CIP_STATUS_PARTIAL_TRANSFER: u8 = 0x06;
48
49#[derive(Debug)]
50struct TagListPage {
51 tags: Vec<TagAttributes>,
52 last_instance_id: Option<u32>,
53 partial_transfer: bool,
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57struct TemplateAttributes {
58 structure_handle: u16,
59 member_count: u16,
60 definition_size_words: u32,
61 structure_size_bytes: u32,
62}
63
64#[derive(Debug, Clone, Copy)]
65enum DiagnosticOperation {
66 Read,
67 Write,
68 Batch,
69}
70
71#[derive(Debug, Default)]
72struct DiagnosticCounters {
73 total_reads: AtomicU64,
74 total_writes: AtomicU64,
75 successful_reads: AtomicU64,
76 successful_writes: AtomicU64,
77 failed_reads: AtomicU64,
78 failed_writes: AtomicU64,
79 batch_operations: AtomicU64,
80 partial_batch_failures: AtomicU64,
81 network_errors: AtomicU64,
82 protocol_errors: AtomicU64,
83 timeout_errors: AtomicU64,
84 tag_not_found_errors: AtomicU64,
85 data_type_errors: AtomicU64,
86 session_errors: AtomicU64,
87 route_path_errors: AtomicU64,
88 embedded_service_errors: AtomicU64,
89 known_controller_limitation_errors: AtomicU64,
90 retriable_errors: AtomicU64,
91 non_retriable_errors: AtomicU64,
92 last_successful_read_time: AtomicU64,
93 last_failed_read_time: AtomicU64,
94 last_successful_write_time: AtomicU64,
95 last_failed_write_time: AtomicU64,
96 last_error_time: AtomicU64,
97 last_error_category: AtomicU8,
98}
99
100impl DiagnosticCounters {
101 fn record_success(&self, operation: Option<DiagnosticOperation>) {
102 let now = current_unix_seconds();
103 match operation {
104 Some(DiagnosticOperation::Read) => {
105 self.total_reads.fetch_add(1, Ordering::Relaxed);
106 self.successful_reads.fetch_add(1, Ordering::Relaxed);
107 self.last_successful_read_time.store(now, Ordering::Relaxed);
108 }
109 Some(DiagnosticOperation::Write) => {
110 self.total_writes.fetch_add(1, Ordering::Relaxed);
111 self.successful_writes.fetch_add(1, Ordering::Relaxed);
112 self.last_successful_write_time
113 .store(now, Ordering::Relaxed);
114 }
115 Some(DiagnosticOperation::Batch) => {
116 self.batch_operations.fetch_add(1, Ordering::Relaxed);
117 }
118 None => {}
119 }
120 }
121
122 fn record_cip_failure(&self, operation: Option<DiagnosticOperation>) {
123 let category = crate::ErrorCategory::CipProtocol;
124 self.record_operation_failure(operation, current_unix_seconds());
125 self.protocol_errors.fetch_add(1, Ordering::Relaxed);
126 self.non_retriable_errors.fetch_add(1, Ordering::Relaxed);
127 self.store_last_error(category);
128 }
129
130 fn record_failure(&self, operation: Option<DiagnosticOperation>, error: &EtherNetIpError) {
131 let now = current_unix_seconds();
132 let category = diagnostic_error_category(error);
133 self.record_operation_failure(operation, now);
134
135 match category {
136 crate::ErrorCategory::Network => self.network_errors.fetch_add(1, Ordering::Relaxed),
137 crate::ErrorCategory::Timeout => self.timeout_errors.fetch_add(1, Ordering::Relaxed),
138 crate::ErrorCategory::Session => self.session_errors.fetch_add(1, Ordering::Relaxed),
139 crate::ErrorCategory::RoutePath => {
140 self.route_path_errors.fetch_add(1, Ordering::Relaxed)
141 }
142 crate::ErrorCategory::CipProtocol => {
143 self.protocol_errors.fetch_add(1, Ordering::Relaxed)
144 }
145 crate::ErrorCategory::BatchEmbeddedService => {
146 self.embedded_service_errors.fetch_add(1, Ordering::Relaxed)
147 }
148 crate::ErrorCategory::KnownControllerLimitation => self
149 .known_controller_limitation_errors
150 .fetch_add(1, Ordering::Relaxed),
151 crate::ErrorCategory::DataType => self.data_type_errors.fetch_add(1, Ordering::Relaxed),
152 crate::ErrorCategory::NotFound => {
153 self.tag_not_found_errors.fetch_add(1, Ordering::Relaxed)
154 }
155 crate::ErrorCategory::Unknown => self.protocol_errors.fetch_add(1, Ordering::Relaxed),
156 };
157
158 if error.is_retriable() || category.is_retriable() {
159 self.retriable_errors.fetch_add(1, Ordering::Relaxed);
160 } else {
161 self.non_retriable_errors.fetch_add(1, Ordering::Relaxed);
162 }
163 self.store_last_error(category);
164 }
165
166 fn record_operation_failure(&self, operation: Option<DiagnosticOperation>, now: u64) {
167 match operation {
168 Some(DiagnosticOperation::Read) => {
169 self.total_reads.fetch_add(1, Ordering::Relaxed);
170 self.failed_reads.fetch_add(1, Ordering::Relaxed);
171 self.last_failed_read_time.store(now, Ordering::Relaxed);
172 }
173 Some(DiagnosticOperation::Write) => {
174 self.total_writes.fetch_add(1, Ordering::Relaxed);
175 self.failed_writes.fetch_add(1, Ordering::Relaxed);
176 self.last_failed_write_time.store(now, Ordering::Relaxed);
177 }
178 Some(DiagnosticOperation::Batch) => {
179 self.batch_operations.fetch_add(1, Ordering::Relaxed);
180 self.partial_batch_failures.fetch_add(1, Ordering::Relaxed);
181 }
182 None => {}
183 }
184 }
185
186 fn store_last_error(&self, category: crate::ErrorCategory) {
187 self.last_error_time
188 .store(current_unix_seconds(), Ordering::Relaxed);
189 self.last_error_category
190 .store(error_category_to_code(category), Ordering::Relaxed);
191 }
192
193 fn operation_metrics(&self) -> crate::OperationMetrics {
194 crate::OperationMetrics {
195 total_reads: self.total_reads.load(Ordering::Relaxed),
196 total_writes: self.total_writes.load(Ordering::Relaxed),
197 successful_reads: self.successful_reads.load(Ordering::Relaxed),
198 successful_writes: self.successful_writes.load(Ordering::Relaxed),
199 failed_reads: self.failed_reads.load(Ordering::Relaxed),
200 failed_writes: self.failed_writes.load(Ordering::Relaxed),
201 batch_operations: self.batch_operations.load(Ordering::Relaxed),
202 subscription_updates: 0,
203 partial_batch_failures: self.partial_batch_failures.load(Ordering::Relaxed),
204 last_successful_read_time: unix_seconds_to_system_time(
205 self.last_successful_read_time.load(Ordering::Relaxed),
206 ),
207 last_failed_read_time: unix_seconds_to_system_time(
208 self.last_failed_read_time.load(Ordering::Relaxed),
209 ),
210 last_successful_write_time: unix_seconds_to_system_time(
211 self.last_successful_write_time.load(Ordering::Relaxed),
212 ),
213 last_failed_write_time: unix_seconds_to_system_time(
214 self.last_failed_write_time.load(Ordering::Relaxed),
215 ),
216 }
217 }
218
219 fn error_metrics(&self) -> crate::ErrorMetrics {
220 let last_error_category =
221 error_category_from_code(self.last_error_category.load(Ordering::Relaxed));
222 crate::ErrorMetrics {
223 network_errors: self.network_errors.load(Ordering::Relaxed),
224 protocol_errors: self.protocol_errors.load(Ordering::Relaxed),
225 timeout_errors: self.timeout_errors.load(Ordering::Relaxed),
226 tag_not_found_errors: self.tag_not_found_errors.load(Ordering::Relaxed),
227 data_type_errors: self.data_type_errors.load(Ordering::Relaxed),
228 session_errors: self.session_errors.load(Ordering::Relaxed),
229 route_path_errors: self.route_path_errors.load(Ordering::Relaxed),
230 embedded_service_errors: self.embedded_service_errors.load(Ordering::Relaxed),
231 known_controller_limitation_errors: self
232 .known_controller_limitation_errors
233 .load(Ordering::Relaxed),
234 retriable_errors: self.retriable_errors.load(Ordering::Relaxed),
235 non_retriable_errors: self.non_retriable_errors.load(Ordering::Relaxed),
236 last_error_time: unix_seconds_to_system_time(
237 self.last_error_time.load(Ordering::Relaxed),
238 ),
239 last_error_message: last_error_category.map(|category| {
240 format!("Most recent counted client operation failed: {category:?}")
241 }),
242 last_error_category,
243 last_retriable_error_time: if last_error_category.is_some_and(|c| c.is_retriable()) {
244 unix_seconds_to_system_time(self.last_error_time.load(Ordering::Relaxed))
245 } else {
246 None
247 },
248 }
249 }
250}
251
252fn current_unix_seconds() -> u64 {
253 SystemTime::now()
254 .duration_since(UNIX_EPOCH)
255 .unwrap_or_default()
256 .as_secs()
257}
258
259fn unix_seconds_to_system_time(seconds: u64) -> Option<SystemTime> {
260 (seconds != 0).then(|| UNIX_EPOCH + Duration::from_secs(seconds))
261}
262
263fn error_category_to_code(category: crate::ErrorCategory) -> u8 {
264 match category {
265 crate::ErrorCategory::Network => 1,
266 crate::ErrorCategory::Timeout => 2,
267 crate::ErrorCategory::Session => 3,
268 crate::ErrorCategory::RoutePath => 4,
269 crate::ErrorCategory::CipProtocol => 5,
270 crate::ErrorCategory::BatchEmbeddedService => 6,
271 crate::ErrorCategory::KnownControllerLimitation => 7,
272 crate::ErrorCategory::DataType => 8,
273 crate::ErrorCategory::NotFound => 9,
274 crate::ErrorCategory::Unknown => 10,
275 }
276}
277
278fn error_category_from_code(code: u8) -> Option<crate::ErrorCategory> {
279 match code {
280 1 => Some(crate::ErrorCategory::Network),
281 2 => Some(crate::ErrorCategory::Timeout),
282 3 => Some(crate::ErrorCategory::Session),
283 4 => Some(crate::ErrorCategory::RoutePath),
284 5 => Some(crate::ErrorCategory::CipProtocol),
285 6 => Some(crate::ErrorCategory::BatchEmbeddedService),
286 7 => Some(crate::ErrorCategory::KnownControllerLimitation),
287 8 => Some(crate::ErrorCategory::DataType),
288 9 => Some(crate::ErrorCategory::NotFound),
289 10 => Some(crate::ErrorCategory::Unknown),
290 _ => None,
291 }
292}
293
294fn diagnostic_error_category(error: &EtherNetIpError) -> crate::ErrorCategory {
295 match error {
296 EtherNetIpError::Io(_) | EtherNetIpError::ConnectionLost(_) => {
297 crate::ErrorCategory::Network
298 }
299 EtherNetIpError::Timeout(_) => crate::ErrorCategory::Timeout,
300 EtherNetIpError::Connection(_) => crate::ErrorCategory::Session,
301 EtherNetIpError::TagNotFound(_) => crate::ErrorCategory::NotFound,
302 EtherNetIpError::DataTypeMismatch { .. }
303 | EtherNetIpError::StringTooLong { .. }
304 | EtherNetIpError::InvalidString { .. } => crate::ErrorCategory::DataType,
305 EtherNetIpError::ReadError { .. }
306 | EtherNetIpError::WriteError { .. }
307 | EtherNetIpError::CipError { .. } => crate::ErrorCategory::CipProtocol,
308 EtherNetIpError::Protocol(message)
309 if message.contains("route") || message.contains("Route") =>
310 {
311 crate::ErrorCategory::RoutePath
312 }
313 EtherNetIpError::Protocol(message)
314 if message.contains("Embedded service") || message.contains("Multiple Service") =>
315 {
316 crate::ErrorCategory::BatchEmbeddedService
317 }
318 EtherNetIpError::Protocol(_) | EtherNetIpError::InvalidResponse { .. } => {
319 crate::ErrorCategory::CipProtocol
320 }
321 EtherNetIpError::Udt(_)
322 | EtherNetIpError::Tag(_)
323 | EtherNetIpError::Permission(_)
324 | EtherNetIpError::Utf8(_)
325 | EtherNetIpError::Other(_)
326 | EtherNetIpError::Subscription(_)
327 | EtherNetIpError::Unsupported { .. } => crate::ErrorCategory::Unknown,
328 }
329}
330
331#[cfg(feature = "ffi")]
333pub(crate) static RUNTIME: LazyLock<std::io::Result<Runtime>> = LazyLock::new(Runtime::new);
334
335#[derive(Clone)]
512pub struct EipClient {
513 stream: Arc<Mutex<Box<dyn EtherNetIpStream>>>,
515 session_handle: Arc<AtomicU32>,
517 stream_poisoned: Arc<AtomicBool>,
519 sender_context_counter: Arc<AtomicU64>,
521 diagnostic_counters: Arc<DiagnosticCounters>,
523 tag_manager: Arc<Mutex<TagManager>>,
525 udt_manager: Arc<Mutex<UdtManager>>,
527 route_path: Arc<StdMutex<Option<RoutePath>>>,
529 max_packet_size: Arc<AtomicU32>,
531 last_activity: Arc<Mutex<Instant>>,
533 batch_config: BatchConfig,
535 subscriptions: Arc<Mutex<Vec<TagSubscription>>>,
537 tag_groups: Arc<Mutex<HashMap<String, TagGroupConfig>>>,
539}
540
541#[cfg(test)]
542const _: fn() = || {
543 fn assert_send_sync_static<T: Send + Sync + 'static>() {}
544 assert_send_sync_static::<EipClient>();
545};
546
547impl std::fmt::Debug for EipClient {
548 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
549 f.debug_struct("EipClient")
550 .field("session_handle", &self.session_handle())
551 .field("stream_poisoned", &self.stream_poisoned())
552 .field("route_path", &self.route_path_snapshot())
553 .field("max_packet_size", &self.max_packet_size())
554 .field("batch_config", &self.batch_config)
555 .field("stream", &"<stream>")
556 .field("diagnostic_counters", &"<diagnostic_counters>")
557 .field("tag_manager", &"<tag_manager>")
558 .field("udt_manager", &"<udt_manager>")
559 .field("subscriptions", &"<subscriptions>")
560 .field("tag_groups", &"<tag_groups>")
561 .finish()
562 }
563}
564
565impl EipClient {
566 async fn from_stream<S>(stream: S) -> Result<Self>
569 where
570 S: EtherNetIpStream + 'static,
571 {
572 let mut client = Self {
573 stream: Arc::new(Mutex::new(Box::new(stream))),
574 session_handle: Arc::new(AtomicU32::new(0)),
575 stream_poisoned: Arc::new(AtomicBool::new(false)),
576 sender_context_counter: Arc::new(AtomicU64::new(1)),
577 diagnostic_counters: Arc::new(DiagnosticCounters::default()),
578 tag_manager: Arc::new(Mutex::new(TagManager::new())),
579 udt_manager: Arc::new(Mutex::new(UdtManager::new())),
580 route_path: Arc::new(StdMutex::new(None)),
581 max_packet_size: Arc::new(AtomicU32::new(4000)),
582 last_activity: Arc::new(Mutex::new(Instant::now())),
583 batch_config: BatchConfig::default(),
584 subscriptions: Arc::new(Mutex::new(Vec::new())),
585 tag_groups: Arc::new(Mutex::new(HashMap::new())),
586 };
587 client.register_session().await?;
588 client.negotiate_packet_size().await?;
589 Ok(client)
590 }
591
592 pub async fn new(addr: &str) -> Result<Self> {
593 let addr = addr
594 .parse::<SocketAddr>()
595 .map_err(|e| EtherNetIpError::Protocol(format!("Invalid address format: {e}")))?;
596 let stream = TcpStream::connect(addr).await?;
597 Self::from_stream(stream).await
598 }
599
600 pub async fn connect(addr: &str) -> Result<Self> {
602 Self::new(addr).await
603 }
604
605 #[cfg(test)]
606 fn new_unconnected_for_testing() -> Self {
607 let (stream, _peer) = tokio::io::duplex(64);
608 Self {
609 stream: Arc::new(Mutex::new(Box::new(stream))),
610 session_handle: Arc::new(AtomicU32::new(0)),
611 stream_poisoned: Arc::new(AtomicBool::new(false)),
612 sender_context_counter: Arc::new(AtomicU64::new(1)),
613 diagnostic_counters: Arc::new(DiagnosticCounters::default()),
614 tag_manager: Arc::new(Mutex::new(TagManager::new())),
615 udt_manager: Arc::new(Mutex::new(UdtManager::new())),
616 route_path: Arc::new(StdMutex::new(None)),
617 max_packet_size: Arc::new(AtomicU32::new(4000)),
618 last_activity: Arc::new(Mutex::new(Instant::now())),
619 batch_config: BatchConfig::default(),
620 subscriptions: Arc::new(Mutex::new(Vec::new())),
621 tag_groups: Arc::new(Mutex::new(HashMap::new())),
622 }
623 }
624
625 async fn register_session(&mut self) -> crate::error::Result<()> {
647 self.ensure_stream_usable()?;
648 tracing::debug!("Starting session registration...");
649 let mut packet = BytesMut::with_capacity(28);
650 EncapsulationHeader::new(REGISTER_SESSION, 4, 0).encode(&mut packet);
651 packet.extend_from_slice(&[0x01, 0x00]); packet.extend_from_slice(&[0x00, 0x00]); tracing::trace!("Sending Register Session packet: {:02X?}", packet);
655 let mut stream = self.stream.lock().await;
656 self.ensure_stream_usable()?;
657 self.stream_poisoned.store(true, Ordering::Relaxed);
660 if let Err(e) = stream.write_all(&packet).await {
661 tracing::error!("Failed to send Register Session packet: {}", e);
662 return Err(EtherNetIpError::Io(e));
663 }
664
665 let mut header_buf = [0u8; 24];
666 tracing::debug!("Waiting for Register Session response...");
667 match timeout(Duration::from_secs(5), stream.read_exact(&mut header_buf)).await {
668 Ok(Ok(_)) => {
669 tracing::trace!("Received Register Session response header");
670 }
671 Ok(Err(e)) => {
672 tracing::error!("Error reading response: {}", e);
673 return Err(EtherNetIpError::Io(e));
674 }
675 Err(_) => {
676 tracing::warn!("Timeout waiting for response");
677 return Err(EtherNetIpError::Timeout(Duration::from_secs(5)));
678 }
679 };
680
681 let mut header_bytes = &header_buf[..];
682 let header = EncapsulationHeader::decode(&mut header_bytes)?;
683 let mut body = vec![0u8; header.length as usize];
684 if !body.is_empty() {
685 match timeout(Duration::from_secs(5), stream.read_exact(&mut body)).await {
686 Ok(Ok(_)) => {}
687 Ok(Err(e)) => {
688 tracing::error!("Error reading response body: {}", e);
689 return Err(EtherNetIpError::Io(e));
690 }
691 Err(_) => {
692 tracing::warn!("Timeout waiting for response body");
693 return Err(EtherNetIpError::Timeout(Duration::from_secs(5)));
694 }
695 }
696 }
697
698 self.stream_poisoned.store(false, Ordering::Relaxed);
699
700 self.set_session_handle(header.session_handle);
702 tracing::debug!("Session handle: 0x{:08X}", self.session_handle());
703
704 let status = header.status;
706 tracing::trace!("Status code: 0x{:08X}", status);
707
708 if status != 0 {
709 tracing::error!("Session registration failed with status: 0x{:08X}", status);
710 return Err(EtherNetIpError::Protocol(format!(
711 "Session registration failed with status: 0x{status:08X}"
712 )));
713 }
714
715 tracing::info!("Session registration successful");
716 Ok(())
717 }
718
719 pub fn set_max_packet_size(&mut self, size: u32) {
721 self.max_packet_size
722 .store(size.min(4000), Ordering::Relaxed);
723 }
724
725 pub(crate) fn max_packet_size(&self) -> u32 {
726 self.max_packet_size.load(Ordering::Relaxed)
727 }
728
729 pub(crate) fn session_handle(&self) -> u32 {
730 self.session_handle.load(Ordering::Relaxed)
731 }
732
733 fn set_session_handle(&self, session_handle: u32) {
734 self.session_handle.store(session_handle, Ordering::Relaxed);
735 }
736
737 fn stream_poisoned(&self) -> bool {
738 self.stream_poisoned.load(Ordering::Relaxed)
739 }
740
741 fn next_sender_context(&self) -> [u8; 8] {
742 self.sender_context_counter
743 .fetch_add(1, Ordering::Relaxed)
744 .to_le_bytes()
745 }
746
747 fn diagnostic_operation_for(cip_request: &[u8]) -> Option<DiagnosticOperation> {
748 match cip_request.first().copied() {
749 Some(READ_TAG | READ_TAG_FRAGMENTED) => Some(DiagnosticOperation::Read),
750 Some(WRITE_TAG | WRITE_TAG_FRAGMENTED) => Some(DiagnosticOperation::Write),
751 Some(MULTIPLE_SERVICE_PACKET) => Some(DiagnosticOperation::Batch),
752 _ => None,
753 }
754 }
755
756 fn ensure_stream_usable(&self) -> crate::error::Result<()> {
757 if self.stream_poisoned() {
758 return Err(EtherNetIpError::ConnectionLost(
759 "connection stream is poisoned after an incomplete transaction; reconnect required"
760 .to_string(),
761 ));
762 }
763 Ok(())
764 }
765
766 fn route_path_snapshot(&self) -> Option<RoutePath> {
767 self.route_path
768 .lock()
769 .unwrap_or_else(|poisoned| poisoned.into_inner())
770 .clone()
771 }
772
773 pub async fn discover_tags(&mut self) -> crate::error::Result<()> {
775 let response = self
776 .send_cip_request(&self.build_list_tags_request())
777 .await?;
778
779 let cip_data = self.extract_cip_from_response(&response)?;
781
782 if let Err(e) = self.check_cip_error(&cip_data) {
784 return Err(crate::error::EtherNetIpError::Protocol(format!(
785 "Tag discovery failed: {}. Some PLCs may not support tag discovery. Try reading tags directly by name.",
786 e
787 )));
788 }
789
790 let tags = {
791 let tag_manager = self.tag_manager.lock().await;
792 tag_manager.parse_tag_list(&cip_data)?
793 };
794
795 tracing::debug!("Initial tag discovery found {} tags", tags.len());
796
797 let hierarchical_tags = {
799 let tag_manager = self.tag_manager.lock().await;
800 let hierarchical_tags = tag_manager.drill_down_tags(&tags).await?;
801 drop(tag_manager);
802 hierarchical_tags
803 };
804
805 tracing::debug!(
806 "After drill-down: {} total tags discovered",
807 hierarchical_tags.len()
808 );
809
810 {
811 let tag_manager = self.tag_manager.lock().await;
812 let mut cache = tag_manager.cache.write()?;
813 for (name, metadata) in hierarchical_tags {
814 cache.insert(name, metadata);
815 }
816 }
817 Ok(())
818 }
819
820 pub async fn discover_udt_members(
822 &mut self,
823 udt_name: &str,
824 ) -> crate::error::Result<Vec<(String, TagMetadata)>> {
825 let definition = self.get_udt_definition(udt_name).await?;
826
827 {
829 let tag_manager = self.tag_manager.lock().await;
830 let mut definitions = tag_manager.udt_definitions.write()?;
831 definitions.insert(udt_name.to_string(), definition.clone());
832 }
833
834 let mut members = Vec::new();
836 for member in &definition.members {
837 let member_name = member.name.clone();
838 let full_name = format!("{}.{}", udt_name, member_name);
839
840 let metadata = TagMetadata {
841 data_type: member.data_type,
842 scope: TagScope::Controller,
843 permissions: TagPermissions {
844 readable: true,
845 writable: true,
846 },
847 is_array: false,
848 dimensions: Vec::new(),
849 last_access: std::time::Instant::now(),
850 size: member.size,
851 array_info: None,
852 last_updated: std::time::Instant::now(),
853 };
854
855 members.push((full_name, metadata));
856 }
857
858 Ok(members)
859 }
860
861 pub async fn get_udt_definition_cached(&self, udt_name: &str) -> Option<UdtDefinition> {
863 let tag_manager = self.tag_manager.lock().await;
864 tag_manager.get_udt_definition_cached(udt_name)
865 }
866
867 pub async fn list_udt_definitions(&self) -> Vec<String> {
869 let tag_manager = self.tag_manager.lock().await;
870 tag_manager.list_udt_definitions()
871 }
872
873 pub async fn discover_tags_detailed(&mut self) -> crate::error::Result<Vec<TagAttributes>> {
876 let (tags, _) = self.discover_tags_detailed_internal(false).await?;
877 Ok(tags)
878 }
879
880 async fn discover_tags_detailed_internal(
881 &mut self,
882 best_effort: bool,
883 ) -> crate::error::Result<(Vec<TagAttributes>, Vec<String>)> {
884 let mut start_instance = 0u32;
885 let mut tags = Vec::new();
886 let mut warnings = Vec::new();
887
888 loop {
889 let request = self.build_tag_list_request_from_instance(start_instance)?;
890 let response = match self.send_cip_request(&request).await {
891 Ok(response) => response,
892 Err(err) if best_effort && !tags.is_empty() => {
893 warnings.push(format!(
894 "Tag discovery stopped early at instance {} after transport/protocol failure: {}",
895 start_instance, err
896 ));
897 break;
898 }
899 Err(err) => return Err(err),
900 };
901 let cip_data = match self.extract_cip_from_response(&response) {
902 Ok(cip_data) => cip_data,
903 Err(err) if best_effort && !tags.is_empty() => {
904 warnings.push(format!(
905 "Tag discovery stopped early at instance {} after response extraction failure: {}",
906 start_instance, err
907 ));
908 break;
909 }
910 Err(err) => return Err(err),
911 };
912 let page = match self.parse_tag_list_response_page(&cip_data) {
913 Ok(page) => page,
914 Err(err) if best_effort && !tags.is_empty() => {
915 warnings.push(format!(
916 "Tag discovery stopped early at instance {} after page-parse failure: {}",
917 start_instance, err
918 ));
919 break;
920 }
921 Err(err) => return Err(err),
922 };
923
924 tags.extend(page.tags);
925
926 if !page.partial_transfer {
927 break;
928 }
929
930 let Some(last_instance_id) = page.last_instance_id else {
931 return Err(crate::error::EtherNetIpError::Protocol(
932 "Tag discovery returned Partial transfer without a last instance ID"
933 .to_string(),
934 ));
935 };
936
937 if last_instance_id == u32::MAX || last_instance_id < start_instance {
938 return Err(crate::error::EtherNetIpError::Protocol(format!(
939 "Tag discovery pagination stalled at instance {}",
940 last_instance_id
941 )));
942 }
943
944 start_instance = last_instance_id.saturating_add(1);
945 }
946
947 Ok((tags, warnings))
948 }
949
950 pub async fn discover_program_tags(
953 &mut self,
954 program_name: &str,
955 ) -> crate::error::Result<Vec<TagAttributes>> {
956 let request = self.build_program_tag_list_request(program_name)?;
958 let response = self.send_cip_request(&request).await?;
959
960 let cip_data = self.extract_cip_from_response(&response)?;
962
963 if let Err(e) = self.check_cip_error(&cip_data) {
965 return Err(crate::error::EtherNetIpError::Protocol(format!(
966 "Program tag discovery failed for '{}': {}. Some PLCs may not support tag discovery. Try reading tags directly by name.",
967 program_name, e
968 )));
969 }
970
971 self.parse_tag_list_response(&cip_data)
973 }
974
975 pub async fn list_cached_tag_attributes(&self) -> Vec<String> {
977 self.udt_manager.lock().await.list_tag_attributes()
978 }
979
980 pub async fn clear_caches(&mut self) {
982 if let Err(error) = self.tag_manager.lock().await.clear_cache().await {
983 tracing::warn!("failed to clear tag metadata cache: {error}");
984 }
985 self.udt_manager.lock().await.clear_cache();
986 }
987
988 pub async fn with_route_path(addr: &str, route: RoutePath) -> crate::error::Result<Self> {
990 let mut client = Self::new(addr).await?;
991 client.set_route_path(route);
992 Ok(client)
993 }
994
995 pub async fn connect_with_stream<S>(stream: S, route: Option<RoutePath>) -> Result<Self>
1023 where
1024 S: EtherNetIpStream + 'static,
1025 {
1026 let mut client = Self::from_stream(stream).await?;
1027 if let Some(route) = route {
1028 client.set_route_path(route);
1029 }
1030 Ok(client)
1031 }
1032
1033 pub fn set_route_path(&mut self, route: RoutePath) {
1035 *self
1036 .route_path
1037 .lock()
1038 .unwrap_or_else(|poisoned| poisoned.into_inner()) = Some(route);
1039 }
1040
1041 pub fn get_route_path(&self) -> Option<RoutePath> {
1043 self.route_path_snapshot()
1044 }
1045
1046 pub fn clear_route_path(&mut self) {
1048 *self
1049 .route_path
1050 .lock()
1051 .unwrap_or_else(|poisoned| poisoned.into_inner()) = None;
1052 }
1053
1054 pub async fn get_tag_metadata(&self, tag_name: &str) -> Option<TagMetadata> {
1056 let tag_manager = self.tag_manager.lock().await;
1057 match tag_manager.cache.read() {
1058 Ok(cache) => cache.get(tag_name).cloned(),
1059 Err(_) => {
1060 tracing::warn!("failed to read tag metadata cache: lock poisoned");
1061 None
1062 }
1063 }
1064 }
1065
1066 pub async fn read_tag(&mut self, tag_name: &str) -> crate::error::Result<PlcValue> {
1132 self.validate_session().await?;
1133
1134 if let Some((base_path, bit_index)) = self.parse_bit_access(tag_name) {
1135 return self
1136 .read_bit_base_direct(&base_path, bit_index)
1137 .await
1138 .map(PlcValue::Bool);
1139 }
1140
1141 if let Some((base_name, index)) = self.parse_array_element_access(tag_name) {
1145 if let Some(bracket_start) = tag_name.find('[')
1148 && let Some(bracket_end_rel) = tag_name[bracket_start..].find(']')
1149 {
1150 let bracket_end_abs = bracket_start + bracket_end_rel;
1151 let after_bracket = &tag_name[bracket_end_abs + 1..];
1152 tracing::debug!(
1153 "Array element detected for '{}': base='{}', index={}, after_bracket='{}'",
1154 tag_name,
1155 base_name,
1156 index,
1157 after_bracket
1158 );
1159 if !after_bracket.starts_with('.') {
1161 tracing::debug!(
1162 "Detected simple array element access: {}[{}], using workaround",
1163 base_name,
1164 index
1165 );
1166 return self.read_array_element_workaround(&base_name, index).await;
1167 } else {
1168 tracing::debug!(
1169 "Array element '{}[{}]' has member access after bracket ('{}'), using TagPath::parse()",
1170 base_name,
1171 index,
1172 after_bracket
1173 );
1174 }
1175 }
1176 }
1177
1178 if let Some((parent_path, index)) = self.parse_final_array_element_access(tag_name)
1182 && self.detect_bool_array_path(&parent_path).await?
1183 {
1184 return self
1185 .read_bool_array_element_workaround(&parent_path, index)
1186 .await;
1187 }
1188
1189 self.read_tag_direct(tag_name).await
1190 }
1191
1192 async fn read_tag_direct(&mut self, tag_name: &str) -> crate::error::Result<PlcValue> {
1193 let response = self
1194 .send_cip_request(&self.build_read_request(tag_name)?)
1195 .await?;
1196 let cip_data = self.extract_cip_from_response(&response)?;
1197 if cip_data.get(2).copied() == Some(CIP_STATUS_PARTIAL_TRANSFER) {
1198 return self.read_tag_fragmented(tag_name).await;
1199 }
1200 self.parse_cip_response(&cip_data)
1201 }
1202
1203 async fn read_tag_fragmented(&mut self, tag_name: &str) -> crate::error::Result<PlcValue> {
1204 let mut offset = 0u32;
1205 let mut reassembled = Vec::new();
1206
1207 loop {
1208 let request = self.build_read_fragmented_request(tag_name, 1, offset)?;
1209 let response = self.send_cip_request(&request).await?;
1210 let cip_data = self.extract_unconnected_data_item(&response)?;
1211 let (status, fragment) = self.parse_read_fragmented_response(&cip_data)?;
1212
1213 if fragment.is_empty() && status == CIP_STATUS_PARTIAL_TRANSFER {
1214 return Err(EtherNetIpError::Protocol(format!(
1215 "Read Tag Fragmented for '{tag_name}' returned an empty partial fragment at offset {offset}"
1216 )));
1217 }
1218
1219 offset = offset
1220 .checked_add(fragment.len() as u32)
1221 .ok_or_else(|| EtherNetIpError::Protocol("fragment offset overflow".to_string()))?;
1222 reassembled.extend_from_slice(fragment);
1223
1224 if status == CIP_STATUS_SUCCESS {
1225 break;
1226 }
1227 }
1228
1229 self.decode_type_prefixed_value(&reassembled)
1230 }
1231
1232 pub async fn read_bit(&mut self, tag_base: &str, bit_index: u8) -> crate::error::Result<bool> {
1243 self.validate_session().await?;
1244 self.read_bit_base_direct(tag_base, bit_index).await
1245 }
1246
1247 async fn read_bit_base_direct(
1248 &mut self,
1249 tag_base: &str,
1250 bit_index: u8,
1251 ) -> crate::error::Result<bool> {
1252 if bit_index >= 32 {
1253 return Err(crate::error::EtherNetIpError::Protocol(
1254 "bit_index must be 0..32 for DINT bit access".to_string(),
1255 ));
1256 }
1257 match self.read_tag_direct(tag_base).await? {
1260 PlcValue::Bool(b) => Ok(b),
1261 PlcValue::Dint(n) => Ok((n >> bit_index) & 1 != 0),
1262 other => Err(crate::error::EtherNetIpError::DataTypeMismatch {
1263 expected: "BOOL or DINT".to_string(),
1264 actual: format!("{:?}", other),
1265 }),
1266 }
1267 }
1268
1269 pub async fn write_bit(
1280 &mut self,
1281 tag_base: &str,
1282 bit_index: u8,
1283 value: bool,
1284 ) -> crate::error::Result<()> {
1285 self.validate_session().await?;
1286 self.write_bit_base_direct(tag_base, bit_index, value).await
1287 }
1288
1289 async fn write_bit_base_direct(
1290 &mut self,
1291 tag_base: &str,
1292 bit_index: u8,
1293 value: bool,
1294 ) -> crate::error::Result<()> {
1295 if bit_index >= 32 {
1296 return Err(crate::error::EtherNetIpError::Protocol(
1297 "bit_index must be 0..32 for DINT bit access".to_string(),
1298 ));
1299 }
1300 match self.read_tag_direct(tag_base).await? {
1304 PlcValue::Dint(current) => {
1305 let mask = 1i32 << bit_index;
1306 let updated = if value {
1307 current | mask
1308 } else {
1309 current & !mask
1310 };
1311 self.write_tag_direct(tag_base, &PlcValue::Dint(updated))
1312 .await
1313 }
1314 PlcValue::Bool(_) if bit_index == 0 => {
1315 self.write_tag_direct(tag_base, &PlcValue::Bool(value))
1316 .await
1317 }
1318 other => Err(crate::error::EtherNetIpError::DataTypeMismatch {
1319 expected: "DINT".to_string(),
1320 actual: format!("{:?}", other),
1321 }),
1322 }
1323 }
1324
1325 fn parse_array_element_access(&self, tag_name: &str) -> Option<(String, u32)> {
1327 if let Some(bracket_pos) = tag_name.rfind('[')
1329 && let Some(close_bracket_pos) = tag_name.rfind(']')
1330 && close_bracket_pos > bracket_pos
1331 {
1332 let base_name = tag_name[..bracket_pos].to_string();
1333 let index_str = &tag_name[bracket_pos + 1..close_bracket_pos];
1334 if let Ok(index) = index_str.parse::<u32>()
1335 && !tag_name[..bracket_pos].contains('[')
1336 {
1337 return Some((base_name, index));
1339 }
1340 }
1341 None
1342 }
1343
1344 fn has_member_suffix_after_first_array_index(&self, tag_name: &str) -> bool {
1345 if let Some(bracket_start) = tag_name.find('[')
1346 && let Some(bracket_end_rel) = tag_name[bracket_start..].find(']')
1347 {
1348 let bracket_end_abs = bracket_start + bracket_end_rel;
1349 return tag_name[bracket_end_abs + 1..].starts_with('.');
1350 }
1351
1352 false
1353 }
1354
1355 fn parse_bit_access(&self, tag_name: &str) -> Option<(String, u8)> {
1356 match TagPath::parse(tag_name).ok()? {
1357 TagPath::Bit {
1358 base_path,
1359 bit_index,
1360 } => Some((base_path.as_string(), bit_index)),
1361 _ => None,
1362 }
1363 }
1364
1365 fn parse_final_array_element_access(&self, tag_name: &str) -> Option<(String, u32)> {
1366 match TagPath::parse(tag_name).ok()? {
1367 TagPath::Array { base_path, indices } if indices.len() == 1 => {
1368 Some((base_path.as_string(), indices[0]))
1369 }
1370 _ => None,
1371 }
1372 }
1373
1374 async fn detect_bool_array_path(&mut self, array_path: &str) -> crate::error::Result<bool> {
1375 let test_response = self
1376 .send_cip_request(&self.build_read_request_with_count(array_path, 1)?)
1377 .await?;
1378 let test_cip_data = self.extract_cip_from_response(&test_response)?;
1379
1380 if self.check_cip_error(&test_cip_data).is_err() || test_cip_data.len() < 6 {
1381 return Ok(false);
1382 }
1383
1384 let test_data_type = u16::from_le_bytes([test_cip_data[4], test_cip_data[5]]);
1385 Ok(test_data_type == values::BOOL_ARRAY_DWORD)
1386 }
1387
1388 fn parse_bool_array_dword_response(&self, cip_data: &[u8]) -> crate::error::Result<u32> {
1389 let mut response_bytes = cip_data;
1390 let response = CipResponse::decode(&mut response_bytes)?;
1391 if response.status != 0 {
1392 return Err(EtherNetIpError::Protocol(format!(
1393 "CIP Error {} when reading BOOL array DWORD: {}",
1394 response.status,
1395 self.get_cip_error_message(response.status)
1396 )));
1397 }
1398
1399 if response.service != 0xCC {
1400 return Err(EtherNetIpError::Protocol(format!(
1401 "Unexpected service reply: 0x{:02X}",
1402 response.service
1403 )));
1404 }
1405
1406 if response.data.len() < 6 {
1407 return Err(EtherNetIpError::Protocol(
1408 "BOOL array response too short for data type and DWORD".to_string(),
1409 ));
1410 }
1411
1412 let data_type = u16::from_le_bytes([response.data[0], response.data[1]]);
1413 if data_type != values::BOOL_ARRAY_DWORD {
1414 return Err(EtherNetIpError::Protocol(format!(
1415 "Expected BOOL array DWORD data type 0x00D3, got 0x{data_type:04X}"
1416 )));
1417 }
1418
1419 let value_data = &response.data[2..];
1420
1421 if value_data.len() < 4 {
1422 return Err(EtherNetIpError::Protocol(format!(
1423 "BOOL array data too short: need 4 bytes (DWORD), got {} bytes",
1424 value_data.len()
1425 )));
1426 }
1427
1428 Ok(u32::from_le_bytes([
1429 value_data[0],
1430 value_data[1],
1431 value_data[2],
1432 value_data[3],
1433 ]))
1434 }
1435
1436 async fn read_array_element_workaround(
1449 &mut self,
1450 base_array_name: &str,
1451 index: u32,
1452 ) -> crate::error::Result<PlcValue> {
1453 tracing::debug!(
1454 "Reading array element '{}[{}]' using element addressing",
1455 base_array_name,
1456 index
1457 );
1458
1459 let test_response = self
1461 .send_cip_request(&self.build_read_request_with_count(base_array_name, 1)?)
1462 .await?;
1463 let test_cip_data = self.extract_cip_from_response(&test_response)?;
1464
1465 if test_cip_data.get(2).copied() != Some(CIP_STATUS_PARTIAL_TRANSFER) {
1469 self.check_cip_error(&test_cip_data)?;
1471
1472 if test_cip_data.len() >= 6 {
1474 let test_data_type = u16::from_le_bytes([test_cip_data[4], test_cip_data[5]]);
1475 if test_data_type == 0x00D3 {
1476 return self
1478 .read_bool_array_element_workaround(base_array_name, index)
1479 .await;
1480 }
1481 }
1482 }
1483
1484 let request = self.build_read_array_request(base_array_name, index, 1);
1487
1488 let response = self.send_cip_request(&request).await?;
1489 let cip_data = self.extract_cip_from_response(&response)?;
1490
1491 if cip_data.get(2).copied() == Some(CIP_STATUS_PARTIAL_TRANSFER) {
1495 return self
1496 .read_tag_fragmented(&format!("{base_array_name}[{index}]"))
1497 .await;
1498 }
1499
1500 self.check_cip_error(&cip_data)?;
1502
1503 self.parse_cip_response(&cip_data)
1506 }
1507
1508 async fn read_bool_array_element_workaround(
1512 &mut self,
1513 base_array_name: &str,
1514 index: u32,
1515 ) -> crate::error::Result<PlcValue> {
1516 tracing::debug!(
1517 "BOOL array detected - reading DWORD and extracting bit [{}]",
1518 index
1519 );
1520
1521 let dword_index = index / 32;
1522
1523 let response = self
1526 .send_cip_request(&self.build_read_array_request(base_array_name, dword_index, 1))
1527 .await?;
1528 let cip_data = self.extract_cip_from_response(&response)?;
1529 let dword_value = self.parse_bool_array_dword_response(&cip_data)?;
1530
1531 let bit_index = (index % 32) as u8;
1534 let bool_value = (dword_value >> bit_index) & 1 != 0;
1535
1536 Ok(PlcValue::Bool(bool_value))
1537 }
1538
1539 async fn read_array_in_chunks(
1546 &mut self,
1547 base_array_name: &str,
1548 data_type: u16,
1549 start_index: u32,
1550 target_element_count: u32,
1551 ) -> crate::error::Result<Vec<u8>> {
1552 let element_size = match data_type {
1554 0x00C1 => 1, 0x00C2 => 1, 0x00C3 => 2, 0x00C4 => 4, 0x00C5 => 8, 0x00C6 => 1, 0x00C7 => 2, 0x00C8 => 4, 0x00C9 => 8, 0x00CA => 4, 0x00CB => 8, _ => {
1566 return Err(EtherNetIpError::Protocol(format!(
1567 "Unsupported array data type for chunked reading: 0x{:04X}",
1568 data_type
1569 )));
1570 }
1571 };
1572
1573 let elements_per_chunk = match element_size {
1576 1 => 30, 2 => 15, 4 => 8, 8 => 4, _ => 8,
1581 };
1582
1583 let end_index = start_index
1584 .checked_add(target_element_count)
1585 .ok_or_else(|| EtherNetIpError::Protocol("Array range overflow".to_string()))?;
1586
1587 let mut all_data = Vec::new();
1588 let mut next_chunk_start = start_index;
1589
1590 tracing::debug!(
1591 "Reading array '{}' in chunks: {} elements per chunk, target: {} elements",
1592 base_array_name,
1593 elements_per_chunk,
1594 target_element_count
1595 );
1596
1597 while next_chunk_start < end_index {
1598 let chunk_end = (next_chunk_start + elements_per_chunk as u32).min(end_index);
1601 let chunk_size = (chunk_end - next_chunk_start) as u16;
1602
1603 tracing::trace!(
1604 "Reading chunk: elements {} to {} ({} elements) using element addressing",
1605 next_chunk_start,
1606 chunk_end - 1,
1607 chunk_size
1608 );
1609
1610 let response = self
1613 .send_cip_request(&self.build_read_array_request(
1614 base_array_name,
1615 next_chunk_start,
1616 chunk_size,
1617 ))
1618 .await?;
1619 let cip_data = self.extract_cip_from_response(&response)?;
1620
1621 let mut response_bytes = cip_data.as_slice();
1622 let response = CipResponse::decode(&mut response_bytes)?;
1623 if response.status != 0 {
1624 let error_msg = self.get_cip_error_message(response.status);
1625 return Err(EtherNetIpError::Protocol(format!(
1626 "CIP Error {} when reading chunk (elements {} to {}): {}",
1627 response.status,
1628 next_chunk_start,
1629 chunk_end - 1,
1630 error_msg
1631 )));
1632 }
1633
1634 if response.service != 0xCC {
1635 return Err(EtherNetIpError::Protocol(format!(
1636 "Unexpected service reply in chunk: 0x{:02X} (expected 0xCC)",
1637 response.service
1638 )));
1639 }
1640
1641 if response.data.len() < 2 {
1642 return Err(EtherNetIpError::Protocol(format!(
1643 "Chunk response too short for data type: got {} bytes, expected at least 6",
1644 cip_data.len()
1645 )));
1646 }
1647
1648 let chunk_data_type = u16::from_le_bytes([response.data[0], response.data[1]]);
1649 if chunk_data_type != data_type {
1650 return Err(EtherNetIpError::Protocol(format!(
1651 "Data type mismatch in chunk: expected 0x{:04X}, got 0x{:04X}",
1652 data_type, chunk_data_type
1653 )));
1654 }
1655
1656 let chunk_value_data = &response.data[2..];
1661 let chunk_complete_bytes = (chunk_value_data.len() / element_size) * element_size;
1662 let chunk_data = &chunk_value_data[..chunk_complete_bytes];
1663
1664 if !chunk_data.is_empty() {
1667 all_data.extend_from_slice(chunk_data);
1668 let elements_received = chunk_data.len() / element_size;
1669 next_chunk_start += elements_received as u32;
1670
1671 tracing::trace!(
1672 "Chunk read: {} elements ({} bytes) starting at index {}, total so far: {} elements",
1673 elements_received,
1674 chunk_data.len(),
1675 next_chunk_start - elements_received as u32,
1676 all_data.len() / element_size
1677 );
1678
1679 if next_chunk_start >= end_index {
1681 tracing::trace!(
1682 "Reached target element count ({}), stopping chunked read",
1683 target_element_count
1684 );
1685 break;
1686 }
1687 } else {
1688 break;
1690 }
1691 }
1692
1693 let final_element_count = all_data.len() / element_size;
1694 tracing::debug!(
1695 "Chunked read complete: {} total elements ({} bytes), target was {} elements",
1696 final_element_count,
1697 all_data.len(),
1698 target_element_count
1699 );
1700
1701 if final_element_count < target_element_count as usize {
1702 return Err(EtherNetIpError::Protocol(format!(
1703 "Incomplete array read: requested {} elements, received {}",
1704 target_element_count, final_element_count
1705 )));
1706 }
1707
1708 Ok(all_data)
1709 }
1710
1711 fn array_element_size(data_type: u16) -> Option<usize> {
1712 match data_type {
1713 0x00C1 => Some(1), 0x00C2 => Some(1), 0x00C3 => Some(2), 0x00C4 => Some(4), 0x00C5 => Some(8), 0x00C6 => Some(1), 0x00C7 => Some(2), 0x00C8 => Some(4), 0x00C9 => Some(8), 0x00CA => Some(4), 0x00CB => Some(8), _ => None,
1725 }
1726 }
1727
1728 fn decode_array_bytes(
1729 &self,
1730 data_type: u16,
1731 bytes: &[u8],
1732 ) -> crate::error::Result<Vec<PlcValue>> {
1733 let Some(element_size) = Self::array_element_size(data_type) else {
1734 return Err(EtherNetIpError::Protocol(format!(
1735 "Unsupported data type for array decoding: 0x{:04X}",
1736 data_type
1737 )));
1738 };
1739
1740 if !bytes.len().is_multiple_of(element_size) {
1741 return Err(EtherNetIpError::Protocol(format!(
1742 "Array payload length {} is not aligned to element size {}",
1743 bytes.len(),
1744 element_size
1745 )));
1746 }
1747
1748 let mut values = Vec::with_capacity(bytes.len() / element_size);
1749 for chunk in bytes.chunks_exact(element_size) {
1750 values.push(values::decode_array_element(data_type, chunk)?);
1751 }
1752
1753 Ok(values)
1754 }
1755
1756 pub async fn read_array_range(
1772 &mut self,
1773 base_array_name: &str,
1774 start_index: u32,
1775 element_count: u32,
1776 ) -> crate::error::Result<Vec<PlcValue>> {
1777 if element_count == 0 {
1778 return Ok(Vec::new());
1779 }
1780
1781 let probe_response = self
1782 .send_cip_request(&self.build_read_array_request(base_array_name, start_index, 1))
1783 .await?;
1784 let probe_cip = self.extract_cip_from_response(&probe_response)?;
1785 self.check_cip_error(&probe_cip)?;
1786
1787 if probe_cip.len() < 6 {
1788 return Err(EtherNetIpError::Protocol(
1789 "Array probe response too short".to_string(),
1790 ));
1791 }
1792
1793 let data_type = u16::from_le_bytes([probe_cip[4], probe_cip[5]]);
1794 let raw = self
1795 .read_array_in_chunks(base_array_name, data_type, start_index, element_count)
1796 .await?;
1797 let values = self.decode_array_bytes(data_type, &raw)?;
1798
1799 if values.len() != element_count as usize {
1800 return Err(EtherNetIpError::Protocol(format!(
1801 "Array read count mismatch: requested {}, got {}",
1802 element_count,
1803 values.len()
1804 )));
1805 }
1806
1807 Ok(values)
1808 }
1809
1810 async fn write_array_element_workaround(
1824 &mut self,
1825 base_array_name: &str,
1826 index: u32,
1827 value: PlcValue,
1828 ) -> crate::error::Result<()> {
1829 tracing::debug!(
1830 "Writing to array element '{}[{}]' using element addressing",
1831 base_array_name,
1832 index
1833 );
1834
1835 let test_response = self
1837 .send_cip_request(&self.build_read_request_with_count(base_array_name, 1)?)
1838 .await?;
1839 let test_cip_data = self.extract_cip_from_response(&test_response)?;
1840
1841 if test_cip_data.len() < 3 {
1843 return Err(EtherNetIpError::Protocol(
1844 "Test read response too short".to_string(),
1845 ));
1846 }
1847
1848 if let Err(e) = self.check_cip_error(&test_cip_data) {
1850 return Err(EtherNetIpError::Protocol(format!(
1851 "Cannot write to array element: Test read failed: {}",
1852 e
1853 )));
1854 }
1855
1856 if test_cip_data.len() < 6 {
1858 return Err(EtherNetIpError::Protocol(
1859 "Test read response too short to determine data type".to_string(),
1860 ));
1861 }
1862
1863 let test_data_type = u16::from_le_bytes([test_cip_data[4], test_cip_data[5]]);
1864
1865 if test_data_type == 0x00D3 {
1867 return self
1868 .write_bool_array_element_workaround(base_array_name, index, value)
1869 .await;
1870 }
1871
1872 let data_type = test_data_type;
1874 let value_bytes = value.to_bytes();
1875
1876 let request = self.build_write_array_request_with_index(
1879 base_array_name,
1880 index,
1881 1, data_type,
1883 &value_bytes,
1884 )?;
1885
1886 let response = self.send_cip_request(&request).await?;
1887 let cip_data = self.extract_cip_from_response(&response)?;
1888
1889 self.check_cip_error(&cip_data)?;
1891
1892 tracing::info!("Array element write completed successfully");
1893 Ok(())
1894 }
1895
1896 async fn write_bool_array_element_workaround(
1904 &mut self,
1905 base_array_name: &str,
1906 index: u32,
1907 value: PlcValue,
1908 ) -> crate::error::Result<()> {
1909 tracing::debug!(
1910 "BOOL array element write - reading DWORD, modifying bit [{}], writing back",
1911 index
1912 );
1913
1914 let dword_index = index / 32;
1915
1916 let response = self
1918 .send_cip_request(&self.build_read_array_request(base_array_name, dword_index, 1))
1919 .await?;
1920 let cip_data = self.extract_cip_from_response(&response)?;
1921
1922 let bool_value = match value {
1924 PlcValue::Bool(b) => b,
1925 _ => {
1926 return Err(EtherNetIpError::Protocol(
1927 "Expected BOOL value for BOOL array element".to_string(),
1928 ));
1929 }
1930 };
1931
1932 let original_dword_value = self.parse_bool_array_dword_response(&cip_data)?;
1934 let mut dword_value = original_dword_value;
1935
1936 let bit_index = (index % 32) as u8;
1937 if bool_value {
1938 dword_value |= 1u32 << bit_index;
1939 } else {
1940 dword_value &= !(1u32 << bit_index);
1941 }
1942
1943 tracing::trace!(
1944 "Modified BOOL[{}] in DWORD: 0x{:08X} -> 0x{:08X} (bit {} = {})",
1945 index,
1946 original_dword_value,
1947 dword_value,
1948 bit_index,
1949 bool_value
1950 );
1951
1952 let write_request = self.build_write_array_request_with_index(
1954 base_array_name,
1955 dword_index,
1956 1,
1957 values::BOOL_ARRAY_DWORD,
1958 &dword_value.to_le_bytes(),
1959 )?;
1960 let write_response = self.send_cip_request(&write_request).await?;
1961 let write_cip_data = self.extract_cip_from_response(&write_response)?;
1962
1963 self.check_cip_error(&write_cip_data)?;
1965
1966 tracing::info!("BOOL array element write completed successfully");
1967 Ok(())
1968 }
1969
1970 #[cfg_attr(not(test), allow(dead_code))]
1999 pub fn build_write_array_request_with_index(
2000 &self,
2001 base_array_name: &str,
2002 start_index: u32,
2003 element_count: u16,
2004 data_type: u16,
2005 data: &[u8],
2006 ) -> crate::error::Result<Vec<u8>> {
2007 let mut cip_request = Vec::new();
2008
2009 cip_request.push(0x4D);
2012
2013 let mut full_path = self.build_base_tag_path(base_array_name);
2016
2017 full_path.extend_from_slice(&self.build_element_id_segment(start_index));
2020
2021 if !full_path.len().is_multiple_of(2) {
2023 full_path.push(0x00);
2024 }
2025
2026 let path_size = (full_path.len() / 2) as u8;
2028 cip_request.push(path_size);
2029 cip_request.extend_from_slice(&full_path);
2030
2031 cip_request.extend_from_slice(&data_type.to_le_bytes());
2034 cip_request.extend_from_slice(&element_count.to_le_bytes());
2035 cip_request.extend_from_slice(data);
2036
2037 Ok(cip_request)
2038 }
2039
2040 pub async fn read_udt_chunked(&mut self, tag_name: &str) -> crate::error::Result<PlcValue> {
2081 self.validate_session().await?;
2082
2083 match self.read_tag(tag_name).await? {
2084 value @ PlcValue::Udt(_) => Ok(value),
2085 other => Err(crate::error::EtherNetIpError::DataTypeMismatch {
2086 expected: "UDT".to_string(),
2087 actual: format!("{other:?}"),
2088 }),
2089 }
2090 }
2091
2092 #[deprecated(
2115 since = "1.2.0",
2116 note = "offset-based UDT member access indexed the CIP envelope, not the UDT payload; use read_udt_chunked + UdtData::parse or direct member tag reads; removal planned for 2.0"
2117 )]
2118 pub async fn read_udt_member_by_offset(
2119 &mut self,
2120 _udt_name: &str,
2121 _member_offset: usize,
2122 _member_size: usize,
2123 _data_type: u16,
2124 ) -> crate::error::Result<PlcValue> {
2125 Err(crate::error::EtherNetIpError::Unsupported {
2126 api: "read_udt_member_by_offset",
2127 reason: "this API indexed the full CIP reply envelope instead of the UDT payload; use read_udt_chunked + UdtData::parse or direct member tag reads instead; removal is planned for 2.0",
2128 })
2129 }
2130
2131 #[deprecated(
2155 since = "1.2.0",
2156 note = "offset-based UDT member writes round-tripped CIP envelope bytes as tag data; use write_udt_member/write_udt_array_member or direct member tag writes; removal planned for 2.0"
2157 )]
2158 pub async fn write_udt_member_by_offset(
2159 &mut self,
2160 _udt_name: &str,
2161 _member_offset: usize,
2162 _member_size: usize,
2163 _data_type: u16,
2164 _value: PlcValue,
2165 ) -> crate::error::Result<()> {
2166 Err(crate::error::EtherNetIpError::Unsupported {
2167 api: "write_udt_member_by_offset",
2168 reason: "this API read the full CIP reply envelope and wrote mutated envelope bytes back to the PLC; use write_udt_member/write_udt_array_member or direct member tag writes instead; removal is planned for 2.0",
2169 })
2170 }
2171
2172 pub async fn get_udt_definition(
2175 &mut self,
2176 udt_name: &str,
2177 ) -> crate::error::Result<UdtDefinition> {
2178 if let Some(cached) = self.udt_manager.lock().await.get_definition(udt_name) {
2180 return Ok(cached.clone());
2181 }
2182
2183 let attributes = self.get_tag_attributes(udt_name).await?;
2185
2186 if attributes.data_type != 0x00A0 {
2188 return Err(crate::error::EtherNetIpError::Protocol(format!(
2189 "Tag '{}' is not a UDT (type: {})",
2190 udt_name, attributes.data_type_name
2191 )));
2192 }
2193
2194 let template_id = attributes.template_instance_id.ok_or_else(|| {
2196 crate::error::EtherNetIpError::Protocol(
2197 "UDT template instance ID not found".to_string(),
2198 )
2199 })?;
2200
2201 let (definition, _structure_size_bytes) = self
2202 .load_udt_definition_from_template(template_id, udt_name)
2203 .await?;
2204
2205 Ok(definition)
2206 }
2207
2208 async fn get_udt_definition_by_template_id(
2209 &mut self,
2210 template_id: u32,
2211 udt_name: &str,
2212 ) -> crate::error::Result<(UdtDefinition, u32)> {
2213 if let Some(cached) = self.udt_manager.lock().await.get_definition(udt_name) {
2214 return Ok((cached.clone(), 0));
2215 }
2216
2217 self.load_udt_definition_from_template(template_id, udt_name)
2218 .await
2219 }
2220
2221 async fn load_udt_definition_from_template(
2222 &mut self,
2223 template_id: u32,
2224 udt_name: &str,
2225 ) -> crate::error::Result<(UdtDefinition, u32)> {
2226 let (template_attributes, template_data) = self.read_udt_template(template_id).await?;
2227 let template = self.udt_manager.lock().await.parse_udt_template(
2228 template_id,
2229 template_attributes.member_count,
2230 template_attributes.structure_size_bytes,
2231 &template_data,
2232 )?;
2233
2234 let definition = UdtDefinition {
2235 name: udt_name.to_string(),
2236 members: template.members,
2237 };
2238
2239 self.udt_manager
2240 .lock()
2241 .await
2242 .add_definition(definition.clone());
2243
2244 Ok((definition, template_attributes.structure_size_bytes))
2245 }
2246
2247 pub async fn get_tag_attributes(
2264 &mut self,
2265 tag_name: &str,
2266 ) -> crate::error::Result<TagAttributes> {
2267 if let Some(cached) = self.udt_manager.lock().await.get_tag_attributes(tag_name) {
2269 return Ok(cached.clone());
2270 }
2271
2272 let request = self.build_get_attributes_request(tag_name)?;
2274
2275 let response = self.send_cip_request(&request).await?;
2277 let cip_data = self.extract_cip_from_response(&response)?;
2278
2279 let attributes = self.parse_attributes_response(tag_name, &cip_data)?;
2281
2282 self.udt_manager
2284 .lock()
2285 .await
2286 .add_tag_attributes(attributes.clone());
2287
2288 Ok(attributes)
2289 }
2290
2291 async fn read_udt_template(
2293 &mut self,
2294 template_id: u32,
2295 ) -> crate::error::Result<(TemplateAttributes, Vec<u8>)> {
2296 let template_attributes = self.get_template_attributes(template_id).await?;
2297 let read_size = template_attributes
2298 .definition_size_words
2299 .checked_mul(4)
2300 .and_then(|bytes| bytes.checked_sub(23))
2301 .ok_or_else(|| {
2302 crate::error::EtherNetIpError::Protocol(format!(
2303 "Template {} reported invalid definition size {} words",
2304 template_id, template_attributes.definition_size_words
2305 ))
2306 })?;
2307
2308 let mut template_data = Vec::with_capacity(read_size as usize);
2309 let mut offset = 0u32;
2310
2311 while offset < read_size {
2312 let chunk_size = (read_size - offset).min(200);
2313 let request = self.build_read_template_request(template_id, offset, chunk_size)?;
2314 let response = self.send_cip_request(&request).await?;
2315 let cip_data = self.extract_cip_from_response(&response)?;
2316 let (chunk, partial_transfer) = self.parse_template_response_chunk(&cip_data)?;
2317
2318 if chunk.is_empty() {
2319 return Err(crate::error::EtherNetIpError::Protocol(format!(
2320 "Template {} returned an empty chunk at offset {}",
2321 template_id, offset
2322 )));
2323 }
2324
2325 offset = offset.saturating_add(chunk.len() as u32);
2326 template_data.extend_from_slice(&chunk);
2327
2328 if !partial_transfer && chunk.len() < chunk_size as usize {
2329 break;
2330 }
2331 }
2332
2333 Ok((template_attributes, template_data))
2334 }
2335
2336 async fn get_template_attributes(
2337 &mut self,
2338 template_id: u32,
2339 ) -> crate::error::Result<TemplateAttributes> {
2340 let request = self.build_get_template_attributes_request(template_id)?;
2341 let response = self.send_cip_request(&request).await?;
2342 let cip_data = self.extract_cip_from_response(&response)?;
2343 self.parse_template_attributes_response(template_id, &cip_data)
2344 }
2345
2346 fn build_get_attributes_request(&self, tag_name: &str) -> crate::error::Result<Vec<u8>> {
2348 let path = self.build_tag_path(tag_name);
2349 let request_data = vec![
2350 0x02, 0x00, 0x01, 0x00, 0x02, 0x00, ];
2354 let request = CipRequest::new(0x03, path, request_data);
2355 let mut encoded = BytesMut::new();
2356 request.encode(&mut encoded)?;
2357 Ok(encoded.to_vec())
2358 }
2359
2360 fn build_get_template_attributes_request(
2361 &self,
2362 template_id: u32,
2363 ) -> crate::error::Result<Vec<u8>> {
2364 let mut request = Vec::new();
2365 let template_id = u16::try_from(template_id).map_err(|_| {
2366 crate::error::EtherNetIpError::Protocol(format!(
2367 "Template instance {} exceeds 16-bit path encoding",
2368 template_id
2369 ))
2370 })?;
2371
2372 request.push(0x03);
2373 request.push(0x03);
2374 request.extend_from_slice(&[0x20, 0x6C, 0x25, 0x00]);
2375 request.extend_from_slice(&template_id.to_le_bytes());
2376 request.extend_from_slice(&[0x04, 0x00]);
2377 request.extend_from_slice(&[0x01, 0x00]);
2378 request.extend_from_slice(&[0x02, 0x00]);
2379 request.extend_from_slice(&[0x04, 0x00]);
2380 request.extend_from_slice(&[0x05, 0x00]);
2381
2382 Ok(request)
2383 }
2384
2385 fn build_read_template_request(
2387 &self,
2388 template_id: u32,
2389 read_offset: u32,
2390 read_size: u32,
2391 ) -> crate::error::Result<Vec<u8>> {
2392 let mut request = Vec::new();
2393 let template_id = u16::try_from(template_id).map_err(|_| {
2394 crate::error::EtherNetIpError::Protocol(format!(
2395 "Template instance {} exceeds 16-bit path encoding",
2396 template_id
2397 ))
2398 })?;
2399 let read_size = u16::try_from(read_size).map_err(|_| {
2400 crate::error::EtherNetIpError::Protocol(format!(
2401 "Template read size {} exceeds 16-bit service limit",
2402 read_size
2403 ))
2404 })?;
2405
2406 request.push(0x4C);
2407 request.push(0x03);
2408 request.extend_from_slice(&[0x20, 0x6C, 0x25, 0x00]);
2409 request.extend_from_slice(&template_id.to_le_bytes());
2410 request.extend_from_slice(&read_offset.to_le_bytes());
2411 request.extend_from_slice(&read_size.to_le_bytes());
2412
2413 Ok(request)
2414 }
2415
2416 fn parse_attributes_response(
2418 &self,
2419 tag_name: &str,
2420 response: &[u8],
2421 ) -> crate::error::Result<TagAttributes> {
2422 let mut response_bytes = response;
2423 let response = CipResponse::decode(&mut response_bytes)?;
2424 if response.service != 0x83 {
2425 return Err(crate::error::EtherNetIpError::Protocol(format!(
2426 "Unexpected Get Attribute List reply service: 0x{:02X}",
2427 response.service
2428 )));
2429 }
2430
2431 if response.status != 0 {
2432 return Err(crate::error::EtherNetIpError::Protocol(format!(
2433 "Get Attribute List for '{}' failed: {}",
2434 tag_name,
2435 self.get_cip_error_message(response.status)
2436 )));
2437 }
2438
2439 if response.data.len() < 2 {
2440 return Err(crate::error::EtherNetIpError::Protocol(
2441 "Attributes response missing attribute count".to_string(),
2442 ));
2443 }
2444
2445 let attr_count = u16::from_le_bytes([response.data[0], response.data[1]]) as usize;
2446 let mut offset = 2;
2447 let mut data_type = None;
2448 let mut template_instance_id = None;
2449 let mut attr_errors = Vec::new();
2450
2451 for _ in 0..attr_count {
2452 if response.data.len() < offset + 4 {
2453 return Err(crate::error::EtherNetIpError::Protocol(
2454 "Attributes response truncated before attribute record header".to_string(),
2455 ));
2456 }
2457
2458 let attr_id = u16::from_le_bytes([response.data[offset], response.data[offset + 1]]);
2459 let attr_status =
2460 u16::from_le_bytes([response.data[offset + 2], response.data[offset + 3]]);
2461 offset += 4;
2462
2463 if attr_status != 0 {
2464 attr_errors.push(format!("attr {attr_id} status 0x{attr_status:04X}"));
2465 continue;
2466 }
2467
2468 match attr_id {
2469 0x0001 => {
2470 if response.data.len() < offset + 2 {
2471 return Err(crate::error::EtherNetIpError::Protocol(
2472 "Attributes response truncated in data type value".to_string(),
2473 ));
2474 }
2475 data_type = Some(u16::from_le_bytes([
2476 response.data[offset],
2477 response.data[offset + 1],
2478 ]));
2479 offset += 2;
2480 }
2481 0x0002 => {
2482 if response.data.len() < offset + 4 {
2483 return Err(crate::error::EtherNetIpError::Protocol(
2484 "Attributes response truncated in instance id value".to_string(),
2485 ));
2486 }
2487 template_instance_id = Some(u32::from_le_bytes([
2488 response.data[offset],
2489 response.data[offset + 1],
2490 response.data[offset + 2],
2491 response.data[offset + 3],
2492 ]));
2493 offset += 4;
2494 }
2495 _ => {
2496 return Err(crate::error::EtherNetIpError::Protocol(format!(
2497 "Unexpected attribute id {attr_id} in Get Attribute List response"
2498 )));
2499 }
2500 }
2501 }
2502
2503 let data_type = data_type.ok_or_else(|| {
2504 crate::error::EtherNetIpError::Protocol(format!(
2505 "Get Attribute List for '{}' did not return data type{}",
2506 tag_name,
2507 if attr_errors.is_empty() {
2508 String::new()
2509 } else {
2510 format!(" ({})", attr_errors.join(", "))
2511 }
2512 ))
2513 })?;
2514 let size = Self::data_type_size(data_type);
2515
2516 let attributes = TagAttributes {
2518 name: tag_name.to_string(),
2519 data_type,
2520 data_type_name: self.get_data_type_name(data_type),
2521 dimensions: Vec::new(), permissions: udt::TagPermissions::ReadWrite, scope: if tag_name.contains(':') {
2524 let parts: Vec<&str> = tag_name.split(':').collect();
2525 if parts.len() >= 2 {
2526 udt::TagScope::Program(parts[0].to_string())
2527 } else {
2528 udt::TagScope::Controller
2529 }
2530 } else {
2531 udt::TagScope::Controller
2532 },
2533 template_instance_id,
2534 size,
2535 };
2536
2537 Ok(attributes)
2538 }
2539
2540 fn data_type_size(data_type: u16) -> u32 {
2541 match data_type {
2542 0x00C1 | 0x00C2 | 0x00C6 => 1,
2543 0x00C3 | 0x00C7 => 2,
2544 0x00C4 | 0x00C8 | 0x00CA => 4,
2545 0x00C5 | 0x00C9 | 0x00CB => 8,
2546 0x00CE => 88,
2547 _ => 4,
2548 }
2549 }
2550
2551 fn parse_template_attributes_response(
2552 &self,
2553 template_id: u32,
2554 response: &[u8],
2555 ) -> crate::error::Result<TemplateAttributes> {
2556 if response.len() < 4 {
2557 return Err(crate::error::EtherNetIpError::Protocol(
2558 "Template attribute response too short".to_string(),
2559 ));
2560 }
2561
2562 let general_status = response[2];
2563 if general_status != 0x00 {
2564 return Err(crate::error::EtherNetIpError::Protocol(format!(
2565 "Template {} attribute read failed: {}",
2566 template_id,
2567 self.get_cip_error_message(general_status)
2568 )));
2569 }
2570
2571 let additional_status_words = response[3] as usize;
2572 let mut offset = 4 + additional_status_words * 2;
2573 if response.len() < offset + 2 {
2574 return Err(crate::error::EtherNetIpError::Protocol(
2575 "Template attribute response missing attribute count".to_string(),
2576 ));
2577 }
2578
2579 let attr_count = u16::from_le_bytes([response[offset], response[offset + 1]]) as usize;
2580 offset += 2;
2581
2582 let mut attributes = TemplateAttributes {
2583 structure_handle: 0,
2584 member_count: 0,
2585 definition_size_words: 0,
2586 structure_size_bytes: 0,
2587 };
2588
2589 for _ in 0..attr_count {
2590 if response.len() < offset + 4 {
2591 return Err(crate::error::EtherNetIpError::Protocol(
2592 "Template attribute response truncated".to_string(),
2593 ));
2594 }
2595
2596 let attr_id = u16::from_le_bytes([response[offset], response[offset + 1]]);
2597 let attr_status = u16::from_le_bytes([response[offset + 2], response[offset + 3]]);
2598 offset += 4;
2599
2600 if attr_status != 0 {
2601 return Err(crate::error::EtherNetIpError::Protocol(format!(
2602 "Template {} attribute {} read returned status 0x{:04X}",
2603 template_id, attr_id, attr_status
2604 )));
2605 }
2606
2607 match attr_id {
2608 1 => {
2609 if response.len() < offset + 2 {
2610 return Err(crate::error::EtherNetIpError::Protocol(
2611 "Template attribute 1 missing value".to_string(),
2612 ));
2613 }
2614 attributes.structure_handle =
2615 u16::from_le_bytes([response[offset], response[offset + 1]]);
2616 offset += 2;
2617 }
2618 2 => {
2619 if response.len() < offset + 2 {
2620 return Err(crate::error::EtherNetIpError::Protocol(
2621 "Template attribute 2 missing value".to_string(),
2622 ));
2623 }
2624 attributes.member_count =
2625 u16::from_le_bytes([response[offset], response[offset + 1]]);
2626 offset += 2;
2627 }
2628 4 => {
2629 if response.len() < offset + 4 {
2630 return Err(crate::error::EtherNetIpError::Protocol(
2631 "Template attribute 4 missing value".to_string(),
2632 ));
2633 }
2634 attributes.definition_size_words = u32::from_le_bytes([
2635 response[offset],
2636 response[offset + 1],
2637 response[offset + 2],
2638 response[offset + 3],
2639 ]);
2640 offset += 4;
2641 }
2642 5 => {
2643 if response.len() < offset + 4 {
2644 return Err(crate::error::EtherNetIpError::Protocol(
2645 "Template attribute 5 missing value".to_string(),
2646 ));
2647 }
2648 attributes.structure_size_bytes = u32::from_le_bytes([
2649 response[offset],
2650 response[offset + 1],
2651 response[offset + 2],
2652 response[offset + 3],
2653 ]);
2654 offset += 4;
2655 }
2656 _ => {
2657 return Err(crate::error::EtherNetIpError::Protocol(format!(
2658 "Unexpected template attribute {} in response",
2659 attr_id
2660 )));
2661 }
2662 }
2663 }
2664
2665 if attributes.definition_size_words == 0 {
2666 return Err(crate::error::EtherNetIpError::Protocol(format!(
2667 "Template {} reported zero definition size",
2668 template_id
2669 )));
2670 }
2671
2672 Ok(attributes)
2673 }
2674
2675 fn parse_template_response_chunk(
2676 &self,
2677 response: &[u8],
2678 ) -> crate::error::Result<(Vec<u8>, bool)> {
2679 if response.len() < 4 {
2680 return Err(crate::error::EtherNetIpError::Protocol(
2681 "Template response too short".to_string(),
2682 ));
2683 }
2684
2685 let general_status = response[2];
2686 let partial_transfer = general_status == 0x06;
2687 if general_status != 0x00 && !partial_transfer {
2688 return Err(crate::error::EtherNetIpError::Protocol(format!(
2689 "Template read failed: {}",
2690 self.get_cip_error_message(general_status)
2691 )));
2692 }
2693
2694 let additional_status_words = response[3] as usize;
2695 let data_start = 4 + additional_status_words * 2;
2696 if data_start > response.len() {
2697 return Err(crate::error::EtherNetIpError::Protocol(
2698 "Template response missing payload".to_string(),
2699 ));
2700 }
2701
2702 Ok((response[data_start..].to_vec(), partial_transfer))
2703 }
2704
2705 fn get_data_type_name(&self, data_type: u16) -> String {
2707 match data_type {
2708 0x00C1 => "BOOL".to_string(),
2709 0x00C2 => "SINT".to_string(),
2710 0x00C3 => "INT".to_string(),
2711 0x00C4 => "DINT".to_string(),
2712 0x00C5 => "LINT".to_string(),
2713 0x00C6 => "USINT".to_string(),
2714 0x00C7 => "UINT".to_string(),
2715 0x00C8 => "UDINT".to_string(),
2716 0x00C9 => "ULINT".to_string(),
2717 0x00CA => "REAL".to_string(),
2718 0x00CB => "LREAL".to_string(),
2719 0x00CE => "STRING".to_string(),
2720 0x00A0 => "UDT".to_string(),
2721 _ => format!("UNKNOWN(0x{:04X})", data_type),
2722 }
2723 }
2724
2725 fn build_tag_list_request_from_instance(
2727 &self,
2728 start_instance: u32,
2729 ) -> crate::error::Result<Vec<u8>> {
2730 let start_instance = u16::try_from(start_instance).map_err(|_| {
2731 crate::error::EtherNetIpError::Protocol(format!(
2732 "Tag discovery start instance {} exceeds 16-bit Symbol Object range",
2733 start_instance
2734 ))
2735 })?;
2736 let mut request = vec![
2737 0x55, 0x03, 0x20, 0x6B, 0x25, 0x00,
2741 ];
2742 request.extend_from_slice(&start_instance.to_le_bytes());
2743
2744 request.extend_from_slice(&[0x02, 0x00]);
2746
2747 request.extend_from_slice(&[0x01, 0x00]);
2749
2750 request.extend_from_slice(&[0x02, 0x00]);
2752
2753 Ok(request)
2754 }
2755
2756 fn build_program_tag_list_request(&self, program_name: &str) -> crate::error::Result<Vec<u8>> {
2758 let scoped_program = if program_name.starts_with("Program:") {
2759 program_name.to_string()
2760 } else {
2761 format!("Program:{program_name}")
2762 };
2763
2764 let mut path = Vec::new();
2765 path.push(0x91);
2766 path.push(scoped_program.len() as u8);
2767 path.extend_from_slice(scoped_program.as_bytes());
2768 if !path.len().is_multiple_of(2) {
2769 path.push(0x00);
2770 }
2771 path.extend_from_slice(&[
2772 0x20, 0x6B, 0x25, 0x00, 0x00, 0x00,
2774 ]);
2775
2776 let path_words = u8::try_from(path.len() / 2).map_err(|_| {
2777 crate::error::EtherNetIpError::Protocol(format!(
2778 "Program tag discovery path too long for '{}'",
2779 program_name
2780 ))
2781 })?;
2782
2783 let mut request = vec![
2784 0x55, path_words,
2786 ];
2787 request.extend_from_slice(&path);
2788
2789 request.extend_from_slice(&[0x02, 0x00]); request.extend_from_slice(&[0x01, 0x00]);
2794
2795 request.extend_from_slice(&[0x02, 0x00]);
2797
2798 Ok(request)
2799 }
2800
2801 fn parse_tag_list_response_page(&self, response: &[u8]) -> crate::error::Result<TagListPage> {
2803 if response.len() < 4 {
2804 return Err(crate::error::EtherNetIpError::Protocol(
2805 "Tag list response too short".to_string(),
2806 ));
2807 }
2808
2809 let general_status = response[2];
2810 let partial_transfer = general_status == 0x06;
2811 if general_status != 0x00 && !partial_transfer {
2812 return Err(crate::error::EtherNetIpError::Protocol(format!(
2813 "Tag discovery failed: {}. Some PLCs may not support tag discovery. Try reading tags directly by name.",
2814 self.get_cip_error_message(general_status)
2815 )));
2816 }
2817
2818 let additional_status_words = response[3] as usize;
2819 let mut offset = 4 + additional_status_words * 2;
2820 if response.len() == offset {
2821 return Ok(TagListPage {
2822 tags: Vec::new(),
2823 last_instance_id: None,
2824 partial_transfer: false,
2825 });
2826 }
2827 if response.len() < offset + 4 {
2828 return Err(crate::error::EtherNetIpError::Protocol(
2829 "Tag list response missing first entry".to_string(),
2830 ));
2831 }
2832 let mut tags = Vec::new();
2833 let mut last_instance_id = None;
2834
2835 while offset + 8 <= response.len() {
2836 let instance_id = u32::from_le_bytes([
2837 response[offset],
2838 response[offset + 1],
2839 response[offset + 2],
2840 response[offset + 3],
2841 ]);
2842 last_instance_id = Some(instance_id);
2843 offset += 4;
2844
2845 let name_length = u16::from_le_bytes([response[offset], response[offset + 1]]) as usize;
2846 offset += 2;
2847
2848 if offset
2849 .checked_add(name_length)
2850 .is_none_or(|end| end > response.len())
2851 {
2852 break;
2853 }
2854
2855 let name_bytes = &response[offset..offset + name_length];
2856 let tag_name = String::from_utf8_lossy(name_bytes).to_string();
2857 offset += name_length;
2858
2859 if offset + 2 > response.len() {
2860 break;
2861 }
2862
2863 let raw_tag_type = u16::from_le_bytes([response[offset], response[offset + 1]]);
2864 offset += 2;
2865
2866 if tag_name.starts_with("__") || tag_name.contains(':') {
2868 continue;
2869 }
2870
2871 let array_dims = ((raw_tag_type & 0x6000) >> 13) as usize;
2872 let is_structure = (raw_tag_type & 0x8000) != 0;
2873 let reserved = (raw_tag_type & 0x1000) != 0;
2874 let type_param = raw_tag_type & 0x0FFF;
2875 let is_user_atomic =
2876 !is_structure && !reserved && (0x0001..=0x00FF).contains(&type_param);
2877 let is_user_structure =
2878 is_structure && !reserved && (0x0100..=0x0EFF).contains(&type_param);
2879
2880 if !is_user_atomic && !is_user_structure {
2881 continue;
2882 }
2883
2884 let data_type = if is_structure {
2885 0x00A0
2886 } else if (raw_tag_type & 0x00FF) == 0x00C1 {
2887 0x00C1
2888 } else {
2889 type_param
2890 };
2891
2892 let template_instance_id = if is_structure && !reserved {
2893 Some(type_param as u32)
2894 } else {
2895 None
2896 };
2897
2898 tags.push(TagAttributes {
2899 name: tag_name,
2900 data_type,
2901 data_type_name: if is_structure {
2902 "UDT".to_string()
2903 } else {
2904 self.get_data_type_name(data_type)
2905 },
2906 dimensions: vec![0; array_dims],
2907 permissions: udt::TagPermissions::ReadWrite,
2908 scope: udt::TagScope::Controller,
2909 template_instance_id,
2910 size: 0,
2911 });
2912 }
2913
2914 Ok(TagListPage {
2915 tags,
2916 last_instance_id,
2917 partial_transfer,
2918 })
2919 }
2920
2921 fn parse_tag_list_response(&self, response: &[u8]) -> crate::error::Result<Vec<TagAttributes>> {
2923 Ok(self.parse_tag_list_response_page(response)?.tags)
2924 }
2925
2926 async fn negotiate_packet_size(&mut self) -> crate::error::Result<()> {
2930 let mut request = vec![
2933 0x03, 0x02, 0x20, 0x02, 0x24, 0x01, ];
2938 request.extend_from_slice(&[0x01, 0x00]); request.extend_from_slice(&[0x04, 0x00]);
2942
2943 let response = self.send_cip_request(&request).await?;
2945 let cip_data = self.extract_cip_from_response(&response)?;
2946
2947 if cip_data.len() >= 12 && cip_data[2] == 0x00 {
2952 let max_packet_size = u16::from_le_bytes([cip_data[10], cip_data[11]]) as u32;
2954
2955 self.max_packet_size
2957 .store(max_packet_size.clamp(504, 4000), Ordering::Relaxed);
2958 tracing::debug!("Negotiated packet size: {} bytes", self.max_packet_size());
2959 } else {
2960 self.max_packet_size.store(4000, Ordering::Relaxed);
2962 tracing::debug!(
2963 "Using default packet size: {} bytes",
2964 self.max_packet_size()
2965 );
2966 }
2967
2968 Ok(())
2969 }
2970
2971 pub async fn write_tag(&mut self, tag_name: &str, value: PlcValue) -> crate::error::Result<()> {
3015 tracing::debug!(
3016 "Writing '{}' to tag '{}'",
3017 match &value {
3018 PlcValue::String(s) => format!("\"{s}\""),
3019 _ => format!("{value:?}"),
3020 },
3021 tag_name
3022 );
3023
3024 let value = if let PlcValue::Udt(udt_data) = &value {
3027 if udt_data.symbol_id == 0 {
3028 tracing::debug!("[UDT WRITE] symbol_id is 0, reading tag to get symbol_id");
3029 let attributes = self.get_tag_attributes(tag_name).await?;
3031 let symbol_id = attributes.template_instance_id.ok_or_else(|| {
3032 crate::error::EtherNetIpError::Protocol(
3033 "UDT template instance ID not found. Cannot write UDT without symbol_id."
3034 .to_string(),
3035 )
3036 })? as i32;
3037
3038 PlcValue::Udt(UdtData {
3040 symbol_id,
3041 data: udt_data.data.clone(),
3042 })
3043 } else {
3044 value
3045 }
3046 } else {
3047 value
3048 };
3049
3050 if let Some((base_path, bit_index)) = self.parse_bit_access(tag_name) {
3051 return match value {
3052 PlcValue::Bool(bit_value) => {
3053 self.write_bit_base_direct(&base_path, bit_index, bit_value)
3054 .await
3055 }
3056 other => Err(crate::error::EtherNetIpError::DataTypeMismatch {
3057 expected: "BOOL".to_string(),
3058 actual: format!("{:?}", other),
3059 }),
3060 };
3061 }
3062
3063 if let Some((base_name, index)) = self.parse_array_element_access(tag_name) {
3067 if !self.has_member_suffix_after_first_array_index(tag_name) {
3068 tracing::debug!(
3069 "Detected array element write: {}[{}], using workaround",
3070 base_name,
3071 index
3072 );
3073 return self
3074 .write_array_element_workaround(&base_name, index, value)
3075 .await;
3076 }
3077
3078 tracing::debug!(
3079 "Array element '{}[{}]' has member access, using TagPath::parse()",
3080 base_name,
3081 index
3082 );
3083 }
3084
3085 if let PlcValue::Bool(_) = value
3086 && let Some((parent_path, index)) = self.parse_final_array_element_access(tag_name)
3087 && self.detect_bool_array_path(&parent_path).await?
3088 {
3089 return self
3090 .write_bool_array_element_workaround(&parent_path, index, value)
3091 .await;
3092 }
3093
3094 self.write_tag_direct(tag_name, &value).await
3095 }
3096
3097 async fn write_tag_direct(
3098 &mut self,
3099 tag_name: &str,
3100 value: &PlcValue,
3101 ) -> crate::error::Result<()> {
3102 if let PlcValue::String(text) = value {
3111 if text.len() <= values::STANDARD_STRING_DATA_LEN {
3112 match self.write_tag_standard(tag_name, value).await {
3113 Ok(()) => return Ok(()),
3114 Err(error) if service_layer::is_2107_type_mismatch(&error) => {
3115 return self.write_string_handle_aware(tag_name, text).await;
3116 }
3117 Err(error) => return Err(error),
3118 }
3119 }
3120 return self.write_string_handle_aware(tag_name, text).await;
3121 }
3122 self.write_tag_standard(tag_name, value).await
3123 }
3124
3125 const SINGLE_PACKET_WRITE_LIMIT: usize = 494;
3129
3130 async fn write_string_handle_aware(
3135 &mut self,
3136 tag_name: &str,
3137 value: &str,
3138 ) -> crate::error::Result<()> {
3139 let (handle, struct_size) = match self.read_tag(tag_name).await? {
3141 PlcValue::String(_) => (
3142 values::STANDARD_STRING_HANDLE,
3143 values::STANDARD_STRING_PAYLOAD_LEN,
3144 ),
3145 PlcValue::Udt(udt) if udt.data.len() >= 6 => (
3146 u16::from_le_bytes([udt.data[0], udt.data[1]]),
3147 udt.data.len() - 2,
3148 ),
3149 other => {
3150 return Err(EtherNetIpError::DataTypeMismatch {
3151 expected: "STRING structure".to_string(),
3152 actual: format!("{other:?}"),
3153 });
3154 }
3155 };
3156
3157 let capacity = struct_size.saturating_sub(4);
3159 if value.len() > capacity {
3160 return Err(EtherNetIpError::StringTooLong {
3161 max_length: capacity,
3162 actual_length: value.len(),
3163 });
3164 }
3165
3166 let mut payload = vec![0u8; struct_size];
3168 payload[0..4].copy_from_slice(&(value.len() as u32).to_le_bytes());
3169 payload[4..4 + value.len()].copy_from_slice(value.as_bytes());
3170
3171 let mut data = Vec::with_capacity(6 + payload.len());
3173 data.extend_from_slice(&values::AB_UDT.to_le_bytes());
3174 data.extend_from_slice(&handle.to_le_bytes());
3175 data.extend_from_slice(&[0x01, 0x00]);
3176 data.extend_from_slice(&payload);
3177
3178 let path = self.build_tag_path(tag_name);
3179 let request = CipRequest::new(WRITE_TAG, path, data);
3180 let mut cip_request = BytesMut::new();
3181 request.encode(&mut cip_request)?;
3182
3183 if cip_request.len() > Self::SINGLE_PACKET_WRITE_LIMIT {
3184 return self
3185 .write_string_fragmented(tag_name, handle, &payload)
3186 .await;
3187 }
3188
3189 let response = self.send_cip_request(&cip_request).await?;
3190 let cip_response = self.extract_cip_from_response(&response)?;
3191 self.check_cip_error(&cip_response)?;
3192 Ok(())
3193 }
3194
3195 async fn write_string_fragmented(
3196 &mut self,
3197 tag_name: &str,
3198 handle: u16,
3199 payload: &[u8],
3200 ) -> crate::error::Result<()> {
3201 let max_fragment = self.max_write_fragment_payload_len(tag_name, handle)?;
3202 let mut offset = 0usize;
3203
3204 while offset < payload.len() {
3205 let end = usize::min(offset + max_fragment, payload.len());
3206 let request = self.build_write_fragmented_request(
3207 tag_name,
3208 handle,
3209 offset as u32,
3210 &payload[offset..end],
3211 )?;
3212 let response = self.send_cip_request(&request).await?;
3213 let cip_response = self.extract_cip_from_response(&response)?;
3214 if cip_response.first().copied() != Some(WRITE_TAG_FRAGMENTED_REPLY) {
3215 return Err(EtherNetIpError::Protocol(format!(
3216 "Unexpected Write Tag Fragmented reply service: 0x{:02X}",
3217 cip_response.first().copied().unwrap_or(0)
3218 )));
3219 }
3220 self.check_cip_error(&cip_response)?;
3221 offset = end;
3222 }
3223
3224 Ok(())
3225 }
3226
3227 async fn write_tag_standard(
3228 &mut self,
3229 tag_name: &str,
3230 value: &PlcValue,
3231 ) -> crate::error::Result<()> {
3232 let cip_request = self.build_write_request(tag_name, value)?;
3233
3234 let response = self.send_cip_request(&cip_request).await?;
3235
3236 let cip_response = self.extract_cip_from_response(&response)?;
3238
3239 if cip_response.len() < 3 {
3240 return Err(EtherNetIpError::Protocol(
3241 "Write response too short".to_string(),
3242 ));
3243 }
3244
3245 let service_reply = cip_response[0]; let general_status = cip_response[2]; tracing::trace!(
3249 "Write response - Service: 0x{:02X}, Status: 0x{:02X}",
3250 service_reply,
3251 general_status
3252 );
3253
3254 if let Err(e) = self.check_cip_error(&cip_response) {
3256 tracing::error!("[WRITE] CIP Error: {}", e);
3257 return Err(e);
3258 }
3259
3260 tracing::info!("Write operation completed successfully");
3261 Ok(())
3262 }
3263
3264 fn build_write_request(
3274 &self,
3275 tag_name: &str,
3276 value: &PlcValue,
3277 ) -> crate::error::Result<Vec<u8>> {
3278 tracing::debug!("Building write request for tag: '{}'", tag_name);
3279
3280 let path = self.build_tag_path(tag_name);
3282
3283 if let PlcValue::String(string_value) = value
3284 && string_value.len() > values::STANDARD_STRING_DATA_LEN
3285 {
3286 return Err(EtherNetIpError::StringTooLong {
3287 max_length: values::STANDARD_STRING_DATA_LEN,
3288 actual_length: string_value.len(),
3289 });
3290 }
3291
3292 let mut data = BytesMut::new();
3293 data.extend_from_slice(&values::write_data_type_bytes(value));
3294 data.extend_from_slice(&[0x01, 0x00]); values::encode_payload(value, &mut data);
3296
3297 let request = CipRequest::new(WRITE_TAG, path, data.to_vec());
3298 let mut cip_request = BytesMut::new();
3299 request.encode(&mut cip_request)?;
3300
3301 tracing::trace!(
3302 "Built CIP write request ({} bytes): {:02X?}",
3303 cip_request.len(),
3304 cip_request
3305 );
3306 Ok(cip_request.to_vec())
3307 }
3308
3309 fn build_read_fragmented_request(
3310 &self,
3311 tag_name: &str,
3312 element_count: u16,
3313 byte_offset: u32,
3314 ) -> crate::error::Result<Vec<u8>> {
3315 let mut data = Vec::with_capacity(6);
3316 data.extend_from_slice(&element_count.to_le_bytes());
3317 data.extend_from_slice(&byte_offset.to_le_bytes());
3318 let request = CipRequest::new(READ_TAG_FRAGMENTED, self.build_tag_path(tag_name), data);
3319 let mut cip_request = BytesMut::new();
3320 request.encode(&mut cip_request)?;
3321 Ok(cip_request.to_vec())
3322 }
3323
3324 fn build_write_fragmented_request(
3325 &self,
3326 tag_name: &str,
3327 handle: u16,
3328 byte_offset: u32,
3329 fragment: &[u8],
3330 ) -> crate::error::Result<Vec<u8>> {
3331 let mut data = Vec::with_capacity(10 + fragment.len());
3332 data.extend_from_slice(&values::AB_UDT.to_le_bytes());
3333 data.extend_from_slice(&handle.to_le_bytes());
3334 data.extend_from_slice(&1u16.to_le_bytes());
3335 data.extend_from_slice(&byte_offset.to_le_bytes());
3336 data.extend_from_slice(fragment);
3337 let request = CipRequest::new(WRITE_TAG_FRAGMENTED, self.build_tag_path(tag_name), data);
3338 let mut cip_request = BytesMut::new();
3339 request.encode(&mut cip_request)?;
3340 Ok(cip_request.to_vec())
3341 }
3342
3343 fn max_write_fragment_payload_len(
3344 &self,
3345 tag_name: &str,
3346 handle: u16,
3347 ) -> crate::error::Result<usize> {
3348 let empty_request = self.build_write_fragmented_request(tag_name, handle, 0, &[])?;
3349 if empty_request.len() >= Self::SINGLE_PACKET_WRITE_LIMIT {
3350 return Err(EtherNetIpError::Protocol(format!(
3351 "Write Tag Fragmented request for '{tag_name}' has {} bytes of overhead, exceeding the {}-byte single-packet limit before payload",
3352 empty_request.len(),
3353 Self::SINGLE_PACKET_WRITE_LIMIT
3354 )));
3355 }
3356
3357 Ok(Self::SINGLE_PACKET_WRITE_LIMIT - empty_request.len())
3358 }
3359
3360 pub fn build_list_tags_request(&self) -> Vec<u8> {
3361 tracing::debug!("Building list tags request");
3362
3363 let path_array = vec![
3365 0x20, 0x6B, 0x25, 0x00, 0x00, 0x00,
3371 ];
3372
3373 let request_data = vec![0x02, 0x00, 0x01, 0x00, 0x02, 0x00];
3375
3376 let request = CipRequest::new(0x55, path_array, request_data);
3378 let mut cip_request = BytesMut::new();
3379 request
3380 .encode(&mut cip_request)
3381 .expect("list-tags request path is static and valid");
3382
3383 tracing::trace!(
3384 "Built CIP list tags request ({} bytes): {:02X?}",
3385 cip_request.len(),
3386 cip_request
3387 );
3388
3389 cip_request.to_vec()
3390 }
3391
3392 fn parse_extended_error(&self, cip_data: &[u8]) -> crate::error::Result<String> {
3406 if cip_data.len() < 4 {
3407 return Err(EtherNetIpError::Protocol(
3408 "CIP response too short for additional-status check".to_string(),
3409 ));
3410 }
3411
3412 let additional_status_size = cip_data[3] as usize; if additional_status_size == 0 {
3414 return Ok("Extended error (no additional status)".to_string());
3415 }
3416
3417 let expected_len = 4 + (additional_status_size * 2);
3418 if cip_data.len() < expected_len {
3419 return Err(EtherNetIpError::Protocol(format!(
3420 "Additional-status response truncated: expected {expected_len} bytes, got {}",
3421 cip_data.len()
3422 )));
3423 }
3424
3425 let extended_error_code = u16::from_le_bytes([cip_data[4], cip_data[5]]);
3426 let error_msg = match extended_error_code {
3427 0x0001 => "Connection failure (extended)".to_string(),
3428 0x0002 => "Resource unavailable (extended)".to_string(),
3429 0x0003 => "Invalid parameter value (extended)".to_string(),
3430 0x0004 => "Path segment error (extended)".to_string(),
3431 0x0005 => "Path destination unknown (extended)".to_string(),
3432 0x0006 => "Partial transfer (extended)".to_string(),
3433 0x0007 => "Connection lost (extended)".to_string(),
3434 0x0008 => "Service not supported (extended)".to_string(),
3435 0x0009 => "Invalid attribute value (extended)".to_string(),
3436 0x000A => "Attribute list error (extended)".to_string(),
3437 0x000B => "Already in requested mode/state (extended)".to_string(),
3438 0x000C => "Object state conflict (extended)".to_string(),
3439 0x000D => "Object already exists (extended)".to_string(),
3440 0x000E => "Attribute not settable (extended)".to_string(),
3441 0x000F => "Privilege violation (extended)".to_string(),
3442 0x0010 => "Device state conflict (extended)".to_string(),
3443 0x0011 => "Reply data too large (extended)".to_string(),
3444 0x0012 => "Fragmentation of a primitive value (extended)".to_string(),
3445 0x0013 => "Not enough data (extended)".to_string(),
3446 0x0014 => "Attribute not supported (extended)".to_string(),
3447 0x0015 => "Too much data (extended)".to_string(),
3448 0x0016 => "Object does not exist (extended)".to_string(),
3449 0x0017 => "Service fragmentation sequence not in progress (extended)".to_string(),
3450 0x0018 => "No stored attribute data (extended)".to_string(),
3451 0x0019 => "Store operation failure (extended)".to_string(),
3452 0x001A => "Routing failure, request packet too large (extended)".to_string(),
3453 0x001B => "Routing failure, response packet too large (extended)".to_string(),
3454 0x001C => "Missing attribute list entry data (extended)".to_string(),
3455 0x001D => "Invalid attribute value list (extended)".to_string(),
3456 0x001E => "Embedded service error (extended)".to_string(),
3457 0x001F => "Vendor specific error (extended)".to_string(),
3458 0x0020 => "Invalid parameter (extended)".to_string(),
3459 0x0021 => "Write-once value or medium already written (extended)".to_string(),
3460 0x0022 => "Invalid reply received (extended)".to_string(),
3461 0x0023 => "Buffer overflow (extended)".to_string(),
3462 0x0024 => "Invalid message format (extended)".to_string(),
3463 0x0025 => "Key failure in path (extended)".to_string(),
3464 0x0026 => "Path size invalid (extended)".to_string(),
3465 0x0027 => "Unexpected attribute in list (extended)".to_string(),
3466 0x0028 => "Invalid member ID (extended)".to_string(),
3467 0x0029 => "Member not settable (extended)".to_string(),
3468 0x002A => "Group 2 only server general failure (extended)".to_string(),
3469 0x002B => "Unknown Modbus error (extended)".to_string(),
3470 0x002C => "Attribute not gettable (extended)".to_string(),
3471 0x2107 => format!(
3472 "Read/Write Tag data-type mismatch extended error: 0x{extended_error_code:04X}. Raw bytes: [0x{:02X}, 0x{:02X}]. Check that the request data type matches the target tag; STRING members inside UDTs can also surface this current-encoding rejection.",
3473 cip_data[4], cip_data[5]
3474 ),
3475 _ => format!(
3476 "Unknown extended CIP error code: 0x{extended_error_code:04X}. Raw bytes: [0x{:02X}, 0x{:02X}]",
3477 cip_data[4], cip_data[5]
3478 ),
3479 };
3480
3481 Ok(error_msg)
3482 }
3483
3484 fn check_cip_error(&self, cip_data: &[u8]) -> crate::error::Result<()> {
3487 if cip_data.len() < 3 {
3488 return Err(EtherNetIpError::Protocol(
3489 "CIP response too short for status check".to_string(),
3490 ));
3491 }
3492
3493 let general_status = cip_data[2];
3494
3495 if general_status == 0x00 {
3496 return Ok(());
3498 }
3499
3500 if cip_data.get(3).copied().unwrap_or(0) > 0 {
3502 let error_msg = self.parse_extended_error(cip_data)?;
3503 return Err(EtherNetIpError::Protocol(format!(
3504 "CIP Extended Error: {error_msg}"
3505 )));
3506 }
3507
3508 let error_msg = self.get_cip_error_message(general_status);
3510 if general_status == 0x01 {
3511 return Err(EtherNetIpError::Connection(format!(
3512 "CIP Error 0x{general_status:02X}: {error_msg}"
3513 )));
3514 }
3515 if general_status == 0x07 {
3516 return Err(EtherNetIpError::ConnectionLost(format!(
3517 "CIP Error 0x{general_status:02X}: {error_msg}"
3518 )));
3519 }
3520 Err(EtherNetIpError::Protocol(format!(
3521 "CIP Error 0x{general_status:02X}: {error_msg}"
3522 )))
3523 }
3524
3525 fn get_cip_error_message(&self, status: u8) -> String {
3526 match status {
3527 0x00 => "Success".to_string(),
3528 0x01 => "Connection failure".to_string(),
3529 0x02 => "Resource unavailable".to_string(),
3530 0x03 => "Invalid parameter value".to_string(),
3531 0x04 => "Path segment error".to_string(),
3532 0x05 => "Path destination unknown".to_string(),
3533 0x06 => "Partial transfer".to_string(),
3534 0x07 => "Connection lost".to_string(),
3535 0x08 => "Service not supported".to_string(),
3536 0x09 => "Invalid attribute value".to_string(),
3537 0x0A => "Attribute list error".to_string(),
3538 0x0B => "Already in requested mode/state".to_string(),
3539 0x0C => "Object state conflict".to_string(),
3540 0x0D => "Object already exists".to_string(),
3541 0x0E => "Attribute not settable".to_string(),
3542 0x0F => "Privilege violation".to_string(),
3543 0x10 => "Device state conflict".to_string(),
3544 0x11 => "Reply data too large".to_string(),
3545 0x12 => "Fragmentation of a primitive value".to_string(),
3546 0x13 => "Not enough data".to_string(),
3547 0x14 => "Attribute not supported".to_string(),
3548 0x15 => "Too much data".to_string(),
3549 0x16 => "Object does not exist".to_string(),
3550 0x17 => "Service fragmentation sequence not in progress".to_string(),
3551 0x18 => "No stored attribute data".to_string(),
3552 0x19 => "Store operation failure".to_string(),
3553 0x1A => "Routing failure, request packet too large".to_string(),
3554 0x1B => "Routing failure, response packet too large".to_string(),
3555 0x1C => "Missing attribute list entry data".to_string(),
3556 0x1D => "Invalid attribute value list".to_string(),
3557 0x1E => "Embedded service error".to_string(),
3558 0x1F => "Vendor specific error".to_string(),
3559 0x20 => "Invalid parameter".to_string(),
3560 0x21 => "Write-once value or medium already written".to_string(),
3561 0x22 => "Invalid reply received".to_string(),
3562 0x23 => "Buffer overflow".to_string(),
3563 0x24 => "Invalid message format".to_string(),
3564 0x25 => "Key failure in path".to_string(),
3565 0x26 => "Path size invalid".to_string(),
3566 0x27 => "Unexpected attribute in list".to_string(),
3567 0x28 => "Invalid member ID".to_string(),
3568 0x29 => "Member not settable".to_string(),
3569 0x2A => "Group 2 only server general failure".to_string(),
3570 0x2B => "Unknown Modbus error".to_string(),
3571 0x2C => "Attribute not gettable".to_string(),
3572 _ => format!("Unknown CIP error code: 0x{status:02X}"),
3573 }
3574 }
3575
3576 fn describe_multiple_service_error(
3577 &self,
3578 general_status: u8,
3579 operations: &[BatchOperation],
3580 ) -> String {
3581 if general_status == 0x1E
3582 && operations.iter().any(|op| {
3583 matches!(
3584 op,
3585 BatchOperation::Write {
3586 value: PlcValue::String(_),
3587 ..
3588 }
3589 )
3590 })
3591 {
3592 return "Multiple Service Response error: 0x1E (Embedded service error). A batched STRING write failed inside the controller; inspect the embedded reply for the rejected service and data-type details.".to_string();
3593 }
3594
3595 format!("Multiple Service Response error: 0x{general_status:02X}")
3596 }
3597
3598 async fn validate_session(&mut self) -> crate::error::Result<()> {
3599 let time_since_activity = self.last_activity.lock().await.elapsed();
3600
3601 if time_since_activity > Duration::from_secs(30) {
3603 self.send_keep_alive().await?;
3604 }
3605
3606 Ok(())
3607 }
3608
3609 async fn send_keep_alive(&mut self) -> crate::error::Result<()> {
3610 self.ensure_stream_usable()?;
3611 let packet = vec![0u8; 24];
3615 let mut stream = self.stream.lock().await;
3620 stream.write_all(&packet).await?;
3621 *self.last_activity.lock().await = Instant::now();
3622 Ok(())
3623 }
3624
3625 fn build_unconnected_send(&self, embedded_message: &[u8]) -> Vec<u8> {
3642 let mut ucmm = vec![
3643 0x52, 0x02,
3646 0x20, 0x06, 0x24, 0x01, 0x0A, 0xF0,
3654 ];
3655
3656 let msg_len = embedded_message.len() as u16;
3658 ucmm.extend_from_slice(&msg_len.to_le_bytes());
3659
3660 ucmm.extend_from_slice(embedded_message);
3662
3663 if embedded_message.len() % 2 == 1 {
3665 ucmm.push(0x00);
3666 }
3667
3668 let route_path_bytes = if let Some(route_path) = self.route_path_snapshot() {
3671 route_path.to_cip_bytes()
3672 } else {
3673 Vec::new()
3674 };
3675
3676 let route_path_words = if route_path_bytes.is_empty() {
3677 0
3678 } else {
3679 (route_path_bytes.len() / 2) as u8
3680 };
3681 ucmm.push(route_path_words);
3682
3683 ucmm.push(0x00);
3685
3686 if !route_path_bytes.is_empty() {
3688 tracing::trace!(
3689 "Adding route path to Unconnected Send: {:02X?} ({} bytes, {} words)",
3690 route_path_bytes,
3691 route_path_bytes.len(),
3692 route_path_words
3693 );
3694 ucmm.extend_from_slice(&route_path_bytes);
3695 }
3696
3697 ucmm
3698 }
3699
3700 pub async fn send_cip_request(&self, cip_request: &[u8]) -> Result<Vec<u8>> {
3707 tracing::trace!(
3708 "Sending CIP request ({} bytes): {:02X?}",
3709 cip_request.len(),
3710 cip_request
3711 );
3712
3713 let ucmm_message = self.build_unconnected_send(cip_request);
3716 let diagnostic_operation = Self::diagnostic_operation_for(cip_request);
3717
3718 tracing::trace!(
3719 "Unconnected Send message ({} bytes): {:02X?}",
3720 ucmm_message.len(),
3721 &ucmm_message[..std::cmp::min(64, ucmm_message.len())]
3722 );
3723
3724 let response_data = match self.send_rr_data_item(&ucmm_message).await {
3725 Ok(response_data) => response_data,
3726 Err(error) => {
3727 self.diagnostic_counters
3728 .record_failure(diagnostic_operation, &error);
3729 return Err(error);
3730 }
3731 };
3732
3733 if let Ok(raw_cip_data) = self.extract_unconnected_data_item(&response_data) {
3734 let use_direct_fallback = raw_cip_data.len() >= 3
3735 && raw_cip_data[0] == 0xD2
3736 && raw_cip_data[2] != 0x00
3737 && cip_request.first().copied() != Some(READ_TAG_FRAGMENTED)
3738 && self.route_path_snapshot().is_none();
3739
3740 if use_direct_fallback {
3741 tracing::warn!(
3742 "Unconnected Send returned 0xD2 status 0x{:02X}; retrying with direct CIP SendRRData fallback",
3743 raw_cip_data[2]
3744 );
3745 return match self.send_rr_data_item(cip_request).await {
3746 Ok(response_data) => {
3747 self.diagnostic_counters
3748 .record_success(diagnostic_operation);
3749 Ok(response_data)
3750 }
3751 Err(error) => {
3752 self.diagnostic_counters
3753 .record_failure(diagnostic_operation, &error);
3754 Err(error)
3755 }
3756 };
3757 }
3758
3759 if raw_cip_data.len() >= 3 && raw_cip_data[2] != 0x00 {
3760 self.diagnostic_counters
3761 .record_cip_failure(diagnostic_operation);
3762 } else {
3763 self.diagnostic_counters
3764 .record_success(diagnostic_operation);
3765 }
3766 } else {
3767 self.diagnostic_counters
3768 .record_success(diagnostic_operation);
3769 }
3770
3771 Ok(response_data)
3772 }
3773
3774 async fn send_rr_data_item(&self, item_data: &[u8]) -> Result<Vec<u8>> {
3775 let send_data = SendDataRequest::unconnected(item_data);
3776 let mut packet = BytesMut::new();
3777 let mut cpf = BytesMut::new();
3778 send_data.encode(&mut cpf);
3779 let sender_context = self.next_sender_context();
3780 EncapsulationHeader::send_rr_data_with_context(
3781 cpf.len() as u16,
3782 self.session_handle(),
3783 sender_context,
3784 )
3785 .encode(&mut packet);
3786 packet.extend_from_slice(&cpf);
3787
3788 tracing::trace!(
3789 "Built packet ({} bytes): {:02X?}",
3790 packet.len(),
3791 &packet[..std::cmp::min(64, packet.len())]
3792 );
3793
3794 self.ensure_stream_usable()?;
3796 let mut stream = self.stream.lock().await;
3797 self.ensure_stream_usable()?;
3798 self.stream_poisoned.store(true, Ordering::Relaxed);
3799 if let Err(e) = stream.write_all(&packet).await {
3800 return Err(EtherNetIpError::Io(e));
3801 }
3802
3803 let mut header = [0u8; 24];
3805 match timeout(Duration::from_secs(10), stream.read_exact(&mut header)).await {
3806 Ok(Ok(_)) => {}
3807 Ok(Err(e)) => return Err(EtherNetIpError::Io(e)),
3808 Err(_) => return Err(EtherNetIpError::Timeout(Duration::from_secs(10))),
3809 }
3810
3811 let mut header_bytes = &header[..];
3813 let response_header = EncapsulationHeader::decode(&mut header_bytes)?;
3814 if response_header.sender_context != sender_context {
3815 return Err(EtherNetIpError::Protocol(format!(
3816 "SendRRData sender_context mismatch: expected {:02X?}, got {:02X?}",
3817 sender_context, response_header.sender_context
3818 )));
3819 }
3820
3821 let response_length = response_header.length as usize;
3823 if response_header.status != 0 {
3824 if response_length > 0 {
3825 let mut response_data = vec![0u8; response_length];
3826 match timeout(
3827 Duration::from_secs(10),
3828 stream.read_exact(&mut response_data),
3829 )
3830 .await
3831 {
3832 Ok(Ok(_)) => {}
3833 Ok(Err(e)) => return Err(EtherNetIpError::Io(e)),
3834 Err(_) => return Err(EtherNetIpError::Timeout(Duration::from_secs(10))),
3835 }
3836 }
3837 self.stream_poisoned.store(false, Ordering::Relaxed);
3838 return Err(EtherNetIpError::Protocol(format!(
3839 "EIP Command failed. Status: 0x{:08X}",
3840 response_header.status
3841 )));
3842 }
3843
3844 if response_length == 0 {
3845 self.stream_poisoned.store(false, Ordering::Relaxed);
3846 return Ok(Vec::new());
3847 }
3848
3849 let mut response_data = vec![0u8; response_length];
3851 match timeout(
3852 Duration::from_secs(10),
3853 stream.read_exact(&mut response_data),
3854 )
3855 .await
3856 {
3857 Ok(Ok(_)) => {}
3858 Ok(Err(e)) => return Err(EtherNetIpError::Io(e)),
3859 Err(_) => return Err(EtherNetIpError::Timeout(Duration::from_secs(10))),
3860 }
3861
3862 self.stream_poisoned.store(false, Ordering::Relaxed);
3863
3864 *self.last_activity.lock().await = Instant::now();
3866
3867 tracing::trace!(
3868 "Received response ({} bytes): {:02X?}",
3869 response_data.len(),
3870 &response_data[..std::cmp::min(32, response_data.len())]
3871 );
3872
3873 Ok(response_data)
3874 }
3875
3876 fn extract_unconnected_data_item(&self, response: &[u8]) -> crate::error::Result<Vec<u8>> {
3877 let mut response = response;
3878 let send_data = SendDataRequest::decode(&mut response)?;
3879 if let Some(item) = send_data
3880 .items
3881 .into_iter()
3882 .find(|item| item.type_id == 0x00B2)
3883 {
3884 return Ok(item.data);
3885 }
3886
3887 Err(EtherNetIpError::Protocol(
3888 "No Unconnected Data Item (0x00B2) found in response".to_string(),
3889 ))
3890 }
3891
3892 fn unwrap_unconnected_send_reply(&self, cip_data: &[u8]) -> crate::error::Result<Vec<u8>> {
3893 if cip_data.is_empty() || cip_data[0] != 0xD2 {
3894 return Ok(cip_data.to_vec());
3895 }
3896
3897 if cip_data.len() < 4 {
3898 return Err(EtherNetIpError::Protocol(
3899 "Unconnected Send reply too short".to_string(),
3900 ));
3901 }
3902
3903 let general_status = cip_data[2];
3904 let additional_status_words = cip_data[3] as usize;
3905 let embedded_offset = 4 + (additional_status_words * 2);
3906
3907 if general_status != 0x00 {
3908 let error_msg = self.get_cip_error_message(general_status);
3909 return Err(EtherNetIpError::Protocol(format!(
3910 "Unconnected Send failed (0xD2): CIP Error 0x{general_status:02X}: {error_msg}"
3911 )));
3912 }
3913
3914 if embedded_offset >= cip_data.len() {
3915 return Err(EtherNetIpError::Protocol(
3916 "Unconnected Send succeeded but no embedded response payload was returned"
3917 .to_string(),
3918 ));
3919 }
3920
3921 Ok(cip_data[embedded_offset..].to_vec())
3922 }
3923
3924 fn extract_cip_from_response(&self, response: &[u8]) -> crate::error::Result<Vec<u8>> {
3926 tracing::trace!(
3927 "Extracting CIP from response ({} bytes): {:02X?}",
3928 response.len(),
3929 &response[..std::cmp::min(32, response.len())]
3930 );
3931 let cip_data = self.extract_unconnected_data_item(response)?;
3932 tracing::trace!(
3933 "Found Unconnected Data Item, extracted CIP data ({} bytes)",
3934 cip_data.len()
3935 );
3936 tracing::trace!(
3937 "CIP data bytes: {:02X?}",
3938 &cip_data[..std::cmp::min(16, cip_data.len())]
3939 );
3940 self.unwrap_unconnected_send_reply(&cip_data)
3941 }
3942
3943 fn parse_cip_response(&self, cip_response: &[u8]) -> crate::error::Result<PlcValue> {
3945 tracing::trace!(
3946 "Parsing CIP response ({} bytes): {:02X?}",
3947 cip_response.len(),
3948 cip_response
3949 );
3950
3951 if let Err(e) = self.check_cip_error(cip_response) {
3952 tracing::error!("CIP Error: {}", e);
3953 return Err(e);
3954 }
3955
3956 let mut response_bytes = cip_response;
3957 let response = CipResponse::decode(&mut response_bytes)?;
3958
3959 if response.service == 0xCC {
3960 self.decode_type_prefixed_value(&response.data)
3961 } else if response.service == 0xCD {
3962 tracing::debug!("Write operation successful");
3963 Ok(PlcValue::Bool(true))
3964 } else {
3965 Err(EtherNetIpError::Protocol(format!(
3966 "Unknown service reply: 0x{:02X}",
3967 response.service
3968 )))
3969 }
3970 }
3971
3972 fn parse_read_fragmented_response<'a>(
3973 &self,
3974 cip_response: &'a [u8],
3975 ) -> crate::error::Result<(u8, &'a [u8])> {
3976 if cip_response.len() < 4 {
3977 return Err(EtherNetIpError::Protocol(
3978 "Read Tag Fragmented response too short".to_string(),
3979 ));
3980 }
3981
3982 let service = cip_response[0];
3983 if service != READ_TAG_FRAGMENTED_REPLY {
3984 return Err(EtherNetIpError::Protocol(format!(
3985 "Unexpected Read Tag Fragmented reply service: 0x{service:02X}"
3986 )));
3987 }
3988
3989 let status = cip_response[2];
3990 if status != CIP_STATUS_SUCCESS && status != CIP_STATUS_PARTIAL_TRANSFER {
3991 self.check_cip_error(cip_response)?;
3992 }
3993
3994 Ok((status, &cip_response[4..]))
3995 }
3996
3997 fn decode_type_prefixed_value(&self, data: &[u8]) -> crate::error::Result<PlcValue> {
3998 if data.len() < 2 {
3999 return Err(EtherNetIpError::Protocol(
4000 "Read response too short for data".to_string(),
4001 ));
4002 }
4003
4004 let data_type = u16::from_le_bytes([data[0], data[1]]);
4005 let value_data = &data[2..];
4006 tracing::trace!(
4007 "Data type: 0x{:04X}, Value data ({} bytes): {:02X?}",
4008 data_type,
4009 value_data.len(),
4010 value_data
4011 );
4012 Ok(values::decode_payload(data_type, value_data)?)
4013 }
4014
4015 pub async fn unregister_session(&mut self) -> crate::error::Result<()> {
4017 tracing::info!("Unregistering session...");
4018
4019 let mut packet = BytesMut::with_capacity(24);
4020 EncapsulationHeader::new(UNREGISTER_SESSION, 0, self.session_handle()).encode(&mut packet);
4021
4022 self.stream
4023 .lock()
4024 .await
4025 .write_all(&packet)
4026 .await
4027 .map_err(EtherNetIpError::Io)?;
4028
4029 tracing::info!("Session unregistered");
4030 Ok(())
4031 }
4032
4033 fn build_read_request(&self, tag_name: &str) -> crate::error::Result<Vec<u8>> {
4035 self.build_read_request_with_count(tag_name, 1)
4036 }
4037
4038 fn build_read_request_with_count(
4042 &self,
4043 tag_name: &str,
4044 element_count: u16,
4045 ) -> crate::error::Result<Vec<u8>> {
4046 tracing::debug!(
4047 "Building read request for tag: '{}' with count: {}",
4048 tag_name,
4049 element_count
4050 );
4051
4052 let path = self.build_tag_path(tag_name);
4054
4055 let path_size_words = (path.len() / 2) as u8;
4057 tracing::debug!(
4058 "Path size calculation: {} bytes / 2 = {} words for tag '{}'",
4059 path.len(),
4060 path_size_words,
4061 tag_name
4062 );
4063 tracing::debug!(
4064 "Path bytes ({} bytes, {} words) for tag '{}': {:02X?}",
4065 path.len(),
4066 path_size_words,
4067 tag_name,
4068 path
4069 );
4070 let request = CipRequest::new(READ_TAG, path, element_count.to_le_bytes().to_vec());
4071 let mut cip_request = BytesMut::new();
4072 request.encode(&mut cip_request)?;
4073
4074 tracing::debug!(
4075 "Built CIP read request ({} bytes) for tag '{}': {:02X?}",
4076 cip_request.len(),
4077 tag_name,
4078 cip_request
4079 );
4080 Ok(cip_request.to_vec())
4081 }
4082
4083 #[cfg_attr(not(test), allow(dead_code))]
4092 pub fn build_element_id_segment(&self, index: u32) -> Vec<u8> {
4093 let mut segment = Vec::new();
4094
4095 if index <= 255 {
4096 segment.push(0x28);
4099 segment.push(index as u8);
4100 } else if index <= 65535 {
4101 segment.push(0x29);
4104 segment.push(0x00); segment.extend_from_slice(&(index as u16).to_le_bytes());
4106 } else {
4107 segment.push(0x2A);
4110 segment.push(0x00); segment.extend_from_slice(&index.to_le_bytes());
4112 }
4113
4114 segment
4115 }
4116
4117 #[cfg_attr(not(test), allow(dead_code))]
4122 pub fn build_base_tag_path(&self, tag_name: &str) -> Vec<u8> {
4123 match TagPath::parse(tag_name) {
4125 Ok(path) => {
4126 let base_path = match &path {
4128 TagPath::Array { base_path, .. } => base_path.as_ref(),
4129 _ => &path,
4130 };
4131 base_path.to_cip_path().unwrap_or_else(|_| {
4132 let mut path = Vec::new();
4135 path.push(0x91); let name_bytes = tag_name.as_bytes();
4137 path.push(name_bytes.len() as u8);
4138 path.extend_from_slice(name_bytes);
4139 if path.len() % 2 != 0 {
4141 path.push(0x00);
4142 }
4143 path
4144 })
4145 }
4146 Err(_) => {
4147 let mut path = Vec::new();
4149 path.push(0x91); let name_bytes = tag_name.as_bytes();
4151 path.push(name_bytes.len() as u8);
4152 path.extend_from_slice(name_bytes);
4153 if path.len() % 2 != 0 {
4155 path.push(0x00);
4156 }
4157 path
4158 }
4159 }
4160 }
4161
4162 #[cfg_attr(not(test), allow(dead_code))]
4190 pub fn build_read_array_request(
4191 &self,
4192 base_array_name: &str,
4193 start_index: u32,
4194 element_count: u16,
4195 ) -> Vec<u8> {
4196 let mut cip_request = Vec::new();
4197
4198 cip_request.push(0x4C);
4201
4202 let mut full_path = self.build_base_tag_path(base_array_name);
4207
4208 tracing::trace!(
4209 "build_read_array_request: base_path for '{}' = {:02X?} ({} bytes)",
4210 base_array_name,
4211 full_path,
4212 full_path.len()
4213 );
4214
4215 let element_segment = self.build_element_id_segment(start_index);
4218 tracing::trace!(
4219 "build_read_array_request: element_segment for index {} = {:02X?} ({} bytes)",
4220 start_index,
4221 element_segment,
4222 element_segment.len()
4223 );
4224 full_path.extend_from_slice(&element_segment);
4225
4226 if !full_path.len().is_multiple_of(2) {
4228 full_path.push(0x00);
4229 }
4230
4231 let path_size = (full_path.len() / 2) as u8;
4233 cip_request.push(path_size);
4234 cip_request.extend_from_slice(&full_path);
4235
4236 cip_request.extend_from_slice(&element_count.to_le_bytes());
4239
4240 tracing::trace!(
4241 "build_read_array_request: final request = {:02X?} ({} bytes), path_size = {} words ({} bytes)",
4242 cip_request,
4243 cip_request.len(),
4244 path_size,
4245 full_path.len()
4246 );
4247
4248 cip_request
4249 }
4250
4251 fn build_tag_path(&self, tag_name: &str) -> Vec<u8> {
4257 match TagPath::parse(tag_name) {
4261 Ok(tag_path) => {
4262 tracing::debug!("Parsed tag path for '{}': {:?}", tag_name, tag_path);
4263 match tag_path.to_cip_path() {
4265 Ok(path) => {
4266 tracing::debug!(
4267 "TagPath generated {} bytes ({} words) for '{}': {:02X?}",
4268 path.len(),
4269 path.len() / 2,
4270 tag_name,
4271 path
4272 );
4273 path
4274 }
4275 Err(e) => {
4276 tracing::warn!("TagPath.to_cip_path() failed for '{}': {}", tag_name, e);
4277 self.build_simple_tag_path_legacy(tag_name)
4279 }
4280 }
4281 }
4282 Err(e) => {
4283 tracing::warn!("TagPath::parse() failed for '{}': {}", tag_name, e);
4284 self.build_simple_tag_path_legacy(tag_name)
4286 }
4287 }
4288 }
4289
4290 fn build_simple_tag_path_legacy(&self, tag_name: &str) -> Vec<u8> {
4292 let mut path = Vec::new();
4293 path.push(0x91); path.push(tag_name.len() as u8);
4295 path.extend_from_slice(tag_name.as_bytes());
4296
4297 if !tag_name.len().is_multiple_of(2) {
4299 path.push(0x00);
4300 }
4301
4302 path
4303 }
4304}
4305
4306#[cfg(test)]
4307mod discovery_tests {
4308 use super::{EipClient, TemplateAttributes};
4309
4310 #[test]
4311 fn build_tag_list_request_rejects_instance_above_u16() {
4312 let client = EipClient::new_unconnected_for_testing();
4313 let request = client
4314 .build_tag_list_request_from_instance(0x12345678)
4315 .expect_err("instance should be rejected");
4316
4317 assert!(format!("{request}").contains("exceeds 16-bit"));
4318 }
4319
4320 #[test]
4321 fn build_tag_list_request_encodes_path_size_and_start_instance() {
4322 let client = EipClient::new_unconnected_for_testing();
4323 let request = client
4324 .build_tag_list_request_from_instance(0x5678)
4325 .expect("request should build");
4326
4327 assert_eq!(request[0], 0x55);
4328 assert_eq!(request[1], 0x03);
4329 assert_eq!(&request[2..8], &[0x20, 0x6B, 0x25, 0x00, 0x78, 0x56]);
4330 }
4331
4332 #[test]
4333 fn build_program_tag_list_request_includes_program_symbol_scope() {
4334 let client = EipClient::new_unconnected_for_testing();
4335 let request = client
4336 .build_program_tag_list_request("MainProgram")
4337 .expect("request should build");
4338
4339 let mut expected = vec![0x55, 0x0E, 0x91, 0x13];
4340 expected.extend_from_slice(b"Program:MainProgram");
4341 expected.push(0x00);
4342 expected.extend_from_slice(&[
4343 0x20, 0x6B, 0x25, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01, 0x00, 0x02, 0x00, ]);
4348
4349 assert_eq!(request, expected);
4350 }
4351
4352 #[test]
4353 fn parse_tag_list_response_page_handles_partial_transfer() {
4354 let client = EipClient::new_unconnected_for_testing();
4355 let response = [
4356 0xD5, 0x00, 0x06,
4357 0x00, 0x34, 0x12, 0x00, 0x00, 0x04, 0x00, b'R', b'a', b't', b'e', 0xC4, 0x00, ];
4363
4364 let page = client
4365 .parse_tag_list_response_page(&response)
4366 .expect("response should parse");
4367
4368 assert!(page.partial_transfer);
4369 assert_eq!(page.last_instance_id, Some(0x1234));
4370 assert_eq!(page.tags.len(), 1);
4371 assert_eq!(page.tags[0].name, "Rate");
4372 assert_eq!(page.tags[0].data_type, 0x00C4);
4373 assert_eq!(page.tags[0].data_type_name, "DINT");
4374 }
4375
4376 #[test]
4377 fn build_get_template_attributes_request_encodes_template_object_path() {
4378 let client = EipClient::new_unconnected_for_testing();
4379 let request = client
4380 .build_get_template_attributes_request(0x0456)
4381 .expect("request should build");
4382
4383 assert_eq!(request[0], 0x03);
4384 assert_eq!(request[1], 0x03);
4385 assert_eq!(&request[2..8], &[0x20, 0x6C, 0x25, 0x00, 0x56, 0x04]);
4386 assert_eq!(
4387 &request[8..],
4388 &[0x04, 0x00, 0x01, 0x00, 0x02, 0x00, 0x04, 0x00, 0x05, 0x00]
4389 );
4390 }
4391
4392 #[test]
4393 fn build_get_attributes_request_encodes_path_words_and_odd_name_padding() {
4394 let client = EipClient::new_unconnected_for_testing();
4395 let request = client
4396 .build_get_attributes_request("Odd")
4397 .expect("request should build");
4398
4399 assert_eq!(
4400 request,
4401 vec![
4402 0x03, 0x03, 0x91, 0x03, b'O', b'd', b'd', 0x00, 0x02, 0x00, 0x01, 0x00, 0x02, 0x00, ]
4409 );
4410 }
4411
4412 #[test]
4413 fn parse_attributes_response_walks_attribute_records() {
4414 let client = EipClient::new_unconnected_for_testing();
4415 let response = [
4416 0x83, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01, 0x00, 0x00, 0x00, 0xC4, 0x00, 0x02, 0x00, 0x00, 0x00, 0x34, 0x12, 0x00, 0x00, ];
4421
4422 let attributes = client
4423 .parse_attributes_response("DINT_TAG", &response)
4424 .expect("response should parse");
4425
4426 assert_eq!(attributes.name, "DINT_TAG");
4427 assert_eq!(attributes.data_type, 0x00C4);
4428 assert_eq!(attributes.data_type_name, "DINT");
4429 assert_eq!(attributes.template_instance_id, Some(0x1234));
4430 assert_eq!(attributes.size, 4);
4431 }
4432
4433 #[test]
4434 fn extended_status_parser_uses_little_endian_additional_status() {
4435 let client = EipClient::new_unconnected_for_testing();
4436 let response = [0xCD, 0x00, 0xFF, 0x01, 0x07, 0x21];
4437
4438 let err = client
4439 .check_cip_error(&response)
4440 .expect_err("extended status should be an error");
4441 let message = err.to_string();
4442
4443 assert!(message.contains("0x2107"));
4444 assert!(message.contains("data-type mismatch"));
4445 assert!(!message.contains("(BE)"));
4446 assert!(!message.contains("0x0721"));
4447 }
4448
4449 #[test]
4450 fn extended_status_parser_does_not_require_general_status_ff() {
4451 let client = EipClient::new_unconnected_for_testing();
4452 let response = [0xCC, 0x00, 0x01, 0x01, 0x05, 0x00];
4453
4454 let err = client
4455 .check_cip_error(&response)
4456 .expect_err("additional status should be decoded");
4457
4458 assert!(err.to_string().contains("Path destination unknown"));
4459 }
4460
4461 #[test]
4462 fn build_read_template_request_encodes_template_read_size() {
4463 let client = EipClient::new_unconnected_for_testing();
4464 let request = client
4465 .build_read_template_request(0x0456, 0x0010, 0x0032)
4466 .expect("request should build");
4467
4468 assert_eq!(request[0], 0x4C);
4469 assert_eq!(request[1], 0x03);
4470 assert_eq!(&request[2..8], &[0x20, 0x6C, 0x25, 0x00, 0x56, 0x04]);
4471 assert_eq!(&request[8..12], &[0x10, 0x00, 0x00, 0x00]);
4472 assert_eq!(&request[12..14], &[0x32, 0x00]);
4473 }
4474
4475 #[test]
4476 fn parse_template_attributes_response_reads_mixed_width_values() {
4477 let client = EipClient::new_unconnected_for_testing();
4478 let response = [
4479 0x83, 0x00, 0x00, 0x00, 0x04, 0x00, 0x01, 0x00, 0x00, 0x00, 0x34, 0x12, 0x02, 0x00, 0x00, 0x00, 0x07, 0x00, 0x04, 0x00, 0x00, 0x00, 0x19, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x58, 0x00, 0x00, 0x00, ];
4486
4487 let attributes = client
4488 .parse_template_attributes_response(0x0456, &response)
4489 .expect("response should parse");
4490
4491 assert_eq!(
4492 attributes,
4493 TemplateAttributes {
4494 structure_handle: 0x1234,
4495 member_count: 7,
4496 definition_size_words: 25,
4497 structure_size_bytes: 88,
4498 }
4499 );
4500 }
4501}
4502
4503#[cfg(test)]
4504mod write_request_tests {
4505 use super::EipClient;
4506 use crate::PlcValue;
4507 use crate::protocol::values;
4508
4509 #[test]
4510 fn build_write_request_encodes_standard_string_structure() {
4511 let client = EipClient::new_unconnected_for_testing();
4512 let request = client
4513 .build_write_request("Tag1", &PlcValue::String("AB".to_string()))
4514 .expect("STRING request should build");
4515
4516 assert_eq!(
4517 &request[..8],
4518 &[0x4D, 0x03, 0x91, 0x04, b'T', b'a', b'g', b'1']
4519 );
4520 let data = &request[8..];
4521 assert_eq!(&data[..6], &[0xA0, 0x02, 0xCE, 0x0F, 0x01, 0x00]);
4522 assert_eq!(&data[6..12], &[2, 0, 0, 0, b'A', b'B']);
4523 assert_eq!(data.len(), 6 + values::STANDARD_STRING_PAYLOAD_LEN);
4524 assert!(data[12..].iter().all(|byte| *byte == 0));
4525 }
4526
4527 #[test]
4528 fn build_write_request_rejects_overlong_standard_string() {
4529 let client = EipClient::new_unconnected_for_testing();
4530 let value = PlcValue::String("x".repeat(values::STANDARD_STRING_DATA_LEN + 1));
4531 let err = client
4532 .build_write_request("Tag1", &value)
4533 .expect_err("overlong STRING should be rejected");
4534
4535 assert!(err.to_string().contains("String too long"));
4536 }
4537}
4538
4539#[cfg(test)]
4540mod transport_tests {
4541 use super::{DiagnosticOperation, EipClient};
4542 use crate::EtherNetIpStream;
4543 use crate::error::EtherNetIpError;
4544 use std::sync::Arc;
4545 use std::time::Duration;
4546 use tokio::io::{AsyncReadExt, AsyncWriteExt};
4547 use tokio::sync::Mutex;
4548
4549 fn register_response(session_handle: u32) -> Vec<u8> {
4550 let mut response = Vec::with_capacity(28);
4551 response.extend_from_slice(&0x0065u16.to_le_bytes());
4552 response.extend_from_slice(&4u16.to_le_bytes());
4553 response.extend_from_slice(&session_handle.to_le_bytes());
4554 response.extend_from_slice(&0u32.to_le_bytes());
4555 response.extend_from_slice(&[0u8; 8]);
4556 response.extend_from_slice(&0u32.to_le_bytes());
4557 response.extend_from_slice(&[0x01, 0x00, 0x00, 0x00]);
4558 response
4559 }
4560
4561 async fn read_register_request(stream: &mut tokio::io::DuplexStream) {
4562 let mut header = [0u8; 24];
4563 stream
4564 .read_exact(&mut header)
4565 .await
4566 .expect("request header");
4567 let body_len = u16::from_le_bytes([header[2], header[3]]) as usize;
4568 let mut body = vec![0u8; body_len];
4569 stream.read_exact(&mut body).await.expect("request body");
4570 }
4571
4572 #[tokio::test]
4573 async fn register_session_accepts_fragmented_reply() {
4574 let (client_stream, mut server_stream) = tokio::io::duplex(128);
4575 let mut client = EipClient::new_unconnected_for_testing();
4576 client.stream = Arc::new(Mutex::new(
4577 Box::new(client_stream) as Box<dyn EtherNetIpStream>
4578 ));
4579
4580 let server = tokio::spawn(async move {
4581 read_register_request(&mut server_stream).await;
4582 let response = register_response(0x0102_0304);
4583 server_stream
4584 .write_all(&response[..10])
4585 .await
4586 .expect("first fragment");
4587 tokio::task::yield_now().await;
4588 server_stream
4589 .write_all(&response[10..])
4590 .await
4591 .expect("second fragment");
4592 });
4593
4594 client
4595 .register_session()
4596 .await
4597 .expect("fragmented register response should parse");
4598 server.await.expect("server task");
4599
4600 assert_eq!(client.session_handle(), 0x0102_0304);
4601 }
4602
4603 #[tokio::test]
4604 async fn reregistration_updates_session_handle_across_clones() {
4605 let (client_stream, mut server_stream) = tokio::io::duplex(256);
4606 let mut client = EipClient::new_unconnected_for_testing();
4607 client.stream = Arc::new(Mutex::new(
4608 Box::new(client_stream) as Box<dyn EtherNetIpStream>
4609 ));
4610 let mut clone = client.clone();
4611
4612 let server = tokio::spawn(async move {
4613 for handle in [0x1111_2222, 0x3333_4444] {
4614 read_register_request(&mut server_stream).await;
4615 server_stream
4616 .write_all(®ister_response(handle))
4617 .await
4618 .expect("register response");
4619 }
4620 });
4621
4622 client.register_session().await.expect("first register");
4623 assert_eq!(client.session_handle(), 0x1111_2222);
4624 assert_eq!(clone.session_handle(), 0x1111_2222);
4625
4626 clone.register_session().await.expect("clone re-register");
4627 server.await.expect("server task");
4628
4629 assert_eq!(client.session_handle(), 0x3333_4444);
4630 assert_eq!(clone.session_handle(), 0x3333_4444);
4631 }
4632
4633 #[tokio::test]
4634 async fn diagnostics_snapshot_reports_counted_operations_and_errors() {
4635 let client = EipClient::new_unconnected_for_testing();
4636
4637 client
4638 .diagnostic_counters
4639 .record_success(Some(DiagnosticOperation::Read));
4640 client.diagnostic_counters.record_failure(
4641 Some(DiagnosticOperation::Write),
4642 &EtherNetIpError::Timeout(Duration::from_secs(1)),
4643 );
4644 client
4645 .diagnostic_counters
4646 .record_cip_failure(Some(DiagnosticOperation::Batch));
4647
4648 let snapshot = client.get_diagnostics_snapshot().await;
4649
4650 assert_eq!(snapshot.operations.total_reads, 1);
4651 assert_eq!(snapshot.operations.successful_reads, 1);
4652 assert_eq!(snapshot.operations.total_writes, 1);
4653 assert_eq!(snapshot.operations.failed_writes, 1);
4654 assert_eq!(snapshot.operations.batch_operations, 1);
4655 assert_eq!(snapshot.operations.partial_batch_failures, 1);
4656 assert_eq!(snapshot.errors.timeout_errors, 1);
4657 assert_eq!(snapshot.errors.protocol_errors, 1);
4658 assert_eq!(snapshot.errors.retriable_errors, 1);
4659 assert_eq!(snapshot.errors.non_retriable_errors, 1);
4660 assert!(snapshot.operations.last_successful_read_time.is_some());
4661 assert!(snapshot.operations.last_failed_write_time.is_some());
4662 assert!(snapshot.errors.last_error_time.is_some());
4663 assert!(snapshot.system_metrics_are_placeholders);
4664 }
4665}
4666
4667