FoE (File over EtherCAT)
FoE 协议用于 EtherCAT 从站的文件传输,支持固件更新、配置文件上传下载等场景。
通过 slave.FoE 访问。从站不支持 FoE 时为 null。
属性
| 属性 | 类型 | 访问 | 说明 |
|---|---|---|---|
| DefaultTimeoutMs | int | 读写 | 默认超时时间(毫秒),默认 5000(5秒) |
| DefaultPassword | uint | 读写 | 默认密码,默认 0 |
文件传输
Download(string filename, uint? password = null, int? timeoutMs = null, bool enableCrc = false)
public byte[]? Download(string filename, uint? password = null, int? timeoutMs = null, bool enableCrc = false)
从从站设备下载(读取)文件。
参数:
filename(string) — 要下载的文件名password(uint?) — FoE 密码(不指定则使用DefaultPassword)timeoutMs(int?) — 超时时间(毫秒,不指定则使用DefaultTimeoutMs)enableCrc(bool) — 启用 CRC32 校验(自动验证数据完整性)
返回值:
byte[]?— 文件内容字节数组,失败返回null
示例:
// 基本下载
byte[]? firmware = slave.FoE.Download("firmware.bin");
// 启用 CRC 校验
byte[]? config = slave.FoE.Download("config.xml", enableCrc: true);
Upload(string filename, byte[] fileData, uint? password = null, int? timeoutMs = null, bool enableCrc = false)
public bool Upload(string filename, byte[] fileData, uint? password = null, int? timeoutMs = null, bool enableCrc = false)
上传(写入)文件到从站设备。
参数:
filename(string) — 要上传的文件名fileData(byte[]) — 要写入的文件数据password(uint?) — FoE 密码(不指定则使用DefaultPassword)timeoutMs(int?) — 超时时间(毫秒,不指定则使用DefaultTimeoutMs)enableCrc(bool) — 启用 CRC32 校验(自动附加校验信息)
返回值:
bool— 成功返回true
示例:
// 基本上传
byte[] newFirmware = File.ReadAllBytes("new_firmware.bin");
bool success = slave.FoE.Upload("firmware.bin", newFirmware);
// 启用 CRC 校验
bool ok = slave.FoE.Upload("firmware.bin", newFirmware, enableCrc: true);
进度事件
ProgressChanged
public event EventHandler<FoEProgressEventArgs>? ProgressChanged;
FoE 传输进度事件。订阅后,Download / Upload 操作会自动触发进度报告。
相关结构:
Slave(ushort) — 从站编号PacketNumber(int) — 数据包序号DataSize(int) — 数据大小(字节)ProgressPercent(int) — 估算的进度百分比 (0-100)
示例:
slave.FoE.ProgressChanged += (sender, e) =>
{
Console.Write($"\r传输进度: {e.ProgressPercent}% (包 {e.PacketNumber})");
};
byte[] firmware = File.ReadAllBytes("new_firmware.bin");
bool success = slave.FoE.Upload("firmware.bin", firmware, enableCrc: true);
Console.WriteLine(success ? "\n上传完成!" : "\n上传失败!");
传输进度
EnableProgressHook(int estimatedTotalPackets = 0)
public bool EnableProgressHook(int estimatedTotalPackets = 0)
启用 FoE 进度钩子。启用后,Download / Upload 操作会通过 ProgressChanged 事件自动报告传输进度。
参数:
estimatedTotalPackets(int) — 估算的总数据包数(用于计算进度百分比,0 = 不估算)
返回值:
bool— 成功返回true
DisableProgressHook()
public bool DisableProgressHook()
禁用 FoE 进度钩子,停止进度报告。
返回值:
bool— 成功返回true
EstimatePacketCount(int fileSize, int mailboxSize = 512)
public int EstimatePacketCount(int fileSize, int mailboxSize = 512)
估算文件传输所需的数据包数量。可用于 EnableProgressHook 的 estimatedTotalPackets 参数。
参数:
fileSize(int) — 文件大小(字节)mailboxSize(int) — 邮箱大小(字节,默认 512)
返回值:
int— 估算的数据包数量
示例:
// 启用进度回调
slave.FoE.ProgressChanged += (sender, e) =>
{
Console.Write($"\r传输进度: {e.ProgressPercent}% (包 {e.PacketNumber}, {e.DataSize} 字节)");
};
byte[] firmware = File.ReadAllBytes("new_firmware.bin");
int packets = slave.FoE.EstimatePacketCount(firmware.Length);
slave.FoE.EnableProgressHook(packets);
bool success = slave.FoE.Upload("firmware.bin", firmware);
slave.FoE.DisableProgressHook();
Console.WriteLine(success ? "\n上传完成!" : "\n上传失败!");
错误处理
FoEInstance.GetErrorDescription(FoEErrorCode errorCode)
public static string GetErrorDescription(FoEErrorCode errorCode)
获取 FoE 错误码的中文描述文本。
参数:
errorCode(FoEErrorCode) — FoE 协议错误码
返回值:
string— 中文描述文本
相关结构:
public enum FoEErrorCode : uint
{
NotDefined = 0x8000, // 未定义错误
NotFound = 0x8001, // 文件未找到
AccessDenied = 0x8002, // 访问被拒绝
DiskFull = 0x8003, // 磁盘已满
Illegal = 0x8004, // 非法操作
PacketNumberWrong = 0x8005, // 数据包序号错误
AlreadyExists = 0x8006, // 文件已存在
NoUser = 0x8007, // 无此用户
BootstrapOnly = 0x8008, // 仅 Bootstrap 模式可用
NotBootstrap = 0x8009, // 不在 Bootstrap 模式
NoRights = 0x800A, // 权限不足
ProgramError = 0x800B // 程序错误
}
示例:
byte[]? data = slave.FoE.Download("firmware.bin");
if (data == null)
{
string desc = FoEInstance.GetErrorDescription(FoEErrorCode.NotFound);
Console.WriteLine($"下载失败: {desc}");
}
完整示例
固件更新
if (slave.FoE != null)
{
// 备份当前固件
byte[]? backup = slave.FoE.Download("firmware.bin");
if (backup != null)
File.WriteAllBytes("firmware_backup.bin", backup);
// 上传新固件(带进度和 CRC 校验)
slave.FoE.ProgressChanged += (s, e) =>
Console.Write($"\r更新进度: {e.ProgressPercent}%");
byte[] newFirmware = File.ReadAllBytes("new_firmware.bin");
bool ok = slave.FoE.Upload("firmware.bin", newFirmware, enableCrc: true);
Console.WriteLine(ok ? "\n固件更新成功" : "\n固件更新失败");
}
配置文件传输
if (slave.FoE != null)
{
slave.FoE.DefaultPassword = 0x12345678;
slave.FoE.DefaultTimeoutMs = 10000; // 10秒
// 读取配置
byte[]? config = slave.FoE.Download("config.xml");
if (config != null)
{
string xmlContent = System.Text.Encoding.UTF8.GetString(config);
Console.WriteLine($"配置内容:\n{xmlContent}");
}
// 写入新配置
string newConfig = "<Config><Param1>100</Param1></Config>";
byte[] configBytes = System.Text.Encoding.UTF8.GetBytes(newConfig);
slave.FoE.Upload("config.xml", configBytes);
}
取消传输与 BUSY 钩子
固件升级或大体积配置传输常常持续数十秒甚至几分钟。两个实用能力:
- 中途取消 — 允许用户按"取消"终止当前传输, 避免一直等到超时
- BUSY 钩子 — 从站在擦写 Flash / 烧录固件期间会周期返回 FoE BUSY 帧 (ETG.1000.6 §5.10 Table 93), 上位机借此显示"xx% 烧写中"
取消传输 (CancellationToken)
通过 DownloadAsync / UploadAsync 的 cancellationToken 参数取消,触发时 Task<T> 抛出 OperationCanceledException。
固件烧写命令可能已部分发送至从站, 取消后需重新下发完整固件。
示例 — 下载带取消:
var cts = new CancellationTokenSource();
// 启动异步下载 (非阻塞)
var progress = new Progress<int>(p => Console.Write($"\r进度: {p}%"));
var task = slave.FoE.DownloadAsync(
"firmware.bin",
progress: progress,
cancellationToken: cts.Token);
// 用户按 "取消" 按钮:
// cts.Cancel();
try
{
byte[]? data = await task;
Console.WriteLine(data != null ? "\n下载完成" : "\n下载失败");
}
catch (OperationCanceledException)
{
Console.WriteLine("\n用户取消传输");
}
示例 — 上传 (固件升级) 带取消:
var cts = new CancellationTokenSource();
byte[] firmware = File.ReadAllBytes("fw.bin");
var progress = new Progress<int>(p => Console.Write($"\r烧写: {p}%"));
var task = slave.FoE.UploadAsync(
"firmware.bin",
firmware,
enableCrc: true,
progress: progress,
cancellationToken: cts.Token);
// 用户中途取消:
// cts.Cancel();
try
{
bool ok = await task;
Console.WriteLine(ok ? "\n固件烧写成功" : "\n固件烧写失败");
}
catch (OperationCanceledException)
{
Console.WriteLine("\n已取消; 固件可能已部分写入, 需重新升级");
}
BUSY 钩子 (烧写进度)
从站在处理耗时 Flash 操作时会按 ETG.1000.6 Table 93 周期回 BUSY 帧, 主站 SDK 解析后通过同一个 ProgressChanged 事件上报, BUSY 相关字段在 FoEProgressEventArgs 扩展属性中:
BusyDone(int) — 从站报告的已完成字节数BusyEntire(int) — 从站报告的总字节数BusyText(string) — 从站自报告的状态文本 (如 "Erasing flash..." / "Programming sector 3/8")
当 BusyEntire > 0 时可认为当前进度来自 BUSY 帧 (否则是 DATA 阶段的包计数进度)。EnableProgressHook() 同时注册了 DATA 与 BUSY 两类回调, 无需额外 API。
示例 — 固件升级进度条 (含 BUSY 烧写显示):
if (slave.FoE == null) return;
slave.FoE.ProgressChanged += (sender, e) =>
{
if (e.BusyEntire > 0)
{
// BUSY 阶段: 从站正在擦写 Flash
double pct = 100.0 * e.BusyDone / e.BusyEntire;
Console.Write(
$"\r[烧写中] {pct:F1}% ({e.BusyDone}/{e.BusyEntire}) {e.BusyText} ");
}
else
{
// DATA 阶段: 主站在推送文件数据
Console.Write($"\r[传输中] {e.ProgressPercent}% (包 {e.PacketNumber}, {e.DataSize}B) ");
}
};
byte[] firmware = File.ReadAllBytes("fw.bin");
int packets = slave.FoE.EstimatePacketCount(firmware.Length);
slave.FoE.EnableProgressHook(packets);
try
{
bool ok = slave.FoE.Upload("firmware.bin", firmware, enableCrc: true);
Console.WriteLine(ok ? "\n烧写完成" : "\n烧写失败");
}
finally
{
slave.FoE.DisableProgressHook();
}
取消 + BUSY 组合 (推荐)
固件升级的实战组合: 异步调用 + 进度条 + BUSY 钩子 + 用户取消。用户看到真实烧写进度, 中途长时间卡在某个扇区时可主动取消避免无限等待。
var cts = new CancellationTokenSource();
slave.FoE.ProgressChanged += (s, e) =>
{
if (e.BusyEntire > 0)
Console.Write($"\r烧写 {100.0 * e.BusyDone / e.BusyEntire:F1}% {e.BusyText} ");
else
Console.Write($"\r传输 {e.ProgressPercent}% ");
};
int packets = slave.FoE.EstimatePacketCount(firmware.Length);
slave.FoE.EnableProgressHook(packets);
var task = slave.FoE.UploadAsync(
"firmware.bin", firmware, enableCrc: true,
cancellationToken: cts.Token);
// 超时兜底: 单扇区卡 > 30 秒自动取消
_ = Task.Delay(TimeSpan.FromSeconds(30), cts.Token).ContinueWith(_ =>
{
if (!task.IsCompleted) cts.Cancel();
}, TaskScheduler.Default);
try { await task; }
catch (OperationCanceledException) { Console.WriteLine("\n已取消"); }
finally { slave.FoE.DisableProgressHook(); }
注意事项
- BUSY 帧由从站决定是否推送, 部分低端从站不实现 BUSY 阶段, 上位机会"安静"几秒直到 FoE ACK 到达
- 取消标志仅影响 下一次 DLL 主循环迭代, 从站当前正在执行的 Flash 写不会被中断
EnableProgressHook必须在Upload/Download之前调用, 否则第一轮进度回调会丢失- 部分 SDK 版本可能不支持 BUSY 进度回调, 此时日志会记录"BUSY 进度信息将不可用", 其他功能不受影响
默认参数
| 属性 | 类型 | 说明 |
|---|---|---|
| DefaultTimeoutMs | int | FoE 默认超时 (毫秒), 默认 5000 |
| DefaultTimeoutUs | int | FoE 默认超时 (微秒), 派生自 DefaultTimeoutMs * 1000. 设置时反向计算 ms |
| DefaultPassword | uint | FoE 默认密码, 默认 0. Download / Upload 不指定 password 时回退使用 |
public int DefaultTimeoutMs { get; set; } = 5000;
public int DefaultTimeoutUs { get; set; } // = DefaultTimeoutMs * 1000
public uint DefaultPassword { get; set; } = 0;
示例:
slave.FoE.DefaultTimeoutMs = 10000;
slave.FoE.DefaultPassword = 0xCAFE0001;
slave.FoE.Download("firmware.bin", out _); // 使用默认值
取消传输
Cancel()
public bool Cancel()
请求取消当前 FoE 下载/上传事务. 从任意线程调用 (与 CancellationToken 等效但不依赖 async). 内部置位 DLL 取消标志, 下一次主循环迭代后 Download / Upload 提前返回.
返回值:
bool—true=请求成功;false=DLL 未导出FOERequestCancel或调用失败
固件烧写中途中断可能导致从站 Flash 部分写入, 需重新上传完整固件.
示例:
var task = Task.Run(() => slave.FoE.Upload("firmware.bin", data));
await Task.Delay(2000);
slave.FoE.Cancel(); // 任意线程, 不需要 cancellationToken
ClearCancel()
public bool ClearCancel()
清除取消标志, 允许重新开始新的 FoE 事务. Download / Upload 入口会自动清零, 仅在异常复位或外部手动操作时调用.
返回值:
bool—true=清除成功
BUSY 帧事件
BusyReceived
public event EventHandler<FoEBusyEventArgs>? BusyReceived;
从站返回 BUSY 帧时触发 (固件 Flash 烧写期间周期上报). 与 ProgressChanged.BusyDone/BusyEntire/BusyText 同源, 二选一即可. 首次订阅时自动注册 BUSY Hook, 最后一个订阅者取消后自动解注册 (除非 ProgressChanged 仍持有 Hook).
回调线程是 DLL worker, UI 应 Invoke 切回主线程.
相关结构:
public sealed class FoEBusyEventArgs : EventArgs
{
public ushort SlaveIndex { get; init; }
public ushort Done { get; init; } // 已完成量
public ushort Entire { get; init; } // 总量
public string Text { get; init; } // 从站文本
public int RetryIndex { get; init; } // 重试序号
public double Percent { get; } // = 100.0 * Done / Entire
}
示例:
slave.FoE.BusyReceived += (s, e) =>
{
Console.WriteLine($"[BUSY] {e.Done}/{e.Entire} ({e.Percent:F1}%) {e.Text}");
};
byte[] fw = File.ReadAllBytes("firmware.bin");
slave.FoE.Upload("firmware.bin", fw);