简介:基于C#/.NET的跨平台物联网网关完整源代码,面向需要接入多品牌PLC、串口设备、数据库及第三方物联网平台的开发者和集成商。通过浏览器可视化配置即可连接AB(罗克韦尔)、三菱、Modbus全协议、MT机床等设备,并支持自定义驱动扩展与边缘计算。内置Mqtt服务端和OPCUA服务端,可实现与Thingsboard、IoTSharp等平台的双向数据通讯,适合工业数据采集、设备上云及SCADA项目二次开发。
压缩包共1147个文件,约28.46MB,包含487个C#源码、124个JavaScript、98个cshtml页面、97个PNG图片、40个TXT说明、29个DLL库及17个工程文件等,类型覆盖驱动实现、前端交互、配置界面与项目文档,便于直接编译学习和部署。目前已有566人浏览学习,适合具备一定C#基础、希望快速构建网关或参考驱动写法的人群。资源中附有完整的项目结构、OPCUA/Mqtt配置示例及多款PLC驱动实现,可作为工业通信与边缘计算开发的实用参考。
1. 基于 .NET 6 的物联网网关:这本开源方案把 PLC、Modbus、MQTT 串成了一条线
做工业数据采集的同行应该都有过这种经历:现场设备五花八门,AB 的 PLC、三菱的 FX 系列、一堆挂 485 总线的 Modbus 仪表,再加上几台老掉牙的机床,每家的协议都不一样。以前的做法是每个设备写一个采集程序,再弄个 OPC 服务中转,折腾几天才能把数据送到上层平台。这套基于 C# .NET 6 的物联网网关源码,把这件事做成了浏览器里拖拽配置的活。它内置了 AB、三菱、Modbus 全协议、MT 机床驱动,还自带 MQTT 服务端和 OPC UA 服务端,数据可以直接推给 ThingsBoard、IoTSharp 或者你自己的平台。对正在选型或者想自己搭采集网关的工程师来说,这套源码的价值在于:不用再从零写驱动,照着改就行。
2. 网关的骨架:从 WTM 可视化配置到驱动加载机制
2.1 为什么用 WTM 框架做配置界面
这套网关的配置界面是基于 WTM(WalkingTec.Mvvm)框架开发的。WTM 是 .NET 生态里一个比较成熟的管理系统脚手架,它把 MVC、EF Core、Vue 和 Element UI 揉在了一起,开发者不需要单独写前后端接口。网关的配置页面,比如设备列表、变量绑定、采集周期这些功能,都是通过 WTM 的 BaseCRUDVM、BasePagedListVM 这些基类快速生成的。
从源码里的BaseCRUDVM.cs、BasePagedListVM.cs、DataContext.cs能看出,整个配置模块遵循的是 WTM 的标准套路:每个设备类型对应一个 ViewModel,CRUD 操作由基类完成,DataContext 统一管理数据库上下文。这样设计的好处是,你新增一个设备类型或者采集点的时候,不需要动前端页面,只要在 ViewModel 里加字段就行。
这套方案的实用性在于:网关的配置界面跑在浏览器里,现场工程师不需要装任何客户端软件,打开网页就能添加设备、绑定寄存器地址、设置采集频率。整个网关是 .NET 6 写的,跨平台部署,Windows 和 Linux 都能跑,这就解决了工业现场常见的"网关程序只能装在 Windows 工控机上"的痛点。
2.2 网关的启动流程和模块装配
网关的入口配置在applicationhost.config,这是 IIS Express 的配置文件,说明项目默认按 Web 应用方式运行。启动流程大致是:
// Program.cs 中典型的 .NET 6 Web 应用启动代码 var builder = WebApplication.CreateBuilder(args); // 注册网关核心服务:设备驱动管理、采集任务调度、MQTT 服务、OPC UA 服务 builder.Services.AddSingleton<DriverManager>(); builder.Services.AddHostedService<CollectWorker>(); builder.Services.AddSingleton<MqttServerService>(); builder.Services.AddSingleton<OpcUaServerService>(); var app = builder.Build(); // 启动内置 MQTT 服务端,监听 1888 端口 var mqttServer = app.Services.GetRequiredService<MqttServerService>(); mqttServer.Start(1888); // 启动内置 OPC UA 服务端,匿名访问端口 62541 var opcServer = app.Services.GetRequiredService<OpcUaServerService>(); opcServer.Start(62541); app.Run();这段代码反映的是这套网关的模块装配思想:DriverManager负责按配置加载对应的 PLC 驱动,CollectWorker是后台采集服务,MqttServerService和OpcUaServerService是两个数据输出通道。实际源码里还有一个ReferenceNodeManager.cs,它是 OPC UA 服务端的节点管理器,负责把采集到的实时数据映射成 OPC UA 节点供外部订阅。
参数上,MQTT 服务端默认端口是 1888,管理员账号密码是 admin / 000000;OPC UA 服务端默认地址是opc.tcp://localhost:62541/Quickstarts/ReferenceServer,匿名访问。这两个参数在源码里都是明文常量,部署的时候建议直接改掉。
2.3 驱动的加载方式与扩展接口
这套网关的驱动机制值得单独说。源码里有CodeGenVM.cs和BaseImportVM.cs这两个文件,它们的用途是代码生成和数据导入,也就是说,驱动模块有一些代码是自动生成的,导入设备表、点位表后可以批量生成采集配置。
驱动扩展接口的思路大概是:
// 驱动接口定义示意 public interface IDeviceDriver { string DeviceType { get; } Task<bool> ConnectAsync(DeviceConfig config); Task<Dictionary<string, object>> PollAsync(); Task DisconnectAsync(); }采集服务轮询每个驱动实例的时候,只关心这个接口,不关心底层是 Modbus 还是 AB。而DCExtension.cs可能是驱动模块的扩展方法集,比如对字节数组做高低位转换、处理 CRC 校验、解析 PLC 的 DB 块数据等。
这套设计对做二次开发很友好。你要是现场有非标的设备,只需要实现这个接口,再把驱动类挂到 DriverManager 的注册列表里,就能在浏览器页面上看到新的设备类型。对只想用时下成熟协议的工程师来说,内置的 AB、三菱、Modbus、MT 机床驱动已经覆盖了大部分工厂场景。
3. Modbus 全协议采集:从串口 RTU 到 TCP 的配置与调试
3.1 Modbus 协议栈的组成与选型
Modbus 驱动是这套网关里最实用的部分。所谓的"全协议支持",指的是同时覆盖 Modbus RTU、Modbus ASCII、Modbus TCP 三种模式。RTU 走串口(RS-232/RS-485),TCP 走以太网,ASCII 多用于老旧设备或无线数传电台。源码里 Modbus 驱动的核心是 CRC 校验算法和报文解析,这两块在工业现场几乎是每天都要面对的问题。
Modbus RTU 的报文格式是:地址码(1 字节)+ 功能码(1 字节)+ 数据区(N 字节)+ CRC 校验(2 字节)。功能码里最常用的是 03(读保持寄存器)、04(读输入寄存器)、06(写单个寄存器)、16(写多个寄存器)。当你用这套网关添加一个 Modbus 设备的时候,配置界面里填的就是这些参数。
3.2 添加一个 Modbus RTU 设备的配置流程
在网关的浏览器配置界面里,添加 Modbus 设备通常按这几步操作:
- 新建设备时选择 Modbus 驱动,填入设备名称和通讯参数
- 串口参数按要求填写:波特率 9600、数据位 8、停止位 1、无校验是仪表出厂最常见的默认值,但 RS-485 总线挂多台设备时建议用 9600 8 E 1,因为偶校验能减少总线冲突时的误码率
- 设置从站地址(站号),范围 1-247,要和设备侧拨码或配置一致
- 在点位表里添加要采集的寄存器,读写类型、寄存器地址、数据类型在这里逐一指定
- 保存后启动采集,网关会按周期轮询这个设备的数据
// Modbus RTU 读取保持寄存器的报文构建代码示意 public static byte[] BuildReadHoldingRegisters(byte slaveId, ushort startAddress, ushort quantity) { byte[] frame = new byte[8]; frame[0] = slaveId; frame[1] = 0x03; // 功能码:读保持寄存器 frame[2] = (byte)(startAddress >> 8); // 起始地址高字节 frame[3] = (byte)(startAddress & 0xFF); // 起始地址低字节 frame[4] = (byte)(quantity >> 8); // 寄存器数量高字节 frame[5] = (byte)(quantity & 0xFF); // 寄存器数量低字节 byte[] crc = CalculateCrc16(frame, 6); // 对前 6 个字节做 CRC 校验 frame[6] = crc[0]; frame[7] = crc[1]; return frame; }这段代码的核心是最后两字节的 CRC 校验。Modbus 用的是 CRC-16/MODBUS 算法,多项式是 0x8005,初值 0xFFFF。需要注意输出时是先低字节后高字节,和大多数人的直觉相反。写设备时也经常有人在这里翻车,把 CRC 高低字节写反,设备直接不响应。
3.3 数据类型的解析与字节序问题
Modbus 寄存器里存的数据类型五花八门,最坑的是字节序。一个 32 位浮点数占两个寄存器(4 字节),但不同厂家存储的顺序不同:有的高字在前(Big-Endian),有的低字在前(Little-Endian),还有的把字内字节也调换。这套网关的配置界面里一般都会有字节序选项,但你需要确认自己的设备是哪种。
// 把两个 Modbus 寄存器转换成 IEEE 754 浮点数 public static float RegistersToFloat(ushort highRegister, ushort lowRegister, bool wordSwap) { byte[] bytes = new byte[4]; if (wordSwap) { // 高字在前:先放高寄存器,再放低寄存器 bytes[0] = (byte)(highRegister >> 8); bytes[1] = (byte)(highRegister & 0xFF); bytes[2] = (byte)(lowRegister >> 8); bytes[3] = (byte)(lowRegister & 0xFF); } else { // 低字在前:先放低寄存器,再放高寄存器 bytes[0] = (byte)(lowRegister >> 8); bytes[1] = (byte)(lowRegister & 0xFF); bytes[2] = (byte)(highRegister >> 8); bytes[3] = (byte)(highRegister & 0xFF); } return BitConverter.ToSingle(bytes, 0); }我一般会在测试阶段用 Modbus Poll 这类工具先读一遍设备,把原始字节打出来看,确认字节序之后再往网关点位表里填。千万别相信设备手册里写的"IEEE 754 标准格式"——标准归标准,很多国产仪表厂商实现的时候私心很重,字节序怎么放的都有。
3.4 Modbus 轮询策略与超时参数
采集网关挂在 RS-485 总线上时,轮询策略直接决定了系统的稳定性。这套网关里 Modbus 驱动的轮询周期、超时时间和重试次数需要重点设置。常见做法是:
- 轮询周期默认 1000ms 起,总线挂的设备越多周期越长,不然单台设备响应慢会造成总线上报文重叠
- 超时时间建议 2000ms,有些老式仪表是慢速芯片,处理一条报文需要 500ms 以上,500ms 超时会被频繁判失败
- 重试次数设 1 次就够了。工业场景下偶尔丢一帧很正常,连续丢两帧再报故障也不迟
- 有大量保持寄存器要采集的时候,按连续地址块批量读取,不要逐地址去读,效率差好几倍
现场出现过一种情况:网关通过 USB 转 485 线连接仪表,轮询一快就死机。后来排查发现是 USB 转串口芯片的驱动缓冲区太小,网关把多台设备的请求并发发出去了,芯片缓冲爆掉。解决方法是把采集改成串行队列,每台设备按顺序执行,即使间隔 20ms 也要串行,不能再并发。
4. MQTT 和 OPC UA 双通道输出:对接 ThingsBoard 与第三方平台
4.1 内置 MQTT 服务端的定位与用途
这套网关自带了一个 MQTT 服务端,监听 1888 端口,支持 WebSocket 接入。这意味着网关不仅可以作为 MQTT 客户端往上层平台推数据,还能做为一个独立的 MQTT Broker,让现场的 HMI、触摸屏或者手机 App 直接订阅它的主题(Topic)拿数据。
内置 MQTT 服务端的管理员账号密码默认是 admin / 000000,这在源码里是直接写死的。部署到生产环境时,这个默认凭据必须改。常见的做法是在配置文件里增加一个 MqttBroker 配置节:
{ "MqttBroker": { "Port": 1888, "Username": "admin", "Password": "000000", "AllowAnonymous": false, "EnableWebSockets": true } }AllowAnonymous 设为 false 之后,所有客户端连接都需要认证,否则会被拒之门外。这个配置项的调整在编译前修改源码里的配置绑定就行。
4.2 MQTT 主题设计和数据上报格式
网关采集到的数据要推给 ThingsBoard 这类平台,Topic 设计和 Payload 格式是关键。ThingsBoard 支持两种接入方式:一种是设备直接连接 ThingsBoard 自带的 MQTT Broker(端口 1883),另一种是网关先把数据通过 HTTP 或 MQTT 推到 ThingsBoard 的网关模块。
这套网关走的是后一种思路。数据上报的主题一般设计成:
// MQTT 客户端发布数据的主题格式 string topic = $"v1/gateway/telemetry"; string payload = JsonSerializer.Serialize(new { deviceName = deviceName, // 设备名称,比如 "modbus-slave-01" telemetry = new { temperature = 23.5f, // 采集到的温度值 pressure = 101.3f // 采集到的压力值 } });这段代码反映的是 ThingsBoard 网关 API 的标准格式:外层是 deviceName 和 telemetry 两个字段,telemetry 对象里是实际的键值对。提交到 ThingsBoard 之后,平台会自动按设备维度存储时序数据。对接 IoTSharp 也类似,它同样提供了一组 MQTT 主题,格式上的差异可以在网关源码里做适配。
4.3 OPC UA 服务端:把网关变成一个标准数据源
网关内置 OPC UA 服务端的价值在于:上层系统(如组态软件、MES、SCADA)如果支持 OPC UA 客户端,就不用再去适配 Modbus 或者各家 PLC 的私有协议,直接连网关的 UA 服务端就能拿数据。
ReferenceNodeManager.cs是 UA 服务端的节点管理器。它做的事是:为每个采集变量创建一个 UA 节点,按地址空间层级组织起来,外部客户端通过 NodeId 读取或订阅节点值。
// OPC UA 节点创建逻辑示意 public void AddVariableNode(string deviceId, string tagName, object initialValue) { var folderId = new NodeId($"ns=2;s={deviceId}"); // 为当前测点创建变量节点 var variableId = new NodeId($"ns=2;s={deviceId}.{tagName}"); var variableState = new DataVariableState(variableId, folderId); variableState.Value = initialValue; variableState.DataType = DataTypes.Double; // 将节点添加到地址空间 _systemContext.NodeManager.AddPredefinedNode(_systemContext, variableState); }这个设计里 NodeId 的命名规范很重要。我一般用ns=2;s=设备名.点名的结构,这样 UA 客户端的地址空间里看起来清晰,比如ns=2;s=boiler-01.temperature。如果你使用 UA Expert 这类工具连接测试,可以按这个 NodeId 直接定位数据。OPC UA 服务端默认端口是 62541,匿名访问,不需要额外装证书,这在局域网内部使用足够了。
4.4 双通道同时输出的必要性与排错方法
MQTT 通道和 OPC UA 通道同时开着,看起来好像重复建设,实际上各有用途。MQTT 适合对接物联网平台、移动端、云端应用,轻量且穿透性强;OPC UA 则适合对接 SCADA、组态软件这些工业软件,因为它们在 OPC UA 上有一套成熟的数据模型,比如地址空间、历史数据、报警事件。
双通道在实际部署时偶尔会遇到想让 MQTT 推数据但 UA 服务端启动不了的情况。通常的排错路径是:
- 先确认端口有没有被占用:netstat -ano | findstr 端口号
- 再看 UA 服务端的日志输出,确认节点创建过程中是否遇到数据类型异常
- MQTT 不生效时,优先用 MQTTX 这类桌面客户端订阅
#通配符主题,看网关到底有没有把报文发出来
提示:如果你打算用这套网关对接自己的平台,先别急着改源码。用 MQTTX 订阅网关的主题,用 UA Expert 连接 UA 端口,把两个通道的数据都验证通了再动协议适配层。
5. AB、三菱、西门子和欧姆龙驱动:协议适配的边界与踩坑排查
5.1 AB(罗克韦尔)PLC 驱动的实现要点
AB PLC 驱动的默认场景是 ControlLogix 和 CompactLogix 系列。这套网关对 AB PLC 的采集实现依赖的是 Ethernet/IP 协议,即通过 CIP(Common Industrial Protocol)在以太网上读写标签数据。源码里 AB 驱动的核心工作是标签发现、数据类型映射、以及 CIP 报文的封装解析。
配置 AB PLC 时需要注意的点:
- 必须填写 PLC 的 IP 地址和槽号(Slot Number),槽号不对会直接连接失败
- 标签名要和 PLC 程序里定义的完全一致,大小写敏感
- AB PLC 的标签类型是分 Timer、Counter、Control、Analog 等结构的,网关需要按结构解析出底层字段才能取到你要的值
5.2 三菱 PLC 驱动:MC 协议的报文细节
三菱 PLC 驱动走的是 MC 协议(Melsec Communication Protocol)。FX 系列走串口时常用 MC 协议的二进制格式(帧格式 1),Q/L 系列走以太网时用 MC 协议的 TCP 格式(3E 帧)。
// 三菱 MC 协议 3E 帧读取 D 寄存器 public static byte[] BuildMc3EReadD(ushort startDevice, ushort points) { // 3E 帧报文结构:帧头 + 子头 + 网络号 + PC号 + IO号 + 站号 + 请求数据长度 + 监视定时器 + 指令 + 子指令 + 起始地址 + 点数 byte[] frame = new byte[40]; frame[0] = 0x50; // 帧头(ASCII 说明是 ASCII 格式,二进制为 0xD0) frame[1] = 0x00; // 子头 frame[2] = 0x00; // 网络号 frame[3] = 0xFF; // PC 号 frame[4] = 0xFF; // IO 号 frame[5] = 0x03; // 站号 // ... 中间省略长度和监视定时器填充 return frame; }三菱 MC 协议有个特点:地址编码是"十六进制 ASCII 码"和"二进制数"混合的。3E 帧默认是二进制帧,但有的型号需要切换成 ASCII 帧模式。源码里 AB 驱动和三菱驱动的代码风格相近,但三菱的报文构造明显更繁琐,这跟日系 PLC 协议设计偏保守有关。如果你的 PLC 型号比较老,Frames 里的响应帧长度字段也要留够,不然会截断数据。
5.3 西门子和欧姆龙协议的接入难度差异
项目简介里提到了西门子和欧姆龙协议。欧姆龙 PLC 走的是 FINS 协议(Host Link 或 FINS/TCP),和欧姆龙的海量指令需要一条条对照。而西门子的 S7 协议,实现起来难度更高,主要体现在:S7 协议需要完成 S7Comm 握手、读取请求、PDU 协商等多个阶段,而且不同型号(S7-200 SMART、S7-300/400、S7-1200/1500)之间报文格式有差异。
这套网关的源码里西门子驱动的实现思路,是参考了 S7 协议的开源实现(如 S7.Net Plus)改过来的。它的工作路径是:连接 PLC 的 102 端口,完成 Coat of Arms 握手,再进行读取。如果你要接的是 S7-1200/1500,还需要在 PLC 侧勾选"允许来自远程对象的通信"选项,否则网关会被拒绝连接。
5.4 常见问题与排查:从连接失败到数据跳变
现象:网关能启动,但某个 PLC 设备一直提示连接失败。
原因:设备侧防火墙拦了网关程序的访问端口,尤其是 S7-1200/1500 的 102 端口常被 Windows 防火墙拦。另外连接超时时间设置过短,PLC 响应稍慢就被判死。
解决:先在 PLC 所在电脑上用 Telnet 测试端口连通性,再在网关配置里把连接超时调到 3000ms 以上。
现象:设备显示已连接,但采集到的数据偶尔会跳变到极大值。
原因:PLC 内部分配给这个地址的数据类型与网关配置的不一致,比如 PLC 里是 16 位整数,你按 32 位浮点解析了,高低字节拼出来一个天文数字是常有的事。
解决:用抓包工具先看一眼 PLC 返回的原始字节,确认字节序和数据类型,再回头改网关的点位配置。
现象:AB PLC 的标签读出来全部为 0,但 PLC 程序里值不是 0。
原因:标签在 PLC 里定义的类型是 BOOL 数组或者结构体,网关卡仅按基本数据类型解析,没有展开数组或结构。
解决:在网关的标签映射里,把这个标签拆成"标签名.数组索引"或"标签名.成员名"再采集。
现象:三菱 PLC 通讯偶尔失败,重试后恢复。
原因:三菱 MC 协议的响应帧里有个监视定时器(通常默认 0 表示无限等待),如果网关报文里没关注这个字段,PLC 侧超时会断开连接。
解决:把请求帧的监视定时器设置为 1000(单位 250ms),请求超时时间对应加大,让 PLC 有足够时间缓冲。
现象:多个设备同时采集时,偶尔出现某个设备的数据一直不更新。
原因:网关的采集任务是并发执行的,但 Modbus 串口设备本身是半双工总线,多个驱动实例同时通过同一个串口发报文会造成总线冲突。
解决:把 Modbus 串口设备全部加到同一个"串口通道"下,由网关按顺序排程,不能一个设备一个串口对象。
6. 把网关跑起来:从源码编译到连接 ThingsBoard 的验证方法
源码拿到手之后,编译运行是第一步门槛。项目是基于 .NET 6 的,你需要先装好 .NET 6 SDK,然后用 Visual Studio 2022 打开项目文件直接编译。启动后浏览器访问本地端口,就能看到 WTM 的配置界面。
整个验证链路我建议按下面这套流程走一遍:
- 编译启动网关,确认 Web 界面能正常打开
- 用 MQTTX 连接本地 1888 端口,账号 admin / 000000,订阅
#主题 - 在配置界面添加一个 Modbus TCP 设备,比如模拟器或者真实仪表的地址为 127.0.0.1:502,站号 1
- 添加几个测试点位,地址填 40001 或 30001 这类常规地址
- 保存并启动采集,观察 MQTTX 里有没有收到 JSON 报文
- 用 UA Expert 连接
opc.tcp://localhost:62541/Quickstarts/ReferenceServer,查看数据节点 - 在 ThingsBoard 里创建设备,使用网关 API 接入,确认遥测数据上云
第三步里我建议先用 Modbus Slave 或者 Modbus Poll 这类工具在本地模拟一个从站设备,这样不用去现场搬 PLC 也能完整验证链路。Modbus Poll 是主站工具,Modbus Slave 是从站模拟器,你用 Modbus Slave 起一个设备监听 502 端口,网关作为主站去采集它,这样整条链路就是通的。
验证 MQTT 数据是否到达 ThingsBoard 时,打开 ThingsBoard 的设备详情页,切到"最新遥测"标签页。如果能看到 key 和 value,说明网关已经把数据推上来了。如果看不到,先检查网关日志里有没有 MQTT 发布的成功记录,再用 MQTTX 看主题和 payload 是否正常——大概率是设备名不匹配,ThingsBoard 要求网关报文里的 deviceName 必须对应平台里已存在的设备名称。
整套跑通之后,你手里就拥有了一条完整的采集链路:Modbus 从站 → 网关采集 → MQTT 双通道输出 → 物联网平台遥测展示。从那以后我每次调试这套网关,都强制走一遍上述流程,从编译到遥测上云,半小时内全部拉通。如果哪一步失败了,就看日志定位——日志里会把连接超时、寄存器地址无效、JSON 解析失败这些问题直接打印出来。希望帮到你。
本文还有配套的精品资源,点击获取