
-
功能需求
功能 需求描述 服药闹钟 支持周定时。
支持使用 App 设置服药时间。错过提醒 定时时间到达后,蜂鸣器开始鸣叫。
若未检测到药盒打开,则三分钟后继续提醒。服药提醒 定时时间到达后蜂鸣器开始鸣叫。
设备上报该 DP 给 App,提醒用户服药。用户可手动关闭提醒。药盒查找 当按下“药盒查找”按钮时,蜂鸣器间隔发声。 低电量报警 检测到电量低于 10% 时,药盒开始报警。 药仓模式 药盒支持大仓、小仓两种不同模式,用户可根据实际需求手动切换。 环境搭建
产品开发
-
登录 涂鸦 IoT 开发平台产品创建页。
-
在左侧 标准类目 中,选择 传感 > 时钟气象站 > 温湿度时钟。
说明:由于用到周定时功能,在 IoT 开发平台现有智能药盒品类下面没有周定时功能面板,因此选择支持周定时面板的品类。
-
完善产品信息。
填写好产品名称,选择通讯协议为蓝牙低功耗,功耗类型为低功耗。
关于创建产品,详情可查看 按品类创建产品。
-
添加产品功能。
-
选择 DP ID 为 1、2、4、8、14、17、24、26、27 的标准功能并单击 确定 添加标准功能。

-
在 标准功能 列表中,单击右侧 操作 栏中的 编辑,将 DP ID 为 17、24、26、27 的功能点分别修改为药仓模式、低电量报警、错过提醒、服药提醒。
关于添加标准功能,详情可查看 添加标准功能。注意:温湿度 DP 为必选项,否则面板无法正常打开。
-
-
选择设备面板。
单击上方 设备面板 页签,任选一个公版面板。
关于选择设备面板,详情可查看 配置 App 界面。
-
完成硬件开发。
-
选择云端接入方式为 TuyaOS。
-
选择云端接入硬件为 BT3L Bluetooth 模组。
-
在硬件的右方的操作栏下,单击 免费领取 2 个激活码,根据页面指导提交订单,免费领取激活码。
关于硬件开发,详情可查看 硬件开发。说明:此步骤仅用于选择模组拿到 Lisence 用于联网,实际使用的的模组为 TYBN1。
单击 TYBN1 模组规格书 ,查看模组详情。
-
获取 SDK
-
单击 Downloads 选择版本,获取 nRF52832 SDK。

-
本次用到的 SDK 版本为 nRF5 SDK 15.3.0。因此,选择 15.3.0 nRF5 SDK。

-
下拉页面至末端,默认勾选所有选项,单击 Download files (.zip)。

注意:不要将下载的 SDK 放在 Linux 共享文件夹或者中文路径下,否则在编译时会出现 No such file or directory 等的未知错误。
-
下载完成后找到对应文件夹进行文件解压。解压完成后,打开
DeviceDownload文件夹,解压nRF5SDK153059ac345文件夹。 -
获取 TYBN1 SDK。
-
下载完成后找到对应文件夹进行文件解压。解压完成后,将
tuya-ble-sdk-demo-project-nrf52832-V2.1.0文件夹复制到原厂 SDK 的DeviceDownload\nRF5_SDK_15.3.\examples\ble_peripheral文件夹下。 -
获取 ARM CMSIS4.5.0。
-
下载安装完成后,打开
tuya-ble-sdk-demo-project-nrf52832-V2.1.0\pca10040\s132\arm5_no_packs文件夹下的 Keil 工程。
下载芯片对应安装包
-
第一次打开工程会自动下载 nRF52832 芯片对应的安装包。
-
(可选)安装包下载完成后,如果第一次开发,会显示以下报错信息,可忽略该报错,继续完成安装。
Cannot execute external request (Install Pack, "NordicSemiconductor:nRF_DeviceFamilyPack"): Pack not found Cannot execute external request (Install Pack, "NordicSemiconductor:nRF_DeviceFamilyPack:8.24.1"): Pack not found -
按下图所示继续安装。

-
安装完成后重启 Keil 即可编译。
-
(可选)由于 Keil 版本问题,编译时可能会出现以下报错信息 :
RTE\Device\nRF52832_xxAA\system_nrf52.c(30): error: #5: cannot open source input file "nrf52_erratas.h": No such file or directory只需用
DeviceDownload\nRF5SDK153059ac345\nRF5_SDK_15.3.0_59ac345\modules\nrfx\mdk文件夹下的system_nrf52.c替换掉DeviceDownload\nRF5SDK153059ac345\examples\ble_peripheral\tuya-ble-sdk-demo-project-nrf52832-V2.1.0\pca10040\s132\arm5_no_packs\RTE\Device\nRF52832_xxAA文件夹下的system_nrf52.c文件,然后关闭 Keil 工程,再重新打开该工程即可编译通过。
修改 PID、UUID、Auth_key、MAC 地址
-
在 Keil 工程中,打开
tuya_ble_sdk_demo文件夹找到tuya_ble_sdk_demo.h文件。 -
回到涂鸦 IoT 开发平台,进入采购订单的调试商品&样品订单,在右侧 操作 栏中,单击 下载授权码清单。
-
打开授权码清单文件,将授权码清单中的 UUID、Auth_key、MAC 地址以及创建产品的 PID 填入下图所示位置。

说明:如果您不知道产品 PID,可到 产品开发 列表中,找到产品,在产品下方可看到产品 ID。
-
在
tuya_ble_sdk_demo.c文件中将 use_ext_license_key、device_id_len 的值与DEVICE_ID_LEN的值分别改为 1 与 16,否则上步修改的 UUID、Auth_key、MAC、PID 信息不会生效。
设备连线
编译成功后将 J-Link 烧录器连接到开发板,连线如下。
模组对应引脚 串口对应引脚 VCC VCC (3.3V) 模组对应引脚 JLINK 对应引脚 SWDIO SWDIO SWC SWCLK GND GND 日志查看
-
烧录 Nodric 协议栈固件
-
在开始菜单中搜索
J-Flash Vx.xxb(x.xx 版本号)并打开,单击 File > New Project,在 Target device 中选择 Nordic Semi nRF52832_xxAA ,确认无误单击 OK。
-
单击 File > Open data file,选中
tuya-ble-sdk-demo-project-nrf52832-V2.1.0\pca10040\s132\arm5_no_packs\hex\material文件夹下的s132_nrf52_6.1.1_softdevice.hex文件进行烧录。说明:Softdevice 是 Nordic 蓝牙协议栈的名称,整个开发过程中只需下载一次。

-
单击 Target > Connect,等待连接成功。
-
单击 Target > Production Programming 开始下载协议栈固件。
-
下载成功后,下方日志口显示
Data file opened successfully字样。 -
单击 Target > Disconnect 断开连接。
-
-
使能日志
Log 默认是关闭的,通过修改宏来使能日志。
- 找到
tuya-ble-sdk-demo-project-nrf52832-V2.1.0\tuya_ble_sdk_demo\board\nRF52832\tuya_ble_port文件夹下的custom_tuya_ble_config.h文件,将第 113 行TUYA_APP_LOG_ENABLE日志输出使能。 - 找到
ble_peripheral\tuya-ble-sdk-demo-project-nrf52832-V2.1.0\tuya_ble_sdk_demo\board文件夹下的board.h文件,将第 38 行TY_LOG_ENABLE日志输出使能。 - 修改、编译完成后将 hex 文件烧录到模组。
- 找到
-
查看日志
-
打开
J-link RTT Viewer Vx.xxb后自动弹出以下对话框:
-
选择 USB,在
Specify Target Device中单击出现下面的弹窗。
-
在红框内输入
nRF52832_xxAA,按回车后双击选定。 -
依次选择上图红框选项,单击 OK 完成设置。
-
当界面内出现以下内容说明连接成功,可以正常查看日志。

-
-
修改测试宏

打开工程目录
tuya_ble_sdk_demo下的tuya_ble_sdk_test.h文件,将第 29 行宏定义 TUYA_BLE_SDK_TEST 关闭。由于该宏为测试模式的开关,打开会导致测试功能模块占用相关 I/O 口资源使部分 I/O 口无法正常使用。
关于 nRF52832 SDK 的使用教程,可查看 学习笔记。
功能实现
药盒查找
在手机与药盒配网的情况下,当用户忘记药盒的位置时,可以点按 App 上的 药盒查找 功能,蜂鸣器间隔发声。
当检测到药盒被打开或者手动关闭 App 上的 药盒查找开关时,停止蜂鸣器发声。
部分功能代码如下:
// APP send find common case DP_BOX_FIND: if (1 == dp_data[4]) { tuya_remind_start_find(); } else if (0 == dp_data[4]) { tuya_beep_stop_play(); } break; // Buzzer interval warning void tuya_remind_start_find(void) { find_handle = 1; return ; } int tuya_remind_box_find(void) { static uint32_t sn = 0; while (find_handle) { // wait for the DP_BOX_FIND command to send from the APP tuya_beep_box_find_play(BEEP_HZ, 50); tuya_ble_device_delay_ms(200); tuya_beep_stop_play(); tuya_ble_device_delay_ms(1000); if (BOX_OPEN == nrf_gpio_pin_read(KEY_OPEN) ) { tuya_beep_stop_play(); find_buf[4] = 0; tuya_ble_dp_data_send(sn++, DP_SEND_TYPE_ACTIVE, DP_SEND_FOR_CLOUD_PANEL, DP_SEND_WITHOUT_RESPONSE, find_buf, BUf_LEN); break; } } find_handle = 0; return 0; }低电量报警
智能药盒采用 3.7V 锂电池供电,电池在充满电的情况下,输出电压为 4.2V。没电时的输出电压 3.7V(满电和没电的实际输出电压都可能有偏差)。
电量采集代码:
static uint16_t tuya_batmon_batval_get(uint16_t vref, uint16_t sample, float ratio) { int i = 0; uint16_t Bat_val = 0; uint16_t adc_Avg = 0; uint16_t voltage_val = 0; nrf_saadc_value_t val_bat = 0; for (i = 0; i < 5; i++) { nrf_drv_saadc_sample_convert(ADC_CHANNEL0, &val_bat); adc_Avg += val_bat; } adc_Avg /= 5; voltage_val = (adc_Avg * vref) / sample; // ADC Sample 10bit Ref voltage 3.6V Bat_val = voltage_val * ratio; // Voltage divider The actual battery voltage value is 2 times the voltage value taken by the adc unit:mv // TUYA_APP_LOG_INFO("battery_val:%dmv", Bat_val); return Bat_val; } int tuya_batmon_bat_level_report(void) { uint8_t i = 0; uint8_t op_ret; uint16_t Bat_val = 0; static uint32_t sn = 0; for (i = 0; i < 5; i++) { Bat_val = tuya_batmon_batval_get(3600, 1024, 2); if (Bat_val <= pet_10) { battery_buf[4] = ALARM; nrf_gpio_pin_write(LED_SWITCH, 1); // low power, red light op_ret = tuya_ble_dp_data_send(sn++, DP_SEND_TYPE_ACTIVE, DP_SEND_FOR_CLOUD_PANEL, DP_SEND_WITHOUT_RESPONSE, battery_buf, DP_BUF_LEN(battery_buf)); if (op_ret != TUYA_BLE_SUCCESS) { TUYA_APP_LOG_ERROR("dp data send failed, error code:%d", op_ret); } } else { battery_buf[4] = NORMAL; nrf_gpio_pin_write(LED_SWITCH, 0); // enough power, green light op_ret = tuya_ble_dp_data_send(sn++, DP_SEND_TYPE_ACTIVE, DP_SEND_FOR_CLOUD_PANEL, DP_SEND_WITHOUT_RESPONSE, battery_buf, DP_BUF_LEN(battery_buf)); if (op_ret != TUYA_BLE_SUCCESS) { TUYA_APP_LOG_ERROR("dp data send failed, error code:%d", op_ret); } } tuya_ble_device_delay_ms(1000); } return 0; }经过实际测量,万用表测的电压为 4.15V,ADC 采集电压为 4154mV。
周定时
产品的 DP ID 14 为周定时功能,数据类型为 RAW,具体的协议格式如下:
下发方式: 全量、16 进制下发和上报,即任何 1 个闹钟上报、下发、删除、更新,都全部上报。按序列拼接即可,无顺序、无编辑等操作。字节说明 字节长度 说明 #1 协议版本号 1 0x00:初始版本 #2 开关+通道号 1 - bit7: 开/关 ,表示此条闹钟开启和关闭,默认为开启。
- bit0~bit6: 表示通道 0~127(通道是从 0 开始,无需关心)。
该值为 128 则闹钟开启,为 0 时则闹钟关闭。
#3 星期 1 - 如果全为 0,表示单次模式,只生效一次,否则为循环模式。
- 判断相应位是否置 1,置 1 表示当天生效。
如为 0x42,表示任务在星期六和星期一生效。注意:必须保证相应的任务开关是处于开启状态)。bit0~bit6 对应周天到周六,bit7 保留为 0。
#4 时间-小时 1 范围允许值为 0~23 #5 时间-分钟 1 范围允许值为 0~59 #6 执行动作 1 0x00 无此功能,不展示,作为未来预留。 #7 1 0x00:无此功能,不展示,作为未来预留。 #8 1 0x00:无此功能,不展示,作为未来预留。 #9 服药数量 18 - 仓位模式为小仓时(3 个仓),1、2、3 号仓服用量分别为 3,1,5,则输入格式为 103201305;
- 仓位模式为大仓时(2 个仓),1、2 号仓服用量分别为 7,14,则输入格式为 107214。
说明:必须按照 ”药盒号 + 服用量”两个字节表示,服药量为个位数时也用两位数表示,中间不能有空格,不能出现汉字。
#10 预留 1【新增】 1 0x00:无此功能,不展示,作为未来预留。 #11 预留 2【新增】 1 0x00:无此功能,不展示,作为未来预留。 -
RTC 设计思路:
由于药盒只有在使用状态下才是正常功耗,其他时刻均处于低功耗,因此周定时采用 RTC 实现计数。
药盒定时的触发时间精度为分钟级别,唤醒事件处理函数依据 RTC 中断每隔 60 秒查询一次 RTC 时间,然后将当前时间与定时时间做比对。
如果时间吻合,判断当天是否有定时任务。如果有定时任务则退出低功耗,各外设开始正常工作,开始蓝牙广播。
OLED 屏幕显示每个仓位的服药数量,蜂鸣器报警、电池电量检测等任务。
当一个完整的服药动作完成后,药盒再次进入低功耗,等待下一个服药闹钟的到来将其唤醒。 -
RTC 唤醒事件处理部分代码:
if (1 == rtc_flag) { // 1s rtc_flag = 0; ty_rtc_get_time(&p_timestamp); // gets the timestamp once per 60s utc_sec = p_timestamp + EAST_8_ZONE; tuya_ble_utc_sec_2_mytime(utc_sec, &rtc_time, 0); // convert Beijing timestamp } else { return 0; } for (i = 0; i < medic_pro->num; i++) { // query each timing info if (medic_pro->clock[i].hour == rtc_time.nHour && medic_pro->clock[i].min == rtc_time.nMin) { if (0x00 == medic_pro->clock[i].sw) { // on_off continue; } if (1 == (medic_pro->clock[i].week & (1 << rtc_time.DayIndex))) { // repeat timing /* wake up */ tuya_remind_wake_up(); tuya_remind_dose_show(medic_pro->clock[i].box1, medic_pro->clock[i].box2, medic_pro->clock[i].box3); tuya_rtc_wakeup_process(); } else if (0 == medic_pro->clock[i].week) { // once timing // tuya_remind_wake_up(); tuya_remind_dose_show(medic_pro->clock[i].box1, medic_pro->clock[i].box2, medic_pro->clock[i].box3); tuya_rtc_wakeup_process(); medic_pro->clock[i].sw = 0; } } } -
周定时协议解析部分代码:
int tuya_local_time_parse(uint8_t *dp_data, uint8_t len) { if (NULL == dp_data) { TUYA_APP_LOG_ERROR("invalid parse dp_data!!!"); return TUYA_BLE_ERR_INVALID_PARAM; } #if 0 //if want to see dp data from APP send open this macro when debug for (int a = 0; a < len; a++) { TUYA_APP_LOG_INFO("time_data[%d] = %d", a, dp_data[a]); } #endif int i = 0; int time_cnt = 0; TY_TIME_PRO_T *time_pro = NULL; time_pro = (TY_TIME_PRO_T *)dp_data; time_cnt = (len - 1) / sizeof(TY_SIGEL_TIME_PRO_T); // (len - 1) -> (clock data) - (1 byte clock version) if (time_cnt > 5) { // The number of alarm clocks exceeds the upper limit TUYA_APP_LOG_ERROR("More than 5 alarm clocks"); return TUYA_BLE_ERR_INVALID_PARAM; } else if (0 == time_cnt) { // alarm clock num is 0 TUYA_APP_LOG_INFO("No alarm clock is available"); return 0; } /* parse version number */ if (time_pro->ver != 0) { TUYA_APP_LOG_ERROR("Invalid timing version!"); return TUYA_BLE_ERR_INTERNAL; } /* parse 'on_off'、week、hour、minute、dose */ medic_info.num = time_cnt; for (i = 0; i < medic_info.num; i++) { medic_info.clock[i].sw = time_pro->singel_time[i].sw; medic_info.clock[i].week = time_pro->singel_time[i].week; medic_info.clock[i].hour = time_pro->singel_time[i].hour; medic_info.clock[i].min = time_pro->singel_time[i].min; medic_info.clock[i].box1 = PER_BOX_MEDICINE(time_pro->singel_time[i].box_dose[1], (time_pro->singel_time[i].box_dose[2])); medic_info.clock[i].box2 = PER_BOX_MEDICINE(time_pro->singel_time[i].box_dose[4], (time_pro->singel_time[i].box_dose[5])); medic_info.clock[i].box3 = PER_BOX_MEDICINE(time_pro->singel_time[i].box_dose[7], (time_pro->singel_time[i].box_dose[8])); } return TUYA_BLE_SUCCESS; }
服药提醒、错过提醒
当定时时间到达后上报一条 DP,App 显示服药提醒。第一次响铃 1 分钟后检测到没有打开药盒,三分钟后再次提醒。
服药提醒与错过提醒部分代码:
if (1 == medic_pro->clock[n].week & (1 << rtc_time.DayIndex) ) { /* wake up */ tuya_remind_wake_up(); tuya_remind_dose_show(medic_pro->clock[n].box1, medic_pro->clock[n].box2, medic_pro->clock[n].box3); tuya_beep_medicine_alarm(BEEP_HZ, 50); // about ring 1min tuya_remind_ble_connect(); tuya_remind_key_scan(); ty_oled_clear(); tuya_remind_miss_alarm(); // after 3mins ring 1min, check box is not open enter sleep n++; if (n == medic_pro->num) { n = 0; } if (3 == tuya_ble_connect_status_get()) { remind_buf[4] = 1; tuya_ble_dp_data_send(0, DP_SEND_TYPE_ACTIVE, DP_SEND_FOR_CLOUD_PANEL,\ DP_SEND_WITHOUT_RESPONSE, remind_buf, 5); tuya_batmon_bat_level_report(); // battery value check } tuya_ble_timer_create(&sec_sleep_timer, 2000, TUYA_BLE_TIMER_SINGLE_SHOT, sec_enter_sleep_cb); tuya_ble_timer_start(sec_sleep_timer); }药仓模式
根据不同的药仓模式,打开不同的指示灯,指示指定药仓,达到提醒服用指定药物的功能。
药仓指示功能的部分代码:
case DP_BOX_MODE:// 药仓模式选择 ty_flash_write(0x66000, &dp_data[4], 1); // write box_mode to flash break; int tuya_remind_box_mode_led_play(uint8_t mode) { if (mode > 1) { TUYA_APP_LOG_ERROR("box mode select error"); return TUYA_BLE_ERR_INVALID_PARAM; } switch (mode) { case SMALL_BOX: tuya_remind_small_mode_led(); break; case LARGE_BOX: tuya_remind_large_mode_led(); break; default: break; } return 0; } -
小结
智能药盒的 DIY 分享到到此结束。您还可以参考本教程,开发改造更多更有意思的智能药盒方案,比如将蜂鸣器换成振动马达,OLED 屏改成断码显示屏,充电锂电池换成纽扣电池方案等。
144

被折叠的 条评论
为什么被折叠?



