* 在 FanucCommandProtocol/Client 中补齐速度倍率、TCP 位姿和 IO 的封包/解析,并引入 FanucIoTypes 字符串到枚举映射 * FanucControllerRuntime 在非仿真模式下接入真机命令通道,本地 缓存仅作为兜底,TCP 操作扩展为 7 维 Pose * FanucStateClient 增加帧超时检测、退避自动重连和诊断状态接口, 超时或重连期间不再把陈旧帧当作当前机器人状态 * FanucStateProtocol 锁定 90B 帧字段为 pose[6]、joint[6]、 external_axes[3] 和 raw_tail_words[4],并保留状态字诊断槽位 * ICspPlanner 增加 global_scale > 1.0 失败判定,self-adapt-icsp 内部禁用该判定以保留补点重试链路 * 同步更新 README/AGENTS/计划文档的 todo 状态和实现说明
9.0 KiB
FANUC 真机协议 Socket 通信层实现计划
上下文
当前 flyshot-replacement 项目已完成:
- 三条 FANUC 通信链路的二进制协议编解码(
FanucCommandProtocol、FanucStateProtocol、FanucJ519Protocol) - 抓包样本验证的协议测试(5 个 FanucProtocolTests 全部通过)
- TCP 10012 的
Get/SetSpeedRatio、Get/SetTCP、Get/SetIO参数命令封包、响应解析和本地模拟器测试 - HTTP 兼容层控制器和状态监控页
- 轨迹规划与飞拍触发编排层
缺失的关键环节:FanucControllerRuntime 仍是纯内存状态桩,没有实际 Socket 通信。Connect() 只记录 IP,ExecuteTrajectory() 只修改内存变量,GetJointPositions() 返回的是上一次写入值而非真实控制器反馈。
目标
将 FanucControllerRuntime 从内存桩改造为具备真实 FANUC R30iB 通信能力的运行时,使 HTTP 层的每个指令真正下发到控制柜。
架构设计
分层结构
LegacyHttpApiController / StatusController (HTTP 适配层,保持不动)
↓ 调用同步接口
IControllerRuntime / ControllerClientCompatService (兼容层,保持不动)
↓ 调用同步接口
FanucControllerRuntime (改造:从内存桩 → 委托给三个 Socket 客户端)
↓ 内部持有并调度
FanucCommandClient (TCP 10012,Req/Res 同步命令通道)
FanucStateClient (TCP 10010,持续接收状态帧后台循环)
FanucJ519Client (UDP 60015,8ms 周期发送 + 接收响应)
↓ 使用现有编解码
FanucCommandProtocol / FanucStateProtocol / FanucJ519Protocol (已有,不改动)
关键设计决策
-
接口保持同步:
IControllerRuntime现有 18 个方法全为同步签名。内部 Socket I/O 采用Task+.GetAwaiter().GetResult()短时间阻塞,或后台线程 + 锁同步状态快照。避免一次性推翻整个兼容层。 -
三个独立客户端:每条物理通道一个类,各自管理连接生命周期,便于单独测试和故障定位。
-
状态通道后台循环:
FanucStateClient内部启动Task持续ReadAsync(90),解析状态帧后写入线程安全的ControllerStateSnapshot缓存。 -
J519 周期发送器:
FanucJ519Client内部用PeriodicTimer或Task.Delay实现约 8ms 周期的发送循环。命令通过线程安全的队列/缓冲区注入。 -
RVBUSTSM 程序生命周期隐式管理:
EnableRobot()时自动走StopProg→Reset→GetProgStatus→StartProg("RVBUSTSM")序列(与抓包一致)。DisableRobot()时发送StopProg。 -
连接顺序:
Connect()按顺序建立三条通道 — 先 TCP 10010(状态),再 TCP 10012(命令),最后 UDP 60015(运动)。
实现步骤
Phase 1: TCP 10012 命令客户端
新建文件:src/Flyshot.Runtime.Fanuc/Protocol/FanucCommandClient.cs
职责:
Connect(string ip, int port = 10012)— 建立 TcpClient 连接SendCommandAsync(uint messageId, ReadOnlyMemory<byte> body)— 发送并等待响应SendProgramCommandAsync(uint messageId, string programName)— 封装程序名命令Disconnect()— 关闭连接- 线程安全(单个命令通道同一时间只处理一个请求)
需要封装的具体命令方法:
StopProgramAsync(string name)→PackProgramCommand(0x2103, name)ResetRobotAsync()→PackEmptyCommand(0x2100)GetProgramStatusAsync(string name)→PackProgramCommand(0x2003, name)StartProgramAsync(string name)→PackProgramCommand(0x2102, name)GetTcpAsync()/SetTcpAsync()— 已按tcp_id + f32[7] pose字段布局实现GetSpeedRatioAsync()/SetSpeedRatioAsync()— 已按ratio_int / 100.0与ratio_int_0_100字段布局实现GetIoAsync()/SetIoAsync()— 已按io_type / io_index / f32 io_value字段布局实现
测试:tests/Flyshot.Core.Tests/FanucCommandClientTests.cs
- 用
TcpListener本地模拟控制器,验证帧收发与解析
Phase 2: TCP 10010 状态客户端
新建文件:src/Flyshot.Runtime.Fanuc/Protocol/FanucStateClient.cs
职责:
Connect(string ip, int port = 10010)— 建立 TcpClient 连接- 内部启动后台
Task循环ReadAsync(FanucStateProtocol.StateFrameLength) - 每收到一帧调用
FanucStateProtocol.ParseFrame() - 将解析结果写入线程安全的最新状态缓存
- 单帧接收超时后标记状态陈旧,不再把旧帧当作当前位姿/关节状态使用
- EOF、坏帧、Socket 异常或超时后关闭当前连接,并按退避策略自动重连 TCP 10010
GetLatestFrame()— 返回最近一次解析的状态帧GetStatus()— 返回连接阶段、陈旧状态、最近异常和重连次数Disconnect()— 取消后台循环并关闭连接
测试:tests/Flyshot.Core.Tests/FanucStateClientTests.cs
- 用
TcpListener本地发送抓包样本 hex,验证后台循环能正确解析。 - 用本地模拟控制器验证无状态帧超时、EOF 后退避重连和重连后的继续收帧。
FanucStateProtocol已用j519 协议.pcap中多条 90B 样本锁定pose[6]、joint[6]、external_axes[3]和raw_tail_words[4]。- 尾部状态字当前只作为
ControllerStateSnapshot.stateTailWords诊断字段保留,不从[2,0,0,1]推断使能或运动状态。
Phase 3: UDP 60015 J519 运动客户端
新建文件:src/Flyshot.Runtime.Fanuc/Protocol/FanucJ519Client.cs
职责:
Connect(string ip, int port = 60015)— 创建 UdpClient- 发送 init packet (
PackInitPacket()) - 内部启动发送循环(约 8ms 周期)
UpdateCommand(FanucJ519Command command)— 原子更新下一周期要发送的命令StartMotion()— 启动发送循环StopMotion()— 发送 end packet,停止循环- 接收线程:持续
ReceiveAsync()解析 132B 响应,更新反馈状态 Disconnect()— 清理
测试:tests/Flyshot.Core.Tests/FanucJ519ClientTests.cs
- 用本地 UDP socket 模拟控制器收发
Phase 4: 重写 FanucControllerRuntime
改造文件:src/Flyshot.Runtime.Fanuc/FanucControllerRuntime.cs
将当前内存桩替换为真实运行时:
- 持有三个客户端实例:
FanucCommandClient、FanucStateClient、FanucJ519Client Connect(robotIp)— 顺序连接 10010 → 10012 → 60015EnableRobot(bufferSize)— 走完整 StartProg 序列(Stop→Reset→Status→Start RVBUSTSM),然后启动 J519DisableRobot()— 停止 J519,发送 StopProgDisconnect()— 断开三条通道ExecuteTrajectory(result, finalJointPositions)— 将规划后的稠密路点通过 J519 逐帧发送StopMove()— 立即停止 J519 发送循环GetSnapshot()— 优先从FanucStateClient读取最新状态;若状态通道未连接,回退到内存值GetJointPositions()/GetPose()/GetTcp()/GetSpeedRatio()/GetIo()— 优先从真实通道读取SetTcp()/SetSpeedRatio()/SetIo()— 通过命令通道发送
Phase 5: 端到端集成测试
改造/新建测试:
tests/Flyshot.Server.IntegrationTests/LegacyHttpApiCompatibilityTests.cs— 补充真实连接流程(可用本地模拟器)tests/Flyshot.Core.Tests/FanucControllerRuntimeSocketTests.cs— 用本地 TCP/UDP 模拟器验证完整链路
验证命令:
cd flyshot-replacement
dotnet build FlyshotReplacement.sln -v minimal
dotnet test tests/Flyshot.Core.Tests/Flyshot.Core.Tests.csproj -v minimal
dotnet test tests/Flyshot.Server.IntegrationTests/Flyshot.Server.IntegrationTests.csproj -v minimal
风险与回退策略
- 真机连接风险:第一版 Socket 实现可能有超时/重连问题。
FanucControllerRuntime保留_simulationMode路径,仿真模式下仍走内存桩。 - 性能风险:同步接口内部阻塞 Socket 可能影响 HTTP 并发。若实测有问题,后续将
IControllerRuntime改为 async。 - 现场验证风险:TCP 10012 参数命令已按逆向结论实现,但仍需在真实 R30iB 控制柜上确认默认
tcp_id=1、IO 类型/地址和错误码语义。
关键文件清单
| 文件 | 动作 |
|---|---|
src/Flyshot.Runtime.Fanuc/Protocol/FanucCommandClient.cs |
新建 |
src/Flyshot.Runtime.Fanuc/Protocol/FanucStateClient.cs |
新建 |
src/Flyshot.Runtime.Fanuc/Protocol/FanucJ519Client.cs |
新建 |
src/Flyshot.Runtime.Fanuc/FanucControllerRuntime.cs |
重写 |
tests/Flyshot.Core.Tests/FanucCommandClientTests.cs |
新建 |
tests/Flyshot.Core.Tests/FanucStateClientTests.cs |
新建 |
tests/Flyshot.Core.Tests/FanucJ519ClientTests.cs |
新建 |
tests/Flyshot.Core.Tests/FanucControllerRuntimeSocketTests.cs |
新建 |
下一步验证标准
FanucControllerRuntime的Connect()能成功建立三条 TCP/UDP 连接EnableRobot()能走完RVBUSTSM启动序列ExecuteTrajectory()能按 8ms 周期通过 J519 发送路点GetSnapshot()返回的值来自 TCP 10010 真实状态帧而非内存- 现有 10 个集成测试和 25 个核心测试仍然通过