本文档面向已经了解 面板小程序开发 的开发者,您需要充分了解什么是 面板小程序 和 产品功能。若对上述概念有所疑问,涂鸦推荐您了解以下预备知识。如已了解,可跳过本文。
面板是 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,
};