5·ESP32-C3入门教程——从本地控制走向云端控制(云端篇)

        距离实现一个完整的物联网小应用只差最后一步了,今天聊聊怎么样在手机上对ESP32芯片发送指令和接收数据,并借助ESP官方的接口——rainmaker,来实现远程控制和通信。我们也借由此进入智能家居时代1.0(部分物联网概念可以看看【序】在23年谈物联网

        在上一篇中已经聊过了本地控制的TCP/IP协议以及UDP协议的概念和基础,通过这两个协议的配置我们已经可以实现在同一个互联网下的远程控制(链接指路:从本地控制走向云端控制(本地篇) )在这一章,将会学习MQTT协议和HTTP协议的特点以及使用,逐渐学会接入ESP RAINMAKER。

level 1: 从MQTT入手云端控制

        MQTT协议(消息队列遥测传输协议),是一种基于基于TCP/IP,低开销、低带宽占用的即时通讯协议。具有轻量、简单、开放和易于实现的优势,采用publish/subscribe(即发布与订阅)模式。

        相比于HTTP,MQTT可以以极少的代码和有限的带宽,为连接远程设备提供实时可靠的消息服务,它主要有以下几项特性:

  • 使用发布/订阅消息模式,提供一对多的消息发布,解除应用程序耦合

  • 对负载内容屏蔽的消息传输

  • 使用TCP/IP提供网络连接

  • 小型传输,开销很小(固定长度的头部是2字节),协议交换最小化,以降低网络流量;

  • 有三种消息发布服务质量:

    •  “至多一次”,消息发布完全依赖底层 TCP/IP 网络。会发生消息丢失或重复。这一级别可用于 如下情况,环境传感器数据,丢失一次读记录无所谓,因为不久后还会有第二次发送。

    • “至少一次”,确保消息到达,但消息重复可能会发生。

      扫描二维码关注公众号,回复: 14583433 查看本文章
    • “只有一次”,确保消息到达一次。这一级别可用于如下情况,在计费系统中,消息重复或丢失 会导致不正确的结果。

        实现 MQTT 协议需要客户端和服务器端通讯完成,在通讯过程中,MQTT协议中有三种身份:发布者(Publish)、代理(Broker)、订阅者(Subscribe)。 在我们的这个实验里,消息的发布者是ESP32开发板开发板,代理使用的是移动的onenet MQTT服务器。 

        当然,ESP也内置了MQTT的例程,在vscode中contorl+shift+p 输入example ,然后选择protocols→mqtt→tcp,当然,除了tcp之外,也有几个例程可用于拓展学习。

我们先从模块的角度去理解这块代码是如何实现的:

         app_main里面的东西原理和本地控制是一模一样的,最近几篇的代码风格都重新统一过了,还算是比较清晰的。需要注意的是,因为调用的是example_connet的配网模式,所以不要忘了在顶层的cmakelist中替换成以下代码(不然会出现个别头文件读不出来无法跳转的情况,虽然这并不影响烧录):

cmake_minimum_required(VERSION 3.5)  
set(EXTRA_COMPONENT_DIRS $ENV{IDF_PATH}/examples/common_components/protocol_examples_common)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(mqtt_tcp)

         wifi的账号和密码则是在esp的设置中,需要在代码里面提前配置好。

         烧录完成后,就是最后一步了——实现MQTT通信,这里我们可以借助MQTTBOX这个软件,

level 2:实现HTTP通信

        HTTP 全称是 HyperText Transfer Protocol,也叫做超文本传输协议,它是互联网上应用最为广泛的一种网络协议。

        HTTP 是一个客户端和服务器端请求和应答的标准(TCP)。客户端(client)是终端用户,服务器(broker)端是网站。通过使用 Web 浏览器、网络爬虫或者其它的工具,客户端发起一个到服务器上指定端口(默认端口为 80)的HTTP 请求,具体的实现流程图如下:

        当然,对于入门来说,还要了解URL的概念(以下面的网址为例子):http://www.example.com:80/path/to/myfile.html?key1=value1&key2=value2#SomewhereInTheDocument

        HTTP 协议使用 URI 定位互联网上的资源。正是因为 URI 的特定功能,在互联网上任意位置的资源都能访问到。URL 带有请求对象的标识符:

方案或协议  http://
  http:// 告诉浏览器使用何种协议。对于大部分 Web 资源,通常使用 HTTP 协议或其安全版本,HTTPS 协议。另外,浏览器也知道如何处理其他协议。例如, mailto: 协议指示浏览器打开邮件客户端;ftp:协议指示浏览器处理文件传输。


主机  www.example.com
  www.example.com 既是一个域名,也代表管理该域名的机构。它指示了需要向网络上的哪一台主机发起请求。当然,也可以直接向主机的 IP address 地址发起请求。但直接使用 IP 地址的场景并不常见。


端口  :80
  www.example.com 既是一个域名,也代表管理该域名的机构。它指示了需要向网络上的哪一台主机发起请求。当然,也可以直接向主机的 IP address 地址发起请求。但直接使用 IP 地址的场景并不常见。


路径  /path/to/myfile.html
  /path/to/myfile.html 是 Web 服务器上资源的路径。以端口后面的第一个 / 开始,到 ? 号之前结束,中间的 每一个/ 都代表了层级(上下级)关系。这个 URL 的请求资源是一个 html 页面。


查询  ?key1=value1&key2=value2
  ?key1=value1&key2=value2 是提供给 Web 服务器的额外参数。如果是 GET 请求,一般带有请求 URL 参数,如果是 POST 请求,则不会在路径后面直接加参数。这些参数是用 & 符号分隔的键/值对列表。key1 = value1 是第一对,key2 = value2 是第二对参数。


片段  #SomewhereInTheDocument
  #SomewhereInTheDocument 是资源本身的某一部分的一个锚点。锚点代表资源内的一种“书签”,它给予浏览器显示位于该“加书签”点的内容的指示。 例如,在HTML文档上,浏览器将滚动到定义锚点的那个点上;在视频或音频文档上,浏览器将转到锚点代表的那个时间。值得注意的是 # 号后面的部分,也称为片段标识符,永远不会与请求一起发送到服务器。

        ESP也内置了HTTP的例程,在vscode中contorl+shift+p 输入example ,然后选择protocols→esp_http_client(level1中导入cmakelist和wifi的操作不要忘记)  

         官方给的例程还是太复杂了,所以引用一段简化后的代码,用整体模块的思路去理解会更加便于入门:

大致使用了以下几个模块:

  • GET :获取资源,GET 方法用来请求访问已被 URI 识别的资源。指定的资源经服务器端解析后返回响应内容。也就是说,如果请求的资源是文本,那就保持原样返回。
  • POST:传输实体,虽然 GET 方法也可以传输主体信息,但是便于区分,我们一般不用 GET 传输实体信息,反而使用 POST 传输实体信息。
  • PUT:传输文件。就像 FTP 协议的文件上传一样,要求在请求报文的主体中包含文件内容,然后保存到请求 URI 指定的位置。
  • DELETE:删除文件,DELETE 方法用来删除文件,是与 PUT 相反的方法。DELETE 方法按请求 URI 删除指定的资源。

简化后的部分代码如下:

#include <string.h>
#include <stdlib.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "esp_system.h"
#include "nvs_flash.h"
#include "esp_event.h"
#include "esp_netif.h"
#include "protocol_examples_common.h"
#include "esp_tls.h"
#include "esp_http_client.h"


#define MAX_HTTP_OUTPUT_BUFFER 2048
static const char *TAG = "HTTP_CLIENT_EXAMPLE";


int esp_http_run (esp_http_client_handle_t client, char* output_buffer)
{
    int content_length = esp_http_client_fetch_headers(client);
    if (content_length <= 0) {
        ESP_LOGD(TAG, "HTTP client fetch headers failed, content_length = %d", content_length);
    } 
    
    int data_read = esp_http_client_read_response(client, output_buffer, MAX_HTTP_OUTPUT_BUFFER);
    if (data_read >= 0) {
        ESP_LOGI(TAG, "HTTP Status = %d, content_length = %d, chunked = %d", esp_http_client_get_status_code(client), esp_http_client_get_content_length(client), esp_http_client_is_chunked_response(client));
        ESP_LOGI(TAG, "HTTP Response|%d %s", data_read, output_buffer);
    } else {
        ESP_LOGE(TAG, "Failed to read response");
    }

    return content_length;
}

void http_native_request(void)
{
    char output_buffer[MAX_HTTP_OUTPUT_BUFFER] = {0};   // Buffer to store response of http request
    int content_length = 0;
    esp_http_client_config_t config = {
        .host = "keyueli.cn",
        .port = 8080,
        .path = "/v1/admin/test",
        .query = "a=1&b=2.2&c=qqq",
    };
    esp_http_client_handle_t client = esp_http_client_init(&config);

    // GET Request1
    ESP_LOGE(TAG, "HTTP Get1");
    esp_http_client_set_url(client, "http://keyueli.cn:8080/v1/admin/test1/get");
    esp_http_client_set_method(client, HTTP_METHOD_GET);
    esp_err_t err = esp_http_client_open(client, 0);    
    if (err != ESP_OK) {
        ESP_LOGE(TAG, "Failed to open HTTP connection: %s", esp_err_to_name(err));
    } else {
        memset(output_buffer, 0, sizeof(output_buffer));
        content_length = esp_http_run(client, output_buffer);
    }
    esp_http_client_close(client);

    // GET Request2
    ESP_LOGE(TAG, "HTTP Get2");
    esp_http_client_set_url(client, "http://keyueli.cn:8080/v1/admin/test/get?a=1&b=2.2&c=qqq");
    esp_http_client_set_method(client, HTTP_METHOD_GET);
    err = esp_http_client_open(client, 0);    
    if (err != ESP_OK) {
        ESP_LOGE(TAG, "Failed to open HTTP connection: %s", esp_err_to_name(err));
    } else {
        memset(output_buffer, 0, sizeof(output_buffer));
        content_length = esp_http_run(client, output_buffer);
    }
    esp_http_client_close(client);

    // POST Request
    ESP_LOGE(TAG, "HTTP Post");
    esp_http_client_set_url(client, "http://keyueli.cn:8080/v1/admin/test/post?a=1&b=2.2&c=qqq");
    esp_http_client_set_method(client, HTTP_METHOD_POST);
    err = esp_http_client_open(client, 0);
    if (err != ESP_OK) {
        ESP_LOGE(TAG, "Failed to open HTTP connection: %s", esp_err_to_name(err));
    } else {
        memset(output_buffer, 0, sizeof(output_buffer));
        content_length = esp_http_run(client, output_buffer);
    }
    esp_http_client_close(client);

    // POST Request
    ESP_LOGE(TAG, "HTTP PostByBody");
    const char *post_data = "{\"a\":1,\"b\":2.2,\"c\":\"qqq\"}";
    esp_http_client_set_url(client, "http://keyueli.cn:8080/v1/admin/test/postByBody");
    esp_http_client_set_method(client, HTTP_METHOD_POST);
    esp_http_client_set_header(client, "Content-Type", "application/json");
    err = esp_http_client_open(client, strlen(post_data));
    if (err != ESP_OK) {
        ESP_LOGE(TAG, "Failed to open HTTP connection: %s", esp_err_to_name(err));
    } else {        
        int wlen = esp_http_client_write(client, post_data, strlen(post_data));
        if (wlen < 0) {
            ESP_LOGE(TAG, "HTTP Post Write failed");
        }
        memset(output_buffer, 0, sizeof(output_buffer));
        content_length = esp_http_run(client, output_buffer);
    }
    esp_http_client_close(client);

    // PUT Request
    ESP_LOGE(TAG, "HTTP Put");
    esp_http_client_set_url(client, "http://keyueli.cn:8080/v1/admin/test/put");
    esp_http_client_set_method(client, HTTP_METHOD_PUT);
    esp_http_client_set_header(client, "Content-Type", "application/json");
    err = esp_http_client_open(client, strlen(post_data));
    if (err != ESP_OK) {
        ESP_LOGE(TAG, "Failed to open HTTP connection: %s", esp_err_to_name(err));
    } else {        
        int wlen = esp_http_client_write(client, post_data, strlen(post_data));
        if (wlen < 0) {
            ESP_LOGE(TAG, "HTTP Post Write failed");
        }
        memset(output_buffer, 0, sizeof(output_buffer));
        content_length = esp_http_run(client, output_buffer);
    }
    esp_http_client_close(client);

    // DELETE Request
    ESP_LOGE(TAG, "HTTP Delete");
    esp_http_client_set_url(client, "http://keyueli.cn:8080/v1/admin/test/delete");
    esp_http_client_set_method(client, HTTP_METHOD_DELETE);
    esp_http_client_set_header(client, "Content-Type", "application/json");
    err = esp_http_client_open(client, strlen(post_data));
    if (err != ESP_OK) {
        ESP_LOGE(TAG, "Failed to open HTTP connection: %s", esp_err_to_name(err));
    } else {        
        int wlen = esp_http_client_write(client, post_data, strlen(post_data));
        if (wlen < 0) {
            ESP_LOGE(TAG, "HTTP Post Write failed");
        }
        memset(output_buffer, 0, sizeof(output_buffer));
        content_length = esp_http_run(client, output_buffer);
    }

    esp_http_client_cleanup(client);
}

static void http_test_task(void *pvParameters)
{
    http_native_request();    

    ESP_LOGI(TAG, "Finish http example");
    vTaskDelete(NULL);
}

void app_main(void)
{
    esp_err_t ret = nvs_flash_init();
    if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
      ESP_ERROR_CHECK(nvs_flash_erase());
      ret = nvs_flash_init();
    }
    ESP_ERROR_CHECK(ret);
    ESP_ERROR_CHECK(esp_netif_init());
    ESP_ERROR_CHECK(esp_event_loop_create_default());

    ESP_ERROR_CHECK(example_connect());
    ESP_LOGI(TAG, "Connected to AP, begin http example");

    xTaskCreate(&http_test_task, "http_test_task", 8192, NULL, 5, NULL);
}

level 3:接入ESP RAINMAKER

        找了很多平台,发现关于这一块的教程非常少,官方有例程在github上可以参考,链接指路:esp rainmaker官方例程,虽然这里面有很丰富的开发例程,但是不容易上手。有一个相对简化一些的代码在这里,可以先从简化版demo去学习,链接指路:ESP32-C3 物联网工程 配套代码 在这个文件夹中的例程有部分注释,可以简要学习一下,全流程的完成实现模块会在不久之后单独发一篇文章详谈。

        

小结

        写到这里,基本上可以说是完成了一个相对完整的物联网应用架构了。在这个模块,陆续写下了七篇文章,一开始从最基础的 点灯→定时器点灯→PWM点灯,学习了最常见的控制方式;再到后来的 按键检测→中断回调控制→多线程控制 ,强化了控制相关的概念理解 ;再到  wifi scan →wifi ap/sta 模式→smart config ,完成了wifi模块概念的学习和使用;最后逐渐走向云端 从TCP/IP 协议→UDP的广播,实现了局域网之下的本地控制;再到这一章 MQTT→HTTP→ESP RAINMAKER 的学习,一路下来的学习大约花了一个月的时间。

        整理的过程还是蛮有收获的,在攻克主线的同时,也在不经意间学到了很多附加的知识,由于时间有限,所以还有一些地方比如蓝牙、lvgl图形化界面等没有展开讲解,未来一定会填坑的(都看到这了,不如关注一下吧(ง •_•)ง),近期的主要方向会更加集中于毫米波人体状态检测(测距)k210机器视觉的图像处理等,当然,最后也会和esp32做一个联动,希望大家敬请期待吧!

猜你喜欢

转载自blog.csdn.net/TenYao_/article/details/129079061