CYBERMON USB CODE
ESP32 + 1.9" ST7789 TFT // USB Serial + Captive Portal
MONITORING MODE
STANDBY MODE
CYBERMON USB SKETCH
cybermon_19inch_USB.ino
/* ╔═══════════════════════════════════════╗ ║ C Y B E R M O N 1.9" ║ ║ ST7789 170x320 TFT (Portrait) ║ ║ ESP32 + Adafruit_ST7789 ║ ╚═══════════════════════════════════════╝ */ #include <Adafruit_GFX.h> #include <Adafruit_ST7789.h> #include <ArduinoJson.h> #include <WiFi.h> #include <WebServer.h> #include <DNSServer.h> #include <Preferences.h> #include <time.h> #include <SPI.h> // ══════════════ DISPLAY PINS ══════════════ #define LCD_MOSI 23 #define LCD_SCLK 18 #define LCD_CS 15 #define LCD_DC 2 #define LCD_RST 4 #define LCD_BLK 32 // ══════════════ WIFI CONFIG PORTAL ══════════════ #define AP_NAME "CyberMon-Setup" Preferences prefs; WebServer *configServer = NULL; DNSServer *dnsServer = NULL; bool portalActive = false; String savedSSID = ""; String savedPass = ""; bool wifiConnected = false; // ══════════════ DISPLAY ══════════════ Adafruit_ST7789 tft = Adafruit_ST7789(LCD_CS, LCD_DC, LCD_RST); #define SW 170 #define SH 320 // ══════════════ CYBERPUNK PALETTE ══════════════ #define NEON_PURPLE 0xA01F #define NEON_BLUE 0x249F #define NEON_CYAN 0x07FF #define NEON_GREEN 0x47E9 #define NEON_PINK 0xF81F #define NEON_ORANGE 0xFD20 #define NEON_RED 0xF800 #define BG_DARK 0x0821 #define BG_PANEL 0x1083 #define BORDER_COLOR 0x3186 #define GRID_COLOR 0x1082 #define TXT_BRIGHT 0xFFFF #define TXT_MED 0xAD75 #define TXT_DIM 0x6B6D #define TXT_DARK 0x39E7 // ══════════════ SENSOR DATA ══════════════ struct SensorData { int cpuTemp, cpuFan, cpuLoad, cpuPower; int gpuTemp, gpuFan, gpuLoad; int gpuPower, gpuVramUsed, gpuVramTotal; int ramUsed, ramTotal; int uptime; int cpuClock, gpuClock; int netDown, netUp; }; SensorData cur = {0}, prev = {0}; bool dataReceived = false; bool firstUpdate = true; // Force-draw all gauges on first data unsigned long lastUpdate = 0; String serialBuffer = ""; bool standbyActive = false; // Dynamic hardware names char cpuShortName[16] = "CPU"; char gpuShortName[16] = "GPU"; char boardShortName[16] = "BOARD"; char prevTimeStr[6] = ""; char prevDateStr[12] = ""; // ══════════════ ARC GAUGE CONSTANTS ══════════════ #define ARC_R 26 // Arc outer radius #define ARC_THICK 5 // Arc band thickness #define ARC_START 135.0f // Start angle (degrees, 0=top, clockwise) #define ARC_END 405.0f // End angle (270° sweep) // Arc centers for side-by-side layout #define CPU_ARC_Y 68 // CPU arc center Y (shifted down to clear label) #define GPU_ARC_Y 210 // GPU arc center Y (shifted down to clear label) #define ARC_LEFT_CX 44 // Left arc (temp) center X #define ARC_RIGHT_CX 126 // Right arc (load) center X #define ARC_CENTER_CX 85 // Centered arc (when only one gauge) // ══════════════ DRAWING HELPERS ══════════════ uint16_t tempColor(int t) { if (t < 50) return NEON_GREEN; if (t < 65) return NEON_BLUE; if (t < 73) return NEON_ORANGE; return NEON_RED; } uint16_t loadColor(int l) { if (l < 40) return NEON_GREEN; if (l < 70) return NEON_CYAN; if (l < 85) return NEON_ORANGE; return NEON_RED; } // Draw a thick arc using filled circles along the arc path // Angles: 0° = top (12 o'clock), increasing = clockwise void fillArc(int cx, int cy, int r, int thickness, float startDeg, float endDeg, uint16_t color) { float rMid = r - thickness / 2.0f; int dotR = thickness / 2; float step = 2.0f; for (float a = startDeg; a <= endDeg; a += step) { float rad = (a - 90.0f) * DEG_TO_RAD; int x = cx + (int)(cosf(rad) * rMid); int y = cy + (int)(sinf(rad) * rMid); tft.fillCircle(x, y, dotR, color); } } // Draw tick marks around arc perimeter void drawArcTicks(int cx, int cy, int r, float startDeg, float endDeg, int count, uint16_t color) { float range = endDeg - startDeg; for (int i = 0; i <= count; i++) { float deg = startDeg + (range * i / count); float rad = (deg - 90.0f) * DEG_TO_RAD; float ca = cosf(rad), sa = sinf(rad); int x1 = cx + (int)(ca * (r + 1)); int y1 = cy + (int)(sa * (r + 1)); int x2 = cx + (int)(ca * (r + 4)); int y2 = cy + (int)(sa * (r + 4)); tft.drawLine(x1, y1, x2, y2, color); } } // Draw glow dot at end of arc void drawGlowDot(int cx, int cy, int r, int thickness, float angleDeg, uint16_t color) { float rMid = r - thickness / 2.0f; float rad = (angleDeg - 90.0f) * DEG_TO_RAD; int dx = cx + (int)(cosf(rad) * rMid); int dy = cy + (int)(sinf(rad) * rMid); tft.fillCircle(dx, dy, 4, color); tft.fillCircle(dx, dy, 2, TXT_BRIGHT); } // Draw a complete arc gauge with value centered inside void drawArcGauge(int cx, int cy, int value, int maxVal, uint16_t color, const char* label, bool showDegree) { // Clear gauge area tft.fillRect(cx - ARC_R - 6, cy - ARC_R - 6, ARC_R * 2 + 12, ARC_R * 2 + 20, BG_DARK); // Tick marks drawArcTicks(cx, cy, ARC_R, ARC_START, ARC_END, 10, TXT_DARK); // Background arc fillArc(cx, cy, ARC_R, ARC_THICK, ARC_START, ARC_END, 0x1082); // Value arc float pct = constrain(value, 0, maxVal) / (float)maxVal; float endA = ARC_START + pct * (ARC_END - ARC_START); if (endA > ARC_START + 2) { fillArc(cx, cy, ARC_R, ARC_THICK, ARC_START, endA, color); // Inner highlight near tip (bright sliver like Mike-Core) if (endA > ARC_START + 20) { fillArc(cx, cy, ARC_R - 1, 3, endA - 15, endA, TXT_BRIGHT); } drawGlowDot(cx, cy, ARC_R, ARC_THICK, endA, color); } // Value text centered char buf[8]; snprintf(buf, sizeof(buf), "%d", value); int len = strlen(buf); tft.setTextSize(2); tft.setTextColor(TXT_BRIGHT); tft.setCursor(cx - len * 6, cy - 7); tft.print(buf); // Unit indicator if (showDegree) { tft.setTextSize(1); tft.setTextColor(color); tft.setCursor(cx + len * 6 + 2, cy - 7); tft.print("o"); } else { tft.setTextSize(1); tft.setTextColor(color); tft.setCursor(cx + len * 6 + 2, cy - 3); tft.print("%"); } // Label below arc tft.setTextSize(1); tft.setTextColor(color); int lw = strlen(label) * 6; tft.setCursor(cx - lw / 2, cy + ARC_R + 6); tft.print(label); } void drawNeonBar(int x, int y, int w, int h, float pct, uint16_t color) { tft.fillRoundRect(x, y, w, h, h / 2, 0x0841); int fw = (int)(w * pct); if (fw > h) { tft.fillRoundRect(x, y, fw, h, h / 2, color); } else if (fw > 2) { tft.fillCircle(x + h / 2, y + h / 2, h / 2, color); } } // ══════════════ BACKGROUND ══════════════ void drawCyberpunkBG() { tft.fillScreen(BG_DARK); for (int y = 0; y < SH; y += 40) tft.drawFastHLine(0, y, SW, GRID_COLOR); for (int x = 0; x < SW; x += 20) tft.drawFastVLine(x, 0, SH, GRID_COLOR); } // ══════════════ HEADER (0-20) ══════════════ void drawHeader() { tft.fillRect(0, 0, SW, 20, BG_PANEL); tft.drawFastHLine(0, 20, SW, NEON_PURPLE); tft.setTextSize(1); tft.setTextColor(NEON_PURPLE); tft.setCursor(4, 3); tft.print("CYBER"); tft.setTextColor(NEON_GREEN); tft.print("MON"); tft.fillCircle(SW - 10, 10, 4, NEON_GREEN); tft.fillCircle(SW - 10, 10, 2, TXT_BRIGHT); tft.setTextColor(TXT_DARK); tft.setCursor(56, 7); tft.print(boardShortName); } // ══════════════ CPU SECTION (22-152) ══════════════ void drawCPUStatic() { tft.setTextSize(1); tft.setTextColor(NEON_PURPLE); tft.setCursor(4, 24); tft.print("CPU"); tft.setTextColor(TXT_DIM); tft.setCursor(24, 24); tft.print(cpuShortName); tft.drawFastHLine(4, 34, SW - 8, BORDER_COLOR); } void updateCPUSection() { char buf[24]; // Temp arc (left) + Load arc (right) // If no temp sensor, center the load arc if (cur.cpuTemp > 0) { if (firstUpdate || cur.cpuTemp != prev.cpuTemp) { drawArcGauge(ARC_LEFT_CX, CPU_ARC_Y, cur.cpuTemp, 100, tempColor(cur.cpuTemp), "TEMP", true); } if (firstUpdate || cur.cpuLoad != prev.cpuLoad) { drawArcGauge(ARC_RIGHT_CX, CPU_ARC_Y, cur.cpuLoad, 100, loadColor(cur.cpuLoad), "LOAD", false); } } else { if (firstUpdate || cur.cpuLoad != prev.cpuLoad) { drawArcGauge(ARC_CENTER_CX, CPU_ARC_Y, cur.cpuLoad, 100, loadColor(cur.cpuLoad), "LOAD", false); } } if (cur.cpuFan != prev.cpuFan || cur.cpuPower != prev.cpuPower) { tft.fillRect(0, 108, SW, 12, BG_DARK); tft.setTextSize(1); tft.setTextColor(NEON_PURPLE); tft.setCursor(4, 109); snprintf(buf, sizeof(buf), "FAN %d", cur.cpuFan); tft.print(buf); tft.setTextColor(TXT_DIM); tft.print("rpm"); tft.setTextColor(NEON_ORANGE); tft.setCursor(108, 109); snprintf(buf, sizeof(buf), "%dW", cur.cpuPower); tft.print(buf); } if (cur.cpuClock != prev.cpuClock) { tft.fillRect(0, 122, SW, 12, BG_DARK); tft.setTextSize(1); tft.setTextColor(NEON_BLUE); tft.setCursor(4, 123); if (cur.cpuClock >= 1000) snprintf(buf, sizeof(buf), "CLK %.2fGHz", cur.cpuClock / 1000.0f); else snprintf(buf, sizeof(buf), "CLK %dMHz", cur.cpuClock); tft.print(buf); } if (cur.ramUsed != prev.ramUsed || cur.ramTotal != prev.ramTotal) { tft.fillRect(0, 136, SW, 20, BG_DARK); tft.setTextSize(1); tft.setTextColor(TXT_DIM); tft.setCursor(4, 136); tft.print("RAM"); tft.setTextColor(TXT_BRIGHT); tft.setCursor(28, 136); if (cur.ramTotal > 0) { snprintf(buf, sizeof(buf), "%.1f/%.0fGB", cur.ramUsed / 1024.0f, cur.ramTotal / 1024.0f); } else { snprintf(buf, sizeof(buf), "---"); } tft.print(buf); float pct = (cur.ramTotal > 0) ? constrain(cur.ramUsed, 0, cur.ramTotal) / (float)cur.ramTotal : 0; drawNeonBar(4, 148, SW - 8, 5, pct, NEON_BLUE); } } // ══════════════ SEPARATOR ══════════════ void drawSeparator() { tft.drawFastHLine(10, 160, SW - 20, NEON_PURPLE); tft.drawFastHLine(10, 161, SW - 20, BORDER_COLOR); } // ══════════════ GPU SECTION (160-290) ══════════════ void drawGPUStatic() { tft.setTextSize(1); tft.setTextColor(NEON_GREEN); tft.setCursor(4, 166); tft.print("GPU"); tft.setTextColor(TXT_DIM); tft.setCursor(24, 166); tft.print(gpuShortName); tft.drawFastHLine(4, 176, SW - 8, BORDER_COLOR); } void updateGPUSection() { char buf[24]; // Temp arc (left) + Load arc (right) // If no temp sensor, center the load arc if (cur.gpuTemp > 0) { if (firstUpdate || cur.gpuTemp != prev.gpuTemp) { drawArcGauge(ARC_LEFT_CX, GPU_ARC_Y, cur.gpuTemp, 100, tempColor(cur.gpuTemp), "TEMP", true); } if (firstUpdate || cur.gpuLoad != prev.gpuLoad) { drawArcGauge(ARC_RIGHT_CX, GPU_ARC_Y, cur.gpuLoad, 100, loadColor(cur.gpuLoad), "LOAD", false); } } else { if (firstUpdate || cur.gpuLoad != prev.gpuLoad) { drawArcGauge(ARC_CENTER_CX, GPU_ARC_Y, cur.gpuLoad, 100, loadColor(cur.gpuLoad), "LOAD", false); } } if (cur.gpuFan != prev.gpuFan || cur.gpuPower != prev.gpuPower) { tft.fillRect(0, 244, SW, 12, BG_DARK); tft.setTextSize(1); tft.setTextColor(NEON_GREEN); tft.setCursor(4, 245); snprintf(buf, sizeof(buf), "FAN %d", cur.gpuFan); tft.print(buf); tft.setTextColor(TXT_DIM); tft.print("rpm"); tft.setTextColor(NEON_PINK); tft.setCursor(108, 245); snprintf(buf, sizeof(buf), "%dW", cur.gpuPower); tft.print(buf); } if (cur.gpuClock != prev.gpuClock) { tft.fillRect(0, 258, SW, 12, BG_DARK); tft.setTextSize(1); tft.setTextColor(NEON_CYAN); tft.setCursor(4, 259); if (cur.gpuClock >= 1000) snprintf(buf, sizeof(buf), "CLK %.2fGHz", cur.gpuClock / 1000.0f); else snprintf(buf, sizeof(buf), "CLK %dMHz", cur.gpuClock); tft.print(buf); } if (cur.gpuVramUsed != prev.gpuVramUsed || cur.gpuVramTotal != prev.gpuVramTotal) { tft.fillRect(0, 272, SW, 20, BG_DARK); tft.setTextSize(1); tft.setTextColor(TXT_DIM); tft.setCursor(4, 272); tft.print("VRAM"); tft.setTextColor(TXT_BRIGHT); tft.setCursor(32, 272); if (cur.gpuVramTotal > 0) { snprintf(buf, sizeof(buf), "%.1f/%.0fGB", cur.gpuVramUsed / 1024.0f, cur.gpuVramTotal / 1024.0f); } else { snprintf(buf, sizeof(buf), "---"); } tft.print(buf); float pct = (cur.gpuVramTotal > 0) ? constrain(cur.gpuVramUsed, 0, cur.gpuVramTotal) / (float)cur.gpuVramTotal : 0; drawNeonBar(4, 284, SW - 8, 5, pct, NEON_CYAN); } } // ══════════════ BOTTOM BAR (290-320) ══════════════ void updateBottomBar() { char buf[24]; int y = 292; tft.fillRect(0, y, SW, SH - y, BG_PANEL); tft.drawFastHLine(0, y, SW, BORDER_COLOR); tft.setTextSize(1); tft.setTextColor(NEON_PURPLE); tft.setCursor(4, y + 4); tft.print("UP"); tft.setTextColor(TXT_BRIGHT); tft.setCursor(20, y + 4); { int h = cur.uptime / 3600; int m = (cur.uptime % 3600) / 60; snprintf(buf, sizeof(buf), "%dh%02dm", h, m); } tft.print(buf); tft.setTextColor(NEON_CYAN); tft.setCursor(4, y + 14); tft.print("NET"); tft.setTextColor(TXT_BRIGHT); tft.setCursor(24, y + 14); if (cur.netDown >= 1024 || cur.netUp >= 1024) snprintf(buf, sizeof(buf), "%.1f/%.1fM", cur.netDown / 1024.0f, cur.netUp / 1024.0f); else snprintf(buf, sizeof(buf), "%d/%dK", cur.netDown, cur.netUp); tft.print(buf); } // ══════════════ STATIC UI ══════════════ void drawStaticUI() { drawCyberpunkBG(); drawHeader(); drawCPUStatic(); drawSeparator(); drawGPUStatic(); } void updateDisplay() { updateCPUSection(); updateGPUSection(); updateBottomBar(); prev = cur; firstUpdate = false; } // ══════════════ WAITING SCREEN ══════════════ void drawWaiting() { drawCyberpunkBG(); tft.setTextSize(2); tft.setTextColor(NEON_PURPLE); tft.setCursor(11, 130); tft.print("CYBER"); tft.setTextColor(NEON_GREEN); tft.print("MON"); tft.drawFastHLine(20, 150, 60, NEON_PURPLE); tft.drawFastHLine(80, 150, 70, NEON_GREEN); tft.fillCircle(150, 150, 3, NEON_GREEN); tft.setTextSize(1); tft.setTextColor(TXT_MED); tft.setCursor(34, 164); tft.print("WAITING..."); tft.setTextColor(TXT_DARK); tft.setCursor(16, 184); tft.print("Connect USB & run"); tft.setCursor(28, 196); tft.print("CyberMon app"); tft.drawRect(8, 122, SW - 16, 88, BORDER_COLOR); } // ══════════════ TERMINAL CLOCK PALETTE ══════════════ #define T_BG 0x0000 #define T_GRID_CLK 0x0120 #define T_DIM_CLK 0x02A4 #define T_MED_CLK 0x0569 #define T_BRIGHT_CLK 0x07E0 #define T_GLOW 0x07F4 #define T_ACCENT 0x04FF #define T_FAINT 0x0182 static int lastClockSec = -1; static uint32_t clockFrame = 0; static int waveIdx = 0; static bool clockBGDrawn = false; void drawClockBG() { tft.fillScreen(T_BG); for (int y = 0; y < SH; y += 16) tft.drawFastHLine(0, y, SW, T_GRID_CLK); for (int x = 0; x < SW; x += 16) tft.drawFastVLine(x, 0, SH, T_GRID_CLK); // Top accent bar + header tft.fillRect(0, 0, SW, 2, T_ACCENT); tft.fillRect(0, 2, SW, 14, T_BG); tft.drawFastHLine(0, 16, SW, T_MED_CLK); tft.setTextSize(1); tft.setTextColor(T_DIM_CLK); tft.setCursor(4, 4); tft.print("CYBER"); tft.setTextColor(T_BRIGHT_CLK); tft.print("MON"); tft.setTextColor(T_ACCENT); tft.setCursor(100, 4); tft.print("STANDBY"); // Waveform box tft.drawRect(4, 20, SW - 8, 48, T_DIM_CLK); tft.setTextSize(1); tft.setTextColor(T_MED_CLK); tft.setCursor(8, 23); tft.print("SIGNAL"); tft.drawFastHLine(8, 32, 40, T_DIM_CLK); tft.drawFastHLine(8, 46, SW - 16, T_FAINT); // Serial info box tft.drawRect(4, 72, SW - 8, 40, T_DIM_CLK); tft.setTextSize(1); tft.setTextColor(T_MED_CLK); tft.setCursor(8, 75); tft.print("CONNECTION"); tft.drawFastHLine(8, 84, 64, T_DIM_CLK); tft.setTextColor(T_DIM_CLK); tft.setCursor(8, 88); tft.print("MODE ........ USB"); tft.setCursor(8, 98); tft.print("BAUD .... 115200"); // System status box tft.drawRect(4, 192, SW - 8, 96, T_DIM_CLK); tft.setTextSize(1); tft.setTextColor(T_MED_CLK); tft.setCursor(8, 195); tft.print("SYSTEM STATUS"); tft.drawFastHLine(8, 204, 80, T_DIM_CLK); tft.setTextColor(T_DIM_CLK); tft.setCursor(8, 210); tft.print("CPU TEMP ..... IDLE"); tft.setCursor(8, 222); tft.print("GPU TEMP ..... IDLE"); tft.setCursor(8, 234); tft.print("SERIAL ... OFFLINE"); tft.setCursor(8, 246); tft.print("DATALINK STANDBY"); tft.setCursor(8, 258); tft.print("NTP ......"); tft.setTextColor(T_ACCENT); tft.print(" ACTIVE"); tft.setTextColor(T_DIM_CLK); tft.setCursor(8, 270); tft.print("UPTIME ..."); // Bottom bar tft.drawFastHLine(0, 296, SW, T_MED_CLK); tft.fillRect(0, 297, SW, 23, T_BG); tft.drawFastHLine(0, 318, SW, T_ACCENT); tft.setTextSize(1); tft.setTextColor(T_DIM_CLK); tft.setCursor(4, 300); tft.print("AWAITING DATALINK"); tft.setTextColor(T_FAINT); tft.setCursor(4, 310); char hwBuf[24]; snprintf(hwBuf, sizeof(hwBuf), "%s // %s", cpuShortName, gpuShortName); tft.print(hwBuf); waveIdx = 0; clockBGDrawn = true; } void drawClockTime(struct tm* t) { int cx = SW / 2; int cy = 145; tft.fillRect(8, 116, SW - 16, 66, T_BG); for (int y = 116; y < 182; y += 16) tft.drawFastHLine(8, y, SW - 16, T_GRID_CLK); for (int x = 16; x < SW; x += 16) tft.drawFastVLine(x, 116, 66, T_GRID_CLK); tft.drawFastHLine(10, 118, 12, T_ACCENT); tft.drawFastVLine(10, 118, 12, T_ACCENT); tft.drawFastHLine(SW - 22, 118, 12, T_ACCENT); tft.drawFastVLine(SW - 11, 118, 12, T_ACCENT); tft.drawFastHLine(10, 178, 12, T_ACCENT); tft.drawFastVLine(10, 167, 12, T_ACCENT); tft.drawFastHLine(SW - 22, 178, 12, T_ACCENT); tft.drawFastVLine(SW - 11, 167, 12, T_ACCENT); char buf[12]; snprintf(buf, sizeof(buf), "%02d:%02d:%02d", t->tm_hour, t->tm_min, t->tm_sec); tft.setTextSize(3); tft.setTextColor(T_GLOW); int tw = strlen(buf) * 18; tft.setCursor(cx - tw / 2, cy - 10); tft.print(buf); tft.drawFastHLine(cx - tw / 2, cy + 16, tw, T_DIM_CLK); static const char* MONTHS[] = {"JAN","FEB","MAR","APR","MAY","JUN","JUL","AUG","SEP","OCT","NOV","DEC"}; char datebuf[16]; snprintf(datebuf, sizeof(datebuf), "%02d %s %d", t->tm_mday, MONTHS[t->tm_mon], t->tm_year + 1900); tft.setTextSize(1); tft.setTextColor(T_MED_CLK); int dw = strlen(datebuf) * 6; tft.setCursor(cx - dw / 2, cy + 22); tft.print(datebuf); static const char* DAYS[] = {"SUNDAY","MONDAY","TUESDAY","WEDNESDAY","THURSDAY","FRIDAY","SATURDAY"}; tft.setTextColor(T_DIM_CLK); int dayW = strlen(DAYS[t->tm_wday]) * 6; tft.setCursor(cx - dayW / 2, cy + 34); tft.print(DAYS[t->tm_wday]); } void clockAnimate() { if ((clockFrame / 10) % 2 == 0) tft.drawFastHLine(0, 0, SW, T_ACCENT); else tft.drawFastHLine(0, 0, SW, T_MED_CLK); int waveW = SW - 20; int wx = 8 + (waveIdx % waveW); tft.drawFastVLine(wx, 34, 30, T_BG); if (wx % 16 == 0) tft.drawFastVLine(wx, 34, 30, T_GRID_CLK); tft.drawPixel(wx, 46, T_FAINT); float phase = clockFrame * 0.1f; int sample = (int)(sinf(phase + wx * 0.06f) * 10.0f); if (random(25) == 0) sample += random(-12, 12); sample = constrain(sample, -12, 12); int py = 46 - sample; tft.drawFastVLine(wx, min(py, 46), abs(sample) + 1, T_BRIGHT_CLK); tft.drawPixel(wx, py, T_GLOW); int curX = 8 + ((waveIdx + 1) % waveW); tft.drawFastVLine(curX, 34, 30, T_MED_CLK); waveIdx++; if (clockFrame % 20 == 0) { unsigned long secs = millis() / 1000; int h = secs / 3600; int m = (secs % 3600) / 60; int s = secs % 60; char uptxt[14]; snprintf(uptxt, sizeof(uptxt), " %02d:%02d:%02d", h, m, s); tft.fillRect(68, 270, 90, 8, T_BG); tft.setTextSize(1); tft.setTextColor(T_DIM_CLK); tft.setCursor(68, 270); tft.print(uptxt); } } void drawStandbyClock() { if (!clockBGDrawn) { lastClockSec = -1; clockFrame = 0; randomSeed(analogRead(0) + millis()); drawClockBG(); } static unsigned long lastFrameMs = 0; unsigned long now = millis(); if (now - lastFrameMs < 50) return; lastFrameMs = now; // Always animate waveform + uptime clockAnimate(); // Draw time if NTP is available struct tm ti; if (getLocalTime(&ti, 0)) { if (ti.tm_sec != lastClockSec) { drawClockTime(&ti); lastClockSec = ti.tm_sec; } } clockFrame++; } // ══════════════ SERIAL PARSING ══════════════ void parseSerial(String line) { JsonDocument doc; DeserializationError err = deserializeJson(doc, line); if (err) return; cur.cpuTemp = doc["ct"] | 0; cur.cpuFan = doc["cf"] | 0; cur.cpuLoad = doc["cl"] | 0; cur.cpuPower = doc["cp"] | 0; cur.gpuTemp = doc["gt"] | 0; cur.gpuFan = doc["gf"] | 0; cur.gpuLoad = doc["gl"] | 0; cur.gpuPower = doc["gp"] | 0; cur.gpuVramUsed = doc["gmu"] | 0; cur.gpuVramTotal = doc["gmt"] | 0; cur.ramUsed = doc["mu"] | 0; cur.ramTotal = doc["mt"] | 0; cur.uptime = doc["up"] | 0; cur.cpuClock = doc["cc"] | 0; cur.gpuClock = doc["gc"] | 0; cur.netDown = doc["nd"] | 0; cur.netUp = doc["nu"] | 0; if (doc.containsKey("cn")) { strncpy(cpuShortName, doc["cn"] | "CPU", sizeof(cpuShortName) - 1); cpuShortName[sizeof(cpuShortName) - 1] = '\0'; } if (doc.containsKey("gn")) { strncpy(gpuShortName, doc["gn"] | "GPU", sizeof(gpuShortName) - 1); gpuShortName[sizeof(gpuShortName) - 1] = '\0'; } if (doc.containsKey("bn")) { strncpy(boardShortName, doc["bn"] | "BOARD", sizeof(boardShortName) - 1); boardShortName[sizeof(boardShortName) - 1] = '\0'; } if (!dataReceived) { dataReceived = true; drawStaticUI(); } lastUpdate = millis(); if (standbyActive) { standbyActive = false; clockBGDrawn = false; prevTimeStr[0] = '\0'; prevDateStr[0] = '\0'; drawStaticUI(); memset(&prev, 0, sizeof(prev)); } updateDisplay(); } // ══════════════ WIFI CONFIG PORTAL ══════════════ const char CONFIG_PAGE[] PROGMEM = R"rawliteral( <!DOCTYPE html><html><head> <meta name="viewport" content="width=device-width,initial-scale=1"> <style> body{font-family:sans-serif;background:#0a0a1a;color:#0ff;text-align:center;padding:20px} h1{color:#a0f;font-size:24px} input{width:80%;padding:12px;margin:8px 0;border:1px solid #0ff;background:#111;color:#0ff;font-size:16px;border-radius:4px} button{width:80%;padding:14px;margin:16px 0;background:#a0f;color:#fff;border:none;font-size:18px;border-radius:4px;cursor:pointer} button:hover{background:#c0f} </style></head><body> <h1>CyberMon WiFi Setup</h1> <p style="color:#666;font-size:14px">WiFi is used for NTP clock sync only</p> <form action="/save" method="POST"> <input name="ssid" placeholder="WiFi Network Name" required><br> <input name="pass" type="password" placeholder="WiFi Password"><br> <button type="submit">Connect</button> </form></body></html> )rawliteral"; const char SAVED_PAGE[] PROGMEM = R"rawliteral( <!DOCTYPE html><html><head> <meta name="viewport" content="width=device-width,initial-scale=1"> <style> body{font-family:sans-serif;background:#0a0a1a;color:#0ff;text-align:center;padding:40px} .ok{color:#0f0;font-size:24px} </style></head><body> <p class="ok">WiFi credentials saved!</p> <p>CyberMon will now restart and connect.</p> </body></html> )rawliteral"; void drawConfigPortal() { drawCyberpunkBG(); tft.setTextSize(2); tft.setTextColor(NEON_PURPLE); tft.setCursor(11, 100); tft.print("CYBER"); tft.setTextColor(NEON_GREEN); tft.print("MON"); tft.setTextSize(1); tft.setTextColor(NEON_CYAN); tft.setCursor(28, 130); tft.print("WiFi Setup Mode"); tft.setTextColor(TXT_BRIGHT); tft.setCursor(10, 160); tft.print("1. Connect to WiFi:"); tft.setTextColor(NEON_GREEN); tft.setCursor(22, 175); tft.print("CyberMon-Setup"); tft.setTextColor(TXT_BRIGHT); tft.setCursor(10, 200); tft.print("2. Open browser to:"); tft.setTextColor(NEON_GREEN); tft.setCursor(22, 215); tft.print("http://192.168.4.1"); tft.setTextColor(TXT_BRIGHT); tft.setCursor(10, 240); tft.print("3. Enter your WiFi"); tft.setCursor(22, 255); tft.print("network credentials"); tft.drawRect(8, 92, SW - 16, 180, BORDER_COLOR); } void startConfigPortal() { portalActive = true; WiFi.mode(WIFI_AP); WiFi.softAP(AP_NAME); delay(500); dnsServer = new DNSServer(); dnsServer->start(53, "*", WiFi.softAPIP()); configServer = new WebServer(80); configServer->on("/", HTTP_GET, []() { configServer->send(200, "text/html", CONFIG_PAGE); }); configServer->on("/save", HTTP_POST, []() { savedSSID = configServer->arg("ssid"); savedPass = configServer->arg("pass"); prefs.begin("cybermon", false); prefs.putString("ssid", savedSSID); prefs.putString("pass", savedPass); prefs.end(); configServer->send(200, "text/html", SAVED_PAGE); delay(2000); ESP.restart(); }); configServer->onNotFound([]() { configServer->sendHeader("Location", "http://192.168.4.1/"); configServer->send(302, "text/plain", ""); }); configServer->begin(); drawConfigPortal(); } bool connectWiFi() { prefs.begin("cybermon", true); savedSSID = prefs.getString("ssid", ""); savedPass = prefs.getString("pass", ""); prefs.end(); if (savedSSID.length() == 0) return false; WiFi.mode(WIFI_STA); WiFi.begin(savedSSID.c_str(), savedPass.c_str()); int attempts = 0; while (WiFi.status() != WL_CONNECTED && attempts < 20) { delay(500); attempts++; } if (WiFi.status() == WL_CONNECTED) { configTime(-5 * 3600, 3600, "pool.ntp.org"); wifiConnected = true; return true; } return false; } // ══════════════ SETUP ══════════════ void setup() { Serial.begin(115200); delay(100); // Init display tft.init(170, 320); tft.fillScreen(ST77XX_BLACK); // Try WiFi for NTP clock if (!connectWiFi()) { startConfigPortal(); Serial.println("Config portal active at 192.168.4.1"); } else { Serial.println("WiFi connected for NTP."); } if (!portalActive) { drawWaiting(); } Serial.println("CYBERMON 1.9\" USB ready."); } // ══════════════ LOOP ══════════════ void loop() { // Handle config portal if (portalActive) { dnsServer->processNextRequest(); configServer->handleClient(); return; } // Read serial data while (Serial.available()) { char c = Serial.read(); if (c == '\n') { serialBuffer.trim(); if (serialBuffer.length() > 0) { parseSerial(serialBuffer); } serialBuffer = ""; } else { serialBuffer += c; } } // Standby clock after 30s of no serial data if (dataReceived && millis() - lastUpdate > 30000) { if (!standbyActive) { standbyActive = true; prevTimeStr[0] = '\0'; prevDateStr[0] = '\0'; } drawStandbyClock(); } // Reconnect WiFi if disconnected (for NTP) if (wifiConnected && WiFi.status() != WL_CONNECTED) { static unsigned long lastReconnect = 0; if (millis() - lastReconnect > 30000) { lastReconnect = millis(); WiFi.reconnect(); } } }
SETUP INSTRUCTIONS
- WiFi (Optional): WiFi is used only for NTP clock sync on the standby screen. On first boot with no saved credentials, a captive portal (
CyberMon-Setup) launches to configure WiFi. - Libraries: Install via Arduino Library Manager:
Adafruit GFX LibraryAdafruit ST7789ArduinoJson
- Board: Select "ESP32 Dev Module" in Arduino IDE.
- USB Serial: The sketch receives JSON sensor data over USB serial at
115200baud. Run the CyberMon desktop app to stream hardware stats. - Standby Mode: After 30 seconds of no serial data, a retro terminal clock with NTP time is displayed automatically.
- Display: 1.9" ST7789 170x320 TFT in portrait orientation. Cyberpunk-themed UI with arc gauges.
WIRING DIAGRAM
| ST7789 Display | ESP32 |
|---|---|
| VCC | 3.3V |
| GND | GND |
| SCL (SCLK) | GPIO 18 |
| SDA (MOSI) | GPIO 23 |
| RES (RST) | GPIO 4 |
| DC | GPIO 2 |
| CS | GPIO 15 |
| BLK (Backlight) | GPIO 32 |