#include #include #include #include #include #include #include "fs_mgmt.h" #include "buzz_proto.h" LOG_MODULE_REGISTER(fs_mgmt, CONFIG_FS_MGMT_LOG_LEVEL); #define FS_PARTITION_ID FLASH_AREA_ID(littlefs_storage) FS_LITTLEFS_DECLARE_DEFAULT_CONFIG(fs_storage_data); #define QSPI_FLASH_NODE DT_ALIAS(qspi_flash) static const struct device *flash_dev = DEVICE_DT_GET(QSPI_FLASH_NODE); #define TAG_MAGIC "TAG!" #define TAG_FORMAT_VERSION 1U static struct fs_mount_t fs_storage_mnt = { .type = FS_LITTLEFS, .fs_data = &fs_storage_data, .storage_dev = (void *)FS_PARTITION_ID, .mnt_point = CONFIG_FS_MGMT_MOUNT_POINT, }; static int open_count = 0; static struct k_mutex flash_pm_lock; #define ACK_WATERMARK (CONFIG_BUZZ_PROTO_SLAB_COUNT / 4) typedef struct __attribute__((packed)) { uint16_t total_size; uint16_t version; uint8_t magic[4]; } tag_footer_t; K_MSGQ_DEFINE(fs_write_msgq, sizeof(struct fs_write_msg), CONFIG_BUZZ_PROTO_SLAB_COUNT, 4); typedef enum { FS_STATE_IDLE, FS_STATE_RECEIVING_FILE, FS_STATE_RECEIVING_TAGS, FS_STATE_RECEIVING_FIRMWARE } fs_thread_state_t; static struct { fs_thread_state_t state; struct fs_file_t file; char filename[FS_MGMT_MAX_PATH_LENGTH]; uint32_t crc32; uint16_t unacked_chunks; off_t audio_len; // Offeset für Tags } write_ctx; /** * @brief Puts the QSPI flash into deep sleep mode to save power * Decrements the open count and suspends the flash if no more users are active * @return 0 on success, negative error code on failure */ static int fs_mgmt_pm_flash_suspend(void) { #if IS_ENABLED(CONFIG_PM_DEVICE) if (!device_is_ready(flash_dev)) { return -ENODEV; } k_mutex_lock(&flash_pm_lock, K_FOREVER); if (open_count > 0) { open_count--; if (open_count == 0) { int rc = pm_device_action_run(flash_dev, PM_DEVICE_ACTION_SUSPEND); if (rc < 0) { LOG_WRN("Could not suspend flash: %d", rc); } else { LOG_DBG("Flash entered deep power-down"); } } } k_mutex_unlock(&flash_pm_lock); #endif /* CONFIG_PM_DEVICE */ return 0; } /** * @brief Resumes the QSPI flash from deep sleep mode * Increments the open count and resumes the flash if it was previously suspended * @return 0 on success, negative error code on failure */ static int fs_mgmt_pm_flash_resume(void) { #if IS_ENABLED(CONFIG_PM_DEVICE) if (!device_is_ready(flash_dev)) return -ENODEV; k_mutex_lock(&flash_pm_lock, K_FOREVER); if (open_count == 0) { int rc = pm_device_action_run(flash_dev, PM_DEVICE_ACTION_RESUME); if (rc == 0) { k_busy_wait(50); // t-exit-dpd LOG_DBG("Flash resumed"); } } open_count++; k_mutex_unlock(&flash_pm_lock); #endif /* CONFIG_PM_DEVICE */ return 0; } int fs_mgmt_pm_open(struct fs_file_t *file, const char *path, fs_mode_t mode) { LOG_DBG("PM Opening file '%s' with mode 0x%02x", path, mode); fs_mgmt_pm_flash_resume(); int rc = fs_open(file, path, mode); if (rc < 0) { fs_mgmt_pm_flash_suspend(); } return rc; } int fs_mgmt_pm_close(struct fs_file_t *file) { LOG_DBG("PM Closing file"); int rc = fs_close(file); fs_mgmt_pm_flash_suspend(); return rc; } int fs_mgmt_pm_opendir(struct fs_dir_t *dirp, const char *path) { LOG_DBG("PM Opening directory '%s'", path); fs_mgmt_pm_flash_resume(); int rc = fs_opendir(dirp, path); if (rc < 0) { fs_mgmt_pm_flash_suspend(); } return rc; } int fs_mgmt_pm_closedir(struct fs_dir_t *dirp) { LOG_DBG("PM Closing directory"); int rc = fs_closedir(dirp); fs_mgmt_pm_flash_suspend(); return rc; } int fs_mgmt_pm_unlink(const char *path) { LOG_DBG("PM Unlinking file '%s'", path); fs_mgmt_pm_flash_resume(); int rc = fs_unlink(path); fs_mgmt_pm_flash_suspend(); return rc; } int fs_mgmt_pm_statvfs(const char *path, struct fs_statvfs *stat) { LOG_DBG("PM Getting filesystem stats for '%s'", path); fs_mgmt_pm_flash_resume(); int rc = fs_statvfs(path, stat); fs_mgmt_pm_flash_suspend(); return rc; } int fs_mgmt_pm_stat(const char *path, struct fs_dirent *entry) { LOG_DBG("PM Getting stat for '%s'", path); fs_mgmt_pm_flash_resume(); int rc = fs_stat(path, entry); fs_mgmt_pm_flash_suspend(); return rc; } int fs_mgmt_pm_mkdir(const char *path) { LOG_DBG("PM Creating directory '%s'", path); fs_mgmt_pm_flash_resume(); int rc = fs_mkdir(path); fs_mgmt_pm_flash_suspend(); return rc; } int fs_mgmt_pm_rename(const char *old_path, const char *new_path) { LOG_DBG("PM Renaming '%s' to '%s'", old_path, new_path); fs_mgmt_pm_flash_resume(); int rc = fs_rename(old_path, new_path); fs_mgmt_pm_flash_suspend(); return rc; } int fs_mgmt_pm_rm_recursive(char *path_buf, size_t max_len) { struct fs_dirent entry; struct fs_dir_t dir; int rc; fs_mgmt_pm_flash_resume(); /* 1. Stat prüfen: Ist es eine Datei? */ rc = fs_stat(path_buf, &entry); if (rc != 0) { fs_mgmt_pm_flash_suspend(); return rc; } /* Wenn es eine Datei ist, direkt löschen und beenden */ if (entry.type == FS_DIR_ENTRY_FILE) { rc = fs_unlink(path_buf); fs_mgmt_pm_flash_suspend(); return rc; } /* 2. Es ist ein Verzeichnis. Schleife bis es leer ist. */ size_t orig_len = strlen(path_buf); while (1) { fs_dir_t_init(&dir); rc = fs_opendir(&dir, path_buf); if (rc != 0) { break; } bool found_something = false; /* Genau EINEN löschbaren Eintrag suchen */ while (1) { rc = fs_readdir(&dir, &entry); if (rc != 0 || entry.name[0] == '\0') { break; /* Ende oder Fehler */ } if (strcmp(entry.name, ".") == 0 || strcmp(entry.name, "..") == 0) { continue; /* Ignorieren */ } found_something = true; break; /* Treffer! Schleife abbrechen. */ } /* WICHTIG: Das Verzeichnis SOFORT schließen, BEVOR wir rekurieren! * Damit geben wir das File-Handle (NUM_DIRS) an Zephyr zurück. */ fs_closedir(&dir); if (!found_something || rc != 0) { break; /* Verzeichnis ist nun restlos leer */ } size_t name_len = strlen(entry.name); if (orig_len + 1 + name_len >= max_len) { rc = -ENAMETOOLONG; break; } /* Pfad für das gefundene Kindelement bauen */ path_buf[orig_len] = '/'; strcpy(&path_buf[orig_len + 1], entry.name); /* Rekursiver Aufruf für das Kind */ rc = fs_mgmt_pm_rm_recursive(path_buf, max_len); /* Puffer sofort wieder auf unser Verzeichnis zurückschneiden */ path_buf[orig_len] = '\0'; if (rc != 0) { break; /* Abbruch, falls beim Löschen des Kindes ein Fehler auftrat */ } } /* 3. Das nun restlos leere Verzeichnis selbst löschen */ if (rc == 0) { rc = fs_unlink(path_buf); } fs_mgmt_pm_flash_suspend(); return rc; } int fs_mgmt_pm_mkdir_recursive(char *path) { int rc = 0; struct fs_dirent entry; char *p = path; /* Führenden Slash überspringen, falls vorhanden (z. B. bei "/lfs") */ if (*p == '/') { p++; } /* Flash für den gesamten Durchlauf aktivieren */ fs_mgmt_pm_flash_resume(); while (*p != '\0') { if (*p == '/') { *p = '\0'; /* String temporär am aktuellen Slash terminieren */ /* Prüfen, ob dieser Pfadabschnitt bereits existiert */ rc = fs_stat(path, &entry); if (rc == -ENOENT) { /* Existiert nicht -> anlegen */ rc = fs_mkdir(path); if (rc != 0) { *p = '/'; /* Bei Fehler Slash wiederherstellen und abbrechen */ break; } } else if (rc == 0) { /* Existiert -> prüfen, ob es ein Verzeichnis ist */ if (entry.type != FS_DIR_ENTRY_DIR) { rc = -ENOTDIR; *p = '/'; break; } } else { /* Anderer Dateisystemfehler */ *p = '/'; break; } *p = '/'; /* Slash für den nächsten Schleifendurchlauf wiederherstellen */ } p++; } /* Letztes Element verarbeiten, falls der Pfad nicht mit '/' endet */ if (rc == 0 && p > path && *(p - 1) != '/') { rc = fs_stat(path, &entry); if (rc == -ENOENT) { rc = fs_mkdir(path); } else if (rc == 0) { if (entry.type != FS_DIR_ENTRY_DIR) { rc = -ENOTDIR; } } } /* Flash am Ende wieder in den Suspend schicken */ fs_mgmt_pm_flash_suspend(); return rc; } int fs_mgmt_init(void) { k_mutex_init(&flash_pm_lock); if (!device_is_ready(flash_dev)) { LOG_ERR("Flash device not ready!"); return -ENODEV; } fs_mgmt_pm_flash_resume(); int rc = fs_mount(&fs_storage_mnt); if (rc < 0) { LOG_ERR("Error mounting filesystem: %d", rc); return rc; } fs_mgmt_pm_flash_suspend(); LOG_DBG("Filesystem mounted successfully"); return 0; } static int fs_get_tag_bounds(struct fs_file_t *fp, off_t file_size, size_t *audio_limit, size_t *payload_len, bool *has_tag) { tag_footer_t footer; if (audio_limit == NULL || payload_len == NULL || has_tag == NULL) { return -EINVAL; } *has_tag = false; *audio_limit = (size_t)file_size; *payload_len = 0U; if (file_size < (off_t)sizeof(tag_footer_t)) { return 0; } fs_seek(fp, -(off_t)sizeof(tag_footer_t), FS_SEEK_END); if (fs_read(fp, &footer, sizeof(tag_footer_t)) != sizeof(tag_footer_t)) { fs_seek(fp, 0, FS_SEEK_SET); return -EIO; } if (memcmp(footer.magic, TAG_MAGIC, 4) != 0) { fs_seek(fp, 0, FS_SEEK_SET); return 0; } uint16_t tag_version = sys_le16_to_cpu(footer.version); uint16_t tag_len = sys_le16_to_cpu(footer.total_size); if (tag_version != TAG_FORMAT_VERSION) { fs_seek(fp, 0, FS_SEEK_SET); return -ENOTSUP; } if (tag_len > (uint16_t)file_size || tag_len < sizeof(tag_footer_t)) { fs_seek(fp, 0, FS_SEEK_SET); return -EBADMSG; } *has_tag = true; *audio_limit = (size_t)file_size - tag_len; *payload_len = tag_len - sizeof(tag_footer_t); fs_seek(fp, 0, FS_SEEK_SET); return 0; } ssize_t fs_get_audio_data_len(struct fs_file_t *fp) { off_t file_size; size_t audio_limit = 0U; size_t payload_len = 0U; bool has_tag = false; fs_seek(fp, 0, FS_SEEK_END); file_size = fs_tell(fp); if (file_size < 0) { fs_seek(fp, 0, FS_SEEK_SET); return -EIO; } if (fs_get_tag_bounds(fp, file_size, &audio_limit, &payload_len, &has_tag) < 0) { fs_seek(fp, 0, FS_SEEK_SET); return -EIO; } fs_seek(fp, 0, FS_SEEK_SET); return has_tag ? (ssize_t)audio_limit : file_size; } int fs_mgmt_submit_write(struct fs_write_msg *msg) { return k_msgq_put(&fs_write_msgq, msg, K_NO_WAIT); } static void fs_thread_entry(void *p1, void *p2, void *p3) { LOG_INF("FS Write Thread started"); write_ctx.state = FS_STATE_IDLE; fs_file_t_init(&write_ctx.file); struct fs_write_msg msg; while (1) { /* Watchdog nur bei aktiven Transfers */ k_timeout_t wait_time = (write_ctx.state == FS_STATE_IDLE) ? K_FOREVER : K_SECONDS(2); int rc = k_msgq_get(&fs_write_msgq, &msg, wait_time); if (rc == -EAGAIN) { LOG_WRN("Write timeout! Aborting transfer."); if (write_ctx.state == FS_STATE_RECEIVING_FILE) { fs_mgmt_pm_close(&write_ctx.file); fs_mgmt_pm_unlink(write_ctx.filename); } write_ctx.state = FS_STATE_IDLE; continue; } switch (write_ctx.state) { case FS_STATE_IDLE: if (msg.op == FS_WRITE_OP_FILE_START) { if (msg.data_len >= sizeof(write_ctx.filename)) { LOG_ERR("Filename too long"); buzz_proto_send_error_reusing_slab(msg.reply_cb, ENAMETOOLONG, msg.slab_ptr); break; } memcpy(write_ctx.filename, msg.slab_ptr + msg.data_offset, msg.data_len); write_ctx.filename[msg.data_len] = '\0'; fs_mgmt_pm_unlink(write_ctx.filename); rc = fs_mgmt_pm_open(&write_ctx.file, write_ctx.filename, FS_O_CREATE | FS_O_WRITE); if (rc == 0) { write_ctx.state = FS_STATE_RECEIVING_FILE; write_ctx.crc32 = 0; write_ctx.unacked_chunks = 0; LOG_INF("File transfer started: %s (Expected: %u bytes)", write_ctx.filename, msg.metadata); uint16_t credits = buzz_proto_get_free_rx_slabs(); buzz_proto_buf_free(&msg.slab_ptr); buzz_proto_send_ack(msg.reply_cb, credits); } else { LOG_ERR("Failed to open %s: %d", write_ctx.filename, rc); buzz_proto_send_error_reusing_slab(msg.reply_cb, abs(rc), msg.slab_ptr); } }/* Innerhalb von case FS_STATE_IDLE: */ else if (msg.op == FS_WRITE_OP_TAGS_START) { if (msg.data_len >= sizeof(write_ctx.filename)) { buzz_proto_send_error_reusing_slab(msg.reply_cb, ENAMETOOLONG, msg.slab_ptr); break; } memcpy(write_ctx.filename, msg.slab_ptr + msg.data_offset, msg.data_len); write_ctx.filename[msg.data_len] = '\0'; /* Datei öffnen: Nur Lese- und Schreibrechte, Datei muss bereits existieren */ int rc = fs_mgmt_pm_open(&write_ctx.file, write_ctx.filename, FS_O_READ | FS_O_WRITE); if (rc == 0) { ssize_t audio_len = fs_get_audio_data_len(&write_ctx.file); if (audio_len < 0) { LOG_ERR("Failed to get audio length: %d", (int)audio_len); fs_mgmt_pm_close(&write_ctx.file); buzz_proto_send_error_reusing_slab(msg.reply_cb, EIO, msg.slab_ptr); break; } /* Datei ab dem Ende der Audiodaten abschneiden (alte Tags entfernen) */ rc = fs_truncate(&write_ctx.file, audio_len); if (rc != 0) { LOG_ERR("Failed to truncate file: %d", rc); fs_mgmt_pm_close(&write_ctx.file); buzz_proto_send_error_reusing_slab(msg.reply_cb, abs(rc), msg.slab_ptr); break; } /* File-Pointer exakt an das neue Ende (audio_len) setzen */ fs_seek(&write_ctx.file, audio_len, FS_SEEK_SET); write_ctx.state = FS_STATE_RECEIVING_TAGS; write_ctx.crc32 = 0; write_ctx.unacked_chunks = 0; write_ctx.audio_len = audio_len; LOG_INF("Tags transfer started: %s (Expected tags: %u bytes)", write_ctx.filename, msg.metadata); uint16_t credits = buzz_proto_get_free_rx_slabs(); buzz_proto_buf_free(&msg.slab_ptr); buzz_proto_send_ack(msg.reply_cb, credits); } else { LOG_ERR("Failed to open %s for tags: %d", write_ctx.filename, rc); buzz_proto_send_error_reusing_slab(msg.reply_cb, abs(rc), msg.slab_ptr); } } else if ( msg.op == FS_WRITE_OP_FW_START) { LOG_WRN("Operation not yet fully implemented in FS state machine"); buzz_proto_send_error_reusing_slab(msg.reply_cb, ENOSYS, msg.slab_ptr); } break; case FS_STATE_RECEIVING_FILE: if (msg.op == FS_WRITE_OP_FILE_CHUNK && msg.slab_ptr) { ssize_t written = fs_write(&write_ctx.file, msg.slab_ptr + msg.data_offset, msg.data_len); if (written == msg.data_len) { write_ctx.crc32 = crc32_ieee_update(write_ctx.crc32, msg.slab_ptr + msg.data_offset, msg.data_len); buzz_proto_buf_free(&msg.slab_ptr); write_ctx.unacked_chunks++; if (write_ctx.unacked_chunks >= ACK_WATERMARK) { uint16_t free_slabs = buzz_proto_get_free_rx_slabs(); uint16_t credits_to_send = MIN(free_slabs, write_ctx.unacked_chunks); if (credits_to_send > 0) { buzz_proto_send_ack(msg.reply_cb, credits_to_send); write_ctx.unacked_chunks -= credits_to_send; } } } else { LOG_ERR("Flash write failed!"); write_ctx.state = FS_STATE_IDLE; buzz_proto_send_error_reusing_slab(msg.reply_cb, EIO, msg.slab_ptr); } } else if (msg.op == FS_WRITE_OP_FILE_END) { fs_mgmt_pm_close(&write_ctx.file); write_ctx.state = FS_STATE_IDLE; if (write_ctx.crc32 == msg.metadata) { LOG_INF("File transfer finished. CRC valid: 0x%08X", write_ctx.crc32); buzz_proto_send_success_reusing_slab(msg.reply_cb, BUZZ_DATA_FILE_PUT, msg.slab_ptr); } else { LOG_ERR("CRC Mismatch! Expected: 0x%08X, Got: 0x%08X", msg.metadata, write_ctx.crc32); fs_mgmt_pm_unlink(write_ctx.filename); buzz_proto_send_error_reusing_slab(msg.reply_cb, EBADMSG, msg.slab_ptr); } } else if (msg.op == FS_WRITE_OP_ABORT) { fs_mgmt_pm_close(&write_ctx.file); fs_mgmt_pm_unlink(write_ctx.filename); write_ctx.state = FS_STATE_IDLE; if (msg.slab_ptr) buzz_proto_buf_free(&msg.slab_ptr); } break; case FS_STATE_RECEIVING_TAGS: if (msg.op == FS_WRITE_OP_FILE_CHUNK && msg.slab_ptr) { ssize_t written = fs_write(&write_ctx.file, msg.slab_ptr + msg.data_offset, msg.data_len); if (written == msg.data_len) { write_ctx.crc32 = crc32_ieee_update(write_ctx.crc32, msg.slab_ptr + msg.data_offset, msg.data_len); buzz_proto_buf_free(&msg.slab_ptr); write_ctx.unacked_chunks++; if (write_ctx.unacked_chunks >= ACK_WATERMARK) { uint16_t free_slabs = buzz_proto_get_free_rx_slabs(); uint16_t credits_to_send = MIN(free_slabs, write_ctx.unacked_chunks); if (credits_to_send > 0) { buzz_proto_send_ack(msg.reply_cb, credits_to_send); write_ctx.unacked_chunks -= credits_to_send; } } } else { LOG_ERR("Flash write failed during tags transfer!"); fs_truncate(&write_ctx.file, write_ctx.audio_len); /* Rollback */ fs_mgmt_pm_close(&write_ctx.file); write_ctx.state = FS_STATE_IDLE; buzz_proto_send_error_reusing_slab(msg.reply_cb, EIO, msg.slab_ptr); } } else if (msg.op == FS_WRITE_OP_FILE_END) { if (write_ctx.crc32 == msg.metadata) { LOG_INF("Tags transfer finished. CRC valid: 0x%08X", write_ctx.crc32); fs_mgmt_pm_close(&write_ctx.file); buzz_proto_send_success_reusing_slab(msg.reply_cb, BUZZ_DATA_TAGS_PUT, msg.slab_ptr); } else { LOG_ERR("Tags CRC Mismatch! Expected: 0x%08X, Got: 0x%08X", msg.metadata, write_ctx.crc32); fs_truncate(&write_ctx.file, write_ctx.audio_len); /* Rollback */ fs_mgmt_pm_close(&write_ctx.file); buzz_proto_send_error_reusing_slab(msg.reply_cb, EBADMSG, msg.slab_ptr); } write_ctx.state = FS_STATE_IDLE; } else if (msg.op == FS_WRITE_OP_ABORT) { fs_truncate(&write_ctx.file, write_ctx.audio_len); /* Rollback */ fs_mgmt_pm_close(&write_ctx.file); write_ctx.state = FS_STATE_IDLE; if (msg.slab_ptr) buzz_proto_buf_free(&msg.slab_ptr); } break; case FS_STATE_RECEIVING_FIRMWARE: break; } /* Garbage Collection: Ungültige Operationen im falschen State abfangen */ if (write_ctx.state == FS_STATE_IDLE && msg.op == FS_WRITE_OP_FILE_CHUNK && msg.slab_ptr) { buzz_proto_buf_free(&msg.slab_ptr); } } } K_THREAD_DEFINE(fs_thread, CONFIG_FS_MGMT_THREAD_STACK_SIZE, fs_thread_entry, NULL, NULL, NULL, CONFIG_FS_MGMT_THREAD_PRIORITY, 0, 0);