Skip to main content

Project Structure

The firmware is organised as a set of ESP-IDF components — the framework's equivalent of packages in a monorepo. Each component owns a hardware subsystem or feature and exposes a clean public API via its include/ directory.


Directory Layout

alarm-clock/
├── CMakeLists.txt # top-level CMake; just lists components dir
├── sdkconfig # board/feature config (generated by idf.py menuconfig)
├── main/
│ ├── CMakeLists.txt # registers main component, lists its deps
│ └── main.c # app_main() entry point
└── components/
├── rtc/
│ ├── CMakeLists.txt
│ ├── include/rtc.h # public API
│ └── rtc.c
├── display/
│ ├── CMakeLists.txt
│ ├── include/display.h
│ └── display.c
├── audio/
│ ├── CMakeLists.txt
│ ├── include/audio.h
│ └── audio.c
├── lfs/
│ ├── CMakeLists.txt
│ ├── include/lfs.h
│ └── lfs.c
├── buttons/
│ ├── CMakeLists.txt
│ ├── include/buttons.h
│ └── buttons.c
├── ntp/
│ ├── CMakeLists.txt
│ ├── include/ntp.h
│ └── ntp.c
└── alarm_manager/
├── CMakeLists.txt
├── include/alarm_manager.h
└── alarm_manager.c

Component CMakeLists

Each component has a minimal CMakeLists.txt:

# components/rtc/CMakeLists.txt
idf_component_register(
SRCS "rtc.c"
INCLUDE_DIRS "include"
REQUIRES "driver" "esp_timer"
)

REQUIRES lists ESP-IDF libraries or other components this one depends on. The build system resolves include paths and link order automatically.


Main Component

The main component ties everything together:

# main/CMakeLists.txt
idf_component_register(
SRCS "main.c"
INCLUDE_DIRS "."
REQUIRES "rtc" "display" "audio" "lfs" "buttons" "ntp" "alarm_manager"
)

Public APIs (Header Design)

Each component's include/ header exposes only what the main task needs. Example for the RTC component:

// components/rtc/include/rtc.h
#pragma once
#include <time.h>
#include "esp_err.h"

esp_err_t rtc_init(void);
esp_err_t rtc_get_time(struct tm *out);
esp_err_t rtc_set_time(const struct tm *in);
float rtc_get_temperature(void);

Implementation details (I2C handle, register constants) stay in rtc.c — not exposed. This mirrors the interface/implementation separation you would use in any other codebase.


sdkconfig as "Environment Variables"

sdkconfig stores compile-time configuration values. Custom config items can be added via a Kconfig file in any component:

# components/ntp/Kconfig
menu "NTP Configuration"
config NTP_SERVER
string "NTP server hostname"
default "pool.ntp.org"

config NTP_SYNC_INTERVAL_SEC
int "NTP sync interval (seconds)"
default 3600
endmenu

Access in C code:

#include "sdkconfig.h"
sntp_setservername(0, CONFIG_NTP_SERVER);

This keeps user-configurable values out of the source code, analogous to environment variables in a server application.