面板
作为 IoT 智能设备在 App 终端上的产品形态,创建产品
之前,首先来了解一下什么是面板
,以及和产品
、设备
之间的关系。
面板
是运行在智能生活 App
、OEM App(涂鸦定制 App)
上的界面交互程序,用于控制 智能设备
的运行,展示 智能设备
实时状态。产品
将 面板
与 智能设备
联系起来,产品描述了其具备的功能、在 App 上面板显示的名称、智能设备拥有的功能点等。智能设备
是搭载了 涂鸦智能模组
的设备,通常在设备上都会贴有一张 二维码
,使用 智能生活 App
扫描二维码,即可在 App 中获取并安装该设备的控制 面板
。下图描述了产品、面板和设备三者之间的关系
由于产品定义了面板和设备所拥有的功能点,所以在开发一个智能设备面板之前,我们首先需要创建一个产品,定义产品有哪些功能点,然后面板中再根据这些功能点一一实现。
这部分我们在 IoT 平台上进行操作,注册登录 IoT 平台:
创建完成产品后,进入功能定义
页面,这里列出了空调类目下可选的标准功能点,这里我们点击全部选择
,点击确定完成产品初始功能点设置:
现在我们已经有了一个产品,并且功能点已设置完成,接下来就进入面板小程序
的开发流程。前往注册登录涂鸦小程序开发者平台,创建我们的小程序项目。
点击新建,输入小程序名称"万能面板",小程序类型选择 面板小程序
,面板类型选择公版,点击确定完成创建:
安装并打开小程序 IDE 工具(前往下载小程序 IDE)
使用涂鸦 IoT 平台账号登录 IDE:
点击新建,输入项目名称
:万能面板,关联智能小程序
选择第 3 步小程序平台创建的小程序,关联产品
选择第 1 步在 IoT 平台创建的产品:
点击下一步选择模版,选择 小程序面板模版 ReduxToolkit
,生成 Ray
面板项目(Ray
类似 Taro
,是一个多端研发框架,编写一套代码编译到多端)
进入项目后,需要使用 智能生活 App
授权登录,
登录后,项目会重新编译加载。
涂鸦小程序 IDE 工具
导入小程序后会自动安装依赖,并实时编译运行。
如果出现Error(MiniKit 不存在指定的版本 2.3.3)
类似的错误,点击环境配置 -> Kit 管理,选择推荐的版本即可:
此时控制台 console 会提示 deviceId 错误的信息,这是因为我们还没有关联产品,点击工具栏打开 面板工具
:
进入面板工具后,点击添加虚拟设备,然后使用 智能生活 App
扫描二维码:
扫描完成后,项目会拉取产品信息并重新加载项目,即可拉取出设备信息
选择产品后,
面板小程序开发主要围绕 IoT 平台、小程序开发平台、小程序 IDE 之间进行,用一张图来概括整体流程:
以上完成了一个面板小程序的创建流程,下面进入到实际代码的开发教程。
首先了解项目的目录结构,点击工具栏在 vscode 打开项目代码:
然后在 vscode 中可以看到项目代码的文件目录结构:
编写代码主要在 pages 文件夹中进行,创建你的页面代码,然后将路由地址配置到 src/routes.config.ts
文件中:
其他文件的说明,会在下文开发步骤中顺路提到。
查看 SDM 智能设备模型文档
在 IDE 中点击 在 vscode 打开
按钮,打开 vscode 编辑器,
在 src/app.tsx 中提供一个 SdmProvider 用来初始化设备信息:
// 创建 SDM 智能设备模型
const value = new SmartDeviceModel({ deviceId });
value.init(); // 执行初始化
// 初始化完成事件
value.onInitialized(() => {
// 初始化完成后可以进行设备信息获取和操作
// 如 value.getDpSchema()
// ....
});
在 src/app.tsx 应用根组件设置如下,使用 SdmProvider
将智能设备模型实例放在 context 中:
import {
SdmProvider,
SmartDeviceModel,
SmartGroupModel,
} from "@ray-js/panel-sdk";
import { initPanelEnvironment } from "@ray-js/ray";
initPanelEnvironment({ useDefaultOffline: true });
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
model: null,
};
}
onLaunch(object) {
const { query = {} } = object;
const { groupId, deviceId } = query;
if (groupId || deviceId) {
value = groupId
? new SmartGroupModel({ groupId })
: new SmartDeviceModel({ deviceId });
value.init();
value.onInitialized(() => {
this.setState({
model: value,
});
});
}
}
render() {
if (this.state.model) {
return (
<SdmProvider value={this.state.model}>
{this.props.children}
</SdmProvider>
);
}
}
}
修改 src/pages/home/index.tsx
文件,输入以下代码:
import React from "react";
import { useDpSchema, useProps } from "@ray-js/panel-sdk";
import { View } from "@ray-js/ray";
export default () => {
// 项目启动时,会自动拉取 IoT 平台 productId 对应的产品 schema 信息
const dpSchema = useDpSchema();
// 从设备模型中读取 dpState 数据
const dpState = useProps(state => state); // 获取所有dpState
const switch = useProps(state => state.switch); // 获取dpCode为switch的值
console.log("dpSchema", dpSchema); // 打印查看 dpSchema 内容
console.log("dpState", dpState); // 打印查看 dpState 内容
return <View>hello world</View>;
};
注意 useProps
使用时,建议精确到读取哪个 dp,例如精确到读取 switch state => state.switch
,只有当 switch
变化时组件才会重新渲染
查看 IDE 调试 console,可以看到打印出 devInfo
内容:
代码中获取 devInfo:
import { useDevInfo } from "@ray-js/panel-sdk";
const devInfo = useDevInfo();
console.log("devInfo:", devInfo);
在 IDE
调试器 console
面板中默认可以看到有很多输出,其中最重要的就是 devInfo
,即设备信息(Device Information
)
需要了解 devInfo
中几个重要的属性,
codeIds
: 一个对象,key 是 dpCode
,值是 dpId
devId
: 设备的 id
,虚拟设备会以 vdevo
开头deviceOnline
: 设备是否在线dps
: 一个对象,key
是 dpId
,值是 DP
的状态idCodes
: 一个对象,与 codeIds
相反panelConfig
: 面板配置,其中 bic
是云功能配置productId
: 当前设备绑定的产品schema
: 产品的功能点定义,其中描述了 DP
的 code
、类型、值范围属性、icon
图标state
: 一个对象,key
是 dpCode
,值是 DP
的状态ui
: 面板的 uiId
通常代码中获取 devInfo 是通过 useDevInfo
API 来读取。(本文中的 DP
即 功能点
,来自 IoT 平台功能定义)
使用 useDpSchema
可以获取到产品的功能定义
:
import { useDpSchema, useProps } from "@ray-js/panel-sdk";
const dpSchema = useDpSchema(); // 功能点定义
const dpState = useProps((state) => state); // 设备功能点状态
遍历 schema
可以获取到产品所有的 DP
功能点及属性:
schema.map((item) => {
// item.code
// item.property
// ...
return <Text>{item.name}</Text>;
});
这里根据 item.property.type
就可以判断并渲染不同类型 DP
功能点的 UI
展示
Ray 开发样式使用 less 语言,支持 css module,新建文件 index.module.less
,编写内容例如:
src/pages/home/index.module.less
.container {
border-radius: 24rpx;
background-color: #fff;
margin-bottom: 24rpx;
}
rpx
单位是 Ray 框架提供的特有单位,能够做到不同设备上的自适应。
在代码中使用样式:
import React from "react";
import { useDpSchema } from "@ray-js/panel-sdk";
import { View } from "@ray-js/components";
import styles from "./index.module.less"; // 注意样式引入方式
export default () => {
const dpSchema = useDpSchema();
// 添加 className
return <View className={styles.container}>hello world</View>;
};
DP
,分别是布尔型、数值型、枚举型、故障型、字符型、透传型。DP
有不同的数值类型和范围,在面板渲染时需要根据类型和范围渲染 UI 视图。通过 console 日志可以了解到 schema 的数据结构:
详细请参考文档 自定义功能
src/home/index.tsx
编写输入以下代码:
import React from "react";
import { useDevInfo, useDpSchema, useProps } from "@ray-js/panel-sdk";
import { View } from "@ray-js/components";
export default () => {
const dpSchema = useDpSchema();
const dpState = useProps((state) => state);
console.log("dpSchema", dpSchema);
console.log("dpState", dpState);
return (
<View>
{Object.keys(dpSchema || {}).map((dpCode) => {
// 遍历渲染每个功能点
return (
<View key={dpCode}>
{dpCode}: {dpState[dpCode]}
</View>
);
})}
</View>
);
};
重新编译,可以看到 IDE
左边面板中显示了所有 DP
的 code
及对应设备状态 :
在 IDE console 面板中,可以看到 bool
类型功能点的数据结构,例如:
{
dptype: "obj";
id: "39";
type: "bool";
}
使用工具包 @ray-js/components-ty
提供的开关组件 TySwitch
来渲染,执行以下命令,安装 Ray 提供的扩展组件库:
yarn add @ray-js/components-ty
继续编写代码,输入以下内容:
import React from "react";
import { useDpSchema } from "@ray-js/panel-sdk";
import { TySwitch } from "@ray-js/components-ty";
import { View } from "@ray-js/components";
const BoolCard = ({ dpCode }) => {
const dpValue = useProps((state) => state[dpCode]);
const actions = useActions();
return (
<View>
{dpCode}:{" "}
<TySwitch
checked={dpValue}
onChange={() => {
actions[dpCode].toggle();
}}
/>
</View>
);
};
export default () => {
const dpSchema = useDpSchema();
const dpState = useProps((state) => state);
console.log("dpSchema", dpSchema);
return (
<View>
{Object.keys(dpSchema || {}).map((dpCode) => {
const props = dpSchema[dpCode];
// 判断如果是 bool 类型,返回 TySwitch
if (props.type === "bool") {
return <BoolCard dpCode={dpCode} />;
}
return (
<View key={dpCode}>
{dpCode}: {dpState[dpCode]}
</View>
);
})}
</View>
);
};
编译后,IDE 中渲染界面如下,看到所有的 bool 型 DP 已经成功渲染出了开关组件
:
DP 在功能定义时,在产品信息中已经有相应的多语言文本,这里使用 src/i18n
下的 Strings 工具获取 DP
文本,输入以下代码:
import React from "react";
import { useDpSchema, useProps } from "@ray-js/panel-sdk";
import { TySwitch } from "@ray-js/components-ty";
import { View } from "@ray-js/components";
import Strings from "@/i18n";
export default () => {
const dpSchema = useDpSchema();
const dpState = useProps((state) => state);
console.log("dpSchema", dpSchema);
return (
<View>
{Object.keys(dpSchema || {}).map((dpCode) => {
const props = dpSchema[dpCode];
if (props.type === "bool") {
// 使用 Strings.getDpLang 方法获取多语言
return (
<View>
{Strings.getDpLang(dpCode)}:{" "}
<TySwitch checked={dpState[dpCode]} />
</View>
);
}
return (
<View key={dpCode}>
{dpCode}: {dpState[dpCode]}
</View>
);
})}
</View>
);
};
再次编译查看 IDE 渲染结果,可以看到所有的 bool 类型 DP 文本都已显示出中文:
这样就完成了 Bool
类型 DP
功能点的 UI 渲染。
上文介绍了如何编写代码渲染 DP 点,但是要控制设备运行,还需要进行 DP 下发
, 使用 SDM 智能设备模型
提供的 hooks useActions
下发能力,例如:
import { useActions } from "@ray-js/panel-sdk";
const actions = useActions();
// 下发给对应设备,actions.xxx 中的 xxx 是 dpCode,set 是下发指令的方法,传入dpValue调用即可下发指令
actions.switch.set(true); // 下发 switch 为 true
actions.switch.on(); // 打开当前布尔类型功能点
actions.switch.off(); // 关闭当前布尔类型功能点
actions.switch.toggle(); // 在 true 和 false 之间切换 bool 值
现在来示例开关功能点的 DP 下发
操作,编写代码:
首先实现 Bool 下发上报组件:
import React from "react";
import { useActions, useProps } from "@ray-js/panel-sdk";
import { View } from "@ray-js/components";
const BoolCard = ({ dpCode }) => {
const actions = useActions();
const dpValue = useProps((state) => state[dpCode]);
return (
<View>
{dpCode}:{" "}
<TySwitch checked={dpValue} onChange={() => actions[dpCode].toggle()} />
</View>
);
};
对于 bool 类型的 DP 功能点,actions.xxx
会有以下几个工具方法:
如 switch 开关 DP 点的 actions:
actions.switch.on; // switch 功能点下发 true
actions.switch.off; // switch 功能点下发 false
actions.switch.toggle; // switch 功能点下发 false
export default () => {
const schema = useDpSchema();
const dpState = useProps((state) => state);
const actions = useActions();
return (
<View>
{Object.keys(schema || {}).map((dpCode) => {
const props = schema[dpCode];
// 判断如果是 bool 类型,返回 TySwitch
if (props.type === "bool") {
return <BoolCard key={dpCode} dpCode={dpCode} />;
}
return (
<View key={dpCode}>
{dpCode}: {dpState[dpCode]}
</View>
);
})}
</View>
);
};
IDE 编译后运行,使用虚拟设备插件进行调试,在调试器 Virtual Device
面板中,选择可视化面板
,点击调试器中的开关,可以看到左边模拟器中的开关收到了 DP 上报,并做出了相同的切换动作。
enum
类型 DP
点的 schema
属性配置如下:
{
dptype: "obj";
id: "20";
range: ["cancel", "1h", "2h", "3h", "4h", "5h", "6h"];
type: "enum";
}
range 中的即枚举条目,使用 @ray-js/components-ty
中的 ActionSheet 弹窗组件
+ Cell 列表组件
进行渲染,编写以下代码:
首先实现 Enum 枚举展示控制组件:
import React from "react";
import { useActions, useProps } from "@ray-js/panel-sdk";
import { View } from "@ray-js/components";
import Strings from "@/i18n";
const EnumCard = ({ dpCode }) => {
const actions = useActions();
const dpValue = useProps((state) => state[dpCode]);
const [showEnumDp, setShowEnumDp] = useState(null);
return (
<View>
{Strings.getDpLang(dpCode)}:
<Button onClick={() => setShowEnumDp(dpCode)}>
{Strings.getDpLang(dpCode, dpState[dpCode])}
</Button>
<TyActionsheet
header={Strings.getDpLang(dpCode)}
show={showEnumDp === dpCode}
onCancel={() => setShowEnumDp(null)}
>
<View style={{ overflow: "auto", height: "200rpx" }}>
<TyCell.Row
rowKey="title"
dataSource={props.range.map((item) => ({
title: Strings.getDpLang(dpCode, item),
onClick: () => {
actions[dpCode].set(item);
},
}))}
/>
</View>
</TyActionsheet>
</View>
);
};
对于 enum 类型的 DP 功能点,actions.xxx
会有以下几个工具方法:
如 work_mode 开关 DP 点的 actions:
actions.work_mode.set("white"); // work_mode 下发 white
actions.work_mode.next(); // 切换下一个枚举
actions.work_mode.prev(); // 切换下一个枚举
actions.work_mode.random(); // 切换到随机一个枚举值
遍历 schema 渲染
import React, { useState } from "react";
import { useDpSchema, useProps } from "@ray-js/panel-sdk";
import { TyActionsheet, TyCell, TySwitch } from "@ray-js/components-ty";
import { Button, ScrollView, View } from "@ray-js/components";
import Strings from "@/i18n";
export default () => {
const schema = useDpSchema();
const dpState = useProps((state) => state);
console.log("schema", schema);
const [showEnumDp, setShowEnumDp] = useState(null);
return (
<View>
{Object.keys(schema).map((dpCode) => {
const props = schema[dpCode];
// 这里为了文章篇幅,忽略其他类型的。本地代码可复制上节 bool 类型的到这里
if (props.type === "enum") {
return <EnumCard key={dpCode} dpCode={dpCode} />;
}
return (
<View key={dpCode}>
{dpCode}: {dpState[dpCode]}
</View>
);
})}
</View>
);
};
点击按钮,触发 ActionSheet 弹窗
,弹窗中使用 Cell 列表组件
渲染多个枚举项,效果如下:
string
类型 DP
点的 schema
属性配置如下:
{
dptype: "obj";
id: "20";
type: "string";
}
首先实现 Input 输入组件:
import React from "react";
import { useActions, useProps } from "@ray-js/panel-sdk";
import { Input } from "@ray-js/components";
import Strings from "@/i18n";
const InputCard = ({ dpCode }) => {
const actions = useActions();
const dpValue = useProps((state) => state[dpCode]);
const [showEnumDp, setShowEnumDp] = useState(null);
return (
<Input
value={dpState[dpCode]}
onInput={(event) => {
actions[dpCode].set(event.detail.value);
}}
/>
);
};
对于 string 类型的 DP 功能点,actions.xxx
会有以下几个工具方法:
如 name 名称 DP 点的 actions:
actions.name.set("my device"); // name 下发 my device
遍历 schema 渲染
import { useDpSchema, useActions } from "@ray-js/panel-sdk";
import { Input } from "@ray-js/components";
export default () => {
const schema = useDpSchema();
const dpState = useProps((state) => state);
console.log("schema", schema);
return (
<View>
{Object.keys(schema || {}).map((dpCode) => {
const props = schema[dpCode];
// 这里为了文章篇幅,忽略其他类型的。本地代码可复制上节类型的到这里
if (props.type === "string") {
return <InputCard value={dpState[dpCode]} />;
}
return (
<View key={dpCode}>
{dpCode}: {dpState[dpCode]}
</View>
);
})}
</View>
);
};
Raw
类型 DP
一般不做渲染,如果需要可以按照 String
类型进行处理
value
类型 DP
点的 schema
属性配置如下:
{
dptype: "obj";
id: "18";
max: 100;
min: 0;
scale: 0;
step: 1;
type: "value";
unit: "%";
}
首先实现 Input 输入组件:
其中定义了数值范围和单位,使用 Slider
组件进行渲染:
import React from "react";
import { useActions, useProps } from "@ray-js/panel-sdk";
import { Slider } from "@ray-js/components";
import Strings from "@/i18n";
const ValueCard = ({ dpCode }) => {
const actions = useActions();
const dpValue = useProps((state) => state[dpCode]);
return (
<View>
{Strings.getDpLang(dpCode)}:
<Slider
step={props?.step}
max={props?.max}
min={props?.min}
value={dpState[dpCode]}
onChange={(event) => {
actions[dpCode].set();
}}
/>
</View>
);
};
对于 value 类型的 DP 功能点,actions.xxx
会有以下几个工具方法:
如 add_ele 用电量 DP 点的 actions:
actions.add_ele.set(1000); // add_ele 下发 1000
actions.add_ele.inc(); // 根据当前功能点步长 step 进行递增
actions.add_ele.dec(); // 根据当前功能点步长 step 进行递减
遍历 schema 渲染
import { useDpSchema, useActions, useProps } from "@ray-js/panel-sdk";
import { Slider } from "@ray-js/components";
export default () => {
const schema = useDpSchema();
const dpState = useProps((state) => state);
console.log("schema", schema);
return (
<View>
{Object.keys(schema).map((dpCode) => {
const props = schema[dpCode];
// 这里为了文章篇幅,忽略其他类型的。本地代码可复制上节类型的到这里
if (props.type === "value") {
return <ValueCard dpCode={dpCode} />;
}
return (
<View key={dpCode}>
{dpCode}: {dpState[dpCode]}
</View>
);
})}
</View>
);
};
Slider
组件是一个滑动条组件,可以根据数值范围进行约束,IDE 渲染如下,所有 value 类型 DP 都渲染出了滑动条:
bitmap
类型一般用作故障上报,属性配置如下:
{
dptype: "obj";
id: "22";
label: ["sensor_fault", "temp_fault"];
maxlen: 2;
type: "bitmap";
}
故障型 DP
使用弹窗渲染,这里使用 @ray-js/ray-components-plus
提供的 Notification
来实现:
import { useDpSchema, useActions, useProps } from "@ray-js/panel-sdk";
import { Notification } from "@ray-js/ray-components-plus";
export default () => {
const schema = useDpSchema();
const dpState = useProps((state) => state);
console.log("schema", schema);
// 弹窗消息在 useEffect 中进行
useEffect(() => {
Object.keys(schema).forEach((dpCode) => {
const props = schema[dpCode];
if (props.type === "bitmap") {
Notification.show({
message: Strings.getFaultStrings(dpCode, dpState[dpCode]),
icon: "warning",
});
}
});
}, [schema, dpState]);
return (
<View>
{Object.keys(schema).map((dpCode) => {
const props = schema[dpCode];
// 这里为了文章篇幅,忽略其他类型的。本地代码可复制上节类型的到这里
return (
<View key={dpCode}>
{dpCode}: {dpState[dpCode]}
</View>
);
})}
</View>
);
};
bitmap
类型通常用来做故障上报,以弹窗消息提示,所以需要在 useEffect 中遍历 schema 找到 bitmap 类型并弹窗提醒。
以上 schema 中是家电类目自带的标准 DP 点,我们还可以增加自定义的 DP,打开 IoT 平台产品详情 -> 功能定义 -> 自定义功能
一栏,
点击添加功能,新建一个 bool 型 DP:
然后回到 IDE 中,点击编译
按钮刷新面板,可以看到渲染出了我们新增加的一个 DP 点:
面板运行时需要获取设备信息
来显示设备的运行状态
,在开发阶段,如果没有真实的智能硬件设备
,我们可以借助虚拟设备
来辅助调试,可以达到和真实设备一样的效果。虚拟设备和真实设备对于面板小程序来说可以是等效的,其关系如下图所示:
在使用虚拟设备前,请先确认已授权登录态,点击右上角登录,使用智能生活 App
扫描二维码授权登录。
登录后进入万能面板
项目,点击调试工具 Virtual Device
,使用智能生活 App
扫码创建虚拟设备:
虚拟设备面板中分为 3 个区块,左边是兜底面板用于展示当前产品的 DP
功能点,右侧是虚拟设备 DP
控制列表,可以改变 DP
状态然后点击上报按钮,下方是 Log
面板用于输出 MQTT
日志。点击"可视化面板",切换到兜底面板视图,与控制面板功能相同,更加直观的展现产品功能点。
下面来调试我们刚编写的代码:
在虚拟设备界面右侧 -> 设备信息,点击 deviceId
右侧的复制按钮
点击编译参数设置,按照格式填入真机调试参数。 例如:deviceId=vdevo165398364416684
设置好编译参数后,点击真机调试
按钮(真机需要前往下载智能生活 App)
使用智能生活 App
扫描二维码,打开并进入小程序面板,可以在真机上进行调试万能面板。
到此你已经熟悉了 6 种 DP
点的渲染及下发,附上万能面板代码开源地址:
经过样式优化后的万能面板展示:
在 IDE
中完成调试后,点击上传源码按钮,输入版本号及说明,点击上传预览包
上传完成后,在小程序开发者平台版本管理中可以看到已经上传的版本列表
在上传完成源码包后,在小程序开发者平台 -> 版本管理中,选择需要预发布的版本,设为体验版:
点击白名单页面,添加自己智能生活 App
的账号:
添加完后,可以点击 版本管理 -> 体验二维码,查看小程序码,使用智能生活 App 扫码预览小程序。
非官方主体的面板小程序,在提交审核前需要完善 IoT 平台展示信息。其余审核上线注意项可参考:上线审核。