6 Commits

Author SHA1 Message Date
abea2d7517 🐛 fix(flyshot): 修复输出选择并同步现场配置与文档
- 恢复 use_do 对 DO/RO 的选择,保留拍照脉冲与清零
- 统一直接关节移动与飞拍预移动的规划和运行倍率
- 补充输出选择、预生成队列和点对点倍率回归测试
- 更新 WhP_test 轨迹配置、系统说明和 README
- 删除本地 Claude 配置文件

验证:构建零警告零错误,定向测试 6 项与集成测试 38 项通过。
核心全量 175 项通过、14 项失败,失败项与修改前基线一致。
2026-10-01 19:11:50 +08:00
acccb94807 📝 docs(api): 添加协作机器人飞拍接口文档
* 说明运行速度倍率设置接口与返回结果
* 说明飞拍启动接口、调用顺序和 curl 示例
2026-09-02 16:12:44 +08:00
c41a03df44 🔧 chore(config): 更新默认机器人连接配置
* 将自动初始化默认机器人切换为现场 FANUC_LR_Mate_200iD_7L

* 将默认控制器地址更新为 192.168.10.11

* 补充不支持机器人名称异常的中文提示
2026-06-30 22:50:53 +08:00
e0162b72b3 🐛 fix(fanuc): 修复 J519 队列等待与日志注入
* 为 FANUC 命令、状态和 J519 客户端注入独立日志记录器
* 补充不可接收命令状态下队列完成等待的回归测试
* 调整 J519 命令流结束后的状态处理语义
2026-06-30 16:05:23 +08:00
9a01ce851d Merge branch 'feat/offline-flyshot-planning' into dev 2026-06-24 11:43:44 +08:00
5193f537b0 ✨ feat(planning): 添加离线飞拍规划能力
* 固化离线飞拍轨迹加载、保存和规划结果。

* 真机初始化失败时保留模型加载后的离线规划能力。

* 更新状态接口、调试页和回归测试覆盖。
2026-06-24 11:40:07 +08:00
37 changed files with 2839 additions and 1134 deletions

View File

@@ -1,42 +0,0 @@
{
"permissions": {
"allow": [
"Bash(git commit -m ':*)",
"Bash(/bin/bash -lc 'DOTNET_CLI_HOME=/tmp NUGET_PACKAGES=/tmp/nuget-packages dotnet build FlyshotReplacement.sln -v minimal 2>&1')",
"Bash(/bin/bash -lc 'DOTNET_CLI_HOME=/tmp NUGET_PACKAGES=/tmp/nuget-packages dotnet test tests/Flyshot.Server.IntegrationTests/Flyshot.Server.IntegrationTests.csproj -v minimal 2>&1')",
"Bash(/bin/bash -lc 'DOTNET_CLI_HOME=/tmp NUGET_PACKAGES=/tmp/nuget-packages dotnet test FlyshotReplacement.sln --no-build -v minimal 2>&1')",
"Bash(DOTNET_CLI_HOME=/tmp NUGET_PACKAGES=/tmp/nuget-packages dotnet build FlyshotReplacement.sln --no-restore -v minimal)",
"Bash(DOTNET_CLI_HOME=/tmp NUGET_PACKAGES=/tmp/nuget-packages dotnet test tests/Flyshot.Server.IntegrationTests/Flyshot.Server.IntegrationTests.csproj --no-build -v minimal)",
"Bash(python -c \"import json; json.load\\(open\\('.vscode/launch.json'\\)\\); json.load\\(open\\('.vscode/tasks.json'\\)\\); print\\('JSON valid.'\\)\")",
"Bash(python -c \"import json; json.load\\(open\\('.vscode/launch.json', encoding='utf-8'\\)\\); json.load\\(open\\('.vscode/tasks.json', encoding='utf-8'\\)\\); print\\('JSON valid.'\\)\")",
"Bash(/bin/bash -lc 'DOTNET_CLI_HOME=/tmp NUGET_PACKAGES=/tmp/nuget-packages dotnet build FlyshotReplacement.sln --no-restore -v minimal')",
"Bash(/bin/bash -lc 'DOTNET_CLI_HOME=/tmp NUGET_PACKAGES=/tmp/nuget-packages dotnet test tests/Flyshot.Core.Tests/Flyshot.Core.Tests.csproj -v minimal')",
"Bash(/bin/bash -lc 'DOTNET_CLI_HOME=/tmp NUGET_PACKAGES=/tmp/nuget-packages dotnet test tests/Flyshot.Server.IntegrationTests/Flyshot.Server.IntegrationTests.csproj -v minimal')",
"Bash(/bin/bash -lc 'DOTNET_CLI_HOME=/tmp NUGET_PACKAGES=/tmp/nuget-packages dotnet build FlyshotReplacement.sln --no-restore -v minimal 2>&1')",
"Bash(taskkill /PID 120812 /F)",
"Bash(/bin/bash -lc 'DOTNET_CLI_HOME=/tmp NUGET_PACKAGES=/tmp/nuget-packages dotnet build flyshot-replacement.sln --no-restore -v minimal 2>&1')",
"Bash(/bin/bash -lc 'DOTNET_CLI_HOME=/tmp NUGET_PACKAGES=/tmp/nuget-packages dotnet build /d/Dev/Codes/rvbust-code/FlyingShotPkg_3.15_VDA/flyshot-replacement/FlyshotReplacement.sln --no-restore -v minimal 2>&1')",
"Bash(capinfos \"20260430.pcap\")",
"Bash(tshark -r \"20260430.pcap\" -T fields -e frame.number -e frame.time_relative -e ip.src -e ip.dst -e udp.srcport -e udp.dstport -e data.len)",
"Bash(tshark -r \"20260430.pcap\" -Y \"udp.port==60015\" -T fields -e frame.number -e frame.time_relative -e ip.src -e udp.srcport -e udp.length -e data.len)",
"Bash(tshark -r \"20260430.pcap\" -Y \"udp.port==60015 and udp.srcport==60015\" -T fields -e frame.number -e frame.time_relative -e udp.length)",
"Bash(tshark -r \"20260430.pcap\" -Y \"udp.port==60015 and udp.dstport==60015\" -T fields -e frame.number -e frame.time_relative -e udp.length)",
"Bash(tshark -r \"20260430.pcap\" -Y \"udp.port==60015\" -T fields -e frame.number -e frame.time_relative -e ip.src -e ip.dst -e udp.srcport -e udp.dstport -e udp.length -e frame.protocols)",
"Bash(tshark -r \"20260430.pcap\" -Y \"udp.port==60015 and frame.number==1\" -V)",
"Bash(tshark -r \"20260430.pcap\" -Y \"udp.port==60015 and udp.dstport!=60015\" -T fields -e frame.number -e frame.time_relative -e ip.dst -e udp.dstport)",
"Bash(tshark -r \"20260430.pcap\" -Y \"udp.dstport==60015\" -T fields -e frame.number -e frame.time_relative -e ip.src -e udp.length)",
"Bash(tshark -r \"20260430.pcap\" -Y \"frj519\" -T fields -e frj519.type -e frj519.seq -e frj519.status -e frj519.j1 -e frj519.j2 -e frj519.j3 -e frj519.j4 -e frj519.j5 -e frj519.j6 -e frj519.j7 -e frj519.j8 -e frj519.j9 -e frj519.timestamp)",
"Bash(tshark -r \"20260430.pcap\" -Y \"frj519\" -T fields -e frj519.type -e frj519.seq -e frj519.status -e frj519.j1 -e frj519.j2 -e frj519.j3 -e frj519.j4 -e frj519.j5 -e frj519.j6 -e frj519.j7 -e frj519.j8 -e frj519.j9 -e frj519.timestamp -e frj519.x -e frj519.y -e frj519.z -e frj519.w -e frj519.p -e frj519.r)",
"Bash(tshark -r \"20260430.pcap\" -Y \"frj519\" -T fields -E header=y)",
"Bash(tshark -G fields)",
"Bash(tshark -r \"20260430.pcap\" -Y \"frj519\" -T fields -e frame.time_relative -e data)",
"Bash(tshark -r \"20260430.pcap\" -Y \"udp.dstport==60015\" -T fields -e frame.number -e frame.time_relative -e data)",
"Bash(tshark -r \"20260430.pcap\" -Y \"udp.port==60015 and udp.srcport==60015\" -T fields -e frame.number -e frame.time_relative -e data)",
"Bash(tshark -r \"20260430.pcap\" --disable-protocol frj519 -Y \"udp.dstport==60015\" -T fields -e frame.number -e frame.time_relative -e data)",
"Bash(/bin/bash -lc 'DOTNET_CLI_HOME=/tmp NUGET_PACKAGES=/tmp/nuget-packages dotnet build src/Flyshot.Runtime.Fanuc/Flyshot.Runtime.Fanuc.csproj --no-restore -v minimal 2>&1 | tail -20')",
"Bash(/bin/bash -lc 'DOTNET_CLI_HOME=/tmp NUGET_PACKAGES=/tmp/nuget-packages dotnet build FlyshotReplacement.sln --no-restore -v minimal 2>&1 | tail -25')",
"Bash(/bin/bash -lc 'DOTNET_CLI_HOME=/tmp NUGET_PACKAGES=/tmp/nuget-packages dotnet build FlyshotReplacement.sln --no-restore -v minimal 2>&1 | tail -15')",
"Bash(/bin/bash -lc 'DOTNET_CLI_HOME=/tmp NUGET_PACKAGES=/tmp/nuget-packages dotnet test tests/Flyshot.Core.Tests/Flyshot.Core.Tests.csproj --no-restore -v minimal 2>&1 | tail -30')"
]
}
}

View File

@@ -10,8 +10,8 @@
"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,
"speed_ratio": 1,
"smooth_start_stop_timing": true "smooth_start_stop_timing": true
}, },
"flying_shots": { "flying_shots": {
@@ -300,6 +300,155 @@
], ],
[] []
] ]
},
"WhP_test": {
"traj_waypoints": [
[
1.056731,
0.011664811,
-0.017892333,
-0.01516874,
0.021492079,
0.009567846
],
[
0.2153671,
0.015257306,
-0.024938405,
-0.059105303,
0.06653474,
-0.005172012
],
[
0.47362572,
0.015260376,
-0.024971163,
-0.059100218,
0.06653733,
-0.00518099
],
[
-0.0861789,
0.024431378,
0.15767263,
-0.059098743,
0.06653532,
-0.0051812795
],
[
-0.029219253,
0.024431676,
0.24181713,
-0.81706756,
0.06653719,
-0.0051850453
],
[
1.056731,
0.011664811,
-0.017892333,
-0.01516874,
0.021492079,
0.009567846
]
],
"shot_flags": [
false,
true,
true,
true,
true,
false
],
"offset_values": [
0,
0,
0,
0,
0,
0
],
"addr": [
[],
[
2,
3,
4
],
[
2,
4,
3
],
[
1,
2,
3
],
[
1,
2,
3
],
[]
]
},
"demo-flyshot": {
"traj_waypoints": [
[
0,
0,
0,
0,
0,
0
],
[
0.001,
0,
0,
0,
0,
0
],
[
0.002,
0,
0,
0,
0,
0
],
[
0.003,
0,
0,
0,
0,
0
]
],
"shot_flags": [
false,
true,
false,
false
],
"offset_values": [
0,
1,
0,
0
],
"addr": [
[],
[
7,
8
],
[],
[]
]
} }
} }
} }

View File

@@ -24,7 +24,9 @@
- 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`。 - 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`,不再创建独立轨迹存储文件。
- 服务启动时支持自动初始化机器人:在 `appsettings.json` 的 `ControllerClientCompat.AutoInit` 节配置 `RobotName` 和 `RobotIp`,启动后会自动执行 `SetUpRobot → SetActiveController → Connect → EnableRobot`;配置缺失或连接失败不会阻止服务启动。 - 服务启动时支持真机优先初始化:在 `ControllerClientCompat.AutoInit` 配置 `RobotName` 和 `RobotIp`,启动后执行 `SetUpRobot → Connect → EnableRobot(8)`;连接失败后进入 `OfflineReady`,模型仍可用于离线飞拍规划。
- 飞拍资产与机器人型号解耦:未加载模型时也可上传、列出和删除;模型自由度与关节限制延迟到规划阶段校验。
- `/save_traj_info/` 已成为离线规划入口,返回最终 8ms 发送点列、ShotEvents、触发绑定和全部导出文件绝对路径;`/execute_flyshot/` 仍只允许 `RealReady`。
单位约定总览: 单位约定总览:
@@ -74,12 +76,15 @@
- [x] 保留新 HTTP 接口路线,明确不再实现旧 `50001/TCP+JSON` 网关 - [x] 保留新 HTTP 接口路线,明确不再实现旧 `50001/TCP+JSON` 网关
- [x] 将飞拍轨迹持久化收敛到运行目录 `Config/RobotConfig.json` - [x] 将飞拍轨迹持久化收敛到运行目录 `Config/RobotConfig.json`
- [x] 服务启动自动初始化机器人:通过 `ControllerClientCompat.AutoInit` 配置节指定 `RobotName` 和 `RobotIp`,启动时自动执行初始化链路 - [x] 服务启动自动初始化机器人:通过 `ControllerClientCompat.AutoInit` 配置节指定 `RobotName` 和 `RobotIp`,启动时自动执行初始化链路
- [x] 完成真机优先与离线飞拍规划改造:`RealReady / OfflineReady / ModelLoadFailed` 状态、固定轨迹资产、完整离线规划响应和 Debug 页面
- [x] 删除生产仿真切换 HTTP 入口及旧 `/init_mpc_robt`,统一使用 `/init_mpc_robot`
剩余 Todo: 剩余 Todo:
1. 配置与测试基线 1. 配置与测试基线
- [x] 修正 `ConfigCompatibilityTests` 当前样本路径漂移:`Rvbust/EOL10_EAU_0/RobotConfig.json` 不再包含 `001`,应改用稳定样本或更新断言。 - [x] 修正 `ConfigCompatibilityTests` 当前样本路径漂移:`Rvbust/EOL10_EAU_0/RobotConfig.json` 不再包含 `001`,应改用稳定样本或更新断言。
- [x] 将 `RobotConfig.json` 中的 `use_do`、`io_keep_cycles`、`acc_limit`、`jerk_limit`、`adapt_icsp_try_num` 全部贯通到规划和执行链路。 - [x] 将 `RobotConfig.json` 中的 `use_do`、`io_keep_cycles`、`acc_limit`、`jerk_limit`、`adapt_icsp_try_num` 全部贯通到规划和执行链路。
- [x] 恢复 `use_do` 的输出选择语义:`true` 使用 DO(2),`false` 使用 RO(9),两种模式均保留飞拍脉冲与清零;见 [输出选择说明](docs/use-do-output-selection.md)。
- [x] 将上传飞拍轨迹统一保存到运行目录 `Config/RobotConfig.json` 的 `flying_shots` 节点。 - [x] 将上传飞拍轨迹统一保存到运行目录 `Config/RobotConfig.json` 的 `flying_shots` 节点。
- [x] 为新 HTTP API 补一份当前现场调用顺序文档,替代旧 `ControllerClient` 工作流:见 `docs/fanuc-field-runtime-workflow.md`。 - [x] 为新 HTTP API 补一份当前现场调用顺序文档,替代旧 `ControllerClient` 工作流:见 `docs/fanuc-field-runtime-workflow.md`。
@@ -113,6 +118,7 @@
- [x] 将飞拍 IO 触发的 `write_io_type/index/mask/value` 与现场控制柜实际 IO 地址逐项对齐;UTTC golden tests 确认 17 个触发点对应 17 个 UDP IO set 脉冲、17 个 clear 帧,mask 集合为 `10/12/14`。 - [x] 将飞拍 IO 触发的 `write_io_type/index/mask/value` 与现场控制柜实际 IO 地址逐项对齐;UTTC golden tests 确认 17 个触发点对应 17 个 UDP IO set 脉冲、17 个 clear 帧,mask 集合为 `10/12/14`。
- [x] 将 `MoveJoint` 从单点最终目标改为临时 PTP 稠密轨迹:按 `status=15` 运动窗口统计,speed=1 抓包 40 点,speed=0.7 抓包 55 点,speed=0.5 抓包 77 点,路径为关节空间直线 + smoothstep 进度。 - [x] 将 `MoveJoint` 从单点最终目标改为临时 PTP 稠密轨迹:按 `status=15` 运动窗口统计,speed=1 抓包 40 点,speed=0.7 抓包 55 点,speed=0.5 抓包 77 点,路径为关节空间直线 + smoothstep 进度。
- [x] `ExecuteFlyShotTraj(move_to_start=true)` 复用临时 PTP 稠密轨迹移动到规划起点,并等待运行时完成后再启动飞拍轨迹,避免第一帧 J519 目标突变导致控制柜报警。 - [x] `ExecuteFlyShotTraj(move_to_start=true)` 复用临时 PTP 稠密轨迹移动到规划起点,并等待运行时完成后再启动飞拍轨迹,避免第一帧 J519 目标突变导致控制柜报警。
- [x] `MoveJoint` 和飞拍预移动统一按 `planning_speed_scale * speed_ratio` 生成点对点轨迹,速度、加速度、跃度分别按有效倍率的一、二、三次方缩放,发送周期保持不变。
- [x] `ExecuteFlyShotTraj(wait=true)` 等待正式飞拍轨迹执行完成后再返回;HTTP `/execute_flyshot/` 已接入旧抓包中的 `wait` 字段,默认值为 `true`。 - [x] `ExecuteFlyShotTraj(wait=true)` 等待正式飞拍轨迹执行完成后再返回;HTTP `/execute_flyshot/` 已接入旧抓包中的 `wait` 字段,默认值为 `true`。
6. 真机联调与运行安全 6. 真机联调与运行安全

View File

@@ -0,0 +1,109 @@
# 协作机器人飞拍接口
本文仅保留协作机器人后端需要调用的两个接口:设置运行速度、启动已存在的飞拍任务。不包含初始化、连接、使能、任务上传或任务管理。
服务地址示例:`http://<飞拍服务地址>:<端口>`。
## 1. 设置运行速度
```http
POST /set_speedRatio/
Content-Type: application/json
```
请求体:
```json
{
"speed": 0.7
}
```
| 字段 | 类型 | 范围 | 说明 |
| --- | --- | --- | --- |
| `speed` | number | `0.01` ~ `1.0` | 运行速度倍率;`1.0` 为全速,`0.7` 为 70% 速度。 |
成功响应:`200 OK`
```json
{
"message": "set_speedRatio executed",
"returnCode": 0
}
```
参数不在范围内时返回 `400 Bad Request`:
```json
{
"detail": "Speed ratio must be between 0.01 and 1.0."
}
```
应在启动飞拍前设置速度。当前接口不约定任务执行过程中的动态改速。
## 2. 启动飞拍任务
```http
POST /execute_flyshot/
Content-Type: application/json
```
请求体:
```json
{
"name": "协作机器人飞拍任务名",
"move_to_start": true,
"method": "icsp",
"save_traj": false,
"use_cache": true,
"wait": false
}
```
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `name` | string | 已存在的飞拍任务名称。 |
| `move_to_start` | boolean | 是否先移动到飞拍轨迹起点;建议为 `true`。 |
| `method` | string | 轨迹规划方法;使用 `"icsp"`。 |
| `save_traj` | boolean | 是否保存规划导出信息;启动任务时使用 `false`。 |
| `use_cache` | boolean | 是否复用规划缓存;建议为 `true`。 |
| `wait` | boolean | `false` 表示启动后立即返回;本接口使用 `false`。 |
成功响应:`200 OK`
```json
{
"status": "FlyShot executed",
"success": true
}
```
`wait=false` 时,`200` 仅表示任务已接受并开始调度,不表示飞拍已经完成。
启动失败时返回 `500 Internal Server Error`:
```json
{
"detail": "具体失败原因"
}
```
## 3. 调用顺序
```text
POST /set_speedRatio/ → 收到 returnCode = 0 → POST /execute_flyshot/
```
curl 示例:
```bash
curl -X POST 'http://<飞拍服务地址>:<端口>/set_speedRatio/' \
-H 'Content-Type: application/json' \
-d '{"speed":0.7}'
curl -X POST 'http://<飞拍服务地址>:<端口>/execute_flyshot/' \
-H 'Content-Type: application/json' \
-d '{"name":"协作机器人飞拍任务名","move_to_start":true,"method":"icsp","save_traj":false,"use_cache":true,"wait":false}'
```

View File

@@ -7,32 +7,27 @@
推荐使用聚合端点完成当前现场的一次性初始化: 推荐使用聚合端点完成当前现场的一次性初始化:
```bash ```bash
POST /init_mpc_robt POST /init_mpc_robot
{ {
"server_ip": "127.0.0.1",
"port": 50001,
"robot_name": "FANUC_LR_Mate_200iD", "robot_name": "FANUC_LR_Mate_200iD",
"robot_ip": "192.168.10.11", "robot_ip": "192.168.10.11"
"sim": false
} }
``` ```
该端点内部顺序: 该端点内部顺序:
1. `ConnectServer(server_ip, port)`:兼容旧参数形状,仅记录服务连接语义。 1. `SetUpRobot(robot_name)`:加载机器人配置、关节限制和伺服周期,状态进入 `OfflineReady`。
2. `SetUpRobot(robot_name)`:加载机器人配置、关节限制和伺服周期。 2. `Connect(robot_ip)`:依次建立 `TCP 10010` 状态通道、`TCP 10012` 命令通道、`UDP 60015` J519 运动通道。
3. `SetActiveController(sim)`:选择仿真或 FANUC 真机运行时。 3. `EnableRobot(8)`:启动 `RVBUSTSM` 和 J519 运动循环,成功后进入 `RealReady`。
4. `Connect(robot_ip)`:真机模式下依次建立 `TCP 10010` 状态通道、`TCP 10012` 命令通道、`UDP 60015` J519 运动通道。
5. `EnableRobot(2)`:真机模式下执行 `StopProg("RVBUSTSM") -> Reset -> GetProgStatus("RVBUSTSM") -> StartProg("RVBUSTSM")`,随后允许 J519 在收到机器人 UDP status 包后回发下一帧命令。 连接或使能失败时接口返回 400,但模型保持加载,状态进入 `OfflineReady`;只有模型加载失败才进入 `ModelLoadFailed`。
也可以使用拆分端点按同样顺序调用: 也可以使用拆分端点按同样顺序调用:
```text ```text
POST /connect_server/?server_ip=127.0.0.1&port=50001
POST /setup_robot/?robot_name=FANUC_LR_Mate_200iD POST /setup_robot/?robot_name=FANUC_LR_Mate_200iD
POST /set_active_controller/?sim=false
POST /connect_robot/?ip=192.168.10.11 POST /connect_robot/?ip=192.168.10.11
GET /enable_robot/?buffer_size=2 GET /enable_robot/?buffer_size=8
``` ```
## 2. 参数设置 ## 2. 参数设置
@@ -98,6 +93,8 @@ POST /move_joint/
## 4. 飞拍轨迹 ## 4. 飞拍轨迹
飞拍资产固定保存在 `Config/RobotConfig.json`。上传、列出和删除不要求模型已加载,也不要求真机连接;`setup_robot` 不会清空已上传轨迹。
上传: 上传:
```bash ```bash
@@ -122,6 +119,18 @@ POST /is_flyShotTrajValid/
} }
``` ```
离线生成最终发送轨迹:
```bash
POST /save_traj_info/
{
"name": "UTTC_MS11",
"method": "self-adapt-icsp"
}
```
该接口只要求模型已加载,不要求连接或使能。响应包含规划时长、最终发送时长、关键/稠密点数量、完整最终 8ms 点列、IO mask/value、ShotEvents、触发采样点绑定和全部导出文件绝对路径。`/debug` 中的“离线飞拍规划”面板提供同一能力,并明确不会向机器人发送命令。
执行: 执行:
```bash ```bash
@@ -142,7 +151,7 @@ POST /execute_flyshot/
2. 使用 `icsp` 或 `self-adapt-icsp` 规划关节轨迹;规划阶段先应用 `acc_limit / jerk_limit`,再应用 replacement-only 的规划加速度校准参数。 2. 使用 `icsp` 或 `self-adapt-icsp` 规划关节轨迹;规划阶段先应用 `acc_limit / jerk_limit`,再应用 replacement-only 的规划加速度校准参数。
3. 生成 `TrajectoryDoEvent`,把拍照触发绑定到轨迹时间。 3. 生成 `TrajectoryDoEvent`,把拍照触发绑定到轨迹时间。
4. 若 `move_to_start=true`,先从运行时当前关节位置生成临时 PTP 稠密轨迹移动到规划轨迹起点,并等待运行时 `IsInMotion=false` 后再启动飞拍轨迹,避免第一帧 J519 目标从当前位置跳到起点。 4. 若 `move_to_start=true`,先从运行时当前关节位置生成临时 PTP 稠密轨迹移动到规划轨迹起点,并等待运行时 `IsInMotion=false` 后再启动飞拍轨迹,避免第一帧 J519 目标从当前位置跳到起点。
5. 真机模式下把规划输出的 `rad` 稠密轨迹按 J519 轨迹时间步长重采样并转成 `deg`,命令实际发包由机器人 UDP status 包驱动。 5. 把规划输出的 `rad` 连续轨迹构建为最终 8ms 发送队列并转成 `deg`;离线规划和真机执行复用同一份结果。
6. 若 `wait=true`,正式飞拍轨迹启动后继续等待运行时 `IsInMotion=false`,机器人执行完整条飞拍轨迹后 HTTP 才返回;`wait=false` 时启动后立即返回。 6. 若 `wait=true`,正式飞拍轨迹启动后继续等待运行时 `IsInMotion=false`,机器人执行完整条飞拍轨迹后 HTTP 才返回;`wait=false` 时启动后立即返回。
7. 启动前若已有 J519 响应且 `accept_cmd` 或 `sysrdy` 未就绪,则拒绝执行。 7. 启动前若已有 J519 响应且 `accept_cmd` 或 `sysrdy` 未就绪,则拒绝执行。
8. 周期命令中嵌入 IO 脉冲;当前 UTTC 抓包确认 mask 集合为 `10/12/14`,共 17 个 set 脉冲和 17 个 clear 帧。 8. 周期命令中嵌入 IO 脉冲;当前 UTTC 抓包确认 mask 集合为 `10/12/14`,共 17 个 set 脉冲和 17 个 clear 帧。
@@ -157,7 +166,7 @@ GET /disable_robot/
POST /disconnect_robot/ POST /disconnect_robot/
``` ```
真机模式下: 真机运行时:
- 正常飞拍、MoveJoint、MovePose 结束时,默认在 J519 队列末尾追加 `LastData=1` 结束帧;结束帧发出后进入仅监控状态,继续接收 J519 状态包,不再回发保姿命令。 - 正常飞拍、MoveJoint、MovePose 结束时,默认在 J519 队列末尾追加 `LastData=1` 结束帧;结束帧发出后进入仅监控状态,继续接收 J519 状态包,不再回发保姿命令。
- `StopMove()` 取消当前稠密轨迹生成任务并走强制停止路径,可停止 J519 状态包驱动发送。 - `StopMove()` 取消当前稠密轨迹生成任务并走强制停止路径,可停止 J519 状态包驱动发送。

View File

@@ -0,0 +1,573 @@
# 飞拍系统介绍说明与使用教程
## 1. 文档目的
本文档用于说明当前飞拍系统的整体组成、数据流、接口边界和现场使用步骤。文档原稿采用 Markdown 编写,适合后续导入或整理为飞书文档;流程图使用 Mermaid 源码保留,导入飞书时可按需要渲染为图片后插入。
系统当前分为三个主要单元:
| 单元 | 工程位置 | 主要职责 |
| --- | --- | --- |
| 上位机单元 | `D:\Dev\Codes\MFD_Solution\FlyShot` | 控制相机、接收 PLC/MES 信息、发起飞拍任务、保存图片、调用 AI 检测、汇总结果并回传 MES/PLC |
| 飞拍控制单元 | `D:\Dev\Codes\rvbust-code\FlyingShotPkg_3.15_VDA\flyshot-replacement` | 提供 HTTP 控制接口,负责机器人初始化、轨迹上传、ICSP/self-adapt-ICSP 规划、触发时序生成和 FANUC 运动执行 |
| YOLO AI 识别模块 | 由上位机配置访问 | 接收图片检测请求,完成 AI 推理,返回图片级 OK/NG、原因、参数明细和结果图路径 |
## 2. 系统总览
```mermaid
flowchart LR
PLC["PLC / OPC UA<br/>任务触发、状态反馈"]
MES["MES<br/>换型下发、PartProcessed 上报"]
PC["上位机单元 FlyShot<br/>WPF 主控程序"]
Camera["工业相机<br/>采图与本地存图"]
Control["飞拍控制单元<br/>ASP.NET Core HTTP API"]
Robot["FANUC 机器人<br/>TCP 10010/10012 + UDP 60015"]
AI["YOLO AI 识别模块<br/>HTTP + 文件传输目录"]
Storage["本地/远端图片目录<br/>原图、结果图、任务 JSON"]
Notify["飞书 / 审核平台 / ClickHouse<br/>异常通知与追溯"]
PLC <--> PC
MES <--> PC
PC --> Camera
Camera --> PC
PC <--> Control
Control <--> Robot
PC <--> AI
PC --> Storage
AI --> Storage
PC --> Notify
```
### 2.1 上位机单元
上位机是现场操作和生产流程的核心入口,当前是 WPF MVVM 程序。它负责:
- 初始化相机并注册图像回调。
- 读取 `appConfig.json`、`Config\TypeData`、`Config\FlyTask` 中的现场配置。
- 通过 `MpcService` 调用飞拍控制单元 HTTP 接口。
- 按飞拍任务配置设置相机曝光、增益和拍摄索引。
- 将图片保存到 `CameraSavePath` 下的任务目录。
- 通过 `ImageProcessingService` 上传图片到 AI 服务器目录并调用 AI 接口。
- 将 AI 结果合并回任务对象,保存任务 JSON。
- 根据结果回写 PLC、发送 MES `PartProcessed`、写入 ClickHouse、必要时发送飞书 NG 报警。
### 2.2 飞拍控制单元
飞拍控制单元是新的 replacement 服务端。它不恢复旧 `50001/TCP+JSON` 网关,而是提供 ASP.NET Core HTTP API。它负责:
- 机器人模型加载与初始化。
- 轨迹上传、查询、删除。
- 普通轨迹 `ExecuteTrajectory`。
- 飞拍轨迹 `ExecuteFlyShotTraj`。
- 通过 `icsp` 或 `self-adapt-icsp` 生成关节轨迹。
- 根据 `shot_flags / offset_values / addrs` 生成拍照触发时间轴。
- 将规划结果转换为 8ms 发送队列,通过 J519 通道控制机器人运动和 IO 脉冲。
- 提供 `/status.html` 状态页和 `/debug.html` 调试页。
### 2.3 YOLO AI 识别模块
YOLO AI 模块由上位机调用。上位机当前不是直接把所有图片以 multipart 方式传给 AI,而是:
1. 将图片上传到配置的 AI 输入目录。
2. 构造包含模型版本、配置版本、产品 ID 和图片明细的 JSON 请求。
3. 调用 `Ai_Model.ApiUrl`。
4. 根据 AI 响应中的 `SubImgInfo` 下载结果图。
5. 将图片级结果、原因、参数明细写回任务结果。
6. 清理远端上传图和结果图。
## 3. 生产主流程
```mermaid
sequenceDiagram
participant PLC as PLC
participant PC as 上位机 FlyShot
participant CTRL as 飞拍控制单元
participant ROBOT as FANUC 机器人
participant CAM as 工业相机
participant AI as YOLO AI
participant MES as MES
PLC->>PC: StartFlyShotTask 上升沿 + 产品/任务信息
PC->>PC: 初始化 WorkStationTask / FlyShotTask
PC->>CAM: 注册回调、设置曝光/增益
PC->>CTRL: POST /execute_flyshot/
CTRL->>ROBOT: move_to_start + J519 飞拍轨迹
ROBOT-->>CAM: 轨迹 IO 脉冲触发拍照
CAM-->>PC: 图像回调
PC->>PC: 保存原图并加入待处理队列
PC->>AI: 上传图片 + 发送 AI 请求
AI-->>PC: 返回 OK/NG、原因、结果图路径
PC->>PC: 下载结果图并汇总任务结果
PC-->>PLC: TaskCompleted / TaskResult / WorkStationResult
PC->>MES: PartProcessed 结果报文
```
## 4. 关键配置
### 4.1 上位机配置
上位机配置文件:
- 基础配置:`D:\Dev\Codes\MFD_Solution\FlyShot\appConfig.json`
- 调试覆盖:`D:\Dev\Codes\MFD_Solution\FlyShot\appConfig.Debug.json`
常用配置项:
| 配置路径 | 示例值 | 说明 |
| --- | --- | --- |
| `MesConfig.EnableMes` | `true` | 是否启用 MES 通信 |
| `MesConfig.ServerIp` / `ServerPort` | `174.34.82.193` / `55065` | MES 服务地址 |
| `MesConfig.LineNo` / `ProcessNo` / `StatNo` | `6958` / `993002` / `9930` | MES 位置上下文 |
| `MesConfig.TelegramFolder` | `D:\MesTelegram` | MES 报文保存目录 |
| `CameraIp` / `LocalIp` | `192.168.11.11` / `192.168.11.188` | 工业相机与本机网卡地址 |
| `CameraSavePath` | `D:\Images\flyShot` | 飞拍原图、结果图和任务 JSON 保存根目录 |
| `MPC.MpcInterfaceIp` | `192.168.11.12` | 飞拍控制单元 HTTP 地址 |
| `MPC.MpcInterfacePort` | `9001` | 上位机调用的飞拍控制单元端口 |
| `MPC.RobotName` | `LR_Mate_200iD_7L` | 机器人型号/名称 |
| `MPC.RobotIp` | `192.168.10.11` | FANUC 控制柜 IP |
| `Ai_Model.ApiUrl` | `http://127.0.0.1:8124/corebg` | AI HTTP 接口地址 |
| `Ai_Model.AI_Model_Version` | `PEU` | AI 模型版本 |
| `Ai_Model.AI_configVersion` | `peu_20241011` | AI 配置版本 |
| `Ai_Model.ai_upload_path` | `/mnt/d/ai_images/ai-feed` | AI 输入图片目录 |
| `Ai_Model.ai_result_path` | `/mnt/d/ai_images` | AI 结果图片目录 |
| `FileTransfer.DefaultService` | `Primary` | 当前使用的文件传输服务 |
| `PLC.EnablePlc` | `true` | 是否启用 PLC 通信 |
| `PLC.OpcUaServer` | `opc.tcp://174.34.178.68:4840` | OPC UA 服务地址 |
> 注意:如果飞拍控制单元实际监听端口是 `5000`,上位机的 `MPC.MpcInterfacePort` 需要同步改为 `5000`,或通过反向代理/部署配置把控制单元暴露为 `9001`。
### 4.2 飞拍控制单元配置
飞拍控制单元配置文件:
- `D:\Dev\Codes\rvbust-code\FlyingShotPkg_3.15_VDA\flyshot-replacement\src\Flyshot.Server.Host\appsettings.json`
常用配置项:
| 配置路径 | 示例值 | 说明 |
| --- | --- | --- |
| `ControllerClientCompat.AutoInit.RobotName` | `FANUC_LR_Mate_200iD_7L` | 服务启动自动初始化的机器人名称 |
| `ControllerClientCompat.AutoInit.RobotIp` | `192.168.10.11` | 服务启动自动连接的机器人 IP |
| `Kestrel.Endpoints.Http.Url` | `http://0.0.0.0:5000` | HTTP 服务监听地址 |
| `Swagger.Enabled` | `true` | 是否启用 Swagger 和调试接口 |
| `Cors.AllowedOrigins` | `*` | 允许跨域调用的来源 |
## 5. 飞拍控制单元 HTTP 接口
### 5.1 初始化机器人
推荐使用聚合初始化接口:
```http
POST /init_mpc_robot
Content-Type: application/json
{
"robot_name": "FANUC_LR_Mate_200iD_7L",
"robot_ip": "192.168.10.11"
}
```
内部执行顺序:
1. `SetUpRobot`:加载机器人模型和关节限制。
2. `Connect`:建立 FANUC 状态、命令、J519 三条通道。
3. `EnableRobot(8)`:启动机器人程序和运动循环。
也可以拆分调用:
```text
POST /setup_robot/?robot_name=FANUC_LR_Mate_200iD_7L
POST /connect_robot/?ip=192.168.10.11
GET /enable_robot/?buffer_size=8
```
### 5.2 上传飞拍轨迹
```http
POST /upload_flyshot/
Content-Type: application/json
{
"name": "UTTC_MS11",
"waypoints": [[0, 0, 0, 0, 0, 0]],
"shot_flags": [true],
"offset_values": [0],
"addrs": [[1, 3]]
}
```
字段说明:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `name` | string | 飞拍轨迹名称,上位机执行时使用 |
| `waypoints` | number[][] | 机器人关节路径点 |
| `shot_flags` | bool[] | 每个路径点是否拍照 |
| `offset_values` | number[] | 拍照触发偏移周期 |
| `addrs` | number[][] | 拍照触发 IO 地址组 |
### 5.3 执行飞拍轨迹
上位机 `MpcService.ExecuteFlyShot` 当前默认发送:
```http
POST /execute_flyshot/
Content-Type: application/json
{
"name": "UTTC_MS11",
"move_to_start": true,
"method": "icsp",
"save_traj": false,
"use_cache": false,
"wait": false
}
```
字段说明:
| 字段 | 推荐值 | 说明 |
| --- | --- | --- |
| `name` | 轨迹名称 | 必须已上传或存在于控制单元缓存 |
| `move_to_start` | `true` | 执行前先移动到规划起点,避免第一帧目标突变 |
| `method` | `icsp` 或 `self-adapt-icsp` | 轨迹规划方法 |
| `save_traj` | `false` | 是否保存本次规划轨迹 |
| `use_cache` | `false` 或 `true` | 是否复用缓存轨迹 |
| `wait` | `false` 或 `true` | 是否等待整条轨迹完成后再返回 |
### 5.4 常用调试接口
| 接口 | 方法 | 用途 |
| --- | --- | --- |
| `/healthz` | GET | 服务健康检查 |
| `/status.html` | GET | 浏览器状态页 |
| `/debug.html` | GET | 浏览器接口调试页 |
| `/api/status/snapshot` | GET | 当前兼容层和运行时状态快照 |
| `/robot_info/` | GET | 查询机器人信息 |
| `/set_speedRatio/` | POST | 设置速度倍率 |
| `/list_flyShotTraj/` | GET | 查询可执行飞拍轨迹名称 |
| `/delete_flyshot/` | POST | 删除飞拍轨迹 |
| `/stop_move/` | GET | 停止当前运动 |
| `/disable_robot/` | GET | 禁用机器人 |
| `/disconnect_robot/` | POST | 断开机器人连接 |
## 6. 上位机使用教程
### 6.1 启动前检查
1. 确认工业相机、机器人控制柜、AI 服务、PLC、MES 网络可达。
2. 检查上位机 `appConfig.json`:
- `CameraIp` 和 `LocalIp` 是否匹配现场网段。
- `CameraSavePath` 是否存在或可创建。
- `MPC.MpcInterfaceIp` / `MpcInterfacePort` 是否指向飞拍控制单元。
- `Ai_Model.ApiUrl` 是否指向 YOLO AI 接口。
- `FileTransfer` 是否能访问 AI 上传和结果目录。
3. 检查飞拍控制单元是否启动:
- 浏览器访问 `http://<控制单元IP>:<端口>/healthz`。
- 浏览器访问 `http://<控制单元IP>:<端口>/status.html`。
4. 检查机器人程序、控制柜状态和安全围栏。
### 6.2 上位机启动流程
```mermaid
flowchart TD
A["启动 FlyShot 上位机"] --> B["读取 appConfig.json"]
B --> C["初始化 MES<br/>可按 EnableMes 关闭"]
B --> D["初始化相机"]
B --> E["查询飞拍控制单元轨迹列表"]
E --> F["设置机器人速度倍率<br/>默认 0.5"]
D --> G["确认系统状态栏无关键 NG"]
F --> G
G --> H["等待手动或 PLC 触发任务"]
```
启动后,上位机会执行:
- 初始化 MES 连接。
- 初始化相机。
- 查询飞拍控制单元中的飞拍任务列表。
- 设置机器人速度倍率。
- 创建或检查图片保存目录。
### 6.3 手动执行飞拍任务
适用于调试、首件验证和现场排查。
1. 打开上位机。
2. 确认相机初始化成功。
3. 确认飞拍任务列表已加载。
4. 输入或扫码产品标识。
5. 选择飞拍任务名称。
6. 点击手动开始。
7. 上位机执行:
- 初始化工位任务。
- 注册相机回调。
- 调用飞拍控制单元 `/execute_flyshot/`。
- 接收相机回调并保存图片。
- 批量调用 AI 检测。
- 汇总 OK/NG。
- 保存任务 JSON。
- 如有 NG,打开人工复判界面并发送通知。
### 6.4 PLC 自动执行飞拍任务
适用于正式生产。
PLC 通过 OPC UA 写入任务触发信号,上位机收到 `StartFlyShotTask` 上升沿后执行任务。
PLC 输入给 PC 的核心信号:
| 信号 | 说明 |
| --- | --- |
| `StartFlyShotTask` | 飞拍任务启动,上升沿触发 |
| `WorkStationId` | 工位号 |
| `FlyShotTaskName` | 飞拍任务名称 |
| `CurrentTaskIndex` | 当前任务序号 |
| `TotalTaskCount` | 当前工位任务总数 |
| `ProductTypeNo` | 产品型号 |
| `ProductIdentifier` | 产品唯一标识 |
| `ProductBatch` | 产品批次 |
| `ResetSystem` | 系统重置信号 |
PC 回写给 PLC 的核心信号:
| 信号 | 说明 |
| --- | --- |
| `TaskCompleted` | 当前任务完成 |
| `TaskResult` | 当前任务结果,true=OK,false=NG |
| `CompletedTaskIndex` | 已完成任务序号 |
| `TaskProcessingTime` | 当前任务耗时 |
| `WorkStationCompleted` | 工位所有任务是否完成 |
| `WorkStationResult` | 工位总结果 |
| `SystemReady` | 系统是否就绪 |
| `SystemBusy` | 系统是否忙碌 |
| `ErrorCode` | 错误码 |
| `ErrorMessage` | 错误信息 |
| `PcHeartbeat` | PC 心跳 |
### 6.5 图片和结果保存规则
上位机会基于 `CameraSavePath` 生成任务目录:
```text
{CameraSavePath}\{yyyyMMdd}\{TypeNo-TypeVar}\{ProductIdentifier}\{FlyShotName}\
```
任务目录内通常包含:
| 内容 | 说明 |
| --- | --- |
| 原始图片 | 相机回调保存的图片 |
| `result` 子目录 | AI 下载后的结果图 |
| `flyShotTask@...json` | 单个飞拍任务详情 |
| `WorkStationTask@...json` | 工位任务汇总 |
AI 结果图命名会带上检测结果,例如:
```text
原图名@OK.jpeg
原图名@NG.jpeg
```
## 7. YOLO AI 识别流程
```mermaid
flowchart TD
A["相机回调保存原图"] --> B["上位机收集待处理图片"]
B --> C["达到 BatchSize 或采集结束"]
C --> D["上传原图到 Ai_Model.ai_upload_path"]
D --> E["构造 AiProcessRequestModel"]
E --> F["POST Ai_Model.ApiUrl"]
F --> G["解析 ProductResult / SubImgInfo / FailureModes"]
G --> H["从 Ai_Model.ai_result_path 下载结果图"]
H --> I["写回 ImageProcessOutcome"]
I --> J["更新 UI 与任务结果"]
J --> K["清理 AI 远端临时文件"]
```
### 7.1 AI 请求结构
AI 请求的典型结构:
```json
{
"ai_modelVersion": "PEU",
"ai_configVersion": "peu_20241011",
"key": "empty",
"product_id": "20250228121929",
"sub_img_info": [
{
"index": 1,
"view": "pipe",
"object": "spring",
"image_path": "000001_161008_1.jpeg",
"ai_image_path": null,
"ai_result": null,
"ai_reason": null,
"ai_param_list:": {}
}
]
}
```
字段说明:
| 字段 | 说明 |
| --- | --- |
| `ai_modelVersion` | AI 模型版本,对应上位机 `Ai_Model.AI_Model_Version` |
| `ai_configVersion` | AI 配置版本,对应上位机 `Ai_Model.AI_configVersion` |
| `product_id` | 本次 AI 任务 ID,通常由时间戳生成 |
| `sub_img_info[].index` | 图片序号 |
| `sub_img_info[].view` | 视角,例如 `pipe`、`surface`、`connector` |
| `sub_img_info[].object` | 检测对象,例如 `spring`、`top`、`side` |
| `sub_img_info[].image_path` | 上传到 AI 输入目录后的图片文件名 |
### 7.2 AI 响应处理
上位机按 `index` 将 AI 响应和原图对应起来,主要读取:
| 字段 | 说明 |
| --- | --- |
| `ProductResult` | 产品级检测结果 |
| `ErrorInfo` | AI 处理错误信息 |
| `AiFailureModel` | AI 不良模式集合 |
| `SubImgInfo[].AiResult` | 单张图片结果,`OK` 视为 OK,其他非空值视为 NG |
| `SubImgInfo[].AiReason` | 单张图片 NG 原因 |
| `SubImgInfo[].AiParamList` | AI 参数明细 |
| `SubImgInfo[].AiImagePath` | AI 结果图远端路径 |
## 8. MES 交互说明
### 8.1 换型下发
上位机启动或手动换型时,会根据 `MesConfig.TypeNo` 和 `MesConfig.TypeVar` 发送 `DataDownloadRequired`。
主要用途:
- 获取或刷新当前型号配置。
- 更新本地 `TypeNo` / `TypeVar`。
- 加载对应 `Config\TypeData` 文件。
### 8.2 结果上报
飞拍任务完成后,上位机会发送 `PartProcessed`。
上报内容包括:
- 产品唯一标识。
- 产品型号和变体。
- 工位结果,`1=OK`,`2=NG`。
- 图片相对路径。
- 每张图片的 AI 结果和人工复判结果。
- 汇总后的 NIO bits。
## 9. 常见使用场景
### 9.1 首次联调
1. 启动飞拍控制单元。
2. 访问 `/healthz` 确认服务正常。
3. 访问 `/status.html` 查看模型和机器人状态。
4. 调用 `/init_mpc_robot` 初始化机器人。
5. 调用 `/list_flyShotTraj/` 确认可用轨迹。
6. 启动上位机。
7. 检查上位机日志中相机、MPC、MES、PLC 状态。
8. 使用手动模式执行一次飞拍任务。
9. 检查图片目录、AI 结果图、任务 JSON 和 MES/PLC 反馈。
### 9.2 正式生产
1. 启动飞拍控制单元并确认状态正常。
2. 启动上位机并确认系统就绪。
3. PLC 写入产品和任务信息。
4. PLC 触发 `StartFlyShotTask` 上升沿。
5. 上位机执行飞拍、AI 检测和结果汇总。
6. 上位机回写 PLC 当前任务和工位结果。
7. 上位机发送 MES `PartProcessed`。
8. PLC 根据 `WorkStationCompleted` 和 `WorkStationResult` 决定后续流程。
### 9.3 离线排查 AI
1. 准备一组本地图片。
2. 在上位机调试页面或相关调试入口选择图片。
3. 确认 `Ai_Model.ApiUrl`、`ai_upload_path`、`ai_result_path` 正确。
4. 执行 AI 请求。
5. 检查 AI 响应、结果图下载、上位机日志和远端文件清理情况。
## 10. 故障排查
```mermaid
flowchart TD
A["发现任务失败或结果异常"] --> B{"失败阶段"}
B -->|相机无图| C["检查 CameraIp / LocalIp / 相机连接 / 触发源 / 曝光增益"]
B -->|控制单元失败| D["检查 /healthz /status.html / MpcInterfaceIp / 端口 / 机器人状态"]
B -->|机器人报警| E["检查 FANUC 控制柜 / RVBUSTSM / J519 通道 / 安全状态"]
B -->|AI 无结果| F["检查 Ai_Model.ApiUrl / 文件传输目录 / AI 服务日志 / 结果图路径"]
B -->|PLC 不触发| G["检查 OPC UA 地址 / ActiveProfile / StartFlyShotTask 上升沿 / SystemReady"]
B -->|MES 失败| H["检查 EnableMes / ServerIp / ServerPort / TelegramFolder / 报文响应"]
C --> I["查看上位机日志和图片目录"]
D --> I
E --> I
F --> I
G --> I
H --> I
```
### 10.1 相机问题
| 现象 | 排查点 |
| --- | --- |
| 无图像回调 | 相机 IP、本机网卡、相机初始化、回调注册 |
| 图片曝光异常 | 飞拍点位配置中的曝光和增益 |
| 图片保存失败 | `CameraSavePath` 权限、磁盘空间、路径是否存在 |
### 10.2 飞拍控制单元问题
| 现象 | 排查点 |
| --- | --- |
| 上位机无法连接 MPC | `MPC.MpcInterfaceIp` / `MpcInterfacePort` 是否指向控制单元 |
| `/execute_flyshot/` 失败 | 轨迹是否已上传、机器人是否初始化、`method` 是否支持 |
| 机器人不运动 | `/status.html` 状态、FANUC 通道、使能状态、控制柜报警 |
| 轨迹执行后无拍照 | `shot_flags`、`offset_values`、`addrs`、J519 IO 脉冲 |
### 10.3 AI 问题
| 现象 | 排查点 |
| --- | --- |
| AI 请求失败 | `Ai_Model.ApiUrl`、AI 服务是否启动、HTTP 是否可达 |
| AI 找不到图片 | `ai_upload_path`、文件传输服务、远端目录权限 |
| 结果图下载失败 | `ai_result_path`、`AiImagePath`、SFTP/SMB/LOCAL 配置 |
| 结果和图片错位 | 检查 AI 响应中的 `index` 是否和请求一致 |
### 10.4 PLC/MES 问题
| 现象 | 排查点 |
| --- | --- |
| PLC 不触发任务 | `PLC.EnablePlc`、OPC UA 地址、变量映射、上升沿保持时间 |
| PLC 反馈不更新 | 输出变量是否可写、`SystemBusy/SystemReady` 状态 |
| MES 连接失败 | `MesConfig.EnableMes`、IP、端口、防火墙 |
| MES 报文异常 | `TelegramFolder` 下保存的 XML 报文和响应 |
## 11. 交付与飞书整理建议
建议后续整理为飞书文档时采用以下方式:
1. 以本文档 Markdown 作为源稿。
2. 将 Mermaid 流程图渲染为 PNG 或 SVG 图片后插入飞书。
3. 保留接口表、配置表和操作步骤,适合现场培训。
4. 如果需要正式发布版本,可再从 Markdown 生成 Word,再导入飞书云文档。
5. 每次接口、配置或流程变更后,优先更新 Markdown 源稿,再同步飞书版本。
建议拆分飞书目录:
```text
飞拍系统说明
├─ 1. 系统总览
├─ 2. 生产主流程
├─ 3. 上位机使用教程
├─ 4. 飞拍控制单元使用教程
├─ 5. YOLO AI 接口说明
├─ 6. PLC 与 MES 交互
└─ 7. 常见问题排查
```

View File

@@ -0,0 +1,32 @@
# 真机优先与离线飞拍规划改造计划
## 已实施目标
- 启动及 `/init_mpc_robot` 固定执行“加载模型 → 连接真机 → `EnableRobot(8)`”。
- 模型加载成功但连接或使能失败时进入 `OfflineReady`,HTTP 宿主继续运行。
- 飞拍资产固定保存到 `Config/RobotConfig.json`,上传、列出、删除不依赖模型或真机状态。
- `/save_traj_info/` 只要求模型已加载,返回完整最终发送点列、触发绑定和导出文件绝对路径。
- `/execute_flyshot/` 仍要求 `RealReady`,离线规划不会伪造机器人连接或使能。
- `/debug` 提供模型加载、轨迹上传/选择、离线生成、分页点列和导出路径查看。
## 状态约定
- `LoadingModel`:正在加载机器人模型。
- `Connecting`:模型已加载,正在连接或使能真机。
- `RealReady`:真机已连接且使能,允许真实运动和 IO。
- `OfflineReady`:模型已加载但真机不可用,允许离线规划。
- `ModelLoadFailed`:机器人模型不可用,禁止规划和执行。
## HTTP 约定
- 保留 `/setup_robot/`,仅加载模型,不连接、不使能,也不清空飞拍资产。
- 新增 `/init_mpc_robot`,请求体只包含 `robot_name`、`robot_ip`。
- 删除 `/init_mpc_robt`、`/connect_server/`、版本查询、环境文件、TCP 显示、`is_setup` 和仿真控制器切换路由。
- `/upload_flyshot/` 在上传阶段只校验资产结构和有限数值;自由度与模型限制在规划阶段校验。
- `/save_traj_info/` 返回规划摘要、关键/稠密轨迹、最终发送点、ShotEvents、触发绑定和导出路径。
## 验证
- 核心测试覆盖离线资产生命周期、模型重载保留资产、连接失败降级和完整离线规划结果。
- 集成测试覆盖删除路由 404、新初始化接口、状态快照和 Debug 离线规划页面。
- 最终执行核心测试、集成测试和解决方案构建。

View File

@@ -0,0 +1,14 @@
# 飞拍 DO / RO 输出选择
`RobotConfig.json` 的 `robot.use_do` 用于选择飞拍输出类型:
- `true`:使用 DO,J519 `writeIoType=2`。
- `false`:使用 RO,J519 `writeIoType=9`。
两种模式都根据 `shot_flags / offset_values / addr` 生成触发事件,保持 `io_keep_cycles` 的脉冲保持与清零行为。`use_do=false` 不再关闭拍照触发。
触发时间轴将输出选择固化到每个事件中;FANUC 运行时根据事件选择写入类型,触发帧和清零帧使用相同类型。未携带输出选择的已有事件默认使用 DO,普通无触发运动保持原行为。
本次不修改配置中的 `addr`,不增加 RO 点位范围校验,现场点位由调用方自行调试。手动 HTTP `SetIo` 仍使用显式 `io_type`。
验证覆盖配置到事件的传递,以及 DO / RO 两种模式下的 J519 脉冲与清零。软件测试不代表末端接线和真实相机触发已完成验收。

View File

@@ -61,7 +61,7 @@ public sealed class AutoInitOptions
public string RobotName { get; set; } = string.Empty; public string RobotName { get; set; } = string.Empty;
/// <summary> /// <summary>
/// 获取或设置机器人 IP 地址,用于连接真机或仿真控制器。 /// 获取或设置机器人 IP 地址,用于连接真实 FANUC 控制器。
/// </summary> /// </summary>
public string RobotIp { get; set; } = string.Empty; public string RobotIp { get; set; } = string.Empty;
} }

View File

@@ -49,7 +49,7 @@ public sealed class ControllerClientCompatRobotCatalog
if (!SupportedRobotModelFileMap.TryGetValue(robotName, out var modelFileName)) if (!SupportedRobotModelFileMap.TryGetValue(robotName, out var modelFileName))
{ {
throw new InvalidOperationException($"Unsupported robot name: {robotName}"); throw new InvalidOperationException($"不支持的机器人名称 Unsupported robot name: {robotName}");
} }
var modelPath = ResolveModelPath(modelFileName); var modelPath = ResolveModelPath(modelFileName);

View File

@@ -25,11 +25,12 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
private RobotProfile? _activeRobotProfile; private RobotProfile? _activeRobotProfile;
private string? _configuredRobotName; private string? _configuredRobotName;
private CompatibilityRobotSettings? _robotSettings; private CompatibilityRobotSettings? _robotSettings;
private string? _connectedServerIp; private string _operatingMode = "ModelLoadFailed";
private int _connectedServerPort; private string _initializationStage = "NotStarted";
private bool _showTcp = true; private string? _robotIp;
private double _showTcpAxisLength = 0.1; private string? _lastConnectionError;
private int _showTcpAxisSize = 2; private DateTimeOffset? _lastConnectionErrorAt;
private DateTimeOffset? _lastInitializationAttemptAt;
/// <summary> /// <summary>
/// 初始化一份 HTTP-only 的 ControllerClient 兼容服务。 /// 初始化一份 HTTP-only 的 ControllerClient 兼容服务。
@@ -60,6 +61,15 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
_artifactWriter = artifactWriter ?? new FlyshotTrajectoryArtifactWriter(_options, new RobotModelLoader()); _artifactWriter = artifactWriter ?? new FlyshotTrajectoryArtifactWriter(_options, new RobotModelLoader());
_trajectoryStore = trajectoryStore ?? new JsonFlyshotTrajectoryStore(_options, _configLoader); _trajectoryStore = trajectoryStore ?? new JsonFlyshotTrajectoryStore(_options, _configLoader);
_logger = logger; _logger = logger;
// 飞拍轨迹是固定 RobotConfig.json 中的独立资产,服务构造后立即加载,不依赖机器人模型。
var savedTrajectories = _trajectoryStore.LoadAll(out var savedSettings);
foreach (var saved in savedTrajectories)
{
_uploadedTrajectories[saved.Key] = saved.Value;
}
_robotSettings = savedSettings;
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -82,47 +92,19 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
/// </summary> /// </summary>
public bool IsInMotion => _runtime.GetSnapshot().IsInMotion; public bool IsInMotion => _runtime.GetSnapshot().IsInMotion;
/// <inheritdoc />
public void ConnectServer(string serverIp, int port)
{
if (string.IsNullOrWhiteSpace(serverIp))
{
throw new ArgumentException("服务端 IP 不能为空。", nameof(serverIp));
}
if (port <= 0)
{
throw new ArgumentOutOfRangeException(nameof(port), "端口必须大于 0。");
}
lock (_stateLock)
{
// HTTP-only 阶段仍记录旧客户端期望的 50001 地址,便于后续 TCP 入口恢复时复用状态。
_connectedServerIp = serverIp;
_connectedServerPort = port;
}
_logger?.LogInformation("ConnectServer 完成: {ServerIp}:{Port}", serverIp, port);
}
/// <inheritdoc />
public string GetServerVersion()
{
return ServerVersion;
}
/// <inheritdoc />
public string GetClientVersion()
{
return "flyshot-replacement-controller-client-compat/0.1.0";
}
/// <inheritdoc /> /// <inheritdoc />
public void SetUpRobot(string robotName) public void SetUpRobot(string robotName)
{ {
_logger?.LogInformation("SetUpRobot 开始: robotName={RobotName}", robotName); _logger?.LogInformation("SetUpRobot 开始: robotName={RobotName}", robotName);
var robotSettings = TryLoadRobotSettings() ?? CreateDefaultRobotSettings(); lock (_stateLock)
{
_initializationStage = "LoadingModel";
}
try
{
var robotSettings = TryLoadRobotSettings() ?? _robotSettings ?? CreateDefaultRobotSettings();
var robotProfile = _robotCatalog.LoadProfile( var robotProfile = _robotCatalog.LoadProfile(
robotName, robotName,
robotSettings.AccLimitScale, robotSettings.AccLimitScale,
@@ -130,25 +112,18 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
lock (_stateLock) lock (_stateLock)
{ {
// 机器人重新初始化时同步重置运行时和上传轨迹目录,保持旧服务初始化语义。 // 重新加载模型只重置 Runtime;飞拍资产继续保留在固定 RobotConfig.json 中。
_configuredRobotName = robotName; _configuredRobotName = robotName;
_activeRobotProfile = robotProfile; _activeRobotProfile = robotProfile;
_uploadedTrajectories.Clear();
_runtime.ResetRobot(robotProfile, robotName); _runtime.ResetRobot(robotProfile, robotName);
// RobotConfig.json 的 speed_ratio 是启动默认值;在线 set_speedRatio 仍可覆盖后续执行。
_runtime.SetSpeedRatio(robotSettings.SpeedRatio); _runtime.SetSpeedRatio(robotSettings.SpeedRatio);
_robotSettings = robotSettings; _robotSettings = robotSettings;
_operatingMode = "OfflineReady";
// 从持久化存储恢复该机器人名下之前已上传的轨迹。 _initializationStage = "ModelLoaded";
var savedTrajectories = _trajectoryStore.LoadAll(robotName, out _);
foreach (var saved in savedTrajectories)
{
_uploadedTrajectories[saved.Key] = saved.Value;
}
} }
_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}, speedRatio={SpeedRatio}, 保留轨迹数={TrajCount}",
robotName, robotName,
robotProfile.DegreesOfFreedom, robotProfile.DegreesOfFreedom,
robotSettings.AccLimitScale, robotSettings.AccLimitScale,
@@ -156,51 +131,72 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
robotSettings.SpeedRatio, robotSettings.SpeedRatio,
_uploadedTrajectories.Count); _uploadedTrajectories.Count);
} }
catch
/// <inheritdoc />
public void SetUpRobotFromEnv(string envFile)
{ {
if (string.IsNullOrWhiteSpace(envFile)) try
{ {
throw new ArgumentException("环境文件路径不能为空。", nameof(envFile)); _runtime.Disconnect();
} }
catch
throw new NotSupportedException("SetUpRobotFromEnv 尚未接入环境文件解析。");
}
/// <inheritdoc />
public void SetShowTcp(bool isShow, double axisLength, int axisSize)
{ {
if (axisLength <= 0.0) // 模型切换失败时优先清理上层状态;底层若尚未初始化,Disconnect 失败可忽略。
{
throw new ArgumentOutOfRangeException(nameof(axisLength), "TCP 坐标轴长度必须大于 0。");
}
if (axisSize <= 0)
{
throw new ArgumentOutOfRangeException(nameof(axisSize), "TCP 坐标轴线宽必须大于 0。");
} }
lock (_stateLock) lock (_stateLock)
{ {
EnsureRobotSetup(); _activeRobotProfile = null;
_configuredRobotName = null;
_operatingMode = "ModelLoadFailed";
_initializationStage = "ModelLoadFailed";
}
// 当前无 GUI 渲染层,先保存显示参数,保证旧 SDK 参数不会在 HTTP 边界丢失。 throw;
_showTcp = isShow;
_showTcpAxisLength = axisLength;
_showTcpAxisSize = axisSize;
} }
} }
/// <inheritdoc /> /// <inheritdoc />
public void SetActiveController(bool sim) public void InitializeRobot(string robotName, string robotIp, int bufferSize = 8)
{ {
lock (_stateLock) lock (_stateLock)
{ {
EnsureRobotSetup(); _lastInitializationAttemptAt = DateTimeOffset.UtcNow;
_robotIp = robotIp;
_lastConnectionError = null;
_lastConnectionErrorAt = null;
} }
_runtime.SetActiveController(sim); SetUpRobot(robotName);
try
{
lock (_stateLock)
{
_initializationStage = "Connecting";
}
// 真机优先初始化固定执行连接和使能,不再选择生产仿真控制器。
Connect(robotIp);
EnableRobot(bufferSize);
lock (_stateLock)
{
_operatingMode = "RealReady";
_initializationStage = "RealReady";
}
}
catch (Exception exception)
{
lock (_stateLock)
{
// 模型已加载时,真机失败必须保留离线规划能力。
_operatingMode = _activeRobotProfile is null ? "ModelLoadFailed" : "OfflineReady";
_initializationStage = "OfflineReady";
_lastConnectionError = exception.Message;
_lastConnectionErrorAt = DateTimeOffset.UtcNow;
}
throw;
}
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -216,6 +212,8 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
lock (_stateLock) lock (_stateLock)
{ {
EnsureRobotSetup(); EnsureRobotSetup();
_robotIp = robotIp;
_initializationStage = "Connecting";
} }
_runtime.Connect(robotIp); _runtime.Connect(robotIp);
@@ -232,6 +230,11 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
} }
_runtime.Disconnect(); _runtime.Disconnect();
lock (_stateLock)
{
_operatingMode = _activeRobotProfile is null ? "ModelLoadFailed" : "OfflineReady";
_initializationStage = _activeRobotProfile is null ? "ModelLoadFailed" : "OfflineReady";
}
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -243,6 +246,13 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
EnsureRobotSetup(); EnsureRobotSetup();
} }
_runtime.EnableRobot(bufferSize); _runtime.EnableRobot(bufferSize);
lock (_stateLock)
{
_operatingMode = "RealReady";
_initializationStage = "RealReady";
_lastConnectionError = null;
_lastConnectionErrorAt = null;
}
_logger?.LogInformation("EnableRobot 完成"); _logger?.LogInformation("EnableRobot 完成");
} }
@@ -257,6 +267,11 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
// DisableRobot 是中断/控制命令,不能被长时间运动串行锁挡住。 // DisableRobot 是中断/控制命令,不能被长时间运动串行锁挡住。
_runtime.DisableRobot(); _runtime.DisableRobot();
lock (_stateLock)
{
_operatingMode = _activeRobotProfile is null ? "ModelLoadFailed" : "OfflineReady";
_initializationStage = _activeRobotProfile is null ? "ModelLoadFailed" : "OfflineReady";
}
_logger?.LogInformation("DisableRobot 完成"); _logger?.LogInformation("DisableRobot 完成");
} }
@@ -290,9 +305,18 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
isSetup, isSetup,
isSetup ? _configuredRobotName : null, isSetup ? _configuredRobotName : null,
isSetup ? _activeRobotProfile!.DegreesOfFreedom : 0, isSetup ? _activeRobotProfile!.DegreesOfFreedom : 0,
isSetup ? _uploadedTrajectories.Keys.ToArray() : Array.Empty<string>(), _uploadedTrajectories.Keys.ToArray(),
GetServerVersion(), ServerVersion,
GetClientVersion()); "flyshot-replacement-controller-client-compat/0.1.0",
_operatingMode,
_initializationStage,
_robotIp,
_lastConnectionError,
_lastConnectionErrorAt,
_lastInitializationAttemptAt,
canPlanOffline: isSetup,
canExecuteRealMotion: string.Equals(_operatingMode, "RealReady", StringComparison.Ordinal)
&& _runtime.GetSnapshot().IsEnabled);
} }
} }
@@ -509,19 +533,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; CompatibilityRobotSettings settings;
lock (_stateLock) lock (_stateLock)
{ {
EnsureRuntimeEnabled(); ValidateUploadedTrajectoryAsset(trajectory);
_uploadedTrajectories[trajectory.Name] = trajectory; _uploadedTrajectories[trajectory.Name] = trajectory;
robotName = _configuredRobotName ?? throw new InvalidOperationException("Robot has not been setup.");
settings = _robotSettings ?? CreateDefaultRobotSettings(); settings = _robotSettings ?? CreateDefaultRobotSettings();
} }
// RobotConfig.json 持久化是文件 I/O,放在状态锁外,避免状态页轮询被磁盘写入拖住。 // RobotConfig.json 持久化是文件 I/O,放在状态锁外,避免状态页轮询被磁盘写入拖住。
_trajectoryStore.Save(robotName, settings, trajectory); _trajectoryStore.Save(settings, trajectory);
_logger?.LogInformation("UploadTrajectory 完成: name={Name}", trajectory.Name); _logger?.LogInformation("UploadTrajectory 完成: name={Name}", trajectory.Name);
} }
@@ -630,6 +651,7 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
/// <summary> /// <summary>
/// 从当前关节位置生成临时 PTP 稠密轨迹并阻塞等待运行时完成,避免后续 J519 目标发生突变。 /// 从当前关节位置生成临时 PTP 稠密轨迹并阻塞等待运行时完成,避免后续 J519 目标发生突变。
/// 规划倍率与当前运行速度倍率相乘后用于生成轨迹,使直接移动和飞拍预移动采用相同限速规则。
/// </summary> /// </summary>
/// <param name="robot">当前机器人模型。</param> /// <param name="robot">当前机器人模型。</param>
/// <param name="targetJointPositions">目标关节位置,单位为弧度。</param> /// <param name="targetJointPositions">目标关节位置,单位为弧度。</param>
@@ -641,11 +663,21 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
EnsureJointVector(targetJointPositions, robot.DegreesOfFreedom, nameof(targetJointPositions)); EnsureJointVector(targetJointPositions, robot.DegreesOfFreedom, nameof(targetJointPositions));
var speedRatio = _runtime.GetSnapshot().SpeedRatio; var speedRatio = _runtime.GetSnapshot().SpeedRatio;
var moveResult = MoveJointTrajectoryGenerator.CreateResult(robot, currentJointPositions, targetJointPositions, speedRatio, _logger); double planningSpeedScale;
lock (_stateLock)
{
planningSpeedScale = RequireRobotSettings().PlanningSpeedScale;
}
// 在生成阶段统一缩放速度、加速度和跃度限制,发送阶段仍保持固定伺服周期。
var effectivePlanningSpeedScale = planningSpeedScale * speedRatio;
var moveResult = MoveJointTrajectoryGenerator.CreateResult(robot, currentJointPositions, targetJointPositions, effectivePlanningSpeedScale, _logger);
_logger?.LogInformation( _logger?.LogInformation(
"{OperationName} PTP规划完成: 当前速度倍率={SpeedRatio}, 规划时长={Duration}s, 采样点数={SampleCount}", "{OperationName} PTP规划完成: 当前速度倍率={SpeedRatio}, 规划倍率={PlanningSpeedScale}, 有效规划倍率={EffectivePlanningSpeedScale}, 规划时长={Duration}s, 采样点数={SampleCount}",
operationName, operationName,
speedRatio, speedRatio,
planningSpeedScale,
effectivePlanningSpeedScale,
moveResult.Duration.TotalSeconds, moveResult.Duration.TotalSeconds,
moveResult.DenseJointTrajectory?.Count ?? 0); moveResult.DenseJointTrajectory?.Count ?? 0);
@@ -743,7 +775,7 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
} }
/// <inheritdoc /> /// <inheritdoc />
public void SaveTrajectoryInfo(string name, string method = "icsp") public OfflineFlyshotPlanningResult SaveTrajectoryInfo(string name, string method = "icsp")
{ {
if (string.IsNullOrWhiteSpace(name)) if (string.IsNullOrWhiteSpace(name))
{ {
@@ -792,6 +824,7 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
speedRatio); speedRatio);
_logger?.LogInformation("SaveTrajectoryInfo 完成: name={Name}", name); _logger?.LogInformation("SaveTrajectoryInfo 完成: name={Name}", name);
return CreateOfflinePlanningResult(name, method, robot, bundle, speedRatio);
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -858,7 +891,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,11 +899,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.");
} }
// 删除持久化文件不占用状态锁,状态页只需要看到内存字典的即时快照。 // 删除持久化文件不占用状态锁,状态页只需要看到内存字典的即时快照。
_trajectoryStore.Delete(robotName, name); _trajectoryStore.Delete(name);
_logger?.LogInformation("DeleteTrajectory 完成: name={Name}", name); _logger?.LogInformation("DeleteTrajectory 完成: name={Name}", name);
} }
@@ -1066,8 +1097,7 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
bundle.ExecutionTrajectory, bundle.ExecutionTrajectory,
bundle.Result, bundle.Result,
robot.ServoPeriod.TotalSeconds, robot.ServoPeriod.TotalSeconds,
requestedSpeedRatio: speedRatio, requestedSpeedRatio: speedRatio);
samplingSpeedRatio: 1.0);
var preparedResult = new TrajectoryResult( var preparedResult = new TrajectoryResult(
programName: bundle.Result.ProgramName, programName: bundle.Result.ProgramName,
method: bundle.Result.Method, method: bundle.Result.Method,
@@ -1146,4 +1176,248 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
adaptIcspTryNum: 5); adaptIcspTryNum: 5);
} }
/// <summary>
/// 校验上传资产自身结构,不在上传阶段绑定机器人自由度或关节限制。
/// </summary>
/// <param name="trajectory">待保存的飞拍资产。</param>
private static void ValidateUploadedTrajectoryAsset(ControllerClientCompatUploadedTrajectory trajectory)
{
if (trajectory.Waypoints.Count == 0)
{
throw new ArgumentException("飞拍轨迹至少需要一个路点。", nameof(trajectory));
}
if (trajectory.ShotFlags.Count != trajectory.Waypoints.Count
|| trajectory.OffsetValues.Count != trajectory.Waypoints.Count
|| trajectory.AddressGroups.Count != trajectory.Waypoints.Count)
{
throw new ArgumentException("飞拍轨迹的辅助数组长度必须与路点数量一致。", nameof(trajectory));
}
for (var waypointIndex = 0; waypointIndex < trajectory.Waypoints.Count; waypointIndex++)
{
if (trajectory.Waypoints[waypointIndex].Length == 0)
{
throw new ArgumentException($"第 {waypointIndex} 个路点不能为空。", nameof(trajectory));
}
for (var jointIndex = 0; jointIndex < trajectory.Waypoints[waypointIndex].Length; jointIndex++)
{
var value = trajectory.Waypoints[waypointIndex][jointIndex];
if (double.IsNaN(value) || double.IsInfinity(value))
{
throw new ArgumentOutOfRangeException(
nameof(trajectory),
$"第 {waypointIndex} 个路点的第 {jointIndex} 个值必须是有限数值。");
}
}
}
}
/// <summary>
/// 把内部规划结果转换成供 HTTP 和 Debug 页面直接消费的完整离线结果。
/// </summary>
/// <param name="name">轨迹名称。</param>
/// <param name="method">规划方法。</param>
/// <param name="robot">当前机器人模型。</param>
/// <param name="bundle">已经完成最终发送队列构建的结果包。</param>
/// <param name="speedRatio">本次速度倍率。</param>
/// <returns>完整离线规划结果。</returns>
private OfflineFlyshotPlanningResult CreateOfflinePlanningResult(
string name,
string method,
RobotProfile robot,
PlannedExecutionBundle bundle,
double speedRatio)
{
var prepared = bundle.PreparedExecution
?? throw new InvalidOperationException("离线规划结果缺少最终发送队列。");
var ioBySampleIndex = BuildIoTimeline(prepared);
var finalSendPoints = prepared.Samples.Select((sample, index) =>
{
ioBySampleIndex.TryGetValue(index, out var io);
return new OfflineFlyshotSendPoint(
sample.SampleIndex,
sample.SendTime,
sample.TrajectoryTime,
sample.JointsDegrees,
io.Mask,
io.Value);
}).ToArray();
var triggerBindings = prepared.TriggerBindings.Select(binding =>
{
var ioValue = ComputeIoValue(binding.Trigger.AddressGroup);
return new OfflineFlyshotTriggerBinding(
binding.Trigger.WaypointIndex,
binding.Trigger.TriggerTime,
binding.SampleIndex,
binding.Sample.SampleIndex,
binding.FoundInWindow,
ioValue,
ioValue);
}).ToArray();
var plannedRows = bundle.ExecutionTrajectory.PlannedWaypoints
.Select((waypoint, index) =>
(IReadOnlyList<double>)[bundle.ExecutionTrajectory.WaypointTimes[index], .. waypoint.Positions])
.ToArray();
var artifactDirectory = Path.Combine(_options.ResolveConfigRoot(), "Data", SanitizeDirectoryName(name));
var artifactPaths = new[]
{
"JointTraj.txt",
"JointDetialTraj.txt",
"CartTraj.txt",
"CartDetialTraj.txt",
"ActualSendJointTraj.txt",
"ActualSendTiming.txt",
"ActualSendJerkStats.txt",
"ShotEvents.json"
}.ToDictionary(
static fileName => fileName,
fileName => Path.GetFullPath(Path.Combine(artifactDirectory, fileName)),
StringComparer.Ordinal);
var cartesianTrajectory = ReadNumericRows(artifactPaths["CartDetialTraj.txt"]);
var diagnostics = CalculateDiagnostics(prepared.Samples);
return new OfflineFlyshotPlanningResult(
name,
method,
bundle.Result.IsValid,
bundle.Result.Duration.TotalSeconds,
prepared.FinalDurationSeconds,
bundle.Result.OriginalWaypointCount,
bundle.Result.PlannedWaypointCount,
bundle.Result.DenseJointTrajectory?.Count ?? 0,
speedRatio,
robot.ServoPeriod.TotalSeconds,
plannedRows,
bundle.Result.DenseJointTrajectory ?? Array.Empty<IReadOnlyList<double>>(),
cartesianTrajectory,
finalSendPoints,
bundle.Result.ShotEvents,
triggerBindings,
artifactPaths,
diagnostics);
}
/// <summary>
/// 根据触发绑定和保持周期构造每个最终发送点的 IO 掩码和值。
/// </summary>
/// <param name="prepared">最终发送结果。</param>
/// <returns>按发送点索引保存的 IO 数据。</returns>
private static Dictionary<int, (ushort Mask, ushort Value)> BuildIoTimeline(FlyshotPreparedExecution prepared)
{
var result = new Dictionary<int, (ushort Mask, ushort Value)>();
foreach (var binding in prepared.TriggerBindings)
{
var ioValue = ComputeIoValue(binding.Trigger.AddressGroup);
var holdCount = Math.Max(1, binding.Trigger.HoldCycles);
for (var offset = 0; offset < holdCount && binding.SampleIndex + offset < prepared.Samples.Count; offset++)
{
result[binding.SampleIndex + offset] = (ioValue, ioValue);
}
}
return result;
}
/// <summary>
/// 把 IO 地址组转换为 16 位写出掩码。
/// </summary>
/// <param name="group">IO 地址组。</param>
/// <returns>写出掩码和值。</returns>
private static ushort ComputeIoValue(IoAddressGroup group)
{
ushort value = 0;
foreach (var address in group.Addresses)
{
if (address is >= 1 and <= 16)
{
value |= (ushort)(1 << (address - 1));
}
}
return value;
}
/// <summary>
/// 清理轨迹名称中的非法路径字符。
/// </summary>
/// <param name="name">原始轨迹名称。</param>
/// <returns>可安全用于目录名的字符串。</returns>
private static string SanitizeDirectoryName(string name)
{
var invalidChars = Path.GetInvalidFileNameChars();
return new string(name.Select(character => invalidChars.Contains(character) ? '_' : character).ToArray());
}
/// <summary>
/// 读取空格分隔的数值轨迹文件。
/// </summary>
/// <param name="path">待读取文件绝对路径。</param>
/// <returns>数值行集合;文件不存在时返回空集合。</returns>
private static IReadOnlyList<IReadOnlyList<double>> ReadNumericRows(string path)
{
if (!File.Exists(path))
{
return Array.Empty<IReadOnlyList<double>>();
}
return File.ReadLines(path)
.Where(static line => !string.IsNullOrWhiteSpace(line))
.Select(line => (IReadOnlyList<double>)line
.Split(' ', StringSplitOptions.RemoveEmptyEntries)
.Select(value => double.Parse(value, System.Globalization.CultureInfo.InvariantCulture))
.ToArray())
.ToArray();
}
/// <summary>
/// 从最终发送点列计算最大绝对速度、加速度和跃度。
/// </summary>
/// <param name="samples">最终发送采样点。</param>
/// <returns>离散运动学诊断统计。</returns>
private static OfflineFlyshotDiagnostics CalculateDiagnostics(IReadOnlyList<FlyshotPreparedSample> samples)
{
var maxVelocity = 0.0;
var maxAcceleration = 0.0;
var maxJerk = 0.0;
double[]? previousVelocity = null;
double[]? previousAcceleration = null;
for (var sampleIndex = 1; sampleIndex < samples.Count; sampleIndex++)
{
var dt = samples[sampleIndex].SendTime - samples[sampleIndex - 1].SendTime;
if (dt <= 0.0)
{
continue;
}
var velocity = new double[samples[sampleIndex].JointsDegrees.Count];
var acceleration = new double[velocity.Length];
for (var jointIndex = 0; jointIndex < velocity.Length; jointIndex++)
{
velocity[jointIndex] =
(samples[sampleIndex].JointsDegrees[jointIndex] - samples[sampleIndex - 1].JointsDegrees[jointIndex]) / dt;
maxVelocity = Math.Max(maxVelocity, Math.Abs(velocity[jointIndex]));
if (previousVelocity is not null)
{
acceleration[jointIndex] = (velocity[jointIndex] - previousVelocity[jointIndex]) / dt;
maxAcceleration = Math.Max(maxAcceleration, Math.Abs(acceleration[jointIndex]));
}
if (previousAcceleration is not null)
{
var jerk = (acceleration[jointIndex] - previousAcceleration[jointIndex]) / dt;
maxJerk = Math.Max(maxJerk, Math.Abs(jerk));
}
}
previousVelocity = velocity;
previousAcceleration = acceleration;
}
return new OfflineFlyshotDiagnostics(maxVelocity, maxAcceleration, maxJerk);
}
} }

View File

@@ -20,7 +20,15 @@ public sealed record ControllerClientStatusSnapshotMetadata
int degreesOfFreedom, int degreesOfFreedom,
IReadOnlyList<string> uploadedTrajectories, IReadOnlyList<string> uploadedTrajectories,
string serverVersion, string serverVersion,
string clientVersion) string clientVersion,
string operatingMode = "ModelLoadFailed",
string initializationStage = "NotStarted",
string? robotIp = null,
string? lastConnectionError = null,
DateTimeOffset? lastConnectionErrorAt = null,
DateTimeOffset? lastInitializationAttemptAt = null,
bool canPlanOffline = false,
bool canExecuteRealMotion = false)
{ {
IsSetup = isSetup; IsSetup = isSetup;
RobotName = robotName; RobotName = robotName;
@@ -28,6 +36,14 @@ public sealed record ControllerClientStatusSnapshotMetadata
UploadedTrajectories = uploadedTrajectories.ToArray(); UploadedTrajectories = uploadedTrajectories.ToArray();
ServerVersion = serverVersion; ServerVersion = serverVersion;
ClientVersion = clientVersion; ClientVersion = clientVersion;
OperatingMode = operatingMode;
InitializationStage = initializationStage;
RobotIp = robotIp;
LastConnectionError = lastConnectionError;
LastConnectionErrorAt = lastConnectionErrorAt;
LastInitializationAttemptAt = lastInitializationAttemptAt;
CanPlanOffline = canPlanOffline;
CanExecuteRealMotion = canExecuteRealMotion;
} }
/// <summary> /// <summary>
@@ -59,4 +75,28 @@ public sealed record ControllerClientStatusSnapshotMetadata
/// 获取兼容客户端版本。 /// 获取兼容客户端版本。
/// </summary> /// </summary>
public string ClientVersion { get; } public string ClientVersion { get; }
/// <summary>获取当前运行模式。</summary>
public string OperatingMode { get; }
/// <summary>获取当前初始化阶段。</summary>
public string InitializationStage { get; }
/// <summary>获取最近尝试连接的机器人 IP。</summary>
public string? RobotIp { get; }
/// <summary>获取最后一次真机连接或使能错误。</summary>
public string? LastConnectionError { get; }
/// <summary>获取最后一次真机连接或使能错误发生时间。</summary>
public DateTimeOffset? LastConnectionErrorAt { get; }
/// <summary>获取最后一次初始化尝试时间。</summary>
public DateTimeOffset? LastInitializationAttemptAt { get; }
/// <summary>获取当前是否允许离线规划。</summary>
public bool CanPlanOffline { get; }
/// <summary>获取当前是否允许真机执行。</summary>
public bool CanExecuteRealMotion { get; }
} }

View File

@@ -34,14 +34,14 @@ public sealed class JsonFlyshotTrajectoryStore
/// <param name="robotName">当前已初始化的机器人名称,仅用于日志诊断。</param> /// <param name="robotName">当前已初始化的机器人名称,仅用于日志诊断。</param>
/// <param name="settings">当前机器人级兼容配置。</param> /// <param name="settings">当前机器人级兼容配置。</param>
/// <param name="trajectory">要保存的已上传轨迹。</param> /// <param name="trajectory">要保存的已上传轨迹。</param>
public void Save(string robotName, CompatibilityRobotSettings settings, ControllerClientCompatUploadedTrajectory trajectory) public void Save(CompatibilityRobotSettings settings, ControllerClientCompatUploadedTrajectory trajectory)
{ {
ArgumentNullException.ThrowIfNull(settings); ArgumentNullException.ThrowIfNull(settings);
ArgumentNullException.ThrowIfNull(trajectory); ArgumentNullException.ThrowIfNull(trajectory);
_logger?.LogInformation( _logger?.LogInformation(
"RobotConfig 保存轨迹: robot={RobotName}, name={TrajectoryName}, waypoints={WaypointCount}", "RobotConfig 保存轨迹: robot={RobotName}, name={TrajectoryName}, waypoints={WaypointCount}",
robotName, "fixed-config",
trajectory.Name, trajectory.Name,
trajectory.Waypoints.Count); trajectory.Waypoints.Count);
@@ -82,19 +82,30 @@ public sealed class JsonFlyshotTrajectoryStore
_logger?.LogInformation("RobotConfig 轨迹已保存到 {Path}", path); _logger?.LogInformation("RobotConfig 轨迹已保存到 {Path}", path);
} }
/// <summary>
/// 兼容旧调用签名;机器人名称不再参与固定配置文件分区。
/// </summary>
/// <param name="robotName">仅用于兼容的机器人名称。</param>
/// <param name="settings">机器人级兼容配置。</param>
/// <param name="trajectory">待保存轨迹。</param>
public void Save(string robotName, CompatibilityRobotSettings settings, ControllerClientCompatUploadedTrajectory trajectory)
{
Save(settings, trajectory);
}
/// <summary> /// <summary>
/// 从统一 RobotConfig.json 删除指定名称的轨迹。 /// 从统一 RobotConfig.json 删除指定名称的轨迹。
/// </summary> /// </summary>
/// <param name="robotName">当前已初始化的机器人名称,仅用于日志诊断。</param> /// <param name="robotName">当前已初始化的机器人名称,仅用于日志诊断。</param>
/// <param name="trajectoryName">要删除的轨迹名称。</param> /// <param name="trajectoryName">要删除的轨迹名称。</param>
public void Delete(string robotName, string trajectoryName) public void Delete(string trajectoryName)
{ {
if (string.IsNullOrWhiteSpace(trajectoryName)) if (string.IsNullOrWhiteSpace(trajectoryName))
{ {
throw new ArgumentException("轨迹名称不能为空。", nameof(trajectoryName)); throw new ArgumentException("轨迹名称不能为空。", nameof(trajectoryName));
} }
_logger?.LogInformation("RobotConfig 删除轨迹: robot={RobotName}, name={TrajectoryName}", robotName, trajectoryName); _logger?.LogInformation("RobotConfig 删除轨迹: name={TrajectoryName}", trajectoryName);
var path = ResolveStorePath(); var path = ResolveStorePath();
if (!File.Exists(path)) if (!File.Exists(path))
@@ -131,13 +142,23 @@ public sealed class JsonFlyshotTrajectoryStore
} }
} }
/// <summary>
/// 兼容旧调用签名;机器人名称不再参与固定配置文件分区。
/// </summary>
/// <param name="robotName">仅用于兼容的机器人名称。</param>
/// <param name="trajectoryName">待删除轨迹名称。</param>
public void Delete(string robotName, string trajectoryName)
{
Delete(trajectoryName);
}
/// <summary> /// <summary>
/// 加载统一 RobotConfig.json 中的所有轨迹,并回传机器人配置。 /// 加载统一 RobotConfig.json 中的所有轨迹,并回传机器人配置。
/// </summary> /// </summary>
/// <param name="robotName">当前已初始化的机器人名称,仅用于日志诊断。</param> /// <param name="robotName">当前已初始化的机器人名称,仅用于日志诊断。</param>
/// <param name="settings">输出 RobotConfig.json 中的机器人配置;若文件不存在或解析失败则为 null。</param> /// <param name="settings">输出 RobotConfig.json 中的机器人配置;若文件不存在或解析失败则为 null。</param>
/// <returns>按轨迹名称索引的已上传轨迹集合。</returns> /// <returns>按轨迹名称索引的已上传轨迹集合。</returns>
public IReadOnlyDictionary<string, ControllerClientCompatUploadedTrajectory> LoadAll(string robotName, out CompatibilityRobotSettings? settings) public IReadOnlyDictionary<string, ControllerClientCompatUploadedTrajectory> LoadAll(out CompatibilityRobotSettings? settings)
{ {
var path = ResolveStorePath(); var path = ResolveStorePath();
if (!File.Exists(path)) if (!File.Exists(path))
@@ -168,7 +189,7 @@ public sealed class JsonFlyshotTrajectoryStore
_logger?.LogInformation( _logger?.LogInformation(
"RobotConfig 加载完成: robot={RobotName}, 轨迹数={Count}, useDo={UseDo}, ioKeepCycles={IoKeepCycles}", "RobotConfig 加载完成: robot={RobotName}, 轨迹数={Count}, useDo={UseDo}, ioKeepCycles={IoKeepCycles}",
robotName, "fixed-config",
dict.Count, dict.Count,
settings?.UseDo, settings?.UseDo,
settings?.IoKeepCycles); settings?.IoKeepCycles);
@@ -183,6 +204,19 @@ public sealed class JsonFlyshotTrajectoryStore
} }
} }
/// <summary>
/// 兼容旧调用签名;机器人名称不再参与固定配置文件分区。
/// </summary>
/// <param name="robotName">仅用于兼容的机器人名称。</param>
/// <param name="settings">输出机器人配置。</param>
/// <returns>固定 RobotConfig.json 中的全部飞拍资产。</returns>
public IReadOnlyDictionary<string, ControllerClientCompatUploadedTrajectory> LoadAll(
string robotName,
out CompatibilityRobotSettings? settings)
{
return LoadAll(out settings);
}
/// <summary> /// <summary>
/// 把机器人兼容配置序列化为 JSON 对象,字段名与旧版 RobotConfig.json 一致。 /// 把机器人兼容配置序列化为 JSON 对象,字段名与旧版 RobotConfig.json 一致。
/// </summary> /// </summary>

View File

@@ -17,25 +17,6 @@ public interface IControllerClientCompatService
/// </summary> /// </summary>
bool IsSetUp { get; } bool IsSetUp { get; }
/// <summary>
/// 保存当前调用方期望连接的 replacement 服务端地址。
/// </summary>
/// <param name="serverIp">客户端传入的服务端 IP。</param>
/// <param name="port">客户端传入的服务端端口。</param>
void ConnectServer(string serverIp, int port);
/// <summary>
/// 获取兼容服务端版本号。
/// </summary>
/// <returns>服务端版本号。</returns>
string GetServerVersion();
/// <summary>
/// 获取兼容客户端版本号。
/// </summary>
/// <returns>客户端版本号。</returns>
string GetClientVersion();
/// <summary> /// <summary>
/// 根据旧客户端使用的机器人名称完成机器人初始化。 /// 根据旧客户端使用的机器人名称完成机器人初始化。
/// </summary> /// </summary>
@@ -43,24 +24,12 @@ public interface IControllerClientCompatService
void SetUpRobot(string robotName); void SetUpRobot(string robotName);
/// <summary> /// <summary>
/// 根据旧客户端传入的环境文件完成机器人初始化。 /// 按“加载模型、连接真机、使能机器人”的固定顺序执行初始化。
/// </summary> /// </summary>
/// <param name="envFile">环境文件路径。</param> /// <param name="robotName">机器人名称。</param>
void SetUpRobotFromEnv(string envFile); /// <param name="robotIp">机器人控制器 IP。</param>
/// <param name="bufferSize">J519 发送缓冲区大小。</param>
/// <summary> void InitializeRobot(string robotName, string robotIp, int bufferSize = 8);
/// 设置是否显示 TCP 坐标轴。
/// </summary>
/// <param name="isShow">是否显示 TCP。</param>
/// <param name="axisLength">坐标轴长度。</param>
/// <param name="axisSize">坐标轴线宽。</param>
void SetShowTcp(bool isShow, double axisLength, int axisSize);
/// <summary>
/// 记录当前激活的控制器类型。
/// </summary>
/// <param name="sim">是否为仿真控制器。</param>
void SetActiveController(bool sim);
/// <summary> /// <summary>
/// 记录当前控制器已经建立连接。 /// 记录当前控制器已经建立连接。
@@ -206,7 +175,7 @@ public interface IControllerClientCompatService
/// </summary> /// </summary>
/// <param name="name">轨迹名称。</param> /// <param name="name">轨迹名称。</param>
/// <param name="method">轨迹生成方法。</param> /// <param name="method">轨迹生成方法。</param>
void SaveTrajectoryInfo(string name, string method = "icsp"); OfflineFlyshotPlanningResult SaveTrajectoryInfo(string name, string method = "icsp");
/// <summary> /// <summary>
/// 检查指定飞拍轨迹是否可执行。 /// 检查指定飞拍轨迹是否可执行。

View File

@@ -0,0 +1,174 @@
using Flyshot.Core.Domain;
namespace Flyshot.ControllerClientCompat;
/// <summary>
/// 表示离线飞拍规划返回给 HTTP、Debug 页面和导出链路的完整结果。
/// </summary>
public sealed class OfflineFlyshotPlanningResult
{
/// <summary>
/// 初始化一份离线飞拍规划结果。
/// </summary>
/// <param name="trajectoryName">轨迹名称。</param>
/// <param name="method">规划方法。</param>
/// <param name="isValid">规划结果是否有效。</param>
/// <param name="planningDurationSeconds">连续规划轨迹时长,单位为秒。</param>
/// <param name="finalSendDurationSeconds">最终发送时长,单位为秒。</param>
/// <param name="originalWaypointCount">原始示教路点数量。</param>
/// <param name="plannedWaypointCount">规划关键路点数量。</param>
/// <param name="densePointCount">稠密轨迹点数量。</param>
/// <param name="speedRatio">本次最终发送使用的速度倍率。</param>
/// <param name="servoPeriodSeconds">最终发送采样周期,单位为秒。</param>
/// <param name="plannedJointTrajectory">关键关节轨迹,每行为时间加弧度制关节值。</param>
/// <param name="denseJointTrajectory">稠密关节轨迹,每行为时间加弧度制关节值。</param>
/// <param name="cartesianTrajectory">稠密笛卡尔轨迹。</param>
/// <param name="finalSendPoints">最终 8ms 发送点列。</param>
/// <param name="shotEvents">理论拍照事件。</param>
/// <param name="triggerBindings">触发事件与最终发送点的绑定结果。</param>
/// <param name="artifactPaths">导出文件名与绝对路径。</param>
/// <param name="diagnostics">最终发送点列的速度、加速度和跃度统计。</param>
public OfflineFlyshotPlanningResult(
string trajectoryName,
string method,
bool isValid,
double planningDurationSeconds,
double finalSendDurationSeconds,
int originalWaypointCount,
int plannedWaypointCount,
int densePointCount,
double speedRatio,
double servoPeriodSeconds,
IEnumerable<IReadOnlyList<double>> plannedJointTrajectory,
IEnumerable<IReadOnlyList<double>> denseJointTrajectory,
IEnumerable<IReadOnlyList<double>> cartesianTrajectory,
IEnumerable<OfflineFlyshotSendPoint> finalSendPoints,
IEnumerable<ShotEvent> shotEvents,
IEnumerable<OfflineFlyshotTriggerBinding> triggerBindings,
IReadOnlyDictionary<string, string> artifactPaths,
OfflineFlyshotDiagnostics diagnostics)
{
TrajectoryName = trajectoryName;
Method = method;
IsValid = isValid;
PlanningDurationSeconds = planningDurationSeconds;
FinalSendDurationSeconds = finalSendDurationSeconds;
OriginalWaypointCount = originalWaypointCount;
PlannedWaypointCount = plannedWaypointCount;
DensePointCount = densePointCount;
SpeedRatio = speedRatio;
ServoPeriodSeconds = servoPeriodSeconds;
PlannedJointTrajectory = plannedJointTrajectory.Select(static row => row.ToArray()).ToArray();
DenseJointTrajectory = denseJointTrajectory.Select(static row => row.ToArray()).ToArray();
CartesianTrajectory = cartesianTrajectory.Select(static row => row.ToArray()).ToArray();
FinalSendPoints = finalSendPoints.ToArray();
ShotEvents = shotEvents.ToArray();
TriggerBindings = triggerBindings.ToArray();
ArtifactPaths = new Dictionary<string, string>(artifactPaths, StringComparer.Ordinal);
Diagnostics = diagnostics;
}
/// <summary>获取轨迹名称。</summary>
public string TrajectoryName { get; }
/// <summary>获取规划方法。</summary>
public string Method { get; }
/// <summary>获取规划结果是否有效。</summary>
public bool IsValid { get; }
/// <summary>获取连续规划轨迹时长,单位为秒。</summary>
public double PlanningDurationSeconds { get; }
/// <summary>获取最终发送时长,单位为秒。</summary>
public double FinalSendDurationSeconds { get; }
/// <summary>获取原始示教路点数量。</summary>
public int OriginalWaypointCount { get; }
/// <summary>获取规划关键路点数量。</summary>
public int PlannedWaypointCount { get; }
/// <summary>获取稠密轨迹点数量。</summary>
public int DensePointCount { get; }
/// <summary>获取最终发送点数量。</summary>
public int FinalSendPointCount => FinalSendPoints.Count;
/// <summary>获取本次最终发送使用的速度倍率。</summary>
public double SpeedRatio { get; }
/// <summary>获取最终发送采样周期,单位为秒。</summary>
public double ServoPeriodSeconds { get; }
/// <summary>获取关键关节轨迹。</summary>
public IReadOnlyList<IReadOnlyList<double>> PlannedJointTrajectory { get; }
/// <summary>获取稠密关节轨迹。</summary>
public IReadOnlyList<IReadOnlyList<double>> DenseJointTrajectory { get; }
/// <summary>获取稠密笛卡尔轨迹。</summary>
public IReadOnlyList<IReadOnlyList<double>> CartesianTrajectory { get; }
/// <summary>获取最终 8ms 发送点列。</summary>
public IReadOnlyList<OfflineFlyshotSendPoint> FinalSendPoints { get; }
/// <summary>获取理论拍照事件。</summary>
public IReadOnlyList<ShotEvent> ShotEvents { get; }
/// <summary>获取触发事件与最终发送点的绑定结果。</summary>
public IReadOnlyList<OfflineFlyshotTriggerBinding> TriggerBindings { get; }
/// <summary>获取导出文件名与绝对路径。</summary>
public IReadOnlyDictionary<string, string> ArtifactPaths { get; }
/// <summary>获取最终发送点列的速度、加速度和跃度统计。</summary>
public OfflineFlyshotDiagnostics Diagnostics { get; }
}
/// <summary>
/// 表示离线规划结果中的单个最终 J519 发送点。
/// </summary>
/// <param name="SampleIndex">发送周期序号。</param>
/// <param name="SendTime">物理发送时间,单位为秒。</param>
/// <param name="TrajectoryTime">映射到连续轨迹的采样时间,单位为秒。</param>
/// <param name="JointsDegrees">角度制关节目标。</param>
/// <param name="IoMask">该点写出的 IO 掩码。</param>
/// <param name="IoValue">该点写出的 IO 值。</param>
public sealed record OfflineFlyshotSendPoint(
long SampleIndex,
double SendTime,
double TrajectoryTime,
IReadOnlyList<double> JointsDegrees,
ushort IoMask,
ushort IoValue);
/// <summary>
/// 表示离线规划结果中的触发采样点绑定。
/// </summary>
/// <param name="WaypointIndex">原始示教路点索引。</param>
/// <param name="TriggerTime">理论触发时间,单位为秒。</param>
/// <param name="SampleIndex">最终发送点数组索引。</param>
/// <param name="SampleSequence">最终发送序号。</param>
/// <param name="FoundInWindow">是否在理论搜索窗口内完成绑定。</param>
/// <param name="IoMask">触发 IO 掩码。</param>
/// <param name="IoValue">触发 IO 值。</param>
public sealed record OfflineFlyshotTriggerBinding(
int WaypointIndex,
double TriggerTime,
int SampleIndex,
long SampleSequence,
bool FoundInWindow,
ushort IoMask,
ushort IoValue);
/// <summary>
/// 表示最终发送点列的离散运动学诊断统计。
/// </summary>
/// <param name="MaxAbsVelocityDegreesPerSecond">最大绝对速度,单位 deg/s。</param>
/// <param name="MaxAbsAccelerationDegreesPerSecondSquared">最大绝对加速度,单位 deg/s²。</param>
/// <param name="MaxAbsJerkDegreesPerSecondCubed">最大绝对跃度,单位 deg/s³。</param>
public sealed record OfflineFlyshotDiagnostics(
double MaxAbsVelocityDegreesPerSecond,
double MaxAbsAccelerationDegreesPerSecondSquared,
double MaxAbsJerkDegreesPerSecondCubed);

View File

@@ -81,7 +81,7 @@ public sealed class CompatibilityRobotSettings
} }
/// <summary> /// <summary>
/// 获取是否启用伺服同步 IO。 /// 获取飞拍同步输出类型选择:true 使用 DO,false 使用 RO;两者都启用触发。
/// </summary> /// </summary>
public bool UseDo { get; } public bool UseDo { get; }

View File

@@ -174,20 +174,28 @@ public sealed class TrajectoryResult
} }
/// <summary> /// <summary>
/// Describes a trigger event that the runtime must inject into the servo timeline. /// 表示运行时注入伺服时间轴的 DO 或 RO 输出触发事件。
/// </summary> /// </summary>
public sealed class TrajectoryDoEvent public sealed class TrajectoryDoEvent
{ {
/// <summary> /// <summary>
/// Initializes a validated runtime trigger event. /// 初始化经过校验的伺服同步输出触发事件。
/// </summary> /// </summary>
/// <param name="waypointIndex">请求拍照的原始示教点索引,从零开始。</param>
/// <param name="triggerTime">离散化之前的理论触发时间,单位秒,必须非负。</param>
/// <param name="offsetCycles">相对于示教点的触发偏移周期数。</param>
/// <param name="holdCycles">输出保持的伺服周期数,必须非负。</param>
/// <param name="addressGroup">要输出的 IO 点位组,不允许为空引用。</param>
/// <param name="referenceJointsDegrees">用于选择最近发送采样点的关节角,单位度;允许 null。</param>
/// <param name="useDo">true 使用 DO,false 使用 RO;默认 true 保留已有事件的 DO 行为。</param>
public TrajectoryDoEvent( public TrajectoryDoEvent(
int waypointIndex, int waypointIndex,
double triggerTime, double triggerTime,
int offsetCycles, int offsetCycles,
int holdCycles, int holdCycles,
IoAddressGroup addressGroup, IoAddressGroup addressGroup,
IEnumerable<double>? referenceJointsDegrees = null) IEnumerable<double>? referenceJointsDegrees = null,
bool useDo = true)
{ {
if (waypointIndex < 0) if (waypointIndex < 0)
{ {
@@ -210,6 +218,7 @@ public sealed class TrajectoryDoEvent
HoldCycles = holdCycles; HoldCycles = holdCycles;
AddressGroup = addressGroup ?? throw new ArgumentNullException(nameof(addressGroup)); AddressGroup = addressGroup ?? throw new ArgumentNullException(nameof(addressGroup));
ReferenceJointsDegrees = referenceJointsDegrees?.ToArray(); ReferenceJointsDegrees = referenceJointsDegrees?.ToArray();
UseDo = useDo;
} }
/// <summary> /// <summary>
@@ -248,6 +257,12 @@ public sealed class TrajectoryDoEvent
/// </summary> /// </summary>
[JsonPropertyName("referenceJointsDegrees")] [JsonPropertyName("referenceJointsDegrees")]
public IReadOnlyList<double>? ReferenceJointsDegrees { get; } public IReadOnlyList<double>? ReferenceJointsDegrees { get; }
/// <summary>
/// 获取飞拍输出选择:true 为 DO,false 为 RO;仅供运行时使用。
/// </summary>
[JsonIgnore]
public bool UseDo { get; }
} }
/// <summary> /// <summary>

View File

@@ -39,7 +39,8 @@ public static class FlyshotExecutionSendSequenceBuilder
throw new ArgumentOutOfRangeException(nameof(servoPeriodSeconds), "伺服周期必须是有限正数。"); throw new ArgumentOutOfRangeException(nameof(servoPeriodSeconds), "伺服周期必须是有限正数。");
} }
var finalSpeedRatio = samplingSpeedRatio ?? 1.0; // 未显式指定独立采样倍率时,最终发送队列必须原样使用请求倍率。
var finalSpeedRatio = samplingSpeedRatio ?? requestedSpeedRatio;
if (finalSpeedRatio <= 0.0 || double.IsNaN(finalSpeedRatio) || double.IsInfinity(finalSpeedRatio)) if (finalSpeedRatio <= 0.0 || double.IsNaN(finalSpeedRatio) || double.IsInfinity(finalSpeedRatio))
{ {
throw new ArgumentOutOfRangeException(nameof(samplingSpeedRatio), "最终队列采样倍率必须是有限正数。"); throw new ArgumentOutOfRangeException(nameof(samplingSpeedRatio), "最终队列采样倍率必须是有限正数。");
@@ -56,7 +57,7 @@ public static class FlyshotExecutionSendSequenceBuilder
robot, robot,
samples, samples,
trajectoryName: result.ProgramName, trajectoryName: result.ProgramName,
validateJerk: false, validateJerk: true,
strictJerkTolerance: true); strictJerkTolerance: true);
} }
catch (InvalidOperationException exception) catch (InvalidOperationException exception)

View File

@@ -12,7 +12,7 @@ namespace Flyshot.Core.Triggering;
/// 本构建器只负责“时间轴映射”和“事件打包”,不负责轨迹规划与运行时发送。 /// 本构建器只负责“时间轴映射”和“事件打包”,不负责轨迹规划与运行时发送。
/// 其中: /// 其中:
/// - ShotEvent 用于诊断、导出和日志观察; /// - ShotEvent 用于诊断、导出和日志观察;
/// - TrajectoryDoEvent 用于运行时伺服链路注入 DO 脉冲。 /// - TrajectoryDoEvent 用于运行时伺服链路注入 DO 或 RO 脉冲。
/// </remarks> /// </remarks>
public sealed class ShotTimelineBuilder public sealed class ShotTimelineBuilder
{ {
@@ -43,7 +43,7 @@ public sealed class ShotTimelineBuilder
/// <param name="trajectory">规划后的轨迹(含补中点信息和机器人配置)。</param> /// <param name="trajectory">规划后的轨迹(含补中点信息和机器人配置)。</param>
/// <param name="holdCycles">IO 保持周期数(对应原系统的 io_keep_cycles)。</param> /// <param name="holdCycles">IO 保持周期数(对应原系统的 io_keep_cycles)。</param>
/// <param name="samplePeriod">稠密采样周期,用于离散化 sample_index 和 sample_time。</param> /// <param name="samplePeriod">稠密采样周期,用于离散化 sample_index 和 sample_time。</param>
/// <param name="useDo">是否生成可注入伺服流的 DO 事件。</param> /// <param name="useDo">输出类型选择:true 使用 DO,false 使用 RO;两者都生成触发事件。</param>
/// <returns>包含 ShotEvent 和 TrajectoryDoEvent 的触发时间轴。</returns> /// <returns>包含 ShotEvent 和 TrajectoryDoEvent 的触发时间轴。</returns>
/// <exception cref="ArgumentNullException">当 <paramref name="trajectory"/> 为 null 时抛出。</exception> /// <exception cref="ArgumentNullException">当 <paramref name="trajectory"/> 为 null 时抛出。</exception>
/// <exception cref="ArgumentOutOfRangeException"> /// <exception cref="ArgumentOutOfRangeException">
@@ -101,17 +101,15 @@ public sealed class ShotTimelineBuilder
sampleTime: sampleTime, sampleTime: sampleTime,
addressGroup: addressGroup)); addressGroup: addressGroup));
if (useDo) // use_do 只选择输出类型;RO 模式也必须保留拍照脉冲与保持周期。
{
// use_do=false 时保留 ShotEvent 诊断信息,但不向运行时下发 IO 脉冲。
triggerTimeline.Add(new TrajectoryDoEvent( triggerTimeline.Add(new TrajectoryDoEvent(
waypointIndex: i, waypointIndex: i,
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))); referenceJointsDegrees: program.Waypoints[i].Positions.Select(static radians => radians * 180.0 / Math.PI),
} useDo: useDo));
} }
_logger?.LogInformation( _logger?.LogInformation(

View File

@@ -14,12 +14,6 @@ public interface IControllerRuntime
/// <param name="robotName">兼容层传入的机器人名称。</param> /// <param name="robotName">兼容层传入的机器人名称。</param>
void ResetRobot(RobotProfile robot, string robotName); void ResetRobot(RobotProfile robot, string robotName);
/// <summary>
/// 选择当前活动控制器类型。
/// </summary>
/// <param name="sim">是否使用仿真控制器。</param>
void SetActiveController(bool sim);
/// <summary> /// <summary>
/// 建立到控制器 IP 的连接。 /// 建立到控制器 IP 的连接。
/// </summary> /// </summary>

View File

@@ -12,7 +12,6 @@ namespace Flyshot.Runtime.Fanuc;
/// <summary> /// <summary>
/// FANUC 控制器运行时,将上层兼容层指令转换为三条真实 Socket 通道的交互。 /// FANUC 控制器运行时,将上层兼容层指令转换为三条真实 Socket 通道的交互。
/// 仿真模式下仍保持内存桩行为,便于离线测试与回退。
/// </summary> /// </summary>
public sealed class FanucControllerRuntime : IControllerRuntime, IDisposable public sealed class FanucControllerRuntime : IControllerRuntime, IDisposable
{ {
@@ -25,7 +24,6 @@ public sealed class FanucControllerRuntime : IControllerRuntime, IDisposable
private RobotProfile? _robot; private RobotProfile? _robot;
private string? _robotName; private string? _robotName;
private bool? _activeControllerIsSimulation;
private string? _connectedRobotIp; private string? _connectedRobotIp;
private bool _isEnabled; private bool _isEnabled;
private bool _isInMotion; private bool _isInMotion;
@@ -41,23 +39,49 @@ public sealed class FanucControllerRuntime : IControllerRuntime, IDisposable
/// 初始化 FANUC 控制器运行时。 /// 初始化 FANUC 控制器运行时。
/// </summary> /// </summary>
/// <param name="logger">运行时日志记录器。</param> /// <param name="logger">运行时日志记录器。</param>
/// <param name="commandLogger">FANUC 10012 命令通道客户端日志记录器。</param>
/// <param name="stateLogger">FANUC 10010 状态通道客户端日志记录器。</param>
/// <param name="j519Logger">J519 客户端日志记录器。</param> /// <param name="j519Logger">J519 客户端日志记录器。</param>
public FanucControllerRuntime(ILogger<FanucControllerRuntime> logger, ILogger<FanucJ519Client> j519Logger) public FanucControllerRuntime(
ILogger<FanucControllerRuntime> logger,
ILogger<FanucCommandClient> commandLogger,
ILogger<FanucStateClient> stateLogger,
ILogger<FanucJ519Client> j519Logger)
{ {
ArgumentNullException.ThrowIfNull(logger); ArgumentNullException.ThrowIfNull(logger);
ArgumentNullException.ThrowIfNull(commandLogger);
ArgumentNullException.ThrowIfNull(stateLogger);
ArgumentNullException.ThrowIfNull(j519Logger); ArgumentNullException.ThrowIfNull(j519Logger);
_commandClient = new FanucCommandClient(); _commandClient = new FanucCommandClient(commandLogger);
_stateClient = new FanucStateClient(); _stateClient = new FanucStateClient(new FanucStateClientOptions(), stateLogger);
_j519Client = new FanucJ519Client(j519Logger); _j519Client = new FanucJ519Client(j519Logger);
_logger = logger; _logger = logger;
} }
/// <summary>
/// 初始化 FANUC 控制器运行时,并仅显式传入运行时与 J519 日志记录器。
/// </summary>
/// <param name="logger">运行时日志记录器。</param>
/// <param name="j519Logger">J519 客户端日志记录器。</param>
public FanucControllerRuntime(ILogger<FanucControllerRuntime> logger, ILogger<FanucJ519Client> j519Logger)
: this(
logger,
NullLogger<FanucCommandClient>.Instance,
NullLogger<FanucStateClient>.Instance,
j519Logger)
{
}
/// <summary> /// <summary>
/// 初始化一份无日志的 FANUC 控制器运行时,供离线测试或手工构造场景使用。 /// 初始化一份无日志的 FANUC 控制器运行时,供离线测试或手工构造场景使用。
/// </summary> /// </summary>
public FanucControllerRuntime() public FanucControllerRuntime()
: this(NullLogger<FanucControllerRuntime>.Instance, NullLogger<FanucJ519Client>.Instance) : this(
NullLogger<FanucControllerRuntime>.Instance,
NullLogger<FanucCommandClient>.Instance,
NullLogger<FanucStateClient>.Instance,
NullLogger<FanucJ519Client>.Instance)
{ {
} }
@@ -88,7 +112,6 @@ public sealed class FanucControllerRuntime : IControllerRuntime, IDisposable
DisconnectClients(); DisconnectClients();
_robot = robot; _robot = robot;
_robotName = robotName; _robotName = robotName;
_activeControllerIsSimulation = null;
_connectedRobotIp = null; _connectedRobotIp = null;
_isEnabled = false; _isEnabled = false;
_isInMotion = false; _isInMotion = false;
@@ -101,21 +124,6 @@ public sealed class FanucControllerRuntime : IControllerRuntime, IDisposable
} }
} }
/// <inheritdoc />
public void SetActiveController(bool sim)
{
_logger?.LogInformation("SetActiveController: sim={Sim}", sim);
lock (_stateLock)
{
EnsureRobotSetup();
DisconnectClients();
_activeControllerIsSimulation = sim;
_connectedRobotIp = null;
_isEnabled = false;
_isInMotion = false;
}
}
/// <inheritdoc /> /// <inheritdoc />
public void Connect(string robotIp) public void Connect(string robotIp)
{ {
@@ -124,21 +132,12 @@ public sealed class FanucControllerRuntime : IControllerRuntime, IDisposable
throw new ArgumentException("控制器 IP 不能为空。", nameof(robotIp)); throw new ArgumentException("控制器 IP 不能为空。", nameof(robotIp));
} }
_logger?.LogInformation("Connect 开始: robotIp={RobotIp}, 仿真={IsSim}", robotIp, _activeControllerIsSimulation); _logger?.LogInformation("Connect 开始: robotIp={RobotIp}", robotIp);
lock (_stateLock) lock (_stateLock)
{ {
EnsureActiveControllerSelected(); EnsureRobotSetup();
if (_activeControllerIsSimulation!.Value) // 顺序建立三条真机通道:状态、命令、运动。
{
_connectedRobotIp = robotIp;
_isEnabled = false;
_isInMotion = false;
_logger?.LogInformation("Connect 完成(仿真): robotIp={RobotIp}", robotIp);
return;
}
// 真机模式:顺序建立三条通道 — 状态 → 命令 → 运动。
_stateClient.ConnectAsync(robotIp, 10010).GetAwaiter().GetResult(); _stateClient.ConnectAsync(robotIp, 10010).GetAwaiter().GetResult();
_commandClient.ConnectAsync(robotIp, 10012).GetAwaiter().GetResult(); _commandClient.ConnectAsync(robotIp, 10012).GetAwaiter().GetResult();
_j519Client.ConnectAsync(robotIp, 60015).GetAwaiter().GetResult(); _j519Client.ConnectAsync(robotIp, 60015).GetAwaiter().GetResult();
@@ -175,7 +174,7 @@ public sealed class FanucControllerRuntime : IControllerRuntime, IDisposable
throw new ArgumentOutOfRangeException(nameof(bufferSize), "buffer_size 必须大于 0。"); throw new ArgumentOutOfRangeException(nameof(bufferSize), "buffer_size 必须大于 0。");
} }
_logger?.LogInformation("EnableRobot 开始: bufferSize={BufferSize}, 仿真={IsSim}", bufferSize, _activeControllerIsSimulation); _logger?.LogInformation("EnableRobot 开始: bufferSize={BufferSize}", bufferSize);
lock (_stateLock) lock (_stateLock)
{ {
@@ -183,14 +182,7 @@ public sealed class FanucControllerRuntime : IControllerRuntime, IDisposable
_bufferSize = bufferSize; _bufferSize = bufferSize;
_j519Client.SetSequenceBufferSize(bufferSize); _j519Client.SetSequenceBufferSize(bufferSize);
if (IsSimulationMode) // 按 all-reconnect.pcap 的重连序列启动 RVBUSTSM。
{
_isEnabled = true;
_logger?.LogInformation("EnableRobot 完成(仿真)");
return;
}
// 真机模式:按 all-reconnect.pcap 的重连序列启动 RVBUSTSM,暂不发送 StopProg。
_commandClient.StopProgramAsync("RVBUSTSM").GetAwaiter().GetResult(); _commandClient.StopProgramAsync("RVBUSTSM").GetAwaiter().GetResult();
_commandClient.ResetRobotAsync().GetAwaiter().GetResult(); _commandClient.ResetRobotAsync().GetAwaiter().GetResult();
//_commandClient.GetProgramStatusAsync("RVBUSTSM").GetAwaiter().GetResult(); //_commandClient.GetProgramStatusAsync("RVBUSTSM").GetAwaiter().GetResult();
@@ -211,11 +203,8 @@ public sealed class FanucControllerRuntime : IControllerRuntime, IDisposable
{ {
EnsureRobotSetup(); EnsureRobotSetup();
CancelSendTaskLocked(); CancelSendTaskLocked();
if (!IsSimulationMode)
{
_j519Client.StopMotionAsync().GetAwaiter().GetResult(); _j519Client.StopMotionAsync().GetAwaiter().GetResult();
_commandClient.StopProgramAsync("RVBUSTSM").GetAwaiter().GetResult(); _commandClient.StopProgramAsync("RVBUSTSM").GetAwaiter().GetResult();
}
_isEnabled = false; _isEnabled = false;
_isInMotion = false; _isInMotion = false;
@@ -231,10 +220,7 @@ public sealed class FanucControllerRuntime : IControllerRuntime, IDisposable
{ {
EnsureRobotSetup(); EnsureRobotSetup();
CancelSendTaskLocked(); CancelSendTaskLocked();
if (!IsSimulationMode)
{
_j519Client.StopMotionAsync().GetAwaiter().GetResult(); _j519Client.StopMotionAsync().GetAwaiter().GetResult();
}
_isInMotion = false; _isInMotion = false;
} }
@@ -247,11 +233,8 @@ public sealed class FanucControllerRuntime : IControllerRuntime, IDisposable
lock (_stateLock) lock (_stateLock)
{ {
EnsureConnected(); EnsureConnected();
if (!IsSimulationMode)
{
var response = _commandClient.GetSpeedRatioAsync().GetAwaiter().GetResult(); var response = _commandClient.GetSpeedRatioAsync().GetAwaiter().GetResult();
_speedRatio = response.Ratio; _speedRatio = response.Ratio;
}
return _speedRatio; return _speedRatio;
} }
@@ -271,11 +254,6 @@ public sealed class FanucControllerRuntime : IControllerRuntime, IDisposable
{ {
//EnsureConnected(); //EnsureConnected();
var clampedRatio = Math.Clamp(ratio, 0.0, 1.0); var clampedRatio = Math.Clamp(ratio, 0.0, 1.0);
if (!IsSimulationMode)
{
//_commandClient.SetSpeedRatioAsync(clampedRatio).GetAwaiter().GetResult();
}
_speedRatio = clampedRatio; _speedRatio = clampedRatio;
} }
@@ -288,7 +266,7 @@ public sealed class FanucControllerRuntime : IControllerRuntime, IDisposable
lock (_stateLock) lock (_stateLock)
{ {
EnsureRobotSetup(); EnsureRobotSetup();
if (_activeControllerIsSimulation is false && !string.IsNullOrWhiteSpace(_connectedRobotIp)) if (!string.IsNullOrWhiteSpace(_connectedRobotIp))
{ {
var response = _commandClient.GetTcpAsync(1).GetAwaiter().GetResult(); var response = _commandClient.GetTcpAsync(1).GetAwaiter().GetResult();
_tcp = response.Pose.Take(3).ToArray(); _tcp = response.Pose.Take(3).ToArray();
@@ -304,11 +282,8 @@ public sealed class FanucControllerRuntime : IControllerRuntime, IDisposable
lock (_stateLock) lock (_stateLock)
{ {
EnsureRobotSetup(); EnsureRobotSetup();
if (_activeControllerIsSimulation is false)
{
EnsureConnected(); EnsureConnected();
_commandClient.SetTcpAsync(1, CreateTcpPose(x, y, z)).GetAwaiter().GetResult(); _commandClient.SetTcpAsync(1, CreateTcpPose(x, y, z)).GetAwaiter().GetResult();
}
_tcp = [x, y, z]; _tcp = [x, y, z];
} }
@@ -327,15 +302,10 @@ public sealed class FanucControllerRuntime : IControllerRuntime, IDisposable
lock (_stateLock) lock (_stateLock)
{ {
EnsureConnected(); EnsureConnected();
if (!IsSimulationMode)
{
var response = _commandClient.GetIoAsync(port, normalizedIoType).GetAwaiter().GetResult(); var response = _commandClient.GetIoAsync(port, normalizedIoType).GetAwaiter().GetResult();
_ioValues[(normalizedIoType, port)] = response.Value; _ioValues[(normalizedIoType, port)] = response.Value;
return response.Value; return response.Value;
} }
return _ioValues.TryGetValue((normalizedIoType, port), out var value) && value;
}
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -351,10 +321,7 @@ public sealed class FanucControllerRuntime : IControllerRuntime, IDisposable
lock (_stateLock) lock (_stateLock)
{ {
EnsureEnabled(); EnsureEnabled();
if (!IsSimulationMode)
{
_commandClient.SetIoAsync(port, value, normalizedIoType).GetAwaiter().GetResult(); _commandClient.SetIoAsync(port, value, normalizedIoType).GetAwaiter().GetResult();
}
_ioValues[(normalizedIoType, port)] = value; _ioValues[(normalizedIoType, port)] = value;
} }
@@ -366,7 +333,7 @@ public sealed class FanucControllerRuntime : IControllerRuntime, IDisposable
lock (_stateLock) lock (_stateLock)
{ {
EnsureRobotSetup(); EnsureRobotSetup();
if (!IsSimulationMode) if (!string.IsNullOrWhiteSpace(_connectedRobotIp))
{ {
var frame = GetFreshStateFrame(); var frame = GetFreshStateFrame();
if (frame?.JointRadians.Count >= _jointPositions.Length) if (frame?.JointRadians.Count >= _jointPositions.Length)
@@ -385,7 +352,7 @@ public sealed class FanucControllerRuntime : IControllerRuntime, IDisposable
lock (_stateLock) lock (_stateLock)
{ {
EnsureRobotSetup(); EnsureRobotSetup();
if (!IsSimulationMode) if (!string.IsNullOrWhiteSpace(_connectedRobotIp))
{ {
var frame = GetFreshStateFrame(); var frame = GetFreshStateFrame();
if (frame?.CartesianPose.Count >= 6) if (frame?.CartesianPose.Count >= 6)
@@ -414,7 +381,7 @@ public sealed class FanucControllerRuntime : IControllerRuntime, IDisposable
bool? j519SystemReady = null; bool? j519SystemReady = null;
bool? j519RobotInMotion = null; bool? j519RobotInMotion = null;
if (!IsSimulationMode) if (!string.IsNullOrWhiteSpace(_connectedRobotIp))
{ {
var frame = GetFreshStateFrame(); var frame = GetFreshStateFrame();
if (frame is not null) if (frame is not null)
@@ -485,11 +452,11 @@ public sealed class FanucControllerRuntime : IControllerRuntime, IDisposable
EnsureJointCount(finalJointPositions.Count); EnsureJointCount(finalJointPositions.Count);
CancelSendTaskLocked(); CancelSendTaskLocked();
if (!IsSimulationMode && result.DenseJointTrajectory is not null) if (result.DenseJointTrajectory is not null)
{ {
EnsureJ519ReadyForDenseExecution(); EnsureJ519ReadyForDenseExecution();
// 真机模式且存在稠密路点:准备可被 StopMove 取消的同步发送任务。 // 存在稠密路点时,准备可被 StopMove 取消的同步发送任务。
_isInMotion = true; _isInMotion = true;
_sendCts = new CancellationTokenSource(); _sendCts = new CancellationTokenSource();
denseSendCancellationSource = _sendCts; denseSendCancellationSource = _sendCts;
@@ -500,14 +467,11 @@ public sealed class FanucControllerRuntime : IControllerRuntime, IDisposable
} }
else else
{ {
if (!IsSimulationMode) // 无稠密路点时回退到单点收敛。
{
// 真机模式无稠密路点:回退到单点收敛。
var command = new FanucJ519Command( var command = new FanucJ519Command(
sequence: 0, sequence: 0,
targetJoints: finalJointPositions.Select(j => (double)j).ToArray()); targetJoints: finalJointPositions.Select(j => (double)j).ToArray());
_j519Client.UpdateCommand(command); _j519Client.UpdateCommand(command);
}
_isInMotion = true; _isInMotion = true;
_jointPositions = finalJointPositions.ToArray(); _jointPositions = finalJointPositions.ToArray();
@@ -558,11 +522,11 @@ public sealed class FanucControllerRuntime : IControllerRuntime, IDisposable
EnsurePoseCount(finalPose.Count); EnsurePoseCount(finalPose.Count);
CancelSendTaskLocked(); CancelSendTaskLocked();
if (!IsSimulationMode && result.DenseCartesianTrajectory is not null) if (result.DenseCartesianTrajectory is not null)
{ {
EnsureJ519ReadyForDenseExecution(); EnsureJ519ReadyForDenseExecution();
// 真机直角模式同样预装完整队列,由机器人状态包节拍驱动出队。 // 直角模式同样预装完整队列,由机器人状态包节拍驱动出队。
_isInMotion = true; _isInMotion = true;
_sendCts = new CancellationTokenSource(); _sendCts = new CancellationTokenSource();
denseSendCancellationSource = _sendCts; denseSendCancellationSource = _sendCts;
@@ -572,15 +536,12 @@ public sealed class FanucControllerRuntime : IControllerRuntime, IDisposable
_logger?.LogInformation("ExecuteCartesianTrajectory 开始同步直角稠密发送任务"); _logger?.LogInformation("ExecuteCartesianTrajectory 开始同步直角稠密发送任务");
} }
else else
{
if (!IsSimulationMode)
{ {
var command = new FanucJ519Command( var command = new FanucJ519Command(
sequence: 0, sequence: 0,
targetValues: BuildCartesianTargetValues(finalPose), targetValues: BuildCartesianTargetValues(finalPose),
dataStyle: 0); dataStyle: 0);
_j519Client.UpdateCommand(command); _j519Client.UpdateCommand(command);
}
_isInMotion = true; _isInMotion = true;
_pose = MergeFinalCartesianPose(finalPose); _pose = MergeFinalCartesianPose(finalPose);
@@ -634,6 +595,10 @@ public sealed class FanucControllerRuntime : IControllerRuntime, IDisposable
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;
// 输出类型随触发事件固化,确保脉冲与清零帧都写入同一路 DO/RO。
byte writeIoType = triggers.FirstOrDefault()?.UseDo == false
? (byte)FanucIoTypes.RobotOutput
: (byte)FanucIoTypes.DigitalOutput;
var servoPeriodSeconds = _robot!.ServoPeriod.TotalSeconds; var servoPeriodSeconds = _robot!.ServoPeriod.TotalSeconds;
var preparedExecution = result.PreparedFlyshotExecution; var preparedExecution = result.PreparedFlyshotExecution;
var durationSeconds = preparedExecution?.FinalDurationSeconds ?? result.Duration.TotalSeconds; var durationSeconds = preparedExecution?.FinalDurationSeconds ?? result.Duration.TotalSeconds;
@@ -749,7 +714,7 @@ public sealed class FanucControllerRuntime : IControllerRuntime, IDisposable
var command = new FanucJ519Command( var command = new FanucJ519Command(
sequence: 0, sequence: 0,
targetJoints: sample.JointsDegrees, targetJoints: sample.JointsDegrees,
writeIoType: 2, writeIoType: writeIoType,
writeIoIndex: 1, writeIoIndex: 1,
writeIoMask: ioMask, writeIoMask: ioMask,
writeIoValue: ioValue); writeIoValue: ioValue);
@@ -802,18 +767,20 @@ public sealed class FanucControllerRuntime : IControllerRuntime, IDisposable
sample.TrajectoryTime); sample.TrajectoryTime);
} }
} }
if (_j519Client.EndCommandStreamAfterQueueDrained && commands.Count > 0) //TODO 不进行结束,一致发送最后一帧
{
// 正常结束命令流时追加独立结束帧,避免把最后一个真实轨迹采样点和结束语义混在一起。 // if (_j519Client.EndCommandStreamAfterQueueDrained && commands.Count > 0)
commands.Add(new FanucJ519Command( // {
sequence: 0, // // 正常结束命令流时追加独立结束帧,避免把最后一个真实轨迹采样点和结束语义混在一起。
targetJoints: commands[^1].TargetJoints, // commands.Add(new FanucJ519Command(
lastData: 1, // sequence: 0,
writeIoType: 2, // targetJoints: commands[^1].TargetJoints,
writeIoIndex: 1, // lastData: 1,
writeIoMask: 0, // writeIoType: 2,
writeIoValue: 0)); // writeIoIndex: 1,
} // writeIoMask: 0,
// writeIoValue: 0));
// }
TryWriteDenseSendArtifacts(result, outputDir, sentJointRows, sentTimingRows, sentJerkRows, triggerBindings); TryWriteDenseSendArtifacts(result, outputDir, sentJointRows, sentTimingRows, sentJerkRows, triggerBindings);
// 上层只负责生成完整目标序列,真实出队节拍交给 J519 状态包驱动。 // 上层只负责生成完整目标序列,真实出队节拍交给 J519 状态包驱动。
@@ -1066,7 +1033,7 @@ public sealed class FanucControllerRuntime : IControllerRuntime, IDisposable
} }
/// <summary> /// <summary>
/// 将六维 WPR 直角目标合并回运行时缓存,保留旧 GetPose 仿真路径的 7 维外形。 /// 将六维 WPR 直角目标合并回运行时缓存,保留 GetPose 的 7 维兼容外形。
/// </summary> /// </summary>
private double[] MergeFinalCartesianPose(IReadOnlyList<double> pose) private double[] MergeFinalCartesianPose(IReadOnlyList<double> pose)
{ {
@@ -1179,22 +1146,6 @@ public sealed class FanucControllerRuntime : IControllerRuntime, IDisposable
return value; return value;
} }
/// <summary>
/// 判断当前是否处于仿真模式;若尚未选择控制器则抛出异常。
/// </summary>
private bool IsSimulationMode
{
get
{
if (_activeControllerIsSimulation is null)
{
return false;
}
return _activeControllerIsSimulation.Value;
}
}
/// <summary> /// <summary>
/// 断开所有真实 Socket 通道,不影响内存状态。 /// 断开所有真实 Socket 通道,不影响内存状态。
/// </summary> /// </summary>
@@ -1252,9 +1203,7 @@ public sealed class FanucControllerRuntime : IControllerRuntime, IDisposable
return "Disconnected"; return "Disconnected";
} }
return _activeControllerIsSimulation is false return ResolveRealConnectionState(_stateClient.GetStatus());
? ResolveRealConnectionState(_stateClient.GetStatus())
: "Connected";
} }
/// <summary> /// <summary>
@@ -1327,24 +1276,12 @@ public sealed class FanucControllerRuntime : IControllerRuntime, IDisposable
} }
} }
/// <summary>
/// 校验活动控制器已经被选择。
/// </summary>
private void EnsureActiveControllerSelected()
{
EnsureRobotSetup();
if (_activeControllerIsSimulation is null)
{
throw new InvalidOperationException("Active controller has not been selected.");
}
}
/// <summary> /// <summary>
/// 校验控制器已经建立连接。 /// 校验控制器已经建立连接。
/// </summary> /// </summary>
private void EnsureConnected() private void EnsureConnected()
{ {
EnsureActiveControllerSelected(); EnsureRobotSetup();
if (string.IsNullOrWhiteSpace(_connectedRobotIp)) if (string.IsNullOrWhiteSpace(_connectedRobotIp))
{ {
throw new InvalidOperationException("Controller has not been connected."); throw new InvalidOperationException("Controller has not been connected.");

View File

@@ -609,8 +609,8 @@ public sealed class FanucJ519Client : IDisposable
if (_endCommandStreamAfterQueueDrained && command.LastData == 1) if (_endCommandStreamAfterQueueDrained && command.LastData == 1)
{ {
// LastData 是命令流的正常结束语义;结束后保留状态接收,但不继续回发保姿包。 // LastData 是命令流的正常结束语义;结束后保留状态接收,但不继续回发保姿包。
_monitorOnlyAfterLastData = true; // _monitorOnlyAfterLastData = true;
_motionStarted = false; // _motionStarted = false;
CompleteCommandQueueLocked(); CompleteCommandQueueLocked();
} }
else if (willDrainQueue && _commandQueue is { Count: 0 }) else if (willDrainQueue && _commandQueue is { Count: 0 })

View File

@@ -25,59 +25,6 @@ public sealed class LegacyHttpApiController : ControllerBase
_logger = logger ?? throw new ArgumentNullException(nameof(logger)); _logger = logger ?? throw new ArgumentNullException(nameof(logger));
} }
/// <summary>
/// 兼容旧根路径探活接口。
/// </summary>
/// <returns>旧 HTTP 服务约定的 Hello World 响应。</returns>
[HttpGet("/")]
public IActionResult Root()
{
return Ok(new { message = "Hello World" });
}
/// <summary>
/// 兼容旧 `/connect_server/` 路由;在 replacement 宿主中仅记录调用方期望连接的地址。
/// </summary>
/// <param name="server_ip">旧客户端传入的服务端 IP。</param>
/// <param name="port">旧客户端传入的服务端端口。</param>
/// <returns>与旧 FastAPI 层一致的状态响应。</returns>
[HttpPost("/connect_server/")]
public IActionResult ConnectServer([FromQuery] string server_ip, [FromQuery] int port)
{
_logger.LogInformation("ConnectServer 调用: server_ip={ServerIp}, port={Port}", server_ip, port);
try
{
_compatService.ConnectServer(server_ip, port);
_logger.LogInformation("ConnectServer 成功: server_ip={ServerIp}, port={Port}", server_ip, port);
return Ok(new { status = "connected" });
}
catch (Exception exception)
{
_logger.LogError(exception, "ConnectServer 失败: server_ip={ServerIp}, port={Port}", server_ip, port);
return LegacyBadRequest("Connect Server failed");
}
}
/// <summary>
/// 兼容旧 `GetServerVersion` 版本查询语义。
/// </summary>
/// <returns>服务端版本号。</returns>
[HttpGet("/get_server_version/")]
public IActionResult GetServerVersion()
{
return Ok(new { server_version = _compatService.GetServerVersion() });
}
/// <summary>
/// 兼容旧 `GetClientVersion` 版本查询语义。
/// </summary>
/// <returns>客户端版本号。</returns>
[HttpGet("/get_client_version/")]
public IActionResult GetClientVersion()
{
return Ok(new { client_version = _compatService.GetClientVersion() });
}
/// <summary> /// <summary>
/// 兼容旧 `/setup_robot/` 路由。 /// 兼容旧 `/setup_robot/` 路由。
/// </summary> /// </summary>
@@ -100,59 +47,6 @@ public sealed class LegacyHttpApiController : ControllerBase
} }
} }
/// <summary>
/// 兼容旧 `SetUpRobotFromEnv(env_file)` 参数形状。
/// </summary>
/// <param name="env_file">环境文件路径。</param>
/// <returns>旧 FastAPI 层风格的状态响应。</returns>
[HttpPost("/setup_robot_from_env/")]
public IActionResult SetupRobotFromEnv([FromQuery] string env_file)
{
try
{
_compatService.SetUpRobotFromEnv(env_file);
return Ok(new { status = "robot setup" });
}
catch
{
return LegacyBadRequest("SetUpRobotFromEnv failed");
}
}
/// <summary>
/// 兼容旧 `SetShowTCP(is_show, axis_length, axis_size)` 参数形状。
/// </summary>
/// <param name="is_show">是否显示 TCP。</param>
/// <param name="axis_length">坐标轴长度。</param>
/// <param name="axis_size">坐标轴线宽。</param>
/// <returns>旧 FastAPI 层风格的状态响应。</returns>
[HttpPost("/set_show_tcp/")]
public IActionResult SetShowTcp(
[FromQuery] bool is_show = true,
[FromQuery] double axis_length = 0.1,
[FromQuery] int axis_size = 2)
{
try
{
_compatService.SetShowTcp(is_show, axis_length, axis_size);
return Ok(new { status = "show TCP set" });
}
catch
{
return LegacyBadRequest("SetShowTCP failed");
}
}
/// <summary>
/// 兼容旧 `/is_setup/` 路由。
/// </summary>
/// <returns>当前机器人是否完成初始化。</returns>
[HttpGet("/is_setup/")]
public IActionResult IsSetup()
{
return Ok(new { is_setup = _compatService.IsSetUp });
}
/// <summary> /// <summary>
/// 兼容旧 `EnableRobot(buffer_size=4)` 参数形状。 /// 兼容旧 `EnableRobot(buffer_size=4)` 参数形状。
/// </summary> /// </summary>
@@ -217,28 +111,6 @@ public sealed class LegacyHttpApiController : ControllerBase
} }
} }
/// <summary>
/// 兼容旧 `/set_active_controller/` 路由。
/// </summary>
/// <param name="sim">是否切到仿真控制器。</param>
/// <returns>旧 FastAPI 层风格的状态响应。</returns>
[HttpPost("/set_active_controller/")]
public IActionResult SetActiveController([FromQuery] bool sim)
{
_logger.LogInformation("SetActiveController 调用: sim={Sim}", sim);
try
{
_compatService.SetActiveController(sim);
_logger.LogInformation("SetActiveController 成功: sim={Sim}", sim);
return Ok(new { status = "active controller set" });
}
catch (Exception exception)
{
_logger.LogError(exception, "SetActiveController 失败: sim={Sim}", sim);
return LegacyBadRequest("SetActiveController failed");
}
}
/// <summary> /// <summary>
/// 兼容旧 `/connect_robot/` 路由。 /// 兼容旧 `/connect_robot/` 路由。
/// </summary> /// </summary>
@@ -487,11 +359,6 @@ public sealed class LegacyHttpApiController : ControllerBase
public IActionResult ListFlyshotTrajectories() public IActionResult ListFlyshotTrajectories()
{ {
var names = _compatService.ListTrajectoryNames(); var names = _compatService.ListTrajectoryNames();
if (names.Count == 0)
{
return LegacyBadRequest("ListFlyShotTraj failed");
}
return Ok(new { flyshot_trajs = names }); return Ok(new { flyshot_trajs = names });
} }
@@ -623,9 +490,9 @@ public sealed class LegacyHttpApiController : ControllerBase
_logger.LogInformation("SaveTrajectoryInfo 调用: name={Name}, method={Method}", request.name, request.method); _logger.LogInformation("SaveTrajectoryInfo 调用: name={Name}, method={Method}", request.name, request.method);
try try
{ {
_compatService.SaveTrajectoryInfo(request.name, request.method); var result = _compatService.SaveTrajectoryInfo(request.name, request.method);
_logger.LogInformation("SaveTrajectoryInfo 成功: name={Name}", request.name); _logger.LogInformation("SaveTrajectoryInfo 成功: name={Name}", request.name);
return Ok(new { status = "trajectory info saved", success = true }); return Ok(result);
} }
catch (NotSupportedException exception) catch (NotSupportedException exception)
{ {
@@ -723,36 +590,26 @@ public sealed class LegacyHttpApiController : ControllerBase
} }
/// <summary> /// <summary>
/// 兼容旧 `/init_mpc_robt` 路由,保留历史拼写。 /// 使用机器人名称和 IP 执行真机优先初始化。
/// </summary> /// </summary>
/// <param name="data">初始化请求体。</param> /// <param name="data">初始化请求体。</param>
/// <returns>旧 FastAPI 层风格的初始化结果。</returns> /// <returns>旧 FastAPI 层风格的初始化结果。</returns>
[HttpPost("/init_mpc_robt")] [HttpPost("/init_mpc_robot")]
public IActionResult InitMpcRobot([FromBody] LegacyInitMpcRobotRequest data) public IActionResult InitMpcRobot([FromBody] InitMpcRobotRequest data)
{ {
_logger.LogInformation( _logger.LogInformation(
"InitMpcRobot 调用: robot_name={RobotName}, robot_ip={RobotIp}, sim={Sim}, server={ServerIp}:{Port}", "InitMpcRobot 调用: robot_name={RobotName}, robot_ip={RobotIp}",
data.robot_name, data.robot_ip, data.sim, data.server_ip, data.port); data.robot_name, data.robot_ip);
try try
{ {
_compatService.InitializeRobot(data.robot_name, data.robot_ip, bufferSize: 8);
_compatService.SetUpRobot(data.robot_name);
if (!_compatService.IsSetUp)
{
_logger.LogWarning("InitMpcRobot 失败: Robot not setup");
return LegacyBadRequest("Robot not setup");
}
_compatService.SetActiveController(data.sim);
_compatService.Connect(data.robot_ip);
_compatService.EnableRobot(8);
_logger.LogInformation("InitMpcRobot 成功: robot_name={RobotName}", data.robot_name); _logger.LogInformation("InitMpcRobot 成功: robot_name={RobotName}", data.robot_name);
return Ok(new { message = "init_Success", returnCode = 0 }); return Ok(new { message = "init_Success", returnCode = 0 });
} }
catch (Exception exception) catch (Exception exception)
{ {
_logger.LogError(exception, "InitMpcRobot 失败"); _logger.LogError(exception, "InitMpcRobot 失败");
return LegacyBadRequest("Connect Server failed"); return BadRequest(new { detail = exception.Message });
} }
} }
@@ -1173,20 +1030,10 @@ public sealed class LegacySpeedRatioRequest
} }
/// <summary> /// <summary>
/// 表示旧 `/init_mpc_robt` 路由使用的初始化请求体。 /// 表示 `/init_mpc_robot` 路由使用的真机初始化请求体。
/// </summary> /// </summary>
public sealed class LegacyInitMpcRobotRequest public sealed class InitMpcRobotRequest
{ {
/// <summary>
/// 获取或设置目标服务端 IP。
/// </summary>
public string server_ip { get; init; } = string.Empty;
/// <summary>
/// 获取或设置目标服务端端口。
/// </summary>
public int port { get; init; }
/// <summary> /// <summary>
/// 获取或设置机器人名称。 /// 获取或设置机器人名称。
/// </summary> /// </summary>
@@ -1197,8 +1044,4 @@ public sealed class LegacyInitMpcRobotRequest
/// </summary> /// </summary>
public string robot_ip { get; init; } = string.Empty; public string robot_ip { get; init; } = string.Empty;
/// <summary>
/// 获取或设置是否使用仿真控制器;默认 false 连接真机。
/// </summary>
public bool sim { get; init; }
} }

View File

@@ -61,6 +61,15 @@ public sealed class StatusController : ControllerBase
RobotName = metadata.RobotName, RobotName = metadata.RobotName,
DegreesOfFreedom = metadata.DegreesOfFreedom, DegreesOfFreedom = metadata.DegreesOfFreedom,
UploadedTrajectories = metadata.UploadedTrajectories, UploadedTrajectories = metadata.UploadedTrajectories,
OperatingMode = metadata.OperatingMode,
InitializationStage = metadata.InitializationStage,
ModelLoaded = metadata.IsSetup,
RobotIp = metadata.RobotIp,
LastConnectionError = metadata.LastConnectionError,
LastConnectionErrorAt = metadata.LastConnectionErrorAt,
LastInitializationAttemptAt = metadata.LastInitializationAttemptAt,
CanPlanOffline = metadata.CanPlanOffline,
CanExecuteRealMotion = metadata.CanExecuteRealMotion,
Snapshot = snapshot Snapshot = snapshot
}); });
} }

View File

@@ -5,7 +5,7 @@ namespace Flyshot.Server.Host.Services;
/// <summary> /// <summary>
/// 服务启动时自动初始化机器人的托管服务。 /// 服务启动时自动初始化机器人的托管服务。
/// 从配置文件读取 robot_name 和 robot_ip,依次执行 SetUpRobot、SetActiveController、Connect、EnableRobot。 /// 从配置文件读取 robot_name 和 robot_ip,执行统一的真机优先初始化流程。
/// 初始化失败不会阻止服务启动,仅记录警告日志。 /// 初始化失败不会阻止服务启动,仅记录警告日志。
/// </summary> /// </summary>
public sealed class AutoInitRobotHostedService : IHostedService public sealed class AutoInitRobotHostedService : IHostedService
@@ -54,22 +54,8 @@ public sealed class AutoInitRobotHostedService : IHostedService
try try
{ {
// 步骤1:加载机器人配置 // 启动与人工初始化复用同一编排,确保状态阶段和失败降级语义一致。
_compatService.SetUpRobot(autoInit.RobotName); _compatService.InitializeRobot(autoInit.RobotName, autoInit.RobotIp, bufferSize: 8);
if (!_compatService.IsSetUp)
{
_logger.LogWarning("自动初始化失败: SetUpRobot 返回 IsSetUp=false,robot_name={RobotName}", autoInit.RobotName);
return Task.CompletedTask;
}
// 步骤2:设置控制器模式(默认非仿真)
_compatService.SetActiveController(sim: false);
// 步骤3:连接机器人控制器
_compatService.Connect(autoInit.RobotIp);
// 步骤4:使能机器人(上伺服)
_compatService.EnableRobot(8);
_logger.LogInformation("自动初始化成功: robot_name={RobotName}", autoInit.RobotName); _logger.LogInformation("自动初始化成功: robot_name={RobotName}", autoInit.RobotName);
} }

View File

@@ -8,8 +8,8 @@
"ControllerClientCompat": { "ControllerClientCompat": {
"ServerVersion": "flyshot-replacement-controller-client-compat/0.1.0", "ServerVersion": "flyshot-replacement-controller-client-compat/0.1.0",
"AutoInit": { "AutoInit": {
"RobotName": "LR_Mate_200iD", "RobotName": "FANUC_LR_Mate_200iD_7L",
"RobotIp": "127.0.0.1" "RobotIp": "192.168.10.11"
} }
}, },
"Swagger": { "Swagger": {

View File

@@ -422,3 +422,64 @@ details > summary {
max-height: none; max-height: none;
} }
} }
.offline-panel {
margin: 16px 0 24px;
padding: 18px;
border: 1px solid rgba(56, 189, 248, 0.35);
border-radius: 12px;
background: rgba(15, 23, 42, 0.72);
}
.offline-warning {
color: #fbbf24;
}
.offline-grid {
display: grid;
grid-template-columns: minmax(220px, 1fr) auto minmax(220px, 1fr) auto;
gap: 10px;
align-items: end;
}
.offline-grid label {
display: grid;
gap: 6px;
color: var(--muted);
}
.offline-grid input,
.offline-grid select {
min-height: 38px;
}
.offline-table-wrap {
max-height: 520px;
overflow: auto;
margin-top: 12px;
}
#offline-send-table {
width: 100%;
border-collapse: collapse;
font-size: 12px;
}
#offline-send-table th,
#offline-send-table td {
padding: 7px;
border-bottom: 1px solid rgba(148, 163, 184, 0.18);
text-align: left;
white-space: nowrap;
}
.offline-summary {
display: grid;
grid-template-columns: max-content 1fr;
gap: 6px 16px;
}
@media (max-width: 900px) {
.offline-grid {
grid-template-columns: 1fr;
}
}

View File

@@ -5,10 +5,10 @@ const HISTORY_LIMIT = 10;
const STATIC_DOCS_ELEMENT_ID = "debug-static-docs"; const STATIC_DOCS_ELEMENT_ID = "debug-static-docs";
const groupTitleByPrefix = [ const groupTitleByPrefix = [
// 基础与状态分组:探活和状态快照两个固定 API 路径 { match: function (op) { return ["/healthz", "/status", "/debug", "/api/status/snapshot"].indexOf(op.path) >= 0; }, title: "基础状态" },
{ match: function (op) { return op.path === "/healthz" || op.path === "/api/status/snapshot"; }, title: "基础与状态" }, { match: function (op) { return ["/setup_robot/", "/init_mpc_robot", "/connect_robot/", "/enable_robot/", "/disable_robot/", "/disconnect_robot/", "/stop_move/"].indexOf(op.path) >= 0; }, title: "初始化与维护" },
// 默认兜底:剩余全部走 ControllerClient 兼容分组 { match: function (op) { return ["/move_joint/", "/move_pose/", "/set_tcp/", "/get_tcp/", "/set_io/", "/get_io/", "/get_joint_position/", "/get_pose", "/robot_info/"].indexOf(op.path) >= 0; }, title: "机器人操作" },
{ match: function () { return true; }, title: "ControllerClient 兼容" } { match: function () { return true; }, title: "轨迹与飞拍" }
]; ];
const state = { const state = {
@@ -19,6 +19,10 @@ const state = {
}; };
const requestBodySamples = { const requestBodySamples = {
"POST /init_mpc_robot": {
robot_name: "FANUC_LR_Mate_200iD",
robot_ip: "192.168.10.101"
},
"POST /move_pose/": { "POST /move_pose/": {
x: 100.0, x: 100.0,
y: 200.0, y: 200.0,
@@ -26,9 +30,19 @@ const requestBodySamples = {
w: 0.0, w: 0.0,
p: 45.0, p: 45.0,
r: 0.0 r: 0.0
},
"POST /save_traj_info/": {
name: "demo-flyshot",
method: "icsp"
} }
}; };
const offlinePlanningState = {
result: null,
page: 1,
pageSize: 50
};
/** 简单的 escape:把任意字符串安全嵌入 textContent 之外的位置时使用。 */ /** 简单的 escape:把任意字符串安全嵌入 textContent 之外的位置时使用。 */
function escapeHtml(value) { function escapeHtml(value) {
return String(value).replace(/[&<>"']/g, function (ch) { return String(value).replace(/[&<>"']/g, function (ch) {
@@ -781,9 +795,9 @@ function renderGroups(operations) {
grouped.get(groupTitle).push(op); grouped.get(groupTitle).push(op);
}); });
// 固定输出顺序:基础与状态在前,ControllerClient 兼容在后,其余按字典序。 // 固定输出顺序与现场操作顺序一致。
const orderedTitles = []; const orderedTitles = [];
["基础与状态", "ControllerClient 兼容"].forEach(function (title) { ["基础状态", "初始化与维护", "机器人操作", "轨迹与飞拍"].forEach(function (title) {
if (grouped.has(title)) orderedTitles.push(title); if (grouped.has(title)) orderedTitles.push(title);
}); });
Array.from(grouped.keys()).sort().forEach(function (title) { Array.from(grouped.keys()).sort().forEach(function (title) {
@@ -807,6 +821,144 @@ function renderGroups(operations) {
}); });
} }
/** 调用 JSON API,并在非 2xx 时把服务端 detail 作为异常抛出。 */
async function requestJson(url, init) {
const response = await fetch(url, init || {});
const text = await response.text();
let payload = null;
if (text) {
try { payload = JSON.parse(text); } catch (error) { payload = { detail: text }; }
}
if (!response.ok) {
throw new Error(payload && payload.detail ? payload.detail : "HTTP " + response.status);
}
return payload;
}
/** 刷新固定 RobotConfig.json 中的飞拍轨迹下拉列表。 */
async function refreshOfflineTrajectoryList() {
const select = document.getElementById("offline-trajectory-select");
const payload = await requestJson("/list_flyShotTraj/");
const names = payload.flyshot_trajs || [];
select.innerHTML = "";
names.forEach(function (name) {
const option = document.createElement("option");
option.value = name;
option.textContent = name;
select.appendChild(option);
});
}
/** 按页渲染最终发送点,避免大点列一次性创建过多 DOM。 */
function renderOfflineSendPage() {
const result = offlinePlanningState.result;
const body = document.querySelector("#offline-send-table tbody");
body.innerHTML = "";
if (!result) {
document.getElementById("offline-page-info").textContent = "0 / 0";
return;
}
const points = result.finalSendPoints || [];
const pageCount = Math.max(1, Math.ceil(points.length / offlinePlanningState.pageSize));
offlinePlanningState.page = Math.min(Math.max(1, offlinePlanningState.page), pageCount);
const start = (offlinePlanningState.page - 1) * offlinePlanningState.pageSize;
points.slice(start, start + offlinePlanningState.pageSize).forEach(function (point) {
const row = document.createElement("tr");
[
point.sampleIndex,
Number(point.sendTime).toFixed(6),
Number(point.trajectoryTime).toFixed(6),
(point.jointsDegrees || []).map(function (value) { return Number(value).toFixed(4); }).join(", "),
point.ioMask,
point.ioValue
].forEach(function (value) {
const cell = document.createElement("td");
cell.textContent = value;
row.appendChild(cell);
});
body.appendChild(row);
});
document.getElementById("offline-page-info").textContent = offlinePlanningState.page + " / " + pageCount;
}
/** 展示离线规划摘要、触发绑定、最终发送点和导出路径。 */
function renderOfflinePlanningResult(result) {
offlinePlanningState.result = result;
offlinePlanningState.page = 1;
document.getElementById("offline-planning-summary").innerHTML =
"<dl class=\"offline-summary\">" +
"<dt>运行结果</dt><dd>" + escapeHtml(result.isValid ? "有效" : "无效") + "</dd>" +
"<dt>规划时长</dt><dd>" + Number(result.planningDurationSeconds).toFixed(6) + " s</dd>" +
"<dt>最终发送时长</dt><dd>" + Number(result.finalSendDurationSeconds).toFixed(6) + " s</dd>" +
"<dt>关键/稠密/发送点</dt><dd>" + result.plannedWaypointCount + " / " + result.densePointCount + " / " + result.finalSendPointCount + "</dd>" +
"<dt>速度倍率</dt><dd>" + result.speedRatio + "</dd>" +
"</dl>";
document.getElementById("offline-shot-events").textContent =
"ShotEvents / TriggerBindings\n" + JSON.stringify({
shotEvents: result.shotEvents,
triggerBindings: result.triggerBindings
}, null, 2);
document.getElementById("offline-artifact-paths").textContent =
"导出文件\n" + JSON.stringify(result.artifactPaths, null, 2);
renderOfflineSendPage();
}
/** 初始化离线规划操作面板。 */
function initializeOfflinePlanningPanel() {
const status = document.getElementById("offline-planning-status");
document.getElementById("offline-load-model").addEventListener("click", async function () {
try {
const robotName = document.getElementById("offline-robot-name").value.trim();
await requestJson("/setup_robot/?robot_name=" + encodeURIComponent(robotName), { method: "POST" });
status.textContent = "模型已加载,当前可离线规划。";
} catch (error) {
status.textContent = "模型加载失败: " + error.message;
}
});
document.getElementById("offline-refresh-trajectories").addEventListener("click", async function () {
try { await refreshOfflineTrajectoryList(); status.textContent = "轨迹列表已刷新。"; }
catch (error) { status.textContent = "刷新失败: " + error.message; }
});
document.getElementById("offline-upload").addEventListener("click", async function () {
try {
const body = document.getElementById("offline-upload-json").value;
await requestJson("/upload_flyshot/", { method: "POST", headers: { "Content-Type": "application/json" }, body: body });
await refreshOfflineTrajectoryList();
status.textContent = "轨迹已上传到固定 RobotConfig.json。";
} catch (error) {
status.textContent = "上传失败: " + error.message;
}
});
document.getElementById("offline-generate").addEventListener("click", async function () {
try {
const name = document.getElementById("offline-trajectory-select").value;
const method = document.getElementById("offline-planning-method").value;
status.textContent = "正在生成最终发送轨迹...";
const result = await requestJson("/save_traj_info/", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: name, method: method })
});
renderOfflinePlanningResult(result);
status.textContent = "离线规划完成,未向机器人发送命令。";
} catch (error) {
status.textContent = "离线规划失败: " + error.message;
}
});
document.getElementById("offline-prev-page").addEventListener("click", function () {
offlinePlanningState.page--;
renderOfflineSendPage();
});
document.getElementById("offline-next-page").addEventListener("click", function () {
offlinePlanningState.page++;
renderOfflineSendPage();
});
refreshOfflineTrajectoryList().catch(function (error) {
status.textContent = "轨迹列表加载失败: " + error.message;
});
}
/** 加载 OpenAPI 文档并渲染。 */ /** 加载 OpenAPI 文档并渲染。 */
async function loadSpecAndRender() { async function loadSpecAndRender() {
const metaSpec = document.getElementById("meta-spec-url"); const metaSpec = document.getElementById("meta-spec-url");
@@ -854,4 +1006,5 @@ document.getElementById("history-clear").addEventListener("click", function () {
}); });
renderHistory(); renderHistory();
initializeOfflinePlanningPanel();
loadSpecAndRender(); loadSpecAndRender();

View File

@@ -28,6 +28,58 @@
<dd id="meta-status">初始化中...</dd> <dd id="meta-status">初始化中...</dd>
</dl> </dl>
</section> </section>
<section class="offline-panel" id="offline-planning-panel">
<h2>离线飞拍规划</h2>
<p class="offline-warning">生成轨迹不会向机器人发送命令;只有 RealReady 状态才能执行真实运动。</p>
<div class="offline-grid">
<label>
机器人模型
<input id="offline-robot-name" value="FANUC_LR_Mate_200iD">
</label>
<button id="offline-load-model" type="button">加载模型</button>
<label>
已上传轨迹
<select id="offline-trajectory-select"></select>
</label>
<button id="offline-refresh-trajectories" type="button" class="secondary">刷新列表</button>
<label>
规划方法
<select id="offline-planning-method">
<option value="icsp">icsp</option>
<option value="self-adapt-icsp">self-adapt-icsp</option>
</select>
</label>
<button id="offline-generate" type="button">生成最终发送轨迹</button>
</div>
<details>
<summary>上传飞拍轨迹 JSON</summary>
<textarea id="offline-upload-json" class="body-editor">{
"name": "demo-flyshot",
"waypoints": [[0,0,0,0,0,0],[0.001,0,0,0,0,0],[0.002,0,0,0,0,0],[0.003,0,0,0,0,0]],
"shot_flags": [false,true,false,false],
"offset_values": [0,1,0,0],
"addrs": [[],[7,8],[],[]]
}</textarea>
<button id="offline-upload" type="button">上传并刷新列表</button>
</details>
<div id="offline-planning-status" class="empty-hint">等待离线规划操作。</div>
<div id="offline-planning-summary"></div>
<div class="offline-table-wrap">
<table id="offline-send-table">
<thead>
<tr><th>序号</th><th>发送时间</th><th>轨迹时间</th><th>关节角(deg)</th><th>IO Mask</th><th>IO Value</th></tr>
</thead>
<tbody></tbody>
</table>
</div>
<div class="button-row">
<button id="offline-prev-page" type="button" class="secondary">上一页</button>
<span id="offline-page-info">0 / 0</span>
<button id="offline-next-page" type="button" class="secondary">下一页</button>
</div>
<pre id="offline-shot-events" class="response-body"></pre>
<pre id="offline-artifact-paths" class="response-body"></pre>
</section>
<div id="debug-console-app"> <div id="debug-console-app">
<div class="empty-hint">正在加载接口列表...</div> <div class="empty-hint">正在加载接口列表...</div>
</div> </div>
@@ -41,11 +93,6 @@
</aside> </aside>
<script id="debug-static-docs" type="application/json"> <script id="debug-static-docs" type="application/json">
{ {
"GET /": {
"summary": "根路径探活",
"description": "返回旧 HTTP 服务约定的 Hello World 响应,用来确认兼容层宿主已启动。",
"requestBodyFields": []
},
"GET /healthz": { "GET /healthz": {
"summary": "宿主健康检查", "summary": "宿主健康检查",
"description": "返回 replacement 宿主的基础健康状态,适合探活和部署验收。", "description": "返回 replacement 宿主的基础健康状态,适合探活和部署验收。",
@@ -66,39 +113,9 @@
"description": "返回当前 ControllerClient 兼容层和 FANUC 运行时的聚合状态快照,供状态页和外部诊断读取。", "description": "返回当前 ControllerClient 兼容层和 FANUC 运行时的聚合状态快照,供状态页和外部诊断读取。",
"requestBodyFields": [] "requestBodyFields": []
}, },
"POST /connect_server/": {
"summary": "记录旧客户端目标服务",
"description": "兼容旧 `connect_server` 调用形状;在 replacement 宿主中只记录调用方期望连接的服务端地址,不会再启动旧 TCP+JSON 网关。",
"requestBodyFields": []
},
"GET /get_server_version/": {
"summary": "读取服务端版本",
"description": "返回当前 replacement 宿主暴露给旧客户端语义的服务端版本号。",
"requestBodyFields": []
},
"GET /get_client_version/": {
"summary": "读取客户端版本",
"description": "返回兼容层对外声明的客户端版本号,便于旧链路做版本诊断。",
"requestBodyFields": []
},
"POST /setup_robot/": { "POST /setup_robot/": {
"summary": "按名称加载机器人", "summary": "按名称加载机器人",
"description": "根据机器人名称加载对应模型和运行配置,为后续连接、使能和执行做准备。", "description": "只加载机器人模型和 RobotConfig.json,不连接、不使能;成功后可进行离线规划。",
"requestBodyFields": []
},
"POST /setup_robot_from_env/": {
"summary": "按环境文件加载机器人",
"description": "兼容旧 `env_file` 入口,根据环境文件路径完成机器人初始化配置加载。",
"requestBodyFields": []
},
"POST /set_show_tcp/": {
"summary": "设置 TCP 显示开关",
"description": "兼容旧可视化参数接口,记录是否显示 TCP 坐标轴及其尺寸参数。",
"requestBodyFields": []
},
"GET /is_setup/": {
"summary": "查询是否已初始化",
"description": "返回当前兼容层是否已经完成 `SetUpRobot` 初始化。",
"requestBodyFields": [] "requestBodyFields": []
}, },
"GET /enable_robot/": { "GET /enable_robot/": {
@@ -116,11 +133,6 @@
"description": "触发兼容层的 `StopMove` 语义,用于终止当前点动或轨迹执行。", "description": "触发兼容层的 `StopMove` 语义,用于终止当前点动或轨迹执行。",
"requestBodyFields": [] "requestBodyFields": []
}, },
"POST /set_active_controller/": {
"summary": "切换控制器模式",
"description": "通过查询参数 `sim` 指定当前使用仿真控制器还是真机控制器。",
"requestBodyFields": []
},
"POST /connect_robot/": { "POST /connect_robot/": {
"summary": "连接机器人控制器", "summary": "连接机器人控制器",
"description": "根据查询参数 `ip` 连接机器人控制器,建立后续命令与状态链路。", "description": "根据查询参数 `ip` 连接机器人控制器,建立后续命令与状态链路。",
@@ -232,8 +244,8 @@
] ]
}, },
"POST /save_traj_info/": { "POST /save_traj_info/": {
"summary": "导出轨迹文件", "summary": "离线生成最终发送轨迹",
"description": "为指定飞拍轨迹生成并导出 `SaveTrajInfo` 兼容文件集合;底层未实现时会返回 501。", "description": "仅要求机器人模型已加载;完成规划、触发绑定、最终 8ms 序列生成和文件导出,并返回完整规划结果。",
"requestBodyFields": [ "requestBodyFields": [
{ "name": "name", "type": "string", "required": true, "description": "目标飞拍轨迹名称。" }, { "name": "name", "type": "string", "required": true, "description": "目标飞拍轨迹名称。" },
{ "name": "method", "type": "string", "required": false, "description": "轨迹规划方法,默认 `icsp`。" } { "name": "method", "type": "string", "required": false, "description": "轨迹规划方法,默认 `icsp`。" }
@@ -262,15 +274,12 @@
{ "name": "name", "type": "string", "required": true, "description": "要删除的飞拍轨迹名称。" } { "name": "name", "type": "string", "required": true, "description": "要删除的飞拍轨迹名称。" }
] ]
}, },
"POST /init_mpc_robt": { "POST /init_mpc_robot": {
"summary": "一键初始化旧链路", "summary": "真机优先初始化",
"description": "兼容旧 `init_mpc_robt` 入口,按历史拼写保留。会串联执行 `SetUpRobot -> SetActiveController -> Connect -> EnableRobot`。", "description": "执行 `加载模型 -> 连接真机 -> EnableRobot(8)`;连接失败时模型仍保持 OfflineReady。",
"requestBodyFields": [ "requestBodyFields": [
{ "name": "server_ip", "type": "string", "required": true, "description": "旧客户端记录的目标服务端 IP,仅保留兼容语义。" },
{ "name": "port", "type": "integer", "required": true, "description": "旧客户端记录的目标服务端端口。" },
{ "name": "robot_name", "type": "string", "required": true, "description": "机器人名称,用于加载模型与配置。" }, { "name": "robot_name", "type": "string", "required": true, "description": "机器人名称,用于加载模型与配置。" },
{ "name": "robot_ip", "type": "string", "required": true, "description": "机器人控制器 IP。" }, { "name": "robot_ip", "type": "string", "required": true, "description": "机器人控制器 IP。" }
{ "name": "sim", "type": "boolean", "required": false, "description": "是否连接仿真控制器;默认 `false` 表示真机。" }
] ]
}, },
"GET /get_pose": { "GET /get_pose": {

View File

@@ -16,6 +16,70 @@ namespace Flyshot.Core.Tests;
/// </summary> /// </summary>
public sealed class FanucControllerRuntimeDenseTests public sealed class FanucControllerRuntimeDenseTests
{ {
/// <summary>
/// 验证飞拍输出选择贯通到 J519 命令,DO/RO 模式都输出两周期脉冲并在同一路清零。
/// </summary>
/// <param name="useDo">true 选择 DO,false 选择 RO。</param>
/// <param name="expectedIoType">预期 J519 写入类型,DO 为 2,RO 为 9。</param>
/// <param name="usePreparedExecution">是否使用上游已完成触发绑定的最终发送队列。</param>
[Theory]
[InlineData(true, 2, false)]
[InlineData(false, 9, false)]
[InlineData(true, 2, true)]
[InlineData(false, 9, true)]
public void ExecuteTrajectory_FlyshotOutputSelection_PreservesPulseAndClear(bool useDo, int expectedIoType, bool usePreparedExecution)
{
using var commandClient = new FanucCommandClient();
using var stateClient = new FanucStateClient();
using var j519Client = new FanucJ519Client();
using var runtime = new FanucControllerRuntime(commandClient, stateClient, j519Client);
runtime.ResetRobot(TestRobotFactory.CreateRobotProfile(), "FANUC_LR_Mate_200iD");
j519Client.EnableCommandHistoryForTests();
ForceRealModeEnabled(runtime, speedRatio: 1.0);
var trigger = new TrajectoryDoEvent(waypointIndex: 1, triggerTime: 0.008,
offsetCycles: 0, holdCycles: 2, addressGroup: new IoAddressGroup([1]), useDo: useDo);
var samples = Enumerable.Range(0, 4)
.Select(index => new FlyshotPreparedSample(index, index * 0.008, index * 0.008,
1.0, [0.0, 0.0, 0.0, 0.0, 0.0, 0.0])).ToArray();
var preparedExecution = usePreparedExecution
? new FlyshotPreparedExecution(samples,
[new FlyshotPreparedTriggerBinding(trigger, samples[1], 1, true)],
Array.Empty<IReadOnlyList<double>>(), Array.Empty<IReadOnlyList<double>>(),
requestSpeedRatio: 1.0, finalSpeedRatio: 1.0, finalDurationSeconds: 0.024, stretchIterationCount: 0)
: null;
// 静止轨迹隔离运动算法影响,仅验证触发、保持和清零对应的协议字段。
var result = new TrajectoryResult(
programName: "output-selection",
method: PlanningMethod.Icsp,
isValid: true,
duration: TimeSpan.FromSeconds(0.024),
shotEvents: Array.Empty<ShotEvent>(),
triggerTimeline: [trigger],
artifacts: Array.Empty<TrajectoryArtifact>(),
failureReason: null,
usedCache: false,
originalWaypointCount: 2,
plannedWaypointCount: 2,
denseJointTrajectory:
[
new double[] { 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 },
new double[] { 0.024, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 }
],
preparedFlyshotExecution: preparedExecution);
runtime.ExecuteTrajectory(result, [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
WaitUntilIdle(runtime);
var commands = j519Client.GetCommandHistoryForTests();
Assert.True(commands.Count >= 4);
Assert.All(commands, command => Assert.Equal((byte)expectedIoType, command.WriteIoType));
Assert.All(commands, command => Assert.Equal((ushort)1, command.WriteIoIndex));
Assert.Equal(new ushort[] { 0, 1, 1, 1 }, commands.Take(4).Select(command => command.WriteIoMask));
Assert.Equal(new ushort[] { 0, 1, 1, 0 }, commands.Take(4).Select(command => command.WriteIoValue));
}
private const double SmoothPtpVelocityShapeCoefficient = 2.1875; private const double SmoothPtpVelocityShapeCoefficient = 2.1875;
private const double SmoothPtpAccelerationShapeCoefficient = 7.513188404399293; private const double SmoothPtpAccelerationShapeCoefficient = 7.513188404399293;
private const double SmoothPtpJerkShapeCoefficient = 52.5; private const double SmoothPtpJerkShapeCoefficient = 52.5;
@@ -900,47 +964,6 @@ public sealed class FanucControllerRuntimeDenseTests
Assert.Equal(0, commands[^1].WriteIoValue); Assert.Equal(0, commands[^1].WriteIoValue);
} }
/// <summary>
/// 验证仿真模式下即使传入稠密路点,也会回退到单点同步更新。
/// </summary>
[Fact]
public void ExecuteTrajectory_WithDenseWaypoints_SimulationMode_FallsBackToSinglePoint()
{
var runtime = new FanucControllerRuntime();
var robot = TestRobotFactory.CreateRobotProfile();
runtime.ResetRobot(robot, "FANUC_LR_Mate_200iD");
runtime.SetActiveController(sim: true);
runtime.Connect("192.168.10.101");
runtime.EnableRobot(bufferSize: 2);
var denseTrajectory = new[]
{
new[] { 0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6 },
new[] { 0.008, 0.11, 0.21, 0.31, 0.41, 0.51, 0.61 },
new[] { 0.016, 0.12, 0.22, 0.32, 0.42, 0.52, 0.62 }
};
var result = new TrajectoryResult(
programName: "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: denseTrajectory);
runtime.ExecuteTrajectory(result, [0.12, 0.22, 0.32, 0.42, 0.52, 0.62]);
var snapshot = runtime.GetSnapshot();
Assert.False(snapshot.IsInMotion);
Assert.Equal([0.12, 0.22, 0.32, 0.42, 0.52, 0.62], snapshot.JointPositions);
}
/// <summary> /// <summary>
/// 验证稠密执行前若 J519 首次未就绪,会先尝试一次 EnableRobot,再在 500ms 后复查状态。 /// 验证稠密执行前若 J519 首次未就绪,会先尝试一次 EnableRobot,再在 500ms 后复查状态。
/// </summary> /// </summary>
@@ -997,24 +1020,6 @@ public sealed class FanucControllerRuntimeDenseTests
Assert.Contains("seq=22", exception.Message, StringComparison.Ordinal); Assert.Contains("seq=22", exception.Message, StringComparison.Ordinal);
} }
/// <summary>
/// 验证 StopMove 在没有任何后台发送任务运行时不会抛出异常。
/// </summary>
[Fact]
public void StopMove_DoesNotThrowWhenNoSendTaskRunning()
{
var runtime = new FanucControllerRuntime();
var robot = TestRobotFactory.CreateRobotProfile();
runtime.ResetRobot(robot, "FANUC_LR_Mate_200iD");
runtime.SetActiveController(sim: true);
runtime.Connect("192.168.10.101");
runtime.EnableRobot(bufferSize: 2);
var exception = Record.Exception(() => runtime.StopMove());
Assert.Null(exception);
Assert.False(runtime.GetSnapshot().IsInMotion);
}
/// <summary> /// <summary>
/// 验证 IO 地址组中的地址号按 writeIoIndex=1 的手册语义映射为 writeIoValue 位掩码。 /// 验证 IO 地址组中的地址号按 writeIoIndex=1 的手册语义映射为 writeIoValue 位掩码。
/// </summary> /// </summary>
@@ -1171,7 +1176,6 @@ public sealed class FanucControllerRuntimeDenseTests
private static void ForceRealModeEnabled(FanucControllerRuntime runtime, double speedRatio) private static void ForceRealModeEnabled(FanucControllerRuntime runtime, double speedRatio)
{ {
SetPrivateField(runtime, "_activeControllerIsSimulation", false);
SetPrivateField(runtime, "_connectedRobotIp", "127.0.0.1"); SetPrivateField(runtime, "_connectedRobotIp", "127.0.0.1");
SetPrivateField(runtime, "_isEnabled", true); SetPrivateField(runtime, "_isEnabled", true);
SetPrivateField(runtime, "_bufferSize", 2); SetPrivateField(runtime, "_bufferSize", 2);

View File

@@ -1,5 +1,6 @@
using Flyshot.Runtime.Common; using Flyshot.Runtime.Common;
using Flyshot.Runtime.Fanuc; using Flyshot.Runtime.Fanuc;
using Flyshot.Runtime.Fanuc.Protocol;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using System.Reflection; using System.Reflection;
@@ -14,7 +15,7 @@ public sealed class FanucControllerRuntimeLoggingRegistrationTests
/// 验证宿主按 IControllerRuntime 解析 FANUC 运行时时,不会因为可选日志参数而退回到无日志实例。 /// 验证宿主按 IControllerRuntime 解析 FANUC 运行时时,不会因为可选日志参数而退回到无日志实例。
/// </summary> /// </summary>
[Fact] [Fact]
public void ServiceProvider_Resolves_FanucControllerRuntime_WithLogger() public void ServiceProvider_Resolves_FanucControllerRuntime_WithAllClientLoggers()
{ {
var services = new ServiceCollection(); var services = new ServiceCollection();
services.AddLogging(); services.AddLogging();
@@ -28,5 +29,27 @@ public sealed class FanucControllerRuntimeLoggingRegistrationTests
Assert.NotNull(loggerField); Assert.NotNull(loggerField);
Assert.NotNull(loggerField!.GetValue(concreteRuntime)); Assert.NotNull(loggerField!.GetValue(concreteRuntime));
AssertClientLoggerIsInjected<FanucCommandClient>(concreteRuntime, "_commandClient");
AssertClientLoggerIsInjected<FanucStateClient>(concreteRuntime, "_stateClient");
AssertClientLoggerIsInjected<FanucJ519Client>(concreteRuntime, "_j519Client");
}
/// <summary>
/// 断言运行时持有的底层协议客户端已注入对应类型的日志记录器。
/// </summary>
/// <typeparam name="TClient">待检查的协议客户端类型。</typeparam>
/// <param name="runtime">已经通过依赖注入解析出的 FANUC 运行时。</param>
/// <param name="clientFieldName">运行时内部保存该客户端实例的字段名。</param>
private static void AssertClientLoggerIsInjected<TClient>(FanucControllerRuntime runtime, string clientFieldName)
{
var clientField = typeof(FanucControllerRuntime).GetField(clientFieldName, BindingFlags.Instance | BindingFlags.NonPublic);
Assert.NotNull(clientField);
var client = Assert.IsType<TClient>(clientField!.GetValue(runtime));
var loggerField = typeof(TClient).GetField("_logger", BindingFlags.Instance | BindingFlags.NonPublic);
Assert.NotNull(loggerField);
Assert.NotNull(loggerField!.GetValue(client));
} }
} }

View File

@@ -279,6 +279,34 @@ public sealed class FanucJ519ClientTests : IDisposable
Assert.True(client.IsCommandQueueDrainedForTests()); Assert.True(client.IsCommandQueueDrainedForTests());
} }
/// <summary>
/// 验证机器人持续不可接收命令时,后续装载的命令队列会被完成,避免执行请求永久等待。
/// </summary>
[Fact]
public async Task LoadCommandQueue_WhenLatestStatusDoesNotAcceptCommands_CompletesQueueWait()
{
using var client = new FanucJ519Client();
await client.ConnectAsync("127.0.0.1", Port, _cts.Token);
var initResult = await _server.ReceiveAsync(_cts.Token);
client.StartMotion();
await SendStatusPacketAsync(initResult.RemoteEndPoint, sequence: 900, status: 0x04);
await Task.Delay(100, _cts.Token);
client.LoadCommandQueue(
[
new FanucJ519Command(sequence: 0, targetJoints: [1.0, 0.0, 0.0, 0.0, 0.0, 0.0])
]);
await SendStatusPacketAsync(initResult.RemoteEndPoint, sequence: 901, status: 0x04);
await client.WaitForCommandQueueDrainedAsync(_cts.Token).WaitAsync(TimeSpan.FromSeconds(1), _cts.Token);
await Assert.ThrowsAsync<TimeoutException>(
() => _server.ReceiveAsync(_cts.Token).AsTask().WaitAsync(TimeSpan.FromMilliseconds(120)));
Assert.True(client.IsCommandQueueDrainedForTests());
await client.StopMotionAsync(_cts.Token);
}
/// <summary> /// <summary>
/// 验证启用命令流结束开关后,LastData 结束帧发送完成会进入仅监控状态,不再继续回发保姿命令。 /// 验证启用命令流结束开关后,LastData 结束帧发送完成会进入仅监控状态,不再继续回发保姿命令。
/// </summary> /// </summary>
@@ -572,13 +600,14 @@ public sealed class FanucJ519ClientTests : IDisposable
private async Task SendStatusPacketAsync( private async Task SendStatusPacketAsync(
IPEndPoint clientEndpoint, IPEndPoint clientEndpoint,
uint sequence, uint sequence,
IReadOnlyList<double>? jointDegrees = null) IReadOnlyList<double>? jointDegrees = null,
byte status = 15)
{ {
var responsePacket = new byte[FanucJ519Protocol.ResponsePacketLength]; var responsePacket = new byte[FanucJ519Protocol.ResponsePacketLength];
BinaryPrimitives.WriteUInt32BigEndian(responsePacket.AsSpan(0x00, 4), 0); BinaryPrimitives.WriteUInt32BigEndian(responsePacket.AsSpan(0x00, 4), 0);
BinaryPrimitives.WriteUInt32BigEndian(responsePacket.AsSpan(0x04, 4), 1); BinaryPrimitives.WriteUInt32BigEndian(responsePacket.AsSpan(0x04, 4), 1);
BinaryPrimitives.WriteUInt32BigEndian(responsePacket.AsSpan(0x08, 4), sequence); BinaryPrimitives.WriteUInt32BigEndian(responsePacket.AsSpan(0x08, 4), sequence);
responsePacket[0x0c] = 15; responsePacket[0x0c] = status;
if (jointDegrees is not null) if (jointDegrees is not null)
{ {

View File

@@ -16,40 +16,6 @@ namespace Flyshot.Core.Tests;
/// </summary> /// </summary>
public sealed class RuntimeOrchestrationTests public sealed class RuntimeOrchestrationTests
{ {
/// <summary>
/// 验证 FANUC 最小运行时执行轨迹后会更新状态快照与最终关节位置。
/// </summary>
[Fact]
public void FanucControllerRuntime_ExecuteTrajectory_UpdatesSnapshotAndFinalJointPositions()
{
var runtime = new FanucControllerRuntime();
var robot = TestRobotFactory.CreateRobotProfile();
runtime.ResetRobot(robot, "FANUC_LR_Mate_200iD");
runtime.SetActiveController(sim: true);
runtime.Connect("192.168.10.101");
runtime.EnableRobot(bufferSize: 2);
var result = new TrajectoryResult(
programName: "demo",
method: PlanningMethod.Icsp,
isValid: true,
duration: TimeSpan.FromSeconds(1.2),
shotEvents: Array.Empty<ShotEvent>(),
triggerTimeline: Array.Empty<TrajectoryDoEvent>(),
artifacts: Array.Empty<TrajectoryArtifact>(),
failureReason: null,
usedCache: false,
originalWaypointCount: 4,
plannedWaypointCount: 4);
runtime.ExecuteTrajectory(result, [1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
var snapshot = runtime.GetSnapshot();
Assert.Equal("Connected", snapshot.ConnectionState);
Assert.False(snapshot.IsInMotion);
Assert.Equal([1.0, 2.0, 3.0, 4.0, 5.0, 6.0], snapshot.JointPositions);
}
/// <summary> /// <summary>
/// 验证真机运行时会把 TCP 10010 状态通道健康度映射为可诊断连接状态。 /// 验证真机运行时会把 TCP 10010 状态通道健康度映射为可诊断连接状态。
/// </summary> /// </summary>
@@ -275,13 +241,14 @@ public sealed class RuntimeOrchestrationTests
var doEvent = Assert.Single(bundle.Result.TriggerTimeline); var doEvent = Assert.Single(bundle.Result.TriggerTimeline);
Assert.Equal(4, doEvent.HoldCycles); Assert.Equal(4, doEvent.HoldCycles);
Assert.True(doEvent.UseDo);
} }
/// <summary> /// <summary>
/// 验证 RobotConfig.json 关闭 use_do 时仍保留 ShotEvent 诊断信息,但不生成伺服 DO 事件。 /// 验证 use_do=false 时仍生成飞拍触发事件,保持周期不变。
/// </summary> /// </summary>
[Fact] [Fact]
public void ControllerClientTrajectoryOrchestrator_PlanUploadedFlyshot_SuppressesDoTimeline_WhenUseDoIsFalse() public void ControllerClientTrajectoryOrchestrator_PlanUploadedFlyshot_PreservesTriggerTimeline_WhenUseDoIsFalse()
{ {
var orchestrator = new ControllerClientTrajectoryOrchestrator(); var orchestrator = new ControllerClientTrajectoryOrchestrator();
var robot = TestRobotFactory.CreateRobotProfile(); var robot = TestRobotFactory.CreateRobotProfile();
@@ -298,7 +265,9 @@ public sealed class RuntimeOrchestrationTests
var bundle = orchestrator.PlanUploadedFlyshot(robot, uploaded, settings: settings); var bundle = orchestrator.PlanUploadedFlyshot(robot, uploaded, settings: settings);
Assert.Single(bundle.Result.ShotEvents); Assert.Single(bundle.Result.ShotEvents);
Assert.Empty(bundle.Result.TriggerTimeline); var trigger = Assert.Single(bundle.Result.TriggerTimeline);
Assert.Equal(4, trigger.HoldCycles);
Assert.False(trigger.UseDo);
} }
/// <summary> /// <summary>
@@ -761,7 +730,6 @@ public sealed class RuntimeOrchestrationTests
{ {
var service = TestRobotFactory.CreateCompatService(); var service = TestRobotFactory.CreateCompatService();
service.SetUpRobot("FANUC_LR_Mate_200iD"); service.SetUpRobot("FANUC_LR_Mate_200iD");
service.SetActiveController(sim: true);
service.Connect("192.168.10.101"); service.Connect("192.168.10.101");
service.EnableRobot(2); service.EnableRobot(2);
@@ -803,7 +771,6 @@ public sealed class RuntimeOrchestrationTests
}; };
service.SetUpRobot("FANUC_LR_Mate_200iD"); service.SetUpRobot("FANUC_LR_Mate_200iD");
service.SetActiveController(sim: false);
service.Connect("192.168.10.101"); service.Connect("192.168.10.101");
service.EnableRobot(2); service.EnableRobot(2);
runtime.SetSpeedRatio(0.5); runtime.SetSpeedRatio(0.5);
@@ -824,6 +791,66 @@ public sealed class RuntimeOrchestrationTests
} }
} }
/// <summary>
/// 验证直接关节移动和飞拍预移动均按规划倍率与当前运行倍率的乘积生成点对点轨迹。
/// </summary>
/// <param name="moveToStart">是否通过飞拍预移动入口执行点对点运动。</param>
/// <param name="planningSpeedScale">配置中的规划速度倍率。</param>
/// <param name="speedRatio">机器人初始化后设置的当前运行速度倍率。</param>
[Theory]
[InlineData(false, 0.74227, 1.0)]
[InlineData(false, 0.74227, 0.5)]
[InlineData(false, 1.0, 0.7)]
[InlineData(true, 0.74227, 0.5)]
public void ControllerClientCompatService_MoveJoint_AppliesCombinedPlanningSpeedScale(
bool moveToStart, double planningSpeedScale, double speedRatio)
{
var configRoot = CreateTempConfigRoot();
try
{
WriteRobotConfigWithDemoTrajectory(configRoot, planningSpeedScale: planningSpeedScale, speedRatio: 1.0);
var options = new ControllerClientCompatOptions { ConfigRoot = configRoot };
double[] startJoints = [0.4, 0.0, 0.0, 0.0, 0.0, 0.0];
double[] targetJoints = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
var runtime = new DelayedCompletionControllerRuntime(startJoints, TimeSpan.Zero);
var service = new ControllerClientCompatService(
options,
new ControllerClientCompatRobotCatalog(options, new RobotModelLoader()),
runtime,
new ControllerClientTrajectoryOrchestrator(),
new RobotConfigLoader());
service.SetUpRobot("FANUC_LR_Mate_200iD");
service.Connect("192.168.10.101");
service.EnableRobot(2);
service.SetSpeedRatio(speedRatio);
// 两个公开入口必须共享相同的点对点限速,且采用当前倍率而非配置默认倍率。
if (moveToStart)
{
service.UploadTrajectory(TestRobotFactory.CreateUploadedTrajectoryWithSingleShot());
service.ExecuteTrajectoryByName("demo-flyshot", new FlyshotExecutionOptions(moveToStart: true, useCache: false));
}
else
{
service.MoveJoint(targetJoints);
}
var robot = new ControllerClientCompatRobotCatalog(options, new RobotModelLoader())
.LoadProfile("FANUC_LR_Mate_200iD", accLimitScale: 1.0, jerkLimitScale: 1.0);
var expected = MoveJointTrajectoryGenerator.CreateResult(robot, startJoints, targetJoints, planningSpeedScale * speedRatio);
var actual = runtime.ExecuteCalls[0].Result;
Assert.Equal(expected.Duration, actual.Duration);
Assert.Equal(expected.DenseJointTrajectory!.Count, actual.DenseJointTrajectory!.Count);
Assert.Equal(targetJoints, runtime.ExecuteCalls[0].FinalJointPositions);
Assert.Equal(speedRatio, runtime.GetSnapshot().SpeedRatio);
Assert.False(runtime.SecondTrajectoryStartedBeforeFirstMotionCompleted);
}
finally
{
Directory.Delete(configRoot, recursive: true);
}
}
/// <summary> /// <summary>
/// 验证 ExecuteFlyShotTraj(move_to_start=true) 会先执行稠密 PTP 到起点,并等待该段运动完成后再启动飞拍轨迹。 /// 验证 ExecuteFlyShotTraj(move_to_start=true) 会先执行稠密 PTP 到起点,并等待该段运动完成后再启动飞拍轨迹。
/// </summary> /// </summary>
@@ -849,7 +876,6 @@ public sealed class RuntimeOrchestrationTests
new RobotConfigLoader()); new RobotConfigLoader());
service.SetUpRobot("FANUC_LR_Mate_200iD"); service.SetUpRobot("FANUC_LR_Mate_200iD");
service.SetActiveController(sim: false);
service.Connect("192.168.10.101"); service.Connect("192.168.10.101");
service.EnableRobot(2); service.EnableRobot(2);
service.UploadTrajectory(TestRobotFactory.CreateUploadedTrajectoryWithSingleShot()); service.UploadTrajectory(TestRobotFactory.CreateUploadedTrajectoryWithSingleShot());
@@ -894,7 +920,6 @@ public sealed class RuntimeOrchestrationTests
new RobotConfigLoader()); new RobotConfigLoader());
service.SetUpRobot("FANUC_LR_Mate_200iD"); service.SetUpRobot("FANUC_LR_Mate_200iD");
service.SetActiveController(sim: false);
service.Connect("192.168.10.101"); service.Connect("192.168.10.101");
service.EnableRobot(2); service.EnableRobot(2);
service.UploadTrajectory(TestRobotFactory.CreateUploadedTrajectoryWithSingleShot()); service.UploadTrajectory(TestRobotFactory.CreateUploadedTrajectoryWithSingleShot());
@@ -938,7 +963,6 @@ public sealed class RuntimeOrchestrationTests
new RobotConfigLoader()); new RobotConfigLoader());
service.SetUpRobot("FANUC_LR_Mate_200iD"); service.SetUpRobot("FANUC_LR_Mate_200iD");
service.SetActiveController(sim: false);
service.Connect("192.168.10.101"); service.Connect("192.168.10.101");
service.EnableRobot(2); service.EnableRobot(2);
service.UploadTrajectory(TestRobotFactory.CreateUploadedTrajectoryWithSingleShot()); service.UploadTrajectory(TestRobotFactory.CreateUploadedTrajectoryWithSingleShot());
@@ -979,7 +1003,6 @@ public sealed class RuntimeOrchestrationTests
new RobotConfigLoader()); new RobotConfigLoader());
service.SetUpRobot("FANUC_LR_Mate_200iD"); service.SetUpRobot("FANUC_LR_Mate_200iD");
service.SetActiveController(sim: false);
service.Connect("192.168.10.101"); service.Connect("192.168.10.101");
service.EnableRobot(2); service.EnableRobot(2);
service.UploadTrajectory(TestRobotFactory.CreateUploadedTrajectoryWithSingleShot()); service.UploadTrajectory(TestRobotFactory.CreateUploadedTrajectoryWithSingleShot());
@@ -1030,7 +1053,6 @@ public sealed class RuntimeOrchestrationTests
new RobotConfigLoader()); new RobotConfigLoader());
service.SetUpRobot("FANUC_LR_Mate_200iD"); service.SetUpRobot("FANUC_LR_Mate_200iD");
service.SetActiveController(sim: false);
service.Connect("192.168.10.101"); service.Connect("192.168.10.101");
service.EnableRobot(2); service.EnableRobot(2);
service.UploadTrajectory(TestRobotFactory.CreateUploadedTrajectoryWithSingleShot()); service.UploadTrajectory(TestRobotFactory.CreateUploadedTrajectoryWithSingleShot());
@@ -1081,7 +1103,6 @@ public sealed class RuntimeOrchestrationTests
new RobotConfigLoader()); new RobotConfigLoader());
service.SetUpRobot("FANUC_LR_Mate_200iD"); service.SetUpRobot("FANUC_LR_Mate_200iD");
service.SetActiveController(sim: false);
service.Connect("192.168.10.101"); service.Connect("192.168.10.101");
service.EnableRobot(2); service.EnableRobot(2);
service.UploadTrajectory(TestRobotFactory.CreateUploadedTrajectoryWithSingleShot()); service.UploadTrajectory(TestRobotFactory.CreateUploadedTrajectoryWithSingleShot());
@@ -1240,8 +1261,8 @@ public sealed class RuntimeOrchestrationTests
$"ActualSend 应按当前 speedRatio 的执行视图导出,不能再用旧 JointDetialTraj 规划时长约束点数。actual={pointRows.Length}, planningRows={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.5, timingRows[1][4], precision: 6);
Assert.Equal(0.0, timingRows[1][5], precision: 6); Assert.Equal(0.0, timingRows[1][5], precision: 6);
Assert.Contains("\"trigger_window_seconds\": 0.1", shotEventsJson); Assert.Contains("\"trigger_window_seconds\": 0.1", shotEventsJson);
@@ -1371,6 +1392,142 @@ public sealed class RuntimeOrchestrationTests
return configRoot; return configRoot;
} }
/// <summary>
/// 验证未加载机器人模型时仍可上传、列出和删除飞拍资产。
/// </summary>
[Fact]
public void ControllerClientCompatService_ManagesFlyshotAssetsWithoutRobotSetup()
{
var configRoot = CreateTempConfigRoot();
try
{
var options = new ControllerClientCompatOptions { ConfigRoot = configRoot };
var service = new ControllerClientCompatService(
options,
new ControllerClientCompatRobotCatalog(options, new RobotModelLoader()),
new RecordingControllerRuntime(),
new ControllerClientTrajectoryOrchestrator(),
new RobotConfigLoader());
service.UploadTrajectory(TestRobotFactory.CreateUploadedTrajectoryWithSingleShot());
Assert.Contains("demo-flyshot", service.ListTrajectoryNames());
Assert.Contains(
"\"demo-flyshot\"",
File.ReadAllText(Path.Combine(configRoot, "RobotConfig.json")),
StringComparison.Ordinal);
service.DeleteTrajectory("demo-flyshot");
Assert.DoesNotContain("demo-flyshot", service.ListTrajectoryNames());
}
finally
{
Directory.Delete(configRoot, recursive: true);
}
}
/// <summary>
/// 验证重新加载机器人模型不会清空已经上传的飞拍资产。
/// </summary>
[Fact]
public void ControllerClientCompatService_SetUpRobot_PreservesUploadedFlyshotAssets()
{
var configRoot = CreateTempConfigRoot();
try
{
var options = new ControllerClientCompatOptions { ConfigRoot = configRoot };
var service = new ControllerClientCompatService(
options,
new ControllerClientCompatRobotCatalog(options, new RobotModelLoader()),
new RecordingControllerRuntime(),
new ControllerClientTrajectoryOrchestrator(),
new RobotConfigLoader());
service.UploadTrajectory(TestRobotFactory.CreateUploadedTrajectoryWithSingleShot());
service.SetUpRobot("FANUC_LR_Mate_200iD");
Assert.Contains("demo-flyshot", service.ListTrajectoryNames());
Assert.Equal("OfflineReady", service.GetStatusSnapshotMetadata().OperatingMode);
Assert.True(service.GetStatusSnapshotMetadata().CanPlanOffline);
Assert.False(service.GetStatusSnapshotMetadata().CanExecuteRealMotion);
}
finally
{
Directory.Delete(configRoot, recursive: true);
}
}
/// <summary>
/// 验证离线状态可以生成完整最终发送点列,并返回导出文件绝对路径。
/// </summary>
[Fact]
public void ControllerClientCompatService_SaveTrajectoryInfo_ReturnsOfflinePlanningResult()
{
var configRoot = CreateTempConfigRoot();
WriteRobotConfigWithDemoTrajectory(configRoot, planningSpeedScale: 0.45, speedRatio: 1.0);
try
{
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");
var result = service.SaveTrajectoryInfo("demo-flyshot");
Assert.True(result.IsValid);
Assert.Equal("demo-flyshot", result.TrajectoryName);
Assert.Equal(1.0, result.SpeedRatio, precision: 6);
Assert.NotEmpty(result.FinalSendPoints);
Assert.NotEmpty(result.ShotEvents);
Assert.NotEmpty(result.CartesianTrajectory);
Assert.True(result.Diagnostics.MaxAbsVelocityDegreesPerSecond > 0.0);
Assert.All(result.ArtifactPaths.Values, path => Assert.True(Path.IsPathFullyQualified(path)));
}
finally
{
Directory.Delete(configRoot, recursive: true);
}
}
/// <summary>
/// 验证模型加载成功但真机连接失败时,状态降级为 OfflineReady 并保留离线规划能力。
/// </summary>
[Fact]
public void ControllerClientCompatService_InitializeRobot_ConnectionFailureKeepsOfflineReady()
{
var configRoot = CreateTempConfigRoot();
try
{
var options = new ControllerClientCompatOptions { ConfigRoot = configRoot };
var runtime = new RecordingControllerRuntime { ThrowOnConnect = true };
var service = new ControllerClientCompatService(
options,
new ControllerClientCompatRobotCatalog(options, new RobotModelLoader()),
runtime,
new ControllerClientTrajectoryOrchestrator(),
new RobotConfigLoader());
Assert.Throws<InvalidOperationException>(
() => service.InitializeRobot("FANUC_LR_Mate_200iD", "192.168.10.101"));
var metadata = service.GetStatusSnapshotMetadata();
Assert.Equal("OfflineReady", metadata.OperatingMode);
Assert.True(metadata.CanPlanOffline);
Assert.False(metadata.CanExecuteRealMotion);
Assert.NotNull(metadata.LastConnectionError);
Assert.NotNull(metadata.LastConnectionErrorAt);
}
finally
{
Directory.Delete(configRoot, recursive: true);
}
}
/// <summary> /// <summary>
/// 写入包含一条飞拍轨迹的最小 RobotConfig.json,供兼容服务从统一配置恢复轨迹。 /// 写入包含一条飞拍轨迹的最小 RobotConfig.json,供兼容服务从统一配置恢复轨迹。
/// </summary> /// </summary>
@@ -1952,7 +2109,7 @@ internal static class TestRobotFactory
return new ControllerClientCompatService( return new ControllerClientCompatService(
options, options,
new ControllerClientCompatRobotCatalog(options, new RobotModelLoader()), new ControllerClientCompatRobotCatalog(options, new RobotModelLoader()),
new FanucControllerRuntime(), new RecordingControllerRuntime(),
new ControllerClientTrajectoryOrchestrator(), new ControllerClientTrajectoryOrchestrator(),
new RobotConfigLoader()); new RobotConfigLoader());
} }
@@ -2024,6 +2181,11 @@ internal sealed class RecordingControllerRuntime : IControllerRuntime
/// </summary> /// </summary>
public TrajectoryResult? LastExecutedResult { get; private set; } public TrajectoryResult? LastExecutedResult { get; private set; }
/// <summary>
/// 获取或设置连接时是否抛出测试异常。
/// </summary>
public bool ThrowOnConnect { get; set; }
/// <inheritdoc /> /// <inheritdoc />
public void ResetRobot(RobotProfile robot, string robotName) public void ResetRobot(RobotProfile robot, string robotName)
{ {
@@ -2038,6 +2200,10 @@ internal sealed class RecordingControllerRuntime : IControllerRuntime
/// <inheritdoc /> /// <inheritdoc />
public void Connect(string robotIp) public void Connect(string robotIp)
{ {
if (ThrowOnConnect)
{
throw new InvalidOperationException("测试连接失败。");
}
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -2123,6 +2289,10 @@ internal sealed class RecordingControllerRuntime : IControllerRuntime
/// </summary> /// </summary>
internal sealed class DelayedCompletionControllerRuntime : IControllerRuntime internal sealed class DelayedCompletionControllerRuntime : IControllerRuntime
{ {
/// <summary>
/// 当前测试运行速度倍率,用于验证初始化与运行中调速对预移动规划的影响。
/// </summary>
private double _speedRatio = 1.0;
private readonly object _lock = new(); private readonly object _lock = new();
private readonly TimeSpan _firstMotionCompletionDelay; private readonly TimeSpan _firstMotionCompletionDelay;
private double[] _jointPositions; private double[] _jointPositions;
@@ -2195,11 +2365,12 @@ internal sealed class DelayedCompletionControllerRuntime : IControllerRuntime
} }
/// <inheritdoc /> /// <inheritdoc />
public double GetSpeedRatio() => 1.0; public double GetSpeedRatio() => _speedRatio;
/// <inheritdoc /> /// <inheritdoc />
public void SetSpeedRatio(double ratio) public void SetSpeedRatio(double ratio)
{ {
_speedRatio = ratio;
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -2240,7 +2411,7 @@ internal sealed class DelayedCompletionControllerRuntime : IControllerRuntime
connectionState: "Connected", connectionState: "Connected",
isEnabled: _isEnabled, isEnabled: _isEnabled,
isInMotion: _isInMotion, isInMotion: _isInMotion,
speedRatio: 1.0, speedRatio: _speedRatio,
jointPositions: _jointPositions.ToArray(), jointPositions: _jointPositions.ToArray(),
cartesianPose: Array.Empty<double>(), cartesianPose: Array.Empty<double>(),
activeAlarms: Array.Empty<RuntimeAlarm>()); activeAlarms: Array.Empty<RuntimeAlarm>());

View File

@@ -36,6 +36,14 @@ public sealed class DebugConsoleEndpointTests(FlyshotServerFactory factory) : IC
Assert.Contains("id=\"debug-static-docs\"", html, StringComparison.Ordinal); Assert.Contains("id=\"debug-static-docs\"", html, StringComparison.Ordinal);
Assert.Contains("POST /move_pose/", html, StringComparison.Ordinal); Assert.Contains("POST /move_pose/", html, StringComparison.Ordinal);
Assert.Contains("POST /upload_flyshot/", html, StringComparison.Ordinal); Assert.Contains("POST /upload_flyshot/", html, StringComparison.Ordinal);
Assert.Contains("POST /init_mpc_robot", html, StringComparison.Ordinal);
Assert.Contains("id=\"offline-planning-panel\"", html, StringComparison.Ordinal);
Assert.Contains("id=\"offline-trajectory-select\"", html, StringComparison.Ordinal);
Assert.Contains("生成轨迹不会向机器人发送命令", html, StringComparison.Ordinal);
Assert.DoesNotContain("/init_mpc_robt", html, StringComparison.Ordinal);
Assert.DoesNotContain("/set_active_controller/", html, StringComparison.Ordinal);
Assert.DoesNotContain("\"sim\"", html, StringComparison.Ordinal);
Assert.DoesNotContain("\"server_ip\"", html, StringComparison.Ordinal);
Assert.DoesNotContain("__SWAGGER_JSON_URL__", html, StringComparison.Ordinal); Assert.DoesNotContain("__SWAGGER_JSON_URL__", html, StringComparison.Ordinal);
Assert.Contains("/assets/debug.css", html, StringComparison.Ordinal); Assert.Contains("/assets/debug.css", html, StringComparison.Ordinal);
Assert.Contains("/assets/debug.js", html, StringComparison.Ordinal); Assert.Contains("/assets/debug.js", html, StringComparison.Ordinal);
@@ -45,6 +53,9 @@ public sealed class DebugConsoleEndpointTests(FlyshotServerFactory factory) : IC
var script = await scriptResponse.Content.ReadAsStringAsync(); var script = await scriptResponse.Content.ReadAsStringAsync();
Assert.Contains("/api/debug/config", script, StringComparison.Ordinal); Assert.Contains("/api/debug/config", script, StringComparison.Ordinal);
Assert.Contains("/list_flyShotTraj/", script, StringComparison.Ordinal);
Assert.Contains("/save_traj_info/", script, StringComparison.Ordinal);
Assert.Contains("renderOfflinePlanningResult", script, StringComparison.Ordinal);
} }
/// <summary> /// <summary>
@@ -157,11 +168,14 @@ public sealed class DebugConsoleEndpointTests(FlyshotServerFactory factory) : IC
using var document = await System.Text.Json.JsonDocument.ParseAsync(responseStream); using var document = await System.Text.Json.JsonDocument.ParseAsync(responseStream);
var paths = document.RootElement.GetProperty("paths"); var paths = document.RootElement.GetProperty("paths");
// 这些路径分别覆盖:基础探活、状态快照、版本查询、上传飞拍轨迹四种典型形态。 // 这些路径分别覆盖:基础探活、状态快照、真机初始化、上传和离线规划。
AssertPathExists(paths, "/healthz"); AssertPathExists(paths, "/healthz");
AssertPathExists(paths, "/api/status/snapshot"); AssertPathExists(paths, "/api/status/snapshot");
AssertPathExists(paths, "/get_server_version"); AssertPathExists(paths, "/init_mpc_robot");
AssertPathExists(paths, "/upload_flyshot"); AssertPathExists(paths, "/upload_flyshot");
AssertPathExists(paths, "/save_traj_info");
Assert.False(paths.TryGetProperty("/init_mpc_robt", out _));
Assert.False(paths.TryGetProperty("/set_active_controller/", out _));
} }
/// <summary> /// <summary>

View File

@@ -1,10 +1,147 @@
using System.Net; using System.Net;
using System.Text.Json; using System.Text.Json;
using Flyshot.Core.Domain;
using Flyshot.Runtime.Common;
using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
namespace Flyshot.Server.IntegrationTests; namespace Flyshot.Server.IntegrationTests;
public sealed class FlyshotServerFactory : WebApplicationFactory<Program>; /// <summary>
/// 使用可控 fake runtime 启动测试宿主,避免集成测试访问真实 FANUC 控制器。
/// </summary>
public sealed class FlyshotServerFactory : WebApplicationFactory<Program>
{
/// <inheritdoc />
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
services.RemoveAll<IControllerRuntime>();
services.AddSingleton<IControllerRuntime, IntegrationFakeControllerRuntime>();
});
}
}
/// <summary>
/// 为 HTTP 集成测试提供不访问硬件的控制器运行时。
/// </summary>
internal sealed class IntegrationFakeControllerRuntime : IControllerRuntime
{
private RobotProfile? _robot;
private bool _connected;
private bool _enabled;
private double _speedRatio = 1.0;
private double[] _tcp = [0.0, 0.0, 0.0];
private double[] _joints = Array.Empty<double>();
private double[] _pose = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0];
/// <inheritdoc />
public void ResetRobot(RobotProfile robot, string robotName)
{
_robot = robot;
_connected = false;
_enabled = false;
_joints = new double[robot.DegreesOfFreedom];
}
/// <inheritdoc />
public void Connect(string robotIp)
{
_ = _robot ?? throw new InvalidOperationException("Robot has not been setup.");
_connected = true;
}
/// <inheritdoc />
public void Disconnect()
{
_connected = false;
_enabled = false;
}
/// <inheritdoc />
public void EnableRobot(int bufferSize)
{
if (!_connected)
{
throw new InvalidOperationException("Controller has not been connected.");
}
_enabled = true;
}
/// <inheritdoc />
public void DisableRobot() => _enabled = false;
/// <inheritdoc />
public void StopMove()
{
}
/// <inheritdoc />
public double GetSpeedRatio() => _speedRatio;
/// <inheritdoc />
public void SetSpeedRatio(double ratio) => _speedRatio = ratio;
/// <inheritdoc />
public IReadOnlyList<double> GetTcp() => _tcp.ToArray();
/// <inheritdoc />
public void SetTcp(double x, double y, double z) => _tcp = [x, y, z];
/// <inheritdoc />
public bool GetIo(int port, string ioType) => false;
/// <inheritdoc />
public void SetIo(int port, bool value, string ioType)
{
}
/// <inheritdoc />
public IReadOnlyList<double> GetJointPositions() => _joints.ToArray();
/// <inheritdoc />
public IReadOnlyList<double> GetPose() => _pose.ToArray();
/// <inheritdoc />
public ControllerStateSnapshot GetSnapshot()
{
return new ControllerStateSnapshot(
DateTimeOffset.UtcNow,
_connected ? "Connected" : "Disconnected",
_enabled,
false,
_speedRatio,
_joints,
_pose,
Array.Empty<RuntimeAlarm>());
}
/// <inheritdoc />
public void ExecuteTrajectory(TrajectoryResult result, IReadOnlyList<double> finalJointPositions)
{
if (!_enabled)
{
throw new InvalidOperationException("Robot has not been enabled.");
}
_joints = finalJointPositions.ToArray();
}
/// <inheritdoc />
public void ExecuteCartesianTrajectory(TrajectoryResult result, IReadOnlyList<double> finalPose)
{
if (!_enabled)
{
throw new InvalidOperationException("Robot has not been enabled.");
}
_pose = [.. finalPose, 1.0];
}
}
public sealed class HealthEndpointTests(FlyshotServerFactory factory) : IClassFixture<FlyshotServerFactory> public sealed class HealthEndpointTests(FlyshotServerFactory factory) : IClassFixture<FlyshotServerFactory>
{ {

View File

@@ -11,18 +11,39 @@ namespace Flyshot.Server.IntegrationTests;
public sealed class LegacyHttpApiCompatibilityTests(FlyshotServerFactory factory) : IClassFixture<FlyshotServerFactory> public sealed class LegacyHttpApiCompatibilityTests(FlyshotServerFactory factory) : IClassFixture<FlyshotServerFactory>
{ {
/// <summary> /// <summary>
/// 验证根路径会返回旧 HTTP 服务使用的 Hello World JSON,而不是跳转到健康检查页。 /// 验证计划中明确移除的旧路由不会再出现在宿主中。
/// </summary>
/// <param name="method">请求方法。</param>
/// <param name="path">已移除路由。</param>
[Theory]
[InlineData("GET", "/")]
[InlineData("POST", "/connect_server/?server_ip=127.0.0.1&port=50001")]
[InlineData("GET", "/get_server_version/")]
[InlineData("GET", "/get_client_version/")]
[InlineData("POST", "/setup_robot_from_env/?env_file=test")]
[InlineData("POST", "/set_show_tcp/")]
[InlineData("GET", "/is_setup/")]
[InlineData("POST", "/set_active_controller/?sim=true")]
[InlineData("POST", "/init_mpc_robt")]
public async Task RemovedRoutes_ReturnNotFound(string method, string path)
{
using var client = factory.CreateClient();
using var request = new HttpRequestMessage(new HttpMethod(method), path);
using var response = await client.SendAsync(request);
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
/// <summary>
/// 验证已移除的旧根路径返回 404。
/// </summary> /// </summary>
[Fact] [Fact]
public async Task Root_ReturnsLegacyHelloWorldPayload() public async Task Root_ReturnsNotFound()
{ {
using var client = factory.CreateClient(); using var client = factory.CreateClient();
using var response = await client.GetAsync("/"); using var response = await client.GetAsync("/");
Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
using var json = await ReadJsonAsync(response);
Assert.Equal("Hello World", json.RootElement.GetProperty("message").GetString());
} }
/// <summary> /// <summary>
@@ -33,47 +54,7 @@ public sealed class LegacyHttpApiCompatibilityTests(FlyshotServerFactory factory
{ {
using var client = factory.CreateClient(); using var client = factory.CreateClient();
using (var connectServerResponse = await client.PostAsync("/connect_server/?server_ip=127.0.0.1&port=50001", content: null)) await InitializeRobotAsync(client);
{
Assert.Equal(HttpStatusCode.OK, connectServerResponse.StatusCode);
using var connectServerJson = await ReadJsonAsync(connectServerResponse);
Assert.Equal("connected", connectServerJson.RootElement.GetProperty("status").GetString());
}
using (var setupResponse = await client.PostAsync("/setup_robot/?robot_name=FANUC_LR_Mate_200iD", content: null))
{
Assert.Equal(HttpStatusCode.OK, setupResponse.StatusCode);
using var setupJson = await ReadJsonAsync(setupResponse);
Assert.Equal("robot setup", setupJson.RootElement.GetProperty("status").GetString());
}
using (var isSetupResponse = await client.GetAsync("/is_setup/"))
{
Assert.Equal(HttpStatusCode.OK, isSetupResponse.StatusCode);
using var isSetupJson = await ReadJsonAsync(isSetupResponse);
Assert.True(isSetupJson.RootElement.GetProperty("is_setup").GetBoolean());
}
using (var activeControllerResponse = await client.PostAsync("/set_active_controller/?sim=true", content: null))
{
Assert.Equal(HttpStatusCode.OK, activeControllerResponse.StatusCode);
using var activeControllerJson = await ReadJsonAsync(activeControllerResponse);
Assert.Equal("active controller set", activeControllerJson.RootElement.GetProperty("status").GetString());
}
using (var connectRobotResponse = await client.PostAsync("/connect_robot/?ip=192.168.10.101", content: null))
{
Assert.Equal(HttpStatusCode.OK, connectRobotResponse.StatusCode);
using var connectRobotJson = await ReadJsonAsync(connectRobotResponse);
Assert.Equal("robot connected", connectRobotJson.RootElement.GetProperty("status").GetString());
}
using (var enableRobotResponse = await client.GetAsync("/enable_robot/"))
{
Assert.Equal(HttpStatusCode.OK, enableRobotResponse.StatusCode);
using var enableRobotJson = await ReadJsonAsync(enableRobotResponse);
Assert.True(enableRobotJson.RootElement.GetProperty("enable_robot").GetBoolean());
}
using (var robotInfoResponse = await client.GetAsync("/robot_info/")) using (var robotInfoResponse = await client.GetAsync("/robot_info/"))
{ {
@@ -207,6 +188,10 @@ public sealed class LegacyHttpApiCompatibilityTests(FlyshotServerFactory factory
{ {
using var client = factory.CreateClient(); using var client = factory.CreateClient();
await InitializeRobotAsync(client); await InitializeRobotAsync(client);
using (var speedResponse = await client.PostAsJsonAsync("/set_speedRatio/", new { speed = 0.5 }))
{
Assert.Equal(HttpStatusCode.OK, speedResponse.StatusCode);
}
var uploadPayload = new var uploadPayload = new
{ {
@@ -222,10 +207,10 @@ public sealed class LegacyHttpApiCompatibilityTests(FlyshotServerFactory factory
shot_flags = new[] { false, true, false, false }, shot_flags = new[] { false, true, false, false },
waypoints = new[] waypoints = new[]
{ {
new[] { 0.1, 0.2, 0.3, 0.4, 0.5, 0.6 }, new[] { 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 },
new[] { 0.2, 0.2, 0.3, 0.4, 0.5, 0.6 }, new[] { 0.001, 0.0, 0.0, 0.0, 0.0, 0.0 },
new[] { 0.3, 0.2, 0.3, 0.4, 0.5, 0.6 }, new[] { 0.002, 0.0, 0.0, 0.0, 0.0, 0.0 },
new[] { 0.4, 0.2, 0.3, 0.4, 0.5, 0.6 } new[] { 0.003, 0.0, 0.0, 0.0, 0.0, 0.0 }
} }
}; };
@@ -282,7 +267,8 @@ public sealed class LegacyHttpApiCompatibilityTests(FlyshotServerFactory factory
{ {
Assert.Equal(HttpStatusCode.OK, saveInfoResponse.StatusCode); Assert.Equal(HttpStatusCode.OK, saveInfoResponse.StatusCode);
using var saveInfoJson = await ReadJsonAsync(saveInfoResponse); using var saveInfoJson = await ReadJsonAsync(saveInfoResponse);
Assert.True(saveInfoJson.RootElement.GetProperty("success").GetBoolean()); Assert.True(saveInfoJson.RootElement.GetProperty("isValid").GetBoolean());
Assert.True(saveInfoJson.RootElement.GetProperty("finalSendPointCount").GetInt32() > 0);
} }
using (var deleteResponse = await client.PostAsJsonAsync("/delete_flyshot/", new { name = "demo-http-flyshot" })) using (var deleteResponse = await client.PostAsJsonAsync("/delete_flyshot/", new { name = "demo-http-flyshot" }))
@@ -298,13 +284,10 @@ public sealed class LegacyHttpApiCompatibilityTests(FlyshotServerFactory factory
/// </summary> /// </summary>
private static async Task InitializeRobotAsync(HttpClient client) private static async Task InitializeRobotAsync(HttpClient client)
{ {
using var initResponse = await client.PostAsJsonAsync("/init_mpc_robt", new using var initResponse = await client.PostAsJsonAsync("/init_mpc_robot", new
{ {
server_ip = "127.0.0.1",
port = 50001,
robot_name = "FANUC_LR_Mate_200iD", robot_name = "FANUC_LR_Mate_200iD",
robot_ip = "192.168.10.101", robot_ip = "192.168.10.101"
sim = true
}); });
Assert.Equal(HttpStatusCode.OK, initResponse.StatusCode); Assert.Equal(HttpStatusCode.OK, initResponse.StatusCode);

View File

@@ -113,13 +113,15 @@ public sealed class StatusEndpointTests(FlyshotServerFactory factory) : IClassFi
Assert.Equal("ok", root.GetProperty("status").GetString()); Assert.Equal("ok", root.GetProperty("status").GetString());
Assert.True(root.GetProperty("isSetup").GetBoolean()); Assert.True(root.GetProperty("isSetup").GetBoolean());
Assert.Equal("RealReady", root.GetProperty("operatingMode").GetString());
Assert.True(root.GetProperty("canPlanOffline").GetBoolean());
Assert.True(root.GetProperty("canExecuteRealMotion").GetBoolean());
Assert.Equal("FANUC_LR_Mate_200iD", root.GetProperty("robotName").GetString()); Assert.Equal("FANUC_LR_Mate_200iD", root.GetProperty("robotName").GetString());
Assert.Equal(6, root.GetProperty("degreesOfFreedom").GetInt32()); Assert.Equal(6, root.GetProperty("degreesOfFreedom").GetInt32());
var uploadedTrajectories = root.GetProperty("uploadedTrajectories") var uploadedTrajectories = root.GetProperty("uploadedTrajectories")
.EnumerateArray() .EnumerateArray()
.Select(static value => value.GetString()) .Select(static value => value.GetString())
.ToArray(); .ToArray();
Assert.Contains("20251015", uploadedTrajectories);
Assert.Contains("UTTC_MS11", uploadedTrajectories); Assert.Contains("UTTC_MS11", uploadedTrajectories);
Assert.Equal("Connected", snapshot.GetProperty("connectionState").GetString()); Assert.Equal("Connected", snapshot.GetProperty("connectionState").GetString());
Assert.True(snapshot.GetProperty("isEnabled").GetBoolean()); Assert.True(snapshot.GetProperty("isEnabled").GetBoolean());
@@ -139,11 +141,6 @@ public sealed class StatusEndpointTests(FlyshotServerFactory factory) : IClassFi
Assert.Equal(HttpStatusCode.OK, setupResponse.StatusCode); Assert.Equal(HttpStatusCode.OK, setupResponse.StatusCode);
} }
using (var activeControllerResponse = await client.PostAsync("/set_active_controller/?sim=true", content: null))
{
Assert.Equal(HttpStatusCode.OK, activeControllerResponse.StatusCode);
}
using (var connectRobotResponse = await client.PostAsync("/connect_robot/?ip=192.168.10.101", content: null)) using (var connectRobotResponse = await client.PostAsync("/connect_robot/?ip=192.168.10.101", content: null))
{ {
Assert.Equal(HttpStatusCode.OK, connectRobotResponse.StatusCode); Assert.Equal(HttpStatusCode.OK, connectRobotResponse.StatusCode);