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;
///
/// 提供串口扫码枪的触发与读取能力。
///
public sealed class SerialScannerService : IScannerService, IDisposable
{
private readonly ScannerOptions _options;
private readonly IAppLogger _logger;
private readonly SemaphoreSlim _ioLock = new(1, 1);
private SerialPort? _serialPort;
private bool _disposed;
///
/// 初始化扫码枪服务。
///
/// 应用根配置。
/// 日志记录器。
public SerialScannerService(AppConfig config, IAppLogger logger)
{
ArgumentNullException.ThrowIfNull(config);
_options = config.Scanner;
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
///
/// 触发一次扫码。
///
/// 取消令牌。
/// 扫码结果。
public async Task 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();
}
}
}
///
/// 测试扫码枪连接。
///
/// 取消令牌。
/// 连接正常返回 ;否则返回 。
public async Task 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();
}
}
}
///
/// 主动断开并重建扫码枪连接。
///
/// 取消令牌。
/// 表示重连完成的任务。
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();
}
}
}
///
/// 释放扫码枪串口资源。
///
public void Dispose()
{
if (_disposed)
{
return;
}
_disposed = true;
ClosePortUnsafe();
_ioLock.Dispose();
}
///
/// 在异步上下文中执行一次扫码流程。
///
/// 取消令牌。
/// 扫码结果。
private async Task 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
};
}
}
///
/// 确保串口已打开并按当前配置初始化。
///
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(_options.Parity, true);
var stopBits = Enum.Parse(_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);
}
///
/// 从串口读取直到遇到终止符或超时。
///
/// 串口实例。
/// 终止符。
/// 总超时时间,单位为毫秒。
/// 取消令牌。
/// 读取到的原始字符串。
private static async Task 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("扫码枪在规定时间内未返回完整报文。");
}
///
/// 关闭当前串口并释放资源。
///
private void ClosePortUnsafe()
{
try
{
if (_serialPort?.IsOpen == true)
{
_serialPort.Close();
}
}
catch
{
// 忽略关闭异常。
}
finally
{
_serialPort?.Dispose();
_serialPort = null;
}
}
}