SSPC-Tester/SSPCTester.Devices/Config/ModbusDeviceProfile.cs
2026-06-30 12:10:29 +08:00

151 lines
6.5 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 存在重复");
}
}