1use std::path::{Path, PathBuf};
12use std::sync::atomic::{AtomicU64, Ordering};
13
14use bacnet_types::enums::{ObjectType, PropertyIdentifier};
15use bacnet_types::primitives::ObjectIdentifier;
16use serde::{Deserialize, Serialize};
17
18use super::{FrontEnd, NULL, Planned, Stored, WriteSpec};
19
20mod fold;
21
22pub const FILE: &str = "ledger.json";
24
25pub(crate) const COVERED: u64 = 0;
28
29static NEXT_TEMP: AtomicU64 = AtomicU64::new(0);
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
33#[serde(rename_all = "lowercase")]
34pub enum State {
35 Sending,
37 Applied,
39 Unknown,
41}
42
43#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
45pub struct Entry {
46 pub id: u64,
48 pub run: String,
50 pub at: String,
52 pub device: u32,
54 pub object_type: u32,
56 pub instance: u32,
58 pub property: u32,
60 #[serde(default)]
62 pub index: Option<u32>,
63 #[serde(default)]
65 pub priority: Option<u8>,
66 pub commandable: bool,
68 pub before: Option<Stored>,
71 pub written: Stored,
73 pub state: State,
75 #[serde(default)]
77 pub kept: bool,
78 #[serde(default)]
81 pub seq: u64,
82 #[serde(flatten)]
84 pub origin: Origin,
85}
86
87#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
91pub struct Origin {
92 #[serde(default, skip_serializing_if = "FrontEnd::is_tui")]
94 pub front_end: FrontEnd,
95 #[serde(default, skip_serializing_if = "Option::is_none")]
97 pub principal: Option<String>,
98}
99
100impl Origin {
101 pub fn mcp(principal: impl Into<String>) -> Self {
103 Self {
104 front_end: FrontEnd::Mcp,
105 principal: Some(principal.into()),
106 }
107 }
108}
109
110impl Entry {
111 pub fn object(&self) -> Option<ObjectIdentifier> {
113 ObjectIdentifier::new(ObjectType::from_raw(self.object_type), self.instance).ok()
114 }
115
116 pub fn property(&self) -> PropertyIdentifier {
118 PropertyIdentifier::from_raw(self.property)
119 }
120
121 pub fn label(&self) -> String {
123 match self.object() {
124 Some(object) => super::label(
125 self.device,
126 object,
127 self.property(),
128 self.index,
129 self.priority,
130 ),
131 None => format!("device {} object {}", self.device, self.object_type),
132 }
133 }
134
135 pub fn undo(&self) -> Option<Stored> {
139 match (&self.before, self.commandable) {
140 (Some(before), _) => Some(before.clone()),
141 (None, true) => Some(Stored::null()),
142 (None, false) => None,
143 }
144 }
145
146 fn same_target(&self, spec: &WriteSpec, priority: Option<u8>) -> bool {
147 self.device == spec.device
148 && self.object_type == spec.object.object_type().to_raw()
149 && self.instance == spec.object.instance_number()
150 && self.property == spec.property.to_raw()
151 && self.index == spec.index
152 && self.priority == priority
153 }
154
155 fn same_property(&self, spec: &WriteSpec) -> bool {
157 self.priority.is_none()
158 && self.device == spec.device
159 && self.object_type == spec.object.object_type().to_raw()
160 && self.instance == spec.object.instance_number()
161 && self.property == spec.property.to_raw()
162 }
163
164 fn same_array(&self, other: &Entry) -> bool {
166 self.priority.is_none()
167 && other.priority.is_none()
168 && (self.device, self.object_type, self.instance, self.property)
169 == (
170 other.device,
171 other.object_type,
172 other.instance,
173 other.property,
174 )
175 }
176
177 fn undone(&self) -> bool {
179 match &self.before {
180 Some(before) => before.bytes == self.written.bytes,
181 None => self.commandable && self.written.bytes == NULL,
182 }
183 }
184}
185
186#[derive(Serialize, Deserialize)]
187struct LedgerFile {
188 version: u32,
189 entries: Vec<Entry>,
190}
191
192#[derive(Debug)]
194pub struct Ledger {
195 path: PathBuf,
196 entries: Vec<Entry>,
197 origin: Origin,
199}
200
201impl Ledger {
202 pub fn open(workspace: &Path) -> Result<Self, String> {
204 let path = workspace.join(FILE);
205 let entries = match std::fs::read_to_string(&path) {
206 Ok(text) => {
207 let file: LedgerFile = serde_json::from_str(&text)
208 .map_err(|e| format!("cannot read {}: {e}", path.display()))?;
209 file.entries
210 }
211 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Vec::new(),
212 Err(e) => return Err(format!("cannot read {}: {e}", path.display())),
213 };
214 Ok(Self {
215 path,
216 entries,
217 origin: Origin::default(),
218 })
219 }
220
221 pub fn written_by(mut self, origin: Origin) -> Self {
224 self.origin = origin;
225 self
226 }
227
228 pub fn entries(&self) -> &[Entry] {
230 &self.entries
231 }
232
233 fn whole_entry(&self, spec: &WriteSpec) -> Option<&Entry> {
235 self.entries
236 .iter()
237 .find(|e| e.index.is_none() && e.same_property(spec))
238 }
239
240 pub(crate) fn owns(&self, spec: &WriteSpec, priority: Option<u8>, current: &[u8]) -> bool {
244 self.entries
245 .iter()
246 .any(|entry| entry.same_target(spec, priority) && entry.written.bytes == current)
247 }
248
249 #[doc(hidden)]
259 pub fn begin(&mut self, planned: &Planned, run: &str, at: &str) -> u64 {
260 let (spec, priority) = (&planned.spec, planned.priority);
261 let seq = self.entries.iter().map(|e| e.seq).max().unwrap_or(0) + 1;
262 if priority.is_none() && spec.index.is_some() && self.whole_entry(spec).is_some() {
263 return COVERED;
264 }
265 if let Some(entry) = self
266 .entries
267 .iter_mut()
268 .find(|e| e.same_target(spec, priority))
269 {
270 entry.written = planned.value.clone();
271 entry.state = State::Sending;
272 entry.seq = seq;
273 entry.origin = self.origin.clone();
274 return entry.id;
275 }
276 let id = self.entries.iter().map(|e| e.id).max().map_or(1, |n| n + 1);
277 let spec = &planned.spec;
278 let mut before = planned.before.clone();
279 if priority.is_none() && spec.index.is_none() {
280 let elements: Vec<Entry> = self
281 .entries
282 .iter()
283 .filter(|e| e.index.is_some() && e.same_property(spec))
284 .cloned()
285 .collect();
286 if !elements.is_empty() {
287 before = fold::original(before.as_ref(), &elements);
288 self.entries
289 .retain(|e| !(e.index.is_some() && e.same_property(spec)));
290 }
291 }
292 self.entries.push(Entry {
293 id,
294 run: run.into(),
295 at: at.into(),
296 device: spec.device,
297 object_type: spec.object.object_type().to_raw(),
298 instance: spec.object.instance_number(),
299 property: spec.property.to_raw(),
300 index: spec.index,
301 priority: planned.priority,
302 commandable: planned.commandable,
303 before,
304 written: planned.value.clone(),
305 state: State::Sending,
306 kept: false,
307 seq,
308 origin: self.origin.clone(),
309 });
310 id
311 }
312
313 #[doc(hidden)]
316 pub fn finish(&mut self, id: u64, applied: Option<bool>, backup: &[Entry]) {
317 match applied {
318 Some(false) => {
319 let failed = self.entries.iter().find(|e| e.id == id).cloned();
320 let old = backup.iter().find(|e| e.id == id).cloned();
321 self.entries.retain(|e| e.id != id);
322 if let Some(old) = old {
323 self.entries.push(old);
324 }
325 if let Some(whole) = failed.filter(|e| e.index.is_none() && e.priority.is_none()) {
327 let folded: Vec<Entry> = backup
328 .iter()
329 .filter(|e| e.index.is_some() && e.same_array(&whole))
330 .filter(|e| !self.entries.iter().any(|kept| kept.id == e.id))
331 .cloned()
332 .collect();
333 self.entries.extend(folded);
334 }
335 self.entries.sort_by_key(|e| e.id);
336 }
337 Some(true) => {
338 if let Some(entry) = self.entries.iter_mut().find(|e| e.id == id) {
339 entry.state = State::Applied;
340 }
341 self.entries.retain(|e| e.id != id || !e.undone());
342 }
343 None => {
344 if let Some(entry) = self.entries.iter_mut().find(|e| e.id == id) {
345 entry.state = State::Unknown;
346 }
347 }
348 }
349 }
350
351 pub(crate) fn newest_first(&self) -> Vec<Entry> {
353 let mut entries = self.entries.clone();
354 entries.sort_by(|a, b| b.seq.cmp(&a.seq).then(b.id.cmp(&a.id)));
355 entries
356 }
357
358 pub(crate) fn settle_abandoned(&mut self) -> Vec<Entry> {
365 let mut settled = Vec::new();
366 for entry in &mut self.entries {
367 if entry.state == State::Sending {
368 entry.state = State::Unknown;
369 settled.push(entry.clone());
370 }
371 }
372 settled
373 }
374
375 pub(crate) fn remove(&mut self, id: u64) {
377 self.entries.retain(|e| e.id != id);
378 }
379
380 pub fn keep_all(&mut self) {
382 for entry in &mut self.entries {
383 entry.kept = true;
384 }
385 }
386
387 pub fn save(&self) -> Result<(), String> {
389 let text = serde_json::to_string_pretty(&LedgerFile {
390 version: 1,
391 entries: self.entries.clone(),
392 })
393 .map_err(|e| format!("cannot encode the ledger: {e}"))?;
394 let directory = self.path.parent().unwrap_or(Path::new("."));
395 let temp = directory.join(format!(
396 ".ledger-{}-{}.tmp",
397 std::process::id(),
398 NEXT_TEMP.fetch_add(1, Ordering::Relaxed)
399 ));
400 super::file::publish(&temp, &self.path, &text, false)
401 .map_err(|e| format!("cannot save {}: {e}", self.path.display()))
402 }
403}
404
405#[cfg(test)]
406mod tests {
407 use bacnet_types::primitives::PropertyValue;
408
409 use super::super::{Effect, Gate, Occupancy, Tier};
410 use super::*;
411
412 fn stored(value: f32) -> Stored {
413 let mut buf = bytes::BytesMut::new();
414 bacnet_encoding::primitives::encode_property_value(&mut buf, &PropertyValue::Real(value))
415 .unwrap();
416 Stored {
417 bytes: buf.to_vec(),
418 text: value.to_string(),
419 }
420 }
421
422 fn planned(value: Stored, before: Option<Stored>) -> Planned {
423 Planned {
424 spec: WriteSpec {
425 device: 100,
426 object: ObjectIdentifier::new(ObjectType::ANALOG_OUTPUT, 1).unwrap(),
427 property: PropertyIdentifier::PRESENT_VALUE,
428 index: None,
429 value: None,
430 priority: Some(8),
431 },
432 priority: Some(8),
433 value,
434 commandable: true,
435 before,
436 occupancy: Occupancy::Empty,
437 effect: Effect::NotPresentValue,
438 out_of_service: None,
439 notes: Vec::new(),
440 gate: Gate::Allowed(Tier::T2),
441 }
442 }
443
444 #[test]
445 fn writes_merge_keep_the_first_pre_image_and_close_on_relinquish() {
446 let dir = tempfile::tempdir().unwrap();
447 let mut ledger = Ledger::open(dir.path()).unwrap();
448 let backup = ledger.entries().to_vec();
449 let id = ledger.begin(&planned(stored(42.0), None), "run-a", "t");
450 ledger.finish(id, Some(true), &backup);
451 let backup = ledger.entries().to_vec();
452 let again = ledger.begin(&planned(stored(43.0), Some(stored(42.0))), "run-a", "t");
453 assert_eq!(again, id, "merged");
454 ledger.finish(again, Some(true), &backup);
455 assert_eq!(ledger.entries()[0].before, None, "the first pre-image");
456 assert_eq!(ledger.entries()[0].undo(), Some(Stored::null()));
457 ledger.save().unwrap();
458 let reloaded = Ledger::open(dir.path()).unwrap();
459 assert_eq!(reloaded.entries(), ledger.entries());
460 let backup = ledger.entries().to_vec();
462 let id = ledger.begin(&planned(stored(44.0), None), "run-a", "t");
463 ledger.finish(id, Some(false), &backup);
464 assert_eq!(ledger.entries(), backup.as_slice());
465 let id = ledger.begin(&planned(Stored::null(), Some(stored(43.0))), "run-a", "t");
467 ledger.finish(id, Some(true), &backup);
468 assert!(ledger.entries().is_empty());
469 }
470
471 fn texts(values: &[&str]) -> Stored {
472 let mut buf = bytes::BytesMut::new();
473 for value in values {
474 let value = PropertyValue::CharacterString((*value).into());
475 bacnet_encoding::primitives::encode_property_value(&mut buf, &value).unwrap();
476 }
477 Stored {
478 bytes: buf.to_vec(),
479 text: values.join(", "),
480 }
481 }
482
483 fn state_text(index: Option<u32>, value: &[&str], before: Option<&[&str]>) -> Planned {
485 let mut planned = planned(texts(value), before.map(texts));
486 planned.spec.object = ObjectIdentifier::new(ObjectType::MULTI_STATE_VALUE, 1).unwrap();
487 planned.spec.property = PropertyIdentifier::STATE_TEXT;
488 planned.spec.index = index;
489 planned.spec.priority = None;
490 (planned.priority, planned.commandable) = (None, false);
491 planned
492 }
493
494 #[test]
497 fn element_and_whole_writes_to_one_array_revert_to_the_original() {
498 let dir = tempfile::tempdir().unwrap();
499 let mut ledger = Ledger::open(dir.path()).unwrap();
500 let t1 = ledger.begin(&state_text(Some(2), &["A"], Some(&["S2"])), "r", "t");
501 ledger.finish(t1, Some(true), &[]);
502 let whole = state_text(None, &["X", "Y", "Z"], Some(&["S1", "A", "S3"]));
503 let t2 = ledger.begin(&whole, "r", "t");
504 ledger.finish(t2, Some(true), &[]);
505 let t3 = ledger.begin(&state_text(Some(2), &["B"], Some(&["Y"])), "r", "t");
506 assert_eq!(t3, COVERED, "the whole entry covers it");
507 ledger.finish(t3, Some(true), &[]);
508 let [entry] = ledger.entries() else {
509 panic!("{:?}", ledger.entries());
510 };
511 assert_eq!(entry.index, None);
512 assert_eq!(
513 entry.undo().unwrap().bytes,
514 texts(&["S1", "S2", "S3"]).bytes
515 );
516 assert_eq!(entry.written.bytes, texts(&["X", "Y", "Z"]).bytes);
517 }
518
519 #[test]
522 fn a_refused_whole_write_unfolds_and_an_unknown_element_stays_unknown() {
523 let dir = tempfile::tempdir().unwrap();
524 let mut ledger = Ledger::open(dir.path()).unwrap();
525 let t1 = ledger.begin(&state_text(Some(2), &["A"], Some(&["S2"])), "r", "t");
526 ledger.finish(t1, Some(true), &[]);
527 let backup = ledger.entries().to_vec();
528 let whole = state_text(None, &["X", "Y", "Z"], Some(&["S1", "A", "S3"]));
529 let t2 = ledger.begin(&whole, "r", "t");
530 assert_eq!(ledger.entries().len(), 1, "folded");
531 ledger.finish(t2, Some(false), &backup);
532 assert_eq!(ledger.entries(), backup.as_slice());
533
534 let resize = state_text(Some(0), &["4"], None);
535 let t0 = ledger.begin(&resize, "r", "t");
536 ledger.finish(t0, Some(true), &[]);
537 let t2 = ledger.begin(&whole, "r", "t");
538 ledger.finish(t2, Some(true), &[]);
539 let [entry] = ledger.entries() else {
540 panic!("{:?}", ledger.entries());
541 };
542 assert_eq!(entry.undo(), None, "a resize was among them");
543 }
544
545 #[test]
546 fn an_unanswered_write_stays_as_unknown() {
547 let dir = tempfile::tempdir().unwrap();
548 let mut ledger = Ledger::open(dir.path()).unwrap();
549 let id = ledger.begin(&planned(stored(42.0), None), "run-a", "t");
550 assert_eq!(ledger.entries()[0].state, State::Sending);
551 ledger.finish(id, None, &[]);
552 assert_eq!(ledger.entries()[0].state, State::Unknown);
553 }
554
555 #[test]
556 fn a_slot_is_ours_only_while_it_holds_what_we_wrote() {
557 let dir = tempfile::tempdir().unwrap();
558 let mut ledger = Ledger::open(dir.path()).unwrap();
559 let ours = planned(stored(42.0), None);
560 let id = ledger.begin(&ours, "run-a", "t");
561 ledger.finish(id, Some(true), &[]);
562 assert!(ledger.owns(&ours.spec, Some(8), &stored(42.0).bytes));
563 assert!(
564 !ledger.owns(&ours.spec, Some(8), &stored(50.0).bytes),
565 "someone overwrote it"
566 );
567 assert!(!ledger.owns(&ours.spec, Some(9), &stored(42.0).bytes));
568 }
569
570 #[test]
571 fn revert_order_follows_the_latest_write_and_abandoned_sends_settle() {
572 let dir = tempfile::tempdir().unwrap();
573 let mut ledger = Ledger::open(dir.path()).unwrap();
574 let first = ledger.begin(&planned(stored(1.0), None), "run-a", "t");
575 let mut other = planned(stored(2.0), None);
576 other.priority = Some(9);
577 let second = ledger.begin(&other, "run-a", "t");
578 ledger.begin(&planned(stored(3.0), None), "run-a", "t");
580 let order: Vec<u64> = ledger.newest_first().iter().map(|e| e.id).collect();
581 assert_eq!(order, [first, second]);
582 let settled = ledger.settle_abandoned();
585 assert_eq!(settled.len(), 2);
586 assert!(ledger.entries().iter().all(|e| e.state == State::Unknown));
587 }
588}