Skip to content

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.

ItemValue
ProjectSL-21310-SL2000C
VendorSL
ModelSL2000C
Business Code21310
Integration Modeedgebus-dtu
Protocolmodbus-rtu

Model catalog

Search for these models in ThinkLink by name or ID.

Model typeNameid_namePlatform model ID
RPC[SL AUTOCTRL CFG]sl_autoctrl_config_21310118639080874340352
RPC[SL AUTOCTRL NOTIFY]sl_autoctrl_notify_21310118642752979103744
RPC[SL AUTOCTRL WINDOW]sl_autoctrl_window_21310118639079884484608
RPC[SL FAULT RESET]sl_fault_reset_action_21310118639074800988160
RPC[SL GET] parasl_get_21310118639076910723072
RPC[SL POWER ACT]sl_power_action_21310118639071638482944
RPC[SL SET] parasl_set_21310118639075765678080
RPC[SL SETPOINT ACT]sl_setpoint_action_21310118639073274261504
TemplateSL-SL2000C-21310118639081822253056
Thing Model[SL2000C]sl_21310118639068635361280
Thing Model[SL-21310-SL2000C资产] 区域聚合sl_asset_21310119677174411063303
Parameter Thing Model[SL2000C-PARA]sl_para_21310118639069788794880
Trigger[SL] 除湿机故障告警触发器sl_alarm_trigger_21310118639077997047808
Trigger[SL] 除湿机温湿度自控触发器sl_autoctrl_trigger_21310118639078986903552

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

FieldNameTypeUnitAddress/SourceDescription
return_tempreturn_tempint16BEFC03@31Source field: return_temp
return_humidityreturn_humidityint16BE%RHFC03@30Source field: return_humidity
temp_settemp_setint16BEFC03@37Source field: temp_set
humidity_sethumidity_setint16BE%RHFC03@1Source field: humidity_set
power_switchpower_switchderived(coil@176 bit0)FC01@176Source field: power_switch
unit_statusunit_statusderived(FC03@35 enum→text)FC03@35Source field: unit_status
compressor/cond_fan_1/cond_fan_2/four_way_valve/supply_fan/heat_valve/cool_valvecompressor/cond_fan_1/cond_fan_2/four_way_valve/supply_fan/heat_valve/cool_valveSource-definedFC01@40-46Source field: compressor/cond_fan_1/cond_fan_2/four_way_valve/supply_fan/heat_valve/cool_valve
fault_textfault_textSource-definedFC01@0-9Source field: fault_text
fault_high_pressfault_high_pressderived(coil@1)FC01@1Source field: fault_high_press
fault_low_pressfault_low_pressderived(coil@2)FC01@2Source field: fault_low_press
has_faulthas_faultderived(coil@27)FC01@27Source 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-9Source field: fault_*(supply_fan_oc/comp_oc/cond_fan1_oc/cond_fan2_oc/phase_seq/user_interlock/humidity_sensor/temp_sensor)

Parameter Definitions

ParameterNameTypeDefaultRangeUnitDescription
app_38pwron_delayuint16LE200msSource parameter: app_38
app_40period_heartuint32LE86400sSource parameter: app_40
app_70period_upuint32LE300sSource parameter: app_70
app_74period_readuint32LE60sSource parameter: app_74
app_110cov_tempint16BE0.5Source parameter: app_110
app_112cov_humidityint16BE2%RHSource parameter: app_112
app_150addr_modbusuint811-32Source parameter: app_150

Requirements Evidence

The following key facts were extracted from the requirements source; line references support verification.

CategorySource FactSource
Protocol/Data> ../input/SL2000C-TY3 .xlsrequirement/requirements.md:4
DeploymentDTU Model KC11 DIN RS-485requirement/requirements.md:22
DeploymentDTU Power Mode AC 85–270V KC11 KB-37requirement/requirements.md:23
Protocol/DataLoRaWAN Class Class C supportsrequirement/requirements.md:24
Protocol/DataPKO-21309-DEHUM +KC11requirement/requirements.md:25
Deployment> KC11 = → otaConfig.Battery=false Class C MT-EB-POWER KB-37requirement/requirements.md:27
Protocol/Data- Modbus-RTU 9600/8/N/1requirement/requirements.md:39
DeploymentDTU KC11 AC 85–270V KC11requirement/requirements.md:57
DeploymentPower-on to Stable Working 1s 1 EB pwron_delayrequirement/requirements.md:63
Protocol/DataInterface Type RS-485requirement/requirements.md:69
Protocol/DataBaud Rate 9600requirement/requirements.md:70
Protocol/DataProtocol Type modbus-rturequirement/requirements.md:97
Protocol/DataDevice Default Address 1 @6 1~32requirement/requirements.md:98
Protocol/Data01= / 05= / 03= / 06=requirement/requirements.md:99
Protocol/Data≤120 ≤8 / supports 1 FC16requirement/requirements.md:100
Protocol/DataModbus PLC = +1requirement/requirements.md:103
Unresolved> ⚠️ TBD#1 @30 / @31 「 (×10)」 01 「×10 」 FC03 int16 ÷10 Commissioning mt_debug_modbusrequirement/requirements.md:105
Protocol/Data> / FC03 /Get FC06requirement/requirements.md:147
Protocol/Data「 ≤120 ≤8 」 EBHelper edgeBus docs/zh/EB/requirement/requirements.md:151
Protocol/DataC FC03 @30 8 @30–37 @30 + @31 + @35 + @37requirement/requirements.md:157
Protocol/DataD FC03 @1 1 @1 @1requirement/requirements.md:158
Deploymentpower_switch — bit FC01@176 — 1 /0requirement/requirements.md:183
Deploymentpwron_delay app_38 uint16LE 200 ≥100 /RS485 5ms BUG-026 EB-DTU (KB-25)requirement/requirements.md:203
Protocol/DataModbus addr_modbus app_150 uint8 1 1–32 DTUrequirement/requirements.md:209

EdgeBus Code

The following is the current EdgeBus business code.

typescript
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

javascript
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

javascript
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

javascript
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

javascript
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

javascript
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

javascript
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

javascript
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

javascript
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

javascript
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

javascript
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

javascript
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

javascript
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

javascript
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 ModelFieldTypeUnitName
sl_21310power_switchnumberPower Switch
sl_21310unit_statusstringUnit Status
sl_21310return_tempnumberReturn Air Temperature
sl_21310return_humiditynumber%RHReturn Air Humidity
sl_21310temp_setnumberTemperature Setpoint
sl_21310humidity_setnumber%RHHumidity Setpoint
sl_21310compressornumberCompressor
sl_21310cond_fan_1numberCondenser Fan 1
sl_21310cond_fan_2numberCondenser Fan 2
sl_21310four_way_valvenumberFour-Way Valve
sl_21310supply_fannumberSupply Fan
sl_21310heat_valvenumberHeating Solenoid Valve
sl_21310cool_valvenumberCooling Solenoid Valve
sl_21310fault_high_pressnumberHigh-Pressure Fault
sl_21310fault_low_pressnumberLow-Pressure Fault
sl_21310fault_textstringFault Status
sl_21310has_faultnumberHas Fault
sl_21310manual_autonumberManual / Auto
sl_asset_21310on_power_switch_cntnumberZone Power Switch
sl_asset_21310zone_return_temp_avgnumberZone Return Air Temperature
sl_asset_21310zone_return_humidity_avgnumber%RHZone Return Air Humidity
sl_asset_21310zone_temp_set_avgnumberZone Temperature Setpoint
sl_asset_21310zone_humidity_set_avgnumber%RHZone Humidity Setpoint
sl_asset_21310on_compressor_cntnumberZone Compressor
sl_asset_21310on_cond_fan_1_cntnumberZone Condenser Fan 1
sl_asset_21310on_cond_fan_2_cntnumberZone Condenser Fan 2
sl_asset_21310on_four_way_valve_cntnumberZone Four-Way Valve
sl_asset_21310on_supply_fan_cntnumberZone Supply Fan
sl_asset_21310on_heat_valve_cntnumberZone Heating Solenoid Valve
sl_asset_21310on_cool_valve_cntnumberZone Cooling Solenoid Valve
sl_asset_21310any_fault_high_pressnumberZone High-Pressure Fault
sl_asset_21310any_fault_low_pressnumberZone Low-Pressure Fault
sl_asset_21310any_has_faultnumberZone Has Fault
sl_asset_21310online_countnumberZone Online Count
sl_para_21310period_upnumbersUpload Period
sl_para_21310period_readnumbersCollect Period
sl_para_21310cov_tempnumberTemp COV Threshold
sl_para_21310cov_humiditynumber%RHHumidity COV Threshold
sl_para_21310period_heartnumbersHeartbeat Period
sl_para_21310pwron_delaynumbermsPower-on Delay
sl_para_21310addr_modbusnumberModbus Address
sl_para_21310TimeOffsetnumberTime Offset
sl_para_21310snrnumberdBSNR
sl_para_21310rssinumberdBmRSSI

RPC Parameter Definitions

RPCParameterTypeDescriptionDefaultUnit
sl_autoctrl_config_21310auto_enabledbooleanAuto Control Enable
sl_autoctrl_config_21310hum_highnumberHumidity High (ON)%RH
sl_autoctrl_config_21310hum_lownumberHumidity Low (OFF)%RH
sl_autoctrl_config_21310notifynotice_groupNotice Groups
sl_autoctrl_notify_21310titlestringTitle
sl_autoctrl_notify_21310descstringDescription
sl_autoctrl_window_21310in_windownumberIn Window (1/0)
sl_fault_reset_action_21310resetbooleanFault Resetfalse
sl_power_action_21310power_switchbooleanPower Switch
sl_set_21310period_heartnumberHeartbeat Period (s)s
sl_set_21310period_upnumberUpload Period (s)s
sl_set_21310period_readnumberCollect Period (s)s
sl_set_21310cov_tempnumberTemp Change Threshold
sl_set_21310cov_humiditynumberHumidity Change Threshold%RH
sl_set_21310addr_modbusnumberModbus Address (1-32)
sl_set_21310pwron_delaynumberPower-on Delay (ms)ms
sl_setpoint_action_21310setpointvariantSetpointhumidity

Trigger Definitions

TriggerCondition SignalsDelay ExpressionRPC ActionsAbort Previous Timer
sl_alarm_trigger_213100alarmtrue
sl_autoctrl_trigger_213102000sl_power_action_21310, sl_autoctrl_notify_21310true

Third-Party Platform Subscription

MQTT Topic: /v32/{Organization Account}/tkl/up/telemetry/{eui}

json
{
  "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.