feat(*): 添加 J519 实发重采样与 JSON 机型模型

* 新增 J519 实发采样器,按 8ms 周期生成 timing/jerk 诊断行并完成 rad->deg 转换
* 兼容层产物导出补充 speedRatio,规划编排补齐 smoothStartStopTiming 与日志透传
* 配置与机型加载切换到运行目录 JSON 模型,并补齐 7L 展开模型与相关单元测试
This commit is contained in:
2026-05-07 17:08:32 +08:00
parent 70b0ccd414
commit c6829d214a
26 changed files with 1417 additions and 409 deletions

View File

@@ -1,10 +1,9 @@
using Flyshot.Core.Config;
using Flyshot.Core.Domain;
namespace Flyshot.Core.Tests;
/// <summary>
/// 锁定 Task 3 的兼容输入行为,确保旧配置、.robot 元数据和路径策略都能被稳定加载。
/// 锁定 Task 3 的兼容输入行为,确保旧配置、JSON 模型元数据和路径策略都能被稳定加载。
/// </summary>
public sealed class ConfigCompatibilityTests
{
@@ -25,6 +24,7 @@ public sealed class ConfigCompatibilityTests
Assert.Equal(1.0, loaded.Robot.AccLimitScale);
Assert.Equal(1.0, loaded.Robot.JerkLimitScale);
Assert.Equal(1.0, loaded.Robot.PlanningSpeedScale);
Assert.True(loaded.Robot.SmoothStartStopTiming);
Assert.Equal(5, loaded.Robot.AdaptIcspTryNum);
var program = Assert.Contains("EOL10_EAU_0", loaded.Programs);
@@ -73,6 +73,7 @@ public sealed class ConfigCompatibilityTests
Assert.Equal(0.5, loaded.Robot.AccLimitScale);
Assert.Equal(0.25, loaded.Robot.JerkLimitScale);
Assert.Equal(1.0, loaded.Robot.PlanningSpeedScale);
Assert.True(loaded.Robot.SmoothStartStopTiming);
Assert.Equal([0, 0, 0], program.OffsetValues);
Assert.All(program.AddressGroups, group => Assert.Empty(group.Addresses));
}
@@ -123,13 +124,53 @@ public sealed class ConfigCompatibilityTests
}
/// <summary>
/// 验证 .robot 解析会保留 Joint3 对 Joint2 的 couple 元数据,并构造规划侧可直接消费的 RobotProfile
/// 验证 RobotConfig.json 可以关闭飞拍执行前的二次平滑起停时间重映射
/// </summary>
[Fact]
public void RobotConfigLoader_LoadsSmoothStartStopTimingSwitch()
{
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,
"smooth_start_stop_timing": false
},
"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.False(loaded.Robot.SmoothStartStopTiming);
}
finally
{
Directory.Delete(tempRoot, recursive: true);
}
}
/// <summary>
/// 验证 JSON 模型解析会保留 Joint3 对 Joint2 的 couple 元数据,并构造规划侧可直接消费的 RobotProfile。
/// </summary>
[Fact]
public void RobotModelLoader_LoadsRobotProfile_WithJointLimitsAndCoupling()
{
var workspaceRoot = GetWorkspaceRoot();
var modelPath = Path.Combine(workspaceRoot, "FlyingShot", "FlyingShot", "Models", "LR_Mate_200iD_7L.robot");
var replacementRoot = GetReplacementRoot();
var modelPath = Path.Combine(replacementRoot, "Config", "LR_Mate_200iD_7L.json");
var profile = new RobotModelLoader().LoadProfile(modelPath);
@@ -147,13 +188,13 @@ public sealed class ConfigCompatibilityTests
}
/// <summary>
/// 验证 RobotConfig 中的 acc_limit 和 jerk_limit 乘子会正确叠加到模型关节限制上。
/// 验证 RobotConfig 中的 acc_limit 和 jerk_limit 乘子会正确叠加到 JSON 模型关节限制上。
/// </summary>
[Fact]
public void RobotModelLoader_AppliesAccelerationAndJerkScales()
{
var workspaceRoot = GetWorkspaceRoot();
var modelPath = Path.Combine(workspaceRoot, "FlyingShot", "FlyingShot", "Models", "LR_Mate_200iD_7L.robot");
var replacementRoot = GetReplacementRoot();
var modelPath = Path.Combine(replacementRoot, "Config", "LR_Mate_200iD_7L.json");
var profile = new RobotModelLoader().LoadProfile(modelPath, accLimitScale: 0.5, jerkLimitScale: 0.25);
@@ -161,6 +202,28 @@ public sealed class ConfigCompatibilityTests
Assert.Equal(62.115, profile.JointLimits[2].JerkLimit, precision: 3);
}
/// <summary>
/// 验证 JSON 模型可一次解析后同时生成规划约束视图和运动学几何视图。
/// </summary>
[Fact]
public void RobotModelLoader_LoadsProfileAndKinematics_FromSingleParse()
{
var replacementRoot = GetReplacementRoot();
var modelPath = Path.Combine(replacementRoot, "Config", "LR_Mate_200iD_7L.json");
var loaded = new RobotModelLoader().LoadProfileAndKinematics(modelPath, accLimitScale: 0.5, jerkLimitScale: 0.25);
Assert.Equal("FANUC_LR_Mate_200iD_7L", loaded.Profile.Name);
Assert.Equal(modelPath, loaded.Profile.ModelPath);
Assert.Equal(6, loaded.Profile.DegreesOfFreedom);
Assert.Equal(14.905, loaded.Profile.JointLimits[2].AccelerationLimit, precision: 3);
Assert.Equal(62.115, loaded.Profile.JointLimits[2].JerkLimit, precision: 3);
Assert.Equal("FANUC_LR_Mate_200iD_7L", loaded.KinematicsModel.Name);
Assert.True(loaded.KinematicsModel.Joints.Count >= loaded.Profile.DegreesOfFreedom);
Assert.Contains(loaded.KinematicsModel.Joints, static joint => joint.Name == "Joint3" && joint.CoupleMaster == "Joint2");
}
/// <summary>
/// 验证路径兼容层只从当前服务配置目录解析相对配置,并按平台策略生成默认用户数据目录。
/// </summary>

View File

@@ -34,7 +34,7 @@ public sealed class ControllerClientCompatConfigRootTests
}
/// <summary>
/// 验证机器人目录优先从显式 ConfigRoot/Models 加载 .robot 文件。
/// 验证机器人目录优先从显式 ConfigRoot/Models 加载现场 JSON 模型文件。
/// </summary>
[Fact]
public void ControllerClientCompatRobotCatalog_LoadsModelFromConfigRootModels()
@@ -48,7 +48,7 @@ public sealed class ControllerClientCompatConfigRootTests
var profile = catalog.LoadProfile("FANUC_LR_Mate_200iD");
Assert.Equal(Path.Combine(configRoot, "Models", "LR_Mate_200iD_7L.robot"), profile.ModelPath);
Assert.Equal(Path.Combine(configRoot, "Models", "LR_Mate_200iD_7L.json"), profile.ModelPath);
}
finally
{
@@ -117,15 +117,15 @@ public sealed class ControllerClientCompatConfigRootTests
}
/// <summary>
/// 复制仓库内已固化的现场机器人模型到临时 Config/Models 目录。
/// 复制仓库内已固化的现场机器人 JSON 模型到临时 Config/Models 目录。
/// </summary>
private static void CopySampleRobotModel(string configRoot)
{
var modelDir = Path.Combine(configRoot, "Models");
Directory.CreateDirectory(modelDir);
File.Copy(
Path.Combine(GetReplacementRoot(), "Config", "Models", "LR_Mate_200iD_7L.robot"),
Path.Combine(modelDir, "LR_Mate_200iD_7L.robot"));
Path.Combine(GetReplacementRoot(), "Config", "Models", "LR_Mate_200iD_7L.json"),
Path.Combine(modelDir, "LR_Mate_200iD_7L.json"));
}
/// <summary>

View File

@@ -237,9 +237,9 @@ public sealed class FanucControllerRuntimeDenseTests
var bundle = orchestrator.PlanUploadedFlyshot(
fixture.Robot,
fixture.Uploaded,
settings: fixture.Settings,
planningSpeedScale: 1.0);
var outputRoot = Path.Combine(AppContext.BaseDirectory, "Config", "Data", bundle.Result.ProgramName);
settings: fixture.Settings);
var result = WithUniqueProgramName(bundle.Result, $"UTTC_MS11_legacyfit_{Guid.NewGuid():N}");
var outputRoot = Path.Combine(AppContext.BaseDirectory, "Config", "Data", result.ProgramName);
var denseSendRoot = Path.Combine(outputRoot, "DenseSend");
var beforeRunDirectories = Directory.Exists(denseSendRoot)
? Directory.GetDirectories(denseSendRoot).ToHashSet(StringComparer.OrdinalIgnoreCase)
@@ -249,7 +249,7 @@ public sealed class FanucControllerRuntimeDenseTests
j519Client.EnableCommandHistoryForTests();
ForceRealModeEnabled(runtime, speedRatio: 1.0);
runtime.ExecuteTrajectory(bundle.Result, bundle.Result.DenseJointTrajectory![0].Skip(1).ToArray());
runtime.ExecuteTrajectory(result, result.DenseJointTrajectory![0].Skip(1).ToArray());
WaitUntilIdle(runtime);
var runDirectory = GetNewDenseSendRunDirectory(outputRoot, beforeRunDirectories);
@@ -269,6 +269,8 @@ public sealed class FanucControllerRuntimeDenseTests
Assert.NotEmpty(pointsLines);
Assert.NotEmpty(timingLines);
Assert.NotEmpty(jerkLines);
Assert.Equal(927, pointsLines.Length);
Assert.Equal(927, timingLines.Length);
var firstPoint = ParseColumns(pointsLines[0]);
var secondPoint = ParseColumns(pointsLines[1]);
@@ -282,7 +284,7 @@ public sealed class FanucControllerRuntimeDenseTests
const double windowHalfWidth = 0.024;
var summaryLines = new List<string>
{
$"program={bundle.Result.ProgramName}",
$"program={result.ProgramName}",
$"send_time_target_seconds={targetSendTime:F6}",
$"window_half_width_seconds={windowHalfWidth:F6}",
$"points_path={pointsPath}",
@@ -1121,16 +1123,8 @@ public sealed class FanucControllerRuntimeDenseTests
/// </summary>
private static UttcMs11RuntimeFixture LoadUttcMs11RuntimeFixture()
{
var configPath = Path.Combine(
TestRobotFactory.GetReplacementRoot(),
"src",
"Flyshot.Server.Host",
"bin",
"Debug",
"net8.0",
"Config",
"RobotConfig.json");
var configRoot = Path.GetDirectoryName(configPath)!;
var configRoot = TestRobotFactory.GetConfigRoot();
var configPath = Path.Combine(configRoot, "RobotConfig.json");
var loaded = new RobotConfigLoader().Load(configPath, configRoot);
var program = loaded.Programs["UTTC_MS11"];
var uploaded = new ControllerClientCompatUploadedTrajectory(
@@ -1149,6 +1143,26 @@ public sealed class FanucControllerRuntimeDenseTests
return new UttcMs11RuntimeFixture(configRoot, loaded.Robot, uploaded, robot);
}
/// <summary>
/// 复制一份结果并替换程序名,让 DenseSend 调试文件写入唯一目录,避免测试之间复用旧目录权限。
/// </summary>
private static TrajectoryResult WithUniqueProgramName(TrajectoryResult result, string programName)
{
return new TrajectoryResult(
programName: programName,
method: result.Method,
isValid: result.IsValid,
duration: result.Duration,
shotEvents: result.ShotEvents,
triggerTimeline: result.TriggerTimeline,
artifacts: result.Artifacts,
failureReason: result.FailureReason,
usedCache: result.UsedCache,
originalWaypointCount: result.OriginalWaypointCount,
plannedWaypointCount: result.PlannedWaypointCount,
denseJointTrajectory: result.DenseJointTrajectory);
}
/// <summary>
/// 解析空格分隔的纯文本数值列。
/// </summary>

View File

@@ -0,0 +1,99 @@
using Flyshot.Core.Planning.Sampling;
namespace Flyshot.Core.Tests;
/// <summary>
/// 验证 J519 实发重采样器在离线导出和运行时下发之间保持一致的时间轴语义。
/// </summary>
public sealed class J519SendTrajectorySamplerTests
{
/// <summary>
/// 验证 speed_ratio 只缩放轨迹时间,物理发送时间仍按固定伺服周期推进。
/// </summary>
[Fact]
public void SampleDenseJointTrajectory_MapsSendTimeToScaledTrajectoryTimeAndDegrees()
{
var denseTrajectory = new[]
{
new[] { 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 },
new[] { 0.008, Math.PI / 2.0, 0.0, 0.0, 0.0, 0.0, 0.0 },
new[] { 0.016, Math.PI, 0.0, 0.0, 0.0, 0.0, 0.0 }
};
var samples = J519SendTrajectorySampler.SampleDenseJointTrajectory(
denseTrajectory,
durationSeconds: 0.016,
servoPeriodSeconds: 0.008,
speedRatio: 0.5);
Assert.Equal(5, samples.Count);
Assert.Equal(0, samples[0].SampleIndex);
Assert.Equal(0.0, samples[0].SendTime, precision: 6);
Assert.Equal(0.0, samples[0].TrajectoryTime, precision: 6);
Assert.Equal(0.0, samples[0].JointsDegrees[0], precision: 6);
Assert.Equal(1, samples[1].SampleIndex);
Assert.Equal(0.008, samples[1].SendTime, precision: 6);
Assert.Equal(0.004, samples[1].TrajectoryTime, precision: 6);
Assert.Equal(45.0, samples[1].JointsDegrees[0], precision: 6);
Assert.Equal(4, samples[^1].SampleIndex);
Assert.Equal(0.032, samples[^1].SendTime, precision: 6);
Assert.Equal(0.016, samples[^1].TrajectoryTime, precision: 6);
Assert.Equal(180.0, samples[^1].JointsDegrees[0], precision: 6);
}
/// <summary>
/// 验证空稠密轨迹会直接暴露为调用错误,避免生成无意义下发点。
/// </summary>
[Fact]
public void SampleDenseJointTrajectory_RejectsEmptyDenseTrajectory()
{
var exception = Assert.Throws<InvalidOperationException>(() =>
J519SendTrajectorySampler.SampleDenseJointTrajectory(
Array.Empty<IReadOnlyList<double>>(),
durationSeconds: 0.016,
servoPeriodSeconds: 0.008,
speedRatio: 1.0));
Assert.Contains("稠密关节轨迹为空", exception.Message);
}
/// <summary>
/// 验证非法 speed_ratio 会在公共入口统一拦截。
/// </summary>
[Theory]
[InlineData(0.0)]
[InlineData(double.NaN)]
[InlineData(double.PositiveInfinity)]
public void SampleDenseJointTrajectory_RejectsInvalidSpeedRatio(double speedRatio)
{
var denseTrajectory = new[]
{
new[] { 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 }
};
Assert.Throws<ArgumentOutOfRangeException>(() =>
J519SendTrajectorySampler.SampleDenseJointTrajectory(
denseTrajectory,
durationSeconds: 0.0,
servoPeriodSeconds: 0.008,
speedRatio: speedRatio));
}
/// <summary>
/// 验证公共诊断行格式与既有 ActualSendTiming 文件保持一致。
/// </summary>
[Fact]
public void BuildTimingRow_UsesLegacyActualSendColumnOrder()
{
var row = J519SendTrajectorySampler.BuildTimingRow(new J519SendSample(
sampleIndex: 2,
sendTime: 0.016,
trajectoryTime: 0.008,
speedRatio: 0.5,
jointsDegrees: [90.0, 0.0, 0.0, 0.0, 0.0, 0.0]));
Assert.Equal([2.0, 0.016, 0.008, 0.5], row);
}
}

View File

@@ -2,7 +2,6 @@ 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;
@@ -24,12 +23,11 @@ public sealed class OfflinePlanTests
/// 使用预设参数生成离线轨迹,输出到 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)]
[InlineData("Rvbust/EOL9 EAU 90/eol9_eau_90.json", "EOL9_EAU_90", false, 1.0)]
[InlineData("Rvbust/EOL9 EAU 90/eol9_eau_90.json", "EOL9_EAU_90", true, 0.9)]
[InlineData("Rvbust/EOL10_EAU_0/EOL10_EAU_0.json", "EOL10_EAU_0", false, 1.0)]
public void GenerateTrajectory_MatchesPythonDemo(
string configPath,
string robotModelPath,
string trajName,
bool useSelfAdapt,
double speedRatio)
@@ -42,9 +40,13 @@ public sealed class OfflinePlanTests
// 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);
var resolvedRobotModelPath = Path.Combine(workspaceRoot, "flyshot-replacement", "Config", "LR_Mate_200iD_7L.json");
var loadedModel = new RobotModelLoader().LoadProfileAndKinematics(
resolvedRobotModelPath,
loadedConfig.Robot.AccLimitScale,
loadedConfig.Robot.JerkLimitScale);
var baseProfile = loadedModel.Profile;
var kinematicsModel = loadedModel.KinematicsModel;
// 2. 应用 speed_ratio 缩放。
var scaledProfile = ScaleRobotProfile(baseProfile, speedRatio);

View File

@@ -66,7 +66,7 @@ public sealed class PlanningCompatibilityTests
{
var workspaceRoot = GetWorkspaceRoot();
var configPath = Path.Combine(workspaceRoot, "Rvbust", "EOL9 EAU 90", "eol9_eau_90.json");
var modelPath = Path.Combine(workspaceRoot, "FlyingShot", "FlyingShot", "Models", "LR_Mate_200iD_7L.robot");
var modelPath = Path.Combine(workspaceRoot, "flyshot-replacement", "Config", "LR_Mate_200iD_7L.json");
var config = new RobotConfigLoader().Load(configPath);
var baseProfile = new RobotModelLoader().LoadProfile(modelPath, config.Robot.AccLimitScale, config.Robot.JerkLimitScale);

View File

@@ -287,7 +287,7 @@ public sealed class RuntimeOrchestrationTests
var bundle = orchestrator.PlanUploadedFlyshot(
fixture.Robot,
fixture.Uploaded,
settings: fixture.Settings,
settings: EnableSmoothStartStopTiming(fixture.Settings),
planningSpeedScale: 1.0);
Assert.Equal("UTTC_MS11", bundle.Result.ProgramName);
@@ -304,6 +304,45 @@ public sealed class RuntimeOrchestrationTests
AssertJointRadiansEqual(fixture.Uploaded.Waypoints[^1], bundle.Result.DenseJointTrajectory[^1].Skip(1).ToArray());
}
/// <summary>
/// 验证现场 UTTC_MS11 配置使用通用规划速度倍率拉长时间轴,并关闭二次平滑时间重映射。
/// </summary>
[Fact]
public void ControllerClientTrajectoryOrchestrator_PlanUploadedFlyshot_FromRuntimeLegacyFitConfig_UsesPlanningScaleWithoutSecondRemap()
{
var fixture = LoadUttcMs11RuntimeFixture();
var orchestrator = new ControllerClientTrajectoryOrchestrator();
var bundle = orchestrator.PlanUploadedFlyshot(
fixture.Robot,
fixture.Uploaded,
settings: fixture.Settings);
var baselineBundle = orchestrator.PlanUploadedFlyshot(
fixture.Robot,
fixture.Uploaded,
settings: EnableSmoothStartStopTiming(fixture.Settings),
planningSpeedScale: 1.0);
var rawDense = TrajectorySampler.SampleJointTrajectory(
bundle.PlannedTrajectory,
samplePeriod: fixture.Robot.ServoPeriod.TotalSeconds);
Assert.Equal(0.7422771653721995, fixture.Settings.PlanningSpeedScale, precision: 12);
Assert.False(fixture.Settings.SmoothStartStopTiming);
Assert.Equal(fixture.Uploaded.Waypoints.Count, bundle.PlannedTrajectory.WaypointTimes.Count);
Assert.Equal(
baselineBundle.PlannedTrajectory.WaypointTimes[^1] / fixture.Settings.PlanningSpeedScale,
bundle.PlannedTrajectory.WaypointTimes[^1],
precision: 6);
Assert.Equal(7.403046, bundle.PlannedTrajectory.WaypointTimes[^1], precision: 3);
// 关闭二次时间重映射时,运行时稠密点应直接使用规划样条采样,避免再次改变通用规划时间轴。
Assert.Equal(rawDense.Count, bundle.Result.DenseJointTrajectory!.Count);
Assert.Equal(rawDense[1][1], bundle.Result.DenseJointTrajectory[1][1], precision: 12);
Assert.True(
Math.Abs(bundle.PlannedTrajectory.WaypointTimes[^1] - bundle.Result.Duration.TotalSeconds) < 1e-6,
$"执行时长应保留规划终点时间planned={bundle.PlannedTrajectory.WaypointTimes[^1]}, result={bundle.Result.Duration.TotalSeconds}");
}
/// <summary>
/// 使用运行时 RobotConfig.json 中的真实 UTTC_MS11 示教点,导出首尾 10 点整形前后的逐点对比数据,
/// 便于和现场报警时间段逐项核对关节、速度、加速度与跃度。
@@ -347,6 +386,7 @@ public sealed class RuntimeOrchestrationTests
{
$"program=UTTC_MS11",
$"planning_speed_scale=1.000000",
$"smooth_start_stop_timing=true",
$"servo_period_seconds={fixture.Robot.ServoPeriod.TotalSeconds.ToString("F6", CultureInfo.InvariantCulture)}",
$"dense_point_count={shapedDense.Count}",
$"leading_first_step_raw_deg={ComputeStepDegrees(rawDense, 1, 0).ToString("F6", CultureInfo.InvariantCulture)}",
@@ -380,7 +420,7 @@ public sealed class RuntimeOrchestrationTests
var bundle = orchestrator.PlanUploadedFlyshot(
fixture.Robot,
fixture.Uploaded,
settings: fixture.Settings,
settings: EnableSmoothStartStopTiming(fixture.Settings),
planningSpeedScale: 1.0);
var rawDense = TrajectorySampler.SampleJointTrajectory(
bundle.PlannedTrajectory,
@@ -412,7 +452,7 @@ public sealed class RuntimeOrchestrationTests
var bundle = orchestrator.PlanUploadedFlyshot(
fixture.Robot,
fixture.Uploaded,
settings: fixture.Settings,
settings: EnableSmoothStartStopTiming(fixture.Settings),
planningSpeedScale: 1.0);
var rawDense = TrajectorySampler.SampleJointTrajectory(
bundle.PlannedTrajectory,
@@ -874,7 +914,55 @@ public sealed class RuntimeOrchestrationTests
}
/// <summary>
/// 创建只包含当前支持机器人模型和 RobotConfig.json 的临时运行配置根
/// 验证 SaveTrajectoryInfo 会同时导出按 J519 8ms 实发周期重采样的点位,并应用当前 speed_ratio
/// </summary>
[Fact]
public void ControllerClientCompatService_SaveTrajectoryInfo_ExportsActualSendRowsWithSpeedRatio()
{
var configRoot = CreateTempConfigRoot();
try
{
WriteRobotConfigWithDemoTrajectory(configRoot);
var options = new ControllerClientCompatOptions { ConfigRoot = configRoot };
var runtime = new RecordingControllerRuntime();
var service = new ControllerClientCompatService(
options,
new ControllerClientCompatRobotCatalog(options, new RobotModelLoader()),
runtime,
new ControllerClientTrajectoryOrchestrator(),
new RobotConfigLoader());
service.SetUpRobot("FANUC_LR_Mate_200iD");
runtime.SetSpeedRatio(0.5);
service.SaveTrajectoryInfo("demo-flyshot");
var outputDir = Path.Combine(configRoot, "Data", "demo-flyshot");
var pointsPath = Path.Combine(outputDir, "ActualSendJointTraj.txt");
var timingPath = Path.Combine(outputDir, "ActualSendTiming.txt");
Assert.True(File.Exists(pointsPath));
Assert.True(File.Exists(timingPath));
var pointRows = File.ReadAllLines(pointsPath).Select(ParseSpaceSeparatedDoubles).ToArray();
var timingRows = File.ReadAllLines(timingPath).Select(ParseSpaceSeparatedDoubles).ToArray();
var duration = double.Parse(File.ReadLines(Path.Combine(outputDir, "JointTraj.txt")).Last().Split(' ')[0], CultureInfo.InvariantCulture);
var expectedRows = (int)Math.Ceiling(Math.Max(0.0, (duration / (0.008 * 0.5)) - 1e-9)) + 1;
Assert.Equal(expectedRows, pointRows.Length);
Assert.Equal(expectedRows, timingRows.Length);
Assert.Equal(0.0, pointRows[0][0], precision: 6);
Assert.Equal(0.008, pointRows[1][0], precision: 6);
Assert.Equal(0.004, timingRows[1][2], precision: 6);
Assert.Equal(0.5, timingRows[1][3], precision: 6);
}
finally
{
Directory.Delete(configRoot, recursive: true);
}
}
/// <summary>
/// 创建只包含当前支持机器人 JSON 模型和 RobotConfig.json 的临时运行配置根。
/// </summary>
private static string CreateTempConfigRoot()
{
@@ -886,8 +974,8 @@ public sealed class RuntimeOrchestrationTests
TestRobotFactory.GetReplacementRoot(),
"Config",
"Models",
"LR_Mate_200iD_7L.robot");
File.Copy(sourceModel, Path.Combine(modelDir, "LR_Mate_200iD_7L.robot"));
"LR_Mate_200iD_7L.json");
File.Copy(sourceModel, Path.Combine(modelDir, "LR_Mate_200iD_7L.json"));
return configRoot;
}
@@ -960,16 +1048,8 @@ public sealed class RuntimeOrchestrationTests
/// </summary>
private static UttcMs11RuntimeFixture LoadUttcMs11RuntimeFixture()
{
var configPath = Path.Combine(
TestRobotFactory.GetReplacementRoot(),
"src",
"Flyshot.Server.Host",
"bin",
"Debug",
"net8.0",
"Config",
"RobotConfig.json");
var configRoot = Path.GetDirectoryName(configPath)!;
var configRoot = TestRobotFactory.GetConfigRoot();
var configPath = Path.Combine(configRoot, "RobotConfig.json");
var loaded = new RobotConfigLoader().Load(configPath, configRoot);
var program = loaded.Programs["UTTC_MS11"];
var uploaded = new ControllerClientCompatUploadedTrajectory(
@@ -988,6 +1068,22 @@ public sealed class RuntimeOrchestrationTests
return new UttcMs11RuntimeFixture(configRoot, loaded.Robot, uploaded, robot);
}
/// <summary>
/// 为首尾平滑对比测试显式打开二次时间重映射,避免受现场 legacy-fit 配置影响。
/// </summary>
private static CompatibilityRobotSettings EnableSmoothStartStopTiming(CompatibilityRobotSettings settings)
{
return new CompatibilityRobotSettings(
useDo: settings.UseDo,
ioAddresses: settings.IoAddresses,
ioKeepCycles: settings.IoKeepCycles,
accLimitScale: settings.AccLimitScale,
jerkLimitScale: settings.JerkLimitScale,
adaptIcspTryNum: settings.AdaptIcspTryNum,
planningSpeedScale: settings.PlanningSpeedScale,
smoothStartStopTiming: true);
}
/// <summary>
/// 把指定窗口内的整形前后逐点数据导出为 CSV包含关节、步长、速度、加速度与跃度。
/// </summary>
@@ -1412,6 +1508,8 @@ internal static class TestRobotFactory
/// </summary>
internal sealed class RecordingControllerRuntime : IControllerRuntime
{
private double _speedRatio = 1.0;
/// <summary>
/// 获取最近一次 ResetRobot 收到的机器人配置。
/// </summary>
@@ -1454,11 +1552,12 @@ internal sealed class RecordingControllerRuntime : IControllerRuntime
}
/// <inheritdoc />
public double GetSpeedRatio() => 1.0;
public double GetSpeedRatio() => _speedRatio;
/// <inheritdoc />
public void SetSpeedRatio(double ratio)
{
_speedRatio = ratio;
}
/// <inheritdoc />
@@ -1491,7 +1590,7 @@ internal sealed class RecordingControllerRuntime : IControllerRuntime
connectionState: "Connected",
isEnabled: true,
isInMotion: false,
speedRatio: 1.0,
speedRatio: _speedRatio,
jointPositions: Array.Empty<double>(),
cartesianPose: Array.Empty<double>(),
activeAlarms: Array.Empty<RuntimeAlarm>());