Skip to main content

instro_ethernetip/
value.rs

1//! Public value types for this crate's user-facing API.
2//!
3//! [`Value`] intentionally mirrors [`rust_ethernet_ip::PlcValue`], but remains a crate-owned
4//! abstraction so the public API does not expose the backend's types directly. That gives us
5//! room to change or replace the underlying EtherNet/IP implementation later without forcing
6//! a breaking change onto callers.
7//!
8//! This module is responsible for converting between the public value types and the backend
9//! types.
10
11use rust_ethernet_ip::{PlcValue, UdtData};
12
13/// User-facing wrapper for [`rust_ethernet_ip::PlcValue`] returned by this crate.
14///
15/// This API hides the wire-level details exposed by the underlying EtherNet/IP client.
16/// Primitive PLC values are mapped into Rust primitives, backend-decoded strings are exposed
17/// as plain [`String`]s, and user-defined payloads are preserved as opaque
18/// [`StructuredValue`] bytes.
19#[derive(Debug, Clone, PartialEq)]
20pub enum Value {
21    Bool(bool),
22    Sint(i8),
23    Int(i16),
24    Dint(i32),
25    Lint(i64),
26    Usint(u8),
27    Uint(u16),
28    Udint(u32),
29    Ulint(u64),
30    Real(f32),
31    Lreal(f64),
32    String(String),
33    Struct(StructuredValue),
34}
35
36/// User-facing wrapper for [`rust_ethernet_ip::UdtData`].
37///
38/// This lets callers work with structured payloads without exposing the transport
39/// library's raw [`rust_ethernet_ip::UdtData`] type in the user-facing API.
40#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct StructuredValue {
42    /// Symbol type identifier used by [`rust_ethernet_ip::UdtData`].
43    ///
44    /// This is only needed when writing a UDT back to a PLC. Reads convert a backend
45    /// `symbol_id` of `0` into `None`.
46    pub symbol_id: Option<i32>,
47    /// Raw UDT payload bytes from [`rust_ethernet_ip::UdtData::data`].
48    pub data: Vec<u8>,
49}
50
51/// Implements [`From`] conversions into [`Value`] for primitive Rust types and crate-owned
52/// wrappers.
53///
54/// The public [`Value`] enum intentionally mirrors the PLC scalar types this crate exposes.
55/// Most conversions are simple variant wrappers, so this macro keeps those impls consistent
56/// and avoids repeating the same boilerplate for every supported Rust input type.
57macro_rules! impl_value_from {
58    ($($ty:ty => $variant:ident),* $(,)?) => {
59        $(
60            impl From<$ty> for Value {
61                fn from(value: $ty) -> Self {
62                    Self::$variant(value)
63                }
64            }
65        )*
66    };
67}
68
69impl_value_from!(
70    bool => Bool,
71    i8 => Sint,
72    i16 => Int,
73    i32 => Dint,
74    i64 => Lint,
75    u8 => Usint,
76    u16 => Uint,
77    u32 => Udint,
78    u64 => Ulint,
79    f32 => Real,
80    f64 => Lreal,
81    String => String,
82    StructuredValue => Struct,
83);
84
85impl From<&str> for Value {
86    fn from(value: &str) -> Self {
87        Self::String(value.to_owned())
88    }
89}
90
91impl From<PlcValue> for Value {
92    fn from(value: PlcValue) -> Self {
93        match value {
94            PlcValue::Bool(value) => Value::Bool(value),
95            PlcValue::Sint(value) => Value::Sint(value),
96            PlcValue::Int(value) => Value::Int(value),
97            PlcValue::Dint(value) => Value::Dint(value),
98            PlcValue::Lint(value) => Value::Lint(value),
99            PlcValue::Usint(value) => Value::Usint(value),
100            PlcValue::Uint(value) => Value::Uint(value),
101            PlcValue::Udint(value) => Value::Udint(value),
102            PlcValue::Ulint(value) => Value::Ulint(value),
103            PlcValue::Real(value) => Value::Real(value),
104            PlcValue::Lreal(value) => Value::Lreal(value),
105            PlcValue::String(value) => Value::String(value),
106            PlcValue::Udt(udt) => udt.into(),
107        }
108    }
109}
110
111impl From<UdtData> for Value {
112    fn from(udt: UdtData) -> Self {
113        Value::Struct(StructuredValue {
114            symbol_id: (udt.symbol_id != 0).then_some(udt.symbol_id),
115            data: udt.data,
116        })
117    }
118}
119
120impl From<StructuredValue> for UdtData {
121    fn from(value: StructuredValue) -> Self {
122        Self {
123            symbol_id: value.symbol_id.unwrap_or_default(),
124            data: value.data,
125        }
126    }
127}
128
129impl From<StructuredValue> for PlcValue {
130    fn from(value: StructuredValue) -> Self {
131        PlcValue::Udt(value.into())
132    }
133}
134
135impl From<Value> for PlcValue {
136    fn from(value: Value) -> Self {
137        match value {
138            Value::Bool(value) => PlcValue::Bool(value),
139            Value::Sint(value) => PlcValue::Sint(value),
140            Value::Int(value) => PlcValue::Int(value),
141            Value::Dint(value) => PlcValue::Dint(value),
142            Value::Lint(value) => PlcValue::Lint(value),
143            Value::Usint(value) => PlcValue::Usint(value),
144            Value::Uint(value) => PlcValue::Uint(value),
145            Value::Udint(value) => PlcValue::Udint(value),
146            Value::Ulint(value) => PlcValue::Ulint(value),
147            Value::Real(value) => PlcValue::Real(value),
148            Value::Lreal(value) => PlcValue::Lreal(value),
149            Value::String(value) => PlcValue::String(value),
150            Value::Struct(value) => value.into(),
151        }
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158
159    /// Verifies that scalar backend PLC values map to the crate's public value variants.
160    #[test]
161    fn converts_scalar_plc_values() {
162        let cases = vec![
163            (PlcValue::Bool(true), Value::Bool(true)),
164            (PlcValue::Sint(-3), Value::Sint(-3)),
165            (PlcValue::Int(-12), Value::Int(-12)),
166            (PlcValue::Dint(1234), Value::Dint(1234)),
167            (PlcValue::Lint(-5678), Value::Lint(-5678)),
168            (PlcValue::Usint(7), Value::Usint(7)),
169            (PlcValue::Uint(42), Value::Uint(42)),
170            (PlcValue::Udint(99), Value::Udint(99)),
171            (PlcValue::Ulint(123_456), Value::Ulint(123_456)),
172            (PlcValue::Real(1.25), Value::Real(1.25)),
173            (PlcValue::Lreal(-9.5), Value::Lreal(-9.5)),
174            (
175                PlcValue::String("hello".to_owned()),
176                Value::String("hello".to_owned()),
177            ),
178        ];
179
180        for (input, expected) in cases {
181            assert_eq!(Value::from(input), expected);
182        }
183    }
184
185    /// Verifies that backend UDT bytes are exposed as a structured value instead of being decoded implicitly.
186    #[test]
187    fn preserves_non_string_udt_as_structured_value() {
188        let udt = UdtData {
189            symbol_id: 99,
190            data: vec![0xde, 0xad, 0xbe, 0xef],
191        };
192
193        assert_eq!(
194            Value::from(udt),
195            Value::Struct(StructuredValue {
196                symbol_id: Some(99),
197                data: vec![0xde, 0xad, 0xbe, 0xef],
198            })
199        );
200    }
201
202    /// Verifies that structured values are converted back into backend UDT payloads for writes.
203    #[test]
204    fn converts_structured_value_back_to_plc_udt() {
205        let plc_value = PlcValue::from(StructuredValue {
206            symbol_id: Some(42),
207            data: vec![0xaa, 0xbb],
208        });
209
210        assert_eq!(
211            plc_value,
212            PlcValue::Udt(UdtData {
213                symbol_id: 42,
214                data: vec![0xaa, 0xbb],
215            })
216        );
217    }
218
219    /// Verifies that public scalar values convert back to the expected backend PLC value variants.
220    #[test]
221    fn converts_scalar_value_back_to_plc_value() {
222        assert_eq!(PlcValue::from(Value::Bool(true)), PlcValue::Bool(true));
223        assert_eq!(PlcValue::from(Value::Dint(123)), PlcValue::Dint(123));
224        assert_eq!(
225            PlcValue::from(Value::String("hello".to_owned())),
226            PlcValue::String("hello".to_owned())
227        );
228    }
229
230    /// Verifies that common Rust scalar types choose the intended public value variants.
231    #[test]
232    fn converts_rust_scalars_into_value_variants() {
233        assert_eq!(Value::from(true), Value::Bool(true));
234        assert_eq!(Value::from(7_i16), Value::Int(7));
235        assert_eq!(Value::from(3.5_f64), Value::Lreal(3.5));
236        assert_eq!(Value::from("abc"), Value::String("abc".to_owned()));
237    }
238
239    /// Verifies that backend UDT symbol id zero is treated as the absence of a symbol id.
240    #[test]
241    fn preserves_zero_symbol_id_as_none_for_structured_value() {
242        let udt = UdtData {
243            symbol_id: 0,
244            data: vec![1, 2, 3, 4],
245        };
246
247        assert_eq!(
248            Value::from(udt),
249            Value::Struct(StructuredValue {
250                symbol_id: None,
251                data: vec![1, 2, 3, 4],
252            })
253        );
254    }
255}