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}