Smart Device Model (SDM) is a concept in the device panel development SDK launched by the Tuya panel miniapp. It is a set of specifications developed around device models. For more information, see Smart Device Model.
This topic describes the development capabilities provided by using SDM:
As a built-in extension package of SDM, DP-Kit can serialize and deserialize DPs according to protocol rules. When a device DP is reported, the dpDataChange
event is triggered. The SDM model detects this event and calls interceptors
from left to right. After the execution is completed, the interceptor uses context
to send the results to the component that invoked useProps
.
If the DP-Kit interceptor is built in, it will traverse each dpCode
in dpState
according to the protocols
parser. If a parser is defined, the parser performs the deserialization process. The flowchart shows how to execute DP-Kit:
Beacon devices do not report DPs, but only accept DPs sent from the cloud. Therefore, the existing requirement is that the panel needs to record the sent DPs to the cloud and restore the status from the cloud when users enter the panel next time. The following sections show how to implement this feature using the DP interceptor.
You can use the SDM panel template for this tutorial.
The SDM supports the following interceptor types:
const lamp = new SmartDeviceModel({
interceptors: {
response: {
onBluetoothAdapterStateChange: [],
onDeviceInfoUpdated: [],
onDeviceOnlineStatusUpdate: [],
onDpDataChange: [],
onNetworkStatusChange: [],
},
request: {
publishDps: [],
},
},
})
onBluetoothAdapterStateChange
: When the device's Bluetooth status changes.onDeviceInfoUpdated
: When the device information changes.onDeviceOnlineStatusUpdate
: When the device goes online or offline.onDpDataChange
: When the device reports a DP.onNetworkStatusChange
: When the network status changes.publishDps
: When the panel DP is sent.The publishDps
interceptor is required here. The sample code is written as follows:
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 is an SDM instance, and getDevInfo is used to get devInfo
// mapDpCode2Dps is used to convert dpCode to dpId
const value = JSON.stringify(mapDpCode2Dps(dpState, ctx.instance.getDevInfo()));
console.log(`[devPropInterceptor] stored in cloud`, key, value);
// Make an API request, save the information to the cloud, and link it with devId
saveDevProperty({
devId: ctx.instance.getDevInfo().devId,
bizType: 0,
propertyList: JSON.stringify([
{
code: key,
value,
},
]),
}).then(() => {
// Call the next function to proceed to the subsequent interceptor middleware.
next(dpState, options); // Proceed to the next middleware, such as sending commands.
});
};
Note: The middleware is a high-order function and will not be executed until the next
method is executed. If the next
method is not called, the process will stop after the current interceptor is executed. That is, the DP will not be sent to the device.
After writing the interceptor code, you can configure the interceptor to Lamp SDM as follows:
const lamp = new SmartDeviceModel({
interceptors: {
request: {
publishDps: [devPropInterceptor],
},
},
})
The panel needs to respond immediately after sending DPs, and the beacon device will not report the DP status. Therefore, you need to configure a DP-Kit interceptor and enable the immediate
mode.
import { createDpKit } from '@ray-js/panel-sdk/lib/sdm/interceptors/dp-kit';
export const dpKit = createDpKit({
sendDpOption: {
immediate: true, // Enable immediate response to DP commands, without waiting for the device to report
},
});
const lamp = new SmartDeviceModel({
interceptors: {
request: {
// Add dpKit.publishDps
publishDps: [dpKit.publishDps, devPropInterceptor],
},
},
})
If you have configured the immediate
mode, when the interceptor executes dpKit.publishDps
, it will simulate the triggering of a DP change event, and the panel will be updated.
A beacon device does not report data, but a virtual device reports data during the development phase. Therefore, after you enable the immediate
mode, the panel might respond twice after DPs are sent. Use the onDpDataChange
interceptor to ignore device reporting. The sample code is written as follows:
import { OnDpDataChangeInterceptor } from '@ray-js/panel-sdk';
export const ignoreDpChangeInterceptor: OnDpDataChangeInterceptor = ctx => next => data => {
// @ts-ignore
if (data.__from__ === 'dp-kit') {
console.log(`[ignoreDpChangeInterceptor] triggered immediately`);
return next(data);
} else {
console.log(`[ignoreDpChangeInterceptor] ignore device reporting`);
}
};
Code logic: There is a hidden property __from__
in the data
object. If the value is dp-kit
, it means this execution comes from SDM, such as an update triggered by immediate
. The role of the next
function is to execute the interceptor backwards. If the next
function is not executed, the props
update will not be performed.
Configure to the SDM interceptor:
const lamp = new SmartDeviceModel<SmartDeviceSchema>({
interceptors: {
response: {
// Add a DP-Kit response parsing interceptor
...dpKit.interceptors.response,
// dpKit.onDpDataChange processes the DP protocol and determines the source. Ignore data from the device.
onDpDataChange: [dpKit.onDpDataChange, ignoreDpChangeInterceptor],
},
request: {
publishDps: [dpKit.publishDps, devPropInterceptor],
},
},
})
Typically, lighting devices have many DPs with complex protocols, such as music sync DP. The following takes the music sync DP of beacon lighting devices as an example.
The DP protocol of the music light is as follows:
Type: raw: AABBBBCCDD
AA
: parameter bit, hexadecimal converted to 8-bit binary. 8 bits: 00000000
. Highest bit: 0
means direct output, while 1
means gradient.BB
: Hue (h
). Valid values: 0 to 360.CC
: Saturation (s
). Valid values: 0 to 100.DD
: Value (v
). Valid values: 0 to 100.Example: {"13":"0000DC6464"}
.
Write code for the music light DP protocol parser:
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) {
// Default value for first entry into the panel
this.defaultValue = {
mode: 0,
h: 220,
s: 100,
v: 100,
};
this.uuid = uuid;
if (defaultValue) {
this.defaultValue = defaultValue;
}
}
// Deserialize to an object, and read specific properties in the panel code
parser(value: string) {
const { length } = value;
if (length !== 10) {
console.warn('Parsing failed because DP data error occurred', value);
return this.defaultValue;
}
const step = generateDpStrStep(value);
return {
mode: step(4).value, // 4 characters
h: step(2).value, // 2 characters
s: step(2).value, // 2 characters
v: step(2).value, // 2 characters
};
}
to16(value, length = 4) {
return decimalToHex(value, length);
}
//Serialize into a string that can be sent
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)}`;
}
}
Configure to the DP-Kit:
// The map object. Declare the parser corresponding to music_data_raw
const protocols = {
music_data_raw: new MusicFormatter('music_data_raw'),
};
export const dpKit = createDpKit<SmartDeviceSchema>({
protocols, //Configure to DP protocol rules
sendDpOption: {
immediate: true,
},
});
// dpKit.onDpDataChange contains the logic for serializing the raw string reported by the device
// dpKit.publishDps contains the logic for deserializing the objects sent by the panel into raw strings
const lamp = new SmartDeviceModel<SmartDeviceSchema>({
interceptors: {
response: {
...dpKit.interceptors.response,
onDpDataChange: [dpKit.onDpDataChange, ignoreDpChangeInterceptor],
},
request: {
publishDps: [dpKit.publishDps, devPropInterceptor],
},
},
})