面板
作为 IoT 智能设备在 App 终端上的产品形态,创建产品
之前,首先来了解一下什么是面板
,以及和产品
、设备
之间的关系。
面板
?是运行在智能生活 App、OEM App(涂鸦定制 App)
?上的界面交互程序,用于控制?智能设备
?的运行,展示?智能设备
?实时状态。产品
?将?面板
?与?智能设备
?联系起来,产品描述了其具备的功能、在 App 上面板显示的名称、智能设备拥有的功能点等。智能设备
?是搭载了?涂鸦智能模组
?的设备,通常在设备上都会贴有一张?二维码
,使用?智能生活 App
?扫描二维码,即可在 App 中获取并安装该设备的控制?面板
。下图描述了产品、面板和设备三者之间的关系
由于产品定义了面板和设备所拥有的功能点,所以在开发一个智能设备面板之前,我们首先需要创建一个产品,定义产品有哪些功能点,然后面板中再根据这些功能点一一实现。
这部分我们在 IoT 平台上进行操作,注册登录?IoT 平台:
创建完成产品后,进入功能定义页面,这里列出了空调类目下可选的标准功能点,这里我们点击全部选择
,点击确定完成产品初始功能点设置:
?
现在我们已经有了一个产品,并且功能点已设置完成,接下来就进入面板小程序
的开发流程。前往注册登录涂鸦小程序开发者平台,创建我们的小程序项目。
点击新建,输入小程序名称"万能面板",小程序类型选择?面板小程序
,面板类型选择公版,点击确定完成创建:
?
安装并打开小程序 IDE 工具(前往下载小程序 IDE)
使用涂鸦 IoT 平台账号登录 IDE 后,使用智能生活 App 扫码授权 IDE:
点击新建,输入项目名称
:万能面板,关联智能小程序
选择第 3 步小程序平台创建的小程序,关联产品
选择第 1 步在 IoT 平台创建的产品:
点击下一步选择模版,选择?App 面板开发 Ray 应用(jotai)
,生成?Ray
?面板项目(Ray
?类似?Taro
,是一个多端研发框架,编写一套代码编译到多端)
创建后,点击工具栏在 vscode 打开项目代码:
涂鸦小程序 IDE 工具
导入小程序后会自动安装依赖,并实时编译运行。
如果出现Error(MiniKit 不存在指定的版本 2.3.3)
类似的错误,点击环境配置 -> Kit 管理,选择推荐的版本即可:
?
面板小程序开发主要围绕 IoT 平台、小程序开发平台、小程序 IDE 之间进行,用一张图来概括整体流程:
?
以上完成了一个面板小程序的创建流程,下面进入到实际代码的开发教程。
首先了解项目的目录结构:
编写代码主要在 pages 文件夹中进行,创建你的页面代码,然后将路由地址配置到?src/routes.config.ts
?文件中:
其他文件的说明,会在下文开发步骤中顺路提到。
在 IDE 中点击?在 vscode 打开
?按钮,打开 vscode 编辑器,修改?src/pages/home/index.tsx
?文件,清空内容,输入以下代码:
import React from "react";
import { useDevInfo } from "@ray-js/panel-sdk";
import { useAtomValue } from "jotai";
import { selectDpStateAtom } from "@/atoms";
import { View } from "@ray-js/ray";
export default () => {
// 项目启动时,会自动拉取 IoT 平台 productId 对应的产品信息
const devInfo = useDevInfo() || {};
// 从 jotai 中读取 dpState 数据
const dpState = useAtomValue(selectDpStateAtom) || {};
console.log("devInfo", devInfo); // 打印查看 devInfo 内容
console.log("dpState", dpState); // 打印查看 dpState 内容
return <View>hello world</View>;
};
查看 IDE 调试 console,可以看到打印出?devInfo
?内容:
在?IDE
?调试器?console
?面板中默认可以看到有很多输出,jotai 状态中内置有很多数据,其中最重要的就是?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 是通过?jotai 状态管理
?API 来读取。(本文中的?DP
?即?功能点
,来自 IoT 平台功能定义)
在?devInfo
?中可以获取到产品的功能定义
:
const schema = devInfo.schema; // 功能点定义
const state = devInfo.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 { useDevInfo } from "@ray-js/panel-sdk";
import { View } from "@ray-js/components";
import styles from "./index.module.less"; // 注意样式引入方式
export default () => {
const devInfo = useDevInfo() || {};
// 添加 className
return <View className={styles.container}>hello world</View>;
};
?
DP
,分别是布尔型、数值型、枚举型、故障型、字符型、透传型。DP
?有不同的数值类型和范围,在面板渲染时需要根据类型和范围渲染 UI 视图。通过 console 日志可以了解到 schema 的数据结构:
详细请参考文档?自定义功能
src/home/index.tsx
?编写输入以下代码:
import React from "react";
import { useAtomValue } from "jotai";
import { useDevInfo } from "@ray-js/panel-sdk";
import { selectDpStateAtom } from "@/atoms";
import { View } from "@ray-js/components";
export default () => {
const devInfo = useDevInfo() || {};
const dpState = useAtomValue(selectDpStateAtom) || {};
console.log("devInfo", devInfo);
console.log("dpState", dpState);
return (
<View>
{Object.keys(devInfo.schema || {}).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 { useAtomValue } from "jotai";
import { useDevInfo } from "@ray-js/panel-sdk";
import { selectDpStateAtom } from "@/atoms";
import { TySwitch } from "@ray-js/components-ty";
import { View } from "@ray-js/components";
export default () => {
const devInfo = useDevInfo() || {};
const dpState = useAtomValue(selectDpStateAtom) || {};
console.log("devInfo", devInfo);
const schema = devInfo.schema || {};
return (
<View>
{Object.keys(schema).map(dpCode => {
const props = schema[dpCode];
// 判断如果是 bool 类型,返回 TySwitch
if (props.type === "bool") {
return (
<View>
{dpCode}: <TySwitch checked={dpState[dpCode]} />
</View>
);
}
return (
<View key={dpCode}>
{dpCode}: {dpState[dpCode]}
</View>
);
})}
</View>
);
};
编译后,IDE 中渲染界面如下,看到所有的 bool 型 DP 已经成功渲染出了开关组件
:
DP 在功能定义时,在产品信息中已经有相应的多语言文本,这里使用?src/i18n
?下的 Strings 工具获取?DP
?文本,输入以下代码:
import React from "react";
import { useAtomValue } from "jotai";
import { useDevInfo } from "@ray-js/panel-sdk";
import { selectDpStateAtom } from "@/atoms";
import { TySwitch } from "@ray-js/components-ty";
import { View } from "@ray-js/components";
import Strings from "@/i18n";
export default () => {
const devInfo = useDevInfo() || {};
const dpState = useAtomValue(selectDpStateAtom) || {};
console.log("devInfo", devInfo);
const schema = devInfo.schema || {};
return (
<View>
{Object.keys(schema).map(dpCode => {
const props = schema[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 下发
, 使用?ray
?框架?API
?下发能力,例如:
import { publishDps } from "@ray-js/api";
// 下发给 deviceId 对应设备,dps中的 key 是 dpId,value 是 dpValue。(关于 dps、dpId 的说明可以看 `编写代码` 一节)
publishDps({
deviceId: devInfo.devId,
dps: {
1: true
}
});
现在来示例开关功能点的?DP 下发
?操作,编写代码:
import React from "react";
import { useAtomValue } from "jotai";
import { useDevInfo } from "@ray-js/panel-sdk";
import { selectDpStateAtom } from "@/atoms";
import { TySwitch } from "@ray-js/components-ty";
import { View } from "@ray-js/components";
export default () => {
const devInfo = useDevInfo() || {};
const dpState = useAtomValue(selectDpStateAtom) || {};
const schema = devInfo.schema || {};
const putDeviceData = (code, value) => {
const dpId = schema[code].id;
publishDps({
deviceId: devInfo.devId,
dps: {
[dpId]: value
}
});
};
return (
<View>
{Object.keys(schema).map(dpCode => {
const props = schema[dpCode];
// 判断如果是 bool 类型,返回 TySwitch
if (props.type === "bool") {
return (
<View>
{dpCode}:{" "}
<TySwitch
checked={dpState[dpCode]}
onChange={value => putDeviceData(dpCode, value)}
/>
</View>
);
}
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 列表组件
?进行渲染,编写以下代码:
import React, { useState } from "react";
import { useAtomValue } from "jotai";
import { useDevInfo } from "@ray-js/panel-sdk";
import { selectDpStateAtom } from "@/atoms";
import { TyActionsheet, TyCell, TySwitch } from "@ray-js/components-ty";
import { Button, ScrollView, View } from "@ray-js/components";
import Strings from "@/i18n";
export default () => {
const devInfo = useDevInfo() || {};
const dpState = useAtomValue(selectDpStateAtom) || {};
console.log("devInfo", devInfo);
const schema = devInfo.schema || {};
const [showEnumDp, setShowEnumDp] = useState(null);
return (
<View>
{Object.keys(schema).map(dpCode => {
const props = schema[dpCode];
// 这里为了文章篇幅,忽略其他类型的。本地代码可复制上节 bool 类型的到这里
if (props.type === "enum") {
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)
}))}
/>
</View>
</TyActionsheet>
</View>
);
}
return (
<View key={dpCode}>
{dpCode}: {dpState[dpCode]}
</View>
);
})}
</View>
);
};
点击按钮,触发?ActionSheet 弹窗
,弹窗中使用?Cell 列表组件
?渲染多个枚举项,效果如下:
?
string
?类型?DP
?点的?schema
?属性配置如下:
{
dptype: "obj";
id: "20";
type: "string";
}
字符型可以使用?Input
?组件渲染,实现如下:
import { useAtomValue } from "jotai";
import { useDevInfo } from "@ray-js/panel-sdk";
import { selectDpStateAtom } from "@/atoms";
import { Input } from "@ray-js/components";
export default () => {
const devInfo = useDevInfo() || {};
const dpState = useAtomValue(selectDpStateAtom) || {};
console.log("devInfo", devInfo);
const schema = devInfo.schema || {};
const [showEnumDp, setShowEnumDp] = useState(null);
return (
<View>
{Object.keys(schema).map(dpCode => {
const props = schema[dpCode];
// 这里为了文章篇幅,忽略其他类型的。本地代码可复制上节类型的到这里
if (props.type === "string") {
return <Input 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: "%";
}
其中定义了数值范围和单位,使用?Slider
?组件进行渲染:
import { useAtomValue } from "jotai";
import { useDevInfo } from "@ray-js/panel-sdk";
import { selectDpStateAtom } from "@/atoms";
import { Slider } from "@ray-js/components";
export default () => {
const devInfo = useDevInfo() || {};
const dpState = useAtomValue(selectDpStateAtom) || {};
console.log("devInfo", devInfo);
const schema = devInfo.schema || {};
const [showEnumDp, setShowEnumDp] = useState(null);
return (
<View>
{Object.keys(schema).map(dpCode => {
const props = schema[dpCode];
// 这里为了文章篇幅,忽略其他类型的。本地代码可复制上节类型的到这里
if (props.type === "value") {
return (
<View>
{Strings.getDpLang(dpCode)}:
<Slider
step={props?.step}
max={props?.max}
min={props?.min}
value={dpState[dpCode]}
/>
</View>
);
}
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 { useAtomValue } from "jotai";
import { useDevInfo } from "@ray-js/panel-sdk";
import { selectDpStateAtom } from "@/atoms";
import { Notification } from "@ray-js/ray-components-plus";
export default () => {
const devInfo = useDevInfo() || {};
const dpState = useAtomValue(selectDpStateAtom) || {};
console.log("devInfo", devInfo);
const schema = devInfo.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 平台展示信息。其余审核上线注意项可参考:上线审核。
?