dp-kit 是一个 SDM 的拓展包,已内置支持,可以对 DP 按照协议规则进行序列化和反序列化。当设备 DP 上报时,触发 dpDataChange 事件,SDM 模型监听到该事件,并从左到右遍历调用 interceptors 拦截器,拦截器执行完成后将结果通过 context 发送到调用 useProps 的组件中。

如内置的 dp-kit 拦截器,根据 protcols 解析器,遍历 dpState 中的每一项 dpCode,如果定义了解析器,则会由解析器进行反序列化处理。dp-kit 执行的流程图如下:

现在有一个需求,beacon 设备不会上报 DP,只接受 DP 下发,所以面板需要将下发的 DP 记录一份到云端,在下次进入面板时从云端恢复状态。下面利用 DP 拦截器来实现该功能:

请使用 SDM 面板模版进行本教程学习

先来看一下 SDM 支持的拦截器类型,如下代码所示:

const lamp = new SmartDeviceModel({
          interceptors: {
          response: {
          onBluetoothAdapterStateChange: [],
          onDeviceInfoUpdated: [],
          onDeviceOnlineStatusUpdate: [],
          onDpDataChange: [],
          onNetworkStatusChange: [],
          },
          request: {
          publishDps: [],
          },
          },
          })
        
  1. onBluetoothAdapterStateChange: 当设备蓝牙状态变更时
  2. onDeviceInfoUpdated: 当设备信息变化时
  3. onDeviceOnlineStatusUpdate: 当设备上下线时
  4. onDpDataChange: 当设备 DP 上报时
  5. onNetworkStatusChange: 当网络状态变化时
  6. publishDps: 当面板 DP 下发时

这里我们需要的是 publishDps 拦截器,开始编写代码:

import { PublishDpsInterceptor } from '@ray-js/panel-sdk';
          import { mapDpCode2Dps } from '@ray-js/panel-sdk/lib/sdm/interceptors/dp-kit/core/mapDpCode2Dps';
          import { saveDevProperty } from '@ray-js/ray';

          const key = 'sdm-dev-dp-props';

          export const devPropInterceptor: PublishDpsInterceptor = ctx => next => (dpState, options) => {
          // ctx.instance 是 SDM 实例,getDevInfo 可获取到 devInfo
          // mapDpCode2Dps 用来将 dpCode 转为 dpId
          const value = JSON.stringify(mapDpCode2Dps(dpState, ctx.instance.getDevInfo()));

          console.log(`[devPropInterceptor] 存入cloud`, key, value);

          // 调用接口,保存到云端,关联到devId
          saveDevProperty({
          devId: ctx.instance.getDevInfo().devId,
          bizType: 0,
          propertyList: JSON.stringify([
          {
          code: key,
          value,
          },
          ]),
          }).then(() => {
          // 调用 next 函数,会向后执行后续的拦截器中间件
          next(dpState, options); // 后续中间件,如下发
          });
          };
        

这里注意到中间件是一个高阶函数,执行 next 方法后才会向后执行,如果不调用 next 方法,会在当前拦截器执行完成后停止,即不会下发 DP 到设备。

编写好拦截器代码后,将拦截器配置到 lamp SDM,代码如下:

const lamp = new SmartDeviceModel({
          interceptors: {
          request: {
          publishDps: [devPropInterceptor],
          },
          },
          })
        

由于需求需要面板下发 DP 后立即响应,beacon 设备不会上报 DP 状态,所以需要配置 dp-kit 拦截器,并启用 immediate 模式:

import { createDpKit } from
          '@ray-js/panel-sdk/lib/sdm/interceptors/dp-kit';

          export const dpKit = createDpKit({
          sendDpOption: {
          immediate: true, // 启用 immediate 模式,下发后会立即响应,无需等待设备上报
          },
          });

          const lamp = new SmartDeviceModel({
          interceptors: {
          request: {
          // 这里增加配置 dpKit.publishDps
          publishDps: [dpKit.publishDps, devPropInterceptor],
          },
          },
          })
        

如果配置了 immediate 模式,拦截器执行到 dpKit.publishDps 时,会模拟触发一个 DP Change 事件,让面板进行更新。

beacon 设备本身不会上报,但在虚拟设备开发阶段,虚拟设备本身还会上报,所以在启用 immediate 模式后,会出现面板下发 DP 后响应 2 次的情况。现在利用 onDpDataChange 拦截器实现忽略设备上报,编写代码如下:

import { OnDpDataChangeInterceptor } from
          '@ray-js/panel-sdk';

          export const ignoreDpChangeInterceptor: OnDpDataChangeInterceptor = ctx => next => data => {
          // @ts-ignore
          if (data.__from__ === 'dp-kit') {
          console.log(`[ignoreDpChangeInterceptor] 立即触发`);
          return next(data);
          } else {
          console.log(`[ignoreDpChangeInterceptor] 忽略设备上报`);
          }
          };
        

代码逻辑十分简单,data对象中有一个隐藏属性 __from__, 如果值是 dp-kit 表示该次执行来自 SDM 如 immediate 触发的更新。这里需要理解到 next 函数的作用,next 函数会向后执行拦截器,如果不执行 next 函数则不会执行 props 更新。

配置到 SDM 拦截器

const lamp = new SmartDeviceModel<SmartDeviceSchema>({
          interceptors: {
          response: {
          // 加入 dp-kit 响应解析拦截器
          ...dpKit.interceptors.response,
          // dpKit.onDpDataChange 先处理完成 dp 协议,然后判断来源如果是设备,就忽略
          onDpDataChange: [dpKit.onDpDataChange, ignoreDpChangeInterceptor],
          },
          request: {
          publishDps: [dpKit.publishDps, devPropInterceptor],
          },
          },
          })
        

照明设备通常有很多复杂协议的 DP,如音乐律动 DP,下面以 beacon 照明设备的音乐律动 DP 为例说明:

音乐灯 DP 协议如下:

类型:raw : AAAABBCCDD;
AAAA:8bit:0000 0000 最高位:0表示直接输出,1表示渐变; BB:H(色度:0-360) CC:S (饱和:0-100 ) DD:V (明度:0-100)

示例:{"13":"0000DC6464"}

编写音乐 DP 协议器:

import { Transformer } from
          '@ray-js/panel-sdk/lib/protocols/lamp/interface';
          import { decimalToHex, generateDpStrStep } from '@ray-js/panel-sdk/lib/utils';

          type IMusicData = {
          mode: number;
          h: number;
          s: number;
          v: number;
          };

          class MusicFotmatter implements Transformer<IMusicData> {
          defaultValue: IMusicData;
          uuid: string;
          constructor(uuid = 'music_data_raw', defaultValue = null) {
          // 首次进入面板的默认值
          this.defaultValue = {
          mode: 0,
          h: 220,
          s: 100,
          v: 100,
          };
          this.uuid = uuid;
          if (defaultValue) {
          this.defaultValue = defaultValue;
          }
          }

          // 反序列化为对象,可在面板代码中读取具体属性
          parser(value: string) {
          const { length } = value;
          if (length !== 10) {
          console.warn('数据有问题,解析失败', value);
          return this.defaultValue;
          }
          const step = generateDpStrStep(value);
          return {
          mode: step(4).value, // 4个字符
          h: step(2).value, // 2个字符
          s: step(2).value, // 2个字符
          v: step(2).value, // 2个字符
          };
          }

          to16(value, length = 4) {
          return decimalToHex(value, length);
          }

          // 序列化为可下发的字符串
          formatter(data: IMusicData) {
          const { mode, h, s, v } = data;
          return `${this.to16(mode, 4)}${this.to16(h, 2)}${this.to16(s, 2)}${this.to16(v, 2)}`;
          }
          }
        

配置到 dp-kit:

// map对象,声明 music_data_raw 对应的解析器
          const protocols = {
          music_data_raw: new MusicFormatter('music_data_raw'),
          };

          export const dpKit = createDpKit<SmartDeviceSchema>({
          protocols, // 配置到 dp 协议规则
          sendDpOption: {
          immediate: true,
          },
          });

          // dpKit.onDpDataChange 中包含了对设备上报来的 raw 字符串序列化逻辑
          // dpKit.publishDps 中包含了对面板下发的对象反序列化为 raw 字符串的逻辑
          const lamp = new SmartDeviceModel<SmartDeviceSchema>({
          interceptors: {
          response: {
          ...dpKit.interceptors.response,
          onDpDataChange: [dpKit.onDpDataChange, ignoreDpChangeInterceptor],
          },
          request: {
          publishDps: [dpKit.publishDps, devPropInterceptor],
          },
          },
          })