本文档面向已经了解 面板小程序开发
的开发者,你需要充分的了解什么是面板小程序
产品功能
若您对上述概念有所疑问,我们推荐你去看一些预备知识。如果您已了解,可跳过本文。
面板
是运行在 智能生活 App
、OEM App(涂鸦定制 App)
上的界面交互程序,用于控制 智能设备
的运行,展示 智能设备
实时状态。产品
将 面板
与 智能设备
联系起来,产品描述了其具备的功能、在 App 上面板显示的名称、智能设备拥有的功能点等。智能设备
是搭载了 涂鸦智能模组
的设备,通常在设备上都会贴有一张二维码,使用 智能生活 App
扫描二维码,即可在 App
中获取并安装该设备的控制 面板
。产品
、面板
、设备
之间的关系可参考下图。照明灯带模板使用 SDM(Smart Device Model)
开发,关于 SDM
相关可以 查看 SDM 文档
产品名称:照明幻彩灯带三路灯
根据支持的白光、彩光 dp 不同,照明中灯分为一路至五路。
其含义为:
1.五路
- 支持彩光 colour_data
、白光亮度 bright_value
、白光色温 temp_value
2.四路
- 支持彩光、白光亮度
3.三路
- 支持彩光
4.二路
- 支持白光亮度、白光色温
5.一路
- 支持白光亮度
开关
:点击开关按钮通过 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 请查看具体协议。
首先需要创建一个照明类产品,定义产品有哪些功能点,然后面板中再根据这些功能点一一实现。
进入IoT 平台,点击左侧产品菜单,产品开发,创建产品,选择标准类目 --> 照明 --> 幻彩灯带 --> 幻彩灯带三路
:
注意 ⚠️:本模版只支持幻彩灯带三路,其它路可按逻辑自行适配
🎉 在这一步,我们创建了一个名为 PublicLamp
的照明灯带产品。
这部分我们在 小程序开发者
平台上进行操作,注册登录 小程序开发者平台。
拉取项目
git clone https://github.com/Tuya-Community/tuya-ray-demo.git
进入照明灯带模板,安装依赖并启动项目
cd ./examples/panel-lamp-strip
打开 IDE 并点击导入
选择下载的路径并导入
并关联到已经创建的面板小程序与产品。
绑定具体设备即可,IDE 会自动安装依赖并构建项目。
上面的步骤我们已经初始化好了一个面板小程序
的开发模板,下面我们介绍下工程目录。
├── 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 #智能设备类型定义文件
IDE
生成 SDM schema
到项目。生成 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",
},
// ...
];
首页
调节色盘下发彩光调色分析需求:
开关按钮
。switch_led
下发的值展示不同按钮图片。开关
组件触发事件,上报「开」「关」的 dp 值。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>
);
};
首页
首页根据是否有彩光、白光 dp 显示对应 tab 页,当前模版支持 3 路调光功能
,彩光主要以 HSV 模型,将 hue(色调)、saturation(饱和度)、value(明度)三个数据整合成 paint_colour_data
(可以查看 iot 创建方案的 dp
具体协议-也可查看具体(代码解析)[https://github.com/Tuya-Community/tuya-ray-demo/tree/main/examples/panel-lamp-strip/src/devices/protocols/parsers/])
dp 进行下发。paint_colour_data
dp 只能记忆最近一次的灯光数据,所以需要将灯带的每个灯珠颜色值进行云端存储,每当 dp 变换时进行云端灯珠颜色同步。情景功能
dreamlight_scene_mode
进行下发音乐律动
dreamlightmic_music_data
进行下发更多
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,
},
],
};
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,
};