bacnet_services/
text_message.rs1use 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#[derive(Debug, Clone, PartialEq, Eq)]
17pub enum MessageClass {
18 Numeric(u32),
19 Text(String),
20}
21
22#[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 primitives::encode_ctx_object_id(buf, 0, &self.source_device);
40 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 primitives::encode_ctx_enumerated(buf, 2, self.message_priority.to_raw());
55 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 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 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 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 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 #[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}