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}