Skip to main content

bacnet_services/
text_message.rs

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