本文档面向已经了解 面板小程序开发 的开发者,你需要充分的了解什么是面板小程序 产品功能 若您对上述概念有所疑问,我们推荐你去看一些预备知识。如果您已了解,可跳过本文。

理解关系

面板作为 IoT 智能设备在 App 终端上的产品形态,创建产品之前,首先来了解一下什么是面板,以及和产品、设备之间的关系。

  1. 面板 是运行在 智能生活 AppOEM App(涂鸦定制 App) 上的界面交互程序,用于控制 智能设备 的运行,展示 智能设备 实时状态。
  2. 产品面板智能设备 联系起来,产品描述了其具备的功能、在 App 上面板显示的名称、智能设备拥有的功能点等。
  3. 智能设备 是搭载了 涂鸦智能模组 的设备,通常在设备上都会贴有一张二维码,使用 智能生活 App 扫描二维码,即可在 App 中获取并安装该设备的控制 面板
  4. 产品面板设备 之间的关系可参考下图。

相关概念

产品名称:通用 wifi 门锁

需求分析

  1. 首页 进行展示安全守护天数、门锁状态显示。
  2. 开门与告警记录。
  3. 临时密码管理、用户管理、门锁状态设置等页面。
  4. 实现远程开门的功能。

产品介绍

Wi-Fi 门锁指通过 Wi-Fi模组实现联网控制的智能门锁。具体功能支持如下

功能

说明

全链路通信加密

App——服务器——Wi-Fi 模组,通信链路全部加密,杜绝通信安全风险

App 远程开门

门锁请求后,App 响应,可以同意开门或拒绝开门

门锁状态显示

App 上可参看门锁的反锁、童锁等状态信息

电量状态显示

电量值或电量状态

开门记录

解锁方式+解锁 id+时间,用户可以通过 App 查询近 12 个月的开门记录,不会丢失数据。支持指纹+密码这样的组合开门

告警记录

低电报警、密码错误、指纹错误、房卡错误、防撬、高温等

用户管理

通过 App 添加用户,与解锁 id 通过 App 手动关联

临时密码管理

最多支持 10 组临时密码,支持创建和删除,临时密码创建后,自动复制,可以在微信、短信中粘贴,临时密码短信支持海外的手机号,可以选择不同的国家码

详细可参考Wi-Fi 门锁接入方案

使用场景

访客在门锁硬件上输入特定的开门密码(厂家自行决定触发方式,可以是指定的物理按钮,或者特殊的开门码,例如 888#)触发门锁远程开门功能,主人能在特定时间内,通过 APP 对该开门请求进行响应,可以同意开门,或拒绝开门

实现流程

交互原型

功能汇总

当前门锁模板必须的功能点:

lock_motor_state,
          unlock_app,
          remote_no_pd_setkey,
          remote_no_dp_key,
        

落锁状态

参数

取值

dpid

55

code

lock_motor_state

type

布尔型(Bool)

mode

可下发可上报(rw)

App 远程解锁

参数

取值

dpid

15

code

unlock_app

type

数值型(Value)

mode

只上报(ro)

function

当发生 app 远程开门的事件后,门锁上报远程开门的记录。使用记录型数据上报

设置新免密远程开门密钥

参数

取值

dpid

49

code

remote_no_pd_setkey

type

透传型(Raw)

mode

可下发可上报(rw)

免密远程开门

参数

取值

dpid

50

code

remote_no_dp_key

type

数值型(Value)

type

透传型(Raw)

mode

可下发可上报(rw)

上面为首页需求必须的功能点,其他功能点这里不做介绍。

首先需要创建一个门锁类产品,定义产品有哪些功能点,然后面板中再根据这些功能点一一实现。

进入IoT 平台,点击左侧产品菜单,产品开发,创建产品,选择标准类目 -> 智能锁 -> 家用门锁PRO:

选择功能点,这里我们只需要默认的标准功能即可。

🎉 在这一步,我们创建了一个名为 PanelLock的家用门锁产品。

开发者平台创建面板小程序

这部分我们在 小程序开发者 平台上进行操作,注册登录 小程序开发者平台

选择模板并运行项目

IDE 中点击新建,选择门锁模板。

导入 IDE,并关联到已经创建的面板小程序与产品。

工程目录

上面的步骤我们已经初始化好了一个面板小程序的开发模板,下面我们介绍下工程目录。

目录结构

├── src
          │ ├── api // 云端API
          │ ├── components
          │ │ ├── LatestLog // 最近一条日志组件
          │ │ ├── RemoteOpenDoor // 远程开门组件
          │ │ ├── StatusBar // 门锁状态栏
          │ │ ├── connect.tsx
          │ │ └── index.ts
          │ ├── config // 常用配置
          │ ├── constant // 放置常用变量
          │ ├── i18n // 多语言
          │ ├── hooks // 常用hooks
          │ ├── models // redux
          │ │ ├── combine.ts // combineReducers
          │ │ ├── configureStore.ts
          │ │ ├── index.ts
          │ │ ├── store.ts
          │ │ └── modules
          │ │ ├── common.ts // 基础 action 及 reducer
          │ ├── pages
          │ │ ├── home // 首页
          │ │ ├── ...
          │ ├── res // 本地图片
          │ ├── utils // 常用工具方法
          │ ├── app.config.ts
          │ ├── app.tsx
          │ ├── composeLayout.tsx // 处理监听子设备添加,解绑,DP变化等
          │ ├── global.config.ts
          │ ├── routes.config.ts // 配置路由
        

需求实现

首页 通过操作 App 进行远程开门

分析需求:

  1. 使用组件 RemoteOpenDoor
  2. 通过 lock_motor_state 获取门锁开启、关闭状态,考虑到门锁设备离线状态,共计三种状态。
  3. 开锁后,通过监听 remote_no_dp_key dp 值获取开锁成功、失败状态

相关代码:

import React, { useMemo, useState, useRef, useEffect } from
          'react';
          import {
          Text,
          View,
          Image,
          showToast,
          hideToast,
          onDpDataChange,
          offDpDataChange,
          showModal,
          } from '@ray-js/ray';
          import { useStore } from 'react-redux';
          import RotateImage from '@ray/rotate-image';

          import Res from '@/res';
          import Strings from '@/i18n';
          import { dpCodes } from '@/config';
          import { formatDpChange } from '@/utils';
          import { remoteOpen as remoteOpenApi } from '@/api';
          import { usePanelStore, useDeviceOnline } from '@/hooks';

          import styles from './index.module.less';

          const { remoteNoDpKey } = dpCodes;

          enum DoorStatus {
          open = 'open',
          close = 'close',
          offline = 'offline',
          }

          /**
          * 开启状态:#F67352
          * 关闭状态:#5EAAFF
          * 离线状态:#A2A2A2
          */
          const statusInfo = {
          open: {
          bg: Res.showOpenImage,
          iconBg: Res.openImage,
          text: Strings.getLang('close'),
          color: '#F67352',
          },
          close: {
          bg: Res.showImage,
          iconBg: Res.lockImage,
          text: Strings.getLang('clickOpen'),
          color: '#5EAAFF',
          },
          offline: {
          bg: Res.outLineImage,
          iconBg: '',
          text: Strings.getLang('offline'),
          color: '#A2A2A2',
          },
          };

          const RemoteOpenDoor = () => {
          const store = useStore();

          const { dpState} = usePanelStore();
          const deviceOnline = useDeviceOnline();

          const { lock_motor_state } = dpState;

          const [loading, setLoading] = useState(false);
          const timer = useRef(null);
          const loadRef = useRef(loading);

          const status = useMemo(
          () =>
          deviceOnline ? (lock_motor_state ? DoorStatus.open : DoorStatus.close) : DoorStatus.offline,
          [deviceOnline, lock_motor_state]
          );

          const text = useMemo(() => {
          let _text = statusInfo[status].text;
          if (deviceOnline) {
          _text = loading
          ? lock_motor_state
          ? Strings.getLang('locking')
          : Strings.getLang('unlocking')
          : statusInfo[status].text;
          }
          return _text;
          }, [deviceOnline, lock_motor_state, loading, status]);

          useEffect(() => {
          onDpDataChange(dpDataChangeHandle);
          return () => {
          offDpDataChange(dpDataChangeHandle);
          };
          }, []);

          useEffect(() => {
          loadRef.current = loading;
          }, [loading]);

          const dpDataChangeHandle = data => {
          const { getState } = store;
          const { devInfo: _devInfo } = getState();
          const _dpState = formatDpChange(_devInfo.arrSchema, data.dps);
          if (Object.keys(_dpState).length > 10) return;
          const remoteOpenResult = _dpState[remoteNoDpKey];
          if (remoteOpenResult) {
          setLoading(false);
          clearTimer();
          const result = remoteOpenResult.slice(0, 2);
          if (result === '00') {
          showToast({ title: Strings.getLang('openSuccess'), icon: 'success' });
          } else {
          showToast({ title: Strings.getLang('openError'), icon: 'error' });
          }
          }
          };

          const remoteOpen = async () => {
          if (loading) return;
          if (!deviceOnline) {
          showModal({
          title: Strings.getLang('offline'),
          content: Strings.getLang('outLineTip'),
          confirmText: Strings.getLang('confirm'),
          showCancel: false,
          });
          return;
          }
          setLoading(true);

          try {
          await remoteOpenApi(!lock_motor_state);
          } catch (e) {
          setLoading(false);
          clearTimer();
          showToast({ title: e?.innerError?.errorMsg, icon: 'error' });
          }
          setTimer();
          };

          const setTimer = () => {
          timer.current = setTimeout(() => {
          if (loadRef.current) {
          showToast({
          title: Strings.getLang('outLineTip'),
          icon: 'none',
          });
          } else {
          hideToast();
          }
          setLoading(false);
          }, 15000);
          };

          const clearTimer = () => {
          timer.current && clearTimeout(timer.current);
          };

          return (
          <View className={styles.container}>
          {status !== DoorStatus.offline && (
          <RotateImage active={loading} rotateImage={statusInfo[status].bg} onLongClick={() => remoteOpen()}>
          <View className={styles.centerView}>
          {!!(statusInfo[status].iconBg) && (
          <Image src={statusInfo[status].iconBg} className={styles.centerImage} />
          )}
          <Text
          className={styles.lockText}
          style={{ color: statusInfo[status].color, marginTop: deviceOnline ? '0' : '160rpx' }}
          >
          {text}
          </Text>
          </View>
          </RotateImage>
          )}
          {status === DoorStatus.offline && (
          <View className={styles.outLineWrap}>
          <Image src={statusInfo[status].bg} className={styles.outLineBg} />
          <Text className={styles.outLineText} style={{ color: statusInfo[status].color }}>
          {text}
          </Text>
          </View>
          )}
          </View>
          );
          };

          export default RemoteOpenDoor;

        

引入了代码之后,我们的首页有了一个可长按点击的圆形按钮,这时我们可以修改 IDE 右侧控制面板 -> lock_motor_state -> false/true 进行虚拟面板 dp 点的下发与上报,查看 IDE 控制面板的状态来验证我们的产品功能