Files
FootwearTest-20260602/Footwear Test methodsfor wholeshoe Slipresistanceperformance/Services/SlipResistanceDeviceService.cs
2026-06-02 19:16:50 +08:00

610 lines
22 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.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", "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 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;
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;
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 StartLiftAsync()
{
await WriteCoilAsync(LowerCoil, false);
await WriteCoilAsync(LiftCoil, true);
Log.Information("提升按下运行M{LiftCoil}=1, M{LowerCoil}=0", LiftCoil, LowerCoil);
}
public async Task StopLiftAsync()
{
await WriteCoilAsync(LiftCoil, false);
Log.Information("提升松开停止M{LiftCoil}=0", LiftCoil);
}
public async Task StartLowerAsync()
{
await WriteCoilAsync(LiftCoil, false);
await WriteCoilAsync(LowerCoil, true);
Log.Information("下降按下运行M{LowerCoil}=1, M{LiftCoil}=0", LowerCoil, LiftCoil);
}
public async Task StopLowerAsync()
{
await WriteCoilAsync(LowerCoil, false);
Log.Information("下降松开停止M{LowerCoil}=0", LowerCoil);
}
public async Task StartMoveLeftAsync()
{
await WriteCoilAsync(MoveRightCoil, false);
await WriteCoilAsync(MoveLeftCoil, true);
Log.Information("左移按下运行M{MoveLeftCoil}=1, M{MoveRightCoil}=0", MoveLeftCoil, MoveRightCoil);
}
public async Task StopMoveLeftAsync()
{
await WriteCoilAsync(MoveLeftCoil, false);
Log.Information("左移松开停止M{MoveLeftCoil}=0", MoveLeftCoil);
}
public async Task StartMoveRightAsync()
{
await WriteCoilAsync(MoveLeftCoil, false);
await WriteCoilAsync(MoveRightCoil, true);
Log.Information("右移按下运行M{MoveRightCoil}=1, M{MoveLeftCoil}=0", MoveRightCoil, MoveLeftCoil);
}
public async Task StopMoveRightAsync()
{
await WriteCoilAsync(MoveRightCoil, false);
Log.Information("右移松开停止M{MoveRightCoil}=0", 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[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;
adcLastError = string.Empty;
isAdcConnected = true;
RefreshSnapshotLocked();
}
Thread.Sleep(10);
}
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 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(() =>
{
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 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()
{
}
}
}
}