当前位置: 首页 > 科技观察

通过面向对象设计串口协议

时间:2023-03-19 15:09:28 科技观察

作者|廖玉东背景自Java语言流行以来,其主要的面向对象编程也成为广为人知的编程思想:“封装、继承、多态”、“易维护、易复用、易扩展”、“解耦和扩展”。隔离”。以过程为中心的“面向过程编程”通常是先分析解决问题所需的步骤,然后依次用函数来实现这些步骤,最后串联起来依次调用,这是一种顺序思维方式.普遍支持的面向过程编程语言包括C语言、COBOL语言等,广泛应用于系统内核、IoT、物联网等领域。比较典型的案例之一就是串口通信协议(驱动、SDK)的集成开发。尽管大多数Web应用已经进入“JsonFree”时代,但仍有大量嵌入式设备使用串口协议。在能耗、尺寸和效率方面获得优势。大多数现有的驱动程序都是使用面向过程的方法用C语言编写的。例如,当我们的应用需要提供线下服务时:用户可以在门户商城使用一体机访问我们的服务,也可以选择使用线下POS机进行信用卡支付(类比肯德基)。我们不仅要在网页后台计算订单价格,还要通知POS机开始“接单”,完成刷卡操作,及时返回交易数据。然而,当我们打开POS机的“bonus”接口文件时,眼花缭乱的二进制大小写和复杂的数据结构让我们不知所措——所有数据都需要通过RS232串口线,连接“01010101”的all-in-一台机器交互。PS:一体机为Windows实体机,通过COM接口(RS232,9芯线)与POS机连接;文章包含代码示例,在电脑上观看效果更佳。令人眼花缭乱的二进制不同于我们日常使用的HTTP协议:具有标准的消息结构和数据编码完整的SDK和生态链工具,无需关注即可轻松实现CS(Client-Server)架构的数据传输应用层(ISOApplicationLayer)下面的技术细节和串口更接近ISO物理层:通过指定频率(Baud波特率)的高低电平(0/1)来传输数据。因此,当你想通过串口传输具有特定含义的数据时,通常需要对二进制数据进行区分、组合、编码,使其具备表达复杂数据结构的能力——串口通信协议。例如一个典型的(但略微复杂)的串口协议报文:一个串口报文的数据结构(用16进制表示字节流例子)string="serial",数据在传输时是按顺序写入的,因此,它需要准确的告诉服务器:StartToken/EndToken,标记当前消息开始和结束的时间Length,当前要读取的数据长度为了提高协议的易用性,会传递不同目的的数据通过Type区分,有不同的序列化规则:Hex(十六进制)BCD(二进制整数)ASC(ASIIC码)数据部分由消息头和多组消息数据组成:(1)关键字段(如ID,Code,Version)都是固定类型、固定长度的数据;(2)而数据字段(Data)在不同的FieldCodes(不同的场景下)是不同的:是变长数据,所以Len也需要在之前,发送和读取数据长度时,必须通过字段代码动态推断并以面向过程的方式顺序构建。创建消息不是一项艰巨的任务。但是不同的功能码(FunctionCode)所包含的消息数据(FieldData)是完全不同的,但是发送过程和序列化方式是一致的。如果我们是面向过程的,以一个功能指令为单位进行开发,不仅会出现大量重复冗余的序列化代码,而且会丢失上层Function和Field的业务意义,使得代码难以理解和维护。publicvoiddecodeMsgData(byte[]msgDataBlocks,intindex)throwsPaymentException{intstart=0;for(inti=0;imessage.header.id"function":"CREDIT_CARD",//->message.header.function"transactionAmount":"20.00",//message.data[]{field:"49",value:"20.00",...}"acquirerName":"VISA"//message.data[]{field:"51",value:"VISA",...}}}基于消息对象再抽象一层,构建更贴近业务的Request/Response。指定命令(函数)的开发和使用与底层数据结构解耦。当我们要支持新的命令时,只需要在功能层之上实现一个新的Field即可。当我们要更新序列化规则的时候,只需要修改协议层的Attribute——协议层下面的全景SDK架构+数据序列化流程+串口异步监听测试当然是为了避免破坏已经存在的功能构建,测试也是开发过程中需要慎重对待的一个环节(毕竟对于二进制数据来说,如果前面少了一个bit,解码出来的信息可能就完全不一样了。。。)对于协议层(协议),TDD是测试和开发的最佳方式。“A->B”,输入输出一目了然,使用起来非常舒服高效。但是一旦涉及到串口通信部分,就需要花点心思了:(1)串口的读写口不同:写口发送数据后,需要等待监听读口接收数据,但Listener模式多为多线程。需要引入额外的同步组件来控制(2)串口连接是一个长链路,没有容错机制,可能会出现丢包、断开等情况:一般是ACK/NACK握手机制(类似于TCP)是为了确保通信而额外设计的。触发重试方式一:搭建多线程测试环境,创建StubServer:使用PipedInputStream和PipedOutputStream将读写流打包到串口并指向创建的pipeline流,然后使用另一个线程模拟终端POS机消费数据接收请求,返回数据,验证数据传输和序列化的正确性。valserverInputStream=PipedInputStream()valserverOutputStream=PipedOutputStream()valclientInputStream=PipedInputStream(serverOutputStream)valclientOutputStream=PipedOutputStream(serverInputStream)valserialConnection=StreamSerialChannel(clientInputStream,clientOutputStream)valmockServer=Thread{//1.等待客户端Thread.sleep(50)//2.服务器端读取请求serverInputStream.read(ByteArray(requestBytes.size))//3.向客户端发送ackserverOutputStream.write(Acknowledgement.ACK.getBytes())//4.通知客户端-模拟通信侦听器serialConnection.onDataAvailable()//5.向客户端发送响应serverOutputStream.write(responseBytes)//6.通知客户端-模拟通信侦听器serialConnection.onDataAvailable()//7.等待客户端Thread.sleep(50)//8.server端读取ackserverInputStream.read(ByteArray(1))}左右交互,模拟上游的字节流进行数据传输Option2:使用Fake的外部程序(1)虚拟串口:Windows和Linux上都有比较成熟的串口调试工具。我使用的是WindowsVirtualSerialPortDriver,因为直接通过虚拟串口写入(二进制)数据不是很方便,所以我创建了创建两个虚拟串口A-B来模拟Client的串口(sender-all-in-one)和服务器(receiver-POS),并将它们连接在一起以相互通信。与方案一类似,启动两个线程分别作为发送方和服务器。接收器连接到相应的串口,一个发送一个接收模拟端到端的交互场景。(2)USB转串口芯片(略硬核)刚好家里有一个树莓派,自带串口,可以用来充当POS系统。然后在某宝买了一个USB转TTL的串口芯片(因为我的电脑已经没有九针接口了),插到Windows主机上,让它可以通过USB发送串口数据。将树莓派和TTL的Read/Write引脚反接,与方案2的测试方法类似,只是两个线程变成了两个独立的主机。CH340芯片方案三:使用测试机物联网设备相对复杂,一般供应商都会提供相应的测试机和测试环境。但是由于沟通的原因,我们拿到测试机的时间很晚;因为疫情,开发者无法接触到POS测试机,只能使用Zoom远程引导调试。因此,我们需要在一个相对准确的环境中尽快验证SDK的功能是否完整。也因为我们提前准备了多层测试,拿到测试机后只用了1个小时就完成了实机集成测试。后记(脑补)这篇文章主要是用“面向对象”的编程思想,重新审视串口协议的设计与实现。利用“封装、继承、多态”的特性,构建更健壮、可扩展、易维护的SDK。但“面向对象”并不是唯一的解决方案——“抽象——编程的本质是问题域和解决方案域的细化”。笔者认为,“抽象”可能是一种更通用的编程思想。编程的核心是选择合适的角度和层次来分析问题,找到共性并得到答案,将解决问题的过程抽象成模型、方法论和原理,并落实到更多的场景和领域中。代码实现只是一种“翻译”工作。随着抽象层次的不同,软件从代码和模块的重用到系统和产品的重用。就像文中的串口协议一样,只是基于下层的服务给出承诺和约定,上层的应用侧重于当前需要解决的问题领域。因此,虽然上面描述了串口协议的开发设计,但是抽象的思维模式在不同领域还是可以产生共鸣的:高级语言是对汇编指令的抽象和封装,而Deployment是对多个Kubernetes的抽象和封装资源。服务是对软件/硬件服务的抽象和封装