158 lines
5.3 KiB
C++
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);
|
|
}
|