Skip to main content

bacnet_ops/engine/
log.rs

1//! A log page's requests, each charged to the limits.
2//!
3//! One page of a Trend Log, Trend Log Multiple, Event Log or Audit Log can
4//! take several requests: the log's record counts, then the ReadRange, and
5//! the counts again when the log moved meanwhile. The page holds its
6//! device's turn throughout, so they still go one at a time; the turn pays
7//! for the first request and each later one takes a slot of its own, so a
8//! page costs what it sends (#2066).
9//!
10//! Once a Trend Log's first page is answered, [`Engine::log_columns`]
11//! reads what it samples, so its values can be named (#2091): one more
12//! request, charged the same way, once per read rather than once per page.
13//! Asking only after an answer keeps a silent device to one timeout.
14//! [`Engine::log_state_texts`] then reads the state texts of the binary
15//! and multi-state objects whose states the columns sample (#2122): one
16//! request an object, at most
17//! [`MAX_STATE_TEXT_OBJECTS`](crate::format::MAX_STATE_TEXT_OBJECTS).
18//! Naming never fails a read, but each says, as a [`Naming`], whether a
19//! request of its timed out, so a caller that counts timeouts counts it
20//! (#2131).
21
22use std::ops::ControlFlow;
23use std::sync::atomic::{AtomicBool, Ordering};
24
25use bacnet_client::client::BACnetClient;
26use bacnet_client::log_reader::{LogRequester, unsigned_value};
27use bacnet_services::read_range::{ReadRangeReply, ReadRangeRequest, ReadRangeValidation};
28use bacnet_transport::port::TransportPort;
29use bacnet_types::enums::PropertyIdentifier;
30use bacnet_types::error::Error;
31use bacnet_types::primitives::ObjectIdentifier;
32
33use super::{Engine, Wanted, timed_out, unreachable};
34use crate::format::{LogColumns, StateTexts};
35use crate::limits::RequestLimits;
36
37/// How a best-effort naming read ended: it never fails the read it names,
38/// but a request it lost to a timeout is still a timeout (#2131).
39#[must_use = "a timed-out naming request is a timeout the caller may count"]
40#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
41pub enum Naming {
42    /// Every request sent was answered, or refused, or none was sent.
43    #[default]
44    Answered,
45    /// A request timed out ([`timed_out`]), and the
46    /// naming stopped there.
47    TimedOut,
48}
49
50impl Naming {
51    /// Whether a request timed out.
52    pub fn timed_out(self) -> bool {
53        self == Self::TimedOut
54    }
55
56    fn of(error: &Error) -> Self {
57        if timed_out(error) {
58            Self::TimedOut
59        } else {
60            Self::Answered
61        }
62    }
63}
64
65/// The requester a page reads through: the device's routed read calls,
66/// each after a slot (the first under the page's turn).
67pub(super) struct PagedLog<'a, T: TransportPort + 'static> {
68    pub(super) client: &'a BACnetClient<T>,
69    pub(super) limits: &'a RequestLimits,
70    pub(super) device: u32,
71    pub(super) network: Option<u16>,
72    /// Set until the first request, which the page's turn paid for.
73    pub(super) first: AtomicBool,
74}
75
76impl<T: TransportPort + 'static> PagedLog<'_, T> {
77    async fn charge(&self) {
78        if !self.first.swap(false, Ordering::Relaxed) {
79            self.limits.slot(self.network).await;
80        }
81    }
82}
83
84impl<T: TransportPort + 'static> LogRequester for PagedLog<'_, T> {
85    async fn read_unsigned(
86        &self,
87        _mac: &[u8],
88        log: ObjectIdentifier,
89        property: PropertyIdentifier,
90    ) -> Result<u64, Error> {
91        self.charge().await;
92        let ack = self
93            .client
94            .read_property_from_device(self.device, log, property, None)
95            .await?;
96        unsigned_value(&ack.property_value)
97    }
98
99    async fn read_range_lenient(
100        &self,
101        _mac: &[u8],
102        request: &ReadRangeRequest,
103    ) -> Result<ReadRangeReply, Error> {
104        self.charge().await;
105        self.client
106            .read_range_from_device_with(self.device, request, ReadRangeValidation::Lenient)
107            .await
108    }
109}
110
111impl<T: TransportPort + 'static> Engine<T> {
112    /// What each column of `log` on `device` samples, from its
113    /// Log_DeviceObjectProperty: one ReadProperty, charged like any other
114    /// read, for a Trend Log or Trend Log Multiple, and none for another
115    /// log. Naming is best effort: a refused, failed or empty read names no
116    /// column, and the values print as the log gives them. The [`Naming`]
117    /// says whether the read timed out.
118    pub async fn log_columns(&self, device: u32, log: ObjectIdentifier) -> (LogColumns, Naming) {
119        if !LogColumns::applies(log.object_type()) {
120            return (LogColumns::default(), Naming::Answered);
121        }
122        match self
123            .read_property(
124                device,
125                log,
126                PropertyIdentifier::LOG_DEVICE_OBJECT_PROPERTY,
127                None,
128            )
129            .await
130        {
131            Ok(data) => (LogColumns::decode(&data), Naming::Answered),
132            Err(error) => (LogColumns::default(), Naming::of(&error)),
133        }
134    }
135
136    /// Name the states `columns` sample with their objects' state texts,
137    /// read from `device`: one charged request an object (ReadPropertyMultiple,
138    /// or ReadProperty each once the device has refused it), each object
139    /// once, only objects on `device`, at most
140    /// [`MAX_STATE_TEXT_OBJECTS`](crate::format::MAX_STATE_TEXT_OBJECTS).
141    /// Best effort: a refused read leaves its columns named as before, and
142    /// a device that stops answering, or `out_of_time` turning true, ends
143    /// the reading before the next object. The [`Naming`] says whether the
144    /// read that ended it timed out.
145    pub async fn log_state_texts(
146        &self,
147        device: u32,
148        columns: &mut LogColumns,
149        out_of_time: impl Fn() -> bool,
150    ) -> Naming {
151        let out_of_time = &out_of_time;
152        // Atomic rather than a Cell: the MCP server's futures are Send.
153        let lost = AtomicBool::new(false);
154        let lost = &lost;
155        columns
156            .read_state_texts(Some(device), |object| async move {
157                if out_of_time() {
158                    return ControlFlow::Break(());
159                }
160                let wanted = Wanted::List(StateTexts::properties(object.object_type()));
161                match self.read(device, object, wanted).await {
162                    Ok(read) => ControlFlow::Continue(StateTexts::from_rows(&read.rows)),
163                    Err(error) if unreachable(&error) => {
164                        lost.store(timed_out(&error), Ordering::Relaxed);
165                        ControlFlow::Break(())
166                    }
167                    Err(_) => ControlFlow::Continue(StateTexts::default()),
168                }
169            })
170            .await;
171        if lost.load(Ordering::Relaxed) {
172            Naming::TimedOut
173        } else {
174            Naming::Answered
175        }
176    }
177}