using Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.Models; using NModbus; using NModbus.IO; using Serilog; using System; using System.Collections.Generic; 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 object plcIoLock = new(); private readonly object adcIoLock = 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", "COM3", "COM4", 115200); private SlipDeviceSnapshot snapshot = SlipDeviceSnapshot.Offline(); private DateTime lastAdcErrorLoggedAt = DateTime.MinValue; private DateTime lastPlcErrorLoggedAt = DateTime.MinValue; private DateTime lastAdcDiagnosticLoggedAt = DateTime.MinValue; private DateTime lastAdcCalibrationWarningLoggedAt = DateTime.MinValue; private string lastAdcCalibrationWarning = string.Empty; 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 isAdcConnected; private bool isPlcConnected; private string adcLastError = string.Empty; private string plcLastError = string.Empty; public SlipDeviceSnapshot CurrentSnapshot { get { lock (sync) { return snapshot; } } } public void Start(DeviceSettings deviceSettings) { settings = deviceSettings; LogAdcCalibrationWarningIfNeeded(); 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; SetAllDisconnected(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); SetAllDisconnected(ex.Message); Stop(); } } public void UpdateSettings(DeviceSettings deviceSettings) { settings = deviceSettings; LogAdcCalibrationWarningIfNeeded(); 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 async Task ApplyOldLiftAsync() { await WriteCoilAsync(LowerCoil, false); await WriteCoilAsync(LiftCoil, true); Log.Information("提升按钮按老代码执行:M{LowerCoil}=0, M{LiftCoil}=1", LowerCoil, LiftCoil); } public async Task ApplyOldLowerAsync() { await WriteCoilAsync(LiftCoil, false); await WriteCoilAsync(LowerCoil, true); Log.Information("下降按钮按老代码执行:M{LiftCoil}=0, M{LowerCoil}=1", LiftCoil, LowerCoil); } public Task ToggleOldMoveLeftAsync() => ToggleCoilAsync(MoveLeftCoil); public Task ToggleOldMoveRightAsync() => ToggleCoilAsync(MoveRightCoil); public async Task StopAllMotionAsync() { await WriteCoilAsync(LiftCoil, false); await WriteCoilAsync(LowerCoil, false); await WriteCoilAsync(MoveLeftCoil, false); await WriteCoilAsync(MoveRightCoil, false); Log.Information("全部运动停止:M{LiftCoil}=0, M{LowerCoil}=0, M{MoveLeftCoil}=0, M{MoveRightCoil}=0", LiftCoil, LowerCoil, MoveLeftCoil, MoveRightCoil); } 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(() => { ushort[] manualSpeedWords; ushort[] manualDisplacementWords; ushort[] testSpeedWords; lock (plcIoLock) { var master = plcMaster ?? throw new InvalidOperationException("PLC 未连接"); manualSpeedWords = master.ReadHoldingRegisters(SlaveId, ManualSpeedRegister, 2); manualDisplacementWords = master.ReadHoldingRegisters(SlaveId, ManualDisplacementRegister, 2); 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; SetAllDisconnected(string.Empty); } 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 { ushort[] data; lock (adcIoLock) { var master = adcMaster; if (master is null) { return; } data = master.ReadHoldingRegisters(SlaveId, 0, 8); } var pressureRaw = UshortToInt(data[0], data[1]); var friction1Raw = UshortToInt(data[2], data[3]); var friction2Raw = UshortToInt(data[6], data[7]); var conversion = ConvertAdcReadings(pressureRaw, friction1Raw, friction2Raw); LogAdcDiagnostic(pressureRaw, friction1Raw, friction2Raw, conversion); lock (sync) { pressureRawValue = pressureRaw; frictionRawValue1 = friction1Raw; frictionRawValue2 = friction2Raw; hasAdcRawValues = true; if (conversion.IsValid) { verticalLoadN = conversion.Pressure; horizontalFrictionN = conversion.Friction; adcLastError = string.Empty; isAdcConnected = true; } else { verticalLoadN = 0; horizontalFrictionN = 0; adcLastError = conversion.Error; isAdcConnected = false; } RefreshSnapshotLocked(); } Thread.Sleep(conversion.IsValid ? 10 : 100); } catch (Exception ex) { LogPollingError("ADC", ex, ref lastAdcErrorLoggedAt); SetAdcConnected(false, ex.Message); Thread.Sleep(250); } } } private void PollPlc(CancellationToken token) { while (!token.IsCancellationRequested) { try { ushort[] displacementWords; bool[] coils; lock (plcIoLock) { var master = plcMaster; if (master is null) { return; } displacementWords = master.ReadHoldingRegisters(SlaveId, PlcDisplacementRegister, 2); 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]; plcLastError = string.Empty; isPlcConnected = true; RefreshSnapshotLocked(); } Thread.Sleep(10); } catch (Exception ex) { LogPollingError("PLC", ex, ref lastPlcErrorLoggedAt); SetPlcConnected(false, ex.Message); Thread.Sleep(250); } } } private Task PulseCoilAsync(ushort coil) => Task.Run(() => { lock (plcIoLock) { var master = plcMaster ?? throw new InvalidOperationException("PLC 未连接"); Log.Information("按老代码逻辑发送 PLC 脉冲线圈:M{Coil}=true -> false", coil); master.WriteSingleCoil(SlaveId, coil, true); Log.Debug("写入 PLC 线圈:M{Coil}=true", coil); master.WriteSingleCoil(SlaveId, coil, false); Log.Debug("写入 PLC 线圈:M{Coil}=false", coil); } }); private Task ToggleCoilAsync(ushort coil) => Task.Run(() => { lock (plcIoLock) { 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(() => { lock (plcIoLock) { 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(() => { lock (plcIoLock) { 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 SetAdcConnected(bool connected, string error) { lock (sync) { isAdcConnected = connected; adcLastError = error; RefreshSnapshotLocked(); } } private void SetPlcConnected(bool connected, string error) { lock (sync) { isPlcConnected = connected; plcLastError = error; if (!connected) { isTestRunning = false; isResetting = false; } RefreshSnapshotLocked(); } } private void SetAllDisconnected(string error) { lock (sync) { isAdcConnected = false; isPlcConnected = false; adcLastError = error; plcLastError = error; isTestRunning = false; isResetting = false; RefreshSnapshotLocked(); } } private void RefreshSnapshotLocked() { var connected = isAdcConnected && isPlcConnected; var error = string.Empty; if (!isAdcConnected && !string.IsNullOrWhiteSpace(adcLastError)) { error = "ADC: " + adcLastError; } if (!isPlcConnected && !string.IsNullOrWhiteSpace(plcLastError)) { error = string.IsNullOrWhiteSpace(error) ? "PLC: " + plcLastError : error + "; PLC: " + plcLastError; } snapshot = new SlipDeviceSnapshot( DateTime.Now, verticalLoadN, horizontalFrictionN, displacementMm, isTestRunning, isResetting, connected, error); } private AdcConversionResult ConvertAdcReadings(int pressureRaw, int friction1Raw, int friction2Raw) { if (!ValidateAdcSettings(out var pressureZero, out var pressureCoefficient, out var frictionZero1, out var frictionCoefficient1, out var frictionZero2, out var frictionCoefficient2, out var error)) { return AdcConversionResult.Invalid(error); } var pressure = ConvertAdc(pressureRaw, pressureZero, pressureCoefficient); // Keep each settings row paired with the same ADC channel used by the legacy zero-capture button. var friction1 = ConvertAdc(friction1Raw, frictionZero1, frictionCoefficient1); var friction2 = ConvertAdc(friction2Raw, frictionZero2, frictionCoefficient2); var friction = (friction1 + friction2) * -1.0; return AdcConversionResult.Valid(pressure, friction1, friction2, friction); } private bool ValidateAdcSettings( out double pressureZero, out double pressureCoefficient, out double frictionZero1, out double frictionCoefficient1, out double frictionZero2, out double frictionCoefficient2, out string error) { var invalid = new List(); TryParseSetting(settings.NormalPressureZero, "正压力零点", false, invalid, out pressureZero); TryParseSetting(settings.NormalPressureCoefficient, "正压力校准系数", true, invalid, out pressureCoefficient); TryParseSetting(settings.FrictionZero1, "摩擦1零点", false, invalid, out frictionZero1); TryParseSetting(settings.FrictionCoefficient1, "摩擦1校准系数", true, invalid, out frictionCoefficient1); TryParseSetting(settings.FrictionZero2, "摩擦2零点", false, invalid, out frictionZero2); TryParseSetting(settings.FrictionCoefficient2, "摩擦2校准系数", true, invalid, out frictionCoefficient2); if (invalid.Count == 0) { error = string.Empty; return true; } error = "ADC 校准参数无效:" + string.Join("、", invalid) + ";请填写旧机实际标定值后再测试"; return false; } private void LogAdcCalibrationWarningIfNeeded() { if (!ValidateAdcSettings(out _, out _, out _, out _, out _, out _, out var error)) { var now = DateTime.UtcNow; if (error == lastAdcCalibrationWarning && now - lastAdcCalibrationWarningLoggedAt < TimeSpan.FromSeconds(5)) { return; } lastAdcCalibrationWarning = error; lastAdcCalibrationWarningLoggedAt = now; Log.Warning( "{Error}. CurrentSettings: NormalPressureZero={NormalPressureZero}, NormalPressureCoefficient={NormalPressureCoefficient}, FrictionZero1={FrictionZero1}, FrictionCoefficient1={FrictionCoefficient1}, FrictionZero2={FrictionZero2}, FrictionCoefficient2={FrictionCoefficient2}", error, settings.NormalPressureZero, settings.NormalPressureCoefficient, settings.FrictionZero1, settings.FrictionCoefficient1, settings.FrictionZero2, settings.FrictionCoefficient2); return; } lastAdcCalibrationWarning = string.Empty; } private void LogAdcDiagnostic(int pressureRaw, int friction1Raw, int friction2Raw, AdcConversionResult conversion) { var now = DateTime.UtcNow; if (now - lastAdcDiagnosticLoggedAt < TimeSpan.FromSeconds(conversion.IsValid ? 1 : 5)) { return; } lastAdcDiagnosticLoggedAt = now; if (conversion.IsValid) { Log.Debug( "ADC 采样:RawPressure={RawPressure}, RawFriction1={RawFriction1}, RawFriction2={RawFriction2}, Pressure={Pressure:F3} N, Friction1={Friction1:F3} N, Friction2={Friction2:F3} N, Friction={Friction:F3} N, Coefficients=[P:{PressureCoefficient}, F1:{FrictionCoefficient1}, F2:{FrictionCoefficient2}], Zeros=[P:{PressureZero}, F1:{FrictionZero1}, F2:{FrictionZero2}]", pressureRaw, friction1Raw, friction2Raw, conversion.Pressure, conversion.Friction1, conversion.Friction2, conversion.Friction, settings.NormalPressureCoefficient, settings.FrictionCoefficient1, settings.FrictionCoefficient2, settings.NormalPressureZero, settings.FrictionZero1, settings.FrictionZero2); } else { Log.Warning( "ADC 采样无效:{Error}. RawPressure={RawPressure}, RawFriction1={RawFriction1}, RawFriction2={RawFriction2}, Coefficients=[P:{PressureCoefficient}, F1:{FrictionCoefficient1}, F2:{FrictionCoefficient2}], Zeros=[P:{PressureZero}, F1:{FrictionZero1}, F2:{FrictionZero2}]", conversion.Error, pressureRaw, friction1Raw, friction2Raw, settings.NormalPressureCoefficient, settings.FrictionCoefficient1, settings.FrictionCoefficient2, settings.NormalPressureZero, settings.FrictionZero1, settings.FrictionZero2); } } private static double ConvertAdc(int rawValue, double zero, double coefficient) => (rawValue - zero) / coefficient; private static void TryParseSetting(string value, string label, bool requireNonZero, List invalid, out double numericValue) { if (!TryParseDouble(value, out numericValue)) { invalid.Add($"{label}=\"{value}\""); return; } if (requireNonZero && Math.Abs(numericValue) < 0.0001) { invalid.Add($"{label}=0"); } } private static bool TryParseDouble(string value, out double numericValue) => double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out numericValue) || double.TryParse(value, NumberStyles.Float, CultureInfo.CurrentCulture, out numericValue); 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() { } } private readonly record struct AdcConversionResult( bool IsValid, double Pressure, double Friction1, double Friction2, double Friction, string Error) { public static AdcConversionResult Valid(double pressure, double friction1, double friction2, double friction) => new(true, pressure, friction1, friction2, friction, string.Empty); public static AdcConversionResult Invalid(string error) => new(false, 0, 0, 0, 0, error); } } }