Skip to content

Commit d29b5cd

Browse files
committed
add typed read/write helpers for common CIP scalar types
1 parent 1d60114 commit d29b5cd

4 files changed

Lines changed: 162 additions & 0 deletions

File tree

src/cip/error.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ pub enum CipError {
88
PathSegmentError,
99
PathDestinationUnknown,
1010
VendorSpecific(u8),
11+
TypeMismatch {
12+
expected: &'static str,
13+
actual: &'static str,
14+
},
1115
}
1216

1317
impl From<u8> for CipError {
@@ -37,6 +41,10 @@ impl fmt::Display for CipError {
3741
CipError::PathDestinationUnknown => write!(f, "Path destination unknown (0x06)"),
3842

3943
CipError::VendorSpecific(code) => write!(f, "Vendor-specific CIP error 0x{:02X}", code),
44+
45+
CipError::TypeMismatch { expected, actual } => {
46+
write!(f, "Type mismatch: expected {expected}, got {actual}")
47+
}
4048
}
4149
}
4250
}

src/client.rs

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -661,4 +661,89 @@ impl EthernetIpClient {
661661

662662
Ok(())
663663
}
664+
665+
pub async fn read_dint(&mut self, tag: &str) -> Result<i32, CipError> {
666+
match self.read_tag(tag).await? {
667+
CipValue::DInt(v) => Ok(v),
668+
other => Err(CipError::TypeMismatch {
669+
expected: "DINT",
670+
actual: other.type_name(),
671+
}),
672+
}
673+
}
674+
675+
pub async fn read_bool(&mut self, tag: &str) -> Result<bool, CipError> {
676+
match self.read_tag(tag).await? {
677+
CipValue::Bool(v) => Ok(v),
678+
other => Err(CipError::TypeMismatch {
679+
expected: "BOOL",
680+
actual: other.type_name(),
681+
}),
682+
}
683+
}
684+
685+
pub async fn read_real(&mut self, tag: &str) -> Result<f32, CipError> {
686+
match self.read_tag(tag).await? {
687+
CipValue::Real(v) => Ok(v),
688+
other => Err(CipError::TypeMismatch {
689+
expected: "REAL",
690+
actual: other.type_name(),
691+
}),
692+
}
693+
}
694+
695+
pub async fn read_string(&mut self, tag: &str) -> Result<String, CipError> {
696+
match self.read_tag(tag).await? {
697+
CipValue::String(s) => Ok(s),
698+
other => Err(CipError::TypeMismatch {
699+
expected: "STRING",
700+
actual: other.type_name(),
701+
}),
702+
}
703+
}
704+
705+
pub async fn write_dint(&mut self, tag: &str, value: i32) -> Result<(), CipError> {
706+
self.write_tag(tag, CipValue::DInt(value)).await
707+
}
708+
709+
pub async fn read_int(&mut self, tag: &str) -> Result<i16, CipError> {
710+
match self.read_tag(tag).await? {
711+
CipValue::Int(v) => Ok(v),
712+
other => Err(CipError::TypeMismatch {
713+
expected: "INT",
714+
actual: other.type_name(),
715+
}),
716+
}
717+
}
718+
719+
pub async fn read_sint(&mut self, tag: &str) -> Result<i8, CipError> {
720+
match self.read_tag(tag).await? {
721+
CipValue::SInt(v) => Ok(v),
722+
other => Err(CipError::TypeMismatch {
723+
expected: "SINT",
724+
actual: other.type_name(),
725+
}),
726+
}
727+
}
728+
729+
pub async fn write_bool(&mut self, tag: &str, value: bool) -> Result<(), CipError> {
730+
self.write_tag(tag, CipValue::Bool(value)).await
731+
}
732+
733+
pub async fn write_real(&mut self, tag: &str, value: f32) -> Result<(), CipError> {
734+
self.write_tag(tag, CipValue::Real(value)).await
735+
}
736+
737+
pub async fn write_string(&mut self, tag: &str, value: &str) -> Result<(), CipError> {
738+
self.write_tag(tag, CipValue::String(value.to_string()))
739+
.await
740+
}
741+
742+
pub async fn write_int(&mut self, tag: &str, value: i16) -> Result<(), CipError> {
743+
self.write_tag(tag, CipValue::Int(value)).await
744+
}
745+
746+
pub async fn write_sint(&mut self, tag: &str, value: i8) -> Result<(), CipError> {
747+
self.write_tag(tag, CipValue::SInt(value)).await
748+
}
664749
}

src/types.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,22 @@ pub enum CipValue {
5353
Unit,
5454
}
5555

56+
impl CipValue {
57+
pub fn type_name(&self) -> &'static str {
58+
match self {
59+
CipValue::Bool(_) => "BOOL",
60+
CipValue::SInt(_) => "SINT",
61+
CipValue::Int(_) => "INT",
62+
CipValue::DInt(_) => "DINT",
63+
CipValue::LInt(_) => "LINT",
64+
CipValue::Real(_) => "REAL",
65+
CipValue::String(_) => "STRING",
66+
CipValue::BoolPacked(_) => "BOOL_PACKED",
67+
CipValue::Unit => "UNIT",
68+
}
69+
}
70+
}
71+
5672
/// Result type used by CIP Multiple Service Packet (MSP) responses.
5773
///
5874
/// - `Ok(T)` contains a successfully decoded CIP value.

tests/error_handling.rs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use ethernetip::cip::CipError;
2+
use ethernetip::types::CipValue;
23

34
#[test]
45
fn map_status_codes() {
@@ -13,3 +14,55 @@ fn map_status_codes() {
1314
CipError::VendorSpecific(0xAB)
1415
));
1516
}
17+
18+
#[test]
19+
fn type_mismatch_error_formats_correctly() {
20+
let err = CipError::TypeMismatch {
21+
expected: "DINT",
22+
actual: "REAL",
23+
};
24+
25+
let msg = format!("{err}");
26+
assert_eq!(msg, "Type mismatch: expected DINT, got REAL");
27+
}
28+
29+
#[test]
30+
fn cip_error_display_variants() {
31+
assert_eq!(
32+
format!("{}", CipError::ConnectionFailure),
33+
"Connection failure (0x01)"
34+
);
35+
assert_eq!(
36+
format!("{}", CipError::ResourceUnavailable),
37+
"Resource unavailable (0x02)"
38+
);
39+
assert_eq!(
40+
format!("{}", CipError::InvalidAttribute),
41+
"Invalid attribute (0x04)"
42+
);
43+
assert_eq!(
44+
format!("{}", CipError::PathSegmentError),
45+
"Path segment error (0x05)"
46+
);
47+
assert_eq!(
48+
format!("{}", CipError::PathDestinationUnknown),
49+
"Path destination unknown (0x06)"
50+
);
51+
assert_eq!(
52+
format!("{}", CipError::VendorSpecific(0xFF)),
53+
"Vendor-specific CIP error 0xFF"
54+
);
55+
}
56+
57+
#[test]
58+
fn cip_value_type_name() {
59+
assert_eq!(CipValue::Bool(true).type_name(), "BOOL");
60+
assert_eq!(CipValue::SInt(1).type_name(), "SINT");
61+
assert_eq!(CipValue::Int(1).type_name(), "INT");
62+
assert_eq!(CipValue::DInt(1).type_name(), "DINT");
63+
assert_eq!(CipValue::LInt(1).type_name(), "LINT");
64+
assert_eq!(CipValue::Real(1.0).type_name(), "REAL");
65+
assert_eq!(CipValue::String("x".into()).type_name(), "STRING");
66+
assert_eq!(CipValue::BoolPacked(vec![1]).type_name(), "BOOL_PACKED");
67+
assert_eq!(CipValue::Unit.type_name(), "UNIT");
68+
}

0 commit comments

Comments
 (0)