15 Commits

Author SHA1 Message Date
1779067b5c ✨ feat(fanuc): 打通飞拍轨迹完整执行链路
* 增加 J519 稠密发送采样校验与保姿回发逻辑
* 调整 saveTrajectory 导出与 sequence buffer 行为
* 补充 10010 解析脚本、ICSP 说明和回归测试
2026-05-08 13:25:02 +08:00
c6829d214a ✨ feat(*): 添加 J519 实发重采样与 JSON 机型模型
* 新增 J519 实发采样器,按 8ms 周期生成 timing/jerk 诊断行并完成 rad->deg 转换
* 兼容层产物导出补充 speedRatio,规划编排补齐 smoothStartStopTiming 与日志透传
* 配置与机型加载切换到运行目录 JSON 模型,并补齐 7L 展开模型与相关单元测试
2026-05-07 17:08:32 +08:00
70b0ccd414 ✨ feat(fanuc): 优化 J519 实时下发与飞拍起停整形
- 改为高优先级 J519 接收线程与复用缓冲区发送链路
- 增加稠密执行前的 J519 就绪重试与状态诊断
- 修正程序状态响应字段顺序与 EnableRobot 默认参数
- 为飞拍轨迹补充平滑起停时间轴与首尾整形验证
- 补充真实运行配置、报警窗口与边界对比测试
- 同步更新限值文档、分析脚本与 .NET 8 SDK 固定配置
2026-05-06 22:37:31 +08:00
783716ff44 ✨ feat(fanuc): 改为按状态包驱动 J519 队列发送
* 预生成稠密轨迹 J519 命令队列,等待机器人状态包逐帧出队
* 让 ExecuteTrajectory 在队列实际取完后返回,避免后台发送提前结束
* 新增 ActualSendTiming.txt,区分实发时间与 speed_ratio 采样时间
* 补充 J519 队列、等待完成和实发时间映射相关单元测试
* 同步文档中的 t_send / t_traj / speed_ratio 说明

Co-authored-by: Copilot <copilot@github.com>
2026-05-06 12:57:56 +08:00
b1710e5d01 ♻️ refactor(compat): 替换 MoveJoint 时间律为解析式 7 阶平滑函数并添加离散限位校验
* 将预捕获 alpha 数据表替换为解析式 7 阶平滑点到点时间律
  s(u)=35u⁴-84u⁵+70u⁶-20u⁷,形状系数按 1~3 阶导数最大值重算
* 新增离散限位校验:按真实 8ms 采样点反算速度/加速度/jerk,
  不满足时自动拉长总时长后重采样,最多迭代 10000 次
* 实发轨迹落盘:ActualSendJointTraj.txt(角度制)、
  ActualSendJerkStats.txt(点间跃度统计),按时间目录归档
* J519 AcceptsCommand 门控:只有机器人就绪时才发送下一帧,
  减少无效下发;状态日志附带最近发送目标关节轴
* FanucControllerRuntime 构造函数改为必选 ILogger 注入,
  确保 DI 解析时稳定拿到日志实例
* LegacyHttpApiController 移除已废弃的 ConnectServer 调用,
  EnableRobot 参数从 2 改为 4
* 新增跃度报警分析文档和六轴限值表,补充反馈远离拒绝测试

Co-authored-by: Copilot <copilot@github.com>
2026-05-06 09:06:28 +08:00
af65ca03a0 ✨ feat(compat): 补齐飞拍执行等待与 FANUC 状态驱动链路
- 为 ExecuteFlyShotTraj 补齐 wait 语义,并让 move_to_start
  先完成临时 PTP 运动后再启动正式飞拍轨迹
- 将 J519 命令发送改为由机器人 UDP status sequence 驱动,
  避免在未收到状态包时主动发周期命令
- 将 10010 状态通道关节字段统一按 JointRadians 命名,
  同步更新运行时读取逻辑与协议测试
- 新增 FANUC 10010 状态帧、流运动手册和 Python client
  逆向文档,并更新 README 与兼容需求说明
- 补充兼容层编排测试与 HTTP 集成测试,覆盖 wait 和
  move_to_start 串行化行为
2026-05-03 19:29:31 +08:00
91c1494cde ✨ feat(*): 添加轨迹产物导出与规划速度倍率隔离
* 新增 FlyshotTrajectoryArtifactWriter,支持 saveTrajectory
  将规划结果导出到 Config/Data/name(JointTraj、CartTraj、
  ShotEvents 等)
* RobotConfig 新增 PlanningSpeedScale,区分规划阶段限速倍率
  与运行时 J519 下发倍率
* 轨迹缓存键纳入 planningSpeedScale,避免降速规划误用缓存
* 完善 FanucCommandClient 命令参数日志与状态通道重连
* 补充 RuntimeOrchestrationTests 覆盖产物导出与倍率隔离
* 更新 README 进度文档
2026-04-30 13:52:09 +08:00
a6579f1e5b ✨ feat(*): 添加 ConfigRoot 运行时配置目录隔离
* 新增 ControllerClientCompatOptions.ConfigRoot 及解析方法
* 兼容层默认从运行目录 Config 加载模型、轨迹和配置
* 移除隐式父工作区根目录推断,旧路径仅在显式配置时生效
* Host 项目编译时将 Config 目录复制到输出目录
* 请求响应日志中间件忽略 /api/status/snapshot 高频轮询
* 补充 ConfigRoot 和日志过滤相关单元测试
2026-04-29 18:27:03 +08:00
c38faddbf0 ✨ feat(server): 添加静态状态页与调试入口
- 将状态页、调试页改为 `wwwroot` 静态资源
  - 补充调试配置接口与前端脚本
  - 为兼容层、规划层和运行时补充日志
  - 更新集成测试覆盖新入口
2026-04-29 14:05:02 +08:00
0724efebed ✨ feat(*): 完善 FANUC J519 闭环、MoveJoint 与现场抓包验证
* 划分 J519 发送循环与稠密轨迹循环职责边界,
  FanucJ519Client 负责 UDP 周期发送,
  FanucControllerRuntime 按轨迹时间更新下一帧命令
* 执行时将规划输出 rad 转为 J519 deg 目标,
  并按 speed_ratio 调整 8ms 发送时间尺度
* 补齐 accept_cmd/received_cmd/sysrdy/rbt_inmotion
  状态位解析与启动前闭环检查
* MoveJoint 改为关节空间直线 + smoothstep 进度
  的临时 PTP 稠密轨迹,按 status=15 运动窗口复现
* 新增 UTTC 2026-04-28 三份抓包 golden tests,
  覆盖 0.5/0.7/1.0 speed_ratio 下的 J519 命令、
  IO 脉冲与响应滞后
* 状态通道补充超时重连策略与退避逻辑
* TCP 10012 命令响应统一检查 result_code
* 状态页扩展 J519 状态位与快照诊断信息
* 新增 docs/fanuc-field-runtime-workflow.md 现场工作流
* 补充 LR Mate 200iD 模型、RobotConfig.json 与 workpiece
2026-04-29 01:03:18 +08:00
0292e077ff ✨ feat(server): 添加浏览器内 OpenAPI 调试页及诊断入口
* 新增 DebugConsoleController,提供 /debug 纯内嵌调试页
  - 零外部依赖,基于 Swagger JSON 自动生成各端点表单
  - 与 Swagger:Enabled 同步开关,避免生产环境误暴露
* 启用 <GenerateDocumentationFile>,将 XML 注释注入 OpenAPI
  - 调试页与 Swagger UI 共用同一份端点标题和说明
* 为 Health/Status/LegacyHttpApi 控制器添加 Tags 分组
* 补充 VS Code launch.json 与 tasks.json,支持现场调试
* 新增 DebugConsoleEndpointTests 覆盖调试页基础响应
* 同步更新 README 进度与待办清单
2026-04-27 10:33:53 +08:00
69fa3edd89 ✨ feat(runtime): 完善 FANUC 命令参数与状态通道重连
* 在 FanucCommandProtocol/Client 中补齐速度倍率、TCP 位姿和
  IO 的封包/解析,并引入 FanucIoTypes 字符串到枚举映射
* FanucControllerRuntime 在非仿真模式下接入真机命令通道,本地
  缓存仅作为兜底,TCP 操作扩展为 7 维 Pose
* FanucStateClient 增加帧超时检测、退避自动重连和诊断状态接口,
  超时或重连期间不再把陈旧帧当作当前机器人状态
* FanucStateProtocol 锁定 90B 帧字段为 pose[6]、joint[6]、
  external_axes[3] 和 raw_tail_words[4],并保留状态字诊断槽位
* ICspPlanner 增加 global_scale > 1.0 失败判定,self-adapt-icsp
  内部禁用该判定以保留补点重试链路
* 同步更新 README/AGENTS/计划文档的 todo 状态和实现说明
2026-04-27 00:18:50 +08:00
390d066ece ✨ feat(runtime): 添加轨迹持久化与密集执行链路
* 新增飞拍轨迹文件存储,支持上传、加载与删除
* 接通 ControllerClientCompat 到运行时的轨迹编排
* 完善 FANUC 命令与 J519 客户端发送链路
* 补充密集轨迹执行、运行时编排和协议客户端测试
* 更新 README 与 AGENTS 中的当前实现状态
2026-04-26 17:14:17 +08:00
a78e6761cb ✨ feat(fanuc): 添加协议编解码与状态页" -m "* 固化 10010 状态帧、10012 命令帧和 60015 J519 包编解码
* 扩展 ControllerClient 兼容层的执行参数和运行时编排
  * 新增 /status 页面与 /api/status/snapshot 状态快照接口
  * 补充 FANUC 协议、客户端和状态接口的最小验证测试
  * 更新 README、兼容要求和真机 Socket 通信实现计划
2026-04-24 21:26:25 +08:00
8a20d9f507 ✨ feat: 实现 ControllerClient HTTP 兼容层及 FANUC 运行时
- 新增 Flyshot.ControllerClientCompat 兼容层模块
  - 新增 Flyshot.Runtime.Fanuc 运行时模块
  - 新增 LegacyHttpApiController 暴露 HTTP 兼容 API
  - 补充 RuntimeOrchestrationTests 等测试覆盖
  - 补充 docs/ 兼容性需求与逆向工程文档
  - 更新 Host 注册、配置及解决方案引用

  变更概览:
  - Flyshot.ControllerClientCompat — 旧 ControllerClient 语义的 HTTP 适配
  - Flyshot.Runtime.Fanuc — IControllerRuntime 的 FANUC 真机实现
  - LegacyHttpApiController — HTTP API 兼容旧 SDK
  - docs/ — 兼容性需求与逆向工程分析文档
  - 测试:RuntimeOrchestrationTests、LegacyHttpApiCompatibilityTests
2026-04-24 16:55:25 +08:00
64 changed files with 570 additions and 11507 deletions

View File

@@ -6,13 +6,11 @@
8 8
], ],
"io_keep_cycles": 2, "io_keep_cycles": 2,
"trigger_sample_index_offset_cycles": 6,
"acc_limit": 1, "acc_limit": 1,
"jerk_limit": 1, "jerk_limit": 1,
"adapt_icsp_try_num": 5, "adapt_icsp_try_num": 5,
"speed_ratio": 1,
"planning_speed_scale": 0.74227, "planning_speed_scale": 0.74227,
"smooth_start_stop_timing": true "smooth_start_stop_timing": false
}, },
"flying_shots": { "flying_shots": {
"UTTC_MS11": { "UTTC_MS11": {
@@ -302,4 +300,4 @@
] ]
} }
} }
} }

View File

@@ -21,7 +21,6 @@
- 2026-04-28 UTTC 抓包确认:UDP 60015 命令 `target[0..5]` 为关节角度制 `deg`,`JointDetialTraj.txt` 为弧度制 `rad`,`speed_ratio=0.7` 体现为 UDP 下发时间轴约 `1.427730x` 拉伸;2026-04-30 实体机确认 `speed_ratio` 不影响生成的 `JointTraj.txt` 规划时长,当前实际生成约 `7.4s` 轨迹。 - 2026-04-28 UTTC 抓包确认:UDP 60015 命令 `target[0..5]` 为关节角度制 `deg`,`JointDetialTraj.txt` 为弧度制 `rad`,`speed_ratio=0.7` 体现为 UDP 下发时间轴约 `1.427730x` 拉伸;2026-04-30 实体机确认 `speed_ratio` 不影响生成的 `JointTraj.txt` 规划时长,当前实际生成约 `7.4s` 轨迹。
- 2026-04-30 本机 `50001/TCP+JSON` 抓包确认:`ExecuteFlyShotTraj(save_traj=true,use_cache=false)` 请求只显式携带规划方法、保存、缓存和等待参数,不携带 `JointLimits / acc_limit / jerk_limit / velocity / acceleration / jerk`。因此旧系统不可见的有效规划限制不再继续假设来自公开链路,新系统按 replacement-only 内部参数限制规划加速度。 - 2026-04-30 本机 `50001/TCP+JSON` 抓包确认:`ExecuteFlyShotTraj(save_traj=true,use_cache=false)` 请求只显式携带规划方法、保存、缓存和等待参数,不携带 `JointLimits / acc_limit / jerk_limit / velocity / acceleration / jerk`。因此旧系统不可见的有效规划限制不再继续假设来自公开链路,新系统按 replacement-only 内部参数限制规划加速度。
- 真机 Socket 客户端已具备基础连接、程序启停、速度倍率/TCP/IO 参数命令和 J519 状态包驱动发送能力;稠密轨迹下发已按 `speed_ratio` 做执行时间缩放,并已用 0.5/0.7/1.0 三份 UTTC 抓包固化 J519 golden tests。真实 R30iB 全流程现场联调仍需执行。 - 真机 Socket 客户端已具备基础连接、程序启停、速度倍率/TCP/IO 参数命令和 J519 状态包驱动发送能力;稠密轨迹下发已按 `speed_ratio` 做执行时间缩放,并已用 0.5/0.7/1.0 三份 UTTC 抓包固化 J519 golden tests。真实 R30iB 全流程现场联调仍需执行。
- 2026-05-11 对 `UTTC_MS11` 的独立 `ConfigRoot` 重新生成确认:flyshot 链路若继续用离散 `jerk` 作为阻断条件,会把新结果拉长到约 `16.576s / 33.152s`;只保留速度、加速度校验并跳过 flyshot jerk 阻断后,新 `1.0x / 0.5x` 时长回落到约 `7.552s / 15.104s`,与旧抓包主段 `7.408s / 14.808s` 的差异已很小,当前按可忽略处理。详见 `docs/uttc-ms11-jerk-validation-decision-20260511.md`。
- `MoveJoint` 已按 `2026042802-mvpoint*.pcap` 复刻为点到点临时轨迹:当前关节到目标关节的关节空间直线,五次 smoothstep 起停,按 `status=15` 运动窗口复现 `40/55/77` 点,并由 J519 层完成 `rad -> deg` 下发。 - `MoveJoint` 已按 `2026042802-mvpoint*.pcap` 复刻为点到点临时轨迹:当前关节到目标关节的关节空间直线,五次 smoothstep 起停,按 `status=15` 运动窗口复现 `40/55/77` 点,并由 J519 层完成 `rad -> deg` 下发。
- 单程序只对应一台机器人,上传/删除/恢复飞拍轨迹统一读写运行目录 `Config/RobotConfig.json`,不再创建独立轨迹存储文件。 - 单程序只对应一台机器人,上传/删除/恢复飞拍轨迹统一读写运行目录 `Config/RobotConfig.json`,不再创建独立轨迹存储文件。

View File

@@ -1,21 +0,0 @@
{
"pcap_path": "D:\\Dev\\Codes\\rvbust-code\\FlyingShotPkg_3.15_VDA\\Rvbust\\uttc-20260428\\2026042802-1.pcap",
"all_command_count": 1788,
"trigger_count": 17,
"existing_runtime_actual_send_exists": true,
"existing_runtime_actual_send_has_io_columns": false,
"existing_shot_events_exists": true,
"pcap_specific_combined_export_preexisting": false,
"average_max_error_deg": 4.241583591714439,
"max_error_deg": 11.130744251720401,
"average_rms_error_deg": 2.5406136706026206,
"max_error_axis_counter": {
"J6": 9,
"J4": 5,
"J5": 1,
"J2": 1,
"J1": 1
},
"order_only_addr_mismatch_count": 14,
"real_addr_mismatch_count": 0
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,18 +0,0 @@
frame_number,time_relative_s,sequence,last_data,write_io_type,write_io_index,write_io_mask,write_io_value,io_addrs,j1_deg,j2_deg,j3_deg,j4_deg,j5_deg,j6_deg,ext1_deg,ext2_deg,ext3_deg
1955,5.888271,1381124,0,2,1,10,10,"[2, 4]",45.8632698059082,2.9724788665771484,-13.609341621398926,-2.1851141452789307,7.582361221313477,5.701517581939697,0.0,0.0,0.0
2151,6.480252,1381198,0,2,1,14,14,"[2, 3, 4]",56.34336471557617,11.66723918914795,-6.600923538208008,-68.1976089477539,6.058437347412109,71.5392837524414,0.0,0.0,0.0
2223,6.704225,1381226,0,2,1,14,14,"[2, 3, 4]",53.50831985473633,7.412222862243652,-9.629739761352539,-32.30571365356445,12.488306045532227,30.826189041137695,0.0,0.0,0.0
2336,7.048266,1381269,0,2,1,10,10,"[2, 4]",44.433006286621094,-2.1156294345855713,-18.39777374267578,4.8371992111206055,33.99372482299805,-20.26228904724121,0.0,0.0,0.0
2477,7.464289,1381321,0,2,1,10,10,"[2, 4]",65.84967041015625,5.695003032684326,-14.563636779785156,-28.69342613220215,27.678611755371094,46.437156677246094,0.0,0.0,0.0
2665,8.024277,1381391,0,2,1,12,12,"[3, 4]",60.847530364990234,24.47777557373047,-5.617043972015381,34.8044319152832,5.786255359649658,-31.2253475189209,0.0,0.0,0.0
2811,8.472317,1381447,0,2,1,12,12,"[3, 4]",70.8250503540039,10.95259952545166,-13.136046409606934,-34.68459701538086,14.022420883178711,38.90604019165039,0.0,0.0,0.0
2908,8.760361,1381483,0,2,1,10,10,"[2, 4]",69.40516662597656,-20.066781997680664,-8.152935981750488,-63.93235397338867,6.775912284851074,70.37761688232422,0.0,0.0,0.0
3112,9.384332,1381561,0,2,1,10,10,"[2, 4]",73.90592956542969,-8.016124725341797,-9.002426147460938,-131.3327178955078,15.526206970214844,142.41668701171875,0.0,0.0,0.0
3170,9.560358,1381583,0,2,1,10,10,"[2, 4]",76.72688293457031,-16.518726348876953,-7.15147066116333,-127.53003692626953,18.53788948059082,138.7897491455078,0.0,0.0,0.0
3350,10.104322,1381651,0,2,1,10,10,"[2, 4]",83.71662139892578,-13.186573028564453,-13.738808631896973,-72.70317840576172,26.66758918762207,91.98423767089844,0.0,0.0,0.0
3470,10.472333,1381697,0,2,1,10,10,"[2, 4]",59.78955841064453,-2.7094168663024902,-8.933828353881836,-108.24857330322266,21.177492141723633,114.14110565185547,0.0,0.0,0.0
3627,10.944371,1381756,0,2,1,12,12,"[3, 4]",86.41329193115234,4.266173362731934,-19.15987777709961,-52.24028015136719,29.853723526000977,55.86907196044922,0.0,0.0,0.0
3710,11.192339,1381787,0,2,1,10,10,"[2, 4]",110.7523193359375,0.8751887679100037,-34.756534576416016,-48.14332580566406,45.0865592956543,87.97444152832031,0.0,0.0,0.0
3796,11.456361,1381820,0,2,1,10,10,"[2, 4]",111.79177856445312,-6.225440502166748,-31.958988189697266,-53.68573760986328,44.76150894165039,89.33454895019531,0.0,0.0,0.0
3870,11.680362,1381848,0,2,1,10,10,"[2, 4]",117.87474060058594,-19.607177734375,-30.329103469848633,-56.600250244140625,49.08655548095703,90.32424926757812,0.0,0.0,0.0
4138,12.496365,1381950,0,2,1,12,12,"[3, 4]",59.93589401245117,-21.102140426635742,-9.684239387512207,70.64311981201172,32.941307067871094,-63.34855651855469,0.0,0.0,0.0
1 frame_number time_relative_s sequence last_data write_io_type write_io_index write_io_mask write_io_value io_addrs j1_deg j2_deg j3_deg j4_deg j5_deg j6_deg ext1_deg ext2_deg ext3_deg
2 1955 5.888271 1381124 0 2 1 10 10 [2, 4] 45.8632698059082 2.9724788665771484 -13.609341621398926 -2.1851141452789307 7.582361221313477 5.701517581939697 0.0 0.0 0.0
3 2151 6.480252 1381198 0 2 1 14 14 [2, 3, 4] 56.34336471557617 11.66723918914795 -6.600923538208008 -68.1976089477539 6.058437347412109 71.5392837524414 0.0 0.0 0.0
4 2223 6.704225 1381226 0 2 1 14 14 [2, 3, 4] 53.50831985473633 7.412222862243652 -9.629739761352539 -32.30571365356445 12.488306045532227 30.826189041137695 0.0 0.0 0.0
5 2336 7.048266 1381269 0 2 1 10 10 [2, 4] 44.433006286621094 -2.1156294345855713 -18.39777374267578 4.8371992111206055 33.99372482299805 -20.26228904724121 0.0 0.0 0.0
6 2477 7.464289 1381321 0 2 1 10 10 [2, 4] 65.84967041015625 5.695003032684326 -14.563636779785156 -28.69342613220215 27.678611755371094 46.437156677246094 0.0 0.0 0.0
7 2665 8.024277 1381391 0 2 1 12 12 [3, 4] 60.847530364990234 24.47777557373047 -5.617043972015381 34.8044319152832 5.786255359649658 -31.2253475189209 0.0 0.0 0.0
8 2811 8.472317 1381447 0 2 1 12 12 [3, 4] 70.8250503540039 10.95259952545166 -13.136046409606934 -34.68459701538086 14.022420883178711 38.90604019165039 0.0 0.0 0.0
9 2908 8.760361 1381483 0 2 1 10 10 [2, 4] 69.40516662597656 -20.066781997680664 -8.152935981750488 -63.93235397338867 6.775912284851074 70.37761688232422 0.0 0.0 0.0
10 3112 9.384332 1381561 0 2 1 10 10 [2, 4] 73.90592956542969 -8.016124725341797 -9.002426147460938 -131.3327178955078 15.526206970214844 142.41668701171875 0.0 0.0 0.0
11 3170 9.560358 1381583 0 2 1 10 10 [2, 4] 76.72688293457031 -16.518726348876953 -7.15147066116333 -127.53003692626953 18.53788948059082 138.7897491455078 0.0 0.0 0.0
12 3350 10.104322 1381651 0 2 1 10 10 [2, 4] 83.71662139892578 -13.186573028564453 -13.738808631896973 -72.70317840576172 26.66758918762207 91.98423767089844 0.0 0.0 0.0
13 3470 10.472333 1381697 0 2 1 10 10 [2, 4] 59.78955841064453 -2.7094168663024902 -8.933828353881836 -108.24857330322266 21.177492141723633 114.14110565185547 0.0 0.0 0.0
14 3627 10.944371 1381756 0 2 1 12 12 [3, 4] 86.41329193115234 4.266173362731934 -19.15987777709961 -52.24028015136719 29.853723526000977 55.86907196044922 0.0 0.0 0.0
15 3710 11.192339 1381787 0 2 1 10 10 [2, 4] 110.7523193359375 0.8751887679100037 -34.756534576416016 -48.14332580566406 45.0865592956543 87.97444152832031 0.0 0.0 0.0
16 3796 11.456361 1381820 0 2 1 10 10 [2, 4] 111.79177856445312 -6.225440502166748 -31.958988189697266 -53.68573760986328 44.76150894165039 89.33454895019531 0.0 0.0 0.0
17 3870 11.680362 1381848 0 2 1 10 10 [2, 4] 117.87474060058594 -19.607177734375 -30.329103469848633 -56.600250244140625 49.08655548095703 90.32424926757812 0.0 0.0 0.0
18 4138 12.496365 1381950 0 2 1 12 12 [3, 4] 59.93589401245117 -21.102140426635742 -9.684239387512207 70.64311981201172 32.941307067871094 -63.34855651855469 0.0 0.0 0.0

View File

@@ -1,37 +0,0 @@
{
"pcap_path": "D:\\Dev\\Codes\\rvbust-code\\FlyingShotPkg_3.15_VDA\\Rvbust\\uttc-20260428\\2026042802-1.pcap",
"command_count": 1788,
"trigger_count": 17,
"command_minus_paired_status_sequence_counter": {
"8": 17
},
"paired_status_average_max_error_deg": 6.469219290127501,
"paired_status_max_error_deg": 16.123934474475888,
"paired_status_max_error_axis_counter": {
"J1": 2,
"J6": 9,
"J2": 4,
"J4": 2
},
"best_status_average_max_error_deg": 0.14953970473465206,
"best_status_max_error_deg": 0.5696919293327056,
"best_status_max_error_axis_counter": {
"J6": 10,
"J1": 3,
"J5": 1,
"J2": 2,
"J4": 1
},
"best_status_delta_from_paired_cycles_counter": {
"10": 8,
"9": 9
},
"best_status_delta_from_trigger_sequence_counter": {
"2": 8,
"1": 9
},
"best_status_time_after_trigger_ms_min": 71.83799999999962,
"best_status_time_after_trigger_ms_max": 79.96399999999859,
"best_status_time_after_trigger_ms_avg": 75.6613529411763,
"search_window_cycles": 20
}

View File

@@ -1,18 +0,0 @@
trigger_no,waypoint_index,trigger_command_sequence,trigger_command_frame,trigger_command_time_relative_s,trigger_current_status_sequence,trigger_current_status_frame,trigger_current_status_time_relative_s,command_leads_status_cycles,trigger_current_status_max_error_axis,trigger_current_status_max_error_deg,trigger_current_status_rms_error_deg,best_status_sequence,best_status_frame,best_status_time_relative_s,best_status_delay_from_current_status_cycles,best_status_delay_from_trigger_command_cycles,best_status_delay_from_trigger_command_ms,best_status_max_error_axis,best_status_max_error_deg,best_status_rms_error_deg
1,1,1381124,1955,5.888271,1381116,1954,5.888166,8,J1,6.588241610414407,3.8225443630072706,1381126,1981,5.968124,10,2,79.85300000000083,J6,0.20758988641565335,0.12239685259209541
2,2,1381198,2151,6.480252,1381190,2150,6.480138,8,J1,2.33531536749085,1.3827543088729335,1381199,2172,6.552164,9,1,71.9120000000002,J1,0.08213390997131853,0.04616054814443023
3,3,1381226,2223,6.704225,1381218,2222,6.704166,8,J6,16.123934474475888,8.852376616648156,1381228,2248,6.784154,10,2,79.92899999999992,J6,0.5696919293327056,0.3239642953120798
4,4,1381269,2336,7.048266,1381261,2335,7.048148,8,J6,4.901882807288516,3.2440556235708993,1381271,2362,7.128175,10,2,79.90899999999979,J6,0.09470430997406254,0.06580474324148583
5,5,1381321,2477,7.464289,1381313,2476,7.464178,8,J6,9.21943820204428,4.621321121191458,1381323,2502,7.544122,10,2,79.83299999999983,J5,0.056622412878706285,0.03357478517689402
6,6,1381391,2665,8.024277,1381383,2664,8.024156,8,J6,6.1464605154096255,3.0478405356855083,1381392,2687,8.096181,9,1,71.90399999999997,J6,0.17697427883735983,0.08563125303054821
7,7,1381447,2811,8.472317,1381439,2810,8.472208,8,J6,13.15171783052326,7.78202379046887,1381448,2834,8.54419,9,1,71.87300000000008,J6,0.30658691011310424,0.18210669390591905
8,8,1381483,2908,8.760361,1381475,2907,8.760245,8,J6,7.436279407197546,5.127041386597523,1381484,2929,8.832199,9,1,71.83799999999962,J2,0.06074767130727565,0.02750705896232285
9,9,1381561,3112,9.384332,1381553,3111,9.384204,8,J6,5.585605293050492,3.277206017832995,1381562,3134,9.456205,9,1,71.87300000000008,J4,0.15225994654176134,0.089150370994325
10,10,1381583,3170,9.560358,1381575,3169,9.56025,8,J2,4.165077087081428,2.128983861449047,1381585,3196,9.640214,10,2,79.85599999999948,J6,0.0734395372110157,0.043285948566531826
11,11,1381651,3350,10.104322,1381643,3349,10.104215,8,J2,1.8309533977318093,1.169340011162825,1381652,3373,10.17622,9,1,71.8980000000009,J6,0.07766831894582538,0.04246508921571141
12,12,1381697,3470,10.472333,1381689,3469,10.472191,8,J4,4.822353276363401,2.8611252897135864,1381698,3493,10.544283,9,1,71.94999999999929,J1,0.021077280093038553,0.014935517911187784
13,13,1381756,3627,10.944371,1381748,3626,10.944251,8,J4,8.152507963730145,5.168659196117862,1381757,3651,11.016226,9,1,71.85499999999934,J1,0.14554304541260876,0.0699602863618738
14,14,1381787,3710,11.192339,1381779,3709,11.19225,8,J6,10.40441920064211,5.679810575214033,1381789,3735,11.272261,10,2,79.92199999999983,J6,0.11945317484617135,0.05835950688086092
15,15,1381820,3796,11.456361,1381812,3795,11.456253,8,J2,2.6161077284098373,1.5911754635072362,1381822,3821,11.536287,10,2,79.92600000000039,J2,0.14980253942005772,0.08716897465096986
16,16,1381848,3870,11.680362,1381840,3869,11.680263,8,J2,4.682473812334727,2.2067399623329567,1381850,3895,11.760326,10,2,79.96399999999859,J6,0.17295643126782068,0.10419421346699378
17,18,1381950,4138,12.496365,1381942,4137,12.496271,8,J6,1.8139599579791934,1.1536991630766555,1381951,4159,12.568313,9,1,71.94799999999901,J6,0.07492339792059965,0.03834497543010648
1 trigger_no waypoint_index trigger_command_sequence trigger_command_frame trigger_command_time_relative_s trigger_current_status_sequence trigger_current_status_frame trigger_current_status_time_relative_s command_leads_status_cycles trigger_current_status_max_error_axis trigger_current_status_max_error_deg trigger_current_status_rms_error_deg best_status_sequence best_status_frame best_status_time_relative_s best_status_delay_from_current_status_cycles best_status_delay_from_trigger_command_cycles best_status_delay_from_trigger_command_ms best_status_max_error_axis best_status_max_error_deg best_status_rms_error_deg
2 1 1 1381124 1955 5.888271 1381116 1954 5.888166 8 J1 6.588241610414407 3.8225443630072706 1381126 1981 5.968124 10 2 79.85300000000083 J6 0.20758988641565335 0.12239685259209541
3 2 2 1381198 2151 6.480252 1381190 2150 6.480138 8 J1 2.33531536749085 1.3827543088729335 1381199 2172 6.552164 9 1 71.9120000000002 J1 0.08213390997131853 0.04616054814443023
4 3 3 1381226 2223 6.704225 1381218 2222 6.704166 8 J6 16.123934474475888 8.852376616648156 1381228 2248 6.784154 10 2 79.92899999999992 J6 0.5696919293327056 0.3239642953120798
5 4 4 1381269 2336 7.048266 1381261 2335 7.048148 8 J6 4.901882807288516 3.2440556235708993 1381271 2362 7.128175 10 2 79.90899999999979 J6 0.09470430997406254 0.06580474324148583
6 5 5 1381321 2477 7.464289 1381313 2476 7.464178 8 J6 9.21943820204428 4.621321121191458 1381323 2502 7.544122 10 2 79.83299999999983 J5 0.056622412878706285 0.03357478517689402
7 6 6 1381391 2665 8.024277 1381383 2664 8.024156 8 J6 6.1464605154096255 3.0478405356855083 1381392 2687 8.096181 9 1 71.90399999999997 J6 0.17697427883735983 0.08563125303054821
8 7 7 1381447 2811 8.472317 1381439 2810 8.472208 8 J6 13.15171783052326 7.78202379046887 1381448 2834 8.54419 9 1 71.87300000000008 J6 0.30658691011310424 0.18210669390591905
9 8 8 1381483 2908 8.760361 1381475 2907 8.760245 8 J6 7.436279407197546 5.127041386597523 1381484 2929 8.832199 9 1 71.83799999999962 J2 0.06074767130727565 0.02750705896232285
10 9 9 1381561 3112 9.384332 1381553 3111 9.384204 8 J6 5.585605293050492 3.277206017832995 1381562 3134 9.456205 9 1 71.87300000000008 J4 0.15225994654176134 0.089150370994325
11 10 10 1381583 3170 9.560358 1381575 3169 9.56025 8 J2 4.165077087081428 2.128983861449047 1381585 3196 9.640214 10 2 79.85599999999948 J6 0.0734395372110157 0.043285948566531826
12 11 11 1381651 3350 10.104322 1381643 3349 10.104215 8 J2 1.8309533977318093 1.169340011162825 1381652 3373 10.17622 9 1 71.8980000000009 J6 0.07766831894582538 0.04246508921571141
13 12 12 1381697 3470 10.472333 1381689 3469 10.472191 8 J4 4.822353276363401 2.8611252897135864 1381698 3493 10.544283 9 1 71.94999999999929 J1 0.021077280093038553 0.014935517911187784
14 13 13 1381756 3627 10.944371 1381748 3626 10.944251 8 J4 8.152507963730145 5.168659196117862 1381757 3651 11.016226 9 1 71.85499999999934 J1 0.14554304541260876 0.0699602863618738
15 14 14 1381787 3710 11.192339 1381779 3709 11.19225 8 J6 10.40441920064211 5.679810575214033 1381789 3735 11.272261 10 2 79.92199999999983 J6 0.11945317484617135 0.05835950688086092
16 15 15 1381820 3796 11.456361 1381812 3795 11.456253 8 J2 2.6161077284098373 1.5911754635072362 1381822 3821 11.536287 10 2 79.92600000000039 J2 0.14980253942005772 0.08716897465096986
17 16 16 1381848 3870 11.680362 1381840 3869 11.680263 8 J2 4.682473812334727 2.2067399623329567 1381850 3895 11.760326 10 2 79.96399999999859 J6 0.17295643126782068 0.10419421346699378
18 17 18 1381950 4138 12.496365 1381942 4137 12.496271 8 J6 1.8139599579791934 1.1536991630766555 1381951 4159 12.568313 9 1 71.94799999999901 J6 0.07492339792059965 0.03834497543010648

View File

@@ -1,18 +0,0 @@
trigger_no,waypoint_index,best_sample_order,best_frame_number,best_sequence,best_time_relative_s,teach_j1_deg,teach_j2_deg,teach_j3_deg,teach_j4_deg,teach_j5_deg,teach_j6_deg,offset_6_frame_number,offset_6_sequence,offset_6_time_relative_s,offset_6_max_error_axis,offset_6_max_error_deg,offset_6_rms_error_deg,offset_6_delta_from_best_ms,offset_6_j1_actual_deg,offset_6_diff_j1_deg,offset_6_j2_actual_deg,offset_6_diff_j2_deg,offset_6_j3_actual_deg,offset_6_diff_j3_deg,offset_6_j4_actual_deg,offset_6_diff_j4_deg,offset_6_j5_actual_deg,offset_6_diff_j5_deg,offset_6_j6_actual_deg,offset_6_diff_j6_deg,offset_7_frame_number,offset_7_sequence,offset_7_time_relative_s,offset_7_max_error_axis,offset_7_max_error_deg,offset_7_rms_error_deg,offset_7_delta_from_best_ms,offset_7_j1_actual_deg,offset_7_diff_j1_deg,offset_7_j2_actual_deg,offset_7_diff_j2_deg,offset_7_j3_actual_deg,offset_7_diff_j3_deg,offset_7_j4_actual_deg,offset_7_diff_j4_deg,offset_7_j5_actual_deg,offset_7_diff_j5_deg,offset_7_j6_actual_deg,offset_7_diff_j6_deg,offset_8_frame_number,offset_8_sequence,offset_8_time_relative_s,offset_8_max_error_axis,offset_8_max_error_deg,offset_8_rms_error_deg,offset_8_delta_from_best_ms,offset_8_j1_actual_deg,offset_8_diff_j1_deg,offset_8_j2_actual_deg,offset_8_diff_j2_deg,offset_8_j3_actual_deg,offset_8_diff_j3_deg,offset_8_j4_actual_deg,offset_8_diff_j4_deg,offset_8_j5_actual_deg,offset_8_diff_j5_deg,offset_8_j6_actual_deg,offset_8_diff_j6_deg,best_of_6_7_8_offset
1,1,730,1941,1381118,5.840207,48.886810269468405,2.1989850886957285,-11.021017368511105,0.4102097980549552,6.248381265333557,2.294990756284542,1955,1381124,5.888271,J6,3.4065268256551553,2.468438977201345,48.06399999999922,45.8632698059082,-3.0235404635602023,2.9724788665771484,0.7734937778814199,-13.609341621398926,-2.588324252887821,-2.1851141452789307,-2.5953239433338857,7.582361221313477,1.3339799559799195,5.701517581939697,3.4065268256551553,1958,1381125,5.896184,J6,4.180931828249149,2.9355776062802286,55.9769999999995,45.449710845947266,-3.43709942352114,3.117816925048828,0.9188318363530996,-13.963269233703613,-2.9422518651925085,-2.845515012741089,-3.255724810796044,7.768581867218018,1.5202006018844605,6.475922584533691,4.180931828249149,1961,1381126,5.904272,J6,5.0045521476156045,3.418446646752538,64.06499999999937,45.06731414794922,-3.8194961215191867,3.2660434246063232,1.0670583359105947,-14.290565490722656,-3.2695481222115514,-3.562589168548584,-3.972798966603539,7.9421586990356445,1.6937774337020874,7.2995429039001465,5.0045521476156045,6
2,2,804,2135,1381192,6.432175,55.34775509527405,11.807039833001637,-7.0090952672806885,-71.01433145543973,6.012065051914967,74.24953191606797,2151,1381198,6.480252,J4,2.8167225076858244,1.6562461612538633,48.07700000000015,56.34336471557617,0.995609620302119,11.66723918914795,-0.13980064385368784,-6.600923538208008,0.40817172907268073,-68.1976089477539,2.8167225076858244,6.058437347412109,0.04637229549714217,71.5392837524414,-2.7102481636265594,2153,1381199,6.488246,J4,3.526759739131137,2.060948429836345,56.071000000000204,56.451515197753906,1.1037601024798533,11.618118286132812,-0.18892154686882456,-6.580313682556152,0.4287815847245362,-67.4875717163086,3.526759739131137,6.09804105758667,0.08597600567170272,70.84339141845703,-3.4061404976109344,2155,1381200,6.496271,J4,4.301478977412387,2.5033932349312007,64.09600000000015,56.54535675048828,1.1976016552142283,11.561552047729492,-0.24548778527214488,-6.571277618408203,0.4378176488724854,-66.71285247802734,4.301478977412387,6.147788047790527,0.13572299587556014,70.078857421875,-4.170674494192966,6
3,3,832,2207,1381220,6.656264,55.109808014787404,8.759497374223619,-8.518216825284897,-41.11725046606459,10.108488114686867,41.956933292858096,2223,1381226,6.704225,J6,11.130744251720401,5.955607695749779,47.96099999999992,53.50831985473633,-1.6014881600510762,7.412222862243652,-1.3472745119799665,-9.629739761352539,-1.111522936067642,-32.30571365356445,8.811536812500137,12.488306045532227,2.379817930845359,30.826189041137695,-11.130744251720401,2227,1381227,6.712268,J6,13.165517124645206,7.03533795629365,56.00399999999972,53.18336486816406,-1.9264431466233418,7.154932975769043,-1.6045643984545759,-9.842713356018066,-1.3244965307331693,-30.750762939453125,10.366487526611465,12.961709976196289,2.8532218615094216,28.79141616821289,-13.165517124645206,2229,1381228,6.720286,J6,15.212756428234073,8.119717107623766,64.02199999999958,52.84844207763672,-2.2613659371506856,6.892857074737549,-1.86664029948607,-10.059968948364258,-1.5417521230793607,-29.200178146362305,11.917072319702285,13.449018478393555,3.340530363706687,26.744176864624023,-15.212756428234073,6
4,4,875,2321,1381263,7.000291,43.65359310453334,-1.629659559507137,-17.715753611915318,5.995208633582219,32.17171770646655,-22.573973337684023,2336,1381269,7.048266,J6,2.3116842904428125,1.3731583485126422,47.9750000000001,44.433006286621094,0.779413182087751,-2.1156294345855713,-0.48596987507843425,-18.39777374267578,-0.6820201307604634,4.8371992111206055,-1.158009422461613,33.99372482299805,1.8220071165314948,-20.26228904724121,2.3116842904428125,2339,1381270,7.056293,J6,3.083185831580508,1.7437179916275436,56.00200000000033,44.65972137451172,1.006128269978376,-2.1617355346679688,-0.5320759751608317,-18.490602493286133,-0.774848881370815,4.406780242919922,-1.5884283906622967,34.24979019165039,2.0780724851838386,-19.490787506103516,3.083185831580508,2341,1381271,7.064278,J6,3.9520155587533594,2.15807761350517,63.987000000000016,44.91068649291992,1.2570933883865791,-2.196718215942383,-0.5670586564352458,-18.575654983520508,-0.85990137160519,3.9164717197418213,-2.0787369138403973,34.48752212524414,2.3158044187775886,-18.621957778930664,3.9520155587533594,6
5,5,927,2457,1381315,7.416267,64.58244475717193,4.262979315506351,-15.669217122643433,-29.952927185666542,29.850439933020308,45.62652743544272,2477,1381321,7.464289,J5,2.171828177649214,1.404704060246151,48.021999999999565,65.84967041015625,1.2672256529843224,5.695003032684326,1.4320237171779748,-14.563636779785156,1.1055803428582767,-28.69342613220215,1.259501053464394,27.678611755371094,-2.171828177649214,46.437156677246094,0.810629241803376,2479,1381322,7.472265,J5,2.585320380408003,1.6670103454750829,55.99799999999977,66.01153564453125,1.4290908873593224,5.957343578338623,1.6943642628322717,-14.353693008422852,1.3155241142205814,-28.232824325561523,1.720102860105019,27.265119552612305,-2.585320380408003,46.24872589111328,0.6221984556705635,2483,1381323,7.480284,J5,3.0049218208376907,1.9507458917658702,64.01699999999977,66.15470886230469,1.57226410513276,6.221103191375732,1.958123875869381,-14.140877723693848,1.5283393989495853,-27.70670509338379,2.2462220922827534,26.845518112182617,-3.0049218208376907,45.96609115600586,0.33956372056314166,6
6,6,997,2649,1381385,7.976283,60.47948252708421,23.068781981390185,-5.01126420129949,36.10685486878251,5.525681179628412,-31.3025636495649,2665,1381391,8.024277,J2,1.408993592340284,0.8424031169595625,47.99400000000009,60.847530364990234,0.36804783790602613,24.47777557373047,1.408993592340284,-5.617043972015381,-0.6057797707158912,34.8044319152832,-1.3024229534993097,5.786255359649658,0.260574180021246,-31.2253475189209,0.07721613064400046,2667,1381392,8.032258,J4,1.7701696881184503,1.0669192783996628,55.975000000000996,60.95240783691406,0.47292530982985426,24.69621467590332,1.6274326945131357,-5.75187873840332,-0.7406145371038306,34.33668518066406,-1.7701696881184503,5.885775089263916,0.3600939096355038,-30.920429229736328,0.38213441982857077,2669,1381393,8.040307,J4,2.303903056038372,1.3309750271252634,64.02400000000074,61.06847381591797,0.5889912888337605,24.908491134643555,1.83970915325337,-5.895160675048828,-0.8838964737493384,33.80295181274414,-2.303903056038372,5.999467372894287,0.4737861932658749,-30.538488388061523,0.7640752615033755,6
7,7,1053,2795,1381441,8.424315,70.47583707168602,16.39384887825908,-13.456948007467595,-27.892318852946243,14.53566195089614,31.71193282686115,2811,1381447,8.472317,J6,7.1941073647892395,4.618545262978315,48.00200000000032,70.8250503540039,0.3492132823178906,10.95259952545166,-5.4412493528074215,-13.136046409606934,0.3209015978606615,-34.68459701538086,-6.7922781624346165,14.022420883178711,-0.5132410677174288,38.90604019165039,7.1941073647892395,2814,1381448,8.480253,J6,8.24868042387127,5.331515658278243,55.93799999999938,70.84622192382812,0.37038485214210937,9.979106903076172,-6.41474197518291,-13.042067527770996,0.414880479696599,-35.67851257324219,-7.786193720295945,13.882524490356445,-0.6531374605396945,39.96061325073242,8.24868042387127,2817,1381449,8.488291,J6,9.275558780804865,6.035874346469871,63.976000000000255,70.85871887207031,0.38288180038429687,8.986706733703613,-7.407142144555468,-12.9381742477417,0.5187737597258959,-36.64576721191406,-8.75344835896782,13.72922134399414,-0.8064406069019991,40.987491607666016,9.275558780804865,6
8,8,1089,2892,1381477,8.71231,69.58246408878419,-17.10571341532583,-8.710589696831251,-58.47569441890704,7.63059003611043,64.43773280685575,2908,1381483,8.760361,J6,5.93988407546847,3.533132924843701,48.05099999999918,69.40516662597656,-0.1772974628076298,-20.066781997680664,-2.9610685823548337,-8.152935981750488,0.5576537150807628,-63.93235397338867,-5.456659554481632,6.775912284851074,-0.8546777512593557,70.37761688232422,5.93988407546847,2910,1381484,8.768294,J6,6.922168621366907,4.090127751125079,55.9839999999987,69.39288330078125,-0.1895807880029423,-20.37982177734375,-3.2741083620179197,-8.094615936279297,0.6159737605519542,-64.83366394042969,-6.357969521522648,6.682157039642334,-0.948432996468096,71.35990142822266,6.922168621366907,2912,1381485,8.77637,J6,7.916950115507532,4.649086823187336,64.05999999999956,69.38455200195312,-0.1979120868310673,-20.654869079589844,-3.5491556642640134,-8.043557167053223,0.6670325297780284,-65.74622344970703,-7.270529030799992,6.598722457885742,-1.0318675782246878,72.35468292236328,7.916950115507532,6
9,9,1167,3096,1381555,9.336375,73.57125874861414,-6.42984524964374,-9.628579811400881,-128.80864727564332,14.671082817606491,140.00204816414424,3112,1381561,9.384332,J4,2.5240706198644887,1.630593251762862,47.95700000000025,73.90592956542969,0.3346708168155459,-8.016124725341797,-1.5862794756980572,-9.002426147460938,0.6261536639399434,-131.3327178955078,-2.5240706198644887,15.526206970214844,0.8551241526083526,142.41668701171875,2.4146388475745084,3114,1381562,9.392355,J4,2.830116152091051,1.8576181251506447,55.97999999999992,73.96361541748047,0.39235666886632714,-8.363268852233887,-1.933423602590147,-8.884164810180664,0.7444150012202169,-131.63876342773438,-2.830116152091051,15.65822696685791,0.987144149251419,142.69912719726562,2.6970790331213834,3118,1381563,9.400331,J4,3.1013106100988637,2.0726473342415157,63.955999999999236,74.02384948730469,0.4525907386905459,-8.728874206542969,-2.299028956899229,-8.76374626159668,0.8648335498042012,-131.9099578857422,-3.1013106100988637,15.788768768310547,1.1176859507040557,142.9464569091797,2.944408745035446,6
10,10,1189,3154,1381577,9.512325,75.56938603377543,-14.67930590788221,-7.294156094303152,-130.92303342701462,17.688361072687904,141.89350789658602,3170,1381583,9.560358,J4,3.3929965007450846,2.1059862266772753,48.033000000000214,76.72688293457031,1.1574969007948823,-16.518726348876953,-1.8394204409947434,-7.15147066116333,0.14268543313982196,-127.53003692626953,3.3929965007450846,18.53788948059082,0.8495284079029162,138.7897491455078,-3.103758751078203,3172,1381584,9.568328,J4,4.172522257581022,2.562681778316925,56.00299999999869,76.96759033203125,1.3982042982558198,-16.79741096496582,-2.1181050570836106,-7.157340049743652,0.1368160445594997,-126.7505111694336,4.172522257581022,18.698274612426758,1.0099135397388537,138.08377075195312,-3.8097371446328907,3175,1381585,9.576354,J4,4.999006937756803,3.042227585289095,64.02899999999967,77.21700286865234,1.6476168348769136,-17.061315536499023,-2.3820096286168138,-7.1720428466796875,0.12211324762346454,-125.92402648925781,4.999006937756803,18.86079978942871,1.1724387167408068,137.33689880371094,-4.556609092875078,6
11,11,1257,3334,1381645,10.056378,86.0934977330502,-14.498333177585573,-13.681510793859989,-69.8682955440411,26.742681901805224,88.9994136192388,3350,1381651,10.104322,J6,2.9848240516596434,2.0134925384056306,47.94399999999932,83.71662139892578,-2.376876334124418,-13.186573028564453,1.3117601490211204,-13.738808631896973,-0.05729783803698396,-72.70317840576172,-2.834882861720615,26.66758918762207,-0.07509271418315322,91.98423767089844,2.9848240516596434,3352,1381652,10.112375,J6,3.668104996483862,2.4582215610180698,55.99699999999963,83.25711822509766,-2.836379507952543,-12.97336483001709,1.5249683475684837,-13.721805572509766,-0.04029477864977693,-73.39217376708984,-3.52387822304874,26.632198333740234,-0.11048356806498916,92.66751861572266,3.668104996483862,3355,1381653,10.120334,J6,4.397696366112768,2.932320329432031,63.955999999999236,82.77881622314453,-3.314681509905668,-12.759296417236328,1.7390367603492454,-13.69810676574707,-0.016595971887081618,-74.13638305664062,-4.2680875125995215,26.590784072875977,-0.15189782892924697,93.39710998535156,4.397696366112768,6
12,12,1303,3455,1381691,10.424364,61.720732564877665,-4.2327893098442155,-9.784423185760874,-108.38408270751574,22.16077221865394,118.14206389103131,3470,1381697,10.472333,J6,4.000958239175844,1.9902461373621054,47.96900000000015,59.78955841064453,-1.9311741542331333,-2.7094168663024902,1.5233724435417253,-8.933828353881836,0.8505948318790377,-108.24857330322266,0.13550940429308866,21.177492141723633,-0.9832800769303063,114.14110565185547,-4.000958239175844,3474,1381698,10.480323,J6,4.9290969720860005,2.3936680954994465,55.95899999999965,59.577754974365234,-2.14297759051243,-2.4748787879943848,1.7579105218498308,-8.810709953308105,0.9737132324527682,-107.96444702148438,0.4196356860313699,21.03158187866211,-1.1291903399918297,113.21296691894531,-4.9290969720860005,3476,1381699,10.488357,J6,5.929425036050844,2.82736270797146,63.99299999999997,59.389400482177734,-2.33133208269993,-2.241248369216919,1.9915409406272966,-8.690967559814453,1.0934556259464205,-107.61585998535156,0.7682227221641824,20.888513565063477,-1.2722586535904625,112.21263885498047,-5.929425036050844,6
13,13,1362,3611,1381750,10.896324,79.75708299218505,4.6402310571176475,-15.311185536748695,-56.03531247084017,26.13313788666632,52.152257172101606,3627,1381756,10.944371,J1,6.656208938967296,4.109282348845555,48.047000000000395,86.41329193115234,6.656208938967296,4.266173362731934,-0.3740576943857139,-19.15987777709961,-3.848692240350914,-52.24028015136719,3.7950323194729805,29.853723526000977,3.720585639334658,55.86907196044922,3.7168147883476124,3629,1381757,10.952347,J1,7.722973770510265,4.798701518313248,56.02299999999971,87.48005676269531,7.722973770510265,4.181386947631836,-0.45884410948581156,-19.803678512573242,-4.492492975824547,-51.80745315551758,4.22785931532259,30.479778289794922,4.346640403128603,56.773921966552734,4.621664794451128,3633,1381758,10.960362,J1,8.793827957521984,5.501947903898078,64.03800000000004,88.55091094970703,8.793827957521984,4.09064245223999,-0.5495886048776573,-20.455745697021484,-5.144560160272789,-51.41057586669922,4.624736604140949,31.11459732055664,4.981459433890322,57.749176025390625,5.596918853289019,6
14,14,1393,3694,1381781,11.144347,108.95555081237923,1.3707600427061273,-33.59442519685133,-48.36736100282485,43.963403989432074,85.03926111958742,3710,1381787,11.192339,J6,2.93518040873289,1.5679827735552678,47.9920000000007,110.7523193359375,1.7967685235582707,0.8751887679100037,-0.4955712747961236,-34.756534576416016,-1.1621093795646829,-48.14332580566406,0.2240351971607879,45.0865592956543,1.1231553062222233,87.97444152832031,2.93518040873289,3712,1381788,11.20037,J6,3.224395496623515,1.7217974200989363,56.02299999999971,110.92666625976562,1.9711154473863957,0.7935436964035034,-0.5772163463026239,-34.86042404174805,-1.265998844896714,-48.102813720703125,0.2645472821217254,45.1879768371582,1.2245728477261295,88.26365661621094,3.224395496623515,3714,1381789,11.208359,J6,3.4549634287524214,1.844232474600388,64.01199999999996,111.0645751953125,2.1090243829332707,0.7117024660110474,-0.6590575766950799,-34.93739700317383,-1.3429718063224954,-48.06473159790039,0.3026294049244598,45.2645149230957,1.3011109336636295,88.49422454833984,3.4549634287524214,6
15,15,1426,3780,1381814,11.408361,110.58484797608095,-3.860899593758653,-32.39640711653168,-51.112590875369015,44.2291595765054,87.62399978413751,3796,1381820,11.456361,J4,2.573146734494266,1.686689503314435,48.00000000000004,111.79177856445312,1.206930588372174,-6.225440502166748,-2.364540908408095,-31.958988189697266,0.4374189268344111,-53.68573760986328,-2.573146734494266,44.76150894165039,0.532349365144988,89.33454895019531,1.7105491660578025,3798,1381821,11.464349,J4,3.0742606260958283,2.037357874022524,55.98800000000104,112.08367919921875,1.498831223137799,-6.6951823234558105,-2.8342827296971573,-31.910240173339844,0.48616694319183296,-54.186851501464844,-3.0742606260958283,44.907752990722656,0.6785934142172536,89.73998260498047,2.1159828208429587,3801,1381822,11.472359,J4,3.579433355588016,2.3980591339233284,63.998000000001554,112.39252471923828,1.8076767431573302,-7.177155494689941,-3.316255900931288,-31.86791229248047,0.528494824051208,-54.69202423095703,-3.579433355588016,45.06621170043945,0.8370521239340505,90.16300964355469,2.5390098594171775,6
16,16,1454,3854,1381842,11.63236,118.09595224767921,-17.37638727211256,-31.06900084212659,-59.56053843778568,48.73257576059714,94.13477131163891,3870,1381848,11.680362,J6,3.8105220440607894,2.197782779718695,48.00200000000032,117.87474060058594,-0.2212116470932699,-19.607177734375,-2.2307904622624406,-30.329103469848633,0.7398973722779587,-56.600250244140625,2.960288193645056,49.08655548095703,0.353979720359888,90.32424926757812,-3.8105220440607894,3872,1381849,11.688367,J6,4.756826365349852,2.710409629634467,56.00699999999925,117.7159423828125,-0.3800098648667074,-19.938819885253906,-2.562432613141347,-30.167478561401367,0.9015222807252243,-55.84638595581055,3.714152481975134,49.0964241027832,0.3638483421860599,89.37794494628906,-4.756826365349852,3874,1381850,11.696354,J6,5.783033335564696,3.263710825457299,63.99399999999922,117.52371978759766,-0.5722324600815512,-20.255823135375977,-2.879435863263417,-29.996501922607422,1.0724989195191696,-55.02521514892578,4.5353232888598995,49.092247009277344,0.3596712486802005,88.35173797607422,-5.783033335564696,6
17,18,1556,4121,1381944,12.448375,60.28248244838438,-22.938080568038327,-10.333247998560786,77.49164224770549,35.58337770883919,-69.66898071584893,4138,1381950,12.496365,J4,6.848522435693766,4.03614029285736,47.99000000000042,59.93589401245117,-0.34658843593320654,-21.102140426635742,1.8359401414025847,-9.684239387512207,0.6490086110485791,70.64311981201172,-6.848522435693766,32.941307067871094,-2.642070640968093,-63.34855651855469,6.320424197294244,4140,1381951,12.504369,J4,8.296535741357829,4.890437505980655,55.9940000000001,59.902915954589844,-0.37956649379453467,-20.689186096191406,2.2488944718469206,-9.52907943725586,0.8041685613049268,69.19510650634766,-8.296535741357829,32.343406677246094,-3.239971031593093,-62.032222747802734,7.636757968046197,4142,1381952,12.512395,J4,9.82779337075236,5.794164211715548,64.0199999999993,59.8752326965332,-0.4072497518511753,-20.24807357788086,2.6900069901574675,-9.361802101135254,0.9714458974255322,67.66384887695312,-9.82779337075236,31.704130172729492,-3.8792475361096947,-60.64377975463867,9.02520096121026,6
1 trigger_no waypoint_index best_sample_order best_frame_number best_sequence best_time_relative_s teach_j1_deg teach_j2_deg teach_j3_deg teach_j4_deg teach_j5_deg teach_j6_deg offset_6_frame_number offset_6_sequence offset_6_time_relative_s offset_6_max_error_axis offset_6_max_error_deg offset_6_rms_error_deg offset_6_delta_from_best_ms offset_6_j1_actual_deg offset_6_diff_j1_deg offset_6_j2_actual_deg offset_6_diff_j2_deg offset_6_j3_actual_deg offset_6_diff_j3_deg offset_6_j4_actual_deg offset_6_diff_j4_deg offset_6_j5_actual_deg offset_6_diff_j5_deg offset_6_j6_actual_deg offset_6_diff_j6_deg offset_7_frame_number offset_7_sequence offset_7_time_relative_s offset_7_max_error_axis offset_7_max_error_deg offset_7_rms_error_deg offset_7_delta_from_best_ms offset_7_j1_actual_deg offset_7_diff_j1_deg offset_7_j2_actual_deg offset_7_diff_j2_deg offset_7_j3_actual_deg offset_7_diff_j3_deg offset_7_j4_actual_deg offset_7_diff_j4_deg offset_7_j5_actual_deg offset_7_diff_j5_deg offset_7_j6_actual_deg offset_7_diff_j6_deg offset_8_frame_number offset_8_sequence offset_8_time_relative_s offset_8_max_error_axis offset_8_max_error_deg offset_8_rms_error_deg offset_8_delta_from_best_ms offset_8_j1_actual_deg offset_8_diff_j1_deg offset_8_j2_actual_deg offset_8_diff_j2_deg offset_8_j3_actual_deg offset_8_diff_j3_deg offset_8_j4_actual_deg offset_8_diff_j4_deg offset_8_j5_actual_deg offset_8_diff_j5_deg offset_8_j6_actual_deg offset_8_diff_j6_deg best_of_6_7_8_offset
2 1 1 730 1941 1381118 5.840207 48.886810269468405 2.1989850886957285 -11.021017368511105 0.4102097980549552 6.248381265333557 2.294990756284542 1955 1381124 5.888271 J6 3.4065268256551553 2.468438977201345 48.06399999999922 45.8632698059082 -3.0235404635602023 2.9724788665771484 0.7734937778814199 -13.609341621398926 -2.588324252887821 -2.1851141452789307 -2.5953239433338857 7.582361221313477 1.3339799559799195 5.701517581939697 3.4065268256551553 1958 1381125 5.896184 J6 4.180931828249149 2.9355776062802286 55.9769999999995 45.449710845947266 -3.43709942352114 3.117816925048828 0.9188318363530996 -13.963269233703613 -2.9422518651925085 -2.845515012741089 -3.255724810796044 7.768581867218018 1.5202006018844605 6.475922584533691 4.180931828249149 1961 1381126 5.904272 J6 5.0045521476156045 3.418446646752538 64.06499999999937 45.06731414794922 -3.8194961215191867 3.2660434246063232 1.0670583359105947 -14.290565490722656 -3.2695481222115514 -3.562589168548584 -3.972798966603539 7.9421586990356445 1.6937774337020874 7.2995429039001465 5.0045521476156045 6
3 2 2 804 2135 1381192 6.432175 55.34775509527405 11.807039833001637 -7.0090952672806885 -71.01433145543973 6.012065051914967 74.24953191606797 2151 1381198 6.480252 J4 2.8167225076858244 1.6562461612538633 48.07700000000015 56.34336471557617 0.995609620302119 11.66723918914795 -0.13980064385368784 -6.600923538208008 0.40817172907268073 -68.1976089477539 2.8167225076858244 6.058437347412109 0.04637229549714217 71.5392837524414 -2.7102481636265594 2153 1381199 6.488246 J4 3.526759739131137 2.060948429836345 56.071000000000204 56.451515197753906 1.1037601024798533 11.618118286132812 -0.18892154686882456 -6.580313682556152 0.4287815847245362 -67.4875717163086 3.526759739131137 6.09804105758667 0.08597600567170272 70.84339141845703 -3.4061404976109344 2155 1381200 6.496271 J4 4.301478977412387 2.5033932349312007 64.09600000000015 56.54535675048828 1.1976016552142283 11.561552047729492 -0.24548778527214488 -6.571277618408203 0.4378176488724854 -66.71285247802734 4.301478977412387 6.147788047790527 0.13572299587556014 70.078857421875 -4.170674494192966 6
4 3 3 832 2207 1381220 6.656264 55.109808014787404 8.759497374223619 -8.518216825284897 -41.11725046606459 10.108488114686867 41.956933292858096 2223 1381226 6.704225 J6 11.130744251720401 5.955607695749779 47.96099999999992 53.50831985473633 -1.6014881600510762 7.412222862243652 -1.3472745119799665 -9.629739761352539 -1.111522936067642 -32.30571365356445 8.811536812500137 12.488306045532227 2.379817930845359 30.826189041137695 -11.130744251720401 2227 1381227 6.712268 J6 13.165517124645206 7.03533795629365 56.00399999999972 53.18336486816406 -1.9264431466233418 7.154932975769043 -1.6045643984545759 -9.842713356018066 -1.3244965307331693 -30.750762939453125 10.366487526611465 12.961709976196289 2.8532218615094216 28.79141616821289 -13.165517124645206 2229 1381228 6.720286 J6 15.212756428234073 8.119717107623766 64.02199999999958 52.84844207763672 -2.2613659371506856 6.892857074737549 -1.86664029948607 -10.059968948364258 -1.5417521230793607 -29.200178146362305 11.917072319702285 13.449018478393555 3.340530363706687 26.744176864624023 -15.212756428234073 6
5 4 4 875 2321 1381263 7.000291 43.65359310453334 -1.629659559507137 -17.715753611915318 5.995208633582219 32.17171770646655 -22.573973337684023 2336 1381269 7.048266 J6 2.3116842904428125 1.3731583485126422 47.9750000000001 44.433006286621094 0.779413182087751 -2.1156294345855713 -0.48596987507843425 -18.39777374267578 -0.6820201307604634 4.8371992111206055 -1.158009422461613 33.99372482299805 1.8220071165314948 -20.26228904724121 2.3116842904428125 2339 1381270 7.056293 J6 3.083185831580508 1.7437179916275436 56.00200000000033 44.65972137451172 1.006128269978376 -2.1617355346679688 -0.5320759751608317 -18.490602493286133 -0.774848881370815 4.406780242919922 -1.5884283906622967 34.24979019165039 2.0780724851838386 -19.490787506103516 3.083185831580508 2341 1381271 7.064278 J6 3.9520155587533594 2.15807761350517 63.987000000000016 44.91068649291992 1.2570933883865791 -2.196718215942383 -0.5670586564352458 -18.575654983520508 -0.85990137160519 3.9164717197418213 -2.0787369138403973 34.48752212524414 2.3158044187775886 -18.621957778930664 3.9520155587533594 6
6 5 5 927 2457 1381315 7.416267 64.58244475717193 4.262979315506351 -15.669217122643433 -29.952927185666542 29.850439933020308 45.62652743544272 2477 1381321 7.464289 J5 2.171828177649214 1.404704060246151 48.021999999999565 65.84967041015625 1.2672256529843224 5.695003032684326 1.4320237171779748 -14.563636779785156 1.1055803428582767 -28.69342613220215 1.259501053464394 27.678611755371094 -2.171828177649214 46.437156677246094 0.810629241803376 2479 1381322 7.472265 J5 2.585320380408003 1.6670103454750829 55.99799999999977 66.01153564453125 1.4290908873593224 5.957343578338623 1.6943642628322717 -14.353693008422852 1.3155241142205814 -28.232824325561523 1.720102860105019 27.265119552612305 -2.585320380408003 46.24872589111328 0.6221984556705635 2483 1381323 7.480284 J5 3.0049218208376907 1.9507458917658702 64.01699999999977 66.15470886230469 1.57226410513276 6.221103191375732 1.958123875869381 -14.140877723693848 1.5283393989495853 -27.70670509338379 2.2462220922827534 26.845518112182617 -3.0049218208376907 45.96609115600586 0.33956372056314166 6
7 6 6 997 2649 1381385 7.976283 60.47948252708421 23.068781981390185 -5.01126420129949 36.10685486878251 5.525681179628412 -31.3025636495649 2665 1381391 8.024277 J2 1.408993592340284 0.8424031169595625 47.99400000000009 60.847530364990234 0.36804783790602613 24.47777557373047 1.408993592340284 -5.617043972015381 -0.6057797707158912 34.8044319152832 -1.3024229534993097 5.786255359649658 0.260574180021246 -31.2253475189209 0.07721613064400046 2667 1381392 8.032258 J4 1.7701696881184503 1.0669192783996628 55.975000000000996 60.95240783691406 0.47292530982985426 24.69621467590332 1.6274326945131357 -5.75187873840332 -0.7406145371038306 34.33668518066406 -1.7701696881184503 5.885775089263916 0.3600939096355038 -30.920429229736328 0.38213441982857077 2669 1381393 8.040307 J4 2.303903056038372 1.3309750271252634 64.02400000000074 61.06847381591797 0.5889912888337605 24.908491134643555 1.83970915325337 -5.895160675048828 -0.8838964737493384 33.80295181274414 -2.303903056038372 5.999467372894287 0.4737861932658749 -30.538488388061523 0.7640752615033755 6
8 7 7 1053 2795 1381441 8.424315 70.47583707168602 16.39384887825908 -13.456948007467595 -27.892318852946243 14.53566195089614 31.71193282686115 2811 1381447 8.472317 J6 7.1941073647892395 4.618545262978315 48.00200000000032 70.8250503540039 0.3492132823178906 10.95259952545166 -5.4412493528074215 -13.136046409606934 0.3209015978606615 -34.68459701538086 -6.7922781624346165 14.022420883178711 -0.5132410677174288 38.90604019165039 7.1941073647892395 2814 1381448 8.480253 J6 8.24868042387127 5.331515658278243 55.93799999999938 70.84622192382812 0.37038485214210937 9.979106903076172 -6.41474197518291 -13.042067527770996 0.414880479696599 -35.67851257324219 -7.786193720295945 13.882524490356445 -0.6531374605396945 39.96061325073242 8.24868042387127 2817 1381449 8.488291 J6 9.275558780804865 6.035874346469871 63.976000000000255 70.85871887207031 0.38288180038429687 8.986706733703613 -7.407142144555468 -12.9381742477417 0.5187737597258959 -36.64576721191406 -8.75344835896782 13.72922134399414 -0.8064406069019991 40.987491607666016 9.275558780804865 6
9 8 8 1089 2892 1381477 8.71231 69.58246408878419 -17.10571341532583 -8.710589696831251 -58.47569441890704 7.63059003611043 64.43773280685575 2908 1381483 8.760361 J6 5.93988407546847 3.533132924843701 48.05099999999918 69.40516662597656 -0.1772974628076298 -20.066781997680664 -2.9610685823548337 -8.152935981750488 0.5576537150807628 -63.93235397338867 -5.456659554481632 6.775912284851074 -0.8546777512593557 70.37761688232422 5.93988407546847 2910 1381484 8.768294 J6 6.922168621366907 4.090127751125079 55.9839999999987 69.39288330078125 -0.1895807880029423 -20.37982177734375 -3.2741083620179197 -8.094615936279297 0.6159737605519542 -64.83366394042969 -6.357969521522648 6.682157039642334 -0.948432996468096 71.35990142822266 6.922168621366907 2912 1381485 8.77637 J6 7.916950115507532 4.649086823187336 64.05999999999956 69.38455200195312 -0.1979120868310673 -20.654869079589844 -3.5491556642640134 -8.043557167053223 0.6670325297780284 -65.74622344970703 -7.270529030799992 6.598722457885742 -1.0318675782246878 72.35468292236328 7.916950115507532 6
10 9 9 1167 3096 1381555 9.336375 73.57125874861414 -6.42984524964374 -9.628579811400881 -128.80864727564332 14.671082817606491 140.00204816414424 3112 1381561 9.384332 J4 2.5240706198644887 1.630593251762862 47.95700000000025 73.90592956542969 0.3346708168155459 -8.016124725341797 -1.5862794756980572 -9.002426147460938 0.6261536639399434 -131.3327178955078 -2.5240706198644887 15.526206970214844 0.8551241526083526 142.41668701171875 2.4146388475745084 3114 1381562 9.392355 J4 2.830116152091051 1.8576181251506447 55.97999999999992 73.96361541748047 0.39235666886632714 -8.363268852233887 -1.933423602590147 -8.884164810180664 0.7444150012202169 -131.63876342773438 -2.830116152091051 15.65822696685791 0.987144149251419 142.69912719726562 2.6970790331213834 3118 1381563 9.400331 J4 3.1013106100988637 2.0726473342415157 63.955999999999236 74.02384948730469 0.4525907386905459 -8.728874206542969 -2.299028956899229 -8.76374626159668 0.8648335498042012 -131.9099578857422 -3.1013106100988637 15.788768768310547 1.1176859507040557 142.9464569091797 2.944408745035446 6
11 10 10 1189 3154 1381577 9.512325 75.56938603377543 -14.67930590788221 -7.294156094303152 -130.92303342701462 17.688361072687904 141.89350789658602 3170 1381583 9.560358 J4 3.3929965007450846 2.1059862266772753 48.033000000000214 76.72688293457031 1.1574969007948823 -16.518726348876953 -1.8394204409947434 -7.15147066116333 0.14268543313982196 -127.53003692626953 3.3929965007450846 18.53788948059082 0.8495284079029162 138.7897491455078 -3.103758751078203 3172 1381584 9.568328 J4 4.172522257581022 2.562681778316925 56.00299999999869 76.96759033203125 1.3982042982558198 -16.79741096496582 -2.1181050570836106 -7.157340049743652 0.1368160445594997 -126.7505111694336 4.172522257581022 18.698274612426758 1.0099135397388537 138.08377075195312 -3.8097371446328907 3175 1381585 9.576354 J4 4.999006937756803 3.042227585289095 64.02899999999967 77.21700286865234 1.6476168348769136 -17.061315536499023 -2.3820096286168138 -7.1720428466796875 0.12211324762346454 -125.92402648925781 4.999006937756803 18.86079978942871 1.1724387167408068 137.33689880371094 -4.556609092875078 6
12 11 11 1257 3334 1381645 10.056378 86.0934977330502 -14.498333177585573 -13.681510793859989 -69.8682955440411 26.742681901805224 88.9994136192388 3350 1381651 10.104322 J6 2.9848240516596434 2.0134925384056306 47.94399999999932 83.71662139892578 -2.376876334124418 -13.186573028564453 1.3117601490211204 -13.738808631896973 -0.05729783803698396 -72.70317840576172 -2.834882861720615 26.66758918762207 -0.07509271418315322 91.98423767089844 2.9848240516596434 3352 1381652 10.112375 J6 3.668104996483862 2.4582215610180698 55.99699999999963 83.25711822509766 -2.836379507952543 -12.97336483001709 1.5249683475684837 -13.721805572509766 -0.04029477864977693 -73.39217376708984 -3.52387822304874 26.632198333740234 -0.11048356806498916 92.66751861572266 3.668104996483862 3355 1381653 10.120334 J6 4.397696366112768 2.932320329432031 63.955999999999236 82.77881622314453 -3.314681509905668 -12.759296417236328 1.7390367603492454 -13.69810676574707 -0.016595971887081618 -74.13638305664062 -4.2680875125995215 26.590784072875977 -0.15189782892924697 93.39710998535156 4.397696366112768 6
13 12 12 1303 3455 1381691 10.424364 61.720732564877665 -4.2327893098442155 -9.784423185760874 -108.38408270751574 22.16077221865394 118.14206389103131 3470 1381697 10.472333 J6 4.000958239175844 1.9902461373621054 47.96900000000015 59.78955841064453 -1.9311741542331333 -2.7094168663024902 1.5233724435417253 -8.933828353881836 0.8505948318790377 -108.24857330322266 0.13550940429308866 21.177492141723633 -0.9832800769303063 114.14110565185547 -4.000958239175844 3474 1381698 10.480323 J6 4.9290969720860005 2.3936680954994465 55.95899999999965 59.577754974365234 -2.14297759051243 -2.4748787879943848 1.7579105218498308 -8.810709953308105 0.9737132324527682 -107.96444702148438 0.4196356860313699 21.03158187866211 -1.1291903399918297 113.21296691894531 -4.9290969720860005 3476 1381699 10.488357 J6 5.929425036050844 2.82736270797146 63.99299999999997 59.389400482177734 -2.33133208269993 -2.241248369216919 1.9915409406272966 -8.690967559814453 1.0934556259464205 -107.61585998535156 0.7682227221641824 20.888513565063477 -1.2722586535904625 112.21263885498047 -5.929425036050844 6
14 13 13 1362 3611 1381750 10.896324 79.75708299218505 4.6402310571176475 -15.311185536748695 -56.03531247084017 26.13313788666632 52.152257172101606 3627 1381756 10.944371 J1 6.656208938967296 4.109282348845555 48.047000000000395 86.41329193115234 6.656208938967296 4.266173362731934 -0.3740576943857139 -19.15987777709961 -3.848692240350914 -52.24028015136719 3.7950323194729805 29.853723526000977 3.720585639334658 55.86907196044922 3.7168147883476124 3629 1381757 10.952347 J1 7.722973770510265 4.798701518313248 56.02299999999971 87.48005676269531 7.722973770510265 4.181386947631836 -0.45884410948581156 -19.803678512573242 -4.492492975824547 -51.80745315551758 4.22785931532259 30.479778289794922 4.346640403128603 56.773921966552734 4.621664794451128 3633 1381758 10.960362 J1 8.793827957521984 5.501947903898078 64.03800000000004 88.55091094970703 8.793827957521984 4.09064245223999 -0.5495886048776573 -20.455745697021484 -5.144560160272789 -51.41057586669922 4.624736604140949 31.11459732055664 4.981459433890322 57.749176025390625 5.596918853289019 6
15 14 14 1393 3694 1381781 11.144347 108.95555081237923 1.3707600427061273 -33.59442519685133 -48.36736100282485 43.963403989432074 85.03926111958742 3710 1381787 11.192339 J6 2.93518040873289 1.5679827735552678 47.9920000000007 110.7523193359375 1.7967685235582707 0.8751887679100037 -0.4955712747961236 -34.756534576416016 -1.1621093795646829 -48.14332580566406 0.2240351971607879 45.0865592956543 1.1231553062222233 87.97444152832031 2.93518040873289 3712 1381788 11.20037 J6 3.224395496623515 1.7217974200989363 56.02299999999971 110.92666625976562 1.9711154473863957 0.7935436964035034 -0.5772163463026239 -34.86042404174805 -1.265998844896714 -48.102813720703125 0.2645472821217254 45.1879768371582 1.2245728477261295 88.26365661621094 3.224395496623515 3714 1381789 11.208359 J6 3.4549634287524214 1.844232474600388 64.01199999999996 111.0645751953125 2.1090243829332707 0.7117024660110474 -0.6590575766950799 -34.93739700317383 -1.3429718063224954 -48.06473159790039 0.3026294049244598 45.2645149230957 1.3011109336636295 88.49422454833984 3.4549634287524214 6
16 15 15 1426 3780 1381814 11.408361 110.58484797608095 -3.860899593758653 -32.39640711653168 -51.112590875369015 44.2291595765054 87.62399978413751 3796 1381820 11.456361 J4 2.573146734494266 1.686689503314435 48.00000000000004 111.79177856445312 1.206930588372174 -6.225440502166748 -2.364540908408095 -31.958988189697266 0.4374189268344111 -53.68573760986328 -2.573146734494266 44.76150894165039 0.532349365144988 89.33454895019531 1.7105491660578025 3798 1381821 11.464349 J4 3.0742606260958283 2.037357874022524 55.98800000000104 112.08367919921875 1.498831223137799 -6.6951823234558105 -2.8342827296971573 -31.910240173339844 0.48616694319183296 -54.186851501464844 -3.0742606260958283 44.907752990722656 0.6785934142172536 89.73998260498047 2.1159828208429587 3801 1381822 11.472359 J4 3.579433355588016 2.3980591339233284 63.998000000001554 112.39252471923828 1.8076767431573302 -7.177155494689941 -3.316255900931288 -31.86791229248047 0.528494824051208 -54.69202423095703 -3.579433355588016 45.06621170043945 0.8370521239340505 90.16300964355469 2.5390098594171775 6
17 16 16 1454 3854 1381842 11.63236 118.09595224767921 -17.37638727211256 -31.06900084212659 -59.56053843778568 48.73257576059714 94.13477131163891 3870 1381848 11.680362 J6 3.8105220440607894 2.197782779718695 48.00200000000032 117.87474060058594 -0.2212116470932699 -19.607177734375 -2.2307904622624406 -30.329103469848633 0.7398973722779587 -56.600250244140625 2.960288193645056 49.08655548095703 0.353979720359888 90.32424926757812 -3.8105220440607894 3872 1381849 11.688367 J6 4.756826365349852 2.710409629634467 56.00699999999925 117.7159423828125 -0.3800098648667074 -19.938819885253906 -2.562432613141347 -30.167478561401367 0.9015222807252243 -55.84638595581055 3.714152481975134 49.0964241027832 0.3638483421860599 89.37794494628906 -4.756826365349852 3874 1381850 11.696354 J6 5.783033335564696 3.263710825457299 63.99399999999922 117.52371978759766 -0.5722324600815512 -20.255823135375977 -2.879435863263417 -29.996501922607422 1.0724989195191696 -55.02521514892578 4.5353232888598995 49.092247009277344 0.3596712486802005 88.35173797607422 -5.783033335564696 6
18 17 18 1556 4121 1381944 12.448375 60.28248244838438 -22.938080568038327 -10.333247998560786 77.49164224770549 35.58337770883919 -69.66898071584893 4138 1381950 12.496365 J4 6.848522435693766 4.03614029285736 47.99000000000042 59.93589401245117 -0.34658843593320654 -21.102140426635742 1.8359401414025847 -9.684239387512207 0.6490086110485791 70.64311981201172 -6.848522435693766 32.941307067871094 -2.642070640968093 -63.34855651855469 6.320424197294244 4140 1381951 12.504369 J4 8.296535741357829 4.890437505980655 55.9940000000001 59.902915954589844 -0.37956649379453467 -20.689186096191406 2.2488944718469206 -9.52907943725586 0.8041685613049268 69.19510650634766 -8.296535741357829 32.343406677246094 -3.239971031593093 -62.032222747802734 7.636757968046197 4142 1381952 12.512395 J4 9.82779337075236 5.794164211715548 64.0199999999993 59.8752326965332 -0.4072497518511753 -20.24807357788086 2.6900069901574675 -9.361802101135254 0.9714458974255322 67.66384887695312 -9.82779337075236 31.704130172729492 -3.8792475361096947 -60.64377975463867 9.02520096121026 6

View File

@@ -1,18 +0,0 @@
{
"rows": 17,
"best_offset_win_counts": {
"6": 17,
"7": 0,
"8": 0
},
"average_max_error_deg": {
"6": 4.241583591714439,
"7": 5.068092118561757,
"8": 5.9316839578472145
},
"average_rms_error_deg": {
"6": 2.5406136706026206,
"7": 3.021296972079456,
"8": 3.5201758351699857
}
}

View File

@@ -1,18 +0,0 @@
trigger_no,waypoint_index,trigger_frame_number,trigger_time_relative_s,trigger_sequence,paired_status_frame_number,paired_status_time_relative_s,paired_status_sequence,paired_status_timestamp,paired_status_to_trigger_sequence_delta,paired_status_to_trigger_time_ms,teach_j1_deg,teach_j2_deg,teach_j3_deg,teach_j4_deg,teach_j5_deg,teach_j6_deg,paired_status_j1_actual_deg,paired_status_diff_j1_deg,paired_status_j2_actual_deg,paired_status_diff_j2_deg,paired_status_j3_actual_deg,paired_status_diff_j3_deg,paired_status_j4_actual_deg,paired_status_diff_j4_deg,paired_status_j5_actual_deg,paired_status_diff_j5_deg,paired_status_j6_actual_deg,paired_status_diff_j6_deg,paired_status_max_error_axis,paired_status_max_error_deg,paired_status_rms_error_deg,best_status_frame_number,best_status_time_relative_s,best_status_sequence,best_status_timestamp,best_status_delta_from_paired_cycles,best_status_delta_from_trigger_sequence,best_status_time_after_trigger_ms,best_status_j1_actual_deg,best_status_diff_j1_deg,best_status_j2_actual_deg,best_status_diff_j2_deg,best_status_j3_actual_deg,best_status_diff_j3_deg,best_status_j4_actual_deg,best_status_diff_j4_deg,best_status_j5_actual_deg,best_status_diff_j5_deg,best_status_j6_actual_deg,best_status_diff_j6_deg,best_status_max_error_axis,best_status_max_error_deg,best_status_rms_error_deg
1,1,1955,5.888271,1381124,1954,5.888166,1381116,624368380,-8,-0.10499999999957765,48.886810269468405,2.1989850886957285,-11.021017368511105,0.4102097980549552,6.248381265333557,2.294990756284542,55.47505187988281,6.588241610414407,1.2506256103515625,-0.948359478344166,-5.400295734405518,5.620721634105587,0.5259769558906555,0.1157671578357003,3.3612561225891113,-2.8871251427444458,0.44421958923339844,-1.8507711670511435,J1,6.588241610414407,3.8225443630072706,1981,5.968124,1381126,624368460,10,2,79.85300000000083,48.76339340209961,-0.12341686736879609,2.256481647491455,0.05749655879572657,-11.157909393310547,-0.13689202479944207,0.37860527634620667,-0.031604521708748556,6.3406662940979,0.0922850287643433,2.5025806427001953,0.20758988641565335,J6,0.20758988641565335,0.12239685259209541
2,2,2151,6.480252,1381198,2150,6.480138,1381190,624368972,-8,-0.11399999999994748,55.34775509527405,11.807039833001637,-7.0090952672806885,-71.01433145543973,6.012065051914967,74.24953191606797,53.0124397277832,-2.33531536749085,11.506516456604004,-0.30052337639763316,-8.466276168823242,-1.4571809015425536,-69.63382720947266,1.3805042459670744,6.426647663116455,0.41458261120148787,72.93537902832031,-1.3141528877476532,J1,2.33531536749085,1.3827543088729335,2172,6.552164,1381199,624369044,9,1,71.9120000000002,55.265621185302734,-0.08213390997131853,11.803675651550293,-0.003364181451344095,-7.050373554229736,-0.04127828694904778,-71.06609344482422,-0.05176198938448806,6.026607990264893,0.014542938349925372,74.28738403320312,0.03785211713515935,J1,0.08213390997131853,0.04616054814443023
3,3,2223,6.704225,1381226,2222,6.704166,1381218,624369196,-8,-0.059000000000253294,55.109808014787404,8.759497374223619,-8.518216825284897,-41.11725046606459,10.108488114686867,41.956933292858096,56.5781364440918,1.4683284293043926,10.494372367858887,1.7348749936352679,-7.114619255065918,1.4035975702189791,-55.112239837646484,-13.994989371581894,7.423236846923828,-2.6852512677630394,58.080867767333984,16.123934474475888,J6,16.123934474475888,8.852376616648156,2248,6.784154,1381228,624369276,10,2,79.92899999999992,55.00252151489258,-0.10728649989482619,8.69005012512207,-0.06944724910154854,-8.577027320861816,-0.05881049557691931,-40.59286117553711,0.5243892905274805,10.210430145263672,0.1019420305768044,41.38724136352539,-0.5696919293327056,J6,0.5696919293327056,0.3239642953120798
4,4,2336,7.048266,1381269,2335,7.048148,1381261,624369540,-8,-0.11799999999961841,43.65359310453334,-1.629659559507137,-17.715753611915318,5.995208633582219,32.17171770646655,-22.573973337684023,44.479610443115234,0.8260173385818916,0.07192186266183853,1.7015814221689756,-15.996956825256348,1.7187967866589702,2.40028715133667,-3.5949214822455486,27.737812042236328,-4.433905664230224,-17.672090530395508,4.901882807288516,J6,4.901882807288516,3.2440556235708993,2362,7.128175,1381271,624369620,10,2,79.90899999999979,43.72715759277344,0.07356448824009476,-1.6386029720306396,-0.008943412523502614,-17.75617218017578,-0.04041856826046342,5.952699661254883,-0.04250897232733575,32.2616081237793,0.08989041731274483,-22.47926902770996,0.09470430997406254,J6,0.09470430997406254,0.06580474324148583
5,5,2477,7.464289,1381321,2476,7.464178,1381313,624369956,-8,-0.11099999999952814,64.58244475717193,4.262979315506351,-15.669217122643433,-29.952927185666542,29.850439933020308,45.62652743544272,60.830230712890625,-3.7522140442813026,1.8808640241622925,-2.382115291344059,-17.297021865844727,-1.6278047432012936,-26.684900283813477,3.268026901853066,33.02191925048828,3.1714793174679734,36.40708923339844,-9.21943820204428,J6,9.21943820204428,4.621321121191458,2502,7.544122,1381323,624370036,10,2,79.83299999999983,64.5761947631836,-0.006249993988333813,4.283629417419434,0.020650101913082253,-15.615406036376953,0.05381108626647979,-29.94143295288086,0.011494232785683067,29.7938175201416,-0.056622412878706285,45.61851119995117,-0.008016235491545842,J5,0.056622412878706285,0.03357478517689402
6,6,2665,8.024277,1381391,2664,8.024156,1381383,624370516,-8,-0.12100000000003774,60.47948252708421,23.068781981390185,-5.01126420129949,36.10685486878251,5.525681179628412,-31.3025636495649,60.89759063720703,0.418108110122823,20.716407775878906,-2.3523742055112784,-4.913214206695557,0.09804999460393304,32.72084045410156,-3.3860144146809503,6.405356407165527,0.8796752275371151,-25.156103134155273,6.1464605154096255,J6,6.1464605154096255,3.0478405356855083,2687,8.096181,1381392,624370588,9,1,71.90399999999997,60.54315185546875,0.06366932838454176,22.99761962890625,-0.07116235248393465,-5.037043571472168,-0.025779370172678284,36.056732177734375,-0.050122691048137824,5.545217037200928,0.019535857572515525,-31.12558937072754,0.17697427883735983,J6,0.17697427883735983,0.08563125303054821
7,7,2811,8.472317,1381447,2810,8.472208,1381439,624370964,-8,-0.10900000000013677,70.47583707168602,16.39384887825908,-13.456948007467595,-27.892318852946243,14.53566195089614,31.71193282686115,69.13842010498047,-1.3374169667055469,22.202428817749023,5.808579939489942,-12.907807350158691,0.5491406573089037,-15.469873428344727,12.422445424601516,14.040997505187988,-0.4946644457081515,18.56021499633789,-13.15171783052326,J6,13.15171783052326,7.78202379046887,2834,8.54419,1381448,624371036,9,1,71.87300000000008,70.3967514038086,-0.07908566787742188,16.456680297851562,0.06283141959248084,-13.330730438232422,0.12621756923517324,-27.611600875854492,0.2807179770917507,14.542427062988281,0.006765112092141479,31.405345916748047,-0.30658691011310424,J6,0.30658691011310424,0.18210669390591905
8,8,2908,8.760361,1381483,2907,8.760245,1381475,624371252,-8,-0.11600000000022703,69.58246408878419,-17.10571341532583,-8.710589696831251,-58.47569441890704,7.63059003611043,64.43773280685575,70.10379791259766,0.5213338238134639,-10.075156211853027,7.030557203472803,-9.98978042602539,-1.2791907291941396,-51.5866584777832,6.889035941123836,9.534638404846191,1.9040483687357614,57.0014533996582,-7.436279407197546,J6,7.436279407197546,5.127041386597523,2929,8.832199,1381484,624371324,9,1,71.83799999999962,69.59716033935547,0.014696250571276437,-17.044965744018555,0.06074767130727565,-8.703485488891602,0.007104207939649498,-58.473426818847656,0.0022676000593833123,7.6463942527771,0.015804216666669646,64.45584869384766,0.01811588699190736,J2,0.06074767130727565,0.02750705896232285
9,9,3112,9.384332,1381561,3111,9.384204,1381553,624371876,-8,-0.128000000000128,73.57125874861414,-6.42984524964374,-9.628579811400881,-128.80864727564332,14.671082817606491,140.00204816414424,72.96144104003906,-0.6098177085750791,-6.3001484870910645,0.12969676255267526,-10.035088539123535,-0.40650872772265423,-123.29313659667969,5.515510678963636,13.165532112121582,-1.505550705484909,134.41644287109375,-5.585605293050492,J6,5.585605293050492,3.277206017832995,3134,9.456205,1381562,624371948,9,1,71.87300000000008,73.55095672607422,-0.02030202253992286,-6.441007614135742,-0.011162364492002474,-9.614203453063965,0.014376358336916084,-128.65638732910156,0.15225994654176134,14.635379791259766,-0.03570302634672551,139.8520965576172,-0.14995160652705408,J4,0.15225994654176134,0.089150370994325
10,10,3170,9.560358,1381583,3169,9.56025,1381575,624372052,-8,-0.10800000000088517,75.56938603377543,-14.67930590788221,-7.294156094303152,-130.92303342701462,17.688361072687904,141.89350789658602,74.36400604248047,-1.2053799912949614,-10.514228820800781,4.165077087081428,-8.236283302307129,-0.9421272080039769,-132.6507110595703,-1.7276776325556966,16.38176155090332,-1.3065995217845838,143.5712890625,1.6777811659139843,J2,4.165077087081428,2.128983861449047,3196,9.640214,1381585,624372132,10,2,79.85599999999948,75.62290954589844,0.05352351212300732,-14.662798881530762,0.016507026351447962,-7.297325611114502,-0.003169516811349915,-130.88812255859375,0.03491086842086588,17.726865768432617,0.03850469574471305,141.820068359375,-0.0734395372110157,J6,0.0734395372110157,0.043285948566531826
11,11,3350,10.104322,1381651,3349,10.104215,1381643,624372596,-8,-0.10699999999985721,86.0934977330502,-14.498333177585573,-13.681510793859989,-69.8682955440411,26.742681901805224,88.9994136192388,87.81878662109375,1.7252888880435506,-16.329286575317383,-1.8309533977318093,-13.073507308959961,0.6080034849000278,-70.97254180908203,-1.1042462650409277,26.366661071777344,-0.3760208300278798,89.37981414794922,0.3804005287104246,J2,1.8309533977318093,1.169340011162825,3373,10.17622,1381652,624372668,9,1,71.8980000000009,86.11857604980469,0.025078316754488128,-14.53605842590332,-0.03772524831774682,-13.651655197143555,0.029855596716434007,-69.8272933959961,0.04100214804500979,26.72992706298828,-0.012754838816942282,88.92174530029297,-0.07766831894582538,J6,0.07766831894582538,0.04246508921571141
12,12,3470,10.472333,1381697,3469,10.472191,1381689,624372964,-8,-0.14200000000030855,61.720732564877665,-4.2327893098442155,-9.784423185760874,-108.38408270751574,22.16077221865394,118.14206389103131,65.91849517822266,4.197762613344992,-6.331206798553467,-2.0984174887092513,-11.008773803710938,-1.2243506179500638,-103.56172943115234,4.822353276363401,23.532543182373047,1.3717709637191078,117.46694946289062,-0.675114428140688,J4,4.822353276363401,2.8611252897135864,3493,10.544283,1381698,624373036,9,1,71.94999999999929,61.7418098449707,0.021077280093038553,-4.212831497192383,0.019957812651832718,-9.789913177490234,-0.00548999172936071,-108.36312103271484,0.020961674800901164,22.163673400878906,0.002901182224967158,118.13783264160156,-0.0042312494297505054,J1,0.021077280093038553,0.014935517911187784
13,13,3627,10.944371,1381756,3626,10.944251,1381748,624373436,-8,-0.12000000000078614,79.75708299218505,4.6402310571176475,-15.311185536748695,-56.03531247084017,26.13313788666632,52.152257172101606,72.04009246826172,-7.716990523923329,4.5515007972717285,-0.08873025984591898,-11.385177612304688,3.926007924444008,-64.18782043457031,-8.152507963730145,22.412456512451172,-3.7206813742151468,54.39056396484375,2.2383067927421436,J4,8.152507963730145,5.168659196117862,3651,11.016226,1381757,624373508,9,1,71.85499999999934,79.90262603759766,0.14554304541260876,4.62429666519165,-0.015934391925997105,-15.370849609375,-0.05966407262630469,-55.99199676513672,0.043315705703449225,26.182939529418945,0.04980164275262666,52.15596008300781,0.003702910906206114,J1,0.14554304541260876,0.0699602863618738
14,14,3710,11.192339,1381787,3709,11.19225,1381779,624373684,-8,-0.08900000000089392,108.95555081237923,1.3707600427061273,-33.59442519685133,-48.36736100282485,43.963403989432074,85.03926111958742,102.20675659179688,-6.748794220582354,2.5045764446258545,1.1338164019197272,-29.148040771484375,4.446384425366958,-48.76271438598633,-0.3953533831614777,39.65629959106445,-4.3071043983676205,74.63484191894531,-10.40441920064211,J6,10.40441920064211,5.679810575214033,3735,11.272261,1381789,624373764,10,2,79.92199999999983,108.94917297363281,-0.006377838746416842,1.3447811603546143,-0.025978882351513022,-33.561458587646484,0.032966609204848396,-48.376487731933594,-0.009126729108743348,44.02882766723633,0.06542367780425451,85.1587142944336,0.11945317484617135,J6,0.11945317484617135,0.05835950688086092
15,15,3796,11.456361,1381820,3795,11.456253,1381812,624373948,-8,-0.10799999999910881,110.58484797608095,-3.860899593758653,-32.39640711653168,-51.112590875369015,44.2291595765054,87.62399978413751,110.58193969726562,-0.002908278815326071,-1.244791865348816,2.6161077284098373,-33.74542999267578,-1.3490228761441045,-48.585723876953125,2.5268669984158905,44.58126449584961,0.35210491934420673,87.75857543945312,0.134575655315615,J2,2.6161077284098373,1.5911754635072362,3821,11.536287,1381822,624374028,10,2,79.92600000000039,110.67292022705078,0.08807225096983018,-4.010702133178711,-0.14980253942005772,-32.37457275390625,0.021834362625426706,-51.22603225708008,-0.11344138171106266,44.249629974365234,0.020470397859831735,87.66435241699219,0.040352632854677495,J2,0.14980253942005772,0.08716897465096986
16,16,3870,11.680362,1381848,3869,11.680263,1381840,624374172,-8,-0.09900000000051534,118.09595224767921,-17.37638727211256,-31.06900084212659,-59.56053843778568,48.73257576059714,94.13477131163891,115.98970031738281,-2.106251930296395,-12.693913459777832,4.682473812334727,-31.563575744628906,-0.49457490250231473,-59.300933837890625,0.2596045998950558,47.15116500854492,-1.5814107520522214,94.34331512451172,0.20854381287280432,J2,4.682473812334727,2.2067399623329567,3895,11.760326,1381850,624374252,10,2,79.96399999999859,118.06391906738281,-0.0320331802963949,-17.536325454711914,-0.15993818259935466,-31.00341796875,0.06558287337659152,-59.507286071777344,0.05325236600833705,48.77106475830078,0.038488997703638006,93.9618148803711,-0.17295643126782068,J6,0.17295643126782068,0.10419421346699378
17,18,4138,12.496365,1381950,4137,12.496271,1381942,624374988,-8,-0.09400000000070463,60.28248244838438,-22.938080568038327,-10.333247998560786,77.49164224770549,35.58337770883919,-69.66898071584893,61.48033142089844,1.197848972514059,-23.1823787689209,-0.2442982008825716,-10.346473693847656,-0.01322569528687012,79.24906921386719,1.7574269661617024,35.91868591308594,0.33530820424675056,-71.48294067382812,-1.8139599579791934,J6,1.8139599579791934,1.1536991630766555,4159,12.568313,1381951,624375060,9,1,71.94799999999901,60.27695846557617,-0.00552398280820654,-22.918088912963867,0.019991655074459658,-10.347146987915039,-0.013898989354252933,77.54158782958984,0.0499455818843586,35.57386016845703,-0.009517540382155687,-69.74390411376953,-0.07492339792059965,J6,0.07492339792059965,0.03834497543010648
1 trigger_no waypoint_index trigger_frame_number trigger_time_relative_s trigger_sequence paired_status_frame_number paired_status_time_relative_s paired_status_sequence paired_status_timestamp paired_status_to_trigger_sequence_delta paired_status_to_trigger_time_ms teach_j1_deg teach_j2_deg teach_j3_deg teach_j4_deg teach_j5_deg teach_j6_deg paired_status_j1_actual_deg paired_status_diff_j1_deg paired_status_j2_actual_deg paired_status_diff_j2_deg paired_status_j3_actual_deg paired_status_diff_j3_deg paired_status_j4_actual_deg paired_status_diff_j4_deg paired_status_j5_actual_deg paired_status_diff_j5_deg paired_status_j6_actual_deg paired_status_diff_j6_deg paired_status_max_error_axis paired_status_max_error_deg paired_status_rms_error_deg best_status_frame_number best_status_time_relative_s best_status_sequence best_status_timestamp best_status_delta_from_paired_cycles best_status_delta_from_trigger_sequence best_status_time_after_trigger_ms best_status_j1_actual_deg best_status_diff_j1_deg best_status_j2_actual_deg best_status_diff_j2_deg best_status_j3_actual_deg best_status_diff_j3_deg best_status_j4_actual_deg best_status_diff_j4_deg best_status_j5_actual_deg best_status_diff_j5_deg best_status_j6_actual_deg best_status_diff_j6_deg best_status_max_error_axis best_status_max_error_deg best_status_rms_error_deg
2 1 1 1955 5.888271 1381124 1954 5.888166 1381116 624368380 -8 -0.10499999999957765 48.886810269468405 2.1989850886957285 -11.021017368511105 0.4102097980549552 6.248381265333557 2.294990756284542 55.47505187988281 6.588241610414407 1.2506256103515625 -0.948359478344166 -5.400295734405518 5.620721634105587 0.5259769558906555 0.1157671578357003 3.3612561225891113 -2.8871251427444458 0.44421958923339844 -1.8507711670511435 J1 6.588241610414407 3.8225443630072706 1981 5.968124 1381126 624368460 10 2 79.85300000000083 48.76339340209961 -0.12341686736879609 2.256481647491455 0.05749655879572657 -11.157909393310547 -0.13689202479944207 0.37860527634620667 -0.031604521708748556 6.3406662940979 0.0922850287643433 2.5025806427001953 0.20758988641565335 J6 0.20758988641565335 0.12239685259209541
3 2 2 2151 6.480252 1381198 2150 6.480138 1381190 624368972 -8 -0.11399999999994748 55.34775509527405 11.807039833001637 -7.0090952672806885 -71.01433145543973 6.012065051914967 74.24953191606797 53.0124397277832 -2.33531536749085 11.506516456604004 -0.30052337639763316 -8.466276168823242 -1.4571809015425536 -69.63382720947266 1.3805042459670744 6.426647663116455 0.41458261120148787 72.93537902832031 -1.3141528877476532 J1 2.33531536749085 1.3827543088729335 2172 6.552164 1381199 624369044 9 1 71.9120000000002 55.265621185302734 -0.08213390997131853 11.803675651550293 -0.003364181451344095 -7.050373554229736 -0.04127828694904778 -71.06609344482422 -0.05176198938448806 6.026607990264893 0.014542938349925372 74.28738403320312 0.03785211713515935 J1 0.08213390997131853 0.04616054814443023
4 3 3 2223 6.704225 1381226 2222 6.704166 1381218 624369196 -8 -0.059000000000253294 55.109808014787404 8.759497374223619 -8.518216825284897 -41.11725046606459 10.108488114686867 41.956933292858096 56.5781364440918 1.4683284293043926 10.494372367858887 1.7348749936352679 -7.114619255065918 1.4035975702189791 -55.112239837646484 -13.994989371581894 7.423236846923828 -2.6852512677630394 58.080867767333984 16.123934474475888 J6 16.123934474475888 8.852376616648156 2248 6.784154 1381228 624369276 10 2 79.92899999999992 55.00252151489258 -0.10728649989482619 8.69005012512207 -0.06944724910154854 -8.577027320861816 -0.05881049557691931 -40.59286117553711 0.5243892905274805 10.210430145263672 0.1019420305768044 41.38724136352539 -0.5696919293327056 J6 0.5696919293327056 0.3239642953120798
5 4 4 2336 7.048266 1381269 2335 7.048148 1381261 624369540 -8 -0.11799999999961841 43.65359310453334 -1.629659559507137 -17.715753611915318 5.995208633582219 32.17171770646655 -22.573973337684023 44.479610443115234 0.8260173385818916 0.07192186266183853 1.7015814221689756 -15.996956825256348 1.7187967866589702 2.40028715133667 -3.5949214822455486 27.737812042236328 -4.433905664230224 -17.672090530395508 4.901882807288516 J6 4.901882807288516 3.2440556235708993 2362 7.128175 1381271 624369620 10 2 79.90899999999979 43.72715759277344 0.07356448824009476 -1.6386029720306396 -0.008943412523502614 -17.75617218017578 -0.04041856826046342 5.952699661254883 -0.04250897232733575 32.2616081237793 0.08989041731274483 -22.47926902770996 0.09470430997406254 J6 0.09470430997406254 0.06580474324148583
6 5 5 2477 7.464289 1381321 2476 7.464178 1381313 624369956 -8 -0.11099999999952814 64.58244475717193 4.262979315506351 -15.669217122643433 -29.952927185666542 29.850439933020308 45.62652743544272 60.830230712890625 -3.7522140442813026 1.8808640241622925 -2.382115291344059 -17.297021865844727 -1.6278047432012936 -26.684900283813477 3.268026901853066 33.02191925048828 3.1714793174679734 36.40708923339844 -9.21943820204428 J6 9.21943820204428 4.621321121191458 2502 7.544122 1381323 624370036 10 2 79.83299999999983 64.5761947631836 -0.006249993988333813 4.283629417419434 0.020650101913082253 -15.615406036376953 0.05381108626647979 -29.94143295288086 0.011494232785683067 29.7938175201416 -0.056622412878706285 45.61851119995117 -0.008016235491545842 J5 0.056622412878706285 0.03357478517689402
7 6 6 2665 8.024277 1381391 2664 8.024156 1381383 624370516 -8 -0.12100000000003774 60.47948252708421 23.068781981390185 -5.01126420129949 36.10685486878251 5.525681179628412 -31.3025636495649 60.89759063720703 0.418108110122823 20.716407775878906 -2.3523742055112784 -4.913214206695557 0.09804999460393304 32.72084045410156 -3.3860144146809503 6.405356407165527 0.8796752275371151 -25.156103134155273 6.1464605154096255 J6 6.1464605154096255 3.0478405356855083 2687 8.096181 1381392 624370588 9 1 71.90399999999997 60.54315185546875 0.06366932838454176 22.99761962890625 -0.07116235248393465 -5.037043571472168 -0.025779370172678284 36.056732177734375 -0.050122691048137824 5.545217037200928 0.019535857572515525 -31.12558937072754 0.17697427883735983 J6 0.17697427883735983 0.08563125303054821
8 7 7 2811 8.472317 1381447 2810 8.472208 1381439 624370964 -8 -0.10900000000013677 70.47583707168602 16.39384887825908 -13.456948007467595 -27.892318852946243 14.53566195089614 31.71193282686115 69.13842010498047 -1.3374169667055469 22.202428817749023 5.808579939489942 -12.907807350158691 0.5491406573089037 -15.469873428344727 12.422445424601516 14.040997505187988 -0.4946644457081515 18.56021499633789 -13.15171783052326 J6 13.15171783052326 7.78202379046887 2834 8.54419 1381448 624371036 9 1 71.87300000000008 70.3967514038086 -0.07908566787742188 16.456680297851562 0.06283141959248084 -13.330730438232422 0.12621756923517324 -27.611600875854492 0.2807179770917507 14.542427062988281 0.006765112092141479 31.405345916748047 -0.30658691011310424 J6 0.30658691011310424 0.18210669390591905
9 8 8 2908 8.760361 1381483 2907 8.760245 1381475 624371252 -8 -0.11600000000022703 69.58246408878419 -17.10571341532583 -8.710589696831251 -58.47569441890704 7.63059003611043 64.43773280685575 70.10379791259766 0.5213338238134639 -10.075156211853027 7.030557203472803 -9.98978042602539 -1.2791907291941396 -51.5866584777832 6.889035941123836 9.534638404846191 1.9040483687357614 57.0014533996582 -7.436279407197546 J6 7.436279407197546 5.127041386597523 2929 8.832199 1381484 624371324 9 1 71.83799999999962 69.59716033935547 0.014696250571276437 -17.044965744018555 0.06074767130727565 -8.703485488891602 0.007104207939649498 -58.473426818847656 0.0022676000593833123 7.6463942527771 0.015804216666669646 64.45584869384766 0.01811588699190736 J2 0.06074767130727565 0.02750705896232285
10 9 9 3112 9.384332 1381561 3111 9.384204 1381553 624371876 -8 -0.128000000000128 73.57125874861414 -6.42984524964374 -9.628579811400881 -128.80864727564332 14.671082817606491 140.00204816414424 72.96144104003906 -0.6098177085750791 -6.3001484870910645 0.12969676255267526 -10.035088539123535 -0.40650872772265423 -123.29313659667969 5.515510678963636 13.165532112121582 -1.505550705484909 134.41644287109375 -5.585605293050492 J6 5.585605293050492 3.277206017832995 3134 9.456205 1381562 624371948 9 1 71.87300000000008 73.55095672607422 -0.02030202253992286 -6.441007614135742 -0.011162364492002474 -9.614203453063965 0.014376358336916084 -128.65638732910156 0.15225994654176134 14.635379791259766 -0.03570302634672551 139.8520965576172 -0.14995160652705408 J4 0.15225994654176134 0.089150370994325
11 10 10 3170 9.560358 1381583 3169 9.56025 1381575 624372052 -8 -0.10800000000088517 75.56938603377543 -14.67930590788221 -7.294156094303152 -130.92303342701462 17.688361072687904 141.89350789658602 74.36400604248047 -1.2053799912949614 -10.514228820800781 4.165077087081428 -8.236283302307129 -0.9421272080039769 -132.6507110595703 -1.7276776325556966 16.38176155090332 -1.3065995217845838 143.5712890625 1.6777811659139843 J2 4.165077087081428 2.128983861449047 3196 9.640214 1381585 624372132 10 2 79.85599999999948 75.62290954589844 0.05352351212300732 -14.662798881530762 0.016507026351447962 -7.297325611114502 -0.003169516811349915 -130.88812255859375 0.03491086842086588 17.726865768432617 0.03850469574471305 141.820068359375 -0.0734395372110157 J6 0.0734395372110157 0.043285948566531826
12 11 11 3350 10.104322 1381651 3349 10.104215 1381643 624372596 -8 -0.10699999999985721 86.0934977330502 -14.498333177585573 -13.681510793859989 -69.8682955440411 26.742681901805224 88.9994136192388 87.81878662109375 1.7252888880435506 -16.329286575317383 -1.8309533977318093 -13.073507308959961 0.6080034849000278 -70.97254180908203 -1.1042462650409277 26.366661071777344 -0.3760208300278798 89.37981414794922 0.3804005287104246 J2 1.8309533977318093 1.169340011162825 3373 10.17622 1381652 624372668 9 1 71.8980000000009 86.11857604980469 0.025078316754488128 -14.53605842590332 -0.03772524831774682 -13.651655197143555 0.029855596716434007 -69.8272933959961 0.04100214804500979 26.72992706298828 -0.012754838816942282 88.92174530029297 -0.07766831894582538 J6 0.07766831894582538 0.04246508921571141
13 12 12 3470 10.472333 1381697 3469 10.472191 1381689 624372964 -8 -0.14200000000030855 61.720732564877665 -4.2327893098442155 -9.784423185760874 -108.38408270751574 22.16077221865394 118.14206389103131 65.91849517822266 4.197762613344992 -6.331206798553467 -2.0984174887092513 -11.008773803710938 -1.2243506179500638 -103.56172943115234 4.822353276363401 23.532543182373047 1.3717709637191078 117.46694946289062 -0.675114428140688 J4 4.822353276363401 2.8611252897135864 3493 10.544283 1381698 624373036 9 1 71.94999999999929 61.7418098449707 0.021077280093038553 -4.212831497192383 0.019957812651832718 -9.789913177490234 -0.00548999172936071 -108.36312103271484 0.020961674800901164 22.163673400878906 0.002901182224967158 118.13783264160156 -0.0042312494297505054 J1 0.021077280093038553 0.014935517911187784
14 13 13 3627 10.944371 1381756 3626 10.944251 1381748 624373436 -8 -0.12000000000078614 79.75708299218505 4.6402310571176475 -15.311185536748695 -56.03531247084017 26.13313788666632 52.152257172101606 72.04009246826172 -7.716990523923329 4.5515007972717285 -0.08873025984591898 -11.385177612304688 3.926007924444008 -64.18782043457031 -8.152507963730145 22.412456512451172 -3.7206813742151468 54.39056396484375 2.2383067927421436 J4 8.152507963730145 5.168659196117862 3651 11.016226 1381757 624373508 9 1 71.85499999999934 79.90262603759766 0.14554304541260876 4.62429666519165 -0.015934391925997105 -15.370849609375 -0.05966407262630469 -55.99199676513672 0.043315705703449225 26.182939529418945 0.04980164275262666 52.15596008300781 0.003702910906206114 J1 0.14554304541260876 0.0699602863618738
15 14 14 3710 11.192339 1381787 3709 11.19225 1381779 624373684 -8 -0.08900000000089392 108.95555081237923 1.3707600427061273 -33.59442519685133 -48.36736100282485 43.963403989432074 85.03926111958742 102.20675659179688 -6.748794220582354 2.5045764446258545 1.1338164019197272 -29.148040771484375 4.446384425366958 -48.76271438598633 -0.3953533831614777 39.65629959106445 -4.3071043983676205 74.63484191894531 -10.40441920064211 J6 10.40441920064211 5.679810575214033 3735 11.272261 1381789 624373764 10 2 79.92199999999983 108.94917297363281 -0.006377838746416842 1.3447811603546143 -0.025978882351513022 -33.561458587646484 0.032966609204848396 -48.376487731933594 -0.009126729108743348 44.02882766723633 0.06542367780425451 85.1587142944336 0.11945317484617135 J6 0.11945317484617135 0.05835950688086092
16 15 15 3796 11.456361 1381820 3795 11.456253 1381812 624373948 -8 -0.10799999999910881 110.58484797608095 -3.860899593758653 -32.39640711653168 -51.112590875369015 44.2291595765054 87.62399978413751 110.58193969726562 -0.002908278815326071 -1.244791865348816 2.6161077284098373 -33.74542999267578 -1.3490228761441045 -48.585723876953125 2.5268669984158905 44.58126449584961 0.35210491934420673 87.75857543945312 0.134575655315615 J2 2.6161077284098373 1.5911754635072362 3821 11.536287 1381822 624374028 10 2 79.92600000000039 110.67292022705078 0.08807225096983018 -4.010702133178711 -0.14980253942005772 -32.37457275390625 0.021834362625426706 -51.22603225708008 -0.11344138171106266 44.249629974365234 0.020470397859831735 87.66435241699219 0.040352632854677495 J2 0.14980253942005772 0.08716897465096986
17 16 16 3870 11.680362 1381848 3869 11.680263 1381840 624374172 -8 -0.09900000000051534 118.09595224767921 -17.37638727211256 -31.06900084212659 -59.56053843778568 48.73257576059714 94.13477131163891 115.98970031738281 -2.106251930296395 -12.693913459777832 4.682473812334727 -31.563575744628906 -0.49457490250231473 -59.300933837890625 0.2596045998950558 47.15116500854492 -1.5814107520522214 94.34331512451172 0.20854381287280432 J2 4.682473812334727 2.2067399623329567 3895 11.760326 1381850 624374252 10 2 79.96399999999859 118.06391906738281 -0.0320331802963949 -17.536325454711914 -0.15993818259935466 -31.00341796875 0.06558287337659152 -59.507286071777344 0.05325236600833705 48.77106475830078 0.038488997703638006 93.9618148803711 -0.17295643126782068 J6 0.17295643126782068 0.10419421346699378
18 17 18 4138 12.496365 1381950 4137 12.496271 1381942 624374988 -8 -0.09400000000070463 60.28248244838438 -22.938080568038327 -10.333247998560786 77.49164224770549 35.58337770883919 -69.66898071584893 61.48033142089844 1.197848972514059 -23.1823787689209 -0.2442982008825716 -10.346473693847656 -0.01322569528687012 79.24906921386719 1.7574269661617024 35.91868591308594 0.33530820424675056 -71.48294067382812 -1.8139599579791934 J6 1.8139599579791934 1.1536991630766555 4159 12.568313 1381951 624375060 9 1 71.94799999999901 60.27695846557617 -0.00552398280820654 -22.918088912963867 0.019991655074459658 -10.347146987915039 -0.013898989354252933 77.54158782958984 0.0499455818843586 35.57386016845703 -0.009517540382155687 -69.74390411376953 -0.07492339792059965 J6 0.07492339792059965 0.03834497543010648

View File

@@ -1,18 +0,0 @@
trigger_no,waypoint_index,frame_number,sequence,time_relative_s,write_io_value,io_addrs,config_addr,max_error_axis,max_error_deg,rms_error_deg,j1_actual_deg,j1_teach_deg,diff_j1_deg,j2_actual_deg,j2_teach_deg,diff_j2_deg,j3_actual_deg,j3_teach_deg,diff_j3_deg,j4_actual_deg,j4_teach_deg,diff_j4_deg,j5_actual_deg,j5_teach_deg,diff_j5_deg,j6_actual_deg,j6_teach_deg,diff_j6_deg
1,1,1955,1381124,5.888271,10,"[2, 4]","[2, 4]",J6,3.4065268256551553,2.468438977201345,45.8632698059082,48.886810269468405,-3.0235404635602023,2.9724788665771484,2.1989850886957285,0.7734937778814199,-13.609341621398926,-11.021017368511105,-2.588324252887821,-2.1851141452789307,0.4102097980549552,-2.5953239433338857,7.582361221313477,6.248381265333557,1.3339799559799195,5.701517581939697,2.294990756284542,3.4065268256551553
2,2,2151,1381198,6.480252,14,"[2, 3, 4]","[3, 4, 2]",J4,2.8167225076858244,1.6562461612538633,56.34336471557617,55.34775509527405,0.995609620302119,11.66723918914795,11.807039833001637,-0.13980064385368784,-6.600923538208008,-7.0090952672806885,0.40817172907268073,-68.1976089477539,-71.01433145543973,2.8167225076858244,6.058437347412109,6.012065051914967,0.04637229549714217,71.5392837524414,74.24953191606797,-2.7102481636265594
3,3,2223,1381226,6.704225,14,"[2, 3, 4]","[3, 4, 2]",J6,11.130744251720401,5.955607695749779,53.50831985473633,55.109808014787404,-1.6014881600510762,7.412222862243652,8.759497374223619,-1.3472745119799665,-9.629739761352539,-8.518216825284897,-1.111522936067642,-32.30571365356445,-41.11725046606459,8.811536812500137,12.488306045532227,10.108488114686867,2.379817930845359,30.826189041137695,41.956933292858096,-11.130744251720401
4,4,2336,1381269,7.048266,10,"[2, 4]","[4, 2]",J6,2.3116842904428125,1.3731583485126422,44.433006286621094,43.65359310453334,0.779413182087751,-2.1156294345855713,-1.629659559507137,-0.48596987507843425,-18.39777374267578,-17.715753611915318,-0.6820201307604634,4.8371992111206055,5.995208633582219,-1.158009422461613,33.99372482299805,32.17171770646655,1.8220071165314948,-20.26228904724121,-22.573973337684023,2.3116842904428125
5,5,2477,1381321,7.464289,10,"[2, 4]","[4, 2]",J5,2.171828177649214,1.404704060246151,65.84967041015625,64.58244475717193,1.2672256529843224,5.695003032684326,4.262979315506351,1.4320237171779748,-14.563636779785156,-15.669217122643433,1.1055803428582767,-28.69342613220215,-29.952927185666542,1.259501053464394,27.678611755371094,29.850439933020308,-2.171828177649214,46.437156677246094,45.62652743544272,0.810629241803376
6,6,2665,1381391,8.024277,12,"[3, 4]","[3, 4]",J2,1.408993592340284,0.8424031169595625,60.847530364990234,60.47948252708421,0.36804783790602613,24.47777557373047,23.068781981390185,1.408993592340284,-5.617043972015381,-5.01126420129949,-0.6057797707158912,34.8044319152832,36.10685486878251,-1.3024229534993097,5.786255359649658,5.525681179628412,0.260574180021246,-31.2253475189209,-31.3025636495649,0.07721613064400046
7,7,2811,1381447,8.472317,12,"[3, 4]","[3, 4]",J6,7.1941073647892395,4.618545262978315,70.8250503540039,70.47583707168602,0.3492132823178906,10.95259952545166,16.39384887825908,-5.4412493528074215,-13.136046409606934,-13.456948007467595,0.3209015978606615,-34.68459701538086,-27.892318852946243,-6.7922781624346165,14.022420883178711,14.53566195089614,-0.5132410677174288,38.90604019165039,31.71193282686115,7.1941073647892395
8,8,2908,1381483,8.760361,10,"[2, 4]","[4, 2]",J6,5.93988407546847,3.533132924843701,69.40516662597656,69.58246408878419,-0.1772974628076298,-20.066781997680664,-17.10571341532583,-2.9610685823548337,-8.152935981750488,-8.710589696831251,0.5576537150807628,-63.93235397338867,-58.47569441890704,-5.456659554481632,6.775912284851074,7.63059003611043,-0.8546777512593557,70.37761688232422,64.43773280685575,5.93988407546847
9,9,3112,1381561,9.384332,10,"[2, 4]","[4, 2]",J4,2.5240706198644887,1.630593251762862,73.90592956542969,73.57125874861414,0.3346708168155459,-8.016124725341797,-6.42984524964374,-1.5862794756980572,-9.002426147460938,-9.628579811400881,0.6261536639399434,-131.3327178955078,-128.80864727564332,-2.5240706198644887,15.526206970214844,14.671082817606491,0.8551241526083526,142.41668701171875,140.00204816414424,2.4146388475745084
10,10,3170,1381583,9.560358,10,"[2, 4]","[4, 2]",J4,3.3929965007450846,2.1059862266772753,76.72688293457031,75.56938603377543,1.1574969007948823,-16.518726348876953,-14.67930590788221,-1.8394204409947434,-7.15147066116333,-7.294156094303152,0.14268543313982196,-127.53003692626953,-130.92303342701462,3.3929965007450846,18.53788948059082,17.688361072687904,0.8495284079029162,138.7897491455078,141.89350789658602,-3.103758751078203
11,11,3350,1381651,10.104322,10,"[2, 4]","[4, 2]",J6,2.9848240516596434,2.0134925384056306,83.71662139892578,86.0934977330502,-2.376876334124418,-13.186573028564453,-14.498333177585573,1.3117601490211204,-13.738808631896973,-13.681510793859989,-0.05729783803698396,-72.70317840576172,-69.8682955440411,-2.834882861720615,26.66758918762207,26.742681901805224,-0.07509271418315322,91.98423767089844,88.9994136192388,2.9848240516596434
12,12,3470,1381697,10.472333,10,"[2, 4]","[4, 2]",J6,4.000958239175844,1.9902461373621054,59.78955841064453,61.720732564877665,-1.9311741542331333,-2.7094168663024902,-4.2327893098442155,1.5233724435417253,-8.933828353881836,-9.784423185760874,0.8505948318790377,-108.24857330322266,-108.38408270751574,0.13550940429308866,21.177492141723633,22.16077221865394,-0.9832800769303063,114.14110565185547,118.14206389103131,-4.000958239175844
13,13,3627,1381756,10.944371,12,"[3, 4]","[4, 3]",J1,6.656208938967296,4.109282348845555,86.41329193115234,79.75708299218505,6.656208938967296,4.266173362731934,4.6402310571176475,-0.3740576943857139,-19.15987777709961,-15.311185536748695,-3.848692240350914,-52.24028015136719,-56.03531247084017,3.7950323194729805,29.853723526000977,26.13313788666632,3.720585639334658,55.86907196044922,52.152257172101606,3.7168147883476124
14,14,3710,1381787,11.192339,10,"[2, 4]","[4, 2]",J6,2.93518040873289,1.5679827735552678,110.7523193359375,108.95555081237923,1.7967685235582707,0.8751887679100037,1.3707600427061273,-0.4955712747961236,-34.756534576416016,-33.59442519685133,-1.1621093795646829,-48.14332580566406,-48.36736100282485,0.2240351971607879,45.0865592956543,43.963403989432074,1.1231553062222233,87.97444152832031,85.03926111958742,2.93518040873289
15,15,3796,1381820,11.456361,10,"[2, 4]","[4, 2]",J4,2.573146734494266,1.686689503314435,111.79177856445312,110.58484797608095,1.206930588372174,-6.225440502166748,-3.860899593758653,-2.364540908408095,-31.958988189697266,-32.39640711653168,0.4374189268344111,-53.68573760986328,-51.112590875369015,-2.573146734494266,44.76150894165039,44.2291595765054,0.532349365144988,89.33454895019531,87.62399978413751,1.7105491660578025
16,16,3870,1381848,11.680362,10,"[2, 4]","[4, 2]",J6,3.8105220440607894,2.197782779718695,117.87474060058594,118.09595224767921,-0.2212116470932699,-19.607177734375,-17.37638727211256,-2.2307904622624406,-30.329103469848633,-31.06900084212659,0.7398973722779587,-56.600250244140625,-59.56053843778568,2.960288193645056,49.08655548095703,48.73257576059714,0.353979720359888,90.32424926757812,94.13477131163891,-3.8105220440607894
17,18,4138,1381950,12.496365,12,"[3, 4]","[4, 3]",J4,6.848522435693766,4.03614029285736,59.93589401245117,60.28248244838438,-0.34658843593320654,-21.102140426635742,-22.938080568038327,1.8359401414025847,-9.684239387512207,-10.333247998560786,0.6490086110485791,70.64311981201172,77.49164224770549,-6.848522435693766,32.941307067871094,35.58337770883919,-2.642070640968093,-63.34855651855469,-69.66898071584893,6.320424197294244
1 trigger_no waypoint_index frame_number sequence time_relative_s write_io_value io_addrs config_addr max_error_axis max_error_deg rms_error_deg j1_actual_deg j1_teach_deg diff_j1_deg j2_actual_deg j2_teach_deg diff_j2_deg j3_actual_deg j3_teach_deg diff_j3_deg j4_actual_deg j4_teach_deg diff_j4_deg j5_actual_deg j5_teach_deg diff_j5_deg j6_actual_deg j6_teach_deg diff_j6_deg
2 1 1 1955 1381124 5.888271 10 [2, 4] [2, 4] J6 3.4065268256551553 2.468438977201345 45.8632698059082 48.886810269468405 -3.0235404635602023 2.9724788665771484 2.1989850886957285 0.7734937778814199 -13.609341621398926 -11.021017368511105 -2.588324252887821 -2.1851141452789307 0.4102097980549552 -2.5953239433338857 7.582361221313477 6.248381265333557 1.3339799559799195 5.701517581939697 2.294990756284542 3.4065268256551553
3 2 2 2151 1381198 6.480252 14 [2, 3, 4] [3, 4, 2] J4 2.8167225076858244 1.6562461612538633 56.34336471557617 55.34775509527405 0.995609620302119 11.66723918914795 11.807039833001637 -0.13980064385368784 -6.600923538208008 -7.0090952672806885 0.40817172907268073 -68.1976089477539 -71.01433145543973 2.8167225076858244 6.058437347412109 6.012065051914967 0.04637229549714217 71.5392837524414 74.24953191606797 -2.7102481636265594
4 3 3 2223 1381226 6.704225 14 [2, 3, 4] [3, 4, 2] J6 11.130744251720401 5.955607695749779 53.50831985473633 55.109808014787404 -1.6014881600510762 7.412222862243652 8.759497374223619 -1.3472745119799665 -9.629739761352539 -8.518216825284897 -1.111522936067642 -32.30571365356445 -41.11725046606459 8.811536812500137 12.488306045532227 10.108488114686867 2.379817930845359 30.826189041137695 41.956933292858096 -11.130744251720401
5 4 4 2336 1381269 7.048266 10 [2, 4] [4, 2] J6 2.3116842904428125 1.3731583485126422 44.433006286621094 43.65359310453334 0.779413182087751 -2.1156294345855713 -1.629659559507137 -0.48596987507843425 -18.39777374267578 -17.715753611915318 -0.6820201307604634 4.8371992111206055 5.995208633582219 -1.158009422461613 33.99372482299805 32.17171770646655 1.8220071165314948 -20.26228904724121 -22.573973337684023 2.3116842904428125
6 5 5 2477 1381321 7.464289 10 [2, 4] [4, 2] J5 2.171828177649214 1.404704060246151 65.84967041015625 64.58244475717193 1.2672256529843224 5.695003032684326 4.262979315506351 1.4320237171779748 -14.563636779785156 -15.669217122643433 1.1055803428582767 -28.69342613220215 -29.952927185666542 1.259501053464394 27.678611755371094 29.850439933020308 -2.171828177649214 46.437156677246094 45.62652743544272 0.810629241803376
7 6 6 2665 1381391 8.024277 12 [3, 4] [3, 4] J2 1.408993592340284 0.8424031169595625 60.847530364990234 60.47948252708421 0.36804783790602613 24.47777557373047 23.068781981390185 1.408993592340284 -5.617043972015381 -5.01126420129949 -0.6057797707158912 34.8044319152832 36.10685486878251 -1.3024229534993097 5.786255359649658 5.525681179628412 0.260574180021246 -31.2253475189209 -31.3025636495649 0.07721613064400046
8 7 7 2811 1381447 8.472317 12 [3, 4] [3, 4] J6 7.1941073647892395 4.618545262978315 70.8250503540039 70.47583707168602 0.3492132823178906 10.95259952545166 16.39384887825908 -5.4412493528074215 -13.136046409606934 -13.456948007467595 0.3209015978606615 -34.68459701538086 -27.892318852946243 -6.7922781624346165 14.022420883178711 14.53566195089614 -0.5132410677174288 38.90604019165039 31.71193282686115 7.1941073647892395
9 8 8 2908 1381483 8.760361 10 [2, 4] [4, 2] J6 5.93988407546847 3.533132924843701 69.40516662597656 69.58246408878419 -0.1772974628076298 -20.066781997680664 -17.10571341532583 -2.9610685823548337 -8.152935981750488 -8.710589696831251 0.5576537150807628 -63.93235397338867 -58.47569441890704 -5.456659554481632 6.775912284851074 7.63059003611043 -0.8546777512593557 70.37761688232422 64.43773280685575 5.93988407546847
10 9 9 3112 1381561 9.384332 10 [2, 4] [4, 2] J4 2.5240706198644887 1.630593251762862 73.90592956542969 73.57125874861414 0.3346708168155459 -8.016124725341797 -6.42984524964374 -1.5862794756980572 -9.002426147460938 -9.628579811400881 0.6261536639399434 -131.3327178955078 -128.80864727564332 -2.5240706198644887 15.526206970214844 14.671082817606491 0.8551241526083526 142.41668701171875 140.00204816414424 2.4146388475745084
11 10 10 3170 1381583 9.560358 10 [2, 4] [4, 2] J4 3.3929965007450846 2.1059862266772753 76.72688293457031 75.56938603377543 1.1574969007948823 -16.518726348876953 -14.67930590788221 -1.8394204409947434 -7.15147066116333 -7.294156094303152 0.14268543313982196 -127.53003692626953 -130.92303342701462 3.3929965007450846 18.53788948059082 17.688361072687904 0.8495284079029162 138.7897491455078 141.89350789658602 -3.103758751078203
12 11 11 3350 1381651 10.104322 10 [2, 4] [4, 2] J6 2.9848240516596434 2.0134925384056306 83.71662139892578 86.0934977330502 -2.376876334124418 -13.186573028564453 -14.498333177585573 1.3117601490211204 -13.738808631896973 -13.681510793859989 -0.05729783803698396 -72.70317840576172 -69.8682955440411 -2.834882861720615 26.66758918762207 26.742681901805224 -0.07509271418315322 91.98423767089844 88.9994136192388 2.9848240516596434
13 12 12 3470 1381697 10.472333 10 [2, 4] [4, 2] J6 4.000958239175844 1.9902461373621054 59.78955841064453 61.720732564877665 -1.9311741542331333 -2.7094168663024902 -4.2327893098442155 1.5233724435417253 -8.933828353881836 -9.784423185760874 0.8505948318790377 -108.24857330322266 -108.38408270751574 0.13550940429308866 21.177492141723633 22.16077221865394 -0.9832800769303063 114.14110565185547 118.14206389103131 -4.000958239175844
14 13 13 3627 1381756 10.944371 12 [3, 4] [4, 3] J1 6.656208938967296 4.109282348845555 86.41329193115234 79.75708299218505 6.656208938967296 4.266173362731934 4.6402310571176475 -0.3740576943857139 -19.15987777709961 -15.311185536748695 -3.848692240350914 -52.24028015136719 -56.03531247084017 3.7950323194729805 29.853723526000977 26.13313788666632 3.720585639334658 55.86907196044922 52.152257172101606 3.7168147883476124
15 14 14 3710 1381787 11.192339 10 [2, 4] [4, 2] J6 2.93518040873289 1.5679827735552678 110.7523193359375 108.95555081237923 1.7967685235582707 0.8751887679100037 1.3707600427061273 -0.4955712747961236 -34.756534576416016 -33.59442519685133 -1.1621093795646829 -48.14332580566406 -48.36736100282485 0.2240351971607879 45.0865592956543 43.963403989432074 1.1231553062222233 87.97444152832031 85.03926111958742 2.93518040873289
16 15 15 3796 1381820 11.456361 10 [2, 4] [4, 2] J4 2.573146734494266 1.686689503314435 111.79177856445312 110.58484797608095 1.206930588372174 -6.225440502166748 -3.860899593758653 -2.364540908408095 -31.958988189697266 -32.39640711653168 0.4374189268344111 -53.68573760986328 -51.112590875369015 -2.573146734494266 44.76150894165039 44.2291595765054 0.532349365144988 89.33454895019531 87.62399978413751 1.7105491660578025
17 16 16 3870 1381848 11.680362 10 [2, 4] [4, 2] J6 3.8105220440607894 2.197782779718695 117.87474060058594 118.09595224767921 -0.2212116470932699 -19.607177734375 -17.37638727211256 -2.2307904622624406 -30.329103469848633 -31.06900084212659 0.7398973722779587 -56.600250244140625 -59.56053843778568 2.960288193645056 49.08655548095703 48.73257576059714 0.353979720359888 90.32424926757812 94.13477131163891 -3.8105220440607894
18 17 18 4138 1381950 12.496365 12 [3, 4] [4, 3] J4 6.848522435693766 4.03614029285736 59.93589401245117 60.28248244838438 -0.34658843593320654 -21.102140426635742 -22.938080568038327 1.8359401414025847 -9.684239387512207 -10.333247998560786 0.6490086110485791 70.64311981201172 77.49164224770549 -6.848522435693766 32.941307067871094 35.58337770883919 -2.642070640968093 -63.34855651855469 -69.66898071584893 6.320424197294244

View File

@@ -1,363 +0,0 @@
#!/usr/bin/env python3
"""提取 2026042802-1 抓包中的 60015 状态反馈,并和 UTTC_MS11 示教点对比。"""
from __future__ import annotations
import csv
import json
import math
import struct
import subprocess
from collections import Counter
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
DEFAULT_PCAP = REPO_ROOT.parent / "Rvbust" / "uttc-20260428" / "2026042802-1.pcap"
DEFAULT_TSHARK = Path(r"D:\Zyx\Downloads\WiresharkPortable32\App\Wireshark\tshark.exe")
OUTPUT_DIR = REPO_ROOT / "analysis" / "2026042802-1"
CONFIG_PATH = REPO_ROOT / "Config" / "RobotConfig.json"
SEARCH_WINDOW_CYCLES = 20
def be_u32(data: bytes, offset: int) -> int:
"""按大端读取 4 字节无符号整数。"""
return struct.unpack(">I", data[offset : offset + 4])[0]
def be_u16(data: bytes, offset: int) -> int:
"""按大端读取 2 字节无符号整数。"""
return struct.unpack(">H", data[offset : offset + 2])[0]
def be_f32(data: bytes, offset: int) -> float:
"""按大端读取 4 字节浮点数。"""
return struct.unpack(">f", data[offset : offset + 4])[0]
def load_udp_rows(pcap: Path, tshark: Path) -> list[list[str]]:
"""提取 UDP 60015 原始字段,后续按方向和长度拆分命令与状态。"""
command = [
str(tshark),
"-r",
str(pcap),
"-Y",
"udp.port==60015",
"-T",
"fields",
"-e",
"frame.number",
"-e",
"frame.time_relative",
"-e",
"ip.src",
"-e",
"ip.dst",
"-e",
"udp.payload",
]
output = subprocess.check_output(command, text=True, encoding="utf-8", errors="ignore")
rows: list[list[str]] = []
for line in output.splitlines():
if not line.strip():
continue
parts = line.split("\t")
if len(parts) >= 5:
rows.append(parts[:5])
return rows
def decode_command_records(rows: list[list[str]], client_ip: str, robot_ip: str) -> list[dict]:
"""把 64B J519 命令帧解码成结构化记录。"""
records: list[dict] = []
for frame_no, time_rel, ip_src, ip_dst, payload_hex in rows:
if ip_src != client_ip or ip_dst != robot_ip:
continue
payload = bytes.fromhex(payload_hex)
if len(payload) != 64:
continue
records.append(
{
"frame_number": int(frame_no),
"time_relative_s": float(time_rel),
"sequence": be_u32(payload, 0x08),
"write_io_value": be_u16(payload, 0x18),
"j1_deg": be_f32(payload, 0x1C),
"j2_deg": be_f32(payload, 0x20),
"j3_deg": be_f32(payload, 0x24),
"j4_deg": be_f32(payload, 0x28),
"j5_deg": be_f32(payload, 0x2C),
"j6_deg": be_f32(payload, 0x30),
}
)
return records
def decode_status_records(rows: list[list[str]], client_ip: str, robot_ip: str) -> list[dict]:
"""把 132B J519 状态帧按运行时代码同口径解码。"""
records: list[dict] = []
for frame_no, time_rel, ip_src, ip_dst, payload_hex in rows:
if ip_src != robot_ip or ip_dst != client_ip:
continue
payload = bytes.fromhex(payload_hex)
if len(payload) != 132:
continue
status = payload[0x0C]
joints = [be_f32(payload, 0x3C + index * 4) for index in range(6)]
pose = [be_f32(payload, 0x18 + index * 4) for index in range(6)]
records.append(
{
"frame_number": int(frame_no),
"time_relative_s": float(time_rel),
"sequence": be_u32(payload, 0x08),
"status": status,
"accepts_command": bool(status & 0b0001),
"received_command": bool(status & 0b0010),
"system_ready": bool(status & 0b0100),
"robot_in_motion": bool(status & 0b1000),
"read_io_value": be_u16(payload, 0x12),
"timestamp": be_u32(payload, 0x14),
"pose_x_mm": pose[0],
"pose_y_mm": pose[1],
"pose_z_mm": pose[2],
"pose_w_deg": pose[3],
"pose_p_deg": pose[4],
"pose_r_deg": pose[5],
"j1_deg": joints[0],
"j2_deg": joints[1],
"j3_deg": joints[2],
"j4_deg": joints[3],
"j5_deg": joints[4],
"j6_deg": joints[5],
}
)
return records
def pick_trigger_first_high_frames(records: list[dict]) -> list[dict]:
"""由于 io_keep_cycles=2,只保留每组高电平脉冲的第一帧。"""
trigger_frames: list[dict] = []
previous_high = False
for record in records:
current_high = record["write_io_value"] > 0
if current_high and not previous_high:
trigger_frames.append(record)
previous_high = current_high
return trigger_frames
def load_uttc_ms11_config() -> dict:
"""读取 UTTC_MS11 的示教点和触发配置。"""
config = json.loads(CONFIG_PATH.read_text(encoding="utf-8"))
return config["flying_shots"]["UTTC_MS11"]
def build_diff_row(prefix: str, actual_deg: list[float], teach_deg: list[float], row: dict) -> tuple[float, float, str]:
"""向结果行写入逐轴误差,并返回聚合误差。"""
diffs = [actual_deg[index] - teach_deg[index] for index in range(6)]
abs_diffs = [abs(value) for value in diffs]
max_error = max(abs_diffs)
max_error_axis = f"J{abs_diffs.index(max_error) + 1}"
rms_error = math.sqrt(sum(value * value for value in diffs) / 6.0)
for joint_index in range(6):
joint_no = joint_index + 1
row[f"{prefix}_j{joint_no}_actual_deg"] = actual_deg[joint_index]
row[f"{prefix}_diff_j{joint_no}_deg"] = diffs[joint_index]
row[f"{prefix}_max_error_axis"] = max_error_axis
row[f"{prefix}_max_error_deg"] = max_error
row[f"{prefix}_rms_error_deg"] = rms_error
return max_error, rms_error, max_error_axis
def build_trigger_status_rows(trigger_frames: list[dict], status_records: list[dict], shot_config: dict) -> list[dict]:
"""按触发顺序对齐命令帧、当前状态帧以及最接近示教点的反馈状态帧。"""
rows: list[dict] = []
trigger_waypoint_indices = [index for index, flag in enumerate(shot_config["shot_flags"]) if flag]
status_by_sequence = {record["sequence"]: record for record in status_records}
status_sequence_set = set(status_by_sequence)
for trigger_no, (trigger_frame, waypoint_index) in enumerate(zip(trigger_frames, trigger_waypoint_indices), start=1):
teach_deg = [math.degrees(value) for value in shot_config["traj_waypoints"][waypoint_index]]
current_status_sequence = trigger_frame["sequence"] - 8
current_status = status_by_sequence[current_status_sequence]
row = {
"trigger_no": trigger_no,
"waypoint_index": waypoint_index,
"trigger_frame_number": trigger_frame["frame_number"],
"trigger_time_relative_s": trigger_frame["time_relative_s"],
"trigger_sequence": trigger_frame["sequence"],
"paired_status_frame_number": current_status["frame_number"],
"paired_status_time_relative_s": current_status["time_relative_s"],
"paired_status_sequence": current_status["sequence"],
"paired_status_timestamp": current_status["timestamp"],
"paired_status_to_trigger_sequence_delta": current_status["sequence"] - trigger_frame["sequence"],
"paired_status_to_trigger_time_ms": (current_status["time_relative_s"] - trigger_frame["time_relative_s"]) * 1000.0,
}
for joint_index in range(6):
joint_no = joint_index + 1
row[f"teach_j{joint_no}_deg"] = teach_deg[joint_index]
build_diff_row(
"paired_status",
[current_status[f"j{joint_no}_deg"] for joint_no in range(1, 7)],
teach_deg,
row,
)
best_candidate = None
for delta_cycles in range(-SEARCH_WINDOW_CYCLES, SEARCH_WINDOW_CYCLES + 1):
candidate_sequence = current_status_sequence + delta_cycles
if candidate_sequence not in status_sequence_set:
continue
candidate = status_by_sequence[candidate_sequence]
diffs = [candidate[f"j{joint_no}_deg"] - teach_deg[joint_no - 1] for joint_no in range(1, 7)]
rms_error = math.sqrt(sum(value * value for value in diffs) / 6.0)
max_error = max(abs(value) for value in diffs)
score = (rms_error, max_error, abs(delta_cycles))
if best_candidate is None or score < best_candidate["score"]:
best_candidate = {
"score": score,
"delta_cycles": delta_cycles,
"record": candidate,
}
if best_candidate is None:
raise RuntimeError(f"Trigger {trigger_no} 未找到候选状态帧。")
best_status = best_candidate["record"]
row["best_status_frame_number"] = best_status["frame_number"]
row["best_status_time_relative_s"] = best_status["time_relative_s"]
row["best_status_sequence"] = best_status["sequence"]
row["best_status_timestamp"] = best_status["timestamp"]
row["best_status_delta_from_paired_cycles"] = best_candidate["delta_cycles"]
row["best_status_delta_from_trigger_sequence"] = best_status["sequence"] - trigger_frame["sequence"]
row["best_status_time_after_trigger_ms"] = (best_status["time_relative_s"] - trigger_frame["time_relative_s"]) * 1000.0
build_diff_row(
"best_status",
[best_status[f"j{joint_no}_deg"] for joint_no in range(1, 7)],
teach_deg,
row,
)
rows.append(row)
return rows
def write_csv(path: Path, rows: list[dict]) -> None:
"""把分析结果落成 UTF-8 CSV。"""
if not rows:
raise ValueError(f"No rows to write: {path}")
path.parent.mkdir(parents=True, exist_ok=True)
with path.open("w", newline="", encoding="utf-8") as handle:
writer = csv.DictWriter(handle, fieldnames=list(rows[0].keys()))
writer.writeheader()
writer.writerows(rows)
def build_summary(command_records: list[dict], trigger_status_rows: list[dict]) -> dict:
"""汇总命令序列偏移和状态反馈误差分布。"""
sequence_offsets = [row["trigger_sequence"] - row["paired_status_sequence"] for row in trigger_status_rows]
best_paired_cycle_offsets = [row["best_status_delta_from_paired_cycles"] for row in trigger_status_rows]
best_trigger_sequence_offsets = [row["best_status_delta_from_trigger_sequence"] for row in trigger_status_rows]
best_time_offsets = [row["best_status_time_after_trigger_ms"] for row in trigger_status_rows]
paired_max_errors = [row["paired_status_max_error_deg"] for row in trigger_status_rows]
best_max_errors = [row["best_status_max_error_deg"] for row in trigger_status_rows]
paired_axes = Counter(row["paired_status_max_error_axis"] for row in trigger_status_rows)
best_axes = Counter(row["best_status_max_error_axis"] for row in trigger_status_rows)
sequence_offset_counter = Counter()
trigger_frames = pick_trigger_first_high_frames(command_records)
status_pairs = {row["trigger_frame_number"]: row for row in trigger_status_rows}
for trigger_frame in trigger_frames:
sequence_offset_counter[trigger_frame["sequence"] - status_pairs[trigger_frame["frame_number"]]["paired_status_sequence"]] += 1
return {
"pcap_path": str(DEFAULT_PCAP),
"command_count": len(command_records),
"trigger_count": len(trigger_status_rows),
"command_minus_paired_status_sequence_counter": dict(sequence_offset_counter),
"paired_status_average_max_error_deg": sum(paired_max_errors) / len(paired_max_errors),
"paired_status_max_error_deg": max(paired_max_errors),
"paired_status_max_error_axis_counter": dict(paired_axes),
"best_status_average_max_error_deg": sum(best_max_errors) / len(best_max_errors),
"best_status_max_error_deg": max(best_max_errors),
"best_status_max_error_axis_counter": dict(best_axes),
"best_status_delta_from_paired_cycles_counter": dict(Counter(best_paired_cycle_offsets)),
"best_status_delta_from_trigger_sequence_counter": dict(Counter(best_trigger_sequence_offsets)),
"best_status_time_after_trigger_ms_min": min(best_time_offsets),
"best_status_time_after_trigger_ms_max": max(best_time_offsets),
"best_status_time_after_trigger_ms_avg": sum(best_time_offsets) / len(best_time_offsets),
"search_window_cycles": SEARCH_WINDOW_CYCLES,
}
def build_manual_compare_rows(trigger_status_rows: list[dict]) -> list[dict]:
"""整理成便于人工逐点核对的三时刻对照表。"""
rows: list[dict] = []
for row in trigger_status_rows:
rows.append(
{
"trigger_no": row["trigger_no"],
"waypoint_index": row["waypoint_index"],
"trigger_command_sequence": row["trigger_sequence"],
"trigger_command_frame": row["trigger_frame_number"],
"trigger_command_time_relative_s": row["trigger_time_relative_s"],
"trigger_current_status_sequence": row["paired_status_sequence"],
"trigger_current_status_frame": row["paired_status_frame_number"],
"trigger_current_status_time_relative_s": row["paired_status_time_relative_s"],
"command_leads_status_cycles": row["trigger_sequence"] - row["paired_status_sequence"],
"trigger_current_status_max_error_axis": row["paired_status_max_error_axis"],
"trigger_current_status_max_error_deg": row["paired_status_max_error_deg"],
"trigger_current_status_rms_error_deg": row["paired_status_rms_error_deg"],
"best_status_sequence": row["best_status_sequence"],
"best_status_frame": row["best_status_frame_number"],
"best_status_time_relative_s": row["best_status_time_relative_s"],
"best_status_delay_from_current_status_cycles": row["best_status_delta_from_paired_cycles"],
"best_status_delay_from_trigger_command_cycles": row["best_status_delta_from_trigger_sequence"],
"best_status_delay_from_trigger_command_ms": row["best_status_time_after_trigger_ms"],
"best_status_max_error_axis": row["best_status_max_error_axis"],
"best_status_max_error_deg": row["best_status_max_error_deg"],
"best_status_rms_error_deg": row["best_status_rms_error_deg"],
}
)
return rows
def main() -> None:
"""执行状态反馈提取、触发对齐和摘要落盘。"""
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
rows = load_udp_rows(DEFAULT_PCAP, DEFAULT_TSHARK)
command_records = decode_command_records(rows, client_ip="192.168.10.10", robot_ip="192.168.10.11")
status_records = decode_status_records(rows, client_ip="192.168.10.10", robot_ip="192.168.10.11")
trigger_frames = pick_trigger_first_high_frames(command_records)
shot_config = load_uttc_ms11_config()
trigger_status_rows = build_trigger_status_rows(trigger_frames, status_records, shot_config)
manual_compare_rows = build_manual_compare_rows(trigger_status_rows)
summary = build_summary(command_records, trigger_status_rows)
write_csv(OUTPUT_DIR / "2026042802-1_j519_status_feedback_all.csv", status_records)
write_csv(OUTPUT_DIR / "2026042802-1_trigger_status_feedback_vs_teach_points.csv", trigger_status_rows)
write_csv(OUTPUT_DIR / "2026042802-1_trigger_manual_compare.csv", manual_compare_rows)
(OUTPUT_DIR / "2026042802-1_status_feedback_summary.json").write_text(
json.dumps(summary, ensure_ascii=False, indent=2),
encoding="utf-8",
)
print(json.dumps(summary, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()

View File

@@ -1,250 +0,0 @@
#!/usr/bin/env python3
"""提取 2026042802-1 抓包中的真实 J519 发包,并对比 UTTC_MS11 示教点。"""
from __future__ import annotations
import csv
import json
import math
import struct
import subprocess
from collections import Counter
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
DEFAULT_PCAP = REPO_ROOT.parent / "Rvbust" / "uttc-20260428" / "2026042802-1.pcap"
DEFAULT_TSHARK = Path(r"D:\Zyx\Downloads\WiresharkPortable32\App\Wireshark\tshark.exe")
OUTPUT_DIR = REPO_ROOT / "analysis" / "2026042802-1"
CONFIG_PATH = REPO_ROOT / "Config" / "RobotConfig.json"
RUNTIME_DATA_DIR = REPO_ROOT / "Config" / "Data" / "UTTC_MS11"
def be_u32(data: bytes, offset: int) -> int:
"""按大端读取 4 字节无符号整数。"""
return struct.unpack(">I", data[offset : offset + 4])[0]
def be_u16(data: bytes, offset: int) -> int:
"""按大端读取 2 字节无符号整数。"""
return struct.unpack(">H", data[offset : offset + 2])[0]
def be_f32(data: bytes, offset: int) -> float:
"""按大端读取 4 字节浮点数。"""
return struct.unpack(">f", data[offset : offset + 4])[0]
def load_j519_command_rows(pcap: Path, tshark: Path) -> list[list[str]]:
"""只提取 UDP 60015 的原始字段,后续再按 IP 方向筛选真实下发命令。"""
command = [
str(tshark),
"-r",
str(pcap),
"-Y",
"udp.port==60015",
"-T",
"fields",
"-e",
"frame.number",
"-e",
"frame.time_relative",
"-e",
"ip.src",
"-e",
"udp.srcport",
"-e",
"ip.dst",
"-e",
"udp.dstport",
"-e",
"udp.payload",
]
output = subprocess.check_output(command, text=True, encoding="utf-8", errors="ignore")
rows: list[list[str]] = []
for line in output.splitlines():
if not line.strip():
continue
parts = line.split("\t")
if len(parts) >= 7:
rows.append(parts[:7])
return rows
def decode_command_records(rows: list[list[str]], client_ip: str, robot_ip: str) -> list[dict]:
"""把抓包中的 64B J519 命令帧解码成带 IO 信息的结构化记录。"""
records: list[dict] = []
for frame_no, time_rel, ip_src, _udp_src, ip_dst, _udp_dst, payload_hex in rows:
if ip_src != client_ip or ip_dst != robot_ip:
continue
payload = bytes.fromhex(payload_hex)
if len(payload) != 64:
continue
io_value = be_u16(payload, 0x18)
io_addrs = [bit + 1 for bit in range(16) if io_value & (1 << bit)]
targets = [be_f32(payload, 0x1C + index * 4) for index in range(9)]
records.append(
{
"frame_number": int(frame_no),
"time_relative_s": float(time_rel),
"sequence": be_u32(payload, 0x08),
"last_data": payload[0x0C],
"write_io_type": payload[0x13],
"write_io_index": be_u16(payload, 0x14),
"write_io_mask": be_u16(payload, 0x16),
"write_io_value": io_value,
"io_addrs": io_addrs,
"j1_deg": targets[0],
"j2_deg": targets[1],
"j3_deg": targets[2],
"j4_deg": targets[3],
"j5_deg": targets[4],
"j6_deg": targets[5],
"ext1_deg": targets[6],
"ext2_deg": targets[7],
"ext3_deg": targets[8],
}
)
return records
def pick_trigger_first_high_frames(records: list[dict]) -> list[dict]:
"""由于 io_keep_cycles=2,只记录每组高电平脉冲的第一帧。"""
trigger_frames: list[dict] = []
previous_high = False
for record in records:
current_high = record["write_io_value"] > 0
if current_high and not previous_high:
trigger_frames.append(record)
previous_high = current_high
return trigger_frames
def load_uttc_ms11_config() -> dict:
"""读取 UTTC_MS11 的示教点和触发配置。"""
config = json.loads(CONFIG_PATH.read_text(encoding="utf-8"))
return config["flying_shots"]["UTTC_MS11"]
def build_trigger_vs_teach_rows(trigger_frames: list[dict], shot_config: dict) -> list[dict]:
"""按 shot_flags 为 true 的 waypoint 顺序,对齐抓包触发帧和示教点。"""
rows: list[dict] = []
trigger_waypoint_indices = [index for index, flag in enumerate(shot_config["shot_flags"]) if flag]
for trigger_no, (frame, waypoint_index) in enumerate(zip(trigger_frames, trigger_waypoint_indices), start=1):
teach_rad = shot_config["traj_waypoints"][waypoint_index]
teach_deg = [math.degrees(value) for value in teach_rad]
actual_deg = [frame[f"j{joint_index}_deg"] for joint_index in range(1, 7)]
diffs = [actual_deg[index] - teach_deg[index] for index in range(6)]
abs_diffs = [abs(value) for value in diffs]
max_error = max(abs_diffs)
max_error_axis = f"J{abs_diffs.index(max_error) + 1}"
rms_error = math.sqrt(sum(value * value for value in diffs) / 6.0)
row = {
"trigger_no": trigger_no,
"waypoint_index": waypoint_index,
"frame_number": frame["frame_number"],
"sequence": frame["sequence"],
"time_relative_s": frame["time_relative_s"],
"write_io_value": frame["write_io_value"],
"io_addrs": frame["io_addrs"],
"config_addr": shot_config["addr"][waypoint_index],
"max_error_axis": max_error_axis,
"max_error_deg": max_error,
"rms_error_deg": rms_error,
}
for joint_index in range(6):
joint_no = joint_index + 1
row[f"j{joint_no}_actual_deg"] = actual_deg[joint_index]
row[f"j{joint_no}_teach_deg"] = teach_deg[joint_index]
row[f"diff_j{joint_no}_deg"] = diffs[joint_index]
rows.append(row)
return rows
def write_csv(path: Path, rows: list[dict]) -> None:
"""把分析结果落成 UTF-8 CSV,便于后续继续筛选和画图。"""
if not rows:
raise ValueError(f"No rows to write: {path}")
serializable_rows: list[dict] = []
for row in rows:
serializable_row: dict = {}
for key, value in row.items():
if isinstance(value, list):
serializable_row[key] = json.dumps(value, ensure_ascii=False)
else:
serializable_row[key] = value
serializable_rows.append(serializable_row)
path.parent.mkdir(parents=True, exist_ok=True)
with path.open("w", newline="", encoding="utf-8") as handle:
writer = csv.DictWriter(handle, fieldnames=list(serializable_rows[0].keys()))
writer.writeheader()
writer.writerows(serializable_rows)
def build_summary(records: list[dict], trigger_rows: list[dict]) -> dict:
"""汇总本次分析关心的导出状态和误差统计。"""
max_errors = [float(row["max_error_deg"]) for row in trigger_rows]
rms_errors = [float(row["rms_error_deg"]) for row in trigger_rows]
axis_counter = Counter(str(row["max_error_axis"]) for row in trigger_rows)
order_only_addr_mismatch = 0
real_addr_mismatch = 0
for row in trigger_rows:
io_addrs = list(row["io_addrs"])
config_addr = list(row["config_addr"])
if io_addrs != config_addr:
if sorted(io_addrs) == sorted(config_addr):
order_only_addr_mismatch += 1
else:
real_addr_mismatch += 1
return {
"pcap_path": str(DEFAULT_PCAP),
"all_command_count": len(records),
"trigger_count": len(trigger_rows),
"existing_runtime_actual_send_exists": (RUNTIME_DATA_DIR / "ActualSendJointTraj.txt").exists(),
"existing_runtime_actual_send_has_io_columns": False,
"existing_shot_events_exists": (RUNTIME_DATA_DIR / "ShotEvents.json").exists(),
"pcap_specific_combined_export_preexisting": False,
"average_max_error_deg": sum(max_errors) / len(max_errors),
"max_error_deg": max(max_errors),
"average_rms_error_deg": sum(rms_errors) / len(rms_errors),
"max_error_axis_counter": dict(axis_counter),
"order_only_addr_mismatch_count": order_only_addr_mismatch,
"real_addr_mismatch_count": real_addr_mismatch,
}
def main() -> None:
"""执行抓包提取、示教点对齐、CSV 导出和摘要落盘。"""
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
rows = load_j519_command_rows(DEFAULT_PCAP, DEFAULT_TSHARK)
records = decode_command_records(rows, client_ip="192.168.10.10", robot_ip="192.168.10.11")
trigger_frames = pick_trigger_first_high_frames(records)
shot_config = load_uttc_ms11_config()
trigger_rows = build_trigger_vs_teach_rows(trigger_frames, shot_config)
summary = build_summary(records, trigger_rows)
write_csv(OUTPUT_DIR / "2026042802-1_j519_actual_send_all_with_io.csv", records)
write_csv(OUTPUT_DIR / "2026042802-1_j519_trigger_frames.csv", trigger_frames)
write_csv(OUTPUT_DIR / "2026042802-1_trigger_vs_teach_points.csv", trigger_rows)
(OUTPUT_DIR / "2026042802-1_analysis_summary.json").write_text(
json.dumps(summary, ensure_ascii=False, indent=2),
encoding="utf-8",
)
print(json.dumps(summary, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()

View File

@@ -1,137 +0,0 @@
#!/usr/bin/env python3
"""生成 2026042802-1 抓包中触发偏移 6/7/8 周期的对照表。"""
from __future__ import annotations
import csv
import json
import math
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
OUTPUT_DIR = REPO_ROOT / "analysis" / "2026042802-1"
CONFIG_PATH = REPO_ROOT / "Config" / "RobotConfig.json"
ACTUAL_SEND_CSV = OUTPUT_DIR / "2026042802-1_j519_actual_send_all_with_io.csv"
TRIGGER_CSV = OUTPUT_DIR / "2026042802-1_j519_trigger_frames.csv"
OUTPUT_CSV = OUTPUT_DIR / "2026042802-1_trigger_offset_6_7_8_compare.csv"
OUTPUT_JSON = OUTPUT_DIR / "2026042802-1_trigger_offset_6_7_8_summary.json"
def load_rows(path: Path) -> list[dict]:
with path.open(encoding="utf-8") as handle:
return list(csv.DictReader(handle))
def to_float_list(record: dict, prefix: str = "j") -> list[float]:
return [float(record[f"{prefix}{index}_deg"]) for index in range(1, 7)]
def compute_diff_metrics(actual_deg: list[float], teach_deg: list[float]) -> tuple[list[float], float, float, str]:
diffs = [actual_deg[index] - teach_deg[index] for index in range(6)]
abs_diffs = [abs(value) for value in diffs]
max_error = max(abs_diffs)
rms_error = math.sqrt(sum(value * value for value in diffs) / 6.0)
max_axis = f"J{abs_diffs.index(max_error) + 1}"
return diffs, max_error, rms_error, max_axis
def main() -> None:
actual_rows = load_rows(ACTUAL_SEND_CSV)
trigger_rows = load_rows(TRIGGER_CSV)
config = json.loads(CONFIG_PATH.read_text(encoding="utf-8"))["flying_shots"]["UTTC_MS11"]
trigger_waypoint_indices = [index for index, flag in enumerate(config["shot_flags"]) if flag]
actual_index_by_frame = {int(row["frame_number"]): row for row in actual_rows}
actual_order_by_frame = {int(row["frame_number"]): idx for idx, row in enumerate(actual_rows)}
output_rows: list[dict] = []
offset_win_counts = {6: 0, 7: 0, 8: 0}
for trigger_no, (trigger_row, waypoint_index) in enumerate(zip(trigger_rows, trigger_waypoint_indices), start=1):
trigger_frame = int(trigger_row["frame_number"])
trigger_order = actual_order_by_frame[trigger_frame]
teach_deg = [math.degrees(value) for value in config["traj_waypoints"][waypoint_index]]
window_start = max(0, trigger_order - 20)
window_end = min(len(actual_rows) - 1, trigger_order + 20)
best_order = trigger_order
best_rms = float("inf")
best_max = float("inf")
for candidate_order in range(window_start, window_end + 1):
candidate = actual_rows[candidate_order]
_, max_error, rms_error, _ = compute_diff_metrics(to_float_list(candidate), teach_deg)
score = (rms_error, max_error, abs(candidate_order - trigger_order))
if (best_rms, best_max, abs(best_order - trigger_order)) > score:
best_order = candidate_order
best_rms = rms_error
best_max = max_error
row = {
"trigger_no": trigger_no,
"waypoint_index": waypoint_index,
"best_sample_order": best_order,
"best_frame_number": int(actual_rows[best_order]["frame_number"]),
"best_sequence": int(actual_rows[best_order]["sequence"]),
"best_time_relative_s": float(actual_rows[best_order]["time_relative_s"]),
}
for joint_index in range(6):
row[f"teach_j{joint_index + 1}_deg"] = teach_deg[joint_index]
for offset in (6, 7, 8):
target_order = min(len(actual_rows) - 1, best_order + offset)
target = actual_rows[target_order]
actual_deg = to_float_list(target)
diffs, max_error, rms_error, max_axis = compute_diff_metrics(actual_deg, teach_deg)
row[f"offset_{offset}_frame_number"] = int(target["frame_number"])
row[f"offset_{offset}_sequence"] = int(target["sequence"])
row[f"offset_{offset}_time_relative_s"] = float(target["time_relative_s"])
row[f"offset_{offset}_max_error_axis"] = max_axis
row[f"offset_{offset}_max_error_deg"] = max_error
row[f"offset_{offset}_rms_error_deg"] = rms_error
row[f"offset_{offset}_delta_from_best_ms"] = (
float(target["time_relative_s"]) - float(actual_rows[best_order]["time_relative_s"])
) * 1000.0
for joint_index in range(6):
joint_no = joint_index + 1
row[f"offset_{offset}_j{joint_no}_actual_deg"] = actual_deg[joint_index]
row[f"offset_{offset}_diff_j{joint_no}_deg"] = diffs[joint_index]
best_offset = min(
(6, 7, 8),
key=lambda offset: (
row[f"offset_{offset}_rms_error_deg"],
row[f"offset_{offset}_max_error_deg"],
),
)
row["best_of_6_7_8_offset"] = best_offset
offset_win_counts[best_offset] += 1
output_rows.append(row)
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
with OUTPUT_CSV.open("w", newline="", encoding="utf-8") as handle:
writer = csv.DictWriter(handle, fieldnames=list(output_rows[0].keys()))
writer.writeheader()
writer.writerows(output_rows)
summary = {
"rows": len(output_rows),
"best_offset_win_counts": offset_win_counts,
"average_max_error_deg": {
str(offset): sum(row[f"offset_{offset}_max_error_deg"] for row in output_rows) / len(output_rows)
for offset in (6, 7, 8)
},
"average_rms_error_deg": {
str(offset): sum(row[f"offset_{offset}_rms_error_deg"] for row in output_rows) / len(output_rows)
for offset in (6, 7, 8)
},
}
OUTPUT_JSON.write_text(json.dumps(summary, ensure_ascii=False, indent=2), encoding="utf-8")
print(json.dumps(summary, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()

View File

@@ -1,200 +0,0 @@
#!/usr/bin/env python3
"""读取 FANUC J519 允许速度/加速度/jerk 上限表。
本脚本只做 UDP packet type=3 查询,不启动运行时服务,也不写入机器人参数。
响应中的 limit 值保留为控制器原始单位:速度 deg/s、加速度 deg/s^2、jerk deg/s^3。
"""
from __future__ import annotations
import argparse
import csv
import socket
import struct
import sys
from dataclasses import dataclass
from pathlib import Path
LIMIT_TYPES = {
0: ("velocity", "deg/s"),
1: ("acceleration", "deg/s^2"),
2: ("jerk", "deg/s^3"),
}
@dataclass(frozen=True)
class LimitTable:
"""保存单个轴、单种限制类型的 20 档 no-payload / max-payload 上限表。"""
axis: int
limit_type: int
vmax_mm_s: int
intermediate_check_s: int
no_payload: tuple[float, ...]
max_payload: tuple[float, ...]
def pack_request(axis: int, limit_type: int) -> bytes:
"""按手册 Table 5 封装允许上限表查询请求。"""
return struct.pack(">IIII", 3, 1, axis, limit_type)
def parse_response(packet: bytes, expected_axis: int, expected_type: int) -> LimitTable:
"""按手册 Table 6 解析允许上限表响应。"""
if len(packet) != 184:
raise ValueError(f"响应长度应为 184 字节,实际为 {len(packet)} 字节。")
packet_type, version, axis, limit_type, vmax, check_time = struct.unpack_from(">IIIIII", packet, 0)
if packet_type != 3 or version != 1:
raise ValueError(f"响应头不正确:packet_type={packet_type}, version={version}。")
if axis != expected_axis or limit_type != expected_type:
raise ValueError(
f"响应与请求不匹配:期望 axis={expected_axis}, type={expected_type},"
f"实际 axis={axis}, type={limit_type}。"
)
values = struct.unpack_from(">40f", packet, 24)
return LimitTable(
axis=axis,
limit_type=limit_type,
vmax_mm_s=vmax,
intermediate_check_s=check_time,
no_payload=tuple(values[:20]),
max_payload=tuple(values[20:]),
)
def query_table(sock: socket.socket, robot_ip: str, port: int, axis: int, limit_type: int) -> LimitTable:
"""发送单个查询请求并等待对应响应。"""
sock.sendto(pack_request(axis, limit_type), (robot_ip, port))
packet, address = sock.recvfrom(2048)
if address[0] != robot_ip:
raise ValueError(f"收到非目标机器人响应:{address[0]}:{address[1]}。")
return parse_response(packet, axis, limit_type)
def iter_requested_tables(
robot_ip: str,
port: int,
timeout_s: float,
axes: range,
limit_types: tuple[int, ...],
) -> list[LimitTable]:
"""按轴和限制类型顺序读取所有上限表。"""
tables: list[LimitTable] = []
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
sock.settimeout(timeout_s)
for axis in axes:
for limit_type in limit_types:
tables.append(query_table(sock, robot_ip, port, axis, limit_type))
return tables
def write_csv(path: Path, tables: list[LimitTable]) -> None:
"""把 20 档速度区间和两套 payload 表写成扁平 CSV。"""
with path.open("w", newline="", encoding="utf-8") as file:
writer = csv.writer(file)
writer.writerow(
[
"axis",
"limit_type",
"unit",
"vmax_mm_s",
"intermediate_check_s",
"speed_bin_index",
"speed_bin_upper_mm_s",
"no_payload_limit",
"max_payload_limit",
]
)
for table in tables:
limit_name, unit = LIMIT_TYPES[table.limit_type]
for index, (no_payload, max_payload) in enumerate(zip(table.no_payload, table.max_payload), start=1):
writer.writerow(
[
table.axis,
limit_name,
unit,
table.vmax_mm_s,
table.intermediate_check_s,
index,
table.vmax_mm_s * index / 20.0,
f"{no_payload:.9g}",
f"{max_payload:.9g}",
]
)
def print_summary(tables: list[LimitTable]) -> None:
"""打印每张表的首末档,便于现场快速确认返回值是否正常。"""
for table in tables:
limit_name, unit = LIMIT_TYPES[table.limit_type]
print(
f"axis={table.axis} type={limit_name:<12} unit={unit:<8} "
f"vmax={table.vmax_mm_s}mm/s check={table.intermediate_check_s}s "
f"no_payload[1]={table.no_payload[0]:.6g} no_payload[20]={table.no_payload[-1]:.6g} "
f"max_payload[1]={table.max_payload[0]:.6g} max_payload[20]={table.max_payload[-1]:.6g}"
)
def parse_args() -> argparse.Namespace:
"""解析命令行参数。"""
parser = argparse.ArgumentParser(
description="Read FANUC J519 packet type=3 allowable upper limit tables.",
)
parser.add_argument("robot_ip", help="机器人控制柜 IP,例如 192.168.10.11。")
parser.add_argument("--port", type=int, default=60015, help="J519 UDP 端口,默认 60015。")
parser.add_argument("--timeout", type=float, default=2.0, help="单次响应超时秒数,默认 2.0。")
parser.add_argument("--axis-start", type=int, default=1, help="起始轴号,默认 1。")
parser.add_argument("--axis-end", type=int, default=9, help="结束轴号,默认 9。")
parser.add_argument(
"--types",
default="0,1,2",
help="限制类型列表:0=velocity, 1=acceleration, 2=jerk;默认 0,1,2。",
)
parser.add_argument(
"--csv",
type=Path,
default=Path("analysis/fanuc_allowable_limit_tables.csv"),
help="CSV 输出路径。",
)
return parser.parse_args()
def main() -> int:
"""脚本入口。"""
args = parse_args()
limit_types = tuple(int(item.strip()) for item in args.types.split(",") if item.strip())
unknown_types = [item for item in limit_types if item not in LIMIT_TYPES]
if unknown_types:
print(f"未知限制类型:{unknown_types}", file=sys.stderr)
return 2
if not 1 <= args.axis_start <= args.axis_end <= 9:
print("轴范围必须满足 1 <= axis-start <= axis-end <= 9。", file=sys.stderr)
return 2
tables = iter_requested_tables(
args.robot_ip,
args.port,
args.timeout,
range(args.axis_start, args.axis_end + 1),
limit_types,
)
args.csv.parent.mkdir(parents=True, exist_ok=True)
write_csv(args.csv, tables)
print_summary(tables)
print(f"\nCSV 已写入:{args.csv}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

Binary file not shown.

View File

@@ -1,526 +0,0 @@
# 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

@@ -1,125 +0,0 @@
# 相机触发实际偏差抓包触发点记录
## 数据来源
- 抓包文件:`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

@@ -1,460 +0,0 @@
# 飞拍任务 `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

@@ -1,657 +0,0 @@
<!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

@@ -1,151 +0,0 @@
# 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` 与旧抓包主段的剩余时长差异已经很小
- 当前判断:这点差异可以忽略
后续除非现场再次提供新的反证,否则本结论作为当前项目实现与验收的记录保留。

View File

@@ -1,6 +1,5 @@
using Flyshot.Core.Config; using Flyshot.Core.Config;
using Flyshot.Core.Domain; using Flyshot.Core.Domain;
using Flyshot.Core.Planning.Sampling;
using Flyshot.Runtime.Common; using Flyshot.Runtime.Common;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -12,7 +11,6 @@ namespace Flyshot.ControllerClientCompat;
public sealed class ControllerClientCompatService : IControllerClientCompatService public sealed class ControllerClientCompatService : IControllerClientCompatService
{ {
private readonly object _stateLock = new(); private readonly object _stateLock = new();
private readonly object _motionLock = new();
private readonly Dictionary<string, ControllerClientCompatUploadedTrajectory> _uploadedTrajectories = new(StringComparer.Ordinal); private readonly Dictionary<string, ControllerClientCompatUploadedTrajectory> _uploadedTrajectories = new(StringComparer.Ordinal);
private readonly ControllerClientCompatOptions _options; private readonly ControllerClientCompatOptions _options;
private readonly ControllerClientCompatRobotCatalog _robotCatalog; private readonly ControllerClientCompatRobotCatalog _robotCatalog;
@@ -135,8 +133,6 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
_activeRobotProfile = robotProfile; _activeRobotProfile = robotProfile;
_uploadedTrajectories.Clear(); _uploadedTrajectories.Clear();
_runtime.ResetRobot(robotProfile, robotName); _runtime.ResetRobot(robotProfile, robotName);
// RobotConfig.json 的 speed_ratio 是启动默认值;在线 set_speedRatio 仍可覆盖后续执行。
_runtime.SetSpeedRatio(robotSettings.SpeedRatio);
_robotSettings = robotSettings; _robotSettings = robotSettings;
// 从持久化存储恢复该机器人名下之前已上传的轨迹。 // 从持久化存储恢复该机器人名下之前已上传的轨迹。
@@ -148,12 +144,11 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
} }
_logger?.LogInformation( _logger?.LogInformation(
"SetUpRobot 完成: robotName={RobotName}, dof={Dof}, accLimit={AccLimit}, jerkLimit={JerkLimit}, speedRatio={SpeedRatio}, 恢复轨迹数={TrajCount}", "SetUpRobot 完成: robotName={RobotName}, dof={Dof}, accLimit={AccLimit}, jerkLimit={JerkLimit}, 恢复轨迹数={TrajCount}",
robotName, robotName,
robotProfile.DegreesOfFreedom, robotProfile.DegreesOfFreedom,
robotSettings.AccLimitScale, robotSettings.AccLimitScale,
robotSettings.JerkLimitScale, robotSettings.JerkLimitScale,
robotSettings.SpeedRatio,
_uploadedTrajectories.Count); _uploadedTrajectories.Count);
} }
@@ -198,9 +193,8 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
lock (_stateLock) lock (_stateLock)
{ {
EnsureRobotSetup(); EnsureRobotSetup();
_runtime.SetActiveController(sim);
} }
_runtime.SetActiveController(sim);
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -216,10 +210,9 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
lock (_stateLock) lock (_stateLock)
{ {
EnsureRobotSetup(); EnsureRobotSetup();
_runtime.Connect(robotIp);
} }
_runtime.Connect(robotIp);
_logger?.LogInformation("Connect 完成: robotIp={RobotIp}", robotIp); _logger?.LogInformation("Connect 完成: robotIp={RobotIp}", robotIp);
} }
@@ -229,9 +222,8 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
lock (_stateLock) lock (_stateLock)
{ {
EnsureRobotSetup(); EnsureRobotSetup();
_runtime.Disconnect();
} }
_runtime.Disconnect();
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -241,8 +233,8 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
lock (_stateLock) lock (_stateLock)
{ {
EnsureRobotSetup(); EnsureRobotSetup();
_runtime.EnableRobot(bufferSize);
} }
_runtime.EnableRobot(bufferSize);
_logger?.LogInformation("EnableRobot 完成"); _logger?.LogInformation("EnableRobot 完成");
} }
@@ -253,10 +245,8 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
lock (_stateLock) lock (_stateLock)
{ {
EnsureRobotSetup(); EnsureRobotSetup();
_runtime.DisableRobot();
} }
// DisableRobot 是中断/控制命令,不能被长时间运动串行锁挡住。
_runtime.DisableRobot();
_logger?.LogInformation("DisableRobot 完成"); _logger?.LogInformation("DisableRobot 完成");
} }
@@ -267,10 +257,8 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
lock (_stateLock) lock (_stateLock)
{ {
EnsureRobotSetup(); EnsureRobotSetup();
_runtime.StopMove();
} }
// StopMove 必须能在运动执行期间直接进入 runtime 取消发送任务。
_runtime.StopMove();
_logger?.LogInformation("StopMove 完成"); _logger?.LogInformation("StopMove 完成");
} }
@@ -280,31 +268,14 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
return _runtime.GetSnapshot(); return _runtime.GetSnapshot();
} }
/// <inheritdoc />
public ControllerClientStatusSnapshotMetadata GetStatusSnapshotMetadata()
{
lock (_stateLock)
{
var isSetup = _activeRobotProfile is not null;
return new ControllerClientStatusSnapshotMetadata(
isSetup,
isSetup ? _configuredRobotName : null,
isSetup ? _activeRobotProfile!.DegreesOfFreedom : 0,
isSetup ? _uploadedTrajectories.Keys.ToArray() : Array.Empty<string>(),
GetServerVersion(),
GetClientVersion());
}
}
/// <inheritdoc /> /// <inheritdoc />
public double GetSpeedRatio() public double GetSpeedRatio()
{ {
lock (_stateLock) lock (_stateLock)
{ {
EnsureRobotSetup(); EnsureRobotSetup();
return _runtime.GetSpeedRatio();
} }
return _runtime.GetSpeedRatio();
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -313,9 +284,8 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
lock (_stateLock) lock (_stateLock)
{ {
EnsureRobotSetup(); EnsureRobotSetup();
_runtime.SetSpeedRatio(ratio);
} }
_runtime.SetSpeedRatio(ratio);
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -324,9 +294,8 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
lock (_stateLock) lock (_stateLock)
{ {
EnsureRobotSetup(); EnsureRobotSetup();
_runtime.SetIo(port, value, ioType);
} }
_runtime.SetIo(port, value, ioType);
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -335,9 +304,8 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
lock (_stateLock) lock (_stateLock)
{ {
EnsureRobotSetup(); EnsureRobotSetup();
return _runtime.GetIo(port, ioType);
} }
return _runtime.GetIo(port, ioType);
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -369,9 +337,8 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
lock (_stateLock) lock (_stateLock)
{ {
EnsureRobotSetup(); EnsureRobotSetup();
_runtime.SetTcp(x, y, z);
} }
_runtime.SetTcp(x, y, z);
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -380,9 +347,8 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
lock (_stateLock) lock (_stateLock)
{ {
EnsureRobotSetup(); EnsureRobotSetup();
return _runtime.GetTcp();
} }
return _runtime.GetTcp();
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -391,9 +357,8 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
lock (_stateLock) lock (_stateLock)
{ {
EnsureRobotSetup(); EnsureRobotSetup();
return _runtime.GetJointPositions();
} }
return _runtime.GetJointPositions();
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -404,44 +369,16 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
_logger?.LogInformation("MoveJoint 开始: 目标关节数={JointCount}", jointPositions.Count); _logger?.LogInformation("MoveJoint 开始: 目标关节数={JointCount}", jointPositions.Count);
_logger?.LogDebug("MoveJoint 目标关节: {Joints}", string.Join(", ", jointPositions.Select(j => j.ToString("F4")))); _logger?.LogDebug("MoveJoint 目标关节: {Joints}", string.Join(", ", jointPositions.Select(j => j.ToString("F4"))));
RobotProfile robot;
lock (_stateLock) lock (_stateLock)
{ {
robot = RequireActiveRobot(); var robot = RequireActiveRobot();
EnsureRuntimeEnabled(); EnsureRuntimeEnabled();
} ExecuteMoveJointAndWaitLocked(robot, jointPositions, "MoveJoint");
lock (_motionLock)
{
ExecuteMoveJointAndWait(robot, jointPositions, "MoveJoint");
} }
_logger?.LogInformation("MoveJoint 完成"); _logger?.LogInformation("MoveJoint 完成");
} }
/// <inheritdoc />
public void MovePose(IReadOnlyList<double> pose)
{
ArgumentNullException.ThrowIfNull(pose);
_logger?.LogInformation("MovePose 开始: 目标位姿维度={PoseCount}", pose.Count);
_logger?.LogDebug("MovePose 目标位姿: {Pose}", string.Join(", ", pose.Select(v => v.ToString("F4"))));
RobotProfile robot;
lock (_stateLock)
{
robot = RequireActiveRobot();
EnsureRuntimeEnabled();
}
lock (_motionLock)
{
ExecuteMovePoseAndWait(robot, pose, "MovePose");
}
_logger?.LogInformation("MovePose 完成");
}
/// <inheritdoc /> /// <inheritdoc />
public void ExecuteTrajectory(IReadOnlyList<IReadOnlyList<double>> waypoints, TrajectoryExecutionOptions? options = null) public void ExecuteTrajectory(IReadOnlyList<IReadOnlyList<double>> waypoints, TrajectoryExecutionOptions? options = null)
{ {
@@ -457,29 +394,21 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
_logger?.LogDebug("ExecuteTrajectory 路点详情: {Waypoints}", _logger?.LogDebug("ExecuteTrajectory 路点详情: {Waypoints}",
string.Join(" | ", waypoints.Select(wp => $"[{string.Join(", ", wp.Select(j => j.ToString("F4")))}]"))); string.Join(" | ", waypoints.Select(wp => $"[{string.Join(", ", wp.Select(j => j.ToString("F4")))}]")));
RobotProfile robot;
CompatibilityRobotSettings settings;
lock (_stateLock) lock (_stateLock)
{ {
robot = RequireActiveRobot(); var robot = RequireActiveRobot();
EnsureRuntimeEnabled(); EnsureRuntimeEnabled();
settings = RequireRobotSettings();
}
lock (_motionLock)
{
// 普通轨迹必须按调用方指定 method 规划,再把规划结果交给运行时执行。 // 普通轨迹必须按调用方指定 method 规划,再把规划结果交给运行时执行。
var planningSpeedScale = settings.PlanningSpeedScale; var planningSpeedScale = RequireRobotSettings().PlanningSpeedScale;
var speedRatio = _runtime.GetSnapshot().SpeedRatio; var bundle = _trajectoryOrchestrator.PlanOrdinaryTrajectory(robot, waypoints, options, planningSpeedScale);
var bundle = _trajectoryOrchestrator.PlanOrdinaryTrajectory(robot, waypoints, options, planningSpeedScale, speedRatio);
_logger?.LogInformation( _logger?.LogInformation(
"ExecuteTrajectory 规划完成: method={Method}, 时长={Duration}s, 有效={IsValid}, 采样点数={SampleCount}, planningSpeedScale={PlanningSpeedScale}, speedRatio={SpeedRatio}", "ExecuteTrajectory 规划完成: method={Method}, 时长={Duration}s, 有效={IsValid}, 采样点数={SampleCount}, planningSpeedScale={PlanningSpeedScale}",
bundle.Result.Method, bundle.Result.Method,
bundle.Result.Duration.TotalSeconds, bundle.Result.Duration.TotalSeconds,
bundle.Result.IsValid, bundle.Result.IsValid,
bundle.Result.DenseJointTrajectory?.Count ?? 0, bundle.Result.DenseJointTrajectory?.Count ?? 0,
planningSpeedScale, planningSpeedScale);
speedRatio);
var finalJointPositions = bundle.PlannedTrajectory.PlannedWaypoints[^1].Positions; var finalJointPositions = bundle.PlannedTrajectory.PlannedWaypoints[^1].Positions;
_runtime.ExecuteTrajectory(bundle.Result, finalJointPositions); _runtime.ExecuteTrajectory(bundle.Result, finalJointPositions);
} }
@@ -493,9 +422,8 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
lock (_stateLock) lock (_stateLock)
{ {
EnsureRobotSetup(); EnsureRobotSetup();
return _runtime.GetPose();
} }
return _runtime.GetPose();
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -509,20 +437,16 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
trajectory.Waypoints.Count, trajectory.Waypoints.Count,
trajectory.ShotFlags.Count(static f => f)); trajectory.ShotFlags.Count(static f => f));
string robotName;
CompatibilityRobotSettings settings;
lock (_stateLock) lock (_stateLock)
{ {
EnsureRuntimeEnabled(); EnsureRuntimeEnabled();
_uploadedTrajectories[trajectory.Name] = trajectory; _uploadedTrajectories[trajectory.Name] = trajectory;
robotName = _configuredRobotName ?? throw new InvalidOperationException("Robot has not been setup."); var robotName = _configuredRobotName ?? throw new InvalidOperationException("Robot has not been setup.");
settings = _robotSettings ?? CreateDefaultRobotSettings(); var settings = _robotSettings ?? CreateDefaultRobotSettings();
_trajectoryStore.Save(robotName, settings, trajectory);
} }
// RobotConfig.json 持久化是文件 I/O,放在状态锁外,避免状态页轮询被磁盘写入拖住。
_trajectoryStore.Save(robotName, settings, trajectory);
_logger?.LogInformation("UploadTrajectory 完成: name={Name}", trajectory.Name); _logger?.LogInformation("UploadTrajectory 完成: name={Name}", trajectory.Name);
} }
@@ -548,80 +472,47 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
"ExecuteTrajectoryByName 开始: name={Name}, method={Method}, moveToStart={MoveToStart}, useCache={UseCache}, wait={Wait}", "ExecuteTrajectoryByName 开始: name={Name}, method={Method}, moveToStart={MoveToStart}, useCache={UseCache}, wait={Wait}",
name, options.Method, options.MoveToStart, options.UseCache, options.Wait); name, options.Method, options.MoveToStart, options.UseCache, options.Wait);
RobotProfile robot;
CompatibilityRobotSettings settings;
ControllerClientCompatUploadedTrajectory trajectory;
lock (_stateLock) lock (_stateLock)
{ {
robot = RequireActiveRobot(); var robot = RequireActiveRobot();
EnsureRuntimeEnabled(); EnsureRuntimeEnabled();
if (!_uploadedTrajectories.TryGetValue(name, out var uploadedTrajectory)) if (!_uploadedTrajectories.TryGetValue(name, out var trajectory))
{ {
_logger?.LogWarning("ExecuteTrajectoryByName 失败: 轨迹不存在 name={Name}", name); _logger?.LogWarning("ExecuteTrajectoryByName 失败: 轨迹不存在 name={Name}", name);
throw new InvalidOperationException("FlyShot trajectory does not exist."); throw new InvalidOperationException("FlyShot trajectory does not exist.");
} }
// 飞拍执行只拿上传轨迹的瞬时副本,后续规划/导出都不再依赖字典锁。
trajectory = CloneUploadedTrajectory(uploadedTrajectory);
if (trajectory.Waypoints.Count == 0) if (trajectory.Waypoints.Count == 0)
{ {
_logger?.LogWarning("ExecuteTrajectoryByName 失败: 轨迹无路点 name={Name}", name); _logger?.LogWarning("ExecuteTrajectoryByName 失败: 轨迹无路点 name={Name}", name);
throw new InvalidOperationException("FlyShot trajectory contains no waypoints."); throw new InvalidOperationException("FlyShot trajectory contains no waypoints.");
} }
settings = RequireRobotSettings();
}
lock (_motionLock)
{
// 已上传飞拍轨迹必须按调用方指定 method 生成 shot timeline 后再交给运行时。 // 已上传飞拍轨迹必须按调用方指定 method 生成 shot timeline 后再交给运行时。
var speedRatio = _runtime.GetSnapshot().SpeedRatio; var settings = RequireRobotSettings();
var bundle = _trajectoryOrchestrator.PlanUploadedFlyshot(robot, trajectory, options, settings, settings.PlanningSpeedScale, speedRatio); var bundle = _trajectoryOrchestrator.PlanUploadedFlyshot(robot, trajectory, options, settings, settings.PlanningSpeedScale);
bundle = PrepareFlyshotExecutionBundle(robot, bundle, speedRatio); ExportFlyshotArtifactsIfRequested(name, options.SaveTrajectory, robot, bundle);
ExportFlyshotArtifactsIfRequested(
name,
options.SaveTrajectory,
robot,
trajectory,
options,
settings,
bundle,
settings.PlanningSpeedScale,
speedRatio);
_logger?.LogInformation( _logger?.LogInformation(
"ExecuteTrajectoryByName 规划完成: name={Name}, method={Method}, 时长={Duration}s, 触发事件数={TriggerCount}, 使用缓存={UsedCache}, planningSpeedScale={PlanningSpeedScale}, speedRatio={SpeedRatio}", "ExecuteTrajectoryByName 规划完成: name={Name}, method={Method}, 时长={Duration}s, 触发事件数={TriggerCount}, 使用缓存={UsedCache}, planningSpeedScale={PlanningSpeedScale}",
name, name,
bundle.Result.Method, bundle.Result.Method,
bundle.Result.Duration.TotalSeconds, bundle.Result.Duration.TotalSeconds,
bundle.Result.TriggerTimeline.Count, bundle.Result.TriggerTimeline.Count,
bundle.Result.UsedCache, bundle.Result.UsedCache,
settings.PlanningSpeedScale, settings.PlanningSpeedScale);
speedRatio);
if (options.MoveToStart) if (options.MoveToStart)
{ {
_logger?.LogInformation("ExecuteTrajectoryByName 先移动到起点"); _logger?.LogInformation("ExecuteTrajectoryByName 先移动到起点");
ExecuteMoveJointAndWait(robot, bundle.PlannedTrajectory.PlannedWaypoints[0].Positions, "ExecuteTrajectoryByName.move_to_start"); ExecuteMoveJointAndWaitLocked(robot, bundle.PlannedTrajectory.PlannedWaypoints[0].Positions, "ExecuteTrajectoryByName.move_to_start");
EnsureFeedbackNearFlyshotStart(bundle.PlannedTrajectory.PlannedWaypoints[0].Positions, name);
}
else
{
// 正式飞拍前必须确认机器人反馈已经在轨迹起点附近,避免 J519 目标突变。
if (!IsFeedbackNearFlyshotStart(bundle.PlannedTrajectory.PlannedWaypoints[0].Positions, name))
{
return;
}
} }
var finalJointPositions = bundle.PlannedTrajectory.PlannedWaypoints[^1].Positions; var finalJointPositions = bundle.PlannedTrajectory.PlannedWaypoints[^1].Positions;
_runtime.ExecuteTrajectory(bundle.Result, finalJointPositions); _runtime.ExecuteTrajectory(bundle.Result, finalJointPositions);
if (options.Wait) if (options.Wait)
{ {
var executionDuration = bundle.PreparedExecution is null WaitForRuntimeMotionComplete("ExecuteTrajectoryByName.flyshot", bundle.Result.Duration);
? bundle.Result.Duration
: TimeSpan.FromSeconds(bundle.PreparedExecution.FinalDurationSeconds);
WaitForRuntimeMotionComplete("ExecuteTrajectoryByName.flyshot", executionDuration);
} }
} }
@@ -634,7 +525,7 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
/// <param name="robot">当前机器人模型。</param> /// <param name="robot">当前机器人模型。</param>
/// <param name="targetJointPositions">目标关节位置,单位为弧度。</param> /// <param name="targetJointPositions">目标关节位置,单位为弧度。</param>
/// <param name="operationName">用于日志和超时异常的操作名。</param> /// <param name="operationName">用于日志和超时异常的操作名。</param>
private void ExecuteMoveJointAndWait(RobotProfile robot, IReadOnlyList<double> targetJointPositions, string operationName) private void ExecuteMoveJointAndWaitLocked(RobotProfile robot, IReadOnlyList<double> targetJointPositions, string operationName)
{ {
var currentJointPositions = _runtime.GetJointPositions(); var currentJointPositions = _runtime.GetJointPositions();
EnsureJointVector(currentJointPositions, robot.DegreesOfFreedom, nameof(currentJointPositions)); EnsureJointVector(currentJointPositions, robot.DegreesOfFreedom, nameof(currentJointPositions));
@@ -653,57 +544,6 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
WaitForRuntimeMotionComplete(operationName, moveResult.Duration); WaitForRuntimeMotionComplete(operationName, moveResult.Duration);
} }
/// <summary>
/// 从当前 TCP 位姿生成临时直角 PTP 稠密轨迹并阻塞等待运行时完成。
/// </summary>
/// <param name="robot">当前机器人模型,用于取得 J519 伺服周期。</param>
/// <param name="targetPose">目标位姿 [x,y,z,w,p,r],单位为 mm/deg。</param>
/// <param name="operationName">用于日志和超时异常的操作名。</param>
private void ExecuteMovePoseAndWait(RobotProfile robot, IReadOnlyList<double> targetPose, string operationName)
{
var currentPose = NormalizeRuntimePose(_runtime.GetPose());
EnsurePoseVector(targetPose, nameof(targetPose));
var speedRatio = _runtime.GetSnapshot().SpeedRatio;
var moveResult = MovePoseTrajectoryGenerator.CreateResult(currentPose, targetPose, robot.ServoPeriod, speedRatio, _logger);
_logger?.LogInformation(
"{OperationName} 直角PTP规划完成: 当前速度倍率={SpeedRatio}, 规划时长={Duration}s, 采样点数={SampleCount}",
operationName,
speedRatio,
moveResult.Duration.TotalSeconds,
moveResult.DenseCartesianTrajectory?.Count ?? 0);
_runtime.ExecuteCartesianTrajectory(moveResult, targetPose);
WaitForRuntimeMotionComplete(operationName, moveResult.Duration);
}
/// <summary>
/// 校验当前反馈是否接近飞拍起点;不接近时直接抛出兼容错误。
/// </summary>
private void EnsureFeedbackNearFlyshotStart(IReadOnlyList<double> targetJointPositions, string name)
{
if (!IsFeedbackNearFlyshotStart(targetJointPositions, name))
{
throw new InvalidOperationException("Robot feedback is not near flyshot start.");
}
}
/// <summary>
/// 检查当前机械臂关节反馈与计划轨迹第一个点之间的差异。
/// </summary>
private bool IsFeedbackNearFlyshotStart(IReadOnlyList<double> targetJointPositions, string name)
{
var currentJointPositions = _runtime.GetJointPositions();
var diff = currentJointPositions.Zip(targetJointPositions, (current, target) => Math.Abs(current - target)).Sum();
if (diff <= 0.01)
{
return true;
}
_logger?.LogWarning("ExecuteTrajectoryByName 当前关节坐标与计划轨迹的第一个点之间的差异过大 name={Name}, diff={Diff}", name, diff);
return false;
}
/// <summary> /// <summary>
/// 等待运行时报告当前运动结束,用于把 move_to_start 与正式飞拍轨迹串行化。 /// 等待运行时报告当前运动结束,用于把 move_to_start 与正式飞拍轨迹串行化。
/// </summary> /// </summary>
@@ -752,44 +592,30 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
_logger?.LogInformation("SaveTrajectoryInfo 开始: name={Name}, method={Method}", name, method); _logger?.LogInformation("SaveTrajectoryInfo 开始: name={Name}, method={Method}", name, method);
RobotProfile robot;
CompatibilityRobotSettings planningSettings;
ControllerClientCompatUploadedTrajectory trajectory;
lock (_stateLock) lock (_stateLock)
{ {
robot = RequireActiveRobot(); var robot = RequireActiveRobot();
if (!_uploadedTrajectories.TryGetValue(name, out var uploadedTrajectory)) if (!_uploadedTrajectories.TryGetValue(name, out var trajectory))
{ {
_logger?.LogWarning("SaveTrajectoryInfo 失败: 轨迹不存在 name={Name}", name); _logger?.LogWarning("SaveTrajectoryInfo 失败: 轨迹不存在 name={Name}", name);
throw new InvalidOperationException("FlyShot trajectory does not exist."); throw new InvalidOperationException("FlyShot trajectory does not exist.");
} }
// 先通过规划校验避免静默接受非法参数,同时把轨迹信息强制刷写到本地 JSON。 // 先通过规划校验避免静默接受非法参数,同时把轨迹信息强制刷写到本地 JSON。
// 保存轨迹信息会执行规划和文件导出,先复制上传数据再释放状态锁。 var planningSettings = RequireRobotSettings();
trajectory = CloneUploadedTrajectory(uploadedTrajectory); var bundle = _trajectoryOrchestrator.PlanUploadedFlyshot(
planningSettings = RequireRobotSettings(); robot,
} trajectory,
new FlyshotExecutionOptions(useCache:false,saveTrajectory: true, method: method),
planningSettings,
planningSettings.PlanningSpeedScale);
_logger?.LogInformation("SaveTrajectoryInfo 规划完成记录到本地");
ExportFlyshotArtifactsIfRequested(name, saveTrajectory: true, robot, bundle);
var speedRatio = _runtime.GetSnapshot().SpeedRatio; // var robotName = _configuredRobotName ?? throw new InvalidOperationException("Robot has not been setup.");
var bundle = _trajectoryOrchestrator.PlanUploadedFlyshot( // var settings = _robotSettings ?? CreateDefaultRobotSettings();
robot, // _trajectoryStore.Save(robotName, settings, trajectory);
trajectory, }
new FlyshotExecutionOptions(useCache: false, saveTrajectory: true, method: method),
planningSettings,
planningSettings.PlanningSpeedScale,
speedRatio);
bundle = PrepareFlyshotExecutionBundle(robot, bundle, speedRatio);
_logger?.LogInformation("SaveTrajectoryInfo 规划完成记录到本地");
ExportFlyshotArtifactsIfRequested(
name,
saveTrajectory: true,
robot,
trajectory,
new FlyshotExecutionOptions(useCache: false, saveTrajectory: true, method: method),
planningSettings,
bundle,
planningSettings.PlanningSpeedScale,
speedRatio);
_logger?.LogInformation("SaveTrajectoryInfo 完成: name={Name}", name); _logger?.LogInformation("SaveTrajectoryInfo 完成: name={Name}", name);
} }
@@ -804,48 +630,30 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
_logger?.LogInformation("IsFlyshotTrajectoryValid 开始: name={Name}, method={Method}", name, method); _logger?.LogInformation("IsFlyshotTrajectoryValid 开始: name={Name}, method={Method}", name, method);
RobotProfile robot;
CompatibilityRobotSettings planningSettings;
ControllerClientCompatUploadedTrajectory trajectory;
lock (_stateLock) lock (_stateLock)
{ {
robot = RequireActiveRobot(); var robot = RequireActiveRobot();
if (!_uploadedTrajectories.TryGetValue(name, out var uploadedTrajectory)) if (!_uploadedTrajectories.TryGetValue(name, out var trajectory))
{ {
_logger?.LogWarning("IsFlyshotTrajectoryValid 失败: 轨迹不存在 name={Name}", name); _logger?.LogWarning("IsFlyshotTrajectoryValid 失败: 轨迹不存在 name={Name}", name);
throw new InvalidOperationException("FlyShot trajectory does not exist."); throw new InvalidOperationException("FlyShot trajectory does not exist.");
} }
// 有效性检查只消费当前快照,不要求和后续上传/删除形成长事务。 var planningSettings = RequireRobotSettings();
trajectory = CloneUploadedTrajectory(uploadedTrajectory); var bundle = _trajectoryOrchestrator.PlanUploadedFlyshot(
planningSettings = RequireRobotSettings(); robot,
trajectory,
new FlyshotExecutionOptions(method: method, saveTrajectory: saveTrajectory),
planningSettings,
planningSettings.PlanningSpeedScale);
ExportFlyshotArtifactsIfRequested(name, saveTrajectory, robot, bundle);
duration = bundle.Result.Duration;
_logger?.LogInformation(
"IsFlyshotTrajectoryValid 结果: name={Name}, valid={Valid}, duration={Duration}s",
name, bundle.Result.IsValid, duration.TotalSeconds);
return bundle.Result.IsValid;
} }
var speedRatio = _runtime.GetSnapshot().SpeedRatio;
var bundle = _trajectoryOrchestrator.PlanUploadedFlyshot(
robot,
trajectory,
new FlyshotExecutionOptions(method: method, saveTrajectory: saveTrajectory),
planningSettings,
planningSettings.PlanningSpeedScale,
speedRatio);
bundle = PrepareFlyshotExecutionBundle(robot, bundle, speedRatio);
ExportFlyshotArtifactsIfRequested(
name,
saveTrajectory,
robot,
trajectory,
new FlyshotExecutionOptions(method: method, saveTrajectory: saveTrajectory),
planningSettings,
bundle,
planningSettings.PlanningSpeedScale,
speedRatio);
duration = bundle.Result.Duration;
_logger?.LogInformation(
"IsFlyshotTrajectoryValid 结果: name={Name}, valid={Valid}, duration={Duration}s",
name, bundle.Result.IsValid, duration.TotalSeconds);
return bundle.Result.IsValid;
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -858,7 +666,6 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
_logger?.LogInformation("DeleteTrajectory 开始: name={Name}", name); _logger?.LogInformation("DeleteTrajectory 开始: name={Name}", name);
string robotName;
lock (_stateLock) lock (_stateLock)
{ {
if (!_uploadedTrajectories.Remove(name)) if (!_uploadedTrajectories.Remove(name))
@@ -867,12 +674,10 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
throw new InvalidOperationException("DeleteFlyShotTraj failed"); throw new InvalidOperationException("DeleteFlyShotTraj failed");
} }
robotName = _configuredRobotName ?? throw new InvalidOperationException("Robot has not been setup."); var robotName = _configuredRobotName ?? throw new InvalidOperationException("Robot has not been setup.");
_trajectoryStore.Delete(robotName, name);
} }
// 删除持久化文件不占用状态锁,状态页只需要看到内存字典的即时快照。
_trajectoryStore.Delete(robotName, name);
_logger?.LogInformation("DeleteTrajectory 完成: name={Name}", name); _logger?.LogInformation("DeleteTrajectory 完成: name={Name}", name);
} }
@@ -912,21 +717,6 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
return _robotSettings ?? CreateDefaultRobotSettings(); return _robotSettings ?? CreateDefaultRobotSettings();
} }
/// <summary>
/// 复制一份上传轨迹快照,避免锁外规划期间观察到可变集合引用。
/// </summary>
/// <param name="trajectory">待复制的上传轨迹。</param>
/// <returns>上传轨迹副本。</returns>
private static ControllerClientCompatUploadedTrajectory CloneUploadedTrajectory(ControllerClientCompatUploadedTrajectory trajectory)
{
return new ControllerClientCompatUploadedTrajectory(
trajectory.Name,
trajectory.Waypoints,
trajectory.ShotFlags,
trajectory.OffsetValues,
trajectory.AddressGroups);
}
/// <summary> /// <summary>
/// 校验机器人已经完成初始化。 /// 校验机器人已经完成初始化。
/// </summary> /// </summary>
@@ -970,125 +760,26 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
} }
} }
/// <summary>
/// 校验直角位姿为 [x,y,z,w,p,r] 六维有限数。
/// </summary>
/// <param name="pose">待校验位姿,单位为 mm/deg。</param>
/// <param name="paramName">调用方参数名。</param>
private static void EnsurePoseVector(IReadOnlyList<double> pose, string paramName)
{
if (pose.Count != 6)
{
throw new ArgumentException("位姿必须为 [x,y,z,w,p,r] 六维数组。", paramName);
}
for (var index = 0; index < pose.Count; index++)
{
var value = pose[index];
if (double.IsNaN(value) || double.IsInfinity(value))
{
throw new ArgumentOutOfRangeException(paramName, $"第 {index} 个位姿值必须是有限数值。");
}
}
}
/// <summary>
/// 将运行时位姿快照归一化为 J519 直角命令需要的 [x,y,z,w,p,r] 六维。
/// </summary>
/// <param name="pose">运行时返回的位姿数组。</param>
/// <returns>六维直角位姿。</returns>
private static IReadOnlyList<double> NormalizeRuntimePose(IReadOnlyList<double> pose)
{
if (pose.Count < 6)
{
throw new InvalidOperationException("Runtime pose must contain at least [x,y,z,w,p,r].");
}
var normalized = pose.Take(6).ToArray();
EnsurePoseVector(normalized, nameof(pose));
return normalized;
}
/// <summary> /// <summary>
/// 根据 saveTrajectory 参数把规划结果点位写入运行目录 Config/Data/name。 /// 根据 saveTrajectory 参数把规划结果点位写入运行目录 Config/Data/name。
/// </summary> /// </summary>
/// <param name="name">飞拍轨迹名称。</param> /// <param name="name">飞拍轨迹名称。</param>
/// <param name="saveTrajectory">是否导出规划结果点位。</param> /// <param name="saveTrajectory">是否导出规划结果点位。</param>
/// <param name="robot">当前机器人模型。</param> /// <param name="robot">当前机器人模型。</param>
/// <param name="uploaded">已上传的飞拍轨迹。</param> /// <param name="bundle">规划结果包。</param>
/// <param name="options">本次规划选项。</param>
/// <param name="settings">当前机器人兼容设置。</param>
/// <param name="executionBundle">运行时和 ActualSend 诊断使用的规划结果包。</param>
/// <param name="planningSpeedScale">规划速度倍率。</param>
/// <param name="speedRatio">本次运行时速度倍率。</param>
private void ExportFlyshotArtifactsIfRequested( private void ExportFlyshotArtifactsIfRequested(
string name, string name,
bool saveTrajectory, bool saveTrajectory,
RobotProfile robot, RobotProfile robot,
ControllerClientCompatUploadedTrajectory uploaded, PlannedExecutionBundle bundle)
FlyshotExecutionOptions options,
CompatibilityRobotSettings settings,
PlannedExecutionBundle executionBundle,
double planningSpeedScale,
double speedRatio)
{ {
if (!saveTrajectory) if (!saveTrajectory)
{ {
return; return;
} }
// 旧格式 Joint/Cart 导出必须只反映 planning_speed_scale;ActualSend* 才反映当前运行时 speedRatio。 var speedRatio = _runtime.GetSnapshot().SpeedRatio;
var exportPlanningBundle = _trajectoryOrchestrator.PlanUploadedFlyshot( _artifactWriter.WriteUploadedFlyshot(name, robot, bundle, speedRatio);
robot,
uploaded,
new FlyshotExecutionOptions(
moveToStart: options.MoveToStart,
useCache: false,
saveTrajectory: options.SaveTrajectory,
method: options.Method,
wait: options.Wait),
settings,
planningSpeedScale,
speedRatio: 1.0);
_artifactWriter.WriteUploadedFlyshot(name, robot, exportPlanningBundle, executionBundle, speedRatio);
}
/// <summary>
/// 为飞拍链路预先构建最终发送队列,确保运行时只消费已经离散校验通过的 8ms 点列。
/// </summary>
private static PlannedExecutionBundle PrepareFlyshotExecutionBundle(
RobotProfile robot,
PlannedExecutionBundle bundle,
double speedRatio)
{
var preparedExecution = FlyshotExecutionSendSequenceBuilder.Build(
robot,
bundle.ExecutionTrajectory,
bundle.Result,
robot.ServoPeriod.TotalSeconds,
requestedSpeedRatio: speedRatio,
samplingSpeedRatio: 1.0);
var preparedResult = new TrajectoryResult(
programName: bundle.Result.ProgramName,
method: bundle.Result.Method,
isValid: bundle.Result.IsValid,
duration: bundle.Result.Duration,
shotEvents: bundle.Result.ShotEvents,
triggerTimeline: bundle.Result.TriggerTimeline,
artifacts: bundle.Result.Artifacts,
failureReason: bundle.Result.FailureReason,
usedCache: bundle.Result.UsedCache,
originalWaypointCount: bundle.Result.OriginalWaypointCount,
plannedWaypointCount: bundle.Result.PlannedWaypointCount,
triggerSampleIndexOffsetCycles: bundle.Result.TriggerSampleIndexOffsetCycles,
denseJointTrajectory: bundle.Result.DenseJointTrajectory,
preparedFlyshotExecution: preparedExecution);
return new PlannedExecutionBundle(
bundle.PlannedTrajectory,
bundle.ShotTimeline,
preparedResult,
preparedExecution,
bundle.ExecutionTrajectory);
} }
/// <summary> /// <summary>
@@ -1140,7 +831,6 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
useDo: false, useDo: false,
ioAddresses: Array.Empty<int>(), ioAddresses: Array.Empty<int>(),
ioKeepCycles: 2, ioKeepCycles: 2,
triggerSampleIndexOffsetCycles: 7,
accLimitScale: 1.0, accLimitScale: 1.0,
jerkLimitScale: 1.0, jerkLimitScale: 1.0,
adaptIcspTryNum: 5); adaptIcspTryNum: 5);

View File

@@ -1,62 +0,0 @@
namespace Flyshot.ControllerClientCompat;
/// <summary>
/// 保存状态页需要的 ControllerClient 兼容层元数据快照,避免状态页连续读取多个受保护字段。
/// </summary>
public sealed record ControllerClientStatusSnapshotMetadata
{
/// <summary>
/// 初始化一份兼容层状态元数据快照。
/// </summary>
/// <param name="isSetup">当前是否已经完成机器人初始化。</param>
/// <param name="robotName">当前机器人名称;未初始化时为空。</param>
/// <param name="degreesOfFreedom">当前机器人自由度;未初始化时为 0。</param>
/// <param name="uploadedTrajectories">当前已上传轨迹名称快照。</param>
/// <param name="serverVersion">兼容服务端版本。</param>
/// <param name="clientVersion">兼容客户端版本。</param>
public ControllerClientStatusSnapshotMetadata(
bool isSetup,
string? robotName,
int degreesOfFreedom,
IReadOnlyList<string> uploadedTrajectories,
string serverVersion,
string clientVersion)
{
IsSetup = isSetup;
RobotName = robotName;
DegreesOfFreedom = degreesOfFreedom;
UploadedTrajectories = uploadedTrajectories.ToArray();
ServerVersion = serverVersion;
ClientVersion = clientVersion;
}
/// <summary>
/// 获取当前是否已经完成机器人初始化。
/// </summary>
public bool IsSetup { get; }
/// <summary>
/// 获取当前机器人名称;未初始化时为空。
/// </summary>
public string? RobotName { get; }
/// <summary>
/// 获取当前机器人自由度;未初始化时为 0。
/// </summary>
public int DegreesOfFreedom { get; }
/// <summary>
/// 获取已上传轨迹名称数组快照。
/// </summary>
public IReadOnlyList<string> UploadedTrajectories { get; }
/// <summary>
/// 获取兼容服务端版本。
/// </summary>
public string ServerVersion { get; }
/// <summary>
/// 获取兼容客户端版本。
/// </summary>
public string ClientVersion { get; }
}

View File

@@ -22,12 +22,6 @@ public sealed class ControllerClientTrajectoryOrchestrator
/// </summary> /// </summary>
private const double DenseLimitStretchFactor = 1.01; private const double DenseLimitStretchFactor = 1.01;
/// <summary>
/// 平滑起停重定时混合系数:0 表示不平滑,1 表示完全使用平滑时间律。
/// 当前取值用于“弱平滑”,仅轻微拉伸首尾时间段,避免起停过慢。
/// </summary>
private const double SmoothStartStopBlend = 0.60;
private readonly ICspPlanner _icspPlanner; private readonly ICspPlanner _icspPlanner;
private readonly SelfAdaptIcspPlanner _selfAdaptIcspPlanner; private readonly SelfAdaptIcspPlanner _selfAdaptIcspPlanner;
private readonly ShotTimelineBuilder _shotTimelineBuilder = new(new WaypointTimestampResolver()); private readonly ShotTimelineBuilder _shotTimelineBuilder = new(new WaypointTimestampResolver());
@@ -53,26 +47,21 @@ public sealed class ControllerClientTrajectoryOrchestrator
/// </summary> /// </summary>
/// <param name="robot">当前机器人配置。</param> /// <param name="robot">当前机器人配置。</param>
/// <param name="waypoints">普通轨迹关节路点。</param> /// <param name="waypoints">普通轨迹关节路点。</param>
/// <param name="options">执行参数。</param>
/// <param name="planningSpeedScale">规划速度倍率。</param>
/// <param name="speedRatio">本次执行速度倍率;普通轨迹在规划阶段折算进有效限速倍率。</param>
/// <returns>包含规划轨迹、空触发时间轴和执行结果的结果包。</returns> /// <returns>包含规划轨迹、空触发时间轴和执行结果的结果包。</returns>
public PlannedExecutionBundle PlanOrdinaryTrajectory( public PlannedExecutionBundle PlanOrdinaryTrajectory(
RobotProfile robot, RobotProfile robot,
IReadOnlyList<IReadOnlyList<double>> waypoints, IReadOnlyList<IReadOnlyList<double>> waypoints,
TrajectoryExecutionOptions? options = null, TrajectoryExecutionOptions? options = null,
double planningSpeedScale = 1.0, double planningSpeedScale = 1.0)
double speedRatio = 1.0)
{ {
ArgumentNullException.ThrowIfNull(robot); ArgumentNullException.ThrowIfNull(robot);
ArgumentNullException.ThrowIfNull(waypoints); ArgumentNullException.ThrowIfNull(waypoints);
options ??= new TrajectoryExecutionOptions(); options ??= new TrajectoryExecutionOptions();
var effectivePlanningSpeedScale = ResolveEffectivePlanningSpeedScale(planningSpeedScale, speedRatio, "普通轨迹"); var planningRobot = ApplyPlanningSpeedScale(robot, planningSpeedScale);
var planningRobot = ApplyPlanningSpeedScale(robot, effectivePlanningSpeedScale);
_logger?.LogInformation( _logger?.LogInformation(
"PlanOrdinaryTrajectory 开始: 路点数={WaypointCount}, method={Method}, planningSpeedScale={PlanningSpeedScale}, speedRatio={SpeedRatio}, effectivePlanningSpeedScale={EffectivePlanningSpeedScale}", "PlanOrdinaryTrajectory 开始: 路点数={WaypointCount}, method={Method}, planningSpeedScale={PlanningSpeedScale}",
waypoints.Count, options.Method, planningSpeedScale, speedRatio, effectivePlanningSpeedScale); waypoints.Count, options.Method, planningSpeedScale);
var program = CreateProgram( var program = CreateProgram(
name: "ordinary-trajectory", name: "ordinary-trajectory",
@@ -92,19 +81,14 @@ public sealed class ControllerClientTrajectoryOrchestrator
var executionTrajectory = plannedTrajectory; var executionTrajectory = plannedTrajectory;
var denseJointTrajectory = CreateLimitCompliantDenseTrajectory(ref executionTrajectory, shapeTrajectoryEdges: false); var denseJointTrajectory = CreateLimitCompliantDenseTrajectory(ref executionTrajectory, shapeTrajectoryEdges: false);
var shotTimeline = new ShotTimeline(Array.Empty<ShotEvent>(), Array.Empty<TrajectoryDoEvent>()); var shotTimeline = new ShotTimeline(Array.Empty<ShotEvent>(), Array.Empty<TrajectoryDoEvent>());
var result = CreateResult( var result = CreateResult(executionTrajectory, shotTimeline, denseJointTrajectory, usedCache: false);
executionTrajectory,
shotTimeline,
denseJointTrajectory,
usedCache: false,
triggerSampleIndexOffsetCycles: 0);
_logger?.LogInformation( _logger?.LogInformation(
"PlanOrdinaryTrajectory 完成: 时长={Duration}s, 采样点数={SampleCount}", "PlanOrdinaryTrajectory 完成: 时长={Duration}s, 采样点数={SampleCount}",
result.Duration.TotalSeconds, result.Duration.TotalSeconds,
result.DenseJointTrajectory?.Count ?? 0); result.DenseJointTrajectory?.Count ?? 0);
return new PlannedExecutionBundle(plannedTrajectory, shotTimeline, result, executionTrajectory: executionTrajectory); return new PlannedExecutionBundle(plannedTrajectory, shotTimeline, result);
} }
/// <summary> /// <summary>
@@ -112,38 +96,24 @@ public sealed class ControllerClientTrajectoryOrchestrator
/// </summary> /// </summary>
/// <param name="robot">当前机器人配置。</param> /// <param name="robot">当前机器人配置。</param>
/// <param name="uploaded">兼容层保存的上传轨迹。</param> /// <param name="uploaded">兼容层保存的上传轨迹。</param>
/// <param name="options">执行参数。</param>
/// <param name="settings">兼容层机器人设置。</param>
/// <param name="planningSpeedScale">规划速度倍率。</param>
/// <param name="speedRatio">本次飞拍使用的速度倍率;为空时使用配置默认值。</param>
/// <returns>包含规划轨迹、触发时间轴和执行结果的结果包。</returns> /// <returns>包含规划轨迹、触发时间轴和执行结果的结果包。</returns>
public PlannedExecutionBundle PlanUploadedFlyshot( public PlannedExecutionBundle PlanUploadedFlyshot(
RobotProfile robot, RobotProfile robot,
ControllerClientCompatUploadedTrajectory uploaded, ControllerClientCompatUploadedTrajectory uploaded,
FlyshotExecutionOptions? options = null, FlyshotExecutionOptions? options = null,
CompatibilityRobotSettings? settings = null, CompatibilityRobotSettings? settings = null,
double? planningSpeedScale = null, double? planningSpeedScale = null)
double? speedRatio = null)
{ {
ArgumentNullException.ThrowIfNull(robot); ArgumentNullException.ThrowIfNull(robot);
ArgumentNullException.ThrowIfNull(uploaded); ArgumentNullException.ThrowIfNull(uploaded);
options ??= new FlyshotExecutionOptions(); options ??= new FlyshotExecutionOptions();
settings ??= CreateDefaultRobotSettings(); settings ??= CreateDefaultRobotSettings();
var requestedSpeedRatio = speedRatio ?? settings.SpeedRatio; var effectivePlanningSpeedScale = planningSpeedScale ?? settings.PlanningSpeedScale;
var configuredPlanningSpeedScale = planningSpeedScale ?? settings.PlanningSpeedScale;
var effectivePlanningSpeedScale = ResolveEffectivePlanningSpeedScale(configuredPlanningSpeedScale, requestedSpeedRatio, "飞拍");
var planningRobot = ApplyPlanningSpeedScale(robot, effectivePlanningSpeedScale); var planningRobot = ApplyPlanningSpeedScale(robot, effectivePlanningSpeedScale);
_logger?.LogInformation( _logger?.LogInformation(
"PlanUploadedFlyshot 开始: name={Name}, waypoints={WaypointCount}, method={Method}, useCache={UseCache}, planningSpeedScale={PlanningSpeedScale}, speedRatio={SpeedRatio}, effectivePlanningSpeedScale={EffectivePlanningSpeedScale}, smoothStartStopTiming={SmoothStartStopTiming}", "PlanUploadedFlyshot 开始: name={Name}, waypoints={WaypointCount}, method={Method}, useCache={UseCache}, planningSpeedScale={PlanningSpeedScale}, smoothStartStopTiming={SmoothStartStopTiming}",
uploaded.Name, uploaded.Name, uploaded.Waypoints.Count, options.Method, options.UseCache, effectivePlanningSpeedScale, settings.SmoothStartStopTiming);
uploaded.Waypoints.Count,
options.Method,
options.UseCache,
configuredPlanningSpeedScale,
requestedSpeedRatio,
effectivePlanningSpeedScale,
settings.SmoothStartStopTiming);
var program = CreateProgram( var program = CreateProgram(
name: uploaded.Name, name: uploaded.Name,
@@ -154,13 +124,21 @@ public sealed class ControllerClientTrajectoryOrchestrator
var method = ParseFlyshotMethod(options.Method); var method = ParseFlyshotMethod(options.Method);
var cacheKey = CreateFlyshotCacheKey(planningRobot, uploaded, options, settings, effectivePlanningSpeedScale); var cacheKey = CreateFlyshotCacheKey(planningRobot, uploaded, options, settings, effectivePlanningSpeedScale);
if (options.UseCache && _flyshotCache.TryGetValue(cacheKey, out var cachedBundle)) //if (options.UseCache && _flyshotCache.TryGetValue(cacheKey, out var cachedBundle))
{ //{
_logger?.LogInformation("PlanUploadedFlyshot 命中缓存: name={Name}, cacheKey={CacheKey}", uploaded.Name, cacheKey); // _logger?.LogInformation("PlanUploadedFlyshot 命中缓存: name={Name}, cacheKey={CacheKey}", uploaded.Name, cacheKey);
// var executionTrajectory = ApplyExecutionTiming(cachedBundle.PlannedTrajectory, settings);
// 命中缓存时只替换 TrajectoryResult 的 usedCache 标志,规划轨迹和触发时间轴保持不可变复用。 // var executionTimeline = _shotTimelineBuilder.Build(
return cachedBundle; // executionTrajectory,
} // holdCycles: settings.IoKeepCycles,
// samplePeriod: planningRobot.ServoPeriod,
// useDo: settings.UseDo);
// // 命中缓存时只替换 TrajectoryResult 的 usedCache 标志,规划轨迹和触发时间轴保持不可变复用。
// return new PlannedExecutionBundle(
// cachedBundle.PlannedTrajectory,
// executionTimeline,
// CreateResult(executionTrajectory, executionTimeline, usedCache: true, shapeTrajectoryEdges: false));
//}
var request = new TrajectoryRequest( var request = new TrajectoryRequest(
robot: planningRobot, robot: planningRobot,
@@ -178,13 +156,8 @@ public sealed class ControllerClientTrajectoryOrchestrator
holdCycles: settings.IoKeepCycles, holdCycles: settings.IoKeepCycles,
samplePeriod: planningRobot.ServoPeriod, samplePeriod: planningRobot.ServoPeriod,
useDo: settings.UseDo); useDo: settings.UseDo);
var result = CreateResult( var result = CreateResult(smoothedExecutionTrajectory, shotTimeline, denseJointTrajectory, usedCache: false);
smoothedExecutionTrajectory, var bundle = new PlannedExecutionBundle(plannedTrajectory, shotTimeline, result);
shotTimeline,
denseJointTrajectory,
usedCache: false,
triggerSampleIndexOffsetCycles: settings.TriggerSampleIndexOffsetCycles);
var bundle = new PlannedExecutionBundle(plannedTrajectory, shotTimeline, result, executionTrajectory: smoothedExecutionTrajectory);
_logger?.LogInformation( _logger?.LogInformation(
"PlanUploadedFlyshot 完成: name={Name}, 时长={Duration}s, 触发事件数={TriggerCount}, 采样点数={SampleCount}", "PlanUploadedFlyshot 完成: name={Name}, 时长={Duration}s, 触发事件数={TriggerCount}, 采样点数={SampleCount}",
@@ -286,7 +259,6 @@ public sealed class ControllerClientTrajectoryOrchestrator
hash.Add(options.SaveTrajectory); hash.Add(options.SaveTrajectory);
hash.Add(settings.UseDo); hash.Add(settings.UseDo);
hash.Add(settings.IoKeepCycles); hash.Add(settings.IoKeepCycles);
hash.Add(settings.TriggerSampleIndexOffsetCycles);
hash.Add(settings.AdaptIcspTryNum); hash.Add(settings.AdaptIcspTryNum);
hash.Add(settings.SmoothStartStopTiming); hash.Add(settings.SmoothStartStopTiming);
@@ -337,29 +309,11 @@ public sealed class ControllerClientTrajectoryOrchestrator
useDo: true, useDo: true,
ioAddresses: Array.Empty<int>(), ioAddresses: Array.Empty<int>(),
ioKeepCycles: 0, ioKeepCycles: 0,
triggerSampleIndexOffsetCycles: 7,
accLimitScale: 1.0, accLimitScale: 1.0,
jerkLimitScale: 1.0, jerkLimitScale: 1.0,
adaptIcspTryNum: 5); adaptIcspTryNum: 5);
} }
/// <summary>
/// 解析飞拍生成轨迹时真正使用的规划速度倍率。
/// </summary>
/// <param name="planningSpeedScale">配置或调用方传入的规划倍率。</param>
/// <param name="speedRatio">本次执行速度倍率。</param>
/// <returns>用于缩放规划关节限制的有效倍率。</returns>
private static double ResolveEffectivePlanningSpeedScale(double planningSpeedScale, double speedRatio, string trajectoryKind)
{
if (speedRatio <= 0.0 || double.IsNaN(speedRatio) || double.IsInfinity(speedRatio))
{
throw new ArgumentOutOfRangeException(nameof(speedRatio), $"{trajectoryKind}速度倍率必须是有限正数。");
}
// 生成轨迹时把执行倍率折算进规划限制,保证 planning_speed_scale * speed_ratio 相同的配置产出同一条轨迹。
return planningSpeedScale * speedRatio;
}
/// <summary> /// <summary>
/// 按运行配置决定是否对规划结果做执行前时间轴重映射。 /// 按运行配置决定是否对规划结果做执行前时间轴重映射。
/// </summary> /// </summary>
@@ -392,7 +346,7 @@ public sealed class ControllerClientTrajectoryOrchestrator
return robot; return robot;
} }
// speed_ratio 已在调用方折算到规划倍率中;运行时只消费已经生成好的 8ms 稠密轨迹。 // RVBUST 规划阶段会用独立限速倍率缩放有效限制;运行时 speedRatio 仍只负责 J519 下发重采样。
var scaledLimits = robot.JointLimits var scaledLimits = robot.JointLimits
.Select(limit => new JointLimit( .Select(limit => new JointLimit(
limit.JointName, limit.JointName,
@@ -445,8 +399,7 @@ public sealed class ControllerClientTrajectoryOrchestrator
PlannedTrajectory plannedTrajectory, PlannedTrajectory plannedTrajectory,
ShotTimeline shotTimeline, ShotTimeline shotTimeline,
IReadOnlyList<IReadOnlyList<double>> denseJointTrajectory, IReadOnlyList<IReadOnlyList<double>> denseJointTrajectory,
bool usedCache, bool usedCache)
int triggerSampleIndexOffsetCycles)
{ {
return new TrajectoryResult( return new TrajectoryResult(
programName: plannedTrajectory.OriginalProgram.Name, programName: plannedTrajectory.OriginalProgram.Name,
@@ -460,7 +413,6 @@ public sealed class ControllerClientTrajectoryOrchestrator
usedCache: usedCache, usedCache: usedCache,
originalWaypointCount: plannedTrajectory.OriginalWaypointCount, originalWaypointCount: plannedTrajectory.OriginalWaypointCount,
plannedWaypointCount: plannedTrajectory.PlannedWaypointCount, plannedWaypointCount: plannedTrajectory.PlannedWaypointCount,
triggerSampleIndexOffsetCycles: triggerSampleIndexOffsetCycles,
denseJointTrajectory: denseJointTrajectory); denseJointTrajectory: denseJointTrajectory);
} }
@@ -483,8 +435,7 @@ public sealed class ControllerClientTrajectoryOrchestrator
TrajectoryLimitValidator.ValidateDenseJointTrajectory( TrajectoryLimitValidator.ValidateDenseJointTrajectory(
executionTrajectory.Robot, executionTrajectory.Robot,
denseJointTrajectory, denseJointTrajectory,
trajectoryName: executionTrajectory.OriginalProgram.Name, trajectoryName: executionTrajectory.OriginalProgram.Name);
validateJerk: false);
return denseJointTrajectory; return denseJointTrajectory;
} }
catch (InvalidOperationException ex) when (iteration < MaxDenseLimitStretchIterations) catch (InvalidOperationException ex) when (iteration < MaxDenseLimitStretchIterations)
@@ -525,51 +476,36 @@ public sealed class ControllerClientTrajectoryOrchestrator
} }
/// <summary> /// <summary>
/// 为飞拍执行生成平滑起停时间轴(仅重定时,不改几何路点)。 /// 为飞拍执行生成一条平滑起停的时间轴。
/// 该方法保持 <see cref="PlannedTrajectory.PlannedWaypoints"/> 不变,只重映射 <see cref="PlannedTrajectory.WaypointTimes"/>, /// 保持路点位置不变,只重映射路点时刻,让起点和终点附近的速度自然收敛。
/// 让轨迹在起点和终点附近具有更柔和的速度变化,从而降低“突然起步/突然收尾”带来的离散差分尖峰。
/// </summary> /// </summary>
/// <param name="plannedTrajectory">规划器输出的原始轨迹(通常是线性时间轴)。</param>
/// <returns>
/// 若满足平滑条件,则返回新的重定时轨迹;若路点数量不足或总时长无效,则直接返回输入轨迹。
/// </returns>
private static PlannedTrajectory ApplySmoothStartStopTiming(PlannedTrajectory plannedTrajectory) private static PlannedTrajectory ApplySmoothStartStopTiming(PlannedTrajectory plannedTrajectory)
{ {
var originalTimes = plannedTrajectory.WaypointTimes; var originalTimes = plannedTrajectory.WaypointTimes;
// 至少需要“起点-中间点-终点”三类点,才有可平滑的中间区间。
if (originalTimes.Count < 3) if (originalTimes.Count < 3)
{ {
return plannedTrajectory; return plannedTrajectory;
} }
var totalDuration = originalTimes[^1]; var totalDuration = originalTimes[^1];
// 轨迹总时长必须为正,避免后续归一化进度出现除零或无意义映射。
if (totalDuration <= 0.0) if (totalDuration <= 0.0)
{ {
return plannedTrajectory; return plannedTrajectory;
} }
var smoothedTimes = new double[originalTimes.Count]; var smoothedTimes = new double[originalTimes.Count];
// 强制固定边界:起点仍在 0,终点仍在总时长,保证任务总耗时不变。
smoothedTimes[0] = 0.0; smoothedTimes[0] = 0.0;
smoothedTimes[^1] = totalDuration; smoothedTimes[^1] = totalDuration;
for (var index = 1; index < originalTimes.Count - 1; index++) for (var index = 1; index < originalTimes.Count - 1; index++)
{ {
// 把原始时刻归一化到 [0, 1],用于统一时间律变换。
var normalizedProgress = originalTimes[index] / totalDuration; var normalizedProgress = originalTimes[index] / totalDuration;
// 先求完整平滑时间律对应的时刻,再与原线性时刻按比例混合。 smoothedTimes[index] = totalDuration * InvertSmoothStartStopProgress(normalizedProgress);
// 这里采用弱平滑(blend=0.35):保留大部分原节奏,仅轻微降低首尾突变。
var smoothedProgress = InvertSmoothStartStopProgress(normalizedProgress);
var blendedProgress = ((1.0 - SmoothStartStopBlend) * normalizedProgress)
+ (SmoothStartStopBlend * smoothedProgress);
smoothedTimes[index] = totalDuration * blendedProgress;
} }
var segmentDurations = new double[smoothedTimes.Length - 1]; var segmentDurations = new double[smoothedTimes.Length - 1];
for (var index = 0; index < segmentDurations.Length; index++) for (var index = 0; index < segmentDurations.Length; index++)
{ {
// 重建每段持续时间,供后续稠密采样和限幅检查使用。
segmentDurations[index] = smoothedTimes[index + 1] - smoothedTimes[index]; segmentDurations[index] = smoothedTimes[index + 1] - smoothedTimes[index];
} }
@@ -586,24 +522,17 @@ public sealed class ControllerClientTrajectoryOrchestrator
} }
/// <summary> /// <summary>
/// 反解平滑时间律进度:给定目标进度 p,求满足 f(t)=p 的 t。 /// 反解 7 次 smootherstep 的时间进度,用二分法把原始线性进度映射成平滑时间轴。
/// 其中 f(t) 由 <see cref="EvaluateSmoothStartStopProgress"/> 给出,是单调递增的 7 次 smootherstep 曲线,
/// 通过二分法可稳定得到对应的归一化时间,避免显式求高次方程根带来的复杂性和数值不稳定。
/// </summary> /// </summary>
/// <param name="normalizedProgress">目标归一化进度,理论范围 [0, 1]。</param>
/// <returns>与目标进度对应的归一化时间,范围 [0, 1]。</returns>
private static double InvertSmoothStartStopProgress(double normalizedProgress) private static double InvertSmoothStartStopProgress(double normalizedProgress)
{ {
// 防御式裁剪,避免上游浮点误差将进度推到区间外。
var target = Math.Clamp(normalizedProgress, 0.0, 1.0); var target = Math.Clamp(normalizedProgress, 0.0, 1.0);
var low = 0.0; var low = 0.0;
var high = 1.0; var high = 1.0;
// 固定迭代次数确保耗时可预测;40 次足以把区间收敛到工程可用精度。
for (var iteration = 0; iteration < 40; iteration++) for (var iteration = 0; iteration < 40; iteration++)
{ {
var middle = (low + high) / 2.0; var middle = (low + high) / 2.0;
var progress = EvaluateSmoothStartStopProgress(middle); var progress = EvaluateSmoothStartStopProgress(middle);
// f(middle) 小于目标,说明根在右半区间;否则在左半区间。
if (progress < target) if (progress < target)
{ {
low = middle; low = middle;
@@ -618,15 +547,10 @@ public sealed class ControllerClientTrajectoryOrchestrator
} }
/// <summary> /// <summary>
/// 计算 7 次 smootherstep 的进度函数值 f(u)。 /// 计算 7 次 smootherstep 进度值,用于整段平滑起停时间律。
/// 该函数在 u=0 和 u=1 处具有更高阶导数连续性,可让起停段速度变化更平滑,
/// 适合用于飞拍轨迹的时间重参数化,减少离散导数在边界处的突变。
/// </summary> /// </summary>
/// <param name="normalizedTime">归一化时间 u,理论范围 [0, 1]。</param>
/// <returns>归一化进度 f(u),范围 [0, 1]。</returns>
private static double EvaluateSmoothStartStopProgress(double normalizedTime) private static double EvaluateSmoothStartStopProgress(double normalizedTime)
{ {
// 先裁剪再计算多项式,确保数值稳定且不受外部越界输入影响。
var u = Math.Clamp(normalizedTime, 0.0, 1.0); var u = Math.Clamp(normalizedTime, 0.0, 1.0);
var u2 = u * u; var u2 = u * u;
var u3 = u2 * u; var u3 = u2 * u;

View File

@@ -49,27 +49,9 @@ public sealed class FlyshotTrajectoryArtifactWriter
/// </summary> /// </summary>
/// <param name="trajectoryName">飞拍轨迹名称。</param> /// <param name="trajectoryName">飞拍轨迹名称。</param>
/// <param name="robot">当前机器人配置。</param> /// <param name="robot">当前机器人配置。</param>
/// <param name="bundle">规划结果包;旧规划导出和实发诊断使用同一份视图。</param> /// <param name="bundle">规划结果包。</param>
/// <param name="speedRatio">请求速度倍率,仅作为诊断列保留;最终采样倍率已在规划阶段处理。</param> /// <param name="speedRatio">导出 J519 实发采样点时使用的速度倍率。</param>
public void WriteUploadedFlyshot(string trajectoryName, RobotProfile robot, PlannedExecutionBundle bundle, double speedRatio = 1.0) public void WriteUploadedFlyshot(string trajectoryName, RobotProfile robot, PlannedExecutionBundle bundle, double speedRatio = 1.0)
{
WriteUploadedFlyshot(trajectoryName, robot, bundle, bundle, speedRatio);
}
/// <summary>
/// 将飞拍规划结果导出到 Config/Data/name,并显式区分旧规划导出视图和实发诊断视图。
/// </summary>
/// <param name="trajectoryName">飞拍轨迹名称。</param>
/// <param name="robot">当前机器人配置。</param>
/// <param name="exportPlanningBundle">旧格式规划导出使用的结果包,不应包含运行时 speedRatio 折算。</param>
/// <param name="executionBundle">实发诊断使用的结果包,应与运行时最终发送队列保持一致。</param>
/// <param name="speedRatio">请求速度倍率,仅作为 ActualSend 诊断列保留。</param>
public void WriteUploadedFlyshot(
string trajectoryName,
RobotProfile robot,
PlannedExecutionBundle exportPlanningBundle,
PlannedExecutionBundle executionBundle,
double speedRatio = 1.0)
{ {
if (string.IsNullOrWhiteSpace(trajectoryName)) if (string.IsNullOrWhiteSpace(trajectoryName))
{ {
@@ -77,32 +59,32 @@ public sealed class FlyshotTrajectoryArtifactWriter
} }
ArgumentNullException.ThrowIfNull(robot); ArgumentNullException.ThrowIfNull(robot);
ArgumentNullException.ThrowIfNull(exportPlanningBundle); ArgumentNullException.ThrowIfNull(bundle);
ArgumentNullException.ThrowIfNull(executionBundle);
var outputDir = Path.Combine(_options.ResolveConfigRoot(), "Data", SanitizeDirectoryName(trajectoryName)); var outputDir = Path.Combine(_options.ResolveConfigRoot(), "Data", SanitizeDirectoryName(trajectoryName));
Directory.CreateDirectory(outputDir); Directory.CreateDirectory(outputDir);
if (exportPlanningBundle.Result.DenseJointTrajectory is null) if (bundle.Result.DenseJointTrajectory is null)
{ {
throw new InvalidOperationException("导出飞拍旧格式规划工件前必须先生成规划明细轨迹。"); throw new InvalidOperationException("导出飞拍轨迹工件前必须先生成执行侧稠密轨迹。");
} }
// 旧格式规划文件固定使用不含运行时 speedRatio 的规划视图,避免和 ActualSend 实发诊断混在一起。 // 明细文件现在定义为“执行侧 8ms 稠密轨迹的 16ms 低频视图”,避免再次从 PlannedTrajectory 生成另一条轨迹。
var kinematicsModel = _robotModelLoader.LoadKinematicsModel(robot.ModelPath); var kinematicsModel = _robotModelLoader.LoadKinematicsModel(robot.ModelPath);
var jointTrajectory = BuildJointRows(exportPlanningBundle.PlannedTrajectory); var jointTrajectory = BuildJointRows(bundle.PlannedTrajectory);
_logger?.LogInformation("规划之后的轨迹点位数量为:{}", jointTrajectory.Count); _logger?.LogInformation("规划之后的轨迹点位数量为:{}", jointTrajectory.Count);
var executionDenseTrajectory = exportPlanningBundle.Result.DenseJointTrajectory; var executionDenseTrajectory = bundle.Result.DenseJointTrajectory;
var jointDetailTrajectory = DownsampleDenseRows( var jointDetailTrajectory = DownsampleDenseRows(
executionDenseTrajectory, executionDenseTrajectory,
samplePeriodSeconds: LegacyDetailSamplePeriodSeconds); samplePeriodSeconds: LegacyDetailSamplePeriodSeconds);
var cartTrajectory = BuildCartesianRows(exportPlanningBundle.PlannedTrajectory, kinematicsModel); var cartTrajectory = BuildCartesianRows(bundle.PlannedTrajectory, kinematicsModel);
var cartDetailTrajectory = BuildCartesianRowsFromJointDense(jointDetailTrajectory, kinematicsModel); var cartDetailTrajectory = BuildCartesianRowsFromJointDense(jointDetailTrajectory, kinematicsModel);
TrajectoryExporter.WriteJointTrajectory(Path.Combine(outputDir, "JointTraj.txt"), jointTrajectory); TrajectoryExporter.WriteJointTrajectory(Path.Combine(outputDir, "JointTraj.txt"), jointTrajectory);
TrajectoryExporter.WriteJointDenseTrajectory(Path.Combine(outputDir, "JointDetialTraj.txt"), jointDetailTrajectory); TrajectoryExporter.WriteJointDenseTrajectory(Path.Combine(outputDir, "JointDetialTraj.txt"), jointDetailTrajectory);
TrajectoryExporter.WriteCartesianTrajectory(Path.Combine(outputDir, "CartTraj.txt"), cartTrajectory); TrajectoryExporter.WriteCartesianTrajectory(Path.Combine(outputDir, "CartTraj.txt"), cartTrajectory);
TrajectoryExporter.WriteCartesianDenseTrajectory(Path.Combine(outputDir, "CartDetialTraj.txt"), cartDetailTrajectory); TrajectoryExporter.WriteCartesianDenseTrajectory(Path.Combine(outputDir, "CartDetialTraj.txt"), cartDetailTrajectory);
WriteActualSendArtifacts(outputDir, robot, executionBundle.Result, speedRatio); TrajectoryExporter.WriteShotEvents(Path.Combine(outputDir, "ShotEvents.json"), bundle.ShotTimeline.ShotEvents);
WriteActualSendArtifacts(outputDir, robot, bundle.Result, speedRatio);
_logger?.LogInformation( _logger?.LogInformation(
"saveTrajectory 已导出规划点位: name={TrajectoryName}, outputDir={OutputDir}, jointRows={JointRows}, detailRows={DetailRows}, speedRatio={SpeedRatio}", "saveTrajectory 已导出规划点位: name={TrajectoryName}, outputDir={OutputDir}, jointRows={JointRows}, detailRows={DetailRows}, speedRatio={SpeedRatio}",
@@ -120,52 +102,37 @@ public sealed class FlyshotTrajectoryArtifactWriter
{ {
ArgumentNullException.ThrowIfNull(robot); ArgumentNullException.ThrowIfNull(robot);
var preparedExecution = result.PreparedFlyshotExecution; if (result.DenseJointTrajectory is null)
if (result.DenseJointTrajectory is null && preparedExecution is null)
{ {
return; return;
} }
var samples = preparedExecution is null if (speedRatio <= 0.0 || double.IsNaN(speedRatio) || double.IsInfinity(speedRatio))
? J519SendTrajectorySampler.SampleDenseJointTrajectory( {
result.DenseJointTrajectory!, throw new ArgumentOutOfRangeException(nameof(speedRatio), "speed_ratio 必须是有限正数。");
result.Duration.TotalSeconds, }
ActualSendServoPeriodSeconds,
speedRatio: 1.0) var samples = J519SendTrajectorySampler.SampleDenseJointTrajectory(
: preparedExecution.Samples.Select(static sample => new J519SendSample( result.DenseJointTrajectory,
sample.SampleIndex, result.Duration.TotalSeconds,
sample.SendTime, ActualSendServoPeriodSeconds,
sample.TrajectoryTime, speedRatio);
sample.SpeedRatio,
sample.JointsDegrees)).ToArray();
var triggerBindings = preparedExecution is null try
? TriggerSampleBinder.Bind( {
result.TriggerTimeline, TrajectoryLimitValidator.ValidateJ519SendSamples(
robot,
samples, samples,
result.TriggerSampleIndexOffsetCycles) trajectoryName: result.ProgramName);
: preparedExecution.TriggerBindings.Select(static binding => }
new TriggerSampleBinding( catch (Exception e)
binding.Trigger, {
new J519SendSample( _logger?.LogError(e, "ValidateJ519SendSamples 失败,program={ProgramName}", result.ProgramName);
binding.Sample.SampleIndex, }
binding.Sample.SendTime,
binding.Sample.TrajectoryTime,
binding.Sample.SpeedRatio,
binding.Sample.JointsDegrees),
binding.SampleIndex,
binding.FoundInWindow)).ToArray();
TrajectoryLimitValidator.ValidateJ519SendSamples(
robot,
samples,
trajectoryName: result.ProgramName,
validateJerk: false);
var jointRows = new List<IReadOnlyList<double>>(samples.Count); var jointRows = new List<IReadOnlyList<double>>(samples.Count);
List<IReadOnlyList<double>> timingRows = preparedExecution is null var timingRows = new List<IReadOnlyList<double>>(samples.Count);
? new List<IReadOnlyList<double>>(samples.Count) var jerkRows = new List<IReadOnlyList<double>>();
: preparedExecution.TimingRows.Select(static row => (IReadOnlyList<double>)row.ToArray()).ToList();
List<IReadOnlyList<double>> jerkRows = preparedExecution is null
? []
: preparedExecution.JerkRows.Select(static row => (IReadOnlyList<double>)row.ToArray()).ToList();
double? previousSendTime = null; double? previousSendTime = null;
double[]? previousJoints = null; double[]? previousJoints = null;
double[]? previousVelocity = null; double[]? previousVelocity = null;
@@ -174,20 +141,17 @@ public sealed class FlyshotTrajectoryArtifactWriter
foreach (var sample in samples) foreach (var sample in samples)
{ {
jointRows.Add(BuildActualSendJointRow(sample.SendTime, sample.JointsDegrees)); jointRows.Add(BuildActualSendJointRow(sample.SendTime, sample.JointsDegrees));
if (preparedExecution is null) timingRows.Add(J519SendTrajectorySampler.BuildTimingRow(sample));
{
timingRows.Add(J519SendTrajectorySampler.BuildTimingRow(sample, speedRatio, stretchIterationCount: 0));
if (previousSendTime is not null && previousJoints is not null) if (previousSendTime is not null && previousJoints is not null)
{ {
jerkRows.Add(J519SendTrajectorySampler.BuildJerkRow( jerkRows.Add(J519SendTrajectorySampler.BuildJerkRow(
previousSendTime.Value, previousSendTime.Value,
sample.SendTime, sample.SendTime,
previousJoints, previousJoints,
sample.JointsDegrees, sample.JointsDegrees,
ref previousVelocity, ref previousVelocity,
ref previousAcceleration)); ref previousAcceleration));
}
} }
previousSendTime = sample.SendTime; previousSendTime = sample.SendTime;
@@ -197,7 +161,6 @@ public sealed class FlyshotTrajectoryArtifactWriter
WriteDenseRows(Path.Combine(outputDir, "ActualSendJointTraj.txt"), jointRows); WriteDenseRows(Path.Combine(outputDir, "ActualSendJointTraj.txt"), jointRows);
WriteDenseRows(Path.Combine(outputDir, "ActualSendTiming.txt"), timingRows); WriteDenseRows(Path.Combine(outputDir, "ActualSendTiming.txt"), timingRows);
WriteDenseRows(Path.Combine(outputDir, "ActualSendJerkStats.txt"), jerkRows); WriteDenseRows(Path.Combine(outputDir, "ActualSendJerkStats.txt"), jerkRows);
TrajectoryExporter.WriteShotEvents(Path.Combine(outputDir, "ShotEvents.json"), result.ShotEvents, triggerBindings);
} }
/// <summary> /// <summary>

View File

@@ -193,12 +193,10 @@ public sealed class JsonFlyshotTrajectoryStore
["use_do"] = JsonValue.Create(settings.UseDo), ["use_do"] = JsonValue.Create(settings.UseDo),
["io_addr"] = JsonSerializer.SerializeToNode(settings.IoAddresses), ["io_addr"] = JsonSerializer.SerializeToNode(settings.IoAddresses),
["io_keep_cycles"] = JsonValue.Create(settings.IoKeepCycles), ["io_keep_cycles"] = JsonValue.Create(settings.IoKeepCycles),
["trigger_sample_index_offset_cycles"] = JsonValue.Create(settings.TriggerSampleIndexOffsetCycles),
["acc_limit"] = JsonValue.Create(settings.AccLimitScale), ["acc_limit"] = JsonValue.Create(settings.AccLimitScale),
["jerk_limit"] = JsonValue.Create(settings.JerkLimitScale), ["jerk_limit"] = JsonValue.Create(settings.JerkLimitScale),
["adapt_icsp_try_num"] = JsonValue.Create(settings.AdaptIcspTryNum), ["adapt_icsp_try_num"] = JsonValue.Create(settings.AdaptIcspTryNum),
["planning_speed_scale"] = JsonValue.Create(settings.PlanningSpeedScale), ["planning_speed_scale"] = JsonValue.Create(settings.PlanningSpeedScale),
["speed_ratio"] = JsonValue.Create(settings.SpeedRatio),
["smooth_start_stop_timing"] = JsonValue.Create(settings.SmoothStartStopTiming) ["smooth_start_stop_timing"] = JsonValue.Create(settings.SmoothStartStopTiming)
}; };
} }

View File

@@ -95,12 +95,6 @@ public interface IControllerClientCompatService
/// <returns>控制器运行时状态快照。</returns> /// <returns>控制器运行时状态快照。</returns>
ControllerStateSnapshot GetControllerSnapshot(); ControllerStateSnapshot GetControllerSnapshot();
/// <summary>
/// 读取状态页所需的兼容层元数据快照。
/// </summary>
/// <returns>兼容层元数据快照。</returns>
ControllerClientStatusSnapshotMetadata GetStatusSnapshotMetadata();
/// <summary> /// <summary>
/// 获取当前速度倍率。 /// 获取当前速度倍率。
/// </summary> /// </summary>
@@ -163,12 +157,6 @@ public interface IControllerClientCompatService
/// <param name="jointPositions">目标关节位置。</param> /// <param name="jointPositions">目标关节位置。</param>
void MoveJoint(IReadOnlyList<double> jointPositions); void MoveJoint(IReadOnlyList<double> jointPositions);
/// <summary>
/// 按直角坐标移动到目标 TCP 位姿。
/// </summary>
/// <param name="pose">目标位姿 [x,y,z,w,p,r],单位为 mm/deg。</param>
void MovePose(IReadOnlyList<double> pose);
/// <summary> /// <summary>
/// 执行普通轨迹。 /// 执行普通轨迹。
/// </summary> /// </summary>

View File

@@ -53,7 +53,7 @@ internal static class MoveJointTrajectoryGenerator
/// ///
/// 处理流程: /// 处理流程:
/// 1. 根据关节限位计算连续时间律理论最短时长 /// 1. 根据关节限位计算连续时间律理论最短时长
/// 2. 按 speedRatio 缩放规划限位,并将时长对齐到固定伺服采样间隔 /// 2. 按 speedRatio 换算轨迹采样周期,并将时长对齐到整数个采样间隔
/// 3. 用 7 阶平滑点到点时间律生成稠密轨迹点 /// 3. 用 7 阶平滑点到点时间律生成稠密轨迹点
/// 4. 按离散点反查速度、加速度和 jerk,必要时拉长时长重算 /// 4. 按离散点反查速度、加速度和 jerk,必要时拉长时长重算
/// 5. 封装为 TrajectoryResult 返回 /// 5. 封装为 TrajectoryResult 返回
@@ -61,7 +61,7 @@ internal static class MoveJointTrajectoryGenerator
/// <param name="robot">机器人配置,含自由度数和关节限位。</param> /// <param name="robot">机器人配置,含自由度数和关节限位。</param>
/// <param name="startJoints">起始关节角(弧度)。</param> /// <param name="startJoints">起始关节角(弧度)。</param>
/// <param name="targetJoints">目标关节角(弧度)。</param> /// <param name="targetJoints">目标关节角(弧度)。</param>
/// <param name="speedRatio">速度倍率,必须大于 0;在生成阶段折算进有效关节限位。</param> /// <param name="speedRatio">速度倍率,必须大于 0;当前链路中用于换算轨迹采样周期。</param>
/// <param name="logger">可选的诊断日志。</param> /// <param name="logger">可选的诊断日志。</param>
public static TrajectoryResult CreateResult( public static TrajectoryResult CreateResult(
RobotProfile robot, RobotProfile robot,
@@ -84,14 +84,13 @@ internal static class MoveJointTrajectoryGenerator
throw new InvalidOperationException($"MoveJoint expects {robot.DegreesOfFreedom} joints."); throw new InvalidOperationException($"MoveJoint expects {robot.DegreesOfFreedom} joints.");
} }
var planningRobot = ApplySpeedRatioToJointLimits(robot, speedRatio); var requestedDurationSeconds = ResolveDurationSeconds(robot, startJoints, targetJoints);
var requestedDurationSeconds = ResolveDurationSeconds(planningRobot, startJoints, targetJoints); var samplePeriodSeconds = robot.ServoPeriod.TotalSeconds * speedRatio;
var samplePeriodSeconds = robot.ServoPeriod.TotalSeconds;
var durationSeconds = AlignDurationToServoStep(requestedDurationSeconds, samplePeriodSeconds); var durationSeconds = AlignDurationToServoStep(requestedDurationSeconds, samplePeriodSeconds);
var denseJointTrajectory = GenerateDenseTrajectory(startJoints, targetJoints, durationSeconds, samplePeriodSeconds); var denseJointTrajectory = GenerateDenseTrajectory(startJoints, targetJoints, durationSeconds, samplePeriodSeconds);
var stretchCount = 0; var stretchCount = 0;
while (!SatisfiesDiscreteJointLimits(planningRobot, denseJointTrajectory)) while (!SatisfiesDiscreteJointLimits(robot, denseJointTrajectory))
{ {
stretchCount++; stretchCount++;
if (stretchCount > MaxDiscreteLimitStretchIterations) if (stretchCount > MaxDiscreteLimitStretchIterations)
@@ -128,34 +127,6 @@ internal static class MoveJointTrajectoryGenerator
denseJointTrajectory: denseJointTrajectory); denseJointTrajectory: denseJointTrajectory);
} }
/// <summary>
/// 将执行速度倍率折算为 MoveJoint 规划限位,保持最终稠密轨迹仍按物理 8ms 周期生成。
/// </summary>
private static RobotProfile ApplySpeedRatioToJointLimits(RobotProfile robot, double speedRatio)
{
if (Math.Abs(speedRatio - 1.0) < 1e-12)
{
return robot;
}
var scaledLimits = robot.JointLimits
.Select(limit => new JointLimit(
limit.JointName,
limit.VelocityLimit * speedRatio,
limit.AccelerationLimit * speedRatio * speedRatio,
limit.JerkLimit * speedRatio * speedRatio * speedRatio))
.ToArray();
return new RobotProfile(
name: robot.Name,
modelPath: robot.ModelPath,
degreesOfFreedom: robot.DegreesOfFreedom,
jointLimits: scaledLimits,
jointCouplings: robot.JointCouplings,
servoPeriod: robot.ServoPeriod,
triggerPeriod: robot.TriggerPeriod);
}
/// <summary> /// <summary>
/// 根据 7 阶平滑点到点时间律和每轴限位,计算 MoveJoint 理论最短时长。 /// 根据 7 阶平滑点到点时间律和每轴限位,计算 MoveJoint 理论最短时长。
/// ///

View File

@@ -1,353 +0,0 @@
using Flyshot.Core.Domain;
using Microsoft.Extensions.Logging;
namespace Flyshot.ControllerClientCompat;
/// <summary>
/// MovePose 直角坐标轨迹生成器。
/// 将起始 TCP 位姿到目标 TCP 位姿的单段运动,按保守直角速度、加速度和 jerk 限制生成 8ms 稠密点。
/// </summary>
internal static class MovePoseTrajectoryGenerator
{
/// <summary>
/// 直角位置轴最大速度,单位为 mm/s。
/// </summary>
private const double LinearVelocityLimit = 50.0;
/// <summary>
/// 直角位置轴最大加速度,单位为 mm/s^2。
/// </summary>
private const double LinearAccelerationLimit = 250.0;
/// <summary>
/// 直角位置轴最大 jerk,单位为 mm/s^3。
/// </summary>
private const double LinearJerkLimit = 1250.0;
/// <summary>
/// 姿态轴最大速度,单位为 deg/s。
/// </summary>
private const double AngularVelocityLimit = 10.0;
/// <summary>
/// 姿态轴最大加速度,单位为 deg/s^2。
/// </summary>
private const double AngularAccelerationLimit = 50.0;
/// <summary>
/// 姿态轴最大 jerk,单位为 deg/s^3。
/// </summary>
private const double AngularJerkLimit = 250.0;
/// <summary>
/// 7 阶平滑点到点时间律的一阶导数最大值。
/// </summary>
private const double SmoothPtpVelocityShapeCoefficient = 2.1875;
/// <summary>
/// 7 阶平滑点到点时间律的二阶导数最大值。
/// </summary>
private const double SmoothPtpAccelerationShapeCoefficient = 7.513188404399293;
/// <summary>
/// 7 阶平滑点到点时间律的三阶导数最大值。
/// </summary>
private const double SmoothPtpJerkShapeCoefficient = 52.5;
/// <summary>
/// 单次 MovePose 最大采样点数上限,避免极低速度倍率生成过大的队列。
/// </summary>
private const int MaxMovePoseSampleCount = 1_000_000;
/// <summary>
/// 离散限位校验允许的浮点容差。
/// </summary>
private const double DiscreteLimitTolerance = 1.000001;
/// <summary>
/// 离散限位校验失败时最多拉长的采样周期次数。
/// </summary>
private const int MaxDiscreteLimitStretchIterations = 10_000;
/// <summary>
/// 计算 MovePose 轨迹的完整结果。
/// </summary>
/// <param name="startPose">起始位姿 [x,y,z,w,p,r],单位为 mm/deg。</param>
/// <param name="targetPose">目标位姿 [x,y,z,w,p,r],单位为 mm/deg。</param>
/// <param name="servoPeriod">J519 伺服发送周期。</param>
/// <param name="speedRatio">速度倍率,必须大于 0。</param>
/// <param name="logger">可选诊断日志。</param>
/// <returns>包含稠密直角轨迹的规划结果。</returns>
public static TrajectoryResult CreateResult(
IReadOnlyList<double> startPose,
IReadOnlyList<double> targetPose,
TimeSpan servoPeriod,
double speedRatio,
ILogger? logger = null)
{
ArgumentNullException.ThrowIfNull(startPose);
ArgumentNullException.ThrowIfNull(targetPose);
EnsurePoseVector(startPose, nameof(startPose));
EnsurePoseVector(targetPose, nameof(targetPose));
if (speedRatio <= 0.0 || double.IsNaN(speedRatio) || double.IsInfinity(speedRatio))
{
throw new InvalidOperationException("Speed ratio must be greater than zero for MovePose execution.");
}
var samplePeriodSeconds = servoPeriod.TotalSeconds;
if (samplePeriodSeconds <= 0.0 || double.IsNaN(samplePeriodSeconds) || double.IsInfinity(samplePeriodSeconds))
{
throw new InvalidOperationException("MovePose servo period must be a finite positive duration.");
}
var requestedDurationSeconds = ResolveDurationSeconds(startPose, targetPose, speedRatio);
var durationSeconds = AlignDurationToServoStep(requestedDurationSeconds, samplePeriodSeconds);
var denseCartesianTrajectory = GenerateDenseTrajectory(startPose, targetPose, durationSeconds, samplePeriodSeconds);
var stretchCount = 0;
while (!SatisfiesDefaultCartesianLimits(denseCartesianTrajectory, speedRatio))
{
stretchCount++;
if (stretchCount > MaxDiscreteLimitStretchIterations)
{
throw new InvalidOperationException("MovePose duration cannot be stretched enough to satisfy Cartesian limits.");
}
// 离散差分以真实下发点为准;不满足时逐周期拉长后重采样。
durationSeconds = AlignDurationToServoStep(durationSeconds + samplePeriodSeconds, samplePeriodSeconds);
denseCartesianTrajectory = GenerateDenseTrajectory(startPose, targetPose, durationSeconds, samplePeriodSeconds);
}
logger?.LogDebug(
"MovePoseTrajectoryGenerator: requestedDuration={RequestedDuration:F4}s, duration={Duration:F4}s, speedRatio={SpeedRatio}, samplePeriod={SamplePeriod:F6}s, sampleCount={SampleCount}, stretchCount={StretchCount}",
requestedDurationSeconds,
durationSeconds,
speedRatio,
samplePeriodSeconds,
denseCartesianTrajectory.Count,
stretchCount);
return new TrajectoryResult(
programName: "move-pose",
method: PlanningMethod.Doubles,
isValid: true,
duration: TimeSpan.FromSeconds(durationSeconds),
shotEvents: Array.Empty<ShotEvent>(),
triggerTimeline: Array.Empty<TrajectoryDoEvent>(),
artifacts: Array.Empty<TrajectoryArtifact>(),
failureReason: null,
usedCache: false,
originalWaypointCount: 2,
plannedWaypointCount: denseCartesianTrajectory.Count,
denseCartesianTrajectory: denseCartesianTrajectory);
}
/// <summary>
/// 检查稠密直角轨迹是否满足默认六维速度、加速度和 jerk 限制。
/// </summary>
/// <param name="rows">稠密轨迹行,每行为 [time,x,y,z,w,p,r]。</param>
/// <param name="speedRatio">生成该轨迹时使用的速度倍率。</param>
/// <returns>满足限制时返回 true,否则返回 false。</returns>
public static bool SatisfiesDefaultCartesianLimits(IReadOnlyList<IReadOnlyList<double>> rows, double speedRatio)
{
if (speedRatio <= 0.0 || double.IsNaN(speedRatio) || double.IsInfinity(speedRatio))
{
throw new InvalidOperationException("Speed ratio must be greater than zero for MovePose limit validation.");
}
double? previousTime = null;
double[]? previousPositions = null;
double[]? previousVelocities = null;
double[]? previousAccelerations = null;
foreach (var row in rows)
{
if (row.Count != 7)
{
throw new InvalidOperationException("MovePose dense trajectory rows must contain time plus six pose values.");
}
var currentTime = row[0];
var currentPositions = row.Skip(1).Take(6).ToArray();
if (previousTime is not null && previousPositions is not null)
{
var dt = currentTime - previousTime.Value;
if (dt <= 0.0)
{
throw new InvalidOperationException("MovePose dense trajectory timestamps must be strictly increasing.");
}
var velocities = new double[6];
var accelerations = new double[6];
for (var index = 0; index < 6; index++)
{
var limits = GetAxisLimits(index, speedRatio);
velocities[index] = (currentPositions[index] - previousPositions[index]) / dt;
if (Math.Abs(velocities[index]) > limits.Velocity * DiscreteLimitTolerance)
{
return false;
}
accelerations[index] = previousVelocities is null
? 0.0
: (velocities[index] - previousVelocities[index]) / dt;
if (Math.Abs(accelerations[index]) > limits.Acceleration * DiscreteLimitTolerance)
{
return false;
}
if (previousAccelerations is not null)
{
var jerk = (accelerations[index] - previousAccelerations[index]) / dt;
if (Math.Abs(jerk) > limits.Jerk * DiscreteLimitTolerance)
{
return false;
}
}
}
previousVelocities = velocities;
previousAccelerations = accelerations;
}
previousTime = currentTime;
previousPositions = currentPositions;
}
return true;
}
/// <summary>
/// 根据 7 阶平滑点到点时间律和直角六维限制计算理论最短时长。
/// </summary>
private static double ResolveDurationSeconds(IReadOnlyList<double> startPose, IReadOnlyList<double> targetPose, double speedRatio)
{
var duration = 0.0;
for (var index = 0; index < 6; index++)
{
var distance = Math.Abs(targetPose[index] - startPose[index]);
if (distance <= 0.0)
{
continue;
}
var limits = GetAxisLimits(index, speedRatio);
var velocityDuration = distance * SmoothPtpVelocityShapeCoefficient / limits.Velocity;
var accelerationDuration = Math.Sqrt(distance * SmoothPtpAccelerationShapeCoefficient / limits.Acceleration);
var jerkDuration = Math.Cbrt(distance * SmoothPtpJerkShapeCoefficient / limits.Jerk);
duration = Math.Max(duration, Math.Max(velocityDuration, Math.Max(accelerationDuration, jerkDuration)));
}
return duration;
}
/// <summary>
/// 生成从起始位姿到目标位姿的稠密等时间隔直角轨迹点序列。
/// </summary>
private static IReadOnlyList<IReadOnlyList<double>> GenerateDenseTrajectory(
IReadOnlyList<double> startPose,
IReadOnlyList<double> targetPose,
double durationSeconds,
double samplePeriodSeconds)
{
var sampleCount = ResolveSampleIntervalCount(durationSeconds, samplePeriodSeconds) + 1;
var rows = new List<IReadOnlyList<double>>(checked((int)sampleCount));
for (var index = 0L; index < sampleCount; index++)
{
var time = Math.Min(index * samplePeriodSeconds, durationSeconds);
rows.Add(CreateRow(time, durationSeconds, startPose, targetPose));
}
return rows;
}
/// <summary>
/// 将请求时长向上对齐到整数个伺服周期。
/// </summary>
private static double AlignDurationToServoStep(double durationSeconds, double samplePeriodSeconds)
{
return ResolveSampleIntervalCount(durationSeconds, samplePeriodSeconds) * samplePeriodSeconds;
}
/// <summary>
/// 计算时长对应的采样间隔数,轨迹至少包含起点和终点两帧。
/// </summary>
private static long ResolveSampleIntervalCount(double durationSeconds, double samplePeriodSeconds)
{
var rawIntervals = durationSeconds / samplePeriodSeconds;
if (double.IsNaN(rawIntervals) || double.IsInfinity(rawIntervals))
{
throw new InvalidOperationException("MovePose sample count is not representable.");
}
var intervals = (long)Math.Ceiling(Math.Max(0.0, rawIntervals) - 1e-9);
intervals = Math.Max(1, intervals);
if (intervals + 1 > MaxMovePoseSampleCount)
{
throw new InvalidOperationException($"MovePose sample count must be between 2 and {MaxMovePoseSampleCount}.");
}
return intervals;
}
/// <summary>
/// 构造单个轨迹行:[time_seconds,x,y,z,w,p,r]。
/// </summary>
private static IReadOnlyList<double> CreateRow(
double timeSeconds,
double durationSeconds,
IReadOnlyList<double> startPose,
IReadOnlyList<double> targetPose)
{
var u = durationSeconds <= 0.0 ? 1.0 : Math.Clamp(timeSeconds / durationSeconds, 0.0, 1.0);
var scale = MoveJointTrajectoryGenerator.EvaluateSmoothPtpPositionScale(u);
var row = new double[7];
row[0] = Math.Round(timeSeconds, 9);
for (var index = 0; index < 6; index++)
{
row[index + 1] = startPose[index] + ((targetPose[index] - startPose[index]) * scale);
}
return row;
}
/// <summary>
/// 获取指定直角轴在当前速度倍率下的有效限制。
/// </summary>
private static CartesianAxisLimit GetAxisLimits(int index, double speedRatio)
{
var linearAxis = index < 3;
var velocity = linearAxis ? LinearVelocityLimit : AngularVelocityLimit;
var acceleration = linearAxis ? LinearAccelerationLimit : AngularAccelerationLimit;
var jerk = linearAxis ? LinearJerkLimit : AngularJerkLimit;
return new CartesianAxisLimit(
velocity * speedRatio,
acceleration * speedRatio * speedRatio,
jerk * speedRatio * speedRatio * speedRatio);
}
/// <summary>
/// 校验位姿向量必须为六维有限数。
/// </summary>
private static void EnsurePoseVector(IReadOnlyList<double> pose, string parameterName)
{
if (pose.Count != 6)
{
throw new ArgumentException("MovePose expects pose [x,y,z,w,p,r].", parameterName);
}
if (pose.Any(static value => double.IsNaN(value) || double.IsInfinity(value)))
{
throw new ArgumentException("MovePose pose values must be finite.", parameterName);
}
}
/// <summary>
/// 表示单个直角轴的有效速度、加速度和 jerk 限制。
/// </summary>
private readonly record struct CartesianAxisLimit(double Velocity, double Acceleration, double Jerk);
}

View File

@@ -12,35 +12,21 @@ public sealed class PlannedExecutionBundle
/// <summary> /// <summary>
/// 初始化一份执行规划结果包。 /// 初始化一份执行规划结果包。
/// </summary> /// </summary>
/// <param name="plannedTrajectory">规划后的原始连续轨迹。</param> /// <param name="plannedTrajectory">规划后的轨迹。</param>
/// <param name="shotTimeline">飞拍触发时间轴。</param> /// <param name="shotTimeline">飞拍触发时间轴。</param>
/// <param name="result">对运行时和监控层暴露的规划结果。</param> /// <param name="result">对运行时和监控层暴露的规划结果。</param>
/// <param name="preparedExecution">飞拍链路预先准备好的最终发送队列。</param> public PlannedExecutionBundle(PlannedTrajectory plannedTrajectory, ShotTimeline shotTimeline, TrajectoryResult result)
/// <param name="executionTrajectory">与运行时结果同口径的连续执行轨迹;为空时复用 plannedTrajectory。</param>
public PlannedExecutionBundle(
PlannedTrajectory plannedTrajectory,
ShotTimeline shotTimeline,
TrajectoryResult result,
FlyshotPreparedExecution? preparedExecution = null,
PlannedTrajectory? executionTrajectory = null)
{ {
PlannedTrajectory = plannedTrajectory ?? throw new ArgumentNullException(nameof(plannedTrajectory)); PlannedTrajectory = plannedTrajectory ?? throw new ArgumentNullException(nameof(plannedTrajectory));
ShotTimeline = shotTimeline ?? throw new ArgumentNullException(nameof(shotTimeline)); ShotTimeline = shotTimeline ?? throw new ArgumentNullException(nameof(shotTimeline));
Result = result ?? throw new ArgumentNullException(nameof(result)); Result = result ?? throw new ArgumentNullException(nameof(result));
PreparedExecution = preparedExecution;
ExecutionTrajectory = executionTrajectory ?? plannedTrajectory;
} }
/// <summary> /// <summary>
/// 获取规划后的原始连续轨迹。 /// 获取规划后的轨迹。
/// </summary> /// </summary>
public PlannedTrajectory PlannedTrajectory { get; } public PlannedTrajectory PlannedTrajectory { get; }
/// <summary>
/// 获取与运行时结果同口径的连续执行轨迹,用于构建最终 8ms 发送队列。
/// </summary>
public PlannedTrajectory ExecutionTrajectory { get; }
/// <summary> /// <summary>
/// 获取飞拍触发时间轴。 /// 获取飞拍触发时间轴。
/// </summary> /// </summary>
@@ -50,9 +36,4 @@ public sealed class PlannedExecutionBundle
/// 获取运行时可消费的规划结果。 /// 获取运行时可消费的规划结果。
/// </summary> /// </summary>
public TrajectoryResult Result { get; } public TrajectoryResult Result { get; }
/// <summary>
/// 获取飞拍链路预先准备好的最终发送队列;普通轨迹与 move_joint 为 null。
/// </summary>
public FlyshotPreparedExecution? PreparedExecution { get; }
} }

View File

@@ -16,12 +16,10 @@ public sealed class CompatibilityRobotSettings
bool useDo, bool useDo,
IEnumerable<int> ioAddresses, IEnumerable<int> ioAddresses,
int ioKeepCycles, int ioKeepCycles,
int triggerSampleIndexOffsetCycles,
double accLimitScale, double accLimitScale,
double jerkLimitScale, double jerkLimitScale,
int adaptIcspTryNum, int adaptIcspTryNum,
double planningSpeedScale = 1.0, double planningSpeedScale = 1.0,
double speedRatio = 1.0,
bool smoothStartStopTiming = true) bool smoothStartStopTiming = true)
{ {
ArgumentNullException.ThrowIfNull(ioAddresses); ArgumentNullException.ThrowIfNull(ioAddresses);
@@ -31,11 +29,6 @@ public sealed class CompatibilityRobotSettings
throw new ArgumentOutOfRangeException(nameof(ioKeepCycles), "IO 保持周期不能为负数。"); throw new ArgumentOutOfRangeException(nameof(ioKeepCycles), "IO 保持周期不能为负数。");
} }
if (triggerSampleIndexOffsetCycles < 0)
{
throw new ArgumentOutOfRangeException(nameof(triggerSampleIndexOffsetCycles), "触发 sample 偏移周期不能为负数。");
}
if (accLimitScale <= 0.0) if (accLimitScale <= 0.0)
{ {
throw new ArgumentOutOfRangeException(nameof(accLimitScale), "加速度倍率必须大于 0。"); throw new ArgumentOutOfRangeException(nameof(accLimitScale), "加速度倍率必须大于 0。");
@@ -51,11 +44,6 @@ public sealed class CompatibilityRobotSettings
throw new ArgumentOutOfRangeException(nameof(planningSpeedScale), "规划速度倍率必须是有限正数。"); throw new ArgumentOutOfRangeException(nameof(planningSpeedScale), "规划速度倍率必须是有限正数。");
} }
if (speedRatio <= 0.0 || speedRatio > 1.0 || double.IsNaN(speedRatio) || double.IsInfinity(speedRatio))
{
throw new ArgumentOutOfRangeException(nameof(speedRatio), "运行时速度倍率必须在 (0, 1] 范围内。");
}
if (adaptIcspTryNum < 0) if (adaptIcspTryNum < 0)
{ {
throw new ArgumentOutOfRangeException(nameof(adaptIcspTryNum), "补点尝试次数不能为负数。"); throw new ArgumentOutOfRangeException(nameof(adaptIcspTryNum), "补点尝试次数不能为负数。");
@@ -71,12 +59,10 @@ public sealed class CompatibilityRobotSettings
UseDo = useDo; UseDo = useDo;
IoAddresses = copiedIoAddresses; IoAddresses = copiedIoAddresses;
IoKeepCycles = ioKeepCycles; IoKeepCycles = ioKeepCycles;
TriggerSampleIndexOffsetCycles = triggerSampleIndexOffsetCycles;
AccLimitScale = accLimitScale; AccLimitScale = accLimitScale;
JerkLimitScale = jerkLimitScale; JerkLimitScale = jerkLimitScale;
AdaptIcspTryNum = adaptIcspTryNum; AdaptIcspTryNum = adaptIcspTryNum;
PlanningSpeedScale = planningSpeedScale; PlanningSpeedScale = planningSpeedScale;
SpeedRatio = speedRatio;
SmoothStartStopTiming = smoothStartStopTiming; SmoothStartStopTiming = smoothStartStopTiming;
} }
@@ -95,12 +81,6 @@ public sealed class CompatibilityRobotSettings
/// </summary> /// </summary>
public int IoKeepCycles { get; } public int IoKeepCycles { get; }
/// <summary>
/// 获取触发绑定到最佳 sample 后,还要再向后偏移的命令周期数。
/// 该值作用在 J519 命令 sample 时间轴,不是 60015 状态反馈周期。
/// </summary>
public int TriggerSampleIndexOffsetCycles { get; }
/// <summary> /// <summary>
/// 获取加速度全局倍率。 /// 获取加速度全局倍率。
/// </summary> /// </summary>
@@ -116,11 +96,6 @@ public sealed class CompatibilityRobotSettings
/// </summary> /// </summary>
public double PlanningSpeedScale { get; } public double PlanningSpeedScale { get; }
/// <summary>
/// 获取运行时默认速度倍率,会在 SetUpRobot 时同步到控制器运行时,供 MoveJoint 和后续执行链路使用。
/// </summary>
public double SpeedRatio { get; }
/// <summary> /// <summary>
/// 获取是否在飞拍执行前对整段时间轴做二次平滑起停重映射。 /// 获取是否在飞拍执行前对整段时间轴做二次平滑起停重映射。
/// </summary> /// </summary>
@@ -209,12 +184,10 @@ public sealed class RobotConfigLoader
useDo: ReadBoolean(robotElement, "use_do", defaultValue: false), useDo: ReadBoolean(robotElement, "use_do", defaultValue: false),
ioAddresses: ReadIntArray(robotElement, "io_addr"), ioAddresses: ReadIntArray(robotElement, "io_addr"),
ioKeepCycles: ReadInt(robotElement, "io_keep_cycles", defaultValue: 0), ioKeepCycles: ReadInt(robotElement, "io_keep_cycles", defaultValue: 0),
triggerSampleIndexOffsetCycles: ReadInt(robotElement, "trigger_sample_index_offset_cycles", defaultValue: 7),
accLimitScale: ReadDouble(robotElement, "acc_limit", defaultValue: 1.0), accLimitScale: ReadDouble(robotElement, "acc_limit", defaultValue: 1.0),
jerkLimitScale: ReadDouble(robotElement, "jerk_limit", defaultValue: 1.0), jerkLimitScale: ReadDouble(robotElement, "jerk_limit", defaultValue: 1.0),
adaptIcspTryNum: ReadInt(robotElement, "adapt_icsp_try_num", defaultValue: 0), adaptIcspTryNum: ReadInt(robotElement, "adapt_icsp_try_num", defaultValue: 0),
planningSpeedScale: ReadDouble(robotElement, "planning_speed_scale", defaultValue: 1.0), planningSpeedScale: ReadDouble(robotElement, "planning_speed_scale", defaultValue: 1.0),
speedRatio: ReadDouble(robotElement, "speed_ratio", defaultValue: 1.0),
smoothStartStopTiming: ReadBoolean(robotElement, "smooth_start_stop_timing", defaultValue: true)); smoothStartStopTiming: ReadBoolean(robotElement, "smooth_start_stop_timing", defaultValue: true));
var programs = new Dictionary<string, FlyshotProgram>(StringComparer.Ordinal); var programs = new Dictionary<string, FlyshotProgram>(StringComparer.Ordinal);
@@ -226,8 +199,8 @@ public sealed class RobotConfigLoader
} }
_logger?.LogInformation( _logger?.LogInformation(
"RobotConfig 加载完成: resolvedPath={ResolvedPath}, useDo={UseDo}, ioKeepCycles={IoKeepCycles}, accLimit={AccLimit}, jerkLimit={JerkLimit}, planningSpeedScale={PlanningSpeedScale}, speedRatio={SpeedRatio}, smoothStartStopTiming={SmoothStartStopTiming}, adaptIcspTryNum={AdaptIcspTryNum}, 程序数={ProgramCount}", "RobotConfig 加载完成: resolvedPath={ResolvedPath}, useDo={UseDo}, ioKeepCycles={IoKeepCycles}, accLimit={AccLimit}, jerkLimit={JerkLimit}, planningSpeedScale={PlanningSpeedScale}, smoothStartStopTiming={SmoothStartStopTiming}, adaptIcspTryNum={AdaptIcspTryNum}, 程序数={ProgramCount}",
resolvedConfigPath, robot.UseDo, robot.IoKeepCycles, robot.AccLimitScale, robot.JerkLimitScale, robot.PlanningSpeedScale, robot.SpeedRatio, robot.SmoothStartStopTiming, robot.AdaptIcspTryNum, programs.Count); resolvedConfigPath, robot.UseDo, robot.IoKeepCycles, robot.AccLimitScale, robot.JerkLimitScale, robot.PlanningSpeedScale, robot.SmoothStartStopTiming, robot.AdaptIcspTryNum, programs.Count);
return new LoadedRobotConfig( return new LoadedRobotConfig(
sourcePath: resolvedConfigPath, sourcePath: resolvedConfigPath,

View File

@@ -1,202 +0,0 @@
namespace Flyshot.Core.Domain;
/// <summary>
/// 表示飞拍链路在进入运行时之前就已经准备完成的最终发送结果。
/// </summary>
public sealed class FlyshotPreparedExecution
{
/// <summary>
/// 初始化一份飞拍最终发送结果。
/// </summary>
/// <param name="samples">最终 8ms 发送点列。</param>
/// <param name="triggerBindings">与最终发送点列对齐的触发绑定结果。</param>
/// <param name="timingRows">与最终发送点列一致的时间映射诊断行。</param>
/// <param name="jerkRows">与最终发送点列一致的跃度诊断行。</param>
/// <param name="requestSpeedRatio">请求的执行倍率。</param>
/// <param name="finalSpeedRatio">最终发送队列采用的采样倍率;请求倍率已折算进规划时为 1。</param>
/// <param name="finalDurationSeconds">最终发送总时长,单位为秒。</param>
/// <param name="stretchIterationCount">历史诊断字段;当前执行侧不自动改写 speed_ratio,因此正常为 0。</param>
public FlyshotPreparedExecution(
IEnumerable<FlyshotPreparedSample> samples,
IEnumerable<FlyshotPreparedTriggerBinding> triggerBindings,
IEnumerable<IReadOnlyList<double>> timingRows,
IEnumerable<IReadOnlyList<double>> jerkRows,
double requestSpeedRatio,
double finalSpeedRatio,
double finalDurationSeconds,
int stretchIterationCount)
{
ArgumentNullException.ThrowIfNull(samples);
ArgumentNullException.ThrowIfNull(triggerBindings);
ArgumentNullException.ThrowIfNull(timingRows);
ArgumentNullException.ThrowIfNull(jerkRows);
if (requestSpeedRatio <= 0.0 || double.IsNaN(requestSpeedRatio) || double.IsInfinity(requestSpeedRatio))
{
throw new ArgumentOutOfRangeException(nameof(requestSpeedRatio), "请求速度倍率必须是有限正数。");
}
if (finalSpeedRatio <= 0.0 || double.IsNaN(finalSpeedRatio) || double.IsInfinity(finalSpeedRatio))
{
throw new ArgumentOutOfRangeException(nameof(finalSpeedRatio), "最终速度倍率必须是有限正数。");
}
if (finalDurationSeconds < 0.0 || double.IsNaN(finalDurationSeconds) || double.IsInfinity(finalDurationSeconds))
{
throw new ArgumentOutOfRangeException(nameof(finalDurationSeconds), "最终发送总时长必须是有限非负数。");
}
if (stretchIterationCount < 0)
{
throw new ArgumentOutOfRangeException(nameof(stretchIterationCount), "倍率改写迭代次数必须是非负整数。");
}
Samples = samples.Select(static sample => sample).ToArray();
TriggerBindings = triggerBindings.Select(static binding => binding).ToArray();
TimingRows = timingRows.Select(static row => row.ToArray()).ToArray();
JerkRows = jerkRows.Select(static row => row.ToArray()).ToArray();
RequestSpeedRatio = requestSpeedRatio;
FinalSpeedRatio = finalSpeedRatio;
FinalDurationSeconds = finalDurationSeconds;
StretchIterationCount = stretchIterationCount;
}
/// <summary>
/// 获取最终 8ms 发送点列。
/// </summary>
public IReadOnlyList<FlyshotPreparedSample> Samples { get; }
/// <summary>
/// 获取与最终发送点列对齐的触发绑定结果。
/// </summary>
public IReadOnlyList<FlyshotPreparedTriggerBinding> TriggerBindings { get; }
/// <summary>
/// 获取与最终发送点列一致的时间映射诊断行。
/// </summary>
public IReadOnlyList<IReadOnlyList<double>> TimingRows { get; }
/// <summary>
/// 获取与最终发送点列一致的跃度诊断行。
/// </summary>
public IReadOnlyList<IReadOnlyList<double>> JerkRows { get; }
/// <summary>
/// 获取请求的执行倍率。
/// </summary>
public double RequestSpeedRatio { get; }
/// <summary>
/// 获取最终发送队列采用的采样倍率;请求倍率保留在 <see cref="RequestSpeedRatio"/> 中用于诊断。
/// </summary>
public double FinalSpeedRatio { get; }
/// <summary>
/// 获取最终发送总时长,单位为秒。
/// </summary>
public double FinalDurationSeconds { get; }
/// <summary>
/// 获取历史诊断字段;当前执行侧不自动改写 speed_ratio,因此正常为 0。
/// </summary>
public int StretchIterationCount { get; }
}
/// <summary>
/// 表示飞拍最终发送队列中的一个 8ms 发送点。
/// </summary>
public sealed class FlyshotPreparedSample
{
/// <summary>
/// 初始化一条最终发送点。
/// </summary>
/// <param name="sampleIndex">发送周期序号。</param>
/// <param name="sendTime">物理发送时间,单位为秒。</param>
/// <param name="trajectoryTime">回映射到规划轨迹的采样时间,单位为秒。</param>
/// <param name="speedRatio">生成该发送点时采用的队列采样倍率,已规划调速路径通常为 1。</param>
/// <param name="jointsDegrees">J519 下发使用的角度制关节目标。</param>
public FlyshotPreparedSample(
long sampleIndex,
double sendTime,
double trajectoryTime,
double speedRatio,
IReadOnlyList<double> jointsDegrees)
{
ArgumentNullException.ThrowIfNull(jointsDegrees);
SampleIndex = sampleIndex;
SendTime = sendTime;
TrajectoryTime = trajectoryTime;
SpeedRatio = speedRatio;
JointsDegrees = jointsDegrees.ToArray();
}
/// <summary>
/// 获取发送周期序号。
/// </summary>
public long SampleIndex { get; }
/// <summary>
/// 获取物理发送时间,单位为秒。
/// </summary>
public double SendTime { get; }
/// <summary>
/// 获取回映射到规划轨迹的采样时间,单位为秒。
/// </summary>
public double TrajectoryTime { get; }
/// <summary>
/// 获取生成该发送点时采用的队列采样倍率。
/// </summary>
public double SpeedRatio { get; }
/// <summary>
/// 获取 J519 下发使用的角度制关节目标。
/// </summary>
public IReadOnlyList<double> JointsDegrees { get; }
}
/// <summary>
/// 表示理论触发事件最终绑定到哪个发送点的结果。
/// </summary>
public sealed class FlyshotPreparedTriggerBinding
{
/// <summary>
/// 初始化一条最终触发绑定结果。
/// </summary>
/// <param name="trigger">理论触发事件。</param>
/// <param name="sample">最终绑定的发送点。</param>
/// <param name="sampleIndex">最终绑定到的发送点索引。</param>
/// <param name="foundInWindow">是否在理论搜索窗口内找到该发送点。</param>
public FlyshotPreparedTriggerBinding(
TrajectoryDoEvent trigger,
FlyshotPreparedSample sample,
int sampleIndex,
bool foundInWindow)
{
Trigger = trigger ?? throw new ArgumentNullException(nameof(trigger));
Sample = sample ?? throw new ArgumentNullException(nameof(sample));
SampleIndex = sampleIndex;
FoundInWindow = foundInWindow;
}
/// <summary>
/// 获取理论触发事件。
/// </summary>
public TrajectoryDoEvent Trigger { get; }
/// <summary>
/// 获取最终绑定的发送点。
/// </summary>
public FlyshotPreparedSample Sample { get; }
/// <summary>
/// 获取最终绑定到的发送点索引。
/// </summary>
public int SampleIndex { get; }
/// <summary>
/// 获取是否在理论搜索窗口内完成绑定。
/// </summary>
public bool FoundInWindow { get; }
}

View File

@@ -22,10 +22,7 @@ public sealed class TrajectoryResult
bool usedCache, bool usedCache,
int originalWaypointCount, int originalWaypointCount,
int plannedWaypointCount, int plannedWaypointCount,
int triggerSampleIndexOffsetCycles = 0, IEnumerable<IReadOnlyList<double>>? denseJointTrajectory = null)
IEnumerable<IReadOnlyList<double>>? denseJointTrajectory = null,
FlyshotPreparedExecution? preparedFlyshotExecution = null,
IEnumerable<IReadOnlyList<double>>? denseCartesianTrajectory = null)
{ {
if (string.IsNullOrWhiteSpace(programName)) if (string.IsNullOrWhiteSpace(programName))
{ {
@@ -47,11 +44,6 @@ public sealed class TrajectoryResult
throw new ArgumentOutOfRangeException(nameof(plannedWaypointCount), "Planned waypoint count must be greater than or equal to the original waypoint count."); throw new ArgumentOutOfRangeException(nameof(plannedWaypointCount), "Planned waypoint count must be greater than or equal to the original waypoint count.");
} }
if (triggerSampleIndexOffsetCycles < 0)
{
throw new ArgumentOutOfRangeException(nameof(triggerSampleIndexOffsetCycles), "Trigger sample index offset cycles must be zero or positive.");
}
ArgumentNullException.ThrowIfNull(shotEvents); ArgumentNullException.ThrowIfNull(shotEvents);
ArgumentNullException.ThrowIfNull(triggerTimeline); ArgumentNullException.ThrowIfNull(triggerTimeline);
ArgumentNullException.ThrowIfNull(artifacts); ArgumentNullException.ThrowIfNull(artifacts);
@@ -61,7 +53,6 @@ public sealed class TrajectoryResult
var copiedTriggerTimeline = triggerTimeline.ToArray(); var copiedTriggerTimeline = triggerTimeline.ToArray();
var copiedArtifacts = artifacts.ToArray(); var copiedArtifacts = artifacts.ToArray();
var copiedDenseJointTrajectory = denseJointTrajectory?.Select(static row => row.ToArray()).ToArray(); var copiedDenseJointTrajectory = denseJointTrajectory?.Select(static row => row.ToArray()).ToArray();
var copiedDenseCartesianTrajectory = denseCartesianTrajectory?.Select(static row => row.ToArray()).ToArray();
ProgramName = programName; ProgramName = programName;
Method = method; Method = method;
@@ -74,10 +65,7 @@ public sealed class TrajectoryResult
UsedCache = usedCache; UsedCache = usedCache;
OriginalWaypointCount = originalWaypointCount; OriginalWaypointCount = originalWaypointCount;
PlannedWaypointCount = plannedWaypointCount; PlannedWaypointCount = plannedWaypointCount;
TriggerSampleIndexOffsetCycles = triggerSampleIndexOffsetCycles;
DenseJointTrajectory = copiedDenseJointTrajectory; DenseJointTrajectory = copiedDenseJointTrajectory;
DenseCartesianTrajectory = copiedDenseCartesianTrajectory;
PreparedFlyshotExecution = preparedFlyshotExecution;
} }
/// <summary> /// <summary>
@@ -146,31 +134,12 @@ public sealed class TrajectoryResult
[JsonPropertyName("plannedWaypointCount")] [JsonPropertyName("plannedWaypointCount")]
public int PlannedWaypointCount { get; } public int PlannedWaypointCount { get; }
/// <summary>
/// Gets the configured command-sample offset applied after trigger binding picks the best sample.
/// </summary>
[JsonPropertyName("triggerSampleIndexOffsetCycles")]
public int TriggerSampleIndexOffsetCycles { get; }
/// <summary> /// <summary>
/// Gets the dense joint trajectory samples where each row is [time, j1, j2, ...]. /// Gets the dense joint trajectory samples where each row is [time, j1, j2, ...].
/// Null when dense sampling was not performed (e.g. simulation fallback). /// Null when dense sampling was not performed (e.g. simulation fallback).
/// </summary> /// </summary>
[JsonPropertyName("denseJointTrajectory")] [JsonPropertyName("denseJointTrajectory")]
public IReadOnlyList<IReadOnlyList<double>>? DenseJointTrajectory { get; } public IReadOnlyList<IReadOnlyList<double>>? DenseJointTrajectory { get; }
/// <summary>
/// 获取稠密直角坐标轨迹采样点,每行格式为 [time, x, y, z, w, p, r]。
/// Null 表示本结果不是直角流式运动。
/// </summary>
[JsonPropertyName("denseCartesianTrajectory")]
public IReadOnlyList<IReadOnlyList<double>>? DenseCartesianTrajectory { get; }
/// <summary>
/// Gets the prepared flyshot execution queue when the flyshot chain has already built the final 8ms send sequence.
/// </summary>
[JsonIgnore]
public FlyshotPreparedExecution? PreparedFlyshotExecution { get; }
} }
/// <summary> /// <summary>
@@ -181,13 +150,7 @@ public sealed class TrajectoryDoEvent
/// <summary> /// <summary>
/// Initializes a validated runtime trigger event. /// Initializes a validated runtime trigger event.
/// </summary> /// </summary>
public TrajectoryDoEvent( public TrajectoryDoEvent(int waypointIndex, double triggerTime, int offsetCycles, int holdCycles, IoAddressGroup addressGroup)
int waypointIndex,
double triggerTime,
int offsetCycles,
int holdCycles,
IoAddressGroup addressGroup,
IEnumerable<double>? referenceJointsDegrees = null)
{ {
if (waypointIndex < 0) if (waypointIndex < 0)
{ {
@@ -209,7 +172,6 @@ public sealed class TrajectoryDoEvent
OffsetCycles = offsetCycles; OffsetCycles = offsetCycles;
HoldCycles = holdCycles; HoldCycles = holdCycles;
AddressGroup = addressGroup ?? throw new ArgumentNullException(nameof(addressGroup)); AddressGroup = addressGroup ?? throw new ArgumentNullException(nameof(addressGroup));
ReferenceJointsDegrees = referenceJointsDegrees?.ToArray();
} }
/// <summary> /// <summary>
@@ -241,13 +203,6 @@ public sealed class TrajectoryDoEvent
/// </summary> /// </summary>
[JsonPropertyName("addressGroup")] [JsonPropertyName("addressGroup")]
public IoAddressGroup AddressGroup { get; } public IoAddressGroup AddressGroup { get; }
/// <summary>
/// Gets the teach waypoint joints converted to degrees.
/// This is used to select the closest real send sample inside the trigger time window.
/// </summary>
[JsonPropertyName("referenceJointsDegrees")]
public IReadOnlyList<double>? ReferenceJointsDegrees { get; }
} }
/// <summary> /// <summary>

View File

@@ -1,7 +1,6 @@
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
using Flyshot.Core.Domain; using Flyshot.Core.Domain;
using Flyshot.Core.Planning.Sampling;
namespace Flyshot.Core.Planning.Export; namespace Flyshot.Core.Planning.Export;
@@ -53,51 +52,13 @@ public static class TrajectoryExporter
/// </summary> /// </summary>
public static void WriteShotEvents(string path, IReadOnlyList<ShotEvent> events) public static void WriteShotEvents(string path, IReadOnlyList<ShotEvent> events)
{ {
WriteShotEvents(path, events, bindings: null);
}
/// <summary>
/// 导出触发事件到 JSON 文件,并可选附带最终绑定到的实际 sample 信息。
/// </summary>
public static void WriteShotEvents(
string path,
IReadOnlyList<ShotEvent> events,
IReadOnlyList<TriggerSampleBinding>? bindings)
{
var bindingByWaypointIndex = bindings?
.GroupBy(static binding => binding.Trigger.WaypointIndex)
.ToDictionary(static group => group.Key, static group => group.First());
var payload = events.Select(e => new var payload = events.Select(e => new
{ {
waypoint_index = e.WaypointIndex, waypoint_index = e.WaypointIndex,
trigger_time = Math.Round(e.TriggerTime, 6), trigger_time = Math.Round(e.TriggerTime, 6),
sample_index = e.SampleIndex, sample_index = e.SampleIndex,
sample_time = Math.Round(e.SampleTime, 6), sample_time = Math.Round(e.SampleTime, 6),
addrs = e.AddressGroup.Addresses.ToList(), addrs = e.AddressGroup.Addresses.ToList()
trigger_window_seconds = bindingByWaypointIndex is not null && bindingByWaypointIndex.TryGetValue(e.WaypointIndex, out var binding)
? Math.Round(TriggerSampleBinder.TriggerBindingToleranceSeconds, 6)
: (double?)null,
selected_sample_index = bindingByWaypointIndex is not null && bindingByWaypointIndex.TryGetValue(e.WaypointIndex, out binding)
? binding.SampleIndex
: (int?)null,
selected_send_time = bindingByWaypointIndex is not null && bindingByWaypointIndex.TryGetValue(e.WaypointIndex, out binding)
? Math.Round(binding.Sample.SendTime, 6)
: (double?)null,
selected_trajectory_time = bindingByWaypointIndex is not null && bindingByWaypointIndex.TryGetValue(e.WaypointIndex, out binding)
? Math.Round(binding.Sample.TrajectoryTime, 6)
: (double?)null,
io_mask = bindingByWaypointIndex is not null && bindingByWaypointIndex.TryGetValue(e.WaypointIndex, out binding)
? ComputeIoValue(binding.Trigger.AddressGroup)
: (ushort?)null,
reference_joints_deg = bindingByWaypointIndex is not null
&& bindingByWaypointIndex.TryGetValue(e.WaypointIndex, out binding)
&& binding.Trigger.ReferenceJointsDegrees is { Count: > 0 }
? binding.Trigger.ReferenceJointsDegrees.Select(static value => Math.Round(value, 6)).ToArray()
: null,
trigger_joints_deg = bindingByWaypointIndex is not null && bindingByWaypointIndex.TryGetValue(e.WaypointIndex, out binding)
? binding.Sample.JointsDegrees.Select(static value => Math.Round(value, 6)).ToArray()
: null
}).ToList(); }).ToList();
var json = JsonSerializer.Serialize(payload, new JsonSerializerOptions var json = JsonSerializer.Serialize(payload, new JsonSerializerOptions
@@ -118,21 +79,4 @@ public static class TrajectoryExporter
File.WriteAllText(path, sb.ToString(), new UTF8Encoding(false)); File.WriteAllText(path, sb.ToString(), new UTF8Encoding(false));
} }
/// <summary>
/// 把 IO 地址组中的地址号映射为 writeIoValue 的位掩码,和运行时 / saveTrajectory 导出保持一致。
/// </summary>
private static ushort ComputeIoValue(IoAddressGroup group)
{
ushort value = 0;
foreach (var addr in group.Addresses)
{
if (addr is >= 1 and <= 16)
{
value |= (ushort)(1 << (addr - 1));
}
}
return value;
}
} }

View File

@@ -1,146 +0,0 @@
using Flyshot.Core.Domain;
namespace Flyshot.Core.Planning.Sampling;
/// <summary>
/// 负责在飞拍进入运行时前构建最终 8ms 发送队列,并执行发送口径的离散限幅校验。
/// </summary>
public static class FlyshotExecutionSendSequenceBuilder
{
/// <summary>
/// 根据连续规划轨迹构建最终发送队列;请求 speedRatio 只作为诊断信息保留。
/// </summary>
/// <param name="robot">机器人关节限值配置。</param>
/// <param name="plannedTrajectory">规划阶段产出的连续轨迹。</param>
/// <param name="result">规划结果。</param>
/// <param name="servoPeriodSeconds">J519 物理发送周期,单位为秒。</param>
/// <param name="requestedSpeedRatio">请求的执行倍率,必须已经在规划阶段折算进轨迹。</param>
/// <param name="samplingSpeedRatio">最终队列采样倍率;为空时使用 1.0,表示直接采样已调速后的规划轨迹。</param>
/// <returns>通过离散校验后的飞拍最终发送结果。</returns>
public static FlyshotPreparedExecution Build(
RobotProfile robot,
PlannedTrajectory plannedTrajectory,
TrajectoryResult result,
double servoPeriodSeconds,
double requestedSpeedRatio,
double? samplingSpeedRatio = null)
{
ArgumentNullException.ThrowIfNull(robot);
ArgumentNullException.ThrowIfNull(plannedTrajectory);
ArgumentNullException.ThrowIfNull(result);
if (requestedSpeedRatio <= 0.0 || double.IsNaN(requestedSpeedRatio) || double.IsInfinity(requestedSpeedRatio))
{
throw new ArgumentOutOfRangeException(nameof(requestedSpeedRatio), "speed_ratio 必须是有限正数。");
}
if (servoPeriodSeconds <= 0.0 || double.IsNaN(servoPeriodSeconds) || double.IsInfinity(servoPeriodSeconds))
{
throw new ArgumentOutOfRangeException(nameof(servoPeriodSeconds), "伺服周期必须是有限正数。");
}
var finalSpeedRatio = samplingSpeedRatio ?? 1.0;
if (finalSpeedRatio <= 0.0 || double.IsNaN(finalSpeedRatio) || double.IsInfinity(finalSpeedRatio))
{
throw new ArgumentOutOfRangeException(nameof(samplingSpeedRatio), "最终队列采样倍率必须是有限正数。");
}
var samples = J519SendTrajectorySampler.SamplePlannedTrajectory(
plannedTrajectory,
servoPeriodSeconds,
finalSpeedRatio);
try
{
TrajectoryLimitValidator.ValidateJ519SendSamples(
robot,
samples,
trajectoryName: result.ProgramName,
validateJerk: false,
strictJerkTolerance: true);
}
catch (InvalidOperationException exception)
{
throw new InvalidOperationException(
$"飞拍最终发送队列在请求 speed_ratio={requestedSpeedRatio:F6} 下离散限幅校验失败。" +
"请降低 planning_speed_scale 或 speed_ratio 后重新规划;执行侧不会在发送阶段临场重采样。",
exception);
}
return BuildPreparedExecution(
result,
samples,
requestedSpeedRatio,
finalSpeedRatio,
stretchIterationCount: 0);
}
/// <summary>
/// 将通过校验的 J519 采样点和触发绑定结果封装为稳定的飞拍执行结果。
/// </summary>
private static FlyshotPreparedExecution BuildPreparedExecution(
TrajectoryResult result,
IReadOnlyList<J519SendSample> samples,
double requestedSpeedRatio,
double finalSpeedRatio,
int stretchIterationCount)
{
var preparedSamples = samples.Select(static sample =>
new FlyshotPreparedSample(
sample.SampleIndex,
sample.SendTime,
sample.TrajectoryTime,
sample.SpeedRatio,
sample.JointsDegrees)).ToArray();
var sampleLookup = preparedSamples.ToDictionary(static sample => sample.SampleIndex);
var triggerBindings = TriggerSampleBinder.Bind(
result.TriggerTimeline,
samples,
result.TriggerSampleIndexOffsetCycles)
.Select(binding =>
new FlyshotPreparedTriggerBinding(
binding.Trigger,
sampleLookup[binding.Sample.SampleIndex],
binding.SampleIndex,
binding.FoundInWindow))
.ToArray();
var timingRows = samples
.Select(sample => J519SendTrajectorySampler.BuildTimingRow(sample, requestedSpeedRatio, stretchIterationCount))
.ToArray();
var jerkRows = BuildJerkRows(samples);
var finalDurationSeconds = preparedSamples.Length == 0 ? 0.0 : preparedSamples[^1].SendTime;
return new FlyshotPreparedExecution(
preparedSamples,
triggerBindings,
timingRows,
jerkRows,
requestedSpeedRatio,
finalSpeedRatio,
finalDurationSeconds,
stretchIterationCount);
}
/// <summary>
/// 为最终发送点列构建跃度诊断行,保证导出工件和运行时发送复用同一份数据。
/// </summary>
private static IReadOnlyList<IReadOnlyList<double>> BuildJerkRows(IReadOnlyList<J519SendSample> samples)
{
var jerkRows = new List<IReadOnlyList<double>>(Math.Max(0, samples.Count - 1));
double[]? previousVelocity = null;
double[]? previousAcceleration = null;
for (var index = 1; index < samples.Count; index++)
{
jerkRows.Add(J519SendTrajectorySampler.BuildJerkRow(
samples[index - 1].SendTime,
samples[index].SendTime,
samples[index - 1].JointsDegrees,
samples[index].JointsDegrees,
ref previousVelocity,
ref previousAcceleration));
}
return jerkRows;
}
}

View File

@@ -1,352 +1,242 @@
using Flyshot.Core.Planning; namespace Flyshot.Core.Planning.Sampling;
namespace Flyshot.Core.Planning.Sampling; /// <summary>
/// 负责把规划层稠密关节轨迹重采样为 J519 物理发送周期上的角度制目标。
/// <summary> /// <para>
/// 负责把规划层关节轨迹重采样为 J519 物理发送周期上的角度制目标。 /// 算法约定:
/// <para> /// 输入的稠密关节轨迹行格式固定为 [time, j1..jN],time 为规划轨迹时间,关节单位为弧度;
/// 算法约定: /// 输出的 J519 采样点按物理伺服周期排列,关节单位转换为角度制,供 UDP 60015 实时下发和离线 ActualSend 文件共用。
/// 输入的稠密关节轨迹行格式固定为 [time, j1..jN],time 为规划轨迹时间,关节单位为弧度; /// </para>
/// 输出的 J519 采样点按物理伺服周期排列,关节单位转换为角度制,供 UDP 60015 实时下发和离线 ActualSend 文件共用。 /// <para>
/// </para> /// 采样点数先按轨迹时间步长 trajectoryStep = servoPeriod * speedRatio 计算:
/// <para> /// sampleCount = ceil(max(0, duration / trajectoryStep - 1e-9)) + 1。
/// 采样点数先按轨迹时间步长 trajectoryStep = servoPeriod * speedRatio 计算: /// 末尾额外保留一个终点钳制周期,确保轨迹时长不是周期整数倍时仍会输出最终点。
/// sampleCount = ceil(max(0, duration / trajectoryStep - 1e-9)) + 1。 /// </para>
/// 末尾额外保留一个终点钳制周期,确保轨迹时长不是周期整数倍时仍会输出最终点。 /// <para>
/// </para> /// 第 k 个采样点的物理发送时间为 sendTime = k * servoPeriod;
/// <para> /// speedRatio 不改变物理发送周期,只用于把发送时间映射回规划轨迹时间:
/// 飞拍最终发送队列优先从 <see cref="PlannedTrajectory"/> 重建连续三次样条并直接取点; /// trajectoryTime = min(sendTime * speedRatio, duration)。
/// 旧稠密点入口仅保留给运行时 fallback 和兼容导出。 /// 之后在原始稠密关节轨迹上按 trajectoryTime 做线性插值,并把每个关节从 rad 转为 deg。
/// 第 k 个采样点的物理发送时间为 sendTime = k * servoPeriod; /// </para>
/// speedRatio 不改变物理发送周期,只用于把发送时间映射回规划轨迹时间: /// <para>
/// trajectoryTime = min(sendTime * speedRatio, duration)。 /// 诊断行也在这里统一生成:Timing 行格式为 sample_index + send_time + trajectory_time + speed_ratio;
/// 之后在连续样条或旧稠密轨迹上求取关节,并把每个关节从 rad 转为 deg。 /// Jerk 行使用相邻发送点上的角度制关节目标做后向差分,依次近似速度、加速度和跃度,格式为
/// </para> /// start_time + end_time + dt + max_abs_jerk + jerk[j1..jN]。
/// <para> /// </para>
/// 诊断行也在这里统一生成:Timing 行格式为 sample_index + send_time + trajectory_time + speed_ratio; /// </summary>
/// Jerk 行使用相邻发送点上的角度制关节目标做后向差分,依次近似速度、加速度和跃度,格式为 public static class J519SendTrajectorySampler
/// start_time + end_time + dt + max_abs_jerk + jerk[j1..jN]。 {
/// </para> /// <summary>
/// </summary> /// 根据 J519 伺服周期和 speed_ratio 生成完整实发采样序列。
public static class J519SendTrajectorySampler /// </summary>
{ /// <param name="denseJointTrajectory">规划层稠密关节轨迹,每行格式为 [time, j1..jN],关节单位为弧度。</param>
/// <summary> /// <param name="durationSeconds">规划轨迹总时长,单位为秒。</param>
/// 根据连续规划样条和 J519 伺服周期生成完整实发采样序列。 /// <param name="servoPeriodSeconds">J519 物理发送周期,单位为秒。</param>
/// </summary> /// <param name="speedRatio">速度倍率;只缩放轨迹采样时间,不改变物理发送周期。</param>
/// <param name="trajectory">规划阶段产出的连续轨迹信息。</param> /// <returns>按 J519 发送周期排列的角度制采样序列。</returns>
/// <param name="servoPeriodSeconds">J519 物理发送周期,单位为秒。</param> public static IReadOnlyList<J519SendSample> SampleDenseJointTrajectory(
/// <param name="speedRatio">速度倍率;只缩放轨迹采样时间,不改变物理发送周期。</param> IReadOnlyList<IReadOnlyList<double>> denseJointTrajectory,
/// <returns>按 J519 发送周期排列的角度制采样序列。</returns> double durationSeconds,
public static IReadOnlyList<J519SendSample> SamplePlannedTrajectory( double servoPeriodSeconds,
PlannedTrajectory trajectory, double speedRatio)
double servoPeriodSeconds, {
double speedRatio) ArgumentNullException.ThrowIfNull(denseJointTrajectory);
{ ValidateInputs(denseJointTrajectory, durationSeconds, servoPeriodSeconds, speedRatio);
ArgumentNullException.ThrowIfNull(trajectory);
ValidatePlannedTrajectoryInputs(trajectory, servoPeriodSeconds, speedRatio); var trajectoryStepSeconds = servoPeriodSeconds * speedRatio;
var sampleCount = CalculateSampleCount(durationSeconds, trajectoryStepSeconds);
var durationSeconds = trajectory.WaypointTimes[^1]; var samples = new List<J519SendSample>((int)Math.Min(sampleCount, int.MaxValue));
var trajectoryStepSeconds = servoPeriodSeconds * speedRatio; var segmentIndex = 0;
var sampleCount = CalculateSampleCount(durationSeconds, trajectoryStepSeconds);
var spline = RebuildSpline(trajectory); for (long sampleIndex = 0; sampleIndex < sampleCount; sampleIndex++)
var samples = new List<J519SendSample>((int)Math.Min(sampleCount, int.MaxValue)); {
// J519 物理周期固定,speed_ratio 只用于把发送时间映射回原始轨迹时间。
for (long sampleIndex = 0; sampleIndex < sampleCount; sampleIndex++) var sendTime = sampleIndex * servoPeriodSeconds;
{ var trajectoryTime = Math.Min(sendTime * speedRatio, durationSeconds);
// 直接在连续样条上按 trajectoryTime 取点,避免先离散再线性回采样引入局部跃度尖峰。 var joints = SampleDenseJointTrajectoryDegrees(denseJointTrajectory, trajectoryTime, ref segmentIndex);
var sendTime = sampleIndex * servoPeriodSeconds; samples.Add(new J519SendSample(sampleIndex, sendTime, trajectoryTime, speedRatio, joints));
var trajectoryTime = Math.Min(sendTime * speedRatio, durationSeconds); }
var joints = spline.Evaluate(trajectoryTime).Select(RadiansToDegrees).ToArray();
samples.Add(new J519SendSample(sampleIndex, sendTime, trajectoryTime, speedRatio, joints)); return samples;
} }
return samples; /// <summary>
} /// 按原始轨迹时长和 speed_ratio 后的轨迹时间步长计算 J519 实发采样数。
/// </summary>
/// <summary> /// <param name="durationSeconds">规划轨迹总时长,单位为秒。</param>
/// 根据 J519 伺服周期和 speed_ratio 生成完整实发采样序列。 /// <param name="trajectoryStepSeconds">每个物理发送周期对应的轨迹时间步长,单位为秒。</param>
/// </summary> /// <returns>包含终点钳制周期的采样点数量。</returns>
/// <param name="denseJointTrajectory">规划层稠密关节轨迹,每行格式为 [time, j1..jN],关节单位为弧度。</param> public static long CalculateSampleCount(double durationSeconds, double trajectoryStepSeconds)
/// <param name="durationSeconds">规划轨迹总时长,单位为秒。</param> {
/// <param name="servoPeriodSeconds">J519 物理发送周期,单位为秒。</param> if (durationSeconds < 0.0)
/// <param name="speedRatio">速度倍率;只缩放轨迹采样时间,不改变物理发送周期。</param> {
/// <returns>按 J519 发送周期排列的角度制采样序列。</returns> throw new ArgumentOutOfRangeException(nameof(durationSeconds), "轨迹时长不能为负数。");
public static IReadOnlyList<J519SendSample> SampleDenseJointTrajectory( }
IReadOnlyList<IReadOnlyList<double>> denseJointTrajectory,
double durationSeconds, if (trajectoryStepSeconds <= 0.0 || double.IsNaN(trajectoryStepSeconds) || double.IsInfinity(trajectoryStepSeconds))
double servoPeriodSeconds, {
double speedRatio) throw new ArgumentOutOfRangeException(nameof(trajectoryStepSeconds), "轨迹采样步长必须是有限正数。");
{ }
ArgumentNullException.ThrowIfNull(denseJointTrajectory);
ValidateInputs(denseJointTrajectory, durationSeconds, servoPeriodSeconds, speedRatio); // 非周期整数倍时多保留一个终点钳制周期,和真实 J519 下发序列保持一致。
return (long)Math.Ceiling(Math.Max(0.0, (durationSeconds / trajectoryStepSeconds) - 1e-9)) + 1;
var trajectoryStepSeconds = servoPeriodSeconds * speedRatio; }
var sampleCount = CalculateSampleCount(durationSeconds, trajectoryStepSeconds);
var samples = new List<J519SendSample>((int)Math.Min(sampleCount, int.MaxValue)); /// <summary>
var segmentIndex = 0; /// 构造实发时间映射文本行,格式为 sample_index + send_time + trajectory_time + speed_ratio。
/// </summary>
for (long sampleIndex = 0; sampleIndex < sampleCount; sampleIndex++) /// <param name="sample">待写出的 J519 实发采样点。</param>
{ /// <returns>与 ActualSendTiming.txt 兼容的数值行。</returns>
// J519 物理周期固定,speed_ratio 只用于把发送时间映射回原始轨迹时间。 public static IReadOnlyList<double> BuildTimingRow(J519SendSample sample)
var sendTime = sampleIndex * servoPeriodSeconds; {
var trajectoryTime = Math.Min(sendTime * speedRatio, durationSeconds); ArgumentNullException.ThrowIfNull(sample);
var joints = SampleDenseJointTrajectoryDegrees(denseJointTrajectory, trajectoryTime, ref segmentIndex); return
samples.Add(new J519SendSample(sampleIndex, sendTime, trajectoryTime, speedRatio, joints)); [
} sample.SampleIndex,
Math.Round(sample.SendTime, 6),
return samples; Math.Round(sample.TrajectoryTime, 6),
} Math.Round(sample.SpeedRatio, 6)
];
/// <summary> }
/// 按原始轨迹时长和 speed_ratio 后的轨迹时间步长计算 J519 实发采样数。
/// </summary> /// <summary>
/// <param name="durationSeconds">规划轨迹总时长,单位为秒。</param> /// 构造相邻发送点之间的角度制跃度统计行。
/// <param name="trajectoryStepSeconds">每个物理发送周期对应的轨迹时间步长,单位为秒。</param> /// </summary>
/// <returns>包含终点钳制周期的采样点数量。</returns> /// <param name="previousTime">上一帧发送时间,单位为秒。</param>
public static long CalculateSampleCount(double durationSeconds, double trajectoryStepSeconds) /// <param name="currentTime">当前帧发送时间,单位为秒。</param>
{ /// <param name="previousJoints">上一帧角度制关节目标。</param>
if (durationSeconds < 0.0) /// <param name="currentJoints">当前帧角度制关节目标。</param>
{ /// <param name="previousVelocity">上一帧关节速度,调用后更新为当前帧速度。</param>
throw new ArgumentOutOfRangeException(nameof(durationSeconds), "轨迹时长不能为负数。"); /// <param name="previousAcceleration">上一帧关节加速度,调用后更新为当前帧加速度。</param>
} /// <returns>与 ActualSendJerkStats.txt 兼容的数值行。</returns>
public static IReadOnlyList<double> BuildJerkRow(
if (trajectoryStepSeconds <= 0.0 || double.IsNaN(trajectoryStepSeconds) || double.IsInfinity(trajectoryStepSeconds)) double previousTime,
{ double currentTime,
throw new ArgumentOutOfRangeException(nameof(trajectoryStepSeconds), "轨迹采样步长必须是有限正数。"); IReadOnlyList<double> previousJoints,
} IReadOnlyList<double> currentJoints,
ref double[]? previousVelocity,
// 非周期整数倍时多保留一个终点钳制周期,和真实 J519 下发序列保持一致。 ref double[]? previousAcceleration)
return (long)Math.Ceiling(Math.Max(0.0, (durationSeconds / trajectoryStepSeconds) - 1e-9)) + 1; {
} ArgumentNullException.ThrowIfNull(previousJoints);
ArgumentNullException.ThrowIfNull(currentJoints);
/// <summary>
/// 构造实发时间映射文本行,格式为 sample_index + send_time + trajectory_time + final_speed_ratio。 var dt = currentTime - previousTime;
/// </summary> if (dt <= 0.0)
/// <param name="sample">待写出的 J519 实发采样点。</param> {
/// <returns>与 ActualSendTiming.txt 兼容的数值行。</returns> dt = 1e-9;
public static IReadOnlyList<double> BuildTimingRow(J519SendSample sample) }
{
ArgumentNullException.ThrowIfNull(sample); var jointCount = currentJoints.Count;
return var currentVelocity = new double[jointCount];
[ var currentAcceleration = new double[jointCount];
sample.SampleIndex, var currentJerk = new double[jointCount];
Math.Round(sample.SendTime, 6), var maxAbsJerk = 0.0;
Math.Round(sample.TrajectoryTime, 6),
Math.Round(sample.SpeedRatio, 6) for (var index = 0; index < jointCount; index++)
]; {
} currentVelocity[index] = (currentJoints[index] - previousJoints[index]) / dt;
if (previousVelocity is not null)
/// <summary> {
/// 构造带倍率诊断信息的实发时间映射文本行。 currentAcceleration[index] = (currentVelocity[index] - previousVelocity[index]) / dt;
/// </summary> }
/// <param name="sample">待写出的 J519 实发采样点。</param>
/// <param name="requestedSpeedRatio">用户请求的执行倍率。</param> if (previousAcceleration is not null)
/// <param name="stretchIterationCount">历史诊断字段;当前执行侧不自动改写 speed_ratio,因此正常为 0。</param> {
/// <returns>前 4 列兼容 ActualSendTiming.txt,后续列记录请求倍率和倍率改写次数。</returns> currentJerk[index] = (currentAcceleration[index] - previousAcceleration[index]) / dt;
public static IReadOnlyList<double> BuildTimingRow( maxAbsJerk = Math.Max(maxAbsJerk, Math.Abs(currentJerk[index]));
J519SendSample sample, }
double requestedSpeedRatio, }
int stretchIterationCount)
{ previousVelocity = currentVelocity;
if (requestedSpeedRatio <= 0.0 || double.IsNaN(requestedSpeedRatio) || double.IsInfinity(requestedSpeedRatio)) previousAcceleration = currentAcceleration;
{
throw new ArgumentOutOfRangeException(nameof(requestedSpeedRatio), "请求 speed_ratio 必须是有限正数。"); var row = new double[jointCount + 4];
} row[0] = Math.Round(previousTime, 6);
row[1] = Math.Round(currentTime, 6);
if (stretchIterationCount < 0) row[2] = Math.Round(dt, 6);
{ row[3] = Math.Round(maxAbsJerk, 6);
throw new ArgumentOutOfRangeException(nameof(stretchIterationCount), "倍率改写次数不能为负数。"); for (var index = 0; index < jointCount; index++)
} {
row[index + 4] = Math.Round(currentJerk[index], 6);
var legacyRow = BuildTimingRow(sample); }
return
[ return row;
legacyRow[0], }
legacyRow[1],
legacyRow[2], /// <summary>
legacyRow[3], /// 在稠密关节轨迹上按时间线性插值,并转换成 J519 下发使用的角度制目标。
Math.Round(requestedSpeedRatio, 6), /// </summary>
stretchIterationCount private static double[] SampleDenseJointTrajectoryDegrees(
]; IReadOnlyList<IReadOnlyList<double>> denseJointTrajectory,
} double trajectoryTime,
ref int segmentIndex)
/// <summary> {
/// 构造相邻发送点之间的角度制跃度统计行。 if (denseJointTrajectory.Count == 1 || trajectoryTime <= denseJointTrajectory[0][0])
/// </summary> {
/// <param name="previousTime">上一帧发送时间,单位为秒。</param> return denseJointTrajectory[0].Skip(1).Select(RadiansToDegrees).ToArray();
/// <param name="currentTime">当前帧发送时间,单位为秒。</param> }
/// <param name="previousJoints">上一帧角度制关节目标。</param>
/// <param name="currentJoints">当前帧角度制关节目标。</param> var lastIndex = denseJointTrajectory.Count - 1;
/// <param name="previousVelocity">上一帧关节速度,调用后更新为当前帧速度。</param> if (trajectoryTime >= denseJointTrajectory[lastIndex][0])
/// <param name="previousAcceleration">上一帧关节加速度,调用后更新为当前帧加速度。</param> {
/// <returns>与 ActualSendJerkStats.txt 兼容的数值行。</returns> return denseJointTrajectory[lastIndex].Skip(1).Select(RadiansToDegrees).ToArray();
public static IReadOnlyList<double> BuildJerkRow( }
double previousTime,
double currentTime, while (segmentIndex < lastIndex - 1 && denseJointTrajectory[segmentIndex + 1][0] < trajectoryTime)
IReadOnlyList<double> previousJoints, {
IReadOnlyList<double> currentJoints, segmentIndex++;
ref double[]? previousVelocity, }
ref double[]? previousAcceleration)
{ var start = denseJointTrajectory[segmentIndex];
ArgumentNullException.ThrowIfNull(previousJoints); var end = denseJointTrajectory[segmentIndex + 1];
ArgumentNullException.ThrowIfNull(currentJoints); var startTime = start[0];
var endTime = end[0];
var dt = currentTime - previousTime; var segmentDuration = endTime - startTime;
if (dt <= 0.0) var alpha = segmentDuration <= 0.0 ? 0.0 : (trajectoryTime - startTime) / segmentDuration;
{ var joints = new double[start.Count - 1];
dt = 1e-9; for (var index = 0; index < joints.Length; index++)
} {
joints[index] = RadiansToDegrees(start[index + 1] + ((end[index + 1] - start[index + 1]) * alpha));
var jointCount = currentJoints.Count; }
var currentVelocity = new double[jointCount];
var currentAcceleration = new double[jointCount]; return joints;
var currentJerk = new double[jointCount]; }
var maxAbsJerk = 0.0;
/// <summary>
for (var index = 0; index < jointCount; index++) /// 校验 J519 实发采样的基础输入,避免错误时间轴进入运行时链路。
{ /// </summary>
currentVelocity[index] = (currentJoints[index] - previousJoints[index]) / dt; private static void ValidateInputs(
if (previousVelocity is not null) IReadOnlyList<IReadOnlyList<double>> denseJointTrajectory,
{ double durationSeconds,
currentAcceleration[index] = (currentVelocity[index] - previousVelocity[index]) / dt; double servoPeriodSeconds,
} double speedRatio)
{
if (previousAcceleration is not null) if (denseJointTrajectory.Count == 0)
{ {
currentJerk[index] = (currentAcceleration[index] - previousAcceleration[index]) / dt; throw new InvalidOperationException("稠密关节轨迹为空。");
maxAbsJerk = Math.Max(maxAbsJerk, Math.Abs(currentJerk[index])); }
}
} if (durationSeconds < 0.0)
{
previousVelocity = currentVelocity; throw new ArgumentOutOfRangeException(nameof(durationSeconds), "轨迹时长不能为负数。");
previousAcceleration = currentAcceleration; }
var row = new double[jointCount + 4]; if (servoPeriodSeconds <= 0.0 || double.IsNaN(servoPeriodSeconds) || double.IsInfinity(servoPeriodSeconds))
row[0] = Math.Round(previousTime, 6); {
row[1] = Math.Round(currentTime, 6); throw new ArgumentOutOfRangeException(nameof(servoPeriodSeconds), "J519 伺服周期必须是有限正数。");
row[2] = Math.Round(dt, 6); }
row[3] = Math.Round(maxAbsJerk, 6);
for (var index = 0; index < jointCount; index++) if (speedRatio <= 0.0 || double.IsNaN(speedRatio) || double.IsInfinity(speedRatio))
{ {
row[index + 4] = Math.Round(currentJerk[index], 6); throw new ArgumentOutOfRangeException(nameof(speedRatio), "speed_ratio 必须是有限正数。");
} }
}
return row;
} /// <summary>
/// 角度单位转换:rad -> deg。
/// <summary> /// </summary>
/// 在稠密关节轨迹上按时间线性插值,并转换成 J519 下发使用的角度制目标。 private static double RadiansToDegrees(double radians)
/// </summary> {
private static double[] SampleDenseJointTrajectoryDegrees( return radians * 180.0 / Math.PI;
IReadOnlyList<IReadOnlyList<double>> denseJointTrajectory, }
double trajectoryTime, }
ref int segmentIndex)
{
if (denseJointTrajectory.Count == 1 || trajectoryTime <= denseJointTrajectory[0][0])
{
return denseJointTrajectory[0].Skip(1).Select(RadiansToDegrees).ToArray();
}
var lastIndex = denseJointTrajectory.Count - 1;
if (trajectoryTime >= denseJointTrajectory[lastIndex][0])
{
return denseJointTrajectory[lastIndex].Skip(1).Select(RadiansToDegrees).ToArray();
}
while (segmentIndex < lastIndex - 1 && denseJointTrajectory[segmentIndex + 1][0] < trajectoryTime)
{
segmentIndex++;
}
var start = denseJointTrajectory[segmentIndex];
var end = denseJointTrajectory[segmentIndex + 1];
var startTime = start[0];
var endTime = end[0];
var segmentDuration = endTime - startTime;
var alpha = segmentDuration <= 0.0 ? 0.0 : (trajectoryTime - startTime) / segmentDuration;
var joints = new double[start.Count - 1];
for (var index = 0; index < joints.Length; index++)
{
joints[index] = RadiansToDegrees(start[index + 1] + ((end[index + 1] - start[index + 1]) * alpha));
}
return joints;
}
/// <summary>
/// 校验 J519 实发采样的基础输入,避免错误时间轴进入运行时链路。
/// </summary>
private static void ValidateInputs(
IReadOnlyList<IReadOnlyList<double>> denseJointTrajectory,
double durationSeconds,
double servoPeriodSeconds,
double speedRatio)
{
if (denseJointTrajectory.Count == 0)
{
throw new InvalidOperationException("稠密关节轨迹为空。");
}
if (durationSeconds < 0.0)
{
throw new ArgumentOutOfRangeException(nameof(durationSeconds), "轨迹时长不能为负数。");
}
if (servoPeriodSeconds <= 0.0 || double.IsNaN(servoPeriodSeconds) || double.IsInfinity(servoPeriodSeconds))
{
throw new ArgumentOutOfRangeException(nameof(servoPeriodSeconds), "J519 伺服周期必须是有限正数。");
}
if (speedRatio <= 0.0 || double.IsNaN(speedRatio) || double.IsInfinity(speedRatio))
{
throw new ArgumentOutOfRangeException(nameof(speedRatio), "speed_ratio 必须是有限正数。");
}
}
/// <summary>
/// 校验连续规划轨迹采样入口的基础输入,避免异常时间轴进入最终发送队列。
/// </summary>
private static void ValidatePlannedTrajectoryInputs(
PlannedTrajectory trajectory,
double servoPeriodSeconds,
double speedRatio)
{
if (trajectory.WaypointTimes.Count < 2)
{
throw new InvalidOperationException("规划轨迹至少需要两个路点才能重建连续样条。");
}
if (trajectory.WaypointTimes[^1] < 0.0)
{
throw new ArgumentOutOfRangeException(nameof(trajectory), "规划轨迹总时长不能为负数。");
}
if (servoPeriodSeconds <= 0.0 || double.IsNaN(servoPeriodSeconds) || double.IsInfinity(servoPeriodSeconds))
{
throw new ArgumentOutOfRangeException(nameof(servoPeriodSeconds), "J519 伺服周期必须是有限正数。");
}
if (speedRatio <= 0.0 || double.IsNaN(speedRatio) || double.IsInfinity(speedRatio))
{
throw new ArgumentOutOfRangeException(nameof(speedRatio), "speed_ratio 必须是有限正数。");
}
}
/// <summary>
/// 从规划轨迹重建连续三次样条,供飞拍最终发送队列直接采样。
/// </summary>
private static CubicSplineInterpolator RebuildSpline(PlannedTrajectory trajectory)
{
var times = trajectory.WaypointTimes.ToArray();
var values = trajectory.PlannedWaypoints.Select(static waypoint => waypoint.Positions.ToArray()).ToArray();
return new CubicSplineInterpolator(times, values);
}
/// <summary>
/// 角度单位转换:rad -> deg。
/// </summary>
private static double RadiansToDegrees(double radians)
{
return radians * 180.0 / Math.PI;
}
}

View File

@@ -19,18 +19,16 @@ public static class TrajectoryLimitValidator
/// <param name="rows">稠密轨迹行,格式为 time + 关节弧度。</param> /// <param name="rows">稠密轨迹行,格式为 time + 关节弧度。</param>
/// <param name="toleranceMultiplier">限值容差倍率,用于过滤浮点舍入误差。</param> /// <param name="toleranceMultiplier">限值容差倍率,用于过滤浮点舍入误差。</param>
/// <param name="trajectoryName">诊断用轨迹名称。</param> /// <param name="trajectoryName">诊断用轨迹名称。</param>
/// <param name="validateJerk">是否校验离散 jerk;飞拍链路可临时关闭,仅保留速度/加速度约束。</param>
public static void ValidateDenseJointTrajectory( public static void ValidateDenseJointTrajectory(
RobotProfile robot, RobotProfile robot,
IReadOnlyList<IReadOnlyList<double>> rows, IReadOnlyList<IReadOnlyList<double>> rows,
double toleranceMultiplier = DefaultLimitTolerance, double toleranceMultiplier = DefaultLimitTolerance,
string? trajectoryName = null, string? trajectoryName = null)
bool validateJerk = true)
{ {
ArgumentNullException.ThrowIfNull(robot); ArgumentNullException.ThrowIfNull(robot);
ArgumentNullException.ThrowIfNull(rows); ArgumentNullException.ThrowIfNull(rows);
ValidateTolerance(toleranceMultiplier); ValidateTolerance(toleranceMultiplier);
ValidateRows(robot, rows, toleranceMultiplier, trajectoryName ?? "dense-joint-trajectory", validateJerk); ValidateRows(robot, rows, toleranceMultiplier, trajectoryName ?? "dense-joint-trajectory");
} }
/// <summary> /// <summary>
@@ -40,15 +38,11 @@ public static class TrajectoryLimitValidator
/// <param name="samples">J519 发送采样点,关节单位为角度。</param> /// <param name="samples">J519 发送采样点,关节单位为角度。</param>
/// <param name="toleranceMultiplier">限值容差倍率,用于过滤浮点舍入误差。</param> /// <param name="toleranceMultiplier">限值容差倍率,用于过滤浮点舍入误差。</param>
/// <param name="trajectoryName">诊断用轨迹名称。</param> /// <param name="trajectoryName">诊断用轨迹名称。</param>
/// <param name="validateJerk">是否校验离散 jerk;飞拍链路可临时关闭,仅保留速度/加速度约束。</param>
/// <param name="strictJerkTolerance">是否按原始 jerk 限值做硬约束;false 时保留历史宽容口径。</param>
public static void ValidateJ519SendSamples( public static void ValidateJ519SendSamples(
RobotProfile robot, RobotProfile robot,
IReadOnlyList<J519SendSample> samples, IReadOnlyList<J519SendSample> samples,
double toleranceMultiplier = DefaultLimitTolerance, double toleranceMultiplier = DefaultLimitTolerance,
string? trajectoryName = null, string? trajectoryName = null)
bool validateJerk = false,
bool strictJerkTolerance = true)
{ {
ArgumentNullException.ThrowIfNull(robot); ArgumentNullException.ThrowIfNull(robot);
ArgumentNullException.ThrowIfNull(samples); ArgumentNullException.ThrowIfNull(samples);
@@ -67,13 +61,7 @@ public static class TrajectoryLimitValidator
rows.Add(row); rows.Add(row);
} }
ValidateRows( ValidateRows(robot, rows, toleranceMultiplier, trajectoryName ?? "j519-send-trajectory");
robot,
rows,
toleranceMultiplier,
trajectoryName ?? "j519-send-trajectory",
validateJerk,
strictJerkTolerance);
} }
/// <summary> /// <summary>
@@ -94,9 +82,7 @@ public static class TrajectoryLimitValidator
RobotProfile robot, RobotProfile robot,
IReadOnlyList<IReadOnlyList<double>> rows, IReadOnlyList<IReadOnlyList<double>> rows,
double toleranceMultiplier, double toleranceMultiplier,
string trajectoryName, string trajectoryName)
bool validateJerk,
bool strictJerkTolerance = false)
{ {
double? previousTime = null; double? previousTime = null;
double[]? previousPositions = null; double[]? previousPositions = null;
@@ -159,7 +145,7 @@ public static class TrajectoryLimitValidator
jointLimit.AccelerationLimit, jointLimit.AccelerationLimit,
toleranceMultiplier); toleranceMultiplier);
if (validateJerk && previousAccelerations is not null) if (previousAccelerations is not null)
{ {
var jerk = (currentAccelerations[index] - previousAccelerations[index]) / dt; var jerk = (currentAccelerations[index] - previousAccelerations[index]) / dt;
ThrowIfExceeded( ThrowIfExceeded(
@@ -171,7 +157,7 @@ public static class TrajectoryLimitValidator
"Jerk", "Jerk",
jerk, jerk,
jointLimit.JerkLimit, jointLimit.JerkLimit,
strictJerkTolerance ? toleranceMultiplier : toleranceMultiplier * 4.0); toleranceMultiplier*4);
} }
} }

View File

@@ -1,134 +0,0 @@
using Flyshot.Core.Domain;
namespace Flyshot.Core.Planning.Sampling;
/// <summary>
/// 负责把理论触发事件绑定到实际 J519 发送 sample。
/// 绑定规则为:先在理论触发时间前后固定时间窗内筛候选点,再优先选择关节差最小的 sample。
/// </summary>
public static class TriggerSampleBinder
{
/// <summary>
/// 当前触发绑定使用的固定近时窗半宽,单位为秒。
/// 即:以理论触发时间为中心,在前后各 100ms 的范围内寻找最优 sample。
/// </summary>
public const double TriggerBindingToleranceSeconds = 0.1;
/// <summary>
/// 把一组触发事件绑定到采样后的 J519 sample。
/// </summary>
/// <param name="triggers">待绑定的触发事件。</param>
/// <param name="samples">J519 实发 sample。</param>
/// <param name="sampleIndexOffsetCycles">在最佳绑定 sample 基础上继续向后偏移的命令周期数。</param>
/// <returns>按最终 sampleIndex 排序的绑定结果。</returns>
public static IReadOnlyList<TriggerSampleBinding> Bind(
IReadOnlyList<TrajectoryDoEvent> triggers,
IReadOnlyList<J519SendSample> samples,
int sampleIndexOffsetCycles = 0)
{
ArgumentNullException.ThrowIfNull(triggers);
ArgumentNullException.ThrowIfNull(samples);
if (sampleIndexOffsetCycles < 0)
{
throw new ArgumentOutOfRangeException(nameof(sampleIndexOffsetCycles), "触发 sample 偏移周期不能为负数。");
}
var bindings = new List<TriggerSampleBinding>(triggers.Count);
if (triggers.Count == 0 || samples.Count == 0)
{
return bindings;
}
var lastSampleIndex = samples.Count - 1;
foreach (var trigger in triggers)
{
var fallbackIndex = FindClosestTrajectoryTimeSampleIndex(samples, trigger.TriggerTime);
var candidateStart = Math.Max(0, fallbackIndex - 1);
var candidateEnd = Math.Min(lastSampleIndex, fallbackIndex + 1);
var bestIndex = fallbackIndex;
var bestScore = double.PositiveInfinity;
var foundInWindow = false;
for (var sampleIndex = candidateStart; sampleIndex <= candidateEnd; sampleIndex++)
{
var sample = samples[sampleIndex];
if (Math.Abs(sample.TrajectoryTime - trigger.TriggerTime) > TriggerBindingToleranceSeconds)
{
continue;
}
foundInWindow = true;
var score = trigger.ReferenceJointsDegrees is { Count: > 0 }
? ComputeJointDifferenceScore(trigger.ReferenceJointsDegrees, sample.JointsDegrees)
: Math.Abs(sample.TrajectoryTime - trigger.TriggerTime);
if (score < bestScore)
{
bestScore = score;
bestIndex = sampleIndex;
}
}
var finalIndex = Math.Min(lastSampleIndex, bestIndex + sampleIndexOffsetCycles);
bindings.Add(new TriggerSampleBinding(trigger, samples[finalIndex], finalIndex, foundInWindow));
}
return bindings
.OrderBy(static binding => binding.SampleIndex)
.ThenBy(static binding => binding.Trigger.WaypointIndex)
.ToArray();
}
/// <summary>
/// 按轨迹时间找到最接近理论触发时刻的 sample。
/// </summary>
public static int FindClosestTrajectoryTimeSampleIndex(IReadOnlyList<J519SendSample> samples, double triggerTime)
{
ArgumentNullException.ThrowIfNull(samples);
var bestIndex = 0;
var bestDelta = double.PositiveInfinity;
for (var sampleIndex = 0; sampleIndex < samples.Count; sampleIndex++)
{
var delta = Math.Abs(samples[sampleIndex].TrajectoryTime - triggerTime);
if (delta < bestDelta)
{
bestDelta = delta;
bestIndex = sampleIndex;
}
}
return bestIndex;
}
/// <summary>
/// 计算参考示教点与候选 sample 的关节差评分,使用平方和即可稳定排序。
/// </summary>
public static double ComputeJointDifferenceScore(
IReadOnlyList<double>? referenceJointsDegrees,
IReadOnlyList<double> sampleJointsDegrees)
{
if (referenceJointsDegrees is null || referenceJointsDegrees.Count == 0)
{
return 0.0;
}
var jointCount = Math.Min(referenceJointsDegrees.Count, sampleJointsDegrees.Count);
var score = 0.0;
for (var index = 0; index < jointCount; index++)
{
var delta = sampleJointsDegrees[index] - referenceJointsDegrees[index];
score += delta * delta;
}
return score;
}
}
/// <summary>
/// 表示一个理论触发事件最终绑定到哪个 J519 sample 的结果。
/// </summary>
public sealed record TriggerSampleBinding(
TrajectoryDoEvent Trigger,
J519SendSample Sample,
int SampleIndex,
bool FoundInWindow);

View File

@@ -8,22 +8,9 @@ namespace Flyshot.Core.Triggering;
/// 根据规划轨迹和飞拍配置生成触发时间轴,把示教点上的 shot_flags / offset_values / addr /// 根据规划轨迹和飞拍配置生成触发时间轴,把示教点上的 shot_flags / offset_values / addr
/// 映射成带理论时间和离散化时间的 ShotEvent,以及可直接注入伺服流的 TrajectoryDoEvent。 /// 映射成带理论时间和离散化时间的 ShotEvent,以及可直接注入伺服流的 TrajectoryDoEvent。
/// </summary> /// </summary>
/// <remarks>
/// 本构建器只负责“时间轴映射”和“事件打包”,不负责轨迹规划与运行时发送。
/// 其中:
/// - ShotEvent 用于诊断、导出和日志观察;
/// - TrajectoryDoEvent 用于运行时伺服链路注入 DO 脉冲。
/// </remarks>
public sealed class ShotTimelineBuilder public sealed class ShotTimelineBuilder
{ {
/// <summary>
/// 将原始示教点索引映射到规划后时间轴上的时间戳解析器。
/// </summary>
private readonly WaypointTimestampResolver _resolver; private readonly WaypointTimestampResolver _resolver;
/// <summary>
/// 可选日志记录器,用于输出时间轴构建统计信息。
/// </summary>
private readonly ILogger<ShotTimelineBuilder>? _logger; private readonly ILogger<ShotTimelineBuilder>? _logger;
/// <summary> /// <summary>
@@ -45,17 +32,6 @@ public sealed class ShotTimelineBuilder
/// <param name="samplePeriod">稠密采样周期,用于离散化 sample_index 和 sample_time。</param> /// <param name="samplePeriod">稠密采样周期,用于离散化 sample_index 和 sample_time。</param>
/// <param name="useDo">是否生成可注入伺服流的 DO 事件。</param> /// <param name="useDo">是否生成可注入伺服流的 DO 事件。</param>
/// <returns>包含 ShotEvent 和 TrajectoryDoEvent 的触发时间轴。</returns> /// <returns>包含 ShotEvent 和 TrajectoryDoEvent 的触发时间轴。</returns>
/// <exception cref="ArgumentNullException">当 <paramref name="trajectory"/> 为 null 时抛出。</exception>
/// <exception cref="ArgumentOutOfRangeException">
/// 当 <paramref name="holdCycles"/> 小于 0,或 <paramref name="samplePeriod"/> 小于等于 0 时抛出。
/// </exception>
/// <remarks>
/// 时间计算规则:
/// - 理论触发时间:timestamp[i] + offset_values[i] * trigger_period;
/// - 离散采样索引:Round(trigger_time / sample_period);
/// - 离散采样时间:sample_index * sample_period。
/// 该离散化与运行时 8ms 周期下发模型一致,可用于对齐导出与诊断。
/// </remarks>
public ShotTimeline Build(PlannedTrajectory trajectory, int holdCycles, TimeSpan samplePeriod, bool useDo = true) public ShotTimeline Build(PlannedTrajectory trajectory, int holdCycles, TimeSpan samplePeriod, bool useDo = true)
{ {
ArgumentNullException.ThrowIfNull(trajectory); ArgumentNullException.ThrowIfNull(trajectory);
@@ -79,7 +55,6 @@ public sealed class ShotTimelineBuilder
var shotEvents = new List<ShotEvent>(); var shotEvents = new List<ShotEvent>();
var triggerTimeline = new List<TrajectoryDoEvent>(); var triggerTimeline = new List<TrajectoryDoEvent>();
// 遍历原始示教点,仅对 shot_flag=true 的点生成触发事件。
for (int i = 0; i < program.Waypoints.Count; i++) for (int i = 0; i < program.Waypoints.Count; i++)
{ {
if (!program.ShotFlags[i]) if (!program.ShotFlags[i])
@@ -87,7 +62,6 @@ public sealed class ShotTimelineBuilder
continue; continue;
} }
// 先得到连续时间上的理论触发时刻,再映射到离散采样周期。
double triggerTime = timestamps[i] + program.OffsetValues[i] * triggerPeriodSeconds; double triggerTime = timestamps[i] + program.OffsetValues[i] * triggerPeriodSeconds;
int sampleIndex = (int)Math.Round(triggerTime / samplePeriodSeconds); int sampleIndex = (int)Math.Round(triggerTime / samplePeriodSeconds);
double sampleTime = sampleIndex * samplePeriodSeconds; double sampleTime = sampleIndex * samplePeriodSeconds;
@@ -109,8 +83,7 @@ public sealed class ShotTimelineBuilder
triggerTime: triggerTime, triggerTime: triggerTime,
offsetCycles: program.OffsetValues[i], offsetCycles: program.OffsetValues[i],
holdCycles: holdCycles, holdCycles: holdCycles,
addressGroup: addressGroup, addressGroup: addressGroup));
referenceJointsDegrees: program.Waypoints[i].Positions.Select(static radians => radians * 180.0 / Math.PI)));
} }
} }

View File

@@ -113,11 +113,4 @@ public interface IControllerRuntime
/// <param name="result">规划结果。</param> /// <param name="result">规划结果。</param>
/// <param name="finalJointPositions">轨迹执行结束后的关节位置。</param> /// <param name="finalJointPositions">轨迹执行结束后的关节位置。</param>
void ExecuteTrajectory(TrajectoryResult result, IReadOnlyList<double> finalJointPositions); void ExecuteTrajectory(TrajectoryResult result, IReadOnlyList<double> finalJointPositions);
/// <summary>
/// 执行一条已经完成规划的直角坐标轨迹,并更新最终 TCP 位姿。
/// </summary>
/// <param name="result">规划结果,必须包含稠密直角坐标轨迹。</param>
/// <param name="finalPose">轨迹执行结束后的 [x,y,z,w,p,r] 位姿。</param>
void ExecuteCartesianTrajectory(TrajectoryResult result, IReadOnlyList<double> finalPose);
} }

View File

@@ -1,7 +1,6 @@
using System.Diagnostics; using System.Diagnostics;
using System.Text; using System.Text;
using Flyshot.Core.Domain; using Flyshot.Core.Domain;
using Flyshot.Core.Planning.Export;
using Flyshot.Core.Planning.Sampling; using Flyshot.Core.Planning.Sampling;
using Flyshot.Runtime.Common; using Flyshot.Runtime.Common;
using Flyshot.Runtime.Fanuc.Protocol; using Flyshot.Runtime.Fanuc.Protocol;
@@ -487,6 +486,11 @@ public sealed class FanucControllerRuntime : IControllerRuntime, IDisposable
if (!IsSimulationMode && result.DenseJointTrajectory is not null) if (!IsSimulationMode && result.DenseJointTrajectory is not null)
{ {
if (_speedRatio <= 0.0)
{
throw new InvalidOperationException("Speed ratio must be greater than zero for dense J519 execution.");
}
EnsureJ519ReadyForDenseExecution(); EnsureJ519ReadyForDenseExecution();
// 真机模式且存在稠密路点:准备可被 StopMove 取消的同步发送任务。 // 真机模式且存在稠密路点:准备可被 StopMove 取消的同步发送任务。
@@ -538,78 +542,6 @@ public sealed class FanucControllerRuntime : IControllerRuntime, IDisposable
} }
} }
/// <inheritdoc />
public void ExecuteCartesianTrajectory(TrajectoryResult result, IReadOnlyList<double> finalPose)
{
ArgumentNullException.ThrowIfNull(result);
ArgumentNullException.ThrowIfNull(finalPose);
CancellationToken denseSendCancellationToken = default;
CancellationTokenSource? denseSendCancellationSource = null;
var shouldRunDenseTrajectory = false;
_logger?.LogInformation(
"ExecuteCartesianTrajectory 开始: program={ProgramName}, 时长={Duration}s, 稠密采样={HasDense}, speedRatio={SpeedRatio}",
result.ProgramName, result.Duration.TotalSeconds, result.DenseCartesianTrajectory is not null, _speedRatio);
lock (_stateLock)
{
EnsureEnabled();
EnsureValidTrajectory(result);
EnsurePoseCount(finalPose.Count);
CancelSendTaskLocked();
if (!IsSimulationMode && result.DenseCartesianTrajectory is not null)
{
EnsureJ519ReadyForDenseExecution();
// 真机直角模式同样预装完整队列,由机器人状态包节拍驱动出队。
_isInMotion = true;
_sendCts = new CancellationTokenSource();
denseSendCancellationSource = _sendCts;
denseSendCancellationToken = _sendCts.Token;
shouldRunDenseTrajectory = true;
_logger?.LogInformation("ExecuteCartesianTrajectory 开始同步直角稠密发送任务");
}
else
{
if (!IsSimulationMode)
{
var command = new FanucJ519Command(
sequence: 0,
targetValues: BuildCartesianTargetValues(finalPose),
dataStyle: 0);
_j519Client.UpdateCommand(command);
}
_isInMotion = true;
_pose = MergeFinalCartesianPose(finalPose);
_isInMotion = false;
_logger?.LogInformation("ExecuteCartesianTrajectory 完成(单点模式)");
}
}
if (shouldRunDenseTrajectory)
{
try
{
SendDenseCartesianTrajectory(result, finalPose, denseSendCancellationToken);
_logger?.LogInformation("ExecuteCartesianTrajectory 完成(稠密模式)");
}
finally
{
lock (_stateLock)
{
if (ReferenceEquals(_sendCts, denseSendCancellationSource))
{
denseSendCancellationSource?.Dispose();
_sendCts = null;
}
}
}
}
}
/// <summary> /// <summary>
/// 释放运行时持有的所有 Socket 客户端。 /// 释放运行时持有的所有 Socket 客户端。
/// </summary> /// </summary>
@@ -629,26 +561,21 @@ public sealed class FanucControllerRuntime : IControllerRuntime, IDisposable
} }
/// <summary> /// <summary>
/// 稠密轨迹发送任务:预生成完整 J519 命令队列,并等待机器人状态包驱动队列执行完成。 /// 稠密轨迹发送任务:预生成完整 J519 命令队列,并等待机器人状态包按 speed_ratio 推进到执行完成。
/// </summary> /// </summary>
private void SendDenseTrajectory(TrajectoryResult result, IReadOnlyList<double> finalJointPositions, CancellationToken cancellationToken) private void SendDenseTrajectory(TrajectoryResult result, IReadOnlyList<double> finalJointPositions, CancellationToken cancellationToken)
{ {
var triggers = result.TriggerTimeline; var triggers = result.TriggerTimeline;
var servoPeriodSeconds = _robot!.ServoPeriod.TotalSeconds; var servoPeriodSeconds = _robot!.ServoPeriod.TotalSeconds;
var preparedExecution = result.PreparedFlyshotExecution; var speedRatio = _speedRatio;
var durationSeconds = preparedExecution?.FinalDurationSeconds ?? result.Duration.TotalSeconds; var trajectoryStepSeconds = servoPeriodSeconds * speedRatio;
var samples = preparedExecution is null var triggerToleranceSeconds = trajectoryStepSeconds / 2.0;
? J519SendTrajectorySampler.SampleDenseJointTrajectory( var durationSeconds = result.Duration.TotalSeconds;
result.DenseJointTrajectory!, var samples = J519SendTrajectorySampler.SampleDenseJointTrajectory(
durationSeconds, result.DenseJointTrajectory!,
servoPeriodSeconds, durationSeconds,
speedRatio: 1.0) servoPeriodSeconds,
: preparedExecution.Samples.Select(static sample => new J519SendSample( speedRatio);
sample.SampleIndex,
sample.SendTime,
sample.TrajectoryTime,
sample.SpeedRatio,
sample.JointsDegrees)).ToArray();
TrajectoryLimitValidator.ValidateJ519SendSamples( TrajectoryLimitValidator.ValidateJ519SendSamples(
_robot!, _robot!,
samples, samples,
@@ -656,25 +583,8 @@ public sealed class FanucControllerRuntime : IControllerRuntime, IDisposable
var sampleCount = samples.Count; var sampleCount = samples.Count;
_logger?.LogInformation( _logger?.LogInformation(
"SendDenseTrajectory 开始: program={ProgramName}, 采样数={SampleCount}, 时长={Duration}s, runtimeSpeedRatio={SpeedRatio}, 周期={Period}ms, 触发事件数={TriggerCount}", "SendDenseTrajectory 开始: program={ProgramName}, 采样数={SampleCount}, 时长={Duration}s, speedRatio={SpeedRatio}, 周期={Period}ms, 触发事件数={TriggerCount}",
result.ProgramName, sampleCount, durationSeconds, _speedRatio, servoPeriodSeconds * 1000, triggers.Count); result.ProgramName, sampleCount, durationSeconds, speedRatio, servoPeriodSeconds * 1000, triggers.Count);
var triggerBindings = preparedExecution is null
? TriggerSampleBinder.Bind(
triggers,
samples,
result.TriggerSampleIndexOffsetCycles)
: preparedExecution.TriggerBindings.Select(static binding =>
new TriggerSampleBinding(
binding.Trigger,
new J519SendSample(
binding.Sample.SampleIndex,
binding.Sample.SendTime,
binding.Sample.TrajectoryTime,
binding.Sample.SpeedRatio,
binding.Sample.JointsDegrees),
binding.SampleIndex,
binding.FoundInWindow)).ToArray();
var stopwatch = Stopwatch.StartNew(); var stopwatch = Stopwatch.StartNew();
ushort ioValue = 0; ushort ioValue = 0;
@@ -682,7 +592,6 @@ public sealed class FanucControllerRuntime : IControllerRuntime, IDisposable
int holdRemaining = -1; int holdRemaining = -1;
long logInterval = Math.Max(1, sampleCount / 10); long logInterval = Math.Max(1, sampleCount / 10);
int triggerFiredCount = 0; int triggerFiredCount = 0;
var nextBindingIndex = 0;
var commands = new List<FanucJ519Command>(sampleCount); var commands = new List<FanucJ519Command>(sampleCount);
var sentJointRows = new List<IReadOnlyList<double>>(sampleCount); var sentJointRows = new List<IReadOnlyList<double>>(sampleCount);
var sentTimingRows = new List<IReadOnlyList<double>>(sampleCount); var sentTimingRows = new List<IReadOnlyList<double>>(sampleCount);
@@ -692,10 +601,6 @@ public sealed class FanucControllerRuntime : IControllerRuntime, IDisposable
double[]? previousJoints = null; double[]? previousJoints = null;
double[]? previousVelocity = null; double[]? previousVelocity = null;
double[]? previousAcceleration = null; double[]? previousAcceleration = null;
var preparedJerkRows = preparedExecution?.JerkRows;
var diagnosticRequestedSpeedRatio = _speedRatio > 0.0 && !double.IsNaN(_speedRatio) && !double.IsInfinity(_speedRatio)
? _speedRatio
: 1.0;
try try
{ {
@@ -719,30 +624,19 @@ public sealed class FanucControllerRuntime : IControllerRuntime, IDisposable
// 检查当前周期是否有新的触发事件。 // 检查当前周期是否有新的触发事件。
if (holdRemaining < 0 && !clearMaskAfterSend) if (holdRemaining < 0 && !clearMaskAfterSend)
{ {
while (nextBindingIndex < triggerBindings.Count foreach (var trigger in triggers)
&& triggerBindings[nextBindingIndex].SampleIndex < sample.SampleIndex)
{ {
nextBindingIndex++; if (Math.Abs(sample.TrajectoryTime - trigger.TriggerTime) <= triggerToleranceSeconds)
} {
ioMask = ComputeIoValue(trigger.AddressGroup);
if (nextBindingIndex < triggerBindings.Count ioValue = ioMask;
&& triggerBindings[nextBindingIndex].SampleIndex == sample.SampleIndex) holdRemaining = Math.Max(trigger.HoldCycles - 1, 0);
{ triggerFiredCount++;
var binding = triggerBindings[nextBindingIndex]; _logger?.LogInformation(
ioMask = ComputeIoValue(binding.Trigger.AddressGroup); "J519 IO触发: time={Time:F4}s, addr=[{Addr}], holdCycles={HoldCycles}",
ioValue = ioMask; sample.TrajectoryTime, string.Join(",", trigger.AddressGroup.Addresses), trigger.HoldCycles);
holdRemaining = Math.Max(binding.Trigger.HoldCycles - 1, 0); break;
triggerFiredCount++; }
nextBindingIndex++;
_logger?.LogInformation(
"J519 IO触发: waypointIndex={WaypointIndex}, theoryTime={TheoryTime:F4}s, sampleIndex={SampleIndex}, sampleSendTime={SampleSendTime:F4}s, sampleTrajectoryTime={SampleTrajectoryTime:F4}s, addr=[{Addr}], holdCycles={HoldCycles}",
binding.Trigger.WaypointIndex,
binding.Trigger.TriggerTime,
binding.SampleIndex,
binding.Sample.SendTime,
binding.Sample.TrajectoryTime,
string.Join(",", binding.Trigger.AddressGroup.Addresses),
binding.Trigger.HoldCycles);
} }
} }
@@ -756,21 +650,9 @@ public sealed class FanucControllerRuntime : IControllerRuntime, IDisposable
commands.Add(command); commands.Add(command);
sentJointRows.Add(BuildDenseSendJointRow(sample.SendTime, sample.JointsDegrees, ioMask, ioValue)); sentJointRows.Add(BuildDenseSendJointRow(sample.SendTime, sample.JointsDegrees, ioMask, ioValue));
sentTimingRows.Add(preparedExecution is null sentTimingRows.Add(J519SendTrajectorySampler.BuildTimingRow(sample));
? J519SendTrajectorySampler.BuildTimingRow(sample, diagnosticRequestedSpeedRatio, stretchIterationCount: 0)
: J519SendTrajectorySampler.BuildTimingRow(
sample,
preparedExecution.RequestSpeedRatio,
preparedExecution.StretchIterationCount));
if (preparedJerkRows is not null) if (previousSendTime is not null && previousJoints is not null)
{
if (sample.SampleIndex > 0 && sample.SampleIndex - 1 < preparedJerkRows.Count)
{
sentJerkRows.Add(preparedJerkRows[(int)sample.SampleIndex - 1]);
}
}
else if (previousSendTime is not null && previousJoints is not null)
{ {
var jerkRow = J519SendTrajectorySampler.BuildJerkRow( var jerkRow = J519SendTrajectorySampler.BuildJerkRow(
previousSendTime.Value, previousSendTime.Value,
@@ -802,7 +684,7 @@ public sealed class FanucControllerRuntime : IControllerRuntime, IDisposable
sample.TrajectoryTime); sample.TrajectoryTime);
} }
} }
TryWriteDenseSendArtifacts(result, outputDir, sentJointRows, sentTimingRows, sentJerkRows, triggerBindings); TryWriteDenseSendArtifacts(outputDir, sentJointRows, sentTimingRows, sentJerkRows);
// 上层只负责生成完整目标序列,真实出队节拍交给 J519 状态包驱动。 // 上层只负责生成完整目标序列,真实出队节拍交给 J519 状态包驱动。
_j519Client.LoadCommandQueue(commands); _j519Client.LoadCommandQueue(commands);
@@ -831,71 +713,6 @@ public sealed class FanucControllerRuntime : IControllerRuntime, IDisposable
} }
} }
/// <summary>
/// 直角稠密轨迹发送任务:预生成 Data format=0 的 J519 命令队列,并等待状态包驱动执行完成。
/// </summary>
private void SendDenseCartesianTrajectory(TrajectoryResult result, IReadOnlyList<double> finalPose, CancellationToken cancellationToken)
{
var rows = result.DenseCartesianTrajectory ?? throw new InvalidOperationException("Cartesian trajectory requires dense Cartesian samples.");
var commands = new List<FanucJ519Command>(rows.Count);
var sentPoseRows = new List<IReadOnlyList<double>>(rows.Count);
var outputDir = CreateDenseSendOutputDirectory(result.ProgramName);
var stopwatch = Stopwatch.StartNew();
try
{
foreach (var row in rows)
{
cancellationToken.ThrowIfCancellationRequested();
if (row.Count != 7)
{
throw new InvalidOperationException("Cartesian dense trajectory rows must contain time plus six pose values.");
}
var pose = row.Skip(1).Take(6).ToArray();
var command = new FanucJ519Command(
sequence: 0,
targetValues: BuildCartesianTargetValues(pose),
dataStyle: 0,
writeIoType: 2,
writeIoIndex: 1,
writeIoMask: 0,
writeIoValue: 0);
commands.Add(command);
sentPoseRows.Add(BuildDenseSendPoseRow(row[0], pose));
}
TryWriteDenseCartesianSendArtifacts(outputDir, sentPoseRows);
_j519Client.LoadCommandQueue(commands);
if (_j519Client.IsConnected)
{
_j519Client.WaitForCommandQueueDrainedAsync(cancellationToken).GetAwaiter().GetResult();
}
_logger?.LogInformation(
"SendDenseCartesianTrajectory 正常完成: 采样数={SampleCount}, 队列装载耗时={ElapsedMs}ms",
commands.Count,
stopwatch.ElapsedMilliseconds);
}
catch (OperationCanceledException)
{
_logger?.LogWarning(
"SendDenseCartesianTrajectory 被取消: 已生成 {Current}/{Total} 条命令",
commands.Count,
rows.Count);
}
finally
{
lock (_stateLock)
{
_isInMotion = false;
_pose = MergeFinalCartesianPose(finalPose);
}
}
}
/// <summary> /// <summary>
/// 若已有 J519 响应,则在启动稠密轨迹前检查伺服侧是否接受命令并处于系统就绪状态。 /// 若已有 J519 响应,则在启动稠密轨迹前检查伺服侧是否接受命令并处于系统就绪状态。
/// </summary> /// </summary>
@@ -917,7 +734,7 @@ public sealed class FanucControllerRuntime : IControllerRuntime, IDisposable
/// <param name="getLatestResponse">读取最新 J519 状态的委托。</param> /// <param name="getLatestResponse">读取最新 J519 状态的委托。</param>
/// <param name="retryEnableRobot">状态未就绪时触发一次 EnableRobot 重试的委托。</param> /// <param name="retryEnableRobot">状态未就绪时触发一次 EnableRobot 重试的委托。</param>
/// <param name="waitAfterRetry">重试后等待状态刷新的委托。</param> /// <param name="waitAfterRetry">重试后等待状态刷新的委托。</param>
internal void EnsureJ519ReadyForDenseExecutionCore( internal static void EnsureJ519ReadyForDenseExecutionCore(
Func<FanucJ519Response?> getLatestResponse, Func<FanucJ519Response?> getLatestResponse,
Action retryEnableRobot, Action retryEnableRobot,
Action waitAfterRetry) Action waitAfterRetry)
@@ -927,22 +744,13 @@ public sealed class FanucControllerRuntime : IControllerRuntime, IDisposable
ArgumentNullException.ThrowIfNull(waitAfterRetry); ArgumentNullException.ThrowIfNull(waitAfterRetry);
var response = getLatestResponse(); var response = getLatestResponse();
if (response is null || IsJ519ReadyForDenseExecution(response)) if (response is null || IsJ519ReadyForDenseExecution(response))
{ {
return; return;
} }
_logger?.LogInformation("当前机器人不满足执行条件,重新启用机器人");
try retryEnableRobot();
{ waitAfterRetry();
retryEnableRobot();
waitAfterRetry();
}
catch (InvalidOperationException exception)
{
// 重试链路自身失败时,优先保留 J519 状态位诊断,底层连接异常作为 inner exception 继续上抛。
throw new InvalidOperationException(BuildJ519DenseExecutionNotReadyMessage(response), exception);
}
response = getLatestResponse(); response = getLatestResponse();
if (response is null || IsJ519ReadyForDenseExecution(response)) if (response is null || IsJ519ReadyForDenseExecution(response))
@@ -1016,65 +824,26 @@ public sealed class FanucControllerRuntime : IControllerRuntime, IDisposable
return row; return row;
} }
/// <summary>
/// 构造实际发送直角位姿文本行,格式为 send_time + X/Y/Z/W/P/R。
/// </summary>
private static IReadOnlyList<double> BuildDenseSendPoseRow(double sendTime, IReadOnlyList<double> pose)
{
var row = new double[pose.Count + 1];
row[0] = Math.Round(sendTime, 6);
for (var index = 0; index < pose.Count; index++)
{
row[index + 1] = Math.Round(pose[index], 6);
}
return row;
}
/// <summary>
/// 构造 J519 直角命令的 9 个目标槽位,当前现场无扩展轴时 E1/E2/E3 补零。
/// </summary>
private static double[] BuildCartesianTargetValues(IReadOnlyList<double> pose)
{
EnsurePoseCount(pose.Count);
return [pose[0], pose[1], pose[2], pose[3], pose[4], pose[5], 0.0, 0.0, 0.0];
}
/// <summary>
/// 将六维 WPR 直角目标合并回运行时缓存,保留旧 GetPose 仿真路径的 7 维外形。
/// </summary>
private double[] MergeFinalCartesianPose(IReadOnlyList<double> pose)
{
EnsurePoseCount(pose.Count);
return _pose.Length >= 7
? [pose[0], pose[1], pose[2], pose[3], pose[4], pose[5], _pose[6]]
: pose.ToArray();
}
/// <summary> /// <summary>
/// 尝试把实际发送点位、时间映射和跃度统计写入纯文本文件;若落盘失败,只记录日志,不影响运动主流程。 /// 尝试把实际发送点位、时间映射和跃度统计写入纯文本文件;若落盘失败,只记录日志,不影响运动主流程。
/// </summary> /// </summary>
private void TryWriteDenseSendArtifacts( private void TryWriteDenseSendArtifacts(
TrajectoryResult result,
string outputDir, string outputDir,
IReadOnlyList<IReadOnlyList<double>> sentJointRows, IReadOnlyList<IReadOnlyList<double>> sentJointRows,
IReadOnlyList<IReadOnlyList<double>> sentTimingRows, IReadOnlyList<IReadOnlyList<double>> sentTimingRows,
IReadOnlyList<IReadOnlyList<double>> sentJerkRows, IReadOnlyList<IReadOnlyList<double>> sentJerkRows)
IReadOnlyList<TriggerSampleBinding> triggerBindings)
{ {
try try
{ {
WriteDenseRows(Path.Combine(outputDir, "ActualSendJointTraj.txt"), sentJointRows); WriteDenseRows(Path.Combine(outputDir, "ActualSendJointTraj.txt"), sentJointRows);
WriteDenseRows(Path.Combine(outputDir, "ActualSendTiming.txt"), sentTimingRows); WriteDenseRows(Path.Combine(outputDir, "ActualSendTiming.txt"), sentTimingRows);
WriteDenseRows(Path.Combine(outputDir, "ActualSendJerkStats.txt"), sentJerkRows); WriteDenseRows(Path.Combine(outputDir, "ActualSendJerkStats.txt"), sentJerkRows);
TrajectoryExporter.WriteShotEvents(Path.Combine(outputDir, "ShotEvents.json"), result.ShotEvents, triggerBindings);
_logger?.LogInformation( _logger?.LogInformation(
"SendDenseTrajectory 已写出实际发送记录: outputDir={OutputDir}, pointRows={PointRows}, timingRows={TimingRows}, jerkRows={JerkRows}, triggerBindings={TriggerBindings}", "SendDenseTrajectory 已写出实际发送记录: outputDir={OutputDir}, pointRows={PointRows}, timingRows={TimingRows}, jerkRows={JerkRows}",
outputDir, outputDir,
sentJointRows.Count, sentJointRows.Count,
sentTimingRows.Count, sentTimingRows.Count,
sentJerkRows.Count, sentJerkRows.Count);
triggerBindings.Count);
} }
catch (Exception exception) catch (Exception exception)
{ {
@@ -1082,25 +851,6 @@ public sealed class FanucControllerRuntime : IControllerRuntime, IDisposable
} }
} }
/// <summary>
/// 尝试把直角实际发送点位写入文本文件;若落盘失败,只记录日志,不影响运动主流程。
/// </summary>
private void TryWriteDenseCartesianSendArtifacts(string outputDir, IReadOnlyList<IReadOnlyList<double>> sentPoseRows)
{
try
{
WriteDenseRows(Path.Combine(outputDir, "ActualSendPoseTraj.txt"), sentPoseRows);
_logger?.LogInformation(
"SendDenseCartesianTrajectory 已写出实际发送记录: outputDir={OutputDir}, poseRows={PoseRows}",
outputDir,
sentPoseRows.Count);
}
catch (Exception exception)
{
_logger?.LogWarning(exception, "SendDenseCartesianTrajectory 写出实际发送记录失败: outputDir={OutputDir}", outputDir);
}
}
/// <summary> /// <summary>
/// 以旧轨迹文本兼容的空格分隔格式写出数值行。 /// 以旧轨迹文本兼容的空格分隔格式写出数值行。
/// </summary> /// </summary>
@@ -1138,16 +888,15 @@ public sealed class FanucControllerRuntime : IControllerRuntime, IDisposable
/// <summary> /// <summary>
/// 把 IO 地址组中的地址号映射为 writeIoValue 的位掩码。 /// 把 IO 地址组中的地址号映射为 writeIoValue 的位掩码。
/// 手册约定 writeIoIndex=1 时,LSB 对应第 1 个 IO 点,因此地址 N 对应 bit(N-1)。
/// </summary> /// </summary>
internal static ushort ComputeIoValue(IoAddressGroup group) internal static ushort ComputeIoValue(IoAddressGroup group)
{ {
ushort value = 0; ushort value = 0;
foreach (var addr in group.Addresses) foreach (var addr in group.Addresses)
{ {
if (addr is >= 1 and <= 16) if (addr is >= 0 and < 16)
{ {
value |= (ushort)(1 << (addr - 1)); value |= (ushort)(1 << addr);
} }
} }
@@ -1280,17 +1029,6 @@ public sealed class FanucControllerRuntime : IControllerRuntime, IDisposable
} }
} }
/// <summary>
/// 校验直角位姿固定为 [x,y,z,w,p,r] 六维。
/// </summary>
private static void EnsurePoseCount(int poseCount)
{
if (poseCount != 6)
{
throw new InvalidOperationException($"Expected 6 Cartesian pose values but received {poseCount}.");
}
}
/// <summary> /// <summary>
/// 校验机器人已经完成初始化。 /// 校验机器人已经完成初始化。
/// </summary> /// </summary>

View File

@@ -7,7 +7,7 @@ namespace Flyshot.Runtime.Fanuc.Protocol;
/// </summary> /// </summary>
public sealed class FanucJ519Command public sealed class FanucJ519Command
{ {
private readonly double[] _targetValues; private readonly double[] _targetJoints;
/// <summary> /// <summary>
/// 初始化 J519 命令数据。 /// 初始化 J519 命令数据。
@@ -35,53 +35,11 @@ public sealed class FanucJ519Command
ushort writeIoIndex = 1, ushort writeIoIndex = 1,
ushort writeIoMask = 0, ushort writeIoMask = 0,
ushort writeIoValue = 0) ushort writeIoValue = 0)
: this(
sequence,
targetValues: targetJoints,
lastData,
readIoType,
readIoIndex,
readIoMask,
dataStyle,
writeIoType,
writeIoIndex,
writeIoMask,
writeIoValue)
{ {
} ArgumentNullException.ThrowIfNull(targetJoints);
if (targetJoints.Count is <= 0 or > 9)
/// <summary>
/// 初始化 J519 命令数据。
/// </summary>
/// <param name="sequence">命令序号。</param>
/// <param name="targetValues">J519 目标槽位,直角模式为 X/Y/Z/W/P/R/E1/E2/E3,关节模式为 J1..J9。</param>
/// <param name="lastData">是否为最后一帧数据。</param>
/// <param name="readIoType">读取 IO 类型。</param>
/// <param name="readIoIndex">读取 IO 起始索引。</param>
/// <param name="readIoMask">读取 IO 掩码。</param>
/// <param name="dataStyle">目标数据类型,0 为直角坐标,1 为关节坐标。</param>
/// <param name="writeIoType">写入 IO 类型。</param>
/// <param name="writeIoIndex">写入 IO 起始索引。</param>
/// <param name="writeIoMask">写入 IO 掩码。</param>
/// <param name="writeIoValue">写入 IO 数值。</param>
public FanucJ519Command(
uint sequence,
IEnumerable<double> targetValues,
byte lastData = 0,
byte readIoType = 2,
ushort readIoIndex = 1,
ushort readIoMask = 255,
byte dataStyle = 1,
byte writeIoType = 2,
ushort writeIoIndex = 1,
ushort writeIoMask = 0,
ushort writeIoValue = 0)
{
ArgumentNullException.ThrowIfNull(targetValues);
var copiedTargetValues = targetValues.ToArray();
if (copiedTargetValues.Length is <= 0 or > 9)
{ {
throw new ArgumentOutOfRangeException(nameof(targetValues), "J519 目标数据必须包含 1 到 9 个槽位。"); throw new ArgumentOutOfRangeException(nameof(targetJoints), "J519 目标数据必须包含 1 到 9 个槽位。");
} }
Sequence = sequence; Sequence = sequence;
@@ -94,7 +52,7 @@ public sealed class FanucJ519Command
WriteIoIndex = writeIoIndex; WriteIoIndex = writeIoIndex;
WriteIoMask = writeIoMask; WriteIoMask = writeIoMask;
WriteIoValue = writeIoValue; WriteIoValue = writeIoValue;
_targetValues = copiedTargetValues; _targetJoints = targetJoints.ToArray();
} }
/// <summary> /// <summary>
@@ -150,12 +108,7 @@ public sealed class FanucJ519Command
/// <summary> /// <summary>
/// 获取目标关节或扩展轴数据。 /// 获取目标关节或扩展轴数据。
/// </summary> /// </summary>
public IReadOnlyList<double> TargetJoints => _targetValues; public IReadOnlyList<double> TargetJoints => _targetJoints;
/// <summary>
/// 获取 J519 目标槽位,直角模式为 X/Y/Z/W/P/R/E1/E2/E3,关节模式为 J1..J9。
/// </summary>
public IReadOnlyList<double> TargetValues => _targetValues;
} }
/// <summary> /// <summary>

View File

@@ -1,13 +0,0 @@
{
"version": 1,
"isRoot": true,
"tools": {
"dotnet-ef": {
"version": "10.0.7",
"commands": [
"dotnet-ef"
],
"rollForward": false
}
}
}

View File

@@ -431,32 +431,6 @@ public sealed class LegacyHttpApiController : ControllerBase
} }
} }
/// <summary>
/// 以直角坐标 `[x,y,z,w,p,r]` 执行点到点移动。
/// </summary>
/// <param name="pose_data">直角位姿请求体。</param>
/// <returns>旧 FastAPI 层风格的状态响应。</returns>
[HttpPost("/move_pose/")]
public IActionResult MovePose([FromBody] JsonElement pose_data)
{
try
{
var poseRequest = LegacyCartesianPoseRequest.FromJson(pose_data);
var pose = poseRequest.ToPoseArray();
_logger.LogInformation("MovePose 调用: x={X}, y={Y}, z={Z}, w={W}, p={P}, r={R}",
poseRequest.x, poseRequest.y, poseRequest.z, poseRequest.w, poseRequest.p, poseRequest.r);
_compatService.MovePose(pose);
_logger.LogInformation("MovePose 成功");
return Ok(new { status = "robot moved" });
}
catch (Exception exception)
{
_logger.LogError(exception, "MovePose 失败");
return LegacyBadRequest("MovePose failed");
}
}
/// <summary> /// <summary>
/// 兼容旧 `GetNearestIK(pose, seed, ik)` 参数形状。 /// 兼容旧 `GetNearestIK(pose, seed, ik)` 参数形状。
/// </summary> /// </summary>
@@ -900,136 +874,6 @@ public sealed class LegacyJointPositionRequest
public List<double> joints { get; init; } = []; public List<double> joints { get; init; } = [];
} }
/// <summary>
/// 表示 `/move_pose/` 路由使用的直角位姿请求体。
/// </summary>
public sealed class LegacyCartesianPoseRequest
{
/// <summary>
/// MovePose 第一版入口硬限制:TCP X/Y 最大绝对值,单位 mm。
/// </summary>
private const double MaxHorizontalMillimeters = 1000.0;
/// <summary>
/// MovePose 第一版入口硬限制:TCP Z 最小值,单位 mm。
/// </summary>
private const double MinZMillimeters = 0.0;
/// <summary>
/// MovePose 第一版入口硬限制:TCP Z 最大值,单位 mm。
/// </summary>
private const double MaxZMillimeters = 1200.0;
/// <summary>
/// MovePose 第一版入口硬限制:W/R 姿态角最大绝对值,单位 deg。
/// </summary>
private const double MaxRollYawDegrees = 180.0;
/// <summary>
/// MovePose 第一版入口硬限制:P 姿态角最大绝对值,单位 deg。
/// </summary>
private const double MaxPitchDegrees = 90.0;
/// <summary>
/// 获取或设置 TCP X,单位为 mm。
/// </summary>
public double x { get; init; }
/// <summary>
/// 获取或设置 TCP Y,单位为 mm。
/// </summary>
public double y { get; init; }
/// <summary>
/// 获取或设置 TCP Z,单位为 mm。
/// </summary>
public double z { get; init; }
/// <summary>
/// 获取或设置姿态 W,单位为 deg。
/// </summary>
public double w { get; init; }
/// <summary>
/// 获取或设置姿态 P,单位为 deg。
/// </summary>
public double p { get; init; }
/// <summary>
/// 获取或设置姿态 R,单位为 deg。
/// </summary>
public double r { get; init; }
/// <summary>
/// 从原始 JSON 请求体解析直角位姿,并显式拒绝缺字段、null 和非有限数。
/// </summary>
/// <param name="json">原始 JSON 请求体。</param>
/// <returns>解析后的直角位姿请求。</returns>
public static LegacyCartesianPoseRequest FromJson(JsonElement json)
{
if (json.ValueKind != JsonValueKind.Object)
{
throw new ArgumentException("MovePose request body must be an object.");
}
// 旧接口要求请求体必须完整提供 x/y/z/w/p/r,不能让模型绑定把缺字段静默补 0。
return new LegacyCartesianPoseRequest
{
x = ReadRequiredFiniteDouble(json, "x", -MaxHorizontalMillimeters, MaxHorizontalMillimeters),
y = ReadRequiredFiniteDouble(json, "y", -MaxHorizontalMillimeters, MaxHorizontalMillimeters),
z = ReadRequiredFiniteDouble(json, "z", MinZMillimeters, MaxZMillimeters),
w = ReadRequiredFiniteDouble(json, "w", -MaxRollYawDegrees, MaxRollYawDegrees),
p = ReadRequiredFiniteDouble(json, "p", -MaxPitchDegrees, MaxPitchDegrees),
r = ReadRequiredFiniteDouble(json, "r", -MaxRollYawDegrees, MaxRollYawDegrees)
};
}
/// <summary>
/// 转换为兼容层使用的六维位姿数组。
/// </summary>
/// <returns>[x,y,z,w,p,r] 数组。</returns>
public IReadOnlyList<double> ToPoseArray()
{
return [x, y, z, w, p, r];
}
/// <summary>
/// 读取必填有限数值字段。
/// </summary>
/// <param name="json">请求体 JSON 对象。</param>
/// <param name="propertyName">字段名。</param>
/// <param name="minInclusive">允许的最小值。</param>
/// <param name="maxInclusive">允许的最大值。</param>
/// <returns>字段对应的有限 double 数值。</returns>
private static double ReadRequiredFiniteDouble(
JsonElement json,
string propertyName,
double minInclusive,
double maxInclusive)
{
if (!json.TryGetProperty(propertyName, out var property) || property.ValueKind != JsonValueKind.Number)
{
throw new ArgumentException($"MovePose request field '{propertyName}' is required and must be a number.");
}
var value = property.GetDouble();
if (double.IsNaN(value) || double.IsInfinity(value))
{
throw new ArgumentOutOfRangeException(propertyName, "MovePose request values must be finite.");
}
if (value < minInclusive || value > maxInclusive)
{
throw new ArgumentOutOfRangeException(
propertyName,
value,
$"MovePose request field '{propertyName}' must be between {minInclusive} and {maxInclusive}.");
}
return value;
}
}
/// <summary> /// <summary>
/// 表示旧 `/upload_flyshot/` 路由使用的飞拍上传请求体。 /// 表示旧 `/upload_flyshot/` 路由使用的飞拍上传请求体。
/// </summary> /// </summary>

View File

@@ -49,18 +49,23 @@ public sealed class StatusController : ControllerBase
public IActionResult GetSnapshot() public IActionResult GetSnapshot()
{ {
var snapshot = _compatService.GetControllerSnapshot(); var snapshot = _compatService.GetControllerSnapshot();
var metadata = _compatService.GetStatusSnapshotMetadata(); var isSetup = _compatService.IsSetUp;
// 状态页需要在机器人未初始化时仍能打开,因此只有初始化后才读取机器人元数据。
var robotName = isSetup ? _compatService.GetRobotName() : null;
var degreesOfFreedom = isSetup ? _compatService.GetDegreesOfFreedom() : 0;
var uploadedTrajectories = isSetup ? _compatService.ListTrajectoryNames() : Array.Empty<string>();
return Ok(new return Ok(new
{ {
Status = "ok", Status = "ok",
Service = "flyshot-server-host", Service = "flyshot-server-host",
ServerVersion = metadata.ServerVersion, ServerVersion = _compatService.GetServerVersion(),
ClientVersion = metadata.ClientVersion, ClientVersion = _compatService.GetClientVersion(),
IsSetup = metadata.IsSetup, IsSetup = isSetup,
RobotName = metadata.RobotName, RobotName = robotName,
DegreesOfFreedom = metadata.DegreesOfFreedom, DegreesOfFreedom = degreesOfFreedom,
UploadedTrajectories = metadata.UploadedTrajectories, UploadedTrajectories = uploadedTrajectories,
Snapshot = snapshot Snapshot = snapshot
}); });
} }

View File

@@ -25,12 +25,5 @@
"AllowedHeaders": [ "AllowedHeaders": [
"*" "*"
] ]
},
"Kestrel": {
"Endpoints": {
"Http": {
"Url": "http://0.0.0.0:5190"
}
}
} }
} }

View File

@@ -30,12 +30,5 @@
"*" "*"
] ]
}, },
"Kestrel": {
"Endpoints": {
"Http": {
"Url": "http://0.0.0.0:5000"
}
}
},
"AllowedHosts": "*" "AllowedHosts": "*"
} }

View File

@@ -16,17 +16,6 @@ const state = {
history: [] history: []
}; };
const requestBodySamples = {
"POST /move_pose/": {
x: 100.0,
y: 200.0,
z: 300.0,
w: 0.0,
p: 45.0,
r: 0.0
}
};
/** 简单的 escape:把任意字符串安全嵌入 textContent 之外的位置时使用。 */ /** 简单的 escape:把任意字符串安全嵌入 textContent 之外的位置时使用。 */
function escapeHtml(value) { function escapeHtml(value) {
return String(value).replace(/[&<>"']/g, function (ch) { return String(value).replace(/[&<>"']/g, function (ch) {
@@ -148,11 +137,6 @@ function storageKey(op) {
return STORAGE_PREFIX + op.method + ":" + op.path; return STORAGE_PREFIX + op.method + ":" + op.path;
} }
/** 返回指定端点的手工调试样例,处理 JsonElement 等 OpenAPI 无法自动推断字段的接口。 */
function getRequestBodySample(op) {
return requestBodySamples[op.method + " " + op.path] || null;
}
/** 读取本端点最近一次输入;解析失败则当作空。 */ /** 读取本端点最近一次输入;解析失败则当作空。 */
function loadInputs(op) { function loadInputs(op) {
try { try {
@@ -297,7 +281,7 @@ function renderBodyEditor(container, op, savedBody) {
if (savedBody !== undefined && savedBody !== null) { if (savedBody !== undefined && savedBody !== null) {
initialText = typeof savedBody === "string" ? savedBody : JSON.stringify(savedBody, null, 2); initialText = typeof savedBody === "string" ? savedBody : JSON.stringify(savedBody, null, 2);
} else { } else {
const sample = getRequestBodySample(op) || buildSampleFromSchema(op.bodySchema, 0); const sample = buildSampleFromSchema(op.bodySchema, 0);
initialText = sample === null ? "" : JSON.stringify(sample, null, 2); initialText = sample === null ? "" : JSON.stringify(sample, null, 2);
} }
textarea.value = initialText; textarea.value = initialText;

View File

@@ -6,9 +6,6 @@
--text: #172033; --text: #172033;
--muted: #5b667a; --muted: #5b667a;
--accent: #007c89; --accent: #007c89;
--accent-dark: #005d67;
--steel: #2f3b4f;
--steel-soft: #e7ebf1;
--good: #12805c; --good: #12805c;
--warn: #b7791f; --warn: #b7791f;
--bad: #b42318; --bad: #b42318;
@@ -191,289 +188,6 @@ dd {
font-family: inherit; font-family: inherit;
} }
.jog-panel {
grid-column: 1 / -1;
overflow: hidden;
}
.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-body {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
gap: 18px;
padding: 16px;
}
.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-head h3 {
margin: 0;
font-size: 16px;
font-weight: 650;
}
.stick-head span {
color: var(--muted);
font-family: Consolas, "Cascadia Mono", monospace;
font-size: 13px;
}
.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%);
}
.jog-button {
min-width: 44px;
min-height: 44px;
color: var(--steel);
font-weight: 650;
touch-action: none;
user-select: none;
}
.axis-button,
.mini-button {
border: 1px solid #99a8ba;
border-radius: 999px;
background: linear-gradient(180deg, #ffffff 0%, #e5ebf2 100%);
box-shadow: 0 2px 0 #b7c1ce;
}
.axis-button {
min-width: 0;
min-height: 64px;
padding: 0 8px;
}
.mini-button {
padding: 0 10px;
background: var(--surface);
}
.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);
}
.mini-button:hover,
.mini-button.active {
border-color: var(--warn);
background: var(--warn);
color: #ffffff;
box-shadow: 0 2px 0 #8a5a13;
}
.jog-button:focus-visible {
outline: 3px solid rgba(0, 124, 137, 0.3);
outline-offset: 2px;
}
.jog-button.active {
transform: translateY(1px);
}
.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);
}
.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;
}
.jog-status {
display: flex;
align-items: center;
justify-content: flex-end;
min-height: 38px;
color: var(--muted);
font-family: Consolas, "Cascadia Mono", monospace;
text-align: right;
}
.jog-status.error {
color: var(--bad);
}
.log-panel {
margin: 0 16px 16px;
overflow: hidden;
border: 1px solid var(--line);
border-radius: 8px;
background: #fbfcfe;
}
.log-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 12px 14px;
border-bottom: 1px solid var(--line);
background: var(--surface);
}
.log-head h3 {
margin: 0;
font-size: 15px;
font-weight: 650;
}
.clear-button {
min-height: 34px;
padding: 0 12px;
border-color: var(--line);
background: #ffffff;
color: var(--muted);
}
.clear-button:hover {
border-color: var(--accent);
color: var(--accent);
}
.request-log {
display: grid;
gap: 8px;
max-height: 300px;
min-height: 54px;
margin: 0;
padding: 12px 14px 14px;
overflow: auto;
font-family: Consolas, "Cascadia Mono", monospace;
font-size: 13px;
}
.log-entry {
display: grid;
grid-template-columns: 88px 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-entry.error {
border-color: rgba(180, 35, 24, 0.35);
background: rgba(180, 35, 24, 0.06);
}
.log-time,
.log-axis {
color: var(--muted);
white-space: nowrap;
}
.log-payload {
overflow-wrap: anywhere;
color: var(--text);
}
.log-empty {
color: var(--muted);
}
@media (max-width: 820px) { @media (max-width: 820px) {
.topbar { .topbar {
align-items: flex-start; align-items: flex-start;
@@ -485,16 +199,6 @@ dd {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.jog-toolbar,
.jog-body {
grid-template-columns: 1fr;
}
.jog-status {
justify-content: flex-start;
text-align: left;
}
dl { dl {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
@@ -508,19 +212,3 @@ dd {
padding-top: 2px; padding-top: 2px;
} }
} }
@media (max-width: 520px) {
.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;
}
}

View File

@@ -15,37 +15,9 @@ const fields = {
joints: document.getElementById("joints"), joints: document.getElementById("joints"),
pose: document.getElementById("pose"), pose: document.getElementById("pose"),
trajectories: document.getElementById("trajectories"), trajectories: document.getElementById("trajectories"),
refresh: document.getElementById("refresh"), refresh: document.getElementById("refresh")
linearStep: document.getElementById("linear-step"),
angularStep: document.getElementById("angular-step"),
jogStatus: document.getElementById("jog-status"),
requestLog: document.getElementById("request-log"),
clearLog: document.getElementById("clear-log"),
jogButtons: Array.from(document.querySelectorAll(".jog-button"))
}; };
const axisIndexes = {
x: 0,
y: 1,
z: 2,
w: 3,
p: 4,
r: 5
};
// 点动状态集中保存,确保按住按钮时不会并发发送多条 MovePose 请求。
const jogState = {
timer: null,
activeButton: null,
inFlight: false,
lastSnapshot: null,
sequence: 0,
logs: []
};
const maxJogLogs = 80;
const jogIntervalMs = 250;
function formatArray(values) { function formatArray(values) {
if (!Array.isArray(values) || values.length === 0) { if (!Array.isArray(values) || values.length === 0) {
return "--"; return "--";
@@ -84,202 +56,12 @@ function setDot(connectionState) {
} }
} }
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 getStepForAxis(axis) {
// 平移和姿态使用不同单位,但共享 0.1 到 20 的现场可调范围。
return axis === "x" || axis === "y" || axis === "z"
? clampStep(fields.linearStep, 0.1, 20)
: clampStep(fields.angularStep, 0.1, 20);
}
function setJogStatus(message, isError) {
fields.jogStatus.textContent = message;
fields.jogStatus.classList.toggle("error", Boolean(isError));
}
function formatDirection(axis, direction) {
return `${axis.toUpperCase()}${direction > 0 ? "+" : "-"}`;
}
function renderJogLogs() {
fields.requestLog.replaceChildren();
if (jogState.logs.length === 0) {
const empty = document.createElement("div");
empty.className = "log-empty";
empty.textContent = "暂无请求记录";
fields.requestLog.appendChild(empty);
return;
}
jogState.logs.forEach(function (entry) {
const row = document.createElement("div");
row.className = `log-entry${entry.ok ? "" : " error"}`;
const time = document.createElement("span");
time.className = "log-time";
time.textContent = entry.time;
const axis = document.createElement("span");
axis.className = "log-axis";
axis.textContent = `#${entry.sequence} ${entry.direction}`;
const payload = document.createElement("span");
payload.className = "log-payload";
payload.textContent = `step=${entry.step} ${entry.unit}; body=${entry.body}; ${entry.result}`;
row.append(time, axis, payload);
fields.requestLog.appendChild(row);
});
}
function addJogLog(entry) {
jogState.logs.unshift(entry);
if (jogState.logs.length > maxJogLogs) {
jogState.logs.length = maxJogLogs;
}
renderJogLogs();
}
async function loadSnapshotForJog() {
const response = await fetch("/api/status/snapshot", { cache: "no-store" });
if (!response.ok) {
throw new Error(`状态快照读取失败: HTTP ${response.status}`);
}
const payload = await response.json();
jogState.lastSnapshot = payload.snapshot;
return payload.snapshot;
}
function buildJogPose(snapshot, axis, direction) {
// 每次点动都从最新 TCP 位姿出发,只修改一个轴,避免连续按压时累积本地误差。
const sourcePose = Array.isArray(snapshot.cartesianPose) ? snapshot.cartesianPose : [];
if (sourcePose.length < 6) {
throw new Error("当前 TCP 位姿不足 6 维,无法点动。");
}
const pose = sourcePose.slice(0, 6).map(Number);
if (pose.some(value => !Number.isFinite(value))) {
throw new Error("当前 TCP 位姿包含非数值,无法点动。");
}
const axisIndex = axisIndexes[axis];
pose[axisIndex] = Number((pose[axisIndex] + direction * getStepForAxis(axis)).toFixed(6));
return {
x: pose[0],
y: pose[1],
z: pose[2],
w: pose[3],
p: pose[4],
r: pose[5]
};
}
async function sendJog(button) {
// MovePose 底层会生成完整直角轨迹;前端这里只负责构造增量目标位姿。
if (jogState.inFlight) {
return;
}
const axis = button.dataset.axis;
const direction = Number(button.dataset.direction);
if (!Object.prototype.hasOwnProperty.call(axisIndexes, axis) || !Number.isFinite(direction)) {
return;
}
jogState.inFlight = true;
const step = getStepForAxis(axis);
const directionLabel = formatDirection(axis, direction);
const sequence = ++jogState.sequence;
let bodyText = "{}";
let resultText = "";
let ok = false;
try {
const snapshot = await loadSnapshotForJog();
const pose = buildJogPose(snapshot, axis, direction);
bodyText = JSON.stringify(pose);
const response = await fetch("/move_pose/", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: bodyText
});
if (!response.ok) {
throw new Error(`MovePose 调用失败: HTTP ${response.status}`);
}
ok = true;
resultText = `HTTP ${response.status}`;
setJogStatus(`${directionLabel} 已发送`, false);
await refreshStatus();
} catch (error) {
resultText = error instanceof Error ? error.message : "点动失败";
setJogStatus(resultText, true);
stopJog();
} finally {
addJogLog({
time: new Date().toLocaleTimeString(),
sequence,
direction: directionLabel,
step,
unit: axis === "x" || axis === "y" || axis === "z" ? "mm" : "deg",
body: bodyText,
result: resultText,
ok
});
jogState.inFlight = false;
}
}
function startJog(event) {
const button = event.currentTarget;
if (!button || button.disabled) {
return;
}
event.preventDefault();
stopJog();
jogState.activeButton = button;
button.classList.add("active");
sendJog(button);
jogState.timer = window.setInterval(function () {
sendJog(button);
}, jogIntervalMs);
}
function stopJog() {
if (jogState.timer !== null) {
window.clearInterval(jogState.timer);
jogState.timer = null;
}
if (jogState.activeButton) {
jogState.activeButton.classList.remove("active");
jogState.activeButton = null;
}
}
async function refreshStatus() { async function refreshStatus() {
fields.refresh.disabled = true; fields.refresh.disabled = true;
try { try {
const response = await fetch("/api/status/snapshot", { cache: "no-store" }); const response = await fetch("/api/status/snapshot", { cache: "no-store" });
const payload = await response.json(); const payload = await response.json();
const snapshot = payload.snapshot; const snapshot = payload.snapshot;
jogState.lastSnapshot = snapshot;
fields.connectionState.textContent = snapshot.connectionState; fields.connectionState.textContent = snapshot.connectionState;
fields.robotName.textContent = payload.robotName || "--"; fields.robotName.textContent = payload.robotName || "--";
@@ -306,28 +88,5 @@ async function refreshStatus() {
} }
fields.refresh.addEventListener("click", refreshStatus); fields.refresh.addEventListener("click", refreshStatus);
fields.linearStep.addEventListener("change", function () {
clampStep(fields.linearStep, 0.1, 20);
});
fields.angularStep.addEventListener("change", function () {
clampStep(fields.angularStep, 0.1, 20);
});
fields.clearLog.addEventListener("click", function () {
jogState.logs = [];
renderJogLogs();
});
fields.jogButtons.forEach(function (button) {
button.addEventListener("pointerdown", startJog);
button.addEventListener("pointerup", stopJog);
button.addEventListener("pointercancel", stopJog);
button.addEventListener("pointerleave", stopJog);
});
window.addEventListener("blur", stopJog);
window.addEventListener("keyup", function (event) {
if (event.key === "Escape") {
stopJog();
}
});
renderJogLogs();
refreshStatus(); refreshStatus();
window.setInterval(refreshStatus, 2000); window.setInterval(refreshStatus, 2000);

View File

@@ -57,73 +57,6 @@
<dt>已上传轨迹</dt><dd id="trajectories" class="empty">--</dd> <dt>已上传轨迹</dt><dd id="trajectories" class="empty">--</dd>
</dl> </dl>
</section> </section>
<section class="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="20" 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="20" step="0.1" value="1">
</label>
<div id="jog-status" class="jog-status">等待点动输入</div>
</div>
<div class="jog-body">
<article class="stick-card">
<div class="stick-head">
<h3>XYZ 平移摇杆</h3>
<span>mm</span>
</div>
<div class="stick-console" aria-label="XYZ 平移摇杆">
<button class="jog-button axis-button up" type="button" data-axis="y" data-direction="1">Y+</button>
<button class="jog-button axis-button left" type="button" data-axis="x" data-direction="-1">X-</button>
<div class="center-stack">
<button class="jog-button mini-button" type="button" data-axis="z" data-direction="1">Z+</button>
<button class="jog-button mini-button" type="button" data-axis="z" data-direction="-1">Z-</button>
</div>
<button class="jog-button axis-button right" type="button" data-axis="x" data-direction="1">X+</button>
<button class="jog-button 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>WPR 姿态摇杆</h3>
<span>deg</span>
</div>
<div class="stick-console" aria-label="WPR 姿态摇杆">
<button class="jog-button axis-button up" type="button" data-axis="p" data-direction="1">P+</button>
<button class="jog-button axis-button left" type="button" data-axis="w" data-direction="-1">W-</button>
<div class="center-stack">
<button class="jog-button mini-button" type="button" data-axis="r" data-direction="-1">R-</button>
<button class="jog-button mini-button" type="button" data-axis="r" data-direction="1">R+</button>
</div>
<button class="jog-button axis-button right" type="button" data-axis="w" data-direction="1">W+</button>
<button class="jog-button 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>
<div class="log-panel">
<div class="log-head">
<h3>请求日志</h3>
<button id="clear-log" class="clear-button" type="button">清空</button>
</div>
<div id="request-log" class="request-log" aria-live="polite">
<div class="log-empty">暂无请求记录</div>
</div>
</div>
</section>
</div> </div>
</main> </main>
<script src="/assets/status.js" defer></script> <script src="/assets/status.js" defer></script>

View File

@@ -8,20 +8,19 @@ namespace Flyshot.Core.Tests;
public sealed class ConfigCompatibilityTests public sealed class ConfigCompatibilityTests
{ {
/// <summary> /// <summary>
/// 验证现有旧样本配置能被加载,并保持关键机器人参数与飞拍程序内容不变。 /// 验证现有 RobotConfig.json 能被加载,并保持关键机器人参数与飞拍程序内容不变。
/// </summary> /// </summary>
[Fact] [Fact]
public void RobotConfigLoader_LoadsLegacyRobotConfig_AndPreservesPrograms() public void RobotConfigLoader_LoadsLegacyRobotConfig_AndPreservesPrograms()
{ {
var workspaceRoot = GetWorkspaceRoot(); var workspaceRoot = GetWorkspaceRoot();
var configPath = Path.Combine(workspaceRoot, "Rvbust", "EOL10_EAU_0", "EOL10_EAU_0.json"); var configPath = Path.Combine(workspaceRoot, "Rvbust", "EOL10_EAU_0", "RobotConfig.json");
var loaded = new RobotConfigLoader().Load(configPath); var loaded = new RobotConfigLoader().Load(configPath);
Assert.True(loaded.Robot.UseDo); Assert.True(loaded.Robot.UseDo);
Assert.Equal([7, 8], loaded.Robot.IoAddresses); Assert.Equal([7, 8], loaded.Robot.IoAddresses);
Assert.Equal(2, loaded.Robot.IoKeepCycles); Assert.Equal(2, loaded.Robot.IoKeepCycles);
Assert.Equal(7, loaded.Robot.TriggerSampleIndexOffsetCycles);
Assert.Equal(1.0, loaded.Robot.AccLimitScale); Assert.Equal(1.0, loaded.Robot.AccLimitScale);
Assert.Equal(1.0, loaded.Robot.JerkLimitScale); Assert.Equal(1.0, loaded.Robot.JerkLimitScale);
Assert.Equal(1.0, loaded.Robot.PlanningSpeedScale); Assert.Equal(1.0, loaded.Robot.PlanningSpeedScale);
@@ -71,11 +70,9 @@ public sealed class ConfigCompatibilityTests
Assert.False(loaded.Robot.UseDo); Assert.False(loaded.Robot.UseDo);
Assert.Empty(loaded.Robot.IoAddresses); Assert.Empty(loaded.Robot.IoAddresses);
Assert.Equal(3, loaded.Robot.IoKeepCycles); Assert.Equal(3, loaded.Robot.IoKeepCycles);
Assert.Equal(7, loaded.Robot.TriggerSampleIndexOffsetCycles);
Assert.Equal(0.5, loaded.Robot.AccLimitScale); Assert.Equal(0.5, loaded.Robot.AccLimitScale);
Assert.Equal(0.25, loaded.Robot.JerkLimitScale); Assert.Equal(0.25, loaded.Robot.JerkLimitScale);
Assert.Equal(1.0, loaded.Robot.PlanningSpeedScale); Assert.Equal(1.0, loaded.Robot.PlanningSpeedScale);
Assert.Equal(1.0, loaded.Robot.SpeedRatio);
Assert.True(loaded.Robot.SmoothStartStopTiming); Assert.True(loaded.Robot.SmoothStartStopTiming);
Assert.Equal([0, 0, 0], program.OffsetValues); Assert.Equal([0, 0, 0], program.OffsetValues);
Assert.All(program.AddressGroups, group => Assert.Empty(group.Addresses)); Assert.All(program.AddressGroups, group => Assert.Empty(group.Addresses));
@@ -86,46 +83,6 @@ public sealed class ConfigCompatibilityTests
} }
} }
/// <summary>
/// 验证 RobotConfig.json 可以显式配置运行时默认速度倍率,供 MoveJoint 和后续执行链路初始化使用。
/// </summary>
[Fact]
public void RobotConfigLoader_LoadsRuntimeSpeedRatio()
{
var tempRoot = CreateTempDirectory();
try
{
var configPath = Path.Combine(tempRoot, "legacy.json");
File.WriteAllText(
configPath,
"""
{
"robot": {
"use_do": true,
"io_keep_cycles": 2,
"acc_limit": 1.0,
"jerk_limit": 1.0,
"speed_ratio": 0.65
},
"flying_shots": {
"demo": {
"traj_waypoints": [[0, 1], [2, 3], [4, 5], [6, 7]],
"shot_flags": [false, false, false, false]
}
}
}
""");
var loaded = new RobotConfigLoader().Load(configPath);
Assert.Equal(0.65, loaded.Robot.SpeedRatio, precision: 6);
}
finally
{
Directory.Delete(tempRoot, recursive: true);
}
}
/// <summary> /// <summary>
/// 验证 RobotConfig.json 可以显式配置规划限速倍率,且该倍率独立于运行时 J519 速度倍率。 /// 验证 RobotConfig.json 可以显式配置规划限速倍率,且该倍率独立于运行时 J519 速度倍率。
/// </summary> /// </summary>
@@ -166,46 +123,6 @@ public sealed class ConfigCompatibilityTests
} }
} }
/// <summary>
/// 验证 RobotConfig.json 可以显式配置触发绑定后的命令 sample 后移周期数。
/// </summary>
[Fact]
public void RobotConfigLoader_LoadsTriggerSampleIndexOffsetCycles()
{
var tempRoot = CreateTempDirectory();
try
{
var configPath = Path.Combine(tempRoot, "legacy.json");
File.WriteAllText(
configPath,
"""
{
"robot": {
"use_do": true,
"io_keep_cycles": 2,
"trigger_sample_index_offset_cycles": 8,
"acc_limit": 1.0,
"jerk_limit": 1.0
},
"flying_shots": {
"demo": {
"traj_waypoints": [[0, 1], [2, 3], [4, 5], [6, 7]],
"shot_flags": [false, false, false, false]
}
}
}
""");
var loaded = new RobotConfigLoader().Load(configPath);
Assert.Equal(8, loaded.Robot.TriggerSampleIndexOffsetCycles);
}
finally
{
Directory.Delete(tempRoot, recursive: true);
}
}
/// <summary> /// <summary>
/// 验证 RobotConfig.json 可以关闭飞拍执行前的二次平滑起停时间重映射。 /// 验证 RobotConfig.json 可以关闭飞拍执行前的二次平滑起停时间重映射。
/// </summary> /// </summary>
@@ -253,7 +170,7 @@ public sealed class ConfigCompatibilityTests
public void RobotModelLoader_LoadsRobotProfile_WithJointLimitsAndCoupling() public void RobotModelLoader_LoadsRobotProfile_WithJointLimitsAndCoupling()
{ {
var replacementRoot = GetReplacementRoot(); var replacementRoot = GetReplacementRoot();
var modelPath = Path.Combine(replacementRoot, "Config", "Models", "LR_Mate_200iD_7L.json"); var modelPath = Path.Combine(replacementRoot, "Config", "LR_Mate_200iD_7L.json");
var profile = new RobotModelLoader().LoadProfile(modelPath); var profile = new RobotModelLoader().LoadProfile(modelPath);
@@ -277,7 +194,7 @@ public sealed class ConfigCompatibilityTests
public void RobotModelLoader_AppliesAccelerationAndJerkScales() public void RobotModelLoader_AppliesAccelerationAndJerkScales()
{ {
var replacementRoot = GetReplacementRoot(); var replacementRoot = GetReplacementRoot();
var modelPath = Path.Combine(replacementRoot, "Config", "Models", "LR_Mate_200iD_7L.json"); var modelPath = Path.Combine(replacementRoot, "Config", "LR_Mate_200iD_7L.json");
var profile = new RobotModelLoader().LoadProfile(modelPath, accLimitScale: 0.5, jerkLimitScale: 0.25); var profile = new RobotModelLoader().LoadProfile(modelPath, accLimitScale: 0.5, jerkLimitScale: 0.25);
@@ -292,7 +209,7 @@ public sealed class ConfigCompatibilityTests
public void RobotModelLoader_LoadsProfileAndKinematics_FromSingleParse() public void RobotModelLoader_LoadsProfileAndKinematics_FromSingleParse()
{ {
var replacementRoot = GetReplacementRoot(); var replacementRoot = GetReplacementRoot();
var modelPath = Path.Combine(replacementRoot, "Config", "Models", "LR_Mate_200iD_7L.json"); var modelPath = Path.Combine(replacementRoot, "Config", "LR_Mate_200iD_7L.json");
var loaded = new RobotModelLoader().LoadProfileAndKinematics(modelPath, accLimitScale: 0.5, jerkLimitScale: 0.25); var loaded = new RobotModelLoader().LoadProfileAndKinematics(modelPath, accLimitScale: 0.5, jerkLimitScale: 0.25);

View File

@@ -71,11 +71,9 @@ public sealed class ControllerClientCompatConfigRootTests
useDo: true, useDo: true,
ioAddresses: [7, 8], ioAddresses: [7, 8],
ioKeepCycles: 2, ioKeepCycles: 2,
triggerSampleIndexOffsetCycles: 7,
accLimitScale: 1.0, accLimitScale: 1.0,
jerkLimitScale: 1.0, jerkLimitScale: 1.0,
adaptIcspTryNum: 5, adaptIcspTryNum: 5);
speedRatio: 0.65);
var trajectory = TestRobotFactory.CreateUploadedTrajectoryWithSingleShot(); var trajectory = TestRobotFactory.CreateUploadedTrajectoryWithSingleShot();
store.Save("FANUC_LR_Mate_200iD", settings, trajectory); store.Save("FANUC_LR_Mate_200iD", settings, trajectory);
@@ -85,8 +83,6 @@ public sealed class ControllerClientCompatConfigRootTests
Assert.False(Directory.Exists(Path.Combine(configRoot, "TrajectoryStore")), "不应再创建独立轨迹存储目录。"); Assert.False(Directory.Exists(Path.Combine(configRoot, "TrajectoryStore")), "不应再创建独立轨迹存储目录。");
var loaded = store.LoadAll("FANUC_LR_Mate_200iD", out var loadedSettings); var loaded = store.LoadAll("FANUC_LR_Mate_200iD", out var loadedSettings);
Assert.NotNull(loadedSettings); Assert.NotNull(loadedSettings);
Assert.Equal(7, loadedSettings.TriggerSampleIndexOffsetCycles);
Assert.Equal(0.65, loadedSettings.SpeedRatio, precision: 6);
Assert.Contains(trajectory.Name, loaded); Assert.Contains(trajectory.Name, loaded);
store.Delete("FANUC_LR_Mate_200iD", trajectory.Name); store.Delete("FANUC_LR_Mate_200iD", trajectory.Name);

View File

@@ -1,6 +1,5 @@
using System.Text.Json; using System.Text.Json;
using Flyshot.Core.Domain; using Flyshot.Core.Domain;
using Flyshot.Core.Planning.Sampling;
namespace Flyshot.Core.Tests; namespace Flyshot.Core.Tests;
@@ -165,8 +164,7 @@ public sealed class DomainModelTests
triggerTime: 0.5, triggerTime: 0.5,
offsetCycles: 0, offsetCycles: 0,
holdCycles: 1, holdCycles: 1,
addressGroup: new IoAddressGroup(new[] { 100 }), addressGroup: new IoAddressGroup(new[] { 100 }))
referenceJointsDegrees: new[] { 12.5, -3.0 })
}, },
artifacts: new[] artifacts: new[]
{ {
@@ -178,8 +176,7 @@ public sealed class DomainModelTests
failureReason: null, failureReason: null,
usedCache: true, usedCache: true,
originalWaypointCount: 4, originalWaypointCount: 4,
plannedWaypointCount: 5, plannedWaypointCount: 5);
triggerSampleIndexOffsetCycles: 7);
var json = JsonSerializer.Serialize(result); var json = JsonSerializer.Serialize(result);
@@ -187,36 +184,6 @@ public sealed class DomainModelTests
Assert.Contains("\"method\":\"SelfAdaptIcsp\"", json); Assert.Contains("\"method\":\"SelfAdaptIcsp\"", json);
Assert.Contains("\"kind\":\"JointDenseTrajectory\"", json); Assert.Contains("\"kind\":\"JointDenseTrajectory\"", json);
Assert.Contains("\"usedCache\":true", json); Assert.Contains("\"usedCache\":true", json);
Assert.Contains("\"triggerSampleIndexOffsetCycles\":7", json);
Assert.Contains("\"referenceJointsDegrees\":[12.5,-3]", json);
}
/// <summary>
/// 验证触发绑定允许在最佳 sample 基础上继续向后偏移固定命令周期。
/// </summary>
[Fact]
public void TriggerSampleBinder_Bind_AppliesConfiguredSampleIndexOffset()
{
var trigger = new TrajectoryDoEvent(
waypointIndex: 1,
triggerTime: 0.008,
offsetCycles: 0,
holdCycles: 2,
addressGroup: new IoAddressGroup([2, 4]),
referenceJointsDegrees: [10.0, 20.0]);
var samples = new[]
{
new J519SendSample(sampleIndex: 0, sendTime: 0.0, trajectoryTime: 0.0, speedRatio: 1.0, jointsDegrees: [0.0, 0.0]),
new J519SendSample(sampleIndex: 1, sendTime: 0.008, trajectoryTime: 0.008, speedRatio: 1.0, jointsDegrees: [10.0, 20.0]),
new J519SendSample(sampleIndex: 2, sendTime: 0.016, trajectoryTime: 0.016, speedRatio: 1.0, jointsDegrees: [11.0, 21.0]),
new J519SendSample(sampleIndex: 3, sendTime: 0.024, trajectoryTime: 0.024, speedRatio: 1.0, jointsDegrees: [12.0, 22.0])
};
var binding = Assert.Single(Flyshot.Core.Planning.Sampling.TriggerSampleBinder.Bind([trigger], samples, sampleIndexOffsetCycles: 2));
Assert.True(binding.FoundInWindow);
Assert.Equal(3, binding.SampleIndex);
Assert.Equal(0.024, binding.Sample.SendTime, precision: 6);
} }
/// <summary> /// <summary>

View File

@@ -1,7 +1,6 @@
using Flyshot.Core.Domain; using Flyshot.Core.Domain;
using Flyshot.ControllerClientCompat; using Flyshot.ControllerClientCompat;
using Flyshot.Core.Config; using Flyshot.Core.Config;
using Flyshot.Core.Planning.Sampling;
using Flyshot.Runtime.Fanuc; using Flyshot.Runtime.Fanuc;
using Flyshot.Runtime.Fanuc.Protocol; using Flyshot.Runtime.Fanuc.Protocol;
using System.Buffers.Binary; using System.Buffers.Binary;
@@ -21,10 +20,10 @@ public sealed class FanucControllerRuntimeDenseTests
private const double SmoothPtpJerkShapeCoefficient = 52.5; private const double SmoothPtpJerkShapeCoefficient = 52.5;
/// <summary> /// <summary>
/// 验证真机 J519 会直接消费已经按 8ms 生成好的稠密轨迹,不再按运行时 speedRatio 临场重采样。 /// 验证真机 J519 会预生成按 8ms 轨迹映射的命令队列,并输出角度制目标。
/// </summary> /// </summary>
[Fact] [Fact]
public void ExecuteTrajectory_WithDenseWaypoints_RealMode_UsesPlannedDenseRowsAndConvertsRadiansToDegrees() public void ExecuteTrajectory_WithDenseWaypoints_RealMode_ResamplesBySpeedRatioAndConvertsRadiansToDegrees()
{ {
using var commandClient = new FanucCommandClient(); using var commandClient = new FanucCommandClient();
using var stateClient = new FanucStateClient(); using var stateClient = new FanucStateClient();
@@ -38,8 +37,8 @@ public sealed class FanucControllerRuntimeDenseTests
var denseTrajectory = new[] var denseTrajectory = new[]
{ {
new[] { 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 }, new[] { 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 },
new[] { 0.008, DegreesToRadians(0.08), 0.0, 0.0, 0.0, 0.0, 0.0 }, new[] { 0.008, Math.PI / 2.0, 0.0, 0.0, 0.0, 0.0, 0.0 },
new[] { 0.016, DegreesToRadians(0.16), 0.0, 0.0, 0.0, 0.0, 0.0 } new[] { 0.016, Math.PI, 0.0, 0.0, 0.0, 0.0, 0.0 }
}; };
var result = new TrajectoryResult( var result = new TrajectoryResult(
@@ -56,19 +55,13 @@ public sealed class FanucControllerRuntimeDenseTests
plannedWaypointCount: 4, plannedWaypointCount: 4,
denseJointTrajectory: denseTrajectory); denseJointTrajectory: denseTrajectory);
runtime.ExecuteTrajectory(result, [DegreesToRadians(0.16), 0.0, 0.0, 0.0, 0.0, 0.0]); runtime.ExecuteTrajectory(result, [Math.PI, 0.0, 0.0, 0.0, 0.0, 0.0]);
WaitUntilIdle(runtime); WaitUntilIdle(runtime);
var commands = j519Client.GetCommandHistoryForTests(); var commands = j519Client.GetCommandHistoryForTests();
Assert.Equal(3, commands.Count); Assert.Equal(5, commands.Count);
Assert.All(commands, static command => Assert.Equal(0u, command.Sequence)); Assert.All(commands, static command => Assert.Equal(0u, command.Sequence));
var expectedJ1Targets = new[] { 0.0, 0.08, 0.16 }; Assert.Equal([0.0, 45.0, 90.0, 135.0, 180.0], commands.Select(static command => command.TargetJoints[0]));
var actualJ1Targets = commands.Select(static command => command.TargetJoints[0]).ToArray();
Assert.Equal(expectedJ1Targets.Length, actualJ1Targets.Length);
for (var index = 0; index < expectedJ1Targets.Length; index++)
{
Assert.Equal(expectedJ1Targets[index], actualJ1Targets[index], precision: 6);
}
Assert.False(j519Client.IsCommandQueueDrainedForTests()); Assert.False(j519Client.IsCommandQueueDrainedForTests());
} }
@@ -96,8 +89,8 @@ public sealed class FanucControllerRuntimeDenseTests
var denseTrajectory = new[] var denseTrajectory = new[]
{ {
new[] { 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 }, new[] { 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 },
new[] { 0.008, DegreesToRadians(0.08), 0.0, 0.0, 0.0, 0.0, 0.0 }, new[] { 0.008, Math.PI / 2.0, 0.0, 0.0, 0.0, 0.0, 0.0 },
new[] { 0.016, DegreesToRadians(0.16), 0.0, 0.0, 0.0, 0.0, 0.0 } new[] { 0.016, Math.PI, 0.0, 0.0, 0.0, 0.0, 0.0 }
}; };
var result = new TrajectoryResult( var result = new TrajectoryResult(
programName: "wait-drain", programName: "wait-drain",
@@ -113,7 +106,7 @@ public sealed class FanucControllerRuntimeDenseTests
plannedWaypointCount: 4, plannedWaypointCount: 4,
denseJointTrajectory: denseTrajectory); denseJointTrajectory: denseTrajectory);
var executeTask = Task.Run(() => runtime.ExecuteTrajectory(result, [DegreesToRadians(0.16), 0.0, 0.0, 0.0, 0.0, 0.0]), cts.Token); var executeTask = Task.Run(() => runtime.ExecuteTrajectory(result, [Math.PI, 0.0, 0.0, 0.0, 0.0, 0.0]), cts.Token);
await WaitUntilAsync(() => j519Client.GetCommandHistoryForTests().Count == 3, cts.Token); await WaitUntilAsync(() => j519Client.GetCommandHistoryForTests().Count == 3, cts.Token);
// 只有机器人状态包把队列全部取出后,ExecuteTrajectory 才能向上层返回。 // 只有机器人状态包把队列全部取出后,ExecuteTrajectory 才能向上层返回。
@@ -153,8 +146,8 @@ public sealed class FanucControllerRuntimeDenseTests
var denseTrajectory = new[] var denseTrajectory = new[]
{ {
new[] { 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 }, new[] { 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 },
new[] { 0.008, DegreesToRadians(1.0), 0.0, 0.0, 0.0, 0.0, 0.0 }, new[] { 0.008, Math.PI / 2.0, 0.0, 0.0, 0.0, 0.0, 0.0 },
new[] { 0.016, DegreesToRadians(2.0), 0.0, 0.0, 0.0, 0.0, 0.0 } new[] { 0.016, Math.PI, 0.0, 0.0, 0.0, 0.0, 0.0 }
}; };
var result = new TrajectoryResult( var result = new TrajectoryResult(
@@ -171,7 +164,7 @@ public sealed class FanucControllerRuntimeDenseTests
plannedWaypointCount: 4, plannedWaypointCount: 4,
denseJointTrajectory: denseTrajectory); denseJointTrajectory: denseTrajectory);
runtime.ExecuteTrajectory(result, [DegreesToRadians(2.0), 0.0, 0.0, 0.0, 0.0, 0.0]); runtime.ExecuteTrajectory(result, [Math.PI, 0.0, 0.0, 0.0, 0.0, 0.0]);
WaitUntilIdle(runtime); WaitUntilIdle(runtime);
var commands = j519Client.GetCommandHistoryForTests(); var commands = j519Client.GetCommandHistoryForTests();
@@ -179,17 +172,14 @@ public sealed class FanucControllerRuntimeDenseTests
var pointsPath = Path.Combine(runDir, "ActualSendJointTraj.txt"); var pointsPath = Path.Combine(runDir, "ActualSendJointTraj.txt");
var timingPath = Path.Combine(runDir, "ActualSendTiming.txt"); var timingPath = Path.Combine(runDir, "ActualSendTiming.txt");
var jerkPath = Path.Combine(runDir, "ActualSendJerkStats.txt"); var jerkPath = Path.Combine(runDir, "ActualSendJerkStats.txt");
var shotEventsPath = Path.Combine(runDir, "ShotEvents.json");
Assert.True(File.Exists(pointsPath)); Assert.True(File.Exists(pointsPath));
Assert.True(File.Exists(timingPath)); Assert.True(File.Exists(timingPath));
Assert.True(File.Exists(jerkPath)); Assert.True(File.Exists(jerkPath));
Assert.True(File.Exists(shotEventsPath));
var pointLines = File.ReadAllLines(pointsPath); var pointLines = File.ReadAllLines(pointsPath);
var timingLines = File.ReadAllLines(timingPath); var timingLines = File.ReadAllLines(timingPath);
var jerkLines = File.ReadAllLines(jerkPath); var jerkLines = File.ReadAllLines(jerkPath);
var shotEventsJson = File.ReadAllText(shotEventsPath);
Assert.Equal(commands.Count, pointLines.Length); Assert.Equal(commands.Count, pointLines.Length);
Assert.Equal(commands.Count, timingLines.Length); Assert.Equal(commands.Count, timingLines.Length);
Assert.Equal(Math.Max(0, commands.Count - 1), jerkLines.Length); Assert.Equal(Math.Max(0, commands.Count - 1), jerkLines.Length);
@@ -201,21 +191,19 @@ public sealed class FanucControllerRuntimeDenseTests
Assert.Equal(9, lastColumns.Length); Assert.Equal(9, lastColumns.Length);
Assert.Equal(0.0, firstColumns[0], precision: 6); Assert.Equal(0.0, firstColumns[0], precision: 6);
Assert.Equal(0.008, secondColumns[0], precision: 6); Assert.Equal(0.008, secondColumns[0], precision: 6);
Assert.Equal(2.0, lastColumns[1], precision: 6); Assert.Equal(180.0, lastColumns[1], precision: 6);
var firstTimingColumns = ParseColumns(timingLines[0]); var firstTimingColumns = ParseColumns(timingLines[0]);
var secondTimingColumns = ParseColumns(timingLines[1]); var secondTimingColumns = ParseColumns(timingLines[1]);
var lastTimingColumns = ParseColumns(timingLines[^1]); var lastTimingColumns = ParseColumns(timingLines[^1]);
Assert.Equal(6, firstTimingColumns.Length); Assert.Equal(4, firstTimingColumns.Length);
Assert.Equal(0.0, firstTimingColumns[0], precision: 6); Assert.Equal(0.0, firstTimingColumns[0], precision: 6);
Assert.Equal(0.0, firstTimingColumns[1], precision: 6); Assert.Equal(0.0, firstTimingColumns[1], precision: 6);
Assert.Equal(0.0, firstTimingColumns[2], precision: 6); Assert.Equal(0.0, firstTimingColumns[2], precision: 6);
Assert.Equal(1.0, firstTimingColumns[3], precision: 6); Assert.Equal(0.5, firstTimingColumns[3], precision: 6);
Assert.Equal(0.5, firstTimingColumns[4], precision: 6);
Assert.Equal(0.0, firstTimingColumns[5], precision: 6);
Assert.Equal(1.0, secondTimingColumns[0], precision: 6); Assert.Equal(1.0, secondTimingColumns[0], precision: 6);
Assert.Equal(0.008, secondTimingColumns[1], precision: 6); Assert.Equal(0.008, secondTimingColumns[1], precision: 6);
Assert.Equal(0.008, secondTimingColumns[2], precision: 6); Assert.Equal(0.004, secondTimingColumns[2], precision: 6);
Assert.Equal(commands.Count - 1, lastTimingColumns[0], precision: 6); Assert.Equal(commands.Count - 1, lastTimingColumns[0], precision: 6);
Assert.Equal(0.016, lastTimingColumns[2], precision: 6); Assert.Equal(0.016, lastTimingColumns[2], precision: 6);
@@ -223,8 +211,6 @@ public sealed class FanucControllerRuntimeDenseTests
Assert.Equal(10, firstJerkColumns.Length); Assert.Equal(10, firstJerkColumns.Length);
Assert.Equal(0.0, firstJerkColumns[0], precision: 6); Assert.Equal(0.0, firstJerkColumns[0], precision: 6);
Assert.Equal(0.008, firstJerkColumns[2], precision: 6); Assert.Equal(0.008, firstJerkColumns[2], precision: 6);
Assert.Equal("[]", shotEventsJson.Trim());
} }
finally finally
{ {
@@ -283,8 +269,8 @@ public sealed class FanucControllerRuntimeDenseTests
Assert.NotEmpty(pointsLines); Assert.NotEmpty(pointsLines);
Assert.NotEmpty(timingLines); Assert.NotEmpty(timingLines);
Assert.NotEmpty(jerkLines); Assert.NotEmpty(jerkLines);
Assert.Equal(result.DenseJointTrajectory!.Count, pointsLines.Length); Assert.Equal(927, pointsLines.Length);
Assert.Equal(result.DenseJointTrajectory.Count, timingLines.Length); Assert.Equal(927, timingLines.Length);
var firstPoint = ParseColumns(pointsLines[0]); var firstPoint = ParseColumns(pointsLines[0]);
var secondPoint = ParseColumns(pointsLines[1]); var secondPoint = ParseColumns(pointsLines[1]);
@@ -333,154 +319,13 @@ public sealed class FanucControllerRuntimeDenseTests
} }
/// <summary> /// <summary>
/// 验证真实 UTTC_MS11 轨迹在非 1 倍 speedRatio 下仍能生成并装载 J519 实发队列。 /// 验证 MoveJoint 会按抓包确认的点到点临时轨迹生成稠密 J519 目标,并继续叠加 speed_ratio 重采样。
/// </summary>
[Theory]
[InlineData(0.7)]
[InlineData(0.5)]
public void ExecuteTrajectory_UttcMs11FromHostRuntimeConfig_RealMode_AllowsNonOneSpeedRatio(double speedRatio)
{
using var commandClient = new FanucCommandClient();
using var stateClient = new FanucStateClient();
using var j519Client = new FanucJ519Client();
using var runtime = new FanucControllerRuntime(commandClient, stateClient, j519Client);
var fixture = LoadUttcMs11RuntimeFixture();
var fullSpeedSettings = new CompatibilityRobotSettings(
useDo: fixture.Settings.UseDo,
ioAddresses: fixture.Settings.IoAddresses,
ioKeepCycles: fixture.Settings.IoKeepCycles,
triggerSampleIndexOffsetCycles: fixture.Settings.TriggerSampleIndexOffsetCycles,
accLimitScale: fixture.Settings.AccLimitScale,
jerkLimitScale: fixture.Settings.JerkLimitScale,
adaptIcspTryNum: fixture.Settings.AdaptIcspTryNum,
planningSpeedScale: 1.0,
smoothStartStopTiming: true);
var orchestrator = new ControllerClientTrajectoryOrchestrator();
var bundle = orchestrator.PlanUploadedFlyshot(
fixture.Robot,
fixture.Uploaded,
settings: fullSpeedSettings,
planningSpeedScale: fullSpeedSettings.PlanningSpeedScale);
var preparedExecution = FlyshotExecutionSendSequenceBuilder.Build(
fixture.Robot,
bundle.ExecutionTrajectory,
bundle.Result,
fixture.Robot.ServoPeriod.TotalSeconds,
speedRatio);
var result = WithUniqueProgramName(new TrajectoryResult(
programName: bundle.Result.ProgramName,
method: bundle.Result.Method,
isValid: bundle.Result.IsValid,
duration: bundle.Result.Duration,
shotEvents: bundle.Result.ShotEvents,
triggerTimeline: bundle.Result.TriggerTimeline,
artifacts: bundle.Result.Artifacts,
failureReason: bundle.Result.FailureReason,
usedCache: bundle.Result.UsedCache,
originalWaypointCount: bundle.Result.OriginalWaypointCount,
plannedWaypointCount: bundle.Result.PlannedWaypointCount,
triggerSampleIndexOffsetCycles: bundle.Result.TriggerSampleIndexOffsetCycles,
denseJointTrajectory: bundle.Result.DenseJointTrajectory,
preparedFlyshotExecution: preparedExecution), $"UTTC_MS11_speedratio_{speedRatio:F1}_{Guid.NewGuid():N}");
var outputRoot = Path.Combine(AppContext.BaseDirectory, "Config", "Data", result.ProgramName);
try
{
runtime.ResetRobot(fixture.Robot, fixture.Robot.Name);
j519Client.EnableCommandHistoryForTests();
ForceRealModeEnabled(runtime, speedRatio);
runtime.ExecuteTrajectory(result, result.DenseJointTrajectory![^1].Skip(1).ToArray());
WaitUntilIdle(runtime);
var commands = j519Client.GetCommandHistoryForTests();
Assert.NotEmpty(commands);
Assert.Equal(preparedExecution.Samples.Count, commands.Count);
Assert.Equal(speedRatio, preparedExecution.FinalSpeedRatio, precision: 6);
Assert.Equal(0, preparedExecution.StretchIterationCount);
Assert.Equal(preparedExecution.Samples[0].JointsDegrees, commands[0].TargetJoints);
}
finally
{
if (Directory.Exists(outputRoot))
{
Directory.Delete(outputRoot, recursive: true);
}
}
}
/// <summary>
/// 验证运行时拿到飞拍预生成发送队列后,会直接消费该队列,而不是再按当前 speedRatio 临场重采样。
/// </summary>
[Fact]
public void ExecuteTrajectory_WithPreparedFlyshotExecution_RealMode_UsesPreparedSamplesDirectly()
{
using var commandClient = new FanucCommandClient();
using var stateClient = new FanucStateClient();
using var j519Client = new FanucJ519Client();
using var runtime = new FanucControllerRuntime(commandClient, stateClient, j519Client);
var robot = TestRobotFactory.CreateRobotProfile();
runtime.ResetRobot(robot, "FANUC_LR_Mate_200iD");
j519Client.EnableCommandHistoryForTests();
ForceRealModeEnabled(runtime, speedRatio: 0.5);
var preparedSamples = new[]
{
new FlyshotPreparedSample(0, 0.0, 0.0, 1.0, [1.0, 0.0, 0.0, 0.0, 0.0, 0.0]),
new FlyshotPreparedSample(1, 0.008, 0.008, 1.0, [2.0, 0.0, 0.0, 0.0, 0.0, 0.0]),
new FlyshotPreparedSample(2, 0.016, 0.016, 1.0, [3.0, 0.0, 0.0, 0.0, 0.0, 0.0])
};
var preparedExecution = new FlyshotPreparedExecution(
samples: preparedSamples,
triggerBindings: Array.Empty<FlyshotPreparedTriggerBinding>(),
timingRows: preparedSamples.Select(static sample => (IReadOnlyList<double>)
[
sample.SampleIndex,
Math.Round(sample.SendTime, 6),
Math.Round(sample.TrajectoryTime, 6),
Math.Round(sample.SpeedRatio, 6)
]).ToArray(),
jerkRows: Array.Empty<IReadOnlyList<double>>(),
requestSpeedRatio: 1.0,
finalSpeedRatio: 1.0,
finalDurationSeconds: 0.016,
stretchIterationCount: 0);
var result = new TrajectoryResult(
programName: "prepared-demo",
method: PlanningMethod.Icsp,
isValid: true,
duration: TimeSpan.FromSeconds(0.016),
shotEvents: Array.Empty<ShotEvent>(),
triggerTimeline: Array.Empty<TrajectoryDoEvent>(),
artifacts: Array.Empty<TrajectoryArtifact>(),
failureReason: null,
usedCache: false,
originalWaypointCount: 4,
plannedWaypointCount: 4,
denseJointTrajectory:
[
new[] { 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 },
new[] { 0.008, Math.PI / 2.0, 0.0, 0.0, 0.0, 0.0, 0.0 },
new[] { 0.016, Math.PI, 0.0, 0.0, 0.0, 0.0, 0.0 }
],
preparedFlyshotExecution: preparedExecution);
runtime.ExecuteTrajectory(result, [DegreesToRadians(3.0), 0.0, 0.0, 0.0, 0.0, 0.0]);
WaitUntilIdle(runtime);
var commands = j519Client.GetCommandHistoryForTests();
Assert.Equal(preparedSamples.Length, commands.Count);
Assert.Equal([1.0, 2.0, 3.0], commands.Select(static command => command.TargetJoints[0]));
}
/// <summary>
/// 验证 MoveJoint 会在生成阶段折算 speed_ratio,运行时只发送已经规划好的 8ms 稠密点列。
/// </summary> /// </summary>
[Theory] [Theory]
[InlineData(1.0)] [InlineData(1.0)]
[InlineData(0.7)] [InlineData(0.7)]
[InlineData(0.5)] [InlineData(0.5)]
public void MoveJoint_RealMode_GeneratesTemporaryPtpTrajectoryWithSpeedRatioAlreadyPlanned(double speedRatio) public void MoveJoint_RealMode_GeneratesTemporaryPtpTrajectoryAndResamplesBySpeedRatio(double speedRatio)
{ {
using var commandClient = new FanucCommandClient(); using var commandClient = new FanucCommandClient();
using var stateClient = new FanucStateClient(); using var stateClient = new FanucStateClient();
@@ -535,7 +380,7 @@ public sealed class FanucControllerRuntimeDenseTests
} }
[Fact] [Fact]
public void MoveJointTrajectoryGenerator_LowerSpeedUsesFixedServoPeriodAndLongerPlannedDuration() public void MoveJointTrajectoryGenerator_LowerSpeedUsesMoreSamplesWithoutFixedCountContract()
{ {
var robot = CreateMoveJointReferenceRobotProfile(); var robot = CreateMoveJointReferenceRobotProfile();
var startJoints = new[] { 1.056731, 0.011664811, -0.017892333, -0.01516874, 0.021492079, 0.009567846 }; var startJoints = new[] { 1.056731, 0.011664811, -0.017892333, -0.01516874, 0.021492079, 0.009567846 };
@@ -547,36 +392,9 @@ public sealed class FanucControllerRuntimeDenseTests
Assert.True(speed07.DenseJointTrajectory!.Count > fullSpeed.DenseJointTrajectory!.Count); Assert.True(speed07.DenseJointTrajectory!.Count > fullSpeed.DenseJointTrajectory!.Count);
Assert.True(speed05.DenseJointTrajectory!.Count > speed07.DenseJointTrajectory!.Count); Assert.True(speed05.DenseJointTrajectory!.Count > speed07.DenseJointTrajectory!.Count);
AssertDenseRowsUseServoPeriod(fullSpeed.DenseJointTrajectory, robot.ServoPeriod.TotalSeconds); Assert.True(fullSpeed.Duration.TotalSeconds >= ExpectedSmoothPtpDuration(robot, startJoints, targetJoints));
AssertDenseRowsUseServoPeriod(speed07.DenseJointTrajectory, robot.ServoPeriod.TotalSeconds); Assert.True(speed07.Duration.TotalSeconds >= ExpectedSmoothPtpDuration(robot, startJoints, targetJoints));
AssertDenseRowsUseServoPeriod(speed05.DenseJointTrajectory, robot.ServoPeriod.TotalSeconds); Assert.True(speed05.Duration.TotalSeconds >= ExpectedSmoothPtpDuration(robot, startJoints, targetJoints));
Assert.True(fullSpeed.Duration.TotalSeconds >= ExpectedSmoothPtpDuration(robot, startJoints, targetJoints, speedRatio: 1.0));
Assert.True(speed07.Duration.TotalSeconds >= ExpectedSmoothPtpDuration(robot, startJoints, targetJoints, speedRatio: 0.7));
Assert.True(speed05.Duration.TotalSeconds >= ExpectedSmoothPtpDuration(robot, startJoints, targetJoints, speedRatio: 0.5));
}
/// <summary>
/// 验证 MovePose 低速倍率仍保持固定伺服周期,并通过拉长时长降低直角运动速度。
/// </summary>
[Fact]
public void MovePoseTrajectoryGenerator_LowerSpeedUsesFixedServoPeriodAndLongerPlannedDuration()
{
var servoPeriod = TimeSpan.FromMilliseconds(8);
var startPose = new[] { 100.0, 200.0, 300.0, 1.0, 2.0, 3.0 };
var targetPose = new[] { 140.0, 260.0, 330.0, 8.0, 10.0, 12.0 };
var fullSpeed = MovePoseTrajectoryGenerator.CreateResult(startPose, targetPose, servoPeriod, speedRatio: 1.0);
var speed07 = MovePoseTrajectoryGenerator.CreateResult(startPose, targetPose, servoPeriod, speedRatio: 0.7);
var speed05 = MovePoseTrajectoryGenerator.CreateResult(startPose, targetPose, servoPeriod, speedRatio: 0.5);
Assert.True(speed07.DenseCartesianTrajectory!.Count > fullSpeed.DenseCartesianTrajectory!.Count);
Assert.True(speed05.DenseCartesianTrajectory!.Count > speed07.DenseCartesianTrajectory!.Count);
AssertDenseRowsUseServoPeriod(fullSpeed.DenseCartesianTrajectory, servoPeriod.TotalSeconds);
AssertDenseRowsUseServoPeriod(speed07.DenseCartesianTrajectory, servoPeriod.TotalSeconds);
AssertDenseRowsUseServoPeriod(speed05.DenseCartesianTrajectory, servoPeriod.TotalSeconds);
AssertPoseEqual(startPose, fullSpeed.DenseCartesianTrajectory[0].Skip(1).ToArray());
AssertPoseEqual(targetPose, fullSpeed.DenseCartesianTrajectory[^1].Skip(1).ToArray());
Assert.True(MovePoseTrajectoryGenerator.SatisfiesDefaultCartesianLimits(speed05.DenseCartesianTrajectory, speedRatio: 0.5));
} }
[Fact] [Fact]
@@ -603,45 +421,10 @@ public sealed class FanucControllerRuntimeDenseTests
} }
/// <summary> /// <summary>
/// 验证 MovePose 会生成直角坐标 J519 队列,并使用 Data format=0 下发 X/Y/Z/W/P/R。 /// 验证 speed_ratio=0 时不会启动无法推进轨迹时间的后台发送任务。
/// </summary> /// </summary>
[Fact] [Fact]
public void MovePose_RealMode_GeneratesCartesianJ519Queue() public void ExecuteTrajectory_WithDenseWaypoints_RealMode_RejectsZeroSpeedRatio()
{
using var commandClient = new FanucCommandClient();
using var stateClient = new FanucStateClient();
using var j519Client = new FanucJ519Client();
using var runtime = new FanucControllerRuntime(commandClient, stateClient, j519Client);
var service = CreateCompatService(runtime);
var startPose = new[] { 100.0, 200.0, 300.0, 1.0, 2.0, 3.0 };
var targetPose = new[] { 110.0, 220.0, 315.0, 4.0, 5.0, 6.0 };
service.SetUpRobot("FANUC_LR_Mate_200iD");
j519Client.EnableCommandHistoryForTests();
ForceRealModeEnabled(runtime, speedRatio: 1.0);
SetPrivateField(runtime, "_pose", startPose);
service.MovePose(targetPose);
WaitUntilIdle(runtime);
var commands = j519Client.GetCommandHistoryForTests();
Assert.NotEmpty(commands);
Assert.All(commands, static command => Assert.Equal(0, command.DataStyle));
AssertPoseEqual(startPose, commands[0].TargetValues.Take(6).ToArray());
AssertPoseEqual(targetPose, commands[^1].TargetValues.Take(6).ToArray());
Assert.All(commands, static command =>
{
Assert.Equal(0.0, command.TargetValues[6], precision: 6);
Assert.Equal(0.0, command.TargetValues[7], precision: 6);
Assert.Equal(0.0, command.TargetValues[8], precision: 6);
});
}
/// <summary>
/// 验证运行时稠密发送不再依赖当前 speed_ratio;倍率合法性应在上游规划/生成阶段处理。
/// </summary>
[Fact]
public void ExecuteTrajectory_WithDenseWaypoints_RealMode_IgnoresRuntimeSpeedRatioForDenseFallback()
{ {
using var commandClient = new FanucCommandClient(); using var commandClient = new FanucCommandClient();
using var stateClient = new FanucStateClient(); using var stateClient = new FanucStateClient();
@@ -649,14 +432,12 @@ public sealed class FanucControllerRuntimeDenseTests
using var runtime = new FanucControllerRuntime(commandClient, stateClient, j519Client); using var runtime = new FanucControllerRuntime(commandClient, stateClient, j519Client);
var robot = TestRobotFactory.CreateRobotProfile(); var robot = TestRobotFactory.CreateRobotProfile();
runtime.ResetRobot(robot, "FANUC_LR_Mate_200iD"); runtime.ResetRobot(robot, "FANUC_LR_Mate_200iD");
j519Client.EnableCommandHistoryForTests();
ForceRealModeEnabled(runtime, speedRatio: 0.0); ForceRealModeEnabled(runtime, speedRatio: 0.0);
var denseTrajectory = new[] var denseTrajectory = new[]
{ {
new[] { 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 }, new[] { 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 },
new[] { 0.008, DegreesToRadians(0.08), 0.0, 0.0, 0.0, 0.0, 0.0 }, new[] { 0.016, Math.PI, 0.0, 0.0, 0.0, 0.0, 0.0 }
new[] { 0.016, DegreesToRadians(0.16), 0.0, 0.0, 0.0, 0.0, 0.0 }
}; };
var result = new TrajectoryResult( var result = new TrajectoryResult(
@@ -673,12 +454,9 @@ public sealed class FanucControllerRuntimeDenseTests
plannedWaypointCount: 4, plannedWaypointCount: 4,
denseJointTrajectory: denseTrajectory); denseJointTrajectory: denseTrajectory);
runtime.ExecuteTrajectory(result, [DegreesToRadians(0.16), 0.0, 0.0, 0.0, 0.0, 0.0]); var exception = Assert.Throws<InvalidOperationException>(
WaitUntilIdle(runtime); () => runtime.ExecuteTrajectory(result, [Math.PI, 0.0, 0.0, 0.0, 0.0, 0.0]));
Assert.Contains("Speed ratio", exception.Message, StringComparison.OrdinalIgnoreCase);
var commands = j519Client.GetCommandHistoryForTests();
Assert.Equal(3, commands.Count);
Assert.Equal(0.16, commands[^1].TargetJoints[0], precision: 6);
} }
/// <summary> /// <summary>
@@ -783,63 +561,8 @@ public sealed class FanucControllerRuntimeDenseTests
var commands = j519Client.GetCommandHistoryForTests(); var commands = j519Client.GetCommandHistoryForTests();
Assert.Equal(4, commands.Count); Assert.Equal(4, commands.Count);
Assert.Equal([(ushort)0, (ushort)5, (ushort)5, (ushort)5], commands.Select(static command => command.WriteIoMask)); Assert.Equal([(ushort)0, (ushort)10, (ushort)10, (ushort)10], commands.Select(static command => command.WriteIoMask));
Assert.Equal([(ushort)0, (ushort)5, (ushort)5, (ushort)0], commands.Select(static command => command.WriteIoValue)); Assert.Equal([(ushort)0, (ushort)10, (ushort)10, (ushort)0], commands.Select(static command => command.WriteIoValue));
}
/// <summary>
/// 验证同一时间窗口内存在多个候选 sample 时,会优先把 IO 挂到关节坐标最接近示教点的那一帧。
/// </summary>
[Fact]
public void ExecuteTrajectory_WithDenseWaypoints_RealMode_PrefersClosestJointSampleWithinTriggerWindow()
{
using var commandClient = new FanucCommandClient();
using var stateClient = new FanucStateClient();
using var j519Client = new FanucJ519Client();
using var runtime = new FanucControllerRuntime(commandClient, stateClient, j519Client);
var robot = TestRobotFactory.CreateRobotProfile();
runtime.ResetRobot(robot, "FANUC_LR_Mate_200iD");
j519Client.EnableCommandHistoryForTests();
ForceRealModeEnabled(runtime, speedRatio: 1.0);
var denseTrajectory = new[]
{
new[] { 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 },
new[] { 0.008, DegreesToRadians(1.0), 0.0, 0.0, 0.0, 0.0, 0.0 },
new[] { 0.016, DegreesToRadians(2.0), 0.0, 0.0, 0.0, 0.0, 0.0 },
new[] { 0.024, DegreesToRadians(3.0), 0.0, 0.0, 0.0, 0.0, 0.0 }
};
var result = new TrajectoryResult(
programName: "demo",
method: PlanningMethod.Icsp,
isValid: true,
duration: TimeSpan.FromSeconds(0.024),
shotEvents: Array.Empty<ShotEvent>(),
triggerTimeline:
[
new TrajectoryDoEvent(
waypointIndex: 1,
triggerTime: 0.012,
offsetCycles: 0,
holdCycles: 1,
addressGroup: new IoAddressGroup([1]),
referenceJointsDegrees: [2.0, 0.0, 0.0, 0.0, 0.0, 0.0])
],
artifacts: Array.Empty<TrajectoryArtifact>(),
failureReason: null,
usedCache: false,
originalWaypointCount: 4,
plannedWaypointCount: 4,
denseJointTrajectory: denseTrajectory);
runtime.ExecuteTrajectory(result, [DegreesToRadians(3.0), 0.0, 0.0, 0.0, 0.0, 0.0]);
WaitUntilIdle(runtime);
var commands = j519Client.GetCommandHistoryForTests();
Assert.Equal(4, commands.Count);
Assert.Equal([(ushort)0, (ushort)0, (ushort)1, (ushort)1], commands.Select(static command => command.WriteIoMask));
Assert.Equal([(ushort)0, (ushort)0, (ushort)1, (ushort)0], commands.Select(static command => command.WriteIoValue));
} }
/// <summary> /// <summary>
@@ -897,10 +620,8 @@ public sealed class FanucControllerRuntimeDenseTests
var enableRobotRetryCount = 0; var enableRobotRetryCount = 0;
var waitCount = 0; var waitCount = 0;
using var runtime = new FanucControllerRuntime();
var exception = Record.Exception( var exception = Record.Exception(
() => runtime.EnsureJ519ReadyForDenseExecutionCore( () => FanucControllerRuntime.EnsureJ519ReadyForDenseExecutionCore(
() => responses.Dequeue(), () => responses.Dequeue(),
() => enableRobotRetryCount++, () => enableRobotRetryCount++,
() => waitCount++)); () => waitCount++));
@@ -924,10 +645,8 @@ public sealed class FanucControllerRuntimeDenseTests
var enableRobotRetryCount = 0; var enableRobotRetryCount = 0;
var waitCount = 0; var waitCount = 0;
using var runtime = new FanucControllerRuntime();
var exception = Assert.Throws<InvalidOperationException>( var exception = Assert.Throws<InvalidOperationException>(
() => runtime.EnsureJ519ReadyForDenseExecutionCore( () => FanucControllerRuntime.EnsureJ519ReadyForDenseExecutionCore(
() => responses.Dequeue(), () => responses.Dequeue(),
() => enableRobotRetryCount++, () => enableRobotRetryCount++,
() => waitCount++)); () => waitCount++));
@@ -958,16 +677,13 @@ public sealed class FanucControllerRuntimeDenseTests
} }
/// <summary> /// <summary>
/// 验证 IO 地址组中的地址号按 writeIoIndex=1 的手册语义映射为 writeIoValue 位掩码。 /// 验证 IO 地址组中的地址号被正确映射为 writeIoValue 的位掩码。
/// </summary> /// </summary>
[Theory] [Theory]
[InlineData(new[] { 1 }, (ushort)1)] [InlineData(new[] { 0 }, (ushort)1)]
[InlineData(new[] { 2, 4 }, (ushort)10)] [InlineData(new[] { 7 }, (ushort)128)]
[InlineData(new[] { 3, 4 }, (ushort)12)] [InlineData(new[] { 7, 8 }, (ushort)384)] // 128 + 256
[InlineData(new[] { 2, 3, 4 }, (ushort)14)] [InlineData(new[] { 15 }, (ushort)32768)]
[InlineData(new[] { 8 }, (ushort)128)]
[InlineData(new[] { 8, 9 }, (ushort)384)] // 128 + 256
[InlineData(new[] { 16 }, (ushort)32768)]
[InlineData(new int[] { }, (ushort)0)] [InlineData(new int[] { }, (ushort)0)]
public void ComputeIoValue_MapsAddressesToBitMask(int[] addresses, ushort expected) public void ComputeIoValue_MapsAddressesToBitMask(int[] addresses, ushort expected)
{ {
@@ -977,12 +693,12 @@ public sealed class FanucControllerRuntimeDenseTests
} }
/// <summary> /// <summary>
/// 验证超出 1..16 范围的地址号会被安全忽略,不会污染位掩码。 /// 验证超过 15 的地址号会被安全忽略,不会溢出位掩码。
/// </summary> /// </summary>
[Fact] [Fact]
public void ComputeIoValue_IgnoresOutOfRangeAddresses() public void ComputeIoValue_IgnoresOutOfRangeAddresses()
{ {
var group = new IoAddressGroup([0, 1, 17, 8]); var group = new IoAddressGroup([0, 16, 7]);
var actual = FanucControllerRuntime.ComputeIoValue(group); var actual = FanucControllerRuntime.ComputeIoValue(group);
Assert.Equal((ushort)(1 | 128), actual); Assert.Equal((ushort)(1 | 128), actual);
} }
@@ -1062,9 +778,8 @@ public sealed class FanucControllerRuntimeDenseTests
var rows = result.DenseJointTrajectory!; var rows = result.DenseJointTrajectory!;
Assert.True( Assert.True(
result.Duration.TotalSeconds >= ExpectedSmoothPtpDuration(robot, startJoints, targetJoints, speedRatio: 0.7), result.Duration.TotalSeconds >= ExpectedSmoothPtpDuration(robot, startJoints, targetJoints),
$"Duration was shortened to {result.Duration.TotalSeconds:F6}s."); $"Duration was shortened to {result.Duration.TotalSeconds:F6}s.");
AssertDenseRowsUseServoPeriod(rows, robot.ServoPeriod.TotalSeconds);
AssertJointDegreesEqual(startJoints, rows[0].Skip(1).Select(RadiansToDegrees).ToArray()); AssertJointDegreesEqual(startJoints, rows[0].Skip(1).Select(RadiansToDegrees).ToArray());
AssertJointDegreesEqual(targetJoints, rows[^1].Skip(1).Select(RadiansToDegrees).ToArray()); AssertJointDegreesEqual(targetJoints, rows[^1].Skip(1).Select(RadiansToDegrees).ToArray());
} }
@@ -1162,15 +877,6 @@ public sealed class FanucControllerRuntimeDenseTests
} }
} }
private static void AssertPoseEqual(IReadOnlyList<double> expected, IReadOnlyList<double> actual)
{
Assert.Equal(expected.Count, actual.Count);
for (var index = 0; index < expected.Count; index++)
{
Assert.Equal(expected[index], actual[index], precision: 6);
}
}
/// <summary> /// <summary>
/// 创建用于就绪状态测试的最小 J519 响应。 /// 创建用于就绪状态测试的最小 J519 响应。
/// </summary> /// </summary>
@@ -1299,14 +1005,8 @@ public sealed class FanucControllerRuntimeDenseTests
private static double ExpectedSmoothPtpDuration( private static double ExpectedSmoothPtpDuration(
RobotProfile robot, RobotProfile robot,
IReadOnlyList<double> startJoints, IReadOnlyList<double> startJoints,
IReadOnlyList<double> targetJoints, IReadOnlyList<double> targetJoints)
double speedRatio = 1.0)
{ {
if (speedRatio <= 0.0 || double.IsNaN(speedRatio) || double.IsInfinity(speedRatio))
{
throw new ArgumentOutOfRangeException(nameof(speedRatio), "speed_ratio 必须是有限正数。");
}
var duration = 0.0; var duration = 0.0;
for (var index = 0; index < robot.DegreesOfFreedom; index++) for (var index = 0; index < robot.DegreesOfFreedom; index++)
{ {
@@ -1317,27 +1017,15 @@ public sealed class FanucControllerRuntimeDenseTests
} }
var limit = robot.JointLimits[index]; var limit = robot.JointLimits[index];
var velocityDuration = distance * SmoothPtpVelocityShapeCoefficient / (limit.VelocityLimit * speedRatio); var velocityDuration = distance * SmoothPtpVelocityShapeCoefficient / limit.VelocityLimit;
var accelerationDuration = Math.Sqrt(distance * SmoothPtpAccelerationShapeCoefficient / (limit.AccelerationLimit * speedRatio * speedRatio)); var accelerationDuration = Math.Sqrt(distance * SmoothPtpAccelerationShapeCoefficient / limit.AccelerationLimit);
var jerkDuration = Math.Cbrt(distance * SmoothPtpJerkShapeCoefficient / (limit.JerkLimit * speedRatio * speedRatio * speedRatio)); var jerkDuration = Math.Cbrt(distance * SmoothPtpJerkShapeCoefficient / limit.JerkLimit);
duration = Math.Max(duration, Math.Max(velocityDuration, Math.Max(accelerationDuration, jerkDuration))); duration = Math.Max(duration, Math.Max(velocityDuration, Math.Max(accelerationDuration, jerkDuration)));
} }
return duration; return duration;
} }
/// <summary>
/// 验证 MoveJoint 稠密轨迹已经是物理伺服周期,不把 speed_ratio 编码到采样间隔里。
/// </summary>
private static void AssertDenseRowsUseServoPeriod(IReadOnlyList<IReadOnlyList<double>> rows, double servoPeriodSeconds)
{
Assert.True(rows.Count > 1);
for (var index = 1; index < rows.Count; index++)
{
Assert.Equal(servoPeriodSeconds, rows[index][0] - rows[index - 1][0], precision: 6);
}
}
private static double DegreesToRadians(double degrees) private static double DegreesToRadians(double degrees)
{ {
return degrees * Math.PI / 180.0; return degrees * Math.PI / 180.0;
@@ -1472,8 +1160,7 @@ public sealed class FanucControllerRuntimeDenseTests
usedCache: result.UsedCache, usedCache: result.UsedCache,
originalWaypointCount: result.OriginalWaypointCount, originalWaypointCount: result.OriginalWaypointCount,
plannedWaypointCount: result.PlannedWaypointCount, plannedWaypointCount: result.PlannedWaypointCount,
denseJointTrajectory: result.DenseJointTrajectory, denseJointTrajectory: result.DenseJointTrajectory);
preparedFlyshotExecution: result.PreparedFlyshotExecution);
} }
/// <summary> /// <summary>

View File

@@ -163,28 +163,6 @@ public sealed class FanucJ519ClientTests : IDisposable
await client.StopMotionAsync(_cts.Token); await client.StopMotionAsync(_cts.Token);
} }
/// <summary>
/// 验证直角坐标命令会把 Data format 写为 0,并按通用目标槽位写入 X/Y/Z/W/P/R。
/// </summary>
[Fact]
public void PackCommandPacket_WritesCartesianDataFormatAndTargetValues()
{
var command = new FanucJ519Command(
sequence: 7,
targetValues: [100.0, 200.0, 300.0, 1.0, 2.0, 3.0, 0.0, 0.0, 0.0],
dataStyle: 0);
var packet = FanucJ519Protocol.PackCommandPacket(command);
Assert.Equal(0, packet[0x12]);
Assert.Equal(100.0f, BinaryPrimitives.ReadSingleBigEndian(packet.AsSpan(0x1c, 4)));
Assert.Equal(200.0f, BinaryPrimitives.ReadSingleBigEndian(packet.AsSpan(0x20, 4)));
Assert.Equal(300.0f, BinaryPrimitives.ReadSingleBigEndian(packet.AsSpan(0x24, 4)));
Assert.Equal(1.0f, BinaryPrimitives.ReadSingleBigEndian(packet.AsSpan(0x28, 4)));
Assert.Equal(2.0f, BinaryPrimitives.ReadSingleBigEndian(packet.AsSpan(0x2c, 4)));
Assert.Equal(3.0f, BinaryPrimitives.ReadSingleBigEndian(packet.AsSpan(0x30, 4)));
}
/// <summary> /// <summary>
/// 验证配置 J519 buffer size 后,实际回发命令序号会在状态包序号基础上增加该缓冲深度。 /// 验证配置 J519 buffer size 后,实际回发命令序号会在状态包序号基础上增加该缓冲深度。
/// </summary> /// </summary>

View File

@@ -1,6 +1,4 @@
using Flyshot.Core.Planning.Sampling; using Flyshot.Core.Planning.Sampling;
using Flyshot.Core.Domain;
using Flyshot.Core.Planning;
namespace Flyshot.Core.Tests; namespace Flyshot.Core.Tests;
@@ -45,58 +43,6 @@ public sealed class J519SendTrajectorySamplerTests
Assert.Equal(180.0, samples[^1].JointsDegrees[0], precision: 6); Assert.Equal(180.0, samples[^1].JointsDegrees[0], precision: 6);
} }
/// <summary>
/// 验证飞拍 speed_ratio 安全队列从连续规划样条直接采样,而不是先把规划轨迹离散成稠密点后再线性插值。
/// </summary>
[Fact]
public void SamplePlannedTrajectory_DirectlyEvaluatesContinuousSplineForSpeedRatio()
{
var trajectory = CreateSingleJointTrajectory();
var samples = J519SendTrajectorySampler.SamplePlannedTrajectory(
trajectory,
servoPeriodSeconds: 0.008,
speedRatio: 0.5);
var sample = Assert.Single(samples.Where(static item => Math.Abs(item.TrajectoryTime - 0.004) < 1e-9));
Assert.Equal(0.008, sample.SendTime, precision: 6);
Assert.Equal(0.004, sample.TrajectoryTime, precision: 6);
Assert.NotEqual(45.0, sample.JointsDegrees[0], precision: 3);
var expected = new CubicSplineInterpolator(
trajectory.WaypointTimes.ToArray(),
trajectory.PlannedWaypoints.Select(static waypoint => waypoint.Positions.ToArray()).ToArray())
.Evaluate(0.004)[0] * 180.0 / Math.PI;
Assert.Equal(expected, sample.JointsDegrees[0], precision: 6);
}
/// <summary>
/// 验证 speed_ratio 等价于执行侧时间轴拉长:原轨迹 0.8 倍执行应与时间整体拉长 1/0.8 的轨迹 1 倍执行一致。
/// </summary>
[Fact]
public void SamplePlannedTrajectory_SpeedRatioMatchesUniformPlanningTimeStretch()
{
var trajectory = CreateSingleJointTrajectory();
var stretchedTrajectory = StretchTrajectoryTime(trajectory, stretchFactor: 1.0 / 0.8);
var ratioSamples = J519SendTrajectorySampler.SamplePlannedTrajectory(
trajectory,
servoPeriodSeconds: 0.008,
speedRatio: 0.8);
var stretchedSamples = J519SendTrajectorySampler.SamplePlannedTrajectory(
stretchedTrajectory,
servoPeriodSeconds: 0.008,
speedRatio: 1.0);
Assert.Equal(stretchedSamples.Count, ratioSamples.Count);
for (var index = 0; index < ratioSamples.Count; index++)
{
Assert.Equal(stretchedSamples[index].SendTime, ratioSamples[index].SendTime, precision: 6);
Assert.Equal(stretchedSamples[index].TrajectoryTime * 0.8, ratioSamples[index].TrajectoryTime, precision: 6);
Assert.Equal(stretchedSamples[index].JointsDegrees[0], ratioSamples[index].JointsDegrees[0], precision: 6);
}
}
/// <summary> /// <summary>
/// 验证空稠密轨迹会直接暴露为调用错误,避免生成无意义下发点。 /// 验证空稠密轨迹会直接暴露为调用错误,避免生成无意义下发点。
/// </summary> /// </summary>
@@ -150,84 +96,4 @@ public sealed class J519SendTrajectorySamplerTests
Assert.Equal([2.0, 0.016, 0.008, 0.5], row); Assert.Equal([2.0, 0.016, 0.008, 0.5], row);
} }
/// <summary>
/// 验证倍率诊断行在保留旧 4 列的同时追加请求倍率和历史改写次数。
/// </summary>
[Fact]
public void BuildTimingRow_WithRatioDiagnostics_AppendsRequestedRatioAndIterationCount()
{
var sample = new J519SendSample(
sampleIndex: 2,
sendTime: 0.016,
trajectoryTime: 0.0076,
speedRatio: 0.475,
jointsDegrees: [90.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
var row = J519SendTrajectorySampler.BuildTimingRow(
sample,
requestedSpeedRatio: 0.5,
stretchIterationCount: 1);
Assert.Equal([2.0, 0.016, 0.0076, 0.475, 0.5, 1.0], row);
}
/// <summary>
/// 构造一个最小单关节规划轨迹,便于验证 speed_ratio 直接样条采样语义。
/// </summary>
private static PlannedTrajectory CreateSingleJointTrajectory()
{
var robot = new RobotProfile(
name: "TestRobot",
modelPath: "Models/Test.robot",
degreesOfFreedom: 1,
jointLimits: [new JointLimit("J1", 100.0, 1000.0, 10000.0)],
jointCouplings: Array.Empty<JointCoupling>(),
servoPeriod: TimeSpan.FromMilliseconds(8),
triggerPeriod: TimeSpan.FromMilliseconds(8));
var program = new FlyshotProgram(
name: "spline-sample",
waypoints:
[
new JointWaypoint([0.0]),
new JointWaypoint([Math.PI / 2.0]),
new JointWaypoint([Math.PI])
],
shotFlags: [false, false, false],
offsetValues: [0, 0, 0],
addressGroups:
[
new IoAddressGroup(Array.Empty<int>()),
new IoAddressGroup(Array.Empty<int>()),
new IoAddressGroup(Array.Empty<int>())
]);
return new PlannedTrajectory(
robot: robot,
originalProgram: program,
plannedWaypoints: program.Waypoints,
waypointTimes: [0.0, 0.008, 0.016],
segmentDurations: [0.008, 0.008],
segmentScales: [1.0, 1.0],
method: PlanningMethod.Icsp,
iterations: 1,
threshold: 0.0);
}
/// <summary>
/// 构造统一拉长时间轴后的轨迹,模拟更低 planning_speed_scale 生成的等几何时间结果。
/// </summary>
private static PlannedTrajectory StretchTrajectoryTime(PlannedTrajectory trajectory, double stretchFactor)
{
return new PlannedTrajectory(
robot: trajectory.Robot,
originalProgram: trajectory.OriginalProgram,
plannedWaypoints: trajectory.PlannedWaypoints,
waypointTimes: trajectory.WaypointTimes.Select(time => time * stretchFactor).ToArray(),
segmentDurations: trajectory.SegmentDurations.Select(duration => duration * stretchFactor).ToArray(),
segmentScales: trajectory.SegmentScales.Select(scale => scale / stretchFactor).ToArray(),
method: trajectory.Method,
iterations: trajectory.Iterations,
threshold: trajectory.Threshold);
}
} }

View File

@@ -40,7 +40,7 @@ public sealed class OfflinePlanTests
// 1. 加载配置和模型。 // 1. 加载配置和模型。
var loadedConfig = new RobotConfigLoader().Load(resolvedConfigPath, repoRoot: workspaceRoot); var loadedConfig = new RobotConfigLoader().Load(resolvedConfigPath, repoRoot: workspaceRoot);
var program = loadedConfig.Programs[trajName]; var program = loadedConfig.Programs[trajName];
var resolvedRobotModelPath = Path.Combine(workspaceRoot, "flyshot-replacement", "Config", "Models", "LR_Mate_200iD_7L.json"); var resolvedRobotModelPath = Path.Combine(workspaceRoot, "flyshot-replacement", "Config", "LR_Mate_200iD_7L.json");
var loadedModel = new RobotModelLoader().LoadProfileAndKinematics( var loadedModel = new RobotModelLoader().LoadProfileAndKinematics(
resolvedRobotModelPath, resolvedRobotModelPath,
loadedConfig.Robot.AccLimitScale, loadedConfig.Robot.AccLimitScale,

View File

@@ -67,7 +67,7 @@ public sealed class PlanningCompatibilityTests
{ {
var workspaceRoot = GetWorkspaceRoot(); var workspaceRoot = GetWorkspaceRoot();
var configPath = Path.Combine(workspaceRoot, "Rvbust", "EOL9 EAU 90", "eol9_eau_90.json"); var configPath = Path.Combine(workspaceRoot, "Rvbust", "EOL9 EAU 90", "eol9_eau_90.json");
var modelPath = Path.Combine(workspaceRoot, "flyshot-replacement", "Config", "Models", "LR_Mate_200iD_7L.json"); var modelPath = Path.Combine(workspaceRoot, "flyshot-replacement", "Config", "LR_Mate_200iD_7L.json");
var config = new RobotConfigLoader().Load(configPath); var config = new RobotConfigLoader().Load(configPath);
var baseProfile = new RobotModelLoader().LoadProfile(modelPath, config.Robot.AccLimitScale, config.Robot.JerkLimitScale); var baseProfile = new RobotModelLoader().LoadProfile(modelPath, config.Robot.AccLimitScale, config.Robot.JerkLimitScale);
@@ -170,51 +170,6 @@ public sealed class PlanningCompatibilityTests
Assert.Equal(1, doEvent.WaypointIndex); Assert.Equal(1, doEvent.WaypointIndex);
Assert.Equal(1, doEvent.OffsetCycles); Assert.Equal(1, doEvent.OffsetCycles);
Assert.Equal(2, doEvent.HoldCycles); Assert.Equal(2, doEvent.HoldCycles);
Assert.NotNull(doEvent.ReferenceJointsDegrees);
Assert.Equal(RadiansToDegrees(1.0), doEvent.ReferenceJointsDegrees![0], precision: 6);
Assert.Equal(0.0, doEvent.ReferenceJointsDegrees![1], precision: 6);
}
/// <summary>
/// 验证时间轴会把示教点关节角保存到 DO 事件里,供运行时在近时窗内做最小关节差绑定。
/// </summary>
[Fact]
public void ShotTimelineBuilder_PopulatesReferenceJointsDegreesForRuntimeBinding()
{
var robot = CreateRobotProfile([1, 1], [1, 1], [1, 1]);
var program = new FlyshotProgram(
name: "demo",
waypoints:
[
new JointWaypoint([0.0, 0.0]),
new JointWaypoint([Math.PI / 6.0, -Math.PI / 4.0])
],
shotFlags: [false, true],
offsetValues: [0, 0],
addressGroups:
[
new IoAddressGroup(Array.Empty<int>()),
new IoAddressGroup([1])
]);
var trajectory = new PlannedTrajectory(
robot: robot,
originalProgram: program,
plannedWaypoints: program.Waypoints,
waypointTimes: [0.0, 0.5],
segmentDurations: [0.5],
segmentScales: [1.0],
method: PlanningMethod.Icsp,
iterations: 1,
threshold: 0.0);
var timeline = new ShotTimelineBuilder(new WaypointTimestampResolver())
.Build(trajectory, holdCycles: 1, samplePeriod: TimeSpan.FromMilliseconds(8));
var trigger = Assert.Single(timeline.TriggerTimeline);
Assert.NotNull(trigger.ReferenceJointsDegrees);
Assert.Equal(30.0, trigger.ReferenceJointsDegrees![0], precision: 6);
Assert.Equal(-45.0, trigger.ReferenceJointsDegrees![1], precision: 6);
} }
/// <summary> /// <summary>
@@ -239,174 +194,6 @@ public sealed class PlanningCompatibilityTests
Assert.Contains("dense-acceleration-check", exception.Message); Assert.Contains("dense-acceleration-check", exception.Message);
} }
/// <summary>
/// 验证飞拍链路临时关闭 jerk 复核时,仍保留速度/加速度校验,但不会再被 jerk 单独阻断。
/// </summary>
[Fact]
public void TrajectoryLimitValidator_CanSkipJerkValidation_ForFlyshotPath()
{
var robot = CreateRobotProfile([100.0], [1000.0], [10.0]);
IReadOnlyList<IReadOnlyList<double>> rows =
[
[0.0, 0.0],
[0.008, 0.0],
[0.016, 0.01],
[0.024, 0.02]
];
var exception = Assert.Throws<InvalidOperationException>(() =>
TrajectoryLimitValidator.ValidateDenseJointTrajectory(
robot,
rows,
trajectoryName: "dense-jerk-check"));
Assert.Contains("Jerk", exception.Message);
TrajectoryLimitValidator.ValidateDenseJointTrajectory(
robot,
rows,
trajectoryName: "dense-jerk-check-skip",
validateJerk: false);
}
/// <summary>
/// 验证飞拍最终 J519 队列的 jerk 硬约束使用严格限值,不再沿用历史上放大 4 倍的宽容口径。
/// </summary>
[Fact]
public void TrajectoryLimitValidator_ThrowsForJ519Jerk_WhenStrictFinalQueueValidationIsEnabled()
{
var robot = CreateRobotProfile([100.0], [1000.0], [100.0]);
var samples = new[]
{
new J519SendSample(0, 0.000, 0.000, 1.0, [0.0]),
new J519SendSample(1, 0.008, 0.008, 1.0, [0.0]),
new J519SendSample(2, 0.016, 0.016, 1.0, [0.003754]),
new J519SendSample(3, 0.024, 0.024, 1.0, [0.007508])
};
var exception = Assert.Throws<InvalidOperationException>(() =>
TrajectoryLimitValidator.ValidateJ519SendSamples(
robot,
samples,
trajectoryName: "strict-final-j519",
validateJerk: true));
Assert.Contains("Jerk", exception.Message);
Assert.Contains("strict-final-j519", exception.Message);
}
/// <summary>
/// 验证飞拍最终发送队列在请求倍率下严格 jerk 超限时直接拒绝执行,而不是自动改写 speed_ratio。
/// </summary>
[Fact]
public void FlyshotExecutionSendSequenceBuilder_ThrowsInsteadOfRewritingRequestedSpeedRatio()
{
var robot = CreateRobotProfile([100.0], [10000.0], [800.0]);
var program = CreateProgram(
[
[0.0],
[0.001]
]);
var trajectory = new PlannedTrajectory(
robot: robot,
originalProgram: program,
plannedWaypoints: program.Waypoints,
waypointTimes: [0.0, 0.024],
segmentDurations: [0.024],
segmentScales: [1.0],
method: PlanningMethod.Icsp,
iterations: 1,
threshold: 0.0);
var result = new TrajectoryResult(
programName: "strict-builder",
method: PlanningMethod.Icsp,
isValid: true,
duration: TimeSpan.FromSeconds(0.024),
shotEvents: Array.Empty<ShotEvent>(),
triggerTimeline: Array.Empty<TrajectoryDoEvent>(),
artifacts: Array.Empty<TrajectoryArtifact>(),
failureReason: null,
usedCache: false,
originalWaypointCount: 2,
plannedWaypointCount: 2);
var initialSamples = J519SendTrajectorySampler.SamplePlannedTrajectory(
trajectory,
servoPeriodSeconds: 0.008,
speedRatio: 1.0);
Assert.Throws<InvalidOperationException>(() =>
TrajectoryLimitValidator.ValidateJ519SendSamples(
robot,
initialSamples,
trajectoryName: "strict-builder-initial",
validateJerk: true));
var exception = Assert.Throws<InvalidOperationException>(() =>
FlyshotExecutionSendSequenceBuilder.Build(
robot,
trajectory,
result,
servoPeriodSeconds: 0.008,
requestedSpeedRatio: 1.0));
Assert.Contains("speed_ratio=1.000000", exception.Message);
Assert.Contains("planning_speed_scale", exception.Message);
}
/// <summary>
/// 验证飞拍最终发送队列通过严格校验时,请求 speed_ratio 会原样成为最终倍率。
/// </summary>
[Fact]
public void FlyshotExecutionSendSequenceBuilder_KeepsRequestedSpeedRatioWhenQueuePasses()
{
var robot = CreateRobotProfile([100.0], [10000.0], [10000.0]);
var program = CreateProgram(
[
[0.0],
[0.001]
]);
var trajectory = new PlannedTrajectory(
robot: robot,
originalProgram: program,
plannedWaypoints: program.Waypoints,
waypointTimes: [0.0, 0.024],
segmentDurations: [0.024],
segmentScales: [1.0],
method: PlanningMethod.Icsp,
iterations: 1,
threshold: 0.0);
var result = new TrajectoryResult(
programName: "rewrite-speedratio-builder",
method: PlanningMethod.Icsp,
isValid: true,
duration: TimeSpan.FromSeconds(0.024),
shotEvents: Array.Empty<ShotEvent>(),
triggerTimeline: Array.Empty<TrajectoryDoEvent>(),
artifacts: Array.Empty<TrajectoryArtifact>(),
failureReason: null,
usedCache: false,
originalWaypointCount: 2,
plannedWaypointCount: 2);
var prepared = FlyshotExecutionSendSequenceBuilder.Build(
robot,
trajectory,
result,
servoPeriodSeconds: 0.008,
requestedSpeedRatio: 0.8);
Assert.Equal(0.8, prepared.RequestSpeedRatio, precision: 6);
Assert.Equal(0.8, prepared.FinalSpeedRatio, precision: 6);
Assert.Equal(0, prepared.StretchIterationCount);
Assert.All(prepared.Samples, sample => Assert.Equal(0.8, sample.SpeedRatio, precision: 6));
Assert.All(prepared.TimingRows, row =>
{
Assert.Equal(0.8, row[3], precision: 6);
Assert.Equal(0.8, row[4], precision: 6);
Assert.Equal(0.0, row[5], precision: 6);
});
}
/// <summary> /// <summary>
/// 构造一个最小 RobotProfile,便于规划层单元测试聚焦在时间轴逻辑上。 /// 构造一个最小 RobotProfile,便于规划层单元测试聚焦在时间轴逻辑上。
/// </summary> /// </summary>
@@ -484,9 +271,4 @@ public sealed class PlanningCompatibilityTests
throw new DirectoryNotFoundException("Unable to locate the flyshot workspace root."); throw new DirectoryNotFoundException("Unable to locate the flyshot workspace root.");
} }
private static double RadiansToDegrees(double radians)
{
return radians * 180.0 / Math.PI;
}
} }

View File

@@ -158,83 +158,6 @@ public sealed class RuntimeOrchestrationTests
$"半速规划时长应接近全速的 2 倍,实际 full={fullSpeed.Result.Duration.TotalSeconds}, half={halfSpeed.Result.Duration.TotalSeconds}"); $"半速规划时长应接近全速的 2 倍,实际 full={fullSpeed.Result.Duration.TotalSeconds}, half={halfSpeed.Result.Duration.TotalSeconds}");
} }
/// <summary>
/// 验证飞拍生成轨迹时会把 speed_ratio 折算进有效规划倍率,使倍率乘积相同的配置生成等价轨迹。
/// </summary>
[Fact]
public void ControllerClientTrajectoryOrchestrator_PlanUploadedFlyshot_UsesSpeedRatioAsPlanningScaleFactor()
{
var orchestrator = new ControllerClientTrajectoryOrchestrator();
var robot = TestRobotFactory.CreateRobotProfile();
var uploaded = TestRobotFactory.CreateUploadedTrajectoryWithSingleShot();
var planningSideSlowdown = CreateFlyshotSettings(planningSpeedScale: 0.45, speedRatio: 1.0);
var runtimeSideSlowdown = CreateFlyshotSettings(planningSpeedScale: 0.9, speedRatio: 0.5);
var planningSideBundle = orchestrator.PlanUploadedFlyshot(robot, uploaded, settings: planningSideSlowdown);
var runtimeSideBundle = orchestrator.PlanUploadedFlyshot(robot, uploaded, settings: runtimeSideSlowdown);
Assert.Equal(planningSideBundle.Result.Duration.TotalSeconds, runtimeSideBundle.Result.Duration.TotalSeconds, precision: 6);
Assert.Equal(planningSideBundle.ExecutionTrajectory.WaypointTimes.Count, runtimeSideBundle.ExecutionTrajectory.WaypointTimes.Count);
for (var index = 0; index < planningSideBundle.ExecutionTrajectory.WaypointTimes.Count; index++)
{
Assert.Equal(
planningSideBundle.ExecutionTrajectory.WaypointTimes[index],
runtimeSideBundle.ExecutionTrajectory.WaypointTimes[index],
precision: 6);
}
Assert.Equal(planningSideBundle.Result.DenseJointTrajectory!.Count, runtimeSideBundle.Result.DenseJointTrajectory!.Count);
for (var rowIndex = 0; rowIndex < planningSideBundle.Result.DenseJointTrajectory.Count; rowIndex++)
{
var planningSideRow = planningSideBundle.Result.DenseJointTrajectory[rowIndex];
var runtimeSideRow = runtimeSideBundle.Result.DenseJointTrajectory[rowIndex];
Assert.Equal(planningSideRow.Count, runtimeSideRow.Count);
for (var columnIndex = 0; columnIndex < planningSideRow.Count; columnIndex++)
{
Assert.Equal(planningSideRow[columnIndex], runtimeSideRow[columnIndex], precision: 6);
}
}
}
/// <summary>
/// 验证普通轨迹生成也会把 speed_ratio 折算进规划倍率,避免把限速留到发送阶段处理。
/// </summary>
[Fact]
public void ControllerClientTrajectoryOrchestrator_PlanOrdinaryTrajectory_UsesSpeedRatioAsPlanningScaleFactor()
{
var orchestrator = new ControllerClientTrajectoryOrchestrator();
var robot = TestRobotFactory.CreateRobotProfile();
var waypoints = new[]
{
new[] { 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 },
new[] { 0.001, 0.0, 0.0, 0.0, 0.0, 0.0 },
new[] { 0.002, 0.0, 0.0, 0.0, 0.0, 0.0 },
new[] { 0.003, 0.0, 0.0, 0.0, 0.0, 0.0 }
};
var planningSideBundle = orchestrator.PlanOrdinaryTrajectory(
robot,
waypoints,
planningSpeedScale: 0.45);
var runtimeSideBundle = orchestrator.PlanOrdinaryTrajectory(
robot,
waypoints,
planningSpeedScale: 0.9,
speedRatio: 0.5);
Assert.Equal(planningSideBundle.Result.Duration.TotalSeconds, runtimeSideBundle.Result.Duration.TotalSeconds, precision: 6);
Assert.Equal(planningSideBundle.Result.DenseJointTrajectory!.Count, runtimeSideBundle.Result.DenseJointTrajectory!.Count);
for (var rowIndex = 0; rowIndex < planningSideBundle.Result.DenseJointTrajectory.Count; rowIndex++)
{
var planningSideRow = planningSideBundle.Result.DenseJointTrajectory[rowIndex];
var runtimeSideRow = runtimeSideBundle.Result.DenseJointTrajectory[rowIndex];
for (var columnIndex = 0; columnIndex < planningSideRow.Count; columnIndex++)
{
Assert.Equal(planningSideRow[columnIndex], runtimeSideRow[columnIndex], precision: 6);
}
}
}
/// <summary> /// <summary>
/// 验证飞拍缓存键包含规划限速倍率,避免降速验证时误用 100% 速度下的规划结果。 /// 验证飞拍缓存键包含规划限速倍率,避免降速验证时误用 100% 速度下的规划结果。
/// </summary> /// </summary>
@@ -266,7 +189,6 @@ public sealed class RuntimeOrchestrationTests
useDo: true, useDo: true,
ioAddresses: [7, 8], ioAddresses: [7, 8],
ioKeepCycles: 4, ioKeepCycles: 4,
triggerSampleIndexOffsetCycles: 7,
accLimitScale: 1.0, accLimitScale: 1.0,
jerkLimitScale: 1.0, jerkLimitScale: 1.0,
adaptIcspTryNum: 5); adaptIcspTryNum: 5);
@@ -290,7 +212,6 @@ public sealed class RuntimeOrchestrationTests
useDo: false, useDo: false,
ioAddresses: [7, 8], ioAddresses: [7, 8],
ioKeepCycles: 4, ioKeepCycles: 4,
triggerSampleIndexOffsetCycles: 7,
accLimitScale: 1.0, accLimitScale: 1.0,
jerkLimitScale: 1.0, jerkLimitScale: 1.0,
adaptIcspTryNum: 5); adaptIcspTryNum: 5);
@@ -391,37 +312,35 @@ public sealed class RuntimeOrchestrationTests
{ {
var fixture = LoadUttcMs11RuntimeFixture(); var fixture = LoadUttcMs11RuntimeFixture();
var orchestrator = new ControllerClientTrajectoryOrchestrator(); var orchestrator = new ControllerClientTrajectoryOrchestrator();
var legacyFitSettings = DisableSmoothStartStopTiming(fixture.Settings);
var bundle = orchestrator.PlanUploadedFlyshot( var bundle = orchestrator.PlanUploadedFlyshot(
fixture.Robot, fixture.Robot,
fixture.Uploaded, fixture.Uploaded,
settings: legacyFitSettings); settings: fixture.Settings);
var baselineBundle = orchestrator.PlanUploadedFlyshot( var baselineBundle = orchestrator.PlanUploadedFlyshot(
fixture.Robot, fixture.Robot,
fixture.Uploaded, fixture.Uploaded,
settings: EnableSmoothStartStopTiming(fixture.Settings), settings: EnableSmoothStartStopTiming(fixture.Settings),
planningSpeedScale: 1.0); planningSpeedScale: 1.0);
var executionDense = TrajectorySampler.SampleJointTrajectory( var rawDense = TrajectorySampler.SampleJointTrajectory(
bundle.ExecutionTrajectory, bundle.PlannedTrajectory,
samplePeriod: fixture.Robot.ServoPeriod.TotalSeconds); samplePeriod: fixture.Robot.ServoPeriod.TotalSeconds);
Assert.Equal(0.74227, legacyFitSettings.PlanningSpeedScale, precision: 6); Assert.Equal(0.7422771653721995, fixture.Settings.PlanningSpeedScale, precision: 12);
Assert.False(legacyFitSettings.SmoothStartStopTiming); Assert.False(fixture.Settings.SmoothStartStopTiming);
Assert.Equal(fixture.Uploaded.Waypoints.Count, bundle.PlannedTrajectory.WaypointTimes.Count); Assert.Equal(fixture.Uploaded.Waypoints.Count, bundle.PlannedTrajectory.WaypointTimes.Count);
Assert.Equal( Assert.Equal(
baselineBundle.PlannedTrajectory.WaypointTimes[^1] / legacyFitSettings.PlanningSpeedScale, baselineBundle.PlannedTrajectory.WaypointTimes[^1] / fixture.Settings.PlanningSpeedScale,
bundle.PlannedTrajectory.WaypointTimes[^1], bundle.PlannedTrajectory.WaypointTimes[^1],
precision: 6); precision: 6);
Assert.Equal(7.403046, bundle.PlannedTrajectory.WaypointTimes[^1], precision: 3); Assert.Equal(7.403046, bundle.PlannedTrajectory.WaypointTimes[^1], precision: 3);
// 关闭二次时间重映射时,稠密点应和最终执行时间轴自洽;若离散限幅额外拉长,也要反映在 ExecutionTrajectory 上。 // 关闭二次时间重映射时,运行时稠密点应直接使用规划样条采样,避免再次改变通用规划时间轴。
Assert.Equal(executionDense.Count, bundle.Result.DenseJointTrajectory!.Count); Assert.Equal(rawDense.Count, bundle.Result.DenseJointTrajectory!.Count);
Assert.Equal(executionDense[1][1], bundle.Result.DenseJointTrajectory[1][1], precision: 12); Assert.Equal(rawDense[1][1], bundle.Result.DenseJointTrajectory[1][1], precision: 12);
Assert.True(bundle.ExecutionTrajectory.WaypointTimes[^1] >= bundle.PlannedTrajectory.WaypointTimes[^1]);
Assert.True( Assert.True(
Math.Abs(bundle.ExecutionTrajectory.WaypointTimes[^1] - bundle.Result.Duration.TotalSeconds) < 1e-6, Math.Abs(bundle.PlannedTrajectory.WaypointTimes[^1] - bundle.Result.Duration.TotalSeconds) < 1e-6,
$"执行时长应保留最终执行时间轴终点,execution={bundle.ExecutionTrajectory.WaypointTimes[^1]}, result={bundle.Result.Duration.TotalSeconds}"); $"执行时长应保留规划终点时间,planned={bundle.PlannedTrajectory.WaypointTimes[^1]}, result={bundle.Result.Duration.TotalSeconds}");
} }
/// <summary> /// <summary>
@@ -776,54 +695,6 @@ public sealed class RuntimeOrchestrationTests
Assert.Throws<ArgumentException>(Act); Assert.Throws<ArgumentException>(Act);
} }
/// <summary>
/// 验证兼容服务执行普通轨迹时会把运行时 speed_ratio 前移到规划阶段。
/// </summary>
[Fact]
public void ControllerClientCompatService_ExecuteTrajectory_PassesSpeedRatioIntoOrdinaryPlanning()
{
var configRoot = CreateTempConfigRoot();
try
{
WriteRobotConfigWithDemoTrajectory(configRoot, planningSpeedScale: 0.9, speedRatio: 1.0);
var options = new ControllerClientCompatOptions { ConfigRoot = configRoot };
var runtime = new RecordingControllerRuntime();
var service = new ControllerClientCompatService(
options,
new ControllerClientCompatRobotCatalog(options, new RobotModelLoader()),
runtime,
new ControllerClientTrajectoryOrchestrator(),
new RobotConfigLoader());
var waypoints = new[]
{
new[] { 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 },
new[] { 0.001, 0.0, 0.0, 0.0, 0.0, 0.0 },
new[] { 0.002, 0.0, 0.0, 0.0, 0.0, 0.0 },
new[] { 0.003, 0.0, 0.0, 0.0, 0.0, 0.0 }
};
service.SetUpRobot("FANUC_LR_Mate_200iD");
service.SetActiveController(sim: false);
service.Connect("192.168.10.101");
service.EnableRobot(2);
runtime.SetSpeedRatio(0.5);
service.ExecuteTrajectory(waypoints);
var robot = new ControllerClientCompatRobotCatalog(options, new RobotModelLoader())
.LoadProfile("FANUC_LR_Mate_200iD", accLimitScale: 1.0, jerkLimitScale: 1.0);
var expected = new ControllerClientTrajectoryOrchestrator()
.PlanOrdinaryTrajectory(robot, waypoints, planningSpeedScale: 0.45);
var actual = Assert.IsType<TrajectoryResult>(runtime.LastExecutedResult);
Assert.Equal(expected.Result.Duration.TotalSeconds, actual.Duration.TotalSeconds, precision: 6);
Assert.Equal(expected.Result.DenseJointTrajectory!.Count, actual.DenseJointTrajectory!.Count);
}
finally
{
Directory.Delete(configRoot, recursive: true);
}
}
/// <summary> /// <summary>
/// 验证 ExecuteFlyShotTraj(move_to_start=true) 会先执行稠密 PTP 到起点,并等待该段运动完成后再启动飞拍轨迹。 /// 验证 ExecuteFlyShotTraj(move_to_start=true) 会先执行稠密 PTP 到起点,并等待该段运动完成后再启动飞拍轨迹。
/// </summary> /// </summary>
@@ -833,7 +704,6 @@ public sealed class RuntimeOrchestrationTests
var configRoot = CreateTempConfigRoot(); var configRoot = CreateTempConfigRoot();
try try
{ {
WriteRobotConfigWithDemoTrajectory(configRoot);
var options = new ControllerClientCompatOptions var options = new ControllerClientCompatOptions
{ {
ConfigRoot = configRoot ConfigRoot = configRoot
@@ -878,7 +748,6 @@ public sealed class RuntimeOrchestrationTests
var configRoot = CreateTempConfigRoot(); var configRoot = CreateTempConfigRoot();
try try
{ {
WriteRobotConfigWithDemoTrajectory(configRoot);
var options = new ControllerClientCompatOptions var options = new ControllerClientCompatOptions
{ {
ConfigRoot = configRoot ConfigRoot = configRoot
@@ -922,7 +791,6 @@ public sealed class RuntimeOrchestrationTests
var configRoot = CreateTempConfigRoot(); var configRoot = CreateTempConfigRoot();
try try
{ {
WriteRobotConfigWithDemoTrajectory(configRoot);
var options = new ControllerClientCompatOptions var options = new ControllerClientCompatOptions
{ {
ConfigRoot = configRoot ConfigRoot = configRoot
@@ -956,153 +824,6 @@ public sealed class RuntimeOrchestrationTests
} }
} }
/// <summary>
/// 验证飞拍执行阻塞在运行时时,状态页元数据快照仍能通过短锁快速返回。
/// </summary>
[Fact]
public async Task ControllerClientCompatService_GetStatusSnapshotMetadata_DoesNotWaitForRunningFlyshot()
{
var configRoot = CreateTempConfigRoot();
try
{
WriteRobotConfigWithDemoTrajectory(configRoot);
var options = new ControllerClientCompatOptions
{
ConfigRoot = configRoot
};
var runtime = new BlockingExecutionControllerRuntime([0.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
var service = new ControllerClientCompatService(
options,
new ControllerClientCompatRobotCatalog(options, new RobotModelLoader()),
runtime,
new ControllerClientTrajectoryOrchestrator(),
new RobotConfigLoader());
service.SetUpRobot("FANUC_LR_Mate_200iD");
service.SetActiveController(sim: false);
service.Connect("192.168.10.101");
service.EnableRobot(2);
service.UploadTrajectory(TestRobotFactory.CreateUploadedTrajectoryWithSingleShot());
var executing = Task.Run(() => service.ExecuteTrajectoryByName(
"demo-flyshot",
new FlyshotExecutionOptions(moveToStart: false, method: "icsp", saveTrajectory: false, useCache: false, wait: true)));
Assert.True(runtime.WaitForExecutionStarted(TimeSpan.FromSeconds(2)));
var metadataTask = Task.Run(() => service.GetStatusSnapshotMetadata());
var completed = await Task.WhenAny(metadataTask, Task.Delay(TimeSpan.FromMilliseconds(150)));
runtime.ReleaseExecution();
await executing;
Assert.Same(metadataTask, completed);
var metadata = await metadataTask;
Assert.True(metadata.IsSetup);
Assert.Equal("FANUC_LR_Mate_200iD", metadata.RobotName);
Assert.Equal(["demo-flyshot"], metadata.UploadedTrajectories);
}
finally
{
Directory.Delete(configRoot, recursive: true);
}
}
/// <summary>
/// 验证两个飞拍执行命令必须串行进入 runtime,避免 J519 队列被并发执行覆盖。
/// </summary>
[Fact]
public async Task ControllerClientCompatService_ExecuteTrajectoryByName_SerializesConcurrentExecutionCommands()
{
var configRoot = CreateTempConfigRoot();
try
{
WriteRobotConfigWithDemoTrajectory(configRoot);
var options = new ControllerClientCompatOptions
{
ConfigRoot = configRoot
};
var runtime = new BlockingExecutionControllerRuntime([0.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
var service = new ControllerClientCompatService(
options,
new ControllerClientCompatRobotCatalog(options, new RobotModelLoader()),
runtime,
new ControllerClientTrajectoryOrchestrator(),
new RobotConfigLoader());
service.SetUpRobot("FANUC_LR_Mate_200iD");
service.SetActiveController(sim: false);
service.Connect("192.168.10.101");
service.EnableRobot(2);
service.UploadTrajectory(TestRobotFactory.CreateUploadedTrajectoryWithSingleShot());
var first = Task.Run(() => service.ExecuteTrajectoryByName(
"demo-flyshot",
new FlyshotExecutionOptions(moveToStart: false, method: "icsp", saveTrajectory: false, useCache: false, wait: true)));
Assert.True(runtime.WaitForExecutionStarted(TimeSpan.FromSeconds(2)));
var second = Task.Run(() => service.ExecuteTrajectoryByName(
"demo-flyshot",
new FlyshotExecutionOptions(moveToStart: false, method: "icsp", saveTrajectory: false, useCache: false, wait: true)));
await Task.Delay(TimeSpan.FromMilliseconds(100));
Assert.Equal(1, runtime.ExecuteCallCount);
runtime.ReleaseExecution();
await first;
Assert.True(runtime.WaitForExecutionStarted(TimeSpan.FromSeconds(2), expectedCallCount: 2));
runtime.ReleaseExecution();
await second;
Assert.Equal(2, runtime.ExecuteCallCount);
}
finally
{
Directory.Delete(configRoot, recursive: true);
}
}
/// <summary>
/// 验证飞拍链路在进入运行时前就会准备最终发送队列,而不是把 speedRatio 重采样留给运行时临场处理。
/// </summary>
[Fact]
public void ControllerClientCompatService_ExecuteTrajectoryByName_PreparesFinalSendQueueBeforeRuntime()
{
var configRoot = CreateTempConfigRoot();
try
{
WriteRobotConfigWithDemoTrajectory(configRoot);
var options = new ControllerClientCompatOptions { ConfigRoot = configRoot };
var runtime = new RecordingControllerRuntime();
var service = new ControllerClientCompatService(
options,
new ControllerClientCompatRobotCatalog(options, new RobotModelLoader()),
runtime,
new ControllerClientTrajectoryOrchestrator(),
new RobotConfigLoader());
service.SetUpRobot("FANUC_LR_Mate_200iD");
service.SetActiveController(sim: false);
service.Connect("192.168.10.101");
service.EnableRobot(2);
service.UploadTrajectory(TestRobotFactory.CreateUploadedTrajectoryWithSingleShot());
runtime.SetSpeedRatio(0.5);
service.ExecuteTrajectoryByName(
"demo-flyshot",
new FlyshotExecutionOptions(moveToStart: false, method: "icsp", saveTrajectory: false, useCache: false));
var result = Assert.IsType<TrajectoryResult>(runtime.LastExecutedResult);
var preparedExecution = Assert.IsType<FlyshotPreparedExecution>(result.PreparedFlyshotExecution);
Assert.NotEmpty(preparedExecution.Samples);
Assert.Equal(preparedExecution.Samples.Count, preparedExecution.TimingRows.Count);
Assert.Equal(result.TriggerTimeline.Count, preparedExecution.TriggerBindings.Count);
}
finally
{
Directory.Delete(configRoot, recursive: true);
}
}
/// <summary> /// <summary>
/// 验证兼容服务初始化机器人时会把 RobotConfig.json 中的 acc_limit / jerk_limit 传给模型加载器。 /// 验证兼容服务初始化机器人时会把 RobotConfig.json 中的 acc_limit / jerk_limit 传给模型加载器。
/// </summary> /// </summary>
@@ -1122,8 +843,7 @@ public sealed class RuntimeOrchestrationTests
"io_keep_cycles": 4, "io_keep_cycles": 4,
"acc_limit": 0.5, "acc_limit": 0.5,
"jerk_limit": 0.25, "jerk_limit": 0.25,
"adapt_icsp_try_num": 3, "adapt_icsp_try_num": 3
"speed_ratio": 0.65
}, },
"flying_shots": {} "flying_shots": {}
} }
@@ -1143,7 +863,6 @@ public sealed class RuntimeOrchestrationTests
var profile = Assert.IsType<RobotProfile>(runtime.LastRobotProfile); var profile = Assert.IsType<RobotProfile>(runtime.LastRobotProfile);
Assert.Equal(14.905, profile.JointLimits[2].AccelerationLimit, precision: 3); Assert.Equal(14.905, profile.JointLimits[2].AccelerationLimit, precision: 3);
Assert.Equal(62.115, profile.JointLimits[2].JerkLimit, precision: 3); Assert.Equal(62.115, profile.JointLimits[2].JerkLimit, precision: 3);
Assert.Equal(0.65, runtime.GetSpeedRatio(), precision: 6);
} }
finally finally
{ {
@@ -1195,7 +914,7 @@ public sealed class RuntimeOrchestrationTests
} }
/// <summary> /// <summary>
/// 验证 SaveTrajectoryInfo 会导出按 J519 8ms 实发周期重采样的点位,并保留当前 speed_ratio 诊断信息。 /// 验证 SaveTrajectoryInfo 会同时导出按 J519 8ms 实发周期重采样的点位,并按执行侧稠密轨迹时长应用当前 speed_ratio。
/// </summary> /// </summary>
[Fact] [Fact]
public void ControllerClientCompatService_SaveTrajectoryInfo_ExportsActualSendRowsWithSpeedRatio() public void ControllerClientCompatService_SaveTrajectoryInfo_ExportsActualSendRowsWithSpeedRatio()
@@ -1221,31 +940,22 @@ public sealed class RuntimeOrchestrationTests
var outputDir = Path.Combine(configRoot, "Data", "demo-flyshot"); var outputDir = Path.Combine(configRoot, "Data", "demo-flyshot");
var pointsPath = Path.Combine(outputDir, "ActualSendJointTraj.txt"); var pointsPath = Path.Combine(outputDir, "ActualSendJointTraj.txt");
var timingPath = Path.Combine(outputDir, "ActualSendTiming.txt"); var timingPath = Path.Combine(outputDir, "ActualSendTiming.txt");
var shotEventsPath = Path.Combine(outputDir, "ShotEvents.json");
Assert.True(File.Exists(pointsPath)); Assert.True(File.Exists(pointsPath));
Assert.True(File.Exists(timingPath)); Assert.True(File.Exists(timingPath));
Assert.True(File.Exists(shotEventsPath));
var pointRows = File.ReadAllLines(pointsPath).Select(ParseSpaceSeparatedDoubles).ToArray(); var pointRows = File.ReadAllLines(pointsPath).Select(ParseSpaceSeparatedDoubles).ToArray();
var timingRows = File.ReadAllLines(timingPath).Select(ParseSpaceSeparatedDoubles).ToArray(); var timingRows = File.ReadAllLines(timingPath).Select(ParseSpaceSeparatedDoubles).ToArray();
var shotEventsJson = File.ReadAllText(shotEventsPath); var executionDuration = double.Parse(
var planningExportDuration = double.Parse(
File.ReadLines(Path.Combine(outputDir, "JointDetialTraj.txt")).Last().Split(' ')[0], File.ReadLines(Path.Combine(outputDir, "JointDetialTraj.txt")).Last().Split(' ')[0],
CultureInfo.InvariantCulture); CultureInfo.InvariantCulture);
var planningExportRowsAtServoPeriod = (int)Math.Ceiling(Math.Max(0.0, (planningExportDuration / 0.008) - 1e-9)) + 1; var expectedRows = (int)Math.Ceiling(Math.Max(0.0, (executionDuration / (0.008 * 0.5)) - 1e-9)) + 1;
Assert.Equal(pointRows.Length, timingRows.Length); Assert.Equal(expectedRows, pointRows.Length);
Assert.True( Assert.Equal(expectedRows, timingRows.Length);
pointRows.Length > planningExportRowsAtServoPeriod,
$"ActualSend 应按当前 speedRatio 的执行视图导出,不能再用旧 JointDetialTraj 规划时长约束点数。actual={pointRows.Length}, planningRows={planningExportRowsAtServoPeriod}");
Assert.Equal(0.0, pointRows[0][0], precision: 6); Assert.Equal(0.0, pointRows[0][0], precision: 6);
Assert.Equal(0.008, pointRows[1][0], precision: 6); Assert.Equal(0.008, pointRows[1][0], precision: 6);
Assert.Equal(0.008, timingRows[1][2], precision: 6); Assert.Equal(0.004, timingRows[1][2], precision: 6);
Assert.Equal(1.0, timingRows[1][3], precision: 6); Assert.Equal(0.5, timingRows[1][3], precision: 6);
Assert.Equal(0.5, timingRows[1][4], precision: 6);
Assert.Equal(0.0, timingRows[1][5], precision: 6);
Assert.Contains("\"trigger_window_seconds\": 0.1", shotEventsJson);
Assert.Contains("\"selected_sample_index\"", shotEventsJson);
} }
finally finally
{ {
@@ -1254,56 +964,11 @@ public sealed class RuntimeOrchestrationTests
} }
/// <summary> /// <summary>
/// 验证 SaveTrajectoryInfo 的旧格式规划导出只受 planning_speed_scale 影响,不跟随运行时 speed_ratio 改变。 /// 验证 saveTrajectory 导出的 JointDetialTraj.txt 来自执行侧 8ms 稠密轨迹的 16ms 视图,
/// 而不是再次从 PlannedTrajectory 独立重采样。
/// </summary> /// </summary>
[Fact] [Fact]
public void ControllerClientCompatService_SaveTrajectoryInfo_KeepsLegacyPlanningExportsIndependentFromSpeedRatio() public void FlyshotTrajectoryArtifactWriter_WriteUploadedFlyshot_JointDetailUsesExecutionDenseDownsample()
{
var fullSpeedRoot = CreateTempConfigRoot();
var halfSpeedRoot = CreateTempConfigRoot();
try
{
WriteRobotConfigWithDemoTrajectory(fullSpeedRoot, planningSpeedScale: 0.45, speedRatio: 1.0);
WriteRobotConfigWithDemoTrajectory(halfSpeedRoot, planningSpeedScale: 0.45, speedRatio: 0.5);
SaveDemoTrajectoryInfo(fullSpeedRoot);
SaveDemoTrajectoryInfo(halfSpeedRoot);
var fullSpeedOutput = Path.Combine(fullSpeedRoot, "Data", "demo-flyshot");
var halfSpeedOutput = Path.Combine(halfSpeedRoot, "Data", "demo-flyshot");
AssertNumericFilesEqual(
Path.Combine(fullSpeedOutput, "JointTraj.txt"),
Path.Combine(halfSpeedOutput, "JointTraj.txt"));
AssertNumericFilesEqual(
Path.Combine(fullSpeedOutput, "JointDetialTraj.txt"),
Path.Combine(halfSpeedOutput, "JointDetialTraj.txt"));
AssertNumericFilesEqual(
Path.Combine(fullSpeedOutput, "CartTraj.txt"),
Path.Combine(halfSpeedOutput, "CartTraj.txt"));
AssertNumericFilesEqual(
Path.Combine(fullSpeedOutput, "CartDetialTraj.txt"),
Path.Combine(halfSpeedOutput, "CartDetialTraj.txt"));
var fullSpeedActualSend = File.ReadAllLines(Path.Combine(fullSpeedOutput, "ActualSendJointTraj.txt"));
var halfSpeedActualSend = File.ReadAllLines(Path.Combine(halfSpeedOutput, "ActualSendJointTraj.txt"));
var fullSpeedTiming = File.ReadAllLines(Path.Combine(fullSpeedOutput, "ActualSendTiming.txt"));
var halfSpeedTiming = File.ReadAllLines(Path.Combine(halfSpeedOutput, "ActualSendTiming.txt"));
Assert.NotEqual(fullSpeedActualSend.Length, halfSpeedActualSend.Length);
Assert.NotEqual(fullSpeedTiming.Length, halfSpeedTiming.Length);
}
finally
{
Directory.Delete(fullSpeedRoot, recursive: true);
Directory.Delete(halfSpeedRoot, recursive: true);
}
}
/// <summary>
/// 验证 saveTrajectory 导出的 JointDetialTraj.txt 来自旧规划导出视图的 16ms 明细轨迹,
/// 而不是来自 speed_ratio 后的实发执行视图。
/// </summary>
[Fact]
public void FlyshotTrajectoryArtifactWriter_WriteUploadedFlyshot_JointDetailUsesExportPlanningDenseDownsample()
{ {
var fixture = LoadUttcMs11RuntimeFixture(); var fixture = LoadUttcMs11RuntimeFixture();
var configRoot = CreateTempConfigRoot(); var configRoot = CreateTempConfigRoot();
@@ -1312,26 +977,20 @@ public sealed class RuntimeOrchestrationTests
var options = new ControllerClientCompatOptions { ConfigRoot = configRoot }; var options = new ControllerClientCompatOptions { ConfigRoot = configRoot };
var writer = new FlyshotTrajectoryArtifactWriter(options, new RobotModelLoader()); var writer = new FlyshotTrajectoryArtifactWriter(options, new RobotModelLoader());
var orchestrator = new ControllerClientTrajectoryOrchestrator(); var orchestrator = new ControllerClientTrajectoryOrchestrator();
var exportBundle = orchestrator.PlanUploadedFlyshot( var bundle = orchestrator.PlanUploadedFlyshot(
fixture.Robot, fixture.Robot,
fixture.Uploaded, fixture.Uploaded,
settings: EnableSmoothStartStopTiming(fixture.Settings), settings: EnableSmoothStartStopTiming(fixture.Settings),
planningSpeedScale: 1.0); planningSpeedScale: 1.0);
var executionBundle = orchestrator.PlanUploadedFlyshot(
fixture.Robot,
fixture.Uploaded,
settings: EnableSmoothStartStopTiming(fixture.Settings),
planningSpeedScale: 1.0,
speedRatio: 0.5);
writer.WriteUploadedFlyshot("UTTC_MS11", fixture.Robot, exportBundle, executionBundle, speedRatio: 0.5); writer.WriteUploadedFlyshot("UTTC_MS11", fixture.Robot, bundle, speedRatio: 1.0);
var outputDir = Path.Combine(configRoot, "Data", "UTTC_MS11"); var outputDir = Path.Combine(configRoot, "Data", "UTTC_MS11");
var exportedRows = File.ReadAllLines(Path.Combine(outputDir, "JointDetialTraj.txt")) var exportedRows = File.ReadAllLines(Path.Combine(outputDir, "JointDetialTraj.txt"))
.Select(ParseSpaceSeparatedDoubles) .Select(ParseSpaceSeparatedDoubles)
.ToArray(); .ToArray();
var expectedRows = DownsampleDenseRows( var expectedRows = DownsampleDenseRows(
exportBundle.Result.DenseJointTrajectory!, bundle.Result.DenseJointTrajectory!,
samplePeriodSeconds: 0.016) samplePeriodSeconds: 0.016)
.Select(static row => row.ToArray()) .Select(static row => row.ToArray())
.ToArray(); .ToArray();
@@ -1375,21 +1034,16 @@ public sealed class RuntimeOrchestrationTests
/// 写入包含一条飞拍轨迹的最小 RobotConfig.json,供兼容服务从统一配置恢复轨迹。 /// 写入包含一条飞拍轨迹的最小 RobotConfig.json,供兼容服务从统一配置恢复轨迹。
/// </summary> /// </summary>
/// <param name="configRoot">测试运行配置根。</param> /// <param name="configRoot">测试运行配置根。</param>
private static void WriteRobotConfigWithDemoTrajectory( private static void WriteRobotConfigWithDemoTrajectory(string configRoot)
string configRoot,
double planningSpeedScale = 0.45,
double speedRatio = 1.0)
{ {
File.WriteAllText( File.WriteAllText(
Path.Combine(configRoot, "RobotConfig.json"), Path.Combine(configRoot, "RobotConfig.json"),
$$""" """
{ {
"robot": { "robot": {
"use_do": true, "use_do": true,
"io_addr": [7, 8], "io_addr": [7, 8],
"io_keep_cycles": 2, "io_keep_cycles": 2,
"speed_ratio": {{speedRatio.ToString(CultureInfo.InvariantCulture)}},
"planning_speed_scale": {{planningSpeedScale.ToString(CultureInfo.InvariantCulture)}},
"acc_limit": 1.0, "acc_limit": 1.0,
"jerk_limit": 1.0, "jerk_limit": 1.0,
"adapt_icsp_try_num": 5 "adapt_icsp_try_num": 5
@@ -1398,9 +1052,9 @@ public sealed class RuntimeOrchestrationTests
"demo-flyshot": { "demo-flyshot": {
"traj_waypoints": [ "traj_waypoints": [
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
[0.001, 0.0, 0.0, 0.0, 0.0, 0.0], [0.1, 0.0, 0.0, 0.0, 0.0, 0.0],
[0.002, 0.0, 0.0, 0.0, 0.0, 0.0], [0.2, 0.0, 0.0, 0.0, 0.0, 0.0],
[0.003, 0.0, 0.0, 0.0, 0.0, 0.0] [0.3, 0.0, 0.0, 0.0, 0.0, 0.0]
], ],
"shot_flags": [false, true, false, false], "shot_flags": [false, true, false, false],
"offset_values": [0, 1, 0, 0], "offset_values": [0, 1, 0, 0],
@@ -1411,42 +1065,6 @@ public sealed class RuntimeOrchestrationTests
"""); """);
} }
/// <summary>
/// 使用指定运行配置根执行 demo-flyshot 的 SaveTrajectoryInfo。
/// </summary>
private static void SaveDemoTrajectoryInfo(string configRoot)
{
var options = new ControllerClientCompatOptions { ConfigRoot = configRoot };
var service = new ControllerClientCompatService(
options,
new ControllerClientCompatRobotCatalog(options, new RobotModelLoader()),
new RecordingControllerRuntime(),
new ControllerClientTrajectoryOrchestrator(),
new RobotConfigLoader());
service.SetUpRobot("FANUC_LR_Mate_200iD");
service.SaveTrajectoryInfo("demo-flyshot");
}
/// <summary>
/// 按数值逐行比较两个轨迹文本文件,避免纯文本格式差异影响判断。
/// </summary>
private static void AssertNumericFilesEqual(string expectedPath, string actualPath)
{
var expectedRows = File.ReadAllLines(expectedPath).Select(ParseSpaceSeparatedDoubles).ToArray();
var actualRows = File.ReadAllLines(actualPath).Select(ParseSpaceSeparatedDoubles).ToArray();
Assert.Equal(expectedRows.Length, actualRows.Length);
for (var rowIndex = 0; rowIndex < expectedRows.Length; rowIndex++)
{
Assert.Equal(expectedRows[rowIndex].Length, actualRows[rowIndex].Length);
for (var columnIndex = 0; columnIndex < expectedRows[rowIndex].Length; columnIndex++)
{
Assert.Equal(expectedRows[rowIndex][columnIndex], actualRows[rowIndex][columnIndex], precision: 6);
}
}
}
/// <summary> /// <summary>
/// 角度转弧度,供轨迹整形测试构造输入。 /// 角度转弧度,供轨迹整形测试构造输入。
/// </summary> /// </summary>
@@ -1509,48 +1127,10 @@ public sealed class RuntimeOrchestrationTests
useDo: settings.UseDo, useDo: settings.UseDo,
ioAddresses: settings.IoAddresses, ioAddresses: settings.IoAddresses,
ioKeepCycles: settings.IoKeepCycles, ioKeepCycles: settings.IoKeepCycles,
triggerSampleIndexOffsetCycles: settings.TriggerSampleIndexOffsetCycles,
accLimitScale: settings.AccLimitScale, accLimitScale: settings.AccLimitScale,
jerkLimitScale: settings.JerkLimitScale, jerkLimitScale: settings.JerkLimitScale,
adaptIcspTryNum: settings.AdaptIcspTryNum, adaptIcspTryNum: settings.AdaptIcspTryNum,
planningSpeedScale: settings.PlanningSpeedScale, planningSpeedScale: settings.PlanningSpeedScale,
speedRatio: settings.SpeedRatio,
smoothStartStopTiming: true);
}
/// <summary>
/// 为 legacy-fit 对比测试显式关闭二次时间重映射。
/// </summary>
private static CompatibilityRobotSettings DisableSmoothStartStopTiming(CompatibilityRobotSettings settings)
{
return new CompatibilityRobotSettings(
useDo: settings.UseDo,
ioAddresses: settings.IoAddresses,
ioKeepCycles: settings.IoKeepCycles,
triggerSampleIndexOffsetCycles: settings.TriggerSampleIndexOffsetCycles,
accLimitScale: settings.AccLimitScale,
jerkLimitScale: settings.JerkLimitScale,
adaptIcspTryNum: settings.AdaptIcspTryNum,
planningSpeedScale: settings.PlanningSpeedScale,
speedRatio: settings.SpeedRatio,
smoothStartStopTiming: false);
}
/// <summary>
/// 构造飞拍倍率测试使用的最小兼容设置。
/// </summary>
private static CompatibilityRobotSettings CreateFlyshotSettings(double planningSpeedScale, double speedRatio)
{
return new CompatibilityRobotSettings(
useDo: true,
ioAddresses: [7, 8],
ioKeepCycles: 2,
triggerSampleIndexOffsetCycles: 7,
accLimitScale: 1.0,
jerkLimitScale: 1.0,
adaptIcspTryNum: 5,
planningSpeedScale: planningSpeedScale,
speedRatio: speedRatio,
smoothStartStopTiming: true); smoothStartStopTiming: true);
} }
@@ -1923,9 +1503,9 @@ internal static class TestRobotFactory
waypoints: waypoints:
[ [
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
[0.001, 0.0, 0.0, 0.0, 0.0, 0.0], [0.1, 0.0, 0.0, 0.0, 0.0, 0.0],
[0.002, 0.0, 0.0, 0.0, 0.0, 0.0], [0.2, 0.0, 0.0, 0.0, 0.0, 0.0],
[0.003, 0.0, 0.0, 0.0, 0.0, 0.0] [0.3, 0.0, 0.0, 0.0, 0.0, 0.0]
], ],
shotFlags: [false, true, false, false], shotFlags: [false, true, false, false],
offsetValues: [0, 1, 0, 0], offsetValues: [0, 1, 0, 0],
@@ -2019,11 +1599,6 @@ internal sealed class RecordingControllerRuntime : IControllerRuntime
/// </summary> /// </summary>
public RobotProfile? LastRobotProfile { get; private set; } public RobotProfile? LastRobotProfile { get; private set; }
/// <summary>
/// 获取最近一次 ExecuteTrajectory 收到的结果对象。
/// </summary>
public TrajectoryResult? LastExecutedResult { get; private set; }
/// <inheritdoc /> /// <inheritdoc />
public void ResetRobot(RobotProfile robot, string robotName) public void ResetRobot(RobotProfile robot, string robotName)
{ {
@@ -2108,13 +1683,6 @@ internal sealed class RecordingControllerRuntime : IControllerRuntime
/// <inheritdoc /> /// <inheritdoc />
public void ExecuteTrajectory(TrajectoryResult result, IReadOnlyList<double> finalJointPositions) public void ExecuteTrajectory(TrajectoryResult result, IReadOnlyList<double> finalJointPositions)
{ {
LastExecutedResult = result;
}
/// <inheritdoc />
public void ExecuteCartesianTrajectory(TrajectoryResult result, IReadOnlyList<double> finalPose)
{
LastExecutedResult = result;
} }
} }
@@ -2277,182 +1845,6 @@ internal sealed class DelayedCompletionControllerRuntime : IControllerRuntime
_jointPositions = finalJointPositions.ToArray(); _jointPositions = finalJointPositions.ToArray();
} }
} }
/// <inheritdoc />
public void ExecuteCartesianTrajectory(TrajectoryResult result, IReadOnlyList<double> finalPose)
{
}
}
/// <summary>
/// 模拟 runtime 执行入口长期占用的测试运行时,用于验证兼容层锁边界。
/// </summary>
internal sealed class BlockingExecutionControllerRuntime : IControllerRuntime
{
private readonly object _lock = new();
private readonly ManualResetEventSlim _executionStarted = new(false);
private readonly ManualResetEventSlim _releaseExecution = new(false);
private readonly double[] _jointPositions;
private bool _isEnabled;
private bool _isInMotion;
private int _executeCallCount;
/// <summary>
/// 初始化一份会阻塞 ExecuteTrajectory 的测试运行时。
/// </summary>
/// <param name="initialJointPositions">初始关节反馈。</param>
public BlockingExecutionControllerRuntime(IReadOnlyList<double> initialJointPositions)
{
_jointPositions = initialJointPositions.ToArray();
}
/// <summary>
/// 获取 runtime 执行入口被调用的次数。
/// </summary>
public int ExecuteCallCount
{
get
{
lock (_lock)
{
return _executeCallCount;
}
}
}
/// <summary>
/// 等待指定序号的执行调用进入 runtime。
/// </summary>
/// <param name="timeout">最长等待时间。</param>
/// <param name="expectedCallCount">期望已经进入的执行次数。</param>
/// <returns>是否在超时前等到。</returns>
public bool WaitForExecutionStarted(TimeSpan timeout, int expectedCallCount = 1)
{
var deadline = DateTimeOffset.UtcNow.Add(timeout);
while (DateTimeOffset.UtcNow < deadline)
{
lock (_lock)
{
if (_executeCallCount >= expectedCallCount)
{
return true;
}
}
_executionStarted.Wait(TimeSpan.FromMilliseconds(10));
}
return false;
}
/// <summary>
/// 释放当前阻塞的执行调用。
/// </summary>
public void ReleaseExecution()
{
_releaseExecution.Set();
}
public void ResetRobot(RobotProfile robot, string robotName)
{
}
public void SetActiveController(bool sim)
{
}
public void Connect(string robotIp)
{
}
public void Disconnect()
{
}
public void EnableRobot(int bufferSize)
{
_isEnabled = true;
}
public void DisableRobot()
{
_isEnabled = false;
}
public void StopMove()
{
lock (_lock)
{
_isInMotion = false;
}
ReleaseExecution();
}
public double GetSpeedRatio() => 1.0;
public void SetSpeedRatio(double ratio)
{
}
public IReadOnlyList<double> GetTcp() => [0.0, 0.0, 0.0];
public void SetTcp(double x, double y, double z)
{
}
public bool GetIo(int port, string ioType) => false;
public void SetIo(int port, bool value, string ioType)
{
}
public IReadOnlyList<double> GetJointPositions()
{
return _jointPositions.ToArray();
}
public IReadOnlyList<double> GetPose() => [0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
public ControllerStateSnapshot GetSnapshot()
{
lock (_lock)
{
return new ControllerStateSnapshot(
capturedAt: DateTimeOffset.UtcNow,
connectionState: "Connected",
isEnabled: _isEnabled,
isInMotion: _isInMotion,
speedRatio: 1.0,
jointPositions: _jointPositions.ToArray(),
cartesianPose: Array.Empty<double>(),
activeAlarms: Array.Empty<RuntimeAlarm>());
}
}
public void ExecuteTrajectory(TrajectoryResult result, IReadOnlyList<double> finalJointPositions)
{
lock (_lock)
{
_executeCallCount++;
_isInMotion = true;
_executionStarted.Set();
_releaseExecution.Reset();
}
_releaseExecution.Wait();
lock (_lock)
{
_isInMotion = false;
_executionStarted.Reset();
}
}
public void ExecuteCartesianTrajectory(TrajectoryResult result, IReadOnlyList<double> finalPose)
{
ExecuteTrajectory(result, _jointPositions);
}
} }
/// <summary> /// <summary>
@@ -2579,8 +1971,4 @@ internal sealed class StickyFeedbackControllerRuntime : IControllerRuntime
_jointPositions = finalJointPositions.ToArray(); _jointPositions = finalJointPositions.ToArray();
} }
} }
public void ExecuteCartesianTrajectory(TrajectoryResult result, IReadOnlyList<double> finalPose)
{
}
} }

View File

@@ -44,28 +44,6 @@ public sealed class DebugConsoleEndpointTests(FlyshotServerFactory factory) : IC
Assert.Contains("/api/debug/config", script, StringComparison.Ordinal); Assert.Contains("/api/debug/config", script, StringComparison.Ordinal);
} }
/// <summary>
/// 调试页应当为 MovePose 提供可直接发送的六字段请求体模板。
/// </summary>
[Fact]
public async Task GetDebugScript_ContainsMovePoseRequestSample()
{
using var configuredFactory = CreateFactoryWithSwaggerEnabled(true);
using var client = configuredFactory.CreateClient();
using var scriptResponse = await client.GetAsync("/assets/debug.js");
Assert.Equal(HttpStatusCode.OK, scriptResponse.StatusCode);
var script = await scriptResponse.Content.ReadAsStringAsync();
Assert.Contains("/move_pose/", script, StringComparison.Ordinal);
Assert.Contains("x: 100.0", script, StringComparison.Ordinal);
Assert.Contains("y: 200.0", script, StringComparison.Ordinal);
Assert.Contains("z: 300.0", script, StringComparison.Ordinal);
Assert.Contains("w: 0.0", script, StringComparison.Ordinal);
Assert.Contains("p: 45.0", script, StringComparison.Ordinal);
Assert.Contains("r: 0.0", script, StringComparison.Ordinal);
}
/// <summary> /// <summary>
/// 当 Swagger 启用时,调试配置 API 应当返回实际 Swagger JSON 地址。 /// 当 Swagger 启用时,调试配置 API 应当返回实际 Swagger JSON 地址。
/// </summary> /// </summary>

View File

@@ -1,6 +1,5 @@
using System.Net; using System.Net;
using System.Net.Http.Json; using System.Net.Http.Json;
using System.Text;
using System.Text.Json; using System.Text.Json;
namespace Flyshot.Server.IntegrationTests; namespace Flyshot.Server.IntegrationTests;
@@ -132,13 +131,6 @@ public sealed class LegacyHttpApiCompatibilityTests(FlyshotServerFactory factory
Assert.Equal("robot moved", moveJointJson.RootElement.GetProperty("status").GetString()); Assert.Equal("robot moved", moveJointJson.RootElement.GetProperty("status").GetString());
} }
using (var movePoseResponse = await client.PostAsJsonAsync("/move_pose/", new { x = 100.0, y = 200.0, z = 300.0, w = 1.0, p = 2.0, r = 3.0 }))
{
Assert.Equal(HttpStatusCode.OK, movePoseResponse.StatusCode);
using var movePoseJson = await ReadJsonAsync(movePoseResponse);
Assert.Equal("robot moved", movePoseJson.RootElement.GetProperty("status").GetString());
}
using (var getJointPositionResponse = await client.GetAsync("/get_joint_position/")) using (var getJointPositionResponse = await client.GetAsync("/get_joint_position/"))
{ {
Assert.Equal(HttpStatusCode.OK, getJointPositionResponse.StatusCode); Assert.Equal(HttpStatusCode.OK, getJointPositionResponse.StatusCode);
@@ -173,32 +165,6 @@ public sealed class LegacyHttpApiCompatibilityTests(FlyshotServerFactory factory
} }
} }
/// <summary>
/// 验证 MovePose 请求必须显式提供六个有限直角坐标字段,避免缺字段被模型绑定静默补 0。
/// </summary>
[Theory]
[InlineData("""{"x":100.0,"y":200.0,"z":300.0,"w":1.0,"p":2.0}""")]
[InlineData("null")]
[InlineData("""{"x":1e999,"y":200.0,"z":300.0,"w":1.0,"p":2.0,"r":3.0}""")]
[InlineData("""{"x":1000.1,"y":200.0,"z":300.0,"w":1.0,"p":2.0,"r":3.0}""")]
[InlineData("""{"x":100.0,"y":-1000.1,"z":300.0,"w":1.0,"p":2.0,"r":3.0}""")]
[InlineData("""{"x":100.0,"y":200.0,"z":-0.1,"w":1.0,"p":2.0,"r":3.0}""")]
[InlineData("""{"x":100.0,"y":200.0,"z":300.0,"w":-180.1,"p":2.0,"r":3.0}""")]
[InlineData("""{"x":100.0,"y":200.0,"z":300.0,"w":1.0,"p":90.1,"r":3.0}""")]
[InlineData("""{"x":100.0,"y":200.0,"z":300.0,"w":1.0,"p":2.0,"r":180.1}""")]
public async Task MovePose_InvalidPayload_ReturnsLegacyBadRequest(string payload)
{
using var client = factory.CreateClient();
await InitializeRobotAsync(client);
using var content = new StringContent(payload, Encoding.UTF8, "application/json");
using var response = await client.PostAsync("/move_pose/", content);
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
using var json = await ReadJsonAsync(response);
Assert.Equal("MovePose failed", json.RootElement.GetProperty("detail").GetString());
}
/// <summary> /// <summary>
/// 验证飞拍 HTTP 接口可以按旧 API 层的路径和字段完成上传、列出、执行与删除。 /// 验证飞拍 HTTP 接口可以按旧 API 层的路径和字段完成上传、列出、执行与删除。
/// </summary> /// </summary>

View File

@@ -34,60 +34,6 @@ public sealed class StatusEndpointTests(FlyshotServerFactory factory) : IClassFi
Assert.Contains("/api/status/snapshot", script, StringComparison.Ordinal); Assert.Contains("/api/status/snapshot", script, StringComparison.Ordinal);
} }
/// <summary>
/// 状态页应当提供双摇杆直角坐标点动面板,并复用现有 MovePose HTTP 接口。
/// </summary>
[Fact]
public async Task GetStatusPageAssets_ExposeCartesianJogControls()
{
using var client = factory.CreateClient();
using var htmlResponse = await client.GetAsync("/status.html");
Assert.Equal(HttpStatusCode.OK, htmlResponse.StatusCode);
var html = await htmlResponse.Content.ReadAsStringAsync();
Assert.Contains("直角坐标点动", html, StringComparison.Ordinal);
Assert.Contains("XYZ 平移摇杆", html, StringComparison.Ordinal);
Assert.Contains("WPR 姿态摇杆", html, StringComparison.Ordinal);
Assert.Contains("id=\"linear-step\"", html, StringComparison.Ordinal);
Assert.Contains("id=\"angular-step\"", html, StringComparison.Ordinal);
Assert.Contains("max=\"20\"", html, StringComparison.Ordinal);
Assert.Contains("请求日志", html, StringComparison.Ordinal);
Assert.Contains("id=\"request-log\"", html, StringComparison.Ordinal);
Assert.Contains("id=\"clear-log\"", html, StringComparison.Ordinal);
foreach (var (axis, negative, positive) in new[]
{
("x", "X-", "X+"),
("y", "Y-", "Y+"),
("z", "Z-", "Z+"),
("w", "W-", "W+"),
("p", "P-", "P+"),
("r", "R-", "R+")
})
{
Assert.Contains($"data-axis=\"{axis}\"", html, StringComparison.Ordinal);
Assert.Contains($"data-axis=\"{axis}\" data-direction=\"1\"", html, StringComparison.Ordinal);
Assert.Contains($"data-axis=\"{axis}\" data-direction=\"-1\"", html, StringComparison.Ordinal);
Assert.Contains(negative, html, StringComparison.Ordinal);
Assert.Contains(positive, html, StringComparison.Ordinal);
}
using var scriptResponse = await client.GetAsync("/assets/status.js");
Assert.Equal(HttpStatusCode.OK, scriptResponse.StatusCode);
var script = await scriptResponse.Content.ReadAsStringAsync();
Assert.Contains("/api/status/snapshot", script, StringComparison.Ordinal);
Assert.Contains("/move_pose/", script, StringComparison.Ordinal);
Assert.Contains("cartesianPose", script, StringComparison.Ordinal);
Assert.Contains("pointerdown", script, StringComparison.Ordinal);
Assert.Contains("pointerup", script, StringComparison.Ordinal);
Assert.Contains("pointerleave", script, StringComparison.Ordinal);
Assert.Contains("blur", script, StringComparison.Ordinal);
Assert.Contains("request-log", script, StringComparison.Ordinal);
Assert.Contains("clear-log", script, StringComparison.Ordinal);
}
/// <summary> /// <summary>
/// 验证状态快照 API 会返回运行时连接、使能、速度和机器人元数据。 /// 验证状态快照 API 会返回运行时连接、使能、速度和机器人元数据。
/// </summary> /// </summary>