324 lines
10 KiB
C#
324 lines
10 KiB
C#
using System;
|
||
using System.IO.Ports;
|
||
using System.Text;
|
||
using System.Threading.Tasks;
|
||
|
||
namespace MesWork
|
||
{
|
||
/// <summary>
|
||
/// 单把扫码枪的串口通信封装
|
||
/// 包含数据接收(缓冲+空闲提交)、防抖、断线自动重连
|
||
/// </summary>
|
||
public class BarcodeScanner : IDisposable
|
||
{
|
||
// ── 基本属性 ──
|
||
/// <summary>关联工位号(OP10/OP20)</summary>
|
||
public string OpName { get; }
|
||
|
||
/// <summary>当前COM口</summary>
|
||
public string ComPort { get; private set; }
|
||
|
||
/// <summary>是否已连接</summary>
|
||
public bool IsConnected
|
||
{
|
||
get { try { return _serial != null && _serial.IsOpen; } catch { return false; } }
|
||
}
|
||
|
||
/// <summary>最近一次扫到的条码</summary>
|
||
public string LastBarcode { get; private set; } = "";
|
||
|
||
/// <summary>最近一次扫码时间</summary>
|
||
public DateTime? LastScanTime { get; private set; }
|
||
|
||
// ── 事件 ──
|
||
/// <summary>扫码完成事件(opName, barcode)</summary>
|
||
public event Action<string, string> BarcodeReceived;
|
||
|
||
/// <summary>日志事件(message),由外部接入系统日志</summary>
|
||
public event Action<string> Log;
|
||
|
||
// ── 私有字段 ──
|
||
private SerialPort _serial;
|
||
private readonly int _baudRate;
|
||
private readonly object _bufferLock = new object();
|
||
private readonly StringBuilder _buffer = new StringBuilder(256);
|
||
private System.Windows.Forms.Timer _flushTimer;
|
||
private const int FlushIntervalMs = 60;
|
||
|
||
// 防抖
|
||
private string _lastDebounceCode = "";
|
||
private DateTime _lastDebounceTime = DateTime.MinValue;
|
||
private const int DebounceMs = 500;
|
||
|
||
// 断线重连
|
||
private System.Windows.Forms.Timer _reconnectTimer;
|
||
private volatile bool _reconnecting = false;
|
||
private bool _disposed = false;
|
||
|
||
public BarcodeScanner(string opName, int baudRate = 9600)
|
||
{
|
||
OpName = opName;
|
||
_baudRate = baudRate;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 连接指定COM口
|
||
/// </summary>
|
||
public bool Connect(string comPort)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(comPort) || comPort.Length < 3)
|
||
return false;
|
||
|
||
try
|
||
{
|
||
Disconnect();
|
||
ComPort = comPort;
|
||
|
||
_serial = new SerialPort
|
||
{
|
||
PortName = comPort,
|
||
BaudRate = _baudRate,
|
||
Parity = Parity.None,
|
||
StopBits = StopBits.One,
|
||
ReadTimeout = 1000,
|
||
DtrEnable = true
|
||
};
|
||
_serial.DataReceived += Serial_DataReceived;
|
||
_serial.Open();
|
||
|
||
// 空闲提交定时器
|
||
if (_flushTimer == null)
|
||
{
|
||
_flushTimer = new System.Windows.Forms.Timer { Interval = FlushIntervalMs };
|
||
_flushTimer.Tick += FlushTimer_Tick;
|
||
}
|
||
|
||
// 断线重连定时器(每3秒检测)
|
||
if (_reconnectTimer == null)
|
||
{
|
||
_reconnectTimer = new System.Windows.Forms.Timer { Interval = 3000 };
|
||
_reconnectTimer.Tick += ReconnectTimer_Tick;
|
||
}
|
||
_reconnectTimer.Start();
|
||
|
||
Log?.Invoke($"[{OpName}] 扫码枪已连接 ({comPort})");
|
||
return true;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Log?.Invoke($"[{OpName}] 扫码枪连接失败 ({comPort}):{ex.Message}");
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 断开连接
|
||
/// </summary>
|
||
public void Disconnect()
|
||
{
|
||
try
|
||
{
|
||
_reconnectTimer?.Stop();
|
||
_flushTimer?.Stop();
|
||
|
||
if (_serial != null)
|
||
{
|
||
_serial.DataReceived -= Serial_DataReceived;
|
||
if (_serial.IsOpen) _serial.Close();
|
||
_serial.Dispose();
|
||
_serial = null;
|
||
}
|
||
}
|
||
catch { }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 测试当前COM口是否可连接
|
||
/// </summary>
|
||
public bool TestConnection(string comPort)
|
||
{
|
||
try
|
||
{
|
||
using (var sp = new SerialPort(comPort, _baudRate))
|
||
{
|
||
sp.Open();
|
||
sp.Close();
|
||
return true;
|
||
}
|
||
}
|
||
catch { return false; }
|
||
}
|
||
|
||
// ── 串口数据接收 ──
|
||
private void Serial_DataReceived(object sender, SerialDataReceivedEventArgs e)
|
||
{
|
||
try
|
||
{
|
||
if (_serial == null || !_serial.IsOpen) return;
|
||
|
||
int bytesToRead = _serial.BytesToRead;
|
||
if (bytesToRead <= 0) return;
|
||
|
||
byte[] buf = new byte[bytesToRead];
|
||
_serial.Read(buf, 0, bytesToRead);
|
||
string chunk = Encoding.UTF8.GetString(buf, 0, bytesToRead);
|
||
|
||
lock (_bufferLock)
|
||
{
|
||
_buffer.Append(chunk);
|
||
string acc = _buffer.ToString();
|
||
|
||
// 按换行符(\r \n)拆分完整条码
|
||
int startIndex = 0;
|
||
for (int i = 0; i < acc.Length; i++)
|
||
{
|
||
if (acc[i] == '\r' || acc[i] == '\n')
|
||
{
|
||
int len = i - startIndex;
|
||
if (len > 0)
|
||
{
|
||
string line = acc.Substring(startIndex, len).Trim();
|
||
if (!string.IsNullOrEmpty(line))
|
||
SubmitBarcode(line);
|
||
}
|
||
startIndex = i + 1;
|
||
}
|
||
}
|
||
|
||
if (startIndex >= acc.Length)
|
||
_buffer.Clear();
|
||
else if (startIndex > 0)
|
||
{
|
||
_buffer.Clear();
|
||
_buffer.Append(acc.Substring(startIndex));
|
||
}
|
||
|
||
// 无换行 → 启动空闲定时器
|
||
if (_buffer.Length > 0)
|
||
{
|
||
try
|
||
{
|
||
_flushTimer?.Stop();
|
||
_flushTimer?.Start();
|
||
}
|
||
catch { }
|
||
}
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
if (ex is System.IO.IOException || ex is InvalidOperationException)
|
||
{
|
||
Log?.Invoke($"[{OpName}] 扫码枪通信异常,尝试重连:{ex.Message}");
|
||
TryReopen();
|
||
}
|
||
}
|
||
finally
|
||
{
|
||
try { _serial?.DiscardInBuffer(); } catch { }
|
||
}
|
||
}
|
||
|
||
// 空闲超时提交缓冲区
|
||
private void FlushTimer_Tick(object sender, EventArgs e)
|
||
{
|
||
_flushTimer.Stop();
|
||
string toSend;
|
||
lock (_bufferLock)
|
||
{
|
||
toSend = _buffer.ToString().Trim();
|
||
_buffer.Clear();
|
||
}
|
||
if (!string.IsNullOrEmpty(toSend))
|
||
SubmitBarcode(toSend);
|
||
}
|
||
|
||
// 提交条码(含防抖)
|
||
private void SubmitBarcode(string barcode)
|
||
{
|
||
barcode = barcode.Replace("\r", "").Replace("\n", "").Replace("\0", "").Trim();
|
||
if (string.IsNullOrEmpty(barcode)) return;
|
||
|
||
// 防抖
|
||
var now = DateTime.Now;
|
||
if (barcode == _lastDebounceCode && (now - _lastDebounceTime).TotalMilliseconds < DebounceMs)
|
||
return;
|
||
_lastDebounceCode = barcode;
|
||
_lastDebounceTime = now;
|
||
|
||
// 更新状态
|
||
LastBarcode = barcode;
|
||
LastScanTime = now;
|
||
|
||
// 触发事件
|
||
BarcodeReceived?.Invoke(OpName, barcode);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 手动发送条码(模拟扫码触发,走完整业务流程)
|
||
/// </summary>
|
||
public void ManualSend(string barcode)
|
||
{
|
||
if (!string.IsNullOrWhiteSpace(barcode))
|
||
SubmitBarcode(barcode.Trim());
|
||
}
|
||
|
||
// ── 断线重连 ──
|
||
private void ReconnectTimer_Tick(object sender, EventArgs e)
|
||
{
|
||
if (_reconnecting || string.IsNullOrWhiteSpace(ComPort)) return;
|
||
if (IsConnected) return;
|
||
|
||
_reconnecting = true;
|
||
Task.Run(() =>
|
||
{
|
||
try { TryReopen(); }
|
||
catch { }
|
||
finally { _reconnecting = false; }
|
||
});
|
||
}
|
||
|
||
private void TryReopen()
|
||
{
|
||
try
|
||
{
|
||
string[] ports = SerialPort.GetPortNames();
|
||
bool exists = Array.Exists(ports, p => string.Equals(p, ComPort, StringComparison.OrdinalIgnoreCase));
|
||
if (!exists) return;
|
||
|
||
if (_serial != null)
|
||
{
|
||
try { if (_serial.IsOpen) _serial.Close(); } catch { }
|
||
try { _serial.Dispose(); } catch { }
|
||
}
|
||
|
||
_serial = new SerialPort
|
||
{
|
||
PortName = ComPort,
|
||
BaudRate = _baudRate,
|
||
Parity = Parity.None,
|
||
StopBits = StopBits.One,
|
||
ReadTimeout = 1000,
|
||
DtrEnable = true
|
||
};
|
||
_serial.DataReceived += Serial_DataReceived;
|
||
_serial.Open();
|
||
Log?.Invoke($"[{OpName}] 扫码枪自动重连成功 ({ComPort})");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Log?.Invoke($"[{OpName}] 扫码枪自动重连失败 ({ComPort}):{ex.Message}");
|
||
}
|
||
}
|
||
|
||
public void Dispose()
|
||
{
|
||
if (_disposed) return;
|
||
_disposed = true;
|
||
Disconnect();
|
||
_flushTimer?.Dispose();
|
||
_reconnectTimer?.Dispose();
|
||
}
|
||
}
|
||
}
|