SDM 是涂鸦面板小程序推出的一个设备面板开发 SDK 工具包中的概念,即 Smart Device Model,是围绕设备模型开发的一套规范。详细介绍请查看 SDM 文档

本文介绍使用 SDM 开发带来的开发能力:

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

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

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

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

首先介绍 SDM 支持的拦截器类型,如下代码所示:

const lamp = new SmartDeviceModel({
  interceptors: {
    response: {
      onBluetoothAdapterStateChange: [],
      onDeviceInfoUpdated: [],
      onDeviceOnlineStatusUpdate: [],
      onDpDataChange: [],
      onNetworkStatusChange: [],
    },
    request: {
      publishDps: [],
    },
  },
})

这里需要 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: AABBBBCCDD

示例:{"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],
    },
  },
})