CYBERMON WiFi CODE
ESP32 + 1.9" ST7789 TFT // Wireless UDP + Captive Portal
MONITORING MODE
STANDBY MODE
CYBERMON WiFi SKETCH
cybermon_19inch_wifi.ino
/* ╔═══════════════════════════════════════╗ ║ C Y B E R M O N 1.9" WiFi ║ ║ ST7789 170x320 TFT (Portrait) ║ ║ ESP32 + Adafruit_ST7789 ║ ║ Receives data via UDP broadcast ║ ╚═══════════════════════════════════════╝ */ #include <Adafruit_GFX.h> #include <Adafruit_ST7789.h> #include <ArduinoJson.h> #include <WiFi.h> #include <WiFiUdp.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 // ══════════════ DISPLAY ══════════════ Adafruit_ST7789 tft = Adafruit_ST7789(LCD_CS, LCD_DC, LCD_RST); #define SW 170 #define SH 320 // ══════════════ UDP ══════════════ #define UDP_PORT_DEFAULT 5555 int udpPort = UDP_PORT_DEFAULT; WiFiUDP udp; // ══════════════ WIFI CONFIG PORTAL ══════════════ #define AP_NAME "CyberMon-Setup" Preferences prefs; WebServer *configServer = NULL; DNSServer *dnsServer = NULL; bool portalActive = false; String savedSSID = ""; String savedPass = ""; // ══════════════ 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; int lastUpdate = 0; bool standbyActive = false; 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 #define ARC_THICK 5 #define ARC_START 135.0f #define ARC_END 405.0f #define CPU_ARC_Y 68 #define GPU_ARC_Y 210 #define ARC_LEFT_CX 44 #define ARC_RIGHT_CX 126 #define ARC_CENTER_CX 85 // ══════════════ 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; } 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); } } 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); } } 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); } void drawArcGauge(int cx, int cy, int value, int maxVal, uint16_t color, const char* label, bool showDegree) { tft.fillRect(cx - ARC_R - 6, cy - ARC_R - 6, ARC_R * 2 + 12, ARC_R * 2 + 20, BG_DARK); drawArcTicks(cx, cy, ARC_R, ARC_START, ARC_END, 10, TXT_DARK); fillArc(cx, cy, ARC_R, ARC_THICK, ARC_START, ARC_END, 0x1082); 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); 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); } 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); 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("%"); } 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 ══════════════ 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"); // WiFi indicator instead of wired dot tft.setTextColor(NEON_CYAN); tft.setCursor(SW - 24, 3); tft.print("WiFi"); tft.setTextColor(TXT_DARK); tft.setCursor(56, 7); tft.print(boardShortName); } // ══════════════ CPU SECTION ══════════════ 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]; 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 ══════════════ 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]; 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 ══════════════ 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 / STANDBY ══════════════ void drawWaiting() { drawCyberpunkBG(); tft.setTextSize(2); tft.setTextColor(NEON_PURPLE); tft.setCursor(11, 120); tft.print("CYBER"); tft.setTextColor(NEON_GREEN); tft.print("MON"); tft.drawFastHLine(20, 140, 60, NEON_PURPLE); tft.drawFastHLine(80, 140, 70, NEON_GREEN); tft.fillCircle(150, 140, 3, NEON_GREEN); tft.setTextSize(1); tft.setTextColor(NEON_CYAN); tft.setCursor(52, 155); tft.print("WiFi Mode"); tft.setTextColor(TXT_MED); tft.setCursor(22, 175); tft.print("Listening on UDP"); char portBuf[16]; snprintf(portBuf, sizeof(portBuf), "port %d", udpPort); int pw = strlen(portBuf) * 6; tft.setCursor((SW - pw) / 2, 187); tft.print(portBuf); tft.setTextColor(TXT_DARK); tft.setCursor(10, 207); tft.print("Run CyberMon WiFi"); tft.setCursor(34, 219); tft.print("on your PC"); tft.drawRect(8, 112, SW - 16, 120, 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 // Terminal clock state static int lastClockSec = -1; static uint32_t clockFrame = 0; static int waveform[80]; static int waveIdx = 0; static bool clockBGDrawn = false; void drawClockBG() { tft.fillScreen(T_BG); // Grid 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 tft.fillRect(0, 0, SW, 2, T_ACCENT); tft.fillRect(0, 2, SW, 16, T_BG); tft.drawFastHLine(0, 18, SW, T_MED_CLK); // Header 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); // WiFi 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 ....... WiFi"); tft.setCursor(8, 98); // Show RSSI and IP char wifiBuf[28]; if (WiFi.status() == WL_CONNECTED) { snprintf(wifiBuf, sizeof(wifiBuf), "RSSI .. %ddBm", WiFi.RSSI()); tft.print(wifiBuf); tft.setTextColor(T_FAINT); tft.setCursor(8, 108); // will be outside box but visible } else { tft.print("RSSI .. N/A"); } // 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("WiFi ......"); if (WiFi.status() == WL_CONNECTED) { tft.setTextColor(T_ACCENT); tft.print(" ONLINE"); } else { tft.setTextColor(T_DIM_CLK); tft.print(" OFFLINE"); } tft.setTextColor(T_DIM_CLK); 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); for (int i = 0; i < 80; i++) waveform[i] = 0; 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); // Large time HH:MM:SS 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); // Glow line under time tft.drawFastHLine(cx - tw / 2, cy + 16, tw, T_DIM_CLK); // Date: 22 MAR 2026 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); // Day of week 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() { // Pulse top accent bar if ((clockFrame / 10) % 2 == 0) tft.drawFastHLine(0, 0, SW, T_ACCENT); else tft.drawFastHLine(0, 0, SW, T_MED_CLK); // Waveform animation 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++; // Uptime counter if (clockFrame % 20 == 0) { int 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(); } // 20 FPS cap static int lastFrameMs = 0; int now = millis(); if (now - lastFrameMs < 50) return; lastFrameMs = now; struct tm ti; if (!getLocalTime(&ti, 0)) { clockFrame++; return; } // Animate clockAnimate(); // Redraw time when seconds change if (ti.tm_sec != lastClockSec) { drawClockTime(&ti); lastClockSec = ti.tm_sec; } clockFrame++; } // ══════════════ 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} .ok{color:#0f0;font-size:20px} </style></head><body> <h1>CyberMon WiFi Setup</h1> <form action="/save" method="POST"> <input name="ssid" placeholder="WiFi Network Name" required><br> <input name="pass" type="password" placeholder="WiFi Password"><br> <input name="port" type="number" placeholder="UDP Port (default 5555)" min="1024" max="65535"><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 connect to your network.</p> <p>This page will close automatically.</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"); String portStr = configServer->arg("port"); if (portStr.length() > 0) { udpPort = portStr.toInt(); if (udpPort < 1024 || udpPort > 65535) udpPort = UDP_PORT_DEFAULT; } // Save to flash prefs.begin("cybermon", false); prefs.putString("ssid", savedSSID); prefs.putString("pass", savedPass); prefs.putInt("port", udpPort); prefs.end(); configServer->send(200, "text/html", SAVED_PAGE); delay(2000); // Restart to connect with new creds ESP.restart(); }); // Captive portal: redirect all requests to config page 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", ""); udpPort = prefs.getInt("port", UDP_PORT_DEFAULT); prefs.end(); if (savedSSID.length() == 0) return false; // Show connecting screen drawCyberpunkBG(); tft.setTextSize(1); tft.setTextColor(NEON_CYAN); tft.setCursor(28, 150); tft.print("Connecting to:"); tft.setTextColor(NEON_GREEN); tft.setCursor(28, 165); tft.print(savedSSID); WiFi.mode(WIFI_STA); WiFi.begin(savedSSID.c_str(), savedPass.c_str()); int attempts = 0; while (WiFi.status() != WL_CONNECTED && attempts < 30) { delay(500); attempts++; tft.fillCircle(28 + attempts * 4, 185, 2, NEON_CYAN); } if (WiFi.status() == WL_CONNECTED) { // Setup NTP for clock configTime(-5 * 3600, 3600, "pool.ntp.org"); tft.setTextColor(NEON_GREEN); tft.setCursor(28, 200); tft.print("Connected!"); tft.setCursor(28, 215); char ipBuf[20]; snprintf(ipBuf, sizeof(ipBuf), "IP: %s", WiFi.localIP().toString().c_str()); tft.print(ipBuf); delay(1500); return true; } return false; } // ══════════════ UDP PARSING ══════════════ void parseUDP(char* payload) { JsonDocument doc; DeserializationError err = deserializeJson(doc, payload); 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)); firstUpdate = true; } updateDisplay(); } // ══════════════ SETUP ══════════════ void setup() { Serial.begin(115200); delay(100); tft.init(170, 320); tft.fillScreen(ST77XX_BLACK); // Try connecting with saved WiFi credentials if (connectWiFi()) { // Connected — start listening for UDP udp.begin(udpPort); drawWaiting(); Serial.println("CyberMon WiFi ready. Listening on UDP port " + String(udpPort)); } else { // No creds or connection failed — start config portal startConfigPortal(); Serial.println("Config portal active at 192.168.4.1"); } } // ══════════════ LOOP ══════════════ void loop() { // Handle config portal if (portalActive) { dnsServer->processNextRequest(); configServer->handleClient(); return; } // Check for UDP packets int packetSize = udp.parsePacket(); if (packetSize > 0) { char buf[1024]; int len = udp.read(buf, sizeof(buf) - 1); if (len > 0) { buf[len] = '\0'; parseUDP(buf); } } // Standby clock after 30s of no data if (dataReceived && millis() - lastUpdate > 30000) { if (!standbyActive) { standbyActive = true; prevTimeStr[0] = '\0'; prevDateStr[0] = '\0'; } drawStandbyClock(); } // Reconnect WiFi if disconnected if (WiFi.status() != WL_CONNECTED) { static int lastReconnect = 0; if (millis() - lastReconnect > 10000) { lastReconnect = millis(); WiFi.reconnect(); } } }
SETUP INSTRUCTIONS
- WiFi Setup: On first boot, the ESP32 creates a hotspot called
CyberMon-Setup. Connect to it and openhttp://192.168.4.1to enter your WiFi credentials. They are stored in flash and persist across reboots. - Libraries: Install via Arduino Library Manager:
Adafruit GFX LibraryAdafruit ST7789ArduinoJson
- Board: Select "ESP32 Dev Module" in Arduino IDE.
- UDP Port: Default is
5555. Configurable via the captive portal. Run the CyberMon WiFi sender on your PC to broadcast sensor data. - Standby Mode: After 30 seconds without data, the display switches to a retro terminal clock with NTP time sync.
- Display: 1.9" ST7789 TFT 170x320, portrait orientation. Connects via SPI.
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 |