1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
//! KNX/IP gateway discovery implementation.
use crate::error::{DiscoveryError, Result};
use std::collections::HashMap;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::time::Duration;
use tokio::net::UdpSocket;
use tokio::time::timeout;
/// Gateway discovery scanner
pub struct GatewayScanner {
socket: UdpSocket,
}
impl GatewayScanner {
/// Create a new gateway scanner
///
/// # Errors
///
/// Returns [`DiscoveryError::NetworkError`] if the UDP socket can't be
/// bound or configured for broadcast.
pub async fn new() -> Result<Self> {
let socket = UdpSocket::bind("0.0.0.0:0")
.await
.map_err(DiscoveryError::NetworkError)?;
socket
.set_broadcast(true)
.map_err(DiscoveryError::NetworkError)?;
Ok(Self { socket })
}
/// Discover KNX/IP gateways on the network
///
/// # Errors
///
/// Returns [`DiscoveryError::NetworkError`] if sending the discovery
/// broadcast fails or receiving fails with anything other than a
/// timeout. Malformed responses from individual gateways are logged and
/// skipped rather than returned as an error.
pub async fn discover(&self, discovery_timeout: Duration) -> Result<Vec<GatewayInfo>> {
let mut gateways = HashMap::new();
// Build and send discovery request
let discovery_request = Self::build_discovery_request();
let broadcast_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::BROADCAST), 3671);
// Send discovery request
if let Err(e) = self
.socket
.send_to(&discovery_request, broadcast_addr)
.await
{
return Err(DiscoveryError::NetworkError(e).into());
}
// Collect responses with timeout
let result = timeout(discovery_timeout, async {
let mut buf = vec![0u8; 1024];
let mut last_error: Option<DiscoveryError> = None;
loop {
match self.socket.recv_from(&mut buf).await {
Ok((len, addr)) => {
match self.parse_discovery_response(&buf[..len], addr) {
Ok(gateway_info) => {
gateways.insert(addr, gateway_info);
}
Err(e) => {
// Log parsing errors but continue discovery
log::debug!("Failed to parse discovery response from {addr}: {e}");
if let crate::error::KnxError::Discovery(disc_err) = e {
last_error = Some(disc_err);
}
}
}
}
Err(e) => {
// Check if this is a timeout or a real error
match e.kind() {
std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut => {
// This is expected when no more responses are coming
break;
}
_ => {
// Real network error
last_error = Some(DiscoveryError::NetworkError(e));
break;
}
}
}
}
}
// Return any parsing errors if no gateways were found
if gateways.is_empty()
&& let Some(err) = last_error
{
return Err(err);
}
Ok(())
})
.await;
match result {
Ok(Ok(())) => {
if gateways.is_empty() {
Err(DiscoveryError::NoGatewaysFound.into())
} else {
Ok(gateways.into_values().collect())
}
}
Ok(Err(e)) => Err(e.into()),
Err(_) => {
// Timeout occurred
if gateways.is_empty() {
Err(DiscoveryError::Timeout {
timeout_ms: discovery_timeout.as_millis() as u64,
}
.into())
} else {
// Return partial results even on timeout
Ok(gateways.into_values().collect())
}
}
}
}
/// Build a KNX/IP discovery request frame
fn build_discovery_request() -> Vec<u8> {
// Build a proper KNX/IP search request frame according to specification
let mut frame = Vec::new();
// KNX/IP Header
frame.extend_from_slice(&[
0x06, 0x10, // Header length (6) and version (1.0)
0x02, 0x01, // Search request service type
0x00, 0x0E, // Total length (14 bytes)
]);
// Discovery endpoint HPAI (Host Protocol Address Information)
frame.extend_from_slice(&[
0x08, 0x01, // Structure length (8) and host protocol code (UDP)
0x00, 0x00, 0x00, 0x00, // IP address (0.0.0.0 = any)
0x00, 0x00, // Port (0 = any)
]);
frame
}
/// Parse a discovery response frame
///
/// # Errors
///
/// Returns [`DiscoveryError::InvalidResponse`] if `data` is too short or
/// doesn't match the expected KNX/IP search-response structure.
pub fn parse_discovery_response(&self, data: &[u8], addr: SocketAddr) -> Result<GatewayInfo> {
if data.len() < 6 {
return Err(DiscoveryError::InvalidResponse {
addr: addr.to_string(),
reason: "Response too short".to_string(),
}
.into());
}
// Verify KNX/IP header
if data[0] != 0x06 || data[1] != 0x10 {
return Err(DiscoveryError::InvalidResponse {
addr: addr.to_string(),
reason: "Invalid KNX/IP header".to_string(),
}
.into());
}
// Check service type (should be search response 0x0202)
if data.len() >= 4 && (data[2] != 0x02 || data[3] != 0x02) {
return Err(DiscoveryError::InvalidResponse {
addr: addr.to_string(),
reason: "Not a search response".to_string(),
}
.into());
}
// Extract total length
let total_length = if data.len() >= 6 {
(u16::from(data[4]) << 8) | u16::from(data[5])
} else {
data.len() as u16
};
if (total_length as usize) > data.len() {
return Err(DiscoveryError::InvalidResponse {
addr: addr.to_string(),
reason: format!(
"Frame length mismatch: expected {}, got {}",
total_length,
data.len()
),
}
.into());
}
// Parse device information from the frame
let mut name = format!("KNX Gateway {}", addr.ip());
let mut capabilities = GatewayCapabilities {
supports_tunneling: false,
supports_routing: false,
supports_device_management: false,
max_tunneling_connections: 1,
};
let mut supported_services = vec![ServiceType::Core];
let mut device_serial = format!("SN{:08X}", addr.ip().to_string().len());
// Try to parse DIBs (Device Information Blocks)
let mut offset = 14; // Skip header and control endpoint HPAI
while offset < data.len() {
if offset + 2 > data.len() {
break;
}
let dib_length = data[offset] as usize;
let dib_type = data[offset + 1];
if dib_length < 2 || offset + dib_length > data.len() {
break;
}
match dib_type {
0x01 => {
// Device hardware DIB
if dib_length >= 54 && offset + 54 <= data.len() {
// Extract serial number (6 bytes starting at offset + 12)
if offset + 18 <= data.len() {
let serial_bytes = &data[offset + 12..offset + 18];
device_serial = format!(
"SN{:02X}{:02X}{:02X}{:02X}{:02X}{:02X}",
serial_bytes[0],
serial_bytes[1],
serial_bytes[2],
serial_bytes[3],
serial_bytes[4],
serial_bytes[5]
);
}
// Extract friendly name (30 bytes starting at offset + 24)
if offset + 54 <= data.len() {
let name_bytes = &data[offset + 24..offset + 54];
if let Some(null_pos) = name_bytes.iter().position(|&b| b == 0)
&& let Ok(parsed_name) =
std::str::from_utf8(&name_bytes[..null_pos])
&& !parsed_name.is_empty()
{
name = parsed_name.to_string();
}
}
}
}
0x02 => {
// Supported service families DIB
let mut services_offset = offset + 2;
supported_services.clear();
supported_services.push(ServiceType::Core); // Always include core
while services_offset + 2 <= offset + dib_length {
let service_family = data[services_offset];
// data[services_offset + 1] is the service version, unused here.
match service_family {
0x03 => {
capabilities.supports_device_management = true;
supported_services.push(ServiceType::DeviceManagement);
}
0x04 => {
capabilities.supports_tunneling = true;
capabilities.max_tunneling_connections = 4; // Default assumption
supported_services.push(ServiceType::Tunneling);
}
0x05 => {
capabilities.supports_routing = true;
supported_services.push(ServiceType::Routing);
}
// 0x02 (Core) is pre-added above; unknown families ignored.
_ => {}
}
services_offset += 2;
}
}
_ => {
// Unknown DIB type, skip
}
}
offset += dib_length;
}
// If no services were parsed from DIB, use defaults based on common gateway capabilities
if supported_services.len() == 1 {
// Only Core service
capabilities.supports_tunneling = true;
capabilities.supports_device_management = true;
capabilities.max_tunneling_connections = 4;
supported_services
.extend_from_slice(&[ServiceType::DeviceManagement, ServiceType::Tunneling]);
}
// Determine multicast address before moving capabilities
let multicast_addr = if capabilities.supports_routing {
Some(SocketAddr::new(
std::net::IpAddr::V4(std::net::Ipv4Addr::new(224, 0, 23, 12)),
3671,
))
} else {
None
};
Ok(GatewayInfo {
addr,
name,
capabilities,
supported_services,
device_serial,
mac_address: None,
multicast_addr,
})
}
}
/// Information about a discovered KNX/IP gateway
#[derive(Debug, Clone)]
pub struct GatewayInfo {
/// Gateway network address
pub addr: SocketAddr,
/// Gateway device name
pub name: String,
/// Gateway capabilities
pub capabilities: GatewayCapabilities,
/// Supported service types
pub supported_services: Vec<ServiceType>,
/// Device serial number
pub device_serial: String,
/// MAC address (if available)
pub mac_address: Option<String>,
/// Multicast address for routing (if supported)
pub multicast_addr: Option<SocketAddr>,
}
/// Gateway capability information
#[derive(Debug, Clone)]
pub struct GatewayCapabilities {
/// Supports KNX/IP tunneling
pub supports_tunneling: bool,
/// Supports KNX/IP routing
pub supports_routing: bool,
/// Supports device management
pub supports_device_management: bool,
/// Maximum number of concurrent tunneling connections
pub max_tunneling_connections: u8,
}
/// KNX/IP service types
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ServiceType {
/// Core services
Core,
/// Device management
DeviceManagement,
/// Tunneling
Tunneling,
/// Routing
Routing,
/// Remote logging
RemoteLogging,
/// Remote configuration
RemoteConfiguration,
/// Object server
ObjectServer,
}