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.transport
68            .send(&frame)
69            .map_err(|_e| MbusError::SendFailed)?;
70
71        Ok(())
72    }
73
74    /// Sends a Read Single Coil request to the specified unit ID and address, and records the expected response.
75    /// This method is a convenience wrapper around `read_multiple_coils` for
76    /// reading a single coil, which simplifies the application logic when only one coil needs to be read.
77    ///
78    /// # Parameters
79    /// - `txn_id`: Transaction ID of the original request. While Modbus Serial (RTU/ASCII)
80    ///   does not natively use transaction IDs, the stack preserves the ID provided in
81    ///   the request and returns it here to allow for asynchronous tracking.
82    /// - `unit_id_slave_addr`: The target Modbus unit ID or slave address.
83    ///   - `unit_id`: if transport is tcp
84    ///   - `slave_addr`: if transport is serial/// - `address`: The address of the coil to read.
85    ///
86    /// # Returns
87    /// - `Ok(())`: If the request was successfully compiled, registered in the expectation queue, and sent.
88    /// - `Err(MbusError)`: If validation fails (e.g., broadcast read), the PDU is invalid, or transport fails.
89    ///
90    /// Note: This uses FC 0x01 with a quantity of 1.
91    #[must_use = "request submission errors should be handled; the request may not have been queued/sent"]
92    pub fn read_single_coil(
93        &mut self,
94        txn_id: u16,
95        unit_id_slave_addr: UnitIdOrSlaveAddr,
96        address: u16,
97    ) -> Result<(), MbusError> {
98        if unit_id_slave_addr.is_broadcast() {
99            return Err(MbusError::BroadcastNotAllowed); // Modbus forbids broadcast Read operations
100        }
101
102        // Traces to: coil::service::ServiceBuilder -> ReqPduCompiler::read_coils_request (qty=1)
103        let transport_type = self.transport.transport_type();
104        let frame = coil::service::ServiceBuilder::read_coils(
105            txn_id,
106            unit_id_slave_addr.get(),
107            address,
108            1,
109            transport_type,
110        )?;
111
112        // Uses OperationMeta::Single to trigger handle_read_coils_response's single-coil logic
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 coil
119                value: 0, // Value is not relevant for read requests
120            }),
121            Self::handle_read_coils_response,
122        )?;
123
124        self.transport
125            .send(&frame)
126            .map_err(|_e| MbusError::SendFailed)?;
127
128        Ok(())
129    }
130
131    /// Sends a Write Single Coil request to the specified unit ID and address with the given value, and records the expected response.
132    ///
133    /// # Parameters
134    /// - `txn_id`: Transaction ID of the original request. While Modbus Serial (RTU/ASCII)
135    ///   does not natively use transaction IDs, the stack preserves the ID provided in
136    ///   the request and returns it here to allow for asynchronous tracking.
137    /// - `unit_id_slave_addr`: The target Modbus unit ID or slave address.
138    ///   - `unit_id`: if transport is tcp
139    ///   - `slave_addr`: if transport is serial/// - `address`: The address of the coil to write.
140    /// - `value`: The boolean value to write to the coil (true for ON, false for OFF).
141    ///
142    /// # Returns
143    /// - `Ok(())`: If the request was successfully compiled, registered in the expectation queue, and sent.
144    /// - `Err(MbusError)`: If validation fails (e.g., broadcast read), the PDU is invalid, or transport fails.
145    #[must_use = "request submission errors should be handled; the request may not have been queued/sent"]
146    pub fn write_single_coil(
147        &mut self,
148        txn_id: u16,
149        unit_id_slave_addr: UnitIdOrSlaveAddr,
150        address: u16,
151        value: bool,
152    ) -> Result<(), MbusError> {
153        let transport_type = self.transport.transport_type(); // Access self.transport directly
154
155        // Traces to: coil::service::ServiceBuilder -> ReqPduCompiler::write_single_coil_request
156        let frame = coil::service::ServiceBuilder::write_single_coil(
157            txn_id,
158            unit_id_slave_addr.get(),
159            address,
160            value,
161            transport_type,
162        )?;
163
164        // Modbus TCP typically does not support broadcast.
165        // Serial Modbus (RTU/ASCII) allows broadcast writes, but the client MUST NOT
166        // expect a response from the server(s).
167        if unit_id_slave_addr.is_broadcast() {
168            if transport_type.is_tcp_type() {
169                return Err(MbusError::BroadcastNotAllowed); // Modbus TCP typically does not support broadcast
170            }
171        } else {
172            // Only add expectation if not a broadcast; servers do not respond to broadcast writes
173            self.add_an_expectation(
174                txn_id,
175                unit_id_slave_addr,
176                &frame,
177                OperationMeta::Single(Single {
178                    address,             // Address of the coil
179                    value: value as u16, // Value written (0x0000 or 0xFF00)
180                }),
181                Self::handle_write_single_coil_response,
182            )?;
183        }
184
185        self.transport
186            .send(&frame)
187            .map_err(|_e| MbusError::SendFailed)?;
188        Ok(())
189    }
190
191    /// Sends a Write Multiple Coils request to the specified unit ID and address with the given values, and records the expected response.
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/// - `address`: The starting address of the coils to write.
200    /// - `quantity`: The number of coils to write.
201    /// - `values`: A slice of boolean values to write to the coils (true for ON, false for OFF).
202    ///
203    /// # Returns
204    /// - `Ok(())`: If the request was successfully compiled, registered in the expectation queue, and sent.
205    /// - `Err(MbusError)`: If validation fails (e.g., broadcast read), the PDU is invalid, or transport fails.
206    #[must_use = "request submission errors should be handled; the request may not have been queued/sent"]
207    pub fn write_multiple_coils(
208        &mut self,
209        txn_id: u16,
210        unit_id_slave_addr: UnitIdOrSlaveAddr,
211        address: u16,
212        values: &Coils,
213    ) -> Result<(), MbusError> {
214        let transport_type = self.transport.transport_type(); // Access self.transport directly
215
216        // Traces to: coil::service::ServiceBuilder -> ReqPduCompiler::write_multiple_coils_request
217        let frame = coil::service::ServiceBuilder::write_multiple_coils(
218            txn_id,
219            unit_id_slave_addr.get(),
220            address,
221            values.quantity(),
222            values,
223            transport_type,
224        )?;
225
226        // Modbus TCP typically does not support broadcast.
227        // Serial Modbus (RTU/ASCII) allows broadcast writes, but the client MUST NOT
228        // expect a response from the server(s).
229        if unit_id_slave_addr.is_broadcast() {
230            if transport_type.is_tcp_type() {
231                return Err(MbusError::BroadcastNotAllowed); // Modbus TCP typically does not support broadcast
232            }
233        } else {
234            self.add_an_expectation(
235                txn_id,
236                unit_id_slave_addr,
237                &frame,
238                OperationMeta::Multiple(Multiple {
239                    address,                     // Starting address of the coils
240                    quantity: values.quantity(), // Number of coils written
241                }),
242                Self::handle_write_multiple_coils_response,
243            )?;
244        }
245
246        self.transport
247            .send(&frame)
248            .map_err(|_e| MbusError::SendFailed)?;
249        Ok(())
250    }
251}