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}