FoE (File over EtherCAT)
FoE 协议用于 EtherCAT 从站的文件传输,支持固件更新、配置文件上传下载等场景。
通过 slave.foe() 获取 FoEInstance 实例,或通过 FoEInstance::new(master_index, slave_index) 直接构造。
公共字段
| 字段 | 类型 | 说明 |
|---|---|---|
| default_timeout_ms | i32 | 默认超时时间(毫秒),默认 5000(5秒) |
| default_password | u32 | 默认密码,默认 0 |
文件传输
download()
pub fn download(&self, filename: &str, password: Option<u32>,
timeout_ms: Option<i32>) -> Result<Vec<u8>>
从从站设备下载(读取)文件。
参数:
filename(&str) — 要下载的文件名password(Option<u32>) — FoE 密码(None则使用default_password)timeout_ms(Option<i32>) — 超时时间(毫秒,None则使用default_timeout_ms)
返回值:
Result<Vec<u8>>— 文件内容字节数组
示例:
let foe = slave.foe();
// 基本下载
let firmware = foe.download("firmware.bin", None, None)?;
// 指定密码和超时
let config = foe.download("config.xml", Some(12345), Some(10000))?;
upload()
pub fn upload(&self, filename: &str, file_data: &[u8], password: Option<u32>,
timeout_ms: Option<i32>) -> Result<()>
上传(写入)文件到从站设备。
参数:
filename(&str) — 要上传的文件名file_data(&[u8]) — 要写入的文件数据password(Option<u32>) — FoE 密码(None则使用default_password)timeout_ms(Option<i32>) — 超时时间(毫秒,None则使用default_timeout_ms)
返回值:
Result<()>— 成功或错误
示例:
let foe = slave.foe();
// 基本上传
let new_firmware = std::fs::read("new_firmware.bin")?;
foe.upload("firmware.bin", &new_firmware, None, None)?;
// 指定密码
foe.upload("firmware.bin", &new_firmware, Some(12345), None)?;
进度事件
FoEProgressCallback
pub type FoEProgressCallback = Box<dyn Fn(u16, i32, i32) + Send + Sync>;
FoE 传输进度回调类型。回调参数: (slave_index, packet_number, data_size)。注册后,
download() / upload() 操作会自动按数据包推送进度。回调在 SDK 内部线程上执行,
不要做长时间阻塞或文件 I/O。
示例:
let foe = slave.foe().ok_or("不支持 FoE")?;
foe.set_progress_hook(Box::new(|slave, pkt, size| {
print!("\rSlave {} 包 {} ({} 字节)", slave, pkt, size);
}));
传输进度
set_progress_hook()
pub fn set_progress_hook(&self, callback: FoEProgressCallback) -> bool
设置 FoE 传输进度回调。
回调签名: Fn(slave_index: u16, packet_number: i32, data_size: i32)
clear_progress_hook()
pub fn clear_progress_hook(&self) -> bool
清除 FoE 进度回调。
estimate_packet_count()
pub fn estimate_packet_count(file_size: usize, mailbox_size: usize) -> usize
估算文件传输所需的数据包数量。
参数:
file_size(usize) — 文件大小(字节)mailbox_size(usize) — 邮箱大小(字节)
示例:
let foe = slave.foe();
let firmware = std::fs::read("new_firmware.bin")?;
let packets = FoEInstance::estimate_packet_count(firmware.len(), 512);
// 设置进度回调
foe.set_progress_hook(move |slave, pkt, _sz| {
let percent = std::cmp::min(100, pkt as usize * 100 / packets.max(1));
print!("\rSlave {} 传输进度: {}% (包 {})", slave, percent, pkt);
});
foe.upload("firmware.bin", &firmware, None, None)?;
foe.clear_progress_hook();
println!("\n上传完成!");
错误处理
get_error_description()
pub fn get_error_description(error_code: FoEErrorCode) -> &'static str
获取 FoE 错误码的中文描述文本。FoEInstance::last_error(&self) -> Option<FoEErrorCode>
返回最近一次 FoE 操作的错误码, 与 Result 的 Err 分支配合使用即可拿到具体失败原因。
相关结构:
#[repr(u32)]
pub enum FoEErrorCode {
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, // 程序错误
}
示例:
let foe = slave.foe().ok_or("不支持 FoE")?;
let data = foe.download("firmware.bin", None, None);
if let Err(e) = &data {
if let Some(error) = foe.last_error() {
let desc = FoEInstance::get_error_description(error);
println!("下载失败: {}", desc);
}
}
完整示例
固件更新
let foe = slave.foe().ok_or("不支持 FoE")?;
// 进度回调
foe.set_progress_hook(|_slave, pkt, _sz| {
print!("\r更新进度: 包 {}", pkt);
});
// 备份当前固件
let backup = foe.download("firmware.bin", None, None)?;
std::fs::write("firmware_backup.bin", &backup)?;
// 上传新固件
let new_firmware = std::fs::read("new_firmware.bin")?;
foe.upload("firmware.bin", &new_firmware, None, None)?;
foe.clear_progress_hook();
println!("\n固件更新成功");
配置文件传输
let foe = slave.foe().ok_or("不支持 FoE")?;
// 读取配置 (使用自定义密码 + 10s 超时)
let config = foe.download("config.xml", Some(0x12345678), Some(10000))?;
let xml_content = String::from_utf8(config)?;
println!("配置内容:\n{}", xml_content);
// 写入新配置
let new_config = "<Config><Param1>100</Param1></Config>";
foe.upload("config.xml", new_config.as_bytes(), Some(0x12345678), Some(10000))?;
取消传输与 BUSY 钩子
固件升级或大体积配置传输常常持续数十秒甚至几分钟。两个实用能力:
- 中途取消 — 允许用户按"取消"终止当前传输, 避免一直等到超时
- BUSY 钩子 — 从站在擦写 Flash / 烧录固件期间会周期返回 FoE BUSY 帧 (ETG.1000.6 §5.10 Table 93), 上位机借此显示"xx% 烧写中"
取消传输 (cancel)
pub fn cancel(&self) -> bool
请求取消当前 FoE 传输, 下一次传输循环迭代起生效, 返回 true 表示已接受取消请求。
clear_cancel(&self) -> bool 用于清除取消标志, 常规情况下 download() / upload() 入口会
自动清零, 仅异常复位时手动调用。
FoE 上传 (固件烧写) 中途取消可能导致从站 Flash 部分写入, 设备进入不可引导状态。
生产环境建议仅在用户明确要求 / 超时兜底场景下调用 cancel(), 且取消后需重新上传完整固件。
示例 — 后台线程上传 + 主线程取消:
use std::sync::Arc;
use std::thread;
let foe = slave.foe().ok_or("不支持 FoE")?;
let foe_cancel = slave.foe().unwrap(); // 多份句柄共享同一从站索引
let firmware = std::fs::read("fw.bin")?;
let handle = thread::spawn(move || {
foe.upload("fw.bin", &firmware, None, None)
});
// 用户按下取消按钮:
// foe_cancel.cancel();
match handle.join().unwrap() {
Ok(()) => println!("固件烧写成功"),
Err(e) => eprintln!("固件烧写失败 / 已取消: {:?}", e),
}
BUSY 钩子 (烧写进度)
从站在处理耗时 Flash 操作时会按 ETG.1000.6 Table 93 周期回 BUSY 帧, 主站 SDK 解析后通过
BUSY 回调上报。set_busy_hook(&self, cb: FoEBusyCallback) 注册回调, 同一主站可注册
多个回调, 按注册顺序依次触发; clear_busy_hooks(&self) 清空所有 BUSY 回调并通知 DLL 解绑。
相关结构:
use std::sync::Arc;
#[derive(Debug, Clone)]
pub struct FoEBusyEvent {
pub slave_index: u16, // 从站索引
pub done: u16, // 已完成进度 (Flash 页 / 字节, 含义由从站定义)
pub entire: u16, // 总进度基数
pub text: String, // 从站附带的描述文本 (BusyText, 无则空串)
pub retry_idx: i32, // 本次 BUSY 重试次数 (DLL 维护)
pub percent: u32, // SDK 自动计算的百分比 (0..=100, entire=0 时为 0)
}
pub type FoEBusyCallback = Arc<dyn Fn(&FoEBusyEvent) + Send + Sync>;
Rust 版在构造 FoEBusyEvent 时就把百分比算好存到 percent 字段, 回调直接读
e.percent 即可, 无需调用方法。回调在 SDK 内部线程上执行, 不要做长时间阻塞或文件 I/O。
示例 — BUSY 回调显示烧写进度:
use std::sync::Arc;
use ethercat::slave::foe::{FoEBusyEvent, FoEBusyCallback};
let foe = slave.foe().ok_or("不支持 FoE")?;
let hook: FoEBusyCallback = Arc::new(|e: &FoEBusyEvent| {
print!(
"\rSlave {} [{:3}%] {} (重试 {})",
e.slave_index, e.percent, e.text, e.retry_idx,
);
});
foe.set_busy_hook(Arc::clone(&hook));
let firmware = std::fs::read("fw.bin")?;
foe.upload("fw.bin", &firmware, None, None)?;
foe.clear_busy_hooks();
取消 + BUSY 组合 (异步推荐)
启用 async-tokio feature 后, FoEInstance 暴露 download_async() / upload_async() 两组
异步签名, 可配合 tokio::select! 与 cancel() 实现用户可中断的长耗时固件升级。
pub async fn download_async(&self, filename: &str, password: Option<u32>,
timeout_ms: Option<i32>) -> Result<Vec<u8>, DarraError>
pub async fn upload_async(&self, filename: &str, file_data: &[u8], password: Option<u32>,
timeout_ms: Option<i32>) -> Result<(), DarraError>
[dependencies]
ethercat = { version = "0.1", features = ["async-tokio"] }
tokio = { version = "1", features = ["macros", "rt-multi-thread", "signal"] }
示例 — 进度条 + BUSY 烧写显示 + Ctrl+C 取消:
use std::sync::Arc;
use ethercat::slave::foe::{FoEBusyEvent, FoEBusyCallback};
let foe = slave.foe().ok_or("不支持 FoE")?;
// 1. BUSY 回调显示进度条
let hook: FoEBusyCallback = Arc::new(|e: &FoEBusyEvent| {
if e.entire > 0 {
print!("\r[烧写中] {:3}% ({}/{}) {}", e.percent, e.done, e.entire, e.text);
}
});
foe.set_busy_hook(Arc::clone(&hook));
// 2. 后台上传 + Ctrl+C 取消
let data = std::fs::read("fw.bin")?;
let foe_cancel = slave.foe().unwrap(); // 多份句柄共享同一索引, 用于 cancel
let upload_task = tokio::spawn(async move {
foe.upload_async("fw.bin", &data, None, None).await
});
tokio::select! {
res = upload_task => {
match res {
Ok(Ok(())) => println!("\n上传完成"),
Ok(Err(e)) => eprintln!("\n上传失败: {:?}", e),
Err(e) => eprintln!("\n任务 panic: {:?}", e),
}
}
_ = tokio::signal::ctrl_c() => {
eprintln!("\n收到 Ctrl+C, 取消 FoE 传输");
foe_cancel.cancel();
}
}
foe_cancel.clear_busy_hooks();
注意事项
- BUSY 帧由从站决定是否推送, 部分低端从站不实现 BUSY 阶段, 上位机会"安静"几秒直到 FoE ACK 到达
- 取消标志仅影响 下一次 DLL 主循环迭代, 从站当前正在执行的 Flash 写不会被中断
set_busy_hook()必须在upload()/download()之前注册, 否则第一轮进度回调会丢失- 部分 SDK 版本可能不支持 BUSY 进度回调, 此时日志会记录"BUSY 进度信息将不可用", 其他功能不受影响
- 非 async 场景可用单独线程跑
upload(), 主线程监控用户输入后调用cancel();FoEInstance多份句柄共享同一主/从站索引, 线程安全
- ETG.1000.6 Table 93 — FoE BUSY 帧字段 (Done / Entire / BusyText)