using System.Text.Json; using System.Text.Json.Serialization; namespace SSPCTester.Devices.Config; public sealed class ModbusDeviceProfile { public string SchemaVersion { get; set; } = ""; public string ProfileId { get; set; } = ""; public string ProfileName { get; set; } = ""; public SerialDefaults SerialDefaults { get; set; } = new(); public FrameOptions Frame { get; set; } = new(); public RelayProtocol Relay { get; set; } = new(); public MeasurementProtocol Voltage { get; set; } = new(); public MeasurementProtocol Current { get; set; } = new(); public List Channels { get; set; } = new(); } public sealed class SerialDefaults { public int BaudRate { get; set; } = 9600; public int DataBits { get; set; } = 8; public string Parity { get; set; } = "None"; public int StopBits { get; set; } = 1; public int ReadTimeoutMs { get; set; } = 500; public int Retry { get; set; } = 2; public string Protocol { get; set; } = "ModbusRTU"; } public sealed class FrameOptions { public int RegisterBytes { get; set; } = 2; public string DataByteOrder { get; set; } = "BigEndian"; public string Crc { get; set; } = "CRC16Modbus"; public string CrcByteOrder { get; set; } = "LittleEndian"; } public sealed class RelayProtocol { public byte FunctionCodeWrite { get; set; } = 5; public string OnValue { get; set; } = "0xFF00"; public string OffValue { get; set; } = "0x0000"; public bool VerifyEchoResponse { get; set; } = true; [JsonIgnore] public ushort OnValueNumber => ParseHex(OnValue); [JsonIgnore] public ushort OffValueNumber => ParseHex(OffValue); private static ushort ParseHex(string value) => Convert.ToUInt16(value.StartsWith("0x", StringComparison.OrdinalIgnoreCase) ? value[2..] : value, 16); } public sealed class MeasurementProtocol { public byte Slave { get; set; } public byte FunctionCodeRead { get; set; } = 3; public ushort StartRegister { get; set; } public ushort RegisterCount { get; set; } public double Range { get; set; } public string Unit { get; set; } = ""; public string Formula { get; set; } = ""; public bool Signed { get; set; } public int DataStartOffset { get; set; } = 3; public int ChannelBytes { get; set; } = 2; public string DataByteOrder { get; set; } = "BigEndian"; } public sealed class ChannelMapping { public int PhysicalSlot { get; set; } public RelayMapping Relay { get; set; } = new(); public RegisterMapping Voltage { get; set; } = new(); public RegisterMapping Current { get; set; } = new(); } public sealed class RelayMapping { public byte Slave { get; set; } public ushort Coil { get; set; } } public sealed class RegisterMapping { public int SourceDisplayChannel { get; set; } public int RegisterOffset { get; set; } } public static class ModbusDeviceProfileLoader { private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNameCaseInsensitive = true }; public static ModbusDeviceProfile Load(string path) { string resolved = Path.IsPathRooted(path) ? path : Path.Combine(AppContext.BaseDirectory, path); if (!File.Exists(resolved)) throw new FileNotFoundException($"找不到 Modbus 设备配置:{resolved}", resolved); var profile = JsonSerializer.Deserialize(File.ReadAllText(resolved), JsonOptions) ?? throw new InvalidDataException("Modbus 设备配置为空。"); Validate(profile); return profile; } public static void Validate(ModbusDeviceProfile p) { var errors = new List(); if (p.SchemaVersion != "1.0") errors.Add($"不支持 schemaVersion '{p.SchemaVersion}'"); if (p.Channels.Count != 24) errors.Add("channels 必须恰好包含 24 项"); var slots = p.Channels.Select(x => x.PhysicalSlot).OrderBy(x => x).ToArray(); if (!slots.SequenceEqual(Enumerable.Range(1, 24))) errors.Add("physicalSlot 必须完整覆盖 1~24 且不能重复"); if (p.Channels.GroupBy(x => (x.Relay.Slave, x.Relay.Coil)).Any(g => g.Count() > 1)) errors.Add("继电器 slave/coil 组合存在重复"); ValidateMeasurement("voltage", p.Voltage, p.Channels.Select(x => x.Voltage.RegisterOffset), errors); ValidateMeasurement("current", p.Current, p.Channels.Select(x => x.Current.RegisterOffset), errors); foreach (var slave in p.Channels.Select(x => x.Relay.Slave).Append(p.Voltage.Slave).Append(p.Current.Slave)) if (slave is < 1 or > 247) errors.Add($"从站地址 {slave} 超出 1~247"); if (p.Frame.RegisterBytes != 2) errors.Add("frame.registerBytes 仅支持 2"); if (p.Frame.DataByteOrder != "BigEndian") errors.Add("frame.dataByteOrder 仅支持 BigEndian"); if (p.Frame.Crc != "CRC16Modbus") errors.Add("frame.crc 仅支持 CRC16Modbus"); if (p.Frame.CrcByteOrder != "LittleEndian") errors.Add("frame.crcByteOrder 仅支持 LittleEndian"); if (p.Relay.FunctionCodeWrite != 5) errors.Add("relay.functionCodeWrite 必须为 5"); try { _ = p.Relay.OnValueNumber; _ = p.Relay.OffValueNumber; } catch { errors.Add("relay.onValue/offValue 必须是有效十六进制值"); } if (errors.Count > 0) throw new InvalidDataException("Modbus 设备配置无效:" + string.Join(";", errors.Distinct())); } private static void ValidateMeasurement( string name, MeasurementProtocol protocol, IEnumerable offsets, List errors) { if (protocol.FunctionCodeRead != 3) errors.Add($"{name}.functionCodeRead 必须为 3"); if (protocol.RegisterCount != 24) errors.Add($"{name}.registerCount 必须为 24"); if (protocol.ChannelBytes != 2) errors.Add($"{name}.channelBytes 仅支持 2"); if (protocol.DataStartOffset != 3) errors.Add($"{name}.dataStartOffset 必须为 3"); if (protocol.DataByteOrder != "BigEndian") errors.Add($"{name}.dataByteOrder 仅支持 BigEndian"); if (protocol.Formula is not ("A" or "B")) errors.Add($"{name}.formula 只能是 A 或 B"); if (protocol.Range <= 0) errors.Add($"{name}.range 必须大于 0"); var values = offsets.ToArray(); if (values.Any(x => x < 0 || x >= protocol.RegisterCount)) errors.Add($"{name}.registerOffset 超出寄存器范围"); if (values.Distinct().Count() != values.Length) errors.Add($"{name}.registerOffset 存在重复"); } }