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