【富芮坤物联网开发板评测】基于FR8016H+ESP8266的新冠肺炎疫情监控平台
0.前言
前几天社区管理员在评测群里说,周末截止提交作品。所以我不得不赶紧趁着周末两天的时间,做出一个小设计出来。板子到手也有近一个月的时间了,期间断断续续也试图实现一些功能,但是示例代码实在是看不明白,所以也就没有怎么上心。
果然,人都是有潜力的,压力就是动力,周末这两天,仔细阅读了SDK使用手册和示例代码,也看了不少社区网友分享的经验总结帖子,总算是做出了一个能拿得出手的小设计:基于FR8016H和ESP8266的新冠肺炎疫情监控平台。
整体效果:
运行界面:
可以看到,整个设计不需要太多的硬件模块,所需要的只是一个非常常见的ESP8266-01S WiFi模块,还有几个跳线帽。屏幕大小虽然仅有1.54寸,分辨率却有240*240,显示效果非常细腻,所以界面看起来还是非常精致的。
整体设计流程为,FR8016H通过串口与ESP8266模块进行交互,配置模块工作模式,WiFi信息,API接口信息。连接上互联网,获取到API接口返回的最新的全球疫情数据,本地接收完成之后,使用cJSON解析库解析出我们想要的数据,再通过LCD界面显示出来。使用定时器实现每隔一定时间自动获取最新数据,刷新显示。通过读取按键输入,可以手动更新最新的疫情数据。
1.获取疫情API接口不知道大家是否了解过我之前的几个小项目:
以上两个项目都是基于Qt跨平台环境实现的,所以本质上没有太大的区别。
2020新冠疫情的爆发,各大互联网IT公司和个人都开发了实时疫情地图平台,腾讯新闻、丁香园、网易、新浪等等,这些数据大小都在几百KB,对于桌面PC和嵌入式Linux来说,不用在意数据量的大小,但是对于MCU这种存储只有几百KB的芯片来说,数据的长度就不得不考虑了。更重要的对于ESP8266,AT指令的方式,SSL缓存最大只有4096个字节的缓存!
所以API的选择至关重要,不仅要数据内容齐全,还要数据量小,由于我们要展示的信息比较少,所以我还是找到了一个数据量小,连接稳定的API接口。
经过网上一番搜索,找到了几个数据量小的API,但是有的接口连接不稳定,刚连上就掉线了,最后终于找到了一个连接稳定,数据量小,数据齐全的接口:https://lab.isaaclin.cn/nCoV/zh
这是一位国人使用服务器爬虫的方式获取丁香园的数据,然后开发了API接口供大家免费使用,目前已经被调用了几千万次,这个网站还包括了多个接口,我只使用到了其中的疫情数据这一个接口:https://lab.isaaclin.cn/nCoV/api/overall,数据量大概为1300个字节。
JSON数据内容如下:
为了能使用ESP8266获取这个API返回的内容,我们还需要知道以下信息:TCP连接类型,端口号,API地址。
我们在浏览器中按F12,打开开发者模式,在地址栏输入https://lab.isaaclin.cn/nCoV/api/overall这个接口地址,可以很容易的获取到我们想要的信息:
服务器地址:47.102.117.253
端口号:443API地址:https://lab.isaaclin.cn/nCoV/api/overall
关于端口号,如果API地址是http开头的,一般是选择TCP连接类型,80端口;如果是https开头的,一般是选择SSL连接类型,443端口。这个信息在后面会用到。
2.ESP8266的使用WiFi模块选择的是乐鑫的ESP8266-01S模组,支持AP、Station和AP&Station混合模式。
在进行正式的开发之前,我们先使用串口模块连接ESP8266,直接发送AT指令的方式来获取疫情数据。
整体流程是:配置工作模式 > 连接WiFi > 与服务器建立SSL连接 > 发送GET请求获取数据
0.为了确保模块保持初始状态,在进行配置之前,先让模块恢复出厂设置:AT+RESTORE
AT+RESTORE ets Jan 8 2013,rst cause:2, boot mode3,7)2nd boot version : 1.5 SPI Speed : 40MHz SPI Mode : DIO SPI Flash Size & Map: 8Mbit(512KB+512KB)jump to run user1 @ 1000ready
获取AT固件版本信息:AT+GMR
AT+GMRAT version:1.2.0.0(Jul 1 2016 20:04:45)SDK version:1.5.4.1(39cb9a32)Ai-Thinker Technology Co. Ltd.Dec 2 2016 14:21:16OK
有的AT固件版本不支持HTTPS连接。最新版本的AT固件是支持HTTPS连接的,下载地址:ESP8266-01S出厂默认 AT 固件
1.WiFi模块设置为Station模式:AT+CWMODE=1
2.配网,连接WiFi:AT+CWJAP="ssid","password"
AT+CWMODE=1OKAT+CWJAP="fr8016h_2019_ncov","www.wangchaochao.top"WIFI CONNECTEDWIFI GOT IPOK
3.设置单连接模式:AT+CIPMUX=0
4.设置SSL连接大小:AT+CIPSSLSIZE=4096
5.与服务器建立HTTPS/SSL连接:AT+CIPSTART="SSL","47.102.117.253",443
6.设置为透传模式:AT+CIPMODE=1
7.启动透传:AT+CIPSEND
8.发送GET HTTPS请求:GET https://lab.isaaclin.cn/nCoV/api/overall
如果以上都配置正确,会收到服务器返回的数据,也就是我们的想要的疫情数据。
如果SSL连接不断开,一直在透传模式,就可以每隔一段时间GET一次API,这样就可以获取到最新的疫情数据了。
经过多次GET请求测试发现,连接还比较稳定,没有出现掉线的情况,但是由于API的访问限制,不要太频繁的发送GET请求,否则可能会被API开发者把IP封掉。
当然,如果连接断开,就要重新执行建立SSL连接,设置透传模式,开始透传这几个操作。如果要主动断开SSL连接,可以先发送不带回车换行的+++退出透传,然后使用AT+CIPCLOSE关闭SSL连接。
单独的AT指令测试没问题,那我们就可以使用MCU的串口来自动完成和ESP8266的AT指令交互了。
3.FR8106H串口的使用FR8016H的每个GPIO使用非常灵活,支持多种复用功能,由于UART1已经使用作为程序下载和调试接口,这里我们使用UART0和ESP8266进行AT指令交互。
串口0和定时器的初始化:
void app_esp8266_init(void){ system_set_port_mux(GPIO_PORT_D, GPIO_BIT_4, PORTD4_FUNC_UART0_RXD); system_set_port_mux(GPIO_PORT_D, GPIO_BIT_5, PORTD5_FUNC_UART0_TXD); uart_init(UART0, BAUD_RATE_115200); NVIC_EnableIRQ(UART0_IRQn); //10ms中断 timer_init(TIMER0, TIME, TIMER_PERIODIC); timer_stop(TIMER0); NVIC_EnableIRQ(TIMER0_IRQn);}
PD4/5复用成串口功能,并使能中断,使用定时器0定时10ms来判断是不是连续的一帧数据。如果2个字符接收间隔超过10ms,则认为不是1次连续数据,也就是超过10ms没有接收到任何数据,则表示此次接收完毕。
串口中断复位函数:
uint8_t rx_buf[RX_BUF_SIZE];uint16_t rx_sta = 0;uint32_t TIME = 10000;__attribute__((weak)) __attribute__((section("ram_code"))) void uart0_isr_ram(void){ uint8_t int_id; uint8_t c; volatile struct uart_reg_t *uart_reg = (volatile struct uart_reg_t *)UART0_BASE; int_id = uart_reg->u3.iir.int_id; if(int_id == 0x04 || int_id == 0x0c ) { c = uart_reg->u1.data; if((rx_sta & (1 << 15)) == 0) { if(rx_sta < RX_BUF_SIZE) { if(rx_sta == 0) { timer_init(TIMER0, TIME, TIMER_PERIODIC); timer_run(TIMER0); } rx_buf[rx_sta++] = c; } else { rx_sta |= 1 << 15; } } } else if(int_id == 0x06) { volatile uint32_t line_status = uart_reg->lsr; }}
定时器中断函数:
__attribute__((weak)) __attribute__((section("ram_code"))) void timer0_isr_ram(void){ rx_sta |= 1<<15; //标记接收完成 timer_stop(TIMER0); timer_clear_interrupt(TIMER0);}
ESP8266的串口指令交互,采用发送命令,并检测返回数据的方式,如发送AT,应该回复OK,如果没有回复,则再次发送AT。具体的实现在工程app_esp8266_wifista.c文件中
为了方便进行串口发送,这里我自定义实现了串口printf函数:
串口1用于输出调试信息:
void LOG(char *fmt,...){ unsigned char UsartPrintfBuf[296]; va_list ap; unsigned char *pStr = UsartPrintfBuf; va_start(ap, fmt); vsnprintf((char *)UsartPrintfBuf, sizeof(UsartPrintfBuf), fmt, ap); //格式化 va_end(ap); while(*pStr != 0) uart_putc_noint(UART1, *pStr++);}
串口0用于和ESP8266进行交互:
void esp8266_printf(char *fmt,...){ unsigned char UsartPrintfBuf[296]; va_list ap; unsigned char *pStr = UsartPrintfBuf; va_start(ap, fmt); vsnprintf((char *)UsartPrintfBuf, sizeof(UsartPrintfBuf), fmt, ap); //格式化 va_end(ap); while(*pStr != 0) uart_putc_noint(UART0, *pStr++);}
实际的流程:
4.疫情数据的解析API接口返回的数据是JSON格式,关于JSON格式,可以参考:使用cJSON库解析和构建JSON字符串
解析库使用的是开源小巧的cJSON库,只有两个文件,使用起来非常方便。由于cJSON采用的动态内存分配的方式,所以在使用之前,要先指定内存申请和内存释放的函数,FR8016H相关的函数在os_mem.h头文件中:
- 内存申请:os_malloc
- 内存释放:os_free
在进行解析之前,先来分析一下JSON原始数据的格式:results键的值是一个数组,数组只有一个JSON对象,获取这个对象对应键的值可以获取到国内现存和新增确诊人数、累计和新增死亡人数,累计和新增治愈人数等数据。
全球疫情数据保存在globalStatistics键里,它的值是一个JSON对象,对象仅包含简单的键值对,这些键的值,就是全球疫情数据,其中updateTime键的值是更新时间,这是毫秒级UNIX时间戳,可以转换为标准北京时间。
{ "results": [{ "currentConfirmedCount": 509, "currentConfirmedIncr": 16, "confirmedCount": 85172, "confirmedIncr": 24, "suspectedCount": 1899, "suspectedIncr": 4, "curedCount": 80015, "curedIncr": 8, "deadCount": 4648, "deadIncr": 0, "seriousCount": 106, "seriousIncr": 9, "globalStatistics": { "currentConfirmedCount": 4589839, "confirmedCount": 9746927, "curedCount": 4663778, "deadCount": 493310, "currentConfirmedIncr": 281, "confirmedIncr": 711, "curedIncr": 424, "deadIncr": 6 }, "updateTime": 1593227489355 }], "success": true}
我们先定义了一个结构体,用于存放解析出来的数据:
struct ncov_data{ long currentConfirmedCount; long currentConfirmedIncr; long confirmedCount; long confirmedIncr; long curedCount; long curedIncr; long seriousCount; long seriousIncr; long deadCount; long deadIncr; char updateTime[20];};
由于全球疫情数据,都已经到了千万级别,而且新增数据有负数,即减少情况,所以这里数据类型统一选择了有符号的长整形。
定义了两个结构体,用于保存国内和全球疫情数据:
struct ncov_data dataChina = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, "01-01 01:00"};struct ncov_data dataGlobal = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, "01-01 01:00"};
完整的解析函数:
uint8_t parse_ncov_data(void){ cJSON *root; cJSON *results_arr; cJSON *results; cJSON *globalStatistics; time_t updateTime; struct tm *time; LOG("接收到的数据:%d\r\n", strlen((const char *)rx_buf)); //JSON原始数据 root = cJSON_Parse((const char *)rx_buf); if(root) { LOG("数据格式正确,开始解析\r\n"); results_arr = cJSON_GetObjectItem(root, "results"); if(results_arr->type == cJSON_Array) { results = cJSON_GetArrayItem(results_arr, 0); if(results->type == cJSON_Object) { dataChina.currentConfirmedCount = cJSON_GetObjectItem(results, "currentConfirmedCount")->valueint; dataChina.currentConfirmedIncr = cJSON_GetObjectItem(results, "currentConfirmedIncr")->valueint; dataChina.confirmedCount = cJSON_GetObjectItem(results, "confirmedCount")->valueint; dataChina.confirmedIncr = cJSON_GetObjectItem(results, "confirmedIncr")->valueint; dataChina.curedCount = cJSON_GetObjectItem(results, "curedCount")->valueint; dataChina.curedIncr = cJSON_GetObjectItem(results, "curedIncr")->valueint; dataChina.deadCount = cJSON_GetObjectItem(results, "deadCount")->valueint; dataChina.deadIncr = cJSON_GetObjectItem(results, "deadIncr")->valueint; dataChina.seriousCount = cJSON_GetObjectItem(results, "seriousCount")->valueint; dataChina.seriousIncr = cJSON_GetObjectItem(results, "seriousIncr")->valueint; LOG("------------国内疫情-------------\r\n"); LOG("现存确诊: %5d, 较昨日:%3d\r\n", dataChina.currentConfirmedCount, dataChina.currentConfirmedIncr); LOG("累计确诊: %5d, 较昨日:%3d\r\n", dataChina.confirmedCount, dataChina.confirmedIncr); LOG("累计治愈: %5d, 较昨日:%3d\r\n", dataChina.curedCount, dataChina.curedIncr); LOG("累计死亡: %5d, 较昨日:%3d\r\n", dataChina.deadCount, dataChina.deadIncr); LOG("现存无症状: %5d, 较昨日:%3d\r\n\r\n", dataChina.seriousCount, dataChina.seriousIncr); globalStatistics = cJSON_GetObjectItem(results, "globalStatistics"); if(globalStatistics->type == cJSON_Object) { dataGlobal.currentConfirmedCount = cJSON_GetObjectItem(globalStatistics, "currentConfirmedCount")->valueint; dataGlobal.confirmedCount = cJSON_GetObjectItem(globalStatistics, "confirmedCount")->valueint; dataGlobal.curedCount = cJSON_GetObjectItem(globalStatistics, "curedCount")->valueint; dataGlobal.deadCount = cJSON_GetObjectItem(globalStatistics, "deadCount")->valueint; dataGlobal.currentConfirmedIncr = cJSON_GetObjectItem(globalStatistics, "currentConfirmedIncr")->valueint; dataGlobal.confirmedIncr = cJSON_GetObjectItem(globalStatistics, "confirmedIncr")->valueint; dataGlobal.curedIncr = cJSON_GetObjectItem(globalStatistics, "curedIncr")->valueint; dataGlobal.deadIncr = cJSON_GetObjectItem(globalStatistics, "deadIncr")->valueint; LOG("------------全球疫情-------------\r\n"); LOG("现存确诊: %8d, 较昨日:%5d\r\n", dataGlobal.currentConfirmedCount, dataGlobal.currentConfirmedIncr); LOG("累计确诊: %8d, 较昨日:%5d\r\n", dataGlobal.confirmedCount, dataGlobal.confirmedIncr); LOG("累计死亡: %8d, 较昨日:%5d\r\n", dataGlobal.deadCount, dataGlobal.deadIncr); LOG("累计治愈: %8d, 较昨日:%5d\r\n\r\n", dataGlobal.curedCount, dataGlobal.curedIncr); } /* 毫秒级时间戳转字符串 */ updateTime = (time_t )(cJSON_GetObjectItem(results, "updateTime")->valuedouble / 1000); updateTime += 8 * 60 * 60; /* UTC8校正 */ time = localtime(&updateTime); /* 格式化时间 */ strftime(dataChina.updateTime, 20, "%m-%d %H:%M", time); LOG("更新于:%s\r\n", dataChina.updateTime);/* 06-24 11:21 */ } } else { LOG("数据格式错误\r\n"); return 0; } } cJSON_Delete(root); LOG("*********更新完成*********\r\n"); return 1;}
在调用cJSON_Parse()之后,一定要调用cJSON_Delete()释放内存,否则会造成内存泄露。
5.疫情数据的显示FR8016H开发板板载了一块超薄的1.54寸的LCD,虽然尺寸很小,但是分辨率却有240*240,显示效果非常细腻。
自定义实现了一些GUI绘图的函数,并简单设计了显示界面,为了减小程序大小,LCD驱动只实现了基本的画点,画线函数,字符的显示,采用的是部分字符取模,只对程序中用到的汉子和字符进行取模。具体的代码在工程中的app_lcd_gui.c文件。
最终效果:
更换了显示颜色:
6.自动更新和按键交互为了让数据能定时自动更新,这里我们使用了软件定时器来实现定时自动更新功能:
定义一个定时器:
os_timer_t timer_ncov;
初始化软件定时:
os_timer_init(&timer_ncov, ncov_update, 0);os_timer_start(&timer_ncov, 1000, true); //1s
数据更新函数:
void ncov_update(void *parg){ static uint16_t cnt = 0; int ret; cnt++; LOG("cnt: %d, k1:%d\r\n", cnt, read_btn_k1()); if(cnt >= 60*5) { cnt = 0; ret = get_ncov_api(api_data, parse_ncov_data); if(ret == 1) { ret = build_ssl_connect(data_cip_type, data_api_ip, data_api_port); if(ret == 1) app_esp8266_wifista_config(WIFI_SSID, WIFI_PWD); } }}
由于没看明白示例工程中的Button读取方式,所以这里我直接使用了一个开源的MultiButton按键驱动库,
配置K1/PC5按键为输入功能:
system_set_port_mux(GPIO_PORT_C, GPIO_BIT_5, PORTC5_FUNC_C5);gpio_set_dir(GPIO_PORT_C, GPIO_BIT_5, GPIO_DIR_IN);system_set_port_pull(GPIO_PC5, true);
读取按键输入状态:
uint8_t read_btn_k1(void){ uint8_t in; in = gpio_portc_read() & (1<<5); return in>>5;}
定义一个Button,并初始化:
struct Button btn_k1;mbutton_init(&btn_k1, read_btn_k1, 0);
绑定回调函数:
button_attach(&btn_k1, SINGLE_CLICK, button_callback);
回调函数的实现:
void button_callback(void *button){ uint32_t btn_event_val; btn_event_val = get_button_event((struct Button *)button); switch(btn_event_val) { case SINGLE_CLICK: co_printf("---> key1 single click! <---\r\n"); get_ncov_api(api_data, parse_ncov_data); break; case DOUBLE_CLICK: co_printf("***> key1 double click! <***\r\n"); break; default:break; }}
这里实现的是K1单击,更新疫情数据。
定义一个软件定时器:
os_timer_t timer_k1;os_timer_init(&timer_k1, timer_k1_fun, 0);os_timer_start(&timer_k1, 10, true);
定时器的函数:
void timer_k1_fun(void *parg){ button_ticks();}
启动按键:
button_start(&btn_k1);
7.代码下载
代码已经开源,欢迎大家参与,丰富这个小项目的功能!
GitHub开源地址:https://github.com/whik/fr8016h_2019_ncov
我的开发板版本是V1.3,如果你手上的硬件和我的一样,只需要修改工程中的user\proj_main.h文件中的WiFi信息,就可以直接使用了。
工程目录结构:
8.总结目前只使用到了一个API接口,这个平台也提供其他接口可供使用:
- 最新的疫情新闻
https://lab.isaaclin.cn//nCoV/api/news
- 最新的各省市疫情数据
https://lab.isaaclin.cn//nCoV/api/area?latest=1&province=%E5%8C%97%E4%BA%AC%E5%B8%82
- 最新的辟谣信息
https://lab.isaaclin.cn//nCoV/api/rumors
对于这个疫情监控的小项目,还有很多可以完善的功能,FR8016H芯片还支持音频解码功能,可以实现自动语音播报,UI界面可以增加更多的疫情信息显示,如最新的疫情动态新闻,疫情辟谣信息等等。作为一款蓝牙MCU,以后会学习一下蓝牙部分的开发。
除此之外,即使疫情结束,有了ESP8266和液晶显示屏,也可以做出很多有意思的项目,通过选择不同的API接口,可以实现不同的功能,如天气台历、每日新闻显示、空气质量、股票信息、汽车限行信息等等。
也可以使用ESP8266连接云平台,如OneNET、阿里云、华为云、腾讯云等物联网云平台,实现智能家居功能,远程监控和远程控制设备。