feat: Zambretti forecast with trend classification

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-17 19:59:17 +07:00
co-authored by Claude Opus 4.8
parent 2d52fcd851
commit 5819739fcb
3 changed files with 127 additions and 0 deletions
+12
View File
@@ -4,6 +4,7 @@
#include "display_ui.h"
#include "sensors.h"
#include "rtc_time.h"
#include "forecast.h"
static void i2cScan() {
Serial.println(F("I2C scan:"));
@@ -34,6 +35,17 @@ void setup() {
Serial.println(F("RTC lost power -> setting to build time"));
rtcSet(RtcTime{2026, 7, 17, 12, 0, 0}); // temporary; NTP will correct later
}
// --- forecast self-test (remove after Task 8) ---
struct { float p; Trend tr; } cases[] = {
{1030, TREND_STEADY}, {1030, TREND_RISING}, {1030, TREND_FALLING},
{1000, TREND_STEADY}, {1000, TREND_FALLING}, {970, TREND_FALLING},
};
for (auto& c : cases) {
Forecast f = computeForecast(c.p, c.tr, 7);
Serial.printf("p=%.0f trend=%d -> %c [%s] (%s)\n",
c.p, c.tr, f.letter, f.text, categoryShort(f.category));
}
}
void loop() {
+100
View File
@@ -0,0 +1,100 @@
#include <math.h>
#include "config.h"
#include "forecast.h"
// Full Zambretti phrases, index 0='A' .. 25='Z'.
static const char* const ZTEXT[26] = {
"Settled fine weather", // A
"Fine weather", // B
"Becoming fine", // C
"Fine, becoming less settled", // D
"Fine, possibly showers", // E
"Fairly fine, improving", // F
"Fairly fine, possibly showers early", // G
"Fairly fine, showers later", // H
"Showery early, improving", // I
"Changeable, improving", // J
"Fairly fine, showers likely", // K
"Rather unsettled, clearing later", // L
"Unsettled, probably improving", // M
"Showery, bright intervals", // N
"Showery, becoming unsettled", // O
"Changeable, some rain", // P
"Unsettled, short fine intervals", // Q
"Unsettled, rain later", // R
"Unsettled, rain at times", // S
"Very unsettled, finer at times", // T
"Rain at times, worse later", // U
"Rain at times, becoming very unsettled", // V
"Rain at frequent intervals", // W
"Very unsettled, rain", // X
"Stormy, possibly improving", // Y
"Stormy, much rain" // Z
};
// Map a constrained Zambretti number to a letter, per trend (G6EJD tables).
static char letterRising(int z) { const char* m = "ABCFGIJLMQTYZ"; return m[z - 1]; } // z 1..13
static char letterFalling(int z) { const char* m = "ABDHORUXZ"; return m[z - 1]; } // z 1..9
static char letterSteady(int z) { const char* m = "ABEKNPSWXZ"; return m[z - 1]; } // z 1..10
static int clampi(int v, int lo, int hi) { return v < lo ? lo : (v > hi ? hi : v); }
static WxCategory categoryOf(char letter) {
switch (letter) {
case 'A': case 'B': case 'C': case 'F': return WX_FINE;
case 'E': case 'G': case 'I': case 'J': case 'K':
case 'M': case 'N': case 'Q': return WX_FAIR;
case 'D': case 'H': case 'L': case 'O': case 'P': return WX_CHANGEABLE;
case 'R': case 'S': case 'T': case 'U': case 'V': case 'W':
case 'X': return WX_RAIN;
case 'Y': case 'Z': return WX_STORM;
default: return WX_UNKNOWN;
}
}
Trend classifyTrend(float d) {
if (d > TREND_THRESHOLD_HPA) return TREND_RISING;
if (d < -TREND_THRESHOLD_HPA) return TREND_FALLING;
return TREND_STEADY;
}
Forecast computeForecast(float p, Trend trend, int month) {
bool winter = (month < 4 || month > 9); // northern hemisphere
int z;
char letter;
if (trend == TREND_RISING) {
z = (int)lround(-0.1449 * p + 150.18);
if (winter) z += 1;
z = clampi(z, 1, 13);
letter = letterRising(z);
} else if (trend == TREND_FALLING) {
z = (int)lround(0.0000257935 * p * p * p
- 0.078482105 * p * p
+ 79.4582219457 * p
- 26762.7164899421);
if (winter) z -= 1;
z = clampi(z, 1, 9);
letter = letterFalling(z);
} else {
z = (int)lround(0.0000258964 * p * p * p
- 0.07753778137 * p * p
+ 77.2287820569 * p
- 25582.130426005);
z = clampi(z, 1, 10);
letter = letterSteady(z);
}
return Forecast{ letter, ZTEXT[letter - 'A'], categoryOf(letter) };
}
const char* categoryShort(WxCategory c) {
switch (c) {
case WX_FINE: return "Fine";
case WX_FAIR: return "Fair";
case WX_CHANGEABLE: return "Changeable";
case WX_RAIN: return "Rain";
case WX_STORM: return "Storm";
default: return "...";
}
}
+15
View File
@@ -0,0 +1,15 @@
#pragma once
#include <Arduino.h>
enum Trend { TREND_FALLING = -1, TREND_STEADY = 0, TREND_RISING = 1 };
enum WxCategory { WX_FINE, WX_FAIR, WX_CHANGEABLE, WX_RAIN, WX_STORM, WX_UNKNOWN };
struct Forecast {
char letter; // 'A'..'Z'
const char* text; // full Zambretti phrase
WxCategory category; // for display icon + short label
};
Trend classifyTrend(float deltaHpa3h);
Forecast computeForecast(float mslHpa, Trend trend, int month);
const char* categoryShort(WxCategory c);