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