Skip to main content

rust_ethernet_ip_protocol/
values.rs

1//! Logix atomic and structure type codes and payload codecs.
2
3use bytes::{Buf, BufMut, BytesMut};
4
5use crate::{Decode, Encode, ProtocolError, Result};
6use rust_ethernet_ip_types::{PlcValue, UdtData};
7
8/// CIP type code for `BOOL`.
9pub const BOOL: u16 = 0x00C1;
10/// CIP type code for `SINT`.
11pub const SINT: u16 = 0x00C2;
12/// CIP type code for `INT`.
13pub const INT: u16 = 0x00C3;
14/// CIP type code for `DINT`.
15pub const DINT: u16 = 0x00C4;
16/// CIP type code for `LINT`.
17pub const LINT: u16 = 0x00C5;
18/// CIP type code for `USINT`.
19pub const USINT: u16 = 0x00C6;
20/// CIP type code for `UINT`.
21pub const UINT: u16 = 0x00C7;
22/// CIP type code for `UDINT`.
23pub const UDINT: u16 = 0x00C8;
24/// CIP type code for `ULINT`.
25pub const ULINT: u16 = 0x00C9;
26/// CIP type code for `REAL`.
27pub const REAL: u16 = 0x00CA;
28/// CIP type code for `LREAL`.
29pub const LREAL: u16 = 0x00CB;
30/// CIP type code for a standard string payload.
31pub const STRING: u16 = 0x00CE;
32/// CIP type code for a short string payload.
33pub const ALT_STRING: u16 = 0x00DA;
34/// CIP type code returned for packed `BOOL` array storage.
35pub const BOOL_ARRAY_DWORD: u16 = 0x00D3;
36/// Generic structure type marker.
37pub const UDT: u16 = 0x00A0;
38/// Logix abbreviated structure type marker.
39pub const AB_UDT: u16 = 0x02A0;
40/// Structure handle used by the built-in Logix `STRING` type.
41pub const STANDARD_STRING_HANDLE: u16 = 0x0FCE;
42/// Maximum byte length of the built-in Logix `STRING.DATA` array.
43pub const STANDARD_STRING_DATA_LEN: usize = 82;
44/// Padding bytes following a built-in Logix `STRING.DATA` array.
45pub const STANDARD_STRING_PAD_LEN: usize = 2;
46/// Total encoded payload length of a built-in Logix `STRING`.
47pub const STANDARD_STRING_PAYLOAD_LEN: usize =
48    4 + STANDARD_STRING_DATA_LEN + STANDARD_STRING_PAD_LEN;
49
50/// Returns the CIP type word used for a write request.
51pub fn write_data_type(value: &PlcValue) -> u16 {
52    if let PlcValue::Udt(_) = value {
53        value.known_data_type().unwrap_or(UDT)
54    } else {
55        value.get_data_type()
56    }
57}
58
59/// Encodes the CIP type prefix used for a write request.
60pub fn write_data_type_bytes(value: &PlcValue) -> Vec<u8> {
61    if matches!(value, PlcValue::String(_)) {
62        let mut bytes = Vec::with_capacity(4);
63        bytes.extend_from_slice(&AB_UDT.to_le_bytes());
64        bytes.extend_from_slice(&STANDARD_STRING_HANDLE.to_le_bytes());
65        bytes
66    } else {
67        write_data_type(value).to_le_bytes().to_vec()
68    }
69}
70
71/// Appends a value payload without a type prefix.
72pub fn encode_payload(value: &PlcValue, buf: &mut BytesMut) {
73    match value {
74        PlcValue::Bool(v) => buf.put_u8(if *v { 0xFF } else { 0x00 }),
75        PlcValue::Sint(v) => buf.put_i8(*v),
76        PlcValue::Int(v) => buf.put_i16_le(*v),
77        PlcValue::Dint(v) => buf.put_i32_le(*v),
78        PlcValue::Lint(v) => buf.put_i64_le(*v),
79        PlcValue::Usint(v) => buf.put_u8(*v),
80        PlcValue::Uint(v) => buf.put_u16_le(*v),
81        PlcValue::Udint(v) => buf.put_u32_le(*v),
82        PlcValue::Ulint(v) => buf.put_u64_le(*v),
83        PlcValue::Real(v) => buf.put_slice(&v.to_le_bytes()),
84        PlcValue::Lreal(v) => buf.put_slice(&v.to_le_bytes()),
85        PlcValue::String(v) => encode_standard_string_payload(v, buf),
86        PlcValue::Udt(udt_data) => buf.put_slice(&udt_data.data),
87    }
88}
89
90/// Appends a CIP type prefix followed by the encoded value payload.
91pub fn encode_type_prefixed(value: &PlcValue, buf: &mut BytesMut) {
92    buf.put_slice(&write_data_type_bytes(value));
93    match value {
94        PlcValue::String(v) => encode_standard_string_payload(v, buf),
95        PlcValue::Udt(udt_data) => buf.put_slice(&udt_data.data),
96        _ => encode_payload(value, buf),
97    }
98}
99
100/// Decodes a value payload for the supplied CIP type code.
101pub fn decode_payload(data_type: u16, value_data: &[u8]) -> Result<PlcValue> {
102    match data_type {
103        BOOL => {
104            require_len(value_data, 1, "BOOL")?;
105            Ok(PlcValue::Bool(value_data[0] != 0))
106        }
107        SINT => {
108            require_len(value_data, 1, "SINT")?;
109            Ok(PlcValue::Sint(value_data[0] as i8))
110        }
111        INT => {
112            require_len(value_data, 2, "INT")?;
113            Ok(PlcValue::Int(i16::from_le_bytes([
114                value_data[0],
115                value_data[1],
116            ])))
117        }
118        DINT => {
119            require_len(value_data, 4, "DINT")?;
120            Ok(PlcValue::Dint(i32::from_le_bytes([
121                value_data[0],
122                value_data[1],
123                value_data[2],
124                value_data[3],
125            ])))
126        }
127        LINT => {
128            require_len(value_data, 8, "LINT")?;
129            Ok(PlcValue::Lint(i64::from_le_bytes(
130                value_data[..8]
131                    .try_into()
132                    .expect("length checked before fixed-width LINT decode"),
133            )))
134        }
135        USINT => {
136            require_len(value_data, 1, "USINT")?;
137            Ok(PlcValue::Usint(value_data[0]))
138        }
139        UINT => {
140            require_len(value_data, 2, "UINT")?;
141            Ok(PlcValue::Uint(u16::from_le_bytes([
142                value_data[0],
143                value_data[1],
144            ])))
145        }
146        UDINT => {
147            require_len(value_data, 4, "UDINT")?;
148            Ok(PlcValue::Udint(u32::from_le_bytes([
149                value_data[0],
150                value_data[1],
151                value_data[2],
152                value_data[3],
153            ])))
154        }
155        ULINT => {
156            require_len(value_data, 8, "ULINT")?;
157            Ok(PlcValue::Ulint(u64::from_le_bytes(
158                value_data[..8]
159                    .try_into()
160                    .expect("length checked before fixed-width ULINT decode"),
161            )))
162        }
163        REAL => {
164            require_len(value_data, 4, "REAL")?;
165            Ok(PlcValue::Real(f32::from_le_bytes([
166                value_data[0],
167                value_data[1],
168                value_data[2],
169                value_data[3],
170            ])))
171        }
172        LREAL => {
173            require_len(value_data, 8, "LREAL")?;
174            Ok(PlcValue::Lreal(f64::from_le_bytes(
175                value_data[..8]
176                    .try_into()
177                    .expect("length checked before fixed-width LREAL decode"),
178            )))
179        }
180        STRING => decode_dint_string(value_data),
181        ALT_STRING => decode_short_string(value_data),
182        AB_UDT | UDT => decode_structure_payload(value_data),
183        BOOL_ARRAY_DWORD => {
184            if value_data.len() >= 4 {
185                Ok(PlcValue::Udint(u32::from_le_bytes([
186                    value_data[0],
187                    value_data[1],
188                    value_data[2],
189                    value_data[3],
190                ])))
191            } else {
192                Err(ProtocolError::new(
193                    "Insufficient data for DWORD value".to_string(),
194                ))
195            }
196        }
197        _ => Err(ProtocolError::new(format!(
198            "Unsupported data type: 0x{data_type:04X}"
199        ))),
200    }
201}
202
203/// Decodes one array element for the supplied CIP element type.
204pub fn decode_array_element(data_type: u16, chunk: &[u8]) -> Result<PlcValue> {
205    decode_payload(data_type, chunk)
206}
207
208fn encode_standard_string_payload(value: &str, buf: &mut BytesMut) {
209    let string_bytes = value.as_bytes();
210    let data_len = string_bytes.len().min(STANDARD_STRING_DATA_LEN);
211    buf.put_u32_le(data_len as u32);
212    buf.put_slice(&string_bytes[..data_len]);
213    buf.resize(buf.len() + (STANDARD_STRING_DATA_LEN - data_len), 0);
214    buf.resize(buf.len() + STANDARD_STRING_PAD_LEN, 0);
215}
216
217fn decode_structure_payload(value_data: &[u8]) -> Result<PlcValue> {
218    if value_data.len() >= 2 {
219        let handle = u16::from_le_bytes([value_data[0], value_data[1]]);
220        if handle == STANDARD_STRING_HANDLE {
221            return decode_standard_string_structure(value_data);
222        }
223    }
224
225    Ok(PlcValue::Udt(UdtData {
226        symbol_id: 0,
227        data: value_data.to_vec(),
228    }))
229}
230
231fn decode_standard_string_structure(value_data: &[u8]) -> Result<PlcValue> {
232    let required = 2 + STANDARD_STRING_PAYLOAD_LEN;
233    if value_data.len() < required {
234        return Err(ProtocolError::new(format!(
235            "Insufficient data for standard STRING structure: need {required} bytes, have {} bytes",
236            value_data.len()
237        )));
238    }
239
240    let payload = &value_data[2..];
241    let length = u32::from_le_bytes([payload[0], payload[1], payload[2], payload[3]]) as usize;
242    if length > STANDARD_STRING_DATA_LEN {
243        return Err(ProtocolError::new(format!(
244            "Invalid standard STRING length: {length} > {STANDARD_STRING_DATA_LEN}"
245        )));
246    }
247
248    Ok(PlcValue::String(
249        String::from_utf8_lossy(&payload[4..4 + length]).to_string(),
250    ))
251}
252
253fn decode_dint_string(value_data: &[u8]) -> Result<PlcValue> {
254    if value_data.len() < 4 {
255        return Err(ProtocolError::new(
256            "Insufficient data for STRING length field".to_string(),
257        ));
258    }
259
260    let length =
261        u32::from_le_bytes([value_data[0], value_data[1], value_data[2], value_data[3]]) as usize;
262    if value_data.len() - 4 < length {
263        return Err(ProtocolError::new(format!(
264            "Insufficient data for STRING value: need {} bytes, have {} bytes",
265            4 + length,
266            value_data.len()
267        )));
268    }
269    Ok(PlcValue::String(
270        String::from_utf8_lossy(&value_data[4..4 + length]).to_string(),
271    ))
272}
273
274fn decode_short_string(value_data: &[u8]) -> Result<PlcValue> {
275    if value_data.is_empty() {
276        return Ok(PlcValue::String(String::new()));
277    }
278    let length = value_data[0] as usize;
279    if value_data.len() < 1 + length {
280        return Err(ProtocolError::new(
281            "Insufficient data for STRING value".to_string(),
282        ));
283    }
284    Ok(PlcValue::String(
285        String::from_utf8_lossy(&value_data[1..1 + length]).to_string(),
286    ))
287}
288
289fn require_len(value_data: &[u8], min_len: usize, name: &str) -> Result<()> {
290    if value_data.len() < min_len {
291        let msg = if min_len == 1 {
292            format!("No data for {name} value")
293        } else {
294            format!("Insufficient data for {name} value")
295        };
296        Err(ProtocolError::new(msg))
297    } else {
298        Ok(())
299    }
300}
301
302impl Encode for PlcValue {
303    fn encode(&self, buf: &mut BytesMut) {
304        encode_type_prefixed(self, buf);
305    }
306}
307
308impl Decode for PlcValue {
309    fn decode(buf: &mut impl Buf) -> Result<Self> {
310        if buf.remaining() < 2 {
311            return Err(ProtocolError::new("Data too short for type".to_string()));
312        }
313        let data_type = buf.get_u16_le();
314        let remaining = buf.copy_to_bytes(buf.remaining());
315        decode_payload(data_type, &remaining)
316    }
317}
318
319impl Encode for UdtData {
320    fn encode(&self, buf: &mut BytesMut) {
321        buf.put_slice(&self.data);
322    }
323}
324
325impl Decode for UdtData {
326    fn decode(buf: &mut impl Buf) -> Result<Self> {
327        let data = buf.copy_to_bytes(buf.remaining()).to_vec();
328        Ok(Self { symbol_id: 0, data })
329    }
330}