use crate::error::{EtherNetIpError, Result};
use crate::PlcValue;
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct UdtDefinition {
pub name: String,
pub members: Vec<UdtMember>,
}
#[derive(Debug, Clone)]
pub struct UdtMember {
pub name: String,
pub data_type: u16,
pub offset: u32,
pub size: u32,
}
#[derive(Debug, Clone)]
pub struct UdtTemplate {
pub template_id: u32,
pub name: String,
pub size: u32,
pub member_count: u16,
pub members: Vec<UdtMember>,
}
#[derive(Debug, Clone)]
pub struct TagAttributes {
pub name: String,
pub data_type: u16,
pub data_type_name: String,
pub dimensions: Vec<u32>,
pub permissions: TagPermissions,
pub scope: TagScope,
pub template_instance_id: Option<u32>,
pub size: u32,
}
#[derive(Debug, Clone, PartialEq)]
pub enum TagPermissions {
ReadOnly,
ReadWrite,
WriteOnly,
Unknown,
}
#[derive(Debug, Clone, PartialEq)]
pub enum TagScope {
Controller,
Program(String),
Unknown,
}
#[derive(Debug)]
pub struct UdtManager {
definitions: HashMap<String, UdtDefinition>,
templates: HashMap<u32, UdtTemplate>,
tag_attributes: HashMap<String, TagAttributes>,
}
impl UdtManager {
pub fn new() -> Self {
Self {
definitions: HashMap::new(),
templates: HashMap::new(),
tag_attributes: HashMap::new(),
}
}
pub fn add_definition(&mut self, definition: UdtDefinition) {
self.definitions.insert(definition.name.clone(), definition);
}
pub fn get_definition(&self, name: &str) -> Option<&UdtDefinition> {
self.definitions.get(name)
}
pub fn add_template(&mut self, template: UdtTemplate) {
self.templates.insert(template.template_id, template);
}
pub fn get_template(&self, template_id: u32) -> Option<&UdtTemplate> {
self.templates.get(&template_id)
}
pub fn add_tag_attributes(&mut self, attributes: TagAttributes) {
self.tag_attributes
.insert(attributes.name.clone(), attributes);
}
pub fn get_tag_attributes(&self, name: &str) -> Option<&TagAttributes> {
self.tag_attributes.get(name)
}
pub fn list_definitions(&self) -> Vec<String> {
self.definitions.keys().cloned().collect()
}
pub fn list_templates(&self) -> Vec<u32> {
self.templates.keys().cloned().collect()
}
pub fn list_tag_attributes(&self) -> Vec<String> {
self.tag_attributes.keys().cloned().collect()
}
pub fn clear_cache(&mut self) {
self.definitions.clear();
self.templates.clear();
self.tag_attributes.clear();
}
pub fn parse_udt_template(&self, template_id: u32, data: &[u8]) -> Result<UdtTemplate> {
if data.len() < 8 {
return Err(EtherNetIpError::Protocol(
"UDT template data too short".to_string(),
));
}
let mut offset = 0;
let structure_size = u32::from_le_bytes([
data[offset],
data[offset + 1],
data[offset + 2],
data[offset + 3],
]);
offset += 4;
let member_count = u16::from_le_bytes([data[offset], data[offset + 1]]);
offset += 2;
offset += 2;
let mut members = Vec::new();
let mut current_offset = 0u32;
for i in 0..member_count {
if offset + 8 > data.len() {
return Err(EtherNetIpError::Protocol(format!(
"UDT template member {} data incomplete",
i
)));
}
let member_info = u32::from_le_bytes([
data[offset],
data[offset + 1],
data[offset + 2],
data[offset + 3],
]);
offset += 4;
let member_name_length = u16::from_le_bytes([data[offset], data[offset + 1]]);
offset += 2;
offset += 2;
let data_type = (member_info & 0xFFFF) as u16;
let _dimensions = ((member_info >> 16) & 0xFF) as u8;
if offset + member_name_length as usize > data.len() {
return Err(EtherNetIpError::Protocol(format!(
"UDT template member {} name data incomplete",
i
)));
}
let name_bytes = &data[offset..offset + member_name_length as usize];
let member_name = String::from_utf8_lossy(name_bytes).to_string();
offset += member_name_length as usize;
offset = (offset + 3) & !3;
let member_size = self.get_data_type_size(data_type);
let member = UdtMember {
name: member_name,
data_type,
offset: current_offset,
size: member_size,
};
members.push(member);
current_offset += member_size;
}
Ok(UdtTemplate {
template_id,
name: format!("Template_{}", template_id),
size: structure_size,
member_count,
members,
})
}
fn get_data_type_size(&self, data_type: u16) -> u32 {
match data_type {
0x00C1 => 1, 0x00C2 => 1, 0x00C3 => 2, 0x00C4 => 4, 0x00C5 => 8, 0x00C6 => 1, 0x00C7 => 2, 0x00C8 => 4, 0x00C9 => 8, 0x00CA => 4, 0x00CB => 8, 0x00CE => 88, _ => 4, }
}
pub fn parse_tag_attributes(&self, tag_name: &str, data: &[u8]) -> Result<TagAttributes> {
if data.len() < 8 {
return Err(EtherNetIpError::Protocol(
"Tag attributes data too short".to_string(),
));
}
let mut offset = 0;
let data_type = u16::from_le_bytes([data[offset], data[offset + 1]]);
offset += 2;
let size = u32::from_le_bytes([
data[offset],
data[offset + 1],
data[offset + 2],
data[offset + 3],
]);
offset += 4;
let mut dimensions = Vec::new();
if data.len() > offset {
let dimension_count = data[offset] as usize;
offset += 1;
for _ in 0..dimension_count {
if offset + 4 <= data.len() {
let dim = u32::from_le_bytes([
data[offset],
data[offset + 1],
data[offset + 2],
data[offset + 3],
]);
dimensions.push(dim);
offset += 4;
}
}
}
let permissions = TagPermissions::ReadWrite;
let scope = if tag_name.contains(':') {
let parts: Vec<&str> = tag_name.split(':').collect();
if parts.len() >= 2 {
TagScope::Program(parts[0].to_string())
} else {
TagScope::Controller
}
} else {
TagScope::Controller
};
let data_type_name = self.get_data_type_name(data_type);
let template_instance_id = if data_type == 0x00A0 {
Some(0) } else {
None
};
Ok(TagAttributes {
name: tag_name.to_string(),
data_type,
data_type_name,
dimensions,
permissions,
scope,
template_instance_id,
size,
})
}
fn get_data_type_name(&self, data_type: u16) -> String {
match data_type {
0x00C1 => "BOOL".to_string(),
0x00C2 => "SINT".to_string(),
0x00C3 => "INT".to_string(),
0x00C4 => "DINT".to_string(),
0x00C5 => "LINT".to_string(),
0x00C6 => "USINT".to_string(),
0x00C7 => "UINT".to_string(),
0x00C8 => "UDINT".to_string(),
0x00C9 => "ULINT".to_string(),
0x00CA => "REAL".to_string(),
0x00CB => "LREAL".to_string(),
0x00CE => "STRING".to_string(),
0x00A0 => "UDT".to_string(),
_ => format!("UNKNOWN(0x{:04X})", data_type),
}
}
pub fn parse_udt_instance(&self, _udt_name: &str, data: &[u8]) -> Result<PlcValue> {
Ok(PlcValue::Udt(crate::UdtData {
symbol_id: 0, data: data.to_vec(),
}))
}
pub fn serialize_udt_instance(
&self,
_udt_value: &HashMap<String, PlcValue>,
) -> Result<Vec<u8>> {
Err(crate::error::EtherNetIpError::Protocol(
"UDT instance serialization is not implemented yet".to_string(),
))
}
}
impl Default for UdtManager {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct UserDefinedType {
pub name: String,
pub size: u32,
pub members: Vec<UdtMember>,
member_offsets: HashMap<String, u32>,
}
impl UserDefinedType {
pub fn new(name: String) -> Self {
Self {
name,
size: 0,
members: Vec::new(),
member_offsets: HashMap::new(),
}
}
pub fn add_member(&mut self, member: UdtMember) {
self.member_offsets
.insert(member.name.clone(), member.offset);
self.members.push(member);
self.size = self
.members
.iter()
.map(|m| m.offset + m.size)
.max()
.unwrap_or(0);
}
pub fn get_member_offset(&self, name: &str) -> Option<u32> {
self.member_offsets.get(name).copied()
}
pub fn from_cip_data(_data: &[u8]) -> crate::error::Result<Self> {
Err(crate::error::EtherNetIpError::Protocol(
"UDT CIP definition parsing is not implemented yet".to_string(),
))
}
pub fn to_hash_map(&self, data: &[u8]) -> crate::error::Result<HashMap<String, PlcValue>> {
if data.is_empty() {
return Err(crate::error::EtherNetIpError::Protocol(
"UDT data is empty".to_string(),
));
}
let mut result = HashMap::new();
for member in &self.members {
let offset = member.offset as usize;
if offset + member.size as usize <= data.len() {
let member_data = &data[offset..offset + member.size as usize];
let value = self.parse_member_value(member, member_data)?;
result.insert(member.name.clone(), value);
}
}
Ok(result)
}
pub fn from_hash_map(
&self,
values: &HashMap<String, PlcValue>,
) -> crate::error::Result<Vec<u8>> {
let mut data = vec![0u8; self.size as usize];
for member in &self.members {
if let Some(value) = values.get(&member.name) {
let member_data = self.serialize_member_value(member, value)?;
let offset = member.offset as usize;
let end_offset = offset + member_data.len();
if end_offset <= data.len() {
data[offset..end_offset].copy_from_slice(&member_data);
} else {
return Err(crate::error::EtherNetIpError::Protocol(format!(
"Member {} data exceeds UDT size",
member.name
)));
}
}
}
Ok(data)
}
pub fn read_member(&self, data: &[u8], member_name: &str) -> crate::error::Result<PlcValue> {
if let Some(member) = self.members.iter().find(|m| m.name == member_name) {
let offset = member.offset as usize;
if offset + member.size as usize <= data.len() {
let member_data = &data[offset..offset + member.size as usize];
self.parse_member_value(member, member_data)
} else {
Err(crate::error::EtherNetIpError::Protocol(format!(
"Member {} data incomplete",
member_name
)))
}
} else {
Err(crate::error::EtherNetIpError::TagNotFound(format!(
"UDT member '{}' not found",
member_name
)))
}
}
pub fn write_member(
&self,
data: &mut [u8],
member_name: &str,
value: &PlcValue,
) -> crate::error::Result<()> {
if let Some(member) = self.members.iter().find(|m| m.name == member_name) {
let member_data = self.serialize_member_value(member, value)?;
let offset = member.offset as usize;
let end_offset = offset + member_data.len();
if end_offset <= data.len() {
data[offset..end_offset].copy_from_slice(&member_data);
Ok(())
} else {
Err(crate::error::EtherNetIpError::Protocol(format!(
"Member {} data exceeds UDT size",
member_name
)))
}
} else {
Err(crate::error::EtherNetIpError::TagNotFound(format!(
"UDT member '{}' not found",
member_name
)))
}
}
pub fn get_member_size(&self, member_name: &str) -> Option<u32> {
self.members
.iter()
.find(|m| m.name == member_name)
.map(|m| m.size)
}
pub fn get_member_data_type(&self, member_name: &str) -> Option<u16> {
self.members
.iter()
.find(|m| m.name == member_name)
.map(|m| m.data_type)
}
pub fn parse_member_value(
&self,
member: &UdtMember,
data: &[u8],
) -> crate::error::Result<PlcValue> {
match member.data_type {
0x00C1 => {
if data.is_empty() {
return Err(crate::error::EtherNetIpError::Protocol(
"BOOL data too short".to_string(),
));
}
Ok(PlcValue::Bool(data[0] != 0))
}
0x00C2 => {
if data.is_empty() {
return Err(crate::error::EtherNetIpError::Protocol(
"SINT data too short".to_string(),
));
}
Ok(PlcValue::Sint(data[0] as i8))
}
0x00C3 => {
if data.len() < 2 {
return Err(crate::error::EtherNetIpError::Protocol(
"INT data too short".to_string(),
));
}
let mut bytes = [0u8; 2];
bytes.copy_from_slice(&data[..2]);
Ok(PlcValue::Int(i16::from_le_bytes(bytes)))
}
0x00C4 => {
if data.len() < 4 {
return Err(crate::error::EtherNetIpError::Protocol(
"DINT data too short".to_string(),
));
}
let mut bytes = [0u8; 4];
bytes.copy_from_slice(&data[..4]);
Ok(PlcValue::Dint(i32::from_le_bytes(bytes)))
}
0x00C5 => {
if data.len() < 8 {
return Err(crate::error::EtherNetIpError::Protocol(
"LINT data too short".to_string(),
));
}
let mut bytes = [0u8; 8];
bytes.copy_from_slice(&data[..8]);
Ok(PlcValue::Lint(i64::from_le_bytes(bytes)))
}
0x00C6 => {
if data.is_empty() {
return Err(crate::error::EtherNetIpError::Protocol(
"USINT data too short".to_string(),
));
}
Ok(PlcValue::Usint(data[0]))
}
0x00C7 => {
if data.len() < 2 {
return Err(crate::error::EtherNetIpError::Protocol(
"UINT data too short".to_string(),
));
}
let mut bytes = [0u8; 2];
bytes.copy_from_slice(&data[..2]);
Ok(PlcValue::Uint(u16::from_le_bytes(bytes)))
}
0x00C8 => {
if data.len() < 4 {
return Err(crate::error::EtherNetIpError::Protocol(
"UDINT data too short".to_string(),
));
}
let mut bytes = [0u8; 4];
bytes.copy_from_slice(&data[..4]);
Ok(PlcValue::Udint(u32::from_le_bytes(bytes)))
}
0x00C9 => {
if data.len() < 8 {
return Err(crate::error::EtherNetIpError::Protocol(
"ULINT data too short".to_string(),
));
}
let mut bytes = [0u8; 8];
bytes.copy_from_slice(&data[..8]);
Ok(PlcValue::Ulint(u64::from_le_bytes(bytes)))
}
0x00CA => {
if data.len() < 4 {
return Err(crate::error::EtherNetIpError::Protocol(
"REAL data too short".to_string(),
));
}
let mut bytes = [0u8; 4];
bytes.copy_from_slice(&data[..4]);
Ok(PlcValue::Real(f32::from_le_bytes(bytes)))
}
0x00CB => {
if data.len() < 8 {
return Err(crate::error::EtherNetIpError::Protocol(
"LREAL data too short".to_string(),
));
}
let mut bytes = [0u8; 8];
bytes.copy_from_slice(&data[..8]);
Ok(PlcValue::Lreal(f64::from_le_bytes(bytes)))
}
0x00CE => {
if data.len() < 4 {
return Err(crate::error::EtherNetIpError::Protocol(
"STRING data too short".to_string(),
));
}
let length = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
if data.len() - 4 < length {
return Err(crate::error::EtherNetIpError::Protocol(
"STRING data incomplete".to_string(),
));
}
let string_data = &data[4..4 + length];
let string_value = String::from_utf8_lossy(string_data).to_string();
Ok(PlcValue::String(string_value))
}
_ => Err(crate::error::EtherNetIpError::Protocol(format!(
"Unsupported UDT data type: 0x{:04X}",
member.data_type
))),
}
}
pub fn serialize_member_value(
&self,
member: &UdtMember,
value: &PlcValue,
) -> crate::error::Result<Vec<u8>> {
match member.data_type {
0x00C1 => match value {
PlcValue::Bool(b) => Ok(vec![if *b { 0xFF } else { 0x00 }]),
_ => Err(crate::error::EtherNetIpError::DataTypeMismatch {
expected: "BOOL".to_string(),
actual: format!("{:?}", value),
}),
},
0x00C2 => match value {
PlcValue::Sint(s) => Ok(vec![*s as u8]),
_ => Err(crate::error::EtherNetIpError::DataTypeMismatch {
expected: "SINT".to_string(),
actual: format!("{:?}", value),
}),
},
0x00C3 => match value {
PlcValue::Int(i) => Ok(i.to_le_bytes().to_vec()),
_ => Err(crate::error::EtherNetIpError::DataTypeMismatch {
expected: "INT".to_string(),
actual: format!("{:?}", value),
}),
},
0x00C4 => match value {
PlcValue::Dint(d) => Ok(d.to_le_bytes().to_vec()),
_ => Err(crate::error::EtherNetIpError::DataTypeMismatch {
expected: "DINT".to_string(),
actual: format!("{:?}", value),
}),
},
0x00C5 => match value {
PlcValue::Lint(l) => Ok(l.to_le_bytes().to_vec()),
_ => Err(crate::error::EtherNetIpError::DataTypeMismatch {
expected: "LINT".to_string(),
actual: format!("{:?}", value),
}),
},
0x00C6 => match value {
PlcValue::Usint(u) => Ok(vec![*u]),
_ => Err(crate::error::EtherNetIpError::DataTypeMismatch {
expected: "USINT".to_string(),
actual: format!("{:?}", value),
}),
},
0x00C7 => match value {
PlcValue::Uint(u) => Ok(u.to_le_bytes().to_vec()),
_ => Err(crate::error::EtherNetIpError::DataTypeMismatch {
expected: "UINT".to_string(),
actual: format!("{:?}", value),
}),
},
0x00C8 => match value {
PlcValue::Udint(u) => Ok(u.to_le_bytes().to_vec()),
_ => Err(crate::error::EtherNetIpError::DataTypeMismatch {
expected: "UDINT".to_string(),
actual: format!("{:?}", value),
}),
},
0x00C9 => match value {
PlcValue::Ulint(u) => Ok(u.to_le_bytes().to_vec()),
_ => Err(crate::error::EtherNetIpError::DataTypeMismatch {
expected: "ULINT".to_string(),
actual: format!("{:?}", value),
}),
},
0x00CA => match value {
PlcValue::Real(r) => Ok(r.to_le_bytes().to_vec()),
_ => Err(crate::error::EtherNetIpError::DataTypeMismatch {
expected: "REAL".to_string(),
actual: format!("{:?}", value),
}),
},
0x00CB => match value {
PlcValue::Lreal(l) => Ok(l.to_le_bytes().to_vec()),
_ => Err(crate::error::EtherNetIpError::DataTypeMismatch {
expected: "LREAL".to_string(),
actual: format!("{:?}", value),
}),
},
0x00CE => {
match value {
PlcValue::String(s) => {
let mut result = Vec::new();
let max_data_len = member.size.saturating_sub(4); let max_chars = (max_data_len as usize).min(82); let length = (s.len() as u32).min(max_chars as u32);
result.extend_from_slice(&length.to_le_bytes());
result.extend_from_slice(&s.as_bytes()[..length as usize]);
while result.len() < member.size as usize && result.len() % 2 != 0 {
result.push(0);
}
if result.len() > member.size as usize {
result.truncate(member.size as usize);
}
Ok(result)
}
_ => Err(crate::error::EtherNetIpError::DataTypeMismatch {
expected: "STRING".to_string(),
actual: format!("{:?}", value),
}),
}
}
_ => Err(crate::error::EtherNetIpError::Protocol(format!(
"Unsupported UDT data type for serialization: 0x{:04X}",
member.data_type
))),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_udt_member_offsets() {
let mut udt = UserDefinedType::new("TestUDT".to_string());
udt.add_member(UdtMember {
name: "Bool1".to_string(),
data_type: 0x00C1,
offset: 0,
size: 1,
});
udt.add_member(UdtMember {
name: "Dint1".to_string(),
data_type: 0x00C4,
offset: 4,
size: 4,
});
assert_eq!(udt.get_member_offset("Bool1"), Some(0));
assert_eq!(udt.get_member_offset("Dint1"), Some(4));
assert_eq!(udt.size, 8);
}
#[test]
fn test_udt_parsing() {
let mut udt = UserDefinedType::new("TestUDT".to_string());
udt.add_member(UdtMember {
name: "Bool1".to_string(),
data_type: 0x00C1,
offset: 0,
size: 1,
});
udt.add_member(UdtMember {
name: "Dint1".to_string(),
data_type: 0x00C4,
offset: 4,
size: 4,
});
let data = vec![0xFF, 0x00, 0x00, 0x00, 0x2A, 0x00, 0x00, 0x00];
let result = udt.to_hash_map(&data).unwrap();
assert_eq!(result.get("Bool1"), Some(&PlcValue::Bool(true)));
assert_eq!(result.get("Dint1"), Some(&PlcValue::Dint(42)));
}
#[test]
fn test_from_cip_data_returns_explicit_error_until_implemented() {
let result = UserDefinedType::from_cip_data(&[0x01, 0x02, 0x03]);
assert!(result.is_err());
let error_text = result.err().unwrap().to_string();
assert!(error_text.contains("not implemented"));
}
#[test]
fn test_serialize_udt_instance_returns_explicit_error_until_implemented() {
let manager = UdtManager::new();
let values = HashMap::new();
let result = manager.serialize_udt_instance(&values);
assert!(result.is_err());
let error_text = result.err().unwrap().to_string();
assert!(error_text.contains("not implemented"));
}
}