95c41bf501
- FTP task stack 8KB → 32KB (SimpleFTPServer file I/O needs deep stack) - Skip calcDirSize when FTP client connected (prevents concurrent SD_MMC access from core 0 + core 1 causing null pointer crash)
586 lines
20 KiB
C++
586 lines
20 KiB
C++
/**
|
|
* @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 <Wire.h>
|
|
#include <lvgl.h>
|
|
#include <kodedot/display_manager.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 <PMIC_BQ25896.h>
|
|
#include <BQ27220.h>
|
|
#include <kode_MAX31329.h>
|
|
#include "ui.h"
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Global objects
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// Display manager (LVGL + panel + touch)
|
|
DisplayManager display;
|
|
|
|
// IO Expander (TCA9555)
|
|
static TCA9555 ioexp(IOEXP_I2C_ADDR);
|
|
|
|
// NeoPixel status LED
|
|
static Adafruit_NeoPixel pixel(NEO_PIXEL_COUNT, NEO_PIXEL_PIN, NEO_GRB + NEO_KHZ800);
|
|
|
|
// FTP server
|
|
FtpServer ftpSrv;
|
|
|
|
// Battery fuel gauge (BQ27220 @ 0x55)
|
|
static BQ27220 fuelGauge;
|
|
|
|
// PMIC / Charger (BQ25896 @ 0x6B)
|
|
static PMIC_BQ25896 pmic;
|
|
|
|
// External RTC (MAX31329 @ 0xD0)
|
|
static MAX31329 rtc;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 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 volatile bool ftp_client_on = false;
|
|
static volatile bool ftp_transfer = false;
|
|
static bool rtc_ok = false;
|
|
static bool gauge_ok = false;
|
|
static bool pmic_ok = false;
|
|
static unsigned long last_time_update = 0;
|
|
static unsigned long last_batt_update = 0;
|
|
|
|
// FTP task → main loop cross-core signalling (set by FTP callbacks, consumed in loop)
|
|
enum FtpUiState : uint8_t { FTP_ST_IDLE, FTP_ST_CONNECTED, FTP_ST_TRANSFERRING };
|
|
static volatile FtpUiState ftp_ui_state = FTP_ST_IDLE;
|
|
static volatile bool ftp_ui_dirty = false; // UI label + LED need refresh
|
|
static volatile bool ftp_sd_dirty = false; // SD usage info needs refresh
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 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 uint64_t calcDirSize(fs::FS &fs, const char *dirname) {
|
|
uint64_t total = 0;
|
|
File root = fs.open(dirname);
|
|
if (!root || !root.isDirectory()) return 0;
|
|
File f;
|
|
while ((f = root.openNextFile())) {
|
|
if (f.isDirectory()) {
|
|
/* On ESP32 Arduino 3.x, f.path() returns the full path */
|
|
String fullPath = String(f.path());
|
|
f.close();
|
|
total += calcDirSize(fs, fullPath.c_str());
|
|
} else {
|
|
total += f.size();
|
|
f.close();
|
|
}
|
|
}
|
|
root.close();
|
|
return total;
|
|
}
|
|
|
|
static void updateSdInfo() {
|
|
if (!sd_ok) return;
|
|
uint64_t total = SD_MMC.totalBytes() / (1024ULL * 1024ULL);
|
|
uint64_t used = SD_MMC.usedBytes() / (1024ULL * 1024ULL);
|
|
/* Skip heavy filesystem walk while FTP is active (concurrent SD_MMC
|
|
access from core 0 + core 1 causes null pointer crashes) */
|
|
if (used == 0 && total > 0 && !ftp_client_on) {
|
|
uint64_t walked = calcDirSize(SD_MMC, "/");
|
|
used = walked / (1024ULL * 1024ULL);
|
|
}
|
|
ui_set_sd(used, total);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// FTP callbacks (SimpleFTPServer v3.x)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Called on FTP connect / disconnect events.
|
|
* NOTE: Runs on the FTP task (core 0) — only set flags here, never call LVGL/UI.
|
|
*/
|
|
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;
|
|
ftp_ui_state = FTP_ST_CONNECTED;
|
|
ftp_ui_dirty = true;
|
|
break;
|
|
case FTP_DISCONNECT:
|
|
Serial.println("[FTP] Client disconnected");
|
|
ftp_client_on = false;
|
|
ftp_transfer = false;
|
|
ftp_ui_state = FTP_ST_IDLE;
|
|
ftp_ui_dirty = true;
|
|
break;
|
|
case FTP_FREE_SPACE_CHANGE:
|
|
Serial.printf("[FTP] Free space: %u / %u\n", freeSpace, totalSpace);
|
|
ftp_sd_dirty = true;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Called during file transfer operations.
|
|
* NOTE: Runs on the FTP task (core 0) — only set flags here, never call LVGL/UI.
|
|
*/
|
|
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;
|
|
ftp_ui_state = FTP_ST_TRANSFERRING;
|
|
ftp_ui_dirty = true;
|
|
break;
|
|
case FTP_DOWNLOAD_START:
|
|
Serial.printf("[FTP] Download start: %s\n", name);
|
|
ftp_transfer = true;
|
|
ftp_ui_state = FTP_ST_TRANSFERRING;
|
|
ftp_ui_dirty = true;
|
|
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;
|
|
ftp_ui_state = ftp_client_on ? FTP_ST_CONNECTED : FTP_ST_IDLE;
|
|
ftp_ui_dirty = true;
|
|
ftp_sd_dirty = true;
|
|
break;
|
|
case FTP_TRANSFER_ERROR:
|
|
Serial.printf("[FTP] Transfer error: %s\n", name);
|
|
ftp_transfer = false;
|
|
ftp_ui_state = ftp_client_on ? FTP_ST_CONNECTED : FTP_ST_IDLE;
|
|
ftp_ui_dirty = true;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Battery monitoring
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/** Update battery percentage and charging status every 5 seconds */
|
|
static void updateBattery() {
|
|
unsigned long now = millis();
|
|
if (now - last_batt_update < 5000) return;
|
|
last_batt_update = now;
|
|
|
|
int soc = -1;
|
|
bool charging = false;
|
|
|
|
/* Read state-of-charge from fuel gauge */
|
|
if (gauge_ok) {
|
|
soc = fuelGauge.readStateOfChargePercent();
|
|
if (soc < 0 || soc > 100) soc = -1; // sanity check
|
|
}
|
|
|
|
/* Read charging status from PMIC */
|
|
if (pmic_ok) {
|
|
pmic.setCONV_START(true); // trigger ADC conversion
|
|
uint8_t chrg_stat = pmic.get_VBUS_STAT_reg().chrg_stat;
|
|
// chrg_stat: 0=Not Charging, 1=Pre-charge, 2=Fast Charging, 3=Charge Done
|
|
charging = (chrg_stat == 1 || chrg_stat == 2);
|
|
}
|
|
|
|
ui_set_battery(soc, charging);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Time display
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/** Update the UI time and date labels 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);
|
|
|
|
char datebuf[32];
|
|
strftime(datebuf, sizeof(datebuf), "%a, %d %b %Y", &timeinfo);
|
|
ui_set_date(datebuf);
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 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;
|
|
}
|
|
|
|
/** Initialise I2C bus and battery / RTC peripherals */
|
|
static void initPeripherals() {
|
|
Wire.begin(48, 47); // SDA=48, SCL=47
|
|
|
|
/* ── Fuel Gauge (BQ27220 @ 0x55) ──────────────────────────── */
|
|
gauge_ok = fuelGauge.begin();
|
|
if (gauge_ok) {
|
|
int soc = fuelGauge.readStateOfChargePercent();
|
|
int mv = fuelGauge.readVoltageMillivolts();
|
|
Serial.printf("[BAT] Fuel gauge ready — SOC: %d%% Voltage: %d mV\n", soc, mv);
|
|
} else {
|
|
Serial.println("[BAT] Warning: BQ27220 fuel gauge not found");
|
|
}
|
|
|
|
/* ── PMIC (BQ25896 @ 0x6B) ───────────────────────────────── */
|
|
pmic.begin();
|
|
pmic_ok = pmic.isConnected();
|
|
if (pmic_ok) {
|
|
pmic.setCONV_START(true); // trigger first ADC conversion
|
|
Serial.printf("[BAT] PMIC ready — VBUS: %d mV VBAT: %d mV\n",
|
|
pmic.getVBUSV(), pmic.getBATV());
|
|
} else {
|
|
Serial.println("[BAT] Warning: BQ25896 PMIC not found");
|
|
}
|
|
|
|
/* ── External RTC (MAX31329 @ 0xD0) ──────────────────────── */
|
|
rtc.begin();
|
|
/* Try reading time — if year is valid (>2020), set ESP32 system clock */
|
|
if (rtc.readTime()) {
|
|
if (rtc.t.year >= 2020 && rtc.t.year <= 2099) {
|
|
rtc_ok = true;
|
|
struct tm t = {};
|
|
t.tm_year = rtc.t.year - 1900;
|
|
t.tm_mon = rtc.t.month - 1;
|
|
t.tm_mday = rtc.t.day;
|
|
t.tm_hour = rtc.t.hour;
|
|
t.tm_min = rtc.t.minute;
|
|
t.tm_sec = rtc.t.second;
|
|
struct timeval tv = { .tv_sec = mktime(&t), .tv_usec = 0 };
|
|
settimeofday(&tv, NULL);
|
|
Serial.printf("[RTC] External RTC time: %04d-%02d-%02d %02d:%02d:%02d\n",
|
|
rtc.t.year, rtc.t.month, rtc.t.day,
|
|
rtc.t.hour, rtc.t.minute, rtc.t.second);
|
|
} else {
|
|
Serial.printf("[RTC] External RTC has invalid year (%d) — will set from NTP\n", rtc.t.year);
|
|
}
|
|
} else {
|
|
Serial.println("[RTC] Warning: MAX31329 external RTC not responding");
|
|
}
|
|
}
|
|
|
|
/** Write current system time to external RTC (call after NTP sync) */
|
|
static void syncRtcFromSystem() {
|
|
if (!rtc_ok && !gauge_ok) return; // RTC not available
|
|
|
|
struct tm timeinfo;
|
|
if (!getLocalTime(&timeinfo, 0)) return;
|
|
|
|
rtc.t.year = timeinfo.tm_year + 1900;
|
|
rtc.t.month = timeinfo.tm_mon + 1;
|
|
rtc.t.day = timeinfo.tm_mday;
|
|
rtc.t.hour = timeinfo.tm_hour;
|
|
rtc.t.minute = timeinfo.tm_min;
|
|
rtc.t.second = timeinfo.tm_sec;
|
|
rtc.t.dayOfWeek = timeinfo.tm_wday;
|
|
|
|
if (rtc.writeTime()) {
|
|
rtc_ok = true;
|
|
Serial.printf("[RTC] Updated external RTC: %04d-%02d-%02d %02d:%02d:%02d\n",
|
|
rtc.t.year, rtc.t.month, rtc.t.day,
|
|
rtc.t.hour, rtc.t.minute, rtc.t.second);
|
|
} else {
|
|
Serial.println("[RTC] Warning: Failed to write time to external RTC");
|
|
}
|
|
}
|
|
|
|
/** 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);
|
|
/* Write NTP time to external RTC for persistence */
|
|
syncRtcFromSystem();
|
|
return;
|
|
}
|
|
}
|
|
Serial.println("[NTP] Warning: time not yet synced (will update in background)");
|
|
}
|
|
|
|
/** FTP task — runs handleFTP() in a dedicated loop on core 0 */
|
|
static void ftpTask(void *param) {
|
|
(void)param;
|
|
for (;;) {
|
|
ftpSrv.handleFTP();
|
|
vTaskDelay(pdMS_TO_TICKS(1)); // yield ~1 ms between iterations
|
|
}
|
|
}
|
|
|
|
/** Start FTP server with callbacks and launch dedicated task */
|
|
static void initFtp() {
|
|
Serial.println("[FTP] Starting FTP server (user: kode, port: 21)...");
|
|
ftpSrv.setCallback(ftpCallback);
|
|
ftpSrv.setTransferCallback(ftpTransferCallback);
|
|
/* Set local IP so PASV mode sends the correct address to clients */
|
|
ftpSrv.setLocalIp(WiFi.localIP());
|
|
ftpSrv.begin("kode", "kode");
|
|
|
|
/* Run FTP in its own task on core 0 so it never blocks LVGL on core 1 */
|
|
xTaskCreatePinnedToCore(ftpTask, "ftp", 32768, NULL, 2, NULL, 0);
|
|
|
|
Serial.printf("[FTP] FTP server ready on core 0 (PASV IP: %s, data port: 50009)\n",
|
|
WiFi.localIP().toString().c_str());
|
|
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);
|
|
delay(100);
|
|
Serial.println();
|
|
Serial.println("===================================");
|
|
Serial.println(" Kode Dot FTP Explorer");
|
|
Serial.println("===================================");
|
|
|
|
// NeoPixel off during init
|
|
pixel.begin();
|
|
pixel.setBrightness(30);
|
|
setLed(COLOR_OFF);
|
|
|
|
// Initialise display + LVGL
|
|
if (!display.init()) {
|
|
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();
|
|
|
|
// Initialise I2C peripherals (battery, RTC)
|
|
initPeripherals();
|
|
|
|
// 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();
|
|
|
|
// 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();
|
|
|
|
// Sync NTP time
|
|
initNtp();
|
|
|
|
// Start FTP server
|
|
initFtp();
|
|
|
|
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() {
|
|
/* Process FTP status flags (set by callbacks on core 0, consumed here on core 1) */
|
|
if (ftp_ui_dirty) {
|
|
ftp_ui_dirty = false;
|
|
static const char *ftp_st_str[] = {"Idle", "Connected", "Transferring"};
|
|
ui_set_ftp(ftp_st_str[ftp_ui_state]);
|
|
updateLed();
|
|
}
|
|
if (ftp_sd_dirty) {
|
|
ftp_sd_dirty = false;
|
|
updateSdInfo();
|
|
}
|
|
|
|
display.update();
|
|
updateTimeDisplay();
|
|
updateBattery();
|
|
delay(5);
|
|
} |