bacnet-client 0.11.0

BACnet async client: TSM, segmentation, discovery, and service APIs
Documentation
use super::*;
use crate::tsm::{CompletionOutcome, CoordinatedCompletion, CoordinatedTerminalPhase};

fn log_mismatch(outcome: &CompletionOutcome, invoke_id: u8, pdu: &str) {
    if let CompletionOutcome::ServiceChoiceMismatch { expected, observed } = outcome {
        warn!(
            invoke_id,
            pdu,
            expected = expected.to_raw(),
            observed = observed.to_raw(),
            "Ignoring acknowledgment labelled for a different confirmed service"
        );
    }
}

pub(super) fn log_coordinated_mismatch(outcome: &CoordinatedCompletion, invoke_id: u8, pdu: &str) {
    match outcome {
        CoordinatedCompletion::Completed(outcome) => {
            log_mismatch(outcome, invoke_id, pdu);
        }
        CoordinatedCompletion::ServiceChoiceMismatch { expected, observed } => {
            warn!(
                invoke_id,
                pdu,
                expected = expected.to_raw(),
                observed = observed.to_raw(),
                "Ignoring acknowledgment labelled for a different confirmed service"
            );
        }
        CoordinatedCompletion::Rejected => {}
    }
}

pub(super) async fn take_current_reassembly(
    tsm: &Arc<Mutex<Tsm>>,
    seg_state: &mut HashMap<SegKey, SegmentedReceiveState>,
    key: &SegKey,
) -> Option<SegmentedReceiveState> {
    let state = seg_state.remove(key)?;
    if tsm
        .lock()
        .await
        .owner_is_current(&key.0, key.1, &state.owner)
    {
        Some(state)
    } else {
        debug!(invoke_id = key.1, "Reclaimed stale segmented receive state");
        None
    }
}

pub(super) async fn current_reassembly_owner(
    tsm: &Arc<Mutex<Tsm>>,
    seg_state: &mut HashMap<SegKey, SegmentedReceiveState>,
    key: &SegKey,
) -> Option<TransactionOwner> {
    let owner = seg_state.get(key)?.owner.clone();
    if tsm.lock().await.owner_is_current(&key.0, key.1, &owner) {
        Some(owner)
    } else {
        seg_state.remove(key);
        debug!(invoke_id = key.1, "Reclaimed stale segmented receive state");
        None
    }
}

pub(super) async fn admit_terminal_during_reassembly(
    tsm: &Arc<Mutex<Tsm>>,
    seg_state: &mut HashMap<SegKey, SegmentedReceiveState>,
    key: &SegKey,
    peer: &CanonicalPeer,
    apdu: &Apdu,
) {
    let Some(owner) = current_reassembly_owner(tsm, seg_state, key).await else {
        return;
    };
    let _ = tsm
        .lock()
        .await
        .try_claim_coordinated_terminal(&key.0, key.1, &owner, peer, apdu);
}

pub(super) enum TerminalDispatchOutcome {
    Completion(CoordinatedCompletion),
    PrematureSegmentedRequestAborted,
}

#[allow(clippy::too_many_arguments)]
pub(super) async fn complete_terminal_response(
    tsm: &Arc<Mutex<Tsm>>,
    tsm_mac: &MacAddr,
    invoke_id: u8,
    peer: &CanonicalPeer,
    apdu: &Apdu,
    response: TsmResponse,
    phase_gate: bool,
    owner: Option<TransactionOwner>,
) -> TerminalDispatchOutcome {
    let mut expected_owner = owner;
    let mut response = Some(response);
    loop {
        let mut tsm = tsm.lock().await;
        let admission = if phase_gate {
            tsm.coordinated_terminal_phase(tsm_mac, invoke_id, expected_owner.as_ref())
        } else {
            let owner = match expected_owner.clone() {
                Some(owner) if tsm.owner_is_current(tsm_mac, invoke_id, &owner) => Some(owner),
                Some(_) => None,
                None => tsm.current_owner(tsm_mac, invoke_id),
            };
            match owner {
                Some(owner) => CoordinatedTerminalPhase::Active(owner),
                None => CoordinatedTerminalPhase::NoTransaction,
            }
        };
        match admission {
            CoordinatedTerminalPhase::FinalSegmentSendPolling { owner, issue } => {
                drop(tsm);
                issue.wait_until_polled().await;
                expected_owner = Some(owner);
            }
            CoordinatedTerminalPhase::Active(owner) => {
                let Some(response) = response.take() else {
                    return TerminalDispatchOutcome::Completion(CoordinatedCompletion::Rejected);
                };
                let completion = tsm.complete_coordinated_terminal_response(
                    tsm_mac,
                    invoke_id,
                    Some(&owner),
                    peer,
                    apdu,
                    response,
                );
                return TerminalDispatchOutcome::Completion(completion);
            }
            CoordinatedTerminalPhase::PrematureSegmentedRequest(owner) => {
                let _ = tsm.try_claim_coordinated_terminal(tsm_mac, invoke_id, &owner, peer, apdu);
                tsm.complete_transaction_for_owner(
                    tsm_mac,
                    invoke_id,
                    &owner,
                    None,
                    TsmResponse::Abort {
                        reason: bacnet_types::enums::AbortReason::INVALID_APDU_IN_THIS_STATE
                            .to_raw(),
                    },
                );
                return TerminalDispatchOutcome::PrematureSegmentedRequestAborted;
            }
            CoordinatedTerminalPhase::NoTransaction => {
                return TerminalDispatchOutcome::Completion(CoordinatedCompletion::Rejected);
            }
        }
    }
}