Files
RadioPhotonic_PCB_software/App/Services/profile_storage.c
2026-04-24 16:51:15 +03:00

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;
}