1use super::EipClient;
2use crate::error::{EtherNetIpError, Result};
3use crate::types::{PlcValue, UdtData};
4use crate::udt::UserDefinedType;
5use std::future::Future;
6use std::pin::Pin;
7
8type WriteFuture<'a> = Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>>;
9
10trait MemberWriteStrategy {
11 fn write_direct_member<'a>(
12 &'a mut self,
13 member_path: &'a str,
14 value: PlcValue,
15 ) -> WriteFuture<'a>;
16
17 fn write_member_via_full_value<'a>(
18 &'a mut self,
19 udt_tag_name: &'a str,
20 member_name: &'a str,
21 value: PlcValue,
22 ) -> WriteFuture<'a>;
23}
24
25impl MemberWriteStrategy for EipClient {
26 fn write_direct_member<'a>(
27 &'a mut self,
28 member_path: &'a str,
29 value: PlcValue,
30 ) -> WriteFuture<'a> {
31 Box::pin(async move { self.write_tag(member_path, value).await })
32 }
33
34 fn write_member_via_full_value<'a>(
35 &'a mut self,
36 udt_tag_name: &'a str,
37 member_name: &'a str,
38 value: PlcValue,
39 ) -> WriteFuture<'a> {
40 Box::pin(async move {
41 self.write_udt_member_via_full_value(udt_tag_name, member_name, value)
42 .await
43 })
44 }
45}
46
47impl EipClient {
48 pub async fn write_string_tag(&mut self, tag_name: &str, value: &str) -> Result<()> {
50 self.write_tag(tag_name, PlcValue::String(value.to_string()))
51 .await
52 }
53
54 pub async fn read_string_tag(&mut self, tag_name: &str) -> Result<String> {
59 match self.read_tag(tag_name).await? {
60 PlcValue::String(value) => Ok(value),
61 PlcValue::Udt(udt) if udt.data.len() >= 6 => {
62 let len = u32::from_le_bytes([udt.data[2], udt.data[3], udt.data[4], udt.data[5]])
63 as usize;
64 let end = (6 + len).min(udt.data.len());
65 Ok(String::from_utf8_lossy(&udt.data[6..end]).to_string())
66 }
67 other => Err(EtherNetIpError::DataTypeMismatch {
68 expected: "STRING".to_string(),
69 actual: format!("{other:?}"),
70 }),
71 }
72 }
73
74 pub async fn write_udt_member(
76 &mut self,
77 udt_tag_name: &str,
78 member_name: &str,
79 value: PlcValue,
80 ) -> Result<()> {
81 write_udt_member_direct_first(self, udt_tag_name, member_name, value).await
82 }
83
84 pub async fn write_udt_array_member(
86 &mut self,
87 udt_array_element_path: &str,
88 member_name: &str,
89 value: PlcValue,
90 ) -> Result<()> {
91 write_udt_member_direct_first(self, udt_array_element_path, member_name, value).await
92 }
93
94 async fn write_udt_member_via_full_value(
95 &mut self,
96 udt_tag_name: &str,
97 member_name: &str,
98 value: PlcValue,
99 ) -> Result<()> {
100 let current = self.read_tag(udt_tag_name).await?;
101 let PlcValue::Udt(udt_data) = current else {
102 return Err(EtherNetIpError::DataTypeMismatch {
103 expected: "UDT".to_string(),
104 actual: format!("{current:?}"),
105 });
106 };
107
108 let definition = self.get_udt_definition(udt_tag_name).await?;
109 let mut user_def = UserDefinedType::new(definition.name.clone());
110 for member in &definition.members {
111 user_def.add_member(member.clone());
112 }
113
114 let mut members = udt_data.parse(&user_def)?;
115 if !members.contains_key(member_name) {
116 return Err(EtherNetIpError::TagNotFound(format!(
117 "{udt_tag_name}.{member_name}"
118 )));
119 }
120
121 members.insert(member_name.to_string(), value);
122 let modified = UdtData::from_hash_map(&members, &user_def, udt_data.symbol_id)?;
123 self.write_tag(udt_tag_name, PlcValue::Udt(modified)).await
124 }
125}
126
127async fn write_udt_member_direct_first<T: MemberWriteStrategy>(
128 strategy: &mut T,
129 udt_tag_name: &str,
130 member_name: &str,
131 value: PlcValue,
132) -> Result<()> {
133 if matches!(value, PlcValue::String(_)) {
134 return strategy
135 .write_member_via_full_value(udt_tag_name, member_name, value)
136 .await;
137 }
138
139 let member_path = format!("{udt_tag_name}.{member_name}");
140 match strategy
141 .write_direct_member(&member_path, value.clone())
142 .await
143 {
144 Ok(()) => Ok(()),
145 Err(error) if is_2107_type_mismatch(&error) => {
146 strategy
147 .write_member_via_full_value(udt_tag_name, member_name, value)
148 .await
149 }
150 Err(error) => Err(error),
151 }
152}
153
154pub(crate) fn is_2107_type_mismatch(error: &EtherNetIpError) -> bool {
155 error.to_string().to_ascii_lowercase().contains("0x2107")
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161 use std::future;
162
163 #[derive(Default)]
164 struct FakeMemberWriteStrategy {
165 direct_error: Option<EtherNetIpError>,
166 direct_paths: Vec<String>,
167 rmw_calls: Vec<(String, String, PlcValue)>,
168 }
169
170 impl MemberWriteStrategy for FakeMemberWriteStrategy {
171 fn write_direct_member<'a>(
172 &'a mut self,
173 member_path: &'a str,
174 _value: PlcValue,
175 ) -> WriteFuture<'a> {
176 self.direct_paths.push(member_path.to_string());
177 Box::pin(future::ready(match self.direct_error.take() {
178 Some(error) => Err(error),
179 None => Ok(()),
180 }))
181 }
182
183 fn write_member_via_full_value<'a>(
184 &'a mut self,
185 udt_tag_name: &'a str,
186 member_name: &'a str,
187 value: PlcValue,
188 ) -> WriteFuture<'a> {
189 self.rmw_calls
190 .push((udt_tag_name.to_string(), member_name.to_string(), value));
191 Box::pin(future::ready(Ok(())))
192 }
193 }
194
195 #[tokio::test]
196 async fn scalar_member_write_falls_back_to_rmw_on_2107() {
197 let mut strategy = FakeMemberWriteStrategy {
198 direct_error: Some(EtherNetIpError::Protocol(
199 "CIP Extended Error: Read/Write Tag data-type mismatch extended error: 0x2107"
200 .to_string(),
201 )),
202 ..Default::default()
203 };
204
205 write_udt_member_direct_first(
206 &mut strategy,
207 "UDT_ARRAY[3]",
208 "DINT_MEMBER",
209 PlcValue::Dint(77),
210 )
211 .await
212 .expect("fallback should succeed");
213
214 assert_eq!(strategy.direct_paths, vec!["UDT_ARRAY[3].DINT_MEMBER"]);
215 assert_eq!(strategy.rmw_calls.len(), 1);
216 assert_eq!(strategy.rmw_calls[0].0, "UDT_ARRAY[3]");
217 assert_eq!(strategy.rmw_calls[0].1, "DINT_MEMBER");
218 assert_eq!(strategy.rmw_calls[0].2, PlcValue::Dint(77));
219 }
220
221 #[tokio::test]
222 async fn string_member_write_uses_rmw_without_direct_attempt() {
223 let mut strategy = FakeMemberWriteStrategy {
224 direct_error: Some(EtherNetIpError::Protocol(
225 "direct write should not be called".to_string(),
226 )),
227 ..Default::default()
228 };
229
230 write_udt_member_direct_first(
231 &mut strategy,
232 "gTestUDT",
233 "Member5_String",
234 PlcValue::String("updated".to_string()),
235 )
236 .await
237 .expect("string RMW should succeed");
238
239 assert!(strategy.direct_paths.is_empty());
240 assert_eq!(strategy.rmw_calls.len(), 1);
241 assert_eq!(strategy.rmw_calls[0].0, "gTestUDT");
242 assert_eq!(strategy.rmw_calls[0].1, "Member5_String");
243 assert_eq!(
244 strategy.rmw_calls[0].2,
245 PlcValue::String("updated".to_string())
246 );
247 }
248
249 #[tokio::test]
250 async fn non_2107_direct_error_does_not_fallback() {
251 let mut strategy = FakeMemberWriteStrategy {
252 direct_error: Some(EtherNetIpError::Protocol(
253 "CIP Error 0x04: Path segment error".to_string(),
254 )),
255 ..Default::default()
256 };
257
258 let error = write_udt_member_direct_first(
259 &mut strategy,
260 "UDT_ARRAY[3]",
261 "DINT_MEMBER",
262 PlcValue::Dint(77),
263 )
264 .await
265 .expect_err("non-2107 error should be returned");
266
267 assert!(error.to_string().contains("0x04"));
268 assert_eq!(strategy.direct_paths, vec!["UDT_ARRAY[3].DINT_MEMBER"]);
269 assert!(strategy.rmw_calls.is_empty());
270 }
271}