using System.Text.Json; using Flyshot.Core.Domain; namespace Flyshot.Core.Config; /// /// 表示从旧版 RobotConfig.json 规范化后的机器人运行参数。 /// public sealed class CompatibilityRobotSettings { /// /// 初始化一份经过规范化的机器人兼容设置。 /// public CompatibilityRobotSettings( bool useDo, IEnumerable ioAddresses, int ioKeepCycles, double accLimitScale, double jerkLimitScale, int adaptIcspTryNum) { ArgumentNullException.ThrowIfNull(ioAddresses); if (ioKeepCycles < 0) { throw new ArgumentOutOfRangeException(nameof(ioKeepCycles), "IO 保持周期不能为负数。"); } if (accLimitScale <= 0.0) { throw new ArgumentOutOfRangeException(nameof(accLimitScale), "加速度倍率必须大于 0。"); } if (jerkLimitScale <= 0.0) { throw new ArgumentOutOfRangeException(nameof(jerkLimitScale), "Jerk 倍率必须大于 0。"); } if (adaptIcspTryNum < 0) { throw new ArgumentOutOfRangeException(nameof(adaptIcspTryNum), "补点尝试次数不能为负数。"); } // 固化地址数组,避免上层代码在加载后原地篡改配置内容。 var copiedIoAddresses = ioAddresses.ToArray(); if (copiedIoAddresses.Any(address => address < 0)) { throw new ArgumentException("IO 地址不能为负数。", nameof(ioAddresses)); } UseDo = useDo; IoAddresses = copiedIoAddresses; IoKeepCycles = ioKeepCycles; AccLimitScale = accLimitScale; JerkLimitScale = jerkLimitScale; AdaptIcspTryNum = adaptIcspTryNum; } /// /// 获取是否启用伺服同步 IO。 /// public bool UseDo { get; } /// /// 获取默认 IO 地址组。 /// public IReadOnlyList IoAddresses { get; } /// /// 获取单次触发保持的伺服周期数。 /// public int IoKeepCycles { get; } /// /// 获取加速度全局倍率。 /// public double AccLimitScale { get; } /// /// 获取 Jerk 全局倍率。 /// public double JerkLimitScale { get; } /// /// 获取自适应补点最大尝试次数。 /// public int AdaptIcspTryNum { get; } } /// /// 表示一次旧版 RobotConfig.json 的完整加载结果。 /// public sealed class LoadedRobotConfig { /// /// 初始化一份规范化后的旧配置文档。 /// public LoadedRobotConfig( string sourcePath, CompatibilityRobotSettings robot, IReadOnlyDictionary programs) { if (string.IsNullOrWhiteSpace(sourcePath)) { throw new ArgumentException("配置源路径不能为空。", nameof(sourcePath)); } SourcePath = sourcePath; Robot = robot ?? throw new ArgumentNullException(nameof(robot)); Programs = programs ?? throw new ArgumentNullException(nameof(programs)); } /// /// 获取实际命中的配置文件路径。 /// public string SourcePath { get; } /// /// 获取机器人级兼容设置。 /// public CompatibilityRobotSettings Robot { get; } /// /// 获取按名称索引的飞拍程序集合。 /// public IReadOnlyDictionary Programs { get; } } /// /// 负责读取旧版 RobotConfig.json 并转换成领域层可直接消费的结构。 /// public sealed class RobotConfigLoader { /// /// 加载一份旧版 RobotConfig.json。 /// /// 调用方传入的配置路径。 /// 用于兼容搜索的仓库根目录;为空时按当前工作目录推断。 /// 规范化后的配置文档。 public LoadedRobotConfig Load(string configPath, string? repoRoot = null) { var resolvedRepoRoot = ResolveRepoRoot(repoRoot); var resolvedConfigPath = PathCompatibility.ResolveConfigPath(configPath, resolvedRepoRoot); using var document = JsonDocument.Parse(File.ReadAllText(resolvedConfigPath)); var root = document.RootElement; var robotElement = root.GetProperty("robot"); var flyingShotsElement = root.GetProperty("flying_shots"); var robot = new CompatibilityRobotSettings( useDo: ReadBoolean(robotElement, "use_do", defaultValue: false), ioAddresses: ReadIntArray(robotElement, "io_addr"), ioKeepCycles: ReadInt(robotElement, "io_keep_cycles", defaultValue: 0), accLimitScale: ReadDouble(robotElement, "acc_limit", defaultValue: 1.0), jerkLimitScale: ReadDouble(robotElement, "jerk_limit", defaultValue: 1.0), adaptIcspTryNum: ReadInt(robotElement, "adapt_icsp_try_num", defaultValue: 0)); var programs = new Dictionary(StringComparer.Ordinal); foreach (var programElement in flyingShotsElement.EnumerateObject()) { var programName = programElement.Name; var program = ParseProgram(programName, programElement.Value); programs.Add(programName, program); } return new LoadedRobotConfig( sourcePath: resolvedConfigPath, robot: robot, programs: programs); } /// /// 把单个 flying_shots 节点转成领域层 FlyshotProgram。 /// private static FlyshotProgram ParseProgram(string programName, JsonElement programElement) { var waypointRows = programElement.GetProperty("traj_waypoints").EnumerateArray().ToArray(); var shotFlagElements = programElement.GetProperty("shot_flags").EnumerateArray().ToArray(); var waypoints = waypointRows .Select(row => new JointWaypoint(row.EnumerateArray().Select(static value => value.GetDouble()))) .ToArray(); var shotFlags = shotFlagElements .Select(static value => value.ValueKind == JsonValueKind.True || (value.ValueKind == JsonValueKind.Number && value.GetInt32() != 0)) .ToArray(); var waypointCount = waypoints.Length; var offsetValues = programElement.TryGetProperty("offset_values", out var offsetValuesElement) ? offsetValuesElement.EnumerateArray().Select(static value => value.GetInt32()).ToArray() : Enumerable.Repeat(0, waypointCount).ToArray(); var addressGroups = programElement.TryGetProperty("addr", out var addressElement) ? addressElement .EnumerateArray() .Select(group => new IoAddressGroup(group.EnumerateArray().Select(static value => value.GetInt32()))) .ToArray() : Enumerable.Range(0, waypointCount) .Select(_ => new IoAddressGroup(Array.Empty())) .ToArray(); return new FlyshotProgram( name: programName, waypoints: waypoints, shotFlags: shotFlags, offsetValues: offsetValues, addressGroups: addressGroups); } /// /// 解析布尔或 0/1 风格的兼容字段。 /// private static bool ReadBoolean(JsonElement parent, string propertyName, bool defaultValue) { if (!parent.TryGetProperty(propertyName, out var property)) { return defaultValue; } return property.ValueKind switch { JsonValueKind.True => true, JsonValueKind.False => false, JsonValueKind.Number => property.GetInt32() != 0, _ => defaultValue }; } /// /// 读取整数数组;字段不存在时返回空数组。 /// private static IReadOnlyList ReadIntArray(JsonElement parent, string propertyName) { return parent.TryGetProperty(propertyName, out var property) ? property.EnumerateArray().Select(static value => value.GetInt32()).ToArray() : Array.Empty(); } /// /// 读取整数兼容字段。 /// private static int ReadInt(JsonElement parent, string propertyName, int defaultValue) { return parent.TryGetProperty(propertyName, out var property) ? property.GetInt32() : defaultValue; } /// /// 读取浮点兼容字段。 /// private static double ReadDouble(JsonElement parent, string propertyName, double defaultValue) { return parent.TryGetProperty(propertyName, out var property) ? property.GetDouble() : defaultValue; } /// /// 推断当前 replacement 仓库根目录,优先使用调用方显式传入的值。 /// private static string ResolveRepoRoot(string? repoRoot) { if (!string.IsNullOrWhiteSpace(repoRoot)) { return Path.GetFullPath(repoRoot); } var current = new DirectoryInfo(Directory.GetCurrentDirectory()); while (current is not null) { if (File.Exists(Path.Combine(current.FullName, "FlyshotReplacement.sln"))) { return current.FullName; } current = current.Parent; } return Directory.GetCurrentDirectory(); } }