diff --git a/extra_scripts/build_framework_libs.py b/extra_scripts/build_framework_libs.py new file mode 100644 index 0000000..a0620e3 --- /dev/null +++ b/extra_scripts/build_framework_libs.py @@ -0,0 +1,37 @@ +""" +Pre-build script for Kode Dot FTP Explorer. + +Resolves ESP32 Arduino Core 3.x framework library paths dynamically and +compiles framework libraries that PlatformIO's LDF doesn't auto-discover +(Network, FFat). Also adds include paths for all framework libraries +needed by SimpleFTPServer. +""" +Import("env") +import os + +framework_dir = env.PioPlatform().get_package_dir("framework-arduinoespressif32") +libs_dir = os.path.join(framework_dir, "libraries") + +# Framework libraries whose include paths are needed by SimpleFTPServer +# and our code (some may already be discovered by LDF — adding extras is harmless) +INCLUDE_LIBS = [ + "Network", "WiFi", "FS", "SD_MMC", "SD", "SPI", + "FFat", "SPIFFS", "LittleFS", +] + +for lib_name in INCLUDE_LIBS: + inc_dir = os.path.join(libs_dir, lib_name, "src") + if os.path.isdir(inc_dir): + env.Append(CPPPATH=[inc_dir]) + +# These libraries are NOT auto-discovered by PlatformIO's LDF +# and must be explicitly compiled +COMPILE_LIBS = ["Network", "FFat"] + +for lib_name in COMPILE_LIBS: + src_dir = os.path.join(libs_dir, lib_name, "src") + if os.path.isdir(src_dir): + env.BuildSources( + os.path.join("$BUILD_DIR", "Framework" + lib_name), + src_dir, + ) diff --git a/platformio.ini b/platformio.ini index 89c5efc..ce014f1 100644 --- a/platformio.ini +++ b/platformio.ini @@ -8,8 +8,9 @@ ; Please visit documentation for the other options and examples ; https://docs.platformio.org/page/projectconf.html ; ============================================================== -; BaseApp for Kode Dot (ESP32‑S3) -; Builds Arduino firmware and flashes app only (keeps bootloader/partitions). +; FTP Explorer for Kode Dot (ESP32‑S3) +; FTP server serving the microSD card over Wi-Fi. +; Flashes app only (keeps bootloader/partitions). ; ============================================================== [platformio] ; Default environment @@ -24,7 +25,7 @@ board = kode_dot framework = arduino lib_compat_mode = off monitor_speed = 115200 -lib_ldf_mode = chain+ +lib_ldf_mode = deep+ ; Serial monitor filters monitor_filters = time, esp32_exception_decoder ; Build flags @@ -35,6 +36,8 @@ build_flags = -Wl,--wrap=esp_ota_mark_app_valid_cancel_rollback ; Enable LVGL simple include -DLV_CONF_INCLUDE_SIMPLE + ; FTP server storage type: SD_MMC + -DDEFAULT_STORAGE_TYPE_ESP32=STORAGE_SD_MMC ; Suppress deprecation warnings -Wno-deprecated-declarations ; Allow #warning without failing build @@ -42,24 +45,20 @@ build_flags = ; Suppress narrowing conversion warnings -Wno-narrowing ; Pre-build scripts -extra_scripts = pre:extra_scripts/auto_port.py, pre:extra_scripts/rename_bin.py +extra_scripts = pre:extra_scripts/auto_port.py, pre:extra_scripts/rename_bin.py, pre:extra_scripts/build_framework_libs.py upload_protocol = custom -upload_port = auto -monitor_port = auto +upload_port = COM10 +monitor_port = COM10 upload_speed = 460800 ; Flash app at 0x400000 (preserve bootloader/partitions) upload_command = esptool --chip esp32s3 --port ${UPLOAD_PORT} --baud ${UPLOAD_SPEED} write-flash --flash-freq 80m --flash-mode dio --flash-size 32MB 0x400000 "${BUILD_DIR}/${PROGNAME}.bin" ; Libraries lib_deps = - adafruit/Adafruit BusIO @ ^1.15.0 - adafruit/Adafruit Unified Sensor @ ^1.1.4 moononournation/GFX Library for Arduino @ ^1.6.0 lvgl/lvgl @ ^8.3.9 adafruit/Adafruit NeoPixel https://github.com/RobTillaart/TCA9555.git#0.4.3 - adafruit/Adafruit LSM6DS @ ^4.7.4 - adafruit/Adafruit LIS2MDL @ ^2.1.8 - adafruit/Adafruit MAX1704X @ ^1.0.3 https://github.com/bitbank2/bb_captouch.git - https://github.com/andhieSetyabudi/BQ25896.git + https://github.com/xreef/SimpleFTPServer.git + bblanchon/ArduinoJson @ ^7 diff --git a/src/lv_conf.h b/src/lv_conf.h index eec74dd..7444848 100644 --- a/src/lv_conf.h +++ b/src/lv_conf.h @@ -9,7 +9,7 @@ /* Memory settings */ #define LV_MEM_CUSTOM 0 -#define LV_MEM_SIZE (64U * 1024U) +#define LV_MEM_SIZE (48U * 1024U) /* HAL settings */ #define LV_DISP_DEF_REFR_PERIOD 30 @@ -17,41 +17,48 @@ /* Feature usage */ #define LV_USE_ANIMATION 1 -#define LV_USE_SHADOW 1 -#define LV_USE_BLEND_MODES 1 -#define LV_USE_OPA_SCALE 1 -#define LV_USE_IMG_TRANSFORM 1 +#define LV_USE_SHADOW 0 +#define LV_USE_BLEND_MODES 0 +#define LV_USE_OPA_SCALE 0 +#define LV_USE_IMG_TRANSFORM 0 /* Widget usage */ -#define LV_USE_ARC 1 -#define LV_USE_ANIMIMG 1 +#define LV_USE_ARC 0 +#define LV_USE_ANIMIMG 0 #define LV_USE_BAR 1 -#define LV_USE_BTN 1 -#define LV_USE_BTNMATRIX 1 -#define LV_USE_CANVAS 1 -#define LV_USE_CHECKBOX 1 -#define LV_USE_DROPDOWN 1 -#define LV_USE_IMG 1 +#define LV_USE_BTN 0 +#define LV_USE_BTNMATRIX 0 +#define LV_USE_CANVAS 0 +#define LV_USE_CHECKBOX 0 +#define LV_USE_DROPDOWN 0 +#define LV_USE_IMG 0 #define LV_USE_LABEL 1 #define LV_USE_LINE 1 -#define LV_USE_LIST 1 -#define LV_USE_METER 1 -#define LV_USE_MSGBOX 1 -#define LV_USE_ROLLER 1 -#define LV_USE_SLIDER 1 -#define LV_USE_SPAN 1 -#define LV_USE_SPINBOX 1 -#define LV_USE_SPINNER 1 -#define LV_USE_SWITCH 1 -#define LV_USE_TEXTAREA 1 -#define LV_USE_TABLE 1 -#define LV_USE_TABVIEW 1 -#define LV_USE_TILEVIEW 1 -#define LV_USE_WIN 1 +#define LV_USE_LIST 0 +#define LV_USE_METER 0 +#define LV_USE_MSGBOX 0 +#define LV_USE_ROLLER 0 +#define LV_USE_SLIDER 0 +#define LV_USE_SPAN 0 +#define LV_USE_SPINBOX 0 +#define LV_USE_SPINNER 0 +#define LV_USE_SWITCH 0 +#define LV_USE_TEXTAREA 0 +#define LV_USE_TABLE 0 +#define LV_USE_TABVIEW 0 +#define LV_USE_TILEVIEW 0 +#define LV_USE_WIN 0 +#define LV_USE_KEYBOARD 0 +#define LV_USE_MENU 0 +#define LV_USE_COLORWHEEL 0 +#define LV_USE_LED 0 +#define LV_USE_IMGBTN 0 +#define LV_USE_CALENDAR 0 +#define LV_USE_CHART 0 /* Themes */ #define LV_USE_THEME_DEFAULT 1 -#define LV_USE_THEME_BASIC 1 +#define LV_USE_THEME_BASIC 0 /* Font usage */ #define LV_FONT_MONTSERRAT_14 1 @@ -59,11 +66,11 @@ #define LV_FONT_MONTSERRAT_22 1 #define LV_FONT_MONTSERRAT_26 1 #define LV_FONT_MONTSERRAT_30 1 -#define LV_FONT_MONTSERRAT_34 1 -#define LV_FONT_MONTSERRAT_38 1 +#define LV_FONT_MONTSERRAT_34 0 +#define LV_FONT_MONTSERRAT_38 0 #define LV_FONT_MONTSERRAT_42 1 -#define LV_FONT_MONTSERRAT_46 1 -#define LV_FONT_MONTSERRAT_48 1 +#define LV_FONT_MONTSERRAT_46 0 +#define LV_FONT_MONTSERRAT_48 0 /* Others */ #define LV_USE_PERF_MONITOR 0 diff --git a/src/main.cpp b/src/main.cpp index 5582000..22d07d9 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,188 +1,388 @@ +/** + * @file main.cpp + * @brief Kode Dot FTP Explorer — ESP32-S3 FTP server serving microSD over Wi-Fi + * + * Reads Wi-Fi credentials from /sdcard/Wi-Fi.json, connects to Wi-Fi, + * syncs time via NTP (Vienna timezone), and serves the SD card over FTP. + * Status is shown on the AMOLED display via LVGL and on the NeoPixel LED. + */ + #include +#include #include #include -#include #include +#include #include +#include "SD_MMC.h" +#include "FS.h" +#include +#include +#include +#include "ui.h" -// Display manager instance +// --------------------------------------------------------------------------- +// Global objects +// --------------------------------------------------------------------------- + +// Display manager (LVGL + panel + touch) DisplayManager display; -// UI labels -static lv_obj_t *touch_label; -static lv_obj_t *button_label; - -// IO Expander +// IO Expander (TCA9555) static TCA9555 ioexp(IOEXP_I2C_ADDR); -// NeoPixel +// NeoPixel status LED static Adafruit_NeoPixel pixel(NEO_PIXEL_COUNT, NEO_PIXEL_PIN, NEO_GRB + NEO_KHZ800); -// Forward declarations -void createUserInterface(); -void createFontExamples(lv_obj_t *parent); -void updateTouchDisplay(); -void updateButtonDisplay(); +// FTP server +FtpServer ftpSrv; + +// --------------------------------------------------------------------------- +// LED colour definitions +// --------------------------------------------------------------------------- +static const uint32_t COLOR_OFF = 0x000000; +static const uint32_t COLOR_GREEN = Adafruit_NeoPixel::Color(0, 60, 0); // Wi-Fi OK, FTP idle +static const uint32_t COLOR_BLUE = Adafruit_NeoPixel::Color(0, 0, 60); // FTP client connected +static const uint32_t COLOR_PURPLE = Adafruit_NeoPixel::Color(40, 0, 60); // File transfer +static const uint32_t COLOR_RED = Adafruit_NeoPixel::Color(60, 0, 0); // Error + +// --------------------------------------------------------------------------- +// State tracking +// --------------------------------------------------------------------------- +static bool sd_ok = false; +static bool wifi_connected = false; +static bool ftp_client_on = false; +static bool ftp_transfer = false; +static unsigned long last_time_update = 0; + +// --------------------------------------------------------------------------- +// NeoPixel helpers +// --------------------------------------------------------------------------- + +static void setLed(uint32_t color) { + pixel.setPixelColor(0, color); + pixel.show(); +} + +/** Refresh LED colour based on current state */ +static void updateLed() { + if (ftp_transfer) { + setLed(COLOR_PURPLE); + } else if (ftp_client_on) { + setLed(COLOR_BLUE); + } else if (wifi_connected && sd_ok) { + setLed(COLOR_GREEN); + } else if (!sd_ok || !wifi_connected) { + setLed(COLOR_RED); + } else { + setLed(COLOR_OFF); + } +} + +// --------------------------------------------------------------------------- +// SD card helpers +// --------------------------------------------------------------------------- + +/** Get used / total space on SD card in MiB and push to UI */ +static void updateSdInfo() { + if (!sd_ok) return; + uint64_t total = SD_MMC.totalBytes() / (1024ULL * 1024ULL); + uint64_t used = SD_MMC.usedBytes() / (1024ULL * 1024ULL); + ui_set_sd(used, total); +} + +// --------------------------------------------------------------------------- +// FTP callbacks (SimpleFTPServer v3.x) +// --------------------------------------------------------------------------- + +/** + * Called on FTP connect / disconnect events. + */ +void ftpCallback(FtpOperation ftpOperation, uint32_t freeSpace, uint32_t totalSpace) { + switch (ftpOperation) { + case FTP_CONNECT: + Serial.println("[FTP] Client connected"); + ftp_client_on = true; + ftp_transfer = false; + ui_set_ftp("Connected"); + break; + case FTP_DISCONNECT: + Serial.println("[FTP] Client disconnected"); + ftp_client_on = false; + ftp_transfer = false; + ui_set_ftp("Idle"); + break; + case FTP_FREE_SPACE_CHANGE: + Serial.printf("[FTP] Free space: %u / %u\n", freeSpace, totalSpace); + updateSdInfo(); + break; + default: + break; + } + updateLed(); +} + +/** + * Called during file transfer operations. + */ +void ftpTransferCallback(FtpTransferOperation ftpOperation, const char *name, uint32_t transferredSize) { + switch (ftpOperation) { + case FTP_UPLOAD_START: + Serial.printf("[FTP] Upload start: %s\n", name); + ftp_transfer = true; + ui_set_ftp("Transferring"); + break; + case FTP_DOWNLOAD_START: + Serial.printf("[FTP] Download start: %s\n", name); + ftp_transfer = true; + ui_set_ftp("Transferring"); + break; + case FTP_UPLOAD: + case FTP_DOWNLOAD: + break; // Ongoing — silent to avoid serial flood + case FTP_TRANSFER_STOP: + Serial.printf("[FTP] Transfer done: %s (%u bytes)\n", name, transferredSize); + ftp_transfer = false; + ui_set_ftp(ftp_client_on ? "Connected" : "Idle"); + updateSdInfo(); + break; + case FTP_TRANSFER_ERROR: + Serial.printf("[FTP] Transfer error: %s\n", name); + ftp_transfer = false; + ui_set_ftp(ftp_client_on ? "Connected" : "Idle"); + break; + default: + break; + } + updateLed(); +} + +// --------------------------------------------------------------------------- +// Time display +// --------------------------------------------------------------------------- + +/** Update the UI time label once per second */ +static void updateTimeDisplay() { + unsigned long now = millis(); + if (now - last_time_update < 1000) return; + last_time_update = now; + + struct tm timeinfo; + if (getLocalTime(&timeinfo, 0)) { + char buf[16]; + strftime(buf, sizeof(buf), "%H:%M:%S", &timeinfo); + ui_set_time(buf); + } +} + +// --------------------------------------------------------------------------- +// Initialisation helpers +// --------------------------------------------------------------------------- + +/** Mount SD card via SDMMC 1-bit mode. Returns true on success. */ +static bool initSdCard() { + Serial.println("[SD] Mounting SD card (SDMMC 1-bit)..."); + SD_MMC.setPins(SD_PIN_CLK, SD_PIN_CMD, SD_PIN_D0); + if (!SD_MMC.begin(SD_MOUNT_POINT, true)) { // true = 1-bit mode + Serial.println("[SD] ERROR: Mount failed"); + return false; + } + + uint8_t cardType = SD_MMC.cardType(); + if (cardType == CARD_NONE) { + Serial.println("[SD] ERROR: No card detected"); + return false; + } + + const char *typeStr = (cardType == CARD_MMC) ? "MMC" : + (cardType == CARD_SD) ? "SD" : + (cardType == CARD_SDHC) ? "SDHC" : "Unknown"; + uint64_t totalMB = SD_MMC.totalBytes() / (1024ULL * 1024ULL); + uint64_t usedMB = SD_MMC.usedBytes() / (1024ULL * 1024ULL); + Serial.printf("[SD] Card type: %s | %llu MB used / %llu MB total\n", typeStr, usedMB, totalMB); + return true; +} + +/** + * Read Wi-Fi credentials from /sdcard/Wi-Fi.json and attempt connection. + * JSON format: [{"ssid":"SSID","pass":"PASSWORD"}, ...] + * Returns true when connected. + */ +static bool initWiFi() { + Serial.println("[WiFi] Reading credentials from /sdcard/Wi-Fi.json..."); + + File file = SD_MMC.open("/Wi-Fi.json", FILE_READ); + if (!file) { + Serial.println("[WiFi] ERROR: Cannot open Wi-Fi.json"); + return false; + } + + String json = file.readString(); + file.close(); + + JsonDocument doc; + DeserializationError err = deserializeJson(doc, json); + if (err) { + Serial.printf("[WiFi] ERROR: JSON parse failed: %s\n", err.c_str()); + return false; + } + + JsonArray networks = doc.as(); + if (networks.size() == 0) { + Serial.println("[WiFi] ERROR: No networks in Wi-Fi.json"); + return false; + } + + WiFi.mode(WIFI_STA); + + for (JsonObject net : networks) { + const char *ssid = net["ssid"]; + const char *pass = net["pass"]; + if (!ssid) continue; + + Serial.printf("[WiFi] Trying \"%s\" ...\n", ssid); + WiFi.begin(ssid, pass); + + unsigned long start = millis(); + while (WiFi.status() != WL_CONNECTED && millis() - start < 20000) { + delay(250); + Serial.print("."); + } + Serial.println(); + + if (WiFi.status() == WL_CONNECTED) { + Serial.printf("[WiFi] Connected to \"%s\" IP: %s\n", ssid, WiFi.localIP().toString().c_str()); + ui_set_wifi(ssid, WiFi.localIP().toString().c_str()); + return true; + } + + Serial.printf("[WiFi] Failed to connect to \"%s\"\n", ssid); + WiFi.disconnect(true); + delay(200); + } + + Serial.println("[WiFi] ERROR: Could not connect to any network"); + return false; +} + +/** Configure NTP with Vienna timezone and wait briefly for sync */ +static void initNtp() { + Serial.println("[NTP] Configuring time (Vienna CET/CEST)..."); + configTzTime("CET-1CEST,M3.5.0,M10.5.0/3", "pool.ntp.org", "time.google.com"); + + struct tm timeinfo; + for (int i = 0; i < 10; i++) { + if (getLocalTime(&timeinfo, 500)) { + char buf[32]; + strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &timeinfo); + Serial.printf("[NTP] Time synced: %s\n", buf); + return; + } + } + Serial.println("[NTP] Warning: time not yet synced (will update in background)"); +} + +/** Start FTP server with callbacks */ +static void initFtp() { + Serial.println("[FTP] Starting FTP server (user: kode, port: 21)..."); + ftpSrv.setCallback(ftpCallback); + ftpSrv.setTransferCallback(ftpTransferCallback); + ftpSrv.begin("kode", "kode"); + Serial.println("[FTP] FTP server ready"); + ui_set_ftp("Idle"); +} + +/** Check SD card presence via IO expander (best-effort) */ +static bool checkSdPresence() { + if (!ioexp.begin(INPUT)) { + Serial.println("[IO] Warning: IO Expander not responding — skipping SD detect"); + return true; // Assume present if expander unavailable + } + uint8_t val = ioexp.read1(EXPANDER_SD_CD); + bool present = (val == LOW); // Card detect is active-low + Serial.printf("[IO] SD card detect pin: %s\n", present ? "PRESENT" : "NOT PRESENT"); + return present; +} + +// --------------------------------------------------------------------------- +// Arduino setup +// --------------------------------------------------------------------------- void setup() { Serial.begin(115200); - Serial.println("Starting Base Project with LVGL..."); + delay(100); + Serial.println(); + Serial.println("==================================="); + Serial.println(" Kode Dot FTP Explorer"); + Serial.println("==================================="); - // Initialize display subsystem + // NeoPixel off during init + pixel.begin(); + pixel.setBrightness(30); + setLed(COLOR_OFF); + + // Initialise display + LVGL if (!display.init()) { - Serial.println("Error: Failed to initialize display"); - while(1) { - delay(1000); + Serial.println("[DISP] FATAL: Display init failed"); + setLed(COLOR_RED); + while (1) delay(1000); + } + Serial.println("[DISP] Display ready"); + + // Create the LVGL status UI + ui_init(); + + // IO Expander — check SD card presence + if (!checkSdPresence()) { + Serial.println("[SD] WARNING: SD card not detected by IO expander"); + ui_set_error("No SD card detected"); + setLed(COLOR_RED); + } + + // Mount SD card + sd_ok = initSdCard(); + if (!sd_ok) { + ui_set_error("SD card mount failed"); + setLed(COLOR_RED); + Serial.println("[MAIN] Halting — SD card required"); + while (1) { + display.update(); + delay(100); } } + updateSdInfo(); - // Create the UI - createUserInterface(); - - // Initialize IO Expander (inputs) - if (!ioexp.begin(INPUT)) { - Serial.println("Warning: IO Expander not connected"); + // Connect to Wi-Fi + wifi_connected = initWiFi(); + if (!wifi_connected) { + ui_set_error("Wi-Fi connection failed"); + setLed(COLOR_RED); + Serial.println("[MAIN] Halting — Wi-Fi required"); + while (1) { + display.update(); + delay(100); + } } + updateLed(); - // Configure TOP button (GPIO with external pull-up) - pinMode(BUTTON_TOP, INPUT); + // Sync NTP time + initNtp(); - // Initialize NeoPixel - pixel.begin(); - pixel.setBrightness(64); - pixel.clear(); - pixel.show(); + // Start FTP server + initFtp(); - Serial.println("System ready!"); + Serial.println("[MAIN] Setup complete — entering main loop"); + Serial.printf("[MAIN] Connect FTP client to %s:21 (user: kode / pass: kode)\n", + WiFi.localIP().toString().c_str()); } +// --------------------------------------------------------------------------- +// Arduino loop +// --------------------------------------------------------------------------- + void loop() { - // Pump display subsystem + ftpSrv.handleFTP(); display.update(); - - // Update touch coordinates on the UI - updateTouchDisplay(); - updateButtonDisplay(); - + updateTimeDisplay(); delay(5); -} - -/** - * @brief Create the main user interface - */ -void createUserInterface() { - lv_obj_t * scr = lv_scr_act(); - lv_obj_set_style_bg_color(scr, lv_color_hex(0x111111), 0); - - // Title - lv_obj_t * title = lv_label_create(scr); - lv_obj_set_style_text_font(title, &lv_font_montserrat_30, 0); - lv_label_set_text(title, "Base Project"); - lv_obj_set_style_text_color(title, lv_color_hex(0xFFFFFF), 0); - lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 30); - - // Label for touch coordinates - touch_label = lv_label_create(scr); - lv_obj_set_style_text_font(touch_label, &lv_font_montserrat_22, 0); - lv_label_set_text(touch_label, "Touch: (-, -)"); - lv_obj_set_style_text_color(touch_label, lv_color_hex(0x00FF00), 0); - lv_obj_align(touch_label, LV_ALIGN_CENTER, 0, 0); - - // Label for buttons state (expander) - button_label = lv_label_create(scr); - lv_obj_set_style_text_font(button_label, &lv_font_montserrat_18, 0); - lv_label_set_text(button_label, "Button: none"); - lv_obj_set_style_text_color(button_label, lv_color_hex(0x66CCFF), 0); - lv_obj_align(button_label, LV_ALIGN_BOTTOM_MID, 0, -10); - - // Ejemplos de diferentes fuentes - createFontExamples(scr); -} - -/** - * @brief Create sample labels using different font sizes - */ -void createFontExamples(lv_obj_t *parent) { - // Small font - lv_obj_t * font_small = lv_label_create(parent); - lv_obj_set_style_text_font(font_small, &lv_font_montserrat_14, 0); - lv_label_set_text(font_small, "Montserrat 14px"); - lv_obj_set_style_text_color(font_small, lv_color_hex(0xCCCCCC), 0); - lv_obj_align(font_small, LV_ALIGN_BOTTOM_LEFT, 20, -80); - - // Medium font - lv_obj_t * font_medium = lv_label_create(parent); - lv_obj_set_style_text_font(font_medium, &lv_font_montserrat_18, 0); - lv_label_set_text(font_medium, "Montserrat 18px"); - lv_obj_set_style_text_color(font_medium, lv_color_hex(0xCCCCCC), 0); - lv_obj_align(font_medium, LV_ALIGN_BOTTOM_LEFT, 20, -50); - - // Large font - lv_obj_t * font_large = lv_label_create(parent); - lv_obj_set_style_text_font(font_large, &lv_font_montserrat_42, 0); - lv_label_set_text(font_large, "42"); - lv_obj_set_style_text_color(font_large, lv_color_hex(0x999999), 0); - lv_obj_align(font_large, LV_ALIGN_BOTTOM_RIGHT, -30, -30); -} - -/** - * @brief Update the touch coordinates label - */ -void updateTouchDisplay() { - if (!touch_label) return; - - int16_t x, y; - if (display.getTouchCoordinates(x, y)) { - lv_label_set_text_fmt(touch_label, "Touch: (%d, %d)", x, y); - } -} - -static inline bool isPressed(uint8_t pinIndex) { - // Entradas con pull-up externa: activo en LOW - int v = ioexp.read1(pinIndex); - return (v != TCA9555_INVALID_READ) && (v == LOW); -} - -static inline bool isGpioPressed(int gpio) { - return digitalRead(gpio) == LOW; // activo en LOW por pull-up externa -} - -void updateButtonDisplay() { - if (!button_label) return; - - const char* status = "none"; - - if (isGpioPressed(BUTTON_TOP)) { - status = "BUTTON_TOP"; - } else if (isPressed(EXPANDER_BUTTON_BOTTOM)) { - status = "BUTTON_BOTTOM"; - } else if (isPressed(EXPANDER_PAD_TOP)) { - status = "PAD_TOP"; - } else if (isPressed(EXPANDER_PAD_BOTTOM)) { - status = "PAD_BOTTOM"; - } else if (isPressed(EXPANDER_PAD_LEFT)) { - status = "PAD_LEFT"; - } else if (isPressed(EXPANDER_PAD_RIGHT)) { - status = "PAD_RIGHT"; - } - - lv_label_set_text_fmt(button_label, "Button: %s", status); - - // Actualizar NeoPixel según botón - uint32_t color = pixel.Color(0, 0, 0); - if (status == (const char*)"BUTTON_TOP") { - color = pixel.Color(255, 0, 0); // rojo - } else if (status == (const char*)"BOTON_INFERIOR") { - color = pixel.Color(0, 0, 255); // azul - } else if (status == (const char*)"PAD_TOP") { - color = pixel.Color(255, 255, 0); // amarillo - } else if (status == (const char*)"PAD_BOTTOM") { - color = pixel.Color(255, 0, 255); // magenta - } else if (status == (const char*)"PAD_LEFT") { - color = pixel.Color(0, 255, 0); // verde - } else if (status == (const char*)"PAD_RIGHT") { - color = pixel.Color(0, 255, 255); // cian - } - pixel.setPixelColor(0, color); - pixel.show(); } \ No newline at end of file diff --git a/src/ui.cpp b/src/ui.cpp new file mode 100644 index 0000000..63f3ad3 --- /dev/null +++ b/src/ui.cpp @@ -0,0 +1,233 @@ +#include "ui.h" +#include + +/* ── colour palette ──────────────────────────────────────────────── */ +#define CLR_BG 0x111111 +#define CLR_PANEL_BG 0x1A1A1A +#define CLR_PANEL_BRD 0x333333 +#define CLR_WHITE 0xFFFFFF +#define CLR_GRAY 0x888888 +#define CLR_CYAN 0x00E5FF +#define CLR_GREEN 0x4CAF50 +#define CLR_RED 0xF44336 +#define CLR_BAR_BG 0x2A2A2A +#define CLR_BAR_FILL 0x4CAF50 +#define CLR_ERROR_BG 0x330000 +#define CLR_DIMMED 0x777777 +#define CLR_SEPARATOR 0x333333 + +/* ── screen dimensions ───────────────────────────────────────────── */ +#define SCR_W 410 +#define SCR_H 502 + +/* ── static widget handles ───────────────────────────────────────── */ +static lv_obj_t *lbl_title; +static lv_obj_t *line_sep; +static lv_obj_t *lbl_wifi; +static lv_obj_t *lbl_ip; +static lv_obj_t *panel_ftp; +static lv_obj_t *lbl_ftp_status; +static lv_obj_t *lbl_ftp_url; +static lv_obj_t *lbl_ftp_creds; +static lv_obj_t *lbl_sd_title; +static lv_obj_t *bar_sd; +static lv_obj_t *lbl_sd_text; +static lv_obj_t *lbl_time; +static lv_obj_t *lbl_date; +static lv_obj_t *lbl_error; + +/* ── cached IP for FTP URL ───────────────────────────────────────── */ +static char cached_ip[48] = "0.0.0.0"; + +/* ── helpers ─────────────────────────────────────────────────────── */ +static lv_obj_t *make_label(lv_obj_t *parent, const lv_font_t *font, + uint32_t color, lv_align_t align, + lv_coord_t x_ofs, lv_coord_t y_ofs, + const char *text) +{ + lv_obj_t *lbl = lv_label_create(parent); + lv_label_set_text(lbl, text); + lv_obj_set_style_text_font(lbl, font, 0); + lv_obj_set_style_text_color(lbl, lv_color_hex(color), 0); + lv_obj_align(lbl, align, x_ofs, y_ofs); + return lbl; +} + +/* ── public API ──────────────────────────────────────────────────── */ + +void ui_init() +{ + lv_obj_t *scr = lv_scr_act(); + + /* screen background */ + lv_obj_set_style_bg_color(scr, lv_color_hex(CLR_BG), 0); + lv_obj_set_style_bg_opa(scr, LV_OPA_COVER, 0); + + /* ── title ───────────────────────────────────────────────────── */ + lbl_title = make_label(scr, &lv_font_montserrat_26, CLR_WHITE, + LV_ALIGN_TOP_MID, 0, 20, "FTP Explorer"); + + /* thin separator line */ + static lv_point_t line_pts[] = {{55, 0}, {355, 0}}; + line_sep = lv_line_create(scr); + lv_line_set_points(line_sep, line_pts, 2); + lv_obj_set_style_line_color(line_sep, lv_color_hex(CLR_SEPARATOR), 0); + lv_obj_set_style_line_width(line_sep, 1, 0); + lv_obj_align(line_sep, LV_ALIGN_TOP_LEFT, 0, 54); + + /* ── WiFi section ────────────────────────────────────────────── */ + lbl_wifi = make_label(scr, &lv_font_montserrat_18, CLR_GREEN, + LV_ALIGN_TOP_LEFT, 24, 70, + LV_SYMBOL_WIFI " Disconnected"); + + lbl_ip = make_label(scr, &lv_font_montserrat_30, CLR_CYAN, + LV_ALIGN_TOP_LEFT, 24, 100, "0.0.0.0"); + + /* ── FTP panel ───────────────────────────────────────────────── */ + panel_ftp = lv_obj_create(scr); + lv_obj_set_size(panel_ftp, SCR_W - 40, 120); + lv_obj_align(panel_ftp, LV_ALIGN_TOP_MID, 0, 145); + lv_obj_set_style_bg_color(panel_ftp, lv_color_hex(CLR_PANEL_BG), 0); + lv_obj_set_style_bg_opa(panel_ftp, LV_OPA_COVER, 0); + lv_obj_set_style_border_color(panel_ftp, lv_color_hex(CLR_PANEL_BRD), 0); + lv_obj_set_style_border_width(panel_ftp, 1, 0); + lv_obj_set_style_radius(panel_ftp, 10, 0); + lv_obj_set_style_pad_all(panel_ftp, 12, 0); + lv_obj_clear_flag(panel_ftp, LV_OBJ_FLAG_SCROLLABLE); + + /* panel title */ + lv_obj_t *lbl_ftp_title = lv_label_create(panel_ftp); + lv_label_set_text(lbl_ftp_title, "FTP Server"); + lv_obj_set_style_text_font(lbl_ftp_title, &lv_font_montserrat_18, 0); + lv_obj_set_style_text_color(lbl_ftp_title, lv_color_hex(CLR_WHITE), 0); + lv_obj_align(lbl_ftp_title, LV_ALIGN_TOP_LEFT, 0, 0); + + lbl_ftp_status = lv_label_create(panel_ftp); + lv_label_set_text(lbl_ftp_status, "Status: Idle"); + lv_obj_set_style_text_font(lbl_ftp_status, &lv_font_montserrat_18, 0); + lv_obj_set_style_text_color(lbl_ftp_status, lv_color_hex(CLR_GREEN), 0); + lv_obj_align(lbl_ftp_status, LV_ALIGN_TOP_LEFT, 0, 28); + + lbl_ftp_url = lv_label_create(panel_ftp); + lv_label_set_text(lbl_ftp_url, "ftp://0.0.0.0"); + lv_obj_set_style_text_font(lbl_ftp_url, &lv_font_montserrat_18, 0); + lv_obj_set_style_text_color(lbl_ftp_url, lv_color_hex(CLR_DIMMED), 0); + lv_obj_align(lbl_ftp_url, LV_ALIGN_TOP_LEFT, 0, 54); + + lbl_ftp_creds = lv_label_create(panel_ftp); + lv_label_set_text(lbl_ftp_creds, "User: kode Pass: kode"); + lv_obj_set_style_text_font(lbl_ftp_creds, &lv_font_montserrat_14, 0); + lv_obj_set_style_text_color(lbl_ftp_creds, lv_color_hex(CLR_GRAY), 0); + lv_obj_align(lbl_ftp_creds, LV_ALIGN_TOP_LEFT, 0, 80); + + /* ── SD card section ─────────────────────────────────────────── */ + lbl_sd_title = make_label(scr, &lv_font_montserrat_18, CLR_WHITE, + LV_ALIGN_TOP_LEFT, 24, 280, "SD Card"); + + bar_sd = lv_bar_create(scr); + lv_obj_set_size(bar_sd, SCR_W - 48, 14); + lv_obj_align(bar_sd, LV_ALIGN_TOP_LEFT, 24, 310); + lv_bar_set_range(bar_sd, 0, 100); + lv_bar_set_value(bar_sd, 0, LV_ANIM_OFF); + lv_obj_set_style_bg_color(bar_sd, lv_color_hex(CLR_BAR_BG), 0); + lv_obj_set_style_bg_opa(bar_sd, LV_OPA_COVER, 0); + lv_obj_set_style_bg_color(bar_sd, lv_color_hex(CLR_BAR_FILL), + LV_PART_INDICATOR); + lv_obj_set_style_bg_opa(bar_sd, LV_OPA_COVER, LV_PART_INDICATOR); + lv_obj_set_style_radius(bar_sd, 4, 0); + lv_obj_set_style_radius(bar_sd, 4, LV_PART_INDICATOR); + + lbl_sd_text = make_label(scr, &lv_font_montserrat_14, CLR_GRAY, + LV_ALIGN_TOP_LEFT, 24, 330, "0.0 / 0.0 GB"); + + /* ── time / date ─────────────────────────────────────────────── */ + lbl_time = make_label(scr, &lv_font_montserrat_42, CLR_WHITE, + LV_ALIGN_TOP_MID, 0, 400, "--:--:--"); + + lbl_date = make_label(scr, &lv_font_montserrat_18, CLR_GRAY, + LV_ALIGN_TOP_MID, 0, 452, "--.--.----"); + + /* ── error overlay (hidden) ──────────────────────────────────── */ + lbl_error = lv_label_create(scr); + lv_label_set_text(lbl_error, ""); + lv_obj_set_width(lbl_error, SCR_W - 48); + lv_obj_set_style_text_font(lbl_error, &lv_font_montserrat_18, 0); + lv_obj_set_style_text_color(lbl_error, lv_color_hex(CLR_RED), 0); + lv_obj_set_style_bg_color(lbl_error, lv_color_hex(CLR_ERROR_BG), 0); + lv_obj_set_style_bg_opa(lbl_error, LV_OPA_COVER, 0); + lv_obj_set_style_pad_all(lbl_error, 8, 0); + lv_obj_set_style_radius(lbl_error, 6, 0); + lv_obj_set_style_text_align(lbl_error, LV_TEXT_ALIGN_CENTER, 0); + lv_label_set_long_mode(lbl_error, LV_LABEL_LONG_WRAP); + lv_obj_align(lbl_error, LV_ALIGN_TOP_MID, 0, 360); + lv_obj_add_flag(lbl_error, LV_OBJ_FLAG_HIDDEN); +} + +void ui_set_wifi(const char *ssid, const char *ip) +{ + if (ssid && ssid[0]) { + lv_label_set_text_fmt(lbl_wifi, LV_SYMBOL_WIFI " %s", ssid); + lv_obj_set_style_text_color(lbl_wifi, lv_color_hex(CLR_GREEN), 0); + } else { + lv_label_set_text(lbl_wifi, LV_SYMBOL_WIFI " Disconnected"); + lv_obj_set_style_text_color(lbl_wifi, lv_color_hex(CLR_RED), 0); + } + + if (ip && ip[0]) { + lv_label_set_text(lbl_ip, ip); + snprintf(cached_ip, sizeof(cached_ip), "%s", ip); + } else { + lv_label_set_text(lbl_ip, "0.0.0.0"); + snprintf(cached_ip, sizeof(cached_ip), "0.0.0.0"); + } + + /* keep FTP URL in sync */ + lv_label_set_text_fmt(lbl_ftp_url, "ftp://%s", cached_ip); +} + +void ui_set_ftp(const char *status) +{ + lv_label_set_text_fmt(lbl_ftp_status, "Status: %s", status); +} + +void ui_set_time(const char *time_str) +{ + /* expects "HH:MM:SS" — split into time and derive date display */ + lv_label_set_text(lbl_time, time_str); +} + +void ui_set_sd(uint64_t used_mb, uint64_t total_mb) +{ + int pct = 0; + if (total_mb > 0) { + pct = (int)((used_mb * 100ULL) / total_mb); + if (pct > 100) pct = 100; + } + lv_bar_set_value(bar_sd, pct, LV_ANIM_OFF); + + /* show in GB with one decimal */ + float used_gb = used_mb / 1024.0f; + float total_gb = total_mb / 1024.0f; + lv_label_set_text_fmt(lbl_sd_text, "%.1f / %.1f GB", (double)used_gb, + (double)total_gb); + + /* colour the bar red when > 90 % */ + if (pct > 90) { + lv_obj_set_style_bg_color(bar_sd, lv_color_hex(CLR_RED), + LV_PART_INDICATOR); + } else { + lv_obj_set_style_bg_color(bar_sd, lv_color_hex(CLR_BAR_FILL), + LV_PART_INDICATOR); + } +} + +void ui_set_error(const char *msg) +{ + if (msg && msg[0]) { + lv_label_set_text(lbl_error, msg); + lv_obj_clear_flag(lbl_error, LV_OBJ_FLAG_HIDDEN); + } else { + lv_label_set_text(lbl_error, ""); + lv_obj_add_flag(lbl_error, LV_OBJ_FLAG_HIDDEN); + } +} diff --git a/src/ui.h b/src/ui.h new file mode 100644 index 0000000..ec2e062 --- /dev/null +++ b/src/ui.h @@ -0,0 +1,10 @@ +#pragma once + +#include + +void ui_init(); +void ui_set_wifi(const char *ssid, const char *ip); +void ui_set_ftp(const char *status); +void ui_set_time(const char *time_str); +void ui_set_sd(uint64_t used_mb, uint64_t total_mb); +void ui_set_error(const char *msg);