🐛 fix(flyshot): 修复输出选择并同步现场配置与文档

- 恢复 use_do 对 DO/RO 的选择,保留拍照脉冲与清零
- 统一直接关节移动与飞拍预移动的规划和运行倍率
- 补充输出选择、预生成队列和点对点倍率回归测试
- 更新 WhP_test 轨迹配置、系统说明和 README
- 删除本地 Claude 配置文件

验证:构建零警告零错误,定向测试 6 项与集成测试 38 项通过。
核心全量 175 项通过、14 项失败,失败项与修改前基线一致。
This commit is contained in:
2026-10-01 19:11:50 +08:00
parent acccb94807
commit abea2d7517
12 changed files with 1228 additions and 372 deletions

View File

@@ -16,6 +16,70 @@ namespace Flyshot.Core.Tests;
/// </summary>
public sealed class FanucControllerRuntimeDenseTests
{
/// <summary>
/// 验证飞拍输出选择贯通到 J519 命令,DO/RO 模式都输出两周期脉冲并在同一路清零。
/// </summary>
/// <param name="useDo">true 选择 DO,false 选择 RO。</param>
/// <param name="expectedIoType">预期 J519 写入类型,DO 为 2,RO 为 9。</param>
/// <param name="usePreparedExecution">是否使用上游已完成触发绑定的最终发送队列。</param>
[Theory]
[InlineData(true, 2, false)]
[InlineData(false, 9, false)]
[InlineData(true, 2, true)]
[InlineData(false, 9, true)]
public void ExecuteTrajectory_FlyshotOutputSelection_PreservesPulseAndClear(bool useDo, int expectedIoType, bool usePreparedExecution)
{
using var commandClient = new FanucCommandClient();
using var stateClient = new FanucStateClient();
using var j519Client = new FanucJ519Client();
using var runtime = new FanucControllerRuntime(commandClient, stateClient, j519Client);
runtime.ResetRobot(TestRobotFactory.CreateRobotProfile(), "FANUC_LR_Mate_200iD");
j519Client.EnableCommandHistoryForTests();
ForceRealModeEnabled(runtime, speedRatio: 1.0);
var trigger = new TrajectoryDoEvent(waypointIndex: 1, triggerTime: 0.008,
offsetCycles: 0, holdCycles: 2, addressGroup: new IoAddressGroup([1]), useDo: useDo);
var samples = Enumerable.Range(0, 4)
.Select(index => new FlyshotPreparedSample(index, index * 0.008, index * 0.008,
1.0, [0.0, 0.0, 0.0, 0.0, 0.0, 0.0])).ToArray();
var preparedExecution = usePreparedExecution
? new FlyshotPreparedExecution(samples,
[new FlyshotPreparedTriggerBinding(trigger, samples[1], 1, true)],
Array.Empty<IReadOnlyList<double>>(), Array.Empty<IReadOnlyList<double>>(),
requestSpeedRatio: 1.0, finalSpeedRatio: 1.0, finalDurationSeconds: 0.024, stretchIterationCount: 0)
: null;
// 静止轨迹隔离运动算法影响,仅验证触发、保持和清零对应的协议字段。
var result = new TrajectoryResult(
programName: "output-selection",
method: PlanningMethod.Icsp,
isValid: true,
duration: TimeSpan.FromSeconds(0.024),
shotEvents: Array.Empty<ShotEvent>(),
triggerTimeline: [trigger],
artifacts: Array.Empty<TrajectoryArtifact>(),
failureReason: null,
usedCache: false,
originalWaypointCount: 2,
plannedWaypointCount: 2,
denseJointTrajectory:
[
new double[] { 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 },
new double[] { 0.024, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 }
],
preparedFlyshotExecution: preparedExecution);
runtime.ExecuteTrajectory(result, [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
WaitUntilIdle(runtime);
var commands = j519Client.GetCommandHistoryForTests();
Assert.True(commands.Count >= 4);
Assert.All(commands, command => Assert.Equal((byte)expectedIoType, command.WriteIoType));
Assert.All(commands, command => Assert.Equal((ushort)1, command.WriteIoIndex));
Assert.Equal(new ushort[] { 0, 1, 1, 1 }, commands.Take(4).Select(command => command.WriteIoMask));
Assert.Equal(new ushort[] { 0, 1, 1, 0 }, commands.Take(4).Select(command => command.WriteIoValue));
}
private const double SmoothPtpVelocityShapeCoefficient = 2.1875;
private const double SmoothPtpAccelerationShapeCoefficient = 7.513188404399293;
private const double SmoothPtpJerkShapeCoefficient = 52.5;

View File

@@ -241,13 +241,14 @@ public sealed class RuntimeOrchestrationTests
var doEvent = Assert.Single(bundle.Result.TriggerTimeline);
Assert.Equal(4, doEvent.HoldCycles);
Assert.True(doEvent.UseDo);
}
/// <summary>
/// 验证 RobotConfig.json 关闭 use_do 时仍保留 ShotEvent 诊断信息,但不生成伺服 DO 事件。
/// 验证 use_do=false 时仍生成飞拍触发事件,保持周期不变。
/// </summary>
[Fact]
public void ControllerClientTrajectoryOrchestrator_PlanUploadedFlyshot_SuppressesDoTimeline_WhenUseDoIsFalse()
public void ControllerClientTrajectoryOrchestrator_PlanUploadedFlyshot_PreservesTriggerTimeline_WhenUseDoIsFalse()
{
var orchestrator = new ControllerClientTrajectoryOrchestrator();
var robot = TestRobotFactory.CreateRobotProfile();
@@ -264,7 +265,9 @@ public sealed class RuntimeOrchestrationTests
var bundle = orchestrator.PlanUploadedFlyshot(robot, uploaded, settings: settings);
Assert.Single(bundle.Result.ShotEvents);
Assert.Empty(bundle.Result.TriggerTimeline);
var trigger = Assert.Single(bundle.Result.TriggerTimeline);
Assert.Equal(4, trigger.HoldCycles);
Assert.False(trigger.UseDo);
}
/// <summary>
@@ -788,6 +791,66 @@ public sealed class RuntimeOrchestrationTests
}
}
/// <summary>
/// 验证直接关节移动和飞拍预移动均按规划倍率与当前运行倍率的乘积生成点对点轨迹。
/// </summary>
/// <param name="moveToStart">是否通过飞拍预移动入口执行点对点运动。</param>
/// <param name="planningSpeedScale">配置中的规划速度倍率。</param>
/// <param name="speedRatio">机器人初始化后设置的当前运行速度倍率。</param>
[Theory]
[InlineData(false, 0.74227, 1.0)]
[InlineData(false, 0.74227, 0.5)]
[InlineData(false, 1.0, 0.7)]
[InlineData(true, 0.74227, 0.5)]
public void ControllerClientCompatService_MoveJoint_AppliesCombinedPlanningSpeedScale(
bool moveToStart, double planningSpeedScale, double speedRatio)
{
var configRoot = CreateTempConfigRoot();
try
{
WriteRobotConfigWithDemoTrajectory(configRoot, planningSpeedScale: planningSpeedScale, speedRatio: 1.0);
var options = new ControllerClientCompatOptions { ConfigRoot = configRoot };
double[] startJoints = [0.4, 0.0, 0.0, 0.0, 0.0, 0.0];
double[] targetJoints = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
var runtime = new DelayedCompletionControllerRuntime(startJoints, TimeSpan.Zero);
var service = new ControllerClientCompatService(
options,
new ControllerClientCompatRobotCatalog(options, new RobotModelLoader()),
runtime,
new ControllerClientTrajectoryOrchestrator(),
new RobotConfigLoader());
service.SetUpRobot("FANUC_LR_Mate_200iD");
service.Connect("192.168.10.101");
service.EnableRobot(2);
service.SetSpeedRatio(speedRatio);
// 两个公开入口必须共享相同的点对点限速,且采用当前倍率而非配置默认倍率。
if (moveToStart)
{
service.UploadTrajectory(TestRobotFactory.CreateUploadedTrajectoryWithSingleShot());
service.ExecuteTrajectoryByName("demo-flyshot", new FlyshotExecutionOptions(moveToStart: true, useCache: false));
}
else
{
service.MoveJoint(targetJoints);
}
var robot = new ControllerClientCompatRobotCatalog(options, new RobotModelLoader())
.LoadProfile("FANUC_LR_Mate_200iD", accLimitScale: 1.0, jerkLimitScale: 1.0);
var expected = MoveJointTrajectoryGenerator.CreateResult(robot, startJoints, targetJoints, planningSpeedScale * speedRatio);
var actual = runtime.ExecuteCalls[0].Result;
Assert.Equal(expected.Duration, actual.Duration);
Assert.Equal(expected.DenseJointTrajectory!.Count, actual.DenseJointTrajectory!.Count);
Assert.Equal(targetJoints, runtime.ExecuteCalls[0].FinalJointPositions);
Assert.Equal(speedRatio, runtime.GetSnapshot().SpeedRatio);
Assert.False(runtime.SecondTrajectoryStartedBeforeFirstMotionCompleted);
}
finally
{
Directory.Delete(configRoot, recursive: true);
}
}
/// <summary>
/// 验证 ExecuteFlyShotTraj(move_to_start=true) 会先执行稠密 PTP 到起点,并等待该段运动完成后再启动飞拍轨迹。
/// </summary>
@@ -2226,6 +2289,10 @@ internal sealed class RecordingControllerRuntime : IControllerRuntime
/// </summary>
internal sealed class DelayedCompletionControllerRuntime : IControllerRuntime
{
/// <summary>
/// 当前测试运行速度倍率,用于验证初始化与运行中调速对预移动规划的影响。
/// </summary>
private double _speedRatio = 1.0;
private readonly object _lock = new();
private readonly TimeSpan _firstMotionCompletionDelay;
private double[] _jointPositions;
@@ -2298,11 +2365,12 @@ internal sealed class DelayedCompletionControllerRuntime : IControllerRuntime
}
/// <inheritdoc />
public double GetSpeedRatio() => 1.0;
public double GetSpeedRatio() => _speedRatio;
/// <inheritdoc />
public void SetSpeedRatio(double ratio)
{
_speedRatio = ratio;
}
/// <inheritdoc />
@@ -2343,7 +2411,7 @@ internal sealed class DelayedCompletionControllerRuntime : IControllerRuntime
connectionState: "Connected",
isEnabled: _isEnabled,
isInMotion: _isInMotion,
speedRatio: 1.0,
speedRatio: _speedRatio,
jointPositions: _jointPositions.ToArray(),
cartesianPose: Array.Empty<double>(),
activeAlarms: Array.Empty<RuntimeAlarm>());