快速实现一款智能镜的设计与原型开发 您所在的位置:网站首页 涂鸦手绘广告语 快速实现一款智能镜的设计与原型开发

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

2024-05-06 01:27| 来源: 网络整理| 查看: 265

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

开发环境

本案例是基于 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 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.c 的 app_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)= 40) { key_buf = 0; if(p->Light_value 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系列模组烧录授权



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有