/** * @file profile_storage.c * @brief Streamed SD-card writer for GUI-initiated profile saves. */ #include "profile_storage.h" #include #include #include #include "app_types.h" #include "storage_sd.h" #define PROFILE_STORAGE_PROFILES_DIR "profiles" #define PROFILE_STORAGE_WAVES_DIR "waves" #define PROFILE_STORAGE_PROFILE_FILE_TEMPLATE "profiles/PROF%04u.INI" #define PROFILE_STORAGE_WAVE_FILE_TEMPLATE "waves/WAVE%04u.CSV" #define PROFILE_STORAGE_MAX_FILE_INDEX 9999u #define PROFILE_STORAGE_INDEX_LINE_BUFFER_SIZE 192u typedef struct profile_storage_context_t { bool active; bool profile_file_open; bool waveform_file_open; char display_name[APP_PROFILE_NAME_LENGTH]; char profile_path[APP_PROFILE_PATH_LENGTH]; char waveform_path[APP_PROFILE_PATH_LENGTH]; uint16_t profile_expected_bytes; uint16_t waveform_expected_bytes; uint16_t profile_received_bytes; uint16_t waveform_received_bytes; FIL profile_file; FIL waveform_file; profile_storage_status_t last_status; } profile_storage_context_t; static profile_storage_context_t g_profile_storage; static profile_storage_status_t profile_storage_set_status(profile_storage_status_t status) { g_profile_storage.last_status = status; return status; } static void profile_storage_reset_context(void) { memset(&g_profile_storage, 0, sizeof(g_profile_storage)); g_profile_storage.last_status = PROFILE_STORAGE_STATUS_OK; } static bool profile_storage_is_name_character_allowed(char character) { return (isalnum((unsigned char)character) != 0) || (character == ' ') || (character == '-') || (character == '_'); } static bool profile_storage_copy_validated_name(char *destination, size_t destination_size, const char *source) { const char *start; const char *end; size_t length; size_t index; if ((destination == NULL) || (destination_size < APP_PROFILE_NAME_LENGTH) || (source == NULL)) { return false; } start = source; while ((*start != '\0') && isspace((unsigned char)*start)) { ++start; } end = start + strlen(start); while ((end > start) && isspace((unsigned char)end[-1])) { --end; } length = (size_t)(end - start); if ((length == 0u) || (length > APP_PROFILE_NAME_MAX_CHARACTERS)) { return false; } for (index = 0u; index < length; ++index) { if (!profile_storage_is_name_character_allowed(start[index])) { return false; } } memcpy(destination, start, length); destination[length] = '\0'; return true; } static void profile_storage_clear_session_paths(void) { g_profile_storage.display_name[0] = '\0'; g_profile_storage.profile_path[0] = '\0'; g_profile_storage.waveform_path[0] = '\0'; } static void profile_storage_close_open_files(void) { if (g_profile_storage.waveform_file_open) { (void)storage_sd_close_file(&g_profile_storage.waveform_file); g_profile_storage.waveform_file_open = false; } if (g_profile_storage.profile_file_open) { (void)storage_sd_close_file(&g_profile_storage.profile_file); g_profile_storage.profile_file_open = false; } } static void profile_storage_remove_partial_files(void) { if (g_profile_storage.waveform_path[0] != '\0') { (void)storage_sd_remove_file(g_profile_storage.waveform_path); } if (g_profile_storage.profile_path[0] != '\0') { (void)storage_sd_remove_file(g_profile_storage.profile_path); } } static void profile_storage_abort_session(void) { profile_storage_close_open_files(); profile_storage_remove_partial_files(); g_profile_storage.active = false; g_profile_storage.profile_expected_bytes = 0u; g_profile_storage.waveform_expected_bytes = 0u; g_profile_storage.profile_received_bytes = 0u; g_profile_storage.waveform_received_bytes = 0u; profile_storage_clear_session_paths(); } static profile_storage_status_t profile_storage_format_target_path(char *destination, size_t destination_size, const char *format_string, uint16_t file_index) { int written; written = snprintf(destination, destination_size, format_string, (unsigned int)file_index); if ((written <= 0) || ((size_t)written >= destination_size)) { return profile_storage_set_status(PROFILE_STORAGE_STATUS_FILE_NAME_EXHAUSTED); } return PROFILE_STORAGE_STATUS_OK; } static profile_storage_status_t profile_storage_find_free_paths(uint16_t waveform_text_bytes) { uint16_t file_index; for (file_index = 1u; file_index <= PROFILE_STORAGE_MAX_FILE_INDEX; ++file_index) { FILINFO file_info; FRESULT result; profile_storage_status_t status; status = profile_storage_format_target_path(g_profile_storage.profile_path, sizeof(g_profile_storage.profile_path), PROFILE_STORAGE_PROFILE_FILE_TEMPLATE, file_index); if (status != PROFILE_STORAGE_STATUS_OK) { return status; } result = storage_sd_stat(g_profile_storage.profile_path, &file_info); if (result == FR_OK) { continue; } if ((result != FR_NO_FILE) && (result != FR_NO_PATH)) { return profile_storage_set_status(PROFILE_STORAGE_STATUS_STORAGE_UNAVAILABLE); } if (waveform_text_bytes > 0u) { status = profile_storage_format_target_path(g_profile_storage.waveform_path, sizeof(g_profile_storage.waveform_path), PROFILE_STORAGE_WAVE_FILE_TEMPLATE, file_index); if (status != PROFILE_STORAGE_STATUS_OK) { return status; } result = storage_sd_stat(g_profile_storage.waveform_path, &file_info); if (result == FR_OK) { continue; } if ((result != FR_NO_FILE) && (result != FR_NO_PATH)) { return profile_storage_set_status(PROFILE_STORAGE_STATUS_STORAGE_UNAVAILABLE); } } else { g_profile_storage.waveform_path[0] = '\0'; } return profile_storage_set_status(PROFILE_STORAGE_STATUS_OK); } return profile_storage_set_status(PROFILE_STORAGE_STATUS_FILE_NAME_EXHAUSTED); } static profile_storage_status_t profile_storage_open_target_files(void) { FRESULT result; result = storage_sd_make_directory(PROFILE_STORAGE_PROFILES_DIR); if (result != FR_OK) { return profile_storage_set_status(PROFILE_STORAGE_STATUS_DIRECTORY_ERROR); } if (g_profile_storage.waveform_expected_bytes > 0u) { result = storage_sd_make_directory(PROFILE_STORAGE_WAVES_DIR); if (result != FR_OK) { return profile_storage_set_status(PROFILE_STORAGE_STATUS_DIRECTORY_ERROR); } } result = storage_sd_open_file(&g_profile_storage.profile_file, g_profile_storage.profile_path, FA_CREATE_ALWAYS | FA_WRITE); if (result != FR_OK) { return profile_storage_set_status(PROFILE_STORAGE_STATUS_FILE_OPEN_ERROR); } g_profile_storage.profile_file_open = true; if (g_profile_storage.waveform_expected_bytes > 0u) { result = storage_sd_open_file(&g_profile_storage.waveform_file, g_profile_storage.waveform_path, FA_CREATE_ALWAYS | FA_WRITE); if (result != FR_OK) { profile_storage_abort_session(); return profile_storage_set_status(PROFILE_STORAGE_STATUS_FILE_OPEN_ERROR); } g_profile_storage.waveform_file_open = true; } return profile_storage_set_status(PROFILE_STORAGE_STATUS_OK); } static profile_storage_status_t profile_storage_write_to_file(FIL *file, uint16_t *received_bytes, uint16_t expected_bytes, const uint8_t *data, uint16_t data_size) { UINT bytes_written = 0u; FRESULT result; if ((file == NULL) || (received_bytes == NULL) || (data == NULL) || (data_size == 0u)) { return profile_storage_set_status(PROFILE_STORAGE_STATUS_INVALID_ARGUMENT); } if ((uint32_t)(*received_bytes) + (uint32_t)data_size > (uint32_t)expected_bytes) { profile_storage_abort_session(); return profile_storage_set_status(PROFILE_STORAGE_STATUS_SIZE_MISMATCH); } result = f_write(file, data, data_size, &bytes_written); if ((result != FR_OK) || (bytes_written != data_size)) { profile_storage_abort_session(); return profile_storage_set_status(PROFILE_STORAGE_STATUS_WRITE_ERROR); } *received_bytes = (uint16_t)(*received_bytes + data_size); return profile_storage_set_status(PROFILE_STORAGE_STATUS_OK); } static profile_storage_status_t profile_storage_append_index_entry(void) { char line_buffer[PROFILE_STORAGE_INDEX_LINE_BUFFER_SIZE]; FIL index_file; UINT bytes_written = 0u; FRESULT result; int line_length; line_length = snprintf(line_buffer, sizeof(line_buffer), "%s,%s,%s\r\n", g_profile_storage.display_name, g_profile_storage.profile_path, g_profile_storage.waveform_path); if ((line_length <= 0) || ((size_t)line_length >= sizeof(line_buffer))) { return profile_storage_set_status(PROFILE_STORAGE_STATUS_INDEX_UPDATE_ERROR); } result = storage_sd_open_file(&index_file, APP_STORAGE_PROFILE_INDEX_FILE, FA_OPEN_ALWAYS | FA_WRITE); if (result != FR_OK) { return profile_storage_set_status(PROFILE_STORAGE_STATUS_INDEX_UPDATE_ERROR); } result = f_lseek(&index_file, f_size(&index_file)); if (result == FR_OK) { result = f_write(&index_file, line_buffer, (UINT)line_length, &bytes_written); } if (result == FR_OK) { result = f_sync(&index_file); } (void)storage_sd_close_file(&index_file); if ((result != FR_OK) || (bytes_written != (UINT)line_length)) { return profile_storage_set_status(PROFILE_STORAGE_STATUS_INDEX_UPDATE_ERROR); } return profile_storage_set_status(PROFILE_STORAGE_STATUS_OK); } void profile_storage_init(void) { profile_storage_reset_context(); } bool profile_storage_is_active(void) { return g_profile_storage.active; } profile_storage_status_t profile_storage_get_last_status(void) { return g_profile_storage.last_status; } profile_storage_status_t profile_storage_begin(const char *display_name, uint16_t profile_text_bytes, uint16_t waveform_text_bytes) { if (profile_storage_is_active()) { return profile_storage_set_status(PROFILE_STORAGE_STATUS_SESSION_ACTIVE); } profile_storage_reset_context(); if (!storage_sd_is_available()) { return profile_storage_set_status(PROFILE_STORAGE_STATUS_STORAGE_UNAVAILABLE); } if ((profile_text_bytes == 0u) || !profile_storage_copy_validated_name(g_profile_storage.display_name, sizeof(g_profile_storage.display_name), display_name)) { return profile_storage_set_status(PROFILE_STORAGE_STATUS_NAME_INVALID); } g_profile_storage.profile_expected_bytes = profile_text_bytes; g_profile_storage.waveform_expected_bytes = waveform_text_bytes; g_profile_storage.active = true; if (profile_storage_find_free_paths(waveform_text_bytes) != PROFILE_STORAGE_STATUS_OK) { g_profile_storage.active = false; return g_profile_storage.last_status; } if (profile_storage_open_target_files() != PROFILE_STORAGE_STATUS_OK) { g_profile_storage.active = false; profile_storage_clear_session_paths(); return g_profile_storage.last_status; } return profile_storage_set_status(PROFILE_STORAGE_STATUS_OK); } profile_storage_status_t profile_storage_write_chunk(uint16_t section_id, const uint8_t *data, uint16_t data_size) { if (!g_profile_storage.active) { return profile_storage_set_status(PROFILE_STORAGE_STATUS_NO_ACTIVE_SESSION); } if ((data == NULL) || (data_size == 0u) || (data_size > APP_PROFILE_SAVE_MAX_DATA_BYTES_PER_PACKET)) { profile_storage_abort_session(); return profile_storage_set_status(PROFILE_STORAGE_STATUS_INVALID_ARGUMENT); } if (section_id == APP_PROFILE_SAVE_SECTION_PROFILE_TEXT) { return profile_storage_write_to_file(&g_profile_storage.profile_file, &g_profile_storage.profile_received_bytes, g_profile_storage.profile_expected_bytes, data, data_size); } if ((section_id == APP_PROFILE_SAVE_SECTION_WAVEFORM_TEXT) && g_profile_storage.waveform_file_open) { return profile_storage_write_to_file(&g_profile_storage.waveform_file, &g_profile_storage.waveform_received_bytes, g_profile_storage.waveform_expected_bytes, data, data_size); } profile_storage_abort_session(); return profile_storage_set_status(PROFILE_STORAGE_STATUS_SECTION_ERROR); } profile_storage_status_t profile_storage_commit(void) { if (!g_profile_storage.active) { return profile_storage_set_status(PROFILE_STORAGE_STATUS_NO_ACTIVE_SESSION); } if ((g_profile_storage.profile_received_bytes != g_profile_storage.profile_expected_bytes) || (g_profile_storage.waveform_received_bytes != g_profile_storage.waveform_expected_bytes)) { profile_storage_abort_session(); return profile_storage_set_status(PROFILE_STORAGE_STATUS_SIZE_MISMATCH); } if (g_profile_storage.profile_file_open && (f_sync(&g_profile_storage.profile_file) != FR_OK)) { profile_storage_abort_session(); return profile_storage_set_status(PROFILE_STORAGE_STATUS_WRITE_ERROR); } if (g_profile_storage.waveform_file_open && (f_sync(&g_profile_storage.waveform_file) != FR_OK)) { profile_storage_abort_session(); return profile_storage_set_status(PROFILE_STORAGE_STATUS_WRITE_ERROR); } profile_storage_close_open_files(); if (profile_storage_append_index_entry() != PROFILE_STORAGE_STATUS_OK) { profile_storage_remove_partial_files(); g_profile_storage.active = false; profile_storage_clear_session_paths(); return g_profile_storage.last_status; } g_profile_storage.active = false; g_profile_storage.profile_expected_bytes = 0u; g_profile_storage.waveform_expected_bytes = 0u; g_profile_storage.profile_received_bytes = 0u; g_profile_storage.waveform_received_bytes = 0u; profile_storage_clear_session_paths(); return profile_storage_set_status(PROFILE_STORAGE_STATUS_OK); } void profile_storage_cancel(void) { if (g_profile_storage.active) { profile_storage_abort_session(); } g_profile_storage.last_status = PROFILE_STORAGE_STATUS_OK; }