* 为 RobotConfig 增加 trigger_sample_index_offset_cycles 配置 * 让 DO 事件携带示教点关节角并按最接近 sample 绑定触发 * 调整运行时 IO 地址位掩码映射并补充 ShotEvents 导出 * 新增 2026042802-1 抓包分析脚本、数据产物与结论文档 * 补齐配置兼容、规划绑定和运行时触发相关测试
379 lines
14 KiB
C#
379 lines
14 KiB
C#
using Flyshot.Core.Config;
|
||
|
||
namespace Flyshot.Core.Tests;
|
||
|
||
/// <summary>
|
||
/// 锁定 Task 3 的兼容输入行为,确保旧配置、JSON 模型元数据和路径策略都能被稳定加载。
|
||
/// </summary>
|
||
public sealed class ConfigCompatibilityTests
|
||
{
|
||
/// <summary>
|
||
/// 验证现有 RobotConfig.json 能被加载,并保持关键机器人参数与飞拍程序内容不变。
|
||
/// </summary>
|
||
[Fact]
|
||
public void RobotConfigLoader_LoadsLegacyRobotConfig_AndPreservesPrograms()
|
||
{
|
||
var workspaceRoot = GetWorkspaceRoot();
|
||
var configPath = Path.Combine(workspaceRoot, "Rvbust", "EOL10_EAU_0", "RobotConfig.json");
|
||
|
||
var loaded = new RobotConfigLoader().Load(configPath);
|
||
|
||
Assert.True(loaded.Robot.UseDo);
|
||
Assert.Equal([7, 8], loaded.Robot.IoAddresses);
|
||
Assert.Equal(2, loaded.Robot.IoKeepCycles);
|
||
Assert.Equal(7, loaded.Robot.TriggerSampleIndexOffsetCycles);
|
||
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);
|
||
Assert.Equal("EOL10_EAU_0", program.Name);
|
||
Assert.Equal(45, program.Waypoints.Count);
|
||
Assert.Equal(42, program.ShotWaypointCount);
|
||
Assert.Empty(program.AddressGroups[0].Addresses);
|
||
Assert.Equal([4, 3], program.AddressGroups[1].Addresses);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 验证旧配置缺少 offset_values 和 addr 字段时,会自动回填与旧系统一致的默认值。
|
||
/// </summary>
|
||
[Fact]
|
||
public void RobotConfigLoader_FillsLegacyDefaults_WhenOptionalFieldsAreMissing()
|
||
{
|
||
var tempRoot = CreateTempDirectory();
|
||
try
|
||
{
|
||
var configPath = Path.Combine(tempRoot, "legacy.json");
|
||
File.WriteAllText(
|
||
configPath,
|
||
"""
|
||
{
|
||
"robot": {
|
||
"use_do": false,
|
||
"io_keep_cycles": 3,
|
||
"acc_limit": 0.5,
|
||
"jerk_limit": 0.25
|
||
},
|
||
"flying_shots": {
|
||
"demo": {
|
||
"traj_waypoints": [[0, 1], [2, 3], [4, 5]],
|
||
"shot_flags": [0, 1, 0]
|
||
}
|
||
}
|
||
}
|
||
""");
|
||
|
||
var loaded = new RobotConfigLoader().Load(configPath);
|
||
var program = Assert.Contains("demo", loaded.Programs);
|
||
|
||
Assert.False(loaded.Robot.UseDo);
|
||
Assert.Empty(loaded.Robot.IoAddresses);
|
||
Assert.Equal(3, loaded.Robot.IoKeepCycles);
|
||
Assert.Equal(7, loaded.Robot.TriggerSampleIndexOffsetCycles);
|
||
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));
|
||
}
|
||
finally
|
||
{
|
||
Directory.Delete(tempRoot, recursive: true);
|
||
}
|
||
}
|
||
|
||
/// <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>
|
||
/// 验证 RobotConfig.json 可以显式配置触发绑定后的命令 sample 后移周期数。
|
||
/// </summary>
|
||
[Fact]
|
||
public void RobotConfigLoader_LoadsTriggerSampleIndexOffsetCycles()
|
||
{
|
||
var tempRoot = CreateTempDirectory();
|
||
try
|
||
{
|
||
var configPath = Path.Combine(tempRoot, "legacy.json");
|
||
File.WriteAllText(
|
||
configPath,
|
||
"""
|
||
{
|
||
"robot": {
|
||
"use_do": true,
|
||
"io_keep_cycles": 2,
|
||
"trigger_sample_index_offset_cycles": 8,
|
||
"acc_limit": 1.0,
|
||
"jerk_limit": 1.0
|
||
},
|
||
"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(8, loaded.Robot.TriggerSampleIndexOffsetCycles);
|
||
}
|
||
finally
|
||
{
|
||
Directory.Delete(tempRoot, recursive: true);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 验证 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 replacementRoot = GetReplacementRoot();
|
||
var modelPath = Path.Combine(replacementRoot, "Config", "LR_Mate_200iD_7L.json");
|
||
|
||
var profile = new RobotModelLoader().LoadProfile(modelPath);
|
||
|
||
Assert.Equal("FANUC_LR_Mate_200iD_7L", profile.Name);
|
||
Assert.Equal(modelPath, profile.ModelPath);
|
||
Assert.Equal(6, profile.DegreesOfFreedom);
|
||
Assert.Equal(6.45, profile.JointLimits[0].VelocityLimit, precision: 2);
|
||
Assert.Equal(29.81, profile.JointLimits[2].AccelerationLimit, precision: 2);
|
||
|
||
var coupling = Assert.Single(profile.JointCouplings);
|
||
Assert.Equal("Joint3", coupling.SlaveJointName);
|
||
Assert.Equal("Joint2", coupling.MasterJointName);
|
||
Assert.Equal(1.0, coupling.Multiplier);
|
||
Assert.Equal(0.0, coupling.Offset);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 验证 RobotConfig 中的 acc_limit 和 jerk_limit 乘子会正确叠加到 JSON 模型关节限制上。
|
||
/// </summary>
|
||
[Fact]
|
||
public void RobotModelLoader_AppliesAccelerationAndJerkScales()
|
||
{
|
||
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);
|
||
|
||
Assert.Equal(14.905, profile.JointLimits[2].AccelerationLimit, precision: 3);
|
||
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>
|
||
[Fact]
|
||
public void PathCompatibility_ResolvesConfigDirectoryOnly_AndBuildsUserDataRoots()
|
||
{
|
||
var tempRoot = CreateTempDirectory();
|
||
try
|
||
{
|
||
var configPath = Path.Combine(tempRoot, "Config", "sample.json");
|
||
Directory.CreateDirectory(Path.GetDirectoryName(configPath)!);
|
||
File.WriteAllText(configPath, "{}");
|
||
|
||
var resolved = PathCompatibility.ResolveConfigPath("sample.json", tempRoot);
|
||
|
||
Assert.Equal(configPath, resolved);
|
||
Assert.Equal("/home/tester/.Rvbust/Data", PathCompatibility.BuildUserDataRoot("/home/tester", CompatibilityPathStyle.Posix));
|
||
Assert.Equal(@"C:\Users\tester\.Rvbust\Data", PathCompatibility.BuildUserDataRoot(@"C:\Users\tester", CompatibilityPathStyle.Windows));
|
||
}
|
||
finally
|
||
{
|
||
Directory.Delete(tempRoot, recursive: true);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 验证旧父工作区候选路径存在时也不会被相对配置解析隐式命中。
|
||
/// </summary>
|
||
[Fact]
|
||
public void PathCompatibility_DoesNotResolveLegacyWorkspaceFallbacks()
|
||
{
|
||
var tempRoot = CreateTempDirectory();
|
||
try
|
||
{
|
||
var legacyConfigPath = Path.Combine(tempRoot, "Rvbust", "Install", "FlyingShot", "Config", "sample.json");
|
||
Directory.CreateDirectory(Path.GetDirectoryName(legacyConfigPath)!);
|
||
File.WriteAllText(legacyConfigPath, "{}");
|
||
|
||
var exception = Assert.Throws<FileNotFoundException>(() => PathCompatibility.ResolveConfigPath("sample.json", tempRoot));
|
||
|
||
Assert.Equal("sample.json", exception.FileName);
|
||
}
|
||
finally
|
||
{
|
||
Directory.Delete(tempRoot, recursive: true);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 验证默认加载配置时使用当前 replacement 仓库内的 Config/RobotConfig.json。
|
||
/// </summary>
|
||
[Fact]
|
||
public void RobotConfigLoader_LoadsRepositoryConfigFromReplacementConfigDirectory()
|
||
{
|
||
var replacementRoot = GetReplacementRoot();
|
||
|
||
var loaded = new RobotConfigLoader().Load("RobotConfig.json");
|
||
|
||
Assert.Equal(Path.Combine(replacementRoot, "Config", "RobotConfig.json"), loaded.SourcePath);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 定位当前工作区根目录,便于复用父仓库中的真实样本。
|
||
/// </summary>
|
||
private static string GetWorkspaceRoot()
|
||
{
|
||
var current = new DirectoryInfo(AppContext.BaseDirectory);
|
||
while (current is not null)
|
||
{
|
||
var slnPath = Path.Combine(current.FullName, "FlyshotReplacement.sln");
|
||
if (File.Exists(slnPath))
|
||
{
|
||
return Path.GetFullPath(Path.Combine(current.FullName, ".."));
|
||
}
|
||
|
||
current = current.Parent;
|
||
}
|
||
|
||
throw new DirectoryNotFoundException("Unable to locate the flyshot workspace root.");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 定位 replacement 仓库根目录,供测试读取仓库内固化配置。
|
||
/// </summary>
|
||
private static string GetReplacementRoot()
|
||
{
|
||
var current = new DirectoryInfo(AppContext.BaseDirectory);
|
||
while (current is not null)
|
||
{
|
||
if (File.Exists(Path.Combine(current.FullName, "FlyshotReplacement.sln")))
|
||
{
|
||
return current.FullName;
|
||
}
|
||
|
||
current = current.Parent;
|
||
}
|
||
|
||
throw new DirectoryNotFoundException("Unable to locate the flyshot replacement root.");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 创建当前测试专用的临时目录,避免不同测试之间相互污染。
|
||
/// </summary>
|
||
private static string CreateTempDirectory()
|
||
{
|
||
var tempPath = Path.Combine(Path.GetTempPath(), "flyshot-config-tests", Guid.NewGuid().ToString("N"));
|
||
Directory.CreateDirectory(tempPath);
|
||
return tempPath;
|
||
}
|
||
}
|