1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
//! Virtual Terminal (VT) services per ASHRAE 135-2020 Clauses 17.2–17.4.
//!
//! Legacy services needed for full spec coverage. The request and VT-Open
//! acknowledgment fields use APPLICATION tags; the VT-Data acknowledgment is
//! the one construct that uses context tags (Clause 21.2.5).
#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
use bacnet_encoding::primitives;
use bacnet_encoding::tags;
use bacnet_types::enums::VTClass;
use bacnet_types::error::{DecodeError, DecodingKind, Error};
use bytes::BytesMut;
use crate::common::MAX_DECODED_ITEMS;
use bacnet_encoding::constructed::tagged::{
decode_app_enumerated, decode_app_primitive, decode_app_unsigned, decode_ctx_boolean,
decode_ctx_unsigned, decode_optional_ctx, expect_end, next_is_context,
};
#[path = "virtual_terminal_error.rs"]
mod error;
pub use error::VTCloseError;
// ---------------------------------------------------------------------------
// VTOpenRequest / VTOpenAck
// ---------------------------------------------------------------------------
/// VT-Open-Request service parameters (Clause 17.2.1, Clause 21.2.5).
///
/// Wire form: an application ENUMERATED `vt-class` followed by an application Unsigned8
/// `local-vt-session-identifier`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VTOpenRequest {
/// Terminal class requested for the session (BACnetVTClass); values beyond the named
/// constants are carried through unchanged.
pub vt_class: VTClass,
/// The requester's own identifier for the new session, in the range 0-255. The responder
/// quotes it as the session identifier when it sends VT-Data in the other direction.
pub local_vt_session_identifier: u8,
}
impl VTOpenRequest {
/// Encode the request parameters into `buf`.
pub fn encode(&self, buf: &mut BytesMut) {
primitives::encode_app_enumerated(buf, self.vt_class.to_raw());
primitives::encode_app_unsigned(buf, u64::from(self.local_vt_session_identifier));
}
/// Decode the request from service-request octets; fails on missing, malformed or
/// truncated fields and on trailing data.
pub fn decode(data: &[u8]) -> Result<Self, Error> {
let (vt_class, offset) = decode_app_enumerated::<u32>(data, 0, "VTOpen vt-class")?;
let vt_class = VTClass::from_raw(vt_class);
let (local_vt_session_identifier, offset) =
decode_app_unsigned::<u8>(data, offset, "VTOpen local-vt-session-identifier")?;
expect_end(data, offset, offset, "VTOpen")?;
Ok(Self {
vt_class,
local_vt_session_identifier,
})
}
}
/// VT-Open-Ack service parameters.
///
/// `remote_vt_session_identifier` is an APPLICATION-tagged Unsigned8.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VTOpenAck {
/// Identifier the responder assigned to the new session, in the range 0-255; the requester
/// quotes it in later VT-Data and VT-Close requests.
pub remote_vt_session_identifier: u8,
}
impl VTOpenAck {
/// Encode the acknowledgment parameter into `buf`.
pub fn encode(&self, buf: &mut BytesMut) {
primitives::encode_app_unsigned(buf, self.remote_vt_session_identifier as u64);
}
/// Decode the acknowledgment from its service-ack octets; fails on malformed or truncated
/// input and on trailing data.
pub fn decode(data: &[u8]) -> Result<Self, Error> {
let (id, offset) = decode_app_unsigned::<u8>(data, 0, "VTOpenAck session-identifier")?;
expect_end(data, offset, offset, "VTOpenAck")?;
Ok(Self {
remote_vt_session_identifier: id,
})
}
}
// ---------------------------------------------------------------------------
// VTCloseRequest
// ---------------------------------------------------------------------------
/// VT-Close-Request service parameters.
///
/// Contains a SEQUENCE OF Unsigned8 (APPLICATION tagged). Clause 17.3.1.1.1 requires at least
/// one identifier, so an empty list is rejected on both encode and decode.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VTCloseRequest {
/// Session identifiers to terminate, as known to the responding device; must not be empty.
pub list_of_remote_vt_session_identifiers: Vec<u8>,
}
impl VTCloseRequest {
/// Encode the request parameters into `buf`; fails if the identifier list is empty.
pub fn encode(&self, buf: &mut BytesMut) -> Result<(), Error> {
if self.list_of_remote_vt_session_identifiers.is_empty() {
return Err(Error::encode("VTClose requires at least one session identifier"));
}
for &id in &self.list_of_remote_vt_session_identifiers {
primitives::encode_app_unsigned(buf, id as u64);
}
Ok(())
}
/// Decode the request from service-request octets; fails on malformed or truncated input
/// and on an empty list.
pub fn decode(data: &[u8]) -> Result<Self, Error> {
let mut offset = 0;
let mut ids = Vec::new();
while offset < data.len() {
if ids.len() >= MAX_DECODED_ITEMS {
return Err(Error::overflow(offset, "VTClose too many session IDs"));
}
let (id, next) = decode_app_unsigned::<u8>(data, offset, "VTClose session-identifier")?;
ids.push(id);
offset = next;
}
if ids.is_empty() {
return Err(Error::decoding(
0,
"VTClose requires at least one session identifier",
));
}
Ok(Self {
list_of_remote_vt_session_identifiers: ids,
})
}
}
// ---------------------------------------------------------------------------
// VTDataRequest / VTDataAck
// ---------------------------------------------------------------------------
/// VT-Data-Request service parameters (Clause 17.4.1.1, Clause 21.2.5).
///
/// Wire form: application Unsigned8 session identifier, application OCTET STRING data, and an
/// application Unsigned `vt-data-flag` limited to 0 or 1.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VTDataRequest {
/// Session the data belongs to, as known to the responding device.
pub vt_session_identifier: u8,
/// Octets of new data for the peer terminal.
pub vt_new_data: Vec<u8>,
/// One-bit sequence number, 0 (`false`) or 1 (`true`), that the sender flips on every fresh
/// VT-Data request within a session so the receiver can spot a repeat. It is sent as an
/// Unsigned, not as a Boolean.
pub vt_data_flag: bool,
}
impl VTDataRequest {
/// Encode the request parameters into `buf`.
pub fn encode(&self, buf: &mut BytesMut) {
primitives::encode_app_unsigned(buf, self.vt_session_identifier as u64);
primitives::encode_app_octet_string(buf, &self.vt_new_data);
primitives::encode_app_unsigned(buf, u64::from(self.vt_data_flag));
}
/// Decode the request from service-request octets; fails on malformed or truncated input,
/// a flag other than 0 or 1, and trailing data.
pub fn decode(data: &[u8]) -> Result<Self, Error> {
let (vt_session_identifier, offset) =
decode_app_unsigned::<u8>(data, 0, "VTData session-identifier")?;
let (octets, offset) =
decode_app_primitive(data, offset, tags::app_tag::OCTET_STRING, "VTData new-data")?;
let vt_new_data = octets.to_vec();
let flag_offset = offset;
let (flag, offset) = decode_app_unsigned::<u64>(data, offset, "VTData data-flag")?;
let vt_data_flag = match flag {
0 => false,
1 => true,
other => {
return Err(DecodeError::new(
DecodingKind::InvalidEncoding,
flag_offset,
"VTData data-flag {} is outside 0..1",
)
.with_value(other)
.into())
}
};
expect_end(data, offset, offset, "VTData")?;
Ok(Self {
vt_session_identifier,
vt_new_data,
vt_data_flag,
})
}
}
/// VT-Data-ACK service parameters (Clause 17.4.1.2, Clause 21.2.5).
///
/// `all-new-data-accepted` is always present as context tag \[0\]; the `accepted-octet-count`
/// in context tag \[1\] is present exactly when that flag is FALSE. The enum makes the pairing
/// impossible to get wrong.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum VTDataAck {
/// Every octet of the request was accepted (`all-new-data-accepted` TRUE, no count).
AllAccepted,
/// Only part of the data was accepted (`all-new-data-accepted` FALSE).
Partial {
/// Number of octets of the request's new data that were actually accepted.
accepted_octet_count: u32,
},
}
impl VTDataAck {
/// Whether all of the new data was accepted.
pub fn all_new_data_accepted(&self) -> bool {
matches!(self, Self::AllAccepted)
}
/// The accepted octet count, present only when the data was partially accepted.
pub fn accepted_octet_count(&self) -> Option<u32> {
match self {
Self::AllAccepted => None,
Self::Partial {
accepted_octet_count,
} => Some(*accepted_octet_count),
}
}
/// Encode the acknowledgment parameters into `buf`.
pub fn encode(&self, buf: &mut BytesMut) {
primitives::encode_ctx_boolean(buf, 0, self.all_new_data_accepted());
if let Some(count) = self.accepted_octet_count() {
primitives::encode_ctx_unsigned(buf, 1, u64::from(count));
}
}
/// Decode the acknowledgment from its service-ack octets; fails when `[0]` is missing or
/// malformed, when the count is absent for FALSE or present for TRUE, and on trailing data.
pub fn decode(data: &[u8]) -> Result<Self, Error> {
if !next_is_context(data, 0, 0)? {
return Err(Error::decoding(
0,
"VTDataAck missing all-new-data-accepted [0]",
));
}
let (all_accepted, offset) =
decode_ctx_boolean(data, 0, 0, "VTDataAck all-new-data-accepted")?;
let count_offset = offset;
let (count, offset) = decode_optional_ctx(
data,
offset,
1,
"VTDataAck accepted-octet-count",
decode_ctx_unsigned::<u32>,
)?;
let ack = match (all_accepted, count) {
(true, None) => Self::AllAccepted,
(false, Some(accepted_octet_count)) => Self::Partial {
accepted_octet_count,
},
(true, Some(_)) => {
return Err(Error::decoding(
count_offset,
"VTDataAck accepted-octet-count present although all data was accepted",
))
}
(false, None) => {
return Err(Error::decoding(
count_offset,
"VTDataAck accepted-octet-count missing although data was refused",
))
}
};
expect_end(data, offset, offset, "VTDataAck")?;
Ok(ack)
}
}
#[cfg(test)]
#[path = "virtual_terminal_tests.rs"]
mod tests;