Merge commit '0952958af7685646cdbf2a4907c6e2ae59d4d352'

This commit is contained in:
2026-05-28 10:44:03 +08:00
parent 4eeaa3fef3
commit 35bdb08d11
144 changed files with 33730 additions and 392 deletions

BIN
docs/0.7x.pcap Normal file

Binary file not shown.

View File

@@ -0,0 +1,526 @@
# 2026042802-1 抓包触发点与示教点对比记录
## 1. 目的
- 使用 `../Rvbust/uttc-20260428/2026042802-1.pcap` 确认当前仓库是否已经导出过这份抓包对应的真实运动轨迹发包记录。
- 如果现有导出不包含 IO 触发信息,则补一份“真实 J519 发包 + IO 标记”导出。
- 将抓包里真实触发 IO 的关节坐标,与 `Config/RobotConfig.json` 中 `flying_shots.UTTC_MS11` 的示教点逐点对比。
- 把结论固定成仓库文档,后续继续和新程序抓包、`ShotEvents.json`、运行时导出结果做同口径比较。
## 2. 数据来源与口径
- 抓包文件:`../Rvbust/uttc-20260428/2026042802-1.pcap`
- 旧目录配置文件:`../Rvbust/uttc-20260428/RobotConfig.json`
- 新版程序运行时配置文件:`Config/RobotConfig.json`
- 对比对象:两份配置中的 `flying_shots.UTTC_MS11`
- 协议口径:只看 `192.168.10.10 -> 192.168.10.11` 的 `UDP 60015` 64B J519 命令帧
- 触发判定:`write_io_value > 0` 视为 IO 置位
- 去重规则:由于 `io_keep_cycles=2`,每次触发只记录首个高电平帧
- 关节单位:抓包 J519 目标关节为 `deg`;`RobotConfig.json` `traj_waypoints` 为 `rad`,分析时先转成 `deg`
补充说明:
- `../Rvbust/uttc-20260428/` 目录中的内容代表旧抓包及其同目录资料。
- `Config/Data/UTTC_MS11/` 目录中的内容代表现在新版程序的运行时导出。
- 本文不会把两者混成同一次运行的数据;这里只拿新版程序目录里的文件做“现状参照”,核心分析对象仍然是旧抓包 `2026042802-1.pcap`。
## 3. 现状核对
### 3.1 仓库里已经存在什么
- 旧抓包目录已有原始抓包:`../Rvbust/uttc-20260428/2026042802-1.pcap`
- 旧抓包目录已有按运动段拆分的导出:`../Rvbust/uttc-20260428/2026042802-1_joint_segments/`
- 旧抓包目录已有速度档位整理结果:`../Rvbust/uttc-20260428/1倍速度 角度坐标点/真实轨迹JointDetialTraj.txt`
- 新版程序目录已有运行时导出:`Config/Data/UTTC_MS11/ActualSendJointTraj.txt`
- 新版程序目录已有运行时导出:`Config/Data/UTTC_MS11/ActualSendTiming.txt`
- 新版程序目录已有理论触发时间轴:`Config/Data/UTTC_MS11/ShotEvents.json`
### 3.2 当前缺的是什么
- 新版程序目录中的 `ActualSendJointTraj.txt` 是现在新版程序落盘的真实发送关节轨迹,但不带 IO 列,也不属于 `2026042802-1.pcap` 这次旧抓包本身。
- 旧抓包目录中的 `2026042802-1_joint_segments/` 是按运动段拆开的 `JointDetialTraj.txt` 风格导出,也不包含整条抓包的 IO 标记。
- 旧抓包目录中的 `1倍速度 角度坐标点/真实轨迹JointDetialTraj.txt` 提供了真实轨迹文本,但同样不带 IO 触发列。
- 本次分析前,没有发现一份专门对应 `2026042802-1.pcap` 的“整条真实 J519 发包记录 + IO 标记 + 触发帧筛选”组合导出。
结论:
旧抓包目录和新版程序目录里都已经有各自的轨迹资料,但在本次分析前,还没有一份专门对应 `2026042802-1.pcap` 的、带 IO 触发信息的整条实际发包记录。
## 4. 本次新增导出
本次补充生成了以下文件:
- `analysis/2026042802-1/2026042802-1_j519_actual_send_all_with_io.csv`
- `1788` 条客户端真实下发 J519 命令
- 包含 `frame_number / time_relative_s / sequence / write_io_value / io_addrs / j1..j6`
- `analysis/2026042802-1/2026042802-1_j519_trigger_frames.csv`
- 只保留每次 IO 高电平脉冲的第一帧
- 共 `17` 条,和 `ShotEvents.json` 的触发次数一致
- `analysis/2026042802-1/2026042802-1_trigger_vs_teach_points.csv`
- 每个真实触发帧与 `UTTC_MS11` 示教点的逐点偏差表
- `analysis/2026042802-1/2026042802-1_analysis_summary.json`
- 本文档引用的汇总统计
对应复现脚本:
- `analysis/analyze_2026042802_1_trigger_vs_teach_points.py`
## 5. 触发与地址核对
- 抓包提取到的真实触发次数:`17`
- `ShotEvents.json` 中的触发次数:`17`
- `write_io_value` 解码出的 `io_addrs` 与 `UTTC_MS11.addr` 没有真实内容不一致
- 其中有 `14` 处列表顺序不同,例如抓包里是 `[2, 3, 4]`,配置里写的是 `[3, 4, 2]`
- 这 `14` 处只是顺序差异,不是触发地址集合差异
- 真正的地址集合不匹配数:`0`
结论:
`2026042802-1.pcap` 中真实发出的 IO 地址集合,与 `UTTC_MS11` 配置的 `addr` 集合是一致的。
## 6. 触发点与示教点误差统计
- 平均单点最大单轴误差:`4.241584 deg`
- 最大单轴误差:`11.130744 deg`
- 平均 RMS 误差:`2.540614 deg`
- 最大误差轴分布:`J6=9`、`J4=5`、`J5=1`、`J2=1`、`J1=1`
这说明:
- 这份旧抓包里的真实触发点,并不是严格贴在配置示教点上触发
- 误差主要集中在 `J6` 和 `J4`
- 由于 `../Rvbust/uttc-20260428/RobotConfig.json` 和当前仓库 `Config/RobotConfig.json` 的 `UTTC_MS11` 四组关键字段完全一致,因此这里的“示教点”对旧抓包和当前配置是同一组数据
- 如果用户的“对焦准确示教点”就是这组 `traj_waypoints`,那么旧程序实际触发时已经存在几度量级的偏移,而不是亚角度级别的完全重合
## 7. 最大偏差点
按最大单轴误差从大到小列出前 5 个触发点:
| 触发序号 | waypoint_index | 实际 io_addrs | 最大误差轴 | 最大误差(deg) | RMS误差(deg) | 主要差值 |
| --- | --- | --- | --- | ---: | ---: | --- |
| 3 | 3 | `[2, 3, 4]` | `J6` | `11.130744` | `5.955608` | `J4 +8.811537`,`J6 -11.130744` |
| 7 | 7 | `[3, 4]` | `J6` | `7.194107` | `4.618545` | `J2 -5.441249`,`J4 -6.792278`,`J6 +7.194107` |
| 17 | 18 | `[3, 4]` | `J4` | `6.848522` | `4.036140` | `J2 +1.835940`,`J4 -6.848522`,`J6 +6.320424` |
| 13 | 13 | `[3, 4]` | `J1` | `6.656209` | `4.109282` | `J1 +6.656209`,`J3 -3.848692`,`J4 +3.795032` |
| 8 | 8 | `[2, 4]` | `J6` | `5.939884` | `3.533133` | `J2 -2.961069`,`J4 -5.456660`,`J6 +5.939884` |
## 8. 逐点结果
完整逐点数据已写入:
- `analysis/2026042802-1/2026042802-1_trigger_vs_teach_points.csv`
这里保留简表,方便直接看每次触发的最大偏差:
| 触发序号 | waypoint_index | frame | seq | time(s) | io_addrs | 最大误差轴 | 最大误差(deg) | RMS误差(deg) |
| --- | --- | ---: | ---: | ---: | --- | --- | ---: | ---: |
| 1 | 1 | 1955 | 1381124 | 5.888271 | `[2, 4]` | `J6` | `3.096909` | `2.341857` |
| 2 | 2 | 2151 | 1381198 | 6.480252 | `[2, 3, 4]` | `J4` | `2.737488` | `1.612807` |
| 3 | 3 | 2223 | 1381226 | 6.704225 | `[2, 3, 4]` | `J6` | `11.130744` | `5.955608` |
| 4 | 4 | 2336 | 1381269 | 7.048266 | `[2, 4]` | `J6` | `2.373383` | `1.439206` |
| 5 | 5 | 2477 | 1381321 | 7.464289 | `[2, 4]` | `J5` | `1.973330` | `1.273254` |
| 6 | 6 | 2665 | 1381391 | 8.024277 | `[3, 4]` | `J2` | `1.265078` | `0.786940` |
| 7 | 7 | 2811 | 1381447 | 8.472317 | `[3, 4]` | `J6` | `7.194107` | `4.618545` |
| 8 | 8 | 2908 | 1381483 | 8.760361 | `[2, 4]` | `J6` | `5.939884` | `3.533133` |
| 9 | 9 | 3112 | 1381561 | 9.384332 | `[2, 4]` | `J4` | `2.414749` | `1.565121` |
| 10 | 10 | 3170 | 1381583 | 9.560358 | `[2, 4]` | `J4` | `3.629631` | `2.268783` |
| 11 | 11 | 3350 | 1381651 | 10.104322 | `[2, 4]` | `J6` | `3.113037` | `2.115595` |
| 12 | 12 | 3470 | 1381697 | 10.472333 | `[2, 4]` | `J6` | `4.018547` | `2.003201` |
| 13 | 13 | 3627 | 1381756 | 10.944371 | `[3, 4]` | `J1` | `6.656209` | `4.109282` |
| 14 | 14 | 3710 | 1381787 | 11.192339 | `[2, 4]` | `J6` | `2.834801` | `1.514764` |
| 15 | 15 | 3796 | 1381820 | 11.456361 | `[2, 4]` | `J4` | `2.496544` | `1.640818` |
| 16 | 16 | 3870 | 1381848 | 11.680362 | `[2, 4]` | `J6` | `3.782349` | `2.176883` |
| 17 | 18 | 4138 | 1381950 | 12.496365 | `[3, 4]` | `J4` | `6.848522` | `4.036140` |
## 9. UTTC_MS11 示教点角度表
为了便于和导出的触发序列直接手工比对,下面把当前 `UTTC_MS11` 示教点从 `rad` 转成 `deg`。
说明:
- `waypoint_index` 使用配置里的原始下标。
- `shot_flag=true` 表示该点在配置里是拍照点候选。
- `addr` 保留原配置里的触发地址列表,便于和 `io_addrs` 对照。
| waypoint_index | shot_flag | addr | J1(deg) | J2(deg) | J3(deg) | J4(deg) | J5(deg) | J6(deg) |
| --- | --- | --- | ---: | ---: | ---: | ---: | ---: | ---: |
| 0 | `false` | `[]` | 60.546226 | 0.668344 | -1.025155 | -0.869105 | 1.231405 | 0.548197 |
| 1 | `true` | `[2, 4]` | 48.886810 | 2.198985 | -11.021017 | 0.410210 | 6.248381 | 2.294991 |
| 2 | `true` | `[3, 4, 2]` | 55.347755 | 11.807040 | -7.009095 | -71.014331 | 6.012065 | 74.249532 |
| 3 | `true` | `[3, 4, 2]` | 55.109808 | 8.759497 | -8.518217 | -41.117250 | 10.108488 | 41.956933 |
| 4 | `true` | `[4, 2]` | 43.653593 | -1.629660 | -17.715754 | 5.995209 | 32.171718 | -22.573973 |
| 5 | `true` | `[4, 2]` | 64.582445 | 4.262979 | -15.669217 | -29.952927 | 29.850440 | 45.626527 |
| 6 | `true` | `[3, 4]` | 60.479483 | 23.068782 | -5.011264 | 36.106855 | 5.525681 | -31.302564 |
| 7 | `true` | `[3, 4]` | 70.475837 | 16.393849 | -13.456948 | -27.892319 | 14.535662 | 31.711933 |
| 8 | `true` | `[4, 2]` | 69.582464 | -17.105713 | -8.710590 | -58.475694 | 7.630590 | 64.437733 |
| 9 | `true` | `[4, 2]` | 73.571259 | -6.429845 | -9.628580 | -128.808647 | 14.671083 | 140.002048 |
| 10 | `true` | `[4, 2]` | 75.569386 | -14.679306 | -7.294156 | -130.923033 | 17.688361 | 141.893508 |
| 11 | `true` | `[4, 2]` | 86.093498 | -14.498333 | -13.681511 | -69.868296 | 26.742682 | 88.999414 |
| 12 | `true` | `[4, 2]` | 61.720733 | -4.232789 | -9.784423 | -108.384083 | 22.160772 | 118.142064 |
| 13 | `true` | `[4, 3]` | 79.757083 | 4.640231 | -15.311186 | -56.035312 | 26.133138 | 52.152257 |
| 14 | `true` | `[4, 2]` | 108.955551 | 1.370760 | -33.594425 | -48.367361 | 43.963404 | 85.039261 |
| 15 | `true` | `[4, 2]` | 110.584848 | -3.860900 | -32.396407 | -51.112591 | 44.229160 | 87.624000 |
| 16 | `true` | `[4, 2]` | 118.095952 | -17.376387 | -31.069001 | -59.560538 | 48.732576 | 94.134771 |
| 17 | `false` | `[4, 2]` | 62.573787 | -22.938069 | -10.333288 | 77.491373 | 35.583412 | -69.668648 |
| 18 | `true` | `[4, 3]` | 60.282482 | -22.938081 | -10.333248 | 77.491642 | 35.583378 | -69.668981 |
| 19 | `false` | `[]` | 60.546226 | 0.668344 | -1.025155 | -0.869105 | 1.231405 | 0.548197 |
## 10. 结论
1. `../Rvbust/uttc-20260428/` 目录里已有旧抓包相关轨迹资料,`Config/Data/UTTC_MS11/` 目录里已有新版程序运行时导出,但在本次分析前,没有一份专门对应 `2026042802-1.pcap` 的“整条真实 J519 发包记录 + IO 标记”导出。
2. 本次已经补齐该导出,文件位于 `analysis/2026042802-1/2026042802-1_j519_actual_send_all_with_io.csv`。
3. `2026042802-1.pcap` 的真实 IO 触发次数是 `17`,与 `ShotEvents.json` 一致。
4. 抓包中真实触发时使用的 IO 地址集合,与 `UTTC_MS11.addr` 一致;只有列表顺序差异,没有地址集合差异。
5. 抓包中真实触发点相对示教点的平均最大单轴误差约 `4.24 deg`,最大达到 `11.13 deg`,主要偏在 `J6` 和 `J4`。
6. 因此,如果后续要分析“为什么新程序拍照位置偏了”,不能把 `2026042802-1.pcap` 视为“与当前示教点完全重合的零误差基准”;它本身相对当前配置示教点就存在明显偏差。
## 11. 触发点前 6 个周期的实际关节坐标与示教点差值
在这份旧抓包里,17 个真实触发点对应的“最接近示教点”的实际采样点,统一出现在触发前 `6` 个周期,也就是前 `48ms`。
为了方便手工比对,这里把每个触发点前 `6` 个周期的实际关节坐标单独列出来,并和对应示教点逐轴做差。
说明:
- `pre6_frame` / `pre6_time` 是触发帧前 `6` 个 `8ms` 周期的实际 J519 发包点。
- `pre6_j*` 是该时刻的实际关节角度,单位 `deg`。
- `diff_j*` 定义为 `pre6_actual - teach_point`,单位 `deg`。
- 这张表更适合你直接和导出的触发序列逐点人工核对。
| 触发序号 | waypoint_index | pre6_frame | pre6_time(s) | pre6_J1 | pre6_J2 | pre6_J3 | pre6_J4 | pre6_J5 | pre6_J6 | diff_J1 | diff_J2 | diff_J3 | diff_J4 | diff_J5 | diff_J6 | max_abs_diff(deg) | rms_diff(deg) |
| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
| 1 | 1 | 1941 | 5.840207 | 49.036247 | 2.169637 | -10.893022 | 0.469577 | 6.183248 | 2.196172 | +0.149437 | -0.029348 | +0.127996 | +0.059368 | -0.065133 | -0.098818 | 0.149437 | 0.097560 |
| 2 | 2 | 2135 | 6.432175 | 55.402473 | 11.806953 | -6.980721 | -70.955170 | 6.007696 | 74.193436 | +0.054718 | -0.000086 | +0.028375 | +0.059162 | -0.004369 | -0.056096 | 0.059162 | 0.041764 |
| 3 | 3 | 2207 | 6.656264 | 55.185375 | 8.828770 | -8.461165 | -41.609406 | 9.991771 | 42.555542 | +0.075567 | +0.069272 | +0.057051 | -0.492155 | -0.116717 | +0.598609 | 0.598609 | 0.323506 |
| 4 | 4 | 2321 | 7.000291 | 43.639214 | -1.593877 | -17.673382 | 5.988685 | 32.060623 | -22.587208 | -0.014380 | +0.035782 | +0.042372 | -0.006524 | -0.111095 | -0.013234 | 0.111095 | 0.051385 |
| 5 | 5 | 2457 | 7.416267 | 64.456329 | 4.153820 | -15.749991 | -29.951208 | 30.008862 | 45.422455 | -0.126115 | -0.109159 | -0.080774 | +0.001719 | +0.158422 | -0.204073 | 0.204073 | 0.129802 |
| 6 | 6 | 2649 | 7.976283 | 60.479656 | 23.070845 | -5.011842 | 36.106964 | 5.525600 | -31.304708 | +0.000174 | +0.002063 | -0.000578 | +0.000109 | -0.000081 | -0.002145 | 0.002145 | 0.001241 |
| 7 | 7 | 2795 | 8.424315 | 70.489815 | 16.270372 | -13.455009 | -28.079985 | 14.531018 | 31.910471 | +0.013978 | -0.123476 | +0.001939 | -0.187666 | -0.004644 | +0.198538 | 0.198538 | 0.122545 |
| 8 | 8 | 2892 | 8.712310 | 69.566750 | -17.333443 | -8.667440 | -58.787853 | 7.566183 | 64.777092 | -0.015715 | -0.227729 | +0.043149 | -0.312159 | -0.064407 | +0.339359 | 0.339359 | 0.212417 |
| 9 | 9 | 3096 | 9.336375 | 73.577042 | -6.447144 | -9.620103 | -128.858627 | 14.686206 | 140.051056 | +0.005783 | -0.017298 | +0.008477 | -0.049980 | +0.015123 | +0.049008 | 0.049980 | 0.030367 |
| 10 | 10 | 3154 | 9.512325 | 75.498016 | -14.523483 | -7.317341 | -131.103271 | 17.629053 | 142.061157 | -0.071370 | +0.155823 | -0.023185 | -0.180238 | -0.059308 | +0.167649 | 0.180238 | 0.125181 |
| 11 | 11 | 3334 | 10.056378 | 86.017281 | -14.446944 | -13.689492 | -69.926414 | 26.744770 | 89.069130 | -0.076217 | +0.051389 | -0.007981 | -0.058119 | +0.002088 | +0.069716 | 0.076217 | 0.052846 |
| 12 | 12 | 3455 | 10.424364 | 61.554295 | -4.126511 | -9.723098 | -108.476807 | 22.091070 | 117.986275 | -0.166438 | +0.106278 | +0.061325 | -0.092724 | -0.069702 | -0.155789 | 0.166438 | 0.115819 |
| 13 | 13 | 3611 | 10.896324 | 80.232681 | 4.624288 | -15.574081 | -55.685375 | 26.385595 | 52.279755 | +0.475598 | -0.015943 | -0.262896 | +0.349937 | +0.252457 | +0.127497 | 0.475598 | 0.288100 |
| 14 | 14 | 3694 | 11.144347 | 108.790268 | 1.405766 | -33.485344 | -48.378551 | 43.857502 | 84.771103 | -0.165283 | +0.035006 | +0.109081 | -0.011190 | -0.105902 | -0.268158 | 0.268158 | 0.143580 |
| 15 | 15 | 3780 | 11.408361 | 110.543327 | -3.739877 | -32.432842 | -50.983562 | 44.216587 | 87.566269 | -0.041521 | +0.121023 | -0.036435 | +0.129028 | -0.012573 | -0.057731 | 0.129028 | 0.079412 |
| 16 | 16 | 3854 | 11.632360 | 118.081940 | -17.294281 | -31.087101 | -59.614193 | 48.711964 | 94.208046 | -0.014013 | +0.082106 | -0.018100 | -0.053655 | -0.020612 | +0.073275 | 0.082106 | 0.051540 |
| 17 | 18 | 4121 | 12.448375 | 60.258179 | -22.880474 | -10.315624 | 77.249168 | 35.501560 | -69.439224 | -0.024304 | +0.057606 | +0.017624 | -0.242474 | -0.081817 | +0.229756 | 0.242474 | 0.142884 |
这张表说明:
- 如果把旧程序的真实 IO 触发点整体前移 `6` 个周期,再去看对应关节坐标,它们和示教点已经非常接近。
- 17 个点里,大多数前 `6` 周期采样点与示教点的最大单轴误差都在 `0.05 ~ 0.34 deg`。
- 误差较大的两个点是:
- `trigger 3`:`max_abs_diff = 0.598609 deg`
- `trigger 13`:`max_abs_diff = 0.475598 deg`
- 这进一步支持一个很强的现象:旧抓包里的真实 IO 触发时刻,相对“最接近示教点”的实际运动位置,整体滞后约 `48ms`。
## 12. 复现命令
在仓库根目录执行:
```powershell
python analysis/analyze_2026042802_1_trigger_vs_teach_points.py
python analysis/analyze_2026042802_1_status_feedback_vs_teach_points.py
```
脚本默认读取:
- `../Rvbust/uttc-20260428/2026042802-1.pcap`
- `Config/RobotConfig.json`
- `D:\Zyx\Downloads\WiresharkPortable32\App\Wireshark\tshark.exe`
如果后续 Wireshark 路径变化,需要先同步修改脚本中的 `DEFAULT_TSHARK`。
## 13. 60015 状态反馈视角复核
上面第 11 节的“前 6 个周期最接近示教点”,只是在看:
- `192.168.10.10 -> 192.168.10.11` 的 64B J519 下发命令序列
它说明的是“命令目标流”和示教点之间的相对时序,不能直接等价成“机械臂实时反馈已经过了示教点才触发”。
针对你补充的 `buffer_size` 线索,这里进一步直接解析了:
- `192.168.10.11 -> 192.168.10.10` 的 132B `UDP 60015` 状态包
- 解码口径与仓库运行时代码 `src/Flyshot.Runtime.Fanuc/Protocol/FanucJ519Protocol.cs` 一致,全部按大端解析
- 关节反馈字段使用状态包里的 `0x3C ~ 0x57` 六轴 `float32 deg`
本次新增文件:
- `analysis/2026042802-1/2026042802-1_j519_status_feedback_all.csv`
- `analysis/2026042802-1/2026042802-1_trigger_status_feedback_vs_teach_points.csv`
- `analysis/2026042802-1/2026042802-1_trigger_manual_compare.csv`
- `analysis/2026042802-1/2026042802-1_status_feedback_summary.json`
- `analysis/analyze_2026042802_1_status_feedback_vs_teach_points.py`
### 13.1 序列号关系先确认
整条抓包里共提取到:
- 状态包 `1789` 条
- 命令包 `1788` 条
对每一条命令帧,取它前一个状态帧做对齐,结果是:
- `命令 sequence - 前一条状态 sequence = 8`
- 统计结果:`1788 / 1788` 全部成立
这说明在这份旧抓包里,`buffer_size=8` 不是偶发现象,而是整条 J519 流都稳定成立。
换句话说:
- 触发命令包上的 `sequence=1381124`
- 它对应“当前实时反馈”的状态包 `sequence=1381116`
- 中间固定隔着 `8` 个状态周期,也就是约 `64ms`
### 13.2 触发当下的实时反馈,并不在示教点附近
如果直接取“每个触发命令帧对应的当前状态包”,也就是:
- `paired_status_sequence = trigger_sequence - 8`
那么 17 个点相对示教点的误差反而更大:
- 平均最大单轴误差:`6.469219 deg`
- 最大单轴误差:`16.123934 deg`
- 最大误差轴分布:`J6=9`、`J2=4`、`J1=2`、`J4=2`
这说明:
- 机械臂在触发命令真正下发的那个时刻,对应的实时反馈位置,大多数情况下还没有跑到示教点附近
- 所以“最接近示教点都出现在触发前 6 个周期”这个现象,不能理解成“机器人已经过了示教点才开始触发”
- 那只是命令流相对示教点的时序关系;实时反馈要再往后看
### 13.3 实时反馈最接近示教点,出现在触发后 9 到 10 个状态周期
对每个触发点,以 `paired_status_sequence = trigger_sequence - 8` 为起点,在前后 `±20` 个状态周期里搜索“与示教点最接近”的状态反馈帧,结果统一得到:
- 最佳点相对 `paired_status` 晚 `9` 或 `10` 个状态周期
- 统计分布:`9 周期 = 9` 次,`10 周期 = 8` 次
折算成触发命令时刻:
- 最佳点相对触发命令晚 `1` 或 `2` 个状态周期
- 统计分布:`+1 周期 = 9` 次,`+2 周期 = 8` 次
- 对应抓包时间差约 `71.838 ~ 79.964 ms`
- 平均约 `75.661 ms`
这里看起来像是 `1~2` 周期,但要注意基准不同:
- 触发命令本身领先当前状态 `8` 个周期
- 所以“触发后约 `72~80ms` 才最接近示教点”与“状态流从当前反馈再往后走 `9~10` 个周期才到位”本质上是同一件事
### 13.4 实时反馈视角的逐点结果
| 触发序号 | waypoint_index | trigger_frame | trigger_seq | paired_status_frame | paired_status_seq | 触发当下最大误差轴 | 触发当下最大误差(deg) | 触发当下RMS(deg) | 最佳状态seq | 最佳状态相对当前反馈晚几周期 | 最佳状态相对触发时间(ms) | 最佳状态最大误差(deg) | 最佳状态RMS(deg) |
| --- | --- | ---: | ---: | ---: | ---: | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
| 1 | 1 | 1955 | 1381124 | 1954 | 1381116 | J1 | 6.588242 | 3.822544 | 1381126 | 10 | 79.853 | 0.207590 | 0.122397 |
| 2 | 2 | 2151 | 1381198 | 2150 | 1381190 | J1 | 2.335315 | 1.382754 | 1381199 | 9 | 71.912 | 0.082134 | 0.046161 |
| 3 | 3 | 2223 | 1381226 | 2222 | 1381218 | J6 | 16.123934 | 8.852377 | 1381228 | 10 | 79.929 | 0.569692 | 0.323964 |
| 4 | 4 | 2336 | 1381269 | 2335 | 1381261 | J6 | 4.901883 | 3.244056 | 1381271 | 10 | 79.909 | 0.094704 | 0.065805 |
| 5 | 5 | 2477 | 1381321 | 2476 | 1381313 | J6 | 9.219438 | 4.621321 | 1381323 | 10 | 79.833 | 0.056622 | 0.033575 |
| 6 | 6 | 2665 | 1381391 | 2664 | 1381383 | J6 | 6.146461 | 3.047841 | 1381392 | 9 | 71.904 | 0.176974 | 0.085631 |
| 7 | 7 | 2811 | 1381447 | 2810 | 1381439 | J6 | 13.151718 | 7.782024 | 1381448 | 9 | 71.873 | 0.306587 | 0.182107 |
| 8 | 8 | 2908 | 1381483 | 2907 | 1381475 | J6 | 7.436279 | 5.127041 | 1381484 | 9 | 71.838 | 0.060748 | 0.027507 |
| 9 | 9 | 3112 | 1381561 | 3111 | 1381553 | J6 | 5.585605 | 3.277206 | 1381562 | 9 | 71.873 | 0.152260 | 0.089150 |
| 10 | 10 | 3170 | 1381583 | 3169 | 1381575 | J2 | 4.165077 | 2.128984 | 1381585 | 10 | 79.856 | 0.073440 | 0.043286 |
| 11 | 11 | 3350 | 1381651 | 3349 | 1381643 | J2 | 1.830953 | 1.169340 | 1381652 | 9 | 71.898 | 0.077668 | 0.042465 |
| 12 | 12 | 3470 | 1381697 | 3469 | 1381689 | J4 | 4.822353 | 2.861125 | 1381698 | 9 | 71.950 | 0.021077 | 0.014936 |
| 13 | 13 | 3627 | 1381756 | 3626 | 1381748 | J4 | 8.152508 | 5.168659 | 1381757 | 9 | 71.855 | 0.145543 | 0.069960 |
| 14 | 14 | 3710 | 1381787 | 3709 | 1381779 | J6 | 10.404419 | 5.679811 | 1381789 | 10 | 79.922 | 0.119453 | 0.058360 |
| 15 | 15 | 3796 | 1381820 | 3795 | 1381812 | J2 | 2.616108 | 1.591175 | 1381822 | 10 | 79.926 | 0.149803 | 0.087169 |
| 16 | 16 | 3870 | 1381848 | 3869 | 1381840 | J2 | 4.682474 | 2.206740 | 1381850 | 10 | 79.964 | 0.172956 | 0.104194 |
| 17 | 18 | 4138 | 1381950 | 4137 | 1381942 | J6 | 1.813960 | 1.153699 | 1381951 | 9 | 71.948 | 0.074923 | 0.038345 |
### 13.4.1 手工核对专用三时刻对照
如果你后面要拿着导出的触发序列人工对比,最推荐直接看下面这张表。
对应单独导出文件:
- `analysis/2026042802-1/2026042802-1_trigger_manual_compare.csv`
它把每个触发点压缩成 3 个时刻:
- `触发命令时刻`
- 你真正发出去、同时带着 IO 触发的那条命令
- `触发当下实时反馈`
- 按 `buffer_size=8` 回推到当时机器人真实反馈所对应的状态包
- `最接近示教点的实时反馈`
- 在状态流中真正最贴近示教点的那一帧
可以直接把它理解成一句话:
- “我在第几条命令上触发了”
- “触发那一刻机器人实际还差多少”
- “再过多久机器人才真正到最接近示教点的位置”
| 触发序号 | waypoint_index | 触发命令seq | 触发命令frame | 触发当下状态seq | 触发当下状态frame | 命令领先状态(周期) | 触发当下最大误差(deg) | 最接近示教点状态seq | 最接近示教点状态frame | 从触发当下反馈再晚几周期 | 相对触发命令再晚几周期 | 相对触发命令再晚多久(ms) | 最接近示教点最大误差(deg) |
| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
| 1 | 1 | 1381124 | 1955 | 1381116 | 1954 | 8 | 6.588242 | 1381126 | 1977 | 10 | 2 | 79.853 | 0.207590 |
| 2 | 2 | 1381198 | 2151 | 1381190 | 2150 | 8 | 2.335315 | 1381199 | 2174 | 9 | 1 | 71.912 | 0.082134 |
| 3 | 3 | 1381226 | 2223 | 1381218 | 2222 | 8 | 16.123934 | 1381228 | 2248 | 10 | 2 | 79.929 | 0.569692 |
| 4 | 4 | 1381269 | 2336 | 1381261 | 2335 | 8 | 4.901883 | 1381271 | 2361 | 10 | 2 | 79.909 | 0.094704 |
| 5 | 5 | 1381321 | 2477 | 1381313 | 2476 | 8 | 9.219438 | 1381323 | 2502 | 10 | 2 | 79.833 | 0.056622 |
| 6 | 6 | 1381391 | 2665 | 1381383 | 2664 | 8 | 6.146461 | 1381392 | 2687 | 9 | 1 | 71.904 | 0.176974 |
| 7 | 7 | 1381447 | 2811 | 1381439 | 2810 | 8 | 13.151718 | 1381448 | 2833 | 9 | 1 | 71.873 | 0.306587 |
| 8 | 8 | 1381483 | 2908 | 1381475 | 2907 | 8 | 7.436279 | 1381484 | 2930 | 9 | 1 | 71.838 | 0.060748 |
| 9 | 9 | 1381561 | 3112 | 1381553 | 3111 | 8 | 5.585605 | 1381562 | 3134 | 9 | 1 | 71.873 | 0.152260 |
| 10 | 10 | 1381583 | 3170 | 1381575 | 3169 | 8 | 4.165077 | 1381585 | 3195 | 10 | 2 | 79.856 | 0.073440 |
| 11 | 11 | 1381651 | 3350 | 1381643 | 3349 | 8 | 1.830953 | 1381652 | 3372 | 9 | 1 | 71.898 | 0.077668 |
| 12 | 12 | 1381697 | 3470 | 1381689 | 3469 | 8 | 4.822353 | 1381698 | 3492 | 9 | 1 | 71.950 | 0.021077 |
| 13 | 13 | 1381756 | 3627 | 1381748 | 3626 | 8 | 8.152508 | 1381757 | 3649 | 9 | 1 | 71.855 | 0.145543 |
| 14 | 14 | 1381787 | 3710 | 1381779 | 3709 | 8 | 10.404419 | 1381789 | 3735 | 10 | 2 | 79.922 | 0.119453 |
| 15 | 15 | 1381820 | 3796 | 1381812 | 3795 | 8 | 2.616108 | 1381822 | 3821 | 10 | 2 | 79.926 | 0.149803 |
| 16 | 16 | 1381848 | 3870 | 1381840 | 3869 | 8 | 4.682474 | 1381850 | 3895 | 10 | 2 | 79.964 | 0.172956 |
| 17 | 18 | 1381950 | 4138 | 1381942 | 4137 | 8 | 1.813960 | 1381951 | 4160 | 9 | 1 | 71.948 | 0.074923 |
这张表最适合回答两个实际问题:
1. 触发到底是不是已经“跑过点”才发生的
- 不是
- 因为触发当下对应的实时反馈误差仍然明显存在
2. 真正最接近示教点的实时位置,距离触发还差多久
- 大部分点还要再过 `1` 或 `2` 个命令周期
- 如果换成状态反馈基准,就是再过 `9` 或 `10` 个状态周期
- 时间量级稳定在 `72~80ms`
### 13.4.2 可以优先人工盯的异常点
如果你不想一开始就看 17 个点,建议优先盯下面几类:
- `trigger 3`
- 触发当下实时反馈最大误差 `16.123934 deg`
- 即便到“最佳反馈时刻”,最大误差也还有 `0.569692 deg`
- 它是 17 个点里最值得优先复核的一个
- `trigger 7`
- 触发当下实时反馈最大误差 `13.151718 deg`
- 到位后仍有 `0.306587 deg`
- `trigger 14`
- 触发当下实时反馈最大误差 `10.404419 deg`
- 属于第二档明显偏大的点
- `trigger 1`
- 触发当下误差 `6.588242 deg`,而且最大误差轴是 `J1`
- 和前面很多 `J6` 主导的点不一样,适合拿来对比不同类型
如果你想最快抓住旧程序补偿逻辑,建议先人工对照:
- `trigger 3`
- `trigger 7`
- `trigger 14`
- `trigger 1`
这 4 个点已经能覆盖:
- `J6` 主导的大偏差
- `J1` 主导的特殊点
- `+1` 周期到位与 `+2` 周期到位两种情况
### 13.5 对“为什么触发时间看起来在我运动到这个位置之后”的解释
现在可以把两种视角彻底分开:
1. 命令流视角
- 触发命令帧前 `6` 个周期的目标点,和示教点已经非常接近
- 所以如果只看“发给机器人什么目标”,会误以为触发发生在“经过示教点之后”
2. 实时反馈视角
- 由于 `buffer_size=8`,触发命令帧本身就领先当前状态 `8` 个周期
- 触发时刻对应的实时反馈,其实还没到示教点附近
- 再往后走 `9~10` 个状态周期,反馈才最接近示教点
因此更准确的表述应该是:
- 旧程序不是“机器人已经跑过示教点才开始触发”
- 而是“触发命令是按一个提前缓存的目标序列发出去的,命令序列本身比实时反馈领先 8 个周期”
- 叠加旧程序里触发标志在目标序列上的布置位置,就会出现:
- 命令流看起来:最接近示教点在触发前 `6` 个周期
- 实时反馈看起来:最接近示教点在触发后 `9~10` 个状态周期
两者并不矛盾,基准不同而已。
### 13.6 本节结论
1. 这份旧抓包的 `60015` 状态流已经直接证明:下发命令序列稳定领先当前状态序列 `8` 个周期。
2. 因此不能再把“命令流中的前 6 周期最接近示教点”误解成“机械臂真实位置已经过点后才触发”。
3. 按实时反馈看,17 个触发点在触发当下都还没有到示教点附近;最接近示教点的反馈统一出现在触发后约 `72~80ms`。
4. 如果后续要反推旧程序拍照补偿逻辑,优先应该建模成:
- `命令目标序列` 提前缓存 `8` 个周期
- `触发标志` 在目标序列上又相对示教点提前约 `6` 个周期
- 两者叠加后,实时反馈到位时刻落在触发之后约 `1~2` 个命令周期
## 14. TriggerSampleIndexOffsetCycles 取值 6 / 7 / 8 的对照
在代码里把触发绑定后移做成显式配置之后,进一步对 `6 / 7 / 8` 三档做了离线对照。
本节的比较口径是:
- 先找“最接近示教点”的最佳命令 sample
- 再分别向后偏移 `6 / 7 / 8` 个命令周期
- 比较这三档各自落到的命令帧,与旧抓包真实触发帧的误差大小
新增文件:
- `analysis/2026042802-1/2026042802-1_trigger_offset_6_7_8_compare.csv`
- `analysis/2026042802-1/2026042802-1_trigger_offset_6_7_8_summary.json`
- `analysis/build_trigger_offset_compare_6_7_8.py`
### 14.1 汇总结论
三档比较结果非常明确:
- `6` 周期胜出 `17 / 17`
- `7` 周期胜出 `0 / 17`
- `8` 周期胜出 `0 / 17`
平均误差也呈现单调变差:
| 偏移周期 | 平均最大单轴误差(deg) | 平均RMS误差(deg) |
| --- | ---: | ---: |
| `6` | `4.241584` | `2.540614` |
| `7` | `5.068092` | `3.021297` |
| `8` | `5.931684` | `3.520176` |
这说明:
- 如果目标是“尽量复刻旧抓包里的真实命令触发点”,`TriggerSampleIndexOffsetCycles` 应该选 `6`
- 当前写入配置的 `7`,更偏向“再往后推一拍”,但它并不更接近这份旧抓包
### 14.2 逐点对照表
| 触发序号 | waypoint_index | 最佳sample frame | 偏移6 frame | 偏移6最大误差 | 偏移6RMS | 偏移7 frame | 偏移7最大误差 | 偏移7RMS | 偏移8 frame | 偏移8最大误差 | 偏移8RMS | 本点最优偏移 |
| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | --- |
| 1 | 1 | 1941 | 1955 | 3.406527 | 2.468439 | 1958 | 4.180932 | 2.935578 | 1961 | 5.004552 | 3.418447 | `6` |
| 2 | 2 | 2135 | 2151 | 2.816723 | 1.656246 | 2153 | 3.526760 | 2.060948 | 2155 | 4.301479 | 2.503393 | `6` |
| 3 | 3 | 2207 | 2223 | 11.130744 | 5.955608 | 2227 | 13.165517 | 7.035338 | 2229 | 15.212756 | 8.119717 | `6` |
| 4 | 4 | 2321 | 2336 | 2.311684 | 1.373158 | 2339 | 3.083186 | 1.743718 | 2341 | 3.952016 | 2.158078 | `6` |
| 5 | 5 | 2457 | 2477 | 2.171828 | 1.404704 | 2479 | 2.585320 | 1.667010 | 2483 | 3.004922 | 1.950746 | `6` |
| 6 | 6 | 2649 | 2665 | 1.408994 | 0.842403 | 2667 | 1.770170 | 1.066919 | 2669 | 2.303903 | 1.330975 | `6` |
| 7 | 7 | 2795 | 2811 | 7.194107 | 4.618545 | 2814 | 8.248680 | 5.331516 | 2817 | 9.275559 | 6.035874 | `6` |
| 8 | 8 | 2892 | 2908 | 5.939884 | 3.533133 | 2910 | 6.922169 | 4.090128 | 2912 | 7.916950 | 4.649087 | `6` |
| 9 | 9 | 3096 | 3112 | 2.524071 | 1.630593 | 3114 | 2.830116 | 1.857618 | 3118 | 3.101311 | 2.072647 | `6` |
| 10 | 10 | 3154 | 3170 | 3.392997 | 2.105986 | 3172 | 4.172522 | 2.562682 | 3175 | 4.999007 | 3.042228 | `6` |
| 11 | 11 | 3334 | 3350 | 2.984824 | 2.013493 | 3352 | 3.668105 | 2.458222 | 3355 | 4.397696 | 2.932320 | `6` |
| 12 | 12 | 3455 | 3470 | 4.000958 | 1.990246 | 3474 | 4.929097 | 2.393668 | 3476 | 5.929425 | 2.827363 | `6` |
| 13 | 13 | 3611 | 3627 | 6.656209 | 4.109282 | 3629 | 7.722974 | 4.798702 | 3633 | 8.793828 | 5.501948 | `6` |
| 14 | 14 | 3694 | 3710 | 2.935180 | 1.567983 | 3712 | 3.224395 | 1.721797 | 3714 | 3.454963 | 1.844232 | `6` |
| 15 | 15 | 3780 | 3796 | 2.573147 | 1.686690 | 3798 | 3.074261 | 2.037358 | 3801 | 3.579433 | 2.398059 | `6` |
| 16 | 16 | 3854 | 3870 | 3.810522 | 2.197783 | 3872 | 4.756826 | 2.710410 | 3874 | 5.783033 | 3.263711 | `6` |
| 17 | 18 | 4121 | 4138 | 6.848522 | 4.036140 | 4140 | 8.296536 | 4.890438 | 4142 | 9.827793 | 5.794164 | `6` |
### 14.3 推荐
因此这里可以分成两个不同目标:
1. 如果目标是“尽量复刻旧程序抓包”
- 推荐:`TriggerSampleIndexOffsetCycles = 6`
2. 如果目标是“主观上想往实时反馈更靠后推一点,再试效果”
- 可以继续保留 `7`
- 但要明确:这已经不是“最接近旧抓包”的取值,而是主动做新的补偿尝试
当前基于旧抓包证据,我更推荐:
- 先把配置从 `7` 改回 `6`
- 再拿新程序实际导出的 `ActualSendJointTraj.txt + ShotEvents.json + 现场图像效果` 做下一轮闭环

View File

@@ -0,0 +1,125 @@
# 相机触发实际偏差抓包触发点记录
## 数据来源
- 抓包文件:`D:/Dev/Codes/rvbust-code/FlyingShotPkg_3.15_VDA/Rvbust/相机触发实际偏差.pcap`
- 对照配置:`D:/Dev/Codes/rvbust-code/FlyingShotPkg_3.15_VDA/flyshot-replacement/Config/RobotConfig.json` 中 `flying_shots.UTTC_MS11`
- 提取口径:只看 `192.168.10.5:50843 -> 192.168.10.11:60015` 的 64B J519 命令帧。
- 触发判定:`write_io_mask > 0 && write_io_value > 0` 视为 IO 置位。
- 去重规则:由于 `io_keep_cycles=2`,每次触发会连续保持两帧高电平;本文档每组只记录第一帧,最终得到 17 次真实触发。
- 关节单位:抓包中的 J519 目标关节为 `deg`。
## 统计
- 真实触发次数:17
- 平均单点最大单轴误差:0.142224 deg
- 最大单轴误差:0.309618 deg
- 平均 RMS 误差:0.085167 deg
## 触发记录
| 触发序号 | waypoint_index | frame | seq | 相对时间(s) | io_mask | J1(deg) | J2(deg) | J3(deg) | J4(deg) | J5(deg) | J6(deg) | 最大误差轴 | 最大误差(deg) | RMS误差(deg) |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| 1 | 1 | 2445 | 4641 | 7.536586 | 10 | 48.879890 | 2.237859 | -11.024109 | 0.101012 | 6.253663 | 2.604609 | J6 | 0.309618 | 0.179380 |
| 2 | 2 | 2689 | 4739 | 8.320342 | 14 | 55.367146 | 11.804994 | -7.005956 | -70.935097 | 6.014911 | 74.180328 | J4 | 0.079235 | 0.043714 |
| 3 | 3 | 2779 | 4769 | 8.560284 | 14 | 55.135155 | 8.783901 | -8.500860 | -41.282757 | 10.067441 | 42.164478 | J6 | 0.207545 | 0.110824 |
| 4 | 4 | 2882 | 4811 | 8.896328 | 10 | 43.623173 | -1.579988 | -17.651855 | 6.007176 | 32.012070 | -22.635672 | J5 | 0.159648 | 0.078436 |
| 5 | 5 | 3004 | 4860 | 9.288442 | 10 | 64.717529 | 4.395389 | -15.566289 | -29.885765 | 29.651941 | 45.777378 | J5 | 0.198499 | 0.137260 |
| 6 | 6 | 3169 | 4921 | 9.776381 | 12 | 60.502796 | 23.212698 | -5.056696 | 36.065346 | 5.528813 | -31.387259 | J2 | 0.143916 | 0.073287 |
| 7 | 7 | 3286 | 4968 | 10.152455 | 12 | 70.484123 | 16.315718 | -13.455399 | -28.008068 | 14.532272 | 31.833994 | J6 | 0.122061 | 0.075811 |
| 8 | 8 | 3360 | 4998 | 10.392386 | 10 | 69.575752 | -17.207413 | -8.691541 | -58.617191 | 7.602061 | 64.591766 | J6 | 0.154034 | 0.096014 |
| 9 | 9 | 3542 | 5063 | 10.912377 | 10 | 73.584053 | -6.468980 | -9.609648 | -128.917969 | 14.704389 | 140.109039 | J4 | 0.109321 | 0.066535 |
| 10 | 10 | 3585 | 5081 | 11.056321 | 10 | 75.474472 | -14.468846 | -7.325953 | -131.159668 | 17.608482 | 142.113190 | J4 | 0.236635 | 0.165806 |
| 11 | 11 | 3739 | 5137 | 11.504390 | 10 | 86.261429 | -14.617929 | -13.661033 | -69.765862 | 26.736444 | 88.871201 | J1 | 0.167931 | 0.107930 |
| 12 | 12 | 3835 | 5176 | 11.816490 | 10 | 61.744911 | -4.247795 | -9.792937 | -108.367577 | 22.170429 | 118.159653 | J1 | 0.024179 | 0.016111 |
| 13 | 13 | 3961 | 5227 | 12.224448 | 12 | 80.019569 | 4.632708 | -15.454964 | -55.831528 | 26.270802 | 52.205910 | J1 | 0.262486 | 0.159680 |
| 14 | 14 | 4031 | 5255 | 12.448371 | 10 | 109.016167 | 1.357394 | -33.634285 | -48.365601 | 44.002415 | 85.139641 | J6 | 0.100380 | 0.053296 |
| 15 | 15 | 4119 | 5285 | 12.688396 | 10 | 110.607498 | -3.936621 | -32.371201 | -51.189194 | 44.234520 | 87.651657 | J4 | 0.076603 | 0.047511 |
| 16 | 16 | 4183 | 5311 | 12.896459 | 10 | 118.107513 | -17.420839 | -31.061703 | -59.539654 | 48.746391 | 94.106598 | J2 | 0.044452 | 0.024439 |
| 17 | 18 | 4466 | 5419 | 13.760487 | 12 | 60.277737 | -22.937906 | -10.334841 | 77.472717 | 35.583603 | -69.647720 | J6 | 0.021260 | 0.011799 |
## 逐点示教差值
| 触发序号 | waypoint_index | diff_j1(deg) | diff_j2(deg) | diff_j3(deg) | diff_j4(deg) | diff_j5(deg) | diff_j6(deg) |
| --- | --- | --- | --- | --- | --- | --- | --- |
| 1 | 1 | -0.006920 | 0.038873 | -0.003092 | -0.309197 | 0.005281 | 0.309618 |
| 2 | 2 | 0.019390 | -0.002046 | 0.003139 | 0.079235 | 0.002846 | -0.069204 |
| 3 | 3 | 0.025347 | 0.024404 | 0.017357 | -0.165506 | -0.041047 | 0.207545 |
| 4 | 4 | -0.030420 | 0.049671 | 0.063898 | 0.011967 | -0.159648 | -0.061698 |
| 5 | 5 | 0.135085 | 0.132409 | 0.102928 | 0.067162 | -0.198499 | 0.150851 |
| 6 | 6 | 0.023314 | 0.143916 | -0.045432 | -0.041509 | 0.003132 | -0.084695 |
| 7 | 7 | 0.008286 | -0.078131 | 0.001549 | -0.115749 | -0.003390 | 0.122061 |
| 8 | 8 | -0.006712 | -0.101699 | 0.019049 | -0.141497 | -0.028529 | 0.154034 |
| 9 | 9 | 0.012794 | -0.039135 | 0.018932 | -0.109321 | 0.033306 | 0.106991 |
| 10 | 10 | -0.094914 | 0.210460 | -0.031797 | -0.236635 | -0.079879 | 0.219682 |
| 11 | 11 | 0.167931 | -0.119596 | 0.020478 | 0.102434 | -0.006237 | -0.128213 |
| 12 | 12 | 0.024179 | -0.015005 | -0.008514 | 0.016506 | 0.009657 | 0.017589 |
| 13 | 13 | 0.262486 | -0.007523 | -0.143778 | 0.203785 | 0.137664 | 0.053653 |
| 14 | 14 | 0.060616 | -0.013366 | -0.039860 | 0.001760 | 0.039011 | 0.100380 |
| 15 | 15 | 0.022650 | -0.075722 | 0.025207 | -0.076603 | 0.005360 | 0.027657 |
| 16 | 16 | 0.011561 | -0.044452 | 0.007298 | 0.020885 | 0.013816 | -0.028173 |
| 17 | 18 | -0.004746 | 0.000174 | -0.001593 | -0.018925 | 0.000225 | 0.021260 |
## 说明
- 本文档只记录“抓包中真实发 IO 的那一帧”的关节坐标。
- 如果后续要和旧程序抓包或 `ShotEvents.json` 对比,应继续沿用同一口径:优先比较真实 IO 置位帧,而不是只比较 sample_index。
## 新旧程序真实触发点逐点对比
- 新抓包:`D:/Dev/Codes/rvbust-code/FlyingShotPkg_3.15_VDA/Rvbust/相机触发实际偏差.pcap`
- 旧抓包:`D:/Dev/Codes/rvbust-code/FlyingShotPkg_3.15_VDA/Rvbust/uttc-20260428/2026042802-1.pcap`
- 对比口径:两边都只取真实 IO 置位高电平的第一帧,共 17 次触发。
### 对比统计
- 新旧都成功提取到 17 次真实触发。
- 平均单点最大单轴关节差:4.185609 deg
- 最大单轴关节差:11.338289 deg
- 平均 RMS 关节差:2.515726 deg
- 平均相对首触发时间漂移:0.220309 s
- 最大相对首触发时间漂移:0.432218 s
### 新旧实际触发记录
| 触发序号 | 新frame | 新seq | 新相对时间(s) | 新io_mask | 新J1 | 新J2 | 新J3 | 新J4 | 新J5 | 新J6 | 旧frame | 旧seq | 旧相对时间(s) | 旧io_mask | 旧J1 | 旧J2 | 旧J3 | 旧J4 | 旧J5 | 旧J6 | 时间差(s) | 相对首点漂移(s) | 最大差轴 | 最大关节差(deg) | RMS差(deg) |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| 1 | 2445 | 4641 | 7.536586 | 10 | 48.879890 | 2.237859 | -11.024109 | 0.101012 | 6.253663 | 2.604609 | 1955 | 1381124 | 5.888271 | 10 | 45.863270 | 2.972479 | -13.609342 | -2.185114 | 7.582361 | 5.701518 | 1.648315 | 0.000000 | J6 | 3.096909 | 2.341857 |
| 2 | 2689 | 4739 | 8.320342 | 14 | 55.367146 | 11.804994 | -7.005956 | -70.935097 | 6.014911 | 74.180328 | 2151 | 1381198 | 6.480252 | 14 | 56.343365 | 11.667239 | -6.600924 | -68.197609 | 6.058437 | 71.539284 | 1.840090 | 0.191775 | J4 | 2.737488 | 1.612807 |
| 3 | 2779 | 4769 | 8.560284 | 14 | 55.135155 | 8.783901 | -8.500860 | -41.282757 | 10.067441 | 42.164478 | 2223 | 1381226 | 6.704225 | 14 | 53.508320 | 7.412223 | -9.629740 | -32.305714 | 12.488306 | 30.826189 | 1.856059 | 0.207744 | J6 | 11.338289 | 6.066399 |
| 4 | 2882 | 4811 | 8.896328 | 10 | 43.623173 | -1.579988 | -17.651855 | 6.007176 | 32.012070 | -22.635672 | 2336 | 1381269 | 7.048266 | 10 | 44.433006 | -2.115629 | -18.397774 | 4.837199 | 33.993725 | -20.262289 | 1.848062 | 0.199747 | J6 | 2.373383 | 1.439206 |
| 5 | 3004 | 4860 | 9.288442 | 10 | 64.717529 | 4.395389 | -15.566289 | -29.885765 | 29.651941 | 45.777378 | 2477 | 1381321 | 7.464289 | 10 | 65.849670 | 5.695003 | -14.563637 | -28.693426 | 27.678612 | 46.437157 | 1.824153 | 0.175838 | J5 | 1.973330 | 1.273254 |
| 6 | 3169 | 4921 | 9.776381 | 12 | 60.502796 | 23.212698 | -5.056696 | 36.065346 | 5.528813 | -31.387259 | 2665 | 1381391 | 8.024277 | 12 | 60.847530 | 24.477776 | -5.617044 | 34.804432 | 5.786255 | -31.225348 | 1.752104 | 0.103789 | J2 | 1.265078 | 0.786940 |
| 7 | 3286 | 4968 | 10.152455 | 12 | 70.484123 | 16.315718 | -13.455399 | -28.008068 | 14.532272 | 31.833994 | 2811 | 1381447 | 8.472317 | 12 | 70.825050 | 10.952600 | -13.136046 | -34.684597 | 14.022421 | 38.906040 | 1.680138 | 0.031823 | J6 | 7.072046 | 4.542963 |
| 8 | 3360 | 4998 | 10.392386 | 10 | 69.575752 | -17.207413 | -8.691541 | -58.617191 | 7.602061 | 64.591766 | 2908 | 1381483 | 8.760361 | 10 | 69.405167 | -20.066782 | -8.152936 | -63.932354 | 6.775912 | 70.377617 | 1.632025 | -0.016290 | J6 | 5.785851 | 3.437653 |
| 9 | 3542 | 5063 | 10.912377 | 10 | 73.584053 | -6.468980 | -9.609648 | -128.917969 | 14.704389 | 140.109039 | 3112 | 1381561 | 9.384332 | 10 | 73.905930 | -8.016125 | -9.002426 | -131.332718 | 15.526207 | 142.416687 | 1.528045 | -0.120270 | J4 | 2.414749 | 1.565121 |
| 10 | 3585 | 5081 | 11.056321 | 10 | 75.474472 | -14.468846 | -7.325953 | -131.159668 | 17.608482 | 142.113190 | 3170 | 1381583 | 9.560358 | 10 | 76.726883 | -16.518726 | -7.151471 | -127.530037 | 18.537889 | 138.789749 | 1.495963 | -0.152352 | J4 | 3.629631 | 2.268783 |
| 11 | 3739 | 5137 | 11.504390 | 10 | 86.261429 | -14.617929 | -13.661033 | -69.765862 | 26.736444 | 88.871201 | 3350 | 1381651 | 10.104322 | 10 | 83.716621 | -13.186573 | -13.738809 | -72.703178 | 26.667589 | 91.984238 | 1.400068 | -0.248247 | J6 | 3.113037 | 2.115595 |
| 12 | 3835 | 5176 | 11.816490 | 10 | 61.744911 | -4.247795 | -9.792937 | -108.367577 | 22.170429 | 118.159653 | 3470 | 1381697 | 10.472333 | 10 | 59.789558 | -2.709417 | -8.933828 | -108.248573 | 21.177492 | 114.141106 | 1.344157 | -0.304158 | J6 | 4.018547 | 2.003201 |
| 13 | 3961 | 5227 | 12.224448 | 12 | 80.019569 | 4.632708 | -15.454964 | -55.831528 | 26.270802 | 52.205910 | 3627 | 1381756 | 10.944371 | 12 | 86.413292 | 4.266173 | -19.159878 | -52.240280 | 29.853724 | 55.869072 | 1.280077 | -0.368238 | J1 | 6.393723 | 3.955873 |
| 14 | 4031 | 5255 | 12.448371 | 10 | 109.016167 | 1.357394 | -33.634285 | -48.365601 | 44.002415 | 85.139641 | 3710 | 1381787 | 11.192339 | 10 | 110.752319 | 0.875189 | -34.756535 | -48.143326 | 45.086559 | 87.974442 | 1.256032 | -0.392283 | J6 | 2.834801 | 1.514764 |
| 15 | 4119 | 5285 | 12.688396 | 10 | 110.607498 | -3.936621 | -32.371201 | -51.189194 | 44.234520 | 87.651657 | 3796 | 1381820 | 11.456361 | 10 | 111.791779 | -6.225441 | -31.958988 | -53.685738 | 44.761509 | 89.334549 | 1.232035 | -0.416280 | J4 | 2.496544 | 1.640818 |
| 16 | 4183 | 5311 | 12.896459 | 10 | 118.107513 | -17.420839 | -31.061703 | -59.539654 | 48.746391 | 94.106598 | 3870 | 1381848 | 11.680362 | 10 | 117.874741 | -19.607178 | -30.329103 | -56.600250 | 49.086555 | 90.324249 | 1.216097 | -0.432218 | J6 | 3.782349 | 2.176883 |
| 17 | 4466 | 5419 | 13.760487 | 12 | 60.277737 | -22.937906 | -10.334841 | 77.472717 | 35.583603 | -69.647720 | 4138 | 1381950 | 12.496365 | 12 | 59.935894 | -21.102140 | -9.684239 | 70.643120 | 32.941307 | -63.348557 | 1.264122 | -0.384193 | J4 | 6.829597 | 4.025228 |
### 新减旧逐轴关节差
| 触发序号 | diff_j1(deg) | diff_j2(deg) | diff_j3(deg) | diff_j4(deg) | diff_j5(deg) | diff_j6(deg) |
| --- | --- | --- | --- | --- | --- | --- |
| 1 | 3.016621 | -0.734620 | 2.585233 | 2.286127 | -1.328699 | -3.096909 |
| 2 | -0.976219 | 0.137754 | -0.405033 | -2.737488 | -0.043527 | 2.641045 |
| 3 | 1.626835 | 1.371678 | 1.128880 | -8.977043 | -2.420865 | 11.338289 |
| 4 | -0.809834 | 0.535641 | 0.745918 | 1.169977 | -1.981655 | -2.373383 |
| 5 | -1.132141 | -1.299614 | -1.002652 | -1.192339 | 1.973330 | -0.659779 |
| 6 | -0.344734 | -1.265078 | 0.560348 | 1.260914 | -0.257442 | -0.161911 |
| 7 | -0.340927 | 5.363118 | -0.319352 | 6.676529 | 0.509851 | -7.072046 |
| 8 | 0.170586 | 2.859369 | -0.538605 | 5.315163 | 0.826149 | -5.785851 |
| 9 | -0.321877 | 1.547145 | -0.607222 | 2.414749 | -0.821818 | -2.307648 |
| 10 | -1.252411 | 2.049880 | -0.174482 | -3.629631 | -0.929407 | 3.323441 |
| 11 | 2.544807 | -1.431356 | 0.077776 | 2.937317 | 0.068855 | -3.113037 |
| 12 | 1.955353 | -1.538378 | -0.859109 | -0.119003 | 0.992937 | 4.018547 |
| 13 | -6.393723 | 0.366534 | 3.704914 | -3.591248 | -3.582922 | -3.663162 |
| 14 | -1.736153 | 0.482206 | 1.122250 | -0.222275 | -1.084145 | -2.834801 |
| 15 | -1.184280 | 2.288819 | -0.412212 | 2.496544 | -0.526989 | -1.682892 |
| 16 | 0.232773 | 2.186338 | -0.732599 | -2.939404 | -0.340164 | 3.782349 |
| 17 | 0.341843 | -1.835766 | -0.650601 | 6.829597 | 2.642296 | -6.299164 |

View File

@@ -0,0 +1,143 @@
# ControllerClient API 兼容逆向约束
> 记录时间:2026-04-24
> 适用仓库:`flyshot-replacement`
> 当前阶段:已落地 HTTP-only `ControllerClientCompat` 服务,并已将轨迹执行接入规划、触发时间轴和最小运行时骨架;不实现 `50001/TCP+JSON` 监听
## 1. 当前目标
本轮目标不是直接实现 `50001/TCP+JSON` 兼容网关,而是先把旧 `ControllerClient` 暴露的公开 API 做成可执行的逆向合同。
本轮交付物固定为两份文档:
- `docs/controller-client-api-compatibility-requirements.md`
- `docs/controller-client-api-reverse-engineering.md`
后续继续扩展 `Flyshot.ControllerClientCompat` 的方法覆盖、兼容测试矩阵或真实控制器联动时,必须以这两份文档为准,不再重新口头约定接口语义。
## 2. 范围边界
本轮只覆盖 `../FlyingShot/FlyingShot/Include/ControllerClient/ControllerClient.h` 中的 32 个公开方法。
分组如下:
- 传输与版本:`ConnectServer`、`GetServerVersion`、`GetClientVersion`
- 机器人初始化:`SetUpRobot`、`SetUpRobotFromEnv`、`IsSetUp`、`SetShowTCP`、`GetName`、`GetDoF`
- 控制器状态:`SetActiveController`、`Connect`、`Disconnect`、`EnableRobot`、`DisableRobot`、`StopMove`
- 参数与 IO:`GetSpeedRatio`、`SetSpeedRatio`、`GetTCP`、`SetTCP`、`GetIO`、`SetIO`
- 运动与求解:`GetJointPosition`、`GetPose`、`GetNearestIK`、`MoveJoint`、`ExecuteTrajectory`
- 飞拍轨迹:`UploadFlyShotTraj`、`DeleteFlyShotTraj`、`ListFlyShotTraj`、`ExecuteFlyShotTraj`、`SaveTrajInfo`、`IsFlyShotTrajValid`
明确不在本轮范围内:
- `ControllerServer` 内部所有未公开 `_Xxx` 方法的完整实现复原
- `50001` 网关代码、TCP server、JSON parser、命令路由实现
- 真机 `10010 / 10012 / 60015` 联调
- 抓包、hook、反汇编级协议完全坐实
- 把当前 replacement 仓库的实现约束误写成旧系统事实
## 3. 证据源优先级
逆向结论必须按以下优先级交叉确认:
1. `../FlyingShot/FlyingShot/Include/ControllerClient/ControllerClient.h`
2. `../FlyingShot/FlyingShot/Example/UseControllerClient.cpp`
3. `../FlyingShot/FlyingShot/Example/UseControllerClient.py`
4. `../FlyingShot/FlyingShot/Example/UseRealRobot.py`
5. `../FlyingShot/FlyingShot/Docs/用户手册/FANUC飞拍软件及SDK用户手册.md`
6. `../analysis/ControllerServer_analysis.md`
7. `../analysis/CommonMsg_protocol_analysis.md`
8. `../analysis/Trajectory_generation_algorithm_analysis.md`
9. `../FlyingShot/FlyingShot/Lib/libControllerClient.so` 与 `../FlyingShot/FlyingShot/Python/ControllerServer/ControllerServer.cpython-37m-x86_64-linux-gnu.so` 的字符串证据
使用规则:
- 头文件负责定义“公开合同”:方法名、参数名、默认值、返回类型。
- 示例和手册负责定义“典型调用方式”:调用顺序、Python 包装形态、用户侧常见用法。
- 逆向分析文档和二进制字符串负责定义“服务端映射”和“协议线索”:`_Xxx` 方法名、错误文本、命令名、部分字段顺序。
- 不能从当前 replacement 仓库的未来设计反推旧系统事实。
## 4. 文档填写规则
主归档文档 `docs/controller-client-api-reverse-engineering.md` 对每个 API 必须固定填写以下项目:
- C++ 公开签名
- Python 包装形态
- 服务端归属
- 协议 / 命令线索
- 返回值与默认值
- 典型工作流位置
- 证据来源
- 置信度
- 待确认点
填写约束:
- 只要精确 JSON 包结构还未恢复,就明确写成“命令名已知,JSON envelope 未恢复”。
- 只要 Python 失败路径没有样例,就不能假装已经确认失败时的返回形态。
- `GetServerVersion` 这类没有显式 `_GetServerVersion` 的接口,必须标成“协议分发层行为”,不能伪造一个服务端实现名。
- `ConnectServer` 与 `GetClientVersion` 必须明确标成客户端侧行为,不写进服务端命令表。
- 对 `GetJointPosition -> _GetJointPositions` 这种命名不一致,要单独注明。
- 当同一结论同时来自头文件、示例和字符串时,置信度可标为“高”;只有示例间接体现时,最多“中”。
## 5. 验收条件
本轮逆向归档完成时,至少满足:
- 32 个公开方法全部覆盖,且每个方法只出现一次
- 29 个服务端相关 API 有明确映射或明确写成协议分发层行为
- `ConnectServer`、`GetClientVersion` 两个客户端侧行为被明确排除在服务端命令表外
- 四条工作流附录完整:
- 初始化工作流
- 控制器状态工作流
- 普通轨迹工作流
- 飞拍轨迹工作流
- 已知高置信协议字段至少记录:
- `GetSpeedRatio`
- `SetSpeedRatio`
- `GetIO`
- `SetIO`
- 仍未恢复的部分必须进入“待确认问题”清单,不能被静默略过
## 6. 当前已确认摘要
当前已确认的高价值结论如下:
- `ControllerClient.h` 中共有 32 个公开方法。
- 其中 29 个方法可与 `ControllerServer` 的公开 `_Xxx` 方法一一对齐。
- `GetServerVersion` 能看到明确字符串证据,但未恢复到显式 `_GetServerVersion` 实现,更接近 `_ClientCB` / `_IsJsonValid` 所在的协议分发层。
- `ConnectServer` 是客户端建立到 `127.0.0.1:50001` 的传输层动作。
- `GetClientVersion` 更像客户端库自身版本查询,不进入服务端命令表。
- `GetSpeedRatio` / `SetSpeedRatio`、`GetIO` / `SetIO` 已有较高置信度的底层字段顺序与 `MsgID` 证据。
- `UploadFlyShotTraj`、`ListFlyShotTraj` 存在额外字符串线索:
- `StartUploadFlyShotTraj`
- `EndUploadFlyShotTraj`
- `GetNextListFlyShotTraj`
- `ExecuteFlyShotTraj`、`IsFlyShotTrajValid`、`SaveTrajInfo` 在工作流上有清晰分工,不能混成一个“执行轨迹”接口。
## 7. 下轮实现约束
当前 HTTP-only 兼容层已经可以承接公开 API 的主要服务端语义,并且 `ExecuteTrajectory` / `ExecuteFlyShotTraj` 已经进入 replacement 自身的规划与运行时链路。后续扩展必须遵守:
- 先以逆向合同建命令表,再写 `TCP + JSON` 入口
- 先做兼容测试矩阵,再补最小命令桩
- 区分“旧系统事实”和“replacement 当前策略”
- 真机未接通前,允许实现层返回稳定错误或模拟状态,但不能反过来污染逆向文档
- `50001/TCP+JSON` 抓包已经覆盖 `SetSpeedRatio` 与 `ExecuteFlyShotTraj(save_traj=true,use_cache=false)`,请求中没有显式 `JointLimits / acc_limit / jerk_limit / velocity / acceleration / jerk` 字段;因此规划限制的补齐必须作为 replacement-only 策略记录,不能写成旧公开 API 合同。
## 8. 当前 replacement 实现状态
以下内容是当前新实现的状态,不反推为旧系统事实:
- `Flyshot.ControllerClientCompat` 继续作为 HTTP 控制器后端兼容服务,不启动 `50001/TCP+JSON` 监听。
- `ExecuteTrajectory` 会先通过 `ICspPlanner` 规划普通轨迹,再把 `TrajectoryResult` 和最终关节位置交给 `IControllerRuntime`。
- `ExecuteFlyShotTraj` 会从上传轨迹目录取出轨迹,通过 `SelfAdaptIcspPlanner` 规划并用 `ShotTimelineBuilder` 生成 `ShotEvent` / `TrajectoryDoEvent`。
- HTTP 控制器已经按公开文档和抓包补齐 `ExecuteTrajectory(method, save_traj)` 与 `ExecuteFlyShotTraj(move_to_start, method, save_traj, use_cache, wait)` 参数,并继续兼容旧的裸 waypoint 数组和只传 `name` 的请求体。
- 规划阶段会继续消费旧配置中的 `acc_limit / jerk_limit`。如果现场需要复现旧服务端不可见的保守约束,replacement 设计上使用内部 `planning_acceleration_scale` 限制规划加速度;该字段不属于旧 `ControllerClient` 公开 API,也不会通过 `50001` JSON 下发。
- `method="icsp"` 与 `method="self-adapt-icsp"` 已接入当前规划器;`method="doubles"` 会被识别但返回显式未实现,不会静默降级成 ICSP。
- `Flyshot.Runtime.Fanuc.Protocol` 已经固化 `10010` 状态帧、`10012` 命令帧和 `60015` J519 数据包的基础编解码,并使用逆向抓包样本覆盖最小测试;`10010` 当前现场确认固定 90B。
- `Flyshot.Runtime.Fanuc` 已具备基础 Socket 客户端、程序启停、速度倍率/TCP/IO 参数命令和 J519 状态包驱动发送链路;稠密轨迹下发已按 `speed_ratio` 推进轨迹时间,并在收到机器人 UDP status 后按该 status sequence 回发命令。真实 R30iB 全流程现场联调仍需执行。
- 2026-04-28 `UTTC_MS11` 抓包确认 J519 命令目标为 `deg`、导出 `JointDetialTraj.txt` 为 `rad`,`speed_ratio=0.5/0.7/1.0` 分别形成 `1851/1322/926` 个主运行 J519 包;实际执行不发送 464 行导出点,而是在 8ms 实发周期上按 `t_traj = t_send * speed_ratio` 采样,包数为 `floor(duration / (0.008 * speed_ratio)) + 1`。
- 宿主已经提供只读 Web 状态页 `/status` 和状态快照 API `/api/status/snapshot`,用于查看兼容层初始化、机器人元数据和运行时快照。
- `MoveJoint` 仍保持旧兼容语义中的直接运动接口,但状态写入已经统一经过运行时,而不是由兼容服务自己维护关节数组。
- `GetNearestIK`、`SetUpRobotFromEnv` 当前已经暴露完整参数形状,但后端求解器 / 环境文件解析仍返回显式未实现。

View File

@@ -0,0 +1,562 @@
# ControllerClient API 全量逆向归档
> 记录时间:2026-04-24
> 适用仓库:`flyshot-replacement`
> 目标:为后续 `50001/TCP+JSON` 兼容网关实现提供只读合同
## 1. 总览
本次归档覆盖 `../FlyingShot/FlyingShot/Include/ControllerClient/ControllerClient.h` 中全部 32 个公开方法。
当前分类结果:
- 29 个 API 可与 `ControllerServer` 公开 `_Xxx` 方法一一映射
- 1 个 API 属于协议分发层行为:`GetServerVersion`
- 2 个 API 属于客户端侧行为:`ConnectServer`、`GetClientVersion`
本文档中的“协议 / 命令线索”有两个层次:
- 命令名:已从头文件、字符串或逆向文档坐实,例如 `SetUpRobot`、`GetIO`
- JSON envelope:请求 JSON 的精确键名、必填规则、整体结构;当前大多仍未完全恢复
因此,除 `GetSpeedRatio` / `SetSpeedRatio` / `GetIO` / `SetIO` 等已在命令通道层坐实字段顺序的接口外,其余接口默认只写“命令名已知,精确 JSON 包结构待确认”。
置信度定义:
- 高:头文件 + 示例/手册 + 字符串/逆向文档三方交叉确认
- 中:头文件 + 示例,或头文件 + 字符串,缺少第三方交叉
- 低:仅有间接线索,未达到本轮归档主结论标准
## 2. 传输与版本
### `ConnectServer`
- C++ 公开签名:`bool ConnectServer(const std::string &server_ip = "127.0.0.1", unsigned port = 50001);`
- Python 包装形态:`c.ConnectServer(server_ip="127.0.0.1", port=50001) -> bool`
- 服务端归属:客户端传输层行为,不进入 `ControllerServer._Xxx` 命令表
- 协议 / 命令线索:建立到 `127.0.0.1:50001` 的 TCP 连接;JSON 负载只发生在连接建立之后
- 返回值与默认值:默认 `server_ip="127.0.0.1"`、`port=50001`;成功返回 `true`
- 典型工作流位置:所有远程 API 的前置步骤
- 证据来源:`ControllerClient.h`、`UseControllerClient.cpp`、`UseControllerClient.py`、`UseRealRobot.py`
- 置信度:高
- 待确认点:重连、超时、握手细节未恢复
### `GetServerVersion`
- C++ 公开签名:`bool GetServerVersion(std::string &version);`
- Python 包装形态:样例中表现为 `controller.GetServerVersion() -> str`
- 服务端归属:协议分发层行为;当前未恢复到显式 `_GetServerVersion`
- 协议 / 命令线索:命令名字符串 `GetServerVersion` 已坐实;服务端存在 `GetServerVersion success: {}.` 日志;精确 JSON envelope 未恢复
- 返回值与默认值:C++ 为 `bool + out string`;Python 样例把结果当作直接字符串使用
- 典型工作流位置:`ConnectServer` 之后、机器人初始化前后均可读取的元信息接口
- 证据来源:`ControllerClient.h`、`UseControllerClient.py`、SDK 手册、`ControllerServer_analysis.md`、`libControllerClient.so`、`ControllerServer.cpython-37m-x86_64-linux-gnu.so`
- 置信度:高
- 待确认点:Python 失败路径的返回形态未看到样例;请求 JSON 的精确字段未恢复
### `GetClientVersion`
- C++ 公开签名:`bool GetClientVersion(std::string &version);`
- Python 包装形态:未见公开样例;从包装库字符串看接口存在
- 服务端归属:客户端本地行为,不进入服务端命令表
- 协议 / 命令线索:`libControllerClient.so` 与 `PyControllerClient` 都有 `GetClientVersion` 符号;未见 `ControllerServer` 对应 `_Xxx`
- 返回值与默认值:C++ 为 `bool + out string`;Python 侧返回形态待确认
- 典型工作流位置:客户端自检或 SDK 版本上报,不依赖服务端状态
- 证据来源:`ControllerClient.h`、`libControllerClient.so`、`PyControllerClient.cpython-37m-x86_64-linux-gnu.so`
- 置信度:高
- 待确认点:实际版本字符串来源与 Python 包装失败行为未恢复
## 3. 机器人初始化
### `SetUpRobot`
- C++ 公开签名:`bool SetUpRobot(const std::string &robot_name);`
- Python 包装形态:`c.SetUpRobot("FANUC_LR_Mate_200iD") -> bool`
- 服务端归属:`ControllerServer._SetUpRobot`
- 协议 / 命令线索:命令名 `SetUpRobot` 已坐实;参数高概率是 `robot_name`;精确 JSON envelope 未恢复
- 返回值与默认值:成功返回 `true`
- 典型工作流位置:创建客户端并连上 `50001` 后首先调用;旧说明明确它是最先执行的服务端初始化动作
- 证据来源:`ControllerClient.h`、`UseControllerClient.cpp`、`UseControllerClient.py`、SDK 手册、`ControllerServer_analysis.md`、二进制字符串
- 置信度:高
- 待确认点:请求字段键名是否为 `robot_name` 仍需抓包或 hook 坐实
### `SetUpRobotFromEnv`
- C++ 公开签名:`bool SetUpRobotFromEnv(const std::string &env_file);`
- Python 包装形态:未见公开样例;服务端与客户端字符串均存在
- 服务端归属:`ControllerServer._SetUpRobotFromEnv`
- 协议 / 命令线索:命令名 `SetUpRobotFromEnv` 已坐实;参数高概率是环境文件绝对路径;精确 JSON envelope 未恢复
- 返回值与默认值:成功返回 `true`
- 典型工作流位置:与 `SetUpRobot` 二选一;当现场通过环境文件而不是型号名初始化时使用
- 证据来源:`ControllerClient.h`、`ControllerServer_analysis.md`、`ControllerServer` 字符串、`libControllerClient.so`
- 置信度:高
- 待确认点:环境文件路径是否必须为绝对路径由注释可见,但服务端是否做额外归一化未恢复
### `IsSetUp`
- C++ 公开签名:`bool IsSetUp();`
- Python 包装形态:`c.IsSetUp() -> bool`
- 服务端归属:`ControllerServer._IsSetUp`
- 协议 / 命令线索:命令名 `IsSetUp` 已坐实;精确 JSON envelope 未恢复
- 返回值与默认值:直接返回布尔值
- 典型工作流位置:`SetUpRobot` 或 `SetUpRobotFromEnv` 之后的状态确认
- 证据来源:`ControllerClient.h`、`UseControllerClient.cpp`、`UseControllerClient.py`、`ControllerServer_analysis.md`、二进制字符串
- 置信度:高
- 待确认点:服务端返回是否仅受机器人对象存在性控制,还是还依赖模型/控制器更深层状态,当前未完全恢复
### `SetShowTCP`
- C++ 公开签名:`bool SetShowTCP(bool is_show = true, double axis_length = 0.1, size_t axis_size = 2);`
- Python 包装形态:`c.SetShowTCP(is_show=True, axis_length=0.1, axis_size=2) -> bool`
- 服务端归属:`ControllerServer._SetShowTCP`
- 协议 / 命令线索:命令名 `SetShowTCP` 已坐实;字符串中可见 `SetShowTCP is_show success/failed`;精确 JSON envelope 未恢复
- 返回值与默认值:默认 `is_show=true`、`axis_length=0.1`、`axis_size=2`
- 典型工作流位置:初始化完成后、切换控制器前的仿真显示配置
- 证据来源:`ControllerClient.h`、`UseControllerClient.cpp`、`UseControllerClient.py`、`ControllerServer_analysis.md`、`ControllerServer` 字符串
- 置信度:高
- 待确认点:仅仿真控制器生效还是真实控制器也接受该命令,当前未见明确负例
### `GetName`
- C++ 公开签名:`std::string GetName();`
- Python 包装形态:`c.GetName() -> str`
- 服务端归属:`ControllerServer._GetName`
- 协议 / 命令线索:命令名 `GetName` 已坐实;精确 JSON envelope 未恢复
- 返回值与默认值:直接返回机器人名称字符串
- 典型工作流位置:完成机器人初始化后读取名称确认
- 证据来源:`ControllerClient.h`、`UseControllerClient.py`、SDK 手册、`ControllerServer_analysis.md`、二进制字符串
- 置信度:高
- 待确认点:失败时是返回空串还是异常/默认值,Python 样例未覆盖
### `GetDoF`
- C++ 公开签名:`int GetDoF();`
- Python 包装形态:`c.GetDoF() -> int`
- 服务端归属:`ControllerServer._GetDoF`
- 协议 / 命令线索:命令名 `GetDoF` 已坐实;精确 JSON envelope 未恢复
- 返回值与默认值:直接返回自由度整数
- 典型工作流位置:完成机器人初始化后读取自由度
- 证据来源:`ControllerClient.h`、`UseControllerClient.py`、SDK 手册、`ControllerServer_analysis.md`、二进制字符串
- 置信度:高
- 待确认点:失败时的客户端行为未恢复
## 4. 控制器状态
### `SetActiveController`
- C++ 公开签名:`bool SetActiveController(bool sim = true);`
- Python 包装形态:`c.SetActiveController(sim=True) -> bool`;真机样例使用 `sim=False`
- 服务端归属:`ControllerServer._SetActiveController`
- 协议 / 命令线索:命令名 `SetActiveController` 已坐实;字符串中可见 `SetActiveController sim success/failed`
- 返回值与默认值:默认 `sim=true`
- 典型工作流位置:机器人初始化后、`Connect` 前,用于在仿真控制器和真实控制器之间切换
- 证据来源:`ControllerClient.h`、`UseControllerClient.py`、`UseRealRobot.py`、SDK 手册、`ControllerServer_analysis.md`、二进制字符串
- 置信度:高
- 待确认点:切换控制器时是否会隐式断开旧控制器,当前只有 `_DisconnectAll` 的间接线索
### `Connect`
- C++ 公开签名:`bool Connect(const std::string &robot_ip);`
- Python 包装形态:`c.Connect("192.168.10.101") -> bool`
- 服务端归属:`ControllerServer._Connect`
- 协议 / 命令线索:命令名 `Connect` 已坐实;字符串中可见 `Connect ip success/failed`
- 返回值与默认值:成功返回 `true`
- 典型工作流位置:选定活动控制器后,通知服务端连接真实机器人 IP
- 证据来源:`ControllerClient.h`、`UseControllerClient.cpp`、`UseControllerClient.py`、`UseRealRobot.py`、SDK 手册、`ControllerServer_analysis.md`、`FANUC_realtime_comm_analysis.md`、二进制字符串
- 置信度:高
- 待确认点:JSON 键名是否为 `ip` 或 `robot_ip` 未恢复;仿真模式下是否接受同一命令仍需实现侧验证
### `Disconnect`
- C++ 公开签名:`bool Disconnect();`
- Python 包装形态:`c.Disconnect() -> bool`
- 服务端归属:`ControllerServer._Disconnect`
- 协议 / 命令线索:命令名 `Disconnect` 已坐实;字符串中可见 `Disconnect success/failed`
- 返回值与默认值:直接返回布尔值
- 典型工作流位置:真实控制器断开或工作流结束时调用
- 证据来源:`ControllerClient.h`、`ControllerServer_analysis.md`、`ControllerServer` 字符串、`libControllerClient.so`
- 置信度:高
- 待确认点:是否同步清理状态通道 / 命令通道 / 伺服通道,当前只在 FANUC 分析文档看到对象链,并未完全坐实释放顺序
### `EnableRobot`
- C++ 公开签名:`bool EnableRobot(unsigned buffer_size = 2);`
- Python 包装形态:`c.EnableRobot() -> bool`
- 服务端归属:`ControllerServer._EnableRobot`
- 协议 / 命令线索:命令名 `EnableRobot` 已坐实;字符串中可见 `EnableRobot success/failed`
- 返回值与默认值:默认 `buffer_size=2`
- 典型工作流位置:`Connect` 后使能机器人;多数运动 API 前置条件
- 证据来源:`ControllerClient.h`、`UseControllerClient.cpp`、`UseControllerClient.py`、`UseRealRobot.py`、`ControllerServer_analysis.md`、`FANUC_realtime_comm_analysis.md`、二进制字符串
- 置信度:高
- 待确认点:`buffer_size` 在旧服务端具体如何映射到伺服缓冲区参数,当前未完全恢复
### `DisableRobot`
- C++ 公开签名:`bool DisableRobot();`
- Python 包装形态:`c.DisableRobot() -> bool`
- 服务端归属:`ControllerServer._DisableRobot`
- 协议 / 命令线索:命令名 `DisableRobot` 已坐实;字符串中可见 `DisableRobot success/failed`
- 返回值与默认值:直接返回布尔值
- 典型工作流位置:停机或退出前关闭机器人使能
- 证据来源:`ControllerClient.h`、`ControllerServer_analysis.md`、`FANUC_realtime_comm_analysis.md`、二进制字符串
- 置信度:高
- 待确认点:与 `StopMove` 的先后顺序约束未恢复
### `StopMove`
- C++ 公开签名:`bool StopMove();`
- Python 包装形态:`c.StopMove() -> bool`
- 服务端归属:`ControllerServer._StopMove`
- 协议 / 命令线索:命令名 `StopMove` 已坐实;字符串中可见 `StopMove success/failed`
- 返回值与默认值:直接返回布尔值
- 典型工作流位置:当前运动中止;Python 示例中调用后会再次 `EnableRobot()`
- 证据来源:`ControllerClient.h`、`UseControllerClient.py`、`ControllerServer_analysis.md`、`FANUC_realtime_comm_analysis.md`、二进制字符串
- 置信度:高
- 待确认点:是否会清空控制器执行队列、是否必须重新使能,当前只从示例看到倾向性而非硬约束
## 5. 参数与 IO
### `GetSpeedRatio`
- C++ 公开签名:`double GetSpeedRatio();`
- Python 包装形态:`c.GetSpeedRatio() -> float`
- 服务端归属:`ControllerServer._GetSpeedRatio`
- 协议 / 命令线索:命令通道 `MsgID = 0x2206`;响应字段为 `ratio_int` 与 `result_code`;成功后客户端将 `ratio_int / 100.0`
- 返回值与默认值:直接返回 `0~1` 之间的倍率
- 典型工作流位置:连机并使能后读取当前速度倍率
- 证据来源:`ControllerClient.h`、`UseControllerClient.cpp`、`UseControllerClient.py`、SDK 手册、`ControllerServer_analysis.md`、`CommonMsg_protocol_analysis.md`
- 置信度:高
- 待确认点:服务端 JSON 层到命令通道层的参数桥接结构未恢复
### `SetSpeedRatio`
- C++ 公开签名:`bool SetSpeedRatio(double ratio);`
- Python 包装形态:`c.SetSpeedRatio(0.8) -> bool`
- 服务端归属:`ControllerServer._SetSpeedRatio`
- 协议 / 命令线索:命令通道 `MsgID = 0x2207`;请求字段为 `ratio_int_0_100`;输入 `double` 先乘 `100` 并夹到 `[0,100]`
- 返回值与默认值:成功返回 `true`
- 典型工作流位置:连机并使能后修改控制器速度倍率
- 证据来源:`ControllerClient.h`、`UseControllerClient.py`、`UseRealRobot.py`、SDK 手册、`ControllerServer_analysis.md`、`CommonMsg_protocol_analysis.md`、二进制字符串
- 置信度:高
- 待确认点:ratio 越界时客户端是裁剪还是直接失败,当前更偏向裁剪,但缺少公开负例
### `GetTCP`
- C++ 公开签名:`bool GetTCP(Pose &tcp);`
- Python 包装形态:`res, tcp = c.GetTCP()`
- 服务端归属:`ControllerServer._GetTCP`
- 协议 / 命令线索:控制器命令通道底层接口 `GetTCP` 已有 `MsgID = 0x2200` 证据,但 `ControllerClient` JSON 层请求结构未完全恢复
- 返回值与默认值:C++ 为 `bool + out Pose`;Python 为 `(bool, Pose)`
- 典型工作流位置:连机后读取当前控制器 TCP
- 证据来源:`ControllerClient.h`、`UseControllerClient.py`、`ControllerServer_analysis.md`、`CommonMsg_protocol_analysis.md`、二进制字符串
- 置信度:高
- 待确认点:JSON 层是否支持多个 TCP ID,目前只看到底层命令通道有 `tcp_id`
### `SetTCP`
- C++ 公开签名:`bool SetTCP(const Pose &tcp);`
- Python 包装形态:`c.SetTCP(tcp) -> bool`
- 服务端归属:`ControllerServer._SetTCP`
- 协议 / 命令线索:控制器命令通道底层接口 `SetTCP` 已有 `MsgID = 0x2201` 证据;JSON 层精确字段未恢复
- 返回值与默认值:成功返回 `true`
- 典型工作流位置:连机后修改控制器 TCP
- 证据来源:`ControllerClient.h`、`UseControllerClient.py`、`ControllerServer_analysis.md`、`CommonMsg_protocol_analysis.md`、`FANUC_realtime_comm_analysis.md`、二进制字符串
- 置信度:高
- 待确认点:JSON 层是否也暴露 `tcp_id` 概念尚未恢复
### `GetIO`
- C++ 公开签名:`bool GetIO(unsigned port, bool &value, IOType type = kIOTypeDI);`
- Python 包装形态:`res, value = c.GetIO(port=1, io_type=IOType.kIOTypeDI)`
- 服务端归属:`ControllerServer._GetIO`
- 协议 / 命令线索:命令通道 `MsgID = 0x2208`;请求字段顺序是 `io_type`、`io_index`;响应字段是 `result_code`、`io_value`
- 返回值与默认值:默认 `type = kIOTypeDI`;C++ 为 `bool + out bool`;Python 为 `(bool, bool)`
- 典型工作流位置:连机后读取 DI/DO/RI/RO 等 IO 值
- 证据来源:`ControllerClient.h`、`UseControllerClient.py`、SDK 手册、`ControllerServer_analysis.md`、`CommonMsg_protocol_analysis.md`、二进制字符串
- 置信度:高
- 待确认点:高层 JSON 是否把字段命名为 `port` / `value` / `type`,还是 `io_index` / `io_type`,当前未抓到完整包
### `SetIO`
- C++ 公开签名:`bool SetIO(unsigned port, bool value, IOType type = kIOTypeDO);`
- Python 包装形态:`c.SetIO(port=1, value=True, io_type=IOType.kIOTypeDO) -> bool`
- 服务端归属:`ControllerServer._SetIO`
- 协议 / 命令线索:命令通道 `MsgID = 0x2209`;请求字段顺序是 `io_type`、`io_index`、`io_value`
- 返回值与默认值:默认 `type = kIOTypeDO`
- 典型工作流位置:连机后设置数字输出;飞拍链路之外的普通 IO 调试也会用到
- 证据来源:`ControllerClient.h`、`UseControllerClient.py`、SDK 手册、`ControllerServer_analysis.md`、`CommonMsg_protocol_analysis.md`、`FANUC_realtime_comm_analysis.md`、二进制字符串
- 置信度:高
- 待确认点:布尔值在高层 JSON 中是否以 `true/false` 传输、在命令通道中以 `float` 传输,当前只坐实了命令通道层
## 6. 运动与求解
### `GetJointPosition`
- C++ 公开签名:`bool GetJointPosition(JointPositions &joint_position);`
- Python 包装形态:`res, joints = c.GetJointPosition()`
- 服务端归属:`ControllerServer._GetJointPositions`
- 协议 / 命令线索:命令名 `GetJointPositions` 已坐实;客户端公开 API 名与服务端方法名存在单复数差异
- 返回值与默认值:C++ 为 `bool + out JointPositions`;Python 为 `(bool, JointPositions)`
- 典型工作流位置:读取当前关节角,常作为 `GetNearestIK` 的 seed 或 `MoveJoint` 的基准
- 证据来源:`ControllerClient.h`、`UseControllerClient.cpp`、`UseControllerClient.py`、SDK 手册、`ControllerServer_analysis.md`、二进制字符串
- 置信度:高
- 待确认点:JSON 层返回数组字段键名未恢复
### `GetPose`
- C++ 公开签名:`bool GetPose(Pose &pose);`
- Python 包装形态:`res, pose = c.GetPose()`
- 服务端归属:`ControllerServer._GetPose`
- 协议 / 命令线索:命令名 `GetPose` 已坐实;字符串中可见 `GetPose success/failed`
- 返回值与默认值:C++ 为 `bool + out Pose`;Python 为 `(bool, Pose)`
- 典型工作流位置:读取当前 TCP 位姿,常与 `GetNearestIK` 配套使用
- 证据来源:`ControllerClient.h`、`UseControllerClient.py`、SDK 手册、`ControllerServer_analysis.md`、二进制字符串
- 置信度:高
- 待确认点:位姿坐标系定义虽然在 `Types.h` 中体现为 7 元数组,但 JSON 层字段结构未恢复
### `GetNearestIK`
- C++ 公开签名:`bool GetNearestIK(const Pose &pose, const JointPositions &seed, JointPositions &ik);`
- Python 包装形态:`res, ik = c.GetNearestIK(pose, joint_seed=joints)`
- 服务端归属:`ControllerServer._GetNearestIK`
- 协议 / 命令线索:命令名 `GetNearestIK` 已坐实;字符串中可见 `GetNearestIK success/failed`
- 返回值与默认值:C++ 为 `bool + out JointPositions`;Python 为 `(bool, JointPositions)`
- 典型工作流位置:先 `GetPose` 得到当前位姿,再构造目标位姿,并使用当前关节或邻近关节作为 seed 求最近 IK
- 证据来源:`ControllerClient.h`、`UseControllerClient.py`、`ControllerServer_analysis.md`、二进制字符串
- 置信度:高
- 待确认点:JSON 层中 seed 参数键名在 Python 里表现为 `joint_seed`,但 C++ 注释写的是 `seed`;高层字段命名仍待确认
### `MoveJoint`
- C++ 公开签名:`bool MoveJoint(const JointPositions &joint_position);`
- Python 包装形态:`c.MoveJoint(home_joint) -> bool`
- 服务端归属:`ControllerServer._MoveJoint`
- 协议 / 命令线索:命令名 `MoveJoint` 已坐实;字符串中可见 `MoveJoint waypoint success/failed`
- 返回值与默认值:成功返回 `true`
- 典型工作流位置:点到点回零或移动到飞拍轨迹起点
- 证据来源:`ControllerClient.h`、`UseControllerClient.cpp`、`UseControllerClient.py`、`ControllerServer_analysis.md`、二进制字符串
- 置信度:高
- 待确认点:移动速度/平滑参数是否固定在服务端内部,公开 API 未暴露
### `ExecuteTrajectory`
- C++ 公开签名:`bool ExecuteTrajectory(const std::vector<JointPositions> &waypoints, const std::string &method = "icsp", bool save_traj = false);`
- Python 包装形态:`c.ExecuteTrajectory(waypoints=[...], method="icsp", save_traj=True) -> bool`
- 服务端归属:`ControllerServer._ExecuteTrajectory`
- 协议 / 命令线索:命令名 `ExecuteTrajectory` 已坐实;`method` 至少支持 `icsp` 与 `doubles`
- 返回值与默认值:默认 `method="icsp"`、`save_traj=false`
- 典型工作流位置:普通多点轨迹执行,不带飞拍 IO;输入是关节空间稀疏 waypoint,而不是笛卡尔点
- 证据来源:`ControllerClient.h`、`UseControllerClient.py`、SDK 手册、`ControllerServer_analysis.md`、`Trajectory_generation_algorithm_analysis.md`、二进制字符串
- 置信度:高
- 待确认点:JSON 中 waypoint 列表的键名和序列化结构未恢复;`doubles` 的失败语义未在公开样例中体现
## 7. 飞拍轨迹
### `UploadFlyShotTraj`
- C++ 公开签名:`bool UploadFlyShotTraj(const std::string &name, const std::vector<JointPositions> &waypoints, const std::vector<bool> &shot_flags, const std::vector<int> &offset_values, const std::vector<std::vector<int>> &addrs);`
- Python 包装形态:`c.UploadFlyShotTraj(name="test_traj", waypoints=..., shot_flags=..., offset_values=..., addrs=...) -> bool`
- 服务端归属:`ControllerServer._UploadFlyShotTraj`
- 协议 / 命令线索:命令名 `UploadFlyShotTraj` 已坐实;客户端字符串中可见 `StartUploadFlyShotTraj` 与 `EndUploadFlyShotTraj`,说明上传阶段很可能分为开始 / 传输 / 结束三个子步骤
- 返回值与默认值:成功返回 `true`;无默认参数
- 典型工作流位置:运行时动态构造飞拍轨迹时,先把轨迹定义登记到服务端
- 证据来源:`ControllerClient.h`、`UseControllerClient.py`、SDK 手册、`ControllerServer_analysis.md`、`Trajectory_generation_algorithm_analysis.md`、`libControllerClient.so`、`ControllerServer` 字符串
- 置信度:高
- 待确认点:JSON 上传是否一次性发送完整 payload,还是像字符串暗示的那样分片上传,当前未完全恢复
### `DeleteFlyShotTraj`
- C++ 公开签名:`bool DeleteFlyShotTraj(const std::string &name);`
- Python 包装形态:`c.DeleteFlyShotTraj(name="test_traj") -> bool`
- 服务端归属:`ControllerServer._DeleteFlyShotTraj`
- 协议 / 命令线索:命令名 `DeleteFlyShotTraj` 已坐实;字符串中可见 `DeleteFlyShotTraj {} success/failed`
- 返回值与默认值:成功返回 `true`
- 典型工作流位置:删除已上传的临时飞拍轨迹定义
- 证据来源:`ControllerClient.h`、`UseControllerClient.py`、SDK 手册、`ControllerServer_analysis.md`、`PyControllerClient` 字符串、`ControllerServer` 字符串
- 置信度:高
- 待确认点:删除配置内固有轨迹名与删除运行时上传轨迹时是否走同一条路径,当前未恢复
### `ListFlyShotTraj`
- C++ 公开签名:`std::vector<std::string> ListFlyShotTraj();`
- Python 包装形态:`c.ListFlyShotTraj() -> list[str]`
- 服务端归属:`ControllerServer._ListFlyShotTraj`
- 协议 / 命令线索:命令名 `ListFlyShotTraj` 已坐实;客户端与服务端字符串都出现 `GetNextListFlyShotTraj`,说明底层列举很可能是迭代式获取,而不是一次性返回整个数组
- 返回值与默认值:直接返回轨迹名称列表
- 典型工作流位置:查看当前服务端已登记的飞拍轨迹
- 证据来源:`ControllerClient.h`、`UseControllerClient.py`、`ControllerServer_analysis.md`、`libControllerClient.so`、`ControllerServer` 字符串
- 置信度:高
- 待确认点:高层 JSON 是否真的直接返回数组,还是客户端内部循环拉取后再拼成数组,当前未完全恢复
### `ExecuteFlyShotTraj`
- C++ 公开签名:`bool ExecuteFlyShotTraj(const std::string &name, const bool move_to_start = false, const std::string &method = "icsp", bool save_traj = false, bool use_cache = false);`
- Python 包装形态:`c.ExecuteFlyShotTraj(name="002", move_to_start=True, method="icsp", save_traj=True, use_cache=False) -> bool`
- 服务端归属:`ControllerServer._ExecuteFlyShotTraj`
- 协议 / 命令线索:命令名 `ExecuteFlyShotTraj` 已坐实;字符串中出现 `move_to_start`、`use_cache`;伪代码级逆向已恢复其主流程
- 返回值与默认值:默认 `move_to_start=false`、`method="icsp"`、`save_traj=false`、`use_cache=false`
- 典型工作流位置:对已存在的飞拍轨迹定义执行“生成 + 挂接 DO 时间轴 + 执行”
- 证据来源:`ControllerClient.h`、`UseControllerClient.cpp`、`UseControllerClient.py`、`UseRealRobot.py`、SDK 手册、`ControllerServer_analysis.md`、`Trajectory_generation_algorithm_analysis.md`、二进制字符串
- 置信度:高
- 待确认点:JSON 包结构未恢复;`use_cache` 的缓存键是否只按 `name`,当前从伪代码判断是,但未见更底层证据
### `SaveTrajInfo`
- C++ 公开签名:`bool SaveTrajInfo(const std::string &name, const std::string &method = "icsp");`
- Python 包装形态:未见运行样例,但包装库和服务端都有明确字符串;语义可由手册和分析文档确认
- 服务端归属:`ControllerServer._SaveTrajInfo`
- 协议 / 命令线索:命令名 `SaveTrajInfo` 已坐实;字符串中可见 `SaveTrajInfo {} success/failed`
- 返回值与默认值:默认 `method="icsp"`;`self-adapt-icsp` 在执行时保存分析文件会回落成 `icsp` 导出语义
- 典型工作流位置:按给定 `name + method` 生成并导出轨迹分析文件,例如 `JointTraj.txt`、`CartDetialTraj.txt`
- 证据来源:`ControllerClient.h`、`ControllerServer_analysis.md`、`Trajectory_generation_algorithm_analysis.md`、二进制字符串
- 置信度:高
- 待确认点:Python 包装是否直接暴露该方法并返回 `bool`,虽然字符串存在,但公开示例未调用
### `IsFlyShotTrajValid`
- C++ 公开签名:`bool IsFlyShotTrajValid(double &time, const std::string &name, const std::string &method = "icsp", bool save_traj = false);`
- Python 包装形态:`valid, time_sec = c.IsFlyShotTrajValid("EOL9_EAU_90", "icsp", save_traj=True)`
- 服务端归属:`ControllerServer._IsFlyShotTrajValid`
- 协议 / 命令线索:命令名 `IsFlyShotTrajValid` 已坐实;作用是“生成 + 合法性检查 + 返回轨迹总时长”
- 返回值与默认值:默认 `method="icsp"`、`save_traj=false`;C++ 为 `bool + out double`;Python 为 `(bool, float)`
- 典型工作流位置:在执行飞拍轨迹前预校验;也可与 `save_traj=True` 结合导出分析文件
- 证据来源:`ControllerClient.h`、`UseControllerClient.py` 中的注释样例、`ControllerServer_analysis.md`、`Trajectory_generation_algorithm_analysis.md`、二进制字符串
- 置信度:高
- 待确认点:`method="self-adapt-icsp"` 是否被该接口完整支持,分析文档显示公开手册更强调 `icsp` / `doubles`,而示例注释里又出现 `self-adapt-icsp`,这里仍需后续抓包或真环境验证
## 8. 四条旧 SDK 工作流
### 8.1 初始化工作流
```text
ControllerClient()
-> ConnectServer("127.0.0.1", 50001)
-> SetUpRobot("FANUC_LR_Mate_200iD") 或 SetUpRobotFromEnv(...)
-> IsSetUp()
-> GetName()
-> GetDoF()
-> SetShowTCP(...)
```
说明:
- 这是最常见的仿真或真机前置流程。
- `SetUpRobot` 是旧说明里明确要求首先执行的服务端初始化动作。
### 8.2 控制器状态工作流
```text
SetActiveController(sim=True/False)
-> Connect(robot_ip)
-> EnableRobot(buffer_size=2)
-> GetSpeedRatio() / SetSpeedRatio(...)
-> GetTCP() / SetTCP(...)
-> GetIO() / SetIO(...)
-> StopMove()
-> DisableRobot()
-> Disconnect()
```
说明:
- 真机样例会使用 `sim=False`。
- `StopMove()` 之后,Python 示例里会再次 `EnableRobot()`,说明停止运动后常伴随重新使能。
### 8.3 普通轨迹工作流
```text
GetJointPosition()
-> GetPose()
-> GetNearestIK(pose, seed)
-> MoveJoint(home_joint)
-> ExecuteTrajectory(waypoints, method="icsp", save_traj=True)
```
说明:
- 输入是关节空间稀疏 waypoint。
- `ExecuteTrajectory` 至少支持 `icsp` 与 `doubles`。
### 8.4 飞拍轨迹工作流
#### 方案 A:轨迹名已在配置中存在
```text
name
-> IsFlyShotTrajValid(name, "icsp", save_traj=True)
-> ExecuteFlyShotTraj(name, move_to_start=True, method="icsp", save_traj=True, use_cache=False)
-> SaveTrajInfo(name, "icsp")
```
#### 方案 B:客户端动态上传飞拍轨迹
```text
UploadFlyShotTraj(name, waypoints, shot_flags, offset_values, addrs)
-> ListFlyShotTraj()
-> IsFlyShotTrajValid(name, "icsp", save_traj=True)
-> ExecuteFlyShotTraj(name, move_to_start=True, method="self-adapt-icsp", save_traj=True, use_cache=True)
-> DeleteFlyShotTraj(name)
```
说明:
- `UploadFlyShotTraj` 只负责登记轨迹定义。
- `IsFlyShotTrajValid` 负责“生成 + 合法性检查”。
- `ExecuteFlyShotTraj` 负责“生成 + 挂接 `TrajectoryDO` + 执行”。
- `SaveTrajInfo` 负责导出分析文件。
## 9. 已知高置信协议线索
当前能直接用于后续实现的高置信协议结论如下:
- 传输层为 `TCP + JSON` 风格协议,不是 HTTP/gRPC。
- JSON 层至少存在 `method` 字段,服务端存在 `_ClientCB` 与 `_IsJsonValid`。
- 命令通道字段已确认:
- `GetSpeedRatio`:`MsgID = 0x2206`
- `SetSpeedRatio`:`MsgID = 0x2207`
- `GetIO`:`MsgID = 0x2208`
- `SetIO`:`MsgID = 0x2209`
- 2026-04-28 `UTTC_MS11` 抓包中,`speed_ratio=0.7` 的效果能从 UDP 60015 主运行段时间尺度反推出来,但机器人侧 `TCP 10012` 未出现 `0x2207 SetSpeedRatio`;兼容实现不能只依赖一次 10012 命令来表达执行倍率,还要在 J519 发送时间轴上应用当前倍率。J519 实发时间为 `t_send = k * 0.008`,原轨迹采样时间为 `t_traj = t_send * speed_ratio`,包数为 `floor(duration / (0.008 * speed_ratio)) + 1`。
- 飞拍轨迹相关额外字符串线索:
- `StartUploadFlyShotTraj`
- `EndUploadFlyShotTraj`
- `GetNextListFlyShotTraj`
- `move_to_start`
- `use_cache`
- `shot_flags`
- `offset_values`
- `addr` / `addrs`
## 10. 待确认问题
以下问题本轮故意保留,不冒充已确认结论:
1. `50001/TCP+JSON` 请求 JSON 的精确 envelope 结构、字段必填规则、响应包统一格式。
2. `GetServerVersion` 在高层 JSON 中的完整请求 / 响应字段。
3. `GetClientVersion` 的实际版本字符串来源,以及 Python 包装失败路径。
4. `ListFlyShotTraj` 是高层一次性返回数组,还是客户端内部循环 `GetNextListFlyShotTraj` 后再拼装列表。
5. `UploadFlyShotTraj` 是否采用开始 / 数据 / 结束的多阶段上传协议。
6. `IsFlyShotTrajValid` 对 `self-adapt-icsp` 的真实支持边界。
7. `SetTCP` / `GetTCP` 在高层 JSON 中是否暴露 `tcp_id` 概念。
8. `SetActiveController` 切换控制器时是否会隐式触发 `_DisconnectAll`。
## 11. 后续实现使用方式
等继续扩展 `Flyshot.ControllerClientCompat` 时,建议按以下顺序使用本文档:
1. 先把 32 个 API 按本文档拆成命令表。
2. 先实现高置信、状态简单的接口:
- `GetServerVersion`
- `SetUpRobot`
- `IsSetUp`
- `GetName`
- `GetDoF`
- `GetSpeedRatio`
- `SetSpeedRatio`
- `GetIO`
- `SetIO`
3. 再实现返回复杂结构的接口:
- `GetTCP`
- `GetJointPosition`
- `GetPose`
- `GetNearestIK`
4. 最后实现飞拍轨迹相关接口,并把本文档中的“待确认问题”逐项收敛成兼容测试。

View File

@@ -0,0 +1,165 @@
# FANUC TCP 10010 状态帧字段说明
本文档整理当前现场真实抓包中 `TCP 10010` 状态通道的已确认字段布局,并明确哪些结论已经由抓包和代码验证,哪些仍然只是工作假设。
读取时间:2026-05-03
## 1. 结论范围
本文基于以下证据整理:
- `../analysis/UTTC_20260428_packet_validation.md`
- `../analysis/J519_stream_motion_analysis.md`
- `Rvbust/uttc-20260428/20260428.pcap`
- `src/Flyshot.Runtime.Fanuc/Protocol/FanucStateProtocol.cs`
- `tests/Flyshot.Core.Tests/FanucProtocolTests.cs`
当前结论仅覆盖现场已确认的 `R30iB + RVBUSTSM` 这一路状态通道行为,不提前推广为所有 FANUC 机型或所有旧版本协议的通用结论。
## 2. 通道性质
真实抓包显示,`TCP 10010` 是控制柜到上位机的单向状态流:
- 上位机先主动建立 TCP 连接。
- 建连后,带应用层 payload 的业务包全部来自 `192.168.10.11:10010 -> 192.168.10.10:41726`。
- 上位机在该通道上只回 TCP `ACK`,没有观察到应用层请求体。
因此当前实现应把 `10010` 当作“持续推送的固定长度状态帧”处理,而不是像 `TCP 10012` 那样按请求/响应语义建模。
## 3. 整体布局
当前现场抓包确认,状态帧固定为 `90B`:
```text
doz 3 bytes
length u32 = 90
msg_id u32
pose[6] f32
joint_or_ext[9] f32
tail[4] u32
zod 3 bytes
```
其中:
- 帧头 magic 固定为 `doz`
- 帧尾 magic 固定为 `zod`
- 长度字段固定为 `0x5a`,即 `90`
- 当前全量抓包中 `msg_id` 恒为 `0`
- `tail[4]` 当前全量抓包中恒为 `(2, 0, 0, 1)`
## 4. 样例帧
以下样例帧来自 `20260428.pcap` 中首条 `tcp.port == 10010 && tcp.len > 0` 的 payload:
```text
646f7a0000005a000000004388a23243f1ed7f43e9de6bc265031ec2b33cc3c278e0153f8742f53c3f128dbc929529bc7861d63cb0184c3c1ca1a7000000000000000000000000000000020000000000000000000000017a6f64
```
对应解析值:
- `pose[6]`
- `273.26715`
- `483.85544`
- `467.73764`
- `-57.253044`
- `-89.618675`
- `-62.21883`
- `joint_or_ext[9]`
- `1.0567309`
- `0.011662138`
- `-0.01789339`
- `-0.015160045`
- `0.02149596`
- `0.009560025`
- `0`
- `0`
- `0`
- `tail[4]`
- `2`
- `0`
- `0`
- `1`
## 5. 正式字段表
| 偏移 | 长度 | 类型 | 样例值(hex) | 样例值(解析后) | 当前推断含义 |
| --- | --- | --- | --- | --- | --- |
| `0x00` | `3` | `char[3]` | `64 6f 7a` | `"doz"` | 固定帧头 magic |
| `0x03` | `4` | `u32 be` | `00 00 00 5a` | `90` | 帧总长度 |
| `0x07` | `4` | `u32 be` | `00 00 00 00` | `0` | `msg_id`,当前抓包全为 `0` |
| `0x0B` | `4` | `f32 be` | `43 88 a2 32` | `273.26715` | `pose[0]`,推断为 TCP `X(mm)` |
| `0x0F` | `4` | `f32 be` | `43 f1 ed 7f` | `483.85544` | `pose[1]`,推断为 TCP `Y(mm)` |
| `0x13` | `4` | `f32 be` | `43 e9 de 6b` | `467.73764` | `pose[2]`,推断为 TCP `Z(mm)` |
| `0x17` | `4` | `f32 be` | `c2 65 03 1e` | `-57.253044` | `pose[3]`,推断为姿态角 `W(deg)` |
| `0x1B` | `4` | `f32 be` | `c2 b3 3c c3` | `-89.618675` | `pose[4]`,推断为姿态角 `P(deg)` |
| `0x1F` | `4` | `f32 be` | `c2 78 e0 15` | `-62.21883` | `pose[5]`,推断为姿态角 `R(deg)` |
| `0x23` | `4` | `f32 be` | `3f 87 42 f5` | `1.0567309` | `joint_or_ext[0]`,推断为 `J1(rad)` |
| `0x27` | `4` | `f32 be` | `3c 3f 12 8d` | `0.011662138` | `joint_or_ext[1]`,推断为 `J2(rad)` |
| `0x2B` | `4` | `f32 be` | `bc 92 95 29` | `-0.01789339` | `joint_or_ext[2]`,推断为 `J3(rad)` |
| `0x2F` | `4` | `f32 be` | `bc 78 61 d6` | `-0.015160045` | `joint_or_ext[3]`,推断为 `J4(rad)` |
| `0x33` | `4` | `f32 be` | `3c b0 18 4c` | `0.02149596` | `joint_or_ext[4]`,推断为 `J5(rad)` |
| `0x37` | `4` | `f32 be` | `3c 1c a1 a7` | `0.009560025` | `joint_or_ext[5]`,推断为 `J6(rad)` |
| `0x3B` | `4` | `f32 be` | `00 00 00 00` | `0` | `joint_or_ext[6]`,扩展轴槽位,当前样本恒 `0` |
| `0x3F` | `4` | `f32 be` | `00 00 00 00` | `0` | `joint_or_ext[7]`,扩展轴槽位,当前样本恒 `0` |
| `0x43` | `4` | `f32 be` | `00 00 00 00` | `0` | `joint_or_ext[8]`,扩展轴槽位,当前样本恒 `0` |
| `0x47` | `4` | `u32 be` | `00 00 00 02` | `2` | `tail[0]`,诊断状态字,物理语义未坐实 |
| `0x4B` | `4` | `u32 be` | `00 00 00 00` | `0` | `tail[1]`,诊断状态字,物理语义未坐实 |
| `0x4F` | `4` | `u32 be` | `00 00 00 00` | `0` | `tail[2]`,诊断状态字,物理语义未坐实 |
| `0x53` | `4` | `u32 be` | `00 00 00 01` | `1` | `tail[3]`,诊断状态字,物理语义未坐实 |
| `0x57` | `3` | `char[3]` | `7a 6f 64` | `"zod"` | 固定帧尾 magic |
## 6. 已确认结论
### 6.1 已由真实抓包确认
1. `TCP 10010` 是独立状态流,不是 `TCP 10012` 的请求/响应复用。
2. 当前现场状态帧固定为 `90B`,不是早期静态分析里出现过的 `134B`。
3. `msg_id` 在 `20260428.pcap` 当前全量样本中恒为 `0`。
4. `tail[4]` 在 `20260428.pcap` 当前全量样本中恒为 `(2, 0, 0, 1)`。
5. `pose[6]` 的量纲表现符合 `X/Y/Z(mm) + W/P/R(deg)`。
6. `joint_or_ext[6..8]` 在当前现场样本中恒为 `0`。
### 6.2 已由数值范围和交叉对照强支持
1. `joint_or_ext[0..5]` 更像关节角 `rad`,而不是 `deg`。
2. 该判断与 `../analysis/UTTC_20260428_packet_validation.md` 的结论一致。
3. 该判断也与 `UDP 60015` 响应包中的关节 `deg` 形成互补关系:二者不能简单视作同单位直接复用。
## 7. 待确认项
以下内容当前不要写死为最终协议真义:
1. `tail[4]` 四个 `u32` 分别代表什么控制器语义。
2. `msg_id` 是否在其他控制柜版本、程序状态或异常态下会出现非零值。
3. `pose[3..5]` 是否可以严格命名为 FANUC 标准 `W/P/R`,还是只是与其数值表现一致。
4. `joint_or_ext[6..8]` 在带外部轴的现场是否仍复用同一布局。
## 8. 与当前代码实现的对齐情况
当前仓库里 `Flyshot.Runtime.Fanuc` 已按 `90B` 固定帧解析:
- `src/Flyshot.Runtime.Fanuc/Protocol/FanucStateProtocol.cs`
- `tests/Flyshot.Core.Tests/FanucProtocolTests.cs`
当前实现已经与抓包对齐的部分:
1. 固定长度 `90B`
2. `doz ... zod` 帧头帧尾校验
3. `pose[6] + joint_or_ext[9] + tail[4]` 的字节布局
4. `tail[4]` 原样保留到 `ControllerStateSnapshot.StateTailWords`
当前仍建议后续关注的点:
1. `FanucStateFrame` 已把该字段从 `JointDegrees` 更正为 `JointRadians`,后续新增代码应继续沿用弧度制命名。
2. 如果后续状态页或运行时逻辑需要直接展示该通道关节值,仍需明确标注这是 `10010` 的弧度值,避免和 `UDP 60015` 的 degree 语义混淆。
## 9. 建议用法
在当前 replacement 实现里,`TCP 10010` 更适合作为以下用途:
1. 提供机器人当前笛卡尔位姿和关节反馈快照。
2. 提供状态通道是否健康、是否陈旧的连接诊断依据。
3. 保留 `tail[4]` 原始状态字,供现场排错或后续继续逆向。
当前不建议直接用 `tail[4]` 去驱动明确业务判断,除非后续拿到新的现场对照证据。

View File

@@ -0,0 +1,182 @@
# FANUC Field Runtime Workflow
本文档记录当前现场主链路的 HTTP 调用顺序,以及每一步在 FANUC 三条真机通道上的动作。它替代旧 `ControllerClient` 工作流说明;旧 `50001/TCP+JSON` 入口不再作为运行目标。
## 1. 初始化
推荐使用聚合端点完成当前现场的一次性初始化:
```bash
POST /init_mpc_robt
{
"server_ip": "127.0.0.1",
"port": 50001,
"robot_name": "FANUC_LR_Mate_200iD",
"robot_ip": "192.168.10.11",
"sim": false
}
```
该端点内部顺序:
1. `ConnectServer(server_ip, port)`:兼容旧参数形状,仅记录服务连接语义。
2. `SetUpRobot(robot_name)`:加载机器人配置、关节限制和伺服周期。
3. `SetActiveController(sim)`:选择仿真或 FANUC 真机运行时。
4. `Connect(robot_ip)`:真机模式下依次建立 `TCP 10010` 状态通道、`TCP 10012` 命令通道、`UDP 60015` J519 运动通道。
5. `EnableRobot(2)`:真机模式下执行 `StopProg("RVBUSTSM") -> Reset -> GetProgStatus("RVBUSTSM") -> StartProg("RVBUSTSM")`,随后允许 J519 在收到机器人 UDP status 包后回发下一帧命令。
也可以使用拆分端点按同样顺序调用:
```text
POST /connect_server/?server_ip=127.0.0.1&port=50001
POST /setup_robot/?robot_name=FANUC_LR_Mate_200iD
POST /set_active_controller/?sim=false
POST /connect_robot/?ip=192.168.10.11
GET /enable_robot/?buffer_size=2
```
## 2. 参数设置
规划约束参数:
当前现场抓包已经确认,`50001/TCP+JSON` 的 `ExecuteFlyShotTraj(save_traj=true,use_cache=false)` 请求不会显式携带 `JointLimits / acc_limit / jerk_limit / velocity / acceleration / jerk`。因此新系统把规划约束分成两层处理:
1. 旧 `RobotConfig.json` 中已有的 `acc_limit / jerk_limit` 继续作为模型加载时的基础倍率。
2. 若旧系统导出的 `JointTraj.txt` 明显比当前 C# 规划更慢,使用 replacement-only 的内部校准参数限制规划阶段加速度,设计字段为 `planning_acceleration_scale`。
`planning_acceleration_scale` 只影响 `JointTraj.txt` 这类规划结果时间轴,不下发到 FANUC 控制柜,也不改变 J519 发送周期。若需要临时整体验证,也可以使用当前已有的 `planning_speed_scale`,但它是新系统兼容开关,不是旧抓包中出现的字段。
速度倍率:
```bash
POST /set_speedRatio/
{ "speed": 0.7 }
```
真机模式下会通过 `TCP 10012` 下发 `0x2207 SetSpeedRatio`,同时运行时保存当前倍率。`speed_ratio` 是执行期倍率,不参与 `IsFlyShotTrajValid` / `SaveTrajInfo` / `ExecuteFlyShotTraj(save_traj=true)` 的规划时长计算。J519 执行时仍按机器人 8ms 节拍更新目标,`speed_ratio` 只缩放原轨迹采样时间:
```text
t_send = k * 0.008
t_traj = t_send * speed_ratio
send_count = floor(duration / (0.008 * speed_ratio)) + 1
```
TCP 和普通 IO:
```text
POST /set_tcp/ body: { "x": 0, "y": 0, "z": 0 }
GET /get_tcp/
POST /set_io/?port=7&value=true&io_type=DO
GET /get_io/?port=7&io_type=DO
```
飞拍触发 IO 不走独立 `TCP 10012 SetIO`,而是嵌入 `UDP 60015` J519 命令包的 `write_io_type/index/mask/value` 字段。
## 3. 点到点 MoveJoint
```bash
POST /move_joint/
{ "joints": [0.8532358, 0.03837953, -0.19235304, 0.0071595116, 0.109054826, 0.040055145] }
```
`MoveJoint` 不再直接把最终关节写成单个 J519 目标,而是按现场抓包确认的 PTP 临时轨迹执行:
1. 从当前运行时状态读取当前关节坐标,单位为 `rad`。
2. 以当前关节和目标关节构造关节空间直线。
3. 用五次 smoothstep `10u^3 - 15u^4 + 6u^5` 生成起停平滑的进度。
4. 真机执行时仍由 J519 层把 `rad` 转成 `deg`,并按当前 `speed_ratio` 重采样。
已确认抓包按响应 `status=15` 运动窗口统计:
| 抓包 | speed_ratio | 运动窗口点数 | 运动窗口时长 |
|------|-------------|----------------------|----------|
| `2026042802-mvpoint.pcap` | 1.0 | 40 | 约 0.312s |
| `2026042802-mvpoint0.7.pcap` | 0.7 | 55 | 约 0.432s |
| `2026042802-mvpoint0.5.pcap` | 0.5 | 77 | 约 0.608s |
抓包命令流在运动窗口前后还会持续发送保持不变的起点/终点目标;功能复刻以 `status=15` 运动窗口为点数口径,并把最后一个采样点压到目标关节。实际目标几乎严格位于“起点 -> 终点”的同一条关节空间直线上,`speed_ratio` 体现为 J519 发送时间轴上的减速重采样,而不是改变路径形状。
## 4. 飞拍轨迹
上传:
```bash
POST /upload_flyshot/
{
"name": "UTTC_MS11",
"waypoints": [[...]],
"shot_flags": [false, true],
"offset_values": [0, 0],
"addrs": [[1, 3]]
}
```
校验:
```bash
POST /is_flyShotTrajValid/
{
"name": "UTTC_MS11",
"method": "self-adapt-icsp",
"save_traj": false
}
```
执行:
```bash
POST /execute_flyshot/
{
"name": "UTTC_MS11",
"move_to_start": true,
"method": "self-adapt-icsp",
"save_traj": false,
"use_cache": true,
"wait": true
}
```
执行链路:
1. 从上传缓存读取 waypoint、shot flag、offset、IO 地址组。
2. 使用 `icsp` 或 `self-adapt-icsp` 规划关节轨迹;规划阶段先应用 `acc_limit / jerk_limit`,再应用 replacement-only 的规划加速度校准参数。
3. 生成 `TrajectoryDoEvent`,把拍照触发绑定到轨迹时间。
4. 若 `move_to_start=true`,先从运行时当前关节位置生成临时 PTP 稠密轨迹移动到规划轨迹起点,并等待运行时 `IsInMotion=false` 后再启动飞拍轨迹,避免第一帧 J519 目标从当前位置跳到起点。
5. 真机模式下把规划输出的 `rad` 稠密轨迹按 J519 轨迹时间步长重采样并转成 `deg`,命令实际发包由机器人 UDP status 包驱动。
6. 若 `wait=true`,正式飞拍轨迹启动后继续等待运行时 `IsInMotion=false`,机器人执行完整条飞拍轨迹后 HTTP 才返回;`wait=false` 时启动后立即返回。
7. 启动前若已有 J519 响应且 `accept_cmd` 或 `sysrdy` 未就绪,则拒绝执行。
8. 周期命令中嵌入 IO 脉冲;当前 UTTC 抓包确认 mask 集合为 `10/12/14`,共 17 个 set 脉冲和 17 个 clear 帧。
`method="doubles"` 当前明确返回未实现;现场主链路使用 `icsp` / `self-adapt-icsp`。
## 5. 停止与断开
```text
GET /stop_move/
GET /disable_robot/
POST /disconnect_robot/
```
真机模式下:
- `StopMove()` 取消当前稠密轨迹生成任务并停止 J519 状态包驱动发送。
- `DisableRobot()` 发送 J519 packet type 2 状态输出停止包,然后 `StopProg("RVBUSTSM")`。
- `Disconnect()` 关闭状态、命令和 J519 三条通道,并清理本地运行状态。
## 6. 现场抓包覆盖
`tests/Flyshot.Core.Tests/UttcJ519GoldenTests.cs` 直接解析以下抓包并与 `Rvbust/uttc-20260428/Data/JointDetialTraj.txt` 对比:
| 抓包 | 速度 | 运行 J519 点数 | 发送时长 |
|------|------|----------------|----------|
| `2026042802-0.5.pcap` | 0.5 | 1851 | 14.800309s |
| `2026042802-0.7.pcap` | 0.7 | 1322 | 10.568313s |
| `2026042802-1.pcap` | 1.0 | 926 | 7.400125s |
测试同时检查:
- 主运行窗口命令序号连续,无重复 seq;J519 客户端单元测试覆盖按最新 status sequence 回发命令。
- 响应 `status=15` 段覆盖主运行窗口,响应相对命令滞后 2 到 8 帧。
- 实发点位相对重采样期望的全局 RMS 小于 `0.012deg`,最大绝对误差小于 `0.07deg`。
- `lastData=0`,结束运动当前依赖 J519 packet type 2 状态输出停止包;`../j519 协议.pcap` 中另有 1 个 `LastData=1` 后紧跟 type 2 的样本,停止语义后续单独验证。
- IO 脉冲数量和 mask 集合 `10/12/14` 与抓包一致。

View File

@@ -0,0 +1,187 @@
# FANUC 真机协议 Socket 通信层实现计划
## 上下文
状态更新:本计划中的 Socket 客户端和 `FanucControllerRuntime` 改造已经落地;当前事实以 `README.md` 和 `docs/fanuc-field-runtime-workflow.md` 为准。本文保留为实现过程记录。
计划制定时 `flyshot-replacement` 项目已完成:
- 三条 FANUC 通信链路的二进制协议编解码(`FanucCommandProtocol`、`FanucStateProtocol`、`FanucJ519Protocol`)
- 抓包样本验证的协议测试(5 个 FanucProtocolTests 全部通过)
- TCP 10012 的 `Get/SetSpeedRatio`、`Get/SetTCP`、`Get/SetIO` 参数命令封包、响应解析和本地模拟器测试
- HTTP 兼容层控制器和状态监控页
- 轨迹规划与飞拍触发编排层
2026-04-28 `Rvbust/uttc-20260428/20260428.pcap` 新增约束:
- `TCP 10010` 状态帧继续确认为固定 `90B`。
- `UDP 60015` 命令 `target[0..5]` 为关节角 `deg`,而 `JointDetialTraj.txt` 为 `rad`。
- `speed_ratio=0.7` 在本抓包中表现为 UDP 下发时间轴约 `1.427730x` 拉伸;机器人侧 `TCP 10012` 未抓到 `0x2207 SetSpeedRatio`。
- `UTTC_MS11` 的 17 个飞拍触发点与 17 个 UDP IO 脉冲一一对齐,`io_keep_cycles=2` 对应约两周期清零。
**历史缺失项(已完成)**:计划制定时 `FanucControllerRuntime` 仍是纯内存状态桩。当前实现已经改为持有 `FanucCommandClient`、`FanucStateClient` 和 `FanucJ519Client`,真机模式会建立三条通道并从状态/J519 响应读取运行状态。
## 目标
将 `FanucControllerRuntime` 从内存桩改造为具备真实 FANUC R30iB 通信能力的运行时,使 HTTP 层的每个指令真正下发到控制柜。
## 架构设计
### 分层结构
```
LegacyHttpApiController / StatusController (HTTP 适配层,保持不动)
↓ 调用同步接口
IControllerRuntime / ControllerClientCompatService (兼容层,保持不动)
↓ 调用同步接口
FanucControllerRuntime (改造:从内存桩 → 委托给三个 Socket 客户端)
↓ 内部持有并调度
FanucCommandClient (TCP 10012,Req/Res 同步命令通道)
FanucStateClient (TCP 10010,持续接收状态帧后台循环)
FanucJ519Client (UDP 60015,8ms 周期发送 + 接收响应)
↓ 使用现有编解码
FanucCommandProtocol / FanucStateProtocol / FanucJ519Protocol (已有,不改动)
```
### 关键设计决策
1. **接口保持同步**:`IControllerRuntime` 现有 18 个方法全为同步签名。内部 Socket I/O 采用 `Task` + `.GetAwaiter().GetResult()` 短时间阻塞,或后台线程 + 锁同步状态快照。避免一次性推翻整个兼容层。
2. **三个独立客户端**:每条物理通道一个类,各自管理连接生命周期,便于单独测试和故障定位。
3. **状态通道后台循环**:`FanucStateClient` 内部启动 `Task` 持续 `ReadAsync(90)`,解析状态帧后写入线程安全的 `ControllerStateSnapshot` 缓存。
4. **J519 周期发送器**:`FanucJ519Client` 内部用 `PeriodicTimer` 或 `Task.Delay` 实现约 8ms 周期的发送循环。命令通过线程安全的队列/缓冲区注入。
5. **RVBUSTSM 程序生命周期隐式管理**:`EnableRobot()` 时自动走 `StopProg→Reset→GetProgStatus→StartProg("RVBUSTSM")` 序列(与抓包一致)。`DisableRobot()` 时发送 `StopProg`。
6. **连接顺序**:`Connect()` 按顺序建立三条通道 — 先 TCP 10010(状态),再 TCP 10012(命令),最后 UDP 60015(运动)。
## 实现步骤
### Phase 1: TCP 10012 命令客户端
**新建文件**:`src/Flyshot.Runtime.Fanuc/Protocol/FanucCommandClient.cs`
职责:
- `Connect(string ip, int port = 10012)` — 建立 TcpClient 连接
- `SendCommandAsync(uint messageId, ReadOnlyMemory<byte> body)` — 发送并等待响应
- `SendProgramCommandAsync(uint messageId, string programName)` — 封装程序名命令
- `Disconnect()` — 关闭连接
- 线程安全(单个命令通道同一时间只处理一个请求)
需要封装的具体命令方法:
- `StopProgramAsync(string name)` → `PackProgramCommand(0x2103, name)`
- `ResetRobotAsync()` → `PackEmptyCommand(0x2100)`
- `GetProgramStatusAsync(string name)` → `PackProgramCommand(0x2003, name)`
- `StartProgramAsync(string name)` → `PackProgramCommand(0x2102, name)`
- `GetTcpAsync()` / `SetTcpAsync()` — 已按 `tcp_id + f32[7] pose` 字段布局实现
- `GetSpeedRatioAsync()` / `SetSpeedRatioAsync()` — 已按 `ratio_int / 100.0` 与 `ratio_int_0_100` 字段布局实现;注意 2026-04-28 真实运行抓包未出现机器人侧 `0x2207`,执行链路仍必须在 UDP 发送时间尺度上应用当前速度倍率
- `GetIoAsync()` / `SetIoAsync()` — 已按 `io_type / io_index / f32 io_value` 字段布局实现
**测试**:`tests/Flyshot.Core.Tests/FanucCommandClientTests.cs`
- 用 `TcpListener` 本地模拟控制器,验证帧收发与解析
### Phase 2: TCP 10010 状态客户端
**新建文件**:`src/Flyshot.Runtime.Fanuc/Protocol/FanucStateClient.cs`
职责:
- `Connect(string ip, int port = 10010)` — 建立 TcpClient 连接
- 内部启动后台 `Task` 循环 `ReadAsync(FanucStateProtocol.StateFrameLength)`
- 每收到一帧调用 `FanucStateProtocol.ParseFrame()`
- 将解析结果写入线程安全的最新状态缓存
- 单帧接收超时后标记状态陈旧,不再把旧帧当作当前位姿/关节状态使用
- EOF、坏帧、Socket 异常或超时后关闭当前连接,并按退避策略自动重连 TCP 10010
- `GetLatestFrame()` — 返回最近一次解析的状态帧
- `GetStatus()` — 返回连接阶段、陈旧状态、最近异常和重连次数
- `Disconnect()` — 取消后台循环并关闭连接
**测试**:`tests/Flyshot.Core.Tests/FanucStateClientTests.cs`
- 用 `TcpListener` 本地发送抓包样本 hex,验证后台循环能正确解析。
- 用本地模拟控制器验证无状态帧超时、EOF 后退避重连和重连后的继续收帧。
- `FanucStateProtocol` 已用 `j519 协议.pcap` 中多条 90B 样本锁定 `pose[6]`、`joint[6]`、`external_axes[3]` 和 `raw_tail_words[4]`。
- `Rvbust/uttc-20260428/20260428.pcap` 再次确认 `10010` 状态帧固定 90B,平均间隔约 25.6ms。
- 尾部状态字当前只作为 `ControllerStateSnapshot.stateTailWords` 诊断字段保留,不从 `[2,0,0,1]` 推断使能或运动状态。
### Phase 3: UDP 60015 J519 运动客户端
**新建文件**:`src/Flyshot.Runtime.Fanuc/Protocol/FanucJ519Client.cs`
职责:
- `Connect(string ip, int port = 60015)` — 创建 UdpClient
- 发送 init packet (`PackInitPacket()`)
- 内部启动发送循环(约 8ms 周期)
- `UpdateCommand(FanucJ519Command command)` — 原子更新下一周期要发送的命令
- `StartMotion()` — 启动发送循环
- `StopMotion()` — 发送 end packet,停止循环
- 接收线程:持续 `ReceiveAsync()` 解析 132B 响应,更新反馈状态
- `Disconnect()` — 清理
执行注意事项:
- 规划层输出关节角为 `rad`,J519 命令 `target[0..5]` 必须转为 `deg`。
- 发送循环不能只按 `JointDetialTraj` 行号逐行发;需要保持约 8ms 的 J519 实发周期,同时按当前 `speed_ratio` 对原轨迹时间轴做缩放。
- 实发规则:第 `k` 个 J519 周期的发送时间为 `t_send = k * 0.008`,轨迹采样时间为 `t_traj = t_send * speed_ratio`,命令包数为 `floor(duration / (0.008 * speed_ratio)) + 1`。`UTTC_MS11` 中 `7.403046 / (0.008 * 0.7) = 1321.9725`,因此主运行实发 `1322` 个运行包,而不是 `JointDetialTraj.txt` 的 `464` 行。
- 飞拍 IO 事件应嵌入 `write_io_type/index/mask/value`,不要用独立 `TCP 10012 SetIO` 模拟拍照触发。
- 响应 `joints_deg` 相对命令目标存在约 7 帧 / 56ms 滞后,闭环判断要容忍该延迟。
**测试**:`tests/Flyshot.Core.Tests/FanucJ519ClientTests.cs`
- 用本地 UDP socket 模拟控制器收发
### Phase 4: 重写 FanucControllerRuntime
**改造文件**:`src/Flyshot.Runtime.Fanuc/FanucControllerRuntime.cs`
将当前内存桩替换为真实运行时:
- 持有三个客户端实例:`FanucCommandClient`、`FanucStateClient`、`FanucJ519Client`
- `Connect(robotIp)` — 顺序连接 10010 → 10012 → 60015
- `EnableRobot(bufferSize)` — 走完整 StartProg 序列(Stop→Reset→Status→Start RVBUSTSM),然后启动 J519
- `DisableRobot()` — 停止 J519,发送 StopProg
- `Disconnect()` — 断开三条通道
- `ExecuteTrajectory(result, finalJointPositions)` — 将规划后的稠密路点经 `rad -> deg` 转换,并按 `t_send = k * 0.008`、`t_traj = t_send * speed_ratio` 重采样后,通过 J519 逐周期发送
- `StopMove()` — 立即停止 J519 发送循环
- `GetSnapshot()` — 优先从 `FanucStateClient` 读取最新状态;若状态通道未连接,回退到内存值
- `GetJointPositions()` / `GetPose()` / `GetTcp()` / `GetSpeedRatio()` / `GetIo()` — 优先从真实通道读取
- `SetTcp()` / `SetSpeedRatio()` / `SetIo()` — 通过命令通道发送
### Phase 5: 端到端集成测试
**改造/新建测试**:
- `tests/Flyshot.Server.IntegrationTests/LegacyHttpApiCompatibilityTests.cs` — 补充真实连接流程(可用本地模拟器)
- `tests/Flyshot.Core.Tests/FanucControllerRuntimeSocketTests.cs` — 用本地 TCP/UDP 模拟器验证完整链路
**验证命令**:
```bash
cd flyshot-replacement
dotnet build FlyshotReplacement.sln -v minimal
dotnet test tests/Flyshot.Core.Tests/Flyshot.Core.Tests.csproj -v minimal
dotnet test tests/Flyshot.Server.IntegrationTests/Flyshot.Server.IntegrationTests.csproj -v minimal
```
## 风险与回退策略
1. **真机连接风险**:第一版 Socket 实现可能有超时/重连问题。`FanucControllerRuntime` 保留 `_simulationMode` 路径,仿真模式下仍走内存桩。
2. **性能风险**:同步接口内部阻塞 Socket 可能影响 HTTP 并发。若实测有问题,后续将 `IControllerRuntime` 改为 async。
3. **现场验证风险**:TCP 10012 参数命令已按逆向结论实现,但仍需在真实 R30iB 控制柜上确认默认 `tcp_id=1`、IO 类型/地址和错误码语义。
## 关键文件清单
| 文件 | 动作 |
|------|------|
| `src/Flyshot.Runtime.Fanuc/Protocol/FanucCommandClient.cs` | 新建 |
| `src/Flyshot.Runtime.Fanuc/Protocol/FanucStateClient.cs` | 新建 |
| `src/Flyshot.Runtime.Fanuc/Protocol/FanucJ519Client.cs` | 新建 |
| `src/Flyshot.Runtime.Fanuc/FanucControllerRuntime.cs` | 重写 |
| `tests/Flyshot.Core.Tests/FanucCommandClientTests.cs` | 新建 |
| `tests/Flyshot.Core.Tests/FanucStateClientTests.cs` | 新建 |
| `tests/Flyshot.Core.Tests/FanucJ519ClientTests.cs` | 新建 |
| `tests/Flyshot.Core.Tests/FanucControllerRuntimeSocketTests.cs` | 新建 |
## 下一步验证标准
- `FanucControllerRuntime` 的 `Connect()` 能成功建立三条 TCP/UDP 连接
- `EnableRobot()` 能走完 `RVBUSTSM` 启动序列
- `ExecuteTrajectory()` 能按 8ms 周期通过 J519 发送路点,并按当前 `speed_ratio` 推进原始轨迹时间
- `GetSnapshot()` 返回的值来自 TCP 10010 真实状态帧而非内存
- 现有 10 个集成测试和 25 个核心测试仍然通过

View File

@@ -0,0 +1,232 @@
# FANUC Stream Motion 文档要点与实现差异
本文记录 `../FANUC_stream_motion.pdf` 中与本仓库 `Flyshot.Runtime.Fanuc` 直接相关的重点,并对照当前实现状态。
读取时间:2026-05-03
## 1. 文档定位
`FANUC_stream_motion.pdf` 对应 FANUC `Stream motion` 功能,选项号为 `A05B-2600-J519`。它描述的是外部设备通过以太网实时发送期望位置,让机器人按外部生成路径运动的控制方式。
文档明确要求外部设备自行生成满足机器人约束的路径,包括速度、加速度、jerk、可达性、姿态连续性等。FANUC 不提供完整运动学、逆解和碰撞检测公式。
## 2. 使用前提与示教程序
1. 机器人侧需要安装 J519 stream motion 选项。
2. 物理网口通过 `$STMO.$PHYS_PORT` 选择,`1` 表示 `CD38A`,`2` 表示 `CD38B`。
3. 机器人程序必须包含成对的 `IBGN start[*]` 和 `IBGN end[*]` 指令,二者编号必须一致,`start` 必须在 `end` 前一行。
4. `IBGN start[*]` 执行期间,机器人根据外部设备发来的期望位置运动;`IBGN end[*]` 之后程序继续执行。
5. 执行时要求 `AUTO` 模式和 `100% OVERRIDE`。
当前实现中的 `FanucControllerRuntime.EnableRobot()` 会按现场抓包流程启动 `RVBUSTSM` 程序,并随后允许 J519 在收到机器人 UDP status 包后回发命令。是否满足 `AUTO / 100% OVERRIDE / IBGN start` 已到位,当前只通过 J519 状态位和现场程序行为间接判断,没有在代码里读取或设置这些控制器状态。
## 3. UDP 60015 协议结构
协议使用 UDP,大端字节序,机器人侧端口为 `60015`。通信周期通常为 `8ms`,部分机型支持 `4ms`。状态包输出可以在任意时间通过 start/stop 控制包启停,不要求已经进入 `IBGN start[*]`。
### 3.1 状态输出 start 包
外部设备发给机器人:
| 字段 | 长度 | 值 |
| --- | --- | --- |
| Packet type | 4B | `0` |
| Version | 4B | `1` |
当前实现:`FanucJ519Protocol.PackInitPacket()` 已按 8B 大端控制包实现,`FanucJ519Client.ConnectAsync()` 连接后立即发送。
### 3.2 状态包
机器人发给外部设备,长度为 `132B`:
| 偏移 | 字段 | 含义 |
| --- | --- | --- |
| `0x00` | Packet type | `0` |
| `0x04` | Version | `1` |
| `0x08` | Sequence No. | 状态包序号,发送 start 包后从 `1` 重新开始 |
| `0x0c` | Status | bit0 接受命令、bit1 已收到命令、bit2 SYSRDY、bit3 运动中 |
| `0x0d..0x12` | Read I/O 回显和值 | 回显命令包中的读取 IO 类型、索引、掩码,并返回 16 点 IO 值 |
| `0x14` | Timestamp | ms 单位,2ms 分辨率 |
| `0x18..0x38` | Cartesian / external axis | `X/Y/Z/W/P/R` 加 3 个扩展轴 |
| `0x3c..0x5c` | Joint | `J1..J9`,单位 degree |
| `0x60..0x80` | Motor current | `J1..J9` 电流,单位 A |
当前实现:`FanucJ519Protocol.ParseResponse()` 已解析 `132B` 状态包,并暴露 `AcceptsCommand`、`ReceivedCommand`、`SystemReady`、`RobotInMotion` 四个状态位。`FanucControllerRuntime.GetSnapshot()` 也会把最新 J519 状态写进快照。
### 3.3 命令包
外部设备发给机器人,长度为 `64B`:
| 偏移 | 字段 | 含义 |
| --- | --- | --- |
| `0x00` | Packet type | `1` |
| `0x04` | Version | `1` |
| `0x08` | Sequence No. | 第一包应等于刚收到的状态包序号,后续逐包递增 |
| `0x0c` | Last data | 正常为 `0`;结束外部控制时最后一包设为 `1` |
| `0x0d..0x11` | Reading I/O | 可读取最多 16 个连续 IO 点 |
| `0x12` | Data format | `0` 笛卡尔,`1` 关节 |
| `0x13..0x19` | Writing I/O | 可写入最多 16 个连续 IO 点 |
| `0x1a` | unused | 2B |
| `0x1c..0x3c` | target[9] | 9 个 f32 目标值;关节格式时单位 degree |
当前实现:`FanucJ519Protocol.PackCommandPacket()` 已按上述布局打包,默认 `dataStyle=1`,也就是关节格式。运行时会把规划输出的弧度制关节轨迹转换为 degree 后下发。
### 3.4 状态输出 stop 包
外部设备发给机器人:
| 字段 | 长度 | 值 |
| --- | --- | --- |
| Packet type | 4B | `2` |
| Version | 4B | `1` |
文档把它定义为“停止状态包输出”的控制包,不是命令流正常终止的首选动作。命令流结束应通过 command packet 的 `Last data=1` 表达。
当前实现:`FanucJ519Client.StopMotionAsync()` 当前会停止状态包驱动发送并发送 packet type `2`,而稠密轨迹执行期间保持 `LastData=0`。这是与 FANUC 文档最明显的语义差异之一;已有多数 UTTC 抓包显示主运行窗口 `LastData=0`,但 `../j519 协议.pcap` 中存在 1 个 `LastData=1` 后紧跟 packet type `2` 的样本,后续应单独校准停止语义。
## 4. 通信时序重点
文档推荐的时序是:
1. 外部设备发送状态输出 start 包。
2. 机器人每个通信周期输出状态包。
3. 机器人程序执行到 `IBGN start[*]` 后,状态包 bit0 变为 `1`,表示等待命令包。
4. 外部设备收到 bit0 为 `1` 的状态包后,立即发送第一帧命令包,第一帧命令序号应等于刚收到的状态包序号。
5. 后续每收到一个状态包,外部设备应立即发送下一帧命令包。
6. 结束命令通信时,发送 `Last data=1` 的最后一帧命令包。
当前实现对照:
1. `FanucJ519Client` 已改为收到机器人 132B status 包后立即回发当前命令,不再由上位机本地固定 8ms 发送循环主动发包。
2. 命令包 sequence 已按刚收到的 status packet sequence 写入,避免第一帧从本地 `0` 起步。
3. `FanucControllerRuntime.ExecuteTrajectory()` 启动前会检查已有 J519 响应中的 `AcceptsCommand` 和 `SystemReady`;但如果还没收到状态包,则会放行,后续命令仍要等第一帧 status 到达才会发出。
4. 当前稠密轨迹结束不发送 `LastData=1`,而是依赖停止 J519 状态包驱动发送和 packet type `2` stop 控制包。
序号和节拍已经按手册方向校准;停止语义仍需在真实 R30iB 联调中继续确认。
## 5. 命令缓冲
文档说明机器人可以缓冲提前到达的 command packet。默认启用,缓冲上限为 `$STMO.$PKT_STACK - 1`,`$PKT_STACK` 默认 `10`,可配置范围 `2..10`。`$STMO.$START_MOVE` 决定积累多少未处理命令包后开始运动,默认 `1`。
注意事项:
1. 只有 command packet 会进入缓冲。
2. status output stop packet 会立即处理。
3. 如果 command buffer 中还有未处理包,不应发送 status output stop packet。
4. 使用 `Last data=1` 时,机器人会先处理完缓冲里的命令包,再结束外部控制。
当前实现没有显式预填 `$PKT_STACK` 缓冲,也没有读取 `$START_MOVE`。`FanucJ519Client` 只保存一个“当前命令”,由后台循环持续发送;`FanucControllerRuntime.SendDenseTrajectory()` 另一个 8ms 循环负责按轨迹时间更新这条当前命令。这与文档的“按状态包响应并可提前发多包缓冲”模型不同。
## 6. 可执行运动条件
文档列出的主要运动约束:
1. 目标点必须可达。
2. 笛卡尔格式下目标点对应的关节解必须唯一,且 configuration 要与 `IBGN start` 开始时一致。
3. 各轴必须满足上下限。
4. 不能发生自碰撞。
5. 必须考虑 FANUC J3 轴定义:J3 不是相对 J2 臂的夹角,而是机器人视角下相对水平面的 J3 臂角度。
6. 外部设备必须控制每轴速度、加速度、jerk 不超过 `$STMO_GRP` 下的限制。
7. 状态包中的当前位置是 servo feedback position,不是 command position。轨迹起点应平滑连接到机器人 command position,而不能简单用当前 servo position 直接起步。
8. reducer load 超限也会导致停机,负载相关计算不公开。
当前实现对这些条件的覆盖:
| 条件 | 当前状态 |
| --- | --- |
| 关节格式下发 | 已实现,当前现场链路默认只使用关节格式 |
| `rad -> deg` | 已实现,并由 UTTC J519 golden tests 覆盖 |
| `speed_ratio` 下发时间轴缩放 | 已实现,J519 实发时间为 `t_send = k * 0.008`,原轨迹采样时间为 `t_traj = t_send * speed_ratio` |
| IO 触发嵌入 J519 命令包 | 已实现,使用 `write_io_type/index/mask/value` |
| 速度、加速度、jerk 约束 | 规划层有 `acc_limit / jerk_limit` 等兼容参数,但未从 FANUC `$STMO_GRP` 在线读取,也未实现手册附录中的 20 档速度/负载插值 |
| J3 轴定义 | 当前文档未见专门处理;需要确认 `.robot` 模型与现场导出轨迹是否已经采用 FANUC J3 定义 |
| command position 起步 | `MoveJoint` 会用当前运行时记录的关节作为起点生成 PTP 稠密轨迹;但没有通过 FANUC HMI 通信读取 command position |
| reducer load | 未建模,依赖保守规划和现场报警反馈 |
| 笛卡尔格式限制 | 运行时不走笛卡尔 J519 目标格式,暂不覆盖 configuration 变化报警 |
## 7. 系统变量
与本仓库后续最相关的变量:
| 变量 | 默认/含义 |
| --- | --- |
| `$STMO.$PHYS_PORT` | 物理口,`1=CD38A`,`2=CD38B` |
| `$STMO.$COM_INT` | 通信周期,单位 ms,通常 `8`,只读 |
| `$STMO.$PKT_STACK` | command buffer 最大保留量,默认 `10` |
| `$STMO.$START_MOVE` | 缓冲中积累多少未处理命令后开始运动,默认 `1` |
| `$STMO_GRP.$JNT_VEL_LIM[*]` | 各轴速度上限,degree/s,只读 |
| `$STMO_GRP.$JNT_ACC_LIM[*]` | 各轴加速度上限,degree/s^2,只读 |
| `$STMO_GRP.$JNT_JRK_LIM[*]` | 各轴 jerk 上限,degree/s^3,只读 |
| `$STMO_GRP.$LMT_MODE` | 加速度/jerk 限制计算模式 |
| `$STMO_GRP.$WARN_LIM` | 接近限制时报警阈值,默认 `80%` |
| `$STMO_GRP.$FLTR_LN` | 命令目标移动平均滤波窗口 |
| `$STMO_GRP.$MAX_SPD` | 用于限制计算的 flange center 最大速度 |
当前实现没有读取或设置上述系统变量。`RobotProfile.ServoPeriod` 当前决定运行时发送周期;对当前现场而言应继续确认它与 `$COM_INT` 一致。
## 8. 附录 B:加速度和 jerk 限制
文档说明,在 `$STMO_GRP[].$LMT_MODE=0` 时,加速度和 jerk 的允许上限会根据 flange center speed 与 payload 计算:
1. 以 `$MAX_SPD` 分成 20 档速度区间。
2. 每个轴、每种限制类型都有无负载和最大负载两张 20 档表。
3. 实际 payload 通过线性插值得到限制表。
4. 实际 flange center speed 在相邻速度档之间线性插值。
5. 限制值不是每个通信周期都更新,而是在超过 `Vmax/20` 到再次跌回阈值的整段时间内,以该段观测到的 `Vpeak` 决定。
6. 如果长时间不跌回阈值,会按中间检查时间做临时判断。
文档还提供了 packet type `3` 的限制表查询协议:
| 包 | 方向 | 重点字段 |
| --- | --- | --- |
| 请求 | 外部设备 -> 机器人 | packet type `3`、version `1`、axis `1..9`、limit type `0=velocity/1=acceleration/2=jerk` |
| 响应 | 机器人 -> 外部设备 | packet type `3`、version `1`、axis、limit type、`Vmax`、中间检查时间、无负载 20 档、最大负载 20 档 |
当前实现没有 packet type `3` 查询,也没有实现手册描述的动态限制表算法。现阶段规划时长和保守程度主要依赖 replacement 自身参数与现场抓包对齐。
## 9. 报警与诊断
文档中与实现最相关的报警:
| 报警 | 含义 |
| --- | --- |
| `MOTN-600` | 命令序号与机器人期望不一致 |
| `MOTN-602` | data format 非法 |
| `MOTN-603` | 后续命令包未在通信周期内到达 |
| `MOTN-604` | 命令包过多,超出缓冲 |
| `MOTN-605` | 目标位置包含 NaN 或 infinity |
| `MOTN-606` | 非 AUTO 或 override 不是 100% |
| `MOTN-607` | 协议版本不匹配 |
| `MOTN-609/610/611` | 速度、加速度、jerk 超限 |
| `MOTN-612/613/614` | 接近速度、加速度、jerk 限制 |
| `MOTN-617` | 目标点与当前位置不连续 |
| `MOTN-619` | 当前机型不支持笛卡尔目标格式 |
| `PRIO-023` | 读写的 IO 类型或索引未分配 |
当前 `ControllerStateSnapshot.ActiveAlarms` 仍为空,Web 状态页也尚未接入 FANUC 报警列表。后续现场联调如果出现报警,应优先按上述表格关联 J519 包序号、目标数据、IO 字段、发送间隔和状态包 bit。
## 10. 与当前代码的结论
已基本对齐:
1. UDP 60015、大端、start/stop 控制包、64B command packet、132B status packet 的基础二进制布局。
2. `Data format=1` 的关节目标下发。
3. 状态位 bit0..bit3 的解析和快照暴露。
4. 规划输出 `rad` 转 J519 `deg`。
5. 根据 `speed_ratio` 做运行期时间轴缩放,而不是改变规划文件时间。
6. 飞拍 IO 触发通过 command packet 的写 IO 字段下发。
7. 命令发送按机器人 UDP status 包驱动,并使用最新 status sequence 回发。
主要差异/风险:
1. 当前未实现命令缓冲预填,也未读取 `$PKT_STACK / $START_MOVE`。
2. 当前停止运动依赖 packet type `2` stop 控制包,没有稳定发送 `LastData=1` 的最后 command packet;这与手册标准结束语义不同。
3. 当前未实现 packet type `3` 的速度/加速度/jerk 限制表查询,也未实现 payload/speed 20 档动态限制算法。
4. 当前没有自动校验 `AUTO / 100% OVERRIDE / brake control / resume offset / payload` 等控制器前置状态。
5. 当前没有报警码读取和 `MOTN-* / PRIO-*` 映射。
建议后续联调优先级:
1. 验证运动结束是否必须补 `LastData=1`;如果当前 stop 控制包能稳定工作,也应在文档中标为现场兼容路径,而不是手册标准路径。
2. 抓一次报警现场包,确认 `MOTN-600/603/617` 等是否能从包序号与状态位直接定位。
3. 如果后续追求更稳的真实机运行,补 packet type `3` 限制表查询,并把规划器的速度、加速度、jerk 校验与 FANUC 手册算法靠近。

View File

@@ -0,0 +1,460 @@
# 飞拍任务 `speedRatio` 执行侧调速设计
## 1. 背景
当前系统同时存在两个速度相关参数:
- `planning_speed_scale`
- 规划阶段参数。
- 作用是让规划结果在时长与整体动力学上更接近现场旧系统或当前配置要求。
- 启动后固定,不希望在运行中再修改语义。
- `speedRatio`
- 运行阶段参数。
- 期望成为统一在线调速入口。
- 本次范围内只要求对“下一次开始执行的飞拍任务”生效,不要求对正在执行中的任务中途热切换。
本次任务已经明确收敛:
- 只修改飞拍任务链路。
- 不修改 `move_joint`。
- 不通过放宽阈值、关闭校验或只看平均速度来规避问题。
## 2. 现状问题
当前飞拍执行链路的核心语义是:
1. 规划层先生成 `DenseJointTrajectory`。
2. 运行时保持 `8ms` 物理发送周期不变。
3. 对第 `k` 个发送点,按 `trajectoryTime = sendTime * speedRatio` 回映射到规划轨迹时间。
4. 再在原始稠密轨迹上做线性插值,得到实际要发的关节目标。
5. 最后再用离散差分检查 `vel/acc/jerk`。
该结构在 `speedRatio < 1` 时有天然风险:
- 物理发送周期固定为 `8ms`,但轨迹时间推进被压缩。
- 线性插值会把规划轨迹中原本“看起来连续”的局部变化映射成新的离散点列。
- 这些新点列在三阶后向差分上容易形成尖峰。
- 该风险在 `speedRatio = 0.5` 时已经暴露为明显的 Jerk 超限告警。
问题本质不是“平均速度是否合理”,而是:
- 最终 `8ms` 发送点列本身没有先被当作一等公民建模。
- `speedRatio` 的执行语义落在“发送前临场回采样”阶段。
- 离散约束校验发生得太晚,只能报错或事后分析。
## 3. 目标
本设计目标如下:
- 保持 `planning_speed_scale` 只属于规划层。
- `speedRatio` 是运行时统一执行倍率,可影响下一次启动的飞拍、普通轨迹和 `MoveJoint`。
- 飞拍链路在任意有效 `speedRatio` 下,最终真实发送的 `8ms` 点列必须满足逐周期 `vel/acc/jerk` 约束。
- 不依赖“插值后自然会平滑”的经验假设。
- 当请求 `speedRatio` 下的最终发送队列不满足约束时,拒绝执行并输出超限诊断;不在执行侧偷偷改写本次请求倍率。
- 不破坏现有 `shot timeline / sample offset` 语义。
- `speedRatio = 1.0` 时行为应与当前基线一致或可解释等价。
## 4. 非目标
本次设计明确不包含以下内容:
- 不恢复旧的发送阶段临场回采样机制;普通轨迹和 `MoveJoint` 允许把 `speedRatio` 前移到规划或临时轨迹生成阶段折算。
- 不讨论 GUI、多机器人或旧 `50001/TCP+JSON` 网关恢复。
- 不修改 `planning_speed_scale` 的含义。
- 不在任务执行中途支持 `speedRatio` 热切换。
- 不通过调大容差、降低限值检查强度或直接跳过 Jerk 检查来“解决问题”。
## 5. 设计总览
飞拍执行链路调整为“两段式”:
1. 规划段
- 维持当前实现。
- 仍由 `planning_speed_scale` 参与规划时长和规划层轨迹生成。
- 输出 `TrajectoryResult.DenseJointTrajectory`、`TriggerTimeline`、`ShotEvents` 等规划结果。
2. 执行准备段
- 新增“飞拍执行侧最终发送序列生成器”。
- 输入规划层稠密轨迹、执行时读取到的 `speedRatio`、机器人关节限位和触发时间轴。
- 生成最终 `8ms` 发送队列。
- 对最终队列做逐周期 `vel/acc/jerk` 校验。
- 如果不通过,则拒绝执行,提示降低 `planning_speed_scale` 重新规划或显式设置更低的 `speedRatio`。
3. 发送段
- 运行时只消费已经准备好的最终 `8ms` 队列。
- 运行时不再根据 `speedRatio` 对 `DenseJointTrajectory` 临场做线性回采样。
核心变化是:
- `speedRatio` 不再直接驱动“发送前临场插值”。
- `speedRatio` 改为驱动“执行侧最终队列构建”。
- 最终队列一旦生成并校验通过,运行时只负责按 J519 状态包节奏出队发送。
## 6. 统一语义
本次以飞拍任务为主线,但 `speedRatio` 允许作为运行时统一执行倍率影响普通轨迹和 `MoveJoint`,需要把三类入口的边界说清楚,避免再次混淆规划倍率和执行倍率。
### 6.1 `planning_speed_scale`
- 只影响规划结果。
- 影响 `DenseJointTrajectory` 的时间轴与规划层速度/加速度/Jerk 使用方式。
- 飞拍开始执行前就已经固定。
- 在线设置 `speedRatio` 时不应反向修改它。
### 6.2 `speedRatio`
- 影响下一次启动的运动任务,不在任务执行中途热切换。
- 对飞拍任务,表示用户期望的执行层时间推进速度,可前移到规划/执行准备阶段生成最终发送队列。
- 对普通轨迹和 `MoveJoint`,允许在规划或临时 PTP 轨迹生成阶段折算进有效关节限位,使运行时只消费已经生成好的 `8ms` 稠密点列。
- `speedRatio = 0.8` 表示执行节奏接近把同几何轨迹时间轴整体拉长 `1 / 0.8`。
- 例如 `planning_speed_scale = 0.9` 且 `speedRatio = 0.8` 时,最终执行节奏应接近同几何轨迹在 `planning_speed_scale ≈ 0.72` 下的结果。
也就是说:
- `speedRatio` 是确定的执行倍率,不是校验失败后的自动搜索变量。
- `finalSpeedRatio` 正常必须等于请求值。
- 若该倍率下最终队列超限,问题暴露给调用方,而不是由执行侧自动改成另一个倍率。
## 7. 最终发送序列生成
### 7.1 新增执行侧生成器
建议新增一个飞拍专用生成器,命名可类似:
- `FlyshotExecutionSendSequenceBuilder`
- 或 `FlyshotExecutionQueueBuilder`
职责:
- 输入规划层连续 `PlannedTrajectory`,优先使用经过执行侧定时处理后的 `ExecutionTrajectory`。
- 输入 `servoPeriodSeconds`、`speedRatio`。
- 输出最终真实发送的 `8ms` 点列。
- 输出与该点列一致的触发绑定结果。
- 输出请求倍率、最终倍率和历史倍率改写次数等诊断信息;当前语义下最终倍率等于请求倍率、改写次数为 0。
### 7.2 最终请求倍率队列
最终队列按固定时间轴语义生成:
- `sendTime = sampleIndex * 0.008`
- `trajectoryTime = min(sendTime * speedRatio, durationSeconds)`
- 关节目标从连续样条直接取点,并从 `rad` 转为 J519 需要的 `deg`
该队列就是本次请求倍率下的最终发送队列,后续只做校验和触发绑定,不再进行二次滤波或倍率搜索。
### 7.3 校验失败策略
当最终队列的离散 `vel/acc/jerk` 校验失败时:
- 不修改规划层轨迹。
- 不修改 `planning_speed_scale`。
- 不放宽校验阈值。
- 不降低本次 `speedRatio`。
- 直接拒绝执行并保留首个超限诊断。
这样可以避免把 `speedRatio` 误实现成安全优化器。现场若希望从 `planning_speed_scale = 0.9` 的 10s 轨迹降到接近 `planning_speed_scale = 0.7` 的执行效果,应显式设置类似 `speedRatio = 0.8`,而不是让系统在失败后自动猜一个更低倍率。
## 8. 逐周期约束校验
最终发送队列必须在执行前通过离散检查。
### 8.1 校验对象
校验对象必须是最终真实发送的 `8ms` 点列,而不是:
- 规划层原始稠密轨迹,
- 中间候选插值轨迹,
- 或平均时长推导。
### 8.2 校验指标
保留现有逐周期离散差分口径:
- 速度
- 加速度
- Jerk
并继续使用当前机器人模型和运行时限值:
- `velocity_eff = velocity_base`
- `acceleration_eff = acceleration_base * acc_limit`
- `jerk_eff = jerk_base * jerk_limit`
### 8.3 校验失败的处理
飞拍链路按当前修正语义采用“确定倍率 + 失败拒绝”:
- 若请求倍率下失败,立即拒绝执行。
- 不进行执行侧自动倍率搜索。
- 调用方可以选择降低 `planning_speed_scale` 重新规划,或显式设置更低的 `speedRatio` 后再次执行。
拒绝执行时必须输出:
- 首次失败的轴
- 时间窗
- 指标类型
- `actual / limit / ratio`
- 请求 `speedRatio`
- 当前 `finalSpeedRatio`,正常等于请求值
## 9. 触发时序与绑定
### 9.1 保持现有语义
以下语义不变:
- `TriggerTimeline`
- `ShotEvents`
- `TriggerSampleIndexOffsetCycles`
- `io_keep_cycles`
### 9.2 绑定对象改为最终发送队列
当前实现中,触发通常绑定到由回采样生成的 `samples`。
本次改为:
- 先生成最终发送队列。
- 再用该最终队列做 `TriggerSampleBinder.Bind(...)`。
这样可以保证:
- 触发绑定和真实发送完全一致。
- 导出工件中的 `ShotEvents.json` 与真实执行时序一致。
- 触发点可被精确追溯到本次请求倍率下的最终发送点索引。
### 9.3 关于触发数量与顺序
请求倍率改变最终发送时长时:
- 触发数量不能变。
- 触发顺序不能变。
- 触发在最终发送队列中的绑定索引应随时间轴拉长后移或保持可解释等价。
## 10. 运行时职责调整
本次需要显式收紧 `FanucControllerRuntime` 的职责。
### 10.1 改造前
`FanucControllerRuntime.SendDenseTrajectory(...)` 当前同时负责:
- 读取 `DenseJointTrajectory`
- 根据 `_speedRatio` 回采样
- 绑定触发
- 生成命令队列
- 写出 `ActualSend*` 工件
### 10.2 改造后
`FanucControllerRuntime` 只负责:
- 接收已经准备好的最终发送队列
- 接收与该队列一致的触发绑定结果
- 预构建命令队列并交给 J519 客户端
- 记录真实发送过程与导出工件
不再负责:
- 按 `_speedRatio` 从 `DenseJointTrajectory` 临场生成发送点
这样做的目的是让:
- 运行时负责发送。
- 执行准备层负责离散稳定性。
两层边界更清晰,也更利于测试。
## 11. 导出工件与日志
### 11.1 旧规划导出和实发诊断必须分开
`saveTrajectory` 目录中存在两类不同用途的文件,不能再混成同一条时间轴:
第一类是旧格式规划导出,用于和旧 RVBUST 导出的规划轨迹、离线分析脚本做对比。它们只受 `planning_speed_scale` 和规划/整形参数影响,不受运行时 `speedRatio` 影响:
- `JointTraj.txt`
- `JointDetialTraj.txt`
- `CartTraj.txt`
- `CartDetialTraj.txt`
- `JointDetialTraj.analysis.txt`
第二类是实发诊断,用于观察当前 `speedRatio` 下真实准备发送的 8ms 队列和触发绑定。它们必须全部从最终发送队列生成:
- `ActualSendJointTraj.txt`
- `ActualSendTiming.txt`
- `ActualSendJerkStats.txt`
- `ShotEvents.json`
不允许再出现:
- 旧规划导出被运行时 `speedRatio` 改写,
- `ActualSend*` 基于一套样本,
- 真实发送队列又基于另一套样本。
### 11.2 推荐新增日志字段
每次飞拍执行建议至少记录:
- 请求 `speedRatio`
- 规划层轨迹时长
- 最终发送队列点数
- 最终发送队列是否通过校验
- 若失败,首个失败窗口的:
- 关节轴
- 时间区间
- 指标类型
- `actual`
- `limit`
- `ratio`
- 倍率改写次数,当前正常为 0
- 最终采用的执行时长
- 最终发送队列点数
- 最终触发绑定数量
- 最终校验通过结论
### 11.3 日志级别建议
- `Information`
- 记录执行请求、最终采用结果、最终通过结论
- `Warning`
- 记录请求倍率下最终发送队列校验失败
- `Debug`
- 记录最终发送队列的时间映射与详细差分统计
## 12. 验收口径
### 12.1 功能验收
- `speedRatio` 可在线设置。
- 该值对下一次启动的飞拍、普通轨迹或 `MoveJoint` 生效。
- 不需要修改 `planning_speed_scale`。
### 12.2 约束验收
在 `speedRatio = {1.0, 0.8, 0.5}` 下:
- 最终发送队列逐周期 `vel/acc/jerk` 不超限。
- 不出现固定区间正负交替的大幅 Jerk 尖峰。
### 12.3 工件与日志验收
- `JointTraj.txt / JointDetialTraj.txt / CartTraj.txt / CartDetialTraj.txt` 不随运行时 `speedRatio` 改变。
- `ActualSend*` 文件能反映最终真实发送点位与时间映射。
- 日志能定位请求倍率、最终发送队列和校验失败窗口。
### 12.4 回归验收
- `speedRatio = 1.0` 时不退化。
- 触发事件数量不异常漂移。
- 触发绑定顺序不异常漂移。
## 13. 测试方案
### 13.1 单元测试
建议新增:
- 飞拍执行侧最终发送序列生成器测试
- `speedRatio` 非法边界值测试
- 请求倍率下队列超限时拒绝执行测试
- `speedRatio = 1.0 / 0.8 / 0.5` 的逐周期限值通过测试
- `planning_speed_scale = 0.9` 加 `speedRatio = 0.8` 的执行时间轴等价于约 `0.72` 规划倍率的回归测试
- 普通轨迹和 `MoveJoint` 在倍率乘积相同时生成等价或可解释等价的稠密执行轨迹
- 触发绑定始终与最终发送队列一致的测试
### 13.2 编排测试
建议补充:
- `ExecuteFlyShotTraj` 进入运行时前,已经拿到最终发送队列
- `FanucControllerRuntime` 不再自行按 `_speedRatio` 对飞拍轨迹回采样
- `ExecuteTrajectory` 和 `MoveJoint` 的调速在规划/生成阶段完成,运行时不再做临场倍率重采样
- `SaveTrajectoryInfo` 的旧格式规划导出不随运行时 `speedRatio` 改变,而 `ActualSend*` 仍随运行时 `speedRatio` 改变
### 13.3 集成与黄金样本
建议至少覆盖:
- `UTTC_MS11`
- `speedRatio = 1.0`
- `speedRatio = 0.8`
- `speedRatio = 0.5`
检查项:
- 最终发送点数
- 最终发送时长
- `ActualSendTiming`
- `ActualSendJointTraj`
- `ActualSendJerkStats`
- `ShotEvents` 绑定结果
- 是否消除当前已知 Jerk 尖峰
## 14. 代码落点建议
建议实现边界如下:
### 14.1 `src/Flyshot.Core.Planning/Sampling/`
新增飞拍执行侧最终发送序列生成器及相关结果类型,负责:
- 候选队列构建
- 离散校验
- 触发绑定输入准备
### 14.2 `src/Flyshot.ControllerClientCompat/`
在飞拍执行准备阶段调用该生成器,把最终发送队列放入执行结果或新的执行上下文对象。
普通轨迹规划入口和 `MoveJoint` 临时轨迹生成入口可以读取当前 `speedRatio`,并在进入运行时前折算成已调速的稠密轨迹。
### 14.3 `src/Flyshot.Runtime.Fanuc/FanucControllerRuntime.cs`
调整 `SendDenseTrajectory(...)`:
- 不再自行根据 `_speedRatio` 从 `DenseJointTrajectory` 临场重采样
- 直接消费已经构建好的最终发送队列
### 14.4 `src/Flyshot.ControllerClientCompat/FlyshotTrajectoryArtifactWriter.cs`
`FlyshotTrajectoryArtifactWriter` 接收两份视图:
- 旧格式规划导出视图:写 `JointTraj.txt`、`JointDetialTraj.txt`、`CartTraj.txt`、`CartDetialTraj.txt`
- 实发诊断视图:写 `ActualSendJointTraj.txt`、`ActualSendTiming.txt`、`ActualSendJerkStats.txt`、`ShotEvents.json`
### 14.5 `tests/Flyshot.Core.Tests/`
补飞拍专项单测、UTTC 回归和工件一致性测试。
## 15. 风险与注意事项
### 15.1 不能把 speedRatio 做成安全优化器
对外语义必须保持:
- 用户请求的是某个 `speedRatio`
- 系统按这个 `speedRatio` 构建最终发送队列
- 若队列超限,则拒绝执行并要求调用方显式选择新的规划倍率或执行倍率
不能因为校验失败就在执行侧自动把 `0.8` 改成 `0.2`;这会让 `speedRatio` 和 `planning_speed_scale` 的关系失去可解释性。
### 15.2 不能让触发绑定脱离最终队列
只要最终发送点数变化,触发绑定就必须重新基于最终队列计算,否则工件与执行会再次分叉。
### 15.3 `speedRatio = 1.0` 必须优先做回归保护
这是最容易被“执行侧重构”误伤的路径,必须作为首要回归项。
## 16. 结论
本次飞拍调速设计的核心结论是:
- `planning_speed_scale` 继续只属于规划层。
- `speedRatio` 是运行时统一执行倍率,并且只对下一次启动的运动任务生效。
- 普通轨迹和 `MoveJoint` 允许把 `speedRatio` 前移到规划或临时轨迹生成阶段,运行时只发送已生成好的 `8ms` 点列。
- 飞拍执行必须先生成最终 `8ms` 发送队列,再对该队列做逐周期 `vel/acc/jerk` 校验。
- `speedRatio` 按 `trajectoryTime = sendTime * speedRatio` 确定性拉长执行时间轴,`finalSpeedRatio` 不应被自动改写。
- 若请求倍率下最终队列不满足约束,则拒绝执行并提示降低 `planning_speed_scale` 重新规划或显式设置更低 `speedRatio`。
- 运行时不再对飞拍轨迹做发送前临场回采样。
- 旧格式规划导出必须保持不受运行时 `speedRatio` 影响;`ActualSend*` 和 `ShotEvents.json` 必须统一到真实发送队列上。
该方案可以在不放宽校验阈值、不中断现有触发语义的前提下,把 `speedRatio` 调速从“发送阶段经验插值”收敛为“规划/准备阶段完成、运行时确定发送”的执行机制。

View File

@@ -0,0 +1,70 @@
# UTTC_MS11 Legacy Fit 计划
## 目标
把 `Rvbust/前两个点正常 飞拍失败的运行` 中的旧 1x 轨迹拟合逻辑收敛到当前 replacement 实现里,让 `UTTC_MS11` 在新系统中尽量复现旧系统的轨迹时间轴和中间点形状。
当前已确认的事实:
- `Config/RobotConfig.json` 里的 `UTTC_MS11` 示教点与旧样本一致。
- 旧 `1倍速度 角度坐标点/waypoint.txt` 已给出 20 个示教点的 legacy 时间节点。
- 旧 1x 的节点时间与当前规划时间存在稳定比例,约为 `0.742277`。
- 当前运行时 `ApplySmoothStartStopTiming` 会再次改写时间轴,这会破坏旧 waypoint time 的拟合结果。
## 拟合策略
优先分两层处理,不把不同问题混成一个旋钮:
1. 时间轴拟合
- 先把 `UTTC_MS11` 的规划时间拉回旧 1x 的节点时间。
- 通过 `planning_speed_scale` 复现旧 `waypoint.txt` 的时间比例。
- 对 legacy-fit 轨迹,禁止运行时二次平滑起停时间重映射。
2. 空间曲线拟合
- 保持原始示教点不变。
- 先用当前插补器 + legacy 时间轴做第一版对齐。
- 如果中间点仍和旧轨迹差异明显,再用旧 `JointDetialTraj.txt` 在 knot 附近反推速度/加速度,升级为 Hermite 拟合。
## 计划分解
### 1. 配置层
- 为 `RobotConfig.json` 增加明确的 legacy-fit 运行开关。
- 当前现场的 `UTTC_MS11` 显式启用:
- `planning_speed_scale = 0.742277`
- `smooth_start_stop_timing = false`
- 保留默认行为为兼容旧实现,以免影响其他轨迹。
### 2. 编排层
- `ControllerClientTrajectoryOrchestrator` 读取运行配置后,按开关决定是否调用 `ApplySmoothStartStopTiming`。
- 缓存键必须包含该开关,避免 legacy-fit 和普通飞拍共用一份结果。
- `saveTrajectory` / `IsFlyshotTrajectoryValid` 仍然输出规划结果,只是 legacy-fit 轨迹不再被二次改写时间轴。
### 3. 运行层
- `FanucControllerRuntime` 继续使用 8ms 物理发送周期。
- DenseSend 实发点数仍按 `duration / (8ms * speedRatio)` 计算。
- 终点要保留完整落点,不因为非整周期而丢掉最后一个点。
### 4. 测试层
- 增加配置测试,确认新开关可解析,默认值不破坏旧行为。
- 增加编排测试,确认 UTTC_MS11 的规划时刻与旧 `waypoint.txt` 一致。
- 增加运行测试,确认 legacy-fit 目录能写出稳定的 DenseSend 诊断文件。
- 继续保留原有平滑起停测试,作为“显式开启平滑时”的回归保护。
## 验收标准
- `UTTC_MS11` 的 waypoint time 与旧 `waypoint.txt` 对齐。
- `UTTC_MS11` 运行时不再额外套一层平滑重映射。
- DenseSend 输出稳定,且不再受旧 bin 目录残留影响。
- 现有默认轨迹和非 UTTC 场景不被破坏。
## 后续可能的第二阶段
如果时间轴对齐后,中间点仍和旧轨迹有明显偏差,再做第二阶段:
- 从旧 `JointDetialTraj.txt` 提取 knot 附近速度/加速度。
- 用 Hermite / quintic Hermite 继续逼近旧曲线形状。
- 将空间曲线拟合与时间轴拟合分开验收。

View File

@@ -0,0 +1,160 @@
# MoveJoint 失败样本六轴限值与 ActualSendJerkStats 对比
记录时间:2026-05-05
## 1. 目的
本文档固定记录以下三类证据,避免后续继续混用测试基线、旧文档结论和当前运行目录中的真实模型数据:
- 当前运行目录 `.robot` 模型中的六轴基础 `velocity / acceleration / jerk`
- 现场示教器读取到的六轴 `velocity / acceleration / jerk`
- 当前运行目录 `RobotConfig.json` 中的 `acc_limit / jerk_limit`
- 当前失败样本 `ActualSendJerkStats.txt` 中的逐轴实发跃度峰值
本次样本对应目录:
- `.robot`:`src/Flyshot.Server.Host/bin/Debug/net8.0/Config/Models/LR_Mate_200iD_7L.robot`
- 配置:`src/Flyshot.Server.Host/bin/Debug/net8.0/Config/RobotConfig.json`
- 实发跃度:`src/Flyshot.Server.Host/bin/Debug/net8.0/Config/Data/move-joint/DenseSend/20260505_203416_563/ActualSendJerkStats.txt`
- 抓包:`src/Flyshot.Server.Host/bin/Debug/net8.0/Config/Data/move-joint/DenseSend/20260505_203416_563/移动点 跃度过大.pcap`
## 2. 当前运行模型的真实六轴限值
当前仓库运行时通过 `RobotModelLoader.LoadProfile(...)` 从 `.robot` 中读取每轴 `limit.velocity / limit.acceleration / limit.jerk`,然后只对加速度和 jerk 叠加 `RobotConfig.json` 的全局倍率:
- `velocity_eff = velocity_base`
- `acceleration_eff = acceleration_base * acc_limit`
- `jerk_eff = jerk_base * jerk_limit`
当前运行目录 `RobotConfig.json` 中:
- `acc_limit = 0.74`
- `jerk_limit = 0.74`
按当前运行目录真实模型解出的六轴基础值与生效值如下:
| Joint | vel_base | acc_base | jerk_base | vel_eff | acc_eff | jerk_eff(rad/s^3) | jerk_eff(deg/s^3) |
| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
| Joint1 | 6.45 | 26.90 | 224.22 | 6.45 | 19.9060 | 165.9228 | 9506.6762 |
| Joint2 | 5.41 | 22.54 | 187.86 | 5.41 | 16.6796 | 139.0164 | 7965.0530 |
| Joint3 | 7.15 | 29.81 | 248.46 | 7.15 | 22.0594 | 183.8604 | 10534.4249 |
| Joint4 | 9.59 | 39.99 | 333.30 | 9.59 | 29.5926 | 246.6420 | 14131.5457 |
| Joint5 | 9.51 | 39.63 | 330.27 | 9.51 | 29.3262 | 244.3998 | 14003.0771 |
| Joint6 | 17.45 | 72.72 | 606.01 | 17.45 | 53.8128 | 448.4474 | 25694.1434 |
重要结论:
- 当前运行目录中,`Joint1.jerk_base` 不是测试基线里常见的 `272.7`,而是 `224.22`。
- 因此当前样本的 `Joint1` 生效 jerk 上限应按 `224.22 * 0.74 = 165.9228 rad/s^3` 计算。
## 3. 示教器读取到的实际机器人限值
2026-05-06 现场从机器人示教器读取到的速度、加速度与加加速度限制如下。示教器显示口径为角度制;下表同时记录换算后的弧度制,便于与 `.robot` / `RobotProfile` 中的基础限值直接对照。
换算公式:
- `rad = deg * π / 180`
| Joint | velocity(deg/s) | velocity(rad/s) | acceleration(deg/s^2) | acceleration(rad/s^2) | jerk(deg/s^3) | jerk(rad/s^3) |
| --- | ---: | ---: | ---: | ---: | ---: | ---: |
| Joint1 | 370 | 6.457718 | 1541.667 | 26.907165 | 12847.223 | 224.226341 |
| Joint2 | 310 | 5.410521 | 1291.667 | 22.543842 | 10763.889 | 187.865303 |
| Joint3 | 410 | 7.155850 | 1708.333 | 29.816036 | 14236.111 | 248.467010 |
| Joint4 | 550 | 9.599311 | 2291.667 | 39.997135 | 19097.223 | 333.309419 |
| Joint5 | 545 | 9.512044 | 2270.833 | 39.633513 | 18923.611 | 330.279318 |
| Joint6 | 1000 | 17.453293 | 4166.667 | 72.722058 | 34722.223 | 606.017115 |
这组数据的含义:
- 示教器读取值与当前运行 `.robot` 中的 `velocity_base / acceleration_base / jerk_base` 基本一致,可作为实际机器人基础限值证据。
- 它还没有叠加当前 `RobotConfig.json` 的 `acc_limit = 0.74` 与 `jerk_limit = 0.74`;若用于本次失败样本比较,仍应使用第 2 节中的 `acc_eff / jerk_eff` 作为生效上限。
## 4. ActualSendJerkStats 的单位边界
`ActualSendJerkStats.txt` 的真实输入不是弧度,而是 J519 下发用的角度制关节目标:
1. `SampleDenseJointTrajectoryDegrees(...)` 先把轨迹点从 `rad` 转成 `deg`
2. `BuildDenseSendJointRow(...)` 把这组角度制关节写入 `ActualSendJointTraj.txt`
3. `BuildDenseSendJerkRow(...)` 再直接基于这组角度制关节做三阶差分
2026-05-06 之后,`ActualSendJointTraj.txt` 第一列和 `ActualSendJerkStats.txt` 的 `dt` 都使用 J519 实发时间;若需要查看被 `speed_ratio` 缩放后的原轨迹采样时间,应读取同目录的 `ActualSendTiming.txt`。因此当前这份 `ActualSendJerkStats.txt` 的逐轴跃度应按以下方式理解:
- 文本中的数值口径:`deg/s^3`
- 若要与 `.robot` / `RobotProfile` 中的 jerk limit 比较,需要先换算为 `rad/s^3`
- 换算公式:`jerk_rad = jerk_deg * π / 180`
## 5. 全文件逐轴最大跃度对比
扫描整份 `ActualSendJerkStats.txt` 后,各轴绝对值最大跃度如下:
| Joint | peak window(s) | peak line | peak actual(deg/s^3) | peak actual(rad/s^3) | jerk_eff(rad/s^3) | peak/limit |
| --- | --- | ---: | ---: | ---: | ---: | ---: |
| Joint1 | `1.056 -> 1.064` | 133 | 21868.115990 | 381.670625 | 165.922800 | 2.3003 |
| Joint2 | `1.056 -> 1.064` | 133 | 40.271793 | 0.702875 | 139.016400 | 0.0051 |
| Joint3 | `1.056 -> 1.064` | 133 | 98.314401 | 1.715910 | 183.860400 | 0.0093 |
| Joint4 | `1.056 -> 1.064` | 133 | 0.207266 | 0.003617 | 246.642000 | 0.0000 |
| Joint5 | `1.056 -> 1.064` | 133 | 26.759688 | 0.467045 | 244.399800 | 0.0019 |
| Joint6 | `1.056 -> 1.064` | 133 | 2.328736 | 0.040644 | 448.447400 | 0.0001 |
结论非常明确:
- 全文件范围内,只有 `Joint1` 的实发跃度显著超过当前生效 jerk 上限。
- 其余 5 个轴即使取全文件峰值,也远低于各自当前生效 jerk limit。
- 当前样本本质上是一个“J1 主导”的跃度问题,而不是六轴普遍同时逼近上限。
## 6. 报警窗口逐轴对比
结合抓包与 J519 序号,报警前最后一个关键窗口是:
- `seq=41552`
- 轨迹时间窗口:`0.296 -> 0.304s`
- `ActualSendJerkStats.txt` 行号:38
该窗口逐轴跃度如下:
| Joint | alarm window jerk(deg/s^3) | alarm window jerk(rad/s^3) | jerk_eff(rad/s^3) | alarm/limit |
| --- | ---: | ---: | ---: | ---: |
| Joint1 | -20395.713579 | 355.972355 | 165.922800 | 2.1454 |
| Joint2 | -37.560252 | 0.655550 | 139.016400 | 0.0047 |
| Joint3 | 91.694793 | 1.600376 | 183.860400 | 0.0087 |
| Joint4 | -0.193310 | 0.003374 | 246.642000 | 0.0000 |
| Joint5 | 24.957931 | 0.435598 | 244.399800 | 0.0018 |
| Joint6 | 2.171939 | 0.037907 | 448.447400 | 0.0001 |
该窗口的直接结论与全局扫描一致:
- 机器人开始报警的 `0.296 -> 0.304s` 窗口里,真正越限的仍然只有 `Joint1`
- `Joint1` 在报警窗口内已经达到当前生效 jerk limit 的 `2.1454x`
- 其余 5 轴在同一窗口仍远低于生效 jerk 上限
## 7. 报警窗口与全局峰值窗口的关系
本次样本不能简单理解为“最大峰值出现的位置就是首次报警位置”。
当前证据表明:
- 首次报警相关窗口在 `0.296 -> 0.304s`
- 全文件最大的 J1 跃度峰值出现在更后面的 `1.056 -> 1.064s`
这说明至少有两件事需要分开:
1. 机器人第一次进入异常态时,`Joint1` 已经在 `0.296 -> 0.304s` 超限约 `2.15x`
2. 即便忽略第一次报警,后续轨迹中仍存在更高的 J1 跃度峰值,说明当前 `MoveJoint` 临时轨迹整体都偏激,不只是单个孤立点异常
## 8. 当前可落地的结论
基于当前运行目录的真实模型、配置和实发跃度文件,本次失败样本可以先固定为下面这组结论:
- 当前运行模型 `Joint1.jerk_base = 224.22`,不是 `272.7`
- 现场示教器读取到的 `Joint1.jerk = 12847.223 deg/s^3 = 224.226341 rad/s^3`,与当前运行模型基础值一致
- 当前样本 `jerk_limit = 0.74`,所以 `Joint1.jerk_eff = 165.9228 rad/s^3`
- `ActualSendJerkStats.txt` 需要按 `deg/s^3` 理解,再换算成 `rad/s^3` 后与模型 jerk limit 对比
- 无论看报警窗口还是看全文件峰值,越限主体都只有 `Joint1`
- 报警窗口 `0.296 -> 0.304s` 中,`Joint1` 已经约为当前生效 jerk 上限的 `2.1454x`
- 全文件最大峰值窗口 `1.056 -> 1.064s` 中,`Joint1` 约为当前生效 jerk 上限的 `2.3003x`
因此当前最合理的根因指向仍然是:
- `MoveJoint` 临时轨迹生成得过于激进
- 当前问题首先应按 `Joint1` 的 jerk 约束失配来处理
- 暂时没有证据支持“六轴普遍一起逼近限制”或“网络链路导致跃度统计失真”这类解释

View File

@@ -0,0 +1,592 @@
# 轨迹规划时长差异调查记录
## 背景
当前新 C# 规划链路在不额外缩放规划约束时,部分真实现场轨迹会比旧 RVBUST/FlyingShot 导出的 `JointTraj.txt` 更短。
最典型现象:
- 真实 `Rvbust/uttc-20260428/Data/JointTraj.txt`:`UTTC_MS11` 总时长约 `7.403046s`。
- 新 C# 当前默认规划输出:`src/Flyshot.Server.Host/bin/Debug/net8.0/Config/Data/UTTC_MS11/JointTraj.txt` 总时长约 `5.495112s`。
- 实体机复核确认:修改运行时 `speed_ratio` 不影响 `IsFlyshotTrajectoryValid` / `SaveTrajectoryInfo` 生成的 `JointTraj.txt` 规划时长。
因此,本问题不应继续归因到运行时 `speed_ratio`,而应归到规划阶段的有效关节约束来源。
## 已确认事实
1. `speed_ratio` 是运行执行倍率。
UTTC 抓包和实体机测试都显示,`speed_ratio=0.7` 会拉伸 J519 实际下发时间和包数,但不会改变已生成的 `JointTraj.txt` 规划时间轴。
2. `JointTraj.txt` 是规划结果点位。
`saveTrajectory` / `SaveTrajInfo` / `IsFlyshotTrajectoryValid(saveTrajectory=true)` 生成的 `JointTraj.txt` 表示规划后的 sparse waypoint 时间轴,不是上传的原始飞拍路径,也不是 J519 逐周期下发点。
3. UTTC_MS11 的差异是整条时间轴等比例缩放。
对 `UTTC_MS11`,真实时间和当前 C# 默认规划时间之间的比例在所有 waypoint 上都一致:
```text
C#默认规划时间 / 真实规划时间 = 5.495112 / 7.403046 = 0.742277
```
这说明路点顺序、相对分段时间和 ICSP 主要逻辑基本一致,差异更像是规划时传入的有效 `vel/acc/jerk` joint limits 存在整体倍率差异。
4. 现场配置中没有找到显式倍率字段。
已检查现场现有配置,未发现类似 `planning_speed_scale` 或等价字段保存了 `0.742277`、`0.7`、`0.9` 等规划倍率值。
## 样本对比
| 样本 | 真实 `JointTraj.txt` 时长 | 当前/已有新规划时长 | 等效倍率 `新规划/真实` | 说明 |
| --- | ---: | ---: | ---: | --- |
| `UTTC_MS11` | `7.403046s` | `5.495112s` | `0.742277` | 所有 waypoint 时间均为同一比例 |
| `UTTC_MS11_TEST01` | `7.805885s` | `5.814370s` | `0.744870` | `20260428 多点` 新增 1 个路径点后仍几乎是整条时间轴等比例缩放 |
| `EOL10_EAU_0` | `14.849788s` | `10.489800s` | `0.706394` | 同样表现为新规划偏快 |
| `EOL9_EAU_0` / `EOL9_EAU_90` | `6.400851s` | `5.651140s` | `0.882873` | `EOL9 EAU 0` 与 `EOL9 EAU 90` 的真实 `JointTraj.txt` 文件一致 |
| `EOL9_EAU_90` | `6.400851s` | `6.471610s` | `1.011055` | 使用 `speedRatio=0.9 + self-adapt-icsp` 的旧离线结果已接近真实 |
这些样本说明差异不是 `UTTC_MS11` 的个案,也不是一个可以全局写死的常数。不同真实样本对应的等效规划倍率不同。
## `20260428 多点` 新样本对比
2026-04-30 追加现场新样本:
```text
../Rvbust/20260428 多点/RobotConfig.json
../Rvbust/20260428 多点/JointTraj.txt
../Rvbust/20260428 多点/JointDetialTraj.txt
```
该样本中的飞拍程序名为:
```text
UTTC_MS11_TEST01
```
配置摘要:
```text
waypoints=21
shot_flags=21
acc_limit=1
jerk_limit=1
```
实机导出的 `JointTraj.txt`:
```text
rows=21
duration=7.805885s
```
用当前 C# `ICspPlanner`、同一个 `LR_Mate_200iD_7L.robot`、同一份 `RobotConfig.json` 规划:
```text
rows=21
duration=5.814370s
C# / 实机 = 0.744870
```
逐点/逐段统计:
```text
point_ratio_std = 2.18e-7
segment_ratio_std = 9.0e-7
max_joint_diff = 5.0e-7 rad
```
这说明:
1. 新样本的路点关节值与 C# 输入基本完全一致,不是解析错点或单位错位。
2. 新增 1 个路径点后,C# 与旧系统仍然保持几乎严格的整条时间轴等比例差异。
3. `UTTC_MS11_TEST01` 的倍率 `0.744870` 与原 `UTTC_MS11` 的 `0.742277` 非常接近,进一步支持“同一类 UTTC 现场导出使用了一组更保守的 effective JointLimits”这一判断。
和原 `UTTC_MS11` 对比:
```text
原 UTTC_MS11 实机 rows=20 duration=7.403046s
新 UTTC_MS11_TEST01 实机 rows=21 duration=7.805885s
新增路径点后实机时长增加 0.402839s
```
当前观察不到新增点导致规划形状或局部段比例失真;它更像是在同一套旧系统规划约束下正常增加了一段路径时间。
## `20260430.pcap` 初始化抓包复核
2026-04-30 继续复核现场提供的完整初始化抓包:
```text
../Rvbust/20260428 多点/20260430.pcap
```
抓包总览:
```text
packet_count=4821
tcp_payload_bytes=35302
udp_payload_bytes=451946
```
主要有效负载会话为:
```text
UDP 192.168.10.11:60015 -> 192.168.10.10:56118 260700B
UDP 192.168.10.11:60015 -> 192.168.10.10:48455 127116B
UDP 192.168.10.10:48455 -> 192.168.10.11:60015 62088B
TCP 192.168.10.11:10010 -> 192.168.10.10:42106 35102B
TCP 192.168.10.11:10012 -> 192.168.10.10:33528 106B
TCP 192.168.10.10:33528 -> 192.168.10.11:10012 94B
```
全包搜索以下明文关键字没有命中:
```text
Joint / joint / Limit / limit / vel / acc / jerk / Speed / speed /
Robot / JSON / Traj / ratio / Ratio / GetJoint / SetJoint
```
`TCP 10012` 命令通道按 `doz + length + message_id + body + zod` 解码后,只看到以下初始化/程序命令:
| 方向 | 消息号 | 含义 | 请求体/结果 |
| --- | ---: | --- | --- |
| C->R | `0x0001` | 未知握手 | 空请求 |
| R->C | `0x0001` | 未知握手响应 | `result=0` |
| C->R | `0x2000` | 未知版本/状态查询 | 空请求 |
| R->C | `0x2000` | 未知版本/状态响应 | 包含 `0.6.0` 字符串 |
| C->R | `0x2100` | `ResetRobot` | 空请求 |
| R->C | `0x2100` | `ResetRobot` 响应 | `result=0` |
| C->R | `0x2003` | `GetProgramStatus("RVBUSTSM")` | 程序名 `RVBUSTSM` |
| R->C | `0x2003` | 程序状态响应 | `result=0, status=1` |
| C->R | `0x2102` | `StartProgram("RVBUSTSM")` | 程序名 `RVBUSTSM` |
| R->C | `0x2102` | 启动响应 | `result=0` |
没有看到:
- `0x2207 SetSpeedRatio`
- `0x2206 GetSpeedRatio`
- `0x2200/0x2201 GetTcp/SetTcp`
- `0x2208/0x2209 GetIo/SetIo`
- 任何疑似 `JointLimits / velocity / acceleration / jerk` 的参数帧
`TCP 10010` 状态通道只有机器人侧到上位机的状态帧:
```text
390 个 90B 状态帧
1 个 2B 连接前导
```
这些帧与已逆向的 `pose[6] + joint[6] + external_axes[3] + raw_tail_words[4]` 状态布局一致,不携带规划约束。
`UDP 60015` J519 通道只出现既有三类长度:
```text
C->R 8B 初始化包 1 个
C->R 64B 目标关节命令包 970 个
R->C 132B 反馈包 2938 个
```
没有出现其他长度的 UDP 参数帧。64B 命令包是 J519 逐周期目标关节/IO 命令,132B 是机器人反馈;这条链路承载的是执行期 streaming motion,而不是旧 RVBUST 规划器的 joint limit 配置。
阶段结论:
```text
20260430.pcap 只覆盖机器人侧 10010 / 10012 / 60015 通信。
它没有 50001/TCP+JSON,也没有 ControllerServer/Python 客户端到旧服务端的配置调用。
因此,这份抓包看不到旧规划阶段的 effective JointLimits。
```
这并不否定“旧系统规划瞬间存在更保守的 effective JointLimits”这一方向;它只说明这份初始化抓包不是抓取该信息的位置。若要抓到这类限制,需要抓旧服务端内部 `_GetJointLimits/_SetJointLimits`,或者抓上层 Python/GUI 与 ControllerServer 之间的配置/规划调用,而不是只抓机器人控制柜侧的执行链路。
## `all-50001.pcap` 本机 50001 抓包复核
2026-04-30 追加复核本机所有网卡抓包:
```text
../Rvbust/20260428 多点/all-50001.pcap
SHA256=C3543F314AE446CABA8E2097EFAFB36F39DD73FFE166F051A1F9387CFD15990F
```
该文件由 `tcpdump -i any` 生成,pcap linktype 为 `113`,即 Linux cooked capture。按 SLL 头解析后,确认抓到了本机到本机的 `50001` TCP JSON 通信:
```text
192.168.1.100:35814 -> 192.168.1.100:50001 217B payload
192.168.1.100:50001 -> 192.168.1.100:35814 91B payload
```
客户端到服务端的完整 JSON 命令序列为:
```json
{"reply_from_client":true}
{"cmd":"SetUpRobot","robot_name":"FANUC_LR_Mate_200iD_7L"}
{"cmd":"IsSetUp"}
{"cmd":"SetActiveController","sim":false}
{"cmd":"Connect","ip":"192.168.10.11"}
{"buffer_size":8,"cmd":"EnableRobot"}
```
服务端返回为:
```json
{"test_from_server": true}
{"res": true}
{"res": true}
{"res": true}
{"res": true}
{"res": true}
```
这说明本次抓包确实覆盖到了旧 `50001` 控制链路,但当前只包含机器人初始化、连接和使能流程。里面没有看到:
- `ExecuteFlyShotTraj`
- `SaveTrajInfo`
- `IsFlyShotTrajValid`
- `GetJointLimits / SetJointLimits`
- `SetVelocityLimit / SetAccelerationLimit / SetJerkLimit`
- 任何包含 `acc_limit / jerk_limit / JointLimits / velocity / acceleration / jerk` 的配置 JSON
阶段结论:
```text
all-50001.pcap 已经证明抓包接口选对了;
但这次只抓到了初始化链路,没有抓到规划/保存轨迹那一刻的 50001 请求。
```
该待确认点已由下一节 `all-50001-plan.pcap` 覆盖:后续抓包确实抓到了 `ExecuteFlyShotTraj(save_traj=true,use_cache=false)`,仍未出现规划限制字段。
## `all-50001-plan.pcap` 规划执行抓包复核
2026-04-30 追加复核规划/执行动作期间的本机 50001 抓包:
```text
../Rvbust/20260428 多点/all-50001-plan.pcap
SHA256=311DC45B4789ED11EBEAB7A396E2EE7A16EC8534E20F10127FB43BBAD823C21D
```
该抓包同样是 `tcpdump -i any` 生成的 Linux cooked capture,已按 SLL 头解析。有效 TCP JSON 流为:
```text
192.168.1.100:35814 -> 192.168.1.100:50001 2612B payload
192.168.1.100:50001 -> 192.168.1.100:35814 516B payload
```
客户端到服务端的关键命令序列为:
```json
{"cmd":"ListFlyShotTraj"}
{"cmd":"GetNextListFlyShotTraj","count":0}
{"cmd":"SetSpeedRatio","ratio":0.5}
{"cmd":"ExecuteFlyShotTraj","method":"icsp","move_to_start":true,"name":"UTTC_MS11_TEST01","save_traj":true,"use_cache":false,"wait":true}
{"cmd":"SetSpeedRatio","ratio":1.0}
{"cmd":"ExecuteFlyShotTraj","method":"icsp","move_to_start":true,"name":"UTTC_MS11_TEST01","save_traj":true,"use_cache":false,"wait":true}
{"cmd":"StartUploadFlyShotTraj","name":"UTTC_MS11"}
{"cmd":"UploadFlyShotTraj", "...":"4 批共 20 个 waypoint,每批包含 waypoints / shot_flags / offset_values / addrs"}
{"cmd":"EndUploadFlyShotTraj","name":"UTTC_MS11"}
{"cmd":"ListFlyShotTraj"}
{"cmd":"GetNextListFlyShotTraj","count":0}
```
两次执行请求均为:
```json
{
"cmd": "ExecuteFlyShotTraj",
"method": "icsp",
"move_to_start": true,
"name": "UTTC_MS11_TEST01",
"save_traj": true,
"use_cache": false,
"wait": true
}
```
它们前面的速度倍率分别为:
```text
第一次:SetSpeedRatio ratio=0.5
第二次:SetSpeedRatio ratio=1.0
```
服务端对所有命令均返回:
```json
{"res": true}
```
这份抓包确认了两点:
1. 公开 50001 JSON 链路确实会把 `SetSpeedRatio` 和 `ExecuteFlyShotTraj(save_traj=true,use_cache=false)` 发给旧服务端。
2. 即便覆盖到了实际执行/保存轨迹动作,请求中仍没有出现 `GetJointLimits / SetJointLimits`、`SetVelocityLimit / SetAccelerationLimit / SetJerkLimit`,也没有 `acc_limit / jerk_limit / velocity / acceleration / jerk / JointLimits` 等规划限制字段。
因此,当前能从 50001 抓包确认的是:
```text
规划方法、是否保存轨迹、是否使用缓存、是否等待执行,都会显式发到旧服务端;
速度倍率通过 SetSpeedRatio 单独发到旧服务端;
但 effective JointLimits 没有通过这次公开 50001 JSON 请求显式传入。
```
这进一步收敛了差异来源:如果旧系统规划时确实使用了更保守的 joint limits,它更可能来自旧服务端在 `SetUpRobot("FANUC_LR_Mate_200iD_7L")` 后加载/初始化的内部状态,或来自 GUI/服务端内部私有路径,而不是这次 50001 公开 JSON 在 `ExecuteFlyShotTraj` 请求中传入的字段。
## Joint3/Joint2 couple A/B 测试
2026-04-30 追加测试:为了验证 `.robot` 中 `Joint3` 对 `Joint2` 的 couple 是否是规划时长差异主因,使用 Python ICSP demo 做了多组只读 A/B。
测试模型来自:
```text
flyshot-replacement/Config/Models/LR_Mate_200iD_7L.robot
```
其中 `Joint3` 的 couple 信息为:
```text
q3_kin = q3_raw + q2_kin * 1.0 + 0.0
```
测试变体:
- `raw`:原始 6 轴路点直接规划。
- `replace_q3=q3+q2`:规划输入中把第 3 轴替换为耦合后的运动学角。
- `replace_q3=q3-q2`:反向符号试探,排除符号理解错误。
- `raw+constraint(q3+q2)`:保留原始 6 轴,同时追加虚拟约束轴 `q3+q2`,用 Joint3 的 `vel/acc/jerk` 限值检查。
- `raw+constraint(q3-q2)`:反向符号的虚拟约束轴试探。
结果:
| 样本 | 真实时长 | 最接近变体 | 变体时长 | 变体/真实 | 与真实差值 | 结论 |
| --- | ---: | --- | ---: | ---: | ---: | --- |
| `UTTC_MS11` | `7.403046s` | `raw` | `5.495112s` | `0.742277` | `1.907934s` | couple 变体全部更短,且破坏原本严格等比例关系 |
| `EOL10_EAU_0` | `14.849788s` | `replace_q3=q3+q2` | `10.600711s` | `0.713863` | `4.249077s` | couple 只改善约 `0.11s`,距离真实仍差 `4.25s` |
| `EOL9_EAU_90` | `6.400851s` | `raw+constraint(q3+q2)` | `5.748560s` | `0.898093` | `0.652291s` | couple 约束有小幅影响,但仍不足以解释真实时长 |
关键观察:
1. `UTTC_MS11` 的 `raw` 规划时间和真实时间保持严格等比例,`point_ratio_std=0`、`segment_ratio_std≈0`;加入 couple 后反而出现分段比例波动。
2. `EOL10_EAU_0` 与 `EOL9_EAU_90` 的 couple 变体只带来小幅时长变化,不能解释 10% 到 30% 级别的差异。
3. 因此,当前证据不支持“只要把 Joint3/Joint2 couple 带入 ICSP,就能对齐旧 RVBUST 规划时长”。
阶段结论:
`Joint3` couple 确实是 C# 与 Python demo 当前都没有进入规划约束的缺口,但它不像本轮时长 mismatch 的主因。它更可能影响 FK/运动学边界或少数局部段约束;当前主要时长差异仍更像有效 joint limits、旧系统运行期规划倍率、或 RPS 内部 ICSP 参数来源不同。
## 同模型复核与更可能的差异层
2026-04-30 继续复核:
1. 当前仓库固化的模型与旧 `FlyingShot/FlyingShot/Models/LR_Mate_200iD_7L.robot` 字节哈希一致。
2. `ControllerClientCompatRobotCatalog` 当前会把 `FANUC_LR_Mate_200iD` 和 `FANUC_LR_Mate_200iD_7L` 都映射到 `LR_Mate_200iD_7L.robot`。
3. `LR_Mate_200iD.robot` 短臂模型的前三轴 `vel/acc/jerk` 比 `7L` 更高。用短臂模型试算会让轨迹更短,不会解释“旧系统真实导出更慢”。
模型 A/B:
| 样本 | 真实时长 | `LR_Mate_200iD_7L.robot` | `LR_Mate_200iD.robot` | 结论 |
| --- | ---: | ---: | ---: | --- |
| `UTTC_MS11` | `7.403046s` | `5.495112s` | `5.345600s` | 短臂模型方向更错 |
| `EOL10_EAU_0` | `14.849788s` | `10.489800s` | `10.342456s` | 短臂模型方向更错 |
因此,如果现场确认机器人模型确实一致,差异层就不应继续放在 `.robot` 静态文件本身,而应放在旧服务端规划时的运行态:
- 服务端内部存在 `_GetJointLimits / _SetJointLimits`,说明规划消费的是一份可能被运行期覆写的 `current JointLimits`。
- `ControllerClient.h` 的 `ExecuteFlyShotTraj(..., use_cache=false)` 明确说明旧服务端可以把计算好的轨迹保存在内存中并复用。
- `SaveTrajInfo(name, method)` 没有 `use_cache` 参数,不能仅凭公开头文件判断它一定每次从当前配置重新规划。
当前更合理的解释是:
```text
同一个 .robot
-> SetUpRobot 初始化基础 JointLimits
-> 旧服务端运行期间可能被 _SetJointLimits / 速度倍率联动 / 缓存轨迹 覆盖
-> SaveTrajInfo 或 IsFlyShotTrajValid(save_traj=true) 导出的是真正规划时那份状态
-> 当前 C# 每次用静态 .robot + RobotConfig 重新规划,所以时长更短
```
尤其需要注意:`EOL10_EAU_0` 的 `新规划/真实` 比例为 `0.706394`,接近 `0.7`;`EOL9_EAU_90` 的比例为 `0.882873`,接近 `0.9`。这不像模型误差,更像历史导出时混入了某个运行态速度/限制倍率。`UTTC_MS11` 的 `0.742277` 不等于抓包确认的执行层 `0.7`,所以不能简单把所有样本都归因到 `SetSpeedRatio`,但“运行态规划约束不是静态模型值”仍是目前最强方向。
## 旧服务端与 GUI 二进制复核
2026-04-30 继续从旧系统二进制字符串中复核,重点看公开 Python/HTTP 层没有暴露出来的运行态对象。
### 服务端确实持有 runtime JointLimits
`../FlyingShot/FlyingShot/Python/ControllerServer/ControllerServer.cpython-37m-x86_64-linux-gnu.so` 中能稳定看到以下方法和关键字:
```text
ControllerServer.ControllerServer._GetJointLimits
ControllerServer.ControllerServer._SetJointLimits
ControllerServer.ControllerServer._IsWaypointInJointLimits
ControllerServer.ControllerServer._IsTrajInJointLimits
ControllerServer.ControllerServer._IsTrajInJerkLimits
ControllerServer.ControllerServer._ExecuteFlyShotTraj
ControllerServer.ControllerServer._SaveTrajInfo
ControllerServer.ControllerServer._IsFlyShotTrajValid
SetVelocityLimit
SetAccelerationLimit
SetJerkLimit
GetMaxVelocity
GetMaxAcceleration
GetMaxJerk
m_acc_limit
m_jerk_limit
save_traj_only
use_cache
```
这比公开 `ControllerClient.h` 暴露的信息更多。它说明旧服务端内部不是只把 `.robot` 静态值直接传给 `TrajectoryRnICSP`,而是存在一份可以查询、设置、校验、再用于规划的运行期 `JointLimits`。
### GUI 也直接接触规划约束与保存逻辑
旧 GUI 二进制里也能看到同一条链:
- `../FlyingShot/FlyingShot/Python/GUI/Robot/RobotManager.cpython-37m-x86_64-linux-gnu.so`
- `GetJointLimits`
- `TrajectoryRnICSP`
- `IsTrajInJointLimits`
- `IsTrajInJerkLimits`
- `acc_limit`
- `jerk_limit`
- `../FlyingShot/FlyingShot/Python/GUI/Robot/RobotConfig.cpython-37m-x86_64-linux-gnu.so`
- `SaveTraj`
- `m_acc_limit`
- `m_jerk_limit`
- `../FlyingShot/FlyingShot/Python/GUI/Panels/FlyshotDockPanel.cpython-37m-x86_64-linux-gnu.so`
- `__SaveTraj`
- `IsTrajInJointLimits`
- `IsTrajInJerkLimits`
- `m_acc_limit`
- `m_jerk_limit`
这说明旧 GUI 的“保存轨迹/检查轨迹”路径很可能不是简单调用公开 `ControllerClient.SaveTrajInfo` 后结束,而是直接拿当前 `JointLimits + acc_limit + jerk_limit` 做规划、合法性检查或保存。
### UAES 接口没有显式对齐 JointLimits
`../flyshot-uaes-interface/main.py` 中 `/execute_flyshot/` 的执行路径是:
```text
c.ExecuteFlyShotTraj(name=name, move_to_start=True, method="icsp", save_traj=True)
```
`/set_speedRatio/` 是单独接口:
```text
c.SetSpeedRatio(speed)
```
同时,`../flyshot-uaes-interface/lib/PyControllerClient.cpython-37m-x86_64-linux-gnu.so` 和 `../flyshot-uaes-interface/lib/libControllerClient.so` 中只看到公开客户端侧的:
```text
GetSpeedRatio
SetSpeedRatio
ExecuteFlyShotTraj
SaveTrajInfo
IsFlyShotTrajValid
JointLimits
```
没有看到客户端侧 `GetJointLimits / SetJointLimits` 符号。也就是说,UAES Python 服务本身大概率没有主动把旧服务端的 runtime JointLimits 设置成某个值;如果现场旧导出时的 limits 被改过,更可能来自:
- 旧 GUI 初始化/保存路径;
- 旧服务端内部默认初始化;
- 服务端隐藏 TCP JSON 方法;
- 历史上某次执行/保存后留下的缓存结果。
### 样本文件与配置文件可能不是同一次运行态
新增一个需要警惕的现象:
- `../Rvbust/EOL9 EAU 0/eol9_eau_0.json` 中 `acc_limit=1`、`jerk_limit=1`。
- `../Rvbust/EOL9 EAU 90/eol9_eau_90.json` 中 `acc_limit=0.8`、`jerk_limit=0.8`。
- 但两个目录下保存的真实 `JointTraj.txt` 内容和时长一致。
哈希复核:
```text
EOL9 EAU 0 JointTraj.txt SHA256=DFD8E1130742CFB4ED72F70D0E8CA4E3A16F421E0D0D9D921B9F5177717536EC
EOL9 EAU 90 JointTraj.txt SHA256=DFD8E1130742CFB4ED72F70D0E8CA4E3A16F421E0D0D9D921B9F5177717536EC
eol9_eau_0.json SHA256=354D0D3F71499951976504802C4B2860132D1E4FF753738715A500529CD0BB68
eol9_eau_90.json SHA256=7F854AA227D842CAE734AFA378FEEFA742D797F99FBE536E1B98DF981CD32B27
```
这说明不能默认认为“某个 JSON 文件当前内容”就一定是旁边 `Data/JointTraj.txt` 的生成状态。旧系统的保存文件可能来自缓存、拷贝、历史运行态,或 GUI/服务端中未落盘到该 JSON 的当前 `JointLimits`。
本轮新增证据把方向进一步收敛为:
```text
同一个 .robot 文件本身不是问题核心;
真正影响时长的是旧系统规划瞬间的 effective JointLimits;
但这份状态没有出现在现有配置、机器人侧抓包或 50001 公开 JSON 中。
```
如果未来能直接进入旧服务端进程,仍可在 `SaveTrajInfo` / `IsFlyShotTrajValid(save_traj=true)` 前后抓取 `_GetJointLimits` 返回值,并把它与 `.robot` 原始 `vel/acc/jerk` 和当前 JSON 的 `acc_limit/jerk_limit` 做数值对比。但这不再阻塞 replacement 的现场对齐:当前设计默认用显式内部规划加速度参数补齐这份不可见状态。
## 当前判断
当前最可信的解释是:
1. 旧 RVBUST/FlyingShot 生成真实 `JointTraj.txt` 时,规划阶段使用的有效 joint limits 并不总是 `.robot` 文件中的原始 `velocity / acceleration / jerk`。
2. 这些有效 joint limits 可能来自服务运行期状态,例如旧服务端内部的 `_SetJointLimits`、上层 GUI/脚本初始化流程、机器人环境配置,或其他未落入当前 JSON 文件的运行时参数。
3. 现有现场 JSON 中只明确保存了:
- `acc_limit`
- `jerk_limit`
- `adapt_icsp_try_num`
- IO 相关配置
4. 已重新抓取机器人侧 `10010/10012/60015` 和本机 `50001/TCP+JSON`,仍没有看到 `JointLimits / velocity / acceleration / jerk / acc_limit / jerk_limit` 通过公开链路在规划时下发。
5. 目前没有证据表明现场配置文件或公开 TCP JSON 显式保存了一个“规划速度倍率”或“规划加速度限制”。
因此,`0.742277` 不应被理解为固定业务常量。它只是 `UTTC_MS11` 在当前 C# 默认约束和真实导出结果之间反推出来的等效规划倍率。
## 兼容设计决策
由于重新抓包后仍抓不到旧系统的 effective limits,新系统后续不再继续假设公开链路会传入这份数据,而是采用 replacement-only 的显式规划约束参数补齐不可见状态。
参数分层如下:
1. `acc_limit / jerk_limit`
- 来源:旧 `RobotConfig.json` 中已经存在的字段。
- 语义:继续作为旧配置的基础倍率,参与 `.robot` 模型加载。
- 限制:现场样本中 `acc_limit=1`、`jerk_limit=1` 时,不能解释旧导出轨迹更慢的问题。
2. `planning_acceleration_scale`
- 来源:新系统内部兼容参数,不声称来自旧 RVBUST 配置或抓包。
- 语义:只用于规划阶段,额外缩放 `JointLimit.AccelerationLimit`,用于复现旧服务端不可见的保守加速度约束。
- 默认值:`1.0`,表示不额外限制。
- 现场校准:若按纯加速度限制解释 `UTTC_MS11_TEST01`,可先用 `(5.814370 / 7.805885)^2 ≈ 0.5548` 作为候选起点,再用真实 `JointTraj.txt` 对拍确认。
3. `planning_speed_scale`
- 来源:当前 C# 已支持的显式兼容字段。
- 语义:把整条规划时间轴按速度倍率解释,联动缩放速度、加速度、jerk。
- 定位:保留为临时整体验证开关;当后续落地 `planning_acceleration_scale` 后,现场默认优先使用加速度限制参数,而不是把 `planning_speed_scale` 当成旧系统事实。
当前 C# 已支持的 `planning_speed_scale` 形式为:
```json
{
"robot": {
"planning_speed_scale": 0.742277
}
}
```
该字段只用于规划阶段:
- `vel *= planning_speed_scale`
- `acc *= planning_speed_scale^2`
- `jerk *= planning_speed_scale^3`
它不等同于运行时 `/set_speedRatio/`,也不改变 J519 的 8ms 发送周期。运行阶段仍按:
```text
t_send = k * 0.008
t_traj = t_send * speed_ratio
```
从已生成轨迹中重采样。
由于现场真实配置和本轮抓包中都没有找到这类倍率,所有 `planning_*` 字段都必须标注为 replacement-only 兼容校准参数,不能声称它们来自旧配置文件或公开 TCP JSON。
## 后续设计方向
1. 默认不再把运行时 `speed_ratio` 混入 `IsFlyshotTrajectoryValid` / `SaveTrajectoryInfo` 的规划时间计算。
2. 后续实现优先新增 `planning_acceleration_scale`,只限制规划加速度,并将其写入 `RobotConfig.json` 的 `robot` 节点或当前现场默认配置。
3. 若只需快速对齐整条时间轴,可临时使用现有 `planning_speed_scale`;但文档、日志和配置说明必须标注它是新系统校准值。
4. 如果未来能直接调用旧服务端 `_GetJointLimits`,再用返回值替换当前反推参数;在此之前,显式内部参数是当前可控且可审计的兼容策略。

View File

@@ -0,0 +1,202 @@
# Python ControllerClient 接口逆向记录
## 背景
本记录用于确认旧 `PyControllerClient` 对 Python 暴露了哪些接口,尤其确认是否能通过 Python client 直接查询或设置旧服务端运行态 `JointLimits`。
复核对象:
```text
../flyshot-uaes-interface/lib/PyControllerClient.cpython-37m-x86_64-linux-gnu.so
../FlyingShot/FlyingShot/Lib/PyControllerClient.cpython-37m-x86_64-linux-gnu.so
../flyshot-uaes-interface/lib/libControllerClient.so
../FlyingShot/FlyingShot/Lib/libControllerClient.so
../FlyingShot/FlyingShot/Include/ControllerClient/ControllerClient.h
../FlyingShot/FlyingShot/Include/ControllerClient/Types.h
../flyshot-uaes-interface/UseControllerClient.py
../flyshot-uaes-interface/main.py
```
两份 Python 扩展与两份底层 client 库哈希一致:
```text
PyControllerClient.cpython-37m-x86_64-linux-gnu.so
SHA256=648CC23CBC6DF83822B58AC4A10211EE1DF8029AD8933D31032187748DF7F4BC
libControllerClient.so
SHA256=6D6FD3F20F0791F1CF11EEE5B1D479E2DCB6A1A2C8AB00A1165575BAB4B62813
```
因此 `flyshot-uaes-interface/lib` 与 `FlyingShot/FlyingShot/Lib` 中的 Python client 可视为同一份接口。
## 暴露的 Python 类型
`PyControllerClient` 暴露以下类型:
| 类型 | 来源 | 说明 |
| --- | --- | --- |
| `ControllerClient` | `ControllerClient.h` | TCP JSON client,高层控制入口 |
| `JointPositions` | `Types.h` | 关节位置容器,可用 6 维列表构造,也支持下标读写 |
| `Pose` | `Types.h` | TCP/末端位姿容器,C++ 侧为 7 元数组 |
| `JointLimits` | `Types.h` | 关节上下限、速度、加速度、jerk 容器 |
| `IOType` | `Types.h` | IO 枚举 |
`IOType` 的枚举值:
```text
IOType.kIOTypeDI = 1
IOType.kIOTypeDO = 2
IOType.kIOTypeRI = 8
IOType.kIOTypeRO = 9
```
## ControllerClient 暴露方法
二进制字符串和 C++ 公开头文件交叉确认,Python client 暴露的方法为:
| Python 方法 | 典型调用 | 返回形态 | 说明 |
| --- | --- | --- | --- |
| `ConnectServer` | `c.ConnectServer(server_ip="127.0.0.1", port=50001)` | `bool` | 连接旧 `50001/TCP+JSON` 服务端 |
| `GetServerVersion` | `c.GetServerVersion()` | `str` | Python 包装层把 C++ out 参数折叠成返回值 |
| `GetClientVersion` | `c.GetClientVersion()` | `str` | 获取 client 版本 |
| `SetUpRobot` | `c.SetUpRobot("FANUC_LR_Mate_200iD")` | `bool` | 按机器人名称初始化服务端机器人模型 |
| `SetUpRobotFromEnv` | `c.SetUpRobotFromEnv(env_file)` | `bool` | 从环境文件初始化 |
| `IsSetUp` | `c.IsSetUp()` | `bool` | 判断服务端是否已经初始化机器人 |
| `SetShowTCP` | `c.SetShowTCP(is_show=True, axis_length=0.1, axis_size=2)` | `bool` | 仿真显示 TCP 坐标系 |
| `GetName` | `c.GetName()` | `str` | 获取机器人名称 |
| `GetDoF` | `c.GetDoF()` | `int` | 获取自由度 |
| `SetActiveController` | `c.SetActiveController(sim=True)` | `bool` | 切换仿真/真实控制器 |
| `Connect` | `c.Connect("192.168.10.101")` | `bool` | 连接机器人控制器 |
| `Disconnect` | `c.Disconnect()` | `bool` | 断开机器人控制器 |
| `EnableRobot` | `c.EnableRobot()` / `c.EnableRobot(8)` | `bool` | 使能机器人,参数为 buffer size |
| `DisableRobot` | `c.DisableRobot()` | `bool` | 下使能 |
| `GetSpeedRatio` | `c.GetSpeedRatio()` | `float` | 获取执行速度倍率 |
| `SetSpeedRatio` | `c.SetSpeedRatio(0.8)` | `bool` | 设置执行速度倍率 |
| `GetTCP` | `res, tcp = c.GetTCP()` | `(bool, Pose)` | 获取 TCP |
| `SetTCP` | `c.SetTCP(tcp)` | `bool` | 设置 TCP |
| `GetIO` | `res, value = c.GetIO(port=1, io_type=IOType.kIOTypeDI)` | `(bool, bool)` | 读取 IO |
| `SetIO` | `c.SetIO(port=1, value=True, io_type=IOType.kIOTypeDO)` | `bool` | 写 IO |
| `StopMove` | `c.StopMove()` | `bool` | 停止运动 |
| `GetJointPosition` | `res, joints = c.GetJointPosition()` | `(bool, JointPositions)` | 获取当前关节角 |
| `GetPose` | `res, pose = c.GetPose()` | `(bool, Pose)` | 获取当前末端位姿 |
| `GetNearestIK` | `res, ik = c.GetNearestIK(pose, joint_seed=joints)` | `(bool, JointPositions)` | 按 seed 求最近 IK |
| `MoveJoint` | `c.MoveJoint(joint_positions)` | `bool` | 关节运动 |
| `ExecuteTrajectory` | `c.ExecuteTrajectory(waypoints=[...], method="icsp", save_traj=True)` | `bool` | 执行普通轨迹 |
| `UploadFlyShotTraj` | `c.UploadFlyShotTraj(name, waypoints, shot_flags, offset_values, addrs)` | `bool` | 上传飞拍轨迹 |
| `DeleteFlyShotTraj` | `c.DeleteFlyShotTraj(name)` | `bool` | 删除飞拍轨迹 |
| `ListFlyShotTraj` | `c.ListFlyShotTraj()` | `list[str]` | 列出已上传飞拍轨迹 |
| `ExecuteFlyShotTraj` | `c.ExecuteFlyShotTraj(name, move_to_start=True, method="icsp", save_traj=True)` | `bool` | 执行飞拍轨迹 |
| `SaveTrajInfo` | `c.SaveTrajInfo(name, method="icsp")` | `bool` | 保存规划结果到 `~/Rvbust/Data` |
| `IsFlyShotTrajValid` | `valid, time = c.IsFlyShotTrajValid(name, method="icsp", save_traj=True)` | `(bool, float)` | 检查飞拍轨迹是否合法并返回规划时长 |
## 没有暴露的关键接口
本轮重点确认:Python client 暴露方法中没有看到:
```text
GetJointLimits
SetJointLimits
_GetJointLimits
_SetJointLimits
```
虽然 `PyControllerClient` 绑定了 `JointLimits` 类型,并且 `libControllerClient.so` 中存在 `JointLimits` 的输出运算符符号,但公开 `ControllerClient` 方法表中没有任何接收或返回 `JointLimits` 的 client 入口。
这和旧服务端二进制不同。旧服务端 `ControllerServer.cpython-37m-x86_64-linux-gnu.so` 中能看到:
```text
ControllerServer.ControllerServer._GetJointLimits
ControllerServer.ControllerServer._SetJointLimits
ControllerServer.ControllerServer._IsWaypointInJointLimits
ControllerServer.ControllerServer._IsTrajInJointLimits
ControllerServer.ControllerServer._IsTrajInJerkLimits
```
因此当前判断是:
```text
Python client 公开 API 不能直接抓 runtime JointLimits;
runtime JointLimits 查询能力存在于旧服务端内部,而不是 PyControllerClient 公开接口中。
```
## UAES Python 服务实际使用的接口
`../flyshot-uaes-interface/main.py` 只使用了公开 client 方法:
- `ConnectServer`
- `SetUpRobot`
- `IsSetUp`
- `EnableRobot`
- `DisableRobot`
- `SetActiveController`
- `Connect`
- `GetName`
- `GetServerVersion`
- `GetDoF`
- `GetSpeedRatio`
- `SetTCP`
- `GetTCP`
- `SetIO`
- `GetJointPosition`
- `MoveJoint`
- `ListFlyShotTraj`
- `UploadFlyShotTraj`
- `ExecuteFlyShotTraj`
- `SetSpeedRatio`
- `DeleteFlyShotTraj`
- `GetPose`
其中 `/execute_flyshot/` 调用:
```text
c.ExecuteFlyShotTraj(name=name, move_to_start=True, method="icsp", save_traj=True)
```
`/set_speedRatio/` 调用:
```text
c.SetSpeedRatio(speed)
```
没有看到 UAES 服务通过 Python client 设置或查询 `JointLimits`。
2026-04-30 追加 `50001/TCP+JSON` 抓包复核后,这个判断进一步收敛。`all-50001-plan.pcap` 中已经抓到两次真实规划/执行请求:
```json
{"cmd":"SetSpeedRatio","ratio":0.5}
{"cmd":"ExecuteFlyShotTraj","method":"icsp","move_to_start":true,"name":"UTTC_MS11_TEST01","save_traj":true,"use_cache":false,"wait":true}
{"cmd":"SetSpeedRatio","ratio":1.0}
{"cmd":"ExecuteFlyShotTraj","method":"icsp","move_to_start":true,"name":"UTTC_MS11_TEST01","save_traj":true,"use_cache":false,"wait":true}
```
请求中仍没有 `JointLimits / acc_limit / jerk_limit / velocity / acceleration / jerk`。因此公开 Python client 与公开 50001 JSON 都没有把规划限制作为参数传给 `ExecuteFlyShotTraj`。
另外,`main.py` 的 `/execute_trajectory/` 中出现:
```text
c.yrxm(waypoints=joint_positions, method='icsp', save_traj=True)
```
`yrxm` 不在 `PyControllerClient` 暴露方法表中,按上下文应是 `ExecuteTrajectory` 的笔误;这条不影响飞拍主路径 `/execute_flyshot/`。
## 对当前时长差异调查的含义
如果要抓旧系统规划时使用的 effective `vel/acc/jerk`,优先级应调整为:
1. 在旧服务端进程内直接调用或插桩 `_GetJointLimits`。
2. 或者逆向 `50001/TCP+JSON` 的 hidden command envelope,再尝试发送 `GetJointLimits` / `_GetJointLimits`。
3. 不应指望现有 `PyControllerClient.ControllerClient` 直接提供 `GetJointLimits`。
如果短期内无法进入旧服务端内部,新系统不再继续等待这份不可见状态;设计上使用 replacement-only 的内部规划约束参数补齐,优先限制规划加速度,例如 `planning_acceleration_scale`。该参数必须标注为新系统校准值,不能写成旧 Python client 或旧 50001 JSON 的公开字段。
最小现场验证脚本可以先确认 Python client 暴露面:
```python
from PyControllerClient import ControllerClient
c = ControllerClient()
names = [x for x in dir(c) if "Limit" in x or "limit" in x]
print(names)
```
按当前二进制逆向,预期不会出现 `GetJointLimits` / `SetJointLimits`。

View File

@@ -0,0 +1,62 @@
# 机器人六轴限值提取表
记录时间:2026-05-05
## 1. 目的
本文档把当前机器人模型中的六轴基础限值整理成一份固定表格,明确区分以下几类信息:
- 模型原始值:来自 `LR_Mate_200iD_7L_clean.json` 中每个关节的 `limit.velocity / limit.acceleration / limit.jerk / limit.effort`
- 配置倍率:来自当前运行配置 `Config/RobotConfig.json` 中的 `acc_limit / jerk_limit`
- 运行时有效值:模型原始值叠加当前配置倍率后的结果
本表只覆盖六个旋转关节 `Joint1` 到 `Joint6`。
`JointEffector` 属于末端固定关节,不计入“每个轴”的速度、加速度、跃度统计。
## 2. 当前取值规则
当前仓库运行时对六轴限值的读取规则是:
- `velocity_eff = velocity_base`
- `acceleration_eff = acceleration_base * acc_limit`
- `jerk_eff = jerk_base * jerk_limit`
当前 `Config/RobotConfig.json` 中的倍率为:
- `acc_limit = 1`
- `jerk_limit = 1`
因此本次表格中的“基础值”和“有效值”数值相同。
## 3. 六轴限值表
| Joint | velocity_base | acceleration_base | jerk_base | effort_raw | acc_limit | jerk_limit | velocity_eff | acceleration_eff | jerk_eff | 备注 |
| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | --- |
| Joint1 | 6.45 | 26.90 | 224.22 | 0.0 | 1 | 1 | 6.45 | 26.90 | 224.22 | 模型字段存在,`effort` 当前为 0 |
| Joint2 | 5.41 | 22.54 | 187.86 | 0.0 | 1 | 1 | 5.41 | 22.54 | 187.86 | 模型字段存在,`effort` 当前为 0 |
| Joint3 | 7.15 | 29.81 | 248.46 | 0.0 | 1 | 1 | 7.15 | 29.81 | 248.46 | 模型字段存在,`effort` 当前为 0 |
| Joint4 | 9.59 | 39.99 | 333.30 | 0.0 | 1 | 1 | 9.59 | 39.99 | 333.30 | 模型字段存在,`effort` 当前为 0 |
| Joint5 | 9.51 | 39.63 | 330.27 | 0.0 | 1 | 1 | 9.51 | 39.63 | 330.27 | 模型字段存在,`effort` 当前为 0 |
| Joint6 | 17.45 | 72.72 | 606.01 | 0.0 | 1 | 1 | 17.45 | 72.72 | 606.01 | 模型字段存在,`effort` 当前为 0 |
## 4. 关于“电流信息”的说明
这份模型文件里确实有 `limit.effort` 字段,但当前证据只能说明:
- 它是模型文件中的一个静态字段
- 当前六轴以及 `JointEffector` 的 `effort` 都是 `0.0`
- 当前 `flyshot-replacement` 代码链路没有把它当作实时电流来源来使用
因此当前结论应固定为:
- `velocity / acceleration / jerk` 可以从这份模型文档中提取
- `effort` 只能当作模型原始字段记录
- `effort` 不能直接解释为现场真实电流,也不能替代 J519 反馈中的电机电流数据
## 5. 后续使用约定
后续如果有人问“每个轴的速度、加速度、跃度是不是从这个文档来的”,默认回答应为:
- 是,六轴基础限值来自模型文件中的 `joint.limit`
- 运行时有效加速度和有效跃度还要再乘 `RobotConfig.json` 的全局倍率
- 不是,真实电流不要从这个文件里的 `effort` 去推断

View File

@@ -0,0 +1,657 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>状态页摇杆点动 Demo</title>
<style>
:root {
color-scheme: light;
--bg: #f5f7fb;
--surface: #ffffff;
--line: #d8dee9;
--text: #172033;
--muted: #5b667a;
--accent: #007c89;
--accent-dark: #005d67;
--steel: #2f3b4f;
--steel-soft: #e7ebf1;
--good: #12805c;
--warn: #b7791f;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
background: var(--bg);
color: var(--text);
font-family: "Segoe UI", "Microsoft YaHei", Arial, sans-serif;
font-size: 15px;
letter-spacing: 0;
}
header {
border-bottom: 1px solid var(--line);
background: var(--surface);
}
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
width: min(1180px, calc(100% - 32px));
margin: 0 auto;
padding: 18px 0;
}
h1 {
margin: 0;
font-size: 22px;
font-weight: 650;
}
.status-pill {
display: inline-flex;
align-items: center;
gap: 8px;
min-height: 36px;
padding: 0 12px;
border: 1px solid rgba(18, 128, 92, 0.28);
border-radius: 999px;
background: rgba(18, 128, 92, 0.08);
color: var(--good);
font-weight: 650;
}
.dot {
width: 10px;
height: 10px;
border-radius: 999px;
background: var(--good);
}
main {
width: min(1180px, calc(100% - 32px));
margin: 22px auto;
}
.summary {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px;
margin-bottom: 16px;
}
.metric,
.panel {
border: 1px solid var(--line);
border-radius: 8px;
background: var(--surface);
}
.metric {
min-height: 86px;
padding: 14px;
}
.label {
color: var(--muted);
font-size: 13px;
}
.value {
margin-top: 8px;
overflow-wrap: anywhere;
font-size: 24px;
font-weight: 650;
}
.panel h2 {
margin: 0;
padding: 14px 16px;
border-bottom: 1px solid var(--line);
font-size: 16px;
font-weight: 650;
}
.jog-panel {
overflow: hidden;
}
.jog-body {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
gap: 18px;
padding: 16px;
}
.jog-toolbar {
display: grid;
grid-template-columns: repeat(2, minmax(180px, 1fr)) minmax(220px, 1fr);
gap: 12px;
padding: 16px;
border-bottom: 1px solid var(--line);
background: #fbfcfe;
}
.step-field {
display: grid;
gap: 6px;
color: var(--muted);
font-size: 13px;
}
.step-field input {
min-height: 38px;
width: 100%;
padding: 0 10px;
border: 1px solid var(--line);
border-radius: 6px;
background: #ffffff;
color: var(--text);
font: inherit;
}
.jog-readout {
display: flex;
align-items: center;
justify-content: flex-end;
min-height: 38px;
color: var(--muted);
font-family: Consolas, "Cascadia Mono", monospace;
text-align: right;
}
.stick-card {
min-width: 0;
border: 1px solid #ccd4df;
border-radius: 8px;
background: linear-gradient(180deg, #ffffff 0%, #f3f6fa 100%);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.9);
}
.stick-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 14px 16px 0;
}
.stick-title {
margin: 0;
font-size: 16px;
font-weight: 650;
}
.stick-unit {
color: var(--muted);
font-size: 13px;
font-family: Consolas, "Cascadia Mono", monospace;
}
.stick-console {
display: grid;
grid-template-columns: 72px 1fr 72px;
grid-template-rows: 72px 72px 72px;
gap: 8px;
width: min(100%, 344px);
margin: 18px auto 16px;
padding: 14px;
border: 1px solid #c2cad7;
border-radius: 999px;
background:
radial-gradient(circle at 50% 50%, #ffffff 0 26%, #e6ebf2 27% 48%, #cdd5e0 49% 50%, #edf1f6 51% 100%);
}
.axis-button {
min-width: 0;
min-height: 64px;
border: 1px solid #99a8ba;
border-radius: 999px;
background: linear-gradient(180deg, #ffffff 0%, #e5ebf2 100%);
color: var(--steel);
font: inherit;
font-weight: 750;
cursor: pointer;
touch-action: none;
user-select: none;
box-shadow: 0 2px 0 #b7c1ce;
}
.axis-button:hover,
.axis-button.active {
border-color: var(--accent);
background: linear-gradient(180deg, #10a4b3 0%, var(--accent) 100%);
color: #ffffff;
box-shadow: 0 2px 0 var(--accent-dark);
}
.axis-button:focus-visible {
outline: 3px solid rgba(0, 124, 137, 0.3);
outline-offset: 2px;
}
.axis-button.up {
grid-column: 2;
grid-row: 1;
}
.axis-button.left {
grid-column: 1;
grid-row: 2;
}
.axis-button.right {
grid-column: 3;
grid-row: 2;
}
.axis-button.down {
grid-column: 2;
grid-row: 3;
}
.center-stack {
display: grid;
grid-column: 2;
grid-row: 2;
grid-template-columns: 1fr 1fr;
gap: 6px;
place-items: center;
padding: 8px;
border-radius: 999px;
background: rgba(47, 59, 79, 0.08);
}
.mini-button {
min-width: 44px;
min-height: 44px;
border: 1px solid #99a8ba;
border-radius: 999px;
background: var(--surface);
color: var(--steel);
font: inherit;
font-weight: 750;
cursor: pointer;
touch-action: none;
user-select: none;
}
.mini-button:hover,
.mini-button.active {
border-color: var(--warn);
background: #b7791f;
color: #ffffff;
}
.hint-row {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 8px;
padding: 0 16px 16px;
color: var(--muted);
font-size: 13px;
text-align: center;
}
.hint-row span {
min-height: 28px;
padding: 5px 8px;
border-radius: 999px;
background: var(--steel-soft);
white-space: nowrap;
}
.log-panel {
margin-top: 16px;
overflow: hidden;
}
.log-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 14px 16px;
border-bottom: 1px solid var(--line);
}
.log-head h2 {
margin: 0;
padding: 0;
border-bottom: 0;
}
.clear-button {
min-height: 34px;
padding: 0 12px;
border: 1px solid var(--line);
border-radius: 6px;
background: #ffffff;
color: var(--muted);
font: inherit;
cursor: pointer;
}
.clear-button:hover {
border-color: var(--accent);
color: var(--accent);
}
.request-log {
display: grid;
gap: 8px;
max-height: 300px;
margin: 0;
padding: 12px 16px 16px;
overflow: auto;
background: #fbfcfe;
font-family: Consolas, "Cascadia Mono", monospace;
font-size: 13px;
}
.log-entry {
display: grid;
grid-template-columns: 86px 96px minmax(0, 1fr);
gap: 10px;
align-items: start;
padding: 9px 10px;
border: 1px solid #e1e7ef;
border-radius: 6px;
background: #ffffff;
}
.log-entry:first-child {
border-color: rgba(0, 124, 137, 0.35);
background: rgba(0, 124, 137, 0.04);
}
.log-time,
.log-axis {
color: var(--muted);
white-space: nowrap;
}
.log-payload {
overflow-wrap: anywhere;
color: var(--text);
}
@media (max-width: 920px) {
.summary,
.jog-body,
.jog-toolbar {
grid-template-columns: 1fr;
}
.jog-readout {
justify-content: flex-start;
text-align: left;
}
}
@media (max-width: 520px) {
.topbar {
align-items: flex-start;
flex-direction: column;
}
.stick-console {
grid-template-columns: 60px 1fr 60px;
grid-template-rows: 60px 60px 60px;
padding: 10px;
}
.axis-button {
min-height: 56px;
}
.log-entry {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<header>
<div class="topbar">
<h1>Flyshot Replacement 状态监控</h1>
<div class="status-pill"><span class="dot"></span><span>Connected</span></div>
</div>
</header>
<main>
<div class="summary">
<div class="metric">
<div class="label">机器人</div>
<div class="value">FANUC</div>
</div>
<div class="metric">
<div class="label">速度倍率</div>
<div class="value">0.70</div>
</div>
<div class="metric">
<div class="label">运动中</div>
<div class="value">否</div>
</div>
<div class="metric">
<div class="label">TCP 位姿</div>
<div class="value" style="font-size: 16px;">120.0, 80.0, 320.0</div>
</div>
</div>
<section class="panel jog-panel">
<h2>直角坐标点动</h2>
<div class="jog-toolbar">
<label class="step-field" for="linear-step">
<span>平移步长 mm</span>
<input id="linear-step" type="number" min="0.1" max="10" step="0.1" value="1">
</label>
<label class="step-field" for="angular-step">
<span>姿态步长 deg</span>
<input id="angular-step" type="number" min="0.1" max="10" step="0.1" value="1">
</label>
<div id="readout" class="jog-readout">等待点动输入</div>
</div>
<div class="jog-body">
<article class="stick-card">
<div class="stick-head">
<h3 class="stick-title">XYZ 平移摇杆</h3>
<span class="stick-unit">mm</span>
</div>
<div class="stick-console" aria-label="XYZ 平移摇杆">
<button class="axis-button up" type="button" data-axis="y" data-direction="1">Y+</button>
<button class="axis-button left" type="button" data-axis="x" data-direction="-1">X-</button>
<div class="center-stack">
<button class="mini-button" type="button" data-axis="z" data-direction="1">Z+</button>
<button class="mini-button" type="button" data-axis="z" data-direction="-1">Z-</button>
</div>
<button class="axis-button right" type="button" data-axis="x" data-direction="1">X+</button>
<button class="axis-button down" type="button" data-axis="y" data-direction="-1">Y-</button>
</div>
<div class="hint-row">
<span>X 左右</span>
<span>Y 上下</span>
<span>Z 中心</span>
</div>
</article>
<article class="stick-card">
<div class="stick-head">
<h3 class="stick-title">WPR 姿态摇杆</h3>
<span class="stick-unit">deg</span>
</div>
<div class="stick-console" aria-label="WPR 姿态摇杆">
<button class="axis-button up" type="button" data-axis="p" data-direction="1">P+</button>
<button class="axis-button left" type="button" data-axis="w" data-direction="-1">W-</button>
<div class="center-stack">
<button class="mini-button" type="button" data-axis="r" data-direction="-1">R-</button>
<button class="mini-button" type="button" data-axis="r" data-direction="1">R+</button>
</div>
<button class="axis-button right" type="button" data-axis="w" data-direction="1">W+</button>
<button class="axis-button down" type="button" data-axis="p" data-direction="-1">P-</button>
</div>
<div class="hint-row">
<span>W 左右</span>
<span>P 上下</span>
<span>R 中心</span>
</div>
</article>
</div>
</section>
<section class="panel log-panel">
<div class="log-head">
<h2>请求日志</h2>
<button id="clear-log" class="clear-button" type="button">清空</button>
</div>
<div id="request-log" class="request-log" aria-live="polite">
<div class="log-entry">
<span class="log-time">--:--:--</span>
<span class="log-axis">等待输入</span>
<span class="log-payload">按住任意摇杆方向后,这里会记录每次模拟 MovePose 请求。</span>
</div>
</div>
</section>
</main>
<script>
const readout = document.getElementById("readout");
const buttons = Array.from(document.querySelectorAll("[data-axis]"));
const linearStepInput = document.getElementById("linear-step");
const angularStepInput = document.getElementById("angular-step");
const requestLog = document.getElementById("request-log");
const clearLog = document.getElementById("clear-log");
const axisIndexes = { x: 0, y: 1, z: 2, w: 3, p: 4, r: 5 };
const poseKeys = ["x", "y", "z", "w", "p", "r"];
const maxLogCount = 80;
let timer = null;
let activeButton = null;
let requestCount = 0;
let currentPose = {
x: 120.0,
y: 80.0,
z: 320.0,
w: 0.0,
p: 45.0,
r: 0.0
};
function labelFor(button) {
const axis = button.dataset.axis.toUpperCase();
const sign = Number(button.dataset.direction) > 0 ? "+" : "-";
return axis + sign;
}
function clampStep(input, min, max) {
const value = Number(input.value);
if (!Number.isFinite(value)) {
input.value = String(min);
return min;
}
const clamped = Math.min(Math.max(value, min), max);
input.value = String(clamped);
return clamped;
}
function stepFor(axis) {
return axis === "x" || axis === "y" || axis === "z"
? clampStep(linearStepInput, 0.1, 10)
: clampStep(angularStepInput, 0.1, 10);
}
function buildMovePoseBody(button) {
const axis = button.dataset.axis;
const direction = Number(button.dataset.direction);
const key = poseKeys[axisIndexes[axis]];
currentPose[key] = Number((currentPose[key] + direction * stepFor(axis)).toFixed(6));
return {
x: currentPose.x,
y: currentPose.y,
z: currentPose.z,
w: currentPose.w,
p: currentPose.p,
r: currentPose.r
};
}
function appendLog(button, body) {
requestCount += 1;
if (requestCount === 1) {
requestLog.replaceChildren();
}
const row = document.createElement("div");
row.className = "log-entry";
const time = document.createElement("span");
time.className = "log-time";
time.textContent = new Date().toLocaleTimeString("zh-CN", { hour12: false });
const axis = document.createElement("span");
axis.className = "log-axis";
axis.textContent = "#" + String(requestCount).padStart(3, "0") + " " + labelFor(button);
const payload = document.createElement("span");
payload.className = "log-payload";
payload.textContent = "POST /move_pose/ " + JSON.stringify(body);
row.append(time, axis, payload);
requestLog.prepend(row);
while (requestLog.children.length > maxLogCount) {
requestLog.lastElementChild.remove();
}
}
function tick(button) {
const body = buildMovePoseBody(button);
readout.textContent = "模拟发送: " + labelFor(button) + " -> " + JSON.stringify(body);
appendLog(button, body);
}
function stop() {
if (timer !== null) {
window.clearInterval(timer);
timer = null;
}
if (activeButton) {
activeButton.classList.remove("active");
activeButton = null;
}
}
buttons.forEach(function (button) {
button.addEventListener("pointerdown", function (event) {
event.preventDefault();
stop();
activeButton = button;
button.classList.add("active");
tick(button);
timer = window.setInterval(function () {
tick(button);
}, 250);
});
button.addEventListener("pointerup", stop);
button.addEventListener("pointerleave", stop);
button.addEventListener("pointercancel", stop);
});
clearLog.addEventListener("click", function () {
requestCount = 0;
requestLog.innerHTML = '<div class="log-entry"><span class="log-time">--:--:--</span><span class="log-axis">已清空</span><span class="log-payload">新的点动请求会继续记录在这里。</span></div>';
readout.textContent = "等待点动输入";
});
window.addEventListener("blur", stop);
</script>
</body>
</html>

View File

@@ -0,0 +1,272 @@
# Minimal Runtime Orchestration Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Build the first real execution slice after the HTTP-only refactor by routing `ExecuteTrajectory` and `ExecuteFlyShotTraj` through planning, triggering, and a new minimal FANUC runtime project instead of in-memory last-point assignment.
**Architecture:** Keep `Flyshot.Server.Host` as a pure HTTP adapter and keep uploaded program state in `Flyshot.ControllerClientCompat`, but move controller runtime state into a new `Flyshot.Runtime.Fanuc` project and add a focused planning/orchestration helper in `Flyshot.ControllerClientCompat`. Ordinary trajectory execution will use `ICspPlanner`; uploaded flyshot execution will use `SelfAdaptIcspPlanner` plus `ShotTimelineBuilder`, then hand the resulting `TrajectoryResult` to the runtime.
**Tech Stack:** C#, .NET 8, xUnit, existing `Flyshot.Core.Domain`, `Flyshot.Core.Planning`, `Flyshot.Core.Triggering`, ASP.NET Core DI.
---
### Task 1: Add Runtime Contracts And Minimal FANUC Runtime
**Files:**
- Create: `src/Flyshot.Runtime.Common/IControllerRuntime.cs`
- Create: `src/Flyshot.Runtime.Fanuc/Flyshot.Runtime.Fanuc.csproj`
- Create: `src/Flyshot.Runtime.Fanuc/FanucControllerRuntime.cs`
- Modify: `FlyshotReplacement.sln`
- Modify: `src/Flyshot.Server.Host/Flyshot.Server.Host.csproj`
- Modify: `src/Flyshot.Server.Host/Program.cs`
- Test: `tests/Flyshot.Core.Tests/RuntimeOrchestrationTests.cs`
- [x] **Step 1: Write the failing runtime test**
```csharp
[Fact]
public void FanucControllerRuntime_ExecuteTrajectory_UpdatesSnapshotAndFinalJointPositions()
{
var runtime = new FanucControllerRuntime();
var robot = TestRobotFactory.CreateRobotProfile();
runtime.ResetRobot(robot, "FANUC_LR_Mate_200iD");
runtime.SetActiveController(sim: false);
runtime.Connect("192.168.10.101");
runtime.EnableRobot(bufferSize: 2);
var result = new TrajectoryResult(
programName: "demo",
method: PlanningMethod.Icsp,
isValid: true,
duration: TimeSpan.FromSeconds(1.2),
shotEvents: Array.Empty<ShotEvent>(),
triggerTimeline: Array.Empty<TrajectoryDoEvent>(),
artifacts: Array.Empty<TrajectoryArtifact>(),
failureReason: null,
usedCache: false,
originalWaypointCount: 4,
plannedWaypointCount: 4);
runtime.ExecuteTrajectory(result, [1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
var snapshot = runtime.GetSnapshot();
Assert.Equal("Connected", snapshot.ConnectionState);
Assert.False(snapshot.IsInMotion);
Assert.Equal([1.0, 2.0, 3.0, 4.0, 5.0, 6.0], snapshot.JointPositions);
}
```
- [x] **Step 2: Run test to verify it fails**
Run: `dotnet test tests/Flyshot.Core.Tests/Flyshot.Core.Tests.csproj --filter FanucControllerRuntime_ExecuteTrajectory_UpdatesSnapshotAndFinalJointPositions -v minimal -m:1 -nodeReuse:false`
Expected: FAIL because `FanucControllerRuntime` and `IControllerRuntime` do not exist.
- [x] **Step 3: Write the minimal runtime contracts and implementation**
```csharp
public interface IControllerRuntime
{
void ResetRobot(RobotProfile robot, string robotName);
void SetActiveController(bool sim);
void Connect(string robotIp);
void Disconnect();
void EnableRobot(int bufferSize);
void DisableRobot();
void StopMove();
double GetSpeedRatio();
void SetSpeedRatio(double ratio);
IReadOnlyList<double> GetTcp();
void SetTcp(double x, double y, double z);
bool GetIo(int port, string ioType);
void SetIo(int port, bool value, string ioType);
IReadOnlyList<double> GetJointPositions();
IReadOnlyList<double> GetPose();
ControllerStateSnapshot GetSnapshot();
void ExecuteTrajectory(TrajectoryResult result, IReadOnlyList<double> finalJointPositions);
}
```
```csharp
public sealed class FanucControllerRuntime : IControllerRuntime
{
// Stage-1 runtime: owns controller state in one place so later sockets can replace internals without rewriting compat service.
}
```
- [x] **Step 4: Run test to verify it passes**
Run: `dotnet test tests/Flyshot.Core.Tests/Flyshot.Core.Tests.csproj --filter FanucControllerRuntime_ExecuteTrajectory_UpdatesSnapshotAndFinalJointPositions -v minimal -m:1 -nodeReuse:false`
Expected: PASS.
### Task 2: Add Planning And Triggering Orchestration For Execution
**Files:**
- Create: `src/Flyshot.ControllerClientCompat/PlannedExecutionBundle.cs`
- Create: `src/Flyshot.ControllerClientCompat/ControllerClientTrajectoryOrchestrator.cs`
- Modify: `src/Flyshot.ControllerClientCompat/Flyshot.ControllerClientCompat.csproj`
- Modify: `tests/Flyshot.Core.Tests/Flyshot.Core.Tests.csproj`
- Test: `tests/Flyshot.Core.Tests/RuntimeOrchestrationTests.cs`
- [x] **Step 1: Write the failing orchestration tests**
```csharp
[Fact]
public void ControllerClientTrajectoryOrchestrator_PlanOrdinaryTrajectory_RejectsThreeTeachPoints()
{
var orchestrator = new ControllerClientTrajectoryOrchestrator();
var robot = TestRobotFactory.CreateRobotProfile();
Assert.Throws<ArgumentException>(() =>
orchestrator.PlanOrdinaryTrajectory(robot,
[
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
[0.5, 0.0, 0.0, 0.0, 0.0, 0.0],
[1.0, 0.0, 0.0, 0.0, 0.0, 0.0]
]));
}
[Fact]
public void ControllerClientTrajectoryOrchestrator_PlanUploadedFlyshot_BuildsShotTimeline()
{
var orchestrator = new ControllerClientTrajectoryOrchestrator();
var robot = TestRobotFactory.CreateRobotProfile();
var uploaded = TestRobotFactory.CreateUploadedTrajectoryWithSingleShot();
var bundle = orchestrator.PlanUploadedFlyshot(robot, uploaded);
Assert.True(bundle.Result.IsValid);
Assert.Single(bundle.Result.ShotEvents);
Assert.Single(bundle.Result.TriggerTimeline);
}
```
- [x] **Step 2: Run tests to verify they fail**
Run: `dotnet test tests/Flyshot.Core.Tests/Flyshot.Core.Tests.csproj --filter ControllerClientTrajectoryOrchestrator -v minimal -m:1 -nodeReuse:false`
Expected: FAIL because the orchestrator types do not exist.
- [x] **Step 3: Write the minimal orchestration layer**
```csharp
public sealed class PlannedExecutionBundle
{
public PlannedExecutionBundle(PlannedTrajectory plannedTrajectory, ShotTimeline shotTimeline, TrajectoryResult result)
{
PlannedTrajectory = plannedTrajectory;
ShotTimeline = shotTimeline;
Result = result;
}
public PlannedTrajectory PlannedTrajectory { get; }
public ShotTimeline ShotTimeline { get; }
public TrajectoryResult Result { get; }
}
```
```csharp
public sealed class ControllerClientTrajectoryOrchestrator
{
public PlannedExecutionBundle PlanOrdinaryTrajectory(RobotProfile robot, IReadOnlyList<IReadOnlyList<double>> waypoints) { ... }
public PlannedExecutionBundle PlanUploadedFlyshot(RobotProfile robot, ControllerClientCompatUploadedTrajectory uploaded) { ... }
}
```
- [x] **Step 4: Run tests to verify they pass**
Run: `dotnet test tests/Flyshot.Core.Tests/Flyshot.Core.Tests.csproj --filter ControllerClientTrajectoryOrchestrator -v minimal -m:1 -nodeReuse:false`
Expected: PASS.
### Task 3: Rewire ControllerClientCompatService To Runtime + Orchestrator
**Files:**
- Modify: `src/Flyshot.ControllerClientCompat/ControllerClientCompatService.cs`
- Modify: `src/Flyshot.ControllerClientCompat/ControllerClientCompatServiceCollectionExtensions.cs`
- Modify: `src/Flyshot.ControllerClientCompat/IControllerClientCompatService.cs`
- Modify: `tests/Flyshot.Server.IntegrationTests/LegacyHttpApiCompatibilityTests.cs`
- Modify: `tests/Flyshot.Server.IntegrationTests/ControllerClientCompatRegistrationTests.cs`
- Test: `tests/Flyshot.Core.Tests/RuntimeOrchestrationTests.cs`
- Test: `tests/Flyshot.Server.IntegrationTests/LegacyHttpApiCompatibilityTests.cs`
- [x] **Step 1: Write the failing compat-service test**
```csharp
[Fact]
public void ControllerClientCompatService_ExecuteTrajectory_RejectsThreeTeachPointsAfterPlanningIsIntroduced()
{
var service = TestRobotFactory.CreateCompatService();
service.SetUpRobot("FANUC_LR_Mate_200iD");
service.SetActiveController(sim: false);
service.Connect("192.168.10.101");
service.EnableRobot(2);
Assert.Throws<ArgumentException>(() =>
service.ExecuteTrajectory(
[
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
[0.5, 0.0, 0.0, 0.0, 0.0, 0.0],
[1.0, 0.0, 0.0, 0.0, 0.0, 0.0]
]));
}
```
- [x] **Step 2: Run test to verify it fails**
Run: `dotnet test tests/Flyshot.Core.Tests/Flyshot.Core.Tests.csproj --filter ControllerClientCompatService_ExecuteTrajectory_RejectsThreeTeachPointsAfterPlanningIsIntroduced -v minimal -m:1 -nodeReuse:false`
Expected: FAIL because current service still treats ordinary execution as "move to last waypoint".
- [x] **Step 3: Rewire service to the runtime and orchestrator**
```csharp
public sealed class ControllerClientCompatService : IControllerClientCompatService
{
private readonly IControllerRuntime _runtime;
private readonly ControllerClientTrajectoryOrchestrator _trajectoryOrchestrator;
public void ExecuteTrajectory(IReadOnlyList<IReadOnlyList<double>> waypoints)
{
var robot = RequireActiveRobot();
var bundle = _trajectoryOrchestrator.PlanOrdinaryTrajectory(robot, waypoints);
_runtime.ExecuteTrajectory(bundle.Result, bundle.PlannedTrajectory.PlannedWaypoints[^1].Positions);
}
public void ExecuteTrajectoryByName(string name)
{
var robot = RequireActiveRobot();
var uploaded = RequireUploadedTrajectory(name);
var bundle = _trajectoryOrchestrator.PlanUploadedFlyshot(robot, uploaded);
_runtime.ExecuteTrajectory(bundle.Result, bundle.PlannedTrajectory.PlannedWaypoints[^1].Positions);
}
}
```
- [x] **Step 4: Run focused tests to verify green**
Run: `dotnet test tests/Flyshot.Core.Tests/Flyshot.Core.Tests.csproj --filter "ControllerClientCompatService|ControllerClientTrajectoryOrchestrator|FanucControllerRuntime" -v minimal -m:1 -nodeReuse:false`
Expected: PASS.
- [x] **Step 5: Run integration verification**
Run: `dotnet test tests/Flyshot.Server.IntegrationTests/Flyshot.Server.IntegrationTests.csproj -v minimal -m:1 -nodeReuse:false`
Expected: PASS, with existing HTTP compatibility tests still green.
### Task 4: Verify Solution Build And Update Progress Docs
**Files:**
- Modify: `README.md`
- Modify: `AGENTS.md`
- Modify: `docs/controller-client-api-compatibility-requirements.md`
- [x] **Step 1: Update docs to reflect the new stage**
```markdown
- [x] 落地最小 FANUC 运行时骨架
- [x] 将 ExecuteTrajectory / ExecuteFlyShotTraj 接入 Planning + Triggering + Runtime
- [x] 落地 Web 状态页
- [x] 固化 10010 / 10012 / 60015 FANUC 基础协议帧编解码
- [ ] 落地真实 10010 / 10012 / 60015 Socket 通讯与现场联调
```
- [x] **Step 2: Run final build**
Run: `dotnet build FlyshotReplacement.sln --no-restore -v minimal -m:1 -nodeReuse:false`
Expected: PASS with 0 errors.

View File

@@ -0,0 +1,151 @@
# UTTC_MS11 跳过 flyshot jerk 阻断的结论记录
## 背景
2026-05-11 对 `UTTC_MS11` 做了两轮独立 `ConfigRoot` 重新生成与对比:
1. 保持 flyshot 离散校验中的 `vel/acc/jerk` 全部生效。
2. 临时只对 flyshot 链路跳过 `jerk` 阻断,仍保留速度与加速度校验。
两轮都使用当前程序重新生成:
- `POST /set_speedRatio/`
- `POST /save_traj_info/`
并与以下旧抓包侧结果做对比:
- `../Rvbust/uttc-20260428/2026042802-1_joint_segments/segment_02/JointDetialTraj.txt`
- `../Rvbust/uttc-20260428/2026042802-0.5_joint_segments/segment_02/JointDetialTraj.txt`
- `../Rvbust/uttc-20260428/1倍速度 角度坐标点/真实轨迹JointDetialTraj.txt`
## 直接结论
对当前 `UTTC_MS11` 现场样本,flyshot 链路若继续用离散 `jerk` 作为阻断条件,会触发大量时间轴 stretch,使新结果远慢于旧抓包。
当 flyshot 链路只保留速度、加速度校验并跳过 `jerk` 阻断后:
- 新 `1.0x` 实发时长从 `16.576s` 回落到 `7.552s`
- 新 `0.5x` 实发时长从 `33.152s` 回落到 `15.104s`
- 与旧抓包主运动段相比,只剩很小的时长差异,可接受并忽略
因此,当前项目对 `UTTC_MS11` 的实际结论定为:
- flyshot 链路临时不再把离散 `jerk` 作为阻断条件
- 仍保留速度、加速度校验
- `jerk` 诊断文件继续保留,用于分析,不再作为该路径是否可运行的唯一否决条件
## 证据
### 1. 开启 jerk 阻断时的现象
重新生成结果目录:
- `../temp/regen-20260511-110950/`
关键结果:
- `1.0x ActualSendTiming.txt`:`2073` 行,`send_duration=16.576s`
- `0.5x ActualSendTiming.txt`:`4145` 行,`send_duration=33.152s`
对比旧抓包主段:
- 旧 `1.0x`:`7.408109s`
- 旧 `0.5x`:`14.808342s`
可见新结果明显被拉长。
### 2. 触发 stretch 的第一条失败证据
日志文件:
- `../temp/regen-20260511-110950/host.log`
第一条离散失败不是加速度,而是开头窗口的 `Joint1 Jerk`:
- `轨迹 UTTC_MS11 第 3 行 Joint1 Jerk超限`
- `time=0.008000->0.016000s`
- `actual=-1732.421875`
- `limit=91.698044`
- `ratio=18.8927`
后续 `CreateLimitCompliantDenseTrajectory(...)` 按 `1.01` 倍持续拉长时间轴,直到通过离散校验,最终把总时长推到约 `16.57s`。
### 3. 只跳过 flyshot jerk 阻断后的结果
重新生成结果目录:
- `../temp/regen-nojerk-20260511-113258/`
关键结果:
- `1.0x ActualSendTiming.txt`:`945` 行,`send_duration=7.552s`
- `0.5x ActualSendTiming.txt`:`1889` 行,`send_duration=15.104s`
与旧抓包主段对比:
| 项目 | 旧抓包时长 | 新结果时长 | 差值 |
| --- | ---: | ---: | ---: |
| `1.0x` | `7.408109s` | `7.552s` | `0.143891s` |
| `0.5x` | `14.808342s` | `15.104s` | `0.295658s` |
该差异已经很小,当前按“可忽略”处理。
## 轨迹误差变化
在同一套对比脚本口径下:
### 开启 jerk 阻断时
- `JointDetialTraj` 新 vs 旧真实轨迹:全局 RMS 约 `46.664 deg`
- `ActualSendJointTraj 1.0x` 新 vs 旧抓包:全局 RMS 约 `46.665 deg`
- `ActualSendJointTraj 0.5x` 新 vs 旧抓包:全局 RMS 约 `46.667 deg`
### 跳过 flyshot jerk 阻断后
- `JointDetialTraj` 新 vs 旧真实轨迹:全局 RMS 约 `8.411 deg`
- `ActualSendJointTraj 1.0x` 新 vs 旧抓包:全局 RMS 约 `8.412 deg`
- `ActualSendJointTraj 0.5x` 新 vs 旧抓包:全局 RMS 约 `8.412 deg`
说明这次差异收敛主要来自去掉 flyshot jerk 阻断后的 stretch,而不是 `speed_ratio` 链路本身被重新设计。
## 对 jerk 的解释
这次调整不等于“现场 jerk 完全合规”,而是明确区分:
1. `jerk` 仍然继续计算和导出
2. `jerk` 仍然可作为诊断指标
3. 但对当前 `UTTC_MS11` flyshot 现场样本,不再把 `jerk` 作为阻断执行的唯一判据
重新生成后的分析文件仍显示:
- 加速度 ratio 明显在限值内
- `jerk` ratio 依然可能大于 `1`
这与旧抓包整理结果的观察一致:当前现场样本里,“加速度合规但跃度超限”是可接受现象,不能单凭 jerk 超限就否决 flyshot 轨迹。
## 当前代码决策范围
本次代码调整范围刻意收窄为:
- 只对 flyshot 链路关闭 `jerk` 阻断
- 保留 flyshot 的速度、加速度校验
- 不修改 `move_joint` 的 jerk 约束逻辑
对应入口包括:
- `ControllerClientTrajectoryOrchestrator.CreateLimitCompliantDenseTrajectory(...)`
- `FlyshotExecutionSendSequenceBuilder.Build(...)`
- `FlyshotTrajectoryArtifactWriter.WriteActualSendArtifacts(...)`
`move_joint` 仍保持原先的 jerk 受限实现,不共享这次豁免策略。
## 当前验收判断
对 `UTTC_MS11`:
- 只保留 flyshot 速度/加速度校验
- 跳过 flyshot jerk 阻断
- 新 `1.0x / 0.5x` 与旧抓包主段的剩余时长差异已经很小
- 当前判断:这点差异可以忽略
后续除非现场再次提供新的反证,否则本结论作为当前项目实现与验收的记录保留。