Skip to main content

oxirs_modbus/codec/
data_decoder.rs

1//! Advanced Modbus data decoder with byte-order awareness
2//!
3//! Provides type-safe decoding of raw Modbus register arrays into strongly-typed
4//! [`ModbusTypedValue`] variants. Supports all four byte-order modes commonly
5//! found in industrial devices:
6//!
7//! | Mode                | Word order | Byte order within word |
8//! |---------------------|-----------|------------------------|
9//! | BigEndian           | Hi-Lo     | Hi-Lo (standard)       |
10//! | LittleEndian        | Lo-Hi     | Hi-Lo                  |
11//! | BigEndianSwapped    | Hi-Lo     | Lo-Hi (CDAB)           |
12//! | LittleEndianSwapped | Lo-Hi     | Lo-Hi (BADC)           |
13//!
14//! The decoder intentionally avoids `unwrap()` and propagates all errors via
15//! [`ModbusError`].
16
17use crate::error::{ModbusError, ModbusResult};
18use crate::mapping::ByteOrder;
19use std::fmt;
20
21/// Strongly-typed value decoded from a Modbus register array.
22///
23/// This is a richer type than [`crate::mapping::ModbusValue`]; it preserves
24/// the exact Rust primitive and supports lossless f64 conversion for
25/// arithmetic / scaling.
26#[derive(Debug, Clone, PartialEq)]
27pub enum ModbusTypedValue {
28    /// Boolean decoded from a coil or discrete input
29    Bool(bool),
30    /// Signed 16-bit integer (single register)
31    I16(i16),
32    /// Unsigned 16-bit integer (single register)
33    U16(u16),
34    /// Signed 32-bit integer (two registers)
35    I32(i32),
36    /// Unsigned 32-bit integer (two registers)
37    U32(u32),
38    /// IEEE-754 single-precision float (two registers)
39    F32(f32),
40    /// IEEE-754 double-precision float (four registers)
41    F64(f64),
42    /// ASCII string decoded from N register pairs
43    Str(String),
44}
45
46impl ModbusTypedValue {
47    /// Convert to `f64` for scaling and range checks.
48    ///
49    /// Returns `None` for string values which cannot be meaningfully converted.
50    pub fn as_f64(&self) -> Option<f64> {
51        match self {
52            ModbusTypedValue::Bool(v) => Some(if *v { 1.0 } else { 0.0 }),
53            ModbusTypedValue::I16(v) => Some(*v as f64),
54            ModbusTypedValue::U16(v) => Some(*v as f64),
55            ModbusTypedValue::I32(v) => Some(*v as f64),
56            ModbusTypedValue::U32(v) => Some(*v as f64),
57            ModbusTypedValue::F32(v) => Some(*v as f64),
58            ModbusTypedValue::F64(v) => Some(*v),
59            ModbusTypedValue::Str(_) => None,
60        }
61    }
62
63    /// Apply a linear scale factor and additive offset, returning an `f64`.
64    ///
65    /// Formula: `physical = raw * scale_factor + offset`
66    ///
67    /// Returns `None` when the value cannot be converted to `f64` (strings).
68    pub fn scale(&self, scale_factor: f64, offset: f64) -> Option<f64> {
69        self.as_f64().map(|v| v * scale_factor + offset)
70    }
71
72    /// Name of the type as a static string.
73    pub fn type_name(&self) -> &'static str {
74        match self {
75            ModbusTypedValue::Bool(_) => "Bool",
76            ModbusTypedValue::I16(_) => "I16",
77            ModbusTypedValue::U16(_) => "U16",
78            ModbusTypedValue::I32(_) => "I32",
79            ModbusTypedValue::U32(_) => "U32",
80            ModbusTypedValue::F32(_) => "F32",
81            ModbusTypedValue::F64(_) => "F64",
82            ModbusTypedValue::Str(_) => "Str",
83        }
84    }
85}
86
87impl fmt::Display for ModbusTypedValue {
88    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
89        match self {
90            ModbusTypedValue::Bool(v) => write!(f, "{}", v),
91            ModbusTypedValue::I16(v) => write!(f, "{}", v),
92            ModbusTypedValue::U16(v) => write!(f, "{}", v),
93            ModbusTypedValue::I32(v) => write!(f, "{}", v),
94            ModbusTypedValue::U32(v) => write!(f, "{}", v),
95            ModbusTypedValue::F32(v) => write!(f, "{}", v),
96            ModbusTypedValue::F64(v) => write!(f, "{}", v),
97            ModbusTypedValue::Str(v) => write!(f, "{}", v),
98        }
99    }
100}
101
102/// Data type specifier for the decoder — a superset of
103/// [`crate::mapping::ModbusDataType`] that includes explicit `Bool`.
104#[derive(Debug, Clone, Copy, PartialEq, Eq)]
105pub enum DecoderDataType {
106    /// Single-bit boolean (from coil / discrete-input value)
107    Bool,
108    /// Signed 16-bit integer (1 register)
109    I16,
110    /// Unsigned 16-bit integer (1 register)
111    U16,
112    /// Signed 32-bit integer (2 registers)
113    I32,
114    /// Unsigned 32-bit integer (2 registers)
115    U32,
116    /// IEEE-754 single-precision float (2 registers)
117    F32,
118    /// IEEE-754 double-precision float (4 registers)
119    F64,
120    /// ASCII string spanning `n` registers (2 chars/register)
121    Str(usize),
122}
123
124impl DecoderDataType {
125    /// Number of 16-bit registers required for this type.
126    pub fn register_count(self) -> usize {
127        match self {
128            DecoderDataType::Bool | DecoderDataType::I16 | DecoderDataType::U16 => 1,
129            DecoderDataType::I32 | DecoderDataType::U32 | DecoderDataType::F32 => 2,
130            DecoderDataType::F64 => 4,
131            DecoderDataType::Str(n) => (n + 1) / 2,
132        }
133    }
134}
135
136impl From<crate::mapping::ModbusDataType> for DecoderDataType {
137    fn from(dt: crate::mapping::ModbusDataType) -> Self {
138        use crate::mapping::ModbusDataType as Mdt;
139        match dt {
140            Mdt::Int16 => DecoderDataType::I16,
141            Mdt::Uint16 | Mdt::Bit(_) => DecoderDataType::U16,
142            Mdt::Int32 => DecoderDataType::I32,
143            Mdt::Uint32 => DecoderDataType::U32,
144            Mdt::Float32 => DecoderDataType::F32,
145            Mdt::Float64 => DecoderDataType::F64,
146            Mdt::String(n) => DecoderDataType::Str(n),
147        }
148    }
149}
150
151/// Stateless decoder for Modbus register arrays.
152///
153/// All methods are pure functions – no mutable state is held. Create one
154/// instance and reuse it freely.
155pub struct ModbusDecoder;
156
157impl ModbusDecoder {
158    /// Decode a slice of raw `u16` Modbus registers into a typed value.
159    ///
160    /// # Arguments
161    ///
162    /// * `regs` - Slice starting at the register of interest. Must contain at
163    ///   least `data_type.register_count()` elements.
164    /// * `data_type` - How to interpret the raw register data.
165    /// * `byte_order` - Word and byte ordering used by the target device.
166    ///
167    /// # Errors
168    ///
169    /// Returns [`ModbusError`] when the slice is too short or the decoded
170    /// floating-point value is non-finite (NaN / infinity).
171    pub fn decode(
172        regs: &[u16],
173        data_type: DecoderDataType,
174        byte_order: ByteOrder,
175    ) -> ModbusResult<ModbusTypedValue> {
176        let required = data_type.register_count();
177        if regs.len() < required {
178            return Err(ModbusError::Io(std::io::Error::new(
179                std::io::ErrorKind::InvalidInput,
180                format!(
181                    "decoder needs {} register(s) for {:?}, got {}",
182                    required,
183                    data_type,
184                    regs.len()
185                ),
186            )));
187        }
188
189        match data_type {
190            DecoderDataType::Bool => {
191                // Treat register as boolean: non-zero = true
192                Ok(ModbusTypedValue::Bool(regs[0] != 0))
193            }
194            DecoderDataType::I16 => Ok(ModbusTypedValue::I16(regs[0] as i16)),
195            DecoderDataType::U16 => Ok(ModbusTypedValue::U16(regs[0])),
196            DecoderDataType::I32 => {
197                let raw = Self::regs_to_u32(regs, byte_order);
198                Ok(ModbusTypedValue::I32(raw as i32))
199            }
200            DecoderDataType::U32 => {
201                let raw = Self::regs_to_u32(regs, byte_order);
202                Ok(ModbusTypedValue::U32(raw))
203            }
204            DecoderDataType::F32 => {
205                let raw = Self::regs_to_u32(regs, byte_order);
206                let v = f32::from_bits(raw);
207                if !v.is_finite() {
208                    return Err(ModbusError::Io(std::io::Error::new(
209                        std::io::ErrorKind::InvalidData,
210                        format!("decoded F32 is non-finite: {}", v),
211                    )));
212                }
213                Ok(ModbusTypedValue::F32(v))
214            }
215            DecoderDataType::F64 => {
216                let raw = Self::regs_to_u64(regs, byte_order);
217                let v = f64::from_bits(raw);
218                if !v.is_finite() {
219                    return Err(ModbusError::Io(std::io::Error::new(
220                        std::io::ErrorKind::InvalidData,
221                        format!("decoded F64 is non-finite: {}", v),
222                    )));
223                }
224                Ok(ModbusTypedValue::F64(v))
225            }
226            DecoderDataType::Str(char_count) => Ok(ModbusTypedValue::Str(Self::regs_to_string(
227                regs, char_count,
228            ))),
229        }
230    }
231
232    /// Apply scale factor and offset to a decoded value, producing an `f64`.
233    ///
234    /// Returns `None` for string values.
235    pub fn scale(value: &ModbusTypedValue, scale_factor: f64, offset: f64) -> Option<f64> {
236        value.scale(scale_factor, offset)
237    }
238
239    // ── private helpers ────────────────────────────────────────────────────
240
241    /// Reassemble two registers into a `u32`, respecting byte order.
242    ///
243    /// Byte order semantics (Modbus convention uses 16-bit "words"):
244    ///
245    /// ```text
246    /// BigEndian           (AB CD): regs[0]=AB_CD, regs[1]=EF_GH → 0xABCDEFGH
247    /// LittleEndian        (EF GH + AB CD): regs[0]=EF_GH, regs[1]=AB_CD → 0xABCDEFGH
248    /// BigEndianSwapped    (BA DC): regs[0]=BA_DC, regs[1]=FE_HG → 0xABCDEFGH
249    /// LittleEndianSwapped (FE HG + BA DC): ...
250    /// ```
251    fn regs_to_u32(regs: &[u16], order: ByteOrder) -> u32 {
252        match order {
253            ByteOrder::BigEndian => {
254                // Standard Modbus: high word first, natural byte order
255                ((regs[0] as u32) << 16) | (regs[1] as u32)
256            }
257            ByteOrder::LittleEndian => {
258                // Low word first
259                ((regs[1] as u32) << 16) | (regs[0] as u32)
260            }
261            ByteOrder::BigEndianSwapped => {
262                // High word first, bytes within each word swapped (CDAB)
263                let hi = ((regs[0] & 0xFF) << 8) | (regs[0] >> 8);
264                let lo = ((regs[1] & 0xFF) << 8) | (regs[1] >> 8);
265                ((hi as u32) << 16) | (lo as u32)
266            }
267            ByteOrder::LittleEndianSwapped => {
268                // Low word first, bytes within each word swapped (BADC)
269                let hi = ((regs[1] & 0xFF) << 8) | (regs[1] >> 8);
270                let lo = ((regs[0] & 0xFF) << 8) | (regs[0] >> 8);
271                ((hi as u32) << 16) | (lo as u32)
272            }
273        }
274    }
275
276    /// Reassemble four registers into a `u64`, respecting byte order.
277    fn regs_to_u64(regs: &[u16], order: ByteOrder) -> u64 {
278        match order {
279            ByteOrder::BigEndian => {
280                ((regs[0] as u64) << 48)
281                    | ((regs[1] as u64) << 32)
282                    | ((regs[2] as u64) << 16)
283                    | (regs[3] as u64)
284            }
285            ByteOrder::LittleEndian => {
286                ((regs[3] as u64) << 48)
287                    | ((regs[2] as u64) << 32)
288                    | ((regs[1] as u64) << 16)
289                    | (regs[0] as u64)
290            }
291            ByteOrder::BigEndianSwapped => {
292                // Swap bytes in each 16-bit word, keep word order hi→lo
293                let mut result: u64 = 0;
294                for (i, &reg) in regs[..4].iter().enumerate() {
295                    let swapped = ((reg & 0xFF) << 8) | (reg >> 8);
296                    result |= (swapped as u64) << ((3 - i) * 16);
297                }
298                result
299            }
300            ByteOrder::LittleEndianSwapped => {
301                // Swap bytes in each 16-bit word, reverse word order
302                let mut result: u64 = 0;
303                for (i, &reg) in regs[..4].iter().enumerate() {
304                    let swapped = ((reg & 0xFF) << 8) | (reg >> 8);
305                    result |= (swapped as u64) << (i * 16);
306                }
307                result
308            }
309        }
310    }
311
312    /// Decode a Modbus register slice into an ASCII string of `char_count` chars.
313    ///
314    /// Null bytes and trailing whitespace are stripped.
315    fn regs_to_string(regs: &[u16], char_count: usize) -> String {
316        let n_regs = (char_count + 1) / 2;
317        let mut bytes: Vec<u8> = Vec::with_capacity(char_count);
318        for &reg in regs.iter().take(n_regs) {
319            bytes.push((reg >> 8) as u8);
320            bytes.push((reg & 0xFF) as u8);
321        }
322        bytes.truncate(char_count);
323        // Strip null terminator
324        while bytes.last() == Some(&0) {
325            bytes.pop();
326        }
327        String::from_utf8_lossy(&bytes).trim_end().to_string()
328    }
329}
330
331/// Encoder: convert typed values back into Modbus register arrays.
332///
333/// Useful for write operations (FC 0x06 / 0x10).
334pub struct ModbusEncoder;
335
336impl ModbusEncoder {
337    /// Encode a typed value into a `Vec<u16>` suitable for Modbus writes.
338    ///
339    /// # Errors
340    ///
341    /// Returns an error when the value and data type are incompatible.
342    pub fn encode(
343        value: &ModbusTypedValue,
344        data_type: DecoderDataType,
345        byte_order: ByteOrder,
346    ) -> ModbusResult<Vec<u16>> {
347        match (value, data_type) {
348            (ModbusTypedValue::Bool(v), DecoderDataType::Bool | DecoderDataType::U16) => {
349                Ok(vec![if *v { 0xFF00 } else { 0x0000 }])
350            }
351            (ModbusTypedValue::I16(v), DecoderDataType::I16) => Ok(vec![*v as u16]),
352            (ModbusTypedValue::U16(v), DecoderDataType::U16) => Ok(vec![*v]),
353            (ModbusTypedValue::I32(v), DecoderDataType::I32) => {
354                Ok(Self::u32_to_regs(*v as u32, byte_order))
355            }
356            (ModbusTypedValue::U32(v), DecoderDataType::U32) => {
357                Ok(Self::u32_to_regs(*v, byte_order))
358            }
359            (ModbusTypedValue::F32(v), DecoderDataType::F32) => {
360                Ok(Self::u32_to_regs(v.to_bits(), byte_order))
361            }
362            (ModbusTypedValue::F64(v), DecoderDataType::F64) => {
363                Ok(Self::u64_to_regs(v.to_bits(), byte_order))
364            }
365            (ModbusTypedValue::Str(s), DecoderDataType::Str(char_count)) => {
366                Ok(Self::string_to_regs(s, char_count))
367            }
368            _ => Err(ModbusError::Io(std::io::Error::new(
369                std::io::ErrorKind::InvalidInput,
370                format!("cannot encode {} as {:?}", value.type_name(), data_type),
371            ))),
372        }
373    }
374
375    fn u32_to_regs(v: u32, order: ByteOrder) -> Vec<u16> {
376        let hi = (v >> 16) as u16;
377        let lo = (v & 0xFFFF) as u16;
378        match order {
379            ByteOrder::BigEndian => vec![hi, lo],
380            ByteOrder::LittleEndian => vec![lo, hi],
381            ByteOrder::BigEndianSwapped => {
382                vec![hi.swap_bytes(), lo.swap_bytes()]
383            }
384            ByteOrder::LittleEndianSwapped => {
385                vec![lo.swap_bytes(), hi.swap_bytes()]
386            }
387        }
388    }
389
390    fn u64_to_regs(v: u64, order: ByteOrder) -> Vec<u16> {
391        let w3 = (v >> 48) as u16;
392        let w2 = ((v >> 32) & 0xFFFF) as u16;
393        let w1 = ((v >> 16) & 0xFFFF) as u16;
394        let w0 = (v & 0xFFFF) as u16;
395        match order {
396            ByteOrder::BigEndian => vec![w3, w2, w1, w0],
397            ByteOrder::LittleEndian => vec![w0, w1, w2, w3],
398            ByteOrder::BigEndianSwapped => {
399                vec![
400                    w3.swap_bytes(),
401                    w2.swap_bytes(),
402                    w1.swap_bytes(),
403                    w0.swap_bytes(),
404                ]
405            }
406            ByteOrder::LittleEndianSwapped => {
407                vec![
408                    w0.swap_bytes(),
409                    w1.swap_bytes(),
410                    w2.swap_bytes(),
411                    w3.swap_bytes(),
412                ]
413            }
414        }
415    }
416
417    fn string_to_regs(s: &str, char_count: usize) -> Vec<u16> {
418        let n_regs = (char_count + 1) / 2;
419        let bytes = s.as_bytes();
420        let mut regs = Vec::with_capacity(n_regs);
421        for i in 0..n_regs {
422            let hi = bytes.get(i * 2).copied().unwrap_or(0);
423            let lo = bytes.get(i * 2 + 1).copied().unwrap_or(0);
424            regs.push(((hi as u16) << 8) | (lo as u16));
425        }
426        regs
427    }
428}
429
430#[cfg(test)]
431mod tests {
432    use super::*;
433
434    // ── decode roundtrip tests ───────────────────────────────────────────
435
436    #[test]
437    fn test_decode_bool() {
438        let v =
439            ModbusDecoder::decode(&[0xFF00], DecoderDataType::Bool, ByteOrder::BigEndian).unwrap();
440        assert_eq!(v, ModbusTypedValue::Bool(true));
441
442        let v =
443            ModbusDecoder::decode(&[0x0000], DecoderDataType::Bool, ByteOrder::BigEndian).unwrap();
444        assert_eq!(v, ModbusTypedValue::Bool(false));
445    }
446
447    #[test]
448    fn test_decode_i16() {
449        // Positive
450        let v = ModbusDecoder::decode(&[255], DecoderDataType::I16, ByteOrder::BigEndian).unwrap();
451        assert_eq!(v, ModbusTypedValue::I16(255));
452
453        // Negative (two's complement)
454        let v =
455            ModbusDecoder::decode(&[0xFFFF], DecoderDataType::I16, ByteOrder::BigEndian).unwrap();
456        assert_eq!(v, ModbusTypedValue::I16(-1));
457    }
458
459    #[test]
460    fn test_decode_u16() {
461        let v =
462            ModbusDecoder::decode(&[0xFFFF], DecoderDataType::U16, ByteOrder::BigEndian).unwrap();
463        assert_eq!(v, ModbusTypedValue::U16(65535));
464    }
465
466    #[test]
467    fn test_decode_i32_big_endian() {
468        // 0x0001_0000 = 65536
469        let regs = [0x0001, 0x0000];
470        let v = ModbusDecoder::decode(&regs, DecoderDataType::I32, ByteOrder::BigEndian).unwrap();
471        assert_eq!(v, ModbusTypedValue::I32(65536));
472    }
473
474    #[test]
475    fn test_decode_i32_little_endian() {
476        // Little-endian: regs[0]=lo, regs[1]=hi
477        // [0x0000, 0x0001] → hi=0x0001, lo=0x0000 → 0x0001_0000 = 65536
478        let regs = [0x0000, 0x0001];
479        let v =
480            ModbusDecoder::decode(&regs, DecoderDataType::I32, ByteOrder::LittleEndian).unwrap();
481        assert_eq!(v, ModbusTypedValue::I32(65536));
482    }
483
484    #[test]
485    fn test_decode_u32_all_byte_orders() {
486        // Value: 0xDEAD_BEEF
487        let be_regs = [0xDEAD, 0xBEEF]; // big-endian
488        let le_regs = [0xBEEF, 0xDEAD]; // little-endian (word-swapped)
489
490        let v_be =
491            ModbusDecoder::decode(&be_regs, DecoderDataType::U32, ByteOrder::BigEndian).unwrap();
492        let v_le =
493            ModbusDecoder::decode(&le_regs, DecoderDataType::U32, ByteOrder::LittleEndian).unwrap();
494
495        assert_eq!(v_be, ModbusTypedValue::U32(0xDEADBEEF));
496        assert_eq!(v_le, ModbusTypedValue::U32(0xDEADBEEF));
497    }
498
499    #[test]
500    fn test_decode_f32_big_endian() {
501        // IEEE 754: 1.0 = 0x3F80_0000
502        let regs = [0x3F80, 0x0000];
503        let v = ModbusDecoder::decode(&regs, DecoderDataType::F32, ByteOrder::BigEndian).unwrap();
504        match v {
505            ModbusTypedValue::F32(f) => assert!((f - 1.0f32).abs() < 1e-6),
506            _ => panic!("Expected F32"),
507        }
508    }
509
510    #[test]
511    fn test_decode_f32_big_endian_swapped() {
512        // 1.0 BE = [0x3F80, 0x0000]
513        // After byte-swap in each word: [0x803F, 0x0000]
514        let swapped_regs = [0x803F, 0x0000];
515        let v = ModbusDecoder::decode(
516            &swapped_regs,
517            DecoderDataType::F32,
518            ByteOrder::BigEndianSwapped,
519        )
520        .unwrap();
521        match v {
522            ModbusTypedValue::F32(f) => assert!((f - 1.0f32).abs() < 1e-6),
523            _ => panic!("Expected F32"),
524        }
525    }
526
527    #[test]
528    fn test_decode_f64_big_endian() {
529        // 1.0_f64 = 0x3FF0_0000_0000_0000
530        let regs = [0x3FF0, 0x0000, 0x0000, 0x0000];
531        let v = ModbusDecoder::decode(&regs, DecoderDataType::F64, ByteOrder::BigEndian).unwrap();
532        match v {
533            ModbusTypedValue::F64(f) => assert!((f - 1.0_f64).abs() < 1e-12),
534            _ => panic!("Expected F64"),
535        }
536    }
537
538    #[test]
539    fn test_decode_string() {
540        // "ABCD" = 0x4142, 0x4344
541        let regs = [0x4142, 0x4344];
542        let v =
543            ModbusDecoder::decode(&regs, DecoderDataType::Str(4), ByteOrder::BigEndian).unwrap();
544        assert_eq!(v, ModbusTypedValue::Str("ABCD".to_string()));
545    }
546
547    #[test]
548    fn test_decode_string_with_nulls() {
549        // "AB\0\0" — trailing nulls should be stripped
550        let regs = [0x4142, 0x0000];
551        let v =
552            ModbusDecoder::decode(&regs, DecoderDataType::Str(4), ByteOrder::BigEndian).unwrap();
553        assert_eq!(v, ModbusTypedValue::Str("AB".to_string()));
554    }
555
556    #[test]
557    fn test_insufficient_registers_error() {
558        let result = ModbusDecoder::decode(&[0x3F80], DecoderDataType::F32, ByteOrder::BigEndian);
559        assert!(result.is_err());
560    }
561
562    // ── scale tests ──────────────────────────────────────────────────────
563
564    #[test]
565    fn test_scale_linear() {
566        let v = ModbusTypedValue::I16(625);
567        // 625 * 0.1 + (-40.0) = 22.5
568        let scaled = ModbusDecoder::scale(&v, 0.1, -40.0).unwrap();
569        assert!((scaled - 22.5).abs() < 1e-9);
570    }
571
572    #[test]
573    fn test_scale_string_returns_none() {
574        let v = ModbusTypedValue::Str("hello".to_string());
575        assert!(ModbusDecoder::scale(&v, 1.0, 0.0).is_none());
576    }
577
578    // ── encoder roundtrip tests ──────────────────────────────────────────
579
580    #[test]
581    fn test_encode_i16_roundtrip() {
582        let original = ModbusTypedValue::I16(-1024);
583        let encoded =
584            ModbusEncoder::encode(&original, DecoderDataType::I16, ByteOrder::BigEndian).unwrap();
585        let decoded =
586            ModbusDecoder::decode(&encoded, DecoderDataType::I16, ByteOrder::BigEndian).unwrap();
587        assert_eq!(decoded, original);
588    }
589
590    #[test]
591    fn test_encode_u32_roundtrip_all_orders() {
592        let orders = [
593            ByteOrder::BigEndian,
594            ByteOrder::LittleEndian,
595            ByteOrder::BigEndianSwapped,
596            ByteOrder::LittleEndianSwapped,
597        ];
598        let original = ModbusTypedValue::U32(0xCAFEBABE);
599        for order in orders {
600            let encoded = ModbusEncoder::encode(&original, DecoderDataType::U32, order).unwrap();
601            let decoded = ModbusDecoder::decode(&encoded, DecoderDataType::U32, order).unwrap();
602            assert_eq!(
603                decoded, original,
604                "roundtrip failed for byte order {:?}",
605                order
606            );
607        }
608    }
609
610    #[test]
611    fn test_encode_f32_roundtrip() {
612        let original = ModbusTypedValue::F32(22.5);
613        let encoded =
614            ModbusEncoder::encode(&original, DecoderDataType::F32, ByteOrder::BigEndian).unwrap();
615        let decoded =
616            ModbusDecoder::decode(&encoded, DecoderDataType::F32, ByteOrder::BigEndian).unwrap();
617        match decoded {
618            ModbusTypedValue::F32(f) => assert!((f - 22.5).abs() < 1e-5),
619            _ => panic!("Expected F32"),
620        }
621    }
622
623    #[test]
624    fn test_encode_f64_roundtrip() {
625        let original = ModbusTypedValue::F64(std::f64::consts::PI);
626        let orders = [ByteOrder::BigEndian, ByteOrder::LittleEndian];
627        for order in orders {
628            let encoded = ModbusEncoder::encode(&original, DecoderDataType::F64, order).unwrap();
629            let decoded = ModbusDecoder::decode(&encoded, DecoderDataType::F64, order).unwrap();
630            match decoded {
631                ModbusTypedValue::F64(f) => {
632                    assert!(
633                        (f - std::f64::consts::PI).abs() < 1e-12,
634                        "f64 roundtrip failed for {:?}",
635                        order
636                    );
637                }
638                _ => panic!("Expected F64"),
639            }
640        }
641    }
642
643    #[test]
644    fn test_encode_string_roundtrip() {
645        let original = ModbusTypedValue::Str("Test".to_string());
646        let encoded =
647            ModbusEncoder::encode(&original, DecoderDataType::Str(4), ByteOrder::BigEndian)
648                .unwrap();
649        let decoded =
650            ModbusDecoder::decode(&encoded, DecoderDataType::Str(4), ByteOrder::BigEndian).unwrap();
651        assert_eq!(decoded, original);
652    }
653
654    #[test]
655    fn test_type_mismatch_error() {
656        let v = ModbusTypedValue::Str("hello".to_string());
657        let result = ModbusEncoder::encode(&v, DecoderDataType::F32, ByteOrder::BigEndian);
658        assert!(result.is_err());
659    }
660
661    // ── as_f64 / Display tests ───────────────────────────────────────────
662
663    #[test]
664    fn test_as_f64_all_types() {
665        assert_eq!(ModbusTypedValue::Bool(true).as_f64(), Some(1.0));
666        assert_eq!(ModbusTypedValue::Bool(false).as_f64(), Some(0.0));
667        assert_eq!(ModbusTypedValue::I16(-5).as_f64(), Some(-5.0));
668        assert_eq!(ModbusTypedValue::U16(100).as_f64(), Some(100.0));
669        assert_eq!(ModbusTypedValue::I32(-1).as_f64(), Some(-1.0));
670        assert_eq!(ModbusTypedValue::U32(42).as_f64(), Some(42.0));
671        assert!(
672            (ModbusTypedValue::F32(std::f32::consts::PI)
673                .as_f64()
674                .unwrap()
675                - std::f64::consts::PI)
676                .abs()
677                < 1e-5
678        );
679        assert!(
680            (ModbusTypedValue::F64(std::f64::consts::E).as_f64().unwrap() - std::f64::consts::E)
681                .abs()
682                < 1e-12
683        );
684        assert!(ModbusTypedValue::Str("x".to_string()).as_f64().is_none());
685    }
686
687    #[test]
688    fn test_display() {
689        assert_eq!(format!("{}", ModbusTypedValue::I16(-42)), "-42");
690        assert_eq!(format!("{}", ModbusTypedValue::Bool(true)), "true");
691    }
692
693    #[test]
694    fn test_type_name() {
695        assert_eq!(ModbusTypedValue::Bool(false).type_name(), "Bool");
696        assert_eq!(ModbusTypedValue::F32(0.0).type_name(), "F32");
697        assert_eq!(ModbusTypedValue::Str(String::new()).type_name(), "Str");
698    }
699}