Skip to main content

bacnet_services/
text_message.rs

1//! ConfirmedTextMessage / UnconfirmedTextMessage services
2//! per ASHRAE 135-2020 Clauses 15.20 and 16.10.7.
3
4use bacnet_encoding::primitives;
5use bacnet_encoding::tags;
6use bacnet_types::enums::MessagePriority;
7use bacnet_types::error::Error;
8use bacnet_types::primitives::ObjectIdentifier;
9use bytes::BytesMut;
10
11// ---------------------------------------------------------------------------
12// MessageClass choice
13// ---------------------------------------------------------------------------
14
15/// The messageClass CHOICE: numeric ([1] Unsigned) or text ([2] CharacterString).
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub enum MessageClass {
18    Numeric(u32),
19    Text(String),
20}
21
22// ---------------------------------------------------------------------------
23// TextMessageRequest (Clause 15.20.1 / 16.10.7)
24// ---------------------------------------------------------------------------
25
26/// Request parameters shared by ConfirmedTextMessage and
27/// UnconfirmedTextMessage.
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct TextMessageRequest {
30    pub source_device: ObjectIdentifier,
31    pub message_class: Option<MessageClass>,
32    pub message_priority: MessagePriority,
33    pub message: String,
34}
35
36impl TextMessageRequest {
37    pub fn encode(&self, buf: &mut BytesMut) -> Result<(), Error> {
38        // [0] textMessageSourceDevice
39        primitives::encode_ctx_object_id(buf, 0, &self.source_device);
40        // messageClass CHOICE (optional): [1] Unsigned or [2] CharacterString
41        if let Some(ref mc) = self.message_class {
42            match mc {
43                MessageClass::Numeric(n) => {
44                    primitives::encode_ctx_unsigned(buf, 1, *n as u64);
45                }
46                MessageClass::Text(s) => {
47                    primitives::encode_ctx_character_string(buf, 2, s)?;
48                }
49            }
50        }
51        // [3] messagePriority
52        primitives::encode_ctx_enumerated(buf, 3, self.message_priority.to_raw());
53        // [4] message
54        primitives::encode_ctx_character_string(buf, 4, &self.message)?;
55        Ok(())
56    }
57
58    pub fn decode(data: &[u8]) -> Result<Self, Error> {
59        let mut offset = 0;
60
61        // [0] textMessageSourceDevice
62        let (tag, pos) = tags::decode_tag(data, offset)?;
63        let end = pos + tag.length as usize;
64        if end > data.len() {
65            return Err(Error::decoding(
66                pos,
67                "TextMessage truncated at sourceDevice",
68            ));
69        }
70        let source_device = ObjectIdentifier::decode(&data[pos..end])?;
71        offset = end;
72
73        // messageClass CHOICE (optional): [1] Unsigned or [2] CharacterString
74        let mut message_class = None;
75        if offset < data.len() {
76            let (tag, pos) = tags::decode_tag(data, offset)?;
77            if tag.is_context(1) {
78                let end = pos + tag.length as usize;
79                if end > data.len() {
80                    return Err(Error::decoding(
81                        pos,
82                        "TextMessage truncated at messageClass numeric",
83                    ));
84                }
85                message_class = Some(MessageClass::Numeric(primitives::decode_unsigned(
86                    &data[pos..end],
87                )? as u32));
88                offset = end;
89            } else if tag.is_context(2) {
90                let end = pos + tag.length as usize;
91                if end > data.len() {
92                    return Err(Error::decoding(
93                        pos,
94                        "TextMessage truncated at messageClass text",
95                    ));
96                }
97                let s = primitives::decode_character_string(&data[pos..end])?;
98                message_class = Some(MessageClass::Text(s));
99                offset = end;
100            }
101            // else: not tag 1 or 2 — no messageClass, don't advance offset
102        }
103
104        // [3] messagePriority
105        let (tag, pos) = tags::decode_tag(data, offset)?;
106        let end = pos + tag.length as usize;
107        if end > data.len() {
108            return Err(Error::decoding(
109                pos,
110                "TextMessage truncated at messagePriority",
111            ));
112        }
113        let message_priority =
114            MessagePriority::from_raw(primitives::decode_unsigned(&data[pos..end])? as u32);
115        offset = end;
116
117        // [4] message
118        let (tag, pos) = tags::decode_tag(data, offset)?;
119        let end = pos + tag.length as usize;
120        if end > data.len() {
121            return Err(Error::decoding(pos, "TextMessage truncated at message"));
122        }
123        let message = primitives::decode_character_string(&data[pos..end])?;
124
125        Ok(Self {
126            source_device,
127            message_class,
128            message_priority,
129            message,
130        })
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137    use bacnet_types::enums::ObjectType;
138
139    #[test]
140    fn request_numeric_class_round_trip() {
141        let req = TextMessageRequest {
142            source_device: ObjectIdentifier::new(ObjectType::DEVICE, 100).unwrap(),
143            message_class: Some(MessageClass::Numeric(5)),
144            message_priority: MessagePriority::URGENT,
145            message: "Fire alarm".into(),
146        };
147        let mut buf = BytesMut::new();
148        req.encode(&mut buf).unwrap();
149        let decoded = TextMessageRequest::decode(&buf).unwrap();
150        assert_eq!(req, decoded);
151    }
152
153    #[test]
154    fn request_text_class_round_trip() {
155        let req = TextMessageRequest {
156            source_device: ObjectIdentifier::new(ObjectType::DEVICE, 200).unwrap(),
157            message_class: Some(MessageClass::Text("maintenance".into())),
158            message_priority: MessagePriority::NORMAL,
159            message: "Scheduled shutdown".into(),
160        };
161        let mut buf = BytesMut::new();
162        req.encode(&mut buf).unwrap();
163        let decoded = TextMessageRequest::decode(&buf).unwrap();
164        assert_eq!(req, decoded);
165    }
166
167    #[test]
168    fn request_no_class_round_trip() {
169        let req = TextMessageRequest {
170            source_device: ObjectIdentifier::new(ObjectType::DEVICE, 1).unwrap(),
171            message_class: None,
172            message_priority: MessagePriority::NORMAL,
173            message: "Hello".into(),
174        };
175        let mut buf = BytesMut::new();
176        req.encode(&mut buf).unwrap();
177        let decoded = TextMessageRequest::decode(&buf).unwrap();
178        assert_eq!(req, decoded);
179    }
180
181    // -----------------------------------------------------------------------
182    // Malformed-input decode error tests
183    // -----------------------------------------------------------------------
184
185    #[test]
186    fn test_decode_empty_input() {
187        assert!(TextMessageRequest::decode(&[]).is_err());
188    }
189
190    #[test]
191    fn test_decode_truncated_1_byte() {
192        let req = TextMessageRequest {
193            source_device: ObjectIdentifier::new(ObjectType::DEVICE, 100).unwrap(),
194            message_class: None,
195            message_priority: MessagePriority::NORMAL,
196            message: "Test".into(),
197        };
198        let mut buf = BytesMut::new();
199        req.encode(&mut buf).unwrap();
200        assert!(TextMessageRequest::decode(&buf[..1]).is_err());
201    }
202
203    #[test]
204    fn test_decode_truncated_half() {
205        let req = TextMessageRequest {
206            source_device: ObjectIdentifier::new(ObjectType::DEVICE, 100).unwrap(),
207            message_class: Some(MessageClass::Text("info".into())),
208            message_priority: MessagePriority::URGENT,
209            message: "Emergency".into(),
210        };
211        let mut buf = BytesMut::new();
212        req.encode(&mut buf).unwrap();
213        let half = buf.len() / 2;
214        assert!(TextMessageRequest::decode(&buf[..half]).is_err());
215    }
216
217    #[test]
218    fn test_decode_invalid_tag() {
219        assert!(TextMessageRequest::decode(&[0xFF, 0xFF, 0xFF]).is_err());
220    }
221}