feat: FTP server with Wi-Fi, NTP, LVGL UI — initial working build

- Rewrite main.cpp: SD_MMC mount, Wi-Fi from JSON, NTP Vienna, FTP server
- Add ui.h/ui.cpp: LVGL status screen (IP, FTP status, SD bar, time)
- Add build_framework_libs.py: dynamic ESP32 Core 3.x lib resolution
- Remove unused sensor libs (IMU, magnetometer, fuel gauge, charger)
- Add SimpleFTPServer + ArduinoJson dependencies
- Build succeeds: RAM 19.2%, Flash 4.2%
This commit is contained in:
Lago
2026-04-03 14:13:09 +02:00
parent a3d9840a92
commit 4510cfe16c
6 changed files with 688 additions and 202 deletions
+39 -32
View File
@@ -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
+358 -158
View File
@@ -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 <Arduino.h>
#include <WiFi.h>
#include <lvgl.h>
#include <kodedot/display_manager.h>
#include <TCA9555.h>
#include <kodedot/pin_config.h>
#include <TCA9555.h>
#include <Adafruit_NeoPixel.h>
#include "SD_MMC.h"
#include "FS.h"
#include <ArduinoJson.h>
#include <SimpleFTPServer.h>
#include <time.h>
#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<JsonArray>();
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();
}
+233
View File
@@ -0,0 +1,233 @@
#include "ui.h"
#include <Arduino.h>
/* ── 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);
}
}
+10
View File
@@ -0,0 +1,10 @@
#pragma once
#include <lvgl.h>
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);