✨ feat(*): 添加轨迹产物导出与规划速度倍率隔离
* 新增 FlyshotTrajectoryArtifactWriter,支持 saveTrajectory 将规划结果导出到 Config/Data/name(JointTraj、CartTraj、 ShotEvents 等) * RobotConfig 新增 PlanningSpeedScale,区分规划阶段限速倍率 与运行时 J519 下发倍率 * 轨迹缓存键纳入 planningSpeedScale,避免降速规划误用缓存 * 完善 FanucCommandClient 命令参数日志与状态通道重连 * 补充 RuntimeOrchestrationTests 覆盖产物导出与倍率隔离 * 更新 README 进度文档
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -61,7 +61,12 @@ public sealed class StatusEndpointTests(FlyshotServerFactory factory) : IClassFi
|
||||
Assert.True(root.GetProperty("isSetup").GetBoolean());
|
||||
Assert.Equal("FANUC_LR_Mate_200iD", root.GetProperty("robotName").GetString());
|
||||
Assert.Equal(6, root.GetProperty("degreesOfFreedom").GetInt32());
|
||||
Assert.Empty(root.GetProperty("uploadedTrajectories").EnumerateArray());
|
||||
var uploadedTrajectories = root.GetProperty("uploadedTrajectories")
|
||||
.EnumerateArray()
|
||||
.Select(static value => value.GetString())
|
||||
.ToArray();
|
||||
Assert.Contains("20251015", uploadedTrajectories);
|
||||
Assert.Contains("UTTC_MS11", uploadedTrajectories);
|
||||
Assert.Equal("Connected", snapshot.GetProperty("connectionState").GetString());
|
||||
Assert.True(snapshot.GetProperty("isEnabled").GetBoolean());
|
||||
Assert.False(snapshot.GetProperty("isInMotion").GetBoolean());
|
||||
|
||||
Reference in New Issue
Block a user