Skip to main content

rustmod_client/
points.rs

1use crate::{ClientError, InvalidResponseKind};
2
3/// A local cache of coil values mapped to their Modbus addresses.
4///
5/// Use [`apply_read`](CoilPoints::apply_read) to merge values returned by
6/// [`ModbusClient::read_coils`](crate::ModbusClient::read_coils).
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct CoilPoints {
9    start_address: u16,
10    values: Vec<bool>,
11}
12
13impl CoilPoints {
14    /// Create a new coil cache with `count` coils starting at `start_address`, all initially `false`.
15    #[must_use]
16    pub fn new(start_address: u16, count: usize) -> Self {
17        Self {
18            start_address,
19            values: vec![false; count],
20        }
21    }
22
23    /// Create a coil cache from existing values.
24    #[must_use]
25    pub fn from_values(start_address: u16, values: Vec<bool>) -> Self {
26        Self {
27            start_address,
28            values,
29        }
30    }
31
32    /// The starting Modbus address of this cache.
33    pub fn start_address(&self) -> u16 {
34        self.start_address
35    }
36
37    /// The number of coils in this cache.
38    pub fn len(&self) -> usize {
39        self.values.len()
40    }
41
42    /// Returns `true` if the cache contains no coils.
43    pub fn is_empty(&self) -> bool {
44        self.values.is_empty()
45    }
46
47    /// View the raw coil values as a slice.
48    pub fn values(&self) -> &[bool] {
49        &self.values
50    }
51
52    /// Get the coil value at the given Modbus address, or `None` if out of range.
53    pub fn get(&self, address: u16) -> Option<bool> {
54        let offset = usize::from(address.checked_sub(self.start_address)?);
55        self.values.get(offset).copied()
56    }
57
58    /// Set the coil value at the given Modbus address.
59    pub fn set(&mut self, address: u16, value: bool) -> Result<(), ClientError> {
60        let offset = usize::from(
61            address
62                .checked_sub(self.start_address)
63                .ok_or(ClientError::InvalidResponse(InvalidResponseKind::Other("coil address out of range")))?,
64        );
65        let slot = self
66            .values
67            .get_mut(offset)
68            .ok_or(ClientError::InvalidResponse(InvalidResponseKind::Other("coil address out of range")))?;
69        *slot = value;
70        Ok(())
71    }
72
73    /// Merge a batch of read values into this cache at the given start address.
74    pub fn apply_read(&mut self, start_address: u16, values: &[bool]) -> Result<(), ClientError> {
75        for (i, value) in values.iter().copied().enumerate() {
76            let offset = u16::try_from(i)
77                .map_err(|_| ClientError::InvalidResponse(InvalidResponseKind::Other("coil address overflow")))?;
78            let addr = start_address
79                .checked_add(offset)
80                .ok_or(ClientError::InvalidResponse(InvalidResponseKind::Other("coil address overflow")))?;
81            self.set(addr, value)?;
82        }
83        Ok(())
84    }
85}
86
87/// A local cache of 16-bit register values mapped to their Modbus addresses.
88///
89/// Use [`apply_read`](RegisterPoints::apply_read) to merge values returned by
90/// [`ModbusClient::read_holding_registers`](crate::ModbusClient::read_holding_registers).
91#[derive(Debug, Clone, PartialEq, Eq)]
92pub struct RegisterPoints {
93    start_address: u16,
94    values: Vec<u16>,
95}
96
97impl RegisterPoints {
98    /// Create a new register cache with `count` registers starting at `start_address`, all initially `0`.
99    #[must_use]
100    pub fn new(start_address: u16, count: usize) -> Self {
101        Self {
102            start_address,
103            values: vec![0; count],
104        }
105    }
106
107    /// Create a register cache from existing values.
108    #[must_use]
109    pub fn from_values(start_address: u16, values: Vec<u16>) -> Self {
110        Self {
111            start_address,
112            values,
113        }
114    }
115
116    /// The starting Modbus address of this cache.
117    pub fn start_address(&self) -> u16 {
118        self.start_address
119    }
120
121    /// The number of registers in this cache.
122    pub fn len(&self) -> usize {
123        self.values.len()
124    }
125
126    /// Returns `true` if the cache contains no registers.
127    pub fn is_empty(&self) -> bool {
128        self.values.is_empty()
129    }
130
131    /// View the raw register values as a slice.
132    pub fn values(&self) -> &[u16] {
133        &self.values
134    }
135
136    /// Get the register value at the given Modbus address, or `None` if out of range.
137    pub fn get(&self, address: u16) -> Option<u16> {
138        let offset = usize::from(address.checked_sub(self.start_address)?);
139        self.values.get(offset).copied()
140    }
141
142    /// Set the register value at the given Modbus address.
143    pub fn set(&mut self, address: u16, value: u16) -> Result<(), ClientError> {
144        let offset = usize::from(
145            address
146                .checked_sub(self.start_address)
147                .ok_or(ClientError::InvalidResponse(InvalidResponseKind::Other("register address out of range")))?,
148        );
149        let slot = self
150            .values
151            .get_mut(offset)
152            .ok_or(ClientError::InvalidResponse(InvalidResponseKind::Other("register address out of range")))?;
153        *slot = value;
154        Ok(())
155    }
156
157    /// Merge a batch of read values into this cache at the given start address.
158    pub fn apply_read(&mut self, start_address: u16, values: &[u16]) -> Result<(), ClientError> {
159        for (i, value) in values.iter().copied().enumerate() {
160            let offset = u16::try_from(i)
161                .map_err(|_| ClientError::InvalidResponse(InvalidResponseKind::Other("register address overflow")))?;
162            let addr = start_address
163                .checked_add(offset)
164                .ok_or(ClientError::InvalidResponse(InvalidResponseKind::Other("register address overflow")))?;
165            self.set(addr, value)?;
166        }
167        Ok(())
168    }
169}
170
171#[cfg(test)]
172mod tests {
173    use super::{CoilPoints, RegisterPoints};
174
175    #[test]
176    fn coil_points_apply_read() {
177        let mut points = CoilPoints::new(10, 4);
178        points.apply_read(11, &[true, false]).unwrap();
179        assert_eq!(points.get(10), Some(false));
180        assert_eq!(points.get(11), Some(true));
181        assert_eq!(points.get(12), Some(false));
182    }
183
184    #[test]
185    fn register_points_apply_read() {
186        let mut points = RegisterPoints::new(100, 3);
187        points.apply_read(100, &[10, 20, 30]).unwrap();
188        assert_eq!(points.get(101), Some(20));
189        points.set(102, 42).unwrap();
190        assert_eq!(points.get(102), Some(42));
191    }
192}