darra-ethercat-master 2.3.0

Commercial EtherCAT master protocol stack, real-time kernel driver integration, Windows and Linux support, multi-language SDKs, complex topology and hot-plug support.
Documentation
//! Hot-Connect 组管理 (ETG.1000.4 / ETG.1020 §8)
//!
//! 对应 C# Master/HotConnect.cs
//!
//! # 语义 "默默支持"
//! - 用户不调用 `add_group`, 所有从站行为 = mandatory (默认; 缺失 → AL_ERROR).
//! - 一旦把从站加入 Hot-Connect 组, 该组缺失就不再算错, Master 可正常进 OP.
//!
//! # 关键约束
//! - Alias Address (ESC 0x0012) 必须预先烧录到从站 EEPROM, 非 0 且组内唯一.
//! - Alias=0 的从站不支持 Hot-Connect (ETG.1020 §8.3).
//!
//! # FFI 路径
//! `HotConnectXxx` 系列 (Add/Remove/GetStatus/ClearAll/GetCount/Enumerate) 一律走
//! [`crate::utils::ffi::dynamic_ffi::ffi_gap()`] 的 `libloading` 运行时解析.
//! Rust extern 静态链接对缺失符号会直接 link error, 因此这些 "FFI gap" 必须懒加载.
//! DLL 缺导出时各方法返回 false / Unknown / 0 / 空 Vec, 调用方安全降级.
//!
//! # 热插拔自修复事件
//! 与 `events::SlaveIdentityMismatch` 共享语义, 这里提供 std 风格的多订阅者注册器
//! `Arc<Mutex<Vec<Box<dyn Fn>>>>`, 跟 `events.rs` 风格一致.

use std::sync::{Arc, Mutex};
use std::os::raw::c_void;
use crate::utils::ffi::dynamic_ffi::ffi_gap;

/// 每个 master 最多支持的 Hot-Connect 组数 (与 C 端 EC_HOTCONNECT_MAX_GROUPS 一致).
pub const MAX_GROUPS: usize = 32;

/// Hot-Connect 组当前状态 (对齐 C# HotConnectStatus).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum HotConnectStatus {
    /// 组未注册或底层查询失败
    Unknown = -1,
    /// 组在当前扫描中未探测到
    Absent = 0,
    /// 组在当前扫描中探测到
    Present = 1,
}

impl HotConnectStatus {
    /// 由 C 层返回值 (>=0 / -1) 转换
    pub fn from_raw(v: i32) -> Self {
        match v {
            1 => Self::Present,
            0 => Self::Absent,
            _ => Self::Unknown,
        }
    }
}

/// Hot-Connect 组的一次性只读快照 (对齐 C# HotConnectGroup).
#[derive(Clone, Debug)]
pub struct HotConnectGroup {
    /// 用户分配的组 ID, 1..65535
    pub group_id: u16,
    /// 期望 Alias 地址 (ESC 0x0012, 必须非 0)
    pub alias_address: u16,
    /// 期望 VendorID, 0=不校验
    pub vendor_id: u32,
    /// 期望 ProductCode, 0=不校验
    pub product_code: u32,
    /// 是否在当前扫描中探测到
    pub is_present: bool,
    /// 探测到时匹配的 slavelist[] 下标, 0=未匹配
    pub detected_slave_index: u16,
}

impl HotConnectGroup {
    /// 状态枚举封装
    pub fn status(&self) -> HotConnectStatus {
        if self.is_present {
            HotConnectStatus::Present
        } else {
            HotConnectStatus::Absent
        }
    }
}

/// C 端 hotconnect_group_t 内存布局 (Pack=1), FFI 补齐后用于 Enumerate.
#[repr(C, packed)]
#[derive(Clone, Copy)]
#[allow(dead_code)]
struct HotConnectGroupNative {
    group_id: u16,
    alias_address: u16,
    vendor_id: u32,
    product_code: u32,
    is_present: i32,
    detected_slave_idx: u16,
    reserved: u16,
}

// ===================== Hot-Connect 管理器 =====================

/// Hot-Connect 组管理器
///
/// 对应 C# `DarraEtherCAT.HotConnect`.
pub struct HotConnect {
    master_index: u16,
}

impl HotConnect {
    /// 创建 Hot-Connect 管理器
    pub fn new(master_index: u16) -> Self {
        Self { master_index }
    }

    /// 注册一个 Hot-Connect 组
    ///
    /// # 参数
    /// * `group_id`     - 组 ID (1..65535, 同 master 唯一)
    /// * `alias`        - 期望 Alias 地址 (必须非 0)
    /// * `vendor_id`    - 期望 VendorID, 0=不校验
    /// * `product_code` - 期望 ProductCode, 0=不校验
    ///
    /// 返回 `true` = DLL 接受组定义; `false` = DLL 未导出该符号或返回拒绝.
    pub fn add_group(&self, group_id: u16, alias: u16,
                     vendor_id: u32, product_code: u32) -> bool {
        match ffi_gap().hot_connect_add_group {
            Some(f) => unsafe { f(self.master_index, group_id, alias, vendor_id, product_code) != 0 },
            None => false,
        }
    }

    /// 删除一个 Hot-Connect 组
    pub fn remove_group(&self, group_id: u16) -> bool {
        match ffi_gap().hot_connect_remove_group {
            Some(f) => unsafe { f(self.master_index, group_id) != 0 },
            None => false,
        }
    }

    /// 查询某个组的当前 Present/Absent 状态
    pub fn group_status(&self, group_id: u16) -> HotConnectStatus {
        match ffi_gap().hot_connect_get_group_status {
            Some(f) => HotConnectStatus::from_raw(unsafe { f(self.master_index, group_id) }),
            None => HotConnectStatus::Unknown,
        }
    }

    /// 清空当前 master 所有 Hot-Connect 组定义
    pub fn clear_all(&self) {
        if let Some(f) = ffi_gap().hot_connect_clear_all {
            unsafe { f(self.master_index) };
        }
    }

    /// 当前已注册组数 (0=无 HC 配置, 所有从站 mandatory)
    pub fn group_count(&self) -> i32 {
        match ffi_gap().hot_connect_get_group_count {
            Some(f) => unsafe { f(self.master_index) },
            None => 0,
        }
    }

    /// 枚举所有已注册的 Hot-Connect 组
    ///
    /// 实现路径:
    /// 1. 分配 `[HotConnectGroupNative; MAX_GROUPS]` 缓冲 (Pack=1, 与 C 端二进制兼容)
    /// 2. 调 `dynamic_ffi.hot_connect_enumerate(master_index, buf.as_mut_ptr(), MAX_GROUPS as c_int)`
    /// 3. 按返回的实际数量逐项 read_unaligned 拷贝到 `Vec<HotConnectGroup>`
    pub fn enumerate(&self) -> Vec<HotConnectGroup> {
        let f = match ffi_gap().hot_connect_enumerate {
            Some(f) => f,
            None => return Vec::new(),
        };
        // 按 C 端布局分配缓冲. packed 结构, 不能直接 .group_id 访问字段, 必须 read_unaligned.
        let mut buf = [HotConnectGroupNative {
            group_id: 0, alias_address: 0, vendor_id: 0, product_code: 0,
            is_present: 0, detected_slave_idx: 0, reserved: 0,
        }; MAX_GROUPS];
        let n = unsafe {
            f(self.master_index, buf.as_mut_ptr() as *mut c_void, MAX_GROUPS as i32)
        };
        if n <= 0 { return Vec::new(); }
        let cnt = (n as usize).min(MAX_GROUPS);
        let mut out = Vec::with_capacity(cnt);
        for i in 0..cnt {
            let entry = &buf[i];
            // packed 结构必须 read_unaligned 避免 UB.
            let gid = unsafe { std::ptr::addr_of!(entry.group_id).read_unaligned() };
            let alias = unsafe { std::ptr::addr_of!(entry.alias_address).read_unaligned() };
            let vid = unsafe { std::ptr::addr_of!(entry.vendor_id).read_unaligned() };
            let pid = unsafe { std::ptr::addr_of!(entry.product_code).read_unaligned() };
            let present = unsafe { std::ptr::addr_of!(entry.is_present).read_unaligned() };
            let det = unsafe { std::ptr::addr_of!(entry.detected_slave_idx).read_unaligned() };
            out.push(HotConnectGroup {
                group_id: gid,
                alias_address: alias,
                vendor_id: vid,
                product_code: pid,
                is_present: present != 0,
                detected_slave_index: det,
            });
        }
        out
    }
}

// ===================== 热插拔自修复事件 (HotPlug) =====================
//
// 与 events::SlaveIdentityMismatchList 共享语义.
// 这里提供独立的 HotPlug 多订阅者注册表, 跟 events.rs 风格一致.

/// HotPlug 自修复事件参数 (与 events::SlaveIdentityMismatch 字段一致)
#[derive(Clone, Copy, Debug)]
pub struct HotPlugIdentityMismatch {
    pub master_index: u16,
    pub slave_index: u16,
    pub expected_vendor: u32,
    pub expected_product: u32,
    pub expected_revision: u32,
    pub actual_vendor: u32,
    pub actual_product: u32,
    pub actual_revision: u32,
}

/// HotPlug 多订阅者回调列表
type HotPlugList = Arc<Mutex<Vec<Box<dyn Fn(HotPlugIdentityMismatch) + Send + 'static>>>>;

/// 全局 HotPlug 订阅表 (对齐 events.rs 风格 — Arc<Mutex<Vec<Box<dyn Fn>>>>)
fn hotplug_list() -> &'static HotPlugList {
    use std::sync::OnceLock;
    static INSTANCE: OnceLock<HotPlugList> = OnceLock::new();
    INSTANCE.get_or_init(|| Arc::new(Mutex::new(Vec::new())))
}

/// 注册 HotPlug 自修复事件回调.
///
/// 多次调用累加多个订阅者, 全部在同一回调线程串行触发.
///
/// # 注意
/// 底层订阅 (DLL `RegisterSlaveIdentityMismatchCallback`) 已由
/// [`crate::master::events::MasterEvents`] 在初始化时挂上;
/// 本函数仅向 HotPlug 自身的订阅表追加 listener.
/// 若需要触发, 请由 events 模块在 IdentityMismatch trampoline 中转发到此处
/// (TODO: events.rs 后续可桥接 trigger_hotplug 调用).
pub fn on_hotplug<F>(callback: F)
where
    F: Fn(HotPlugIdentityMismatch) + Send + 'static,
{
    if let Ok(mut v) = hotplug_list().lock() {
        v.push(Box::new(callback));
    }
}

/// 清空 HotPlug 订阅
pub fn clear_hotplug_listeners() {
    if let Ok(mut v) = hotplug_list().lock() {
        v.clear();
    }
}

/// 向所有订阅者派发 HotPlug 事件 (供 events.rs IdentityMismatch trampoline 调用)
///
/// 多线程安全; 持锁期间不要重新注册 (会死锁).
pub fn dispatch_hotplug(ev: HotPlugIdentityMismatch) {
    let listeners = match hotplug_list().lock() {
        Ok(g) => g,
        Err(p) => p.into_inner(),
    };
    for cb in listeners.iter() {
        cb(ev);
    }
}