[CJ188-COOLWATER-PARA] 对接文档
传感器简介
Source file does not provide it
| 项目 | 值 |
|---|---|
| 项目名称 | cj188_coolwater_para |
| 厂商 | TKL |
| 型号 | [CJ188-COOLWATER-PARA] |
| 业务代码 | Not applicable |
| 对接模式 | Source file does not provide it |
| 协议 | Source file does not provide it |
模型清单
可使用下列名称或 ID 在 ThinkLink 中搜索对应模型。
| 模型类型 | 名称 | id_name | 平台模型 ID |
|---|---|---|---|
| RPC | [CJ188 GET] para 23103 | cj188_coolwater_get_23103 | 92512371725045765 |
| RPC | [CJ188 SET] para 23103 | cj188_coolwater_set_23103 | 92512050479108101 |
| Template | CJ188 COOL WATER | 92529888803688453 | |
| Thing Model | [CJ188 COOLWATER] | cj188_coolwater_23103 | 92527788782784517 |
| Thing Model | [TKL-23103-CJ188-COOLWATER资产] 区域聚合 | cj188_coolwater_asset_23103 | 119677189950959624 |
| Parameter Thing Model | [CJ188-COOLWATER-PARA] | cj188_coolwater_para_23103 | 92528875162046469 |
产品特点与适用范围
本项目面向“Source file does not provide it”场景。能力边界、字段和可部署模型均以当前 descriptor、requirements 与模型源文件为准。
部署与采集信息
| DTU | Not applicable | | 供电 | Source file does not provide it | | LoRaWAN Class | Source file does not provide it | | 接口 | Source file does not provide it | | 波特率 | Not applicable |
遥测与寄存器定义
源文件未提供独立遥测字段表;请以模型清单中的当前物模型为准。
参数定义
无项目专用参数定义。
Requirements 证据
以下关键事实由 requirements 源文件提取;原始行号用于复核。
| 类别 | 来源事实 | 来源 |
|---|---|---|
| 协议/数据 | | Device Type | 冷水表(CJ/T 188-2004 协议体系,表类型代码 0x10) | | requirement/requirements.md:14 |
| 协议/数据 | > 读数 DI 为 90 1F、地址编码为 7 字节 BCD/hex 小端、状态字规则兼容,均可复用本模板 | requirement/requirements.md:18 |
| 协议/数据 | > (少量地址/厂商差异内即可适配)。同 biz_code (23103) 的姊妹项目 | requirement/requirements.md:19 |
| 部署 | | DTU Power Mode | Battery(电池供电,电池容量 10800mAh) | | requirement/requirements.md:29 |
| 协议/数据 | | LoRaWAN Class | Class A(Battery: true) | | requirement/requirements.md:30 |
| 协议/数据 | > DTU 选型说明:CJ/T188 冷水表通过 M-Bus 接口接入,故选用 KC22(KC22 支持 | requirement/requirements.md:33 |
| 部署 | > M-Bus 从站,一主一从/一主多从,无极性接线,半双工,300~9600bps 可配,本项目实际 | requirement/requirements.md:34 |
| 部署 | > VCC/GND),总线由主站(KC22)向从站供电。 | requirement/requirements.md:36 |
| 协议/数据 | > JQ-23103-CJ188-coolwater/descriptor.yaml(dtu_model: "KC22", interface: "M-Bus") | requirement/requirements.md:42 |
| 协议/数据 | > 均不符。判断为平台模型标签的历史遗留/复制粘贴错误,以 KC22/M-Bus 为准(协议层 | requirement/requirements.md:43 |
| 部署 | - 场景 3:存量 CJ188 冷水表无线化改造 — 已部署 CJ/T188 协议冷水表的存量项目, | requirement/requirements.md:55 |
| 协议/数据 | (MQTT 转发)与远程参数(表地址、采集/上传周期)配置的项目。 | requirement/requirements.md:58 |
| 协议/数据 | - 采用 CJ/T188 标准协议,协议清晰、行业通用,便于标准化批量接入。 | requirement/requirements.md:62 |
| 部署 | - 通过 M-Bus 接入,总线供电、无极性接线,安装简便。 | requirement/requirements.md:63 |
| 协议/数据 | - 支持远程读取表地址、瞬时/累计流量、表计状态字,并支持远程设置表地址与采集周期。 | requirement/requirements.md:64 |
| 协议/数据 | - 模板通用性强,可复用到其他兼容 CJ/T188 冷水表协议的品牌水表。 | requirement/requirements.md:65 |
| 部署 | | Voltage | KC22 电池供电,M-Bus 总线对外供电(对从站/水表) | manual (input/) | | requirement/requirements.md:79 |
| 部署 | | Power Consumption | 电池容量 10800mAh | manual (input/) | | requirement/requirements.md:80 |
| 部署 | | Power-on to Stable Working | 1s(默认假设) | 说明书未标注,默认为 1 秒。如实际设备需要更长时间请修改。 | | requirement/requirements.md:86 |
| 协议/数据 | | Interface Type | M-Bus | | requirement/requirements.md:92 |
| 协议/数据 | | Baud Rate | 2400 bps | | requirement/requirements.md:93 |
| 部署 | KC22 作为 M-Bus 主站,向总线提供电源;从站(水表)无独立 VCC/GND, | requirement/requirements.md:102 |
| 部署 | 半双工通信,无极性接线。 | requirement/requirements.md:103 |
| 部署 | | M-Bus+ | 总线(供电+数据,无极性) | KC22 M-Bus 端子 | | requirement/requirements.md:108 |
物模型与 RPC 代码
RPC: [CJ188 GET] para 23103
id_name: cj188_coolwater_get_23103 · ID: 92512371725045765
import {RPCHelper, Utils} from '#tklHelper';
function rpc_script({ device, params, alarms, logger, org_params }) {
let paraDef= {
app_34: {name: "retries", field_name: "retries", unit: "", type: "uint8"},
app_38: { name: "pwron_delay", field_name: "pwron_delay", unit: "ms", index: 38, type: "uint16le" },
app_70: {name: "period", field_name: "period", unit: "m", type: "uint32LE"},
app_150: {name: "addr", field_name: "addr", unit: "", type: "hexLE7"},
}
let frames=RPCHelper.buildFrame({
paraDef:paraDef,
params:params});
let msg=RPCHelper.makeMSG({
msgType:Utils.msgType.paras,
device:device,
dnBuffer:frames.readBuffer,
sleepTime:0,
})
return [msg]
}
rpc_script({
device: { server_attrs: {}, shared_attrs: {} },
params: {},
alarms: [],
logger: console,
org_params: {}
})RPC: [CJ188 SET] para 23103
id_name: cj188_coolwater_set_23103 · ID: 92512050479108101
import {RPCHelper, Utils} from '#tklHelper';
import {Buffer} from "buffer";
function rpc_script({ device, params, alarms, logger, org_params }) {
let classMode = (device && device.shared_attrs && device.shared_attrs.class_mode) || "ClassA";
const rpcName="cj188_coolwater_set_23103"
let paraDef= {
app_34: {name: "retries", field_name: "retries", unit: "", type: "uint8"},
app_38: { name: "pwron_delay", field_name: "pwron_delay", unit: "ms", index: 38, type: "uint16le" },
app_70: {name: "period", field_name: "period", unit: "m", type: "uint32LE"},
app_150: {name: "addr", field_name: "addr", unit: "", type: "hexLE7"},
}
logger.info("set paras")
let paraInput = {...params}
paraInput.retries=2
paraInput.pwron_delay=1000
paraInput.period=params.period*60
let frames=RPCHelper.buildFrame({
paraDef:paraDef,
params:paraInput});
let redoBuffer=RPCHelper.redo()
let dnBuffer=Buffer.alloc(frames.writeBuffer.length+frames.readBuffer.length);
frames.writeBuffer.copy(dnBuffer,0)
frames.readBuffer.copy(dnBuffer,frames.writeBuffer.length)
let msgQue=Utils.makeParaSetMSG({
device:device,
classMode:classMode,
rpcName:rpcName,
params:params,
paraDownBuffer:dnBuffer,
extraAppBuffer:redoBuffer
})
if (msgQue.length==0) return null
return msgQue
}
rpc_script({
device: { server_attrs: {}, shared_attrs: {} },
params: {},
alarms: [],
logger: console,
org_params: {}
})Thing Model: [CJ188 COOLWATER]
id_name: cj188_coolwater_23103 · ID: 92527788782784517
import {PayloadParser, RPCHelper} from '#tklHelper';
function payload_parser(device, msg, thingModelId, noticeAttrs, org_params) {
let frameInfo = {
port:22,
dataLen:-1,
rssi:true,
battery:4,
tagList:[
{ index:0, tag:0x87 },
{ index:1, tag:0x03 }
]
}
let appInfo = [
{ name:"addr", field_name:"addr", unit:"", index:5, type:"hexLE7" },
{ name:"instantflow", field_name:"instantflow", unit:"m³", index:12, type:"bcdLE4", coefficient:0.01,decimal:2 },
{ name:"totalflow", field_name:"totalflow", unit:"m³", index:17, type:"bcdLE4", coefficient:0.01,decimal:2 },
{ name:"status", field_name:"status", unit:"", index:22, type:"uint8" }
]
let tdata={}
let payParser=new PayloadParser({
device:device,
msg:msg,
frameInfo:frameInfo,
appInfo:appInfo,
})
tdata= payParser.telemetry()
if((tdata?.status&0x02)===0x02) { // time out , just update the status.
const status=tdata.status
tdata={ ...(device.telemetry_data?.[thingModelId] ?? {})}
tdata.status=status
}
let result = {
telemetry_data: tdata,
server_attrs: null,
shared_attrs: null
};
// ── rpcHook (设备对接通用规则) ──
if (typeof RPCHelper.buildRpcHookAction === "function") {
const __a = RPCHelper.buildRpcHookAction({ device, msg, thingModelId, idName: "cj188_coolwater_23103", name: "[CJ188 COOLWATER]", parsed: { telemetry_data: result.telemetry_data, server_attrs: result.server_attrs, shared_attrs: result.shared_attrs } });
if (__a) { result.actions = result.actions || []; result.actions.push(__a); }
}
return result;
}
console.log(payload_parser(
{ telemetry_data: {} },
{ userdata: { port: 11, payload: "" } },
"1",
{ telemetry_data: true },
{}
))Thing Model: [TKL-23103-CJ188-COOLWATER资产] 区域聚合
id_name: cj188_coolwater_asset_23103 · ID: 119677189950959624
import {PayloadParser, RPCHelper} from '#tklHelper';
// 资产(VIRTUAL)聚合物模型 — 通用 agg_spec 驱动聚合器,见 docs/references/workflow/asset-aggregation-model.md
// device=资产, msg=源设备快照, thingModelId=本物模型ID, noticeAttrs=变更标记。
// 聚合规格在资产 server_attrs.agg_spec:min_snr=取最小(snr);min_rssi=取最小(rssi);min_battery=取最小(battery);zone_totalflow_sum=谨慎累加(totalflow);zone_instantflow_sum=谨慎累加(instantflow);online_count=在线数(—)
function payload_parser(device, msg, thingModelId, noticeAttrs) {
if (!noticeAttrs || !noticeAttrs.telemetry_data) {
return { telemetry_data: null, server_attrs: null, shared_attrs: null };
}
const sa = device?.server_attrs || {};
const spec = sa.agg_spec || {};
const members = { ...(sa.members || {}) };
const staleMs = sa.stale_ms != null ? sa.stale_ms : 600000;
const now = (typeof Date !== "undefined" && Date.now) ? Date.now() : null;
// 合并源设备各物模型的最新快照为一个扁平对象
const srcTd = {};
const all = msg?.telemetry_data || {};
for (const k in all) { Object.assign(srcTd, all[k]); }
// 收集 spec 引用到的全部源字段,把本帧带到的值并进该成员快照
const want = {};
for (const out in spec) { if (spec[out].src) want[spec[out].src] = true; }
const got = {}; let touched = false;
for (const f in want) { if (srcTd[f] !== undefined && srcTd[f] !== null) { got[f] = srcTd[f]; touched = true; } }
if (touched && msg && msg.eui) {
const prev = (members[msg.eui] && members[msg.eui].v) || {};
const seq = (members[msg.eui] && members[msg.eui].ts) || 0;
members[msg.eui] = { v: Object.assign({}, prev, got), ts: now != null ? now : seq + 1 };
}
const euis = Object.keys(members);
const fresh = euis.filter(function (e) {
if (now == null || staleMs <= 0) return true;
const ts = members[e].ts;
return ts != null && ts <= now && (now - ts) <= staleMs;
});
const num = function (x) { return (x != null && !isNaN(x)) ? Number(x) : null; };
const out = {};
for (const field in spec) {
const op = spec[field].op, src = spec[field].src;
if (op === "count_online") { out[field] = fresh.length; continue; }
const pool = (op === "sum_cumul") ? euis : fresh;
const vals = pool.map(function (e) { return num(members[e].v && members[e].v[src]); }).filter(function (x) { return x != null; });
if (op === "avg") out[field] = vals.length ? Number((vals.reduce(function (a, b) { return a + b; }, 0) / vals.length).toFixed(2)) : null;
else if (op === "sum_snap" || op === "sum_cumul") out[field] = vals.length ? Number(vals.reduce(function (a, b) { return a + b; }, 0).toFixed(3)) : null;
else if (op === "max") out[field] = vals.length ? Math.max.apply(null, vals) : null;
else if (op === "min") out[field] = vals.length ? Math.min.apply(null, vals) : null;
else if (op === "or") out[field] = pool.some(function (e) { const x = members[e].v && members[e].v[src]; return x != null && x != 0; }) ? 1 : 0;
else if (op === "count_on") out[field] = pool.filter(function (e) { const x = members[e].v && members[e].v[src]; return x != null && x != 0; }).length;
else out[field] = null;
}
let result = { telemetry_data: out, server_attrs: { members: members }, shared_attrs: null };
// ── rpcHook (设备对接通用规则) ──
if (typeof RPCHelper.buildRpcHookAction === "function") {
const __a = RPCHelper.buildRpcHookAction({ device, msg, thingModelId, idName: "cj188_coolwater_asset_23103", name: "[TKL-23103-CJ188-COOLWATER资产] 区域聚合", parsed: { telemetry_data: result.telemetry_data, server_attrs: result.server_attrs, shared_attrs: result.shared_attrs } });
if (__a) { result.actions = result.actions || []; result.actions.push(__a); }
}
return result;
}
// --- local debug ---(两台成员先后上报;平台会把返回的 members 持久化回 device,这里手动模拟)
const agg_spec = {
"min_snr": {
"op": "min",
"src": "snr"
},
"min_rssi": {
"op": "min",
"src": "rssi"
},
"min_battery": {
"op": "min",
"src": "battery"
},
"zone_totalflow_sum": {
"op": "sum_cumul",
"src": "totalflow"
},
"zone_instantflow_sum": {
"op": "sum_cumul",
"src": "instantflow"
},
"online_count": {
"op": "count_online",
"src": ""
}
};
const device = { name: "TKL-23103-CJ188-COOLWATER资产-1", server_attrs: { agg_spec, stale_ms: 1800000, members: {} } };
const r1 = payload_parser(device, { eui: "MEMBER_A", telemetry_data: { m1: {"snr":1,"rssi":1,"battery":1,"totalflow":1,"instantflow":1} } }, "tmid", { telemetry_data: true });
device.server_attrs.members = r1.server_attrs.members; // 平台持久化
console.log("成员A上报后:", JSON.stringify(r1.telemetry_data));
const r2 = payload_parser(device, { eui: "MEMBER_B", telemetry_data: { m1: {"snr":2,"rssi":2,"battery":2,"totalflow":2,"instantflow":2} } }, "tmid", { telemetry_data: true });
device.server_attrs.members = r2.server_attrs.members;
console.log("成员B上报后(2台聚合):", JSON.stringify(r2.telemetry_data));
void PayloadParser;Parameter Thing Model: [CJ188-COOLWATER-PARA]
id_name: cj188_coolwater_para_23103 · ID: 92528875162046469
import {PayloadParser, RPCHelper, Utils} from '#tklHelper';
function payload_parser(device, msg, thingModelId, noticeAttrs, org_params) {
let port=msg?.userdata?.port || null;
const rpcName="cj188_coolwater_set_23103"
let paraDef= {
app_34: {name: "retries", field_name: "retries", unit: "", type: "uint8"},
app_38: { name: "pwron_delay", field_name: "pwron_delay", unit: "ms", index: 38, type: "uint16le" },
app_70: {name: "period", field_name: "period", unit: "m", type: "uint32LE"},
app_150: {name: "addr", field_name: "addr", unit: "", type: "hexLE7"},
}
if (port!==214) {
let checkData=Utils.paraCheck(rpcName,device.server_attrs,device.shared_attrs)
return {
server_attrs: checkData.sdata,
actions:checkData.actions,
}
}
let pdata=(new PayloadParser({
device:device,
msg:msg,
paraInfo:paraDef,
})).paras()
if(pdata?.period!=undefined){ pdata.period=pdata.period/60}
let checkData= Utils.paraCheck(rpcName,device.server_attrs,pdata)
let result = {
telemetry_data: pdata,
server_attrs: checkData.sdata,
shared_attrs: pdata,
actions: checkData.actions,
};
// ── rpcHook (设备对接通用规则) ──
if (typeof RPCHelper.buildRpcHookAction === "function") {
const __a = RPCHelper.buildRpcHookAction({ device, msg, thingModelId, idName: "cj188_coolwater_para_23103", name: "[CJ188-COOLWATER-PARA]", parsed: { telemetry_data: result.telemetry_data, server_attrs: result.server_attrs, shared_attrs: result.shared_attrs } });
if (__a) { result.actions = result.actions || []; result.actions.push(__a); }
}
return result;
}
console.log(payload_parser(
{ telemetry_data: {} },
{ userdata: { port: 11, payload: "" } },
"1",
{ telemetry_data: true },
{}
))物模型字段定义
| 物模型 | 字段 | 类型 | 单位 | 名称 |
|---|---|---|---|---|
| cj188_coolwater_23103 | snr | number | dB | SNR |
| cj188_coolwater_23103 | addr | number | hex | Device Address |
| cj188_coolwater_23103 | rssi | number | dBm | RSSI |
| cj188_coolwater_23103 | status | number | Status | |
| cj188_coolwater_23103 | battery | number | v | Battery |
| cj188_coolwater_23103 | totalflow | number | m³ | Total Flow |
| cj188_coolwater_23103 | instantflow | number | m³ | Instantaneous Flow |
| cj188_coolwater_asset_23103 | min_snr | number | dB | Zone SNR |
| cj188_coolwater_asset_23103 | min_rssi | number | dBm | Zone RSSI |
| cj188_coolwater_asset_23103 | min_battery | number | v | Zone Battery |
| cj188_coolwater_asset_23103 | zone_totalflow_sum | number | m³ | Zone Total Flow |
| cj188_coolwater_asset_23103 | zone_instantflow_sum | number | m³ | Zone Instantaneous Flow |
| cj188_coolwater_asset_23103 | online_count | number | Zone Online Count | |
| cj188_coolwater_para_23103 | addr | string | hex | Device Address |
| cj188_coolwater_para_23103 | period | number | min | Period |
| cj188_coolwater_para_23103 | pwron_delay | number | ms | Power-on Delay |
RPC 参数定义
| RPC | 参数 | 类型 | 说明 | 默认值 | 单位 |
|---|---|---|---|---|---|
| cj188_coolwater_set_23103 | addr | string | Modbus Address | — | hex |
| cj188_coolwater_set_23103 | period | number | Upload/Collect Period | — | min |
触发器定义
无项目专用触发器。
第三方平台数据订阅
MQTT Topic: /v32/{Organization Account}/tkl/up/telemetry/{eui}
{
"eui": "6353012af10a9331",
"active_time": "2026-07-13T08:35:48.000Z",
"thingModelId": "Use the platform model ID from the model inventory",
"thingModelIdName": "Use the current telemetry model id_name",
"telemetry_data": {
"snr": 13.5,
"rssi": -51,
"battery": 3.37
}
}模板选择与验证
在 ThinkLink 平台按项目名称、业务代码或模型清单中的 id_name、ID 搜索并绑定模型。部署后使用真实上行帧核对字段、单位、RPC 和触发器行为。
补充说明
需求与决策依据:requirement/requirements.md。