跳到主要内容

邮箱网关 (Mailbox Gateway)

邮箱网关实现 ETG.8200 标准,允许外部诊断工具通过 UDP/IP 访问 EtherCAT 主站和从站的对象字典。

通过 master.mailbox_gateway() 访问。

ETG.8200 标准

邮箱网关服务符合 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: 34980(默认)
  • 实例 2: 34981
  • 实例 N: 34980 + (N - 1)

如需手动指定端口,在 start() 前调用 set_port() 即可覆盖自动值。

警告

端口号必须在服务启动前设置。服务运行时无法更改端口。如果期望端口被占用,会自动回退到可用端口,port() 会更新为实际值。

is_running()

pub fn is_running(&self) -> bool

服务是否正在运行。

start()

pub fn start(&mut self) -> Result<(), String>

启动邮箱网关服务,开始监听 UDP 请求。如果期望端口被占用,会自动尝试备用端口。

示例:

let mut gw = master.mailbox_gateway();
gw.start().unwrap();
println!("邮箱网关已启动,端口: {}", gw.port());

stop()

pub fn stop(&mut self)

停止邮箱网关服务,释放 UDP 端口。停止后可再次调用 start() 重新启动。

支持的协议

协议类型说明
CoE0x03SDO 读写,支持主站(ETG.1510)和从站对象字典
SoE0x04IDN 参数读写(ETG.1020 标准)
FoE0x05文件传输(当前仅支持单包)
VoE0x0F厂商特定协议透传
地址路由
  • Address = 0x0000 -> 访问主站对象字典(仅 CoE)
  • Address = 从站站地址 -> 透传到对应从站(CoE / SoE / FoE / VoE)

帧结构

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::stats() 实时取快照.

stats()

pub fn stats(&self) -> MailboxGatewayStats

返回当前累计统计的快照. 内部使用原子计数, 无需加锁, 可在任何线程频繁调用.

MailboxGatewayStats 结构

#[derive(Debug, Clone, Default)]
pub struct MailboxGatewayStats {
/// 接收的 UDP 请求总数 (含解析失败)
pub total_requests: u64,
/// 解析成功并转发到 EtherCAT 的请求数
pub forwarded_requests: u64,
/// 处理失败的请求数 (帧格式错误 / 路由失败 / 超时)
pub failed_requests: u64,
/// CoE 协议处理次数
pub coe_count: u64,
/// SoE 协议处理次数
pub soe_count: u64,
/// FoE 协议处理次数
pub foe_count: u64,
/// VoE 协议处理次数
pub voe_count: u64,
/// 服务启动时间戳 (Unix 毫秒, 0 表示未启动)
pub started_at_ms: u64,
}

示例:

let mut gw = master.mailbox_gateway();
gw.start().unwrap();

// ... 客户端发请求一段时间 ...

let s = gw.stats();
println!(
"总请求 {} / 转发 {} / 失败 {} | CoE={} SoE={} FoE={} VoE={}",
s.total_requests, s.forwarded_requests, s.failed_requests,
s.coe_count, s.soe_count, s.foe_count, s.voe_count,
);

if s.total_requests > 0 {
let rate = s.failed_requests as f64 / s.total_requests as f64;
if rate > 0.05 {
eprintln!("警告: 邮箱网关失败率过高 {:.1}%", rate * 100.0);
}
}

完整示例

场景 A — 集成现有配置/调试工具

让第三方配置工具(或工程师工具)通过 UDP 访问主站/从站对象字典,无需修改被控设备固件。

场景 B — 自定义诊断或自动化脚本

通过脚本/服务定期采集从站参数、批量读取对象或执行远程配置。

场景 C — 远程监控与告警采集

把网关用于远程被动监控(周期性读取或订阅),并把关键数据汇报到集中监控系统。

启动邮箱网关

use ethercat::EtherCATMaster;

let mut master = EtherCATMaster::from_json_file("config.json")?;

master.set_state(EcState::Op)?;

// 启动邮箱网关(可选:自定义端口)
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]);

外部 Python 客户端

import socket
import struct

class MailboxGatewayClient:
"""ETG.8200 邮箱网关 UDP 客户端"""

def __init__(self, host="127.0.0.1", port=0x88A4):
self.addr = (host, port)
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.sock.settimeout(3.0)

def _build_frame(self, address: int, mb_type: int, mb_data: bytes) -> bytes:
mb_len = len(mb_data)
ecat_len = 6 + mb_len
ecat_header = (0x05 << 12) | (ecat_len & 0x07FF)

frame = struct.pack('<H', ecat_header) # EtherCAT Header
frame += struct.pack('<H', mb_len) # Mailbox Length
frame += struct.pack('<H', address) # Address
frame += bytes([0x00]) # Channel + Priority
frame += bytes([mb_type << 4]) # Type
frame += mb_data
return frame

def send_request(self, address: int, mb_type: int, mb_data: bytes) -> bytes:
frame = self._build_frame(address, mb_type, mb_data)
self.sock.sendto(frame, self.addr)
resp, _ = self.sock.recvfrom(4096)
return resp

def coe_sdo_read(self, address: int, index: int, subindex: int) -> bytes:
"""CoE SDO Upload(读取对象字典)"""
co_data = bytearray(8)
co_data[1] = 0x20 # CoE Type: SDO Request
co_data[2] = 0x40 # SDO Upload (Read)
struct.pack_into('<H', co_data, 3, index)
co_data[5] = subindex
return self.send_request(address, 0x03, bytes(co_data))

def coe_sdo_write(self, address: int, index: int, subindex: int, value: bytes) -> bytes:
"""CoE SDO Download(写入对象字典)"""
co_data = bytearray(6 + len(value))
co_data[1] = 0x20 # CoE Type: SDO Request
co_data[2] = 0x20 # SDO Download (Write)
struct.pack_into('<H', co_data, 3, index)
co_data[5] = subindex
co_data[6:6+len(value)] = value
return self.send_request(address, 0x03, bytes(co_data))

def soe_read(self, address: int, idn: int, drive_no: int = 0, element: int = 0x05) -> bytes:
"""SoE Read(读取 IDN 参数)"""
soe_data = bytearray(4)
soe_data[0] = 0x01 # OpCode: Read
soe_data[1] = (drive_no << 3) | (element & 0x07)
struct.pack_into('<H', soe_data, 2, idn)
return self.send_request(address, 0x04, bytes(soe_data))

def soe_write(self, address: int, idn: int, value: bytes, drive_no: int = 0, element: int = 0x05) -> bytes:
"""SoE Write(写入 IDN 参数)"""
soe_data = bytearray(4 + len(value))
soe_data[0] = 0x02 # OpCode: Write
soe_data[1] = (drive_no << 3) | (element & 0x07)
struct.pack_into('<H', soe_data, 2, idn)
soe_data[4:4+len(value)] = value
return self.send_request(address, 0x04, bytes(soe_data))

def foe_read(self, address: int, filename: str) -> bytes:
"""FoE Read(从从站下载文件,仅支持单包)"""
name_bytes = filename.encode('ascii') + b'\x00'
foe_data = bytearray(5 + len(name_bytes))
foe_data[0] = 0x01 # OpCode: Read Request
foe_data[5:] = name_bytes
return self.send_request(address, 0x05, bytes(foe_data))

def voe_send(self, address: int, vendor_id: int, vendor_type: int, data: bytes) -> bytes:
"""VoE 发送/接收(厂商特定协议)"""
voe_data = bytearray(6 + len(data))
struct.pack_into('<I', voe_data, 0, vendor_id) # VendorID
struct.pack_into('<H', voe_data, 4, vendor_type) # VendorType
voe_data[6:6+len(data)] = data
return self.send_request(address, 0x0F, bytes(voe_data))

# --- 使用示例 ---
client = MailboxGatewayClient("192.168.1.100")

# CoE: 读取主站对象字典
resp = client.coe_sdo_read(0x0000, 0x1018, 0x01)
print(f"主站 VendorID: {resp.hex()}")

# CoE: 读取/写入从站对象字典
resp = client.coe_sdo_read(0x03E9, 0x6040, 0x00)
client.coe_sdo_write(0x03E9, 0x6040, 0x00, struct.pack('<H', 0x0006))

# SoE: 读取从站 IDN 参数值
resp = client.soe_read(0x03E9, idn=32, drive_no=0, element=0x05)

# SoE: 写入从站 IDN 参数值
client.soe_write(0x03E9, idn=32, value=struct.pack('<I', 1000))

# FoE: 从从站下载文件
resp = client.foe_read(0x03E9, "firmware.bin")

# VoE: 厂商特定协议
resp = client.voe_send(0x03E9, vendor_id=0x00001164, vendor_type=0x0001, data=b'\x01\x02')

协议详解

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 接口
  • 自动处理请求/响应
  • 支持任意长度数据

错误码:

  • 0x0001 — 服务不可用
  • 0x0002 — 无效帧
  • 0x0003 — 无响应
  • 0x0000 — 一般错误

错误处理

协议错误类型错误码说明
CoE对象不存在0x06020000请求的对象或从站不存在
CoE不支持的访问0x06010000协议类型不支持或从站无对应功能
CoE写保护0x06010002尝试写入只读对象
CoE命令未实现0x05040001SDO 命令规范不支持
CoE一般错误0x08000000其他处理错误
SoEIDN不存在0x1009请求的IDN参数不存在
SoE写入失败0x7002IDN参数写入失败
FoE文件未找到0x00008002请求的文件不存在
FoE非法文件名0x00008003文件名格式错误
VoE无响应0x0003从站无VoE响应

限制

已实现功能

  • CoE 完整透传
  • SoE IDN 参数访问
  • FoE 单包文件传输
  • VoE 厂商协议透传
  • 主站对象字典访问
  • 从站邮箱透传
  • 错误处理和错误码映射

待实现功能

  • FoE 分段传输(大文件支持)
  • Address 映射表 (+0x8000 虚拟映射)
  • 多播/广播请求
  • 会话状态管理
网络安全

默认网关是强透传的,允许网络访问主站和从站的全部对象字典。建议:

  • 务必在受控网络中使用,或通过 IP 白名单 / VPN 隧道 限制访问
  • 配合操作系统防火墙规则,仅放行可信来源 IP
  • 生产环境中谨慎开启,避免暴露到公网
性能影响

邮箱网关运行在独立线程,对主循环性能影响极小。但大量同步 SDO/邮箱操作仍可能影响总体延迟——请合理限制并发与速率。