using Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.Models; using NModbus; using NModbus.IO; using Serilog; using System; using System.Globalization; using System.IO.Ports; using System.Threading; using System.Threading.Tasks; namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.Services { public sealed class SlipResistanceDeviceService : IDisposable { private const byte SlaveId = 1; private const ushort PlcDisplacementRegister = 402; private const ushort PlcStateCoilStart = 81; private const ushort StartTestCoil = 80; private const ushort StopTestCoil = 83; private const ushort ResetCoil = 90; private const ushort MoveLeftCoil = 1; private const ushort MoveRightCoil = 2; private const ushort LowerCoil = 4; private const ushort LiftCoil = 5; private const ushort ManualSpeedRegister = 302; private const ushort TestSpeedRegister = 310; private const ushort ManualDisplacementRegister = 320; private readonly object sync = new(); private readonly ModbusFactory modbusFactory = new(); private CancellationTokenSource? cancellationTokenSource; private Task? adcTask; private Task? plcTask; private SerialPort? plcPort; private SerialPort? adcPort; private IModbusSerialMaster? plcMaster; private IModbusSerialMaster? adcMaster; private DeviceSettings settings = new("0.00", "0.00", "0.30", "0", "0.00", "0", "0.00", "0", "0.00", "COM7", "COM8", 115200); private SlipDeviceSnapshot snapshot = SlipDeviceSnapshot.Offline(); private DateTime lastAdcErrorLoggedAt = DateTime.MinValue; private DateTime lastPlcErrorLoggedAt = DateTime.MinValue; private double verticalLoadN; private double horizontalFrictionN; private double displacementMm; private int pressureRawValue; private int frictionRawValue1; private int frictionRawValue2; private bool hasAdcRawValues; private bool isTestRunning; private bool isResetting; private bool isConnected; private string lastError = string.Empty; public SlipDeviceSnapshot CurrentSnapshot { get { lock (sync) { return snapshot; } } } public void Start(DeviceSettings deviceSettings) { settings = deviceSettings; Stop(); try { Log.Information( "启动防滑测试设备连接:PLC={PlcPort}, ADC={AdcPort}, BaudRate={BaudRate}, SlaveId={SlaveId}", settings.PlcPortName, settings.AdcPortName, settings.BaudRate, SlaveId); plcPort = CreatePort(settings.PlcPortName, settings.BaudRate); adcPort = CreatePort(settings.AdcPortName, settings.BaudRate); plcPort.Open(); adcPort.Open(); plcMaster = modbusFactory.CreateRtuMaster(new SerialPortResource(plcPort)); adcMaster = modbusFactory.CreateRtuMaster(new SerialPortResource(adcPort)); plcMaster.Transport.ReadTimeout = 2000; adcMaster.Transport.ReadTimeout = 2000; cancellationTokenSource = new CancellationTokenSource(); var token = cancellationTokenSource.Token; SetConnected(true, string.Empty); adcTask = Task.Run(() => PollAdc(token), token); plcTask = Task.Run(() => PollPlc(token), token); Log.Information("防滑测试设备连接成功,ADC/PLC 轮询已启动"); } catch (Exception ex) { Log.Error( ex, "防滑测试设备连接失败:PLC={PlcPort}, ADC={AdcPort}, BaudRate={BaudRate}", settings.PlcPortName, settings.AdcPortName, settings.BaudRate); SetConnected(false, ex.Message); Stop(); } } public void UpdateSettings(DeviceSettings deviceSettings) { settings = deviceSettings; Log.Debug( "设备设置已更新:PLC={PlcPort}, ADC={AdcPort}, BaudRate={BaudRate}, TestSpeed={TestSpeed}, ManualSpeed={ManualSpeed}, ManualDisplacement={ManualDisplacement}", settings.PlcPortName, settings.AdcPortName, settings.BaudRate, settings.TestSpeed, settings.ManualSpeed, settings.ManualDisplacement); } public Task PulseStartTestAsync() => PulseCoilAsync(StartTestCoil); public Task PulseStopTestAsync() => PulseCoilAsync(StopTestCoil); public Task PulseResetAsync() => PulseCoilAsync(ResetCoil); public Task ToggleMoveLeftAsync() => ToggleCoilAsync(MoveLeftCoil); public Task ToggleMoveRightAsync() => ToggleCoilAsync(MoveRightCoil); public async Task LiftAsync() { await WriteCoilAsync(LowerCoil, false); await WriteCoilAsync(LiftCoil, true); } public async Task LowerAsync() { await WriteCoilAsync(LiftCoil, false); await WriteCoilAsync(LowerCoil, true); } public Task WriteManualSpeedAsync(double value) => WriteFloatRegisterAsync(ManualSpeedRegister, value); public Task WriteTestSpeedAsync(double value) => WriteFloatRegisterAsync(TestSpeedRegister, value); public Task WriteManualDisplacementAsync(double value) => WriteFloatRegisterAsync(ManualDisplacementRegister, value); public Task ReadDeviceParametersAsync() => Task.Run(() => { var master = plcMaster ?? throw new InvalidOperationException("PLC 未连接"); var manualSpeedWords = master.ReadHoldingRegisters(SlaveId, ManualSpeedRegister, 2); var manualDisplacementWords = master.ReadHoldingRegisters(SlaveId, ManualDisplacementRegister, 2); var testSpeedWords = master.ReadHoldingRegisters(SlaveId, TestSpeedRegister, 2); var parameters = new PlcDeviceParameters( UshortToFloat(manualSpeedWords[1], manualSpeedWords[0]), UshortToFloat(manualDisplacementWords[1], manualDisplacementWords[0]), UshortToFloat(testSpeedWords[1], testSpeedWords[0])); Log.Information( "读取 PLC 参数完成:D{ManualSpeedRegister}={ManualSpeed:F3}, D{ManualDisplacementRegister}={ManualDisplacement:F3}, D{TestSpeedRegister}={TestSpeed:F3}", ManualSpeedRegister, parameters.ManualSpeed, ManualDisplacementRegister, parameters.ManualDisplacement, TestSpeedRegister, parameters.TestSpeed); return parameters; }); public AdcZeroCalibration CaptureCurrentAdcZero() { lock (sync) { if (!hasAdcRawValues) { throw new InvalidOperationException("ADC 尚未读取到有效原始数据"); } Log.Information( "采集 ADC 零点:NormalPressureZero={NormalPressureZero}, FrictionZero1={FrictionZero1}, FrictionZero2={FrictionZero2}", pressureRawValue, frictionRawValue1, frictionRawValue2); return new AdcZeroCalibration(pressureRawValue, frictionRawValue1, frictionRawValue2); } } public void Stop() { Log.Information("停止防滑测试设备连接与轮询"); cancellationTokenSource?.Cancel(); try { adcTask?.Wait(200); plcTask?.Wait(200); } catch { } cancellationTokenSource?.Dispose(); cancellationTokenSource = null; adcTask = null; plcTask = null; plcMaster?.Dispose(); adcMaster?.Dispose(); plcMaster = null; adcMaster = null; ClosePort(plcPort); ClosePort(adcPort); plcPort = null; adcPort = null; } public void Dispose() { Stop(); } private static SerialPort CreatePort(string portName, int baudRate) => new(portName, baudRate, Parity.None, 8, StopBits.One) { ReadTimeout = 2000, WriteTimeout = 2000 }; private static void ClosePort(SerialPort? port) { if (port is null) { return; } try { if (port.IsOpen) { port.Close(); } } catch { } port.Dispose(); } private void PollAdc(CancellationToken token) { while (!token.IsCancellationRequested) { try { var master = adcMaster; if (master is null) { return; } var data = master.ReadHoldingRegisters(SlaveId, 0, 8); var pressureRaw = UshortToInt(data[0], data[1]); var friction1Raw = UshortToInt(data[6], data[7]); var friction2Raw = UshortToInt(data[2], data[3]); var pressure = ConvertAdc(pressureRaw, settings.NormalPressureZero, settings.NormalPressureCoefficient); // Keep the old instrument channel wiring: ADC 6/7 uses zero 1 with coefficient 2, // and ADC 2/3 uses zero 2 with coefficient 1. var friction1 = ConvertAdc(friction1Raw, settings.FrictionZero1, settings.FrictionCoefficient2); var friction2 = ConvertAdc(friction2Raw, settings.FrictionZero2, settings.FrictionCoefficient1); var friction = (friction1 + friction2) * -1.0; lock (sync) { pressureRawValue = pressureRaw; frictionRawValue1 = friction1Raw; frictionRawValue2 = friction2Raw; hasAdcRawValues = true; verticalLoadN = pressure; horizontalFrictionN = friction; lastError = string.Empty; isConnected = true; RefreshSnapshotLocked(); } Thread.Sleep(10); } catch (Exception ex) { LogPollingError("ADC", ex, ref lastAdcErrorLoggedAt); SetConnected(false, ex.Message); Thread.Sleep(250); } } } private void PollPlc(CancellationToken token) { while (!token.IsCancellationRequested) { try { var master = plcMaster; if (master is null) { return; } var displacementWords = master.ReadHoldingRegisters(SlaveId, PlcDisplacementRegister, 2); var coils = master.ReadCoils(SlaveId, PlcStateCoilStart, 10); lock (sync) { displacementMm = UshortToFloat(displacementWords[1], displacementWords[0]); isTestRunning = coils.Length > 0 && coils[0]; isResetting = coils.Length > 9 && coils[9]; lastError = string.Empty; isConnected = true; RefreshSnapshotLocked(); } Thread.Sleep(10); } catch (Exception ex) { LogPollingError("PLC", ex, ref lastPlcErrorLoggedAt); SetConnected(false, ex.Message); Thread.Sleep(250); } } } private async Task PulseCoilAsync(ushort coil) { Log.Information("发送 PLC 脉冲线圈:M{Coil}", coil); await WriteCoilAsync(coil, true); await Task.Delay(80); await WriteCoilAsync(coil, false); } private Task ToggleCoilAsync(ushort coil) => Task.Run(() => { var master = plcMaster ?? throw new InvalidOperationException("PLC 未连接"); var current = master.ReadCoils(SlaveId, coil, 1)[0]; master.WriteSingleCoil(SlaveId, coil, !current); Log.Information("切换 PLC 线圈:M{Coil}, Before={Before}, After={After}", coil, current, !current); }); private Task WriteCoilAsync(ushort coil, bool value) => Task.Run(() => { var master = plcMaster ?? throw new InvalidOperationException("PLC 未连接"); master.WriteSingleCoil(SlaveId, coil, value); Log.Debug("写入 PLC 线圈:M{Coil}={Value}", coil, value); }); private Task WriteFloatRegisterAsync(ushort register, double value) => Task.Run(() => { var master = plcMaster ?? throw new InvalidOperationException("PLC 未连接"); master.WriteMultipleRegisters(SlaveId, register, SplitFloatToUShortArray((float)value)); Log.Information("写入 PLC 浮点寄存器:D{Register}={Value:F3}", register, value); }); private void LogPollingError(string source, Exception exception, ref DateTime lastLoggedAt) { var now = DateTime.UtcNow; if (now - lastLoggedAt < TimeSpan.FromSeconds(5)) { return; } lastLoggedAt = now; Log.Error( exception, "{Source} 轮询失败:PLC={PlcPort}, ADC={AdcPort}, BaudRate={BaudRate}", source, settings.PlcPortName, settings.AdcPortName, settings.BaudRate); } private void SetConnected(bool connected, string error) { lock (sync) { isConnected = connected; lastError = error; RefreshSnapshotLocked(); } } private void RefreshSnapshotLocked() { snapshot = new SlipDeviceSnapshot( DateTime.Now, verticalLoadN, horizontalFrictionN, displacementMm, isTestRunning, isResetting, isConnected, lastError); } private static double ConvertAdc(int rawValue, string zeroText, string coefficientText) { var zero = ParseDouble(zeroText); var coefficient = ParseDouble(coefficientText); if (Math.Abs(coefficient) < 0.0001) { return 0; } return (rawValue - zero) / coefficient; } private static double ParseDouble(string value) => double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var invariantValue) ? invariantValue : double.TryParse(value, NumberStyles.Float, CultureInfo.CurrentCulture, out var localValue) ? localValue : 0; private static int UshortToInt(ushort first, ushort second) { var bytes = new byte[4]; BitConverter.GetBytes(first).CopyTo(bytes, 0); BitConverter.GetBytes(second).CopyTo(bytes, 2); return BitConverter.ToInt32(bytes, 0); } private static float UshortToFloat(ushort first, ushort second) { var intSign = first / 32768; var intSignRest = first % 32768; var intExponent = intSignRest / 128; var intExponentRest = intSignRest % 128; var digit = (float)(intExponentRest * 65536 + second) / 8388608; return (float)Math.Pow(-1, intSign) * (float)Math.Pow(2, intExponent - 127) * (digit + 1); } private static ushort[] SplitFloatToUShortArray(float value) { var bytes = BitConverter.GetBytes(value); return [ BitConverter.ToUInt16(bytes, 0), BitConverter.ToUInt16(bytes, 2) ]; } private sealed class SerialPortResource(SerialPort serialPort) : IStreamResource { public int InfiniteTimeout => SerialPort.InfiniteTimeout; public int ReadTimeout { get => serialPort.ReadTimeout; set => serialPort.ReadTimeout = value; } public int WriteTimeout { get => serialPort.WriteTimeout; set => serialPort.WriteTimeout = value; } public void DiscardInBuffer() => serialPort.DiscardInBuffer(); public int Read(byte[] buffer, int offset, int count) => serialPort.Read(buffer, offset, count); public void Write(byte[] buffer, int offset, int count) => serialPort.Write(buffer, offset, count); public void Dispose() { } } } }