Use this checklist when adding a feature module. Keep the feature aligned with
the current ServiceRegistry + initializer architecture.
If the module needs retained configuration:
- Add a POD struct in
src/system/rtc/types/Rtc<Feature>Types.h. - Use fixed-size
char[]buffers, enums, integers, and POD structs only. - Add the struct to
RTC::ConfigStoreinsrc/system/rtc/RtcConfig.h. - Increment
RTC::kSchemaVersionwhen the RTC layout changes. - Add defaults in the RTC default/config store implementation.
Packed struct rule:
- Do not bind packed members by reference.
- Validate into a temporary value, then assign to the packed field.
Runtime JSON config lives under src/config/json/.
- Define or reuse key constants in the appropriate config header.
- Use
snake_caseJSON keys for new fields. - Add
<Feature>ConfigJson.h/.cpp. - Keep validation shared between load/save and API update paths.
- Register the loader/saver in
src/config/ConfigManager.cpp.
Preferred update pattern:
auto current = RTC::getConfigSafeCopy().feature;
auto next = current;
CONFIG::JSON::updateFeatureFromJson(obj, next);
const bool changed = memcmp(¤t, &next, sizeof(next)) != 0;Create the domain service under src/<feature>/.
Rules:
- Keep business logic out of API classes.
- Pass dependencies through constructor or
begin(). - Do not add new global
externdependencies. - Do not add
Service::instance()singletons for new modules. - Use
LOGI,LOGW, andLOGE; avoidSerial.print(). - If the module starts a task, define stack, priority, and core affinity in
CONFIG::TASKSinsrc/config/TaskConfig.h.
Create API code under src/api/<feature>/.
Rules:
- Keep API services transport-focused: parse, validate, call service, serialize.
- Use existing auth wrappers and response helpers.
- Use shared ConfigJson validation for settings updates.
- Return explicit no-change/success/error responses, following nearby modules.
- Stream large JSON responses instead of building large temporary
Stringbuffers.
Current composition path:
src/system/services/ServiceRegistry.hsrc/system/services/ServiceRegistryInitialization.cppsrc/system/services/ServiceRegistryInitializationRuntime.*src/system/init/services/*Initializer.*src/system/services/ServiceRegistryApi.hsrc/system/init/services/ApiServicesInitializer.*
Where to wire depends on ownership:
- Long-lived domain services belong in
ServiceRegistry. - Small construction/begin helpers can live in
ServiceRegistryInitializationRuntime.*when they make host tests cleaner. - API services belong in the opaque
ApiServicescontainer and are initialized throughApiServicesInitializer. - Runtime tasks usually start from
RuntimeTasksInitializer.
Document shutdown or restart implications when the feature owns tasks, sockets, file handles, BLE/Wi-Fi state, or matrix display state.
If the feature has UI:
- Add or update TypeScript types using the same
snake_casekeys as firmware. - Add API client methods under
interface/src/lib/services/api/. - Add routes/components under
interface/src/routes/orinterface/src/lib/features/following nearby feature structure. - Add Vitest coverage for parsers, stores, and save/error flows.
Recommended checks:
pio test -e native
./scripts/build-fast.sh
cd interface && npm run quality:frontend:fast
python scripts/contract/verify_api_contract.pyUpdate these when relevant:
docs/engineering/api-contract.jsondocs/engineering/api-contract.mddocs/engineering/architecture/architecture.mddocs/user-guide/README.md