From 13aff7f03ca256ed13c18a654835da840b8dd7cf Mon Sep 17 00:00:00 2001 From: Robert Vollmer Date: Fri, 11 Oct 2024 23:02:52 +0200 Subject: [PATCH 1/3] refactor: create applyConfig() --- Firmware/LowLevel/src/main.cpp | 44 ++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/Firmware/LowLevel/src/main.cpp b/Firmware/LowLevel/src/main.cpp index 6323e350..0513b2fa 100644 --- a/Firmware/LowLevel/src/main.cpp +++ b/Firmware/LowLevel/src/main.cpp @@ -552,6 +552,29 @@ void sendConfigMessage(uint8_t pkt_type) { free(msg); } +void applyConfig(const uint8_t *buffer, size_t size) { + // This is a flexible length package where the size may vary when ll_high_level_config struct got enhanced only on one side. + // If payload size is larger than our struct size, ensure that we only copy those we know of = our struct size. + // If payload size is smaller than our struct size, copy only the payload we got, but ensure that the unsent member(s) have reasonable defaults. + size_t payload_size = min(sizeof(ll_high_level_config), size - 2); // exclude crc + + // Use a temporary config for easier sanity adaption and copy our live config, which has at least reasonable defaults. + // The live config copy ensures that we've reasonable values for the case that HL config struct is older (smaller) than ours. + auto tmp_config = llhl_config; + + // Copy payload to temporary config + memcpy(&tmp_config, buffer, payload_size); + + // Sanity + tmp_config.v_charge_cutoff = min(tmp_config.v_charge_cutoff, V_CHARGE_ABS_MAX); // Fix exceed of hardware limits + tmp_config.i_charge_cutoff = min(tmp_config.i_charge_cutoff, I_CHARGE_ABS_MAX); // Fix exceed of hardware limits + + // Make config live + llhl_config = tmp_config; + + // PR-80: Assign volume & language if not already stored in flash-config +} + void onPacketReceived(const uint8_t *buffer, size_t size) { // sanity check for CRC to work (1 type, 1 data, 2 CRC) if (size < 4) @@ -587,26 +610,7 @@ void onPacketReceived(const uint8_t *buffer, size_t size) { // copy the state last_high_level_state = *((struct ll_high_level_state *) buffer); } else if (buffer[0] == PACKET_ID_LL_HIGH_LEVEL_CONFIG_REQ || buffer[0] == PACKET_ID_LL_HIGH_LEVEL_CONFIG_RSP) { - // This is a flexible length package where the size may vary when ll_high_level_config struct got enhanced only on one side. - // If payload size is larger than our struct size, ensure that we only copy those we know of = our struct size. - // If payload size is smaller than our struct size, copy only the payload we got, but ensure that the unsent member(s) have reasonable defaults. - size_t payload_size = min(sizeof(ll_high_level_config), size - 3); // -1 type -2 crc - - // Use a temporary config for easier sanity adaption and copy our live config, which has at least reasonable defaults. - // The live config copy ensures that we've reasonable values for the case that HL config struct is older (smaller) than ours. - auto tmp_config = llhl_config; - - // Copy payload to temporary config (behind type) - memcpy(&tmp_config, buffer + 1, payload_size); - - // Sanity - tmp_config.v_charge_cutoff = min(tmp_config.v_charge_cutoff, V_CHARGE_ABS_MAX); // Fix exceed of hardware limits - tmp_config.i_charge_cutoff = min(tmp_config.i_charge_cutoff, I_CHARGE_ABS_MAX); // Fix exceed of hardware limits - - // Make config live - llhl_config = tmp_config; - - // PR-80: Assign volume & language if not already stored in flash-config + applyConfig(buffer + 1, size - 1); // Skip type if (buffer[0] == PACKET_ID_LL_HIGH_LEVEL_CONFIG_REQ) sendConfigMessage(PACKET_ID_LL_HIGH_LEVEL_CONFIG_RSP); From 1c0cfacb801e5e21bd3c39f4b6be79a5c9e25708 Mon Sep 17 00:00:00 2001 From: Robert Vollmer Date: Fri, 11 Oct 2024 23:10:30 +0200 Subject: [PATCH 2/3] PoC for LittleFS --- Firmware/LowLevel/include/nv_config.h | 84 ------------ Firmware/LowLevel/platformio.ini | 1 + Firmware/LowLevel/src/main.cpp | 50 ++++++- Firmware/LowLevel/src/nv_config.cpp | 181 -------------------------- 4 files changed, 49 insertions(+), 267 deletions(-) delete mode 100644 Firmware/LowLevel/include/nv_config.h delete mode 100644 Firmware/LowLevel/src/nv_config.cpp diff --git a/Firmware/LowLevel/include/nv_config.h b/Firmware/LowLevel/include/nv_config.h deleted file mode 100644 index 4a8f57c1..00000000 --- a/Firmware/LowLevel/include/nv_config.h +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright 2024 Jörg Ebeling (Apehaenger) - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE.* Testing/examples for reading/writing flash on the RP2040. - * - * Heavily inspired, copied from, as well as must-read: - * https://github.com/MakerMatrix/RP2040_flash_programming - */ - -#ifndef _NV_CONFIG_H -#define _NV_CONFIG_H - -#include - -#ifdef ENABLE_SOUND_MODULE -#include "soundsystem.h" -#else -#define VOLUME_DEFAULT 80 -#endif - -#define VOLUME_DEFAULT 80 // FIXME: Backported from PR-80 - -#include "../src/datatypes.h" - -#define NV_CONFIG_MAX_SAVE_INTERVAL 60000UL // Don't save more often than once a minute - -// config_bitmask. Don't mistake with LL_HIGH_LEVEL_CONFIG_BIT. Similar, but not mandatory equal! -#define NV_CONFIG_BIT_DFPIS5V 1 << 0 // DFP is set to 5V - -#define NV_RECORD_ID 0x4F4D4331 // Record struct identifier "OMC1" for future flexible length Record.config -#define NV_RECORD_ALIGNMENT (_Alignof(uint32_t)) // Ptr alignment of Record.id for quick in memory access - -namespace nv_config { -#pragma pack(push, 1) -// This is where our application values should go. -// It's possible to extended it, but any extension should add an extension related CRC so that an old/last stored Record.config isn't lost. -// (a new extension-crc isn't valid with a old Record.config. Thus the new extension values may get set i.e. with default values) -struct Config { - // Config bitmask: - // Bit 0: DFP is 5V (enable full sound). See NV_CONFIG_BIT_DFPIS5V - uint8_t config_bitmask = 0; // Don't mistake with LL_HIGH_LEVEL_CONFIG_BIT. Similar, but not mandatory equal! - uint8_t volume = VOLUME_DEFAULT; // Sound volume (0-100%) - iso639_1 language = {'e', 'n'}; // Default to 'en' - uint32_t rain_threshold = 700; // If (stock CoverUI) rain value < rain_threshold then it rains. Expected to differ between C500, SA and SC types - - /* Possible future config settings - uint16_t free; // Future config setting - uint16_t free_n; // Future config setting - uint16_t crc_n; // Future config CRC16 (for the new member) for detection if loaded (possibly old) config already has the new member */ -} __attribute__((packed)); - -// Record(s) get placed sequentially into a flash page. -// The Record structure shouldn't get changed, because it would make old flash Record's unusable. Instead of, change Record.config -struct Record { - const uint32_t id = NV_RECORD_ID; // Fixed record identifier, used to identify a possible Record within a flash page. If width get changed, change also NV_RECORD_ALIGNMENT - uint32_t num_sector_erase = 0; // For wear level stats - uint32_t num_page_write = 0; // For informational purposes - uint16_t crc; // Required to ensure that a found NV_RECORD_ID is really a Record - Config config; -} __attribute__((packed)); -#pragma pack(pop) - -Config *get(); // Returned Config pointer hold the data of the last saved Record.config, or a default one. Config member are writable, see delayedSaveChanges() -void delayedSaveChanges(); // Handle a possible changed nv_config::config member and save it to flash, but only within NV_CONFIG_MAX_SAVE_INTERVAL timeout for wear level protection - -} // namespace nv_config - -#endif // _NV_CONFIG_H \ No newline at end of file diff --git a/Firmware/LowLevel/platformio.ini b/Firmware/LowLevel/platformio.ini index 8fcaa171..4ae1fba1 100644 --- a/Firmware/LowLevel/platformio.ini +++ b/Firmware/LowLevel/platformio.ini @@ -23,6 +23,7 @@ lib_deps = Wire SPI FastCRC + LittleFS bakercp/PacketSerial@^1.4.0 powerbroker2/FireTimer@^1.0.5 https://github.com/ClemensElflein/NeoPixelConnect.git diff --git a/Firmware/LowLevel/src/main.cpp b/Firmware/LowLevel/src/main.cpp index 0513b2fa..4b5edc38 100644 --- a/Firmware/LowLevel/src/main.cpp +++ b/Firmware/LowLevel/src/main.cpp @@ -24,7 +24,7 @@ #include "ui_board.h" #include "imu.h" #include "debug.h" -#include "nv_config.h" +#include #ifdef ENABLE_SOUND_MODULE #include @@ -119,13 +119,15 @@ uint8_t ui_topic_bitmask = Topic_set_leds; // UI subscription, default to Set_LE uint16_t ui_interval = 1000; // UI send msg (LED/State) interval (ms) struct ll_high_level_config llhl_config; // LL/HL configuration (is initialized with YF-C500 defaults) -nv_config::Config *nv_cfg; // Non-volatile configuration +const String CONFIG_FILENAME = "/openmower.cfg"; void sendMessage(void *message, size_t size); void sendUIMessage(void *message, size_t size); void onPacketReceived(const uint8_t *buffer, size_t size); void onUIPacketReceived(const uint8_t *buffer, size_t size); void manageUISubscriptions(); +void saveConfigToFlash(); +void readConfigFromFlash(); void setRaspiPower(bool power) { // Update status bits in the status message @@ -418,6 +420,10 @@ void setup() { UISerial.setStream(&UI1_SERIAL); UISerial.setPacketHandler(&onUIPacketReceived); + // Initialize flash and try to read config + LittleFS.begin(); + readConfigFromFlash(); + /* * IMU INITIALIZATION */ @@ -612,6 +618,9 @@ void onPacketReceived(const uint8_t *buffer, size_t size) { } else if (buffer[0] == PACKET_ID_LL_HIGH_LEVEL_CONFIG_REQ || buffer[0] == PACKET_ID_LL_HIGH_LEVEL_CONFIG_RSP) { applyConfig(buffer + 1, size - 1); // Skip type + // Store in flash + saveConfigToFlash(); + if (buffer[0] == PACKET_ID_LL_HIGH_LEVEL_CONFIG_REQ) sendConfigMessage(PACKET_ID_LL_HIGH_LEVEL_CONFIG_RSP); } @@ -800,3 +809,40 @@ void sendUIMessage(void *message, size_t size) { UISerial.send((uint8_t *) message, size); } + +void saveConfigToFlash() { + uint16_t crc = CRC16.ccitt((const uint8_t*) &llhl_config, sizeof(llhl_config)); + // TODO: Return early if CRC is unchanged to avoid flash wear. + + File f = LittleFS.open(CONFIG_FILENAME, "w"); + f.write((const uint8_t*) &llhl_config, sizeof(llhl_config)); + f.write((const uint8_t*) &crc, 2); + f.close(); +} + +void readConfigFromFlash() { + File f = LittleFS.open(CONFIG_FILENAME, "r"); + if (!f) return; + + // sanity check for CRC to work (1 data, 2 CRC) + const size_t size = f.size(); + if (size < 3) { + f.close(); + return; + } + + // read config + uint8_t *buffer = (uint8_t *)malloc(f.size()); + f.read(buffer, size); + f.close(); + + // check the CRC + uint16_t crc = CRC16.ccitt(buffer, size - 2); + + if (buffer[size - 1] != ((crc >> 8) & 0xFF) || + buffer[size - 2] != (crc & 0xFF)) + return; + + applyConfig(buffer, size); + free(buffer); +} diff --git a/Firmware/LowLevel/src/nv_config.cpp b/Firmware/LowLevel/src/nv_config.cpp deleted file mode 100644 index 054e9381..00000000 --- a/Firmware/LowLevel/src/nv_config.cpp +++ /dev/null @@ -1,181 +0,0 @@ -/* - * Copyright 2024 Jörg Ebeling (Apehaenger) - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE.* Testing/examples for reading/writing flash on the RP2040. - * - * Heavily inspired, copied from, as well as must-read: - * https://github.com/MakerMatrix/RP2040_flash_programming - */ -#include "nv_config.h" - -#include - -#include - -#include "debug.h" - -extern "C" { -#include -#include -}; - -#define FLASH_TARGET_OFFSET (PICO_FLASH_SIZE_BYTES - FLASH_SECTOR_SIZE) // Target offset to the last sector of flash -#define NV_MIN_RECORD_SIZE (sizeof(Record) - sizeof(Config)) // Minimum Record size (Record size without Config member) -#define NV_PAGES_IN_SECTOR (FLASH_SECTOR_SIZE / FLASH_PAGE_SIZE) // How much pages fit into one sector -#define NV_RECORD_CRC_DATALEN (sizeof(Record) - sizeof(Config) - sizeof(cur_record.crc)) - -#ifdef DEBUG_PREFIX -#undef DEBUG_PREFIX -#define DEBUG_PREFIX "[NVC] " -#endif - -namespace nv_config { -static_assert(sizeof(Record) <= FLASH_PAGE_SIZE, "Record struct size larger than page size"); - -int first_empty_page = -1; // First empty page in sector -int last_used_page = -1; // Last used page with non-empty data -int last_record_pos = -1; // Last record position within page (page_buf) - -uint8_t page_buf[FLASH_PAGE_SIZE]; // Page buffer required for subsequential flash_range_program() of the same page -Record cur_record; // Current (last or (if there hasn't been a Record flashed before) default) Record -uint16_t config_crc; // CRC of current Record->config used for config change detection - -FastCRC16 CRC16; - -unsigned long next_save_millis = millis() + NV_CONFIG_MAX_SAVE_INTERVAL; - -Config *get() { - DEBUG_PRINTF("FLASH_PAGE_SIZE: %d, FLASH_SECTOR_SIZE: %d, FLASH_BLOCK_SIZE: %d, PICO_FLASH_SIZE_BYTES: %d, XIP_BASE: 0x%x, FLASH_TARGET_OFFSET: %d\n", - FLASH_PAGE_SIZE, FLASH_SECTOR_SIZE, FLASH_BLOCK_SIZE, PICO_FLASH_SIZE_BYTES, XIP_BASE, FLASH_TARGET_OFFSET); - DEBUG_PRINTF("NV_PAGES_IN_SECTOR: %d, sizeof(Record): %d, sizeof(Config): %d, possible num of records within one page: %d\n", - NV_PAGES_IN_SECTOR, sizeof(Record), sizeof(Config), FLASH_PAGE_SIZE / sizeof(Record)); - - // Read flash until first empty page found - int addr; - unsigned int page; - int32_t *p; - for (page = 0; page < FLASH_SECTOR_SIZE / FLASH_PAGE_SIZE; page++) { - p = (int32_t *)(XIP_BASE + FLASH_TARGET_OFFSET + (page * FLASH_PAGE_SIZE)); - - // 0xFFFFFFFF cast as an int is -1 so this is how we detect an empty page - DEBUG_PRINTF("Page %d (0x%x): record_id = %ld (0x%lx)\n", page, int(p), *p, *p); - if (*p == -1 && first_empty_page < 0) { - first_empty_page = page; - break; - } - last_used_page = page; - } - DEBUG_PRINTF("last_used_page %d, first_empty_page %d\n", last_used_page, first_empty_page); - - // Find last written Record - if (last_used_page >= 0) { - // RLoop over last_used_page to find latest record via it's record.id - DEBUG_PRINTF("Last possible record (unaligned) = FLASH_PAGE_SIZE %d - NV_MIN_RECORD_SIZE %d = %u\n", FLASH_PAGE_SIZE, NV_MIN_RECORD_SIZE, FLASH_PAGE_SIZE - NV_MIN_RECORD_SIZE); - int addr = XIP_BASE + FLASH_TARGET_OFFSET + (last_used_page * FLASH_PAGE_SIZE); - // Calc last possible record offset without config, for the case that Record.config get changed, and pointer-align to "next multiple of" - int last_possible_record_offset = (FLASH_PAGE_SIZE - NV_MIN_RECORD_SIZE) - ((FLASH_PAGE_SIZE - NV_MIN_RECORD_SIZE) % NV_RECORD_ALIGNMENT) + NV_RECORD_ALIGNMENT; - DEBUG_PRINTF("last_possible_record_offset (aligned) 0x%x for Record.id alignment of %d\n", last_possible_record_offset, NV_RECORD_ALIGNMENT); - // Record test_record; - for (size_t i = last_possible_record_offset; i >= 0; i = i - NV_RECORD_ALIGNMENT) { - uint32_t *p = (uint32_t *)(addr + i); - if (*p != NV_RECORD_ID) - continue; - - // Validate if the found NV_RECORD_ID is a valid Record.id - DEBUG_PRINTF("Page %d, offset 0x%x (at %p): Record.id prospect = 0x%lx\n", last_used_page, i, p, *p); - Record test_record; - memcpy(&test_record, (uint8_t *)p, sizeof(Record)); - DEBUG_PRINTF("Test- Record: num_sector_erase: %ld, num_page_write: %ld, crc: 0x%lx\n", test_record.num_sector_erase, test_record.num_page_write, test_record.crc); - uint16_t test_crc = CRC16.ccitt((uint8_t *)&test_record, NV_RECORD_CRC_DATALEN); - DEBUG_PRINTF("Test- Record CRC 0x%x of datalen %d\n", test_crc, NV_RECORD_CRC_DATALEN); - if (test_crc != test_record.crc) - continue; - - DEBUG_PRINTF("Test- Record is valid. Buffer the full flash page\n"); - memcpy(&page_buf, (uint8_t *)addr, FLASH_PAGE_SIZE); - memcpy(&cur_record, &test_record, sizeof(Record)); - last_record_pos = i; - break; - } - } - DEBUG_PRINTF("Last (or default) config's volume: %d\n", cur_record.config.volume); - - // CRC for simple checking if config has changed - config_crc = CRC16.ccitt((uint8_t *)&cur_record.config, sizeof(Config)); - DEBUG_PRINTF("cur_record.config CRC: 0x%x\n", config_crc); - - return &cur_record.config; -} - -void delayedSaveChanges() { - // Wear level protection - if (millis() < next_save_millis) - return; - next_save_millis = millis() + NV_CONFIG_MAX_SAVE_INTERVAL; - - // Check CRC if config changed - uint16_t check_crc = CRC16.ccitt((uint8_t *)&cur_record.config, sizeof(Config)); - DEBUG_PRINTF("Actual- vs. stored- config CRC: 0x%x ?= 0x%x\n", check_crc, config_crc); - if (check_crc == config_crc) - return; // Record.config doesn't changed - - // Prepare record position within page buffer - if (last_record_pos == -1) - last_record_pos = 0; - else { - last_record_pos += sizeof(Record); - last_record_pos = last_record_pos - (last_record_pos % NV_RECORD_ALIGNMENT) + NV_RECORD_ALIGNMENT; // Adjust to "next multiple of" NV_RECORD_ALIGNMENT - } - if ((last_record_pos + sizeof(Record)) > FLASH_PAGE_SIZE) { - // Will not fit into the current page anymore - last_used_page++; - last_record_pos = 0; - first_empty_page++; - std::fill_n(page_buf, FLASH_PAGE_SIZE, 0xff); // Mark page buffer as (flash) erased - } - - // If no empty page got found during get(), or last page got eaten, let's erase our flash sector - if (first_empty_page < 0 || last_used_page >= NV_PAGES_IN_SECTOR) { - DEBUG_PRINTF("Full sector, erase...\n"); - uint32_t ints = save_and_disable_interrupts(); - flash_range_erase(FLASH_TARGET_OFFSET, FLASH_SECTOR_SIZE); - restore_interrupts(ints); - // Reset positioning vars - last_used_page = 0; - first_empty_page = 0; - cur_record.num_sector_erase++; - std::fill_n(page_buf, FLASH_PAGE_SIZE, 0xff); // Mark page buffer as (flash) erased - } - - // Prepare record - cur_record.num_page_write++; - cur_record.crc = CRC16.ccitt((uint8_t *)&cur_record, NV_RECORD_CRC_DATALEN); - DEBUG_PRINTF("Record CRC 0x%x of datalen %d\n", cur_record.crc, NV_RECORD_CRC_DATALEN); - - // memcopy cur_record to page buffer - memcpy(&page_buf[last_record_pos], &cur_record, sizeof(Record)); - - // Write page to flash - DEBUG_PRINTF("Write to page %d, with new record pos %d: volume %d...\n", last_used_page, last_record_pos, cur_record.config.volume); - uint32_t ints = save_and_disable_interrupts(); - flash_range_program(FLASH_TARGET_OFFSET + (last_used_page * FLASH_PAGE_SIZE), (uint8_t *)page_buf, sizeof(page_buf)); - restore_interrupts(ints); - config_crc = check_crc; // Update last saved config CRC -} -} // namespace nv_config \ No newline at end of file From 1ff7d3181e6e85466d4faa70444a1be03dea20fa Mon Sep 17 00:00:00 2001 From: Robert Vollmer Date: Fri, 11 Oct 2024 23:33:19 +0200 Subject: [PATCH 3/3] Don't write config to flash if it hasn't changed --- Firmware/LowLevel/src/main.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Firmware/LowLevel/src/main.cpp b/Firmware/LowLevel/src/main.cpp index 4b5edc38..6d113ea9 100644 --- a/Firmware/LowLevel/src/main.cpp +++ b/Firmware/LowLevel/src/main.cpp @@ -120,6 +120,7 @@ uint16_t ui_interval = 1000; // UI send msg (LED/State) interval ( struct ll_high_level_config llhl_config; // LL/HL configuration (is initialized with YF-C500 defaults) const String CONFIG_FILENAME = "/openmower.cfg"; +uint16_t config_crc_in_flash = 0; void sendMessage(void *message, size_t size); void sendUIMessage(void *message, size_t size); @@ -812,7 +813,7 @@ void sendUIMessage(void *message, size_t size) { void saveConfigToFlash() { uint16_t crc = CRC16.ccitt((const uint8_t*) &llhl_config, sizeof(llhl_config)); - // TODO: Return early if CRC is unchanged to avoid flash wear. + if (crc == config_crc_in_flash) return; File f = LittleFS.open(CONFIG_FILENAME, "w"); f.write((const uint8_t*) &llhl_config, sizeof(llhl_config)); @@ -843,6 +844,7 @@ void readConfigFromFlash() { buffer[size - 2] != (crc & 0xFF)) return; + config_crc_in_flash = crc; applyConfig(buffer, size); free(buffer); }