feat(flyshot): 引入飞拍执行侧最终发送队列构建与校验机制

* 新增 FlyshotExecutionSendSequenceBuilder,负责在运行时前构建最终 8ms 发送队列,并进行离散限幅校验。
* 引入 FlyshotPreparedExecution 类,封装最终发送结果及相关诊断信息。
* 调整 ControllerClientCompatService 和 FanucControllerRuntime,确保运行时直接使用预生成的发送队列,避免临场重采样。
* 更新 TrajectoryResult 和 PlannedExecutionBundle,支持准备好的执行队列。
* 增加单元测试,验证非 1 倍 speedRatio 下的执行行为与预生成队列的使用。
This commit is contained in:
2026-05-09 19:06:49 +08:00
parent f7e2bb0e7b
commit 74761bb5da
11 changed files with 1185 additions and 75 deletions

View File

@@ -826,6 +826,48 @@ public sealed class RuntimeOrchestrationTests
}
}
/// <summary>
/// 验证飞拍链路在进入运行时前就会准备最终发送队列,而不是把 speedRatio 重采样留给运行时临场处理。
/// </summary>
[Fact]
public void ControllerClientCompatService_ExecuteTrajectoryByName_PreparesFinalSendQueueBeforeRuntime()
{
var configRoot = CreateTempConfigRoot();
try
{
WriteRobotConfigWithDemoTrajectory(configRoot);
var options = new ControllerClientCompatOptions { ConfigRoot = configRoot };
var runtime = new RecordingControllerRuntime();
var service = new ControllerClientCompatService(
options,
new ControllerClientCompatRobotCatalog(options, new RobotModelLoader()),
runtime,
new ControllerClientTrajectoryOrchestrator(),
new RobotConfigLoader());
service.SetUpRobot("FANUC_LR_Mate_200iD");
service.SetActiveController(sim: false);
service.Connect("192.168.10.101");
service.EnableRobot(2);
service.UploadTrajectory(TestRobotFactory.CreateUploadedTrajectoryWithSingleShot());
runtime.SetSpeedRatio(0.5);
service.ExecuteTrajectoryByName(
"demo-flyshot",
new FlyshotExecutionOptions(moveToStart: false, method: "icsp", saveTrajectory: false, useCache: false));
var result = Assert.IsType<TrajectoryResult>(runtime.LastExecutedResult);
var preparedExecution = Assert.IsType<FlyshotPreparedExecution>(result.PreparedFlyshotExecution);
Assert.NotEmpty(preparedExecution.Samples);
Assert.Equal(preparedExecution.Samples.Count, preparedExecution.TimingRows.Count);
Assert.Equal(result.TriggerTimeline.Count, preparedExecution.TriggerBindings.Count);
}
finally
{
Directory.Delete(configRoot, recursive: true);
}
}
/// <summary>
/// 验证兼容服务初始化机器人时会把 RobotConfig.json 中的 acc_limit / jerk_limit 传给模型加载器。
/// </summary>
@@ -953,14 +995,14 @@ public sealed class RuntimeOrchestrationTests
var executionDuration = double.Parse(
File.ReadLines(Path.Combine(outputDir, "JointDetialTraj.txt")).Last().Split(' ')[0],
CultureInfo.InvariantCulture);
var expectedRows = (int)Math.Ceiling(Math.Max(0.0, (executionDuration / (0.008 * 0.5)) - 1e-9)) + 1;
var minimumExpectedRows = (int)Math.Ceiling(Math.Max(0.0, (executionDuration / (0.008 * 0.5)) - 1e-9)) + 1;
Assert.Equal(expectedRows, pointRows.Length);
Assert.Equal(expectedRows, timingRows.Length);
Assert.Equal(pointRows.Length, timingRows.Length);
Assert.True(pointRows.Length >= minimumExpectedRows, $"最终发送点数应不少于请求倍率的首轮候选值actual={pointRows.Length}, min={minimumExpectedRows}");
Assert.Equal(0.0, pointRows[0][0], precision: 6);
Assert.Equal(0.008, pointRows[1][0], precision: 6);
Assert.Equal(0.004, timingRows[1][2], precision: 6);
Assert.Equal(0.5, timingRows[1][3], precision: 6);
Assert.True(timingRows[1][2] <= 0.004 + 1e-6, $"自动拉长后 trajectory_time 推进不应快于请求倍率actual={timingRows[1][2]:F6}");
Assert.True(timingRows[1][3] <= 0.5 + 1e-6, $"最终采用倍率不应快于请求倍率actual={timingRows[1][3]:F6}");
Assert.Contains("\"trigger_window_seconds\": 0.1", shotEventsJson);
Assert.Contains("\"selected_sample_index\"", shotEventsJson);
}
@@ -1607,6 +1649,11 @@ internal sealed class RecordingControllerRuntime : IControllerRuntime
/// </summary>
public RobotProfile? LastRobotProfile { get; private set; }
/// <summary>
/// 获取最近一次 ExecuteTrajectory 收到的结果对象。
/// </summary>
public TrajectoryResult? LastExecutedResult { get; private set; }
/// <inheritdoc />
public void ResetRobot(RobotProfile robot, string robotName)
{
@@ -1691,6 +1738,7 @@ internal sealed class RecordingControllerRuntime : IControllerRuntime
/// <inheritdoc />
public void ExecuteTrajectory(TrajectoryResult result, IReadOnlyList<double> finalJointPositions)
{
LastExecutedResult = result;
}
}