DeterministicESPAsyncWebServer 1.2.0
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
expert.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 expert.ino
6 * @brief Expert example demonstrating connection-pool inspection, request profiling, rate-limiting, and memory-safe
7 * stack templating.
8 *
9 * Demonstrates:
10 * 1. Direct low-level query of the Layer 4 connection pool (conn_pool[MAX_CONNS])
11 * to monitor TCP states, activity timestamps, and ring buffer fill-levels.
12 * 2. Token Bucket Rate Limiting: zero-allocation, time-based threshold checks
13 * returning "429 Too Many Requests".
14 * 3. Execution Profiling: measuring route execution times in microseconds.
15 * 4. Dynamic Stack-Allocated Response Templating: generating format-sensitive
16 * error pages (JSON vs. Plaintext) in the 404 handler based on request headers.
17 * 5. Real-time stack watermarking (uxTaskGetStackHighWaterMark) to audit stack headroom.
18 *
19 * To run this example:
20 * - Configure SSID/PASSWORD, and upload to an ESP32.
21 * - Trigger high-frequency requests to see the rate limiter (429 status) in action.
22 * - Monitor connection state diagnostics printed on the Serial interface.
23 */
24
25#include "DeterministicESPAsyncWebServer.h"
26#include "network_drivers/physical.h"
27#include "network_drivers/transport.h" // Needed to access conn_pool and ConnState
28#include <WiFi.h>
29
30static const char *SSID = "YOUR_SSID";
31static const char *PASSWORD = "YOUR_PASSWORD";
32
33DetWebServer server;
34
35// Uptime tracker and execution counters
36static unsigned long total_routed_requests = 0;
37static unsigned long total_rate_limited = 0;
38
39// --- Token Bucket Rate Limiter State ---
40static float bucket_tokens = 5.0f; // Max burst size
41static const float bucket_capacity = 5.0f;
42static const float refill_rate_per_sec = 2.0f; // Tokens added per second
43static unsigned long last_refill_time_ms = 0;
44
45/**
46 * @brief Zero-heap Token Bucket Rate Limiter
47 * Refills tokens based on time elapsed and consumes one token per request.
48 * @return true if allowed, false if rate-limited (bucket empty).
49 */
50bool acquire_rate_limit_token()
51{
52 unsigned long now = millis();
53 unsigned long elapsed_ms = now - last_refill_time_ms;
54 last_refill_time_ms = now;
55
56 // Refill bucket
57 bucket_tokens += (elapsed_ms / 1000.0f) * refill_rate_per_sec;
58 if (bucket_tokens > bucket_capacity)
59 {
60 bucket_tokens = bucket_capacity;
61 }
62
63 // Check if token is available
64 if (bucket_tokens >= 1.0f)
65 {
66 bucket_tokens -= 1.0f;
67 return true;
68 }
69 return false;
70}
71
72// --- Connection Pool Monitoring ---
73
74/**
75 * @brief Helper function to log current TCP connection pool statistics to Serial.
76 * Iterates through the static Layer 4 pool to analyze resource usage.
77 */
78void print_connection_pool_stats()
79{
80 Serial.println("\n--- Connection Pool Snapshot ---");
81 for (int i = 0; i < MAX_CONNS; i++)
82 {
83 TcpConn *conn = &conn_pool[i];
84 const char *state_str = "UNKNOWN";
85 switch (conn->state)
86 {
87 case CONN_FREE:
88 state_str = "FREE";
89 break;
90 case CONN_ACTIVE:
91 state_str = "ACTIVE";
92 break;
93 case CONN_CLOSING:
94 state_str = "CLOSING";
95 break;
96 }
97
98 size_t rx_unread = 0;
99 if (conn->state == CONN_ACTIVE)
100 {
101 // Calculate fill occupancy of the circular ring buffer
102 rx_unread = (conn->rx_head >= conn->rx_tail) ? (conn->rx_head - conn->rx_tail)
103 : (RX_BUF_SIZE - (conn->rx_tail - conn->rx_head));
104 }
105
106 Serial.printf("Slot [%d]: State=%-7s | UnreadBytes=%4zu | LastActivity=%6lu ms ago | PCB_Addr=%p\n", i,
107 state_str, rx_unread, (conn->state == CONN_ACTIVE) ? (millis() - conn->last_activity_ms) : 0,
108 conn->pcb);
109 }
110 Serial.println("---------------------------------");
111}
112
113// --- Route Handlers with microsecond profiling ---
114
115/**
116 * @brief GET /api/diagnostics
117 * Returns detailed telemetry. Profiler measures how fast this handler runs.
118 */
119void handle_diagnostics(uint8_t slot_id, HttpReq *req)
120{
121 unsigned long start_us = micros();
122 total_routed_requests++;
123
124 // Apply Rate Limiting first
125 if (!acquire_rate_limit_token())
126 {
127 total_rate_limited++;
128 server.send(slot_id, 429, "application/json", "{\"error\":\"Too Many Requests. Rate limit exceeded.\"}");
129 return;
130 }
131
132 // Audit local task stack headroom
133 UBaseType_t stack_high_water = uxTaskGetStackHighWaterMark(NULL);
134
135 char response_buf[384];
136 snprintf(response_buf, sizeof(response_buf),
137 "{"
138 "\"uptime_ms\":%lu,"
139 "\"routed_requests\":%lu,"
140 "\"rate_limited_count\":%lu,"
141 "\"free_heap_bytes\":%u,"
142 "\"task_stack_headroom_words\":%u,"
143 "\"bucket_tokens_left\":%.2f"
144 "}",
145 millis(), total_routed_requests, total_rate_limited, ESP.getFreeHeap(), (unsigned int)stack_high_water,
146 bucket_tokens);
147
148 unsigned long duration_us = micros() - start_us;
149
150 // We send the reply first.
151 server.send(slot_id, 200, "application/json", response_buf);
152
153 Serial.printf("[Profile] Route GET %s handled in %lu us\n", req->path, duration_us);
154}
155
156/**
157 * @brief GET /api/compute
158 * Demonstrates a heavier compute-bound route with timing checks.
159 */
160void handle_compute(uint8_t slot_id, HttpReq *req)
161{
162 unsigned long start_us = micros();
163 total_routed_requests++;
164
165 if (!acquire_rate_limit_token())
166 {
167 total_rate_limited++;
168 server.send(slot_id, 429, "application/json", "{\"error\":\"Too Many Requests\"}");
169 return;
170 }
171
172 // Perform a mock deterministic heavy calculation (e.g. integer math)
173 volatile uint32_t val = 12345;
174 for (int i = 0; i < 500; i++)
175 {
176 val = (val ^ 37821) * 31;
177 }
178
179 char response_buf[64];
180 snprintf(response_buf, sizeof(response_buf), "{\"result\":%u}", val);
181
182 server.send(slot_id, 200, "application/json", response_buf);
183
184 unsigned long duration_us = micros() - start_us;
185 Serial.printf("[Profile] Route GET %s (heavy compute) handled in %lu us\n", req->path, duration_us);
186}
187
188/**
189 * @brief Dynamic template fallback handler
190 * Chooses response type based on 'Accept' or 'Content-Type' headers.
191 */
192void handle_expert_not_found(uint8_t slot_id, HttpReq *req)
193{
194 unsigned long start_us = micros();
195 total_routed_requests++;
196
197 const char *accept_header = http_get_header(req, "Accept");
198 bool wants_json = (accept_header && strstr(accept_header, "application/json") != nullptr);
199
200 char error_buf[256];
201 if (wants_json)
202 {
203 snprintf(error_buf, sizeof(error_buf), "{\"error\":\"not_found\",\"requested_path\":\"%s\",\"uptime\":%lu}",
204 req->path, millis());
205 server.send(slot_id, 404, "application/json", error_buf);
206 }
207 else
208 {
209 snprintf(error_buf, sizeof(error_buf),
210 "--- Error 404 ---\nPath: %s\nUptime: %lu ms\nESP32 High-Reliability Node", req->path, millis());
211 server.send(slot_id, 404, "text/plain", error_buf);
212 }
213
214 unsigned long duration_us = micros() - start_us;
215 Serial.printf("[Profile] Fallback 404 matched in %lu us (JSON: %d)\n", duration_us, wants_json);
216}
217
218void setup()
219{
220 Serial.begin(115200);
221 delay(1000);
222 Serial.println("\n--- DetWebServer Expert Performance Example ---");
223
224 init_wifi_physical(SSID, PASSWORD);
225 while (!wifi_ready())
226 {
227 delay(500);
228 Serial.print(".");
229 }
230 Serial.println("\nWiFi online!");
231 Serial.print("Local IP Address: ");
232 Serial.println(WiFi.localIP());
233
234 last_refill_time_ms = millis();
235
236 // Map optimized endpoints
237 server.on("/api/diagnostics", HTTP_GET, handle_diagnostics);
238 server.on("/api/compute", HTTP_GET, handle_compute);
239 server.on_not_found(handle_expert_not_found);
240
241 if (server.begin(80))
242 {
243 Serial.println("Telemetry server started on port 80");
244 }
245}
246
247void loop()
248{
249 server.handle();
250
251 // Periodically display active pool diagnostics every 5 seconds
252 static unsigned long last_snapshot = 0;
253 if (millis() - last_snapshot >= 5000)
254 {
255 last_snapshot = millis();
256 print_connection_pool_stats();
257 }
258}