diff --git a/.claude/settings.local.json b/.claude/settings.local.json
deleted file mode 100644
index 6d50327..0000000
--- a/.claude/settings.local.json
+++ /dev/null
@@ -1,42 +0,0 @@
-{
- "permissions": {
- "allow": [
- "Bash(git commit -m ':*)",
- "Bash(/bin/bash -lc 'DOTNET_CLI_HOME=/tmp NUGET_PACKAGES=/tmp/nuget-packages dotnet build FlyshotReplacement.sln -v minimal 2>&1')",
- "Bash(/bin/bash -lc 'DOTNET_CLI_HOME=/tmp NUGET_PACKAGES=/tmp/nuget-packages dotnet test tests/Flyshot.Server.IntegrationTests/Flyshot.Server.IntegrationTests.csproj -v minimal 2>&1')",
- "Bash(/bin/bash -lc 'DOTNET_CLI_HOME=/tmp NUGET_PACKAGES=/tmp/nuget-packages dotnet test FlyshotReplacement.sln --no-build -v minimal 2>&1')",
- "Bash(DOTNET_CLI_HOME=/tmp NUGET_PACKAGES=/tmp/nuget-packages dotnet build FlyshotReplacement.sln --no-restore -v minimal)",
- "Bash(DOTNET_CLI_HOME=/tmp NUGET_PACKAGES=/tmp/nuget-packages dotnet test tests/Flyshot.Server.IntegrationTests/Flyshot.Server.IntegrationTests.csproj --no-build -v minimal)",
- "Bash(python -c \"import json; json.load\\(open\\('.vscode/launch.json'\\)\\); json.load\\(open\\('.vscode/tasks.json'\\)\\); print\\('JSON valid.'\\)\")",
- "Bash(python -c \"import json; json.load\\(open\\('.vscode/launch.json', encoding='utf-8'\\)\\); json.load\\(open\\('.vscode/tasks.json', encoding='utf-8'\\)\\); print\\('JSON valid.'\\)\")",
- "Bash(/bin/bash -lc 'DOTNET_CLI_HOME=/tmp NUGET_PACKAGES=/tmp/nuget-packages dotnet build FlyshotReplacement.sln --no-restore -v minimal')",
- "Bash(/bin/bash -lc 'DOTNET_CLI_HOME=/tmp NUGET_PACKAGES=/tmp/nuget-packages dotnet test tests/Flyshot.Core.Tests/Flyshot.Core.Tests.csproj -v minimal')",
- "Bash(/bin/bash -lc 'DOTNET_CLI_HOME=/tmp NUGET_PACKAGES=/tmp/nuget-packages dotnet test tests/Flyshot.Server.IntegrationTests/Flyshot.Server.IntegrationTests.csproj -v minimal')",
- "Bash(/bin/bash -lc 'DOTNET_CLI_HOME=/tmp NUGET_PACKAGES=/tmp/nuget-packages dotnet build FlyshotReplacement.sln --no-restore -v minimal 2>&1')",
- "Bash(taskkill /PID 120812 /F)",
- "Bash(/bin/bash -lc 'DOTNET_CLI_HOME=/tmp NUGET_PACKAGES=/tmp/nuget-packages dotnet build flyshot-replacement.sln --no-restore -v minimal 2>&1')",
- "Bash(/bin/bash -lc 'DOTNET_CLI_HOME=/tmp NUGET_PACKAGES=/tmp/nuget-packages dotnet build /d/Dev/Codes/rvbust-code/FlyingShotPkg_3.15_VDA/flyshot-replacement/FlyshotReplacement.sln --no-restore -v minimal 2>&1')",
- "Bash(capinfos \"20260430.pcap\")",
- "Bash(tshark -r \"20260430.pcap\" -T fields -e frame.number -e frame.time_relative -e ip.src -e ip.dst -e udp.srcport -e udp.dstport -e data.len)",
- "Bash(tshark -r \"20260430.pcap\" -Y \"udp.port==60015\" -T fields -e frame.number -e frame.time_relative -e ip.src -e udp.srcport -e udp.length -e data.len)",
- "Bash(tshark -r \"20260430.pcap\" -Y \"udp.port==60015 and udp.srcport==60015\" -T fields -e frame.number -e frame.time_relative -e udp.length)",
- "Bash(tshark -r \"20260430.pcap\" -Y \"udp.port==60015 and udp.dstport==60015\" -T fields -e frame.number -e frame.time_relative -e udp.length)",
- "Bash(tshark -r \"20260430.pcap\" -Y \"udp.port==60015\" -T fields -e frame.number -e frame.time_relative -e ip.src -e ip.dst -e udp.srcport -e udp.dstport -e udp.length -e frame.protocols)",
- "Bash(tshark -r \"20260430.pcap\" -Y \"udp.port==60015 and frame.number==1\" -V)",
- "Bash(tshark -r \"20260430.pcap\" -Y \"udp.port==60015 and udp.dstport!=60015\" -T fields -e frame.number -e frame.time_relative -e ip.dst -e udp.dstport)",
- "Bash(tshark -r \"20260430.pcap\" -Y \"udp.dstport==60015\" -T fields -e frame.number -e frame.time_relative -e ip.src -e udp.length)",
- "Bash(tshark -r \"20260430.pcap\" -Y \"frj519\" -T fields -e frj519.type -e frj519.seq -e frj519.status -e frj519.j1 -e frj519.j2 -e frj519.j3 -e frj519.j4 -e frj519.j5 -e frj519.j6 -e frj519.j7 -e frj519.j8 -e frj519.j9 -e frj519.timestamp)",
- "Bash(tshark -r \"20260430.pcap\" -Y \"frj519\" -T fields -e frj519.type -e frj519.seq -e frj519.status -e frj519.j1 -e frj519.j2 -e frj519.j3 -e frj519.j4 -e frj519.j5 -e frj519.j6 -e frj519.j7 -e frj519.j8 -e frj519.j9 -e frj519.timestamp -e frj519.x -e frj519.y -e frj519.z -e frj519.w -e frj519.p -e frj519.r)",
- "Bash(tshark -r \"20260430.pcap\" -Y \"frj519\" -T fields -E header=y)",
- "Bash(tshark -G fields)",
- "Bash(tshark -r \"20260430.pcap\" -Y \"frj519\" -T fields -e frame.time_relative -e data)",
- "Bash(tshark -r \"20260430.pcap\" -Y \"udp.dstport==60015\" -T fields -e frame.number -e frame.time_relative -e data)",
- "Bash(tshark -r \"20260430.pcap\" -Y \"udp.port==60015 and udp.srcport==60015\" -T fields -e frame.number -e frame.time_relative -e data)",
- "Bash(tshark -r \"20260430.pcap\" --disable-protocol frj519 -Y \"udp.dstport==60015\" -T fields -e frame.number -e frame.time_relative -e data)",
- "Bash(/bin/bash -lc 'DOTNET_CLI_HOME=/tmp NUGET_PACKAGES=/tmp/nuget-packages dotnet build src/Flyshot.Runtime.Fanuc/Flyshot.Runtime.Fanuc.csproj --no-restore -v minimal 2>&1 | tail -20')",
- "Bash(/bin/bash -lc 'DOTNET_CLI_HOME=/tmp NUGET_PACKAGES=/tmp/nuget-packages dotnet build FlyshotReplacement.sln --no-restore -v minimal 2>&1 | tail -25')",
- "Bash(/bin/bash -lc 'DOTNET_CLI_HOME=/tmp NUGET_PACKAGES=/tmp/nuget-packages dotnet build FlyshotReplacement.sln --no-restore -v minimal 2>&1 | tail -15')",
- "Bash(/bin/bash -lc 'DOTNET_CLI_HOME=/tmp NUGET_PACKAGES=/tmp/nuget-packages dotnet test tests/Flyshot.Core.Tests/Flyshot.Core.Tests.csproj --no-restore -v minimal 2>&1 | tail -30')"
- ]
- }
-}
diff --git a/Config/RobotConfig.json b/Config/RobotConfig.json
index 711ebf8..78f5d6c 100644
--- a/Config/RobotConfig.json
+++ b/Config/RobotConfig.json
@@ -1,305 +1,454 @@
-{
- "robot": {
- "use_do": true,
- "io_addr": [
- 7,
- 8
- ],
- "io_keep_cycles": 2,
- "trigger_sample_index_offset_cycles": 6,
- "acc_limit": 1,
- "jerk_limit": 1,
- "adapt_icsp_try_num": 5,
- "speed_ratio": 1,
- "planning_speed_scale": 0.74227,
- "smooth_start_stop_timing": true
- },
- "flying_shots": {
- "UTTC_MS11": {
- "traj_waypoints": [
- [
- 1.056731,
- 0.011664811,
- -0.017892333,
- -0.01516874,
- 0.021492079,
- 0.009567846
- ],
- [
- 0.8532358,
- 0.03837953,
- -0.19235304,
- 0.0071595116,
- 0.109054826,
- 0.040055145
- ],
- [
- 0.96600056,
- 0.20607172,
- -0.12233179,
- -1.2394339,
- 0.10493033,
- 1.2958988
- ],
- [
- 0.9618476,
- 0.15288207,
- -0.14867093,
- -0.7176314,
- 0.1764264,
- 0.73228663
- ],
- [
- 0.76189893,
- -0.028442925,
- -0.30919823,
- 0.10463613,
- 0.5615024,
- -0.39399016
- ],
- [
- 1.1271763,
- 0.074403025,
- -0.27347943,
- -0.5227772,
- 0.52098846,
- 0.79633313
- ],
- [
- 1.0555661,
- 0.4026262,
- -0.08746306,
- 0.6301835,
- 0.09644133,
- -0.5463328
- ],
- [
- 1.2300354,
- 0.28612664,
- -0.23486805,
- -0.4868128,
- 0.25369516,
- 0.55347764
- ],
- [
- 1.2144431,
- -0.29855102,
- -0.15202847,
- -1.0205934,
- 0.13317892,
- 1.1246506
- ],
- [
- 1.2840607,
- -0.11222197,
- -0.16805042,
- -2.248135,
- 0.2560587,
- 2.4434967
- ],
- [
- 1.3189346,
- -0.25620222,
- -0.12730704,
- -2.285038,
- 0.30872014,
- 2.4765089
- ],
- [
- 1.502615,
- -0.25304365,
- -0.23878741,
- -1.2194318,
- 0.46674785,
- 1.5533328
- ],
- [
- 1.07723,
- -0.07387611,
- -0.1707704,
- -1.8916591,
- 0.38677844,
- 2.061968
- ],
- [
- 1.3920237,
- 0.08098731,
- -0.2672306,
- -0.9780007,
- 0.4561093,
- 0.9102286
- ],
- [
- 1.9016331,
- 0.023924276,
- -0.58633333,
- -0.8441697,
- 0.76730615,
- 1.4842151
- ],
- [
- 1.9300697,
- -0.06738541,
- -0.56542397,
- -0.892083,
- 0.77194446,
- 1.5293273
- ],
- [
- 2.0611632,
- -0.30327517,
- -0.54225636,
- -1.0395275,
- 0.8505439,
- 1.6429617
- ],
- [
- 1.0921186,
- -0.40034482,
- -0.1803499,
- 1.3524796,
- 0.6210477,
- -1.2159473
- ],
- [
- 1.0521278,
- -0.40034503,
- -0.1803492,
- 1.3524843,
- 0.6210471,
- -1.2159531
- ],
- [
- 1.056731,
- 0.011664811,
- -0.017892333,
- -0.01516874,
- 0.021492079,
- 0.009567846
- ]
- ],
- "shot_flags": [
- false,
- true,
- true,
- true,
- true,
- true,
- true,
- true,
- true,
- true,
- true,
- true,
- true,
- true,
- true,
- true,
- true,
- false,
- true,
- false
- ],
- "offset_values": [
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0
- ],
- "addr": [
- [],
- [
- 2,
- 4
- ],
- [
- 3,
- 4,
- 2
- ],
- [
- 3,
- 4,
- 2
- ],
- [
- 4,
- 2
- ],
- [
- 4,
- 2
- ],
- [
- 3,
- 4
- ],
- [
- 3,
- 4
- ],
- [
- 4,
- 2
- ],
- [
- 4,
- 2
- ],
- [
- 4,
- 2
- ],
- [
- 4,
- 2
- ],
- [
- 4,
- 2
- ],
- [
- 4,
- 3
- ],
- [
- 4,
- 2
- ],
- [
- 4,
- 2
- ],
- [
- 4,
- 2
- ],
- [
- 4,
- 2
- ],
- [
- 4,
- 3
- ],
- []
- ]
- }
- }
-}
+{
+ "robot": {
+ "use_do": true,
+ "io_addr": [
+ 7,
+ 8
+ ],
+ "io_keep_cycles": 2,
+ "trigger_sample_index_offset_cycles": 6,
+ "acc_limit": 1,
+ "jerk_limit": 1,
+ "adapt_icsp_try_num": 5,
+ "planning_speed_scale": 0.74227,
+ "speed_ratio": 1,
+ "smooth_start_stop_timing": true
+ },
+ "flying_shots": {
+ "UTTC_MS11": {
+ "traj_waypoints": [
+ [
+ 1.056731,
+ 0.011664811,
+ -0.017892333,
+ -0.01516874,
+ 0.021492079,
+ 0.009567846
+ ],
+ [
+ 0.8532358,
+ 0.03837953,
+ -0.19235304,
+ 0.0071595116,
+ 0.109054826,
+ 0.040055145
+ ],
+ [
+ 0.96600056,
+ 0.20607172,
+ -0.12233179,
+ -1.2394339,
+ 0.10493033,
+ 1.2958988
+ ],
+ [
+ 0.9618476,
+ 0.15288207,
+ -0.14867093,
+ -0.7176314,
+ 0.1764264,
+ 0.73228663
+ ],
+ [
+ 0.76189893,
+ -0.028442925,
+ -0.30919823,
+ 0.10463613,
+ 0.5615024,
+ -0.39399016
+ ],
+ [
+ 1.1271763,
+ 0.074403025,
+ -0.27347943,
+ -0.5227772,
+ 0.52098846,
+ 0.79633313
+ ],
+ [
+ 1.0555661,
+ 0.4026262,
+ -0.08746306,
+ 0.6301835,
+ 0.09644133,
+ -0.5463328
+ ],
+ [
+ 1.2300354,
+ 0.28612664,
+ -0.23486805,
+ -0.4868128,
+ 0.25369516,
+ 0.55347764
+ ],
+ [
+ 1.2144431,
+ -0.29855102,
+ -0.15202847,
+ -1.0205934,
+ 0.13317892,
+ 1.1246506
+ ],
+ [
+ 1.2840607,
+ -0.11222197,
+ -0.16805042,
+ -2.248135,
+ 0.2560587,
+ 2.4434967
+ ],
+ [
+ 1.3189346,
+ -0.25620222,
+ -0.12730704,
+ -2.285038,
+ 0.30872014,
+ 2.4765089
+ ],
+ [
+ 1.502615,
+ -0.25304365,
+ -0.23878741,
+ -1.2194318,
+ 0.46674785,
+ 1.5533328
+ ],
+ [
+ 1.07723,
+ -0.07387611,
+ -0.1707704,
+ -1.8916591,
+ 0.38677844,
+ 2.061968
+ ],
+ [
+ 1.3920237,
+ 0.08098731,
+ -0.2672306,
+ -0.9780007,
+ 0.4561093,
+ 0.9102286
+ ],
+ [
+ 1.9016331,
+ 0.023924276,
+ -0.58633333,
+ -0.8441697,
+ 0.76730615,
+ 1.4842151
+ ],
+ [
+ 1.9300697,
+ -0.06738541,
+ -0.56542397,
+ -0.892083,
+ 0.77194446,
+ 1.5293273
+ ],
+ [
+ 2.0611632,
+ -0.30327517,
+ -0.54225636,
+ -1.0395275,
+ 0.8505439,
+ 1.6429617
+ ],
+ [
+ 1.0921186,
+ -0.40034482,
+ -0.1803499,
+ 1.3524796,
+ 0.6210477,
+ -1.2159473
+ ],
+ [
+ 1.0521278,
+ -0.40034503,
+ -0.1803492,
+ 1.3524843,
+ 0.6210471,
+ -1.2159531
+ ],
+ [
+ 1.056731,
+ 0.011664811,
+ -0.017892333,
+ -0.01516874,
+ 0.021492079,
+ 0.009567846
+ ]
+ ],
+ "shot_flags": [
+ false,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ false,
+ true,
+ false
+ ],
+ "offset_values": [
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "addr": [
+ [],
+ [
+ 2,
+ 4
+ ],
+ [
+ 3,
+ 4,
+ 2
+ ],
+ [
+ 3,
+ 4,
+ 2
+ ],
+ [
+ 4,
+ 2
+ ],
+ [
+ 4,
+ 2
+ ],
+ [
+ 3,
+ 4
+ ],
+ [
+ 3,
+ 4
+ ],
+ [
+ 4,
+ 2
+ ],
+ [
+ 4,
+ 2
+ ],
+ [
+ 4,
+ 2
+ ],
+ [
+ 4,
+ 2
+ ],
+ [
+ 4,
+ 2
+ ],
+ [
+ 4,
+ 3
+ ],
+ [
+ 4,
+ 2
+ ],
+ [
+ 4,
+ 2
+ ],
+ [
+ 4,
+ 2
+ ],
+ [
+ 4,
+ 2
+ ],
+ [
+ 4,
+ 3
+ ],
+ []
+ ]
+ },
+ "WhP_test": {
+ "traj_waypoints": [
+ [
+ 1.056731,
+ 0.011664811,
+ -0.017892333,
+ -0.01516874,
+ 0.021492079,
+ 0.009567846
+ ],
+ [
+ 0.2153671,
+ 0.015257306,
+ -0.024938405,
+ -0.059105303,
+ 0.06653474,
+ -0.005172012
+ ],
+ [
+ 0.47362572,
+ 0.015260376,
+ -0.024971163,
+ -0.059100218,
+ 0.06653733,
+ -0.00518099
+ ],
+ [
+ -0.0861789,
+ 0.024431378,
+ 0.15767263,
+ -0.059098743,
+ 0.06653532,
+ -0.0051812795
+ ],
+ [
+ -0.029219253,
+ 0.024431676,
+ 0.24181713,
+ -0.81706756,
+ 0.06653719,
+ -0.0051850453
+ ],
+ [
+ 1.056731,
+ 0.011664811,
+ -0.017892333,
+ -0.01516874,
+ 0.021492079,
+ 0.009567846
+ ]
+ ],
+ "shot_flags": [
+ false,
+ true,
+ true,
+ true,
+ true,
+ false
+ ],
+ "offset_values": [
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "addr": [
+ [],
+ [
+ 2,
+ 3,
+ 4
+ ],
+ [
+ 2,
+ 4,
+ 3
+ ],
+ [
+ 1,
+ 2,
+ 3
+ ],
+ [
+ 1,
+ 2,
+ 3
+ ],
+ []
+ ]
+ },
+ "demo-flyshot": {
+ "traj_waypoints": [
+ [
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ [
+ 0.001,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ [
+ 0.002,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ [
+ 0.003,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ ],
+ "shot_flags": [
+ false,
+ true,
+ false,
+ false
+ ],
+ "offset_values": [
+ 0,
+ 1,
+ 0,
+ 0
+ ],
+ "addr": [
+ [],
+ [
+ 7,
+ 8
+ ],
+ [],
+ []
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/README.md b/README.md
index c2af03a..d87c8cd 100644
--- a/README.md
+++ b/README.md
@@ -84,6 +84,7 @@
1. 配置与测试基线
- [x] 修正 `ConfigCompatibilityTests` 当前样本路径漂移:`Rvbust/EOL10_EAU_0/RobotConfig.json` 不再包含 `001`,应改用稳定样本或更新断言。
- [x] 将 `RobotConfig.json` 中的 `use_do`、`io_keep_cycles`、`acc_limit`、`jerk_limit`、`adapt_icsp_try_num` 全部贯通到规划和执行链路。
+ - [x] 恢复 `use_do` 的输出选择语义:`true` 使用 DO(2),`false` 使用 RO(9),两种模式均保留飞拍脉冲与清零;见 [输出选择说明](docs/use-do-output-selection.md)。
- [x] 将上传飞拍轨迹统一保存到运行目录 `Config/RobotConfig.json` 的 `flying_shots` 节点。
- [x] 为新 HTTP API 补一份当前现场调用顺序文档,替代旧 `ControllerClient` 工作流:见 `docs/fanuc-field-runtime-workflow.md`。
@@ -117,6 +118,7 @@
- [x] 将飞拍 IO 触发的 `write_io_type/index/mask/value` 与现场控制柜实际 IO 地址逐项对齐;UTTC golden tests 确认 17 个触发点对应 17 个 UDP IO set 脉冲、17 个 clear 帧,mask 集合为 `10/12/14`。
- [x] 将 `MoveJoint` 从单点最终目标改为临时 PTP 稠密轨迹:按 `status=15` 运动窗口统计,speed=1 抓包 40 点,speed=0.7 抓包 55 点,speed=0.5 抓包 77 点,路径为关节空间直线 + smoothstep 进度。
- [x] `ExecuteFlyShotTraj(move_to_start=true)` 复用临时 PTP 稠密轨迹移动到规划起点,并等待运行时完成后再启动飞拍轨迹,避免第一帧 J519 目标突变导致控制柜报警。
+ - [x] `MoveJoint` 和飞拍预移动统一按 `planning_speed_scale * speed_ratio` 生成点对点轨迹,速度、加速度、跃度分别按有效倍率的一、二、三次方缩放,发送周期保持不变。
- [x] `ExecuteFlyShotTraj(wait=true)` 等待正式飞拍轨迹执行完成后再返回;HTTP `/execute_flyshot/` 已接入旧抓包中的 `wait` 字段,默认值为 `true`。
6. 真机联调与运行安全
diff --git a/docs/flyshot-system-guide.md b/docs/flyshot-system-guide.md
new file mode 100644
index 0000000..ba0fe6c
--- /dev/null
+++ b/docs/flyshot-system-guide.md
@@ -0,0 +1,573 @@
+# 飞拍系统介绍说明与使用教程
+
+## 1. 文档目的
+
+本文档用于说明当前飞拍系统的整体组成、数据流、接口边界和现场使用步骤。文档原稿采用 Markdown 编写,适合后续导入或整理为飞书文档;流程图使用 Mermaid 源码保留,导入飞书时可按需要渲染为图片后插入。
+
+系统当前分为三个主要单元:
+
+| 单元 | 工程位置 | 主要职责 |
+| --- | --- | --- |
+| 上位机单元 | `D:\Dev\Codes\MFD_Solution\FlyShot` | 控制相机、接收 PLC/MES 信息、发起飞拍任务、保存图片、调用 AI 检测、汇总结果并回传 MES/PLC |
+| 飞拍控制单元 | `D:\Dev\Codes\rvbust-code\FlyingShotPkg_3.15_VDA\flyshot-replacement` | 提供 HTTP 控制接口,负责机器人初始化、轨迹上传、ICSP/self-adapt-ICSP 规划、触发时序生成和 FANUC 运动执行 |
+| YOLO AI 识别模块 | 由上位机配置访问 | 接收图片检测请求,完成 AI 推理,返回图片级 OK/NG、原因、参数明细和结果图路径 |
+
+## 2. 系统总览
+
+```mermaid
+flowchart LR
+ PLC["PLC / OPC UA
任务触发、状态反馈"]
+ MES["MES
换型下发、PartProcessed 上报"]
+ PC["上位机单元 FlyShot
WPF 主控程序"]
+ Camera["工业相机
采图与本地存图"]
+ Control["飞拍控制单元
ASP.NET Core HTTP API"]
+ Robot["FANUC 机器人
TCP 10010/10012 + UDP 60015"]
+ AI["YOLO AI 识别模块
HTTP + 文件传输目录"]
+ Storage["本地/远端图片目录
原图、结果图、任务 JSON"]
+ Notify["飞书 / 审核平台 / ClickHouse
异常通知与追溯"]
+
+ PLC <--> PC
+ MES <--> PC
+ PC --> Camera
+ Camera --> PC
+ PC <--> Control
+ Control <--> Robot
+ PC <--> AI
+ PC --> Storage
+ AI --> Storage
+ PC --> Notify
+```
+
+### 2.1 上位机单元
+
+上位机是现场操作和生产流程的核心入口,当前是 WPF MVVM 程序。它负责:
+
+- 初始化相机并注册图像回调。
+- 读取 `appConfig.json`、`Config\TypeData`、`Config\FlyTask` 中的现场配置。
+- 通过 `MpcService` 调用飞拍控制单元 HTTP 接口。
+- 按飞拍任务配置设置相机曝光、增益和拍摄索引。
+- 将图片保存到 `CameraSavePath` 下的任务目录。
+- 通过 `ImageProcessingService` 上传图片到 AI 服务器目录并调用 AI 接口。
+- 将 AI 结果合并回任务对象,保存任务 JSON。
+- 根据结果回写 PLC、发送 MES `PartProcessed`、写入 ClickHouse、必要时发送飞书 NG 报警。
+
+### 2.2 飞拍控制单元
+
+飞拍控制单元是新的 replacement 服务端。它不恢复旧 `50001/TCP+JSON` 网关,而是提供 ASP.NET Core HTTP API。它负责:
+
+- 机器人模型加载与初始化。
+- 轨迹上传、查询、删除。
+- 普通轨迹 `ExecuteTrajectory`。
+- 飞拍轨迹 `ExecuteFlyShotTraj`。
+- 通过 `icsp` 或 `self-adapt-icsp` 生成关节轨迹。
+- 根据 `shot_flags / offset_values / addrs` 生成拍照触发时间轴。
+- 将规划结果转换为 8ms 发送队列,通过 J519 通道控制机器人运动和 IO 脉冲。
+- 提供 `/status.html` 状态页和 `/debug.html` 调试页。
+
+### 2.3 YOLO AI 识别模块
+
+YOLO AI 模块由上位机调用。上位机当前不是直接把所有图片以 multipart 方式传给 AI,而是:
+
+1. 将图片上传到配置的 AI 输入目录。
+2. 构造包含模型版本、配置版本、产品 ID 和图片明细的 JSON 请求。
+3. 调用 `Ai_Model.ApiUrl`。
+4. 根据 AI 响应中的 `SubImgInfo` 下载结果图。
+5. 将图片级结果、原因、参数明细写回任务结果。
+6. 清理远端上传图和结果图。
+
+## 3. 生产主流程
+
+```mermaid
+sequenceDiagram
+ participant PLC as PLC
+ participant PC as 上位机 FlyShot
+ participant CTRL as 飞拍控制单元
+ participant ROBOT as FANUC 机器人
+ participant CAM as 工业相机
+ participant AI as YOLO AI
+ participant MES as MES
+
+ PLC->>PC: StartFlyShotTask 上升沿 + 产品/任务信息
+ PC->>PC: 初始化 WorkStationTask / FlyShotTask
+ PC->>CAM: 注册回调、设置曝光/增益
+ PC->>CTRL: POST /execute_flyshot/
+ CTRL->>ROBOT: move_to_start + J519 飞拍轨迹
+ ROBOT-->>CAM: 轨迹 IO 脉冲触发拍照
+ CAM-->>PC: 图像回调
+ PC->>PC: 保存原图并加入待处理队列
+ PC->>AI: 上传图片 + 发送 AI 请求
+ AI-->>PC: 返回 OK/NG、原因、结果图路径
+ PC->>PC: 下载结果图并汇总任务结果
+ PC-->>PLC: TaskCompleted / TaskResult / WorkStationResult
+ PC->>MES: PartProcessed 结果报文
+```
+
+## 4. 关键配置
+
+### 4.1 上位机配置
+
+上位机配置文件:
+
+- 基础配置:`D:\Dev\Codes\MFD_Solution\FlyShot\appConfig.json`
+- 调试覆盖:`D:\Dev\Codes\MFD_Solution\FlyShot\appConfig.Debug.json`
+
+常用配置项:
+
+| 配置路径 | 示例值 | 说明 |
+| --- | --- | --- |
+| `MesConfig.EnableMes` | `true` | 是否启用 MES 通信 |
+| `MesConfig.ServerIp` / `ServerPort` | `174.34.82.193` / `55065` | MES 服务地址 |
+| `MesConfig.LineNo` / `ProcessNo` / `StatNo` | `6958` / `993002` / `9930` | MES 位置上下文 |
+| `MesConfig.TelegramFolder` | `D:\MesTelegram` | MES 报文保存目录 |
+| `CameraIp` / `LocalIp` | `192.168.11.11` / `192.168.11.188` | 工业相机与本机网卡地址 |
+| `CameraSavePath` | `D:\Images\flyShot` | 飞拍原图、结果图和任务 JSON 保存根目录 |
+| `MPC.MpcInterfaceIp` | `192.168.11.12` | 飞拍控制单元 HTTP 地址 |
+| `MPC.MpcInterfacePort` | `9001` | 上位机调用的飞拍控制单元端口 |
+| `MPC.RobotName` | `LR_Mate_200iD_7L` | 机器人型号/名称 |
+| `MPC.RobotIp` | `192.168.10.11` | FANUC 控制柜 IP |
+| `Ai_Model.ApiUrl` | `http://127.0.0.1:8124/corebg` | AI HTTP 接口地址 |
+| `Ai_Model.AI_Model_Version` | `PEU` | AI 模型版本 |
+| `Ai_Model.AI_configVersion` | `peu_20241011` | AI 配置版本 |
+| `Ai_Model.ai_upload_path` | `/mnt/d/ai_images/ai-feed` | AI 输入图片目录 |
+| `Ai_Model.ai_result_path` | `/mnt/d/ai_images` | AI 结果图片目录 |
+| `FileTransfer.DefaultService` | `Primary` | 当前使用的文件传输服务 |
+| `PLC.EnablePlc` | `true` | 是否启用 PLC 通信 |
+| `PLC.OpcUaServer` | `opc.tcp://174.34.178.68:4840` | OPC UA 服务地址 |
+
+> 注意:如果飞拍控制单元实际监听端口是 `5000`,上位机的 `MPC.MpcInterfacePort` 需要同步改为 `5000`,或通过反向代理/部署配置把控制单元暴露为 `9001`。
+
+### 4.2 飞拍控制单元配置
+
+飞拍控制单元配置文件:
+
+- `D:\Dev\Codes\rvbust-code\FlyingShotPkg_3.15_VDA\flyshot-replacement\src\Flyshot.Server.Host\appsettings.json`
+
+常用配置项:
+
+| 配置路径 | 示例值 | 说明 |
+| --- | --- | --- |
+| `ControllerClientCompat.AutoInit.RobotName` | `FANUC_LR_Mate_200iD_7L` | 服务启动自动初始化的机器人名称 |
+| `ControllerClientCompat.AutoInit.RobotIp` | `192.168.10.11` | 服务启动自动连接的机器人 IP |
+| `Kestrel.Endpoints.Http.Url` | `http://0.0.0.0:5000` | HTTP 服务监听地址 |
+| `Swagger.Enabled` | `true` | 是否启用 Swagger 和调试接口 |
+| `Cors.AllowedOrigins` | `*` | 允许跨域调用的来源 |
+
+## 5. 飞拍控制单元 HTTP 接口
+
+### 5.1 初始化机器人
+
+推荐使用聚合初始化接口:
+
+```http
+POST /init_mpc_robot
+Content-Type: application/json
+
+{
+ "robot_name": "FANUC_LR_Mate_200iD_7L",
+ "robot_ip": "192.168.10.11"
+}
+```
+
+内部执行顺序:
+
+1. `SetUpRobot`:加载机器人模型和关节限制。
+2. `Connect`:建立 FANUC 状态、命令、J519 三条通道。
+3. `EnableRobot(8)`:启动机器人程序和运动循环。
+
+也可以拆分调用:
+
+```text
+POST /setup_robot/?robot_name=FANUC_LR_Mate_200iD_7L
+POST /connect_robot/?ip=192.168.10.11
+GET /enable_robot/?buffer_size=8
+```
+
+### 5.2 上传飞拍轨迹
+
+```http
+POST /upload_flyshot/
+Content-Type: application/json
+
+{
+ "name": "UTTC_MS11",
+ "waypoints": [[0, 0, 0, 0, 0, 0]],
+ "shot_flags": [true],
+ "offset_values": [0],
+ "addrs": [[1, 3]]
+}
+```
+
+字段说明:
+
+| 字段 | 类型 | 说明 |
+| --- | --- | --- |
+| `name` | string | 飞拍轨迹名称,上位机执行时使用 |
+| `waypoints` | number[][] | 机器人关节路径点 |
+| `shot_flags` | bool[] | 每个路径点是否拍照 |
+| `offset_values` | number[] | 拍照触发偏移周期 |
+| `addrs` | number[][] | 拍照触发 IO 地址组 |
+
+### 5.3 执行飞拍轨迹
+
+上位机 `MpcService.ExecuteFlyShot` 当前默认发送:
+
+```http
+POST /execute_flyshot/
+Content-Type: application/json
+
+{
+ "name": "UTTC_MS11",
+ "move_to_start": true,
+ "method": "icsp",
+ "save_traj": false,
+ "use_cache": false,
+ "wait": false
+}
+```
+
+字段说明:
+
+| 字段 | 推荐值 | 说明 |
+| --- | --- | --- |
+| `name` | 轨迹名称 | 必须已上传或存在于控制单元缓存 |
+| `move_to_start` | `true` | 执行前先移动到规划起点,避免第一帧目标突变 |
+| `method` | `icsp` 或 `self-adapt-icsp` | 轨迹规划方法 |
+| `save_traj` | `false` | 是否保存本次规划轨迹 |
+| `use_cache` | `false` 或 `true` | 是否复用缓存轨迹 |
+| `wait` | `false` 或 `true` | 是否等待整条轨迹完成后再返回 |
+
+### 5.4 常用调试接口
+
+| 接口 | 方法 | 用途 |
+| --- | --- | --- |
+| `/healthz` | GET | 服务健康检查 |
+| `/status.html` | GET | 浏览器状态页 |
+| `/debug.html` | GET | 浏览器接口调试页 |
+| `/api/status/snapshot` | GET | 当前兼容层和运行时状态快照 |
+| `/robot_info/` | GET | 查询机器人信息 |
+| `/set_speedRatio/` | POST | 设置速度倍率 |
+| `/list_flyShotTraj/` | GET | 查询可执行飞拍轨迹名称 |
+| `/delete_flyshot/` | POST | 删除飞拍轨迹 |
+| `/stop_move/` | GET | 停止当前运动 |
+| `/disable_robot/` | GET | 禁用机器人 |
+| `/disconnect_robot/` | POST | 断开机器人连接 |
+
+## 6. 上位机使用教程
+
+### 6.1 启动前检查
+
+1. 确认工业相机、机器人控制柜、AI 服务、PLC、MES 网络可达。
+2. 检查上位机 `appConfig.json`:
+ - `CameraIp` 和 `LocalIp` 是否匹配现场网段。
+ - `CameraSavePath` 是否存在或可创建。
+ - `MPC.MpcInterfaceIp` / `MpcInterfacePort` 是否指向飞拍控制单元。
+ - `Ai_Model.ApiUrl` 是否指向 YOLO AI 接口。
+ - `FileTransfer` 是否能访问 AI 上传和结果目录。
+3. 检查飞拍控制单元是否启动:
+ - 浏览器访问 `http://<控制单元IP>:<端口>/healthz`。
+ - 浏览器访问 `http://<控制单元IP>:<端口>/status.html`。
+4. 检查机器人程序、控制柜状态和安全围栏。
+
+### 6.2 上位机启动流程
+
+```mermaid
+flowchart TD
+ A["启动 FlyShot 上位机"] --> B["读取 appConfig.json"]
+ B --> C["初始化 MES
可按 EnableMes 关闭"]
+ B --> D["初始化相机"]
+ B --> E["查询飞拍控制单元轨迹列表"]
+ E --> F["设置机器人速度倍率
默认 0.5"]
+ D --> G["确认系统状态栏无关键 NG"]
+ F --> G
+ G --> H["等待手动或 PLC 触发任务"]
+```
+
+启动后,上位机会执行:
+
+- 初始化 MES 连接。
+- 初始化相机。
+- 查询飞拍控制单元中的飞拍任务列表。
+- 设置机器人速度倍率。
+- 创建或检查图片保存目录。
+
+### 6.3 手动执行飞拍任务
+
+适用于调试、首件验证和现场排查。
+
+1. 打开上位机。
+2. 确认相机初始化成功。
+3. 确认飞拍任务列表已加载。
+4. 输入或扫码产品标识。
+5. 选择飞拍任务名称。
+6. 点击手动开始。
+7. 上位机执行:
+ - 初始化工位任务。
+ - 注册相机回调。
+ - 调用飞拍控制单元 `/execute_flyshot/`。
+ - 接收相机回调并保存图片。
+ - 批量调用 AI 检测。
+ - 汇总 OK/NG。
+ - 保存任务 JSON。
+ - 如有 NG,打开人工复判界面并发送通知。
+
+### 6.4 PLC 自动执行飞拍任务
+
+适用于正式生产。
+
+PLC 通过 OPC UA 写入任务触发信号,上位机收到 `StartFlyShotTask` 上升沿后执行任务。
+
+PLC 输入给 PC 的核心信号:
+
+| 信号 | 说明 |
+| --- | --- |
+| `StartFlyShotTask` | 飞拍任务启动,上升沿触发 |
+| `WorkStationId` | 工位号 |
+| `FlyShotTaskName` | 飞拍任务名称 |
+| `CurrentTaskIndex` | 当前任务序号 |
+| `TotalTaskCount` | 当前工位任务总数 |
+| `ProductTypeNo` | 产品型号 |
+| `ProductIdentifier` | 产品唯一标识 |
+| `ProductBatch` | 产品批次 |
+| `ResetSystem` | 系统重置信号 |
+
+PC 回写给 PLC 的核心信号:
+
+| 信号 | 说明 |
+| --- | --- |
+| `TaskCompleted` | 当前任务完成 |
+| `TaskResult` | 当前任务结果,true=OK,false=NG |
+| `CompletedTaskIndex` | 已完成任务序号 |
+| `TaskProcessingTime` | 当前任务耗时 |
+| `WorkStationCompleted` | 工位所有任务是否完成 |
+| `WorkStationResult` | 工位总结果 |
+| `SystemReady` | 系统是否就绪 |
+| `SystemBusy` | 系统是否忙碌 |
+| `ErrorCode` | 错误码 |
+| `ErrorMessage` | 错误信息 |
+| `PcHeartbeat` | PC 心跳 |
+
+### 6.5 图片和结果保存规则
+
+上位机会基于 `CameraSavePath` 生成任务目录:
+
+```text
+{CameraSavePath}\{yyyyMMdd}\{TypeNo-TypeVar}\{ProductIdentifier}\{FlyShotName}\
+```
+
+任务目录内通常包含:
+
+| 内容 | 说明 |
+| --- | --- |
+| 原始图片 | 相机回调保存的图片 |
+| `result` 子目录 | AI 下载后的结果图 |
+| `flyShotTask@...json` | 单个飞拍任务详情 |
+| `WorkStationTask@...json` | 工位任务汇总 |
+
+AI 结果图命名会带上检测结果,例如:
+
+```text
+原图名@OK.jpeg
+原图名@NG.jpeg
+```
+
+## 7. YOLO AI 识别流程
+
+```mermaid
+flowchart TD
+ A["相机回调保存原图"] --> B["上位机收集待处理图片"]
+ B --> C["达到 BatchSize 或采集结束"]
+ C --> D["上传原图到 Ai_Model.ai_upload_path"]
+ D --> E["构造 AiProcessRequestModel"]
+ E --> F["POST Ai_Model.ApiUrl"]
+ F --> G["解析 ProductResult / SubImgInfo / FailureModes"]
+ G --> H["从 Ai_Model.ai_result_path 下载结果图"]
+ H --> I["写回 ImageProcessOutcome"]
+ I --> J["更新 UI 与任务结果"]
+ J --> K["清理 AI 远端临时文件"]
+```
+
+### 7.1 AI 请求结构
+
+AI 请求的典型结构:
+
+```json
+{
+ "ai_modelVersion": "PEU",
+ "ai_configVersion": "peu_20241011",
+ "key": "empty",
+ "product_id": "20250228121929",
+ "sub_img_info": [
+ {
+ "index": 1,
+ "view": "pipe",
+ "object": "spring",
+ "image_path": "000001_161008_1.jpeg",
+ "ai_image_path": null,
+ "ai_result": null,
+ "ai_reason": null,
+ "ai_param_list:": {}
+ }
+ ]
+}
+```
+
+字段说明:
+
+| 字段 | 说明 |
+| --- | --- |
+| `ai_modelVersion` | AI 模型版本,对应上位机 `Ai_Model.AI_Model_Version` |
+| `ai_configVersion` | AI 配置版本,对应上位机 `Ai_Model.AI_configVersion` |
+| `product_id` | 本次 AI 任务 ID,通常由时间戳生成 |
+| `sub_img_info[].index` | 图片序号 |
+| `sub_img_info[].view` | 视角,例如 `pipe`、`surface`、`connector` |
+| `sub_img_info[].object` | 检测对象,例如 `spring`、`top`、`side` |
+| `sub_img_info[].image_path` | 上传到 AI 输入目录后的图片文件名 |
+
+### 7.2 AI 响应处理
+
+上位机按 `index` 将 AI 响应和原图对应起来,主要读取:
+
+| 字段 | 说明 |
+| --- | --- |
+| `ProductResult` | 产品级检测结果 |
+| `ErrorInfo` | AI 处理错误信息 |
+| `AiFailureModel` | AI 不良模式集合 |
+| `SubImgInfo[].AiResult` | 单张图片结果,`OK` 视为 OK,其他非空值视为 NG |
+| `SubImgInfo[].AiReason` | 单张图片 NG 原因 |
+| `SubImgInfo[].AiParamList` | AI 参数明细 |
+| `SubImgInfo[].AiImagePath` | AI 结果图远端路径 |
+
+## 8. MES 交互说明
+
+### 8.1 换型下发
+
+上位机启动或手动换型时,会根据 `MesConfig.TypeNo` 和 `MesConfig.TypeVar` 发送 `DataDownloadRequired`。
+
+主要用途:
+
+- 获取或刷新当前型号配置。
+- 更新本地 `TypeNo` / `TypeVar`。
+- 加载对应 `Config\TypeData` 文件。
+
+### 8.2 结果上报
+
+飞拍任务完成后,上位机会发送 `PartProcessed`。
+
+上报内容包括:
+
+- 产品唯一标识。
+- 产品型号和变体。
+- 工位结果,`1=OK`,`2=NG`。
+- 图片相对路径。
+- 每张图片的 AI 结果和人工复判结果。
+- 汇总后的 NIO bits。
+
+## 9. 常见使用场景
+
+### 9.1 首次联调
+
+1. 启动飞拍控制单元。
+2. 访问 `/healthz` 确认服务正常。
+3. 访问 `/status.html` 查看模型和机器人状态。
+4. 调用 `/init_mpc_robot` 初始化机器人。
+5. 调用 `/list_flyShotTraj/` 确认可用轨迹。
+6. 启动上位机。
+7. 检查上位机日志中相机、MPC、MES、PLC 状态。
+8. 使用手动模式执行一次飞拍任务。
+9. 检查图片目录、AI 结果图、任务 JSON 和 MES/PLC 反馈。
+
+### 9.2 正式生产
+
+1. 启动飞拍控制单元并确认状态正常。
+2. 启动上位机并确认系统就绪。
+3. PLC 写入产品和任务信息。
+4. PLC 触发 `StartFlyShotTask` 上升沿。
+5. 上位机执行飞拍、AI 检测和结果汇总。
+6. 上位机回写 PLC 当前任务和工位结果。
+7. 上位机发送 MES `PartProcessed`。
+8. PLC 根据 `WorkStationCompleted` 和 `WorkStationResult` 决定后续流程。
+
+### 9.3 离线排查 AI
+
+1. 准备一组本地图片。
+2. 在上位机调试页面或相关调试入口选择图片。
+3. 确认 `Ai_Model.ApiUrl`、`ai_upload_path`、`ai_result_path` 正确。
+4. 执行 AI 请求。
+5. 检查 AI 响应、结果图下载、上位机日志和远端文件清理情况。
+
+## 10. 故障排查
+
+```mermaid
+flowchart TD
+ A["发现任务失败或结果异常"] --> B{"失败阶段"}
+ B -->|相机无图| C["检查 CameraIp / LocalIp / 相机连接 / 触发源 / 曝光增益"]
+ B -->|控制单元失败| D["检查 /healthz /status.html / MpcInterfaceIp / 端口 / 机器人状态"]
+ B -->|机器人报警| E["检查 FANUC 控制柜 / RVBUSTSM / J519 通道 / 安全状态"]
+ B -->|AI 无结果| F["检查 Ai_Model.ApiUrl / 文件传输目录 / AI 服务日志 / 结果图路径"]
+ B -->|PLC 不触发| G["检查 OPC UA 地址 / ActiveProfile / StartFlyShotTask 上升沿 / SystemReady"]
+ B -->|MES 失败| H["检查 EnableMes / ServerIp / ServerPort / TelegramFolder / 报文响应"]
+ C --> I["查看上位机日志和图片目录"]
+ D --> I
+ E --> I
+ F --> I
+ G --> I
+ H --> I
+```
+
+### 10.1 相机问题
+
+| 现象 | 排查点 |
+| --- | --- |
+| 无图像回调 | 相机 IP、本机网卡、相机初始化、回调注册 |
+| 图片曝光异常 | 飞拍点位配置中的曝光和增益 |
+| 图片保存失败 | `CameraSavePath` 权限、磁盘空间、路径是否存在 |
+
+### 10.2 飞拍控制单元问题
+
+| 现象 | 排查点 |
+| --- | --- |
+| 上位机无法连接 MPC | `MPC.MpcInterfaceIp` / `MpcInterfacePort` 是否指向控制单元 |
+| `/execute_flyshot/` 失败 | 轨迹是否已上传、机器人是否初始化、`method` 是否支持 |
+| 机器人不运动 | `/status.html` 状态、FANUC 通道、使能状态、控制柜报警 |
+| 轨迹执行后无拍照 | `shot_flags`、`offset_values`、`addrs`、J519 IO 脉冲 |
+
+### 10.3 AI 问题
+
+| 现象 | 排查点 |
+| --- | --- |
+| AI 请求失败 | `Ai_Model.ApiUrl`、AI 服务是否启动、HTTP 是否可达 |
+| AI 找不到图片 | `ai_upload_path`、文件传输服务、远端目录权限 |
+| 结果图下载失败 | `ai_result_path`、`AiImagePath`、SFTP/SMB/LOCAL 配置 |
+| 结果和图片错位 | 检查 AI 响应中的 `index` 是否和请求一致 |
+
+### 10.4 PLC/MES 问题
+
+| 现象 | 排查点 |
+| --- | --- |
+| PLC 不触发任务 | `PLC.EnablePlc`、OPC UA 地址、变量映射、上升沿保持时间 |
+| PLC 反馈不更新 | 输出变量是否可写、`SystemBusy/SystemReady` 状态 |
+| MES 连接失败 | `MesConfig.EnableMes`、IP、端口、防火墙 |
+| MES 报文异常 | `TelegramFolder` 下保存的 XML 报文和响应 |
+
+## 11. 交付与飞书整理建议
+
+建议后续整理为飞书文档时采用以下方式:
+
+1. 以本文档 Markdown 作为源稿。
+2. 将 Mermaid 流程图渲染为 PNG 或 SVG 图片后插入飞书。
+3. 保留接口表、配置表和操作步骤,适合现场培训。
+4. 如果需要正式发布版本,可再从 Markdown 生成 Word,再导入飞书云文档。
+5. 每次接口、配置或流程变更后,优先更新 Markdown 源稿,再同步飞书版本。
+
+建议拆分飞书目录:
+
+```text
+飞拍系统说明
+├─ 1. 系统总览
+├─ 2. 生产主流程
+├─ 3. 上位机使用教程
+├─ 4. 飞拍控制单元使用教程
+├─ 5. YOLO AI 接口说明
+├─ 6. PLC 与 MES 交互
+└─ 7. 常见问题排查
+```
diff --git a/docs/use-do-output-selection.md b/docs/use-do-output-selection.md
new file mode 100644
index 0000000..a06c186
--- /dev/null
+++ b/docs/use-do-output-selection.md
@@ -0,0 +1,14 @@
+# 飞拍 DO / RO 输出选择
+
+`RobotConfig.json` 的 `robot.use_do` 用于选择飞拍输出类型:
+
+- `true`:使用 DO,J519 `writeIoType=2`。
+- `false`:使用 RO,J519 `writeIoType=9`。
+
+两种模式都根据 `shot_flags / offset_values / addr` 生成触发事件,保持 `io_keep_cycles` 的脉冲保持与清零行为。`use_do=false` 不再关闭拍照触发。
+
+触发时间轴将输出选择固化到每个事件中;FANUC 运行时根据事件选择写入类型,触发帧和清零帧使用相同类型。未携带输出选择的已有事件默认使用 DO,普通无触发运动保持原行为。
+
+本次不修改配置中的 `addr`,不增加 RO 点位范围校验,现场点位由调用方自行调试。手动 HTTP `SetIo` 仍使用显式 `io_type`。
+
+验证覆盖配置到事件的传递,以及 DO / RO 两种模式下的 J519 脉冲与清零。软件测试不代表末端接线和真实相机触发已完成验收。
diff --git a/src/Flyshot.ControllerClientCompat/ControllerClientCompatService.cs b/src/Flyshot.ControllerClientCompat/ControllerClientCompatService.cs
index 3cf539a..33c3435 100644
--- a/src/Flyshot.ControllerClientCompat/ControllerClientCompatService.cs
+++ b/src/Flyshot.ControllerClientCompat/ControllerClientCompatService.cs
@@ -651,6 +651,7 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
///
/// 从当前关节位置生成临时 PTP 稠密轨迹并阻塞等待运行时完成,避免后续 J519 目标发生突变。
+ /// 规划倍率与当前运行速度倍率相乘后用于生成轨迹,使直接移动和飞拍预移动采用相同限速规则。
///
/// 当前机器人模型。
/// 目标关节位置,单位为弧度。
@@ -662,11 +663,21 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
EnsureJointVector(targetJointPositions, robot.DegreesOfFreedom, nameof(targetJointPositions));
var speedRatio = _runtime.GetSnapshot().SpeedRatio;
- var moveResult = MoveJointTrajectoryGenerator.CreateResult(robot, currentJointPositions, targetJointPositions, speedRatio, _logger);
+ double planningSpeedScale;
+ lock (_stateLock)
+ {
+ planningSpeedScale = RequireRobotSettings().PlanningSpeedScale;
+ }
+
+ // 在生成阶段统一缩放速度、加速度和跃度限制,发送阶段仍保持固定伺服周期。
+ var effectivePlanningSpeedScale = planningSpeedScale * speedRatio;
+ var moveResult = MoveJointTrajectoryGenerator.CreateResult(robot, currentJointPositions, targetJointPositions, effectivePlanningSpeedScale, _logger);
_logger?.LogInformation(
- "{OperationName} PTP规划完成: 当前速度倍率={SpeedRatio}, 规划时长={Duration}s, 采样点数={SampleCount}",
+ "{OperationName} PTP规划完成: 当前速度倍率={SpeedRatio}, 规划倍率={PlanningSpeedScale}, 有效规划倍率={EffectivePlanningSpeedScale}, 规划时长={Duration}s, 采样点数={SampleCount}",
operationName,
speedRatio,
+ planningSpeedScale,
+ effectivePlanningSpeedScale,
moveResult.Duration.TotalSeconds,
moveResult.DenseJointTrajectory?.Count ?? 0);
diff --git a/src/Flyshot.Core.Config/RobotConfigLoader.cs b/src/Flyshot.Core.Config/RobotConfigLoader.cs
index 5b4bffd..8dc3af3 100644
--- a/src/Flyshot.Core.Config/RobotConfigLoader.cs
+++ b/src/Flyshot.Core.Config/RobotConfigLoader.cs
@@ -81,7 +81,7 @@ public sealed class CompatibilityRobotSettings
}
///
- /// 获取是否启用伺服同步 IO。
+ /// 获取飞拍同步输出类型选择:true 使用 DO,false 使用 RO;两者都启用触发。
///
public bool UseDo { get; }
diff --git a/src/Flyshot.Core.Domain/TrajectoryResult.cs b/src/Flyshot.Core.Domain/TrajectoryResult.cs
index d90e515..55ff510 100644
--- a/src/Flyshot.Core.Domain/TrajectoryResult.cs
+++ b/src/Flyshot.Core.Domain/TrajectoryResult.cs
@@ -174,20 +174,28 @@ public sealed class TrajectoryResult
}
///
-/// Describes a trigger event that the runtime must inject into the servo timeline.
+/// 表示运行时注入伺服时间轴的 DO 或 RO 输出触发事件。
///
public sealed class TrajectoryDoEvent
{
///
- /// Initializes a validated runtime trigger event.
+ /// 初始化经过校验的伺服同步输出触发事件。
///
+ /// 请求拍照的原始示教点索引,从零开始。
+ /// 离散化之前的理论触发时间,单位秒,必须非负。
+ /// 相对于示教点的触发偏移周期数。
+ /// 输出保持的伺服周期数,必须非负。
+ /// 要输出的 IO 点位组,不允许为空引用。
+ /// 用于选择最近发送采样点的关节角,单位度;允许 null。
+ /// true 使用 DO,false 使用 RO;默认 true 保留已有事件的 DO 行为。
public TrajectoryDoEvent(
int waypointIndex,
double triggerTime,
int offsetCycles,
int holdCycles,
IoAddressGroup addressGroup,
- IEnumerable? referenceJointsDegrees = null)
+ IEnumerable? referenceJointsDegrees = null,
+ bool useDo = true)
{
if (waypointIndex < 0)
{
@@ -210,6 +218,7 @@ public sealed class TrajectoryDoEvent
HoldCycles = holdCycles;
AddressGroup = addressGroup ?? throw new ArgumentNullException(nameof(addressGroup));
ReferenceJointsDegrees = referenceJointsDegrees?.ToArray();
+ UseDo = useDo;
}
///
@@ -248,6 +257,12 @@ public sealed class TrajectoryDoEvent
///
[JsonPropertyName("referenceJointsDegrees")]
public IReadOnlyList? ReferenceJointsDegrees { get; }
+
+ ///
+ /// 获取飞拍输出选择:true 为 DO,false 为 RO;仅供运行时使用。
+ ///
+ [JsonIgnore]
+ public bool UseDo { get; }
}
///
diff --git a/src/Flyshot.Core.Triggering/ShotTimelineBuilder.cs b/src/Flyshot.Core.Triggering/ShotTimelineBuilder.cs
index affbea5..efd7085 100644
--- a/src/Flyshot.Core.Triggering/ShotTimelineBuilder.cs
+++ b/src/Flyshot.Core.Triggering/ShotTimelineBuilder.cs
@@ -12,7 +12,7 @@ namespace Flyshot.Core.Triggering;
/// 本构建器只负责“时间轴映射”和“事件打包”,不负责轨迹规划与运行时发送。
/// 其中:
/// - ShotEvent 用于诊断、导出和日志观察;
-/// - TrajectoryDoEvent 用于运行时伺服链路注入 DO 脉冲。
+/// - TrajectoryDoEvent 用于运行时伺服链路注入 DO 或 RO 脉冲。
///
public sealed class ShotTimelineBuilder
{
@@ -43,7 +43,7 @@ public sealed class ShotTimelineBuilder
/// 规划后的轨迹(含补中点信息和机器人配置)。
/// IO 保持周期数(对应原系统的 io_keep_cycles)。
/// 稠密采样周期,用于离散化 sample_index 和 sample_time。
- /// 是否生成可注入伺服流的 DO 事件。
+ /// 输出类型选择:true 使用 DO,false 使用 RO;两者都生成触发事件。
/// 包含 ShotEvent 和 TrajectoryDoEvent 的触发时间轴。
/// 当 为 null 时抛出。
///
@@ -101,17 +101,15 @@ public sealed class ShotTimelineBuilder
sampleTime: sampleTime,
addressGroup: addressGroup));
- if (useDo)
- {
- // use_do=false 时保留 ShotEvent 诊断信息,但不向运行时下发 IO 脉冲。
- triggerTimeline.Add(new TrajectoryDoEvent(
- waypointIndex: i,
- triggerTime: triggerTime,
- offsetCycles: program.OffsetValues[i],
- holdCycles: holdCycles,
- addressGroup: addressGroup,
- referenceJointsDegrees: program.Waypoints[i].Positions.Select(static radians => radians * 180.0 / Math.PI)));
- }
+ // use_do 只选择输出类型;RO 模式也必须保留拍照脉冲与保持周期。
+ triggerTimeline.Add(new TrajectoryDoEvent(
+ waypointIndex: i,
+ triggerTime: triggerTime,
+ offsetCycles: program.OffsetValues[i],
+ holdCycles: holdCycles,
+ addressGroup: addressGroup,
+ referenceJointsDegrees: program.Waypoints[i].Positions.Select(static radians => radians * 180.0 / Math.PI),
+ useDo: useDo));
}
_logger?.LogInformation(
diff --git a/src/Flyshot.Runtime.Fanuc/FanucControllerRuntime.cs b/src/Flyshot.Runtime.Fanuc/FanucControllerRuntime.cs
index 45a609e..6049e8e 100644
--- a/src/Flyshot.Runtime.Fanuc/FanucControllerRuntime.cs
+++ b/src/Flyshot.Runtime.Fanuc/FanucControllerRuntime.cs
@@ -595,6 +595,10 @@ public sealed class FanucControllerRuntime : IControllerRuntime, IDisposable
private void SendDenseTrajectory(TrajectoryResult result, IReadOnlyList finalJointPositions, CancellationToken cancellationToken)
{
var triggers = result.TriggerTimeline;
+ // 输出类型随触发事件固化,确保脉冲与清零帧都写入同一路 DO/RO。
+ byte writeIoType = triggers.FirstOrDefault()?.UseDo == false
+ ? (byte)FanucIoTypes.RobotOutput
+ : (byte)FanucIoTypes.DigitalOutput;
var servoPeriodSeconds = _robot!.ServoPeriod.TotalSeconds;
var preparedExecution = result.PreparedFlyshotExecution;
var durationSeconds = preparedExecution?.FinalDurationSeconds ?? result.Duration.TotalSeconds;
@@ -710,7 +714,7 @@ public sealed class FanucControllerRuntime : IControllerRuntime, IDisposable
var command = new FanucJ519Command(
sequence: 0,
targetJoints: sample.JointsDegrees,
- writeIoType: 2,
+ writeIoType: writeIoType,
writeIoIndex: 1,
writeIoMask: ioMask,
writeIoValue: ioValue);
diff --git a/tests/Flyshot.Core.Tests/FanucControllerRuntimeDenseTests.cs b/tests/Flyshot.Core.Tests/FanucControllerRuntimeDenseTests.cs
index 098e402..500b9f4 100644
--- a/tests/Flyshot.Core.Tests/FanucControllerRuntimeDenseTests.cs
+++ b/tests/Flyshot.Core.Tests/FanucControllerRuntimeDenseTests.cs
@@ -16,6 +16,70 @@ namespace Flyshot.Core.Tests;
///
public sealed class FanucControllerRuntimeDenseTests
{
+ ///
+ /// 验证飞拍输出选择贯通到 J519 命令,DO/RO 模式都输出两周期脉冲并在同一路清零。
+ ///
+ /// true 选择 DO,false 选择 RO。
+ /// 预期 J519 写入类型,DO 为 2,RO 为 9。
+ /// 是否使用上游已完成触发绑定的最终发送队列。
+ [Theory]
+ [InlineData(true, 2, false)]
+ [InlineData(false, 9, false)]
+ [InlineData(true, 2, true)]
+ [InlineData(false, 9, true)]
+ public void ExecuteTrajectory_FlyshotOutputSelection_PreservesPulseAndClear(bool useDo, int expectedIoType, bool usePreparedExecution)
+ {
+ using var commandClient = new FanucCommandClient();
+ using var stateClient = new FanucStateClient();
+ using var j519Client = new FanucJ519Client();
+ using var runtime = new FanucControllerRuntime(commandClient, stateClient, j519Client);
+ runtime.ResetRobot(TestRobotFactory.CreateRobotProfile(), "FANUC_LR_Mate_200iD");
+ j519Client.EnableCommandHistoryForTests();
+ ForceRealModeEnabled(runtime, speedRatio: 1.0);
+
+ var trigger = new TrajectoryDoEvent(waypointIndex: 1, triggerTime: 0.008,
+ offsetCycles: 0, holdCycles: 2, addressGroup: new IoAddressGroup([1]), useDo: useDo);
+ var samples = Enumerable.Range(0, 4)
+ .Select(index => new FlyshotPreparedSample(index, index * 0.008, index * 0.008,
+ 1.0, [0.0, 0.0, 0.0, 0.0, 0.0, 0.0])).ToArray();
+ var preparedExecution = usePreparedExecution
+ ? new FlyshotPreparedExecution(samples,
+ [new FlyshotPreparedTriggerBinding(trigger, samples[1], 1, true)],
+ Array.Empty>(), Array.Empty>(),
+ requestSpeedRatio: 1.0, finalSpeedRatio: 1.0, finalDurationSeconds: 0.024, stretchIterationCount: 0)
+ : null;
+
+ // 静止轨迹隔离运动算法影响,仅验证触发、保持和清零对应的协议字段。
+ var result = new TrajectoryResult(
+ programName: "output-selection",
+ method: PlanningMethod.Icsp,
+ isValid: true,
+ duration: TimeSpan.FromSeconds(0.024),
+ shotEvents: Array.Empty(),
+ triggerTimeline: [trigger],
+ artifacts: Array.Empty(),
+ failureReason: null,
+ usedCache: false,
+ originalWaypointCount: 2,
+ plannedWaypointCount: 2,
+ denseJointTrajectory:
+ [
+ new double[] { 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 },
+ new double[] { 0.024, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 }
+ ],
+ preparedFlyshotExecution: preparedExecution);
+
+ runtime.ExecuteTrajectory(result, [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
+ WaitUntilIdle(runtime);
+
+ var commands = j519Client.GetCommandHistoryForTests();
+ Assert.True(commands.Count >= 4);
+ Assert.All(commands, command => Assert.Equal((byte)expectedIoType, command.WriteIoType));
+ Assert.All(commands, command => Assert.Equal((ushort)1, command.WriteIoIndex));
+ Assert.Equal(new ushort[] { 0, 1, 1, 1 }, commands.Take(4).Select(command => command.WriteIoMask));
+ Assert.Equal(new ushort[] { 0, 1, 1, 0 }, commands.Take(4).Select(command => command.WriteIoValue));
+ }
+
private const double SmoothPtpVelocityShapeCoefficient = 2.1875;
private const double SmoothPtpAccelerationShapeCoefficient = 7.513188404399293;
private const double SmoothPtpJerkShapeCoefficient = 52.5;
diff --git a/tests/Flyshot.Core.Tests/RuntimeOrchestrationTests.cs b/tests/Flyshot.Core.Tests/RuntimeOrchestrationTests.cs
index b5e35f3..78a6794 100644
--- a/tests/Flyshot.Core.Tests/RuntimeOrchestrationTests.cs
+++ b/tests/Flyshot.Core.Tests/RuntimeOrchestrationTests.cs
@@ -241,13 +241,14 @@ public sealed class RuntimeOrchestrationTests
var doEvent = Assert.Single(bundle.Result.TriggerTimeline);
Assert.Equal(4, doEvent.HoldCycles);
+ Assert.True(doEvent.UseDo);
}
///
- /// 验证 RobotConfig.json 关闭 use_do 时仍保留 ShotEvent 诊断信息,但不生成伺服 DO 事件。
+ /// 验证 use_do=false 时仍生成飞拍触发事件,保持周期不变。
///
[Fact]
- public void ControllerClientTrajectoryOrchestrator_PlanUploadedFlyshot_SuppressesDoTimeline_WhenUseDoIsFalse()
+ public void ControllerClientTrajectoryOrchestrator_PlanUploadedFlyshot_PreservesTriggerTimeline_WhenUseDoIsFalse()
{
var orchestrator = new ControllerClientTrajectoryOrchestrator();
var robot = TestRobotFactory.CreateRobotProfile();
@@ -264,7 +265,9 @@ public sealed class RuntimeOrchestrationTests
var bundle = orchestrator.PlanUploadedFlyshot(robot, uploaded, settings: settings);
Assert.Single(bundle.Result.ShotEvents);
- Assert.Empty(bundle.Result.TriggerTimeline);
+ var trigger = Assert.Single(bundle.Result.TriggerTimeline);
+ Assert.Equal(4, trigger.HoldCycles);
+ Assert.False(trigger.UseDo);
}
///
@@ -788,6 +791,66 @@ public sealed class RuntimeOrchestrationTests
}
}
+ ///
+ /// 验证直接关节移动和飞拍预移动均按规划倍率与当前运行倍率的乘积生成点对点轨迹。
+ ///
+ /// 是否通过飞拍预移动入口执行点对点运动。
+ /// 配置中的规划速度倍率。
+ /// 机器人初始化后设置的当前运行速度倍率。
+ [Theory]
+ [InlineData(false, 0.74227, 1.0)]
+ [InlineData(false, 0.74227, 0.5)]
+ [InlineData(false, 1.0, 0.7)]
+ [InlineData(true, 0.74227, 0.5)]
+ public void ControllerClientCompatService_MoveJoint_AppliesCombinedPlanningSpeedScale(
+ bool moveToStart, double planningSpeedScale, double speedRatio)
+ {
+ var configRoot = CreateTempConfigRoot();
+ try
+ {
+ WriteRobotConfigWithDemoTrajectory(configRoot, planningSpeedScale: planningSpeedScale, speedRatio: 1.0);
+ var options = new ControllerClientCompatOptions { ConfigRoot = configRoot };
+ double[] startJoints = [0.4, 0.0, 0.0, 0.0, 0.0, 0.0];
+ double[] targetJoints = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
+ var runtime = new DelayedCompletionControllerRuntime(startJoints, TimeSpan.Zero);
+ var service = new ControllerClientCompatService(
+ options,
+ new ControllerClientCompatRobotCatalog(options, new RobotModelLoader()),
+ runtime,
+ new ControllerClientTrajectoryOrchestrator(),
+ new RobotConfigLoader());
+ service.SetUpRobot("FANUC_LR_Mate_200iD");
+ service.Connect("192.168.10.101");
+ service.EnableRobot(2);
+ service.SetSpeedRatio(speedRatio);
+
+ // 两个公开入口必须共享相同的点对点限速,且采用当前倍率而非配置默认倍率。
+ if (moveToStart)
+ {
+ service.UploadTrajectory(TestRobotFactory.CreateUploadedTrajectoryWithSingleShot());
+ service.ExecuteTrajectoryByName("demo-flyshot", new FlyshotExecutionOptions(moveToStart: true, useCache: false));
+ }
+ else
+ {
+ service.MoveJoint(targetJoints);
+ }
+
+ var robot = new ControllerClientCompatRobotCatalog(options, new RobotModelLoader())
+ .LoadProfile("FANUC_LR_Mate_200iD", accLimitScale: 1.0, jerkLimitScale: 1.0);
+ var expected = MoveJointTrajectoryGenerator.CreateResult(robot, startJoints, targetJoints, planningSpeedScale * speedRatio);
+ var actual = runtime.ExecuteCalls[0].Result;
+ Assert.Equal(expected.Duration, actual.Duration);
+ Assert.Equal(expected.DenseJointTrajectory!.Count, actual.DenseJointTrajectory!.Count);
+ Assert.Equal(targetJoints, runtime.ExecuteCalls[0].FinalJointPositions);
+ Assert.Equal(speedRatio, runtime.GetSnapshot().SpeedRatio);
+ Assert.False(runtime.SecondTrajectoryStartedBeforeFirstMotionCompleted);
+ }
+ finally
+ {
+ Directory.Delete(configRoot, recursive: true);
+ }
+ }
+
///
/// 验证 ExecuteFlyShotTraj(move_to_start=true) 会先执行稠密 PTP 到起点,并等待该段运动完成后再启动飞拍轨迹。
///
@@ -2226,6 +2289,10 @@ internal sealed class RecordingControllerRuntime : IControllerRuntime
///
internal sealed class DelayedCompletionControllerRuntime : IControllerRuntime
{
+ ///
+ /// 当前测试运行速度倍率,用于验证初始化与运行中调速对预移动规划的影响。
+ ///
+ private double _speedRatio = 1.0;
private readonly object _lock = new();
private readonly TimeSpan _firstMotionCompletionDelay;
private double[] _jointPositions;
@@ -2298,11 +2365,12 @@ internal sealed class DelayedCompletionControllerRuntime : IControllerRuntime
}
///
- public double GetSpeedRatio() => 1.0;
+ public double GetSpeedRatio() => _speedRatio;
///
public void SetSpeedRatio(double ratio)
{
+ _speedRatio = ratio;
}
///
@@ -2343,7 +2411,7 @@ internal sealed class DelayedCompletionControllerRuntime : IControllerRuntime
connectionState: "Connected",
isEnabled: _isEnabled,
isInMotion: _isInMotion,
- speedRatio: 1.0,
+ speedRatio: _speedRatio,
jointPositions: _jointPositions.ToArray(),
cartesianPose: Array.Empty(),
activeAlarms: Array.Empty());