mbus_client/services/register/apis.rs
1use crate::app::RegisterResponse;
2use crate::services::{
3 ClientCommon, ClientServices, Mask, Multiple, OperationMeta, Single, register,
4};
5use mbus_core::{
6 errors::MbusError,
7 transport::{Transport, UnitIdOrSlaveAddr},
8};
9
10impl<TRANSPORT, APP, const N: usize> ClientServices<TRANSPORT, APP, N>
11where
12 TRANSPORT: Transport,
13 APP: RegisterResponse + ClientCommon,
14{
15 /// Sends a Read Holding Registers request to the specified unit ID and address range, and records the expected response.
16 ///
17 /// # Parameters
18 /// - `txn_id`: Transaction ID of the original request. While Modbus Serial (RTU/ASCII)
19 /// does not natively use transaction IDs, the stack preserves the ID provided in
20 /// the request and returns it here to allow for asynchronous tracking.
21 /// - `unit_id_slave_addr`: The target Modbus unit ID or slave address.
22 /// - `unit_id`: if transport is tcp
23 /// - `slave_addr`: if transport is serial
24 /// - `from_address`: The starting address of the holding registers to read.
25 /// - `quantity`: The number of holding registers to read.
26 ///
27 /// # Returns
28 /// `Ok(())` if the request was successfully enqueued and transmitted.
29 ///
30 /// # Errors
31 /// Returns `Err(MbusError::BroadcastNotAllowed)` if attempting to read from address `0` (Broadcast).
32 #[must_use = "request submission errors should be handled; the request may not have been queued/sent"]
33 pub fn read_holding_registers(
34 &mut self,
35 txn_id: u16,
36 unit_id_slave_addr: UnitIdOrSlaveAddr,
37 from_address: u16,
38 quantity: u16,
39 ) -> Result<(), MbusError> {
40 if unit_id_slave_addr.is_broadcast() {
41 return Err(MbusError::BroadcastNotAllowed); // Modbus forbids broadcast Read operations
42 }
43
44 let frame = register::service::ServiceBuilder::read_holding_registers(
45 txn_id,
46 unit_id_slave_addr.get(),
47 from_address,
48 quantity,
49 self.transport.transport_type(),
50 )?;
51
52 self.add_an_expectation(
53 txn_id,
54 unit_id_slave_addr,
55 &frame,
56 OperationMeta::Multiple(Multiple {
57 address: from_address, // Starting address of the read operation
58 quantity, // Number of registers to read
59 }),
60 Self::handle_read_holding_registers_response,
61 )?;
62
63 self.dispatch_request_frame(txn_id, unit_id_slave_addr, &frame)?;
64
65 Ok(())
66 }
67
68 /// Sends a Read Holding Registers request for a single register (Function Code 0x03).
69 ///
70 /// This is a convenience wrapper around `read_holding_registers` with a quantity of 1.
71 /// It allows the application to receive a simplified `read_single_holding_register_response`
72 /// callback instead of handling a register collection.
73 ///
74 /// # Parameters
75 /// - `txn_id`: Transaction ID of the original request. While Modbus Serial (RTU/ASCII)
76 /// does not natively use transaction IDs, the stack preserves the ID provided in
77 /// the request and returns it here to allow for asynchronous tracking.
78 /// - `unit_id_slave_addr`: The target Modbus unit ID or slave address.
79 /// - `unit_id`: if transport is tcp
80 /// - `slave_addr`: if transport is serial
81 /// - `address`: The starting address of the holding registers to read.
82 ///
83 /// # Returns
84 /// `Ok(())` if the request was successfully enqueued and transmitted.
85 ///
86 /// # Errors
87 /// Returns `Err(MbusError::BroadcastNotAllowed)` if attempting to read from address `0` (Broadcast).
88 #[must_use = "request submission errors should be handled; the request may not have been queued/sent"]
89 pub fn read_single_holding_register(
90 &mut self,
91 txn_id: u16,
92 unit_id_slave_addr: UnitIdOrSlaveAddr,
93 address: u16,
94 ) -> Result<(), MbusError> {
95 use crate::services::Single;
96
97 // Modbus protocol specification: Broadcast is not supported for Read operations.
98 if unit_id_slave_addr.is_broadcast() {
99 return Err(MbusError::BroadcastNotAllowed); // Modbus forbids broadcast Read operations
100 }
101
102 // Construct the ADU frame using the register service builder with quantity = 1
103 let frame = register::service::ServiceBuilder::read_holding_registers(
104 txn_id,
105 unit_id_slave_addr.get(),
106 address,
107 1, // quantity = 1
108 self.transport.transport_type(),
109 )?;
110
111 // Register an expectation. We use OperationMeta::Single to signal the response
112 // handler to trigger the single-register specific callback in the app layer.
113 self.add_an_expectation(
114 txn_id,
115 unit_id_slave_addr,
116 &frame,
117 OperationMeta::Single(Single {
118 address, // Address of the single register
119 value: 0, // Value is not relevant for read requests
120 }),
121 Self::handle_read_holding_registers_response,
122 )?;
123
124 // Dispatch the compiled frame through the underlying transport.
125 self.dispatch_request_frame(txn_id, unit_id_slave_addr, &frame)?;
126
127 Ok(())
128 }
129
130 /// Sends a Read Input Registers request (Function Code 0x04).
131 ///
132 /// This function is used to read from 1 to 125 contiguous input registers in a remote device.
133 /// Input registers are typically used for read-only data like sensor readings.
134 ///
135 /// # Parameters
136 /// - `txn_id`: Transaction ID of the original request. While Modbus Serial (RTU/ASCII)
137 /// does not natively use transaction IDs, the stack preserves the ID provided in
138 /// the request and returns it here to allow for asynchronous tracking.
139 /// - `unit_id_slave_addr`: The target Modbus unit ID or slave address.
140 /// - `unit_id`: if transport is tcp
141 /// - `slave_addr`: if transport is serial
142 /// - `address`: The starting address of the input registers to read (0x0000 to 0xFFFF).
143 /// - `quantity`: The number of input registers to read (1 to 125).
144 ///
145 /// # Returns
146 /// - `Ok(())`: If the request was successfully built, the expectation was queued,
147 /// and the frame was transmitted.
148 ///
149 /// # Errors
150 /// Returns `Err(MbusError::BroadcastNotAllowed)` if attempting to read from address `0` (Broadcast).
151 #[must_use = "request submission errors should be handled; the request may not have been queued/sent"]
152 pub fn read_input_registers(
153 &mut self,
154 txn_id: u16,
155 unit_id_slave_addr: UnitIdOrSlaveAddr,
156 address: u16,
157 quantity: u16,
158 ) -> Result<(), MbusError> {
159 if unit_id_slave_addr.is_broadcast() {
160 return Err(MbusError::BroadcastNotAllowed); // Modbus forbids broadcast Read operations
161 }
162
163 let frame = register::service::ServiceBuilder::read_input_registers(
164 txn_id,
165 unit_id_slave_addr.get(),
166 address,
167 quantity,
168 self.transport.transport_type(),
169 )?;
170
171 self.add_an_expectation(
172 txn_id,
173 unit_id_slave_addr,
174 &frame,
175 OperationMeta::Multiple(Multiple {
176 address, // Starting address of the read operation
177 quantity, // Number of registers to read
178 }),
179 Self::handle_read_input_registers_response,
180 )?;
181
182 self.dispatch_request_frame(txn_id, unit_id_slave_addr, &frame)?;
183
184 Ok(())
185 }
186
187 /// Sends a Read Input Registers request for a single register (Function Code 0x04).
188 ///
189 /// This is a convenience wrapper around `read_input_registers` with a quantity of 1.
190 /// It allows the application to receive a simplified `read_single_input_register_response`
191 /// callback instead of handling a register collection.
192 ///
193 /// # Parameters
194 /// - `txn_id`: Transaction ID of the original request. While Modbus Serial (RTU/ASCII)
195 /// does not natively use transaction IDs, the stack preserves the ID provided in
196 /// the request and returns it here to allow for asynchronous tracking.
197 /// - `unit_id_slave_addr`: The target Modbus unit ID or slave address.
198 /// - `unit_id`: if transport is tcp
199 /// - `slave_addr`: if transport is serial
200 /// - `address`: The exact address of the input register to read.
201 ///
202 /// # Returns
203 /// `Ok(())` if the request was successfully enqueued and transmitted.
204 ///
205 /// # Errors
206 /// Returns `Err(MbusError::BroadcastNotAllowed)` if attempting to read from a broadcast address.
207 #[must_use = "request submission errors should be handled; the request may not have been queued/sent"]
208 pub fn read_single_input_register(
209 &mut self,
210 txn_id: u16,
211 unit_id_slave_addr: UnitIdOrSlaveAddr,
212 address: u16,
213 ) -> Result<(), MbusError> {
214 if unit_id_slave_addr.is_broadcast() {
215 return Err(MbusError::BroadcastNotAllowed); // Modbus forbids broadcast Read operations
216 }
217
218 let frame = register::service::ServiceBuilder::read_input_registers(
219 txn_id,
220 unit_id_slave_addr.get(),
221 address,
222 1,
223 self.transport.transport_type(),
224 )?;
225
226 self.add_an_expectation(
227 txn_id,
228 unit_id_slave_addr,
229 &frame,
230 OperationMeta::Single(Single {
231 address, // Address of the single register
232 value: 0, // Value is not relevant for read requests
233 }),
234 Self::handle_read_input_registers_response,
235 )?;
236
237 self.dispatch_request_frame(txn_id, unit_id_slave_addr, &frame)?;
238
239 Ok(())
240 }
241
242 /// Sends a Write Single Register request (Function Code 0x06).
243 ///
244 /// This function is used to write a single holding register in a remote device.
245 ///
246 /// # Parameters
247 /// - `txn_id`: Transaction ID of the original request. While Modbus Serial (RTU/ASCII)
248 /// does not natively use transaction IDs, the stack preserves the ID provided in
249 /// the request and returns it here to allow for asynchronous tracking.
250 /// - `unit_id_slave_addr`: The target Modbus unit ID or slave address.
251 /// - `unit_id`: if transport is tcp
252 /// - `slave_addr`: if transport is serial
253 /// - `address`: The address of the holding register to be written.
254 /// - `value`: The 16-bit value to be written to the register.
255 ///
256 /// # Returns
257 /// `Ok(())` if the request was successfully enqueued and transmitted.
258 ///
259 /// # Broadcast Support
260 /// Serial Modbus (RTU/ASCII) allows broadcast writes (Slave Address 0). In this case,
261 /// the request is sent to all slaves, and no response is expected or queued.
262 ///
263 /// # Errors
264 /// Returns `Err(MbusError::BroadcastNotAllowed)` if attempting to broadcast over TCP.
265 #[must_use = "request submission errors should be handled; the request may not have been queued/sent"]
266 pub fn write_single_register(
267 &mut self,
268 txn_id: u16,
269 unit_id_slave_addr: UnitIdOrSlaveAddr,
270 address: u16,
271 value: u16,
272 ) -> Result<(), MbusError> {
273 let transport_type = self.transport.transport_type();
274 let frame = register::service::ServiceBuilder::write_single_register(
275 txn_id,
276 unit_id_slave_addr.get(),
277 address,
278 value,
279 transport_type,
280 )?;
281
282 // Modbus TCP typically does not support broadcast.
283 // Serial Modbus (RTU/ASCII) allows broadcast writes, but the client MUST NOT
284 // expect a response from the server(s).
285 if unit_id_slave_addr.is_broadcast() {
286 if transport_type.is_tcp_type() {
287 return Err(MbusError::BroadcastNotAllowed); // Modbus TCP typically does not support broadcast
288 }
289 } else {
290 self.add_an_expectation(
291 txn_id,
292 unit_id_slave_addr,
293 &frame,
294 OperationMeta::Single(Single { address, value }),
295 Self::handle_write_single_register_response, // Callback for successful response
296 )?; // Expect a response for non-broadcast writes
297 }
298
299 self.dispatch_request_frame(txn_id, unit_id_slave_addr, &frame)?;
300 Ok(())
301 }
302
303 /// Sends a Write Multiple Registers request (Function Code 0x10).
304 ///
305 /// This function is used to write a block of contiguous registers (1 to 123 registers)
306 /// in a remote device.
307 ///
308 /// # Parameters
309 /// - `txn_id`: Transaction ID of the original request. While Modbus Serial (RTU/ASCII)
310 /// does not natively use transaction IDs, the stack preserves the ID provided in
311 /// the request and returns it here to allow for asynchronous tracking.
312 /// - `unit_id_slave_addr`: The target Modbus unit ID or slave address.
313 /// - `unit_id`: if transport is tcp
314 /// - `slave_addr`: if transport is serial
315 /// - `quantity`: The number of registers to write (1 to 123).
316 /// - `values`: A slice of `u16` values to be written. The length must match `quantity`.
317 ///
318 /// # Returns
319 /// `Ok(())` if the request was successfully enqueued and transmitted.
320 ///
321 /// # Broadcast Support
322 /// Serial Modbus allows broadcast. No response is expected for broadcast requests.
323 ///
324 /// # Errors
325 /// Returns `Err(MbusError::BroadcastNotAllowed)` if attempting to broadcast over TCP.
326 #[must_use = "request submission errors should be handled; the request may not have been queued/sent"]
327 pub fn write_multiple_registers(
328 &mut self,
329 txn_id: u16,
330 unit_id_slave_addr: UnitIdOrSlaveAddr,
331 address: u16,
332 quantity: u16,
333 values: &[u16],
334 ) -> Result<(), MbusError> {
335 let transport_type = self.transport.transport_type();
336 let frame = register::service::ServiceBuilder::write_multiple_registers(
337 txn_id,
338 unit_id_slave_addr.get(),
339 address,
340 quantity,
341 values,
342 transport_type,
343 )?;
344
345 // Modbus TCP typically does not support broadcast.
346 // Serial Modbus (RTU/ASCII) allows broadcast writes, but the client MUST NOT
347 // expect a response from the server(s).
348 if unit_id_slave_addr.is_broadcast() {
349 if transport_type.is_tcp_type() {
350 return Err(MbusError::BroadcastNotAllowed); // Modbus TCP typically does not support broadcast
351 }
352 } else {
353 self.add_an_expectation(
354 txn_id,
355 unit_id_slave_addr,
356 &frame,
357 OperationMeta::Multiple(Multiple { address, quantity }),
358 Self::handle_write_multiple_registers_response, // Callback for successful response
359 )?; // Expect a response for non-broadcast writes
360 }
361
362 self.dispatch_request_frame(txn_id, unit_id_slave_addr, &frame)?;
363 Ok(())
364 }
365
366 /// Sends a Read/Write Multiple Registers request (FC 23).
367 ///
368 /// This function performs a combination of one read operation and one write operation in a single
369 /// Modbus transaction. The write operation is performed before the read.
370 ///
371 /// # Parameters
372 /// - `txn_id`: Transaction ID of the original request. While Modbus Serial (RTU/ASCII)
373 /// does not natively use transaction IDs, the stack preserves the ID provided in
374 /// the request and returns it here to allow for asynchronous tracking.
375 /// - `unit_id_slave_addr`: The target Modbus unit ID or slave address.
376 /// - `unit_id`: if transport is tcp
377 /// - `slave_addr`: if transport is serial
378 /// - `read_address`: The starting address of the registers to read.
379 /// - `read_quantity`: The number of registers to read.
380 /// - `write_address`: The starting address of the registers to write.
381 /// - `write_values`: A slice of `u16` values to be written to the device.
382 ///
383 /// # Returns
384 /// `Ok(())` if the request was successfully sent, or an `MbusError` if there was an error
385 /// constructing the request (e.g., invalid quantity) or sending it over the transport.
386 #[must_use = "request submission errors should be handled; the request may not have been queued/sent"]
387 pub fn read_write_multiple_registers(
388 &mut self,
389 txn_id: u16,
390 unit_id_slave_addr: UnitIdOrSlaveAddr,
391 read_address: u16,
392 read_quantity: u16,
393 write_address: u16,
394 write_values: &[u16],
395 ) -> Result<(), MbusError> {
396 if unit_id_slave_addr.is_broadcast() {
397 return Err(MbusError::BroadcastNotAllowed); // FC 23 explicitly forbids broadcast
398 }
399
400 // 1. Construct the ADU frame using the register service
401 let transport_type = self.transport.transport_type();
402 let frame = register::service::ServiceBuilder::read_write_multiple_registers(
403 txn_id,
404 unit_id_slave_addr.get(),
405 read_address,
406 read_quantity,
407 write_address,
408 write_values,
409 transport_type,
410 )?;
411
412 // 2. Queue the expected response to match against the incoming server reply
413 self.add_an_expectation(
414 txn_id,
415 unit_id_slave_addr,
416 &frame,
417 OperationMeta::Multiple(Multiple {
418 address: read_address, // Starting address of the read operation
419 quantity: read_quantity, // Number of registers to read
420 }),
421 Self::handle_read_write_multiple_registers_response,
422 )?;
423
424 // 3. Transmit the frame via the configured transport
425 self.dispatch_request_frame(txn_id, unit_id_slave_addr, &frame)?;
426 Ok(())
427 }
428
429 /// Sends a Mask Write Register request.
430 ///
431 /// This function is used to modify the contents of a single holding register using a combination
432 /// of an AND mask and an OR mask. The new value of the register is calculated as:
433 /// `(current_value AND and_mask) OR (or_mask AND (NOT and_mask))`
434 ///
435 /// The request is added to the `expected_responses` queue to await a corresponding reply from the Modbus server.
436 ///
437 /// # Parameters
438 /// - `txn_id`: Transaction ID of the original request. While Modbus Serial (RTU/ASCII)
439 /// does not natively use transaction IDs, the stack preserves the ID provided in
440 /// the request and returns it here to allow for asynchronous tracking.
441 /// - `unit_id_slave_addr`: The target Modbus unit ID or slave address.
442 /// - `unit_id`: if transport is tcp
443 /// - `slave_addr`: if transport is serial
444 /// - `address`: The address of the register to apply the mask to.
445 /// - `and_mask`: The 16-bit AND mask to apply to the current register value.
446 /// - `or_mask`: The 16-bit OR mask to apply to the current register value.
447 ///
448 /// # Returns
449 /// `Ok(())` if the request was successfully sent and queued for a response,
450 /// or an `MbusError` if there was an error during request construction,
451 /// sending over the transport, or if the `expected_responses` queue is full.
452 #[must_use = "request submission errors should be handled; the request may not have been queued/sent"]
453 pub fn mask_write_register(
454 &mut self,
455 txn_id: u16,
456 unit_id_slave_addr: UnitIdOrSlaveAddr,
457 address: u16,
458 and_mask: u16,
459 or_mask: u16,
460 ) -> Result<(), MbusError> {
461 let frame = register::service::ServiceBuilder::mask_write_register(
462 txn_id,
463 unit_id_slave_addr.get(),
464 address,
465 and_mask,
466 or_mask,
467 self.transport.transport_type(),
468 )?;
469
470 if unit_id_slave_addr.is_broadcast() {
471 if self.transport.transport_type().is_tcp_type() {
472 return Err(MbusError::BroadcastNotAllowed);
473 }
474 } else {
475 self.add_an_expectation(
476 txn_id,
477 unit_id_slave_addr,
478 &frame,
479 OperationMeta::Masking(Mask {
480 address, // Address of the register to mask
481 and_mask, // AND mask used in the request
482 or_mask, // OR mask used in the request
483 }),
484 Self::handle_mask_write_register_response,
485 )?;
486 }
487
488 self.dispatch_request_frame(txn_id, unit_id_slave_addr, &frame)?;
489 Ok(())
490 }
491}