using System.Text.Json; using System.Text.Json.Nodes; using Flyshot.Core.Config; using Flyshot.Core.Domain; using Microsoft.Extensions.Logging; namespace Flyshot.ControllerClientCompat; /// /// 定义已上传飞拍轨迹的持久化存储契约。 /// public interface IFlyshotTrajectoryStore { /// /// 将单条轨迹持久化到本地 JSON,同时更新所属机器人配置段。 /// /// 当前已初始化的机器人名称。 /// 当前机器人级兼容配置。 /// 要保存的已上传轨迹。 void Save(string robotName, CompatibilityRobotSettings settings, ControllerClientCompatUploadedTrajectory trajectory); /// /// 从本地 JSON 删除指定名称的轨迹。 /// /// 当前已初始化的机器人名称。 /// 要删除的轨迹名称。 void Delete(string robotName, string trajectoryName); /// /// 加载指定机器人名下所有已持久化的轨迹,并回传保存时的机器人配置。 /// /// 当前已初始化的机器人名称。 /// 输出保存时的机器人配置;若文件不存在或解析失败则为 null。 /// 按轨迹名称索引的已上传轨迹集合。 IReadOnlyDictionary LoadAll(string robotName, out CompatibilityRobotSettings? settings); } /// /// 使用与旧版 RobotConfig.json 一致的 JSON 格式在运行目录 Config 中持久化飞拍轨迹和机器人配置。 /// public sealed class JsonFlyshotTrajectoryStore : IFlyshotTrajectoryStore { private readonly ControllerClientCompatOptions _options; private readonly RobotConfigLoader _configLoader; private readonly ILogger? _logger; /// /// 初始化基于 JSON 文件的轨迹存储。 /// /// 兼容层基础配置,用于定位运行配置根目录。 /// 旧版 RobotConfig.json 加载器,用于反序列化已保存的轨迹。 /// 日志记录器;允许 null。 public JsonFlyshotTrajectoryStore(ControllerClientCompatOptions options, RobotConfigLoader configLoader, ILogger? logger = null) { _options = options ?? throw new ArgumentNullException(nameof(options)); _configLoader = configLoader ?? throw new ArgumentNullException(nameof(configLoader)); _logger = logger; } /// public void Save(string robotName, CompatibilityRobotSettings settings, ControllerClientCompatUploadedTrajectory trajectory) { ArgumentNullException.ThrowIfNull(settings); ArgumentNullException.ThrowIfNull(trajectory); _logger?.LogInformation( "TrajectoryStore 保存轨迹: robot={RobotName}, name={TrajectoryName}, waypoints={WaypointCount}", robotName, trajectory.Name, trajectory.Waypoints.Count); var path = ResolveStorePath(robotName); var directory = Path.GetDirectoryName(path)!; Directory.CreateDirectory(directory); JsonObject root; if (File.Exists(path)) { var existingJson = File.ReadAllText(path); root = JsonNode.Parse(existingJson)?.AsObject() ?? new JsonObject(); } else { root = new JsonObject(); } // 更新 robot 配置段,保持与旧版 RobotConfig.json 字段名一致。 root["robot"] = SerializeRobotSettings(settings); // 确保 flying_shots 节点存在。 if (!root.TryGetPropertyValue("flying_shots", out var flyingShotsNode) || flyingShotsNode is not JsonObject flyingShotsObj) { flyingShotsObj = new JsonObject(); root["flying_shots"] = flyingShotsObj; } flyingShotsObj[trajectory.Name] = SerializeTrajectory(trajectory); var writeOptions = new JsonSerializerOptions { WriteIndented = true, PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower }; File.WriteAllText(path, root.ToJsonString(writeOptions)); _logger?.LogInformation("TrajectoryStore 轨迹已保存到 {Path}", path); } /// public void Delete(string robotName, string trajectoryName) { if (string.IsNullOrWhiteSpace(trajectoryName)) { throw new ArgumentException("轨迹名称不能为空。", nameof(trajectoryName)); } _logger?.LogInformation("TrajectoryStore 删除轨迹: robot={RobotName}, name={TrajectoryName}", robotName, trajectoryName); var path = ResolveStorePath(robotName); if (!File.Exists(path)) { _logger?.LogWarning("TrajectoryStore 删除失败: 文件不存在 {Path}", path); return; } var existingJson = File.ReadAllText(path); var root = JsonNode.Parse(existingJson)?.AsObject(); if (root is null) { _logger?.LogWarning("TrajectoryStore 删除失败: 无法解析 JSON {Path}", path); return; } if (root.TryGetPropertyValue("flying_shots", out var flyingShotsNode) && flyingShotsNode is JsonObject flyingShotsObj) { var removed = flyingShotsObj.Remove(trajectoryName); if (removed) { var writeOptions = new JsonSerializerOptions { WriteIndented = true, PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower }; File.WriteAllText(path, root.ToJsonString(writeOptions)); _logger?.LogInformation("TrajectoryStore 轨迹已删除: {TrajectoryName}", trajectoryName); } else { _logger?.LogWarning("TrajectoryStore 删除失败: 轨迹不存在 {TrajectoryName}", trajectoryName); } } } /// public IReadOnlyDictionary LoadAll(string robotName, out CompatibilityRobotSettings? settings) { var path = ResolveStorePath(robotName); if (!File.Exists(path)) { _logger?.LogInformation("TrajectoryStore 无持久化数据: {Path}", path); settings = null; return new Dictionary(StringComparer.Ordinal); } try { _logger?.LogInformation("TrajectoryStore 正在加载: {Path}", path); var loaded = _configLoader.Load(path, _options.ResolveConfigRoot()); settings = loaded.Robot; var dict = new Dictionary(StringComparer.Ordinal); foreach (var program in loaded.Programs) { var traj = new ControllerClientCompatUploadedTrajectory( name: program.Value.Name, waypoints: program.Value.Waypoints.Select(static wp => wp.Positions), shotFlags: program.Value.ShotFlags, offsetValues: program.Value.OffsetValues, addressGroups: program.Value.AddressGroups.Select(static g => g.Addresses)); dict[program.Key] = traj; } _logger?.LogInformation( "TrajectoryStore 加载完成: robot={RobotName}, 轨迹数={Count}, useDo={UseDo}, ioKeepCycles={IoKeepCycles}", robotName, dict.Count, settings?.UseDo, settings?.IoKeepCycles); return dict; } catch (Exception ex) { _logger?.LogError(ex, "TrajectoryStore 加载失败: {Path}", path); settings = null; return new Dictionary(StringComparer.Ordinal); } } /// /// 把机器人兼容配置序列化为 JSON 对象,字段名与旧版 RobotConfig.json 一致。 /// private static JsonObject SerializeRobotSettings(CompatibilityRobotSettings settings) { return new JsonObject { ["use_do"] = JsonValue.Create(settings.UseDo), ["io_addr"] = JsonSerializer.SerializeToNode(settings.IoAddresses), ["io_keep_cycles"] = JsonValue.Create(settings.IoKeepCycles), ["acc_limit"] = JsonValue.Create(settings.AccLimitScale), ["jerk_limit"] = JsonValue.Create(settings.JerkLimitScale), ["adapt_icsp_try_num"] = JsonValue.Create(settings.AdaptIcspTryNum) }; } /// /// 把已上传轨迹序列化为 JSON 对象,字段名与旧版 RobotConfig.json 的 flying_shots 节点一致。 /// private static JsonObject SerializeTrajectory(ControllerClientCompatUploadedTrajectory trajectory) { return new JsonObject { ["traj_waypoints"] = JsonSerializer.SerializeToNode(trajectory.Waypoints), ["shot_flags"] = JsonSerializer.SerializeToNode(trajectory.ShotFlags), ["offset_values"] = JsonSerializer.SerializeToNode(trajectory.OffsetValues), ["addr"] = JsonSerializer.SerializeToNode(trajectory.AddressGroups) }; } /// /// 解析当前机器人对应的持久化文件路径。 /// private string ResolveStorePath(string robotName) { var storeDir = Path.Combine(_options.ResolveConfigRoot(), "TrajectoryStore"); return Path.Combine(storeDir, $"{robotName}_trajectories.json"); } }