* 新增 ControllerClientCompatOptions.ConfigRoot 及解析方法 * 兼容层默认从运行目录 Config 加载模型、轨迹和配置 * 移除隐式父工作区根目录推断,旧路径仅在显式配置时生效 * Host 项目编译时将 Config 目录复制到输出目录 * 请求响应日志中间件忽略 /api/status/snapshot 高频轮询 * 补充 ConfigRoot 和日志过滤相关单元测试
156 lines
6.7 KiB
C#
156 lines
6.7 KiB
C#
using Flyshot.Core.Config;
|
||
using Flyshot.Core.Domain;
|
||
using Flyshot.Core.Planning;
|
||
using Flyshot.Core.Planning.Export;
|
||
using Flyshot.Core.Planning.Kinematics;
|
||
using Flyshot.Core.Planning.Sampling;
|
||
using Flyshot.Core.Triggering;
|
||
using Xunit.Abstractions;
|
||
|
||
namespace Flyshot.Core.Tests;
|
||
|
||
/// <summary>
|
||
/// 离线轨迹生成入口,与 Python demo 的 CLI 对齐。
|
||
///
|
||
/// 为什么放在测试项目里?
|
||
/// ---
|
||
/// 测试项目已经引用了所有需要的模块(Config、Domain、Planning、Triggering),
|
||
/// 不需要额外创建 Console 项目。通过 dotnet test --filter 可以直接调用,
|
||
/// 也能利用 xUnit 的断言做结果验证。
|
||
/// </summary>
|
||
public sealed class OfflinePlanTests
|
||
{
|
||
/// <summary>
|
||
/// 使用预设参数生成离线轨迹,输出到 analysis/output/dotnet/。
|
||
/// </summary>
|
||
[Theory]
|
||
[InlineData("Rvbust/EOL9 EAU 90/eol9_eau_90.json", "FlyingShot/FlyingShot/Models/LR_Mate_200iD_7L.robot", "EOL9_EAU_90", false, 1.0)]
|
||
[InlineData("Rvbust/EOL9 EAU 90/eol9_eau_90.json", "FlyingShot/FlyingShot/Models/LR_Mate_200iD_7L.robot", "EOL9_EAU_90", true, 0.9)]
|
||
[InlineData("Rvbust/EOL10_EAU_0/EOL10_EAU_0.json", "FlyingShot/FlyingShot/Models/LR_Mate_200iD_7L.robot", "EOL10_EAU_0", false, 1.0)]
|
||
public void GenerateTrajectory_MatchesPythonDemo(
|
||
string configPath,
|
||
string robotModelPath,
|
||
string trajName,
|
||
bool useSelfAdapt,
|
||
double speedRatio)
|
||
{
|
||
var workspaceRoot = GetWorkspaceRoot();
|
||
var resolvedConfigPath = Path.Combine(workspaceRoot, configPath);
|
||
var outputDir = Path.Combine(workspaceRoot, "analysis", "output", "dotnet", $"{trajName}_sr{speedRatio:F2}_{(useSelfAdapt ? "adapt" : "icsp")}");
|
||
Directory.CreateDirectory(outputDir);
|
||
|
||
// 1. 加载配置和模型。
|
||
var loadedConfig = new RobotConfigLoader().Load(resolvedConfigPath, repoRoot: workspaceRoot);
|
||
var program = loadedConfig.Programs[trajName];
|
||
var resolvedRobotModelPath = Path.Combine(workspaceRoot, robotModelPath);
|
||
var baseProfile = new RobotModelLoader().LoadProfile(resolvedRobotModelPath, loadedConfig.Robot.AccLimitScale, loadedConfig.Robot.JerkLimitScale);
|
||
var kinematicsModel = new RobotModelLoader().LoadKinematicsModel(resolvedRobotModelPath);
|
||
|
||
// 2. 应用 speed_ratio 缩放。
|
||
var scaledProfile = ScaleRobotProfile(baseProfile, speedRatio);
|
||
|
||
// 3. 规划轨迹。
|
||
var request = new TrajectoryRequest(
|
||
robot: scaledProfile,
|
||
program: program,
|
||
method: useSelfAdapt ? PlanningMethod.SelfAdaptIcsp : PlanningMethod.Icsp);
|
||
|
||
PlannedTrajectory trajectory;
|
||
if (useSelfAdapt)
|
||
{
|
||
trajectory = new SelfAdaptIcspPlanner().Plan(request, loadedConfig.Robot.AdaptIcspTryNum);
|
||
}
|
||
else
|
||
{
|
||
trajectory = new ICspPlanner().Plan(request);
|
||
}
|
||
|
||
// 4. 生成触发时间轴。
|
||
var timeline = new ShotTimelineBuilder(new WaypointTimestampResolver())
|
||
.Build(trajectory, holdCycles: loadedConfig.Robot.IoKeepCycles, samplePeriod: TimeSpan.FromMilliseconds(16));
|
||
|
||
// 5. 稠密采样。
|
||
double samplePeriod = 0.016;
|
||
var jointDense = TrajectorySampler.SampleJointTrajectory(trajectory, samplePeriod);
|
||
var cartDense = TrajectorySampler.SampleCartesianTrajectory(trajectory, kinematicsModel, samplePeriod);
|
||
|
||
// 6. 导出文件。
|
||
TrajectoryExporter.WriteJointDenseTrajectory(Path.Combine(outputDir, "JointDetialTraj.demo.txt"), jointDense);
|
||
TrajectoryExporter.WriteCartesianDenseTrajectory(Path.Combine(outputDir, "CartDetialTraj.demo.txt"), cartDense);
|
||
TrajectoryExporter.WriteShotEvents(Path.Combine(outputDir, "ShotEvents.demo.json"), timeline.ShotEvents);
|
||
|
||
// 7. 打印统计信息到测试输出。
|
||
_testOutputHelper.WriteLine($"traj_name={trajName}");
|
||
_testOutputHelper.WriteLine($"speed_ratio={speedRatio:F6}");
|
||
_testOutputHelper.WriteLine($"duration={trajectory.WaypointTimes[^1]:F6}");
|
||
_testOutputHelper.WriteLine($"joint_dense_rows={jointDense.Count}");
|
||
_testOutputHelper.WriteLine($"cart_dense_rows={cartDense.Count}");
|
||
_testOutputHelper.WriteLine($"shot_events={timeline.ShotEvents.Count}");
|
||
_testOutputHelper.WriteLine($"output_dir={outputDir}");
|
||
|
||
// 最小验证:输出文件存在且非空。
|
||
Assert.True(File.Exists(Path.Combine(outputDir, "JointDetialTraj.demo.txt")));
|
||
Assert.True(File.Exists(Path.Combine(outputDir, "CartDetialTraj.demo.txt")));
|
||
Assert.True(File.Exists(Path.Combine(outputDir, "ShotEvents.demo.json")));
|
||
Assert.True(jointDense.Count > 0);
|
||
Assert.True(cartDense.Count > 0);
|
||
}
|
||
|
||
private readonly ITestOutputHelper _testOutputHelper;
|
||
|
||
public OfflinePlanTests(ITestOutputHelper testOutputHelper)
|
||
{
|
||
_testOutputHelper = testOutputHelper;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 按 speed_ratio 缩放机器人关节限制。
|
||
///
|
||
/// 缩放律:
|
||
/// - 速度按一次方:speedRatio^1
|
||
/// - 加速度按平方:speedRatio^2
|
||
/// - jerk 按立方:speedRatio^3
|
||
///
|
||
/// 为什么加速度是平方?
|
||
/// ---
|
||
/// 如果把时间轴整体缩放 1/speedRatio 倍,速度按 speedRatio 缩放,加速度按 speedRatio^2 缩放。
|
||
/// 所以降低 speed_ratio 意味着降低速度、更大幅度地降低加速度和 jerk,轨迹更平滑。
|
||
/// </summary>
|
||
private static RobotProfile ScaleRobotProfile(RobotProfile source, double speedRatio)
|
||
{
|
||
return new RobotProfile(
|
||
name: source.Name,
|
||
modelPath: source.ModelPath,
|
||
degreesOfFreedom: source.DegreesOfFreedom,
|
||
jointLimits: source.JointLimits
|
||
.Select(limit => new JointLimit(
|
||
limit.JointName,
|
||
limit.VelocityLimit * speedRatio,
|
||
limit.AccelerationLimit * speedRatio * speedRatio,
|
||
limit.JerkLimit * speedRatio * speedRatio * speedRatio))
|
||
.ToArray(),
|
||
jointCouplings: source.JointCouplings,
|
||
servoPeriod: source.ServoPeriod,
|
||
triggerPeriod: source.TriggerPeriod);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 定位父工作区根目录。
|
||
/// </summary>
|
||
private static string GetWorkspaceRoot()
|
||
{
|
||
var current = new DirectoryInfo(AppContext.BaseDirectory);
|
||
while (current is not null)
|
||
{
|
||
if (File.Exists(Path.Combine(current.FullName, "FlyshotReplacement.sln")))
|
||
{
|
||
return Path.GetFullPath(Path.Combine(current.FullName, ".."));
|
||
}
|
||
|
||
current = current.Parent;
|
||
}
|
||
|
||
throw new DirectoryNotFoundException("Unable to locate the flyshot workspace root.");
|
||
}
|
||
}
|