Skip to content

Commit c5c7ae1

Browse files
committed
add DiscoveredIdentity struct and Display impl; fully rewrite discovery.rs to use CPF item parsing and return structured identity results instead of tuples
1 parent 1da0abb commit c5c7ae1

3 files changed

Lines changed: 105 additions & 17 deletions

File tree

src/client/discovery.rs

Lines changed: 90 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,59 +6,134 @@ use tokio::net::UdpSocket;
66
use tokio::time::{timeout, Duration};
77

88
use crate::encapsulation::*;
9+
use crate::types::DiscoveredIdentity;
910

1011
use super::Discovery;
1112

1213
pub struct DiscoveryImpl;
1314

1415
#[async_trait::async_trait]
1516
impl Discovery for DiscoveryImpl {
16-
async fn discover() -> io::Result<Vec<(String, String)>> {
17+
async fn discover() -> io::Result<Vec<DiscoveredIdentity>> {
1718
const ENIP_PORT: u16 = 44818;
1819
const DISCOVERY_TIMEOUT: Duration = Duration::from_secs(1);
1920
const RECV_TIMEOUT: Duration = Duration::from_millis(200);
20-
const MIN_ENCAP_HEADER: usize = 24;
21-
const ETHERNET_IP_HEADER_SKIP: usize = 30;
22-
const IDENTITY_HEADER_LEN: usize = 32;
2321

2422
let socket = UdpSocket::bind("0.0.0.0:0").await?;
2523
socket.set_broadcast(true)?;
2624

25+
// Send ListIdentity request
2726
let msg = EncapsulationHeader::new(COMMAND_LIST_IDENTITY, 0, 0).to_bytes();
2827
socket
2928
.send_to(&msg, SocketAddr::from(([255, 255, 255, 255], ENIP_PORT)))
3029
.await?;
3130

3231
let mut results = Vec::new();
33-
let mut buf = [0u8; 1024];
32+
let mut buf = [0u8; 2048];
3433
let start = Instant::now();
3534

3635
while start.elapsed() < DISCOVERY_TIMEOUT {
3736
if let Ok(Ok((len, addr))) = timeout(RECV_TIMEOUT, socket.recv_from(&mut buf)).await {
38-
if len < ETHERNET_IP_HEADER_SKIP + MIN_ENCAP_HEADER {
37+
if len < 24 {
3938
continue;
4039
}
4140

42-
let data = &buf[..len];
43-
let payload = &data[ETHERNET_IP_HEADER_SKIP..];
41+
// Parse encapsulation header
42+
let hdr = match EncapsulationHeader::from_bytes(&buf[..24]) {
43+
Some(h) => h,
44+
None => continue,
45+
};
4446

45-
if payload.len() < IDENTITY_HEADER_LEN + 1 {
47+
if hdr.command != COMMAND_LIST_IDENTITY || hdr.status != 0 {
4648
continue;
4749
}
4850

49-
let name_len = payload[IDENTITY_HEADER_LEN] as usize;
50-
let name_start = IDENTITY_HEADER_LEN + 1;
51-
if payload.len() < name_start + name_len {
51+
let payload = &buf[24..len];
52+
if payload.len() < 2 {
5253
continue;
5354
}
5455

55-
let name = String::from_utf8_lossy(&payload[name_start..name_start + name_len])
56-
.into_owned();
56+
// CPF item count
57+
let item_count = u16::from_le_bytes([payload[0], payload[1]]) as usize;
58+
let mut pos = 2;
5759

58-
results.push((addr.ip().to_string(), name));
60+
for _ in 0..item_count {
61+
if payload.len() < pos + 4 {
62+
break;
63+
}
64+
65+
let type_id = u16::from_le_bytes([payload[pos], payload[pos + 1]]);
66+
let item_len =
67+
u16::from_le_bytes([payload[pos + 2], payload[pos + 3]]) as usize;
68+
pos += 4;
69+
70+
if payload.len() < pos + item_len {
71+
break;
72+
}
73+
74+
// Identity Item = 0x000C
75+
if type_id == 0x000C {
76+
if let Some(info) = parse_discovery_identity(
77+
&payload[pos..pos + item_len],
78+
addr.ip().to_string(),
79+
) {
80+
results.push(info);
81+
}
82+
}
83+
84+
pos += item_len;
85+
}
5986
}
6087
}
6188

6289
Ok(results)
6390
}
6491
}
92+
93+
/// Parse the Identity Item (0x000C) returned by ListIdentity (0x63).
94+
///
95+
/// This is *not* the same as the Identity Object (Class 0x01).
96+
fn parse_discovery_identity(item: &[u8], ip: String) -> Option<DiscoveredIdentity> {
97+
// Minimum fields before product name:
98+
// 0-1: Encapsulation Protocol Version
99+
// 2-3: Socket Address Family
100+
// 4-9: Socket Address (ignored)
101+
// 10-11: Vendor ID
102+
// 12-13: Device Type
103+
// 14-15: Product Code
104+
// 16: Revision Major
105+
// 17: Revision Minor
106+
// 18-19: Status
107+
// 20-23: Serial Number
108+
// 24: Product Name Length
109+
if item.len() < 25 {
110+
return None;
111+
}
112+
113+
let vendor_id = u16::from_le_bytes([item[10], item[11]]);
114+
let device_type = u16::from_le_bytes([item[12], item[13]]);
115+
let product_code = u16::from_le_bytes([item[14], item[15]]);
116+
let revision_major = item[16];
117+
let revision_minor = item[17];
118+
let status = u16::from_le_bytes([item[18], item[19]]);
119+
let serial = u32::from_le_bytes([item[20], item[21], item[22], item[23]]);
120+
121+
let name_len = item[24] as usize;
122+
if item.len() < 25 + name_len {
123+
return None;
124+
}
125+
126+
let product_name = String::from_utf8_lossy(&item[25..25 + name_len]).into_owned();
127+
128+
Some(DiscoveredIdentity {
129+
ip,
130+
vendor_id,
131+
device_type,
132+
product_code,
133+
revision_major,
134+
revision_minor,
135+
status,
136+
serial,
137+
product_name,
138+
})
139+
}

src/client/traits.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use std::io;
22

33
use crate::cip::CipError;
44
use crate::client::ForwardOpenError;
5-
use crate::types::{CipValue, ConnectionManagerInfo, IdentityInfo, SymbolInfo};
5+
use crate::types::{CipValue, ConnectionManagerInfo, DiscoveredIdentity, IdentityInfo, SymbolInfo};
66
use crate::MultiResult;
77

88
pub trait ConnectionManagement {
@@ -23,7 +23,7 @@ pub trait Connectable: Sized {
2323

2424
#[async_trait::async_trait]
2525
pub trait Discovery {
26-
async fn discover() -> io::Result<Vec<(String, String)>>;
26+
async fn discover() -> io::Result<Vec<DiscoveredIdentity>>;
2727
}
2828

2929
#[async_trait::async_trait]

src/types.rs

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

56+
#[derive(Debug, Clone, PartialEq)]
57+
pub struct DiscoveredIdentity {
58+
pub ip: String,
59+
pub vendor_id: u16,
60+
pub device_type: u16,
61+
pub product_code: u16,
62+
pub revision_major: u8,
63+
pub revision_minor: u8,
64+
pub status: u16,
65+
pub serial: u32,
66+
pub product_name: String,
67+
}
68+
5669
#[derive(Debug, Clone, PartialEq)]
5770
pub struct IdentityInfo {
5871
pub vendor_id: u16,

0 commit comments

Comments
 (0)