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
use mbus_core::{
errors::MbusError,
models::coil::Coils,
transport::{Transport, UnitIdOrSlaveAddr},
};
use crate::{
app::CoilResponse,
services::{ClientCommon, ClientServices, Multiple, OperationMeta, Single, coil},
};
impl<TRANSPORT, APP, const N: usize> ClientServices<TRANSPORT, APP, N>
where
TRANSPORT: Transport,
APP: ClientCommon + CoilResponse,
{
/// Sends a Read Coils request to the specified unit ID and address range, and records the expected response.
///
/// # 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/// - `address`: The starting address of the coils to read.
/// - `quantity`: The number of coils to read.
///
/// # Returns
/// - `Ok(())`: If the request was successfully compiled, registered in the expectation queue, and sent.
/// - `Err(MbusError)`: If validation fails (e.g., broadcast read), the PDU is invalid, or transport fails.
#[must_use = "request submission errors should be handled; the request may not have been queued/sent"]
pub fn read_multiple_coils(
&mut self,
txn_id: u16,
unit_id_slave_addr: UnitIdOrSlaveAddr,
address: u16,
quantity: u16,
) -> Result<(), MbusError> {
if unit_id_slave_addr.is_broadcast() {
return Err(MbusError::BroadcastNotAllowed); // Modbus forbids broadcast Read operations
}
// 1. Compile the ADU frame (PDU + Transport Header/Footer)
// Traces to: coil::service::ServiceBuilder -> ReqPduCompiler::read_coils_request
let frame = coil::service::ServiceBuilder::read_coils(
txn_id,
unit_id_slave_addr.get(),
address,
quantity,
self.transport.transport_type(),
)?;
// 2. Register the request in the expectation manager to handle the incoming response
// Traces to: ClientServices::add_an_expectation
self.add_an_expectation(
txn_id,
unit_id_slave_addr,
&frame,
OperationMeta::Multiple(Multiple {
address, // Starting address of the read operation
quantity, // Number of coils to read
}),
Self::handle_read_coils_response,
)?;
// 3. Dispatch the raw bytes to the physical/network layer
self.transport
.send(&frame)
.map_err(|_e| MbusError::SendFailed)?;
Ok(())
}
/// Sends a Read Single Coil request to the specified unit ID and address, and records the expected response.
/// This method is a convenience wrapper around `read_multiple_coils` for
/// reading a single coil, which simplifies the application logic when only one coil needs to be read.
///
/// # 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/// - `address`: The address of the coil to read.
///
/// # Returns
/// - `Ok(())`: If the request was successfully compiled, registered in the expectation queue, and sent.
/// - `Err(MbusError)`: If validation fails (e.g., broadcast read), the PDU is invalid, or transport fails.
///
/// Note: This uses FC 0x01 with a quantity of 1.
#[must_use = "request submission errors should be handled; the request may not have been queued/sent"]
pub fn read_single_coil(
&mut self,
txn_id: u16,
unit_id_slave_addr: UnitIdOrSlaveAddr,
address: u16,
) -> Result<(), MbusError> {
if unit_id_slave_addr.is_broadcast() {
return Err(MbusError::BroadcastNotAllowed); // Modbus forbids broadcast Read operations
}
// Traces to: coil::service::ServiceBuilder -> ReqPduCompiler::read_coils_request (qty=1)
let transport_type = self.transport.transport_type();
let frame = coil::service::ServiceBuilder::read_coils(
txn_id,
unit_id_slave_addr.get(),
address,
1,
transport_type,
)?;
// Uses OperationMeta::Single to trigger handle_read_coils_response's single-coil logic
self.add_an_expectation(
txn_id,
unit_id_slave_addr,
&frame,
OperationMeta::Single(Single {
address, // Address of the single coil
value: 0, // Value is not relevant for read requests
}),
Self::handle_read_coils_response,
)?;
self.transport
.send(&frame)
.map_err(|_e| MbusError::SendFailed)?;
Ok(())
}
/// Sends a Write Single Coil request to the specified unit ID and address with the given value, and records the expected response.
///
/// # 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/// - `address`: The address of the coil to write.
/// - `value`: The boolean value to write to the coil (true for ON, false for OFF).
///
/// # Returns
/// - `Ok(())`: If the request was successfully compiled, registered in the expectation queue, and sent.
/// - `Err(MbusError)`: If validation fails (e.g., broadcast read), the PDU is invalid, or transport fails.
#[must_use = "request submission errors should be handled; the request may not have been queued/sent"]
pub fn write_single_coil(
&mut self,
txn_id: u16,
unit_id_slave_addr: UnitIdOrSlaveAddr,
address: u16,
value: bool,
) -> Result<(), MbusError> {
let transport_type = self.transport.transport_type(); // Access self.transport directly
// Traces to: coil::service::ServiceBuilder -> ReqPduCompiler::write_single_coil_request
let frame = coil::service::ServiceBuilder::write_single_coil(
txn_id,
unit_id_slave_addr.get(),
address,
value,
transport_type,
)?;
// Modbus TCP typically does not support broadcast.
// Serial Modbus (RTU/ASCII) allows broadcast writes, but the client MUST NOT
// expect a response from the server(s).
if unit_id_slave_addr.is_broadcast() {
if transport_type.is_tcp_type() {
return Err(MbusError::BroadcastNotAllowed); // Modbus TCP typically does not support broadcast
}
} else {
// Only add expectation if not a broadcast; servers do not respond to broadcast writes
self.add_an_expectation(
txn_id,
unit_id_slave_addr,
&frame,
OperationMeta::Single(Single {
address, // Address of the coil
value: value as u16, // Value written (0x0000 or 0xFF00)
}),
Self::handle_write_single_coil_response,
)?;
}
self.transport
.send(&frame)
.map_err(|_e| MbusError::SendFailed)?;
Ok(())
}
/// Sends a Write Multiple Coils request to the specified unit ID and address with the given values, and records the expected response.
///
/// # 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/// - `address`: The starting address of the coils to write.
/// - `quantity`: The number of coils to write.
/// - `values`: A slice of boolean values to write to the coils (true for ON, false for OFF).
///
/// # Returns
/// - `Ok(())`: If the request was successfully compiled, registered in the expectation queue, and sent.
/// - `Err(MbusError)`: If validation fails (e.g., broadcast read), the PDU is invalid, or transport fails.
#[must_use = "request submission errors should be handled; the request may not have been queued/sent"]
pub fn write_multiple_coils(
&mut self,
txn_id: u16,
unit_id_slave_addr: UnitIdOrSlaveAddr,
address: u16,
values: &Coils,
) -> Result<(), MbusError> {
let transport_type = self.transport.transport_type(); // Access self.transport directly
// Traces to: coil::service::ServiceBuilder -> ReqPduCompiler::write_multiple_coils_request
let frame = coil::service::ServiceBuilder::write_multiple_coils(
txn_id,
unit_id_slave_addr.get(),
address,
values.quantity(),
values,
transport_type,
)?;
// Modbus TCP typically does not support broadcast.
// Serial Modbus (RTU/ASCII) allows broadcast writes, but the client MUST NOT
// expect a response from the server(s).
if unit_id_slave_addr.is_broadcast() {
if transport_type.is_tcp_type() {
return Err(MbusError::BroadcastNotAllowed); // Modbus TCP typically does not support broadcast
}
} else {
self.add_an_expectation(
txn_id,
unit_id_slave_addr,
&frame,
OperationMeta::Multiple(Multiple {
address, // Starting address of the coils
quantity: values.quantity(), // Number of coils written
}),
Self::handle_write_multiple_coils_response,
)?;
}
self.transport
.send(&frame)
.map_err(|_e| MbusError::SendFailed)?;
Ok(())
}
}