SSPC-Tester/SSPCTester.Devices/Config/ModbusDeviceProfile.cs

151 lines
6.5 KiB
C#
Raw Normal View History

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<ChannelMapping> 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<ModbusDeviceProfile>(File.ReadAllText(resolved), JsonOptions)
?? throw new InvalidDataException("Modbus 设备配置为空。");
Validate(profile);
return profile;
}
public static void Validate(ModbusDeviceProfile p)
{
var errors = new List<string>();
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 必须完整覆盖 124 且不能重复");
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} 超出 1247");
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<int> offsets, List<string> 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 存在重复");
}
}