feat(*): 添加轨迹产物导出与规划速度倍率隔离

* 新增 FlyshotTrajectoryArtifactWriter,支持 saveTrajectory
  将规划结果导出到 Config/Data/name(JointTraj、CartTraj、
  ShotEvents 等)
* RobotConfig 新增 PlanningSpeedScale,区分规划阶段限速倍率
  与运行时 J519 下发倍率
* 轨迹缓存键纳入 planningSpeedScale,避免降速规划误用缓存
* 完善 FanucCommandClient 命令参数日志与状态通道重连
* 补充 RuntimeOrchestrationTests 覆盖产物导出与倍率隔离
* 更新 README 进度文档
This commit is contained in:
2026-04-30 13:52:09 +08:00
parent a6579f1e5b
commit 91c1494cde
20 changed files with 593 additions and 133 deletions

View File

@@ -7,38 +7,9 @@ using Microsoft.Extensions.Logging;
namespace Flyshot.ControllerClientCompat;
/// <summary>
/// 定义已上传飞拍轨迹的持久化存储契约
/// 使用运行目录 Config/RobotConfig.json 持久化单机器人飞拍轨迹和机器人配置
/// </summary>
public interface IFlyshotTrajectoryStore
{
/// <summary>
/// 将单条轨迹持久化到本地 JSON同时更新所属机器人配置段。
/// </summary>
/// <param name="robotName">当前已初始化的机器人名称。</param>
/// <param name="settings">当前机器人级兼容配置。</param>
/// <param name="trajectory">要保存的已上传轨迹。</param>
void Save(string robotName, CompatibilityRobotSettings settings, ControllerClientCompatUploadedTrajectory trajectory);
/// <summary>
/// 从本地 JSON 删除指定名称的轨迹。
/// </summary>
/// <param name="robotName">当前已初始化的机器人名称。</param>
/// <param name="trajectoryName">要删除的轨迹名称。</param>
void Delete(string robotName, string trajectoryName);
/// <summary>
/// 加载指定机器人名下所有已持久化的轨迹,并回传保存时的机器人配置。
/// </summary>
/// <param name="robotName">当前已初始化的机器人名称。</param>
/// <param name="settings">输出保存时的机器人配置;若文件不存在或解析失败则为 null。</param>
/// <returns>按轨迹名称索引的已上传轨迹集合。</returns>
IReadOnlyDictionary<string, ControllerClientCompatUploadedTrajectory> LoadAll(string robotName, out CompatibilityRobotSettings? settings);
}
/// <summary>
/// 使用与旧版 RobotConfig.json 一致的 JSON 格式在运行目录 Config 中持久化飞拍轨迹和机器人配置。
/// </summary>
public sealed class JsonFlyshotTrajectoryStore : IFlyshotTrajectoryStore
public sealed class JsonFlyshotTrajectoryStore
{
private readonly ControllerClientCompatOptions _options;
private readonly RobotConfigLoader _configLoader;
@@ -57,19 +28,24 @@ public sealed class JsonFlyshotTrajectoryStore : IFlyshotTrajectoryStore
_logger = logger;
}
/// <inheritdoc />
/// <summary>
/// 将单条轨迹持久化到统一 RobotConfig.json同时更新机器人配置段。
/// </summary>
/// <param name="robotName">当前已初始化的机器人名称,仅用于日志诊断。</param>
/// <param name="settings">当前机器人级兼容配置。</param>
/// <param name="trajectory">要保存的已上传轨迹。</param>
public void Save(string robotName, CompatibilityRobotSettings settings, ControllerClientCompatUploadedTrajectory trajectory)
{
ArgumentNullException.ThrowIfNull(settings);
ArgumentNullException.ThrowIfNull(trajectory);
_logger?.LogInformation(
"TrajectoryStore 保存轨迹: robot={RobotName}, name={TrajectoryName}, waypoints={WaypointCount}",
"RobotConfig 保存轨迹: robot={RobotName}, name={TrajectoryName}, waypoints={WaypointCount}",
robotName,
trajectory.Name,
trajectory.Waypoints.Count);
var path = ResolveStorePath(robotName);
var path = ResolveStorePath();
var directory = Path.GetDirectoryName(path)!;
Directory.CreateDirectory(directory);
@@ -103,10 +79,14 @@ public sealed class JsonFlyshotTrajectoryStore : IFlyshotTrajectoryStore
};
File.WriteAllText(path, root.ToJsonString(writeOptions));
_logger?.LogInformation("TrajectoryStore 轨迹已保存到 {Path}", path);
_logger?.LogInformation("RobotConfig 轨迹已保存到 {Path}", path);
}
/// <inheritdoc />
/// <summary>
/// 从统一 RobotConfig.json 删除指定名称的轨迹。
/// </summary>
/// <param name="robotName">当前已初始化的机器人名称,仅用于日志诊断。</param>
/// <param name="trajectoryName">要删除的轨迹名称。</param>
public void Delete(string robotName, string trajectoryName)
{
if (string.IsNullOrWhiteSpace(trajectoryName))
@@ -114,12 +94,12 @@ public sealed class JsonFlyshotTrajectoryStore : IFlyshotTrajectoryStore
throw new ArgumentException("轨迹名称不能为空。", nameof(trajectoryName));
}
_logger?.LogInformation("TrajectoryStore 删除轨迹: robot={RobotName}, name={TrajectoryName}", robotName, trajectoryName);
_logger?.LogInformation("RobotConfig 删除轨迹: robot={RobotName}, name={TrajectoryName}", robotName, trajectoryName);
var path = ResolveStorePath(robotName);
var path = ResolveStorePath();
if (!File.Exists(path))
{
_logger?.LogWarning("TrajectoryStore 删除失败: 文件不存在 {Path}", path);
_logger?.LogWarning("RobotConfig 删除失败: 文件不存在 {Path}", path);
return;
}
@@ -127,7 +107,7 @@ public sealed class JsonFlyshotTrajectoryStore : IFlyshotTrajectoryStore
var root = JsonNode.Parse(existingJson)?.AsObject();
if (root is null)
{
_logger?.LogWarning("TrajectoryStore 删除失败: 无法解析 JSON {Path}", path);
_logger?.LogWarning("RobotConfig 删除失败: 无法解析 JSON {Path}", path);
return;
}
@@ -142,29 +122,34 @@ public sealed class JsonFlyshotTrajectoryStore : IFlyshotTrajectoryStore
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
};
File.WriteAllText(path, root.ToJsonString(writeOptions));
_logger?.LogInformation("TrajectoryStore 轨迹已删除: {TrajectoryName}", trajectoryName);
_logger?.LogInformation("RobotConfig 轨迹已删除: {TrajectoryName}", trajectoryName);
}
else
{
_logger?.LogWarning("TrajectoryStore 删除失败: 轨迹不存在 {TrajectoryName}", trajectoryName);
_logger?.LogWarning("RobotConfig 删除失败: 轨迹不存在 {TrajectoryName}", trajectoryName);
}
}
}
/// <inheritdoc />
/// <summary>
/// 加载统一 RobotConfig.json 中的所有轨迹,并回传机器人配置。
/// </summary>
/// <param name="robotName">当前已初始化的机器人名称,仅用于日志诊断。</param>
/// <param name="settings">输出 RobotConfig.json 中的机器人配置;若文件不存在或解析失败则为 null。</param>
/// <returns>按轨迹名称索引的已上传轨迹集合。</returns>
public IReadOnlyDictionary<string, ControllerClientCompatUploadedTrajectory> LoadAll(string robotName, out CompatibilityRobotSettings? settings)
{
var path = ResolveStorePath(robotName);
var path = ResolveStorePath();
if (!File.Exists(path))
{
_logger?.LogInformation("TrajectoryStore 无持久化数据: {Path}", path);
_logger?.LogInformation("RobotConfig 无持久化数据: {Path}", path);
settings = null;
return new Dictionary<string, ControllerClientCompatUploadedTrajectory>(StringComparer.Ordinal);
}
try
{
_logger?.LogInformation("TrajectoryStore 正在加载: {Path}", path);
_logger?.LogInformation("RobotConfig 正在加载: {Path}", path);
var loaded = _configLoader.Load(path, _options.ResolveConfigRoot());
settings = loaded.Robot;
@@ -182,7 +167,7 @@ public sealed class JsonFlyshotTrajectoryStore : IFlyshotTrajectoryStore
}
_logger?.LogInformation(
"TrajectoryStore 加载完成: robot={RobotName}, 轨迹数={Count}, useDo={UseDo}, ioKeepCycles={IoKeepCycles}",
"RobotConfig 加载完成: robot={RobotName}, 轨迹数={Count}, useDo={UseDo}, ioKeepCycles={IoKeepCycles}",
robotName,
dict.Count,
settings?.UseDo,
@@ -192,7 +177,7 @@ public sealed class JsonFlyshotTrajectoryStore : IFlyshotTrajectoryStore
}
catch (Exception ex)
{
_logger?.LogError(ex, "TrajectoryStore 加载失败: {Path}", path);
_logger?.LogError(ex, "RobotConfig 加载失败: {Path}", path);
settings = null;
return new Dictionary<string, ControllerClientCompatUploadedTrajectory>(StringComparer.Ordinal);
}
@@ -229,11 +214,10 @@ public sealed class JsonFlyshotTrajectoryStore : IFlyshotTrajectoryStore
}
/// <summary>
/// 解析当前机器人对应的持久化文件路径。
/// 解析单程序单机器人的统一配置文件路径。
/// </summary>
private string ResolveStorePath(string robotName)
private string ResolveStorePath()
{
var storeDir = Path.Combine(_options.ResolveConfigRoot(), "TrajectoryStore");
return Path.Combine(storeDir, $"{robotName}_trajectories.json");
return Path.Combine(_options.ResolveConfigRoot(), "RobotConfig.json");
}
}