#include "../Inc/settings_backup.h" #include "../Inc/settings_backup_config.h" #include #include #include /* * Архитектура переносимого автомата * --------------------------------- * 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; }