Skip to main content

mbus_async/client/
encode.rs

1//! Request frame encoders — one free function per Modbus function code.
2//!
3//! Each function accepts user-supplied parameters plus the transaction id
4//! assigned by the task, builds the corresponding PDU via `mbus_core` helpers,
5//! and wraps it into a complete ADU frame ready to hand to `AsyncTransport::send`.
6//!
7//! # Conventions
8//! - All functions return `Result<Vec<u8, MAX_ADU_FRAME_LEN>, MbusError>`.
9//! - `txn_id` is always the task-assigned identifier; callers never pick it.
10//! - `unit` is the `UnitIdOrSlaveAddr` already validated before this point.
11//! - Broadcast reads are rejected at the `client_core` layer; no guards here.
12
13use heapless::Vec;
14
15use mbus_core::{
16    data_unit::common::{self, MAX_ADU_FRAME_LEN, Pdu},
17    errors::MbusError,
18    function_codes::public::FunctionCode,
19    transport::{TransportType, UnitIdOrSlaveAddr},
20};
21
22#[cfg(feature = "diagnostics")]
23use mbus_core::function_codes::public::{DiagnosticSubFunction, EncapsulatedInterfaceType};
24#[cfg(feature = "coils")]
25use mbus_core::models::coil::Coils;
26#[cfg(feature = "diagnostics")]
27use mbus_core::models::diagnostic::{ObjectId, ReadDeviceIdCode};
28#[cfg(feature = "file-record")]
29use mbus_core::models::file_record::SubRequest;
30
31use crate::client::command::ClientRequest;
32
33// ─── Coils (FC 01 / 05 / 0F) ─────────────────────────────────────────────────
34
35#[cfg(feature = "coils")]
36/// Encodes a Read Multiple Coils (FC 01) request frame.
37pub(crate) fn encode_read_coils(
38    txn_id: u16,
39    unit: UnitIdOrSlaveAddr,
40    address: u16,
41    quantity: u16,
42    transport_type: TransportType,
43) -> Result<Vec<u8, MAX_ADU_FRAME_LEN>, MbusError> {
44    if !(1..=2000).contains(&quantity) {
45        return Err(MbusError::InvalidQuantity);
46    }
47    let pdu = Pdu::build_read_window(FunctionCode::ReadCoils, address, quantity)?;
48    common::compile_adu_frame(txn_id, unit.get(), pdu, transport_type)
49}
50
51#[cfg(feature = "coils")]
52/// Encodes a Write Single Coil (FC 05) request frame.
53pub(crate) fn encode_write_single_coil(
54    txn_id: u16,
55    unit: UnitIdOrSlaveAddr,
56    address: u16,
57    value: bool,
58    transport_type: TransportType,
59) -> Result<Vec<u8, MAX_ADU_FRAME_LEN>, MbusError> {
60    let coil_value: u16 = if value { 0xFF00 } else { 0x0000 };
61    let pdu = Pdu::build_write_single_u16(FunctionCode::WriteSingleCoil, address, coil_value)?;
62    common::compile_adu_frame(txn_id, unit.get(), pdu, transport_type)
63}
64
65#[cfg(feature = "coils")]
66/// Encodes a Write Multiple Coils (FC 0F) request frame.
67pub(crate) fn encode_write_multiple_coils(
68    txn_id: u16,
69    unit: UnitIdOrSlaveAddr,
70    address: u16,
71    coils: &Coils,
72    transport_type: TransportType,
73) -> Result<Vec<u8, MAX_ADU_FRAME_LEN>, MbusError> {
74    let quantity = coils.quantity();
75    if !(1..=1968).contains(&quantity) {
76        return Err(MbusError::InvalidPduLength);
77    }
78    let byte_count = quantity.div_ceil(8) as usize;
79    let pdu = Pdu::build_write_multiple(
80        FunctionCode::WriteMultipleCoils,
81        address,
82        quantity,
83        &coils.values()[..byte_count],
84    )?;
85    common::compile_adu_frame(txn_id, unit.get(), pdu, transport_type)
86}
87
88// ─── Registers (FC 03 / 04 / 06 / 10 / 16 / 17) ─────────────────────────────
89
90#[cfg(feature = "holding-registers")]
91/// Encodes a Read Holding Registers (FC 03) request frame.
92pub(crate) fn encode_read_holding_registers(
93    txn_id: u16,
94    unit: UnitIdOrSlaveAddr,
95    address: u16,
96    quantity: u16,
97    transport_type: TransportType,
98) -> Result<Vec<u8, MAX_ADU_FRAME_LEN>, MbusError> {
99    if !(1..=125).contains(&quantity) {
100        return Err(MbusError::InvalidQuantity);
101    }
102    let pdu = Pdu::build_read_window(FunctionCode::ReadHoldingRegisters, address, quantity)?;
103    common::compile_adu_frame(txn_id, unit.get(), pdu, transport_type)
104}
105
106#[cfg(feature = "input-registers")]
107/// Encodes a Read Input Registers (FC 04) request frame.
108pub(crate) fn encode_read_input_registers(
109    txn_id: u16,
110    unit: UnitIdOrSlaveAddr,
111    address: u16,
112    quantity: u16,
113    transport_type: TransportType,
114) -> Result<Vec<u8, MAX_ADU_FRAME_LEN>, MbusError> {
115    if !(1..=125).contains(&quantity) {
116        return Err(MbusError::InvalidQuantity);
117    }
118    let pdu = Pdu::build_read_window(FunctionCode::ReadInputRegisters, address, quantity)?;
119    common::compile_adu_frame(txn_id, unit.get(), pdu, transport_type)
120}
121
122#[cfg(feature = "holding-registers")]
123/// Encodes a Write Single Register (FC 06) request frame.
124pub(crate) fn encode_write_single_register(
125    txn_id: u16,
126    unit: UnitIdOrSlaveAddr,
127    address: u16,
128    value: u16,
129    transport_type: TransportType,
130) -> Result<Vec<u8, MAX_ADU_FRAME_LEN>, MbusError> {
131    let pdu = Pdu::build_write_single_u16(FunctionCode::WriteSingleRegister, address, value)?;
132    common::compile_adu_frame(txn_id, unit.get(), pdu, transport_type)
133}
134
135#[cfg(feature = "holding-registers")]
136/// Encodes a Write Multiple Registers (FC 10) request frame.
137pub(crate) fn encode_write_multiple_registers(
138    txn_id: u16,
139    unit: UnitIdOrSlaveAddr,
140    address: u16,
141    values: &[u16],
142    transport_type: TransportType,
143) -> Result<Vec<u8, MAX_ADU_FRAME_LEN>, MbusError> {
144    let quantity = values.len() as u16;
145    if !(1..=123).contains(&quantity) {
146        return Err(MbusError::InvalidQuantity);
147    }
148    let byte_pairs: Vec<u8, { MAX_ADU_FRAME_LEN }> =
149        values.iter().flat_map(|v| v.to_be_bytes()).collect();
150    let pdu = Pdu::build_write_multiple(
151        FunctionCode::WriteMultipleRegisters,
152        address,
153        quantity,
154        &byte_pairs,
155    )?;
156    common::compile_adu_frame(txn_id, unit.get(), pdu, transport_type)
157}
158
159#[cfg(feature = "holding-registers")]
160/// Encodes a Read/Write Multiple Registers (FC 17) request frame.
161pub(crate) fn encode_read_write_multiple_registers(
162    txn_id: u16,
163    unit: UnitIdOrSlaveAddr,
164    read_address: u16,
165    read_quantity: u16,
166    write_address: u16,
167    write_values: &[u16],
168    transport_type: TransportType,
169) -> Result<Vec<u8, MAX_ADU_FRAME_LEN>, MbusError> {
170    let write_quantity = write_values.len() as u16;
171    let byte_pairs: Vec<u8, { MAX_ADU_FRAME_LEN }> =
172        write_values.iter().flat_map(|v| v.to_be_bytes()).collect();
173    let pdu = Pdu::build_read_write_multiple(
174        read_address,
175        read_quantity,
176        write_address,
177        write_quantity,
178        &byte_pairs,
179    )?;
180    common::compile_adu_frame(txn_id, unit.get(), pdu, transport_type)
181}
182
183#[cfg(feature = "holding-registers")]
184/// Encodes a Mask Write Register (FC 16) request frame.
185pub(crate) fn encode_mask_write_register(
186    txn_id: u16,
187    unit: UnitIdOrSlaveAddr,
188    address: u16,
189    and_mask: u16,
190    or_mask: u16,
191    transport_type: TransportType,
192) -> Result<Vec<u8, MAX_ADU_FRAME_LEN>, MbusError> {
193    let pdu = Pdu::build_mask_write_register(address, and_mask, or_mask)?;
194    common::compile_adu_frame(txn_id, unit.get(), pdu, transport_type)
195}
196
197// ─── Discrete inputs (FC 02) ─────────────────────────────────────────────────
198
199#[cfg(feature = "discrete-inputs")]
200/// Encodes a Read Discrete Inputs (FC 02) request frame.
201pub(crate) fn encode_read_discrete_inputs(
202    txn_id: u16,
203    unit: UnitIdOrSlaveAddr,
204    address: u16,
205    quantity: u16,
206    transport_type: TransportType,
207) -> Result<Vec<u8, MAX_ADU_FRAME_LEN>, MbusError> {
208    if !(1..=2000).contains(&quantity) {
209        return Err(MbusError::InvalidQuantity);
210    }
211    let pdu = Pdu::build_read_window(FunctionCode::ReadDiscreteInputs, address, quantity)?;
212    common::compile_adu_frame(txn_id, unit.get(), pdu, transport_type)
213}
214
215// ─── FIFO queue (FC 18) ───────────────────────────────────────────────────────
216
217#[cfg(feature = "fifo")]
218/// Encodes a Read FIFO Queue (FC 18) request frame.
219pub(crate) fn encode_read_fifo_queue(
220    txn_id: u16,
221    unit: UnitIdOrSlaveAddr,
222    address: u16,
223    transport_type: TransportType,
224) -> Result<Vec<u8, MAX_ADU_FRAME_LEN>, MbusError> {
225    let pdu = Pdu::build_u16_payload(FunctionCode::ReadFifoQueue, address)?;
226    common::compile_adu_frame(txn_id, unit.get(), pdu, transport_type)
227}
228
229// ─── File record (FC 14 / 15) ─────────────────────────────────────────────────
230
231#[cfg(feature = "file-record")]
232/// Encodes a Read File Record (FC 14) request frame.
233pub(crate) fn encode_read_file_record(
234    txn_id: u16,
235    unit: UnitIdOrSlaveAddr,
236    sub_request: &SubRequest,
237    transport_type: TransportType,
238) -> Result<Vec<u8, MAX_ADU_FRAME_LEN>, MbusError> {
239    use mbus_core::models::file_record::PduDataBytes;
240    let payload_bytes = sub_request.to_sub_req_pdu_bytes()?;
241    // `to_sub_req_pdu_bytes` already prepends the Modbus byte-count field;
242    // do NOT pass through `build_byte_count_payload` (that would add a second one).
243    let data_len = payload_bytes.len() as u8;
244    let pdu = Pdu::new(FunctionCode::ReadFileRecord, payload_bytes, data_len);
245    common::compile_adu_frame(txn_id, unit.get(), pdu, transport_type)
246}
247
248#[cfg(feature = "file-record")]
249/// Encodes a Write File Record (FC 15) request frame.
250pub(crate) fn encode_write_file_record(
251    txn_id: u16,
252    unit: UnitIdOrSlaveAddr,
253    sub_request: &SubRequest,
254    transport_type: TransportType,
255) -> Result<Vec<u8, MAX_ADU_FRAME_LEN>, MbusError> {
256    use mbus_core::models::file_record::PduDataBytes;
257    let payload_bytes = sub_request.to_sub_req_pdu_bytes()?;
258    // `to_sub_req_pdu_bytes` already prepends the Modbus byte-count field;
259    // do NOT pass through `build_byte_count_payload` (that would add a second one).
260    let data_len = payload_bytes.len() as u8;
261    let pdu = Pdu::new(FunctionCode::WriteFileRecord, payload_bytes, data_len);
262    common::compile_adu_frame(txn_id, unit.get(), pdu, transport_type)
263}
264
265// ─── Diagnostics (FC 07 / 08 / 0B / 0C / 11 / 2B) ───────────────────────────
266
267#[cfg(feature = "diagnostics")]
268/// Encodes a Read Device Identification (FC 43 / MEI 0E) request frame.
269pub(crate) fn encode_read_device_identification(
270    txn_id: u16,
271    unit: UnitIdOrSlaveAddr,
272    read_device_id_code: ReadDeviceIdCode,
273    object_id: ObjectId,
274    transport_type: TransportType,
275) -> Result<Vec<u8, MAX_ADU_FRAME_LEN>, MbusError> {
276    let object_id_byte = u8::from(object_id);
277    let payload: [u8; 2] = [read_device_id_code as u8, object_id_byte];
278    let pdu = Pdu::build_mei_type(
279        FunctionCode::EncapsulatedInterfaceTransport,
280        EncapsulatedInterfaceType::ReadDeviceIdentification as u8,
281        &payload,
282    )?;
283    common::compile_adu_frame(txn_id, unit.get(), pdu, transport_type)
284}
285
286#[cfg(feature = "diagnostics")]
287/// Encodes a generic Encapsulated Interface Transport (FC 43) request frame.
288pub(crate) fn encode_encapsulated_interface_transport(
289    txn_id: u16,
290    unit: UnitIdOrSlaveAddr,
291    mei_type: EncapsulatedInterfaceType,
292    data: &[u8],
293    transport_type: TransportType,
294) -> Result<Vec<u8, MAX_ADU_FRAME_LEN>, MbusError> {
295    let pdu = Pdu::build_mei_type(
296        FunctionCode::EncapsulatedInterfaceTransport,
297        mei_type as u8,
298        data,
299    )?;
300    common::compile_adu_frame(txn_id, unit.get(), pdu, transport_type)
301}
302
303#[cfg(feature = "diagnostics")]
304/// Encodes a Read Exception Status (FC 07) request frame (Serial only).
305pub(crate) fn encode_read_exception_status(
306    unit: UnitIdOrSlaveAddr,
307    transport_type: TransportType,
308) -> Result<Vec<u8, MAX_ADU_FRAME_LEN>, MbusError> {
309    let pdu = Pdu::build_empty(FunctionCode::ReadExceptionStatus);
310    // txn_id is unused on serial; pass 0
311    common::compile_adu_frame(0, unit.get(), pdu, transport_type)
312}
313
314#[cfg(feature = "diagnostics")]
315/// Encodes a Diagnostics (FC 08) request frame (Serial only).
316pub(crate) fn encode_diagnostics(
317    unit: UnitIdOrSlaveAddr,
318    sub_function: DiagnosticSubFunction,
319    data: &[u16],
320    transport_type: TransportType,
321) -> Result<Vec<u8, MAX_ADU_FRAME_LEN>, MbusError> {
322    let pdu = Pdu::build_sub_function(FunctionCode::Diagnostics, sub_function as u16, data)?;
323    common::compile_adu_frame(0, unit.get(), pdu, transport_type)
324}
325
326#[cfg(feature = "diagnostics")]
327/// Encodes a Get Comm Event Counter (FC 0B) request frame (Serial only).
328pub(crate) fn encode_get_comm_event_counter(
329    unit: UnitIdOrSlaveAddr,
330    transport_type: TransportType,
331) -> Result<Vec<u8, MAX_ADU_FRAME_LEN>, MbusError> {
332    let pdu = Pdu::build_empty(FunctionCode::GetCommEventCounter);
333    common::compile_adu_frame(0, unit.get(), pdu, transport_type)
334}
335
336#[cfg(feature = "diagnostics")]
337/// Encodes a Get Comm Event Log (FC 0C) request frame (Serial only).
338pub(crate) fn encode_get_comm_event_log(
339    unit: UnitIdOrSlaveAddr,
340    transport_type: TransportType,
341) -> Result<Vec<u8, MAX_ADU_FRAME_LEN>, MbusError> {
342    let pdu = Pdu::build_empty(FunctionCode::GetCommEventLog);
343    common::compile_adu_frame(0, unit.get(), pdu, transport_type)
344}
345
346#[cfg(feature = "diagnostics")]
347/// Encodes a Report Server ID (FC 11) request frame (Serial only).
348pub(crate) fn encode_report_server_id(
349    unit: UnitIdOrSlaveAddr,
350    transport_type: TransportType,
351) -> Result<Vec<u8, MAX_ADU_FRAME_LEN>, MbusError> {
352    let pdu = Pdu::build_empty(FunctionCode::ReportServerId);
353    common::compile_adu_frame(0, unit.get(), pdu, transport_type)
354}
355
356// ─── Top-level dispatcher ─────────────────────────────────────────────────────
357
358/// Encodes a [`ClientRequest`] into a complete ADU frame using the given `txn_id`.
359///
360/// For serial-only function codes (FC 07 / 08 / 0B / 0C / 11) the `txn_id` is
361/// passed through but the serial ADU format does not include it on the wire.
362///
363/// [`ClientRequest`]: crate::client::command::ClientRequest
364pub fn encode_request(
365    txn_id: u16,
366    req: &ClientRequest,
367    transport_type: TransportType,
368) -> Result<Vec<u8, MAX_ADU_FRAME_LEN>, MbusError> {
369    match req {
370        #[cfg(feature = "coils")]
371        ClientRequest::ReadMultipleCoils {
372            unit,
373            address,
374            quantity,
375        } => encode_read_coils(txn_id, *unit, *address, *quantity, transport_type),
376
377        #[cfg(feature = "coils")]
378        ClientRequest::WriteSingleCoil {
379            unit,
380            address,
381            value,
382        } => encode_write_single_coil(txn_id, *unit, *address, *value, transport_type),
383
384        #[cfg(feature = "coils")]
385        ClientRequest::WriteMultipleCoils {
386            unit,
387            address,
388            coils,
389        } => encode_write_multiple_coils(txn_id, *unit, *address, coils, transport_type),
390
391        #[cfg(feature = "holding-registers")]
392        ClientRequest::ReadHoldingRegisters {
393            unit,
394            address,
395            quantity,
396        } => encode_read_holding_registers(txn_id, *unit, *address, *quantity, transport_type),
397
398        #[cfg(feature = "input-registers")]
399        ClientRequest::ReadInputRegisters {
400            unit,
401            address,
402            quantity,
403        } => encode_read_input_registers(txn_id, *unit, *address, *quantity, transport_type),
404
405        #[cfg(feature = "holding-registers")]
406        ClientRequest::WriteSingleRegister {
407            unit,
408            address,
409            value,
410        } => encode_write_single_register(txn_id, *unit, *address, *value, transport_type),
411
412        #[cfg(feature = "holding-registers")]
413        ClientRequest::WriteMultipleRegisters {
414            unit,
415            address,
416            values,
417        } => encode_write_multiple_registers(txn_id, *unit, *address, values, transport_type),
418
419        #[cfg(feature = "holding-registers")]
420        ClientRequest::ReadWriteMultipleRegisters {
421            unit,
422            read_address,
423            read_quantity,
424            write_address,
425            write_values,
426        } => encode_read_write_multiple_registers(
427            txn_id,
428            *unit,
429            *read_address,
430            *read_quantity,
431            *write_address,
432            write_values,
433            transport_type,
434        ),
435
436        #[cfg(feature = "holding-registers")]
437        ClientRequest::MaskWriteRegister {
438            unit,
439            address,
440            and_mask,
441            or_mask,
442        } => {
443            encode_mask_write_register(txn_id, *unit, *address, *and_mask, *or_mask, transport_type)
444        }
445
446        #[cfg(feature = "discrete-inputs")]
447        ClientRequest::ReadDiscreteInputs {
448            unit,
449            address,
450            quantity,
451        } => encode_read_discrete_inputs(txn_id, *unit, *address, *quantity, transport_type),
452
453        #[cfg(feature = "fifo")]
454        ClientRequest::ReadFifoQueue { unit, address } => {
455            encode_read_fifo_queue(txn_id, *unit, *address, transport_type)
456        }
457
458        #[cfg(feature = "file-record")]
459        ClientRequest::ReadFileRecord { unit, sub_request } => {
460            encode_read_file_record(txn_id, *unit, sub_request, transport_type)
461        }
462
463        #[cfg(feature = "file-record")]
464        ClientRequest::WriteFileRecord { unit, sub_request } => {
465            encode_write_file_record(txn_id, *unit, sub_request, transport_type)
466        }
467
468        #[cfg(feature = "diagnostics")]
469        ClientRequest::ReadDeviceIdentification {
470            unit,
471            read_device_id_code,
472            object_id,
473        } => encode_read_device_identification(
474            txn_id,
475            *unit,
476            *read_device_id_code,
477            *object_id,
478            transport_type,
479        ),
480
481        #[cfg(feature = "diagnostics")]
482        ClientRequest::EncapsulatedInterfaceTransport {
483            unit,
484            mei_type,
485            data,
486        } => {
487            encode_encapsulated_interface_transport(txn_id, *unit, *mei_type, data, transport_type)
488        }
489
490        #[cfg(feature = "diagnostics")]
491        ClientRequest::ReadExceptionStatus { unit } => {
492            encode_read_exception_status(*unit, transport_type)
493        }
494
495        #[cfg(feature = "diagnostics")]
496        ClientRequest::Diagnostics {
497            unit,
498            sub_function,
499            data,
500        } => encode_diagnostics(*unit, *sub_function, data, transport_type),
501
502        #[cfg(feature = "diagnostics")]
503        ClientRequest::GetCommEventCounter { unit } => {
504            encode_get_comm_event_counter(*unit, transport_type)
505        }
506
507        #[cfg(feature = "diagnostics")]
508        ClientRequest::GetCommEventLog { unit } => encode_get_comm_event_log(*unit, transport_type),
509
510        #[cfg(feature = "diagnostics")]
511        ClientRequest::ReportServerId { unit } => encode_report_server_id(*unit, transport_type),
512
513        #[allow(unreachable_patterns)]
514        _ => unreachable!(),
515    }
516}