#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); } void displaySplash(const char* line1, const char* line2) { tft.fillScreen(ST77XX_BLACK); tft.setTextColor(ST77XX_WHITE); tft.setTextSize(2); tft.setCursor(4, 20); tft.print(line1); tft.setTextSize(1); tft.setCursor(4, 50); tft.print(line2); } // 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: { 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 + (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 + (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 + (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 + (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.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; } } // 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(2, 2); tft.print(buf); // Date tft.setTextSize(1); 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 (arrow only when rising/falling) snprintf(buf, sizeof(buf), "%.0f hPa", s.mslHpa); tft.setCursor(2, 34); tft.print(buf); 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(2, 46); tft.print(buf); // 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); }