feat(runtime): 添加轨迹持久化与密集执行链路

* 新增飞拍轨迹文件存储,支持上传、加载与删除
* 接通 ControllerClientCompat 到运行时的轨迹编排
* 完善 FANUC 命令与 J519 客户端发送链路
* 补充密集轨迹执行、运行时编排和协议客户端测试
* 更新 README 与 AGENTS 中的当前实现状态
This commit is contained in:
2026-04-26 17:14:17 +08:00
parent a78e6761cb
commit 390d066ece
19 changed files with 1172 additions and 57 deletions

View File

@@ -1,3 +1,4 @@
using Flyshot.Core.Config;
using Flyshot.Core.Domain;
using Flyshot.Runtime.Common;
@@ -14,8 +15,11 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
private readonly ControllerClientCompatRobotCatalog _robotCatalog;
private readonly IControllerRuntime _runtime;
private readonly ControllerClientTrajectoryOrchestrator _trajectoryOrchestrator;
private readonly RobotConfigLoader _configLoader;
private readonly IFlyshotTrajectoryStore _trajectoryStore;
private RobotProfile? _activeRobotProfile;
private string? _configuredRobotName;
private CompatibilityRobotSettings? _robotSettings;
private string? _connectedServerIp;
private int _connectedServerPort;
private bool _showTcp = true;
@@ -29,16 +33,22 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
/// <param name="robotCatalog">机器人模型目录。</param>
/// <param name="runtime">控制器运行时。</param>
/// <param name="trajectoryOrchestrator">轨迹规划与触发编排器。</param>
/// <param name="configLoader">旧版 RobotConfig.json 加载器。</param>
/// <param name="trajectoryStore">已上传轨迹持久化存储。</param>
public ControllerClientCompatService(
ControllerClientCompatOptions options,
ControllerClientCompatRobotCatalog robotCatalog,
IControllerRuntime runtime,
ControllerClientTrajectoryOrchestrator trajectoryOrchestrator)
ControllerClientTrajectoryOrchestrator trajectoryOrchestrator,
RobotConfigLoader configLoader,
IFlyshotTrajectoryStore trajectoryStore)
{
_options = options ?? throw new ArgumentNullException(nameof(options));
_robotCatalog = robotCatalog ?? throw new ArgumentNullException(nameof(robotCatalog));
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
_trajectoryOrchestrator = trajectoryOrchestrator ?? throw new ArgumentNullException(nameof(trajectoryOrchestrator));
_configLoader = configLoader ?? throw new ArgumentNullException(nameof(configLoader));
_trajectoryStore = trajectoryStore ?? throw new ArgumentNullException(nameof(trajectoryStore));
}
/// <inheritdoc />
@@ -97,7 +107,11 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
/// <inheritdoc />
public void SetUpRobot(string robotName)
{
var robotProfile = _robotCatalog.LoadProfile(robotName);
var robotSettings = TryLoadRobotSettings() ?? CreateDefaultRobotSettings();
var robotProfile = _robotCatalog.LoadProfile(
robotName,
robotSettings.AccLimitScale,
robotSettings.JerkLimitScale);
lock (_stateLock)
{
@@ -106,6 +120,14 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
_activeRobotProfile = robotProfile;
_uploadedTrajectories.Clear();
_runtime.ResetRobot(robotProfile, robotName);
_robotSettings = robotSettings;
// 从持久化存储恢复该机器人名下之前已上传的轨迹。
var savedTrajectories = _trajectoryStore.LoadAll(robotName, out _);
foreach (var saved in savedTrajectories)
{
_uploadedTrajectories[saved.Key] = saved.Value;
}
}
}
@@ -361,6 +383,10 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
{
EnsureRuntimeEnabled();
_uploadedTrajectories[trajectory.Name] = trajectory;
var robotName = _configuredRobotName ?? throw new InvalidOperationException("Robot has not been setup.");
var settings = _robotSettings ?? CreateDefaultRobotSettings();
_trajectoryStore.Save(robotName, settings, trajectory);
}
}
@@ -398,7 +424,7 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
}
// 已上传飞拍轨迹必须按调用方指定 method 生成 shot timeline 后再交给运行时。
var bundle = _trajectoryOrchestrator.PlanUploadedFlyshot(robot, trajectory, options);
var bundle = _trajectoryOrchestrator.PlanUploadedFlyshot(robot, trajectory, options, RequireRobotSettings());
if (options.MoveToStart)
{
_runtime.ExecuteTrajectory(CreateImmediateMoveResult(), bundle.PlannedTrajectory.PlannedWaypoints[0].Positions);
@@ -425,11 +451,16 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
throw new InvalidOperationException("FlyShot trajectory does not exist.");
}
// 当前阶段没有落地文件导出,先通过 saveTrajectory=true 走规划校验避免静默接受非法参数。
// 先通过规划校验避免静默接受非法参数,同时把轨迹信息强制刷写到本地 JSON
_ = _trajectoryOrchestrator.PlanUploadedFlyshot(
robot,
trajectory,
new FlyshotExecutionOptions(saveTrajectory: true, method: method));
new FlyshotExecutionOptions(saveTrajectory: true, method: method),
RequireRobotSettings());
var robotName = _configuredRobotName ?? throw new InvalidOperationException("Robot has not been setup.");
var settings = _robotSettings ?? CreateDefaultRobotSettings();
_trajectoryStore.Save(robotName, settings, trajectory);
}
}
@@ -452,7 +483,8 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
var bundle = _trajectoryOrchestrator.PlanUploadedFlyshot(
robot,
trajectory,
new FlyshotExecutionOptions(method: method, saveTrajectory: saveTrajectory));
new FlyshotExecutionOptions(method: method, saveTrajectory: saveTrajectory),
RequireRobotSettings());
duration = bundle.Result.Duration;
return bundle.Result.IsValid;
@@ -473,6 +505,9 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
{
throw new InvalidOperationException("DeleteFlyShotTraj failed");
}
var robotName = _configuredRobotName ?? throw new InvalidOperationException("Robot has not been setup.");
_trajectoryStore.Delete(robotName, name);
}
}
@@ -503,6 +538,15 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
return _activeRobotProfile ?? throw new InvalidOperationException("Robot has not been setup.");
}
/// <summary>
/// 获取当前机器人兼容配置;未加载旧配置时回退到现场默认值。
/// </summary>
/// <returns>当前机器人配置。</returns>
private CompatibilityRobotSettings RequireRobotSettings()
{
return _robotSettings ?? CreateDefaultRobotSettings();
}
/// <summary>
/// 校验机器人已经完成初始化。
/// </summary>
@@ -542,4 +586,61 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
originalWaypointCount: 1,
plannedWaypointCount: 1);
}
/// <summary>
/// 尝试从工作区加载旧版 RobotConfig.json 获取机器人配置;失败时返回 null。
/// </summary>
/// <returns>加载到的机器人配置,或 null。</returns>
private CompatibilityRobotSettings? TryLoadRobotSettings()
{
try
{
var workspaceRoot = !string.IsNullOrWhiteSpace(_options.WorkspaceRoot)
? Path.GetFullPath(_options.WorkspaceRoot)
: ResolveWorkspaceRootFromBaseDirectory();
var configPath = PathCompatibility.ResolveConfigPath("RobotConfig.json", workspaceRoot);
var loaded = _configLoader.Load(configPath, workspaceRoot);
return loaded.Robot;
}
catch
{
return null;
}
}
/// <summary>
/// 构造与旧现场默认行为一致的机器人兼容配置。
/// </summary>
/// <returns>默认机器人配置。</returns>
private static CompatibilityRobotSettings CreateDefaultRobotSettings()
{
return new CompatibilityRobotSettings(
useDo: false,
ioAddresses: Array.Empty<int>(),
ioKeepCycles: 2,
accLimitScale: 1.0,
jerkLimitScale: 1.0,
adaptIcspTryNum: 5);
}
/// <summary>
/// 从当前程序基目录向上搜索 FlyshotReplacement.sln 以推断工作区根目录。
/// </summary>
/// <returns>父工作区根目录。</returns>
private static string ResolveWorkspaceRootFromBaseDirectory()
{
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.");
}
}