frozencandles

上一节中,介绍了 ESP8266 的使用方法。不过上一节中都是通过串口调试工具手动发送信息的方式来操作 ESP8266 ,这肯定不能用于实际开发。因此,本节介绍如何编写合适的程序来和 ESP8266 交互,从而收发并解析网络数据。

TCP服务器

在 TCP 服务器下,可以使用移动设备主动连接 ESP8266 提供的 WiFi 。如果编写正确的程序,那么可以使用移动设备控制 ESP8266 。

建立TCP服务器

从上一节的介绍可以了解到,程序和 ESP8266 的交互主要是通过发送 AT 指令完成的,因此程序中首要的任务就是编写合适的程序向 ESP8266 发送指令。

不过在发送指令后,可能还需要判断指令是否被成功接收。一般来说,ESP8266 执行失败时可能返回各种信息,但在成功执行指令后都会返回 OK 。发送指令可以通过以下函数完成:

uint8_t ESP8266_SendCmd(char* cmd, uint8_t timeout) {
    ESP8266_Buffer.Length = 0;
    memset(ESP8266_Buffer.Body, 0, USART_RX_BUF_SIZE);
    USART_printf(USART3, "%s\r\n", cmd);
    while (timeout--) {
        delay_ms(100);
        if (strstr(ESP8266_Buffer.Body, "OK"))
            return 0;
    }
    return 1;
}

由于不同指令处理的时间也不一致,因此在程序中引入了一个倒计时器,在倒计时结束前不断检查接收到的信息中是否包含 "OK" ,如果是则结束当前倒计时,这样可以确保在指令执行完后就可以立即退出延时,提高程序执行效率。

程序中与 ESP8266 交互基本是都采用这种方式。例如,在程序下载后若希望使 ESP8266 也重启,则可以通过拉低 RST 引脚复位 ESP8266 ,复位后会接到 "ready" 信息,则可以编写以下函数:

uint8_t ESP8266_Reset(uint16_t timeout) {
    ESP8266_RST(RESET);
    delay_ms(500);
    ESP8266_RST(SET);
    while (timeout--) {
        delay_ms(100);
        if (strstr(ESP8266_Buffer.Body, "ready"))
            return 0;
    }
    return 1;
}

如果某条指令有其余回复的情况,只需要参照以上略做修改即可。

有了以上函数后,就可以编写代码,逐条发送指令了。这里将 ESP8266 设置为 AP 模式,使其变成一个 WiFi 热点,使计算机可以直接连接上 ESP8266 并收发信息,因此首先需要发送 AT+CWMODE=2 指令:

if (ESP8266_Reset(50))
    return 1;
if (ESP8266_SendCmd("AT+CWMODE=2", 50))
    return 2;

然后可以使用 AT+CWSAP="<ssid>","<password>",<chl>,<enc> 设置 WiFi 参数,一般来说通道号和加密类型都设置为 4 即可:

char cmd[64];
sprintf(cmd, "AT+CWSAP=\"%s\",\"%s\",%d,%d\r\n", SSID, PASSWORD, 4, WPA_WPA2_PSK);
if (ESP8266_SendCmd(cmd, 50))
    return 3;

可以将这些参数设置为宏定义以方便修改。接下来的许多设置都和以上代码类似,可以以此为模板替换为其它命令,因此不再展示代码,仅介绍主要命令。

如果要设置固定的局域网 IP ,可以通过 AT+CIPAP="<ip>" 完成。

接下来可以通过指令 AT+CIPMODE=<mode> 设置 ESP8266 的传输模式。该命令可以设置 ESP8266 的两种传输模式:

  1. 普通传输模式(Normal Transmission Mode),该模式下,用户可以通过 AT 指令发送 TCP 数据,同时 ESP8266 也会将接收到的数据以 +IPD 等指令的形式返回
  2. 透传接收模式(Passthrough Receiving Mode):该模式下,ESP8266 无法发送 TCP 数据,同时 ESP8266 会将接收到的数据以原始的形式返回给 STM32

透传接收模式一般用于开启透传模式。关于透传模式会在后续介绍。

ESP8266 支持多路连接,即一个 TCP 端口可以建立多个连接。通过 AT+CIPMUX=1 可以启用多连接,每个连接到端口上的客户端通过 <id> 标识,连接的数量最后为 5 个,因此 <id> 的取值范围为 0~4 。

多连接必须在所有连接都断开且服务器也关闭时才可以设置,并且只有普通传输模式下才能设置为多连接。

接下来,可以通过 AT+CIPSERVER=1,8266 开启一个位于端口 8266 上的 TCP 服务器。根据以上步骤,TCP 服务器便建立完成,可以准备接收客户端发来的数据了。

数据获取与解析

在建立了 TCP 服务器后,ESP8266 便会等待客户端的连接。

TCP 客户端在接到客户端的数据时,会以 +IPD,<id>,<len>:<data> 的指令形式转交数据给 STM32 。由于以上开启了多路连接,因此接收的数据中多了一个字段 <id>

因此判断是否有数据收到也很简单,只需要判断接收缓冲区内是否有子串 "+IPD" 即可:

bool ESP8266_HasData(void) {
    return strstr(ESP8266_Buffer.Body,"+IPD") 
        && strstr(ESP8266_Buffer.Body,":");
}

以上同时查找子串 ":" 确保数据有效性。根据以上格式,拆解该字符串并截取有效数据如下:

int8_t ESP8266_MuxGetData(char* data, uint16_t* len) {
    uint8_t id;
    char* data_ptr = strstr(ESP8266_Buffer.Body, "+IPD");
    if (sscanf(data_ptr,"+IPD,%d,%d", &id, len) == 2) {
        memcpy(data, strstr(data_ptr, ":") + 1, *len);
        data[*len] = '\0';
        ESP8266_Buffer.Length = 0;
        memset(ESP8266_Buffer.Body, 0, USART_RX_BUF_SIZE);
        return id;
    }
    return -1;
}

以上函数略显复杂。之所以要这么复杂,主要有以下两个方面的原因:scanf() 类函数使用字符串转换说明时,它在读入数据时如果遇到一个空格或回车符,会丢弃后面的所有数据,这显然不能用于截取用户数据。

另外上一节说过工程中接收串口传来的不定长数据的方式是使用串口空闲中断,然而空闲中断接收的一包数据并不都是符合期望的一包数据:在接收到 TCP 连接时,ESP8266 会发送 <id>,CONNECT 表示连接已建立,如果此时建立的连接接收到任何数据,ESP8266 也会立即转发该数据。因此如果连接建立后马上收到数据,那么两次发送的数据时间相隔过短,可能会没有引起空闲中断而被 STM32 认为是同一包数据。在连接取消时,也有同样的问题。

不过发送数据的函数可能更加复杂:

uint8_t ESP8266_MuxSendData(uint8_t* data, uint16_t length, uint8_t id, uint8_t timeout) {
    ESP8266_ClearBuffer();
    USART_printf(USART3, "AT+CIPSEND=%d,%d\r\n", id, length);
    while (timeout--) {
        delay_ms(10);
        if (strstr(ESP8266_Buffer.Body, ">"))
            break;
    }
    if (timeout > 0) {
        ESP8266_ClearBuffer();
        USART_SendBytes(USART3, data, length);
        while (timeout--) {
            delay_ms(10);
            if (strstr(ESP8266_Buffer.Body, "SEND OK")) {
                ESP8266_ClearBuffer();
                return 0;
            }
            if (strstr(ESP8266_Buffer.Body, "link is not valid")) {
                ESP8266_ClearBuffer();
                return 2;
            }
        }
        return 3;
    }
    else
        return 1;
}

上一节介绍了发送数据主要使用 AT+CIPSEND 指令完成(多连接下需要一个额外的字段指示发送给的 <id> ),如果可以发送 ESP8266 会返回 "> " 作提示。如果发送成功,ESP8266 会返回 "SEND OK" ,通过返回提示就可以知道发送状态。

有了以上函数以后,就可以着手编写主程序了。主程序的处理逻辑非常简单,在建立 TCP 服务器后,便不断判断是否有数据到达,如果有那么便读取数据并回复信息:

char ipd_data[512];
int8_t ipd_id;
uint16_t ipd_len;
while (ESP8266_CreateTcpServer())
    delay_ms(200);
while (1) {
    if (ESP8266_HasData()) {
        ipd_id = ESP8266_MuxGetData(ipd_data, &ipd_len);
        ESP8266_MuxSendData("Acknowledge", 12, ipd_id, 30);
    }
    delay_ms(500);
}

可以将得到的数据显示在串口中。在计算机客户端,编写如下套接字程序:

import socket, time
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('192.168.10.1', 8266))
client.send(time.ctime().encode())
message = client.recv(1024)
print(message.decode())
client.close()

将计算机连接到 ESP8266 创建的 WiFi 上并运行该套接字程序,即可观察到实验现象。如果为程序编写合适的用户界面并在 STM32 上进一步解析数据,那么便可以实现手机端控制 STM32 了。

TCP客户端与HTTP请求

TCP 客户端的建立与 TCP 服务器类似,这里先使 ESP8266 连接到路由器,借助路由器访问公网上的服务器。

前几步操作与 TCP 客户端类似:在复位 ESP8266 后,首先通过指令 AT+CWMODE=1 设置 Sta 模式,然后通过 AT+CWJAP="<ssid>","<password>" 连接到路由器中。由于客户端无需多个连接,可以使用 AT+CIPMUX=0 关闭多连接。

本次采用透传模式(Passthrough Mode)来收发数据。透传模式是一种特殊的收发数据模式,在透传模式下,用户不能发送 AT 指令,发送的任何数据都会作为原始的数据发送到传输对端;从传输对端收到的数据也会不经由任何 +IPD 封装而原封不动地返回给 STM32 。

使用 AT+CIPMODE=1 可以设置传输模式为透传模式。通过 AT+CIPSTART 连接上服务器后,直接执行 AT+CIPSEND ,待 ESP8266 返回 "> " 后就进入了透传模式。透传模式下,每包数据以 20ms 间隔区分,每包最大 2048 字节,发送和接收数据都不需要封装成指令,方便处理。

正常退出透传模式的唯一方式就是单独发送一包发送指令 +++

根据以上原理,可以使用 STM32 发送相应指令,连接到服务器后进入透传模式,并准备发送相应的数据。其代码和上文服务端类似,例如:

if (ESP8266_Reset(50))
    while (1);
if (ESP8266_SendCmd("AT+CWMODE=1", 20))
    while (1);
if (ESP8266_SendCmd("AT+CWJAP=\"TP_LINK\",\"abc123456\"", 100))
    while (1);
// ... and so on

当然,考虑到一些指令执行成功时不总是返回 OK ,并且为了使程序逻辑更清晰,可以将一些指令封装成函数。例如,以下函数根据地址(可以是 IP 地址或域名,DNS 解析将自动完成)和端口号,连接到特定的 TCP 服务器中并进入透传模式:

uint8_t ESP8266_ConnectServer(char* address, uint16_t port, uint8_t timeout) {
    ESP8266_ClearBuffer();
    USART_printf(USART3, "AT+CIPSTART=\"TCP\",\"%s\",%d\r\n", address, port);
    while (timeout--) {
        delay_ms(100);
        if (strstr(ESP8266_Buffer.Body, "CONNECT")) {
            ESP8266_ClearBuffer();
            USART_printf(USART3, "AT+CIPSEND\r\n");
            while (timeout--) {
                delay_ms(100);
                if (strstr(ESP8266_Buffer.Body, "\r\nOK\r\n\r\n>"))
                    return 0;
            }
            return 4;
        }
        if (strstr(ESP8266_Buffer.Body, "CLOSED"))
            return 1;
        if (strstr(ESP8266_Buffer.Body, "ALREADY CONNECTED"))
            return 2;
    }
    return 3;                   //超时错误,返回3
}

可以仿照该函数将其它指令封装成具有抽象功能的对应函数。

在本示例中,在使用 TCP 连接到远程服务器的 80 端口的基础上,手动构造合适的 HTTP 请求并发送:

while(1) {
    USART_SendString(USART3, "GET /api/temperature?time=now HTTP/1.1\r\n"
                            "Connection: keep-alive\r\n"
                            "Host: 192.168.1.105:80\r\n\r\n");
    delay_s(5);
    printf("%s", ESP8266_Buffer.Body);
}

这里 5 秒钟便查询一次数据。如果间隔过长,连接可能断开,那么可以先主动断开连接,等需要查询时再发起 TCP 连接。

通过 HTTP 服务器提供的合适接口,ESP8266 便可以从互联网中获取到非常广泛的数据。在测试用的服务器中,该接口返回一个 json 响应并被转发到 STM32 中,串口调试工具中显示的原始数据如下:

HTTP/1.1 200 OK
Date: Wed, 13 Jul 2022 10:57:48 GMT
Server: WSGIServer/0.2 CPython/3.9.1
Content-Type: application/json
X-Frame-Options: DENY
Content-Length: 93
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin

{"temperature": {"high": 37.6, "low": 28.1, "now": 36.7}, "humidity": "50%", "wind": "11mph"}

通过解析请求头 Content-Length 就可以获取数据的长度,然后查找子串 "{" 的位置便可以提取出接口返回的 json 数据,并可以使用 cJSON 等第三方库解析其中的数据。互联网中存在许多类似的接口,只需要构造合适的请求头,便可以抓取很多有用的信息,不过这需要有一定的抓包或爬虫的基础。

通过路由器可以接入互联网,在 TCP 服务的基础上,构造出合适的 HTTP 等应用层协议的封装,便可以采集互联网中的各种数据,或者向服务器报告自身传感器的数据,由此真正实现物联网的基础。

例如,可以向 HTTP 服务器提供的接口发送 POST 请求,将传感器数据作为参数发送给服务器,服务器解析 POST 请求并更新数据库,然后便可以显示在前端上,这样便可以在任何地点查看 STM32 的状态了。不过由于其实现涉及到的知识点过于广泛,无论是环境的配置还是程序的编写都不是一篇文章能完整介绍的,这里便不再涉及。

一个比较有趣的实现是利用 SMTP 发送电子邮件,可以阅读这篇文章了解 SMTP 应用层协议的原理与基本报文格式,文章中附带了 Python 套接字程序实现,它和 AT 指令的思路具有一定相似性,移植到 STM32 的主要难点是使用 base64 编码完成身份验证,有兴趣的读者可以尝试自行实现。

参考资料/延伸阅读

https://docs.espressif.com/projects/esp-at/en/release-v2.2.0.0_esp8266/AT_Command_Set/TCP-IP_AT_Commands.html

TCP/IP 相关 AT 指令集的官方文档。

相关文章: