Skip to main content

bacnet_client/client/
file_list.rs

1use super::*;
2
3fn validate_read_range_ack(
4    request: &bacnet_services::read_range::ReadRangeRequest,
5    ack: &bacnet_services::read_range::ReadRangeAck,
6) -> Result<(), Error> {
7    if ack.object_identifier != request.object_identifier {
8        return Err(Error::decoding(
9            0,
10            "ReadRange ACK object identifier does not match the request",
11        ));
12    }
13    if ack.property_identifier != request.property_identifier {
14        return Err(Error::decoding(
15            0,
16            "ReadRange ACK property identifier does not match the request",
17        ));
18    }
19    if ack.property_array_index != request.property_array_index {
20        return Err(Error::decoding(
21            0,
22            "ReadRange ACK array index does not match the request",
23        ));
24    }
25
26    let sequence_range = matches!(
27        request.range.as_ref(),
28        Some(
29            bacnet_services::read_range::RangeSpec::BySequenceNumber { .. }
30                | bacnet_services::read_range::RangeSpec::ByTime { .. }
31        )
32    );
33    match (sequence_range, ack.item_count, ack.first_sequence_number) {
34        (true, 1.., Some(1..)) | (true, 0, None) | (false, _, None) => {}
35        (true, 1.., _) => {
36            return Err(Error::decoding(
37                0,
38                "nonempty ReadRange By Sequence/Time ACK requires a nonzero first sequence number",
39            ));
40        }
41        _ => {
42            return Err(Error::decoding(
43                0,
44                "ReadRange ACK first sequence number is invalid for the request range",
45            ));
46        }
47    }
48
49    Ok(())
50}
51
52fn validate_atomic_read_file_ack(
53    request: &bacnet_services::file::FileAccessMethod,
54    ack: &bacnet_services::file::AtomicReadFileAck,
55) -> Result<(), Error> {
56    use bacnet_services::file::{FileAccessMethod, FileReadAckMethod};
57
58    match (request, &ack.access) {
59        (
60            FileAccessMethod::Stream {
61                requested_octet_count,
62                ..
63            },
64            FileReadAckMethod::Stream { file_data, .. },
65        ) if file_data.len() <= *requested_octet_count as usize => Ok(()),
66        (FileAccessMethod::Stream { .. }, FileReadAckMethod::Stream { .. }) => Err(
67            Error::decoding(0, "AtomicReadFile ACK exceeds the requested octet window"),
68        ),
69        (
70            FileAccessMethod::Record {
71                requested_record_count,
72                ..
73            },
74            FileReadAckMethod::Record {
75                returned_record_count,
76                file_record_data,
77                ..
78            },
79        ) if returned_record_count <= requested_record_count
80            && *returned_record_count as usize == file_record_data.len() =>
81        {
82            Ok(())
83        }
84        (FileAccessMethod::Record { .. }, FileReadAckMethod::Record { .. }) => Err(
85            Error::decoding(0, "AtomicReadFile ACK exceeds the requested record window"),
86        ),
87        _ => Err(Error::decoding(
88            0,
89            "AtomicReadFile ACK access method does not match the request",
90        )),
91    }
92}
93
94impl<T: TransportPort + 'static> BACnetClient<T> {
95    /// Get event information from a remote device.
96    pub async fn get_event_information(
97        &self,
98        destination_mac: &[u8],
99        last_received_object_identifier: Option<bacnet_types::primitives::ObjectIdentifier>,
100    ) -> Result<Bytes, Error> {
101        use bacnet_services::alarm_event::GetEventInformationRequest;
102
103        let request = GetEventInformationRequest {
104            last_received_object_identifier,
105        };
106        let mut buf = BytesMut::new();
107        request.encode(&mut buf);
108
109        self.confirmed_request(
110            destination_mac,
111            ConfirmedServiceChoice::GET_EVENT_INFORMATION,
112            &buf,
113        )
114        .await
115    }
116
117    /// Send a caller-supplied AcknowledgeAlarm request without fabricating fields.
118    pub async fn acknowledge_alarm_request(
119        &self,
120        destination_mac: &[u8],
121        request: &bacnet_services::alarm_event::AcknowledgeAlarmRequest,
122    ) -> Result<(), Error> {
123        let mut buf = BytesMut::new();
124        request.encode(&mut buf)?;
125
126        let _ = self
127            .confirmed_request(
128                destination_mac,
129                ConfirmedServiceChoice::ACKNOWLEDGE_ALARM,
130                &buf,
131            )
132            .await?;
133
134        Ok(())
135    }
136
137    /// Acknowledge an alarm on a remote device.
138    ///
139    /// This compatibility helper fabricates sequence-zero timestamps, which
140    /// are not a valid general correlation mechanism. Use
141    /// [`Self::acknowledge_alarm_request`] with the original notification's
142    /// exact timestamp and a caller-selected acknowledgment time.
143    #[deprecated(
144        note = "use acknowledge_alarm_request with caller-supplied correlation timestamps"
145    )]
146    pub async fn acknowledge_alarm(
147        &self,
148        destination_mac: &[u8],
149        acknowledging_process_identifier: u32,
150        event_object_identifier: bacnet_types::primitives::ObjectIdentifier,
151        event_state_acknowledged: u32,
152        acknowledgment_source: &str,
153    ) -> Result<(), Error> {
154        use bacnet_services::alarm_event::AcknowledgeAlarmRequest;
155
156        let request = AcknowledgeAlarmRequest {
157            acknowledging_process_identifier,
158            event_object_identifier,
159            event_state_acknowledged,
160            timestamp: bacnet_types::primitives::BACnetTimeStamp::SequenceNumber(0),
161            acknowledgment_source: acknowledgment_source.to_string(),
162            time_of_acknowledgment: bacnet_types::primitives::BACnetTimeStamp::SequenceNumber(0),
163        };
164        self.acknowledge_alarm_request(destination_mac, &request)
165            .await
166    }
167
168    /// Read a range of items from a list or log-buffer property.
169    pub async fn read_range(
170        &self,
171        destination_mac: &[u8],
172        object_identifier: bacnet_types::primitives::ObjectIdentifier,
173        property_identifier: bacnet_types::enums::PropertyIdentifier,
174        property_array_index: Option<u32>,
175        range: Option<bacnet_services::read_range::RangeSpec>,
176    ) -> Result<bacnet_services::read_range::ReadRangeAck, Error> {
177        use bacnet_services::read_range::{ReadRangeAck, ReadRangeRequest};
178
179        let request = ReadRangeRequest {
180            object_identifier,
181            property_identifier,
182            property_array_index,
183            range,
184        };
185        let mut buf = BytesMut::new();
186        request.encode(&mut buf);
187
188        let response_data = self
189            .confirmed_request(destination_mac, ConfirmedServiceChoice::READ_RANGE, &buf)
190            .await?;
191
192        let ack = ReadRangeAck::decode(&response_data)?;
193        validate_read_range_ack(&request, &ack)?;
194        Ok(ack)
195    }
196
197    /// Read file data from a remote device (stream or record access).
198    pub async fn atomic_read_file(
199        &self,
200        destination_mac: &[u8],
201        file_identifier: bacnet_types::primitives::ObjectIdentifier,
202        access: bacnet_services::file::FileAccessMethod,
203    ) -> Result<Bytes, Error> {
204        use bacnet_services::file::AtomicReadFileRequest;
205
206        let request = AtomicReadFileRequest {
207            file_identifier,
208            access,
209        };
210        let mut buf = BytesMut::new();
211        request.encode(&mut buf);
212
213        self.confirmed_request(
214            destination_mac,
215            ConfirmedServiceChoice::ATOMIC_READ_FILE,
216            &buf,
217        )
218        .await
219    }
220
221    /// Read and strictly decode one AtomicReadFile ACK window.
222    ///
223    /// This additive typed boundary validates that the ACK uses the requested
224    /// stream/record access arm and does not return more octets or records than
225    /// requested. Use [`Self::atomic_read_file`] when the raw encoded service
226    /// payload is required for compatibility.
227    pub async fn atomic_read_file_decoded(
228        &self,
229        destination_mac: &[u8],
230        file_identifier: bacnet_types::primitives::ObjectIdentifier,
231        access: bacnet_services::file::FileAccessMethod,
232    ) -> Result<bacnet_services::file::AtomicReadFileAck, Error> {
233        let requested_access = access.clone();
234        let response = self
235            .atomic_read_file(destination_mac, file_identifier, access)
236            .await?;
237        let ack = bacnet_services::file::AtomicReadFileAck::decode(&response)?;
238        validate_atomic_read_file_ack(&requested_access, &ack)?;
239        Ok(ack)
240    }
241
242    /// Write file data to a remote device (stream or record access).
243    pub async fn atomic_write_file(
244        &self,
245        destination_mac: &[u8],
246        file_identifier: bacnet_types::primitives::ObjectIdentifier,
247        access: bacnet_services::file::FileWriteAccessMethod,
248    ) -> Result<Bytes, Error> {
249        use bacnet_services::file::AtomicWriteFileRequest;
250
251        let request = AtomicWriteFileRequest {
252            file_identifier,
253            access,
254        };
255        let mut buf = BytesMut::new();
256        request.encode(&mut buf);
257
258        self.confirmed_request(
259            destination_mac,
260            ConfirmedServiceChoice::ATOMIC_WRITE_FILE,
261            &buf,
262        )
263        .await
264    }
265
266    /// Add elements to a list property on a remote device.
267    pub async fn add_list_element(
268        &self,
269        destination_mac: &[u8],
270        object_identifier: bacnet_types::primitives::ObjectIdentifier,
271        property_identifier: bacnet_types::enums::PropertyIdentifier,
272        property_array_index: Option<u32>,
273        list_of_elements: Vec<u8>,
274    ) -> Result<(), Error> {
275        use bacnet_services::list_manipulation::ListElementRequest;
276
277        let request = ListElementRequest {
278            object_identifier,
279            property_identifier,
280            property_array_index,
281            list_of_elements,
282        };
283        let mut buf = BytesMut::new();
284        request.encode(&mut buf);
285
286        let _ = self
287            .confirmed_request(
288                destination_mac,
289                ConfirmedServiceChoice::ADD_LIST_ELEMENT,
290                &buf,
291            )
292            .await?;
293
294        Ok(())
295    }
296
297    /// Remove elements from a list property on a remote device.
298    pub async fn remove_list_element(
299        &self,
300        destination_mac: &[u8],
301        object_identifier: bacnet_types::primitives::ObjectIdentifier,
302        property_identifier: bacnet_types::enums::PropertyIdentifier,
303        property_array_index: Option<u32>,
304        list_of_elements: Vec<u8>,
305    ) -> Result<(), Error> {
306        use bacnet_services::list_manipulation::ListElementRequest;
307
308        let request = ListElementRequest {
309            object_identifier,
310            property_identifier,
311            property_array_index,
312            list_of_elements,
313        };
314        let mut buf = BytesMut::new();
315        request.encode(&mut buf);
316
317        let _ = self
318            .confirmed_request(
319                destination_mac,
320                ConfirmedServiceChoice::REMOVE_LIST_ELEMENT,
321                &buf,
322            )
323            .await?;
324
325        Ok(())
326    }
327}
328
329#[cfg(test)]
330mod tests {
331    use super::*;
332    use bacnet_services::read_range::{RangeSpec, ReadRangeAck, ReadRangeRequest};
333    use bacnet_types::enums::{ObjectType, PropertyIdentifier};
334    use bacnet_types::primitives::{Date, ObjectIdentifier, Time};
335
336    fn request(range: Option<RangeSpec>) -> ReadRangeRequest {
337        ReadRangeRequest {
338            object_identifier: ObjectIdentifier::new(ObjectType::TREND_LOG, 1).unwrap(),
339            property_identifier: PropertyIdentifier::LOG_BUFFER,
340            property_array_index: Some(1),
341            range,
342        }
343    }
344
345    fn ack(request: &ReadRangeRequest, item_count: u32, first: Option<u32>) -> ReadRangeAck {
346        ReadRangeAck {
347            object_identifier: request.object_identifier,
348            property_identifier: request.property_identifier,
349            property_array_index: request.property_array_index,
350            result_flags: (true, true, false),
351            item_count,
352            item_data: Vec::new(),
353            first_sequence_number: first,
354        }
355    }
356
357    #[test]
358    fn read_range_ack_must_echo_the_request() {
359        let request = request(Some(RangeSpec::ByPosition {
360            reference_index: 1,
361            count: 1,
362        }));
363        let valid = ack(&request, 1, None);
364        assert!(validate_read_range_ack(&request, &valid).is_ok());
365
366        let mut wrong_object = valid.clone();
367        wrong_object.object_identifier = ObjectIdentifier::new(ObjectType::TREND_LOG, 2).unwrap();
368        assert!(validate_read_range_ack(&request, &wrong_object).is_err());
369
370        let mut wrong_property = valid.clone();
371        wrong_property.property_identifier = PropertyIdentifier::PRESENT_VALUE;
372        assert!(validate_read_range_ack(&request, &wrong_property).is_err());
373
374        let mut wrong_index = valid;
375        wrong_index.property_array_index = Some(2);
376        assert!(validate_read_range_ack(&request, &wrong_index).is_err());
377    }
378
379    #[test]
380    fn first_sequence_number_must_match_the_requested_range() {
381        let by_sequence = request(Some(RangeSpec::BySequenceNumber {
382            reference_seq: 1,
383            count: 1,
384        }));
385        assert!(validate_read_range_ack(&by_sequence, &ack(&by_sequence, 1, Some(1))).is_ok());
386        assert!(validate_read_range_ack(&by_sequence, &ack(&by_sequence, 1, None)).is_err());
387        assert!(validate_read_range_ack(&by_sequence, &ack(&by_sequence, 1, Some(0))).is_err());
388        assert!(validate_read_range_ack(&by_sequence, &ack(&by_sequence, 0, None)).is_ok());
389        assert!(validate_read_range_ack(&by_sequence, &ack(&by_sequence, 0, Some(1))).is_err());
390
391        let by_time = request(Some(RangeSpec::ByTime {
392            reference_time: (
393                Date {
394                    year: 126,
395                    month: 3,
396                    day: 1,
397                    day_of_week: 7,
398                },
399                Time {
400                    hour: 14,
401                    minute: 30,
402                    second: 0,
403                    hundredths: 0,
404                },
405            ),
406            count: 1,
407        }));
408        assert!(validate_read_range_ack(&by_time, &ack(&by_time, 1, Some(1))).is_ok());
409        assert!(validate_read_range_ack(&by_time, &ack(&by_time, 1, None)).is_err());
410        assert!(validate_read_range_ack(&by_time, &ack(&by_time, 1, Some(0))).is_err());
411
412        let by_position = request(Some(RangeSpec::ByPosition {
413            reference_index: 1,
414            count: 1,
415        }));
416        assert!(validate_read_range_ack(&by_position, &ack(&by_position, 1, Some(1))).is_err());
417
418        let no_range = request(None);
419        assert!(validate_read_range_ack(&no_range, &ack(&no_range, 1, Some(1))).is_err());
420    }
421}
422
423#[cfg(test)]
424#[path = "file_list_decoded_tests.rs"]
425mod decoded_tests;