/* * ============================================ * ESP32 Universal TFT UI Renderer * ============================================ * Library: TFT_eSPI, ArduinoJson, WiFi, HTTPClient * Screen: ILI9488 480x320 (Landscape) * * Upload this code ONCE to ESP32. * All UI changes are synced wirelessly from the web editor. * ============================================ */ #include #include #include #include // ===== CONFIGURATION ===== const char* WIFI_SSID = "YOUR_WIFI_SSID"; const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD"; const char* SERVER_URL = "http://YOUR_SERVER_IP/fiberid/api_sync.php?action=latest"; const char* PUSH_URL = "http://YOUR_SERVER_IP/fiberid/api_sync.php?action=push"; const int POLL_INTERVAL = 5000; // ms between polls // ===== GLOBALS ===== TFT_eSPI tft = TFT_eSPI(); TFT_eSprite sprite = TFT_eSprite(&tft); // Double buffer String lastVersionHash = ""; unsigned long lastPollTime = 0; // ===== RGB565 Conversion ===== uint16_t hexToRGB565(const char* hex) { // Skip '#' if present if (hex[0] == '#') hex++; uint32_t val = strtoul(hex, NULL, 16); uint8_t r = (val >> 16) & 0xFF; uint8_t g = (val >> 8) & 0xFF; uint8_t b = val & 0xFF; return ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3); } // ===== Render UI from JSON ===== void renderUI(String jsonPayload) { DynamicJsonDocument doc(32768); // 32KB buffer DeserializationError err = deserializeJson(doc, jsonPayload); if (err) { Serial.print("JSON parse error: "); Serial.println(err.c_str()); return; } // Create sprite for double-buffering (anti-flicker) sprite.createSprite(480, 320); sprite.fillSprite(TFT_BLACK); JsonArray elements = doc["elements"].as(); for (JsonObject item : elements) { const char* type = item["type"] | "unknown"; int x = item["x"] | 0; int y = item["y"] | 0; int w = item["w"] | 0; int h = item["h"] | 0; const char* fillHex = item["fill"] | "#FFFFFF"; uint16_t color = hexToRGB565(fillHex); int radius = item["radius"] | 0; // ---- RECTANGLE ---- if (strcmp(type, "rect") == 0 || strcmp(type, "roundedRect") == 0) { if (radius > 0) { sprite.fillRoundRect(x, y, w, h, radius, color); } else { sprite.fillRect(x, y, w, h, color); } // Stroke const char* strokeHex = item["stroke"] | "transparent"; if (strcmp(strokeHex, "transparent") != 0) { uint16_t strokeColor = hexToRGB565(strokeHex); if (radius > 0) { sprite.drawRoundRect(x, y, w, h, radius, strokeColor); } else { sprite.drawRect(x, y, w, h, strokeColor); } } } // ---- CIRCLE ---- else if (strcmp(type, "circle") == 0) { int rv = item["radius_val"] | (w / 2); int cx = x + rv; int cy = y + rv; sprite.fillCircle(cx, cy, rv, color); } // ---- LINE ---- else if (strcmp(type, "line") == 0) { int x2 = item["x2"] | (x + w); int y2 = item["y2"] | y; sprite.drawLine(x, y, x2, y2, color); } // ---- TEXT / HEADING ---- else if (strcmp(type, "text") == 0 || strcmp(type, "heading") == 0) { const char* text = item["text"] | "Text"; int fontSize = item["fontSize"] | 16; int tftSize = max(1, fontSize / 8); sprite.setTextSize(tftSize); sprite.setTextColor(color); sprite.drawString(text, x, y); } // ---- BUTTON ---- else if (strcmp(type, "button") == 0) { int br = item["radius"] | 8; sprite.fillRoundRect(x, y, w, h, br, color); const char* text = item["text"] | ""; if (strlen(text) > 0) { const char* txtColorHex = item["textColor"] | "#FFFFFF"; uint16_t txtColor = hexToRGB565(txtColorHex); int fontSize = item["fontSize"] | 16; int tftSize = max(1, fontSize / 8); sprite.setTextSize(tftSize); sprite.setTextColor(txtColor); sprite.setTextDatum(MC_DATUM); sprite.drawString(text, x + w/2, y + h/2); sprite.setTextDatum(TL_DATUM); } } // ---- TOGGLE ---- else if (strcmp(type, "toggle") == 0) { sprite.fillRoundRect(x, y, w, h, h/2, color); sprite.fillCircle(x + w - h/2, y + h/2, h/2 - 2, TFT_WHITE); } // ---- SLIDER ---- else if (strcmp(type, "slider") == 0) { int trackY = y + h/2 - 2; sprite.fillRoundRect(x, trackY, w, 4, 2, 0x8410); // gray track int handleX = x + (int)(w * 0.4); sprite.fillCircle(handleX, y + h/2, 8, color); } // ---- PROGRESS BAR ---- else if (strcmp(type, "progressbar") == 0) { sprite.fillRoundRect(x, y, w, h, 4, 0x2104); // dark bg int pct = item["value"] | 60; int pw = w * pct / 100; sprite.fillRoundRect(x, y, pw, h, 4, color); } // ---- IMAGE PLACEHOLDER ---- else if (strcmp(type, "image") == 0) { sprite.drawRect(x, y, w, h, 0x8410); sprite.setTextSize(1); sprite.setTextColor(0x8410); sprite.setTextDatum(MC_DATUM); sprite.drawString("IMG", x + w/2, y + h/2); sprite.setTextDatum(TL_DATUM); } } // Push sprite to display (one shot = no flicker) sprite.pushSprite(0, 0); sprite.deleteSprite(); Serial.println("UI rendered successfully."); } // ===== Poll Server ===== void pollServer() { if (WiFi.status() != WL_CONNECTED) { Serial.println("WiFi not connected."); return; } HTTPClient http; http.begin(SERVER_URL); int httpCode = http.GET(); if (httpCode == HTTP_CODE_OK) { String payload = http.getString(); // Parse just to check version hash DynamicJsonDocument doc(1024); deserializeJson(doc, payload); String newHash = doc["version_hash"].as(); if (newHash != lastVersionHash) { Serial.println("New design detected! Rendering..."); lastVersionHash = newHash; renderUI(payload); } else { Serial.println("No changes detected."); } } else { Serial.printf("HTTP error: %d\n", httpCode); } http.end(); } // ===== SETUP ===== void setup() { Serial.begin(115200); // Init TFT tft.init(); tft.setRotation(1); // Landscape 480x320 tft.fillScreen(TFT_BLACK); tft.setTextColor(TFT_WHITE); tft.setTextSize(2); tft.drawString("Connecting...", 80, 230); // Connect WiFi WiFi.begin(WIFI_SSID, WIFI_PASSWORD); Serial.print("Connecting to WiFi"); int attempts = 0; while (WiFi.status() != WL_CONNECTED && attempts < 30) { delay(500); Serial.print("."); attempts++; } if (WiFi.status() == WL_CONNECTED) { Serial.println("\nWiFi connected!"); Serial.print("IP: "); Serial.println(WiFi.localIP()); tft.fillScreen(TFT_BLACK); tft.drawString("WiFi OK!", 100, 220); tft.drawString(WiFi.localIP().toString().c_str(), 60, 250); delay(1000); } else { Serial.println("\nWiFi FAILED!"); tft.fillScreen(TFT_RED); tft.drawString("WiFi Failed!", 70, 230); } // First poll pollServer(); } // ===== LOOP ===== void loop() { unsigned long now = millis(); if (now - lastPollTime >= POLL_INTERVAL) { lastPollTime = now; pollServer(); } } /* * ============================================= * pushState() — Send current UI to server * ============================================= * Call this after you design the initial UI in setup(). * It serializes all drawn elements and POSTs them to api_sync.php?action=push * so the web editor (design.php) can load and visually redesign them. * * HOW TO USE: * 1. Design your UI in setup() using tft.fillRect(), tft.drawString(), etc. * 2. At the end of setup(), call pushState() with your elements. * 3. Open design.php in browser and click "Pull ESP32" to see your design. * * Example: * pushState("[{\"type\":\"rect\",\"x\":10,\"y\":10,\"w\":100,\"h\":50,\"fill\":\"#FF0000\"}]"); */ void pushState(String elementsJsonArray) { if (WiFi.status() != WL_CONNECTED) return; // Build full design JSON String json = "{\"projectName\":\"ESP32 Design\",\"screens\":[{\"name\":\"Screen 1\",\"bgColor\":\"#000000\",\"objects\":"; json += elementsJsonArray; json += "}]}"; HTTPClient http; http.begin(PUSH_URL); http.addHeader("Content-Type", "application/json"); int httpCode = http.POST(json); if (httpCode == HTTP_CODE_OK) { Serial.println("UI state pushed to server!"); Serial.println(http.getString()); } else { Serial.printf("Push failed, HTTP code: %d\n", httpCode); } http.end(); }