#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum EsiPhysicsPortType {
NotUsed = 0,
Mii = 1,
EBus = 2,
HotConnect = 3,
}
impl From<u8> for EsiPhysicsPortType {
fn from(val: u8) -> Self {
match val {
0 => Self::NotUsed,
1 => Self::Mii,
2 => Self::EBus,
3 => Self::HotConnect,
_ => Self::NotUsed,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum EsiRevisionCheckStrategy {
None,
Eq,
EqOrG,
LwEq,
HwEq,
LwEqOrG,
HwEqOrG,
}
pub fn parse_uint_hex_or_dec(s: &str) -> Option<u32> {
let s = s.trim();
if s.is_empty() {
return None;
}
if let Some(hex) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
return u32::from_str_radix(hex, 16).ok();
}
if let Some(hex) = s.strip_prefix("#x").or_else(|| s.strip_prefix("#X")) {
return u32::from_str_radix(hex, 16).ok();
}
if let Some(hex) = s.strip_prefix('#') {
return u32::from_str_radix(hex, 16).ok();
}
if let Ok(val) = s.parse::<u32>() {
return Some(val);
}
if s.chars().all(|c| c.is_ascii_hexdigit()) && s.chars().any(|c| c.is_ascii_alphabetic()) {
return u32::from_str_radix(s, 16).ok();
}
None
}
pub fn parse_u16_hex_or_dec(s: &str) -> Option<u16> {
parse_uint_hex_or_dec(s).map(|v| v as u16)
}
#[derive(Debug, Clone, Default)]
pub struct EsiBootstrapInfo {
pub recv_mailbox_offset: u16,
pub recv_mailbox_size: u16,
pub send_mailbox_offset: u16,
pub send_mailbox_size: u16,
}
#[derive(Debug, Clone, Default)]
pub struct EsiCoEDetails {
pub complete_access: bool,
pub diag_history: bool,
pub pdo_assign: bool,
pub pdo_config: bool,
pub sdo_info: bool,
pub segmented_sdo: bool,
}
#[derive(Debug, Clone, Default)]
pub struct EsiDcOpMode {
pub assign_activate: u16,
pub cycle_time_sync0: u32,
pub cycle_time_sync0_factor: i32,
pub cycle_time_sync1: u32,
pub cycle_time_sync1_factor: i32,
pub description: String,
pub name: String,
pub shift_time_sync0: i32,
pub shift_time_sync1: i32,
}
impl EsiDcOpMode {
pub fn is_dc_sync(&self) -> bool { self.assign_activate != 0 }
pub fn sync0_enabled(&self) -> bool { (self.assign_activate & 0x0100) != 0 }
pub fn sync1_enabled(&self) -> bool { (self.assign_activate & 0x0400) != 0 }
}
#[derive(Debug, Clone, Default)]
pub struct EsiDcConfiguration {
pub op_modes: Vec<EsiDcOpMode>,
pub unknown_64bit: bool,
}
impl EsiDcConfiguration {
pub fn dc_sync_op_mode(&self) -> Option<&EsiDcOpMode> {
self.op_modes.iter().find(|m| m.assign_activate != 0)
}
}
#[derive(Debug, Clone, Default)]
pub struct EsiEepromConfiguration {
pub bootstrap: Option<EsiBootstrapInfo>,
pub byte_size: i32,
pub config_data: String,
}
#[derive(Debug, Clone, Default)]
pub struct EsiElectricalInfo {
pub ebus_current: i32,
}
impl EsiElectricalInfo {
pub fn is_power_supply(&self) -> bool { self.ebus_current < 0 }
pub fn power_consumption(&self) -> i32 { if self.is_power_supply() { 0 } else { self.ebus_current } }
}
#[derive(Debug, Clone, Default)]
pub struct EsiIdentification {
pub identification_ado: u16,
pub identification_value: u16,
}
#[derive(Debug, Clone)]
pub struct EsiMailboxTimeout {
pub request_timeout: i32,
pub response_timeout: i32,
}
impl Default for EsiMailboxTimeout {
fn default() -> Self { Self { request_timeout: 100, response_timeout: 2000 } }
}
#[derive(Debug, Clone, Default)]
pub struct EsiPdoEntry {
pub bit_length: u8,
pub data_type: String,
pub index: u16,
pub name: String,
pub sub_index: u8,
}
#[derive(Debug, Clone, Default)]
pub struct EsiPdoInfo {
pub entries: Vec<EsiPdoEntry>,
pub exclude_indices: Vec<u16>,
pub index: u16,
pub is_fixed: bool,
pub is_mandatory: bool,
pub name: String,
pub sync_manager: u8,
}
#[derive(Debug, Clone, Default)]
pub struct EsiPdoConfiguration {
pub rx_pdos: Vec<EsiPdoInfo>,
pub tx_pdos: Vec<EsiPdoInfo>,
}
#[derive(Debug, Clone)]
pub struct EsiPhysicsPort {
pub code: char,
pub description: String,
pub index: usize,
pub port_type: EsiPhysicsPortType,
}
#[derive(Debug, Clone, Default)]
pub struct EsiPhysicsInfo {
pub ports: Vec<EsiPhysicsPort>,
pub raw_value: String,
}
impl EsiPhysicsInfo {
pub fn parse(physics: &str) -> Self {
let mut info = Self { raw_value: physics.to_string(), ports: Vec::new() };
for (i, ch) in physics.chars().enumerate() {
let (pt, desc) = match ch.to_ascii_uppercase() {
'Y' => (EsiPhysicsPortType::Mii, "MII (100Mbit)"),
'K' => (EsiPhysicsPortType::EBus, "E-Bus"),
'H' => (EsiPhysicsPortType::HotConnect, "Hot Connect"),
_ => (EsiPhysicsPortType::NotUsed, "Not Used"),
};
info.ports.push(EsiPhysicsPort { code: ch, description: desc.to_string(), index: i, port_type: pt });
}
info
}
}
#[derive(Debug, Clone, Default)]
pub struct EsiPortInfo {
pub index: i32,
pub label: String,
pub port_type: String,
}
#[derive(Debug, Clone, Default)]
pub struct EsiStartupSdo {
pub data_type: String,
pub index: u16,
pub name: String,
pub sub_index: u8,
pub value: String,
}
#[derive(Debug, Clone, Default)]
pub struct EsiStatistics {
pub total_devices: i32,
pub total_files: i32,
pub vendors: Vec<String>,
}
#[derive(Debug, Clone, Default)]
pub struct EsiSyncManagerInfo {
pub control_byte: u8,
pub default_size: u16,
pub enable: bool,
pub index: i32,
pub max_size: u16,
pub min_size: u16,
pub name: String,
pub start_address: u16,
}
impl EsiSyncManagerInfo {
pub fn sdo_index(&self) -> u16 { 0x1C10 + self.index as u16 }
}
#[derive(Debug, Clone, Default)]
pub struct EsiDeviceInfo {
pub coe_details: Option<EsiCoEDetails>,
pub dc_configuration: Option<EsiDcConfiguration>,
pub device_class: String,
pub eeprom_configuration: Option<EsiEepromConfiguration>,
pub electrical: Option<EsiElectricalInfo>,
pub file_name: String,
pub group_type: String,
pub identification: Option<EsiIdentification>,
pub mailbox_timeout: Option<EsiMailboxTimeout>,
pub physics: String,
pub physics_info: Option<EsiPhysicsInfo>,
pub ports: Vec<EsiPortInfo>,
pub product_id: u32,
pub product_name: String,
pub product_text: String,
pub product_url: String,
pub profile_number: String,
pub revision_check_strategy: EsiRevisionCheckStrategy,
pub revision_id: u32,
pub supports_coe: bool,
pub supports_eoe: bool,
pub supports_foe: bool,
pub supports_soe: bool,
pub supports_voe: bool,
pub supports_frame_repeat: bool,
pub vendor_comment: String,
pub vendor_id: u32,
pub vendor_name: String,
}
impl EsiDeviceInfo {
pub fn dc_assign_activate(&self) -> u16 {
self.dc_configuration.as_ref()
.and_then(|dc| dc.dc_sync_op_mode())
.map(|m| m.assign_activate)
.unwrap_or(0)
}
pub fn supports_dc(&self) -> bool {
self.dc_configuration.as_ref()
.map(|dc| dc.op_modes.iter().any(|m| m.is_dc_sync()))
.unwrap_or(false)
}
pub fn protocol_summary(&self) -> String {
let mut p = Vec::new();
if self.supports_coe { p.push("CoE"); }
if self.supports_foe { p.push("FoE"); }
if self.supports_eoe { p.push("EoE"); }
if self.supports_soe { p.push("SoE"); }
if self.supports_voe { p.push("VoE"); }
if p.is_empty() { "None".to_string() } else { p.join(", ") }
}
}
impl Default for EsiRevisionCheckStrategy {
fn default() -> Self { Self::None }
}
pub fn calculate_eeprom_crc(data: &[u8], length: usize) -> u8 {
let mut crc: u8 = 0xFF;
for i in 0..length.min(data.len()) {
crc ^= data[i];
for _ in 0..8 {
if crc & 0x80 != 0 {
crc = (crc << 1) ^ 0x07;
} else {
crc <<= 1;
}
}
}
crc
}
pub fn validate_eeprom_crc(eeprom_data: &[u8]) -> bool {
if eeprom_data.len() < 15 { return false; }
calculate_eeprom_crc(eeprom_data, 14) == eeprom_data[14]
}
pub fn get_config_data_bytes(config_data_hex: &str) -> Vec<u8> {
let hex = config_data_hex.trim();
if hex.is_empty() {
return Vec::new();
}
let mut result = Vec::with_capacity(hex.len() / 2);
for i in (0..hex.len()).step_by(2) {
if i + 1 < hex.len() {
if let Ok(b) = u8::from_str_radix(&hex[i..i + 2], 16) {
result.push(b);
}
}
}
result
}
pub fn match_revision(actual: u32, expected: u32, strategy: EsiRevisionCheckStrategy) -> bool {
if expected == 0 { return true; }
match strategy {
EsiRevisionCheckStrategy::None => true,
EsiRevisionCheckStrategy::Eq => actual == expected,
EsiRevisionCheckStrategy::EqOrG => actual >= expected,
EsiRevisionCheckStrategy::LwEq => (actual & 0xFFFF) == (expected & 0xFFFF),
EsiRevisionCheckStrategy::HwEq => (actual >> 16) == (expected >> 16),
EsiRevisionCheckStrategy::LwEqOrG => (actual & 0xFFFF) >= (expected & 0xFFFF),
EsiRevisionCheckStrategy::HwEqOrG => (actual >> 16) >= (expected >> 16),
}
}
use std::collections::HashSet;
use std::ffi::CString;
use std::sync::Mutex;
static ESI_MANAGER_FILES: Mutex<Option<HashSet<String>>> = Mutex::new(None);
fn ensure_files_init(set: &mut Option<HashSet<String>>) -> &mut HashSet<String> {
if set.is_none() {
*set = Some(HashSet::new());
}
set.as_mut().unwrap()
}
pub struct EsiManager;
impl EsiManager {
pub fn default_path() -> String {
if let Ok(cwd) = std::env::current_dir() {
let dir = cwd.join("ESI");
if !dir.exists() {
let _ = std::fs::create_dir_all(&dir);
}
return dir.to_string_lossy().into_owned();
}
"./ESI".to_string()
}
pub fn add_file(file_path: &str) -> i32 {
if file_path.is_empty() {
return 0;
}
if let Ok(mut g) = ESI_MANAGER_FILES.lock() {
ensure_files_init(&mut g).insert(file_path.to_string());
}
let cpath = match CString::new(file_path) {
Ok(c) => c,
Err(_) => return 0,
};
unsafe { crate::utils::ffi::EcEsi_LoadFile(cpath.as_ptr()) }
}
pub fn load_path(dir_path: &str) -> i32 {
if dir_path.is_empty() {
return 0;
}
if let Ok(entries) = std::fs::read_dir(dir_path) {
if let Ok(mut g) = ESI_MANAGER_FILES.lock() {
let set = ensure_files_init(&mut g);
for e in entries.flatten() {
let path = e.path();
let name_lc = path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("")
.to_lowercase();
if name_lc.ends_with(".xml") || name_lc.ends_with(".esi") {
set.insert(path.to_string_lossy().into_owned());
}
}
}
}
let cpath = match CString::new(dir_path) {
Ok(c) => c,
Err(_) => return 0,
};
unsafe { crate::utils::ffi::EcEsi_LoadDirectory(cpath.as_ptr()) }
}
pub fn match_revision(actual: u32, expected: u32, strategy: EsiRevisionCheckStrategy) -> bool {
match_revision(actual, expected, strategy)
}
pub fn bind_to_slave(master_index: u16, slave_index: u16, file_path: &str) -> i32 {
let cpath = match CString::new(file_path) {
Ok(c) => c,
Err(_) => return 0,
};
unsafe {
crate::utils::ffi::EcEsi_BindToSlave(master_index, slave_index, cpath.as_ptr())
}
}
pub fn apply_all_slaves(master_index: u16) -> i32 {
unsafe { crate::utils::ffi::EcEsi_ApplyAllSlaves(master_index) }
}
pub fn get_loaded_count() -> i32 {
let dll_n = unsafe { crate::utils::ffi::EcEsi_GetLoadedCount() };
if dll_n > 0 {
return dll_n;
}
if let Ok(g) = ESI_MANAGER_FILES.lock() {
return g.as_ref().map(|s| s.len() as i32).unwrap_or(0);
}
0
}
pub fn get_files() -> Vec<String> {
if let Ok(g) = ESI_MANAGER_FILES.lock() {
if let Some(set) = g.as_ref() {
return set.iter().cloned().collect();
}
}
Vec::new()
}
pub fn clear() {
if let Ok(mut g) = ESI_MANAGER_FILES.lock() {
if let Some(set) = g.as_mut() {
set.clear();
}
}
unsafe { crate::utils::ffi::EcEsi_Clear() }
}
}
pub fn load_eni(path: &str) -> Result<crate::utils::xml::MasterXmlConfiguration, String> {
crate::utils::xml::load_xml_configuration(path)
}
pub fn save_eni(path: &str,
config: &crate::utils::xml::MasterXmlConfiguration) -> Result<(), String> {
use quick_xml::writer::Writer;
use quick_xml::events::{BytesDecl, BytesEnd, BytesStart, BytesText, Event};
use std::io::Cursor;
if path.trim().is_empty() {
return Err("save_eni: path 为空".to_string());
}
if let Some(parent) = std::path::Path::new(path).parent() {
if !parent.as_os_str().is_empty() && !parent.exists() {
std::fs::create_dir_all(parent)
.map_err(|e| format!("save_eni: 创建父目录失败: {}", e))?;
}
}
let mut buf: Vec<u8> = Vec::new();
{
let cursor = Cursor::new(&mut buf);
let mut w = Writer::new_with_indent(cursor, b' ', 2);
w.write_event(Event::Decl(BytesDecl::new("1.0", Some("UTF-8"), None)))
.map_err(|e| format!("save_eni: 写 XML 声明失败: {}", e))?;
fn write_text<W: std::io::Write>(
w: &mut Writer<W>, tag: &str, text: &str,
) -> Result<(), String> {
w.write_event(Event::Start(BytesStart::new(tag)))
.map_err(|e| format!("save_eni: 写 <{}> 失败: {}", tag, e))?;
w.write_event(Event::Text(BytesText::new(text)))
.map_err(|e| format!("save_eni: 写 <{}> 文本失败: {}", tag, e))?;
w.write_event(Event::End(BytesEnd::new(tag)))
.map_err(|e| format!("save_eni: 写 </{}> 失败: {}", tag, e))?;
Ok(())
}
let mut root = BytesStart::new("EtherCATConfig");
root.push_attribute(("Version", "1.0"));
root.push_attribute(("xmlns:deni", "http://www.darra-tech.com/schemas/eni/v3.0"));
w.write_event(Event::Start(root))
.map_err(|e| format!("save_eni: 写 <EtherCATConfig> 失败: {}", e))?;
w.write_event(Event::Start(BytesStart::new("Config")))
.map_err(|e| format!("save_eni: 写 <Config> 失败: {}", e))?;
w.write_event(Event::Start(BytesStart::new("Master")))
.map_err(|e| format!("save_eni: 写 <Master> 失败: {}", e))?;
w.write_event(Event::Start(BytesStart::new("Info")))
.map_err(|e| format!("save_eni: 写 <Info> 失败: {}", e))?;
write_text(&mut w, "Name", "DarraEtherCAT Master")?;
write_text(&mut w, "Source", "DarraEtherCAT Rust SDK SaveENI")?;
write_text(&mut w, "VendorId", "#x00001164")?;
write_text(&mut w, "ProductCode", "#x00000001")?;
w.write_event(Event::End(BytesEnd::new("Info")))
.map_err(|e| format!("save_eni: 写 </Info> 失败: {}", e))?;
write_text(&mut w, "CycleTime", &config.cycle_time_us.to_string())?;
write_text(&mut w, "DcCycleTime", &config.dc_cycle_ns.to_string())?;
let mut deni_master = BytesStart::new("deni:MasterConfig");
deni_master.push_attribute(("xmlns:deni", "http://www.darra-tech.com/schemas/eni/v3.0"));
w.write_event(Event::Start(deni_master))
.map_err(|e| format!("save_eni: 写 <deni:MasterConfig> 失败: {}", e))?;
{
let mut ct = BytesStart::new("deni:CycleTime");
ct.push_attribute(("unit", "us"));
w.write_event(Event::Start(ct))
.map_err(|e| format!("save_eni: 写 deni:CycleTime 失败: {}", e))?;
w.write_event(Event::Text(BytesText::new(&config.cycle_time_us.to_string())))
.map_err(|e| format!("save_eni: 写 deni:CycleTime 文本失败: {}", e))?;
w.write_event(Event::End(BytesEnd::new("deni:CycleTime")))
.map_err(|e| format!("save_eni: 写 </deni:CycleTime> 失败: {}", e))?;
let mut dct = BytesStart::new("deni:DcCycleTime");
dct.push_attribute(("unit", "ns"));
w.write_event(Event::Start(dct))
.map_err(|e| format!("save_eni: 写 deni:DcCycleTime 失败: {}", e))?;
w.write_event(Event::Text(BytesText::new(&config.dc_cycle_ns.to_string())))
.map_err(|e| format!("save_eni: 写 deni:DcCycleTime 文本失败: {}", e))?;
w.write_event(Event::End(BytesEnd::new("deni:DcCycleTime")))
.map_err(|e| format!("save_eni: 写 </deni:DcCycleTime> 失败: {}", e))?;
write_text(&mut w, "deni:ExpectedSlaveCount",
&config.expected_slave_count.to_string())?;
}
w.write_event(Event::End(BytesEnd::new("deni:MasterConfig")))
.map_err(|e| format!("save_eni: 写 </deni:MasterConfig> 失败: {}", e))?;
w.write_event(Event::End(BytesEnd::new("Master")))
.map_err(|e| format!("save_eni: 写 </Master> 失败: {}", e))?;
let mut auto_inc_counter: i32 = 0;
for sl in &config.slaves {
w.write_event(Event::Start(BytesStart::new("Slave")))
.map_err(|e| format!("save_eni: 写 <Slave> 失败: {}", e))?;
w.write_event(Event::Start(BytesStart::new("Info")))
.map_err(|e| format!("save_eni: 写 <Slave>/<Info> 失败: {}", e))?;
write_text(&mut w, "Name", &sl.name)?;
write_text(&mut w, "PhysAddr", &sl.config_addr.to_string())?;
write_text(&mut w, "AutoIncAddr", &(-auto_inc_counter).to_string())?;
auto_inc_counter += 1;
write_text(&mut w, "AliasAddr", &sl.alias_addr.to_string())?;
write_text(&mut w, "VendorId", &format!("#x{:08X}", sl.vendor_id))?;
write_text(&mut w, "ProductCode", &format!("#x{:08X}", sl.product_code))?;
write_text(&mut w, "RevisionNo", &format!("#x{:08X}", sl.revision))?;
w.write_event(Event::End(BytesEnd::new("Info")))
.map_err(|e| format!("save_eni: 写 </Info> 失败: {}", e))?;
if sl.input_size > 0 || sl.output_size > 0 {
w.write_event(Event::Start(BytesStart::new("ProcessData")))
.map_err(|e| format!("save_eni: 写 <ProcessData> 失败: {}", e))?;
if sl.output_size > 0 {
let mut sm = BytesStart::new("Sm");
sm.push_attribute(("No", "2"));
w.write_event(Event::Start(sm))
.map_err(|e| format!("save_eni: 写 <Sm No=2> 失败: {}", e))?;
write_text(&mut w, "DefaultSize", &sl.output_size.to_string())?;
write_text(&mut w, "StartAddress", "0")?;
w.write_event(Event::End(BytesEnd::new("Sm")))
.map_err(|e| format!("save_eni: 写 </Sm> 失败: {}", e))?;
}
if sl.input_size > 0 {
let mut sm = BytesStart::new("Sm");
sm.push_attribute(("No", "3"));
w.write_event(Event::Start(sm))
.map_err(|e| format!("save_eni: 写 <Sm No=3> 失败: {}", e))?;
write_text(&mut w, "DefaultSize", &sl.input_size.to_string())?;
write_text(&mut w, "StartAddress", "0")?;
w.write_event(Event::End(BytesEnd::new("Sm")))
.map_err(|e| format!("save_eni: 写 </Sm> 失败: {}", e))?;
}
w.write_event(Event::End(BytesEnd::new("ProcessData")))
.map_err(|e| format!("save_eni: 写 </ProcessData> 失败: {}", e))?;
}
let dc_match = config.slave_dc_configs.iter()
.find(|c| c.slave_index as i32 == sl.index);
if let Some(dc) = dc_match {
if dc.sync0_cycle_ns != 0 || dc.sync1_cycle_ns != 0 {
w.write_event(Event::Start(BytesStart::new("Dc")))
.map_err(|e| format!("save_eni: 写 <Dc> 失败: {}", e))?;
let aa: u16 = if dc.sync1_cycle_ns != 0 { 0x0300 } else { 0x0100 };
write_text(&mut w, "AssignActivate", &format!("#x{:04X}", aa))?;
write_text(&mut w, "CycleTimeSync0", &dc.sync0_cycle_ns.to_string())?;
write_text(&mut w, "CycleTimeSync1", &dc.sync1_cycle_ns.to_string())?;
write_text(&mut w, "ShiftTimeSync0", &dc.shift_ns.to_string())?;
w.write_event(Event::End(BytesEnd::new("Dc")))
.map_err(|e| format!("save_eni: 写 </Dc> 失败: {}", e))?;
}
}
w.write_event(Event::End(BytesEnd::new("Slave")))
.map_err(|e| format!("save_eni: 写 </Slave> 失败: {}", e))?;
}
w.write_event(Event::End(BytesEnd::new("Config")))
.map_err(|e| format!("save_eni: 写 </Config> 失败: {}", e))?;
w.write_event(Event::End(BytesEnd::new("EtherCATConfig")))
.map_err(|e| format!("save_eni: 写 </EtherCATConfig> 失败: {}", e))?;
}
std::fs::write(path, &buf)
.map_err(|e| format!("save_eni: 写文件 {} 失败: {}", path, e))?;
Ok(())
}