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            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            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            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            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 = 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 = 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 = 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            TRANSPORT::TRANSPORT_TYPE,
468        )?;
469
470        if unit_id_slave_addr.is_broadcast() {
471            if 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}