Skip to main content

bacnet_encoding/constructed/
schedule.rs

1//! Clause 21 codecs for the Schedule object's constructed properties
2//! (Clause 12.24): Weekly_Schedule and Exception_Schedule.
3//!
4//! - `BACnetTimeValue` is a SEQUENCE of two untagged members, so it is an
5//!   application-tagged Time followed by one application-tagged primitive
6//!   value, with no frame. [`BACnetTimeValue::value`] holds that value typed.
7//! - `BACnetDailySchedule` is a SEQUENCE with one member, day-schedule `[0]`,
8//!   a SEQUENCE OF `BACnetTimeValue`: an opening/closing `[0]` pair around the
9//!   time-values. Weekly_Schedule is a BACnetARRAY\[7\] of them, Monday first.
10//! - `BACnetSpecialEvent` is three members with no outer frame:
11//!
12//! | Member              | Tag          | Encoding                                         |
13//! |---------------------|--------------|--------------------------------------------------|
14//! | period              | (untagged)   | CHOICE: calendar-entry `[0]` or calendar-reference `[1]` |
15//! | list-of-time-values | `[2]`        | opening/closing pair around the time-values      |
16//! | event-priority      | `[3]`        | primitive Unsigned, 1 through 16 when valid      |
17//!
18//! The calendar-entry alternative wraps a `BACnetCalendarEntry`, itself a
19//! CHOICE, so it is an opening/closing `[0]` pair around the entry's own
20//! tagged encoding (see [`super::calendar`]). The calendar-reference
21//! alternative is a primitive `[1]` Calendar object identifier.
22//! Exception_Schedule is a BACnetARRAY of special events, concatenated when
23//! read whole.
24//!
25//! These codecs check structure only, like the calendar-entry codec: a
26//! well-formed event with a priority outside 1 through 16 decodes, and the
27//! consumer range-checks it (#1087).
28
29#[cfg(not(feature = "std"))]
30use alloc::vec::Vec;
31
32use bacnet_types::constructed::{BACnetSpecialEvent, BACnetTimeValue, SpecialEventPeriod};
33use bacnet_types::error::Error;
34use bacnet_types::primitives::Time;
35use bytes::BytesMut;
36
37use super::calendar::{decode_calendar_entry, encode_calendar_entry};
38use super::tagged::{
39    contents, decode_ctx_object_id, decode_ctx_unsigned, expect_closing, expect_end,
40    expect_opening, next_is_closing,
41};
42use super::MAX_FRAMED_ITEMS;
43use crate::primitives;
44use crate::tags::{self, TagClass};
45
46/// Encode one `BACnetTimeValue`: the application-tagged Time, then the
47/// application-tagged value.
48///
49/// Refuses a value that is not of a primitive datatype (a `List` or
50/// `ApplicationData`), which no time-value can carry.
51pub fn encode_time_value(buf: &mut BytesMut, time_value: &BACnetTimeValue) -> Result<(), Error> {
52    if !time_value.value.is_primitive() {
53        return Err(Error::Encoding(
54            "time-value: the value must be of a primitive datatype".into(),
55        ));
56    }
57    primitives::encode_app_time(buf, &time_value.time);
58    primitives::encode_property_value(buf, &time_value.value)
59}
60
61/// Decode one `BACnetTimeValue` at `offset`.
62///
63/// The value must be a single application-tagged primitive. Returns the
64/// time-value and the offset just past it.
65pub fn decode_time_value(data: &[u8], offset: usize) -> Result<(BACnetTimeValue, usize), Error> {
66    let (tag, content) = tags::decode_tag(data, offset)?;
67    if tag.class != TagClass::Application || tag.number != tags::app_tag::TIME || tag.length != 4 {
68        return Err(Error::decoding(
69            offset,
70            "time-value: expected an application-tagged Time (4 octets)",
71        ));
72    }
73    let (octets, value_start) = contents(data, content, 4)?;
74    let time = Time::decode(octets)?;
75
76    let (tag, _) = tags::decode_tag(data, value_start)?;
77    if tag.class != TagClass::Application {
78        return Err(Error::decoding(
79            value_start,
80            format!(
81                "time-value: expected an application-tagged value, got context tag [{}]",
82                tag.number
83            ),
84        ));
85    }
86    let (value, value_end) = primitives::decode_application_value(data, value_start)?;
87    Ok((BACnetTimeValue { time, value }, value_end))
88}
89
90/// Encode a SEQUENCE OF `BACnetTimeValue` inside an opening/closing `tag` pair.
91fn encode_time_values(
92    buf: &mut BytesMut,
93    tag: u8,
94    time_values: &[BACnetTimeValue],
95) -> Result<(), Error> {
96    tags::encode_opening_tag(buf, tag);
97    for time_value in time_values {
98        encode_time_value(buf, time_value)?;
99    }
100    tags::encode_closing_tag(buf, tag);
101    Ok(())
102}
103
104/// Decode a SEQUENCE OF `BACnetTimeValue` framed by an opening/closing `tag`
105/// pair at `offset`; returns the time-values and the offset past the closing tag.
106fn decode_time_values(
107    data: &[u8],
108    offset: usize,
109    tag: u8,
110    what: &str,
111) -> Result<(Vec<BACnetTimeValue>, usize), Error> {
112    let mut pos = expect_opening(data, offset, tag, what)?;
113    let mut time_values = Vec::new();
114    loop {
115        if next_is_closing(data, pos, tag)? {
116            return Ok((time_values, expect_closing(data, pos, tag, what)?));
117        }
118        if time_values.len() >= MAX_FRAMED_ITEMS {
119            return Err(Error::overflow(
120                pos,
121                format!("{what}: time-values exceed item limit"),
122            ));
123        }
124        let (time_value, next) = decode_time_value(data, pos)?;
125        time_values.push(time_value);
126        pos = next;
127    }
128}
129
130/// Encode the period CHOICE of a `BACnetSpecialEvent`.
131pub fn encode_special_event_period(buf: &mut BytesMut, period: &SpecialEventPeriod) {
132    match period {
133        SpecialEventPeriod::CalendarEntry(entry) => {
134            tags::encode_opening_tag(buf, 0);
135            encode_calendar_entry(buf, entry);
136            tags::encode_closing_tag(buf, 0);
137        }
138        SpecialEventPeriod::CalendarReference(oid) => primitives::encode_ctx_object_id(buf, 1, oid),
139    }
140}
141
142/// Decode the period CHOICE of a `BACnetSpecialEvent` at `offset`.
143pub fn decode_special_event_period(
144    data: &[u8],
145    offset: usize,
146) -> Result<(SpecialEventPeriod, usize), Error> {
147    let (tag, content) = tags::decode_tag(data, offset)?;
148    if tag.is_opening_tag(0) {
149        let (entry, after) = decode_calendar_entry(data, content)?;
150        let end = expect_closing(data, after, 0, "special event calendar-entry")?;
151        return Ok((SpecialEventPeriod::CalendarEntry(entry), end));
152    }
153    if tag.is_context(1) {
154        let (oid, end) = decode_ctx_object_id(data, offset, 1, "special event calendar-reference")?;
155        return Ok((SpecialEventPeriod::CalendarReference(oid), end));
156    }
157    Err(Error::decoding(
158        offset,
159        "special event period requires calendar-entry [0] or calendar-reference [1]",
160    ))
161}
162
163/// Encode one `BACnetSpecialEvent`; fails only on a time-value that
164/// [`encode_time_value`] refuses. The event-priority goes out as held, in or
165/// out of its range.
166pub fn encode_special_event(buf: &mut BytesMut, event: &BACnetSpecialEvent) -> Result<(), Error> {
167    encode_special_event_period(buf, &event.period);
168    encode_time_values(buf, 2, &event.list_of_time_values)?;
169    primitives::encode_ctx_unsigned(buf, 3, event.event_priority);
170    Ok(())
171}
172
173/// Decode one `BACnetSpecialEvent` at `offset`; returns the event and the
174/// offset just past it.
175///
176/// The event-priority decodes as any Unsigned. Its 1 through 16 range is a
177/// property of the value, not of the encoding, so a consumer that stores or
178/// acts on the event checks it: the Schedule object refuses a priority out of
179/// range with VALUE_OUT_OF_RANGE (Clause 15.9.1.3), as it does for an inline
180/// calendar entry holding an octet out of range.
181pub fn decode_special_event(
182    data: &[u8],
183    offset: usize,
184) -> Result<(BACnetSpecialEvent, usize), Error> {
185    let (period, pos) = decode_special_event_period(data, offset)?;
186    let (list_of_time_values, pos) =
187        decode_time_values(data, pos, 2, "special event list-of-time-values")?;
188    let (event_priority, end) =
189        decode_ctx_unsigned::<u64>(data, pos, 3, "special event event-priority")?;
190    Ok((
191        BACnetSpecialEvent {
192            period,
193            list_of_time_values,
194            event_priority,
195        },
196        end,
197    ))
198}
199
200/// Encode one `BACnetDailySchedule`: its time-values inside `[0]`. Fails only
201/// on a time-value that [`encode_time_value`] refuses.
202pub fn encode_daily_schedule(
203    buf: &mut BytesMut,
204    time_values: &[BACnetTimeValue],
205) -> Result<(), Error> {
206    encode_time_values(buf, 0, time_values)
207}
208
209/// Decode one `BACnetDailySchedule` at `offset`; returns its time-values and
210/// the offset just past the closing `[0]`.
211pub fn decode_daily_schedule(
212    data: &[u8],
213    offset: usize,
214) -> Result<(Vec<BACnetTimeValue>, usize), Error> {
215    decode_time_values(data, offset, 0, "daily schedule")
216}
217
218/// Encode a whole Weekly_Schedule: seven daily schedules, Monday first.
219pub fn encode_weekly_schedule(
220    buf: &mut BytesMut,
221    days: &[Vec<BACnetTimeValue>; 7],
222) -> Result<(), Error> {
223    for day in days {
224        encode_daily_schedule(buf, day)?;
225    }
226    Ok(())
227}
228
229/// Decode a whole Weekly_Schedule.
230///
231/// The data must hold exactly seven daily schedules and nothing after them:
232/// the property is a fixed-size BACnetARRAY\[7\].
233pub fn decode_weekly_schedule(data: &[u8]) -> Result<[Vec<BACnetTimeValue>; 7], Error> {
234    let mut days: [Vec<BACnetTimeValue>; 7] = Default::default();
235    let mut pos = 0;
236    for (index, day) in days.iter_mut().enumerate() {
237        let (time_values, next) = decode_daily_schedule(data, pos)
238            .map_err(|e| Error::decoding(pos, format!("weekly schedule day {index}: {e}")))?;
239        *day = time_values;
240        pos = next;
241    }
242    expect_end(data, pos, pos, "weekly schedule after 7 daily schedules")?;
243    Ok(days)
244}
245
246/// Encode a whole Exception_Schedule: the special events, concatenated.
247pub fn encode_exception_schedule(
248    buf: &mut BytesMut,
249    events: &[BACnetSpecialEvent],
250) -> Result<(), Error> {
251    for event in events {
252        encode_special_event(buf, event)?;
253    }
254    Ok(())
255}
256
257/// Decode a whole Exception_Schedule: zero or more special events, back to
258/// back, with every byte belonging to one. Like [`decode_special_event`], it
259/// leaves each event-priority's range to the caller.
260pub fn decode_exception_schedule(data: &[u8]) -> Result<Vec<BACnetSpecialEvent>, Error> {
261    let mut events = Vec::new();
262    let mut pos = 0;
263    while pos < data.len() {
264        if events.len() >= MAX_FRAMED_ITEMS {
265            return Err(Error::overflow(
266                pos,
267                "exception schedule exceeds item limit",
268            ));
269        }
270        let (event, next) = decode_special_event(data, pos).map_err(|e| {
271            Error::decoding(
272                pos,
273                format!("exception schedule entry {}: {e}", events.len()),
274            )
275        })?;
276        events.push(event);
277        pos = next;
278    }
279    Ok(events)
280}