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