- 添加扫码枪串口启动探活,检测端口占用并更新 UI 状态 - 新增 ShutdownHelper 安全停止 Host 扩展方法 - 新增 README.md 项目说明文档 - 更新 WorkflowHostedService 启动探活逻辑 - 补充 ShutdownHelper 与 WorkflowHostedService 单元测试 - 优化 DashboardPage 与 SystemSettingsPage 界面布局 - 调整 ModbusTcpPlcService 监控镜像读取逻辑
312 lines
10 KiB
C#
312 lines
10 KiB
C#
using AxiOmron.PcbCheck.Models;
|
|
using AxiOmron.PcbCheck.Options;
|
|
using AxiOmron.PcbCheck.Services.Interfaces;
|
|
using IoTClient.Clients.Modbus;
|
|
using IoTClient.Enums;
|
|
namespace AxiOmron.PcbCheck.Services.Implementations;
|
|
|
|
/// <summary>
|
|
/// 提供基于 IoTClient ModbusTcpClient 的 PLC 读写能力。
|
|
/// </summary>
|
|
public sealed class ModbusTcpPlcService : IPlcService, IDisposable
|
|
{
|
|
private readonly PlcOptions _options;
|
|
private readonly IAppLogger<ModbusTcpPlcService> _logger;
|
|
private readonly SemaphoreSlim _ioLock = new(1, 1);
|
|
private ModbusTcpClient? _client;
|
|
private PlcProcessState? _lastWrittenState;
|
|
private bool _disposed;
|
|
|
|
/// <summary>
|
|
/// 初始化 PLC 通信服务。
|
|
/// </summary>
|
|
/// <param name="config">应用根配置。</param>
|
|
/// <param name="logger">日志记录器。</param>
|
|
public ModbusTcpPlcService(AppConfig config, IAppLogger<ModbusTcpPlcService> logger)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(config);
|
|
_options = config.Plc;
|
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
|
}
|
|
|
|
/// <summary>
|
|
/// 读取 PLC 输入信号快照。
|
|
/// </summary>
|
|
/// <param name="cancellationToken">取消令牌。</param>
|
|
/// <returns>输入信号快照。</returns>
|
|
public async Task<PlcSignalSnapshot> ReadSignalsAsync(CancellationToken cancellationToken)
|
|
{
|
|
await _ioLock.WaitAsync(cancellationToken).ConfigureAwait(false);
|
|
try
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
EnsureConnected();
|
|
|
|
return new PlcSignalSnapshot
|
|
{
|
|
PcbArrived = ReadDiscrete(_options.Inputs.PcbArrived),
|
|
PlcReset = ReadDiscrete(_options.Inputs.PlcReset),
|
|
PlcAckRelease = ReadDiscrete(_options.Inputs.PlcAckRelease),
|
|
CapturedAt = DateTimeOffset.Now
|
|
};
|
|
}
|
|
catch
|
|
{
|
|
DisconnectUnsafe();
|
|
throw;
|
|
}
|
|
finally
|
|
{
|
|
_ioLock.Release();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 读取 PLC 当前监控镜像,包括输入、输出和寄存器的实际值。
|
|
/// </summary>
|
|
/// <param name="cancellationToken">取消令牌。</param>
|
|
/// <returns>监控镜像快照。</returns>
|
|
public async Task<PlcMonitorSnapshot> ReadMonitorSnapshotAsync(CancellationToken cancellationToken)
|
|
{
|
|
await _ioLock.WaitAsync(cancellationToken).ConfigureAwait(false);
|
|
try
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
EnsureConnected();
|
|
|
|
DateTimeOffset capturedAt = DateTimeOffset.Now;
|
|
return new PlcMonitorSnapshot
|
|
{
|
|
Inputs = new PlcSignalSnapshot
|
|
{
|
|
PcbArrived = ReadDiscrete(_options.Inputs.PcbArrived),
|
|
PlcReset = ReadDiscrete(_options.Inputs.PlcReset),
|
|
PlcAckRelease = ReadDiscrete(_options.Inputs.PlcAckRelease),
|
|
CapturedAt = capturedAt
|
|
},
|
|
Outputs = new PlcProcessState
|
|
{
|
|
PcBusy = ReadCoil(_options.Outputs.PcBusy),
|
|
ReleasePermit = ReadCoil(_options.Outputs.ReleasePermit),
|
|
ResultCode = ReadRegister(_options.Registers.ResultCode)
|
|
},
|
|
CapturedAt = capturedAt
|
|
};
|
|
}
|
|
catch
|
|
{
|
|
DisconnectUnsafe();
|
|
throw;
|
|
}
|
|
finally
|
|
{
|
|
_ioLock.Release();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 写入 PLC 输出状态与寄存器值。
|
|
/// </summary>
|
|
/// <param name="state">待写入的输出状态。</param>
|
|
/// <param name="cancellationToken">取消令牌。</param>
|
|
/// <returns>表示写入完成的任务。</returns>
|
|
public async Task WriteStateAsync(PlcProcessState state, CancellationToken cancellationToken)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(state);
|
|
|
|
await _ioLock.WaitAsync(cancellationToken).ConfigureAwait(false);
|
|
try
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
EnsureConnected();
|
|
WriteChangedState(state);
|
|
_lastWrittenState = state.Clone();
|
|
}
|
|
catch
|
|
{
|
|
DisconnectUnsafe();
|
|
throw;
|
|
}
|
|
finally
|
|
{
|
|
_ioLock.Release();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 主动断开并重建 PLC 连接。
|
|
/// </summary>
|
|
/// <param name="cancellationToken">取消令牌。</param>
|
|
/// <returns>表示重连完成的任务。</returns>
|
|
public async Task ForceReconnectAsync(CancellationToken cancellationToken)
|
|
{
|
|
await _ioLock.WaitAsync(cancellationToken).ConfigureAwait(false);
|
|
try
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
DisconnectUnsafe();
|
|
_lastWrittenState = null;
|
|
EnsureConnected();
|
|
}
|
|
finally
|
|
{
|
|
_ioLock.Release();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 释放 PLC 通信资源。
|
|
/// </summary>
|
|
public void Dispose()
|
|
{
|
|
if (_disposed)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_disposed = true;
|
|
DisconnectUnsafe();
|
|
_ioLock.Dispose();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 确保 IoTClient Modbus TCP 客户端已连接。
|
|
/// </summary>
|
|
private void EnsureConnected()
|
|
{
|
|
if (_client is { Connected: true })
|
|
{
|
|
return;
|
|
}
|
|
|
|
DisconnectUnsafe();
|
|
var client = new ModbusTcpClient(_options.Host, _options.Port, _options.ConnectTimeoutMs, EndianFormat.ABCD, false);
|
|
var openResult = client.Open();
|
|
EnsureSuccess(openResult.IsSucceed, openResult.Err, "连接 PLC 失败");
|
|
_client = client;
|
|
_logger.LogInformation("已通过 IoTClient 连接 PLC {Host}:{Port}", false, _options.Host, _options.Port);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 读取单个离散输入位。
|
|
/// </summary>
|
|
/// <param name="address">离散输入地址。</param>
|
|
/// <returns>读取到的布尔值。</returns>
|
|
private bool ReadDiscrete(int address)
|
|
{
|
|
var client = _client ?? throw new InvalidOperationException("PLC 客户端尚未初始化。");
|
|
var result = client.ReadDiscrete(address, _options.UnitId, 2);
|
|
EnsureSuccess(result.IsSucceed, result.Err, $"读取离散输入失败,地址={address}");
|
|
return result.Value;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 读取单个输出线圈位。
|
|
/// </summary>
|
|
/// <param name="address">线圈地址。</param>
|
|
/// <returns>读取到的布尔值。</returns>
|
|
private bool ReadCoil(int address)
|
|
{
|
|
var client = _client ?? throw new InvalidOperationException("PLC 客户端尚未初始化。");
|
|
var result = client.ReadCoil(address, _options.UnitId, 1);
|
|
EnsureSuccess(result.IsSucceed, result.Err, $"读取线圈失败,地址={address}");
|
|
return result.Value;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 读取单个保持寄存器。
|
|
/// </summary>
|
|
/// <param name="address">寄存器地址。</param>
|
|
/// <returns>读取到的寄存器值。</returns>
|
|
private ushort ReadRegister(int address)
|
|
{
|
|
var client = _client ?? throw new InvalidOperationException("PLC 客户端尚未初始化。");
|
|
var result = client.ReadUInt16(address, _options.UnitId, 3);
|
|
EnsureSuccess(result.IsSucceed, result.Err, $"读取保持寄存器失败,地址={address}");
|
|
return result.Value;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 写入所有发生变化的线圈位与寄存器。
|
|
/// </summary>
|
|
/// <param name="state">目标状态。</param>
|
|
private void WriteChangedState(PlcProcessState state)
|
|
{
|
|
var previous = _lastWrittenState;
|
|
|
|
WriteSingleCoilIfChanged(previous?.PcBusy, state.PcBusy, _options.Outputs.PcBusy);
|
|
WriteSingleCoilIfChanged(previous?.ReleasePermit, state.ReleasePermit, _options.Outputs.ReleasePermit);
|
|
|
|
WriteSingleRegisterIfChanged(previous?.ResultCode, state.ResultCode, _options.Registers.ResultCode);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 仅当值发生变化时写入单个线圈位。
|
|
/// </summary>
|
|
/// <param name="previous">上一值。</param>
|
|
/// <param name="current">当前值。</param>
|
|
/// <param name="address">线圈地址。</param>
|
|
private void WriteSingleCoilIfChanged(bool? previous, bool current, int address)
|
|
{
|
|
if (previous.HasValue && previous.Value == current)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var client = _client ?? throw new InvalidOperationException("PLC 客户端尚未初始化。");
|
|
var result = client.Write(address.ToString(), current, _options.UnitId, 5);
|
|
EnsureSuccess(result.IsSucceed, result.Err, $"写入线圈失败,地址={address},值={current}");
|
|
}
|
|
|
|
/// <summary>
|
|
/// 仅当值发生变化时写入单个保持寄存器。
|
|
/// </summary>
|
|
/// <param name="previous">上一值。</param>
|
|
/// <param name="current">当前值。</param>
|
|
/// <param name="address">保持寄存器地址。</param>
|
|
private void WriteSingleRegisterIfChanged(ushort? previous, ushort current, int address)
|
|
{
|
|
if (previous.HasValue && previous.Value == current)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var client = _client ?? throw new InvalidOperationException("PLC 客户端尚未初始化。");
|
|
var result = client.Write(address.ToString(), current, _options.UnitId);
|
|
EnsureSuccess(result.IsSucceed, result.Err, $"写入保持寄存器失败,地址={address},值={current}");
|
|
}
|
|
|
|
/// <summary>
|
|
/// 校验 IoTClient 调用结果是否成功。
|
|
/// </summary>
|
|
/// <param name="isSucceed">调用是否成功。</param>
|
|
/// <param name="error">错误消息。</param>
|
|
/// <param name="message">异常消息前缀。</param>
|
|
private static void EnsureSuccess(bool isSucceed, string? error, string message)
|
|
{
|
|
if (!isSucceed)
|
|
{
|
|
throw new InvalidOperationException(string.IsNullOrWhiteSpace(error) ? message : $"{message}: {error}");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 断开当前 PLC 客户端连接。
|
|
/// </summary>
|
|
private void DisconnectUnsafe()
|
|
{
|
|
try
|
|
{
|
|
_client?.Close();
|
|
}
|
|
catch
|
|
{
|
|
// 忽略关闭异常。
|
|
}
|
|
finally
|
|
{
|
|
_client = null;
|
|
}
|
|
}
|
|
}
|