151 lines
6.5 KiB
C#
151 lines
6.5 KiB
C#
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 必须完整覆盖 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<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 存在重复");
|
||
}
|
||
}
|