跳到主要内容

FoE (File over EtherCAT)

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

通过 slave.GetFoE() 获取 FoE& 引用。

文件读取(下载)

Download()

std::vector<uint8_t> Download(const std::string& filename,
uint32_t password = 0,
int timeoutMs = 0) const;

从从站设备读取(下载)文件。失败返回空 vector。

参数:

  • filename — 要读取的文件名
  • password — FoE 密码(0 = 使用默认密码)
  • timeoutMs — 超时(0 = 使用默认超时)

示例:

auto& foe = master.GetSlave(1).GetFoE();

// 读取配置文件
auto config = foe.Download("config.xml");
if (!config.empty()) {
printf("配置内容 (%zu 字节):\n%.*s\n",
config.size(), (int)config.size(), config.data());
}

文件写入(上传)

Upload()

bool Upload(const std::string& filename, const uint8_t* data, int dataLen,
uint32_t password = 0, int timeoutMs = 0) const;

bool Upload(const std::string& filename, const std::vector<uint8_t>& data,
uint32_t password = 0, int timeoutMs = 0) const;

上传(写入)文件到从站设备。支持原始指针和 std::vector 两种方式。

示例:

auto& foe = master.GetSlave(1).GetFoE();

// 写入配置
std::string new_config = "<Config><Param1>100</Param1></Config>";
std::vector<uint8_t> data(new_config.begin(), new_config.end());
bool ok = foe.Upload("config.xml", data, 0x12345678);
printf("%s\n", ok ? "写入成功" : "写入失败");

默认参数

auto& foe = master.GetSlave(1).GetFoE();

// 默认超时
foe.DefaultTimeoutMs(10000); // 10秒
printf("默认超时: %d ms\n", foe.DefaultTimeoutMs());

// 默认密码
foe.DefaultPassword(0x12345678);
printf("默认密码: 0x%08X\n", foe.DefaultPassword());

传输进度

SetProgressCallback()

void SetProgressCallback(FoEProgressCallback cb);

设置文件传输进度回调。设置后,Download / Upload / DownloadEx / UploadEx 操作会自动触发回调,每传输一个数据包调用一次。

回调类型:

using FoEProgressCallback = std::function<void(
uint16_t slave, int packetNumber, int dataSize, int progressPercent)>;

回调参数:

  • slave (uint16_t) — 从站编号
  • packetNumber (int) — 当前数据包序号(从 1 开始递增)
  • dataSize (int) — 当前包传输的数据大小(字节)
  • progressPercent (int) — 估算的进度百分比 (0-100)

EstimatePacketCount()

static int EstimatePacketCount(int fileSize, int mailboxSize = 512);

估算文件传输所需的数据包数量。FoE 协议头占 6 字节,因此每包有效数据为 mailboxSize - 6 字节。可用于进度百分比计算。

参数:

  • fileSize (int) — 文件大小(字节)
  • mailboxSize (int) — 邮箱大小(字节,默认 512)

返回值:

  • int — 估算的数据包数量

基本用法

auto& foe = master.GetSlave(1).GetFoE();

// 设置进度回调
foe.SetProgressCallback([](uint16_t slave, int pkt, int sz, int percent) {
printf("\r从站 %d 传输进度: %d%% (包 %d, %d 字节)", slave, percent, pkt, sz);
fflush(stdout);
});

// 上传文件
bool ok = foe.Upload("firmware.bin", firmware);
printf("\n%s\n", ok ? "上传完成" : "上传失败");

完整示例: 带进度和错误处理的固件更新

#include "ethercat.hpp"
#include <fstream>
#include <iterator>

using namespace darra;

int main() {
EtherCATMaster master(dll);
master.SetNetwork("\\Device\\NPF_{...}")
.SetENI("config.deni")
.Build();
master.SetState(EcState::OP);
master.Start();

auto& foe = master.GetSlave(1).GetFoE();

// 设置超时和密码
foe.DefaultTimeoutMs(30000);

// 加载固件文件
std::ifstream file("new_firmware.bin", std::ios::binary);
std::vector<uint8_t> firmware(
(std::istreambuf_iterator<char>(file)),
std::istreambuf_iterator<char>());

// 估算包数量
int totalPackets = FoE::EstimatePacketCount(static_cast<int>(firmware.size()));
printf("文件大小: %zu 字节, 预计 %d 个数据包\n", firmware.size(), totalPackets);

// 设置进度回调 (带百分比计算)
foe.SetProgressCallback([totalPackets](uint16_t slave, int pkt, int sz, int percent) {
// percent 由 SDK 自动计算; 也可以手动计算:
// int manualPercent = totalPackets > 0 ? (pkt * 100) / totalPackets : 0;
printf("\r固件上传: %d%% (包 %d/%d)", percent, pkt, totalPackets);
fflush(stdout);
});

// 上传固件
bool ok = foe.Upload("firmware.bin", firmware);
printf("\n");

if (ok) {
printf("固件更新成功\n");
} else {
// 获取错误详情
auto error = foe.LastErrorCode();
auto desc = FoE::GetErrorDescription(error);
printf("固件更新失败: %s (0x%04X)\n", desc.c_str(), static_cast<uint16_t>(error));
}

getchar();
return 0;
}

带 CRC 校验的下载

auto& foe = master.GetSlave(1).GetFoE();

// 设置进度回调
foe.SetProgressCallback([](uint16_t slave, int pkt, int sz, int percent) {
printf("\r下载进度: %d%% (包 %d)", percent, pkt);
fflush(stdout);
});

// 使用 CRC 校验下载文件
auto data = foe.Download("config.bin", -1, 0, true /* enableCrc */);
printf("\n");

if (!data.empty()) {
printf("下载成功: %zu 字节\n", data.size());
} else {
printf("下载失败: %s\n",
FoE::GetErrorDescription(foe.LastErrorCode()).c_str());
}

异步 API (std::future)

长时间的 FoE 传输(固件升级动辄数 MB / 数分钟)会阻塞调用线程,C++ SDK 在同步 Download / Upload / DownloadEx / UploadEx 之外提供 4 个 std::future 风格的异步入口,内部用 std::async(std::launch::async) 把同步调用甩到后台线程。语义对齐 C# FoEInstance.DownloadAsync / UploadAsync / Java downloadAsync / uploadAsync / Rust download_async / upload_async

取消语义

std::future 本身不能强制中断后台线程。要真正提前结束传输,需配合 Cancel() — 后台 std::async 线程下一次 mailbox 迭代退出时 future.get() 才会立即返回。SetBusyHook() 注册的回调在 *Async 路径上同样生效。

DownloadAsync()

std::future<std::vector<uint8_t>> DownloadAsync(
const std::string& filename,
uint32_t password = 0,
int timeoutMs = 0) const;

异步从从站读取(下载)文件。失败时 future 持有空 vector,可继续通过 LastErrorCode() 查询错误码。

UploadAsync()

std::future<bool> UploadAsync(
const std::string& filename,
std::vector<uint8_t> data,
uint32_t password = 0,
int timeoutMs = 0) const;

异步上传(写入)文件到从站。data 按值捕获,可在调用后立即析构原对象。

DownloadExAsync() / UploadExAsync()

std::future<std::vector<uint8_t>> DownloadExAsync(
const std::string& filename, uint32_t password = 0,
int timeoutMs = 0, bool enableCrc = false) const;

std::future<bool> UploadExAsync(
const std::string& filename, std::vector<uint8_t> data,
uint32_t password = 0, int timeoutMs = 0, bool enableCrc = false) const;

*Ex 系列的异步版本,额外暴露 enableCrc 参数(启用 CRC 段校验)。

示例 (并行升级多个从站):

#include <future>
#include <vector>

auto& foeA = master.GetSlave(1).GetFoE();
auto& foeB = master.GetSlave(2).GetFoE();

auto fa = foeA.UploadAsync("firmware.bin", LoadFirmware("a.bin"));
auto fb = foeB.UploadAsync("firmware.bin", LoadFirmware("b.bin"));

bool okA = fa.get(); // 阻塞等待从站 1 完成
bool okB = fb.get(); // 阻塞等待从站 2 完成
printf("A=%d B=%d\n", okA, okB);
// 配合 std::future_status::ready 做轮询 / UI 进度
auto fut = foeA.DownloadAsync("config.bin");
while (fut.wait_for(std::chrono::milliseconds(50)) != std::future_status::ready) {
UpdateUiProgressBar();
}
auto data = fut.get();

错误处理

FoEErrorCode 枚举定义传输失败的诊断错误码:

枚举值说明
NotDefined0x8000未定义错误
NotFound0x8001文件未找到
AccessDenied0x8002访问被拒绝
DiskFull0x8003磁盘已满
Illegal0x8004非法操作
PacketNumberWrong0x8005数据包序号错误
AlreadyExists0x8006文件已存在
NoUser0x8007无此用户
BootstrapOnly0x8008仅 Bootstrap 模式可用
NotBootstrap0x8009不在 Bootstrap 模式
NoRights0x800A权限不足
ProgramError0x800B程序错误

GetErrorDescription()

static std::string GetErrorDescription(FoEErrorCode errorCode);

获取 FoE 错误码的中文描述文本。

LastErrorCode() / GetLastError()

FoEErrorCode LastErrorCode() const;
FoEErrorCode GetLastError() const;

获取最近一次 FoE 操作的错误码。当 Download / Upload 失败时,可通过此方法获取具体错误原因。

示例:

auto& foe = master.GetSlave(1).GetFoE();
auto data = foe.Download("firmware.bin");
if (data.empty()) {
auto error = foe.LastErrorCode();
auto desc = FoE::GetErrorDescription(error);
printf("下载失败: %s\n", desc.c_str());
}

完整示例

固件更新

#include "ethercat.hpp"
#include <fstream>
#include <iterator>

using namespace darra;

int main() {
EtherCATMaster master(dll);
master.SetNetwork("\\Device\\NPF_{...}")
.SetENI("config.deni")
.Build();
master.SetState(EcState::OP);
master.Start();

auto& foe = master.GetSlave(1).GetFoE();

// 设置超时
foe.DefaultTimeoutMs(30000); // 30秒

// 加载固件文件
std::ifstream file("new_firmware.bin", std::ios::binary);
std::vector<uint8_t> firmware(
(std::istreambuf_iterator<char>(file)),
std::istreambuf_iterator<char>());

// 上传固件
bool ok = foe.Upload("firmware.bin", firmware);
printf("%s\n", ok ? "固件更新成功" : "固件更新失败");

getchar();
return 0;
}

FoE Cancel / BusyHook

长时间的 FoE 传输(固件升级可达数分钟)需要两个能力:用户取消(关闭对话框、超时、应急停止时中止传输)与细粒度进度(含当前包位置、重试计数、文本描述)。SDK 提供 Cancel() / ClearCancel() / SetBusyHook() 三个 API 组合使用。

FoEBusyEvent

struct FoEBusyEvent {
int slave_index; // 从站索引
uint16_t done; // 已完成包数
uint16_t entire; // 总包数
std::string text; // 状态描述 ("Downloading" / "Erasing flash" / "Verifying" 等)
int retry_index; // 重试计数 (0 表示首次, >0 表示第 N 次重试)

double Percent() const {
return entire > 0 ? 100.0 * done / entire : 0.0;
}
};

Cancel()

class FoE {
public:
bool Cancel();
};

请求取消当前正在进行的 FoE 传输。设置内部取消标志,SDK 在下一次包边界检测到标志后中止传输并返回错误。返回 true 表示取消标志已设置(并不代表传输已实际停止,需等待传输线程响应)。

ClearCancel()

bool ClearCancel();

清除取消标志,为下一次传输做准备。通常在取消后、启动新的 Download / Upload 前调用。

SetBusyHook()

using BusyCallback = std::function<void(const FoEBusyEvent&)>;
void SetBusyHook(BusyCallback cb);

注册 Busy 回调。与 SetProgressCallback 不同,BusyHook 提供更细粒度的状态信息(包括重试计数和文本描述),适合显示在 UI 进度条上。每次包传输前后均会触发。

示例 (C++20 std::jthread + stop_token 取消):

using namespace std::chrono_literals;

auto& foe = master.GetSlave(1).GetFoE();

// 设置 BusyHook 显示详细进度
foe.SetBusyHook([](const FoEBusyEvent& e) {
std::cout << "[从站 " << e.slave_index << "] "
<< e.Percent() << "% "
<< e.text;
if (e.retry_index > 0)
std::cout << " (重试 " << e.retry_index << ")";
std::cout << std::endl;
});

// 清除旧的取消标志
foe.ClearCancel();

// 用 std::jthread 启动固件上传,支持外部取消
std::vector<uint8_t> firmware = LoadFirmware();
std::jthread worker([&](std::stop_token st) {
auto fut = std::async(std::launch::async, [&] {
return foe.Upload("firmware.bin", firmware);
});

// 轮询 stop_token, 触发 cancel
while (fut.wait_for(100ms) == std::future_status::timeout) {
if (st.stop_requested()) {
foe.Cancel();
break;
}
}
bool ok = fut.get();
std::cout << (ok ? "上传完成" : "上传取消或失败") << "\n";
});

// 用户点击取消按钮 / 超时等场景
std::this_thread::sleep_for(30s);
worker.request_stop(); // jthread 自动 join
Cancel 语义
  • Cancel() 仅设置标志,实际中止发生在下一次包边界(最多延迟一个包的传输时间,通常 <100ms)
  • 取消后的 Upload / Download 返回 falseLastErrorCode() 可查询具体原因
  • 取消不会破坏从站 Flash:SDK 仅在接收到从站 ACK 后递增已完成包数,中途取消从站会自动丢弃未完成的写入块
BusyHook vs ProgressCallback
  • ProgressCallback (SetProgressCallback): 简单百分比 + 包号 + 字节数,适合命令行工具
  • BusyHook (SetBusyHook): 增加文本描述和重试计数,适合 GUI 显示状态栏或日志
  • 两者可同时注册,互不影响
参考

ETG.1020 §15 FoE 取消语义; ETG.5003 §8 固件升级最佳实践。