Skip to content

1. 物模型 ​

2.00.066 属性功能:服务端/共享属性默认不转发,仅发送勾选字段;过程属性(process_data)用于持久化计算状态。操作、脚本和模板行为见设备属性转发与过程数据。

物模型(Thing Model)是 ThinkLink 平台中用于定义设备功能和数据结构的核心模块。通过物模型,可以将来自 LoRaWAN 网络服务器(NS)或其他协议接入的原始数据解析为标准化的应用层数据(如遥测数据、属性信息),并支持在表格、图表或仪表盘中进行可视化展示。

物模型列表页面

1.1. 创建物模型 ​

新建一个物模型时,需分别填写名称与 ID Name。名称用于显示,建议根据设备类型或功能命名(例如:温湿度传感器_型号A);ID Name 用于标识模型。

创建完成后,可通过配置"解析脚本"实现对上行数据的解码与映射。

基础信息表单 ​

字段必填与默认行为
名称必填,模型的显示名称。
ID Name必填,独立于显示名称的标识字段;不能只填名称而遗漏它。
应用到必填,新增初值为“全部”;按模型要处理的对象选择。
标签、制造商、设备型号可选,用于归类和检索。
备注、变更说明可选;变更说明用于交代本次修改,方便与历史版本对照。

编辑器分为 UI配置、JSON配置和测试页签。除了 payload 解析脚本,UI 配置还包含展示字段、Modbus 映射、BACnet 字段。定义解析输出后,应为需要显示或转出到协议的数据分别配置这些字段;脚本存在并不代表映射已经完成。

1.2. 解析脚本说明 ​

解析脚本的作用是将外部平台发送至 ThinkLink 的原始数据转换为结构化的遥测数据(telemetry_data)和共享属性(shared_attrs),以便后续应用处理与展示。

1.2.1. 沙盒运行环境与限制 ​

解析脚本在 vm2 沙盒中执行,与 Node.js 主进程完全隔离。以下约束由运行时强制保证,脚本中违反这些规则会直接抛出异常或返回错误。

运行限制

限制项值说明
执行超时1000 ms脚本执行超过 1 秒立即中断,跳过本次解析。
eval❌ 禁用不能在脚本中动态执行字符串代码。
WebAssembly❌ 禁用不能加载或执行 .wasm 模块。
async / await❌ 不支持解析脚本是同步运行的,不支持异步操作。
require / import❌ 不支持不能引入任何 Node.js 内置模块或第三方包。
process、path、fs 等 Node 全局对象❌ 不可用沙盒不注入这些对象,调用会抛出 ReferenceError。
setTimeout / setInterval❌ 不可用沙盒中无定时器全局函数。
console❌ 不可用无法使用 console.log;调试请使用物模型日志功能。

Buffer 的可用方法限制

沙盒注入的 Buffer 是受限版本(SafeBuffer),仅开放以下静态方法:

允许的方法说明
Buffer.alloc(size)分配并清零的 Buffer
Buffer.from(...)从字符串、数组、ArrayBuffer 创建 Buffer
Buffer.isBuffer(obj)检查是否为 Buffer 实例
Buffer.byteLength(str, enc)计算字节长度
Buffer.compare(a, b)比较两个 Buffer
Buffer.concat(list, totalLen?)拼接多个 Buffer

Buffer.allocUnsafe()、Buffer.allocUnsafeSlow() 被明确屏蔽,调用会报错。Buffer 实例的读写方法(readUInt8、writeUInt16LE 等)正常可用。

沙盒可用全局变量

下表列出脚本中可直接使用的所有变量(即平台注入沙盒的全部内容):

变量来源说明
device平台注入,只读设备对象(vm.freeze),不可修改其属性。
msg平台注入本次上行的原始消息体。
noticeAttrs平台注入本次触发的属性变更标志。
thingModelId平台注入当前物模型 ID。
org_params平台注入,只读组织级环境变量(vm.freeze)。
subDevices平台注入,只读父设备的子设备缓存映射(vm.freeze)。
env平台注入,只读挂载插件声明的环境变量(高级功能)。
plugins平台注入,只读挂载的插件实例(高级功能)。
Buffer平台注入受限版 SafeBuffer(见上方说明)。
BufferTKL / PayloadParser / MSparser / Utils / TriggerHelper / RPCHelpertklHelper,只读平台工具库(vm.freeze),详见 tklHelper。

注意:device 及 tklHelper 各类均以 vm.freeze 冻结方式注入,脚本中对其属性赋值不会报错但不会生效,也不会影响平台内部状态。要向平台写回数据,必须通过脚本的返回值。

1.2.2. 输入参数 ​

脚本执行时,系统自动传入以下参数:

参数名类型说明
deviceObject消息所属设备对象(完整属性见下方 device 对象属性参考)。
msgObject来自 LoRaWAN 应用服务器(AS)的数据包原始信息。若为 MQTT 接入,则为 JSON 格式的消息体。
thingModelIdString当前物模型的唯一 ID,用于获取该设备对应物模型的历史遥测数据。
noticeAttrsObject标识哪些属性发生变化并触发通知事件,用于条件判断。
org_paramsObject当前组织(租户)的"环境变量",由 系统管理 → 服务器配置 → 组织参数 维护。详见服务器配置 §1.4。

函数签名示例:

javascript
function payload_parser(device, msg, thingModelId, noticeAttrs, org_params) {
    // ...
}

device 对象属性参考

属性类型说明
euistring设备 EUI(16位十六进制字符串,全局唯一主键)。
namestring设备名称。
device_typestring设备类型:"NORMAL"(普通设备)、"SUB_DEVICE"(子设备)、"VIRTUAL"(资产/虚拟设备)。
data_fromstring数据来源:"LoRaWAN"(走 NS 上行)或 "OTHER"(直接 MQTT 接入)。
parentstring | null父设备 EUI(仅 SUB_DEVICE 类型有值)。
onlineboolean设备当前是否在线。
active_timestring最近一次上行时间(ISO 8601 格式)。
update_timestring设备信息最近更新时间(ISO 8601 格式)。
heart_periodnumber心跳检测周期(秒),平台据此判断在线/离线状态。
shared_attrsobject平台与设备双向同步的共享属性,如设备上报的配置参数、固件信息等。
server_attrsobject仅存储在平台侧的服务端属性,设备不可读,常用于存储告警阈值、联动 EUI、下行记录等。
mac_attrsobjectLoRaWAN MAC 层属性(频段、ADR 等),非 LoRaWAN 设备为空对象。
telemetry_dataobject各物模型的最新遥测快照,以 thingModelId 为键。例如:device.telemetry_data[thingModelId]。
thing_modelstring[]已挂载的物模型 ID 数组。
rpcstring[]已挂载的 RPC 模型 ID 数组。
triggerstring[]已挂载的触发模型 ID 数组。
tagsstring[]设备标签数组,用于筛选和分组。
tenant_codestring租户标识符。
real_time_storageboolean是否开启实时历史数据存储。
location_idstring | null绑定的地理位置 ID。
remarkstring | null备注说明。

注意:device 以 vm.freeze 冻结方式注入,脚本中对其属性赋值不会生效。访问不存在的属性返回 undefined,建议使用可选链(?.)防止报错。

提示:若您已有基于 ChirpStack 编写的解析脚本,可直接选择 ChirpStack 兼容模式。ThinkLink 已完成接口适配,只需复制原代码即可无缝运行。

1.2.2. 遥测模型参考代码 ​

以下是一个典型的 LoRaWAN 温湿度传感器的解析脚本示例,适用于端口为 11、固定长度为 15 字节的二进制负载。配置参数(COV 阈值、周期等)通过端口 214 发送和返回,物模型将其解析到共享属性中。

javascript
let    payload = Buffer.from(msg?.userdata?.payload, "base64");
    let    port=msg?.userdata?.port || null;
    function parseSharedAttrs(payload) {
        if (port!=214||payload[0]!=0x2F) { return null}
        let shared_attrs = {};
        if (payload.length<5) { return null}
        shared_attrs.content = payload.toString('hex');
        let size=payload.length-4
        let regAddress=payload[2]
        for (let i=0; i<size; i++) {
            regAddress=payload[2]+i
            switch (regAddress) {
                case  58:
                    if  ( size<(2+i) ) { break }
                    shared_attrs.period_data = payload.readUInt16LE(4+i)
                    break;
                case 142:
                    if  ( size<(2+i) ) { break }
                    shared_attrs.period_measure = payload.readUInt16LE(4+i)
                    break;
                case 144:
                    if  ( size<(1+i) ) { break }
                    shared_attrs.cov_temperatrue = payload.readUInt8(4+i)*0.1
                    break;
                case 145:
                    if  ( size<(1+i) ) { break }
                    shared_attrs.cov_humidity = payload.readUInt8(4+i)*0.1
                    break;
                default: break
            }
        }
        if (Object.keys(shared_attrs).length == 0) {
            return null
        }
        return shared_attrs;
    }
    function parseTelemetry(payload){
        if (port!=11||payload[0]!=0x21||payload[1]!=0x07||payload[2]!=0x03||payload.length !=15){
            return null
        }
        let telemetry_data = {};
        telemetry_data.period_data =payload.readUInt16LE(5)
        telemetry_data.status ="normal"
        if ((payload[7]&0x01)!=0){  telemetry_data.status ="fault" }
        telemetry_data.temperatrue=Number(((payload.readUInt16LE(8)-1000)/10.00).toFixed(2))
        telemetry_data.humidity=Number((payload.readUInt16LE(10)/10.0).toFixed(2))
        let vbat=payload.readUInt8(12)
        telemetry_data.vbat=Number(((vbat*1.6)/254 +2.0).toFixed(2))
        telemetry_data.rssi=msg.gwrx[0].rssi
        telemetry_data.snr=msg.gwrx[0].lsnr
        return telemetry_data
    }
    let tdata=parseTelemetry(payload)
    let sdata=parseSharedAttrs(payload)
    if (tdata?.period_data!=null){
        if (sdata===null) {sdata={}}
        sdata.period_data = tdata.period_data
    }
    return {
            sub_device:null,
            telemetry_data: tdata,
            server_attrs: null,
            shared_attrs: sdata
    }

1.2.3. 关键参数详解 ​

1.2.3.1. device – 设备对象 ​

表示接收到消息的设备实例,包含其所有已知状态和属性:

  • device.telemetry_data[thingModelId]:获取该设备下指定物模型的最新一包遥测数据。
  • 支持多物模型查询:若设备挂载多个物模型,可通过不同 thingModelId 分别访问各自的历史数据。

1.2.3.2. msg – 原始消息数据 ​

ThinkLink 接收的 LoRaWAN AS 接口标准数据格式如下所示:

json
{
  "if": "loraWAN",
  "gwrx": [
    {
      "eui": "5a53012501030011",
      "chan": 0,
      "lsnr": 13.2,
      "rfch": 1,
      "rssi": -92,
      "time": "2025-09-17T03:51:04.8516751Z",
      "tmms": 0,
      "tmst": 2845948222,
      "ftime": 0
    }
  ],
  "type": "data",
  "token": 14892,
  "moteTx": {
    "codr": "4/5",
    "datr": "SF7BW125",
    "freq": 471.5,
    "modu": "LORA",
    "macAck": "",
    "macCmd": ""
  },
  "geoInfo": {
    "type": "gw:wifi",
    "accuracy": 50,
    "altitude": 0,
    "latitude": 34.19925,
    "longitude": 108.8659
  },
  "moteeui": "6353012af1090498",
  "version": "3.0",
  "userdata": {
    "port": 11,
    "class": "ClassA",
    "seqno": 18654,
    "payload": "IQcDDG4PAADWBIsC34IG",
    "confirmed": false
  }
}

常用字段提取方式:

  • 负载数据:msg.userdata.payload(Base64 编码)
  • 端口号:msg.userdata.port
  • 信号强度(RSSI):msg.gwrx[0].rssi
  • 信噪比(SNR):msg.gwrx[0].lsnr
  • 时间戳:msg.gwrx[0].time

对于非 LoRaWAN 数据源(如 MQTT 上报的 JSON 数据),可直接按 JSON 层级访问对应字段。

1.2.3.3. thingModelId – 物模型标识符 ​

用于访问设备关联的特定物模型的历史数据。例如:

javascript
let lastData = device.telemetry_data['temp_humi_v1'];

1.2.3.4. noticeAttrs – 变更通知标志 ​

此对象指示本次消息是否因某些属性变化而触发:

字段类型说明
server_attrsBoolean若为 true,表示服务端属性发生变更
shared_attrsBoolean若为 true,表示共享属性发生变更
telemetry_dataBoolean若为 true,表示遥测数据更新

可用于控制是否需要执行特定逻辑(如仅在遥测更新时写入数据库)。

1.2.3.5. 返回值格式 ​

解析脚本必须返回符合以下规范的 key-value 结构:

javascript
return {
    sub_device: null,
    telemetry_data: {    // 解析后的遥测数据,无则设为 null
        temperature: 23.5,
        humidity: 60.2,
        rssi: -85
    },
    server_attrs: null,  // 服务端专用属性,一般保持 null
    shared_attrs: {      // 共享属性(可选),常用于保存周期、地址等配置
        heartbeat_interval: 30
    }
};

1.2.3.6. 返回值字段定义 ​

字段名默认值说明
shared_attrsnull共享属性,存储在设备层面的持久化数据,可在系统内被其他模块调用(如联动规则、告警判断等)。
sub_devicenull子设备标识。若主设备(如 DTU)通过 RS-485 或 M-Bus 抄读子设备数据,该字段应填写子设备的通信地址(如 485 地址)。ThinkLink 将据此在设备管理系统中虚拟生成一个子设备,并自动分配唯一 EUI。
telemetry_datanull遥测数据,用于上报设备的实时运行数据和应用层解析后的结果,常用于仪表板、趋势图、历史数据查询等场景。
server_attrsnull服务端属性,存放不需存储于设备本地的元数据或配置信息,仅保存在 ThinkLink 服务器端,适用于高级功能扩展。

说明:各字段均可为 null(表示本次上报不包含该类数据),但应根据业务需求合理填写,以确保相关功能正常运作。数据类型须为合法的字符串、数值、布尔值或对象。

1.3. 展示字段 ​

通过"展示字段"配置,可以定义在应用层数据展示界面(如表格、卡片、仪表盘等)中呈现的具体数据项。

  • 遥测字段:必须从设备上报的 telemetry_data 数据项中选取,且字段名称需完全一致。
  • 序号:用于控制该字段在表格或卡片视图中的显示顺序。
  • 别名:在前端界面中显示的字段名称,提升可读性。
  • 单位:该字段数值对应的单位(如 °C、%、m³ 等)。
  • 类型:根据实际数据类型选择,建议数值类数据选择 number 类型以支持格式化显示。
  • 图标:可选配图标用于可视化展示。支持 SVG 格式,推荐从 阿里图标矢量库 下载并嵌入 SVG 代码。

提示:正确配置展示字段后,系统将自动从物模型解析后的数据中提取对应字段,并在应用端进行可视化渲染。

1.4. BACnet 字段配置 ​

在 ThinkLink 系统中,通过 BACnet 协议对外提供设备数据时,需对相关数据字段进行映射与配置。以下为新增 BACnet 字段时的各项参数说明:

  • field_name
    遥测来源填物模型字段标识符;2.00.085 起属性来源填 _online、status.value 等属性路径。

  • object_name_suffix
    名称后缀。ThinkLink 系统将使用设备的 EUI 地址与此后缀拼接,生成唯一的 object_name,确保每个 BACnet 对象在网络中的唯一性。

  • object_type
    选择该数据项对应的 BACnet 对象类型,例如模拟输入(Analog Input)、二进制输出(Binary Output)等,需根据实际数据性质进行选择。

  • unit
    配置该数据项的计量单位,确保在 BACnet 客户端(如 BAS 或 YABE)中可正确显示物理意义。

  • cov_increment
    变化值触发上传阈值(Change of Value Increment)。当数据变化幅度超过此设定值时,系统将主动发送 COV(Change of Value)通知,适用于支持 COV 功能的场景。

  • default_value
    数据项的初始值,在设备未上报数据前,系统将使用该默认值进行展示和响应。

  • RPC
    关联该字段对应的远程控制功能。若希望对此字段执行写操作或下发指令,需预先在 RPC 模型中完成配置,方可在此处选择对应 RPC 功能。

1.4.1. 使用 YABE 查看 BACnet 数据 ​

YABE(Yet Another BACnet Explorer)是一款常用的 BACnet 设备调试与浏览工具。配置完成后,可通过 YABE 连接 ThinkLink 系统,发现并查看已发布的 BACnet 设备及其对象属性,验证数据是否正常发布。

1.4.2. 生成 BACnet 点表 ​

若用户的 BMS(楼宇管理系统)需要正式部署,通常需提供标准的 BACnet 点表文件,以便于系统集成与调试。

  1. 进入运维管理 → BACnet 模块。
  2. 点击增量生成按钮。
  3. 在弹出的窗口中选择目标物模型。
  4. 确认后,为绑定所选物模型且开启 BACnet 的设备生成对应字段点表。

生成后的点表包含:对象类型、对象实例号、对象名称、属性描述、数据类型、单位、读写权限等关键信息,可用于交付给第三方系统进行对接。

1.5. 关联物模型 ​

  1. 导航至运维管理 → 设备管理,在设备列表中找到目标设备。
  2. 点击该设备的"详情"按钮,进入设备详细信息页面。
  3. 切换至物模型选项卡,点击"新增"按钮。
  4. 在弹出的列表中选择已创建的物模型,确认后即可完成物模型与设备的关联。

注意:如需为多个设备批量关联相同的物模型,建议使用模板功能进行高效操作,提升配置效率。

1.5.1. 查看应用数据 ​

  1. 进入左侧菜单栏的应用数据页面。
  2. 在页面顶部选择相应的物模型,系统将自动加载与该物模型绑定的所有设备数据。
  3. 当设备有上行数据时,系统通过物模型完成解析,您可在此查看到解析后的应用层数据。
  4. 点击历史数据,可切换至历史数据视图,以曲线图和数据表格两种形式查看指定时间段内的数据变化趋势与明细。

1.5.2. 通过设备属性查看数据 ​

  • 当设备上报共享属性数据后:
    • 进入运维管理 → 设备管理,选择目标设备并点击"详情"。
    • 切换至共享属性选项卡,可实时查看当前属性值及其最新的更新时间。
  • 当设备上报遥测数据后:
    • 同样进入目标设备的"详情"页面。
    • 切换至遥测数据选项卡,系统将展示最近一次更新的遥测信息,便于快速掌握设备运行状态。

历史版本与还原 ​

每次保存物模型,平台都会记录一个版本快照。在编辑器中点击历史版本即可浏览早期版本。每条记录提供:

  • 详情 —— 查看该快照内容(并可与相邻版本对比)。
  • 还原 —— 将当前值回滚到该快照。还原会生成一个新的版本记录(因此历史不会丢失),并将列表刷新到第一页;确认弹窗会提示「还原会产生一个新版本」。

RPC 模型、物模型、触发模型与转发器脚本编辑器均提供相同的「历史版本 / 还原」操作。

不依赖设备测试解析脚本 ​

当你只想确认解析脚本能正确解码输入——例如在配置 NFC 模板 时——测试操作支持 skipDeviceCheck 标志。当 skipDeviceCheck = true 时,平台不再按 EUI 查找真实设备,而是注入一个合成占位设备,即使没有匹配的设备也能完成测试。常规测试请不要设置该标志,此时设备必须存在,否则平台返回 Device not found。

BACnet 来源配置补充(2.00.085) ​

BACnet 字段增加自由字符串 data_from,建议 telemetry_data(默认)、server_attrs、shared_attrs。旧配置保持遥测语义;属性来源无需物模型,对象保存时 thing_model=""。配置以“来源 + 字段名”为组合 key,兼容旧 key,不同来源同名字段可以共存。物模型字段 unit 映射到对象 units。生成对象只覆盖绑定本物模型且开启 BACnet 的设备。路径、空值回退和只读在线点位见BACnet 属性来源。

脚本上下文 online 不可直接等同于检测器 server_attrs._online;检测默认关闭,关闭后属性可能为历史值,历史消息按接收时间刷新。详见三种状态口径。