@@ -6,59 +6,134 @@ use tokio::net::UdpSocket;
66use tokio:: time:: { timeout, Duration } ;
77
88use crate :: encapsulation:: * ;
9+ use crate :: types:: DiscoveredIdentity ;
910
1011use super :: Discovery ;
1112
1213pub struct DiscoveryImpl ;
1314
1415#[ async_trait:: async_trait]
1516impl 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+ }
0 commit comments