给多功能台灯赋予智能感应的能力

更新时间Invalid date

概况

在台灯的基本功能基础上,我们设计了一种更加自动化、智能化、人性化的台灯。在设计中融合了微波雷达、光照强度等一些传感器,让台灯的使用变得更加便捷、有趣、智能。

功能优势

  • 智能感应:自动感应人体是否在场而实现自动开关灯。
  • 自动调光:在不同的时间段,通过获取外界环境来自动调整灯光亮度,有利于节能环保。
  • 坐姿纠正:使得用户能够养成良好的学习工作习惯并保护视力。
  • 休息提醒:自动调时计时提醒休息,让使用者的眼睛更加健康。

硬件框图

功能.png

软件逻辑

软件逻辑.png

物料清单

硬件 (9)软件 (2)
  • CBU 云模组

    数量:1

    基于涂鸦智能的一款低功耗嵌入式Wi-Fi+BLE 双协议CBU 模组开发的一款主控板。您可通过此主控板,搭配其他功能模块,实现对应的功能。在设计台灯过程中,我们对此开发板进行了部分沿用。查看详情

  • ISK1101 微波雷达检测传感器

    数量:1

    是用于区域感应和距离测量应用的毫米波传感器参考设计。ISK1101 设计中包括矽典微(一家中国电子器件制造商)智能毫米波传感器芯片、单发单收毫米波天线和主控 MCU 等硬件,预留主控调试烧录口。

  • BH1750 照度检测模块

    数量:1

    用于光照度检测。模块搭载一个 BH1750FVI,是 I2C 总线接口的数字环境光传感器 IC。可以准确读取 1-65535 lx 的环境照度。

  • 电容式点动型触摸开关模块

    数量:1

    选用基于触摸检测 IC(TTP223B) 的电容式点动型触摸开关模块。常态下模块输出低电平。

  • 锂电池

    数量:1

    供电电压为 3.7V~4.25。

  • 锂电池充电管理模块

    数量:1

    用于过电保护和管理锂电。

  • 稳压模块

    数量:1

    保证稳定输出电压值。

  • 0.91 寸 OLED 显示屏

    数量:1

    进行时间和电量显示。

  • 蜂鸣器

    数量:1

    用作工作计时提示和低电量报警。

步骤

一:硬件方案及选型

1:主控模组

基于涂鸦智能的一款低功耗嵌入式Wi-Fi+BLE 双协议 CBU Wi-Fi 云模组 开发的一款主控板。

您可通过此主控板,搭配其他功能模块,实现其它对应的功能。在设计台灯过程中我们对此开发板进行了部分沿用。

  • 原理图点击下载资料文件

    image-20210302172656975

  • PCB点击下载资料文件

    image-20210302174737205

  • 焊接图

    注意:红色标出的是样机所需要焊接的地方,CN9、CN8、CN4、CN5按红色连线短接即可。

  • 制板GerBer点击下载资料文件

  • BOM清单点击下载资料文件

    ID 描述 坐标 包装 数量
    1 0.1uF/10V C10,C12,C13 c0402 3
    2 10uF/6.3V C11 c0402 1
    3 HS211UR13-02 LED2 led0603 1
    4 0R R1 r0805 1
    5 MMBT3904 Q2 sot23-3 1
    6 1K/1% R6,R9 r0402 2
    7 10K/1% R7,R8 r0402 2
    8 10k/1% R10 r0603 1
    9 CBU U3 moudle-cbu-4-21 1

2:环境、光照、触摸传感器

  • 微波雷达检测(点击下载资料文件)

    ISK1101 是用于区域感应和距离测量应用的毫米波传感器参考设计。

    ISK1101 设计中包括 矽典微(一家中国电子器件制造商)智能毫米波传感器芯片、单发单收毫米波天线和主控 MCU 等硬件,预留主控调试烧录口。

    利用矽典微智能毫米波传感器芯片高集成度、高带宽特征,可以在较小的体积和较少的元器件下实现灵活和精确的设置。

    通过毫米波感应判断有人进入检测区域和是否有人一直在检测区域内,并发出控制信号。可区分人体和静物,并可设定距离范围和触发阈值。

    image-20210302182559175

    主要器件说明

    名称 品牌 型号 功能描述
    U1 矽典微 S3KM111 毫米波传感器
    U2 GigaDevice GD32F350K8U6 主控 MCU

    接口功能说明

    名称 功能描述 说明
    J1 包括电源、RX、TX、接地。RX、TX一般用于 UART 通讯,还有一个备用的 IIC 接口 2.54mm pitch 间距插针
    J2 包括电源、SWCLK、SWDIO、接地。SWCLK、SWDIO 用于 MCU 程序调试与下载 2.54mm pitch 间距触点
    • 原理图

    • PCB

  • 照度检测(点击下载资料文件)

    光照度检测选取一个 BH1750 照度检测模块来实现。

    BH1750 照度检测模块搭载一个 BH1750FVI,是 I2C 总线接口的数字环境光传感器 IC。可以准确读取 1-65535XL 的环境照度。

    image-20210308163608700

    原理图

    管脚介绍

    名称 VCC GND SCL SDA ADDR
    功能描述 3~5V供电 参考地 IIC时钟线 IIC数据线 地址线
  • 触摸检测

    选用基于触摸检测 IC(TTP223B) 的电容式点动型触摸开关模块。常态下模块输出低电平。

    • 当用户用手指触摸模块背面的圆形区域时,模块会输出高电平。
      台灯中此模块 AB 两点不焊接。

      image-20210302194423681

    管脚介绍

    VCC IO GND
    2.5V~5.5V供电 默认低电平输出 参考地

    模块功能

    效果 B点 A点
    点动高电平输出 不焊接 不焊接
    点动低电平输出 不焊接 焊接
    自锁高电平输出 焊接 不焊接
    自锁低电平输出 焊接 焊接

3:其它功能模块

  • 单节锂电池(供电电压为 3.7V~4.25)

  • 锂电池充电管理模块

    image-20210302203236295

    功能说明

    输入电压 充电电流 充电截止电压 电池过放保护电压 电池过流保护电流
    5V <=1000mA 4.2V 2.5V 3A

    电流调节

    RPROG(K) IBAT(mA)
    30 50
    20 70
    10 130
    5 250
    4 300
    2 580
    1.66 690
    1.5 780
    1.33 900
    1.2 1000

    注意

    1. 第一次接入电池时,可能无电压输出,这时接入5V电压充一下电就可以激活保护电路了。
    2. 锂电池从板子上断开再接上的时候也需要充一下电以激活保护电路。
    3. 当使用手机充电器来作为输入时注意充电器必须要输出1A或者以上的,不然可能会不能正常充电。
    4. 如果5V的输入电压偏高,比如5.2V甚至5.5V,会造成充电电流不足1000mA,这是正常的。

      电压高了芯片发热会自动减少充电电流,不至于芯片烧毁。芯片在工作中60度左右发热是正常的,因为充电电流大。
    5. 输入反接对芯片没有影响,但是输出(电池端)反接会烧坏芯片。
    6. 锂电池电压的两个采样电阻实际在焊接的时候焊接在模块输出电压处(标记3.7V和GND之间),并不是图上锂电池接入的位置处。
    7. 对电阻R5阻值进行调节,可以改变输出电流大小。实际样机中把R5阻值替换成1.2K,从而增大输出电流。
  • 稳压模块

    这是一款升降压稳压模块(DC开关稳压电源),无论它的输入电压高于还是低于输出电压都可以保证稳定输出电压值。

    image-20210302203236295

    功能说明

    输入电压 输出电压 效率 输出电流 静态电流
    DC1.8V~5.5V DC3.3V 96% 最大电流2A 器件的静态电流小于 50μA

    管脚介绍

    VIN GND VOU GND
    电压输入口正极 电压输入口负极 升降压电压输出口 电压输出口负极

    注意:接线需要注意,最好用粗的导线焊接使用。根据实际操作经验,用杜邦线插针接达不到功率要求。

  • OLED 显示(点击下载资料文件)

    ​台灯中使用0.91寸OLED屏进行时间和电量显示。

    原理图

    管脚介绍

    名称 VCC GND SCL SDA
    功能描述 3.3V~6V供电 参考地 IIC时钟线 IIC数据线
  • 蜂鸣器(点击下载资料文件)

    台灯中使用蜂鸣器来做工作计时提示和低电量报警。可以把单片机的管脚直接拉高对蜂鸣器进行控制发声。

    管脚介绍

    S VCC -
    参考地 1.5V~15V 信号脚

    注意:由于选用的蜂鸣器有印刷反向的原因,Demo 中的源蜂鸣器的 S 脚接地,- 脚接I/O口。

二:产品原型搭建

样机原理图

框图标识介绍

框图编号 含义
1 锂电池,用于供电
2 稳压模块,用于输出 3.3V
3 采集电路,用于两个阻值 100K 的电阻串联对锂电池电压进行分压,实际样机中电阻焊接在输出电压处(标识 3.7V 和 GND 之间)
4 触摸按键 1,用于电源开关
5 触摸按键 2,用于设置按键
6 触摸按键 3,用于上升按键
7 触摸按键 4,用于下降按键
8 台灯灯板,用于 50 个单色 LED 灯组合在一起,25 个并联,两组
9 蜂鸣器模块
10 OLED 显示模块
11 光照度传感器模块
12 雷达传感器模块
13 负载电源控制电路
14 台灯灯板信号脚 1 控制电路
15 台灯灯板信号脚 2 控制电路

样机接线图

image-20210303160320588

注意:

  1. 样机接线图中,OLED 屏,雷达传感器,光照度传感器,蜂鸣器的电源都是单片机 I/O(P17) 控制 PMOS 管电路输出的电源。
  2. 样机接线图中,控制 LED 灯板的的 R 和 B 脚不是直接接到单片机的 I/O 口的,而是分别经过 NMOS 控制电路后再接到 LED 灯板的两个信号脚。

三:在涂鸦 IoT 平台创建产品

您可以参考 选品类创建产品 详细了解如何在涂鸦 IoT 平台创建台灯。此处简单介绍如何创建一个采用自定义开发方案的台灯。

  1. 进入涂鸦 IoT 平台

    创建产品

    • 选择 照明 > 氛围照明 > 台灯。
    • 选择自定义方案,输入产品名称,选择通讯协议为WI-FI+蓝牙,点击创建产品。
  2. 根据要实现的设备功能,创建好DP功能点。

  3. 设定完功能点后,点击 设备面板,选择 App 的面板样式。

    推荐选择开发调试面板,比较直观,且可以开到dp数据包的接收和发送,方便开发阶段调试使用。

至此,产品的创建基本完成,可以正式开始嵌入式软件部分的开发。

四:嵌入式功能开发

嵌入式代码基于 BK7231n 芯片平台,使用涂鸦通用 Wi-Fi SDK 进行 SoC 开发,具体代码可下载查看 Demo 例程

1:应用层

下载并打开 Demo 例程后,您看到的 apps 文件夹则包含了 Demo 的应用代码。应用代码结构如下:

├── src
| ├── app_driver
| | ├── lamp_pwm.c           //台灯 PWM 驱动相关文件
| | ├── sh1106.c             //OLED 屏驱动相关文件
| | ├── bh1750.c             //光照强度传感器驱动相关文件
| | └── app_key.c            //触摸按键相关代码文件
| ├── app_soc                //Tuya SDK SoC 层接口相关文件
| ├── tuya_device.c          //应用层入口文件
| ├── app_lamp.c             //主要应用层
| └── lamp_control.c         //按键相关逻辑
|
├── include                  //头文件目录
| ├── app_driver
| | ├── lamp_pwm.h
| | ├── sh1106.h
| | ├── bh1750.h
| | └── app_key.h
| ├── app_soc
| ├── tuya_device.h
| ├── app_lamp.h
| └── lamp_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_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;
	}

	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_lamp_init(APP_LAMP_NORMAL);
	if(OPRT_OK != op_ret) {
		PR_ERR("app init err!");
		return op_ret;
	}

	return op_ret;
}

在BK7231平台的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_lamp_init(APP_LAMP_NORMAL);
    	if(OPRT_OK != op_ret) {
    		PR_ERR("app init err!");
    		return op_ret;
    	}
    

2:应用结构

本 Demo 应用代码主要分三层来实现:

  • 最底层为一些外设、传感器的驱动代码。

    例如光照传感器、OLED屏幕、微波雷达、触摸按键、灯板等,封装出常用接口。

  • 第二层为控制逻辑部分的代码。

    调用驱动层的各类接口,实现各个组件的控制逻辑,封装出数据处理轮询接口。

  • 第一层为主要应用层。

    创建应用任务调用第二层的接口,同时处理DP点数据的上报和接受解析。

    第一层就是在app_lamp.c文件中实现的,大致内容如下:

    • app_lamp_init() 调用第二层封装出的设备初始化接口,创建应用任务。
    OPERATE_RET app_lamp_init(IN APP_LAMP_MODE mode)
    {
    	OPERATE_RET op_ret = OPRT_OK;
    
    	if(APP_LAMP_NORMAL == mode) {
    
    		lamp_device_init();
    
    		//create ADC sensor data collection thread
    		tuya_hal_thread_create(NULL, "thread_data_get", 512*4, 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_4, key_scan_thread, NULL);
    
    		tuya_hal_thread_create(NULL, "thread_data_report", 512*4, TRD_PRIO_4, sensor_data_report_thread, NULL);
    	}else {
    		//not factory test mode
    	}
    
    	return op_ret;
    }
    
    • app_report_all_dp_status() 用于上报所有DP数据:
    VOID app_report_all_dp_status(VOID)
    {
    	OPERATE_RET op_ret = OPRT_OK;
    
    	INT_T dp_cnt = 0;
    	dp_cnt = 5;
    
    	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_DELAY_OFF;
    	dp_arr[0].type = PROP_BOOL;
    	dp_arr[0].time_stamp = 0;
    	dp_arr[0].value.dp_value = lamp_ctrl_data.Lamp_delay_off;
    
    	dp_arr[1].dpid = DPID_LIGHT_MODE;
    	dp_arr[1].type = PROP_ENUM;
    	dp_arr[1].time_stamp = 0;
    	dp_arr[1].value.dp_value = lamp_ctrl_data.Light_mode;
    
    	dp_arr[2].dpid = DPID_SIT_REMIND;
    	dp_arr[2].type = PROP_BOOL;
    	dp_arr[2].time_stamp = 0;
    	dp_arr[2].value.dp_value = lamp_ctrl_data.Sit_remind;
    
    	dp_arr[3].dpid = DPID_AUTO_LIGHT;
    	dp_arr[3].type = PROP_BOOL;
    	dp_arr[3].time_stamp = 0;
    	dp_arr[3].value.dp_value = lamp_ctrl_data.Auto_light;
    
    	dp_arr[4].dpid = DPID_LOW_POW_ALARM;
    	dp_arr[4].type = PROP_BOOL;
    	dp_arr[4].time_stamp = 0;
    	dp_arr[4].value.dp_value = lamp_ctrl_data.Low_pow_alarm;
    
    	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;
    }
    
    • 任务函数,任务内循环调用的lamp_get_sensor_data()lamp_key_poll()lamp_ctrl_handle() 都是第二层的接口,实现在 lamp_control.c 文件中,分别负责传感器数据的采集,触摸按键扫描轮询及数据处理和功能逻辑实现轮询:
    STATIC VOID sensor_data_get_thread(PVOID_T pArg)
    {
    	while(1) {
    		PR_DEBUG("sensor_data_get_thread");
    		lamp_get_sensor_data();
    		tuya_hal_system_sleep(TASKDELAY_SEC/2);
    
    	}
    }
    
    STATIC VOID key_scan_thread(PVOID_T pArg)
    {
    
    	while(1) {
    		lamp_key_poll();
    
    		tuya_hal_system_sleep(25);
    	}
    
    }
    
    STATIC VOID sensor_data_deal_thread(PVOID_T pArg)
    {
    	while(1) {
    		lamp_ctrl_handle();
    		tuya_hal_system_sleep(TASKDELAY_SEC);
    
    	}
    }
    
    STATIC VOID sensor_data_report_thread(PVOID_T pArg)
    {
    	while(1) {
    
    		tuya_hal_system_sleep(TASKDELAY_SEC*10);
    
    		app_report_all_dp_status();
    	}
    
    }
    
    • deal_dp_proc() 处理接收到的DP数据,通过识别DP ID来进行相应的数据接收处理:
    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_DELAY_OFF:
    		PR_DEBUG("set led switch:%d",root->value.dp_bool);
    		lamp_ctrl_data.Lamp_delay_off = root->value.dp_bool;
    		break;
    
    	case DPID_LIGHT_MODE:
    		PR_DEBUG("set light mode:%d",root->value.dp_enum);
    		lamp_ctrl_data.Light_mode = root->value.dp_enum;
    		break;
    
    	case DPID_SIT_REMIND:
    		PR_DEBUG("set sit remind switch:%d",root->value.dp_bool);
    		lamp_ctrl_data.Sit_remind = root->value.dp_bool;
    		break;
    
    	case DPID_AUTO_LIGHT:
    		PR_DEBUG("set auto switch:%d",root->value.dp_bool);
    		lamp_ctrl_data.Auto_light = root->value.dp_bool;
    		break;
    
    	case DPID_LOW_POW_ALARM:
    		PR_DEBUG("set low power alarm switch:%d",root->value.dp_bool);
    		lamp_ctrl_data.Low_pow_alarm = root->value.dp_bool;
    		break;
    
    	default:
    		break;
    	}
    
    	app_report_all_dp_status();
    
    	return;
    
    }
    

实现了上述的几个函数后,应用层代码的大概结构就已经确定下来了,接下来就需要实现上面提到的被调用的第二层接口,这些接口都放在本 Demo 的lamp_control.c文件中。在下面的内容里,本篇文档将根据实现的具体功能解说 Demo 例程。

3:触摸按键设计

​本 Demo 硬件设计上设计了四个触摸按键,需要实现以下功能:

  • 灯开关
  • 灯光颜色切换
  • 档位调光和无极调光
  • 静音模式开启关闭
  • 延时关灯等

因此,需要实现长按、短按和组合键三种不同的触发方式来应对多种按键功能。

app_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 = 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 是只在按键松手后才会复位的键值。通过这两种不同特效的键值,就可以实现长短按和组合键的功能。

按键触发后对应的具体功能实现在lamp_control.c中,分两个函数:

STATIC VOID lamp_key_event(UINT8_T key_event)
{
	if(key_event == KEY_CODE_SWITCH) {
		PR_NOTICE("----POWER ON!!!!!!!----");
		if(lamp_ctrl_data.Lamp_switch == FALSE) {
			lamp_ctrl_data.Lamp_switch = TRUE;
			lamp_pwm_set(lamp_ctrl_data.Light_mode,user_pwm_duty);
		}else{
			lamp_ctrl_data.Lamp_switch = FALSE;
			lamp_pwm_off();
		}
	}else if(key_event == KEY_CODE_SET_LIGHT_COLOR) {
		lamp_ctrl_data.Light_mode++;
		if(lamp_ctrl_data.Light_mode > 2){
			lamp_ctrl_data.Light_mode = 0;
		}
		lamp_pwm_set(lamp_ctrl_data.Light_mode,user_pwm_duty);
		PR_NOTICE("----change light mode to %d----",lamp_ctrl_data.Light_mode);
	}
	else if(key_event == KEY_CODE_UP) {
		if(user_pwm_duty != 600) {
			if(user_pwm_duty > 400){
				user_pwm_duty = 600;
			}else{
				user_pwm_duty += 200;
			}
			lamp_pwm_set(lamp_ctrl_data.Light_mode,user_pwm_duty);
			PR_NOTICE("----PWM_VALUE UP ONCE----");
		}
	}
	else if(key_event == KEY_CODE_DOWN) {
		if(user_pwm_duty != 0) {
			if(user_pwm_duty < 200){
				user_pwm_duty = 0;
			}else{
				user_pwm_duty -= 200;
			}
			lamp_pwm_set(lamp_ctrl_data.Light_mode,user_pwm_duty);
			PR_NOTICE("----PWM_VALUE DOWN ONCE----");
		}
	}
	else if(key_event == KEY_CODE_SET_BEEP) {
		lamp_ctrl_data.Silent_mode = !lamp_ctrl_data.Silent_mode;
		PR_NOTICE("----SET BEEP----");
	}
		__ctrl_beep(100);
}

VOID lamp_key_poll(VOID)
{
	app_key_scan(&key_trg,&key_cont);

	switch (key_cont)
	{
	case KEY_CODE_RELEASE:
		if(key_buf != 0) {
			lamp_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(lamp_ctrl_data.Lamp_switch == FALSE) {
			key_buf = 0;
			return ;
		}
		vTaskDelay(10);
		app_key_scan(&key_trg,&key_cont);
		if(key_cont == KEY_CODE_SET_LIGHT_COLOR) {
			key_buf = KEY_CODE_SET_LIGHT_COLOR;
		}
		key_old = KEY_CODE_SET_LIGHT_COLOR;
		break;
	case KEY_CODE_UP:
		if(lamp_ctrl_data.Lamp_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(user_pwm_duty <= 590) {
				user_pwm_duty += 10;
				lamp_pwm_set(lamp_ctrl_data.Light_mode,user_pwm_duty);
			}
		}

		key_old = KEY_CODE_UP;
		break;
	case KEY_CODE_DOWN:
		if(lamp_ctrl_data.Lamp_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(user_pwm_duty>=10) {
				user_pwm_duty -= 10;
				lamp_pwm_set(lamp_ctrl_data.Light_mode,user_pwm_duty);
			}
		}

		key_old = KEY_CODE_DOWN;
		break;
	case KEY_CODE_SET_BEEP:
		vTaskDelay(10);
		app_key_scan(&key_trg,&key_cont);
		if(key_cont == KEY_CODE_SET_BEEP) {
			key_buf = KEY_CODE_SET_BEEP;
		}
		break;
	case KEY_CODE_DELAY_OFF:

		break;
	default:
		break;
	}
}

4:时间显示

本 Demo 通过 Tuya SDK 可以在台灯联网后获取本地时间,并显示在 OLED 屏幕上。

屏幕 SH1106 的驱动和封装的接口都在 sh1106.c 文件中,以软件实现的 IIC 来驱动屏幕。封装的接口有以下几个:

  • tuya_sh1106_init()屏幕驱动初始化,传参设置为 SDA、SCL 脚的 I/O 口:

    UCHAR_T tuya_sh1106_init(sh1106_init_t* param)
    {
    	UCHAR_T error = 0;
    
    	int opRet = -1;
    
    	i2c_pin_t i2c_config = {
    		.ucSDA_IO = param ->SDA_PIN,
    		.ucSCL_IO = param ->SCL_PIN,
    	};
    	opRet = opSocI2CInit(&i2c_config);          /* SDA&SCL GPIO INIT */
    	PR_NOTICE("SocI2CInit = %d",opRet);
    
    	UCHAR_T i;
    	for(i = 0; i  < 25; i++) {
    		sh1106_send_cmd(oled_init_cmd[i]);
    	}
    }
    
  • tuya_sh1106_full()tuya_sh1106_clear() 显示屏幕全亮或清空显示内容:

    VOID tuya_sh1106_full(VOID)
    {
    	UCHAR_T i,j,k;
    	UCHAR_T *p;
    	for(i = 0; i < 4; i++) {
    		for(j = 0; j < 16; j++) {
    			OLED_GRAM[i][j] = full_buff;
    		}
    	}
    	for(i = 0; i < OLED_PAGE_NUMBER; i++) {
    		sh1106_page_set(i);
    		sh1106_column_set(0);
    		for(j = 0; j < (OLED_COLUMN_NUMBER/8); j++) {
    			p = OLED_GRAM[i][j];
    			for(k = 0; k < 8; k++) {
    				sh1106_send_data(*p);
    				p++;
    			}
    		}
    	}
    }
    
    VOID tuya_sh1106_clear(VOID)
    {
    	UCHAR_T i,j,k;
    	UCHAR_T *p;
    	for(i = 0; i < 4; i++) {
    		for(j = 0; j < 16; j++) {
    			OLED_GRAM[i][j] = clear_buff;
    		}
    	}
    	for(i = 0; i < OLED_PAGE_NUMBER; i++) {
    		sh1106_page_set(i);
    		sh1106_column_set(0);
    		for(j = 0; j < (OLED_COLUMN_NUMBER/8); j++) {
    			p = OLED_GRAM[i][j];
    			for(k = 0; k < 8; k++) {
    				sh1106_send_data(*p);
    				p++;
    			}
    		}
    	}
    }
    
  • tuya_sh1106_gram_point_set() 按坐标点更改显存内容,改变将要显示的图案,传参分别为页数、行数、字模缓存数组的首地址:

    VOID tuya_sh1106_gram_point_set(UCHAR_T x, UCHAR_T y, CONST UCHAR_T *ptr_pic)
    {
    	UCHAR_T i;
    	UCHAR_T *p;
    
    	if((x < 4)&&(y < 16)) {
    		OLED_GRAM[x][y] = ptr_pic;
    	}
    	p = OLED_GRAM[x][y];
    
    	sh1106_page_set(x);
    	sh1106_column_set(y*8);
    
    	for(i = 0; i < 8; i++) {
    		sh1106_send_data(*p);
    		p++;
    	}
    }
    
  • tuya_sh1106_display() 根据显存内容显示图像:

    VOID tuya_sh1106_gram_point_set(UCHAR_T x, UCHAR_T y, CONST UCHAR_T *ptr_pic)
    {
    	UCHAR_T i;
    	UCHAR_T *p;
    
    	if((x < 4)&&(y < 16)) {
    		OLED_GRAM[x][y] = ptr_pic;
    	}
    	p = OLED_GRAM[x][y];
    
    	sh1106_page_set(x);
    	sh1106_column_set(y*8);
    
    	for(i = 0; i < 8; i++) {
    		sh1106_send_data(*p);
    		p++;
    	}
    }
    
  • tuya_sh1106_on()tuya_sh1106_off() 点亮屏幕和熄灭屏幕,由于屏幕的点亮和熄灭需要时间,所以调用前后需要至少 150ms 的延时:

    VOID tuya_sh1106_on(VOID)
    {
    	sh1106_send_cmd(0x8D);
    	sh1106_send_cmd(0x14);
    	sh1106_send_cmd(0xAF);
    }
    
    VOID tuya_sh1106_off(VOID)
    {
    	sh1106_send_cmd(0x8D);
    	sh1106_send_cmd(0x10);
    	sh1106_send_cmd(0xAE);
    }
    

完成屏幕驱动后,就可以使用取模软件将时间转换出字模来显示。

  • 要获取本地时间,首先需包含头文件 uni_time.h

  • 定义一个本地时间结构体变量,然后作为传参调用 uni_local_time_get() 接口获取时间:

    POSIX_TM_S cur_time;
    
    if( uni_local_time_get(&cur_time) != OPRT_OK ) {
    	PR_NOTICE("cant get local time");
    }
    lamp_ctrl_data.time_hour = cur_time.tm_hour;
    lamp_ctrl_data.time_min = cur_time.tm_min;
    
  • 按位解析时间,将对应字模缓存传入显存,然后开启显示:

    for(i = 4; i < 8; i++) {
    	tuya_sh1106_gram_point_set(0,i,&diplay_buffer_time[(i+14)*OLED_PIX_HEIGHT]);
    	tuya_sh1106_gram_point_set(1,i,&diplay_buffer_time[(i+14)*OLED_PIX_HEIGHT+8]);
    }
    
    if(lamp_ctrl_data.time_hour < 10) {
    	tuya_sh1106_gram_point_set(0,9,&diplay_buffer_time[0]);
    	tuya_sh1106_gram_point_set(1,9,&diplay_buffer_time[8]);
    }else {
    	tuya_sh1106_gram_point_set(0,9,&diplay_buffer_time[(lamp_ctrl_data.time_hour/10)*OLED_PIX_HEIGHT]);
    	tuya_sh1106_gram_point_set(1,9,&diplay_buffer_time[(lamp_ctrl_data.time_hour/10)*OLED_PIX_HEIGHT+8]);
    }
    tuya_sh1106_gram_point_set(0,10,&diplay_buffer_time[(lamp_ctrl_data.time_hour%10)*OLED_PIX_HEIGHT]);
    tuya_sh1106_gram_point_set(1,10,&diplay_buffer_time[(lamp_ctrl_data.time_hour%10)*OLED_PIX_HEIGHT+8]);
    
    //flicker effect of ':'
    tuya_sh1106_gram_point_set(0,11,&diplay_buffer_time[10*OLED_PIX_HEIGHT]);
    tuya_sh1106_gram_point_set(1,11,&diplay_buffer_time[10*OLED_PIX_HEIGHT+8]);
    
    if(lamp_ctrl_data.time_min < 10) {
    	tuya_sh1106_gram_point_set(0,12,&diplay_buffer_time[0]);
    	tuya_sh1106_gram_point_set(1,12,&diplay_buffer_time[8]);
    }else {
    	tuya_sh1106_gram_point_set(0,12,&diplay_buffer_time[(lamp_ctrl_data.time_min/10)*OLED_PIX_HEIGHT]);
    	tuya_sh1106_gram_point_set(1,12,&diplay_buffer_time[(lamp_ctrl_data.time_min/10)*OLED_PIX_HEIGHT+8]);
    }
    
    tuya_sh1106_gram_point_set(0,13,&diplay_buffer_time[(lamp_ctrl_data.time_min%10)*OLED_PIX_HEIGHT]);
    tuya_sh1106_gram_point_set(1,13,&diplay_buffer_time[(lamp_ctrl_data.time_min%10)*OLED_PIX_HEIGHT+8]);
    
    tuya_sh1106_display();
    

5:电量显示和低电告警

本 Demo 通过 ADC 读取电池电压的 1/2 分压。

根据读到的 ADC 值换算回电压值并将剩余电压用百分比的方式显示在OLED屏幕上。同时当电压值低于一定水平时,例如20%,驱动蜂鸣器实现低电量告警。

  • 调用 Tuya HAL 接口,初始化 ADC ,获取 ADC 值:

    USHORT_T adc_value = 0;
    float adc_voltage = 0.0;
    tuya_hal_adc_init(&tuya_adc);
    tuya_hal_adc_value_get(TEMP_ADC_DATA_LEN, &adc_value);
    PR_NOTICE("----adc_value = %d----",adc_value);
    adc_voltage = 2.4*((float)adc_value/2048);
    
    PR_NOTICE("----adc_voltage = %f----",adc_voltage);
    tuya_hal_adc_finalize(&tuya_adc);
    
  • 根据计算出的电压值估算电池大概的剩余电量,并在低电量时驱动蜂鸣器:

    if(adc_voltage > 1.95) {
    	lamp_ctrl_data.Battery_remain = 100;
    	return ;
    }
    if(adc_voltage > 1.92) {
    	lamp_ctrl_data.Battery_remain = 80;
    	return ;
    }
    if(adc_voltage > 1.89) {
    	lamp_ctrl_data.Battery_remain = 60;
    	return ;
    }
    if(adc_voltage > 1.86) {
    	lamp_ctrl_data.Battery_remain = 40;
    	return ;
    }
    if(adc_voltage > 1.8) {
    	lamp_ctrl_data.Battery_remain = 20;
    	if(lamp_ctrl_data.Low_pow_alarm) {
    		__ctrl_beep(300);
    	}
    	return ;
    }
    
  • 根据剩余电量,解析出对应字模,显示在屏幕上:

    STATIC VOID lamp_display_power(VOID)
    {
    	if(lamp_ctrl_data.Lamp_switch != TRUE) {
    		return ;
    	}
    
    	UCHAR_T i = 0;
    	for(i = 4; i < 9; i++) {
    		tuya_sh1106_gram_point_set(2,i,&diplay_buffer_time[(i+8)*OLED_PIX_HEIGHT]);
    		tuya_sh1106_gram_point_set(3,i,&diplay_buffer_time[(i+8)*OLED_PIX_HEIGHT+8]);
    	}
    
    	//flicker effect of ':'
    	tuya_sh1106_gram_point_set(2,9,&diplay_buffer_time[17*OLED_PIX_HEIGHT]);
    	tuya_sh1106_gram_point_set(3,9,&diplay_buffer_time[17*OLED_PIX_HEIGHT+8]);
    
    	if(lamp_ctrl_data.Battery_remain == 100) {
    		tuya_sh1106_gram_point_set(2,10,&diplay_buffer_time[OLED_PIX_HEIGHT]);
    		tuya_sh1106_gram_point_set(3,10,&diplay_buffer_time[OLED_PIX_HEIGHT+8]);
    
    		tuya_sh1106_gram_point_set(2,11,&diplay_buffer_time[0]);
    		tuya_sh1106_gram_point_set(3,11,&diplay_buffer_time[8]);
    		tuya_sh1106_gram_point_set(2,12,&diplay_buffer_time[0]);
    		tuya_sh1106_gram_point_set(3,12,&diplay_buffer_time[8]);
    	}else {
    		tuya_sh1106_gram_point_set(2,10,&diplay_buffer_time[17*OLED_PIX_HEIGHT]);
    		tuya_sh1106_gram_point_set(3,10,&diplay_buffer_time[17*OLED_PIX_HEIGHT+8]);
    
    		tuya_sh1106_gram_point_set(2,11,&diplay_buffer_time[(lamp_ctrl_data.Battery_remain/10)*OLED_PIX_HEIGHT]);
    		tuya_sh1106_gram_point_set(3,11,&diplay_buffer_time[(lamp_ctrl_data.Battery_remain/10)*OLED_PIX_HEIGHT+8]);
    		tuya_sh1106_gram_point_set(2,12,&diplay_buffer_time[(lamp_ctrl_data.Battery_remain%10)*OLED_PIX_HEIGHT]);
    		tuya_sh1106_gram_point_set(3,12,&diplay_buffer_time[(lamp_ctrl_data.Battery_remain%10)*OLED_PIX_HEIGHT+8]);
    	}
    
    	tuya_sh1106_gram_point_set(2,13,&diplay_buffer_time[11*OLED_PIX_HEIGHT]);
    	tuya_sh1106_gram_point_set(3,13,&diplay_buffer_time[11*OLED_PIX_HEIGHT+8]);
    
    }
    

6:光照传感器

为了实现后续的自动开关灯功能,硬件上还需借助光照传感器来检测当前的亮暗程度,从而判断当前是否需要允许自动开灯。

选用的传感器型号为 BH1750,通过 I2C 协议与 SoC 进行通信,相关接口封装都在 bh1750.c 文件中。模块具体使用流程如下:

  1. 调用tuya_bh1750_init初始化模组:

    VOID lamp_device_init(VOID)
    {
    	......
    	tuya_bh1750_init(&bh1750_int_param);
    	......
    }
    
  2. 调用tuya_bh1750_get_bright_value获取光照强度值:

    VOID lamp_light_detect(VOID)
    {
    	lamp_ctrl_data.Light_intensity = tuya_bh1750_get_bright_value();
    	PR_NOTICE("light_intensity_value = %d",lamp_ctrl_data.Light_intensity);
    }
    

7:坐姿检测和自动开关灯

本 Demo 采用的微波雷达通过串口不间断的向 SoC 发送包含运动状态、距离和能量的不定长字符串,而soc则根据这些参数来实现简易的坐姿检测,并配合环境光照强度实现自动开关灯。

  1. 通过检索特定的符号字符来读取需要的参数:

    VOID lamp_get_sensor_data(VOID)
    {
    
    	UCHAR_T data[50];
    	memset(data, 0, sizeof(data));
    
    	CHAR_T opt;
    	opt = get_radar_data(data,50);
    	if(opt == 0){
    		UCHAR_T i;
    		if((data[0] == 'S')&&(data[6] == ':')) {
    			if(data[8] == '[') {
    				lamp_ctrl_data.Radar_sensor = TRUE;
    			}else {
    				lamp_ctrl_data.Radar_sensor = FALSE;
    				lamp_ctrl_data.Human_distance = 0;
    				PR_NOTICE("----NO MAN AROUND----");
    			}
    		}
    		if(lamp_ctrl_data.Radar_sensor == FALSE) {
    			return ;
    		}
    		for(i=0;i<50;i++) {
    			if(data[i]=='R') {
    				if((data[i+8] >= '0')&&(data[i+8] <= '9')) {
    					lamp_ctrl_data.Human_distance = ((data[i+7] - '0') * 10) + (data[i+8] - '0');
    				}else {
    					lamp_ctrl_data.Human_distance = (data[i+7] - '0');
    				}
    				PR_NOTICE("----Human_distance = %d----",lamp_ctrl_data.Human_distance);
    				return ;
    			}
    		}
    	}
    }
    
  2. 在App开启或按键开启坐姿提醒功能的前提下,当距离小于一定数值时,触发坐姿告警:

    VOID STATIC lamp_sit_remind(VOID)
    {
    	if(lamp_ctrl_data.Sit_remind != TRUE) {
    		alert_count = 0;
    		return ;
    	}
    
    	if((lamp_ctrl_data.Human_distance <= 5)&&(lamp_ctrl_data.Radar_sensor == TRUE)) {
    		PR_NOTICE("----enter sit remind----");
    		alert_count++;
    		if(alert_count >= 3) {
    			__ctrl_beep(300);
    		}
    	}else{
    		alert_count = 0;
    	}
    }
    
  3. 在App开启自动开关灯功能的前提下,当雷达检测到有人靠近时,若当前环境光照强度很弱则自动打开灯光,同时当检测到无人在附近一定时间后,自动关闭灯光:

    VOID STATIC lamp_light_control(VOID)
    {
    	if((light_mode_old != lamp_ctrl_data.Light_mode)&&(lamp_ctrl_data.Lamp_switch == TRUE)) {
    		lamp_pwm_set(lamp_ctrl_data.Light_mode,user_pwm_duty);
    	}
    	light_mode_old = lamp_ctrl_data.Light_mode;
    
    
    	if(lamp_ctrl_data.Auto_light != TRUE) {
    		return ;
    	}
    
    	if(lamp_ctrl_data.Radar_sensor == FALSE) {
    		lamp_ctrl_data.Lamp_switch = FALSE;
    		lamp_pwm_off();
    	}else if((lamp_ctrl_data.Human_distance <= DISTANCE_THRESHOLD)&&\\
    			(lamp_ctrl_data.Light_intensity <= LIGHT_INTENSITY_THRESHOLD)) {
    		if(lamp_ctrl_data.Lamp_switch == FALSE) {
    			lamp_ctrl_data.Lamp_switch = TRUE;
    			lamp_pwm_set(lamp_ctrl_data.Light_mode,user_pwm_duty);
    		}
    	}
    }
    

8:编译和烧录

在 Linux 终端输入命令运行 SDK 环境目录下的 build_app.sh 脚本来编译代码生成固件。固件生成路径为 apps > APP_PATH > output

  • 命令格式:

    build_app.sh <APP_PATH> <APP_NAME> <APP_VERSION>
    
  • 命令示例:

    /home/share/samba/ci/ty_iot_wf_bt_sdk_bk7231t$ sudo sh build_app.sh apps/bk7231t_plant_grow_mach_demo bk7231t_plant_grow_mach_demo 1.0.0
    
  • 成功返回示例:若出现下图所示提示,则表示编译成功,固件已经生成。

将固件烧录至模组后,即可开始功能调试阶段。有关烧录和授权方式请参考 WB 系列模组烧录授权

五:设备控制

设备控制的第一步是添加设备并配网,您可以在移动应用商店中(例如 App Store)下载涂鸦智能或智能生活 App。然后打开 App 进行设备添加和控制。

小结

至此智能感应台灯就完成了,它可以 App 远程控制、自动调光、坐姿检测、工作计时、低电量报警等功能。对于手势调节和坐姿检测,我们只提供基本的方法检测,大家可以用更高工作频段的微波雷达或者单发多收的微波雷达来实现尽可能多的功能。同时您可以基于 涂鸦 IoT 平台 丰富它的功能,也可以更加方便的搭建更多智能产品原型,加速智能产品的开发流程。