21310-SL2000C Integration Guide
Sensor Overview
SL2000C is integrated with ThinkLink through modbus-rtu in edgebus-dtu mode. This guide documents the current deployable artifacts and their source-traceable versions.
| Item | Value |
|---|---|
| Project | SL-21310-SL2000C |
| Vendor | SL |
| Model | SL2000C |
| Business Code | 21310 |
| Integration Mode | edgebus-dtu |
| Protocol | modbus-rtu |
Model catalog
Search for these models in ThinkLink by name or ID.
| Model type | Name | id_name | Platform model ID |
|---|---|---|---|
| RPC | [SL AUTOCTRL CFG] | sl_autoctrl_config_21310 | 118639080874340352 |
| RPC | [SL AUTOCTRL NOTIFY] | sl_autoctrl_notify_21310 | 118642752979103744 |
| RPC | [SL AUTOCTRL WINDOW] | sl_autoctrl_window_21310 | 118639079884484608 |
| RPC | [SL FAULT RESET] | sl_fault_reset_action_21310 | 118639074800988160 |
| RPC | [SL GET] para | sl_get_21310 | 118639076910723072 |
| RPC | [SL POWER ACT] | sl_power_action_21310 | 118639071638482944 |
| RPC | [SL SET] para | sl_set_21310 | 118639075765678080 |
| RPC | [SL SETPOINT ACT] | sl_setpoint_action_21310 | 118639073274261504 |
| Template | SL-SL2000C-21310 | 118639081822253056 | |
| Thing Model | [SL2000C] | sl_21310 | 118639068635361280 |
| Thing Model | [SL-21310-SL2000C资产] 区域聚合 | sl_asset_21310 | 119677174411063303 |
| Parameter Thing Model | [SL2000C-PARA] | sl_para_21310 | 118639069788794880 |
| Trigger | [SL] 除湿机故障告警触发器 | sl_alarm_trigger_21310 | 118639077997047808 |
| Trigger | [SL] 除湿机温湿度自控触发器 | sl_autoctrl_trigger_21310 | 118639078986903552 |
Features and Application Scope
This project covers SL2000C through modbus-rtu. Its capability boundary, fields, and deployable models follow the current descriptor, requirements, and model sources.
Deployment and Data Acquisition
| DTU | KC11 | | Power | ac | | LoRaWAN Class | C | | Interface | RS-485 | | Baud Rate | 9600 |
Telemetry and Register Definitions
| Field | Name | Type | Unit | Address/Source | Description |
|---|---|---|---|---|---|
| return_temp | return_temp | int16BE | ℃ | FC03@31 | Source field: return_temp |
| return_humidity | return_humidity | int16BE | %RH | FC03@30 | Source field: return_humidity |
| temp_set | temp_set | int16BE | ℃ | FC03@37 | Source field: temp_set |
| humidity_set | humidity_set | int16BE | %RH | FC03@1 | Source field: humidity_set |
| power_switch | power_switch | derived(coil@176 bit0) | FC01@176 | Source field: power_switch | |
| unit_status | unit_status | derived(FC03@35 enum→text) | FC03@35 | Source field: unit_status | |
| compressor/cond_fan_1/cond_fan_2/four_way_valve/supply_fan/heat_valve/cool_valve | compressor/cond_fan_1/cond_fan_2/four_way_valve/supply_fan/heat_valve/cool_valve | Source-defined | FC01@40-46 | Source field: compressor/cond_fan_1/cond_fan_2/four_way_valve/supply_fan/heat_valve/cool_valve | |
| fault_text | fault_text | Source-defined | FC01@0-9 | Source field: fault_text | |
| fault_high_press | fault_high_press | derived(coil@1) | FC01@1 | Source field: fault_high_press | |
| fault_low_press | fault_low_press | derived(coil@2) | FC01@2 | Source field: fault_low_press | |
| has_fault | has_fault | derived(coil@27) | FC01@27 | Source field: has_fault | |
| fault_*(supply_fan_oc/comp_oc/cond_fan1_oc/cond_fan2_oc/phase_seq/user_interlock/humidity_sensor/temp_sensor) | fault_*(supply_fan_oc/comp_oc/cond_fan1_oc/cond_fan2_oc/phase_seq/user_interlock/humidity_sensor/temp_sensor) | derived(coil@0/@3-9) | FC01@0,@3-9 | Source field: fault_*(supply_fan_oc/comp_oc/cond_fan1_oc/cond_fan2_oc/phase_seq/user_interlock/humidity_sensor/temp_sensor) |
Parameter Definitions
| Parameter | Name | Type | Default | Range | Unit | Description |
|---|---|---|---|---|---|---|
| app_38 | pwron_delay | uint16LE | 200 | — | ms | Source parameter: app_38 |
| app_40 | period_heart | uint32LE | 86400 | — | s | Source parameter: app_40 |
| app_70 | period_up | uint32LE | 300 | — | s | Source parameter: app_70 |
| app_74 | period_read | uint32LE | 60 | — | s | Source parameter: app_74 |
| app_110 | cov_temp | int16BE | 0.5 | — | ℃ | Source parameter: app_110 |
| app_112 | cov_humidity | int16BE | 2 | — | %RH | Source parameter: app_112 |
| app_150 | addr_modbus | uint8 | 1 | 1-32 | Source parameter: app_150 |
Requirements Evidence
The following key facts were extracted from the requirements source; line references support verification.
| Category | Source Fact | Source |
|---|---|---|
| Protocol/Data | > ../input/SL2000C-TY3 .xls | requirement/requirements.md:4 |
| Deployment | DTU Model KC11 DIN RS-485 | requirement/requirements.md:22 |
| Deployment | DTU Power Mode AC 85–270V KC11 KB-37 | requirement/requirements.md:23 |
| Protocol/Data | LoRaWAN Class Class C supports | requirement/requirements.md:24 |
| Protocol/Data | PKO-21309-DEHUM +KC11 | requirement/requirements.md:25 |
| Deployment | > KC11 = → otaConfig.Battery=false Class C MT-EB-POWER KB-37 | requirement/requirements.md:27 |
| Protocol/Data | - Modbus-RTU 9600/8/N/1 | requirement/requirements.md:39 |
| Deployment | DTU KC11 AC 85–270V KC11 | requirement/requirements.md:57 |
| Deployment | Power-on to Stable Working 1s 1 EB pwron_delay | requirement/requirements.md:63 |
| Protocol/Data | Interface Type RS-485 | requirement/requirements.md:69 |
| Protocol/Data | Baud Rate 9600 | requirement/requirements.md:70 |
| Protocol/Data | Protocol Type modbus-rtu | requirement/requirements.md:97 |
| Protocol/Data | Device Default Address 1 @6 1~32 | requirement/requirements.md:98 |
| Protocol/Data | 01= / 05= / 03= / 06= | requirement/requirements.md:99 |
| Protocol/Data | ≤120 ≤8 / supports 1 FC16 | requirement/requirements.md:100 |
| Protocol/Data | Modbus PLC = +1 | requirement/requirements.md:103 |
| Unresolved | > ⚠️ TBD#1 @30 / @31 「 (×10)」 01 「×10 」 FC03 int16 ÷10 Commissioning mt_debug_modbus | requirement/requirements.md:105 |
| Protocol/Data | > / FC03 /Get FC06 | requirement/requirements.md:147 |
| Protocol/Data | 「 ≤120 ≤8 」 EBHelper edgeBus docs/zh/EB/ | requirement/requirements.md:151 |
| Protocol/Data | C FC03 @30 8 @30–37 @30 + @31 + @35 + @37 | requirement/requirements.md:157 |
| Protocol/Data | D FC03 @1 1 @1 @1 | requirement/requirements.md:158 |
| Deployment | power_switch — bit FC01@176 — 1 /0 | requirement/requirements.md:183 |
| Deployment | pwron_delay app_38 uint16LE 200 ≥100 /RS485 5ms BUG-026 EB-DTU (KB-25) | requirement/requirements.md:203 |
| Protocol/Data | Modbus addr_modbus app_150 uint8 1 1–32 DTU | requirement/requirements.md:209 |
EdgeBus Code
The following is the current EdgeBus business code.
import { Buffer } from "buffer";
import { buildOtaFile } from "@EBSDK/run";
import { EBModel } from "@EBSDK/EBCompiler/EBModel/EBModel";
import { EventInfoItem } from "@EBSDK/EBCompiler/plugins/EBHelper";
import type { UserConfUPItem } from "@EBSDK/EBCompiler/plugins/EBHelper";
import { CheckbitEnum, getOtaConfig } from "@EBSDK/otaConfig";
// SL2000C-TY3 除湿机数据分散在线圈(FC01)+寄存器(FC03)、地址不连续,受协议「读位≤120 / 读寄存器≤8 / 写单点」
// 约束分 4 组顺序查询拼成一帧(最后一组 isLast 触发上行;帧内数据按查询顺序排列):
// A FC01 @0-46(47bit→6B) 故障@0-9 + 有无故障@27 + 手自动@28 + 负载@40-46 — HEX COV(任意变化即上报)
// B FC01 @176(1bit→1B) 开关机状态 — HEX COV
// C FC03 @30-37(读8寄存器,只取@30/@31/@35/@37) 回风湿度/回风温度 COV + 机组状态/设定温 周期
// D FC03 @1(设定湿度) 周期,isLast 触发上行
// 上行心跳(upPeriodIndex 70)与查询采集(periodIndex 74)分离:DTU 按采集周期高频轮询,无变化按心跳上报,命中变化即时上行。
// COV 阈值:回风温度 app_110、回风湿度 app_112;从机地址动态注入 app_150(indexAPP→cmd[0])。
const eventInfo: UserConfUPItem[] = [
{
name: "sl_dehum", port: 22, version: "0x87", dataType: "0x10", upPeriodIndex: 70,
quInfo: [
// A — 线圈块 @0-46:故障/有无故障/手自动/负载,整块 6 字节位图,任意位变化即上报
{
protocol: "modbus", addr: "0x01", code: "0x01", periodIndex: 74,
indexAPP: 150, indexCMD: 0, copySize: 1,
listVal: [
{ start: "0x00", end: "0x2E", covType: "HEX" }
]
},
// B — 线圈 @176:开关机状态,1 字节位图,变化即上报
{
protocol: "modbus", addr: "0x01", code: "0x01", periodIndex: 74,
indexAPP: 150, indexCMD: 0, copySize: 1,
listVal: [
{ start: "0xB0", end: "0xB0", covType: "HEX" }
]
},
// C — 寄存器读 @30-37(8 寄存器,仅抽取 @30/@31/@35/@37)
{
protocol: "modbus", addr: "0x01", code: "0x03", periodIndex: 74,
indexAPP: 150, indexCMD: 0, copySize: 1,
listVal: [
{ start: "0x1E", end: "0x1E", covType: "Int16BE", covAppIndex: 112 }, // 回风湿度 @30 — COV(阈值 app_112,÷10%RH)
{ start: "0x1F", end: "0x1F", covType: "Int16BE", covAppIndex: 110 }, // 回风温度 @31 — COV(阈值 app_110,÷10℃)
{ start: "0x23", end: "0x23" }, // 机组状态 @35 — 周期
{ start: "0x25", end: "0x25" } // 设定温度 @37 — 周期
]
},
// D — 寄存器读 @1:设定湿度,isLast 触发上行
{
protocol: "modbus", addr: "0x01", code: "0x03", periodIndex: 74,
indexAPP: 150, indexCMD: 0, copySize: 1, isLast: true,
listVal: [
{ start: "0x01", end: "0x01" } // 设定湿度 @1 — 周期
]
}
]
}
];
let otaConfig = getOtaConfig({
SwVersion: 31,
BaudRate: 9600,
StopBits: 1,
DataBits: 8,
Checkbit: CheckbitEnum.NONE,
Battery: false,
ConfirmDuty: 60,
BzType: 21310,
BzVersion: 1
});
const MODBUS_TT = (ebModel: EBModel) => {
for (let i = 0; i < eventInfo.length; i++) {
let event = new EventInfoItem(eventInfo[i]);
event.upEventSetup();
event.eventInstall();
}
return JSON.stringify(ebModel, null, 2);
};
buildOtaFile(import.meta.url, otaConfig, MODBUS_TT);Thing Model and RPC Code
RPC: [SL AUTOCTRL CFG]
id_name: sl_autoctrl_config_21310 · ID: 118639080874340352
import {RPCHelper, Utils} from '#tklHelper';
function rpc_script({ device, params, alarms, logger, org_params }) {
// 运维配置入口:把自控参数写入 server_attrs(顶层键,mergeDeviceAttrs 浅合并安全,KB-36)。
// 仅写本次传入字段(部分更新),并同步一条人类可读策略摘要 auto_policy(给人看,不驱动逻辑)。
const cur = device?.server_attrs || {};
const sa = {};
if (params.auto_enabled !== undefined) sa.auto_enabled = params.auto_enabled;
if (params.hum_high !== undefined) sa.hum_high = params.hum_high;
if (params.hum_low !== undefined) sa.hum_low = params.hum_low;
if (params.notify !== undefined) sa.notify = (params.notify && params.notify.length) ? params.notify : null; // 空=不通知
if (Object.keys(sa).length === 0) { logger.info("no config fields"); return null; }
const m = { ...cur, ...sa }; // 合并当前值后重算摘要
sa.auto_policy = (m.auto_enabled === true)
? `自动除湿已开启:回风湿度≥${m.hum_high}%RH 开机、≤${m.hum_low}%RH 关机;运行时段由定时任务控制(见「设备执行」面板),非运行时段强制关机。`
: "自动除湿已关闭(全手动,由人工或定时任务直接控制开关)。";
logger.info(`config -> ${JSON.stringify(sa)}`);
return [{ sleepTimeMs: 0, type: "modifyAttrs", dnMsg: { server_attrs: sa } }];
}
rpc_script({
device: { eui: "0102030400050001", server_attrs: { auto_enabled: false } },
params: { auto_enabled: true, hum_high: 70, hum_low: 55, notify: ["g1"] },
alarms: [], logger: console, org_params: {}
})RPC: [SL AUTOCTRL NOTIFY]
id_name: sl_autoctrl_notify_21310 · ID: 118642752979103744
import {Utils} from '#tklHelper';
function rpc_script({ device, params, alarms, logger, org_params }) {
// 开关机事件推送:type:"notify" 仅站内/邮件推送,不进告警中心、无需手动清除(事件类,区别于故障 alarm)。
const groups = params.notice_groups ?? device?.server_attrs?.notify ?? [];
if (!groups.length) { logger.info("no notice groups"); return null; }
return [{
sleepTimeMs: 0,
target: device.eui,
type: "notify",
dnMsg: { data: { title: params.title, desc: params.desc, notice_groups: groups } }
}];
}
rpc_script({
device: { eui: "0102030400050001", name: "SL除湿机", server_attrs: { notify: ["g1"] } },
params: { title: "[SL除湿机] 除湿机已开机", desc: "回风湿度 78%RH ≥上限 70%RH,已自动开机" },
alarms: [], logger: console, org_params: {}
})RPC: [SL AUTOCTRL WINDOW]
id_name: sl_autoctrl_window_21310 · ID: 118639079884484608
import {RPCHelper, Utils} from '#tklHelper';
function rpc_script({ device, params, alarms, logger, org_params }) {
// 供 CronTask 调度调用:翻转 server_attrs.in_window(1=允许运行/窗口内, 0=窗外强制关)。运维不手动调。
// 全天生效场景:绑定时把 in_window 预置为 1;要排定时段时由 CronTask 开窗(1)/闭窗(0)。
const inWin = params.in_window ? 1 : 0;
logger.info(`set in_window=${inWin}`);
return [{ sleepTimeMs: 0, type: "modifyAttrs", dnMsg: { server_attrs: { in_window: inWin } } }];
}
rpc_script({
device: { eui: "0102030400050001", server_attrs: {} },
params: { in_window: 1 },
alarms: [], logger: console, org_params: {}
})RPC: [SL FAULT RESET]
id_name: sl_fault_reset_action_21310 · ID: 118639074800988160
import {RPCHelper, Utils} from '#tklHelper';
function rpc_script({ device, params, alarms, logger, org_params }) {
let classMode = (device?.shared_attrs?.class_mode) || "ClassA";
let addr_modbus = device.shared_attrs?.addr_modbus ?? 1;
let rpcName = "sl_fault_reset_action_21310";
// 故障/低压报警复位(客户重点需求⑥):设备自带「故障复位」线圈 @177(FC05 写位),写 1 复位,
// 等价于原主板手动复位。瞬动命令,不写回 shared_attrs(复位无持久状态;平台告警由 sl_alarm_trigger
// 在故障位恢复后自动 clearCondition 消除)。表单 reset(boolean) 须勾选为 true 才下发,避免误触。
if (params.reset !== true) return null;
let coilVal = 0xFF00; // FC05 写单线圈 ON = 0xFF00(写 1 触发复位)
let dnBuffer = RPCHelper.modbusAction(addr_modbus, 5, 177, coilVal);
// FC05 回显 8 字节: [addr][05][coilH][coilL][valH][valL][crcL][crcH];校验写入值 = uint16BE @ 4 = 0xFF00。
let appInfo = [{field_name: "reset", index: 4, type: "uint16BE", val: coilVal}];
let checkInfo = {
frameInfo: {port: 51, dataLen: dnBuffer.length},
appInfo: appInfo,
// 复位是瞬动动作,无设定值回写:telemetry_data/server_attrs/shared_attrs 均不写。
server_attrs: null,
shared_attrs: null
};
let msgQue = Utils.makeParaSetMSG({
msgType: Utils.msgType.transParent,
checkInfo: checkInfo,
device: device,
classMode: classMode,
rpcName: rpcName,
params: params,
paraDownBuffer: dnBuffer,
timeout: 10000, // KB-26: 与开关机同量级往返;现场实测后微调
maxRetries: 3
});
if (msgQue.length === 0) return null;
return msgQue;
}
rpc_script({
device: {server_attrs: {}, shared_attrs: {addr_modbus: 1, class_mode: "ClassC"}},
params: {reset: true},
alarms: [],
logger: console,
org_params: {}
})RPC: [SL GET] para
id_name: sl_get_21310 · ID: 118639076910723072
import {RPCHelper, Utils} from '#tklHelper';
function rpc_script({ device, params, alarms, logger, org_params }) {
let classMode = (device?.shared_attrs?.class_mode) || "ClassA";
let sleepMs = classMode === "ClassA" ? 500 : 5000;
// 读回 DTU 参数(paraDef 须与 sl_set_21310 / sl_para_21310 完全一致,KB-41 字段集对齐)。
let paraDef = {
app_38: {name: "pwron_delay", field_name: "pwron_delay", unit: "ms", type: "uint16LE"},
app_40: {name: "period_heart", field_name: "period_heart", unit: "s", type: "uint32LE"},
app_70: {name: "period_up", field_name: "period_up", unit: "s", type: "uint32LE"},
app_74: {name: "period_read", field_name: "period_read", unit: "s", type: "uint32LE"},
app_110: {name: "cov_temp", field_name: "cov_temp", unit: "℃", type: "int16BE", coefficient: 0.1, decimal: 1},
app_112: {name: "cov_humidity", field_name: "cov_humidity", unit: "%RH", type: "int16BE", coefficient: 0.1, decimal: 1},
app_150: {name: "addr_modbus", field_name: "addr_modbus", unit: "", type: "uint8"}
};
let frames = RPCHelper.buildFrame({paraDef: paraDef, params: params});
let msg = RPCHelper.makeMSG({
msgType: Utils.msgType.paras,
device: device,
dnBuffer: frames.readBuffer,
sleepTime: sleepMs,
});
return [msg];
}
rpc_script({
device: {server_attrs: {}, shared_attrs: {}},
params: {},
alarms: [],
logger: console,
org_params: {}
})RPC: [SL POWER ACT]
id_name: sl_power_action_21310 · ID: 118639071638482944
import {RPCHelper, Utils} from '#tklHelper';
function rpc_script({ device, params, alarms, logger, org_params }) {
let classMode = (device?.shared_attrs?.class_mode) || "ClassA";
let addr_modbus = device.shared_attrs?.addr_modbus ?? 1;
let rpcName = "sl_power_action_21310";
// 开关机独立一条 RPC(KB-28:控制类频繁操作单独成条,自控触发器以此路开/关机)。
// 表单字段 power_switch(boolean),default_from:shared_attrs 回填当前状态。
// 设备开关机 = FC05 写线圈 @176(用户确认):0xFF00=开机 / 0x0000=关机(标准写单线圈,1=开/0=关)。
if (params.power_switch === undefined) return null;
let coilVal = params.power_switch ? 0xFF00 : 0x0000;
let dnBuffer = RPCHelper.modbusAction(addr_modbus, 5, 176, coilVal);
// FC05 回显 8 字节: [addr][05][coilH][coilL][valH][valL][crcL][crcH],原样回显「线圈地址+写入值」。
// 校验写入值 = uint16BE @ 4(0xFF00=开/0x0000=关);index 6-7 是 CRC,切勿用作校验。
let appInfo = [{field_name: "power_switch", index: 4, type: "uint16BE", val: coilVal}];
let checkInfo = {
frameInfo: {port: 51, dataLen: dnBuffer.length},
appInfo: appInfo,
// 回写:telemetry_data.power_switch 数字 0/1(遥测物模型 type:number);
// shared_attrs.power_switch 布尔(表单 type:boolean,default_from:shared_attrs 回填须同类型,KB-21)。
telemetry_data: {power_switch: params.power_switch ? 1 : 0},
server_attrs: null,
shared_attrs: {power_switch: params.power_switch}
};
let msgQue = Utils.makeParaSetMSG({
msgType: Utils.msgType.transParent,
checkInfo: checkInfo,
device: device,
classMode: classMode,
rpcName: rpcName,
params: params,
paraDownBuffer: dnBuffer,
timeout: 10000, // KB-26: 透传单帧往返实测约 8s(参 PKO 同类),×1.25 余量;现场实测后微调
maxRetries: 3
});
if (msgQue.length === 0) return null;
return msgQue;
}
rpc_script({
device: {server_attrs: {}, shared_attrs: {addr_modbus: 1, class_mode: "ClassC"}},
params: {power_switch: true},
alarms: [],
logger: console,
org_params: {}
})RPC: [SL SET] para
id_name: sl_set_21310 · ID: 118639075765678080
import {RPCHelper, Utils} from '#tklHelper';
function rpc_script({ device, params, alarms, logger, org_params }) {
let classMode = (device?.shared_attrs?.class_mode) || "ClassA";
let intervalms = classMode === "ClassA" ? 0 : 2000;
const rpcName = "sl_set_21310";
// DTU app 槽位(见 eb/21310-SL2000C.ts)。app_38 pwron_delay(上电延时,2字节)、app_40 period_heart(心跳,4字节)
// 相邻共占 app.38.6(EB __heart__ 事件读 6 字节)。COV:上行周期 app_70 / 查询周期 app_74 分离;
// SL 温湿度寄存器均 ×10 → COV 阈值 coefficient 0.1(与 PARA/EB Int16BE 原始值标度一致,0=任意变化即上报)。
let paraDef = {
app_38: {name: "pwron_delay", field_name: "pwron_delay", unit: "ms", type: "uint16LE"},
app_40: {name: "period_heart", field_name: "period_heart", unit: "s", type: "uint32LE"},
app_70: {name: "period_up", field_name: "period_up", unit: "s", type: "uint32LE"},
app_74: {name: "period_read", field_name: "period_read", unit: "s", type: "uint32LE"},
app_110: {name: "cov_temp", field_name: "cov_temp", unit: "℃", type: "int16BE", coefficient: 0.1, decimal: 1},
app_112: {name: "cov_humidity", field_name: "cov_humidity", unit: "%RH", type: "int16BE", coefficient: 0.1, decimal: 1},
app_150: {name: "addr_modbus", field_name: "addr_modbus", unit: "", type: "uint8"}
};
// ── 心跳省电(KB-25):当前心跳 < 1h 且本次未显式指定 → 兜底 24h(86400s,新对接默认,取代旧 8h)。
const HEART_MIN_KEEP = 3600; // 1h 阈值
const HEART_DEFAULT = 86400; // 24h 默认(省电)
let curHeart = device?.shared_attrs?.period_heart;
if (params.period_heart == null && (curHeart == null || curHeart < HEART_MIN_KEEP)) {
params.period_heart = HEART_DEFAULT;
logger.info(`period_heart default -> ${HEART_DEFAULT}s (cur=${curHeart})`);
}
// ── 上电延时(KB-25 / BUG-026):未指定或 <100ms 兜底 200ms(换能器上电稳定;5ms 致查询超时 status=0x02)。
if (params.pwron_delay == null || params.pwron_delay < 100) {
params.pwron_delay = 200;
logger.info("pwron_delay default -> 200ms");
}
let frames = RPCHelper.buildFrame({paraDef: paraDef, params: params});
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);
logger.info("set para");
let msgQue = Utils.makeParaSetMSG({
device: device,
classMode: classMode,
rpcName: rpcName,
params: params,
paraDownBuffer: dnBuffer,
extraAppBuffer: redoBuffer
});
if (msgQue.length === 0) return null;
return msgQue;
}
// 当前心跳 15min(<1h) 且本次未指定 → period_heart 兜底 86400(24h);pwron_delay 未指定 → 200ms
rpc_script({
device: {server_attrs: {}, shared_attrs: {class_mode: "ClassC", period_heart: 900}},
params: {period_up: 300, period_read: 60, addr_modbus: 1},
alarms: [],
logger: console,
org_params: {}
})RPC: [SL SETPOINT ACT]
id_name: sl_setpoint_action_21310 · ID: 118639073274261504
import {RPCHelper, Utils} from '#tklHelper';
function rpc_script({ device, params, alarms, logger, org_params }) {
let classMode = (device?.shared_attrs?.class_mode) || "ClassA";
let addr_modbus = device.shared_attrs?.addr_modbus ?? 1;
let rpcName = "sl_setpoint_action_21310";
// 变体入参 setpoint(KB-28):下拉选「设定湿度 / 设定温度」,两路骨架相同
//(FC06 写单寄存器、写入值=设定值×10),只差寄存器地址与回写字段,合并成一条精简 RPC。
// 提交形态:params.setpoint = { value: 判别值, <判别值>: 设定值 }(VARIANT_DISCRIMINATOR_KEY="value")。
// 子字段 default_from:"shared_attrs" + attr_field_name(humidity_set/temp_set) 供弹窗回填当前设定点(KB-28)。
// 缺判别值/缺设定值一律 return null 安全兜底(含被定时任务/设备执行排程时,KB-27)。
const SETPOINT = {
humidity: {reg: 0x0001, attr: "humidity_set"}, // 设定湿度 寄存器@1,回写 shared_attrs.humidity_set,10-80%RH
temp: {reg: 0x0025, attr: "temp_set"} // 设定温度 寄存器@37,回写 shared_attrs.temp_set,5-50℃
};
const which = params?.setpoint?.value; // 判别值(嵌套 value 键)
if (which == null) return null;
const conf = SETPOINT[which];
if (!conf) return null; // 未知判别值安全兜底
const setVal = params?.setpoint?.[which]; // 设定值(嵌套 params.setpoint[which])
if (setVal === undefined || setVal === null) return null;
const regVal = Math.round(setVal * 10); // 下行寄存器值 = 设定值×10(仅用于构 FC06 帧)
let dnBuffer = RPCHelper.modbusAction(addr_modbus, 6, conf.reg, regVal);
// FC06 回显 8 字节: [addr][06][regH][regL][valH][valL][crcL][crcH];校验位 = uint16BE @ 4(index6-7 是 CRC)。
// coefficient:0.1 让 PayloadParser 把回显寄存器值还原为真实设定值后比对,故 val 直接用 setVal。
let appInfo = [{field_name: conf.attr, index: 4, type: "uint16BE", coefficient: 0.1, val: setVal}];
let checkInfo = {
frameInfo: {port: 51, dataLen: dnBuffer.length},
appInfo: appInfo,
// 设定点是属性非遥测:回显成功只写回 shared_attrs(paraCheck 不读 telemetry_data;实时显示由
// 周期帧经遥测模型输出,KB-24)。该 shared_attrs 服务①弹窗回填②自控触发器/Get 读当前设定点。
server_attrs: null,
shared_attrs: {[conf.attr]: setVal}
};
let msgQue = Utils.makeParaSetMSG({
msgType: Utils.msgType.transParent,
checkInfo: checkInfo,
device: device,
classMode: classMode,
rpcName: rpcName,
params: params,
paraDownBuffer: dnBuffer,
timeout: 10000, // KB-26: 实测往返≈8s,×1.25 余量
maxRetries: 3
});
if (msgQue.length === 0) return null;
return msgQue;
}
rpc_script({
device: {server_attrs: {}, shared_attrs: {addr_modbus: 1, class_mode: "ClassC", humidity_set: 55, temp_set: 25}},
params: {setpoint: {value: "humidity", humidity: 60}},
alarms: [],
logger: console,
org_params: {}
})Thing Model: [SL2000C]
id_name: sl_21310 · ID: 118639068635361280
import {PayloadParser, RPCHelper, Utils} from '#tklHelper';
function payload_parser(device, msg, thingModelId, noticeAttrs, org_params) {
let port = msg?.userdata?.port || null;
// 纯遥测物模型:只产出实时显示数据(telemetry_data),不写任何属性。
// 设定值回填源同步(port 22 当前设定值 → shared_attrs)与 action 回显闭环(port 51)统一交给
// 参数物模型 sl_para_21310 —— 属性写回归 PARA 模型(thingModel skill「设定值回填同步」规则)。
if (port !== 22) return null;
// 帧布局见 requirements §9:header 6 + 线圈块A(@0-46,6B) + 线圈B(@176,1B) + 寄存器C(@30/@31/@35/@37) + 寄存器D(@1)。
let frameInfo = {
port: 22, dataLen: 23, rssi: true, battery: 4,
tagList: [{index: 0, tag: 0x87}, {index: 1, tag: 0x10}]
};
// 线圈位组用 uint8 整字节读入后位移解析(KB-27:勿用 bitBEn,低字节位读不到)。
// 寄存器全为带符号 int16(协议「数据均为带符号整型」);模拟量 ×10,coefficient 0.1 还原。
let appInfo = [
{name: "fb0", field_name: "fb0", unit: "", index: 6, type: "uint8"}, // 线圈@0-7 故障
{name: "fb1", field_name: "fb1", unit: "", index: 7, type: "uint8"}, // 线圈@8-15 传感器故障
{name: "sb", field_name: "sb", unit: "", index: 9, type: "uint8"}, // 线圈@24-31 有无故障/手自动
{name: "lb", field_name: "lb", unit: "", index: 11, type: "uint8"}, // 线圈@40-47 负载
{name: "pb", field_name: "pb", unit: "", index: 12, type: "uint8"}, // 线圈@176 开关机
{name: "return_humidity", field_name: "return_humidity", unit: "%RH", index: 13, type: "int16BE", coefficient: 0.1, decimal: 1},
{name: "return_temp", field_name: "return_temp", unit: "℃", index: 15, type: "int16BE", coefficient: 0.1, decimal: 1},
{name: "us", field_name: "us", unit: "", index: 17, type: "int16BE"}, // 机组状态原始值
{name: "temp_set", field_name: "temp_set", unit: "℃", index: 19, type: "int16BE", coefficient: 0.1, decimal: 1},
{name: "humidity_set", field_name: "humidity_set", unit: "%RH", index: 21, type: "int16BE", coefficient: 0.1, decimal: 1},
];
let payParser = new PayloadParser({device: device, msg: msg, frameInfo: frameInfo, appInfo: appInfo});
let tdata = payParser.telemetry();
// 查询超时(status bit1):保留上帧遥测,仅更新状态位,避免清零显示。
if ((tdata?.status & 0x02) === 0x02) {
const status = tdata.status;
tdata = {...(device.telemetry_data?.[thingModelId] ?? {})};
tdata.status = status;
tdata._modelName = "sl_21310";
return {telemetry_data: tdata, server_attrs: null, shared_attrs: null};
}
if (tdata) {
// ── 开关机状态(线圈@176,B 段 1 字节 bit0)──
tdata.power_switch = (Number(tdata.pb) >> 0) & 1;
// ── 机组状态(寄存器@35)→ 可读文本 ──
const UNIT = {0: "待机", 1: "除湿", 2: "除霜", 3: "停机中"};
tdata.unit_status = UNIT[Number(tdata.us)] ?? ("未知(" + tdata.us + ")");
// ── 故障位(线圈@0-9,A 段 byte0/byte1)逐位解析;负载与故障严格分组(KB-32)──
let fb0 = Number(tdata.fb0) || 0, fb1 = Number(tdata.fb1) || 0, sb = Number(tdata.sb) || 0;
tdata.fault_supply_fan_oc = (fb0 >> 0) & 1; // @0 送风机过载
tdata.fault_high_press = (fb0 >> 1) & 1; // @1 压缩机高压/过载
tdata.fault_low_press = (fb0 >> 2) & 1; // @2 压缩机低压
tdata.fault_comp_oc = (fb0 >> 3) & 1; // @3 压缩机过载
tdata.fault_cond_fan1_oc = (fb0 >> 4) & 1; // @4 冷凝风机一过载
tdata.fault_cond_fan2_oc = (fb0 >> 5) & 1; // @5 冷凝风机二过载
tdata.fault_phase_seq = (fb0 >> 6) & 1; // @6 相序保护
tdata.fault_user_interlock = (fb0 >> 7) & 1; // @7 用户故障连锁
tdata.fault_humidity_sensor = (fb1 >> 0) & 1; // @8 湿度传感器故障
tdata.fault_temp_sensor = (fb1 >> 1) & 1; // @9 温度探头故障
tdata.has_fault = (sb >> 3) & 1; // @27 有无故障
tdata.manual_auto = (sb >> 4) & 1; // @28 手动(1)/自动(0)
const FAULTS = [
["fault_supply_fan_oc", "送风机过载"], ["fault_high_press", "压缩机高压/过载"],
["fault_low_press", "压缩机低压"], ["fault_comp_oc", "压缩机过载"],
["fault_cond_fan1_oc", "冷凝风机一过载"], ["fault_cond_fan2_oc", "冷凝风机二过载"],
["fault_phase_seq", "相序保护"], ["fault_user_interlock", "用户故障连锁"],
["fault_humidity_sensor", "湿度传感器故障"], ["fault_temp_sensor", "温度探头故障"]
];
let active = FAULTS.filter(([k]) => tdata[k]).map(([, n]) => `[${n}]`);
tdata.fault_text = active.length ? active.join(";") : "正常";
// ── 负载工作状态(线圈@40-46,A 段 byte5)每路一个独立开关字段(是否在工作,非故障)──
let lw = Number(tdata.lb) || 0;
tdata.compressor = (lw >> 0) & 1; // @40 压缩机
tdata.cond_fan_1 = (lw >> 1) & 1; // @41 冷凝风机1
tdata.four_way_valve = (lw >> 2) & 1; // @42 四通阀
tdata.cond_fan_2 = (lw >> 3) & 1; // @43 冷凝风机2
tdata.supply_fan = (lw >> 4) & 1; // @44 送风机
tdata.heat_valve = (lw >> 5) & 1; // @45 升温电磁阀
tdata.cool_valve = (lw >> 6) & 1; // @46 降温电磁阀
// 删除中间原始字节字段,保持 telemetry_data 整洁
delete tdata.fb0; delete tdata.fb1; delete tdata.sb; delete tdata.lb; delete tdata.pb; delete tdata.us;
tdata._modelName = "sl_21310";
}
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: "sl_21310", name: "[SL2000C]", 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;
}
// 本地自测:port22 帧 — 低压故障@2=1、有无故障@27=1、压缩机@40+送风机@44 在工作、开关机=开、
// 回风湿度55.0 回风温度22.5 机组状态1(除湿) 设定温25.0 设定湿60.0
// 期望:power_switch=1 unit_status="除湿" fault_low_press=1 has_fault=1 fault_text="[压缩机低压]"
// compressor=1 supply_fan=1 return_humidity=55 return_temp=22.5 temp_set=25 humidity_set=60
console.log(payload_parser(
{telemetry_data: {}},
{userdata: {port: 22, payload: "hxAAAAABBAAACAARAQImAOEAAQD6Alg="}},
"1",
{telemetry_data: true},
{}
))Thing Model: sl_asset_21310
id_name: sl_asset_21310 · ID: 119677174411063303
import {PayloadParser, RPCHelper} from '#tklHelper';
// 资产(VIRTUAL)聚合物模型 — 通用 agg_spec 驱动聚合器,见 docs/references/workflow/asset-aggregation-model.md
// device=资产, msg=源设备快照, thingModelId=本物模型ID, noticeAttrs=变更标记。
// 聚合规格在资产 server_attrs.agg_spec:on_power_switch_cnt=ON计数(power_switch);zone_return_temp_avg=空间均值(return_temp);zone_return_humidity_avg=空间均值(return_humidity);zone_temp_set_avg=空间均值(temp_set);zone_humidity_set_avg=空间均值(humidity_set);on_compressor_cnt=ON计数(compressor);on_cond_fan_1_cnt=ON计数(cond_fan_1);on_cond_fan_2_cnt=ON计数(cond_fan_2);on_four_way_valve_cnt=ON计数(four_way_valve);on_supply_fan_cnt=ON计数(supply_fan);on_heat_valve_cnt=ON计数(heat_valve);on_cool_valve_cnt=ON计数(cool_valve);any_fault_high_press=逻辑或(fault_high_press);any_fault_low_press=逻辑或(fault_low_press);any_has_fault=逻辑或(has_fault);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: "sl_asset_21310", name: "[SL-21310-SL2000C资产] 区域聚合", 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 = {
"on_power_switch_cnt": {
"op": "count_on",
"src": "power_switch"
},
"zone_return_temp_avg": {
"op": "avg",
"src": "return_temp"
},
"zone_return_humidity_avg": {
"op": "avg",
"src": "return_humidity"
},
"zone_temp_set_avg": {
"op": "avg",
"src": "temp_set"
},
"zone_humidity_set_avg": {
"op": "avg",
"src": "humidity_set"
},
"on_compressor_cnt": {
"op": "count_on",
"src": "compressor"
},
"on_cond_fan_1_cnt": {
"op": "count_on",
"src": "cond_fan_1"
},
"on_cond_fan_2_cnt": {
"op": "count_on",
"src": "cond_fan_2"
},
"on_four_way_valve_cnt": {
"op": "count_on",
"src": "four_way_valve"
},
"on_supply_fan_cnt": {
"op": "count_on",
"src": "supply_fan"
},
"on_heat_valve_cnt": {
"op": "count_on",
"src": "heat_valve"
},
"on_cool_valve_cnt": {
"op": "count_on",
"src": "cool_valve"
},
"any_fault_high_press": {
"op": "or",
"src": "fault_high_press"
},
"any_fault_low_press": {
"op": "or",
"src": "fault_low_press"
},
"any_has_fault": {
"op": "or",
"src": "has_fault"
},
"online_count": {
"op": "count_online",
"src": ""
}
};
const device = { name: "SL-21310-SL2000C资产-1", server_attrs: { agg_spec, stale_ms: 1800000, members: {} } };
const r1 = payload_parser(device, { eui: "MEMBER_A", telemetry_data: { m1: {"power_switch":1,"return_temp":1,"return_humidity":1,"temp_set":1,"humidity_set":1,"compressor":1,"cond_fan_1":1,"cond_fan_2":1,"four_way_valve":1,"supply_fan":1,"heat_valve":1,"cool_valve":1,"fault_high_press":1,"fault_low_press":1,"has_fault":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: {"power_switch":2,"return_temp":2,"return_humidity":2,"temp_set":2,"humidity_set":2,"compressor":2,"cond_fan_1":2,"cond_fan_2":2,"four_way_valve":2,"supply_fan":2,"heat_valve":2,"cool_valve":2,"fault_high_press":2,"fault_low_press":2,"has_fault":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: [SL2000C-PARA]
id_name: sl_para_21310 · ID: 118639069788794880
import {PayloadParser, RPCHelper, Utils} from '#tklHelper';
function payload_parser(device, msg, thingModelId, noticeAttrs, org_params) {
let port = msg?.userdata?.port || null;
const rpcName = "sl_set_21310";
// EB app 槽位(见 eb/21310-SL2000C.ts 与 requirements §6.3/§9)。系统字段 TimeOffset(app_20)/pwron_delay(app_38)
// 按 EB-DTU 规约纳入(KB-25/KB-37:新对接 SET 须可配 pwron_delay+心跳)。SL 温湿度均 ×10 → COV 阈值 coeff 0.1。
let paraDef = {
app_20: {name: "TimeOffset", field_name: "TimeOffset", unit: "", type: "uint32LE"},
app_38: {name: "pwron_delay", field_name: "pwron_delay", unit: "ms", type: "uint16LE"},
app_40: {name: "period_heart", field_name: "period_heart", unit: "s", type: "uint32LE"},
app_70: {name: "period_up", field_name: "period_up", unit: "s", type: "uint32LE"},
app_74: {name: "period_read", field_name: "period_read", unit: "s", type: "uint32LE"},
app_110: {name: "cov_temp", field_name: "cov_temp", unit: "℃", type: "int16BE", coefficient: 0.1, decimal: 1},
app_112: {name: "cov_humidity", field_name: "cov_humidity", unit: "%RH", type: "int16BE", coefficient: 0.1, decimal: 1},
app_150: {name: "addr_modbus", field_name: "addr_modbus", unit: "", type: "uint8"}
};
// ── port 51:action RPC 透传回显闭环(sl_power_action / sl_setpoint_action / sl_fault_reset_action)──
// 属性写回归 PARA。设备只支持单点写:FC05 写线圈(开关机@176/故障复位@177)、FC06 写单寄存器(设定温@37/湿@1)。
// 两类回显均 8 字节,写入值在 uint16BE@(帧首+4):FC05→0xFF00开/0x0000关;FC06→寄存器值。
// checkInfo.appInfo 单条目 index=4 比对写入值;index6-7 为 CRC,勿用作校验。
if (port === 51) {
let pending = device.server_attrs?.[Utils.msgType.transParent];
if (pending?.checkInfo != undefined) {
let echo = (new PayloadParser({
device: device, msg: msg,
frameInfo: pending.checkInfo.frameInfo, appInfo: pending.checkInfo.appInfo,
})).telemetry();
let want = pending.checkInfo.appInfo?.[0];
let got = echo?.[want?.field_name];
if (typeof console !== "undefined") {
console.log("[port51] echo " + want?.field_name + ": want=" + want?.val + " got=" + got);
}
}
let retval = Utils.paraCheck(Utils.msgType.transParent, device.server_attrs, {device: device, msg: msg});
return {
telemetry_data: retval.tdata,
server_attrs: retval.sdata,
shared_attrs: retval.pdata,
actions: retval.actions,
}
}
// ── port 22:周期帧里的"当前设定值"同步进 shared_attrs(RPC 表单/自控/Get 回填源,KB-24)──
// 设备每周期上报当前 开关机/设定温/设定湿。本地面板改动或冷启动从未下过 RPC 时,唯一权威来源是此周期帧。
// 类型对齐表单字段(KB-21):power_switch→boolean,temp_set/humidity_set→number。
// 只写本帧确有的字段(!==undefined 守卫,避免 KB-18 部分帧覆盖)。port 22 同时推进 sl_set 状态机。
if (port === 22) {
let setApp = [
{name: "pb", field_name: "pb", unit: "", index: 12, type: "uint8"},
{name: "temp_set", field_name: "temp_set", unit: "℃", index: 19, type: "int16BE", coefficient: 0.1, decimal: 1},
{name: "humidity_set", field_name: "humidity_set", unit: "%RH", index: 21, type: "int16BE", coefficient: 0.1, decimal: 1}
];
let setVals = (new PayloadParser({
device: device, msg: msg,
frameInfo: {port: 22, dataLen: 23, rssi: true, battery: 4, tagList: [{index: 0, tag: 0x87}, {index: 1, tag: 0x10}]},
appInfo: setApp,
})).telemetry();
let sdata = {};
if (setVals?.pb !== undefined) sdata.power_switch = !!((Number(setVals.pb) >> 0) & 1);
if (setVals?.temp_set !== undefined) sdata.temp_set = setVals.temp_set;
if (setVals?.humidity_set !== undefined) sdata.humidity_set = setVals.humidity_set;
let checkData = Utils.paraCheck(rpcName, device.server_attrs, device.shared_attrs);
return {
server_attrs: checkData.sdata,
shared_attrs: sdata,
actions: checkData.actions,
}
}
// ── port 214:DTU 参数读回(period/COV阈值/pwron_delay/addr)──
if (port === 214) {
let pdata = (new PayloadParser({device: device, msg: msg, paraInfo: paraDef})).paras();
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: "sl_para_21310", name: "[SL2000C-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;
}
// ── 其它端口(心跳等):仅推进 sl_set 状态机的超时/重发判定 ──
let checkData = Utils.paraCheck(rpcName, device.server_attrs, device.shared_attrs);
return {
server_attrs: checkData.sdata,
actions: checkData.actions,
}
}
console.log(payload_parser(
{telemetry_data: {}, server_attrs: {}, shared_attrs: {}},
{userdata: {port: 11, payload: ""}},
"1",
{telemetry_data: true},
{}
))Trigger: sl_alarm_trigger_21310
id_name: sl_alarm_trigger_21310 · ID: 118639077997047808
import {Utils} from '#tklHelper';
function trigger_script(device, thingModelId) {
const td = device?.telemetry_data?.[thingModelId];
// 业务帧门控(KB-30:生产触发器不注入模型名标识,勿用其判别)。has_fault 在每个 port-22 业务帧都解析为 0/1,
// 非业务帧/陈旧帧为 undefined → 跳过,避免在参数帧上误判故障恢复。
if (!td || td.has_fault == null) return null;
const eui = device.eui, dName = device.name;
const notify = device?.server_attrs?.notify ?? []; // KB-38:内置 alarm 方法只读 params.notify(非 group)
// 故障位均为数字二值信号:cond=故障置位,clr=条件取反(KB-10 数字信号无需迟滞死区)。
// 高压/低压为客户重点关注项(high);复位经 sl_fault_reset_action_21310 写线圈@177。
const checks = [
{ v: td.fault_high_press, name: "sl_high_press", title: `[${dName}] 压缩机高压告警`, level: "high", desc: "压缩机高压/过载,请检查冷凝散热" },
{ v: td.fault_low_press, name: "sl_low_press", title: `[${dName}] 压缩机低压告警`, level: "high", desc: "压缩机低压,请检查制冷剂/蒸发侧(可远程复位)" },
{ v: td.fault_phase_seq, name: "sl_phase_seq", title: `[${dName}] 相序保护告警`, level: "high", desc: "相序保护动作,请检查三相电源接线" },
{ v: td.fault_user_interlock, name: "sl_user_interlock", title: `[${dName}] 用户故障连锁`, level: "high", desc: "外部故障连锁触发" },
{ v: td.fault_comp_oc, name: "sl_comp_oc", title: `[${dName}] 压缩机过载`, level: "mid", desc: "压缩机过载" },
{ v: td.fault_supply_fan_oc, name: "sl_supply_fan_oc", title: `[${dName}] 送风机过载`, level: "mid", desc: "送风机过载" },
{ v: td.fault_cond_fan1_oc, name: "sl_cond_fan1_oc", title: `[${dName}] 冷凝风机一过载`, level: "mid", desc: "冷凝风机一过载" },
{ v: td.fault_cond_fan2_oc, name: "sl_cond_fan2_oc", title: `[${dName}] 冷凝风机二过载`, level: "mid", desc: "冷凝风机二过载" },
{ v: td.fault_humidity_sensor,name: "sl_humidity_sensor",title: `[${dName}] 湿度传感器故障`, level: "mid", desc: "湿度传感器故障,湿度闭环不可靠" },
{ v: td.fault_temp_sensor, name: "sl_temp_sensor", title: `[${dName}] 温度探头故障`, level: "mid", desc: "温度探头故障" },
];
const actions = checks.flatMap(c => {
if (c.v === 1) return [{ method: "alarm", params: { _eui: eui, action: Utils.ACTION.new,
name: c.name, title: c.title, level: c.level, desc: c.desc, notify } }];
if (c.v === 0) return [{ method: "alarm", params: { _eui: eui, action: Utils.ACTION.clear,
name: c.name, title: c.title, level: c.level, desc: "故障恢复,自动消除", notify } }];
return [];
});
if (!actions.length) return null;
return { delayms: 0, abort_previous_timer: true, actions };
}
// 用例:低压故障置位 + 其余正常 → 低压 new,其它 clear
console.log(JSON.stringify(trigger_script(
{ eui: "0102030400050001", name: "SL除湿机", server_attrs: { notify: ["g1"] },
telemetry_data: { "1": { has_fault: 1, fault_low_press: 1, fault_high_press: 0, fault_phase_seq: 0,
fault_user_interlock: 0, fault_comp_oc: 0, fault_supply_fan_oc: 0, fault_cond_fan1_oc: 0,
fault_cond_fan2_oc: 0, fault_humidity_sensor: 0, fault_temp_sensor: 0 } } },
"1"
), null, 1))Trigger: sl_autoctrl_trigger_21310
id_name: sl_autoctrl_trigger_21310 · ID: 118639078986903552
import {Utils} from '#tklHelper';
function trigger_script(device, thingModelId) {
const sa = device?.server_attrs || {};
if (sa.auto_enabled !== true) return null; // 总开关:未启用自控则让位人工/定时
const td = device?.telemetry_data?.[thingModelId];
if (!td) return null;
// 业务帧由 return_humidity/power_switch 判别(KB-30:生产触发器不注入模型名标识)。
if (td.return_humidity == null || td.power_switch == null) return null;
const power = td.power_switch; // 设备真实回报的开关状态
const group = sa.notify ?? [];
const eui = device.eui;
const dName = device.name;
// 窗口外:强制关机(安全网;闭窗 Cron 也会关)。全天生效场景下 in_window 默认 1,此分支不触发。
if (sa.in_window !== 1) {
if (power === 1) {
return { delayms: 2000, abort_previous_timer: true, should_dispatch: true, actions: [
{ method: "sl_power_action_21310", params: { _eui: eui, power_switch: false } },
{ method: "sl_autoctrl_notify_21310", params: {
_eui: eui, title: `[${dName}] 除湿机已关机`,
desc: "非定时运行时段,已自动关机", notice_groups: group } }
]};
}
return null;
}
// 窗口内:回风湿度迟滞启停(≥上限开、≤下限关;死区内不动作,防频繁启停)。
// 纯开关控制——不改写设备内部设定湿度,保留用户在设备/平台设的设定点(与 PKO 强制设定点的写法不同,更简更稳)。
const HI = sa.hum_high, LO = sa.hum_low;
if (HI == null || LO == null) return null;
let desired = null;
if (td.return_humidity >= HI && power !== 1) desired = 1; // 开机
else if (td.return_humidity <= LO && power === 1) desired = 0; // 关机
if (desired === null) return null; // 死区 → 不动作
const onoff = desired ? "开机" : "关机";
return { delayms: 2000, abort_previous_timer: true, should_dispatch: true, actions: [
{ method: "sl_power_action_21310", params: { _eui: eui, power_switch: desired === 1 } },
{ method: "sl_autoctrl_notify_21310", params: {
_eui: eui, title: `[${dName}] 除湿机已${onoff}`,
desc: `回风湿度 ${td.return_humidity}%RH ${desired ? "≥上限 " + HI : "≤下限 " + LO}%RH,已自动${onoff}`,
notice_groups: group } }
]};
}
// 用例:auto+窗口内+湿度78≥70+当前关机 → 期望 开机 + 通知 两条 action
console.log(trigger_script(
{ eui: "0102030400050001", name: "SL除湿机",
server_attrs: { auto_enabled: true, in_window: 1, hum_high: 70, hum_low: 55, notify: ["g1"] },
telemetry_data: { "1": { return_humidity: 78, power_switch: 0 } } },
"1"
))Thing Model Field Definitions
| Thing Model | Field | Type | Unit | Name |
|---|---|---|---|---|
| sl_21310 | power_switch | number | Power Switch | |
| sl_21310 | unit_status | string | Unit Status | |
| sl_21310 | return_temp | number | ℃ | Return Air Temperature |
| sl_21310 | return_humidity | number | %RH | Return Air Humidity |
| sl_21310 | temp_set | number | ℃ | Temperature Setpoint |
| sl_21310 | humidity_set | number | %RH | Humidity Setpoint |
| sl_21310 | compressor | number | Compressor | |
| sl_21310 | cond_fan_1 | number | Condenser Fan 1 | |
| sl_21310 | cond_fan_2 | number | Condenser Fan 2 | |
| sl_21310 | four_way_valve | number | Four-Way Valve | |
| sl_21310 | supply_fan | number | Supply Fan | |
| sl_21310 | heat_valve | number | Heating Solenoid Valve | |
| sl_21310 | cool_valve | number | Cooling Solenoid Valve | |
| sl_21310 | fault_high_press | number | High-Pressure Fault | |
| sl_21310 | fault_low_press | number | Low-Pressure Fault | |
| sl_21310 | fault_text | string | Fault Status | |
| sl_21310 | has_fault | number | Has Fault | |
| sl_21310 | manual_auto | number | Manual / Auto | |
| sl_asset_21310 | on_power_switch_cnt | number | Zone Power Switch | |
| sl_asset_21310 | zone_return_temp_avg | number | ℃ | Zone Return Air Temperature |
| sl_asset_21310 | zone_return_humidity_avg | number | %RH | Zone Return Air Humidity |
| sl_asset_21310 | zone_temp_set_avg | number | ℃ | Zone Temperature Setpoint |
| sl_asset_21310 | zone_humidity_set_avg | number | %RH | Zone Humidity Setpoint |
| sl_asset_21310 | on_compressor_cnt | number | Zone Compressor | |
| sl_asset_21310 | on_cond_fan_1_cnt | number | Zone Condenser Fan 1 | |
| sl_asset_21310 | on_cond_fan_2_cnt | number | Zone Condenser Fan 2 | |
| sl_asset_21310 | on_four_way_valve_cnt | number | Zone Four-Way Valve | |
| sl_asset_21310 | on_supply_fan_cnt | number | Zone Supply Fan | |
| sl_asset_21310 | on_heat_valve_cnt | number | Zone Heating Solenoid Valve | |
| sl_asset_21310 | on_cool_valve_cnt | number | Zone Cooling Solenoid Valve | |
| sl_asset_21310 | any_fault_high_press | number | Zone High-Pressure Fault | |
| sl_asset_21310 | any_fault_low_press | number | Zone Low-Pressure Fault | |
| sl_asset_21310 | any_has_fault | number | Zone Has Fault | |
| sl_asset_21310 | online_count | number | Zone Online Count | |
| sl_para_21310 | period_up | number | s | Upload Period |
| sl_para_21310 | period_read | number | s | Collect Period |
| sl_para_21310 | cov_temp | number | ℃ | Temp COV Threshold |
| sl_para_21310 | cov_humidity | number | %RH | Humidity COV Threshold |
| sl_para_21310 | period_heart | number | s | Heartbeat Period |
| sl_para_21310 | pwron_delay | number | ms | Power-on Delay |
| sl_para_21310 | addr_modbus | number | Modbus Address | |
| sl_para_21310 | TimeOffset | number | Time Offset | |
| sl_para_21310 | snr | number | dB | SNR |
| sl_para_21310 | rssi | number | dBm | RSSI |
RPC Parameter Definitions
| RPC | Parameter | Type | Description | Default | Unit |
|---|---|---|---|---|---|
| sl_autoctrl_config_21310 | auto_enabled | boolean | Auto Control Enable | — | |
| sl_autoctrl_config_21310 | hum_high | number | Humidity High (ON) | — | %RH |
| sl_autoctrl_config_21310 | hum_low | number | Humidity Low (OFF) | — | %RH |
| sl_autoctrl_config_21310 | notify | notice_group | Notice Groups | — | |
| sl_autoctrl_notify_21310 | title | string | Title | ||
| sl_autoctrl_notify_21310 | desc | string | Description | ||
| sl_autoctrl_window_21310 | in_window | number | In Window (1/0) | — | |
| sl_fault_reset_action_21310 | reset | boolean | Fault Reset | false | |
| sl_power_action_21310 | power_switch | boolean | Power Switch | — | |
| sl_set_21310 | period_heart | number | Heartbeat Period (s) | — | s |
| sl_set_21310 | period_up | number | Upload Period (s) | — | s |
| sl_set_21310 | period_read | number | Collect Period (s) | — | s |
| sl_set_21310 | cov_temp | number | Temp Change Threshold | — | ℃ |
| sl_set_21310 | cov_humidity | number | Humidity Change Threshold | — | %RH |
| sl_set_21310 | addr_modbus | number | Modbus Address (1-32) | — | |
| sl_set_21310 | pwron_delay | number | Power-on Delay (ms) | — | ms |
| sl_setpoint_action_21310 | setpoint | variant | Setpoint | humidity |
Trigger Definitions
| Trigger | Condition Signals | Delay Expression | RPC Actions | Abort Previous Timer |
|---|---|---|---|---|
| sl_alarm_trigger_21310 | — | 0 | alarm | true |
| sl_autoctrl_trigger_21310 | — | 2000 | sl_power_action_21310, sl_autoctrl_notify_21310 | true |
Third-Party Platform Subscription
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,
"return_temp": 1,
"return_humidity": 2,
"temp_set": 3,
"humidity_set": 4,
"power_switch": 6.25,
"unit_status": "sample-6",
"compressor/cond_fan_1/cond_fan_2/four_way_valve/supply_fan/heat_valve/cool_valve": 8.75,
"fault_text": "sample-8"
}
}Template Selection and Verification
Search ThinkLink by project name, business code, or the id_name and ID values in the model inventory. After deployment, use real uplinks to verify fields, units, RPCs, and trigger behavior.
Supplementary Notes
Requirements and decisions: requirement/requirements.md.