邮箱网关 (Mailbox Gateway)
邮箱网关实现 ETG.8200 标准,允许外部诊断工具通过 UDP/IP 访问 EtherCAT 主站和从站的对象字典。
产品入口 MailboxGateway::new(master.index())。master.mailbox_gateway() 返回 MailboxGatewayService。
邮箱网关服务符合 ETG.8200 标准规范,使用标准 UDP 端口 0x88A4 (34980),帧结构完全符合 Table 1 要求。
服务控制
port() / set_port()
pub fn port(&self) -> u16
pub fn set_port(&mut self, port: u16)
UDP 端口号,默认 0x88A4 (34980)。启动后会更新为实际绑定的端口。
运行多个主站实例时,端口会根据 master_number(主站编号,从 1 起)自动偏移,避免端口冲突:
- 主站 1:
34980(默认) - 主站 N:
34980 + (N - 1)
如需手动指定端口,在 start() 前调用 set_port() 即可覆盖自动值。
端口号必须在服务启动前设置。服务运行时无法更改端口。如果期望端口被占用,会自动回退到可用端口,port() 会更新为实际值。
is_running()
pub fn is_running(&self) -> bool
服务是否正在运行。
start()
pub fn start(&mut self) -> Result<()>
启动邮箱网关服务,开始监听 UDP 请求。如果期望端口被占用,会自动尝试备用端口。
示例:
let mut gw = master.mailbox_gateway();
gw.start().unwrap();
println!("邮箱网关已启动,端口: {}", gw.port());
stop()
pub fn stop(&mut self)
停止邮箱网关服务,释放 UDP 端口。停止后可再次调用 start() 重新启动。
支持的协议
| 协议 | 类型 | 说明 |
|---|---|---|
| CoE | 0x03 | SDO 读写,支持主站(ETG.1510)和从站对象字典 |
| FoE | 0x04 | 文件传输(当前仅支持单包) |
| SoE | 0x05 | IDN 参数读写(ETG.1020 标准) |
| VoE | 0x0F | 厂商特定协议透传 |
- Address =
0x0000-> 访问主站对象字典(仅 CoE) - Address = 从站站地址 -> 透传到对应从站(CoE / SoE / FoE / VoE)
从站邮箱操作默认超时 5 秒(微秒精度传给底层),短读立即返回。
帧结构
EtherCAT Header:
- Length: Mailbox Header + Data 的总长度
- Data Type: 固定为 0x05 (Mailbox communication)
Mailbox Header:
- Length: 邮箱数据部分长度
- Address: 0x0000=主站, 其他=从站站地址
- Type: 0x03=CoE, 0x04=SoE, 0x05=FoE, 0x0F=VoE
- Cnt: 邮箱计数器 (1-7 循环)
运行时统计 (MailboxGatewayStats)
获取邮箱网关自启动以来的累计统计 (收发包量 / 路由计数 / 丢弃计数), 用于监控面板与告警阈值。通过 MailboxGatewayService::get_statistics() 实时取快照, reset_statistics() 清零。
get_statistics() / reset_statistics()
pub fn get_statistics(&self) -> MailboxGatewayStats
pub fn reset_statistics(&self)
get_statistics() 返回当前累计统计的快照 —— 内部使用原子计数, 无需加锁, 可在任何线程频繁调用; reset_statistics() 将所有计数清零。
MailboxGatewayStats 结构
#[derive(Debug, Default, Clone, Copy)]
pub struct MailboxGatewayStats {
pub rx_packets: u64, // 收到的 UDP 包数
pub tx_packets: u64, // 发送的响应包数
pub rx_bytes: u64, // 收到的字节数
pub tx_bytes: u64, // 发送的字节数
pub master_od_requests: u64, // 路由到主站 OD 的请求数
pub slave_requests: u64, // 路由到从站邮箱的请求数
pub drop_malformed: u64, // 帧格式错误丢弃数
pub drop_timeout: u64, // 发送失败丢弃数(非从站响应超时)
}
示例:
let mut gw = master.mailbox_gateway();
gw.start().unwrap();
// ... 客户端发请求一段时间 ...
let s = gw.get_statistics();
println!(
"收包 {} / 发包 {} | 主站OD={} 从站={} | 丢弃(格式错误)={} 丢弃(超时)={}",
s.rx_packets, s.tx_packets,
s.master_od_requests, s.slave_requests,
s.drop_malformed, s.drop_timeout,
);
let total = s.rx_packets;
if total > 0 {
let dropped = s.drop_malformed + s.drop_timeout;
let rate = dropped as f64 / total as f64;
if rate > 0.05 {
eprintln!("警告: 邮箱网关丢弃率过高 {:.1}%", rate * 100.0);
}
}
MailboxGateway
薄封装 native MBG FFI(UDP/TCP 收发、路由、统计由 DLL 内核侧承载)。与上方 MailboxGatewayService(Rust 用户态实现)择一使用。
MailboxGateway::new()
pub fn new(master_index: u16) -> Self
创建 native 邮箱网关句柄。
参数:
master_index(u16) — 主站索引
示例:
use darra_ethercat::MailboxGateway;
let gw = MailboxGateway::new(master.index());
gw.start_default()?;
start() / start_default()
pub fn start(&self, udp_port: u16) -> Result<()>
pub fn start_default(&self) -> Result<()>
启动 UDP 邮箱网关(native MBGStart)。start_default() 使用标准端口 0x88A4 (34980)。
参数:
udp_port(u16) — UDP 端口
返回值:
Result<()>— DLL 非 0 成功;失败Err,不包装成 Ok
stop()
pub fn stop(&self) -> Result<()>
停止 UDP 邮箱网关(native MBGStop)。失败走 Err。
is_running() / port()
pub fn is_running(&self) -> bool
pub fn port(&self) -> u16
is_running() 查询 UDP 网关是否运行(native MBGIsRunning)。port() 取实际 UDP 端口(native MBGGetPort)。
statistics() / reset_statistics()
pub fn statistics(&self) -> Option<MbgNativeStats>
pub fn reset_statistics(&self)
statistics() 取 native 统计快照(MBGGetStats),DLL 失败返回 None。reset_statistics() 清零(MBGResetStats,无返回值)。
相关结构:
pub struct MbgNativeStats {
pub rx_packets: u64,
pub rx_bytes: u64,
pub tx_packets: u64,
pub tx_bytes: u64,
pub forwarded_ok: u64,
pub forwarded_failed: u64,
pub timeout_count: u64,
pub invalid_frame: u64,
pub unknown_target: u64,
pub unsupported_type: u64,
pub master_requests: u64,
pub udp_port: u16,
pub is_running: bool,
pub discovery_requests: u64,
pub tcp_connections: u64,
pub tcp_active: u64,
pub tcp_port: u16,
pub tcp_running: bool,
pub selected_master: u8,
pub master_od_requests: u64,
pub slave_requests: u64,
pub drop_malformed: u64,
pub drop_timeout: u64,
}
示例:
let gw = MailboxGateway::new(master.index());
gw.start_default()?;
if let Some(s) = gw.statistics() {
println!("收包 {} / 发包 {}", s.rx_packets, s.tx_packets);
}
start_tcp() / stop_tcp()
pub fn start_tcp(&self, tcp_port: u16) -> Result<()>
pub fn stop_tcp(&self) -> Result<()>
启动 / 停止 TCP 邮箱网关(native MBGStartTcp / MBGStopTcp)。失败走 Err。
参数:
tcp_port(u16) — TCP 端口
tcp_is_running() / tcp_port()
pub fn tcp_is_running(&self) -> bool
pub fn tcp_port(&self) -> u16
查询 TCP 通道是否运行,以及实际 TCP 端口。
selected_master() / set_selected_master()
pub fn selected_master(&self) -> u16
pub fn set_selected_master(&self, selected: u16) -> Result<()>
读取 / 设置 Discovery 选中的主站索引。set_selected_master 失败走 Err。
完整示例
场景 A — 集成现有配置/调试工具
让第三方配置工具(或工程师工具)通过 UDP 访问主站/从站对象字典,无需修改被控设备固件。
场景 B — 自定义诊断或自动化脚本
通过脚本/服务定期采集从站参数、批量读取对象或执行远程配置。
场景 C — 远程监控与告警采集
把网关用于远程被动监控(周期性读取或订阅),并把关键数据汇报到集中监控系统。
启动邮箱网关
use darra_ethercat::EtherCATMaster;
let mut master = EtherCATMaster::from_json_file("config.json")?;
master.set_state(EcState::Operational)?;
// 启动邮箱网关(可选:自定义端口)
let mut gw = master.mailbox_gateway();
// gw.set_port(12345); // 默认 34980,如需修改请在 start 前设置
gw.start().unwrap();
println!("邮箱网关已启动,端口: {}", gw.port());
// PDO 自动在后台线程运行,网关在 OP 状态下提供邮箱服务
// ... 等待中 ...
gw.stop();
// master 超出作用域时自动释放
外部 Rust 客户端
use std::net::UdpSocket;
/// ETG.8200 邮箱网关 UDP 客户端
struct MailboxGatewayClient {
socket: UdpSocket,
gateway_addr: String,
}
impl MailboxGatewayClient {
fn new(host: &str, port: u16) -> Self {
let socket = UdpSocket::bind("0.0.0.0:0").unwrap();
socket.set_read_timeout(Some(std::time::Duration::from_secs(3))).unwrap();
Self {
socket,
gateway_addr: format!("{}:{}", host, port),
}
}
/// 构建 ETG.8200 帧: EtherCAT Header (2) + Mailbox Header (6) + Data
fn build_frame(&self, address: u16, mb_type: u8, mb_data: &[u8]) -> Vec<u8> {
let mb_len = mb_data.len() as u16;
let ecat_len = 6 + mb_len;
let mut frame = Vec::with_capacity(2 + 6 + mb_data.len());
// EtherCAT Header: Length[10:0] | DataType[15:12]=0x05
let ecat_header: u16 = (0x05 << 12) | (ecat_len & 0x07FF);
frame.extend_from_slice(&ecat_header.to_le_bytes());
// Mailbox Header
frame.extend_from_slice(&mb_len.to_le_bytes()); // Length
frame.extend_from_slice(&address.to_le_bytes()); // Address
frame.push(0x00); // Channel + Priority
frame.push(mb_type << 4); // Type[7:4]
// Mailbox Data
frame.extend_from_slice(mb_data);
frame
}
/// 发送请求并接收响应
fn send_request(&self, address: u16, mb_type: u8, mb_data: &[u8]) -> Vec<u8> {
let frame = self.build_frame(address, mb_type, mb_data);
self.socket.send_to(&frame, &self.gateway_addr).unwrap();
let mut buf = [0u8; 4096];
let (len, _) = self.socket.recv_from(&mut buf).unwrap();
buf[..len].to_vec()
}
/// CoE SDO Upload(读取对象字典)
/// address: 0x0000=主站, 其他=从站站地址
fn coe_sdo_read(&self, address: u16, index: u16, subindex: u8) -> Vec<u8> {
let mut co_data = vec![0u8; 8];
co_data[1] = 0x20; // CoE Type: SDO Request
co_data[2] = 0x40; // SDO Upload (Read)
co_data[3..5].copy_from_slice(&index.to_le_bytes());
co_data[5] = subindex;
self.send_request(address, 0x03, &co_data) // 0x03 = CoE
}
/// CoE SDO Download(写入对象字典)
fn coe_sdo_write(&self, address: u16, index: u16, subindex: u8, value: &[u8]) -> Vec<u8> {
let mut co_data = vec![0u8; 6 + value.len()];
co_data[1] = 0x20; // CoE Type: SDO Request
co_data[2] = 0x20; // SDO Download (Write)
co_data[3..5].copy_from_slice(&index.to_le_bytes());
co_data[5] = subindex;
co_data[6..6 + value.len()].copy_from_slice(value);
self.send_request(address, 0x03, &co_data)
}
/// SoE Read(读取 IDN 参数)
/// element_flags: 0x00=Name, 0x01=Attribute, 0x02=Unit, 0x03=Min, 0x04=Max, 0x05=Value, 0x06=Default, 0x07=DataType
fn soe_read(&self, address: u16, idn: u16, drive_no: u8, element: u8) -> Vec<u8> {
let mut soe_data = vec![0u8; 4];
soe_data[0] = 0x01; // OpCode: Read
soe_data[1] = (drive_no << 3) | (element & 0x07);
soe_data[2..4].copy_from_slice(&idn.to_le_bytes());
self.send_request(address, 0x04, &soe_data) // 0x04 = SoE
}
/// SoE Write(写入 IDN 参数)
fn soe_write(&self, address: u16, idn: u16, value: &[u8], drive_no: u8, element: u8) -> Vec<u8> {
let mut soe_data = vec![0u8; 4 + value.len()];
soe_data[0] = 0x02; // OpCode: Write
soe_data[1] = (drive_no << 3) | (element & 0x07);
soe_data[2..4].copy_from_slice(&idn.to_le_bytes());
soe_data[4..4 + value.len()].copy_from_slice(value);
self.send_request(address, 0x04, &soe_data)
}
/// FoE Read(从从站下载文件,仅支持单包)
fn foe_read(&self, address: u16, filename: &str) -> Vec<u8> {
let name_bytes = filename.as_bytes();
let mut foe_data = vec![0u8; 5 + name_bytes.len() + 1];
foe_data[0] = 0x01; // OpCode: Read Request
foe_data[5..5 + name_bytes.len()].copy_from_slice(name_bytes);
foe_data[5 + name_bytes.len()] = 0x00; // null terminator
self.send_request(address, 0x05, &foe_data) // 0x05 = FoE
}
/// VoE 发送/接收(厂商特定协议)
fn voe_send(&self, address: u16, vendor_id: u32, vendor_type: u16, data: &[u8]) -> Vec<u8> {
let mut voe_data = vec![0u8; 6 + data.len()];
voe_data[0..4].copy_from_slice(&vendor_id.to_le_bytes()); // VendorID
voe_data[4..6].copy_from_slice(&vendor_type.to_le_bytes()); // VendorType
voe_data[6..6 + data.len()].copy_from_slice(data);
self.send_request(address, 0x0F, &voe_data) // 0x0F = VoE
}
}
// --- 使用示例 ---
let client = MailboxGatewayClient::new("192.168.1.100", 0x88A4);
// CoE: 读取主站对象字典 (address=0x0000)
let resp = client.coe_sdo_read(0x0000, 0x1018, 0x01);
println!("主站 VendorID: {:02X?}", resp);
// CoE: 读取/写入从站对象字典
let resp = client.coe_sdo_read(0x03E9, 0x6040, 0x00);
client.coe_sdo_write(0x03E9, 0x6040, 0x00, &0x0006u16.to_le_bytes());
// SoE: 读取从站 IDN 参数值
let resp = client.soe_read(0x03E9, 32, 0, 0x05);
// SoE: 写入从站 IDN 参数值
client.soe_write(0x03E9, 32, &1000u32.to_le_bytes(), 0, 0x05);
// FoE: 从从站下载文件
let resp = client.foe_read(0x03E9, "firmware.bin");
// VoE: 厂商特定协议
let resp = client.voe_send(0x03E9, 0x00001164, 0x0001, &[0x01, 0x02]);
协议详解
CoE (CAN over EtherCAT)
支持的操作:
- SDO Upload (0x40) - 读取对象字典
- SDO Download (0x20) - 写入对象字典
- SDO Abort (0x80) - 错误响应
错误码:
0x06020000— 对象不存在0x06010000— 不支持的访问0x06010002— 写保护0x05040001— 命令未实现0x08000000— 一般错误
SoE (Servo over EtherCAT)
支持的操作:
- IDN Read (0x01) - 读取 IDN 参数
- IDN Write (0x02) - 写入 IDN 参数
支持的元素:
- Value (0x05) — 参数值
- Name (0x00) — 参数名称
- Attribute (0x01) — 参数属性
- Unit (0x02) — 单位
- Min/Max (0x03/0x04) — 最小/最大值
- Default (0x06) — 默认值
- Data Type (0x07) — 数据类型
错误码:
0x8001— 服务不可用0x1001— 无效命令0x1009— IDN不存在0x3002— 无效数据大小0x7002— 写入失败0x8000— 一般错误
FoE (File over EtherCAT)
支持的操作:
- Read Request (0x01) - 文件下载请求
- Write Request (0x02) - 文件上传准备
- Data (0x03) - 文件数据传输
- Ack (0x04) - 确认
- Error (0x05) - 错误响应
当前限制:
- 仅支持单包文件传输(小文件)
- 完整分段传输需要会话状态管理(未来版本)
错误码:
0x00008001— 无效操作码 / 未实现0x00008002— 文件未找到0x00008003— 非法文件名0x00008000— 一般错误
VoE (Vendor over EtherCAT)
帧格式:
- VendorID (4 bytes) — 厂商标识
- VendorType (2 bytes) — 厂商类型
- Data (n bytes) — 厂商特定数据
功能:
- 完整透传到从站 VoE 接口
- 自动处理请求/响应
- 支持任意长度数据
错误码:
- 错误帧 = VendorID
0+ VendorType0xFFFF(无独立错误码字段)
错误处理
| 协议 | 错误类型 | 错误码 | 说明 |
|---|---|---|---|
| CoE | 对象不存在 | 0x06020000 | 请求的对象或从站不存在 |
| CoE | 不支持的访问 | 0x06010000 | 协议类型不支持或从站无对应功能 |
| CoE | 写保护 | 0x06010002 | 尝试写入只读对象 |
| CoE | 命令未实现 | 0x05040001 | SDO 命令规范不支持 |
| CoE | 一般错误 | 0x08000000 | 其他处理错误 |
| SoE | 协议错误/不支持 | 0x7001 | 帧格式错或操作码不支持 |
| SoE | 写入失败 | 0x7002 | IDN 参数写入失败 |
| FoE | 未实现/帧错误 | 0x00008001 | 操作码不支持或帧太短 |
| FoE | 文件未找到 | 0x00008002 | 请求的文件不存在 |
| VoE | 错误帧 | — | VendorID 0 + VendorType 0xFFFF |
限制
已实现功能
- CoE 完整透传
- SoE IDN 参数访问
- FoE 单包文件传输
- VoE 厂商协议透传
- 主站对象字典访问
- 从站邮箱透传
- 错误处理和错误码映射
待实现功能
- FoE 分段传输(大文件支持)
- Address 映射表 (+0x8000 虚拟映射)
- 多播/广播请求
- 会话状态管理
默认网关是强透传的,允许网络访问主站和从站的全部对象字典。建议:
- 务必在受控网络中使用,或通过 IP 白名单 / VPN 隧道 限制访问
- 配合操作系统防火墙规则,仅放行可信来源 IP
- 生产环境中谨慎开启,避免暴露到公网
邮箱网关运行在独立线程,对主循环性能影响极小。但大量同步 SDO/邮箱操作仍可能影响总体延迟——请合理限制并发与速率。