Files
FootwearTest-20260602/Footwear Test methodsfor wholeshoe Slipresistanceperformance/Services/SlipResistanceDeviceService.cs
2026-06-05 15:40:48 +08:00

725 lines
28 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 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<PlcDeviceParameters> 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<string>();
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<string> 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);
}
}
}