Skip to main content

oxirs_modbus/mapping/
data_types.rs

1//! Modbus data type conversions
2//!
3//! Handles conversion from raw Modbus register values to typed values
4//! and RDF literals with proper XSD datatypes.
5
6use crate::error::{ModbusError, ModbusResult};
7use std::fmt;
8use std::str::FromStr;
9
10/// Modbus data types for register interpretation
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum ModbusDataType {
13    /// Signed 16-bit integer (single register)
14    Int16,
15    /// Unsigned 16-bit integer (single register)
16    Uint16,
17    /// Signed 32-bit integer (two registers, big-endian)
18    Int32,
19    /// Unsigned 32-bit integer (two registers, big-endian)
20    Uint32,
21    /// IEEE 754 single precision float (two registers, big-endian)
22    Float32,
23    /// IEEE 754 double precision float (four registers, big-endian)
24    Float64,
25    /// Single bit extraction from register (0-15)
26    Bit(u8),
27    /// String (multiple registers, ASCII)
28    String(usize),
29}
30
31impl ModbusDataType {
32    /// Number of 16-bit registers required for this data type
33    pub fn register_count(&self) -> usize {
34        match self {
35            ModbusDataType::Int16 | ModbusDataType::Uint16 => 1,
36            ModbusDataType::Int32 | ModbusDataType::Uint32 | ModbusDataType::Float32 => 2,
37            ModbusDataType::Float64 => 4,
38            ModbusDataType::Bit(_) => 1,
39            ModbusDataType::String(len) => (*len + 1) / 2, // 2 chars per register
40        }
41    }
42
43    /// XSD datatype IRI for RDF literals
44    pub fn xsd_datatype(&self) -> &'static str {
45        match self {
46            ModbusDataType::Int16 => "http://www.w3.org/2001/XMLSchema#short",
47            ModbusDataType::Uint16 => "http://www.w3.org/2001/XMLSchema#unsignedShort",
48            ModbusDataType::Int32 => "http://www.w3.org/2001/XMLSchema#int",
49            ModbusDataType::Uint32 => "http://www.w3.org/2001/XMLSchema#unsignedInt",
50            ModbusDataType::Float32 | ModbusDataType::Float64 => {
51                "http://www.w3.org/2001/XMLSchema#float"
52            }
53            ModbusDataType::Bit(_) => "http://www.w3.org/2001/XMLSchema#boolean",
54            ModbusDataType::String(_) => "http://www.w3.org/2001/XMLSchema#string",
55        }
56    }
57}
58
59impl FromStr for ModbusDataType {
60    type Err = ModbusError;
61
62    fn from_str(s: &str) -> Result<Self, Self::Err> {
63        let s_upper = s.to_uppercase();
64        match s_upper.as_str() {
65            "INT16" | "SHORT" => Ok(ModbusDataType::Int16),
66            "UINT16" | "WORD" | "USHORT" => Ok(ModbusDataType::Uint16),
67            "INT32" | "DINT" | "INT" => Ok(ModbusDataType::Int32),
68            "UINT32" | "DWORD" | "UDINT" => Ok(ModbusDataType::Uint32),
69            "FLOAT32" | "REAL" | "FLOAT" => Ok(ModbusDataType::Float32),
70            "FLOAT64" | "LREAL" | "DOUBLE" => Ok(ModbusDataType::Float64),
71            _ if s_upper.starts_with("BIT") => {
72                // Parse "BIT0", "BIT15", etc.
73                let bit_num: u8 = s_upper[3..].parse().map_err(|_| {
74                    ModbusError::Io(std::io::Error::new(
75                        std::io::ErrorKind::InvalidInput,
76                        format!("Invalid bit number in {}", s),
77                    ))
78                })?;
79                if bit_num > 15 {
80                    return Err(ModbusError::Io(std::io::Error::new(
81                        std::io::ErrorKind::InvalidInput,
82                        "Bit number must be 0-15",
83                    )));
84                }
85                Ok(ModbusDataType::Bit(bit_num))
86            }
87            _ if s_upper.starts_with("STRING") => {
88                // Parse "STRING10", "STRING32", etc.
89                let len: usize = s_upper[6..].parse().map_err(|_| {
90                    ModbusError::Io(std::io::Error::new(
91                        std::io::ErrorKind::InvalidInput,
92                        format!("Invalid string length in {}", s),
93                    ))
94                })?;
95                Ok(ModbusDataType::String(len))
96            }
97            _ => Err(ModbusError::Io(std::io::Error::new(
98                std::io::ErrorKind::InvalidInput,
99                format!("Unknown data type: {}", s),
100            ))),
101        }
102    }
103}
104
105impl fmt::Display for ModbusDataType {
106    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
107        match self {
108            ModbusDataType::Int16 => write!(f, "INT16"),
109            ModbusDataType::Uint16 => write!(f, "UINT16"),
110            ModbusDataType::Int32 => write!(f, "INT32"),
111            ModbusDataType::Uint32 => write!(f, "UINT32"),
112            ModbusDataType::Float32 => write!(f, "FLOAT32"),
113            ModbusDataType::Float64 => write!(f, "FLOAT64"),
114            ModbusDataType::Bit(n) => write!(f, "BIT{}", n),
115            ModbusDataType::String(len) => write!(f, "STRING{}", len),
116        }
117    }
118}
119
120/// Decoded value from Modbus registers
121#[derive(Debug, Clone, PartialEq)]
122pub enum ModbusValue {
123    /// Signed integer value
124    Int(i64),
125    /// Unsigned integer value
126    Uint(u64),
127    /// Floating point value
128    Float(f64),
129    /// Boolean value
130    Bool(bool),
131    /// String value
132    String(String),
133}
134
135impl ModbusValue {
136    /// Convert to f64 (for scaling)
137    pub fn as_f64(&self) -> Option<f64> {
138        match self {
139            ModbusValue::Int(v) => Some(*v as f64),
140            ModbusValue::Uint(v) => Some(*v as f64),
141            ModbusValue::Float(v) => Some(*v),
142            ModbusValue::Bool(v) => Some(if *v { 1.0 } else { 0.0 }),
143            ModbusValue::String(_) => None,
144        }
145    }
146
147    /// Convert to RDF literal string representation
148    pub fn to_rdf_literal(&self) -> String {
149        match self {
150            ModbusValue::Int(v) => v.to_string(),
151            ModbusValue::Uint(v) => v.to_string(),
152            ModbusValue::Float(v) => {
153                // Format float with appropriate precision
154                if v.fract() == 0.0 {
155                    format!("{:.1}", v)
156                } else {
157                    format!("{}", v)
158                }
159            }
160            ModbusValue::Bool(v) => if *v { "true" } else { "false" }.to_string(),
161            ModbusValue::String(s) => s.clone(),
162        }
163    }
164}
165
166impl fmt::Display for ModbusValue {
167    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
168        match self {
169            ModbusValue::Int(v) => write!(f, "{}", v),
170            ModbusValue::Uint(v) => write!(f, "{}", v),
171            ModbusValue::Float(v) => write!(f, "{}", v),
172            ModbusValue::Bool(v) => write!(f, "{}", v),
173            ModbusValue::String(s) => write!(f, "{}", s),
174        }
175    }
176}
177
178/// Linear scaling parameters (physical = raw * multiplier + offset)
179#[derive(Debug, Clone, Copy, PartialEq)]
180pub struct LinearScaling {
181    /// Multiplier (default: 1.0)
182    pub multiplier: f64,
183    /// Offset (default: 0.0)
184    pub offset: f64,
185}
186
187impl Default for LinearScaling {
188    fn default() -> Self {
189        Self {
190            multiplier: 1.0,
191            offset: 0.0,
192        }
193    }
194}
195
196impl LinearScaling {
197    /// Create new scaling with multiplier and offset
198    pub fn new(multiplier: f64, offset: f64) -> Self {
199        Self { multiplier, offset }
200    }
201
202    /// Apply scaling: physical = raw * multiplier + offset
203    pub fn apply(&self, raw: f64) -> f64 {
204        raw * self.multiplier + self.offset
205    }
206
207    /// Reverse scaling: raw = (physical - offset) / multiplier
208    pub fn reverse(&self, physical: f64) -> f64 {
209        (physical - self.offset) / self.multiplier
210    }
211
212    /// Check if scaling is identity (no change)
213    pub fn is_identity(&self) -> bool {
214        (self.multiplier - 1.0).abs() < f64::EPSILON && self.offset.abs() < f64::EPSILON
215    }
216}
217
218/// Decode raw register values to typed values
219pub fn decode_registers(registers: &[u16], data_type: ModbusDataType) -> ModbusResult<ModbusValue> {
220    let required = data_type.register_count();
221    if registers.len() < required {
222        return Err(ModbusError::Io(std::io::Error::new(
223            std::io::ErrorKind::InvalidInput,
224            format!(
225                "Need {} registers for {:?}, got {}",
226                required,
227                data_type,
228                registers.len()
229            ),
230        )));
231    }
232
233    match data_type {
234        ModbusDataType::Int16 => {
235            let value = registers[0] as i16;
236            Ok(ModbusValue::Int(value as i64))
237        }
238        ModbusDataType::Uint16 => {
239            let value = registers[0];
240            Ok(ModbusValue::Uint(value as u64))
241        }
242        ModbusDataType::Int32 => {
243            // Big-endian: high word first
244            let value = ((registers[0] as i32) << 16) | (registers[1] as i32);
245            Ok(ModbusValue::Int(value as i64))
246        }
247        ModbusDataType::Uint32 => {
248            // Big-endian: high word first
249            let value = ((registers[0] as u32) << 16) | (registers[1] as u32);
250            Ok(ModbusValue::Uint(value as u64))
251        }
252        ModbusDataType::Float32 => {
253            // Big-endian: high word first
254            let bits = ((registers[0] as u32) << 16) | (registers[1] as u32);
255            let value = f32::from_bits(bits);
256            Ok(ModbusValue::Float(value as f64))
257        }
258        ModbusDataType::Float64 => {
259            // Big-endian: high word first
260            let bits = ((registers[0] as u64) << 48)
261                | ((registers[1] as u64) << 32)
262                | ((registers[2] as u64) << 16)
263                | (registers[3] as u64);
264            let value = f64::from_bits(bits);
265            Ok(ModbusValue::Float(value))
266        }
267        ModbusDataType::Bit(bit_num) => {
268            let value = (registers[0] >> bit_num) & 1 == 1;
269            Ok(ModbusValue::Bool(value))
270        }
271        ModbusDataType::String(len) => {
272            let mut chars = Vec::with_capacity(len);
273            for reg in registers.iter().take((len + 1) / 2) {
274                chars.push((reg >> 8) as u8);
275                chars.push((reg & 0xFF) as u8);
276            }
277            chars.truncate(len);
278            // Remove null terminator if present
279            while chars.last() == Some(&0) {
280                chars.pop();
281            }
282            let s = String::from_utf8_lossy(&chars).to_string();
283            Ok(ModbusValue::String(s))
284        }
285    }
286}
287
288/// Encode typed values to raw register values
289pub fn encode_value(value: &ModbusValue, data_type: ModbusDataType) -> ModbusResult<Vec<u16>> {
290    match (value, data_type) {
291        (ModbusValue::Int(v), ModbusDataType::Int16) => {
292            let val = *v as i16;
293            Ok(vec![val as u16])
294        }
295        (ModbusValue::Uint(v), ModbusDataType::Uint16) => {
296            let val = *v as u16;
297            Ok(vec![val])
298        }
299        (ModbusValue::Int(v), ModbusDataType::Uint16) => {
300            let val = *v as u16;
301            Ok(vec![val])
302        }
303        (ModbusValue::Int(v), ModbusDataType::Int32) => {
304            let val = *v as i32;
305            Ok(vec![(val >> 16) as u16, (val & 0xFFFF) as u16])
306        }
307        (ModbusValue::Uint(v), ModbusDataType::Uint32) => {
308            let val = *v as u32;
309            Ok(vec![(val >> 16) as u16, (val & 0xFFFF) as u16])
310        }
311        (ModbusValue::Int(v), ModbusDataType::Uint32) => {
312            let val = *v as u32;
313            Ok(vec![(val >> 16) as u16, (val & 0xFFFF) as u16])
314        }
315        (ModbusValue::Float(v), ModbusDataType::Float32) => {
316            let bits = (*v as f32).to_bits();
317            Ok(vec![(bits >> 16) as u16, (bits & 0xFFFF) as u16])
318        }
319        (ModbusValue::Float(v), ModbusDataType::Float64) => {
320            let bits = v.to_bits();
321            Ok(vec![
322                (bits >> 48) as u16,
323                ((bits >> 32) & 0xFFFF) as u16,
324                ((bits >> 16) & 0xFFFF) as u16,
325                (bits & 0xFFFF) as u16,
326            ])
327        }
328        (ModbusValue::Bool(v), ModbusDataType::Bit(bit_num)) => {
329            // Note: This only sets/clears the specified bit
330            // In practice, you'd need to read-modify-write
331            let val = if *v { 1u16 << bit_num } else { 0 };
332            Ok(vec![val])
333        }
334        (ModbusValue::String(s), ModbusDataType::String(len)) => {
335            let mut registers = Vec::with_capacity((len + 1) / 2);
336            let bytes = s.as_bytes();
337            for i in (0..len).step_by(2) {
338                let high = bytes.get(i).copied().unwrap_or(0);
339                let low = bytes.get(i + 1).copied().unwrap_or(0);
340                registers.push(((high as u16) << 8) | (low as u16));
341            }
342            Ok(registers)
343        }
344        _ => Err(ModbusError::Io(std::io::Error::new(
345            std::io::ErrorKind::InvalidInput,
346            format!("Cannot encode {:?} as {:?}", value, data_type),
347        ))),
348    }
349}
350
351// ── Serde support for ModbusDataType ─────────────────────────────────────────
352// Uses the Display/FromStr implementations (e.g. "FLOAT32", "BIT5", "STRING10")
353// for a compact, human-readable serialization.
354
355impl serde::Serialize for ModbusDataType {
356    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
357        serializer.serialize_str(&self.to_string())
358    }
359}
360
361impl<'de> serde::Deserialize<'de> for ModbusDataType {
362    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
363        let s = String::deserialize(deserializer)?;
364        s.parse().map_err(serde::de::Error::custom)
365    }
366}
367
368#[cfg(test)]
369mod tests {
370    use super::*;
371
372    #[test]
373    fn test_data_type_from_str() {
374        assert_eq!(
375            "INT16".parse::<ModbusDataType>().unwrap(),
376            ModbusDataType::Int16
377        );
378        assert_eq!(
379            "UINT16".parse::<ModbusDataType>().unwrap(),
380            ModbusDataType::Uint16
381        );
382        assert_eq!(
383            "FLOAT32".parse::<ModbusDataType>().unwrap(),
384            ModbusDataType::Float32
385        );
386        assert_eq!(
387            "BIT5".parse::<ModbusDataType>().unwrap(),
388            ModbusDataType::Bit(5)
389        );
390        assert_eq!(
391            "STRING10".parse::<ModbusDataType>().unwrap(),
392            ModbusDataType::String(10)
393        );
394    }
395
396    #[test]
397    fn test_decode_int16() {
398        // Positive value
399        let regs = [0x00FF];
400        let val = decode_registers(&regs, ModbusDataType::Int16).unwrap();
401        assert_eq!(val, ModbusValue::Int(255));
402
403        // Negative value (two's complement)
404        let regs = [0xFFFF];
405        let val = decode_registers(&regs, ModbusDataType::Int16).unwrap();
406        assert_eq!(val, ModbusValue::Int(-1));
407    }
408
409    #[test]
410    fn test_decode_uint16() {
411        let regs = [0xFFFF];
412        let val = decode_registers(&regs, ModbusDataType::Uint16).unwrap();
413        assert_eq!(val, ModbusValue::Uint(65535));
414    }
415
416    #[test]
417    fn test_decode_int32() {
418        // Big-endian: 0x0001_0000 = 65536
419        let regs = [0x0001, 0x0000];
420        let val = decode_registers(&regs, ModbusDataType::Int32).unwrap();
421        assert_eq!(val, ModbusValue::Int(65536));
422
423        // Negative: -1
424        let regs = [0xFFFF, 0xFFFF];
425        let val = decode_registers(&regs, ModbusDataType::Int32).unwrap();
426        assert_eq!(val, ModbusValue::Int(-1));
427    }
428
429    #[test]
430    fn test_decode_float32() {
431        // IEEE 754: 1.0 = 0x3F800000
432        let regs = [0x3F80, 0x0000];
433        let val = decode_registers(&regs, ModbusDataType::Float32).unwrap();
434        match val {
435            ModbusValue::Float(v) => assert!((v - 1.0).abs() < 0.0001),
436            _ => panic!("Expected Float"),
437        }
438
439        // IEEE 754: 22.5 = 0x41B40000
440        let regs = [0x41B4, 0x0000];
441        let val = decode_registers(&regs, ModbusDataType::Float32).unwrap();
442        match val {
443            ModbusValue::Float(v) => assert!((v - 22.5).abs() < 0.0001),
444            _ => panic!("Expected Float"),
445        }
446    }
447
448    #[test]
449    fn test_decode_bit() {
450        let regs = [0b0000_0000_0010_0100]; // Bits 2 and 5 are set
451
452        let val = decode_registers(&regs, ModbusDataType::Bit(2)).unwrap();
453        assert_eq!(val, ModbusValue::Bool(true));
454
455        let val = decode_registers(&regs, ModbusDataType::Bit(5)).unwrap();
456        assert_eq!(val, ModbusValue::Bool(true));
457
458        let val = decode_registers(&regs, ModbusDataType::Bit(0)).unwrap();
459        assert_eq!(val, ModbusValue::Bool(false));
460    }
461
462    #[test]
463    fn test_decode_string() {
464        // "AB" = 0x4142
465        let regs = [0x4142, 0x4344];
466        let val = decode_registers(&regs, ModbusDataType::String(4)).unwrap();
467        assert_eq!(val, ModbusValue::String("ABCD".to_string()));
468    }
469
470    #[test]
471    fn test_linear_scaling() {
472        let scaling = LinearScaling::new(0.1, -40.0);
473
474        // Temperature sensor: raw 625 = 22.5°C
475        let raw = 625.0;
476        let physical = scaling.apply(raw);
477        assert!((physical - 22.5).abs() < 0.01);
478
479        // Reverse scaling
480        let back = scaling.reverse(physical);
481        assert!((back - raw).abs() < 0.01);
482    }
483
484    #[test]
485    fn test_scaling_identity() {
486        let identity = LinearScaling::default();
487        assert!(identity.is_identity());
488
489        let non_identity = LinearScaling::new(0.1, 0.0);
490        assert!(!non_identity.is_identity());
491    }
492
493    #[test]
494    fn test_encode_int16() {
495        let val = ModbusValue::Int(-1);
496        let regs = encode_value(&val, ModbusDataType::Int16).unwrap();
497        assert_eq!(regs, vec![0xFFFF]);
498    }
499
500    #[test]
501    fn test_encode_float32() {
502        let val = ModbusValue::Float(1.0);
503        let regs = encode_value(&val, ModbusDataType::Float32).unwrap();
504        assert_eq!(regs, vec![0x3F80, 0x0000]);
505    }
506
507    #[test]
508    fn test_rdf_literal() {
509        assert_eq!(ModbusValue::Int(42).to_rdf_literal(), "42");
510        assert_eq!(ModbusValue::Float(22.5).to_rdf_literal(), "22.5");
511        assert_eq!(ModbusValue::Bool(true).to_rdf_literal(), "true");
512        assert_eq!(
513            ModbusValue::String("test".to_string()).to_rdf_literal(),
514            "test"
515        );
516    }
517
518    #[test]
519    fn test_xsd_datatypes() {
520        assert_eq!(
521            ModbusDataType::Int16.xsd_datatype(),
522            "http://www.w3.org/2001/XMLSchema#short"
523        );
524        assert_eq!(
525            ModbusDataType::Float32.xsd_datatype(),
526            "http://www.w3.org/2001/XMLSchema#float"
527        );
528        assert_eq!(
529            ModbusDataType::Bit(0).xsd_datatype(),
530            "http://www.w3.org/2001/XMLSchema#boolean"
531        );
532    }
533
534    #[test]
535    fn test_register_count() {
536        assert_eq!(ModbusDataType::Int16.register_count(), 1);
537        assert_eq!(ModbusDataType::Float32.register_count(), 2);
538        assert_eq!(ModbusDataType::Float64.register_count(), 4);
539        assert_eq!(ModbusDataType::String(10).register_count(), 5);
540    }
541}