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:
+39
-32
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
Reference in New Issue
Block a user