01 工业数据采集全景与架构
本篇定位:给已经会写 Java、但没系统摸过车间的工程师,建立一张"从传感器到云端"的完整地图——搞清采集什么、怎么采、采上来放在哪、以及为什么工业现场的采集比调用一个 HTTP 接口难十倍。本篇不展开具体协议的报文细节(那是 03/04 篇的事),也不展开字节序(详见
03-字节序与数据类型编码.md)。
本篇目录
- 1. 什么是设备数据采集:OT 与 IT 的边界
- 2. ISA-95 五层模型:工业系统的分层语言
- 3. 一条数据的完整旅程:从传感器到云端
- 4. 采集对象分类:你要面对的到底是什么设备
- 5. 采集方式五选一:直采 / OPC / 网关 / 硬接线 / 相机
- 6. 采集架构:边缘采集器 → 消息总线 → 时序库 → 应用层
- 7. 点表(Tag)与测点模型设计
- 8. 采集模式:轮询 / 变化上报 / 订阅 / 触发
- 9. 采集质量指标:怎么衡量"采得好不好"
- 10. 采集难点清单:现场踩坑总纲
- 11. 数据上云:MQTT 主题、QoS、断网续传与压缩
- 小结
- 常见误区
1. 什么是设备数据采集:OT 与 IT 的边界
设备数据采集(Device Data Acquisition / OT Data Collection),指的是把工业现场设备内部的数据(寄存器值、状态位、报警、产量、工艺参数)以自动化方式读取出来,转换成 IT 系统可消费的结构化数据,并可靠地送到上位系统的过程。
它本质上是一次跨边界的翻译工作:
| 维度 | OT 侧(运营技术) | IT 侧(信息技术) |
|---|---|---|
| 主体 | 电气工程师、工艺工程师、设备厂商 | 后端/平台工程师 |
| 关注点 | 实时性、确定性、安全(人身与设备)、可用性 | 吞吐量、一致性、可扩展、可观测 |
| 时间尺度 | 毫秒 ~ 秒,硬实时 | 秒 ~ 天,最终一致 |
| 协议 | Modbus RTU/TCP、S7comm、MC、Profinet、CAN | HTTP、MQTT、Kafka、gRPC |
| 数据形态 | 寄存器、位、字节流 | JSON、行/列式表、对象 |
| 变更态度 | 极度保守,"能跑就不要动" | 快速迭代、灰度发布 |
| 故障代价 | 停机、废品、人身事故 | 服务降级、用户投诉 |
| 生命周期 | 10~20 年 | 3~5 年 |
这条边界带来三个结构性矛盾,几乎所有采集项目的麻烦都源于此:
- 语义落差:PLC 里只有
DB1.DBW20这种"地址 + 字节",没有任何业务语义。把一个 16 位整数解释成"主轴温度"的知识,只存在于电气工程师的脑子里或者一份 Excel 点表里。 - 可靠性落差:IT 默认"网络终将恢复,重传即可";OT 现场会断网、会掉电、会被人拔网线、会被电焊机干扰。采集侧必须能自己兜住这些。
- 写操作的不对称性:读错了顶多是图表难看;写错了会让阀门动作、电机启动。所以采集平台必须把"读通道"和"写通道"当成两个安全等级完全不同的功能来设计。
一句话判断:采集系统的第一优先级不是"快",是**"不添乱"**——绝对不能因为采集平台的存在,让原本正常运行的产线多一个故障点。
2. ISA-95 五层模型:工业系统的分层语言
ISA-95(IEC 62264)把制造企业自动化系统划分为 5 层。这是你跟甲方/集成商沟通时最有效的共同语言。
Level 4 企业层 ERP / SCM / CRM
├─ 关注:订单、成本、库存、财务
├─ 时间尺度:天 / 月
└─ 典型系统:SAP、用友、金蝶
Level 3 工厂/车间管理层 MES / WMS / QMS / EAM
├─ 关注:工单、报工、追溯、质量、OEE
├─ 时间尺度:分钟 / 小时 / 班
└─ 典型系统:SIMATIC IT、达索 Apriso、国产 MES
Level 2 过程控制层 SCADA / HMI / DCS 操作站 / 历史库
├─ 关注:工艺流程、报警、配方、趋势曲线
├─ 时间尺度:秒 / 分钟
└─ 典型系统:WinCC、iFix、Intouch、组态王
Level 1 基础自动化层 PLC / DCS 控制器 / CNC 数控 / 机器人控制器
├─ 关注:逻辑控制、闭环调节、联锁保护
├─ 时间尺度:毫秒 ~ 百毫秒
└─ 典型设备:S7-1200/1500、汇川、三菱 Q/R、Fanuc、KUKA
Level 0 实际生产过程 传感器 / 执行器 / 电机 / 阀门 / 仪表
├─ 关注:物理量
└─ 时间尺度:微秒 ~ 毫秒(硬实时)
几个实用的判断:
- 物联网平台(FastBee/ThingsBoard 这类)通常落在 L2.5 位置:它从 L1/L2 拿数据,向 L3/L4 供数据,自己不参与控制。定位清楚这一点能省掉无数扯皮。
- 跨层不要越权:绝对不要让云平台直接写 L1 的 PLC 输出点。需要写的时候,必须经过 L2 的 SCADA 或者一个带联锁保护的受控通道。
- L3 的接口对象和 L1 完全不同:L3(MES)关心"工单 WO-20240501-003 在第 3 工序的实际节拍是 47.2 秒",L1(PLC)只知道
DB10.DBD24 = 472。中间的映射关系就是点表(见第 7 节)。 - "两化融合 / 工业互联网平台"这类名词,落到架构上基本就是"L2 以上打通 + 数据建模 + 应用开发"。
3. 一条数据的完整旅程:从传感器到云端
以"注塑机料筒温度"为例,看一个物理量是怎么变成数据库里的一行记录的:
[1] 物理量
料筒实际温度 = 218.6 ℃
[2] 传感器 / 变送器 (L0)
热电偶 K 型 → 温度变送器 → 4~20mA 电流信号
电流 = (218.6 / 300) * 16 + 4 = 15.66 mA
[3] PLC 模拟量输入模块 (L1)
A/D 转换,16 位有符号整数
原始码值 RAW = (15.66 - 4) / 16 * 27648 = 20150
→ 存入 PLC 地址,例如 S7 的 DB3.DBW 10,或 Modbus 保持寄存器 400101
[4] PLC 用户程序 (L1)
可能做:滤波、量程标定、报警判断(>250℃ 置 M10.0)
注意:此刻数据仍然只是一个 16 位整数,没有任何单位信息
[5] 采集器读取 (边缘侧)
通过 S7comm 或 Modbus TCP 读取该地址
得到字节流:0x4E 0xB6(大端)
→ 按点表配置解释为 INT16,值 = 20150
[6] 工程量转换 (边缘侧)
EGU = RAW / 27648 * (300 - 0) + 0 = 218.6
→ 得到 { tag: "M01.BarrelTemp", value: 218.6, unit: "℃", ts: 1717... }
[7] 边缘处理
死区过滤(±0.5℃) → 变化才上报 → 本地环形缓存 → 打包
[8] 上云传输
MQTT PUBLISH → topic: iot/A01/M01/telemetry, QoS 1
[9] 平台接入
MQTT Broker → 规则引擎 → 解析 → 时序库写入
[10] 存储
TDengine 超级表 `sensor_telemetry`:
(ts, device_id, tag, val_double, quality) VALUES (...)
[11] 应用
实时看板 / 报警 / OEE 分析 / 工艺优化模型
关键观察:步骤 [5]→[6] 之间的转换知识(地址、数据类型、字节序、量程)不在设备里,也不在协议里,它必须由人配置出来,并且被持久化管理。这就是点表存在的理由,也是采集平台最核心的资产。
反过来说:没有点表的采集器只是个协议转换器,不是采集平台。
4. 采集对象分类:你要面对的到底是什么设备
不同设备的"可采集性"差异巨大。下面这张表决定了一个项目的工作量与风险。
| 设备类别 | 典型代表 | 常用接口/协议 | 数据特征 | 采集难度 | 主要坑点 |
|---|---|---|---|---|---|
| PLC(小型) | 西门子 S7-200 SMART / S7-1200、三菱 FX5U、欧姆龙 CP1H、汇川 H3U | Modbus RTU/TCP、S7comm、MC、FINS、HostLink | 开关量、模拟量、产量计数 | ★★☆☆☆ | 连接数上限、老固件不支持以太网 |
| PLC(中大型) | S7-300/400/1500、三菱 Q/iQ-R、AB ControlLogix、施耐德 M580 | S7comm、MC/SLMP、EtherNet/IP CIP、Modbus TCP | 大规模 DB 块、工艺参数、报警区 | ★★★☆☆ | 优化块(Optimized DB)地址不可读、需要符号寻址 |
| DCS | 霍尼韦尔 TPS/Experion、横河 CENTUM、浙大中控 ECS | OPC UA/DA、Modbus、专有网关 | 海量位号(万级)、高可靠要求 | ★★★★☆ | 几乎不开放直连,必须走 OPC;安全审计严格 |
| CNC 数控机床 | Fanuc、西门子 840D、三菱 M70/M80、广数、华中 | Focas(Fanuc)、OPC UA(840D)、Modbus、MTConnect | 主轴负载、进给、坐标、程序号、报警号 | ★★★★☆ | 厂商 SDK 授权费用、老机床只有 RS232 串口输出 |
| 机器人 | KUKA、ABB、Fanuc、安川、埃斯顿 | KUKA VarProxy/EKI、ABB PC SDK/RWS、OPC UA | 关节角、TCP 位姿、电流、报警、程序状态 | ★★★★☆ | 需厂商选件包(如 KUKA EKI 是付费选项) |
| 智能仪表 | 电表(DL/T645、Modbus)、水表、流量计、温控表、称重仪 | Modbus RTU/TCP、DL/T645、CJ/T188 | 累积量、瞬时量、报警状态 | ★★☆☆☆ | 地址表各家自定义、BCD 码、浮点字序五花八门 |
| 传感器(独立联网) | 温湿度、振动、压力、光电 | Modbus、CAN、LoRa、NB-IoT、4-20mA | 单值或三轴时序 | ★☆☆☆☆ | 供电与布线比协议更难 |
| 条码 / RFID | 扫码枪、固定式读码器、UHF RFID 读写器 | TCP Socket / 串口 / OPC UA / HTTP | 事件型字符串 | ★★☆☆☆ | 触发时机、去重、与产线节拍对齐 |
| 视觉系统 | 基恩士、康耐视、海康/大华工业相机 | TCP/UDP 私有协议、GigE Vision、Modbus TCP | 判定结果(OK/NG)、尺寸值、图片 | ★★★☆☆ | 结果走协议、图片走文件系统,两条链路要对齐 |
| 变频 / 伺服 / 执行器 | 西门子 V90、ABB ACS、台达、汇川 | Modbus RTU、CANopen、Profinet、EtherCAT | 频率、电流、转矩、故障码 | ★★★☆☆ | 参数号 ≠ 寄存器地址,需要查参数手册 |
判断采集难度的经验法则:
- 越靠近 L0,越依赖硬件改造(加传感器、加网关、布线)。
- 越靠近 L2,越依赖厂商配合(要授权、要 SDK、要开放接口)。
- 越老的设备,越可能只有串口,而且手册可能已经找不到了。
- 有 PLC 程序源码 = 难度减半;没有源码只能靠猜和抓包反推(这是常态,做好心理准备)。
5. 采集方式五选一:直采 / OPC / 网关 / 硬接线 / 相机
| 采集方式 | 原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 协议直采 | 采集器直接发 Modbus/S7/MC 报文读 PLC | 无中间环节、延迟最低、成本最低、可控性强 | 协议碎片化;需要懂地址与字节序;直连 PLC 有安全与负载争议 | 中小型项目、PLC 型号可控、自研平台(FastBee 的 Modbus/S7/MC 驱动即此类) |
| OPC UA | 通过 OPC UA Server 统一暴露,客户端标准订阅 | 语义化(有 NodeId、有数据类型、有单位);跨厂商统一;支持订阅与安全加密 | 需要部署 Server(KepServer/KEPServerEX、Matrikon、Prosys);授权费;性能受 Server 限制;配置工作量大 | 多品牌 PLC 混合、大型项目、已有 OPC 基础设施 |
| OPC DA(Classic) | 基于 Windows DCOM 的老标准 | 老系统普遍支持 | DCOM 配置地狱、跨机跨域几乎不可用、无安全模型、已被淘汰 | 新项目不要用;只在必须接入老 SCADA 时被动兼容 |
| 网关透传/协议转换 | 串口服务器、DTU、边缘网关把 RS485 转成以太网或 MQTT | 解决布线问题;屏蔽底层协议;可脱网缓存;可跨公网 | 多一跳延迟;多一个故障点;网关本身要配置;透传模式下报文分析困难 | 老设备联网、远程站点、串口设备集中上云 |
| 硬接线 IO | 把无源干接点/继电器信号接到采集模块的 DI 通道 | 完全不依赖设备协议、电气隔离、绝对可靠 | 需要布线与停电施工;点位成本高;只能取开关量 | 关键状态(运行/停机/故障)、安全相关信号、无通信接口的老设备 |
| 工业相机 / 视觉 | 相机拍照 + 算法识别(读码、OCR、缺陷、计数) | 能采到"没有数据接口"的信息(表盘读数、指示灯、仪表数字) | 算法成本高、受光照影响、结果非确定性 | 老式仪表读数、无接口设备、质检工位 |
选型建议(有观点)
- 能直采就不要加 OPC Server。OPC UA 的价值在"统一语义",但如果你只有 2~3 种 PLC,引入一个商业 OPC Server 只是增加成本和故障点。
- 能靠协议拿到的,绝不要硬接线。硬接线每增加一个点都是真金白银的施工费,而且改点要停机。
- 但关键状态信号(运行/故障/急停)值得硬接线。这类信号的可靠性要求高于数据丰富度,且当 PLC 通信挂掉时,硬接线是你唯一还能看到的东西。
- 串口设备一律先过网关。「RS485 总线 → 串口服务器 → 采集器 TCP」这条路虽然多一跳,但把最难的物理层问题封装掉了,值得。
- OPC DA 只做兼容,不做新架构。如果甲方坚持要 OPC DA,先用 OPC DA→UA 桥接器(如 OPC UA Wrapper)转成 UA 再接入。
五种方式的链路对比
直采: [PLC] ──以太网── [采集器] ──MQTT── [平台]
延迟 1~20ms 跳数 1
OPC UA: [PLC] ──S7comm── [OPC UA Server] ──UA── [采集器] ──MQTT── [平台]
延迟 50~500ms 跳数 2
网关透传: [仪表] ──RS485── [串口服务器] ──TCP── [采集器] ──4G/MQTT── [平台]
延迟 30~500ms 跳数 2(透传模式下采集器仍要自己解 Modbus RTU)
硬接线: [继电器触点] ──导线── [IO 模块] ──Modbus── [采集器] ──MQTT── [平台]
延迟 5~50ms 跳数 2(但可靠性最高)
视觉: [相机] ──GigE── [视觉控制器/算法] ──TCP/Modbus── [采集器] ──MQTT── [平台]
延迟 100ms~数秒 跳数 2(图片另走文件通道)
6. 采集架构:边缘采集器 → 消息总线 → 时序库 → 应用层
这是一套经过大量项目验证的主流架构。FastBee、ThingsBoard、以及大多数国产工业互联网平台基本都是这个形状。
┌─────────────────────────────────────────────────────────────────────────────────┐
│ L0/L1 现场设备层 │
│ │
│ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ ┌──────────────┐ │
│ │ S7-1200│ │S7-1500 │ │ 三菱Q │ │ 汇川PLC│ │ 仪表 │ │ CNC / 机器人│ │
│ │ (S7) │ │ (S7) │ │ (MC) │ │(Modbus)│ │(RTU) │ │ (OPC UA) │ │
│ └───┬────┘ └───┬────┘ └───┬────┘ └───┬────┘ └───┬────┘ └──────┬───────┘ │
│ │S7comm │S7comm │MC/TCP │MB/TCP │RS485 │ UA │
└───────┼───────────┼───────────┼───────────┼───────────┼──────────────┼───────────┘
│ │ │ │ │ │
│ │ │ │ ┌──┴───┐ │
│ │ │ │ │串口 │ │
│ │ │ │ │服务器│ │
│ │ │ │ └──┬───┘ │
└───────────┴───────────┴───────────┴───────────┴──────────────┘
│ 工业交换机(VLAN 隔离)
┌───────────────────────────────┼─────────────────────────────────────────────────┐
│ L2.5 边缘层(Edge) ▼ │
│ ┌────────────────────────────────────────────────────────────────────┐ │
│ │ 边缘采集器 (Edge Collector) │ │
│ │ ┌──────────────────────────────────────────────────────────────┐ │ │
│ │ │ 1. 驱动层 Driver Manager │ │ │
│ │ │ ModbusTcpDriver | S7Driver | MCDriver | OpcUaDriver │ │ │
│ │ │ ─ 连接复用(一个 PLC 一条长连接,批量读) │ │ │
│ │ │ ─ 自动重连 + 指数退避 │ │ │
│ │ └──────────────────────────────────────────────────────────────┘ │ │
│ │ ┌──────────────────────────────────────────────────────────────┐ │ │
│ │ │ 2. 点表层 Tag Table(本地缓存,支持离线加载) │ │ │
│ │ │ 设备 → 测点 → {地址, 数据类型, 字节序, 采集频率, 死区, 量程} │ │ │
│ │ └──────────────────────────────────────────────────────────────┘ │ │
│ │ ┌──────────────────────────────────────────────────────────────┐ │ │
│ │ │ 3. 调度层 Polling Scheduler │ │ │
│ │ │ 按频率分组:100ms组 / 1s组 / 10s组 / 60s组 │ │ │
│ │ │ 分组批量合并读(把连续地址合成一次报文) │ │ │
│ │ └──────────────────────────────────────────────────────────────┘ │ │
│ │ ┌──────────────────────────────────────────────────────────────┐ │ │
│ │ │ 4. 解码层 Decoder │ │ │
│ │ │ 字节 → 类型 → 工程量(RAW→EGU) → 质量码(GOOD/BAD) │ │ │
│ │ │ (字节序处理详见 03-字节序与数据类型编码.md) │ │ │
│ │ └──────────────────────────────────────────────────────────────┘ │ │
│ │ ┌──────────────────────────────────────────────────────────────┐ │ │
│ │ │ 5. 缓冲层 Local Store(断网续传的关键) │ │ │
│ │ │ 内存环形队列 + 磁盘持久化(SQLite/LevelDB/文件) │ │ │
│ │ └──────────────────────────────────────────────────────────────┘ │ │
│ │ ┌──────────────────────────────────────────────────────────────┐ │ │
│ │ │ 6. 上行层 Publisher(MQTT Client, QoS1, 自动重连, Clean=F) │ │ │
│ │ └──────────────────────────────────────────────────────────────┘ │ │
│ │ ┌──────────────────────────────────────────────────────────────┐ │ │
│ │ │ 7. 写通道 Write Channel(独立鉴权 + 白名单 + 二次确认 + 审计)│ │ │
│ │ └──────────────────────────────────────────────────────────────┘ │ │
│ └────────────────────────────────────────────────────────────────────┘ │
└───────────────────────────────────┬─────────────────────────────────────────────┘
│ MQTT over TLS (8883)
▼
┌─────────────────────────────────────────────────────────────────────────────────┐
│ L3 平台层(Cloud / 私有云) │
│ │
│ ┌──────────────┐ ┌──────────────────┐ ┌───────────────────────────────┐ │
│ │ MQTT Broker │──▶│ 规则引擎/接入 │──▶│ 时序数据库 │ │
│ │ EMQX/Mosquitto│ │ 解析 · 校验 · │ │ TDengine / InfluxDB / │ │
│ │ │ │ 告警判定 · 转发 │ │ IoTDB / ClickHouse │ │
│ └──────────────┘ └──────────────────┘ └───────────────┬───────────────┘ │
│ ▲ │ │ │
│ │ ▼ ▼ │
│ │ ┌──────────────────┐ ┌──────────────────────────┐ │
│ │ │ 业务关系库 │ │ 数据服务 API │ │
│ └────────────│ MySQL/PostgreSQL │ │ 最新值 / 历史 / 聚合 │ │
│ 下行指令 │ 设备·产品·点表·用户│ └────────────┬─────────────┘ │
│ └──────────────────┘ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────────────────────┐ │
│ │ 应用层:实时看板 | 报警中心 | OEE 分析 | 能耗管理 | 工艺追溯 | 预测性维护 │ │
│ └──────────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────────┘
各层的职责边界(重要)
| 层 | 必须做的事 | 绝对不要做的事 |
|---|---|---|
| 设备 | 提供数据 | 被采集平台写崩 |
| 驱动 | 建连、报文收发、超时重试、解码 | 理解业务语义(它只该知道"读 20 号寄存器 10 个字") |
| 点表 | 把地址翻译成业务语义 | 放在采集器进程的内存里(必须持久化) |
| 调度 | 控频、合批、错峰 | 无节制并发(会把 PLC 打死) |
| 缓冲 | 断网时兜住数据 | 无限增长(必须有限额和落盘策略) |
| Broker | 传输 | 做业务计算 |
| 时序库 | 存时序、做降采样 | 存设备主数据(那是关系库的事) |
7. 点表(Tag)与测点模型设计
7.1 什么是点表
**点表(Tag Table / 测点表 / 位号表)**是"设备内部地址"与"业务语义"之间的映射字典。它是采集系统里最核心、也最容易被低估的设计。
一个测点至少要说清楚这 9 件事:
| 要素 | 说明 | 示例 |
|---|---|---|
| 设备 | 属于哪台物理设备 | INJ-01(1 号注塑机) |
| 测点标识 | 平台内唯一标识 | INJ-01.BarrelTemp.Zone1 |
| 字段/地址 | 协议相关的定位信息 | S7: DB3.DBD10;Modbus: HR:400101;MC: D100 |
| 数据类型 | 决定读几个字节、怎么解释 | FLOAT32 |
| 字节序 | 决定多字节怎么拼 | CDAB(详见 03 篇) |
| 采集频率 | 多久读一次 | 1000ms |
| 读写属性 | RO / WO / RW | RO |
| 量程与缩放 | RAW → EGU 的线性变换 | RAW 0~27648 → 0~300.0 ℃ |
| 死区与精度 | 变化多少才上报、保留几位小数 | 死区 0.5,精度 1 位 |
| 质量与单位 | 单位、质量码 | ℃, GOOD |
7.2 测点表结构设计(关系库)
-- 设备表
CREATE TABLE device (
id BIGINT PRIMARY KEY,
device_key VARCHAR(64) NOT NULL COMMENT '平台内唯一标识',
device_name VARCHAR(128) NOT NULL,
product_key VARCHAR(64) NOT NULL COMMENT '所属产品/型号',
protocol VARCHAR(32) NOT NULL COMMENT 'MODBUS_TCP / S7 / MC / OPCUA',
conn_params TEXT COMMENT 'JSON: host,port,slot,rack,unitId,timeout...',
poll_enabled TINYINT DEFAULT 1,
status VARCHAR(16) DEFAULT 'OFFLINE',
created_at DATETIME,
UNIQUE KEY uk_device_key (device_key)
);
-- 测点定义表(核心)
CREATE TABLE tag (
id BIGINT PRIMARY KEY,
device_id BIGINT NOT NULL,
tag_key VARCHAR(64) NOT NULL COMMENT '测点标识,如 BarrelTemp.Zone1',
tag_name VARCHAR(128) COMMENT '中文名:料筒1区温度',
address VARCHAR(128) NOT NULL COMMENT '协议地址: DB3.DBD10 / 400101 / D100',
data_type VARCHAR(32) NOT NULL COMMENT 'BOOL/INT16/UINT16/INT32/UINT32/FLOAT32/DOUBLE64/STRING/BCD',
byte_order VARCHAR(8) NOT NULL DEFAULT 'ABCD' COMMENT 'ABCD/BADC/CDAB/DCBA',
bit_index TINYINT DEFAULT NULL COMMENT 'BOOL 类型时在字内的位号 0-15',
access_mode VARCHAR(8) NOT NULL DEFAULT 'RO' COMMENT 'RO/WO/RW',
-- 采集策略
collect_mode VARCHAR(16) NOT NULL DEFAULT 'POLL' COMMENT 'POLL/RBE/SUBSCRIBE/TRIGGER',
poll_interval INT NOT NULL DEFAULT 1000 COMMENT '采集周期 ms',
deadband DOUBLE DEFAULT NULL COMMENT '死区,变化超过该值才上报',
-- 工程量转换
raw_min DOUBLE DEFAULT NULL COMMENT '原始值下限',
raw_max DOUBLE DEFAULT NULL,
eng_min DOUBLE DEFAULT NULL COMMENT '工程量下限',
eng_max DOUBLE DEFAULT NULL,
scale_factor DOUBLE DEFAULT 1 COMMENT '线性系数 a (EGU = a*RAW + b)',
scale_offset DOUBLE DEFAULT 0 COMMENT '线性偏移 b',
-- 展示与校验
unit VARCHAR(16) COMMENT '单位 ℃/MPa/kW',
precision_num TINYINT DEFAULT 2 COMMENT '小数位数',
value_min DOUBLE DEFAULT NULL COMMENT '合理范围下限(越界判 BAD)',
value_max DOUBLE DEFAULT NULL,
enum_map TEXT COMMENT 'JSON: {"0":"停止","1":"运行","2":"故障"}',
-- 扩展
group_name VARCHAR(64) COMMENT '分组,便于批量管理',
enabled TINYINT DEFAULT 1,
remark VARCHAR(256),
UNIQUE KEY uk_device_tag (device_id, tag_key),
INDEX idx_device (device_id)
);
-- 测点最新值表(热表,供实时看板秒级读取)
CREATE TABLE tag_latest (
device_id BIGINT,
tag_key VARCHAR(64),
raw_value VARCHAR(64) COMMENT '原始值(保留便于排查)',
eng_value DOUBLE COMMENT '工程量值',
str_value VARCHAR(256) COMMENT '字符串/枚举值',
quality VARCHAR(16) DEFAULT 'GOOD' COMMENT 'GOOD/BAD/STALE/UNCERTAIN',
ts_device DATETIME(3) COMMENT '设备侧时间戳(若设备提供)',
ts_collect DATETIME(3) COMMENT '采集时刻(边缘)',
ts_server DATETIME(3) COMMENT '平台入库时刻',
PRIMARY KEY (device_id, tag_key)
);
7.3 Java 侧测点模型
package com.fastbee.iot.model;
import java.io.Serializable;
/**
* 测点定义(点表条目)。
*
* 设计原则:
* 1) 地址(address)、数据类型、字节序这三项是"协议相关"的,其余都是"业务相关"的;
* 2) 采集器只依赖这个对象,不依赖任何具体协议实现;
* 3) 这个对象必须可序列化 —— 它要能下发到边缘采集器并持久化。
*/
public class TagDefinition implements Serializable {
private static final long serialVersionUID = 1L;
/** 数据类型枚举 */
public enum DataType {
BOOL, BYTE, INT16, UINT16, INT32, UINT32, INT64,
FLOAT32, DOUBLE64, STRING, BCD16, BCD32, DATE_TIME
}
/** 字节序:详见 03-字节序与数据类型编码.md */
public enum ByteOrder { ABCD, BADC, CDAB, DCBA }
/** 采集模式 */
public enum CollectMode { POLL, RBE, SUBSCRIBE, TRIGGER }
/** 读写属性 */
public enum AccessMode { RO, WO, RW }
private String tagKey; // 测点唯一标识
private String tagName; // 中文名
private String address; // 协议地址,如 "DB3.DBD10" / "400101" / "D100"
private DataType dataType; // 数据类型
private ByteOrder byteOrder = ByteOrder.ABCD;
private Integer bitIndex; // BOOL 位号
private Integer length; // 字符串长度 / 读取字(寄存器)数
private AccessMode accessMode = AccessMode.RO;
private CollectMode collectMode = CollectMode.POLL;
private int pollIntervalMs = 1000;
private Double deadband; // 死区
private Double rawMin; // RAW 下限
private Double rawMax; // RAW 上限
private Double engMin; // EGU 下限
private Double engMax; // EGU 上限
private double scaleFactor = 1.0;
private double scaleOffset = 0.0;
private String unit;
private int precision = 2;
private Double valueMin; // 合理值下限(越界 → BAD)
private Double valueMax;
private String groupName;
/**
* RAW → EGU 线性变换。
* 优先使用 rawMin/rawMax/engMin/engMax 的两点标定;
* 未配置量程时退化为 EGU = a * RAW + b。
*/
public double toEng(double raw) {
if (rawMin != null && rawMax != null && engMin != null && engMax != null
&& Math.abs(rawMax - rawMin) > 1e-9) {
return engMin + (raw - rawMin) * (engMax - engMin) / (rawMax - rawMin);
}
return scaleFactor * raw + scaleOffset;
}
/** EGU → RAW 反向变换(写操作前必须调用,把工程量还原成设备码值) */
public double toRaw(double eng) {
if (rawMin != null && rawMax != null && engMin != null && engMax != null
&& Math.abs(rawMax - rawMin) > 1e-9) {
return rawMin + (eng - engMin) * (rawMax - rawMin) / (engMax - engMin);
}
if (Math.abs(scaleFactor) < 1e-9) {
throw new IllegalStateException("scaleFactor 为 0,无法反算: " + tagKey);
}
return (eng - scaleOffset) / scaleFactor;
}
/** 值是否在合理范围内(超出判为 BAD 质量码,而不是直接丢弃) */
public boolean inRange(double eng) {
return (valueMin == null || eng >= valueMin)
&& (valueMax == null || eng <= valueMax);
}
// ===== getter / setter 省略,实际项目用 Lombok 或 IDE 生成 =====
public String getTagKey() { return tagKey; }
public void setTagKey(String tagKey) { this.tagKey = tagKey; }
public String getAddress() { return address; }
public void setAddress(String address) { this.address = address; }
public DataType getDataType() { return dataType; }
public void setDataType(DataType dataType) { this.dataType = dataType; }
public ByteOrder getByteOrder() { return byteOrder; }
public void setByteOrder(ByteOrder byteOrder) { this.byteOrder = byteOrder; }
public CollectMode getCollectMode() { return collectMode; }
public void setCollectMode(CollectMode collectMode) { this.collectMode = collectMode; }
public int getPollIntervalMs() { return pollIntervalMs; }
public void setPollIntervalMs(int pollIntervalMs) { this.pollIntervalMs = pollIntervalMs; }
}
7.4 Modbus 地址模型(最容易搞错的一块)
Modbus 用"区号 + 偏移"定位数据,而且协议报文里的地址是 0 基偏移,人类书写的地址是 1 基。这个 1 的差值是新手第一大坑。
| 区 | 人类写法 | 含义 | 读写 | 功能码 | 报文内偏移(0 基) |
|---|---|---|---|---|---|
| 0x | 00001 ~ 09999 |
线圈 Coil(位) | 读写 | 01 读 / 05 写单 / 0F 写多 | 00001 → 0 |
| 1x | 10001 ~ 19999 |
离散输入 Discrete Input(位) | 只读 | 02 | 10001 → 0 |
| 3x | 30001 ~ 39999 |
输入寄存器 Input Register(16 位字) | 只读 | 04 | 30001 → 0 |
| 4x | 40001 ~ 49999 |
保持寄存器 Holding Register(16 位字) | 读写 | 03 读 / 06 写单 / 10(0x10) 写多 | 40001 → 0 |
例:点表上写 "400101",实际报文里要填 100(十进制)或 0x0064
换算:offset = 人类地址 - 40001 = 400101 - 400001 = 100
(注意五位数与六位数的基数不同:40001 系列基数 40001;
400001 系列基数 400001,两种写法都存在于现场,必须确认)
常用功能码:
0x01 读线圈 0x02 读离散输入
0x03 读保持寄存器 0x04 读输入寄存器
0x05 写单个线圈 0x06 写单个保持寄存器
0x0F 写多个线圈 0x10 写多个保持寄存器
0x17 读/写多个寄存器(少见)
异常响应:功能码 | 0x80,后续 1 字节异常码
常见异常码 0x01 非法功能 / 0x02 非法数据地址 / 0x03 非法数据值
强烈建议:点表里存的永远是**"报文偏移量(0 基)+ 区号"**,而不是
400101这种字符串。字符串地址在各家设备上歧义太大(S7 的DB3.DBW10、MC 的D100、Modbus 的400101完全不同),让点表存结构化字段,UI 层再负责渲染成人类可读格式。
8. 采集模式:轮询 / 变化上报 / 订阅 / 触发
| 模式 | 原理 | 实时性 | 带宽占用 | 实现复杂度 | 适用协议/场景 |
|---|---|---|---|---|---|
| 轮询 (Polling) | 采集器按固定周期主动发读请求 | 取决于周期,≤周期延迟 | 高(大量无效读) | 低 | Modbus RTU/TCP、S7comm、MC 的默认模式;绝大多数现场 |
| 变化上报 (RBE, Report by Exception) | 设备/采集器只在值变化(或超死区)时上报 | 高(变化时立即) | 低 | 中(需死区配置 + 缓存最新值) | 适用于慢变量(温度、液位、累计量);也可在采集器侧"轮询 + 死区过滤"模拟 |
| 订阅 (Subscribe) | 建立会话后由服务端主动推送变化 | 高(10~100ms 级) | 低(只推变化) | 高(会话管理、存活检测、重订阅) | OPC UA Subscription、EtherNet/IP implicit、Profinet cyclic |
| 触发采集 (Trigger) | 外部事件触发一次性采集 | 事件驱动,延迟最低 | 极低 | 中(需事件源) | 条码扫描后读 PLC、工单下达时抓快照、报警时抓上下文 |
带宽与实时性量化对比
假设 1000 个测点,平均每个值变化周期 10 秒,单条上报负载 60 字节:
| 模式 | 采集周期 | 每秒请求数 | 每秒上行流量 | 平均延迟 | 评价 |
|---|---|---|---|---|---|
| 纯轮询 1s | 1000ms | 1000 次(合批后约 50~200 报文) | ~60 KB/s(全量上报) | ≤1s | 简单,但 90% 数据是重复的 |
| 纯轮询 10s | 10s | 100 次 | ~6 KB/s | ≤10s | 慢变量够用,快变量丢细节 |
| 轮询 1s + 死区上报 | 1000ms | 1000 次读,~100 条上报 | ~6 KB/s | ≤1s | 推荐折中:读得勤、传得省 |
| 订阅 (OPC UA) | 由服务端决定 | 0(服务端推) | ~6 KB/s | 10~500ms | 最优,但依赖设备支持与 Server 性能 |
| 触发 | 事件驱动 | 不定 | 极低 | 事件后立即 | 特殊场景补充 |
工程实践结论:在 Modbus/S7/MC 这类"只能轮询"的协议上,"高频轮询 + 边缘死区过滤 + 变化上报"几乎总是最优解。原因:读一次的边际成本很低(一条报文能读 100 多个寄存器),而上云的边际成本高(流量、存储、数据库写入)。所以把判断放在边缘,把传输留给变化。
合批(Batching)是轮询的生命线
错误做法:100 个测点,每个点一条读报文
→ 100 次 TCP 往返,PLC 通信负载 100 倍,延迟 = 100 × RTT
正确做法:按"设备 + 区 + 地址连续性"合并
地址 400101~400110 连续 → 1 条报文读 10 个寄存器
地址 400150~400155 连续 → 1 条报文读 6 个寄存器
→ 2 条报文搞定,延迟 = 2 × RTT
合批约束(务必注意):
· Modbus 单次最多读 125 个保持寄存器(0x7D),超过必须拆分
· S7comm 单次 PDU 大小受协商限制(S7-1200 常见 240 字节,需实测确认)
· MC 协议单次读取点数也有限制(按位/按字单位不同,需实测确认)
· 跨"不连续地址"不要硬合:读 1000 个寄存器只为拿其中 2 个是浪费
· 不同采集频率的点不要放进同一批
9. 采集质量指标:怎么衡量"采得好不好"
没有指标就没法验收。交付项目时,下面这张表应该写进技术协议。
| 指标 | 定义 | 典型目标值 | 测量方法 |
|---|---|---|---|
| 采集周期 (Cycle) | 同一测点两次成功采集的间隔 | 配置值 ±10% | 统计相邻两条数据时间戳差值的分布 |
| 周期抖动 (Jitter) | 实际周期的标准差 / P99 偏差 | P99 ≤ 周期 × 1.5 | 对周期序列求 P50/P95/P99 |
| 采集成功率 | 成功读数次数 / 总尝试次数 | ≥ 99.9% | 驱动层埋点计数 |
| 丢包率 | 未成功上报到平台的数据条数 / 总采集条数 | ≤ 0.1%(断网续传后趋近 0) | 边缘计数 vs 平台入库计数对比 |
| 断线重连时间 | 从检测到断线到恢复采集的耗时 | ≤ 30s(TCP 场景通常 ≤10s) | 模拟断网,记录恢复时刻 |
| 数据完整率 | 指定时间窗内应有数据点数 / 实际点数 | ≥ 99% | 按测点、按小时统计空洞 |
| 端到端延迟 | 设备值变化 → 平台可见 | ≤ 3 × 采集周期 | 打时间戳对比(需时钟同步) |
| 时钟偏差 | 采集器与平台时钟差 | ≤ 1s(用 NTP 后可 ≤100ms) | NTP 对时,定期巡检 |
| PLC 通信负载 | 采集占用的 PLC 通信资源占比 | 通信负载 ≤ 30% | PLC 侧诊断(西门子 CPU 的通信负载率) |
时间戳:这是最容易被忽略、事后最难追的坑
一条数据至少要带 3 个时间戳,而且必须分开存:
ts_device 设备侧时间戳 设备自己打的时间(如 PLC 的 DATE_AND_TIME)
├─ 优点:反映"这个值是设备什么时候产生的"
└─ 缺点:设备时钟可能不准、可能没对时、可能为 0
ts_collect 采集时刻 边缘采集器读到这个值的本地时刻
├─ 优点:一定有,反映采集行为的真实节奏
└─ 缺点:包含通信延迟,不是物理量发生的精确时刻
ts_server 入库时刻 平台收到并落库的时刻
├─ 优点:一定有,用于排查链路延迟
└─ 缺点:受网络与消息队列积压影响,抖动最大
判断原则:
- 默认用
ts_collect作为数据的主时间轴。它是三者中最可靠的折中。 ts_device只在设备明确提供且已对时时才采信,并且要记录偏差,偏差超过阈值(如 5s)就降级不用并告警。- 绝对不要用
ts_server做业务分析。它会把网络抖动"注入"到工艺分析里,导致趋势曲线出现假峰。 - 补传数据的两难:断网期间缓存的数据,恢复后上报,
ts_collect是过去的时间。这时要支持"历史回填"写入(TDengine/IoTDB 支持写入指定时间戳),并且标记quality = STALE或者加一个late标志,让应用层知道这批数据是补的。
10. 采集难点清单:现场踩坑总纲
这一节是本篇最值钱的部分。下面每条都对应真实项目里会遇到的麻烦。
| # | 难点 | 具体表现 | 应对思路 |
|---|---|---|---|
| 1 | 协议碎片化 | 同一个"读寄存器",Modbus/S7/MC 的地址格式、报文、错误码完全不同;同品牌不同系列还不一样 | 抽象统一 Driver 接口;协议差异收在驱动内部;点表用结构化字段而非字符串 |
| 2 | 字节序与字序 | 32 位值读出来是 1.2e-38 这种天文数字 |
点表显式配置 ABCD/BADC/CDAB/DCBA;详见 03-字节序与数据类型编码.md |
| 3 | 位操作 | 一个 16 位寄存器打包 16 个 bool;一个字节里 8 个报警位 | 点表支持 bitIndex;位序(MSB/LSB)要单独配置 |
| 4 | 寄存器地址映射 | 400101 是 0 基还是 1 基?DB3.DBW10 的 DB 是不是优化块? |
统一为"0 基偏移 + 区号";S7 优化块需关闭优化或走符号寻址 |
| 5 | 网络不稳 | 现场交换机老化、网线被叉车压断、4G 信号波动、WiFi 干扰 | TCP 长连接 + 心跳 + 指数退避重连 + 边缘缓存 |
| 6 | 连接数上限 | PLC 同时能承载的 TCP 连接数非常有限(S7-1200 的通信连接资源受型号/固件限制,工程上常按 8 路左右估算,需实测确认);一个 PLC 被 HMI + SCADA + 采集器 + 调试 PG 抢连接 | 每个 PLC 只开一条连接并复用;采集器做连接池;不要为每个测点建连接 |
| 7 | 写入安全 | 写错地址 → 设备误动作;写错值 → 工艺事故;写得太快 → 覆盖人工操作 | 写通道独立鉴权;地址白名单;量程校验;二次确认;写前读回校验;全量审计日志 |
| 8 | 脏数据 | 设备掉电瞬间寄存器是随机值;通信中断期间值是旧的 | 质量码机制(GOOD/BAD/STALE);合理范围校验;断线期间标记 STALE 而非沿用旧值 |
| 9 | 数据语义缺失 | 拿到 20150 但不知道是温度还是压力 |
点表是唯一解,必须有 |
| 10 | 时钟不同步 | 设备、边缘、平台三个时钟各走各的 | NTP 强制对时;三个时间戳分开存 |
| 11 | 老设备无文档 | 手册丢失、厂家倒闭、程序无源码 | 抓包反推(详见 02-协议栈与工业网络.md);Wireshark + 穷举地址 |
| 12 | 干扰与接地 | RS485 通信时好时坏,跟天气/开机有关 | 屏蔽双绞线、单点接地、终端电阻 120Ω、远离动力线 |
| 13 | PLC 扫描周期 | 采得快没用,PLC 本身 20ms 才刷新一次数据 | 采集周期 ≥ PLC 扫描周期 × 2;理解"数据新鲜度"上限 |
| 14 | 授权与合规 | 甲方要求不连生产网、要求等保、要求数据不出厂 | 单向网闸/工业防火墙;DMZ 区部署;数据脱敏 |
关于"写操作安全"的专门说明
写通道必须和读通道是两个独立的东西,至少要做到:
1. 权限隔离:读写权限分开授予,默认所有点只读
2. 地址白名单:只能写点表里 accessMode 含 W 的地址
3. 值域校验:先过 engMin/engMax,再过 valueMin/valueMax
4. 类型校验:FLOAT32 的点不能写 100000 这种溢出值
5. 二次确认:关键写操作(启停、设定值)要求带 ticket/token
6. 写前读回:写入后立即读回比对,不一致要报警
7. 限速:同一地址单位时间内的写次数上限(防程序 BUG 疯狂写)
8. 审计:谁、什么时候、写了哪个点、从什么值到什么值、来源 IP
9. 会话绑定:写操作必须能追溯到具体用户会话,禁止匿名写
10. 物理联锁:真正危险的动作(启动电机、开阀)应由 PLC 程序做最终判断,
采集平台只下发"请求",不直接驱动执行器
11. 数据上云:MQTT 主题、QoS、断网续传与压缩
11.1 MQTT 主题设计
主题(Topic)是 MQTT 的路由键,设计得好能省掉大量后端逻辑。推荐的分层结构:
{租户}/{产品}/{设备}/{消息类型}
遥测(时序数据,量最大)
iot/{tenantId}/{productKey}/{deviceKey}/telemetry
属性(设备静态/半静态属性)
iot/{tenantId}/{productKey}/{deviceKey}/attributes
事件(报警、状态变化,低频但重要)
iot/{tenantId}/{productKey}/{deviceKey}/event
命令下发响应
iot/{tenantId}/{productKey}/{deviceKey}/cmd/reply/+
生命周期(上线/下线/心跳)
iot/{tenantId}/{productKey}/{deviceKey}/lifecycle
下行(平台 → 设备)
iot/{tenantId}/{productKey}/{deviceKey}/cmd/down
iot/{tenantId}/{productKey}/{deviceKey}/config/down # 点表下发
设计要点:
- 不要用 topic 承载测点名。把测点放进 payload 的 JSON 数组里,一条消息打包多个测点。否则 1000 个测点就是 1000 个 topic,Broker 的订阅树会爆炸,且无法批量。
- 租户必须在第一层,方便用 ACL 做隔离(
iot/{tenantId}/#)。 - 不要用
/开头(iot/...而不是/iot/...),会多出一个空层级。 - 不要用
+/#在发布端,通配符只能用于订阅。 - 设备侧订阅自己的下行 topic,平台侧订阅
$share/组名/...(EMQX 共享订阅)做负载均衡。
11.2 Payload 结构
{
"msgId": "9f2c1a80-7b11-4e2a-9c31-001122334455",
"deviceKey": "INJ-01",
"ts": 1717171717000,
"type": "telemetry",
"values": [
{ "tag": "BarrelTemp.Zone1", "v": 218.6, "q": 0, "ts": 1717171716980 },
{ "tag": "BarrelTemp.Zone2", "v": 221.1, "q": 0, "ts": 1717171716980 },
{ "tag": "MotorCurrent", "v": 32.45, "q": 0, "ts": 1717171716980 },
{ "tag": "RunStatus", "s": "RUNNING", "q": 0, "ts": 1717171716980 },
{ "tag": "AlarmCode", "v": 0, "q": 1, "ts": 1717171716980 }
]
}
字段说明:
q(quality):0=GOOD,1=BAD(通信失败/超量程),2=STALE(断线期间/补传),3=UNCERTAINv数值、s字符串/枚举,二选一。分开存能避免 JSON 数值精度问题。ts是采集时刻(ts_collect),不带则是平台入库时刻。- 一条消息打包多个测点,但单条消息别超过 256 KB(MQTT 协议上限),工程上建议 ≤ 32 KB。
11.3 QoS 选择
| QoS | 语义 | 报文开销 | 适用场景 | 工业采集建议 |
|---|---|---|---|---|
| 0 | 至多一次,fire and forget | 1 个报文 | 高频可丢数据 | 100ms 级振动数据、视频流元数据 |
| 1 | 至少一次,可能重复 | 2 个报文(PUBLISH + PUBACK) | 绝大多数遥测 | 推荐默认 |
| 2 | 恰好一次 | 4 个报文(PUBLISH/PUBREC/PUBREL/PUBCOMP) | 不允许重复的关键消息 | 写指令响应、计费数据 |
判断:遥测用 QoS 1 是最优解。QoS 2 的四次握手在弱网下会显著增加延迟和重传风暴,而"重复"的问题在时序库侧更好解决——让 payload 带唯一 msgId,入库时做幂等去重(或利用时序库对相同时间戳的覆盖语义)。
11.4 断网续传(边缘缓存)
这是边缘采集器必须具备的能力,也是区分"玩具"和"产品"的分水岭。
设计要点:
1. 分层缓冲
内存环形队列(快,断电即失)
↓ 队列水位 > 80%
磁盘持久化(SQLite / LevelDB / 分片文件)
↓ 磁盘配额 > 阈值
丢弃策略(优先丢最老的,或优先丢高频低价值点)
2. 配额管理
按磁盘剩余空间动态计算配额(如最多 1 GB 或 100 万条,取小值)
关键报警事件单独队列,永不被遥测挤掉
3. 恢复策略
重连后限速补传(避免瞬间打爆 Broker 和数据库)
补传与实时数据分流:实时数据优先,补传走低优先级队列
补传数据打 late 标记,写入时用 ts_collect 作为时间戳
4. 一致性
本地队列必须持久化 msgId 与写入位点,防止重复补传
上报成功(收到 PUBACK)后才从队列删除
5. 简单实现示例(Java 伪结构,可直接落为代码)
┌──────────────────────────────────────────────┐
│ class OfflineBuffer { │
│ ArrayBlockingQueue<Packet> memQueue; │
│ DiskQueue diskQueue; // 溢出落盘 │
│ AtomicBoolean online; │
│ void offer(Packet p) { │
│ if (online.get() && memQueue.offer(p)) { │
│ publishAsync(p); │
│ } else { │
│ diskQueue.append(p); │
│ } │
│ } │
│ void onReconnect() { │
│ while (diskQueue.hasMore() && online) { │
│ publishSync(diskQueue.peek()); │
│ diskQueue.remove(); │
│ rateLimit(); │
│ } │
│ } │
│ } │
└──────────────────────────────────────────────┘
11.5 时序数据压缩
| 方法 | 原理 | 压缩比 | 适用场景 |
|---|---|---|---|
| 死区过滤 | 变化 < 阈值不上报 | 5~50× | 慢变量(温度、液位),最实用 |
| 变化率过滤 | 变化速率 < 阈值不上报 | 2~10× | 缓慢漂移的量 |
| 批量打包 | 多条合并成一个 payload | 2~5×(省协议头) | 通用 |
| 数值精度裁剪 | 218.6345678 → 218.6 | 1.5~3× | 展示类数据 |
| JSON → Protobuf/CBOR | 二进制编码代替文本 | 3~8× | 带宽紧张(4G 按流量计费) |
| 时间戳差分 (Delta) | 存时间差而非绝对时间 | 2~5× | 固定周期采集 |
| 列式压缩(库侧) | TDengine/IoTDB 内置 | 5~20× | 存储侧,与传输无关 |
| Delta-of-Delta + XOR | Facebook Gorilla / InfluxDB 浮点压缩 | 10×+ | 库侧自动完成 |
实践建议:不要一开始就把压缩做得太复杂。先做死区过滤 + 批量打包这两件,通常就能解决 80% 的问题。Protobuf 改造收益大但会让调试变痛苦(抓包看不懂),只在 4G/NB-IoT 按流量计费的场景才值得。
小结
- 采集的本质是跨 OT/IT 边界的翻译:把 PLC 里的字节翻译成有业务语义、有单位、有质量码、有时间戳的数据。
- ISA-95 五层模型是沟通工具:物联网平台通常卡在 L2.5,向 L3/L4 供数;不要越权直写 L1。
- 一次完整旅程包含 11 步:物理量 → 4~20mA → A/D 码值 → PLC 地址 → 协议读取 → 字节解码 → 工程量转换 → 死区过滤 → MQTT 上报 → 入库 → 应用。中间的转换知识不在设备里,只能靠点表。
- 采集方式有五种:协议直采 / OPC UA / 网关透传 / 硬接线 IO / 视觉。判断标准是"延迟、成本、可靠性、施工难度"四维权衡;能直采不加 OPC,能通信不硬接线,但关键状态信号值得硬接线。
- 主流架构是六层:设备 → 边缘采集器(驱动/点表/调度/解码/缓冲/上行/写通道)→ MQTT Broker → 规则引擎 → 时序库 → 应用。边缘层是重点,它承担了断网续传、死区过滤、协议适配三件最脏的活。
- 点表是采集平台最核心的资产,一个测点要说清 9 件事:设备、标识、地址、类型、字节序、频率、读写属性、量程、死区。地址建议存"0 基偏移 + 区号"的结构化字段而非
400101字符串。 - 采集模式首选"高频轮询 + 边缘死区 + 变化上报"。Modbus/S7/MC 都只能轮询,但把"读"和"传"解耦之后,可以在低成本下拿到高实时性。合批是轮询的生命线。
- 质量指标要写进技术协议:采集周期、抖动、成功率、丢包率、断线重连时间、完整率、端到端延迟、时钟偏差、PLC 通信负载。
- 时间戳必须三分离:
ts_device(设备)/ts_collect(采集)/ts_server(入库)。业务分析默认用ts_collect,绝不用ts_server。 - 三大 hardest 难点:字节序(见 03 篇)、连接数上限(每 PLC 单连接复用)、写操作安全(读通道与写通道必须是两个安全等级)。
- 上云用 MQTT:主题
{租户}/{产品}/{设备}/{类型},测点放 payload 不放 topic;遥测默认 QoS 1 并在库侧幂等去重;边缘必须有分级缓冲与限速补传。
常见误区
- 误区 1:采集频率越高越好。 错。采集周期必须 ≥ PLC 扫描周期的 2 倍,超过设备刷新能力的高频采集只是在重复读同一个值,还会把 PLC 通信资源占满,影响 HMI 和 SCADA 的正常通信。
- 误区 2:PLC 一定能同时接很多客户端。 错。PLC 的通信连接数是硬资源,HMI、SCADA、PG、采集器要共享。正确做法是一台设备一条连接、采集器内复用,而不是每个测点或每个业务模块各开一条。
- 误区 3:数据读到就算采到了。 错。没有点表(语义、量程、单位)的采集数据无法用于分析;没有质量码的数据无法区分"真的没变化"和"通信断了所以没变化"。
- 误区 4:断线期间沿用上一个值。 错。这会让看板上显示一个"看起来正常但其实是 3 小时前"的值,是最危险的假象。断线期间应该标记
STALE或让前端显示"数据过期"。 - 误区 5:用平台入库时间做工艺分析。 错。
ts_server包含网络与队列抖动,会把链路噪声注入分析结论。应当使用ts_collect并对齐时钟。 - 误区 6:用 Modbus 写来实现控制。 错。寄存器写操作没有事务性、没有联锁、没有确认机制,也不受 PLC 程序的安全逻辑保护。危险动作应下发"请求",由 PLC 程序做最终判断。
- 误区 7:上了 OPC UA 就万事大吉。 错。OPC UA 只解决"统一访问接口",不解决"语义"(NodeId 依然是
ns=3;s="DB1".Temperature这种字符串,业务含义仍要人工映射),也不解决字节序与点表。 - 误区 8:遥测数据用 QoS 2 保证不丢。 错。QoS 2 的四次握手在弱网下代价极高,且时序数据天然可以用"相同时间戳覆盖"来做幂等。QoS 1 + msgId 去重是更务实的选择。
- 误区 9:多个测点用多个 MQTT topic。 错。会造成 topic 数量爆炸,且无法批量打包。测点应放在 payload 里,一条消息带多个值。
- 误区 10:采集器轮询失败就一直重试。 错。无退避的疯狂重试会在网络刚恢复的瞬间制造重连风暴,把 PLC 和交换机一起打挂。必须做指数退避 + 抖动(jitter)。
- 误区 11:认为"连上了就能读"。 错。S7 需要正确的 rack/slot 与 PDU 协商,MC 需要正确的网络号/站号,Modbus 需要正确的 unitId(从站地址);TCP 三次握手成功只代表网络通,不代表协议握手成功。
- 误区 12:把点表配置放在采集器内存或配置文件里。 错。点表是核心资产,必须持久化在数据库、可版本化、可审计、可下发,否则换一次机器就丢一次。
本文是《工业通信与数据采集》栏目第 1/22 篇 · 栏目总览 | 下一篇:协议栈与工业网络