bacnet_types/constructed.rs
1//! BACnet constructed data types per ASHRAE 135-2020.
2//!
3//! This module provides compound/structured types that are used by higher-level
4//! BACnet objects (Calendar, Schedule, TrendLog, NotificationClass, Loop, etc.).
5//! All types follow the same `no_std`-compatible pattern used in `primitives.rs`.
6
7#[cfg(not(feature = "std"))]
8use alloc::{vec, vec::Vec};
9
10use crate::error::Error;
11use crate::primitives::{Date, ObjectIdentifier, Time};
12use crate::MacAddr;
13
14// ---------------------------------------------------------------------------
15// BACnetDateRange (Clause 21 -- used by CalendarEntry and BACnetSpecialEvent)
16// ---------------------------------------------------------------------------
17
18/// BACnet date range: a SEQUENCE of start and end Date values.
19///
20/// Encoded as 8 bytes: 4 bytes for start_date followed by 4 bytes for end_date.
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct BACnetDateRange {
23 /// The start of the date range (inclusive).
24 pub start_date: Date,
25 /// The end of the date range (inclusive).
26 pub end_date: Date,
27}
28
29impl BACnetDateRange {
30 /// Encode to 8 bytes (start_date || end_date).
31 pub fn encode(&self) -> [u8; 8] {
32 let mut out = [0u8; 8];
33 out[..4].copy_from_slice(&self.start_date.encode());
34 out[4..].copy_from_slice(&self.end_date.encode());
35 out
36 }
37
38 /// Decode from at least 8 bytes.
39 pub fn decode(data: &[u8]) -> Result<Self, Error> {
40 if data.len() < 8 {
41 return Err(Error::buffer_too_short(8, data.len()));
42 }
43 Ok(Self {
44 start_date: Date::decode(&data[0..4])?,
45 end_date: Date::decode(&data[4..8])?,
46 })
47 }
48}
49
50// ---------------------------------------------------------------------------
51// BACnetWeekNDay (Clause 21 -- used by CalendarEntry)
52// ---------------------------------------------------------------------------
53
54/// BACnet Week-And-Day: OCTET STRING(3) encoding month, week_of_month,
55/// and day_of_week.
56///
57/// Each field may be `0xFF` to mean "any" (wildcard).
58///
59/// - `month`: 1-12, 13=odd, 14=even, 0xFF=any
60/// - `week_of_month`: 1=first, 2=second, ..., 5=last, 6=any-in-first,
61/// 0xFF=any
62/// - `day_of_week`: 1=Monday..7=Sunday, 0xFF=any
63#[derive(Debug, Clone, PartialEq, Eq)]
64pub struct BACnetWeekNDay {
65 /// Month (1-14, or 0xFF for any).
66 pub month: u8,
67 /// Week of month (1-6, or 0xFF for any).
68 pub week_of_month: u8,
69 /// Day of week (1-7, or 0xFF for any).
70 pub day_of_week: u8,
71}
72
73impl BACnetWeekNDay {
74 /// Wildcard value indicating "any" for any field.
75 pub const ANY: u8 = 0xFF;
76
77 /// Encode to 3 bytes.
78 pub fn encode(&self) -> [u8; 3] {
79 [self.month, self.week_of_month, self.day_of_week]
80 }
81
82 /// Decode from at least 3 bytes.
83 pub fn decode(data: &[u8]) -> Result<Self, Error> {
84 if data.len() < 3 {
85 return Err(Error::buffer_too_short(3, data.len()));
86 }
87 Ok(Self {
88 month: data[0],
89 week_of_month: data[1],
90 day_of_week: data[2],
91 })
92 }
93}
94
95// ---------------------------------------------------------------------------
96// BACnetCalendarEntry (Clause 12.6.3 -- property list of Calendar object)
97// ---------------------------------------------------------------------------
98
99/// BACnet calendar entry: a CHOICE between a specific date, a date range,
100/// or a week-and-day pattern.
101///
102/// Context tags per spec:
103/// - `[0]` Date
104/// - `[1]` DateRange
105/// - `[2]` WeekNDay
106#[derive(Debug, Clone, PartialEq, Eq)]
107pub enum BACnetCalendarEntry {
108 /// A single specific date (context tag 0).
109 Date(Date),
110 /// A contiguous date range (context tag 1).
111 DateRange(BACnetDateRange),
112 /// A recurring week-and-day pattern (context tag 2).
113 WeekNDay(BACnetWeekNDay),
114}
115
116// ---------------------------------------------------------------------------
117// BACnetTimeValue (Clause 12.17.4 -- used by Schedule weekly_schedule)
118// ---------------------------------------------------------------------------
119
120/// BACnet time-value pair: a Time followed by an application-tagged value.
121///
122/// The `value` field holds raw application-tagged bytes because the value
123/// type is polymorphic (Real, Boolean, Unsigned, Null, etc.) and the Schedule
124/// object stores them opaquely for later dispatch.
125#[derive(Debug, Clone, PartialEq)]
126pub struct BACnetTimeValue {
127 /// The time at which the value applies.
128 pub time: Time,
129 /// Raw application-tagged BACnet encoding of the value.
130 pub value: Vec<u8>,
131}
132
133// ---------------------------------------------------------------------------
134// SpecialEventPeriod (Clause 12.17.5 -- used by BACnetSpecialEvent)
135// ---------------------------------------------------------------------------
136
137/// The period portion of a BACnetSpecialEvent: either an inline
138/// CalendarEntry or a reference to an existing Calendar object.
139///
140/// Context tags per spec:
141/// - `[0]` CalendarEntry (constructed)
142/// - `[1]` CalendarReference (ObjectIdentifier)
143#[derive(Debug, Clone, PartialEq, Eq)]
144pub enum SpecialEventPeriod {
145 /// An inline calendar entry (context tag 0).
146 CalendarEntry(BACnetCalendarEntry),
147 /// A reference to a Calendar object (context tag 1).
148 CalendarReference(ObjectIdentifier),
149}
150
151// ---------------------------------------------------------------------------
152// BACnetSpecialEvent (Clause 12.17.5 -- exception_schedule of Schedule)
153// ---------------------------------------------------------------------------
154
155/// BACnet special event: an exception schedule entry combining a period
156/// definition, a list of time-value pairs, and a priority.
157#[derive(Debug, Clone, PartialEq)]
158pub struct BACnetSpecialEvent {
159 /// The period this special event applies to.
160 pub period: SpecialEventPeriod,
161 /// Ordered list of time-value pairs to apply during this period.
162 pub list_of_time_values: Vec<BACnetTimeValue>,
163 /// Priority for conflict resolution (1=highest..16=lowest).
164 pub event_priority: u8,
165}
166
167// ---------------------------------------------------------------------------
168// BACnetObjectPropertyReference (Clause 21 -- used by Loop and others)
169// ---------------------------------------------------------------------------
170
171/// A reference to a specific property (and optionally an array index) on
172/// a specific object within the same device.
173#[derive(Debug, Clone, PartialEq, Eq)]
174pub struct BACnetObjectPropertyReference {
175 /// The object being referenced.
176 pub object_identifier: ObjectIdentifier,
177 /// The property being referenced (PropertyIdentifier raw value).
178 pub property_identifier: u32,
179 /// Optional array index within the property.
180 pub property_array_index: Option<u32>,
181}
182
183impl BACnetObjectPropertyReference {
184 /// Create a reference without an array index.
185 pub fn new(object_identifier: ObjectIdentifier, property_identifier: u32) -> Self {
186 Self {
187 object_identifier,
188 property_identifier,
189 property_array_index: None,
190 }
191 }
192
193 /// Create a reference with an array index.
194 pub fn new_indexed(
195 object_identifier: ObjectIdentifier,
196 property_identifier: u32,
197 array_index: u32,
198 ) -> Self {
199 Self {
200 object_identifier,
201 property_identifier,
202 property_array_index: Some(array_index),
203 }
204 }
205}
206
207// ---------------------------------------------------------------------------
208// BACnetDeviceObjectPropertyReference (Clause 21 -- used by several objects)
209// ---------------------------------------------------------------------------
210
211/// Like `BACnetObjectPropertyReference` but may also specify a remote device.
212///
213/// When `device_identifier` is `None`, the reference is to an object in the
214/// local device.
215#[derive(Debug, Clone, PartialEq, Eq)]
216pub struct BACnetDeviceObjectPropertyReference {
217 /// The object being referenced.
218 pub object_identifier: ObjectIdentifier,
219 /// The property being referenced (PropertyIdentifier raw value).
220 pub property_identifier: u32,
221 /// Optional array index within the property.
222 pub property_array_index: Option<u32>,
223 /// Optional device identifier (None = local device).
224 pub device_identifier: Option<ObjectIdentifier>,
225}
226
227impl BACnetDeviceObjectPropertyReference {
228 /// Create a local-device reference without an array index.
229 pub fn new_local(object_identifier: ObjectIdentifier, property_identifier: u32) -> Self {
230 Self {
231 object_identifier,
232 property_identifier,
233 property_array_index: None,
234 device_identifier: None,
235 }
236 }
237
238 /// Create a remote-device reference without an array index.
239 pub fn new_remote(
240 object_identifier: ObjectIdentifier,
241 property_identifier: u32,
242 device_identifier: ObjectIdentifier,
243 ) -> Self {
244 Self {
245 object_identifier,
246 property_identifier,
247 property_array_index: None,
248 device_identifier: Some(device_identifier),
249 }
250 }
251
252 /// Create a reference with an array index (may be local or remote).
253 pub fn with_index(mut self, array_index: u32) -> Self {
254 self.property_array_index = Some(array_index);
255 self
256 }
257}
258
259// ---------------------------------------------------------------------------
260// BACnetAddress (Clause 21 -- network address used by BACnetRecipient)
261// ---------------------------------------------------------------------------
262
263/// A BACnet network address: network number (0 = local) plus MAC address.
264#[derive(Debug, Clone, PartialEq, Eq)]
265pub struct BACnetAddress {
266 /// Network number (0 = local network, 1-65534 = remote, 65535 = broadcast).
267 pub network_number: u16,
268 /// MAC-layer address (variable length; empty = local broadcast).
269 pub mac_address: MacAddr,
270}
271
272impl BACnetAddress {
273 /// Create a local-broadcast address.
274 pub fn local_broadcast() -> Self {
275 Self {
276 network_number: 0,
277 mac_address: MacAddr::new(),
278 }
279 }
280
281 /// Create a BACnet/IP address from a 6-byte octet-string (4-byte IPv4 + 2-byte port).
282 pub fn from_ip(ip_port_bytes: [u8; 6]) -> Self {
283 Self {
284 network_number: 0,
285 mac_address: MacAddr::from_slice(&ip_port_bytes),
286 }
287 }
288}
289
290// ---------------------------------------------------------------------------
291// BACnetRecipient (Clause 21 -- used by BACnetDestination / NotificationClass)
292// ---------------------------------------------------------------------------
293
294/// A BACnet notification recipient: either a Device object reference or a
295/// network address.
296///
297/// Context tags per spec:
298/// - `[0]` Device (ObjectIdentifier)
299/// - `[1]` Address (BACnetAddress)
300#[derive(Debug, Clone, PartialEq, Eq)]
301pub enum BACnetRecipient {
302 /// A device identified by its Object Identifier (context tag 0).
303 Device(ObjectIdentifier),
304 /// A device identified by its network address (context tag 1).
305 Address(BACnetAddress),
306}
307
308// ---------------------------------------------------------------------------
309// BACnetDestination (Clause 12.15.5 -- recipient_list of NotificationClass)
310// ---------------------------------------------------------------------------
311
312/// A single entry in a NotificationClass recipient list.
313///
314/// Specifies *who* receives the notification, *when* (days/times), and *how*
315/// (confirmed vs. unconfirmed, which transition types).
316#[derive(Debug, Clone, PartialEq, Eq)]
317pub struct BACnetDestination {
318 /// Bitmask of valid days (bit 0 = Monday ... bit 6 = Sunday), 7 bits used.
319 pub valid_days: u8,
320 /// Start of the daily time window during which this destination is active.
321 pub from_time: Time,
322 /// End of the daily time window.
323 pub to_time: Time,
324 /// The notification recipient.
325 pub recipient: BACnetRecipient,
326 /// Process identifier on the receiving device.
327 pub process_identifier: u32,
328 /// If true, use ConfirmedEventNotification; otherwise unconfirmed.
329 pub issue_confirmed_notifications: bool,
330 /// Bitmask of event transitions to send (bit 0=ToOffNormal, bit 1=ToFault,
331 /// bit 2=ToNormal), 3 bits used.
332 pub transitions: u8,
333}
334
335// ---------------------------------------------------------------------------
336// LogDatum (Clause 12.20.5 -- log_buffer element datum of TrendLog)
337// ---------------------------------------------------------------------------
338
339/// The datum field of a BACnetLogRecord: a CHOICE covering all possible
340/// logged value types.
341///
342/// Context tags per spec (Clause 12.20.5):
343/// - `[0]` log-status (BACnetLogStatus, 8-bit flags)
344/// - `[1]` boolean-value
345/// - `[2]` real-value
346/// - `[3]` enum-value (unsigned)
347/// - `[4]` unsigned-value
348/// - `[5]` signed-value
349/// - `[6]` bitstring-value
350/// - `[7]` null-value
351/// - `[8]` failure (BACnetError)
352/// - `[9]` time-change (REAL, clock-adjustment seconds)
353/// - `[10]` any-value (raw application-tagged bytes)
354#[derive(Debug, Clone, PartialEq)]
355pub enum LogDatum {
356 /// Log-status flags (context tag 0). Bit 0=log-disabled, bit 1=buffer-purged,
357 /// bit 2=log-interrupted.
358 LogStatus(u8),
359 /// Boolean value (context tag 1).
360 BooleanValue(bool),
361 /// Real (f32) value (context tag 2).
362 RealValue(f32),
363 /// Enumerated value (context tag 3).
364 EnumValue(u32),
365 /// Unsigned integer value (context tag 4).
366 UnsignedValue(u64),
367 /// Signed integer value (context tag 5).
368 SignedValue(i64),
369 /// Bit-string value (context tag 6).
370 BitstringValue {
371 /// Number of unused bits in the last byte.
372 unused_bits: u8,
373 /// The bit data.
374 data: Vec<u8>,
375 },
376 /// Null value (context tag 7).
377 NullValue,
378 /// Error (context tag 8): error class + error code.
379 Failure {
380 /// Raw BACnet error class value.
381 error_class: u32,
382 /// Raw BACnet error code value.
383 error_code: u32,
384 },
385 /// Time-change: clock-adjustment amount in seconds (context tag 9).
386 TimeChange(f32),
387 /// Any-value: raw application-tagged bytes for types not enumerated above
388 /// (context tag 10).
389 AnyValue(Vec<u8>),
390}
391
392// ---------------------------------------------------------------------------
393// BACnetLogRecord (Clause 12.20.5 -- log_buffer of TrendLog)
394// ---------------------------------------------------------------------------
395
396/// A single record stored in a TrendLog object's log buffer.
397///
398/// Contains a timestamp (date + time), the logged datum, and optional
399/// status flags that were in effect at logging time.
400#[derive(Debug, Clone, PartialEq)]
401pub struct BACnetLogRecord {
402 /// The date at which this record was logged.
403 pub date: Date,
404 /// The time at which this record was logged.
405 pub time: Time,
406 /// The logged datum.
407 pub log_datum: LogDatum,
408 /// Optional status flags at time of logging (4-bit BACnet StatusFlags).
409 pub status_flags: Option<u8>,
410}
411
412// ---------------------------------------------------------------------------
413// BACnetScale (Clause 21)
414// ---------------------------------------------------------------------------
415
416/// BACnet Scale: CHOICE { float-scale [0] Real, integer-scale [1] Integer }.
417#[derive(Debug, Clone, PartialEq)]
418pub enum BACnetScale {
419 FloatScale(f32),
420 IntegerScale(i32),
421}
422
423// ---------------------------------------------------------------------------
424// BACnetPrescale (Clause 21)
425// ---------------------------------------------------------------------------
426
427/// BACnet Prescale: SEQUENCE { multiplier Unsigned, modulo-divide Unsigned }.
428#[derive(Debug, Clone, PartialEq, Eq)]
429pub struct BACnetPrescale {
430 pub multiplier: u32,
431 pub modulo_divide: u32,
432}
433
434// ---------------------------------------------------------------------------
435// BACnetPropertyStates (Clause 21)
436// ---------------------------------------------------------------------------
437
438/// BACnet Property States — CHOICE type with 40+ variants.
439/// We represent common variants typed, uncommon as raw bytes.
440#[derive(Debug, Clone, PartialEq)]
441pub enum BACnetPropertyStates {
442 BooleanValue(bool), // [0]
443 BinaryValue(u32), // [1] BACnetBinaryPV
444 EventType(u32), // [2]
445 Polarity(u32), // [3]
446 ProgramChange(u32), // [4]
447 ProgramState(u32), // [5]
448 ReasonForHalt(u32), // [6]
449 Reliability(u32), // [7]
450 State(u32), // [8] BACnetEventState
451 SystemStatus(u32), // [9]
452 Units(u32), // [10]
453 LifeSafetyMode(u32), // [12]
454 LifeSafetyState(u32), // [13]
455 /// Catch-all for uncommon variants.
456 Other {
457 tag: u8,
458 data: Vec<u8>,
459 },
460}
461
462// ---------------------------------------------------------------------------
463// BACnetShedLevel (Clause 12 — used by LoadControl)
464// ---------------------------------------------------------------------------
465
466/// BACnet ShedLevel — CHOICE for LoadControl.
467#[derive(Debug, Clone, PartialEq)]
468pub enum BACnetShedLevel {
469 /// Shed level as a percentage (0–100).
470 Percent(u32),
471 /// Shed level as an abstract level value.
472 Level(u32),
473 /// Shed level as a floating-point amount.
474 Amount(f32),
475}
476
477// ---------------------------------------------------------------------------
478// BACnetLightingCommand (Clause 21 -- used by LightingOutput)
479// ---------------------------------------------------------------------------
480
481/// BACnet Lighting Command -- controls lighting operations.
482///
483/// Per ASHRAE 135-2020 Clause 21, this type is used by the LightingOutput
484/// object's LIGHTING_COMMAND property to specify a lighting operation
485/// (e.g., fade, ramp, step) with optional parameters.
486#[derive(Debug, Clone, PartialEq)]
487pub struct BACnetLightingCommand {
488 /// The lighting operation (LightingOperation enum raw value).
489 pub operation: u32,
490 /// Optional target brightness level (0.0 to 100.0 percent).
491 pub target_level: Option<f32>,
492 /// Optional ramp rate (percent per second).
493 pub ramp_rate: Option<f32>,
494 /// Optional step increment (percent).
495 pub step_increment: Option<f32>,
496 /// Optional fade time (milliseconds).
497 pub fade_time: Option<u32>,
498 /// Optional priority (1-16).
499 pub priority: Option<u32>,
500}
501
502// ---------------------------------------------------------------------------
503// BACnetDeviceObjectReference (Clause 21 -- used by Access Control objects)
504// ---------------------------------------------------------------------------
505
506/// BACnet Device Object Reference (simplified).
507///
508/// References an object, optionally on a specific device. Used by access
509/// control objects (e.g., BACnetAccessRule location).
510#[derive(Debug, Clone, PartialEq, Eq)]
511pub struct BACnetDeviceObjectReference {
512 /// Optional device identifier (None = local device).
513 pub device_identifier: Option<ObjectIdentifier>,
514 /// The object being referenced.
515 pub object_identifier: ObjectIdentifier,
516}
517
518// ---------------------------------------------------------------------------
519// BACnetAccessRule (Clause 12 -- used by AccessRights object)
520// ---------------------------------------------------------------------------
521
522/// BACnet Access Rule for access control objects.
523///
524/// Specifies a time range and location with an enable/disable flag,
525/// used in positive and negative access rules lists.
526#[derive(Debug, Clone, PartialEq)]
527pub struct BACnetAccessRule {
528 /// Time range specifier: 0 = specified, 1 = always.
529 pub time_range_specifier: u32,
530 /// Optional time range (start date, start time, end date, end time).
531 /// Present only when `time_range_specifier` is 0 (specified).
532 pub time_range: Option<(Date, Time, Date, Time)>,
533 /// Location specifier: 0 = specified, 1 = all.
534 pub location_specifier: u32,
535 /// Optional location reference. Present only when `location_specifier` is 0 (specified).
536 pub location: Option<BACnetDeviceObjectReference>,
537 /// Whether access is enabled or disabled by this rule.
538 pub enable: bool,
539}
540
541// ---------------------------------------------------------------------------
542// BACnetAssignedAccessRights (Clause 12 -- used by AccessCredential/AccessUser)
543// ---------------------------------------------------------------------------
544
545/// BACnet Assigned Access Rights.
546///
547/// Associates a reference to an AccessRights object with an enable flag.
548#[derive(Debug, Clone, PartialEq, Eq)]
549pub struct BACnetAssignedAccessRights {
550 /// Reference to an AccessRights object.
551 pub assigned_access_rights: ObjectIdentifier,
552 /// Whether these access rights are currently enabled.
553 pub enable: bool,
554}
555
556// ---------------------------------------------------------------------------
557// BACnetAssignedLandingCalls (Clause 12 -- used by ElevatorGroup)
558// ---------------------------------------------------------------------------
559
560/// BACnet Assigned Landing Calls for elevator group.
561#[derive(Debug, Clone, PartialEq)]
562pub struct BACnetAssignedLandingCalls {
563 /// The floor number for this landing call.
564 pub floor_number: u8,
565 /// Direction: 0=up, 1=down, 2=unknown.
566 pub direction: u32,
567}
568
569// ---------------------------------------------------------------------------
570// FaultParameters (Clause 12.12.50)
571// ---------------------------------------------------------------------------
572
573/// Fault parameter variants for configuring fault detection algorithms.
574#[derive(Debug, Clone, PartialEq)]
575pub enum FaultParameters {
576 /// No fault detection.
577 FaultNone,
578 /// Fault on characterstring match.
579 FaultCharacterString { fault_values: Vec<String> },
580 /// Vendor-defined fault algorithm.
581 FaultExtended {
582 vendor_id: u16,
583 extended_fault_type: u32,
584 parameters: Vec<u8>,
585 },
586 /// Fault on life safety state match.
587 FaultLifeSafety {
588 fault_values: Vec<u32>,
589 mode_for_reference: BACnetDeviceObjectPropertyReference,
590 },
591 /// Fault on property state match.
592 FaultState {
593 fault_values: Vec<BACnetPropertyStates>,
594 },
595 /// Fault on status flags change.
596 FaultStatusFlags {
597 reference: BACnetDeviceObjectPropertyReference,
598 },
599 /// Fault when value exceeds range.
600 FaultOutOfRange { min_normal: f64, max_normal: f64 },
601 /// Fault from listed reference.
602 FaultListed {
603 reference: BACnetDeviceObjectPropertyReference,
604 },
605}
606
607// ---------------------------------------------------------------------------
608// BACnetRecipientProcess (Clause 21)
609// ---------------------------------------------------------------------------
610
611/// BACnet Recipient Process — a recipient with an associated process identifier.
612#[derive(Debug, Clone, PartialEq)]
613pub struct BACnetRecipientProcess {
614 pub recipient: BACnetRecipient,
615 pub process_identifier: u32,
616}
617
618// ---------------------------------------------------------------------------
619// BACnetCOVSubscription (Clause 21)
620// ---------------------------------------------------------------------------
621
622/// BACnet COV Subscription — represents an active COV subscription.
623///
624/// Per Clause 12.11.40, `monitored_property_reference` is a
625/// `BACnetObjectPropertyReference` (object + property + optional index).
626#[derive(Debug, Clone, PartialEq)]
627pub struct BACnetCOVSubscription {
628 pub recipient: BACnetRecipientProcess,
629 pub monitored_property_reference: BACnetObjectPropertyReference,
630 pub issue_confirmed_notifications: bool,
631 pub time_remaining: u32,
632 pub cov_increment: Option<f32>,
633}
634
635// ---------------------------------------------------------------------------
636// BACnetValueSource (Clause 21)
637// ---------------------------------------------------------------------------
638
639/// BACnet Value Source — identifies the source of a property value write.
640#[derive(Debug, Clone, PartialEq)]
641pub enum BACnetValueSource {
642 None,
643 Object(ObjectIdentifier),
644 Address(BACnetAddress),
645}
646
647// ---------------------------------------------------------------------------
648// Tests
649// ---------------------------------------------------------------------------
650
651#[cfg(test)]
652mod tests {
653 use super::*;
654 use crate::enums::{ObjectType, PropertyIdentifier};
655
656 // --- BACnetDateRange ---
657
658 #[test]
659 fn date_range_encode_decode_round_trip() {
660 let range = BACnetDateRange {
661 start_date: Date {
662 year: 124,
663 month: 1,
664 day: 1,
665 day_of_week: 1,
666 },
667 end_date: Date {
668 year: 124,
669 month: 12,
670 day: 31,
671 day_of_week: 2,
672 },
673 };
674 let encoded = range.encode();
675 assert_eq!(encoded.len(), 8);
676 let decoded = BACnetDateRange::decode(&encoded).unwrap();
677 assert_eq!(range, decoded);
678 }
679
680 #[test]
681 fn date_range_encode_decode_all_unspecified() {
682 let range = BACnetDateRange {
683 start_date: Date {
684 year: Date::UNSPECIFIED,
685 month: Date::UNSPECIFIED,
686 day: Date::UNSPECIFIED,
687 day_of_week: Date::UNSPECIFIED,
688 },
689 end_date: Date {
690 year: Date::UNSPECIFIED,
691 month: Date::UNSPECIFIED,
692 day: Date::UNSPECIFIED,
693 day_of_week: Date::UNSPECIFIED,
694 },
695 };
696 let encoded = range.encode();
697 let decoded = BACnetDateRange::decode(&encoded).unwrap();
698 assert_eq!(range, decoded);
699 }
700
701 #[test]
702 fn date_range_buffer_too_short() {
703 // 7 bytes — one short
704 let result = BACnetDateRange::decode(&[0; 7]);
705 assert!(result.is_err());
706 match result.unwrap_err() {
707 Error::BufferTooShort { need, have } => {
708 assert_eq!(need, 8);
709 assert_eq!(have, 7);
710 }
711 other => panic!("unexpected error: {other:?}"),
712 }
713 }
714
715 #[test]
716 fn date_range_buffer_empty() {
717 let result = BACnetDateRange::decode(&[]);
718 assert!(result.is_err());
719 }
720
721 #[test]
722 fn date_range_extra_bytes_ignored() {
723 let range = BACnetDateRange {
724 start_date: Date {
725 year: 100,
726 month: 6,
727 day: 15,
728 day_of_week: 5,
729 },
730 end_date: Date {
731 year: 100,
732 month: 6,
733 day: 30,
734 day_of_week: 6,
735 },
736 };
737 let encoded = range.encode();
738 let mut extended = encoded.to_vec();
739 extended.extend_from_slice(&[0xFF, 0xFF]); // extra bytes
740 let decoded = BACnetDateRange::decode(&extended).unwrap();
741 assert_eq!(range, decoded);
742 }
743
744 // --- BACnetWeekNDay ---
745
746 #[test]
747 fn week_n_day_encode_decode_round_trip() {
748 let wnd = BACnetWeekNDay {
749 month: 3,
750 week_of_month: 2,
751 day_of_week: 5, // Friday
752 };
753 let encoded = wnd.encode();
754 assert_eq!(encoded.len(), 3);
755 let decoded = BACnetWeekNDay::decode(&encoded).unwrap();
756 assert_eq!(wnd, decoded);
757 }
758
759 #[test]
760 fn week_n_day_encode_decode_all_any() {
761 let wnd = BACnetWeekNDay {
762 month: BACnetWeekNDay::ANY,
763 week_of_month: BACnetWeekNDay::ANY,
764 day_of_week: BACnetWeekNDay::ANY,
765 };
766 let encoded = wnd.encode();
767 assert_eq!(encoded, [0xFF, 0xFF, 0xFF]);
768 let decoded = BACnetWeekNDay::decode(&encoded).unwrap();
769 assert_eq!(wnd, decoded);
770 }
771
772 #[test]
773 fn week_n_day_buffer_too_short() {
774 // 2 bytes — one short
775 let result = BACnetWeekNDay::decode(&[0x03, 0x02]);
776 assert!(result.is_err());
777 match result.unwrap_err() {
778 Error::BufferTooShort { need, have } => {
779 assert_eq!(need, 3);
780 assert_eq!(have, 2);
781 }
782 other => panic!("unexpected error: {other:?}"),
783 }
784 }
785
786 #[test]
787 fn week_n_day_buffer_empty() {
788 let result = BACnetWeekNDay::decode(&[]);
789 assert!(result.is_err());
790 }
791
792 // --- BACnetObjectPropertyReference ---
793
794 #[test]
795 fn object_property_reference_basic_construction() {
796 let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
797 let opr = BACnetObjectPropertyReference::new(oid, 85); // prop 85 = present-value
798 assert_eq!(opr.object_identifier, oid);
799 assert_eq!(opr.property_identifier, 85);
800 assert_eq!(opr.property_array_index, None);
801 }
802
803 #[test]
804 fn object_property_reference_with_index() {
805 let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
806 let opr = BACnetObjectPropertyReference::new_indexed(oid, 85, 3);
807 assert_eq!(opr.property_array_index, Some(3));
808 }
809
810 #[test]
811 fn object_property_reference_equality() {
812 let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 5).unwrap();
813 let a = BACnetObjectPropertyReference::new(oid, 85);
814 let b = BACnetObjectPropertyReference::new(oid, 85);
815 assert_eq!(a, b);
816
817 let c = BACnetObjectPropertyReference::new(oid, 77); // different property
818 assert_ne!(a, c);
819 }
820
821 // --- BACnetDeviceObjectPropertyReference ---
822
823 #[test]
824 fn device_object_property_reference_local() {
825 let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 10).unwrap();
826 let dopr = BACnetDeviceObjectPropertyReference::new_local(oid, 85);
827 assert_eq!(dopr.object_identifier, oid);
828 assert_eq!(dopr.property_identifier, 85);
829 assert_eq!(dopr.property_array_index, None);
830 assert_eq!(dopr.device_identifier, None);
831 }
832
833 #[test]
834 fn device_object_property_reference_remote() {
835 let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 10).unwrap();
836 let dev_oid = ObjectIdentifier::new(ObjectType::DEVICE, 1234).unwrap();
837 let dopr = BACnetDeviceObjectPropertyReference::new_remote(oid, 85, dev_oid);
838 assert_eq!(dopr.device_identifier, Some(dev_oid));
839 }
840
841 #[test]
842 fn device_object_property_reference_with_index() {
843 let oid = ObjectIdentifier::new(ObjectType::MULTI_STATE_INPUT, 3).unwrap();
844 let dopr = BACnetDeviceObjectPropertyReference::new_local(oid, 74).with_index(2); // prop 74 = state-text
845 assert_eq!(dopr.property_array_index, Some(2));
846 assert_eq!(dopr.device_identifier, None);
847 }
848
849 // --- BACnetAddress ---
850
851 #[test]
852 fn bacnet_address_local_broadcast() {
853 let addr = BACnetAddress::local_broadcast();
854 assert_eq!(addr.network_number, 0);
855 assert!(addr.mac_address.is_empty());
856 }
857
858 #[test]
859 fn bacnet_address_from_ip() {
860 let ip_port: [u8; 6] = [192, 168, 1, 100, 0xBA, 0xC0]; // 192.168.1.100:47808
861 let addr = BACnetAddress::from_ip(ip_port);
862 assert_eq!(addr.network_number, 0);
863 assert_eq!(addr.mac_address.as_slice(), &ip_port);
864 }
865
866 // --- BACnetRecipient ---
867
868 #[test]
869 fn bacnet_recipient_device_variant() {
870 let dev_oid = ObjectIdentifier::new(ObjectType::DEVICE, 42).unwrap();
871 let recipient = BACnetRecipient::Device(dev_oid);
872 match recipient {
873 BACnetRecipient::Device(oid) => assert_eq!(oid.instance_number(), 42),
874 BACnetRecipient::Address(_) => panic!("wrong variant"),
875 }
876 }
877
878 #[test]
879 fn bacnet_recipient_address_variant() {
880 let addr = BACnetAddress {
881 network_number: 100,
882 mac_address: MacAddr::from_slice(&[0x01, 0x02, 0x03]),
883 };
884 let recipient = BACnetRecipient::Address(addr.clone());
885 match recipient {
886 BACnetRecipient::Device(_) => panic!("wrong variant"),
887 BACnetRecipient::Address(a) => assert_eq!(a, addr),
888 }
889 }
890
891 // --- BACnetDestination ---
892
893 #[test]
894 fn bacnet_destination_construction() {
895 let dev_oid = ObjectIdentifier::new(ObjectType::DEVICE, 99).unwrap();
896 let dest = BACnetDestination {
897 valid_days: 0b0111_1111, // all days
898 from_time: Time {
899 hour: 0,
900 minute: 0,
901 second: 0,
902 hundredths: 0,
903 },
904 to_time: Time {
905 hour: 23,
906 minute: 59,
907 second: 59,
908 hundredths: 99,
909 },
910 recipient: BACnetRecipient::Device(dev_oid),
911 process_identifier: 1,
912 issue_confirmed_notifications: true,
913 transitions: 0b0000_0111, // all transitions
914 };
915 assert_eq!(dest.valid_days & 0x7F, 0x7F);
916 assert!(dest.issue_confirmed_notifications);
917 assert_eq!(dest.transitions & 0x07, 0x07);
918 }
919
920 // --- LogDatum ---
921
922 #[test]
923 fn log_datum_variants_clone_eq() {
924 let real = LogDatum::RealValue(72.5_f32);
925 assert_eq!(real.clone(), LogDatum::RealValue(72.5_f32));
926
927 let bits = LogDatum::BitstringValue {
928 unused_bits: 3,
929 data: vec![0b1010_0000],
930 };
931 assert_eq!(bits.clone(), bits);
932
933 let fail = LogDatum::Failure {
934 error_class: 2,
935 error_code: 31,
936 };
937 assert_eq!(fail.clone(), fail);
938
939 assert_eq!(LogDatum::NullValue, LogDatum::NullValue);
940 assert_ne!(LogDatum::BooleanValue(true), LogDatum::BooleanValue(false));
941 }
942
943 // --- BACnetLogRecord ---
944
945 #[test]
946 fn log_record_construction() {
947 let record = BACnetLogRecord {
948 date: Date {
949 year: 124,
950 month: 3,
951 day: 15,
952 day_of_week: 5,
953 },
954 time: Time {
955 hour: 10,
956 minute: 30,
957 second: 0,
958 hundredths: 0,
959 },
960 log_datum: LogDatum::RealValue(23.4_f32),
961 status_flags: None,
962 };
963 assert_eq!(record.date.year, 124);
964 assert_eq!(record.status_flags, None);
965 }
966
967 #[test]
968 fn log_record_with_status_flags() {
969 let record = BACnetLogRecord {
970 date: Date {
971 year: 124,
972 month: 1,
973 day: 1,
974 day_of_week: 1,
975 },
976 time: Time {
977 hour: 0,
978 minute: 0,
979 second: 0,
980 hundredths: 0,
981 },
982 log_datum: LogDatum::LogStatus(0b010), // buffer-purged
983 status_flags: Some(0b0100), // FAULT set
984 };
985 assert_eq!(record.status_flags, Some(0b0100));
986 match record.log_datum {
987 LogDatum::LogStatus(s) => assert_eq!(s, 0b010),
988 _ => panic!("wrong datum variant"),
989 }
990 }
991
992 // --- BACnetCalendarEntry ---
993
994 #[test]
995 fn calendar_entry_variants() {
996 let date_entry = BACnetCalendarEntry::Date(Date {
997 year: 124,
998 month: 6,
999 day: 15,
1000 day_of_week: 6,
1001 });
1002 let range_entry = BACnetCalendarEntry::DateRange(BACnetDateRange {
1003 start_date: Date {
1004 year: 124,
1005 month: 1,
1006 day: 1,
1007 day_of_week: 1,
1008 },
1009 end_date: Date {
1010 year: 124,
1011 month: 12,
1012 day: 31,
1013 day_of_week: 2,
1014 },
1015 });
1016 let wnd_entry = BACnetCalendarEntry::WeekNDay(BACnetWeekNDay {
1017 month: BACnetWeekNDay::ANY,
1018 week_of_month: 1,
1019 day_of_week: 1, // first Monday of every month
1020 });
1021 // Just verify they can be constructed and cloned
1022 let _a = date_entry.clone();
1023 let _b = range_entry.clone();
1024 let _c = wnd_entry.clone();
1025 }
1026
1027 // --- BACnetSpecialEvent ---
1028
1029 #[test]
1030 fn special_event_inline_calendar_entry() {
1031 let event = BACnetSpecialEvent {
1032 period: SpecialEventPeriod::CalendarEntry(BACnetCalendarEntry::WeekNDay(
1033 BACnetWeekNDay {
1034 month: 12,
1035 week_of_month: BACnetWeekNDay::ANY,
1036 day_of_week: BACnetWeekNDay::ANY,
1037 },
1038 )),
1039 list_of_time_values: vec![BACnetTimeValue {
1040 time: Time {
1041 hour: 8,
1042 minute: 0,
1043 second: 0,
1044 hundredths: 0,
1045 },
1046 value: vec![0x10, 0x00], // raw-tagged Null
1047 }],
1048 event_priority: 16, // lowest priority
1049 };
1050 assert_eq!(event.event_priority, 16);
1051 assert_eq!(event.list_of_time_values.len(), 1);
1052 }
1053
1054 #[test]
1055 fn special_event_calendar_reference() {
1056 let cal_oid = ObjectIdentifier::new(ObjectType::CALENDAR, 0).unwrap();
1057 let event = BACnetSpecialEvent {
1058 period: SpecialEventPeriod::CalendarReference(cal_oid),
1059 list_of_time_values: vec![],
1060 event_priority: 1, // highest priority
1061 };
1062 match &event.period {
1063 SpecialEventPeriod::CalendarReference(oid) => {
1064 assert_eq!(oid.instance_number(), 0);
1065 }
1066 SpecialEventPeriod::CalendarEntry(_) => panic!("wrong period variant"),
1067 }
1068 }
1069
1070 // --- BACnetRecipientProcess ---
1071
1072 #[test]
1073 fn recipient_process_construction() {
1074 let dev_oid = ObjectIdentifier::new(ObjectType::DEVICE, 100).unwrap();
1075 let rp = BACnetRecipientProcess {
1076 recipient: BACnetRecipient::Device(dev_oid),
1077 process_identifier: 42,
1078 };
1079 assert_eq!(rp.process_identifier, 42);
1080 match &rp.recipient {
1081 BACnetRecipient::Device(oid) => assert_eq!(oid.instance_number(), 100),
1082 BACnetRecipient::Address(_) => panic!("wrong variant"),
1083 }
1084 }
1085
1086 // --- BACnetCOVSubscription ---
1087
1088 #[test]
1089 fn cov_subscription_creation() {
1090 let dev_oid = ObjectIdentifier::new(ObjectType::DEVICE, 200).unwrap();
1091 let ai_oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
1092 let sub = BACnetCOVSubscription {
1093 recipient: BACnetRecipientProcess {
1094 recipient: BACnetRecipient::Device(dev_oid),
1095 process_identifier: 7,
1096 },
1097 monitored_property_reference: BACnetObjectPropertyReference::new(
1098 ai_oid,
1099 PropertyIdentifier::PRESENT_VALUE.to_raw(),
1100 ),
1101 issue_confirmed_notifications: true,
1102 time_remaining: 300,
1103 cov_increment: Some(0.5),
1104 };
1105 assert_eq!(sub.recipient.process_identifier, 7);
1106 assert_eq!(
1107 sub.monitored_property_reference
1108 .object_identifier
1109 .instance_number(),
1110 1
1111 );
1112 assert!(sub.issue_confirmed_notifications);
1113 assert_eq!(sub.time_remaining, 300);
1114 assert_eq!(sub.cov_increment, Some(0.5));
1115 }
1116
1117 #[test]
1118 fn cov_subscription_without_increment() {
1119 let dev_oid = ObjectIdentifier::new(ObjectType::DEVICE, 50).unwrap();
1120 let bv_oid = ObjectIdentifier::new(ObjectType::BINARY_VALUE, 3).unwrap();
1121 let sub = BACnetCOVSubscription {
1122 recipient: BACnetRecipientProcess {
1123 recipient: BACnetRecipient::Device(dev_oid),
1124 process_identifier: 1,
1125 },
1126 monitored_property_reference: BACnetObjectPropertyReference::new(
1127 bv_oid,
1128 PropertyIdentifier::PRESENT_VALUE.to_raw(),
1129 ),
1130 issue_confirmed_notifications: false,
1131 time_remaining: 0,
1132 cov_increment: None,
1133 };
1134 assert!(!sub.issue_confirmed_notifications);
1135 assert_eq!(sub.cov_increment, None);
1136 }
1137
1138 // --- BACnetValueSource ---
1139
1140 #[test]
1141 fn value_source_none_variant() {
1142 let vs = BACnetValueSource::None;
1143 assert_eq!(vs, BACnetValueSource::None);
1144 }
1145
1146 #[test]
1147 fn value_source_object_variant() {
1148 let dev_oid = ObjectIdentifier::new(ObjectType::DEVICE, 1).unwrap();
1149 let vs = BACnetValueSource::Object(dev_oid);
1150 match vs {
1151 BACnetValueSource::Object(oid) => assert_eq!(oid.instance_number(), 1),
1152 _ => panic!("wrong variant"),
1153 }
1154 }
1155
1156 #[test]
1157 fn value_source_address_variant() {
1158 let addr = BACnetAddress::from_ip([192, 168, 1, 10, 0xBA, 0xC0]);
1159 let vs = BACnetValueSource::Address(addr.clone());
1160 match vs {
1161 BACnetValueSource::Address(a) => assert_eq!(a, addr),
1162 _ => panic!("wrong variant"),
1163 }
1164 }
1165}