TKK_E32230273/esp32_tft_renderer.ino

284 lines
8.4 KiB
C++

/*
* ============================================
* 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 <WiFi.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <TFT_eSPI.h>
// ===== 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<JsonArray>();
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<String>();
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();
}