704 lines
20 KiB
C++
704 lines
20 KiB
C++
/**
|
||
* @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) {
|
||
// 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;
|
||
}
|