Skip to main content

r413d08_lib/
protocol.rs

1//! Defines data structures, constants, and protocol logic for interacting
2//! with an 8-Channel Multifunction RS485 Module via Modbus RTU, based on
3//! the protocol specification document.
4//!
5//! This module covers:
6//! - Representing port states ([`PortState`], [`PortStates`]).
7//! - Identifying specific ports ([`Port`]) and the Modbus device address ([`Address`]).
8//! - Defining Modbus register addresses and data values for reading states and controlling ports.
9//! - Encoding and decoding values from/to Modbus register format ([`Word`]).
10//! - Error handling for invalid port or address values.
11//!
12//! Assumes standard Modbus function codes "Read Holding Registers" (0x03) and
13//! "Write Single Register" (0x06) are used externally to interact with the device.
14
15use thiserror::Error;
16
17/// A comprehensive error type for all operations within the `protocol` module.
18///
19/// This enum consolidates errors that can occur during the decoding
20/// data, encoding of values, or validation of parameters.
21#[derive(Error, Debug, PartialEq)]
22pub enum Error {
23    /// Error indicating that the data received from a Modbus read has an unexpected length.
24    #[error("Invalid data length: expected {expected}, got {got}")]
25    UnexpectedDataLength { expected: usize, got: usize },
26
27    /// Error for malformed data within a register, e.g., an unexpected non-zero upper byte.
28    #[error("Invalid data in register: {details}")]
29    InvalidData { details: String },
30
31    /// Error for an invalid value read from a register, e.g., an undefined baud rate code.
32    #[error("Invalid value code for {entity}: {code}")]
33    InvalidValueCode { entity: String, code: u16 },
34
35    /// Error for an attempt to encode a value that is not supported, e.g., a `NaN` temperature.
36    #[error("Cannot encode value: {reason}")]
37    EncodeError { reason: String },
38}
39
40/// Represents a single 16-bit value stored in a Modbus register.
41///
42/// Modbus RTU typically operates on 16-bit registers.
43pub type Word = u16;
44
45/// The total number of controllable digital I/O ports on the module.
46pub const NUMBER_OF_PORTS: usize = 8;
47
48/// Represents the state of a single digital I/O port (e.g., relay).
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
50#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
51pub enum PortState {
52    /// The port is inactive/relay OFF.
53    Close,
54    /// The port is active/relay ON.
55    Open,
56}
57
58impl PortState {
59    /// Decodes a [`PortState`] from a single Modbus holding register value (`Word`).
60    ///
61    /// # Arguments
62    ///
63    /// * `word`: The [`Word`] (u16) value read from the Modbus register for this port.
64    ///
65    /// # Returns
66    ///
67    /// The corresponding [`PortState`].
68    ///
69    /// # Example
70    /// ```
71    /// # use r413d08_lib::protocol::PortState;
72    /// assert_eq!(PortState::decode_from_holding_registers(0x0000), PortState::Close);
73    /// assert_eq!(PortState::decode_from_holding_registers(0x0001), PortState::Open);
74    /// ```
75    pub fn decode_from_holding_registers(word: Word) -> Self {
76        // According to the device protocol document:
77        // - `0x0000` represents [`PortState::Close`].
78        // - `0x0001` (and likely any non-zero value) represents [`PortState::Open`].
79        if word != 0 {
80            PortState::Open
81        } else {
82            PortState::Close
83        }
84    }
85}
86
87/// Provides a human-readable string representation ("close" or "open").
88impl std::fmt::Display for PortState {
89    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90        match self {
91            Self::Close => write!(f, "close"),
92            Self::Open => write!(f, "open"),
93        }
94    }
95}
96
97/// Represents the collective states of all [`NUMBER_OF_PORTS`] ports.
98///
99/// This struct holds an array of [`PortState`] and provides constants
100/// for reading all port states using a single Modbus "Read Holding Registers" (0x03) command.
101#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
102#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
103pub struct PortStates([PortState; NUMBER_OF_PORTS]);
104
105impl PortStates {
106    /// The Modbus function code 0x03 (Read Holding Registers) register address used for reading all port states sequentially.
107    pub const ADDRESS: u16 = 0x0001;
108    /// The number of consecutive Modbus registers (`Word`s) to read to get all port states.
109    ///
110    /// This corresponds to [`NUMBER_OF_PORTS`].
111    pub const QUANTITY: u16 = NUMBER_OF_PORTS as u16;
112
113    /// Decodes the states of all ports from a slice of Modbus holding register values.
114    ///
115    /// Expects `words` to contain [`NUMBER_OF_PORTS`] values. Each word in the slice
116    /// corresponds to the state of a single port, decoded via [`PortState::decode_from_holding_registers`].
117    ///
118    /// If `words` contains fewer than [`NUMBER_OF_PORTS`] items, the remaining
119    /// port states in the returned struct will retain their default initialized value (`PortState::Close`).
120    /// Extra items in `words` beyond [`NUMBER_OF_PORTS`] are ignored.
121    ///
122    /// # Arguments
123    ///
124    /// * `words`: A slice of [`Word`] containing the register values read from the device.
125    ///
126    /// # Returns
127    ///
128    /// A [`PortStates`] struct containing the decoded state for each port.
129    ///
130    /// This function is robust against malformed data:
131    /// - If `words` contains fewer than [`NUMBER_OF_PORTS`] items, the remaining
132    ///   port states default to [`PortState::Close`].
133    /// - If `words` contains more than [`NUMBER_OF_PORTS`] items, the extra
134    ///   items are ignored.
135    ///
136    /// # Example
137    /// ```
138    /// # use r413d08_lib::protocol::{PortState, PortStates, Word, NUMBER_OF_PORTS};
139    /// // Example data mimicking a Modbus read response for 8 registers
140    /// let modbus_data: [Word; NUMBER_OF_PORTS] = [0x1, 0x0, 0xFFFF, 0x0, 0x0, 0x0, 0x1234, 0x0];
141    /// let decoded_states = PortStates::decode_from_holding_registers(&modbus_data);
142    /// assert_eq!(decoded_states.as_array()[0], PortState::Open);
143    /// assert_eq!(decoded_states.as_array()[1], PortState::Close);
144    /// assert_eq!(decoded_states.as_array()[2], PortState::Open); // Non-zero treated as Open
145    /// // ... and so on for all ports
146    /// ```
147    pub fn decode_from_holding_registers(words: &[Word]) -> Self {
148        let mut port_states = [PortState::Close; NUMBER_OF_PORTS];
149        // Iterate over the words read, up to the number of ports we have storage for.
150        for (i, word) in words.iter().enumerate().take(NUMBER_OF_PORTS) {
151            port_states[i] = PortState::decode_from_holding_registers(*word);
152        }
153        Self(port_states)
154    }
155
156    /// Returns an iterator over the individual [`PortState`] values in the order of the ports.
157    pub fn iter(&self) -> std::slice::Iter<'_, PortState> {
158        self.0.iter()
159    }
160
161    /// Returns a slice containing all `Temperature` values.
162    pub fn as_slice(&self) -> &[PortState] {
163        &self.0
164    }
165
166    /// Provides direct access to the underlying array of port states.
167    pub fn as_array(&self) -> &[PortState; NUMBER_OF_PORTS] {
168        &self.0
169    }
170}
171
172impl IntoIterator for PortStates {
173    type Item = PortState;
174    type IntoIter = std::array::IntoIter<PortState, NUMBER_OF_PORTS>;
175
176    fn into_iter(self) -> Self::IntoIter {
177        self.0.into_iter()
178    }
179}
180
181impl<'a> IntoIterator for &'a PortStates {
182    type Item = &'a PortState;
183    type IntoIter = std::slice::Iter<'a, PortState>;
184
185    fn into_iter(self) -> Self::IntoIter {
186        self.0.iter()
187    }
188}
189
190/// Provides a comma-separated string representation of all port states (e.g., "close, open, close, ...").
191impl std::fmt::Display for PortStates {
192    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
193        let p_strs: Vec<String> = self.0.iter().map(|t| t.to_string()).collect();
194        write!(f, "{}", p_strs.join(", "))
195    }
196}
197
198impl std::ops::Index<usize> for PortStates {
199    type Output = PortState;
200
201    /// Allows indexing into the port states array.
202    ///
203    /// # Panics
204    /// Panics if the index is out of bounds (0-7).
205    fn index(&self, index: usize) -> &Self::Output {
206        &self.0[index]
207    }
208}
209
210/// Represents a validated port index, guaranteed to be within the valid range `0..NUMBER_OF_PORTS`.
211///
212/// Use [`Port::try_from`] to create an instance from a `u8`.
213/// This struct also defines constants for the *data values* used when controlling a specific port via
214/// Modbus "Write Single Register" (function code 0x06). The register *address*
215/// corresponds to the 1-based port index (see [`Port::address_for_write_register`]).
216#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
217#[cfg_attr(feature = "serde", derive(serde::Serialize))]
218pub struct Port(u8); // Internally stores 0-based index
219
220#[cfg(feature = "serde")]
221impl<'de> serde::Deserialize<'de> for Port {
222    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
223    where
224        D: serde::Deserializer<'de>,
225    {
226        let value = u8::deserialize(deserializer)?;
227        Port::try_from(value).map_err(serde::de::Error::custom)
228    }
229}
230
231impl Port {
232    /// The minimum valid port index (inclusive).
233    pub const MIN: u8 = 0;
234    /// The maximum valid port index (inclusive).
235    pub const MAX: u8 = NUMBER_OF_PORTS as u8 - 1;
236
237    // --- Modbus Register Data Values for Port Control ---
238    // These constant `Word` values are written to the specific port's Modbus register address
239    // using Modbus function code 0x06 (Write Single Register).
240
241    /// Register data value to **open** the specified port (turn relay ON / activate output).
242    pub const REG_DATA_SET_PORT_OPEN: Word = 0x0100;
243    /// Register data value to **close** the specified port (turn relay OFF / deactivate output).
244    pub const REG_DATA_SET_PORT_CLOSE: Word = 0x0200;
245    /// Register data value to **toggle** the state of the specified port (Open <-> Close). Also called "Self-locking".
246    pub const REG_DATA_SET_PORT_TOGGLE: Word = 0x0300;
247    /// Register data value to **latch** the specified port (set this port Open, set all others Close). Also called "Inter-locking".
248    pub const REG_DATA_SET_PORT_LATCH: Word = 0x0400;
249    /// Register data value to activate the specified port **momentarily** (Open for ~1 second, then automatically Close). Also called "Non-locking".
250    pub const REG_DATA_SET_PORT_MOMENTARY: Word = 0x0500;
251    /// Base register data value to initiate a **delayed action** on the specified port.
252    /// The actual delay (0-255 seconds) must be added to this value using [`Port::encode_delay_for_write_register`].
253    /// The action is typically Open -> wait delay -> Close.
254    pub const REG_DATA_SET_PORT_DELAY: Word = 0x0600;
255
256    /// Returns the Modbus register address for controlling this specific port.
257    ///
258    /// The address is used with Modbus function 0x06 (Write Single Register), where the
259    /// *data* written to this address determines the action (e.g., [`Port::REG_DATA_SET_PORT_OPEN`]).
260    ///
261    /// # Returns
262    ///
263    /// The `u16` Modbus address for controlling this port.
264    ///
265    /// # Example
266    /// ```
267    /// # use r413d08_lib::protocol::Port;
268    /// assert_eq!(Port::try_from(0).unwrap().address_for_write_register(), 0x0001);
269    /// assert_eq!(Port::try_from(7).unwrap().address_for_write_register(), 0x0008);
270    /// ```
271    pub fn address_for_write_register(&self) -> u16 {
272        // Add 1 to the 0-based port index to get the 1-based Modbus address.
273        (self.0 + 1) as u16
274    }
275
276    /// Encodes the register data value (`Word`) for setting a delayed action on a port.
277    ///
278    /// This combines the command code [`Port::REG_DATA_SET_PORT_DELAY`] (in the high byte)
279    /// with the desired delay duration (in the low byte). The resulting `Word` should be written
280    /// to the port's specific address (see [`Port::address_for_write_register`]) using
281    /// Modbus function 0x06.
282    ///
283    /// # Arguments
284    ///
285    /// * `delay`: The delay duration in seconds.
286    ///
287    /// # Returns
288    ///
289    /// The corresponding `Word` to be written to the register for the delayed action command.
290    ///
291    /// # Example
292    /// ```
293    /// # use r413d08_lib::protocol::{Port, Word};
294    /// // Command data to trigger a delayed action after 10 seconds:
295    /// let delay_command_data = Port::encode_delay_for_write_register(10);
296    /// assert_eq!(delay_command_data, 0x060A); // 0x0600 + 10
297    ///
298    /// // This value (0x060A) would then be written to the target port's Modbus address.
299    /// // e.g., for port 3 (address 0x0004): WriteRegister(address=4, value=0x060A)
300    /// ```
301    pub fn encode_delay_for_write_register(delay: u8) -> Word {
302        // Adds the delay (lower byte) to the command code (upper byte).
303        Self::REG_DATA_SET_PORT_DELAY + (delay as Word)
304    }
305}
306
307/// Allows accessing the inner `u8` port index value directly.
308impl std::ops::Deref for Port {
309    type Target = u8;
310    fn deref(&self) -> &Self::Target {
311        &self.0
312    }
313}
314
315/// Error indicating that a provided port index (`u8`) is outside the valid range
316/// defined by [`Port::MIN`] and [`Port::MAX`] (inclusive).
317#[derive(Error, Debug, Clone, Copy, PartialEq, Eq)]
318#[error(
319    "The port value {0} is outside the valid range of {min} to {max}",
320    min = Port::MIN,
321    max = Port::MAX
322)]
323pub struct ErrorPortOutOfRange(
324    /// The invalid port value that caused the error.
325    pub u8,
326);
327
328impl TryFrom<u8> for Port {
329    type Error = ErrorPortOutOfRange;
330
331    /// Attempts to create a [`Port`] from a `u8` value, validating its 0-based range.
332    ///
333    /// # Arguments
334    ///
335    /// * `value`: The port index to validate.
336    ///
337    /// # Returns
338    ///
339    /// * `Ok(Port)`: If the `value` is within the valid range [[`Port::MIN`], [`Port::MAX`]].
340    /// * `Err(ErrorPortOutOfRange)`: If the `value` is outside the valid range.
341    ///
342    /// # Example
343    /// ```
344    /// # use r413d08_lib::protocol::{Port, ErrorPortOutOfRange, NUMBER_OF_PORTS};
345    /// let max_port_index = (NUMBER_OF_PORTS - 1) as u8; // Should be 7
346    /// assert!(Port::try_from(0).is_ok());
347    /// assert!(Port::try_from(max_port_index).is_ok());
348    /// let invalid_index = max_port_index + 1; // Should be 8
349    /// assert_eq!(Port::try_from(invalid_index).unwrap_err(), ErrorPortOutOfRange(invalid_index));
350    /// ```
351    fn try_from(value: u8) -> Result<Self, Self::Error> {
352        // Check if the value as usize is within the valid range constants.
353        if (Self::MIN..=Self::MAX).contains(&value) {
354            Ok(Self(value))
355        } else {
356            Err(ErrorPortOutOfRange(value))
357        }
358    }
359}
360
361impl std::fmt::Display for Port {
362    /// Formats the port as its number (e.g., "0", "7").
363    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
364        write!(f, "{}", self.0)
365    }
366}
367
368/// A zero-sized type providing constants for controlling all ports simultaneously.
369pub struct PortsAll;
370
371impl PortsAll {
372    /// The address is used with Modbus function 0x06 (Write Single Register) for commands that affect
373    /// all ports simultaneously, where the *data* that is written to this address determines the action
374    /// (e.g. [`PortsAll::REG_DATA_SET_ALL_OPEN`]).
375    pub const ADDRESS: u16 = 0x0000;
376
377    /// Register data value to **open all** ports simultaneously (turn all relays ON).
378    /// This value should be written to [`PortsAll::ADDRESS`].
379    pub const REG_DATA_SET_ALL_OPEN: Word = 0x0700;
380
381    /// Register data value to **close all** ports simultaneously (turn all relays OFF).
382    /// This value should be written to [`PortsAll::ADDRESS`].
383    pub const REG_DATA_SET_ALL_CLOSE: Word = 0x0800;
384}
385
386/// Represents a validated Modbus device address, used for RTU communication over RS485.
387///
388/// Valid addresses are in the range 1 to 247 (inclusive).
389/// Use [`Address::try_from`] to create an instance from a `u8`.
390/// Provides constants and methods for reading/writing the device address itself via Modbus commands.
391#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
392#[cfg_attr(feature = "serde", derive(serde::Serialize))]
393pub struct Address(u8);
394
395#[cfg(feature = "serde")]
396impl<'de> serde::Deserialize<'de> for Address {
397    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
398    where
399        D: serde::Deserializer<'de>,
400    {
401        let value = u8::deserialize(deserializer)?;
402        Address::try_from(value).map_err(serde::de::Error::custom)
403    }
404}
405
406/// Allows accessing the inner `u8` address value directly.
407impl std::ops::Deref for Address {
408    type Target = u8;
409    fn deref(&self) -> &Self::Target {
410        &self.0
411    }
412}
413
414impl Default for Address {
415    /// Returns the factory default Modbus address.
416    fn default() -> Self {
417        // Safe because `0x01` is within the valid range 1-247.
418        Self(0x01)
419    }
420}
421
422impl Address {
423    /// The Modbus register address used to read or write the device's own Modbus address.
424    ///
425    /// Use Modbus function 0x03 (Read Holding Registers) to read (requires addressing
426    /// the device with its *current* address or the broadcast address).
427    /// Use function 0x06 (Write Single Register) to change the address (also requires
428    /// addressing the device with its current address).
429    pub const ADDRESS: u16 = 0x00FF;
430
431    /// The number of registers to read when reading the device address.
432    pub const QUANTITY: u16 = 1;
433
434    /// The minimum valid assignable Modbus device address (inclusive).
435    pub const MIN: u8 = 1;
436    /// The maximum valid assignable Modbus device address (inclusive).
437    pub const MAX: u8 = 247;
438
439    /// The Modbus broadcast address (`0xFF` or 255).
440    ///
441    /// Can be used for reading the device address when it's unknown.
442    /// This address cannot be assigned to a device as its permanent address.
443    pub const BROADCAST: Address = Address(0xFF);
444
445    /// Decodes the device [`Address`] from a Modbus holding register value read from the device.
446    ///
447    /// Expects `words` to contain the single register value read from the device address
448    /// configuration register ([`Address::ADDRESS`]).
449    /// It validates that the decoded address is within the assignable range ([`Address::MIN`]..=[`Address::MAX`]).
450    ///
451    /// # Arguments
452    ///
453    /// * `words`: A slice containing the [`Word`] value read from the device address register. Expected to have length 1.
454    ///
455    /// # Returns
456    ///
457    /// An [`Address`] struct containing the decoded and validated value.
458    ///
459    /// # Panics
460    ///
461    /// Panics if:
462    /// 1.  `words` is empty.
463    /// 2.  The upper byte of the `Word` containing the address is non-zero, indicating unexpected data.
464    /// 3.  The address value read from the register is outside the valid assignable range.
465    pub fn decode_from_holding_registers(words: &[Word]) -> Result<Self, Error> {
466        if words.len() != Self::QUANTITY as usize {
467            return Err(Error::UnexpectedDataLength {
468                expected: Self::QUANTITY as usize,
469                got: words.len(),
470            });
471        }
472        let word_value = words[0];
473
474        if word_value & 0xFF00 != 0 {
475            return Err(Error::InvalidData {
476                details: format!(
477                    "Upper byte of address register is non-zero (value: {word_value:#06X})"
478                ),
479            });
480        }
481
482        let address_byte = word_value as u8;
483        match Self::try_from(address_byte) {
484            Ok(address) => Ok(address),
485            Err(err) => Err(Error::InvalidData {
486                details: format!("{err}"),
487            }),
488        }
489    }
490
491    /// Encodes the device [`Address`] into a [`Word`] value suitable for writing to the
492    /// device address configuration register ([`Address::ADDRESS`]) using Modbus function 0x06.
493    ///
494    /// # Returns
495    ///
496    /// The [`Word`] (u16) representation of the address value.
497    pub fn encode_for_write_register(&self) -> Word {
498        self.0 as u16
499    }
500}
501
502/// Error indicating that a provided Modbus device address (`u8`) is outside the valid range
503/// for assignable addresses, defined by [`Address::MIN`] and [`Address::MAX`] (inclusive).
504#[derive(Error, Debug, Clone, Copy, PartialEq, Eq)]
505#[error(
506    "The address value {0} is outside the valid assignable range of {min} to {max}",
507    min = Address::MIN,
508    max = Address::MAX
509)]
510pub struct ErrorAddressOutOfRange(
511    /// The invalid address value that caused the error.
512    pub u8,
513);
514
515impl TryFrom<u8> for Address {
516    type Error = ErrorAddressOutOfRange;
517
518    /// Attempts to create an [`Address`] from a `u8` value, validating its assignable range [[`Address::MIN`], [`Address::MAX`]].
519    ///
520    /// # Arguments
521    ///
522    /// * `value`: The Modbus address to validate.
523    ///
524    /// # Returns
525    ///
526    /// * `Ok(Address)`: If the `value` is within the valid assignable range [[`Address::MIN`], [`Address::MAX`]].
527    /// * `Err(ErrorAddressOutOfRange)`: If the `value` is outside the valid assignable range (e.g., 0 or > 247).
528    ///
529    /// # Example
530    /// ```
531    /// # use r413d08_lib::protocol::{Address, ErrorAddressOutOfRange};
532    /// assert!(Address::try_from(0).is_err());
533    /// assert!(Address::try_from(1).is_ok());
534    /// assert!(Address::try_from(247).is_ok());
535    /// assert_eq!(Address::try_from(248).unwrap_err(), ErrorAddressOutOfRange(248));
536    /// assert!(Address::try_from(255).is_err()); // Broadcast address is not valid for TryFrom
537    /// ```
538    fn try_from(value: u8) -> Result<Self, Self::Error> {
539        if (Self::MIN..=Self::MAX).contains(&value) {
540            Ok(Self(value))
541        } else {
542            Err(ErrorAddressOutOfRange(value))
543        }
544    }
545}
546
547/// Provides a hexadecimal string representation (e.g., "0x01", "0xf7").
548impl std::fmt::Display for Address {
549    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
550        write!(f, "{:#04x}", self.0)
551    }
552}
553
554#[cfg(test)]
555mod tests {
556    use super::*;
557    use assert_matches::assert_matches;
558
559    // --- Address Tests ---
560    #[test]
561    fn address_try_from_validation() {
562        assert!(matches!(
563            Address::try_from(0),
564            Err(ErrorAddressOutOfRange(0))
565        ));
566        assert!(matches!(Address::try_from(Address::MIN), Ok(Address(1))));
567        assert!(matches!(Address::try_from(Address::MAX), Ok(Address(247))));
568        assert!(matches!(
569            Address::try_from(Address::MAX + 1),
570            Err(ErrorAddressOutOfRange(248))
571        ));
572        assert!(matches!(
573            Address::try_from(255),
574            Err(ErrorAddressOutOfRange(255))
575        ));
576        assert!(matches!(Address::try_from(100), Ok(Address(100))));
577    }
578
579    #[test]
580    fn address_default() {
581        assert_eq!(Address::default(), Address(1));
582    }
583
584    #[test]
585    fn address_encode_decode() {
586        let addr = Address::try_from(42).unwrap();
587        let encoded = addr.encode_for_write_register();
588        assert_eq!(encoded, 42u16);
589        // Test decode (assumes valid input from register)
590        let decoded = Address::decode_from_holding_registers(&[encoded]).unwrap();
591        assert_eq!(decoded, addr);
592    }
593
594    #[test]
595    fn address_decode_panics_on_empty() {
596        assert_matches!(Address::decode_from_holding_registers(&[]), Err(..));
597    }
598
599    #[test]
600    fn address_decode_panics_on_invalid_value_zero() {
601        // 0 is outside the valid 1-247 range
602        assert_matches!(Address::decode_from_holding_registers(&[0x0000]), Err(..));
603    }
604
605    #[test]
606    fn address_decode_panics_on_invalid_value_high() {
607        // 248 is outside the valid 1-247 range
608        assert_matches!(Address::decode_from_holding_registers(&[0x00F8]), Err(..));
609    }
610
611    #[test]
612    fn address_decode_valid() {
613        assert_eq!(
614            Address::decode_from_holding_registers(&[0x0001]),
615            Ok(Address(1))
616        );
617        assert_eq!(
618            Address::decode_from_holding_registers(&[0x00F7]),
619            Ok(Address(247))
620        );
621    }
622
623    // --- Port Tests ---
624    #[test]
625    fn port_try_from_validation() {
626        assert!(matches!(Port::try_from(Port::MIN), Ok(Port(0))));
627        assert!(matches!(Port::try_from(Port::MAX), Ok(Port(7))));
628        assert!(matches!(
629            Port::try_from(Port::MAX + 1),
630            Err(ErrorPortOutOfRange(8))
631        ));
632        assert!(matches!(Port::try_from(3), Ok(Port(3))));
633    }
634
635    #[test]
636    fn port_address_for_write_register_is_one_based() {
637        // Check if address is 1-based according to documentation
638        assert_eq!(Port::try_from(0).unwrap().address_for_write_register(), 1); // Port 0 -> Address 1
639        assert_eq!(Port::try_from(1).unwrap().address_for_write_register(), 2); // Port 1 -> Address 2
640        assert_eq!(Port::try_from(7).unwrap().address_for_write_register(), 8); // Port 7 -> Address 8
641    }
642
643    #[test]
644    fn port_encode_delay() {
645        assert_eq!(Port::encode_delay_for_write_register(0), 0x0600);
646        assert_eq!(Port::encode_delay_for_write_register(10), 0x060A);
647        assert_eq!(Port::encode_delay_for_write_register(255), 0x06FF);
648    }
649
650    // --- PortState / PortStates Tests ---
651    #[test]
652    fn port_state_decode() {
653        assert_eq!(
654            PortState::decode_from_holding_registers(0x0000),
655            PortState::Close
656        );
657        assert_eq!(
658            PortState::decode_from_holding_registers(0x0001),
659            PortState::Open
660        );
661        assert_eq!(
662            PortState::decode_from_holding_registers(0xFFFF),
663            PortState::Open
664        );
665    }
666
667    #[test]
668    fn port_states_decode() {
669        // Test case 1: Correct number of words, all closed
670        let words_all_closed = [0x0000; NUMBER_OF_PORTS];
671        let expected_all_closed = PortStates([PortState::Close; NUMBER_OF_PORTS]);
672        assert_eq!(
673            PortStates::decode_from_holding_registers(&words_all_closed),
674            expected_all_closed,
675            "Should decode all-closed states correctly"
676        );
677
678        // Test case 2: Correct number of words, mixed states
679        let words_mixed = [
680            0x0001, 0x0000, 0x0001, 0x0000, 0x0001, 0x0000, 0x0001, 0x0000,
681        ];
682        let expected_mixed = PortStates([
683            PortState::Open,
684            PortState::Close,
685            PortState::Open,
686            PortState::Close,
687            PortState::Open,
688            PortState::Close,
689            PortState::Open,
690            PortState::Close,
691        ]);
692        assert_eq!(
693            PortStates::decode_from_holding_registers(&words_mixed),
694            expected_mixed,
695            "Should decode mixed states correctly"
696        );
697
698        // Test case 3: Fewer words than expected (short read)
699        let words_short = [0x0001, 0x0000]; // Only 2 words
700        let mut expected_short_arr = [PortState::Close; NUMBER_OF_PORTS];
701        expected_short_arr[0] = PortState::Open;
702        expected_short_arr[1] = PortState::Close;
703        let expected_short = PortStates(expected_short_arr);
704        assert_eq!(
705            PortStates::decode_from_holding_registers(&words_short),
706            expected_short,
707            "Should handle short reads by filling with Close"
708        );
709
710        // Test case 4: More words than expected (long read)
711        let words_long = [
712            0x0000, 0x0001, 0x0000, 0x0001, 0x0000, 0x0001, 0x0000, 0x0001, 0x9999, 0x8888,
713        ]; // 10 words
714        let expected_long = PortStates([
715            PortState::Close,
716            PortState::Open,
717            PortState::Close,
718            PortState::Open,
719            PortState::Close,
720            PortState::Open,
721            PortState::Close,
722            PortState::Open,
723        ]);
724        assert_eq!(
725            PortStates::decode_from_holding_registers(&words_long),
726            expected_long,
727            "Should handle long reads by ignoring extra words"
728        );
729
730        // Test case 5: Empty slice
731        let words_empty: [Word; 0] = [];
732        let expected_empty = PortStates([PortState::Close; NUMBER_OF_PORTS]);
733        assert_eq!(
734            PortStates::decode_from_holding_registers(&words_empty),
735            expected_empty,
736            "Should handle empty slice by returning all-closed"
737        );
738    }
739
740    // --- Display Tests ---
741    #[test]
742    fn display_formats() {
743        assert_eq!(PortState::Open.to_string(), "open");
744        assert_eq!(PortState::Close.to_string(), "close");
745        assert_eq!(Address(1).to_string(), "0x01");
746        assert_eq!(Address(247).to_string(), "0xf7");
747        assert_eq!(Address::BROADCAST.to_string(), "0xff"); // Direct creation
748        let states = PortStates([
749            PortState::Open,
750            PortState::Close,
751            PortState::Open,
752            PortState::Close,
753            PortState::Close,
754            PortState::Close,
755            PortState::Close,
756            PortState::Close,
757        ]);
758        assert_eq!(
759            states.to_string(),
760            "open, close, open, close, close, close, close, close"
761        );
762    }
763}