ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

C#实现SICK RFID读卡器TCP异步通信:从协议解析到断线重连实践

C#实现SICK RFID读卡器TCP异步通信:从协议解析到断线重连实践 简介面向需与德国SICK RFID读卡器RFU630通信的C#开发者这份资源提供了一套基于TCP客户端的异步读取程序可应用于自动化、物流、生产流程中的物体识别与追踪场景解决工业设备数据采集、多线程处理和界面卡顿等问题。压缩包共32个文件整体仅60KB涵盖10个C#源文件.cs、可执行程序.exe、配置文件.config、项目工程.csproj/.sln、资源与调试符号.resx/.resources/.pdb等其中源文件承载核心逻辑exe可直观演示config便于调整连接参数。已有1296人学习下载。通过分析源码可重点掌握TcpClient双向通信、async/await异步读取NetworkStream、Task/Thread并发管理以及委托配合Control.BeginInvoke安全更新UI的完整思路。工程内还单独对委托进行示例说明帮助理解多线程跨线程机制适合中高级.NET研发人员快速上手SICK RFID二次开发。 最近在搞一个产线信息化项目设备端用的是德国SICK的RFID读卡器上位机程序用C#实现整体架构就是读卡器作为TCP服务端我这边写一个TCP客户端通过异步方式把标签数据读取下来解析完再交给MES系统。需求听起来很常规但真正动手做的时候会发现设备通信协议、半包粘包处理、断线重连、界面卡顿这几点每一个都能让你多花一个晚上。这篇文章把我实际写过的SICK RFID读卡程序方案完整捋一遍包括为什么选TCP客户端、为什么必须异步读取、报文是怎么解析的以及我在现场排障时踩过的坑。如果你是做仓储、AGV调度、产线追溯这类上位机开发的应该能直接抄作业。1. 项目背景与整体方案拆解1.1 读卡场景RFID读卡器在产线里干什么SICK的RFID读卡器在工厂里最常见的使用方式是给“移动的物体”做身份识别。比如AGV小车走到某个站点地面上的读卡器自动识别车底标签或者工装托盘经过工位时读卡器读到托盘上的标签系统就知道这个托盘绑定的是哪张工单、该执行什么工序。在这个项目里读卡器被固定在一个装配工位旁边当装有RFID标签的载具经过时读卡器把标签ID、信号强度这些数据读出来再通过网络发送给上位机。读卡器本身有点像扫码枪但区别是它不需要人去扣扳机——标签一进入射频场设备就会自动读取。触发源在设备端上位机要做的就是“等事件”。标题里提到的“读卡程序”本质上就是上位机和读卡器之间的通信模块。这个模块要解决的问题包括建立网络连接、接收设备主动推送的数据、把数据解析成业务能用的字段、以及在链路中断后自动恢复。1.2 为什么选“C# TCP客户端 异步读取”先说选型的理由。SICK的RFID读卡器例如RFU63x系列本身就带以太网口支持上位机通过socket直接连接不需要额外转接硬件。相比于串口或者IO-LinkTCP方式有几个明显好处传输距离远、一台 PC 可以同时挂多台读卡器、数据量也更大。串口方案虽然简单但距离短、一台工控机要接多个读卡器时还得扩展串口卡实在不方便。TCP方案只要交换机端口够理论上可以带几十台设备而且SICK设备直接暴露一个固定端口上位机作为TCP客户端去连接它逻辑非常清晰。再看为什么用C#。做产线上位机C#的优势在于开发效率高WinForm/WPF做界面方便和MES、数据库、PLC通信的生态都很成熟。对于这种中等数据量的物联网通信场景.NET的异步机制完全够用。至于“异步读取”这是必须选的。你想象一下如果用一个同步Read方法去读读卡器数据当没有标签经过时Read会一直阻塞在那里界面线程直接卡死用户一看就是“程序未响应”。而且同步Read在断线场景下很难感知通常要等到超时才能反应过来这在产线上是不可接受的。异步读取则能让网络接收在后台持续运行数据到了就回调UI线程始终保持流畅。方案优点缺点串口同步读取简单直接距离短、单台设备、界面易卡顿TCP同步读取独立线程能跑通断线感知差、线程关闭麻烦TCP异步读取async/await不占线程、可优雅取消、UI不卡需要理解状态机代码稍散SocketAsyncEventArgs高性能、低延迟代码复杂度高上位机场景一般用不上1.3 数据从标签到系统完整链路是怎么走的别看最终效果就是“刷一下卡数据进系统”这条链路其实有六个环节RFID标签本身、读卡器天线射频场、读卡器内部基带处理、以太网口、交换机或网线、上位机TCP客户端。其中任何一个环节出问题都会表现为“读不到数据”或“数据不对”。我在项目里排障的习惯是先从射频端查确认标签能被读卡器识别再去查网络最后查软件解析。因为软件解析问题往往最隐蔽比如收到一串数据但拆错了帧程序不报错但数据就是不对。后面我会专门讲这块。2. 协议细节与关键设计2.1 SICK RFID读卡器的通信配置IP、端口、报文格式SICK的RFID读卡器在以太网通信方式下常见的是XML-over-TCP协议。拿RFU63x举例上位机作为TCP客户端去连接读卡器默认端口一般是21111。当然不同型号、不同固件版本的默认端口可能不一样正式开发前一定要先查你手头设备的手册别上来就默认。设备端的IP地址一般可以通过SICK的配置工具SOPAS ET来设定也可以直接在设备参数里改。建议上线前把IP固定下来比如设成192.168.1.10不要用DHCP否则设备IP一变上位机就连不上了。链接建立之后双方通过XML文本消息交互。SICK的Host Interface协议里有连接建立、看门狗设置、读写配置、标签数据上报等不同消息类型。因为是文本协议调试起来比纯二进制协议直观得多。2.2 报文具体长什么样命令与数据上报我用一个实际项目里的例子来说明。上位机连接设备后需要按协议发一条连接指令类似sickconnect//sick设备会回复连接成功的确认消息。之后如果读卡器处于自动读取模式一旦有标签进入射频场设备就会主动通过TCP连接推送标签数据格式大概是下面这样sick radio tag id code2E28011606000020100001A5B/id rssi-46/rssi antenna1/antenna /tag /radio /sick这串XML里id是标签的EPC编码现在UHF超高频标签通常以十六进制字符串形式展示比如E280开头的这串就是EPC码rssi是信号强度单位是dBm数值越大表示信号越强比如-46dBm比-65dBm要好antenna是天线编号多天线设备用来区分标签是从哪个天线读到的。要特别说明的是上面的XML结构是根据项目经验整理的不同固件版本的字段名和嵌套层级可能有差异。所以写解析代码之前一定要先抓几包真实的设备数据看清楚你的设备到底输出什么格式再按实际情况写解析逻辑。2.3 异步读取的底层逻辑C#的async/await并不是“开了一个后台线程”这么简单它本质上是编译成一个状态机。当执行到await stream.ReadAsync()时当前线程会立即返回不阻塞。数据到达后操作系统IO完成端口触发回调状态机会从await的下一行继续执行。整个过程里没有线程被白白占着等数据。这在工业上位机里非常重要。你想想如果每个读卡器都占一个线程线程池里的线程大部分时间都在休眠一旦设备多了线程上下文切换的开销也会上来。异步读写让少量线程就可以管理大量网络连接而且UI线程永远不会因为网络等待而卡顿。另外C#里做异步网络接收有两种常见写法一种是NetworkStream.ReadAsync配合CancellationToken代码简洁直观适合大多数上位机项目另一种是SocketAsyncEventArgs性能更好但代码复杂度明显上升。除非你有极高的吞吐量要求否则我建议用前者把精力放在数据解析和业务逻辑上。3. C#实现从连接建立到数据落地3.1 一个最小的TCP客户端骨架我习惯把一个读卡器的通信逻辑封装成一个独立的类叫它SickRfidClient。这样上层WinForm界面不需要关心socket细节只要订阅事件就行。下面是最基础的结构public class SickRfidClient { private TcpClient _tcp; private CancellationTokenSource _cts; public event Actionstring FrameReceived; public event Action Disconnected; public async Task ConnectAsync(string ip, int port) { _tcp new TcpClient(); await Task.Run(() _tcp.Connect(ip, port)); _cts new CancellationTokenSource(); _ ReceiveLoopAsync(_cts.Token); // 启动后台接收 } private async Task ReceiveLoopAsync(CancellationToken token) { var stream _tcp.GetStream(); var buffer new byte[4096]; try { while (!token.IsCancellationRequested) { int read await stream.ReadAsync(buffer, 0, buffer.Length, token); if (read 0) break; AppendData(buffer, 0, read); } } catch (OperationCanceledException) { } catch (IOException) { } finally { Disconnected?.Invoke(); } } }ReceiveLoopAsync里那个while循环就是核心每当有数据到达ReadAsync返回读取的字节数然后我们把字节追加到缓冲区。如果read 0说明对端关闭了连接循环退出。任何IO异常都会跳出循环再由上层决定是否重连。3.2 半包与粘包按结束标签切帧TCP是流协议没有消息边界。SICK那边发出的一整条XML消息到了你这边可能被拆成几个包到达也可能几条消息粘在一起一次到达。如果不做分帧处理解析出来的数据一定是坏的。我的做法是维护一个字节列表先把收到的数据追加进去然后尝试从中提取完整帧。因为SICK的数据是XML文本而一条消息通常以/sick结束所以可以用这个闭合标签作为切帧依据private readonly Listbyte _pending new Listbyte(); private const string FrameEndTag /sick; private void AppendData(byte[] data, int offset, int count) { for (int i 0; i count; i) _pending.Add(data[offset i]); TryExtractFrame(); } private void TryExtractFrame() { while (true) { string text Encoding.UTF8.GetString(_pending.ToArray()); int endIdx text.IndexOf(FrameEndTag, StringComparison.OrdinalIgnoreCase); if (endIdx 0) return; int frameLen endIdx FrameEndTag.Length; string frame text.Substring(0, frameLen); _pending.RemoveRange(0, frameLen); FrameReceived?.Invoke(frame); } }这段代码逻辑不复杂但解决了一个关键问题无论网络层把数据切得多碎只要设备发的是完整XML消息最终都能正确拼出来。如果标签上报频率很高比如每秒几十次这样全量转字符串再查找会有点浪费但一般产线场景完全够用。真遇到高频场景可以改造成逐字节匹配结束标签的内存流方式。3.3 心跳与看门狗为什么连接会莫名断开SICK的设备端有一个看门狗机制。连接建立后如果上位机在一段时间内没有发送任何指令设备可能会主动断开连接目的是防止僵尸连接占用资源。所以上位机要定时发送心跳消息保活。我用一个System.Timers.Timer每隔5秒发一次心跳。不同设备的保活消息格式不一样有的设备是发看门狗配置命令有的是发一个空指令具体以手册为准。private System.Timers.Timer _heartbeatTimer; private void StartHeartbeat(int intervalMs 5000) { _heartbeatTimer new System.Timers.Timer(intervalMs); _heartbeatTimer.Elapsed async (s, e) { try { await SendAsync(sickheartbeat//sick); } catch { // 发送失败交给断线重连逻辑处理 } }; _heartbeatTimer.Start(); }发送命令的方法也很简单把字符串转成UTF8字节写到网络流里public async Task SendAsync(string xml) { if (_tcp null || !_tcp.Connected) return; byte[] data Encoding.UTF8.GetBytes(xml); await _tcp.GetStream().WriteAsync(data, 0, data.Length); }3.4 断线重连产线程序的基本修养上位机程序跑在产线上一跑就是几个月网络抖动、交换机重启、读卡器断电都是会发生的。如果断线后不自动重连系统就等着宕机吧。我在Disconnected事件触发后会启动一个重连循环private async Task ReconnectLoopAsync() { while (!_cts.IsCancellationRequested) { try { await Task.Delay(3000, _cts.Token); if (_tcp null || !_tcp.Connected) { await ConnectAsync(_ip, _port); break; // 重连成功退出循环 } } catch { } } }重连必须加延时不然设备还没恢复你这边就以毫秒级频率疯狂尝试连接把自己的CPU和日志都刷爆了。我一般用3秒到5秒的固定延时如果项目要求高可以做指数退避比如第一次等2秒第二次等4秒直到上限。3.5 封装成上位机能直接用的模块把上面这些组合起来对外暴露的数据事件可以再细化。比如解析完XML后直接抛出一个TagReadEventArgs里面带上标签ID、RSSI、天线号WinForm里只要一行代码就能订阅client.TagRead (s, e) { textBox1.Invoke(() { listBox1.Items.Add(${e.TagId} RSSI: {e.Rssi} Ant: {e.Antenna}); }); };这里注意事件回调是在异步接收线程里触发的UI控件更新一定要用Invoke或BeginInvoke否则会报跨线程操作异常。很多新手在这里卡很久其实就是一个线程切换问题。4. 实操过程与首次联调记录4.1 第一次把SICK读卡器跑通我做了这几步拿到读卡器别急着写代码。我强烈建议先花半小时用SICK官方的配置软件SOPAS ET把设备摸一遍确认硬件和网络都正常再回过来写C#。具体步骤是这样的先给读卡器接上电源和网线网线可以直接插到电脑网口也可以进交换机。然后用SOPAS ET扫描网段里的设备找到读卡器后进入参数配置界面把IP地址设成固定IP比如192.168.1.10电脑这边设成192.168.1.100保证两个IP在同一网段。接下来在配置软件里把读卡器的工作模式设置成自动读取也就是说不需要上位机发读卡指令只要标签进射频场设备就主动上报。这个模式在上位机集成时最省事。还要注意看一下天线功率功率太低标签离得远一点就完全读不到。网络和射频都确认无误后再用最小化的代码验证TCP通信。我这里说的最小化代码就是一个控制台程序连接读卡器把所有收到的数据打印出来。放一张卡进去如果控制台里能看到类似XML的文本说明整条链路已经通了剩下的就是解析和封装。4.2 上线前必须确认的几个设备参数代码写完后真正上线前有几个设备参数我会再检查一遍。第一个是天线功率。读取距离不够时首先想到的不是改代码而是去配置软件里把输出功率调大同时检查标签的安装位置是不是被金属遮挡了。第二个是读卡区域。有的设备支持多天线要确认你读的是不是正确的天线区域别出现A天线的数据走到B天线的逻辑里。第三个是EPC过滤。如果现场同时有多个标签可以设置只上报符合某个前缀的标签减少无用数据。还有一个容易被忽略的点SICK读卡器的输出格式。有些设备可以配置输出为SICK默认的XML格式也可以配置输出为自定义二进制格式。一定要保证设备配置和你上位机解析逻辑是一致的。我见过有项目改了半天代码最后发现是设备输出格式被人改掉了。4.3 调试工具没有模拟器就自己造做这类项目手头有几个调试工具能省很多事。第一是SICK的SOPAS ET前面说过调参数、看状态必备。第二是Wireshark抓包看TCP数据流确认设备到底发了什么过来这是排查半包粘包和协议格式的利器。第三是自己写一个TCP调试小工具可以手动连接读卡器发任意指令看返回数据方便验证心跳和看门狗逻辑。我还会在程序里加一个原始数据日志开关。调试阶段把所有收到的字节同时以十六进制和UTF8文本两种形式写进日志文件。十六进制可以确认底层数据是否有异常字节文本则方便直接阅读XML内容。正式上线之后我会把这个开关关掉避免日志文件无限膨胀。5. 常见问题排查与避坑经验5.1 连不上、连上就断网络与参数排查很多人搜索“RFID数据连接错误”时其实遇到的都是类似下面这些情况。我根据自己的现场经验整理了一个速查表现象可能原因解决方法TCP连接失败IP不在同一网段、防火墙拦截先ping设备IP再telnet端口测通连上后几秒就断未处理设备看门狗按协议设置看门狗或定时发心跳连接正常但无标签数据设备未开自动读取、天线功率过低在配置软件里开启自动读取、调高功率数据不完整或乱码半包粘包没处理、编码不一致按结束标签切帧统一使用UTF8标签读到了但重复报送标签长时间停在射频场业务层按标签ID时间戳去重其中“连上后几秒就断”这个问题我印象最深。第一次调试时连接建立成功但不到30秒就被设备主动断开查了半天才发现是看门狗机制。后来加上心跳保活问题马上消失。这类问题光看代码看不出来一定要先了解设备协议的特性。5.2 半包和粘包是常态不是异常TCP分帧问题值得单独拿出来说。读卡器上报数据时网络层可能把一条完整的XML消息切成两次发送第一次收到前半段第二次才收到后半段。如果代码里拿到数据就立刻解析那前半段肯定是解析失败的。正确做法就是我第3.2节写的缓冲切帧收到数据先放进缓冲区每次都尝试从缓冲区里提取完整帧提取不到就继续等后面的数据。这样无论网络怎么切包最终都能正确拼出完整消息。还有一种情况是多条消息粘在一起。比如标签读取频率很高几条XML消息背靠背到达。这时候切帧逻辑要放在while循环里一次把所有完整帧都提取出来而不是只取一条。我见过有人只取了一条帧剩下半条留在缓冲区里导致下一条消息解析错位整个数据流全乱了。5.3 现场踩坑清单最后整理几条我做这类项目的经验每一条都是用加班换来的。先说TcpClient.Connected属性。这个属性不可靠它只表示最后一次通信时连接是通的不代表现在仍然正常。我判断连接是否存活的可靠方式是看接收循环是否还在运行一旦ReadAsync抛异常或者返回0就说明连接已经断了。再说日志。上位机程序一定要有完善的日志框架除了业务数据还要记录连接建立、断开、重连这些状态变化。否则程序在产线上跑几个月后突然出现问题你连当时发生了什么都不知道排障会非常痛苦。还有UI线程的坑。接收数据的事件回调发生在后台线程如果你直接去更新WinForm控件会触发跨线程异常。解决方案就是Invoke或者使用SynchronizationContext。这个不难但几乎每个新手都会忘。另外就是重连逻辑。不要做无退避的疯狂重连那会把CPU打满还会让日志文件变得巨大。合适的做法是固定延时3到5秒或者指数退避。如果项目里有多台SICK读卡器建议每个读卡器实例化一个独立的SickRfidClient对象不要共用一个socket。每个实例自己维护连接、接收循环和重连逻辑互不干扰排查问题也方便。最后再分享一个经验协议细节一定要以实际抓包为准不要迷信网上找的资料。不同型号、不同固件版本SICK的XML结构可能会有细微差别。我现在的习惯是接到项目后先用SOPAS ET和Wireshark抓几帧真实数据把字段确认清楚再写解析代码。这套流程走顺了从零到一个稳定运行的读卡程序基本一天之内能搞定。本文还有配套的精品资源点击获取
返回列表