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

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

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