Files
templates/c/settings-backup/Src/settings_backup.c

363 lines
16 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "../Inc/settings_backup.h"
#include "../Inc/settings_backup_config.h"
#include <stdarg.h>
#include <stdio.h>
#include <string.h>
/*
* Архитектура переносимого автомата
* ---------------------------------
* 1. Приложение передаёт только подтверждённый нормализованный snapshot.
* 2. Notify копирует его, поэтому lifetime исходного указателя не важен.
* 3. Canonical CRC охватывает только пользовательскую конфигурацию.
* 4. RTC и backend остаются metadata и не создают ложный content duplicate.
* 5. Mount, поиск, resolve, write и retention разнесены по main-loop проходам.
* 6. Ни один переход не содержит delay, busy wait или прямого доступа к HAL.
* 7. Ошибка файловой callback закрывает логическую попытку и ставит retry.
* 8. Первые три retry короткие; дальнейшие выполняются после cooldown.
* 9. Новая committed revision немедленно заменяет устаревший pending snapshot.
* 10. JSON строится перед write целиком и не передаётся порту усечённым.
* 11. Все числовые единицы зафиксированы отдельным объектом schema.
* 12. ROM выводится как полный uppercase HEX без locale и разделителей.
* 13. Никакие строки проекта не попадают в JSON, поэтому секреты исключены.
* 14. Автомат никогда не читает JSON и принципиально не восстанавливает Flash.
* 15. Диагностический getter не меняет phase и пригоден для Modbus polling.
*/
/* Фазы разделяют потенциально медленные FAT-операции между main-loop проходами. */
enum {
BACKUP_PHASE_MOUNT = 0,
BACKUP_PHASE_DEDUPE,
BACKUP_PHASE_RESOLVE,
BACKUP_PHASE_WRITE,
BACKUP_PHASE_RETENTION
};
/* Добавляет форматированный фрагмент и никогда не оставляет усечённый JSON. */
static uint8_t append_text(char *output, uint32_t capacity, uint32_t *used,
const char *format, ...)
{
va_list arguments;
int length;
if ((*used >= capacity) || (format == NULL)) {
return 0U;
}
va_start(arguments, format);
length = vsnprintf(output + *used, capacity - *used, format, arguments);
va_end(arguments);
if ((length < 0) || ((uint32_t)length >= (capacity - *used))) {
return 0U;
}
*used += (uint32_t)length;
return 1U;
}
/* CRC32 IEEE считается по стабильному бинарному представлению настроек. */
static uint32_t crc32_add(uint32_t crc, uint8_t value)
{
uint8_t bit;
crc ^= value;
for (bit = 0U; bit < 8U; ++bit) {
crc = ((crc & 1U) != 0U) ? (crc >> 1U) ^ 0xEDB88320UL : crc >> 1U;
}
return crc;
}
/* Время и backend исключены: hash обозначает именно пользовательские настройки. */
uint32_t SettingsBackup_CalculateContentCrc(const SettingsBackupSnapshot *snapshot)
{
uint32_t crc = 0xFFFFFFFFUL;
uint8_t index;
uint8_t byte;
if ((snapshot == NULL) || (snapshot->room_count > SETTINGS_BACKUP_MAX_ROOMS) ||
(snapshot->sensor_count > SETTINGS_BACKUP_MAX_SENSORS)) {
return 0U;
}
crc = crc32_add(crc, snapshot->room_count);
crc = crc32_add(crc, snapshot->sensor_count);
for (index = 0U; index < snapshot->room_count; ++index) {
const SettingsBackupRoom *room = &snapshot->rooms[index];
crc = crc32_add(crc, (uint8_t)room->setpoint_x10);
crc = crc32_add(crc, (uint8_t)(room->setpoint_x10 >> 8U));
crc = crc32_add(crc, room->hysteresis_x10);
crc = crc32_add(crc, room->calibration_start_pct);
crc = crc32_add(crc, (uint8_t)room->full_open_time_100ms);
crc = crc32_add(crc, (uint8_t)(room->full_open_time_100ms >> 8U));
crc = crc32_add(crc, room->target_position_pct);
crc = crc32_add(crc, room->confirmed_position_pct);
}
for (index = 0U; index < snapshot->sensor_count; ++index) {
for (byte = 0U; byte < 8U; ++byte) {
crc = crc32_add(crc, snapshot->sensors[index].rom[byte]);
}
crc = crc32_add(crc, snapshot->sensors[index].room);
}
return ~crc;
}
/* Формирует schema v1: все строки ASCII/UTF-8 и не требуют locale/float. */
int SettingsBackup_FormatJson(const SettingsBackupSnapshot *snapshot,
char *output, uint32_t capacity)
{
uint32_t used = 0U;
uint8_t index;
uint8_t byte;
if ((snapshot == NULL) || (output == NULL) || (capacity == 0U) ||
(snapshot->room_count > SETTINGS_BACKUP_MAX_ROOMS) ||
(snapshot->sensor_count > SETTINGS_BACKUP_MAX_SENSORS)) {
return -1;
}
output[0] = '\0';
if (!append_text(output, capacity, &used,
"{\n \"schema\":\"niceOne.settings\",\n \"schema_version\":1,\n"
" \"snapshot_revision\":%lu,\n \"snapshot_crc32\":\"%08lX\",\n",
(unsigned long)snapshot->revision,
(unsigned long)snapshot->content_crc32)) {
return -1;
}
if (snapshot->rtc.valid != 0U) {
if (!append_text(output, capacity, &used,
" \"rtc\":{\"valid\":true,\"datetime\":\"%04u-%02u-%02uT%02u:%02u:%02u\"},\n",
snapshot->rtc.year, snapshot->rtc.month, snapshot->rtc.date,
snapshot->rtc.hours, snapshot->rtc.minutes, snapshot->rtc.seconds)) {
return -1;
}
} else if (!append_text(output, capacity, &used,
" \"rtc\":{\"valid\":false,\"datetime\":null},\n")) {
return -1;
}
if (!append_text(output, capacity, &used,
" \"storage\":{\"requested_backend\":\"%s\",\"active_backend\":\"%s\"},\n"
" \"units\":{\"temperature\":\"0.1_degC\",\"hysteresis\":\"0.1_degC\","
"\"position\":\"percent\",\"full_open_time\":\"100_ms\"},\n \"rooms\":[\n",
snapshot->requested_backend ? "external_spi_nor" : "internal_flash",
snapshot->active_backend ? "external_spi_nor" : "internal_flash")) {
return -1;
}
for (index = 0U; index < snapshot->room_count; ++index) {
const SettingsBackupRoom *room = &snapshot->rooms[index];
if (!append_text(output, capacity, &used,
" {\"room\":%u,\"setpoint_x10\":%u,\"hysteresis_x10\":%u,"
"\"calibration\":{\"start_position_pct\":%u,\"full_open_time_100ms\":%u},"
"\"valve\":{\"target_position_pct\":%u,\"confirmed_position_pct\":%u}}%s\n",
(unsigned)(index + 1U), room->setpoint_x10, room->hysteresis_x10,
room->calibration_start_pct, room->full_open_time_100ms,
room->target_position_pct, room->confirmed_position_pct,
(index + 1U < snapshot->room_count) ? "," : "")) {
return -1;
}
}
if (!append_text(output, capacity, &used, " ],\n \"sensors\":[\n")) {
return -1;
}
for (index = 0U; index < snapshot->sensor_count; ++index) {
if (!append_text(output, capacity, &used, " {\"rom\":\"")) {
return -1;
}
for (byte = 0U; byte < 8U; ++byte) {
if (!append_text(output, capacity, &used, "%02X",
snapshot->sensors[index].rom[byte])) {
return -1;
}
}
if (!append_text(output, capacity, &used, "\",\"room\":%u}%s\n",
snapshot->sensors[index].room,
(index + 1U < snapshot->sensor_count) ? "," : "")) {
return -1;
}
}
if (!append_text(output, capacity, &used,
" ],\n \"restore_policy\":\"manual_validation_only\"\n}\n")) {
return -1;
}
return (int)used;
}
/* Проверяет инженерные диапазоны до помещения снимка в очередь. */
static uint8_t snapshot_is_valid(const SettingsBackupSnapshot *snapshot)
{
uint8_t index;
if ((snapshot == NULL) || (snapshot->revision == 0U) ||
(snapshot->room_count != SETTINGS_BACKUP_MAX_ROOMS) ||
(snapshot->sensor_count > SETTINGS_BACKUP_MAX_SENSORS)) {
return 0U;
}
for (index = 0U; index < snapshot->room_count; ++index) {
const SettingsBackupRoom *room = &snapshot->rooms[index];
if ((room->setpoint_x10 < 50U) || (room->setpoint_x10 > 400U) ||
(room->hysteresis_x10 < 1U) || (room->hysteresis_x10 > 100U) ||
(room->calibration_start_pct > 100U) ||
(room->target_position_pct > 100U) ||
(room->confirmed_position_pct > 100U) ||
(room->full_open_time_100ms < 10U) ||
(room->full_open_time_100ms > 36000U)) {
return 0U;
}
}
return 1U;
}
/* Инициализация валидирует полный набор callback, нужный атомарному протоколу. */
uint8_t SettingsBackup_Init(SettingsBackup *instance,
const SettingsBackupPort *port, void *port_context,
char *json_buffer, uint32_t json_capacity, uint16_t maximum_copies)
{
if ((instance == NULL) || (port == NULL) || (json_buffer == NULL) ||
(json_capacity < 1024U) || (port->tick_ms == NULL) ||
(port->mount == NULL) || (port->unmount == NULL) ||
(port->find_identical == NULL) || (port->resolve_paths == NULL) ||
(port->atomic_write == NULL) || (port->apply_retention == NULL)) {
return 0U;
}
memset(instance, 0, sizeof(*instance));
instance->port = *port;
instance->port_context = port_context;
instance->json_buffer = json_buffer;
instance->json_capacity = json_capacity;
instance->maximum_copies = (maximum_copies == 0U) ? 1U : maximum_copies;
instance->status = SETTINGS_BACKUP_IDLE;
return 1U;
}
/* Pending копируется целиком и получает вычисленный canonical CRC. */
uint8_t SettingsBackup_Notify(SettingsBackup *instance,
const SettingsBackupSnapshot *snapshot)
{
if ((instance == NULL) || (snapshot_is_valid(snapshot) == 0U)) {
if (instance != NULL) {
instance->status = SETTINGS_BACKUP_LAST_INVALID_SNAPSHOT;
}
return 0U;
}
instance->pending_snapshot = *snapshot;
instance->pending_snapshot.content_crc32 =
SettingsBackup_CalculateContentCrc(&instance->pending_snapshot);
instance->phase = BACKUP_PHASE_MOUNT;
instance->retry_count = 0U;
instance->pending = 1U;
instance->status = SETTINGS_BACKUP_PENDING;
return 1U;
}
/* Ошибка закрывает том и назначает ограниченный retry/cooldown без busy wait. */
static void schedule_retry(SettingsBackup *instance,
SettingsBackupPortResult result)
{
uint32_t delay_ms;
/* Порт завершает владение попыткой; общий физический том может оставить
* смонтированным, если его разделяет с независимым temperature logger. */
instance->port.unmount(instance->port_context);
instance->status = (result == SETTINGS_BACKUP_PORT_NOT_READY) ?
SETTINGS_BACKUP_LAST_NOT_READY :
(result == SETTINGS_BACKUP_PORT_NO_SPACE) ?
SETTINGS_BACKUP_LAST_NO_SPACE : SETTINGS_BACKUP_LAST_IO_ERROR;
/* Счётчик насыщать не требуется: после 255 ошибок unsigned wrap только
* вернёт одну короткую попытку и не нарушит сохранность данных. */
++instance->retry_count;
delay_ms = (instance->retry_count <= SETTINGS_BACKUP_RETRY_LIMIT) ?
SETTINGS_BACKUP_RETRY_MS : SETTINGS_BACKUP_COOLDOWN_MS;
instance->next_retry_ms = instance->port.tick_ms(instance->port_context) + delay_ms;
instance->phase = BACKUP_PHASE_MOUNT;
}
/* Автомат намеренно выполняет только одну callback-операцию на каждом проходе. */
void SettingsBackup_Service(SettingsBackup *instance)
{
SettingsBackupPortResult result;
uint8_t found = 0U;
int json_length;
uint32_t now_ms;
if ((instance == NULL) || (instance->pending == 0U)) {
return;
}
now_ms = instance->port.tick_ms(instance->port_context);
if ((instance->next_retry_ms != 0U) &&
((int32_t)(now_ms - instance->next_retry_ms) < 0)) {
return;
}
instance->next_retry_ms = 0U;
if (instance->phase == BACKUP_PHASE_MOUNT) {
/* Mount — единственная операция этой итерации; scan начнётся позже. */
result = instance->port.mount(instance->port_context);
if (result != SETTINGS_BACKUP_PORT_OK) {
schedule_retry(instance, result);
return;
}
instance->phase = BACKUP_PHASE_DEDUPE;
return;
}
if (instance->phase == BACKUP_PHASE_DEDUPE) {
/* Restart dedupe обязан выполняться до выбора нового имени файла. */
result = instance->port.find_identical(instance->port_context,
instance->pending_snapshot.revision,
instance->pending_snapshot.content_crc32, &found);
if (result != SETTINGS_BACKUP_PORT_OK) {
schedule_retry(instance, result);
return;
}
if (found != 0U) {
instance->pending = 0U;
instance->status = SETTINGS_BACKUP_LAST_OK;
instance->port.unmount(instance->port_context);
return;
}
instance->phase = BACKUP_PHASE_RESOLVE;
return;
}
if (instance->phase == BACKUP_PHASE_RESOLVE) {
/* Resolve резервирует концептуальную пару final/temp без их создания. */
result = instance->port.resolve_paths(instance->port_context,
&instance->pending_snapshot, instance->final_path,
sizeof(instance->final_path), instance->temporary_path,
sizeof(instance->temporary_path));
if (result != SETTINGS_BACKUP_PORT_OK) {
schedule_retry(instance, result);
return;
}
instance->phase = BACKUP_PHASE_WRITE;
return;
}
if (instance->phase == BACKUP_PHASE_WRITE) {
/* Форматирование RAM не касается FAT; atomicity начинается в callback. */
json_length = SettingsBackup_FormatJson(&instance->pending_snapshot,
instance->json_buffer, instance->json_capacity);
if (json_length < 0) {
schedule_retry(instance, SETTINGS_BACKUP_PORT_IO_ERROR);
return;
}
result = instance->port.atomic_write(instance->port_context,
instance->temporary_path, instance->final_path,
instance->json_buffer, (uint32_t)json_length);
if (result != SETTINGS_BACKUP_PORT_OK) {
schedule_retry(instance, result);
return;
}
instance->phase = BACKUP_PHASE_RETENTION;
return;
}
/* Retention выполняется только после успешного rename новой good copy. */
result = instance->port.apply_retention(instance->port_context,
instance->maximum_copies);
if (result != SETTINGS_BACKUP_PORT_OK) {
schedule_retry(instance, result);
return;
}
instance->pending = 0U;
instance->status = SETTINGS_BACKUP_LAST_OK;
instance->port.unmount(instance->port_context);
}
/* Getter не меняет автомат и безопасен для диагностического регистра. */
SettingsBackupStatus SettingsBackup_GetStatus(const SettingsBackup *instance)
{
return (instance == NULL) ? SETTINGS_BACKUP_LAST_IO_ERROR : instance->status;
}