主题
water — 水务遥测(SZY206-2016 / SL/T 427)
模块名:water
哪些平台能用
只有两个:ESP32-S3 固件(LP3232 / LP3288 等 S3 机型)和软 PLC 模拟器(Windows / Linux)。
- 普通 ESP32 / STM32 机型:固件里没有
water模块,脚本里调water.creat()会直接报错。这套终端逻辑要常驻一个独立任务和一块待确认队列,小机型放不下。 - 上位机 DTU 配置:「水务遥测」子标签页只在支持的机型上出现(依据
features.dtu.water_supported),不支持的机型看不到这一页。
拿不准手上的设备支不支持,在脚本里打一句 print(type(water)),输出 table 才算支持。
两条硬边界,配之前先确认现场符合
- 不做低功耗。 定位是常供电站点,没有休眠、没有唤醒上报。电池 + 太阳能的极低功耗站点不要用这个方案。
- 没有 RTC 芯片,掉电不保持时钟。 时钟靠「中心站对时 > NTP > 掉电前落盘的粗时间」三级方案,而且时钟没同步之前不采集也不上报——宁可缺一段数据,也不把 1970 年的假时标写进平台数据库。
第 2 条在专网(只能连到中心站、拨不通公网 NTP)场景下尤其要注意:这时唯一的对时出路是中心站下发 AFN=11H。设备连上后会立刻发一帧链路检测让平台发现它,就是为了尽快拿到这一帧对时。
这东西干什么
把 PLC 寄存器里的水位、雨量、流量,按调度编成水务规约报文上报给一到四个中心站;断网期间存起来,恢复后按原始时标补报。
要做的事只有三件:填测站身份 → 填中心站地址 → 把要素绑到寄存器。分帧、编码、重发、补报、对时全部由固件按标准做,不需要写一行协议代码。
不想写脚本?
整套功能在上位机里点几下就能配出来,见 不写 Lua:在上位机里配。建议第一次就走这条路——比脚本少一半出错机会。
三个版本怎么选
creat 的 version 参数:
| 取值 | 标准 | 什么时候选 |
|---|---|---|
water.SZY206_2016 | SZY206-2016 | 水资源监测(取水口、水源井、供水管网),流量单位习惯用 m³/h |
water.SL427_2021 | SL/T 427-2021 | 水文监测(水位站、雨量站、水库),现在的主流版本 |
water.SL427_2008 | SL 427-2008 | 老平台还在用 2008 版时选它 |
三个版本是同一条脉络,帧结构和 BCD 习惯完全一致,差异集中在要素表和个别数据域上。版本必须和平台一致——填错了帧长度和校验都对不上,平台会直接丢弃,而且不会回任何错误提示。
测站身份
平台是按地址域认站的,这两个字段由当地水利部门下发,不能自己编。
| 字段 | 范围 | 说明 |
|---|---|---|
addr_mode | 1 / 2 | 1 = 区划码 + 测站号;2 = 水文测站编码 |
region | 6 位数字 | 行政区划码,如南京 320100 |
station | 1 ~ 60000 | 测站号。60001~65534 是中继地址(本版不做),65535 是广播地址,终端不能用 |
hydro_code | 8 位 HEX | 方式二用 |
password | 0 ~ 65535 | 规约唯一的访问控制手段。中心站下发设置类命令要带上它,对不上终端不执行 |
要素表
这张表是核对量纲的唯一依据。「报文字节数」和「小数位」是标准规定的,改不了;您能改的只有 scale / offset(怎么把寄存器值换算成工程量)。
| 代码 | 名称 | 报文字节 | 小数位 | 单位 | 参数类 | 带符号 |
|---|---|---|---|---|---|---|
rainfall | 雨量 | 3 | 1 | mm | 雨量 | 否 |
water_level | 水位 | 4 | 3 | m | 水位 | 是 |
flow_rate | 瞬时流量 | 5 | 3 | m³/h | 流量 | 是 |
total_water | 累计水量 | 5 | 0 | m³ | 流量 | 是(仅最高位) |
velocity | 流速 | 3 | 3 | m/s | 流速 | 是 |
gate | 闸位 | 3 | 2 | m | 闸位 | 否 |
power | 功率 | 3 | 0 | kW | 功率 | 否 |
air_pressure | 气压 | 3 | 0 | 10Pa | 气压 | 否 |
wind | 风速 | 3 | 2 | m/s | 风速 | 否 |
water_temp | 水温 | 2 | 1 | ℃ | 水温 | 否 |
soil_moisture | 土壤含水率 | 2 | 1 | % | 土壤 | 否 |
evaporation | 蒸发量 | 3 | 1 | mm | 蒸发 | 否 |
water_pressure | 水压 | 4 | 2 | kPa | 水压 | 否 |
volt | 终端电压 | 2 | 2 | V | 电压 | 否 |
两个最容易差 100 倍的单位
- 气压是 10Pa,不是 hPa。 标准大气压 1013hPa 要上报成
10130。 - 水压是 kPa,不是 MPa。 0.35MPa 要上报成
350。
配错了平台照样收得下(数值合法、校验正确),只是曲线上的量级差两个数量级,事后极难发现是哪一环错的。
水质要素(pH、COD、氨氮、总磷、叶绿素、浊度、溶解氧)首批不实现,配了会在启动时报错而不是静默忽略——目的是让您在配置阶段就知道,而不是配上了却永远不上报。
scale / offset 怎么填
固定语义:上报的工程量 = 寄存器原值 × scale + offset。
| 现场情况 | type | scale | 举例 |
|---|---|---|---|
| 寄存器里是整数毫米,要报米(3 位小数) | word | 0.001 | 寄存器 5203 → 上报 5.203 m |
| 寄存器里是整数厘米,要报米 | word | 0.01 | 寄存器 520 → 上报 5.20 m |
| 寄存器里已经是浮点工程量 | float | 忽略 | 寄存器 5.203 → 上报 5.203 m |
| 4-20mA 原始码 0~4095 对应 0~10m | word | 0.0024420 | 需要 offset 时一起填 |
type 是 'float' 时 scale 被忽略——寄存器里本来就是工程量,再乘一遍是双重换算。这是固件的明确语义,不是 bug。
上位机在您换要素时会自动把 scale 预填成 10^-小数位,也就是「寄存器里存整数最小单位」这种最常见的接法。
为什么配 3 个要素,平台收到 3 帧
这是标准的规定,不是实现取巧。规约里一帧自报只装一个「参数类」,类别由链路层功能码表达,数据域是该类全部要素按固定宽度顺序排列——没有逐要素的标识字节。
两个直接后果,交给平台侧的人之前先说清:
- 同一类的成员总是一起发。 只配了瞬时流量、没配累计水量,累计水量的位置仍然占 5 字节、填全
FF(无效值)。这是定长布局,平台看到的是正常报文。 - 三帧带同一个时标。 时标在
Tp(5 字节:秒分时日 BCD + 1 字节延时分钟),不在数据域里。
上位机的水务页上有一块「报文分帧预览」,按您当前配的要素直接算出会发几帧、每帧装什么,可以先在那里核对。
定时上报与越限加报是两种报文
| 定时 / 手动上报 | 越限加报 | |
|---|---|---|
| AFN | C0H(定时/实时数据自报) | 81H(随机自报报警) |
| 触发 | 按 report 调度,或脚本调 report_now() | 数值穿越阈值时立刻发 |
| 平台侧 | 进历史数据表 | 进报警表 |
把定时数据也发成 81H 是最典型的对接错误——平台会把每一次周期数据都记成一条报警。固件已经按标准分开了,不需要您做什么,但看平台日志时要知道这两个 AFN 的区别。
上报调度三种模式
mode | 参数 | 含义 |
|---|---|---|
water.MODE_HOUR | offset 0~3599 | 每小时的第 offset 秒。推荐,一个区县几百个站按站号错开偏移,避免整点并发把平台打垮 |
water.MODE_INTERVAL | interval 60~86400 | 从 epoch 起每 interval 秒一次 |
water.MODE_PERIOD | period_min 必须整除 60 | 把一小时等分,如 15 → 每小时 0/15/30/45 分 |
时钟:三档质量,未同步不工作
优先级固定:中心站 > NTP > flash 粗时间。已同步之后,flash 里的粗时间绝不会把它降级。
| 字段 | 范围 | 说明 |
|---|---|---|
center_time_first | 布尔 | 中心站对时优先。专网场景必须为真 |
ntp_enable | 布尔 | 专网拨不通公网时关掉,省得每次都白等解析超时 |
ntp_hosts | 数组,取前 2 个 | 默认 ntp1.aliyun.com / cn.pool.ntp.org |
ntp_interval | 3600 ~ 86400 秒 | 重新对时间隔 |
jump_alarm | 1 ~ 86400 秒 | 时钟跳变超过它就记一条 ERC11 并投事件 |
flash_interval | 300 ~ 3600 秒 | 粗时间落盘间隔。调太小会伤 flash 寿命 |
时钟跳变不会重写已排队的报文
时钟被大幅调整时只记账 + 报事件,已经在待确认队列里等补报的帧一律按原时标发送。重写时标会让平台侧的数据错乱,而且事后分不清哪个是对的。
断网补报:不丢、不乱序、不重
- 每个中心站有各自独立的游标,总是取「自己还没确认过的最旧一条」,因此天然不乱序。
- 一帧只存一份,各中心站用位掩码各记「我确认过了没」;所有需要确认的站都确认了才释放。
- 队列满了会丢最旧的一条并投
store_full事件。丢新的会造成「恢复网络后补的全是老数据、当前值一直不刷新」,现场更难解释。 backfill_gap(100~5000ms)限制往外倒数据的速度。调太小会把 4G 链路和中心站一起打满,也会让当前的定时自报排在几百帧之后。
need_ack = false 等于放弃「不丢帧」
确认过平台真的不回应答帧才关掉它。关掉之后发出即视为成功,不再等确认也不重发——断网重连时这一段数据就真的没了。
不写 Lua:在上位机里配
上位机 → 选中设备 → DTU 功能 → 水务遥测。这一页配出来的东西和写脚本完全等价(设备端就是拿这份 JSON 去调同一套 API)。

操作顺序:
| 步骤 | 做什么 | 注意 |
|---|---|---|
| 1 | 打开「启用水务遥测」开关 | 关掉开关等于删掉整段配置 |
| 2 | 填测站身份:规约版本、区划码、测站号、密码 | 必须和平台下发的一致 |
| 3 | 加中心站,填地址端口,勾一个「主站」 | 最多 4 个(一机四发);主站是单选 |
| 4 | 设上报调度:方式 + 链路检测间隔 + 重发 / 超时 | 建议整点上报 + 按站号错开偏移 |
| 5 | 加要素:挑要素 → 绑寄存器 → 核对下面那行灰字提示的量纲 | 灰字会直接写出「报文里几字节、几位小数、单位、参数类」 |
| 6 | (可选)加越限加报 | 加报点必须挂在已配置的要素上 |
| 7 | 看一眼报文分帧预览,确认会发几帧 | 这一步能提前解释平台侧「怎么收到好几帧」 |
| 8 | 点「写入配置」→ 点「重启生效」 | 不重启不生效 |
页面顶部的红色清单必须清空
水务这边配错不是静默降级:整段配置会被拒,设备日志里只有一行 water config error: ...,界面上完全看不出异常,现场表现为「配了但一帧都不上报」。
所以上位机在写入前会把「固件一定会拒」的项全列在页面顶部。红字没清空就别点写入。
写入后想确认设备端真的收到了,看设备日志(上位机 → 设备参数 → 设备日志)里应该有:
[DTU] 水务节点已启动: 站号1234 中心站1 要素3看到 water config error: 就是配置被拒了,后面那句就是原因。
函数总览
库级函数
| 函数 | 作用 |
|---|---|
water.creat{...} | 创建水务节点(一个设备只允许一个) |
对象方法
| 方法 | 作用 |
|---|---|
node:add_center{...} | 增加一个中心站(最多 4 个) |
node:add_element(code, {...}) | 把一个要素绑到 PLC 寄存器(最多 16 个) |
node:add_alarm(code, {...}) | 给某个要素加越限加报(最多 16 个) |
node:start() | 启动节点 |
node:close() | 停止节点 |
node:is_online([idx]) | 第 idx 个中心站是否在线,不传参查主站 |
node:report_now() | 立刻上报一次 |
node:sync_time() | 立刻发起一次 NTP 对时 |
node:preview(kind) | 生成一帧十六进制文本但不发送,'report' / 'keepalive' / 'settime' |
node:stat() | 运行统计(时钟质量、各中心站计数、补报积压、要素当前值) |
node:reg(event, fn) | 注册事件回调 |
配置必须在 start() 之前做完
add_center / add_element / add_alarm 在节点已经启动时会直接报错。要改配置就 close() → 改 → start()。
原因和 BACnet 一样:这几张表同时被三个任务访问(Lua 任务、协议任务、PLC 扫描任务),在线增删会让协议任务读到已释放的内存。
事件
node:reg(事件名, 回调),事件名可以用字符串:
| 事件 | 什么时候投 |
|---|---|
center_online / center_offline | 某个中心站上线/掉线(只在跳变时投一次) |
clock_synced / clock_bad | 对时成功 / 长时间拿不到时间 |
report_done | 一帧被所有需要确认的中心站确认了 |
backfill_progress | 积压清空(「有积压 → 没积压」那一刻投一次) |
cmd | 收到中心站下行命令,参数是 AFN |
store_full | 队列满了,丢掉了最旧的一条 |
常量
上面这些事件名除了写字符串,也可以用整数常量(两者等价,字符串更不容易拼错)。配置里其他几个取值域同样有对应的 water. 常量,这是本机型上真实存在的名字,大小写要照抄:
规约版本(water.creat{ version = ... })
| 常量 | 含义 |
|---|---|
water.SZY206_2016 | 水务协议 SZY206-2016 |
water.SL427_2021 | 水利 SL/T 427-2021 |
water.SL427_2008 | 水利 SL 427-2008 |
传输协议(node:add_center{ proto = ... })
| 常量 | 值 | 含义 |
|---|---|---|
water.TCP | 0 | 长连接,配了 need_ack 才有重传意义 |
water.UDP | 1 | 无连接,不保证到达 |
要素值类型(node:add_element(code, { type = ... }),即这个数在寄存器里怎么摆)
| 常量 | 值 | 占几个寄存器 |
|---|---|---|
water.WORD | 0 | 1(16 位) |
water.DWORD | 1 | 2(32 位整数) |
water.FLOAT | 2 | 2(32 位单精度) |
water.BIT | 3 | 1 个位 |
上报模式(调度配置里的 mode)
| 常量 | 值 | 配套字段 | 取值范围 |
|---|---|---|---|
water.MODE_HOUR | 0 | offset | 整点后偏移秒,0~3599 |
water.MODE_INTERVAL | 1 | interval | 间隔秒,60~86400 |
water.MODE_PERIOD | 2 | period_min | 只允许 5 / 10 / 15 / 30 / 60 分钟 |
多台设备别用同一个 offset
MODE_HOUR 下几台站填了相同 offset,会在每个整点同时向平台发起上报、把对端打满。错开几十秒是配置的一部分,不是可选优化。
时钟质量(node:stat().clock_quality 的取值,含义见上一节「时钟:三档质量」)
| 常量 | 值 | 能做什么 |
|---|---|---|
water.CLK_NONE | 0 | 完全没有时间,只允许发链路检测 |
water.CLK_COARSE | 1 | 从 flash 恢复的粗时间,只用于防止时间倒退 |
water.CLK_SYNCED | 2 | 中心站对时或 NTP 成功,全功能 |
事件常量(与上面「事件」那张表的字符串一一对应,node:reg 两种都收)
water.CENTER_ONLINE、water.CENTER_OFFLINE、water.CLOCK_SYNCED、water.CLOCK_BAD、 water.REPORT_DONE、water.BACKFILL、water.CMD、water.STORE_FULL
这 8 个的具体整数值随固件版本的消息表排布变化,不要在自己的脚本里写死数字,一律用常量名或字符串名。传了这 8 个之外的值,node:reg 会直接报 water reg: unknown event。
拿来就能跑的示例
一个取水口监测站:水位 + 雨量 + 累计水量,整点上报,水位越限加报,掉线点亮 M100。
lua
--------------------------------------------------------------------
-- 水务遥测:取水口监测站
-- 寄存器约定:D200 水位(整数毫米) D201 雨量(0.1mm) D204 累计水量(升,32位)
--------------------------------------------------------------------
local node = water.creat{
version = water.SZY206_2016,
addr_mode = 1,
region = '320100', -- 行政区划码,由水利部门下发
station = 1234, -- 测站号
password = 0,
keepalive = 300, -- 链路检测 5 分钟一次
retry = 3,
ack_timeout = 10,
backfill_gap = 200,
report = { mode = water.MODE_HOUR, offset = 34 }, -- 每小时第 34 秒,按站号错开
store = { enable = true, days = 30, max_kb = 2048 },
clock = {
ntp_enable = true,
ntp_hosts = { 'ntp1.aliyun.com', 'cn.pool.ntp.org' },
center_time_first = true,
},
on_offline = 'M100', -- 掉线时置位,梯形图里可以本地报警
on_clock_bad = 'M101', -- 时钟没同步时置位,可以用它停掉计量逻辑
}
node:add_center{ ip = '10.20.30.40', port = 2404, proto = water.TCP,
primary = true, need_ack = true }
-- 水位 4 字节 3 位小数:寄存器存整数毫米,所以 scale = 0.001
node:add_element('water_level', { reg = 'D200', type = 'word', scale = 0.001 })
-- 雨量 3 字节 1 位小数:寄存器存 0.1mm,scale = 0.1
node:add_element('rainfall', { reg = 'D201', type = 'word', scale = 0.1 })
-- 累计水量 5 字节 0 位小数:寄存器存升,要报立方米,scale = 0.001
node:add_element('total_water', { reg = 'D204', type = 'dword', scale = 0.001 })
-- 水位越限加报。回差 0.02m 防止水面波动反复触发
node:add_alarm('water_level', { high = 5.2, low = 0.3, mode = 'edge',
deadband = 0.02, min_gap = 300 })
node:reg('center_online', function(ip) print('中心站上线 '..tostring(ip)) end)
node:reg('center_offline', function(ip) print('中心站掉线 '..tostring(ip)) end)
node:reg('clock_synced', function(jump, src) print('对时成功 源='..tostring(src)) end)
node:start()
print('水务节点已启动')例程
每个例程都是可直接下载运行的完整脚本:配置完调
node:start()后脚本即结束,报文分帧/编码/重发/补报全部由固件按标准做。 API 只用本页已文档化的函数;统一模板「场景 / 完整代码 / 要点 / 验证方法 / 注意事项」。 注意water只在 ESP32-S3 固件(LP3232 / LP3288)和软 PLC 模拟器 上有,普通 ESP32/STM32 调water.creat会直接报错。
例程 1:取水口监测站——水位/雨量/累计水量整点上报 + 越限加报
适用:LP3232 / LP3288 / 软PLC模拟器 | 依赖模块:water plc | 难度:★★☆
场景:一个取水口站,水位(D200 整数毫米)、雨量(D201 0.1mm)、累计水量(D204 升,32 位)三个要素,按整点偏移秒上报,水位越限加报,掉线把 M100 置位给梯形图做本地报警。版本必须和平台一致,否则平台静默丢弃、不回任何错误。
完整代码(可直接下载运行):
lua
--------------------------------------------------------------------
-- 水务遥测:取水口监测站
-- 寄存器约定:D200 水位(整数毫米) D201 雨量(0.1mm) D204 累计水量(升,32位)
--------------------------------------------------------------------
local node = water.creat{
version = water.SZY206_2016,
addr_mode = 1,
region = '320100', -- 行政区划码,由水利部门下发,不能自己编
station = 1234, -- 测站号
password = 0,
keepalive = 300, -- 链路检测 5 分钟一次
retry = 3,
ack_timeout = 10,
backfill_gap = 200,
report = { mode = water.MODE_HOUR, offset = 34 }, -- 每小时第 34 秒,按站号错开
store = { enable = true, days = 30, max_kb = 2048 },
clock = {
ntp_enable = true,
ntp_hosts = { 'ntp1.aliyun.com', 'cn.pool.ntp.org' },
center_time_first = true, -- 专网场景必须为真
},
on_offline = 'M100', -- 掉线时置位,梯形图可本地报警
on_clock_bad = 'M101', -- 时钟没同步时置位,可用来停计量
}
node:add_center{ ip = '10.20.30.40', port = 2404, proto = water.TCP,
primary = true, need_ack = true }
-- 水位 4 字节 3 位小数:寄存器存整数毫米,scale = 0.001
node:add_element('water_level', { reg = 'D200', type = 'word', scale = 0.001 })
-- 雨量 3 字节 1 位小数:寄存器存 0.1mm,scale = 0.1
node:add_element('rainfall', { reg = 'D201', type = 'word', scale = 0.1 })
-- 累计水量 5 字节 0 位小数:寄存器存升,要报立方米,scale = 0.001
node:add_element('total_water', { reg = 'D204', type = 'dword', scale = 0.001 })
-- 水位越限加报。回差 0.02m 防止水面波动反复触发
node:add_alarm('water_level', { high = 5.2, low = 0.3, mode = 'edge',
deadband = 0.02, min_gap = 300 })
node:reg('center_online', function(ip) print('中心站上线 '..tostring(ip)) end)
node:reg('center_offline', function(ip) print('中心站掉线 '..tostring(ip)) end)
node:reg('clock_synced', function(jump, src) print('对时成功 源='..tostring(src)) end)
node:start()
print('水务节点已启动')要点:
add_center/add_element/add_alarm必须在start()之前做完;节点已启动时再改会直接报错(表同时被协议/PLC 任务读,在线增删会踩到已释放内存)。- 上报调度用
MODE_HOUR+offset:一个区县几百个站靠offset错开,避免整点并发打垮平台;多台别填同一个offset。 scale语义是「工程量 = 寄存器原值 × scale + offset」;type = 'float'时scale被忽略(寄存器已是工程量,再乘是双重换算)。on_offline/on_clock_bad直接填软元件名字符串,掉线/时钟异常由固件自动置位,Lua 不用轮询。- 气压是 10Pa、水压是 kPa,差 100 倍的量级坑已在页面顶部特别强调,配错平台照样收下只是曲线量级错。
验证方法:
- 下位机配好
config.lua并下载,设备日志应出现[DTU] 水务节点已启动: 站号1234 中心站1 要素3。 - 看到
water config error:就是配置被拒(页面顶部红字没清空会这样),后面那句即原因——这是非静默降级,要当场修。 - 自带中心站模拟器连 127.0.0.1:20206:首帧链路检测后被下发
AFN=11H对时,随后出现afn=C0(report_rt)自报帧。 - Ctrl-C 掉中心站再起回来:
node:stat().backfill.pending涨上去,恢复后按原始时标从旧到新补齐、归零(验证断网补报)。
注意事项:
- 配错不是静默降级:整段配置被拒,日志只有一行
water config error:,界面看不出异常,现场表现「配了但一帧都不上报」。 - 把定时数据发成
81H(报警 AFN)会让平台把每条周期数据记成报警——固件已按标准分开,但看平台日志要分清C0H/81H。 need_ack = false等于放弃「不丢帧」:发出即视为成功、不再重发,断网重连那一段真没了。- 版本(
version)或区划码/测站号和平台不一致时,平台不回任何错误直接丢弃帧,排错要靠preview('report')逐字节对。
例程 2:没有平台也能自测——preview 看报文
适用:同上 | 依赖模块:water | 难度:★☆☆
场景:对接平台前先在本机把报文跑通。不连任何对端,用 node:preview(kind) 直接生成一帧十六进制文本(不发送),把 report / keepalive / settime 三种帧发给平台方逐字节核对,比抓包快得多。
完整代码(可直接下载运行,接在例程 1 的 node 创建之后):
lua
-- 节点已配好要素后,生成但不发送,用来核对帧格式
print(node:preview('keepalive')) --> 68 07 68 B1 00 01 32 D2 04 02 5F 16
print(node:preview('report')) --> 一帧定时自报,按当前寄存器值编码
print(node:preview('settime')) --> 中心站会下发的对时帧长什么样要点:
preview生成的是真实编解码出来的字节(和上位机/平台共用同一份),所以不需要自己再写 BCD 编解码。- 三种
kind:'report'(定时自报)、'keepalive'(链路检测)、'settime'(对时帧)。
验证方法:
- 平台方说「您的帧格式不对」时,把这三行输出发过去逐字节对。
- 整帧长度 =
L + 5,L统计C到Tp结束的字节数;长度对不上正是「平台说校验错」的典型症状。
注意事项:帧长度算错会让平台报校验错;首次对接新平台若出现「每帧都校验错」,需要一帧平台认可的真实报文来钉死 CRC7 的三个参数。
例程 3:运行状态自检——stat + 事件回调盯时钟与积压
适用:同上 | 依赖模块:water plc | 难度:★★☆
场景:现场排障时,最该先确认两件事:时钟有没有同步(没同步固件不采集也不上报,宁缺数据不写 1970 假时标),以及待确认队列有没有积压。用 node:stat() 周期性打印,配合 clock_synced / store_full 事件,把异常情况反映到 M102 / M103 给梯形图。
完整代码(可直接下载运行,接在例程 1 的 node:start() 之后):
lua
node:reg('clock_synced', function(jump, src)
plc.m(102, 1) -- 时钟已同步
print('对时成功 源=', src)
end)
node:reg('store_full', function()
plc.m(103, 1) -- 队列满了,丢过最旧的一帧
print('水务队列已满,曾丢弃最旧帧')
end)
timer.start(60000, function()
local s = node:stat()
-- clock_quality: 0 未知 / 1 粗时间 / 2 已同步;只有 2 才采集+上报
print('时钟质量', s.clock_quality, '来源', s.clock_source)
print('上报 成功', s.report.ok, '失败', s.report.fail)
print('待确认积压', s.backfill.pending, '已补发', s.backfill.sent, '丢弃', s.backfill.dropped)
for i, c in ipairs(s.centers) do
print(i, c.ip, '在线='..tostring(c.online), '发送='..c.sent, '确认='..c.acked)
end
end)要点:
clock_quality是0 未知 / 1 粗时间 / 2 已同步三档;只有 2 才采集和上报,1 只保证重启后时间不倒退、仍然不上报。backfill.pending是断网补报积压;恢复后按原始时标从旧到新发、backfill_gap(100~5000ms)限制倒数据速度,调太小会打满链路和平台。- 时钟跳变不会重写已排队帧的时标(重写会让平台数据错乱),只会记账 + 投事件。
- 事件名既可用字符串也可用
water.CENTER_ONLINE之类常量,不要写死数字——8 个事件的整数值随固件版本的消息表排布变化。
验证方法:
- 启动时若
clock_quality一直是 0/1:确认专网下平台真的会下发AFN=11H对时,或公网 NTP 可达(ntp_enable)。 - 拔掉中心站再起回来:
pending涨上去,恢复后归零;若dropped > 0且M103置位,说明队列曾满、丢过最旧帧。 - 用
e.hex还能看到每个要素编码后的字节(见stat().elements),直接对平台比对。
注意事项:
- 节点起来了但不上报,九成是时钟没同步(
clock_quality ~= 2),先查stat()而不是查网络。 - 队列曾满会丢最旧帧(
store_full),调大backfill_gap或确认need_ack开着,否则断网段数据真没了。 center_time_first = true在专网必须开,否则唯一对时出路(中心站AFN=11H)被跳过。
没有平台也能自测
对接平台之前,先在本机把整条链路跑通。三个办法,从轻到重:
办法一:preview() 看报文,不用任何对端
lua
print(node:preview('keepalive')) --> 68 07 68 B1 00 01 32 D2 04 02 5F 16
print(node:preview('report')) --> 一帧定时自报,按当前寄存器值编码
print(node:preview('settime')) --> 中心站会下发的对时帧长什么样平台方说「您的帧格式不对」时,把这三行的输出发给对方逐字节对,比抓包快得多。
一帧上行自报的结构:
整帧长度 = L + 5。L 统计的是 C 到 Tp 结束的字节数。
办法二:软 PLC 模拟器 + 中心站模拟器
模拟器附带一个中心站模拟器工具,和终端共用同一套编解码——这一点是刻意的:平台侧另写一份编解码的话,两份会各自漂,联调时看到的「通了」可能是两个错法凑巧对上。
bash
# 1) 起中心站模拟器,只监听回环,默认端口 20206
center_sim 20206 0
# 2) 模拟器的水务节点连 127.0.0.1:20206它会打印每一帧的语义、回确认、并在第一帧之后下发一次 AFN=11H 对时。正常的一次交互长这样:
<- rx(12): 68 07 68 B1 00 01 32 D2 04 02 5F 16
addr: region=320100 station=1234
dir=1 func=1 fcb=3 afn=02(link_test) data_len=0
-> ack(12): 68 07 68 30 00 01 32 D2 04 02 3C 16
-> set_clock(25): 68 14 68 01 ... 11 ...
<- rx(24): 68 13 68 81 00 01 32 D2 04 C0 ... 16
dir=1 func=1 fcb=0 afn=C0(report_rt) data_len=12
Tp: day=23 00:15:00 delay=0min
rainfall 123 mm
alarm=0000 term=0002拔掉中心站(Ctrl-C)再起回来,就能验证补报:node:stat() 里的 pending 会涨上去,恢复后按时标从旧到新全部补齐、归零。
办法三:stat() 看运行状态
lua
local s = node:stat()
-- clock_quality: 0 未知 / 1 粗时间 / 2 已同步。只有 2 才会采集和上报
print('时钟质量', s.clock_quality, '来源', s.clock_source) -- 来源 center/ntp/flash/none
print('上报成功', s.report.ok, '失败', s.report.fail)
print('待确认积压', s.backfill.pending, '已补发', s.backfill.sent,
'丢弃', s.backfill.dropped)
for i, c in ipairs(s.centers) do
print(i, c.ip, '在线='..tostring(c.online), '发送='..c.sent, '确认='..c.acked)
end
for code, e in pairs(s.elements) do
-- raw 是寄存器原值,value 是换算后的工程量,hex 是它在报文里的那几个字节
print(code, e.raw, '->', e.value, e.unit, '报文里='..e.hex)
ende.hex 直接给出这个要素编码后在报文里的那几个字节,用的就是固件那一份编解码——上位机和平台都不需要自己再写一份 BCD 编解码。
现场排查
| 现象 | 先查这个 |
|---|---|
| 一帧都没发出去 | 设备日志有没有 [DTU] 水务节点已启动。没有就是配置被拒了,同一行附近会有 water config error: |
| 节点起来了但不上报 | 时钟没同步。stat().clock_quality 要等于 2(0 未知 / 1 粗时间 / 2 已同步);专网下要靠中心站 AFN=11H,确认平台真的会下发 |
| 连不上中心站 | is_online() 为假 → 查 IP/端口/4G 是否拨上;模拟器上还要看 Windows 防火墙 |
| 平台收不到,抓包能看到帧 | 版本(version)和平台不一致,或区划码/测站号不对。平台丢弃时不回任何错误 |
| 平台说校验错 | 帧长对不上的典型症状。用 preview('report') 把整帧发给平台方逐字节对 |
| 平台每条数据都进报警表 | 平台把 C0H 当成了 81H。固件这边已按标准分开,需要平台侧确认它认的是哪个 AFN |
| 曲线量级差 100 倍 | 气压填成了 hPa(应为 10Pa),或水压填成了 MPa(应为 kPa) |
| 曲线量级差 1000 倍 | scale 和要素小数位不匹配。对照上面的要素表重算 |
| 某个要素永远是无效值 | 该要素没配(同一参数类里没配的成员按标准填 FF),或者配了「无效值判定」而寄存器正好等于那个值 |
| 断网恢复后数据缺一段 | need_ack 被关掉了,或队列曾经满过(查 stat() 里的丢弃计数和 store_full 事件) |
| 补报把链路打满 | backfill_gap 太小,往大调 |
已知限制
老实说清楚,免得现场才发现:
| 项 | 现状 |
|---|---|
| 低功耗 | 不做。定位常供电站点 |
| RTC 芯片 | 没有。掉电不保持时钟,靠三级对时 |
| 待确认队列 | 目前在 RAM 里,固定 64 条。掉电会丢未确认的那几帧;落 FlashDB 的时序库是下一步 |
| 中继功能 | 不做。测站号只允许 1~60000 |
| IC 卡 / 充值量 / 定值控制 | 不做。收到这些命令会回否认,并把 AFN 抛给 cmd 事件,脚本里可以自己处理 |
| 水质要素 | 首批不做,配了会在启动时报错 |
| 风向 | 风速那 3 字节里含风向半字节,本版只填风速,风向留 0 |
stat() 的信号强度与存储用量 | 暂无这两项 |
| CRC7 的初值/移位方向 | 标准正文没写这三个参数。算法(多项式 X⁷+X⁶+X⁵+X²+1,单字节)是确定的,但首次对接新平台时若出现「每帧都校验错」,需要一帧平台的真实报文来钉死这三个参数 |
对接新平台时最值得先要的一样东西
一帧平台侧认可的真实报文(十六进制即可)。有了它,上面最后一行那个不确定项能在十分钟内定掉;没有它就只能试。
相关页面
- DTU 配置式编程 · 水务遥测 —— 这一页字段在界面上怎么填
- plc — PLC 数据交互 —— 要素绑的就是这些寄存器
- BACnet 楼宇自控 —— 同一档平台限制,配置做法相似
函数速查表
下表列出本库全部函数与方法(含尚未单独成节的),点「说明」可跳转到对应小节。签名按通用记法:括号内按顺序列全参数,方括号里的可以省略(不传走默认值),... 表示还可继续传同类参数;h: 开头的是对象方法,先用库级函数拿回句柄再冒号调用。
| 方法与签名 | 作用 | 说明 |
|---|---|---|
water.creat(...) | — | 跳转 |
h:add_alarm(string) | — | 本文开头 |
h:add_center(…) | — | 跳转 |
h:add_element(string) | — | 本文开头 |
h:close(…) | — | 本文开头 |
h:is_online(…) | — | 本文开头 |
h:preview(…) | — | 本文开头 |
h:reg(…) | node:reg(event, callback) | 本文开头 |
h:report_now(…) | 运行期失败返回 false,"原因"(报告 7.7) | 本文开头 |
h:start(…) | — | 本文开头 |
h:stat(…) | node:stat() | 本文开头 |
h:sync_time(…) | — | 本文开头 |
water.creat
- 调用形态:
water.creat(...)(库级函数) - 参数:无参数
- 返回:1 个值
- 可用机型:ESP32 机型 · 软 PLC 模拟器 / 上位机(仅 ESP32-S3)
water.add_center
- 调用形态:
h:add_center(…)(对象方法,h为本库函数返回的句柄,冒号调用不传自身) - 参数:无参数
- 返回:无
- 可用机型:ESP32 机型 · 软 PLC 模拟器 / 上位机(仅 ESP32-S3)