跳到主要内容

FoE (File over EtherCAT)

FoE 协议用于 EtherCAT 从站的文件传输,支持固件更新、配置文件上传下载等场景。

通过 slave.foe() 获取 FoEInstance 实例,或通过 FoEInstance::new(master_index, slave_index) 直接构造。

公共字段

字段类型说明
default_timeout_msi32默认超时时间(毫秒),默认 5000(5秒)
default_passwordu32默认密码,默认 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 操作的错误码, 与 ResultErr 分支配合使用即可拿到具体失败原因。

相关结构:

#[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>;
percent 为字段而非方法

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>
Cargo.toml 依赖
[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)