diff --git a/src/main.cpp b/src/main.cpp index ecda118..35eb422 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #define SCREEN_WIDTH 800 #define SCREEN_HEIGHT 480 @@ -159,6 +160,11 @@ const unsigned long WIFI_CONNECT_TIMEOUT = 15000; bool wifiConnecting = false; unsigned long wifiConnectStartTime = 0; +bool timeSynced = false; +bool firstBoot = true; +const char* ntpServer = "pool.ntp.org"; +bool displayFlipped = false; + #define HISTORY_DAYS 30 double btcHistory[HISTORY_DAYS] = {0}; double goldHistory[HISTORY_DAYS] = {0}; @@ -168,12 +174,31 @@ bool goldHistoryLoaded = false; bool silverHistoryLoaded = false; unsigned long lastHistoryRefresh = 0; +#define HM_HISTORY_MAX 100 +double btcHmHistory[HM_HISTORY_MAX] = {0}; +double goldHmHistory[HM_HISTORY_MAX] = {0}; +double silverHmHistory[HM_HISTORY_MAX] = {0}; +int btcHmCount = 0; +int goldHmCount = 0; +int silverHmCount = 0; +bool btcHmLoaded = false; +bool goldHmLoaded = false; +bool silverHmLoaded = false; +double btcHighDay = 0, btcLowDay = 0, btcHighMonth = 0, btcLowMonth = 0, btcHighYear = 0, btcLowYear = 0; +double goldHighDay = 0, goldLowDay = 0, goldHighMonth = 0, goldLowMonth = 0, goldHighYear = 0, goldLowYear = 0; +double silverHighDay = 0, silverLowDay = 0, silverHighMonth = 0, silverLowMonth = 0, silverHighYear = 0, silverLowYear = 0; +int hmSparklinePeriod = 2; +bool hmNeedsRefresh = false; +unsigned long hmLastFetch = 0; +const unsigned long HM_FETCH_INTERVAL = 60000; + enum ScreenState { SCREEN_DASHBOARD, SCREEN_CUSTOMIZE_STOCKS, SCREEN_CUSTOMIZE_INDICES, SCREEN_WIFI_CONFIG, - SCREEN_WIFI_PASSWORD + SCREEN_WIFI_PASSWORD, + SCREEN_HARD_MONEY_DETAIL }; ScreenState currentScreen = SCREEN_DASHBOARD; @@ -360,6 +385,223 @@ void fetchAllHistory() { fetchHistory("SI=F", silverHistory, &silverHistoryLoaded); } +bool fetchHmHistoryForSymbol(const char* symbol, double* history, int* count, int period) { + const char* interval; + const char* range; + + switch (period) { + case 0: interval = "5m"; range = "5d"; break; + case 1: interval = "1h"; range = "7d"; break; + case 2: interval = "1d"; range = "1mo"; break; + case 3: interval = "1wk"; range = "1y"; break; + case 4: interval = "1mo"; range = "max"; break; + default: interval = "1d"; range = "1mo"; break; + } + + WiFiClientSecure client; + client.setInsecure(); + + HTTPClient http; + String url = "https://query1.finance.yahoo.com/v8/finance/chart/" + String(symbol) + "?interval=" + String(interval) + "&range=" + String(range); + + if (!http.begin(client, url)) return false; + + http.setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"); + http.setTimeout(15000); + + int httpCode = http.GET(); + if (httpCode != HTTP_CODE_OK) { + http.end(); + return false; + } + + String payload = http.getString(); + http.end(); + + String arraySearch = "\"close\":["; + int arrayIdx = payload.indexOf(arraySearch); + if (arrayIdx == -1) return false; + + int arrayStart = arrayIdx + arraySearch.length(); + int arrayEnd = payload.indexOf("]", arrayStart); + if (arrayEnd == -1) return false; + + String closeArray = payload.substring(arrayStart, arrayEnd); + + *count = 0; + int pos = 0; + + while (pos < closeArray.length() && *count < HM_HISTORY_MAX) { + while (pos < closeArray.length() && (closeArray[pos] == ' ' || closeArray[pos] == ',')) pos++; + if (pos >= closeArray.length()) break; + + int valueStart = pos; + while (pos < closeArray.length() && closeArray[pos] != ',' && closeArray[pos] != ' ') pos++; + + String valueStr = closeArray.substring(valueStart, pos); + + if (!valueStr.equals("null") && valueStr.length() > 0) { + double value = valueStr.toDouble(); + if (value > 0) { + history[*count] = value; + (*count)++; + } + } + pos++; + } + + return *count > 0; +} + +void fetchHmHighLowForSymbol(const char* symbol, double* highDay, double* lowDay, double* highMonth, double* lowMonth, double* highYear, double* lowYear) { + if (WiFi.status() != WL_CONNECTED) return; + + WiFiClientSecure client; + client.setInsecure(); + HTTPClient http; + + // First API call: hourly data for today's intraday high/low + String url = "https://query1.finance.yahoo.com/v8/finance/chart/" + String(symbol) + "?interval=1h&range=1d"; + if (http.begin(client, url)) { + http.setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"); + http.setTimeout(15000); + int httpCode = http.GET(); + if (httpCode == HTTP_CODE_OK) { + String payload = http.getString(); + http.end(); + + String arraySearch = "\"close\":["; + int arrayIdx = payload.indexOf(arraySearch); + if (arrayIdx != -1) { + int arrayStart = arrayIdx + arraySearch.length(); + int arrayEnd = payload.indexOf("]", arrayStart); + if (arrayEnd != -1) { + String closeArray = payload.substring(arrayStart, arrayEnd); + + double minDay = 1e18, maxDay = 0; + int pos = 0; + while (pos < closeArray.length()) { + while (pos < closeArray.length() && (closeArray[pos] == ' ' || closeArray[pos] == ',')) pos++; + if (pos >= closeArray.length()) break; + int valueStart = pos; + while (pos < closeArray.length() && closeArray[pos] != ',' && closeArray[pos] != ' ') pos++; + String valueStr = closeArray.substring(valueStart, pos); + if (!valueStr.equals("null") && valueStr.length() > 0) { + double value = valueStr.toDouble(); + if (value > 0) { + if (value < minDay) minDay = value; + if (value > maxDay) maxDay = value; + } + } + } + if (maxDay > 0) { + *highDay = maxDay; + *lowDay = minDay; + } + } + } + } else { + http.end(); + } + } + + // Second API call: daily data for month and year high/low + url = "https://query1.finance.yahoo.com/v8/finance/chart/" + String(symbol) + "?interval=1d&range=1y"; + if (!http.begin(client, url)) return; + + http.setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"); + http.setTimeout(15000); + int httpCode = http.GET(); + if (httpCode != HTTP_CODE_OK) { + http.end(); + return; + } + + String payload = http.getString(); + http.end(); + + String arraySearch = "\"close\":["; + int arrayIdx = payload.indexOf(arraySearch); + if (arrayIdx == -1) return; + + int arrayStart = arrayIdx + arraySearch.length(); + int arrayEnd = payload.indexOf("]", arrayStart); + if (arrayEnd == -1) return; + + String closeArray = payload.substring(arrayStart, arrayEnd); + + // First pass: count values and find year high/low + double minYear = 1e18, maxYear = 0; + int valueCount = 0; + int pos = 0; + + while (pos < closeArray.length()) { + while (pos < closeArray.length() && (closeArray[pos] == ' ' || closeArray[pos] == ',')) pos++; + if (pos >= closeArray.length()) break; + int valueStart = pos; + while (pos < closeArray.length() && closeArray[pos] != ',' && closeArray[pos] != ' ') pos++; + String valueStr = closeArray.substring(valueStart, pos); + if (!valueStr.equals("null") && valueStr.length() > 0) { + double value = valueStr.toDouble(); + if (value > 0) { + valueCount++; + if (value < minYear) minYear = value; + if (value > maxYear) maxYear = value; + } + } + } + + if (valueCount > 0) { + *highYear = maxYear; + *lowYear = minYear; + + // Second pass: find month high/low (last 22 trading days) + int monthStart = valueCount > 22 ? valueCount - 22 : 0; + double minMonth = 1e18, maxMonth = 0; + int idx = 0; + pos = 0; + while (pos < closeArray.length()) { + while (pos < closeArray.length() && (closeArray[pos] == ' ' || closeArray[pos] == ',')) pos++; + if (pos >= closeArray.length()) break; + int valueStart = pos; + while (pos < closeArray.length() && closeArray[pos] != ',' && closeArray[pos] != ' ') pos++; + String valueStr = closeArray.substring(valueStart, pos); + if (!valueStr.equals("null") && valueStr.length() > 0) { + double value = valueStr.toDouble(); + if (value > 0) { + if (idx >= monthStart) { + if (value < minMonth) minMonth = value; + if (value > maxMonth) maxMonth = value; + } + idx++; + } + } + } + *highMonth = maxMonth; + *lowMonth = minMonth; + } +} + +void fetchAllHmData(int period) { + btcHmCount = goldHmCount = silverHmCount = 0; + btcHmLoaded = goldHmLoaded = silverHmLoaded = false; + + fetchHmHistoryForSymbol("BTC-USD", btcHmHistory, &btcHmCount, period); + btcHmLoaded = btcHmCount > 0; + + fetchHmHistoryForSymbol("GC=F", goldHmHistory, &goldHmCount, period); + goldHmLoaded = goldHmCount > 0; + + fetchHmHistoryForSymbol("SI=F", silverHmHistory, &silverHmCount, period); + silverHmLoaded = silverHmCount > 0; + + fetchHmHighLowForSymbol("BTC-USD", &btcHighDay, &btcLowDay, &btcHighMonth, &btcLowMonth, &btcHighYear, &btcLowYear); + fetchHmHighLowForSymbol("GC=F", &goldHighDay, &goldLowDay, &goldHighMonth, &goldLowMonth, &goldHighYear, &goldLowYear); + fetchHmHighLowForSymbol("SI=F", &silverHighDay, &silverLowDay, &silverHighMonth, &silverLowMonth, &silverHighYear, &silverLowYear); + + hmLastFetch = millis(); +} + void drawSparkline(int x, int y, int width, int height, double* history, bool loaded, uint16_t color) { if (!loaded) return; @@ -434,27 +676,115 @@ void saveConfig() { } } - preferences.end(); + preferences.end(); +} + +int dayOfWeek(int year, int month, int day) { + if (month < 3) { + month += 12; + year--; + } + return (day + 13 * (month + 1) / 5 + year + year / 4 - year / 100 + year / 400) % 7; +} + +bool isUSDST(int year, int month, int day, int hour) { + int springDOW = dayOfWeek(year, 3, 8); + int springStart = 8 + ((14 - springDOW) % 7); + + int fallDOW = dayOfWeek(year, 11, 1); + int fallEnd = 1 + ((14 - fallDOW) % 7); + + if (month < 3 || month > 11) return false; + if (month > 3 && month < 11) return true; + + if (month == 3) { + if (day < springStart) return false; + if (day > springStart) return true; + return hour >= 2; + } + + if (month == 11) { + if (day < fallEnd) return true; + if (day > fallEnd) return false; + return hour < 2; + } + + return false; +} + +bool isUSMarketOpen() { + if (!timeSynced) return firstBoot; + + time_t now = time(nullptr); + struct tm* utc = gmtime(&now); + + int utcHour = utc->tm_hour; + int utcMinute = utc->tm_min; + int year = utc->tm_year + 1900; + int month = utc->tm_mon + 1; + int day = utc->tm_mday; + int utcWday = utc->tm_wday; + + bool dst = isUSDST(year, month, day, utcHour); + int etOffset = dst ? -4 : -5; + + int etHour = utcHour + etOffset; + int etWday = utcWday; + + if (etHour < 0) { + etHour += 24; + etWday = (etWday + 6) % 7; + } else if (etHour >= 24) { + etHour -= 24; + etWday = (etWday + 1) % 7; + } + + if (etWday == 0 || etWday == 6) return false; + + if (etHour < 9 || etHour >= 16) return false; + if (etHour == 9 && utcMinute < 30) return false; + + return true; +} + +void syncTime() { + configTime(0, 0, ntpServer); + + int retries = 0; + while (time(nullptr) < 100000 && retries < 20) { + delay(500); + retries++; + } + + if (time(nullptr) >= 100000) { + timeSynced = true; + } } void updatePrices() { - if (WiFi.status() != WL_CONNECTED) return; + if (WiFi.status() != WL_CONNECTED) return; - fetchPriceFinnhub("BINANCE:BTCUSDT", &btcPrice); - fetchPriceYahoo("GC=F", &goldPrice); - fetchPriceYahoo("SI=F", &silverPrice); + fetchPriceFinnhub("BINANCE:BTCUSDT", &btcPrice); + fetchPriceYahoo("GC=F", &goldPrice); + fetchPriceYahoo("SI=F", &silverPrice); - for (int i = 0; i < MAX_STOCKS; i++) { - if (strlen(stockTickers[i]) > 0) { - fetchPriceFinnhub(stockTickers[i], &stockPrices[i]); - } - } + if (isUSMarketOpen()) { + for (int i = 0; i < MAX_STOCKS; i++) { + if (strlen(stockTickers[i]) > 0) { + fetchPriceFinnhub(stockTickers[i], &stockPrices[i]); + } + } - for (int i = 0; i < MAX_INDICES; i++) { - if (indexSelected[i] && strlen(indexTickers[i]) > 0) { - fetchPriceYahoo(indexTickers[i], &indexPrices[i]); - } - } + for (int i = 0; i < MAX_INDICES; i++) { + if (indexSelected[i] && strlen(indexTickers[i]) > 0) { + fetchPriceYahoo(indexTickers[i], &indexPrices[i]); + } + } + } + + if (firstBoot) { + firstBoot = false; + } } void drawKeyboard(int y) { @@ -714,6 +1044,42 @@ lcd.setTextColor(TFT_WHITE, TFT_ORANGE); lcd.drawString("BACK", 125, SCREEN_HEIGHT - 30); } +void drawGearIcon(int cx, int cy, int radius, uint16_t color) { + for (int i = 0; i < 8; i++) { + float angle = i * 45.0 * DEG_TO_RAD; + float halfWidth = 0.35; + float cosA = cos(angle); + float sinA = sin(angle); + int x0 = cx + cosA * radius; + int y0 = cy + sinA * radius; + int x1 = cx + cosA * (radius + 6); + int y1 = cy + sinA * (radius + 6); + int dx = halfWidth * (-sinA) * radius; + int dy = halfWidth * cosA * radius; + lcd.fillTriangle(x0 - dx, y0 - dy, x1 - dx, y1 - dy, x1 + dx, y1 + dy, color); + lcd.fillTriangle(x0 - dx, y0 - dy, x1 + dx, y1 + dy, x0 + dx, y0 + dy, color); + } + lcd.fillCircle(cx, cy, radius, color); + lcd.fillCircle(cx, cy, radius - 3, TFT_ORANGE); +} + +void drawFlipIcon(int cx, int cy, int radius, uint16_t color) { + lcd.setColor(color); + lcd.drawArc(cx, cy, radius, radius - 3, 60, 300); + float arrowAngle = 60.0 * DEG_TO_RAD; + int arrowX = cx + cos(arrowAngle) * radius; + int arrowY = cy - sin(arrowAngle) * radius; + float tangentAngle = arrowAngle - PI / 2; + int tipX = arrowX + cos(tangentAngle) * 6; + int tipY = arrowY - sin(tangentAngle) * 6; + lcd.fillTriangle( + arrowX + cos(tangentAngle) * 2 - sin(tangentAngle) * 4, + arrowY - sin(tangentAngle) * 2 - cos(tangentAngle) * 4, + arrowX + cos(tangentAngle) * 2 + sin(tangentAngle) * 4, + arrowY - sin(tangentAngle) * 2 + cos(tangentAngle) * 4, + tipX, tipY, color); +} + void drawDashboard() { lcd.fillScreen(TFT_BLACK); @@ -724,6 +1090,7 @@ void drawDashboard() { lcd.drawString("HARD MONEY", 20, 22); lcd.drawString("STOCKS", 280, 22); lcd.drawString("INDICES", 540, 22); + drawFlipIcon(SCREEN_WIDTH - 32, 22, 12, TFT_WHITE); int col1X = 20; int col2X = 280; @@ -777,8 +1144,13 @@ indexY += 65; String wifiStatus; if (wifiConnecting) { wifiStatus = "Connecting..."; + } else if (WiFi.status() == WL_CONNECTED) { + wifiStatus = WiFi.SSID(); + if (wifiStatus.length() > 20) { + wifiStatus = wifiStatus.substring(0, 17) + "..."; + } } else { - wifiStatus = WiFi.status() == WL_CONNECTED ? "OK" : "OFFLINE"; + wifiStatus = "OFFLINE"; } lcd.drawString("WiFi: " + wifiStatus, 20, SCREEN_HEIGHT - 20); @@ -791,8 +1163,7 @@ indexY += 65; lcd.setTextDatum(textdatum_t::middle_center); lcd.drawString(String(timeStr), SCREEN_WIDTH / 2, SCREEN_HEIGHT - 20); - lcd.fillSmoothRoundRect(SCREEN_WIDTH - 160, SCREEN_HEIGHT - 36, 140, 32, 6, TFT_DARKGRAY); - lcd.drawString("CUSTOMIZE", SCREEN_WIDTH - 90, SCREEN_HEIGHT - 20); +drawGearIcon(SCREEN_WIDTH - 32, SCREEN_HEIGHT - 20, 9, TFT_WHITE); } void drawFullKeyboard(int y, bool showSpecials) { @@ -1273,12 +1644,219 @@ void showRefreshingMessage() { lcd.setTextDatum(textdatum_t::middle_left); String wifiStatus = WiFi.status() == WL_CONNECTED ? "OK" : "OFFLINE"; lcd.drawString("WiFi: " + wifiStatus, 20, SCREEN_HEIGHT - 20); - + lcd.setTextDatum(textdatum_t::middle_center); lcd.drawString("REFRESHING...", SCREEN_WIDTH / 2, SCREEN_HEIGHT - 20); - - lcd.fillSmoothRoundRect(SCREEN_WIDTH - 160, SCREEN_HEIGHT - 36, 140, 32, 6, TFT_DARKGRAY); - lcd.drawString("CUSTOMIZE", SCREEN_WIDTH - 90, SCREEN_HEIGHT - 20); + + drawGearIcon(SCREEN_WIDTH - 32, SCREEN_HEIGHT - 20, 9, TFT_WHITE); +} + +void drawHardMoneyDetailScreen() { + lcd.fillScreen(TFT_BLACK); + + lcd.fillSmoothRoundRect(0, 0, SCREEN_WIDTH, 45, 8, TFT_ORANGE); + lcd.setTextColor(TFT_WHITE, TFT_ORANGE); + lcd.setFont(&fonts::DejaVu24); + lcd.setTextDatum(textdatum_t::middle_center); + lcd.drawString("HARD MONEY", SCREEN_WIDTH / 2, 22); + + int col1X = 50; + int col2X = 310; + int col3X = 570; + int colWidth = 180; + + int headerY = 65; + int priceY = 90; + + lcd.setTextColor(TFT_ORANGE, TFT_BLACK); + lcd.setFont(&fonts::Roboto_Thin_24); + lcd.setTextDatum(textdatum_t::middle_left); + lcd.drawString("BITCOIN", col1X, headerY); + lcd.drawString(formatPriceUS(btcPrice, 0), col1X, priceY); + + lcd.setTextColor(TFT_GOLD, TFT_BLACK); + lcd.drawString("GOLD", col2X, headerY); + lcd.drawString(formatPriceUS(goldPrice, 2), col2X, priceY); + + lcd.setTextColor(TFT_SILVER, TFT_BLACK); + lcd.drawString("SILVER", col3X, headerY); + lcd.drawString(formatPriceUS(silverPrice, 2), col3X, priceY); + + int highLowY = priceY + 30; + int rowHeight = 22; + + lcd.setFont(&fonts::DejaVu12); + lcd.setTextDatum(textdatum_t::middle_left); + + lcd.setTextColor(TFT_GREEN, TFT_BLACK); + lcd.drawString("Day High:", col1X, highLowY); + lcd.drawString(formatPriceUS(btcHighDay, 0, true), col1X + 65, highLowY); + lcd.setTextColor(TFT_GREEN, TFT_BLACK); + lcd.drawString("Day High:", col2X, highLowY); + lcd.drawString(formatPriceUS(goldHighDay, 2, true), col2X + 65, highLowY); + lcd.drawString("Day High:", col3X, highLowY); + lcd.drawString(formatPriceUS(silverHighDay, 2, true), col3X + 65, highLowY); + + lcd.setTextColor(0xF800, TFT_BLACK); + lcd.drawString("Day Low:", col1X, highLowY + rowHeight); + lcd.drawString(formatPriceUS(btcLowDay, 0, true), col1X + 65, highLowY + rowHeight); + lcd.drawString("Day Low:", col2X, highLowY + rowHeight); + lcd.drawString(formatPriceUS(goldLowDay, 2, true), col2X + 65, highLowY + rowHeight); + lcd.drawString("Day Low:", col3X, highLowY + rowHeight); + lcd.drawString(formatPriceUS(silverLowDay, 2, true), col3X + 65, highLowY + rowHeight); + + lcd.setTextColor(TFT_GREEN, TFT_BLACK); + lcd.drawString("Mo High:", col1X, highLowY + rowHeight * 2); + lcd.drawString(formatPriceUS(btcHighMonth, 0, true), col1X + 65, highLowY + rowHeight * 2); + lcd.drawString("Mo High:", col2X, highLowY + rowHeight * 2); + lcd.drawString(formatPriceUS(goldHighMonth, 2, true), col2X + 65, highLowY + rowHeight * 2); + lcd.drawString("Mo High:", col3X, highLowY + rowHeight * 2); + lcd.drawString(formatPriceUS(silverHighMonth, 2, true), col3X + 65, highLowY + rowHeight * 2); + + lcd.setTextColor(0xF800, TFT_BLACK); + lcd.drawString("Mo Low:", col1X, highLowY + rowHeight * 3); + lcd.drawString(formatPriceUS(btcLowMonth, 0, true), col1X + 65, highLowY + rowHeight * 3); + lcd.drawString("Mo Low:", col2X, highLowY + rowHeight * 3); + lcd.drawString(formatPriceUS(goldLowMonth, 2, true), col2X + 65, highLowY + rowHeight * 3); + lcd.drawString("Mo Low:", col3X, highLowY + rowHeight * 3); + lcd.drawString(formatPriceUS(silverLowMonth, 2, true), col3X + 65, highLowY + rowHeight * 3); + + lcd.setTextColor(TFT_GREEN, TFT_BLACK); + lcd.drawString("Yr High:", col1X, highLowY + rowHeight * 4); + lcd.drawString(formatPriceUS(btcHighYear, 0, true), col1X + 65, highLowY + rowHeight * 4); + lcd.drawString("Yr High:", col2X, highLowY + rowHeight * 4); + lcd.drawString(formatPriceUS(goldHighYear, 2, true), col2X + 65, highLowY + rowHeight * 4); + lcd.drawString("Yr High:", col3X, highLowY + rowHeight * 4); + lcd.drawString(formatPriceUS(silverHighYear, 2, true), col3X + 65, highLowY + rowHeight * 4); + + lcd.setTextColor(0xF800, TFT_BLACK); + lcd.drawString("Yr Low:", col1X, highLowY + rowHeight * 5); + lcd.drawString(formatPriceUS(btcLowYear, 0, true), col1X + 65, highLowY + rowHeight * 5); + lcd.drawString("Yr Low:", col2X, highLowY + rowHeight * 5); + lcd.drawString(formatPriceUS(goldLowYear, 2, true), col2X + 65, highLowY + rowHeight * 5); + lcd.drawString("Yr Low:", col3X, highLowY + rowHeight * 5); + lcd.drawString(formatPriceUS(silverLowYear, 2, true), col3X + 65, highLowY + rowHeight * 5); + + int sparklineY = highLowY + rowHeight * 6 + 15; + + const char* periodLabels[] = {"D", "W", "M", "Y", "A"}; + int btnWidth = 50; + int btnHeight = 30; + int btnGap = 10; + int totalBtnWidth = 5 * btnWidth + 4 * btnGap; + int btnStartX = (SCREEN_WIDTH - totalBtnWidth) / 2; + + for (int i = 0; i < 5; i++) { + int btnX = btnStartX + i * (btnWidth + btnGap); + uint16_t bgColor = (hmSparklinePeriod == i) ? TFT_ORANGE : TFT_DARKGRAY; + lcd.fillSmoothRoundRect(btnX, sparklineY, btnWidth, btnHeight, 6, bgColor); + lcd.setTextColor(TFT_WHITE, bgColor); + lcd.setFont(&fonts::DejaVu18); + lcd.setTextDatum(textdatum_t::middle_center); + lcd.drawString(periodLabels[i], btnX + btnWidth / 2, sparklineY + btnHeight / 2); + } + + int sparklineDrawY = sparklineY + btnHeight + 15; + int sparklineHeight = 80; + + if (btcHmLoaded && btcHmCount > 1) { + double minVal = btcHmHistory[0]; + double maxVal = btcHmHistory[0]; + for (int i = 0; i < btcHmCount; i++) { + if (btcHmHistory[i] < minVal) minVal = btcHmHistory[i]; + if (btcHmHistory[i] > maxVal) maxVal = btcHmHistory[i]; + } + double range = maxVal - minVal; + if (range < 1) range = 1; + + for (int i = 0; i < btcHmCount - 1; i++) { + int x1 = col1X + (i * colWidth) / (btcHmCount - 1); + int x2 = col1X + ((i + 1) * colWidth) / (btcHmCount - 1); + int y1 = sparklineDrawY + sparklineHeight - (int)((btcHmHistory[i] - minVal) * sparklineHeight / range); + int y2 = sparklineDrawY + sparklineHeight - (int)((btcHmHistory[i + 1] - minVal) * sparklineHeight / range); + lcd.drawLine(x1, y1, x2, y2, TFT_ORANGE); + } + } + + if (goldHmLoaded && goldHmCount > 1) { + double minVal = goldHmHistory[0]; + double maxVal = goldHmHistory[0]; + for (int i = 0; i < goldHmCount; i++) { + if (goldHmHistory[i] < minVal) minVal = goldHmHistory[i]; + if (goldHmHistory[i] > maxVal) maxVal = goldHmHistory[i]; + } + double range = maxVal - minVal; + if (range < 1) range = 1; + + for (int i = 0; i < goldHmCount - 1; i++) { + int x1 = col2X + (i * colWidth) / (goldHmCount - 1); + int x2 = col2X + ((i + 1) * colWidth) / (goldHmCount - 1); + int y1 = sparklineDrawY + sparklineHeight - (int)((goldHmHistory[i] - minVal) * sparklineHeight / range); + int y2 = sparklineDrawY + sparklineHeight - (int)((goldHmHistory[i + 1] - minVal) * sparklineHeight / range); + lcd.drawLine(x1, y1, x2, y2, TFT_GOLD); + } + } + + if (silverHmLoaded && silverHmCount > 1) { + double minVal = silverHmHistory[0]; + double maxVal = silverHmHistory[0]; + for (int i = 0; i < silverHmCount; i++) { + if (silverHmHistory[i] < minVal) minVal = silverHmHistory[i]; + if (silverHmHistory[i] > maxVal) maxVal = silverHmHistory[i]; + } + double range = maxVal - minVal; + if (range < 1) range = 1; + + for (int i = 0; i < silverHmCount - 1; i++) { + int x1 = col3X + (i * colWidth) / (silverHmCount - 1); + int x2 = col3X + ((i + 1) * colWidth) / (silverHmCount - 1); + int y1 = sparklineDrawY + sparklineHeight - (int)((silverHmHistory[i] - minVal) * sparklineHeight / range); + int y2 = sparklineDrawY + sparklineHeight - (int)((silverHmHistory[i + 1] - minVal) * sparklineHeight / range); + lcd.drawLine(x1, y1, x2, y2, TFT_SILVER); + } + } + + lcd.fillSmoothRoundRect(20, SCREEN_HEIGHT - 50, 150, 40, 8, TFT_GRAY); + lcd.setTextColor(TFT_WHITE, TFT_GRAY); + lcd.setFont(&fonts::DejaVu18); + lcd.setTextDatum(textdatum_t::middle_center); + lcd.drawString("BACK", 95, SCREEN_HEIGHT - 30); +} + +bool handleHardMoneyTouch(int32_t x, int32_t y) { + if (x >= 20 && x < 170 && y >= SCREEN_HEIGHT - 50 && y < SCREEN_HEIGHT - 10) { + currentScreen = SCREEN_DASHBOARD; + needsRefresh = true; + return true; + } + + int btnWidth = 50; + int btnHeight = 30; + int btnGap = 10; + int totalBtnWidth = 5 * btnWidth + 4 * btnGap; + int btnStartX = (SCREEN_WIDTH - totalBtnWidth) / 2; + + int headerY = 55; + int priceY = 80; + int highLowY = priceY + 30; + int rowHeight = 22; + int sparklineY = highLowY + rowHeight * 6 + 15; + + for (int i = 0; i < 5; i++) { + int btnX = btnStartX + i * (btnWidth + btnGap); + if (x >= btnX && x < btnX + btnWidth && y >= sparklineY && y < sparklineY + btnHeight) { + if (hmSparklinePeriod != i) { + hmSparklinePeriod = i; + btcHmLoaded = false; + goldHmLoaded = false; + silverHmLoaded = false; + hmNeedsRefresh = true; + } + return true; + } + } + + return false; } void refreshDataAndShow() { @@ -1290,7 +1868,24 @@ void refreshDataAndShow() { } bool handleDashboardTouch(int32_t x, int32_t y) { - if (x >= SCREEN_WIDTH - 160 && x < SCREEN_WIDTH - 20 && y >= SCREEN_HEIGHT - 36 && y < SCREEN_HEIGHT - 4) { + if (x >= SCREEN_WIDTH - 55 && x < SCREEN_WIDTH - 10 && y >= 5 && y < 40) { + displayFlipped = !displayFlipped; + preferences.begin("dashboard", false); + preferences.putBool("flipped", displayFlipped); + preferences.end(); + lcd.setRotation(displayFlipped ? 2 : 0); + needsRefresh = true; + return true; + } + + if (x >= 20 && x < 200 && y >= 0 && y < 45) { + currentScreen = SCREEN_HARD_MONEY_DETAIL; + hmSparklinePeriod = 2; + hmNeedsRefresh = true; + return true; + } + + if (x >= SCREEN_WIDTH - 50 && x < SCREEN_WIDTH - 14 && y >= SCREEN_HEIGHT - 32 && y < SCREEN_HEIGHT - 8) { currentScreen = SCREEN_CUSTOMIZE_STOCKS; selectedStockSlot = -1; keyboardPos = 0; @@ -1322,7 +1917,12 @@ void setup() { lcd.init(); lcd.setBrightness(255); - lcd.setRotation(0); + + preferences.begin("dashboard", true); + displayFlipped = preferences.getBool("flipped", false); + preferences.end(); + + lcd.setRotation(displayFlipped ? 2 : 0); lcd.fillScreen(TFT_WHITE); delay(250); @@ -1417,32 +2017,38 @@ void loop() { case SCREEN_WIFI_CONFIG: handleWifiConfigTouch(x, y); break; + case SCREEN_WIFI_PASSWORD: + handleWifiPasswordTouch(x, y); + break; + case SCREEN_HARD_MONEY_DETAIL: + handleHardMoneyTouch(x, y); + break; + } + + if (!needsRefresh) { + switch (currentScreen) { + case SCREEN_DASHBOARD: + updatePrices(); + lastRefresh = millis(); + drawDashboard(); + break; + case SCREEN_CUSTOMIZE_STOCKS: + drawStockCustomizeScreen(); + break; + case SCREEN_CUSTOMIZE_INDICES: + drawIndexCustomizeScreen(); + break; + case SCREEN_WIFI_CONFIG: + drawWifiConfigScreen(); + break; case SCREEN_WIFI_PASSWORD: - handleWifiPasswordTouch(x, y); + drawWifiPasswordScreen(); + break; + case SCREEN_HARD_MONEY_DETAIL: + drawHardMoneyDetailScreen(); break; } - - if (!needsRefresh) { - switch (currentScreen) { - case SCREEN_DASHBOARD: - updatePrices(); - lastRefresh = millis(); - drawDashboard(); - break; - case SCREEN_CUSTOMIZE_STOCKS: - drawStockCustomizeScreen(); - break; - case SCREEN_CUSTOMIZE_INDICES: - drawIndexCustomizeScreen(); - break; - case SCREEN_WIFI_CONFIG: - drawWifiConfigScreen(); - break; - case SCREEN_WIFI_PASSWORD: - drawWifiPasswordScreen(); - break; - } - } + } delay(300); } @@ -1453,13 +2059,29 @@ void loop() { drawFooterCountdown(); } + if (currentScreen == SCREEN_HARD_MONEY_DETAIL) { + if (hmNeedsRefresh || !btcHmLoaded || !goldHmLoaded || !silverHmLoaded) { + hmNeedsRefresh = false; + drawHardMoneyDetailScreen(); + fetchAllHmData(hmSparklinePeriod); + drawHardMoneyDetailScreen(); + } + if (millis() - hmLastFetch > HM_FETCH_INTERVAL) { + fetchAllHmData(hmSparklinePeriod); + drawHardMoneyDetailScreen(); + } + } + if (wifiConnecting) { - if (WiFi.status() == WL_CONNECTED) { - wifiConnecting = false; - if (currentScreen == SCREEN_DASHBOARD) { - drawDashboard(); - } - } else if (millis() - wifiConnectStartTime > WIFI_CONNECT_TIMEOUT) { + if (WiFi.status() == WL_CONNECTED) { + wifiConnecting = false; + if (!timeSynced) { + syncTime(); + } + if (currentScreen == SCREEN_DASHBOARD) { + drawDashboard(); + } + } else if (millis() - wifiConnectStartTime > WIFI_CONNECT_TIMEOUT) { wifiConnecting = false; if (currentScreen == SCREEN_DASHBOARD) { drawDashboard();