Skip to content

Commit 8fc779a

Browse files
committed
Optimizations
1 parent 8ac7a2d commit 8fc779a

File tree

17 files changed

+535
-140
lines changed

17 files changed

+535
-140
lines changed

crates/bacnet-client/src/client.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1912,6 +1912,31 @@ impl<T: TransportPort + 'static> BACnetClient<T> {
19121912
self.device_table.lock().await.clear();
19131913
}
19141914

1915+
/// Manually register a device in the device table.
1916+
///
1917+
/// Useful for adding known devices without requiring WhoIs/IAm exchange.
1918+
/// Sets default values for max_apdu_length (1476), segmentation (NONE),
1919+
/// and vendor_id (0) since these are unknown without IAm.
1920+
pub async fn add_device(&self, instance: u32, mac: &[u8]) -> Result<(), Error> {
1921+
let oid = bacnet_types::primitives::ObjectIdentifier::new(
1922+
bacnet_types::enums::ObjectType::DEVICE,
1923+
instance,
1924+
)?;
1925+
let device = DiscoveredDevice {
1926+
object_identifier: oid,
1927+
mac_address: MacAddr::from_slice(mac),
1928+
max_apdu_length: 1476,
1929+
segmentation_supported: bacnet_types::enums::Segmentation::NONE,
1930+
max_segments_accepted: None,
1931+
vendor_id: 0,
1932+
last_seen: std::time::Instant::now(),
1933+
source_network: None,
1934+
source_address: None,
1935+
};
1936+
self.device_table.lock().await.upsert(device);
1937+
Ok(())
1938+
}
1939+
19151940
/// Stop the client, aborting the dispatch task.
19161941
pub async fn stop(&mut self) -> Result<(), Error> {
19171942
if let Some(task) = self.dispatch_task.take() {

crates/bacnet-gateway/src/api/devices.rs

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ pub struct DiscoverRequest {
1919
pub low_instance: Option<u32>,
2020
pub high_instance: Option<u32>,
2121
pub timeout_seconds: Option<u64>,
22+
/// Target address for unicast discovery (e.g., "192.168.1.100:47808").
23+
/// If omitted, sends a broadcast WhoIs.
24+
pub target: Option<String>,
2225
}
2326

2427
pub async fn discover_devices(
@@ -47,16 +50,57 @@ pub async fn discover_devices(
4750
.unwrap_or(3)
4851
.min(MAX_DISCOVER_TIMEOUT_SECS);
4952

50-
if let Err(e) = client.who_is(low, high).await {
51-
let (status, err) = ApiError::from_bacnet_error(&e);
52-
return (status, Json(err)).into_response();
53+
let target = body.as_ref().and_then(|b| b.target.as_ref());
54+
match target {
55+
Some(t) => {
56+
let addr: std::net::SocketAddrV4 = match t.parse() {
57+
Ok(a) => a,
58+
Err(e) => {
59+
return ApiError::bad_request(format!("invalid target address: {e}"))
60+
.into_response()
61+
}
62+
};
63+
let mac = crate::parse::socket_addr_to_mac(addr);
64+
if let Err(e) = client.who_is_directed(&mac, low, high).await {
65+
let (status, err) = ApiError::from_bacnet_error(&e);
66+
return (status, Json(err)).into_response();
67+
}
68+
}
69+
None => {
70+
if let Err(e) = client.who_is(low, high).await {
71+
let (status, err) = ApiError::from_bacnet_error(&e);
72+
return (status, Json(err)).into_response();
73+
}
74+
}
5375
}
5476

5577
tokio::time::sleep(std::time::Duration::from_secs(timeout)).await;
5678

5779
list_known_devices_inner(&state).await.into_response()
5880
}
5981

82+
/// POST /api/v1/devices
83+
#[derive(Debug, Deserialize)]
84+
pub struct RegisterDeviceRequest {
85+
pub instance: u32,
86+
pub address: String,
87+
}
88+
89+
pub async fn register_device(
90+
State(state): State<GatewayState>,
91+
Json(req): Json<RegisterDeviceRequest>,
92+
) -> impl IntoResponse {
93+
match state.add_device_manual(req.instance, &req.address).await {
94+
Ok(()) => Json(serde_json::json!({
95+
"status": "registered",
96+
"instance": req.instance,
97+
"address": req.address,
98+
}))
99+
.into_response(),
100+
Err(msg) => ApiError::bad_request(msg).into_response(),
101+
}
102+
}
103+
60104
/// GET /api/v1/devices
61105
pub async fn list_devices(State(state): State<GatewayState>) -> impl IntoResponse {
62106
list_known_devices_inner(&state).await

crates/bacnet-gateway/src/api/mod.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,10 @@ pub fn api_router(state: GatewayState, auth: Option<Box<dyn Authenticator>>) ->
3838
get(objects::get_object_property).put(objects::put_object_property),
3939
)
4040
// Device discovery.
41-
.route("/devices", get(devices::list_devices))
41+
.route(
42+
"/devices",
43+
get(devices::list_devices).post(devices::register_device),
44+
)
4245
.route("/devices/discover", post(devices::discover_devices))
4346
.route("/devices/{instance}", get(devices::get_device_info))
4447
// Remote property access.

crates/bacnet-gateway/src/api/objects.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ use crate::state::GatewayState;
1212

1313
use super::types::{
1414
json_to_property_value, object_type_name, parse_object_specifier, parse_object_type,
15-
parse_property_name, property_name, property_value_to_json, ApiError, WritePropertyRequest,
15+
parse_property_name, property_name, property_value_to_json,
16+
property_value_to_json_with_context, ApiError, WritePropertyRequest,
1617
};
1718

1819
#[derive(Debug, Deserialize)]
@@ -87,7 +88,7 @@ pub async fn get_object(
8788
obj.read_property(prop, None).ok().map(|val| {
8889
serde_json::json!({
8990
"property": property_name(prop),
90-
"value": property_value_to_json(&val),
91+
"value": property_value_to_json_with_context(&val, prop),
9192
})
9293
})
9394
})
@@ -135,7 +136,7 @@ pub async fn get_object_property(
135136
match obj.read_property(property, array_index) {
136137
Ok(val) => Json(serde_json::json!({
137138
"property": property_name(property),
138-
"value": property_value_to_json(&val),
139+
"value": property_value_to_json_with_context(&val, property),
139140
}))
140141
.into_response(),
141142
Err(e) => {

crates/bacnet-gateway/src/api/properties.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ use bacnet_types::primitives::ObjectIdentifier;
1010
use crate::state::GatewayState;
1111

1212
use super::types::{
13-
decode_raw_property_to_json, json_to_property_value, object_type_name, parse_object_specifier,
14-
parse_property_name, property_name, ApiError, WritePropertyRequest,
13+
decode_raw_property_to_json_with_context, json_to_property_value, object_type_name,
14+
parse_object_specifier, parse_property_name, property_name, ApiError, WritePropertyRequest,
1515
};
1616

1717
/// GET /api/v1/devices/{instance}/objects/{specifier}/properties/{property}
@@ -61,7 +61,7 @@ pub async fn read_remote_property(
6161
"device": instance,
6262
"object": format!("{}:{}", object_type_name(obj_type), obj_instance),
6363
"property": property_name(property),
64-
"value": decode_raw_property_to_json(&ack.property_value),
64+
"value": decode_raw_property_to_json_with_context(&ack.property_value, property),
6565
}))
6666
.into_response(),
6767
Err(e) => {

crates/bacnet-gateway/src/builder.rs

Lines changed: 78 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use bacnet_transport::bip::BipTransport;
1111
use bacnet_types::error::Error;
1212

1313
use crate::config::GatewayConfig;
14+
use crate::parse::{construct_object, parse_object_type};
1415
use crate::state::GatewayState;
1516

1617
/// Builder for constructing a fully wired Gateway.
@@ -39,6 +40,27 @@ impl GatewayBuilder {
3940
})?;
4041
db.add(Box::new(device))?;
4142

43+
// Pre-populate local objects from config.
44+
for obj_cfg in &self.config.objects {
45+
let obj_type = parse_object_type(&obj_cfg.object_type)
46+
.map_err(|e| Error::Encoding(format!("object config: {e}")))?;
47+
let obj = construct_object(
48+
obj_type,
49+
obj_cfg.instance,
50+
&obj_cfg.name,
51+
obj_cfg.number_of_states,
52+
)
53+
.map_err(|e| Error::Encoding(format!("object config: {e}")))?;
54+
db.add(obj)
55+
.map_err(|e| Error::Encoding(format!("object config: {e}")))?;
56+
tracing::info!(
57+
"Pre-populated object {}:{} ({})",
58+
obj_cfg.object_type,
59+
obj_cfg.instance,
60+
obj_cfg.name
61+
);
62+
}
63+
4264
// Determine BIP settings (required for now).
4365
let bip = self.config.transports.bip.as_ref().ok_or_else(|| {
4466
Error::Encoding("[transports.bip] is required for the gateway".to_string())
@@ -53,23 +75,69 @@ impl GatewayBuilder {
5375
.parse()
5476
.map_err(|e| Error::Encoding(format!("invalid BIP broadcast: {e}")))?;
5577

56-
// Start server on the configured BIP port.
57-
let server = BACnetServer::bip_builder()
58-
.interface(interface)
59-
.port(bip.port)
60-
.broadcast_address(broadcast)
78+
// Parse foreign device config once (used for both server and client transports).
79+
let fd_config = if let Some(fd) = &self.config.foreign_device {
80+
let addr: std::net::SocketAddrV4 = fd.bbmd.parse().map_err(|e| {
81+
Error::Encoding(format!("invalid foreign_device.bbmd '{}': {e}", fd.bbmd))
82+
})?;
83+
Some(bacnet_transport::bip::ForeignDeviceConfig {
84+
bbmd_ip: *addr.ip(),
85+
bbmd_port: addr.port(),
86+
ttl: fd.ttl,
87+
})
88+
} else {
89+
None
90+
};
91+
92+
// Create and configure BIP transport for the server.
93+
let mut server_transport = BipTransport::new(interface, bip.port, broadcast);
94+
95+
if let Some(ref fdc) = fd_config {
96+
server_transport.register_as_foreign_device(fdc.clone());
97+
tracing::info!(
98+
"Registered as foreign device with BBMD {}:{}",
99+
fdc.bbmd_ip,
100+
fdc.bbmd_port
101+
);
102+
}
103+
104+
if let Some(bbmd_cfg) = &self.config.bbmd {
105+
if bbmd_cfg.enabled {
106+
let mut bdt_entries = Vec::new();
107+
for entry_str in &bbmd_cfg.bdt {
108+
let addr: std::net::SocketAddrV4 = entry_str.parse().map_err(|e| {
109+
Error::Encoding(format!("invalid BDT entry '{entry_str}': {e}"))
110+
})?;
111+
bdt_entries.push(bacnet_transport::bbmd::BdtEntry {
112+
ip: addr.ip().octets(),
113+
port: addr.port(),
114+
broadcast_mask: [0xff, 0xff, 0xff, 0xff],
115+
});
116+
}
117+
server_transport.enable_bbmd(bdt_entries);
118+
tracing::info!("BBMD enabled with {} BDT entries", bbmd_cfg.bdt.len());
119+
}
120+
}
121+
122+
// Start server with the pre-configured transport.
123+
let server = BACnetServer::generic_builder()
124+
.transport(server_transport)
61125
.database(db)
126+
.vendor_id(self.config.device.vendor_id)
62127
.build()
63128
.await?;
64129

65130
let server_mac = server.local_mac().to_vec();
66131
let db = server.database().clone();
67132

68-
// Start client on an ephemeral port.
69-
let client = BACnetClient::bip_builder()
70-
.interface(interface)
71-
.port(0)
72-
.broadcast_address(broadcast)
133+
// Create client transport (also needs foreign device config for broadcast routing).
134+
let mut client_transport = BipTransport::new(interface, 0, broadcast);
135+
if let Some(ref fdc) = fd_config {
136+
client_transport.register_as_foreign_device(fdc.clone());
137+
}
138+
139+
let client = BACnetClient::generic_builder()
140+
.transport(client_transport)
73141
.apdu_timeout_ms(6000)
74142
.build()
75143
.await?;

crates/bacnet-gateway/src/config.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,8 @@ pub struct ObjectConfig {
191191
pub name: String,
192192
/// Engineering units (optional).
193193
pub units: Option<String>,
194+
/// Number of states for multi-state objects (default: 2).
195+
pub number_of_states: Option<u32>,
194196
}
195197

196198
/// Configuration validation error.

0 commit comments

Comments
 (0)