本文档面向已经了解 面板小程序开发 的开发者,您需要充分了解什么是 面板小程序 和 产品功能。若对上述概念有所疑问,涂鸦推荐您了解以下预备知识。如已了解,可跳过本文。
面板是 IoT 智能设备在 App 终端上的产品形态。创建产品之前,首先来了解一下什么是面板,以及面板和产品、设备之间的关系:

照明灯带模板使用 SDM (Smart Device Model) 开发。关于 SDM 相关,可以参考 SDM 文档。
产品名称:照明幻彩灯带三路灯
根据支持的白光、彩光 DP 不同,照明中灯分为一路至五路。
其含义为:
colour_data 、白光亮度 bright_value 、白光色温 temp_value。switch_led DP 切换开关状态。
paint_colour_data,上方灯带会同步改变。paint_colour_data,上方灯带会同步改变。paint_colour_data,上方灯带会同步改变。paint_colour_data,上方灯带会同步改变。
dreamlight_scene_mode,灯带变化。
dreamlightmic_music_data 持续下发。
countdown 下发数据,以秒(s)为单位。

当前照明灯带模板必需的功能点:
switch_led,
work_mode,
paint_colour_data,
dreamlight_scene_mode,
dreamlightmic_music_data,
light_pixel,
lightpixel_number_set,
当前照明灯带模板可选的功能点:
countdown,
light_length,
| 参数 | 取值 | 
| dpid | 20 | 
| code | switch_led | 
| type | 布尔型(Bool) | 
| mode | 可下发可上报(rw) | 
| 参数 | 取值 | 
| dpid | 21 | 
| code | work_mode | 
| type | 枚举型(Enum) | 
| mode | 可下发可上报(rw) | 
| property | 枚举值: white, colour, scene, music | 
| 参数 | 取值 | 
| dpid | 26 | 
| code | countdown | 
| type | value | 
| mode | 可下发可上报(rw) | 
| property | { "unit": "s", "min": 0, "max": 86400, "scale": 0, "step": 1, "type": "value"} | 
| 参数 | 取值 | 
| dpid | 46 | 
| code | light_length | 
| type | value | 
| mode | 上报(ro) | 
| property | {"unit": "cm", "min": 1, "max": 10000, "scale": 0, "step": 1, "type": "value" } | 
| 参数 | 取值 | 
| dpid | 47 | 
| code | light_pixel | 
| type | value | 
| mode | 上报(ro) | 
| property | { "min": 1, "max": 1024, "scale": 0, "step": 1, "type": "value" } | 
| 参数 | 取值 | 
| dpid | 53 | 
| code | lightpixel_number_set | 
| type | value | 
| mode | 可下发可上报(rw) | 
| property | { "min": 1, "max": 1000, "scale": 0, "step": 1, "type": "value" } | 
以上为raw 类型 以外的功能点,raw 类型 DP 请查看具体协议。

首先需要创建一个产品,定义产品有哪些功能点,然后再在面板中一一实现这些功能点。
注册登录 涂鸦开发者平台,并在平台创建产品:
注意:本模板只支持幻彩灯带三路,其它路可按逻辑自行适配。

🎉 在这一步,一个名为 PublicLamp 的照明灯带产品创建完成。
面板小程序的开发在 小程序开发者 平台上进行操作,首先请前往 小程序开发者平台 完成平台的注册登录。
详细操作步骤可以参考 面板小程序 > 创建面板小程序。

面板模板仓库:仓库地址
git clone https://github.com/Tuya-Community/tuya-ray-materials.git
cd ./template/PublicPanelStripLamp




完成以上步骤后,一个面板小程序的开发模板初始化完成。以下为工程目录及其介绍:
├── public
│ ├── images # 项目的静态资源
├── src
│ ├── app.config.ts # 自动生成配置
│ ├── app.tsx # App 根组件
│ ├── components # 组件目录
│ ├── constant # 常量目录
│ ├── containers # 聚合的组件目录
│ ├── devices # 智能设备模型目录
│ │ ├── index.ts # 定义并导出智能设备模型
│ │ ├── protocols # 定义当前设备所需要的 DP 声明
│ │ ├─────parses # 定义当前设备所需要的 DP 复杂协议
│ │ └── schema.ts # 当前智能设备 DP 功能点描述,IDE 可自动生成
│ ├── global.config.ts
│ ├── hooks # 自定义 Hooks 目录
│ ├── i18n # 多语言目录
│ ├── pages # 页面目录
│ └── routes.config.ts # 路由配置
│─── typings # 业务类型定义目录
│ └── sdm.d.ts # 智能设备类型定义文件

生成 SDM schema 至项目中,可以查看 src/devices/schema.ts。
export const defaultSchema = [
	{
		attr: 641,
		canTrigger: true,
		code: "switch_led",
		defaultRecommend: true,
		editPermission: false,
		executable: true,
		extContent: "",
		iconname: "icon-dp_power",
		id: 20,
		mode: "rw",
		name: "开关",
		property: {
			type: "bool",
		},
		type: "obj",
	},
	{
		attr: 640,
		canTrigger: true,
		code: "work_mode",
		defaultRecommend: true,
		editPermission: false,
		executable: true,
		extContent: "",
		iconname: "icon-dp_mode",
		id: 21,
		mode: "rw",
		name: "模式",
		property: {
			range: ["white", "colour", "scene", "music"],
			type: "enum",
		},
		type: "obj",
	},
	// ...
] as const;
switch_led 下发的值展示不同按钮图片。switch_led,可作为属性传入组件。useProps 获取实时下发的 DP 值。示例:import React from "react";
import { View } from "@ray-js/ray";
import { useProps } from "@ray-js/panel-sdk";
export default function () {
	const power = useProps((props) => props.switch_led);
	return (
     <View style={{ flex: 1 }}>
         <View>switch_led: {power}</View>
     </View>
	);
}
useActions 下发 DP。关于如何通过 SDM 下发 DP,请参考:SDM 下发教程。示例:
import React from "react";
import { View } from "@ray-js/ray";
import { useActions } from "@ray-js/panel-sdk";
export default function () {
	const handleTogglePower = () => {
		actions.switch_led.toggle({ throttle: 300 });
	};
	return (
		<View style={{ flex: 1 }}>
			<Button img={power} onClick={handleTogglePower} />
		</View>
	);
}
根据上述分析,来实现开关组件:开关组件实现。
import React from "react";
import res from "@/res";
import { View, Image } from "@ray-js/ray";
import dpCodes from "@/config/dpCodes";
import { Button } from "@/components";
import { useActions, useProps } from "@ray-js/panel-sdk";
import styles from "./index.module.less";
export const PowerButton = () => {
	const power = useProps((props) => props.switch_led);
	const actions = useActions();
	const handleTogglePower = () => {
		actions.switch_led.toggle({ throttle: 300 });
	};
	return (
		<View className={styles.container}>
			<Image className={styles.bg} mode="aspectFill" src={bottom_dark} />
			<Button
				img={power}
				onClick={handleTogglePower}
				imgClassName={styles.powerBtn}
				className={styles.powerBox}
			/>
		</View>
	);
};
paint_colour_data。您可以查看开发者平台创建方案的 DP 具体协议,也可查看具体 代码解析 DP 进行下发。paint_colour_data DP 只能记忆最近一次的灯光数据,所以需要将灯带的每个灯珠颜色值进行云端存储,每当 DP 变换时进行云端灯珠颜色同步。dreamlight_scene_mode 进行下发。dreamlightmic_music_data 进行下发。src/devices/protocols/index 中声明,如下:import dpParser from './parsers';
import { lampSchemaMap } from '@/devices/schema';
const { colour_data } = lampSchemaMap;
export const protocols = {
  [colourCode]: [
     {
      name: 'hue' as const,
      bytes: 2,
      default: 0,
      defaultValue: 0,
    },
    {
      name: 'saturation' as const,
      bytes: 2,
      defaultValue: 1,
    },
    {
      name: 'value' as const,
      bytes: 2,
      defaultValue: 1,
    },
  ],
};
src/devices/protocols/parsers 中,再到 src/devices/index 中进行引用,如下:    import _ from 'lodash-es';
    import { utils } from '@ray-js/panel-sdk';
    import { lampSchemaMap } from '@/devices/schema';
    import { log } from '@/utils';
    /** 调光器模式 */
    export enum DimmerMode {
        white,
        colour,
        colourCard,
        combination,
    }
    export type DimmerTab = keyof typeof DimmerMode;
    /** 涂抹类型 */
    enum SmearMode {
        all,
        single,
        clear,
    }
    interface ColourData {
        hue: number;
        saturation: number;
        value: number;
    }
    /** 调光器的 Value 类型 */
    export interface DimmerValue {
        colour?: ColourData;
        colourCard?: ColourData;
        combination?: ColourData[];
    }
    /** 涂抹 DP 数据 */
    export interface SmearDataType {
        /** 版本号 */
        version: number;
        /** 模式 (0: 白光, 1: 彩光, 2: 色卡, 3: 组合) */
        dimmerMode: DimmerMode;
        /** 涂抹效果 (0: 无, 1: 渐变) */
        effect?: number;
        /** 灯带 UI 段数 */
        ledNumber?: number;
        /** 涂抹动作 (0: 油漆桶, 1: 涂抹, 2: 橡皮擦) */
        smearMode?: SmearMode;
        /** 彩光色相 */
        hue?: number;
        /** 彩光饱和度 */
        saturation?: number;
        /** 彩光亮度 */
        value?: number;
        /** 白光亮度 */
        brightness?: number;
        /** 白光色温 */
        temperature?: number;
        /** 当前涂抹色是否是彩光 */
        isColour?: boolean;
        /** 点选类型(0: 连续,1: 单点) */
        singleType?: number;
        /** 当次操作的灯带数 */
        quantity?: number;
        /** 组合类型 */
        combineType?: number;
        /** 颜色组合 */
        combination?: ColourData[];
        /** 编号 */
        indexs?: Set<number>;
    }
    const { toFixed, generateDpStrStep, numToHexString } = utils;
    const { paint_colour_data } = lampSchemaMap;
    function nToHS(value = 0, num = 2) {
        return numToHexString(value || 0, num);
    }
    /** 转化为 Number */
    function toN(n: any) {
        return +n || 0;
    }
    function avgSplit(str = '', num = 1) {
        const reg = new RegExp(`.{1,${num}}`, 'g');
        return str.match(reg) || [];
    }
    function sToN(str = '', base = 16) {
        return parseInt(str, base) || 0;
    }
    const _defaultValue = {
        version: 0,
        dimmerMode: DimmerMode.colour,
        effect: 0,
        smearMode: SmearMode.all,
        hue: 0,
        saturation: 1000,
        value: 1000,
        brightness: 1000,
        temperature: 1000,
        ledNumber: 20,
    } as SmearDataType;
    export default class SmearFormater {
        uuid: string;
        defaultValue: SmearDataType;
        constructor(uuid = paint_colour_data.code, defaultValue = _defaultValue) {
            this.uuid = uuid;
            this.defaultValue = defaultValue;
        }
        parser(val = ''): SmearDataType {
            const validVal = (val || '').slice(0, 1) === '0';
            if (!val || typeof val !== 'string' || !validVal) {
                console.warn(paint_colour_data.code, 'DP 数据有问题,无法解析', val);
                return this.defaultValue;
            }
            const step = generateDpStrStep(val);
            const version = toN(step(2).value); // 版本号
            const dimmerMode = toN(step(2).value); // 调节模式
            const effect = toN(step(2).value); // 调节效果
            const ledNumber = toN(step(2).value); // 段数
            const result = {
                version,
                dimmerMode,
                effect,
                ledNumber,
            } as SmearDataType;
            if (dimmerMode === DimmerMode.white) {
                // 白光
                result.smearMode = toN(step(2).value); // 调节动作
                result.brightness = toN(step(4).value); // B 亮度
                result.temperature = toN(step(4).value); // T 色温
            } else if ([DimmerMode.colour, DimmerMode.colourCard].includes(dimmerMode)) {
                // 彩光/色卡
                const smearMode = toN(step(2).value); // 调节动作
                result.smearMode = smearMode;
                result.hue = toN(step(4).value); // H
                result.saturation = toN(step(4).value); // S
                result.value = toN(step(4).value); // V
                if ([SmearMode.clear, SmearMode.single].includes(smearMode)) {
                    const singleDataStr = toFixed(toN(step(2).value).toString(2), 8); // 段选
                    const singleType = sToN(singleDataStr.slice(0, 1), 2);
                    result.singleType = singleType; // 段选 => 0:连续;1:不连续;
                    result.quantity = sToN(singleDataStr.slice(1), 2); // 段选 => 连续时,无意义;不连续时,代表段的总数
                    let indexItem = step(2);
                    const indexs = new Set<number>();
                    while (!indexItem.done) {
                        indexs.add(toN(indexItem.value - 1));
                        indexItem = step(2);
                    }
                    if (indexItem.done) {
                        indexItem.value !== null && indexs.add(toN(indexItem.value - 1));
                    }
                    result.indexs = indexs;
                }
            } else if (dimmerMode === DimmerMode.combination) {
                result.smearMode = SmearMode.all;
                // 颜色组合
                result.combineType = toN(step(2).value);
                const getHSV = () => {
                    const hue = toN(step(4).value);
                    const saturation = toN(step(4).value);
                    const lastItem = step(4);
                    const value = toN(lastItem.value);
                    return { hue, saturation, value, done: lastItem.done };
                };
                result.combination = [];
                let _done = false;
                while (!_done) {
                    const { hue, saturation, value, done } = getHSV();
                    const res = { hue, saturation, value };
                    result.combination.push(res);
                    _done = done;
                }
            }
            log(result, 'SmearDataType result');
            return result;
        }
        formatter(_data: SmearDataType): string {
            const data = {
                ...this.defaultValue,
                ..._data,
            };
            const {
                version = 0,
                dimmerMode = DimmerMode.colour,
                effect = 0,
                ledNumber = 20,
                smearMode = SmearMode.all,
                hue = 0,
                saturation = 0,
                value = 0,
                brightness = 0,
                temperature = 0,
                combineType = 0,
                combination = [],
            } = data;
            // 白光不支持渐变
            const _effect = nToHS(dimmerMode === DimmerMode.white ? 0 : effect);
            let result = `${nToHS(version)}${nToHS(dimmerMode)}${_effect}${nToHS(ledNumber)}`;
            if (dimmerMode === DimmerMode.white) {
                // 白光
                result += `${nToHS(smearMode)}${nToHS(brightness, 4)}${nToHS(temperature, 4)}`;
            } else if ([DimmerMode.colour, DimmerMode.colourCard].includes(dimmerMode)) {
                // 彩光/色卡
                const isClear = smearMode === SmearMode.clear;
                if (isClear) {
                    // 橡皮擦关灯下发颜色全部为 0
                    const _hue = 0;
                    const _saturation = 0;
                    const _value = 0;
                    result += `${nToHS(smearMode)}${nToHS(_hue, 4)}${nToHS(_saturation, 4)}${nToHS(_value, 4)}`;
                } else {
                    result += `${nToHS(smearMode)}${nToHS(hue, 4)}${nToHS(saturation, 4)}${nToHS(value, 4)}`;
                }
                if ([SmearMode.single, SmearMode.clear].includes(smearMode)) {
                    const { singleType = 1, indexs = new Set() } = data;
                    const quantity = indexs.size;
                    const singleDataStr = `${nToHS(
                        parseInt(`${singleType}${toFixed(quantity.toString(2), 7)}`, 2)
                    )}`;
                    const indexsStr = `${[...indexs].reduce((acc, cur) => acc + nToHS(cur + 1), '')}`;
                    result += `${singleDataStr}${indexsStr}`;
                }
            } else if (dimmerMode === DimmerMode.combination) {
                // 组合
                const colors = combination.map(
                    item => `${nToHS(item.hue, 4)}${nToHS(item.saturation, 4)}${nToHS(item.value, 4)}`
                );
                result += `${nToHS(combineType)}${colors.join('')}`;
            }
            log(new SmearFormater().parser(result), 'SmearDataType formatter');
            return result;
        }
    }
src/devices/protocols/parsers/index.ts 集中导出处理。import dpParser from "./parsers";
import { lampSchemaMap } from "@/devices/schema";
const { dreamlightmic_music_data, paint_colour_data } = lampSchemaMap;
export const protocols = {
    [dreamlightmic_music_data.code]: dpParser.micMusicTransformer,
    [paint_colour_data.code]: dpParser.smearFormatter,
};