扫码枪在工业现场中的应用日益广泛。目前常见的扫码枪主要分为两种类型:一种是即插即用的扫码枪,类似于键盘操作,只要有焦点即可进行扫码,无须进行额外的二次开发;另一种是基于串口通信的扫码枪,需要根据说明书预先设置通信参数。前者操作简单、便捷,但在项目集成时必须确保获取焦点,且在扫码后的逻辑处理方面可能较为烦琐。后者虽然需要一定的开发工作,但其易于集成的特性使其在项目集成阶段更为便利。本案例中采用的扫码枪为新大陆OY10系列,详见图3-1。
图3-1 新大陆OY10系列的扫码枪
在完成驱动安装和相关参数设置后,扫码枪即可投入使用。为了验证其功能,可以通过串口调试助手进行测试。首先,确保串口调试助手连接到正确的串口端口,然后单击连接按钮。接着,使用扫码枪扫描一个条形码,观察是否能够正确接收到条形码的数值。经过测试,确认可以准确地接收并显示条形码。
通过串口调试助手,我们可以初步完成PC与扫码枪之间的通信测试,但在实际的项目应用中,通常需要编写专门的程序实现与扫码枪的通信和数据处理,以实现更复杂的业务逻辑和功能。这里以扫码枪为控制对象,开发一个简易的通信案例。首先通过VS2022创建一个新的窗体应用程序,项目模板选择Windows窗体应用(.Net Framework),项目名称为ScannerSerialPro,框架选择.Net Framework 4.6,如图3-2所示。
图3-2 创建新项目
项目创建完成后,我们设计一下项目的用户界面(UI)。界面上半部分使用的是ToolStrip组件,其中放置了3个按钮,分别为建立连接、断开连接和串口设置。下半部分主要是ListBox(显示消息)、Label、TextBox和Button控件。最终的界面设计效果如图3-3所示。
图3-3 最终的界面设计效果
结合图3-3可以看出,本程序具备以下功能:
(1)串口参数的设置及读取;
(2)串口的连接与断开;
(3)扫码内容的显示;
(4)扫码枪故障时支持手动输入;
(5)扫码错误及重复判断等相关辅助功能。
因为项目业务不是很复杂,我们就不采用三层架构及类库的方式来分层了。我们在三层架构的基础上,结合上位机的实际情况,设计了一套“341”分层架构。针对一般的上位机项目开发,“341”是3层4类1工具的简写,具体如图3-4所示。
图3-4 “341”分层架构
在项目中,创建5个文件夹以对应各个层和库,然后将前面做好的主界面拖放到对应的界面层中。程序的整体架构设计如图3-5所示。
图3-5 程序的整体架构设计
串口参数的配置及读取:串口参数的配置,其本质是配置文件的读写。使用C#语言实现配置文件的方式有多种,如txt、ini、xml、json、csv等方式。在本项目中,我们使用ini文件的方式存储串口的配置信息。后面大家遇到类似的配置存储,也可以参考本节的内容来做。
1.工具库
首先我们需要写一个操作ini文件的帮助类—IniConfigHelper,这个类中提供了各种操作ini文件的方法。下面对IniConfigHelper进行分析,便于大家理解。
1)API函数声明
IniConfigHelper是在Windows系统上基于kernel32库进行二次封装的工具。kernel32.dll是一个用C++编写的系统库。在C#中调用C++的.dll库时,我们使用DllImport来引入这些库。简单来说,当我们在C#中调用WritePrivateProfileString等方法时,实际上会自动调用kernel32.dll库中同名的方法,具体如下所示。
2)读取Section
我们首先需要了解一下ini文件的存储格式。一个典型的ini文件格式如图3-6所示。
图3-6 一个典型的ini文件格式
从图3-6中可以看出,文件中的内容由若干个Section组成,每个Section由若干个Key-Value键值对组成。
我们可以通过下面的方法读取文件中所有的Section,具体如下所示。
3)读取某个Section下所有的Key
一个Section由若干个Key-Value键值对组成,如果我们想读取某个Section下所有的Key,可以通过下面这个方法来实现。
4)读取某个Section下某个Key对应的Value
实际项目中经常使用的方法是ReadIniData,即根据文件、Section、Key这三个参数获取对应的值,如果读取不到,则会返回一个默认值,具体方法如下。
5)在某个Section下的某个Key中写入对应的Value
与读取Value对应的方法是写入方法,就是向指定文件指定Section的指定Key中写入对应的值。如果Key已存在,则覆盖其原有的Value值;如果Key不存在,则添加该Key-Value键值对,具体实现如下所示。
2.实体库
我们需要配置和保存的是串口通信的相关信息,包括端口号、波特率、校验位、数据位和停止位。为了便于后续扩展,我们将这些信息封装在一个实体类ScannerInfo中,具体实现如下所示。
3.数据层
我们在数据层创建了一个ScannerInfoService类,该类主要提供了用户界面中需要使用的方法。其中,ScannerInfoService的核心方法是配置文件与实体对象之间的相互转换。保存配置信息,就是将一个实体对象存储到一个指定的文件中;读取配置信息,就是从指定文件中读取数据,返回一个实体对象。
(1)读取配置文件返回ScannerInfo对象的方法如下所示。
(2)保存配置对象到指定路径中的方法如下所示。
4.界面层
在主窗体中,首先创建3个对象,分别是配置文件路径、配置服务对象和配置实体对象,具体实现如下所示。
在窗体的加载事件中,通过调用配置服务对象的读取配置文件的方法,返回一个配置实体对象,具体实现如下所示。
参数保存是在串口设置中实现的。所以,根据相关功能设计,我们新建了一个界面—串口设置界面,如图3-7所示。
图3-7 串口设置界面
单击串口设置按钮后,弹出串口设置界面,并将文件路径、配置实体对象、配置服务对象通过构造方法传递进去,这样在串口设置界面中就可以显示当前的配置信息,具体实现如下所示。
在串口配置界面中,单击确定按钮,将配置信息存储到文件中,具体实现如下所示。
1.通信层
(1)我们在通信层创建了一个ScannerComLib类,该类实际上是通过封装串口通信对象SerialPort而形成的,其内部创建了一个SerialPort对象。作为ScannerComLib的一个成员字段,它在SerialPort对象的基础上实现了打开和关闭串口的功能,具体实现如下所示。
(2)对于扫码枪这种串口应用,由于其属于单向只接收不发送的情况,我们可以利用串口类自带的DataReceived事件来实现数据的接收功能。在DataReceived事件处理方法中,需要添加适当的延时,以确保完整接收到扫码枪发送的数据。最后,在ScannerComLib中创建了一个事件,用于通过事件机制将扫码结果传送出去,具体实现如下所示。
2.数据层
(1)数据层是对通信层的封装,这里结合实体类重新封装了打开串口的方法,将参数作为一个对象传递进去,具体实现如下所示。
(2)数据层中的数据最终要显示在界面层,因此这里创建了一个事件,实现将消息传递出去,具体实现如下所示。
1.连接串口
首先,在创建一个ScannerComService数据层对象时,需要在连接串口的按钮事件中调用其Connect方法,并同时绑定CodeReceived事件。这样,在事件处理方法中便可以自动获取扫描条码的内容,从而进行后续的扫码处理,具体实现如下所示。
2.断开连接
调用ScannerComService的DisConn方法,即可实现断开连接,关闭串口,具体实现如下所示。
3.扫码处理
扫码处理主要是在CodeReceived事件绑定的方法中实现。这里创建了一个codeList的集合,每次获得条码之后,首先判断集合中是否存在该条码,如果存在,则通过日志进行提示,如果不存在,则显示条码并将该条码添加到集合中,具体实现如下所示。
4.日志显示处理
这里的日志内容展示,我们使用ListBox控件进行展示。这里有个特别需要注意的地方,即串口的DataReceived事件虽然没有直接创建多线程,但是底层是通过多线程实现的,所以如果直接更新主线程的ListBox控件,会报跨线程访问的错误。因此,这里做了一层封装,如果判断是多线程访问,则通过委托来实现;如果判断不是多线程访问,则直接更新控件,具体实现如下所示。
5.实现效果
将扫码枪与计算机连接,根据实际串口进行参数设置,然后使用扫码枪扫描一个条码,这时,软件上将自动显示出结果,如图3-8所示。
图3-8 项目运行效果图
6.仿真测试
如果大家没有扫码枪,我们也可以通过虚拟串口仿真的方式来测试程序逻辑。关于虚拟串口的使用,在后续的章节中会有详细的讲解,这里大家作为了解即可。我们首先通过虚拟串口软件VSPD虚拟出一对串口(COM19和COM20),最终结果如图3-9所示。
图3-9 VSPD创建虚拟串口
打开串口调试助手,让其连接COM20,串口的参数设置如图3-10所示。然后打开扫码枪软件,将其串口号设置为COM19,串口参数与调试软件一致,最后通过串口调试软件发送条码SN9874122554412354,扫码枪软件即可接收到对应的条码内容,如图3-11所示。
图3-10 串口调试助手模拟发送条码
图3-11 扫码枪软件接收条码