与普通药盒相比,智能药盒能起到吃药提醒作用。然而,市面上现存的智能药盒大部分只有单一固定大小的药仓,或通过隔板变换仓位体积,仓位不密封容易受潮。另外,大部分智能药盒虽然能起到提醒服药的作用,但是无法提示药品服用数量。针对这些问题,本教程设计了一款多功能智能药盒。
本教程对下图所示药盒进行智能化改造。该药盒具有以下特点:
该药盒有 2 种尺寸的药仓,可以根据所需存放药丸的数量选择合适的大小。
该药盒的药仓独立且具备单独的盖子,这样我们打开透明外盒的盖子时,不会发生药因抖动混在一起或洒出来的"人间悲剧"。每个药仓独立可取,如可以保留 3 个药盒仓位,空出的位置方便放置改造的线路板;
药盒透明外盒有密封圈,密封性好。使用智能药盒可以让药物在湿度高的地方不易受潮。
本方案基于涂鸦智能的一款低功耗蓝牙模块作为控制单元和无线连接单元。该模组一共有 12 个 GPI/O 口可以使用,其中 I/O 2~4 可配置成 ADC 口,I/O 8 可为串口 TXD 口,I/O 20 可为串口 RXD 口。
"低电量提醒"的设计思路:
通过使用模组的 ADC 采样口对电池的电压进行采集,该 ADC 采集口 可允许输入的电压范围为 0~3.6V,由于电池电压(最高 4.2V)超出了该范围,故设计电阻分压,使其符合允许的输入电压范围。外围电路预留调试接口以及复位按键。
我们选用蜂鸣器,以实现药盒声音提醒的功能。本次挑选的蜂鸣器为 GSC5025YA 贴片式电磁无源蜂鸣器。
蜂鸣器特点:
设计思路:
由于蜂鸣器的驱动电流较大,我们没有办法直接使用 I/O 口驱动,所以我们使用三极管、续流二极管、电阻、电容组成放大电路来驱动蜂鸣器。
原理图设计:
原理图说明:
部件 | 说明 |
---|---|
1N4148W 二极管 | 由于无源蜂鸣器是感性元件,电流不能瞬变,需要有一个续流二极管提供续流回路,可以避免蜂鸣器两端的反向电动势产生尖峰脉冲电压进而损坏蜂鸣器。 |
R17 | 限流电阻,可防止流过蜂鸣器的电流过大而损坏蜂鸣器。 |
蜂鸣器 | 设置工作电流设置为 17mA 左右。 蜂鸣器的工作电流为 ≤100mAh,工作电压为 2V。 |
R7 | 限流电阻,可以防止流过基集的电流过大而损坏三极管。 |
R8 | 有两个重要作用。
|
我们选用指示灯指示药仓,达到提醒服用指定药物的功能。
我们有两种药盒尺寸,大尺寸药盒共有 2 个药仓,小尺寸药盒共有 3 个药仓,一共需要 5 个指示灯。
设计思路:
如果按照常规的设计,一个指示灯需要一个 I/O 口来控制。对于本次选用的模组,可以使用的 I/O 口数量一共需 12 个。由于还有其他功能需要使用 I/O 口,为了节约 I/O 口资源,我们需要用尽可能少的 I/O 口去控制更多的灯。因此,我们采用查理复用,用 n 个 I/O 口驱动 n*(n-1) 个 LED。
原理图:
说明:我们可以用 3 个 I/O 口实现对 5 个 LED 灯的控制。需要指示多个仓位时,由于人眼的暂留效应,当切换频率足够快时,就能达到同时显示的效果。
我们选用 OLED 显示屏,以提示不同药仓药物的服用数量等信息。
出于低功耗的考虑,我们还用 NMOS 管和电阻搭建了开关电路。当药盒待机时,“KEY_OLED” 引脚拉低,NMOS 管截止,“GND_OLED”与 GND 断开,OLED 屏断电。
原理图:
我们设计了形成行程开关和按键,服药行为的反馈记录。
行程开关
仓位按键
设计思路:
仅采用行程开关只能了解到是否有打开药盒的行为,不能确定是否有服用指定仓位的药。为此,我们增加了仓位按键,服用不同仓位的药品后,按下对应的仓位的按键,给到主控单元信号反馈。
由于本次方案是在现有药盒的基础上进行改造,不方便进行结构配合,所以需要手动操作按键。如若重新设计机构,可以使用轻触开关和药仓盖子联动设计。在开盖时,按键按下,可自动检测到服药行为。
原理图:
说明:当按下 S4 按键时,ADC_KEY 采集到的电压值为 3.3V。当按下 S5 按键时,ADC_KEY 采集到的电压值为 1.65V。当按下 S6 按键时,ADC_KEY 采集到的电压值为 0.825V。利用按下不同按键时,ADC 采集到的不同电压值,巧妙地进行判断,可以有效节约 I/O 口资源。
我们将药盒设计为可充电式药盒,实现便携目的。
设计思路:
充电管理芯片选择常用的 TP4056,该芯片是为单节锂电池进行恒压/恒流充电管理的芯片。TP4056 内部集成有电池温度监测电路,可以防止温度过高或过低对电池造成的损害。
原理图:
原理图说明:
实现方法为测量芯片 TEMP 引脚的电压值变化进行判断。
当 TEMP 脚的电压值小于输入电压的 45% 或大于输入电压的 80% 时,则意味着电池温度过低或过高,充电会被暂停。
TEMP 脚的电压是由电池内的 NTC 热敏电阻和芯片外围电路电阻(在我们设计电路中即 R18、R9)分压后的值,若不想启用电池温度检测功能,将 TEMP 脚接地即可。
R18 和 R19 的值是根据电池的温度监测范围(TL~TH)对应的热敏电阻的阻值来确定的,根据手册可知:
$$R18=\frac{R_{TL}R_{TH}(K_{2}-K_{1})}{(R_{TL}-R_{TH})K_{2}K_{1}}$$ $$R19=\frac{R_{TL}R_{TH}(K_{2}-K_{1})}{R_{TL}(K_{1}-K_{2}K_{1})-R_{TH}(K_{2}-K_{2}K_{1})}$$
计算说明:
部件 | 说明 |
---|---|
$$R_{TL}$$ | 电池温度 TL 时的阻值。 |
$$R_{TH}$$ | 电池温度 TH 时的阻值。 |
K1 | 电池温度 TH 时的阻值,为 0.45。 |
K2 | 为电池温度 TH 时的阻值,为 0.8。 |
$$R_{TL}$$ | 电池的使用温度范围为 0℃ ~ 60℃,查询电池的 NTC 电阻的温度—阻值对应表可知$为 29.28KΩ。 |
$$R_{TH}$$ | 电池的使用温度范围为 0℃ ~ 60℃,查询电池的 NTC 电阻的温度—阻值对应表可知$为 2.913KΩ。 |
R18 | 计算可得 R18=3.15KΩ(实际取 3.3K)。 |
R19 | R19=22.05KΩ(实际取 22K)。 |
$$R_{PROG}$$ | PROG 引脚外接的电阻。 |
恒流充电电流的计算方式: $$I_{BAT}=\frac{1200}{R_{PROG}}$$ 因此,本方案充电电流设计为 0.5A。
我们选取尺寸较小的电池以及静态功耗较低的芯片,以满足药盒的便携功能以及提高电池的续航能力。
电池
设计思路:
考虑到药盒的应用场景为长时间携带外出,故在设计的时候需要考虑电池是续航时长,产品设计需要尽可能降低功耗。而出于携带便捷性的考虑,药盒的体积不宜过大和和重量不宜过重,因此无法选用大容量的电池。
电池参数:
参数 | 说明 |
---|---|
尺寸 | 37 * 30 * 5 mm |
容量 | 600mAh |
工作电流 | ≤600mA |
重量 | 10g |
LDO
设计思路:
选型的时候,在满足性能需求的同时,尽可能的选取静态功耗低的芯片。本次选取的 LDO 型号为 SGM2040-3.3YN5G/TR,该芯片为固定输出 3.3V 电压的电压转化芯片,静态电流 $I_Q$ 仅为 1uA。
LDO 参数:
参数 | 说明 |
---|---|
封装 | SOT23-5 |
$$V_{drop}$$ | 85mV |
$R_{\Theta JA}$ | 207℃/W |
结温 | 150℃ |
额定输出电流 | 250mA |
后级主要负载的工作电流:
部件 | 负载电流 |
---|---|
蜂鸣器 | 消耗电流 17mA |
OLED 全屏显示 | 最大功耗 18mA |
模组 | 最大工作电流 13mA |
综上:该药盒理论最大消耗电流为 48mA,该款芯片的额定输出电流 250mA,远满足需求。
芯片功耗计算方式:
$P_D=(V_{in}-V_{out})*I{out}+V_{in}*I_Q=(4.2V-3.3V)48mA+3.3V1uA=43.2mW$
结温计算方式:
$T_J=T_A+R_{\Theta JA}P_D=45℃+207℃/W43.2mW=53.9424℃$
综上:计算结果为该结温小于芯片最高结温。选取这款芯片符合我们的应用需求。
说明:$T_A $ 表示封装所处的环境温度 ( ℃ ),本次用实际最高环境温度 45 ℃ 带入计算。
原理图:
功能 | 需求描述 |
---|---|
服药闹钟 | 支持周定时。 支持使用 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 模组规格书 ,查看模组详情。
单击 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 工程,再重新打开该工程即可编译通过。
在 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
日志输出使能。查看日志
打开 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 个闹钟上报、下发、删除、更新,都全部上报。按序列拼接即可,无顺序、无编辑等操作。
说明:关于 16 进制转码,可查看 16 进制转码操作。
字节说明 | 字节长度 | 说明 |
---|---|---|
#1 协议版本号 | 1 | 0x00:初始版本 |
#2 开关+通道号 | 1 |
|
#3 星期 | 1 |
|
#4 时间-小时 | 1 | 范围允许值为 0~23 |
#5 时间-分钟 | 1 | 范围允许值为 0~59 |
#6 执行动作 | 1 | 0x00 无此功能,不展示,作为未来预留。 |
#7 | 1 | 0x00:无此功能,不展示,作为未来预留。 |
#8 | 1 | 0x00:无此功能,不展示,作为未来预留。 |
#9 服药数量 | 18 |
|
#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;
}
功能 | 需求描述 |
---|---|
服药闹钟 | 支持周定时。 支持使用 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 模组规格书 ,查看模组详情。
单击 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 工程,再重新打开该工程即可编译通过。
在 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
日志输出使能。查看日志
打开 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。
![](https://images.tuyacn.com/smart/huanling_zone/nrf_adc1.jpg)
产品的 DP ID 14 为周定时功能,数据类型为 RAW,具体的协议格式如下:
下发方式: 全量、16 进制下发和上报,即任何 1 个闹钟上报、下发、删除、更新,都全部上报。按序列拼接即可,无顺序、无编辑等操作。
说明:关于 16 进制转码,可查看 16 进制转码操作。
字节说明 | 字节长度 | 说明 |
---|---|---|
#1 协议版本号 | 1 | 0x00:初始版本 |
#2 开关+通道号 | 1 |
|
#3 星期 | 1 |
|
#4 时间-小时 | 1 | 范围允许值为 0~23 |
#5 时间-分钟 | 1 | 范围允许值为 0~59 |
#6 执行动作 | 1 | 0x00 无此功能,不展示,作为未来预留。 |
#7 | 1 | 0x00:无此功能,不展示,作为未来预留。 |
#8 | 1 | 0x00:无此功能,不展示,作为未来预留。 |
#9 服药数量 | 18 |
|
#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 屏改成断码显示屏,充电锂电池换成纽扣电池方案等。