Skip to main content

mbus_client/services/diagnostic/
apis.rs

1use crate::{
2    app::DiagnosticsResponse,
3    services::{
4        ClientCommon, ClientServices, Diag, OperationMeta,
5        diagnostic::{self, ObjectId, ReadDeviceIdCode},
6    },
7};
8use mbus_core::{
9    errors::MbusError,
10    function_codes::public::{DiagnosticSubFunction, EncapsulatedInterfaceType},
11    transport::{Transport, UnitIdOrSlaveAddr},
12};
13
14impl<TRANSPORT, APP, const N: usize> ClientServices<TRANSPORT, APP, N>
15where
16    TRANSPORT: Transport,
17    APP: ClientCommon + DiagnosticsResponse,
18{
19    /// Sends a Read Device Identification request (FC 43 / 14).
20    ///
21    /// # Parameters
22    /// - `txn_id`: Transaction ID of the original request. While Modbus Serial (RTU/ASCII)
23    ///   does not natively use transaction IDs, the stack preserves the ID provided in
24    ///   the request and returns it here to allow for asynchronous tracking.
25    /// - `unit_id_slave_addr`: The target Modbus unit ID or slave address.
26    ///   - `unit_id`: if transport is tcp
27    ///   - `slave_addr`: if transport is serial/// - `read_device_id_code`: The type of access (01=Basic, 02=Regular, 03=Extended, 04=Specific).
28    /// - `object_id`: The object ID to start reading from.
29    #[must_use = "request submission errors should be handled; the request may not have been queued/sent"]
30    pub fn read_device_identification(
31        &mut self,
32        txn_id: u16,
33        unit_id_slave_addr: UnitIdOrSlaveAddr,
34        read_device_id_code: ReadDeviceIdCode,
35        object_id: ObjectId,
36    ) -> Result<(), MbusError> {
37        if unit_id_slave_addr.is_broadcast() {
38            return Err(MbusError::BroadcastNotAllowed); // Modbus forbids broadcast Read operations
39        }
40
41        let frame = diagnostic::service::ServiceBuilder::read_device_identification(
42            txn_id,
43            unit_id_slave_addr.get(),
44            read_device_id_code,
45            object_id,
46            self.transport.transport_type(),
47        )?;
48
49        self.add_an_expectation(
50            txn_id,
51            unit_id_slave_addr,
52            &frame,
53            OperationMeta::Diag(Diag {
54                device_id_code: read_device_id_code,
55                encap_type: EncapsulatedInterfaceType::Err,
56            }),
57            Self::handle_read_device_identification_rsp,
58        )?;
59
60        self.dispatch_request_frame(txn_id, unit_id_slave_addr, &frame)?;
61        Ok(())
62    }
63
64    /// Sends a generic Encapsulated Interface Transport request (FC 43).
65    ///
66    /// # Parameters
67    /// - `txn_id`: Transaction ID of the original request. While Modbus Serial (RTU/ASCII)
68    ///   does not natively use transaction IDs, the stack preserves the ID provided in
69    ///   the request and returns it here to allow for asynchronous tracking.
70    /// - `unit_id_slave_addr`: The target Modbus unit ID or slave address.
71    ///   - `unit_id`: if transport is tcp
72    ///   - `slave_addr`: if transport is serial/// - `mei_type`: The MEI type (e.g., `CanopenGeneralReference`).
73    /// - `data`: The data payload to be sent with the request.
74    #[must_use = "request submission errors should be handled; the request may not have been queued/sent"]
75    pub fn encapsulated_interface_transport(
76        &mut self,
77        txn_id: u16,
78        unit_id_slave_addr: UnitIdOrSlaveAddr,
79        mei_type: EncapsulatedInterfaceType,
80        data: &[u8],
81    ) -> Result<(), MbusError> {
82        let frame = diagnostic::service::ServiceBuilder::encapsulated_interface_transport(
83            txn_id,
84            unit_id_slave_addr.get(),
85            mei_type,
86            data,
87            self.transport.transport_type(),
88        )?;
89
90        // If this is a broadcast and serial transport, we do not expect a response. Do not queue it.
91        if unit_id_slave_addr.is_broadcast() {
92            if self.transport.transport_type().is_tcp_type() {
93                return Err(MbusError::BroadcastNotAllowed);
94            }
95        } else {
96            self.add_an_expectation(
97                txn_id,
98                unit_id_slave_addr,
99                &frame,
100                OperationMeta::Diag(Diag {
101                    device_id_code: ReadDeviceIdCode::Err,
102                    encap_type: mei_type,
103                }),
104                Self::handle_encapsulated_interface_transport_rsp,
105            )?;
106        }
107
108        self.dispatch_request_frame(txn_id, unit_id_slave_addr, &frame)?;
109        Ok(())
110    }
111
112    /// Sends a Read Exception Status request (Function Code 07).
113    ///
114    /// This function is specific to **Serial Line** Modbus. It is used to read the contents
115    /// of eight Exception Status outputs in a remote device. The meaning of these status
116    /// bits is device-dependent.
117    ///
118    /// # Parameters
119    /// - `txn_id`: Transaction ID of the original request. While Modbus Serial (RTU/ASCII)
120    ///   does not natively use transaction IDs, the stack preserves the ID provided in
121    ///   the request and returns it here to allow for asynchronous tracking.
122    /// - `unit_id_slave_addr`: The target Modbus unit ID or slave address.
123    ///   - `unit_id`: if transport is tcp
124    ///   - `slave_addr`: if transport is serial
125    #[must_use = "request submission errors should be handled; the request may not have been queued/sent"]
126    pub fn read_exception_status(
127        &mut self,
128        txn_id: u16,
129        unit_id_slave_addr: UnitIdOrSlaveAddr,
130    ) -> Result<(), MbusError> {
131        // FC 07 does not support broadcast addresses as it requires a specific device response.
132        if unit_id_slave_addr.is_broadcast() {
133            return Err(MbusError::BroadcastNotAllowed);
134        }
135        // Delegate PDU and ADU construction to the ServiceBuilder.
136        let frame = diagnostic::service::ServiceBuilder::read_exception_status(
137            unit_id_slave_addr.get(),
138            self.transport.transport_type(),
139        )?;
140
141        // Register the expectation so the client knows how to handle the incoming response byte.
142        self.add_an_expectation(
143            txn_id,
144            unit_id_slave_addr,
145            &frame,
146            OperationMeta::Other,
147            Self::handle_read_exception_status_rsp,
148        )?;
149
150        // Dispatch the frame through the configured serial transport.
151        self.dispatch_request_frame(txn_id, unit_id_slave_addr, &frame)?;
152        Ok(())
153    }
154
155    /// Sends a Diagnostics request (Function Code 08).
156    ///
157    /// This function provides a series of tests for checking the communication system
158    /// between a client (Master) and a server (Slave), or for checking various internal
159    /// error conditions within a server.
160    ///
161    /// **Note:** This function code is supported on **Serial Line only** (RTU/ASCII).
162    ///
163    /// # Parameters
164    /// - `txn_id`: Transaction ID of the original request. While Modbus Serial (RTU/ASCII)
165    ///   does not natively use transaction IDs, the stack preserves the ID provided in
166    ///   the request and returns it here to allow for asynchronous tracking.
167    /// - `unit_id_slave_addr`: The target Modbus unit ID or slave address.
168    ///   - `unit_id`: if transport is tcp
169    ///   - `slave_addr`: if transport is serial/// - `sub_function`: The specific diagnostic test to perform (e.g., `ReturnQueryData`,
170    ///     `RestartCommunicationsOption`, `ClearCounters`).
171    /// - `data`: A slice of 16-bit words required by the specific sub-function. Many
172    ///   sub-functions expect a single word (e.g., `0x0000` or `0xFF00`).
173    ///
174    /// # Broadcast Support
175    /// Only the following sub-functions are allowed with a broadcast address:
176    /// - `RestartCommunicationsOption`
177    /// - `ForceListenOnlyMode`
178    /// - `ClearCountersAndDiagnosticRegister`
179    /// - `ClearOverrunCounterAndFlag`
180    ///
181    /// If a broadcast is sent, no response is expected and no expectation is queued.
182    #[must_use = "request submission errors should be handled; the request may not have been queued/sent"]
183    pub fn diagnostics(
184        &mut self,
185        txn_id: u16,
186        unit_id_slave_addr: UnitIdOrSlaveAddr,
187        sub_function: DiagnosticSubFunction,
188        data: &[u16],
189    ) -> Result<(), MbusError> {
190        const ALLOWED_BROADCAST_SUB_FUNCTIONS: [DiagnosticSubFunction; 4] = [
191            DiagnosticSubFunction::RestartCommunicationsOption,
192            DiagnosticSubFunction::ForceListenOnlyMode,
193            DiagnosticSubFunction::ClearCountersAndDiagnosticRegister,
194            DiagnosticSubFunction::ClearOverrunCounterAndFlag,
195        ];
196        if unit_id_slave_addr.is_broadcast()
197            && !ALLOWED_BROADCAST_SUB_FUNCTIONS.contains(&sub_function)
198        {
199            return Err(MbusError::BroadcastNotAllowed);
200        }
201        let frame = diagnostic::service::ServiceBuilder::diagnostics(
202            unit_id_slave_addr.get(),
203            sub_function,
204            data,
205            self.transport.transport_type(),
206        )?;
207
208        // If this is a broadcast and serial transport, we do not expect a response. Do not queue it.
209        // Note: TCP evaluation isn't strictly needed here because ServiceBuilder::diagnostics
210        // already restricts this to serial only, but we check broadcast to avoid queuing.
211
212        if !unit_id_slave_addr.is_broadcast() {
213            self.add_an_expectation(
214                txn_id,
215                unit_id_slave_addr,
216                &frame,
217                OperationMeta::Other,
218                Self::handle_diagnostics_rsp,
219            )?;
220        }
221
222        self.dispatch_request_frame(txn_id, unit_id_slave_addr, &frame)?;
223        Ok(())
224    }
225
226    /// Sends a Get Comm Event Counter request (FC 11). Serial Line only.
227    ///
228    /// # Parameters
229    /// - `txn_id`: Transaction ID of the original request. While Modbus Serial (RTU/ASCII)
230    ///   does not natively use transaction IDs, the stack preserves the ID provided in
231    ///   the request and returns it here to allow for asynchronous tracking.
232    /// - `unit_id_slave_addr`: The target Modbus unit ID or slave address.
233    ///   - `unit_id`: if transport is tcp
234    ///   - `slave_addr`: if transport is serial
235    #[must_use = "request submission errors should be handled; the request may not have been queued/sent"]
236    pub fn get_comm_event_counter(
237        &mut self,
238        txn_id: u16,
239        unit_id_slave_addr: UnitIdOrSlaveAddr,
240    ) -> Result<(), MbusError> {
241        if unit_id_slave_addr.is_broadcast() {
242            return Err(MbusError::BroadcastNotAllowed);
243        }
244        let frame = diagnostic::service::ServiceBuilder::get_comm_event_counter(
245            unit_id_slave_addr.get(),
246            self.transport.transport_type(),
247        )?;
248
249        self.add_an_expectation(
250            txn_id,
251            unit_id_slave_addr,
252            &frame,
253            OperationMeta::Other,
254            Self::handle_get_comm_event_counter_rsp,
255        )?;
256
257        self.dispatch_request_frame(txn_id, unit_id_slave_addr, &frame)?;
258        Ok(())
259    }
260
261    /// Sends a Get Comm Event Log request (FC 12). Serial Line only.
262    ///
263    /// # Parameters
264    /// - `txn_id`: Transaction ID of the original request. While Modbus Serial (RTU/ASCII)
265    ///   does not natively use transaction IDs, the stack preserves the ID provided in
266    ///   the request and returns it here to allow for asynchronous tracking.
267    /// - `unit_id_slave_addr`: The target Modbus unit ID or slave address.
268    ///   - `unit_id`: if transport is tcp
269    ///   - `slave_addr`: if transport is serial
270    #[must_use = "request submission errors should be handled; the request may not have been queued/sent"]
271    pub fn get_comm_event_log(
272        &mut self,
273        txn_id: u16,
274        unit_id_slave_addr: UnitIdOrSlaveAddr,
275    ) -> Result<(), MbusError> {
276        if unit_id_slave_addr.is_broadcast() {
277            return Err(MbusError::BroadcastNotAllowed);
278        }
279        let frame = diagnostic::service::ServiceBuilder::get_comm_event_log(
280            unit_id_slave_addr.get(),
281            self.transport.transport_type(),
282        )?;
283
284        self.add_an_expectation(
285            txn_id,
286            unit_id_slave_addr,
287            &frame,
288            OperationMeta::Other,
289            Self::handle_get_comm_event_log_rsp,
290        )?;
291
292        self.dispatch_request_frame(txn_id, unit_id_slave_addr, &frame)?;
293        Ok(())
294    }
295
296    /// Sends a Report Server ID request (FC 17). Serial Line only.
297    ///
298    /// # Parameters
299    /// - `txn_id`: Transaction ID of the original request. While Modbus Serial (RTU/ASCII)
300    ///   does not natively use transaction IDs, the stack preserves the ID provided in
301    ///   the request and returns it here to allow for asynchronous tracking.
302    /// - `unit_id_slave_addr`: The target Modbus unit ID or slave address.
303    ///   - `unit_id`: if transport is tcp
304    ///   - `slave_addr`: if transport is serial
305    #[must_use = "request submission errors should be handled; the request may not have been queued/sent"]
306    pub fn report_server_id(
307        &mut self,
308        txn_id: u16,
309        unit_id_slave_addr: UnitIdOrSlaveAddr,
310    ) -> Result<(), MbusError> {
311        if unit_id_slave_addr.is_broadcast() {
312            return Err(MbusError::BroadcastNotAllowed);
313        }
314
315        let frame = diagnostic::service::ServiceBuilder::report_server_id(
316            unit_id_slave_addr.get(),
317            self.transport.transport_type(),
318        )?;
319
320        self.add_an_expectation(
321            txn_id,
322            unit_id_slave_addr,
323            &frame,
324            OperationMeta::Other,
325            Self::handle_report_server_id_rsp,
326        )?;
327
328        self.dispatch_request_frame(txn_id, unit_id_slave_addr, &frame)?;
329        Ok(())
330    }
331}