快速实现一款智能镜的设计与原型开发

更新时间Invalid date

概况

照镜子,对我们来说是每天必不可少的事情之一。现代人快节奏的生活中,一面能镜子能和人产生互动的镜子,能带来非同寻常的体验,还能为您节省不少时间。
比如安装在浴室里,您可以在梳妆打扮的时候听听新闻资讯,看看实时天气、空气指数、穿衣建议等。
我们设计了一款智能化妆镜,能显示时间和天气预报,可以根据天气预报化合适的妆出门。智能镜有白光、暖光和暖白光三种模式,且支持无极调光,可以模拟晴天和阴天的光环境。它选用了一个四路触摸按键,操作简单,加上人体感应开启模块,使用起来十分舒心。电源方案采用了锂电池供电,支持无线充电。最后,智能镜还能通过 App 进行控制。

物料清单

硬件 (19)
  • 涂鸦 Wi-Fi&BLE 双模模组

    数量:1

    智能镜采纳的双模联网模组查看详情

  • 主控和电源板

    数量:1

    可采购嘉立创的产品

  • LED 控制部分

    数量:1

    可采购嘉立创的产品

  • LED 部分

    数量:8

    可采购嘉立创的产品

  • 触摸板

    数量:1

  • 屏幕

    数量:1

    可根据自身需求进行采购

  • 弹簧

    数量:4

    8*8*5*0.47 规格

  • XH 端子线

    数量:1

    250MM3P 单头沾锡

  • XH 端子线

    数量:2

    250MM4P 双头同向

  • XH端子线

    数量:1

    250MM4P 单头沾锡

  • XH 端子线

    数量:1

    250MM7P 双头同向

  • 锂电池

    数量:1

  • 无线充电发射和接收部分

    数量:1

  • 激光雕刻结构件

    数量:3

  • 3D 打印结构件

    数量:6

  • 平头自攻螺丝

    数量:9

    M3.5*10

  • 平头自攻螺丝

    数量:12

    M2.2*8

  • PCB 板跳线

    数量:1

    U型0.4-3mm

  • PIR

    数量:1

    用户人体检测

步骤

  • 第 1 步:确定功能与逻辑

    作为一款个性化的智能镜,我们需要以下功能点:

    • 天气、时间等图案显示
    • 暖光、冷光和冷暖混合三种补光模式
    • 人体感应
    • 四路触摸按键控制(开关、模式、亮度+、亮度-)
    • App 远程遥控
    • 锂电池供电
    • 无线充电
    • 独一无二的外形设计

    理清楚这些功能点以后,我们再将这些功能点的业务流程串起来。

    业务流程

  • 第 2 步:硬件选型

    主控

    主控选用 涂鸦 Wi-Fi&BLE 双模模组云模组 是涂鸦提供的支持多种通信协议、多种尺寸规格、多种工作温度、多种焊接方式的一系列超高性价比自研模组,广泛应用于各种产品类型和开发方式,您可根据自身的产品需求进行灵活选择。

    屏幕

    屏幕可以选择一个易于开发,并能节省主控资源的产品,比如 MF219

    4

    2.19 寸集成串口屏是一款串口屏模块,点阵分辨率为 240x376。其内置国标一级、二级简体中文字库和英文 ASCII 字符集,同时开放内部点阵 DDRAM,能够在屏幕上的任意位置显示图片及图形。硬件上它提供了 UART 接口方式,接线简单。软件驱动方面,用户只要几条简单指令就能设计出美观绚丽的用户界面,集成串口屏丰富的片上资源及强大的指令集,使屏幕开发变得十分容易。

    触摸按键

    触摸功能采用 BS814A-1 触摸芯片实现。BS81x 系列芯片具有 2~16 个触摸按键,可用来检测外部触摸按键上人手的触摸动作。该系列的芯片具有较高的集成度,仅需极少的外部组件便可实现触摸按键的检测。BS81x 系列提供了串行及并行输出功能,可方便与外部 MCU 之间的通讯,实现设备安装及触摸引脚监测目的。芯片内部采用特殊的集成电路,具有高电源电压抑制比,可减少按键检测错误的发生,此特性保证在不利环境条件的应用中芯片仍具有很高的可靠性。此系列的触摸芯片具有自动校准功能,低待机电流,抗电压波动等特性,为各种触摸按键的应用提供了一种简单而又有效的实现方法。

    特性

    • 工作电压:2.2V~5.5V
    • 低待机电流
    • 自动校准功能
    • 可靠的触摸按键检测
    • 自动切换待机 / 工作模式
    • 最长按键输出时间检测
    • 具备抗电压波动功能
    • Level Hold,可选高有效或低有效
    • NMOS 输出内建上拉电阻 / CMOS 直接输出
    • 外接电容调整感度
    • 极少的外围组件
    4

    灯圈

    灯圈的核心是 LED 驱动和灯珠,为了使光照均匀,我们需要设计一个灯圈。

    OC7141 是一种带 PWM 调光功能的线 性降压 LED 恒流驱动器,仅需外接一个电阻 和一个 NMOS 管就可以构成一个完整的 LED 恒流驱动电路,调节该外接电阻就可以 调节输出电流,输出电流可调范围为 10mA 到3000mA。

    特点

    • 电源电压:2.5V~6V
    • 低静态电流:60uA
    • 输出电流:10mA 到 3000mA。
    • PWM 调光:最高频率 10KHz
    • 输出电流精度:优于±4%
    • 内置过热保护
    • 电源电压可扩展至 400V 以上。
    芯片

    人体感应

    人体感应可以直接使用 PIR 模块。

    PIR

    电源设计

    电源设计在整个系统中十分关键,电源这一部分包含的内容也比较多,主要有整机供电电源芯片,电池、电量检测、无线充电、有线充电等内容。

    整机供电电源芯片

    整机电源我们直接采用两个 SY8089A1AAC 实现,前面一颗芯片可以将 5V 的输入电源降到 4.35V 为锂电池充电,后面一颗可以将电池输出电压降到 3.3V 为主控、触摸、灯圈、屏幕、PIR 供电。
    SY8089A1 是一款高效率的 1.5MHz 同步降压 DC/DC 稳压器,能够交付高达 2A 的输出电流。它可以在 2.5V 到 5.5V 的宽输入电压范围内工作。条件允许的情况下,用电源管理芯片和 buck-boost 芯片代替这两颗芯片会更合理一些。

    特点

    • 2.5V ~ 5.5V 输入电压范围
    • 50μA 低静态电流
    • 低 RDS(ON) (上/下)130mΩ/ 85mΩ
    • 高开关频率 1.5MHz 最小化外部组件
    • 内部软启动限制涌流
    • 自动放电功能
    电源芯片

    电池

    耗电部分主要是灯圈部分比较耗电我们需要准备 2A 的负载网络,电池需要选择一个容量比较大才行,这里我们直接选择了一个 3.8V 6000mAh 的聚合物电池。

    电池

    特点

    • 标称电压:3.8V
    • 容量:6000mAh
    • 放电电流:4A
    • 放电截止:2.75V
    • 充电电流:0.5C
    • 充电限制:4.35V
    • 充电温度:0~45℃
    • 放电温度:-20~+60℃

    无线充电

    无线充电主要由两部分组成,无线充电发射模块和无线充电接收模块。

    无线充电发射模块

    无线充电接收模块

  • 第 3 步:硬件结构设计

    通过整机的逻辑框图,我们可以延伸出来我们的硬件框图:

    简单构思一下外壳设计,考虑到成本,我们需要把整个硬件系统拆分一下,分为主控和电源部分、触摸和 PIR 部分和灯圈部分。

    主控&电源

    主控&电源部分是整个硬件部分的核心,包含DC4.35V充电电路、DC3.3V放电电路、涂鸦 Wi-Fi&BLE 双模模组主控,USB充电、ADC电量检测、屏幕控制、CH340N串口烧录、复位电路、以及各个接口组成。

    触摸、PIR

    我们将触摸和 PIR 部分整合到了一起,置于智能镜的正前方,方便操控。

    灯圈

    灯圈部分为了节约成本,有分为了LED部分和控制部分,由8块LED部分和1块控制部分共同组成一盒灯圈。

    结构设计

    结构部分由镜面、镜面遮光板、镜面卡、灯罩、灯罩卡、机身、机身底、充电顶、充电底九部分组成。

    • 镜面
    • 镜面遮光板
    • 镜面卡
    • 灯罩
    • 灯罩卡
    • 机身
    • 机身底
    • 充电顶
    • 充电底
    • 整体装配图
    • 整体爆炸图

  • 第 4 步:IoT 平台产品创建

    1. 首先登录 涂鸦智能IoT平台 创建产品,在 标准类目 页签最下方找到 找不到品类,单击进入自定义产品创建页面。

    2. 输入产品名称和描述,通讯协议选择WIFI-蓝牙,点击创建产品。

    3. 在功能定义一栏添加DP点,如下图所示,本demo添加了标准功能:“开关”、“灯光开关”、“灯光模式”、“亮度值”、“电量状态”、“人体感应状态”、“人体感应开关”等功能;功能点可以根据需求自行增减,功能点名称以及属性也可根据需求自行修改。

    4. 进入设备面板,可以选择自己喜欢的模板或者自己自定义面板,调试阶段推荐选择开发调试面板,便于测试。

    5. 选择硬件开发平台,可以根据需要自行选择开发平台。

    注意:需要选择芯片平台并上传固件后,面板才能够使用;固件标识名要和上传的固件名称需保持一致,否则烧录授权时将无法通过。

    1. 至此,产品创建阶段已经基本完成,此时可以通过涂鸦智能 App 和虚拟设备体验 DP 数据的接收和发送。
  • 第 5 步:嵌入式系统搭建

    产品创建完毕后,接下来需要实现产品模组固件,首先要做的就是搞定开发环境。

    开发环境

    本案例是基于 BK7231N 平台进行的 SoC 开发,开发所用的涂鸦通用 SDK 编译需要 linux 环境,首先要安装linux开发环境,然后从涂鸦仓库拉取包含 SDK 环境的 Demo 例程。

    • 下载 Tuya IoTOS Embeded WiFi & BLE sdk

      $ cd "your directory"
      $ git clone https://github.com/tuya/tuya-iotos-embeded-sdk-wifi-ble-bk7231n
      

      在自己建立的目录中git下来demo,里面有附带有 SDK 环境,同时“apps”目录中也有几个应用案例,我们就使用apps/tuya_demo_template这个demo为开发模板,在此基础上增减代码,实现一个嵌入式系统框架。

    实现配网连接涂鸦平台的最简嵌入式系统框架

    在正式开始智能镜应用代码开发前,我们可以先搭建一个基本的嵌入式框架,实现设备联网,与平台建立上报下发通道。

    • 在现有的 Demo 基础上搭建系统框架
      当前 tuya_demo_template 应用程序的文件组成如下:
    ├── src	
    |    └── tuya_device.c            //应用层入口文件
    |
    ├── include				//头文件目录
    |    └──  tuya_device.h
    |
    └── output              //编译产物
    
    • tuya_demo_template 文件夹更名为 bk7231n_mirror_demo,再把 include 文件夹中的 tuya_device.h 文件里的 PRODECT_ID 宏定义修改为我们刚刚创建的智能镜产品的 PID。

    • 创建一个 tuya_app.c 文件,做为智能镜应用代码的主要文件
      创建后的 Demo 文件目录如下:
    ├── src	
    |    └── tuya_device.c            //应用层入口文件
    |    └── tuya_app.c            //主要应用文件
    |
    ├── include				//头文件目录
    |    └──  tuya_device.h
    |    └──  tuya_app.h
    |
    └── output              //编译产物
    
    • tuya_app.h 中涉及到 DP 点宏定义以及 DP 点上报、下发数据处理函数的声明。
    #ifndef __TUYA_APP_H__
    #define __TUYA_APP_H__
    
    #include "uni_log.h"
    #include "tuya_cloud_error_code.h"
    #include "tuya_cloud_com_defs.h"
    
    #ifdef __cplusplus
    extern "C" {
    #endif  /* __cplusplus */
    
    /***********************************************************
    *************************variable define********************
    ***********************************************************/
    typedef enum{
        APP_MIRROR_NORMAL,       //normal mode
        APP_MIRROR_PRODTEST      //prodact test mode
    }APP_MIRROR_MODE;
    
    #define DPID_SWITCH                      1
    #define DPID_SWITCH_LED                  2
    #define DPID_LIGHT_MODE                  4
    #define DPID_LIGHT_VALUE                 5
    #define DPID_BATTERY_STATUS              101
    #define DPID_PIR_MODE                    103
    #define DPID_PIR_STATE                   105
    /********************************************************************************
     * FUNCTION:       app_mirror_init
     * DESCRIPTION:    application initialization
     * INPUT:          mode:application mode
     * OUTPUT:         none
     * RETURN:         none
     * OTHERS:         none
     * HISTORY:        2021-01-12
     *******************************************************************************/
    OPERATE_RET app_mirror_init(IN APP_MIRROR_MODE mode);
    
    /********************************************************************************
     * FUNCTION:       deal_dp_proc
     * DESCRIPTION:    deal the data sented by app
     * INPUT:          root:app issued data structure
     * OUTPUT:         none
     * RETURN:         none
     * OTHERS:         none
     * HISTORY:        2021-01-12
     *******************************************************************************/
    VOID deal_dp_proc(IN CONST TY_OBJ_DP_S *root);
    
    /*********************************************************************************
     * FUNCTION:       app_report_all_dp_status
     * DESCRIPTION:    report all dp date
     * INPUT:          none
     * OUTPUT:         none
     * RETURN:         none
     * OTHERS:         dp_cnt needs to be modified when adding or deleting the dp function
     * HISTORY:        2021-01-12
     *******************************************************************************/
    VOID app_report_all_dp_status(VOID);
    
    #ifdef __cplusplus
    }
    #endif /* __cplusplus */
    
    
    #endif  /* __TUYA_APP_H__*/
    
    • tuya_app.c 中需实现下发 dp 点数据处理函数:deal_dp_proc 和 dp 数据上报函数:app_report_all_dp_status 。这里由于涂鸦SDK在上报 dp 数据时会做筛选处理,所有可以一次性把所有dp点全部上报。
    //接收下发数据,解析 dp id 和 dp 内容
    VOID deal_dp_proc(IN CONST TY_OBJ_DP_S *root)
    {
        UCHAR_T dpid;
    
        dpid = root->dpid;
        PR_DEBUG("dpid:%d",dpid);
        
        switch (dpid) {
    
        case DPID_SWITCH:
        
            //获取dp内容值
            mirror_ctrl_data.Mirror_switch = root->value.dp_bool;
            break;
            
        case DPID_SWITCH_LED:
        
            ......
            break;
    
        case DPID_LIGHT_MODE:
        
            ......
            break;
    
        case DPID_LIGHT_VALUE:
        
            ......
            break;
    
        case DPID_PIR_MODE:
        
            ......
            break;
    
        default:
            break;
        }
        
        app_report_all_dp_status();
    
        return;
    
    }
    
    //dp数据上报,判断已经连接涂鸦平台后,上报所有 dp 点。
    
    VOID app_report_all_dp_status(VOID)
    {
        OPERATE_RET op_ret = OPRT_OK;
    
        GW_WIFI_NW_STAT_E wifi_state = 0xFF;
    
        op_ret = get_wf_gw_nw_status(&wifi_state);
        if (OPRT_OK != op_ret) {
            PR_ERR("get wifi state err");
            return;
        }
        if (wifi_state <= STAT_AP_STA_DISC || wifi_state == STAT_STA_DISC) {
            return;
        }
        
        INT_T dp_cnt = 0;
        dp_cnt = 7;
    
        if(!mirror_ctrl_data.Wifi_state) {
            return;
        } 
    
        TY_OBJ_DP_S *dp_arr = (TY_OBJ_DP_S *)Malloc(dp_cnt*SIZEOF(TY_OBJ_DP_S));
        if(NULL == dp_arr) {
            PR_ERR("malloc failed");
            return;
        }
    
        memset(dp_arr, 0, dp_cnt*SIZEOF(TY_OBJ_DP_S));
    
        dp_arr[0].dpid = DPID_SWITCH;
        dp_arr[0].type = PROP_BOOL;
        dp_arr[0].time_stamp = 0;
        dp_arr[0].value.dp_value = mirror_ctrl_data.Mirror_switch;
    
        dp_arr[1].dpid = DPID_SWITCH_LED;
        dp_arr[1].type = PROP_BOOL;
        dp_arr[1].time_stamp = 0;
        dp_arr[1].value.dp_value = mirror_ctrl_data.Light_switch;
    
        dp_arr[2].dpid = DPID_LIGHT_MODE;
        dp_arr[2].type = PROP_ENUM;
        dp_arr[2].time_stamp = 0;
        dp_arr[2].value.dp_value = mirror_ctrl_data.Light_mode;
    
        dp_arr[3].dpid = DPID_LIGHT_VALUE;
        dp_arr[3].type = PROP_VALUE;
        dp_arr[3].time_stamp = 0;
        dp_arr[3].value.dp_value = mirror_ctrl_data.Light_value;
        
        dp_arr[4].dpid = DPID_BATTERY_STATUS;
        dp_arr[4].type = PROP_ENUM;
        dp_arr[4].time_stamp = 0;
        dp_arr[4].value.dp_value = mirror_ctrl_data.Battery_remain;
    
        dp_arr[5].dpid = DPID_PIR_MODE;
        dp_arr[5].type = PROP_BOOL;
        dp_arr[5].time_stamp = 0;
        dp_arr[5].value.dp_value = mirror_ctrl_data.PIR_switch;
    
        dp_arr[6].dpid = DPID_PIR_STATE;
        dp_arr[6].type = PROP_ENUM;
        dp_arr[6].time_stamp = 0;
        dp_arr[6].value.dp_value = mirror_ctrl_data.PIR_state;
    
        op_ret = dev_report_dp_json_async(NULL,dp_arr,dp_cnt);
        Free(dp_arr);
        if(OPRT_OK != op_ret) {
            PR_ERR("dev_report_dp_json_async relay_config data error,err_num",op_ret);
        }
    
        PR_DEBUG("dp_query report_all_dp_data");
        return;
    }
    
    • 修改 tuya_device.c 中的几处回调,将上面实现的 dp 上报和下发处理函数填进去。
    VOID dev_obj_dp_cb(IN CONST TY_RECV_OBJ_DP_S *dp)
    {
        PR_DEBUG("dp->cid:%s dp->dps_cnt:%d",dp->cid,dp->dps_cnt);
        UCHAR_T i = 0;
    
        for(i = 0;i < dp->dps_cnt;i++) {
            deal_dp_proc(&(dp->dps[i]));
            dev_report_dp_json_async(get_gw_cntl()->gw_if.id, dp->dps, dp->dps_cnt);
        }
    }
    
    VOID hw_report_all_dp_status(VOID)
    {
        app_report_all_dp_status();
    }
    
    STATIC VOID dev_dp_query_cb(IN CONST TY_DP_QUERY_S *dp_qry) 
    {
        PR_NOTICE("Recv DP Query Cmd");
    
        hw_report_all_dp_status();
    }
    

    至此一个基本的配网上报下发接收框架就搭建好了,可以在 dp 下发处理函数中不做任何操作,只执行打印日志,以便测试整个通信链路是否正常。

    Demo 代码介绍

    应用入口

    打开 Demo 例程,其中的 apps 文件夹内就是 Demo 的应用代码。应用代码结构如下:

    ├── src	
    |    ├── mirror_driver
    |    |    ├── tuya_mirror_pwm.c             //PWM驱动相关文件
    |    |    ├── tuya_mirror_key.c             //触摸按键相关代码文件
    |    |    └── tuya_mirror_screen.c            //显示屏相关代码文件
    |    ├── mirror_soc                   //tuya SDK soc层接口相关文件
    |    ├── tuya_device.c             //应用层入口文件
    |    ├── tuya_app.c            //主要应用层
    |    ├── svc_weather_service.c            //天气服务组件(暂不对外开放)
    |    └── tuya_mirror_control.c             //设备功能逻辑
    | 
    ├── include				//头文件目录
    |    ├── mirror_driver
    |    |    ├── tuya_mirror_pwm.h      
    |    |    ├── tuya_mirror_key.h   
    |    |    └── tuya_mirror_screen.h         
    |    ├── mirror_soc
    |    ├── tuya_device.h
    |    ├── tuya_app.h
    |    ├── svc_weather_service.h
    |    └── tuya_mirror_control.h
    |
    └── output              //编译产物
    

    打开 tuya_device.c 文件,找到 device_init 函数:

    OPERATE_RET device_init(VOID_T) 
    {
      OPERATE_RET op_ret = OPRT_OK;
    
      TY_IOT_CBS_S wf_cbs = {
          status_changed_cb,\
          gw_ug_inform_cb,\
          gw_reset_cb,\
          dev_obj_dp_cb,\
          dev_raw_dp_cb,\
          dev_dp_query_cb,\
          NULL,
      };
    
      op_ret = tuya_iot_wf_soc_dev_init_param(WIFI_WORK_MODE_SEL, WF_START_SMART_ONLY, &wf_cbs, NULL, PRODECT_ID, DEV_SW_VERSION);
      if(OPRT_OK != op_ret) {
          PR_ERR("tuya_iot_wf_soc_dev_init_param error,err_num:%d",op_ret);
          return op_ret;
      }
    
      op_ret = tuya_iot_reg_get_wf_nw_stat_cb(wf_nw_status_cb);
      if(OPRT_OK != op_ret) {
          PR_ERR("tuya_iot_reg_get_wf_nw_stat_cb is error,err_num:%d",op_ret);
          return op_ret;
      }
    
      op_ret = app_mirror_init(APP_MIRROR_NORMAL);
      if(OPRT_OK != op_ret) {
          PR_ERR("app init err!");
          return op_ret;
      }
      
      return op_ret;
    }
    
                      在BK7231N平台的SDK环境中,该函数为重要的应用代码入口,设备上电后平台适配层运行完一系列初始化代码后就会调用该函数来进行应用层的初始化操作。
    

    该函数做了三件事:

    • 调用 tuya_iot_wf_soc_dev_init_param()接口进行SDK初始化,配置了工作模式、配网模式,同时注册了各种回调函数并存入了PID(代码中宏定义为PRODECT_KEY)。
       TY_IOT_CBS_S wf_cbs = {
           status_changed_cb,\
           gw_ug_inform_cb,\
           gw_reset_cb,\
           dev_obj_dp_cb,\
           dev_raw_dp_cb,\
           dev_dp_query_cb,\
           NULL,
       };
    
       op_ret = tuya_iot_wf_soc_dev_init_param(WIFI_WORK_MODE_SEL, WF_START_SMART_FIRST, &wf_cbs, NULL, PRODECT_ID, DEV_SW_VERSION);
       if(OPRT_OK != op_ret) {
           PR_ERR("tuya_iot_wf_soc_dev_init_param error,err_num:%d",op_ret);
           return op_ret;
       }
    
    • 调用 tuya_iot_reg_get_wf_nw_stat_cb()接口注册设备网络状态回调函数。
       op_ret = tuya_iot_reg_get_wf_nw_stat_cb(wf_nw_status_cb);
       if(OPRT_OK != op_ret) {
           PR_ERR("tuya_iot_reg_get_wf_nw_stat_cb is error,err_num:%d",op_ret);
           return op_ret;
       }
    
    • 调用应用层初始化函数
       op_ret = app_mirror_init(APP_MIRROR_NORMAL);
       if(OPRT_OK != op_ret) {
           PR_ERR("app init err!");
           return op_ret;
       }
    

    应用层初始化函数 app_mirror_init 就实现在我们的 tuya_app.c 中,进入该函数中时就可以创建应用层的线程,从而开始运行应用功能代码。

    应用初始化

    在调用到 tuya_app.capp_mirror_init 函数时,首先读取 flash 中存放的设备数据,然后调用mirror_device_init 进行功能逻辑层的初始化操作,最后开启各个功能逻辑线程:

    OPERATE_RET app_mirror_init(IN APP_MIRROR_MODE mode)
    {
       OPERATE_RET op_ret = OPRT_OK;
    
       if(APP_MIRROR_NORMAL == mode) {
           
           UCHAR_T read_buff[SAVE_DATA_LEN] = {0};
           uiSocFlashRead(APP_DATA_SAVE,APP_DATA_SAVE_OFFSET,SAVE_DATA_LEN,read_buff);
           mirror_data_load(read_buff);
    
           UCHAR_T i = 0;
           for(i = 0;i < SAVE_DATA_LEN;i++){
               PR_NOTICE("------- readbuff = %d -----",read_buff[i]);
           }
    
           mirror_device_init();
    
           tuya_hal_thread_create(NULL, "thread_data_get", 512*8, TRD_PRIO_4, sensor_data_get_thread, NULL);
    
           tuya_hal_thread_create(NULL, "thread_data_deal", 512*4, TRD_PRIO_4, sensor_data_deal_thread, NULL);
    
           tuya_hal_thread_create(NULL, "key_scan_thread", 512*4, TRD_PRIO_3, key_scan_thread, NULL);
    
           tuya_hal_thread_create(NULL, "diplay_send_thread", 512*4, TRD_PRIO_3, diplay_send_thread, NULL);
    
       }else {
           //not factory test mode
       }
    
       return op_ret;
    }
    

    然后就需要实现各个线程handle, handle 内调用的函数皆在 mirror.control.c 中:

    #define TASKDELAY_SEC         1000
    
    STATIC VOID sensor_data_get_thread(PVOID_T pArg)
    {   
       while(1) {
    
           mirror_data_get_handle();
    
           tuya_hal_system_sleep(TASKDELAY_SEC);
       }
    }
    
    STATIC VOID diplay_send_thread(PVOID_T pArg)
    {     
       while(1) {
    
           tuya_hal_system_sleep(TASKDELAY_SEC/2);
    
           mirror_display_poll();
       }
    }
    
    STATIC VOID key_scan_thread(PVOID_T pArg)
    {   
       while(1) {
    
           mirror_key_poll();
          
           tuya_hal_system_sleep(25);       
       }
    }
    
    STATIC VOID sensor_data_deal_thread(PVOID_T pArg)
    {   
       while(1) {
           
           tuya_hal_system_sleep(TASKDELAY_SEC/2);
    
           if(mirror_ctrl_data.Wifi_state == connecting)
           {
               mirror_wifi_light_handle();
           }else {
    
               mirror_ctrl_handle();
           }
       }
    }
    

    触摸按键

    tuya_mirror_key.c 文件中,封装了 app_key_init()app_key_scan() 两个函数。app_key_init() 用于初始化按键I/O,app_key_scan()用于扫描按键按下情况获取键值:

    void app_key_scan(unsigned char *trg,unsigned char *cont)
    {
        unsigned char read_data;
        
        if(KEY_RELEAS_LEVEL) {
            read_data = 0x0F;
        }else {
            read_data = 0x00; 
        }
    
        read_data = (tuya_gpio_read(KEY_SWITCH_PIN)<<3)|(tuya_gpio_read(KEY_SET_PIN)<<2)|(tuya_gpio_read(KEY_UP_PIN)<<1)|(tuya_gpio_read(KEY_DOWN_PIN));
        *trg = (read_data & (read_data ^ (*cont)));
        *cont = read_data;
    }
    

    该函数会检测四个按键的按下情况,然后将键值赋值给传参,其中trg是在整个按键按下动作中只出现一次的键值,而cont是只在按键松手后才会复位的键值。通过这两种不同特效的键值,就可以实现长短按和组合键的功能。

    key_scan_thread 是上文提到的几个应用线程 handle 的其中一个,在该handle 里调用的是触摸按键扫描轮询函数,实现在 tuya_mirror_control.c 中,此时线程的休眠时间即是按键扫描间隔时间。

    VOID mirror_key_poll(VOID)
    {
       MIRROR_CTRL_DATA_T *p;
    
       p = &mirror_ctrl_data;
    
       app_key_scan(&key_trg,&key_cont);
       
       switch (key_cont)
       {
       case KEY_CODE_RELEASE:
    
           if(key_buf != 0) {
    
               mirror_key_event(key_buf);
           }
    
           key_buf = 0;
           key_old = KEY_CODE_RELEASE;
    
           break;
       case KEY_CODE_SWITCH:
    
           vTaskDelay(10);
           app_key_scan(&key_trg,&key_cont);
    
           if(key_cont == KEY_CODE_SWITCH) {
    
               key_buf = KEY_CODE_SWITCH;
           }
    
           key_old = KEY_CODE_SWITCH;
    
           break;
       case KEY_CODE_SET_LIGHT_COLOR:
    
           if(key_old == KEY_CODE_SET_LIGHT_COLOR) {
    
               key_delay_cont++;
           }else{
    
               key_delay_cont = 0;
           }
    
           if(key_delay_cont >= 2) {
    
               key_buf = KEY_CODE_SET_LIGHT_COLOR;
           }
    
           if(key_delay_cont >= 200) {
    
               key_buf = 0;
               key_delay_cont = 0;
               tuya_iot_wf_gw_unactive();
    
               // start connect;
           }
    
           key_old = KEY_CODE_SET_LIGHT_COLOR;
    
           break;
       case KEY_CODE_UP:
    
           if(p->Mirror_switch == FALSE) {
    
               key_buf = 0;
    
               return ;
           }
    
           if(p->Light_switch == FALSE) {
    
               key_buf = 0;
    
               return ;
           }
    
           if(key_old == KEY_CODE_UP) {
    
               key_delay_cont++;
           }else{
    
               key_delay_cont = 0;
           }
    
           if(key_delay_cont >= 2) {
    
               key_buf = KEY_CODE_UP;
           }
    
           if(key_delay_cont >= 40) {
    
               key_buf = 0;
    
               if(p->Light_value <= 995) {
    
                   p->Light_value += 10;
    
                   mirror_pwm_set(p->Light_mode,p->Light_value);
               }
           }
           
           key_old = KEY_CODE_UP;
           break;
       case KEY_CODE_DOWN:
    
           if(p->Mirror_switch == FALSE) {
    
               key_buf = 0;
    
               return ;
           }
    
           if(p->Light_switch == FALSE) {
    
               key_buf = 0;
    
               return ;
           }
    
           if(key_old == KEY_CODE_DOWN) {
    
               key_delay_cont++;
           }else{
    
               key_delay_cont = 0;
           }
    
           if(key_delay_cont >= 2) {
    
               key_buf = KEY_CODE_DOWN;
           }
    
           if(key_delay_cont >= 40) {
    
               key_buf = 0;
    
               if(p->Light_value>=205) {
    
                   p->Light_value -= 10;
    
                   mirror_pwm_set(p->Light_mode,p->Light_value);
               } 
           }
           
           key_old = KEY_CODE_DOWN;        
           break;          
       default:
           break;
       }
    
    }
    

    按键扫描获取对应键值,再根据键值执行相应的操作事件,即可完成触摸按键部分的功能。

    屏幕显示

    本demo选用的是一个块2.19寸集成模块串口屏,分辨率376x240,参照通信协议发送指令即可驱动,非常简单。不过要显示时间日期等信息,光靠屏幕内置的字库和图案所呈现的效果肯定是毫无美感的,我们需要自己准备字符素材,以一个个图片的方式实现显示效果。根据屏幕分辨率限制,将屏幕元素进行一个大致的规划,然后按照规划去搜集大小适合的图片素材:

    收集完素材后,全部打包为 .bin 文件烧录至屏幕模组里即可。

    在例程的 tuya_mirror_screen.c 里面封装了屏幕初始化函数 screen_init() 和各个元素的显示函数:screen_display_time()screen_display_week()screen_display_year()screen_display_day 等,在应用线程中调用时传入获取到的本地时间和日期即可在屏幕上显示出来。

    VOID screen_init(VOID);
    
    VOID screen_display_time(INT_T hours, INT_T mins);
    
    VOID screen_display_week(INT_T weeks);
    
    VOID screen_display_year(INT_T year);
    
    VOID screen_display_battery(BATTERY_STATE state);
    
    VOID screen_display_day(INT_T month, INT_T day);
    

    每个图片素材烧录在屏幕 flash 中,显示图片时需要向屏幕模组发送包含对应图片的地址、大小和显示位置的字符串。
    这里可以把所有地址的字符串都存在数组里,方便检索:

    UINT8_T *icon_buff[] = {
       /* 0 ~ 9 and ':' */
       "2289100","2290660","2292220","2293780","2295340","2296900","2298460","2300020","2301580","2303140","2305600",
    
       /* character: '年'、'月'、'日' (11~13) */
       "2312464","2314264","2316064",
    
       /* character: '周日' ~ '周六' (14~20) */
       "2349832","2317864","2323192","2328520","2333848","2339176","2344504",
    
       /* '℃' and white block (21~22)*/
       "2386480","2387560",
    
       /* icon of condtion : sun、rain、cloud (23~25)*/
    
       "2241228","2249420","2258520",
    
       /* character of condtion : sun、rain、cloud (26~28)*/
       "2267620","2272380","2277140",
    
       /* icon of battery : high、medium、low、charging (29~32)*/
       "2355160","2362990","2370820","2378650",
    };
    

    以星期显示为例,根据传参值检索数组获取对应图片的地址,然后拼接成完整的字符串指令发送给屏幕模组:

    VOID screen_display_week(INT_T weeks)
    {
     if((weeks < 0)||(weeks > 6)) {
         return;
     }
    
     uint8_t data_buff[40] = {0};
     
     snprintf(data_buff,sizeof(data_buff),"FSIMG(%s,280,95,72,37,0);\r\n",icon_buff[weeks+14]);
    
     tuya_uart_write(uart0, data_buff, strlen(data_buff));
    }
    

    其他元素的显示也是类似的流程,完成各元素的显示函数后屏幕部分代码就基本完成了。

    时间与日期获取

    本 Demo 通过 tuya SDK 的接口在联网后获取本地时间。

    要获取本地时间,首先需包含头文件 uni_time.h。定义一个本地时间结构体变量,然后作为传参调用 uni_local_time_get() 接口获取时间:

    STATIC VOID mirror_date_get(VOID)
    {
     if(mirror_ctrl_data.Wifi_state == connecting) {
         
         return;
     }
    
     POSIX_TM_S cur_time;
     if( uni_local_time_get(&cur_time) != OPRT_OK ) {
         PR_NOTICE("cant get local time");
     }
    
     mirror_ctrl_data.Mirror_time.sec = (UCHAR_T)cur_time.tm_sec;
     mirror_ctrl_data.Mirror_time.min = (UCHAR_T)cur_time.tm_min;
     mirror_ctrl_data.Mirror_time.hour = (UCHAR_T)cur_time.tm_hour;
    
     if(mirror_ctrl_data.Mirror_time.year != cur_time.tm_year) {
    
         mirror_ctrl_data.Mirror_time.year = (1900 + cur_time.tm_year);
    
     }
    
     if((mirror_ctrl_data.Mirror_time.mon != cur_time.tm_mon)||(mirror_ctrl_data.Mirror_time.mday != cur_time.tm_mday)) {
    
         mirror_ctrl_data.Mirror_time.mon= (UCHAR_T)cur_time.tm_mon;
         mirror_ctrl_data.Mirror_time.mday = (UCHAR_T)cur_time.tm_mday;
    
     }
    
     if(mirror_ctrl_data.Mirror_time.wday != cur_time.tm_wday) {
         mirror_ctrl_data.Mirror_time.wday = (UCHAR_T)cur_time.tm_wday;
    
     }
    }
    

    此处唯一要注意的点是年份给出的是从1900开始的数值,比如获取到 cur_time.tm_year == 121 就代表是2012年。拿到了日期时间数据,就可以通过前面实现的显示函数呈现在屏幕上了。

    PWM 驱动灯板

    本 Demo 使用了一冷一暖两种 LED 灯,通过输出两路 PWM 驱动,实现灯光亮度可调和冷暖色的切换。例程中有关 PWM 的初始化和启动、占空比设置等相关代码都实现在 tuya_mirror_pwm.c 文件中。

    OPERATE_RET mirror_pwm_init(VOID);
    
    OPERATE_RET mirror_pwm_set(UCHAR_T color, USHORT_T duty);
    
    OPERATE_RET mirror_pwm_off(VOID);
    

    mirror_pwm_set() 可以改变控制连接冷暖两路的 PWM 的开启和停止以及各自的占空比,直接输入灯色传参和占空比即可实现灯板的冷暖光切换和亮度调整:

    OPERATE_RET mirror_pwm_set(UCHAR_T color, USHORT_T duty)
    {
      FLOAT_T percent = 0.0;
    
      if(user_pwm_init_flag != TRUE){
          PR_ERR("user pwm not init!");
          return OPRT_INVALID_PARM;
      }
    
      percent = (FLOAT_T)(duty/1000.0);
    
      bk_pwm_stop(pChannelList[0]);
      bk_pwm_stop(pChannelList[1]);
    
      switch (color)
      {
      case 0:
          PR_NOTICE("set light cold");
          bk_pwm_update_param(pChannelList[0], pwm_period, (UINT32)(percent * pwm_period), 0, 0);
          bk_pwm_start(pChannelList[0]);
          break;
      case 1:
          PR_NOTICE("set light medium");
          bk_pwm_update_param(pChannelList[0], pwm_period, (UINT32)((percent * pwm_period)/2), 0, 0);
          bk_pwm_update_param(pChannelList[1], pwm_period, (UINT32)((percent * pwm_period)/2), 0, 0);
          bk_pwm_start(pChannelList[0]);
          bk_pwm_start(pChannelList[1]);
          break;
      case 2:
          PR_NOTICE("set light warm");
          bk_pwm_update_param(pChannelList[1], pwm_period, (UINT32)(percent * pwm_period), 0, 0);
          //bk_pwm_update_param(pChannelList[0], pwm_period, 0, 0, 0);
          bk_pwm_start(pChannelList[1]);
          break;
      default:
          break;
      }
    
      return OPRT_OK;
    }
    

    封装好这几个接口后,接下来就需要在应用代码中合适的地方调用来控制灯板。

    人体感应

    本 Demo 还有一个人体感应开关灯光和屏幕的功能,是通过一个 PIR 传感器来简单实现的。该传感器会在检测到人体运动的时候输出高电平,简单易用。

    直接写一个读取连接传感器 I/O 的电平的函数,然后把它放到线程里周期运行,并在读到高电平的时候保存 PIR 状态在设备数据结构体当中:

    STATIC VOID mirror_pir_detect(VOID)
    {    
        if(tuya_gpio_read(PIR_IN_PORT)) {
            PR_NOTICE("-----------SOMEONE HERE-------------");
            mirror_ctrl_data.PIR_state = trigger;
        }
    }
    
    VOID mirror_data_get_handle(VOID)
    {   
     	......
     	// Detect pir IO port
     	mirror_pir_detect();
    }
    
    STATIC VOID sensor_data_get_thread(PVOID_T pArg)
    {   
     	 while(1) {
     	
     	     mirror_data_get_handle();
     	
     	     tuya_hal_system_sleep(TASKDELAY_SEC);
     	 }
    }
    

    然后另一个线程对设备数据结构体中 PIR 的状态进行判定,当设备打开人体感应功能,且设备总开关处于打开的情况下,PIR 检测到有人的时候将会打开灯光开关,同时启动定时器。当定时器触发进入中断时将会关闭灯光开关。

    VOID pir_data_handle(VOID)
    {
     MIRROR_CTRL_DATA_T *p;
    
     p = &mirror_ctrl_data;
    
     if(p->PIR_state == trigger) {
         p->PIR_state = none;
    
         if((p->PIR_switch != TRUE)||(p->Mirror_switch != TRUE)) {
    
             return;
         }
    
         if(IsThisSysTimerRun(off_timer) == TRUE) {
             sys_stop_timer(off_timer);
             sys_start_timer(off_timer, 1000*600, TIMER_ONCE);
         }else {
             sys_start_timer(off_timer, 1000*600, TIMER_ONCE);
         }
    
         p->Light_switch = TRUE;
    
     }
    }
    

    这样就实现了人来即亮,延时熄灭的效果。

    其他功能

    经过上面的步骤,智能镜 Demo 就只剩下电池电量检测及显示功能尚未实现,该功能通过 ADC 采样得到电池电压,再根据电压值预估电池剩余电量,同时把之前屏幕素材收集阶段准备的几张电池图案显示在屏幕上。
    把所有功能的运行逻辑加以整合和修改,同时引入涂鸦云功能点下发控制逻辑,一个可用手机 App 控制的智能镜嵌入式 Demo 代码就完成了。

    编译和烧录

    在 Linux 终端输入指令运行SDK环境目录下的 build_app.sh 脚本来编译代码生成固件,指令格式为 sh build_app.sh APP_PATH APP_NAME APP_VERSION

    若出现下图所示提示,则表示编译成功,固件已经生成:

    固件生成路径为:apps > APP_PATH > output

    将固件烧录至模组即可开始功能调试阶段,有关烧录和授权方式可以参照文档: WB系列模组烧录授权

小结

至此智能镜 Demo 就完成了,它可以通过 App 远程控制和本地触摸按键控制调整灯光模式及亮度,同时屏幕上可以显示日期时间天气等信息。您可以基于涂鸦 IoT 平台丰富它的功能,也可以更加方便的搭建更多智能产品原型,加速智能产品的开发流程。

更进一步