Skip to main content

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(&param.file_number.to_be_bytes())
200                .map_err(|_| MbusError::BufferLenMissmatch)?;
201            bytes
202                .extend_from_slice(&param.record_number.to_be_bytes())
203                .map_err(|_| MbusError::BufferLenMissmatch)?;
204            bytes
205                .extend_from_slice(&param.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}