Skip to content

Commit 8a67099

Browse files
committed
Large spec review around objects, services, client/server.
1 parent f759b9d commit 8a67099

File tree

23 files changed

+706
-152
lines changed

23 files changed

+706
-152
lines changed

crates/bacnet-client/src/client.rs

Lines changed: 118 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ use tokio::time::{timeout, Duration};
1515
use tracing::{debug, warn};
1616

1717
use bacnet_encoding::apdu::{
18-
self, encode_apdu, Apdu, ConfirmedRequest as ConfirmedRequestPdu, SegmentAck as SegmentAckPdu,
19-
SimpleAck,
18+
self, encode_apdu, AbortPdu, Apdu, ConfirmedRequest as ConfirmedRequestPdu,
19+
SegmentAck as SegmentAckPdu, SimpleAck,
2020
};
2121
use bacnet_encoding::npdu::NpduAddress;
2222
use bacnet_network::layer::NetworkLayer;
@@ -233,6 +233,10 @@ struct SegmentedReceiveState {
233233
expected_next_seq: u8,
234234
/// Timestamp of last received segment (for reaping stale sessions).
235235
last_activity: Instant,
236+
/// Window position counter for per-window SegmentAck (Clause 5.2.2).
237+
window_position: u8,
238+
/// Proposed window size from the server.
239+
proposed_window_size: u8,
236240
}
237241

238242
/// Timeout for idle segmented reassembly sessions.
@@ -536,15 +540,47 @@ impl<T: TransportPort + 'static> BACnetClient<T> {
536540
let seg_ack_senders: Arc<Mutex<HashMap<SegKey, mpsc::Sender<SegmentAckPdu>>>> =
537541
Arc::new(Mutex::new(HashMap::new()));
538542
let seg_ack_senders_dispatch = Arc::clone(&seg_ack_senders);
543+
let segmented_response_accepted = config.segmented_response_accepted;
539544

540545
let dispatch_task = tokio::spawn(async move {
541546
let mut seg_state: HashMap<SegKey, SegmentedReceiveState> = HashMap::new();
547+
let mut last_device_purge = Instant::now();
548+
const DEVICE_PURGE_INTERVAL: Duration = Duration::from_secs(300);
549+
const DEVICE_MAX_AGE: Duration = Duration::from_secs(600);
542550

543551
while let Some(received) = apdu_rx.recv().await {
544552
let now = Instant::now();
545-
seg_state.retain(|_key, state| {
546-
now.duration_since(state.last_activity) < SEG_RECEIVER_TIMEOUT
547-
});
553+
554+
// Periodically purge stale device table entries
555+
if now.duration_since(last_device_purge) >= DEVICE_PURGE_INTERVAL {
556+
device_table_dispatch
557+
.lock()
558+
.await
559+
.purge_stale(DEVICE_MAX_AGE);
560+
last_device_purge = now;
561+
}
562+
// Reap stale segmented sessions and send Abort to the server
563+
let stale_keys: Vec<SegKey> = seg_state
564+
.iter()
565+
.filter(|(_, state)| {
566+
now.duration_since(state.last_activity) >= SEG_RECEIVER_TIMEOUT
567+
})
568+
.map(|(key, _)| key.clone())
569+
.collect();
570+
for key in &stale_keys {
571+
if let Some(_state) = seg_state.remove(key) {
572+
let abort = Apdu::Abort(AbortPdu {
573+
sent_by_server: false,
574+
invoke_id: key.1,
575+
abort_reason: bacnet_types::enums::AbortReason::TSM_TIMEOUT,
576+
});
577+
let mut buf = BytesMut::with_capacity(4);
578+
encode_apdu(&mut buf, &abort);
579+
let _ = network_dispatch
580+
.send_apdu(&buf, &key.0, false, NetworkPriority::NORMAL)
581+
.await;
582+
}
583+
}
548584

549585
match apdu::decode_apdu(received.apdu.clone()) {
550586
Ok(decoded) => {
@@ -558,6 +594,7 @@ impl<T: TransportPort + 'static> BACnetClient<T> {
558594
&received.source_mac,
559595
&received.source_network,
560596
decoded,
597+
segmented_response_accepted,
561598
)
562599
.await;
563600
}
@@ -592,6 +629,7 @@ impl<T: TransportPort + 'static> BACnetClient<T> {
592629
source_mac: &[u8],
593630
source_network: &Option<NpduAddress>,
594631
apdu: Apdu,
632+
segmented_response_accepted: bool,
595633
) {
596634
match apdu {
597635
Apdu::SimpleAck(ack) => {
@@ -601,8 +639,15 @@ impl<T: TransportPort + 'static> BACnetClient<T> {
601639
}
602640
Apdu::ComplexAck(ack) => {
603641
if ack.segmented {
604-
Self::handle_segmented_complex_ack(tsm, network, seg_state, source_mac, ack)
605-
.await;
642+
Self::handle_segmented_complex_ack(
643+
tsm,
644+
network,
645+
seg_state,
646+
source_mac,
647+
ack,
648+
segmented_response_accepted,
649+
)
650+
.await;
606651
} else {
607652
debug!(invoke_id = ack.invoke_id, "Received ComplexAck");
608653
let mut tsm = tsm.lock().await;
@@ -760,6 +805,7 @@ impl<T: TransportPort + 'static> BACnetClient<T> {
760805
seg_state: &mut HashMap<SegKey, SegmentedReceiveState>,
761806
source_mac: &[u8],
762807
ack: bacnet_encoding::apdu::ComplexAck,
808+
segmented_response_accepted: bool,
763809
) {
764810
let seq = ack.sequence_number.unwrap_or(0);
765811
let key = (MacAddr::from_slice(source_mac), ack.invoke_id);
@@ -771,6 +817,21 @@ impl<T: TransportPort + 'static> BACnetClient<T> {
771817
"Received segmented ComplexAck"
772818
);
773819

820+
// If client doesn't support segmented reception, send Abort per Clause 5.4.4.2
821+
if !segmented_response_accepted {
822+
let abort = Apdu::Abort(AbortPdu {
823+
sent_by_server: false,
824+
invoke_id: ack.invoke_id,
825+
abort_reason: bacnet_types::enums::AbortReason::SEGMENTATION_NOT_SUPPORTED,
826+
});
827+
let mut buf = BytesMut::with_capacity(4);
828+
encode_apdu(&mut buf, &abort);
829+
let _ = network
830+
.send_apdu(&buf, source_mac, false, NetworkPriority::NORMAL)
831+
.await;
832+
return;
833+
}
834+
774835
const MAX_CONCURRENT_SEG_SESSIONS: usize = 64;
775836
if !seg_state.contains_key(&key) && seg_state.len() >= MAX_CONCURRENT_SEG_SESSIONS {
776837
warn!(
@@ -781,37 +842,55 @@ impl<T: TransportPort + 'static> BACnetClient<T> {
781842
return;
782843
}
783844

845+
let proposed_ws = ack.proposed_window_size.unwrap_or(1);
784846
let state = seg_state
785847
.entry(key.clone())
786848
.or_insert_with(|| SegmentedReceiveState {
787849
receiver: SegmentReceiver::new(),
788850
expected_next_seq: 0,
789851
last_activity: Instant::now(),
852+
window_position: 0,
853+
proposed_window_size: proposed_ws,
790854
});
791855

792856
state.last_activity = Instant::now();
793857

794858
if seq != state.expected_next_seq {
795-
warn!(
796-
invoke_id = ack.invoke_id,
797-
expected = state.expected_next_seq,
798-
received = seq,
799-
"Segment gap detected, sending negative SegmentAck"
800-
);
859+
// Check for duplicate (already received) vs true gap
860+
if seq < state.expected_next_seq {
861+
// Duplicate segment — discard silently and ack
862+
debug!(
863+
invoke_id = ack.invoke_id,
864+
seq, "Discarding duplicate segment"
865+
);
866+
} else {
867+
// True gap — send negative SegmentAck with last correctly received seq
868+
warn!(
869+
invoke_id = ack.invoke_id,
870+
expected = state.expected_next_seq,
871+
received = seq,
872+
"Segment gap detected, sending negative SegmentAck"
873+
);
874+
}
801875
let neg_ack = Apdu::SegmentAck(SegmentAckPdu {
802-
negative_ack: true,
876+
negative_ack: seq >= state.expected_next_seq,
803877
sent_by_server: false,
804878
invoke_id: ack.invoke_id,
805-
sequence_number: state.expected_next_seq,
806-
actual_window_size: ack.proposed_window_size.unwrap_or(1),
879+
// Spec: sequence_number = last correctly received sequence number
880+
sequence_number: if state.expected_next_seq > 0 {
881+
state.expected_next_seq.wrapping_sub(1)
882+
} else {
883+
0
884+
},
885+
actual_window_size: proposed_ws,
807886
});
808887
let mut buf = BytesMut::with_capacity(4);
809888
encode_apdu(&mut buf, &neg_ack);
810889
if let Err(e) = network
811890
.send_apdu(&buf, source_mac, false, NetworkPriority::NORMAL)
812891
.await
813892
{
814-
warn!(error = %e, "Failed to send negative SegmentAck");
893+
warn!(error = %e, "Failed to send SegmentAck");
815894
}
816895
return;
817896
}
@@ -821,21 +900,28 @@ impl<T: TransportPort + 'static> BACnetClient<T> {
821900
return;
822901
}
823902
state.expected_next_seq = seq.wrapping_add(1);
903+
state.window_position += 1;
824904

825-
let seg_ack = Apdu::SegmentAck(SegmentAckPdu {
826-
negative_ack: false,
827-
sent_by_server: false,
828-
invoke_id: ack.invoke_id,
829-
sequence_number: seq,
830-
actual_window_size: ack.proposed_window_size.unwrap_or(1),
831-
});
832-
let mut buf = BytesMut::with_capacity(4);
833-
encode_apdu(&mut buf, &seg_ack);
834-
if let Err(e) = network
835-
.send_apdu(&buf, source_mac, false, NetworkPriority::NORMAL)
836-
.await
837-
{
838-
warn!(error = %e, "Failed to send SegmentAck");
905+
// Per-window SegmentAck: only ack at window boundary or final segment (Clause 5.2.2)
906+
let should_ack = !ack.more_follows || state.window_position >= state.proposed_window_size;
907+
908+
if should_ack {
909+
state.window_position = 0;
910+
let seg_ack = Apdu::SegmentAck(SegmentAckPdu {
911+
negative_ack: false,
912+
sent_by_server: false,
913+
invoke_id: ack.invoke_id,
914+
sequence_number: seq,
915+
actual_window_size: proposed_ws,
916+
});
917+
let mut buf = BytesMut::with_capacity(4);
918+
encode_apdu(&mut buf, &seg_ack);
919+
if let Err(e) = network
920+
.send_apdu(&buf, source_mac, false, NetworkPriority::NORMAL)
921+
.await
922+
{
923+
warn!(error = %e, "Failed to send SegmentAck");
924+
}
839925
}
840926

841927
if !ack.more_follows {
@@ -1100,6 +1186,7 @@ impl<T: TransportPort + 'static> BACnetClient<T> {
11001186
self.seg_ack_senders.lock().await.insert(key, seg_ack_tx);
11011187
}
11021188

1189+
// Tseg: use APDU timeout for now (configurable via apdu_timeout_ms)
11031190
let timeout_duration = Duration::from_millis(self.config.apdu_timeout_ms);
11041191
let max_ack_retries = self.config.apdu_retries;
11051192
let mut window_size = self.config.proposed_window_size.max(1) as usize;

crates/bacnet-encoding/src/apdu.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ fn decode_max_apdu(value: u8) -> u16 {
8181
if idx < MAX_APDU_DECODE.len() {
8282
MAX_APDU_DECODE[idx]
8383
} else {
84+
tracing::warn!(
85+
value = idx,
86+
"Reserved max-APDU-length field value, defaulting to 1476"
87+
);
8488
1476
8589
}
8690
}
@@ -468,12 +472,19 @@ fn decode_segment_ack(data: Bytes) -> Result<SegmentAck, Error> {
468472
}
469473

470474
let byte0 = data[0];
475+
let raw_window = data[3];
476+
if raw_window == 0 || raw_window > 127 {
477+
tracing::warn!(
478+
actual_window_size = raw_window,
479+
"SegmentAck window size out of range (1-127), clamping"
480+
);
481+
}
471482
Ok(SegmentAck {
472483
negative_ack: byte0 & 0x02 != 0,
473484
sent_by_server: byte0 & 0x01 != 0,
474485
invoke_id: data[1],
475486
sequence_number: data[2],
476-
actual_window_size: data[3],
487+
actual_window_size: raw_window.clamp(1, 127),
477488
})
478489
}
479490

crates/bacnet-integration-tests/tests/server.rs

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -929,23 +929,27 @@ async fn server_handles_segmented_request() {
929929

930930
/// DCC DISABLE (deprecated in 2020 spec) is rejected with SERVICE_REQUEST_DENIED.
931931
#[tokio::test]
932-
async fn dcc_disable_rejected_per_2020_spec() {
932+
async fn dcc_disable_sets_comm_state() {
933933
use bacnet_types::enums::EnableDisable;
934934

935935
let mut server = make_server().await;
936936
let mut client = make_client().await;
937937
let server_mac = server.local_mac().to_vec();
938938

939-
// Clause 16.1.1.3.1: deprecated DISABLE shall be rejected
939+
// Clause 16.1: DISABLE sets comm_state to 1 (per spec, all three values are supported)
940940
let result = client
941941
.device_communication_control(&server_mac, EnableDisable::DISABLE, None, None)
942942
.await;
943-
assert!(
944-
result.is_err(),
945-
"DCC DISABLE should be rejected per 2020 spec"
946-
);
943+
assert!(result.is_ok(), "DCC DISABLE should succeed per Clause 16.1");
944+
945+
// Server should be in DISABLE state (1)
946+
assert_eq!(server.comm_state(), 1);
947947

948-
// Server should remain in ENABLE state
948+
// Re-enable should work (DCC is allowed even when disabled)
949+
let result = client
950+
.device_communication_control(&server_mac, EnableDisable::ENABLE, None, None)
951+
.await;
952+
assert!(result.is_ok(), "DCC ENABLE should succeed");
949953
assert_eq!(server.comm_state(), 0);
950954

951955
client.stop().await.unwrap();

0 commit comments

Comments
 (0)