让你的ESP32显示一张公网图片吧

让你的ESP32显示一张公网图片吧

众所周知,ESP32它是能联网的,而配上屏幕,它就可以显示一张图片,那么它可以显示一张互联网上的图片吗?当然可以!
虽然之前已经做过了视频的传输,而视频不过是很多图片的集合,但是那篇文章中的传输都是建立在局域网之中,通信建立在Socket上。
而这次要突破局域网,使用HTTP协议来传输数据,所以会比之前复杂一些。

使用的网络请求库为AsyncTCP-esphome,所以将以下内容添加到你的platformio.ini中

1
2
3
lib_deps = 
ottowinter/AsyncTCP-esphome@^1.1.1
ottowinter/ESPAsyncWebServer-esphome@^1.2.7

这里先介绍一下HTTP协议吧,引用百度百科的原文
超文本传输协议(Hypertext Transfer Protocol,HTTP)是一个简单的请求-响应协议,它通常运行在TCP之上。它指定了客户端可能发送给服务器什么样的消息以及得到什么样的响应。
在这个通信过程中,客户端和服务端以报文的形式通信,客户端发送的报文叫做请求报文服务端发送的报文叫做应答报文
其中请求报文的格式结构如下
请求行 - 通用信息头 - 请求头 - 实体头 - 报文主体
应答报文结构如下
状态行 - 通用信息头 - 响应头 - 实体头 - 报文主体
知道报文格式后,解析报文,就能获取我们想要的内容。
下面通过一次演习来研究下报文有什么内容,然后再做后续开发。

演习

为了此次演习,我在阿里云OSS中上传了一张很小的JPG图片,借助阿里云OSS,这张图片就有了一个固定的公网地址,
![idea.jpg](https://github.com/chaosgoo/blog_pic/blob/master/oss_backup/response.jpg?raw=true
https://github.com/chaosgoo/blog_pic/blob/master/oss_backup/response.jpg

下面打开Postman,把图片地址填进去,请求头参数保持默认,然后发送请求,直接查看Postman的控制台,点击Show raw log查看原始请求日志。
![](https://github.com/chaosgoo/blog_pic/blob/master/oss_backup/blog_pic/ESP32HTTP/PostmanRequestRaw.jpg?raw=true

请求报文如下

1
2
3
4
5
6
7
GET /response.jpg HTTP/1.1
User-Agent: PostmanRuntime/7.26.10
Accept: */*
Postman-Token: 8f142c2a-1547-40df-a07e-637450e576c4
Host: chaosgoo-pic.oss-cn-shanghai.aliyuncs.com
Accept-Encoding: gzip, deflate, br
Connection: keep-alive

请求报文的结构如图所示
请求报文的结构
请求报文中需要关注的内容有

内容 备注
GET /response.jpg HTTP/1.1 请求行,由方法字段、URL字段和HTTP协议版本字段组成
Host: chaosgoo-pic.oss-cn-shanghai.aliyuncs.com 请求的主机名和端口号

可以发现,表中内容就是由图片的链接构造出来的。

再看一眼应答报文,应答报文的结构如图所示
应答报文的结构

应答报文如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
HTTP/1.1 200 OK
Server: AliyunOSS
Date: Thu, 04 Mar 2021 08:52:28 GMT
Content-Type: image/jpeg
Content-Length: 2544
Connection: keep-alive
x-oss-request-id: 60409FCC0EF7D03534F4A1F5
Accept-Ranges: bytes
ETag: "C6C67E44834D62DC1FC9729C94E74CCB"
Last-Modified: Wed, 03 Mar 2021 18:04:44 GMT
x-oss-object-type: Normal
x-oss-hash-crc64ecma: 18170594484795111235
x-oss-storage-class: Standard
Content-Disposition: attachment
x-oss-force-download: true
Content-MD5: xsZ+RINNYtwfyXKclOdMyw==
x-oss-server-time: 27
#The console does not support viewing response bodies with media files.

有很多现在不需要关注的内容,只需要关心下表中的内容

内容 备注
HTTP/1.1 200 OK 状态行,由协议版本、状态码与原因短语组成
Content-Type: image/jpeg 实体的媒体类型
Content-Length: 2544 实体的长度,此处即为JPG图片的大小

有了状态行,我们就可以知道此次请求是否成功;有了Content-Length,我们就知道目标图片的尺寸。
后续操作就是先解析报文,然后根据请求行判断请求是否成功,如果成功,就继续解析应答首部,获取图片的尺寸,如果不成功,那就慢慢Debug吧。
一切就绪后,那可以开始读取剩下的内容吧。
由于传输过程中可能无法一次性将实体传输过来,所以实体会被分割成很多小块发送,使用extractBodyChunkFromResponseRaw提取实体并返回提取到的长度。
前面*client->onData([&] (void *arg, AsyncClient *c, void *data, size_t len)会被调用很多次。
onData中的data
*就是每一数据帧去掉帧头和帧尾的部分,即为实体被分成的小块。
由于是通过TCP传输的,所以这个大小存在上限,采用Arduino方式开发ESP32的话,MSS的值会被设置成1436。
很荣幸的能够观察到这个现象,感觉摸到MTU了

代码时间

请求部分代码

关于网络请求操作的代码,参阅ESP32异步网络请求,本文只关注应答报文处理函数

main.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
#include <Arduino.h>
#include <AsyncTCP.h>
#include <TFT_eSPI.h>
#include <TJpg_Decoder.h>
#include <WiFi.h>
#include <Wire.h>

#define ssid "XXXX"
#define password "XXXX"
// 显示sprite的内容
TFT_eSPI tft;
// 图片绘制的Sprite
TFT_eSprite imageSprite = TFT_eSprite(&tft);
// 应答报文的Sprite
TFT_eSprite headerSprite = TFT_eSprite(&tft);

WiFiClient client;

// 待显示图片尺寸
uint16_t imageSize;
// 已经下载的实体尺寸
uint16_t readBytesSize = -1;
// 图片缓存, 限制大小为16KB,你也可以设置成其他尺寸。
uint8_t *imageBuffer = (uint8_t *)heap_caps_malloc(16 * 1024, MALLOC_CAP_8BIT);
// 两张sprite的偏移
int16_t headerSpriteOffset = 0;
String header;
enum ParseState {
PARSE_REQ_START,
PARSE_REQ_HEADERS,
PARSE_REQ_BODY,
PARSE_REQ_END,
PARSE_REQ_FAIL,
PARSE_REQ_PENDING
};

ParseState _parseState;
/**
* @brief 从初次原始报文中提取Content-Length
* @param responseBody 原始报文
* @param bodyBeginIndex body起始索引
* @return 无
**/
uint16_t extractContentLengthFromResponseRaw(String &responseBody,
uint16_t &bodyBeginIndex) {
int index = responseBody.indexOf("Content-Length:");
int eIndex = responseBody.indexOf('\n', index);
String value = responseBody.substring(index + 16, eIndex);
bodyBeginIndex = responseBody.indexOf("\r\n\r\n") + 4;
return atoi(value.c_str());
}

/**
* @brief 深度复制bytes
* @param buffer 目标的容器
* @param source 原始内容
* @param length 数据长度
* @param SrcOffset 原始内容的读取偏移
* @param bufferOffset 目标的容器的写入偏移
* @return 处理的数据长度
**/
size_t copyBytes(uint8_t *source, uint8_t *buffer, size_t length,
size_t SrcOffset, size_t bufferOffset) {
size_t count = 0;
while (count + SrcOffset < length) {
*(buffer + count + bufferOffset) = source[count + SrcOffset];
count++;
}
return count;
}

/**
* @brief 从初次原始报文中提取Content-Length
* @param frameData 原始报文
* @param bodyData body数据容器
* @param length body数据长度
* @param offset body数据起始索引
* @return 处理的body长度
**/
uint16_t extractBodyChunkFromResponseRaw(uint8_t *frameData, uint8_t *bodyData,
uint16_t length, uint16_t srcOffset,
size_t bufferOffset) {
copyBytes(frameData, bodyData, length, srcOffset, bufferOffset);
return length - srcOffset;
}

void asyncReqeust() {
static AsyncClient *aClient;
if (aClient) {
// aClient already exists
// aClient 繁忙
return;
}
aClient = new AsyncClient();
if (!aClient) {
// could not allocate aClient
// aClient 无法创建
return;
}
aClient->onError(
[&](void *arg, AsyncClient *client, int error) {
Serial.println("getImageAsync:Connect Error");
aClient = NULL;
delete client;
},
NULL);
// 请求的回调
aClient->onConnect(
[&](void *arg, AsyncClient *client) {
Serial.println("getImageAsync:Connected");
aClient->onError(NULL, NULL);
// 连接断开的动作
client->onDisconnect(
[&](void *arg, AsyncClient *c) {
// status = 3;
aClient = NULL;
delete c;
Serial.printf("getImageAsync:Disconnected\n");
_parseState = PARSE_REQ_START;
},
NULL);
// 接收到服务器数据时候的动作
client->onData(
[&](void *arg, AsyncClient *c, void *data, size_t len) {
uint8_t *d = (uint8_t *)data;
// _parseState为解析状态枚举类型
if (_parseState == PARSE_REQ_START) {
String content = String((char *)data);
_parseState = PARSE_REQ_HEADERS;
uint16_t bodyBeginIndex = 0;
// 从响应头中获取长度
imageSize = extractContentLengthFromResponseRaw(content,
bodyBeginIndex);
Serial.printf("Content-Length:%d\n", imageSize);
header = content.substring(0, bodyBeginIndex);
if (imageSize > 0) {
_parseState = PARSE_REQ_BODY;
readBytesSize = extractBodyChunkFromResponseRaw(
d, imageBuffer, len, bodyBeginIndex, 0);
}

} else if (_parseState == PARSE_REQ_BODY) {
readBytesSize += extractBodyChunkFromResponseRaw(
d, imageBuffer, len, 0, readBytesSize);
Serial.printf("extract size:%d\n", readBytesSize);
}
if (readBytesSize == imageSize) {
// 接收完成
Serial.printf("Body Received Done.received %d bytes\n",
readBytesSize);
// for (int i = 0; i < imageSize; i++) {
// Serial.write(*buffer + i);
// }
// Serial.println();
_parseState = PARSE_REQ_END;
}
},
NULL);
// 发送请求
client->write(
"GET /response.jpg HTTP/1.1\r\n"
"Host: chaosgoo-pic.oss-cn-shanghai.aliyuncs.com\r\n"
"Accept: */*\r\n"
"Accept-Encoding: gzip, deflate, br"
"Connection: keep-alive"
"User-Agent: PostmanRuntime/7.26.10\r\n\r\n");
},
NULL);
if (!aClient->connect("chaosgoo-pic.oss-cn-shanghai.aliyuncs.com", 80)) {
Serial.println("Connect Fail");
AsyncClient *client = aClient;
aClient = NULL;
delete client;
}
}

// JPG解码绘制回调
bool drawCallback(int16_t x, int16_t y, uint16_t w, uint16_t h,
uint16_t *bitmap) {
if (imageSize == readBytesSize) {
if (y >= 135) {
return true;
}
imageSprite.pushImage(x, y, w, h, bitmap);
}
return true;
}

// 推送到屏幕
void pushToTFT(int offset) {
headerSprite.pushSprite(0, offset);
imageSprite.pushSprite(0, offset + 135);
}

void setup() {
Serial.begin(115200);
client.setTimeout(1);
WiFi.begin(ssid, password);
WiFi.setAutoReconnect(true);
// 初始化屏幕
tft.init();
// 旋转屏幕方向,从竖变为横向
tft.setRotation(1);
// 清空屏幕内容
tft.fillScreen(TFT_BLACK);
// 创建Sprite
imageSprite.createSprite(240, 32);
headerSprite.createSprite(240, 135);
// 配置TJpeg
TJpgDec.setJpgScale(1);
TJpgDec.setSwapBytes(true);
// 设置TJpg解码器回调函数
TJpgDec.setCallback(drawCallback);
delay(3000);
asyncReqeust();
}

void loop() {
if (_parseState == PARSE_REQ_END) {
TJpgDec.drawJpg(0, 0, imageBuffer, imageSize);
_parseState = PARSE_REQ_PENDING;
headerSprite.setTextFont(1);
headerSprite.setTextColor(TFT_WHITE);
for (int i = 0; i < header.length(); i++) {
headerSprite.print(header[i]);
Serial.print(header[i]);
headerSprite.pushSprite(0, 0);
}
}
pushToTFT(headerSpriteOffset);
delay(100);
if (headerSpriteOffset > -32) {
headerSpriteOffset -= headerSprite.fontHeight(1);
}
}
platformio.ini
1
2
3
4
5
6
7
8
9
10
11
12
[env:pico32]
platform = espressif32
board = pico32
framework = arduino
monitor_speed = 115200
lib_deps =
adafruit/Adafruit BusIO@^1.6.0
adafruit/Adafruit GFX Library@1.10.3
bodmer/TFT_eSPI@^2.3.41
ottowinter/AsyncTCP-esphome@^1.1.1
ottowinter/ESPAsyncWebServer-esphome@^1.2.7
bodmer/TJpg_Decoder@^0.1.0

参考资料