2026-06-02 18:14:01 +08:00
|
|
|
|
using Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.Models;
|
|
|
|
|
|
using NModbus;
|
|
|
|
|
|
using NModbus.IO;
|
2026-06-02 18:45:14 +08:00
|
|
|
|
using Serilog;
|
2026-06-02 18:14:01 +08:00
|
|
|
|
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();
|
2026-06-02 18:45:14 +08:00
|
|
|
|
private DateTime lastAdcErrorLoggedAt = DateTime.MinValue;
|
|
|
|
|
|
private DateTime lastPlcErrorLoggedAt = DateTime.MinValue;
|
2026-06-02 18:14:01 +08:00
|
|
|
|
|
|
|
|
|
|
private double verticalLoadN;
|
|
|
|
|
|
private double horizontalFrictionN;
|
|
|
|
|
|
private double displacementMm;
|
2026-06-02 18:45:14 +08:00
|
|
|
|
private int pressureRawValue;
|
|
|
|
|
|
private int frictionRawValue1;
|
|
|
|
|
|
private int frictionRawValue2;
|
|
|
|
|
|
private bool hasAdcRawValues;
|
2026-06-02 18:14:01 +08:00
|
|
|
|
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
|
|
|
|
|
|
{
|
2026-06-02 18:45:14 +08:00
|
|
|
|
Log.Information(
|
|
|
|
|
|
"启动防滑测试设备连接:PLC={PlcPort}, ADC={AdcPort}, BaudRate={BaudRate}, SlaveId={SlaveId}",
|
|
|
|
|
|
settings.PlcPortName,
|
|
|
|
|
|
settings.AdcPortName,
|
|
|
|
|
|
settings.BaudRate,
|
|
|
|
|
|
SlaveId);
|
|
|
|
|
|
|
2026-06-02 18:14:01 +08:00
|
|
|
|
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);
|
2026-06-02 18:45:14 +08:00
|
|
|
|
Log.Information("防滑测试设备连接成功,ADC/PLC 轮询已启动");
|
2026-06-02 18:14:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
2026-06-02 18:45:14 +08:00
|
|
|
|
Log.Error(
|
|
|
|
|
|
ex,
|
|
|
|
|
|
"防滑测试设备连接失败:PLC={PlcPort}, ADC={AdcPort}, BaudRate={BaudRate}",
|
|
|
|
|
|
settings.PlcPortName,
|
|
|
|
|
|
settings.AdcPortName,
|
|
|
|
|
|
settings.BaudRate);
|
2026-06-02 18:14:01 +08:00
|
|
|
|
SetConnected(false, ex.Message);
|
|
|
|
|
|
Stop();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public void UpdateSettings(DeviceSettings deviceSettings)
|
|
|
|
|
|
{
|
|
|
|
|
|
settings = deviceSettings;
|
2026-06-02 18:45:14 +08:00
|
|
|
|
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);
|
2026-06-02 18:14:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public Task PulseStartTestAsync() => PulseCoilAsync(StartTestCoil);
|
|
|
|
|
|
|
|
|
|
|
|
public Task PulseStopTestAsync() => PulseCoilAsync(StopTestCoil);
|
|
|
|
|
|
|
|
|
|
|
|
public Task PulseResetAsync() => PulseCoilAsync(ResetCoil);
|
|
|
|
|
|
|
2026-06-02 18:53:31 +08:00
|
|
|
|
public async Task StartLiftAsync()
|
2026-06-02 18:14:01 +08:00
|
|
|
|
{
|
|
|
|
|
|
await WriteCoilAsync(LowerCoil, false);
|
|
|
|
|
|
await WriteCoilAsync(LiftCoil, true);
|
2026-06-02 18:53:31 +08:00
|
|
|
|
Log.Information("提升按下运行:M{LiftCoil}=1, M{LowerCoil}=0", LiftCoil, LowerCoil);
|
2026-06-02 18:14:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-02 18:53:31 +08:00
|
|
|
|
public async Task StopLiftAsync()
|
|
|
|
|
|
{
|
|
|
|
|
|
await WriteCoilAsync(LiftCoil, false);
|
|
|
|
|
|
Log.Information("提升松开停止:M{LiftCoil}=0", LiftCoil);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public async Task StartLowerAsync()
|
2026-06-02 18:14:01 +08:00
|
|
|
|
{
|
|
|
|
|
|
await WriteCoilAsync(LiftCoil, false);
|
|
|
|
|
|
await WriteCoilAsync(LowerCoil, true);
|
2026-06-02 18:53:31 +08:00
|
|
|
|
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);
|
2026-06-02 18:14:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public Task WriteManualSpeedAsync(double value) => WriteFloatRegisterAsync(ManualSpeedRegister, value);
|
|
|
|
|
|
|
|
|
|
|
|
public Task WriteTestSpeedAsync(double value) => WriteFloatRegisterAsync(TestSpeedRegister, value);
|
|
|
|
|
|
|
|
|
|
|
|
public Task WriteManualDisplacementAsync(double value) => WriteFloatRegisterAsync(ManualDisplacementRegister, value);
|
|
|
|
|
|
|
2026-06-02 18:45:14 +08:00
|
|
|
|
public Task<PlcDeviceParameters> 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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-02 18:14:01 +08:00
|
|
|
|
public void Stop()
|
|
|
|
|
|
{
|
2026-06-02 18:45:14 +08:00
|
|
|
|
Log.Information("停止防滑测试设备连接与轮询");
|
2026-06-02 18:14:01 +08:00
|
|
|
|
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)
|
|
|
|
|
|
{
|
2026-06-02 18:45:14 +08:00
|
|
|
|
pressureRawValue = pressureRaw;
|
|
|
|
|
|
frictionRawValue1 = friction1Raw;
|
|
|
|
|
|
frictionRawValue2 = friction2Raw;
|
|
|
|
|
|
hasAdcRawValues = true;
|
2026-06-02 18:14:01 +08:00
|
|
|
|
verticalLoadN = pressure;
|
|
|
|
|
|
horizontalFrictionN = friction;
|
|
|
|
|
|
lastError = string.Empty;
|
|
|
|
|
|
isConnected = true;
|
|
|
|
|
|
RefreshSnapshotLocked();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Thread.Sleep(10);
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
2026-06-02 18:45:14 +08:00
|
|
|
|
LogPollingError("ADC", ex, ref lastAdcErrorLoggedAt);
|
2026-06-02 18:14:01 +08:00
|
|
|
|
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)
|
|
|
|
|
|
{
|
2026-06-02 18:45:14 +08:00
|
|
|
|
LogPollingError("PLC", ex, ref lastPlcErrorLoggedAt);
|
2026-06-02 18:14:01 +08:00
|
|
|
|
SetConnected(false, ex.Message);
|
|
|
|
|
|
Thread.Sleep(250);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private async Task PulseCoilAsync(ushort coil)
|
|
|
|
|
|
{
|
2026-06-02 18:45:14 +08:00
|
|
|
|
Log.Information("发送 PLC 脉冲线圈:M{Coil}", coil);
|
2026-06-02 18:14:01 +08:00
|
|
|
|
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);
|
2026-06-02 18:45:14 +08:00
|
|
|
|
Log.Information("切换 PLC 线圈:M{Coil}, Before={Before}, After={After}", coil, current, !current);
|
2026-06-02 18:14:01 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
private Task WriteCoilAsync(ushort coil, bool value) =>
|
|
|
|
|
|
Task.Run(() =>
|
|
|
|
|
|
{
|
|
|
|
|
|
var master = plcMaster ?? throw new InvalidOperationException("PLC 未连接");
|
|
|
|
|
|
master.WriteSingleCoil(SlaveId, coil, value);
|
2026-06-02 18:45:14 +08:00
|
|
|
|
Log.Debug("写入 PLC 线圈:M{Coil}={Value}", coil, value);
|
2026-06-02 18:14:01 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
private Task WriteFloatRegisterAsync(ushort register, double value) =>
|
|
|
|
|
|
Task.Run(() =>
|
|
|
|
|
|
{
|
|
|
|
|
|
var master = plcMaster ?? throw new InvalidOperationException("PLC 未连接");
|
|
|
|
|
|
master.WriteMultipleRegisters(SlaveId, register, SplitFloatToUShortArray((float)value));
|
2026-06-02 18:45:14 +08:00
|
|
|
|
Log.Information("写入 PLC 浮点寄存器:D{Register}={Value:F3}", register, value);
|
2026-06-02 18:14:01 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-06-02 18:45:14 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-02 18:14:01 +08:00
|
|
|
|
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()
|
|
|
|
|
|
{
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|