端到端最小闭环:headless_run
标准示例文档:源码清单、学习路线、逐段讲解、常见误区、双平台构建、真实输出与退出码。
用途
用公开头跑通最短路径:RegisterBuiltinTools() → ToolGroup::Create → AddTool → RunSession::Run(),再把「工具是否在途」接到采集端联合停流门禁,最后由宿主显式停两端。
读完应能回答三个问题:谁拥有流与工具?Run 的返回值怎么读?为什么停相机必须由宿主显式做?
源码
examples/headless_run.cpp—— 入口main():建组、装载工具、跑一次、取消、停两端(片段 1–5) · 查看完整源文件examples/headless_camera.cpp—— 宿主侧相机接线实现:ArmCamera/WireToolWorkHook/StopCameraExplicitly(片段 6–8) · 查看完整源文件examples/headless_camera.hpp—— 上述三函数的声明(宿主集成示范的公开外形) · 查看完整源文件examples/CMakeLists.txt——headless_run目标:链接Vyntek::toolgroup,产物进build/<preset>/bin/· 查看完整源文件
下文片段均为源码节选,标注行号,与仓库当前源码逐字一致。
学习路线
按四步读,每步对应下文片段:
- 对象从哪来(片段 1):
ToolGroup是shared_ptr,工具是unique_ptr且被流接管,RunSession只观察。 - 两个 session 如何分工(片段 2、6、7):编排侧与采集侧各自独立;钩子只上报「工具在途」,不碰采集状态。
- 结果怎么读(片段 3、4):先看
RunStatus,再看diagnostics;GetOutput是标量投影,不是保留集。 - 怎么退出(片段 5、8):取消是协作请求;停流是宿主的显式动作,且必须等到
Stopped。
片段 1 · 建组、注册、装载(headless_run.cpp:16-32)
查看完整源文件 using namespace vyntek::toolgroup;
using vyntek::imaging::AcquisitionSession;
auto group = ToolGroup::Create("headless");
if (!group) {
std::fprintf(stderr, "ToolGroup::Create failed\n");
return 1;
}
ToolRegistry registry;
registry.RegisterBuiltinTools();
auto tool = registry.Create("passthrough", "t1");
if (!tool || !group->AddTool(std::move(tool))) {
std::fprintf(stderr, "passthrough AddTool failed\n");
return 2;
}
RunSession session(group);讲解:
ToolGroup::Create返回std::shared_ptr<ToolGroup>,空即失败——先判空再解引用。RegisterBuiltinTools()注册内建工具;当前只有passthrough这个契约夹具,它的Run是基类桩(只报NotImplemented)。registry.Create(...)返回std::unique_ptr<Tool>;group->AddTool(std::move(tool))接管所有权并返回非拥有的观察指针。判空用返回值,不要再用已 move 的tool。RunSession session(group)只观察流,不取得流的所有权。
片段 2 · 把工具执行接到采集端(headless_run.cpp:34-40)
查看完整源文件 // DEBT-IMG-004 host wiring sketch: drive the acquisition joint stop gate from real tool
// execution instead of the skeleton's default-true flag.
AcquisitionSession camera;
if (const int rc = headless::ArmCamera(camera, "headless-cam"); rc != 0) {
return rc;
}
headless::WireToolWorkHook(session, camera);讲解:
AcquisitionSession是采集端对象;RunSession是编排端对象。两者生命周期独立,示例里并列构造。ArmCamera内部先Arm再Start(片段 6),失败直接以 6 退出。WireToolWorkHook只做一件事:把「工具在途」回调转成camera.SetToolWorkDone(bool)(片段 7)。它不停止相机。
片段 3 · 读取 Run 结果(headless_run.cpp:42-46)
查看完整源文件 const RunResult result = session.Run();
if (result.status != RunStatus::Succeeded) {
std::fprintf(stderr, "Run status %d\n", static_cast<int>(result.status));
return 3;
}讲解:
Run()同步执行,返回RunResult。判成功用status == RunStatus::Succeeded(或result.ok())。- 失败细节看
result.diagnostics,message只是摘要;failed_tool_id指出是哪个工具触发的 abort。 Busy表示「另一个 Run 仍在途」,不是失败:先结束在途那次,不要忙等重试。
片段 4 · GetOutput 的误区(headless_run.cpp:48-52)
查看完整源文件 // GetOutput is not KeepLatest retention (output-inspection).
if (!session.GetOutput("t1.out").empty()) {
std::fprintf(stderr, "GetOutput stub must stay empty\n");
return 4;
}讲解:
GetOutput只做标量投影;图像类输出没有字符串形式,因此这里必须是空串。空串不代表 Run 失败。GetOutput不是 KeepLatest 保留语义;保留集/观察入口在toolgroup/output_inspector.hpp(见 output-inspection)。- 这条断言防止有人把「观察」误接到「执行结果」上。
片段 5 · 取消是请求,不是停流(headless_run.cpp:54-58)
查看完整源文件 session.RequestCancel();
if (!session.cancel_requested()) {
std::fprintf(stderr, "RequestCancel did not latch\n");
return 5;
}讲解:
RequestCancel()只置位协作取消;cancel_requested()反映「已请求」,不保证工具已退出。- 取消对采集端没有任何作用:它不会替你
RequestStop。
片段 6 · 采集端启动(headless_camera.cpp:9-15)
查看完整源文件int ArmCamera(vyntek::imaging::AcquisitionSession & camera, const char * name) {
if (camera.Arm(name) != vyntek::imaging::Status::Ok || camera.Start() != vyntek::imaging::Status::Ok) {
std::fprintf(stderr, "AcquisitionSession Arm/Start failed\n");
return 6;
}
return 0;
}讲解:Arm 建立命名会话,Start 进入 Running。两步都用 Status 返回,示例用 OR 短路——任一失败都不继续。
片段 7 · 钩子只上报(headless_camera.cpp:17-19)
查看完整源文件void WireToolWorkHook(vyntek::toolgroup::RunSession & session, vyntek::imaging::AcquisitionSession & camera) {
session.SetToolWorkDoneHook([&camera](bool tool_work_done) { camera.SetToolWorkDone(tool_work_done); });
}讲解:
- 回调参数
tool_work_done:进入工具执行发false,退出执行发true,覆盖正常结束、工具失败、协作取消、缺流、Validate 失败全部路径。 - lambda 捕获
camera的引用:相机必须活得比 hook 久。示例里camera在main栈上,顺序安全。 - Busy 早返回时钩子不武装:否则会误报「工具已结束」而错误解锁停流门禁。
片段 8 · 宿主显式停流(headless_camera.cpp:21-38)
查看完整源文件int StopCameraExplicitly(vyntek::imaging::AcquisitionSession & camera) {
// The hook did NOT stop the camera — that is the host's job, done explicitly.
if (camera.state() != vyntek::imaging::AcquisitionState::Running) {
std::fprintf(stderr, "tool-work hook must not stop the acquisition end\n");
return 7;
}
if (!camera.stop_progress().tool_work_done) {
std::fprintf(stderr, "tool work must be reported done after Run returns\n");
return 8;
}
if (camera.RequestStop() != vyntek::imaging::Status::Ok ||
camera.WaitStop(vyntek::imaging::StopTimeout{ 1000 }) != vyntek::imaging::Status::Ok ||
camera.state() != vyntek::imaging::AcquisitionState::Stopped) {
std::fprintf(stderr, "explicit host stop must reach Stopped\n");
return 9;
}
return 0;
}讲解(退出码 7/8/9 就是这三条断言的编号):
state() == Running:证明 Run 返回后钩子没有把相机停掉(M3-07 / TC-SESS-05:两端不自动联动)。stop_progress().tool_work_done:证明工具工作已上报完成,联合门禁可以解锁。RequestStop()+WaitStop(StopTimeout{1000})到Stopped:停流是宿主显式动作;超时不会伪造Stopped。
常见误区
- 「Run 成功就说明算法算完了」 ——
passthrough是夹具,基类Run只报NotImplemented;空组Run仍可Succeeded - 「
RequestCancel会顺便停相机」 —— 不会。两端各停各的,宿主必须显式调两次 - 「
Latest()是队列头」 ——Latest()是所有权预览槽(最近一次TryPostOwned的 Share);FIFO 头是PeekIngressFront() - 「
GetOutput空串 = 没跑成功」 —— 空串是投影限制,不是状态
构建与运行
VYNTEK_BUILD_EXAMPLES=ON 才会构建示例;ninja-debug-all 预设已同时打开 tests 与 examples。二进制统一落在 build/ninja-debug-all/bin/。
Linux(本机 VPS,AGENTS.md「本机 VPS」小节)
cmake --preset ninja-debug-all
cmake --build --preset ninja-debug-all --target headless_run
./build/ninja-debug-all/bin/headless_runWindows(PowerShell,AGENTS.md「构建」小节,仓库在 D:\Works\Vyntek)
cmake --preset ninja-debug-all
cmake --build --preset ninja-debug-all --target headless_run
.\build\ninja-debug-all\bin\headless_run.exe若坚持用 AGENTS.md 的单预设写法,需显式加开关:
cmake --preset ninja-debug -DVYNTEK_BUILD_TESTS=ON -DVYNTEK_BUILD_EXAMPLES=ON。
输出
本机实跑(Linux,2026-10-22):
$ ./build/ninja-debug-all/bin/headless_run
vyntek 0.1.0 (commit <build-sha>)
headless_run ok
$ echo $?
0<build-sha> 是构建时的 HEAD(示例编译进二进制),因此与阅读时的仓库提交不一定相同。
退出码
0—— 全部步骤通过,打印headless_run ok1——ToolGroup::Create("headless")返回空2——registry.Create("passthrough", "t1")或AddTool失败3——Run()结果不是Succeeded(细节看RunResult.diagnostics)4——GetOutput("t1.out")非空——标量投影不应有值5——RequestCancel()后cancel_requested()未置位6——AcquisitionSession::Arm/Start失败7—— Run 结束后采集端已不是Running——钩子不得自动停相机(M3-07)8——stop_progress().tool_work_done为 false——工具工作未上报完成9—— 显式RequestStop+WaitStop(1000)未到Stopped
7/8/9 不是普通错误码,而是契约断言:任何一条触发,说明「两端停止互不联动」这条边界被改坏了。