pub(crate) mod limits;
pub(crate) mod names;
pub(crate) mod read;
pub(crate) mod view;
use std::collections::{HashMap, HashSet};
use std::future::Future;
use std::pin::Pin;
use std::sync::{Arc, Mutex, PoisonError};
use bacnet_client::client::BACnetClient;
use bacnet_transport::port::TransportPort;
use bacnet_types::enums::PropertyIdentifier;
use bacnet_types::primitives::ObjectIdentifier;
use futures_util::stream::{FuturesUnordered, StreamExt};
use tokio::sync::mpsc;
use self::limits::{Job, RateCap, Scheduler, Started, Task};
use self::names::NameQueue;
use self::read::{Halt, Reader, Wanted};
use super::EventSink;
use crate::tui::message::{Command, WorkerEvent};
use crate::tui::point::{BrowseRead, BrowseResult, NameAsk, ReqId};
pub(crate) const DEFAULT_MAX_RATE: u32 = 20;
const PROGRESS_EVERY: u32 = 25;
const CARD: [PropertyIdentifier; 13] = [
PropertyIdentifier::OBJECT_NAME,
PropertyIdentifier::VENDOR_NAME,
PropertyIdentifier::VENDOR_IDENTIFIER,
PropertyIdentifier::MODEL_NAME,
PropertyIdentifier::FIRMWARE_REVISION,
PropertyIdentifier::APPLICATION_SOFTWARE_VERSION,
PropertyIdentifier::PROTOCOL_VERSION,
PropertyIdentifier::PROTOCOL_REVISION,
PropertyIdentifier::PROTOCOL_SERVICES_SUPPORTED,
PropertyIdentifier::PROTOCOL_OBJECT_TYPES_SUPPORTED,
PropertyIdentifier::MAX_APDU_LENGTH_ACCEPTED,
PropertyIdentifier::SEGMENTATION_SUPPORTED,
PropertyIdentifier::DATABASE_REVISION,
];
struct Done {
device: u32,
refused_rpm: bool,
}
type JobFuture<'a> = Pin<Box<dyn Future<Output = Done> + Send + 'a>>;
type Queues = HashMap<u32, Arc<Mutex<NameQueue>>>;
fn with_queue<R>(queues: &Queues, device: u32, f: impl FnOnce(&mut NameQueue) -> R) -> Option<R> {
let queue = queues.get(&device)?;
Some(f(&mut queue.lock().unwrap_or_else(PoisonError::into_inner)))
}
const MAX_QUEUES: usize = 256;
fn prune(queues: &mut Queues) {
if queues.len() > MAX_QUEUES {
queues.retain(|_, queue| {
Arc::strong_count(queue) > 1
|| !queue.lock().unwrap_or_else(PoisonError::into_inner).idle()
});
}
}
pub(crate) async fn run<T: TransportPort + 'static>(
client: &BACnetClient<T>,
sink: &EventSink,
mut commands: mpsc::Receiver<Command>,
rate: &RateCap,
) {
let mut scheduler = Scheduler::default();
let mut no_rpm = HashSet::new();
let mut queues = Queues::new();
let mut jobs: FuturesUnordered<JobFuture<'_>> = FuturesUnordered::new();
let start = |started: Started, no_rpm: &HashSet<u32>, queues: &Queues| -> JobFuture<'_> {
let rpm = !no_rpm.contains(&started.device);
let queue = queues.get(&started.device).cloned().unwrap_or_default();
Box::pin(job(client, sink, rate, started, rpm, queue))
};
loop {
tokio::select! {
command = commands.recv() => match command {
Some(Command::Names { req, asks }) => {
for NameAsk { device, address, max_apdu, objects, retry } in asks {
queues.entry(device).or_default();
let waiting = with_queue(&queues, device, |queue| {
if retry {
queue.retry();
}
queue.want(req, address, max_apdu, objects)
});
if waiting == Some(true) {
if let Some(started) = scheduler.submit_names(device) {
jobs.push(start(started, &no_rpm, &queues));
}
}
}
prune(&mut queues);
}
Some(Command::Browse { req, device, read }) => {
if read == BrowseRead::Device {
with_queue(&queues, device, NameQueue::forget);
}
if let Some(started) = scheduler.submit(device, Job { req, read }) {
jobs.push(start(started, &no_rpm, &queues));
}
}
Some(Command::StopBrowse { device }) => scheduler.stop(device),
Some(_) => {}
None => break,
},
Some(done) = jobs.next(), if !jobs.is_empty() => {
if done.refused_rpm {
no_rpm.insert(done.device);
}
let names = with_queue(&queues, done.device, |queue| queue.waiting()).unwrap_or(false);
if let Some(started) = scheduler.finished(done.device, names) {
jobs.push(start(started, &no_rpm, &queues));
}
}
}
}
}
async fn job<T: TransportPort + 'static>(
client: &BACnetClient<T>,
sink: &EventSink,
rate: &RateCap,
started: Started,
rpm: bool,
queue: Arc<Mutex<NameQueue>>,
) -> Done {
let Started {
device,
task,
cancel,
} = started;
let mut reader = Reader {
client,
device,
rate,
cancel: &cancel,
rpm,
};
let (req, read) = match task {
Task::Read(Job { req, read }) => (req, read),
Task::Names => {
let report = |req, address, names| {
deliver(
sink,
req,
BrowseResult::Names {
device,
address,
names,
},
)
};
names::read_names(&mut reader, &queue, report).await;
return Done {
device,
refused_rpm: rpm && !reader.rpm,
};
}
};
let outcome = match read {
BrowseRead::Device => read_device(&mut reader, sink, req).await,
BrowseRead::Object(object) => read_object(&mut reader, sink, req, object).await,
};
match outcome {
Ok(()) | Err(Halt::Cancelled) => {}
Err(Halt::Device(error)) => {
deliver(sink, req, BrowseResult::Failed(error.to_string())).await;
}
}
Done {
device,
refused_rpm: rpm && !reader.rpm,
}
}
async fn deliver(sink: &EventSink, req: ReqId, result: BrowseResult) {
sink.deliver(WorkerEvent::Browse { req, result }).await;
}
async fn read_device<T: TransportPort + 'static>(
reader: &mut Reader<'_, T>,
sink: &EventSink,
req: ReqId,
) -> Result<(), Halt> {
let device = reader.device_object()?;
let (rows, via) = reader.properties(device, Wanted::List(&CARD)).await?;
deliver(sink, req, BrowseResult::Card(view::card(&rows, via))).await;
let read = reader
.object_list(|done, total| {
if done.is_multiple_of(PROGRESS_EVERY) {
sink.offer(WorkerEvent::Browse {
req,
result: BrowseResult::Progress { done, total },
});
}
})
.await?;
deliver(
sink,
req,
BrowseResult::Objects {
objects: read.objects,
indexed: read.indexed,
stopped: read.stopped,
},
)
.await;
Ok(())
}
async fn read_object<T: TransportPort + 'static>(
reader: &mut Reader<'_, T>,
sink: &EventSink,
req: ReqId,
object: ObjectIdentifier,
) -> Result<(), Halt> {
let (rows, via) = reader.properties(object, Wanted::All).await?;
let view = view::object_view(object, &rows, via);
deliver(sink, req, BrowseResult::Object(Box::new(view))).await;
Ok(())
}
#[cfg(test)]
mod tests {
use bacnet_types::enums::ObjectType;
use super::*;
#[test]
fn idle_name_queues_past_the_cap_are_dropped() {
let mut queues = Queues::new();
for device in 0..u32::try_from(MAX_QUEUES).unwrap() {
queues.entry(device).or_default();
}
prune(&mut queues);
assert_eq!(queues.len(), MAX_QUEUES, "at the cap nothing goes");
let held = Arc::clone(queues.entry(10_000).or_default());
let object = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
queues.entry(10_001).or_default();
with_queue(&queues, 10_001, |queue| {
queue.want(1, "10.0.0.1:47808".into(), None, vec![object])
});
prune(&mut queues);
let mut kept: Vec<u32> = queues.keys().copied().collect();
kept.sort_unstable();
assert_eq!(kept, [10_000, 10_001]);
drop(held);
with_queue(&queues, 10_001, |queue| {
queue.want(2, "10.0.0.1:47808".into(), None, Vec::new())
});
for device in 0..u32::try_from(MAX_QUEUES).unwrap() {
queues.entry(device).or_default();
}
prune(&mut queues);
assert!(queues.is_empty(), "{:?}", queues.keys());
}
}