Files
Arduino/WeatherPredictor/web_server.cpp
T
oskarvitaliiandClaude Opus 4.8 4b59b8936b fix: stream large API responses to stop heap-exhaustion truncation
Serving /api/history (and /api/forecasts) built a ~12 KB String +
JsonDocument every 15 s; over days this fragmented the ESP8266 heap until
the TCP stack could no longer send a full response, causing the web page
to fail with ERR_CONTENT_LENGTH_MISMATCH. Stream both array endpoints via
chunked transfer (small stack buffer, no big allocations). Add free-heap
to /api/current and Serial, plus a low-memory safety restart (history is
persisted first).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 06:57:09 +07:00

106 lines
3.5 KiB
C++

#include <ESP8266WebServer.h>
#include <ArduinoJson.h>
#include "state.h"
#include "app_settings.h"
#include "history.h"
#include "flog.h"
#include "forecast.h"
#include "web_page.h"
static ESP8266WebServer server(80);
static void handleRoot() {
server.send_P(200, "text/html", INDEX_HTML);
}
static void handleCurrent() {
JsonDocument doc;
char t[24];
snprintf(t, sizeof(t), "%04u-%02u-%02u %02u:%02u:%02u",
g_state.now.year, g_state.now.month, g_state.now.day,
g_state.now.hour, g_state.now.minute, g_state.now.second);
doc["time"] = t;
doc["abs"] = g_state.absHpa;
doc["msl"] = g_state.mslHpa;
doc["temp"] = g_state.tempC;
doc["trend"] = (int)g_state.trend;
doc["haveTrend"] = g_state.haveTrend;
doc["forecast"] = g_state.haveTrend ? g_state.forecast.text : "Collecting data...";
doc["category"] = g_state.haveTrend ? categoryShort(g_state.forecast.category) : "...";
doc["heap"] = ESP.getFreeHeap(); // for monitoring memory health
String out;
serializeJson(doc, out);
server.send(200, "application/json", out);
}
// Streamed (chunked) so we never build a large String/JsonDocument on the heap.
static void handleHistory() {
server.setContentLength(CONTENT_LENGTH_UNKNOWN);
server.send(200, "application/json", "");
server.sendContent("[");
char buf[80];
int n = historyCount();
for (int i = 0; i < n; i++) {
Sample s = historyGet(i);
snprintf(buf, sizeof(buf), "%s{\"t\":%lu,\"msl\":%.1f,\"temp\":%.1f}",
i ? "," : "", (unsigned long)s.epoch, s.mslHpa, s.tempC);
server.sendContent(buf);
}
server.sendContent("]");
}
// Streamed (chunked). Forecast phrases contain no quotes/backslashes, so they
// are safe to embed directly in JSON.
static void handleForecasts() {
server.setContentLength(CONTENT_LENGTH_UNKNOWN);
server.send(200, "application/json", "");
server.sendContent("[");
char buf[128];
int n = flogCount();
for (int i = n - 1, first = 1; i >= 0; i--, first = 0) { // newest first
FcastEntry e = flogGet(i);
snprintf(buf, sizeof(buf), "%s{\"t\":%lu,\"cat\":\"%s\",\"text\":\"%s\"}",
first ? "" : ",", (unsigned long)e.epoch,
categoryShort((WxCategory)e.cat), forecastTextForLetter(e.letter));
server.sendContent(buf);
}
server.sendContent("]");
}
static void handleGetSettings() {
JsonDocument doc;
doc["altitude"] = settings.altitudeM;
doc["tz"] = settings.tzOffsetMin;
doc["lat"] = settings.lat;
doc["lon"] = settings.lon;
String out;
serializeJson(doc, out);
server.send(200, "application/json", out);
}
static void handlePostSettings() {
JsonDocument doc;
if (deserializeJson(doc, server.arg("plain"))) {
server.send(400, "application/json", "{\"ok\":false,\"err\":\"bad json\"}");
return;
}
settings.altitudeM = doc["altitude"] | settings.altitudeM;
settings.tzOffsetMin = doc["tz"] | settings.tzOffsetMin;
settings.lat = doc["lat"] | settings.lat;
settings.lon = doc["lon"] | settings.lon;
settingsSave();
server.send(200, "application/json", "{\"ok\":true}");
}
void webBegin() {
server.on("/", handleRoot);
server.on("/api/current", handleCurrent);
server.on("/api/history", handleHistory);
server.on("/api/forecasts", handleForecasts);
server.on("/api/settings", HTTP_GET, handleGetSettings);
server.on("/api/settings", HTTP_POST, handlePostSettings);
server.begin();
}
void webLoop() { server.handleClient(); }