Record each forecast change (Zambretti letter transition) with a timestamp into a 24-entry ring buffer, persisted to LittleFS and restored on boot. New /api/forecasts endpoint and a 'Forecast log' panel in the web UI (newest first, category colour, timestamp). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
104 lines
2.7 KiB
Arduino
104 lines
2.7 KiB
Arduino
#include <Arduino.h>
|
|
#include <Wire.h>
|
|
#include "config.h"
|
|
#include "sensors.h"
|
|
#include "rtc_time.h"
|
|
#include "display_ui.h"
|
|
#include "forecast.h"
|
|
#include "history.h"
|
|
#include "flog.h"
|
|
#include "app_settings.h"
|
|
#include "net.h"
|
|
#include "web_server.h"
|
|
#include "state.h"
|
|
|
|
AppState g_state;
|
|
|
|
static unsigned long tSample = 0;
|
|
static unsigned long tHistory = 0;
|
|
static unsigned long tFlush = 0;
|
|
|
|
static void sampleNow() {
|
|
g_state.now = rtcNow();
|
|
g_state.absHpa = readAbsPressureHpa();
|
|
g_state.mslHpa = toSeaLevelHpa(g_state.absHpa, settings.altitudeM);
|
|
g_state.tempC = readTemperatureC();
|
|
|
|
float d;
|
|
g_state.haveTrend = historyTrendDelta(d);
|
|
if (g_state.haveTrend) {
|
|
g_state.trend = classifyTrend(d);
|
|
g_state.forecast = computeForecast(g_state.mslHpa, g_state.trend, g_state.now.month);
|
|
flogConsider(rtcEpoch(), g_state.forecast); // log only when the forecast changes
|
|
} else {
|
|
g_state.trend = TREND_STEADY;
|
|
}
|
|
}
|
|
|
|
void setup() {
|
|
Serial.begin(115200);
|
|
delay(200);
|
|
Serial.println(F("\n=== Weather Predictor ==="));
|
|
Wire.begin(I2C_SDA, I2C_SCL);
|
|
|
|
displayBegin();
|
|
displaySplash("Weather", "Predictor");
|
|
|
|
if (!sensorsBegin()) Serial.println(F("BMP180 not found!"));
|
|
if (!rtcBegin()) Serial.println(F("DS3231 not found!"));
|
|
if (rtcLostPower()) {
|
|
Serial.println(F("RTC lost power -> temporary time set"));
|
|
rtcSet(RtcTime{2026, 7, 17, 12, 0, 0});
|
|
}
|
|
settingsBegin();
|
|
historyBegin();
|
|
historyLoad(); // restore prior samples (empty on first ever boot)
|
|
Serial.printf("history restored: %d samples\n", historyCount());
|
|
flogBegin();
|
|
flogLoad(); // restore forecast change log
|
|
Serial.printf("forecast log restored: %d entries\n", flogCount());
|
|
|
|
displaySplash("WiFi", "Connect / portal");
|
|
if (netBegin()) {
|
|
Serial.printf("WiFi connected: %s\n", netIP().c_str());
|
|
if (ntpSync(settings.tzOffsetMin))
|
|
Serial.println(F("RTC synced from NTP"));
|
|
else
|
|
Serial.println(F("NTP sync failed"));
|
|
} else {
|
|
Serial.println(F("WiFi not connected - running offline"));
|
|
}
|
|
|
|
// Seed first sample immediately.
|
|
sampleNow();
|
|
historyAdd(rtcEpoch(), g_state.mslHpa, g_state.tempC);
|
|
displayRender(g_state);
|
|
|
|
webBegin();
|
|
if (netConnected()) Serial.printf("Web UI: http://%s/\n", netIP().c_str());
|
|
|
|
tSample = tHistory = tFlush = millis();
|
|
}
|
|
|
|
void loop() {
|
|
webLoop();
|
|
unsigned long now = millis();
|
|
|
|
if (now - tSample >= SAMPLE_INTERVAL_MS) {
|
|
tSample = now;
|
|
sampleNow();
|
|
displayRender(g_state);
|
|
}
|
|
|
|
if (now - tHistory >= HISTORY_INTERVAL_MS) {
|
|
tHistory = now;
|
|
historyAdd(rtcEpoch(), g_state.mslHpa, g_state.tempC);
|
|
}
|
|
|
|
if (now - tFlush >= FLUSH_INTERVAL_MS) {
|
|
tFlush = now;
|
|
historySave();
|
|
Serial.printf("history flushed: %d samples\n", historyCount());
|
|
}
|
|
}
|