简介:面向需与德国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-Link,TCP方式有几个明显好处:传输距离远、一台 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 报文具体长什么样:命令与数据上报
我用一个实际项目里的例子来说明。上位机连接设备后,需要按协议发一条连接指令,类似:
<sick><connect/></sick>设备会回复连接成功的确认消息。之后如果读卡器处于自动读取模式,一旦有标签进入射频场,设备就会主动通过TCP连接推送标签数据,格式大概是下面这样:
<sick> <radio> <tag> <id code="2">E28011606000020100001A5B</id> <rssi>-46</rssi> <antenna>1</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 Action<string> 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 List<byte> _pending = new List<byte>(); 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("<sick><heartbeat/></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抓几帧真实数据,把字段确认清楚再写解析代码。这套流程走顺了,从零到一个稳定运行的读卡程序,基本一天之内能搞定。
本文还有配套的精品资源,点击获取