DeterministicESPAsyncWebServer 1.2.0
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
advanced.ino
Go to the documentation of this file.
1// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file advanced.ino
6 * @brief Advanced example demonstrating RESTful CRUD APIs, header verification, and zero-allocation JSON parsing.
7 *
8 * This example showcases:
9 * 1. RESTful CRUD endpoints (GET, POST, PATCH, DELETE) for a mock sensor database.
10 * 2. Strict HTTP request validation:
11 * - Authorization headers (bearer token) verified before modifications.
12 * - Content-Type header checked for incoming POST/PATCH requests.
13 * 3. Extraction of wildcard ID parameters from routes (e.g., /api/sensors/2).
14 * 4. Zero-heap manual JSON scanning for incoming payloads.
15 * 5. Response diversity using different HTTP status codes:
16 * - 200 OK
17 * - 201 Created
18 * - 204 No Content
19 * - 400 Bad Request
20 * - 401 Unauthorized
21 * - 404 Not Found
22 * 6. Query string filtering based on parameter values.
23 *
24 * To run this example:
25 * - Configure SSID/PASSWORD, and upload to an ESP32.
26 * - Use a REST client (like Postman or curl) to interact with /api/sensors.
27 */
28
29#include "DeterministicESPAsyncWebServer.h"
30#include "network_drivers/physical.h"
31#include <WiFi.h>
32
33static const char *SSID = "YOUR_SSID";
34static const char *PASSWORD = "YOUR_PASSWORD";
35
36DetWebServer server;
37
38// Security token required for write/delete actions
39static const char *EXPECTED_TOKEN = "Bearer secret_admin_token";
40
41// Zero-allocation static "database" model
42struct SensorDevice
43{
44 int id;
45 char name[16];
46 float temperature;
47 bool active;
48 bool in_use; // Slot flag
49};
50
51#define MAX_SENSORS 4
52static SensorDevice sensor_db[MAX_SENSORS] = {
53 {0, "Living Room", 22.4, true, true},
54 {1, "Kitchen", 24.1, true, true},
55 {2, "Garage", 15.8, false, true},
56 {3, "", 0.0, false, false} // Empty slot
57};
58
59// --- Helper Functions ---
60
61/**
62 * @brief Checks if a request has a valid Authorization header.
63 * @return true if authenticated, false otherwise.
64 */
65bool is_authorized(const HttpReq *req)
66{
67 const char *auth_hdr = http_get_header(req, "Authorization");
68 if (auth_hdr && strcmp(auth_hdr, EXPECTED_TOKEN) == 0)
69 {
70 return true;
71 }
72 return false;
73}
74
75/**
76 * @brief Custom helper to scan float values out of a simple JSON body.
77 * Works without heap allocation by scanning the buffer.
78 */
79bool json_get_float(const char *json, const char *key, float &out_val)
80{
81 // Search for key pattern, e.g., "temp":
82 char key_pattern[32];
83 snprintf(key_pattern, sizeof(key_pattern), "\"%s\":", key);
84
85 const char *ptr = strstr(json, key_pattern);
86 if (!ptr)
87 return false;
88
89 // Move pointer past key
90 ptr += strlen(key_pattern);
91
92 // Skip spaces
93 while (*ptr == ' ' || *ptr == '\t')
94 ptr++;
95
96 out_val = (float)atof(ptr);
97 return true;
98}
99
100/**
101 * @brief Custom helper to scan string values out of a simple JSON body.
102 */
103bool json_get_string(const char *json, const char *key, char *out_buf, size_t max_len)
104{
105 char key_pattern[32];
106 snprintf(key_pattern, sizeof(key_pattern), "\"%s\":", key);
107
108 const char *ptr = strstr(json, key_pattern);
109 if (!ptr)
110 return false;
111
112 ptr += strlen(key_pattern);
113
114 // Skip spaces or quotes
115 while (*ptr == ' ' || *ptr == '\t' || *ptr == '"')
116 ptr++;
117
118 size_t i = 0;
119 while (*ptr && *ptr != '"' && *ptr != ',' && *ptr != '}' && i < (max_len - 1))
120 {
121 out_buf[i++] = *ptr++;
122 }
123 out_buf[i] = '\0';
124 return true;
125}
126
127/**
128 * @brief Custom helper to scan boolean values out of a simple JSON body.
129 */
130bool json_get_bool(const char *json, const char *key, bool &out_val)
131{
132 char key_pattern[32];
133 snprintf(key_pattern, sizeof(key_pattern), "\"%s\":", key);
134
135 const char *ptr = strstr(json, key_pattern);
136 if (!ptr)
137 return false;
138
139 ptr += strlen(key_pattern);
140 while (*ptr == ' ' || *ptr == '\t')
141 ptr++;
142
143 if (strncmp(ptr, "true", 4) == 0)
144 {
145 out_val = true;
146 return true;
147 }
148 else if (strncmp(ptr, "false", 5) == 0)
149 {
150 out_val = false;
151 return true;
152 }
153 return false;
154}
155
156// --- Route Handlers ---
157
158/**
159 * @brief GET /api/sensors
160 * Serves list of sensors. Supports query filter: ?active=1 or ?active=0
161 */
162void handle_get_sensors(uint8_t slot_id, HttpReq *req)
163{
164 const char *active_filter = http_get_query(req, "active");
165 bool filter_by_active = (active_filter != nullptr);
166 bool active_target_val = (filter_by_active && strcmp(active_filter, "1") == 0);
167
168 // Build the JSON list response. We construct this incrementally on stack.
169 char response_buf[512];
170 int len = snprintf(response_buf, sizeof(response_buf), "[");
171
172 bool first = true;
173 for (int i = 0; i < MAX_SENSORS; i++)
174 {
175 if (!sensor_db[i].in_use)
176 continue;
177 if (filter_by_active && (sensor_db[i].active != active_target_val))
178 continue;
179
180 if (!first)
181 {
182 len += snprintf(response_buf + len, sizeof(response_buf) - len, ",");
183 }
184 first = false;
185
186 len += snprintf(response_buf + len, sizeof(response_buf) - len,
187 "{\"id\":%d,\"name\":\"%s\",\"temp\":%.1f,\"active\":%s}", sensor_db[i].id, sensor_db[i].name,
188 sensor_db[i].temperature, sensor_db[i].active ? "true" : "false");
189 }
190 snprintf(response_buf + len, sizeof(response_buf) - len, "]");
191
192 server.send(slot_id, 200, "application/json", response_buf);
193}
194
195/**
196 * @brief GET /api/sensors/* (wildcard match)
197 * Extracts sensor ID from the path prefix /api/sensors/
198 */
199void handle_get_sensor_by_id(uint8_t slot_id, HttpReq *req)
200{
201 // Length of "/api/sensors/" is 13
202 if (strlen(req->path) <= 13)
203 {
204 server.send(slot_id, 400, "text/plain", "Missing sensor ID");
205 return;
206 }
207
208 int id = atoi(req->path + 13);
209 if (id < 0 || id >= MAX_SENSORS || !sensor_db[id].in_use)
210 {
211 server.send(slot_id, 404, "application/json", "{\"error\":\"Sensor not found\"}");
212 return;
213 }
214
215 char response_buf[192];
216 snprintf(response_buf, sizeof(response_buf), "{\"id\":%d,\"name\":\"%s\",\"temp\":%.1f,\"active\":%s}",
217 sensor_db[id].id, sensor_db[id].name, sensor_db[id].temperature, sensor_db[id].active ? "true" : "false");
218
219 server.send(slot_id, 200, "application/json", response_buf);
220}
221
222/**
223 * @brief POST /api/sensors
224 * Creates a new sensor. Requires authentication and JSON payload.
225 */
226void handle_create_sensor(uint8_t slot_id, HttpReq *req)
227{
228 // 1. Verify Authorization
229 if (!is_authorized(req))
230 {
231 server.send(slot_id, 401, "text/plain", "401 Unauthorized: Invalid token");
232 return;
233 }
234
235 // 2. Verify Content-Type
236 const char *content_type = http_get_header(req, "Content-Type");
237 if (!content_type || strstr(content_type, "application/json") == nullptr)
238 {
239 server.send(slot_id, 400, "text/plain", "400 Bad Request: Content-Type must be application/json");
240 return;
241 }
242
243 // Find an empty slot
244 int empty_slot = -1;
245 for (int i = 0; i < MAX_SENSORS; i++)
246 {
247 if (!sensor_db[i].in_use)
248 {
249 empty_slot = i;
250 break;
251 }
252 }
253
254 if (empty_slot == -1)
255 {
256 server.send(slot_id, 409, "application/json", "{\"error\":\"Database table full\"}");
257 return;
258 }
259
260 // 3. Extract JSON keys
261 const char *body = (const char *)req->body;
262 char name[16] = "";
263 float temp = 0.0;
264 bool active = false;
265
266 if (!json_get_string(body, "name", name, sizeof(name)) || !json_get_float(body, "temp", temp) ||
267 !json_get_bool(body, "active", active))
268 {
269 server.send(slot_id, 400, "application/json", "{\"error\":\"Invalid JSON format or missing keys\"}");
270 return;
271 }
272
273 // Save item
274 sensor_db[empty_slot].id = empty_slot;
275 strncpy(sensor_db[empty_slot].name, name, sizeof(sensor_db[empty_slot].name) - 1);
276 sensor_db[empty_slot].temperature = temp;
277 sensor_db[empty_slot].active = active;
278 sensor_db[empty_slot].in_use = true;
279
280 char response_buf[192];
281 snprintf(response_buf, sizeof(response_buf), "{\"id\":%d,\"name\":\"%s\",\"status\":\"created\"}", empty_slot,
282 sensor_db[empty_slot].name);
283
284 server.send(slot_id, 201, "application/json", response_buf);
285}
286
287/**
288 * @brief PATCH /api/sensors/*
289 * Partially updates a sensor's temperature or activity state. Requires authentication.
290 */
291void handle_patch_sensor(uint8_t slot_id, HttpReq *req)
292{
293 if (!is_authorized(req))
294 {
295 server.send(slot_id, 401, "text/plain", "401 Unauthorized: Invalid token");
296 return;
297 }
298
299 if (strlen(req->path) <= 13)
300 {
301 server.send(slot_id, 400, "text/plain", "Missing sensor ID");
302 return;
303 }
304
305 int id = atoi(req->path + 13);
306 if (id < 0 || id >= MAX_SENSORS || !sensor_db[id].in_use)
307 {
308 server.send(slot_id, 404, "application/json", "{\"error\":\"Sensor not found\"}");
309 return;
310 }
311
312 const char *body = (const char *)req->body;
313 float new_temp;
314 bool new_active;
315
316 if (json_get_float(body, "temp", new_temp))
317 {
318 sensor_db[id].temperature = new_temp;
319 }
320 if (json_get_bool(body, "active", new_active))
321 {
322 sensor_db[id].active = new_active;
323 }
324
325 char response_buf[192];
326 snprintf(response_buf, sizeof(response_buf), "{\"id\":%d,\"name\":\"%s\",\"temp\":%.1f,\"active\":%s}",
327 sensor_db[id].id, sensor_db[id].name, sensor_db[id].temperature, sensor_db[id].active ? "true" : "false");
328
329 server.send(slot_id, 200, "application/json", response_buf);
330}
331
332/**
333 * @brief DELETE /api/sensors/*
334 * Removes a sensor from the database. Requires authentication.
335 */
336void handle_delete_sensor(uint8_t slot_id, HttpReq *req)
337{
338 if (!is_authorized(req))
339 {
340 server.send(slot_id, 401, "text/plain", "401 Unauthorized: Invalid token");
341 return;
342 }
343
344 if (strlen(req->path) <= 13)
345 {
346 server.send(slot_id, 400, "text/plain", "Missing sensor ID");
347 return;
348 }
349
350 int id = atoi(req->path + 13);
351 if (id < 0 || id >= MAX_SENSORS || !sensor_db[id].in_use)
352 {
353 server.send(slot_id, 404, "application/json", "{\"error\":\"Sensor not found\"}");
354 return;
355 }
356
357 // Free slot in database
358 sensor_db[id].in_use = false;
359
360 // 204 status requires no response body
361 server.send_empty(slot_id, 204);
362}
363
364void setup()
365{
366 Serial.begin(115200);
367 delay(1000);
368 Serial.println("\n--- DetWebServer Advanced REST CRUD Example ---");
369
370 init_wifi_physical(SSID, PASSWORD);
371 while (!wifi_ready())
372 {
373 delay(500);
374 Serial.print(".");
375 }
376 Serial.println("\nWiFi Associated!");
377 Serial.print("Local IP: ");
378 Serial.println(WiFi.localIP());
379
380 server.set_cors("*");
381
382 // Map REST routes using methods
383 server.on("/api/sensors", HTTP_GET, handle_get_sensors);
384 server.on("/api/sensors/*", HTTP_GET, handle_get_sensor_by_id);
385 server.on("/api/sensors", HTTP_POST, handle_create_sensor);
386 server.on("/api/sensors/*", HTTP_PATCH, handle_patch_sensor);
387 server.on("/api/sensors/*", HTTP_DELETE, handle_delete_sensor);
388
389 if (server.begin(80))
390 {
391 Serial.println("REST API Server running on port 80");
392 Serial.println("Admin token expected in headers: 'Authorization: Bearer secret_admin_token'");
393 }
394}
395
396void loop()
397{
398 server.handle();
399}