use rusty_modbus_types::{DeviceIdCode, MeiType};
use crate::error::DecodeError;
fn valid_conformity_level(level: u8) -> bool {
matches!(level, 0x01 | 0x02 | 0x03 | 0x81 | 0x82 | 0x83)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DeviceIdObjectEntry<'buf> {
pub id: u8,
pub value: &'buf [u8],
}
#[derive(Debug)]
pub struct ReadDeviceIdentificationResponse<'buf> {
pub device_id_code: DeviceIdCode,
pub conformity_level: u8,
pub more_follows: bool,
pub next_object_id: u8,
pub num_objects: u8,
pub object_data: &'buf [u8],
}
impl<'buf> ReadDeviceIdentificationResponse<'buf> {
pub fn decode(data: &'buf [u8]) -> Result<Self, DecodeError> {
if data.len() < 6 {
return Err(DecodeError::Truncated {
expected: 6,
actual: data.len(),
});
}
if data[0] != MeiType::ReadDeviceIdentification.code() {
return Err(DecodeError::UnknownMeiType(data[0]));
}
let device_id_code =
DeviceIdCode::from_raw(data[1]).ok_or(DecodeError::InvalidDeviceIdCode(data[1]))?;
let conformity_level = data[2];
if !valid_conformity_level(conformity_level) {
return Err(DecodeError::InvalidDeviceIdConformityLevel(
conformity_level,
));
}
let more_follows = match data[3] {
0x00 => false,
0xFF => true,
value => return Err(DecodeError::InvalidDeviceIdMoreFollows(value)),
};
let next_object_id = data[4];
if !more_follows && next_object_id != 0 {
return Err(DecodeError::InvalidDeviceIdNextObjectId(next_object_id));
}
let num_objects = data[5];
if device_id_code == DeviceIdCode::Individual && num_objects != 1 {
return Err(DecodeError::InvalidDeviceIdObjectCount(num_objects));
}
let object_data = &data[6..];
let mut offset = 0;
for _ in 0..num_objects {
if offset + 2 > object_data.len() {
return Err(DecodeError::Truncated {
expected: 6 + offset + 2,
actual: data.len(),
});
}
let obj_len = object_data[offset + 1] as usize;
offset += 2 + obj_len;
if offset > object_data.len() {
return Err(DecodeError::Truncated {
expected: 6 + offset,
actual: data.len(),
});
}
}
if offset != object_data.len() {
return Err(DecodeError::LengthMismatch {
expected: 6 + offset,
actual: data.len(),
});
}
Ok(Self {
device_id_code,
conformity_level,
more_follows,
next_object_id,
num_objects,
object_data,
})
}
#[must_use]
pub fn objects(&self) -> DeviceIdObjectIter<'buf> {
DeviceIdObjectIter {
data: self.object_data,
remaining: self.num_objects,
}
}
}
pub struct DeviceIdObjectIter<'buf> {
data: &'buf [u8],
remaining: u8,
}
impl<'buf> Iterator for DeviceIdObjectIter<'buf> {
type Item = DeviceIdObjectEntry<'buf>;
fn next(&mut self) -> Option<Self::Item> {
if self.remaining == 0 || self.data.len() < 2 {
return None;
}
let id = self.data[0];
let len = self.data[1] as usize;
if self.data.len() < 2 + len {
self.remaining = 0;
self.data = &[];
return None;
}
let value = &self.data[2..2 + len];
self.data = &self.data[2 + len..];
self.remaining -= 1;
Some(DeviceIdObjectEntry { id, value })
}
}
#[cfg(test)]
mod tests {
extern crate alloc;
use alloc::vec::Vec;
use super::*;
#[test]
fn decode_single_object_response() {
let data = [
0x0E, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x05, b'h', b'e', b'l', b'l', b'o',
];
let resp = ReadDeviceIdentificationResponse::decode(&data).unwrap();
assert_eq!(resp.device_id_code, DeviceIdCode::BasicStream);
assert!(!resp.more_follows);
assert_eq!(resp.num_objects, 1);
let objs: Vec<_> = resp.objects().collect();
assert_eq!(objs[0].id, 0x00);
assert_eq!(objs[0].value, b"hello");
}
#[test]
fn decode_multiple_objects() {
let data = [
0x0E, 0x01, 0x01, 0x00, 0x00, 0x03, 0x00, 0x04, b't', b'e', b's', b't', 0x01, 0x03,
b'X', b'Y', b'Z', 0x02, 0x05, b'1', b'.', b'0', b'.', b'0',
];
let resp = ReadDeviceIdentificationResponse::decode(&data).unwrap();
let objs: Vec<_> = resp.objects().collect();
assert_eq!(objs.len(), 3);
assert_eq!(objs[0].value, b"test");
assert_eq!(objs[1].value, b"XYZ");
assert_eq!(objs[2].value, b"1.0.0");
}
#[test]
fn decode_more_follows() {
let data = [0x0E, 0x01, 0x01, 0xFF, 0x02, 0x01, 0x00, 0x02, b'O', b'K'];
let resp = ReadDeviceIdentificationResponse::decode(&data).unwrap();
assert!(resp.more_follows);
assert_eq!(resp.next_object_id, 0x02);
}
#[test]
fn decode_truncated_header() {
let data = [0x0E, 0x01, 0x01];
assert!(matches!(
ReadDeviceIdentificationResponse::decode(&data),
Err(DecodeError::Truncated { .. })
));
}
#[test]
fn decode_truncated_object() {
let data = [0x0E, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x05, b'h', b'i'];
assert!(matches!(
ReadDeviceIdentificationResponse::decode(&data),
Err(DecodeError::Truncated { .. })
));
}
#[test]
fn decode_rejects_invalid_conformity_level() {
let data = [0x0E, 0x01, 0x04, 0x00, 0x00, 0x00];
assert!(matches!(
ReadDeviceIdentificationResponse::decode(&data),
Err(DecodeError::InvalidDeviceIdConformityLevel(0x04))
));
}
#[test]
fn decode_rejects_invalid_more_follows_value() {
let data = [0x0E, 0x01, 0x01, 0x01, 0x00, 0x00];
assert!(matches!(
ReadDeviceIdentificationResponse::decode(&data),
Err(DecodeError::InvalidDeviceIdMoreFollows(0x01))
));
}
#[test]
fn decode_rejects_next_object_id_without_more_follows() {
let data = [0x0E, 0x01, 0x01, 0x00, 0x02, 0x00];
assert!(matches!(
ReadDeviceIdentificationResponse::decode(&data),
Err(DecodeError::InvalidDeviceIdNextObjectId(0x02))
));
}
#[test]
fn decode_rejects_individual_response_without_one_object() {
let data = [0x0E, 0x04, 0x81, 0x00, 0x00, 0x00];
assert!(matches!(
ReadDeviceIdentificationResponse::decode(&data),
Err(DecodeError::InvalidDeviceIdObjectCount(0))
));
}
#[test]
fn object_iterator_stops_on_malformed_manual_response() {
let resp = ReadDeviceIdentificationResponse {
device_id_code: DeviceIdCode::BasicStream,
conformity_level: 0x01,
more_follows: false,
next_object_id: 0,
num_objects: 1,
object_data: &[0x00, 0x05, b'h', b'i'],
};
assert!(resp.objects().next().is_none());
}
}