Skip to content

Commit 20f15c2

Browse files
committed
add full CIP object access support and extend explicit messaging features
1 parent 81e1c33 commit 20f15c2

6 files changed

Lines changed: 191 additions & 33 deletions

File tree

src/cip/read_write.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,58 @@ pub fn build_read_fragmented_request(
163163
cip
164164
}
165165

166+
fn encode_object_path(class_id: u8, instance_id: u8, slot: Option<u8>) -> Vec<u8> {
167+
let mut segments = Vec::new();
168+
169+
if let Some(slot) = slot {
170+
segments.extend_from_slice(&[0x01, slot, 0x00, 0x00]);
171+
}
172+
173+
segments.extend_from_slice(&[0x20, class_id, 0x24, instance_id]);
174+
175+
let word_count = (segments.len() / 2) as u8;
176+
let mut out = Vec::with_capacity(1 + segments.len());
177+
out.push(word_count);
178+
out.extend_from_slice(&segments);
179+
out
180+
}
181+
182+
fn encode_object_attribute_path(
183+
class_id: u8,
184+
instance_id: u8,
185+
attribute_id: u8,
186+
slot: Option<u8>,
187+
) -> Vec<u8> {
188+
let mut path = encode_object_path(class_id, instance_id, slot);
189+
path.extend_from_slice(&[0x30, attribute_id]);
190+
let word_count = ((path.len() - 1) / 2) as u8;
191+
path[0] = word_count;
192+
path
193+
}
194+
195+
pub fn build_get_attribute_single_request(
196+
class_id: u8,
197+
instance_id: u8,
198+
attribute_id: u8,
199+
slot: Option<u8>,
200+
) -> Vec<u8> {
201+
let path = encode_object_attribute_path(class_id, instance_id, attribute_id, slot);
202+
203+
let mut cip = Vec::with_capacity(1 + path.len());
204+
cip.push(CipService::GetAttributeSingle as u8);
205+
cip.extend_from_slice(&path);
206+
cip
207+
}
208+
209+
pub fn build_get_attribute_all_request(class_id: u8, instance_id: u8, slot: Option<u8>) -> Vec<u8> {
210+
let path = encode_object_path(class_id, instance_id, slot);
211+
212+
let mut cip = Vec::with_capacity(1 + path.len());
213+
cip.push(CipService::GetAttributeAll as u8);
214+
cip.extend_from_slice(&path);
215+
cip
216+
}
217+
166218
fn encode_connection_manager_path(slot: Option<u8>) -> Vec<u8> {
167219
let mut segments = Vec::new();
168220

src/cip/service.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
#[derive(Debug, Copy, Clone)]
22
pub enum CipService {
3+
GetAttributeAll = 0x01,
4+
GetAttributeSingle = 0x0E,
35
ReadData = 0x4C,
46
WriteData = 0x4D,
57
ReadFragmented = 0x52,

src/client/tag_rw.rs

Lines changed: 66 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
use std::io;
22

33
use crate::cip::{
4-
build_read_request, build_write_request, decode_cip_response, decode_write_response, CipError,
4+
build_get_attribute_all_request, build_get_attribute_single_request, build_read_request,
5+
build_write_request, decode_cip_response, decode_write_response, CipError,
56
};
67
use crate::client::{ConnectedMessaging, TagReadWrite, UnconnectedMessaging};
78
use crate::types::CipValue;
@@ -20,23 +21,9 @@ impl TagReadWrite for EthernetIpClient {
2021
};
2122

2223
let res = res.map_err(|_| CipError::VendorSpecific(0xFF))?;
24+
let payload = parse_cip_response_payload(&res)?;
2325

24-
if res.len() < 4 {
25-
return Err(CipError::VendorSpecific(0xFE));
26-
}
27-
28-
let general_status = res[2];
29-
if general_status != 0 {
30-
return Err(CipError::from(general_status));
31-
}
32-
33-
let ext_words = res[3] as usize;
34-
let data_start = 4 + ext_words * 2;
35-
if res.len() < data_start {
36-
return Err(CipError::VendorSpecific(0xFD));
37-
}
38-
39-
decode_cip_response(&res[data_start..]).ok_or(CipError::VendorSpecific(0xFC))
26+
decode_cip_response(payload).ok_or(CipError::VendorSpecific(0xFC))
4027
}
4128

4229
async fn write_tag(&mut self, tag: &str, value: CipValue) -> Result<(), CipError> {
@@ -66,27 +53,56 @@ impl TagReadWrite for EthernetIpClient {
6653
};
6754

6855
let res = res.map_err(|_| CipError::VendorSpecific(0xFF))?;
56+
let payload = parse_cip_response_payload(&res)?;
6957

70-
if res.len() < 4 {
71-
return Err(CipError::VendorSpecific(0xFE));
58+
if payload.len() < 2 {
59+
return Err(CipError::VendorSpecific(0xFD));
7260
}
7361

74-
let general_status = res[2];
75-
if general_status != 0 {
76-
return Err(CipError::from(general_status));
77-
}
62+
let type_id = u16::from_le_bytes([payload[0], payload[1]]);
63+
let payload = &payload[2..];
7864

79-
let ext_words = res[3] as usize;
80-
let data_start = 4 + ext_words * 2;
65+
Ok(crate::cip::decode_cip_data_list(type_id, payload))
66+
}
8167

82-
if res.len() < data_start + 2 {
83-
return Err(CipError::VendorSpecific(0xFD));
84-
}
68+
async fn read_object_attribute(
69+
&mut self,
70+
class_id: u8,
71+
instance_id: u8,
72+
attribute_id: u8,
73+
) -> Result<CipValue, CipError> {
74+
let cip =
75+
build_get_attribute_single_request(class_id, instance_id, attribute_id, self.slot);
8576

86-
let type_id = u16::from_le_bytes([res[data_start], res[data_start + 1]]);
87-
let payload = &res[data_start + 2..];
77+
let res: io::Result<Vec<u8>> = if self.connected {
78+
self.send_unit_data(cip).await
79+
} else {
80+
self.send_rr_data(cip).await
81+
};
8882

89-
Ok(crate::cip::decode_cip_data_list(type_id, payload))
83+
let res = res.map_err(|_| CipError::VendorSpecific(0xFF))?;
84+
let payload = parse_cip_response_payload(&res)?;
85+
86+
decode_cip_response(payload).ok_or(CipError::VendorSpecific(0xFC))
87+
}
88+
89+
async fn read_object_attributes(
90+
&mut self,
91+
class_id: u8,
92+
instance_id: u8,
93+
) -> Result<Vec<u8>, CipError> {
94+
let cip = build_get_attribute_all_request(class_id, instance_id, self.slot);
95+
96+
let res: io::Result<Vec<u8>> = if self.connected {
97+
self.send_unit_data(cip).await
98+
} else {
99+
self.send_rr_data(cip).await
100+
};
101+
102+
let res = res.map_err(|_| CipError::VendorSpecific(0xFF))?;
103+
let payload = parse_cip_response_payload(&res)?;
104+
105+
Ok(payload.to_vec())
90106
}
91107

92108
async fn write_tag_multi(&mut self, tag: &str, values: &[CipValue]) -> Result<(), CipError> {
@@ -182,3 +198,22 @@ impl TagReadWrite for EthernetIpClient {
182198
.await
183199
}
184200
}
201+
202+
fn parse_cip_response_payload(res: &[u8]) -> Result<&[u8], CipError> {
203+
if res.len() < 4 {
204+
return Err(CipError::VendorSpecific(0xFE));
205+
}
206+
207+
let general_status = res[2];
208+
if general_status != 0 {
209+
return Err(CipError::from(general_status));
210+
}
211+
212+
let ext_words = res[3] as usize;
213+
let data_start = 4 + ext_words * 2;
214+
if res.len() < data_start {
215+
return Err(CipError::VendorSpecific(0xFD));
216+
}
217+
218+
Ok(&res[data_start..])
219+
}

src/client/traits.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,27 @@ pub trait TagReadWrite {
4949
async fn read_tag_multi(&mut self, tag: &str, count: usize) -> Result<Vec<CipValue>, CipError>;
5050
async fn write_tag_multi(&mut self, tag: &str, values: &[CipValue]) -> Result<(), CipError>;
5151

52+
async fn read_object_attribute(
53+
&mut self,
54+
class_id: u8,
55+
instance_id: u8,
56+
attribute_id: u8,
57+
) -> Result<CipValue, CipError>;
58+
59+
async fn read_object_attributes(
60+
&mut self,
61+
class_id: u8,
62+
instance_id: u8,
63+
) -> Result<Vec<u8>, CipError>;
64+
65+
async fn read_identity_attribute(&mut self, attribute_id: u8) -> Result<CipValue, CipError> {
66+
self.read_object_attribute(0x01, 0x01, attribute_id).await
67+
}
68+
69+
async fn read_identity_attributes(&mut self) -> Result<Vec<u8>, CipError> {
70+
self.read_object_attributes(0x01, 0x01).await
71+
}
72+
5273
async fn read_bool(&mut self, tag: &str) -> Result<bool, CipError>;
5374
async fn read_sint(&mut self, tag: &str) -> Result<i8, CipError>;
5475
async fn read_int(&mut self, tag: &str) -> Result<i16, CipError>;

src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ pub mod fake_plc;
55
pub mod types;
66

77
pub use cip::{
8-
build_cip_multiple_service_request, build_read_request, build_write_request,
8+
build_cip_multiple_service_request, build_get_attribute_all_request,
9+
build_get_attribute_single_request, build_read_request, build_write_request,
910
decode_cip_response, encode_epath, encode_epath_with_slot, parse_cip_multiple_service_response,
1011
parse_symbol_browse_response, CipError, CipService,
1112
};

tests/cip_builders.rs

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use ethernetip::cip::{
2-
build_forward_close_request, build_forward_open_request, build_large_forward_close_request,
2+
build_forward_close_request, build_forward_open_request, build_get_attribute_all_request,
3+
build_get_attribute_single_request, build_large_forward_close_request,
34
build_large_forward_open_request, build_read_fragmented_request, build_read_request,
45
build_write_request, decode_extended_status, describe_extended_status, map_extended_status,
56
service::CipService, ConnectionParams, ForwardOpenError, TransportTrigger,
@@ -62,6 +63,52 @@ fn test_build_read_fragmented_request() {
6263
assert_eq!(offset, 200);
6364
}
6465

66+
#[test]
67+
fn test_build_get_attribute_single_request() {
68+
let cip = build_get_attribute_single_request(0x06, 0x01, 0x03, None);
69+
assert_eq!(cip[0], CipService::GetAttributeSingle as u8);
70+
assert_eq!(cip[1], 3); // word count for class+instance+attribute
71+
assert_eq!(cip[2], 0x20);
72+
assert_eq!(cip[3], 0x06);
73+
assert_eq!(cip[4], 0x24);
74+
assert_eq!(cip[5], 0x01);
75+
assert_eq!(cip[6], 0x30);
76+
assert_eq!(cip[7], 0x03);
77+
}
78+
79+
#[test]
80+
fn test_build_get_attribute_single_request_slot() {
81+
let cip = build_get_attribute_single_request(0x06, 0x01, 0x03, Some(2));
82+
assert_eq!(cip[0], CipService::GetAttributeSingle as u8);
83+
assert_eq!(cip[1], 5); // word count for port segment + object path
84+
assert_eq!(cip[2], 0x01);
85+
assert_eq!(cip[3], 2);
86+
assert_eq!(cip[6], 0x20);
87+
assert_eq!(cip[7], 0x06);
88+
}
89+
90+
#[test]
91+
fn test_build_get_attribute_all_request() {
92+
let cip = build_get_attribute_all_request(0x02, 0x01, None);
93+
assert_eq!(cip[0], CipService::GetAttributeAll as u8);
94+
assert_eq!(cip[1], 2); // word count for class + instance
95+
assert_eq!(cip[2], 0x20);
96+
assert_eq!(cip[3], 0x02);
97+
assert_eq!(cip[4], 0x24);
98+
assert_eq!(cip[5], 0x01);
99+
}
100+
101+
#[test]
102+
fn test_build_get_attribute_all_request_slot() {
103+
let cip = build_get_attribute_all_request(0x02, 0x01, Some(3));
104+
assert_eq!(cip[0], CipService::GetAttributeAll as u8);
105+
assert_eq!(cip[1], 4); // word count for port segment + class + instance
106+
assert_eq!(cip[2], 0x01);
107+
assert_eq!(cip[3], 3);
108+
assert_eq!(cip[6], 0x20);
109+
assert_eq!(cip[7], 0x02);
110+
}
111+
65112
#[test]
66113
fn test_forward_open_request_structure() {
67114
let cip = build_forward_open_request(Some(2), ConnectionParams::default());

0 commit comments

Comments
 (0)