feat: implement LittleFSHandler and FSGuard for improved file management

This commit is contained in:
GW_MC
2026-01-30 15:23:44 +08:00
parent b6c4477c46
commit 2a5088bec3
7 changed files with 821 additions and 85 deletions

552
main/io/fs_handler.cpp Normal file
View File

@@ -0,0 +1,552 @@
#include "io/fs_handler.h"
#include <sys/stat.h>
#include <dirent.h>
#include <errno.h>
#include <cstring>
#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<std::string>& 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<uint8_t> 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<bool> 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_;
}

95
main/io/fs_handler.h Normal file
View File

@@ -0,0 +1,95 @@
#pragma once
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "esp_littlefs.h"
#include "esp_err.h"
#include <string>
#include <vector>
#include <map>
#include <memory>
#include "esp_log.h"
#include <semaphore_guard.h>
// 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<std::string>& 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_;
};

View File

@@ -13,6 +13,7 @@
#include "common/constants.h" #include "common/constants.h"
#include "common/queue_defs.h" #include "common/queue_defs.h"
#include "io/nvs_handler.h" #include "io/nvs_handler.h"
#include "io/fs_handler.h"
#include "info/info.h" #include "info/info.h"
#include "display/eink_display_handler.h" #include "display/eink_display_handler.h"
#include "display/lvgl_handler.h" #include "display/lvgl_handler.h"
@@ -31,7 +32,6 @@
// nvs storage namespaces, 15 characters max // nvs storage namespaces, 15 characters max
#define DEFAULT_STORAGE_NAMESPACE "storage" #define DEFAULT_STORAGE_NAMESPACE "storage"
#define WIFI_CREDENTIALS_STORAGE_NAMESPACE "wifi_cred"
#define TAG "Main" #define TAG "Main"
extern "C" void app_main(void); extern "C" void app_main(void);
@@ -61,9 +61,15 @@ void app_main(void) {
DEFAULT_STORAGE_NAMESPACE DEFAULT_STORAGE_NAMESPACE
); );
auto wifi_handler = std::make_unique<WifiHandler>( auto fs_handler = std::make_shared<LittleFSHandler>();
std::unique_ptr<KVStorageHandler>(new NVSStorageHandler(WIFI_CREDENTIALS_STORAGE_NAMESPACE)) 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<WifiHandler>(fs_handler);
NetworkHandler* network_handler = new NetworkHandler(std::move(wifi_handler)); NetworkHandler* network_handler = new NetworkHandler(std::move(wifi_handler));
EInkDisplayHandler* display_handler = new EInkDisplayHandler(); EInkDisplayHandler* display_handler = new EInkDisplayHandler();
// Initialize display and touch // Initialize display and touch

View File

@@ -8,16 +8,27 @@
#include "common/semaphore_guard.h" #include "common/semaphore_guard.h"
#include "cJSON.h" #include "cJSON.h"
static const char* TAG = "WifiHandler"; #define TAG "WifiHandler"
static const char* WIFI_SSID_KEY = "ssid"; #define WIFI_CRED_FILE_PATH "wifi_credentials.json"
static const char* WIFI_PASSWORD_STORE_KEY = "psw";
/*
* WiFi Credentials JSON Structure:
* {
* "current_ssid": "MyWiFi",
* "credentials": {
* "MyWiFi": {
* "password": "mypassword123"
* },
* "OtherNetwork": {
* "password": "otherpass456"
* }
* }
* }
*/
WifiHandler::WifiHandler( WifiHandler::WifiHandler(
// this handler is used to store/retrieve WiFi credentials std::shared_ptr<LittleFSHandler> fs_handler_
// should have a unique namespace for WiFi credentials ) : fs_handler_(std::move(fs_handler_)) {
// it will be owned by WifiHandler and deleted in its destructor
std::unique_ptr<KVStorageHandler> kvs
) : kvs(std::move(kvs)) {
this->s_wifi_event_group = xEventGroupCreate(); this->s_wifi_event_group = xEventGroupCreate();
if (!this->s_wifi_event_group) { if (!this->s_wifi_event_group) {
ESP_LOGE(TAG, "Failed to create WiFi event group"); ESP_LOGE(TAG, "Failed to create WiFi event group");
@@ -34,10 +45,13 @@ WifiHandler::WifiHandler(
if (!this->credential_mutex) { if (!this->credential_mutex) {
ESP_LOGE(TAG, "Failed to create credential mutex"); ESP_LOGE(TAG, "Failed to create credential mutex");
} }
if (this->kvs == nullptr) { if (this->fs_handler_ == nullptr) {
ESP_LOGW(TAG, "KVStorageHandler is null, WiFi credentials will not be stored"); ESP_LOGW(TAG, "FSHandler is null, WiFi credentials will not be stored");
} else { } 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(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); esp_event_handler_unregister(IP_EVENT, IP_EVENT_STA_GOT_IP, &WifiHandler::wifi_event_handler);
this->initialized = false; 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) { void WifiHandler::store_wifi_credentials(const std::string& ssid, const std::string& password) {
if (!kvs) { if (!fs_handler_) {
ESP_LOGW(TAG, "KVStorageHandler not set, cannot store WiFi credentials"); ESP_LOGW(TAG, "FSHandler not set, cannot store WiFi credentials");
return; return;
} }
SemaphoreGuard guard(this->credential_mutex); SemaphoreGuard guard(this->credential_mutex);
@@ -334,36 +348,52 @@ void WifiHandler::store_wifi_credentials(const std::string& ssid, const std::str
return; 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; cJSON* json = nullptr;
if (password_key_store.empty()) {
// create new JSON object // Try to read existing credentials file
json = cJSON_CreateObject(); if (fs_handler_->file_exists(WIFI_CRED_FILE_PATH)) {
} else { // Read existing file
// parse existing JSON size_t file_size = 0;
json = cJSON_Parse(password_key_store.c_str()); esp_err_t err = fs_handler_->get_file_size(WIFI_CRED_FILE_PATH, file_size);
if (err == ESP_OK && file_size > 0) {
std::vector<uint8_t> 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<const char*>(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) { if (json == nullptr) {
ESP_LOGE(TAG, "Failed to parse existing WiFi password JSON, creating new");
json = cJSON_CreateObject(); 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"); cJSON* credentials = cJSON_GetObjectItem(json, "credentials");
if (credentials == nullptr || !cJSON_IsObject(credentials)) { if (credentials == nullptr || !cJSON_IsObject(credentials)) {
// create credentials object if it doesn't exist
credentials = cJSON_CreateObject(); credentials = cJSON_CreateObject();
cJSON_AddItemToObject(json, "credentials", credentials); 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); int credential_count = cJSON_GetArraySize(credentials);
if (credential_count >= 10) { if (credential_count >= 10) {
ESP_LOGW(TAG, "Too many stored credentials (%d), clearing old ones", credential_count); ESP_LOGW(TAG, "Too many stored credentials (%d), keeping only current SSID", credential_count);
// Keep only the current SSID's credentials, clear others // Keep only the current SSID's credentials
cJSON_DeleteItemFromObject(credentials, ssid.c_str()); // Remove if exists
cJSON* new_credentials = cJSON_CreateObject(); cJSON* new_credentials = cJSON_CreateObject();
cJSON_ReplaceItemInObject(json, "credentials", new_credentials); cJSON_ReplaceItemInObject(json, "credentials", new_credentials);
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 // Remove existing entry for this SSID to update it
cJSON_DeleteItemFromObject(credentials, ssid.c_str()); cJSON_DeleteItemFromObject(credentials, ssid.c_str());
// create SSID object // Create SSID object with password
cJSON* ssid_item = cJSON_CreateObject(); cJSON* ssid_item = cJSON_CreateObject();
// add password field
cJSON_AddStringToObject(ssid_item, "password", password.c_str()); cJSON_AddStringToObject(ssid_item, "password", password.c_str());
// add SSID object to credentials
cJSON_AddItemToObject(credentials, ssid.c_str(), ssid_item); cJSON_AddItemToObject(credentials, ssid.c_str(), ssid_item);
// store updated JSON string // Serialize and write to file
char* updated_json_str = cJSON_PrintUnformatted(json); char* json_str = cJSON_PrintUnformatted(json);
if (updated_json_str) { if (json_str) {
esp_err_t err = ESP_OK; size_t bytes_written = 0;
kvs->put(WIFI_PASSWORD_STORE_KEY, std::string(updated_json_str)); esp_err_t err = fs_handler_->write_file(
// Note: Error handling is done in nvs_handler.cpp put() method WIFI_CRED_FILE_PATH,
cJSON_free(updated_json_str); reinterpret_cast<const uint8_t*>(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 { } else {
ESP_LOGE(TAG, "Failed to serialize WiFi credentials JSON"); 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) { void WifiHandler::get_wifi_credentials(std::string& out_ssid, std::string& out_password) {
if (!kvs) { if (!fs_handler_) {
ESP_LOGW(TAG, "KVStorageHandler not set, cannot get WiFi credentials"); ESP_LOGW(TAG, "FSHandler 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()) {
out_ssid = ""; out_ssid = "";
out_password = ""; out_password = "";
return; return;
} }
// password is from KV storage, may be nullptr
std::string password_key_store = kvs->get(WIFI_PASSWORD_STORE_KEY); SemaphoreGuard guard(this->credential_mutex);
if (password_key_store.empty()) { // 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 = ""; out_password = "";
return; 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<uint8_t> 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<const char*>(file_data.data()));
if (json == nullptr) { 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 = ""; out_password = "";
return; 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"); cJSON* credentials = cJSON_GetObjectItem(json, "credentials");
if (credentials == nullptr || !cJSON_IsObject(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 = ""; out_password = "";
cJSON_Delete(json); cJSON_Delete(json);
return; return;
} }
// get the ssid value
// Get SSID entry
cJSON* ssid_item = cJSON_GetObjectItem(credentials, out_ssid.c_str()); cJSON* ssid_item = cJSON_GetObjectItem(credentials, out_ssid.c_str());
if (ssid_item == nullptr || !cJSON_IsObject(ssid_item)) { 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 = ""; out_password = "";
cJSON_Delete(json); cJSON_Delete(json);
return; return;
} }
cJSON* password = cJSON_GetObjectItem(ssid_item, "password");
if (password == nullptr || !cJSON_IsString(password)) { // Get password
ESP_LOGE(TAG, "WiFi password JSON does not contain valid 'password' field for SSID: %s", out_ssid.c_str()); 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 = ""; out_password = "";
cJSON_Delete(json); cJSON_Delete(json);
return; return;
} }
out_password = password->valuestring; out_password = password_item->valuestring;
cJSON_Delete(json); 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) { EventBits_t WifiHandler::wait_for_connection(TickType_t ticks_to_wait) {

View File

@@ -1,5 +1,6 @@
#pragma once #pragma once
#include "io/io.h" #include "freertos/FreeRTOS.h"
#include "io/fs_handler.h"
#include "esp_wifi.h" #include "esp_wifi.h"
#include "freertos/event_groups.h" #include "freertos/event_groups.h"
@@ -13,7 +14,7 @@ public:
// this handler is used to store/retrieve WiFi credentials // this handler is used to store/retrieve WiFi credentials
// should have a unique namespace for WiFi credentials // should have a unique namespace for WiFi credentials
// it will be owned by WifiHandler and deleted in its destructor // it will be owned by WifiHandler and deleted in its destructor
std::unique_ptr<KVStorageHandler> kvs std::shared_ptr<LittleFSHandler> fs_handler_
); );
~WifiHandler(); ~WifiHandler();
@@ -44,7 +45,7 @@ private:
void get_wifi_credentials(std::string& out_ssid, std::string& out_password); void get_wifi_credentials(std::string& out_ssid, std::string& out_password);
bool initialized = false; bool initialized = false;
std::unique_ptr<KVStorageHandler> kvs = nullptr; std::shared_ptr<LittleFSHandler> fs_handler_ = nullptr;
EventGroupHandle_t s_wifi_event_group = 0; EventGroupHandle_t s_wifi_event_group = 0;
SemaphoreHandle_t scan_mutex = nullptr; SemaphoreHandle_t scan_mutex = nullptr;
SemaphoreHandle_t connection_mutex = nullptr; SemaphoreHandle_t connection_mutex = nullptr;

View File

@@ -8,5 +8,5 @@ phy_init, data, phy, , 0x1000,
# OTA Partitions 10MB # OTA Partitions 10MB
ota_0, app, ota_0, , 0xA00000, ota_0, app, ota_0, , 0xA00000,
ota_1, app, ota_1, , 0xA00000, ota_1, app, ota_1, , 0xA00000,
# SPIFFS 11MB # LittleFS 11MB
storage, data, spiffs, , 0xB00000, storage, data, littlefs, , 0xB00000,
1 # Name, Type, SubType, Offset, Size, Flags
8 # OTA Partitions 10MB
9 ota_0, app, ota_0, , 0xA00000,
10 ota_1, app, ota_1, , 0xA00000,
11 # SPIFFS 11MB # LittleFS 11MB
12 storage, data, spiffs, , 0xB00000, storage, data, littlefs, , 0xB00000,

View File

@@ -587,10 +587,10 @@ CONFIG_ESPTOOLPY_MONITOR_BAUD=115200
# CONFIG_PARTITION_TABLE_SINGLE_APP is not set # CONFIG_PARTITION_TABLE_SINGLE_APP is not set
# CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE 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 is not set
CONFIG_PARTITION_TABLE_TWO_OTA_LARGE=y # CONFIG_PARTITION_TABLE_TWO_OTA_LARGE is not set
# CONFIG_PARTITION_TABLE_CUSTOM is not set CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv" 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_OFFSET=0x8000
CONFIG_PARTITION_TABLE_MD5=y CONFIG_PARTITION_TABLE_MD5=y
# end of Partition Table # end of Partition Table
@@ -1996,7 +1996,7 @@ CONFIG_LITTLEFS_MALLOC_STRATEGY_DEFAULT=y
# CONFIG_LITTLEFS_MALLOC_STRATEGY_SPIRAM is not set # CONFIG_LITTLEFS_MALLOC_STRATEGY_SPIRAM is not set
CONFIG_LITTLEFS_ASSERTS=y CONFIG_LITTLEFS_ASSERTS=y
# CONFIG_LITTLEFS_MMAP_PARTITION is not set # CONFIG_LITTLEFS_MMAP_PARTITION is not set
CONFIG_LITTLEFS_WDT_RESET=y # CONFIG_LITTLEFS_WDT_RESET is not set
# end of LittleFS # end of LittleFS
# #
@@ -2303,10 +2303,10 @@ CONFIG_LV_USE_WIN=y
# #
CONFIG_LV_USE_THEME_DEFAULT=y CONFIG_LV_USE_THEME_DEFAULT=y
# CONFIG_LV_THEME_DEFAULT_DARK is not set # CONFIG_LV_THEME_DEFAULT_DARK is not set
CONFIG_LV_THEME_DEFAULT_GROW=y # CONFIG_LV_THEME_DEFAULT_GROW is not set
CONFIG_LV_THEME_DEFAULT_TRANSITION_TIME=80 CONFIG_LV_THEME_DEFAULT_TRANSITION_TIME=0
CONFIG_LV_USE_THEME_SIMPLE=y CONFIG_LV_USE_THEME_SIMPLE=y
# CONFIG_LV_USE_THEME_MONO is not set CONFIG_LV_USE_THEME_MONO=y
# end of Themes # end of Themes
# #