Skip to content

Commit 623e80a

Browse files
committed
refactor: switch CIP API from io::Error to CipError and align fragmented reads
1 parent 0423574 commit 623e80a

2 files changed

Lines changed: 87 additions & 59 deletions

File tree

src/cip/error.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,14 @@ impl From<u8> for CipError {
2323
}
2424
}
2525

26+
impl std::fmt::Display for CipError {
27+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28+
write!(f, "{:?}", self)
29+
}
30+
}
31+
32+
impl std::error::Error for CipError {}
33+
2634
#[derive(Debug)]
2735
pub enum ForwardOpenError {
2836
GeneralStatus(u8),

src/client.rs

Lines changed: 79 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -174,104 +174,113 @@ impl EthernetIpClient {
174174
Err(io::Error::other("No CIP data item found in CPF"))
175175
}
176176

177-
pub async fn browse_symbols(&mut self) -> io::Result<Vec<SymbolInfo>> {
177+
pub async fn browse_symbols(&mut self) -> Result<Vec<SymbolInfo>, CipError> {
178178
let cip = build_symbol_browse_request();
179+
180+
// Transport layer stays io::Error → map into CIP domain
179181
let res = if self.connected {
180-
self.send_unit_data(cip).await?
182+
self.send_unit_data(cip).await
181183
} else {
182-
self.send_rr_data(cip).await?
183-
};
184+
self.send_rr_data(cip).await
185+
}
186+
.map_err(|_| CipError::VendorSpecific(0xFF))?;
184187

188+
// CIP response must be at least 4 bytes
185189
if res.len() < 4 {
186-
return Err(io::Error::other("Malformed CIP response for symbol browse"));
190+
return Err(CipError::VendorSpecific(0xFE)); // malformed
187191
}
188192

189193
let general_status = res[2];
190194
if general_status != 0 {
191-
return Err(io::Error::other(format!(
192-
"PLC returned error 0x{:02X} for symbol browse",
193-
general_status
194-
)));
195+
return Err(CipError::from(general_status));
195196
}
196197

197198
let ext_words = res[3] as usize;
198199
let data_start = 4 + ext_words * 2;
200+
199201
if res.len() < data_start {
200-
return Err(io::Error::other("Symbol browse response too short"));
202+
return Err(CipError::VendorSpecific(0xFD)); // too short
201203
}
202204

203205
let symbols = parse_symbol_browse_response(&res[data_start..]);
204206
Ok(symbols)
205207
}
206208

207-
pub async fn read_tag(&mut self, tag: &str) -> io::Result<CipValue> {
209+
pub async fn read_tag(&mut self, tag: &str) -> Result<CipValue, CipError> {
208210
let cip = build_read_request(tag, self.slot);
209211

212+
// Transport layer stays io::Error
210213
let res = if self.connected {
211-
self.send_unit_data(cip).await?
214+
self.send_unit_data(cip).await
212215
} else {
213-
self.send_rr_data(cip).await?
214-
};
216+
self.send_rr_data(cip).await
217+
}
218+
.map_err(|_e| CipError::VendorSpecific(0xFF))?; // transport failure → CIP error domain
215219

216220
if res.len() < 4 {
217-
return Err(io::Error::other("Malformed CIP read response"));
221+
return Err(CipError::VendorSpecific(0xFE)); // malformed response
218222
}
219223

220224
let general_status = res[2];
221225
if general_status != 0 {
222-
return Err(io::Error::other(format!(
223-
"PLC returned error 0x{:02X}",
224-
general_status
225-
)));
226+
return Err(CipError::from(general_status));
226227
}
227228

228229
let ext_words = res[3] as usize;
229230
let data_start = 4 + ext_words * 2;
230231
if res.len() < data_start {
231-
return Err(io::Error::other("CIP read response too short"));
232+
return Err(CipError::VendorSpecific(0xFD)); // too short
232233
}
233234

234-
decode_cip_response(&res[data_start..]).ok_or(io::Error::other("Decode error"))
235+
decode_cip_response(&res[data_start..]).ok_or(CipError::VendorSpecific(0xFC))
236+
// decode error
235237
}
236238

237-
pub async fn write_tag(&mut self, tag: &str, value: CipValue) -> io::Result<()> {
239+
pub async fn write_tag(&mut self, tag: &str, value: CipValue) -> Result<(), CipError> {
238240
let cip = build_write_request(tag, &value, self.slot);
241+
239242
let res = if self.connected {
240-
self.send_unit_data(cip).await?
243+
self.send_unit_data(cip).await
241244
} else {
242-
self.send_rr_data(cip).await?
243-
};
245+
self.send_rr_data(cip).await
246+
}
247+
.map_err(|_| CipError::VendorSpecific(0xFF))?;
244248

245-
decode_write_response(&res).map_err(|status| {
246-
io::Error::other(format!("PLC returned write error 0x{:02X}", status))
247-
})
249+
match decode_write_response(&res) {
250+
Ok(()) => Ok(()),
251+
Err(status) => Err(CipError::from(status)),
252+
}
248253
}
249254

250-
pub async fn read_tag_multi(&mut self, tag: &str, count: usize) -> io::Result<Vec<CipValue>> {
255+
pub async fn read_tag_multi(
256+
&mut self,
257+
tag: &str,
258+
count: usize,
259+
) -> Result<Vec<CipValue>, CipError> {
251260
let cip = build_read_request_count(tag, count, self.slot);
261+
262+
// Transport layer stays io::Error → map into CIP domain
252263
let res = if self.connected {
253-
self.send_unit_data(cip).await?
264+
self.send_unit_data(cip).await
254265
} else {
255-
self.send_rr_data(cip).await?
256-
};
266+
self.send_rr_data(cip).await
267+
}
268+
.map_err(|_| CipError::VendorSpecific(0xFF))?;
257269

258270
if res.len() < 4 {
259-
return Err(io::Error::other("Malformed CIP read response"));
271+
return Err(CipError::VendorSpecific(0xFE)); // malformed
260272
}
261273

262274
let general_status = res[2];
263275
if general_status != 0 {
264-
return Err(io::Error::other(format!(
265-
"PLC returned error 0x{:02X}",
266-
general_status
267-
)));
276+
return Err(CipError::from(general_status));
268277
}
269278

270279
let ext_words = res[3] as usize;
271280
let data_start = 4 + ext_words * 2;
272281

273282
if res.len() < data_start + 2 {
274-
return Err(io::Error::other("CIP multi read response too short"));
283+
return Err(CipError::VendorSpecific(0xFD)); // too short
275284
}
276285

277286
// Extract type ID
@@ -281,27 +290,39 @@ impl EthernetIpClient {
281290
Ok(crate::cip::decode_cip_data_list(type_id, payload))
282291
}
283292

284-
pub async fn write_tag_multi(&mut self, tag: &str, values: &[CipValue]) -> io::Result<()> {
293+
pub async fn write_tag_multi(
294+
&mut self,
295+
tag: &str,
296+
values: &[CipValue],
297+
) -> Result<(), CipError> {
285298
for (i, v) in values.iter().enumerate() {
286299
let indexed = format!("{tag}[{i}]");
287300
self.write_tag(&indexed, v.clone()).await?;
288301
}
289302
Ok(())
290303
}
291304

292-
pub async fn read_tags_msp(&mut self, tags: &[&str]) -> io::Result<Vec<MultiResult<CipValue>>> {
305+
pub async fn read_tags_msp(
306+
&mut self,
307+
tags: &[&str],
308+
) -> Result<Vec<MultiResult<CipValue>>, CipError> {
293309
let mut reqs = Vec::with_capacity(tags.len());
294310
for tag in tags {
295311
let cip = build_read_request(tag, self.slot);
296312
reqs.push(cip);
297313
}
298314

299315
let msp = build_cip_multiple_service_request(&reqs);
316+
317+
// Transport → CIP fallback
300318
let res = if self.connected {
301-
self.send_unit_data(msp).await?
319+
self.send_unit_data(msp).await
302320
} else {
303-
self.send_rr_data(msp).await?
304-
};
321+
self.send_rr_data(msp).await
322+
}
323+
.map_err(|_| CipError::VendorSpecific(0xFF))?;
324+
325+
// MSP parser handles CIP status codes internally
305326
Ok(parse_cip_multiple_service_response(&res))
306327
}
307328

@@ -417,36 +438,40 @@ impl EthernetIpClient {
417438
&mut self,
418439
tag: &str,
419440
count: u16,
420-
) -> io::Result<(u16, Vec<u8>)> {
441+
) -> Result<(u16, Vec<u8>), CipError> {
421442
let mut all_data = Vec::new();
422443
let mut offset: u32 = 0;
423444
let mut type_id: u16 = 0;
424445

425446
loop {
426447
let cip = build_read_fragmented_request(tag, count, offset, self.slot);
448+
449+
// Transport layer stays io::Error → map into CIP domain
427450
let res = if self.connected {
428-
self.send_unit_data(cip).await?
451+
self.send_unit_data(cip).await
429452
} else {
430-
self.send_rr_data(cip).await?
431-
};
453+
self.send_rr_data(cip).await
454+
}
455+
.map_err(|_e| CipError::VendorSpecific(0xFF))?;
432456

433457
if res.len() < 4 {
434-
return Err(io::Error::other("Fragmented response too short"));
458+
return Err(CipError::VendorSpecific(0xFE)); // malformed
435459
}
436460

437461
let general_status = res[2];
438462
let ext_words = res[3] as usize;
439463
let data_start = 4 + (ext_words * 2);
440464

441465
if res.len() < data_start {
442-
return Err(io::Error::other("No payload in fragment response"));
466+
return Err(CipError::VendorSpecific(0xFD)); // no payload
443467
}
444468

445469
let mut payload = &res[data_start..];
446470

471+
// First fragment contains Type ID
447472
if offset == 0 {
448473
if payload.len() < 2 {
449-
return Err(io::Error::other("Missing Type ID in first fragment"));
474+
return Err(CipError::VendorSpecific(0xFC)); // missing type ID
450475
}
451476
type_id = u16::from_le_bytes([payload[0], payload[1]]);
452477
payload = &payload[2..];
@@ -455,21 +480,16 @@ impl EthernetIpClient {
455480
all_data.extend_from_slice(payload);
456481

457482
match general_status {
458-
0x00 => break,
459-
0x06 => offset = all_data.len() as u32,
460-
_ => {
461-
return Err(io::Error::other(format!(
462-
"PLC Error: 0x{:02X}",
463-
general_status
464-
)))
465-
}
483+
0x00 => break, // done
484+
0x06 => offset = all_data.len() as u32, // partial transfer → continue
485+
other => return Err(CipError::from(other)),
466486
}
467487
}
468488

469489
Ok((type_id, all_data))
470490
}
471491

472-
pub async fn read_array(&mut self, tag: &str, count: u16) -> io::Result<Vec<CipValue>> {
492+
pub async fn read_array(&mut self, tag: &str, count: u16) -> Result<Vec<CipValue>, CipError> {
473493
let (type_id, raw) = self.read_tag_fragmented(tag, count).await?;
474494
Ok(crate::cip::decode_cip_data_list(type_id, &raw))
475495
}

0 commit comments

Comments
 (0)