Skip to content

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 才算支持。

两条硬边界,配之前先确认现场符合

  1. 不做低功耗。 定位是常供电站点,没有休眠、没有唤醒上报。电池 + 太阳能的极低功耗站点不要用这个方案。
  2. 没有 RTC 芯片,掉电不保持时钟。 时钟靠「中心站对时 > NTP > 掉电前落盘的粗时间」三级方案,而且时钟没同步之前不采集也不上报——宁可缺一段数据,也不把 1970 年的假时标写进平台数据库。

第 2 条在专网(只能连到中心站、拨不通公网 NTP)场景下尤其要注意:这时唯一的对时出路是中心站下发 AFN=11H。设备连上后会立刻发一帧链路检测让平台发现它,就是为了尽快拿到这一帧对时。

这东西干什么 ​

把 PLC 寄存器里的水位、雨量、流量,按调度编成水务规约报文上报给一到四个中心站;断网期间存起来,恢复后按原始时标补报。

要做的事只有三件:填测站身份 → 填中心站地址 → 把要素绑到寄存器。分帧、编码、重发、补报、对时全部由固件按标准做,不需要写一行协议代码。

不想写脚本?

整套功能在上位机里点几下就能配出来,见 不写 Lua:在上位机里配。建议第一次就走这条路——比脚本少一半出错机会。

三个版本怎么选 ​

creat 的 version 参数:

取值标准什么时候选
water.SZY206_2016SZY206-2016水资源监测(取水口、水源井、供水管网),流量单位习惯用 m³/h
water.SL427_2021SL/T 427-2021水文监测(水位站、雨量站、水库),现在的主流版本
water.SL427_2008SL 427-2008老平台还在用 2008 版时选它

三个版本是同一条脉络,帧结构和 BCD 习惯完全一致,差异集中在要素表和个别数据域上。版本必须和平台一致——填错了帧长度和校验都对不上,平台会直接丢弃,而且不会回任何错误提示。

测站身份 ​

平台是按地址域认站的,这两个字段由当地水利部门下发,不能自己编。

字段范围说明
addr_mode1 / 21 = 区划码 + 测站号;2 = 水文测站编码
region6 位数字行政区划码,如南京 320100
station1 ~ 60000测站号。60001~65534 是中继地址(本版不做),65535 是广播地址,终端不能用
hydro_code8 位 HEX方式二用
password0 ~ 65535规约唯一的访问控制手段。中心站下发设置类命令要带上它,对不上终端不执行

要素表 ​

这张表是核对量纲的唯一依据。「报文字节数」和「小数位」是标准规定的,改不了;您能改的只有 scale / offset(怎么把寄存器值换算成工程量)。

代码名称报文字节小数位单位参数类带符号
rainfall雨量31mm雨量否
water_level水位43m水位是
flow_rate瞬时流量53m³/h流量是
total_water累计水量50m³流量是(仅最高位)
velocity流速33m/s流速是
gate闸位32m闸位否
power功率30kW功率否
air_pressure气压3010Pa气压否
wind风速32m/s风速否
water_temp水温21℃水温否
soil_moisture土壤含水率21%土壤否
evaporation蒸发量31mm蒸发否
water_pressure水压42kPa水压否
volt终端电压22V电压否

两个最容易差 100 倍的单位

  • 气压是 10Pa,不是 hPa。 标准大气压 1013hPa 要上报成 10130。
  • 水压是 kPa,不是 MPa。 0.35MPa 要上报成 350。

配错了平台照样收得下(数值合法、校验正确),只是曲线上的量级差两个数量级,事后极难发现是哪一环错的。

水质要素(pH、COD、氨氮、总磷、叶绿素、浊度、溶解氧)首批不实现,配了会在启动时报错而不是静默忽略——目的是让您在配置阶段就知道,而不是配上了却永远不上报。

scale / offset 怎么填 ​

固定语义:上报的工程量 = 寄存器原值 × scale + offset。

现场情况typescale举例
寄存器里是整数毫米,要报米(3 位小数)word0.001寄存器 5203 → 上报 5.203 m
寄存器里是整数厘米,要报米word0.01寄存器 520 → 上报 5.20 m
寄存器里已经是浮点工程量float忽略寄存器 5.203 → 上报 5.203 m
4-20mA 原始码 0~4095 对应 0~10mword0.0024420需要 offset 时一起填

type 是 'float' 时 scale 被忽略——寄存器里本来就是工程量,再乘一遍是双重换算。这是固件的明确语义,不是 bug。

上位机在您换要素时会自动把 scale 预填成 10^-小数位,也就是「寄存器里存整数最小单位」这种最常见的接法。

为什么配 3 个要素,平台收到 3 帧 ​

这是标准的规定,不是实现取巧。规约里一帧自报只装一个「参数类」,类别由链路层功能码表达,数据域是该类全部要素按固定宽度顺序排列——没有逐要素的标识字节。

两个直接后果,交给平台侧的人之前先说清:

  • 同一类的成员总是一起发。 只配了瞬时流量、没配累计水量,累计水量的位置仍然占 5 字节、填全 FF(无效值)。这是定长布局,平台看到的是正常报文。
  • 三帧带同一个时标。 时标在 Tp(5 字节:秒分时日 BCD + 1 字节延时分钟),不在数据域里。

上位机的水务页上有一块「报文分帧预览」,按您当前配的要素直接算出会发几帧、每帧装什么,可以先在那里核对。

定时上报与越限加报是两种报文 ​

定时 / 手动上报越限加报
AFNC0H(定时/实时数据自报)81H(随机自报报警)
触发按 report 调度,或脚本调 report_now()数值穿越阈值时立刻发
平台侧进历史数据表进报警表

把定时数据也发成 81H 是最典型的对接错误——平台会把每一次周期数据都记成一条报警。固件已经按标准分开了,不需要您做什么,但看平台日志时要知道这两个 AFN 的区别。

上报调度三种模式 ​

mode参数含义
water.MODE_HOURoffset 0~3599每小时的第 offset 秒。推荐,一个区县几百个站按站号错开偏移,避免整点并发把平台打垮
water.MODE_INTERVALinterval 60~86400从 epoch 起每 interval 秒一次
water.MODE_PERIODperiod_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_interval3600 ~ 86400 秒重新对时间隔
jump_alarm1 ~ 86400 秒时钟跳变超过它就记一条 ERC11 并投事件
flash_interval300 ~ 3600 秒粗时间落盘间隔。调太小会伤 flash 寿命

时钟跳变不会重写已排队的报文

时钟被大幅调整时只记账 + 报事件,已经在待确认队列里等补报的帧一律按原时标发送。重写时标会让平台侧的数据错乱,而且事后分不清哪个是对的。

断网补报:不丢、不乱序、不重 ​

  • 每个中心站有各自独立的游标,总是取「自己还没确认过的最旧一条」,因此天然不乱序。
  • 一帧只存一份,各中心站用位掩码各记「我确认过了没」;所有需要确认的站都确认了才释放。
  • 队列满了会丢最旧的一条并投 store_full 事件。丢新的会造成「恢复网络后补的全是老数据、当前值一直不刷新」,现场更难解释。
  • backfill_gap(100~5000ms)限制往外倒数据的速度。调太小会把 4G 链路和中心站一起打满,也会让当前的定时自报排在几百帧之后。

need_ack = false 等于放弃「不丢帧」

确认过平台真的不回应答帧才关掉它。关掉之后发出即视为成功,不再等确认也不重发——断网重连时这一段数据就真的没了。

不写 Lua:在上位机里配 ​

上位机 → 选中设备 → DTU 功能 → 水务遥测。这一页配出来的东西和写脚本完全等价(设备端就是拿这份 JSON 去调同一套 API)。

上位机 → DTU 功能 → 水务遥测:测站身份 + 中心站 + 上报调度 + 上报要素,从上往下一段段填

操作顺序:

步骤做什么注意
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.TCP0长连接,配了 need_ack 才有重传意义
water.UDP1无连接,不保证到达

要素值类型(node:add_element(code, { type = ... }),即这个数在寄存器里怎么摆)

常量值占几个寄存器
water.WORD01(16 位)
water.DWORD12(32 位整数)
water.FLOAT22(32 位单精度)
water.BIT31 个位

上报模式(调度配置里的 mode)

常量值配套字段取值范围
water.MODE_HOUR0offset整点后偏移秒,0~3599
water.MODE_INTERVAL1interval间隔秒,60~86400
water.MODE_PERIOD2period_min只允许 5 / 10 / 15 / 30 / 60 分钟

多台设备别用同一个 offset

MODE_HOUR 下几台站填了相同 offset,会在每个整点同时向平台发起上报、把对端打满。错开几十秒是配置的一部分,不是可选优化。

时钟质量(node:stat().clock_quality 的取值,含义见上一节「时钟:三档质量」)

常量值能做什么
water.CLK_NONE0完全没有时间,只允许发链路检测
water.CLK_COARSE1从 flash 恢复的粗时间,只用于防止时间倒退
water.CLK_SYNCED2中心站对时或 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 倍的量级坑已在页面顶部特别强调,配错平台照样收下只是曲线量级错。

验证方法:

  1. 下位机配好 config.lua 并下载,设备日志应出现 [DTU] 水务节点已启动: 站号1234 中心站1 要素3。
  2. 看到 water config error: 就是配置被拒(页面顶部红字没清空会这样),后面那句即原因——这是非静默降级,要当场修。
  3. 自带中心站模拟器连 127.0.0.1:20206:首帧链路检测后被下发 AFN=11H 对时,随后出现 afn=C0(report_rt) 自报帧。
  4. 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'(对时帧)。

验证方法:

  1. 平台方说「您的帧格式不对」时,把这三行输出发过去逐字节对。
  2. 整帧长度 = 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 个事件的整数值随固件版本的消息表排布变化。

验证方法:

  1. 启动时若 clock_quality 一直是 0/1:确认专网下平台真的会下发 AFN=11H 对时,或公网 NTP 可达(ntp_enable)。
  2. 拔掉中心站再起回来:pending 涨上去,恢复后归零;若 dropped > 0 且 M103 置位,说明队列曾满、丢过最旧帧。
  3. 用 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)
end

e.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,单字节)是确定的,但首次对接新平台时若出现「每帧都校验错」,需要一帧平台的真实报文来钉死这三个参数

对接新平台时最值得先要的一样东西

一帧平台侧认可的真实报文(十六进制即可)。有了它,上面最后一行那个不确定项能在十分钟内定掉;没有它就只能试。

相关页面 ​

函数速查表 ​

下表列出本库全部函数与方法(含尚未单独成节的),点「说明」可跳转到对应小节。签名按通用记法:括号内按顺序列全参数,方括号里的可以省略(不传走默认值),... 表示还可继续传同类参数;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)

LPPLC 工业物联网云平台 · 软硬件与云平台全栈自研