diff --git a/WeatherPredictor/WeatherPredictor.ino b/WeatherPredictor/WeatherPredictor.ino index 1954855..2ec88ca 100644 --- a/WeatherPredictor/WeatherPredictor.ino +++ b/WeatherPredictor/WeatherPredictor.ino @@ -1,80 +1,71 @@ #include #include #include "config.h" -#include "display_ui.h" #include "sensors.h" #include "rtc_time.h" +#include "display_ui.h" #include "forecast.h" #include "history.h" #include "app_settings.h" +#include "state.h" -static void i2cScan() { - Serial.println(F("I2C scan:")); - byte found = 0; - for (byte addr = 1; addr < 127; addr++) { - Wire.beginTransmission(addr); - if (Wire.endTransmission() == 0) { - Serial.printf(" device at 0x%02X\n", addr); - found++; - } +AppState g_state; + +static unsigned long tSample = 0; +static unsigned long tHistory = 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); + } else { + g_state.trend = TREND_STEADY; } - Serial.printf(" %d device(s) found\n", found); } void setup() { Serial.begin(115200); delay(200); - Serial.println(F("\n=== Weather Predictor booting ===")); - Wire.begin(I2C_SDA, I2C_SCL); // MUST come before any I2C driver begin() - i2cScan(); + Serial.println(F("\n=== Weather Predictor ===")); + Wire.begin(I2C_SDA, I2C_SCL); displayBegin(); - displaySplash("Weather", "Predictor v0.1"); + 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 -> setting to build time")); - rtcSet(RtcTime{2026, 7, 17, 12, 0, 0}); // temporary; NTP will correct later + Serial.println(F("RTC lost power -> temporary time set")); + rtcSet(RtcTime{2026, 7, 17, 12, 0, 0}); } - - // --- 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)); - } - - // --- history self-test (remove after Task 8) --- - historyBegin(); - uint32_t base = 1000000000UL; - for (int i = 0; i < 40; i++) // 40 samples @ 5 min = 3h20m, pressure falling - historyAdd(base + (uint32_t)i * 300, 1015.0f - i * 0.2f, 20.0f); - float d; - if (historyTrendDelta(d)) - Serial.printf("history count=%d 3h delta=%.2f hPa trend=%d\n", - historyCount(), d, classifyTrend(d)); - else - Serial.println(F("history: not enough data for trend")); - - // --- settings self-test (remove after Task 8) --- settingsBegin(); - Serial.printf("settings: alt=%.0f tz=%d lat=%.4f lon=%.4f\n", - settings.altitudeM, settings.tzOffsetMin, settings.lat, settings.lon); - settings.altitudeM += 1.0f; // change and persist to test round-trip - settingsSave(); - Serial.println(F("settings: incremented altitude and saved")); + historyBegin(); + + // Seed first sample immediately. + sampleNow(); + historyAdd(rtcEpoch(), g_state.mslHpa, g_state.tempC); + displayRender(g_state); + tSample = tHistory = millis(); } void loop() { - RtcTime t = rtcNow(); - float abs_ = readAbsPressureHpa(); - Serial.printf("%04u-%02u-%02u %02u:%02u:%02u abs=%.1f msl=%.1f t=%.1f\n", - t.year, t.month, t.day, t.hour, t.minute, t.second, - abs_, toSeaLevelHpa(abs_, DEFAULT_ALTITUDE_M), readTemperatureC()); - delay(2000); + 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); + } } diff --git a/WeatherPredictor/display_ui.cpp b/WeatherPredictor/display_ui.cpp index 58e72ec..b9ff26e 100644 --- a/WeatherPredictor/display_ui.cpp +++ b/WeatherPredictor/display_ui.cpp @@ -4,14 +4,15 @@ #include "config.h" #include "display_ui.h" -static Adafruit_ST7735 tft = Adafruit_ST7735(TFT_CS, TFT_DC, TFT_RST); +// Non-static so displayRender() can reference it via extern. +Adafruit_ST7735 tft = Adafruit_ST7735(TFT_CS, TFT_DC, TFT_RST); void displayBegin() { pinMode(TFT_BLK, OUTPUT); digitalWrite(TFT_BLK, HIGH); // backlight on - tft.initR(INITR_MINI160x80); // 0.96" 80x160 ST7735S - tft.invertDisplay(false); // this panel needs inversion; flip if colours look wrong - tft.setRotation(3); // portrait, 80 wide x 160 tall + tft.initR(INITR_MINI160x80); // 0.96" ST7735S + tft.invertDisplay(false); // this panel: no inversion + tft.setRotation(3); // landscape, 160 wide x 80 tall tft.fillScreen(ST77XX_BLACK); } @@ -19,9 +20,83 @@ void displaySplash(const char* line1, const char* line2) { tft.fillScreen(ST77XX_BLACK); tft.setTextColor(ST77XX_WHITE); tft.setTextSize(2); - tft.setCursor(4, 40); + tft.setCursor(4, 20); tft.print(line1); tft.setTextSize(1); - tft.setCursor(4, 70); + tft.setCursor(4, 50); tft.print(line2); } + +// Simple 34x34 weather icon at (x, y). +static void drawIcon(WxCategory c, int x, int y) { + uint16_t sun = ST77XX_YELLOW; + uint16_t cloud = ST77XX_WHITE; + uint16_t rain = ST77XX_CYAN; + switch (c) { + case WX_FINE: + tft.fillCircle(x + 17, y + 17, 11, sun); + break; + case WX_FAIR: + tft.fillCircle(x + 12, y + 12, 8, sun); + tft.fillRoundRect(x + 8, y + 18, 24, 12, 6, cloud); + break; + case WX_CHANGEABLE: + tft.fillRoundRect(x + 4, y + 12, 26, 14, 7, cloud); + break; + case WX_RAIN: + tft.fillRoundRect(x + 4, y + 8, 26, 14, 7, cloud); + for (int i = 0; i < 3; i++) + tft.drawFastVLine(x + 9 + i * 8, y + 24, 8, rain); + break; + case WX_STORM: + tft.fillRoundRect(x + 4, y + 8, 26, 14, 7, cloud); + tft.fillTriangle(x + 16, y + 22, x + 12, y + 32, x + 20, y + 30, ST77XX_YELLOW); + break; + default: + tft.drawRect(x + 4, y + 10, 26, 18, cloud); // unknown / collecting + break; + } +} + +// Layout for landscape 160 x 80. +void displayRender(const AppState& s) { + tft.fillScreen(ST77XX_BLACK); + tft.setTextColor(ST77XX_WHITE); + char buf[24]; + + // Time (large), top-left + snprintf(buf, sizeof(buf), "%02u:%02u", s.now.hour, s.now.minute); + tft.setTextSize(2); + tft.setCursor(4, 4); + tft.print(buf); + + // Icon, top-right + drawIcon(s.haveTrend ? s.forecast.category : WX_UNKNOWN, 120, 4); + + // Date + snprintf(buf, sizeof(buf), "%04u-%02u-%02u", s.now.year, s.now.month, s.now.day); + tft.setTextSize(1); + tft.setCursor(4, 26); + tft.print(buf); + + // Pressure + trend arrow + snprintf(buf, sizeof(buf), "%.0f hPa", s.mslHpa); + tft.setCursor(4, 40); + tft.print(buf); + const char* arrow = (s.trend == TREND_RISING) ? "^" : (s.trend == TREND_FALLING ? "v" : "="); + tft.setCursor(96, 40); + tft.setTextColor(ST77XX_CYAN); + tft.print(arrow); + tft.setTextColor(ST77XX_WHITE); + + // Temperature + snprintf(buf, sizeof(buf), "%.1f C", s.tempC); + tft.setCursor(4, 52); + tft.print(buf); + + // Forecast short label (full width, bottom row) + tft.setTextColor(ST77XX_YELLOW); + tft.setCursor(4, 66); + tft.print(s.haveTrend ? categoryShort(s.forecast.category) : "Collecting..."); + tft.setTextColor(ST77XX_WHITE); +} diff --git a/WeatherPredictor/display_ui.h b/WeatherPredictor/display_ui.h index c21b7a2..480781b 100644 --- a/WeatherPredictor/display_ui.h +++ b/WeatherPredictor/display_ui.h @@ -1,5 +1,7 @@ #pragma once #include +#include "state.h" void displayBegin(); void displaySplash(const char* line1, const char* line2); +void displayRender(const AppState& s); diff --git a/WeatherPredictor/state.h b/WeatherPredictor/state.h new file mode 100644 index 0000000..4a629df --- /dev/null +++ b/WeatherPredictor/state.h @@ -0,0 +1,15 @@ +#pragma once +#include "rtc_time.h" +#include "forecast.h" + +struct AppState { + float absHpa; + float mslHpa; + float tempC; + Trend trend; + Forecast forecast; + RtcTime now; + bool haveTrend; +}; + +extern AppState g_state;