r4dcb08_lib/
protocol.rs

1//! Defines data structures, constants, and protocol logic for interacting
2//! with an 8-Channel temperature module using the Modbus RTU protocol, based on
3//! the protocol specification document.
4//!
5//! Assumes standard Modbus function codes "Read Holding Registers" (0x03) and
6//! "Write Single Register" (0x06) are used externally to interact with the device.
7
8use std::time::Duration;
9use thiserror::Error;
10
11/// A comprehensive error type for all operations within the `protocol` module.
12///
13/// This enum consolidates errors that can occur during the decoding
14/// data, encoding of values, or validation of parameters.
15#[derive(Error, Debug, PartialEq)]
16pub enum Error {
17    /// Error indicating that the data received from a Modbus read has an unexpected length.
18    #[error("Invalid data length: expected {expected}, got {got}")]
19    UnexpectedDataLength { expected: usize, got: usize },
20
21    /// Error for malformed data within a register, e.g., an unexpected non-zero upper byte.
22    #[error("Invalid data in register: {details}")]
23    InvalidData { details: String },
24
25    /// Error for an invalid value read from a register, e.g., an undefined baud rate code.
26    #[error("Invalid value code for {entity}: {code}")]
27    InvalidValueCode { entity: String, code: u16 },
28
29    /// Error for an attempt to encode a value that is not supported, e.g., a `NaN` temperature.
30    #[error("Cannot encode value: {reason}")]
31    EncodeError { reason: String },
32}
33
34/// Represents a single 16-bit value stored in a Modbus register.
35///
36/// Modbus RTU typically operates on 16-bit registers.
37pub type Word = u16;
38
39/// Number of temperature channels available in the R4DCB08 module.
40pub const NUMBER_OF_CHANNELS: usize = 8;
41
42/// Enum representing the supported baud rates for RS485 communication.
43#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
44#[cfg_attr(feature = "serde", derive(serde::Serialize))]
45#[repr(u16)]
46pub enum BaudRate {
47    /// 1200 baud rate.
48    B1200 = 1200, // Corresponds to register value 0.
49    /// 2400 baud rate.
50    B2400 = 2400, // Corresponds to register value 1.
51    /// 4800 baud rate.
52    B4800 = 4800, // Corresponds to register value 2.
53    /// 9600 baud rate.
54    #[default]
55    B9600 = 9600, // Factory default rate. Corresponds to register value 3.
56    /// 19200 baud rate.
57    B19200 = 19200, // Corresponds to register value 4.
58}
59
60#[cfg(feature = "serde")]
61impl<'de> serde::Deserialize<'de> for BaudRate {
62    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
63    where
64        D: serde::Deserializer<'de>,
65    {
66        let value = u16::deserialize(deserializer)?;
67        BaudRate::try_from(value).map_err(|e| serde::de::Error::custom(e.to_string()))
68    }
69}
70
71impl BaudRate {
72    /// Modbus register address for reading/writing the baud rate.
73    pub const ADDRESS: u16 = 0x00FF;
74    /// Number of Modbus registers holding the baud rate value.
75    pub const QUANTITY: u16 = 1;
76
77    /// Decodes a `BaudRate` from a Modbus holding register value.
78    ///
79    /// Reads the first word from the provided slice, which should contain the
80    /// raw value read from the device's baud rate register (0x00FF).
81    ///
82    /// # Arguments
83    ///
84    /// * `words` - A slice containing the `Word`(s) read from the register.
85    ///
86    /// # Returns
87    ///
88    /// The corresponding `BaudRate` or an `Error` if decoding fails.
89    ///
90    /// # Errors
91    ///
92    /// * [`Error::UnexpectedDataLength`]: if `words` does not contain exactly one element.
93    /// * [`Error::InvalidValueCode`]: if the value from the register is not a valid baud rate code.
94    pub fn decode_from_holding_registers(words: &[Word]) -> Result<Self, Error> {
95        if words.len() != Self::QUANTITY as usize {
96            return Err(Error::UnexpectedDataLength {
97                expected: Self::QUANTITY as usize,
98                got: words.len(),
99            });
100        }
101        match words[0] {
102            0 => Ok(Self::B1200),
103            1 => Ok(Self::B2400),
104            2 => Ok(Self::B4800),
105            3 => Ok(Self::B9600),
106            4 => Ok(Self::B19200),
107            invalid_code => Err(Error::InvalidValueCode {
108                entity: "BaudRate".to_string(),
109                code: invalid_code,
110            }),
111        }
112    }
113
114    /// Encodes the `BaudRate` into its corresponding `Word` value for writing to the Modbus register.
115    ///
116    /// # Returns
117    ///
118    /// The `Word` representation of the baud rate.
119    pub fn encode_for_write_register(&self) -> Word {
120        match self {
121            Self::B1200 => 0,
122            Self::B2400 => 1,
123            Self::B4800 => 2,
124            Self::B9600 => 3,
125            Self::B19200 => 4,
126        }
127    }
128}
129
130/// Error returned when attempting to create a `BaudRate` from an unsupported `u16` value.
131#[derive(Error, Debug, PartialEq, Eq)]
132#[error("Unsupported baud rate value: {0}. Must be 1200, 2400, 4800, 9600, or 19200.")]
133pub struct ErrorInvalidBaudRate(
134    /// The invalid baud rate value that caused the error.
135    pub u16,
136);
137
138impl TryFrom<u16> for BaudRate {
139    type Error = ErrorInvalidBaudRate;
140
141    /// Attempts to convert a standard `u16` baud rate value (e.g., 9600) into a `BaudRate` enum variant.
142    /// Returns an error if the value does not match a supported baud rate.
143    fn try_from(value: u16) -> Result<Self, Self::Error> {
144        match value {
145            1200 => Ok(BaudRate::B1200),
146            2400 => Ok(BaudRate::B2400),
147            4800 => Ok(BaudRate::B4800),
148            9600 => Ok(BaudRate::B9600),
149            19200 => Ok(BaudRate::B19200),
150            _ => Err(ErrorInvalidBaudRate(value)),
151        }
152    }
153}
154
155impl From<BaudRate> for u16 {
156    /// Converts a `BaudRate` variant into its standard `u16` representation (e.g., `BaudRate::B9600` becomes `9600`).
157    fn from(baud_rate: BaudRate) -> u16 {
158        match baud_rate {
159            BaudRate::B1200 => 1200,
160            BaudRate::B2400 => 2400,
161            BaudRate::B4800 => 4800,
162            BaudRate::B9600 => 9600,
163            BaudRate::B19200 => 19200,
164        }
165    }
166}
167
168impl From<BaudRate> for u32 {
169    fn from(baud_rate: BaudRate) -> u32 {
170        baud_rate as u16 as u32
171    }
172}
173
174impl std::fmt::Display for BaudRate {
175    /// Formats the `BaudRate` as its numeric value (e.g., "9600").
176    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
177        write!(f, "{}", *self as u16)
178    }
179}
180
181/// Represents a validated Modbus device address, used for RTU communication over RS485.
182///
183/// Valid addresses are in the range 1 to 247 (inclusive).
184/// Use [`Address::try_from`] to create an instance from a `u8`.
185/// Provides constants and methods for reading/writing the device address itself via Modbus commands.
186#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
187#[cfg_attr(feature = "serde", derive(serde::Serialize))]
188pub struct Address(u8);
189
190#[cfg(feature = "serde")]
191impl<'de> serde::Deserialize<'de> for Address {
192    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
193    where
194        D: serde::Deserializer<'de>,
195    {
196        let value = u8::deserialize(deserializer)?;
197        Address::try_from(value).map_err(|e| serde::de::Error::custom(e.to_string()))
198    }
199}
200
201/// Allows direct access to the underlying `u8` address value.
202impl std::ops::Deref for Address {
203    type Target = u8;
204    fn deref(&self) -> &Self::Target {
205        &self.0
206    }
207}
208
209impl Default for Address {
210    /// Returns the factory default Modbus address.
211    fn default() -> Self {
212        // Safe because `0x01` is within the valid range 1-247.
213        Self(0x01)
214    }
215}
216
217impl Address {
218    /// The Modbus register address used to read or write the device's own Modbus address.
219    ///
220    /// Use Modbus function 0x03 (Read Holding Registers) to read (requires addressing
221    /// the device with its *current* address or the broadcast address).
222    /// Use function 0x06 (Write Single Register) to change the address (also requires
223    /// addressing the device with its current address).
224    pub const ADDRESS: u16 = 0x00FE;
225
226    /// The number of registers to read when reading the device address.
227    pub const QUANTITY: u16 = 1; // The protocol specification indicates 2 bytes but the documentation accurately reflects that it expects a slice of length 1
228
229    /// The minimum valid assignable Modbus device address (inclusive).
230    pub const MIN: u8 = 1;
231    /// The maximum valid assignable Modbus device address (inclusive).
232    pub const MAX: u8 = 247;
233
234    /// The Modbus broadcast address (`0xFF` or 255).
235    ///
236    /// Can be used for reading the device address when it's unknown.
237    /// This address cannot be assigned to a device as its permanent address.
238    pub const BROADCAST: Address = Address(0xFF);
239
240    /// Decodes the device [`Address`] from a Modbus holding register value read from the device.
241    ///
242    /// Expects `words` to contain the single register value read from the device address
243    /// configuration register ([`Address::ADDRESS`]).
244    /// It validates that the decoded address is within the assignable range ([`Address::MIN`]..=[`Address::MAX`]).
245    ///
246    /// # Arguments
247    ///
248    /// * `words`: A slice containing the [`Word`] value read from the device address register. Expected to have length 1.
249    ///
250    /// # Returns
251    ///
252    /// An [`Address`] struct containing the decoded and validated value, or an `Error` if decoding fails.
253    ///
254    /// # Errors
255    ///
256    /// * [`Error::UnexpectedDataLength`]: if `words` does not contain exactly one element.
257    /// * [`Error::InvalidData`]: if the upper byte of the register value is non-zero.
258    pub fn decode_from_holding_registers(words: &[Word]) -> Result<Self, Error> {
259        if words.len() != Self::QUANTITY as usize {
260            return Err(Error::UnexpectedDataLength {
261                expected: Self::QUANTITY as usize,
262                got: words.len(),
263            });
264        }
265        let word_value = words[0];
266
267        if word_value & 0xFF00 != 0 {
268            return Err(Error::InvalidData {
269                details: format!(
270                    "Upper byte of address register is non-zero (value: {word_value:#06X})"
271                ),
272            });
273        }
274
275        let address_byte = word_value as u8;
276        match Self::try_from(address_byte) {
277            Ok(address) => Ok(address),
278            Err(err) => Err(Error::InvalidData {
279                details: format!("{err}"),
280            }),
281        }
282    }
283
284    /// Encodes the `Address` into its `Word` representation for writing to the Modbus register.
285    ///
286    /// # Returns
287    ///
288    /// The `Word` representation of the address.
289    pub fn encode_for_write_register(&self) -> Word {
290        self.0 as Word
291    }
292}
293
294/// Error indicating that a provided Modbus device address (`u8`) is outside the valid range
295/// for assignable addresses, defined by [`Address::MIN`] and [`Address::MAX`] (inclusive).
296#[derive(Error, Debug, Clone, Copy, PartialEq, Eq)]
297#[error(
298    "The address value {0} is outside the valid assignable range of {min} to {max}",
299    min = Address::MIN,
300    max = Address::MAX
301)]
302pub struct ErrorAddressOutOfRange(
303    /// The invalid address value that caused the error.
304    pub u8,
305);
306
307impl TryFrom<u8> for Address {
308    type Error = ErrorAddressOutOfRange;
309
310    /// Attempts to create an [`Address`] from a `u8` value, validating its assignable range [[`Address::MIN`], [`Address::MAX`]].
311    ///
312    /// # Arguments
313    ///
314    /// * `value`: The Modbus address to validate.
315    ///
316    /// # Returns
317    ///
318    /// * `Ok(Address)`: If the `value` is within the valid assignable range [[`Address::MIN`], [`Address::MAX`]].
319    /// * `Err(ErrorAddressOutOfRange)`: If the `value` is outside the valid assignable range (e.g., 0 or > 247).
320    ///
321    /// # Example
322    /// ```
323    /// # use r4dcb08_lib::protocol::{Address, ErrorAddressOutOfRange};
324    /// assert!(matches!(Address::try_from(0), Err(ErrorAddressOutOfRange(_))));
325    /// assert!(Address::try_from(1).is_ok());
326    /// assert!(Address::try_from(247).is_ok());
327    /// assert_eq!(Address::try_from(248).unwrap_err(), ErrorAddressOutOfRange(248));
328    /// assert!(matches!(Address::try_from(255), Err(ErrorAddressOutOfRange(_)))); // Broadcast address is not valid for TryFrom
329    /// ```
330    fn try_from(value: u8) -> Result<Self, Self::Error> {
331        if (Self::MIN..=Self::MAX).contains(&value) {
332            Ok(Self(value))
333        } else {
334            Err(ErrorAddressOutOfRange(value))
335        }
336    }
337}
338
339/// Provides a hexadecimal string representation (e.g., "0x01", "0xf7").
340impl std::fmt::Display for Address {
341    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
342        write!(f, "{:#04x}", self.0)
343    }
344}
345
346/// Represents a temperature value in degrees Celsius, as read from or written to the R4DCB08.
347///
348/// Valid addresses are in the range -3276.8 to 3276.7 (inclusive).
349/// Use [`Temperature::try_from`] to create an instance from a `f32`.
350#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
351#[cfg_attr(feature = "serde", derive(serde::Serialize))]
352pub struct Temperature(f32);
353
354#[cfg(feature = "serde")]
355impl<'de> serde::Deserialize<'de> for Temperature {
356    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
357    where
358        D: serde::Deserializer<'de>,
359    {
360        let value = f32::deserialize(deserializer)?;
361        // Allow NAN during deserialization, but TryFrom will catch out-of-range non-NAN values.
362        if value.is_nan() {
363            Ok(Temperature::NAN)
364        } else {
365            Temperature::try_from(value).map_err(|e| serde::de::Error::custom(e.to_string()))
366        }
367    }
368}
369
370impl Temperature {
371    /// Represents a Not-a-Number temperature, often indicating a sensor error or disconnection.
372    pub const NAN: Temperature = Self(f32::NAN); // Corresponds to the register value 0x8000 (32768)
373    /// Minimum measurable/representable temperature value (-3276.7 °C).
374    pub const MIN: f32 = -3276.7; // Derived from the most negative signed 16-bit integer divided by 10.
375    /// Maximum measurable/representable temperature value (+3276.7 °C).
376    pub const MAX: f32 = 3276.7; // Derived from the most positive signed 16-bit integer divided by 10.
377
378    /// Decodes a `Temperature` from a single Modbus holding register `Word`.
379    ///
380    /// # Arguments
381    ///
382    /// * `word` - The `Word` value read from the temperature or correction register.
383    ///
384    /// # Returns
385    ///
386    /// The decoded `Temperature`. Returns `Temperature::NAN` indicating a sensor error or disconnection.
387    pub fn decode_from_holding_registers(word: Word) -> Self {
388        // Implements the decoding logic described in the protocol:
389        // - Value `0x8000` (32768) represents NAN (no sensor/error).
390        // - If the highest bit is 1 (value > 0x8000), it's negative: `(value - 65536) / 10.0`.
391        // - If the highest bit is 0 (value < 0x8000), it's positive: `value / 10.0`.
392        match word {
393            0x8000 => Self::NAN,
394            w if w > 0x8000 => Self(((w as f32) - 65536.0) / 10.0),
395            w => Self((w as f32) / 10.0),
396        }
397    }
398
399    /// Encodes the `Temperature` into a `Word` value for writing to a Modbus register.
400    ///
401    /// # Returns
402    ///
403    /// The `Word` representation of the temperature, ready for writing, or an `Error` if encoding fails.
404    ///
405    /// # Errors
406    ///
407    /// * [`Error::EncodeError`]: if the temperature value is `NAN`.
408    pub fn encode_for_write_register(&self) -> Result<Word, Error> {
409        // Implements the encoding logic derived from protocol examples:
410        // - Positive values are multiplied by 10.
411        // - Negative values are encoded using two's complement representation after multiplying by 10.
412        //   `(value * 10.0) as i16 as u16` achieves this.
413        // - NAN cannot be directly encoded; attempting to encode NAN will result in an undefined `Word` value.
414        if self.0.is_nan() {
415            return Err(Error::EncodeError {
416                reason: "Temperature value is NAN, which cannot be encoded.".to_string(),
417            });
418        }
419        if self.0 >= 0.0 {
420            Ok((self.0 * 10.0) as Word)
421        } else {
422            Ok((65536.0 + self.0 * 10.0) as Word)
423        }
424    }
425}
426
427/// Allows direct access to the underlying `f32` temperature value.
428impl std::ops::Deref for Temperature {
429    type Target = f32;
430    fn deref(&self) -> &Self::Target {
431        &self.0
432    }
433}
434
435/// Error indicating that the degree Celsius value is out of range.
436///
437/// # Arguments
438///
439/// * `0` - The degree Celsius value that caused the error.
440#[derive(Error, Debug, PartialEq)]
441#[error(
442    "The degree celsius value {0} is outside the valid range of {min} to {max}",
443    min = Temperature::MIN,
444    max = Temperature::MAX
445)]
446pub struct ErrorDegreeCelsiusOutOfRange(
447    /// The invalid degree celsius value that caused the error.
448    pub f32,
449);
450
451impl TryFrom<f32> for Temperature {
452    type Error = ErrorDegreeCelsiusOutOfRange;
453
454    /// Attempts to create a `Temperature` from an `f32` value.
455    /// Returns an error if the value is outside the representable range [`MIN`, `MAX`].
456    /// Note: This does **not** allow creating `Temperature::NAN` via `try_from`.
457    /// Use `Temperature::NAN` constant directly.
458    fn try_from(value: f32) -> Result<Self, Self::Error> {
459        if value.is_nan() {
460            // Explicitly disallow creating NAN via try_from, require using the constant.
461            Err(ErrorDegreeCelsiusOutOfRange(value))
462        } else if !(Self::MIN..=Self::MAX).contains(&value) {
463            Err(ErrorDegreeCelsiusOutOfRange(value))
464        } else {
465            Ok(Self(value))
466        }
467    }
468}
469
470impl std::fmt::Display for Temperature {
471    /// Formats the `Temperature` to one decimal place (e.g., "21.9", "-3.0", "NAN").
472    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
473        if self.0.is_nan() {
474            write!(f, "NAN")
475        } else {
476            write!(f, "{:.1}", self.0)
477        }
478    }
479}
480
481/// Represents the temperature readings for all 8 channels.
482#[derive(Debug, Clone, Copy, PartialEq)]
483#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
484pub struct Temperatures([Temperature; NUMBER_OF_CHANNELS]);
485
486impl Temperatures {
487    /// Register address for reading and writing the temperatures.
488    pub const ADDRESS: u16 = 0x0000;
489    /// Number of Modbus registers required to read all channel temperatures.
490    pub const QUANTITY: u16 = NUMBER_OF_CHANNELS as u16;
491
492    /// Decodes `Temperatures` for all channels from a slice of Modbus holding register values.
493    ///
494    /// Expects a slice containing `NUMBER_OF_CHANNELS` words.
495    ///
496    /// # Arguments
497    ///
498    /// * `words` - A slice of `Word` containing the register values for all channels.
499    ///
500    /// # Returns
501    ///
502    /// A `Temperatures` struct containing the decoded value for each channel.
503    ///
504    /// # Errors
505    ///
506    /// * [`Error::UnexpectedDataLength`]: if `words` does not have length equal to [`NUMBER_OF_CHANNELS`].
507    pub fn decode_from_holding_registers(words: &[Word]) -> Result<Self, Error> {
508        if words.len() != NUMBER_OF_CHANNELS {
509            return Err(Error::UnexpectedDataLength {
510                expected: NUMBER_OF_CHANNELS,
511                got: words.len(),
512            });
513        }
514        let mut temperatures = [Temperature::NAN; NUMBER_OF_CHANNELS];
515        for (i, word) in words.iter().enumerate() {
516            temperatures[i] = Temperature::decode_from_holding_registers(*word);
517        }
518        Ok(Self(temperatures))
519    }
520
521    /// Returns an iterator over the individual `Temperature` values.
522    pub fn iter(&self) -> std::slice::Iter<'_, Temperature> {
523        self.0.iter()
524    }
525
526    /// Returns a slice containing all `Temperature` values.
527    pub fn as_slice(&self) -> &[Temperature] {
528        &self.0
529    }
530
531    /// Provides direct access to the underlying array of port states.
532    pub fn as_array(&self) -> &[Temperature; NUMBER_OF_CHANNELS] {
533        &self.0
534    }
535}
536
537impl IntoIterator for Temperatures {
538    type Item = Temperature;
539    type IntoIter = std::array::IntoIter<Temperature, NUMBER_OF_CHANNELS>;
540
541    fn into_iter(self) -> Self::IntoIter {
542        self.0.into_iter()
543    }
544}
545
546impl<'a> IntoIterator for &'a Temperatures {
547    type Item = &'a Temperature;
548    type IntoIter = std::slice::Iter<'a, Temperature>;
549
550    fn into_iter(self) -> Self::IntoIter {
551        self.0.iter()
552    }
553}
554
555impl std::fmt::Display for Temperatures {
556    /// Formats the temperatures as a comma-separated string (e.g., "21.9, -11.2, ...").
557    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
558        let temp_strs: Vec<String> = self.0.iter().map(|t| t.to_string()).collect();
559        write!(f, "{}", temp_strs.join(", "))
560    }
561}
562
563impl std::ops::Index<usize> for Temperatures {
564    type Output = Temperature;
565
566    /// Allows indexing into the temperatures array (e.g., `temps[0]` for CH1).
567    ///
568    /// # Panics
569    ///
570    /// Panics if the index is out of bounds (0-7).
571    fn index(&self, index: usize) -> &Self::Output {
572        &self.0[index]
573    }
574}
575
576/// Represents a validated temperature channel index, guaranteed to be within the valid range `0..NUMBER_OF_CHANNELS`.
577///
578/// Use [`Channel::try_from`] to create an instance from a `u8`.
579#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
580#[cfg_attr(feature = "serde", derive(serde::Serialize))]
581pub struct Channel(u8);
582
583#[cfg(feature = "serde")]
584impl<'de> serde::Deserialize<'de> for Channel {
585    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
586    where
587        D: serde::Deserializer<'de>,
588    {
589        let value = u8::deserialize(deserializer)?;
590        Channel::try_from(value).map_err(|e| serde::de::Error::custom(e.to_string()))
591    }
592}
593
594impl Channel {
595    /// The minimum valid channel index (inclusive).
596    pub const MIN: u8 = 0;
597    /// The maximum valid channel index (inclusive).
598    pub const MAX: u8 = NUMBER_OF_CHANNELS as u8 - 1;
599}
600
601/// Allows direct access to the underlying `u8` channel number.
602impl std::ops::Deref for Channel {
603    type Target = u8;
604    fn deref(&self) -> &Self::Target {
605        &self.0
606    }
607}
608
609/// Error indicating that the channel value is out of range.
610///
611/// # Arguments
612///
613/// * `0` - The channel value that caused the error.
614#[derive(Error, Debug, PartialEq, Eq)]
615#[error(
616    "The channel value {0} is outside the valid range of {min} to {max}",
617    min = Channel::MIN,
618    max = Channel::MAX
619)]
620pub struct ErrorChannelOutOfRange(
621    /// The invalid channel value that caused the error.
622    pub u8,
623);
624
625impl TryFrom<u8> for Channel {
626    type Error = ErrorChannelOutOfRange;
627
628    /// Attempts to create a `Channel` from a `u8` value.
629    /// Returns an error if the value is outside the valid range [0, 7].
630    fn try_from(value: u8) -> Result<Self, Self::Error> {
631        if (Self::MIN..=Self::MAX).contains(&value) {
632            Ok(Self(value))
633        } else {
634            Err(ErrorChannelOutOfRange(value))
635        }
636    }
637}
638
639impl std::fmt::Display for Channel {
640    /// Formats the channel as its number (e.g., "0", "7").
641    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
642        write!(f, "{}", self.0)
643    }
644}
645
646/// Represents the temperature correction values for all 8 channels.
647/// Correction values are added to the raw temperature reading.
648/// Setting a correction value to 0 disables correction for that channel.
649#[derive(Debug, Clone, Copy, PartialEq)]
650#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
651pub struct TemperatureCorrection([Temperature; NUMBER_OF_CHANNELS]);
652
653impl TemperatureCorrection {
654    /// Starting Modbus register address for reading/writing correction values.
655    pub const ADDRESS: u16 = 0x0008;
656    /// Number of Modbus registers required for all channel correction values.
657    pub const QUANTITY: u16 = NUMBER_OF_CHANNELS as u16;
658
659    /// Decodes `TemperatureCorrection` values for all channels from a slice of Modbus holding register values.
660    ///
661    /// Expects a slice containing [`NUMBER_OF_CHANNELS`] words.
662    ///
663    /// # Arguments
664    ///
665    /// * `words` - A slice of `Word` containing the register values for all correction channels.
666    ///
667    /// # Returns
668    ///
669    /// A `TemperatureCorrection` struct containing the decoded value.
670    ///
671    /// # Errors
672    ///
673    /// * [`Error::UnexpectedDataLength`]: if `words` does not have length equal to [`NUMBER_OF_CHANNELS`].
674    pub fn decode_from_holding_registers(words: &[Word]) -> Result<Self, Error> {
675        if words.len() != NUMBER_OF_CHANNELS {
676            return Err(Error::UnexpectedDataLength {
677                expected: NUMBER_OF_CHANNELS,
678                got: words.len(),
679            });
680        }
681        let mut corrections = [Temperature::NAN; NUMBER_OF_CHANNELS];
682        for (i, word) in words.iter().enumerate() {
683            // Decoding is the same as Temperature
684            corrections[i] = Temperature::decode_from_holding_registers(*word);
685        }
686        Ok(Self(corrections))
687    }
688
689    /// Returns an iterator over the individual `Temperature` correction values.
690    pub fn iter(&self) -> std::slice::Iter<'_, Temperature> {
691        self.0.iter()
692    }
693
694    /// Returns a slice containing all `Temperature` correction values.
695    pub fn as_slice(&self) -> &[Temperature] {
696        &self.0
697    }
698
699    /// Provides direct access to the underlying array of port states.
700    pub fn as_array(&self) -> &[Temperature; NUMBER_OF_CHANNELS] {
701        &self.0
702    }
703
704    /// Encodes a single `Temperature` correction value into a `Word` for writing to the appropriate channel register.
705    ///
706    /// Use `channel_address` to determine the correct register address for writing.
707    ///
708    /// # Arguments
709    ///
710    /// * `correction_value` - The `Temperature` correction value to encode.
711    ///
712    /// # Returns
713    ///
714    /// The `Word` representation of the correction value.
715    ///
716    /// # Errors
717    ///
718    /// Returns an [`Error::EncodeError`] if the `correction_value` is `NAN`.
719    pub fn encode_for_write_register(correction_value: Temperature) -> Result<Word, Error> {
720        correction_value.encode_for_write_register()
721    }
722
723    /// Calculates the Modbus register address for a specific channel's correction value.
724    ///
725    /// # Arguments
726    ///
727    /// * `channel` - The `Channel` for which to get the correction register address.
728    ///
729    /// # Returns
730    ///
731    /// The `u16` Modbus register address.
732    pub fn channel_address(channel: Channel) -> u16 {
733        Self::ADDRESS + (*channel as u16)
734    }
735}
736
737impl IntoIterator for TemperatureCorrection {
738    type Item = Temperature;
739    type IntoIter = std::array::IntoIter<Temperature, NUMBER_OF_CHANNELS>;
740
741    fn into_iter(self) -> Self::IntoIter {
742        self.0.into_iter()
743    }
744}
745
746impl<'a> IntoIterator for &'a TemperatureCorrection {
747    type Item = &'a Temperature;
748    type IntoIter = std::slice::Iter<'a, Temperature>;
749
750    fn into_iter(self) -> Self::IntoIter {
751        self.0.iter()
752    }
753}
754
755impl std::fmt::Display for TemperatureCorrection {
756    /// Formats the correction values as a comma-separated string.
757    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
758        let corr_strs: Vec<String> = self.0.iter().map(|t| t.to_string()).collect();
759        write!(f, "{}", corr_strs.join(", "))
760    }
761}
762
763impl std::ops::Index<usize> for TemperatureCorrection {
764    type Output = Temperature;
765    /// Allows indexing into the corrections array (e.g., `corrections[0]` for CH1).
766    ///
767    /// # Panics
768    ///
769    /// Panics if the index is out of bounds (0-7).
770    fn index(&self, index: usize) -> &Self::Output {
771        &self.0[index]
772    }
773}
774
775/// Represents the automatic temperature reporting interval in seconds.
776/// A value of 0 disables automatic reporting (query mode).
777/// Values 1-255 set the reporting interval in seconds.
778#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
779#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
780pub struct AutomaticReport(u8);
781
782impl AutomaticReport {
783    /// Modbus register address for reading/writing the automatic report interval.
784    pub const ADDRESS: u16 = 0x00FD;
785    /// The number of registers to read when reading the interval value.
786    pub const QUANTITY: u16 = 1;
787
788    /// Minimum valid interval duration (0 seconds = disabled).
789    pub const DURATION_MIN: u8 = 0;
790    /// Maximum valid interval duration (255 seconds).
791    pub const DURATION_MAX: u8 = 255;
792
793    /// Disables automatic reporting. Equivalent to `AutomaticReport(0)`.
794    pub const DISABLED: AutomaticReport = AutomaticReport(0);
795
796    /// Decodes an `AutomaticReport` interval from Modbus holding register data.
797    ///
798    /// # Arguments
799    ///
800    /// * `words` - A slice of `Word` containing the register value.
801    ///
802    /// # Returns
803    ///
804    /// The decoded `AutomaticReport` struct or an `Error` if decoding fails.
805    ///
806    /// # Errors
807    ///
808    /// * [`Error::UnexpectedDataLength`]: if `words` does not contain exactly one element.
809    /// * [`Error::InvalidData`]: if the upper byte of the register value is non-zero.
810    pub fn decode_from_holding_registers(words: &[Word]) -> Result<Self, Error> {
811        if words.len() != Self::QUANTITY as usize {
812            return Err(Error::UnexpectedDataLength {
813                expected: Self::QUANTITY as usize,
814                got: words.len(),
815            });
816        }
817        let word_value = words[0];
818
819        if word_value & 0xFF00 != 0 {
820            return Err(Error::InvalidData {
821                details: format!(
822                    "Upper byte of automatic report register is non-zero (value: {word_value:#06X})"
823                ),
824            });
825        }
826
827        // not  use Self::try_from() - conversion cannot fail
828        Ok(Self::from(word_value as u8))
829    }
830
831    /// Encodes the `AutomaticReport` interval into a `Word` value for writing to the Modbus register.
832    ///
833    /// # Returns
834    ///
835    /// The `Word` representation (0-255) of the interval.
836    pub fn encode_for_write_register(&self) -> Word {
837        self.0 as Word
838    }
839
840    /// Returns the interval duration in seconds. 0 means disabled.
841    pub fn as_secs(&self) -> u8 {
842        self.0
843    }
844
845    /// Returns the interval as a `std::time::Duration`.
846    /// Returns `Duration::ZERO` if the interval is 0 (disabled).
847    pub fn as_duration(&self) -> Duration {
848        Duration::from_secs(self.0 as u64)
849    }
850
851    /// Checks if automatic reporting is disabled (interval is 0).
852    pub fn is_disabled(&self) -> bool {
853        self.0 == Self::DURATION_MIN
854    }
855}
856
857impl std::ops::Deref for AutomaticReport {
858    type Target = u8;
859    /// Allows direct access to the underlying `u8` interval value (seconds).
860    fn deref(&self) -> &Self::Target {
861        &self.0
862    }
863}
864
865impl From<u8> for AutomaticReport {
866    /// Creates an `AutomaticReport` from a `u8` value (seconds).
867    /// Assumes the value is within the valid range [0, 255].
868    /// Consider using `try_from(Duration)` for validation.
869    fn from(interval_seconds: u8) -> AutomaticReport {
870        // No bounds check here, relies on caller providing valid value or using TryFrom
871        Self(interval_seconds)
872    }
873}
874
875/// Error indicating that a `Duration` cannot be represented as an `AutomaticReport` interval (0-255 seconds).
876#[derive(Error, Debug, PartialEq, Eq)]
877#[error(
878    "The duration {0} seconds is outside the valid automatic report range [{min}, {max}] seconds",
879    min = AutomaticReport::DURATION_MIN,
880    max = AutomaticReport::DURATION_MAX
881)]
882pub struct ErrorDurationOutOfRange(
883    /// The invalid duration value that caused the error.
884    pub u64,
885);
886
887impl TryFrom<Duration> for AutomaticReport {
888    type Error = ErrorDurationOutOfRange;
889
890    /// Attempts to create an `AutomaticReport` interval from a `std::time::Duration`.
891    /// Returns an error if the duration (in whole seconds) is outside the range [0, 255].
892    /// Fractional seconds are ignored.
893    fn try_from(value: Duration) -> Result<Self, Self::Error> {
894        let secs = value.as_secs();
895        if (Self::DURATION_MIN as u64..=Self::DURATION_MAX as u64).contains(&secs) {
896            Ok(Self(secs as u8))
897        } else {
898            Err(ErrorDurationOutOfRange(secs))
899        }
900    }
901}
902
903impl std::fmt::Display for AutomaticReport {
904    /// Formats the interval (e.g., "0s", "10s", "255s").
905    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
906        write!(f, "{}s", self.0)
907    }
908}
909
910/// Represents the factory reset command. This is a write-only operation.
911#[derive(Debug, Clone, Copy)]
912pub struct FactoryReset;
913
914impl FactoryReset {
915    /// Modbus register address used for triggering a factory reset.
916    pub const ADDRESS: u16 = 0x00FF;
917    /// The specific data value that must be written to `ADDRESS` to trigger a factory reset.
918    const DATA: u16 = 5;
919
920    /// Encodes the required `Word` value to trigger a factory reset when written
921    /// to the appropriate register (`ADDRESS`).
922    ///
923    /// # Returns
924    ///
925    /// The constant `Word` value (5).
926    pub fn encode_for_write_register() -> Word {
927        Self::DATA
928    }
929}
930
931#[cfg(test)]
932mod tests {
933    use super::*;
934    use assert_matches::assert_matches;
935
936    // --- Temperature Tests ---
937    #[test]
938    fn temperature_decode() {
939        // Positive values
940        assert_eq!(
941            Temperature::decode_from_holding_registers(219),
942            Temperature::try_from(21.9).unwrap()
943        );
944        assert_eq!(
945            Temperature::decode_from_holding_registers(100),
946            Temperature::try_from(10.0).unwrap()
947        );
948        assert_eq!(
949            Temperature::decode_from_holding_registers(0),
950            Temperature::try_from(0.0).unwrap()
951        );
952        // Negative values
953        assert_eq!(
954            Temperature::decode_from_holding_registers(65424),
955            Temperature::try_from(-11.2).unwrap()
956        );
957        assert_eq!(
958            Temperature::decode_from_holding_registers(65506),
959            Temperature::try_from(-3.0).unwrap()
960        );
961        // Boundary Negative (Smallest negative value representable)
962        assert_eq!(
963            Temperature::decode_from_holding_registers(0xFFFF),
964            Temperature::try_from(-0.1).unwrap()
965        ); // -1 -> FF F5? No, (FFFFh as i16) = -1. So -0.1
966           // Boundary Negative (Largest negative value / MIN)
967           // 65536 - 32768 = 32768 = 0x8000 (NAN)
968           // Smallest s16 is -32768 -> 0x8000. Encoded is -3276.8
969        assert_eq!(
970            Temperature::decode_from_holding_registers(0x8001),
971            Temperature::try_from(-3276.7).unwrap()
972        ); // -32767 -> 8001h
973        assert!(Temperature::decode_from_holding_registers(i16::MIN as u16).is_nan()); // -32768 -> 8000h (conflict with NAN) - Let's test the documented limit
974        assert_eq!(
975            Temperature::decode_from_holding_registers(32769),
976            Temperature::try_from(-3276.7).unwrap()
977        ); // From original tests, assuming this was intended limit instead of -3276.8
978
979        // Boundary Positive (Largest positive value / MAX)
980        assert_eq!(
981            Temperature::decode_from_holding_registers(32767),
982            Temperature::try_from(3276.7).unwrap()
983        );
984        // NAN value
985        assert!(Temperature::decode_from_holding_registers(32768).is_nan());
986        assert!(Temperature::decode_from_holding_registers(0x8000).is_nan());
987    }
988
989    #[test]
990    fn temperature_encode() {
991        // Positive
992        assert_eq!(
993            Temperature::try_from(21.9)
994                .unwrap()
995                .encode_for_write_register(),
996            Ok(219)
997        );
998        assert_eq!(
999            Temperature::try_from(10.0)
1000                .unwrap()
1001                .encode_for_write_register(),
1002            Ok(100)
1003        );
1004        assert_eq!(
1005            Temperature::try_from(0.0)
1006                .unwrap()
1007                .encode_for_write_register(),
1008            Ok(0)
1009        );
1010        // Negative
1011        assert_eq!(
1012            Temperature::try_from(-11.2)
1013                .unwrap()
1014                .encode_for_write_register(),
1015            Ok((-11.2 * 10.0) as i16 as u16)
1016        ); // 65424
1017        assert_eq!(
1018            Temperature::try_from(-3.0)
1019                .unwrap()
1020                .encode_for_write_register(),
1021            Ok((-3.0 * 10.0) as i16 as u16)
1022        ); // 65506
1023        assert_eq!(
1024            Temperature::try_from(-0.1)
1025                .unwrap()
1026                .encode_for_write_register(),
1027            Ok((-0.1 * 10.0) as i16 as u16)
1028        ); // 0xFFFF
1029
1030        // Boundaries
1031        assert_eq!(
1032            Temperature::try_from(Temperature::MAX)
1033                .unwrap()
1034                .encode_for_write_register(),
1035            Ok(32767)
1036        );
1037        assert_eq!(
1038            Temperature::try_from(Temperature::MIN)
1039                .unwrap()
1040                .encode_for_write_register(),
1041            Ok((Temperature::MIN * 10.0) as i16 as u16)
1042        ); // -32767 -> 0x8001
1043    }
1044
1045    #[test]
1046    fn temperature_encode_nan() {
1047        // NAN
1048        // Asserting specific value might be brittle, depends on desired handling.
1049        // Here we assume it encodes to 0x8000 as per our implementation note.
1050        assert_matches!(Temperature::NAN.encode_for_write_register(), Err(..));
1051    }
1052
1053    #[test]
1054    fn temperature_try_from() {
1055        // Valid
1056        assert_matches!(Temperature::try_from(21.9), Ok(t) if *t == 21.9);
1057        assert_matches!(Temperature::try_from(-11.2), Ok(t) if *t == -11.2);
1058        assert_matches!(Temperature::try_from(Temperature::MAX), Ok(t) if *t == Temperature::MAX);
1059        assert_matches!(Temperature::try_from(Temperature::MIN), Ok(t) if *t == Temperature::MIN);
1060
1061        // Invalid (Out of Range)
1062        let max_err_val = Temperature::MAX + 0.1;
1063        assert_matches!(Temperature::try_from(max_err_val), Err(ErrorDegreeCelsiusOutOfRange(value)) if value == max_err_val);
1064
1065        let min_err_val = Temperature::MIN - 0.1;
1066        assert_matches!(Temperature::try_from(min_err_val), Err(ErrorDegreeCelsiusOutOfRange(value)) if value == min_err_val);
1067
1068        assert_matches!(
1069            Temperature::try_from(f32::NAN),
1070            Err(ErrorDegreeCelsiusOutOfRange(..))
1071        );
1072    }
1073
1074    #[test]
1075    fn temperature_display() {
1076        assert_eq!(Temperature::try_from(21.9).unwrap().to_string(), "21.9");
1077        assert_eq!(Temperature::try_from(-3.0).unwrap().to_string(), "-3.0");
1078        assert_eq!(Temperature::try_from(0.0).unwrap().to_string(), "0.0");
1079        assert_eq!(Temperature::NAN.to_string(), "NAN");
1080    }
1081
1082    // --- Address Tests ---
1083    #[test]
1084    fn address_try_from() {
1085        assert_matches!(Address::try_from(0), Err(ErrorAddressOutOfRange(0)));
1086        assert_matches!(Address::try_from(Address::MIN), Ok(a) if *a == Address::MIN);
1087        assert_matches!(Address::try_from(100), Ok(a) if *a == 100);
1088        assert_matches!(Address::try_from(Address::MAX), Ok(a) if *a == Address::MAX);
1089        assert_matches!(
1090            Address::try_from(Address::MAX + 1),
1091            Err(ErrorAddressOutOfRange(248))
1092        );
1093        assert_matches!(Address::try_from(255), Err(ErrorAddressOutOfRange(255)));
1094        // Broadcast address is not valid via TryFrom
1095    }
1096
1097    #[test]
1098    fn address_decode() {
1099        assert_eq!(
1100            Address::decode_from_holding_registers(&[0x0001]).unwrap(),
1101            Address::try_from(1).unwrap()
1102        );
1103        assert_eq!(
1104            Address::decode_from_holding_registers(&[0x00F7]).unwrap(),
1105            Address::try_from(247).unwrap()
1106        );
1107        // Example from protocol returns 0x0001 for address 1
1108        assert_eq!(
1109            Address::decode_from_holding_registers(&[0x0001]).unwrap(),
1110            Address::try_from(1).unwrap()
1111        );
1112    }
1113
1114    #[test]
1115    fn address_decode_empty() {
1116        assert_matches!(Address::decode_from_holding_registers(&[]), Err(..));
1117    }
1118
1119    #[test]
1120    fn address_decode_invalid() {
1121        assert_matches!(Address::decode_from_holding_registers(&[0x0000]), Err(..));
1122        // Address 0 is invalid
1123    }
1124
1125    #[test]
1126    fn address_encode() {
1127        assert_eq!(
1128            Address::try_from(1).unwrap().encode_for_write_register(),
1129            0x0001
1130        );
1131        assert_eq!(
1132            Address::try_from(247).unwrap().encode_for_write_register(),
1133            0x00F7
1134        );
1135        assert_eq!(Address::default().encode_for_write_register(), 0x0001);
1136    }
1137
1138    #[test]
1139    fn address_display() {
1140        assert_eq!(Address::try_from(1).unwrap().to_string(), "0x01");
1141        assert_eq!(Address::try_from(25).unwrap().to_string(), "0x19");
1142        assert_eq!(Address::try_from(247).unwrap().to_string(), "0xf7");
1143    }
1144
1145    #[test]
1146    fn address_constants() {
1147        assert_eq!(*Address::BROADCAST, 0xFF);
1148    }
1149
1150    // --- BaudRate Tests ---
1151    #[test]
1152    fn baudrate_decode() {
1153        assert_matches!(
1154            BaudRate::decode_from_holding_registers(&[0]).unwrap(),
1155            BaudRate::B1200
1156        );
1157        assert_matches!(
1158            BaudRate::decode_from_holding_registers(&[1]).unwrap(),
1159            BaudRate::B2400
1160        );
1161        assert_matches!(
1162            BaudRate::decode_from_holding_registers(&[2]).unwrap(),
1163            BaudRate::B4800
1164        );
1165        assert_matches!(
1166            BaudRate::decode_from_holding_registers(&[3]).unwrap(),
1167            BaudRate::B9600
1168        );
1169        assert_matches!(
1170            BaudRate::decode_from_holding_registers(&[4]).unwrap(),
1171            BaudRate::B19200
1172        );
1173    }
1174
1175    #[test]
1176    fn baudrate_decode_panics_on_invalid_value_high1() {
1177        assert_matches!(BaudRate::decode_from_holding_registers(&[5]), Err(..));
1178    }
1179
1180    #[test]
1181    fn baudrate_decode_panics_on_invalid_value_zero() {
1182        assert_matches!(BaudRate::decode_from_holding_registers(&[]), Err(..));
1183    }
1184
1185    #[test]
1186    fn baudrate_encode() {
1187        assert_eq!(BaudRate::B1200.encode_for_write_register(), 0);
1188        assert_eq!(BaudRate::B2400.encode_for_write_register(), 1);
1189        assert_eq!(BaudRate::B4800.encode_for_write_register(), 2);
1190        assert_eq!(BaudRate::B9600.encode_for_write_register(), 3);
1191        assert_eq!(BaudRate::B19200.encode_for_write_register(), 4);
1192        assert_eq!(BaudRate::default().encode_for_write_register(), 3);
1193    }
1194
1195    #[test]
1196    fn baudrate_try_from_u16() {
1197        assert_matches!(BaudRate::try_from(1200), Ok(BaudRate::B1200));
1198        assert_matches!(BaudRate::try_from(2400), Ok(BaudRate::B2400));
1199        assert_matches!(BaudRate::try_from(4800), Ok(BaudRate::B4800));
1200        assert_matches!(BaudRate::try_from(9600), Ok(BaudRate::B9600));
1201        assert_matches!(BaudRate::try_from(19200), Ok(BaudRate::B19200));
1202        // Invalid values
1203        assert_matches!(BaudRate::try_from(0), Err(ErrorInvalidBaudRate(0)));
1204        assert_matches!(BaudRate::try_from(1000), Err(ErrorInvalidBaudRate(1000)));
1205        assert_matches!(BaudRate::try_from(38400), Err(ErrorInvalidBaudRate(38400)));
1206    }
1207
1208    #[test]
1209    fn baudrate_from_baudrate_to_u16() {
1210        assert_eq!(u16::from(BaudRate::B1200), 1200);
1211        assert_eq!(u16::from(BaudRate::B2400), 2400);
1212        assert_eq!(u16::from(BaudRate::B4800), 4800);
1213        assert_eq!(u16::from(BaudRate::B9600), 9600);
1214        assert_eq!(u16::from(BaudRate::B19200), 19200);
1215        assert_eq!(u16::from(BaudRate::default()), 9600);
1216    }
1217
1218    #[test]
1219    fn baudrate_display() {
1220        assert_eq!(BaudRate::B1200.to_string(), "1200");
1221        assert_eq!(BaudRate::B9600.to_string(), "9600");
1222        assert_eq!(BaudRate::B19200.to_string(), "19200");
1223        assert_eq!(BaudRate::default().to_string(), "9600");
1224    }
1225
1226    // --- Channel Tests ---
1227    #[test]
1228    fn channel_try_from() {
1229        assert_matches!(Channel::try_from(Channel::MIN), Ok(c) if *c == Channel::MIN);
1230        assert_matches!(Channel::try_from(3), Ok(c) if *c == 3);
1231        assert_matches!(Channel::try_from(Channel::MAX), Ok(c) if *c == Channel::MAX);
1232        // Invalid
1233        // MIN is 0, so no lower bound test needed for u8
1234        assert_matches!(
1235            Channel::try_from(Channel::MAX + 1),
1236            Err(ErrorChannelOutOfRange(8))
1237        );
1238        assert_matches!(Channel::try_from(255), Err(ErrorChannelOutOfRange(255)));
1239    }
1240
1241    #[test]
1242    fn channel_new() {
1243        assert_eq!(*Channel::try_from(0).unwrap(), 0);
1244        assert_eq!(*Channel::try_from(7).unwrap(), 7);
1245    }
1246
1247    #[test]
1248    fn channel_display() {
1249        assert_eq!(Channel::try_from(0).unwrap().to_string(), "0");
1250        assert_eq!(Channel::try_from(7).unwrap().to_string(), "7");
1251    }
1252
1253    // --- Temperatures Tests ---
1254    #[test]
1255    fn temperatures_decode() {
1256        let words = [219, 65424, 100, 65506, 32767, 32769, 0, 32768]; // Mix of temps + NAN
1257        let temps = Temperatures::decode_from_holding_registers(&words).unwrap();
1258
1259        assert_eq!(temps[0], Temperature::try_from(21.9).unwrap());
1260        assert_eq!(temps[1], Temperature::try_from(-11.2).unwrap());
1261        assert_eq!(temps[2], Temperature::try_from(10.0).unwrap());
1262        assert_eq!(temps[3], Temperature::try_from(-3.0).unwrap());
1263        assert_eq!(temps[4], Temperature::try_from(3276.7).unwrap()); // MAX
1264        assert_eq!(temps[5], Temperature::try_from(-3276.7).unwrap()); // MIN
1265        assert_eq!(temps[6], Temperature::try_from(0.0).unwrap());
1266        assert!(temps[7].is_nan()); // NAN
1267
1268        // Check iterator
1269        let mut iter = temps.iter();
1270        assert_eq!(iter.next(), Some(&Temperature::try_from(21.9).unwrap()));
1271        assert_eq!(iter.next(), Some(&Temperature::try_from(-11.2).unwrap()));
1272        // ... (check others if desired)
1273        assert!(iter.nth(5).expect("7th element (index 7)").is_nan());
1274        assert_eq!(iter.next(), None);
1275
1276        // Check indexing
1277        assert_eq!(temps[0], Temperature::try_from(21.9).unwrap());
1278        assert!(temps[7].is_nan());
1279    }
1280
1281    #[test]
1282    fn temperatures_decode_wrong_size() {
1283        let words = [219, 65424]; // Too few
1284        assert_matches!(Temperatures::decode_from_holding_registers(&words), Err(..));
1285    }
1286
1287    #[test]
1288    #[should_panic]
1289    fn temperatures_index_out_of_bounds() {
1290        let words = [0; NUMBER_OF_CHANNELS];
1291        let temps = Temperatures::decode_from_holding_registers(&words).unwrap();
1292        let _ = temps[NUMBER_OF_CHANNELS]; // Index 8 is invalid
1293    }
1294
1295    #[test]
1296    fn temperatures_display() {
1297        let words = [219, 65424, 32768, 0, 0, 0, 0, 0];
1298        let temps = Temperatures::decode_from_holding_registers(&words).unwrap();
1299        assert_eq!(
1300            temps.to_string(),
1301            "21.9, -11.2, NAN, 0.0, 0.0, 0.0, 0.0, 0.0"
1302        );
1303    }
1304
1305    // --- TemperatureCorrection Tests ---
1306    #[test]
1307    fn temperature_correction_decode() {
1308        // Uses the same Temperature decoding logic
1309        let words = [10, 65526, 0, 32768, 1, 2, 3, 4]; // 1.0, -1.0, 0.0, NAN etc.
1310        let corrections = TemperatureCorrection::decode_from_holding_registers(&words).unwrap();
1311
1312        assert_eq!(corrections[0], Temperature::try_from(1.0).unwrap());
1313        assert_eq!(corrections[1], Temperature::try_from(-1.0).unwrap());
1314        assert_eq!(corrections[2], Temperature::try_from(0.0).unwrap());
1315        assert!(corrections[3].is_nan());
1316        assert_eq!(corrections.as_slice().len(), 8);
1317    }
1318
1319    #[test]
1320    fn temperature_correction_encode_single() {
1321        // Uses the same Temperature encoding logic
1322        assert_eq!(
1323            TemperatureCorrection::encode_for_write_register(Temperature::try_from(2.0).unwrap()),
1324            Ok(20)
1325        );
1326        assert_eq!(
1327            TemperatureCorrection::encode_for_write_register(Temperature::try_from(-1.5).unwrap()),
1328            Ok((-1.5 * 10.0) as i16 as Word)
1329        ); // 65521
1330        assert_eq!(
1331            TemperatureCorrection::encode_for_write_register(Temperature::try_from(0.0).unwrap()),
1332            Ok(0)
1333        );
1334    }
1335
1336    #[test]
1337    fn temperature_correction_encode_nan() {
1338        // Encoding NAN might depend on desired behavior, assuming 0x8000 based on Temperature encode
1339        assert_matches!(
1340            TemperatureCorrection::encode_for_write_register(Temperature::NAN),
1341            Err(..)
1342        );
1343    }
1344
1345    #[test]
1346    fn temperature_correction_channel_address() {
1347        assert_eq!(
1348            TemperatureCorrection::channel_address(Channel::try_from(0).unwrap()),
1349            TemperatureCorrection::ADDRESS + 0
1350        ); // 0x0008
1351        assert_eq!(
1352            TemperatureCorrection::channel_address(Channel::try_from(1).unwrap()),
1353            TemperatureCorrection::ADDRESS + 1
1354        ); // 0x0009
1355        assert_eq!(
1356            TemperatureCorrection::channel_address(Channel::try_from(7).unwrap()),
1357            TemperatureCorrection::ADDRESS + 7
1358        ); // 0x000F
1359
1360        assert_eq!(TemperatureCorrection::ADDRESS, 0x0008);
1361        assert_eq!(TemperatureCorrection::QUANTITY, 8);
1362    }
1363
1364    #[test]
1365    fn temperature_correction_display() {
1366        let words = [10, 65526, 0, 32768, 0, 0, 0, 0];
1367        let corr = TemperatureCorrection::decode_from_holding_registers(&words).unwrap();
1368        assert_eq!(corr.to_string(), "1.0, -1.0, 0.0, NAN, 0.0, 0.0, 0.0, 0.0");
1369    }
1370
1371    // --- AutomaticReport Tests ---
1372    #[test]
1373    fn automatic_report_decode() {
1374        assert_matches!(AutomaticReport::decode_from_holding_registers(&[0x0000]).unwrap(), report if *report == 0);
1375        assert_matches!(AutomaticReport::decode_from_holding_registers(&[0x0001]).unwrap(), report if *report == 1);
1376        assert_matches!(AutomaticReport::decode_from_holding_registers(&[0x000A]).unwrap(), report if *report == 10); // 10 seconds
1377        assert_matches!(AutomaticReport::decode_from_holding_registers(&[0x00FF]).unwrap(), report if *report == 255);
1378        // Max seconds
1379    }
1380
1381    #[test]
1382    fn automatic_report_decode_invalid() {
1383        assert_matches!(
1384            AutomaticReport::decode_from_holding_registers(&[0x0100]),
1385            Err(..)
1386        ); // Value 256 is invalid
1387    }
1388
1389    #[test]
1390    fn automatic_report_encode() {
1391        assert_eq!(AutomaticReport(0).encode_for_write_register(), 0);
1392        assert_eq!(AutomaticReport(1).encode_for_write_register(), 1);
1393        assert_eq!(AutomaticReport(10).encode_for_write_register(), 10);
1394        assert_eq!(AutomaticReport(255).encode_for_write_register(), 255);
1395        assert_eq!(AutomaticReport::DISABLED.encode_for_write_register(), 0);
1396    }
1397
1398    #[test]
1399    fn automatic_report_try_from_duration() {
1400        assert_matches!(AutomaticReport::try_from(Duration::ZERO), Ok(r) if r.as_secs() == 0);
1401        assert_matches!(AutomaticReport::try_from(Duration::from_secs(1)), Ok(r) if r.as_secs() == 1);
1402        assert_matches!(AutomaticReport::try_from(Duration::from_secs(10)), Ok(r) if r.as_secs() == 10);
1403        assert_matches!(AutomaticReport::try_from(Duration::from_secs(255)), Ok(r) if r.as_secs() == 255);
1404        // Includes fractional part (ignored)
1405        assert_matches!(AutomaticReport::try_from(Duration::from_millis(10500)), Ok(r) if r.as_secs() == 10);
1406
1407        // Invalid duration
1408        let invalid_secs = (AutomaticReport::DURATION_MAX as u64) + 1;
1409        assert_matches!(AutomaticReport::try_from(Duration::from_secs(invalid_secs)), Err(ErrorDurationOutOfRange(value_secs)) if value_secs == invalid_secs);
1410    }
1411
1412    #[test]
1413    fn automatic_report_helpers() {
1414        let report_10s = AutomaticReport::try_from(10).unwrap();
1415        let report_disabled = AutomaticReport::DISABLED;
1416
1417        assert_eq!(report_10s.as_secs(), 10);
1418        assert_eq!(report_10s.as_duration(), Duration::from_secs(10));
1419        assert!(!report_10s.is_disabled());
1420
1421        assert_eq!(report_disabled.as_secs(), 0);
1422        assert_eq!(report_disabled.as_duration(), Duration::ZERO);
1423        assert!(report_disabled.is_disabled());
1424    }
1425
1426    #[test]
1427    fn automatic_report_display() {
1428        assert_eq!(AutomaticReport(0).to_string(), "0s");
1429        assert_eq!(AutomaticReport(1).to_string(), "1s");
1430        assert_eq!(AutomaticReport(255).to_string(), "255s");
1431    }
1432
1433    // --- FactoryReset Tests ---
1434    #[test]
1435    fn factory_reset_encode() {
1436        assert_eq!(FactoryReset::encode_for_write_register(), 5);
1437    }
1438
1439    #[test]
1440    fn factory_reset_address() {
1441        // Ensure it uses the same address as BaudRate write
1442        assert_eq!(FactoryReset::ADDRESS, BaudRate::ADDRESS);
1443    }
1444}