big refactoring and features added

This commit is contained in:
Ayzen
2026-04-24 16:51:15 +03:00
parent eafc328caa
commit ea1fbb071d
184 changed files with 35336 additions and 75480 deletions

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

View 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 */

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

View 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
View 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
View 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
View 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
View 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 */

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

View 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
View 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
View 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 */