22107-DSOD705 对接文档
传感器简介
DSOD705 非接触式紫外荧光水面油污检测传感器,Modbus-RTU 通过 KC11 市电版 DTU 接入,Class C 常在线
| 项目 | 值 |
|---|---|
| 项目名称 | DSOD-22107-DSOD705 |
| 厂商 | 深圳迪森科技 (DISEN) |
| 型号 | DSOD705 |
| 业务代码 | 22107 |
| 对接模式 | edgebus-dtu |
| 协议 | modbus-rtu |
模型清单
可使用下列名称或 ID 在 ThinkLink 中搜索对应模型。
| 模型类型 | 名称 | id_name | 平台模型 ID |
|---|---|---|---|
| RPC | [DSOD705 GET] para | dsod705_get_22107 | 100637692055785477 |
| RPC | [DSOD705 SET] para | dsod705_set_22107 | 100638082046365701 |
| Template | DSOD705-22107 | 100638935071330309 | |
| Parameter Thing Model | [DSOD705-PARA] | dsod705_para_22107 | 100632408214212613 |
| Thing Model | [DSOD705] | dsod_22107 | 100631632184086533 |
| Thing Model | [DSOD-22107-DSOD705资产] 区域聚合 | dsod_asset_22107 | 119677174411063306 |
产品特点与适用范围
本项目面向“水面油污检测传感器”场景。能力边界、字段和可部署模型均以当前 descriptor、requirements 与模型源文件为准。
部署与采集信息
| DTU | KC11 | | 供电 | ac | | LoRaWAN Class | C | | 接口 | RS-485 | | 波特率 | 9600 |
遥测与寄存器定义
| 字段 | 名称 | 类型 | 单位 | 地址/来源 | 说明 |
|---|---|---|---|---|---|
| oil | 油污浓度 | floatLE | % | 0x0000-0x0001 | 水面油污浓度,IEEE 754 floatLE,Modbus FC 04 读输入寄存器 |
参数定义
| 参数 | 名称 | 类型 | 默认值 | 范围 | 单位 | 说明 |
|---|---|---|---|---|---|---|
| app_70 | period_up | uint32LE | — | 0-4294967295 | s | — |
| app_74 | period_read | uint32LE | — | 0-4294967295 | s | — |
| app_110 | cov_oil | floatLE | — | % | — | |
| app_150 | addr_modbus | uint8 | 1 | 1-247 | — |
Requirements 证据
以下关键事实由 requirements 源文件提取;原始行号用于复核。
| 类别 | 来源事实 | 来源 |
|---|---|---|
| 部署 | | DTU Power Mode | AC 市电(220 V,KC11 常规 85-270 VAC 供电) | | requirement/requirements.md:18 |
| 部署 | | LoRaWAN Class | Class C(KC11 恒定市电供电,无需询问,KB-37;EB 代码 Battery: false, ConfirmDuty: 60) | | requirement/requirements.md:19 |
| 部署 | > KC11 恒定市电供电、Class C 常在线,符合 DSOD705 需要实时/准实时监测油污泄漏的场景要求。 | requirement/requirements.md:22 |
| 部署 | | DSOD705 Power Consumption | < 2 W(直流),部分资料提及 < 10 W | manual | | requirement/requirements.md:55 |
| 部署 | | KC11 DTU Voltage | 220 V AC 市电供电 | manual + descriptor.yaml | | requirement/requirements.md:56 |
| 未确认事项 | | Power-on to Stable Working | 1 s (default) | 说明书未标注,默认为 1 秒。如实际设备需要更长时间请修改。TODO: 向厂商/现场核实实际上电稳定时间。 | | requirement/requirements.md:62 |
| 协议/数据 | | Interface Type | RS-485 / Modbus RTU | | requirement/requirements.md:68 |
| 协议/数据 | | Baud Rate | 9600 bps | | requirement/requirements.md:69 |
| 部署 | | 红色线 +24 V | 电源 + | 外部 24 VDC 电源正极(非 KC11 输出,DSOD705 需独立 24 VDC 供电) | | requirement/requirements.md:91 |
| 部署 | DTU-specific wiring notes: | requirement/requirements.md:97 |
| 部署 | - KC11:AC L/N 接 220 V 市电为 DTU 供电;RS-485A/485B 接 DSOD705 传感器 A/B;DSOD705 本体需要独立 +24 VDC 供电(红/黑线),不经 KC11 供电输出。4-20 mA 与继电器接口本方案未接入 KC11,可按现场需求独立接入本地控制/报警系统。 | requirement/requirements.md:99 |
| 协议/数据 | | Protocol Type | modbus-rtu | | requirement/requirements.md:107 |
| 协议/数据 | | Device Default Address | 0x01(对应 addr_modbus 参数,可通过 RPC 远程修改,范围 1-247) | | requirement/requirements.md:108 |
| 协议/数据 | | Frame Format | Modbus RTU,功能码 0x04(读输入寄存器),起始地址 0x0000,寄存器数量 2(组成 1 个 FloatLE 值) | | requirement/requirements.md:109 |
| 协议/数据 | 只读输入寄存器(本方案实际采集 0x0000~0x0001): | requirement/requirements.md:114 |
| 协议/数据 | | Data Item | Address | Data Type | Length (bytes) | Coefficient | R/W | Notes | | requirement/requirements.md:116 |
| 协议/数据 | | 传感器溢油检测值(oil) | 0x0000 | Float(DCBA/FloatLE) | 4(2 寄存器) | 1 | R | FC 0x04 读取,对接实际采集字段 | | requirement/requirements.md:118 |
| 协议/数据 | | 传感器温度 | 0x0001 | Float(DCBA) | 4(2 寄存器) | — | R | 手册列出,本方案未采集 | | requirement/requirements.md:119 |
| 协议/数据 | | 传感器 SN | 0x0010 | ASCII-HEX | 12(6 寄存器) | — | R | 手册列出,本方案未采集 | | requirement/requirements.md:120 |
| 协议/数据 | | 硬件/软件版本 | 0x0016 | HEX | 4(2 寄存器) | — | R | 手册列出,本方案未采集 | | requirement/requirements.md:121 |
| 协议/数据 | 保持寄存器(本方案未通过 RPC 直接暴露,供参考/现场调试用): | requirement/requirements.md:123 |
| 协议/数据 | | Data Item | Address | Data Type | Length | R/W | Notes | | requirement/requirements.md:125 |
| 协议/数据 | | 传感器地址 | 0x0000 | Short | 1 | R/W | 范围 1-247 | | requirement/requirements.md:127 |
| 协议/数据 | | 波特率 | 0x0001 | Short | 1 | R/W | 0=4800,1=9600,2=19200,3=56000,4=57600,5=115200 | | requirement/requirements.md:128 |
EdgeBus 代码
以下为当前使用的 EdgeBus 业务代码。
typescript
import {Buffer} from "buffer";
import {buildOtaFile} from "@EBSDK/run";
import {LoraUpEvent} from "@EBSDK/EBCompiler/all_variable";
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, HwTypeEnum, UpgrdTypeEnum} from "@EBSDK/otaConfig";
////////////////////////////////////////////////////////////////////////////////////////
const eventInfo:UserConfUPItem[]=[
{
name:"dsod705",port:22, version:"0x86",dataType:"0x07",upPeriodIndex:70,
quInfo:[
{
protocol:"modbus",code:"0x04", periodIndex:74,//addr:"0x01",
indexAPP:150, indexCMD:0, copySize:1,isLast:false,//period:"900s",payIndex:3,ackAddrIndex:0,
listVal:[
{ start: "0x0000", end: "0x0001" ,covType:"FloatLE",covAppIndex:110}, //covAppIndex:
]
}
]
}
]
let otaConfig = getOtaConfig({
BaudRate: 9600,
StopBits: 1,
DataBits: 8,
Checkbit: CheckbitEnum.NONE,
Battery: false,
ConfirmDuty: 60,
BzType: 22107,
BzVersion: 11
})
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)物模型与 RPC 代码
RPC: [DSOD705 GET] para
id_name: dsod705_get_22107 · ID: 100637692055785477
javascript
import {RPCHelper, Utils} from '#tklHelper';
function rpc_script({ device, params, alarms, logger, org_params }) {
let paraDef = {
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_oil", field_name: "cov_oil", unit: "%", type: "floatLE",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:0,
})
return [msg]
}
rpc_script({
device: { server_attrs: {}, shared_attrs: {} },
params: {},
alarms: [],
logger: console,
org_params: {}
})RPC: [DSOD705 SET] para
id_name: dsod705_set_22107 · ID: 100638082046365701
javascript
import {RPCHelper, Utils} from '#tklHelper';
import {Buffer} from "buffer";
function rpc_script({ device, params, alarms, logger, org_params }) {
let classMode = (device && device.shared_attrs && device.shared_attrs.class_mode) || "ClassA";
const rpcName="dsod705_set_22107"
let paraDef = {
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_oil", field_name: "cov_oil", unit: "%", type: "floatLE",decimal:1},
app_150:{name: "addr_modbus", field_name: "addr_modbus", unit:"", type: "uint8"}
}
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
}
rpc_script({
device: { server_attrs: {}, shared_attrs: {} },
params: {},
alarms: [],
logger: console,
org_params: {}
})Parameter Thing Model: [DSOD705-PARA]
id_name: dsod705_para_22107 · ID: 100632408214212613
javascript
import {PayloadParser, RPCHelper, Utils} from '#tklHelper';
function payload_parser(device, msg, thingModelId, noticeAttrs, org_params) {
let port=msg?.userdata?.port || null;
const rpcName="dsod705_set_22107";
let paraDef = {
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_oil", field_name: "cov_oil", unit: "%", type: "floatLE",decimal:1},
app_150:{name: "addr_modbus", field_name: "addr_modbus", unit:"", type: "uint8"}
}
if (port!==214) {
let checkData=Utils.paraCheck(rpcName,device.server_attrs,device.shared_attrs)
return {
server_attrs: checkData.sdata,
actions:checkData.actions,
}
}
let pdata=(new PayloadParser({
device:device,
msg:msg,
paraInfo:paraDef,
})).paras()
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: "dsod705_para_22107", name: "[DSOD705-PARA]", parsed: { telemetry_data: result.telemetry_data, server_attrs: result.server_attrs, shared_attrs: result.shared_attrs } });
if (__a) { result.actions = result.actions || []; result.actions.push(__a); }
}
return result;
}
console.log(payload_parser(
{ telemetry_data: {} },
{ userdata: { port: 11, payload: "" } },
"1",
{ telemetry_data: true },
{}
))Thing Model: [DSOD705]
id_name: dsod_22107 · ID: 100631632184086533
javascript
import {PayloadParser, RPCHelper} from '#tklHelper';
function payload_parser(device, msg, thingModelId, noticeAttrs, org_params) {
let port=msg?.userdata?.port || null;
if(port!=22) return null
let frameInfo={
port:22, dataLen:16,rssi:true,battery:4,
tagList:[{ index:0, tag:0x86}, { index:1, tag:0x07}]
}
let appInfo = [
{ name: "oil", field_name: "oil", unit:"%", index: 6, type: "floatLE",coefficient:1,decimal:2},
]
let payParser=new PayloadParser({
device:device,
msg:msg,
frameInfo:frameInfo,
appInfo:appInfo,
})
let tdata= payParser.telemetry()
if(tdata==undefined) { return null}
if((tdata?.status&0x02)===0x02) { // time out , just update the status.
const status=tdata.status
tdata={ ...(device.telemetry_data?.[thingModelId] ?? {})}
tdata.status=status
}
let result = {
telemetry_data: tdata,
server_attrs: null,
shared_attrs: null
};
// ── rpcHook (设备对接通用规则) ──
if (typeof RPCHelper.buildRpcHookAction === "function") {
const __a = RPCHelper.buildRpcHookAction({ device, msg, thingModelId, idName: "dsod_22107", name: "[DSOD705]", parsed: { telemetry_data: result.telemetry_data, server_attrs: result.server_attrs, shared_attrs: result.shared_attrs } });
if (__a) { result.actions = result.actions || []; result.actions.push(__a); }
}
return result;
}
console.log(payload_parser(
{ telemetry_data: {} },
{ userdata: { port: 11, payload: "" } },
"1",
{ telemetry_data: true },
{}
))Thing Model: [DSOD-22107-DSOD705资产] 区域聚合
id_name: dsod_asset_22107 · ID: 119677174411063306
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:zone_oil_avg=空间均值(oil);min_snr=取最小(snr);min_rssi=取最小(rssi);min_battery=取最小(battery);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: "dsod_asset_22107", name: "[DSOD-22107-DSOD705资产] 区域聚合", 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 = {
"zone_oil_avg": {
"op": "avg",
"src": "oil"
},
"min_snr": {
"op": "min",
"src": "snr"
},
"min_rssi": {
"op": "min",
"src": "rssi"
},
"min_battery": {
"op": "min",
"src": "battery"
},
"online_count": {
"op": "count_online",
"src": ""
}
};
const device = { name: "DSOD-22107-DSOD705资产-1", server_attrs: { agg_spec, stale_ms: 1800000, members: {} } };
const r1 = payload_parser(device, { eui: "MEMBER_A", telemetry_data: { m1: {"oil":1,"snr":1,"rssi":1,"battery":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: {"oil":2,"snr":2,"rssi":2,"battery":2} } }, "tmid", { telemetry_data: true });
device.server_attrs.members = r2.server_attrs.members;
console.log("成员B上报后(2台聚合):", JSON.stringify(r2.telemetry_data));
void PayloadParser;物模型字段定义
| 物模型 | 字段 | 类型 | 单位 | 名称 |
|---|---|---|---|---|
| dsod705_para_22107 | cov_oil | number | % | Oil Change Threshold |
| dsod705_para_22107 | period_up | number | s | Upload Period |
| dsod705_para_22107 | addr_modbus | number | Modbus Address | |
| dsod705_para_22107 | period_read | number | s | Collect Period |
| dsod_22107 | oil | number | % | Oil Level |
| dsod_22107 | snr | number | dB | SNR |
| dsod_22107 | rssi | number | dBm | RSSI |
| dsod_22107 | ratio | number | Ratio | |
| dsod_22107 | battery | number | v | Battery |
| dsod_asset_22107 | zone_oil_avg | number | % | Zone Oil Level |
| dsod_asset_22107 | min_snr | number | dB | Zone SNR |
| dsod_asset_22107 | min_rssi | number | dBm | Zone RSSI |
| dsod_asset_22107 | min_battery | number | v | Zone Battery |
| dsod_asset_22107 | online_count | number | Zone Online Count |
RPC 参数定义
| RPC | 参数 | 类型 | 说明 | 默认值 | 单位 |
|---|---|---|---|---|---|
| dsod705_set_22107 | cov_oil | number | Oil Change Threshold | — | % |
| dsod705_set_22107 | period_up | number | Upload Period (s) | — | s |
| dsod705_set_22107 | addr_modbus | number | Modbus Address (1-247) | — | |
| dsod705_set_22107 | period_read | number | Collect Period (s) | — | s |
触发器定义
无项目专用触发器。
第三方平台数据订阅
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,
"oil": 1.25
}
}模板选择与验证
在 ThinkLink 平台按项目名称、业务代码或模型清单中的 id_name、ID 搜索并绑定模型。部署后使用真实上行帧核对字段、单位、RPC 和触发器行为。
补充说明
需求与决策依据:requirement/requirements.md。