Skip to main content

bacnet_client/client/
file_list.rs

1use super::*;
2
3fn validate_atomic_read_file_ack(
4    request: &bacnet_services::file::FileAccessMethod,
5    ack: &bacnet_services::file::AtomicReadFileAck,
6) -> Result<(), Error> {
7    use bacnet_services::file::{FileAccessMethod, FileReadAckMethod};
8
9    match (request, &ack.access) {
10        (
11            FileAccessMethod::Stream {
12                requested_octet_count,
13                ..
14            },
15            FileReadAckMethod::Stream { file_data, .. },
16        ) if file_data.len() <= *requested_octet_count as usize => Ok(()),
17        (FileAccessMethod::Stream { .. }, FileReadAckMethod::Stream { .. }) => Err(
18            Error::decoding(0, "AtomicReadFile ACK exceeds the requested octet window"),
19        ),
20        (
21            FileAccessMethod::Record {
22                requested_record_count,
23                ..
24            },
25            FileReadAckMethod::Record {
26                returned_record_count,
27                file_record_data,
28                ..
29            },
30        ) if returned_record_count <= requested_record_count
31            && *returned_record_count as usize == file_record_data.len() =>
32        {
33            Ok(())
34        }
35        (FileAccessMethod::Record { .. }, FileReadAckMethod::Record { .. }) => Err(
36            Error::decoding(0, "AtomicReadFile ACK exceeds the requested record window"),
37        ),
38        _ => Err(Error::decoding(
39            0,
40            "AtomicReadFile ACK access method does not match the request",
41        )),
42    }
43}
44
45impl<T: TransportPort + 'static> BACnetClient<T> {
46    /// Send a caller-supplied AcknowledgeAlarm request without fabricating fields.
47    pub async fn acknowledge_alarm_request(
48        &self,
49        destination_mac: &[u8],
50        request: &bacnet_services::alarm_event::AcknowledgeAlarmRequest,
51    ) -> Result<(), Error> {
52        let mut buf = BytesMut::new();
53        request.encode(&mut buf)?;
54
55        let _ = self
56            .confirmed_request(
57                destination_mac,
58                ConfirmedServiceChoice::ACKNOWLEDGE_ALARM,
59                &buf,
60            )
61            .await?;
62
63        Ok(())
64    }
65
66    /// Send exact caller-supplied alarm acknowledgment fields to a discovered device.
67    ///
68    /// Routes when needed and preserves both timestamps, confirmation and errors
69    /// from [`Self::acknowledge_alarm_request`]. A missing device fails before sending.
70    pub async fn acknowledge_alarm_request_from_device(
71        &self,
72        device_instance: u32,
73        request: &bacnet_services::alarm_event::AcknowledgeAlarmRequest,
74    ) -> Result<(), Error> {
75        let (mac, routing) = self.resolve_device(device_instance).await?;
76        if let Some((network, address)) = routing {
77            let mut buf = BytesMut::new();
78            request.encode(&mut buf)?;
79            self.confirmed_request_routed(
80                &mac,
81                network,
82                &address,
83                ConfirmedServiceChoice::ACKNOWLEDGE_ALARM,
84                &buf,
85            )
86            .await?;
87            Ok(())
88        } else {
89            self.acknowledge_alarm_request(&mac, request).await
90        }
91    }
92
93    /// Acknowledge an alarm on a remote device.
94    ///
95    /// This compatibility helper fabricates sequence-zero timestamps, which
96    /// are not a valid general correlation mechanism. Use
97    /// [`Self::acknowledge_alarm_request`] with the original notification's
98    /// exact timestamp and a caller-selected acknowledgment time.
99    #[deprecated(
100        note = "use acknowledge_alarm_request with caller-supplied correlation timestamps"
101    )]
102    pub async fn acknowledge_alarm(
103        &self,
104        destination_mac: &[u8],
105        acknowledging_process_identifier: u32,
106        event_object_identifier: bacnet_types::primitives::ObjectIdentifier,
107        event_state_acknowledged: bacnet_types::enums::EventState,
108        acknowledgment_source: &str,
109    ) -> Result<(), Error> {
110        use bacnet_services::alarm_event::AcknowledgeAlarmRequest;
111
112        let request = AcknowledgeAlarmRequest {
113            acknowledging_process_identifier,
114            event_object_identifier,
115            event_state_acknowledged,
116            timestamp: bacnet_types::primitives::BACnetTimeStamp::SequenceNumber(0),
117            acknowledgment_source: acknowledgment_source.to_string(),
118            time_of_acknowledgment: bacnet_types::primitives::BACnetTimeStamp::SequenceNumber(0),
119        };
120        self.acknowledge_alarm_request(destination_mac, &request)
121            .await
122    }
123
124    /// Read file data from a remote device (stream or record access).
125    pub async fn atomic_read_file(
126        &self,
127        destination_mac: &[u8],
128        file_identifier: bacnet_types::primitives::ObjectIdentifier,
129        access: bacnet_services::file::FileAccessMethod,
130    ) -> Result<Bytes, Error> {
131        use bacnet_services::file::AtomicReadFileRequest;
132
133        let request = AtomicReadFileRequest {
134            file_identifier,
135            access,
136        };
137        let mut buf = BytesMut::new();
138        request.encode(&mut buf);
139
140        self.confirmed_request(
141            destination_mac,
142            ConfirmedServiceChoice::ATOMIC_READ_FILE,
143            &buf,
144        )
145        .await
146    }
147
148    /// Read and strictly decode one AtomicReadFile ACK window.
149    ///
150    /// This additive typed boundary validates that the ACK uses the requested
151    /// stream/record access arm and does not return more octets or records than
152    /// requested. Use [`Self::atomic_read_file`] when the raw encoded service
153    /// payload is required for compatibility.
154    pub async fn atomic_read_file_decoded(
155        &self,
156        destination_mac: &[u8],
157        file_identifier: bacnet_types::primitives::ObjectIdentifier,
158        access: bacnet_services::file::FileAccessMethod,
159    ) -> Result<bacnet_services::file::AtomicReadFileAck, Error> {
160        let requested_access = access.clone();
161        let response = self
162            .atomic_read_file(destination_mac, file_identifier, access)
163            .await?;
164        let ack = bacnet_services::file::AtomicReadFileAck::decode(&response)?;
165        validate_atomic_read_file_ack(&requested_access, &ack)?;
166        Ok(ack)
167    }
168
169    /// Read raw file acknowledgement bytes from a discovered device.
170    ///
171    /// Routes stream and record access when needed. The device address is
172    /// resolved once for this request; a missing device fails before sending.
173    /// Use [`Self::atomic_read_file_decoded_from_device`] for a checked ACK.
174    pub async fn atomic_read_file_from_device(
175        &self,
176        device_instance: u32,
177        file_identifier: bacnet_types::primitives::ObjectIdentifier,
178        access: bacnet_services::file::FileAccessMethod,
179    ) -> Result<Bytes, Error> {
180        let (mac, routing) = self.resolve_device(device_instance).await?;
181        if let Some((network, address)) = routing {
182            let request = bacnet_services::file::AtomicReadFileRequest {
183                file_identifier,
184                access,
185            };
186            let mut buf = BytesMut::new();
187            request.encode(&mut buf);
188            self.confirmed_request_routed(
189                &mac,
190                network,
191                &address,
192                ConfirmedServiceChoice::ATOMIC_READ_FILE,
193                &buf,
194            )
195            .await
196        } else {
197            self.atomic_read_file(&mac, file_identifier, access).await
198        }
199    }
200
201    /// Read one checked file window from a discovered device, routing if needed.
202    ///
203    /// Preserves the access-arm and window checks of
204    /// [`Self::atomic_read_file_decoded`]. Successive calls resolve separately;
205    /// a sequence of windows is not an atomic snapshot of the remote file.
206    pub async fn atomic_read_file_decoded_from_device(
207        &self,
208        device_instance: u32,
209        file_identifier: bacnet_types::primitives::ObjectIdentifier,
210        access: bacnet_services::file::FileAccessMethod,
211    ) -> Result<bacnet_services::file::AtomicReadFileAck, Error> {
212        let requested_access = access.clone();
213        let response = self
214            .atomic_read_file_from_device(device_instance, file_identifier, access)
215            .await?;
216        let ack = bacnet_services::file::AtomicReadFileAck::decode(&response)?;
217        validate_atomic_read_file_ack(&requested_access, &ack)?;
218        Ok(ack)
219    }
220
221    /// Write file data to a remote device (stream or record access).
222    pub async fn atomic_write_file(
223        &self,
224        destination_mac: &[u8],
225        file_identifier: bacnet_types::primitives::ObjectIdentifier,
226        access: bacnet_services::file::FileWriteAccessMethod,
227    ) -> Result<Bytes, Error> {
228        use bacnet_services::file::AtomicWriteFileRequest;
229
230        let request = AtomicWriteFileRequest {
231            file_identifier,
232            access,
233        };
234        let mut buf = BytesMut::new();
235        request.encode(&mut buf);
236
237        self.confirmed_request(
238            destination_mac,
239            ConfirmedServiceChoice::ATOMIC_WRITE_FILE,
240            &buf,
241        )
242        .await
243    }
244
245    /// Write stream or record data to a discovered device, routing if needed.
246    ///
247    /// Returns the raw acknowledgement, as in [`Self::atomic_write_file`].
248    /// A missing device fails before sending.
249    pub async fn atomic_write_file_from_device(
250        &self,
251        device_instance: u32,
252        file_identifier: bacnet_types::primitives::ObjectIdentifier,
253        access: bacnet_services::file::FileWriteAccessMethod,
254    ) -> Result<Bytes, Error> {
255        let (mac, routing) = self.resolve_device(device_instance).await?;
256        if let Some((network, address)) = routing {
257            let request = bacnet_services::file::AtomicWriteFileRequest {
258                file_identifier,
259                access,
260            };
261            let mut buf = BytesMut::new();
262            request.encode(&mut buf);
263            self.confirmed_request_routed(
264                &mac,
265                network,
266                &address,
267                ConfirmedServiceChoice::ATOMIC_WRITE_FILE,
268                &buf,
269            )
270            .await
271        } else {
272            self.atomic_write_file(&mac, file_identifier, access).await
273        }
274    }
275
276    /// Add elements to a list property on a remote device.
277    ///
278    /// Rejects index zero, empty elements and malformed tag framing before
279    /// transaction admission or traffic. Element datatype remains target-owned.
280    pub async fn add_list_element(
281        &self,
282        destination_mac: &[u8],
283        object_identifier: bacnet_types::primitives::ObjectIdentifier,
284        property_identifier: bacnet_types::enums::PropertyIdentifier,
285        property_array_index: Option<u32>,
286        list_of_elements: Vec<u8>,
287    ) -> Result<(), Error> {
288        use bacnet_services::list_manipulation::ListElementRequest;
289
290        let request = ListElementRequest {
291            object_identifier,
292            property_identifier,
293            property_array_index,
294            list_of_elements,
295        };
296        let mut buf = BytesMut::new();
297        request.encode(&mut buf)?;
298
299        let _ = self
300            .confirmed_request(
301                destination_mac,
302                ConfirmedServiceChoice::ADD_LIST_ELEMENT,
303                &buf,
304            )
305            .await?;
306
307        Ok(())
308    }
309
310    /// Remove elements from a list property on a remote device.
311    ///
312    /// Rejects index zero, empty elements and malformed tag framing before
313    /// transaction admission or traffic. Element datatype remains target-owned.
314    pub async fn remove_list_element(
315        &self,
316        destination_mac: &[u8],
317        object_identifier: bacnet_types::primitives::ObjectIdentifier,
318        property_identifier: bacnet_types::enums::PropertyIdentifier,
319        property_array_index: Option<u32>,
320        list_of_elements: Vec<u8>,
321    ) -> Result<(), Error> {
322        use bacnet_services::list_manipulation::ListElementRequest;
323
324        let request = ListElementRequest {
325            object_identifier,
326            property_identifier,
327            property_array_index,
328            list_of_elements,
329        };
330        let mut buf = BytesMut::new();
331        request.encode(&mut buf)?;
332
333        let _ = self
334            .confirmed_request(
335                destination_mac,
336                ConfirmedServiceChoice::REMOVE_LIST_ELEMENT,
337                &buf,
338            )
339            .await?;
340
341        Ok(())
342    }
343}
344
345#[cfg(test)]
346#[path = "file_list_decoded_tests.rs"]
347mod decoded_tests;
348
349#[cfg(test)]
350#[path = "routed_file_tests.rs"]
351mod routed_tests;