描述
基于正点原子STM32F103战舰开发板,实现功能:
- 光敏传感器采集环境光线强度,单片机通过PWM输出控制LED亮度
- DHT11传感器采集环境温湿度,显示在LCD屏幕上和通过蓝牙模块发送
- 蓝牙模块无线控制LED开关
硬件配置
DHT11
四脚的直接插入预留的DHT11接口,三脚的使用转接线连接到接口,没有连接到预留DHT11接口,程序是无法正常运行的
HC05
用杜邦线将HC05模块连接到USB转TTL
HC05 |
STATE | RXD | TXD | GND | VCC | EN |
---|---|---|---|---|---|---|
USB转TTL | TXD | RXD | GND | 5V | 3.3V |
连接到安装了CH340串口驱动的个人计算机。如果HC05的红色指示灯长亮,闪烁间隔在1s左右,则说明模块已进入AT模式。使用串口调试软件,设定发送波特率为38400,发送AT指令,其中最关键的是设置波特率为9600,详细内容附在链接当中
蓝牙模块连接,到开发板
HC05 |
STATE | RXD | TXD | GND | VCC | EN |
---|---|---|---|---|---|---|
开发板 | PA4 | PA3 | PA2 | GND | 5V | PC4 |
软件设计
HC05
hc05.c
#include "hc05.h"
#include "usart2.h"
#include "delay.h"
#include "lcd.h"
//初始化HC05模块
//返回值:0,成功;1,失败
u8 HC05_Init(void) {
u8 t;
u8 retry = 10;
u8 temp = 1;
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_EN|RCC_STATE,ENABLE); //使能PORTA C时钟
GPIO_InitStructure.GPIO_Pin = STATE_Pin; // 端口配置
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //上拉输入
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //IO口速度为50MHz
GPIO_Init(GPIOA, &GPIO_InitStructure); //根据设定参数初始化PA4
GPIO_InitStructure.GPIO_Pin = EN_Pin; // 端口配置
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //IO口速度为50MHz
GPIO_Init(GPIOC, &GPIO_InitStructure); //根据设定参数初始化PC4
GPIO_SetBits(GPIOC,EN_Pin);
USART2_Init(9600); //初始化串口2的波特率为:9600
delay_ms(1500);
while ( retry-- ) {
HC05_EN = 1; //PC4置高,进入AT模式
delay_ms(10);
u2_printf("AT\r\n"); //发送AT测试指令
HC05_EN = 0; //PC4拉低,退出AT模式
for ( t = 0; t < 10; t++ ) { //最长等待50ms,来接收HC05模块的回应
if ( USART2_RX_STA & 0X8000 ) {
break;
}
delay_ms(5);
}
if ( USART2_RX_STA & 0X8000 ) { //接收到一次数据了
temp = USART2_RX_STA & 0X7FFF; //得到数据长度
USART2_RX_STA = 0;
if ( temp == 4 && USART2_RX_BUF[0] == 'O' && USART2_RX_BUF[1] == 'K' ) { //接收到OK响应
temp=0;
break;
}
}
}
if ( retry == 0 ) {
temp=1; //检测失败
}
return temp;
}
//获取ATK-HC05模块的角色
//返回值:0,从机;1,主机;0XFF,获取失败
u8 HC05_Get_Role(void)
{
u8 retry = 0X0F;
u8 temp;
u8 t;
while ( retry-- ) {
HC05_EN = 1; //KEY置高,进入AT模式
delay_ms(10);
u2_printf("AT+ROLE?\r\n"); //查询角色
for( t = 0; t < 20; t++ ) { //最长等待200ms,来接收HC05模块的回应
delay_ms(10);
if ( USART2_RX_STA & 0X8000 ) {
break;
}
}
HC05_EN = 0; //KEY拉低,退出AT模式
if ( USART2_RX_STA & 0X8000 ) { //接收到一次数据了
temp = USART2_RX_STA & 0X7FFF; //得到数据长度
USART2_RX_STA = 0;
if ( temp == 13 && USART2_RX_BUF[0] == '+' ) { //接收到正确的应答了
temp = USART2_RX_BUF[6] - '0'; //得到主从模式值
break;
}
}
}
if ( retry == 0 ) {
temp = 0XFF;//查询失败
}
return temp;
}
//ATK-HC05设置命令
//此函数用于设置ATK-HC05,适用于仅返回OK应答的AT指令
//atstr:AT指令串.比如:"AT+RESET"/"AT+UART=9600,0,0"/"AT+ROLE=0"等字符串
//返回值:0,设置成功;其他,设置失败.
u8 HC05_Set_Cmd(u8* atstr)
{
u8 retry = 0X0F;
u8 temp;
u8 t;
while ( retry-- ) {
HC05_EN = 1; //PC4置高,进入AT模式
delay_ms(10);
u2_printf("%s\r\n",atstr); //发送AT字符串
HC05_EN = 0; //PC4拉低,退出AT模式
for ( t = 0; t < 20; t++ ) { //最长等待100ms,来接收HC05模块的回应
if ( USART2_RX_STA & 0X8000 ) {
break;
}
delay_ms(5);
}
if ( USART2_RX_STA & 0X8000 ) { //接收到一次数据了
temp = USART2_RX_STA & 0X7FFF; //得到数据长度
USART2_RX_STA = 0;
if ( temp == 4 && USART2_RX_BUF[0] == 'O' ) { //接收到OK应答了
temp = 0;
break;
}
}
}
if ( retry == 0 ) {
temp = 0XFF;//设置失败
}
return temp;
}
//显示ATK-HC05模块的主从状态
void HC05_Role_Show(void)
{
if ( HC05_Get_Role() == 1 ) {
LCD_ShowString(30,150,200,16,16,(u8*)"ROLE:Master"); //主机
} else {
LCD_ShowString(30,150,200,16,16,(u8*)"ROLE:Slave "); //从机
}
}
//显示ATK-HC05模块的连接状态
void HC05_Sta_Show(void)
{
if ( HC05_STATE ){ //检测PA4引脚
LCD_ShowString(120,150,120,16,16,(u8*)"STA:Connected "); //连接成功
}else{
LCD_ShowString(120,150,120,16,16,(u8*)"STA:Disconnect"); //未连接
}
}
hc05.h
#ifndef __HC05_H
#define __HC05_H
#include "sys.h"
//连接模块GPIO相关参数的一层封装
//**********************************************************************************
#define RCC_STATE RCC_APB2Periph_GPIOA
#define RCC_EN RCC_APB2Periph_GPIOC
#define STATE_Pin GPIO_Pin_4
#define EN_Pin GPIO_Pin_4
//**********************************************************************************
#define HC05_EN PCout(4) //蓝牙控制EN信号
#define HC05_STATE PAin(4) //蓝牙连接状态信号
u8 HC05_Init(void);
//void HC05_CFG_CMD(u8 *str);
u8 HC05_Get_Role(void);
u8 HC05_Set_Cmd(u8* atstr);
void HC05_Role_Show(void);
void HC05_Sta_Show(void);
#endif
USART2
usart2.c
#include "usart2.h"
//串口发送缓存区
__align(8) u8 USART2_TX_BUF[USART2_MAX_SEND_LEN]; //发送缓冲,最大USART2_MAX_SEND_LEN字节
#ifdef USART2_RX_EN //如果使能了接收
//串口接收缓存区
u8 USART2_RX_BUF[USART2_MAX_RECV_LEN]; //接收缓冲,最大USART2_MAX_RECV_LEN个字节.
//通过判断接收连续2个字符之间的时间差不大于10ms来决定是不是一次连续的数据.
//如果2个字符接收间隔超过10ms,则认为不是1次连续数据.也就是超过10ms没有接收到
//任何数据,则表示此次接收完毕.
//接收到的数据状态
//[15]:0,没有接收到数据;1,接收到了一批数据.
//[14:0]:接收到的数据长度
u16 USART2_RX_STA = 0;
void USART2_IRQHandler(void) //接收函数
{
u8 res;
if ( USART_GetITStatus( USART2, USART_IT_RXNE ) != RESET ) { //接收到数据 引起中断
res = USART_ReceiveData(USART2); //从串口2读取数据
if ( ( USART2_RX_STA & (1<<15) ) == 0 ) {
if ( USART2_RX_STA < USART2_MAX_RECV_LEN ) { //还可以接收数据
TIM_SetCounter(TIM4,0); //计数器清空
if ( USART2_RX_STA == 0 ) {
TIM4_Set(1); //使能定时器4的中断
}
USART2_RX_BUF[USART2_RX_STA++] = res; //记录接收到的值
} else {
USART2_RX_STA |= 1<<15; //强制标记接收完成
}
}
}
}
//初始化USART2
//bound:波特率
void USART2_Init(u32 bound)
{
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB1PeriphClockCmd(RCC_USART,ENABLE);
RCC_APB2PeriphClockCmd(RCC_TX,ENABLE);
RCC_APB2PeriphClockCmd(RCC_RX,ENABLE);
USART_DeInit(USART); //复位串口
//UART2_TX PA.2
GPIO_InitStructure.GPIO_Pin=USART_TX_Pin;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF_PP;
GPIO_Init(GPIOA,&GPIO_InitStructure);
//USART2_RX PA.3
GPIO_InitStructure.GPIO_Pin=USART_RX_Pin;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IN_FLOATING; //浮空输入
GPIO_Init(GPIOA,&GPIO_InitStructure);
USART_InitStructure.USART_BaudRate = bound;//一般设置为9600;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;//字长为8位数据格式
USART_InitStructure.USART_StopBits = USART_StopBits_1;//一个停止位
USART_InitStructure.USART_Parity = USART_Parity_No;//无奇偶校验位
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//无硬件数据流控制
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; //收发模式
USART_Init(USART,&USART_InitStructure); //初始化串口
USART_DMACmd(USART2,USART_DMAReq_Tx,ENABLE); //使能串口2的DMA发送
UART_DMA_Config(DMA1_Channel7,(u32)&USART2->DR,(u32)USART2_TX_BUF);//DMA1通道7,外设为串口2,存储器为USART2_TX_BUF
USART_Cmd(USART2, ENABLE); //使能串口
#ifdef USART2_RX_EN //如果使能了接收
//使能接收中断
USART_ITConfig(USART2, USART_IT_RXNE, ENABLE);//开启中断
NVIC_InitStructure.NVIC_IRQChannel = USART2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=2 ;//抢占优先级2
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3; //子优先级3
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道使能
NVIC_Init(&NVIC_InitStructure); //根据指定的参数初始化VIC寄存器
TIM4_Init(99,7199); //10ms中断
USART2_RX_STA = 0; //清零
TIM4_Set(0); //关闭定时器4
#endif
}
//arr:自动重装值。
//psc:时钟预分频数
void TIM4_Init(u16 arr,u16 psc)
{
NVIC_InitTypeDef NVIC_InitStructure;
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE); //时钟使能//TIM4时钟使能
//定时器TIM4初始化
TIM_TimeBaseStructure.TIM_Period = arr; //设置在下一个更新事件装入活动的自动重装载寄存器周期的值
TIM_TimeBaseStructure.TIM_Prescaler =psc; //设置用来作为TIMx时钟频率除数的预分频值
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; //设置时钟分割:TDTS = Tck_tim
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //TIM向上计数模式
TIM_TimeBaseInit(TIM4, &TIM_TimeBaseStructure); //根据指定的参数初始化TIMx的时间基数单位
TIM_ITConfig(TIM4,TIM_IT_Update,ENABLE ); //使能指定的TIM4中断,允许更新中断
NVIC_InitStructure.NVIC_IRQChannel = TIM4_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=1 ;//抢占优先级1
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2; //子优先级2
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道使能
NVIC_Init(&NVIC_InitStructure); //根据指定的参数初始化VIC寄存器
}
//定时器4中断服务程序
void TIM4_IRQHandler(void)
{
if ( TIM_GetITStatus(TIM4, TIM_IT_Update) != RESET ) {
USART2_RX_STA |= 1<<15; //标记接收完成
TIM_ClearITPendingBit(TIM4, TIM_IT_Update); //清除TIMx更新中断标志
TIM4_Set(0); //关闭TIM4
}
}
//设置TIM4的开关
//sta:0,关闭;1,开启;
void TIM4_Set(u8 sta)
{
if ( sta ) {
TIM_SetCounter(TIM4,0);//计数器清空
TIM_Cmd(TIM4, ENABLE); //使能TIMx
} else {
TIM_Cmd(TIM4, DISABLE);//关闭定时器4
}
}
//串口2,printf 函数
//确保一次发送数据不超过USART2_MAX_SEND_LEN字节
void u2_printf(char* fmt,...)
{
va_list ap;
va_start(ap,fmt);
vsprintf((char*)USART2_TX_BUF,fmt,ap);
va_end(ap);
while ( DMA_GetCurrDataCounter(DMA1_Channel7) != 0 ) {
;//等待通道7传输完成
}
UART_DMA_Enable(DMA1_Channel7,strlen((const char*)USART2_TX_BUF)); //通过dma发送出去
}
#endif
//DMA1的各通道配置
//这里的传输形式是固定的,这点要根据不同的情况来修改
//从存储器->外设模式/8位数据宽度/存储器增量模式
//DMA_CHx:DMA通道CHx
//cpar:外设地址
//cmar:存储器地址
void UART_DMA_Config(DMA_Channel_TypeDef*DMA_CHx,u32 cpar,u32 cmar)
{
DMA_InitTypeDef DMA_InitStructure;
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); //使能DMA传输
DMA_DeInit(DMA_CHx); //将DMA的通道1寄存器重设为缺省值
DMA_InitStructure.DMA_PeripheralBaseAddr = cpar; //DMA外设ADC基地址
DMA_InitStructure.DMA_MemoryBaseAddr = cmar; //DMA内存基地址
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST; //数据传输方向,从内存读取发送到外设
DMA_InitStructure.DMA_BufferSize = 0; //DMA通道的DMA缓存的大小
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //外设地址寄存器不变
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //内存地址寄存器递增
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; //数据宽度为8位
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; //数据宽度为8位
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; //工作在正常缓存模式
DMA_InitStructure.DMA_Priority = DMA_Priority_Medium; //DMA通道 x拥有中优先级
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; //DMA通道x没有设置为内存到内存传输
DMA_Init(DMA_CHx, &DMA_InitStructure); //根据DMA_InitStruct中指定的参数初始化DMA的通道USART1_Tx_DMA_Channel所标识的寄存器
}
//开启一次DMA传输
void UART_DMA_Enable(DMA_Channel_TypeDef*DMA_CHx,u8 len)
{
DMA_Cmd(DMA_CHx, DISABLE ); //关闭 指示的通道
DMA_SetCurrDataCounter(DMA_CHx,len);//DMA通道的DMA缓存的大小
DMA_Cmd(DMA_CHx, ENABLE); //开启DMA传输
}
usart2.h
#ifndef __USART2_H
#define __USART2_H
#include "sys.h"
#include <stdarg.h>
#include <stdio.h>
#include <string.h>
//对USART2相关参数的一层封装
//**********************************************************************************
#define RCC_USART RCC_APB1Periph_USART2
#define RCC_TX RCC_APB2Periph_GPIOA
#define RCC_RX RCC_APB2Periph_GPIOA
#define USART USART2
#define USART_TX_Pin GPIO_Pin_2; //USART2_TX PA.2
#define USART_RX_Pin GPIO_Pin_3; //USART2_RX PA.3
//**********************************************************************************
#define USART2_MAX_RECV_LEN 200 //最大接收缓存字节数
#define USART2_MAX_SEND_LEN 200 //最大发送缓存字节数
#define USART2_RX_EN 1 //0,不接收;1,接收.
extern u8 USART2_RX_BUF[USART2_MAX_RECV_LEN]; //接收缓冲,最大USART2_MAX_RECV_LEN字节
extern u8 USART2_TX_BUF[USART2_MAX_SEND_LEN]; //发送缓冲,最大USART2_MAX_SEND_LEN字节
extern u16 USART2_RX_STA; //接收数据状态
void USART2_Init(u32 bound);
void TIM4_Set(u8 sta);
void TIM4_Init(u16 arr,u16 psc);
void UART_DMA_Config(DMA_Channel_TypeDef*DMA_CHx,u32 cpar,u32 cmar);
void UART_DMA_Enable(DMA_Channel_TypeDef*DMA_CHx,u8 len);
void u2_printf(char* fmt, ...);
#endif
main.c
主函数里面用到的一些例如delay
延时等,都是调用正点原子的编写的库函数,自行添加
#include "led.h"
#include "delay.h"
#include "key.h"
#include "sys.h"
#include "lcd.h"
#include "usart.h"
#include "adc.h"
#include "lsens.h"
#include "timer.h"
#include "dht11.h"
#include "string.h"
#include "hc05.h"
#include "usart2.h"
int main(void) {
u8 t; //计数器
u8 temperature; //温度
u8 humidity; //湿度
u8 adcx; //光敏数值
u8 key; //按键信息
u8 sendmask = 0; //自动/开启发送命令
u8 sendbuf[20]; //发送缓冲区
u8 reclen = 0;
delay_init(); //延时函数初始化
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //设置NVIC中断分组2:2位抢占优先级,2位响应优先级
uart_init(9600); //串口初始化为9600
LED_Init(); //初始化与LED连接的硬件接口
KEY_Init(); //初始化按键
LCD_Init(); //初始化LCD
Lsens_Init(); //初始化光敏传感器
POINT_COLOR = RED; //设置LCD屏幕画笔为红色
LCD_ShowString(30,30,200,16,16,(u8*)"SMART WINE CABINET"); //显示设计项目名字
//等待DHT11的回应
//返回1:未检测到DHT11的存在
//返回0:存在
while ( DHT11_Init() ) {
LCD_ShowString(30,70,200,16,16,(u8*)"DHT11 Error"); //显示错误信息
delay_ms(200);
LCD_Fill(30,70,239,130+16,WHITE); //清除错误信息 再次检测DHT11是否存在
delay_ms(200);
}
LCD_ShowString(30,70,200,16,16,(u8*)"Ligh:"); //光强数值提示信息
LCD_ShowString(30,90,200,16,16,(u8*)"Temp: C"); //显示温度格式
LCD_ShowString(30,110,200,16,16,(u8*)"Humi: %"); //显示湿度格式
TIM3_PWM_Init(899, 0); //初始化TIM3定时器 红灯亮起
//不分频 PWM频率=72000000/900=80Khz
//初始化HC05模块
//返回0 成功
//返回1 失败
while ( HC05_Init() ) {
LCD_ShowString(30,150,200,16,16,(u8*)"HC05 Error!"); //显示错误信息
delay_ms(500);
LCD_Fill(30,150,240,230,WHITE);
delay_ms(100);
}
POINT_COLOR = BLUE; //设置LCD画笔为蓝色
HC05_Role_Show(); //显示蓝牙工作模式
HC05_Sta_Show(); //显示蓝牙连接状态
LCD_ShowString(30,170,200,16,16,(u8*)"Send:");
LCD_ShowString(30,190,200,16,16,(u8*)"Receive:");
delay_ms(100);
USART2_RX_STA = 0; //蓝牙模块初始化的时候 和串口是有通信的 预先将接收标志清零 避免误处理AT设置信息
while ( 1 ) {
adcx = Lsens_Get_Val(); //获取环境光线数值
LCD_ShowxNum(30+40,70,adcx,3,16,0); //显示环境光线数值
if ( t % 10 == 0 ){
DHT11_Read_Data(&temperature,&humidity); //读取温湿度值
LCD_ShowNum(30+40,90,temperature,2,16); //显示温度
LCD_ShowNum(30+40,110,humidity,2,16); //显示湿度
}
TIM_SetCompare2(TIM3,300-3*adcx); //PWM输出控制LED0亮度
key = KEY_Scan(0); //按键处理函数 返回按键值
switch ( key ) {
case WKUP_PRES: //按下KEY_UP则切换蓝牙工作模式
key = HC05_Get_Role(); //获取当前蓝牙模块工作模式
if ( key != 0xFF ) { //如果key为0xff则说明工作模式获取失败
if ( key ) {
HC05_Set_Cmd((u8*)"AT+ROLE=0");//发送AT指令给蓝牙模块 设置为从模式
} else {
HC05_Set_Cmd((u8*)"AT+ROLE=1");//设置为主模式
}
}
HC05_Role_Show(); //显示蓝牙模块当前的工作模式
HC05_Set_Cmd((u8*)"AT+RESET"); //复位HC05模块
delay_ms(200);
break;
case KEY0_PRES:
sendmask = !sendmask; //开启或关闭自动发送
if ( sendmask == 0 ) {
LCD_Fill(30+40,170,30+40+210,170+20,WHITE);//清除显示
}
break;
default:
break;
}
if ( t == 10 ) { //定时发送
if ( sendmask ){ //自动发送开启的时候
sprintf((char*)sendbuf,"G%dS%dW%d\r\n", adcx, humidity, temperature); //将格式字符串写入发送缓冲区
LCD_ShowString(30+40,170,200,16,16,sendbuf); //显示发送数据
u2_printf("G%uS%uW%u", adcx, humidity, temperature); //向串口2发送数据
}
HC05_Sta_Show(); //显示HC05模块的连接状态
t = 0;
}
if ( USART2_RX_STA & 0X8000 ) { //接收到一次数据了
LCD_Fill(30,210,320,320,WHITE); //清除显示
reclen = USART2_RX_STA & 0X7FFF; //得到数据长度
USART2_RX_BUF[reclen] = 0; //加入结束符
if ( reclen == 9 || reclen == 8 ) {
if ( strcmp((const char*)USART2_RX_BUF,"+LED1 ON") == 0 ) { //检测到收到了开启LED1的信息
LED1 = 0; //打开LED1
}
if ( strcmp((const char*)USART2_RX_BUF,"+LED1 OFF") == 0 ) { //检测到收到了关闭LED1的信息
LED1 = 1; //关闭LED1
}
}
LCD_ShowString(30,210,200,16,16,USART2_RX_BUF); //显示接收到的数据
USART2_RX_STA = 0; //接收标志清零
memset(USART2_RX_BUF, 0, USART2_MAX_RECV_LEN); //接收标志清零不够 用memset把整个缓冲区置零
}
t++;
}
}
链接
https://gitee.com/wwiandmc/space/tree/master/smart wine cabinet