mbus_core/models/file_record/model.rs
1//! # Modbus File Record Models
2//!
3//! This module provides the data structures for handling **Read File Record** (Function Code 0x14)
4//! and **Write File Record** (Function Code 0x15).
5//!
6//! File records allow access to a large, structured memory area organized into files and records.
7//! Unlike coils or registers, file record operations are composed of one or more "sub-requests"
8//! within a single Modbus PDU.
9//!
10//! ## Key Components
11//! - [`SubRequest`]: A container that aggregates multiple read or write operations into a single PDU.
12//! - [`SubRequestParams`]: The specific parameters (File No, Record No, Length, Data) for an individual operation.
13//! - [`MAX_SUB_REQUESTS_PER_PDU`]: The protocol limit of 35 sub-requests per frame.
14//!
15//! ## Constraints
16//! - The total size of all sub-requests (including headers and data) must not exceed the
17//! maximum Modbus PDU data length of 252 bytes.
18//! - For Read requests, the response size is also validated during sub-request addition to
19//! ensure the server can fit the requested data into a single response PDU.
20
21use crate::{data_unit::common::MAX_PDU_DATA_LEN, errors::MbusError};
22use heapless::Vec;
23
24/// Maximum number of sub-requests allowed in a single PDU (35).
25pub const MAX_SUB_REQUESTS_PER_PDU: usize = 35;
26/// Byte count is 1 byte for each sub-request
27///(reference type + file number + record number + record length) + 1 byte for the byte count itself
28pub const SUB_REQ_PARAM_BYTE_LEN: usize = 6 + 1;
29/// The reference type for file record requests (0x06).
30pub const FILE_RECORD_REF_TYPE: u8 = 0x06;
31
32/// A trait for converting Modbus PDU data structures into a byte vector.
33///
34/// This is specifically used for file record sub-requests to serialize their parameters
35/// into the format expected within a Modbus PDU.
36pub trait PduDataBytes {
37 /// Converts the sub-request parameters into a byte vector for the PDU.
38 fn to_sub_req_pdu_bytes(&self) -> Result<Vec<u8, MAX_PDU_DATA_LEN>, MbusError>;
39}
40
41/// Parameters for a single file record sub-request.
42#[derive(Debug, Clone, PartialEq)]
43pub struct SubRequestParams {
44 /// The file number to be read/written (0x0001 to 0xFFFF).
45 /// In Modbus, files are logical groupings of records.
46 pub file_number: u16,
47 /// The starting record number within the file (0x0000 to 0x270F).
48 /// Each record is typically 2 bytes (one 16-bit register).
49 pub record_number: u16,
50 /// The length of the record in number of 16-bit registers.
51 /// For Read (FC 0x14), this is the amount to retrieve.
52 /// For Write (FC 0x15), this must match the length of `record_data`.
53 pub record_length: u16,
54 /// The actual register values to be written to the file.
55 /// This field is `Some` for Write File Record (FC 0x15) and `None` for Read File Record (FC 0x14).
56 /// The data is stored in a `heapless::Vec` to ensure `no_std` compatibility.
57 pub record_data: Option<Vec<u16, MAX_PDU_DATA_LEN>>,
58}
59
60/// Represents a collection of sub-requests for Modbus File Record operations.
61///
62/// A single Modbus PDU for FC 0x14 or 0x15 can contain multiple sub-requests,
63/// allowing the client to read from or write to different files and records in one transaction.
64///
65/// This struct manages the aggregation of these requests and performs validation to ensure
66/// the resulting PDU does not exceed the Modbus protocol limit of 253 bytes.
67#[derive(Debug, Clone, Default)]
68pub struct SubRequest {
69 /// A fixed-capacity vector of individual sub-request parameters.
70 /// The capacity is limited to 35 as per the Modbus specification.
71 params: Vec<SubRequestParams, MAX_SUB_REQUESTS_PER_PDU>,
72 /// The cumulative count of registers (16-bit words) requested for reading.
73 /// Used to calculate and validate the expected response size.
74 total_read_bytes_length: u16,
75}
76
77impl SubRequest {
78 /// Creates a new empty `SubRequest`.
79 pub fn new() -> Self {
80 SubRequest {
81 params: Vec::new(),
82 total_read_bytes_length: 0,
83 }
84 }
85
86 /// Adds a sub-request for reading a file record.
87 ///
88 /// # Arguments
89 /// * `file_number` - The file number.
90 /// * `record_number` - The starting record number.
91 /// * `record_length` - The number of registers to read.
92 pub fn add_read_sub_request(
93 &mut self,
94 file_number: u16,
95 record_number: u16,
96 record_length: u16,
97 ) -> Result<(), MbusError> {
98 if self.params.len() >= MAX_SUB_REQUESTS_PER_PDU {
99 return Err(MbusError::TooManyFileReadSubRequests);
100 }
101 // Calculate expected response size to prevent overflow
102 // Response PDU: FC(1) + ByteCount(1) + N * (Len(1) + Ref(1) + Data(Regs*2))
103 // Total bytes = 2 + 2*N + 2*TotalRegs <= 253
104 // N + TotalRegs <= 125
105 // 125 is the approximate limit for (SubRequests + TotalRegisters) to fit in 253 bytes.
106 if (self.params.len() as u16 + 1) + (self.total_read_bytes_length + record_length) > 125 {
107 return Err(MbusError::FileReadPduOverflow);
108 }
109 self.params
110 .push(SubRequestParams {
111 file_number,
112 record_number,
113 record_length,
114 record_data: None,
115 })
116 .map_err(|_| MbusError::TooManyFileReadSubRequests)?;
117
118 self.total_read_bytes_length += record_length;
119 Ok(())
120 }
121
122 /// Adds a sub-request for writing a file record.
123 ///
124 /// # Arguments
125 /// * `file_number` - The file number.
126 /// * `record_number` - The starting record number.
127 /// * `record_length` - The number of registers to write.
128 /// * `record_data` - The data to write.
129 pub fn add_write_sub_request(
130 &mut self,
131 file_number: u16,
132 record_number: u16,
133 record_length: u16,
134 record_data: Vec<u16, MAX_PDU_DATA_LEN>,
135 ) -> Result<(), MbusError> {
136 if self.params.len() >= MAX_SUB_REQUESTS_PER_PDU {
137 return Err(MbusError::TooManyFileReadSubRequests);
138 }
139 if record_data.len() != record_length as usize {
140 return Err(MbusError::BufferLenMissmatch);
141 }
142
143 // Calculate projected PDU size: 1 (Byte Count Field) + Current Payload + New SubReq (7 + Data)
144 let current_payload_size = self.byte_count();
145 // 7 bytes header (Ref + File + RecNum + RecLen) + Data bytes (2 * registers)
146 let new_sub_req_size = SUB_REQ_PARAM_BYTE_LEN + (record_data.len() * 2);
147
148 // Check if adding this request exceeds the maximum PDU data length (252 bytes).
149 // 1 byte for the main Byte Count field + current payload + new request size.
150 if 1 + current_payload_size + new_sub_req_size > MAX_PDU_DATA_LEN {
151 return Err(MbusError::FileReadPduOverflow);
152 }
153 self.params
154 .push(SubRequestParams {
155 file_number,
156 record_number,
157 record_length,
158 record_data: Some(record_data),
159 })
160 .map_err(|_| MbusError::TooManyFileReadSubRequests)?;
161
162 self.total_read_bytes_length += record_length;
163 Ok(())
164 }
165
166 /// Calculates the total byte count for the sub-requests payload.
167 pub fn byte_count(&self) -> usize {
168 self.params
169 .iter()
170 .map(|p| {
171 // 7 bytes for sub-request header + data bytes (if any)
172 7 + p.record_data.as_ref().map(|d| d.len() * 2).unwrap_or(0)
173 })
174 .sum()
175 }
176
177 /// Clears all sub-requests.
178 pub fn clear_all(&mut self) {
179 self.total_read_bytes_length = 0;
180 self.params.clear();
181 }
182}
183
184impl PduDataBytes for SubRequest {
185 fn to_sub_req_pdu_bytes(&self) -> Result<Vec<u8, MAX_PDU_DATA_LEN>, MbusError> {
186 let mut bytes = Vec::new();
187 // Byte Count: 1 byte (0x07 to 0xF5 bytes)
188 let byte_count = self.byte_count();
189 bytes
190 .push(byte_count as u8)
191 .map_err(|_| MbusError::BufferTooSmall)?;
192
193 for param in &self.params {
194 // Reference Type: 1 byte (0x06)
195 bytes
196 .push(FILE_RECORD_REF_TYPE)
197 .map_err(|_| MbusError::BufferTooSmall)?;
198 bytes
199 .extend_from_slice(¶m.file_number.to_be_bytes())
200 .map_err(|_| MbusError::BufferLenMissmatch)?;
201 bytes
202 .extend_from_slice(¶m.record_number.to_be_bytes())
203 .map_err(|_| MbusError::BufferLenMissmatch)?;
204 bytes
205 .extend_from_slice(¶m.record_length.to_be_bytes())
206 .map_err(|_| MbusError::BufferLenMissmatch)?;
207 if let Some(ref data) = param.record_data {
208 for val in data {
209 bytes
210 .extend_from_slice(&val.to_be_bytes())
211 .map_err(|_| MbusError::BufferLenMissmatch)?;
212 }
213 }
214 }
215 Ok(bytes)
216 }
217}