feat(*): 添加轨迹产物导出与规划速度倍率隔离

* 新增 FlyshotTrajectoryArtifactWriter,支持 saveTrajectory
  将规划结果导出到 Config/Data/name(JointTraj、CartTraj、
  ShotEvents 等)
* RobotConfig 新增 PlanningSpeedScale,区分规划阶段限速倍率
  与运行时 J519 下发倍率
* 轨迹缓存键纳入 planningSpeedScale,避免降速规划误用缓存
* 完善 FanucCommandClient 命令参数日志与状态通道重连
* 补充 RuntimeOrchestrationTests 覆盖产物导出与倍率隔离
* 更新 README 进度文档
This commit is contained in:
2026-04-30 13:52:09 +08:00
parent a6579f1e5b
commit 91c1494cde
20 changed files with 593 additions and 133 deletions

View File

@@ -24,6 +24,7 @@ public sealed class ConfigCompatibilityTests
Assert.Equal(2, loaded.Robot.IoKeepCycles);
Assert.Equal(1.0, loaded.Robot.AccLimitScale);
Assert.Equal(1.0, loaded.Robot.JerkLimitScale);
Assert.Equal(1.0, loaded.Robot.PlanningSpeedScale);
Assert.Equal(5, loaded.Robot.AdaptIcspTryNum);
var program = Assert.Contains("EOL10_EAU_0", loaded.Programs);
@@ -71,6 +72,7 @@ public sealed class ConfigCompatibilityTests
Assert.Equal(3, loaded.Robot.IoKeepCycles);
Assert.Equal(0.5, loaded.Robot.AccLimitScale);
Assert.Equal(0.25, loaded.Robot.JerkLimitScale);
Assert.Equal(1.0, loaded.Robot.PlanningSpeedScale);
Assert.Equal([0, 0, 0], program.OffsetValues);
Assert.All(program.AddressGroups, group => Assert.Empty(group.Addresses));
}
@@ -80,6 +82,46 @@ public sealed class ConfigCompatibilityTests
}
}
/// <summary>
/// 验证 RobotConfig.json 可以显式配置规划限速倍率,且该倍率独立于运行时 J519 速度倍率。
/// </summary>
[Fact]
public void RobotConfigLoader_LoadsPlanningSpeedScale()
{
var tempRoot = CreateTempDirectory();
try
{
var configPath = Path.Combine(tempRoot, "legacy.json");
File.WriteAllText(
configPath,
"""
{
"robot": {
"use_do": true,
"io_keep_cycles": 2,
"acc_limit": 1.0,
"jerk_limit": 1.0,
"planning_speed_scale": 0.742277
},
"flying_shots": {
"demo": {
"traj_waypoints": [[0, 1], [2, 3], [4, 5], [6, 7]],
"shot_flags": [false, false, false, false]
}
}
}
""");
var loaded = new RobotConfigLoader().Load(configPath);
Assert.Equal(0.742277, loaded.Robot.PlanningSpeedScale, precision: 6);
}
finally
{
Directory.Delete(tempRoot, recursive: true);
}
}
/// <summary>
/// 验证 .robot 解析会保留 Joint3 对 Joint2 的 couple 元数据,并构造规划侧可直接消费的 RobotProfile。
/// </summary>

View File

@@ -57,10 +57,10 @@ public sealed class ControllerClientCompatConfigRootTests
}
/// <summary>
/// 验证 JSON 轨迹存储保存、加载和删除都落在 ConfigRoot/TrajectoryStore 目录
/// 验证 JSON 轨迹存储保存、加载和删除都落在 ConfigRoot/RobotConfig.json
/// </summary>
[Fact]
public void JsonFlyshotTrajectoryStore_PersistsTrajectoriesUnderConfigRootStore()
public void JsonFlyshotTrajectoryStore_PersistsTrajectoriesInRobotConfigJson()
{
var configRoot = CreateTempConfigRoot();
try
@@ -77,9 +77,10 @@ public sealed class ControllerClientCompatConfigRootTests
var trajectory = TestRobotFactory.CreateUploadedTrajectoryWithSingleShot();
store.Save("FANUC_LR_Mate_200iD", settings, trajectory);
var expectedPath = Path.Combine(configRoot, "TrajectoryStore", "FANUC_LR_Mate_200iD_trajectories.json");
var expectedPath = Path.Combine(configRoot, "RobotConfig.json");
Assert.True(File.Exists(expectedPath), $"应在运行目录 Config 下创建轨迹文件: {expectedPath}");
Assert.True(File.Exists(expectedPath), $"应在运行目录 Config 下创建统一配置文件: {expectedPath}");
Assert.False(Directory.Exists(Path.Combine(configRoot, "TrajectoryStore")), "不应再创建独立轨迹存储目录。");
var loaded = store.LoadAll("FANUC_LR_Mate_200iD", out var loadedSettings);
Assert.NotNull(loadedSettings);
Assert.Contains(trajectory.Name, loaded);

View File

@@ -555,8 +555,7 @@ public sealed class FanucControllerRuntimeDenseTests
new ControllerClientCompatRobotCatalog(options, new RobotModelLoader()),
runtime,
new ControllerClientTrajectoryOrchestrator(),
new RobotConfigLoader(),
new InMemoryFlyshotTrajectoryStore());
new RobotConfigLoader());
}
private static double ComputeLineAlpha(

View File

@@ -136,6 +136,42 @@ public sealed class RuntimeOrchestrationTests
Assert.Single(bundle.Result.TriggerTimeline);
}
/// <summary>
/// 验证飞拍规划会把规划限速倍率纳入速度/加速度/Jerk 限制,而不是复用运行时下发倍率。
/// </summary>
[Fact]
public void ControllerClientTrajectoryOrchestrator_PlanUploadedFlyshot_AppliesPlanningSpeedScaleToLimits()
{
var orchestrator = new ControllerClientTrajectoryOrchestrator();
var robot = TestRobotFactory.CreateRobotProfile();
var uploaded = TestRobotFactory.CreateUploadedTrajectoryWithSingleShot();
var fullSpeed = orchestrator.PlanUploadedFlyshot(robot, uploaded, planningSpeedScale: 1.0);
var halfSpeed = orchestrator.PlanUploadedFlyshot(robot, uploaded, planningSpeedScale: 0.5);
Assert.True(
halfSpeed.Result.Duration.TotalSeconds > fullSpeed.Result.Duration.TotalSeconds * 1.9,
$"半速规划时长应接近全速的 2 倍,实际 full={fullSpeed.Result.Duration.TotalSeconds}, half={halfSpeed.Result.Duration.TotalSeconds}");
}
/// <summary>
/// 验证飞拍缓存键包含规划限速倍率,避免降速验证时误用 100% 速度下的规划结果。
/// </summary>
[Fact]
public void ControllerClientTrajectoryOrchestrator_PlanUploadedFlyshot_CacheKeyIncludesPlanningSpeedScale()
{
var orchestrator = new ControllerClientTrajectoryOrchestrator();
var robot = TestRobotFactory.CreateRobotProfile();
var uploaded = TestRobotFactory.CreateUploadedTrajectoryWithSingleShot();
var options = new FlyshotExecutionOptions(useCache: true);
var fullSpeed = orchestrator.PlanUploadedFlyshot(robot, uploaded, options, planningSpeedScale: 1.0);
var halfSpeed = orchestrator.PlanUploadedFlyshot(robot, uploaded, options, planningSpeedScale: 0.5);
Assert.False(halfSpeed.Result.UsedCache);
Assert.True(halfSpeed.Result.Duration > fullSpeed.Result.Duration);
}
/// <summary>
/// 验证飞拍编排会使用 RobotConfig.json 中的 IO 保持周期。
/// </summary>
@@ -289,8 +325,7 @@ public sealed class RuntimeOrchestrationTests
new ControllerClientCompatRobotCatalog(options, new RobotModelLoader()),
runtime,
new ControllerClientTrajectoryOrchestrator(),
new RobotConfigLoader(),
new InMemoryFlyshotTrajectoryStore());
new RobotConfigLoader());
service.SetUpRobot("FANUC_LR_Mate_200iD");
@@ -304,6 +339,49 @@ public sealed class RuntimeOrchestrationTests
}
}
/// <summary>
/// 验证 IsFlyshotTrajectoryValid(saveTrajectory=true) 会把规划后的结果点位导出到 Config/Data/name。
/// </summary>
[Fact]
public void ControllerClientCompatService_IsFlyshotTrajectoryValid_SaveTrajectoryExportsPlannedData()
{
var configRoot = CreateTempConfigRoot();
try
{
WriteRobotConfigWithDemoTrajectory(configRoot);
var options = new ControllerClientCompatOptions { ConfigRoot = configRoot };
var service = new ControllerClientCompatService(
options,
new ControllerClientCompatRobotCatalog(options, new RobotModelLoader()),
new RecordingControllerRuntime(),
new ControllerClientTrajectoryOrchestrator(),
new RobotConfigLoader());
service.SetUpRobot("FANUC_LR_Mate_200iD");
var valid = service.IsFlyshotTrajectoryValid(
out var duration,
"demo-flyshot",
method: "icsp",
saveTrajectory: true);
var outputDir = Path.Combine(configRoot, "Data", "demo-flyshot");
Assert.True(valid);
Assert.True(duration > TimeSpan.Zero);
Assert.True(File.Exists(Path.Combine(outputDir, "JointTraj.txt")));
Assert.True(File.Exists(Path.Combine(outputDir, "JointDetialTraj.txt")));
Assert.True(File.Exists(Path.Combine(outputDir, "CartTraj.txt")));
Assert.True(File.Exists(Path.Combine(outputDir, "CartDetialTraj.txt")));
Assert.True(File.Exists(Path.Combine(outputDir, "ShotEvents.json")));
Assert.NotEmpty(File.ReadAllLines(Path.Combine(outputDir, "JointDetialTraj.txt")));
Assert.NotEmpty(File.ReadAllLines(Path.Combine(outputDir, "CartDetialTraj.txt")));
}
finally
{
Directory.Delete(configRoot, recursive: true);
}
}
/// <summary>
/// 创建只包含当前支持机器人模型和 RobotConfig.json 的临时运行配置根。
/// </summary>
@@ -322,6 +400,41 @@ public sealed class RuntimeOrchestrationTests
return configRoot;
}
/// <summary>
/// 写入包含一条飞拍轨迹的最小 RobotConfig.json供兼容服务从统一配置恢复轨迹。
/// </summary>
/// <param name="configRoot">测试运行配置根。</param>
private static void WriteRobotConfigWithDemoTrajectory(string configRoot)
{
File.WriteAllText(
Path.Combine(configRoot, "RobotConfig.json"),
"""
{
"robot": {
"use_do": true,
"io_addr": [7, 8],
"io_keep_cycles": 2,
"acc_limit": 1.0,
"jerk_limit": 1.0,
"adapt_icsp_try_num": 5
},
"flying_shots": {
"demo-flyshot": {
"traj_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]
],
"shot_flags": [false, true, false, false],
"offset_values": [0, 1, 0, 0],
"addr": [[], [7, 8], [], []]
}
}
}
""");
}
}
/// <summary>
@@ -389,8 +502,7 @@ internal static class TestRobotFactory
new ControllerClientCompatRobotCatalog(options, new RobotModelLoader()),
new FanucControllerRuntime(),
new ControllerClientTrajectoryOrchestrator(),
new RobotConfigLoader(),
new InMemoryFlyshotTrajectoryStore());
new RobotConfigLoader());
}
/// <summary>
@@ -443,33 +555,6 @@ internal static class TestRobotFactory
}
}
/// <summary>
/// 内存中的轨迹存储实现,用于避免单元测试污染真实文件系统。
/// </summary>
internal sealed class InMemoryFlyshotTrajectoryStore : IFlyshotTrajectoryStore
{
private readonly Dictionary<string, ControllerClientCompatUploadedTrajectory> _store = new(StringComparer.Ordinal);
/// <inheritdoc />
public void Save(string robotName, CompatibilityRobotSettings settings, ControllerClientCompatUploadedTrajectory trajectory)
{
_store[trajectory.Name] = trajectory;
}
/// <inheritdoc />
public void Delete(string robotName, string trajectoryName)
{
_store.Remove(trajectoryName);
}
/// <inheritdoc />
public IReadOnlyDictionary<string, ControllerClientCompatUploadedTrajectory> LoadAll(string robotName, out CompatibilityRobotSettings? settings)
{
settings = null;
return _store;
}
}
/// <summary>
/// 记录 ResetRobot 入参的测试运行时,用于验证兼容服务传递的机器人配置。
/// </summary>