498 lines
16 KiB
C
498 lines
16 KiB
C
/**
|
|
* @file profile_storage.c
|
|
* @brief Streamed SD-card writer for GUI-initiated profile saves.
|
|
*/
|
|
|
|
#include "profile_storage.h"
|
|
|
|
#include <ctype.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
#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;
|
|
}
|