284 lines
8.4 KiB
C++
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();
|
|
}
|