use std::pin::pin;
use std::sync::Mutex;
use std::time::Duration;
use bacnet_objects::channel::{
ChannelDistribution, ChannelMember, MemberDatatype, coerce_channel_value,
};
use bacnet_objects::command::{CommandRun, WriteFailure};
use bacnet_objects::database::ObjectDatabase;
use bacnet_types::constructed::BACnetActionCommand;
use bacnet_types::enums::PropertyIdentifier;
use bacnet_types::primitives::{ObjectIdentifier, PropertyValue};
use futures_util::StreamExt;
use futures_util::stream::{self, FuturesUnordered};
use tokio::time::Instant;
use tracing::debug;
use super::remote_slots::RemoteSlot;
use super::target::{self, Failed};
use super::{Owner, RunHost};
use crate::server::RemoteRequestError;
struct Shared {
start: Instant,
silent: Mutex<Vec<ObjectIdentifier>>,
}
impl Shared {
fn is_silent(&self, device: ObjectIdentifier) -> bool {
self.silent
.lock()
.is_ok_and(|silent| silent.contains(&device))
}
fn found_silent(&self, device: ObjectIdentifier) {
if let Ok(mut silent) = self.silent.lock() {
silent.push(device);
}
}
fn due(&self, member: &ChannelMember) -> Instant {
self.start + Duration::from_millis(member.delay_ms.into())
}
}
pub(super) async fn distribute<H: RunHost>(
host: &H,
run: &CommandRun,
distribution: &ChannelDistribution,
owner: &mut Owner<'_, H>,
) -> Option<Result<(), WriteFailure>> {
let shared = Shared {
start: Instant::now(),
silent: Mutex::new(Vec::new()),
};
let (mut local, remote): (Vec<&ChannelMember>, Vec<(&ChannelMember, ObjectIdentifier)>) = {
let db = host.database().read().await;
if !current(&db, run) {
return None;
}
let mut local = Vec::new();
let mut remote = Vec::new();
for member in &distribution.members {
match member.reference.device_identifier {
Some(device) if !db.local_device().is_local(Some(device)) => {
remote.push((member, device));
}
_ => local.push(member),
}
}
(local, remote)
};
local.sort_by_key(|member| member.delay_ms);
let shared = &shared;
let local = stream::unfold(local.into_iter(), |mut members| async move {
let member = members.next()?;
tokio::time::sleep_until(shared.due(member)).await;
let written = write_member(host, run, distribution, member, None, shared).await;
Some((written, members))
});
let mut local = pin!(local.fuse());
let mut remote: FuturesUnordered<_> = remote
.into_iter()
.map(|(member, device)| write_remote(host, run, distribution, member, device, shared))
.collect();
let mut outcome = Ok(());
let mut made = 0;
loop {
let finished = tokio::select! {
biased;
Some(finished) = remote.next() => finished,
Some(finished) = local.next() => finished,
else => break,
};
outcome = outcome.and(finished?);
made += 1;
owner.progress(made, outcome);
}
Some(outcome)
}
fn current(db: &ObjectDatabase, run: &CommandRun) -> bool {
db.get(&run.source)
.and_then(|object| object.command_generation_internal())
== Some(run.generation)
}
async fn write_remote<H: RunHost>(
host: &H,
run: &CommandRun,
distribution: &ChannelDistribution,
member: &ChannelMember,
device: ObjectIdentifier,
shared: &Shared,
) -> Option<Result<(), WriteFailure>> {
let datatype = match member.learned {
Some(learned) => learned,
None => learn(host, run, distribution, member, device, shared).await,
};
tokio::time::sleep_until(shared.due(member)).await;
write_member(
host,
run,
distribution,
member,
Some((device, datatype)),
shared,
)
.await
}
async fn slot<H: RunHost>(host: &H, device: ObjectIdentifier) -> Option<RemoteSlot<'_>> {
match host.remote_slots() {
Some(slots) => Some(slots.acquire(device).await),
None => None,
}
}
async fn learn<H: RunHost>(
host: &H,
run: &CommandRun,
distribution: &ChannelDistribution,
member: &ChannelMember,
device: ObjectIdentifier,
shared: &Shared,
) -> MemberDatatype {
let reference = &member.reference;
let property = PropertyIdentifier::from_raw(reference.property_identifier);
let object_type = reference.object_identifier.object_type();
let unread = MemberDatatype::of(object_type, property, None);
let decides = !matches!(
distribution.value,
PropertyValue::Null | PropertyValue::ApplicationData(_)
) && unread == MemberDatatype::Unknown;
if !decides {
return unread;
}
let read = {
let _slot = slot(host, device).await;
let current = current(&*host.database().read().await, run);
if !current || shared.is_silent(device) {
return unread;
}
host.read_remote(device, reference).await
};
let datatype = match read {
Ok(value) => MemberDatatype::of(object_type, property, Some(&value)),
Err(error @ (RemoteRequestError::Unanswered | RemoteRequestError::Undiscovered)) => {
debug!(
channel = %run.source,
%device,
target = %reference.object_identifier,
?property,
%error,
"Channel member's device answered no datatype read; it counts as silent"
);
shared.found_silent(device);
return unread;
}
Err(error) => {
debug!(
channel = %run.source,
%device,
target = %reference.object_identifier,
?property,
%error,
"Channel member's datatype not learned; its value goes as written"
);
return unread;
}
};
if datatype != MemberDatatype::Unknown {
remember(host, run, member, Some(datatype)).await;
}
datatype
}
async fn remember<H: RunHost>(
host: &H,
run: &CommandRun,
member: &ChannelMember,
datatype: Option<MemberDatatype>,
) {
let mut db = host.database().write().await;
if let Some(channel) = db.get_mut(&run.source) {
channel.remember_member_datatype_internal(member.slot, &member.reference, datatype);
}
}
async fn write_member<H: RunHost>(
host: &H,
run: &CommandRun,
distribution: &ChannelDistribution,
member: &ChannelMember,
remote: Option<(ObjectIdentifier, MemberDatatype)>,
shared: &Shared,
) -> Option<Result<(), WriteFailure>> {
let reference = &member.reference;
let property = PropertyIdentifier::from_raw(reference.property_identifier);
let (device, datatype) = {
let db = host.database().read().await;
if !current(&db, run) {
return None;
}
match remote {
Some((device, datatype)) => (Some(device), datatype),
None => {
let value = db.get(&reference.object_identifier).and_then(|object| {
object
.read_property(property, reference.property_array_index)
.ok()
});
let object_type = reference.object_identifier.object_type();
(
None,
MemberDatatype::of(object_type, property, value.as_ref()),
)
}
}
};
let _slot = match device {
Some(device) => slot(host, device).await,
None => None,
};
if let Some(device) = device.filter(|device| shared.is_silent(*device)) {
debug!(
channel = %run.source,
%device,
target = %reference.object_identifier,
?property,
"Channel member not sent: its device answered nothing earlier in this distribution"
);
return Some(Err(WriteFailure::Communication));
}
let Ok(value) = coerce_channel_value(&distribution.value, datatype) else {
debug!(
channel = %run.source,
device = ?device,
target = %reference.object_identifier,
?property,
?datatype,
"Channel value can't be coerced to the member's datatype"
);
return Some(Err(WriteFailure::Configuration));
};
let command = BACnetActionCommand {
device_identifier: device,
object_identifier: reference.object_identifier,
property_identifier: property,
property_array_index: reference.property_array_index,
property_value: value,
priority: distribution.priority,
post_delay: None,
quit_on_failure: false,
write_successful: false,
};
Some(match target::write(host, run, device, &command).await {
Ok(()) => Ok(()),
Err(Failed {
answer: Some(answer),
..
}) if command.property_value == PropertyValue::Null
&& target::refuses_datatype(&answer) =>
{
Ok(())
}
Err(failed) => {
if let Some(device) = device.filter(|_| failed.unanswered) {
shared.found_silent(device);
}
let refused = failed.answer.is_some() && failed.failure == WriteFailure::Configuration;
let kept = remote.is_some_and(|(_, datatype)| {
!matches!(
datatype,
MemberDatatype::Unknown
| MemberDatatype::LightingCommand
| MemberDatatype::ColorCommand
| MemberDatatype::XyColor
)
});
if refused && kept {
remember(host, run, member, None).await;
}
Err(failed.failure)
}
})
}