Zum Hauptinhalt springen

Configuration Architecture

Why Multiple Configuration Layers?​

MyStation's configuration system spans multiple layers of the application stack, each serving a specific purpose. Understanding why this complexity exists is crucial for maintaining and extending the system.

The Five-Layer Architecture​

RTC Storage Layer refers to RTC_DATA_ATTR variables for fast access after deep sleep. RTC_DATA_ATTR variables are preverved across deep sleep cycles but lost on full power loss or pushing reset button.

NVS Storage Layer refers to ESP32's Non-Volatile Storage for persistent configuration. After Power loss/pushing reset button, data in NVS remains intact.

Layer 1: HTML/UI Layer (User Interface)​

Purpose: Present configuration options to the user

Technology: HTML form elements with IDs

Example:

<input type="number" id="weather-interval" value="{{WEATHER_INTERVAL}}">
<input type="text" id="city-display" value="{{CITY_NAME}}">

Why This Layer?

  • User-Friendly: Provides human-readable labels and input controls
  • Validation: Browser-native input validation (min/max, required, etc.)
  • Accessibility: Standard HTML forms work with screen readers and assistive devices
  • Template System: Mustache-style {{VARIABLES}} allow dynamic content injection

Naming Convention: kebab-case (e.g., weather-interval, city-display)


Layer 2: JavaScript/JSON Layer (Data Transport)​

Purpose: Collect form data and transmit to server

Technology: JavaScript objects and JSON serialization

Example:

var weatherInterval = document.getElementById('weather-interval').value;
var config = {
weatherInterval: parseInt(weatherInterval),
cityName: document.getElementById('city-display').value
};

fetch('/save-config', {
method: 'POST',
body: JSON.stringify(config)
});

Why This Layer?

  • Type Conversion: Convert string inputs to proper types (int, float, bool)
  • Validation: Client-side validation before sending to server
  • Structured Data: JSON provides clear, type-safe data structure
  • Debugging: Easy to inspect network traffic and payloads
  • Standard Protocol: HTTP POST with JSON is universal and well-understood

Naming Convention: camelCase (e.g., weatherInterval, cityName)


Layer 3: C++ Application Layer (Business Logic)​

Purpose: Process configuration, apply defaults, validate ranges

Technology: C++ struct with member variables

Example:

struct StationConfig {
int weatherInterval = 60; // Default: 60 minutes
char cityName[32] = "Frankfurt"; // Default city
float latitude = 50.1109;
float longitude = 8.6821;

// ... other fields
};

Why This Layer?

  • Type Safety: Strong typing prevents errors
  • Default Values: Compile-time defaults ensure system always has valid config
  • Memory Management: Fixed-size arrays prevent overflow
  • Performance: Direct memory access, no parsing overhead
  • Validation: Range checks and business logic validation
  • Cross-Module Access: Other C++ modules can directly access configuration

Naming Convention: camelCase (e.g., weatherInterval, cityName)


Layer 4: RTC Storage Layer (Persistence)​

Purpose: Store configuration RTC Memory for fast access after deep sleep

Technology: ESP32 RTC Memory

Why This Layer?

  • Persistence: Survives deep sleep
  • Key-Value Store: Simple, efficient storage mechanism

Naming Convention: Abbreviated (≀15 chars) due to NVS limitations

  • Examples: weatherInt, city, stopId, transStart

Why Abbreviated?

  • NVS Limitation: Keys limited to 15 characters maximum
  • Flash Efficiency: Shorter keys = less flash usage
  • Performance: Faster key lookups with shorter strings

Layer 5: Flash Memory (Hardware)​

Purpose: Physical storage of configuration data

Technology: SPI Flash with wear leveling

Why This Layer?

  • Non-Volatile: Data persists when power is removed
  • Wear Leveling: NVS library manages flash cell wear automatically
  • Partition Isolation: Configuration stored in dedicated NVS partition
  • Backup: Can implement backup/restore mechanisms

Data Flow: User to Flash​

Save Configuration Flow​

Load Configuration Flow​


Key Naming Mapping Strategy​

The Transformation Path​

Naming Rules​

LayerConventionReasonExample
HTML (Layer 1)kebab-caseHTML/CSS standardweather-interval
JavaScript (L2)camelCaseJavaScript conventionweatherInterval
JSON (L2)camelCaseJSON/REST API standardweatherInterval
C++ (Layer 3)camelCaseC++ convention (members)weatherInterval
RTC (Layer 4)camelCaseC++ conventionweatherInterval
NVS (Layer 5)Abbreviated15-char limitweatherInt

Consistency Benefits​

  • Predictable: Developers can guess key names
  • Maintainable: Easy to trace through layers
  • Debuggable: Can search for variations of same name
  • Documented: Clear mapping tables exist

Special Cases​

RTC Storage Layer (Layer 4): Fast Access After Deep Sleep​

Purpose: Provide ultra-fast access to configuration data after deep sleep wake, avoiding slow NVS reads

Technology: RTC_DATA_ATTR variables stored in RTC RAM

Example:

// Configuration cached in RTC RAM
RTC_DATA_ATTR int rtc_weatherInterval = 60;
RTC_DATA_ATTR char rtc_cityName[32] = "";
RTC_DATA_ATTR bool rtc_configValid = false;

// Load strategy
void ConfigManager::loadConfig() {
if (rtc_configValid) {
// Fast path: Use RTC cached values after deep sleep wake
g_stationConfig.weatherInterval = rtc_weatherInterval;
strcpy(g_stationConfig.cityName, rtc_cityName);
} else {
// Slow path: Load from NVS after power loss/reset
g_stationConfig.weatherInterval = preferences.getInt("weatherInt", 60);
preferences.getString("city", g_stationConfig.cityName, sizeof(g_stationConfig.cityName));

// Cache in RTC for next wake
rtc_weatherInterval = g_stationConfig.weatherInterval;
strcpy(rtc_cityName, g_stationConfig.cityName);
rtc_configValid = true;
}
}

Why This Layer?

  • Performance: RTC RAM access is ~100x faster than NVS flash reads
  • Battery Life: Reduces flash access, saving power on each wake cycle
  • Deep Sleep Support: Data persists across deep sleep cycles
  • No Flash Wear: Avoids unnecessary flash write cycles

When RTC Data is Valid:

  • βœ… After deep sleep wake (most common case)
  • βœ… During normal operation between sleeps

When RTC Data is Invalid:

  • ❌ After power loss (battery removed/dead)
  • ❌ After pressing reset button
  • ❌ After firmware upload
  • ❌ First boot ever

Trade-offs:

  • Limited Lifetime: Lost on full power loss (unlike NVS/Flash)
  • Small Size: RTC RAM is limited (8KB on ESP32)
  • No Persistence: Not suitable for long-term storage

Best Practices:

  • Always check validity flag before using RTC data
  • Always have NVS as fallback
  • Cache frequently-accessed config in RTC
  • Don't store sensitive data in RTC (not encrypted)

Performance Considerations​

Access Speed Comparison​

Storage TypeRead SpeedSurvives Deep SleepSurvives Power Loss
C++ RAMFastest❌ No❌ No
RTC RAM (L4)Very Fastβœ… Yes❌ No
NVS Flash (L5)Slowβœ… Yesβœ… Yes

NVS Access Best Practices​

Fast Operations:

  • βœ… Reading all config at boot (once)
  • βœ… Bulk save operation (all fields at once)
  • βœ… Using RTC cache for frequent reads

Slow Operations:

  • ❌ Reading NVS every wake cycle (use RTC instead)
  • ❌ Saving single field repeatedly
  • ❌ Loading config multiple times per wake

Typical Performance Numbers​

OperationTimeNote
Load from RTC (deep sleep)~1 msFast path, most common
Load from NVS (power loss)~50 msSlow path, infrequent
Save to NVS + RTC~100 msOne-time cost on config save
Deep sleep wake (with RTC)~200 msIncludes WiFi reconnect
Cold boot (without RTC)~250 msIncludes NVS read + WiFi

NVS Config Version (OTA Migration Safety)​

Problem​

When new firmware adds, renames, or changes NVS keys, devices updating via OTA may have stale or incompatible configuration. While NVS key-value storage is inherently forward-compatible (new keys simply get defaults), structural changes can still cause issues.

Solution​

A CURRENT_CONFIG_VERSION integer is stored in NVS under the key cfgVersion. On every boot, loadFromNVS() compares the saved version against the compiled-in version:

static constexpr int CURRENT_CONFIG_VERSION = 1;

// In loadFromNVS():
int savedVersion = preferences.getInt("cfgVersion", 0);
if (savedVersion < CURRENT_CONFIG_VERSION) {
preferences.clear(); // Reset all NVS config
preferences.putInt("cfgVersion", CURRENT_CONFIG_VERSION);
}

When to Increment​

Change TypeActionExample
Add new NVS keyNo increment neededAdding weatherMdl β€” defaults to ""
Rename existing keyIncrement + clearweatherInt β†’ wInterval
Change value semanticsIncrement + clearInterval unit minutes β†’ seconds
Remove critical keyIncrement + clearRemoving a required field

Why is adding a new NVS key safe but changing the RTC struct is not?

NVS is a key-value store β€” each key is stored independently. Adding weatherMdl between sleepEnd and weekendMode in the code doesn't change how NVS stores them. Old devices simply don't have that key yet, so preferences.getString("weatherMdl", "") returns the default. No corruption.

RTC memory (RTC_DATA_ATTR) is a raw binary struct. Field order determines the memory layout. Adding a 32-byte field shifts everything after it. However, this is safe for OTA because OTA triggers a full reboot (not a deep sleep wake). On full reboot, wakeupCount == 1, so loadFromNVS() always runs and repopulates the struct from NVS β€” the stale RTC layout is never read.

RTC struct changes would only be dangerous if a device could somehow deep-sleep-wake into new firmware without rebooting, which is not possible with OTA.

What Happens After Version Bump​

  1. Device updates firmware via OTA and reboots
  2. loadFromNVS() detects cfgVersion mismatch
  3. NVS is cleared (all settings lost)
  4. Device enters configuration mode (Phase 1 or Phase 2)
  5. User reconfigures via web interface

Version History​

VersionFirmwareChange
0< v0.7.0No versioning (legacy devices)
1v0.7.0+Initial versioned config, added weatherMdl

Best Practices​

  • Only increment for truly breaking NVS changes
  • Adding a new key with a default does not require an increment
  • Document every version bump in the table above
  • saveToNVS() always writes cfgVersion to ensure it's set after configuration

Testing Config Version Migration​

To verify the NVS clear works correctly without creating a real OTA release:

  1. Flash current firmware (debug build) and configure the device normally:

    pio run -e esp32-s3-e1001-debug -t upload
  2. Configure the device β€” set stop, weather model, intervals, etc.

  3. Bump the version in src/config/config_manager.cpp:

    static constexpr int CURRENT_CONFIG_VERSION = 2; // was 1
  4. Flash again and monitor serial output:

    pio run -e esp32-s3-e1001-debug -t upload && pio device monitor
  5. Expected serial output:

    [WARN][CONFIG_MANAGER] NVS config version 1 < 2 β€” clearing NVS for clean migration
    [INFO][MAIN] Configuration Phase: 2 (App Setup)
  6. Verify:

    • WiFi still connects (credentials stored in separate namespace) βœ…
    • Device enters Phase 2 configuration mode (app settings cleared) βœ…
    • All app settings reset to defaults βœ…
  7. Revert the version change when done:

    # Reset to original version
    sed -i '' 's/CURRENT_CONFIG_VERSION = 2/CURRENT_CONFIG_VERSION = 1/' src/config/config_manager.cpp

Tip: For debug builds, increase delay() in initSerialConnector() to 3000ms if you need more time for the serial monitor to connect before boot logs appear.


Summary​

Why This Complexity?​

The five-layer architecture exists because:

  1. Separation of Concerns: Each layer has one job
  2. Type Safety: Strong typing prevents errors
  3. Validation: Multiple validation points
  4. Persistence: Survives power loss
  5. Performance: Optimized for embedded systems with RTC caching
  6. Maintainability: Clear boundaries, easy to debug
  7. Extensibility: Easy to add new fields
  8. Migration: Can evolve configuration format
  9. Battery Efficiency: RTC layer minimizes flash access

Key Principle​

Data flows through transformations, each layer adding value

  • Layer 1 - HTML: User interaction and presentation
  • Layer 2 - JavaScript/JSON: Client-side logic and transport format
  • Layer 3 - C++ Application: Business logic, validation, and runtime access
  • Layer 4 - RTC Storage: Fast access cache for deep sleep scenarios
  • Layer 5 - NVS Storage: Persistent storage surviving power loss

The Smart Loading Strategy​

Power-On/Reset: NVS (slow) β†’ C++ β†’ RTC β†’ Display
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
Cache for future wakes

Deep Sleep Wake: RTC (fast) β†’ C++ β†’ Display
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
Skip NVS entirely

This architecture has served MyStation well and should be maintained as the project evolves.