💥 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

@@ -1,4 +1,3 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
@@ -27,8 +26,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Flyshot.ControllerClientCom
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Flyshot.Runtime.Fanuc", "src\Flyshot.Runtime.Fanuc\Flyshot.Runtime.Fanuc.csproj", "{B705FA6C-19CA-44A8-882C-6CE26A5379C9}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Flyshot.Native.Planning", "src\Flyshot.Native.Planning\Flyshot.Native.Planning.csproj", "{928CF396-C0E1-488E-BCC1-CF0279CF2DD0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Flyshot.Native.Planning", "src\Flyshot.Native.Planning\Flyshot.Native.Planning.csproj", "{928CF396-C0E1-488E-BCC1-CF0279CF2DD0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Flyshot.CSharp.Planning", "src\Flyshot.CSharp.Planning\Flyshot.CSharp.Planning.csproj", "{4717558C-0A09-4D4D-BB1A-C0FD9241070E}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -159,18 +160,30 @@ Global
{B705FA6C-19CA-44A8-882C-6CE26A5379C9}.Release|x64.Build.0 = Release|Any CPU
{B705FA6C-19CA-44A8-882C-6CE26A5379C9}.Release|x86.ActiveCfg = Release|Any CPU
{B705FA6C-19CA-44A8-882C-6CE26A5379C9}.Release|x86.Build.0 = Release|Any CPU
{928CF396-C0E1-488E-BCC1-CF0279CF2DD0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{928CF396-C0E1-488E-BCC1-CF0279CF2DD0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{928CF396-C0E1-488E-BCC1-CF0279CF2DD0}.Debug|x64.ActiveCfg = Debug|Any CPU
{928CF396-C0E1-488E-BCC1-CF0279CF2DD0}.Debug|x64.Build.0 = Debug|Any CPU
{928CF396-C0E1-488E-BCC1-CF0279CF2DD0}.Debug|x86.ActiveCfg = Debug|Any CPU
{928CF396-C0E1-488E-BCC1-CF0279CF2DD0}.Debug|x86.Build.0 = Debug|Any CPU
{928CF396-C0E1-488E-BCC1-CF0279CF2DD0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{928CF396-C0E1-488E-BCC1-CF0279CF2DD0}.Release|Any CPU.Build.0 = Release|Any CPU
{928CF396-C0E1-488E-BCC1-CF0279CF2DD0}.Release|x64.ActiveCfg = Release|Any CPU
{928CF396-C0E1-488E-BCC1-CF0279CF2DD0}.Release|x64.Build.0 = Release|Any CPU
{928CF396-C0E1-488E-BCC1-CF0279CF2DD0}.Release|x86.ActiveCfg = Release|Any CPU
{928CF396-C0E1-488E-BCC1-CF0279CF2DD0}.Release|x86.Build.0 = Release|Any CPU
{928CF396-C0E1-488E-BCC1-CF0279CF2DD0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{928CF396-C0E1-488E-BCC1-CF0279CF2DD0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{928CF396-C0E1-488E-BCC1-CF0279CF2DD0}.Debug|x64.ActiveCfg = Debug|Any CPU
{928CF396-C0E1-488E-BCC1-CF0279CF2DD0}.Debug|x64.Build.0 = Debug|Any CPU
{928CF396-C0E1-488E-BCC1-CF0279CF2DD0}.Debug|x86.ActiveCfg = Debug|Any CPU
{928CF396-C0E1-488E-BCC1-CF0279CF2DD0}.Debug|x86.Build.0 = Debug|Any CPU
{928CF396-C0E1-488E-BCC1-CF0279CF2DD0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{928CF396-C0E1-488E-BCC1-CF0279CF2DD0}.Release|Any CPU.Build.0 = Release|Any CPU
{928CF396-C0E1-488E-BCC1-CF0279CF2DD0}.Release|x64.ActiveCfg = Release|Any CPU
{928CF396-C0E1-488E-BCC1-CF0279CF2DD0}.Release|x64.Build.0 = Release|Any CPU
{928CF396-C0E1-488E-BCC1-CF0279CF2DD0}.Release|x86.ActiveCfg = Release|Any CPU
{928CF396-C0E1-488E-BCC1-CF0279CF2DD0}.Release|x86.Build.0 = Release|Any CPU
{4717558C-0A09-4D4D-BB1A-C0FD9241070E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4717558C-0A09-4D4D-BB1A-C0FD9241070E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4717558C-0A09-4D4D-BB1A-C0FD9241070E}.Debug|x64.ActiveCfg = Debug|Any CPU
{4717558C-0A09-4D4D-BB1A-C0FD9241070E}.Debug|x64.Build.0 = Debug|Any CPU
{4717558C-0A09-4D4D-BB1A-C0FD9241070E}.Debug|x86.ActiveCfg = Debug|Any CPU
{4717558C-0A09-4D4D-BB1A-C0FD9241070E}.Debug|x86.Build.0 = Debug|Any CPU
{4717558C-0A09-4D4D-BB1A-C0FD9241070E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4717558C-0A09-4D4D-BB1A-C0FD9241070E}.Release|Any CPU.Build.0 = Release|Any CPU
{4717558C-0A09-4D4D-BB1A-C0FD9241070E}.Release|x64.ActiveCfg = Release|Any CPU
{4717558C-0A09-4D4D-BB1A-C0FD9241070E}.Release|x64.Build.0 = Release|Any CPU
{4717558C-0A09-4D4D-BB1A-C0FD9241070E}.Release|x86.ActiveCfg = Release|Any CPU
{4717558C-0A09-4D4D-BB1A-C0FD9241070E}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -186,6 +199,7 @@ Global
{E4DDC34C-9AB6-4050-A927-3DF69804708A} = {64EABE09-B1E0-4476-A213-32C93E46E7C3}
{5B45CC23-3551-4D0F-B3CC-22659C2A8BA3} = {64EABE09-B1E0-4476-A213-32C93E46E7C3}
{B705FA6C-19CA-44A8-882C-6CE26A5379C9} = {64EABE09-B1E0-4476-A213-32C93E46E7C3}
{928CF396-C0E1-488E-BCC1-CF0279CF2DD0} = {64EABE09-B1E0-4476-A213-32C93E46E7C3}
{928CF396-C0E1-488E-BCC1-CF0279CF2DD0} = {64EABE09-B1E0-4476-A213-32C93E46E7C3}
{4717558C-0A09-4D4D-BB1A-C0FD9241070E} = {64EABE09-B1E0-4476-A213-32C93E46E7C3}
EndGlobalSection
EndGlobal

View File

@@ -16,8 +16,8 @@
- 当前仓库不再恢复旧 `50001/TCP+JSON` 监听入口;旧 `ControllerClient` 逆向资料只作为接口语义参考,不作为运行时目标。
- 宿主只保留 ASP.NET Core HTTP 控制器层,以及其后端 `Flyshot.ControllerClientCompat` 兼容服务。
- `ExecuteTrajectory` 与 `ExecuteFlyShotTraj` 已经接入 `Planning + Triggering + Runtime` 链路;Web 状态页已通过 `/status` 和 `/api/status/snapshot` 暴露当前兼容层与运行时状态。
- `Flyshot.Core.Planning` 的 ICSP / self-adapt-icsp 轨迹已经完成旧系统导出轨迹对齐;`doubles` 仍未实现。
- `Flyshot.Native.Planning` 已提供显式 Rust ICSP 规划入口,通过 C ABI 调用 `flyshot_planning`;普通轨迹和飞拍轨迹可使用准确参数 `method="icsp_rust"` 选择该后端,默认 `icsp` 仍使用 C# `ICspPlanner`。
- `Flyshot.Core.Planning` 保留规划公共模型、样条、采样、导出和运动学能力;C# ICSP / self-adapt-icsp 已拆分到可选项目 `Flyshot.CSharp.Planning`。
- `Flyshot.Native.Planning` 已提供默认 Rust ICSP 规划入口,通过 C ABI 调用 `flyshot_planning`;普通轨迹和飞拍轨迹的默认 `method="icsp"` 使用 Rust 后端,`method="icsp-csharp"` 和 `method="self-adapt-icsp"` 只有显式注册 C# 后端时可用。
- `Flyshot.Runtime.Fanuc` 已固化 `10010 / 10012 / 60015` 基础协议帧编解码。`10010` 状态通道以 `j519 协议.pcap` 和 `Rvbust/uttc-20260428/20260428.pcap` 真机抓包确认为 90B 固定帧。
- 2026-04-28 UTTC 抓包确认:UDP 60015 命令 `target[0..5]` 为关节角度制 `deg`,`JointDetialTraj.txt` 为弧度制 `rad`,`speed_ratio=0.7` 体现为 UDP 下发时间轴约 `1.427730x` 拉伸;2026-04-30 实体机确认 `speed_ratio` 不影响生成的 `JointTraj.txt` 规划时长,当前实际生成约 `7.4s` 轨迹。
- 2026-04-30 本机 `50001/TCP+JSON` 抓包确认:`ExecuteFlyShotTraj(save_traj=true,use_cache=false)` 请求只显式携带规划方法、保存、缓存和等待参数,不携带 `JointLimits / acc_limit / jerk_limit / velocity / acceleration / jerk`。因此旧系统不可见的有效规划限制不再继续假设来自公开链路,新系统按 replacement-only 内部参数限制规划加速度。
@@ -65,7 +65,7 @@
- [x] 打通最小宿主与 `/healthz`
- [x] 建立领域模型与模块边界
- [x] 落地配置兼容与机器人模型解析
- [x] 落地 ICSP / self-adapt-icsp 轨迹规划与飞拍触发时间轴
- [x] 落地 Rust ICSP、可选 C# ICSP / self-adapt-icsp 轨迹规划与飞拍触发时间轴
- [x] 完成 ICSP 轨迹导出结果与旧系统对齐
- [x] 增加 Rust `cdylib` ICSP 核心、C# P/Invoke 适配和 C#/Rust 自动对拍
- [x] 将 `ExecuteTrajectory` / `ExecuteFlyShotTraj` 接入 FANUC 运行时链路
@@ -81,9 +81,11 @@
- [x] 完成真机优先与离线飞拍规划改造:`RealReady / OfflineReady / ModelLoadFailed` 状态、固定轨迹资产、完整离线规划响应和 Debug 页面
- [x] 删除生产仿真切换 HTTP 入口及旧 `/init_mpc_robt`,统一使用 `/init_mpc_robot`
### Rust ICSP 原生库构建
### 规划后端与 Rust ICSP 原生库构建
Rust 规划器通过普通轨迹或飞拍轨迹的准确参数 `method="icsp_rust"` 显式选择,不替换默认 `icsp` 使用的 C# `ICspPlanner`,也不会在原生库缺失时静默回退。
普通轨迹或飞拍轨迹的默认参数 `method="icsp"` 会调用 Rust ICSP 后端。旧 `method="icsp_rust"` 已不再作为正式入口,调用时会提示改用 `method="icsp"`;Rust 原生库缺失时会明确报错,不会静默回退 C#。
C# `ICspPlanner` 与 `SelfAdaptIcspPlanner` 已拆分到 `Flyshot.CSharp.Planning.dll`。默认服务项目不引用该 DLL;只有在部署方显式引用并调用 `AddCSharpPlanningBackends()` 后,`method="icsp-csharp"` 与 `method="self-adapt-icsp"` 才可用。
Windows x64 需要安装:

View File

@@ -130,11 +130,11 @@
以下内容是当前新实现的状态,不反推为旧系统事实:
- `Flyshot.ControllerClientCompat` 继续作为 HTTP 控制器后端兼容服务,不启动 `50001/TCP+JSON` 监听。
- `ExecuteTrajectory` 会先通过 `ICspPlanner` 规划普通轨迹,再把 `TrajectoryResult` 和最终关节位置交给 `IControllerRuntime`。
- `ExecuteFlyShotTraj` 会从上传轨迹目录取出轨迹,通过 `SelfAdaptIcspPlanner` 规划并用 `ShotTimelineBuilder` 生成 `ShotEvent` / `TrajectoryDoEvent`。
- `ExecuteTrajectory` 会先通过默认 Rust `method="icsp"` 后端规划普通轨迹,再把 `TrajectoryResult` 和最终关节位置交给 `IControllerRuntime`。
- `ExecuteFlyShotTraj` 会从上传轨迹目录取出轨迹,通过已注册的规划后端生成轨迹,并用 `ShotTimelineBuilder` 生成 `ShotEvent` / `TrajectoryDoEvent`。
- HTTP 控制器已经按公开文档和抓包补齐 `ExecuteTrajectory(method, save_traj)` 与 `ExecuteFlyShotTraj(move_to_start, method, save_traj, use_cache, wait)` 参数,并继续兼容旧的裸 waypoint 数组和只传 `name` 的请求体。
- 规划阶段会继续消费旧配置中的 `acc_limit / jerk_limit`。如果现场需要复现旧服务端不可见的保守约束,replacement 设计上使用内部 `planning_acceleration_scale` 限制规划加速度;该字段不属于旧 `ControllerClient` 公开 API,也不会通过 `50001` JSON 下发。
- `method="icsp"` 与 `method="self-adapt-icsp"` 已接入当前规划器;`method="doubles"` 会被识别但返回显式未实现,不会静默降级成 ICSP。
- `method="icsp"` 已接入默认 Rust 后端;`method="icsp-csharp"` 与 `method="self-adapt-icsp"` 只有显式注册 `Flyshot.CSharp.Planning` 后端时可用;`method="doubles"` 会被识别但返回显式未实现,不会静默降级成 ICSP。
- `Flyshot.Runtime.Fanuc.Protocol` 已经固化 `10010` 状态帧、`10012` 命令帧和 `60015` J519 数据包的基础编解码,并使用逆向抓包样本覆盖最小测试;`10010` 当前现场确认固定 90B。
- `Flyshot.Runtime.Fanuc` 已具备基础 Socket 客户端、程序启停、速度倍率/TCP/IO 参数命令和 J519 状态包驱动发送链路;稠密轨迹下发已按 `speed_ratio` 推进轨迹时间,并在收到机器人 UDP status 后按该 status sequence 回发命令。真实 R30iB 全流程现场联调仍需执行。
- 2026-04-28 `UTTC_MS11` 抓包确认 J519 命令目标为 `deg`、导出 `JointDetialTraj.txt` 为 `rad`,`speed_ratio=0.5/0.7/1.0` 分别形成 `1851/1322/926` 个主运行 J519 包;实际执行不发送 464 行导出点,而是在 8ms 实发周期上按 `t_traj = t_send * speed_ratio` 采样,包数为 `floor(duration / (0.008 * speed_ratio)) + 1`。

View File

@@ -428,7 +428,7 @@ function Install-ObfuscatedAssemblies
} |
Remove-Item -Force
# Rust ICSP 原生库必须随框架依赖包一起交付,否则 method=icsp_rust 会在运行时加载失败。
# Rust ICSP 原生库必须随框架依赖包一起交付,否则默认 method=icsp 会在运行时加载失败。
$nativePlanningLibraryPath = Join-Path $PublishedDirectory 'flyshot_planning.dll'
if (-not (Test-Path -LiteralPath $nativePlanningLibraryPath -PathType Leaf))
{

View File

@@ -0,0 +1,44 @@
using Flyshot.Core.Domain;
using Flyshot.Core.Planning;
using Microsoft.Extensions.Logging;
namespace Flyshot.CSharp.Planning;
/// <summary>
/// 将拆分后的 C# ICSP 规划器暴露为可选规划后端。
/// </summary>
public sealed class CSharpIcspPlanningBackend : IPlanningBackend
{
private readonly ICspPlanner _planner;
/// <summary>
/// 初始化 C# ICSP 后端。
/// </summary>
/// <param name="logger">C# ICSP 规划器日志记录器;允许为空。</param>
public CSharpIcspPlanningBackend(ILogger<ICspPlanner>? logger = null)
{
_planner = new ICspPlanner(logger: logger);
}
/// <summary>
/// 获取外部调用 C# ICSP 后端时使用的准确方法名。
/// </summary>
public string MethodName => "icsp-csharp";
/// <summary>
/// 获取 C# ICSP 后端写入结果对象的规划方法。
/// </summary>
public PlanningMethod ResultMethod => PlanningMethod.IcspCSharp;
/// <summary>
/// 执行 C# ICSP 规划,并将结果标记为 C# 后端。
/// </summary>
/// <param name="request">包含机器人约束和示教路点的规划请求。</param>
/// <param name="context">规划上下文;普通 C# ICSP 当前不使用该参数。</param>
/// <returns>由 C# ICSP 规划器生成的轨迹结果。</returns>
public PlannedTrajectory Plan(TrajectoryRequest request, PlanningBackendContext context)
{
ArgumentNullException.ThrowIfNull(context);
return _planner.Plan(request);
}
}

View File

@@ -0,0 +1,24 @@
using Flyshot.Core.Planning;
using Microsoft.Extensions.DependencyInjection;
namespace Flyshot.CSharp.Planning;
/// <summary>
/// 提供可选 C# 规划算法后端的依赖注入注册入口。
/// </summary>
public static class CSharpPlanningServiceCollectionExtensions
{
/// <summary>
/// 显式注册 C# ICSP 与 self-adapt-icsp 后端。
/// </summary>
/// <param name="services">当前宿主服务集合。</param>
/// <returns>同一服务集合,便于链式调用。</returns>
public static IServiceCollection AddCSharpPlanningBackends(this IServiceCollection services)
{
ArgumentNullException.ThrowIfNull(services);
services.AddSingleton<IPlanningBackend, CSharpIcspPlanningBackend>();
services.AddSingleton<IPlanningBackend, SelfAdaptIcspPlanningBackend>();
return services;
}
}

View File

@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\Flyshot.Core.Domain\Flyshot.Core.Domain.csproj" />
<ProjectReference Include="..\Flyshot.Core.Planning\Flyshot.Core.Planning.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.1" />
</ItemGroup>
</Project>

View File

@@ -1,6 +1,6 @@
# ICSP 算法说明(`ICspPlanner`)
本文档用于解释 `Flyshot.Core.Planning.ICspPlanner` 当前实现的 **ICSP 规划算法**在本仓库中的真实含义与计算逻辑,便于与逆向结论对照、以及指导后续改造(例如“按约束生成中间点位”)。
本文档用于解释 `Flyshot.CSharp.Planning.ICspPlanner` 当前实现的 **C# ICSP 规划算法**在本仓库中的真实含义与计算逻辑,便于与逆向结论对照、以及指导后续改造(例如“按约束生成中间点位”)。
> 适用范围:本文描述的是当前 C# 实现的 **“CubicSpline + 逐段时间缩放迭代(retiming)”** 版本。
> 重要澄清:`ICspPlanner` 的主要输出是 **时间轴**(每个示教点的时间戳),而不是直接输出固定周期的稠密点序列;稠密点在后续采样层生成。
@@ -186,9 +186,8 @@ globalScale > 1 + \text{finalScaleTolerance}
## 7. 关联实现位置(便于跳转)
- 算法入口:`src/Flyshot.Core.Planning/ICspPlanner.cs`
- 自适应补点:`src/Flyshot.Core.Planning/SelfAdaptIcspPlanner.cs`
- 算法入口:`src/Flyshot.CSharp.Planning/ICspPlanner.cs`
- 自适应补点:`src/Flyshot.CSharp.Planning/SelfAdaptIcspPlanner.cs`
- 三次样条实现(clamped-zero + 解析导峰值):`src/Flyshot.Core.Planning/CubicSplineInterpolator.cs`
- 规划层稠密采样:`src/Flyshot.Core.Planning/Sampling/TrajectorySampler.cs`
- J519 实发重采样:`src/Flyshot.Core.Planning/Sampling/J519SendTrajectorySampler.cs`

View File

@@ -1,7 +1,8 @@
using Flyshot.Core.Domain;
using Flyshot.Core.Planning;
using Microsoft.Extensions.Logging;
namespace Flyshot.Core.Planning;
namespace Flyshot.CSharp.Planning;
/// <summary>
/// 基于 CubicSpline + 逐段时间缩放迭代的 ICSP 规划器。
@@ -196,7 +197,7 @@ public sealed class ICspPlanner
waypointTimes: bestWaypointTimes,
segmentDurations: bestDurations,
segmentScales: bestScales,
method: PlanningMethod.Icsp,
method: PlanningMethod.IcspCSharp,
iterations: bestIterations,
threshold: bestThreshold);
}

View File

@@ -1,7 +1,8 @@
using Flyshot.Core.Domain;
using Microsoft.Extensions.Logging;
namespace Flyshot.Core.Planning;
using Flyshot.Core.Domain;
using Flyshot.Core.Planning;
using Microsoft.Extensions.Logging;
namespace Flyshot.CSharp.Planning;
/// <summary>
/// 在 ICSP 外层包裹补中点策略,实现 self-adapt-icsp 行为。

View File

@@ -0,0 +1,44 @@
using Flyshot.Core.Domain;
using Flyshot.Core.Planning;
using Microsoft.Extensions.Logging;
namespace Flyshot.CSharp.Planning;
/// <summary>
/// 将 C# self-adapt-icsp 规划器暴露为可选规划后端。
/// </summary>
public sealed class SelfAdaptIcspPlanningBackend : IPlanningBackend
{
private readonly SelfAdaptIcspPlanner _planner;
/// <summary>
/// 初始化 C# self-adapt-icsp 后端。
/// </summary>
/// <param name="logger">self-adapt-icsp 规划器日志记录器;允许为空。</param>
public SelfAdaptIcspPlanningBackend(ILogger<SelfAdaptIcspPlanner>? logger = null)
{
_planner = new SelfAdaptIcspPlanner(logger);
}
/// <summary>
/// 获取外部调用 self-adapt-icsp 后端时使用的准确方法名。
/// </summary>
public string MethodName => "self-adapt-icsp";
/// <summary>
/// 获取 self-adapt-icsp 后端写入结果对象的规划方法。
/// </summary>
public PlanningMethod ResultMethod => PlanningMethod.SelfAdaptIcsp;
/// <summary>
/// 执行 C# 自适应 ICSP 规划,按上下文使用配置中的补点次数。
/// </summary>
/// <param name="request">包含机器人约束、示教路点和触发元数据的规划请求。</param>
/// <param name="context">包含 self-adapt-icsp 最大补点次数的规划上下文。</param>
/// <returns>由 self-adapt-icsp 规划器生成的轨迹结果。</returns>
public PlannedTrajectory Plan(TrajectoryRequest request, PlanningBackendContext context)
{
ArgumentNullException.ThrowIfNull(context);
return _planner.Plan(request, context.AdaptIcspTryNum);
}
}

View File

@@ -1,4 +1,6 @@
using Flyshot.Core.Config;
using Flyshot.Core.Planning;
using Flyshot.Native.Planning;
using Flyshot.Runtime.Common;
using Flyshot.Runtime.Fanuc;
using Microsoft.Extensions.Configuration;
@@ -31,6 +33,7 @@ public static class ControllerClientCompatServiceCollectionExtensions
services.AddSingleton<RobotModelLoader>();
services.AddSingleton<RobotConfigLoader>();
services.AddSingleton<ControllerClientCompatRobotCatalog>();
services.AddSingleton<IPlanningBackend, RustIcspPlanningBackend>();
services.AddSingleton<ControllerClientTrajectoryOrchestrator>();
services.AddSingleton<FlyshotTrajectoryArtifactWriter>();
services.AddSingleton<JsonFlyshotTrajectoryStore>();

View File

@@ -29,9 +29,7 @@ public sealed class ControllerClientTrajectoryOrchestrator
/// </summary>
private const double SmoothStartStopBlend = 0.60;
private readonly ICspPlanner _icspPlanner;
private readonly RustIcspPlanner _rustIcspPlanner;
private readonly SelfAdaptIcspPlanner _selfAdaptIcspPlanner;
private readonly IReadOnlyDictionary<string, IPlanningBackend> _planningBackends;
private readonly ShotTimelineBuilder _shotTimelineBuilder = new(new WaypointTimestampResolver());
private readonly Dictionary<string, PlannedExecutionBundle> _flyshotCache = new(StringComparer.Ordinal);
private readonly ILogger<ControllerClientTrajectoryOrchestrator>? _logger;
@@ -39,16 +37,16 @@ public sealed class ControllerClientTrajectoryOrchestrator
/// <summary>
/// 初始化轨迹编排器。
/// </summary>
/// <param name="planningBackends">显式注册的规划后端;为空时仅启用默认 Rust icsp 后端。</param>
/// <param name="logger">日志记录器;允许 null。</param>
/// <param name="loggerFactory">日志工厂;允许 null。</param>
public ControllerClientTrajectoryOrchestrator(
IEnumerable<IPlanningBackend>? planningBackends = null,
ILogger<ControllerClientTrajectoryOrchestrator>? logger = null,
ILoggerFactory? loggerFactory = null)
{
_logger = logger;
_icspPlanner = new(logger: loggerFactory?.CreateLogger<ICspPlanner>());
_rustIcspPlanner = new();
_selfAdaptIcspPlanner = new(logger: loggerFactory?.CreateLogger<SelfAdaptIcspPlanner>());
_planningBackends = BuildPlanningBackendMap(planningBackends);
}
/// <summary>
@@ -84,14 +82,15 @@ public sealed class ControllerClientTrajectoryOrchestrator
offsetValues: Enumerable.Repeat(0, waypoints.Count),
addressGroups: Enumerable.Range(0, waypoints.Count).Select(static _ => Array.Empty<int>()));
var method = ParseOrdinaryMethod(options.Method);
var methodName = ParseOrdinaryMethod(options.Method);
var backend = ResolvePlanningBackend(methodName);
var request = new TrajectoryRequest(
robot: planningRobot,
program: program,
method: method,
method: backend.ResultMethod,
saveTrajectoryArtifacts: options.SaveTrajectory);
var plannedTrajectory = PlanByMethod(request, method);
var plannedTrajectory = PlanByBackend(request, backend);
var executionTrajectory = plannedTrajectory;
var denseJointTrajectory = CreateLimitCompliantDenseTrajectory(ref executionTrajectory, shapeTrajectoryEdges: false);
var shotTimeline = new ShotTimeline(Array.Empty<ShotEvent>(), Array.Empty<TrajectoryDoEvent>());
@@ -155,7 +154,8 @@ public sealed class ControllerClientTrajectoryOrchestrator
offsetValues: uploaded.OffsetValues,
addressGroups: uploaded.AddressGroups);
var method = ParseFlyshotMethod(options.Method);
var methodName = ParseFlyshotMethod(options.Method);
var backend = ResolvePlanningBackend(methodName);
var cacheKey = CreateFlyshotCacheKey(planningRobot, uploaded, options, settings, effectivePlanningSpeedScale);
if (options.UseCache && _flyshotCache.TryGetValue(cacheKey, out var cachedBundle))
{
@@ -168,12 +168,12 @@ public sealed class ControllerClientTrajectoryOrchestrator
var request = new TrajectoryRequest(
robot: planningRobot,
program: program,
method: method,
method: backend.ResultMethod,
moveToStart: options.MoveToStart,
saveTrajectoryArtifacts: options.SaveTrajectory,
useCache: options.UseCache);
var plannedTrajectory = PlanByMethod(request, method, settings);
var plannedTrajectory = PlanByBackend(request, backend, settings);
var smoothedExecutionTrajectory = ApplyExecutionTiming(plannedTrajectory, settings);
var denseJointTrajectory = CreateLimitCompliantDenseTrajectory(ref smoothedExecutionTrajectory, shapeTrajectoryEdges: false);
var shotTimeline = _shotTimelineBuilder.Build(
@@ -206,14 +206,15 @@ public sealed class ControllerClientTrajectoryOrchestrator
/// </summary>
/// <param name="method">旧 SDK 传入的方法名。</param>
/// <returns>领域层规划方法。</returns>
private static PlanningMethod ParseOrdinaryMethod(string method)
private static string ParseOrdinaryMethod(string method)
{
var normalized = NormalizeMethod(method);
return normalized switch
{
"icsp" => PlanningMethod.Icsp,
"icsp_rust" => PlanningMethod.IcspRust,
"doubles" => PlanningMethod.Doubles,
"icsp" => normalized,
"icsp-csharp" => normalized,
"icsp_rust" => throw new ArgumentException("method=\"icsp_rust\" 已改名为 method=\"icsp\"。", nameof(method)),
"doubles" => normalized,
_ => throw new ArgumentException($"Unsupported ExecuteTrajectory method: {method}", nameof(method))
};
}
@@ -223,35 +224,80 @@ public sealed class ControllerClientTrajectoryOrchestrator
/// </summary>
/// <param name="method">旧 SDK 传入的方法名。</param>
/// <returns>领域层规划方法。</returns>
private static PlanningMethod ParseFlyshotMethod(string method)
private static string ParseFlyshotMethod(string method)
{
var normalized = NormalizeMethod(method);
return normalized switch
{
"icsp" => PlanningMethod.Icsp,
"icsp_rust" => PlanningMethod.IcspRust,
"self-adapt-icsp" => PlanningMethod.SelfAdaptIcsp,
"doubles" => PlanningMethod.Doubles,
"icsp" => normalized,
"icsp-csharp" => normalized,
"icsp_rust" => throw new ArgumentException("method=\"icsp_rust\" 已改名为 method=\"icsp\"。", nameof(method)),
"self-adapt-icsp" => normalized,
"doubles" => normalized,
_ => throw new ArgumentException($"Unsupported ExecuteFlyShotTraj method: {method}", nameof(method))
};
}
/// <summary>
/// 按领域枚举分派到当前已经落地的规划器。
/// 使用显式注册的规划后端执行规划。
/// </summary>
/// <param name="request">规划请求。</param>
/// <param name="method">规划方法。</param>
/// <param name="backend">已解析的规划后端。</param>
/// <param name="settings">当前机器人兼容配置;为空时使用默认补点次数。</param>
/// <returns>规划轨迹。</returns>
private PlannedTrajectory PlanByMethod(TrajectoryRequest request, PlanningMethod method, CompatibilityRobotSettings? settings = null)
private static PlannedTrajectory PlanByBackend(
TrajectoryRequest request,
IPlanningBackend backend,
CompatibilityRobotSettings? settings = null)
{
return method switch
var context = new PlanningBackendContext(settings?.AdaptIcspTryNum ?? 5);
return backend.Plan(request, context);
}
/// <summary>
/// 从已注册后端中解析 method 对应的规划实现。
/// </summary>
/// <param name="methodName">已经归一化的外部 method 名称。</param>
/// <returns>可执行该 method 的规划后端。</returns>
private IPlanningBackend ResolvePlanningBackend(string methodName)
{
if (methodName == "doubles")
{
PlanningMethod.Icsp => _icspPlanner.Plan(request),
PlanningMethod.IcspRust => _rustIcspPlanner.Plan(request),
PlanningMethod.SelfAdaptIcsp => _selfAdaptIcspPlanner.Plan(request, settings?.AdaptIcspTryNum ?? 5),
PlanningMethod.Doubles => throw new NotSupportedException("doubles 轨迹规划尚未落地。"),
_ => throw new ArgumentOutOfRangeException(nameof(method), method, "未知轨迹规划方法。")
};
throw new NotSupportedException("doubles 轨迹规划尚未落地。");
}
if (_planningBackends.TryGetValue(methodName, out var backend))
{
return backend;
}
throw new NotSupportedException($"轨迹规划方法 {methodName} 未注册对应后端。");
}
/// <summary>
/// 构造规划后端查找表,未显式注册时只启用默认 Rust icsp 后端。
/// </summary>
/// <param name="planningBackends">调用方显式注册的规划后端集合。</param>
/// <returns>按 method 名称索引的后端表。</returns>
private static IReadOnlyDictionary<string, IPlanningBackend> BuildPlanningBackendMap(IEnumerable<IPlanningBackend>? planningBackends)
{
var backends = planningBackends?.ToArray() ?? [];
if (backends.Length == 0)
{
backends = [new RustIcspPlanningBackend()];
}
var map = new Dictionary<string, IPlanningBackend>(StringComparer.Ordinal);
foreach (var backend in backends)
{
var methodName = NormalizeMethod(backend.MethodName);
if (!map.TryAdd(methodName, backend))
{
throw new InvalidOperationException($"规划后端 method={methodName} 被重复注册。");
}
}
return map;
}
/// <summary>

View File

@@ -9,7 +9,7 @@ public sealed class FlyshotExecutionOptions
/// 初始化飞拍轨迹执行参数。
/// </summary>
/// <param name="moveToStart">执行前是否自动移动到轨迹起点。</param>
/// <param name="method">轨迹生成方法,支持 `icsp`、`icsp_rust`、`doubles` 或 `self-adapt-icsp`。</param>
/// <param name="method">轨迹生成方法,支持默认 Rust `icsp`、可选 C# `icsp-csharp`、`self-adapt-icsp` 或 `doubles`。</param>
/// <param name="saveTrajectory">是否保存轨迹信息。</param>
/// <param name="useCache">是否优先复用已规划轨迹缓存。</param>
/// <param name="wait">是否等待机器人执行完整条飞拍轨迹后再返回。</param>

View File

@@ -8,7 +8,7 @@ public sealed class TrajectoryExecutionOptions
/// <summary>
/// 初始化普通轨迹执行参数。
/// </summary>
/// <param name="method">轨迹生成方法,支持 `icsp`、`icsp_rust` 或 `doubles`。</param>
/// <param name="method">轨迹生成方法,支持默认 Rust `icsp`、可选 C# `icsp-csharp` 或 `doubles`。</param>
/// <param name="saveTrajectory">是否保存轨迹信息。</param>
public TrajectoryExecutionOptions(string method = "icsp", bool saveTrajectory = false)
{

View File

@@ -70,14 +70,14 @@ public sealed class TrajectoryRequest
public enum PlanningMethod
{
/// <summary>
/// Uses the cubic-spline ICSP planner.
/// 使用正式 icsp 后端;当前默认实现为 Rust 原生 ICSP。
/// </summary>
Icsp,
/// <summary>
/// 使用 Rust 原生库实现的 ICSP 规划器。
/// 使用拆分到可选程序集中的 C# ICSP 规划器。
/// </summary>
IcspRust,
IcspCSharp,
/// <summary>
/// Uses the adaptive ICSP planner that may insert additional points.

View File

@@ -0,0 +1,27 @@
using Flyshot.Core.Domain;
namespace Flyshot.Core.Planning;
/// <summary>
/// 定义可由兼容层按 method 字符串分派的轨迹规划后端。
/// </summary>
public interface IPlanningBackend
{
/// <summary>
/// 获取外部 HTTP/ControllerClient method 参数使用的准确方法名。
/// </summary>
string MethodName { get; }
/// <summary>
/// 获取该后端成功规划后写入结果对象的领域规划方法。
/// </summary>
PlanningMethod ResultMethod { get; }
/// <summary>
/// 执行轨迹规划。
/// </summary>
/// <param name="request">包含机器人约束、轨迹程序和调用标志的规划请求。</param>
/// <param name="context">兼容层传入的规划上下文参数。</param>
/// <returns>规划后的轨迹时间轴、路点和收敛信息。</returns>
PlannedTrajectory Plan(TrajectoryRequest request, PlanningBackendContext context);
}

View File

@@ -0,0 +1,21 @@
namespace Flyshot.Core.Planning;
/// <summary>
/// 表示规划后端执行时由兼容层补充的运行参数。
/// </summary>
public sealed class PlanningBackendContext
{
/// <summary>
/// 初始化规划后端上下文。
/// </summary>
/// <param name="adaptIcspTryNum">self-adapt-icsp 允许的最大补点重试次数。</param>
public PlanningBackendContext(int adaptIcspTryNum = 5)
{
AdaptIcspTryNum = adaptIcspTryNum;
}
/// <summary>
/// 获取 self-adapt-icsp 允许的最大补点重试次数。
/// </summary>
public int AdaptIcspTryNum { get; }
}

View File

@@ -109,7 +109,7 @@ public sealed class RustIcspPlanner
waypointTimes: waypointTimes,
segmentDurations: segmentDurations,
segmentScales: segmentScales,
method: PlanningMethod.IcspRust,
method: PlanningMethod.Icsp,
iterations: nativeResult.Iterations,
threshold: nativeResult.Threshold);
}

View File

@@ -0,0 +1,43 @@
using Flyshot.Core.Domain;
using Flyshot.Core.Planning;
namespace Flyshot.Native.Planning;
/// <summary>
/// 将 Rust ICSP 规划器暴露为默认正式 icsp 后端。
/// </summary>
public sealed class RustIcspPlanningBackend : IPlanningBackend
{
private readonly RustIcspPlanner _planner;
/// <summary>
/// 初始化 Rust ICSP 后端。
/// </summary>
/// <param name="planner">可选的 Rust ICSP 规划器实例;为空时使用默认原生库解析规则。</param>
public RustIcspPlanningBackend(RustIcspPlanner? planner = null)
{
_planner = planner ?? new RustIcspPlanner();
}
/// <summary>
/// 获取外部调用 Rust ICSP 后端时使用的正式方法名。
/// </summary>
public string MethodName => "icsp";
/// <summary>
/// 获取 Rust ICSP 后端写入结果对象的规划方法。
/// </summary>
public PlanningMethod ResultMethod => PlanningMethod.Icsp;
/// <summary>
/// 执行 Rust ICSP 规划。
/// </summary>
/// <param name="request">包含机器人约束和示教路点的规划请求。</param>
/// <param name="context">规划上下文;Rust ICSP 当前不使用该参数。</param>
/// <returns>由 Rust 原生库生成的轨迹结果。</returns>
public PlannedTrajectory Plan(TrajectoryRequest request, PlanningBackendContext context)
{
ArgumentNullException.ThrowIfNull(context);
return _planner.Plan(request);
}
}

View File

@@ -16,7 +16,7 @@
<Content Include="..\..\NLog.config" Link="NLog.config" CopyToOutputDirectory="PreserveNewest" />
<!-- 运行时配置根目录固定为输出目录 Config,调试和发布都复制仓库内固化配置。 -->
<Content Include="..\..\Config\**\*" Link="Config\%(RecursiveDir)%(Filename)%(Extension)" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
<!-- Rust ICSP 原生规划库:从 src/Libs 固定复制到运行与发布目录,供 method=icsp_rust 加载。 -->
<!-- Rust ICSP 原生规划库:从 src/Libs 固定复制到运行与发布目录,供默认 method=icsp 加载。 -->
<Content Include="..\Libs\flyshot_planning.dll" Link="flyshot_planning.dll" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" Condition="Exists('..\Libs\flyshot_planning.dll')" />
<Content Include="..\Libs\libflyshot_planning.so" Link="libflyshot_planning.so" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" Condition="Exists('..\Libs\libflyshot_planning.so')" />
</ItemGroup>

View File

@@ -46,6 +46,7 @@
规划方法
<select id="offline-planning-method">
<option value="icsp">icsp</option>
<option value="icsp-csharp">icsp-csharp</option>
<option value="self-adapt-icsp">self-adapt-icsp</option>
</select>
</label>
@@ -214,7 +215,7 @@
"description": "执行普通关节轨迹。该接口兼容两种历史请求体形态:一是对象形态 `{ waypoints, method, save_traj }`,二是直接传纯数组路点;查询参数里的 `method` 和 `save_traj` 仍可覆盖请求体值。",
"requestBodyFields": [
{ "name": "waypoints", "type": "array<array<number>> | array<object>", "required": true, "description": "历史兼容对象形态中的轨迹路点集合;每个路点既可直接是关节数组,也可写成 `{ joints: [...] }`。" },
{ "name": "method", "type": "string", "required": false, "description": "对象形态中的轨迹规划方法,默认 `icsp`。" },
{ "name": "method", "type": "string", "required": false, "description": "对象形态中的轨迹规划方法,默认 `icsp`;`icsp-csharp` 需要显式注册 C# 后端。" },
{ "name": "save_traj", "type": "boolean", "required": false, "description": "对象形态中的是否保存轨迹标记,默认 `false`。" },
{ "name": "根数组", "type": "array<array<number>> | array<object>", "required": true, "description": "纯数组形态时,请直接提交路点数组本身,而不是对象包裹层。" },
{ "name": "根数组[].joints", "type": "array<number>", "required": false, "description": "当路点元素是对象形态时,兼容层会读取其中的 `joints` 数组。" }
@@ -237,7 +238,7 @@
"requestBodyFields": [
{ "name": "name", "type": "string", "required": true, "description": "要执行的飞拍轨迹名称。" },
{ "name": "move_to_start", "type": "boolean", "required": false, "description": "是否先用临时 PTP 轨迹移动到飞拍起点,默认 `true`。" },
{ "name": "method", "type": "string", "required": false, "description": "飞拍轨迹生成方法,默认 `icsp`。" },
{ "name": "method", "type": "string", "required": false, "description": "飞拍轨迹生成方法,默认 `icsp`;`icsp-csharp` 和 `self-adapt-icsp` 需要显式注册 C# 后端。" },
{ "name": "save_traj", "type": "boolean", "required": false, "description": "是否保存轨迹信息,默认 `true`。" },
{ "name": "use_cache", "type": "boolean", "required": false, "description": "是否优先复用已缓存的轨迹结果,默认 `true`。" },
{ "name": "wait", "type": "boolean", "required": false, "description": "是否等待整条飞拍轨迹执行完成后再返回,默认 `true`。" }
@@ -248,7 +249,7 @@
"description": "仅要求机器人模型已加载;完成规划、触发绑定、最终 8ms 序列生成和文件导出,并返回完整规划结果。",
"requestBodyFields": [
{ "name": "name", "type": "string", "required": true, "description": "目标飞拍轨迹名称。" },
{ "name": "method", "type": "string", "required": false, "description": "轨迹规划方法,默认 `icsp`。" }
{ "name": "method", "type": "string", "required": false, "description": "轨迹规划方法,默认 `icsp`;`icsp-csharp` 和 `self-adapt-icsp` 需要显式注册 C# 后端。" }
]
},
"POST /is_flyShotTrajValid/": {
@@ -256,7 +257,7 @@
"description": "检查指定飞拍轨迹是否可生成有效执行结果,并返回轨迹时长;底层未实现时会返回 501。",
"requestBodyFields": [
{ "name": "name", "type": "string", "required": true, "description": "要校验的飞拍轨迹名称。" },
{ "name": "method", "type": "string", "required": false, "description": "轨迹规划方法,默认 `icsp`。" },
{ "name": "method", "type": "string", "required": false, "description": "轨迹规划方法,默认 `icsp`;`icsp-csharp` 和 `self-adapt-icsp` 需要显式注册 C# 后端。" },
{ "name": "save_traj", "type": "boolean", "required": false, "description": "是否同时保存轨迹信息,默认 `true`。" }
]
},

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>