using System.Diagnostics; using System.IO.Ports; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using SSPCTester.Devices.Config; using SSPCTester.Devices.Interfaces; using SSPCTester.Devices.Protocol; namespace SSPCTester.Devices.Drivers.Real; /// 按实测 Profile 实现的 24 路 Modbus-RTU SSPC 驱动。 public sealed class SspcModbus : ISspc, IDisposable { private readonly DeviceOptions _rootOptions; private readonly ILogger _logger; private readonly SemaphoreSlim _busLock = new(1, 1); private readonly bool[] _states = new bool[24]; private SerialPort? _port; private ModbusDeviceProfile? _profile; public SspcModbus(IOptions options, ILogger logger) { _rootOptions = options.Value; _logger = logger; } public int ChannelCount => 24; public bool IsConnected => _port?.IsOpen == true; public string DisplayName => "SSPC (Modbus-RTU)"; public event EventHandler? ConnectionChanged; public Task ConnectAsync(CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); if (IsConnected) return Task.FromResult(true); var options = _rootOptions.Sspc; _profile = ModbusDeviceProfileLoader.Load(options.DeviceProfilePath); var defaults = _profile.SerialDefaults; _port = new SerialPort( options.PortName, options.BaudRate > 0 ? options.BaudRate : defaults.BaudRate, ParseParity(options.Parity), options.DataBits > 0 ? options.DataBits : defaults.DataBits, ParseStopBits(options.StopBits)) { ReadTimeout = options.ReadTimeoutMs > 0 ? options.ReadTimeoutMs : defaults.ReadTimeoutMs, WriteTimeout = options.ReadTimeoutMs > 0 ? options.ReadTimeoutMs : defaults.ReadTimeoutMs, Handshake = Handshake.None }; _port.Open(); _logger.LogInformation("Modbus 已连接:{Port} {BaudRate}bps", _port.PortName, _port.BaudRate); ConnectionChanged?.Invoke(this, true); return Task.FromResult(true); } public async Task DisconnectAsync() { if (!IsConnected) return; if (_rootOptions.Sspc.CloseAllRelaysOnDisconnect) { for (int slot = 1; slot <= ChannelCount; slot++) { try { await TurnOffAsync(slot).ConfigureAwait(false); } catch (Exception ex) { _logger.LogWarning(ex, "断开前关闭 CH{Slot} 失败。", slot); } } } _port?.Close(); _port?.Dispose(); _port = null; ConnectionChanged?.Invoke(this, false); _logger.LogInformation("Modbus 已断开。"); } public Task TurnOnAsync(int physicalSlot, CancellationToken ct = default) => SetRelayAsync(physicalSlot, true, ct); public Task TurnOffAsync(int physicalSlot, CancellationToken ct = default) => SetRelayAsync(physicalSlot, false, ct); public async Task FlashAsync(int physicalSlot, int milliseconds, CancellationToken ct = default) { await TurnOnAsync(physicalSlot, ct).ConfigureAwait(false); try { await Task.Delay(milliseconds, ct).ConfigureAwait(false); } finally { await TurnOffAsync(physicalSlot, CancellationToken.None).ConfigureAwait(false); } } public Task GetChannelStateAsync(int physicalSlot, CancellationToken ct = default) { ValidateSlot(physicalSlot); return Task.FromResult(_states[physicalSlot - 1]); } public async Task ReadMeasurementAsync(int physicalSlot, CancellationToken ct = default) { ValidateSlot(physicalSlot); var all = await ReadMeasurementsAsync(ct).ConfigureAwait(false); return all[physicalSlot - 1]; } public async Task> ReadMeasurementsAsync(CancellationToken ct = default) { EnsureConnected(); var profile = _profile!; var voltageFrame = await ReadRegistersAsync(profile.Voltage, ct).ConfigureAwait(false); var currentFrame = await ReadRegistersAsync(profile.Current, ct).ConfigureAwait(false); var timestamp = DateTimeOffset.Now; return profile.Channels.OrderBy(x => x.PhysicalSlot).Select(mapping => { ushort vRaw = ReadUInt16(voltageFrame, profile.Voltage, mapping.Voltage.RegisterOffset); ushort currentBits = ReadUInt16(currentFrame, profile.Current, mapping.Current.RegisterOffset); short iRaw = unchecked((short)currentBits); return new ChannelMeasurement { PhysicalSlot = mapping.PhysicalSlot, VoltageRaw = vRaw, CurrentRaw = iRaw, Voltage = ModbusValueConverter.ConvertRaw( vRaw, profile.Voltage.Signed, profile.Voltage.Formula, profile.Voltage.Range), Current = Math.Abs(ModbusValueConverter.ConvertRaw( currentBits, profile.Current.Signed, profile.Current.Formula, profile.Current.Range)), Timestamp = timestamp }; }).ToArray(); } private async Task SetRelayAsync(int physicalSlot, bool isOn, CancellationToken ct) { ValidateSlot(physicalSlot); EnsureConnected(); var mapping = _profile!.Channels.Single(x => x.PhysicalSlot == physicalSlot); ushort value = isOn ? _profile.Relay.OnValueNumber : _profile.Relay.OffValueNumber; byte[] request = ModbusRtuProtocol.BuildRequest( mapping.Relay.Slave, _profile.Relay.FunctionCodeWrite, mapping.Relay.Coil, value); byte[] response = await TransactAsync(request, 8, ct).ConfigureAwait(false); if (_profile.Relay.VerifyEchoResponse && !response.SequenceEqual(request)) throw new InvalidDataException($"继电器 CH{physicalSlot} 回显与请求不一致。"); _states[physicalSlot - 1] = isOn; } private async Task ReadRegistersAsync(MeasurementProtocol protocol, CancellationToken ct) { byte[] request = ModbusRtuProtocol.BuildRequest( protocol.Slave, protocol.FunctionCodeRead, protocol.StartRegister, protocol.RegisterCount); int expectedLength = 3 + protocol.RegisterCount * 2 + 2; byte[] response = await TransactAsync(request, expectedLength, ct).ConfigureAwait(false); if (response[0] != protocol.Slave) throw new InvalidDataException("Modbus 响应从站地址错误。"); if (response[1] != protocol.FunctionCodeRead) throw new InvalidDataException("Modbus 响应功能码错误。"); if (response[2] != protocol.RegisterCount * 2) throw new InvalidDataException("Modbus 响应字节数错误。"); return response; } private async Task TransactAsync(byte[] request, int expectedLength, CancellationToken ct) { await _busLock.WaitAsync(ct).ConfigureAwait(false); try { int retry = Math.Max(0, _rootOptions.Sspc.Retry); Exception? last = null; for (int attempt = 0; attempt <= retry; attempt++) { ct.ThrowIfCancellationRequested(); try { var sw = Stopwatch.StartNew(); _port!.DiscardInBuffer(); _port.Write(request, 0, request.Length); byte[] response = ReadResponse(expectedLength, ct); ModbusRtuProtocol.ValidateCrc(response); if (response[0] != request[0]) throw new InvalidDataException("Modbus 响应从站与请求不一致。"); if ((response[1] & 0x80) != 0) throw new InvalidDataException($"Modbus 异常响应,异常码 0x{response[2]:X2}。"); LogFrame(request, response, sw.ElapsedMilliseconds, null); return response; } catch (Exception ex) when (ex is TimeoutException or InvalidDataException or IOException) { last = ex; LogFrame(request, null, 0, ex); try { _port?.DiscardInBuffer(); } catch { } } } throw new IOException($"Modbus 请求失败,已尝试 {retry + 1} 次。", last); } finally { _busLock.Release(); } } private byte[] ReadResponse(int normalLength, CancellationToken ct) { var bytes = new List(normalLength); while (bytes.Count < 3) { ct.ThrowIfCancellationRequested(); bytes.Add((byte)_port!.ReadByte()); } int length = (bytes[1] & 0x80) != 0 ? 5 : normalLength; while (bytes.Count < length) { ct.ThrowIfCancellationRequested(); bytes.Add((byte)_port!.ReadByte()); } return bytes.ToArray(); } private void LogFrame(byte[] request, byte[]? response, long elapsedMs, Exception? error) { if (!_rootOptions.Sspc.LogRawFrames && error == null) return; string tx = Convert.ToHexString(request); string rx = response == null ? "-" : Convert.ToHexString(response); if (error == null) _logger.LogInformation("Modbus TX={Tx} RX={Rx} {Elapsed}ms", tx, rx, elapsedMs); else _logger.LogWarning(error, "Modbus TX={Tx} RX={Rx} 失败", tx, rx); } private static ushort ReadUInt16(byte[] frame, MeasurementProtocol p, int registerOffset) { int position = p.DataStartOffset + registerOffset * p.ChannelBytes; if (position + 1 >= frame.Length - 2) throw new InvalidDataException("寄存器映射超出响应数据区。"); return (ushort)((frame[position] << 8) | frame[position + 1]); } private void EnsureConnected() { if (!IsConnected || _profile == null) throw new InvalidOperationException("Modbus 尚未连接。"); } private static void ValidateSlot(int slot) { if (slot is < 1 or > 24) throw new ArgumentOutOfRangeException(nameof(slot), slot, "物理通道范围必须为 1~24。"); } private static Parity ParseParity(string value) => Enum.TryParse(value, true, out var result) ? result : Parity.None; private static StopBits ParseStopBits(string value) => Enum.TryParse(value, true, out var result) ? result : StopBits.One; public void Dispose() { try { _port?.Dispose(); } catch { } _busLock.Dispose(); } }