use std::sync::Arc;
use std::sync::atomic::{AtomicU8, Ordering};
use bacnet_client::EndpointRequester;
use bacnet_client::client::ClientConfig;
use bacnet_encoding::apdu::Apdu;
use bacnet_endpoint_core::coordinator::{
AdmissionOutcome, CoordinatorError, OutboundTransactionCoordinator,
};
use bacnet_endpoint_core::endpoint_ingress::{
ClassifierExit, EndpointIngress, PolicyOutcome, PolicyReason,
};
use bacnet_network::layer::ReceivedApdu;
use bacnet_network::network_number::LocalNetworkNumber;
use bacnet_objects::database::ObjectDatabase;
use bacnet_objects::traits::BACnetObject;
use bacnet_transport::port::TransportPort;
use bacnet_types::enums::{ObjectType, PropertyIdentifier};
use bacnet_types::error::Error;
use bacnet_types::primitives::{ObjectIdentifier, PropertyValue};
use tokio::sync::{Mutex, RwLock, mpsc, oneshot};
use tokio::task::JoinHandle;
#[path = "source_profile.rs"]
mod source_profile;
#[path = "source_reporter.rs"]
pub(crate) mod source_reporter;
#[path = "session_start.rs"]
mod startup;
#[path = "device_writes.rs"]
mod device_writes;
pub use device_writes::{RESPONDER_OPT_IN_SERVICES, RESPONDER_SERVICES, ResponderOptIn};
#[path = "registered_port.rs"]
mod registered_port;
use crate::roles::{
ClientRoleHandle, ServerRoleHandle, SessionToken, admit_once, decode_terminal,
inbound_canonical_peer, is_requester_lease,
};
use bacnet_server::server::{
__endpoint_EndpointResponder as EndpointResponder,
__endpoint_NotificationTransactions as NotificationTransactions,
__endpoint_settle_durable_writes as settle_durable_writes, drop_database_off_runtime,
};
#[derive(Clone, Copy, PartialEq, Eq)]
enum Lifecycle {
Ready = 0,
Running = 1,
Stopped = 2,
Stopping = 3,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SessionRole {
ClientOnly,
ServerOnly,
Both,
}
#[derive(Clone, Debug)]
pub struct SessionConfig {
pub queue_capacity: usize,
pub apdu_timeout_ms: u64,
pub apdu_retries: u8,
pub max_apdu_length: u16,
pub read_work_limit: usize,
pub min_request_interval_ms: u64,
}
impl Default for SessionConfig {
fn default() -> Self {
Self {
queue_capacity: 16,
apdu_timeout_ms: 1_000,
apdu_retries: 0,
max_apdu_length: 480,
read_work_limit: bacnet_server::server::ReadPropertyMultipleBudget::default()
.max_result_elements,
min_request_interval_ms: 0,
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct PolicyCountersSnapshot {
pub ingress_policy: u64,
pub no_server_role: u64,
pub no_client_role: u64,
pub unclaimed_terminal: u64,
pub responder_declined: u64,
}
struct PolicyCounters {
ingress_policy: u64,
no_server_role: u64,
no_client_role: u64,
unclaimed_terminal: u64,
responder_declined: u64,
}
struct SessionShared {
token: Arc<SessionToken>,
counters: Mutex<PolicyCounters>,
}
pub struct EndpointSession<T: TransportPort + 'static = bacnet_transport::any::AnyTransport> {
shared: Arc<SessionShared>,
coordinator: Arc<OutboundTransactionCoordinator>,
ingress: Option<EndpointIngress<T>>,
requester: Option<EndpointRequester>,
responder: Option<Arc<EndpointResponder>>,
notifications: Option<Arc<NotificationTransactions>>,
client_handle: Option<ClientRoleHandle>,
server_handle: Option<ServerRoleHandle>,
dispatch_task: Option<JoinHandle<SessionExit>>,
network_number_task: Option<JoinHandle<()>>,
cancel_tx: Option<oneshot::Sender<()>>,
lifecycle: AtomicU8,
role: SessionRole,
#[allow(dead_code)]
config: SessionConfig,
client_config: ClientConfig,
database: Option<Arc<RwLock<ObjectDatabase>>>,
source_audit_reporter: Option<ObjectIdentifier>,
source_audit: Option<Arc<crate::source_audit::SourceAudit>>,
pub(crate) source_audit_bindings: Vec<(ObjectIdentifier, std::net::SocketAddrV4)>,
source_recipient: Option<Arc<crate::source_audit::recipient::SourceRecipient>>,
stop_exit: Option<Result<SessionExit, Error>>,
durable_settlement: Option<JoinHandle<()>>,
ingress_stopped: bool,
identity: Option<crate::identity::DeviceIdentity>,
local_receive_capacity: u16,
bip_local_address: Option<std::net::SocketAddrV4>,
registered_network_port: Option<ObjectIdentifier>,
registered_port_lease: std::sync::Weak<()>,
device_write_authorizer: Option<bacnet_server::mutation::DeviceWriteAuthorizer>,
pub(crate) reinitialize: Option<bacnet_server::server::ReinitializeHandler>,
pub(crate) reinit_password: Option<String>,
egress: Option<bacnet_endpoint_core::endpoint_ingress::EndpointEgress>,
pub(crate) bip_status: Option<bacnet_transport::bip::BvlcClientStatus>,
pub(crate) mstp_diagnostics: Option<bacnet_transport::mstp::MstpDiagnostics>,
}
#[derive(Debug)]
pub enum SessionExit {
Cancelled,
Ingress(ClassifierExit),
ReceiversClosed,
}
impl<T: TransportPort + 'static> EndpointSession<T> {
pub fn new(transport: T, role: SessionRole, config: SessionConfig) -> Result<Self, Error> {
if config.queue_capacity == 0 {
return Err(Error::Encoding(
"endpoint session queue capacity must be greater than zero".into(),
));
}
if config.read_work_limit == 0 {
return Err(Error::Encoding(
"endpoint session read work limit must be greater than zero".into(),
));
}
let interval = config.min_request_interval_ms;
if interval > bacnet_client::client::MAX_MIN_REQUEST_INTERVAL_MS {
return Err(Error::Encoding(format!(
"endpoint session min-request-interval {interval} ms is more than {} ms",
bacnet_client::client::MAX_MIN_REQUEST_INTERVAL_MS
)));
}
let coordinator = Arc::new(OutboundTransactionCoordinator::new());
let token = SessionToken::new(Arc::clone(&coordinator));
let client_config = ClientConfig {
apdu_timeout_ms: config.apdu_timeout_ms,
apdu_retries: config.apdu_retries,
max_apdu_length: config.max_apdu_length,
min_request_interval_ms: interval,
..ClientConfig::default()
};
let local_receive_capacity = transport.local_receive_apdu_capacity();
Ok(Self {
local_receive_capacity,
shared: Arc::new(SessionShared {
token,
counters: Mutex::new(PolicyCounters {
ingress_policy: 0,
no_server_role: 0,
no_client_role: 0,
unclaimed_terminal: 0,
responder_declined: 0,
}),
}),
coordinator,
ingress: Some(EndpointIngress::new(transport, config.queue_capacity)),
requester: None,
responder: None,
notifications: None,
client_handle: None,
server_handle: None,
dispatch_task: None,
network_number_task: None,
cancel_tx: None,
lifecycle: AtomicU8::new(Lifecycle::Ready as u8),
role,
config,
client_config,
database: None,
source_audit_reporter: None,
source_audit: None,
source_audit_bindings: Vec::new(),
source_recipient: None,
stop_exit: None,
durable_settlement: None,
ingress_stopped: false,
identity: None,
bip_local_address: None,
registered_network_port: None,
registered_port_lease: std::sync::Weak::new(),
device_write_authorizer: None,
reinitialize: None,
reinit_password: None,
egress: None,
bip_status: None,
mstp_diagnostics: None,
})
}
pub fn with_database(mut self, db: ObjectDatabase) -> Self {
self.assert_configurable();
self.database = Some(Arc::new(RwLock::new(db)));
self
}
pub fn with_source_audit_reporter(mut self, reporter: ObjectIdentifier) -> Self {
self.assert_configurable();
self.source_audit_reporter = Some(reporter);
self
}
pub fn device(mut self, identity: crate::identity::DeviceIdentity) -> Self {
self.assert_configurable();
self.client_config.max_apdu_length = identity.max_apdu_length();
self.config.max_apdu_length = identity.max_apdu_length();
self.identity = Some(identity);
self
}
fn assert_configurable(&self) {
assert_eq!(
self.lifecycle.load(Ordering::Acquire),
Lifecycle::Ready as u8,
"endpoint configuration must precede startup"
);
}
pub async fn start(&mut self) -> Result<(), Error> {
if self.lifecycle.load(Ordering::Acquire) != Lifecycle::Ready as u8 {
return Err(Error::Encoding(
"endpoint session cannot be started more than once".into(),
));
}
let (device_target, source_routes) = self.prepare_start().await?;
if self.lifecycle.compare_exchange(
Lifecycle::Ready as u8,
Lifecycle::Running as u8,
Ordering::AcqRel,
Ordering::Acquire,
) != Ok(Lifecycle::Ready as u8)
{
return Err(Error::Encoding(
"endpoint session cannot be started more than once".into(),
));
}
let ingress = self
.ingress
.as_mut()
.ok_or_else(|| Error::Encoding("endpoint session ingress owner is missing".into()))?;
let receivers = match ingress.start().await {
Ok(receivers) => receivers,
Err(error) => {
let _ = self.stop().await;
return Err(error);
}
};
if let Err(error) = self
.publish_registered_port(
(receivers.bip_port.clone(), receivers.bip6_port.clone()),
receivers.egress.local_network_number(),
)
.await
{
let _ = self.stop().await;
return Err(error);
}
let bip_local_address = receivers.bip_local_address;
if let Err(error) = self.start_roles(receivers, source_routes, device_target) {
let _ = self.stop().await;
return Err(error);
}
self.bip_local_address = bip_local_address;
Ok(())
}
pub async fn stop(&mut self) -> Result<SessionExit, Error> {
let state = self.lifecycle.load(Ordering::Acquire);
if state != Lifecycle::Running as u8 && state != Lifecycle::Stopping as u8 {
return Err(Error::Encoding("endpoint session is not running".into()));
}
self.lifecycle
.store(Lifecycle::Stopping as u8, Ordering::Release);
self.shared.token.shutdown();
if let Some(task) = self.network_number_task.as_mut() {
task.abort();
let _ = task.await;
}
self.network_number_task = None;
if let Some(source) = &self.source_audit {
source.close();
}
if let Some(requester) = &self.requester {
requester.close();
}
if let Some(responder) = &self.responder {
responder.close();
}
if let Some(notifications) = &self.notifications {
notifications.close();
}
if let Some(cancel) = self.cancel_tx.take() {
let _ = cancel.send(());
}
if self.stop_exit.is_none() {
self.stop_exit = Some(match self.dispatch_task.as_mut() {
Some(task) => task.await.map_err(|error| {
Error::Encoding(format!("endpoint session dispatch failed: {error}"))
}),
None => Ok(SessionExit::ReceiversClosed),
});
self.dispatch_task.take();
}
if let Some(notifications) = &self.notifications {
while let Some(result) = notifications.join_next().await {
NotificationTransactions::observe(Some(result));
}
}
if !self.ingress_stopped {
if let Some(ingress) = self.ingress.as_mut()
&& let Err(error) = ingress.stop().await
&& self.stop_exit.as_ref().is_some_and(Result::is_ok)
{
self.stop_exit = Some(Err(error));
}
self.ingress_stopped = true;
}
if let Some(runtime) = &self.source_recipient {
runtime.uninstall(
&mut *self
.database
.as_ref()
.expect("source database")
.write()
.await,
);
}
self.source_recipient.take();
self.source_audit.take();
self.notifications.take();
self.requester.take();
self.responder.take();
self.client_handle = None;
self.server_handle = None;
if self.durable_settlement.is_none()
&& let Some(db) = &self.database
{
self.durable_settlement = Some(tokio::spawn(settle_durable_writes(Arc::clone(db))));
}
if let Some(task) = self.durable_settlement.as_mut() {
let result = task.await;
self.durable_settlement = None;
result.map_err(|error| {
Error::Encoding(format!("endpoint durable settlement failed: {error}"))
})?;
}
self.lifecycle
.store(Lifecycle::Stopped as u8, Ordering::Release);
self.stop_exit.take().expect("joined dispatch")
}
pub fn client(&self) -> Option<&ClientRoleHandle> {
self.client_handle.as_ref()
}
pub fn server(&self) -> Option<&ServerRoleHandle> {
self.server_handle.as_ref()
}
pub fn cloned_client_handle(&self) -> Option<ClientRoleHandle> {
self.client_handle.clone()
}
pub fn cloned_server_handle(&self) -> Option<ServerRoleHandle> {
self.server_handle.clone()
}
pub async fn policy_counters(&self) -> PolicyCountersSnapshot {
let counters = self.shared.counters.lock().await;
PolicyCountersSnapshot {
ingress_policy: counters.ingress_policy,
no_server_role: counters.no_server_role,
no_client_role: counters.no_client_role,
unclaimed_terminal: counters.unclaimed_terminal,
responder_declined: counters.responder_declined,
}
}
pub fn active_leases(&self) -> usize {
self.coordinator.active_count().unwrap_or(usize::MAX)
}
#[doc(hidden)]
pub fn coordinator(&self) -> Arc<OutboundTransactionCoordinator> {
Arc::clone(&self.coordinator)
}
pub fn identity(&self) -> Option<&crate::identity::DeviceIdentity> {
self.identity.as_ref()
}
pub fn bip_local_address(&self) -> Option<std::net::SocketAddrV4> {
(self.is_running() && self.egress.as_ref().is_some_and(|egress| egress.is_open()))
.then_some(self.bip_local_address)
.flatten()
}
pub fn foreign_registration(
&self,
) -> Option<bacnet_transport::bip::ForeignRegistrationSnapshot> {
let status = self.bip_status.as_ref()?;
Some(status.snapshot().foreign_registration)
}
pub fn mstp_diagnostics(&self) -> Option<bacnet_transport::mstp::MstpDiagnostics> {
self.mstp_diagnostics.clone()
}
pub fn session_role(&self) -> SessionRole {
self.role
}
pub fn is_running(&self) -> bool {
self.lifecycle.load(Ordering::Acquire) == Lifecycle::Running as u8
}
pub async fn broadcast_i_am(&self) -> Result<(), Error> {
let identity = self
.identity
.as_ref()
.ok_or_else(|| Error::Encoding("endpoint I-Am requires a composed identity".into()))?;
let egress = self
.egress
.as_ref()
.ok_or_else(|| Error::Encoding("endpoint session is not running".into()))?;
if !self.is_running() {
return Err(Error::Encoding("endpoint session is not running".into()));
}
let database = self
.database
.as_ref()
.ok_or_else(|| Error::Encoding("endpoint I-Am has no Device database".into()))?;
let database = database.read().await;
if !database
.get(&identity.device_oid())
.is_some_and(|object| object.object_identifier() == identity.device_oid())
{
return Err(Error::Encoding("endpoint I-Am Device is absent".into()));
}
let apdu = crate::identity::encode_iam_apdu(identity)?;
egress
.send_apdu(
apdu,
bacnet_endpoint_core::endpoint_ingress::EndpointApduDestination::LocalBroadcast,
false,
bacnet_npdu::NetworkPriority::Normal,
Vec::new(),
)
.await
}
}
#[cfg(test)]
#[path = "source_reporter_tests.rs"]
mod source_reporter_tests;
#[cfg(test)]
#[path = "device_write_tests.rs"]
mod device_write_tests;
impl<T: TransportPort + 'static> Drop for EndpointSession<T> {
fn drop(&mut self) {
self.shared.token.shutdown();
if let Some(task) = &self.network_number_task {
task.abort();
}
if let Some(source) = self.source_audit.take() {
source.close();
}
if let Some(requester) = self.requester.take() {
requester.close();
}
if let Some(responder) = self.responder.take() {
responder.close();
}
if let Some(notifications) = self.notifications.take() {
notifications.close();
}
if let Some(cancel) = self.cancel_tx.take() {
let _ = cancel.send(());
}
if let Some(task) = self.dispatch_task.take() {
task.abort();
}
if let Some(task) = self.durable_settlement.take() {
task.abort();
}
if let Some(db) = self.database.take() {
drop(drop_database_off_runtime(db));
}
}
}
struct DispatchParts {
inbound: mpsc::Receiver<ReceivedApdu>,
terminal: mpsc::Receiver<ReceivedApdu>,
policy: mpsc::Receiver<PolicyOutcome>,
requester: Option<EndpointRequester>,
responder: Option<Arc<EndpointResponder>>,
notifications: Option<Arc<NotificationTransactions>>,
coordinator: Arc<OutboundTransactionCoordinator>,
shared: Arc<SessionShared>,
local_network: LocalNetworkNumber,
}
#[path = "session_dispatch.rs"]
mod dispatch;
use dispatch::{DispatchEvent, next_event};
async fn dispatch_loop(mut parts: DispatchParts, mut cancel: oneshot::Receiver<()>) -> SessionExit {
let mut next = 0;
loop {
let event = tokio::select! {
biased;
_ = &mut cancel => return SessionExit::Cancelled,
event = next_event(&mut parts, &mut next) => event,
};
match event {
DispatchEvent::Worker(joined) => NotificationTransactions::observe(Some(joined)),
DispatchEvent::Inbound(received) => handle_inbound(&mut parts, received).await,
DispatchEvent::Terminal(received) => handle_terminal(&mut parts, received).await,
DispatchEvent::Policy(outcome) => handle_ingress_policy(&parts.shared, outcome).await,
DispatchEvent::Closed => return SessionExit::ReceiversClosed,
}
}
}
async fn handle_inbound(parts: &mut DispatchParts, received: ReceivedApdu) {
let _peer = inbound_canonical_peer(&received, parts.local_network.get());
let _link_group = received.link_layer_group;
let _is_group = received.is_group;
let _attributes = received.data_attributes.clone();
let _provenance = &received.provenance;
let _ingress = received.ingress_network;
let Some(responder) = parts.responder.as_ref() else {
let mut counters = parts.shared.counters.lock().await;
counters.no_server_role += 1;
if let Some(reply_tx) = received.reply_tx {
drop(reply_tx);
}
return;
};
let mut received = received;
if !received.provenance.is_direct_peer()
&& received.direct_response.is_none()
&& received.reply_tx.is_some()
&& parts.shared.token.take_suspend()
{
let _ = received.reply_tx.take();
}
match responder.handle(received).await {
Ok(true) | Ok(false) => {}
Err(_) => {
let mut counters = parts.shared.counters.lock().await;
counters.responder_declined += 1;
}
}
}
async fn handle_terminal(parts: &mut DispatchParts, received: ReceivedApdu) {
let Some(apdu) = decode_terminal(&received) else {
let mut counters = parts.shared.counters.lock().await;
counters.unclaimed_terminal += 1;
if let Some(reply_tx) = received.reply_tx {
drop(reply_tx);
}
return;
};
if matches!(
apdu,
Apdu::ConfirmedRequest(_) | Apdu::UnconfirmedRequest(_)
) {
let mut counters = parts.shared.counters.lock().await;
counters.unclaimed_terminal += 1;
if let Some(reply_tx) = received.reply_tx {
drop(reply_tx);
}
return;
}
let outcome = admit_once(
&parts.coordinator,
&received,
parts.local_network.get(),
&apdu,
);
let admission = match outcome {
Ok(AdmissionOutcome::Admitted(admission)) => admission,
Ok(_) => {
let mut counters = parts.shared.counters.lock().await;
counters.unclaimed_terminal += 1;
if let Some(reply_tx) = received.reply_tx {
drop(reply_tx);
}
return;
}
Err(CoordinatorError::StatePoisoned) => {
let mut counters = parts.shared.counters.lock().await;
counters.unclaimed_terminal += 1;
if let Some(reply_tx) = received.reply_tx {
drop(reply_tx);
}
return;
}
};
if is_requester_lease(&admission) {
let Some(requester) = parts.requester.as_ref() else {
let _ = parts.coordinator.release(admission.token());
let mut counters = parts.shared.counters.lock().await;
counters.no_client_role += 1;
if let Some(reply_tx) = received.reply_tx {
drop(reply_tx);
}
return;
};
let _ = requester
.complete_pre_admitted(admission, apdu, received)
.await;
} else {
let Some(notifications) = parts.notifications.as_ref() else {
let _ = parts.coordinator.release(admission.token());
let mut counters = parts.shared.counters.lock().await;
counters.no_client_role += 1;
if let Some(reply_tx) = received.reply_tx {
drop(reply_tx);
}
return;
};
let _ = notifications.complete_pre_admitted(admission, &apdu);
if let Some(reply_tx) = received.reply_tx {
drop(reply_tx);
}
}
}
async fn handle_ingress_policy(shared: &Arc<SessionShared>, outcome: PolicyOutcome) {
let mut counters = shared.counters.lock().await;
counters.ingress_policy += 1;
let _ = outcome.reason;
if let Some(reply_tx) = outcome.received.reply_tx {
drop(reply_tx);
}
let _ = PolicyReason::MalformedApdu;
}
#[cfg(test)]
#[path = "source_read_tests.rs"]
mod source_read_tests;
#[cfg(test)]
#[path = "registered_port_bip6_tests.rs"]
mod registered_port_bip6_tests;
#[cfg(test)]
#[path = "registered_port_lifetime_tests.rs"]
mod registered_port_lifetime_tests;
#[cfg(test)]
#[path = "registered_port_wire_tests.rs"]
mod registered_port_wire_tests;
#[cfg(test)]
#[path = "network_number_tests.rs"]
mod network_number_tests;
#[cfg(test)]
#[path = "../tests/support/port_retry.rs"]
mod port_retry;
#[cfg(test)]
#[path = "local_network_tests.rs"]
mod local_network_tests;
#[cfg(test)]
#[path = "read_work_limit_tests.rs"]
mod read_work_limit_tests;
#[cfg(test)]
#[path = "client_pacing_tests.rs"]
mod client_pacing_tests;
#[path = "device_identity.rs"]
mod device_identity;