本帖最后由 电子禅石 于 2020-7-4 13:20 编辑
【RT-Thread作品秀】疫情智能小管家
作者: 樊晓杰
概述 简单介绍项目应用产生的背景 ,所产生的软硬件方案 及主要实现的功能。 应用产生背景名字的由来,2019.12.23 日以来的这 次新冠疫情,尤其是居家隔离不能外出,然后待的就无所事事,很害怕,造成心理还有生活及计划安排一塌糊涂,整个人都颓废了。和朋友聊天有的说,这两个月在家待的够够的了。这时候,就想弄个小的时间管理小管家小工具,类似上小学时候,有早操,下课,上 课及课间休息有铃声,下午校园广播 播相关的节目,有了这样的时间安排,隔离的日子,就不会乱,就会多姿多彩,就可以辅助更高效的管理生活,学习等,最终响应科技让生活更美好的理念。
所采用的软硬件方案1.硬件方案采用 :麻雀一号 麻雀一号开发板采用的主控芯片是 BK7252 , 是一款高性能 WiFi 模块,采用高集成的无线射频芯片,内部集成 2.4GHz Wi-Fi 1T1R 先进技术,支持摄像头图像输出,拥有最佳的功耗性能、射频性能、稳定性、通用性和可靠性,适用于各种应用和不同产品需求。 模块内部拥有 512KB 内嵌 RAM 和 4Mbyte Flash 空间,CPU 主频高达 180Mhz。并且集成了天线开关、功率放大器、低噪放大器、过滤器、电源管理模块, 支持 802.11e 以及 WMM-PS 协议, 支持 WPA、WPA2 和 WAPI 安全协议,同时集成了蓝牙 BLE 收发器,
支持 BLE4.2,支持主机或从机模式。 开发板特点: • 小巧精致: 麻雀一号开发板采用双层板设计,元器件布局精美合理,尺寸细小。
• 功能强大: CPU 主频高达 180Mhz,具有丰富的接口。
• 低功耗:在深度睡眠模式下,仅需 8 uA。
• 创造性强:内置 WiFi 、BLE 模块,可快速实现网路通信,配置摄像头、音频扬声器、MIC 录音、TF
卡座、五向按键、LCD 屏、Typec 供电调试接口,可以快速 DIY 各种音视频 Demo 。 我们的方案用到的资源有:LCD屏 , 五向按键,WIFI, 音频播放器,TF 卡。 实现功能系统运行状态指示,可以指示系统正常运行,异常情况; LCD 显示时间 日期 清晨祝福语等 信息 ; 模式一:语音 定时提醒 起床 睡觉 运动,休息等; 模式二:厨房做菜熬粥 定时闹钟 ; 模式三:故事机 磨耳朵 模式 ; wifi 配置信息 自动保存,上电自动重连路由器 。 支持TFTP 传输MP3 文件;
RT-Thread使用情况概述 应用中RT-Thread 使用情况: 内核部分: 1.事件 :按键发出相应事件 ,方便进行 线程同步。 2.消息队列:用于将 从服务器上获取的信息闹钟信息及留言信息,发到 LCD 显示线程。消息队列可以应用于发送不定长消息的场合,包括线程与线程间的消息交换,以及中断服务例程中给线程发送消息,用在这里比较合适。 3.线程:线程是实现任务的载体,它是 RT-Thread 中最基本的调度单位,它描述了一个任务执行的运行环境,也描述了这个任务所处的优先等级,重要的任务可设置相对较高的优先级,非重要的任务可以设置较低的优先级,不同的任务还可以设置相同的优先级,轮流运行。本次主要创建5个线程,分别是 LCD 显示线程,MP3 播放线程,按键处理线程,UCLOUD 线程,NTC 同步时间线程。
组件部分: DFS 虚拟文件系统: 主要作用是 存储 mp3 等音频资源 。 WLAN 设备及 SAL 网络框架 : 主要作用是通过wifi 连接网络 ,获取网络资源。 音乐播放器组件: 主要作用来播放MP3 等歌曲及提示音。 FinSH 控制台:前期开发调试使用 RTC组件 主要用于时间设置 及系统时间获取
软件包部分: 硬件框架 file:///D:/TimeManager_19nCov/BK7252Note/TimeManager_19nCov/doc/figure/hw.png?lastModify=1593829658 [td]主控芯片 | BK7252 | 高性能wifi 模块 512KB 内嵌 RAM 和 4Mbyte Flash 主频180M | LCD | ST7789 | 240 * 240 | 扬声器 | 5W | 播放音频及提示音 | SD卡 | 16G | 存放字体 及MP3 音频文件 | | | |
硬件方案 直接使用 麻雀1 号开发板,使用的外设说明如下:
软件框架说明【主要介绍应用所采用的软件方案框图、流程图等】 file:///D:/TimeManager_19nCov/BK7252Note/TimeManager_19nCov/doc/figure/%25E6%25B5%2581%25E7%25A8%258B%25E5%259B%25BE.png?lastModify=1593829658 最终的使用的线程情况如下: msh />ps
thread pri status stack_addr sp stack size max used left tick error
-------- --- ------- ---------- ---------- ---------- --------- ---------- ---
fivedir 10 suspend 0x00418380 0x00418ae8 0x00000800 54% 0x00000001 000
mp3 21 suspend 0x00425f6c 0x004265c8 0x00000800 49% 0x00000002 000
lcd 20 suspend 0x004256bc 0x00425cd8 0x00000800 66% 0x00000005 000
led 29 ready 0x0042550c 0x00425588 0x00000100 64% 0x00000008 -02
player_w 15 suspend 0x00424c5c 0x004253f0 0x00000800 05% 0x0000000a 000
player 2 suspend 0x00422364 0x00424aa8 0x00002800 07% 0x00000014 000
ns_worke 20 suspend 0x0041fab4 0x00422248 0x00002800 01% 0x0000000a 000
core_thr 2 suspend 0x0041f180 0x0041f8b0 0x00000800 21% 0x00000003 000
wpas_thr 5 suspend 0x0041e174 0x0041ee30 0x00000dac 42% 0x00000005 000
kmsgbk 3 suspend 0x0041cf6c 0x0041d6e0 0x00000800 13% 0x00000003 000
tshell 20 ready 0x0041ae48 0x0041ccd8 0x00002000 04% 0x00000003 000
pwr_btn 10 suspend 0x0041a598 0x0041ad18 0x00000800 10% 0x00000009 000
ntp_sync 26 suspend 0x00419ee8 0x0041a468 0x00000600 55% 0x00000001 000
tcpip 4 suspend 0x004195c8 0x00419ce8 0x00000800 50% 0x0000000a 000
tidle 31 ready 0x00405c78 0x00405e30 0x00000200 23% 0x00000013 000
timer 4 suspend 0x00405f34 0x00406ed0 0x00001000 03% 0x00000009 000
软件流程: 上电开机 系统初始化 显示 开机提示语; 连接指定wifi 然后通过ntp 同步时间 并显示在 LCD 上; 连接 ucloud 接收设置时间命令 检测按键 进行模式切换。 左边按键 进入 厨房 定时器 模式, UP 按键 一下 ,分钟增加1分钟, 中间按键 确认,开始倒计时 并显示在屏幕上, 退出此模式 按 DOWN 按键; 右边按键故事机 音乐播放模式,循环播放 sd 卡中 存的音频文件;退出此模式按 DOWN 按键; 默认 就是时钟显示模式。
软件模块说明(介绍应用软件关键部分的逻辑、采用的实现方式等) 1.显示模块处理 显示模块,是软件与用户交互的关键模块,主要使用led 显示系统状态和 lcd 显示时间 ,具体如下: (1)led 状态指示,使用一个led线程 来每隔1s 闪烁一次,表示系统正常运行。 其中使用的LED 编号为 12 。
void led_process_thread(void)
{
rt_thread_t tid = RT_NULL;
tid = rt_thread_create("led",
led_thread_entry,
RT_NULL,
1024,
3,
10);
if (tid != RT_NULL)
rt_thread_startup(tid);
}(2)lcd 显示时间 关键是如何把 时间转换成 字符串并显示出来。 time_t now;
int ntp_error_num = 0;
struct tm *p;
int min = 0,hour = 0,second = 0;
char mstr[3];
char hstr[3];
char sstr[3];
char time_str[12];
now = time(RT_NULL);
rt_kprintf("%s\n",ctime(&now));
p = gmtime((const time_t *)&now);
hour = p->tm_hour;
min = p->tm_min;
second = p->tm_sec;
sprintf(mstr, "%02d",min);
sprintf(hstr,"%02d",hour);
sprintf(sstr, "%02d",second);
sprintf(time_str,"%s : %s",hstr,mstr);
lcd_disp_str_at(20,80, time_str);
2.按键处理模块file:///D:/TimeManager_19nCov/BK7252Note/TimeManager_19nCov/doc/figure/five_dir.png?lastModify=1593829658 主要是几种模式的切换: 单独的线程,采用轮询的方式检测五向按键,需要延时,来释放cpu, 否则 msh 会无法输入。 (1)循环播放模式:右键进入 循环播放磨耳朵模式, 中间按键 进入暂停模式,再按右键 退出 播放循环播放模式 。 (2)厨房定时器模式: 按下左键 进入厨房定时器模式,默认5分钟,上下键按键 调整设定时间,中间按键确认,定时时间到后,播放指定音频文件。 关键代码:通过发送事件 rt_event_send(&event_play_mp3, PLAYER_EVENT); 来通知mp3播放线程进行播放。 static void thread_entry(void *paramer)
{
/* 按键检测线程中循环检测按键的状态并进行一定的消抖处理 */
while(1)
{
if(rt_pin_read(KEY_UP) == PIN_LOW)
{
rt_thread_delay(120);
if(rt_pin_read(KEY_UP) == PIN_LOW)
{
rt_kprintf(" key up is press ...\r\n");
if (flag_kitchen_timer == 1)
{
kitchen_timer_minute ++;
}
}
}
if(rt_pin_read(KEY_DOWN) == PIN_LOW)
{
rt_thread_delay(80);
if(rt_pin_read(KEY_DOWN) == PIN_LOW)
{
rt_kprintf(" key down is press ...\r\n");
if (flag_kitchen_timer == 1)
{
kitchen_timer_minute --;
}
}
}
if(rt_pin_read(KEY_LEFT) == PIN_LOW)
{
rt_thread_delay(100);
if(rt_pin_read(KEY_LEFT) == PIN_LOW)
{
rt_kprintf(" key left is press ...\r\n");
rt_kprintf("enter kitchen timer \n");
flag_kitchen_timer = 1;
}
}
if(rt_pin_read(KEY_RIGHT) == PIN_LOW)
{
rt_thread_delay(100);
if(rt_pin_read(KEY_RIGHT) == PIN_LOW)
{
rt_kprintf(" key right is press ...\r\n");
if (flag_cycle_ear_mode > 0)
{
rt_kprintf(" enter cycle ear mode %d\n",flag_cycle_ear_mode);
flag_cycle_ear_mode = 0;
play_mp3_stop();
}
else
{
flag_cycle_ear_mode = 1;
//play_mp3_cycle_ear(); /* code */
rt_event_send(&event_play_mp3, PLAYER_EVENT);
}
}
}
if(rt_pin_read(KEY_MID) == PIN_LOW)
{
rt_thread_delay(100);
if(rt_pin_read(KEY_MID) == PIN_LOW)
{
rt_kprintf(" key mid is press ...\r\n");
if (flag_kitchen_timer == 1)
{
rt_kprintf("the kitchen_timer_minute is %d\n", kitchen_timer_minute);
kitchen_timer_all_seconds = kitchen_timer_minute * 60;
//lcd_clear(WHITE);
// lcd_set_color(WHITE, BLACK);
lcd_disp_str_at(20,120, "KITCHEN");
while (flag_kitchen_timer == 1)
{
if (kitchen_timer_all_seconds == 0)
{
rt_kprintf(" time is up start play mp3 \n");
flag_kitchen_timer_mode = 1;
rt_event_send(&event_play_mp3, PLAYER_EVENT);
flag_kitchen_timer = 0;
kitchen_timer_minute = 0;
break;
}
lcd_display_kitchen_timer(kitchen_timer_all_seconds);
rt_thread_mdelay(1000);
kitchen_timer_all_seconds--;
}
}
if(flag_cycle_ear_mode == 1)
{
//暂停播放
pause_mp3();
flag_cycle_ear_mode ++;
}
else if (flag_cycle_ear_mode == 2)
{
flag_cycle_ear_mode =1;
resume_play_mp3();
}
}
}
rt_thread_delay(80);
}
}
3. 网络交互处理模块主要是处理 与ucloud 平台的数据交互处理。具体如下: (1) 连接指点wifi : if (network_mode == WIFI_STATION)
{
/* get wlan device */
wlan = (struct rt_wlan_device *)rt_device_find(WIFI_DEVICE_STA_NAME);
if (!wlan)
{
rt_kprintf("no wlan:%s device\n", WIFI_DEVICE_STA_NAME);
return -1;
}
/* wifi station */
rt_wlan_info_init(&info, WIFI_STATION, SECURITY_WPA2_MIXED_PSK, wifi_ssid);
result = rt_wlan_init(wlan, WIFI_STATION);
if (result == RT_EOK)
{
result = rt_wlan_connect(wlan, &info, wifi_key);
}
}通过下面函数来等待wifi 连接成功。 do
{
tick ++;
rt_thread_delay(rt_tick_from_millisecond(1000));
if (tick >= 30)
{
rt_kprintf("GET IP Time Out!!! \n");
return -1;
}
}while (!get_wifi_status(g_wlan_device->parent.netif));
(2)rtc 开始 ntp 同步自动时间,包括定期触发 同步时间 时间同步后,发送相关信号量给 时间处理线程,用于设置时间。其实不发送也可以,时间模块 只根据系统时间处理,不管是否已经同步时间。 static void ntp_sync_thread_enrty(void *param)
{
extern time_t ntp_sync_to_rtc(const char *host_name);
/* first sync delay for network connect */
rt_thread_delay(RTC_NTP_FIRST_SYNC_DELAY * RT_TICK_PER_SECOND);
while (1)
{
ntp_sync_to_rtc(NULL); //40天 定期的同步时间
rt_thread_delay(RTC_NTP_SYNC_PERIOD * RT_TICK_PER_SECOND);
}
}
(3) 通过mqtt 连接ucloud 后台,接收设置闹钟 时间 歌曲 及相关留言 。 接收到信息后,通过设置相关 留言信息及 闹钟值; 关键代码1:针对接收的信息的解析处理如下: /**
* MQTT消息接收处理函数
*
* @param topicName topic主题
* @param topicNameLen topic长度
* @param message 已订阅消息的结构
* @param userData 消息负载
*/
static void on_message_callback(void *pClient, MQTTMessage *message, void *userData) {
if (message == NULL) {
return;
}
HAL_Printf("Receive Message With topicName:%.*s, payload:%.*s\n",
(int) message->topic_len, message->topic, (int) message->payload_len, (char *) message->payload);
/* 发送这个消息指针给 mq 消息队列 */
rt_mq_send(&mq, (void*)&msg_ptr, sizeof(struct msg));
}
关键代码2: 通过消息队列的方式,将 mqtt 接收到的数据传输到 lcd 线程,并用 json 解析显示: 关键是 消息队列是直接的数据内容复制,所以在上面的例子中,都采用了局部变量的方式保存消息结构体,这样也就免去动态内存分配的烦恼了(也就不用担心,接收线程在接收到消息时,消息内存空间已经被释放)。 while(1)
{
/* 从消息队列中接收消息到 msg_ptr 中 */
if (rt_mq_recv(&mq, (void*)&msg_ptr, sizeof(struct msg), RT_WAITING_NO) == RT_EOK)//RT_WAITING_FOREVER
{
rt_kprintf("threadlcd: recv msg from msg queue, the content:%s\n", msg_ptr.data_ptr);
//{"Hour":"5","Minute":"30","note":"just for fun"}
object_json = json_get_object(JSOBJECT, (char *)msg_ptr.data_ptr);
h = json_get_value_by_name(object_json, strlen(object_json),"Hour", &p_iValueLen, &p_iValueType);
memcpy(hour_str,h,p_iValueLen);
alarm_hour = atoi(hour_str);
rt_kprintf("\n the ivalueLen is %d : the ivalueType is %d the hour is %d \n",p_iValueLen, p_iValueType,alarm_hour);
m = json_get_value_by_name(object_json, strlen(object_json),"Minute",&p_iValueLen, &p_iValueType);
memcpy(minute,m, p_iValueLen);
alarm_min = atoi(minute);
rt_kprintf("\n the ivalueLen is %d : the ivalueType is %d the minute is %d \n",p_iValueLen, p_iValueType,alarm_min);
s = json_get_value_by_name(object_json, strlen(object_json), "note", &p_iValueLen, &p_iValueType);
memcpy(note,s,p_iValueLen);
rt_kprintf("\n the note is %s \n", note);
lcd_disp_str_at(20,20, note);
4. 时间处理模块主要处理时间相关内容,具体逻辑如下: (1) RTC 设备驱动使用 并开启NTP 时间自动同步 int rt_rtc_ntp_sync_init(void)
{
static rt_bool_t init_ok = RT_FALSE;
rt_thread_t thread;
if (init_ok)
{
return 0;
}
thread = rt_thread_create("ntp_sync", ntp_sync_thread_enrty, RT_NULL, 1536, 26, 2);
if (thread)
{
rt_thread_startup(thread);
}
else
{
return -RT_ENOMEM;
}
init_ok = RT_TRUE;
return RT_EOK;
}
INIT_COMPONENT_EXPORT(rt_rtc_ntp_sync_init);(2)倒计时模式 处理 直接在五向按键线程中处理,显示也是在五向按键线程中,其中关键代码如下: <span class="md-plain" style="box-sizing: border-box;" md-inline="plain">static void thread_entry(void *paramer)
{
/* 按键检测线程中循环检测按键的状态并进行一定的消抖处理 */
while(1)
{
if(rt_pin_read(KEY_UP) == PIN_LOW)
{
rt_thread_delay(120);
if(rt_pin_read(KEY_UP) == PIN_LOW)
{
rt_kprintf(" key up is press ...\r\n");
if (flag_kitchen_timer == 1)
{
kitchen_timer_minute ++;
}
}
}
if(rt_pin_read(KEY_DOWN) == PIN_LOW)
{
rt_thread_delay(80);
if(rt_pin_read(KEY_DOWN) == PIN_LOW)
{
rt_kprintf(" key down is press ...\r\n");
if (flag_kitchen_timer == 1)
{
kitchen_timer_minute --;
}
}
}
if(rt_pin_read(KEY_LEFT) == PIN_LOW)
{
rt_thread_delay(100);
if(rt_pin_read(KEY_LEFT) == PIN_LOW)
{
rt_kprintf(" key left is press ...\r\n");
rt_kprintf("enter kitchen timer \n");
flag_kitchen_timer = 1;
}
}
if(rt_pin_read(KEY_RIGHT) == PIN_LOW)
{
rt_thread_delay(100);
if(rt_pin_read(KEY_RIGHT) == PIN_LOW)
{
rt_kprintf(" key right is press ...\r\n");
if (flag_cycle_ear_mode > 0)
{
rt_kprintf(" enter cycle ear mode %d\n",flag_cycle_ear_mode);
flag_cycle_ear_mode = 0;
play_mp3_stop();
}
else
{
flag_cycle_ear_mode = 1;
//play_mp3_cycle_ear(); /* code */
rt_event_send(&event_play_mp3, PLAYER_EVENT);
}
}
}
if(rt_pin_read(KEY_MID) == PIN_LOW)
{
rt_thread_delay(100);
if(rt_pin_read(KEY_MID) == PIN_LOW)
{
rt_kprintf(" key mid is press ...\r\n");
if (flag_kitchen_timer == 1)
{
rt_kprintf("the kitchen_timer_minute is %d\n", kitchen_timer_minute);
kitchen_timer_all_seconds = kitchen_timer_minute * 60;
//lcd_clear(WHITE);
// lcd_set_color(WHITE, BLACK);
lcd_disp_str_at(20,120, "KITCHEN");
while (flag_kitchen_timer == 1)
{
if (kitchen_timer_all_seconds == 0)
{
rt_kprintf(" time is up start play mp3 \n");
flag_kitchen_timer_mode = 1;
rt_event_send(&event_play_mp3, PLAYER_EVENT);
flag_kitchen_timer = 0;
kitchen_timer_minute = 0;
break;
}
lcd_display_kitchen_timer(kitchen_timer_all_seconds);
rt_thread_mdelay(1000);
kitchen_timer_all_seconds--;
}
}
if(flag_cycle_ear_mode == 1)
{
//暂停播放
pause_mp3();
flag_cycle_ear_mode ++;
}
else if (flag_cycle_ear_mode == 2)
{
flag_cycle_ear_mode =1;
resume_play_mp3();
}
}
}
rt_thread_delay(80);
}
}</span><span class="md-plain" style="box-sizing: border-box;" md-inline="plain"> </span>
5. 音频播放模块主要是按定时事件处理音频数据。主要是根据 定时事件 或者 按键播放事件 来切换循环播放模式 或者 定时播放模式,主要内容如下: (1)循环播放模式 找到 下一首歌曲名字关键代码: int find_next_song_path( char *uri)
{
DIR *dirp;
struct dirent *d;
/* 打开 / dir_test 目录 */
dirp = opendir("/sd/Music");
if (dirp == RT_NULL)
{
rt_kprintf("open directory error!\n");
}
else
{
/* 读取目录 */
while ((d = readdir(dirp)) != RT_NULL)
{
rt_kprintf("found %s\n", d->d_name);
rt_sprintf(uri,"/sd/mp3/%s",d->d_name);
rt_kprintf("%s\n",uri);
rt_kprintf("//////////////////////////// player_play \n");
player_stop();
player_set_uri(uri);
player_play();
dump_status();
value = player_get_duration();//s
rt_thread_mdelay(value*1000);//这个延时很重要 必须需要
rt_kprintf("//////////////////////////// player_play end \n");
}
/* 关闭目录 */
closedir(dirp);
}
}(2)MP3 播放接收事件 处理 void mp3_player_process(void)
{
rt_uint32_t e;
char uri[256] = {0};
while(1)
{
if(rt_event_recv(&event_play_mp3, (PLAYER_EVENT),
RT_EVENT_FLAG_OR | RT_EVENT_FLAG_CLEAR,
RT_WAITING_FOREVER, &e) == RT_EOK)
{
rt_kprintf("mp3_player thread recv event 0x%x\n",e);
while(flag_cycle_ear_mode == 1) //循环模式
{
find_next_song_path(uri);
}
}
}
}
有个问题:就是 停止以后 如何第二次再次进入循环播放模式。演示效果 功能1:开机 连接指定wifi ,并同步 网络 时间,显示开机提示语 Have a Nice Day !
file:///D:/TimeManager_19nCov/BK7252Note/TimeManager_19nCov/doc/figure/niceday.jpg?lastModify=1593829658
功能2: 连接上UCLOUD,发送命令:
file:///D:/TimeManager_19nCov/BK7252Note/TimeManager_19nCov/doc/figure/cmd_mqtt.png?lastModify=1593829658 连接上
file:///D:/TimeManager_19nCov/BK7252Note/TimeManager_19nCov/doc/figure/mqtt.png?lastModify=1593829658 功能3:厨房定时器模式
file:///D:/TimeManager_19nCov/BK7252Note/TimeManager_19nCov/doc/figure/kitchen.jpg?lastModify=1593829658 视频播放地址: 疫情小管家 代码地址 地址
视频播放地址: https://www.bilibili.com/video/BV1gV41167Pk/
小管家
code
代码地址:
https://gitee.com/Gerryfan/TimeManager_19nCov
|