big refactoring and features added
This commit is contained in:
525
App/Services/profile_repository.c
Normal file
525
App/Services/profile_repository.c
Normal file
@ -0,0 +1,525 @@
|
||||
/**
|
||||
* @file profile_repository.c
|
||||
* @brief Future SD-card profile repository with minimal standalone parsing.
|
||||
*/
|
||||
|
||||
#include "profile_repository.h"
|
||||
|
||||
#include <ctype.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
|
||||
#include "ad9102_device.h"
|
||||
#include "storage_sd.h"
|
||||
|
||||
#define PROFILE_REPOSITORY_BUFFER_SIZE 1024u
|
||||
|
||||
static char *profile_repository_trim(char *text)
|
||||
{
|
||||
char *end;
|
||||
|
||||
while ((*text != '\0') && isspace((unsigned char)*text))
|
||||
{
|
||||
++text;
|
||||
}
|
||||
|
||||
if (*text == '\0')
|
||||
{
|
||||
return text;
|
||||
}
|
||||
|
||||
end = text + strlen(text) - 1u;
|
||||
while ((end > text) && isspace((unsigned char)*end))
|
||||
{
|
||||
*end = '\0';
|
||||
--end;
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
static void profile_repository_copy_string(char *destination, size_t destination_size, const char *source)
|
||||
{
|
||||
if ((destination == NULL) || (destination_size == 0u))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (source == NULL)
|
||||
{
|
||||
destination[0] = '\0';
|
||||
return;
|
||||
}
|
||||
|
||||
strncpy(destination, source, destination_size - 1u);
|
||||
destination[destination_size - 1u] = '\0';
|
||||
}
|
||||
|
||||
static bool profile_repository_parse_bool(const char *value, bool *out_value)
|
||||
{
|
||||
if ((value == NULL) || (out_value == NULL))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((strcmp(value, "1") == 0) || (strcasecmp(value, "true") == 0) || (strcasecmp(value, "yes") == 0))
|
||||
{
|
||||
*out_value = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
if ((strcmp(value, "0") == 0) || (strcasecmp(value, "false") == 0) || (strcasecmp(value, "no") == 0))
|
||||
{
|
||||
*out_value = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static uint16_t profile_repository_parse_u16(const char *value)
|
||||
{
|
||||
return (uint16_t)strtoul(value, NULL, 0);
|
||||
}
|
||||
|
||||
static uint32_t profile_repository_parse_u32(const char *value)
|
||||
{
|
||||
return (uint32_t)strtoul(value, NULL, 0);
|
||||
}
|
||||
|
||||
static float profile_repository_parse_float(const char *value)
|
||||
{
|
||||
return strtof(value, NULL);
|
||||
}
|
||||
|
||||
static void profile_repository_reset_profile(profile_t *profile)
|
||||
{
|
||||
memset(profile, 0, sizeof(*profile));
|
||||
profile->auto_run = true;
|
||||
profile->boot_enabled = true;
|
||||
profile->waveform.mode = WAVEFORM_MODE_SAW;
|
||||
profile->waveform.enabled = 1u;
|
||||
profile->waveform.saw_step = 1u;
|
||||
profile->waveform.pat_period_base = 2u;
|
||||
profile->waveform.pat_period = 0xFFFFu;
|
||||
profile->waveform.sample_count = AD9102_SRAM_DEFAULT_SAMPLE_COUNT;
|
||||
profile->waveform.hold_cycles = AD9102_SRAM_DEFAULT_HOLD;
|
||||
profile->waveform.amplitude = AD9102_SRAM_DEFAULT_AMPLITUDE;
|
||||
profile->ds1809.apply_position = false;
|
||||
}
|
||||
|
||||
static void profile_repository_apply_key_value(profile_t *profile, const char *key, const char *value)
|
||||
{
|
||||
bool bool_value;
|
||||
|
||||
if ((profile == NULL) || (key == NULL) || (value == NULL))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (strcmp(key, "profile_name") == 0)
|
||||
{
|
||||
profile_repository_copy_string(profile->display_name, sizeof(profile->display_name), value);
|
||||
}
|
||||
else if (strcmp(key, "auto_run") == 0)
|
||||
{
|
||||
if (profile_repository_parse_bool(value, &bool_value))
|
||||
{
|
||||
profile->auto_run = bool_value;
|
||||
}
|
||||
}
|
||||
else if (strcmp(key, "boot_enabled") == 0)
|
||||
{
|
||||
if (profile_repository_parse_bool(value, &bool_value))
|
||||
{
|
||||
profile->boot_enabled = bool_value;
|
||||
}
|
||||
}
|
||||
else if (strcmp(key, "work_enable") == 0)
|
||||
{
|
||||
profile->work_config.work_enabled = (uint8_t)profile_repository_parse_u16(value);
|
||||
}
|
||||
else if (strcmp(key, "u5v1_enable") == 0)
|
||||
{
|
||||
profile->work_config.supply_5v1_enabled = (uint8_t)profile_repository_parse_u16(value);
|
||||
}
|
||||
else if (strcmp(key, "u5v2_enable") == 0)
|
||||
{
|
||||
profile->work_config.supply_5v2_enabled = (uint8_t)profile_repository_parse_u16(value);
|
||||
}
|
||||
else if (strcmp(key, "ld1_enable") == 0)
|
||||
{
|
||||
profile->work_config.laser1_enabled = (uint8_t)profile_repository_parse_u16(value);
|
||||
}
|
||||
else if (strcmp(key, "ld2_enable") == 0)
|
||||
{
|
||||
profile->work_config.laser2_enabled = (uint8_t)profile_repository_parse_u16(value);
|
||||
}
|
||||
else if (strcmp(key, "ref1_enable") == 0)
|
||||
{
|
||||
profile->work_config.reference1_enabled = (uint8_t)profile_repository_parse_u16(value);
|
||||
}
|
||||
else if (strcmp(key, "ref2_enable") == 0)
|
||||
{
|
||||
profile->work_config.reference2_enabled = (uint8_t)profile_repository_parse_u16(value);
|
||||
}
|
||||
else if (strcmp(key, "tec1_enable") == 0)
|
||||
{
|
||||
profile->work_config.tec1_enabled = (uint8_t)profile_repository_parse_u16(value);
|
||||
}
|
||||
else if (strcmp(key, "tec2_enable") == 0)
|
||||
{
|
||||
profile->work_config.tec2_enabled = (uint8_t)profile_repository_parse_u16(value);
|
||||
}
|
||||
else if (strcmp(key, "ts1_enable") == 0)
|
||||
{
|
||||
profile->work_config.temp_sensor1_enabled = (uint8_t)profile_repository_parse_u16(value);
|
||||
}
|
||||
else if (strcmp(key, "ts2_enable") == 0)
|
||||
{
|
||||
profile->work_config.temp_sensor2_enabled = (uint8_t)profile_repository_parse_u16(value);
|
||||
}
|
||||
else if (strcmp(key, "pid1_from_host") == 0)
|
||||
{
|
||||
profile->work_config.pid1_from_host = (uint8_t)profile_repository_parse_u16(value);
|
||||
}
|
||||
else if (strcmp(key, "pid2_from_host") == 0)
|
||||
{
|
||||
profile->work_config.pid2_from_host = (uint8_t)profile_repository_parse_u16(value);
|
||||
}
|
||||
else if (strcmp(key, "averages") == 0)
|
||||
{
|
||||
profile->work_config.averages = profile_repository_parse_u16(value);
|
||||
}
|
||||
else if (strcmp(key, "message_id") == 0)
|
||||
{
|
||||
profile->work_config.message_id = profile_repository_parse_u16(value);
|
||||
}
|
||||
else if (strcmp(key, "laser1_target_temp") == 0)
|
||||
{
|
||||
profile->laser_channels[0].target_temperature_raw = profile_repository_parse_u16(value);
|
||||
}
|
||||
else if (strcmp(key, "laser2_target_temp") == 0)
|
||||
{
|
||||
profile->laser_channels[1].target_temperature_raw = profile_repository_parse_u16(value);
|
||||
}
|
||||
else if (strcmp(key, "laser1_current") == 0)
|
||||
{
|
||||
profile->laser_channels[0].current_raw = profile_repository_parse_u16(value);
|
||||
}
|
||||
else if (strcmp(key, "laser2_current") == 0)
|
||||
{
|
||||
profile->laser_channels[1].current_raw = profile_repository_parse_u16(value);
|
||||
}
|
||||
else if (strcmp(key, "laser1_pid_p") == 0)
|
||||
{
|
||||
profile->laser_channels[0].pid_p = profile_repository_parse_float(value);
|
||||
}
|
||||
else if (strcmp(key, "laser1_pid_i") == 0)
|
||||
{
|
||||
profile->laser_channels[0].pid_i = profile_repository_parse_float(value);
|
||||
}
|
||||
else if (strcmp(key, "laser2_pid_p") == 0)
|
||||
{
|
||||
profile->laser_channels[1].pid_p = profile_repository_parse_float(value);
|
||||
}
|
||||
else if (strcmp(key, "laser2_pid_i") == 0)
|
||||
{
|
||||
profile->laser_channels[1].pid_i = profile_repository_parse_float(value);
|
||||
}
|
||||
else if (strcmp(key, "waveform_mode") == 0)
|
||||
{
|
||||
if (strcasecmp(value, "saw") == 0)
|
||||
{
|
||||
profile->waveform.mode = WAVEFORM_MODE_SAW;
|
||||
}
|
||||
else if (strcasecmp(value, "generated_sram") == 0)
|
||||
{
|
||||
profile->waveform.mode = WAVEFORM_MODE_SRAM_GENERATED;
|
||||
}
|
||||
else if (strcasecmp(value, "custom_sram") == 0)
|
||||
{
|
||||
profile->waveform.mode = WAVEFORM_MODE_SRAM_CUSTOM;
|
||||
}
|
||||
}
|
||||
else if (strcmp(key, "waveform_enable") == 0)
|
||||
{
|
||||
profile->waveform.enabled = (uint8_t)profile_repository_parse_u16(value);
|
||||
}
|
||||
else if (strcmp(key, "waveform_triangle") == 0)
|
||||
{
|
||||
profile->waveform.triangle = (uint8_t)profile_repository_parse_u16(value);
|
||||
}
|
||||
else if (strcmp(key, "waveform_saw_step") == 0)
|
||||
{
|
||||
profile->waveform.saw_step = (uint8_t)profile_repository_parse_u16(value);
|
||||
}
|
||||
else if (strcmp(key, "waveform_pat_base") == 0)
|
||||
{
|
||||
profile->waveform.pat_period_base = (uint8_t)profile_repository_parse_u16(value);
|
||||
}
|
||||
else if (strcmp(key, "waveform_pat_period") == 0)
|
||||
{
|
||||
profile->waveform.pat_period = profile_repository_parse_u16(value);
|
||||
}
|
||||
else if (strcmp(key, "waveform_sample_count") == 0)
|
||||
{
|
||||
profile->waveform.sample_count = profile_repository_parse_u16(value);
|
||||
}
|
||||
else if (strcmp(key, "waveform_hold_cycles") == 0)
|
||||
{
|
||||
profile->waveform.hold_cycles = (uint8_t)profile_repository_parse_u16(value);
|
||||
}
|
||||
else if (strcmp(key, "waveform_amplitude") == 0)
|
||||
{
|
||||
profile->waveform.amplitude = profile_repository_parse_u16(value);
|
||||
}
|
||||
else if (strcmp(key, "waveform_source") == 0)
|
||||
{
|
||||
profile_repository_copy_string(profile->waveform.source_path, sizeof(profile->waveform.source_path), value);
|
||||
}
|
||||
else if (strcmp(key, "ad9833_enable") == 0)
|
||||
{
|
||||
profile->ad9833.enabled = (uint8_t)profile_repository_parse_u16(value);
|
||||
}
|
||||
else if (strcmp(key, "ad9833_triangle") == 0)
|
||||
{
|
||||
profile->ad9833.triangle = (uint8_t)profile_repository_parse_u16(value);
|
||||
}
|
||||
else if (strcmp(key, "ad9833_frequency_word") == 0)
|
||||
{
|
||||
profile->ad9833.frequency_word = profile_repository_parse_u32(value);
|
||||
}
|
||||
else if (strcmp(key, "stm32_dac_enable") == 0)
|
||||
{
|
||||
profile->stm32_dac.enabled = (uint8_t)profile_repository_parse_u16(value);
|
||||
}
|
||||
else if (strcmp(key, "stm32_dac_code") == 0)
|
||||
{
|
||||
profile->stm32_dac.code = profile_repository_parse_u16(value);
|
||||
}
|
||||
else if (strcmp(key, "ds1809_apply") == 0)
|
||||
{
|
||||
if (profile_repository_parse_bool(value, &bool_value))
|
||||
{
|
||||
profile->ds1809.apply_position = bool_value;
|
||||
}
|
||||
}
|
||||
else if (strcmp(key, "ds1809_position_from_min") == 0)
|
||||
{
|
||||
profile->ds1809.position_from_min = (uint8_t)profile_repository_parse_u16(value);
|
||||
}
|
||||
}
|
||||
|
||||
static bool profile_repository_load_ini_file(const char *profile_path, profile_t *profile)
|
||||
{
|
||||
char buffer[PROFILE_REPOSITORY_BUFFER_SIZE];
|
||||
UINT bytes_read = 0u;
|
||||
char *cursor;
|
||||
FRESULT result;
|
||||
|
||||
result = storage_sd_read_bytes(profile_path, 0u, buffer, sizeof(buffer) - 1u, &bytes_read);
|
||||
if (result != FR_OK)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
buffer[bytes_read] = '\0';
|
||||
cursor = buffer;
|
||||
|
||||
while (*cursor != '\0')
|
||||
{
|
||||
char *line_start = cursor;
|
||||
char *line_end = strpbrk(cursor, "\r\n");
|
||||
char *separator;
|
||||
char *key;
|
||||
char *value;
|
||||
|
||||
if (line_end != NULL)
|
||||
{
|
||||
*line_end = '\0';
|
||||
cursor = line_end + 1;
|
||||
while ((*cursor == '\r') || (*cursor == '\n'))
|
||||
{
|
||||
++cursor;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
cursor += strlen(cursor);
|
||||
}
|
||||
|
||||
line_start = profile_repository_trim(line_start);
|
||||
if ((*line_start == '\0') || (*line_start == '#') || (*line_start == ';'))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
separator = strchr(line_start, '=');
|
||||
if (separator == NULL)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
*separator = '\0';
|
||||
key = profile_repository_trim(line_start);
|
||||
value = profile_repository_trim(separator + 1);
|
||||
profile_repository_apply_key_value(profile, key, value);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool profile_repository_load_by_valid_line_index(uint16_t target_index,
|
||||
profile_t *out_profile,
|
||||
uint16_t *out_total_valid_lines)
|
||||
{
|
||||
char buffer[PROFILE_REPOSITORY_BUFFER_SIZE];
|
||||
UINT bytes_read = 0u;
|
||||
char *cursor;
|
||||
uint16_t valid_line_index = 0u;
|
||||
FRESULT result;
|
||||
|
||||
if ((out_profile == NULL) || !storage_sd_file_exists(APP_STORAGE_PROFILE_INDEX_FILE))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
result = storage_sd_read_bytes(APP_STORAGE_PROFILE_INDEX_FILE, 0u, buffer, sizeof(buffer) - 1u, &bytes_read);
|
||||
if (result != FR_OK)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
buffer[bytes_read] = '\0';
|
||||
cursor = buffer;
|
||||
|
||||
while (*cursor != '\0')
|
||||
{
|
||||
char *line_start = cursor;
|
||||
char *line_end = strpbrk(cursor, "\r\n");
|
||||
char *display_name;
|
||||
char *profile_path;
|
||||
char *waveform_path;
|
||||
char *separator_1;
|
||||
char *separator_2;
|
||||
|
||||
if (line_end != NULL)
|
||||
{
|
||||
*line_end = '\0';
|
||||
cursor = line_end + 1;
|
||||
while ((*cursor == '\r') || (*cursor == '\n'))
|
||||
{
|
||||
++cursor;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
cursor += strlen(cursor);
|
||||
}
|
||||
|
||||
line_start = profile_repository_trim(line_start);
|
||||
if ((*line_start == '\0') || (*line_start == '#') || (*line_start == ';'))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
separator_1 = strchr(line_start, ',');
|
||||
if (separator_1 == NULL)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
*separator_1 = '\0';
|
||||
separator_2 = strchr(separator_1 + 1, ',');
|
||||
if (separator_2 != NULL)
|
||||
{
|
||||
*separator_2 = '\0';
|
||||
}
|
||||
|
||||
display_name = profile_repository_trim(line_start);
|
||||
profile_path = profile_repository_trim(separator_1 + 1);
|
||||
waveform_path = (separator_2 != NULL) ? profile_repository_trim(separator_2 + 1) : "";
|
||||
|
||||
if (*profile_path == '\0')
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (valid_line_index == target_index)
|
||||
{
|
||||
profile_repository_reset_profile(out_profile);
|
||||
profile_repository_copy_string(out_profile->display_name, sizeof(out_profile->display_name), display_name);
|
||||
profile_repository_copy_string(out_profile->profile_path, sizeof(out_profile->profile_path), profile_path);
|
||||
profile_repository_copy_string(out_profile->waveform_path, sizeof(out_profile->waveform_path), waveform_path);
|
||||
profile_repository_copy_string(out_profile->waveform.source_path,
|
||||
sizeof(out_profile->waveform.source_path),
|
||||
waveform_path);
|
||||
|
||||
if (!profile_repository_load_ini_file(profile_path, out_profile))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (out_total_valid_lines != NULL)
|
||||
{
|
||||
*out_total_valid_lines = (uint16_t)(valid_line_index + 1u);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
++valid_line_index;
|
||||
}
|
||||
|
||||
if (out_total_valid_lines != NULL)
|
||||
{
|
||||
*out_total_valid_lines = valid_line_index;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool profile_repository_load_first(profile_t *out_profile, uint16_t *out_index)
|
||||
{
|
||||
uint16_t total_lines = 0u;
|
||||
|
||||
if (!profile_repository_load_by_valid_line_index(0u, out_profile, &total_lines))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (out_index != NULL)
|
||||
{
|
||||
*out_index = 0u;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool profile_repository_load_next(uint16_t *in_out_index, profile_t *out_profile)
|
||||
{
|
||||
uint16_t requested_index;
|
||||
uint16_t total_lines = 0u;
|
||||
|
||||
if ((in_out_index == NULL) || (out_profile == NULL))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
requested_index = (uint16_t)(*in_out_index + 1u);
|
||||
if (profile_repository_load_by_valid_line_index(requested_index, out_profile, &total_lines))
|
||||
{
|
||||
*in_out_index = requested_index;
|
||||
return true;
|
||||
}
|
||||
|
||||
if ((total_lines == 0u) || !profile_repository_load_by_valid_line_index(0u, out_profile, NULL))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
*in_out_index = 0u;
|
||||
return true;
|
||||
}
|
||||
36
App/Services/profile_repository.h
Normal file
36
App/Services/profile_repository.h
Normal file
@ -0,0 +1,36 @@
|
||||
/**
|
||||
* @file profile_repository.h
|
||||
* @brief Future SD-card profile repository with minimal standalone parsing.
|
||||
*/
|
||||
|
||||
#ifndef PROFILE_REPOSITORY_H
|
||||
#define PROFILE_REPOSITORY_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "app_types.h"
|
||||
|
||||
/**
|
||||
* @brief Load the first valid profile listed in `profiles.csv`.
|
||||
*
|
||||
* @param out_profile Destination profile object.
|
||||
* @param out_index Receives the zero-based valid profile index.
|
||||
*
|
||||
* @retval true A valid profile was loaded.
|
||||
* @retval false No valid profile entry was available.
|
||||
*/
|
||||
bool profile_repository_load_first(profile_t *out_profile, uint16_t *out_index);
|
||||
|
||||
/**
|
||||
* @brief Load the next valid profile, wrapping around to the first entry.
|
||||
*
|
||||
* @param in_out_index Current profile index on input, next index on success.
|
||||
* @param out_profile Destination profile object.
|
||||
*
|
||||
* @retval true The next valid profile was loaded.
|
||||
* @retval false No valid profile entry was available.
|
||||
*/
|
||||
bool profile_repository_load_next(uint16_t *in_out_index, profile_t *out_profile);
|
||||
|
||||
#endif /* PROFILE_REPOSITORY_H */
|
||||
497
App/Services/profile_storage.c
Normal file
497
App/Services/profile_storage.c
Normal file
@ -0,0 +1,497 @@
|
||||
/**
|
||||
* @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;
|
||||
}
|
||||
50
App/Services/profile_storage.h
Normal file
50
App/Services/profile_storage.h
Normal file
@ -0,0 +1,50 @@
|
||||
/**
|
||||
* @file profile_storage.h
|
||||
* @brief Streamed SD-card writer for GUI-initiated profile saves.
|
||||
*
|
||||
* Architectural note:
|
||||
* The desktop GUI already knows the current human-facing configuration values.
|
||||
* It serialises them into the same INI/CSV text format that standalone boot
|
||||
* understands, while this service owns the SD-card side: validating the
|
||||
* profile name, generating 8.3-compatible file names, streaming incoming text
|
||||
* into files, and updating `profiles.csv` only after the full upload succeeds.
|
||||
*/
|
||||
|
||||
#ifndef PROFILE_STORAGE_H
|
||||
#define PROFILE_STORAGE_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/**
|
||||
* @brief Result codes returned by the streamed profile-save service.
|
||||
*/
|
||||
typedef enum profile_storage_status_t {
|
||||
PROFILE_STORAGE_STATUS_OK = 0,
|
||||
PROFILE_STORAGE_STATUS_INVALID_ARGUMENT,
|
||||
PROFILE_STORAGE_STATUS_NAME_INVALID,
|
||||
PROFILE_STORAGE_STATUS_STORAGE_UNAVAILABLE,
|
||||
PROFILE_STORAGE_STATUS_SESSION_ACTIVE,
|
||||
PROFILE_STORAGE_STATUS_NO_ACTIVE_SESSION,
|
||||
PROFILE_STORAGE_STATUS_DIRECTORY_ERROR,
|
||||
PROFILE_STORAGE_STATUS_FILE_NAME_EXHAUSTED,
|
||||
PROFILE_STORAGE_STATUS_FILE_OPEN_ERROR,
|
||||
PROFILE_STORAGE_STATUS_WRITE_ERROR,
|
||||
PROFILE_STORAGE_STATUS_INDEX_UPDATE_ERROR,
|
||||
PROFILE_STORAGE_STATUS_SIZE_MISMATCH,
|
||||
PROFILE_STORAGE_STATUS_SECTION_ERROR
|
||||
} profile_storage_status_t;
|
||||
|
||||
void profile_storage_init(void);
|
||||
bool profile_storage_is_active(void);
|
||||
profile_storage_status_t profile_storage_get_last_status(void);
|
||||
profile_storage_status_t profile_storage_begin(const char *display_name,
|
||||
uint16_t profile_text_bytes,
|
||||
uint16_t waveform_text_bytes);
|
||||
profile_storage_status_t profile_storage_write_chunk(uint16_t section_id,
|
||||
const uint8_t *data,
|
||||
uint16_t data_size);
|
||||
profile_storage_status_t profile_storage_commit(void);
|
||||
void profile_storage_cancel(void);
|
||||
|
||||
#endif /* PROFILE_STORAGE_H */
|
||||
214
App/Services/storage_sd.c
Normal file
214
App/Services/storage_sd.c
Normal file
@ -0,0 +1,214 @@
|
||||
/**
|
||||
* @file storage_sd.c
|
||||
* @brief Narrow SD-card storage wrapper built directly on FatFs.
|
||||
*/
|
||||
|
||||
#include "storage_sd.h"
|
||||
|
||||
#include "fatfs.h"
|
||||
|
||||
#include "board_io.h"
|
||||
|
||||
static uint8_t g_storage_mounted = 0u;
|
||||
static uint16_t g_storage_mount_depth = 0u;
|
||||
|
||||
bool storage_sd_is_available(void)
|
||||
{
|
||||
return board_io_is_sd_card_present();
|
||||
}
|
||||
|
||||
FRESULT storage_sd_begin(void)
|
||||
{
|
||||
FRESULT result;
|
||||
|
||||
if (!storage_sd_is_available())
|
||||
{
|
||||
return FR_NOT_READY;
|
||||
}
|
||||
|
||||
if (g_storage_mount_depth > 0u)
|
||||
{
|
||||
++g_storage_mount_depth;
|
||||
return FR_OK;
|
||||
}
|
||||
|
||||
result = f_mount(&SDFatFS, SDPath, 1u);
|
||||
if (result == FR_OK)
|
||||
{
|
||||
g_storage_mounted = 1u;
|
||||
g_storage_mount_depth = 1u;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
FRESULT storage_sd_end(void)
|
||||
{
|
||||
FRESULT result;
|
||||
|
||||
if (g_storage_mount_depth == 0u)
|
||||
{
|
||||
return FR_OK;
|
||||
}
|
||||
|
||||
--g_storage_mount_depth;
|
||||
if (g_storage_mount_depth > 0u)
|
||||
{
|
||||
return FR_OK;
|
||||
}
|
||||
|
||||
result = f_mount(NULL, SDPath, 1u);
|
||||
if (result == FR_OK)
|
||||
{
|
||||
g_storage_mounted = 0u;
|
||||
}
|
||||
else
|
||||
{
|
||||
g_storage_mount_depth = 1u;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
FRESULT storage_sd_open_file(FIL *file, const char *path, BYTE mode)
|
||||
{
|
||||
FRESULT result;
|
||||
|
||||
if ((file == NULL) || (path == NULL))
|
||||
{
|
||||
return FR_INVALID_PARAMETER;
|
||||
}
|
||||
|
||||
result = storage_sd_begin();
|
||||
if (result != FR_OK)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
result = f_open(file, path, mode);
|
||||
if (result != FR_OK)
|
||||
{
|
||||
(void)storage_sd_end();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
FRESULT storage_sd_close_file(FIL *file)
|
||||
{
|
||||
FRESULT close_result = FR_OK;
|
||||
FRESULT end_result;
|
||||
|
||||
if (file != NULL)
|
||||
{
|
||||
close_result = f_close(file);
|
||||
}
|
||||
|
||||
end_result = storage_sd_end();
|
||||
if (close_result != FR_OK)
|
||||
{
|
||||
return close_result;
|
||||
}
|
||||
|
||||
return end_result;
|
||||
}
|
||||
|
||||
FRESULT storage_sd_read_bytes(const char *path, FSIZE_t offset, void *data, UINT size, UINT *bytes_read)
|
||||
{
|
||||
FIL file;
|
||||
UINT local_bytes_read = 0u;
|
||||
FRESULT result = storage_sd_open_file(&file, path, FA_READ);
|
||||
|
||||
if (result != FR_OK)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
result = f_lseek(&file, offset);
|
||||
if (result == FR_OK)
|
||||
{
|
||||
result = f_read(&file, data, size, &local_bytes_read);
|
||||
}
|
||||
(void)storage_sd_close_file(&file);
|
||||
|
||||
if (bytes_read != NULL)
|
||||
{
|
||||
*bytes_read = local_bytes_read;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
FRESULT storage_sd_stat(const char *path, FILINFO *out_info)
|
||||
{
|
||||
FRESULT result;
|
||||
|
||||
if (path == NULL)
|
||||
{
|
||||
return FR_INVALID_PARAMETER;
|
||||
}
|
||||
|
||||
result = storage_sd_begin();
|
||||
if (result != FR_OK)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
result = f_stat(path, out_info);
|
||||
(void)storage_sd_end();
|
||||
return result;
|
||||
}
|
||||
|
||||
bool storage_sd_file_exists(const char *path)
|
||||
{
|
||||
FILINFO info;
|
||||
return storage_sd_stat(path, &info) == FR_OK;
|
||||
}
|
||||
|
||||
FRESULT storage_sd_make_directory(const char *path)
|
||||
{
|
||||
FRESULT result;
|
||||
|
||||
if (path == NULL)
|
||||
{
|
||||
return FR_INVALID_PARAMETER;
|
||||
}
|
||||
|
||||
result = storage_sd_begin();
|
||||
if (result != FR_OK)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
result = f_mkdir(path);
|
||||
if (result == FR_EXIST)
|
||||
{
|
||||
result = FR_OK;
|
||||
}
|
||||
(void)storage_sd_end();
|
||||
return result;
|
||||
}
|
||||
|
||||
FRESULT storage_sd_remove_file(const char *path)
|
||||
{
|
||||
FRESULT result;
|
||||
|
||||
if (path == NULL)
|
||||
{
|
||||
return FR_INVALID_PARAMETER;
|
||||
}
|
||||
|
||||
result = storage_sd_begin();
|
||||
if (result != FR_OK)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
result = f_unlink(path);
|
||||
if ((result == FR_NO_FILE) || (result == FR_NO_PATH))
|
||||
{
|
||||
result = FR_OK;
|
||||
}
|
||||
(void)storage_sd_end();
|
||||
return result;
|
||||
}
|
||||
30
App/Services/storage_sd.h
Normal file
30
App/Services/storage_sd.h
Normal file
@ -0,0 +1,30 @@
|
||||
/**
|
||||
* @file storage_sd.h
|
||||
* @brief Narrow SD-card storage wrapper built directly on FatFs.
|
||||
*
|
||||
* Architectural note:
|
||||
* An earlier storage layer mixed allocation-heavy tutorial helpers with
|
||||
* application-specific policies. This service now exposes only the minimal
|
||||
* file-oriented operations needed by profile loading and streamed profile
|
||||
* saving, while keeping FatFs details out of higher-level services.
|
||||
*/
|
||||
|
||||
#ifndef STORAGE_SD_H
|
||||
#define STORAGE_SD_H
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
#include "ff.h"
|
||||
|
||||
bool storage_sd_is_available(void);
|
||||
FRESULT storage_sd_begin(void);
|
||||
FRESULT storage_sd_end(void);
|
||||
FRESULT storage_sd_read_bytes(const char *path, FSIZE_t offset, void *data, UINT size, UINT *bytes_read);
|
||||
FRESULT storage_sd_open_file(FIL *file, const char *path, BYTE mode);
|
||||
FRESULT storage_sd_close_file(FIL *file);
|
||||
FRESULT storage_sd_stat(const char *path, FILINFO *out_info);
|
||||
bool storage_sd_file_exists(const char *path);
|
||||
FRESULT storage_sd_make_directory(const char *path);
|
||||
FRESULT storage_sd_remove_file(const char *path);
|
||||
|
||||
#endif /* STORAGE_SD_H */
|
||||
87
App/Services/telemetry.c
Normal file
87
App/Services/telemetry.c
Normal file
@ -0,0 +1,87 @@
|
||||
/**
|
||||
* @file telemetry.c
|
||||
* @brief Telemetry-frame creation, checksum finalisation, and serialisation.
|
||||
*/
|
||||
|
||||
#include "telemetry.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include "app_uart_protocol.h"
|
||||
|
||||
void telemetry_reset(telemetry_frame_t *frame)
|
||||
{
|
||||
if (frame == NULL)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
memset(frame->words, 0, sizeof(frame->words));
|
||||
frame->words[0] = APP_PACKET_HEADER_WORK_CONFIG;
|
||||
}
|
||||
|
||||
void telemetry_set_message_id(telemetry_frame_t *frame, uint16_t message_id)
|
||||
{
|
||||
if (frame != NULL)
|
||||
{
|
||||
frame->words[13] = message_id;
|
||||
}
|
||||
}
|
||||
|
||||
void telemetry_set_live_data(telemetry_frame_t *frame,
|
||||
uint16_t laser1_power,
|
||||
uint16_t laser2_power,
|
||||
uint32_t tick_10ms,
|
||||
uint16_t laser1_temperature,
|
||||
uint16_t laser2_temperature,
|
||||
uint16_t adc_slot_7,
|
||||
uint16_t adc_slot_8,
|
||||
uint16_t adc_slot_9,
|
||||
uint16_t adc_slot_10,
|
||||
uint16_t adc_slot_11,
|
||||
uint16_t adc_slot_12)
|
||||
{
|
||||
if (frame == NULL)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
frame->words[1] = laser1_power;
|
||||
frame->words[2] = laser2_power;
|
||||
frame->words[3] = (uint16_t)(tick_10ms & 0xFFFFu);
|
||||
frame->words[4] = (uint16_t)((tick_10ms >> 16) & 0xFFFFu);
|
||||
frame->words[5] = laser1_temperature;
|
||||
frame->words[6] = laser2_temperature;
|
||||
frame->words[7] = adc_slot_7;
|
||||
frame->words[8] = adc_slot_8;
|
||||
frame->words[9] = adc_slot_9;
|
||||
frame->words[10] = adc_slot_10;
|
||||
frame->words[11] = adc_slot_11;
|
||||
frame->words[12] = adc_slot_12;
|
||||
}
|
||||
|
||||
void telemetry_finalize(telemetry_frame_t *frame)
|
||||
{
|
||||
if (frame == NULL)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
frame->words[14] = app_protocol_calculate_checksum(&frame->words[1], 13u);
|
||||
}
|
||||
|
||||
void telemetry_to_bytes(const telemetry_frame_t *frame, uint8_t *out_bytes)
|
||||
{
|
||||
uint8_t index;
|
||||
|
||||
if ((frame == NULL) || (out_bytes == NULL))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (index = 0u; index < APP_TELEMETRY_WORD_COUNT; ++index)
|
||||
{
|
||||
out_bytes[index * 2u] = (uint8_t)(frame->words[index] & 0x00FFu);
|
||||
out_bytes[(index * 2u) + 1u] = (uint8_t)((frame->words[index] >> 8) & 0x00FFu);
|
||||
}
|
||||
}
|
||||
30
App/Services/telemetry.h
Normal file
30
App/Services/telemetry.h
Normal file
@ -0,0 +1,30 @@
|
||||
/**
|
||||
* @file telemetry.h
|
||||
* @brief Telemetry-frame creation, checksum finalisation, and serialisation.
|
||||
*/
|
||||
|
||||
#ifndef TELEMETRY_H
|
||||
#define TELEMETRY_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "app_types.h"
|
||||
|
||||
void telemetry_reset(telemetry_frame_t *frame);
|
||||
void telemetry_set_message_id(telemetry_frame_t *frame, uint16_t message_id);
|
||||
void telemetry_set_live_data(telemetry_frame_t *frame,
|
||||
uint16_t laser1_power,
|
||||
uint16_t laser2_power,
|
||||
uint32_t tick_10ms,
|
||||
uint16_t laser1_temperature,
|
||||
uint16_t laser2_temperature,
|
||||
uint16_t adc_slot_7,
|
||||
uint16_t adc_slot_8,
|
||||
uint16_t adc_slot_9,
|
||||
uint16_t adc_slot_10,
|
||||
uint16_t adc_slot_11,
|
||||
uint16_t adc_slot_12);
|
||||
void telemetry_finalize(telemetry_frame_t *frame);
|
||||
void telemetry_to_bytes(const telemetry_frame_t *frame, uint8_t *out_bytes);
|
||||
|
||||
#endif /* TELEMETRY_H */
|
||||
67
App/Services/temperature_control.c
Normal file
67
App/Services/temperature_control.c
Normal file
@ -0,0 +1,67 @@
|
||||
/**
|
||||
* @file temperature_control.c
|
||||
* @brief Temperature-control services for the laser TEC loops.
|
||||
*/
|
||||
|
||||
#include "temperature_control.h"
|
||||
|
||||
uint16_t temperature_control_compute_pid(const laser_channel_config_t *channel_config,
|
||||
laser_runtime_t *runtime_state,
|
||||
uint8_t channel_index,
|
||||
uint32_t current_tick_1ms,
|
||||
uint32_t *shared_pid_reference_tick)
|
||||
{
|
||||
int32_t error;
|
||||
float proportional_gain;
|
||||
float integral_term;
|
||||
int32_t output;
|
||||
|
||||
if ((channel_config == NULL) || (runtime_state == NULL) || (shared_pid_reference_tick == NULL))
|
||||
{
|
||||
return 32768u;
|
||||
}
|
||||
|
||||
error = (int32_t)runtime_state->current_temperature_raw - (int32_t)channel_config->target_temperature_raw;
|
||||
integral_term = runtime_state->integral_error;
|
||||
|
||||
if ((error < 3000) && (error > -3000))
|
||||
{
|
||||
integral_term += channel_config->pid_i *
|
||||
(float)error *
|
||||
(float)(current_tick_1ms - *shared_pid_reference_tick) /
|
||||
100.0f;
|
||||
}
|
||||
|
||||
proportional_gain = channel_config->pid_p;
|
||||
|
||||
if (integral_term > 32000.0f)
|
||||
{
|
||||
integral_term = 32000.0f;
|
||||
}
|
||||
else if (integral_term < -32000.0f)
|
||||
{
|
||||
integral_term = -32000.0f;
|
||||
}
|
||||
|
||||
runtime_state->integral_error = integral_term;
|
||||
|
||||
output = 32768 + (int32_t)(proportional_gain * (float)error) + (int32_t)integral_term;
|
||||
|
||||
if (output < 1000)
|
||||
{
|
||||
output = 8800;
|
||||
}
|
||||
else if (output > 56800)
|
||||
{
|
||||
output = 56800;
|
||||
}
|
||||
|
||||
/* Both PID channels use a shared timing reference updated after the
|
||||
* second TEC computation. This preserves the original controller timing. */
|
||||
if (channel_index == 2u)
|
||||
{
|
||||
*shared_pid_reference_tick = current_tick_1ms;
|
||||
}
|
||||
|
||||
return (uint16_t)output;
|
||||
}
|
||||
19
App/Services/temperature_control.h
Normal file
19
App/Services/temperature_control.h
Normal file
@ -0,0 +1,19 @@
|
||||
/**
|
||||
* @file temperature_control.h
|
||||
* @brief Temperature-control services for the laser TEC loops.
|
||||
*/
|
||||
|
||||
#ifndef TEMPERATURE_CONTROL_H
|
||||
#define TEMPERATURE_CONTROL_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "app_types.h"
|
||||
|
||||
uint16_t temperature_control_compute_pid(const laser_channel_config_t *channel_config,
|
||||
laser_runtime_t *runtime_state,
|
||||
uint8_t channel_index,
|
||||
uint32_t current_tick_1ms,
|
||||
uint32_t *shared_pid_reference_tick);
|
||||
|
||||
#endif /* TEMPERATURE_CONTROL_H */
|
||||
237
App/Services/ui_status.c
Normal file
237
App/Services/ui_status.c
Normal file
@ -0,0 +1,237 @@
|
||||
/**
|
||||
* @file ui_status.c
|
||||
* @brief Standalone LCD/button frontend for profile-driven operation.
|
||||
*
|
||||
* Architectural note:
|
||||
* This service owns all user-facing status formatting and button debouncing.
|
||||
* The application core pushes state into this module, while this module
|
||||
* returns high-level events such as "select next profile".
|
||||
*/
|
||||
|
||||
#include "ui_status.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "board_io.h"
|
||||
#include "lcd1602_display.h"
|
||||
|
||||
#define UI_STATUS_LCD_COLUMNS 16u
|
||||
#define UI_STATUS_BUTTON_DEBOUNCE_TICKS_10MS 3u
|
||||
|
||||
typedef struct ui_button_state_t {
|
||||
bool raw_pressed;
|
||||
bool stable_pressed;
|
||||
uint32_t last_change_tick_10ms;
|
||||
} ui_button_state_t;
|
||||
|
||||
static char g_profile_name[APP_PROFILE_NAME_LENGTH];
|
||||
static app_mode_t g_mode = APP_MODE_IDLE;
|
||||
static uint8_t g_error_flags = 0u;
|
||||
static uint8_t g_initialised = 0u;
|
||||
static uint8_t g_display_dirty = 0u;
|
||||
static ui_button_state_t g_button = {false, false, 0u};
|
||||
|
||||
static void ui_status_mark_dirty(void);
|
||||
static void ui_status_copy_padded(char *destination, const char *source);
|
||||
static const char *ui_status_mode_name(app_mode_t mode);
|
||||
static void ui_status_build_line_1(char *line_buffer);
|
||||
static void ui_status_build_line_2(char *line_buffer);
|
||||
static void ui_status_refresh_display(void);
|
||||
|
||||
void ui_status_init(void)
|
||||
{
|
||||
bool raw_pressed;
|
||||
|
||||
memset(g_profile_name, 0, sizeof(g_profile_name));
|
||||
g_mode = APP_MODE_IDLE;
|
||||
g_error_flags = 0u;
|
||||
|
||||
board_io_init_standalone_ui();
|
||||
raw_pressed = board_io_is_standalone_ui_button_pressed();
|
||||
|
||||
/*
|
||||
* Synchronise the debounce state with the physical input level at boot.
|
||||
* This prevents a stuck-low or already-held button from generating a
|
||||
* synthetic "next profile" event immediately after startup.
|
||||
*/
|
||||
g_button.raw_pressed = raw_pressed;
|
||||
g_button.stable_pressed = raw_pressed;
|
||||
g_button.last_change_tick_10ms = 0u;
|
||||
|
||||
lcd1602_display_init();
|
||||
|
||||
g_initialised = 1u;
|
||||
g_display_dirty = 1u;
|
||||
ui_status_refresh_display();
|
||||
}
|
||||
|
||||
void ui_status_set_profile_name(const char *profile_name)
|
||||
{
|
||||
char next_name[APP_PROFILE_NAME_LENGTH] = {0};
|
||||
|
||||
if (profile_name != NULL)
|
||||
{
|
||||
strncpy(next_name, profile_name, sizeof(next_name) - 1u);
|
||||
}
|
||||
|
||||
if (strncmp(g_profile_name, next_name, sizeof(g_profile_name)) != 0)
|
||||
{
|
||||
memcpy(g_profile_name, next_name, sizeof(g_profile_name));
|
||||
ui_status_mark_dirty();
|
||||
}
|
||||
}
|
||||
|
||||
void ui_status_set_mode(app_mode_t mode)
|
||||
{
|
||||
/*
|
||||
* TX_CURRENT is a transient transport state rather than a user-visible
|
||||
* operating mode. Ignoring it prevents needless LCD churn during polling.
|
||||
*/
|
||||
if (mode == APP_MODE_TX_CURRENT)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (g_mode != mode)
|
||||
{
|
||||
g_mode = mode;
|
||||
ui_status_mark_dirty();
|
||||
}
|
||||
}
|
||||
|
||||
void ui_status_set_error(uint8_t error_flags)
|
||||
{
|
||||
if (g_error_flags != error_flags)
|
||||
{
|
||||
g_error_flags = error_flags;
|
||||
ui_status_mark_dirty();
|
||||
}
|
||||
}
|
||||
|
||||
app_event_t ui_status_poll_event(uint32_t tick_10ms)
|
||||
{
|
||||
bool raw_pressed = board_io_is_standalone_ui_button_pressed();
|
||||
app_event_t event = APP_EVENT_NONE;
|
||||
|
||||
if (raw_pressed != g_button.raw_pressed)
|
||||
{
|
||||
g_button.raw_pressed = raw_pressed;
|
||||
g_button.last_change_tick_10ms = tick_10ms;
|
||||
}
|
||||
else if ((tick_10ms - g_button.last_change_tick_10ms) >= UI_STATUS_BUTTON_DEBOUNCE_TICKS_10MS)
|
||||
{
|
||||
if (g_button.stable_pressed != g_button.raw_pressed)
|
||||
{
|
||||
g_button.stable_pressed = g_button.raw_pressed;
|
||||
if (g_button.stable_pressed)
|
||||
{
|
||||
event = APP_EVENT_PROFILE_NEXT;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (g_display_dirty != 0u)
|
||||
{
|
||||
ui_status_refresh_display();
|
||||
}
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
static void ui_status_mark_dirty(void)
|
||||
{
|
||||
g_display_dirty = 1u;
|
||||
}
|
||||
|
||||
static void ui_status_copy_padded(char *destination, const char *source)
|
||||
{
|
||||
size_t index;
|
||||
|
||||
for (index = 0u; index < UI_STATUS_LCD_COLUMNS; ++index)
|
||||
{
|
||||
destination[index] = ' ';
|
||||
}
|
||||
destination[UI_STATUS_LCD_COLUMNS] = '\0';
|
||||
|
||||
if (source == NULL)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (index = 0u; (index < UI_STATUS_LCD_COLUMNS) && (source[index] != '\0'); ++index)
|
||||
{
|
||||
destination[index] = source[index];
|
||||
}
|
||||
}
|
||||
|
||||
static const char *ui_status_mode_name(app_mode_t mode)
|
||||
{
|
||||
switch (mode)
|
||||
{
|
||||
case APP_MODE_WORK:
|
||||
return "WORK";
|
||||
|
||||
case APP_MODE_ERROR:
|
||||
return "ERROR";
|
||||
|
||||
case APP_MODE_IDLE:
|
||||
default:
|
||||
return "IDLE";
|
||||
}
|
||||
}
|
||||
|
||||
static void ui_status_build_line_1(char *line_buffer)
|
||||
{
|
||||
const char *label = g_profile_name;
|
||||
|
||||
if (line_buffer == NULL)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (label[0] == '\0')
|
||||
{
|
||||
label = (g_mode == APP_MODE_WORK) ? "Manual control" : "No profile";
|
||||
}
|
||||
|
||||
ui_status_copy_padded(line_buffer, label);
|
||||
}
|
||||
|
||||
static void ui_status_build_line_2(char *line_buffer)
|
||||
{
|
||||
char status_text[UI_STATUS_LCD_COLUMNS + 1u];
|
||||
|
||||
if (line_buffer == NULL)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (g_error_flags == 0u)
|
||||
{
|
||||
(void)snprintf(status_text, sizeof(status_text), "%s OK", ui_status_mode_name(g_mode));
|
||||
}
|
||||
else
|
||||
{
|
||||
(void)snprintf(status_text, sizeof(status_text), "%s ERR %02X", ui_status_mode_name(g_mode), g_error_flags);
|
||||
}
|
||||
|
||||
ui_status_copy_padded(line_buffer, status_text);
|
||||
}
|
||||
|
||||
static void ui_status_refresh_display(void)
|
||||
{
|
||||
char line_1[UI_STATUS_LCD_COLUMNS + 1u];
|
||||
char line_2[UI_STATUS_LCD_COLUMNS + 1u];
|
||||
|
||||
if (g_initialised == 0u)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ui_status_build_line_1(line_1);
|
||||
ui_status_build_line_2(line_2);
|
||||
lcd1602_display_set_lines(line_1, line_2);
|
||||
g_display_dirty = 0u;
|
||||
}
|
||||
19
App/Services/ui_status.h
Normal file
19
App/Services/ui_status.h
Normal file
@ -0,0 +1,19 @@
|
||||
/**
|
||||
* @file ui_status.h
|
||||
* @brief Future LCD/UI abstraction used by standalone profile mode.
|
||||
*/
|
||||
|
||||
#ifndef UI_STATUS_H
|
||||
#define UI_STATUS_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "app_types.h"
|
||||
|
||||
void ui_status_init(void);
|
||||
void ui_status_set_profile_name(const char *profile_name);
|
||||
void ui_status_set_mode(app_mode_t mode);
|
||||
void ui_status_set_error(uint8_t error_flags);
|
||||
app_event_t ui_status_poll_event(uint32_t tick_10ms);
|
||||
|
||||
#endif /* UI_STATUS_H */
|
||||
Reference in New Issue
Block a user