From 2e3af80188558ec279ea54dc736cbc5c49681b17 Mon Sep 17 00:00:00 2001 From: Vitali Date: Fri, 17 Jul 2026 23:05:13 +0700 Subject: [PATCH] feat(display): mini forecast icon + Wi-Fi IP, BGR colour fix Rework the 160x80 status screen: large predicted-weather icon in the right window (scaled to fill), Wi-Fi/IP line at the bottom (or 'offline'), colour-coded forecast category, and the trend arrow shown only when rising/falling. Colours are built R/B-swapped for this BGR ST7735S panel. Co-Authored-By: Claude Opus 4.8 --- WeatherPredictor/display_ui.cpp | 125 +++++++++++++++++++++++--------- 1 file changed, 90 insertions(+), 35 deletions(-) diff --git a/WeatherPredictor/display_ui.cpp b/WeatherPredictor/display_ui.cpp index b9ff26e..ff82eb5 100644 --- a/WeatherPredictor/display_ui.cpp +++ b/WeatherPredictor/display_ui.cpp @@ -1,18 +1,41 @@ #include #include #include +#include #include "config.h" #include "display_ui.h" +#include "net.h" // Non-static so displayRender() can reference it via extern. Adafruit_ST7735 tft = Adafruit_ST7735(TFT_CS, TFT_DC, TFT_RST); +// This 0.96" ST7735S panel is wired BGR (red/blue swapped), so build every +// colour with R and B exchanged and it displays as intended. +static uint16_t rgb(uint8_t r, uint8_t g, uint8_t b) { return tft.color565(b, g, r); } + +static uint16_t C_MUTED, C_FRAME, C_LINE, C_SUN, C_CLOUD; +static uint16_t catColor(WxCategory c) { + switch (c) { + case WX_FINE: return rgb(143,190,138); // fine green + case WX_FAIR: return rgb(228,182,88); // amber + case WX_CHANGEABLE: return rgb(121,194,208); // mercury + case WX_RAIN: return rgb(92,134,214); // blue + case WX_STORM: return rgb(228,87,59); // vermilion + default: return C_MUTED; + } +} + void displayBegin() { pinMode(TFT_BLK, OUTPUT); digitalWrite(TFT_BLK, HIGH); // backlight on tft.initR(INITR_MINI160x80); // 0.96" ST7735S tft.invertDisplay(false); // this panel: no inversion tft.setRotation(3); // landscape, 160 wide x 80 tall + C_MUTED = rgb(140,160,190); + C_FRAME = rgb(42,58,90); + C_LINE = rgb(121,194,208); // mercury cyan + C_SUN = rgb(240,200,70); + C_CLOUD = rgb(224,232,240); tft.fillScreen(ST77XX_BLACK); } @@ -27,33 +50,50 @@ void displaySplash(const char* line1, const char* line2) { 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; +// Weather icon scaled into a W x H box at (x,y). +static void drawIcon(WxCategory c, int x, int y, int W, int H) { + int cx = x + W / 2, cy = y + H / 2; + int m = (W < H ? W : H); switch (c) { - case WX_FINE: - tft.fillCircle(x + 17, y + 17, 11, sun); + case WX_FINE: { + int r = (int)(m * 0.30f); + tft.fillCircle(cx, cy, r, C_SUN); + for (int k = 0; k < 8; k++) { + float a = k * 0.7853982f; // 45 deg steps + int x0 = cx + (int)((r + 4) * cosf(a)), y0 = cy + (int)((r + 4) * sinf(a)); + int x1 = cx + (int)((r + 11) * cosf(a)), y1 = cy + (int)((r + 11) * sinf(a)); + tft.drawLine(x0, y0, x1, y1, C_SUN); + } break; + } case WX_FAIR: - tft.fillCircle(x + 12, y + 12, 8, sun); - tft.fillRoundRect(x + 8, y + 18, 24, 12, 6, cloud); + tft.fillCircle(x + (int)(W * 0.34f), y + (int)(H * 0.34f), (int)(m * 0.24f), C_SUN); + tft.fillRoundRect(x + (int)(W * 0.12f), y + (int)(H * 0.48f), + (int)(W * 0.76f), (int)(H * 0.40f), (int)(H * 0.20f), C_CLOUD); break; case WX_CHANGEABLE: - tft.fillRoundRect(x + 4, y + 12, 26, 14, 7, cloud); + tft.fillRoundRect(x + (int)(W * 0.06f), y + (int)(H * 0.28f), + (int)(W * 0.88f), (int)(H * 0.46f), (int)(H * 0.23f), C_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); + case WX_RAIN: { + tft.fillRoundRect(x + (int)(W * 0.06f), y + (int)(H * 0.14f), + (int)(W * 0.88f), (int)(H * 0.40f), (int)(H * 0.20f), C_CLOUD); + int ry = y + (int)(H * 0.60f); + for (int i = 0; i < 5; i++) + tft.drawFastVLine(x + (int)(W * 0.18f) + i * (int)(W * 0.16f), ry, (int)(H * 0.26f), C_LINE); 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); + } + case WX_STORM: { + tft.fillRoundRect(x + (int)(W * 0.06f), y + (int)(H * 0.14f), + (int)(W * 0.88f), (int)(H * 0.40f), (int)(H * 0.20f), C_CLOUD); + int by = y + (int)(H * 0.58f); + tft.fillTriangle(cx, by, cx - 8, by + (int)(H * 0.30f), cx + 9, by + (int)(H * 0.22f), C_SUN); break; + } default: - tft.drawRect(x + 4, y + 10, 26, 18, cloud); // unknown / collecting + tft.drawRoundRect(x + 2, y + (int)(H * 0.2f), W - 4, (int)(H * 0.55f), 8, C_FRAME); + tft.setTextSize(2); tft.setTextColor(C_MUTED); + tft.setCursor(cx - 6, cy - 8); tft.print("?"); break; } } @@ -67,36 +107,51 @@ void displayRender(const AppState& s) { // Time (large), top-left snprintf(buf, sizeof(buf), "%02u:%02u", s.now.hour, s.now.minute); tft.setTextSize(2); - tft.setCursor(4, 4); + tft.setCursor(2, 2); 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); + snprintf(buf, sizeof(buf), "%04u-%02u-%02u", s.now.year, s.now.month, s.now.day); + tft.setCursor(2, 22); tft.print(buf); - // Pressure + trend arrow + // Pressure + trend arrow (arrow only when rising/falling) snprintf(buf, sizeof(buf), "%.0f hPa", s.mslHpa); - tft.setCursor(4, 40); + tft.setCursor(2, 34); 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); + if (s.trend != TREND_STEADY) { + tft.setTextColor(C_LINE); + tft.setCursor(80, 34); + tft.print(s.trend == TREND_RISING ? "^" : "v"); + tft.setTextColor(ST77XX_WHITE); + } // Temperature snprintf(buf, sizeof(buf), "%.1f C", s.tempC); - tft.setCursor(4, 52); + tft.setCursor(2, 46); tft.print(buf); - // Forecast short label (full width, bottom row) - tft.setTextColor(ST77XX_YELLOW); - tft.setCursor(4, 66); + // Predicted-weather icon, top-right (fills to just above the divider) + drawIcon(s.haveTrend ? s.forecast.category : WX_UNKNOWN, 78, 2, 78, 61); + + // Forecast category (colour-coded) + tft.setTextSize(1); + tft.setTextColor(s.haveTrend ? catColor(s.forecast.category) : C_MUTED); + tft.setCursor(2, 56); tft.print(s.haveTrend ? categoryShort(s.forecast.category) : "Collecting..."); + + // Bottom strip: divider + Wi-Fi / IP + tft.drawFastHLine(0, 65, 160, C_FRAME); + tft.setTextSize(1); + if (netConnected()) { + tft.setTextColor(C_LINE); + tft.setCursor(2, 69); + tft.print(netIP()); + } else { + tft.setTextColor(C_MUTED); + tft.setCursor(2, 69); + tft.print("WiFi: offline"); + } tft.setTextColor(ST77XX_WHITE); }