mbus_core/models/discrete_input/model.rs
1//! # Modbus Discrete Input Models
2//!
3//! This module defines the data structures for handling **Discrete Inputs** (Function Code 0x02).
4//!
5//! In Modbus, Discrete Inputs are single-bit, read-only data objects. They are typically used
6//! to represent digital inputs from physical devices, such as limit switches, sensor states,
7//! or status indicators.
8//!
9//! ## Key Components
10//! - [`DiscreteInputs`]: A container for a block of bit-packed input states.
11//! - [`MAX_DISCRETE_INPUTS_PER_PDU`]: The protocol limit for a single read operation.
12//!
13//! ## Data Packing¯
14//! Discrete inputs are packed into bytes in the Modbus PDU. The first input requested
15//! is stored in the Least Significant Bit (LSB) of the first data byte.
16//!
17//! ### Example
18//! If 3 inputs are read (Address 10, 11, 12) and the first and third are ON:
19//! - Byte 0: `0000 0101` (Binary) -> `0x05` (Hex)
20//! - Bit 0 (Address 10): 1 (ON)
21//! - Bit 1 (Address 11): 0 (OFF)
22//! - Bit 2 (Address 12): 1 (ON)
23
24use crate::errors::MbusError;
25
26/// The maximum number of discrete inputs that can be requested in a single Read Discrete Inputs (FC 02) PDU.
27///
28/// According to the Modbus Application Protocol Specification V1.1b3, the quantity of inputs
29/// must be between 1 and 2000 (0x07D0).
30pub const MAX_DISCRETE_INPUTS_PER_PDU: usize = 2000;
31
32/// The maximum number of bytes required to store the bit-packed states of 2000 discrete inputs.
33///
34/// Calculated as `ceil(2000 / 8) = 250` bytes.
35pub const MAX_DISCRETE_INPUT_BYTES: usize = MAX_DISCRETE_INPUTS_PER_PDU.div_ceil(8);
36
37/// A collection of discrete input states retrieved from a Modbus server.
38///
39/// This structure maintains the context of the read operation (starting address and quantity)
40/// and stores the actual bit-packed values in a memory-efficient `heapless::Vec`, making it
41/// suitable for `no_std` and embedded environments.
42///
43/// Use the [`value()`](Self::value) method to extract individual boolean states without
44/// manually performing bitwise operations.
45///
46/// # Internal Representation
47/// The `values` array stores these discrete input states. Each byte in `values` holds 8 input states,
48/// where the least significant bit (LSB) of the first byte (`values[0]`) corresponds to the
49/// `from_address`, the next bit to `from_address + 1`, and so on. This bit-packing is efficient
50/// for memory usage and network transmission.
51///
52/// The `MAX_DISCRETE_INPUT_BYTES` constant ensures that the `values` array has enough space to
53/// accommodate the maximum possible number of discrete inputs allowed in a single Modbus PDU
54/// (`MAX_DISCRETE_INPUTS_PER_PDU`).
55///
56/// # Examples
57///
58/// ```rust
59/// use mbus_core::models::discrete_input::{DiscreteInputs, MAX_DISCRETE_INPUT_BYTES};
60/// use mbus_core::errors::MbusError;
61///
62/// // Initialize a block of 8 discrete inputs starting at Modbus address 100.
63/// // Initially all inputs are OFF (0).
64/// let mut inputs = DiscreteInputs::new(100, 8).unwrap();
65///
66/// // Verify initial state: all inputs are false
67/// assert_eq!(inputs.value(100).unwrap(), false);
68/// assert_eq!(inputs.value(107).unwrap(), false);
69///
70/// // Simulate receiving data where inputs at offsets 0 and 2 are ON (0b0000_0101)
71/// let received_data = [0x05, 0x00, 0x00, 0x00]; // Only the first byte is relevant for 8 inputs
72/// inputs = inputs.with_values(&received_data, 8).expect("Valid quantity and data");
73///
74/// // Read individual input values
75/// assert_eq!(inputs.value(100).unwrap(), true); // Address 100 (offset 0) -> LSB of 0x05 is 1
76/// assert_eq!(inputs.value(101).unwrap(), false); // Address 101 (offset 1) -> next bit is 0
77/// assert_eq!(inputs.value(102).unwrap(), true); // Address 102 (offset 2) -> next bit is 1
78/// assert_eq!(inputs.value(107).unwrap(), false); // Address 107 (offset 7) -> MSB of 0x05 is 0
79///
80/// // Accessing values out of bounds will return an error
81/// assert_eq!(inputs.value(99), Err(MbusError::InvalidAddress));
82/// assert_eq!(inputs.value(108), Err(MbusError::InvalidAddress));
83///
84/// // Get the raw bit-packed bytes (only the first byte is active for 8 inputs)
85/// assert_eq!(inputs.values(), &[0x05]);
86/// ```
87#[derive(Debug, PartialEq, Eq, Clone)]
88pub struct DiscreteInputs {
89 /// The starting address of the first input in this block.
90 from_address: u16,
91 /// The number of inputs in this block.
92 quantity: u16,
93 /// The input states packed into bytes, where each bit represents an input (1 for ON, 0 for OFF).
94 /// The least significant bit of `values[0]` corresponds to `from_address`.
95 values: [u8; MAX_DISCRETE_INPUT_BYTES],
96}
97
98impl DiscreteInputs {
99 /// Creates a new `DiscreteInputs` instance representing a block of read-only discrete inputs.
100 ///
101 /// The internal `values` array is initialized to all zeros, meaning all discrete inputs
102 /// are initially considered OFF (`false`).
103 ///
104 /// # Arguments
105 /// * `from_address` - The starting Modbus address for this block of inputs.
106 /// * `quantity` - The total number of discrete inputs contained in this block.
107 ///
108 /// # What happens:
109 /// 1. The `quantity` is validated to ensure it does not exceed `MAX_DISCRETE_INPUTS_PER_PDU`.
110 /// 2. A new `DiscreteInputs` instance is created with the specified `from_address` and `quantity`.
111 /// 3. The internal `values` array, which stores the bit-packed states, is initialized to all `0u8`s.
112 ///
113 /// # Errors
114 /// Returns `MbusError::InvalidQuantity` if the requested `quantity` exceeds
115 /// `MAX_DISCRETE_INPUTS_PER_PDU`.
116 /// # Returns
117 /// A new initialized `DiscreteInputs` instance.
118 pub fn new(from_address: u16, quantity: u16) -> Result<Self, MbusError> {
119 if quantity > MAX_DISCRETE_INPUTS_PER_PDU as u16 {
120 return Err(MbusError::InvalidQuantity);
121 }
122 Ok(Self {
123 from_address,
124 quantity,
125 values: [0; MAX_DISCRETE_INPUT_BYTES],
126 })
127 }
128
129 /// Sets the bit-packed values for the discrete inputs and validates the length.
130 ///
131 /// This method is typically used to populate a `DiscreteInputs` instance with actual
132 /// data received from a Modbus server. It copies the relevant portion of the provided
133 /// `values` slice into the internal fixed-size buffer.
134 ///
135 /// # Arguments
136 /// * `values` - A slice of bytes containing the bit-packed states. This slice should
137 /// be at least as long as the number of bytes required to store `bits_length` inputs.
138 /// * `bits_length` - The number of bits (inputs) actually contained in the provided values.
139 /// This parameter specifies the *actual* number of bits (discrete inputs) present in
140 /// the `values` slice, which should typically match the `quantity` of the
141 /// `DiscreteInputs` instance.
142 ///
143 /// # What happens:
144 /// 1. The `bits_length` is checked against the `quantity` of the `DiscreteInputs` instance.
145 /// 2. The necessary number of bytes (`byte_length`) is calculated from `bits_length`.
146 /// 3. The relevant portion of the input `values` slice is copied into the internal `self.values` array.
147 ///
148 /// # Errors
149 /// Returns `MbusError::InvalidQuantity` if `bits_length` does not match `self.quantity`.
150 pub fn with_values(mut self, values: &[u8], bits_length: u16) -> Result<Self, MbusError> {
151 if bits_length > self.quantity {
152 return Err(MbusError::InvalidQuantity);
153 }
154
155 // Ensure we aren't receiving fewer bits than the quantity we expect to manage
156 if bits_length < self.quantity {
157 return Err(MbusError::InvalidQuantity);
158 }
159
160 // Calculate how many bytes are needed to represent the bits_length (round up)
161 let byte_length = bits_length.div_ceil(8);
162 // Copy only the relevant portion of the input array into the internal buffer
163 self.values[..byte_length as usize].copy_from_slice(&values[..byte_length as usize]);
164 Ok(self)
165 }
166
167 /// Returns the starting Modbus address of the first discrete input in this block.
168 pub fn from_address(&self) -> u16 {
169 self.from_address
170 }
171
172 /// Returns the total number of discrete inputs managed by this instance.
173 pub fn quantity(&self) -> u16 {
174 self.quantity
175 }
176
177 /// Returns a reference to the active bytes containing the bit-packed input states.
178 ///
179 /// This method returns a slice `&[u8]` that contains only the bytes relevant to the
180 /// `quantity` of discrete inputs managed by this instance. It does not return the
181 /// entire `MAX_DISCRETE_INPUT_BYTES` array if `quantity` is smaller.
182 /// The length of the returned slice is calculated as `ceil(self.quantity / 8)`.
183 ///
184 pub fn values(&self) -> &[u8] {
185 let byte_length = (self.quantity as usize).div_ceil(8);
186 &self.values[..byte_length]
187 }
188
189 /// Retrieves the boolean state of a specific input by its address.
190 ///
191 /// This method performs boundary checking to ensure the requested address is within
192 /// the range [from_address, from_address + quantity).
193 ///
194 /// # Arguments
195 /// * `address` - The Modbus address of the discrete input to query.
196 ///
197 /// # What happens:
198 /// 1. **Boundary Check**: The `address` is validated to ensure it falls within the range
199 /// `[self.from_address, self.from_address + self.quantity)`.
200 /// 2. **Bit Index Calculation**: The `bit_index` (zero-based offset from `from_address`) is calculated.
201 /// 3. **Byte and Bit Position**: The `byte_index` (`bit_index / 8`) determines which byte in the
202 /// `values` array contains the target bit, and `bit_in_byte` (`bit_index % 8`) determines
203 /// its position within that byte.
204 /// 4. **Masking**: A `bit_mask` (e.g., `0b0000_0001` for bit 0, `0b0000_0010` for bit 1) is created
205 /// to isolate the specific bit.
206 /// 5. **Extraction**: A bitwise AND operation (`&`) with the `bit_mask` is performed on the relevant byte.
207 /// If the result is non-zero, the bit is ON (`true`); otherwise, it's OFF (`false`).
208 ///
209 /// # Returns
210 /// * `Ok(true)` if the input is ON (1).
211 /// * `Ok(false)` if the input is OFF (0).
212 /// * `Err(MbusError::InvalidAddress)` if the address is out of the block's range.
213 pub fn value(&self, address: u16) -> Result<bool, MbusError> {
214 // Check if the requested address falls within our managed range
215 if address < self.from_address || address >= self.from_address + self.quantity {
216 return Err(MbusError::InvalidAddress);
217 }
218
219 // Calculate which byte and which bit within that byte contains the state
220 let bit_index = (address - self.from_address) as usize;
221 let byte_index = bit_index / 8;
222 let bit_mask = 1u8 << (bit_index % 8);
223
224 // Extract the bit using the mask and convert to boolean
225 Ok(self.values[byte_index] & bit_mask != 0)
226 }
227}