Files
Arduino/WeatherPredictor/display_ui.cpp
T
oskarvitaliiandClaude Opus 4.8 11f57a244d chore: make repo an Arduino collection with WeatherPredictor project
Flatten so the repo root is the Arduino sketchbook; the project lives in
WeatherPredictor/. Add a collection index README at the root and move the
detailed project README into WeatherPredictor/.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 11:58:33 +07:00

158 lines
5.3 KiB
C++

#include <Adafruit_GFX.h>
#include <Adafruit_ST7735.h>
#include <SPI.h>
#include <math.h>
#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);
}