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}