* 创建 AxiOmron.PcbCheck 项目主框架及解决方案 * 添加 Dashboard 和系统设置页面 * 实现 Modbus TCP PLC、扫码枪、SFTP 查询等核心服务 * 集成 Andon 报警、工作流托管服务与日志配置 * 补充项目文档和 UI 设计规范
320 lines
10 KiB
C#
320 lines
10 KiB
C#
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.IO.Ports;
|
|
using System.Text;
|
|
using AxiOmron.PcbCheck.Models;
|
|
using AxiOmron.PcbCheck.Options;
|
|
using AxiOmron.PcbCheck.Services.Interfaces;
|
|
using AxiOmron.PcbCheck.Utils;
|
|
|
|
namespace AxiOmron.PcbCheck.Services.Implementations;
|
|
|
|
/// <summary>
|
|
/// 提供串口扫码枪的触发与读取能力。
|
|
/// </summary>
|
|
public sealed class SerialScannerService : IScannerService, IDisposable
|
|
{
|
|
private readonly ScannerOptions _options;
|
|
private readonly IAppLogger<SerialScannerService> _logger;
|
|
private readonly SemaphoreSlim _ioLock = new(1, 1);
|
|
private SerialPort? _serialPort;
|
|
private bool _disposed;
|
|
|
|
/// <summary>
|
|
/// 初始化扫码枪服务。
|
|
/// </summary>
|
|
/// <param name="config">应用根配置。</param>
|
|
/// <param name="logger">日志记录器。</param>
|
|
public SerialScannerService(AppConfig config, IAppLogger<SerialScannerService> logger)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(config);
|
|
_options = config.Scanner;
|
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
|
}
|
|
|
|
/// <summary>
|
|
/// 触发一次扫码。
|
|
/// </summary>
|
|
/// <param name="cancellationToken">取消令牌。</param>
|
|
/// <returns>扫码结果。</returns>
|
|
public async Task<ScanOperationResult> TriggerScanAsync(CancellationToken cancellationToken)
|
|
{
|
|
var lockTaken = false;
|
|
try
|
|
{
|
|
await _ioLock.WaitAsync(cancellationToken).ConfigureAwait(false);
|
|
lockTaken = true;
|
|
return await TriggerScanInternalAsync(cancellationToken).ConfigureAwait(false);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "扫码操作失败");
|
|
return new ScanOperationResult
|
|
{
|
|
IsSuccess = false,
|
|
IsSystemError = ex is not OperationCanceledException,
|
|
DeviceConnected = false,
|
|
ErrorMessage = ex.Message
|
|
};
|
|
}
|
|
finally
|
|
{
|
|
if (lockTaken)
|
|
{
|
|
_ioLock.Release();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 测试扫码枪连接。
|
|
/// </summary>
|
|
/// <param name="cancellationToken">取消令牌。</param>
|
|
/// <returns>连接正常返回 <see langword="true"/>;否则返回 <see langword="false"/>。</returns>
|
|
public async Task<bool> TestConnectionAsync(CancellationToken cancellationToken)
|
|
{
|
|
var lockTaken = false;
|
|
try
|
|
{
|
|
await _ioLock.WaitAsync(cancellationToken).ConfigureAwait(false);
|
|
lockTaken = true;
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
EnsurePortOpen();
|
|
return _serialPort is { IsOpen: true };
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "扫码枪连接测试失败");
|
|
ClosePortUnsafe();
|
|
return false;
|
|
}
|
|
finally
|
|
{
|
|
if (lockTaken)
|
|
{
|
|
_ioLock.Release();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 主动断开并重建扫码枪连接。
|
|
/// </summary>
|
|
/// <param name="cancellationToken">取消令牌。</param>
|
|
/// <returns>表示重连完成的任务。</returns>
|
|
public async Task ForceReconnectAsync(CancellationToken cancellationToken)
|
|
{
|
|
var lockTaken = false;
|
|
try
|
|
{
|
|
await _ioLock.WaitAsync(cancellationToken).ConfigureAwait(false);
|
|
lockTaken = true;
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
ClosePortUnsafe();
|
|
EnsurePortOpen();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "扫码枪强制重连失败");
|
|
ClosePortUnsafe();
|
|
}
|
|
finally
|
|
{
|
|
if (lockTaken)
|
|
{
|
|
_ioLock.Release();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 释放扫码枪串口资源。
|
|
/// </summary>
|
|
public void Dispose()
|
|
{
|
|
if (_disposed)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_disposed = true;
|
|
ClosePortUnsafe();
|
|
_ioLock.Dispose();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 在异步上下文中执行一次扫码流程。
|
|
/// </summary>
|
|
/// <param name="cancellationToken">取消令牌。</param>
|
|
/// <returns>扫码结果。</returns>
|
|
private async Task<ScanOperationResult> TriggerScanInternalAsync(CancellationToken cancellationToken)
|
|
{
|
|
var stopwatch = Stopwatch.StartNew();
|
|
try
|
|
{
|
|
EnsurePortOpen();
|
|
var port = _serialPort ?? throw new InvalidOperationException("扫码枪串口尚未初始化。");
|
|
port.DiscardInBuffer();
|
|
port.DiscardOutBuffer();
|
|
port.Write(StringEscapeHelper.Unescape(_options.TriggerCommand));
|
|
|
|
var rawMessage = await ReadUntilTerminatorAsync(
|
|
port,
|
|
StringEscapeHelper.Unescape(_options.ResponseTerminator),
|
|
_options.ReadTimeoutMs,
|
|
cancellationToken).ConfigureAwait(false);
|
|
var barcode = BarcodeCleaner.Clean(rawMessage);
|
|
if (string.IsNullOrEmpty(barcode))
|
|
{
|
|
return new ScanOperationResult
|
|
{
|
|
IsSuccess = false,
|
|
DeviceConnected = true,
|
|
RawMessage = rawMessage,
|
|
ErrorMessage = "扫码返回空字符串或仅包含控制字符。",
|
|
DurationMs = stopwatch.ElapsedMilliseconds
|
|
};
|
|
}
|
|
|
|
return new ScanOperationResult
|
|
{
|
|
IsSuccess = true,
|
|
DeviceConnected = true,
|
|
Barcode = barcode,
|
|
RawMessage = rawMessage,
|
|
DurationMs = stopwatch.ElapsedMilliseconds
|
|
};
|
|
}
|
|
catch (TimeoutException ex)
|
|
{
|
|
_logger.LogWarning(ex, "扫码等待超时");
|
|
return new ScanOperationResult
|
|
{
|
|
IsSuccess = false,
|
|
DeviceConnected = true,
|
|
ErrorMessage = "扫码超时。",
|
|
DurationMs = stopwatch.ElapsedMilliseconds
|
|
};
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
throw;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "扫码枪执行失败");
|
|
ClosePortUnsafe();
|
|
return new ScanOperationResult
|
|
{
|
|
IsSuccess = false,
|
|
IsSystemError = true,
|
|
DeviceConnected = false,
|
|
ErrorMessage = ex.Message,
|
|
DurationMs = stopwatch.ElapsedMilliseconds
|
|
};
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 确保串口已打开并按当前配置初始化。
|
|
/// </summary>
|
|
private void EnsurePortOpen()
|
|
{
|
|
if (_serialPort is { IsOpen: true })
|
|
{
|
|
return;
|
|
}
|
|
|
|
ClosePortUnsafe();
|
|
|
|
var availablePorts = SerialPort.GetPortNames();
|
|
if (!availablePorts.Contains(_options.PortName, StringComparer.OrdinalIgnoreCase))
|
|
{
|
|
_logger.LogWarning("本地不存在串口 {PortName},可用串口: {AvailablePorts}", false, _options.PortName, string.Join(", ", availablePorts));
|
|
throw new IOException($"串口 {_options.PortName} 不存在。可用串口: {string.Join(", ", availablePorts)}");
|
|
}
|
|
|
|
var parity = Enum.Parse<Parity>(_options.Parity, true);
|
|
var stopBits = Enum.Parse<StopBits>(_options.StopBits, true);
|
|
var serialPort = new SerialPort(_options.PortName, _options.BaudRate, parity, _options.DataBits, stopBits)
|
|
{
|
|
ReadTimeout = 200,
|
|
WriteTimeout = 1000,
|
|
Encoding = Encoding.ASCII,
|
|
DtrEnable = true,
|
|
RtsEnable = true,
|
|
NewLine = StringEscapeHelper.Unescape(_options.ResponseTerminator)
|
|
};
|
|
|
|
try
|
|
{
|
|
serialPort.Open();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "打开串口 {PortName} 失败", false, _options.PortName);
|
|
serialPort.Dispose();
|
|
throw;
|
|
}
|
|
|
|
_serialPort = serialPort;
|
|
_logger.LogInformation("已连接扫码枪串口 {PortName}", false, _options.PortName);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 从串口读取直到遇到终止符或超时。
|
|
/// </summary>
|
|
/// <param name="port">串口实例。</param>
|
|
/// <param name="terminator">终止符。</param>
|
|
/// <param name="timeoutMs">总超时时间,单位为毫秒。</param>
|
|
/// <param name="cancellationToken">取消令牌。</param>
|
|
/// <returns>读取到的原始字符串。</returns>
|
|
private static async Task<string> ReadUntilTerminatorAsync(SerialPort port, string terminator, int timeoutMs, CancellationToken cancellationToken)
|
|
{
|
|
var stopwatch = Stopwatch.StartNew();
|
|
var buffer = new StringBuilder();
|
|
|
|
while (stopwatch.ElapsedMilliseconds < timeoutMs)
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
var fragment = port.ReadExisting();
|
|
if (!string.IsNullOrEmpty(fragment))
|
|
{
|
|
buffer.Append(fragment);
|
|
if (string.IsNullOrEmpty(terminator) || buffer.ToString().Contains(terminator, StringComparison.Ordinal))
|
|
{
|
|
return buffer.ToString();
|
|
}
|
|
}
|
|
|
|
await Task.Delay(20, cancellationToken).ConfigureAwait(false);
|
|
}
|
|
|
|
throw new TimeoutException("扫码枪在规定时间内未返回完整报文。");
|
|
}
|
|
|
|
/// <summary>
|
|
/// 关闭当前串口并释放资源。
|
|
/// </summary>
|
|
private void ClosePortUnsafe()
|
|
{
|
|
try
|
|
{
|
|
if (_serialPort?.IsOpen == true)
|
|
{
|
|
_serialPort.Close();
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// 忽略关闭异常。
|
|
}
|
|
finally
|
|
{
|
|
_serialPort?.Dispose();
|
|
_serialPort = null;
|
|
}
|
|
}
|
|
}
|
|
|