MT-Gateway 平台能力对接文档
能力简介
门思科技网关统一模型:gw_heart 心跳物模型(解析网关经 NMS 上行的 handle/固件/IP/连接状态)+ mt_gw_his_get(历史数据查询)+ mt_gw_wifi_list_get(周边 WiFi 扫描)+ mt_gw_locate(GPS优先/腾讯WiFi定位兜底)+ mt_gw_geo_set_config(配腾讯LBS key)。2026-06-16 新增 WiFi 定位闭环:转发器把 nms/up/gw 的 wifiList 回灌 AS → mt_gw_wifi 物模型解析落 shared_attrs.wifi_list → mt_gw_locate_on_wifi 触发器自动派发 mt_gw_locate(绕行 BUG-048)。网关在平台以 device_type:VIRTUAL(资产)登记,eui=网关真实 eui。
| 项目 | 值 |
|---|---|
| 项目名称 | mt-gateway |
| 厂商 | 门思科技 (ManThink) |
| 型号 | MT-Gateway |
| 业务代码 | Not applicable |
| 对接模式 | lorawan-native |
| 协议 | gateway-raw |
模型清单
可使用下列名称或 ID 在 ThinkLink 中搜索对应模型。
| 模型类型 | 名称 | id_name | 平台模型 ID |
|---|---|---|---|
| RPC | [GW SET] geo config | mt_gw_geo_set_config | 119427355042017280 |
| RPC | [GW ACT] his get | mt_gw_his_get | 120041209778302976 |
| RPC | [GW ACT] history get | mt_gw_history_get | 120039711216398337 |
| RPC | [GW ACT] locate | mt_gw_locate | 119427355042017281 |
| RPC | [GW ACT] wifi list get | mt_gw_wifi_list_get | 119403377514672128 |
| Template | MT-Gateway | 120034832892719104 | |
| Thing Model | [GW HEART] | mt_gw_heart | 95121332907282437 |
| Thing Model | [GW WIFI] scan result | mt_gw_wifi | 119474792070803456 |
| Trigger | [GW LOCATE] on wifi scan | mt_gw_locate_on_wifi | 119474793920491520 |
mt_gw_his_get 是当前模板使用的历史查询 RPC;mt_gw_history_get 仅用于旧绑定识别和迁移。
运维 Skill
部署或调试历史查询、WiFi 扫描、腾讯定位、转发器与定时任务时,使用 create-gateway。该 Skill 会按链路逐段验证 NMS 上行、AS 回灌、触发器和属性读回。
产品特点与适用范围
本项目面向“网关 NMS 心跳物模型 + 运维 RPC + WiFi 定位闭环(一套网关模型)”场景。能力边界、字段和可部署模型均以当前 descriptor、requirements 与模型源文件为准。
部署与采集信息
| DTU | Not applicable | | 供电 | ac | | LoRaWAN Class | C | | 接口 | LoRaWAN | | 波特率 | Not applicable |
遥测与寄存器定义
| 字段 | 名称 | 类型 | 单位 | 地址/来源 | 说明 |
|---|---|---|---|---|---|
| handle | 网关 deviceHandle | string | — | 网关 deviceHandle | |
| hwVersion | 硬件版本 | string | — | 硬件版本 | |
| fwVersion | 固件版本 | string | — | 固件版本 | |
| netType | 回程网络类型(如 mteth0) | string | — | 回程网络类型(如 mteth0) | |
| validIP | 当前有效 IP | string | — | 当前有效 IP | |
| nsConnect | NS 连接状态 | boolean | — | NS 连接状态 | |
| nmsConnect | NMS 连接状态 | boolean | — | NMS 连接状态 | |
| pmcConnect1 | PMC 连接状态 | boolean | — | PMC 连接状态 | |
| wifi_ap_count | 本次扫到的周边 WiFi AP 数(标量遥测,触发 mt_gw_locate + 观测扫描是否成功) | number | — | 本次扫到的周边 WiFi AP 数(标量遥测,触发 mt_gw_locate + 观测扫描是否成功) |
参数定义
无项目专用参数定义。
Requirements 证据
以下关键事实由 requirements 源文件提取;原始行号用于复核。
| 类别 | 来源事实 | 来源 |
|---|---|---|
| 协议/数据 | - 心跳物模型 mt_gw_heart:解析网关经 NMS 上行的心跳帧(deviceHandle/固件/IP/ns/nms/pmc 连接状态)→ 遥测 + shared_attrs。 | requirement/requirements.md:15 |
| 协议/数据 | → [物模型 mt_gw_wifi (CUSTOMER)] 解析 AS 帧 | requirement/requirements.md:41 |
| 协议/数据 | · telemetry_data.wifi_ap_count = 本帧扫到的 AP 数(标量) | requirement/requirements.md:42 |
| 协议/数据 | → [触发器 mt_gw_locate_on_wifi] 绑 mt_gw_wifi:本帧 wifi_ap_count>0 → dispatch mt_gw_locate | requirement/requirements.md:44 |
| 协议/数据 | shared_attrs.gw_latitude/gw_longitude/gw_accuracy/gw_address/gw_city/geo_source/geo_fetched_at | requirement/requirements.md:46 |
| 协议/数据 | | 物模型 | thinklink/thingModel/mt_gw_wifi.{js,json} | CUSTOMER 解析 AS 回灌帧 → wifi_ap_count(遥测) + wifi_list(shared_attrs) | 119474792070803456 | | requirement/requirements.md:53 |
| 协议/数据 | | 转发器 | forwarder/mt-gw-wifilist-to-as.js | 订阅 nms/up/gw/+ → 滤 wifiList → 归一 AP → 回灌 AS 上行(paste-ready, 无 import) | —(脚本,无模型 id) | | requirement/requirements.md:55 |
| 部署 | | 定时任务 | descriptor.yaml scheduled_task(部署期建) | 每 24h 下发 mt_gw_wifi_list_get 启动闭环 | —(平台任务) | | requirement/requirements.md:56 |
| 协议/数据 | | gw_address / gw_city | shared_attrs | mt_gw_locate 写:地址 / 城市 | | requirement/requirements.md:68 |
| 协议/数据 | - 解析器边界:mt_gw_wifi parser 保持同步且不发起 HTTP 请求;HTTP 定位只由 mt_gw_locate RPC 执行。 | requirement/requirements.md:73 |
| 协议/数据 | - 触发器判别用本帧业务字段 wifi_ap_count(KB-30:禁用 _modelName,避免生产环境静默失效)。 | requirement/requirements.md:75 |
| 协议/数据 | - 双向不污染:heart(NMS, 有 msg.data)与 wifi(AS, 有 msg.userdata.payload)两类帧各自被对应 parser 处理、对方 parser 守卫返回 null。 | requirement/requirements.md:78 |
| 未确认事项 | - TBD-1(nms/up/gw 回复 AP 字段名):data 内各 AP 的字段名(ssid/bssid/rssi vs ESSID/Address/Signal 等)以真机一帧抓包为准。转发器已对常见命名容错归一(bssid|mac|Address、rssi|signal、ssid|essid),抓包后核对一次。 | requirement/requirements.md:84 |
| 未确认事项 | - TBD-2(转发器 broker 类型):源 nms/up/gw 与目标 as/up/data 的 broker 类型(from/to emqx type)部署期确认;脚本本身 broker 无关。 | requirement/requirements.md:85 |
| 未确认事项 | - TBD-3(设备绑定):网关设备需在 mt_gw_heart 之外追加绑定 mt_gw_wifi,并关联 mt_gw_locate_on_wifi 触发器,才能解析 AS 回灌帧并派发定位。 | requirement/requirements.md:86 |
| 协议/数据 | 1. 推送 4 产物到 PUBLIC(SYSADMIN,/thinklink/system/... 路由,带 _commitMessage):mt_gw_wifi(物模型) + mt_gw_locate_on_wifi(触发器);转发器经转发器配置接口建。 | requirement/requirements.md:92 |
| 未确认事项 | 3. 建转发器:from /v32/{tenant}/nms/up/gw/+ → to /v32/{tenant}/as/up/data/{eui},脚本 = forwarder/mt-gw-wifilist-to-as.js(broker 类型见 TBD-2)。 | requirement/requirements.md:94 |
| 未确认事项 | 6. 验证:等一轮(或手动触发一次扫描)→ 回读 shared_attrs.wifi_list 非空、telemetry wifi_ap_count>0、shared_attrs.gw_latitude/gw_longitude 落库;抓一帧 nms/up/gw 核对 TBD-1。 | requirement/requirements.md:97 |
| 协议/数据 | | 2026-06-16 | wifi 数据:AP 列表进 shared_attrs,额外加标量遥测 wifi_ap_count | 触发器须 gate 在本帧遥测(device.telemetry_data[id])才时序新鲜+合 KB-30;AP 列表仍只在 shared_attrs 不污染历史。用户"不入 telemetry"本意(别让冗长列表入历史)不违背 | mt_gw_wifi | 再讨论 | | requirement/requirements.md:107 |
| 部署 | | 2026-06-16 | 扫描周期 = 每 24h(cron_task);非上电检测 | 网关近静止、低频定位即可,最省;无需上电事件判定状态 | scheduled_task | 再讨论 | | requirement/requirements.md:108 |
| 协议/数据 | | 2026-06-16 | 修复 complete-js-local.sh glob over-match(*/thinklink/*.js→收紧到 thingModel/rpc/trigger 子目录) | 工作区根目录名为 thinklink/,旧 glob 误匹配工作区所有 .js(含 paste-ready 的 forwarder/),把无 import 的转发器误报 | scripts/complete-js-local.sh | 修复 | | requirement/requirements.md:109 |
物模型与 RPC 代码
RPC: [GW SET] geo config
id_name: mt_gw_geo_set_config · ID: 119427355042017280
import {BufferTKL, RPCHelper, Utils} from "#tklHelper";
import {Buffer} from "buffer";
// 配置网关定位服务(腾讯位置服务)的 apikey 与 base_url,写入网关设备 server_attrs。
// 供 mt_gw_locate 调用腾讯 WiFi 定位 API 时读取(key 必填;base_url 默认 https://apis.map.qq.com)。
// 腾讯定位 API: POST {base_url}/ws/location/v1/network body {key, device_id, wifiinfo:[{mac,rssi}]}
// 文档 https://lbs.qq.com/service/webService/webServiceGuide/position/location
function rpc_script({ device, params, alarms, logger, org_params }) {
const sa = {};
if (params.tencent_lbs_key != null) sa.tencent_lbs_key = params.tencent_lbs_key;
if (params.tencent_lbs_base_url != null) sa.tencent_lbs_base_url = params.tencent_lbs_base_url;
if (Object.keys(sa).length === 0) return null; // 无有效配置项不下发
return [{ sleepTimeMs: 0, type: "modifyAttrs", dnMsg: { server_attrs: sa } }];
}
rpc_script({
device: { eui: "5a53012a000002e9", server_attrs: {} },
params: { tencent_lbs_key: "TEST_KEY", tencent_lbs_base_url: "https://apis.map.qq.com" },
logger: console,
org_params: {}
})RPC: [GW ACT] his get
id_name: mt_gw_his_get · ID: 120041209778302976
import {BufferTKL, RPCHelper, Utils} from "#tklHelper";
import {Buffer} from "buffer";
function rpc_script({ device, params, alarms, logger, org_params }) {
logger.info("read history")
if (params.start==undefined||params.end==undefined) return null
if (params.start.length<=8 || params.end.length<=8) return null
let whereInfo = `pgsql://creat >= '${params.start}' AND creat <= '${params.end}'`;
let msg={
type:"nsDnGwc",
sleepTimeMs: 0,
target:device.eui,
dnMsg: {
version: "2",
EUI: device.eui,
token: new Date().getTime(),
action:"gwHis",
mode:"rxdataRead",
data: {
whereInfo: whereInfo,
gwToken: Date.now()%(0xFFFF),
intervalms:10
}
}
}
return [msg]
}
if (process.env.TKL_SELF_TEST === "1") {
rpc_script({
device: { eui: "5a53012a000002e9" },
params: {
start: "2026-07-29 00:00:00",
end: "2026-07-29 01:00:00"
},
logger: console,
org_params: {}
})
}RPC: [GW ACT] locate
id_name: mt_gw_locate · ID: 119427355042017281
import {BufferTKL, RPCHelper, Utils} from "#tklHelper";
import {Buffer} from "buffer";
// 网关定位(输出位置信息到 shared_attrs)。来源优先级:① GPS(网关 GPS 模块直接给经纬度,最准);
// ② 取不到 GPS 时用 WiFi——把 wifiList 扫到的 AP(bssid/rssi)送腾讯位置服务 WiFi 定位换经纬度。
// 腾讯 API: POST {base_url}/ws/location/v1/network body {key, device_id, wifiinfo:[{mac,rssi}]}
// → {status:0, result:{location:{latitude,longitude,accuracy}, address, ad_info:{city}}}
// 文档 https://lbs.qq.com/service/webService/webServiceGuide/position/location
// key / base_url 经 mt_gw_geo_set_config 写入 server_attrs(base_url 默认 https://apis.map.qq.com)。
//
// 数据来源(按「对应方式」就位即可,二者皆从 shared_attrs 读,缺则用入参兜底测试):
// GPS ← device.shared_attrs.gps = {lat, lon, accuracy?, valid?}(网关 GPS 经 DMS geoGet 上报落库后)
// WiFi ← device.shared_attrs.wifi_list = [{bssid|mac, rssi, essid?}](wifiList 响应落库后)
async function rpc_script({ device, params, alarms, logger, org_params }) {
const sa = device?.server_attrs || {};
const sh = device?.shared_attrs || {};
const num = (v) => (v == null || isNaN(Number(v)) ? null : Number(v));
// ── ① GPS 优先 ──────────────────────────────────────────────
const gps = params?.gps || sh.gps || null;
const gLat = gps ? num(gps.lat != null ? gps.lat : gps.latitude) : null;
const gLon = gps ? num(gps.lon != null ? gps.lon : gps.longitude) : null;
const gpsValid = gps && (gps.valid !== false) && gLat != null && gLon != null && !(gLat === 0 && gLon === 0);
if (gpsValid) {
return [{ sleepTimeMs: 0, type: "modifyAttrs", dnMsg: { shared_attrs: {
gw_latitude: gLat, gw_longitude: gLon,
gw_accuracy: num(gps.accuracy) != null ? num(gps.accuracy) : 1,
gw_address: null, gw_city: null,
geo_source: "gps", geo_fetched_at: new Date().toISOString()
} } }];
}
// ── ② WiFi 兜底(腾讯 WiFi 定位)─────────────────────────────
const key = params?.tencent_lbs_key || sa.tencent_lbs_key;
if (!key) { logger && logger.systemWarn && logger.systemWarn("mt_gw_locate: no GPS fix and missing tencent_lbs_key"); return null; }
const baseUrl = (params?.tencent_lbs_base_url || sa.tencent_lbs_base_url || "https://apis.map.qq.com").replace(/\/+$/, "");
let wifiinfo = Array.isArray(params?.wifiinfo) ? params.wifiinfo : null;
if (!wifiinfo) {
const wl = sh.wifi_list;
if (Array.isArray(wl)) wifiinfo = wl.map((a) => ({ mac: a.mac || a.bssid, rssi: a.rssi }));
}
wifiinfo = (wifiinfo || []).map((a) => ({ mac: String(a.mac), rssi: num(a.rssi) })).filter((a) => a.mac && a.mac !== "null");
if (!wifiinfo.length) { logger && logger.systemWarn && logger.systemWarn("mt_gw_locate: no GPS and no wifi list"); return null; }
const body = { key, device_id: device.eui, wifiinfo };
let resp;
try {
resp = await axios.post(`${baseUrl}/ws/location/v1/network`, body, { headers: { "Content-Type": "application/json" } });
} catch (e) {
logger && logger.systemError && logger.systemError(`mt_gw_locate tencent http error: ${e && e.message}`);
return null; // 取数失败/超时 → 不写脏数据
}
const d = resp && resp.data;
if (!d || d.status !== 0 || !d.result || !d.result.location) {
logger && logger.systemWarn && logger.systemWarn(`mt_gw_locate tencent status=${d && d.status} msg=${d && d.message}`);
return null;
}
const loc = d.result.location;
const adi = d.result.ad_info || {};
return [{ sleepTimeMs: 0, type: "modifyAttrs", dnMsg: { shared_attrs: {
gw_latitude: num(loc.latitude), gw_longitude: num(loc.longitude), gw_accuracy: num(loc.accuracy),
gw_address: d.result.address != null ? d.result.address : null,
gw_city: adi.city != null ? adi.city : null,
geo_source: "tencent_wifi", geo_fetched_at: new Date().toISOString()
} } }];
}
// 本地测试:无 GPS、无 key、无 wifi → 早返回 null(不触达 axios,node 可直接跑通)
rpc_script({
device: { eui: "5a53012a000002e9", server_attrs: {}, shared_attrs: {} },
params: {},
logger: console,
org_params: {}
})RPC: [GW ACT] wifi list get
id_name: mt_gw_wifi_list_get · ID: 119403377514672128
import {BufferTKL, RPCHelper, Utils} from "#tklHelper";
import {Buffer} from "buffer";
// 获取网关周围的 WiFi 列表(SSID/BSSID/RSSI)。
// 走 NMS 下行通道:平台据 type:"nmsDnGw" 把 dnMsg 原样发到
// /v32/<tenant>/nms/dn/gw/<eui> (NMS gateway downlink topic)
// 网关执行 `iwlist <wlan0> scan`,结果异步回到上行 topic
// /v32/<tenant>/nms/up/gw/<eui> 并用 token 回显匹配本次请求。
// 注意:①网关需启用 WiFi 接口(boardInfo.Wifi0.Enable)否则 data 返回 "";
// ②扫描底层超时 30s,响应可能延迟数秒到数十秒。
function rpc_script({ device, params, alarms, logger, org_params }) {
logger.info("wifiList scan")
let msg = {
type: "nmsDnGw",
sleepTimeMs: 0,
target: device.eui,
dnMsg: {
version: "3.0",
eui: device.eui,
action: "wifiList",
token: Date.now(), // 任意 uint64,原样回显在响应里用于匹配请求
mode: "read", // wifiList 不校验 mode/data,填上更规范
data: null
}
}
return [msg]
}
rpc_script({
device: { eui: "5a53012501030066" },
params: {},
logger: console,
org_params: {}
})Thing Model: [GW HEART]
id_name: mt_gw_heart · ID: 95121332907282437
import {Buffer} from "buffer";
import {BufferTKL, PayloadParser, RPCHelper, Utils} from "#tklHelper";
// 网关心跳物模型 [GW HEART] —— 平台 id_name: mt_gw_heart(与线上一致)。
// 解析网关经 NMS 上行的心跳帧(msg.data:deviceHandle/hwVersion/fwVersion/netType/
// validIP/nsConnect/nmsConnect/pmcConnect1),写入遥测 + shared_attrs。
// 心跳间隔 > 5min 时,顺带触发 mt_gw_his_get 补拉断档期间的历史数据。
// 配套 RPC(同一套网关模型):mt_gw_his_get(历史查询)、mt_gw_wifi_list_get(周边 WiFi 扫描)。
function payload_parser(device, msg, thingModelId, noticeAttrs, org_params) {
if (msg?.data == undefined) return null;
let data = msg.data;
let pdata = {};
let changed = false;
let lasttime = device.shared_attrs?.updatems;
let action = [];
if (lasttime != undefined) {
const diff = Date.now() - lasttime;
if (diff > 300000) {
action.push({
method: "mt_gw_his_get",
params: {
start: device.shared_attrs?.update_time,
end: new Date().toLocaleTimeString()
},
});
}
}
pdata.updatems = Date.now();
pdata.handle = data.deviceHandle;
pdata.hwVersion = data.hwVersion;
pdata.fwVersion = data.fwVersion;
pdata.netType = data.netType;
pdata.validIP = data.validIP;
pdata.nsConnect = data.nsConnect;
pdata.nmsConnect = data.nmsConnect;
pdata.pmcConnect1 = data.pmcConnect1;
if (pdata.handle !== device.shared_attrs?.handle) changed = true;
if (pdata.hwVersion !== data.hwVersion) changed = true;
if (pdata.fwVersion !== data.fwVersion) changed = true;
if (pdata.netType !== data.netType) changed = true;
if (pdata.validIP !== data.validIP) changed = true;
if (pdata.nsConnect !== data.nsConnect) changed = true;
if (pdata.nmsConnect !== data.nmsConnect) changed = true;
if (pdata.pmcConnect1 !== data.pmcConnect1) changed = true;
let result = {
telemetry_data: changed ? pdata : null,
server_attrs: null,
shared_attrs: pdata,
action: action,
};
// ── rpcHook (设备对接通用规则) ──
if (typeof RPCHelper.buildRpcHookAction === "function") {
const __a = RPCHelper.buildRpcHookAction({ device, msg, thingModelId, idName: "mt_gw_heart", name: "[GW HEART]", 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;
}
payload_parser(
{ eui: "5a53012a000002e9", shared_attrs: {} },
{
action: "heart",
data: {
deviceHandle: "GDI514-AGCN470-G-2",
hwVersion: "3.1.3",
fwVersion: "3.2.01",
netType: "mteth0",
validIP: "192.168.1.72",
nsConnect: true,
nmsConnect: true,
pmcConnect1: true
}
},
"95121332907282437",
{ telemetry_data: true }
)Thing Model: [GW WIFI] scan result
id_name: mt_gw_wifi · ID: 119474792070803456
import {Buffer} from "buffer";
import {BufferTKL, PayloadParser, RPCHelper, Utils} from "#tklHelper";
// 网关周边 WiFi 扫描结果物模型 [GW WIFI] —— 平台 id_name: mt_gw_wifi。
// 数据来源:mt_gw_wifi_list_get(nmsDnGw) 触发网关 iwlist 扫描,回复落 /v32/<tenant>/nms/up/gw/<eui>;
// 平台 tkl-main 仅落库 heart 分支(不落 wifiList,BUG-048),故由转发器 mt-gw-wifilist-to-as 订阅 nms/up/gw、
// 把 AP 列表重封装成 AS 上行(/v32/<tenant>/as/up/data/<eui>, userdata.payload=base64(JSON {w:[...]})),
// 本 CUSTOMER 解析器解 AS 帧 → 遥测 wifi_ap_count(本帧标量,供触发器判别+观测) + shared_attrs.wifi_list(供腾讯定位) + wifi_scanned_at。
// AP 列表只进 shared_attrs(每次扫描整体刷新、不入历史趋势);遥测只留计数标量。
function payload_parser(device, msg, thingModelId, noticeAttrs, org_params) {
let o;
try {
const raw = msg && msg.userdata && msg.userdata.payload;
if (raw == null) return null; // 非 AS 注入帧(如 heart 经 NMS 上行)→ 让位
o = typeof raw === "string" ? JSON.parse(Buffer.from(raw, "base64").toString("utf8")) : raw;
} catch (e) {
return null; // 解析失败不写脏数据
}
if (!o || typeof o !== "object" || !Array.isArray(o.w)) return null; // 非 wifi 帧(无 w 数组)→ 让位
const num = (v) => (v == null || isNaN(Number(v)) ? null : Number(v));
const aps = o.w
.map((a) => ({
bssid: a.bssid != null ? String(a.bssid) : null,
rssi: num(a.rssi),
essid: a.essid != null ? String(a.essid) : null
}))
.filter((a) => a.bssid); // 丢弃无 bssid 的项(腾讯定位以 bssid/mac 为键)
if (!aps.length) return null; // 无有效 AP 不写脏数据
let result = {
telemetry_data: { wifi_ap_count: aps.length },
server_attrs: null,
shared_attrs: { wifi_list: aps, wifi_scanned_at: o.ts != null ? o.ts : new Date().toISOString() }
};
// ── rpcHook (设备对接通用规则) ──
if (typeof RPCHelper.buildRpcHookAction === "function") {
const __a = RPCHelper.buildRpcHookAction({ device, msg, thingModelId, idName: "mt_gw_wifi", name: "[GW WIFI] scan result", 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;
}
// 本地测试:转发器注入的 AS 帧(base64 JSON {w:[...]}) → 期望 telemetry wifi_ap_count + shared_attrs.wifi_list
payload_parser(
{ eui: "5a53012a000002e9", shared_attrs: {} },
{
userdata: {
port: 2,
payload: Buffer.from(JSON.stringify({
w: [
{ bssid: "34:ce:00:11:22:33", rssi: -45, essid: "GDI514-AGCN470" },
{ bssid: "9c:21:6a:aa:bb:cc", rssi: -67, essid: "office-2.4G" }
]
})).toString("base64")
}
},
"119474792070803456",
{ telemetry_data: true }
)Trigger: [GW LOCATE] on wifi scan
id_name: mt_gw_locate_on_wifi · ID: 119474793920491520
import {Utils} from "#tklHelper";
// 网关定位触发器 [GW LOCATE] —— 平台 id_name: mt_gw_locate_on_wifi。
// 绑定到 mt_gw_wifi 物模型:每次 wifiList 扫描结果经转发器回灌 AS、被 mt_gw_wifi 解析出
// telemetry wifi_ap_count(>0) + shared_attrs.wifi_list 后,本触发器派发 mt_gw_locate,
// 由其读 shared_attrs.wifi_list → 调腾讯 WiFi 定位 → 写经纬度到 shared_attrs。
function trigger_script(device, thingModelId) {
const td = device?.telemetry_data?.[thingModelId];
if (!td) return null;
if (td.wifi_ap_count == null || td.wifi_ap_count <= 0) return null; // 无有效扫描结果不定位(KB-30:业务字段判别,非_modelName)
return {
should_dispatch: true, // 相同 AP 计数的相邻扫描仍重新定位
delayms: 1000, // 给 shared_attrs.wifi_list 落库留余量后再派发 locate
abort_previous_timer: true,
actions: [
{ method: "mt_gw_locate", params: { _eui: device.eui } }
]
};
}
// 用例:扫到 2 个 AP → 期望派发 mt_gw_locate 带 _eui
console.log(JSON.stringify(trigger_script(
{ eui: "5a53012a000002e9", telemetry_data: { "119474792070803456": { wifi_ap_count: 2 } } },
"119474792070803456"
)));
// 反例:无 AP(wifi_ap_count=0) → 期望 null
console.log(JSON.stringify(trigger_script(
{ eui: "5a53012a000002e9", telemetry_data: { "119474792070803456": { wifi_ap_count: 0 } } },
"119474792070803456"
)));物模型字段定义
| 物模型 | 字段 | 类型 | 单位 | 名称 |
|---|---|---|---|---|
| mt_gw_heart | handle | string | Device Handle | |
| mt_gw_heart | hwVersion | string | Hardware Version | |
| mt_gw_heart | fwVersion | string | Firmware Version | |
| mt_gw_heart | netType | string | Network Type | |
| mt_gw_heart | validIP | string | Valid IP | |
| mt_gw_heart | nsConnect | boolean | NS Connection | |
| mt_gw_heart | nmsConnect | boolean | NMS Connection | |
| mt_gw_heart | pmcConnect1 | boolean | PMC Connection | |
| mt_gw_wifi | wifi_ap_count | number | WiFi AP Count |
RPC 参数定义
| RPC | 参数 | 类型 | 说明 | 默认值 | 单位 |
|---|---|---|---|---|---|
| mt_gw_geo_set_config | tencent_lbs_key | string | Tencent LBS Key | — | |
| mt_gw_geo_set_config | tencent_lbs_base_url | string | Tencent LBS Base URL | https://apis.map.qq.com |
触发器定义
| 触发器 | 条件信号 | 延迟表达式 | RPC 动作 | 取消前序定时器 |
|---|---|---|---|---|
| mt_gw_locate_on_wifi | — | 1000 | mt_gw_locate | true |
第三方平台数据订阅
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,
"handle": "sample-1",
"hwVersion": "sample-2",
"fwVersion": "sample-3",
"netType": "sample-4",
"validIP": "sample-5",
"nsConnect": true,
"nmsConnect": true,
"pmcConnect1": true
}
}模板选择与验证
在 ThinkLink 平台按项目名称、业务代码或模型清单中的 id_name、ID 搜索并绑定模型。部署后使用真实上行帧核对字段、单位、RPC 和触发器行为。
补充说明
需求与决策依据:requirement/requirements.md。