From 2a5088bec312c19284c2446bf19697878c61d7df Mon Sep 17 00:00:00 2001 From: GW_MC <72297530+GWMCwing@users.noreply.github.com> Date: Fri, 30 Jan 2026 15:23:44 +0800 Subject: [PATCH] feat: implement LittleFSHandler and FSGuard for improved file management --- main/io/fs_handler.cpp | 552 ++++++++++++++++++++++++++++++++++ main/io/fs_handler.h | 95 ++++++ main/main.cpp | 14 +- main/network/wifi_handler.cpp | 220 +++++++++----- main/network/wifi_handler.h | 7 +- partitions.csv | 4 +- sdkconfig.default | 14 +- 7 files changed, 821 insertions(+), 85 deletions(-) create mode 100644 main/io/fs_handler.cpp create mode 100644 main/io/fs_handler.h diff --git a/main/io/fs_handler.cpp b/main/io/fs_handler.cpp new file mode 100644 index 0000000..b127355 --- /dev/null +++ b/main/io/fs_handler.cpp @@ -0,0 +1,552 @@ +#include "io/fs_handler.h" +#include +#include +#include +#include +#include "esp_partition.h" + +#define TAG "LittleFSHandler" +#define PARTITION_LABEL "storage" +#define BLOCK_SIZE 512 // Match typical flash sector size + +// +// FSGuard implementation +// + +FSGuard::FSGuard(LittleFSHandler* fs_handler, const std::string& relative_path, const char* flags) + : fs_handler_(fs_handler), file_(nullptr) { + if (fs_handler_ != nullptr) { + fs_handler_->open_file_(relative_path, flags, file_); + } else { + ESP_LOGE("FSGuard", "FSGuard initialized with null LittleFSHandler"); + } +} + +FSGuard::~FSGuard() { + this->close(); +} + +esp_err_t FSGuard::close() { + if (file_ != nullptr && fs_handler_ != nullptr) { + esp_err_t err = fs_handler_->close_file_(file_); + file_ = nullptr; + fs_handler_ = nullptr; + if (err != ESP_OK) { + ESP_LOGE("FSGuard", "Error closing file: %s", esp_err_to_name(err)); + } + return err; + } + return ESP_OK; +} + +// +// LittleFSHandler implementation +// + +LittleFSHandler::LittleFSHandler() { + this->fs_mutex_ = xSemaphoreCreateMutex(); + if (this->fs_mutex_ == nullptr) { + ESP_LOGE(TAG, "Failed to create filesystem mutex"); + } +} + +LittleFSHandler::~LittleFSHandler() { + if (this->is_initialized_()) { + esp_vfs_littlefs_unregister(PARTITION_LABEL); + this->initialized_ = false; + } + if (this->fs_mutex_ != nullptr) { + vSemaphoreDelete(this->fs_mutex_); + this->fs_mutex_ = nullptr; + } +} + +esp_err_t LittleFSHandler::init(std::string base_path) { + // default config + esp_vfs_littlefs_conf_t config = {}; + config.dont_mount = false; + config.partition_label = PARTITION_LABEL; + config.base_path = base_path.c_str(); + config.format_if_mount_failed = true; + // + base_path_ = base_path; + return init(config); +} + +esp_err_t LittleFSHandler::init(const esp_vfs_littlefs_conf_t& config) { + base_path_ = std::string(config.base_path); + if (this->is_initialized_()) { + ESP_LOGW(TAG, "LittleFS already initialized, skipping"); + return ESP_OK; + } + + esp_err_t err = esp_vfs_littlefs_register(&config); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to initialize LittleFS: %s", esp_err_to_name(err)); + + if (err == ESP_ERR_NOT_FOUND) { + ESP_LOGE(TAG, "Listing all available partitions:"); + esp_partition_iterator_t it = esp_partition_find(ESP_PARTITION_TYPE_ANY, ESP_PARTITION_SUBTYPE_ANY, NULL); + while (it != NULL) { + const esp_partition_t* part = esp_partition_get(it); + ESP_LOGE(TAG, " - Label: '%s', Type: 0x%02x, Subtype: 0x%02x, Address: 0x%08x, Size: 0x%08x", + part->label, part->type, part->subtype, part->address, part->size); + it = esp_partition_next(it); + } + esp_partition_iterator_release(it); + } + + return ESP_ERR_INVALID_STATE; + } + this->initialized_ = true; + return ESP_OK; +} + +std::string LittleFSHandler::get_base_path() const { + if (!this->is_initialized_()) { + ESP_LOGE(TAG, "LittleFS is not initialized, cannot get base path"); + return ""; + } + return base_path_; +} + +std::string LittleFSHandler::get_full_path(const std::string& relative_path) const { + if (!this->is_initialized_()) { + ESP_LOGE(TAG, "LittleFS is not initialized, cannot get full path"); + return ""; + } + return base_path_ + "/" + relative_path; +} + +esp_err_t LittleFSHandler::write_file(const std::string& relative_path, const uint8_t* data, size_t size, size_t& out_bytes_written) { + if (!this->is_initialized_()) { + ESP_LOGE(TAG, "LittleFS is not initialized"); + return ESP_ERR_INVALID_STATE; + } + if (data == nullptr) { + ESP_LOGE(TAG, "Data pointer is null"); + return ESP_ERR_INVALID_ARG; + } + + SemaphoreGuard guard(this->fs_mutex_); + if (guard.take() != pdTRUE) { + ESP_LOGE(TAG, "Failed to acquire filesystem mutex"); + return ESP_ERR_TIMEOUT; + } + + // Try to open with r+b first to preserve existing content for comparison + FSGuard file_guard(this, relative_path, "r+b"); + + // If file doesn't exist, open with wb + if (!file_guard.is_open()) { + FSGuard new_file_guard(this, relative_path, "wb"); + if (!new_file_guard.is_open()) { + return ESP_ERR_NOT_FOUND; + } + return this->write_if_different_(new_file_guard.get_file(), data, size, out_bytes_written); + } + return this->write_if_different_(file_guard.get_file(), data, size, out_bytes_written); +} + + +// Caller is responsible for opening the file in appropriate mode +// If the file doesn't exist, use write_file with "wb" mode +// If the file exists, use "r+b" mode to read and write +esp_err_t LittleFSHandler::write_file(FILE* file, const uint8_t* data, size_t size, size_t& out_bytes_written) { + return this->write_if_different_(file, data, size, out_bytes_written); +} + +esp_err_t LittleFSHandler::append_file(const std::string& relative_path, const uint8_t* data, size_t size, size_t& out_bytes_written) { + if (!this->is_initialized_()) { + ESP_LOGE(TAG, "LittleFS is not initialized"); + return ESP_ERR_INVALID_STATE; + } + if (data == nullptr) { + ESP_LOGE(TAG, "Data pointer is null"); + return ESP_ERR_INVALID_ARG; + } + + SemaphoreGuard guard(this->fs_mutex_); + if (guard.take() != pdTRUE) { + ESP_LOGE(TAG, "Failed to acquire filesystem mutex"); + return ESP_ERR_TIMEOUT; + } + + FSGuard file_guard(this, relative_path, "ab"); + FILE* file = file_guard.get_file(); + if (file == nullptr) { + return ESP_ERR_NOT_FOUND; + } + return this->append_file(file, data, size, out_bytes_written); +} + +esp_err_t LittleFSHandler::append_file(FILE* file, const uint8_t* data, size_t size, size_t& out_bytes_written) { + if (file == nullptr) { + ESP_LOGE(TAG, "File pointer is null"); + return ESP_ERR_INVALID_ARG; + } + if (data == nullptr) { + ESP_LOGE(TAG, "Data pointer is null"); + return ESP_ERR_INVALID_ARG; + } + + if (fseek(file, 0, SEEK_END) != 0) { + ESP_LOGE(TAG, "Failed to seek to end of file"); + return ESP_ERR_INVALID_STATE; + } + // write data with POSIX + size_t bytes_written = fwrite(data, 1, size, file); + if (bytes_written != size) { + ESP_LOGE(TAG, "Failed to write all data to file, expected %zu bytes, wrote %zu bytes", size, bytes_written); + return ESP_ERR_NO_MEM; + } + if (fflush(file) != 0) { + ESP_LOGE(TAG, "Failed to flush file"); + return ESP_ERR_INVALID_STATE; + } + out_bytes_written = bytes_written; + return ESP_OK; +} + +esp_err_t LittleFSHandler::read_file(const std::string& relative_path, const size_t max_size, uint8_t* out_data, size_t& out_size) { + if (!this->is_initialized_()) { + ESP_LOGE(TAG, "LittleFS is not initialized"); + return ESP_ERR_INVALID_STATE; + } + if (out_data == nullptr) { + ESP_LOGE(TAG, "Output data pointer is null"); + return ESP_ERR_INVALID_ARG; + } + + SemaphoreGuard guard(this->fs_mutex_); + if (guard.take() != pdTRUE) { + ESP_LOGE(TAG, "Failed to acquire filesystem mutex"); + return ESP_ERR_TIMEOUT; + } + + FSGuard file_guard(this, relative_path, "rb"); + FILE* file = file_guard.get_file(); + if (file == nullptr) { + return ESP_ERR_NOT_FOUND; + } + return this->read_file(file, max_size, out_data, out_size); +} + +esp_err_t LittleFSHandler::read_file(FILE* file, const size_t max_size, uint8_t* out_data, size_t& out_size) { + if (file == nullptr) { + ESP_LOGE(TAG, "File pointer is null"); + return ESP_ERR_INVALID_ARG; + } + if (out_data == nullptr) { + ESP_LOGE(TAG, "Output data pointer is null"); + return ESP_ERR_INVALID_ARG; + } + + size_t bytes_read = fread(out_data, 1, max_size, file); + if (bytes_read == 0 && ferror(file)) { + ESP_LOGE(TAG, "Failed to read from file"); + return ESP_ERR_INVALID_STATE; + } + out_size = bytes_read; + return ESP_OK; +} + +esp_err_t LittleFSHandler::delete_file(const std::string& relative_path) { + if (!this->is_initialized_()) { + ESP_LOGE(TAG, "LittleFS is not initialized"); + return ESP_ERR_INVALID_STATE; + } + + SemaphoreGuard guard(this->fs_mutex_); + if (guard.take() != pdTRUE) { + ESP_LOGE(TAG, "Failed to acquire filesystem mutex"); + return ESP_ERR_TIMEOUT; + } + + std::string full_path = this->get_full_path(relative_path); + if (remove(full_path.c_str()) != 0) { + ESP_LOGE(TAG, "Failed to delete file %s: %s", full_path.c_str(), strerror(errno)); + return ESP_ERR_NOT_FOUND; + } + return ESP_OK; +} + +bool LittleFSHandler::file_exists(const std::string& relative_path) { + if (!this->is_initialized_()) { + ESP_LOGE(TAG, "LittleFS is not initialized"); + return false; + } + + SemaphoreGuard guard(this->fs_mutex_); + if (guard.take() != pdTRUE) { + ESP_LOGE(TAG, "Failed to acquire filesystem mutex"); + return false; + } + + std::string full_path = this->get_full_path(relative_path); + struct stat st; + return (stat(full_path.c_str(), &st) == 0 && S_ISREG(st.st_mode)); +} + +esp_err_t LittleFSHandler::get_file_size(const std::string& relative_path, size_t& out_size) { + if (!this->is_initialized_()) { + ESP_LOGE(TAG, "LittleFS is not initialized"); + return ESP_ERR_INVALID_STATE; + } + + SemaphoreGuard guard(this->fs_mutex_); + if (guard.take() != pdTRUE) { + ESP_LOGE(TAG, "Failed to acquire filesystem mutex"); + return ESP_ERR_TIMEOUT; + } + + std::string full_path = this->get_full_path(relative_path); + struct stat st; + if (stat(full_path.c_str(), &st) != 0) { + ESP_LOGE(TAG, "Failed to stat file %s", full_path.c_str()); + return ESP_ERR_NOT_FOUND; + } + out_size = st.st_size; + return ESP_OK; +} + +esp_err_t LittleFSHandler::create_directory(const std::string& relative_path) { + if (!this->is_initialized_()) { + ESP_LOGE(TAG, "LittleFS is not initialized"); + return ESP_ERR_INVALID_STATE; + } + + SemaphoreGuard guard(this->fs_mutex_); + if (guard.take() != pdTRUE) { + ESP_LOGE(TAG, "Failed to acquire filesystem mutex"); + return ESP_ERR_TIMEOUT; + } + + std::string full_path = this->get_full_path(relative_path); + if (mkdir(full_path.c_str(), 0755) != 0) { + if (errno == EEXIST) { + ESP_LOGW(TAG, "Directory %s already exists", full_path.c_str()); + return ESP_OK; + } + ESP_LOGE(TAG, "Failed to create directory %s: %s", full_path.c_str(), strerror(errno)); + return ESP_ERR_INVALID_STATE; + } + return ESP_OK; +} + +esp_err_t LittleFSHandler::list_directory(const std::string& relative_path, std::vector& out_entries) { + if (!this->is_initialized_()) { + ESP_LOGE(TAG, "LittleFS is not initialized"); + return ESP_ERR_INVALID_STATE; + } + + SemaphoreGuard guard(this->fs_mutex_); + if (guard.take() != pdTRUE) { + ESP_LOGE(TAG, "Failed to acquire filesystem mutex"); + return ESP_ERR_TIMEOUT; + } + + std::string full_path = this->get_full_path(relative_path); + DIR* dir = opendir(full_path.c_str()); + if (dir == nullptr) { + ESP_LOGE(TAG, "Failed to open directory %s", full_path.c_str()); + return ESP_ERR_NOT_FOUND; + } + + out_entries.clear(); + struct dirent* entry; + while ((entry = readdir(dir)) != nullptr) { + // Skip . and .. + if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) { + continue; + } + out_entries.push_back(entry->d_name); + } + closedir(dir); + return ESP_OK; +} + +// +// Private methods +// + +esp_err_t LittleFSHandler::open_file_(const std::string& relative_path, const char* flags, FILE*& out_file) { + if (!this->is_initialized_()) { + ESP_LOGE(TAG, "LittleFS is not initialized, cannot open file"); + return ESP_ERR_INVALID_STATE; + } + std::string full_path = this->get_full_path(relative_path); + FILE* file = fopen(full_path.c_str(), flags); + if (file == nullptr) { + // Use debug level if file doesn't exist (ENOENT), warning level for other errors + if (errno == ENOENT) { + ESP_LOGD(TAG, "File %s does not exist (flags %s)", full_path.c_str(), flags); + } else { + ESP_LOGW(TAG, "Failed to open file %s with flags %s: %s", full_path.c_str(), flags, strerror(errno)); + } + return ESP_ERR_NOT_FOUND; + } + out_file = file; + return ESP_OK; +} + +esp_err_t LittleFSHandler::close_file_(FILE* file) { + if (file == nullptr) { + return ESP_OK; + } + if (fclose(file) != 0) { + ESP_LOGE(TAG, "Failed to close file: %s", strerror(errno)); + return ESP_ERR_INVALID_STATE; + } + return ESP_OK; +} + +esp_err_t LittleFSHandler::write_if_different_(FILE* file, const uint8_t* data, size_t size) { + size_t out_bytes_written = 0; + return this->write_if_different_(file, data, size, out_bytes_written); +} + +esp_err_t LittleFSHandler::write_if_different_(FILE* file, const uint8_t* data, size_t size, size_t& out_bytes_written) { + if (file == nullptr || data == nullptr) { + ESP_LOGE(TAG, "Invalid parameters"); + return ESP_ERR_INVALID_ARG; + } + + // Get existing file size + if (fseek(file, 0, SEEK_END) != 0) { + ESP_LOGE(TAG, "Failed to seek to end of file"); + return ESP_ERR_INVALID_STATE; + } + long file_size_long = ftell(file); + if (file_size_long < 0) { + ESP_LOGE(TAG, "Failed to get file size"); + return ESP_ERR_INVALID_STATE; + } + size_t file_size = (size_t)file_size_long; + + if (fseek(file, 0, SEEK_SET) != 0) { + ESP_LOGE(TAG, "Failed to seek to beginning of file"); + return ESP_ERR_INVALID_STATE; + } + + out_bytes_written = 0; + size_t compare_size = (file_size < size) ? file_size : size; + + // Read entire file content for comparison + std::vector existing_data; + if (file_size > 0) { + existing_data.resize(file_size); + size_t bytes_read = fread(existing_data.data(), 1, file_size, file); + if (bytes_read != file_size) { + ESP_LOGE(TAG, "Failed to read existing file data"); + return ESP_ERR_INVALID_STATE; + } + } + + // Compare and identify blocks that need updating + std::vector block_needs_update((size + BLOCK_SIZE - 1) / BLOCK_SIZE, false); + bool any_changes = false; + + for (size_t offset = 0; offset < compare_size; offset += BLOCK_SIZE) { + size_t chunk_size = BLOCK_SIZE; + if (offset + chunk_size > compare_size) { + chunk_size = compare_size - offset; + } + + if (memcmp(existing_data.data() + offset, data + offset, chunk_size) != 0) { + block_needs_update[offset / BLOCK_SIZE] = true; + any_changes = true; + } + } + + // Check if size changed or there are additional blocks to write + if (size != file_size) { + any_changes = true; + } + + if (!any_changes) { + ESP_LOGD(TAG, "File content unchanged, skipping write"); + return ESP_OK; + } + + // Seek to beginning to start writing + if (fseek(file, 0, SEEK_SET) != 0) { + ESP_LOGE(TAG, "Failed to seek to beginning for writing"); + return ESP_ERR_INVALID_STATE; + } + + // Write only changed blocks + for (size_t offset = 0; offset < compare_size; offset += BLOCK_SIZE) { + size_t block_index = offset / BLOCK_SIZE; + if (!block_needs_update[block_index]) { + // Skip unchanged block + if (fseek(file, offset + BLOCK_SIZE, SEEK_SET) != 0) { + // If at end of compare region, this is OK + if (offset + BLOCK_SIZE > compare_size) { + if (fseek(file, compare_size, SEEK_SET) != 0) { + ESP_LOGE(TAG, "Failed to seek past unchanged block"); + return ESP_ERR_INVALID_STATE; + } + } else { + ESP_LOGE(TAG, "Failed to seek past unchanged block at %zu", offset); + return ESP_ERR_INVALID_STATE; + } + } + continue; + } + + size_t chunk_size = BLOCK_SIZE; + if (offset + chunk_size > compare_size) { + chunk_size = compare_size - offset; + } + + if (fseek(file, offset, SEEK_SET) != 0) { + ESP_LOGE(TAG, "Failed to seek to offset %zu", offset); + return ESP_ERR_INVALID_STATE; + } + + size_t written = fwrite(data + offset, 1, chunk_size, file); + if (written != chunk_size) { + ESP_LOGE(TAG, "Failed to write block at offset %zu", offset); + return ESP_ERR_INVALID_STATE; + } + out_bytes_written += written; + } + + // Handle size differences + if (size > file_size) { + // Write additional data beyond original file size + if (fseek(file, file_size, SEEK_SET) != 0) { + ESP_LOGE(TAG, "Failed to seek to end for appending"); + return ESP_ERR_INVALID_STATE; + } + size_t written = fwrite(data + file_size, 1, size - file_size, file); + if (written != (size - file_size)) { + ESP_LOGE(TAG, "Failed to write additional data"); + return ESP_ERR_INVALID_STATE; + } + out_bytes_written += written; + } else if (size < file_size) { + // Truncate file to new size + if (ftruncate(fileno(file), size) != 0) { + ESP_LOGE(TAG, "Failed to truncate file to size %zu", size); + return ESP_ERR_INVALID_STATE; + } + } + + // Flush to ensure data is written to storage + if (fflush(file) != 0) { + ESP_LOGE(TAG, "Failed to flush file after write"); + return ESP_ERR_INVALID_STATE; + } + + return ESP_OK; +} + +bool LittleFSHandler::is_initialized_() const { + return this->initialized_; +} + + + + diff --git a/main/io/fs_handler.h b/main/io/fs_handler.h new file mode 100644 index 0000000..86894b8 --- /dev/null +++ b/main/io/fs_handler.h @@ -0,0 +1,95 @@ +#pragma once +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" +#include "esp_littlefs.h" +#include "esp_err.h" +#include +#include +#include +#include +#include "esp_log.h" +#include + +// Forward declaration +class LittleFSHandler; + +class FSGuard { +public: + FSGuard(LittleFSHandler* fs_handler, const std::string& relative_path, const char* flags); + ~FSGuard(); + + esp_err_t close(); + + FILE* get_file() { + return file_; + } + + bool is_open() const { + return file_ != nullptr; + } + +private: + LittleFSHandler* fs_handler_ = nullptr; + FILE* file_; + + // prevent copying and moving + FSGuard(const FSGuard&) = delete; + FSGuard& operator=(const FSGuard&) = delete; + FSGuard(FSGuard&& other) = delete; + FSGuard& operator=(FSGuard&& other) = delete; +}; + +//LittleFSHandler interface +// All paths are relative to the mounted filesystem root +// Implementations should handle initialization of the filesystem, and mounting if necessary +// When destroyed, implementations should unmount the filesystem if necessary +// All paths are relative to the mounted filesystem root, e.g. if mounted at /littlefs, and file is /data.txt, the full path is /littlefs/data.txt +// File operations use standard C FILE* wrapped in FSGuard for RAII +class LittleFSHandler { +public: + LittleFSHandler(); + ~LittleFSHandler(); + + esp_err_t init(std::string base_path); + esp_err_t init(const esp_vfs_littlefs_conf_t& config); + std::string get_base_path() const; + std::string get_full_path(const std::string& relative_path) const; + // File operations + esp_err_t write_file(const std::string& relative_path, const uint8_t* data, size_t size, size_t& out_bytes_written); + esp_err_t write_file(FILE* file, const uint8_t* data, size_t size, size_t& out_bytes_written); + // + esp_err_t append_file(const std::string& relative_path, const uint8_t* data, size_t size, size_t& out_bytes_written); + esp_err_t append_file(FILE* file, const uint8_t* data, size_t size, size_t& out_bytes_written); + // + esp_err_t read_file(const std::string& relative_path, const size_t max_size, uint8_t* out_data, size_t& out_size); + esp_err_t read_file(FILE* file, const size_t max_size, uint8_t* out_data, size_t& out_size); + // + esp_err_t delete_file(const std::string& relative_path); + // + bool file_exists(const std::string& relative_path); + esp_err_t get_file_size(const std::string& relative_path, size_t& out_size); + // Directory operations + esp_err_t create_directory(const std::string& relative_path); + esp_err_t list_directory(const std::string& relative_path, std::vector& out_entries); + +protected: + + esp_err_t open_file_(const std::string& relative_path, const char* flags, FILE*& out_file); + esp_err_t close_file_(FILE* file); + + // uses standard C FILE* for file operations + esp_err_t write_if_different_(FILE* file, const uint8_t* data, size_t size); + esp_err_t write_if_different_(FILE* file, const uint8_t* data, size_t size, size_t& out_bytes_written); + + friend class FSGuard; + +private: + // + bool is_initialized_() const; + + SemaphoreHandle_t fs_mutex_ = nullptr; + bool initialized_ = false; + std::string base_path_; +}; + + diff --git a/main/main.cpp b/main/main.cpp index 2367e0d..0b5ffc7 100644 --- a/main/main.cpp +++ b/main/main.cpp @@ -13,6 +13,7 @@ #include "common/constants.h" #include "common/queue_defs.h" #include "io/nvs_handler.h" +#include "io/fs_handler.h" #include "info/info.h" #include "display/eink_display_handler.h" #include "display/lvgl_handler.h" @@ -31,7 +32,6 @@ // nvs storage namespaces, 15 characters max #define DEFAULT_STORAGE_NAMESPACE "storage" -#define WIFI_CREDENTIALS_STORAGE_NAMESPACE "wifi_cred" #define TAG "Main" extern "C" void app_main(void); @@ -61,9 +61,15 @@ void app_main(void) { DEFAULT_STORAGE_NAMESPACE ); - auto wifi_handler = std::make_unique( - std::unique_ptr(new NVSStorageHandler(WIFI_CREDENTIALS_STORAGE_NAMESPACE)) - ); + auto fs_handler = std::make_shared(); + esp_err_t fs_err = fs_handler->init("/littlefs"); + if (fs_err != ESP_OK) { + ESP_LOGE(TAG, "Failed to initialize LittleFS: %s", esp_err_to_name(fs_err)); + vTaskDelay(5000 / portTICK_PERIOD_MS); + return esp_restart(); + } + + auto wifi_handler = std::make_unique(fs_handler); NetworkHandler* network_handler = new NetworkHandler(std::move(wifi_handler)); EInkDisplayHandler* display_handler = new EInkDisplayHandler(); // Initialize display and touch diff --git a/main/network/wifi_handler.cpp b/main/network/wifi_handler.cpp index a5a08ab..dd0ba1d 100644 --- a/main/network/wifi_handler.cpp +++ b/main/network/wifi_handler.cpp @@ -8,16 +8,27 @@ #include "common/semaphore_guard.h" #include "cJSON.h" -static const char* TAG = "WifiHandler"; -static const char* WIFI_SSID_KEY = "ssid"; -static const char* WIFI_PASSWORD_STORE_KEY = "psw"; +#define TAG "WifiHandler" +#define WIFI_CRED_FILE_PATH "wifi_credentials.json" + +/* + * WiFi Credentials JSON Structure: + * { + * "current_ssid": "MyWiFi", + * "credentials": { + * "MyWiFi": { + * "password": "mypassword123" + * }, + * "OtherNetwork": { + * "password": "otherpass456" + * } + * } + * } + */ WifiHandler::WifiHandler( - // this handler is used to store/retrieve WiFi credentials - // should have a unique namespace for WiFi credentials - // it will be owned by WifiHandler and deleted in its destructor - std::unique_ptr kvs -) : kvs(std::move(kvs)) { + std::shared_ptr fs_handler_ +) : fs_handler_(std::move(fs_handler_)) { this->s_wifi_event_group = xEventGroupCreate(); if (!this->s_wifi_event_group) { ESP_LOGE(TAG, "Failed to create WiFi event group"); @@ -34,10 +45,13 @@ WifiHandler::WifiHandler( if (!this->credential_mutex) { ESP_LOGE(TAG, "Failed to create credential mutex"); } - if (this->kvs == nullptr) { - ESP_LOGW(TAG, "KVStorageHandler is null, WiFi credentials will not be stored"); + if (this->fs_handler_ == nullptr) { + ESP_LOGW(TAG, "FSHandler is null, WiFi credentials will not be stored"); } else { - this->kvs->init(nullptr); + esp_err_t err = this->fs_handler_->init("/littlefs"); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to initialize FSHandler: %s", esp_err_to_name(err)); + } } } @@ -55,8 +69,8 @@ WifiHandler::~WifiHandler() { esp_event_handler_unregister(WIFI_EVENT, ESP_EVENT_ANY_ID, &WifiHandler::wifi_event_handler); esp_event_handler_unregister(IP_EVENT, IP_EVENT_STA_GOT_IP, &WifiHandler::wifi_event_handler); this->initialized = false; - // unique_ptr will automatically delete the object - this->kvs = nullptr; + // + this->fs_handler_ = nullptr; } } @@ -323,8 +337,8 @@ void WifiHandler::wifi_event_handler(void* arg, esp_event_base_t event_base, int // void WifiHandler::store_wifi_credentials(const std::string& ssid, const std::string& password) { - if (!kvs) { - ESP_LOGW(TAG, "KVStorageHandler not set, cannot store WiFi credentials"); + if (!fs_handler_) { + ESP_LOGW(TAG, "FSHandler not set, cannot store WiFi credentials"); return; } SemaphoreGuard guard(this->credential_mutex); @@ -334,36 +348,52 @@ void WifiHandler::store_wifi_credentials(const std::string& ssid, const std::str return; } - // Store current SSID - kvs->put(WIFI_SSID_KEY, ssid); - - // Store the password according to the JSON structure - std::string password_key_store = kvs->get(WIFI_PASSWORD_STORE_KEY); cJSON* json = nullptr; - if (password_key_store.empty()) { - // create new JSON object - json = cJSON_CreateObject(); - } else { - // parse existing JSON - json = cJSON_Parse(password_key_store.c_str()); - if (json == nullptr) { - ESP_LOGE(TAG, "Failed to parse existing WiFi password JSON, creating new"); - json = cJSON_CreateObject(); + + // Try to read existing credentials file + if (fs_handler_->file_exists(WIFI_CRED_FILE_PATH)) { + // Read existing file + size_t file_size = 0; + esp_err_t err = fs_handler_->get_file_size(WIFI_CRED_FILE_PATH, file_size); + if (err == ESP_OK && file_size > 0) { + std::vector file_data(file_size + 1); // +1 for null terminator + size_t bytes_read = 0; + err = fs_handler_->read_file(WIFI_CRED_FILE_PATH, file_size, file_data.data(), bytes_read); + if (err == ESP_OK) { + file_data[bytes_read] = '\0'; // Null terminate + json = cJSON_Parse(reinterpret_cast(file_data.data())); + if (json == nullptr) { + ESP_LOGE(TAG, "Failed to parse existing WiFi credentials JSON, creating new"); + } + } } } + + // Create new JSON if parsing failed or file doesn't exist + if (json == nullptr) { + json = cJSON_CreateObject(); + } + + // Set current SSID + cJSON* current_ssid_item = cJSON_GetObjectItem(json, "current_ssid"); + if (current_ssid_item != nullptr) { + cJSON_SetValuestring(current_ssid_item, ssid.c_str()); + } else { + cJSON_AddStringToObject(json, "current_ssid", ssid.c_str()); + } + + // Get or create credentials object cJSON* credentials = cJSON_GetObjectItem(json, "credentials"); if (credentials == nullptr || !cJSON_IsObject(credentials)) { - // create credentials object if it doesn't exist credentials = cJSON_CreateObject(); cJSON_AddItemToObject(json, "credentials", credentials); } - // Limit stored credentials to prevent NVS overflow (keep max 10 SSIDs) + // Limit stored credentials to prevent excessive file size (keep max 10 SSIDs) int credential_count = cJSON_GetArraySize(credentials); if (credential_count >= 10) { - ESP_LOGW(TAG, "Too many stored credentials (%d), clearing old ones", credential_count); - // Keep only the current SSID's credentials, clear others - cJSON_DeleteItemFromObject(credentials, ssid.c_str()); // Remove if exists + ESP_LOGW(TAG, "Too many stored credentials (%d), keeping only current SSID", credential_count); + // Keep only the current SSID's credentials cJSON* new_credentials = cJSON_CreateObject(); cJSON_ReplaceItemInObject(json, "credentials", new_credentials); credentials = new_credentials; @@ -372,20 +402,27 @@ void WifiHandler::store_wifi_credentials(const std::string& ssid, const std::str // Remove existing entry for this SSID to update it cJSON_DeleteItemFromObject(credentials, ssid.c_str()); - // create SSID object + // Create SSID object with password cJSON* ssid_item = cJSON_CreateObject(); - // add password field cJSON_AddStringToObject(ssid_item, "password", password.c_str()); - // add SSID object to credentials cJSON_AddItemToObject(credentials, ssid.c_str(), ssid_item); - // store updated JSON string - char* updated_json_str = cJSON_PrintUnformatted(json); - if (updated_json_str) { - esp_err_t err = ESP_OK; - kvs->put(WIFI_PASSWORD_STORE_KEY, std::string(updated_json_str)); - // Note: Error handling is done in nvs_handler.cpp put() method - cJSON_free(updated_json_str); + // Serialize and write to file + char* json_str = cJSON_PrintUnformatted(json); + if (json_str) { + size_t bytes_written = 0; + esp_err_t err = fs_handler_->write_file( + WIFI_CRED_FILE_PATH, + reinterpret_cast(json_str), + strlen(json_str), + bytes_written + ); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to write WiFi credentials to file: %s", esp_err_to_name(err)); + } else { + ESP_LOGI(TAG, "Stored WiFi credentials for SSID: %s", ssid.c_str()); + } + cJSON_free(json_str); } else { ESP_LOGE(TAG, "Failed to serialize WiFi credentials JSON"); } @@ -393,59 +430,104 @@ void WifiHandler::store_wifi_credentials(const std::string& ssid, const std::str } void WifiHandler::get_wifi_credentials(std::string& out_ssid, std::string& out_password) { - if (!kvs) { - ESP_LOGW(TAG, "KVStorageHandler not set, cannot get WiFi credentials"); - return; - } - SemaphoreGuard guard(this->credential_mutex); - // wait up to 5 seconds to take the mutex - if (!guard.take(5000 / portTICK_PERIOD_MS)) { - ESP_LOGE(TAG, "Failed to take credential mutex"); - return; - } - out_ssid = kvs->get(WIFI_SSID_KEY); - if (out_ssid.empty()) { + if (!fs_handler_) { + ESP_LOGW(TAG, "FSHandler not set, cannot get WiFi credentials"); out_ssid = ""; out_password = ""; return; } - // password is from KV storage, may be nullptr - std::string password_key_store = kvs->get(WIFI_PASSWORD_STORE_KEY); - if (password_key_store.empty()) { + + SemaphoreGuard guard(this->credential_mutex); + // wait up to 5 seconds to take the mutex + if (!guard.take(5000 / portTICK_PERIOD_MS)) { + ESP_LOGE(TAG, "Failed to take credential mutex"); + out_ssid = ""; out_password = ""; return; } - // parse from json - cJSON* json = cJSON_Parse(password_key_store.c_str()); + + // Check if credentials file exists + if (!fs_handler_->file_exists(WIFI_CRED_FILE_PATH)) { + ESP_LOGD(TAG, "WiFi credentials file does not exist"); + out_ssid = ""; + out_password = ""; + return; + } + + // Read credentials file + size_t file_size = 0; + esp_err_t err = fs_handler_->get_file_size(WIFI_CRED_FILE_PATH, file_size); + if (err != ESP_OK || file_size == 0) { + ESP_LOGE(TAG, "Failed to get WiFi credentials file size"); + out_ssid = ""; + out_password = ""; + return; + } + + std::vector file_data(file_size + 1); // +1 for null terminator + size_t bytes_read = 0; + err = fs_handler_->read_file(WIFI_CRED_FILE_PATH, file_size, file_data.data(), bytes_read); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to read WiFi credentials file: %s", esp_err_to_name(err)); + out_ssid = ""; + out_password = ""; + return; + } + file_data[bytes_read] = '\0'; // Null terminate + + // Parse JSON + cJSON* json = cJSON_Parse(reinterpret_cast(file_data.data())); if (json == nullptr) { - ESP_LOGE(TAG, "Failed to parse WiFi password JSON"); + ESP_LOGE(TAG, "Failed to parse WiFi credentials JSON"); + out_ssid = ""; out_password = ""; return; } + + // Get current SSID + cJSON* current_ssid_item = cJSON_GetObjectItem(json, "current_ssid"); + if (current_ssid_item == nullptr || !cJSON_IsString(current_ssid_item)) { + ESP_LOGE(TAG, "WiFi credentials JSON does not contain valid 'current_ssid' field"); + out_ssid = ""; + out_password = ""; + cJSON_Delete(json); + return; + } + out_ssid = current_ssid_item->valuestring; + + // Get credentials object cJSON* credentials = cJSON_GetObjectItem(json, "credentials"); if (credentials == nullptr || !cJSON_IsObject(credentials)) { - ESP_LOGE(TAG, "WiFi password JSON does not contain valid 'credentials' object"); + ESP_LOGE(TAG, "WiFi credentials JSON does not contain valid 'credentials' object"); + out_ssid = ""; out_password = ""; cJSON_Delete(json); return; } - // get the ssid value + + // Get SSID entry cJSON* ssid_item = cJSON_GetObjectItem(credentials, out_ssid.c_str()); if (ssid_item == nullptr || !cJSON_IsObject(ssid_item)) { - ESP_LOGE(TAG, "WiFi password JSON does not contain valid SSID field for SSID: %s", out_ssid.c_str()); + ESP_LOGE(TAG, "WiFi credentials JSON does not contain entry for SSID: %s", out_ssid.c_str()); + out_ssid = ""; out_password = ""; cJSON_Delete(json); return; } - cJSON* password = cJSON_GetObjectItem(ssid_item, "password"); - if (password == nullptr || !cJSON_IsString(password)) { - ESP_LOGE(TAG, "WiFi password JSON does not contain valid 'password' field for SSID: %s", out_ssid.c_str()); + + // Get password + cJSON* password_item = cJSON_GetObjectItem(ssid_item, "password"); + if (password_item == nullptr || !cJSON_IsString(password_item)) { + ESP_LOGE(TAG, "WiFi credentials JSON does not contain valid 'password' field for SSID: %s", out_ssid.c_str()); + out_ssid = ""; out_password = ""; cJSON_Delete(json); return; } - out_password = password->valuestring; + out_password = password_item->valuestring; + cJSON_Delete(json); + ESP_LOGD(TAG, "Retrieved WiFi credentials for SSID: %s", out_ssid.c_str()); } EventBits_t WifiHandler::wait_for_connection(TickType_t ticks_to_wait) { diff --git a/main/network/wifi_handler.h b/main/network/wifi_handler.h index 53e3213..c0dcf59 100644 --- a/main/network/wifi_handler.h +++ b/main/network/wifi_handler.h @@ -1,5 +1,6 @@ #pragma once -#include "io/io.h" +#include "freertos/FreeRTOS.h" +#include "io/fs_handler.h" #include "esp_wifi.h" #include "freertos/event_groups.h" @@ -13,7 +14,7 @@ public: // this handler is used to store/retrieve WiFi credentials // should have a unique namespace for WiFi credentials // it will be owned by WifiHandler and deleted in its destructor - std::unique_ptr kvs + std::shared_ptr fs_handler_ ); ~WifiHandler(); @@ -44,7 +45,7 @@ private: void get_wifi_credentials(std::string& out_ssid, std::string& out_password); bool initialized = false; - std::unique_ptr kvs = nullptr; + std::shared_ptr fs_handler_ = nullptr; EventGroupHandle_t s_wifi_event_group = 0; SemaphoreHandle_t scan_mutex = nullptr; SemaphoreHandle_t connection_mutex = nullptr; diff --git a/partitions.csv b/partitions.csv index 3ed090e..32c9d13 100644 --- a/partitions.csv +++ b/partitions.csv @@ -8,5 +8,5 @@ phy_init, data, phy, , 0x1000, # OTA Partitions 10MB ota_0, app, ota_0, , 0xA00000, ota_1, app, ota_1, , 0xA00000, -# SPIFFS 11MB -storage, data, spiffs, , 0xB00000, \ No newline at end of file +# LittleFS 11MB +storage, data, littlefs, , 0xB00000, \ No newline at end of file diff --git a/sdkconfig.default b/sdkconfig.default index 6da4130..056c866 100644 --- a/sdkconfig.default +++ b/sdkconfig.default @@ -587,10 +587,10 @@ CONFIG_ESPTOOLPY_MONITOR_BAUD=115200 # CONFIG_PARTITION_TABLE_SINGLE_APP is not set # CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE is not set # CONFIG_PARTITION_TABLE_TWO_OTA is not set -CONFIG_PARTITION_TABLE_TWO_OTA_LARGE=y -# CONFIG_PARTITION_TABLE_CUSTOM is not set +# CONFIG_PARTITION_TABLE_TWO_OTA_LARGE is not set +CONFIG_PARTITION_TABLE_CUSTOM=y CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv" -CONFIG_PARTITION_TABLE_FILENAME="partitions_two_ota_large.csv" +CONFIG_PARTITION_TABLE_FILENAME="partitions.csv" CONFIG_PARTITION_TABLE_OFFSET=0x8000 CONFIG_PARTITION_TABLE_MD5=y # end of Partition Table @@ -1996,7 +1996,7 @@ CONFIG_LITTLEFS_MALLOC_STRATEGY_DEFAULT=y # CONFIG_LITTLEFS_MALLOC_STRATEGY_SPIRAM is not set CONFIG_LITTLEFS_ASSERTS=y # CONFIG_LITTLEFS_MMAP_PARTITION is not set -CONFIG_LITTLEFS_WDT_RESET=y +# CONFIG_LITTLEFS_WDT_RESET is not set # end of LittleFS # @@ -2303,10 +2303,10 @@ CONFIG_LV_USE_WIN=y # CONFIG_LV_USE_THEME_DEFAULT=y # CONFIG_LV_THEME_DEFAULT_DARK is not set -CONFIG_LV_THEME_DEFAULT_GROW=y -CONFIG_LV_THEME_DEFAULT_TRANSITION_TIME=80 +# CONFIG_LV_THEME_DEFAULT_GROW is not set +CONFIG_LV_THEME_DEFAULT_TRANSITION_TIME=0 CONFIG_LV_USE_THEME_SIMPLE=y -# CONFIG_LV_USE_THEME_MONO is not set +CONFIG_LV_USE_THEME_MONO=y # end of Themes #