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}