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 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 = 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 = 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 = 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}