use std::borrow::Cow;
use std::sync::{Arc, Mutex, PoisonError};
use bacnet_npdu::{Address, GLOBAL_NETWORK, NetworkPriority, Npdu};
use bacnet_transport::port::ReceivedNpdu;
use bacnet_types::error::Error;
use bytes::Bytes;
use tokio::sync::{mpsc, watch};
use tokio::task::JoinHandle;
use tokio::time::{Duration, Instant, timeout_at};
use tracing::debug;
use super::application_replies::Replies;
use super::application_transactions::{Owner, Side, Transactions};
use super::options::sealed::ApduSource;
use crate::layer::ReceivedApdu;
pub(super) const QUEUE: usize = 4;
pub(super) const STALL: Duration = Duration::from_millis(100);
const _: () = assert!(STALL.as_millis() == 100);
#[derive(Default)]
pub(super) struct Consumers {
slots: [Option<mpsc::Sender<ReceivedNpdu>>; 2],
closed: bool,
}
pub(super) fn slot(side: Option<Side>) -> usize {
match side {
None | Some(Side::Client) => 0,
Some(Side::Server) => 1,
}
}
pub(super) struct Feed {
consumers: Arc<watch::Sender<Consumers>>,
source: Mutex<Option<ApduSource>>,
task: Mutex<Option<JoinHandle<()>>>,
pub(super) replies: [Arc<Replies>; 2],
pub(super) transactions: Option<Arc<Transactions>>,
}
impl Feed {
pub(super) fn new(source: ApduSource, split: bool) -> Self {
Self {
consumers: Arc::new(watch::Sender::new(Consumers::default())),
source: Mutex::new(Some(source)),
task: Mutex::new(None),
replies: Default::default(),
transactions: split.then(Arc::default),
}
}
pub(super) fn attach(&self, slot: usize) -> mpsc::Receiver<ReceivedNpdu> {
let (tx, rx) = mpsc::channel(QUEUE);
self.consumers.send_modify(|consumers| {
if !consumers.closed {
consumers.slots[slot] = Some(tx);
}
});
let source = self
.source
.lock()
.unwrap_or_else(PoisonError::into_inner)
.take();
if let Some(source) = source {
let task = tokio::spawn(run(
source,
Arc::clone(&self.consumers),
Router {
replies: self.replies.clone(),
transactions: self.transactions.clone(),
},
));
*self.task.lock().unwrap_or_else(PoisonError::into_inner) = Some(task);
}
rx
}
pub(super) fn detach(&self, slot: usize) {
self.consumers
.send_modify(|consumers| consumers.slots[slot] = None);
}
}
impl Drop for Feed {
fn drop(&mut self) {
if let Some(task) = self
.task
.get_mut()
.unwrap_or_else(PoisonError::into_inner)
.take()
{
task.abort();
}
}
}
async fn unless_changed<F: Future>(
consumers: &mut watch::Receiver<Consumers>,
future: F,
) -> Option<F::Output> {
tokio::select! {
output = future => Some(output),
_ = consumers.changed() => None,
}
}
async fn run(mut apdus: ApduSource, consumers: Arc<watch::Sender<Consumers>>, router: Router) {
let mut changes = consumers.subscribe();
let mut stalled = [false; 2];
loop {
let senders = changes.borrow_and_update().slots.clone();
let live = senders
.each_ref()
.map(|tx| tx.as_ref().filter(|tx| !tx.is_closed()));
if live.iter().all(Option::is_none) {
if changes.changed().await.is_err() {
return;
}
continue;
}
let deadline = Instant::now() + STALL;
let rooms = async {
tokio::join!(
room(live[0], stalled[0], deadline),
room(live[1], stalled[1], deadline)
)
};
let Some(rooms) = unless_changed(&mut changes, rooms).await else {
continue;
};
let mut permits = [None, None];
for (index, room) in <[Room<'_>; 2]>::from(rooms).into_iter().enumerate() {
match room {
Room::Permit(permit) => {
stalled[index] = false;
permits[index] = Some(permit);
}
Room::Stalled => {
if !stalled[index] {
debug!(slot = index, "router application: a consumer stalled");
}
stalled[index] = true;
}
Room::None => {}
}
}
if permits.iter().all(Option::is_none) {
let first = async {
match live {
[Some(a), Some(b)] => tokio::select! {
permit = a.reserve() => (0, permit),
permit = b.reserve() => (1, permit),
},
[Some(a), None] => (0, a.reserve().await),
[None, Some(b)] => (1, b.reserve().await),
[None, None] => unreachable!("one consumer is live"),
}
};
match unless_changed(&mut changes, first).await {
Some((index, Ok(permit))) => {
stalled[index] = false;
permits[index] = Some(permit);
}
_ => continue,
}
}
let apdu = match unless_changed(&mut changes, apdus.recv()).await {
None => continue,
Some(Some(apdu)) => apdu,
Some(None) => break,
};
for (index, permit) in permits.iter_mut().enumerate() {
if permit.is_none() {
*permit = live[index].and_then(|tx| tx.try_reserve().ok());
}
}
let room = permits.each_ref().map(Option::is_some);
for (index, npdu) in router.deliver(apdu, room).into_iter().enumerate() {
let Some(npdu) = npdu else { continue };
match permits[index].take() {
Some(permit) => {
permit.send(npdu);
stalled[index] = false;
}
None => debug!(
slot = index,
"router application: an APDU dropped for a consumer without room"
),
}
}
}
consumers.send_modify(|consumers| {
consumers.slots = [None, None];
consumers.closed = true;
});
}
enum Room<'a> {
Permit(mpsc::Permit<'a, ReceivedNpdu>),
Stalled,
None,
}
async fn room(
tx: Option<&mpsc::Sender<ReceivedNpdu>>,
stalled: bool,
deadline: Instant,
) -> Room<'_> {
let Some(tx) = tx else {
return Room::None;
};
if stalled {
return match tx.try_reserve() {
Ok(permit) => Room::Permit(permit),
Err(mpsc::error::TrySendError::Full(())) => Room::Stalled,
Err(mpsc::error::TrySendError::Closed(())) => Room::None,
};
}
match timeout_at(deadline, tx.reserve()).await {
Ok(Ok(permit)) => Room::Permit(permit),
Ok(Err(_)) => Room::None,
Err(_) => Room::Stalled,
}
}
struct Router {
replies: [Arc<Replies>; 2],
transactions: Option<Arc<Transactions>>,
}
impl Router {
fn deliver(&self, apdu: ReceivedApdu, room: [bool; 2]) -> [Option<ReceivedNpdu>; 2] {
let (network, mac) = match (&apdu.source_network, apdu.ingress_network) {
(Some(source), _) => (source.network, source.mac_address.as_slice()),
(None, Some(network)) => (network, apdu.source_mac.as_slice()),
(None, None) => {
debug!("router application: an APDU with no ingress network");
return [None, None];
}
};
let mut payload = Cow::Borrowed(&apdu.apdu[..]);
let slots = match &self.transactions {
None => [true, false],
Some(transactions) => match classify(&apdu.apdu) {
Route::To(side) => to(side),
Route::Both | Route::ServerAnswers => [true, true],
Route::Answer { invoke_id, ends } => {
match transactions.answer(network, mac, invoke_id, ends) {
Owner::Stack(side, local) => {
if local != invoke_id {
payload.to_mut()[1] = local;
}
to(side)
}
Owner::Unknown => to(Side::Client),
Owner::Ambiguous => {
debug!(
invoke_id,
"router application: an answer that would match a translated request"
);
return [None, None];
}
}
}
Route::Neither => {
debug!("router application: an APDU neither stack takes");
return [None, None];
}
},
};
let npdu = Npdu {
expecting_reply: false,
priority: NetworkPriority::Normal,
destination: apdu.global_broadcast.then_some(Address {
network: GLOBAL_NETWORK,
mac: &[],
}),
source: Some(Address { network, mac }),
hop_count: 255,
message_type: None,
vendor_id: None,
payload: &payload,
payload_offset: 0,
};
let mut buf = vec![0; npdu.encoded_len()];
if let Err(error) = npdu.encode_into(&mut buf) {
debug!(%error, "router application: an APDU with no writable source");
return [None, None];
}
if let Some(response) = apdu.router_response {
if slots == [true, true] && room[1] {
self.replies[1].save(network, mac, &apdu.apdu, response);
self.replies[0].silence(network, mac, &apdu.apdu);
} else {
let slot = if slots == [true, true] {
0
} else {
usize::from(slots[1])
};
self.replies[slot].save(network, mac, &apdu.apdu, response);
}
}
let npdu = Bytes::from(buf);
let up = |npdu: Bytes| ReceivedNpdu {
npdu,
source_mac: apdu.source_mac.clone(),
link_layer_group: apdu.is_group && !apdu.global_broadcast,
data_attributes: apdu.data_attributes.clone(),
provenance: apdu.provenance.clone(),
direct_response: None,
reply_tx: None,
};
slots.map(|wanted| wanted.then(|| up(npdu.clone())))
}
}
fn to(side: Side) -> [bool; 2] {
match side {
Side::Client => [true, false],
Side::Server => [false, true],
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum Route {
To(Side),
Both,
ServerAnswers,
Answer {
invoke_id: u8,
ends: bool,
},
Neither,
}
pub(super) fn classify(apdu: &[u8]) -> Route {
let Some(&first) = apdu.first() else {
return Route::Neither;
};
let server_bit = first & 0x01 != 0;
let answer = |ends| match apdu.get(1) {
Some(&invoke_id) => Route::Answer { invoke_id, ends },
None => Route::Neither,
};
match first >> 4 {
0x0 => {
let service = apdu.get(if first & 0x08 != 0 { 5 } else { 3 });
match service {
Some(1) => Route::To(Side::Client),
Some(2) => Route::ServerAnswers,
_ => Route::To(Side::Server),
}
}
0x1 => Route::Both,
0x2 | 0x5 | 0x6 => answer(true),
0x3 => answer(first & 0x04 == 0),
0x4 if server_bit => answer(false),
0x7 if server_bit => answer(true),
0x4 | 0x7 => Route::To(Side::Server),
_ => Route::Neither,
}
}
pub(super) fn outbound<'a>(
transactions: &Transactions,
side: Side,
to: Address<'_>,
apdu: &'a [u8],
) -> Result<Cow<'a, [u8]>, Error> {
if to.mac.is_empty() {
return Ok(Cow::Borrowed(apdu));
}
let (Some(&first), Some(&id1), Some(&id2)) = (apdu.first(), apdu.get(1), apdu.get(2)) else {
return Ok(Cow::Borrowed(apdu));
};
let server_bit = first & 0x01 != 0;
let (at, local, wire) = match first >> 4 {
0x0 => {
let wire = transactions.request(side, to, id2).ok_or_else(|| {
Error::Transport(std::io::Error::new(
std::io::ErrorKind::WouldBlock,
"every invoke ID to the device is in use by the router's application",
))
})?;
(2, id2, wire)
}
0x4 if !server_bit => (1, id1, transactions.requester(side, to, id1, false)),
0x7 if !server_bit => (1, id1, transactions.requester(side, to, id1, true)),
_ => return Ok(Cow::Borrowed(apdu)),
};
if wire == local {
return Ok(Cow::Borrowed(apdu));
}
let mut apdu = apdu.to_vec();
apdu[at] = wire;
Ok(Cow::Owned(apdu))
}
#[cfg(test)]
#[path = "application_feed_tests.rs"]
mod tests;