Skip to content

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] paradsod705_get_22107100637692055785477
RPC[DSOD705 SET] paradsod705_set_22107100638082046365701
TemplateDSOD705-22107100638935071330309
Parameter Thing Model[DSOD705-PARA]dsod705_para_22107100632408214212613
Thing Model[DSOD705]dsod_22107100631632184086533
Thing Model[DSOD-22107-DSOD705资产] 区域聚合dsod_asset_22107119677174411063306

产品特点与适用范围

本项目面向“水面油污检测传感器”场景。能力边界、字段和可部署模型均以当前 descriptor、requirements 与模型源文件为准。

部署与采集信息

| DTU | KC11 | | 供电 | ac | | LoRaWAN Class | C | | 接口 | RS-485 | | 波特率 | 9600 |

遥测与寄存器定义

字段名称类型单位地址/来源说明
oil油污浓度floatLE%0x0000-0x0001水面油污浓度,IEEE 754 floatLE,Modbus FC 04 读输入寄存器

参数定义

参数名称类型默认值范围单位说明
app_70period_upuint32LE0-4294967295s
app_74period_readuint32LE0-4294967295s
app_110cov_oilfloatLE%
app_150addr_modbusuint811-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_22107cov_oilnumber%Oil Change Threshold
dsod705_para_22107period_upnumbersUpload Period
dsod705_para_22107addr_modbusnumberModbus Address
dsod705_para_22107period_readnumbersCollect Period
dsod_22107oilnumber%Oil Level
dsod_22107snrnumberdBSNR
dsod_22107rssinumberdBmRSSI
dsod_22107rationumberRatio
dsod_22107batterynumbervBattery
dsod_asset_22107zone_oil_avgnumber%Zone Oil Level
dsod_asset_22107min_snrnumberdBZone SNR
dsod_asset_22107min_rssinumberdBmZone RSSI
dsod_asset_22107min_batterynumbervZone Battery
dsod_asset_22107online_countnumberZone Online Count

RPC 参数定义

RPC参数类型说明默认值单位
dsod705_set_22107cov_oilnumberOil Change Threshold%
dsod705_set_22107period_upnumberUpload Period (s)s
dsod705_set_22107addr_modbusnumberModbus Address (1-247)
dsod705_set_22107period_readnumberCollect 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