using System.Text.Json;
using System.Text.Json.Nodes;
using Flyshot.Core.Config;
using Flyshot.Core.Domain;
using Microsoft.Extensions.Logging;
namespace Flyshot.ControllerClientCompat;
///
/// 使用运行目录 Config/RobotConfig.json 持久化单机器人飞拍轨迹和机器人配置。
///
public sealed class JsonFlyshotTrajectoryStore
{
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;
}
///
/// 将单条轨迹持久化到统一 RobotConfig.json,同时更新机器人配置段。
///
/// 当前已初始化的机器人名称,仅用于日志诊断。
/// 当前机器人级兼容配置。
/// 要保存的已上传轨迹。
public void Save(string robotName, CompatibilityRobotSettings settings, ControllerClientCompatUploadedTrajectory trajectory)
{
ArgumentNullException.ThrowIfNull(settings);
ArgumentNullException.ThrowIfNull(trajectory);
_logger?.LogInformation(
"RobotConfig 保存轨迹: robot={RobotName}, name={TrajectoryName}, waypoints={WaypointCount}",
robotName,
trajectory.Name,
trajectory.Waypoints.Count);
var path = ResolveStorePath();
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("RobotConfig 轨迹已保存到 {Path}", path);
}
///
/// 从统一 RobotConfig.json 删除指定名称的轨迹。
///
/// 当前已初始化的机器人名称,仅用于日志诊断。
/// 要删除的轨迹名称。
public void Delete(string robotName, string trajectoryName)
{
if (string.IsNullOrWhiteSpace(trajectoryName))
{
throw new ArgumentException("轨迹名称不能为空。", nameof(trajectoryName));
}
_logger?.LogInformation("RobotConfig 删除轨迹: robot={RobotName}, name={TrajectoryName}", robotName, trajectoryName);
var path = ResolveStorePath();
if (!File.Exists(path))
{
_logger?.LogWarning("RobotConfig 删除失败: 文件不存在 {Path}", path);
return;
}
var existingJson = File.ReadAllText(path);
var root = JsonNode.Parse(existingJson)?.AsObject();
if (root is null)
{
_logger?.LogWarning("RobotConfig 删除失败: 无法解析 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("RobotConfig 轨迹已删除: {TrajectoryName}", trajectoryName);
}
else
{
_logger?.LogWarning("RobotConfig 删除失败: 轨迹不存在 {TrajectoryName}", trajectoryName);
}
}
}
///
/// 加载统一 RobotConfig.json 中的所有轨迹,并回传机器人配置。
///
/// 当前已初始化的机器人名称,仅用于日志诊断。
/// 输出 RobotConfig.json 中的机器人配置;若文件不存在或解析失败则为 null。
/// 按轨迹名称索引的已上传轨迹集合。
public IReadOnlyDictionary LoadAll(string robotName, out CompatibilityRobotSettings? settings)
{
var path = ResolveStorePath();
if (!File.Exists(path))
{
_logger?.LogInformation("RobotConfig 无持久化数据: {Path}", path);
settings = null;
return new Dictionary(StringComparer.Ordinal);
}
try
{
_logger?.LogInformation("RobotConfig 正在加载: {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(
"RobotConfig 加载完成: robot={RobotName}, 轨迹数={Count}, useDo={UseDo}, ioKeepCycles={IoKeepCycles}",
robotName,
dict.Count,
settings?.UseDo,
settings?.IoKeepCycles);
return dict;
}
catch (Exception ex)
{
_logger?.LogError(ex, "RobotConfig 加载失败: {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()
{
return Path.Combine(_options.ResolveConfigRoot(), "RobotConfig.json");
}
}