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/// Parsed read sub-request from FC14 PDU data.
33///
34/// Represents a single object within a Read File Record (FC 0x14) request.
35#[derive(Clone, Copy, Debug, PartialEq, Eq)]
36pub struct FileRecordReadSubRequest {
37 /// The file number to be read (0x0001 to 0xFFFF).
38 pub file_number: u16,
39 /// The starting record number within the file (0x0000 to 0x270F).
40 pub record_number: u16,
41 /// The length of the record in number of 16-bit registers.
42 pub record_length: u16,
43}
44
45/// Parsed write sub-request from FC15 PDU data.
46///
47/// Represents a single object within a Write File Record (FC 0x15) request.
48#[derive(Clone, Copy, Debug, PartialEq, Eq)]
49pub struct FileRecordWriteSubRequest<'a> {
50 /// The file number to be written (0x0001 to 0xFFFF).
51 pub file_number: u16,
52 /// The starting record number within the file (0x0000 to 0x270F).
53 pub record_number: u16,
54 /// The length of the record in number of 16-bit registers.
55 pub record_length: u16,
56 /// The raw bytes of the record data to be written.
57 pub record_data_bytes: &'a [u8],
58}
59
60/// A trait for converting Modbus PDU data structures into a byte vector.
61///
62/// This is specifically used for file record sub-requests to serialize their parameters
63/// into the format expected within a Modbus PDU.
64pub trait PduDataBytes {
65 /// Converts the sub-request parameters into a byte vector for the PDU.
66 fn to_sub_req_pdu_bytes(&self) -> Result<Vec<u8, MAX_PDU_DATA_LEN>, MbusError>;
67}
68
69/// Parameters for a single file record sub-request.
70#[derive(Debug, Clone, PartialEq)]
71pub struct SubRequestParams {
72 /// The file number to be read/written (0x0001 to 0xFFFF).
73 /// In Modbus, files are logical groupings of records.
74 pub file_number: u16,
75 /// The starting record number within the file (0x0000 to 0x270F).
76 /// Each record is typically 2 bytes (one 16-bit register).
77 pub record_number: u16,
78 /// The length of the record in number of 16-bit registers.
79 /// For Read (FC 0x14), this is the amount to retrieve.
80 /// For Write (FC 0x15), this must match the length of `record_data`.
81 pub record_length: u16,
82 /// The actual register values to be written to the file.
83 /// This field is `Some` for Write File Record (FC 0x15) and `None` for Read File Record (FC 0x14).
84 /// The data is stored in a `heapless::Vec` to ensure `no_std` compatibility.
85 pub record_data: Option<Vec<u16, MAX_PDU_DATA_LEN>>,
86}
87
88/// Represents a collection of sub-requests for Modbus File Record operations.
89///
90/// A single Modbus PDU for FC 0x14 or 0x15 can contain multiple sub-requests,
91/// allowing the client to read from or write to different files and records in one transaction.
92///
93/// This struct manages the aggregation of these requests and performs validation to ensure
94/// the resulting PDU does not exceed the Modbus protocol limit of 253 bytes.
95#[derive(Debug, Clone, Default)]
96pub struct SubRequest {
97 /// A fixed-capacity vector of individual sub-request parameters.
98 /// The capacity is limited to 35 as per the Modbus specification.
99 params: Vec<SubRequestParams, MAX_SUB_REQUESTS_PER_PDU>,
100 /// The cumulative count of registers (16-bit words) requested for reading.
101 /// Used to calculate and validate the expected response size.
102 total_read_bytes_length: u16,
103}
104
105impl SubRequest {
106 /// Creates a new empty `SubRequest`.
107 pub fn new() -> Self {
108 SubRequest {
109 params: Vec::new(),
110 total_read_bytes_length: 0,
111 }
112 }
113
114 /// Adds a sub-request for reading a file record.
115 ///
116 /// # Arguments
117 /// * `file_number` - The file number.
118 /// * `record_number` - The starting record number.
119 /// * `record_length` - The number of registers to read.
120 pub fn add_read_sub_request(
121 &mut self,
122 file_number: u16,
123 record_number: u16,
124 record_length: u16,
125 ) -> Result<(), MbusError> {
126 if self.params.len() >= MAX_SUB_REQUESTS_PER_PDU {
127 return Err(MbusError::TooManyFileReadSubRequests);
128 }
129 // Calculate expected response size to prevent overflow
130 // Response PDU: FC(1) + ByteCount(1) + N * (Len(1) + Ref(1) + Data(Regs*2))
131 // Total bytes = 2 + 2*N + 2*TotalRegs <= 253
132 // N + TotalRegs <= 125
133 // 125 is the approximate limit for (SubRequests + TotalRegisters) to fit in 253 bytes.
134 if (self.params.len() as u16 + 1) + (self.total_read_bytes_length + record_length) > 125 {
135 return Err(MbusError::FileReadPduOverflow);
136 }
137 self.params
138 .push(SubRequestParams {
139 file_number,
140 record_number,
141 record_length,
142 record_data: None,
143 })
144 .map_err(|_| MbusError::TooManyFileReadSubRequests)?;
145
146 self.total_read_bytes_length += record_length;
147 Ok(())
148 }
149
150 /// Adds a sub-request for writing a file record.
151 ///
152 /// # Arguments
153 /// * `file_number` - The file number.
154 /// * `record_number` - The starting record number.
155 /// * `record_length` - The number of registers to write.
156 /// * `record_data` - The data to write.
157 pub fn add_write_sub_request(
158 &mut self,
159 file_number: u16,
160 record_number: u16,
161 record_length: u16,
162 record_data: Vec<u16, MAX_PDU_DATA_LEN>,
163 ) -> Result<(), MbusError> {
164 if self.params.len() >= MAX_SUB_REQUESTS_PER_PDU {
165 return Err(MbusError::TooManyFileReadSubRequests);
166 }
167 if record_data.len() != record_length as usize {
168 return Err(MbusError::BufferLenMissmatch);
169 }
170
171 // Calculate projected PDU size: 1 (Byte Count Field) + Current Payload + New SubReq (7 + Data)
172 let current_payload_size = self.byte_count();
173 // 7 bytes header (Ref + File + RecNum + RecLen) + Data bytes (2 * registers)
174 let new_sub_req_size = SUB_REQ_PARAM_BYTE_LEN + (record_data.len() * 2);
175
176 // Check if adding this request exceeds the maximum PDU data length (252 bytes).
177 // 1 byte for the main Byte Count field + current payload + new request size.
178 if 1 + current_payload_size + new_sub_req_size > MAX_PDU_DATA_LEN {
179 return Err(MbusError::FileReadPduOverflow);
180 }
181 self.params
182 .push(SubRequestParams {
183 file_number,
184 record_number,
185 record_length,
186 record_data: Some(record_data),
187 })
188 .map_err(|_| MbusError::TooManyFileReadSubRequests)?;
189
190 self.total_read_bytes_length += record_length;
191 Ok(())
192 }
193
194 /// Calculates the total byte count for the sub-requests payload.
195 pub fn byte_count(&self) -> usize {
196 self.params
197 .iter()
198 .map(|p| {
199 // 7 bytes for sub-request header + data bytes (if any)
200 7 + p.record_data.as_ref().map(|d| d.len() * 2).unwrap_or(0)
201 })
202 .sum()
203 }
204
205 /// Clears all sub-requests.
206 pub fn clear_all(&mut self) {
207 self.total_read_bytes_length = 0;
208 self.params.clear();
209 }
210}
211
212impl PduDataBytes for SubRequest {
213 fn to_sub_req_pdu_bytes(&self) -> Result<Vec<u8, MAX_PDU_DATA_LEN>, MbusError> {
214 let mut bytes = Vec::new();
215 // Byte Count: 1 byte (0x07 to 0xF5 bytes)
216 let byte_count = self.byte_count();
217 bytes
218 .push(byte_count as u8)
219 .map_err(|_| MbusError::BufferTooSmall)?;
220
221 for param in &self.params {
222 // Reference Type: 1 byte (0x06)
223 bytes
224 .push(FILE_RECORD_REF_TYPE)
225 .map_err(|_| MbusError::BufferTooSmall)?;
226 bytes
227 .extend_from_slice(¶m.file_number.to_be_bytes())
228 .map_err(|_| MbusError::BufferLenMissmatch)?;
229 bytes
230 .extend_from_slice(¶m.record_number.to_be_bytes())
231 .map_err(|_| MbusError::BufferLenMissmatch)?;
232 bytes
233 .extend_from_slice(¶m.record_length.to_be_bytes())
234 .map_err(|_| MbusError::BufferLenMissmatch)?;
235 if let Some(ref data) = param.record_data {
236 for val in data {
237 bytes
238 .extend_from_slice(&val.to_be_bytes())
239 .map_err(|_| MbusError::BufferLenMissmatch)?;
240 }
241 }
242 }
243 Ok(bytes)
244 }
245}