feat: Zambretti forecast with trend classification
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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() {
|
||||
|
||||
@@ -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 "...";
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
Reference in New Issue
Block a user