Files
FlyShotHost/tests/Flyshot.Core.Tests/RuntimeOrchestrationTests.cs
yunxiao.zhu 8a20d9f507 feat: 实现 ControllerClient HTTP 兼容层及 FANUC 运行时
- 新增 Flyshot.ControllerClientCompat 兼容层模块
  - 新增 Flyshot.Runtime.Fanuc 运行时模块
  - 新增 LegacyHttpApiController 暴露 HTTP 兼容 API
  - 补充 RuntimeOrchestrationTests 等测试覆盖
  - 补充 docs/ 兼容性需求与逆向工程文档
  - 更新 Host 注册、配置及解决方案引用

  变更概览:
  - Flyshot.ControllerClientCompat — 旧 ControllerClient 语义的 HTTP 适配
  - Flyshot.Runtime.Fanuc — IControllerRuntime 的 FANUC 真机实现
  - LegacyHttpApiController — HTTP API 兼容旧 SDK
  - docs/ — 兼容性需求与逆向工程分析文档
  - 测试:RuntimeOrchestrationTests、LegacyHttpApiCompatibilityTests
2026-04-24 16:55:25 +08:00

196 lines
7.0 KiB
C#

using Flyshot.ControllerClientCompat;
using Flyshot.Core.Config;
using Flyshot.Core.Domain;
using Flyshot.Runtime.Fanuc;
namespace Flyshot.Core.Tests;
/// <summary>
/// 验证最小运行时编排链路会把规划结果交给控制器运行时,而不是停留在兼容层内存状态。
/// </summary>
public sealed class RuntimeOrchestrationTests
{
/// <summary>
/// 验证 FANUC 最小运行时执行轨迹后会更新状态快照与最终关节位置。
/// </summary>
[Fact]
public void FanucControllerRuntime_ExecuteTrajectory_UpdatesSnapshotAndFinalJointPositions()
{
var runtime = new FanucControllerRuntime();
var robot = TestRobotFactory.CreateRobotProfile();
runtime.ResetRobot(robot, "FANUC_LR_Mate_200iD");
runtime.SetActiveController(sim: false);
runtime.Connect("192.168.10.101");
runtime.EnableRobot(bufferSize: 2);
var result = new TrajectoryResult(
programName: "demo",
method: PlanningMethod.Icsp,
isValid: true,
duration: TimeSpan.FromSeconds(1.2),
shotEvents: Array.Empty<ShotEvent>(),
triggerTimeline: Array.Empty<TrajectoryDoEvent>(),
artifacts: Array.Empty<TrajectoryArtifact>(),
failureReason: null,
usedCache: false,
originalWaypointCount: 4,
plannedWaypointCount: 4);
runtime.ExecuteTrajectory(result, [1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
var snapshot = runtime.GetSnapshot();
Assert.Equal("Connected", snapshot.ConnectionState);
Assert.False(snapshot.IsInMotion);
Assert.Equal([1.0, 2.0, 3.0, 4.0, 5.0, 6.0], snapshot.JointPositions);
}
/// <summary>
/// 验证普通轨迹会先进入 ICSP 规划,并沿用 ICSP 对示教点数量的约束。
/// </summary>
[Fact]
public void ControllerClientTrajectoryOrchestrator_PlanOrdinaryTrajectory_RejectsThreeTeachPoints()
{
var orchestrator = new ControllerClientTrajectoryOrchestrator();
var robot = TestRobotFactory.CreateRobotProfile();
void Act() =>
orchestrator.PlanOrdinaryTrajectory(
robot,
[
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
[0.5, 0.0, 0.0, 0.0, 0.0, 0.0],
[1.0, 0.0, 0.0, 0.0, 0.0, 0.0]
]);
Assert.Throws<ArgumentException>(Act);
}
/// <summary>
/// 验证已上传飞拍轨迹会经过 self-adapt-icsp 并生成拍照触发时间轴。
/// </summary>
[Fact]
public void ControllerClientTrajectoryOrchestrator_PlanUploadedFlyshot_BuildsShotTimeline()
{
var orchestrator = new ControllerClientTrajectoryOrchestrator();
var robot = TestRobotFactory.CreateRobotProfile();
var uploaded = TestRobotFactory.CreateUploadedTrajectoryWithSingleShot();
var bundle = orchestrator.PlanUploadedFlyshot(robot, uploaded);
Assert.True(bundle.Result.IsValid);
Assert.Single(bundle.Result.ShotEvents);
Assert.Single(bundle.Result.TriggerTimeline);
}
/// <summary>
/// 验证兼容服务执行普通轨迹时会进入规划链路,而不是直接把最后一个路点写入状态。
/// </summary>
[Fact]
public void ControllerClientCompatService_ExecuteTrajectory_RejectsThreeTeachPointsAfterPlanningIsIntroduced()
{
var service = TestRobotFactory.CreateCompatService();
service.SetUpRobot("FANUC_LR_Mate_200iD");
service.SetActiveController(sim: false);
service.Connect("192.168.10.101");
service.EnableRobot(2);
void Act() =>
service.ExecuteTrajectory(
[
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
[0.5, 0.0, 0.0, 0.0, 0.0, 0.0],
[1.0, 0.0, 0.0, 0.0, 0.0, 0.0]
]);
Assert.Throws<ArgumentException>(Act);
}
}
/// <summary>
/// 为运行时编排测试构造稳定的最小领域对象。
/// </summary>
internal static class TestRobotFactory
{
/// <summary>
/// 构造六轴测试机器人配置,避免运行时测试依赖真实 .robot 文件。
/// </summary>
/// <returns>可用于规划和运行时状态校验的机器人配置。</returns>
public static RobotProfile CreateRobotProfile()
{
return new RobotProfile(
name: "TestRobot",
modelPath: "Models/Test.robot",
degreesOfFreedom: 6,
jointLimits: Enumerable.Range(1, 6)
.Select(static index => new JointLimit($"J{index}", 10.0, 20.0, 100.0))
.ToArray(),
jointCouplings: Array.Empty<JointCoupling>(),
servoPeriod: TimeSpan.FromMilliseconds(8),
triggerPeriod: TimeSpan.FromMilliseconds(8));
}
/// <summary>
/// 构造一条含单个拍照点的上传飞拍轨迹。
/// </summary>
/// <returns>可用于触发时间轴测试的上传轨迹。</returns>
public static ControllerClientCompatUploadedTrajectory CreateUploadedTrajectoryWithSingleShot()
{
return new ControllerClientCompatUploadedTrajectory(
name: "demo-flyshot",
waypoints:
[
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
[0.1, 0.0, 0.0, 0.0, 0.0, 0.0],
[0.2, 0.0, 0.0, 0.0, 0.0, 0.0],
[0.3, 0.0, 0.0, 0.0, 0.0, 0.0]
],
shotFlags: [false, true, false, false],
offsetValues: [0, 1, 0, 0],
addressGroups:
[
Array.Empty<int>(),
[7, 8],
Array.Empty<int>(),
Array.Empty<int>()
]);
}
/// <summary>
/// 构造一份真实依赖注入等价的兼容服务,覆盖运行时和编排器协作。
/// </summary>
/// <returns>可执行 ControllerClient 兼容语义的服务实例。</returns>
public static ControllerClientCompatService CreateCompatService()
{
var options = new ControllerClientCompatOptions
{
WorkspaceRoot = GetWorkspaceRoot()
};
return new ControllerClientCompatService(
options,
new ControllerClientCompatRobotCatalog(options, new RobotModelLoader()),
new FanucControllerRuntime(),
new ControllerClientTrajectoryOrchestrator());
}
/// <summary>
/// 定位父工作区根目录,供兼容服务加载真实机器人模型。
/// </summary>
/// <returns>父工作区根目录。</returns>
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.");
}
}