feat(compat): 补齐飞拍执行等待与 FANUC 状态驱动链路

- 为 ExecuteFlyShotTraj 补齐 wait 语义,并让 move_to_start
  先完成临时 PTP 运动后再启动正式飞拍轨迹
- 将 J519 命令发送改为由机器人 UDP status sequence 驱动,
  避免在未收到状态包时主动发周期命令
- 将 10010 状态通道关节字段统一按 JointRadians 命名,
  同步更新运行时读取逻辑与协议测试
- 新增 FANUC 10010 状态帧、流运动手册和 Python client
  逆向文档,并更新 README 与兼容需求说明
- 补充兼容层编排测试与 HTTP 集成测试,覆盖 wait 和
  move_to_start 串行化行为
This commit is contained in:
2026-05-03 19:29:31 +08:00
parent 91c1494cde
commit af65ca03a0
17 changed files with 1694 additions and 214 deletions

View File

@@ -293,6 +293,92 @@ public sealed class RuntimeOrchestrationTests
Assert.Throws<ArgumentException>(Act);
}
/// <summary>
/// 验证 ExecuteFlyShotTraj(move_to_start=true) 会先执行稠密 PTP 到起点,并等待该段运动完成后再启动飞拍轨迹。
/// </summary>
[Fact]
public void ControllerClientCompatService_ExecuteTrajectoryByName_MoveToStartWaitsBeforeFlyshot()
{
var configRoot = CreateTempConfigRoot();
try
{
var options = new ControllerClientCompatOptions
{
ConfigRoot = configRoot
};
var runtime = new DelayedCompletionControllerRuntime(
initialJointPositions: [0.4, 0.0, 0.0, 0.0, 0.0, 0.0],
firstMotionCompletionDelay: TimeSpan.FromMilliseconds(80));
var service = new ControllerClientCompatService(
options,
new ControllerClientCompatRobotCatalog(options, new RobotModelLoader()),
runtime,
new ControllerClientTrajectoryOrchestrator(),
new RobotConfigLoader());
service.SetUpRobot("FANUC_LR_Mate_200iD");
service.SetActiveController(sim: false);
service.Connect("192.168.10.101");
service.EnableRobot(2);
service.UploadTrajectory(TestRobotFactory.CreateUploadedTrajectoryWithSingleShot());
service.ExecuteTrajectoryByName(
"demo-flyshot",
new FlyshotExecutionOptions(moveToStart: true, method: "icsp", saveTrajectory: false, useCache: false));
Assert.True(runtime.ExecuteCalls.Count >= 2);
Assert.NotNull(runtime.ExecuteCalls[0].Result.DenseJointTrajectory);
Assert.True(runtime.ExecuteCalls[0].Result.DenseJointTrajectory!.Count > 1);
Assert.False(runtime.SecondTrajectoryStartedBeforeFirstMotionCompleted);
}
finally
{
Directory.Delete(configRoot, recursive: true);
}
}
/// <summary>
/// 验证 ExecuteFlyShotTraj(wait=true) 会等待正式飞拍轨迹完成后再返回。
/// </summary>
[Fact]
public void ControllerClientCompatService_ExecuteTrajectoryByName_WaitTrueWaitsForFlyshotCompletion()
{
var configRoot = CreateTempConfigRoot();
try
{
var options = new ControllerClientCompatOptions
{
ConfigRoot = configRoot
};
var runtime = new DelayedCompletionControllerRuntime(
initialJointPositions: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
firstMotionCompletionDelay: TimeSpan.FromMilliseconds(80));
var service = new ControllerClientCompatService(
options,
new ControllerClientCompatRobotCatalog(options, new RobotModelLoader()),
runtime,
new ControllerClientTrajectoryOrchestrator(),
new RobotConfigLoader());
service.SetUpRobot("FANUC_LR_Mate_200iD");
service.SetActiveController(sim: false);
service.Connect("192.168.10.101");
service.EnableRobot(2);
service.UploadTrajectory(TestRobotFactory.CreateUploadedTrajectoryWithSingleShot());
service.ExecuteTrajectoryByName(
"demo-flyshot",
new FlyshotExecutionOptions(moveToStart: false, method: "icsp", saveTrajectory: false, useCache: false, wait: true));
Assert.Single(runtime.ExecuteCalls);
Assert.False(runtime.GetSnapshot().IsInMotion);
}
finally
{
Directory.Delete(configRoot, recursive: true);
}
}
/// <summary>
/// 验证兼容服务初始化机器人时会把 RobotConfig.json 中的 acc_limit / jerk_limit 传给模型加载器。
/// </summary>
@@ -650,3 +736,164 @@ internal sealed class RecordingControllerRuntime : IControllerRuntime
{
}
}
/// <summary>
/// 模拟第一段运动异步完成的测试运行时,用于验证兼容层是否等待 move_to_start 完成。
/// </summary>
internal sealed class DelayedCompletionControllerRuntime : IControllerRuntime
{
private readonly object _lock = new();
private readonly TimeSpan _firstMotionCompletionDelay;
private double[] _jointPositions;
private bool _isEnabled;
private bool _isInMotion;
private bool _firstMotionCompleted;
/// <summary>
/// 初始化可延迟完成第一段运动的测试运行时。
/// </summary>
/// <param name="initialJointPositions">运行时报告的初始关节位置。</param>
/// <param name="firstMotionCompletionDelay">第一段运动完成前保持忙碌的时间。</param>
public DelayedCompletionControllerRuntime(
IReadOnlyList<double> initialJointPositions,
TimeSpan firstMotionCompletionDelay)
{
_jointPositions = initialJointPositions.ToArray();
_firstMotionCompletionDelay = firstMotionCompletionDelay;
}
/// <summary>
/// 获取所有 ExecuteTrajectory 调用记录。
/// </summary>
public List<(TrajectoryResult Result, IReadOnlyList<double> FinalJointPositions)> ExecuteCalls { get; } = [];
/// <summary>
/// 获取第二条轨迹是否在第一段 move_to_start 完成前启动。
/// </summary>
public bool SecondTrajectoryStartedBeforeFirstMotionCompleted { get; private set; }
/// <inheritdoc />
public void ResetRobot(RobotProfile robot, string robotName)
{
}
/// <inheritdoc />
public void SetActiveController(bool sim)
{
}
/// <inheritdoc />
public void Connect(string robotIp)
{
}
/// <inheritdoc />
public void Disconnect()
{
}
/// <inheritdoc />
public void EnableRobot(int bufferSize)
{
_isEnabled = true;
}
/// <inheritdoc />
public void DisableRobot()
{
_isEnabled = false;
}
/// <inheritdoc />
public void StopMove()
{
lock (_lock)
{
_isInMotion = false;
}
}
/// <inheritdoc />
public double GetSpeedRatio() => 1.0;
/// <inheritdoc />
public void SetSpeedRatio(double ratio)
{
}
/// <inheritdoc />
public IReadOnlyList<double> GetTcp() => [0.0, 0.0, 0.0];
/// <inheritdoc />
public void SetTcp(double x, double y, double z)
{
}
/// <inheritdoc />
public bool GetIo(int port, string ioType) => false;
/// <inheritdoc />
public void SetIo(int port, bool value, string ioType)
{
}
/// <inheritdoc />
public IReadOnlyList<double> GetJointPositions()
{
lock (_lock)
{
return _jointPositions.ToArray();
}
}
/// <inheritdoc />
public IReadOnlyList<double> GetPose() => Array.Empty<double>();
/// <inheritdoc />
public ControllerStateSnapshot GetSnapshot()
{
lock (_lock)
{
return new ControllerStateSnapshot(
capturedAt: DateTimeOffset.UtcNow,
connectionState: "Connected",
isEnabled: _isEnabled,
isInMotion: _isInMotion,
speedRatio: 1.0,
jointPositions: _jointPositions.ToArray(),
cartesianPose: Array.Empty<double>(),
activeAlarms: Array.Empty<RuntimeAlarm>());
}
}
/// <inheritdoc />
public void ExecuteTrajectory(TrajectoryResult result, IReadOnlyList<double> finalJointPositions)
{
lock (_lock)
{
ExecuteCalls.Add((result, finalJointPositions.ToArray()));
if (ExecuteCalls.Count == 1)
{
_isInMotion = true;
_ = Task.Run(async () =>
{
await Task.Delay(_firstMotionCompletionDelay).ConfigureAwait(false);
lock (_lock)
{
_jointPositions = finalJointPositions.ToArray();
_isInMotion = false;
_firstMotionCompleted = true;
}
});
return;
}
if (!_firstMotionCompleted)
{
SecondTrajectoryStartedBeforeFirstMotionCompleted = true;
}
_jointPositions = finalJointPositions.ToArray();
}
}
}