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
+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();
}