邮箱网关 (Mailbox Gateway)
邮箱网关实现 ETG.8200 标准,允许外部诊断工具通过 UDP/IP 访问 EtherCAT 主站和从站的对象字典。
通过 master.MailboxGateway 访问。
ETG.8200标准
邮箱网关服务符合 ETG.8200 标准规范,使用标准 UDP 端口 0x88A4 (34980),帧结构完全符合 Table 1 要求。
服务控制
Port
public int Port { get; set; }
UDP 端口号,默认 0x88A4 (34980)。启动后会更新为实际绑定的端口。
多实例自动偏移
运行多个主站实例时,端口会根据 MasterNumber 自动偏移,避免端口冲突:
- 实例 1:
34980(默认) - 实例 2:
34981 - 实例 N:
34980 + (N - 1)
如需手动指定端口,在 Start() 前设置 Port 属性即可覆盖自动值。
警告
端口号必须在服务启动前设置。服务运行时无法更改端口。如果期望端口被占用,会自动回退到可用端口,Port 会更新为实际值。
IsRunning
public bool IsRunning { get; }
服务是否正在运行。
Start()
public void Start()
启动邮箱网关服务,开始监听 UDP 请求。如果期望端口被占用,会自动尝试备用端口,最终由系统分配。
示例:
var gateway = master.MailboxGateway;
gateway.Start();
Console.WriteLine($"邮箱网关已启动,端口: {gateway.Port}");
Stop()
public void Stop()
停止邮箱网关服务,释放 UDP 端口。停止后可再次调用 Start() 重新启动。
示例:
master.MailboxGateway.Stop();
支持的协议
| 协议 | 类型 | 说明 |
|---|---|---|
| CoE | 0x03 | SDO 读写,支持主站(ETG.1510)和从站对象字典 |
| SoE | 0x04 | IDN 参数读写(ETG.1020 标准) |
| FoE | 0x05 | 文件传输(当前仅支持单包) |
| VoE | 0x0F | 厂商特定协议透传 |
地址路由
- 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 循环)
使用场景
场景 A — 集成现有配置/调试工具
让第三方配置工具(或工程师工具)通过 UDP 访问主站/从站对象字典,无需修改被控设备固件。
场景 B — 自定义诊断或自动化脚本
通过脚本/服务定期采集从站参数、批量读取对象或执行远程配置。
场景 C — 远程监控与告警采集
把网关用于远程被动监控(周期性读取或订阅),并把关键数据汇报到集中监控系统。
完整示例
自己启动案例
using DarraEtherCAT_Master;
var master = new DarraEtherCAT();
master.StartWithENI("config.xml");
master.StartCyclic();
// 启动邮箱网关(可选:自定义端口)
var gateway = master.MailboxGateway;
// gateway.Port = 12345; // 默认 34980,如需修改请在 Start 前设置
gateway.Start();
Console.WriteLine($"邮箱网关已启动,端口: {gateway.Port}");
try
{
while (true)
{
master.UpdateIO();
Thread.Sleep(1);
}
}
finally
{
gateway.Stop();
master.StopCyclic();
}
外部代码案例
C# 外部客户端
using System.Net;
using System.Net.Sockets;
/// <summary>
/// ETG.8200 邮箱网关 UDP 客户端
/// 用于从外部程序访问主站/从站对象字典
/// </summary>
class MailboxGatewayClient
{
private readonly UdpClient _udp = new();
private readonly IPEndPoint _gateway;
public MailboxGatewayClient(string host = "127.0.0.1", int port = 0x88A4)
{
_gateway = new IPEndPoint(IPAddress.Parse(host), port);
}
/// <summary>
/// 构建 ETG.8200 帧:EtherCAT Header (2) + Mailbox Header (6) + Data
/// </summary>
private byte[] BuildFrame(ushort address, byte mailboxType, byte[] mailboxData)
{
ushort mbLength = (ushort)mailboxData.Length;
ushort ecatLength = (ushort)(6 + mbLength); // Mailbox Header + Data
var frame = new byte[2 + 6 + mbLength];
// EtherCAT Header: Length[10:0] | DataType[15:12]=0x05
ushort ecatHeader = (ushort)((0x05 << 12) | (ecatLength & 0x07FF));
BitConverter.GetBytes(ecatHeader).CopyTo(frame, 0);
// Mailbox Header
BitConverter.GetBytes(mbLength).CopyTo(frame, 2); // Length
BitConverter.GetBytes(address).CopyTo(frame, 4); // Address
frame[6] = 0x00; // Channel + Priority
frame[7] = (byte)(mailboxType << 4); // Type[7:4]
// Mailbox Data
mailboxData.CopyTo(frame, 8);
return frame;
}
/// <summary>
/// 发送请求并接收响应
/// </summary>
public byte[] SendRequest(ushort address, byte mailboxType, byte[] mailboxData)
{
var frame = BuildFrame(address, mailboxType, mailboxData);
_udp.Send(frame, frame.Length, _gateway);
var remoteEP = new IPEndPoint(IPAddress.Any, 0);
return _udp.Receive(ref remoteEP);
}
/// <summary>
/// CoE SDO Upload(读取对象字典)
/// address: 0x0000=主站, 其他=从站站地址
/// </summary>
public byte[] CoE_SDO_Read(ushort address, ushort index, byte subindex)
{
// CoE Header (2) + SDO Request (6)
var coData = new byte[8];
coData[0] = 0x00;
coData[1] = 0x20; // CoE Type: SDO Request
coData[2] = 0x40; // SDO Upload (Read)
BitConverter.GetBytes(index).CopyTo(coData, 3); // Index
coData[5] = subindex; // SubIndex
return SendRequest(address, 0x03, coData); // 0x03 = CoE
}
/// <summary>
/// CoE SDO Download(写入对象字典)
/// </summary>
public byte[] CoE_SDO_Write(ushort address, ushort index, byte subindex, byte[] value)
{
var coData = new byte[6 + value.Length];
coData[0] = 0x00;
coData[1] = 0x20; // CoE Type: SDO Request
coData[2] = 0x20; // SDO Download (Write)
BitConverter.GetBytes(index).CopyTo(coData, 3);
coData[5] = subindex;
value.CopyTo(coData, 6);
return SendRequest(address, 0x03, coData);
}
/// <summary>
/// SoE Read(读取 IDN 参数)
/// elementFlags: 0x00=Name, 0x01=Attribute, 0x02=Unit, 0x03=Min, 0x04=Max, 0x05=Value, 0x06=Default, 0x07=DataType
/// </summary>
public byte[] SoE_Read(ushort address, ushort idn, byte driveNo = 0, byte elementFlags = 0x05)
{
var soeData = new byte[4];
soeData[0] = 0x01; // OpCode: Read
soeData[1] = (byte)((driveNo << 3) | (elementFlags & 0x07));
BitConverter.GetBytes(idn).CopyTo(soeData, 2);
return SendRequest(address, 0x04, soeData); // 0x04 = SoE
}
/// <summary>
/// SoE Write(写入 IDN 参数)
/// </summary>
public byte[] SoE_Write(ushort address, ushort idn, byte[] value, byte driveNo = 0, byte elementFlags = 0x05)
{
var soeData = new byte[4 + value.Length];
soeData[0] = 0x02; // OpCode: Write
soeData[1] = (byte)((driveNo << 3) | (elementFlags & 0x07));
BitConverter.GetBytes(idn).CopyTo(soeData, 2);
value.CopyTo(soeData, 4);
return SendRequest(address, 0x04, soeData);
}
/// <summary>
/// FoE Read(从从站下载文件,仅支持单包)
/// </summary>
public byte[] FoE_Read(ushort address, string filename)
{
var nameBytes = Encoding.ASCII.GetBytes(filename + '\0');
var foeData = new byte[5 + nameBytes.Length];
foeData[0] = 0x01; // OpCode: Read Request
nameBytes.CopyTo(foeData, 5);
return SendRequest(address, 0x05, foeData); // 0x05 = FoE
}
/// <summary>
/// VoE 发送/接收(厂商特定协议)
/// </summary>
public byte[] VoE_SendReceive(ushort address, uint vendorId, ushort vendorType, byte[] data)
{
var voeData = new byte[6 + data.Length];
BitConverter.GetBytes(vendorId).CopyTo(voeData, 0); // VendorID
BitConverter.GetBytes(vendorType).CopyTo(voeData, 4); // VendorType
data.CopyTo(voeData, 6);
return SendRequest(address, 0x0F, voeData); // 0x0F = VoE
}
}
// --- 使用示例 ---
var client = new MailboxGatewayClient("192.168.1.100");
// CoE: 读取主站对象字典 (address=0x0000)
var resp = client.CoE_SDO_Read(0x0000, 0x1018, 0x01);
Console.WriteLine($"主站 VendorID: {BitConverter.ToString(resp)}");
// CoE: 读取/写入从站对象字典
resp = client.CoE_SDO_Read(0x03E9, 0x6040, 0x00);
client.CoE_SDO_Write(0x03E9, 0x6040, 0x00, BitConverter.GetBytes((ushort)0x0006));
// SoE: 读取从站 IDN 参数值
resp = client.SoE_Read(0x03E9, 32, driveNo: 0, elementFlags: 0x05);
// SoE: 写入从站 IDN 参数值
client.SoE_Write(0x03E9, 32, BitConverter.GetBytes(1000), driveNo: 0);
// FoE: 从从站下载文件
resp = client.FoE_Read(0x03E9, "firmware.bin");
// VoE: 厂商特定协议
resp = client.VoE_SendReceive(0x03E9, 0x00001164, 0x0001, new byte[] { 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')
注意事项
网络安全
默认网关是强透传的,允许网络访问主站和从站的全部对象字典。建议:
- 务必在受控网络中使用,或通过 IP 白名单 / VPN 隧道 限制访问
- 配合操作系统防火墙规则,仅放行可信来源 IP
- 生产环境中谨慎开启,避免暴露到公网
性能影响
邮箱网关运行在独立线程,对主循环性能影响极小。但大量同步 SDO/邮箱操作仍可能影响总体延迟——请合理限制并发与速率。
协议详解
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 | 命令未实现 | 0x05040001 | SDO 命令规范不支持 |
| CoE | 一般错误 | 0x08000000 | 其他处理错误 |
| SoE | IDN不存在 | 0x1009 | 请求的IDN参数不存在 |
| SoE | 写入失败 | 0x7002 | IDN参数写入失败 |
| FoE | 文件未找到 | 0x00008002 | 请求的文件不存在 |
| FoE | 非法文件名 | 0x00008003 | 文件名格式错误 |
| VoE | 无响应 | 0x0003 | 从站无VoE响应 |
限制
已实现功能
- CoE 完整透传
- SoE IDN 参数访问
- FoE 单包文件传输
- VoE 厂商协议透传
- 主站对象字典访问
- 从站邮箱透传
- 错误处理和错误码映射
待实现功能
- FoE 分段传输(大文件支持)
- Address 映射表 (+0x8000 虚拟映射)
- 多播/广播请求
- 会话状态管理