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;
///
/// 离线轨迹生成入口,与 Python demo 的 CLI 对齐。
///
/// 为什么放在测试项目里?
/// ---
/// 测试项目已经引用了所有需要的模块(Config、Domain、Planning、Triggering),
/// 不需要额外创建 Console 项目。通过 dotnet test --filter 可以直接调用,
/// 也能利用 xUnit 的断言做结果验证。
///
public sealed class OfflinePlanTests
{
///
/// 使用预设参数生成离线轨迹,输出到 analysis/output/dotnet/。
///
[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 outputDir = Path.Combine(workspaceRoot, "analysis", "output", "dotnet", $"{trajName}_sr{speedRatio:F2}_{(useSelfAdapt ? "adapt" : "icsp")}");
Directory.CreateDirectory(outputDir);
// 1. 加载配置和模型。
var loadedConfig = new RobotConfigLoader().Load(configPath, 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;
}
///
/// 按 speed_ratio 缩放机器人关节限制。
///
/// 缩放律:
/// - 速度按一次方:speedRatio^1
/// - 加速度按平方:speedRatio^2
/// - jerk 按立方:speedRatio^3
///
/// 为什么加速度是平方?
/// ---
/// 如果把时间轴整体缩放 1/speedRatio 倍,速度按 speedRatio 缩放,加速度按 speedRatio^2 缩放。
/// 所以降低 speed_ratio 意味着降低速度、更大幅度地降低加速度和 jerk,轨迹更平滑。
///
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);
}
///
/// 定位父工作区根目录。
///
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.");
}
}