Skip to main content

mbus_client/services/coil/
apis.rs

1use mbus_core::{
2    errors::MbusError,
3    models::coil::Coils,
4    transport::{Transport, UnitIdOrSlaveAddr},
5};
6
7use crate::{
8    app::CoilResponse,
9    services::{ClientCommon, ClientServices, Multiple, OperationMeta, Single, coil},
10};
11
12impl<TRANSPORT, APP, const N: usize> ClientServices<TRANSPORT, APP, N>
13where
14    TRANSPORT: Transport,
15    APP: ClientCommon + CoilResponse,
16{
17    /// Sends a Read Coils request to the specified unit ID and address range, and records the expected response.
18    ///
19    /// # Parameters
20    /// - `txn_id`: Transaction ID of the original request. While Modbus Serial (RTU/ASCII)
21    ///   does not natively use transaction IDs, the stack preserves the ID provided in
22    ///   the request and returns it here to allow for asynchronous tracking.
23    /// - `unit_id_slave_addr`: The target Modbus unit ID or slave address.
24    ///   - `unit_id`: if transport is tcp
25    ///   - `slave_addr`: if transport is serial/// - `address`: The starting address of the coils to read.
26    /// - `quantity`: The number of coils to read.
27    ///
28    /// # Returns
29    /// - `Ok(())`: If the request was successfully compiled, registered in the expectation queue, and sent.
30    /// - `Err(MbusError)`: If validation fails (e.g., broadcast read), the PDU is invalid, or transport fails.
31    #[must_use = "request submission errors should be handled; the request may not have been queued/sent"]
32    pub fn read_multiple_coils(
33        &mut self,
34        txn_id: u16,
35        unit_id_slave_addr: UnitIdOrSlaveAddr,
36        address: u16,
37        quantity: u16,
38    ) -> Result<(), MbusError> {
39        if unit_id_slave_addr.is_broadcast() {
40            return Err(MbusError::BroadcastNotAllowed); // Modbus forbids broadcast Read operations
41        }
42
43        // 1. Compile the ADU frame (PDU + Transport Header/Footer)
44        // Traces to: coil::service::ServiceBuilder -> ReqPduCompiler::read_coils_request
45        let frame = coil::service::ServiceBuilder::read_coils(
46            txn_id,
47            unit_id_slave_addr.get(),
48            address,
49            quantity,
50            self.transport.transport_type(),
51        )?;
52
53        // 2. Register the request in the expectation manager to handle the incoming response
54        // Traces to: ClientServices::add_an_expectation
55        self.add_an_expectation(
56            txn_id,
57            unit_id_slave_addr,
58            &frame,
59            OperationMeta::Multiple(Multiple {
60                address,  // Starting address of the read operation
61                quantity, // Number of coils to read
62            }),
63            Self::handle_read_coils_response,
64        )?;
65
66        // 3. Dispatch the raw bytes to the physical/network layer
67        self.dispatch_request_frame(txn_id, unit_id_slave_addr, &frame)?;
68
69        Ok(())
70    }
71
72    /// Sends a Read Single Coil request to the specified unit ID and address, and records the expected response.
73    /// This method is a convenience wrapper around `read_multiple_coils` for
74    /// reading a single coil, which simplifies the application logic when only one coil needs to be read.
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/// - `address`: The address of the coil to read.
83    ///
84    /// # Returns
85    /// - `Ok(())`: If the request was successfully compiled, registered in the expectation queue, and sent.
86    /// - `Err(MbusError)`: If validation fails (e.g., broadcast read), the PDU is invalid, or transport fails.
87    ///
88    /// Note: This uses FC 0x01 with a quantity of 1.
89    #[must_use = "request submission errors should be handled; the request may not have been queued/sent"]
90    pub fn read_single_coil(
91        &mut self,
92        txn_id: u16,
93        unit_id_slave_addr: UnitIdOrSlaveAddr,
94        address: u16,
95    ) -> Result<(), MbusError> {
96        if unit_id_slave_addr.is_broadcast() {
97            return Err(MbusError::BroadcastNotAllowed); // Modbus forbids broadcast Read operations
98        }
99
100        // Traces to: coil::service::ServiceBuilder -> ReqPduCompiler::read_coils_request (qty=1)
101        let transport_type = self.transport.transport_type();
102        let frame = coil::service::ServiceBuilder::read_coils(
103            txn_id,
104            unit_id_slave_addr.get(),
105            address,
106            1,
107            transport_type,
108        )?;
109
110        // Uses OperationMeta::Single to trigger handle_read_coils_response's single-coil logic
111        self.add_an_expectation(
112            txn_id,
113            unit_id_slave_addr,
114            &frame,
115            OperationMeta::Single(Single {
116                address,  // Address of the single coil
117                value: 0, // Value is not relevant for read requests
118            }),
119            Self::handle_read_coils_response,
120        )?;
121
122        self.dispatch_request_frame(txn_id, unit_id_slave_addr, &frame)?;
123
124        Ok(())
125    }
126
127    /// Sends a Write Single Coil request to the specified unit ID and address with the given value, and records the expected response.
128    ///
129    /// # Parameters
130    /// - `txn_id`: Transaction ID of the original request. While Modbus Serial (RTU/ASCII)
131    ///   does not natively use transaction IDs, the stack preserves the ID provided in
132    ///   the request and returns it here to allow for asynchronous tracking.
133    /// - `unit_id_slave_addr`: The target Modbus unit ID or slave address.
134    ///   - `unit_id`: if transport is tcp
135    ///   - `slave_addr`: if transport is serial/// - `address`: The address of the coil to write.
136    /// - `value`: The boolean value to write to the coil (true for ON, false for OFF).
137    ///
138    /// # Returns
139    /// - `Ok(())`: If the request was successfully compiled, registered in the expectation queue, and sent.
140    /// - `Err(MbusError)`: If validation fails (e.g., broadcast read), the PDU is invalid, or transport fails.
141    #[must_use = "request submission errors should be handled; the request may not have been queued/sent"]
142    pub fn write_single_coil(
143        &mut self,
144        txn_id: u16,
145        unit_id_slave_addr: UnitIdOrSlaveAddr,
146        address: u16,
147        value: bool,
148    ) -> Result<(), MbusError> {
149        let transport_type = self.transport.transport_type(); // Access self.transport directly
150
151        // Traces to: coil::service::ServiceBuilder -> ReqPduCompiler::write_single_coil_request
152        let frame = coil::service::ServiceBuilder::write_single_coil(
153            txn_id,
154            unit_id_slave_addr.get(),
155            address,
156            value,
157            transport_type,
158        )?;
159
160        // Modbus TCP typically does not support broadcast.
161        // Serial Modbus (RTU/ASCII) allows broadcast writes, but the client MUST NOT
162        // expect a response from the server(s).
163        if unit_id_slave_addr.is_broadcast() {
164            if transport_type.is_tcp_type() {
165                return Err(MbusError::BroadcastNotAllowed); // Modbus TCP typically does not support broadcast
166            }
167        } else {
168            // Only add expectation if not a broadcast; servers do not respond to broadcast writes
169            self.add_an_expectation(
170                txn_id,
171                unit_id_slave_addr,
172                &frame,
173                OperationMeta::Single(Single {
174                    address,             // Address of the coil
175                    value: value as u16, // Value written (0x0000 or 0xFF00)
176                }),
177                Self::handle_write_single_coil_response,
178            )?;
179        }
180
181        self.dispatch_request_frame(txn_id, unit_id_slave_addr, &frame)?;
182        Ok(())
183    }
184
185    /// Sends a Write Multiple Coils request to the specified unit ID and address with the given values, and records the expected response.
186    ///
187    /// # Parameters
188    /// - `txn_id`: Transaction ID of the original request. While Modbus Serial (RTU/ASCII)
189    ///   does not natively use transaction IDs, the stack preserves the ID provided in
190    ///   the request and returns it here to allow for asynchronous tracking.
191    /// - `unit_id_slave_addr`: The target Modbus unit ID or slave address.
192    ///   - `unit_id`: if transport is tcp
193    ///   - `slave_addr`: if transport is serial/// - `address`: The starting address of the coils to write.
194    /// - `quantity`: The number of coils to write.
195    /// - `values`: A slice of boolean values to write to the coils (true for ON, false for OFF).
196    ///
197    /// # Returns
198    /// - `Ok(())`: If the request was successfully compiled, registered in the expectation queue, and sent.
199    /// - `Err(MbusError)`: If validation fails (e.g., broadcast read), the PDU is invalid, or transport fails.
200    #[must_use = "request submission errors should be handled; the request may not have been queued/sent"]
201    pub fn write_multiple_coils(
202        &mut self,
203        txn_id: u16,
204        unit_id_slave_addr: UnitIdOrSlaveAddr,
205        address: u16,
206        values: &Coils,
207    ) -> Result<(), MbusError> {
208        let transport_type = self.transport.transport_type(); // Access self.transport directly
209
210        // Traces to: coil::service::ServiceBuilder -> ReqPduCompiler::write_multiple_coils_request
211        let frame = coil::service::ServiceBuilder::write_multiple_coils(
212            txn_id,
213            unit_id_slave_addr.get(),
214            address,
215            values.quantity(),
216            values,
217            transport_type,
218        )?;
219
220        // Modbus TCP typically does not support broadcast.
221        // Serial Modbus (RTU/ASCII) allows broadcast writes, but the client MUST NOT
222        // expect a response from the server(s).
223        if unit_id_slave_addr.is_broadcast() {
224            if transport_type.is_tcp_type() {
225                return Err(MbusError::BroadcastNotAllowed); // Modbus TCP typically does not support broadcast
226            }
227        } else {
228            self.add_an_expectation(
229                txn_id,
230                unit_id_slave_addr,
231                &frame,
232                OperationMeta::Multiple(Multiple {
233                    address,                     // Starting address of the coils
234                    quantity: values.quantity(), // Number of coils written
235                }),
236                Self::handle_write_multiple_coils_response,
237            )?;
238        }
239
240        self.dispatch_request_frame(txn_id, unit_id_slave_addr, &frame)?;
241        Ok(())
242    }
243}