feat: implement LittleFSHandler and FSGuard for improved file management
This commit is contained in:
552
main/io/fs_handler.cpp
Normal file
552
main/io/fs_handler.cpp
Normal 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
95
main/io/fs_handler.h
Normal 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_;
|
||||
};
|
||||
|
||||
|
||||
@@ -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<WifiHandler>(
|
||||
std::unique_ptr<KVStorageHandler>(new NVSStorageHandler(WIFI_CREDENTIALS_STORAGE_NAMESPACE))
|
||||
);
|
||||
auto fs_handler = std::make_shared<LittleFSHandler>();
|
||||
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));
|
||||
EInkDisplayHandler* display_handler = new EInkDisplayHandler();
|
||||
// Initialize display and touch
|
||||
|
||||
@@ -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<KVStorageHandler> kvs
|
||||
) : kvs(std::move(kvs)) {
|
||||
std::shared_ptr<LittleFSHandler> 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<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) {
|
||||
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<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 {
|
||||
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<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) {
|
||||
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) {
|
||||
|
||||
@@ -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<KVStorageHandler> kvs
|
||||
std::shared_ptr<LittleFSHandler> 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<KVStorageHandler> kvs = nullptr;
|
||||
std::shared_ptr<LittleFSHandler> fs_handler_ = nullptr;
|
||||
EventGroupHandle_t s_wifi_event_group = 0;
|
||||
SemaphoreHandle_t scan_mutex = nullptr;
|
||||
SemaphoreHandle_t connection_mutex = nullptr;
|
||||
|
||||
Reference in New Issue
Block a user