diff --git a/esp32-s3-cliproxy-dashboard/esp32-s3-cliproxy-dashboard.ino b/esp32-s3-cliproxy-dashboard/esp32-s3-cliproxy-dashboard.ino index ad2b084..46fdd30 100644 --- a/esp32-s3-cliproxy-dashboard/esp32-s3-cliproxy-dashboard.ino +++ b/esp32-s3-cliproxy-dashboard/esp32-s3-cliproxy-dashboard.ino @@ -64,6 +64,8 @@ long g_reasoning_tokens= 0; long g_cached_tokens = 0; long g_failed = 0; +long g_tokens30d = 0; // tổng token 30 ngày (range=30d) + AccountStat g_accounts[8]; int g_accountCount = 0; ModelStat g_models[6]; @@ -208,17 +210,47 @@ void tft_showFetchError() { tftUnlock(); } -// ── Footer hiển thị IP để truy cập trang Config ── +// ── Footer hiển thị token 30 ngày + IP để truy cập trang Config ── void drawFooter() { - // Vẽ thanh footer ở 16px dưới cùng - tft.drawFastHLine(0, SCREEN_HEIGHT-17, SCREEN_WIDTH, COLOR_HEADER); - tft.setTextSize(1); tft.setTextColor(COLOR_CYAN); - tft.setCursor(6, SCREEN_HEIGHT-13); + int footerTop = SCREEN_HEIGHT - 32; + tft.drawFastHLine(0, footerTop - 1, SCREEN_WIDTH, COLOR_HEADER); + + // Dòng 1: tổng token 30 ngày + char buf[40]; + tft.setTextSize(1); tft.setTextColor(COLOR_YELLOW); + tft.setCursor(6, footerTop + 3); + tft.print("30d Tokens: "); + tft.setTextColor(COLOR_ORANGE); + fmtNum(g_tokens30d, buf); + tft.print(buf); + + // Dòng 2: IP config + tft.setTextColor(COLOR_CYAN); + tft.setCursor(6, footerTop + 15); tft.print("Config: http://"); tft.setTextColor(COLOR_ORANGE); tft.print(WiFi.localIP().toString()); } +// ── Format số kiểu EU: dấu . phân cách hàng nghìn, không thập phân ── +void fmtNum(long n, char* out) { + char tmp[20]; + bool neg = n < 0; + if (neg) n = -n; + int dig = 0, i = 0; + if (n == 0) { tmp[i++] = '0'; dig = 1; } + while (n > 0) { + if (dig > 0 && dig % 3 == 0) tmp[i++] = '.'; + tmp[i++] = '0' + (n % 10); + n /= 10; + dig++; + } + if (neg) tmp[i++] = '-'; + int j = 0; + while (i > 0) out[j++] = tmp[--i]; + out[j] = '\0'; +} + // ── Vẽ 4 trang dữ liệu ─────────────────────────── void drawPage0() { // Tổng quan @@ -230,13 +262,13 @@ void drawPage0() { tft.setTextSize(1); tft.setTextColor(COLOR_CYAN); tft.setCursor(8,48); tft.print("Requests"); tft.setTextSize(4); tft.setTextColor(COLOR_GREEN); - snprintf(buf,sizeof(buf),"%ld",g_requests); + fmtNum(g_requests, buf); tft.setCursor(8,62); tft.print(buf); tft.setTextSize(1); tft.setTextColor(COLOR_CYAN); tft.setCursor(8,108); tft.print("Failed"); tft.setTextSize(2); tft.setTextColor(g_failed > 0 ? COLOR_RED : COLOR_TEXT); - snprintf(buf,sizeof(buf),"%ld",g_failed); + fmtNum(g_failed, buf); tft.setCursor(8,120); tft.print(buf); // Cột phải: token breakdown @@ -246,32 +278,34 @@ void drawPage0() { tft.setTextSize(1); tft.setTextColor(COLOR_YELLOW); tft.setCursor(x,y); tft.print("Total Tokens:"); y+=14; tft.setTextSize(2); tft.setTextColor(COLOR_ORANGE); - snprintf(buf,sizeof(buf),"%ld",g_total_tokens); + fmtNum(g_total_tokens, buf); tft.setCursor(x,y); tft.print(buf); y+=24; tft.setTextSize(1); tft.setTextColor(COLOR_CYAN); tft.setCursor(x,y); tft.print("Input : "); - tft.setTextColor(COLOR_TEXT); snprintf(buf,sizeof(buf),"%ld",g_input_tokens); tft.println(buf); y+=14; + tft.setTextColor(COLOR_TEXT); fmtNum(g_input_tokens, buf); tft.println(buf); y+=14; tft.setTextColor(COLOR_CYAN); tft.setCursor(x,y); tft.print("Output : "); - tft.setTextColor(COLOR_TEXT); snprintf(buf,sizeof(buf),"%ld",g_output_tokens); tft.println(buf); y+=14; + tft.setTextColor(COLOR_TEXT); fmtNum(g_output_tokens, buf); tft.println(buf); y+=14; tft.setTextColor(COLOR_CYAN); tft.setCursor(x,y); tft.print("Reasoning: "); - tft.setTextColor(COLOR_TEXT); snprintf(buf,sizeof(buf),"%ld",g_reasoning_tokens); tft.println(buf); y+=14; + tft.setTextColor(COLOR_TEXT); fmtNum(g_reasoning_tokens, buf); tft.println(buf); y+=14; tft.setTextColor(COLOR_CYAN); tft.setCursor(x,y); tft.print("Cached : "); - tft.setTextColor(COLOR_TEXT); snprintf(buf,sizeof(buf),"%ld",g_cached_tokens); tft.println(buf); + tft.setTextColor(COLOR_TEXT); fmtNum(g_cached_tokens, buf); tft.println(buf); } void drawPage1() { // Accounts tft.fillScreen(COLOR_BG); drawHeader("Account", 1, TOTAL_PAGES); - int y=48; char buf[48]; + int y=48; char buf[48], nb[16], tb[16]; for (int i=0; i 6 ? g_hourCount - 6 : 0; // 6 giờ gần nhất for (int i=start; i SCREEN_HEIGHT - 28) break; + if (y > SCREEN_HEIGHT - 36) break; } if (g_hourCount == 0) { tft.setTextColor(COLOR_TEXT); tft.setCursor(6,y); tft.print("(khong co du lieu)"); @@ -424,6 +462,47 @@ bool fetchAndParseData() { return true; } +// ═════════════════════════════════════════════════ +// Fetch tổng token 30 ngày (range=30d) +// ═════════════════════════════════════════════════ +void fetch30dTokens() { + // Tạo URL với range=30d từ g_api_url hiện tại + String url30 = String(g_api_url); + int q = url30.indexOf('?'); + if (q >= 0) { + // Có query string -> thay thế/thêm range=30d + int r = url30.indexOf("range="); + if (r >= 0) { + int amp = url30.indexOf('&', r); + if (amp >= 0) url30 = url30.substring(0, r) + "range=30d" + url30.substring(amp); + else url30 = url30.substring(0, r) + "range=30d"; + } else { + url30 += "&range=30d"; + } + } else { + url30 += "?range=30d"; + } + + Serial.printf("[FETCH30] GET %s\n", url30.c_str()); + HTTPClient http; + http.begin(url30); + http.setTimeout(10000); + int code = http.GET(); + if (code != 200) { http.end(); return; } + + String payload = http.getString(); + http.end(); + + DynamicJsonDocument doc(4096); + DeserializationError err = deserializeJson(doc, payload); + if (err) { + Serial.printf("[FETCH30] JSON error: %s\n", err.c_str()); + return; + } + g_tokens30d = doc["summary"]["total_tokens"] | 0L; + Serial.printf("[FETCH30] 30d tokens = %ld\n", g_tokens30d); +} + // ═════════════════════════════════════════════════ // HTML // ═════════════════════════════════════════════════ @@ -606,10 +685,13 @@ void fetch_task(void* pv) { drawCurrentPage(); Serial.println("Đèn Blue"); pixels.setPixelColor(0, pixels.Color(0, 0, 255)); // (Chỉ số LED, R, G, B) - pixels.show(); // Cập nhật hiển thị lên đèn + pixels.show(); // Cập nhật hiển thị lên đèn } else { tft_showFetchError(); } + // Lấy tổng token 30 ngày + fetch30dTokens(); + drawCurrentPage(); } // Nếu có yêu cầu fetch ngay (sau khi đổi API URL), không delay 60s if (g_fetchNow) {