bacnet_server/
trend_log.rs1use std::collections::HashMap;
8use std::sync::Arc;
9use std::time::Instant;
10
11use tokio::sync::RwLock;
12use tracing::warn;
13
14use bacnet_objects::database::ObjectDatabase;
15use bacnet_types::constructed::{BACnetLogRecord, LogDatum};
16
17fn days_to_date(total_days: u64) -> (u8, u8, u8, u8) {
20 let dow = ((total_days + 3) % 7) as u8 + 1; let z = total_days as i64;
25 let era = z.div_euclid(146097);
26 let doe = z.rem_euclid(146097);
27 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
28 let y = yoe + era * 400 + 1970;
29 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
30 let mp = (5 * doy + 2) / 153;
31 let d = doy - (153 * mp + 2) / 5 + 1;
32 let m = if mp < 10 { mp + 3 } else { mp - 9 };
33 let y = if m <= 2 { y + 1 } else { y };
34
35 let year = (y - 1900).clamp(0, 255) as u8;
37 (year, m as u8, d as u8, dow)
38}
39use bacnet_types::enums::{ObjectType, PropertyIdentifier};
40use bacnet_types::primitives::{Date, ObjectIdentifier, PropertyValue, Time};
41
42pub type TrendLogState = Arc<tokio::sync::Mutex<HashMap<ObjectIdentifier, Instant>>>;
45
46fn property_value_to_log_datum(pv: &PropertyValue) -> LogDatum {
48 match pv {
49 PropertyValue::Real(v) => LogDatum::RealValue(*v),
50 PropertyValue::Unsigned(v) => LogDatum::UnsignedValue(*v),
51 PropertyValue::Signed(v) => LogDatum::SignedValue(*v as i64),
52 PropertyValue::Boolean(v) => LogDatum::BooleanValue(*v),
53 PropertyValue::Enumerated(v) => LogDatum::EnumValue(*v),
54 _ => LogDatum::NullValue,
55 }
56}
57
58fn make_record(datum: LogDatum) -> BACnetLogRecord {
60 let now = {
61 let dur = std::time::SystemTime::now()
62 .duration_since(std::time::UNIX_EPOCH)
63 .unwrap_or_default();
64 let secs = dur.as_secs();
65 let total_days = secs / 86400;
67 let (year, month, day, dow) = days_to_date(total_days);
68 let hour = ((secs % 86400) / 3600) as u8;
69 let minute = ((secs % 3600) / 60) as u8;
70 let second = (secs % 60) as u8;
71 (year, month, day, dow, hour, minute, second)
72 };
73 BACnetLogRecord {
74 date: Date {
75 year: now.0,
76 month: now.1,
77 day: now.2,
78 day_of_week: now.3,
79 },
80 time: Time {
81 hour: now.4,
82 minute: now.5,
83 second: now.6,
84 hundredths: 0,
85 },
86 log_datum: datum,
87 status_flags: None,
88 }
89}
90
91pub async fn poll_trend_logs(db: &Arc<RwLock<ObjectDatabase>>, state: &TrendLogState) {
97 let mut last_log = state.lock().await;
98 let now = Instant::now();
99
100 let to_poll: Vec<(ObjectIdentifier, u32, ObjectIdentifier, u32)> = {
101 let db_read = db.read().await;
102 let trend_oids = db_read.find_by_type(ObjectType::TREND_LOG);
103 let mut result = Vec::new();
104 for oid in trend_oids {
105 if let Some(obj) = db_read.get(&oid) {
106 let log_interval = match obj.read_property(PropertyIdentifier::LOG_INTERVAL, None) {
107 Ok(PropertyValue::Unsigned(v)) if v > 0 => v as u32,
108 _ => continue,
109 };
110
111 let logging_type = match obj.read_property(PropertyIdentifier::LOGGING_TYPE, None) {
112 Ok(PropertyValue::Enumerated(v)) => v,
113 _ => 0,
114 };
115
116 if logging_type == 1 {
117 warn!(object = %oid, "COV-based trend logging not yet implemented");
118 continue;
119 }
120 if logging_type == 2 {
121 warn!(object = %oid, "Triggered trend logging not yet implemented");
122 continue;
123 }
124
125 let monitored_ref =
126 match obj.read_property(PropertyIdentifier::LOG_DEVICE_OBJECT_PROPERTY, None) {
127 Ok(PropertyValue::List(ref items)) if items.len() >= 2 => {
128 let target_oid = match &items[0] {
129 PropertyValue::ObjectIdentifier(o) => *o,
130 _ => continue,
131 };
132 let prop_id = match &items[1] {
133 PropertyValue::Unsigned(v) => *v as u32,
134 _ => continue,
135 };
136 (target_oid, prop_id)
137 }
138 _ => continue,
139 };
140
141 let elapsed = last_log
142 .get(&oid)
143 .map(|t| now.duration_since(*t).as_secs() as u32)
144 .unwrap_or(u32::MAX);
145
146 if elapsed >= log_interval {
147 result.push((oid, log_interval, monitored_ref.0, monitored_ref.1));
148 }
149 }
150 }
151 result
152 };
153
154 if to_poll.is_empty() {
155 return;
156 }
157
158 let mut db_write = db.write().await;
159 for (trend_oid, _interval, target_oid, prop_id) in to_poll {
160 let datum = if let Some(target_obj) = db_write.get(&target_oid) {
161 match target_obj.read_property(PropertyIdentifier::from_raw(prop_id), None) {
162 Ok(pv) => property_value_to_log_datum(&pv),
163 Err(_) => LogDatum::NullValue,
164 }
165 } else {
166 LogDatum::NullValue
167 };
168
169 let record = make_record(datum);
170
171 if let Some(trend_obj) = db_write.get_mut(&trend_oid) {
172 trend_obj.add_trend_record(record);
173 }
174
175 last_log.insert(trend_oid, now);
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182
183 #[test]
184 fn property_value_to_datum_real() {
185 let pv = PropertyValue::Real(42.5);
186 match property_value_to_log_datum(&pv) {
187 LogDatum::RealValue(v) => assert!((v - 42.5).abs() < f32::EPSILON),
188 other => panic!("expected RealValue, got {:?}", other),
189 }
190 }
191
192 #[test]
193 fn property_value_to_datum_unsigned() {
194 let pv = PropertyValue::Unsigned(100);
195 match property_value_to_log_datum(&pv) {
196 LogDatum::UnsignedValue(v) => assert_eq!(v, 100),
197 other => panic!("expected UnsignedValue, got {:?}", other),
198 }
199 }
200
201 #[test]
202 fn property_value_to_datum_boolean() {
203 let pv = PropertyValue::Boolean(true);
204 match property_value_to_log_datum(&pv) {
205 LogDatum::BooleanValue(v) => assert!(v),
206 other => panic!("expected BooleanValue, got {:?}", other),
207 }
208 }
209
210 #[test]
211 fn make_record_has_valid_time() {
212 let record = make_record(LogDatum::RealValue(1.0));
213 assert!(record.time.hour < 24);
215 assert!(record.time.minute < 60);
216 assert!(record.time.second < 60);
217 }
218}