Skip to main content

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}