ARM

基于灵动MM32的新冠肺炎疫情数据实时监控平台

开源小项目

Posted by Wang Chao on June 27, 2020

前言

2020,新冠肺炎疫情全球爆发,国内得到了有效的控制。最近北京疫情,形势又紧张起来了,我们公司也为了配合防控疫情的要求,由之前的复工复产改为了居家办公。

不知道大家是否了解过,我之前春节假期在家做的的两个初学Qt的实战项目:

由于Qt的跨平台特性,所以在嵌入式Linux上的移植也比较顺利。对于PC和嵌入式ARM来说,疫情数据的获取,数据的图表显示,辟谣信息、疫情新闻的显示,做起来都比较轻松。

作为疫情监控平台三部曲:桌面 > 嵌入式ARM Linux > MCU。在前面两个平台实现之后,就一直想着在内存和性能有限的MCU上实现,但一直都没有找到一个合适的API接口,直到最近发现了一个数据量比较小,连接比较稳定的API。于是,我利用有限的业余时间,设计了这个基于灵动MM32 MCU的疫情监控平台,MM32通过串口和ESP8266进行AT指令交互,连接互联网获取最新的疫情数据,并显示在TFT显示屏上,可以直观方便的了解到最新的疫情数据信息。

疫情监控平台

获取疫情数据API接口

2020新冠疫情的爆发,各大互联网IT公司和个人都开发了实时疫情地图平台,腾讯新闻、丁香园、网易、新浪等等,这些数据大小都在几百KB,对于PC和嵌入式Linux来说,不用在意数据量的大小,但是对于存储非常有限的MCU来说,数据量的大小是不得不考虑的一个问题,而且对于ESP8266来说,AT指令的方式,SLL缓存最大只有4096个字节的缓存!

经过网上一番搜索,找到了几个数据量小的API,但是有的接口连接不稳定,刚连上就掉线了,最后终于找到了一个连接稳定,数据量小,数据齐全的接口:https://lab.isaaclin.cn/nCoV/zh

这是一位国人使用服务器爬虫获取了丁香园的数据,然后开放了API接口供大家免费使用,目前已经被调用了2千万次,这个网站还包括了多个接口,我只使用到了其中的疫情数据这一个接口:https://lab.isaaclin.cn/nCoV/api/overall,数据量大概为1300个字节。

JSON数据内容如下:

json数据格式

为了能使用ESP8266获取这个API返回的内容,我们还需要知道以下信息:TCP连接类型,端口号,API地址。

我们在浏览器中按F12,打开开发者模式,在地址栏输入https://lab.isaaclin.cn/nCoV/api/overall这个接口地址,可以很容易的获取到我们想要的信息:

服务器地址:47.102.117.253
端口号:443
API地址:https://lab.isaaclin.cn/nCoV/api/overall

关于端口号,如果API地址是http开头的,一般是选择TCP连接类型,80端口;如果是https开头的,一般是选择SSL连接类型,443端口。这个信息在后面会用到。

API获取

ESP8266发送HTTPS请求

WiFi模块选择的是乐鑫的ESP8266-01S模组,支持AP、Station和AP&Station混合模式。

ESP8266-01S

在进行正式的开发之前,我们先使用串口模块连接ESP8266,直接发送AT指令的方式来获取疫情数据。

整体流程是:配置工作模式 > 连接WiFi > 与服务器建立SSL连接 > 发送GET请求获取数据

0.为了确保模块保持初始状态,在进行配置之前,先让模块恢复出厂设置:AT+RESTORE


AT+RESTORE
    
 ets Jan  8 2013,rst cause:2, boot mode:(3,7)

2nd boot version : 1.5
  SPI Speed      : 40MHz
  SPI Mode       : DIO
  SPI Flash Size & Map: 8Mbit(512KB+512KB)
jump to run user1 @ 1000

ready

获取AT固件版本信息:AT+GMR


AT+GMR

AT 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:16
OK

有的AT固件版本不支持HTTPS连接。最新版本的AT固件是支持HTTPS连接的,下载地址:ESP8266-01S出厂默认 AT 固件

1.WiFi模块设置为Station模式:AT+CWMODE=1

2.配网,连接WiFi:AT+CWJAP="ssid","password"


AT+CWMODE=1

OK
AT+CWJAP="mm32_2019_ncov","www.wangchaochao.top"

WIFI CONNECTED
WIFI GOT IP

OK

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指令交互了。

MM32获取疫情数据过程

JSON数据的解析

数据是JSON格式的,解析库使用的是开源小巧的cJSON库,只有两个文件,使用起来非常方便。

在进行解析之前,先来分析一下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
}

先定义了结构体ncov_data,用于存储国内和全球疫情数据:


struct ncov_data{
    int confirmedCount; //累计确诊
    int curedCount;     //累计治愈
    int deadCount;      //累计死亡
    int confirmedIncr;  //新增确诊
    int curedIncr;      //新增治愈
    int deadIncr;        //新增死亡
    char updateTime[20];
};

对应的解析函数:


#include "ncov_data.h"

struct ncov_data dataChina;
struct ncov_data dataGlobal;

uint8_t parse_ncov_data(struct ncov_data *dataChina, struct ncov_data *dataGlobal)
{
    cJSON *root;
    cJSON *results_arr;
    cJSON *results;
    cJSON *globalStatistics;
    
    time_t updateTime;
    struct tm *time;
    
    printf("开始解析疫情数据, 数据长度:%d\r\n", strlen((const char *)UART2_RX_BUF));
	
	/* 申请内存 */
    root = cJSON_Parse((const char *)UART2_RX_BUF);
    if(!root)
    {
        printf("疫情数据格式错误\r\n");
        return 0;
    }
    else
    {
        printf("疫情数据格式正确,开始解析\r\n");

        results_arr = cJSON_GetObjectItem(root, "results");
        if(results_arr)
        {
            results = cJSON_GetArrayItem(results_arr, 0);
            if(results)
            {
                dataChina->confirmedCount = cJSON_GetObjectItem(results, "currentConfirmedCount")->valueint;
                dataChina->confirmedIncr = cJSON_GetObjectItem(results, "currentConfirmedIncr")->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;

                printf("------------国内疫情-------------\r\n");
                printf("现存确诊: %-7d, 较昨日:%-5d\r\n", 
                    dataChina->confirmedCount, dataChina->confirmedIncr);
                printf("累计治愈: %-7d, 较昨日:%-5d\r\n", 
                    dataChina->curedCount, dataChina->curedIncr);
                printf("累计死亡: %-7d, 较昨日:%-5d\r\n", 
                    dataChina->deadCount, dataChina->deadIncr);

                globalStatistics = cJSON_GetObjectItem(results, "globalStatistics");
                if(globalStatistics)
                {
                    dataGlobal->confirmedCount = cJSON_GetObjectItem(globalStatistics, "currentConfirmedCount")->valueint;
                    dataGlobal->confirmedIncr = cJSON_GetObjectItem(globalStatistics, "currentConfirmedIncr")->valueint;
                    dataGlobal->curedCount = cJSON_GetObjectItem(globalStatistics, "curedCount")->valueint;
                    dataGlobal->curedIncr = cJSON_GetObjectItem(globalStatistics, "curedIncr")->valueint;
                    dataGlobal->deadCount = cJSON_GetObjectItem(globalStatistics, "deadCount")->valueint;
                    dataGlobal->deadIncr = cJSON_GetObjectItem(globalStatistics, "deadIncr")->valueint;

                    printf("------------全球疫情-------------\r\n");
                    printf("现存确诊: %-7d, 较昨日:%-5d\r\n", 
                        dataGlobal->confirmedCount, dataGlobal->confirmedIncr);
                    printf("累计治愈: %-7d, 较昨日:%-5d\r\n",
                        dataGlobal->deadCount, dataGlobal->curedIncr);
                    printf("累计死亡: %-7d, 较昨日:%-5d\r\n", 
                        dataGlobal->curedCount, dataGlobal->deadIncr);
                }
                
                /* 毫秒级时间戳转字符串 */
                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);
                printf("更新于:%s\r\n", dataChina->updateTime);/* 06-24 11:21 */
            }
        }
    }
	
	/* 释放内存 */
    cJSON_Delete(root);

    printf("*********更新完成*********\r\n");
    return 1;
}

在调用cJSON_Parse()之后,一定要调用cJSON_Delete()释放内存,否则会造成内存泄露。

如果解析失败,可以把启动文件里的堆栈大小设置大一点:

2020-06-27_123459

TFT显示

液晶屏使用的是1.8寸TFT,128*160分辨率,SPI接口,使用的GPIO软件模拟的方式。由于屏幕分辨率比较低,可显示的内容有限,所以只是显示了最基本的几个疫情数据。为了减小程序大小,LCD驱动只实现了基本的画点,画线函数,字符的显示,采用的是部分字符取模,只对程序中用到的汉子和字符进行取模。为了增强可移植性,程序中并没有使用外置SPI Flash存储整个字库,下面是显示效果:

全球疫情数据展示

待优化和调整

目前只使用到了一个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

代码下载

代码已经开源,地址在文末,欢迎大家参与,丰富这个小项目的功能!

GitHub开源地址:https://github.com/whik/mm32_2019_ncov

如果你手上的硬件和我的一样,只需要修改工程中的USER\config.h文件中的WiFi信息,就可以直接使用了。