💥 feat(planning)!: 拆分 C# ICSP 并默认 Rust

* 新增可选 Flyshot.CSharp.Planning 项目

* 将 icsp 映射到 Rust 后端并拒绝旧 icsp_rust

* 补充后端注册、文档、调试页和回归测试

BREAKING CHANGE: method=icsp 现在默认调用 Rust 后端;C# ICSP 改为 method=icsp-csharp,旧 method=icsp_rust 不再作为正式入口。
This commit is contained in:
2026-06-27 15:03:05 +08:00
parent 32cc740df5
commit 537c6b5320
28 changed files with 478 additions and 134 deletions

View File

@@ -27,6 +27,7 @@
<ProjectReference Include="..\..\src\Flyshot.Core.Config\Flyshot.Core.Config.csproj" />
<ProjectReference Include="..\..\src\Flyshot.Core.Domain\Flyshot.Core.Domain.csproj" />
<ProjectReference Include="..\..\src\Flyshot.Core.Planning\Flyshot.Core.Planning.csproj" />
<ProjectReference Include="..\..\src\Flyshot.CSharp.Planning\Flyshot.CSharp.Planning.csproj" />
<ProjectReference Include="..\..\src\Flyshot.Core.Triggering\Flyshot.Core.Triggering.csproj" />
<ProjectReference Include="..\..\src\Flyshot.Native.Planning\Flyshot.Native.Planning.csproj" />
<ProjectReference Include="..\..\src\Flyshot.Runtime.Fanuc\Flyshot.Runtime.Fanuc.csproj" />

View File

@@ -1,4 +1,5 @@
using Flyshot.Core.Config;
using Flyshot.CSharp.Planning;
using Flyshot.Core.Domain;
using Flyshot.Core.Planning;
using Flyshot.Core.Planning.Export;
@@ -55,7 +56,7 @@ public sealed class OfflinePlanTests
var request = new TrajectoryRequest(
robot: scaledProfile,
program: program,
method: useSelfAdapt ? PlanningMethod.SelfAdaptIcsp : PlanningMethod.Icsp);
method: useSelfAdapt ? PlanningMethod.SelfAdaptIcsp : PlanningMethod.IcspCSharp);
PlannedTrajectory trajectory;
if (useSelfAdapt)

View File

@@ -1,4 +1,5 @@
using Flyshot.Core.Config;
using Flyshot.CSharp.Planning;
using Flyshot.Core.Domain;
using Flyshot.Core.Planning;
using Flyshot.Core.Planning.Sampling;
@@ -27,7 +28,7 @@ public sealed class PlanningCompatibilityTests
new[] { 0.8, 0.3, 0.0, 0.0, 0.0, 0.0 },
new[] { 1.0, 0.2, 0.0, 0.0, 0.0, 0.0 }
}),
method: PlanningMethod.Icsp);
method: PlanningMethod.IcspCSharp);
var trajectory = new ICspPlanner().Plan(request);
@@ -51,7 +52,7 @@ public sealed class PlanningCompatibilityTests
new[] { 20.0 },
new[] { 30.0 }
}),
method: PlanningMethod.Icsp);
method: PlanningMethod.IcspCSharp);
var planner = new ICspPlanner(maxIterations: 0);

View File

@@ -1,4 +1,5 @@
using Flyshot.ControllerClientCompat;
using Flyshot.CSharp.Planning;
using Flyshot.Core.Config;
using Flyshot.Core.Domain;
using Flyshot.Core.Planning;
@@ -16,6 +17,19 @@ namespace Flyshot.Core.Tests;
/// </summary>
public sealed class RuntimeOrchestrationTests
{
/// <summary>
/// 创建显式注册 C# ICSP 后端的编排器,用于验证可选算法 DLL 的行为边界。
/// </summary>
/// <returns>包含 C# 普通 ICSP 与 self-adapt-icsp 后端的轨迹编排器。</returns>
private static ControllerClientTrajectoryOrchestrator CreateOrchestratorWithCSharpBackends()
{
return new ControllerClientTrajectoryOrchestrator(
[
new CSharpIcspPlanningBackend(),
new SelfAdaptIcspPlanningBackend()
]);
}
/// <summary>
/// 验证真机运行时会把 TCP 10010 状态通道健康度映射为可诊断连接状态。
/// </summary>
@@ -107,54 +121,10 @@ public sealed class RuntimeOrchestrationTests
}
/// <summary>
/// 验证普通轨迹使用准确字串 icsp_rust 时会真实调用 Rust DLL,并在结果中标记原生后端。
/// 验证普通轨迹默认 icsp 会真实调用 Rust DLL,并在结果中保留正式 icsp 方法标记。
/// </summary>
[Fact]
public void ControllerClientTrajectoryOrchestrator_PlanOrdinaryTrajectory_UsesRustPlanner()
{
var orchestrator = new ControllerClientTrajectoryOrchestrator();
var robot = TestRobotFactory.CreateRobotProfile();
var bundle = orchestrator.PlanOrdinaryTrajectory(
robot,
[
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
[0.001, 0.0, 0.0, 0.0, 0.0, 0.0],
[0.002, 0.0, 0.0, 0.0, 0.0, 0.0],
[0.003, 0.0, 0.0, 0.0, 0.0, 0.0]
],
new TrajectoryExecutionOptions(method: "icsp_rust"));
Assert.Equal(PlanningMethod.IcspRust, bundle.PlannedTrajectory.Method);
Assert.Equal(PlanningMethod.IcspRust, bundle.Result.Method);
}
/// <summary>
/// 验证飞拍轨迹使用准确字串 icsp_rust 时会真实调用 Rust DLL,同时保留触发时间轴。
/// </summary>
[Fact]
public void ControllerClientTrajectoryOrchestrator_PlanUploadedFlyshot_UsesRustPlanner()
{
var orchestrator = new ControllerClientTrajectoryOrchestrator();
var robot = TestRobotFactory.CreateRobotProfile();
var uploaded = TestRobotFactory.CreateUploadedTrajectoryWithSingleShot();
var bundle = orchestrator.PlanUploadedFlyshot(
robot,
uploaded,
new FlyshotExecutionOptions(method: "icsp_rust", useCache: false));
Assert.Equal(PlanningMethod.IcspRust, bundle.PlannedTrajectory.Method);
Assert.Equal(PlanningMethod.IcspRust, bundle.Result.Method);
Assert.Single(bundle.Result.ShotEvents);
Assert.Single(bundle.Result.TriggerTimeline);
}
/// <summary>
/// 验证普通轨迹默认 icsp 仍使用现有 C# 规划器。
/// </summary>
[Fact]
public void ControllerClientTrajectoryOrchestrator_PlanOrdinaryTrajectory_KeepsManagedIcspPlanner()
public void ControllerClientTrajectoryOrchestrator_PlanOrdinaryTrajectory_DefaultIcspUsesRustPlanner()
{
var orchestrator = new ControllerClientTrajectoryOrchestrator();
var robot = TestRobotFactory.CreateRobotProfile();
@@ -174,12 +144,98 @@ public sealed class RuntimeOrchestrationTests
}
/// <summary>
/// 验证 self-adapt-icsp 仍使用 C# 自适应规划器,不切换到 Rust。
/// 验证旧的 icsp_rust 字串不再作为正式入口,并提示调用方改用 icsp。
/// </summary>
[Fact]
public void ControllerClientTrajectoryOrchestrator_PlanUploadedFlyshot_KeepsManagedSelfAdaptPlanner()
public void ControllerClientTrajectoryOrchestrator_PlanOrdinaryTrajectory_RejectsLegacyIcspRustName()
{
var orchestrator = new ControllerClientTrajectoryOrchestrator();
var robot = TestRobotFactory.CreateRobotProfile();
var exception = Assert.Throws<ArgumentException>(() => orchestrator.PlanOrdinaryTrajectory(
robot,
[
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
[0.001, 0.0, 0.0, 0.0, 0.0, 0.0],
[0.002, 0.0, 0.0, 0.0, 0.0, 0.0],
[0.003, 0.0, 0.0, 0.0, 0.0, 0.0]
],
new TrajectoryExecutionOptions(method: "icsp_rust")));
Assert.Contains("icsp", exception.Message);
Assert.Contains("icsp_rust", exception.Message);
}
/// <summary>
/// 验证未注册 C# 后端时,icsp-csharp 会明确报错而不是静默回退。
/// </summary>
[Fact]
public void ControllerClientTrajectoryOrchestrator_PlanOrdinaryTrajectory_RejectsCSharpIcspWhenBackendIsMissing()
{
var orchestrator = new ControllerClientTrajectoryOrchestrator();
var robot = TestRobotFactory.CreateRobotProfile();
var exception = Assert.Throws<NotSupportedException>(() => orchestrator.PlanOrdinaryTrajectory(
robot,
[
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
[0.001, 0.0, 0.0, 0.0, 0.0, 0.0],
[0.002, 0.0, 0.0, 0.0, 0.0, 0.0],
[0.003, 0.0, 0.0, 0.0, 0.0, 0.0]
],
new TrajectoryExecutionOptions(method: "icsp-csharp")));
Assert.Contains("icsp-csharp", exception.Message);
}
/// <summary>
/// 验证显式注册 C# 后端后,icsp-csharp 使用拆分后的托管规划器。
/// </summary>
[Fact]
public void ControllerClientTrajectoryOrchestrator_PlanOrdinaryTrajectory_UsesRegisteredCSharpIcspPlanner()
{
var orchestrator = CreateOrchestratorWithCSharpBackends();
var robot = TestRobotFactory.CreateRobotProfile();
var bundle = orchestrator.PlanOrdinaryTrajectory(
robot,
[
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
[0.001, 0.0, 0.0, 0.0, 0.0, 0.0],
[0.002, 0.0, 0.0, 0.0, 0.0, 0.0],
[0.003, 0.0, 0.0, 0.0, 0.0, 0.0]
],
new TrajectoryExecutionOptions(method: "icsp-csharp"));
Assert.Equal(PlanningMethod.IcspCSharp, bundle.PlannedTrajectory.Method);
Assert.Equal(PlanningMethod.IcspCSharp, bundle.Result.Method);
}
/// <summary>
/// 验证未注册 C# 后端时,self-adapt-icsp 会明确报错而不是把行为藏在主服务里。
/// </summary>
[Fact]
public void ControllerClientTrajectoryOrchestrator_PlanUploadedFlyshot_RejectsSelfAdaptWhenBackendIsMissing()
{
var orchestrator = new ControllerClientTrajectoryOrchestrator();
var robot = TestRobotFactory.CreateRobotProfile();
var uploaded = TestRobotFactory.CreateUploadedTrajectoryWithSingleShot();
var exception = Assert.Throws<NotSupportedException>(() => orchestrator.PlanUploadedFlyshot(
robot,
uploaded,
new FlyshotExecutionOptions(method: "self-adapt-icsp", useCache: false)));
Assert.Contains("self-adapt-icsp", exception.Message);
}
/// <summary>
/// 验证显式注册 C# 后端后,self-adapt-icsp 仍使用 C# 自适应规划器。
/// </summary>
[Fact]
public void ControllerClientTrajectoryOrchestrator_PlanUploadedFlyshot_UsesRegisteredManagedSelfAdaptPlanner()
{
var orchestrator = CreateOrchestratorWithCSharpBackends();
var robot = new RobotProfile(
name: "ManagedSelfAdaptTestRobot",
modelPath: "Models/ManagedSelfAdaptTest.robot",

View File

@@ -1,4 +1,5 @@
using Flyshot.Core.Domain;
using Flyshot.CSharp.Planning;
using Flyshot.Core.Planning;
using Flyshot.Native.Planning;
@@ -35,7 +36,7 @@ public sealed class RustIcspPlannerInteropTests
var result = planner.Plan(CreateSimpleRequest());
Assert.Equal(PlanningMethod.IcspRust, result.Method);
Assert.Equal(PlanningMethod.Icsp, result.Method);
Assert.Equal(result.PlannedWaypointCount, result.WaypointTimes.Count);
Assert.Equal(result.PlannedWaypointCount - 1, result.SegmentDurations.Count);
Assert.Equal(result.SegmentDurations.Count, result.SegmentScales.Count);

View File

@@ -1,4 +1,5 @@
using Flyshot.Core.Domain;
using Flyshot.CSharp.Planning;
using Flyshot.Core.Planning;
using Flyshot.Native.Planning;
@@ -19,8 +20,8 @@ public sealed class RustIcspPlannerParityTests
PlannedTrajectory managed = new ICspPlanner().Plan(request);
PlannedTrajectory native = new RustIcspPlanner().Plan(request);
Assert.Equal(PlanningMethod.Icsp, managed.Method);
Assert.Equal(PlanningMethod.IcspRust, native.Method);
Assert.Equal(PlanningMethod.IcspCSharp, managed.Method);
Assert.Equal(PlanningMethod.Icsp, native.Method);
AssertTrajectoryMatches(managed, native, tolerance: 1e-9);
}
@@ -35,8 +36,8 @@ public sealed class RustIcspPlannerParityTests
PlannedTrajectory managed = new ICspPlanner(enforceFinalScale: false).Plan(request);
PlannedTrajectory native = new RustIcspPlanner(options).Plan(request);
Assert.Equal(PlanningMethod.Icsp, managed.Method);
Assert.Equal(PlanningMethod.IcspRust, native.Method);
Assert.Equal(PlanningMethod.IcspCSharp, managed.Method);
Assert.Equal(PlanningMethod.Icsp, native.Method);
AssertTrajectoryMatches(managed, native, tolerance: 1e-9);
}
@@ -116,7 +117,7 @@ public sealed class RustIcspPlannerParityTests
.Select(_ => new IoAddressGroup([]))
.ToArray());
return new TrajectoryRequest(robot, program, PlanningMethod.Icsp);
return new TrajectoryRequest(robot, program, PlanningMethod.IcspCSharp);
}
/// <summary>