串口助手 dame
最终优化版・完整落地详细操作流程(修复 Timer 报错 + UI 卡死 + 事件重复订阅)一、整体架构回顾
手动扫码:主窗体调用ScannerHelper.ScanUUT1_Manual(),弹出FrmScanner调试窗体,支持手动选串口、扫码、保存配置
自动扫码:调用ScanUUT1_Auto(),后台静默串口扫码,无窗体
单 UUT 使用,XML 默认配置SingleUUT,后续可无缝扩展双 UUT
本次核心优化:修复 Timer 编译报错、UI 线程卡死、事件重复绑定、串口未释放问题
前置目录结构确认plaintextTestingInfo
├─ Common
│ ├─ Device
│ │ └─ Scanner
│ │ ├─ UutScannerMode.cs
│ │ ├─ ScannerConfig.cs
│ │ ├─ FrmScanner.cs(本次重点优化)
│ │ └─ ScannerHelper.cs(优化事件防重复订阅)
│ ├─ Models
│ │ └─ DeviceRootConfig.cs
│ └─ Utils
│ └─ XmlConfigHelper.cs
├─ 业务主窗体 Frmpannello.cs
步骤 1:基础 4 个配置类(代码无需修改,沿用之前正确版本)1.1 UutScannerMode.cscsharp运行namespace TestingInfo.Common.Device.Scanner
{
public enum UutScannerMode
{
SingleUUT,
DoubleUUT
}
}
1.2 ScannerConfig.cscsharp运行using System.IO.Ports;
namespace TestingInfo.Common.Device.Scanner
{
public class ScannerConfig
{
public string ComPort { get; set; }
public int BaudRate { get; set; }
public Parity Parity { get; set; }
public int DataBits { get; set; }
public StopBits StopBits { get; set; }
public int TimeoutMs { get; set; }
}
public class DoubleUutScannerConfig
{
public UutScannerMode ScannerMode { get; set; }
public ScannerConfig UUT1 { get; set; }
public ScannerConfig UUT2 { get; set; }
}
}
1.3 DeviceRootConfig.cscsharp运行using TestingInfo.Common.Device.Scanner;
namespace TestingInfo.Common.Models
{
public class DeviceRootConfig
{
public DoubleUutScannerConfig ScannerConfig { get; set; }
}
}
1.4 XmlConfigHelper.cscsharp运行using System;
using System.IO;
using System.Windows.Forms;
using System.Xml.Serialization;
using TestingInfo.Common.Models;
using TestingInfo.Common.Device.Scanner;
namespace TestingInfo.Common.Utils
{
public static class XmlConfigHelper
{
private static readonly string _configFilePath = Path.Combine(Application.StartupPath, "DeviceConfig.xml");
public static DeviceRootConfig LoadDeviceConfig()
{
if (!File.Exists(_configFilePath))
{
var defaultCfg = GetDefaultConfig();
SaveDeviceConfig(defaultCfg);
return defaultCfg;
}
try
{
using (var fs = new FileStream(_configFilePath, FileMode.Open))
{
var xml = new XmlSerializer(typeof(DeviceRootConfig));
return xml.Deserialize(fs) as DeviceRootConfig;
}
}
catch
{
return GetDefaultConfig();
}
}
public static void SaveDeviceConfig(DeviceRootConfig config)
{
using (var fs = new FileStream(_configFilePath, FileMode.Create))
{
var xml = new XmlSerializer(typeof(DeviceRootConfig));
xml.Serialize(fs, config);
}
}
private static DeviceRootConfig GetDefaultConfig()
{
return new DeviceRootConfig
{
ScannerConfig = new DoubleUutScannerConfig
{
ScannerMode = UutScannerMode.SingleUUT,
UUT1 = new ScannerConfig
{
ComPort = "COM3",
BaudRate = 115200,
Parity = Parity.None,
DataBits = 8,
StopBits = StopBits.One,
TimeoutMs = 2000
},
UUT2 = new ScannerConfig
{
ComPort = "COM4",
BaudRate = 115200,
Parity = Parity.None,
DataBits = 8,
StopBits = StopBits.One,
TimeoutMs = 2000
}
}
};
}
}
}
步骤 2:最终优化版 FrmScanner.cs(解决 Timer 报错 + 卡死,直接全选替换)前置窗体操作
窗体新增按钮:Name=btnSaveConfig,Text=保存当前工位配置,双击绑定点击事件
原有控件名称不变:cmbPortName、cmbBaudRate、cboParity、cboDataBits、cboStopBits、btnOpenScanner、btnCloseScanner、btnStartScan、btnClearLog、btnResetScan、txt_ScanRec
完整可直接粘贴代码csharp运行using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO.Ports;
using System.Text;
using System.Windows.Forms;
using TestingInfo.Common.Models;
using TestingInfo.Common.Utils;
namespace TestingInfo.Common.Device.Scanner
{
public partial class FrmScanner : Form
{
private readonly ScannerConfig _currentConfig;
private readonly int _uutNumber;
private readonly SerialPort _newlandScanner = new SerialPort();
public event Action<string> BarcodeResultReceived;
private readonly byte[] _startScanCmd = { 0x01, 0x54, 0x04 };
private readonly byte[] _stopScanCmd = { 0x01, 0x50, 0x04 };
private const int ScanTimeoutMs = 2500;
private string _savedValidBarcode = string.Empty;
private bool _isScanLocked = false;
private int _currentFailScanCount = 0;
private readonly int _maxAllowScanCount = 6;
private bool _isWaitingScanResult = false;
public FrmScanner(ScannerConfig scannerConfig, int uutNo)
{
InitializeComponent();
_currentConfig = scannerConfig;
_uutNumber = uutNo;
this.Text = $"UUT{_uutNumber} 扫码调试窗口";
cmbPortName.Text = _currentConfig.ComPort;
cmbBaudRate.Text = _currentConfig.BaudRate.ToString();
cboParity.Text = _currentConfig.Parity.ToString();
cboDataBits.Text = _currentConfig.DataBits.ToString();
cboStopBits.Text = _currentConfig.StopBits.ToString();
_newlandScanner.DataReceived += Scanner_DataReceived;
}
private void btnSaveConfig_Click(object sender, EventArgs e)
{
try
{
var root = XmlConfigHelper.LoadDeviceConfig();
var scannerCfg = root.ScannerConfig;
var newCfg = new ScannerConfig
{
ComPort = cmbPortName.Text.Trim(),
BaudRate = int.Parse(cmbBaudRate.Text),
Parity = (Parity)Enum.Parse(typeof(Parity), cboParity.Text),
DataBits = int.Parse(cboDataBits.Text),
StopBits = (StopBits)Enum.Parse(typeof(StopBits), cboStopBits.Text),
TimeoutMs = 2000
};
if (_uutNumber == 1)
scannerCfg.UUT1 = newCfg;
else
scannerCfg.UUT2 = newCfg;
XmlConfigHelper.SaveDeviceConfig(root);
MessageBox.Show("配置保存成功!关闭窗口重新打开即可生效。");
}
catch (Exception ex)
{
MessageBox.Show($"配置保存失败:{ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
private void FrmScanner_Load(object sender, EventArgs e)
{
if (cmbPortName.Items.Count == 0)
{
string[] ports = SerialPort.GetPortNames();
foreach (var p in ports) cmbPortName.Items.Add(p);
}
ResetScanStatus();
txt_ScanRec.AppendText($"【初始化】最大失败次数:{_maxAllowScanCount}次,单次超时{ScanTimeoutMs}ms\r\n");
txt_ScanRec.AppendText($"启动指令:{BitConverter.ToString(_startScanCmd)} 停止指令:{BitConverter.ToString(_stopScanCmd)}\r\n");
}
private void btnOpenScanner_Click(object sender, EventArgs e)
{
if (_newlandScanner.IsOpen)
{
MessageBox.Show("串口已打开,无需重复连接!");
return;
}
try
{
_newlandScanner.PortName = cmbPortName.Text;
_newlandScanner.BaudRate = Convert.ToInt32(cmbBaudRate.Text);
_newlandScanner.DataBits = int.Parse(cboDataBits.Text);
_newlandScanner.Parity = (Parity)Enum.Parse(typeof(Parity), cboParity.Text);
_newlandScanner.StopBits = (StopBits)Enum.Parse(typeof(StopBits), cboStopBits.Text);
_newlandScanner.ReadTimeout = 5000;
_newlandScanner.WriteTimeout = 5000;
_newlandScanner.Open();
_newlandScanner.DiscardInBuffer();
_newlandScanner.DiscardOutBuffer();
btnOpenScanner.BackColor = Color.Green;
txt_ScanRec.AppendText($"{DateTime.Now:HH:mm:ss} 【成功】{_newlandScanner.PortName} 扫码枪已连接\r\n");
}
catch (Exception ex)
{
MessageBox.Show($"串口打开失败:{ex.Message}", "异常", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
private void btnCloseScanner_Click(object sender, EventArgs e)
{
if (_newlandScanner.IsOpen)
{
StopScan();
_newlandScanner.Close();
btnOpenScanner.BackColor = Color.White;
txt_ScanRec.AppendText($"{DateTime.Now:HH:mm:ss} 【提示】串口已关闭\r\n");
}
}
//=====【优化后无Timer报错的扫码触发方法】=====
private void btnStartScan_Click(object sender, EventArgs e)
{
if (!_newlandScanner.IsOpen)
{
MessageBox.Show("请先打开扫码串口!", "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
if (_isScanLocked)
{
MessageBox.Show("当前扫码已锁定,请先重置!", "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
if (_isWaitingScanResult)
{
txt_ScanRec.AppendText($"{DateTime.Now:HH:mm:ss} 【提示】正在等待上一次扫码结果\r\n");
return;
}
try
{
_newlandScanner.Write(_startScanCmd, 0, _startScanCmd.Length);
_isWaitingScanResult = true;
txt_ScanRec.AppendText($"{DateTime.Now:HH:mm:ss} 【下发指令】启动扫码,超时{ScanTimeoutMs / 1000}秒\r\n");
System.Windows.Forms.Timer timeoutTimer = new System.Windows.Forms.Timer();
timeoutTimer.Interval = ScanTimeoutMs;
EventHandler timerHandler = null;
timerHandler = (s, args) =>
{
timeoutTimer.Stop();
timeoutTimer.Tick -= timerHandler;
timeoutTimer.Dispose();
if (_isWaitingScanResult)
{
StopScan();
ScanFailHandle();
txt_ScanRec.AppendText($"{DateTime.Now:HH:mm:ss} 【超时】未识别条码,计入失败次数\r\n");
this.BeginInvoke(new Action(() =>
{
BarcodeResultReceived?.Invoke(string.Empty);
this.Close();
}));
}
};
timeoutTimer.Tick += timerHandler;
timeoutTimer.Start();
}
catch (Exception ex)
{
txt_ScanRec.AppendText($"{DateTime.Now:HH:mm:ss} 【指令异常】{ex.Message}\r\n");
_isWaitingScanResult = false;
}
}
private void StopScan()
{
if (_newlandScanner.IsOpen)
{
try { _newlandScanner.Write(_stopScanCmd, 0, _stopScanCmd.Length); }
catch { }
}
_isWaitingScanResult = false;
}
private void Scanner_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
try
{
if (_isScanLocked || !_isWaitingScanResult) return;
int len = _newlandScanner.BytesToRead;
byte[] buf = new byte[len];
_newlandScanner.Read(buf, 0, len);
string code = Encoding.UTF8.GetString(buf).Trim();
if (string.IsNullOrEmpty(code)) return;
StopScan();
this.Invoke(new Action(() =>
{
txt_ScanRec.AppendText($"{DateTime.Now:HH:mm:ss} 【扫码成功】{code}\r\n");
BarcodeResultReceived?.Invoke(code);
this.BeginInvoke(new Action(this.Close));
}));
}
catch (Exception ex)
{
this.Invoke(new Action(() =>
{
txt_ScanRec.AppendText($"{DateTime.Now:HH:mm:ss} 【接收异常】{ex.Message}\r\n");
StopScan();
}));
}
}
private void ScanFailHandle()
{
_currentFailScanCount++;
if (_currentFailScanCount >= _maxAllowScanCount)
{
_isScanLocked = true;
txt_ScanRec.AppendText($"{DateTime.Now:HH:mm:ss} 【锁定】失败次数用尽,请重置扫码\r\n");
}
}
private void btnClearLog_Click(object sender, EventArgs e) => txt_ScanRec.Clear();
private void btnResetScan_Click(object sender, EventArgs e)
{
StopScan();
ResetScanStatus();
txt_ScanRec.AppendText($"\n{DateTime.Now:HH:mm:ss} 【已重置】可重新扫码\r\n");
}
private void ResetScanStatus()
{
_isScanLocked = false;
_currentFailScanCount = 0;
_savedValidBarcode = string.Empty;
_isWaitingScanResult = false;
}
private void FrmScanner_FormClosing(object sender, FormClosingEventArgs e)
{
StopScan();
if (_newlandScanner != null)
{
_newlandScanner.DataReceived -= Scanner_DataReceived;
if (_newlandScanner.IsOpen) _newlandScanner.Close();
_newlandScanner.Dispose();
}
}
}
}
重要:本次已删除独立TimeoutTimer_Tick方法,使用匿名委托自解绑,彻底解决编译报错。
步骤 3:优化版 ScannerHelper.cs(防止事件重复绑定卡死)csharp运行using System;
using System.IO.Ports;
using TestingInfo.Common.Models;
using TestingInfo.Common.Utils;
namespace TestingInfo.Common.Device.Scanner
{
public class ScannerHelper
{
private readonly DoubleUutScannerConfig _scannerConfig;
public ScannerHelper()
{
var root = XmlConfigHelper.LoadDeviceConfig();
_scannerConfig = root.ScannerConfig;
}
#region 手动弹窗扫码(当前使用)
public string ScanUUT1_Manual()
{
return SingleScanner_Manual(_scannerConfig.UUT1, 1);
}
public string ScanUUT2_Manual()
{
return SingleScanner_Manual(_scannerConfig.UUT2, 2);
}
private string SingleScanner_Manual(ScannerConfig config, int uutNo)
{
if (config == null) return string.Empty;
string barcode = string.Empty;
using (var frm = new FrmScanner(config, uutNo))
{
Action<string> receive = code => barcode = code;
frm.BarcodeResultReceived -= receive;
frm.BarcodeResultReceived += receive;
frm.ShowDialog();
frm.BarcodeResultReceived -= receive;
}
return barcode.Trim();
}
#endregion
#region 自动静默后台扫码
public string ScanUUT1_Auto()
{
return SingleScanner_Auto(_scannerConfig.UUT1);
}
public string ScanUUT2_Auto()
{
return SingleScanner_Auto(_scannerConfig.UUT2);
}
private string SingleScanner_Auto(ScannerConfig config)
{
if (config == null) return string.Empty;
SerialPort sp = null;
string code = string.Empty;
try
{
sp = new SerialPort(config.ComPort, config.BaudRate, config.Parity, config.DataBits, config.StopBits);
sp.ReadTimeout = config.TimeoutMs;
sp.DiscardInBuffer();
sp.DiscardOutBuffer();
sp.Open();
sp.DataReceived += (s, e) =>
{
byte[] buf = new byte[sp.BytesToRead];
sp.Read(buf, 0, buf.Length);
string temp = System.Text.Encoding.UTF8.GetString(buf).Trim();
if (!string.IsNullOrEmpty(temp)) code = temp;
};
byte[] cmd = { 0x01, 0x54, 0x04 };
sp.Write(cmd, 0, cmd.Length);
var end = DateTime.Now.AddMilliseconds(config.TimeoutMs);
while (string.IsNullOrEmpty(code) && DateTime.Now < end)
System.Threading.Thread.Sleep(50);
}
catch
{
code = string.Empty;
}
finally
{
if (sp != null)
{
sp.DataReceived -= (s, e) => { };
if (sp.IsOpen) sp.Close();
sp.Dispose();
}
}
return code;
}
#endregion
public UutScannerMode GetCurrentScannerMode()
{
return _scannerConfig.ScannerMode;
}
}
}
步骤 4:业务主窗体调用规范
删除主窗体 FrmScanner frmScanner = null; 代码
头部引入命名空间
csharp运行using TestingInfo.Common.Device.Scanner;
手动扫码按钮代码
csharp运行private void btnManualScan_Click(object sender, EventArgs e)
{
try
{
ScannerHelper helper = new ScannerHelper();
string barcode = helper.ScanUUT1_Manual();
if (string.IsNullOrEmpty(barcode))
{
MessageBox.Show("未扫码或扫码超时");
return;
}
txt_Barcode.Text = barcode.Substring(1, 20);
}
catch (Exception ex)
{
MessageBox.Show($"扫码异常:{ex.Message}");
}
}
步骤 5:编译 + 三步验证测试5.1 编译操作
菜单栏:【生成】→ 清理解决方案
重新生成解决方案,确认无编译报错
5.2 功能验证
正常扫码测试:打开串口→点击扫码→识别条码→窗口自动关闭,条码回填主窗体,无卡死
超时测试:打开串口不扫码,等待 2.5 秒,窗口正常关闭,提示未扫码
连续多次弹窗测试:连续打开关闭扫码窗口 5 次,无卡死、无重复回调、无串口占用
避坑检查清单
btnSaveConfig 按钮必须绑定点击事件
所有类命名空间必须和文件夹路径一致
XML 枚举值SingleUUT/DoubleUUT大小写不能修改
关闭扫码窗口前优先关闭串口,避免 COM 端口残留占用
更多推荐

所有评论(0)