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 /// # Example
105 /// ```
106 /// # use r413d08_lib::protocol::{PortState, PortStates, Word, NUMBER_OF_PORTS};
107 /// // Example data mimicking a Modbus read response for 8 registers
108 /// let modbus_data: [Word; NUMBER_OF_PORTS] = [0x1, 0x0, 0xFFFF, 0x0, 0x0, 0x0, 0x1234, 0x0];
109 /// let decoded_states = PortStates::decode_from_holding_registers(&modbus_data);
110 /// assert_eq!(decoded_states.as_array()[0], PortState::Open);
111 /// assert_eq!(decoded_states.as_array()[1], PortState::Close);
112 /// assert_eq!(decoded_states.as_array()[2], PortState::Open); // Non-zero treated as Open
113 /// // ... and so on for all ports
114 /// ```
115 pub fn decode_from_holding_registers(words: &[Word]) -> Self {
116 let mut port_states = [PortState::Close; NUMBER_OF_PORTS];
117 // Iterate over the words read, up to the number of ports we have storage for.
118 for (i, word) in words.iter().enumerate().take(NUMBER_OF_PORTS) {
119 port_states[i] = PortState::decode_from_holding_registers(*word);
120 }
121 Self(port_states)
122 }
123
124 /// Returns an iterator over the individual [`PortState`] values in the order of the ports.
125 pub fn iter(&self) -> std::slice::Iter<'_, PortState> {
126 self.0.iter()
127 }
128
129 /// Provides direct access to the underlying array of port states.
130 pub fn as_array(&self) -> &[PortState; NUMBER_OF_PORTS] {
131 &self.0
132 }
133}
134
135/// Provides a comma-separated string representation of all port states (e.g., "close, open, close, ...").
136impl std::fmt::Display for PortStates {
137 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
138 let mut first = true;
139 for state in self.iter() {
140 if !first {
141 write!(f, ", ")?;
142 }
143 write!(f, "{}", state)?;
144 first = false;
145 }
146 Ok(())
147 }
148}
149
150/// Represents a validated port index, guaranteed to be within the valid range `0..NUMBER_OF_PORTS`.
151///
152/// Use [`Port::try_from`] to create an instance from a `u8`.
153/// This struct also defines constants for the *data values* used when controlling a specific port via
154/// Modbus "Write Single Register" (function code 0x06). The register *address*
155/// corresponds to the 1-based port index (see [`Port::address_for_write_register`]).
156#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
157#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
158pub struct Port(u8); // Internally stores 0-based index
159
160impl Port {
161 /// The minimum valid port index (inclusive).
162 pub const MIN: u8 = 0;
163 /// The maximum valid port index (inclusive).
164 pub const MAX: u8 = NUMBER_OF_PORTS as u8 - 1;
165
166 // --- Modbus Register Data Values for Port Control ---
167 // These constant `Word` values are written to the specific port's Modbus register address
168 // using Modbus function code 0x06 (Write Single Register).
169
170 /// Register data value to **open** the specified port (turn relay ON / activate output).
171 pub const REG_DATA_SET_PORT_OPEN: Word = 0x0100;
172 /// Register data value to **close** the specified port (turn relay OFF / deactivate output).
173 pub const REG_DATA_SET_PORT_CLOSE: Word = 0x0200;
174 /// Register data value to **toggle** the state of the specified port (Open <-> Close). Also called "Self-locking".
175 pub const REG_DATA_SET_PORT_TOGGLE: Word = 0x0300;
176 /// Register data value to **latch** the specified port (set this port Open, set all others Close). Also called "Inter-locking".
177 pub const REG_DATA_SET_PORT_LATCH: Word = 0x0400;
178 /// Register data value to activate the specified port **momentarily** (Open for ~1 second, then automatically Close). Also called "Non-locking".
179 pub const REG_DATA_SET_PORT_MOMENTARY: Word = 0x0500;
180 /// Base register data value to initiate a **delayed action** on the specified port.
181 /// The actual delay (0-255 seconds) must be added to this value using [`Port::encode_delay_for_write_register`].
182 /// The action is typically Open -> wait delay -> Close.
183 pub const REG_DATA_SET_PORT_DELAY: Word = 0x0600;
184
185 /// Returns the Modbus register address for controlling this specific port.
186 ///
187 /// The address is used with Modbus function 0x06 (Write Single Register), where the
188 /// *data* written to this address determines the action (e.g., [`Port::REG_DATA_SET_PORT_OPEN`]).
189 ///
190 /// # Returns
191 ///
192 /// The `u16` Modbus address for controlling this port.
193 ///
194 /// # Example
195 /// ```
196 /// # use r413d08_lib::protocol::Port;
197 /// assert_eq!(Port::try_from(0).unwrap().address_for_write_register(), 0x0001);
198 /// assert_eq!(Port::try_from(7).unwrap().address_for_write_register(), 0x0008);
199 /// ```
200 pub fn address_for_write_register(&self) -> u16 {
201 // Add 1 to the 0-based port index to get the 1-based Modbus address.
202 (self.0 + 1) as u16
203 }
204
205 /// Encodes the register data value (`Word`) for setting a delayed action on a port.
206 ///
207 /// This combines the command code [`Port::REG_DATA_SET_PORT_DELAY`] (in the high byte)
208 /// with the desired delay duration (in the low byte). The resulting `Word` should be written
209 /// to the port's specific address (see [`Port::address_for_write_register`]) using
210 /// Modbus function 0x06.
211 ///
212 /// # Arguments
213 ///
214 /// * `delay`: The delay duration in seconds.
215 ///
216 /// # Returns
217 ///
218 /// The corresponding `Word` to be written to the register for the delayed action command.
219 ///
220 /// # Example
221 /// ```
222 /// # use r413d08_lib::protocol::{Port, Word};
223 /// // Command data to trigger a delayed action after 10 seconds:
224 /// let delay_command_data = Port::encode_delay_for_write_register(10);
225 /// assert_eq!(delay_command_data, 0x060A); // 0x0600 + 10
226 ///
227 /// // This value (0x060A) would then be written to the target port's Modbus address.
228 /// // e.g., for port 3 (address 0x0004): WriteRegister(address=4, value=0x060A)
229 /// ```
230 pub fn encode_delay_for_write_register(delay: u8) -> Word {
231 // Adds the delay (lower byte) to the command code (upper byte).
232 Self::REG_DATA_SET_PORT_DELAY + (delay as Word)
233 }
234}
235
236/// Allows accessing the inner `u8` port index value directly.
237impl std::ops::Deref for Port {
238 type Target = u8;
239 fn deref(&self) -> &Self::Target {
240 &self.0
241 }
242}
243
244/// Error indicating that a provided port index (`u8`) is outside the valid range
245/// defined by [`Port::MIN`] and [`Port::MAX`] (inclusive).
246#[derive(Error, Debug, Clone, Copy, PartialEq, Eq)]
247#[error(
248 "The port value {0} is outside the valid range of {min} to {max}",
249 min = Port::MIN,
250 max = Port::MAX
251)]
252pub struct ErrorPortOutOfRange(
253 /// The invalid port value that caused the error.
254 pub u8,
255);
256
257impl TryFrom<u8> for Port {
258 type Error = ErrorPortOutOfRange;
259
260 /// Attempts to create a [`Port`] from a `u8` value, validating its 0-based range.
261 ///
262 /// # Arguments
263 ///
264 /// * `value`: The port index to validate.
265 ///
266 /// # Returns
267 ///
268 /// * `Ok(Port)`: If the `value` is within the valid range [[`Port::MIN`], [`Port::MAX`]].
269 /// * `Err(ErrorPortOutOfRange)`: If the `value` is outside the valid range.
270 ///
271 /// # Example
272 /// ```
273 /// # use r413d08_lib::protocol::{Port, ErrorPortOutOfRange, NUMBER_OF_PORTS};
274 /// let max_port_index = (NUMBER_OF_PORTS - 1) as u8; // Should be 7
275 /// assert!(Port::try_from(0).is_ok());
276 /// assert!(Port::try_from(max_port_index).is_ok());
277 /// let invalid_index = max_port_index + 1; // Should be 8
278 /// assert_eq!(Port::try_from(invalid_index).unwrap_err(), ErrorPortOutOfRange(invalid_index));
279 /// ```
280 fn try_from(value: u8) -> Result<Self, Self::Error> {
281 // Check if the value as usize is within the valid range constants.
282 if (Self::MIN..=Self::MAX).contains(&value) {
283 Ok(Self(value))
284 } else {
285 Err(ErrorPortOutOfRange(value))
286 }
287 }
288}
289
290/// A zero-sized type providing constants for controlling all ports simultaneously.
291pub struct PortsAll;
292
293impl PortsAll {
294 /// The address is used with Modbus function 0x06 (Write Single Register) for commands that affect
295 /// all ports simultaneously, where the *data* that is written to this address determines the action
296 /// (e.g. [`PortsAll::REG_DATA_SET_ALL_OPEN`]).
297 pub const ADDRESS: u16 = 0x0000;
298
299 /// Register data value to **open all** ports simultaneously (turn all relays ON).
300 /// This value should be written to [`PortsAll::ADDRESS`].
301 pub const REG_DATA_SET_ALL_OPEN: Word = 0x0700;
302
303 /// Register data value to **close all** ports simultaneously (turn all relays OFF).
304 /// This value should be written to [`PortsAll::ADDRESS`].
305 pub const REG_DATA_SET_ALL_CLOSE: Word = 0x0800;
306}
307
308/// Represents a validated Modbus device address, used for RTU communication over RS485.
309///
310/// Valid addresses are in the range 1 to 247 (inclusive).
311/// Use [`Address::try_from`] to create an instance from a `u8`.
312/// Provides constants and methods for reading/writing the device address itself via Modbus commands.
313#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
314#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
315pub struct Address(u8);
316
317/// Allows accessing the inner `u8` address value directly.
318impl std::ops::Deref for Address {
319 type Target = u8;
320 fn deref(&self) -> &Self::Target {
321 &self.0
322 }
323}
324
325impl Default for Address {
326 /// Returns the factory default Modbus address.
327 fn default() -> Self {
328 // Safe because `0x01` is within the valid range 1-247.
329 Self(0x01)
330 }
331}
332
333impl Address {
334 /// The Modbus register address used to read or write the device's own Modbus address.
335 ///
336 /// Use Modbus function 0x03 (Read Holding Registers) to read (requires addressing
337 /// the device with its *current* address or the broadcast address).
338 /// Use function 0x06 (Write Single Register) to change the address (also requires
339 /// addressing the device with its current address).
340 pub const ADDRESS: u16 = 0x00FF;
341
342 /// The number of registers to read when reading the device address.
343 pub const QUANTITY: u16 = 1;
344
345 /// The minimum valid assignable Modbus device address (inclusive).
346 pub const MIN: u8 = 1;
347 /// The maximum valid assignable Modbus device address (inclusive).
348 pub const MAX: u8 = 247;
349
350 /// The Modbus broadcast address (`0xFF` or 255).
351 ///
352 /// Can be used for reading the device address when it's unknown.
353 /// This address cannot be assigned to a device as its permanent address.
354 pub const BROADCAST: Address = Address(0xFF);
355
356 /// Decodes the device [`Address`] from a Modbus holding register value read from the device.
357 ///
358 /// Expects `words` to contain the single register value read from the device address
359 /// configuration register ([`Address::ADDRESS`]).
360 /// It validates that the decoded address is within the assignable range ([`Address::MIN`]..=[`Address::MAX`]).
361 ///
362 /// # Arguments
363 ///
364 /// * `words`: A slice containing the [`Word`] value read from the device address register. Expected to have length 1.
365 ///
366 /// # Returns
367 ///
368 /// An [`Address`] struct containing the decoded and validated value.
369 ///
370 /// # Panics
371 ///
372 /// Panics if:
373 /// 1. `words` is empty.
374 /// 2. The address value read from the register is outside the valid range.
375 pub fn decode_from_holding_registers(words: &[Word]) -> Self {
376 let word_value = *words
377 .first()
378 .expect("Register data for address must not be empty");
379 // Attempt to convert the u8 value, panicking if it's out of range.
380 Self::try_from(word_value as u8).expect("Invalid address value read from device register")
381 }
382
383 /// Encodes the device [`Address`] into a [`Word`] value suitable for writing to the
384 /// device address configuration register ([`Address::ADDRESS`]) using Modbus function 0x06.
385 ///
386 /// # Returns
387 ///
388 /// The [`Word`] (u16) representation of the address value.
389 pub fn encode_for_write_register(&self) -> Word {
390 self.0 as u16
391 }
392}
393
394/// Error indicating that a provided Modbus device address (`u8`) is outside the valid range
395/// for assignable addresses, defined by [`Address::MIN`] and [`Address::MAX`] (inclusive).
396#[derive(Error, Debug, Clone, Copy, PartialEq, Eq)]
397#[error(
398 "The address value {0} is outside the valid assignable range of {min} to {max}",
399 min = Address::MIN,
400 max = Address::MAX
401)]
402pub struct ErrorAddressOutOfRange(
403 /// The invalid address value that caused the error.
404 pub u8,
405);
406
407impl TryFrom<u8> for Address {
408 type Error = ErrorAddressOutOfRange;
409
410 /// Attempts to create an [`Address`] from a `u8` value, validating its assignable range [[`Address::MIN`], [`Address::MAX`]].
411 ///
412 /// # Arguments
413 ///
414 /// * `value`: The Modbus address to validate.
415 ///
416 /// # Returns
417 ///
418 /// * `Ok(Address)`: If the `value` is within the valid assignable range [[`Address::MIN`], [`Address::MAX`]].
419 /// * `Err(ErrorAddressOutOfRange)`: If the `value` is outside the valid assignable range (e.g., 0 or > 247).
420 ///
421 /// # Example
422 /// ```
423 /// # use r413d08_lib::protocol::{Address, ErrorAddressOutOfRange};
424 /// assert!(Address::try_from(0).is_err());
425 /// assert!(Address::try_from(1).is_ok());
426 /// assert!(Address::try_from(247).is_ok());
427 /// assert_eq!(Address::try_from(248).unwrap_err(), ErrorAddressOutOfRange(248));
428 /// assert!(Address::try_from(255).is_err()); // Broadcast address is not valid for TryFrom
429 /// ```
430 fn try_from(value: u8) -> Result<Self, Self::Error> {
431 if (Self::MIN..=Self::MAX).contains(&value) {
432 Ok(Self(value))
433 } else {
434 Err(ErrorAddressOutOfRange(value))
435 }
436 }
437}
438
439/// Provides a hexadecimal string representation (e.g., "0x01", "0xf7").
440impl std::fmt::Display for Address {
441 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
442 // Format as 2-digit hexadecimal with "0x" prefix.
443 write!(f, "0x{:02x}", self.0)
444 }
445}
446
447#[cfg(test)]
448mod tests {
449 use super::*;
450
451 // --- Address Tests ---
452 #[test]
453 fn address_try_from_validation() {
454 assert!(matches!(
455 Address::try_from(0),
456 Err(ErrorAddressOutOfRange(0))
457 ));
458 assert!(matches!(Address::try_from(Address::MIN), Ok(Address(1))));
459 assert!(matches!(Address::try_from(Address::MAX), Ok(Address(247))));
460 assert!(matches!(
461 Address::try_from(Address::MAX + 1),
462 Err(ErrorAddressOutOfRange(248))
463 ));
464 assert!(matches!(
465 Address::try_from(255),
466 Err(ErrorAddressOutOfRange(255))
467 ));
468 assert!(matches!(Address::try_from(100), Ok(Address(100))));
469 }
470
471 #[test]
472 fn address_default() {
473 assert_eq!(Address::default(), Address(1));
474 }
475
476 #[test]
477 fn address_encode_decode() {
478 let addr = Address::try_from(42).unwrap();
479 let encoded = addr.encode_for_write_register();
480 assert_eq!(encoded, 42u16);
481 // Test decode (assumes valid input from register)
482 let decoded = Address::decode_from_holding_registers(&[encoded]);
483 assert_eq!(decoded, addr);
484 }
485
486 #[test]
487 #[should_panic(expected = "Register data for address must not be empty")]
488 fn address_decode_panics_on_empty() {
489 let _ = Address::decode_from_holding_registers(&[]);
490 }
491
492 #[test]
493 #[should_panic(expected = "Invalid address value read from device register")]
494 fn address_decode_panics_on_invalid_value_zero() {
495 // 0 is outside the valid 1-247 range
496 let _ = Address::decode_from_holding_registers(&[0x0000]);
497 }
498
499 #[test]
500 #[should_panic(expected = "Invalid address value read from device register")]
501 fn address_decode_panics_on_invalid_value_high() {
502 // 248 is outside the valid 1-247 range
503 let _ = Address::decode_from_holding_registers(&[0x00F8]);
504 }
505
506 #[test]
507 fn address_decode_valid() {
508 assert_eq!(
509 Address::decode_from_holding_registers(&[0x0001]),
510 Address(1)
511 );
512 assert_eq!(
513 Address::decode_from_holding_registers(&[0x00F7]),
514 Address(247)
515 );
516 }
517
518 // --- Port Tests ---
519 #[test]
520 fn port_try_from_validation() {
521 assert!(matches!(Port::try_from(Port::MIN), Ok(Port(0))));
522 assert!(matches!(Port::try_from(Port::MAX), Ok(Port(7))));
523 assert!(matches!(
524 Port::try_from(Port::MAX + 1),
525 Err(ErrorPortOutOfRange(8))
526 ));
527 assert!(matches!(Port::try_from(3), Ok(Port(3))));
528 }
529
530 #[test]
531 fn port_address_for_write_register_is_one_based() {
532 // Check if address is 1-based according to documentation
533 assert_eq!(Port::try_from(0).unwrap().address_for_write_register(), 1); // Port 0 -> Address 1
534 assert_eq!(Port::try_from(1).unwrap().address_for_write_register(), 2); // Port 1 -> Address 2
535 assert_eq!(Port::try_from(7).unwrap().address_for_write_register(), 8); // Port 7 -> Address 8
536 }
537
538 #[test]
539 fn port_encode_delay() {
540 assert_eq!(Port::encode_delay_for_write_register(0), 0x0600);
541 assert_eq!(Port::encode_delay_for_write_register(10), 0x060A);
542 assert_eq!(Port::encode_delay_for_write_register(255), 0x06FF);
543 }
544
545 // --- PortState / PortStates Tests ---
546 #[test]
547 fn port_state_decode() {
548 assert_eq!(
549 PortState::decode_from_holding_registers(0x0000),
550 PortState::Close
551 );
552 assert_eq!(
553 PortState::decode_from_holding_registers(0x0001),
554 PortState::Open
555 );
556 assert_eq!(
557 PortState::decode_from_holding_registers(0xFFFF),
558 PortState::Open
559 ); // Non-zero
560 }
561
562 #[test]
563 fn port_states_decode() {
564 let words_all_closed = [0x0000; NUMBER_OF_PORTS];
565 let words_mixed = [
566 0x0001, 0x0000, 0xFFFF, 0x0000, 0x0001, 0x0000, 0x0001, 0x0000,
567 ];
568 let words_short = [0x0001, 0x0000];
569 let words_long = [
570 0x0001, 0x0000, 0x0001, 0x0000, 0x0001, 0x0000, 0x0001, 0x0000, 0x9999,
571 ];
572
573 let expected_all_closed = PortStates([PortState::Close; NUMBER_OF_PORTS]);
574 let expected_mixed = PortStates([
575 PortState::Open,
576 PortState::Close,
577 PortState::Open,
578 PortState::Close,
579 PortState::Open,
580 PortState::Close,
581 PortState::Open,
582 PortState::Close,
583 ]);
584
585 let mut expected_short_arr = [PortState::Close; NUMBER_OF_PORTS];
586 expected_short_arr[0] = PortState::Open;
587 expected_short_arr[1] = PortState::Close;
588 let expected_short = PortStates(expected_short_arr);
589
590 assert_eq!(
591 PortStates::decode_from_holding_registers(&words_all_closed),
592 expected_all_closed
593 );
594 assert_eq!(
595 PortStates::decode_from_holding_registers(&words_mixed),
596 expected_mixed
597 );
598 assert_eq!(
599 PortStates::decode_from_holding_registers(&words_short),
600 expected_short
601 );
602 assert_eq!(
603 PortStates::decode_from_holding_registers(&words_long),
604 expected_mixed
605 ); // Ignores extra
606 }
607
608 // --- Display Tests ---
609 #[test]
610 fn display_formats() {
611 assert_eq!(PortState::Open.to_string(), "open");
612 assert_eq!(PortState::Close.to_string(), "close");
613 assert_eq!(Address(1).to_string(), "0x01");
614 assert_eq!(Address(247).to_string(), "0xf7");
615 assert_eq!(Address::BROADCAST.to_string(), "0xff"); // Direct creation
616 let states = PortStates([
617 PortState::Open,
618 PortState::Close,
619 PortState::Open,
620 PortState::Close,
621 PortState::Close,
622 PortState::Close,
623 PortState::Close,
624 PortState::Close,
625 ]);
626 assert_eq!(
627 states.to_string(),
628 "open, close, open, close, close, close, close, close"
629 );
630 }
631}