* 在 FanucCommandProtocol/Client 中补齐速度倍率、TCP 位姿和 IO 的封包/解析,并引入 FanucIoTypes 字符串到枚举映射 * FanucControllerRuntime 在非仿真模式下接入真机命令通道,本地 缓存仅作为兜底,TCP 操作扩展为 7 维 Pose * FanucStateClient 增加帧超时检测、退避自动重连和诊断状态接口, 超时或重连期间不再把陈旧帧当作当前机器人状态 * FanucStateProtocol 锁定 90B 帧字段为 pose[6]、joint[6]、 external_axes[3] 和 raw_tail_words[4],并保留状态字诊断槽位 * ICspPlanner 增加 global_scale > 1.0 失败判定,self-adapt-icsp 内部禁用该判定以保留补点重试链路 * 同步更新 README/AGENTS/计划文档的 todo 状态和实现说明
229 lines
7.8 KiB
C#
229 lines
7.8 KiB
C#
using System.Text.Json;
|
|
using Flyshot.Core.Domain;
|
|
|
|
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 }))
|
|
},
|
|
artifacts: new[]
|
|
{
|
|
new TrajectoryArtifact(
|
|
kind: TrajectoryArtifactKind.JointDenseTrajectory,
|
|
logicalName: "JointDetialTraj.txt",
|
|
relativePath: "artifacts/JointDetialTraj.txt")
|
|
},
|
|
failureReason: null,
|
|
usedCache: true,
|
|
originalWaypointCount: 4,
|
|
plannedWaypointCount: 5);
|
|
|
|
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);
|
|
}
|
|
|
|
/// <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>())
|
|
});
|
|
}
|
|
}
|