-
Notifications
You must be signed in to change notification settings - Fork 7.6k
/
Copy pathWebServer.ino
376 lines (304 loc) · 11.7 KB
/
WebServer.ino
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
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
// @file WebServer.ino
// @brief Example WebServer implementation using the ESP32 WebServer
// and most common use cases related to web servers.
//
// * Setup a web server
// * redirect when accessing the url with servername only
// * get real time by using builtin NTP functionality
// * send HTML responses from Sketch (see builtinfiles.h)
// * use a LittleFS file system on the data partition for static files
// * use http ETag Header for client side caching of static files
// * use custom ETag calculation for static files
// * extended FileServerHandler for uploading and deleting static files
// * extended FileServerHandler for uploading and deleting static files
// * serve APIs using REST services (/api/list, /api/sysinfo)
// * define HTML response when no file/api/handler was found
//
// See also README.md for instructions and hints.
//
// Please use the following Arduino IDE configuration
//
// * Board: "ESP32 Dev Module" or other board with ESP32
// * Partition Scheme: "Default 4MB with spiffs" or any other scheme with spiffs or FAT
// but LittleFS will be used in the partition (not SPIFFS)
// * other setting as applicable
//
// Changelog:
// 21.07.2021 creation, first version
// 08.01.2023 ESP32 version with ETag
// 02.08.2024 support LitteFS and FAT file systems (on large Flash chips)
#include <Arduino.h>
#include <WebServer.h>
#include <WiFi.h>
#include "secrets.h" // add WLAN Credentials in here.
#include "esp_partition.h" // to check existing data partitions in Flash memory
#include <FS.h> // File System for Web Server Files
#include <LittleFS.h> // Use LittleFSThis file system is used.
#include <FFat.h> // or.. FAT
// mark parameters not used in example
#define UNUSED __attribute__((unused))
// TRACE output simplified, can be deactivated here
#define TRACE(...) Serial.printf(__VA_ARGS__)
// name of the server. You reach it using http://webserver
#define HOSTNAME "webserver"
// local time zone definition (Berlin)
#define TIMEZONE "CET-1CEST,M3.5.0,M10.5.0/3"
// need a WebServer for http access on port 80.
WebServer server(80);
// The file system in use...
fs::FS *fsys = nullptr;
// The text of builtin files are in this header file
#include "builtinfiles.h"
// enable the CUSTOM_ETAG_CALC to enable calculation of ETags by a custom function
#define CUSTOM_ETAG_CALC
// ===== Simple functions used to answer simple GET requests =====
// This function is called when the WebServer was requested without giving a filename.
// This will redirect to the file index.htm when it is existing otherwise to the built-in $upload.htm page
void handleRedirect() {
TRACE("Redirect...\n");
String url = "/index.htm";
if (!fsys->exists(url)) {
url = "/$upload.htm";
}
server.sendHeader("Location", url, true);
server.send(302);
} // handleRedirect()
// This function is called when the WebServer was requested to list all existing files in the filesystem.
// a JSON array with file information is returned.
void handleListFiles() {
File dir = fsys->open("/", "r");
String result;
result += "[\n";
while (File entry = dir.openNextFile()) {
if (result.length() > 4) {
result += ",\n";
}
result += " {";
result += "\"type\": \"file\", ";
result += "\"name\": \"" + String(entry.name()) + "\", ";
result += "\"size\": " + String(entry.size()) + ", ";
result += "\"time\": " + String(entry.getLastWrite());
result += "}";
} // while
result += "\n]";
server.sendHeader("Cache-Control", "no-cache");
server.send(200, "text/javascript; charset=utf-8", result);
} // handleListFiles()
// This function is called when the sysInfo service was requested.
void handleSysInfo() {
String result;
result += "{\n";
result += " \"Chip Model\": " + String(ESP.getChipModel()) + ",\n";
result += " \"Chip Cores\": " + String(ESP.getChipCores()) + ",\n";
result += " \"Chip Revision\": " + String(ESP.getChipRevision()) + ",\n";
result += " \"flashSize\": " + String(ESP.getFlashChipSize()) + ",\n";
result += " \"freeHeap\": " + String(ESP.getFreeHeap()) + ",\n";
if (fsys == (fs::FS *)&FFat) {
result += " \"fsTotalBytes\": " + String(FFat.totalBytes()) + ",\n";
result += " \"fsUsedBytes\": " + String(FFat.usedBytes()) + ",\n";
} else {
result += " \"fsTotalBytes\": " + String(LittleFS.totalBytes()) + ",\n";
result += " \"fsUsedBytes\": " + String(LittleFS.usedBytes()) + ",\n";
}
result += "}";
server.sendHeader("Cache-Control", "no-cache");
server.send(200, "text/javascript; charset=utf-8", result);
} // handleSysInfo()
// ===== Request Handler class used to answer more complex requests =====
// The FileServerHandler is registered to the web server to support DELETE and UPLOAD of files into the filesystem.
class FileServerHandler : public RequestHandler {
public:
// @brief Construct a new File Server Handler object
// @param fs The file system to be used.
// @param path Path to the root folder in the file system that is used for serving static data down and upload.
// @param cache_header Cache Header to be used in replies.
FileServerHandler() {
TRACE("FileServerHandler is registered\n");
}
// @brief check incoming request. Can handle POST for uploads and DELETE.
// @param requestMethod method of the http request line.
// @param requestUri request resource from the http request line.
// @return true when method can be handled.
bool canHandle(WebServer &server, HTTPMethod requestMethod, const String &uri) override {
return ((requestMethod == HTTP_POST) || (requestMethod == HTTP_DELETE));
} // canHandle()
bool canUpload(WebServer &server, const String &uri) override {
// only allow upload on root fs level.
return (uri == "/");
} // canUpload()
bool handle(WebServer &server, HTTPMethod requestMethod, const String &requestUri) override {
// ensure that filename starts with '/'
String fName = requestUri;
if (!fName.startsWith("/")) {
fName = "/" + fName;
}
if (requestMethod == HTTP_POST) {
// all done in upload. no other forms.
} else if (requestMethod == HTTP_DELETE) {
if (fsys->exists(fName)) {
TRACE("DELETE %s\n", fName.c_str());
fsys->remove(fName);
}
} // if
server.send(200); // all done.
return (true);
} // handle()
// uploading process
void upload(WebServer UNUSED &server, const String &requestUri, HTTPUpload &upload) override {
// ensure that filename starts with '/'
static size_t uploadSize;
if (upload.status == UPLOAD_FILE_START) {
String fName = upload.filename;
// Open the file for writing
if (!fName.startsWith("/")) {
fName = "/" + fName;
}
TRACE("start uploading file %s...\n", fName.c_str());
if (fsys->exists(fName)) {
fsys->remove(fName);
} // if
_fsUploadFile = fsys->open(fName, "w");
uploadSize = 0;
} else if (upload.status == UPLOAD_FILE_WRITE) {
// Write received bytes
if (_fsUploadFile) {
size_t written = _fsUploadFile.write(upload.buf, upload.currentSize);
if (written < upload.currentSize) {
// upload failed
TRACE(" write error!\n");
_fsUploadFile.close();
// delete file to free up space in filesystem
String fName = upload.filename;
if (!fName.startsWith("/")) {
fName = "/" + fName;
}
fsys->remove(fName);
}
uploadSize += upload.currentSize;
// TRACE("free:: %d of %d\n", LittleFS.usedBytes(), LittleFS.totalBytes());
// TRACE("written:: %d of %d\n", written, upload.currentSize);
// TRACE("totalSize: %d\n", upload.currentSize + upload.totalSize);
} // if
} else if (upload.status == UPLOAD_FILE_END) {
TRACE("upload done.\n");
// Close the file
if (_fsUploadFile) {
_fsUploadFile.close();
TRACE(" %d bytes uploaded.\n", upload.totalSize);
}
} // if
} // upload()
protected:
File _fsUploadFile;
};
// Setup everything to make the webserver work.
void setup(void) {
delay(3000); // wait for serial monitor to start completely.
// Use Serial port for some trace information from the example
Serial.begin(115200);
Serial.setDebugOutput(false);
TRACE("Starting WebServer example...\n");
// ----- check partitions for finding the filesystem type -----
esp_partition_iterator_t i;
i = esp_partition_find(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_FAT, nullptr);
if (i) {
TRACE("FAT partition found.");
fsys = &FFat;
} else {
i = esp_partition_find(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_SPIFFS, nullptr);
if (i) {
TRACE("SPIFFS partition found.");
fsys = &LittleFS;
} else {
TRACE("no partition found.");
}
}
esp_partition_iterator_release(i);
// mount and format as needed
TRACE("Mounting the filesystem...\n");
if (!fsys) {
TRACE("need to change the board configuration to include a partition for files.\n");
delay(30000);
} else if ((fsys == (fs::FS *)&FFat) && (!FFat.begin())) {
TRACE("could not mount the filesystem...\n");
delay(2000);
TRACE("formatting FAT...\n");
FFat.format();
delay(2000);
TRACE("restarting...\n");
delay(2000);
ESP.restart();
} else if ((fsys == (fs::FS *)&LittleFS) && (!LittleFS.begin())) {
TRACE("could not mount the filesystem...\n");
delay(2000);
TRACE("formatting LittleFS...\n");
LittleFS.format();
delay(2000);
TRACE("restarting...\n");
delay(2000);
ESP.restart();
}
// allow to address the device by the given name e.g. http://webserver
WiFi.setHostname(HOSTNAME);
// start WiFI
WiFi.mode(WIFI_STA);
if (strlen(ssid) == 0) {
WiFi.begin();
} else {
WiFi.begin(ssid, passPhrase);
}
TRACE("Connect to WiFi...\n");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
TRACE(".");
}
TRACE("connected.\n");
// Ask for the current time using NTP request builtin into ESP firmware.
TRACE("Setup ntp...\n");
configTzTime(TIMEZONE, "pool.ntp.org");
TRACE("Register redirect...\n");
// register a redirect handler when only domain name is given.
server.on("/", HTTP_GET, handleRedirect);
TRACE("Register service handlers...\n");
// serve a built-in htm page
server.on("/$upload.htm", []() {
server.send(200, "text/html", FPSTR(uploadContent));
});
// register some REST services
server.on("/api/list", HTTP_GET, handleListFiles);
server.on("/api/sysinfo", HTTP_GET, handleSysInfo);
TRACE("Register file system handlers...\n");
// UPLOAD and DELETE of files in the file system using a request handler.
server.addHandler(new FileServerHandler());
// enable CORS header in webserver results
server.enableCORS(true);
// enable ETAG header in webserver results (used by serveStatic handler)
#if defined(CUSTOM_ETAG_CALC)
// This is a fast custom eTag generator. It returns a value based on the time the file was updated like
// ETag: 63bbceb5
server.enableETag(true, [](FS &fs, const String &path) -> String {
File f = fs.open(path, "r");
String eTag = String(f.getLastWrite(), 16); // use file modification timestamp to create ETag
f.close();
return (eTag);
});
#else
// enable standard ETAG calculation using md5 checksum of file content.
server.enableETag(true);
#endif
// serve all static files
server.serveStatic("/", *fsys, "/");
TRACE("Register default (not found) answer...\n");
// handle cases when file is not found
server.onNotFound([]() {
// standard not found in browser.
server.send(404, "text/html", FPSTR(notFoundContent));
});
server.begin();
TRACE("open <http://%s> or <http://%s>\n", WiFi.getHostname(), WiFi.localIP().toString().c_str());
} // setup
// run the server...
void loop(void) {
server.handleClient();
} // loop()
// end.