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