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: [],
},
},
})
onBluetoothAdapterStateChange
: 当设备蓝牙状态变更时。onDeviceInfoUpdated
: 当设备信息变化时。onDeviceOnlineStatusUpdate
: 当设备上下线时。onDpDataChange
: 当设备 DP 上报时。onNetworkStatusChange
: 当网络状态变化时。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: 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],
},
},
})