diff --git a/libraries/ArduinoStorage/ArduinoStorage.h b/libraries/ArduinoStorage/ArduinoStorage.h new file mode 100644 index 000000000..2dbd27781 --- /dev/null +++ b/libraries/ArduinoStorage/ArduinoStorage.h @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2024 Arduino SA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ARDUINO_STORAGE_H +#define ARDUINO_STORAGE_H + +/** + * @brief Arduino Storage Library + * + * This library provides a unified interface for all Arduino storage implementations. + * It defines the base classes and error handling that all storage types (QSPI, SD, Flash, etc.) + * conform to. + * + * Include this header to get access to: + * - StorageError: Error handling class + * - File: Abstract base class for file operations + * - Folder: Abstract base class for folder operations + * - FileMode: Enum for file opening modes + * - FilesystemType: Enum for filesystem types + * + * For specific storage implementations, include their respective headers: + * - QSPIStorage.h: QSPI flash storage + * - SDStorage.h: SD card storage + * - FlashStorage.h: Internal flash storage + */ + +#include "StorageCommon.h" +#include "StorageError.h" +#include "StorageFile.h" +#include "StorageFolder.h" + +#endif // ARDUINO_STORAGE_H diff --git a/libraries/ArduinoStorage/CMakeLists.txt b/libraries/ArduinoStorage/CMakeLists.txt new file mode 100644 index 000000000..a7dccfab9 --- /dev/null +++ b/libraries/ArduinoStorage/CMakeLists.txt @@ -0,0 +1,10 @@ +# SPDX-License-Identifier: Apache-2.0 +zephyr_include_directories(.) + +if(NOT DEFINED ARDUINO_BUILD_PATH) + zephyr_sources_ifdef(CONFIG_FILE_SYSTEM + StorageError.cpp + StorageFile.cpp + StorageFolder.cpp + ) +endif() diff --git a/libraries/ArduinoStorage/StorageCommon.h b/libraries/ArduinoStorage/StorageCommon.h new file mode 100644 index 000000000..0c448e8db --- /dev/null +++ b/libraries/ArduinoStorage/StorageCommon.h @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2024 Arduino SA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ARDUINO_STORAGE_COMMON_H +#define ARDUINO_STORAGE_COMMON_H + +#include + +// File opening modes +enum class FileMode { + READ, // Open for reading, file must exist + WRITE, // Open for writing, creates if doesn't exist, truncates if exists + APPEND, // Open for writing at end, creates if doesn't exist + READ_WRITE, // Open for reading and writing, file must exist + READ_WRITE_CREATE // Open for reading and writing, creates if doesn't exist +}; + +// Supported filesystem types +enum class FilesystemType { + LITTLEFS, // LittleFS - recommended for flash storage + FAT, // FAT32 - better compatibility, larger overhead + EXT2, // Extended 2 - Linux-style filesystem + AUTO // Auto-detect or use default +}; + +// Storage information structure +struct StorageInfo { + char mountPoint[64]; + FilesystemType fsType; + size_t totalBytes; + size_t usedBytes; + size_t availableBytes; + size_t blockSize; + size_t totalBlocks; + size_t usedBlocks; + bool readOnly; + bool mounted; +}; + +// Storage health structure +struct StorageHealth { + bool healthy; // Overall health status + uint32_t errorCount; // Number of errors encountered + uint32_t badBlocks; // Number of bad blocks (flash) + uint32_t writeCount; // Total write operations + uint32_t eraseCount; // Total erase operations + float fragmentationPercent; // File system fragmentation + char statusMessage[128]; // Human-readable status +}; + +// Partition information +struct PartitionInfo { + const char* label; // Partition name/label + size_t offset; // Start offset in bytes + size_t size; // Size in bytes + FilesystemType fsType; // File system type for this partition +}; + +// Maximum path length +constexpr size_t STORAGE_MAX_PATH_LENGTH = 256; + +#endif // ARDUINO_STORAGE_COMMON_H diff --git a/libraries/ArduinoStorage/StorageError.cpp b/libraries/ArduinoStorage/StorageError.cpp new file mode 100644 index 000000000..15ee09f94 --- /dev/null +++ b/libraries/ArduinoStorage/StorageError.cpp @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2024 Arduino SA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "StorageError.h" +#include + +StorageError::StorageError() : code_(StorageErrorCode::NONE) { + message_[0] = '\0'; +} + +StorageErrorCode StorageError::getCode() const { + return code_; +} + +const char* StorageError::getMessage() const { + if (message_[0] != '\0') { + return message_; + } + + // Return default message based on error code + switch (code_) { + case StorageErrorCode::NONE: + return "No error"; + case StorageErrorCode::FILE_NOT_FOUND: + return "File not found"; + case StorageErrorCode::FOLDER_NOT_FOUND: + return "Folder not found"; + case StorageErrorCode::ALREADY_EXISTS: + return "Already exists"; + case StorageErrorCode::INVALID_PATH: + return "Invalid path"; + case StorageErrorCode::PERMISSION_DENIED: + return "Permission denied"; + case StorageErrorCode::READ_ERROR: + return "Read error"; + case StorageErrorCode::WRITE_ERROR: + return "Write error"; + case StorageErrorCode::SEEK_ERROR: + return "Seek error"; + case StorageErrorCode::OPEN_ERROR: + return "Open error"; + case StorageErrorCode::CLOSE_ERROR: + return "Close error"; + case StorageErrorCode::STORAGE_FULL: + return "Storage full"; + case StorageErrorCode::STORAGE_NOT_MOUNTED: + return "Storage not mounted"; + case StorageErrorCode::STORAGE_CORRUPTED: + return "Storage corrupted"; + case StorageErrorCode::STORAGE_NOT_FORMATTED: + return "Storage not formatted"; + case StorageErrorCode::INVALID_OPERATION: + return "Invalid operation"; + case StorageErrorCode::INVALID_MODE: + return "Invalid mode"; + case StorageErrorCode::BUFFER_OVERFLOW: + return "Buffer overflow"; + case StorageErrorCode::OUT_OF_MEMORY: + return "Out of memory"; + case StorageErrorCode::TIMEOUT: + return "Timeout"; + case StorageErrorCode::HARDWARE_ERROR: + return "Hardware error"; + case StorageErrorCode::NOT_INITIALIZED: + return "Not initialized"; + case StorageErrorCode::UNKNOWN_ERROR: + default: + return "Unknown error"; + } +} + +bool StorageError::hasError() const { + return code_ != StorageErrorCode::NONE; +} + +void StorageError::setError(StorageErrorCode code, const char* message) { + code_ = code; + if (message != nullptr) { + strncpy(message_, message, sizeof(message_) - 1); + message_[sizeof(message_) - 1] = '\0'; + } else { + message_[0] = '\0'; + } +} + +void StorageError::clear() { + code_ = StorageErrorCode::NONE; + message_[0] = '\0'; +} + +StorageError::operator bool() const { + return hasError(); +} diff --git a/libraries/ArduinoStorage/StorageError.h b/libraries/ArduinoStorage/StorageError.h new file mode 100644 index 000000000..510cb4f8d --- /dev/null +++ b/libraries/ArduinoStorage/StorageError.h @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2024 Arduino SA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ARDUINO_STORAGE_ERROR_H +#define ARDUINO_STORAGE_ERROR_H + +#include + +enum class StorageErrorCode { + NONE = 0, + + // File/Folder errors + FILE_NOT_FOUND, + FOLDER_NOT_FOUND, + ALREADY_EXISTS, + INVALID_PATH, + PERMISSION_DENIED, + + // I/O errors + READ_ERROR, + WRITE_ERROR, + SEEK_ERROR, + OPEN_ERROR, + CLOSE_ERROR, + + // Storage errors + STORAGE_FULL, + STORAGE_NOT_MOUNTED, + STORAGE_CORRUPTED, + STORAGE_NOT_FORMATTED, + + // Operation errors + INVALID_OPERATION, + INVALID_MODE, + BUFFER_OVERFLOW, + OUT_OF_MEMORY, + TIMEOUT, + + // Hardware errors + HARDWARE_ERROR, + NOT_INITIALIZED, + + // Generic + UNKNOWN_ERROR +}; + +class StorageError { +public: + StorageError(); + + // Error state + StorageErrorCode getCode() const; + const char* getMessage() const; + bool hasError() const; + + // Error setting (for implementations) + void setError(StorageErrorCode code, const char* message = nullptr); + void clear(); + + // Convenience operators + operator bool() const; // Returns true if error exists + +private: + StorageErrorCode code_; + char message_[128]; +}; + +#endif // ARDUINO_STORAGE_ERROR_H diff --git a/libraries/ArduinoStorage/StorageFile.cpp b/libraries/ArduinoStorage/StorageFile.cpp new file mode 100644 index 000000000..e2aac2dc1 --- /dev/null +++ b/libraries/ArduinoStorage/StorageFile.cpp @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2024 Arduino SA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "StorageFile.h" +#include + +File::File() { + path_[0] = '\0'; +} + +File::File(const char* path) { + if (path != nullptr) { + strncpy(path_, path, sizeof(path_) - 1); + path_[sizeof(path_) - 1] = '\0'; + } else { + path_[0] = '\0'; + } +} + +File::File(const String& path) { + strncpy(path_, path.c_str(), sizeof(path_) - 1); + path_[sizeof(path_) - 1] = '\0'; +} + +File::~File() { +} + +const char* File::getPath() const { + return path_; +} + +String File::getPathAsString() const { + return String(path_); +} + +String File::getFilename() const { + const char* lastSep = strrchr(path_, '/'); + if (lastSep != nullptr) { + return String(lastSep + 1); + } + return String(path_); +} + +void File::setErrorIfNotNull(StorageError* error, StorageErrorCode code, const char* message) { + if (error != nullptr) { + error->setError(code, message); + } +} diff --git a/libraries/ArduinoStorage/StorageFile.h b/libraries/ArduinoStorage/StorageFile.h new file mode 100644 index 000000000..800cadbab --- /dev/null +++ b/libraries/ArduinoStorage/StorageFile.h @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2024 Arduino SA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ARDUINO_STORAGE_FILE_H +#define ARDUINO_STORAGE_FILE_H + +#include +#include "StorageCommon.h" +#include "StorageError.h" + +// Forward declaration +class Folder; + +/** + * @brief Abstract base class for file operations in Arduino storage implementations. + * + * This class defines the interface for all file operations. Concrete implementations + * (QSPIFile, SDFile, etc.) should inherit from this class and implement all + * pure virtual methods. + */ +class File { +public: + File(); + File(const char* path); + File(const String& path); + virtual ~File(); + + // Opening and Closing + virtual bool open(const char* filename, FileMode mode, StorageError* error = nullptr) = 0; + virtual bool open(const String& filename, FileMode mode, StorageError* error = nullptr) = 0; + virtual bool open(FileMode mode = FileMode::READ, StorageError* error = nullptr) = 0; + virtual bool close(StorageError* error = nullptr) = 0; + virtual bool changeMode(FileMode mode, StorageError* error = nullptr) = 0; + virtual bool isOpen() const = 0; + + // Reading Operations + virtual size_t read(uint8_t* buffer, size_t size, StorageError* error = nullptr) = 0; + virtual int read(StorageError* error = nullptr) = 0; + virtual String readAsString(StorageError* error = nullptr) = 0; + virtual uint32_t available(StorageError* error = nullptr) = 0; + virtual bool seek(size_t offset, StorageError* error = nullptr) = 0; + virtual size_t position(StorageError* error = nullptr) = 0; + virtual size_t size(StorageError* error = nullptr) = 0; + + // Writing Operations + virtual size_t write(const uint8_t* buffer, size_t size, StorageError* error = nullptr) = 0; + virtual size_t write(const String& data, StorageError* error = nullptr) = 0; + virtual size_t write(uint8_t value, StorageError* error = nullptr) = 0; + virtual bool flush(StorageError* error = nullptr) = 0; + + // File Management + virtual bool exists(StorageError* error = nullptr) const = 0; + virtual bool remove(StorageError* error = nullptr) = 0; + virtual bool rename(const char* newFilename, StorageError* error = nullptr) = 0; + virtual bool rename(const String& newFilename, StorageError* error = nullptr) = 0; + + // Path Information + virtual const char* getPath() const; + virtual String getPathAsString() const; + virtual String getFilename() const; + +protected: + char path_[STORAGE_MAX_PATH_LENGTH]; + + // Helper to set error if pointer is not null + static void setErrorIfNotNull(StorageError* error, StorageErrorCode code, const char* message = nullptr); +}; + +#endif // ARDUINO_STORAGE_FILE_H diff --git a/libraries/ArduinoStorage/StorageFolder.cpp b/libraries/ArduinoStorage/StorageFolder.cpp new file mode 100644 index 000000000..3373323ea --- /dev/null +++ b/libraries/ArduinoStorage/StorageFolder.cpp @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2024 Arduino SA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "StorageFolder.h" +#include + +Folder::Folder() { + path_[0] = '\0'; +} + +Folder::Folder(const char* path) { + if (path != nullptr) { + strncpy(path_, path, sizeof(path_) - 1); + path_[sizeof(path_) - 1] = '\0'; + } else { + path_[0] = '\0'; + } +} + +Folder::Folder(const String& path) { + strncpy(path_, path.c_str(), sizeof(path_) - 1); + path_[sizeof(path_) - 1] = '\0'; +} + +Folder::~Folder() { +} + +const char* Folder::getPath() const { + return path_; +} + +String Folder::getPathAsString() const { + return String(path_); +} + +String Folder::getFolderName() const { + const char* lastSep = strrchr(path_, '/'); + if (lastSep != nullptr && *(lastSep + 1) != '\0') { + return String(lastSep + 1); + } + // Handle root path or trailing slash + if (path_[0] == '/' && path_[1] == '\0') { + return String("/"); + } + return String(path_); +} + +void Folder::setErrorIfNotNull(StorageError* error, StorageErrorCode code, const char* message) { + if (error != nullptr) { + error->setError(code, message); + } +} diff --git a/libraries/ArduinoStorage/StorageFolder.h b/libraries/ArduinoStorage/StorageFolder.h new file mode 100644 index 000000000..6396c7ba6 --- /dev/null +++ b/libraries/ArduinoStorage/StorageFolder.h @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2024 Arduino SA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ARDUINO_STORAGE_FOLDER_H +#define ARDUINO_STORAGE_FOLDER_H + +#include +#include +#include "StorageCommon.h" +#include "StorageError.h" +#include "StorageFile.h" + +/** + * @brief Abstract base class for folder/directory operations in Arduino storage implementations. + * + * This class defines the interface for all folder operations. Concrete implementations + * (QSPIFolder, SDFolder, etc.) should inherit from this class and implement all + * pure virtual methods. + */ +class Folder { +public: + Folder(); + Folder(const char* path); + Folder(const String& path); + virtual ~Folder(); + + // Directory Management + virtual bool exists(StorageError* error = nullptr) const = 0; + virtual bool create(StorageError* error = nullptr) = 0; + virtual bool remove(bool recursive = false, StorageError* error = nullptr) = 0; + virtual bool rename(const char* newName, StorageError* error = nullptr) = 0; + virtual bool rename(const String& newName, StorageError* error = nullptr) = 0; + + // Content Enumeration + virtual size_t getFileCount(StorageError* error = nullptr) = 0; + virtual size_t getFolderCount(StorageError* error = nullptr) = 0; + + // Path Information + virtual const char* getPath() const; + virtual String getPathAsString() const; + virtual String getFolderName() const; + +protected: + char path_[STORAGE_MAX_PATH_LENGTH]; + + // Helper to set error if pointer is not null + static void setErrorIfNotNull(StorageError* error, StorageErrorCode code, const char* message = nullptr); +}; + +#endif // ARDUINO_STORAGE_FOLDER_H diff --git a/libraries/ArduinoStorage/library.properties b/libraries/ArduinoStorage/library.properties new file mode 100644 index 000000000..cd73b48be --- /dev/null +++ b/libraries/ArduinoStorage/library.properties @@ -0,0 +1,9 @@ +name=ArduinoStorage +version=1.0.0 +author=Arduino +maintainer=Arduino +sentence=Base storage API for Arduino storage implementations. +paragraph=Provides abstract File, Folder, and StorageError classes that storage implementations (QSPI, SD, etc.) inherit from. +category=Data Storage +url=https://github.com/arduino/ArduinoCore-zephyr +architectures=* diff --git a/libraries/CMakeLists.txt b/libraries/CMakeLists.txt deleted file mode 100644 index 5600b327d..000000000 --- a/libraries/CMakeLists.txt +++ /dev/null @@ -1,4 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 - -add_subdirectory(Wire) -add_subdirectory(SPI) diff --git a/libraries/QSPIStorage/QSPIFile.cpp b/libraries/QSPIStorage/QSPIFile.cpp new file mode 100644 index 000000000..2fb8a4623 --- /dev/null +++ b/libraries/QSPIStorage/QSPIFile.cpp @@ -0,0 +1,444 @@ +/* + * Copyright (c) 2024 Arduino SA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "QSPIFile.h" +#include "QSPIFolder.h" + +#include +#include +#include + +QSPIFile::QSPIFile() : File(), file_(nullptr), is_open_(false), mode_(FileMode::READ) { +} + +QSPIFile::QSPIFile(const char* path) : File(path), file_(nullptr), is_open_(false), mode_(FileMode::READ) { +} + +QSPIFile::QSPIFile(const String& path) : File(path), file_(nullptr), is_open_(false), mode_(FileMode::READ) { +} + +QSPIFile::~QSPIFile() { + if (is_open_) { + close(nullptr); + } + freeFileHandle(); +} + +bool QSPIFile::ensureFileHandle() { + if (file_ == nullptr) { + file_ = new struct fs_file_t; + if (file_ == nullptr) { + return false; + } + fs_file_t_init(file_); + } + return true; +} + +void QSPIFile::freeFileHandle() { + if (file_ != nullptr) { + delete file_; + file_ = nullptr; + } +} + +int QSPIFile::fileModeToFlags(FileMode mode) { + switch (mode) { + case FileMode::READ: + return FS_O_READ; + case FileMode::WRITE: + return FS_O_WRITE | FS_O_CREATE; + case FileMode::APPEND: + return FS_O_WRITE | FS_O_CREATE | FS_O_APPEND; + case FileMode::READ_WRITE: + return FS_O_READ | FS_O_WRITE; + case FileMode::READ_WRITE_CREATE: + return FS_O_READ | FS_O_WRITE | FS_O_CREATE; + default: + return FS_O_READ; + } +} + +StorageErrorCode QSPIFile::mapZephyrError(int err) { + if (err >= 0) { + return StorageErrorCode::NONE; + } + + switch (-err) { + case ENOENT: + return StorageErrorCode::FILE_NOT_FOUND; + case EEXIST: + return StorageErrorCode::ALREADY_EXISTS; + case EACCES: + case EPERM: + return StorageErrorCode::PERMISSION_DENIED; + case ENOSPC: + return StorageErrorCode::STORAGE_FULL; + case EINVAL: + return StorageErrorCode::INVALID_PATH; + case EIO: + return StorageErrorCode::HARDWARE_ERROR; + case ENOMEM: + return StorageErrorCode::OUT_OF_MEMORY; + default: + return StorageErrorCode::UNKNOWN_ERROR; + } +} + +bool QSPIFile::open(const char* filename, FileMode mode, StorageError* error) { + // Update path + if (filename != nullptr) { + strncpy(path_, filename, sizeof(path_) - 1); + path_[sizeof(path_) - 1] = '\0'; + } + return open(mode, error); +} + +bool QSPIFile::open(const String& filename, FileMode mode, StorageError* error) { + return open(filename.c_str(), mode, error); +} + +bool QSPIFile::open(FileMode mode, StorageError* error) { + if (is_open_) { + close(nullptr); + } + + if (path_[0] == '\0') { + if (error) { + error->setError(StorageErrorCode::INVALID_PATH, "No file path specified"); + } + return false; + } + + if (!ensureFileHandle()) { + if (error) { + error->setError(StorageErrorCode::OUT_OF_MEMORY, "Failed to allocate file handle"); + } + return false; + } + + fs_file_t_init(file_); + + int flags = fileModeToFlags(mode); + int ret = fs_open(file_, path_, flags); + + if (ret < 0) { + if (error) { + error->setError(mapZephyrError(ret), "Failed to open file"); + } + return false; + } + + is_open_ = true; + mode_ = mode; + return true; +} + +bool QSPIFile::close(StorageError* error) { + if (!is_open_ || file_ == nullptr) { + return true; // Already closed + } + + int ret = fs_close(file_); + is_open_ = false; + + if (ret < 0) { + if (error) { + error->setError(mapZephyrError(ret), "Failed to close file"); + } + return false; + } + + return true; +} + +bool QSPIFile::changeMode(FileMode mode, StorageError* error) { + if (!is_open_) { + if (error) { + error->setError(StorageErrorCode::INVALID_OPERATION, "File not open"); + } + return false; + } + + // Close and reopen with new mode + close(nullptr); + return open(mode, error); +} + +bool QSPIFile::isOpen() const { + return is_open_; +} + +size_t QSPIFile::read(uint8_t* buffer, size_t size, StorageError* error) { + if (!is_open_ || file_ == nullptr) { + if (error) { + error->setError(StorageErrorCode::INVALID_OPERATION, "File not open"); + } + return 0; + } + + ssize_t ret = fs_read(file_, buffer, size); + + if (ret < 0) { + if (error) { + error->setError(mapZephyrError(ret), "Read failed"); + } + return 0; + } + + return static_cast(ret); +} + +int QSPIFile::read(StorageError* error) { + uint8_t byte; + size_t ret = read(&byte, 1, error); + if (ret == 1) { + return byte; + } + return -1; +} + +String QSPIFile::readAsString(StorageError* error) { + if (!is_open_) { + if (error) { + error->setError(StorageErrorCode::INVALID_OPERATION, "File not open"); + } + return String(); + } + + // Get file size + size_t fileSize = this->size(error); + if (fileSize == 0) { + return String(); + } + + // Seek to beginning + seek(0, error); + + // Read entire file + char* buffer = new char[fileSize + 1]; + if (buffer == nullptr) { + if (error) { + error->setError(StorageErrorCode::OUT_OF_MEMORY, "Failed to allocate buffer"); + } + return String(); + } + + size_t bytesRead = read(reinterpret_cast(buffer), fileSize, error); + buffer[bytesRead] = '\0'; + + String result(buffer); + delete[] buffer; + + return result; +} + +uint32_t QSPIFile::available(StorageError* error) { + if (!is_open_) { + return 0; + } + + size_t fileSize = this->size(error); + size_t currentPos = this->position(error); + + if (fileSize > currentPos) { + return fileSize - currentPos; + } + return 0; +} + +bool QSPIFile::seek(size_t offset, StorageError* error) { + if (!is_open_ || file_ == nullptr) { + if (error) { + error->setError(StorageErrorCode::INVALID_OPERATION, "File not open"); + } + return false; + } + + int ret = fs_seek(file_, offset, FS_SEEK_SET); + + if (ret < 0) { + if (error) { + error->setError(mapZephyrError(ret), "Seek failed"); + } + return false; + } + + return true; +} + +size_t QSPIFile::position(StorageError* error) { + if (!is_open_ || file_ == nullptr) { + if (error) { + error->setError(StorageErrorCode::INVALID_OPERATION, "File not open"); + } + return 0; + } + + off_t pos = fs_tell(file_); + + if (pos < 0) { + if (error) { + error->setError(mapZephyrError(pos), "Failed to get position"); + } + return 0; + } + + return static_cast(pos); +} + +size_t QSPIFile::size(StorageError* error) { + struct fs_dirent entry; + int ret = fs_stat(path_, &entry); + + if (ret < 0) { + if (error) { + error->setError(mapZephyrError(ret), "Failed to get file size"); + } + return 0; + } + + return entry.size; +} + +size_t QSPIFile::write(const uint8_t* buffer, size_t size, StorageError* error) { + if (!is_open_ || file_ == nullptr) { + if (error) { + error->setError(StorageErrorCode::INVALID_OPERATION, "File not open"); + } + return 0; + } + + ssize_t ret = fs_write(file_, buffer, size); + + if (ret < 0) { + if (error) { + error->setError(mapZephyrError(ret), "Write failed"); + } + return 0; + } + + return static_cast(ret); +} + +size_t QSPIFile::write(const String& data, StorageError* error) { + return write(reinterpret_cast(data.c_str()), data.length(), error); +} + +size_t QSPIFile::write(uint8_t value, StorageError* error) { + return write(&value, 1, error); +} + +bool QSPIFile::flush(StorageError* error) { + if (!is_open_ || file_ == nullptr) { + if (error) { + error->setError(StorageErrorCode::INVALID_OPERATION, "File not open"); + } + return false; + } + + int ret = fs_sync(file_); + + if (ret < 0) { + if (error) { + error->setError(mapZephyrError(ret), "Flush failed"); + } + return false; + } + + return true; +} + +bool QSPIFile::exists(StorageError* error) const { + if (path_[0] == '\0') { + return false; + } + + struct fs_dirent entry; + int ret = fs_stat(path_, &entry); + + return (ret == 0 && entry.type == FS_DIR_ENTRY_FILE); +} + +bool QSPIFile::remove(StorageError* error) { + if (is_open_) { + close(nullptr); + } + + if (path_[0] == '\0') { + if (error) { + error->setError(StorageErrorCode::INVALID_PATH, "No file path specified"); + } + return false; + } + + int ret = fs_unlink(path_); + + if (ret < 0) { + if (error) { + error->setError(mapZephyrError(ret), "Failed to remove file"); + } + return false; + } + + return true; +} + +bool QSPIFile::rename(const char* newFilename, StorageError* error) { + if (is_open_) { + close(nullptr); + } + + if (path_[0] == '\0' || newFilename == nullptr) { + if (error) { + error->setError(StorageErrorCode::INVALID_PATH, "Invalid path"); + } + return false; + } + + int ret = fs_rename(path_, newFilename); + + if (ret < 0) { + if (error) { + error->setError(mapZephyrError(ret), "Failed to rename file"); + } + return false; + } + + // Update internal path + strncpy(path_, newFilename, sizeof(path_) - 1); + path_[sizeof(path_) - 1] = '\0'; + + return true; +} + +bool QSPIFile::rename(const String& newFilename, StorageError* error) { + return rename(newFilename.c_str(), error); +} + +QSPIFolder QSPIFile::getParentFolder(StorageError* error) const { + if (path_[0] == '\0') { + if (error) { + error->setError(StorageErrorCode::INVALID_PATH, "No file path"); + } + return QSPIFolder(); + } + + char parentPath[STORAGE_MAX_PATH_LENGTH]; + strncpy(parentPath, path_, sizeof(parentPath) - 1); + parentPath[sizeof(parentPath) - 1] = '\0'; + + // Find last separator + char* lastSep = strrchr(parentPath, '/'); + if (lastSep != nullptr && lastSep != parentPath) { + *lastSep = '\0'; + } else if (lastSep == parentPath) { + // Root folder + parentPath[1] = '\0'; + } + + return QSPIFolder(parentPath); +} diff --git a/libraries/QSPIStorage/QSPIFile.h b/libraries/QSPIStorage/QSPIFile.h new file mode 100644 index 000000000..785eb1e21 --- /dev/null +++ b/libraries/QSPIStorage/QSPIFile.h @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2024 Arduino SA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef QSPI_FILE_H +#define QSPI_FILE_H + +#include +#include + +// Forward declarations - avoid including zephyr headers in public header +// to prevent static initialization issues +struct fs_file_t; +class QSPIFolder; + +/** + * @brief QSPI File implementation + * + * Implements the File interface for QSPI storage using Zephyr's + * file system API. + */ +class QSPIFile : public File { +public: + QSPIFile(); + QSPIFile(const char* path); + QSPIFile(const String& path); + ~QSPIFile() override; + + // Opening and Closing + bool open(const char* filename, FileMode mode, StorageError* error = nullptr) override; + bool open(const String& filename, FileMode mode, StorageError* error = nullptr) override; + bool open(FileMode mode = FileMode::READ, StorageError* error = nullptr) override; + bool close(StorageError* error = nullptr) override; + bool changeMode(FileMode mode, StorageError* error = nullptr) override; + bool isOpen() const override; + + // Reading Operations + size_t read(uint8_t* buffer, size_t size, StorageError* error = nullptr) override; + int read(StorageError* error = nullptr) override; + String readAsString(StorageError* error = nullptr) override; + uint32_t available(StorageError* error = nullptr) override; + bool seek(size_t offset, StorageError* error = nullptr) override; + size_t position(StorageError* error = nullptr) override; + size_t size(StorageError* error = nullptr) override; + + // Writing Operations + size_t write(const uint8_t* buffer, size_t size, StorageError* error = nullptr) override; + size_t write(const String& data, StorageError* error = nullptr) override; + size_t write(uint8_t value, StorageError* error = nullptr) override; + bool flush(StorageError* error = nullptr) override; + + // File Management + bool exists(StorageError* error = nullptr) const override; + bool remove(StorageError* error = nullptr) override; + bool rename(const char* newFilename, StorageError* error = nullptr) override; + bool rename(const String& newFilename, StorageError* error = nullptr) override; + + // Path Information + QSPIFolder getParentFolder(StorageError* error = nullptr) const; + +private: + struct fs_file_t* file_; // Pointer to avoid including zephyr headers + bool is_open_; + FileMode mode_; + + // Internal helpers + bool resolvePath(const char* path, char* resolved, StorageError* error); + int fileModeToFlags(FileMode mode); + bool ensureFileHandle(); // Allocate file handle if needed + void freeFileHandle(); // Free file handle + + // Map Zephyr error codes to StorageErrorCode + static StorageErrorCode mapZephyrError(int err); +}; + +#endif // QSPI_FILE_H diff --git a/libraries/QSPIStorage/QSPIFolder.cpp b/libraries/QSPIStorage/QSPIFolder.cpp new file mode 100644 index 000000000..94abe120f --- /dev/null +++ b/libraries/QSPIStorage/QSPIFolder.cpp @@ -0,0 +1,471 @@ +/* + * Copyright (c) 2024 Arduino SA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "QSPIFolder.h" + +#include +#include +#include + +QSPIFolder::QSPIFolder() : Folder() { +} + +QSPIFolder::QSPIFolder(const char* path) : Folder(path) { +} + +QSPIFolder::QSPIFolder(const String& path) : Folder(path) { +} + +QSPIFolder::~QSPIFolder() { +} + +StorageErrorCode QSPIFolder::mapZephyrError(int err) { + if (err >= 0) { + return StorageErrorCode::NONE; + } + + switch (-err) { + case ENOENT: + return StorageErrorCode::FOLDER_NOT_FOUND; + case EEXIST: + return StorageErrorCode::ALREADY_EXISTS; + case EACCES: + case EPERM: + return StorageErrorCode::PERMISSION_DENIED; + case ENOSPC: + return StorageErrorCode::STORAGE_FULL; + case EINVAL: + return StorageErrorCode::INVALID_PATH; + case EIO: + return StorageErrorCode::HARDWARE_ERROR; + case ENOMEM: + return StorageErrorCode::OUT_OF_MEMORY; + case ENOTEMPTY: + return StorageErrorCode::INVALID_OPERATION; + default: + return StorageErrorCode::UNKNOWN_ERROR; + } +} + +bool QSPIFolder::exists(StorageError* error) const { + if (path_[0] == '\0') { + return false; + } + + struct fs_dirent entry; + int ret = fs_stat(path_, &entry); + + return (ret == 0 && entry.type == FS_DIR_ENTRY_DIR); +} + +bool QSPIFolder::create(StorageError* error) { + if (path_[0] == '\0') { + if (error) { + error->setError(StorageErrorCode::INVALID_PATH, "No folder path specified"); + } + return false; + } + + int ret = fs_mkdir(path_); + + if (ret < 0 && ret != -EEXIST) { + if (error) { + error->setError(mapZephyrError(ret), "Failed to create folder"); + } + return false; + } + + return true; +} + +bool QSPIFolder::remove(bool recursive, StorageError* error) { + if (path_[0] == '\0') { + if (error) { + error->setError(StorageErrorCode::INVALID_PATH, "No folder path specified"); + } + return false; + } + + if (recursive) { + return removeRecursive(path_, error); + } + + int ret = fs_unlink(path_); + + if (ret < 0) { + if (error) { + error->setError(mapZephyrError(ret), "Failed to remove folder"); + } + return false; + } + + return true; +} + +bool QSPIFolder::removeRecursive(const char* path, StorageError* error) { + struct fs_dir_t dir; + fs_dir_t_init(&dir); + + int ret = fs_opendir(&dir, path); + if (ret < 0) { + if (error) { + error->setError(mapZephyrError(ret), "Failed to open directory"); + } + return false; + } + + struct fs_dirent entry; + char childPath[STORAGE_MAX_PATH_LENGTH]; + + while (true) { + ret = fs_readdir(&dir, &entry); + if (ret < 0 || entry.name[0] == '\0') { + break; + } + + // Build full path + snprintf(childPath, sizeof(childPath), "%s/%s", path, entry.name); + + if (entry.type == FS_DIR_ENTRY_DIR) { + // Recursively remove subdirectory + if (!removeRecursive(childPath, error)) { + fs_closedir(&dir); + return false; + } + } else { + // Remove file + ret = fs_unlink(childPath); + if (ret < 0) { + fs_closedir(&dir); + if (error) { + error->setError(mapZephyrError(ret), "Failed to remove file"); + } + return false; + } + } + } + + fs_closedir(&dir); + + // Now remove the empty directory + ret = fs_unlink(path); + if (ret < 0) { + if (error) { + error->setError(mapZephyrError(ret), "Failed to remove directory"); + } + return false; + } + + return true; +} + +bool QSPIFolder::rename(const char* newName, StorageError* error) { + if (path_[0] == '\0' || newName == nullptr) { + if (error) { + error->setError(StorageErrorCode::INVALID_PATH, "Invalid path"); + } + return false; + } + + int ret = fs_rename(path_, newName); + + if (ret < 0) { + if (error) { + error->setError(mapZephyrError(ret), "Failed to rename folder"); + } + return false; + } + + // Update internal path + strncpy(path_, newName, sizeof(path_) - 1); + path_[sizeof(path_) - 1] = '\0'; + + return true; +} + +bool QSPIFolder::rename(const String& newName, StorageError* error) { + return rename(newName.c_str(), error); +} + +QSPIFile QSPIFolder::createFile(const char* filename, FileMode mode, StorageError* error) { + char fullPath[STORAGE_MAX_PATH_LENGTH]; + snprintf(fullPath, sizeof(fullPath), "%s/%s", path_, filename); + + QSPIFile file(fullPath); + if (!file.open(mode, error)) { + return QSPIFile(); + } + + return file; +} + +QSPIFile QSPIFolder::createFile(const String& filename, FileMode mode, StorageError* error) { + return createFile(filename.c_str(), mode, error); +} + +QSPIFile QSPIFolder::getFile(const char* filename, StorageError* error) { + char fullPath[STORAGE_MAX_PATH_LENGTH]; + snprintf(fullPath, sizeof(fullPath), "%s/%s", path_, filename); + + QSPIFile file(fullPath); + + if (!file.exists(error)) { + if (error) { + error->setError(StorageErrorCode::FILE_NOT_FOUND, "File not found"); + } + return QSPIFile(); + } + + return file; +} + +QSPIFile QSPIFolder::getFile(const String& filename, StorageError* error) { + return getFile(filename.c_str(), error); +} + +QSPIFolder QSPIFolder::createSubfolder(const char* name, bool overwrite, StorageError* error) { + char fullPath[STORAGE_MAX_PATH_LENGTH]; + snprintf(fullPath, sizeof(fullPath), "%s/%s", path_, name); + + QSPIFolder folder(fullPath); + + if (folder.exists(nullptr)) { + if (overwrite) { + if (!folder.remove(true, error)) { + return QSPIFolder(); + } + } else { + if (error) { + error->setError(StorageErrorCode::ALREADY_EXISTS, "Folder already exists"); + } + return folder; // Return existing folder + } + } + + if (!folder.create(error)) { + return QSPIFolder(); + } + + return folder; +} + +QSPIFolder QSPIFolder::createSubfolder(const String& name, bool overwrite, StorageError* error) { + return createSubfolder(name.c_str(), overwrite, error); +} + +QSPIFolder QSPIFolder::getSubfolder(const char* name, StorageError* error) { + char fullPath[STORAGE_MAX_PATH_LENGTH]; + snprintf(fullPath, sizeof(fullPath), "%s/%s", path_, name); + + QSPIFolder folder(fullPath); + + if (!folder.exists(error)) { + if (error) { + error->setError(StorageErrorCode::FOLDER_NOT_FOUND, "Folder not found"); + } + return QSPIFolder(); + } + + return folder; +} + +QSPIFolder QSPIFolder::getSubfolder(const String& name, StorageError* error) { + return getSubfolder(name.c_str(), error); +} + +std::vector QSPIFolder::getFiles(StorageError* error) { + std::vector files; + + if (path_[0] == '\0') { + if (error) { + error->setError(StorageErrorCode::INVALID_PATH, "No folder path"); + } + return files; + } + + struct fs_dir_t dir; + fs_dir_t_init(&dir); + + int ret = fs_opendir(&dir, path_); + if (ret < 0) { + if (error) { + error->setError(mapZephyrError(ret), "Failed to open directory"); + } + return files; + } + + struct fs_dirent entry; + char fullPath[STORAGE_MAX_PATH_LENGTH]; + + while (true) { + ret = fs_readdir(&dir, &entry); + if (ret < 0 || entry.name[0] == '\0') { + break; + } + + if (entry.type == FS_DIR_ENTRY_FILE) { + snprintf(fullPath, sizeof(fullPath), "%s/%s", path_, entry.name); + files.push_back(QSPIFile(fullPath)); + } + } + + fs_closedir(&dir); + return files; +} + +std::vector QSPIFolder::getFolders(StorageError* error) { + std::vector folders; + + if (path_[0] == '\0') { + if (error) { + error->setError(StorageErrorCode::INVALID_PATH, "No folder path"); + } + return folders; + } + + struct fs_dir_t dir; + fs_dir_t_init(&dir); + + int ret = fs_opendir(&dir, path_); + if (ret < 0) { + if (error) { + error->setError(mapZephyrError(ret), "Failed to open directory"); + } + return folders; + } + + struct fs_dirent entry; + char fullPath[STORAGE_MAX_PATH_LENGTH]; + + while (true) { + ret = fs_readdir(&dir, &entry); + if (ret < 0 || entry.name[0] == '\0') { + break; + } + + if (entry.type == FS_DIR_ENTRY_DIR) { + snprintf(fullPath, sizeof(fullPath), "%s/%s", path_, entry.name); + folders.push_back(QSPIFolder(fullPath)); + } + } + + fs_closedir(&dir); + return folders; +} + +size_t QSPIFolder::getFileCount(StorageError* error) { + size_t count = 0; + + if (path_[0] == '\0') { + return 0; + } + + struct fs_dir_t dir; + fs_dir_t_init(&dir); + + int ret = fs_opendir(&dir, path_); + if (ret < 0) { + if (error) { + error->setError(mapZephyrError(ret), "Failed to open directory"); + } + return 0; + } + + struct fs_dirent entry; + + while (true) { + ret = fs_readdir(&dir, &entry); + if (ret < 0 || entry.name[0] == '\0') { + break; + } + + if (entry.type == FS_DIR_ENTRY_FILE) { + count++; + } + } + + fs_closedir(&dir); + return count; +} + +size_t QSPIFolder::getFolderCount(StorageError* error) { + size_t count = 0; + + if (path_[0] == '\0') { + return 0; + } + + struct fs_dir_t dir; + fs_dir_t_init(&dir); + + int ret = fs_opendir(&dir, path_); + if (ret < 0) { + if (error) { + error->setError(mapZephyrError(ret), "Failed to open directory"); + } + return 0; + } + + struct fs_dirent entry; + + while (true) { + ret = fs_readdir(&dir, &entry); + if (ret < 0 || entry.name[0] == '\0') { + break; + } + + if (entry.type == FS_DIR_ENTRY_DIR) { + count++; + } + } + + fs_closedir(&dir); + return count; +} + +QSPIFolder QSPIFolder::getParentFolder(StorageError* error) const { + if (path_[0] == '\0') { + if (error) { + error->setError(StorageErrorCode::INVALID_PATH, "No folder path"); + } + return QSPIFolder(); + } + + char parentPath[STORAGE_MAX_PATH_LENGTH]; + strncpy(parentPath, path_, sizeof(parentPath) - 1); + parentPath[sizeof(parentPath) - 1] = '\0'; + + // Find last separator + char* lastSep = strrchr(parentPath, '/'); + if (lastSep != nullptr && lastSep != parentPath) { + *lastSep = '\0'; + } else if (lastSep == parentPath) { + // Root folder + parentPath[1] = '\0'; + } + + return QSPIFolder(parentPath); +} + +bool QSPIFolder::resolvePath(const char* path, char* resolved, StorageError* error) { + if (path == nullptr || resolved == nullptr) { + if (error) { + error->setError(StorageErrorCode::INVALID_PATH, "Invalid path pointer"); + } + return false; + } + + // If path is absolute, use it directly + if (path[0] == '/') { + strncpy(resolved, path, STORAGE_MAX_PATH_LENGTH - 1); + resolved[STORAGE_MAX_PATH_LENGTH - 1] = '\0'; + } else { + // Relative path - combine with current folder path + snprintf(resolved, STORAGE_MAX_PATH_LENGTH, "%s/%s", path_, path); + } + + return true; +} diff --git a/libraries/QSPIStorage/QSPIFolder.h b/libraries/QSPIStorage/QSPIFolder.h new file mode 100644 index 000000000..3cc525fe9 --- /dev/null +++ b/libraries/QSPIStorage/QSPIFolder.h @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2024 Arduino SA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef QSPI_FOLDER_H +#define QSPI_FOLDER_H + +#include +#include +#include + +// Note: zephyr/fs/fs.h is only included in the .cpp file +// to avoid static initialization issues + +#include "QSPIFile.h" + +/** + * @brief QSPI Folder implementation + * + * Implements the Folder interface for QSPI storage using Zephyr's + * file system API. + */ +class QSPIFolder : public Folder { +public: + QSPIFolder(); + QSPIFolder(const char* path); + QSPIFolder(const String& path); + ~QSPIFolder() override; + + // File Operations + QSPIFile createFile(const char* filename, FileMode mode = FileMode::WRITE, StorageError* error = nullptr); + QSPIFile createFile(const String& filename, FileMode mode = FileMode::WRITE, StorageError* error = nullptr); + QSPIFile getFile(const char* filename, StorageError* error = nullptr); + QSPIFile getFile(const String& filename, StorageError* error = nullptr); + + // Directory Management + bool exists(StorageError* error = nullptr) const override; + bool create(StorageError* error = nullptr) override; + bool remove(bool recursive = false, StorageError* error = nullptr) override; + bool rename(const char* newName, StorageError* error = nullptr) override; + bool rename(const String& newName, StorageError* error = nullptr) override; + + // Subfolder Operations + QSPIFolder createSubfolder(const char* name, bool overwrite = false, StorageError* error = nullptr); + QSPIFolder createSubfolder(const String& name, bool overwrite = false, StorageError* error = nullptr); + QSPIFolder getSubfolder(const char* name, StorageError* error = nullptr); + QSPIFolder getSubfolder(const String& name, StorageError* error = nullptr); + + // Content Enumeration + std::vector getFiles(StorageError* error = nullptr); + std::vector getFolders(StorageError* error = nullptr); + size_t getFileCount(StorageError* error = nullptr) override; + size_t getFolderCount(StorageError* error = nullptr) override; + + // Path Information + QSPIFolder getParentFolder(StorageError* error = nullptr) const; + +private: + // Internal helpers + bool resolvePath(const char* path, char* resolved, StorageError* error); + bool removeRecursive(const char* path, StorageError* error); + + // Map Zephyr error codes to StorageErrorCode + static StorageErrorCode mapZephyrError(int err); +}; + +#endif // QSPI_FOLDER_H diff --git a/libraries/QSPIStorage/QSPIStorage.cpp b/libraries/QSPIStorage/QSPIStorage.cpp new file mode 100644 index 000000000..9a2b47607 --- /dev/null +++ b/libraries/QSPIStorage/QSPIStorage.cpp @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2024 Arduino SA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "QSPIStorage.h" + +#include +#include + +bool QSPIStorage::begin(StorageError* error) { + if (mounted_) { + return true; + } + + // Check if the filesystem is already mounted (via devicetree FSTAB auto-mount) + struct fs_statvfs stat; + int ret = fs_statvfs("/storage", &stat); + + if (ret == 0) { + mounted_ = true; + return true; + } + + // Filesystem not mounted - provide helpful error message + if (error) { + if (ret == -ENOENT) { + error->setError(StorageErrorCode::STORAGE_NOT_MOUNTED, + "Filesystem not mounted. Ensure LittleFS FSTAB is configured in devicetree."); + } else { + error->setError(StorageErrorCode::UNKNOWN_ERROR, "Failed to access filesystem"); + } + } + return false; +} + +bool QSPIStorage::getStorageInfo(size_t& total, size_t& used, size_t& available, StorageError* error) { + if (!mounted_) { + if (error) { + error->setError(StorageErrorCode::STORAGE_NOT_MOUNTED, "Storage not mounted"); + } + return false; + } + + struct fs_statvfs stat; + int ret = fs_statvfs("/storage", &stat); + if (ret != 0) { + if (error) { + error->setError(StorageErrorCode::READ_ERROR, "Failed to get storage info"); + } + return false; + } + + total = stat.f_frsize * stat.f_blocks; + available = stat.f_frsize * stat.f_bfree; + used = total - available; + + return true; +} + +QSPIFolder QSPIStorage::getRootFolder(StorageError* error) { + if (!mounted_) { + if (error) { + error->setError(StorageErrorCode::STORAGE_NOT_MOUNTED, "Storage not mounted"); + } + return QSPIFolder(); + } + return QSPIFolder("/storage"); +} diff --git a/libraries/QSPIStorage/QSPIStorage.h b/libraries/QSPIStorage/QSPIStorage.h new file mode 100644 index 000000000..d8bc710c2 --- /dev/null +++ b/libraries/QSPIStorage/QSPIStorage.h @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2024 Arduino SA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef QSPI_STORAGE_H +#define QSPI_STORAGE_H + +#include +#include +#include "QSPIFolder.h" + +/** + * @brief QSPI Storage manager class + * + * Provides high-level access to QSPI flash storage with LittleFS filesystem. + * + * IMPORTANT: This library requires LittleFS to be auto-mounted via devicetree FSTAB. + * Your board's devicetree must include an FSTAB entry that mounts LittleFS at "/storage". + */ +class QSPIStorage { +public: + QSPIStorage() : mounted_(false) {} + ~QSPIStorage() {} + + /** + * Initialize and verify the QSPI file system is mounted + * @param error Optional error output parameter + * @return true if successful, false otherwise + */ + bool begin(StorageError* error = nullptr); + + /** + * Mark storage as not in use + * @param error Optional error output parameter + */ + void end(StorageError* error = nullptr) { mounted_ = false; } + + /** + * Check if storage is mounted and ready + * @return true if mounted + */ + bool isMounted() const { return mounted_; } + + /** + * Get the mount point path + * @return Mount point string "/storage" + */ + const char* getMountPoint() const { return "/storage"; } + + /** + * Get storage statistics + * @param total Output total bytes + * @param used Output used bytes + * @param available Output available bytes + * @param error Optional error output parameter + * @return true if successful + */ + bool getStorageInfo(size_t& total, size_t& used, size_t& available, StorageError* error = nullptr); + + /** + * Get the root folder of the storage + * @param error Optional error output parameter + * @return QSPIFolder representing the root directory + */ + QSPIFolder getRootFolder(StorageError* error = nullptr); + + /** + * Format is not supported with FSTAB mounting + */ + bool format(FilesystemType fsType = FilesystemType::LITTLEFS, StorageError* error = nullptr) { + if (error) { + error->setError(StorageErrorCode::INVALID_OPERATION, "Format not supported with FSTAB mounting."); + } + return false; + } + +private: + bool mounted_; +}; + +#endif // QSPI_STORAGE_H diff --git a/libraries/QSPIStorage/examples/AdvancedStorageUsage/AdvancedStorageUsage.ino b/libraries/QSPIStorage/examples/AdvancedStorageUsage/AdvancedStorageUsage.ino new file mode 100644 index 000000000..f5c661263 --- /dev/null +++ b/libraries/QSPIStorage/examples/AdvancedStorageUsage/AdvancedStorageUsage.ino @@ -0,0 +1,511 @@ +/* + QSPIStorage - Advanced Storage Usage Example + + This example demonstrates advanced storage operations including: + - Binary data storage with structs + - Data logging with rotation + - Recursive folder operations + - Configuration file management + - Error recovery patterns + - Storage statistics and health monitoring + + Note: QSPI flash must be configured in the board's device tree overlay. +*/ + +#include + +QSPIStorage storage; + +// Sensor data structure for binary storage +struct SensorReading { + uint32_t timestamp; + float temperature; + float humidity; + float pressure; + uint16_t lightLevel; + uint8_t batteryPercent; + uint8_t flags; +}; + +// Configuration structure +struct DeviceConfig { + char deviceName[32]; + uint32_t sampleInterval; + uint32_t logRotationSize; + bool debugEnabled; + uint8_t logLevel; + uint8_t reserved[26]; // Padding to 64 bytes +}; + +// Constants +const char* CONFIG_FILE = "/storage/config.bin"; +const char* DATA_DIR = "/storage/data"; +const char* LOG_DIR = "/storage/logs"; +const uint32_t MAX_LOG_SIZE = 4096; // Rotate logs at 4KB + +void setup() { + Serial.begin(115200); + while (!Serial) { + delay(10); + } + + Serial.println("QSPIStorage - Advanced Usage Example"); + Serial.println("=====================================\n"); + + // Initialize storage with error recovery + if (!initializeWithRecovery()) { + Serial.println("Fatal: Could not initialize storage"); + while (1) delay(1000); + } + + // Setup directory structure + setupDirectoryStructure(); + + // Demo advanced features + demoConfigManagement(); + demoBinaryDataStorage(); + demoDataLogging(); + demoFolderOperations(); + demoStorageStatistics(); + + Serial.println("\n=== All advanced demos completed ==="); +} + +void loop() { + // Simulate periodic data logging + static unsigned long lastLog = 0; + if (millis() - lastLog > 5000) { + lastLog = millis(); + logSensorReading(); + } +} + +// Initialize storage with automatic recovery +bool initializeWithRecovery() { + StorageError error; + + Serial.println("Initializing storage with recovery..."); + + // First attempt: normal mount + if (storage.begin(&error)) { + Serial.println("Storage mounted successfully"); + return true; + } + + Serial.print("Initial mount failed: "); + Serial.println(error.getMessage()); + + // Recovery attempt based on error type + switch (error.getCode()) { + case StorageErrorCode::STORAGE_NOT_FORMATTED: + case StorageErrorCode::STORAGE_CORRUPTED: + Serial.println("Attempting format recovery..."); + error.clear(); + + if (storage.format(FilesystemType::LITTLEFS, &error)) { + Serial.println("Format successful, retrying mount..."); + error.clear(); + return storage.begin(&error); + } + Serial.print("Format failed: "); + Serial.println(error.getMessage()); + break; + + case StorageErrorCode::HARDWARE_ERROR: + Serial.println("Hardware error - check QSPI connections"); + break; + + default: + Serial.println("Unknown error - cannot recover"); + break; + } + + return false; +} + +// Create the directory structure for the application +void setupDirectoryStructure() { + Serial.println("\nSetting up directory structure..."); + + StorageError error; + QSPIFolder root = storage.getRootFolder(&error); + + // Create data directory + QSPIFolder dataDir = root.createSubfolder("data", false, &error); + if (!error || error.getCode() == StorageErrorCode::ALREADY_EXISTS) { + Serial.println(" /data - OK"); + } + error.clear(); + + // Create logs directory + QSPIFolder logsDir = root.createSubfolder("logs", false, &error); + if (!error || error.getCode() == StorageErrorCode::ALREADY_EXISTS) { + Serial.println(" /logs - OK"); + } + error.clear(); + + // Create backup directory + QSPIFolder backupDir = root.createSubfolder("backup", false, &error); + if (!error || error.getCode() == StorageErrorCode::ALREADY_EXISTS) { + Serial.println(" /backup - OK"); + } + + Serial.println(); +} + +// Demo: Configuration file management +void demoConfigManagement() { + Serial.println("--- Demo: Configuration Management ---"); + + StorageError error; + DeviceConfig config; + + // Try to load existing config + if (loadConfig(config, &error)) { + Serial.println("Loaded existing configuration:"); + } else { + Serial.println("No config found, creating default..."); + + // Initialize default config + strncpy(config.deviceName, "QSPI_Device_001", sizeof(config.deviceName)); + config.sampleInterval = 1000; + config.logRotationSize = MAX_LOG_SIZE; + config.debugEnabled = true; + config.logLevel = 2; + + if (!saveConfig(config, &error)) { + Serial.print("Failed to save config: "); + Serial.println(error.getMessage()); + return; + } + Serial.println("Default configuration saved."); + } + + // Display config + Serial.print(" Device Name: "); + Serial.println(config.deviceName); + Serial.print(" Sample Interval: "); + Serial.print(config.sampleInterval); + Serial.println(" ms"); + Serial.print(" Log Rotation Size: "); + Serial.print(config.logRotationSize); + Serial.println(" bytes"); + Serial.print(" Debug Enabled: "); + Serial.println(config.debugEnabled ? "Yes" : "No"); + Serial.print(" Log Level: "); + Serial.println(config.logLevel); + + // Modify and save config + config.sampleInterval = 2000; + if (saveConfig(config, &error)) { + Serial.println("Configuration updated successfully"); + } + + Serial.println(); +} + +// Load configuration from binary file +bool loadConfig(DeviceConfig& config, StorageError* error) { + QSPIFile file(CONFIG_FILE); + + if (!file.exists(error)) { + return false; + } + + if (!file.open(FileMode::READ, error)) { + return false; + } + + size_t bytesRead = file.read(reinterpret_cast(&config), sizeof(config), error); + file.close(error); + + return (bytesRead == sizeof(config)); +} + +// Save configuration to binary file +bool saveConfig(const DeviceConfig& config, StorageError* error) { + QSPIFile file(CONFIG_FILE); + + if (!file.open(FileMode::WRITE, error)) { + return false; + } + + size_t bytesWritten = file.write(reinterpret_cast(&config), sizeof(config), error); + file.flush(error); + file.close(error); + + return (bytesWritten == sizeof(config)); +} + +// Demo: Binary data storage +void demoBinaryDataStorage() { + Serial.println("--- Demo: Binary Data Storage ---"); + + StorageError error; + + // Create sample sensor readings + const int NUM_READINGS = 10; + SensorReading readings[NUM_READINGS]; + + Serial.println("Generating sensor readings..."); + for (int i = 0; i < NUM_READINGS; i++) { + readings[i].timestamp = millis() + (i * 100); + readings[i].temperature = 20.0 + (random(100) / 10.0); + readings[i].humidity = 40.0 + (random(200) / 10.0); + readings[i].pressure = 1000.0 + (random(50) / 10.0); + readings[i].lightLevel = random(1024); + readings[i].batteryPercent = 80 + random(20); + readings[i].flags = 0x01; + } + + // Write binary data + QSPIFile dataFile("/storage/data/readings.bin"); + + if (dataFile.open(FileMode::WRITE, &error)) { + // Write header (number of readings) + uint32_t count = NUM_READINGS; + dataFile.write(reinterpret_cast(&count), sizeof(count), &error); + + // Write all readings + size_t dataSize = sizeof(SensorReading) * NUM_READINGS; + size_t written = dataFile.write(reinterpret_cast(readings), dataSize, &error); + dataFile.close(&error); + + Serial.print("Wrote "); + Serial.print(written + sizeof(count)); + Serial.println(" bytes of binary data"); + } + + // Read binary data back + if (dataFile.open(FileMode::READ, &error)) { + uint32_t readCount; + dataFile.read(reinterpret_cast(&readCount), sizeof(readCount), &error); + + Serial.print("Reading "); + Serial.print(readCount); + Serial.println(" sensor readings:"); + + SensorReading reading; + for (uint32_t i = 0; i < min(readCount, (uint32_t)3); i++) { // Show first 3 + dataFile.read(reinterpret_cast(&reading), sizeof(reading), &error); + Serial.print(" ["); + Serial.print(i); + Serial.print("] T="); + Serial.print(reading.temperature, 1); + Serial.print("C, H="); + Serial.print(reading.humidity, 1); + Serial.print("%, P="); + Serial.print(reading.pressure, 1); + Serial.println("hPa"); + } + if (readCount > 3) { + Serial.println(" ..."); + } + + dataFile.close(&error); + } + + Serial.println(); +} + +// Demo: Data logging with rotation +void demoDataLogging() { + Serial.println("--- Demo: Data Logging with Rotation ---"); + + StorageError error; + + // Create some log entries + for (int i = 0; i < 5; i++) { + String logEntry = "[" + String(millis()) + "] INFO: Sample log entry #" + String(i + 1); + appendToLog("app.log", logEntry, &error); + delay(50); + } + + // Show log content + QSPIFile logFile("/storage/logs/app.log"); + if (logFile.open(FileMode::READ, &error)) { + Serial.println("Current log content:"); + Serial.println("---"); + Serial.println(logFile.readAsString(&error)); + Serial.println("---"); + Serial.print("Log size: "); + Serial.print(logFile.size(&error)); + Serial.println(" bytes"); + logFile.close(&error); + } + + Serial.println(); +} + +// Append to log file with automatic rotation +void appendToLog(const char* logName, const String& message, StorageError* error) { + char logPath[64]; + snprintf(logPath, sizeof(logPath), "%s/%s", LOG_DIR, logName); + + QSPIFile logFile(logPath); + + // Check if rotation is needed + if (logFile.exists(error) && logFile.size(error) > MAX_LOG_SIZE) { + rotateLog(logName, error); + } + + // Append new entry + if (logFile.open(FileMode::APPEND, error)) { + logFile.write(message + "\n", error); + logFile.close(error); + } +} + +// Rotate log file +void rotateLog(const char* logName, StorageError* error) { + char currentPath[64]; + char backupPath[64]; + + snprintf(currentPath, sizeof(currentPath), "%s/%s", LOG_DIR, logName); + snprintf(backupPath, sizeof(backupPath), "%s/%s.old", LOG_DIR, logName); + + // Delete old backup if exists + QSPIFile oldBackup(backupPath); + if (oldBackup.exists(error)) { + oldBackup.remove(error); + } + + // Rename current to backup + QSPIFile current(currentPath); + current.rename(backupPath, error); + + Serial.println("Log rotated"); +} + +// Log a single sensor reading (called from loop) +void logSensorReading() { + StorageError error; + + // Generate fake reading + SensorReading reading; + reading.timestamp = millis(); + reading.temperature = 22.0 + (random(50) / 10.0); + reading.humidity = 45.0; + reading.pressure = 1013.25; + reading.lightLevel = random(1024); + reading.batteryPercent = 95; + reading.flags = 0; + + String logEntry = "[" + String(reading.timestamp) + "] SENSOR: T=" + + String(reading.temperature, 1) + "C, L=" + + String(reading.lightLevel); + appendToLog("sensor.log", logEntry, &error); +} + +// Demo: Folder operations +void demoFolderOperations() { + Serial.println("--- Demo: Folder Operations ---"); + + StorageError error; + QSPIFolder root = storage.getRootFolder(&error); + + // Create nested folder structure + Serial.println("Creating nested folders..."); + QSPIFolder dataFolder = root.getSubfolder("data", &error); + + QSPIFolder year2024 = dataFolder.createSubfolder("2024", false, &error); + QSPIFolder jan = year2024.createSubfolder("01", false, &error); + QSPIFolder feb = year2024.createSubfolder("02", false, &error); + + // Create files in nested folders + QSPIFile janData = jan.createFile("data.csv", FileMode::WRITE, &error); + if (janData.isOpen()) { + janData.write("timestamp,value\n1704067200,42\n", &error); + janData.close(&error); + } + + // Count items + Serial.print("Files in /data/2024: "); + Serial.println(year2024.getFileCount(&error)); + Serial.print("Folders in /data/2024: "); + Serial.println(year2024.getFolderCount(&error)); + + // List all folders + Serial.println("\nFolders in /data/2024:"); + std::vector subfolders = year2024.getFolders(&error); + for (auto& folder : subfolders) { + Serial.print(" [DIR] "); + Serial.println(folder.getFolderName()); + + // List files in each subfolder + std::vector files = folder.getFiles(&error); + for (auto& file : files) { + Serial.print(" - "); + Serial.println(file.getFilename()); + } + } + + // Get parent folder + QSPIFolder parent = jan.getParentFolder(&error); + Serial.print("\nParent of /data/2024/01: "); + Serial.println(parent.getPath()); + + Serial.println(); +} + +// Demo: Storage statistics +void demoStorageStatistics() { + Serial.println("--- Demo: Storage Statistics ---"); + + StorageError error; + size_t total, used, available; + + if (storage.getStorageInfo(total, used, available, &error)) { + Serial.println("Storage Statistics:"); + Serial.println("-------------------"); + Serial.print("Total Space: "); + printSize(total); + Serial.print("Used Space: "); + printSize(used); + Serial.print("Available Space: "); + printSize(available); + Serial.print("Usage: "); + Serial.print((used * 100) / total); + Serial.println("%"); + } + + // Count all files recursively + QSPIFolder root = storage.getRootFolder(&error); + uint32_t fileCount = 0; + uint32_t folderCount = 0; + countRecursive(root, fileCount, folderCount, &error); + + Serial.print("\nTotal Files: "); + Serial.println(fileCount); + Serial.print("Total Folders: "); + Serial.println(folderCount); + + Serial.println(); +} + +// Helper: Print size in human-readable format +void printSize(size_t bytes) { + if (bytes >= 1024 * 1024) { + Serial.print(bytes / (1024 * 1024)); + Serial.println(" MB"); + } else if (bytes >= 1024) { + Serial.print(bytes / 1024); + Serial.println(" KB"); + } else { + Serial.print(bytes); + Serial.println(" bytes"); + } +} + +// Helper: Count files and folders recursively +void countRecursive(QSPIFolder& folder, uint32_t& fileCount, uint32_t& folderCount, StorageError* error) { + std::vector files = folder.getFiles(error); + fileCount += files.size(); + + std::vector subfolders = folder.getFolders(error); + folderCount += subfolders.size(); + + for (auto& subfolder : subfolders) { + countRecursive(subfolder, fileCount, folderCount, error); + } +} diff --git a/libraries/QSPIStorage/examples/BasicFileOperations/BasicFileOperations.ino b/libraries/QSPIStorage/examples/BasicFileOperations/BasicFileOperations.ino new file mode 100644 index 000000000..33ceb4481 --- /dev/null +++ b/libraries/QSPIStorage/examples/BasicFileOperations/BasicFileOperations.ino @@ -0,0 +1,216 @@ +/* + QSPIStorage - Basic File Operations Example + + This example demonstrates basic file and folder operations using the + QSPIStorage library. + + IMPORTANT REQUIREMENTS: + ======================= + This library requires LittleFS to be auto-mounted via devicetree FSTAB. + Your board's devicetree must include: + + 1. A storage partition on the QSPI flash + 2. An FSTAB entry that mounts LittleFS at "/storage" + + If you see "Filesystem not mounted" error, the board's devicetree + needs to be configured for auto-mounting. + + For boards without FSTAB configuration, use the low-level QSPI library + directly (see QSPI/examples/QSPIFilesystem.ino). +*/ + +#include + +QSPIStorage storage; + +void setup() { + Serial.begin(115200); + while (!Serial) { + delay(10); + } + + Serial.println("QSPIStorage - Basic File Operations"); + Serial.println("====================================\n"); + + // Initialize storage + StorageError error; + + Serial.println("Initializing QSPI storage..."); + if (!storage.begin(&error)) { + Serial.print("Storage initialization failed: "); + Serial.println(error.getMessage()); + Serial.println("\nNote: This library requires LittleFS auto-mount via devicetree FSTAB."); + Serial.println("Check your board's devicetree configuration."); + while (1) delay(1000); + } + + Serial.println("Storage mounted successfully!\n"); + + // Show storage info + showStorageInfo(); + + // Run file operation demos + demoWriteFile(); + demoReadFile(); + demoListFiles(); + demoDeleteFile(); + + Serial.println("\n=== All demos completed ==="); +} + +void loop() { + delay(1000); +} + +void showStorageInfo() { + StorageError error; + size_t total, used, available; + + if (storage.getStorageInfo(total, used, available, &error)) { + Serial.println("Storage Information:"); + Serial.print(" Total: "); + Serial.print(total / 1024); + Serial.println(" KB"); + Serial.print(" Used: "); + Serial.print(used / 1024); + Serial.println(" KB"); + Serial.print(" Available: "); + Serial.print(available / 1024); + Serial.println(" KB"); + Serial.println(); + } +} + +void demoWriteFile() { + Serial.println("--- Demo: Writing a File ---"); + + StorageError error; + QSPIFile file("/storage/hello.txt"); + + if (file.open(FileMode::WRITE, &error)) { + String content = "Hello from QSPIStorage!\n"; + content += "Timestamp: "; + content += String(millis()); + content += " ms"; + + size_t written = file.write(content, &error); + file.close(&error); + + if (!error) { + Serial.print("Wrote "); + Serial.print(written); + Serial.println(" bytes to hello.txt"); + } else { + Serial.print("Write error: "); + Serial.println(error.getMessage()); + } + } else { + Serial.print("Failed to open file: "); + Serial.println(error.getMessage()); + } + + Serial.println(); +} + +void demoReadFile() { + Serial.println("--- Demo: Reading a File ---"); + + StorageError error; + QSPIFile file("/storage/hello.txt"); + + if (file.open(FileMode::READ, &error)) { + String content = file.readAsString(&error); + file.close(&error); + + if (!error) { + Serial.println("Content of hello.txt:"); + Serial.println("---"); + Serial.println(content); + Serial.println("---"); + } else { + Serial.print("Read error: "); + Serial.println(error.getMessage()); + } + } else { + Serial.print("Failed to open file: "); + Serial.println(error.getMessage()); + } + + Serial.println(); +} + +void demoListFiles() { + Serial.println("--- Demo: Listing Files ---"); + + StorageError error; + QSPIFolder root = storage.getRootFolder(&error); + + if (error) { + Serial.print("Error getting root folder: "); + Serial.println(error.getMessage()); + return; + } + + // List files in root + Serial.println("Files in /storage:"); + std::vector files = root.getFiles(&error); + + if (files.empty()) { + Serial.println(" (no files)"); + } else { + for (auto& f : files) { + Serial.print(" "); + Serial.print(f.getFilename()); + Serial.print(" ("); + Serial.print(f.size(&error)); + Serial.println(" bytes)"); + } + } + + // List folders + Serial.println("\nFolders in /storage:"); + std::vector folders = root.getFolders(&error); + + if (folders.empty()) { + Serial.println(" (no folders)"); + } else { + for (auto& folder : folders) { + Serial.print(" [DIR] "); + Serial.println(folder.getFolderName()); + } + } + + Serial.println(); +} + +void demoDeleteFile() { + Serial.println("--- Demo: Deleting a File ---"); + + StorageError error; + + // Create a temp file + QSPIFile tempFile("/storage/temp.txt"); + if (tempFile.open(FileMode::WRITE, &error)) { + tempFile.write("Temporary file", &error); + tempFile.close(&error); + Serial.println("Created temp.txt"); + } + + // Check existence + Serial.print("temp.txt exists: "); + Serial.println(tempFile.exists(&error) ? "Yes" : "No"); + + // Delete it + if (tempFile.remove(&error)) { + Serial.println("Deleted temp.txt"); + } else { + Serial.print("Delete failed: "); + Serial.println(error.getMessage()); + } + + // Verify deletion + Serial.print("temp.txt exists: "); + Serial.println(tempFile.exists(&error) ? "Yes" : "No"); + + Serial.println(); +} diff --git a/libraries/QSPIStorage/examples/MinimalTest/MinimalTest.ino b/libraries/QSPIStorage/examples/MinimalTest/MinimalTest.ino new file mode 100644 index 000000000..4d7ce8bd5 --- /dev/null +++ b/libraries/QSPIStorage/examples/MinimalTest/MinimalTest.ino @@ -0,0 +1,45 @@ +/* + QSPIStorage Minimal Test +*/ + +#include + +QSPIStorage storage; + +void setup() { + Serial.begin(115200); + while (!Serial) delay(10); + + Serial.println("QSPIStorage Test"); + Serial.println("================"); + Serial.print("Mount point: "); + Serial.println(storage.getMountPoint()); + + Serial.println("Calling begin()..."); + StorageError error; + if (storage.begin(&error)) { + Serial.println("Storage mounted!"); + + size_t total, used, available; + if (storage.getStorageInfo(total, used, available)) { + Serial.print("Total: "); + Serial.print(total / 1024); + Serial.println(" KB"); + Serial.print("Used: "); + Serial.print(used / 1024); + Serial.println(" KB"); + Serial.print("Available: "); + Serial.print(available / 1024); + Serial.println(" KB"); + } + } else { + Serial.print("Mount failed: "); + Serial.println(error.getMessage()); + } + + Serial.println("Done!"); +} + +void loop() { + delay(1000); +} diff --git a/libraries/QSPIStorage/library.properties b/libraries/QSPIStorage/library.properties new file mode 100644 index 000000000..36e216a54 --- /dev/null +++ b/libraries/QSPIStorage/library.properties @@ -0,0 +1,10 @@ +name=QSPIStorage +version=1.0.0 +author=Arduino +maintainer=Arduino +sentence=High-level QSPI flash storage library with file and folder abstractions. +paragraph=Provides QSPIStorage, QSPIFile, and QSPIFolder classes for easy file operations on QSPI flash. Requires LittleFS auto-mount via devicetree FSTAB. +category=Data Storage +url=https://github.com/arduino/ArduinoCore-zephyr +architectures=* +depends=ArduinoStorage diff --git a/libraries/proposed/BaseStorageAPI.md b/libraries/proposed/BaseStorageAPI.md new file mode 100644 index 000000000..be0bb265f --- /dev/null +++ b/libraries/proposed/BaseStorageAPI.md @@ -0,0 +1,311 @@ +# Base Storage API Reference + +## Overview + +This document defines the base API for all Arduino storage libraries. It provides a unified interface inspired by Arduino_UnifiedStorage, with comprehensive error handling through an error object system. This API serves as a reference specification and should not be implemented directly - instead, storage implementations (QSPI, SD, Flash, etc.) should conform to these interfaces. + +## Core Design Principles + +1. **Unified Interface**: All storage types expose the same API surface +2. **Error Object Pattern**: Every method accepts an optional `StorageError*` parameter +3. **Path-based Operations**: Files and folders are referenced by paths +4. **Minimal Dependencies**: Standard Arduino types (String, uint8_t, etc.) +5. **Resource Safety**: Explicit open/close semantics with automatic cleanup + +--- + +## Error Handling + +### StorageError Class + +The `StorageError` class provides detailed error information across all storage operations. + +```cpp +enum class StorageErrorCode { + NONE = 0, + // File/Folder errors + FILE_NOT_FOUND, + FOLDER_NOT_FOUND, + ALREADY_EXISTS, + INVALID_PATH, + PERMISSION_DENIED, + + // I/O errors + READ_ERROR, + WRITE_ERROR, + SEEK_ERROR, + OPEN_ERROR, + CLOSE_ERROR, + + // Storage errors + STORAGE_FULL, + STORAGE_NOT_MOUNTED, + STORAGE_CORRUPTED, + STORAGE_NOT_FORMATTED, + + // Operation errors + INVALID_OPERATION, + INVALID_MODE, + BUFFER_OVERFLOW, + OUT_OF_MEMORY, + TIMEOUT, + + // Hardware errors + HARDWARE_ERROR, + NOT_INITIALIZED, + + // Generic + UNKNOWN_ERROR +}; + +class StorageError { +public: + StorageError(); + + // Error state + StorageErrorCode getCode() const; + const char* getMessage() const; + bool hasError() const; + + // Error setting (for implementations) + void setError(StorageErrorCode code, const char* message = nullptr); + void clear(); + + // Convenience operators + operator bool() const; // Returns true if error exists + +private: + StorageErrorCode code_; + char message_[128]; +}; +``` + +### Error Usage Pattern + +```cpp +// Example 1: Check error after operation +StorageError error; +file.open("data.txt", FileMode::READ, &error); +if (error) { + Serial.print("Error: "); + Serial.println(error.getMessage()); +} + +// Example 2: Ignore errors (backwards compatible) +file.open("data.txt", FileMode::READ); + +// Example 3: Error accumulation +StorageError error; +file.open("data.txt", FileMode::WRITE, &error); +file.write(buffer, size, &error); +file.close(&error); +if (error) { + Serial.println("Operation failed"); +} +``` + +--- + +## File Modes + +```cpp +enum class FileMode { + READ, // Open for reading, file must exist + WRITE, // Open for writing, creates if doesn't exist, truncates if exists + APPEND, // Open for writing at end, creates if doesn't exist + READ_WRITE, // Open for reading and writing, file must exist + READ_WRITE_CREATE // Open for reading and writing, creates if doesn't exist +}; +``` + +--- + +## File Class + +Represents a file in the storage system. Files can be read, written, and manipulated. + +### Constructors + +```cpp +File(); +File(const char* path); +File(const String& path); +``` + +### Opening and Closing + +```cpp +// Open file with specific mode +bool open(const char* filename, FileMode mode, StorageError* error = nullptr); +bool open(const String& filename, FileMode mode, StorageError* error = nullptr); +bool open(FileMode mode = FileMode::READ, StorageError* error = nullptr); // Uses constructor path + +// Close file and release resources +bool close(StorageError* error = nullptr); + +// Change mode without closing/reopening +bool changeMode(FileMode mode, StorageError* error = nullptr); + +// Check if file is currently open +bool isOpen() const; +``` + +### Reading Operations + +```cpp +// Read data into buffer, returns bytes actually read +size_t read(uint8_t* buffer, size_t size, StorageError* error = nullptr); + +// Read single byte, returns -1 on error or EOF +int read(StorageError* error = nullptr); + +// Read entire file as string +String readAsString(StorageError* error = nullptr); + +// Check bytes available for reading +uint32_t available(StorageError* error = nullptr); + +// Position file pointer +bool seek(size_t offset, StorageError* error = nullptr); + +// Get current position +size_t position(StorageError* error = nullptr); + +// Get file size +size_t size(StorageError* error = nullptr); +``` + +### Writing Operations + +```cpp +// Write buffer to file, returns bytes actually written +size_t write(const uint8_t* buffer, size_t size, StorageError* error = nullptr); + +// Write string to file +size_t write(const String& data, StorageError* error = nullptr); + +// Write single byte +size_t write(uint8_t value, StorageError* error = nullptr); + +// Flush write buffer to storage +bool flush(StorageError* error = nullptr); +``` + +### File Management + +```cpp +// Check if file exists +bool exists(StorageError* error = nullptr) const; + +// Delete file +bool remove(StorageError* error = nullptr); + +// Rename file +bool rename(const char* newFilename, StorageError* error = nullptr); +bool rename(const String& newFilename, StorageError* error = nullptr); +``` + +### Path Information + +```cpp +// Get file path as C-string +const char* getPath() const; + +// Get file path as Arduino String +String getPathAsString() const; + +// Get parent folder +Folder getParentFolder(StorageError* error = nullptr) const; + +// Get filename without path +String getFilename() const; +``` + +--- + +## Folder Class + +Represents a folder/directory in the storage system. + +### Constructors + +```cpp +Folder(); +Folder(const char* path); +Folder(const String& path); +``` + +### File Operationsf + +```cpp +// Create file in this folder +File createFile(const char* filename, FileMode mode = FileMode::WRITE, StorageError* error = nullptr); +File createFile(const String& filename, FileMode mode = FileMode::WRITE, StorageError* error = nullptr); + +// Get file from this folder (doesn't create) +File getFile(const char* filename, StorageError* error = nullptr); +File getFile(const String& filename, StorageError* error = nullptr); +``` + +### Directory Management + +```cpp +// Check if folder exists +bool exists(StorageError* error = nullptr) const; + +// Create this folder if it doesn't exist +bool create(StorageError* error = nullptr); + +// Delete folder and all contents +bool remove(bool recursive = false, StorageError* error = nullptr); + +// Rename folder +bool rename(const char* newName, StorageError* error = nullptr); +bool rename(const String& newName, StorageError* error = nullptr); +``` + +### Subfolder Operations + +```cpp +// Create subfolder +Folder createSubfolder(const char* name, bool overwrite = false, StorageError* error = nullptr); +Folder createSubfolder(const String& name, bool overwrite = false, StorageError* error = nullptr); + +// Get existing subfolder +Folder getSubfolder(const char* name, StorageError* error = nullptr); +Folder getSubfolder(const String& name, StorageError* error = nullptr); +``` + +### Content Enumeration + +```cpp +// Get all files in this folder (non-recursive) +std::vector getFiles(StorageError* error = nullptr); + +// Get all subfolders (non-recursive) +std::vector getFolders(StorageError* error = nullptr); + +// Get number of files +size_t getFileCount(StorageError* error = nullptr); + +// Get number of subfolders +size_t getFolderCount(StorageError* error = nullptr); +``` + +### Path Information + +```cpp +// Get folder path as C-string +const char* getPath() const; + +// Get folder path as Arduino String +String getPathAsString() const; + +// Get parent folder +Folder getParentFolder(StorageError* error = nullptr) const; + +// Get folder name without path +String getFolderName() const; +``` + +--- diff --git a/libraries/proposed/QSPIStorage.md b/libraries/proposed/QSPIStorage.md new file mode 100644 index 000000000..c28cde164 --- /dev/null +++ b/libraries/proposed/QSPIStorage.md @@ -0,0 +1,514 @@ +# QSPI Storage Implementation + +## Overview + +The QSPI Storage library provides a file system implementation for QSPI flash memory, conforming to the Base Storage API. It builds upon the low-level `QSPIClass` interface to provide file and folder abstractions with comprehensive error handling. + +This library implements the Base Storage API on top of a file system (LittleFS recommended) mounted on QSPI flash. + +--- + +## Architecture + +``` +┌─────────────────────────────────────┐ +│ QSPIFile / QSPIFolder Classes │ ← Base Storage API Implementation +├─────────────────────────────────────┤ +│ File System Layer (LittleFS) │ ← Zephyr FS Subsystem +├─────────────────────────────────────┤ +│ QSPIClass Interface │ ← Low-level flash operations +├─────────────────────────────────────┤ +│ Zephyr Flash Driver (QSPI) │ ← Hardware abstraction +└─────────────────────────────────────┘ +``` + +--- + +## Dependencies + +- **Base Storage API**: Conforms to `BaseStorageAPI.md` specification +- **QSPIClass**: Low-level flash interface (`QSPI.h`) +- **Zephyr FS**: File system subsystem (LittleFS or FAT) +- **StorageError**: Shared error handling from Base API + +--- + +## QSPIStorage Class + +Main storage manager that handles mounting, formatting, and partition management. + +### Initialization + +```cpp +class QSPIStorage { +public: + QSPIStorage(); + + // Initialize and mount file system + bool begin(StorageError* error = nullptr); + + // Unmount and deinitialize + void end(StorageError* error = nullptr); + + // Check if storage is mounted and ready + bool isMounted() const; + + // Get mount point path (e.g., "/qspi") + const char* getMountPoint() const; + + // Get storage statistics + bool getStorageInfo(size_t& total, size_t& used, size_t& available, StorageError* error = nullptr); + +private: + const char* mount_point_; + bool mounted_; + struct fs_mount_t mount_config_; +}; +``` + +### Usage Example + +```cpp +#include + +QSPIStorage storage; +StorageError error; + +void setup() { + Serial.begin(115200); + + // Initialize QSPI storage + if (!storage.begin(&error)) { + Serial.print("Storage init failed: "); + Serial.println(error.getMessage()); + return; + } + + Serial.println("QSPI Storage ready!"); + + // Get storage info + size_t total, used, available; + storage.getStorageInfo(total, used, available, &error); + Serial.print("Total: "); Serial.print(total / 1024); Serial.println(" KB"); + Serial.print("Used: "); Serial.print(used / 1024); Serial.println(" KB"); + Serial.print("Available: "); Serial.print(available / 1024); Serial.println(" KB"); +} +``` + +--- + +## QSPIFile Class + +Implements the Base Storage API `StorageFile` interface for QSPI storage. + +### Header Definition + +```cpp +class QSPIFile { +public: + // Constructors + QSPIFile(); + QSPIFile(const char* path); + QSPIFile(const String& path); + + // Opening and Closing + bool open(const char* filename, FileMode mode, StorageError* error = nullptr); + bool open(const String& filename, FileMode mode, StorageError* error = nullptr); + bool open(FileMode mode = FileMode::READ, StorageError* error = nullptr); + bool close(StorageError* error = nullptr); + bool changeMode(FileMode mode, StorageError* error = nullptr); + bool isOpen() const; + + // Reading Operations + size_t read(uint8_t* buffer, size_t size, StorageError* error = nullptr); + int read(StorageError* error = nullptr); + String readAsString(StorageError* error = nullptr); + uint32_t available(StorageError* error = nullptr); + bool seek(size_t offset, StorageError* error = nullptr); + size_t position(StorageError* error = nullptr); + size_t size(StorageError* error = nullptr); + + // Writing Operations + size_t write(const uint8_t* buffer, size_t size, StorageError* error = nullptr); + size_t write(const String& data, StorageError* error = nullptr); + size_t write(uint8_t value, StorageError* error = nullptr); + bool flush(StorageError* error = nullptr); + + // File Management + bool exists(StorageError* error = nullptr) const; + bool remove(StorageError* error = nullptr); + bool rename(const char* newFilename, StorageError* error = nullptr); + bool rename(const String& newFilename, StorageError* error = nullptr); + + // Path Information + const char* getPath() const; + String getPathAsString() const; + QSPIFolder getParentFolder(StorageError* error = nullptr) const; + String getFilename() const; + +private: + char path_[256]; + struct fs_file_t file_; + bool is_open_; + FileMode mode_; + + // Internal helpers + bool resolvePath(const char* path, char* resolved, StorageError* error); + int fileModeToFlags(FileMode mode); +}; +``` + +### Implementation Notes + +1. **Path Resolution**: All paths are resolved relative to mount point (e.g., "/qspi/data.txt") +2. **File Handles**: Uses Zephyr `fs_file_t` structure +3. **Buffering**: Write operations are buffered; use `flush()` to ensure data is written +4. **Error Mapping**: Zephyr error codes are mapped to `StorageErrorCode` + +### Usage Example + +```cpp +QSPIFile file("/qspi/config.txt"); +StorageError error; + +// Write configuration +if (file.open(FileMode::WRITE, &error)) { + String config = "wifi_ssid=MyNetwork\n"; + config += "wifi_pass=MyPassword\n"; + + file.write(config, &error); + file.flush(&error); + file.close(&error); +} + +if (error) { + Serial.print("Write error: "); + Serial.println(error.getMessage()); +} + +// Read configuration +if (file.open(FileMode::READ, &error)) { + String content = file.readAsString(&error); + Serial.println(content); + file.close(&error); +} +``` + +--- + +## QSPIFolder Class + +Implements the Base Storage API `StorageFolder` interface for QSPI storage. + +### Header Definition + +```cpp +class QSPIFolder { +public: + // Constructors + QSPIFolder(); + QSPIFolder(const char* path); + QSPIFolder(const String& path); + + // File Operations + QSPIFile createFile(const char* filename, FileMode mode = FileMode::WRITE, StorageError* error = nullptr); + QSPIFile createFile(const String& filename, FileMode mode = FileMode::WRITE, StorageError* error = nullptr); + QSPIFile getFile(const char* filename, StorageError* error = nullptr); + QSPIFile getFile(const String& filename, StorageError* error = nullptr); + + // Directory Management + bool exists(StorageError* error = nullptr) const; + bool create(StorageError* error = nullptr); + bool remove(bool recursive = false, StorageError* error = nullptr); + bool rename(const char* newName, StorageError* error = nullptr); + bool rename(const String& newName, StorageError* error = nullptr); + + // Subfolder Operations + QSPIFolder createSubfolder(const char* name, bool overwrite = false, StorageError* error = nullptr); + QSPIFolder createSubfolder(const String& name, bool overwrite = false, StorageError* error = nullptr); + QSPIFolder getSubfolder(const char* name, StorageError* error = nullptr); + QSPIFolder getSubfolder(const String& name, StorageError* error = nullptr); + + // Content Enumeration + std::vector getFiles(StorageError* error = nullptr); + std::vector getFolders(StorageError* error = nullptr); + size_t getFileCount(StorageError* error = nullptr); + size_t getFolderCount(StorageError* error = nullptr); + + // Path Information + const char* getPath() const; + String getPathAsString() const; + QSPIFolder getParentFolder(StorageError* error = nullptr) const; + String getFolderName() const; + +private: + char path_[256]; + + // Internal helpers + bool resolvePath(const char* path, char* resolved, StorageError* error); + bool removeRecursive(const char* path, StorageError* error); +}; +``` + +### Implementation Notes + +1. **Directory Operations**: Uses Zephyr `fs_opendir()` / `fs_readdir()` / `fs_closedir()` +2. **Recursive Operations**: `remove(true)` handles nested structures +3. **Path Building**: Automatically handles path separators and mount points +4. **Enumeration**: Returns vectors of file/folder objects for easy iteration + +### Usage Example + +```cpp +QSPIFolder dataFolder("/qspi/data"); +StorageError error; + +// Create folder structure +if (!dataFolder.exists(&error)) { + dataFolder.create(&error); +} + +// Create subfolders +QSPIFolder logsFolder = dataFolder.createSubfolder("logs", false, &error); +QSPIFolder configFolder = dataFolder.createSubfolder("config", false, &error); + +// Create files in subfolder +QSPIFile logFile = logsFolder.createFile("app.log", FileMode::WRITE, &error); +if (logFile.isOpen()) { + logFile.write("Application started\n", &error); + logFile.close(&error); +} + +// List all files +std::vector files = dataFolder.getFiles(&error); +Serial.print("Found "); +Serial.print(files.size()); +Serial.println(" files:"); + +for (auto& file : files) { + Serial.print(" - "); + Serial.println(file.getFilename()); +} +``` + +--- + +## Error Code Mapping + +Zephyr file system errors are mapped to `StorageErrorCode`: + +| Zephyr Error | StorageErrorCode | +|--------------|------------------| +| `-ENOENT` | `FILE_NOT_FOUND` / `FOLDER_NOT_FOUND` | +| `-EEXIST` | `ALREADY_EXISTS` | +| `-EINVAL` | `INVALID_PATH` | +| `-EACCES` | `PERMISSION_DENIED` | +| `-EIO` | `READ_ERROR` / `WRITE_ERROR` | +| `-ENOSPC` | `STORAGE_FULL` | +| `-EROFS` | `PERMISSION_DENIED` | +| `-ENODEV` | `STORAGE_NOT_MOUNTED` | +| Other | `UNKNOWN_ERROR` | + +--- + +## Performance Considerations + +### Write Optimization + +1. **Buffering**: Enable write buffering in LittleFS configuration +2. **Block Alignment**: Align writes to flash page size when possible +3. **Batch Operations**: Group multiple writes before calling `flush()` + +### Read Optimization + +1. **Read-Ahead**: Configure LittleFS cache size appropriately +2. **Sequential Access**: Sequential reads are faster than random access +3. **File Size**: Check file size before reading to allocate buffers efficiently + +### Memory Usage + +- **Stack**: Path buffers use 256 bytes per object +- **Heap**: File system cache configurable (default 512 bytes per cache) +- **Static**: Mount structures and device handles + +--- + +## Configuration + +### Zephyr Configuration (prj.conf) + +```ini +# File system support +CONFIG_FILE_SYSTEM=y +CONFIG_FILE_SYSTEM_LITTLEFS=y + +# QSPI Flash +CONFIG_FLASH=y +CONFIG_FLASH_MAP=y + +# LittleFS settings +CONFIG_FS_LITTLEFS_CACHE_SIZE=512 +CONFIG_FS_LITTLEFS_LOOKAHEAD_SIZE=32 +CONFIG_FS_LITTLEFS_BLOCK_CYCLES=512 +``` + +### Device Tree Partition (board.overlay) + +```dts +&mx25r64 { + partitions { + compatible = "fixed-partitions"; + #address-cells = <1>; + #size-cells = <1>; + + /* Storage partition: 7.5MB */ + storage_partition: partition@80000 { + label = "storage"; + reg = <0x00080000 0x00780000>; + }; + }; +}; +``` + +--- + +## Complete Example + +```cpp +#include +#include + +QSPIStorage storage; + +void setup() { + Serial.begin(115200); + while (!Serial) delay(10); + + // Initialize storage + StorageError error; + if (!storage.begin(&error)) { + Serial.print("Failed to initialize storage: "); + Serial.println(error.getMessage()); + return; + } + + // Create folder structure + QSPIFolder root("/qspi"); + QSPIFolder dataFolder = root.createSubfolder("data", false, &error); + + // Create and write to file + QSPIFile dataFile = dataFolder.createFile("sensor.csv", FileMode::WRITE, &error); + if (dataFile.isOpen()) { + dataFile.write("timestamp,temperature,humidity\n", &error); + dataFile.write("1234567890,23.5,45.2\n", &error); + dataFile.flush(&error); + dataFile.close(&error); + } + + // Read back file + dataFile.open(FileMode::READ, &error); + if (dataFile.isOpen()) { + String content = dataFile.readAsString(&error); + Serial.println("File content:"); + Serial.println(content); + dataFile.close(&error); + } + + // List all files in folder + std::vector files = dataFolder.getFiles(&error); + Serial.print("Files in /qspi/data: "); + Serial.println(files.size()); + + for (auto& file : files) { + Serial.print(" - "); + Serial.print(file.getFilename()); + Serial.print(" ("); + Serial.print(file.size(&error)); + Serial.println(" bytes)"); + } + + if (error) { + Serial.print("Error occurred: "); + Serial.println(error.getMessage()); + } +} + +void loop() { + // Nothing to do +} +``` + +--- + +## Testing Guidelines + +### Unit Tests + +1. **Initialization**: Test mount/unmount cycles +2. **File Operations**: Test create, read, write, delete, rename +3. **Folder Operations**: Test create, enumerate, remove (recursive) +4. **Error Handling**: Test error propagation and recovery +5. **Edge Cases**: Test full storage, long paths, special characters + +### Integration Tests + +1. **Power Loss**: Verify file system integrity after simulated power loss +2. **Stress Test**: Continuous read/write cycles +3. **Fragmentation**: Test performance with many small files +4. **Wear Leveling**: Monitor flash wear distribution + +--- + +## Limitations + +1. **Path Length**: Maximum path length is 255 characters +2. **Filename**: Maximum filename length depends on FS (typically 255 chars for LittleFS) +3. **Open Files**: Limited by `CONFIG_FS_LITTLEFS_CACHE_SIZE` and available memory +4. **Concurrent Access**: No file locking; avoid concurrent writes to same file +5. **Flash Wear**: QSPI flash has limited write/erase cycles (~100K typical) + +--- + +## Migration from Raw QSPI + +### Before (Raw QSPI) + +```cpp +#include + +QSPI.begin(); +uint8_t data[256]; +QSPI.read(0x1000, data, 256); +QSPI.write(0x2000, data, 256); +``` + +### After (QSPI Storage) + +```cpp +#include + +QSPIStorage storage; +storage.begin(); + +QSPIFile file("/qspi/data.bin"); +file.open(FileMode::READ_WRITE_CREATE); +file.write(data, 256); +file.seek(0); +file.read(buffer, 256); +file.close(); +``` + +**Benefits**: +- File system structure and organization +- Automatic wear leveling +- Power-loss recovery +- Standard file operations + +--- + +## Version + +**Library Version**: 1.0.0 +**Base API Version**: 1.0.0 +**Status**: Draft Proposal +**Last Updated**: 2025-12-04 diff --git a/libraries/proposed/StaticStorage.md b/libraries/proposed/StaticStorage.md new file mode 100644 index 000000000..6a7cde505 --- /dev/null +++ b/libraries/proposed/StaticStorage.md @@ -0,0 +1,843 @@ +# Static Storage Utilities Library + +## Overview + +The Static Storage library provides utility functions for managing storage devices across all storage implementations (QSPI, SD, Flash, etc.). It handles cross-storage operations like formatting, partitioning, and advanced copy/move operations that may span multiple storage backends. + +This library provides **static methods** that work with any storage implementation conforming to the Base Storage API, enabling operations that are not tied to a specific storage instance. + +--- + +## Design Principles + +1. **Storage-Agnostic**: Works with any Base Storage API implementation +2. **Static Interface**: All methods are static - no instantiation required +3. **Cross-Storage Operations**: Support operations between different storage types +4. **Comprehensive Error Handling**: All methods use `StorageError*` parameter +5. **Utility Functions**: High-level operations built on Base Storage API + +--- + +## Architecture + +``` +┌────────────────────────────────────────────┐ +│ StaticStorage Utility Layer │ ← Static helper methods +├────────────────────────────────────────────┤ +│ Base Storage API (File/Folder/Error) │ ← Common interface +├─────────────┬──────────────┬───────────────┤ +│ QSPI Storage│ SD Storage │ Flash Storage │ ← Specific implementations +└─────────────┴──────────────┴───────────────┘ +``` + +--- + +## Dependencies + +- **Base Storage API**: Uses `StorageFile`, `StorageFolder`, `StorageError` +- **Storage Implementations**: Works with any conforming implementation +- **Zephyr FS API**: For low-level formatting and partition operations + +--- + +## StaticStorage Class + +All methods are static. No instantiation required. + +```cpp +#include + +// Direct usage without creating an object +StaticStorage::format("/qspi", FilesystemType::LITTLEFS); +``` + +--- + +## File System Types + +```cpp +enum class FilesystemType { + LITTLEFS, // LittleFS - recommended for flash storage + FAT, // FAT32 - better compatibility, larger overhead + EXT2, // Extended 2 - Linux-style filesystem + AUTO // Auto-detect or use default +}; +``` + +--- + +## Formatting Operations + +### Format Storage + +Format a storage device with a specific file system. + +```cpp +class StaticStorage { +public: + /** + * Format a storage device + * @param mountPoint Mount point path (e.g., "/qspi", "/sd") + * @param fsType File system type to format with + * @param error Optional error output parameter + * @return true if successful, false otherwise + */ + static bool format( + const char* mountPoint, + FilesystemType fsType = FilesystemType::AUTO, + StorageError* error = nullptr + ); + + static bool format( + const String& mountPoint, + FilesystemType fsType = FilesystemType::AUTO, + StorageError* error = nullptr + ); + + /** + * Quick format (faster but less thorough) + * @param mountPoint Mount point path + * @param fsType File system type + * @param error Optional error output parameter + * @return true if successful + */ + static bool quickFormat( + const char* mountPoint, + FilesystemType fsType = FilesystemType::AUTO, + StorageError* error = nullptr + ); + + /** + * Check if a storage device needs formatting + * @param mountPoint Mount point path + * @param error Optional error output parameter + * @return true if formatting is needed + */ + static bool needsFormatting( + const char* mountPoint, + StorageError* error = nullptr + ); +}; +``` + +### Usage Example + +```cpp +#include + +void setup() { + Serial.begin(115200); + + StorageError error; + + // Check if QSPI needs formatting + if (StaticStorage::needsFormatting("/qspi", &error)) { + Serial.println("QSPI needs formatting..."); + + // Format with LittleFS + if (StaticStorage::format("/qspi", FilesystemType::LITTLEFS, &error)) { + Serial.println("Format successful!"); + } else { + Serial.print("Format failed: "); + Serial.println(error.getMessage()); + } + } +} +``` + +--- + +## Partitioning Operations + +### Partition Management + +Create and manage partitions on storage devices. + +```cpp +struct PartitionInfo { + const char* label; // Partition name/label + size_t offset; // Start offset in bytes + size_t size; // Size in bytes + FilesystemType fsType; // File system type for this partition +}; + +class StaticStorage { +public: + /** + * Create partitions on a storage device + * @param mountPoint Base mount point (e.g., "/qspi") + * @param partitions Array of partition definitions + * @param count Number of partitions + * @param error Optional error output parameter + * @return true if successful + */ + static bool createPartitions( + const char* mountPoint, + const PartitionInfo* partitions, + size_t count, + StorageError* error = nullptr + ); + + /** + * List existing partitions + * @param mountPoint Mount point to query + * @param partitions Output vector of partition info + * @param error Optional error output parameter + * @return true if successful + */ + static bool listPartitions( + const char* mountPoint, + std::vector& partitions, + StorageError* error = nullptr + ); + + /** + * Remove all partitions (restore to single partition) + * @param mountPoint Mount point + * @param error Optional error output parameter + * @return true if successful + */ + static bool removePartitions( + const char* mountPoint, + StorageError* error = nullptr + ); + + /** + * Get partition by label + * @param mountPoint Base mount point + * @param label Partition label + * @param info Output partition information + * @param error Optional error output parameter + * @return true if found + */ + static bool getPartition( + const char* mountPoint, + const char* label, + PartitionInfo& info, + StorageError* error = nullptr + ); +}; +``` + +### Usage Example + +```cpp +#include + +void setup() { + Serial.begin(115200); + StorageError error; + + // Define partitions for QSPI flash + // Assuming 8MB QSPI flash + PartitionInfo partitions[] = { + {"config", 0x000000, 512 * 1024, FilesystemType::LITTLEFS}, // 512KB for config + {"data", 0x080000, 2 * 1024 * 1024, FilesystemType::LITTLEFS}, // 2MB for data + {"logs", 0x280000, 1 * 1024 * 1024, FilesystemType::LITTLEFS}, // 1MB for logs + {"storage", 0x380000, 4 * 1024 * 1024, FilesystemType::FAT} // 4MB for storage + }; + + // Create partitions + if (StaticStorage::createPartitions("/qspi", partitions, 4, &error)) { + Serial.println("Partitions created successfully!"); + + // List partitions + std::vector found; + StaticStorage::listPartitions("/qspi", found, &error); + + Serial.print("Found "); + Serial.print(found.size()); + Serial.println(" partitions:"); + + for (auto& part : found) { + Serial.print(" - "); + Serial.print(part.label); + Serial.print(": "); + Serial.print(part.size / 1024); + Serial.println(" KB"); + } + } else { + Serial.print("Partition creation failed: "); + Serial.println(error.getMessage()); + } +} +``` + +--- + +## Cross-Storage Copy/Move Operations + +Advanced copy and move operations that work across different storage backends. + +```cpp +class StaticStorage { +public: + /** + * Copy file between different storage devices + * @param srcFile Source file object (any storage type) + * @param destPath Destination path (may be different storage) + * @param overwrite Overwrite if exists + * @param progress Optional progress callback (bytes_copied, total_bytes) + * @param error Optional error output parameter + * @return true if successful + */ + static bool copyFile( + const StorageFile& srcFile, + const char* destPath, + bool overwrite = false, + void (*progress)(size_t, size_t) = nullptr, + StorageError* error = nullptr + ); + + static bool copyFile( + const char* srcPath, + const char* destPath, + bool overwrite = false, + void (*progress)(size_t, size_t) = nullptr, + StorageError* error = nullptr + ); + + /** + * Move file between different storage devices + * @param srcFile Source file object + * @param destPath Destination path + * @param overwrite Overwrite if exists + * @param progress Optional progress callback + * @param error Optional error output parameter + * @return true if successful + */ + static bool moveFile( + const StorageFile& srcFile, + const char* destPath, + bool overwrite = false, + void (*progress)(size_t, size_t) = nullptr, + StorageError* error = nullptr + ); + + static bool moveFile( + const char* srcPath, + const char* destPath, + bool overwrite = false, + void (*progress)(size_t, size_t) = nullptr, + StorageError* error = nullptr + ); + + /** + * Copy folder between different storage devices (recursive) + * @param srcFolder Source folder object + * @param destPath Destination path + * @param overwrite Overwrite if exists + * @param progress Optional progress callback + * @param error Optional error output parameter + * @return true if successful + */ + static bool copyFolder( + const StorageFolder& srcFolder, + const char* destPath, + bool overwrite = false, + void (*progress)(size_t, size_t) = nullptr, + StorageError* error = nullptr + ); + + static bool copyFolder( + const char* srcPath, + const char* destPath, + bool overwrite = false, + void (*progress)(size_t, size_t) = nullptr, + StorageError* error = nullptr + ); + + /** + * Move folder between different storage devices (recursive) + * @param srcFolder Source folder object + * @param destPath Destination path + * @param overwrite Overwrite if exists + * @param progress Optional progress callback + * @param error Optional error output parameter + * @return true if successful + */ + static bool moveFolder( + const StorageFolder& srcFolder, + const char* destPath, + bool overwrite = false, + void (*progress)(size_t, size_t) = nullptr, + StorageError* error = nullptr + ); + + static bool moveFolder( + const char* srcPath, + const char* destPath, + bool overwrite = false, + void (*progress)(size_t, size_t) = nullptr, + StorageError* error = nullptr + ); + + /** + * Synchronize folders (copy only changed files) + * @param srcPath Source folder path + * @param destPath Destination folder path + * @param bidirectional If true, sync both ways + * @param progress Optional progress callback + * @param error Optional error output parameter + * @return true if successful + */ + static bool syncFolders( + const char* srcPath, + const char* destPath, + bool bidirectional = false, + void (*progress)(size_t, size_t) = nullptr, + StorageError* error = nullptr + ); +}; +``` + +### Usage Example + +```cpp +#include +#include +#include + +void progressCallback(size_t copied, size_t total) { + int percent = (copied * 100) / total; + Serial.print("\rProgress: "); + Serial.print(percent); + Serial.print("%"); +} + +void setup() { + Serial.begin(115200); + + // Initialize both storage devices + QSPIStorage qspi; + SDStorage sd; + + StorageError error; + + qspi.begin(&error); + sd.begin(&error); + + // Copy file from QSPI to SD with progress + Serial.println("Copying large file from QSPI to SD..."); + if (StaticStorage::copyFile( + "/qspi/data/large_file.bin", + "/sd/backup/large_file.bin", + true, // overwrite + progressCallback, + &error + )) { + Serial.println("\nCopy successful!"); + } else { + Serial.print("\nCopy failed: "); + Serial.println(error.getMessage()); + } + + // Backup entire QSPI config folder to SD + Serial.println("Backing up config folder..."); + if (StaticStorage::copyFolder( + "/qspi/config", + "/sd/backup/config", + true, + progressCallback, + &error + )) { + Serial.println("\nBackup successful!"); + } + + // Synchronize data folders between QSPI and SD + Serial.println("Syncing data folders..."); + if (StaticStorage::syncFolders( + "/qspi/data", + "/sd/data", + true, // bidirectional + progressCallback, + &error + )) { + Serial.println("\nSync successful!"); + } +} +``` + +--- + +## Storage Information and Analysis + +```cpp +class StaticStorage { +public: + /** + * Get detailed storage information + * @param mountPoint Mount point to query + * @param info Output storage information structure + * @param error Optional error output parameter + * @return true if successful + */ + static bool getStorageInfo( + const char* mountPoint, + StorageInfo& info, + StorageError* error = nullptr + ); + + /** + * Check storage health + * @param mountPoint Mount point to check + * @param health Output health information + * @param error Optional error output parameter + * @return true if successful + */ + static bool checkHealth( + const char* mountPoint, + StorageHealth& health, + StorageError* error = nullptr + ); + + /** + * Estimate available write cycles (for flash storage) + * @param mountPoint Mount point + * @param error Optional error output parameter + * @return Estimated remaining write cycles (0 = unknown) + */ + static uint32_t estimateRemainingCycles( + const char* mountPoint, + StorageError* error = nullptr + ); + + /** + * Optimize storage (defragment, garbage collect, etc.) + * @param mountPoint Mount point + * @param error Optional error output parameter + * @return true if successful + */ + static bool optimize( + const char* mountPoint, + StorageError* error = nullptr + ); +}; + +struct StorageInfo { + char mountPoint[64]; + FilesystemType fsType; + size_t totalBytes; + size_t usedBytes; + size_t availableBytes; + size_t blockSize; + size_t totalBlocks; + size_t usedBlocks; + bool readOnly; + bool mounted; +}; + +struct StorageHealth { + bool healthy; // Overall health status + uint32_t errorCount; // Number of errors encountered + uint32_t badBlocks; // Number of bad blocks (flash) + uint32_t writeCount; // Total write operations + uint32_t eraseCount; // Total erase operations + float fragmentationPercent; // File system fragmentation + char statusMessage[128]; // Human-readable status +}; +``` + +### Usage Example + +```cpp +#include + +void printStorageInfo(const char* mountPoint) { + StorageInfo info; + StorageHealth health; + StorageError error; + + // Get storage info + if (StaticStorage::getStorageInfo(mountPoint, info, &error)) { + Serial.println("=== Storage Information ==="); + Serial.print("Mount Point: "); Serial.println(info.mountPoint); + Serial.print("Total: "); Serial.print(info.totalBytes / 1024); Serial.println(" KB"); + Serial.print("Used: "); Serial.print(info.usedBytes / 1024); Serial.println(" KB"); + Serial.print("Available: "); Serial.print(info.availableBytes / 1024); Serial.println(" KB"); + Serial.print("Usage: "); + Serial.print((info.usedBytes * 100) / info.totalBytes); + Serial.println("%"); + } + + // Check health + if (StaticStorage::checkHealth(mountPoint, health, &error)) { + Serial.println("\n=== Storage Health ==="); + Serial.print("Status: "); + Serial.println(health.healthy ? "HEALTHY" : "WARNING"); + Serial.print("Errors: "); Serial.println(health.errorCount); + Serial.print("Bad Blocks: "); Serial.println(health.badBlocks); + Serial.print("Fragmentation: "); + Serial.print(health.fragmentationPercent); + Serial.println("%"); + Serial.print("Message: "); Serial.println(health.statusMessage); + + // Optimize if fragmented + if (health.fragmentationPercent > 50.0) { + Serial.println("High fragmentation detected. Optimizing..."); + if (StaticStorage::optimize(mountPoint, &error)) { + Serial.println("Optimization complete!"); + } + } + } + + // Estimate remaining cycles (for flash) + uint32_t cycles = StaticStorage::estimateRemainingCycles(mountPoint, &error); + if (cycles > 0) { + Serial.print("Estimated remaining write cycles: "); + Serial.println(cycles); + } +} + +void setup() { + Serial.begin(115200); + + QSPIStorage qspi; + qspi.begin(); + + printStorageInfo("/qspi"); +} +``` + +--- + +## Utility Functions + +```cpp +class StaticStorage { +public: + /** + * Compare two files (byte-by-byte) + * @param file1Path First file path + * @param file2Path Second file path + * @param error Optional error output parameter + * @return true if files are identical + */ + static bool compareFiles( + const char* file1Path, + const char* file2Path, + StorageError* error = nullptr + ); + + /** + * Calculate file checksum (CRC32) + * @param filePath File path + * @param checksum Output checksum value + * @param error Optional error output parameter + * @return true if successful + */ + static bool calculateChecksum( + const char* filePath, + uint32_t& checksum, + StorageError* error = nullptr + ); + + /** + * Verify file integrity using checksum + * @param filePath File path + * @param expectedChecksum Expected checksum value + * @param error Optional error output parameter + * @return true if checksum matches + */ + static bool verifyChecksum( + const char* filePath, + uint32_t expectedChecksum, + StorageError* error = nullptr + ); + + /** + * Create backup of file/folder + * @param srcPath Source path + * @param backupSuffix Backup suffix (e.g., ".bak", ".backup") + * @param error Optional error output parameter + * @return true if successful + */ + static bool createBackup( + const char* srcPath, + const char* backupSuffix = ".bak", + StorageError* error = nullptr + ); + + /** + * Restore from backup + * @param backupPath Backup file path + * @param restorePath Where to restore (nullptr = original location) + * @param error Optional error output parameter + * @return true if successful + */ + static bool restoreBackup( + const char* backupPath, + const char* restorePath = nullptr, + StorageError* error = nullptr + ); + + /** + * Wipe storage securely (overwrite with zeros/random) + * @param mountPoint Mount point to wipe + * @param passes Number of overwrite passes (1-3 recommended) + * @param progress Optional progress callback + * @param error Optional error output parameter + * @return true if successful + */ + static bool secureWipe( + const char* mountPoint, + uint8_t passes = 1, + void (*progress)(size_t, size_t) = nullptr, + StorageError* error = nullptr + ); + + /** + * Mount storage device + * @param mountPoint Mount point path + * @param devicePath Device path (implementation-specific) + * @param fsType File system type + * @param readOnly Mount as read-only + * @param error Optional error output parameter + * @return true if successful + */ + static bool mount( + const char* mountPoint, + const char* devicePath, + FilesystemType fsType = FilesystemType::AUTO, + bool readOnly = false, + StorageError* error = nullptr + ); + + /** + * Unmount storage device + * @param mountPoint Mount point to unmount + * @param error Optional error output parameter + * @return true if successful + */ + static bool unmount( + const char* mountPoint, + StorageError* error = nullptr + ); +}; +``` + +### Usage Example + +```cpp +#include + +void setup() { + Serial.begin(115200); + StorageError error; + + // Calculate and verify file checksum + uint32_t checksum; + if (StaticStorage::calculateChecksum("/qspi/config.txt", checksum, &error)) { + Serial.print("File checksum: 0x"); + Serial.println(checksum, HEX); + + // Later, verify integrity + if (StaticStorage::verifyChecksum("/qspi/config.txt", checksum, &error)) { + Serial.println("File integrity verified!"); + } else { + Serial.println("File may be corrupted!"); + } + } + + // Create backup before modifying + if (StaticStorage::createBackup("/qspi/important.dat", ".bak", &error)) { + Serial.println("Backup created successfully"); + + // Modify file... + // If something goes wrong, restore: + StaticStorage::restoreBackup("/qspi/important.dat.bak", nullptr, &error); + } + + // Compare files + if (StaticStorage::compareFiles( + "/qspi/file1.txt", + "/sd/file1.txt", + &error + )) { + Serial.println("Files are identical"); + } else { + Serial.println("Files differ"); + } +} +``` + +--- + +## Complete Example: Storage Manager + +```cpp +#include +#include +#include + +QSPIStorage qspi; + +void setup() { + Serial.begin(115200); + while (!Serial) delay(10); + + StorageError error; + + // Initialize QSPI + if (!qspi.begin(&error)) { + Serial.println("QSPI init failed"); + return; + } + + // Check if formatting needed + if (StaticStorage::needsFormatting("/qspi", &error)) { + Serial.println("Formatting QSPI with LittleFS..."); + StaticStorage::format("/qspi", FilesystemType::LITTLEFS, &error); + } + + // Create partitions + PartitionInfo partitions[] = { + {"system", 0x000000, 1024 * 1024, FilesystemType::LITTLEFS}, + {"data", 0x100000, 6 * 1024 * 1024, FilesystemType::LITTLEFS} + }; + + StaticStorage::createPartitions("/qspi", partitions, 2, &error); + + // Get storage info + StorageInfo info; + if (StaticStorage::getStorageInfo("/qspi", info, &error)) { + Serial.print("Storage: "); + Serial.print(info.usedBytes / 1024); + Serial.print(" / "); + Serial.print(info.totalBytes / 1024); + Serial.println(" KB used"); + } + + // Check health + StorageHealth health; + if (StaticStorage::checkHealth("/qspi", health, &error)) { + Serial.print("Health: "); + Serial.println(health.statusMessage); + + if (health.fragmentationPercent > 30.0) { + Serial.println("Optimizing storage..."); + StaticStorage::optimize("/qspi", &error); + } + } + + Serial.println("Storage manager ready!"); +} + +void loop() { + // Monitor storage health periodically + static unsigned long lastCheck = 0; + if (millis() - lastCheck > 60000) { // Check every minute + lastCheck = millis(); + + StorageHealth health; + StorageError error; + + if (StaticStorage::checkHealth("/qspi", health, &error)) { + if (!health.healthy) { + Serial.print("WARNING: Storage health issue: "); + Serial.println(health.statusMessage); + } + } + } +} +``` \ No newline at end of file diff --git a/variants/arduino_portenta_h7_stm32h747xx_m7/arduino_portenta_h7_stm32h747xx_m7.conf b/variants/arduino_portenta_h7_stm32h747xx_m7/arduino_portenta_h7_stm32h747xx_m7.conf index a5cc5a0a3..27998ea1f 100644 --- a/variants/arduino_portenta_h7_stm32h747xx_m7/arduino_portenta_h7_stm32h747xx_m7.conf +++ b/variants/arduino_portenta_h7_stm32h747xx_m7/arduino_portenta_h7_stm32h747xx_m7.conf @@ -110,3 +110,33 @@ CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=2048 CONFIG_BT_RX_STACK_SIZE=4096 CONFIG_STM32H7_BOOT_M4_AT_INIT=n + +# QSPI Flash Support +CONFIG_FLASH_STM32_QSPI=y +CONFIG_FLASH_MAP=y +CONFIG_FLASH_PAGE_LAYOUT=y + +# Filesystem Support +CONFIG_FILE_SYSTEM=y +CONFIG_FILE_SYSTEM_MKFS=y +CONFIG_FILE_SYSTEM_MAX_FILE_NAME=128 +CONFIG_DISK_ACCESS=y +CONFIG_DISK_DRIVER_FLASH=y + +# LittleFS Configuration +CONFIG_FILE_SYSTEM_LITTLEFS=y +CONFIG_FS_LITTLEFS_PROG_SIZE=4096 +CONFIG_FS_LITTLEFS_CACHE_SIZE=4096 + +# FAT Filesystem Configuration +CONFIG_FAT_FILESYSTEM_ELM=y +CONFIG_FS_FATFS_EXFAT=n +CONFIG_FS_FATFS_MKFS=y +CONFIG_FS_FATFS_LFN=y +CONFIG_FS_FATFS_LFN_MODE_HEAP=y +CONFIG_FS_FATFS_CODEPAGE=437 +CONFIG_FS_FATFS_MIN_SS=4096 +CONFIG_FS_FATFS_MAX_SS=4096 +CONFIG_FS_FATFS_MAX_LFN=255 +CONFIG_FS_FATFS_FSTAB_AUTOMOUNT=y +CONFIG_FS_FATFS_CUSTOM_MOUNT_POINTS="wlan,ota" \ No newline at end of file diff --git a/variants/arduino_portenta_h7_stm32h747xx_m7/arduino_portenta_h7_stm32h747xx_m7.overlay b/variants/arduino_portenta_h7_stm32h747xx_m7/arduino_portenta_h7_stm32h747xx_m7.overlay index f3f377b3c..aeda0f2ae 100644 --- a/variants/arduino_portenta_h7_stm32h747xx_m7/arduino_portenta_h7_stm32h747xx_m7.overlay +++ b/variants/arduino_portenta_h7_stm32h747xx_m7/arduino_portenta_h7_stm32h747xx_m7.overlay @@ -14,6 +14,7 @@ status = "okay"; }; + &i2c3 { status = "okay"; @@ -389,3 +390,12 @@ qspi_flash: &mx25l12833f {}; <&adc1 13>; /* Hack for D20 */ }; }; + + +/* QSPI flash (MX25L12833F) is already configured in arduino_portenta_h7-common.dtsi + * with the correct pins: IO2=PF7, IO3=PD13 (different from Giga R1!) + */ +qspi_flash: &mx25l12833f {}; + +/* Include common flash filesystem configuration */ +#include "../common/arduino_flash_fs.dtsi"