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..1d86c4b48 --- /dev/null +++ b/libraries/ArduinoStorage/CMakeLists.txt @@ -0,0 +1,3 @@ +# SPDX-License-Identifier: Apache-2.0 +# Header-only library - just expose include directory +zephyr_include_directories(.) 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.h b/libraries/ArduinoStorage/StorageError.h new file mode 100644 index 000000000..779a7888e --- /dev/null +++ b/libraries/ArduinoStorage/StorageError.h @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2024 Arduino SA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ARDUINO_STORAGE_ERROR_H +#define ARDUINO_STORAGE_ERROR_H + +#include +#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() : code_(StorageErrorCode::NONE) { + message_[0] = '\0'; + } + + // Error state + StorageErrorCode getCode() const { + return code_; + } + + const char* 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 hasError() const { + return code_ != StorageErrorCode::NONE; + } + + // Error setting (for implementations) + void setError(StorageErrorCode code, const char* message = nullptr) { + code_ = code; + if (message != nullptr) { + strncpy(message_, message, sizeof(message_) - 1); + message_[sizeof(message_) - 1] = '\0'; + } else { + message_[0] = '\0'; + } + } + + void clear() { + code_ = StorageErrorCode::NONE; + message_[0] = '\0'; + } + + // Convenience operators + operator bool() const { + return hasError(); + } + +private: + StorageErrorCode code_; + char message_[128]; +}; + +#endif // ARDUINO_STORAGE_ERROR_H diff --git a/libraries/ArduinoStorage/StorageFile.h b/libraries/ArduinoStorage/StorageFile.h new file mode 100644 index 000000000..e245b278b --- /dev/null +++ b/libraries/ArduinoStorage/StorageFile.h @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2024 Arduino SA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ARDUINO_STORAGE_FILE_H +#define ARDUINO_STORAGE_FILE_H + +#include +#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() { + path_[0] = '\0'; + } + + File(const char* path) { + if (path != nullptr) { + strncpy(path_, path, sizeof(path_) - 1); + path_[sizeof(path_) - 1] = '\0'; + } else { + path_[0] = '\0'; + } + } + + File(const String& path) { + strncpy(path_, path.c_str(), sizeof(path_) - 1); + path_[sizeof(path_) - 1] = '\0'; + } + + 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 { + return path_; + } + + virtual String getPathAsString() const { + return String(path_); + } + + virtual String getFilename() const { + const char* lastSep = strrchr(path_, '/'); + if (lastSep != nullptr) { + return String(lastSep + 1); + } + return String(path_); + } + +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) { + if (error != nullptr) { + error->setError(code, message); + } + } +}; + +#endif // ARDUINO_STORAGE_FILE_H diff --git a/libraries/ArduinoStorage/StorageFolder.h b/libraries/ArduinoStorage/StorageFolder.h new file mode 100644 index 000000000..bff38eeb9 --- /dev/null +++ b/libraries/ArduinoStorage/StorageFolder.h @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2024 Arduino SA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ARDUINO_STORAGE_FOLDER_H +#define ARDUINO_STORAGE_FOLDER_H + +#include +#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() { + path_[0] = '\0'; + } + + Folder(const char* path) { + if (path != nullptr) { + strncpy(path_, path, sizeof(path_) - 1); + path_[sizeof(path_) - 1] = '\0'; + } else { + path_[0] = '\0'; + } + } + + Folder(const String& path) { + strncpy(path_, path.c_str(), sizeof(path_) - 1); + path_[sizeof(path_) - 1] = '\0'; + } + + 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 { + return path_; + } + + virtual String getPathAsString() const { + return String(path_); + } + + virtual String 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_); + } + +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) { + if (error != nullptr) { + error->setError(code, message); + } + } +}; + +#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..915dd96d2 --- /dev/null +++ b/libraries/QSPIStorage/QSPIFile.h @@ -0,0 +1,255 @@ +/* + * 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; + +/** + * @class QSPIFile + * @brief File operations for QSPI flash storage. + * + * QSPIFile provides read, write, and seek operations for files stored on + * QSPI flash memory. It implements the File interface from ArduinoStorage. + * + * @note Files are stored on the LittleFS partition mounted at /storage. + * + * @example SimpleReadWrite.ino + * @code + * QSPIFile file("/storage/data.txt"); + * file.open(FileMode::WRITE); + * file.write("Hello World!"); + * file.close(); + * + * file.open(FileMode::READ); + * String content = file.readAsString(); + * file.close(); + * @endcode + */ +class QSPIFile : public File { +public: + /** + * @brief Default constructor. + * + * Creates an empty file object. Use open() with a path to access a file. + */ + QSPIFile(); + + /** + * @brief Construct a file object with the specified path. + * @param path Absolute path to the file (e.g., "/storage/myfile.txt") + */ + QSPIFile(const char* path); + + /** + * @brief Construct a file object with the specified path. + * @param path Absolute path to the file as a String + */ + QSPIFile(const String& path); + + /** + * @brief Destructor. Closes the file if open. + */ + ~QSPIFile() override; + + // ==================== Opening and Closing ==================== + + /** + * @brief Open a file at the specified path. + * @param filename Path to the file to open + * @param mode File mode (READ, WRITE, APPEND, READ_WRITE) + * @param error Optional pointer to receive error details + * @return true if successful, false otherwise + */ + bool open(const char* filename, FileMode mode, StorageError* error = nullptr) override; + + /** + * @brief Open a file at the specified path. + * @param filename Path to the file as a String + * @param mode File mode (READ, WRITE, APPEND, READ_WRITE) + * @param error Optional pointer to receive error details + * @return true if successful, false otherwise + */ + bool open(const String& filename, FileMode mode, StorageError* error = nullptr) override; + + /** + * @brief Open the file at the path set in the constructor. + * @param mode File mode (READ, WRITE, APPEND, READ_WRITE) + * @param error Optional pointer to receive error details + * @return true if successful, false otherwise + */ + bool open(FileMode mode = FileMode::READ, StorageError* error = nullptr) override; + + /** + * @brief Close the file. + * @param error Optional pointer to receive error details + * @return true if successful, false otherwise + */ + bool close(StorageError* error = nullptr) override; + + /** + * @brief Change the file mode without closing. + * @param mode New file mode + * @param error Optional pointer to receive error details + * @return true if successful, false otherwise + */ + bool changeMode(FileMode mode, StorageError* error = nullptr) override; + + /** + * @brief Check if the file is currently open. + * @return true if open, false otherwise + */ + bool isOpen() const override; + + // ==================== Reading Operations ==================== + + /** + * @brief Read bytes from the file into a buffer. + * @param buffer Destination buffer for the data + * @param size Maximum number of bytes to read + * @param error Optional pointer to receive error details + * @return Number of bytes actually read + */ + size_t read(uint8_t* buffer, size_t size, StorageError* error = nullptr) override; + + /** + * @brief Read a single byte from the file. + * @param error Optional pointer to receive error details + * @return The byte read, or -1 if end of file or error + */ + int read(StorageError* error = nullptr) override; + + /** + * @brief Read the entire file contents as a String. + * @param error Optional pointer to receive error details + * @return File contents as a String + */ + String readAsString(StorageError* error = nullptr) override; + + /** + * @brief Get the number of bytes available to read. + * @param error Optional pointer to receive error details + * @return Number of bytes from current position to end of file + */ + uint32_t available(StorageError* error = nullptr) override; + + /** + * @brief Seek to a position in the file. + * @param offset Byte offset from the beginning of the file + * @param error Optional pointer to receive error details + * @return true if successful, false otherwise + */ + bool seek(size_t offset, StorageError* error = nullptr) override; + + /** + * @brief Get the current read/write position. + * @param error Optional pointer to receive error details + * @return Current byte offset from the beginning of the file + */ + size_t position(StorageError* error = nullptr) override; + + /** + * @brief Get the file size in bytes. + * @param error Optional pointer to receive error details + * @return File size in bytes + */ + size_t size(StorageError* error = nullptr) override; + + // ==================== Writing Operations ==================== + + /** + * @brief Write bytes to the file. + * @param buffer Source buffer containing data to write + * @param size Number of bytes to write + * @param error Optional pointer to receive error details + * @return Number of bytes actually written + */ + size_t write(const uint8_t* buffer, size_t size, StorageError* error = nullptr) override; + + /** + * @brief Write a String to the file. + * @param data String to write + * @param error Optional pointer to receive error details + * @return Number of bytes written + */ + size_t write(const String& data, StorageError* error = nullptr) override; + + /** + * @brief Write a single byte to the file. + * @param value Byte to write + * @param error Optional pointer to receive error details + * @return 1 if successful, 0 otherwise + */ + size_t write(uint8_t value, StorageError* error = nullptr) override; + + /** + * @brief Flush any buffered data to storage. + * @param error Optional pointer to receive error details + * @return true if successful, false otherwise + */ + bool flush(StorageError* error = nullptr) override; + + // ==================== File Management ==================== + + /** + * @brief Check if the file exists on storage. + * @param error Optional pointer to receive error details + * @return true if the file exists, false otherwise + */ + bool exists(StorageError* error = nullptr) const override; + + /** + * @brief Delete the file from storage. + * @param error Optional pointer to receive error details + * @return true if successful, false otherwise + */ + bool remove(StorageError* error = nullptr) override; + + /** + * @brief Rename or move the file. + * @param newFilename New path for the file + * @param error Optional pointer to receive error details + * @return true if successful, false otherwise + */ + bool rename(const char* newFilename, StorageError* error = nullptr) override; + + /** + * @brief Rename or move the file. + * @param newFilename New path for the file as a String + * @param error Optional pointer to receive error details + * @return true if successful, false otherwise + */ + bool rename(const String& newFilename, StorageError* error = nullptr) override; + + // ==================== Path Information ==================== + + /** + * @brief Get the parent folder of this file. + * @param error Optional pointer to receive error details + * @return QSPIFolder representing the parent directory + */ + QSPIFolder getParentFolder(StorageError* error = nullptr) const; + +private: + struct fs_file_t* file_; + bool is_open_; + FileMode mode_; + + bool resolvePath(const char* path, char* resolved, StorageError* error); + int fileModeToFlags(FileMode mode); + bool ensureFileHandle(); + void freeFileHandle(); + 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..1aeffe4ce --- /dev/null +++ b/libraries/QSPIStorage/QSPIFolder.h @@ -0,0 +1,224 @@ +/* + * 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" + +/** + * @class QSPIFolder + * @brief Folder/directory operations for QSPI flash storage. + * + * QSPIFolder provides directory management operations including creating, + * listing, and removing folders on QSPI flash memory. It implements the + * Folder interface from ArduinoStorage. + * + * @note Folders are created on the LittleFS partition mounted at /storage. + * + * @example BasicFolderOperations.ino + * @code + * QSPIFolder root = storage.getRootFolder(); + * QSPIFolder myFolder = root.createSubfolder("mydata"); + * + * QSPIFile file = myFolder.createFile("log.txt", FileMode::WRITE); + * file.write("Hello!"); + * file.close(); + * + * std::vector files = myFolder.getFiles(); + * @endcode + */ +class QSPIFolder : public Folder { +public: + /** + * @brief Default constructor. + * + * Creates an empty folder object. + */ + QSPIFolder(); + + /** + * @brief Construct a folder object with the specified path. + * @param path Absolute path to the folder (e.g., "/storage/myfolder") + */ + QSPIFolder(const char* path); + + /** + * @brief Construct a folder object with the specified path. + * @param path Absolute path to the folder as a String + */ + QSPIFolder(const String& path); + + /** + * @brief Destructor. + */ + ~QSPIFolder() override; + + // ==================== File Operations ==================== + + /** + * @brief Create a new file in this folder. + * @param filename Name of the file to create + * @param mode File mode for the new file (default: WRITE) + * @param error Optional pointer to receive error details + * @return QSPIFile object for the created file (already open) + */ + QSPIFile createFile(const char* filename, FileMode mode = FileMode::WRITE, StorageError* error = nullptr); + + /** + * @brief Create a new file in this folder. + * @param filename Name of the file as a String + * @param mode File mode for the new file (default: WRITE) + * @param error Optional pointer to receive error details + * @return QSPIFile object for the created file (already open) + */ + QSPIFile createFile(const String& filename, FileMode mode = FileMode::WRITE, StorageError* error = nullptr); + + /** + * @brief Get a file object for an existing file in this folder. + * @param filename Name of the file + * @param error Optional pointer to receive error details + * @return QSPIFile object (not opened, use open() to access) + */ + QSPIFile getFile(const char* filename, StorageError* error = nullptr); + + /** + * @brief Get a file object for an existing file in this folder. + * @param filename Name of the file as a String + * @param error Optional pointer to receive error details + * @return QSPIFile object (not opened, use open() to access) + */ + QSPIFile getFile(const String& filename, StorageError* error = nullptr); + + // ==================== Directory Management ==================== + + /** + * @brief Check if the folder exists on storage. + * @param error Optional pointer to receive error details + * @return true if the folder exists, false otherwise + */ + bool exists(StorageError* error = nullptr) const override; + + /** + * @brief Create this folder on storage. + * @param error Optional pointer to receive error details + * @return true if successful, false otherwise + */ + bool create(StorageError* error = nullptr) override; + + /** + * @brief Remove this folder from storage. + * @param recursive If true, delete all contents first; if false, folder must be empty + * @param error Optional pointer to receive error details + * @return true if successful, false otherwise + */ + bool remove(bool recursive = false, StorageError* error = nullptr) override; + + /** + * @brief Rename or move the folder. + * @param newName New name or path for the folder + * @param error Optional pointer to receive error details + * @return true if successful, false otherwise + */ + bool rename(const char* newName, StorageError* error = nullptr) override; + + /** + * @brief Rename or move the folder. + * @param newName New name or path as a String + * @param error Optional pointer to receive error details + * @return true if successful, false otherwise + */ + bool rename(const String& newName, StorageError* error = nullptr) override; + + // ==================== Subfolder Operations ==================== + + /** + * @brief Create a subfolder within this folder. + * @param name Name of the subfolder to create + * @param overwrite If true, remove existing folder first + * @param error Optional pointer to receive error details + * @return QSPIFolder object for the created subfolder + */ + QSPIFolder createSubfolder(const char* name, bool overwrite = false, StorageError* error = nullptr); + + /** + * @brief Create a subfolder within this folder. + * @param name Name of the subfolder as a String + * @param overwrite If true, remove existing folder first + * @param error Optional pointer to receive error details + * @return QSPIFolder object for the created subfolder + */ + QSPIFolder createSubfolder(const String& name, bool overwrite = false, StorageError* error = nullptr); + + /** + * @brief Get a subfolder object for an existing subfolder. + * @param name Name of the subfolder + * @param error Optional pointer to receive error details + * @return QSPIFolder object for the subfolder + */ + QSPIFolder getSubfolder(const char* name, StorageError* error = nullptr); + + /** + * @brief Get a subfolder object for an existing subfolder. + * @param name Name of the subfolder as a String + * @param error Optional pointer to receive error details + * @return QSPIFolder object for the subfolder + */ + QSPIFolder getSubfolder(const String& name, StorageError* error = nullptr); + + // ==================== Content Enumeration ==================== + + /** + * @brief Get a list of all files in this folder. + * @param error Optional pointer to receive error details + * @return Vector of QSPIFile objects + */ + std::vector getFiles(StorageError* error = nullptr); + + /** + * @brief Get a list of all subfolders in this folder. + * @param error Optional pointer to receive error details + * @return Vector of QSPIFolder objects + */ + std::vector getFolders(StorageError* error = nullptr); + + /** + * @brief Get the number of files in this folder. + * @param error Optional pointer to receive error details + * @return Number of files (not including subfolders) + */ + size_t getFileCount(StorageError* error = nullptr) override; + + /** + * @brief Get the number of subfolders in this folder. + * @param error Optional pointer to receive error details + * @return Number of subfolders + */ + size_t getFolderCount(StorageError* error = nullptr) override; + + // ==================== Path Information ==================== + + /** + * @brief Get the parent folder of this folder. + * @param error Optional pointer to receive error details + * @return QSPIFolder representing the parent directory + */ + QSPIFolder getParentFolder(StorageError* error = nullptr) const; + +private: + bool resolvePath(const char* path, char* resolved, StorageError* error); + bool removeRecursive(const char* path, StorageError* error); + 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..b9640c104 --- /dev/null +++ b/libraries/QSPIStorage/QSPIStorage.cpp @@ -0,0 +1,188 @@ +/* + * Copyright (c) 2024 Arduino SA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "QSPIStorage.h" + +#include +#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"); +} + +// ========== Static Mount Info Methods ========== + +int QSPIStorage::getMountCount() { + int count = 0; + int idx = 0; + const char *mnt_point; + + while (fs_readmount(&idx, &mnt_point) >= 0) { + count++; + } + + return count; +} + +bool QSPIStorage::getMountInfo(int index, QSPIMountInfo& info) { + int idx = 0; + int current = 0; + const char *mnt_point; + + while (fs_readmount(&idx, &mnt_point) >= 0) { + if (current == index) { + info.mountPoint = mnt_point; + // FAT mount points end with ':' + size_t len = strlen(mnt_point); + info.isFAT = (len > 0 && mnt_point[len - 1] == ':'); + return true; + } + current++; + } + + return false; +} + +void QSPIStorage::listMounts() { + int idx = 0; + const char *mnt_point; + bool found = false; + + while (fs_readmount(&idx, &mnt_point) >= 0) { + Serial.print("Mount point "); + Serial.print(idx - 1); + Serial.print(": "); + Serial.print(mnt_point); + + // Detect filesystem type by mount point naming convention + size_t len = strlen(mnt_point); + if (len > 0 && mnt_point[len - 1] == ':') { + Serial.print(" (FAT)"); + } else { + Serial.print(" (LittleFS)"); + } + Serial.println(); + + found = true; + } + + if (!found) { + Serial.println("No mounted filesystems found!"); + } +} + +void QSPIStorage::listDirectory(const char* path) { + struct fs_dir_t dir; + fs_dir_t_init(&dir); + + int ret = fs_opendir(&dir, path); + if (ret < 0) { + Serial.print("Error opening directory "); + Serial.print(path); + Serial.print(" [error: "); + Serial.print(ret); + Serial.println("]"); + return; + } + + Serial.print("\nContents of "); + Serial.print(path); + Serial.println(":"); + + struct fs_dirent entry; + bool empty = true; + + while (true) { + ret = fs_readdir(&dir, &entry); + if (ret < 0 || entry.name[0] == '\0') { + break; + } + + empty = false; + if (entry.type == FS_DIR_ENTRY_FILE) { + Serial.print(" [FILE] "); + Serial.print(entry.name); + Serial.print(" ("); + Serial.print((size_t)entry.size); + Serial.println(" bytes)"); + } else if (entry.type == FS_DIR_ENTRY_DIR) { + Serial.print(" [DIR ] "); + Serial.println(entry.name); + } + } + + if (empty) { + Serial.println(" "); + } + + fs_closedir(&dir); +} + +void QSPIStorage::listAllMounts() { + int idx = 0; + const char *mnt_point; + + while (fs_readmount(&idx, &mnt_point) >= 0) { + listDirectory(mnt_point); + } +} diff --git a/libraries/QSPIStorage/QSPIStorage.h b/libraries/QSPIStorage/QSPIStorage.h new file mode 100644 index 000000000..55f7b36b8 --- /dev/null +++ b/libraries/QSPIStorage/QSPIStorage.h @@ -0,0 +1,185 @@ +/* + * Copyright (c) 2024 Arduino SA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef QSPI_STORAGE_H +#define QSPI_STORAGE_H + +#include +#include +#include "QSPIFolder.h" + +/** + * @struct QSPIMountInfo + * @brief Information about a mounted filesystem. + */ +struct QSPIMountInfo { + const char* mountPoint; ///< Mount point path (e.g., "/storage") + bool isFAT; ///< true if FAT filesystem, false if LittleFS +}; + +/** + * @class QSPIStorage + * @brief Main interface for QSPI flash storage access. + * + * QSPIStorage provides high-level access to QSPI flash storage with LittleFS + * filesystem. Use this class to initialize storage, get storage statistics, + * and access the root folder for file operations. + * + * The filesystem is automatically mounted at boot via devicetree FSTAB + * configuration. This library verifies the mount and provides access. + * + * @note Requires LittleFS auto-mount via devicetree FSTAB at "/storage". + * + * @example SimpleReadWrite.ino + * @code + * #include + * + * QSPIStorage storage; + * + * void setup() { + * if (!storage.begin()) { + * Serial.println("Storage init failed!"); + * return; + * } + * + * QSPIFile file("/storage/test.txt"); + * file.open(FileMode::WRITE); + * file.write("Hello QSPI!"); + * file.close(); + * } + * @endcode + */ +class QSPIStorage { +public: + /** + * @brief Default constructor. + */ + QSPIStorage() : mounted_(false) {} + + /** + * @brief Destructor. + */ + ~QSPIStorage() {} + + // ==================== Initialization ==================== + + /** + * @brief Initialize and verify the QSPI filesystem is mounted. + * + * Checks that LittleFS is mounted at /storage. The filesystem + * is auto-mounted by Zephyr via devicetree FSTAB configuration. + * + * @param error Optional pointer to receive error details + * @return true if storage is ready, false otherwise + */ + bool begin(StorageError* error = nullptr); + + /** + * @brief Mark storage as not in use. + * + * Does not unmount the filesystem (it remains mounted by the OS). + * + * @param error Optional pointer to receive error details + */ + void end(StorageError* error = nullptr) { mounted_ = false; } + + /** + * @brief Check if storage is initialized and ready. + * @return true if mounted and ready for use + */ + bool isMounted() const { return mounted_; } + + /** + * @brief Get the mount point path. + * @return Mount point string "/storage" + */ + const char* getMountPoint() const { return "/storage"; } + + // ==================== Storage Information ==================== + + /** + * @brief Get storage space statistics. + * + * @param total Output: total storage capacity in bytes + * @param used Output: used storage space in bytes + * @param available Output: available storage space in bytes + * @param error Optional pointer to receive error details + * @return true if successful, false otherwise + * + * @code + * size_t total, used, available; + * if (storage.getStorageInfo(total, used, available)) { + * Serial.print("Used: "); + * Serial.print(used / 1024); + * Serial.println(" KB"); + * } + * @endcode + */ + bool getStorageInfo(size_t& total, size_t& used, size_t& available, StorageError* error = nullptr); + + /** + * @brief Get the root folder of the storage. + * + * Returns a QSPIFolder object representing /storage, which can be + * used to create files, subfolders, and enumerate contents. + * + * @param error Optional pointer to receive error details + * @return QSPIFolder representing the root directory + * + * @code + * QSPIFolder root = storage.getRootFolder(); + * QSPIFolder data = root.createSubfolder("data"); + * @endcode + */ + QSPIFolder getRootFolder(StorageError* error = nullptr); + + // ==================== Static Mount Utilities ==================== + + /** + * @brief Get the number of mounted filesystems. + * + * Includes all filesystems mounted by the OS (LittleFS, FAT, etc.). + * + * @return Number of mounted filesystems + */ + static int getMountCount(); + + /** + * @brief Get mount point information by index. + * @param index Mount index (0-based) + * @param info Output structure to receive mount information + * @return true if mount exists at index, false otherwise + */ + static bool getMountInfo(int index, QSPIMountInfo& info); + + /** + * @brief Print all mounted filesystems to Serial. + * + * Useful for debugging mount configuration. + */ + static void listMounts(); + + /** + * @brief Print directory contents to Serial. + * + * Lists all files and folders at the specified path with sizes. + * + * @param path Directory path to list (e.g., "/storage") + */ + static void listDirectory(const char* path); + + /** + * @brief Print contents of all mounted filesystems to Serial. + * + * Calls listDirectory() for each mounted filesystem. + */ + static void listAllMounts(); + +private: + bool mounted_; +}; + +#endif // QSPI_STORAGE_H diff --git a/libraries/QSPIStorage/README.md b/libraries/QSPIStorage/README.md new file mode 100644 index 000000000..494940574 --- /dev/null +++ b/libraries/QSPIStorage/README.md @@ -0,0 +1,217 @@ +# QSPIStorage Library + +The **QSPIStorage** library provides a simple interface for accessing QSPI flash storage on Arduino boards running Zephyr RTOS. It offers file and folder operations using the LittleFS filesystem. + +## Features + +- Read and write files to QSPI flash memory +- Create, rename, and delete files and folders +- Binary data storage for structs and arrays +- Directory listing and enumeration +- Storage statistics (total, used, available space) +- Seek operations for random file access +- Copy and move files between locations + +## Compatibility + +This library is designed for Arduino boards with QSPI flash storage running the Zephyr-based Arduino core: + +| Board | QSPI Flash | Status | +|-------|------------|--------| +| Arduino GIGA R1 WiFi | 16 MB | Supported | +| Arduino Portenta H7 | 16 MB | Supported | +| Arduino Portenta C33 | 16 MB | Supported | + +## Examples + +### SimpleReadWrite + +The simplest example - write a string to a file and read it back: + +```cpp +#include + +QSPIStorage storage; + +void setup() { + storage.begin(); + + QSPIFile file("/storage/test.txt"); + file.open(FileMode::WRITE); + file.write("Hello World!"); + file.close(); + + file.open(FileMode::READ); + String content = file.readAsString(); + Serial.println(content); + file.close(); +} +``` + +### BasicFileOperations + +Demonstrates common file operations including creating, reading, writing, and deleting files with proper error handling. + +### BasicFolderOperations + +Shows how to create folders, list directory contents, create nested folder structures, and manage subfolders. + +### BinaryStorage + +Store and retrieve binary data such as structs and arrays - useful for sensor data logging and configuration storage. + +### FileCopyMove + +Copy and move files and folders between locations on the storage. + +### ListFiles + +List all mounted filesystems and display their contents with file sizes. + +## Quick Start + +### 1. Include the library + +```cpp +#include +``` + +### 2. Create a storage instance and initialize + +```cpp +QSPIStorage storage; + +void setup() { + Serial.begin(115200); + + if (!storage.begin()) { + Serial.println("Failed to initialize storage!"); + return; + } +} +``` + +### 3. Work with files + +```cpp +// Write a file +QSPIFile file("/storage/data.txt"); +file.open(FileMode::WRITE); +file.write("Hello!"); +file.close(); + +// Read a file +file.open(FileMode::READ); +String content = file.readAsString(); +file.close(); + +// Append to a file +file.open(FileMode::APPEND); +file.write("\nMore data"); +file.close(); +``` + +### 4. Work with folders + +```cpp +QSPIFolder root = storage.getRootFolder(); + +// Create a subfolder +QSPIFolder data = root.createSubfolder("data"); + +// Create a file in the folder +QSPIFile log = data.createFile("log.txt", FileMode::WRITE); +log.write("Log entry"); +log.close(); + +// List files +std::vector files = data.getFiles(); +for (auto& f : files) { + Serial.println(f.getFilename()); +} +``` + +## File Modes + +| Mode | Description | +|------|-------------| +| `FileMode::READ` | Open for reading (file must exist) | +| `FileMode::WRITE` | Create or truncate file for writing | +| `FileMode::APPEND` | Open for writing at end of file | +| `FileMode::READ_WRITE` | Open for both reading and writing | + +## Error Handling + +All methods accept an optional `StorageError*` parameter: + +```cpp +StorageError error; +if (!file.open(FileMode::READ, &error)) { + Serial.print("Error: "); + Serial.println(error.getMessage()); +} +``` + +## Storage Layout + +The QSPI flash is partitioned via devicetree: + +| Partition | Filesystem | Mount Point | Purpose | +|-----------|------------|-------------|---------| +| wlan | FAT | `/wlan:` | WiFi certificates | +| ota | FAT | `/ota:` | OTA updates | +| storage | LittleFS | `/storage` | User data | + +This library accesses the `/storage` partition. + +## API Reference + +### QSPIStorage + +| Method | Description | +|--------|-------------| +| `begin()` | Initialize and verify storage is mounted | +| `end()` | Mark storage as not in use | +| `isMounted()` | Check if storage is ready | +| `getStorageInfo()` | Get total, used, and available space | +| `getRootFolder()` | Get the root folder object | +| `listMounts()` | Print mounted filesystems to Serial | +| `listDirectory()` | Print directory contents to Serial | + +### QSPIFile + +| Method | Description | +|--------|-------------| +| `open()` | Open the file with specified mode | +| `close()` | Close the file | +| `read()` | Read bytes into a buffer | +| `readAsString()` | Read entire file as String | +| `write()` | Write bytes or String | +| `seek()` | Move to position in file | +| `position()` | Get current position | +| `size()` | Get file size | +| `available()` | Get bytes remaining to read | +| `exists()` | Check if file exists | +| `remove()` | Delete the file | +| `rename()` | Rename or move the file | + +### QSPIFolder + +| Method | Description | +|--------|-------------| +| `exists()` | Check if folder exists | +| `create()` | Create the folder | +| `remove()` | Delete the folder | +| `rename()` | Rename or move the folder | +| `createFile()` | Create a file in this folder | +| `getFile()` | Get a file object | +| `createSubfolder()` | Create a subfolder | +| `getSubfolder()` | Get a subfolder object | +| `getFiles()` | List all files | +| `getFolders()` | List all subfolders | +| `getFileCount()` | Count files | +| `getFolderCount()` | Count subfolders | + +## License + +Copyright (c) 2024 Arduino SA. Licensed under the Apache License, Version 2.0. 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/BasicFolderOperations/BasicFolderOperations.ino b/libraries/QSPIStorage/examples/BasicFolderOperations/BasicFolderOperations.ino new file mode 100644 index 000000000..ba7de55cf --- /dev/null +++ b/libraries/QSPIStorage/examples/BasicFolderOperations/BasicFolderOperations.ino @@ -0,0 +1,185 @@ +/* + QSPIStorage - Basic Folder Operations Example + + Simple example showing how to create, list, and manage folders. + + This example code is in the public domain. +*/ + +#include + +QSPIStorage storage; + +void setup() { + Serial.begin(115200); + while (!Serial) { + delay(10); + } + + Serial.println("QSPIStorage - Basic Folder Operations\n"); + + if (!storage.begin()) { + Serial.println("Failed to initialize storage!"); + while (1) delay(1000); + } + Serial.println("Storage ready.\n"); + + // ====================== + // CREATE a folder + // ====================== + Serial.println("Creating folder..."); + + QSPIFolder root = storage.getRootFolder(); + QSPIFolder myFolder = root.createSubfolder("myfolder"); + + Serial.print("Created: "); + Serial.println(myFolder.getPath()); + + // ====================== + // CREATE nested folders + // ====================== + Serial.println("\nCreating nested folders..."); + + QSPIFolder level1 = root.createSubfolder("level1"); + QSPIFolder level2 = level1.createSubfolder("level2"); + QSPIFolder level3 = level2.createSubfolder("level3"); + + Serial.println("Created: /storage/level1/level2/level3"); + + // ====================== + // CREATE files in folder + // ====================== + Serial.println("\nCreating files in folder..."); + + QSPIFile file1 = myFolder.createFile("file1.txt", FileMode::WRITE); + file1.write("Content of file 1"); + file1.close(); + + QSPIFile file2 = myFolder.createFile("file2.txt", FileMode::WRITE); + file2.write("Content of file 2"); + file2.close(); + + Serial.println("Created 2 files in myfolder"); + + // ====================== + // LIST folder contents + // ====================== + Serial.println("\nListing folder contents..."); + + Serial.print("Files in "); + Serial.print(myFolder.getPath()); + Serial.println(":"); + + std::vector files = myFolder.getFiles(); + for (auto& f : files) { + Serial.print(" "); + Serial.print(f.getFilename()); + Serial.print(" ("); + Serial.print(f.size()); + Serial.println(" bytes)"); + } + + // ====================== + // COUNT items + // ====================== + Serial.println("\nCounting items..."); + + Serial.print("Files in myfolder: "); + Serial.println(myFolder.getFileCount()); + + Serial.print("Folders in root: "); + Serial.println(root.getFolderCount()); + + // ====================== + // LIST subfolders + // ====================== + Serial.println("\nListing subfolders in root:"); + + std::vector folders = root.getFolders(); + for (auto& folder : folders) { + Serial.print(" [DIR] "); + Serial.println(folder.getFolderName()); + } + + // ====================== + // GET subfolder + // ====================== + Serial.println("\nGetting existing subfolder..."); + + QSPIFolder existing = root.getSubfolder("myfolder"); + Serial.print("Got folder: "); + Serial.println(existing.getPath()); + Serial.print("It has "); + Serial.print(existing.getFileCount()); + Serial.println(" files"); + + // ====================== + // CHECK if folder exists + // ====================== + Serial.println("\nChecking folder existence..."); + + Serial.print("myfolder exists: "); + Serial.println(myFolder.exists() ? "Yes" : "No"); + + QSPIFolder fake("/storage/doesnotexist"); + Serial.print("doesnotexist exists: "); + Serial.println(fake.exists() ? "Yes" : "No"); + + // ====================== + // GET parent folder + // ====================== + Serial.println("\nGetting parent folder..."); + + QSPIFolder parent = level3.getParentFolder(); + Serial.print("Parent of level3: "); + Serial.println(parent.getPath()); + + // ====================== + // RENAME folder + // ====================== + Serial.println("\nRenaming folder..."); + + QSPIFolder toRename = root.createSubfolder("oldname"); + Serial.print("Created: "); + Serial.println(toRename.getPath()); + + toRename.rename("newname"); + Serial.println("Renamed to: newname"); + + // ====================== + // DELETE empty folder + // ====================== + Serial.println("\nDeleting empty folder..."); + + QSPIFolder emptyFolder = root.createSubfolder("todelete"); + Serial.println("Created: todelete"); + + emptyFolder.remove(); + Serial.println("Deleted: todelete"); + + // ====================== + // DELETE folder with contents (recursive) + // ====================== + Serial.println("\nDeleting folder with contents..."); + + QSPIFolder withFiles = root.createSubfolder("withfiles"); + QSPIFile tempFile = withFiles.createFile("temp.txt", FileMode::WRITE); + tempFile.write("temp"); + tempFile.close(); + Serial.println("Created: withfiles/temp.txt"); + + withFiles.remove(true); // true = recursive + Serial.println("Deleted: withfiles (recursive)"); + + // ====================== + // FINAL listing + // ====================== + Serial.println("\nFinal folder listing:"); + QSPIStorage::listDirectory("/storage"); + + Serial.println("\nDone!"); +} + +void loop() { + delay(10000); +} diff --git a/libraries/QSPIStorage/examples/BinaryStorage/BinaryStorage.ino b/libraries/QSPIStorage/examples/BinaryStorage/BinaryStorage.ino new file mode 100644 index 000000000..eefafb693 --- /dev/null +++ b/libraries/QSPIStorage/examples/BinaryStorage/BinaryStorage.ino @@ -0,0 +1,164 @@ +/* + QSPIStorage - Binary Storage Example + + Simple example showing how to store and retrieve binary data (structs). + + This example code is in the public domain. +*/ + +#include + +QSPIStorage storage; + +// Example struct to store +struct SensorData { + uint32_t timestamp; + float temperature; + float humidity; + uint16_t lightLevel; +}; + +void setup() { + Serial.begin(115200); + while (!Serial) { + delay(10); + } + + Serial.println("QSPIStorage - Binary Storage Example\n"); + + if (!storage.begin()) { + Serial.println("Failed to initialize storage!"); + while (1) delay(1000); + } + Serial.println("Storage ready.\n"); + + // ====================== + // WRITE a single struct + // ====================== + Serial.println("Writing single struct..."); + + SensorData data; + data.timestamp = millis(); + data.temperature = 23.5; + data.humidity = 65.2; + data.lightLevel = 512; + + QSPIFile file("/storage/sensor.bin"); + file.open(FileMode::WRITE); + file.write((uint8_t*)&data, sizeof(data)); + file.close(); + + Serial.print("Wrote "); + Serial.print(sizeof(data)); + Serial.println(" bytes\n"); + + // ====================== + // READ a single struct + // ====================== + Serial.println("Reading single struct..."); + + SensorData readData; + file.open(FileMode::READ); + file.read((uint8_t*)&readData, sizeof(readData)); + file.close(); + + Serial.print(" Timestamp: "); + Serial.println(readData.timestamp); + Serial.print(" Temperature: "); + Serial.println(readData.temperature); + Serial.print(" Humidity: "); + Serial.println(readData.humidity); + Serial.print(" Light: "); + Serial.println(readData.lightLevel); + + // ====================== + // WRITE array of structs + // ====================== + Serial.println("\nWriting array of structs..."); + + const int COUNT = 5; + SensorData readings[COUNT]; + + for (int i = 0; i < COUNT; i++) { + readings[i].timestamp = millis() + (i * 1000); + readings[i].temperature = 20.0 + i; + readings[i].humidity = 50.0 + (i * 2); + readings[i].lightLevel = 100 * (i + 1); + } + + QSPIFile arrayFile("/storage/readings.bin"); + arrayFile.open(FileMode::WRITE); + + // Write count first + uint32_t count = COUNT; + arrayFile.write((uint8_t*)&count, sizeof(count)); + + // Write all readings + arrayFile.write((uint8_t*)readings, sizeof(readings)); + arrayFile.close(); + + Serial.print("Wrote "); + Serial.print(COUNT); + Serial.println(" readings\n"); + + // ====================== + // READ array of structs + // ====================== + Serial.println("Reading array of structs..."); + + arrayFile.open(FileMode::READ); + + // Read count + uint32_t readCount; + arrayFile.read((uint8_t*)&readCount, sizeof(readCount)); + Serial.print("Found "); + Serial.print(readCount); + Serial.println(" readings:"); + + // Read each one + for (uint32_t i = 0; i < readCount; i++) { + SensorData r; + arrayFile.read((uint8_t*)&r, sizeof(r)); + Serial.print(" ["); + Serial.print(i); + Serial.print("] T="); + Serial.print(r.temperature); + Serial.print(", H="); + Serial.print(r.humidity); + Serial.print(", L="); + Serial.println(r.lightLevel); + } + arrayFile.close(); + + // ====================== + // APPEND binary data + // ====================== + Serial.println("\nAppending more data..."); + + arrayFile.open(FileMode::READ); + arrayFile.read((uint8_t*)&count, sizeof(count)); + arrayFile.close(); + + // Update count and append new reading + SensorData newReading = {millis(), 30.0, 70.0, 999}; + + arrayFile.open(FileMode::READ_WRITE); + + // Update count at beginning + count++; + arrayFile.write((uint8_t*)&count, sizeof(count)); + + // Seek to end and append + arrayFile.seek(sizeof(count) + (count - 1) * sizeof(SensorData)); + arrayFile.write((uint8_t*)&newReading, sizeof(newReading)); + arrayFile.close(); + + Serial.print("Total readings now: "); + Serial.println(count); + + Serial.println("\nDone!"); +} + +void loop() { + delay(10000); +} diff --git a/libraries/QSPIStorage/examples/FileCopyMove/FileCopyMove.ino b/libraries/QSPIStorage/examples/FileCopyMove/FileCopyMove.ino new file mode 100644 index 000000000..07aec5498 --- /dev/null +++ b/libraries/QSPIStorage/examples/FileCopyMove/FileCopyMove.ino @@ -0,0 +1,272 @@ +/* + QSPIStorage - File Copy and Move Example + + This example demonstrates how to copy and move files and folders + using the QSPIStorage library. + + Operations demonstrated: + - Creating files with content + - Copying files (read + write) + - Moving/renaming files + - Creating folders + - Copying folders recursively + - Moving/renaming folders + + This example code is in the public domain. +*/ + +#include + +QSPIStorage storage; + +// Helper function to copy a file +bool copyFile(const char* srcPath, const char* dstPath) { + QSPIFile srcFile(srcPath); + StorageError error; + + if (!srcFile.open(FileMode::READ, &error)) { + Serial.print("Failed to open source file: "); + Serial.println(error.getMessage()); + return false; + } + + QSPIFile dstFile(dstPath); + if (!dstFile.open(FileMode::WRITE, &error)) { + Serial.print("Failed to create destination file: "); + Serial.println(error.getMessage()); + srcFile.close(); + return false; + } + + // Copy in chunks + uint8_t buffer[256]; + size_t totalCopied = 0; + + while (srcFile.available() > 0) { + size_t bytesRead = srcFile.read(buffer, sizeof(buffer), &error); + if (bytesRead == 0) break; + + size_t bytesWritten = dstFile.write(buffer, bytesRead, &error); + if (bytesWritten != bytesRead) { + Serial.println("Write error during copy"); + srcFile.close(); + dstFile.close(); + return false; + } + totalCopied += bytesWritten; + } + + srcFile.close(); + dstFile.close(); + + Serial.print("Copied "); + Serial.print(totalCopied); + Serial.println(" bytes"); + return true; +} + +// Helper function to copy a folder recursively +bool copyFolder(QSPIFolder& srcFolder, QSPIFolder& dstFolder) { + StorageError error; + + // Create destination folder if it doesn't exist + if (!dstFolder.exists()) { + if (!dstFolder.create(&error)) { + Serial.print("Failed to create destination folder: "); + Serial.println(error.getMessage()); + return false; + } + } + + // Copy all files + std::vector files = srcFolder.getFiles(&error); + for (auto& file : files) { + String srcPath = file.getPath(); + String filename = file.getFilename(); + String dstPath = String(dstFolder.getPath()) + "/" + filename; + + Serial.print(" Copying file: "); + Serial.print(filename); + Serial.print(" -> "); + Serial.println(dstPath); + + if (!copyFile(srcPath.c_str(), dstPath.c_str())) { + return false; + } + } + + // Recursively copy subfolders + std::vector subfolders = srcFolder.getFolders(&error); + for (auto& subfolder : subfolders) { + String subName = subfolder.getFolderName(); + String dstSubPath = String(dstFolder.getPath()) + "/" + subName; + + Serial.print(" Copying subfolder: "); + Serial.println(subName); + + QSPIFolder dstSubfolder(dstSubPath); + if (!copyFolder(subfolder, dstSubfolder)) { + return false; + } + } + + return true; +} + +void setup() { + Serial.begin(115200); + while (!Serial) { + delay(10); + } + + Serial.println("\n========================================"); + Serial.println("QSPIStorage - File Copy & Move Example"); + Serial.println("========================================\n"); + + // Initialize storage + StorageError error; + if (!storage.begin(&error)) { + Serial.print("Storage initialization failed: "); + Serial.println(error.getMessage()); + return; + } + Serial.println("Storage initialized successfully!\n"); + + // Get root folder + QSPIFolder root = storage.getRootFolder(); + + // ======================================== + // Part 1: Create test files and folders + // ======================================== + Serial.println("--- Creating Test Files ---\n"); + + // Create a test folder + QSPIFolder testFolder = root.createSubfolder("test_copy", true, &error); + if (error) { + Serial.print("Failed to create test folder: "); + Serial.println(error.getMessage()); + return; + } + Serial.println("Created /storage/test_copy/"); + + // Create a test file with content + QSPIFile file1 = testFolder.createFile("original.txt", FileMode::WRITE, &error); + if (!error) { + file1.write("Hello, this is the original file content!\n"); + file1.write("Line 2 of the file.\n"); + file1.write("Line 3 - the end.\n"); + file1.close(); + Serial.println("Created /storage/test_copy/original.txt"); + } + + // Create a subfolder with files + QSPIFolder subFolder = testFolder.createSubfolder("subfolder", false, &error); + if (!error) { + Serial.println("Created /storage/test_copy/subfolder/"); + + QSPIFile subFile = subFolder.createFile("data.txt", FileMode::WRITE, &error); + if (!error) { + subFile.write("Data in subfolder\n"); + subFile.close(); + Serial.println("Created /storage/test_copy/subfolder/data.txt"); + } + } + + // ======================================== + // Part 2: Copy a file + // ======================================== + Serial.println("\n--- Copying Files ---\n"); + + Serial.println("Copying original.txt to copy.txt..."); + if (copyFile("/storage/test_copy/original.txt", "/storage/test_copy/copy.txt")) { + Serial.println("File copied successfully!"); + } + + // ======================================== + // Part 3: Move/Rename a file + // ======================================== + Serial.println("\n--- Moving/Renaming Files ---\n"); + + // Create a file to move + QSPIFile moveFile = testFolder.createFile("to_move.txt", FileMode::WRITE, &error); + if (!error) { + moveFile.write("This file will be moved.\n"); + moveFile.close(); + Serial.println("Created /storage/test_copy/to_move.txt"); + } + + // Rename/move the file + QSPIFile fileToMove("/storage/test_copy/to_move.txt"); + if (fileToMove.rename("/storage/test_copy/moved.txt", &error)) { + Serial.println("Renamed to_move.txt -> moved.txt"); + } else { + Serial.print("Failed to rename file: "); + Serial.println(error.getMessage()); + } + + // ======================================== + // Part 4: Copy a folder + // ======================================== + Serial.println("\n--- Copying Folders ---\n"); + + Serial.println("Copying test_copy/ to test_backup/..."); + QSPIFolder backupFolder = root.createSubfolder("test_backup", true, &error); + if (copyFolder(testFolder, backupFolder)) { + Serial.println("Folder copied successfully!"); + } + + // ======================================== + // Part 5: Rename a folder + // ======================================== + Serial.println("\n--- Renaming Folders ---\n"); + + // Create a folder to rename + QSPIFolder renameFolder = root.createSubfolder("old_name", true, &error); + if (!error) { + // Add a file to it + QSPIFile rf = renameFolder.createFile("info.txt", FileMode::WRITE, &error); + if (!error) { + rf.write("Folder rename test\n"); + rf.close(); + } + Serial.println("Created /storage/old_name/"); + + // Rename the folder + if (renameFolder.rename("new_name", &error)) { + Serial.println("Renamed old_name/ -> new_name/"); + } else { + Serial.print("Failed to rename folder: "); + Serial.println(error.getMessage()); + } + } + + // ======================================== + // Part 6: Show results + // ======================================== + Serial.println("\n--- Final Directory Contents ---\n"); + QSPIStorage::listDirectory("/storage"); + QSPIStorage::listDirectory("/storage/test_copy"); + QSPIStorage::listDirectory("/storage/test_backup"); + + // ======================================== + // Part 7: Cleanup (optional) + // ======================================== + Serial.println("\n--- Cleanup ---\n"); + Serial.println("To clean up test files, uncomment the cleanup code."); + + // Uncomment to clean up: + /* + QSPIFolder("/storage/test_copy").remove(true, &error); + QSPIFolder("/storage/test_backup").remove(true, &error); + QSPIFolder("/storage/new_name").remove(true, &error); + Serial.println("Test folders removed."); + */ + + Serial.println("\n========================================"); + Serial.println("Done!"); + Serial.println("========================================"); +} + +void loop() { + delay(10000); +} diff --git a/libraries/QSPIStorage/examples/ListFiles/ListFiles.ino b/libraries/QSPIStorage/examples/ListFiles/ListFiles.ino new file mode 100644 index 000000000..2b9cbd74b --- /dev/null +++ b/libraries/QSPIStorage/examples/ListFiles/ListFiles.ino @@ -0,0 +1,160 @@ +/* + QSPIStorage - List Files Example + + This example demonstrates how to list all mounted filesystems + and their contents using the QSPIStorage library. + + It shows: + - All mounted filesystems (FAT and LittleFS) + - Directory contents with file sizes + - Storage statistics + + This example code is in the public domain. +*/ + +#include + +QSPIStorage storage; + +void setup() { + Serial.begin(115200); + while (!Serial) { + delay(10); + } + + Serial.println("\n========================================"); + Serial.println("QSPIStorage - List Files Example"); + Serial.println("========================================\n"); + + // Initialize storage + StorageError error; + if (!storage.begin(&error)) { + Serial.print("Storage initialization failed: "); + Serial.println(error.getMessage()); + Serial.println("\nNote: The filesystem must be formatted first."); + Serial.println("Run the FormatStorage example if needed."); + } else { + Serial.println("Storage initialized successfully!\n"); + } + + // List all mounted filesystems + Serial.println("=== Mounted Filesystems ==="); + QSPIStorage::listMounts(); + + // List contents of all mounted filesystems + Serial.println("\n=== Filesystem Contents ==="); + QSPIStorage::listAllMounts(); + + // Show storage statistics for /storage partition + Serial.println("\n=== Storage Statistics ==="); + showStorageStats(); + + // Demo: Using QSPIFolder to list files with more detail + Serial.println("\n=== Using QSPIFolder API ==="); + listWithFolderAPI(); + + Serial.println("\n========================================"); + Serial.println("Done!"); +} + +void loop() { + delay(10000); +} + +void showStorageStats() { + StorageError error; + size_t total, used, available; + + if (storage.getStorageInfo(total, used, available, &error)) { + Serial.println("User Storage (/storage):"); + Serial.print(" Total: "); + printSize(total); + Serial.print(" Used: "); + printSize(used); + Serial.print(" Available: "); + printSize(available); + Serial.print(" Usage: "); + if (total > 0) { + Serial.print((used * 100) / total); + Serial.println("%"); + } else { + Serial.println("N/A"); + } + } else { + Serial.print("Error getting storage info: "); + Serial.println(error.getMessage()); + } +} + +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"); + } +} + +void listWithFolderAPI() { + StorageError error; + + if (!storage.isMounted()) { + Serial.println("Storage not mounted, skipping folder API demo."); + return; + } + + QSPIFolder root = storage.getRootFolder(&error); + if (error) { + Serial.print("Error getting root folder: "); + Serial.println(error.getMessage()); + return; + } + + Serial.print("Root folder: "); + Serial.println(root.getPath()); + + // Count items + size_t fileCount = root.getFileCount(&error); + size_t folderCount = root.getFolderCount(&error); + + Serial.print(" Files: "); + Serial.println(fileCount); + Serial.print(" Folders: "); + Serial.println(folderCount); + + // List files with details + if (fileCount > 0) { + Serial.println("\n Files:"); + std::vector files = root.getFiles(&error); + for (auto& file : files) { + Serial.print(" "); + Serial.print(file.getFilename()); + Serial.print(" ("); + Serial.print(file.size(&error)); + Serial.println(" bytes)"); + } + } + + // List folders + if (folderCount > 0) { + Serial.println("\n Folders:"); + std::vector folders = root.getFolders(&error); + for (auto& folder : folders) { + Serial.print(" [DIR] "); + Serial.println(folder.getFolderName()); + + // Show contents of each subfolder + size_t subFiles = folder.getFileCount(&error); + size_t subFolders = folder.getFolderCount(&error); + Serial.print(" ("); + Serial.print(subFiles); + Serial.print(" files, "); + Serial.print(subFolders); + Serial.println(" folders)"); + } + } +} 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/examples/SimpleReadWrite/SimpleReadWrite.ino b/libraries/QSPIStorage/examples/SimpleReadWrite/SimpleReadWrite.ino new file mode 100644 index 000000000..e70bc37d0 --- /dev/null +++ b/libraries/QSPIStorage/examples/SimpleReadWrite/SimpleReadWrite.ino @@ -0,0 +1,133 @@ +/* + QSPIStorage - Simple Read/Write Example + + The simplest example showing how to write, read, and seek in a file. + + This example code is in the public domain. +*/ + +#include + +QSPIStorage storage; + +void setup() { + Serial.begin(115200); + while (!Serial) { + delay(10); + } + + Serial.println("QSPIStorage - Simple Read/Write Example\n"); + + // Initialize storage + if (!storage.begin()) { + Serial.println("Failed to initialize storage!"); + while (1) delay(1000); + } + Serial.println("Storage ready.\n"); + + // ====================== + // WRITE to a file + // ====================== + Serial.println("Writing to file..."); + + QSPIFile file("/storage/test.txt"); + file.open(FileMode::WRITE); + file.write("Line 1: Hello\n"); + file.write("Line 2: World\n"); + file.write("Line 3: Done!\n"); + file.close(); + + Serial.println("Write complete.\n"); + + // ====================== + // READ entire file + // ====================== + Serial.println("Reading entire file:"); + + file.open(FileMode::READ); + String content = file.readAsString(); + file.close(); + + Serial.println("---"); + Serial.print(content); + Serial.println("---\n"); + + // ====================== + // READ byte by byte + // ====================== + Serial.println("Reading byte by byte (first 20 chars):"); + + file.open(FileMode::READ); + for (int i = 0; i < 20 && file.available() > 0; i++) { + int c = file.read(); + if (c >= 0) { + Serial.print((char)c); + } + } + Serial.println("...\n"); + + // ====================== + // SEEK and read + // ====================== + Serial.println("Seeking to position 14 (start of Line 2):"); + + file.seek(14); // Skip "Line 1: Hello\n" + String line2 = ""; + int c; + while ((c = file.read()) >= 0 && c != '\n') { + line2 += (char)c; + } + file.close(); + + Serial.print("Read: "); + Serial.println(line2); + + // ====================== + // READ into buffer + // ====================== + Serial.println("\nReading into buffer:"); + + file.open(FileMode::READ); + uint8_t buffer[32]; + size_t bytesRead = file.read(buffer, sizeof(buffer) - 1); + buffer[bytesRead] = '\0'; // Null terminate + file.close(); + + Serial.print("Buffer ("); + Serial.print(bytesRead); + Serial.print(" bytes): "); + Serial.println((char*)buffer); + + // ====================== + // APPEND to file + // ====================== + Serial.println("\nAppending to file..."); + + file.open(FileMode::APPEND); + file.write("Line 4: Appended!\n"); + file.close(); + + // Read back to verify + file.open(FileMode::READ); + Serial.println("File after append:"); + Serial.println("---"); + Serial.print(file.readAsString()); + Serial.println("---"); + file.close(); + + // ====================== + // File info + // ====================== + Serial.print("\nFile size: "); + Serial.print(file.size()); + Serial.println(" bytes"); + + Serial.print("File exists: "); + Serial.println(file.exists() ? "Yes" : "No"); + + Serial.println("\nDone!"); +} + +void loop() { + delay(10000); +} 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"