Navigation: Project README · Engineering Reference · Architecture
This project has two configuration layers:
- compile-time constants in
src/config/*.h - runtime configuration loaded from
/config/config.jsoninto the RTC/PSRAM-backed config store
This document describes how those layers are split and how code should use them.
The core configuration headers are:
src/config/
├── App.h
├── Hardware.h
├── Network.h
├── System.h
├── TaskConfig.h
└── json/*.h / json/*.cpp
Use for:
- global JSON keys in
CONFIG::Keys - factory defaults
- application-level constants
- notification / macro / compensation / feature constants
This file is also where the persisted JSON schema names are defined, so changing a key here can affect backend persistence and frontend compatibility.
Use for:
- GPIO mappings
- board-specific hardware constants
- thermal thresholds
- low-level hardware-related timing or safety constants
Use for:
- Wi-Fi / HTTP / auth constants
- API paths shared across firmware
- BLE UUIDs and radio/network constants
Use for:
- aggregate inclusion of the config headers
Use for:
- task stacks, priorities, and core affinity
- system timeouts
- task monitoring thresholds
Include the specific header you need where practical. Use System.h when a
translation unit genuinely needs the aggregate config view.
Example:
#include "config/Hardware.h"
#include "config/System.h"The canonical runtime config file is:
/config/config.json
CONFIG::save() writes atomically using:
/config/settings.tmp/config/settings.bak
So the runtime config path is no longer settings.json in the project root.
src/core/config/ConfigManager.cpp is the central JSON orchestrator.
It:
- reads the config file into a PSRAM-backed
JsonDocument - dispatches each section to
src/config/json/* - rebuilds the full JSON document on save
Current top-level sections come from CONFIG::Keys and include:
notificationwifiSensingbleshellyalarmsheartbeatudp_pusherairmousematrixmacrosloggingpowercompensationusb_terminal
Note that the schema is still mixed in a few historical places. New keys should follow the repo rule of snake_case.
Runtime configuration is initialized in StorageInitializer:
- initialize NVS
- mount LittleFS early only on cold boot
- call
RTC::initConfig(LittleFS) - create config and subsystem locks
Boot path rules:
- cold boot: initialize defaults, then load
/config/config.json - warm boot after deep sleep: restore config from the RTC shadow copy and skip early filesystem load
That warm/cold split is implemented in src/system/rtc/RtcConfigLoader.*.
Preferred patterns:
RTC::withConfig(...)for reading selected fields under lockRTC::getConfigSafeCopy()when a full consistent copy is needed
Avoid copying the whole config store onto the stack unless there is a real reason.
Preferred pattern:
RTC::updateConfig(...)
That gives:
- lock protection
magic/ schema / CRC refresh
If the update must survive cold boot, it must also be persisted:
- typically via
CONFIG::save(LittleFS) - or via a service update handler that already calls
CONFIG::save(...)
HTTP-facing settings services often wrap a config block using RtcStatefulService<T>.
Examples:
- BLE settings
- matrix settings
- notification settings
- Wi-Fi sensing settings
This pattern is useful when a module naturally owns one RTC-backed state object and exposes it over an endpoint. Other modules update config directly with RTC::updateConfig(...) instead.
- power settings still keep a small NVS backup in namespace
power_cfg - most feature settings persist through the centralized JSON config file
- runtime-only counters and caches are not part of
config.json
For the actual deep-sleep persistence model, see rtc_persistence.md.
Navigation: Project README · Engineering Reference · Architecture