Skip to main content

bacnet_services/
device_mgmt.rs

1//! Device management services per ASHRAE 135-2020 Clauses 15-16.
2//!
3//! - DeviceCommunicationControl (Clause 15.4)
4//! - ReinitializeDevice (Clause 15.4)
5//! - TimeSynchronization (Clause 16.10)
6//! - UTCTimeSynchronization (Clause 16.10)
7
8use bacnet_encoding::primitives;
9use bacnet_encoding::tags;
10use bacnet_types::enums::{EnableDisable, ReinitializedState};
11use bacnet_types::error::Error;
12use bacnet_types::primitives::{Date, Time};
13use bytes::BytesMut;
14
15// ---------------------------------------------------------------------------
16// DeviceCommunicationControlRequest
17// ---------------------------------------------------------------------------
18
19/// DeviceCommunicationControl-Request service parameters.
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct DeviceCommunicationControlRequest {
22    pub time_duration: Option<u16>,
23    pub enable_disable: EnableDisable,
24    pub password: Option<String>,
25}
26
27impl DeviceCommunicationControlRequest {
28    pub fn encode(&self, buf: &mut BytesMut) -> Result<(), Error> {
29        // [0] time-duration (optional)
30        if let Some(dur) = self.time_duration {
31            primitives::encode_ctx_unsigned(buf, 0, dur as u64);
32        }
33        // [1] enable-disable
34        primitives::encode_ctx_enumerated(buf, 1, self.enable_disable.to_raw());
35        // [2] password (optional)
36        if let Some(ref pw) = self.password {
37            primitives::encode_ctx_character_string(buf, 2, pw)?;
38        }
39        Ok(())
40    }
41
42    pub fn decode(data: &[u8]) -> Result<Self, Error> {
43        let mut offset = 0;
44
45        // [0] time-duration (optional)
46        let mut time_duration = None;
47        let (opt_data, new_offset) = tags::decode_optional_context(data, offset, 0)?;
48        if let Some(content) = opt_data {
49            time_duration = Some(primitives::decode_unsigned(content)? as u16);
50            offset = new_offset;
51        }
52
53        // [1] enable-disable
54        let (tag, pos) = tags::decode_tag(data, offset)?;
55        let end = pos + tag.length as usize;
56        if end > data.len() {
57            return Err(Error::decoding(pos, "DCC truncated at enable-disable"));
58        }
59        let enable_disable =
60            EnableDisable::from_raw(primitives::decode_unsigned(&data[pos..end])? as u32);
61        offset = end;
62
63        // [2] password (optional, max 20 characters)
64        let mut password = None;
65        if offset < data.len() {
66            let (opt_data, _new_offset) = tags::decode_optional_context(data, offset, 2)?;
67            if let Some(content) = opt_data {
68                let s = primitives::decode_character_string(content)?;
69                if s.len() > 20 {
70                    return Err(Error::Encoding("DCC password exceeds 20 characters".into()));
71                }
72                password = Some(s);
73            }
74        }
75
76        Ok(Self {
77            time_duration,
78            enable_disable,
79            password,
80        })
81    }
82}
83
84// ---------------------------------------------------------------------------
85// ReinitializeDeviceRequest
86// ---------------------------------------------------------------------------
87
88/// ReinitializeDevice-Request service parameters.
89#[derive(Debug, Clone, PartialEq, Eq)]
90pub struct ReinitializeDeviceRequest {
91    pub reinitialized_state: ReinitializedState,
92    pub password: Option<String>,
93}
94
95impl ReinitializeDeviceRequest {
96    pub fn encode(&self, buf: &mut BytesMut) -> Result<(), Error> {
97        // [0] reinitialized-state
98        primitives::encode_ctx_enumerated(buf, 0, self.reinitialized_state.to_raw());
99        // [1] password (optional)
100        if let Some(ref pw) = self.password {
101            primitives::encode_ctx_character_string(buf, 1, pw)?;
102        }
103        Ok(())
104    }
105
106    pub fn decode(data: &[u8]) -> Result<Self, Error> {
107        let mut offset = 0;
108
109        // [0] reinitialized-state
110        let (tag, pos) = tags::decode_tag(data, offset)?;
111        let end = pos + tag.length as usize;
112        if end > data.len() {
113            return Err(Error::decoding(pos, "Reinitialize truncated at state"));
114        }
115        let reinitialized_state =
116            ReinitializedState::from_raw(primitives::decode_unsigned(&data[pos..end])? as u32);
117        offset = end;
118
119        // [1] password (optional)
120        let mut password = None;
121        if offset < data.len() {
122            let (opt_data, _new_offset) = tags::decode_optional_context(data, offset, 1)?;
123            if let Some(content) = opt_data {
124                let s = primitives::decode_character_string(content)?;
125                if s.len() > 20 {
126                    return Err(Error::decoding(
127                        offset,
128                        "ReinitializeDevice password exceeds 20 characters",
129                    ));
130                }
131                password = Some(s);
132            }
133        }
134
135        Ok(Self {
136            reinitialized_state,
137            password,
138        })
139    }
140}
141
142// ---------------------------------------------------------------------------
143// TimeSynchronizationRequest
144// ---------------------------------------------------------------------------
145
146/// TimeSynchronization-Request service parameters (APPLICATION-tagged).
147///
148/// Used for both TimeSynchronization and UTCTimeSynchronization.
149#[derive(Debug, Clone, PartialEq, Eq)]
150pub struct TimeSynchronizationRequest {
151    pub date: Date,
152    pub time: Time,
153}
154
155impl TimeSynchronizationRequest {
156    pub fn encode(&self, buf: &mut BytesMut) {
157        primitives::encode_app_date(buf, &self.date);
158        primitives::encode_app_time(buf, &self.time);
159    }
160
161    pub fn decode(data: &[u8]) -> Result<Self, Error> {
162        let mut offset = 0;
163
164        let (tag, pos) = tags::decode_tag(data, offset)?;
165        let end = pos + tag.length as usize;
166        if end > data.len() {
167            return Err(Error::decoding(pos, "TimeSync truncated at date"));
168        }
169        let date = Date::decode(&data[pos..end])?;
170        offset = end;
171
172        let (tag, pos) = tags::decode_tag(data, offset)?;
173        let end = pos + tag.length as usize;
174        if end > data.len() {
175            return Err(Error::decoding(pos, "TimeSync truncated at time"));
176        }
177        let time = Time::decode(&data[pos..end])?;
178
179        Ok(Self { date, time })
180    }
181}
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186
187    #[test]
188    fn dcc_round_trip() {
189        let req = DeviceCommunicationControlRequest {
190            time_duration: Some(60),
191            enable_disable: EnableDisable::DISABLE,
192            password: Some("secret".into()),
193        };
194        let mut buf = BytesMut::new();
195        req.encode(&mut buf).unwrap();
196        let decoded = DeviceCommunicationControlRequest::decode(&buf).unwrap();
197        assert_eq!(req, decoded);
198    }
199
200    #[test]
201    fn dcc_no_optionals() {
202        let req = DeviceCommunicationControlRequest {
203            time_duration: None,
204            enable_disable: EnableDisable::ENABLE,
205            password: None,
206        };
207        let mut buf = BytesMut::new();
208        req.encode(&mut buf).unwrap();
209        let decoded = DeviceCommunicationControlRequest::decode(&buf).unwrap();
210        assert_eq!(req, decoded);
211    }
212
213    #[test]
214    fn reinitialize_round_trip() {
215        let req = ReinitializeDeviceRequest {
216            reinitialized_state: ReinitializedState::WARMSTART,
217            password: Some("admin".into()),
218        };
219        let mut buf = BytesMut::new();
220        req.encode(&mut buf).unwrap();
221        let decoded = ReinitializeDeviceRequest::decode(&buf).unwrap();
222        assert_eq!(req, decoded);
223    }
224
225    #[test]
226    fn time_sync_round_trip() {
227        let req = TimeSynchronizationRequest {
228            date: Date {
229                year: 124,
230                month: 6,
231                day: 15,
232                day_of_week: 6,
233            },
234            time: Time {
235                hour: 14,
236                minute: 30,
237                second: 0,
238                hundredths: 0,
239            },
240        };
241        let mut buf = BytesMut::new();
242        req.encode(&mut buf);
243        let decoded = TimeSynchronizationRequest::decode(&buf).unwrap();
244        assert_eq!(req, decoded);
245    }
246
247    // -----------------------------------------------------------------------
248    // Malformed-input decode error tests
249    // -----------------------------------------------------------------------
250
251    #[test]
252    fn test_decode_dcc_empty_input() {
253        assert!(DeviceCommunicationControlRequest::decode(&[]).is_err());
254    }
255
256    #[test]
257    fn test_decode_dcc_truncated_1_byte() {
258        let req = DeviceCommunicationControlRequest {
259            time_duration: Some(60),
260            enable_disable: EnableDisable::DISABLE,
261            password: Some("secret".into()),
262        };
263        let mut buf = BytesMut::new();
264        req.encode(&mut buf).unwrap();
265        assert!(DeviceCommunicationControlRequest::decode(&buf[..1]).is_err());
266    }
267
268    #[test]
269    fn test_decode_dcc_truncated_3_bytes() {
270        let req = DeviceCommunicationControlRequest {
271            time_duration: Some(60),
272            enable_disable: EnableDisable::DISABLE,
273            password: Some("secret".into()),
274        };
275        let mut buf = BytesMut::new();
276        req.encode(&mut buf).unwrap();
277        assert!(DeviceCommunicationControlRequest::decode(&buf[..3]).is_err());
278    }
279
280    #[test]
281    fn test_decode_dcc_invalid_tag() {
282        assert!(DeviceCommunicationControlRequest::decode(&[0xFF, 0xFF, 0xFF]).is_err());
283    }
284
285    #[test]
286    fn test_decode_reinitialize_empty_input() {
287        assert!(ReinitializeDeviceRequest::decode(&[]).is_err());
288    }
289
290    #[test]
291    fn test_decode_reinitialize_truncated_1_byte() {
292        let req = ReinitializeDeviceRequest {
293            reinitialized_state: ReinitializedState::WARMSTART,
294            password: Some("admin".into()),
295        };
296        let mut buf = BytesMut::new();
297        req.encode(&mut buf).unwrap();
298        assert!(ReinitializeDeviceRequest::decode(&buf[..1]).is_err());
299    }
300
301    #[test]
302    fn test_decode_reinitialize_invalid_tag() {
303        assert!(ReinitializeDeviceRequest::decode(&[0xFF, 0xFF, 0xFF]).is_err());
304    }
305
306    #[test]
307    fn test_decode_time_sync_empty_input() {
308        assert!(TimeSynchronizationRequest::decode(&[]).is_err());
309    }
310
311    #[test]
312    fn test_decode_time_sync_truncated_1_byte() {
313        let req = TimeSynchronizationRequest {
314            date: Date {
315                year: 124,
316                month: 6,
317                day: 15,
318                day_of_week: 6,
319            },
320            time: Time {
321                hour: 14,
322                minute: 30,
323                second: 0,
324                hundredths: 0,
325            },
326        };
327        let mut buf = BytesMut::new();
328        req.encode(&mut buf);
329        assert!(TimeSynchronizationRequest::decode(&buf[..1]).is_err());
330    }
331
332    #[test]
333    fn test_decode_time_sync_truncated_3_bytes() {
334        let req = TimeSynchronizationRequest {
335            date: Date {
336                year: 124,
337                month: 6,
338                day: 15,
339                day_of_week: 6,
340            },
341            time: Time {
342                hour: 14,
343                minute: 30,
344                second: 0,
345                hundredths: 0,
346            },
347        };
348        let mut buf = BytesMut::new();
349        req.encode(&mut buf);
350        assert!(TimeSynchronizationRequest::decode(&buf[..3]).is_err());
351    }
352
353    #[test]
354    fn test_decode_time_sync_truncated_half() {
355        let req = TimeSynchronizationRequest {
356            date: Date {
357                year: 124,
358                month: 6,
359                day: 15,
360                day_of_week: 6,
361            },
362            time: Time {
363                hour: 14,
364                minute: 30,
365                second: 0,
366                hundredths: 0,
367            },
368        };
369        let mut buf = BytesMut::new();
370        req.encode(&mut buf);
371        let half = buf.len() / 2;
372        assert!(TimeSynchronizationRequest::decode(&buf[..half]).is_err());
373    }
374
375    #[test]
376    fn test_decode_time_sync_invalid_tag() {
377        assert!(TimeSynchronizationRequest::decode(&[0xFF, 0xFF, 0xFF]).is_err());
378    }
379}