Files
Kettenoeler/Software/src/config.cpp

704 lines
20 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.

/**
* @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 <Arduino.h>
#include <Wire.h>
#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) {
// Dont 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 WiFi SSIDs/passwords.
*
* Allowed: AZ, az, 09 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;
}