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],
    },
  },
})