Files
Axi_Omron/src/AxiOmron.PcbCheck/Services/Implementations/ModbusTcpPlcService.cs
yunxiao.zhu d70b94e904 feat(*): 添加扫码枪启动探活、全局退出助手及 README
- 添加扫码枪串口启动探活,检测端口占用并更新 UI 状态
- 新增 ShutdownHelper 安全停止 Host 扩展方法
- 新增 README.md 项目说明文档
- 更新 WorkflowHostedService 启动探活逻辑
- 补充 ShutdownHelper 与 WorkflowHostedService 单元测试
- 优化 DashboardPage 与 SystemSettingsPage 界面布局
- 调整 ModbusTcpPlcService 监控镜像读取逻辑
2026-04-19 14:29:07 +08:00

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;
}
}
}