✨ feat: 实现 ControllerClient HTTP 兼容层及 FANUC 运行时
- 新增 Flyshot.ControllerClientCompat 兼容层模块 - 新增 Flyshot.Runtime.Fanuc 运行时模块 - 新增 LegacyHttpApiController 暴露 HTTP 兼容 API - 补充 RuntimeOrchestrationTests 等测试覆盖 - 补充 docs/ 兼容性需求与逆向工程文档 - 更新 Host 注册、配置及解决方案引用 变更概览: - Flyshot.ControllerClientCompat — 旧 ControllerClient 语义的 HTTP 适配 - Flyshot.Runtime.Fanuc — IControllerRuntime 的 FANUC 真机实现 - LegacyHttpApiController — HTTP API 兼容旧 SDK - docs/ — 兼容性需求与逆向工程分析文档 - 测试:RuntimeOrchestrationTests、LegacyHttpApiCompatibilityTests
This commit is contained in:
30
AGENTS.md
30
AGENTS.md
@@ -37,7 +37,12 @@
|
||||
flyshot-replacement/
|
||||
├─ src/
|
||||
│ ├─ Flyshot.Server.Host/
|
||||
│ ├─ Flyshot.ControllerClientCompat/
|
||||
│ ├─ Flyshot.Core.Config/
|
||||
│ ├─ Flyshot.Core.Domain/
|
||||
│ ├─ Flyshot.Core.Planning/
|
||||
│ ├─ Flyshot.Core.Triggering/
|
||||
│ ├─ Flyshot.Runtime.Fanuc/
|
||||
│ └─ Flyshot.Runtime.Common/
|
||||
├─ tests/
|
||||
│ ├─ Flyshot.Server.IntegrationTests/
|
||||
@@ -64,8 +69,10 @@ flyshot-replacement/
|
||||
- `Flyshot.Core.Triggering`
|
||||
- `TrajectoryDO` 等价时间轴
|
||||
- `shot_flags / offset_values / addr` 解析
|
||||
- `Flyshot.LegacyGateway`
|
||||
- `50001/TCP+JSON` 兼容接入
|
||||
- `Flyshot.ControllerClientCompat`
|
||||
- HTTP 控制器后端兼容服务
|
||||
- 旧 `ControllerClient` 语义适配
|
||||
- 不启动 `50001/TCP+JSON` 监听
|
||||
- `Flyshot.Runtime.Fanuc`
|
||||
- `10010 / 10012 / 60015`
|
||||
- `Flyshot.Web.Status`
|
||||
@@ -135,6 +142,20 @@ flyshot-replacement/
|
||||
- `../analysis/FANUC_realtime_comm_analysis.md`
|
||||
- `../FlyingShot/FlyingShot/Include/ControllerClient/ControllerClient.h`
|
||||
|
||||
### 6.1 父目录资料引用约定
|
||||
|
||||
- 日常开发、测试和 Codex 会话默认从 `flyshot-replacement/` 根目录启动。
|
||||
- 当前仓库内的 `@` 引用默认只覆盖本仓库文件,不要假设它能索引父目录资料。
|
||||
- 引用父目录资料时,统一直接写明确路径,优先使用相对路径,例如:
|
||||
- `../analysis/ICSP_algorithm_reverse_analysis.md`
|
||||
- `../analysis/ControllerServer_analysis.md`
|
||||
- `../FlyingShot/FlyingShot/Include/ControllerClient/ControllerClient.h`
|
||||
- 当路径较长或跨工具复制时,可以使用绝对路径,但在文档和注释中优先保留相对路径写法,便于仓库整体搬迁。
|
||||
- 父目录中的 `analysis/`、`FlyingShot/`、`RobotController/`、`RPS/` 默认视为参考资料区,不在这些目录中继续落地新实现。
|
||||
- 新实现、测试、兼容层代码、设计文档和运行说明,都应优先写入 `flyshot-replacement/` 内部。
|
||||
- 如果父目录资料中的某段结论会长期影响本仓库实现,应在本仓库 `docs/` 中补充归纳说明,并标明来源路径,而不是要求后续开发反复回看聊天记录。
|
||||
- 如果需要引用父目录样本文件做测试输入,优先通过只读方式加载;只有在测试需要固化样本且样本已明确收敛时,才复制到本仓库测试数据目录。
|
||||
|
||||
## 7. 任务推进方式
|
||||
|
||||
- `README.md` 中的 Todo 需要随着阶段推进同步更新。
|
||||
@@ -148,3 +169,8 @@ flyshot-replacement/
|
||||
- `Flyshot.Server.Host` 已提供最小 `/healthz`。
|
||||
- 最小集成测试已通过。
|
||||
- 解决方案构建已通过。
|
||||
- HTTP-only `ControllerClientCompat` 已覆盖旧 HTTP 控制器后端的主要兼容语义。
|
||||
- `Flyshot.Core.Planning` 已落地 `icsp` 与 `self-adapt-icsp` 的最小规划链路。
|
||||
- `Flyshot.Core.Triggering` 已能从 `shot_flags / offset_values / addr` 生成触发时间轴。
|
||||
- `Flyshot.Runtime.Fanuc` 已提供状态型最小运行时骨架,供兼容服务执行规划结果。
|
||||
- `ExecuteTrajectory` / `ExecuteFlyShotTraj` 已接入 `Planning + Triggering + Runtime`,不再只是兼容层内存赋值。
|
||||
|
||||
@@ -64,8 +64,10 @@ flyshot-replacement/
|
||||
- `Flyshot.Core.Triggering`
|
||||
- `TrajectoryDO` 等价时间轴
|
||||
- `shot_flags / offset_values / addr` 解析
|
||||
- `Flyshot.LegacyGateway`
|
||||
- `50001/TCP+JSON` 兼容接入
|
||||
- `Flyshot.ControllerClientCompat`
|
||||
- HTTP 控制器后端兼容服务
|
||||
- 旧 `ControllerClient` 语义适配
|
||||
- 不启动 `50001/TCP+JSON` 监听
|
||||
- `Flyshot.Runtime.Fanuc`
|
||||
- `10010 / 10012 / 60015`
|
||||
- `Flyshot.Web.Status`
|
||||
|
||||
@@ -23,6 +23,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Flyshot.Core.Planning", "sr
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Flyshot.Core.Triggering", "src\Flyshot.Core.Triggering\Flyshot.Core.Triggering.csproj", "{E4DDC34C-9AB6-4050-A927-3DF69804708A}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Flyshot.ControllerClientCompat", "src\Flyshot.ControllerClientCompat\Flyshot.ControllerClientCompat.csproj", "{5B45CC23-3551-4D0F-B3CC-22659C2A8BA3}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Flyshot.Runtime.Fanuc", "src\Flyshot.Runtime.Fanuc\Flyshot.Runtime.Fanuc.csproj", "{B705FA6C-19CA-44A8-882C-6CE26A5379C9}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -129,6 +133,30 @@ Global
|
||||
{E4DDC34C-9AB6-4050-A927-3DF69804708A}.Release|x64.Build.0 = Release|Any CPU
|
||||
{E4DDC34C-9AB6-4050-A927-3DF69804708A}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{E4DDC34C-9AB6-4050-A927-3DF69804708A}.Release|x86.Build.0 = Release|Any CPU
|
||||
{5B45CC23-3551-4D0F-B3CC-22659C2A8BA3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{5B45CC23-3551-4D0F-B3CC-22659C2A8BA3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{5B45CC23-3551-4D0F-B3CC-22659C2A8BA3}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{5B45CC23-3551-4D0F-B3CC-22659C2A8BA3}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{5B45CC23-3551-4D0F-B3CC-22659C2A8BA3}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{5B45CC23-3551-4D0F-B3CC-22659C2A8BA3}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{5B45CC23-3551-4D0F-B3CC-22659C2A8BA3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{5B45CC23-3551-4D0F-B3CC-22659C2A8BA3}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{5B45CC23-3551-4D0F-B3CC-22659C2A8BA3}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{5B45CC23-3551-4D0F-B3CC-22659C2A8BA3}.Release|x64.Build.0 = Release|Any CPU
|
||||
{5B45CC23-3551-4D0F-B3CC-22659C2A8BA3}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{5B45CC23-3551-4D0F-B3CC-22659C2A8BA3}.Release|x86.Build.0 = Release|Any CPU
|
||||
{B705FA6C-19CA-44A8-882C-6CE26A5379C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B705FA6C-19CA-44A8-882C-6CE26A5379C9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B705FA6C-19CA-44A8-882C-6CE26A5379C9}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{B705FA6C-19CA-44A8-882C-6CE26A5379C9}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{B705FA6C-19CA-44A8-882C-6CE26A5379C9}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{B705FA6C-19CA-44A8-882C-6CE26A5379C9}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{B705FA6C-19CA-44A8-882C-6CE26A5379C9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B705FA6C-19CA-44A8-882C-6CE26A5379C9}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{B705FA6C-19CA-44A8-882C-6CE26A5379C9}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{B705FA6C-19CA-44A8-882C-6CE26A5379C9}.Release|x64.Build.0 = Release|Any CPU
|
||||
{B705FA6C-19CA-44A8-882C-6CE26A5379C9}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{B705FA6C-19CA-44A8-882C-6CE26A5379C9}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@@ -142,5 +170,7 @@ Global
|
||||
{6CC8418D-2A13-4D70-8F94-585CD71F0B74} = {CB517CF5-2EF6-43A8-B335-ABD3A6FCE3BE}
|
||||
{154CA299-80D8-4BE2-B1C9-4BC133FA8B28} = {64EABE09-B1E0-4476-A213-32C93E46E7C3}
|
||||
{E4DDC34C-9AB6-4050-A927-3DF69804708A} = {64EABE09-B1E0-4476-A213-32C93E46E7C3}
|
||||
{5B45CC23-3551-4D0F-B3CC-22659C2A8BA3} = {64EABE09-B1E0-4476-A213-32C93E46E7C3}
|
||||
{B705FA6C-19CA-44A8-882C-6CE26A5379C9} = {64EABE09-B1E0-4476-A213-32C93E46E7C3}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
||||
14
README.md
14
README.md
@@ -13,6 +13,15 @@
|
||||
|
||||
- 这是长期运行的无头后台服务,不是 GUI 桌面程序。
|
||||
- 第一版仅面向当前现场组合,后续再扩展机型与控制柜适配。
|
||||
- 当前仓库内已经移除宿主中的 `50001/TCP+JSON` 监听实现;现阶段只保留 ASP.NET Core HTTP 控制器层,以及其后端 `Flyshot.ControllerClientCompat` 兼容服务。
|
||||
- `ExecuteTrajectory` 与 `ExecuteFlyShotTraj` 已经接入 `Planning + Triggering + Runtime` 最小链路;当前 `Flyshot.Runtime.Fanuc` 仍是状态型骨架,尚未接通真实 `10010 / 10012 / 60015` 通讯。
|
||||
- `50001/TCP+JSON` 的真实兼容入口如果后续需要恢复,必须基于 `docs/controller-client-api-compatibility-requirements.md` 与 `docs/controller-client-api-reverse-engineering.md` 重新评估,而不是直接把旧的 TCP 网关方向接回宿主。
|
||||
|
||||
开发约定:
|
||||
|
||||
- 建议从 `flyshot-replacement/` 根目录启动 IDE、终端和 Codex 会话。
|
||||
- 当前仓库内的 `@` 引用主要覆盖本仓库文件;引用父目录资料时,请直接写相对路径,如 `../analysis/ICSP_algorithm_reverse_analysis.md`。
|
||||
- 父目录中的 `analysis/`、`FlyingShot/`、`RobotController/`、`RPS/` 主要作为逆向参考资料和样本来源,新实现默认只落地在当前仓库。
|
||||
|
||||
当前 Todo:
|
||||
|
||||
@@ -21,4 +30,7 @@
|
||||
- [x] 打通最小宿主与 `/healthz`
|
||||
- [x] 建立领域模型与模块边界
|
||||
- [x] 落地配置兼容与机器人模型解析
|
||||
- [ ] 落地轨迹规划、实时控制和 Web 状态页
|
||||
- [x] 落地轨迹规划与飞拍触发时间轴
|
||||
- [x] 将 `ExecuteTrajectory` / `ExecuteFlyShotTraj` 接入最小 FANUC 运行时骨架
|
||||
- [ ] 落地真实 `10010 / 10012 / 60015` FANUC 通讯
|
||||
- [ ] 落地 Web 状态页
|
||||
|
||||
135
docs/controller-client-api-compatibility-requirements.md
Normal file
135
docs/controller-client-api-compatibility-requirements.md
Normal file
@@ -0,0 +1,135 @@
|
||||
# ControllerClient API 兼容逆向约束
|
||||
|
||||
> 记录时间:2026-04-24
|
||||
> 适用仓库:`flyshot-replacement`
|
||||
> 当前阶段:已落地 HTTP-only `ControllerClientCompat` 服务,并已将轨迹执行接入规划、触发时间轴和最小运行时骨架;不实现 `50001/TCP+JSON` 监听
|
||||
|
||||
## 1. 当前目标
|
||||
|
||||
本轮目标不是直接实现 `50001/TCP+JSON` 兼容网关,而是先把旧 `ControllerClient` 暴露的公开 API 做成可执行的逆向合同。
|
||||
|
||||
本轮交付物固定为两份文档:
|
||||
|
||||
- `docs/controller-client-api-compatibility-requirements.md`
|
||||
- `docs/controller-client-api-reverse-engineering.md`
|
||||
|
||||
后续继续扩展 `Flyshot.ControllerClientCompat` 的方法覆盖、兼容测试矩阵或真实控制器联动时,必须以这两份文档为准,不再重新口头约定接口语义。
|
||||
|
||||
## 2. 范围边界
|
||||
|
||||
本轮只覆盖 `../FlyingShot/FlyingShot/Include/ControllerClient/ControllerClient.h` 中的 32 个公开方法。
|
||||
|
||||
分组如下:
|
||||
|
||||
- 传输与版本:`ConnectServer`、`GetServerVersion`、`GetClientVersion`
|
||||
- 机器人初始化:`SetUpRobot`、`SetUpRobotFromEnv`、`IsSetUp`、`SetShowTCP`、`GetName`、`GetDoF`
|
||||
- 控制器状态:`SetActiveController`、`Connect`、`Disconnect`、`EnableRobot`、`DisableRobot`、`StopMove`
|
||||
- 参数与 IO:`GetSpeedRatio`、`SetSpeedRatio`、`GetTCP`、`SetTCP`、`GetIO`、`SetIO`
|
||||
- 运动与求解:`GetJointPosition`、`GetPose`、`GetNearestIK`、`MoveJoint`、`ExecuteTrajectory`
|
||||
- 飞拍轨迹:`UploadFlyShotTraj`、`DeleteFlyShotTraj`、`ListFlyShotTraj`、`ExecuteFlyShotTraj`、`SaveTrajInfo`、`IsFlyShotTrajValid`
|
||||
|
||||
明确不在本轮范围内:
|
||||
|
||||
- `ControllerServer` 内部所有未公开 `_Xxx` 方法的完整实现复原
|
||||
- `50001` 网关代码、TCP server、JSON parser、命令路由实现
|
||||
- 真机 `10010 / 10012 / 60015` 联调
|
||||
- 抓包、hook、反汇编级协议完全坐实
|
||||
- 把当前 replacement 仓库的实现约束误写成旧系统事实
|
||||
|
||||
## 3. 证据源优先级
|
||||
|
||||
逆向结论必须按以下优先级交叉确认:
|
||||
|
||||
1. `../FlyingShot/FlyingShot/Include/ControllerClient/ControllerClient.h`
|
||||
2. `../FlyingShot/FlyingShot/Example/UseControllerClient.cpp`
|
||||
3. `../FlyingShot/FlyingShot/Example/UseControllerClient.py`
|
||||
4. `../FlyingShot/FlyingShot/Example/UseRealRobot.py`
|
||||
5. `../FlyingShot/FlyingShot/Docs/用户手册/FANUC飞拍软件及SDK用户手册.md`
|
||||
6. `../analysis/ControllerServer_analysis.md`
|
||||
7. `../analysis/CommonMsg_protocol_analysis.md`
|
||||
8. `../analysis/Trajectory_generation_algorithm_analysis.md`
|
||||
9. `../FlyingShot/FlyingShot/Lib/libControllerClient.so` 与 `../FlyingShot/FlyingShot/Python/ControllerServer/ControllerServer.cpython-37m-x86_64-linux-gnu.so` 的字符串证据
|
||||
|
||||
使用规则:
|
||||
|
||||
- 头文件负责定义“公开合同”:方法名、参数名、默认值、返回类型。
|
||||
- 示例和手册负责定义“典型调用方式”:调用顺序、Python 包装形态、用户侧常见用法。
|
||||
- 逆向分析文档和二进制字符串负责定义“服务端映射”和“协议线索”:`_Xxx` 方法名、错误文本、命令名、部分字段顺序。
|
||||
- 不能从当前 replacement 仓库的未来设计反推旧系统事实。
|
||||
|
||||
## 4. 文档填写规则
|
||||
|
||||
主归档文档 `docs/controller-client-api-reverse-engineering.md` 对每个 API 必须固定填写以下项目:
|
||||
|
||||
- C++ 公开签名
|
||||
- Python 包装形态
|
||||
- 服务端归属
|
||||
- 协议 / 命令线索
|
||||
- 返回值与默认值
|
||||
- 典型工作流位置
|
||||
- 证据来源
|
||||
- 置信度
|
||||
- 待确认点
|
||||
|
||||
填写约束:
|
||||
|
||||
- 只要精确 JSON 包结构还未恢复,就明确写成“命令名已知,JSON envelope 未恢复”。
|
||||
- 只要 Python 失败路径没有样例,就不能假装已经确认失败时的返回形态。
|
||||
- `GetServerVersion` 这类没有显式 `_GetServerVersion` 的接口,必须标成“协议分发层行为”,不能伪造一个服务端实现名。
|
||||
- `ConnectServer` 与 `GetClientVersion` 必须明确标成客户端侧行为,不写进服务端命令表。
|
||||
- 对 `GetJointPosition -> _GetJointPositions` 这种命名不一致,要单独注明。
|
||||
- 当同一结论同时来自头文件、示例和字符串时,置信度可标为“高”;只有示例间接体现时,最多“中”。
|
||||
|
||||
## 5. 验收条件
|
||||
|
||||
本轮逆向归档完成时,至少满足:
|
||||
|
||||
- 32 个公开方法全部覆盖,且每个方法只出现一次
|
||||
- 29 个服务端相关 API 有明确映射或明确写成协议分发层行为
|
||||
- `ConnectServer`、`GetClientVersion` 两个客户端侧行为被明确排除在服务端命令表外
|
||||
- 四条工作流附录完整:
|
||||
- 初始化工作流
|
||||
- 控制器状态工作流
|
||||
- 普通轨迹工作流
|
||||
- 飞拍轨迹工作流
|
||||
- 已知高置信协议字段至少记录:
|
||||
- `GetSpeedRatio`
|
||||
- `SetSpeedRatio`
|
||||
- `GetIO`
|
||||
- `SetIO`
|
||||
- 仍未恢复的部分必须进入“待确认问题”清单,不能被静默略过
|
||||
|
||||
## 6. 当前已确认摘要
|
||||
|
||||
当前已确认的高价值结论如下:
|
||||
|
||||
- `ControllerClient.h` 中共有 32 个公开方法。
|
||||
- 其中 29 个方法可与 `ControllerServer` 的公开 `_Xxx` 方法一一对齐。
|
||||
- `GetServerVersion` 能看到明确字符串证据,但未恢复到显式 `_GetServerVersion` 实现,更接近 `_ClientCB` / `_IsJsonValid` 所在的协议分发层。
|
||||
- `ConnectServer` 是客户端建立到 `127.0.0.1:50001` 的传输层动作。
|
||||
- `GetClientVersion` 更像客户端库自身版本查询,不进入服务端命令表。
|
||||
- `GetSpeedRatio` / `SetSpeedRatio`、`GetIO` / `SetIO` 已有较高置信度的底层字段顺序与 `MsgID` 证据。
|
||||
- `UploadFlyShotTraj`、`ListFlyShotTraj` 存在额外字符串线索:
|
||||
- `StartUploadFlyShotTraj`
|
||||
- `EndUploadFlyShotTraj`
|
||||
- `GetNextListFlyShotTraj`
|
||||
- `ExecuteFlyShotTraj`、`IsFlyShotTrajValid`、`SaveTrajInfo` 在工作流上有清晰分工,不能混成一个“执行轨迹”接口。
|
||||
|
||||
## 7. 下轮实现约束
|
||||
|
||||
当前 HTTP-only 兼容层已经可以承接公开 API 的主要服务端语义,并且 `ExecuteTrajectory` / `ExecuteFlyShotTraj` 已经进入 replacement 自身的规划与运行时链路。后续扩展必须遵守:
|
||||
|
||||
- 先以逆向合同建命令表,再写 `TCP + JSON` 入口
|
||||
- 先做兼容测试矩阵,再补最小命令桩
|
||||
- 区分“旧系统事实”和“replacement 当前策略”
|
||||
- 真机未接通前,允许实现层返回稳定错误或模拟状态,但不能反过来污染逆向文档
|
||||
|
||||
## 8. 当前 replacement 实现状态
|
||||
|
||||
以下内容是当前新实现的状态,不反推为旧系统事实:
|
||||
|
||||
- `Flyshot.ControllerClientCompat` 继续作为 HTTP 控制器后端兼容服务,不启动 `50001/TCP+JSON` 监听。
|
||||
- `ExecuteTrajectory` 会先通过 `ICspPlanner` 规划普通轨迹,再把 `TrajectoryResult` 和最终关节位置交给 `IControllerRuntime`。
|
||||
- `ExecuteFlyShotTraj` 会从上传轨迹目录取出轨迹,通过 `SelfAdaptIcspPlanner` 规划并用 `ShotTimelineBuilder` 生成 `ShotEvent` / `TrajectoryDoEvent`。
|
||||
- `Flyshot.Runtime.Fanuc` 当前只保存连接、使能、速度、IO、TCP、关节位置和执行结果状态;真实 `10010 / 10012 / 60015` Socket 通讯尚未落地。
|
||||
- `MoveJoint` 仍保持旧兼容语义中的直接运动接口,但状态写入已经统一经过运行时,而不是由兼容服务自己维护关节数组。
|
||||
561
docs/controller-client-api-reverse-engineering.md
Normal file
561
docs/controller-client-api-reverse-engineering.md
Normal file
@@ -0,0 +1,561 @@
|
||||
# ControllerClient API 全量逆向归档
|
||||
|
||||
> 记录时间:2026-04-24
|
||||
> 适用仓库:`flyshot-replacement`
|
||||
> 目标:为后续 `50001/TCP+JSON` 兼容网关实现提供只读合同
|
||||
|
||||
## 1. 总览
|
||||
|
||||
本次归档覆盖 `../FlyingShot/FlyingShot/Include/ControllerClient/ControllerClient.h` 中全部 32 个公开方法。
|
||||
|
||||
当前分类结果:
|
||||
|
||||
- 29 个 API 可与 `ControllerServer` 公开 `_Xxx` 方法一一映射
|
||||
- 1 个 API 属于协议分发层行为:`GetServerVersion`
|
||||
- 2 个 API 属于客户端侧行为:`ConnectServer`、`GetClientVersion`
|
||||
|
||||
本文档中的“协议 / 命令线索”有两个层次:
|
||||
|
||||
- 命令名:已从头文件、字符串或逆向文档坐实,例如 `SetUpRobot`、`GetIO`
|
||||
- JSON envelope:请求 JSON 的精确键名、必填规则、整体结构;当前大多仍未完全恢复
|
||||
|
||||
因此,除 `GetSpeedRatio` / `SetSpeedRatio` / `GetIO` / `SetIO` 等已在命令通道层坐实字段顺序的接口外,其余接口默认只写“命令名已知,精确 JSON 包结构待确认”。
|
||||
|
||||
置信度定义:
|
||||
|
||||
- 高:头文件 + 示例/手册 + 字符串/逆向文档三方交叉确认
|
||||
- 中:头文件 + 示例,或头文件 + 字符串,缺少第三方交叉
|
||||
- 低:仅有间接线索,未达到本轮归档主结论标准
|
||||
|
||||
## 2. 传输与版本
|
||||
|
||||
### `ConnectServer`
|
||||
|
||||
- C++ 公开签名:`bool ConnectServer(const std::string &server_ip = "127.0.0.1", unsigned port = 50001);`
|
||||
- Python 包装形态:`c.ConnectServer(server_ip="127.0.0.1", port=50001) -> bool`
|
||||
- 服务端归属:客户端传输层行为,不进入 `ControllerServer._Xxx` 命令表
|
||||
- 协议 / 命令线索:建立到 `127.0.0.1:50001` 的 TCP 连接;JSON 负载只发生在连接建立之后
|
||||
- 返回值与默认值:默认 `server_ip="127.0.0.1"`、`port=50001`;成功返回 `true`
|
||||
- 典型工作流位置:所有远程 API 的前置步骤
|
||||
- 证据来源:`ControllerClient.h`、`UseControllerClient.cpp`、`UseControllerClient.py`、`UseRealRobot.py`
|
||||
- 置信度:高
|
||||
- 待确认点:重连、超时、握手细节未恢复
|
||||
|
||||
### `GetServerVersion`
|
||||
|
||||
- C++ 公开签名:`bool GetServerVersion(std::string &version);`
|
||||
- Python 包装形态:样例中表现为 `controller.GetServerVersion() -> str`
|
||||
- 服务端归属:协议分发层行为;当前未恢复到显式 `_GetServerVersion`
|
||||
- 协议 / 命令线索:命令名字符串 `GetServerVersion` 已坐实;服务端存在 `GetServerVersion success: {}.` 日志;精确 JSON envelope 未恢复
|
||||
- 返回值与默认值:C++ 为 `bool + out string`;Python 样例把结果当作直接字符串使用
|
||||
- 典型工作流位置:`ConnectServer` 之后、机器人初始化前后均可读取的元信息接口
|
||||
- 证据来源:`ControllerClient.h`、`UseControllerClient.py`、SDK 手册、`ControllerServer_analysis.md`、`libControllerClient.so`、`ControllerServer.cpython-37m-x86_64-linux-gnu.so`
|
||||
- 置信度:高
|
||||
- 待确认点:Python 失败路径的返回形态未看到样例;请求 JSON 的精确字段未恢复
|
||||
|
||||
### `GetClientVersion`
|
||||
|
||||
- C++ 公开签名:`bool GetClientVersion(std::string &version);`
|
||||
- Python 包装形态:未见公开样例;从包装库字符串看接口存在
|
||||
- 服务端归属:客户端本地行为,不进入服务端命令表
|
||||
- 协议 / 命令线索:`libControllerClient.so` 与 `PyControllerClient` 都有 `GetClientVersion` 符号;未见 `ControllerServer` 对应 `_Xxx`
|
||||
- 返回值与默认值:C++ 为 `bool + out string`;Python 侧返回形态待确认
|
||||
- 典型工作流位置:客户端自检或 SDK 版本上报,不依赖服务端状态
|
||||
- 证据来源:`ControllerClient.h`、`libControllerClient.so`、`PyControllerClient.cpython-37m-x86_64-linux-gnu.so`
|
||||
- 置信度:高
|
||||
- 待确认点:实际版本字符串来源与 Python 包装失败行为未恢复
|
||||
|
||||
## 3. 机器人初始化
|
||||
|
||||
### `SetUpRobot`
|
||||
|
||||
- C++ 公开签名:`bool SetUpRobot(const std::string &robot_name);`
|
||||
- Python 包装形态:`c.SetUpRobot("FANUC_LR_Mate_200iD") -> bool`
|
||||
- 服务端归属:`ControllerServer._SetUpRobot`
|
||||
- 协议 / 命令线索:命令名 `SetUpRobot` 已坐实;参数高概率是 `robot_name`;精确 JSON envelope 未恢复
|
||||
- 返回值与默认值:成功返回 `true`
|
||||
- 典型工作流位置:创建客户端并连上 `50001` 后首先调用;旧说明明确它是最先执行的服务端初始化动作
|
||||
- 证据来源:`ControllerClient.h`、`UseControllerClient.cpp`、`UseControllerClient.py`、SDK 手册、`ControllerServer_analysis.md`、二进制字符串
|
||||
- 置信度:高
|
||||
- 待确认点:请求字段键名是否为 `robot_name` 仍需抓包或 hook 坐实
|
||||
|
||||
### `SetUpRobotFromEnv`
|
||||
|
||||
- C++ 公开签名:`bool SetUpRobotFromEnv(const std::string &env_file);`
|
||||
- Python 包装形态:未见公开样例;服务端与客户端字符串均存在
|
||||
- 服务端归属:`ControllerServer._SetUpRobotFromEnv`
|
||||
- 协议 / 命令线索:命令名 `SetUpRobotFromEnv` 已坐实;参数高概率是环境文件绝对路径;精确 JSON envelope 未恢复
|
||||
- 返回值与默认值:成功返回 `true`
|
||||
- 典型工作流位置:与 `SetUpRobot` 二选一;当现场通过环境文件而不是型号名初始化时使用
|
||||
- 证据来源:`ControllerClient.h`、`ControllerServer_analysis.md`、`ControllerServer` 字符串、`libControllerClient.so`
|
||||
- 置信度:高
|
||||
- 待确认点:环境文件路径是否必须为绝对路径由注释可见,但服务端是否做额外归一化未恢复
|
||||
|
||||
### `IsSetUp`
|
||||
|
||||
- C++ 公开签名:`bool IsSetUp();`
|
||||
- Python 包装形态:`c.IsSetUp() -> bool`
|
||||
- 服务端归属:`ControllerServer._IsSetUp`
|
||||
- 协议 / 命令线索:命令名 `IsSetUp` 已坐实;精确 JSON envelope 未恢复
|
||||
- 返回值与默认值:直接返回布尔值
|
||||
- 典型工作流位置:`SetUpRobot` 或 `SetUpRobotFromEnv` 之后的状态确认
|
||||
- 证据来源:`ControllerClient.h`、`UseControllerClient.cpp`、`UseControllerClient.py`、`ControllerServer_analysis.md`、二进制字符串
|
||||
- 置信度:高
|
||||
- 待确认点:服务端返回是否仅受机器人对象存在性控制,还是还依赖模型/控制器更深层状态,当前未完全恢复
|
||||
|
||||
### `SetShowTCP`
|
||||
|
||||
- C++ 公开签名:`bool SetShowTCP(bool is_show = true, double axis_length = 0.1, size_t axis_size = 2);`
|
||||
- Python 包装形态:`c.SetShowTCP(is_show=True, axis_length=0.1, axis_size=2) -> bool`
|
||||
- 服务端归属:`ControllerServer._SetShowTCP`
|
||||
- 协议 / 命令线索:命令名 `SetShowTCP` 已坐实;字符串中可见 `SetShowTCP is_show success/failed`;精确 JSON envelope 未恢复
|
||||
- 返回值与默认值:默认 `is_show=true`、`axis_length=0.1`、`axis_size=2`
|
||||
- 典型工作流位置:初始化完成后、切换控制器前的仿真显示配置
|
||||
- 证据来源:`ControllerClient.h`、`UseControllerClient.cpp`、`UseControllerClient.py`、`ControllerServer_analysis.md`、`ControllerServer` 字符串
|
||||
- 置信度:高
|
||||
- 待确认点:仅仿真控制器生效还是真实控制器也接受该命令,当前未见明确负例
|
||||
|
||||
### `GetName`
|
||||
|
||||
- C++ 公开签名:`std::string GetName();`
|
||||
- Python 包装形态:`c.GetName() -> str`
|
||||
- 服务端归属:`ControllerServer._GetName`
|
||||
- 协议 / 命令线索:命令名 `GetName` 已坐实;精确 JSON envelope 未恢复
|
||||
- 返回值与默认值:直接返回机器人名称字符串
|
||||
- 典型工作流位置:完成机器人初始化后读取名称确认
|
||||
- 证据来源:`ControllerClient.h`、`UseControllerClient.py`、SDK 手册、`ControllerServer_analysis.md`、二进制字符串
|
||||
- 置信度:高
|
||||
- 待确认点:失败时是返回空串还是异常/默认值,Python 样例未覆盖
|
||||
|
||||
### `GetDoF`
|
||||
|
||||
- C++ 公开签名:`int GetDoF();`
|
||||
- Python 包装形态:`c.GetDoF() -> int`
|
||||
- 服务端归属:`ControllerServer._GetDoF`
|
||||
- 协议 / 命令线索:命令名 `GetDoF` 已坐实;精确 JSON envelope 未恢复
|
||||
- 返回值与默认值:直接返回自由度整数
|
||||
- 典型工作流位置:完成机器人初始化后读取自由度
|
||||
- 证据来源:`ControllerClient.h`、`UseControllerClient.py`、SDK 手册、`ControllerServer_analysis.md`、二进制字符串
|
||||
- 置信度:高
|
||||
- 待确认点:失败时的客户端行为未恢复
|
||||
|
||||
## 4. 控制器状态
|
||||
|
||||
### `SetActiveController`
|
||||
|
||||
- C++ 公开签名:`bool SetActiveController(bool sim = true);`
|
||||
- Python 包装形态:`c.SetActiveController(sim=True) -> bool`;真机样例使用 `sim=False`
|
||||
- 服务端归属:`ControllerServer._SetActiveController`
|
||||
- 协议 / 命令线索:命令名 `SetActiveController` 已坐实;字符串中可见 `SetActiveController sim success/failed`
|
||||
- 返回值与默认值:默认 `sim=true`
|
||||
- 典型工作流位置:机器人初始化后、`Connect` 前,用于在仿真控制器和真实控制器之间切换
|
||||
- 证据来源:`ControllerClient.h`、`UseControllerClient.py`、`UseRealRobot.py`、SDK 手册、`ControllerServer_analysis.md`、二进制字符串
|
||||
- 置信度:高
|
||||
- 待确认点:切换控制器时是否会隐式断开旧控制器,当前只有 `_DisconnectAll` 的间接线索
|
||||
|
||||
### `Connect`
|
||||
|
||||
- C++ 公开签名:`bool Connect(const std::string &robot_ip);`
|
||||
- Python 包装形态:`c.Connect("192.168.10.101") -> bool`
|
||||
- 服务端归属:`ControllerServer._Connect`
|
||||
- 协议 / 命令线索:命令名 `Connect` 已坐实;字符串中可见 `Connect ip success/failed`
|
||||
- 返回值与默认值:成功返回 `true`
|
||||
- 典型工作流位置:选定活动控制器后,通知服务端连接真实机器人 IP
|
||||
- 证据来源:`ControllerClient.h`、`UseControllerClient.cpp`、`UseControllerClient.py`、`UseRealRobot.py`、SDK 手册、`ControllerServer_analysis.md`、`FANUC_realtime_comm_analysis.md`、二进制字符串
|
||||
- 置信度:高
|
||||
- 待确认点:JSON 键名是否为 `ip` 或 `robot_ip` 未恢复;仿真模式下是否接受同一命令仍需实现侧验证
|
||||
|
||||
### `Disconnect`
|
||||
|
||||
- C++ 公开签名:`bool Disconnect();`
|
||||
- Python 包装形态:`c.Disconnect() -> bool`
|
||||
- 服务端归属:`ControllerServer._Disconnect`
|
||||
- 协议 / 命令线索:命令名 `Disconnect` 已坐实;字符串中可见 `Disconnect success/failed`
|
||||
- 返回值与默认值:直接返回布尔值
|
||||
- 典型工作流位置:真实控制器断开或工作流结束时调用
|
||||
- 证据来源:`ControllerClient.h`、`ControllerServer_analysis.md`、`ControllerServer` 字符串、`libControllerClient.so`
|
||||
- 置信度:高
|
||||
- 待确认点:是否同步清理状态通道 / 命令通道 / 伺服通道,当前只在 FANUC 分析文档看到对象链,并未完全坐实释放顺序
|
||||
|
||||
### `EnableRobot`
|
||||
|
||||
- C++ 公开签名:`bool EnableRobot(unsigned buffer_size = 2);`
|
||||
- Python 包装形态:`c.EnableRobot() -> bool`
|
||||
- 服务端归属:`ControllerServer._EnableRobot`
|
||||
- 协议 / 命令线索:命令名 `EnableRobot` 已坐实;字符串中可见 `EnableRobot success/failed`
|
||||
- 返回值与默认值:默认 `buffer_size=2`
|
||||
- 典型工作流位置:`Connect` 后使能机器人;多数运动 API 前置条件
|
||||
- 证据来源:`ControllerClient.h`、`UseControllerClient.cpp`、`UseControllerClient.py`、`UseRealRobot.py`、`ControllerServer_analysis.md`、`FANUC_realtime_comm_analysis.md`、二进制字符串
|
||||
- 置信度:高
|
||||
- 待确认点:`buffer_size` 在旧服务端具体如何映射到伺服缓冲区参数,当前未完全恢复
|
||||
|
||||
### `DisableRobot`
|
||||
|
||||
- C++ 公开签名:`bool DisableRobot();`
|
||||
- Python 包装形态:`c.DisableRobot() -> bool`
|
||||
- 服务端归属:`ControllerServer._DisableRobot`
|
||||
- 协议 / 命令线索:命令名 `DisableRobot` 已坐实;字符串中可见 `DisableRobot success/failed`
|
||||
- 返回值与默认值:直接返回布尔值
|
||||
- 典型工作流位置:停机或退出前关闭机器人使能
|
||||
- 证据来源:`ControllerClient.h`、`ControllerServer_analysis.md`、`FANUC_realtime_comm_analysis.md`、二进制字符串
|
||||
- 置信度:高
|
||||
- 待确认点:与 `StopMove` 的先后顺序约束未恢复
|
||||
|
||||
### `StopMove`
|
||||
|
||||
- C++ 公开签名:`bool StopMove();`
|
||||
- Python 包装形态:`c.StopMove() -> bool`
|
||||
- 服务端归属:`ControllerServer._StopMove`
|
||||
- 协议 / 命令线索:命令名 `StopMove` 已坐实;字符串中可见 `StopMove success/failed`
|
||||
- 返回值与默认值:直接返回布尔值
|
||||
- 典型工作流位置:当前运动中止;Python 示例中调用后会再次 `EnableRobot()`
|
||||
- 证据来源:`ControllerClient.h`、`UseControllerClient.py`、`ControllerServer_analysis.md`、`FANUC_realtime_comm_analysis.md`、二进制字符串
|
||||
- 置信度:高
|
||||
- 待确认点:是否会清空控制器执行队列、是否必须重新使能,当前只从示例看到倾向性而非硬约束
|
||||
|
||||
## 5. 参数与 IO
|
||||
|
||||
### `GetSpeedRatio`
|
||||
|
||||
- C++ 公开签名:`double GetSpeedRatio();`
|
||||
- Python 包装形态:`c.GetSpeedRatio() -> float`
|
||||
- 服务端归属:`ControllerServer._GetSpeedRatio`
|
||||
- 协议 / 命令线索:命令通道 `MsgID = 0x2206`;响应字段为 `ratio_int` 与 `result_code`;成功后客户端将 `ratio_int / 100.0`
|
||||
- 返回值与默认值:直接返回 `0~1` 之间的倍率
|
||||
- 典型工作流位置:连机并使能后读取当前速度倍率
|
||||
- 证据来源:`ControllerClient.h`、`UseControllerClient.cpp`、`UseControllerClient.py`、SDK 手册、`ControllerServer_analysis.md`、`CommonMsg_protocol_analysis.md`
|
||||
- 置信度:高
|
||||
- 待确认点:服务端 JSON 层到命令通道层的参数桥接结构未恢复
|
||||
|
||||
### `SetSpeedRatio`
|
||||
|
||||
- C++ 公开签名:`bool SetSpeedRatio(double ratio);`
|
||||
- Python 包装形态:`c.SetSpeedRatio(0.8) -> bool`
|
||||
- 服务端归属:`ControllerServer._SetSpeedRatio`
|
||||
- 协议 / 命令线索:命令通道 `MsgID = 0x2207`;请求字段为 `ratio_int_0_100`;输入 `double` 先乘 `100` 并夹到 `[0,100]`
|
||||
- 返回值与默认值:成功返回 `true`
|
||||
- 典型工作流位置:连机并使能后修改控制器速度倍率
|
||||
- 证据来源:`ControllerClient.h`、`UseControllerClient.py`、`UseRealRobot.py`、SDK 手册、`ControllerServer_analysis.md`、`CommonMsg_protocol_analysis.md`、二进制字符串
|
||||
- 置信度:高
|
||||
- 待确认点:ratio 越界时客户端是裁剪还是直接失败,当前更偏向裁剪,但缺少公开负例
|
||||
|
||||
### `GetTCP`
|
||||
|
||||
- C++ 公开签名:`bool GetTCP(Pose &tcp);`
|
||||
- Python 包装形态:`res, tcp = c.GetTCP()`
|
||||
- 服务端归属:`ControllerServer._GetTCP`
|
||||
- 协议 / 命令线索:控制器命令通道底层接口 `GetTCP` 已有 `MsgID = 0x2200` 证据,但 `ControllerClient` JSON 层请求结构未完全恢复
|
||||
- 返回值与默认值:C++ 为 `bool + out Pose`;Python 为 `(bool, Pose)`
|
||||
- 典型工作流位置:连机后读取当前控制器 TCP
|
||||
- 证据来源:`ControllerClient.h`、`UseControllerClient.py`、`ControllerServer_analysis.md`、`CommonMsg_protocol_analysis.md`、二进制字符串
|
||||
- 置信度:高
|
||||
- 待确认点:JSON 层是否支持多个 TCP ID,目前只看到底层命令通道有 `tcp_id`
|
||||
|
||||
### `SetTCP`
|
||||
|
||||
- C++ 公开签名:`bool SetTCP(const Pose &tcp);`
|
||||
- Python 包装形态:`c.SetTCP(tcp) -> bool`
|
||||
- 服务端归属:`ControllerServer._SetTCP`
|
||||
- 协议 / 命令线索:控制器命令通道底层接口 `SetTCP` 已有 `MsgID = 0x2201` 证据;JSON 层精确字段未恢复
|
||||
- 返回值与默认值:成功返回 `true`
|
||||
- 典型工作流位置:连机后修改控制器 TCP
|
||||
- 证据来源:`ControllerClient.h`、`UseControllerClient.py`、`ControllerServer_analysis.md`、`CommonMsg_protocol_analysis.md`、`FANUC_realtime_comm_analysis.md`、二进制字符串
|
||||
- 置信度:高
|
||||
- 待确认点:JSON 层是否也暴露 `tcp_id` 概念尚未恢复
|
||||
|
||||
### `GetIO`
|
||||
|
||||
- C++ 公开签名:`bool GetIO(unsigned port, bool &value, IOType type = kIOTypeDI);`
|
||||
- Python 包装形态:`res, value = c.GetIO(port=1, io_type=IOType.kIOTypeDI)`
|
||||
- 服务端归属:`ControllerServer._GetIO`
|
||||
- 协议 / 命令线索:命令通道 `MsgID = 0x2208`;请求字段顺序是 `io_type`、`io_index`;响应字段是 `result_code`、`io_value`
|
||||
- 返回值与默认值:默认 `type = kIOTypeDI`;C++ 为 `bool + out bool`;Python 为 `(bool, bool)`
|
||||
- 典型工作流位置:连机后读取 DI/DO/RI/RO 等 IO 值
|
||||
- 证据来源:`ControllerClient.h`、`UseControllerClient.py`、SDK 手册、`ControllerServer_analysis.md`、`CommonMsg_protocol_analysis.md`、二进制字符串
|
||||
- 置信度:高
|
||||
- 待确认点:高层 JSON 是否把字段命名为 `port` / `value` / `type`,还是 `io_index` / `io_type`,当前未抓到完整包
|
||||
|
||||
### `SetIO`
|
||||
|
||||
- C++ 公开签名:`bool SetIO(unsigned port, bool value, IOType type = kIOTypeDO);`
|
||||
- Python 包装形态:`c.SetIO(port=1, value=True, io_type=IOType.kIOTypeDO) -> bool`
|
||||
- 服务端归属:`ControllerServer._SetIO`
|
||||
- 协议 / 命令线索:命令通道 `MsgID = 0x2209`;请求字段顺序是 `io_type`、`io_index`、`io_value`
|
||||
- 返回值与默认值:默认 `type = kIOTypeDO`
|
||||
- 典型工作流位置:连机后设置数字输出;飞拍链路之外的普通 IO 调试也会用到
|
||||
- 证据来源:`ControllerClient.h`、`UseControllerClient.py`、SDK 手册、`ControllerServer_analysis.md`、`CommonMsg_protocol_analysis.md`、`FANUC_realtime_comm_analysis.md`、二进制字符串
|
||||
- 置信度:高
|
||||
- 待确认点:布尔值在高层 JSON 中是否以 `true/false` 传输、在命令通道中以 `float` 传输,当前只坐实了命令通道层
|
||||
|
||||
## 6. 运动与求解
|
||||
|
||||
### `GetJointPosition`
|
||||
|
||||
- C++ 公开签名:`bool GetJointPosition(JointPositions &joint_position);`
|
||||
- Python 包装形态:`res, joints = c.GetJointPosition()`
|
||||
- 服务端归属:`ControllerServer._GetJointPositions`
|
||||
- 协议 / 命令线索:命令名 `GetJointPositions` 已坐实;客户端公开 API 名与服务端方法名存在单复数差异
|
||||
- 返回值与默认值:C++ 为 `bool + out JointPositions`;Python 为 `(bool, JointPositions)`
|
||||
- 典型工作流位置:读取当前关节角,常作为 `GetNearestIK` 的 seed 或 `MoveJoint` 的基准
|
||||
- 证据来源:`ControllerClient.h`、`UseControllerClient.cpp`、`UseControllerClient.py`、SDK 手册、`ControllerServer_analysis.md`、二进制字符串
|
||||
- 置信度:高
|
||||
- 待确认点:JSON 层返回数组字段键名未恢复
|
||||
|
||||
### `GetPose`
|
||||
|
||||
- C++ 公开签名:`bool GetPose(Pose &pose);`
|
||||
- Python 包装形态:`res, pose = c.GetPose()`
|
||||
- 服务端归属:`ControllerServer._GetPose`
|
||||
- 协议 / 命令线索:命令名 `GetPose` 已坐实;字符串中可见 `GetPose success/failed`
|
||||
- 返回值与默认值:C++ 为 `bool + out Pose`;Python 为 `(bool, Pose)`
|
||||
- 典型工作流位置:读取当前 TCP 位姿,常与 `GetNearestIK` 配套使用
|
||||
- 证据来源:`ControllerClient.h`、`UseControllerClient.py`、SDK 手册、`ControllerServer_analysis.md`、二进制字符串
|
||||
- 置信度:高
|
||||
- 待确认点:位姿坐标系定义虽然在 `Types.h` 中体现为 7 元数组,但 JSON 层字段结构未恢复
|
||||
|
||||
### `GetNearestIK`
|
||||
|
||||
- C++ 公开签名:`bool GetNearestIK(const Pose &pose, const JointPositions &seed, JointPositions &ik);`
|
||||
- Python 包装形态:`res, ik = c.GetNearestIK(pose, joint_seed=joints)`
|
||||
- 服务端归属:`ControllerServer._GetNearestIK`
|
||||
- 协议 / 命令线索:命令名 `GetNearestIK` 已坐实;字符串中可见 `GetNearestIK success/failed`
|
||||
- 返回值与默认值:C++ 为 `bool + out JointPositions`;Python 为 `(bool, JointPositions)`
|
||||
- 典型工作流位置:先 `GetPose` 得到当前位姿,再构造目标位姿,并使用当前关节或邻近关节作为 seed 求最近 IK
|
||||
- 证据来源:`ControllerClient.h`、`UseControllerClient.py`、`ControllerServer_analysis.md`、二进制字符串
|
||||
- 置信度:高
|
||||
- 待确认点:JSON 层中 seed 参数键名在 Python 里表现为 `joint_seed`,但 C++ 注释写的是 `seed`;高层字段命名仍待确认
|
||||
|
||||
### `MoveJoint`
|
||||
|
||||
- C++ 公开签名:`bool MoveJoint(const JointPositions &joint_position);`
|
||||
- Python 包装形态:`c.MoveJoint(home_joint) -> bool`
|
||||
- 服务端归属:`ControllerServer._MoveJoint`
|
||||
- 协议 / 命令线索:命令名 `MoveJoint` 已坐实;字符串中可见 `MoveJoint waypoint success/failed`
|
||||
- 返回值与默认值:成功返回 `true`
|
||||
- 典型工作流位置:点到点回零或移动到飞拍轨迹起点
|
||||
- 证据来源:`ControllerClient.h`、`UseControllerClient.cpp`、`UseControllerClient.py`、`ControllerServer_analysis.md`、二进制字符串
|
||||
- 置信度:高
|
||||
- 待确认点:移动速度/平滑参数是否固定在服务端内部,公开 API 未暴露
|
||||
|
||||
### `ExecuteTrajectory`
|
||||
|
||||
- C++ 公开签名:`bool ExecuteTrajectory(const std::vector<JointPositions> &waypoints, const std::string &method = "icsp", bool save_traj = false);`
|
||||
- Python 包装形态:`c.ExecuteTrajectory(waypoints=[...], method="icsp", save_traj=True) -> bool`
|
||||
- 服务端归属:`ControllerServer._ExecuteTrajectory`
|
||||
- 协议 / 命令线索:命令名 `ExecuteTrajectory` 已坐实;`method` 至少支持 `icsp` 与 `doubles`
|
||||
- 返回值与默认值:默认 `method="icsp"`、`save_traj=false`
|
||||
- 典型工作流位置:普通多点轨迹执行,不带飞拍 IO;输入是关节空间稀疏 waypoint,而不是笛卡尔点
|
||||
- 证据来源:`ControllerClient.h`、`UseControllerClient.py`、SDK 手册、`ControllerServer_analysis.md`、`Trajectory_generation_algorithm_analysis.md`、二进制字符串
|
||||
- 置信度:高
|
||||
- 待确认点:JSON 中 waypoint 列表的键名和序列化结构未恢复;`doubles` 的失败语义未在公开样例中体现
|
||||
|
||||
## 7. 飞拍轨迹
|
||||
|
||||
### `UploadFlyShotTraj`
|
||||
|
||||
- C++ 公开签名:`bool UploadFlyShotTraj(const std::string &name, const std::vector<JointPositions> &waypoints, const std::vector<bool> &shot_flags, const std::vector<int> &offset_values, const std::vector<std::vector<int>> &addrs);`
|
||||
- Python 包装形态:`c.UploadFlyShotTraj(name="test_traj", waypoints=..., shot_flags=..., offset_values=..., addrs=...) -> bool`
|
||||
- 服务端归属:`ControllerServer._UploadFlyShotTraj`
|
||||
- 协议 / 命令线索:命令名 `UploadFlyShotTraj` 已坐实;客户端字符串中可见 `StartUploadFlyShotTraj` 与 `EndUploadFlyShotTraj`,说明上传阶段很可能分为开始 / 传输 / 结束三个子步骤
|
||||
- 返回值与默认值:成功返回 `true`;无默认参数
|
||||
- 典型工作流位置:运行时动态构造飞拍轨迹时,先把轨迹定义登记到服务端
|
||||
- 证据来源:`ControllerClient.h`、`UseControllerClient.py`、SDK 手册、`ControllerServer_analysis.md`、`Trajectory_generation_algorithm_analysis.md`、`libControllerClient.so`、`ControllerServer` 字符串
|
||||
- 置信度:高
|
||||
- 待确认点:JSON 上传是否一次性发送完整 payload,还是像字符串暗示的那样分片上传,当前未完全恢复
|
||||
|
||||
### `DeleteFlyShotTraj`
|
||||
|
||||
- C++ 公开签名:`bool DeleteFlyShotTraj(const std::string &name);`
|
||||
- Python 包装形态:`c.DeleteFlyShotTraj(name="test_traj") -> bool`
|
||||
- 服务端归属:`ControllerServer._DeleteFlyShotTraj`
|
||||
- 协议 / 命令线索:命令名 `DeleteFlyShotTraj` 已坐实;字符串中可见 `DeleteFlyShotTraj {} success/failed`
|
||||
- 返回值与默认值:成功返回 `true`
|
||||
- 典型工作流位置:删除已上传的临时飞拍轨迹定义
|
||||
- 证据来源:`ControllerClient.h`、`UseControllerClient.py`、SDK 手册、`ControllerServer_analysis.md`、`PyControllerClient` 字符串、`ControllerServer` 字符串
|
||||
- 置信度:高
|
||||
- 待确认点:删除配置内固有轨迹名与删除运行时上传轨迹时是否走同一条路径,当前未恢复
|
||||
|
||||
### `ListFlyShotTraj`
|
||||
|
||||
- C++ 公开签名:`std::vector<std::string> ListFlyShotTraj();`
|
||||
- Python 包装形态:`c.ListFlyShotTraj() -> list[str]`
|
||||
- 服务端归属:`ControllerServer._ListFlyShotTraj`
|
||||
- 协议 / 命令线索:命令名 `ListFlyShotTraj` 已坐实;客户端与服务端字符串都出现 `GetNextListFlyShotTraj`,说明底层列举很可能是迭代式获取,而不是一次性返回整个数组
|
||||
- 返回值与默认值:直接返回轨迹名称列表
|
||||
- 典型工作流位置:查看当前服务端已登记的飞拍轨迹
|
||||
- 证据来源:`ControllerClient.h`、`UseControllerClient.py`、`ControllerServer_analysis.md`、`libControllerClient.so`、`ControllerServer` 字符串
|
||||
- 置信度:高
|
||||
- 待确认点:高层 JSON 是否真的直接返回数组,还是客户端内部循环拉取后再拼成数组,当前未完全恢复
|
||||
|
||||
### `ExecuteFlyShotTraj`
|
||||
|
||||
- C++ 公开签名:`bool ExecuteFlyShotTraj(const std::string &name, const bool move_to_start = false, const std::string &method = "icsp", bool save_traj = false, bool use_cache = false);`
|
||||
- Python 包装形态:`c.ExecuteFlyShotTraj(name="002", move_to_start=True, method="icsp", save_traj=True, use_cache=False) -> bool`
|
||||
- 服务端归属:`ControllerServer._ExecuteFlyShotTraj`
|
||||
- 协议 / 命令线索:命令名 `ExecuteFlyShotTraj` 已坐实;字符串中出现 `move_to_start`、`use_cache`;伪代码级逆向已恢复其主流程
|
||||
- 返回值与默认值:默认 `move_to_start=false`、`method="icsp"`、`save_traj=false`、`use_cache=false`
|
||||
- 典型工作流位置:对已存在的飞拍轨迹定义执行“生成 + 挂接 DO 时间轴 + 执行”
|
||||
- 证据来源:`ControllerClient.h`、`UseControllerClient.cpp`、`UseControllerClient.py`、`UseRealRobot.py`、SDK 手册、`ControllerServer_analysis.md`、`Trajectory_generation_algorithm_analysis.md`、二进制字符串
|
||||
- 置信度:高
|
||||
- 待确认点:JSON 包结构未恢复;`use_cache` 的缓存键是否只按 `name`,当前从伪代码判断是,但未见更底层证据
|
||||
|
||||
### `SaveTrajInfo`
|
||||
|
||||
- C++ 公开签名:`bool SaveTrajInfo(const std::string &name, const std::string &method = "icsp");`
|
||||
- Python 包装形态:未见运行样例,但包装库和服务端都有明确字符串;语义可由手册和分析文档确认
|
||||
- 服务端归属:`ControllerServer._SaveTrajInfo`
|
||||
- 协议 / 命令线索:命令名 `SaveTrajInfo` 已坐实;字符串中可见 `SaveTrajInfo {} success/failed`
|
||||
- 返回值与默认值:默认 `method="icsp"`;`self-adapt-icsp` 在执行时保存分析文件会回落成 `icsp` 导出语义
|
||||
- 典型工作流位置:按给定 `name + method` 生成并导出轨迹分析文件,例如 `JointTraj.txt`、`CartDetialTraj.txt`
|
||||
- 证据来源:`ControllerClient.h`、`ControllerServer_analysis.md`、`Trajectory_generation_algorithm_analysis.md`、二进制字符串
|
||||
- 置信度:高
|
||||
- 待确认点:Python 包装是否直接暴露该方法并返回 `bool`,虽然字符串存在,但公开示例未调用
|
||||
|
||||
### `IsFlyShotTrajValid`
|
||||
|
||||
- C++ 公开签名:`bool IsFlyShotTrajValid(double &time, const std::string &name, const std::string &method = "icsp", bool save_traj = false);`
|
||||
- Python 包装形态:`valid, time_sec = c.IsFlyShotTrajValid("EOL9_EAU_90", "icsp", save_traj=True)`
|
||||
- 服务端归属:`ControllerServer._IsFlyShotTrajValid`
|
||||
- 协议 / 命令线索:命令名 `IsFlyShotTrajValid` 已坐实;作用是“生成 + 合法性检查 + 返回轨迹总时长”
|
||||
- 返回值与默认值:默认 `method="icsp"`、`save_traj=false`;C++ 为 `bool + out double`;Python 为 `(bool, float)`
|
||||
- 典型工作流位置:在执行飞拍轨迹前预校验;也可与 `save_traj=True` 结合导出分析文件
|
||||
- 证据来源:`ControllerClient.h`、`UseControllerClient.py` 中的注释样例、`ControllerServer_analysis.md`、`Trajectory_generation_algorithm_analysis.md`、二进制字符串
|
||||
- 置信度:高
|
||||
- 待确认点:`method="self-adapt-icsp"` 是否被该接口完整支持,分析文档显示公开手册更强调 `icsp` / `doubles`,而示例注释里又出现 `self-adapt-icsp`,这里仍需后续抓包或真环境验证
|
||||
|
||||
## 8. 四条旧 SDK 工作流
|
||||
|
||||
### 8.1 初始化工作流
|
||||
|
||||
```text
|
||||
ControllerClient()
|
||||
-> ConnectServer("127.0.0.1", 50001)
|
||||
-> SetUpRobot("FANUC_LR_Mate_200iD") 或 SetUpRobotFromEnv(...)
|
||||
-> IsSetUp()
|
||||
-> GetName()
|
||||
-> GetDoF()
|
||||
-> SetShowTCP(...)
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- 这是最常见的仿真或真机前置流程。
|
||||
- `SetUpRobot` 是旧说明里明确要求首先执行的服务端初始化动作。
|
||||
|
||||
### 8.2 控制器状态工作流
|
||||
|
||||
```text
|
||||
SetActiveController(sim=True/False)
|
||||
-> Connect(robot_ip)
|
||||
-> EnableRobot(buffer_size=2)
|
||||
-> GetSpeedRatio() / SetSpeedRatio(...)
|
||||
-> GetTCP() / SetTCP(...)
|
||||
-> GetIO() / SetIO(...)
|
||||
-> StopMove()
|
||||
-> DisableRobot()
|
||||
-> Disconnect()
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- 真机样例会使用 `sim=False`。
|
||||
- `StopMove()` 之后,Python 示例里会再次 `EnableRobot()`,说明停止运动后常伴随重新使能。
|
||||
|
||||
### 8.3 普通轨迹工作流
|
||||
|
||||
```text
|
||||
GetJointPosition()
|
||||
-> GetPose()
|
||||
-> GetNearestIK(pose, seed)
|
||||
-> MoveJoint(home_joint)
|
||||
-> ExecuteTrajectory(waypoints, method="icsp", save_traj=True)
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- 输入是关节空间稀疏 waypoint。
|
||||
- `ExecuteTrajectory` 至少支持 `icsp` 与 `doubles`。
|
||||
|
||||
### 8.4 飞拍轨迹工作流
|
||||
|
||||
#### 方案 A:轨迹名已在配置中存在
|
||||
|
||||
```text
|
||||
name
|
||||
-> IsFlyShotTrajValid(name, "icsp", save_traj=True)
|
||||
-> ExecuteFlyShotTraj(name, move_to_start=True, method="icsp", save_traj=True, use_cache=False)
|
||||
-> SaveTrajInfo(name, "icsp")
|
||||
```
|
||||
|
||||
#### 方案 B:客户端动态上传飞拍轨迹
|
||||
|
||||
```text
|
||||
UploadFlyShotTraj(name, waypoints, shot_flags, offset_values, addrs)
|
||||
-> ListFlyShotTraj()
|
||||
-> IsFlyShotTrajValid(name, "icsp", save_traj=True)
|
||||
-> ExecuteFlyShotTraj(name, move_to_start=True, method="self-adapt-icsp", save_traj=True, use_cache=True)
|
||||
-> DeleteFlyShotTraj(name)
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- `UploadFlyShotTraj` 只负责登记轨迹定义。
|
||||
- `IsFlyShotTrajValid` 负责“生成 + 合法性检查”。
|
||||
- `ExecuteFlyShotTraj` 负责“生成 + 挂接 `TrajectoryDO` + 执行”。
|
||||
- `SaveTrajInfo` 负责导出分析文件。
|
||||
|
||||
## 9. 已知高置信协议线索
|
||||
|
||||
当前能直接用于后续实现的高置信协议结论如下:
|
||||
|
||||
- 传输层为 `TCP + JSON` 风格协议,不是 HTTP/gRPC。
|
||||
- JSON 层至少存在 `method` 字段,服务端存在 `_ClientCB` 与 `_IsJsonValid`。
|
||||
- 命令通道字段已确认:
|
||||
- `GetSpeedRatio`:`MsgID = 0x2206`
|
||||
- `SetSpeedRatio`:`MsgID = 0x2207`
|
||||
- `GetIO`:`MsgID = 0x2208`
|
||||
- `SetIO`:`MsgID = 0x2209`
|
||||
- 飞拍轨迹相关额外字符串线索:
|
||||
- `StartUploadFlyShotTraj`
|
||||
- `EndUploadFlyShotTraj`
|
||||
- `GetNextListFlyShotTraj`
|
||||
- `move_to_start`
|
||||
- `use_cache`
|
||||
- `shot_flags`
|
||||
- `offset_values`
|
||||
- `addr` / `addrs`
|
||||
|
||||
## 10. 待确认问题
|
||||
|
||||
以下问题本轮故意保留,不冒充已确认结论:
|
||||
|
||||
1. `50001/TCP+JSON` 请求 JSON 的精确 envelope 结构、字段必填规则、响应包统一格式。
|
||||
2. `GetServerVersion` 在高层 JSON 中的完整请求 / 响应字段。
|
||||
3. `GetClientVersion` 的实际版本字符串来源,以及 Python 包装失败路径。
|
||||
4. `ListFlyShotTraj` 是高层一次性返回数组,还是客户端内部循环 `GetNextListFlyShotTraj` 后再拼装列表。
|
||||
5. `UploadFlyShotTraj` 是否采用开始 / 数据 / 结束的多阶段上传协议。
|
||||
6. `IsFlyShotTrajValid` 对 `self-adapt-icsp` 的真实支持边界。
|
||||
7. `SetTCP` / `GetTCP` 在高层 JSON 中是否暴露 `tcp_id` 概念。
|
||||
8. `SetActiveController` 切换控制器时是否会隐式触发 `_DisconnectAll`。
|
||||
|
||||
## 11. 后续实现使用方式
|
||||
|
||||
等继续扩展 `Flyshot.ControllerClientCompat` 时,建议按以下顺序使用本文档:
|
||||
|
||||
1. 先把 32 个 API 按本文档拆成命令表。
|
||||
2. 先实现高置信、状态简单的接口:
|
||||
- `GetServerVersion`
|
||||
- `SetUpRobot`
|
||||
- `IsSetUp`
|
||||
- `GetName`
|
||||
- `GetDoF`
|
||||
- `GetSpeedRatio`
|
||||
- `SetSpeedRatio`
|
||||
- `GetIO`
|
||||
- `SetIO`
|
||||
3. 再实现返回复杂结构的接口:
|
||||
- `GetTCP`
|
||||
- `GetJointPosition`
|
||||
- `GetPose`
|
||||
- `GetNearestIK`
|
||||
4. 最后实现飞拍轨迹相关接口,并把本文档中的“待确认问题”逐项收敛成兼容测试。
|
||||
@@ -0,0 +1,271 @@
|
||||
# Minimal Runtime Orchestration Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Build the first real execution slice after the HTTP-only refactor by routing `ExecuteTrajectory` and `ExecuteFlyShotTraj` through planning, triggering, and a new minimal FANUC runtime project instead of in-memory last-point assignment.
|
||||
|
||||
**Architecture:** Keep `Flyshot.Server.Host` as a pure HTTP adapter and keep uploaded program state in `Flyshot.ControllerClientCompat`, but move controller runtime state into a new `Flyshot.Runtime.Fanuc` project and add a focused planning/orchestration helper in `Flyshot.ControllerClientCompat`. Ordinary trajectory execution will use `ICspPlanner`; uploaded flyshot execution will use `SelfAdaptIcspPlanner` plus `ShotTimelineBuilder`, then hand the resulting `TrajectoryResult` to the runtime.
|
||||
|
||||
**Tech Stack:** C#, .NET 8, xUnit, existing `Flyshot.Core.Domain`, `Flyshot.Core.Planning`, `Flyshot.Core.Triggering`, ASP.NET Core DI.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Add Runtime Contracts And Minimal FANUC Runtime
|
||||
|
||||
**Files:**
|
||||
- Create: `src/Flyshot.Runtime.Common/IControllerRuntime.cs`
|
||||
- Create: `src/Flyshot.Runtime.Fanuc/Flyshot.Runtime.Fanuc.csproj`
|
||||
- Create: `src/Flyshot.Runtime.Fanuc/FanucControllerRuntime.cs`
|
||||
- Modify: `FlyshotReplacement.sln`
|
||||
- Modify: `src/Flyshot.Server.Host/Flyshot.Server.Host.csproj`
|
||||
- Modify: `src/Flyshot.Server.Host/Program.cs`
|
||||
- Test: `tests/Flyshot.Core.Tests/RuntimeOrchestrationTests.cs`
|
||||
|
||||
- [x] **Step 1: Write the failing runtime test**
|
||||
|
||||
```csharp
|
||||
[Fact]
|
||||
public void FanucControllerRuntime_ExecuteTrajectory_UpdatesSnapshotAndFinalJointPositions()
|
||||
{
|
||||
var runtime = new FanucControllerRuntime();
|
||||
var robot = TestRobotFactory.CreateRobotProfile();
|
||||
runtime.ResetRobot(robot, "FANUC_LR_Mate_200iD");
|
||||
runtime.SetActiveController(sim: false);
|
||||
runtime.Connect("192.168.10.101");
|
||||
runtime.EnableRobot(bufferSize: 2);
|
||||
|
||||
var result = new TrajectoryResult(
|
||||
programName: "demo",
|
||||
method: PlanningMethod.Icsp,
|
||||
isValid: true,
|
||||
duration: TimeSpan.FromSeconds(1.2),
|
||||
shotEvents: Array.Empty<ShotEvent>(),
|
||||
triggerTimeline: Array.Empty<TrajectoryDoEvent>(),
|
||||
artifacts: Array.Empty<TrajectoryArtifact>(),
|
||||
failureReason: null,
|
||||
usedCache: false,
|
||||
originalWaypointCount: 4,
|
||||
plannedWaypointCount: 4);
|
||||
|
||||
runtime.ExecuteTrajectory(result, [1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
|
||||
|
||||
var snapshot = runtime.GetSnapshot();
|
||||
Assert.Equal("Connected", snapshot.ConnectionState);
|
||||
Assert.False(snapshot.IsInMotion);
|
||||
Assert.Equal([1.0, 2.0, 3.0, 4.0, 5.0, 6.0], snapshot.JointPositions);
|
||||
}
|
||||
```
|
||||
|
||||
- [x] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `dotnet test tests/Flyshot.Core.Tests/Flyshot.Core.Tests.csproj --filter FanucControllerRuntime_ExecuteTrajectory_UpdatesSnapshotAndFinalJointPositions -v minimal -m:1 -nodeReuse:false`
|
||||
Expected: FAIL because `FanucControllerRuntime` and `IControllerRuntime` do not exist.
|
||||
|
||||
- [x] **Step 3: Write the minimal runtime contracts and implementation**
|
||||
|
||||
```csharp
|
||||
public interface IControllerRuntime
|
||||
{
|
||||
void ResetRobot(RobotProfile robot, string robotName);
|
||||
void SetActiveController(bool sim);
|
||||
void Connect(string robotIp);
|
||||
void Disconnect();
|
||||
void EnableRobot(int bufferSize);
|
||||
void DisableRobot();
|
||||
void StopMove();
|
||||
double GetSpeedRatio();
|
||||
void SetSpeedRatio(double ratio);
|
||||
IReadOnlyList<double> GetTcp();
|
||||
void SetTcp(double x, double y, double z);
|
||||
bool GetIo(int port, string ioType);
|
||||
void SetIo(int port, bool value, string ioType);
|
||||
IReadOnlyList<double> GetJointPositions();
|
||||
IReadOnlyList<double> GetPose();
|
||||
ControllerStateSnapshot GetSnapshot();
|
||||
void ExecuteTrajectory(TrajectoryResult result, IReadOnlyList<double> finalJointPositions);
|
||||
}
|
||||
```
|
||||
|
||||
```csharp
|
||||
public sealed class FanucControllerRuntime : IControllerRuntime
|
||||
{
|
||||
// Stage-1 runtime: owns controller state in one place so later sockets can replace internals without rewriting compat service.
|
||||
}
|
||||
```
|
||||
|
||||
- [x] **Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `dotnet test tests/Flyshot.Core.Tests/Flyshot.Core.Tests.csproj --filter FanucControllerRuntime_ExecuteTrajectory_UpdatesSnapshotAndFinalJointPositions -v minimal -m:1 -nodeReuse:false`
|
||||
Expected: PASS.
|
||||
|
||||
### Task 2: Add Planning And Triggering Orchestration For Execution
|
||||
|
||||
**Files:**
|
||||
- Create: `src/Flyshot.ControllerClientCompat/PlannedExecutionBundle.cs`
|
||||
- Create: `src/Flyshot.ControllerClientCompat/ControllerClientTrajectoryOrchestrator.cs`
|
||||
- Modify: `src/Flyshot.ControllerClientCompat/Flyshot.ControllerClientCompat.csproj`
|
||||
- Modify: `tests/Flyshot.Core.Tests/Flyshot.Core.Tests.csproj`
|
||||
- Test: `tests/Flyshot.Core.Tests/RuntimeOrchestrationTests.cs`
|
||||
|
||||
- [x] **Step 1: Write the failing orchestration tests**
|
||||
|
||||
```csharp
|
||||
[Fact]
|
||||
public void ControllerClientTrajectoryOrchestrator_PlanOrdinaryTrajectory_RejectsThreeTeachPoints()
|
||||
{
|
||||
var orchestrator = new ControllerClientTrajectoryOrchestrator();
|
||||
var robot = TestRobotFactory.CreateRobotProfile();
|
||||
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
orchestrator.PlanOrdinaryTrajectory(robot,
|
||||
[
|
||||
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
|
||||
[0.5, 0.0, 0.0, 0.0, 0.0, 0.0],
|
||||
[1.0, 0.0, 0.0, 0.0, 0.0, 0.0]
|
||||
]));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ControllerClientTrajectoryOrchestrator_PlanUploadedFlyshot_BuildsShotTimeline()
|
||||
{
|
||||
var orchestrator = new ControllerClientTrajectoryOrchestrator();
|
||||
var robot = TestRobotFactory.CreateRobotProfile();
|
||||
var uploaded = TestRobotFactory.CreateUploadedTrajectoryWithSingleShot();
|
||||
|
||||
var bundle = orchestrator.PlanUploadedFlyshot(robot, uploaded);
|
||||
|
||||
Assert.True(bundle.Result.IsValid);
|
||||
Assert.Single(bundle.Result.ShotEvents);
|
||||
Assert.Single(bundle.Result.TriggerTimeline);
|
||||
}
|
||||
```
|
||||
|
||||
- [x] **Step 2: Run tests to verify they fail**
|
||||
|
||||
Run: `dotnet test tests/Flyshot.Core.Tests/Flyshot.Core.Tests.csproj --filter ControllerClientTrajectoryOrchestrator -v minimal -m:1 -nodeReuse:false`
|
||||
Expected: FAIL because the orchestrator types do not exist.
|
||||
|
||||
- [x] **Step 3: Write the minimal orchestration layer**
|
||||
|
||||
```csharp
|
||||
public sealed class PlannedExecutionBundle
|
||||
{
|
||||
public PlannedExecutionBundle(PlannedTrajectory plannedTrajectory, ShotTimeline shotTimeline, TrajectoryResult result)
|
||||
{
|
||||
PlannedTrajectory = plannedTrajectory;
|
||||
ShotTimeline = shotTimeline;
|
||||
Result = result;
|
||||
}
|
||||
|
||||
public PlannedTrajectory PlannedTrajectory { get; }
|
||||
public ShotTimeline ShotTimeline { get; }
|
||||
public TrajectoryResult Result { get; }
|
||||
}
|
||||
```
|
||||
|
||||
```csharp
|
||||
public sealed class ControllerClientTrajectoryOrchestrator
|
||||
{
|
||||
public PlannedExecutionBundle PlanOrdinaryTrajectory(RobotProfile robot, IReadOnlyList<IReadOnlyList<double>> waypoints) { ... }
|
||||
public PlannedExecutionBundle PlanUploadedFlyshot(RobotProfile robot, ControllerClientCompatUploadedTrajectory uploaded) { ... }
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run tests to verify they pass**
|
||||
|
||||
Run: `dotnet test tests/Flyshot.Core.Tests/Flyshot.Core.Tests.csproj --filter ControllerClientTrajectoryOrchestrator -v minimal -m:1 -nodeReuse:false`
|
||||
Expected: PASS.
|
||||
|
||||
### Task 3: Rewire ControllerClientCompatService To Runtime + Orchestrator
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/Flyshot.ControllerClientCompat/ControllerClientCompatService.cs`
|
||||
- Modify: `src/Flyshot.ControllerClientCompat/ControllerClientCompatServiceCollectionExtensions.cs`
|
||||
- Modify: `src/Flyshot.ControllerClientCompat/IControllerClientCompatService.cs`
|
||||
- Modify: `tests/Flyshot.Server.IntegrationTests/LegacyHttpApiCompatibilityTests.cs`
|
||||
- Modify: `tests/Flyshot.Server.IntegrationTests/ControllerClientCompatRegistrationTests.cs`
|
||||
- Test: `tests/Flyshot.Core.Tests/RuntimeOrchestrationTests.cs`
|
||||
- Test: `tests/Flyshot.Server.IntegrationTests/LegacyHttpApiCompatibilityTests.cs`
|
||||
|
||||
- [x] **Step 1: Write the failing compat-service test**
|
||||
|
||||
```csharp
|
||||
[Fact]
|
||||
public void ControllerClientCompatService_ExecuteTrajectory_RejectsThreeTeachPointsAfterPlanningIsIntroduced()
|
||||
{
|
||||
var service = TestRobotFactory.CreateCompatService();
|
||||
service.SetUpRobot("FANUC_LR_Mate_200iD");
|
||||
service.SetActiveController(sim: false);
|
||||
service.Connect("192.168.10.101");
|
||||
service.EnableRobot(2);
|
||||
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
service.ExecuteTrajectory(
|
||||
[
|
||||
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
|
||||
[0.5, 0.0, 0.0, 0.0, 0.0, 0.0],
|
||||
[1.0, 0.0, 0.0, 0.0, 0.0, 0.0]
|
||||
]));
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `dotnet test tests/Flyshot.Core.Tests/Flyshot.Core.Tests.csproj --filter ControllerClientCompatService_ExecuteTrajectory_RejectsThreeTeachPointsAfterPlanningIsIntroduced -v minimal -m:1 -nodeReuse:false`
|
||||
Expected: FAIL because current service still treats ordinary execution as "move to last waypoint".
|
||||
|
||||
- [x] **Step 3: Rewire service to the runtime and orchestrator**
|
||||
|
||||
```csharp
|
||||
public sealed class ControllerClientCompatService : IControllerClientCompatService
|
||||
{
|
||||
private readonly IControllerRuntime _runtime;
|
||||
private readonly ControllerClientTrajectoryOrchestrator _trajectoryOrchestrator;
|
||||
|
||||
public void ExecuteTrajectory(IReadOnlyList<IReadOnlyList<double>> waypoints)
|
||||
{
|
||||
var robot = RequireActiveRobot();
|
||||
var bundle = _trajectoryOrchestrator.PlanOrdinaryTrajectory(robot, waypoints);
|
||||
_runtime.ExecuteTrajectory(bundle.Result, bundle.PlannedTrajectory.PlannedWaypoints[^1].Positions);
|
||||
}
|
||||
|
||||
public void ExecuteTrajectoryByName(string name)
|
||||
{
|
||||
var robot = RequireActiveRobot();
|
||||
var uploaded = RequireUploadedTrajectory(name);
|
||||
var bundle = _trajectoryOrchestrator.PlanUploadedFlyshot(robot, uploaded);
|
||||
_runtime.ExecuteTrajectory(bundle.Result, bundle.PlannedTrajectory.PlannedWaypoints[^1].Positions);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run focused tests to verify green**
|
||||
|
||||
Run: `dotnet test tests/Flyshot.Core.Tests/Flyshot.Core.Tests.csproj --filter "ControllerClientCompatService|ControllerClientTrajectoryOrchestrator|FanucControllerRuntime" -v minimal -m:1 -nodeReuse:false`
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 5: Run integration verification**
|
||||
|
||||
Run: `dotnet test tests/Flyshot.Server.IntegrationTests/Flyshot.Server.IntegrationTests.csproj -v minimal -m:1 -nodeReuse:false`
|
||||
Expected: PASS, with existing HTTP compatibility tests still green.
|
||||
|
||||
### Task 4: Verify Solution Build And Update Progress Docs
|
||||
|
||||
**Files:**
|
||||
- Modify: `README.md`
|
||||
- Modify: `AGENTS.md`
|
||||
- Modify: `docs/controller-client-api-compatibility-requirements.md`
|
||||
|
||||
- [x] **Step 1: Update docs to reflect the new stage**
|
||||
|
||||
```markdown
|
||||
- [x] 落地最小 FANUC 运行时骨架
|
||||
- [x] 将 ExecuteTrajectory / ExecuteFlyShotTraj 接入 Planning + Triggering + Runtime
|
||||
- [ ] 落地真实 10010 / 10012 / 60015 通讯
|
||||
- [ ] 落地 Web 状态页
|
||||
```
|
||||
|
||||
- [x] **Step 2: Run final build**
|
||||
|
||||
Run: `dotnet build FlyshotReplacement.sln --no-restore -v minimal -m:1 -nodeReuse:false`
|
||||
Expected: PASS with 0 errors.
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace Flyshot.ControllerClientCompat;
|
||||
|
||||
/// <summary>
|
||||
/// 表示 HTTP-only ControllerClient 兼容层的基础配置。
|
||||
/// </summary>
|
||||
public sealed class ControllerClientCompatOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取或设置对外暴露的服务端版本号。
|
||||
/// </summary>
|
||||
public string ServerVersion { get; set; } = "flyshot-replacement-controller-client-compat/0.1.0";
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置父工作区根目录;为空时由运行时自动推断。
|
||||
/// </summary>
|
||||
public string? WorkspaceRoot { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
using Flyshot.Core.Config;
|
||||
using Flyshot.Core.Domain;
|
||||
|
||||
namespace Flyshot.ControllerClientCompat;
|
||||
|
||||
/// <summary>
|
||||
/// 根据旧版 ControllerClient 的机器人名称,解析当前 replacement 仓库支持的真实模型文件。
|
||||
/// </summary>
|
||||
public sealed class ControllerClientCompatRobotCatalog
|
||||
{
|
||||
/// <summary>
|
||||
/// 保存当前现场支持的机器人名称到模型相对路径映射。
|
||||
/// </summary>
|
||||
private static readonly IReadOnlyDictionary<string, string> SupportedRobotModelMap = new Dictionary<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
["FANUC_LR_Mate_200iD"] = Path.Combine("FlyingShot", "FlyingShot", "Models", "LR_Mate_200iD_7L.robot"),
|
||||
["FANUC_LR_Mate_200iD_7L"] = Path.Combine("FlyingShot", "FlyingShot", "Models", "LR_Mate_200iD_7L.robot")
|
||||
};
|
||||
|
||||
private readonly ControllerClientCompatOptions _options;
|
||||
private readonly RobotModelLoader _robotModelLoader;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化机器人兼容目录解析器。
|
||||
/// </summary>
|
||||
/// <param name="options">兼容层基础配置。</param>
|
||||
/// <param name="robotModelLoader">.robot 文件加载器。</param>
|
||||
public ControllerClientCompatRobotCatalog(
|
||||
ControllerClientCompatOptions options,
|
||||
RobotModelLoader robotModelLoader)
|
||||
{
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
_robotModelLoader = robotModelLoader ?? throw new ArgumentNullException(nameof(robotModelLoader));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据旧客户端的机器人名称加载对应模型。
|
||||
/// </summary>
|
||||
/// <param name="robotName">旧客户端传入的机器人名称。</param>
|
||||
/// <returns>兼容层加载出的机器人模型。</returns>
|
||||
public RobotProfile LoadProfile(string robotName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(robotName))
|
||||
{
|
||||
throw new ArgumentException("机器人名称不能为空。", nameof(robotName));
|
||||
}
|
||||
|
||||
if (!SupportedRobotModelMap.TryGetValue(robotName, out var modelRelativePath))
|
||||
{
|
||||
throw new InvalidOperationException($"Unsupported robot name: {robotName}");
|
||||
}
|
||||
|
||||
var workspaceRoot = ResolveWorkspaceRoot();
|
||||
var modelPath = Path.Combine(workspaceRoot, modelRelativePath);
|
||||
return _robotModelLoader.LoadProfile(modelPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析父工作区根目录,优先使用显式配置。
|
||||
/// </summary>
|
||||
/// <returns>包含 `FlyingShot/` 与 `Rvbust/` 的父工作区根目录。</returns>
|
||||
private string ResolveWorkspaceRoot()
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(_options.WorkspaceRoot))
|
||||
{
|
||||
return Path.GetFullPath(_options.WorkspaceRoot);
|
||||
}
|
||||
|
||||
var current = new DirectoryInfo(AppContext.BaseDirectory);
|
||||
while (current is not null)
|
||||
{
|
||||
// 宿主和测试都从 replacement 仓库内启动;找到 sln 后回退一级就是父工作区根目录。
|
||||
if (File.Exists(Path.Combine(current.FullName, "FlyshotReplacement.sln")))
|
||||
{
|
||||
return Path.GetFullPath(Path.Combine(current.FullName, ".."));
|
||||
}
|
||||
|
||||
current = current.Parent;
|
||||
}
|
||||
|
||||
throw new DirectoryNotFoundException("Unable to locate the flyshot workspace root.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,409 @@
|
||||
using Flyshot.Core.Domain;
|
||||
using Flyshot.Runtime.Common;
|
||||
|
||||
namespace Flyshot.ControllerClientCompat;
|
||||
|
||||
/// <summary>
|
||||
/// 在宿主进程内实现 HTTP-only ControllerClient 兼容语义,并把控制器状态委托给运行时。
|
||||
/// </summary>
|
||||
public sealed class ControllerClientCompatService : IControllerClientCompatService
|
||||
{
|
||||
private readonly object _stateLock = new();
|
||||
private readonly Dictionary<string, ControllerClientCompatUploadedTrajectory> _uploadedTrajectories = new(StringComparer.Ordinal);
|
||||
private readonly ControllerClientCompatOptions _options;
|
||||
private readonly ControllerClientCompatRobotCatalog _robotCatalog;
|
||||
private readonly IControllerRuntime _runtime;
|
||||
private readonly ControllerClientTrajectoryOrchestrator _trajectoryOrchestrator;
|
||||
private RobotProfile? _activeRobotProfile;
|
||||
private string? _configuredRobotName;
|
||||
private string? _connectedServerIp;
|
||||
private int _connectedServerPort;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化一份 HTTP-only 的 ControllerClient 兼容服务。
|
||||
/// </summary>
|
||||
/// <param name="options">兼容层基础配置。</param>
|
||||
/// <param name="robotCatalog">机器人模型目录。</param>
|
||||
/// <param name="runtime">控制器运行时。</param>
|
||||
/// <param name="trajectoryOrchestrator">轨迹规划与触发编排器。</param>
|
||||
public ControllerClientCompatService(
|
||||
ControllerClientCompatOptions options,
|
||||
ControllerClientCompatRobotCatalog robotCatalog,
|
||||
IControllerRuntime runtime,
|
||||
ControllerClientTrajectoryOrchestrator trajectoryOrchestrator)
|
||||
{
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
_robotCatalog = robotCatalog ?? throw new ArgumentNullException(nameof(robotCatalog));
|
||||
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
|
||||
_trajectoryOrchestrator = trajectoryOrchestrator ?? throw new ArgumentNullException(nameof(trajectoryOrchestrator));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string ServerVersion => _options.ServerVersion;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsSetUp
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_stateLock)
|
||||
{
|
||||
return _activeRobotProfile is not null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前运行时是否处于运动态。
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetUpRobot(string robotName)
|
||||
{
|
||||
var robotProfile = _robotCatalog.LoadProfile(robotName);
|
||||
|
||||
lock (_stateLock)
|
||||
{
|
||||
// 机器人重新初始化时同步重置运行时和上传轨迹目录,保持旧服务初始化语义。
|
||||
_configuredRobotName = robotName;
|
||||
_activeRobotProfile = robotProfile;
|
||||
_uploadedTrajectories.Clear();
|
||||
_runtime.ResetRobot(robotProfile, robotName);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetActiveController(bool sim)
|
||||
{
|
||||
lock (_stateLock)
|
||||
{
|
||||
EnsureRobotSetup();
|
||||
_runtime.SetActiveController(sim);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Connect(string robotIp)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(robotIp))
|
||||
{
|
||||
throw new ArgumentException("控制器 IP 不能为空。", nameof(robotIp));
|
||||
}
|
||||
|
||||
lock (_stateLock)
|
||||
{
|
||||
EnsureRobotSetup();
|
||||
_runtime.Connect(robotIp);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Disconnect()
|
||||
{
|
||||
lock (_stateLock)
|
||||
{
|
||||
EnsureRobotSetup();
|
||||
_runtime.Disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void EnableRobot(int bufferSize)
|
||||
{
|
||||
lock (_stateLock)
|
||||
{
|
||||
EnsureRobotSetup();
|
||||
_runtime.EnableRobot(bufferSize);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void DisableRobot()
|
||||
{
|
||||
lock (_stateLock)
|
||||
{
|
||||
EnsureRobotSetup();
|
||||
_runtime.DisableRobot();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void StopMove()
|
||||
{
|
||||
lock (_stateLock)
|
||||
{
|
||||
EnsureRobotSetup();
|
||||
_runtime.StopMove();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public double GetSpeedRatio()
|
||||
{
|
||||
lock (_stateLock)
|
||||
{
|
||||
EnsureRobotSetup();
|
||||
return _runtime.GetSpeedRatio();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetSpeedRatio(double ratio)
|
||||
{
|
||||
lock (_stateLock)
|
||||
{
|
||||
EnsureRobotSetup();
|
||||
_runtime.SetSpeedRatio(ratio);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetIo(int port, bool value, string ioType)
|
||||
{
|
||||
lock (_stateLock)
|
||||
{
|
||||
EnsureRobotSetup();
|
||||
_runtime.SetIo(port, value, ioType);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool GetIo(int port, string ioType)
|
||||
{
|
||||
lock (_stateLock)
|
||||
{
|
||||
EnsureRobotSetup();
|
||||
return _runtime.GetIo(port, ioType);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetTcp(double x, double y, double z)
|
||||
{
|
||||
lock (_stateLock)
|
||||
{
|
||||
EnsureRobotSetup();
|
||||
_runtime.SetTcp(x, y, z);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<double> GetTcp()
|
||||
{
|
||||
lock (_stateLock)
|
||||
{
|
||||
EnsureRobotSetup();
|
||||
return _runtime.GetTcp();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<double> GetJointPositions()
|
||||
{
|
||||
lock (_stateLock)
|
||||
{
|
||||
EnsureRobotSetup();
|
||||
return _runtime.GetJointPositions();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void MoveJoint(IReadOnlyList<double> jointPositions)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(jointPositions);
|
||||
|
||||
lock (_stateLock)
|
||||
{
|
||||
EnsureRobotSetup();
|
||||
_runtime.ExecuteTrajectory(CreateImmediateMoveResult(), jointPositions);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ExecuteTrajectory(IReadOnlyList<IReadOnlyList<double>> waypoints)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(waypoints);
|
||||
if (waypoints.Count == 0)
|
||||
{
|
||||
throw new ArgumentException("轨迹路点不能为空。", nameof(waypoints));
|
||||
}
|
||||
|
||||
lock (_stateLock)
|
||||
{
|
||||
var robot = RequireActiveRobot();
|
||||
EnsureRuntimeEnabled();
|
||||
|
||||
// 普通轨迹必须先通过 ICSP 规划,再把规划结果交给运行时执行。
|
||||
var bundle = _trajectoryOrchestrator.PlanOrdinaryTrajectory(robot, waypoints);
|
||||
var finalJointPositions = bundle.PlannedTrajectory.PlannedWaypoints[^1].Positions;
|
||||
_runtime.ExecuteTrajectory(bundle.Result, finalJointPositions);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<double> GetPose()
|
||||
{
|
||||
lock (_stateLock)
|
||||
{
|
||||
EnsureRobotSetup();
|
||||
return _runtime.GetPose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void UploadTrajectory(ControllerClientCompatUploadedTrajectory trajectory)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(trajectory);
|
||||
|
||||
lock (_stateLock)
|
||||
{
|
||||
EnsureRuntimeEnabled();
|
||||
_uploadedTrajectories[trajectory.Name] = trajectory;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<string> ListTrajectoryNames()
|
||||
{
|
||||
lock (_stateLock)
|
||||
{
|
||||
return _uploadedTrajectories.Keys.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ExecuteTrajectoryByName(string name)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
throw new ArgumentException("轨迹名称不能为空。", nameof(name));
|
||||
}
|
||||
|
||||
lock (_stateLock)
|
||||
{
|
||||
var robot = RequireActiveRobot();
|
||||
EnsureRuntimeEnabled();
|
||||
|
||||
if (!_uploadedTrajectories.TryGetValue(name, out var trajectory))
|
||||
{
|
||||
throw new InvalidOperationException("FlyShot trajectory does not exist.");
|
||||
}
|
||||
|
||||
if (trajectory.Waypoints.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("FlyShot trajectory contains no waypoints.");
|
||||
}
|
||||
|
||||
// 已上传飞拍轨迹必须生成 shot timeline 后再交给运行时。
|
||||
var bundle = _trajectoryOrchestrator.PlanUploadedFlyshot(robot, trajectory);
|
||||
var finalJointPositions = bundle.PlannedTrajectory.PlannedWaypoints[^1].Positions;
|
||||
_runtime.ExecuteTrajectory(bundle.Result, finalJointPositions);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void DeleteTrajectory(string name)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
throw new ArgumentException("轨迹名称不能为空。", nameof(name));
|
||||
}
|
||||
|
||||
lock (_stateLock)
|
||||
{
|
||||
if (!_uploadedTrajectories.Remove(name))
|
||||
{
|
||||
throw new InvalidOperationException("DeleteFlyShotTraj failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GetRobotName()
|
||||
{
|
||||
lock (_stateLock)
|
||||
{
|
||||
return _configuredRobotName ?? throw new InvalidOperationException("Robot has not been setup.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public int GetDegreesOfFreedom()
|
||||
{
|
||||
lock (_stateLock)
|
||||
{
|
||||
return _activeRobotProfile?.DegreesOfFreedom ?? throw new InvalidOperationException("Robot has not been setup.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前机器人配置,未初始化时抛出兼容错误。
|
||||
/// </summary>
|
||||
/// <returns>当前机器人配置。</returns>
|
||||
private RobotProfile RequireActiveRobot()
|
||||
{
|
||||
return _activeRobotProfile ?? throw new InvalidOperationException("Robot has not been setup.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 校验机器人已经完成初始化。
|
||||
/// </summary>
|
||||
private void EnsureRobotSetup()
|
||||
{
|
||||
_ = RequireActiveRobot();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 校验运行时已经处于可执行状态。
|
||||
/// </summary>
|
||||
private void EnsureRuntimeEnabled()
|
||||
{
|
||||
EnsureRobotSetup();
|
||||
if (!_runtime.GetSnapshot().IsEnabled)
|
||||
{
|
||||
throw new InvalidOperationException("Robot has not been enabled.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构造 MoveJoint 直达运行时所需的最小合法轨迹结果。
|
||||
/// </summary>
|
||||
/// <returns>可立即执行的轨迹结果。</returns>
|
||||
private static TrajectoryResult CreateImmediateMoveResult()
|
||||
{
|
||||
return new TrajectoryResult(
|
||||
programName: "move-joint",
|
||||
method: PlanningMethod.Icsp,
|
||||
isValid: true,
|
||||
duration: TimeSpan.Zero,
|
||||
shotEvents: Array.Empty<ShotEvent>(),
|
||||
triggerTimeline: Array.Empty<TrajectoryDoEvent>(),
|
||||
artifacts: Array.Empty<TrajectoryArtifact>(),
|
||||
failureReason: null,
|
||||
usedCache: false,
|
||||
originalWaypointCount: 1,
|
||||
plannedWaypointCount: 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using Flyshot.Core.Config;
|
||||
using Flyshot.Runtime.Common;
|
||||
using Flyshot.Runtime.Fanuc;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Flyshot.ControllerClientCompat;
|
||||
|
||||
/// <summary>
|
||||
/// 提供 ControllerClient HTTP 兼容层的依赖注入注册入口。
|
||||
/// </summary>
|
||||
public static class ControllerClientCompatServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 将 HTTP-only 的 ControllerClient 兼容服务注册到当前宿主。
|
||||
/// </summary>
|
||||
/// <param name="services">当前宿主服务集合。</param>
|
||||
/// <param name="configuration">宿主配置根。</param>
|
||||
/// <returns>同一服务集合,便于链式调用。</returns>
|
||||
public static IServiceCollection AddControllerClientCompat(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
ArgumentNullException.ThrowIfNull(configuration);
|
||||
|
||||
services
|
||||
.AddOptions<ControllerClientCompatOptions>()
|
||||
.Bind(configuration.GetSection("ControllerClientCompat"));
|
||||
|
||||
services.AddSingleton(static serviceProvider => serviceProvider.GetRequiredService<IOptions<ControllerClientCompatOptions>>().Value);
|
||||
services.AddSingleton<RobotModelLoader>();
|
||||
services.AddSingleton<ControllerClientCompatRobotCatalog>();
|
||||
services.AddSingleton<ControllerClientTrajectoryOrchestrator>();
|
||||
services.AddSingleton<IControllerRuntime, FanucControllerRuntime>();
|
||||
services.AddSingleton<IControllerClientCompatService, ControllerClientCompatService>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
namespace Flyshot.ControllerClientCompat;
|
||||
|
||||
/// <summary>
|
||||
/// 保存一条已上传到兼容层内存目录中的飞拍轨迹,供 HTTP API 层列出、执行和删除。
|
||||
/// </summary>
|
||||
public sealed class ControllerClientCompatUploadedTrajectory
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化一条飞拍轨迹快照,并立即复制所有数组,避免调用方后续原地修改。
|
||||
/// </summary>
|
||||
/// <param name="name">轨迹名称。</param>
|
||||
/// <param name="waypoints">关节路点集合。</param>
|
||||
/// <param name="shotFlags">拍摄标志集合。</param>
|
||||
/// <param name="offsetValues">偏移周期集合。</param>
|
||||
/// <param name="addressGroups">地址组集合。</param>
|
||||
public ControllerClientCompatUploadedTrajectory(
|
||||
string name,
|
||||
IEnumerable<IReadOnlyList<double>> waypoints,
|
||||
IEnumerable<bool> shotFlags,
|
||||
IEnumerable<int> offsetValues,
|
||||
IEnumerable<IReadOnlyList<int>> addressGroups)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
throw new ArgumentException("轨迹名称不能为空。", nameof(name));
|
||||
}
|
||||
|
||||
ArgumentNullException.ThrowIfNull(waypoints);
|
||||
ArgumentNullException.ThrowIfNull(shotFlags);
|
||||
ArgumentNullException.ThrowIfNull(offsetValues);
|
||||
ArgumentNullException.ThrowIfNull(addressGroups);
|
||||
|
||||
Name = name;
|
||||
Waypoints = waypoints.Select(static waypoint => waypoint.ToArray()).ToArray();
|
||||
ShotFlags = shotFlags.ToArray();
|
||||
OffsetValues = offsetValues.ToArray();
|
||||
AddressGroups = addressGroups.Select(static group => group.ToArray()).ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取轨迹名称。
|
||||
/// </summary>
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取已复制的关节路点集合。
|
||||
/// </summary>
|
||||
public IReadOnlyList<double[]> Waypoints { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取拍摄标志集合。
|
||||
/// </summary>
|
||||
public IReadOnlyList<bool> ShotFlags { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取偏移周期集合。
|
||||
/// </summary>
|
||||
public IReadOnlyList<int> OffsetValues { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取地址组集合。
|
||||
/// </summary>
|
||||
public IReadOnlyList<int[]> AddressGroups { get; }
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
using Flyshot.Core.Domain;
|
||||
using Flyshot.Core.Planning;
|
||||
using Flyshot.Core.Triggering;
|
||||
|
||||
namespace Flyshot.ControllerClientCompat;
|
||||
|
||||
/// <summary>
|
||||
/// 负责把 ControllerClient 兼容层的轨迹输入转换为规划结果和触发时间轴。
|
||||
/// </summary>
|
||||
public sealed class ControllerClientTrajectoryOrchestrator
|
||||
{
|
||||
private readonly ICspPlanner _icspPlanner = new();
|
||||
private readonly SelfAdaptIcspPlanner _selfAdaptIcspPlanner = new();
|
||||
private readonly ShotTimelineBuilder _shotTimelineBuilder = new(new WaypointTimestampResolver());
|
||||
|
||||
/// <summary>
|
||||
/// 对普通轨迹执行 ICSP 规划。
|
||||
/// </summary>
|
||||
/// <param name="robot">当前机器人配置。</param>
|
||||
/// <param name="waypoints">普通轨迹关节路点。</param>
|
||||
/// <returns>包含规划轨迹、空触发时间轴和执行结果的结果包。</returns>
|
||||
public PlannedExecutionBundle PlanOrdinaryTrajectory(
|
||||
RobotProfile robot,
|
||||
IReadOnlyList<IReadOnlyList<double>> waypoints)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(robot);
|
||||
ArgumentNullException.ThrowIfNull(waypoints);
|
||||
|
||||
var program = CreateProgram(
|
||||
name: "ordinary-trajectory",
|
||||
waypoints: waypoints,
|
||||
shotFlags: Enumerable.Repeat(false, waypoints.Count),
|
||||
offsetValues: Enumerable.Repeat(0, waypoints.Count),
|
||||
addressGroups: Enumerable.Range(0, waypoints.Count).Select(static _ => Array.Empty<int>()));
|
||||
|
||||
var request = new TrajectoryRequest(
|
||||
robot: robot,
|
||||
program: program,
|
||||
method: PlanningMethod.Icsp);
|
||||
|
||||
var plannedTrajectory = _icspPlanner.Plan(request);
|
||||
var shotTimeline = new ShotTimeline(Array.Empty<ShotEvent>(), Array.Empty<TrajectoryDoEvent>());
|
||||
var result = CreateResult(plannedTrajectory, shotTimeline);
|
||||
|
||||
return new PlannedExecutionBundle(plannedTrajectory, shotTimeline, result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 对已经上传的飞拍轨迹执行自适应 ICSP 规划并生成触发时间轴。
|
||||
/// </summary>
|
||||
/// <param name="robot">当前机器人配置。</param>
|
||||
/// <param name="uploaded">兼容层保存的上传轨迹。</param>
|
||||
/// <returns>包含规划轨迹、触发时间轴和执行结果的结果包。</returns>
|
||||
public PlannedExecutionBundle PlanUploadedFlyshot(RobotProfile robot, ControllerClientCompatUploadedTrajectory uploaded)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(robot);
|
||||
ArgumentNullException.ThrowIfNull(uploaded);
|
||||
|
||||
var program = CreateProgram(
|
||||
name: uploaded.Name,
|
||||
waypoints: uploaded.Waypoints,
|
||||
shotFlags: uploaded.ShotFlags,
|
||||
offsetValues: uploaded.OffsetValues,
|
||||
addressGroups: uploaded.AddressGroups);
|
||||
|
||||
var request = new TrajectoryRequest(
|
||||
robot: robot,
|
||||
program: program,
|
||||
method: PlanningMethod.SelfAdaptIcsp);
|
||||
|
||||
var plannedTrajectory = _selfAdaptIcspPlanner.Plan(request);
|
||||
var shotTimeline = _shotTimelineBuilder.Build(
|
||||
plannedTrajectory,
|
||||
holdCycles: 0,
|
||||
samplePeriod: robot.ServoPeriod);
|
||||
var result = CreateResult(plannedTrajectory, shotTimeline);
|
||||
|
||||
return new PlannedExecutionBundle(plannedTrajectory, shotTimeline, result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 把兼容层输入数组转换成领域层 FlyshotProgram。
|
||||
/// </summary>
|
||||
/// <param name="name">轨迹名称。</param>
|
||||
/// <param name="waypoints">关节路点。</param>
|
||||
/// <param name="shotFlags">拍照标志。</param>
|
||||
/// <param name="offsetValues">偏移周期。</param>
|
||||
/// <param name="addressGroups">IO 地址组。</param>
|
||||
/// <returns>领域层飞拍程序。</returns>
|
||||
private static FlyshotProgram CreateProgram(
|
||||
string name,
|
||||
IEnumerable<IReadOnlyList<double>> waypoints,
|
||||
IEnumerable<bool> shotFlags,
|
||||
IEnumerable<int> offsetValues,
|
||||
IEnumerable<IReadOnlyList<int>> addressGroups)
|
||||
{
|
||||
return new FlyshotProgram(
|
||||
name: name,
|
||||
waypoints: waypoints.Select(static waypoint => new JointWaypoint(waypoint)).ToArray(),
|
||||
shotFlags: shotFlags.ToArray(),
|
||||
offsetValues: offsetValues.ToArray(),
|
||||
addressGroups: addressGroups.Select(static group => new IoAddressGroup(group)).ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从规划轨迹和触发时间轴构造运行时可消费的稳定结果对象。
|
||||
/// </summary>
|
||||
/// <param name="plannedTrajectory">规划后的轨迹。</param>
|
||||
/// <param name="shotTimeline">触发时间轴。</param>
|
||||
/// <returns>运行时执行结果描述。</returns>
|
||||
private static TrajectoryResult CreateResult(PlannedTrajectory plannedTrajectory, ShotTimeline shotTimeline)
|
||||
{
|
||||
return new TrajectoryResult(
|
||||
programName: plannedTrajectory.OriginalProgram.Name,
|
||||
method: plannedTrajectory.Method,
|
||||
isValid: true,
|
||||
duration: TimeSpan.FromSeconds(plannedTrajectory.WaypointTimes[^1]),
|
||||
shotEvents: shotTimeline.ShotEvents,
|
||||
triggerTimeline: shotTimeline.TriggerTimeline,
|
||||
artifacts: Array.Empty<TrajectoryArtifact>(),
|
||||
failureReason: null,
|
||||
usedCache: false,
|
||||
originalWaypointCount: plannedTrajectory.OriginalWaypointCount,
|
||||
plannedWaypointCount: plannedTrajectory.PlannedWaypointCount);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Flyshot.Core.Config\Flyshot.Core.Config.csproj" />
|
||||
<ProjectReference Include="..\Flyshot.Core.Domain\Flyshot.Core.Domain.csproj" />
|
||||
<ProjectReference Include="..\Flyshot.Core.Planning\Flyshot.Core.Planning.csproj" />
|
||||
<ProjectReference Include="..\Flyshot.Core.Triggering\Flyshot.Core.Triggering.csproj" />
|
||||
<ProjectReference Include="..\Flyshot.Runtime.Common\Flyshot.Runtime.Common.csproj" />
|
||||
<ProjectReference Include="..\Flyshot.Runtime.Fanuc\Flyshot.Runtime.Fanuc.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,165 @@
|
||||
namespace Flyshot.ControllerClientCompat;
|
||||
|
||||
/// <summary>
|
||||
/// 定义 HTTP-only 兼容层对外暴露的 ControllerClient 语义服务接口。
|
||||
/// </summary>
|
||||
public interface IControllerClientCompatService
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取当前兼容层对外报告的服务端版本号。
|
||||
/// </summary>
|
||||
string ServerVersion { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前是否已经完成机器人初始化。
|
||||
/// </summary>
|
||||
bool IsSetUp { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 保存当前调用方期望连接的 replacement 服务端地址。
|
||||
/// </summary>
|
||||
/// <param name="serverIp">客户端传入的服务端 IP。</param>
|
||||
/// <param name="port">客户端传入的服务端端口。</param>
|
||||
void ConnectServer(string serverIp, int port);
|
||||
|
||||
/// <summary>
|
||||
/// 根据旧客户端使用的机器人名称完成机器人初始化。
|
||||
/// </summary>
|
||||
/// <param name="robotName">机器人名称。</param>
|
||||
void SetUpRobot(string robotName);
|
||||
|
||||
/// <summary>
|
||||
/// 记录当前激活的控制器类型。
|
||||
/// </summary>
|
||||
/// <param name="sim">是否为仿真控制器。</param>
|
||||
void SetActiveController(bool sim);
|
||||
|
||||
/// <summary>
|
||||
/// 记录当前控制器已经建立连接。
|
||||
/// </summary>
|
||||
/// <param name="robotIp">控制器 IP。</param>
|
||||
void Connect(string robotIp);
|
||||
|
||||
/// <summary>
|
||||
/// 记录当前控制器已经断开。
|
||||
/// </summary>
|
||||
void Disconnect();
|
||||
|
||||
/// <summary>
|
||||
/// 记录当前机器人进入使能态。
|
||||
/// </summary>
|
||||
/// <param name="bufferSize">缓冲区大小。</param>
|
||||
void EnableRobot(int bufferSize);
|
||||
|
||||
/// <summary>
|
||||
/// 记录当前机器人退出使能态。
|
||||
/// </summary>
|
||||
void DisableRobot();
|
||||
|
||||
/// <summary>
|
||||
/// 停止当前运动状态。
|
||||
/// </summary>
|
||||
void StopMove();
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前速度倍率。
|
||||
/// </summary>
|
||||
/// <returns>当前速度倍率。</returns>
|
||||
double GetSpeedRatio();
|
||||
|
||||
/// <summary>
|
||||
/// 更新当前速度倍率。
|
||||
/// </summary>
|
||||
/// <param name="ratio">目标速度倍率。</param>
|
||||
void SetSpeedRatio(double ratio);
|
||||
|
||||
/// <summary>
|
||||
/// 写入兼容层缓存的 IO 数值。
|
||||
/// </summary>
|
||||
/// <param name="port">IO 端口号。</param>
|
||||
/// <param name="value">IO 值。</param>
|
||||
/// <param name="ioType">IO 类型。</param>
|
||||
void SetIo(int port, bool value, string ioType);
|
||||
|
||||
/// <summary>
|
||||
/// 读取兼容层缓存的 IO 数值。
|
||||
/// </summary>
|
||||
/// <param name="port">IO 端口号。</param>
|
||||
/// <param name="ioType">IO 类型。</param>
|
||||
/// <returns>缓存中的 IO 值。</returns>
|
||||
bool GetIo(int port, string ioType);
|
||||
|
||||
/// <summary>
|
||||
/// 设置当前 TCP 三维坐标。
|
||||
/// </summary>
|
||||
/// <param name="x">TCP X。</param>
|
||||
/// <param name="y">TCP Y。</param>
|
||||
/// <param name="z">TCP Z。</param>
|
||||
void SetTcp(double x, double y, double z);
|
||||
|
||||
/// <summary>
|
||||
/// 读取当前 TCP 三维坐标。
|
||||
/// </summary>
|
||||
/// <returns>TCP 数组。</returns>
|
||||
IReadOnlyList<double> GetTcp();
|
||||
|
||||
/// <summary>
|
||||
/// 读取当前关节位置。
|
||||
/// </summary>
|
||||
/// <returns>关节位置数组。</returns>
|
||||
IReadOnlyList<double> GetJointPositions();
|
||||
|
||||
/// <summary>
|
||||
/// 更新当前关节位置。
|
||||
/// </summary>
|
||||
/// <param name="jointPositions">目标关节位置。</param>
|
||||
void MoveJoint(IReadOnlyList<double> jointPositions);
|
||||
|
||||
/// <summary>
|
||||
/// 执行普通轨迹。
|
||||
/// </summary>
|
||||
/// <param name="waypoints">轨迹路点集合。</param>
|
||||
void ExecuteTrajectory(IReadOnlyList<IReadOnlyList<double>> waypoints);
|
||||
|
||||
/// <summary>
|
||||
/// 读取当前末端位姿快照。
|
||||
/// </summary>
|
||||
/// <returns>位姿数组。</returns>
|
||||
IReadOnlyList<double> GetPose();
|
||||
|
||||
/// <summary>
|
||||
/// 上传一条飞拍轨迹。
|
||||
/// </summary>
|
||||
/// <param name="trajectory">飞拍轨迹。</param>
|
||||
void UploadTrajectory(ControllerClientCompatUploadedTrajectory trajectory);
|
||||
|
||||
/// <summary>
|
||||
/// 列出当前已上传的飞拍轨迹名称。
|
||||
/// </summary>
|
||||
/// <returns>轨迹名称列表。</returns>
|
||||
IReadOnlyList<string> ListTrajectoryNames();
|
||||
|
||||
/// <summary>
|
||||
/// 执行指定名称的飞拍轨迹。
|
||||
/// </summary>
|
||||
/// <param name="name">轨迹名称。</param>
|
||||
void ExecuteTrajectoryByName(string name);
|
||||
|
||||
/// <summary>
|
||||
/// 删除指定名称的飞拍轨迹。
|
||||
/// </summary>
|
||||
/// <param name="name">轨迹名称。</param>
|
||||
void DeleteTrajectory(string name);
|
||||
|
||||
/// <summary>
|
||||
/// 读取当前配置过的机器人名称。
|
||||
/// </summary>
|
||||
/// <returns>机器人名称。</returns>
|
||||
string GetRobotName();
|
||||
|
||||
/// <summary>
|
||||
/// 读取当前机器人自由度。
|
||||
/// </summary>
|
||||
/// <returns>机器人自由度。</returns>
|
||||
int GetDegreesOfFreedom();
|
||||
}
|
||||
39
src/Flyshot.ControllerClientCompat/PlannedExecutionBundle.cs
Normal file
39
src/Flyshot.ControllerClientCompat/PlannedExecutionBundle.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using Flyshot.Core.Domain;
|
||||
using Flyshot.Core.Planning;
|
||||
using Flyshot.Core.Triggering;
|
||||
|
||||
namespace Flyshot.ControllerClientCompat;
|
||||
|
||||
/// <summary>
|
||||
/// 表示兼容层执行轨迹前生成的完整规划结果包。
|
||||
/// </summary>
|
||||
public sealed class PlannedExecutionBundle
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化一份执行规划结果包。
|
||||
/// </summary>
|
||||
/// <param name="plannedTrajectory">规划后的轨迹。</param>
|
||||
/// <param name="shotTimeline">飞拍触发时间轴。</param>
|
||||
/// <param name="result">对运行时和监控层暴露的规划结果。</param>
|
||||
public PlannedExecutionBundle(PlannedTrajectory plannedTrajectory, ShotTimeline shotTimeline, TrajectoryResult result)
|
||||
{
|
||||
PlannedTrajectory = plannedTrajectory ?? throw new ArgumentNullException(nameof(plannedTrajectory));
|
||||
ShotTimeline = shotTimeline ?? throw new ArgumentNullException(nameof(shotTimeline));
|
||||
Result = result ?? throw new ArgumentNullException(nameof(result));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取规划后的轨迹。
|
||||
/// </summary>
|
||||
public PlannedTrajectory PlannedTrajectory { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取飞拍触发时间轴。
|
||||
/// </summary>
|
||||
public ShotTimeline ShotTimeline { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取运行时可消费的规划结果。
|
||||
/// </summary>
|
||||
public TrajectoryResult Result { get; }
|
||||
}
|
||||
@@ -6,4 +6,8 @@
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Flyshot.Core.Domain\Flyshot.Core.Domain.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
116
src/Flyshot.Runtime.Common/IControllerRuntime.cs
Normal file
116
src/Flyshot.Runtime.Common/IControllerRuntime.cs
Normal file
@@ -0,0 +1,116 @@
|
||||
using Flyshot.Core.Domain;
|
||||
|
||||
namespace Flyshot.Runtime.Common;
|
||||
|
||||
/// <summary>
|
||||
/// 定义控制器运行时的最小状态与执行契约,供兼容层在不关心底层 Socket 细节的情况下调度轨迹。
|
||||
/// </summary>
|
||||
public interface IControllerRuntime
|
||||
{
|
||||
/// <summary>
|
||||
/// 重置当前机器人模型并清空控制器运行时状态。
|
||||
/// </summary>
|
||||
/// <param name="robot">当前机器人配置。</param>
|
||||
/// <param name="robotName">兼容层传入的机器人名称。</param>
|
||||
void ResetRobot(RobotProfile robot, string robotName);
|
||||
|
||||
/// <summary>
|
||||
/// 选择当前活动控制器类型。
|
||||
/// </summary>
|
||||
/// <param name="sim">是否使用仿真控制器。</param>
|
||||
void SetActiveController(bool sim);
|
||||
|
||||
/// <summary>
|
||||
/// 建立到控制器 IP 的连接。
|
||||
/// </summary>
|
||||
/// <param name="robotIp">控制器 IP。</param>
|
||||
void Connect(string robotIp);
|
||||
|
||||
/// <summary>
|
||||
/// 断开当前控制器连接。
|
||||
/// </summary>
|
||||
void Disconnect();
|
||||
|
||||
/// <summary>
|
||||
/// 使能机器人并记录底层缓冲区大小。
|
||||
/// </summary>
|
||||
/// <param name="bufferSize">运行时缓冲区大小。</param>
|
||||
void EnableRobot(int bufferSize);
|
||||
|
||||
/// <summary>
|
||||
/// 关闭机器人使能。
|
||||
/// </summary>
|
||||
void DisableRobot();
|
||||
|
||||
/// <summary>
|
||||
/// 停止当前运动。
|
||||
/// </summary>
|
||||
void StopMove();
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前速度倍率。
|
||||
/// </summary>
|
||||
/// <returns>速度倍率。</returns>
|
||||
double GetSpeedRatio();
|
||||
|
||||
/// <summary>
|
||||
/// 设置当前速度倍率。
|
||||
/// </summary>
|
||||
/// <param name="ratio">目标速度倍率。</param>
|
||||
void SetSpeedRatio(double ratio);
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前 TCP 坐标。
|
||||
/// </summary>
|
||||
/// <returns>TCP 三维坐标。</returns>
|
||||
IReadOnlyList<double> GetTcp();
|
||||
|
||||
/// <summary>
|
||||
/// 设置当前 TCP 坐标。
|
||||
/// </summary>
|
||||
/// <param name="x">TCP X。</param>
|
||||
/// <param name="y">TCP Y。</param>
|
||||
/// <param name="z">TCP Z。</param>
|
||||
void SetTcp(double x, double y, double z);
|
||||
|
||||
/// <summary>
|
||||
/// 读取指定 IO 端口。
|
||||
/// </summary>
|
||||
/// <param name="port">IO 端口。</param>
|
||||
/// <param name="ioType">IO 类型。</param>
|
||||
/// <returns>IO 当前值。</returns>
|
||||
bool GetIo(int port, string ioType);
|
||||
|
||||
/// <summary>
|
||||
/// 写入指定 IO 端口。
|
||||
/// </summary>
|
||||
/// <param name="port">IO 端口。</param>
|
||||
/// <param name="value">目标 IO 值。</param>
|
||||
/// <param name="ioType">IO 类型。</param>
|
||||
void SetIo(int port, bool value, string ioType);
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前关节位置。
|
||||
/// </summary>
|
||||
/// <returns>当前关节位置。</returns>
|
||||
IReadOnlyList<double> GetJointPositions();
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前末端位姿。
|
||||
/// </summary>
|
||||
/// <returns>当前末端位姿。</returns>
|
||||
IReadOnlyList<double> GetPose();
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前运行时状态快照。
|
||||
/// </summary>
|
||||
/// <returns>控制器状态快照。</returns>
|
||||
ControllerStateSnapshot GetSnapshot();
|
||||
|
||||
/// <summary>
|
||||
/// 执行一条已经完成规划的轨迹,并更新最终关节位置。
|
||||
/// </summary>
|
||||
/// <param name="result">规划结果。</param>
|
||||
/// <param name="finalJointPositions">轨迹执行结束后的关节位置。</param>
|
||||
void ExecuteTrajectory(TrajectoryResult result, IReadOnlyList<double> finalJointPositions);
|
||||
}
|
||||
366
src/Flyshot.Runtime.Fanuc/FanucControllerRuntime.cs
Normal file
366
src/Flyshot.Runtime.Fanuc/FanucControllerRuntime.cs
Normal file
@@ -0,0 +1,366 @@
|
||||
using Flyshot.Core.Domain;
|
||||
using Flyshot.Runtime.Common;
|
||||
|
||||
namespace Flyshot.Runtime.Fanuc;
|
||||
|
||||
/// <summary>
|
||||
/// 提供第一阶段 FANUC 控制器运行时骨架,集中保存连接、使能、IO 和运动结果状态。
|
||||
/// </summary>
|
||||
public sealed class FanucControllerRuntime : IControllerRuntime
|
||||
{
|
||||
private readonly object _stateLock = new();
|
||||
private readonly Dictionary<(string IoType, int Port), bool> _ioValues = new();
|
||||
private RobotProfile? _robot;
|
||||
private string? _robotName;
|
||||
private bool? _activeControllerIsSimulation;
|
||||
private string? _connectedRobotIp;
|
||||
private bool _isEnabled;
|
||||
private bool _isInMotion;
|
||||
private int _bufferSize;
|
||||
private double _speedRatio = 1.0;
|
||||
private double[] _tcp = [0.0, 0.0, 0.0];
|
||||
private double[] _jointPositions = 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)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(robot);
|
||||
if (string.IsNullOrWhiteSpace(robotName))
|
||||
{
|
||||
throw new ArgumentException("机器人名称不能为空。", nameof(robotName));
|
||||
}
|
||||
|
||||
lock (_stateLock)
|
||||
{
|
||||
// 重新初始化机器人时清空底层控制器状态,匹配旧 ControllerClient 的初始化顺序。
|
||||
_robot = robot;
|
||||
_robotName = robotName;
|
||||
_activeControllerIsSimulation = null;
|
||||
_connectedRobotIp = null;
|
||||
_isEnabled = false;
|
||||
_isInMotion = false;
|
||||
_bufferSize = 0;
|
||||
_speedRatio = 1.0;
|
||||
_tcp = [0.0, 0.0, 0.0];
|
||||
_jointPositions = new double[robot.DegreesOfFreedom];
|
||||
_pose = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0];
|
||||
_ioValues.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetActiveController(bool sim)
|
||||
{
|
||||
lock (_stateLock)
|
||||
{
|
||||
EnsureRobotSetup();
|
||||
_activeControllerIsSimulation = sim;
|
||||
_connectedRobotIp = null;
|
||||
_isEnabled = false;
|
||||
_isInMotion = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Connect(string robotIp)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(robotIp))
|
||||
{
|
||||
throw new ArgumentException("控制器 IP 不能为空。", nameof(robotIp));
|
||||
}
|
||||
|
||||
lock (_stateLock)
|
||||
{
|
||||
EnsureActiveControllerSelected();
|
||||
_connectedRobotIp = robotIp;
|
||||
_isEnabled = false;
|
||||
_isInMotion = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Disconnect()
|
||||
{
|
||||
lock (_stateLock)
|
||||
{
|
||||
EnsureRobotSetup();
|
||||
_connectedRobotIp = null;
|
||||
_isEnabled = false;
|
||||
_isInMotion = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void EnableRobot(int bufferSize)
|
||||
{
|
||||
if (bufferSize <= 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(bufferSize), "buffer_size 必须大于 0。");
|
||||
}
|
||||
|
||||
lock (_stateLock)
|
||||
{
|
||||
EnsureConnected();
|
||||
_bufferSize = bufferSize;
|
||||
_isEnabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void DisableRobot()
|
||||
{
|
||||
lock (_stateLock)
|
||||
{
|
||||
EnsureRobotSetup();
|
||||
_isEnabled = false;
|
||||
_isInMotion = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void StopMove()
|
||||
{
|
||||
lock (_stateLock)
|
||||
{
|
||||
EnsureRobotSetup();
|
||||
_isInMotion = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public double GetSpeedRatio()
|
||||
{
|
||||
lock (_stateLock)
|
||||
{
|
||||
EnsureConnected();
|
||||
return _speedRatio;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetSpeedRatio(double ratio)
|
||||
{
|
||||
if (double.IsNaN(ratio) || double.IsInfinity(ratio))
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(ratio), "ratio 必须是有限数值。");
|
||||
}
|
||||
|
||||
lock (_stateLock)
|
||||
{
|
||||
EnsureConnected();
|
||||
_speedRatio = Math.Clamp(ratio, 0.0, 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<double> GetTcp()
|
||||
{
|
||||
lock (_stateLock)
|
||||
{
|
||||
EnsureRobotSetup();
|
||||
return _tcp.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetTcp(double x, double y, double z)
|
||||
{
|
||||
lock (_stateLock)
|
||||
{
|
||||
EnsureRobotSetup();
|
||||
_tcp = [x, y, z];
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool GetIo(int port, string ioType)
|
||||
{
|
||||
if (port < 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(port), "IO 端口不能为负数。");
|
||||
}
|
||||
|
||||
var normalizedIoType = NormalizeIoType(ioType);
|
||||
|
||||
lock (_stateLock)
|
||||
{
|
||||
EnsureConnected();
|
||||
return _ioValues.TryGetValue((normalizedIoType, port), out var value) && value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetIo(int port, bool value, string ioType)
|
||||
{
|
||||
if (port < 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(port), "IO 端口不能为负数。");
|
||||
}
|
||||
|
||||
var normalizedIoType = NormalizeIoType(ioType);
|
||||
|
||||
lock (_stateLock)
|
||||
{
|
||||
EnsureEnabled();
|
||||
_ioValues[(normalizedIoType, port)] = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<double> GetJointPositions()
|
||||
{
|
||||
lock (_stateLock)
|
||||
{
|
||||
EnsureRobotSetup();
|
||||
return _jointPositions.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<double> GetPose()
|
||||
{
|
||||
lock (_stateLock)
|
||||
{
|
||||
EnsureRobotSetup();
|
||||
return _pose.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ControllerStateSnapshot GetSnapshot()
|
||||
{
|
||||
lock (_stateLock)
|
||||
{
|
||||
return new ControllerStateSnapshot(
|
||||
capturedAt: DateTimeOffset.UtcNow,
|
||||
connectionState: ResolveConnectionState(),
|
||||
isEnabled: _isEnabled,
|
||||
isInMotion: _isInMotion,
|
||||
speedRatio: _speedRatio,
|
||||
jointPositions: _jointPositions,
|
||||
cartesianPose: _pose,
|
||||
activeAlarms: Array.Empty<RuntimeAlarm>());
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ExecuteTrajectory(TrajectoryResult result, IReadOnlyList<double> finalJointPositions)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(result);
|
||||
ArgumentNullException.ThrowIfNull(finalJointPositions);
|
||||
|
||||
lock (_stateLock)
|
||||
{
|
||||
EnsureEnabled();
|
||||
EnsureValidTrajectory(result);
|
||||
EnsureJointCount(finalJointPositions.Count);
|
||||
|
||||
// 第一阶段没有真机 Socket 流,先把执行结果收敛到统一运行时状态。
|
||||
_isInMotion = true;
|
||||
_jointPositions = finalJointPositions.ToArray();
|
||||
_isInMotion = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 归一化 IO 类型字符串,避免调用方大小写差异影响缓存键。
|
||||
/// </summary>
|
||||
/// <param name="ioType">调用方传入的 IO 类型。</param>
|
||||
/// <returns>标准化后的 IO 类型。</returns>
|
||||
private static string NormalizeIoType(string ioType)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(ioType))
|
||||
{
|
||||
throw new ArgumentException("IO 类型不能为空。", nameof(ioType));
|
||||
}
|
||||
|
||||
return ioType.Trim().ToUpperInvariant();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 校验轨迹规划结果可执行。
|
||||
/// </summary>
|
||||
/// <param name="result">规划结果。</param>
|
||||
private static void EnsureValidTrajectory(TrajectoryResult result)
|
||||
{
|
||||
if (!result.IsValid)
|
||||
{
|
||||
throw new InvalidOperationException(result.FailureReason ?? "Trajectory result is invalid.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据当前内部状态生成连接状态标签。
|
||||
/// </summary>
|
||||
/// <returns>面向监控和测试的连接状态。</returns>
|
||||
private string ResolveConnectionState()
|
||||
{
|
||||
if (_robot is null)
|
||||
{
|
||||
return "NotConfigured";
|
||||
}
|
||||
|
||||
return string.IsNullOrWhiteSpace(_connectedRobotIp) ? "Disconnected" : "Connected";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 校验给定关节数组长度与当前机器人自由度一致。
|
||||
/// </summary>
|
||||
/// <param name="jointCount">调用方传入的关节数。</param>
|
||||
private void EnsureJointCount(int jointCount)
|
||||
{
|
||||
var expectedJointCount = _robot?.DegreesOfFreedom ?? throw new InvalidOperationException("Robot has not been setup.");
|
||||
if (jointCount != expectedJointCount)
|
||||
{
|
||||
throw new InvalidOperationException($"Expected {expectedJointCount} joints but received {jointCount}.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 校验机器人已经完成初始化。
|
||||
/// </summary>
|
||||
private void EnsureRobotSetup()
|
||||
{
|
||||
if (_robot is null || string.IsNullOrWhiteSpace(_robotName))
|
||||
{
|
||||
throw new InvalidOperationException("Robot has not been setup.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 校验活动控制器已经被选择。
|
||||
/// </summary>
|
||||
private void EnsureActiveControllerSelected()
|
||||
{
|
||||
EnsureRobotSetup();
|
||||
if (_activeControllerIsSimulation is null)
|
||||
{
|
||||
throw new InvalidOperationException("Active controller has not been selected.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 校验控制器已经建立连接。
|
||||
/// </summary>
|
||||
private void EnsureConnected()
|
||||
{
|
||||
EnsureActiveControllerSelected();
|
||||
if (string.IsNullOrWhiteSpace(_connectedRobotIp))
|
||||
{
|
||||
throw new InvalidOperationException("Controller has not been connected.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 校验机器人已经处于使能态。
|
||||
/// </summary>
|
||||
private void EnsureEnabled()
|
||||
{
|
||||
EnsureConnected();
|
||||
if (!_isEnabled || _bufferSize <= 0)
|
||||
{
|
||||
throw new InvalidOperationException("Robot has not been enabled.");
|
||||
}
|
||||
}
|
||||
}
|
||||
14
src/Flyshot.Runtime.Fanuc/Flyshot.Runtime.Fanuc.csproj
Normal file
14
src/Flyshot.Runtime.Fanuc/Flyshot.Runtime.Fanuc.csproj
Normal file
@@ -0,0 +1,14 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Flyshot.Core.Domain\Flyshot.Core.Domain.csproj" />
|
||||
<ProjectReference Include="..\Flyshot.Runtime.Common\Flyshot.Runtime.Common.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
24
src/Flyshot.Server.Host/Controllers/HealthController.cs
Normal file
24
src/Flyshot.Server.Host/Controllers/HealthController.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Flyshot.Server.Host.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 提供宿主基础探活与诊断接口。
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
public sealed class HealthController : ControllerBase
|
||||
{
|
||||
/// <summary>
|
||||
/// 返回宿主健康状态。
|
||||
/// </summary>
|
||||
/// <returns>固定的健康检查 JSON。</returns>
|
||||
[HttpGet("/healthz")]
|
||||
public IActionResult GetHealth()
|
||||
{
|
||||
return Ok(new
|
||||
{
|
||||
status = "ok",
|
||||
service = "flyshot-server-host"
|
||||
});
|
||||
}
|
||||
}
|
||||
614
src/Flyshot.Server.Host/Controllers/LegacyHttpApiController.cs
Normal file
614
src/Flyshot.Server.Host/Controllers/LegacyHttpApiController.cs
Normal file
@@ -0,0 +1,614 @@
|
||||
using System.Text.Json;
|
||||
using Flyshot.ControllerClientCompat;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Flyshot.Server.Host.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 提供对 `flyshot-uaes-interface` 既有 FastAPI HTTP 路由层的一比一 MVC 兼容控制器。
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
public sealed class LegacyHttpApiController : ControllerBase
|
||||
{
|
||||
private readonly IControllerClientCompatService _compatService;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化旧 HTTP 兼容控制器。
|
||||
/// </summary>
|
||||
/// <param name="compatService">ControllerClient 兼容服务。</param>
|
||||
public LegacyHttpApiController(IControllerClientCompatService compatService)
|
||||
{
|
||||
_compatService = compatService ?? throw new ArgumentNullException(nameof(compatService));
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
try
|
||||
{
|
||||
_compatService.ConnectServer(server_ip, port);
|
||||
return Ok(new { status = "connected" });
|
||||
}
|
||||
catch
|
||||
{
|
||||
return LegacyBadRequest("Connect Server failed");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 兼容旧 `/setup_robot/` 路由。
|
||||
/// </summary>
|
||||
/// <param name="robot_name">旧 HTTP 层使用的机器人名称。</param>
|
||||
/// <returns>旧 FastAPI 层风格的状态响应。</returns>
|
||||
[HttpPost("/setup_robot/")]
|
||||
public IActionResult SetupRobot([FromQuery] string robot_name)
|
||||
{
|
||||
try
|
||||
{
|
||||
_compatService.SetUpRobot(robot_name);
|
||||
return Ok(new { status = "robot setup" });
|
||||
}
|
||||
catch
|
||||
{
|
||||
return LegacyBadRequest("SetUpRobot failed");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 兼容旧 `/is_setup/` 路由。
|
||||
/// </summary>
|
||||
/// <returns>当前机器人是否完成初始化。</returns>
|
||||
[HttpGet("/is_setup/")]
|
||||
public IActionResult IsSetup()
|
||||
{
|
||||
return Ok(new { is_setup = _compatService.IsSetUp });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 兼容旧 `/enable_robot/` 路由;保持原 Python 服务固定传 `8` 的行为。
|
||||
/// </summary>
|
||||
/// <returns>旧 FastAPI 层风格的布尔状态响应。</returns>
|
||||
[HttpGet("/enable_robot/")]
|
||||
public IActionResult EnableRobot()
|
||||
{
|
||||
try
|
||||
{
|
||||
_compatService.EnableRobot(8);
|
||||
return Ok(new { enable_robot = true });
|
||||
}
|
||||
catch
|
||||
{
|
||||
return LegacyBadRequest("EnableRobot failed");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 兼容旧 `/disable_robot/` 路由。
|
||||
/// </summary>
|
||||
/// <returns>旧 FastAPI 层风格的布尔状态响应。</returns>
|
||||
[HttpGet("/disable_robot/")]
|
||||
public IActionResult DisableRobot()
|
||||
{
|
||||
try
|
||||
{
|
||||
_compatService.DisableRobot();
|
||||
return Ok(new { disable_robot = true });
|
||||
}
|
||||
catch
|
||||
{
|
||||
return LegacyBadRequest("DisableRobot failed");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 兼容旧 `/set_active_controller/` 路由。
|
||||
/// </summary>
|
||||
/// <param name="sim">是否切到仿真控制器。</param>
|
||||
/// <returns>旧 FastAPI 层风格的状态响应。</returns>
|
||||
[HttpPost("/set_active_controller/")]
|
||||
public IActionResult SetActiveController([FromQuery] bool sim)
|
||||
{
|
||||
try
|
||||
{
|
||||
_compatService.SetActiveController(sim);
|
||||
return Ok(new { status = "active controller set" });
|
||||
}
|
||||
catch
|
||||
{
|
||||
return LegacyBadRequest("SetActiveController failed");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 兼容旧 `/connect_robot/` 路由。
|
||||
/// </summary>
|
||||
/// <param name="ip">控制器 IP。</param>
|
||||
/// <returns>旧 FastAPI 层风格的状态响应。</returns>
|
||||
[HttpPost("/connect_robot/")]
|
||||
public IActionResult ConnectRobot([FromQuery] string ip)
|
||||
{
|
||||
try
|
||||
{
|
||||
_compatService.Connect(ip);
|
||||
return Ok(new { status = "robot connected" });
|
||||
}
|
||||
catch
|
||||
{
|
||||
return LegacyBadRequest("Connect failed");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 兼容旧 `/robot_info/` 路由。
|
||||
/// </summary>
|
||||
/// <returns>旧 HTTP 层聚合的机器人元信息。</returns>
|
||||
[HttpGet("/robot_info/")]
|
||||
public IActionResult GetRobotInfo()
|
||||
{
|
||||
try
|
||||
{
|
||||
return Ok(new
|
||||
{
|
||||
name = _compatService.GetRobotName(),
|
||||
server_version = _compatService.ServerVersion,
|
||||
dof = _compatService.GetDegreesOfFreedom(),
|
||||
speed_ratio = _compatService.GetSpeedRatio()
|
||||
});
|
||||
}
|
||||
catch
|
||||
{
|
||||
return LegacyBadRequest("GetRobotInfo failed");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 兼容旧 `/set_tcp/` 路由。
|
||||
/// </summary>
|
||||
/// <param name="tcp_data">三维 TCP 请求体。</param>
|
||||
/// <returns>旧 FastAPI 层风格的状态响应。</returns>
|
||||
[HttpPost("/set_tcp/")]
|
||||
public IActionResult SetTcp([FromBody] LegacyTcpRequest tcp_data)
|
||||
{
|
||||
try
|
||||
{
|
||||
_compatService.SetTcp(tcp_data.x, tcp_data.y, tcp_data.z);
|
||||
return Ok(new { status = "TCP set" });
|
||||
}
|
||||
catch
|
||||
{
|
||||
return LegacyBadRequest("SetTCP failed");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 兼容旧 `/get_tcp/` 路由。
|
||||
/// </summary>
|
||||
/// <returns>当前 TCP 三维坐标。</returns>
|
||||
[HttpGet("/get_tcp/")]
|
||||
public IActionResult GetTcp()
|
||||
{
|
||||
try
|
||||
{
|
||||
return Ok(new { tcp = _compatService.GetTcp() });
|
||||
}
|
||||
catch
|
||||
{
|
||||
return LegacyBadRequest("GetTCP failed");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 兼容旧 `/set_io/` 路由。
|
||||
/// </summary>
|
||||
/// <param name="port">IO 端口号。</param>
|
||||
/// <param name="value">IO 值。</param>
|
||||
/// <param name="io_type">IO 类型字符串。</param>
|
||||
/// <returns>旧 FastAPI 层风格的状态响应。</returns>
|
||||
[HttpPost("/set_io/")]
|
||||
public IActionResult SetIo([FromQuery] int port, [FromQuery] bool value, [FromQuery] string io_type)
|
||||
{
|
||||
try
|
||||
{
|
||||
_compatService.SetIo(port, value, io_type);
|
||||
return Ok(new { status = "IO set" });
|
||||
}
|
||||
catch
|
||||
{
|
||||
return LegacyBadRequest("SetDigitalOutput failed");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 兼容旧 `/get_joint_position/` 路由。
|
||||
/// </summary>
|
||||
/// <returns>旧 HTTP 层定义的关节位置 JSON 外形。</returns>
|
||||
[HttpGet("/get_joint_position/")]
|
||||
public IActionResult GetJointPosition()
|
||||
{
|
||||
try
|
||||
{
|
||||
return Ok(new { success = true, points = _compatService.GetJointPositions() });
|
||||
}
|
||||
catch
|
||||
{
|
||||
return LegacyBadRequest("GetJointPosition failed");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 兼容旧 `/move_joint/` 路由。
|
||||
/// </summary>
|
||||
/// <param name="joint_data">关节位置请求体。</param>
|
||||
/// <returns>旧 FastAPI 层风格的状态响应。</returns>
|
||||
[HttpPost("/move_joint/")]
|
||||
public IActionResult MoveJoint([FromBody] LegacyJointPositionRequest joint_data)
|
||||
{
|
||||
try
|
||||
{
|
||||
_compatService.MoveJoint(joint_data.joints);
|
||||
return Ok(new { status = "robot moved" });
|
||||
}
|
||||
catch
|
||||
{
|
||||
return LegacyBadRequest("MoveJoint failed");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 兼容旧 `/list_flyShotTraj/` 路由。
|
||||
/// </summary>
|
||||
/// <returns>已上传飞拍轨迹名称列表。</returns>
|
||||
[HttpGet("/list_flyShotTraj/")]
|
||||
public IActionResult ListFlyshotTrajectories()
|
||||
{
|
||||
var names = _compatService.ListTrajectoryNames();
|
||||
if (names.Count == 0)
|
||||
{
|
||||
return LegacyBadRequest("ListFlyShotTraj failed");
|
||||
}
|
||||
|
||||
return Ok(new { flyshot_trajs = names });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 兼容旧 `/execute_trajectory/` 路由,并接受两种历史请求体形状。
|
||||
/// </summary>
|
||||
/// <param name="waypoints">轨迹请求体。</param>
|
||||
/// <returns>旧 FastAPI 层风格的状态响应。</returns>
|
||||
[HttpPost("/execute_trajectory/")]
|
||||
public IActionResult ExecuteTrajectory([FromBody] JsonElement waypoints)
|
||||
{
|
||||
try
|
||||
{
|
||||
_compatService.ExecuteTrajectory(ParseLegacyTrajectoryWaypoints(waypoints));
|
||||
return Ok(new { status = "trajectory executed" });
|
||||
}
|
||||
catch
|
||||
{
|
||||
return LegacyBadRequest("ExecuteTrajectory failed");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 兼容旧 `/upload_flyshot/` 路由。
|
||||
/// </summary>
|
||||
/// <param name="trajectory_data">飞拍上传请求体。</param>
|
||||
/// <returns>旧 FastAPI 层风格的状态响应。</returns>
|
||||
[HttpPost("/upload_flyshot/")]
|
||||
public IActionResult UploadFlyshot([FromBody] LegacyFlightTrajectoryRequest trajectory_data)
|
||||
{
|
||||
if (trajectory_data.shot_flags.Count != trajectory_data.waypoints.Count)
|
||||
{
|
||||
return LegacyValidationError("shot_flags长度必须与路点数量相同");
|
||||
}
|
||||
|
||||
if (trajectory_data.offset_values.Count != trajectory_data.waypoints.Count)
|
||||
{
|
||||
return LegacyValidationError("offset_values长度必须与路点数量相同");
|
||||
}
|
||||
|
||||
if (trajectory_data.addrs.Count != trajectory_data.waypoints.Count)
|
||||
{
|
||||
return LegacyValidationError("addrs长度必须与路点数量相同");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var trajectory = new ControllerClientCompatUploadedTrajectory(
|
||||
name: trajectory_data.name,
|
||||
waypoints: trajectory_data.waypoints,
|
||||
shotFlags: trajectory_data.shot_flags,
|
||||
offsetValues: trajectory_data.offset_values.Select(static value => (int)value),
|
||||
addressGroups: trajectory_data.addrs);
|
||||
|
||||
_compatService.UploadTrajectory(trajectory);
|
||||
return Ok(new { status = "FlyShot uploaded" });
|
||||
}
|
||||
catch
|
||||
{
|
||||
return LegacyBadRequest("UploadFlyShotTraj failed");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 兼容旧 `/execute_flyshot/` 路由。
|
||||
/// </summary>
|
||||
/// <param name="data">包含轨迹名称的请求体。</param>
|
||||
/// <returns>旧 FastAPI 层风格的状态响应。</returns>
|
||||
[HttpPost("/execute_flyshot/")]
|
||||
public IActionResult ExecuteFlyshot([FromBody] LegacyNameRequest data)
|
||||
{
|
||||
try
|
||||
{
|
||||
_compatService.ExecuteTrajectoryByName(data.name);
|
||||
return Ok(new { status = "FlyShot executed", success = true });
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, new { detail = exception.Message });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 兼容旧 `/set_speedRatio/` 路由。
|
||||
/// </summary>
|
||||
/// <param name="data">速度倍率请求体。</param>
|
||||
/// <returns>旧 FastAPI 层风格的状态响应。</returns>
|
||||
[HttpPost("/set_speedRatio/")]
|
||||
public IActionResult SetSpeedRatio([FromBody] LegacySpeedRatioRequest data)
|
||||
{
|
||||
try
|
||||
{
|
||||
_compatService.SetSpeedRatio(data.speed);
|
||||
return Ok(new { message = "set_speedRatio executed", returnCode = 0 });
|
||||
}
|
||||
catch
|
||||
{
|
||||
return LegacyBadRequest("set_speedRatio failed");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 兼容旧 `/delete_flyshot/` 路由。
|
||||
/// </summary>
|
||||
/// <param name="request">包含轨迹名称的请求体。</param>
|
||||
/// <returns>旧 FastAPI 层风格的状态响应。</returns>
|
||||
[HttpPost("/delete_flyshot/")]
|
||||
public IActionResult DeleteFlyshot([FromBody] LegacyNameRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
_compatService.DeleteTrajectory(request.name);
|
||||
return Ok(new { status = "FlyShot deleted" });
|
||||
}
|
||||
catch
|
||||
{
|
||||
return LegacyBadRequest("DeleteFlyShotTraj failed");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 兼容旧 `/init_mpc_robt` 路由,保留历史拼写。
|
||||
/// </summary>
|
||||
/// <param name="data">初始化请求体。</param>
|
||||
/// <returns>旧 FastAPI 层风格的初始化结果。</returns>
|
||||
[HttpPost("/init_mpc_robt")]
|
||||
public IActionResult InitMpcRobot([FromBody] LegacyInitMpcRobotRequest data)
|
||||
{
|
||||
try
|
||||
{
|
||||
_compatService.ConnectServer(data.server_ip, data.port);
|
||||
_compatService.SetUpRobot(data.robot_name);
|
||||
if (!_compatService.IsSetUp)
|
||||
{
|
||||
return LegacyBadRequest("Robot not setup");
|
||||
}
|
||||
|
||||
_compatService.SetActiveController(sim: false);
|
||||
_compatService.Connect(data.robot_ip);
|
||||
_compatService.EnableRobot(2);
|
||||
return Ok(new { message = "init_Success", returnCode = 0 });
|
||||
}
|
||||
catch
|
||||
{
|
||||
return LegacyBadRequest("Connect Server failed");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 兼容旧 `/get_pose` 路由。
|
||||
/// </summary>
|
||||
/// <returns>当前末端位姿数组。</returns>
|
||||
[HttpGet("/get_pose")]
|
||||
public IActionResult GetPose()
|
||||
{
|
||||
try
|
||||
{
|
||||
return Ok(new { pose = _compatService.GetPose() });
|
||||
}
|
||||
catch
|
||||
{
|
||||
return LegacyBadRequest("GetPose failed");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析旧 `/execute_trajectory/` 可能出现的两种历史请求体形状。
|
||||
/// </summary>
|
||||
/// <param name="waypoints">原始 JSON 请求体。</param>
|
||||
/// <returns>统一后的关节路点集合。</returns>
|
||||
private static IReadOnlyList<IReadOnlyList<double>> ParseLegacyTrajectoryWaypoints(JsonElement waypoints)
|
||||
{
|
||||
if (waypoints.ValueKind != JsonValueKind.Array)
|
||||
{
|
||||
throw new InvalidOperationException("ExecuteTrajectory request body must be an array.");
|
||||
}
|
||||
|
||||
var parsedWaypoints = new List<IReadOnlyList<double>>();
|
||||
foreach (var waypointElement in waypoints.EnumerateArray())
|
||||
{
|
||||
if (waypointElement.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
parsedWaypoints.Add(waypointElement.EnumerateArray().Select(static value => value.GetDouble()).ToArray());
|
||||
continue;
|
||||
}
|
||||
|
||||
if (waypointElement.ValueKind == JsonValueKind.Object && waypointElement.TryGetProperty("joints", out var jointElement))
|
||||
{
|
||||
parsedWaypoints.Add(jointElement.EnumerateArray().Select(static value => value.GetDouble()).ToArray());
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("Unsupported waypoint payload shape.");
|
||||
}
|
||||
|
||||
return parsedWaypoints;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构造与旧 FastAPI `HTTPException(status_code=400, detail=...)` 等价的响应。
|
||||
/// </summary>
|
||||
/// <param name="detail">错误详情文本。</param>
|
||||
/// <returns>400 JSON 响应。</returns>
|
||||
private BadRequestObjectResult LegacyBadRequest(string detail)
|
||||
{
|
||||
return BadRequest(new { detail });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构造与旧 FastAPI `422` 输入校验失败等价的响应。
|
||||
/// </summary>
|
||||
/// <param name="detail">错误详情文本。</param>
|
||||
/// <returns>422 JSON 响应。</returns>
|
||||
private ObjectResult LegacyValidationError(string detail)
|
||||
{
|
||||
return StatusCode(StatusCodes.Status422UnprocessableEntity, new { detail });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 表示旧 `/set_tcp/` 路由使用的三维 TCP 请求体。
|
||||
/// </summary>
|
||||
public sealed class LegacyTcpRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取或设置 TCP X。
|
||||
/// </summary>
|
||||
public double x { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置 TCP Y。
|
||||
/// </summary>
|
||||
public double y { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置 TCP Z。
|
||||
/// </summary>
|
||||
public double z { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 表示旧 `/move_joint/` 路由使用的关节请求体。
|
||||
/// </summary>
|
||||
public sealed class LegacyJointPositionRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取或设置目标关节数组。
|
||||
/// </summary>
|
||||
public List<double> joints { get; init; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 表示旧 `/upload_flyshot/` 路由使用的飞拍上传请求体。
|
||||
/// </summary>
|
||||
public sealed class LegacyFlightTrajectoryRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取或设置地址组集合。
|
||||
/// </summary>
|
||||
public List<List<int>> addrs { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置飞拍轨迹名称。
|
||||
/// </summary>
|
||||
public string name { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置偏移周期集合。
|
||||
/// </summary>
|
||||
public List<double> offset_values { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置拍照标志集合。
|
||||
/// </summary>
|
||||
public List<bool> shot_flags { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置关节路点集合。
|
||||
/// </summary>
|
||||
public List<List<double>> waypoints { get; init; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 表示旧 `/execute_flyshot/` 与 `/delete_flyshot/` 路由使用的名称请求体。
|
||||
/// </summary>
|
||||
public sealed class LegacyNameRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取或设置轨迹名称。
|
||||
/// </summary>
|
||||
public string name { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 表示旧 `/set_speedRatio/` 路由使用的速度倍率请求体。
|
||||
/// </summary>
|
||||
public sealed class LegacySpeedRatioRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取或设置目标速度倍率。
|
||||
/// </summary>
|
||||
public double speed { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 表示旧 `/init_mpc_robt` 路由使用的初始化请求体。
|
||||
/// </summary>
|
||||
public sealed class LegacyInitMpcRobotRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取或设置目标服务端 IP。
|
||||
/// </summary>
|
||||
public string server_ip { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置目标服务端端口。
|
||||
/// </summary>
|
||||
public int port { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置机器人名称。
|
||||
/// </summary>
|
||||
public string robot_name { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置机器人控制器 IP。
|
||||
/// </summary>
|
||||
public string robot_ip { get; init; } = string.Empty;
|
||||
}
|
||||
@@ -1,6 +1,11 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Flyshot.Core.Domain\Flyshot.Core.Domain.csproj" />
|
||||
<ProjectReference Include="..\Flyshot.ControllerClientCompat\Flyshot.ControllerClientCompat.csproj" />
|
||||
<ProjectReference Include="..\Flyshot.Runtime.Common\Flyshot.Runtime.Common.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
27
src/Flyshot.Server.Host/HostCorsOptions.cs
Normal file
27
src/Flyshot.Server.Host/HostCorsOptions.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
namespace Flyshot.Server.Host;
|
||||
|
||||
/// <summary>
|
||||
/// 表示宿主 CORS 策略的标准配置项。
|
||||
/// </summary>
|
||||
public sealed class HostCorsOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取或设置 CORS 策略名称。
|
||||
/// </summary>
|
||||
public string PolicyName { get; set; } = "LegacyHttpApi";
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置允许的源列表。
|
||||
/// </summary>
|
||||
public string[] AllowedOrigins { get; set; } = ["*"];
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置允许的 HTTP 方法列表。
|
||||
/// </summary>
|
||||
public string[] AllowedMethods { get; set; } = ["GET", "POST", "OPTIONS"];
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置允许的请求头列表。
|
||||
/// </summary>
|
||||
public string[] AllowedHeaders { get; set; } = ["*"];
|
||||
}
|
||||
37
src/Flyshot.Server.Host/HostSwaggerOptions.cs
Normal file
37
src/Flyshot.Server.Host/HostSwaggerOptions.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
namespace Flyshot.Server.Host;
|
||||
|
||||
/// <summary>
|
||||
/// 表示宿主 Swagger/OpenAPI 文档的标准配置项。
|
||||
/// </summary>
|
||||
public sealed class HostSwaggerOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取或设置是否启用 Swagger。
|
||||
/// </summary>
|
||||
public bool Enabled { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置文档名称。
|
||||
/// </summary>
|
||||
public string DocumentName { get; set; } = "v1";
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置 Swagger 文档标题。
|
||||
/// </summary>
|
||||
public string Title { get; set; } = "Flyshot Replacement HTTP API";
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置 Swagger 文档版本文本。
|
||||
/// </summary>
|
||||
public string Version { get; set; } = "v1";
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置 Swagger JSON 路由模板。
|
||||
/// </summary>
|
||||
public string JsonRouteTemplate { get; set; } = "swagger/{documentName}/swagger.json";
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置 Swagger UI 路由前缀。
|
||||
/// </summary>
|
||||
public string RoutePrefix { get; set; } = "swagger";
|
||||
}
|
||||
@@ -1,12 +1,85 @@
|
||||
using Flyshot.ControllerClientCompat;
|
||||
using Flyshot.Server.Host;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.OpenApi.Models;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Services.Configure<HostSwaggerOptions>(builder.Configuration.GetSection("Swagger"));
|
||||
builder.Services.Configure<HostCorsOptions>(builder.Configuration.GetSection("Cors"));
|
||||
builder.Services.AddControllerClientCompat(builder.Configuration);
|
||||
builder.Services.AddControllers();
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
|
||||
var swaggerOptions = builder.Configuration.GetSection("Swagger").Get<HostSwaggerOptions>() ?? new HostSwaggerOptions();
|
||||
builder.Services.AddSwaggerGen(options =>
|
||||
{
|
||||
options.SwaggerDoc(swaggerOptions.DocumentName, new OpenApiInfo
|
||||
{
|
||||
Title = swaggerOptions.Title,
|
||||
Version = swaggerOptions.Version
|
||||
});
|
||||
});
|
||||
|
||||
var corsOptions = builder.Configuration.GetSection("Cors").Get<HostCorsOptions>() ?? new HostCorsOptions();
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
options.AddPolicy(corsOptions.PolicyName, policyBuilder =>
|
||||
{
|
||||
// 兼容本地调试时最常见的任意源配置,同时保留显式白名单模式。
|
||||
if (corsOptions.AllowedOrigins.Length == 1 && string.Equals(corsOptions.AllowedOrigins[0], "*", StringComparison.Ordinal))
|
||||
{
|
||||
policyBuilder.AllowAnyOrigin();
|
||||
}
|
||||
else
|
||||
{
|
||||
policyBuilder.WithOrigins(corsOptions.AllowedOrigins);
|
||||
}
|
||||
|
||||
if (corsOptions.AllowedMethods.Length == 1 && string.Equals(corsOptions.AllowedMethods[0], "*", StringComparison.Ordinal))
|
||||
{
|
||||
policyBuilder.AllowAnyMethod();
|
||||
}
|
||||
else
|
||||
{
|
||||
policyBuilder.WithMethods(corsOptions.AllowedMethods);
|
||||
}
|
||||
|
||||
if (corsOptions.AllowedHeaders.Length == 1 && string.Equals(corsOptions.AllowedHeaders[0], "*", StringComparison.Ordinal))
|
||||
{
|
||||
policyBuilder.AllowAnyHeader();
|
||||
}
|
||||
else
|
||||
{
|
||||
policyBuilder.WithHeaders(corsOptions.AllowedHeaders);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
app.MapGet("/", () => Results.Redirect("/healthz"));
|
||||
app.MapGet("/healthz", () => Results.Ok(new
|
||||
var resolvedSwaggerOptions = app.Services.GetRequiredService<IOptions<HostSwaggerOptions>>().Value;
|
||||
var resolvedCorsOptions = app.Services.GetRequiredService<IOptions<HostCorsOptions>>().Value;
|
||||
|
||||
if (resolvedSwaggerOptions.Enabled)
|
||||
{
|
||||
status = "ok",
|
||||
service = "flyshot-server-host"
|
||||
}));
|
||||
app.UseSwagger(options =>
|
||||
{
|
||||
options.RouteTemplate = resolvedSwaggerOptions.JsonRouteTemplate;
|
||||
});
|
||||
|
||||
app.UseSwaggerUI(options =>
|
||||
{
|
||||
options.RoutePrefix = resolvedSwaggerOptions.RoutePrefix;
|
||||
options.SwaggerEndpoint(
|
||||
$"/swagger/{resolvedSwaggerOptions.DocumentName}/swagger.json",
|
||||
$"{resolvedSwaggerOptions.Title} {resolvedSwaggerOptions.Version}");
|
||||
options.DocumentTitle = resolvedSwaggerOptions.Title;
|
||||
});
|
||||
}
|
||||
|
||||
app.UseCors(resolvedCorsOptions.PolicyName);
|
||||
app.MapControllers();
|
||||
|
||||
app.Run();
|
||||
|
||||
|
||||
@@ -4,5 +4,26 @@
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"ControllerClientCompat": {
|
||||
"ServerVersion": "flyshot-replacement-controller-client-compat/0.1.0"
|
||||
},
|
||||
"Swagger": {
|
||||
"Enabled": true
|
||||
},
|
||||
"Cors": {
|
||||
"PolicyName": "LegacyHttpApi",
|
||||
"AllowedOrigins": [
|
||||
"http://localhost:3000",
|
||||
"http://127.0.0.1:3000"
|
||||
],
|
||||
"AllowedMethods": [
|
||||
"GET",
|
||||
"POST",
|
||||
"OPTIONS"
|
||||
],
|
||||
"AllowedHeaders": [
|
||||
"*"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,5 +5,30 @@
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"ControllerClientCompat": {
|
||||
"ServerVersion": "flyshot-replacement-controller-client-compat/0.1.0"
|
||||
},
|
||||
"Swagger": {
|
||||
"Enabled": true,
|
||||
"DocumentName": "v1",
|
||||
"Title": "Flyshot Replacement HTTP API",
|
||||
"Version": "v1",
|
||||
"JsonRouteTemplate": "swagger/{documentName}/swagger.json",
|
||||
"RoutePrefix": "swagger"
|
||||
},
|
||||
"Cors": {
|
||||
"PolicyName": "LegacyHttpApi",
|
||||
"AllowedOrigins": [
|
||||
"*"
|
||||
],
|
||||
"AllowedMethods": [
|
||||
"GET",
|
||||
"POST",
|
||||
"OPTIONS"
|
||||
],
|
||||
"AllowedHeaders": [
|
||||
"*"
|
||||
]
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
|
||||
@@ -23,10 +23,12 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Flyshot.ControllerClientCompat\Flyshot.ControllerClientCompat.csproj" />
|
||||
<ProjectReference Include="..\..\src\Flyshot.Core.Config\Flyshot.Core.Config.csproj" />
|
||||
<ProjectReference Include="..\..\src\Flyshot.Core.Domain\Flyshot.Core.Domain.csproj" />
|
||||
<ProjectReference Include="..\..\src\Flyshot.Core.Planning\Flyshot.Core.Planning.csproj" />
|
||||
<ProjectReference Include="..\..\src\Flyshot.Core.Triggering\Flyshot.Core.Triggering.csproj" />
|
||||
<ProjectReference Include="..\..\src\Flyshot.Runtime.Fanuc\Flyshot.Runtime.Fanuc.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
195
tests/Flyshot.Core.Tests/RuntimeOrchestrationTests.cs
Normal file
195
tests/Flyshot.Core.Tests/RuntimeOrchestrationTests.cs
Normal file
@@ -0,0 +1,195 @@
|
||||
using Flyshot.ControllerClientCompat;
|
||||
using Flyshot.Core.Config;
|
||||
using Flyshot.Core.Domain;
|
||||
using Flyshot.Runtime.Fanuc;
|
||||
|
||||
namespace Flyshot.Core.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// 验证最小运行时编排链路会把规划结果交给控制器运行时,而不是停留在兼容层内存状态。
|
||||
/// </summary>
|
||||
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: false);
|
||||
runtime.Connect("192.168.10.101");
|
||||
runtime.EnableRobot(bufferSize: 2);
|
||||
|
||||
var result = new TrajectoryResult(
|
||||
programName: "demo",
|
||||
method: PlanningMethod.Icsp,
|
||||
isValid: true,
|
||||
duration: TimeSpan.FromSeconds(1.2),
|
||||
shotEvents: Array.Empty<ShotEvent>(),
|
||||
triggerTimeline: Array.Empty<TrajectoryDoEvent>(),
|
||||
artifacts: Array.Empty<TrajectoryArtifact>(),
|
||||
failureReason: null,
|
||||
usedCache: false,
|
||||
originalWaypointCount: 4,
|
||||
plannedWaypointCount: 4);
|
||||
|
||||
runtime.ExecuteTrajectory(result, [1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
|
||||
|
||||
var snapshot = runtime.GetSnapshot();
|
||||
Assert.Equal("Connected", snapshot.ConnectionState);
|
||||
Assert.False(snapshot.IsInMotion);
|
||||
Assert.Equal([1.0, 2.0, 3.0, 4.0, 5.0, 6.0], snapshot.JointPositions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证普通轨迹会先进入 ICSP 规划,并沿用 ICSP 对示教点数量的约束。
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ControllerClientTrajectoryOrchestrator_PlanOrdinaryTrajectory_RejectsThreeTeachPoints()
|
||||
{
|
||||
var orchestrator = new ControllerClientTrajectoryOrchestrator();
|
||||
var robot = TestRobotFactory.CreateRobotProfile();
|
||||
|
||||
void Act() =>
|
||||
orchestrator.PlanOrdinaryTrajectory(
|
||||
robot,
|
||||
[
|
||||
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
|
||||
[0.5, 0.0, 0.0, 0.0, 0.0, 0.0],
|
||||
[1.0, 0.0, 0.0, 0.0, 0.0, 0.0]
|
||||
]);
|
||||
|
||||
Assert.Throws<ArgumentException>(Act);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证已上传飞拍轨迹会经过 self-adapt-icsp 并生成拍照触发时间轴。
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ControllerClientTrajectoryOrchestrator_PlanUploadedFlyshot_BuildsShotTimeline()
|
||||
{
|
||||
var orchestrator = new ControllerClientTrajectoryOrchestrator();
|
||||
var robot = TestRobotFactory.CreateRobotProfile();
|
||||
var uploaded = TestRobotFactory.CreateUploadedTrajectoryWithSingleShot();
|
||||
|
||||
var bundle = orchestrator.PlanUploadedFlyshot(robot, uploaded);
|
||||
|
||||
Assert.True(bundle.Result.IsValid);
|
||||
Assert.Single(bundle.Result.ShotEvents);
|
||||
Assert.Single(bundle.Result.TriggerTimeline);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证兼容服务执行普通轨迹时会进入规划链路,而不是直接把最后一个路点写入状态。
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ControllerClientCompatService_ExecuteTrajectory_RejectsThreeTeachPointsAfterPlanningIsIntroduced()
|
||||
{
|
||||
var service = TestRobotFactory.CreateCompatService();
|
||||
service.SetUpRobot("FANUC_LR_Mate_200iD");
|
||||
service.SetActiveController(sim: false);
|
||||
service.Connect("192.168.10.101");
|
||||
service.EnableRobot(2);
|
||||
|
||||
void Act() =>
|
||||
service.ExecuteTrajectory(
|
||||
[
|
||||
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
|
||||
[0.5, 0.0, 0.0, 0.0, 0.0, 0.0],
|
||||
[1.0, 0.0, 0.0, 0.0, 0.0, 0.0]
|
||||
]);
|
||||
|
||||
Assert.Throws<ArgumentException>(Act);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为运行时编排测试构造稳定的最小领域对象。
|
||||
/// </summary>
|
||||
internal static class TestRobotFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// 构造六轴测试机器人配置,避免运行时测试依赖真实 .robot 文件。
|
||||
/// </summary>
|
||||
/// <returns>可用于规划和运行时状态校验的机器人配置。</returns>
|
||||
public static RobotProfile CreateRobotProfile()
|
||||
{
|
||||
return new RobotProfile(
|
||||
name: "TestRobot",
|
||||
modelPath: "Models/Test.robot",
|
||||
degreesOfFreedom: 6,
|
||||
jointLimits: Enumerable.Range(1, 6)
|
||||
.Select(static index => new JointLimit($"J{index}", 10.0, 20.0, 100.0))
|
||||
.ToArray(),
|
||||
jointCouplings: Array.Empty<JointCoupling>(),
|
||||
servoPeriod: TimeSpan.FromMilliseconds(8),
|
||||
triggerPeriod: TimeSpan.FromMilliseconds(8));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构造一条含单个拍照点的上传飞拍轨迹。
|
||||
/// </summary>
|
||||
/// <returns>可用于触发时间轴测试的上传轨迹。</returns>
|
||||
public static ControllerClientCompatUploadedTrajectory CreateUploadedTrajectoryWithSingleShot()
|
||||
{
|
||||
return new ControllerClientCompatUploadedTrajectory(
|
||||
name: "demo-flyshot",
|
||||
waypoints:
|
||||
[
|
||||
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
|
||||
[0.1, 0.0, 0.0, 0.0, 0.0, 0.0],
|
||||
[0.2, 0.0, 0.0, 0.0, 0.0, 0.0],
|
||||
[0.3, 0.0, 0.0, 0.0, 0.0, 0.0]
|
||||
],
|
||||
shotFlags: [false, true, false, false],
|
||||
offsetValues: [0, 1, 0, 0],
|
||||
addressGroups:
|
||||
[
|
||||
Array.Empty<int>(),
|
||||
[7, 8],
|
||||
Array.Empty<int>(),
|
||||
Array.Empty<int>()
|
||||
]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构造一份真实依赖注入等价的兼容服务,覆盖运行时和编排器协作。
|
||||
/// </summary>
|
||||
/// <returns>可执行 ControllerClient 兼容语义的服务实例。</returns>
|
||||
public static ControllerClientCompatService CreateCompatService()
|
||||
{
|
||||
var options = new ControllerClientCompatOptions
|
||||
{
|
||||
WorkspaceRoot = GetWorkspaceRoot()
|
||||
};
|
||||
|
||||
return new ControllerClientCompatService(
|
||||
options,
|
||||
new ControllerClientCompatRobotCatalog(options, new RobotModelLoader()),
|
||||
new FanucControllerRuntime(),
|
||||
new ControllerClientTrajectoryOrchestrator());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 定位父工作区根目录,供兼容服务加载真实机器人模型。
|
||||
/// </summary>
|
||||
/// <returns>父工作区根目录。</returns>
|
||||
private static string GetWorkspaceRoot()
|
||||
{
|
||||
var current = new DirectoryInfo(AppContext.BaseDirectory);
|
||||
while (current is not null)
|
||||
{
|
||||
if (File.Exists(Path.Combine(current.FullName, "FlyshotReplacement.sln")))
|
||||
{
|
||||
return Path.GetFullPath(Path.Combine(current.FullName, ".."));
|
||||
}
|
||||
|
||||
current = current.Parent;
|
||||
}
|
||||
|
||||
throw new DirectoryNotFoundException("Unable to locate the flyshot workspace root.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using Flyshot.ControllerClientCompat;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Flyshot.Server.IntegrationTests;
|
||||
|
||||
/// <summary>
|
||||
/// 锁定宿主当前应注册 HTTP-only 的 ControllerClient 兼容服务,而不是旧 TCP 网关入口。
|
||||
/// </summary>
|
||||
public sealed class ControllerClientCompatRegistrationTests(FlyshotServerFactory factory) : IClassFixture<FlyshotServerFactory>
|
||||
{
|
||||
/// <summary>
|
||||
/// 验证宿主能从 DI 中解析新的兼容服务。
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Host_Registers_ControllerClientCompat_Service()
|
||||
{
|
||||
var service = factory.Services.GetService<IControllerClientCompatService>();
|
||||
|
||||
Assert.NotNull(service);
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Flyshot.ControllerClientCompat\Flyshot.ControllerClientCompat.csproj" />
|
||||
<ProjectReference Include="..\..\src\Flyshot.Server.Host\Flyshot.Server.Host.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace Flyshot.Server.IntegrationTests;
|
||||
|
||||
/// <summary>
|
||||
/// 锁定标准 MVC 宿主需要提供的 Swagger 与 CORS 行为,避免后续回退成只够跑通的最小配置。
|
||||
/// </summary>
|
||||
public sealed class HostMvcConfigurationTests(FlyshotServerFactory factory) : IClassFixture<FlyshotServerFactory>
|
||||
{
|
||||
/// <summary>
|
||||
/// 验证宿主会公开标准 Swagger JSON,并且文档标题和旧 HTTP 兼容路径都能从配置和控制器路由中导出。
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task SwaggerDocument_ExposesConfiguredMetadataAndLegacyRoutes()
|
||||
{
|
||||
using var configuredFactory = CreateConfiguredFactory(factory);
|
||||
using var client = configuredFactory.CreateClient();
|
||||
|
||||
using var response = await client.GetAsync("/swagger/v1/swagger.json");
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
|
||||
await using var responseStream = await response.Content.ReadAsStreamAsync();
|
||||
using var document = await JsonDocument.ParseAsync(responseStream);
|
||||
var root = document.RootElement;
|
||||
var paths = root.GetProperty("paths");
|
||||
|
||||
Assert.Equal("3.0.1", root.GetProperty("openapi").GetString());
|
||||
Assert.Equal("Flyshot Replacement HTTP API", root.GetProperty("info").GetProperty("title").GetString());
|
||||
// OpenAPI 文档会把部分带尾斜杠的路由规范化为无尾斜杠形式,这里同时接受两种键。
|
||||
Assert.True(paths.TryGetProperty("/robot_info/", out _) || paths.TryGetProperty("/robot_info", out _));
|
||||
Assert.True(paths.TryGetProperty("/healthz", out _) || paths.TryGetProperty("/healthz/", out _));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证宿主会按配置对旧 HTTP API 路由返回标准 CORS 预检响应。
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task CorsPreflight_ReturnsConfiguredAllowOriginHeaders()
|
||||
{
|
||||
using var configuredFactory = CreateConfiguredFactory(factory);
|
||||
using var client = configuredFactory.CreateClient();
|
||||
|
||||
using var request = new HttpRequestMessage(HttpMethod.Options, "/robot_info/");
|
||||
request.Headers.Add("Origin", "http://localhost:3000");
|
||||
request.Headers.Add("Access-Control-Request-Method", "GET");
|
||||
request.Headers.Add("Access-Control-Request-Headers", "content-type");
|
||||
|
||||
using var response = await client.SendAsync(request);
|
||||
Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
|
||||
Assert.True(response.Headers.TryGetValues("Access-Control-Allow-Origin", out var allowedOrigins));
|
||||
Assert.Contains("http://localhost:3000", allowedOrigins);
|
||||
Assert.True(response.Headers.TryGetValues("Access-Control-Allow-Methods", out var allowedMethods));
|
||||
Assert.Contains("GET", string.Join(",", allowedMethods));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为测试宿主注入标准 Swagger 与 CORS 配置,避免依赖开发机本地环境。
|
||||
/// </summary>
|
||||
private static WebApplicationFactory<Program> CreateConfiguredFactory(FlyshotServerFactory factory)
|
||||
{
|
||||
return factory.WithWebHostBuilder(builder =>
|
||||
{
|
||||
builder.ConfigureAppConfiguration((_, configurationBuilder) =>
|
||||
{
|
||||
configurationBuilder.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
["Swagger:Enabled"] = "true",
|
||||
["Swagger:DocumentName"] = "v1",
|
||||
["Swagger:Title"] = "Flyshot Replacement HTTP API",
|
||||
["Swagger:Version"] = "v1",
|
||||
["Cors:PolicyName"] = "LegacyHttpApi",
|
||||
["Cors:AllowedOrigins:0"] = "http://localhost:3000",
|
||||
["Cors:AllowedMethods:0"] = "GET",
|
||||
["Cors:AllowedMethods:1"] = "POST",
|
||||
["Cors:AllowedMethods:2"] = "OPTIONS",
|
||||
["Cors:AllowedHeaders:0"] = "content-type"
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Flyshot.Server.IntegrationTests;
|
||||
|
||||
/// <summary>
|
||||
/// 锁定 flyshot-uaes-interface 现有 FastAPI 层的 HTTP 路径、参数绑定和返回 JSON 外形。
|
||||
/// </summary>
|
||||
public sealed class LegacyHttpApiCompatibilityTests(FlyshotServerFactory factory) : IClassFixture<FlyshotServerFactory>
|
||||
{
|
||||
/// <summary>
|
||||
/// 验证根路径会返回旧 HTTP 服务使用的 Hello World JSON,而不是跳转到健康检查页。
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Root_ReturnsLegacyHelloWorldPayload()
|
||||
{
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
using var response = await client.GetAsync("/");
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
|
||||
using var json = await ReadJsonAsync(response);
|
||||
Assert.Equal("Hello World", json.RootElement.GetProperty("message").GetString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证初始化链路和机器人信息接口会保持旧 FastAPI 服务的路径与返回字段风格。
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task InitEndpoints_ExposeLegacyRobotInfoAndSpeedRatioShape()
|
||||
{
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
using (var connectServerResponse = await client.PostAsync("/connect_server/?server_ip=127.0.0.1&port=50001", content: null))
|
||||
{
|
||||
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=false", 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/"))
|
||||
{
|
||||
Assert.Equal(HttpStatusCode.OK, robotInfoResponse.StatusCode);
|
||||
using var robotInfoJson = await ReadJsonAsync(robotInfoResponse);
|
||||
var robotInfoRoot = robotInfoJson.RootElement;
|
||||
|
||||
Assert.Equal("FANUC_LR_Mate_200iD", robotInfoRoot.GetProperty("name").GetString());
|
||||
Assert.Equal("flyshot-replacement-controller-client-compat/0.1.0", robotInfoRoot.GetProperty("server_version").GetString());
|
||||
Assert.Equal(6, robotInfoRoot.GetProperty("dof").GetInt32());
|
||||
Assert.Equal(1.0, robotInfoRoot.GetProperty("speed_ratio").GetDouble(), precision: 6);
|
||||
}
|
||||
|
||||
using (var setSpeedRatioResponse = await client.PostAsJsonAsync("/set_speedRatio/", new { speed = 0.8 }))
|
||||
{
|
||||
Assert.Equal(HttpStatusCode.OK, setSpeedRatioResponse.StatusCode);
|
||||
using var setSpeedRatioJson = await ReadJsonAsync(setSpeedRatioResponse);
|
||||
Assert.Equal("set_speedRatio executed", setSpeedRatioJson.RootElement.GetProperty("message").GetString());
|
||||
Assert.Equal(0, setSpeedRatioJson.RootElement.GetProperty("returnCode").GetInt32());
|
||||
}
|
||||
|
||||
using var updatedRobotInfoResponse = await client.GetAsync("/robot_info/");
|
||||
Assert.Equal(HttpStatusCode.OK, updatedRobotInfoResponse.StatusCode);
|
||||
using var updatedRobotInfoJson = await ReadJsonAsync(updatedRobotInfoResponse);
|
||||
Assert.Equal(0.8, updatedRobotInfoJson.RootElement.GetProperty("speed_ratio").GetDouble(), precision: 6);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证 TCP、关节位置和位姿相关 HTTP 接口会保持旧服务的请求体与响应体结构。
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task MotionStateEndpoints_RoundTripLegacyPayloadShapes()
|
||||
{
|
||||
using var client = factory.CreateClient();
|
||||
await InitializeRobotAsync(client);
|
||||
|
||||
using (var setTcpResponse = await client.PostAsJsonAsync("/set_tcp/", new { x = 1.0, y = 2.0, z = 3.0 }))
|
||||
{
|
||||
Assert.Equal(HttpStatusCode.OK, setTcpResponse.StatusCode);
|
||||
using var setTcpJson = await ReadJsonAsync(setTcpResponse);
|
||||
Assert.Equal("TCP set", setTcpJson.RootElement.GetProperty("status").GetString());
|
||||
}
|
||||
|
||||
using (var getTcpResponse = await client.GetAsync("/get_tcp/"))
|
||||
{
|
||||
Assert.Equal(HttpStatusCode.OK, getTcpResponse.StatusCode);
|
||||
using var getTcpJson = await ReadJsonAsync(getTcpResponse);
|
||||
var tcpValues = getTcpJson.RootElement.GetProperty("tcp").EnumerateArray().Select(static value => value.GetDouble()).ToArray();
|
||||
Assert.Equal([1.0, 2.0, 3.0], tcpValues);
|
||||
}
|
||||
|
||||
using (var moveJointResponse = await client.PostAsJsonAsync("/move_joint/", new { joints = new[] { 1.0, 2.0, 3.0, 4.0, 5.0, 6.0 } }))
|
||||
{
|
||||
Assert.Equal(HttpStatusCode.OK, moveJointResponse.StatusCode);
|
||||
using var moveJointJson = await ReadJsonAsync(moveJointResponse);
|
||||
Assert.Equal("robot moved", moveJointJson.RootElement.GetProperty("status").GetString());
|
||||
}
|
||||
|
||||
using (var getJointPositionResponse = await client.GetAsync("/get_joint_position/"))
|
||||
{
|
||||
Assert.Equal(HttpStatusCode.OK, getJointPositionResponse.StatusCode);
|
||||
using var getJointPositionJson = await ReadJsonAsync(getJointPositionResponse);
|
||||
var root = getJointPositionJson.RootElement;
|
||||
Assert.True(root.GetProperty("success").GetBoolean());
|
||||
var jointValues = root.GetProperty("points").EnumerateArray().Select(static value => value.GetDouble()).ToArray();
|
||||
Assert.Equal([1.0, 2.0, 3.0, 4.0, 5.0, 6.0], jointValues);
|
||||
}
|
||||
|
||||
using var getPoseResponse = await client.GetAsync("/get_pose");
|
||||
Assert.Equal(HttpStatusCode.OK, getPoseResponse.StatusCode);
|
||||
using var getPoseJson = await ReadJsonAsync(getPoseResponse);
|
||||
Assert.Equal(7, getPoseJson.RootElement.GetProperty("pose").GetArrayLength());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证飞拍 HTTP 接口可以按旧 API 层的路径和字段完成上传、列出、执行与删除。
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task FlyshotEndpoints_RoundTripLegacyUploadExecuteAndDeleteFlow()
|
||||
{
|
||||
using var client = factory.CreateClient();
|
||||
await InitializeRobotAsync(client);
|
||||
|
||||
var uploadPayload = new
|
||||
{
|
||||
addrs = new[]
|
||||
{
|
||||
new[] { 7, 8 },
|
||||
new[] { 7, 8 }
|
||||
},
|
||||
name = "demo-http-flyshot",
|
||||
offset_values = new[] { 0.0, 1.0 },
|
||||
shot_flags = new[] { false, true },
|
||||
waypoints = new[]
|
||||
{
|
||||
new[] { 0.1, 0.2, 0.3, 0.4, 0.5, 0.6 },
|
||||
new[] { 1.1, 1.2, 1.3, 1.4, 1.5, 1.6 }
|
||||
}
|
||||
};
|
||||
|
||||
using (var uploadResponse = await client.PostAsJsonAsync("/upload_flyshot/", uploadPayload))
|
||||
{
|
||||
Assert.Equal(HttpStatusCode.OK, uploadResponse.StatusCode);
|
||||
using var uploadJson = await ReadJsonAsync(uploadResponse);
|
||||
Assert.Equal("FlyShot uploaded", uploadJson.RootElement.GetProperty("status").GetString());
|
||||
}
|
||||
|
||||
using (var listResponse = await client.GetAsync("/list_flyShotTraj/"))
|
||||
{
|
||||
Assert.Equal(HttpStatusCode.OK, listResponse.StatusCode);
|
||||
using var listJson = await ReadJsonAsync(listResponse);
|
||||
var names = listJson.RootElement.GetProperty("flyshot_trajs").EnumerateArray().Select(static value => value.GetString()).ToArray();
|
||||
Assert.Contains("demo-http-flyshot", names);
|
||||
}
|
||||
|
||||
using (var executeResponse = await client.PostAsJsonAsync("/execute_flyshot/", new { name = "demo-http-flyshot" }))
|
||||
{
|
||||
Assert.Equal(HttpStatusCode.OK, executeResponse.StatusCode);
|
||||
using var executeJson = await ReadJsonAsync(executeResponse);
|
||||
var executeRoot = executeJson.RootElement;
|
||||
Assert.Equal("FlyShot executed", executeRoot.GetProperty("status").GetString());
|
||||
Assert.True(executeRoot.GetProperty("success").GetBoolean());
|
||||
}
|
||||
|
||||
using (var deleteResponse = await client.PostAsJsonAsync("/delete_flyshot/", new { name = "demo-http-flyshot" }))
|
||||
{
|
||||
Assert.Equal(HttpStatusCode.OK, deleteResponse.StatusCode);
|
||||
using var deleteJson = await ReadJsonAsync(deleteResponse);
|
||||
Assert.Equal("FlyShot deleted", deleteJson.RootElement.GetProperty("status").GetString());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 复用旧 API 层常见的初始化顺序,把当前宿主推进到可执行动作的最小状态。
|
||||
/// </summary>
|
||||
private static async Task InitializeRobotAsync(HttpClient client)
|
||||
{
|
||||
using var initResponse = await client.PostAsJsonAsync("/init_mpc_robt", new
|
||||
{
|
||||
server_ip = "127.0.0.1",
|
||||
port = 50001,
|
||||
robot_name = "FANUC_LR_Mate_200iD",
|
||||
robot_ip = "192.168.10.101"
|
||||
});
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, initResponse.StatusCode);
|
||||
using var initJson = await ReadJsonAsync(initResponse);
|
||||
Assert.Equal("init_Success", initJson.RootElement.GetProperty("message").GetString());
|
||||
Assert.Equal(0, initJson.RootElement.GetProperty("returnCode").GetInt32());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 统一把 HTTP 响应体解析成 JsonDocument,便于对旧接口的字段形状做精确断言。
|
||||
/// </summary>
|
||||
private static async Task<JsonDocument> ReadJsonAsync(HttpResponseMessage response)
|
||||
{
|
||||
await using var responseStream = await response.Content.ReadAsStreamAsync();
|
||||
return await JsonDocument.ParseAsync(responseStream);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user