/** * @file config.cpp * @brief EEPROM handling and configuration storage for the ChainLube firmware. * * Responsibilities: * - Bring-up of the external I²C EEPROM * - Robust availability checks with optional bus recovery * - Central processing of EEPROM requests (save/load/format/move page) * - CRC32 utilities and debug dump helpers * * Design notes: * - The device boots with sane in-RAM defaults so the system stays operable * even when EEPROM is missing. Actual lube execution is gated by DTCs elsewhere. * - The DTC DTC_NO_EEPROM_FOUND is set/cleared only in EEPROM_Process(), never here ad-hoc. * - Background recovery is non-blocking and driven by millis(). */ #include #include #include "config.h" #include "debugger.h" #include "globals.h" // Recovery edge flag: set when availability changes 0 -> 1 static bool eeRecoveredOnce = false; // Non-blocking retry scheduling static uint32_t eeNextTryMs = 0; static uint32_t eeRetryIntervalMs = 2000; // ms between background attempts // I²C EEPROM instance I2C_eeprom ee(0x50, EEPROM_SIZE_BYTES); // Configuration and persistence data LubeConfig_t LubeConfig; persistenceData_t PersistenceData; // EEPROM structure version (bumped when layout changes) const uint16_t eeVersion = EEPROM_STRUCTURE_REVISION; // Latched availability flag static bool eeAvailable = false; // EEPROM layout offsets const uint16_t startofLubeConfig = 16; const uint16_t startofPersistence = 16 + sizeof(LubeConfig) + (sizeof(LubeConfig) % 16); // availability probe bool EEPROM_Available(bool recover = false, uint8_t attempts = 3, uint16_t delay_ms = 25); // Robust EEPROM handling (internal helpers) void I2C_BusReset(); bool TryRecoverEEPROM(uint8_t attempts = 5, uint16_t delay_ms = 50); /** * @brief Initialize I²C and EEPROM driver, load in-RAM defaults. * * Loads defaults into RAM to keep the application operational. * Availability is checked but no DTC is set here—EEPROM_Process() is the single place * that sets/clears DTC_NO_EEPROM_FOUND. */ void InitEEPROM() { LubeConfig = LubeConfig_defaults; PersistenceData = {0}; Wire.begin(); ee.begin(); eeAvailable = ee.isConnected(); } /** * @brief Try to free a stuck I²C bus and enforce a STOP condition. * * Pulses SCL up to 9 times to release a held SDA, then issues a STOP (SDA ↑ while SCL ↑). * Finally returns control to Wire. */ void I2C_BusReset() { pinMode(SCL, OUTPUT_OPEN_DRAIN); pinMode(SDA, INPUT_PULLUP); for (int i = 0; i < 9; i++) { digitalWrite(SCL, LOW); delayMicroseconds(5); digitalWrite(SCL, HIGH); delayMicroseconds(5); } pinMode(SDA, OUTPUT_OPEN_DRAIN); digitalWrite(SDA, LOW); delayMicroseconds(5); digitalWrite(SCL, HIGH); delayMicroseconds(5); digitalWrite(SDA, HIGH); delayMicroseconds(5); pinMode(SCL, INPUT); pinMode(SDA, INPUT); } /** * @brief Attempt to recover EEPROM connectivity. * * Sequence per attempt: * - I²C bus reset * - Wire.begin() * - ee.begin() * - short settle delay * * On first successful probe (0->1) the eeRecoveredOnce flag is raised. * * @param attempts Number of attempts * @param delay_ms Delay between attempts (after ee.begin()) * @return true if EEPROM is reachable after recovery, false otherwise */ bool TryRecoverEEPROM(uint8_t attempts, uint16_t delay_ms) { for (uint8_t n = 0; n < attempts; n++) { I2C_BusReset(); // ESP8266 core: Wire.end() is not available; re-begin is sufficient Wire.begin(); delay(2); ee.begin(); delay(delay_ms); if (ee.isConnected()) { if (!eeAvailable) eeRecoveredOnce = true; // edge 0 -> 1 eeAvailable = true; return true; } } eeAvailable = false; return false; } /** * @brief Central EEPROM task: background recovery, DTC handling, and request dispatch. * * Called periodically from the main loop. Non-blocking by design. * - Schedules gentle recovery tries based on millis() * - Sets DTC_NO_EEPROM_FOUND when unavailable * - On successful recovery edge, clears DTC and reloads CFG/PDS exactly once * - Executes requested actions (save/load/format/move) */ void EEPROM_Process() { // Background recovery (single soft attempt per interval) const uint32_t now = millis(); if (!EEPROM_Available() && now >= eeNextTryMs) { (void)TryRecoverEEPROM(1, 10); eeNextTryMs = now + eeRetryIntervalMs; } // Central DTC handling if (!EEPROM_Available()) { MaintainDTC(DTC_NO_EEPROM_FOUND, true); } // Recovery edge: clear DTC and reload persisted data exactly once if (EEPROM_Available() && eeRecoveredOnce) { MaintainDTC(DTC_NO_EEPROM_FOUND, false); GetConfig_EEPROM(); GetPersistence_EEPROM(); eeRecoveredOnce = false; Debug_pushMessage("EEPROM recovered – reloaded CFG/PDS\n"); } // Request dispatcher switch (globals.requestEEAction) { case EE_CFG_SAVE: StoreConfig_EEPROM(); globals.requestEEAction = EE_IDLE; Debug_pushMessage("Stored EEPROM CFG\n"); break; case EE_CFG_LOAD: GetConfig_EEPROM(); globals.requestEEAction = EE_IDLE; Debug_pushMessage("Loaded EEPROM CFG\n"); break; case EE_CFG_FORMAT: FormatConfig_EEPROM(); globals.requestEEAction = EE_IDLE; GetConfig_EEPROM(); Debug_pushMessage("Formatted EEPROM CFG\n"); break; case EE_PDS_SAVE: StorePersistence_EEPROM(); globals.requestEEAction = EE_IDLE; Debug_pushMessage("Stored EEPROM PDS\n"); break; case EE_PDS_LOAD: GetPersistence_EEPROM(); globals.requestEEAction = EE_IDLE; Debug_pushMessage("Loaded EEPROM PDS\n"); break; case EE_PDS_FORMAT: FormatPersistence_EEPROM(); globals.requestEEAction = EE_IDLE; GetPersistence_EEPROM(); Debug_pushMessage("Formatted EEPROM PDS\n"); break; case EE_FORMAT_ALL: FormatConfig_EEPROM(); FormatPersistence_EEPROM(); GetConfig_EEPROM(); GetPersistence_EEPROM(); globals.requestEEAction = EE_IDLE; Debug_pushMessage("Formatted EEPROM ALL\n"); break; case EE_ALL_SAVE: StorePersistence_EEPROM(); StoreConfig_EEPROM(); globals.requestEEAction = EE_IDLE; Debug_pushMessage("Stored EEPROM ALL\n"); break; case EE_REINITIALIZE: { // quick burst of attempts const bool ok = TryRecoverEEPROM(5, 20); if (ok) { // Edge & reload are handled by the block above Debug_pushMessage("EEPROM reinitialize OK\n"); } else { MaintainDTC(DTC_NO_EEPROM_FOUND, true); Debug_pushMessage("EEPROM reinitialize FAILED\n"); } globals.requestEEAction = EE_IDLE; break; } case EE_IDLE: default: globals.requestEEAction = EE_IDLE; break; } } /** * @brief Store configuration to EEPROM (with CRC and sanity report). * * Writes only if EEPROM is available. On completion, DTC_EEPROM_CFG_SANITY is * raised if any config fields are out of plausible bounds (bitmask payload). */ void StoreConfig_EEPROM() { LubeConfig.checksum = 0; LubeConfig.checksum = Checksum_EEPROM((uint8_t *)&LubeConfig, sizeof(LubeConfig)); if (!EEPROM_Available()) return; ee.updateBlock(startofLubeConfig, (uint8_t *)&LubeConfig, sizeof(LubeConfig)); const uint32_t sanity = ConfigSanityCheck(false); if (sanity > 0) { MaintainDTC(DTC_EEPROM_CFG_SANITY, true, sanity); } } /** * @brief Load configuration from EEPROM and validate. * * On CRC failure: raise DTC_EEPROM_CFG_BAD and fall back to in-RAM defaults (no writes). * On CRC OK: run sanity with autocorrect=true and raise DTC_EEPROM_CFG_SANITY with bitmask if needed. */ void GetConfig_EEPROM() { if (!EEPROM_Available()) return; ee.readBlock(startofLubeConfig, (uint8_t *)&LubeConfig, sizeof(LubeConfig)); const uint32_t checksum = LubeConfig.checksum; LubeConfig.checksum = 0; const bool badCrc = (Checksum_EEPROM((uint8_t *)&LubeConfig, sizeof(LubeConfig)) != checksum); MaintainDTC(DTC_EEPROM_CFG_BAD, badCrc); if (badCrc) { // Don’t keep corrupted data in RAM LubeConfig = LubeConfig_defaults; LubeConfig.EEPROM_Version = EEPROM_STRUCTURE_REVISION; // explicit in-RAM version return; } // CRC OK → restore checksum and sanitize (with autocorrect) LubeConfig.checksum = checksum; const uint32_t sanity = ConfigSanityCheck(true); MaintainDTC(DTC_EEPROM_CFG_SANITY, (sanity > 0), sanity); } /** * @brief Store persistence record to EEPROM (wear-levelled page). * * Increments the write-cycle counter and moves the page if close to the limit. * Writes only if EEPROM is available. */ void StorePersistence_EEPROM() { if (PersistenceData.writeCycleCounter >= 0xFFF0) MovePersistencePage_EEPROM(false); else PersistenceData.writeCycleCounter++; PersistenceData.checksum = 0; PersistenceData.checksum = Checksum_EEPROM((uint8_t *)&PersistenceData, sizeof(PersistenceData)); if (!EEPROM_Available()) return; ee.updateBlock(globals.eePersistenceAddress, (uint8_t *)&PersistenceData, sizeof(PersistenceData)); } /** * @brief Load persistence record, validating address range and CRC. * * If the stored start address is out of range, the persistence partition is reset, * formatted, and DTC_EEPROM_PDSADRESS_BAD is raised. * Otherwise, the record is read and checked; on CRC failure DTC_EEPROM_PDS_BAD is raised * and the in-RAM persistence data is reset to a safe default (no writes performed here). */ void GetPersistence_EEPROM() { if (!EEPROM_Available()) return; // Read wear-level start address ee.readBlock(0, (uint8_t *)&globals.eePersistenceAddress, sizeof(globals.eePersistenceAddress)); const uint16_t addr = globals.eePersistenceAddress; const uint16_t need = sizeof(PersistenceData); const uint16_t dev = ee.getDeviceSize(); // Strict range check: addr must be within partition and block must fit into device if (addr < startofPersistence || (uint32_t)addr + (uint32_t)need > (uint32_t)dev) { MovePersistencePage_EEPROM(true); FormatPersistence_EEPROM(); MaintainDTC(DTC_EEPROM_PDSADRESS_BAD, true); return; } // Safe to read the record ee.readBlock(addr, (uint8_t *)&PersistenceData, sizeof(PersistenceData)); const uint32_t checksum = PersistenceData.checksum; PersistenceData.checksum = 0; const bool badCrc = (Checksum_EEPROM((uint8_t *)&PersistenceData, sizeof(PersistenceData)) != checksum); MaintainDTC(DTC_EEPROM_PDS_BAD, badCrc); if (badCrc) { // Do not keep corrupted data in RAM; leave DTC set, no EEPROM writes here PersistenceData = {0}; return; } // CRC ok -> restore checksum into the struct kept in RAM PersistenceData.checksum = checksum; } /** * @brief Reset the configuration partition to defaults and write it. */ void FormatConfig_EEPROM() { Debug_pushMessage("Formatting Config partition\n"); LubeConfig = LubeConfig_defaults; LubeConfig.EEPROM_Version = eeVersion; StoreConfig_EEPROM(); } /** * @brief Reset the persistence partition and write an empty record. */ void FormatPersistence_EEPROM() { Debug_pushMessage("Formatting Persistence partition\n"); PersistenceData = {0}; StorePersistence_EEPROM(); } /** * @brief Advance the persistence page (wear levelling) and store the new start address. * * When end-of-device (or reset=true), wrap back to startofPersistence. * Requires EEPROM availability. * * @param reset If true, force wrap to the start of the partition. */ void MovePersistencePage_EEPROM(boolean reset) { if (!EEPROM_Available()) return; globals.eePersistenceAddress += sizeof(PersistenceData); PersistenceData.writeCycleCounter = 0; if ((globals.eePersistenceAddress + sizeof(PersistenceData)) > ee.getDeviceSize() || reset) { globals.eePersistenceAddress = startofPersistence; } ee.updateBlock(0, (uint8_t *)&globals.eePersistenceAddress, sizeof(globals.eePersistenceAddress)); } /** * @brief Compute CRC-32 (poly 0xEDB88320) over a byte buffer. */ uint32_t Checksum_EEPROM(uint8_t const *data, size_t len) { if (data == NULL) return 0; uint32_t crc = 0xFFFFFFFF; while (len--) { crc ^= *data++; for (uint8_t k = 0; k < 8; k++) crc = (crc >> 1) ^ (0xEDB88320 & (-(int32_t)(crc & 1))); } return ~crc; } /** * @brief Print a hex/ASCII dump of a region of the EEPROM for debugging. * * Output format: * Address 00 01 02 ... 0F ASCII * 0x00000: XX XX ... ..... */ void dumpEEPROM(uint16_t memoryAddress, uint16_t length) { #define BLOCK_TO_LENGTH 16 if (!EEPROM_Available()) return; char ascii_buf[BLOCK_TO_LENGTH + 1]; sprintf(ascii_buf, "%*s", BLOCK_TO_LENGTH, "ASCII"); Debug_pushMessage(PSTR("\nAddress ")); for (int x = 0; x < BLOCK_TO_LENGTH; x++) Debug_pushMessage("%3d", x); memoryAddress = (memoryAddress / BLOCK_TO_LENGTH) * BLOCK_TO_LENGTH; length = ((length + BLOCK_TO_LENGTH - 1) / BLOCK_TO_LENGTH) * BLOCK_TO_LENGTH; for (unsigned int i = 0; i < length; i++) { const int blockpoint = memoryAddress % BLOCK_TO_LENGTH; if (blockpoint == 0) { ascii_buf[BLOCK_TO_LENGTH] = 0; Debug_pushMessage(" %s", ascii_buf); Debug_pushMessage("\n0x%05X:", memoryAddress); } ascii_buf[blockpoint] = ee.readByte(memoryAddress); Debug_pushMessage(" %02X", ascii_buf[blockpoint]); if (ascii_buf[blockpoint] < 0x20 || ascii_buf[blockpoint] > 0x7E) ascii_buf[blockpoint] = '.'; memoryAddress++; } Debug_pushMessage("\n"); } /** * @brief Unified availability probe with optional recovery. * * Fast path returns the latched availability flag. If not available, * performs a direct probe and, optionally, a recovery sequence. * * @param recover If true, attempt recovery when not available (default: false). * @param attempts Recovery attempts (default: 3). * @param delay_ms Delay between attempts in ms (default: 25). * @return true if EEPROM is available, false otherwise. */ bool EEPROM_Available(bool recover, uint8_t attempts, uint16_t delay_ms) { if (eeAvailable) return true; if (ee.isConnected()) { eeAvailable = true; eeRecoveredOnce = true; // edge 0 -> 1 return true; } if (recover) { return TryRecoverEEPROM(attempts, delay_ms); } return false; } /** * @brief Validate config fields; return bitmask of invalid entries. * * If autocorrect is true, invalid fields are reset to default values. * Each bit in the returned mask identifies a specific field-group that was out-of-bounds. */ uint32_t ConfigSanityCheck(bool autocorrect) { uint32_t setting_reset_bits = 0; if (!(LubeConfig.DistancePerLube_Default > 0) || !(LubeConfig.DistancePerLube_Default < 50000)) { SET_BIT(setting_reset_bits, 0); if (autocorrect) LubeConfig.DistancePerLube_Default = LubeConfig_defaults.DistancePerLube_Default; } if (!(LubeConfig.DistancePerLube_Rain > 0) || !(LubeConfig.DistancePerLube_Rain < 50000)) { SET_BIT(setting_reset_bits, 1); if (autocorrect) LubeConfig.DistancePerLube_Rain = LubeConfig_defaults.DistancePerLube_Rain; } if (!(LubeConfig.tankCapacity_ml > 0) || !(LubeConfig.tankCapacity_ml < 5000)) { SET_BIT(setting_reset_bits, 2); if (autocorrect) LubeConfig.tankCapacity_ml = LubeConfig_defaults.tankCapacity_ml; } if (!(LubeConfig.amountPerDose_microL > 0) || !(LubeConfig.amountPerDose_microL < 100)) { SET_BIT(setting_reset_bits, 3); if (autocorrect) LubeConfig.amountPerDose_microL = LubeConfig_defaults.amountPerDose_microL; } if (!(LubeConfig.TankRemindAtPercentage >= 0) || !(LubeConfig.TankRemindAtPercentage <= 100)) { SET_BIT(setting_reset_bits, 4); if (autocorrect) LubeConfig.TankRemindAtPercentage = LubeConfig_defaults.TankRemindAtPercentage; } if (LubeConfig.SpeedSource == SOURCE_IMPULSE) { if (!(LubeConfig.PulsePerRevolution > 0) || !(LubeConfig.PulsePerRevolution < 1000)) { SET_BIT(setting_reset_bits, 5); if (autocorrect) LubeConfig.PulsePerRevolution = LubeConfig_defaults.PulsePerRevolution; } if (!(LubeConfig.TireWidth_mm > 0) || !(LubeConfig.TireWidth_mm < 500)) { SET_BIT(setting_reset_bits, 6); if (autocorrect) LubeConfig.TireWidth_mm = LubeConfig_defaults.TireWidth_mm; } if (!(LubeConfig.TireWidthHeight_Ratio > 0) || !(LubeConfig.TireWidthHeight_Ratio < 150)) { SET_BIT(setting_reset_bits, 7); if (autocorrect) LubeConfig.TireWidthHeight_Ratio = LubeConfig_defaults.TireWidthHeight_Ratio; } if (!(LubeConfig.RimDiameter_Inch > 0) || !(LubeConfig.RimDiameter_Inch < 30)) { SET_BIT(setting_reset_bits, 8); if (autocorrect) LubeConfig.RimDiameter_Inch = LubeConfig_defaults.RimDiameter_Inch; } if (!(LubeConfig.DistancePerRevolution_mm > 0) || !(LubeConfig.DistancePerRevolution_mm < 10000)) { SET_BIT(setting_reset_bits, 9); if (autocorrect) LubeConfig.DistancePerRevolution_mm = LubeConfig_defaults.DistancePerRevolution_mm; } } if (!(LubeConfig.BleedingPulses > 0) || !(LubeConfig.BleedingPulses < 1001)) { SET_BIT(setting_reset_bits, 10); if (autocorrect) LubeConfig.BleedingPulses = LubeConfig_defaults.BleedingPulses; } if (!(LubeConfig.SpeedSource >= 0) || !(LubeConfig.SpeedSource < SpeedSourceString_Elements)) { SET_BIT(setting_reset_bits, 11); if (autocorrect) LubeConfig.SpeedSource = LubeConfig_defaults.SpeedSource; } if (!(LubeConfig.GPSBaudRate >= 0) || !(LubeConfig.GPSBaudRate < GPSBaudRateString_Elements)) { SET_BIT(setting_reset_bits, 12); if (autocorrect) LubeConfig.GPSBaudRate = LubeConfig_defaults.GPSBaudRate; } if (!(LubeConfig.CANSource >= 0) || !(LubeConfig.CANSource < CANSourceString_Elements)) { SET_BIT(setting_reset_bits, 13); if (autocorrect) LubeConfig.CANSource = LubeConfig_defaults.CANSource; } if (!validateWiFiString(LubeConfig.wifi_ap_ssid, sizeof(LubeConfig.wifi_ap_ssid))) { SET_BIT(setting_reset_bits, 14); if (autocorrect) strncpy(LubeConfig.wifi_ap_ssid, LubeConfig_defaults.wifi_ap_ssid, sizeof(LubeConfig.wifi_ap_ssid)); } if (!validateWiFiString(LubeConfig.wifi_ap_password, sizeof(LubeConfig.wifi_ap_password))) { SET_BIT(setting_reset_bits, 15); if (autocorrect) strncpy(LubeConfig.wifi_ap_password, LubeConfig_defaults.wifi_ap_password, sizeof(LubeConfig.wifi_ap_password)); } if (!validateWiFiString(LubeConfig.wifi_client_ssid, sizeof(LubeConfig.wifi_client_ssid))) { SET_BIT(setting_reset_bits, 16); if (autocorrect) strncpy(LubeConfig.wifi_client_ssid, LubeConfig_defaults.wifi_client_ssid, sizeof(LubeConfig.wifi_client_ssid)); } if (!validateWiFiString(LubeConfig.wifi_client_password, sizeof(LubeConfig.wifi_client_password))) { SET_BIT(setting_reset_bits, 17); if (autocorrect) strncpy(LubeConfig.wifi_client_password, LubeConfig_defaults.wifi_client_password, sizeof(LubeConfig.wifi_client_password)); } return setting_reset_bits; } /** * @brief Validate that a string contains only characters allowed for Wi‑Fi SSIDs/passwords. * * Allowed: A‑Z, a‑z, 0‑9 and the printable ASCII punctuation: ! " # $ % & ' ( ) * + , - . / : ; * < = > ? @ [ \ ] ^ _ ` { | } ~ * * @return true if valid (or empty), false otherwise. */ bool validateWiFiString(char *string, size_t size) { if (string == NULL) return false; for (size_t i = 0; i < size; i++) { char c = string[i]; if (c == '\0') return true; // reached end with valid chars if (!((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '!' || c == '"' || c == '#' || c == '$' || c == '%' || c == '&' || c == '\'' || c == '(' || c == ')' || c == '*' || c == '+' || c == ',' || c == '-' || c == '.' || c == '/' || c == ':' || c == ';' || c == '<' || c == '=' || c == '>' || c == '?' || c == '@' || c == '[' || c == '\\' || c == ']' || c == '^' || c == '_' || c == '`' || c == '{' || c == '|' || c == '}' || c == '~')) { return false; } } // No NUL within buffer: treat as invalid return false; }