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