Modern UI: antialiased DejaVu fonts, color-coded assets, proper RGB565 colors

This commit is contained in:
Chad 2026-03-29 16:19:31 -05:00
parent 29a65760c7
commit 28ca31e0e6
2 changed files with 417 additions and 375 deletions

125
docs/README.md Normal file
View file

@ -0,0 +1,125 @@
# Bitcoin Dashboard for Elecrow 7-inch HMI Display
A real-time financial dashboard displaying cryptocurrency, precious metals, stocks, and market indices on the Elecrow 7.0-inch ESP32 HMI Display.
## Hardware
- **Display**: Elecrow 7.0-inch ESP32 HMI Display (Advance series)
- **Resolution**: 800x480 RGB parallel interface
- **Touch**: GT911 capacitive touch controller
- **MCU**: ESP32-S3 with N4R8 module (4MB Flash, 8MB PSRAM)
## Features
- Real-time price data from Yahoo Finance API
- Auto-refresh every 5 minutes
- Touch to manual refresh
- WiFi credentials stored in persistent memory
- Antialiased fonts (DejaVu family)
## Displayed Assets
| Column | Assets |
|--------|--------|
| Hard Money | Bitcoin (BTC/USD), Gold, Silver |
| Stocks | MSTR, STRC |
| Indices | NASDAQ, DOW, S&P 500 |
## Color Scheme
| Asset | Color | Hex |
|-------|-------|-----|
| Bitcoin | Orange | `#F7931A` |
| Gold | Gold | `#FFD700` |
| Silver | Silver | `#C0C0C0` |
| Stocks/Indices | Dollar Green | `#00A86B` |
| Status Bar | Bitcoin Orange | `#F7931A` |
## Software Stack
- **Framework**: Arduino (PlatformIO)
- **Display Library**: LovyanGFX v1.2.19
- **Fonts**: DejaVu (antialiased)
## Building
```bash
cd /home/chad/Documents/PlatformIO/Projects/bitcoin_dashboard
pio run -t upload
```
## PlatformIO Configuration
- **Platform**: espressif32@6.8.1
- **Board**: esp32-s3-devkitc-1
- **Partition**: huge_app.csv (3MB app, no OTA)
- **Flash Mode**: QIO @ 80MHz
- **PSRAM**: OPI (8MB)
## Data Source
Yahoo Finance API:
```
https://query1.finance.yahoo.com/v8/finance/chart/{SYMBOL}?interval=1d&range=1d
```
### Symbol Mapping
| Display | Yahoo Symbol |
|---------|--------------|
| Bitcoin | BTC-USD |
| Gold | GC=F |
| Silver | SI=F |
| MSTR | MSTR |
| STRC | STRC |
| NASDAQ | ^IXIC |
| DOW | ^DJI |
| S&P 500 | ^GSPC |
## Pin Configuration
### RGB Display Interface
| Signal | GPIO |
|--------|------|
| D0-D15 | 15, 7, 6, 5, 4, 9, 46, 3, 8, 16, 1, 14, 21, 47, 48, 45 |
| HSYNC | GPIO 39 |
| VSYNC | GPIO 40 |
| DE | GPIO 41 |
| PCLK | GPIO 0 |
| Backlight | GPIO 2 |
### Touch Controller (GT911)
| Signal | GPIO |
|--------|------|
| SDA | GPIO 19 |
| SCL | GPIO 20 |
| I2C Address | 0x14 |
## WiFi Setup
WiFi credentials are stored in ESP32 preferences (persistent storage):
```cpp
preferences.begin("wifi", true);
preferences.putString("ssid", "your_ssid");
preferences.putString("password", "your_password");
preferences.end();
```
## Project Structure
```
bitcoin_dashboard/
├── src/
│ └── main.cpp # Main application
├── docs/
│ └── README.md # This file
├── platformio.ini # PlatformIO configuration
└── .gitignore
```
## License
MIT

View file

@ -1,6 +1,6 @@
/** /**
* Bitcoin Dashboard for Elecrow 7-inch HMI Display * Bitcoin Dashboard for Elecrow 7-inch HMI Display
* Simplified version with better WiFi/API debugging * Three-column layout with antialiased fonts
*/ */
#include <Arduino.h> #include <Arduino.h>
@ -18,11 +18,10 @@
#define TFT_WHITE 0xFFFF #define TFT_WHITE 0xFFFF
#define TFT_BLACK 0x0000 #define TFT_BLACK 0x0000
#define TFT_RED 0xF800 #define TFT_ORANGE 0xFD20
#define TFT_GOLD 0xFEA0
#define TFT_SILVER 0xC618
#define TFT_GREEN 0x07E0 #define TFT_GREEN 0x07E0
#define TFT_BLUE 0x001F
#define TFT_ORANGE 0xFD40
#define TFT_GRAY 0x7BEF
class LGFX : public lgfx::LGFX_Device { class LGFX : public lgfx::LGFX_Device {
public: public:
@ -114,60 +113,39 @@ public:
static LGFX lcd; static LGFX lcd;
static Preferences preferences; static Preferences preferences;
// Price data double btcPrice = 0, goldPrice = 0, silverPrice = 0;
double btcPrice = 0; double mstrPrice = 0, strtPrice = 0;
double goldPrice = 0; double nasdaqPrice = 0, dowPrice = 0, spxPrice = 0;
double silverPrice = 0;
double mstrPrice = 0;
double strtPrice = 0;
double nasdaqPrice = 0;
double dowPrice = 0;
double spxPrice = 0;
unsigned long lastRefresh = 0; unsigned long lastRefresh = 0;
const unsigned long REFRESH_INTERVAL = 300000; const unsigned long REFRESH_INTERVAL = 300000;
// Format price with US-style commas (e.g., 66,647.29)
String formatPriceUS(double price, int decimals = 2, bool includeDollar = true) { String formatPriceUS(double price, int decimals = 2, bool includeDollar = true) {
if (price <= 0) return "..."; if (price <= 0) return "...";
// Build the number string
String result = ""; String result = "";
// Handle the integer part with commas
long longPrice = (long)price; long longPrice = (long)price;
String intStr = String(longPrice); String intStr = String(longPrice);
int len = intStr.length(); int len = intStr.length();
int commaPos = len % 3;
for (int i = 0; i < len; i++) { for (int i = 0; i < len; i++) {
if (i > 0 && (i - (len % 3)) % 3 == 0 && (len % 3 != 0 || i > 0)) { if (i > 0 && (len - i) % 3 == 0) {
result += ",";
} else if (len % 3 == 0 && i > 0 && i % 3 == 0) {
result += ","; result += ",";
} }
result += intStr[i]; result += intStr[i];
} }
// Add decimals if needed
if (decimals > 0) { if (decimals > 0) {
result += "."; result += ".";
double frac = price - longPrice; double frac = price - longPrice;
for (int d = 0; d < decimals; d++) { for (int d = 0; d < decimals; d++) frac *= 10;
frac *= 10; long fracLong = (long)(frac + 0.5);
}
long fracLong = (long)(frac + 0.5); // Round
String fracStr = String(fracLong); String fracStr = String(fracLong);
while (fracStr.length() < decimals) { while (fracStr.length() < decimals) fracStr = "0" + fracStr;
fracStr = "0" + fracStr;
}
result += fracStr; result += fracStr;
} }
if (includeDollar) { return includeDollar ? "$" + result : result;
return "$" + result;
}
return result;
} }
bool fetchPrice(const char* symbol, double* result) { bool fetchPrice(const char* symbol, double* result) {
@ -177,22 +155,13 @@ bool fetchPrice(const char* symbol, double* result) {
HTTPClient http; HTTPClient http;
String url = "https://query1.finance.yahoo.com/v8/finance/chart/" + String(symbol) + "?interval=1d&range=1d"; String url = "https://query1.finance.yahoo.com/v8/finance/chart/" + String(symbol) + "?interval=1d&range=1d";
Serial.println("Fetching: " + String(symbol)); if (!http.begin(client, url)) return false;
Serial.println("URL: " + url);
if (!http.begin(client, url)) {
Serial.println("HTTP begin failed");
return false;
}
http.setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"); http.setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");
http.setTimeout(15000); http.setTimeout(15000);
int httpCode = http.GET(); int httpCode = http.GET();
Serial.println("HTTP code: " + String(httpCode));
if (httpCode != HTTP_CODE_OK) { if (httpCode != HTTP_CODE_OK) {
Serial.println("HTTP error: " + http.errorToString(httpCode));
http.end(); http.end();
return false; return false;
} }
@ -200,48 +169,22 @@ bool fetchPrice(const char* symbol, double* result) {
String payload = http.getString(); String payload = http.getString();
http.end(); http.end();
Serial.println("Response length: " + String(payload.length()));
// Parse regularMarketPrice from meta section
// Format: "regularMarketPrice":66647.29
String searchStr = "\"regularMarketPrice\":"; String searchStr = "\"regularMarketPrice\":";
int idx = payload.indexOf(searchStr); int idx = payload.indexOf(searchStr);
if (idx == -1) return false;
if (idx == -1) {
Serial.println("regularMarketPrice not found");
return false;
}
int start = idx + searchStr.length(); int start = idx + searchStr.length();
int end = payload.indexOf(",", start); int end = payload.indexOf(",", start);
if (end == -1) end = payload.indexOf("}", start); if (end == -1) end = payload.indexOf("}", start);
if (end <= start) return false;
if (end <= start) {
Serial.println("Could not find end of price");
return false;
}
String priceStr = payload.substring(start, end); String priceStr = payload.substring(start, end);
Serial.println("Price string: " + priceStr);
*result = priceStr.toDouble(); *result = priceStr.toDouble();
Serial.println("Parsed: " + String(*result));
return true; return true;
} }
void updatePrices() { void updatePrices() {
Serial.println("\n=== Updating prices ==="); if (WiFi.status() != WL_CONNECTED) return;
Serial.println("WiFi status: " + String(WiFi.status()));
Serial.println("WiFi connected: " + String(WiFi.status() == WL_CONNECTED ? "YES" : "NO"));
if (WiFi.status() != WL_CONNECTED) {
Serial.println("WiFi not connected, skipping update");
return;
}
Serial.println("IP: " + WiFi.localIP().toString());
fetchPrice("BTC-USD", &btcPrice); fetchPrice("BTC-USD", &btcPrice);
fetchPrice("GC=F", &goldPrice); fetchPrice("GC=F", &goldPrice);
fetchPrice("SI=F", &silverPrice); fetchPrice("SI=F", &silverPrice);
@ -250,117 +193,99 @@ void updatePrices() {
fetchPrice("^IXIC", &nasdaqPrice); fetchPrice("^IXIC", &nasdaqPrice);
fetchPrice("^DJI", &dowPrice); fetchPrice("^DJI", &dowPrice);
fetchPrice("^GSPC", &spxPrice); fetchPrice("^GSPC", &spxPrice);
Serial.println("=== Update complete ===\n");
} }
void drawDashboard() { void drawDashboard() {
lcd.fillScreen(TFT_BLACK); lcd.fillScreen(TFT_BLACK);
// Title bar
lcd.fillRect(0, 0, SCREEN_WIDTH, 45, TFT_ORANGE);
lcd.setTextColor(TFT_BLACK, TFT_ORANGE);
lcd.setTextSize(3);
lcd.setTextDatum(textdatum_t::middle_center);
lcd.drawString("BITCOIN DASHBOARD", SCREEN_WIDTH / 2, 22);
// Column positions
int col1X = 20; int col1X = 20;
int col2X = 280; int col2X = 280;
int col3X = 540; int col3X = 540;
int startY = 60; int startY = 20;
// Column separators lcd.setFont(&fonts::DejaVu18);
lcd.drawLine(265, startY, 265, SCREEN_HEIGHT - 35, TFT_GRAY);
lcd.drawLine(525, startY, 525, SCREEN_HEIGHT - 35, TFT_GRAY);
// Column 1: HARD MONEY
lcd.setTextColor(TFT_ORANGE, TFT_BLACK);
lcd.setTextSize(2);
lcd.setTextDatum(textdatum_t::top_left); lcd.setTextDatum(textdatum_t::top_left);
lcd.setTextColor(TFT_WHITE, TFT_BLACK);
lcd.drawString("HARD MONEY", col1X, startY); lcd.drawString("HARD MONEY", col1X, startY);
lcd.setTextColor(TFT_WHITE, TFT_BLACK); lcd.setTextColor(TFT_WHITE, TFT_BLACK);
lcd.drawString("Bitcoin:", col1X, startY + 35);
lcd.drawString(formatPriceUS(btcPrice, 0, true), col1X, startY + 60);
lcd.drawString("Gold:", col1X, startY + 95);
lcd.drawString(formatPriceUS(goldPrice, 2, true), col1X, startY + 120);
lcd.drawString("Silver:", col1X, startY + 155);
lcd.drawString(formatPriceUS(silverPrice, 2, true), col1X, startY + 180);
// Column 2: STOCKS
lcd.setTextColor(TFT_ORANGE, TFT_BLACK);
lcd.drawString("STOCKS", col2X, startY); lcd.drawString("STOCKS", col2X, startY);
lcd.setTextColor(TFT_WHITE, TFT_BLACK); lcd.setTextColor(TFT_WHITE, TFT_BLACK);
lcd.drawString("MSTR:", col2X, startY + 35);
lcd.drawString(formatPriceUS(mstrPrice, 2, true), col2X, startY + 60);
lcd.drawString("STRC:", col2X, startY + 95);
lcd.drawString(formatPriceUS(strtPrice, 2, true), col2X, startY + 120);
// Column 3: INDICES
lcd.setTextColor(TFT_ORANGE, TFT_BLACK);
lcd.drawString("INDICES", col3X, startY); lcd.drawString("INDICES", col3X, startY);
lcd.setTextColor(TFT_WHITE, TFT_BLACK); int labelY = startY + 40;
lcd.drawString("NASDAQ:", col3X, startY + 35); int priceY = labelY + 28;
lcd.drawString(formatPriceUS(nasdaqPrice, 2, true), col3X, startY + 60);
lcd.drawString("DOW:", col3X, startY + 95); lcd.setTextColor(TFT_ORANGE, TFT_BLACK);
lcd.drawString(formatPriceUS(dowPrice, 2, true), col3X, startY + 120); lcd.setFont(&fonts::DejaVu18);
lcd.drawString("Bitcoin:", col1X, labelY);
lcd.drawString(formatPriceUS(btcPrice, 0), col1X, priceY);
lcd.drawString("SPX:", col3X, startY + 155); lcd.setTextColor(TFT_GOLD, TFT_BLACK);
lcd.drawString(formatPriceUS(spxPrice, 2, true), col3X, startY + 180); lcd.drawString("Gold:", col1X, labelY + 60);
lcd.drawString(formatPriceUS(goldPrice, 2), col1X, priceY + 60);
// Status bar lcd.setTextColor(TFT_SILVER, TFT_BLACK);
lcd.fillRect(0, SCREEN_HEIGHT - 30, SCREEN_WIDTH, 30, TFT_GRAY); lcd.drawString("Silver:", col1X, labelY + 120);
lcd.setTextColor(TFT_BLACK, TFT_GRAY); lcd.drawString(formatPriceUS(silverPrice, 2), col1X, priceY + 120);
lcd.setTextSize(1);
lcd.setTextColor(TFT_GREEN, TFT_BLACK);
lcd.drawString("MSTR:", col2X, labelY);
lcd.drawString(formatPriceUS(mstrPrice, 2), col2X, priceY);
lcd.drawString("STRC:", col2X, labelY + 60);
lcd.drawString(formatPriceUS(strtPrice, 2), col2X, priceY + 60);
lcd.drawString("NASDAQ:", col3X, labelY);
lcd.drawString(formatPriceUS(nasdaqPrice, 2), col3X, priceY);
lcd.drawString("DOW:", col3X, labelY + 60);
lcd.drawString(formatPriceUS(dowPrice, 2), col3X, priceY + 60);
lcd.drawString("SPX:", col3X, labelY + 120);
lcd.drawString(formatPriceUS(spxPrice, 2), col3X, priceY + 120);
lcd.fillSmoothRoundRect(0, SCREEN_HEIGHT - 35, SCREEN_WIDTH, 35, 6, TFT_ORANGE);
lcd.setTextColor(TFT_WHITE, TFT_ORANGE);
lcd.setFont(&fonts::DejaVu12);
lcd.setTextDatum(textdatum_t::middle_left); lcd.setTextDatum(textdatum_t::middle_left);
String wifiStatus = WiFi.status() == WL_CONNECTED ? "Connected" : "DISCONNECTED"; String wifiStatus = WiFi.status() == WL_CONNECTED ? "Connected" : "DISCONNECTED";
lcd.drawString("WiFi: " + wifiStatus, 10, SCREEN_HEIGHT - 15); lcd.drawString("WiFi: " + wifiStatus, 15, SCREEN_HEIGHT - 17);
lcd.setTextDatum(textdatum_t::middle_center); lcd.setTextDatum(textdatum_t::middle_center);
lcd.drawString("TOUCH TO REFRESH", SCREEN_WIDTH / 2, SCREEN_HEIGHT - 15); lcd.drawString("TOUCH TO REFRESH", SCREEN_WIDTH / 2, SCREEN_HEIGHT - 17);
} }
void setup() { void setup() {
Serial.begin(115200); Serial.begin(115200);
delay(3000); delay(3000);
Serial.println("\n\n=== STARTING ===");
lcd.init(); lcd.init();
lcd.setBrightness(255); lcd.setBrightness(255);
lcd.setRotation(0); lcd.setRotation(0);
lcd.fillScreen(TFT_WHITE); lcd.fillScreen(TFT_WHITE);
delay(250); delay(250);
lcd.fillScreen(TFT_RED); lcd.fillScreen(TFT_ORANGE);
delay(250);
lcd.fillScreen(TFT_GREEN);
delay(250);
lcd.fillScreen(TFT_BLUE);
delay(250); delay(250);
lcd.fillScreen(TFT_BLACK); lcd.fillScreen(TFT_BLACK);
delay(250);
lcd.setTextSize(2);
lcd.setTextColor(TFT_WHITE, TFT_BLACK); lcd.setTextColor(TFT_WHITE, TFT_BLACK);
lcd.setFont(&fonts::DejaVu24);
lcd.setTextDatum(textdatum_t::middle_center); lcd.setTextDatum(textdatum_t::middle_center);
lcd.drawString("Bitcoin Dashboard", SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2 - 40); lcd.drawString("Bitcoin Dashboard", SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2 - 40);
lcd.setFont(&fonts::DejaVu18);
lcd.drawString("Starting...", SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2); lcd.drawString("Starting...", SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2);
// Load WiFi credentials
preferences.begin("wifi", true); preferences.begin("wifi", true);
String ssid = preferences.getString("ssid", ""); String ssid = preferences.getString("ssid", "");
String pass = preferences.getString("password", ""); String pass = preferences.getString("password", "");
preferences.end(); preferences.end();
if (ssid.length() > 0) { if (ssid.length() > 0) {
Serial.println("Connecting to WiFi: " + ssid); lcd.setFont(&fonts::DejaVu18);
lcd.drawString("WiFi: " + ssid, SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2 + 40); lcd.drawString("WiFi: " + ssid, SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2 + 40);
WiFi.mode(WIFI_STA); WiFi.mode(WIFI_STA);
@ -372,12 +297,10 @@ void setup() {
Serial.print("."); Serial.print(".");
attempts++; attempts++;
} }
Serial.println();
if (WiFi.status() == WL_CONNECTED) { if (WiFi.status() == WL_CONNECTED) {
Serial.println("WiFi connected!");
Serial.println("IP: " + WiFi.localIP().toString());
lcd.drawString("Connected! IP:", SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2 + 80); lcd.drawString("Connected! IP:", SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2 + 80);
lcd.setFont(&fonts::DejaVu12);
lcd.drawString(WiFi.localIP().toString(), SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2 + 110); lcd.drawString(WiFi.localIP().toString(), SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2 + 110);
delay(1000); delay(1000);
@ -387,14 +310,10 @@ void setup() {
return; return;
} }
Serial.println("WiFi connection failed");
lcd.drawString("WiFi failed", SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2 + 80); lcd.drawString("WiFi failed", SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2 + 80);
delay(2000); delay(2000);
} }
// Show WiFi setup screen
lcd.drawString("No WiFi configured", SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2 + 40);
delay(2000);
drawDashboard(); drawDashboard();
} }
@ -402,9 +321,8 @@ void loop() {
int32_t x, y; int32_t x, y;
if (lcd.getTouch(&x, &y)) { if (lcd.getTouch(&x, &y)) {
Serial.println("Touch detected - refreshing...");
lcd.setTextColor(TFT_WHITE, TFT_BLACK); lcd.setTextColor(TFT_WHITE, TFT_BLACK);
lcd.setTextSize(2); lcd.setFont(&fonts::DejaVu24);
lcd.setTextDatum(textdatum_t::middle_center); lcd.setTextDatum(textdatum_t::middle_center);
lcd.fillRect(0, SCREEN_HEIGHT / 2 - 20, SCREEN_WIDTH, 40, TFT_BLACK); lcd.fillRect(0, SCREEN_HEIGHT / 2 - 20, SCREEN_WIDTH, 40, TFT_BLACK);
lcd.drawString("Refreshing...", SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2); lcd.drawString("Refreshing...", SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2);
@ -416,7 +334,6 @@ void loop() {
delay(500); delay(500);
} }
// Auto-refresh every 5 minutes
if (millis() - lastRefresh > REFRESH_INTERVAL) { if (millis() - lastRefresh > REFRESH_INTERVAL) {
updatePrices(); updatePrices();
lastRefresh = millis(); lastRefresh = millis();