* 为 RobotConfig 增加 trigger_sample_index_offset_cycles 配置 * 让 DO 事件携带示教点关节角并按最接近 sample 绑定触发 * 调整运行时 IO 地址位掩码映射并补充 ShotEvents 导出 * 新增 2026042802-1 抓包分析脚本、数据产物与结论文档 * 补齐配置兼容、规划绑定和运行时触发相关测试
262 lines
9.4 KiB
C#
262 lines
9.4 KiB
C#
using System.Text.Json;
|
|
using Flyshot.Core.Domain;
|
|
using Flyshot.Core.Planning.Sampling;
|
|
|
|
namespace Flyshot.Core.Tests;
|
|
|
|
/// <summary>
|
|
/// Verifies the Task 2 domain contracts before planning and runtime code depend on them.
|
|
/// </summary>
|
|
public sealed class DomainModelTests
|
|
{
|
|
/// <summary>
|
|
/// Ensures robot profiles keep a stable copy of joint limits and reject invalid dimensions.
|
|
/// </summary>
|
|
[Fact]
|
|
public void RobotProfile_CopiesJointLimits_AndRejectsMismatchedDof()
|
|
{
|
|
var jointLimits = new[]
|
|
{
|
|
new JointLimit("J1", 7.85, 32.72, 272.7),
|
|
new JointLimit("J2", 6.63, 27.63, 230.28)
|
|
};
|
|
|
|
var profile = new RobotProfile(
|
|
name: "LR_Mate_200iD_7L",
|
|
modelPath: "Models/LR_Mate_200iD_7L.robot",
|
|
degreesOfFreedom: 2,
|
|
jointLimits: jointLimits,
|
|
jointCouplings: Array.Empty<JointCoupling>(),
|
|
servoPeriod: TimeSpan.FromMilliseconds(8),
|
|
triggerPeriod: TimeSpan.FromMilliseconds(8));
|
|
|
|
Assert.Equal(2, profile.DegreesOfFreedom);
|
|
Assert.NotSame(jointLimits, profile.JointLimits);
|
|
|
|
// The planner must not accept a profile whose DOF and limits disagree.
|
|
Assert.Throws<ArgumentException>(() => new RobotProfile(
|
|
name: "InvalidRobot",
|
|
modelPath: "Models/Invalid.robot",
|
|
degreesOfFreedom: 3,
|
|
jointLimits: jointLimits,
|
|
jointCouplings: Array.Empty<JointCoupling>(),
|
|
servoPeriod: TimeSpan.FromMilliseconds(8),
|
|
triggerPeriod: TimeSpan.FromMilliseconds(8)));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ensures uploaded flyshot programs keep their shot metadata aligned with teach waypoints.
|
|
/// </summary>
|
|
[Fact]
|
|
public void FlyshotProgram_RejectsMisalignedShotMetadata()
|
|
{
|
|
var waypoints = new[]
|
|
{
|
|
new JointWaypoint(new[] { 0.0, 1.0 }),
|
|
new JointWaypoint(new[] { 2.0, 3.0 })
|
|
};
|
|
|
|
var validProgram = new FlyshotProgram(
|
|
name: "EOL10_EAU_0",
|
|
waypoints: waypoints,
|
|
shotFlags: new[] { true, false },
|
|
offsetValues: new[] { 0, 1 },
|
|
addressGroups: new[]
|
|
{
|
|
new IoAddressGroup(new[] { 100 }),
|
|
new IoAddressGroup(Array.Empty<int>())
|
|
});
|
|
|
|
Assert.Equal(1, validProgram.ShotWaypointCount);
|
|
|
|
// The gateway cannot recover from count mismatches after this point, so fail fast here.
|
|
Assert.Throws<ArgumentException>(() => new FlyshotProgram(
|
|
name: "BrokenProgram",
|
|
waypoints: waypoints,
|
|
shotFlags: new[] { true },
|
|
offsetValues: new[] { 0, 1 },
|
|
addressGroups: new[]
|
|
{
|
|
new IoAddressGroup(new[] { 100 }),
|
|
new IoAddressGroup(Array.Empty<int>())
|
|
}));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ensures execution requests start from predictable defaults for compatibility paths.
|
|
/// </summary>
|
|
[Fact]
|
|
public void TrajectoryRequest_UsesExpectedDefaults()
|
|
{
|
|
var request = new TrajectoryRequest(
|
|
robot: CreateRobotProfile(),
|
|
program: CreateProgram(),
|
|
method: PlanningMethod.Icsp);
|
|
|
|
Assert.False(request.MoveToStart);
|
|
Assert.False(request.SaveTrajectoryArtifacts);
|
|
Assert.False(request.UseCache);
|
|
Assert.Equal(PlanningMethod.Icsp, request.Method);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ensures runtime snapshots expose safe empty collections before the controller connects.
|
|
/// </summary>
|
|
[Fact]
|
|
public void ControllerStateSnapshot_InitializesEmptyRuntimeCollections()
|
|
{
|
|
var snapshot = new ControllerStateSnapshot(
|
|
capturedAt: DateTimeOffset.Parse("2026-04-23T10:00:00+08:00"),
|
|
connectionState: "Disconnected",
|
|
isEnabled: false,
|
|
isInMotion: false,
|
|
speedRatio: 100.0);
|
|
|
|
Assert.Empty(snapshot.JointPositions);
|
|
Assert.Empty(snapshot.CartesianPose);
|
|
Assert.Empty(snapshot.ActiveAlarms);
|
|
Assert.Empty(snapshot.StateTailWords);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 验证控制器快照会保留 TCP 10010 尾部状态字作为诊断字段。
|
|
/// </summary>
|
|
[Fact]
|
|
public void ControllerStateSnapshot_CopiesStateTailWordsForDiagnostics()
|
|
{
|
|
var snapshot = new ControllerStateSnapshot(
|
|
capturedAt: DateTimeOffset.Parse("2026-04-23T10:00:00+08:00"),
|
|
connectionState: "Connected",
|
|
isEnabled: true,
|
|
isInMotion: false,
|
|
speedRatio: 1.0,
|
|
stateTailWords: [2u, 0u, 0u, 1u]);
|
|
|
|
var json = JsonSerializer.Serialize(snapshot);
|
|
|
|
Assert.Equal([2u, 0u, 0u, 1u], snapshot.StateTailWords);
|
|
Assert.Contains("\"stateTailWords\":[2,0,0,1]", json);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ensures JSON payloads keep stable enum and property names for downstream SDKs.
|
|
/// </summary>
|
|
[Fact]
|
|
public void DomainObjects_SerializeStableContract()
|
|
{
|
|
var result = new TrajectoryResult(
|
|
programName: "EOL10_EAU_0",
|
|
method: PlanningMethod.SelfAdaptIcsp,
|
|
isValid: true,
|
|
duration: TimeSpan.FromSeconds(1.25),
|
|
shotEvents: new[]
|
|
{
|
|
new ShotEvent(
|
|
waypointIndex: 0,
|
|
triggerTime: 0.5,
|
|
sampleIndex: 62,
|
|
sampleTime: 0.496,
|
|
addressGroup: new IoAddressGroup(new[] { 100 }))
|
|
},
|
|
triggerTimeline: new[]
|
|
{
|
|
new TrajectoryDoEvent(
|
|
waypointIndex: 0,
|
|
triggerTime: 0.5,
|
|
offsetCycles: 0,
|
|
holdCycles: 1,
|
|
addressGroup: new IoAddressGroup(new[] { 100 }),
|
|
referenceJointsDegrees: new[] { 12.5, -3.0 })
|
|
},
|
|
artifacts: new[]
|
|
{
|
|
new TrajectoryArtifact(
|
|
kind: TrajectoryArtifactKind.JointDenseTrajectory,
|
|
logicalName: "JointDetialTraj.txt",
|
|
relativePath: "artifacts/JointDetialTraj.txt")
|
|
},
|
|
failureReason: null,
|
|
usedCache: true,
|
|
originalWaypointCount: 4,
|
|
plannedWaypointCount: 5,
|
|
triggerSampleIndexOffsetCycles: 7);
|
|
|
|
var json = JsonSerializer.Serialize(result);
|
|
|
|
Assert.Contains("\"programName\":\"EOL10_EAU_0\"", json);
|
|
Assert.Contains("\"method\":\"SelfAdaptIcsp\"", json);
|
|
Assert.Contains("\"kind\":\"JointDenseTrajectory\"", json);
|
|
Assert.Contains("\"usedCache\":true", json);
|
|
Assert.Contains("\"triggerSampleIndexOffsetCycles\":7", json);
|
|
Assert.Contains("\"referenceJointsDegrees\":[12.5,-3]", json);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 验证触发绑定允许在最佳 sample 基础上继续向后偏移固定命令周期。
|
|
/// </summary>
|
|
[Fact]
|
|
public void TriggerSampleBinder_Bind_AppliesConfiguredSampleIndexOffset()
|
|
{
|
|
var trigger = new TrajectoryDoEvent(
|
|
waypointIndex: 1,
|
|
triggerTime: 0.008,
|
|
offsetCycles: 0,
|
|
holdCycles: 2,
|
|
addressGroup: new IoAddressGroup([2, 4]),
|
|
referenceJointsDegrees: [10.0, 20.0]);
|
|
var samples = new[]
|
|
{
|
|
new J519SendSample(sampleIndex: 0, sendTime: 0.0, trajectoryTime: 0.0, speedRatio: 1.0, jointsDegrees: [0.0, 0.0]),
|
|
new J519SendSample(sampleIndex: 1, sendTime: 0.008, trajectoryTime: 0.008, speedRatio: 1.0, jointsDegrees: [10.0, 20.0]),
|
|
new J519SendSample(sampleIndex: 2, sendTime: 0.016, trajectoryTime: 0.016, speedRatio: 1.0, jointsDegrees: [11.0, 21.0]),
|
|
new J519SendSample(sampleIndex: 3, sendTime: 0.024, trajectoryTime: 0.024, speedRatio: 1.0, jointsDegrees: [12.0, 22.0])
|
|
};
|
|
|
|
var binding = Assert.Single(Flyshot.Core.Planning.Sampling.TriggerSampleBinder.Bind([trigger], samples, sampleIndexOffsetCycles: 2));
|
|
|
|
Assert.True(binding.FoundInWindow);
|
|
Assert.Equal(3, binding.SampleIndex);
|
|
Assert.Equal(0.024, binding.Sample.SendTime, precision: 6);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a representative robot profile for request-level domain tests.
|
|
/// </summary>
|
|
private static RobotProfile CreateRobotProfile()
|
|
{
|
|
return new RobotProfile(
|
|
name: "LR_Mate_200iD_7L",
|
|
modelPath: "Models/LR_Mate_200iD_7L.robot",
|
|
degreesOfFreedom: 2,
|
|
jointLimits: new[]
|
|
{
|
|
new JointLimit("J1", 7.85, 32.72, 272.7),
|
|
new JointLimit("J2", 6.63, 27.63, 230.28)
|
|
},
|
|
jointCouplings: Array.Empty<JointCoupling>(),
|
|
servoPeriod: TimeSpan.FromMilliseconds(8),
|
|
triggerPeriod: TimeSpan.FromMilliseconds(8));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a representative uploaded program for request-level domain tests.
|
|
/// </summary>
|
|
private static FlyshotProgram CreateProgram()
|
|
{
|
|
return new FlyshotProgram(
|
|
name: "EOL10_EAU_0",
|
|
waypoints: new[]
|
|
{
|
|
new JointWaypoint(new[] { 0.0, 1.0 }),
|
|
new JointWaypoint(new[] { 2.0, 3.0 })
|
|
},
|
|
shotFlags: new[] { true, false },
|
|
offsetValues: new[] { 0, 1 },
|
|
addressGroups: new[]
|
|
{
|
|
new IoAddressGroup(new[] { 100 }),
|
|
new IoAddressGroup(Array.Empty<int>())
|
|
});
|
|
}
|
|
}
|