Skip to main content

Project Structure

Safi/
├── main/ Application glue and boot
│ ├── main.c app_main: boot order, auto-resume hook
│ ├── system_init.c Peripheral bring-up sequence
│ ├── wifi_manager.c Association, retries, TX power ladder, SoftAP provisioning
│ ├── event_handlers.c Input events to player commands (control only, no UI)
│ ├── usb_console.c Native USB serial console
│ ├── app_config.h Pin map, task priorities, tuning constants
│ └── Kconfig.projbuild The "Safi Configuration" menu
├── components/ One directory per subsystem, each with include/ and src/
│ ├── event_bus/
│ ├── audio_player/ Codecs, I2S output, prescan, the decode task
│ ├── stream_client/
│ ├── display_driver/ Panel driver, LVGL glue, screens, fonts
│ ├── sd_manager/
│ ├── state_manager/ NVS, favorites, scheduler, time sync, boot report
│ ├── web_server/ HTTPS, REST (one file per domain), WebSocket, auth
│ ├── rfid_manager/
│ ├── input_handler/
│ ├── power_manager/
│ └── rtc_ds3231/ DS3231 real-time clock driver, shared I2C bus
├── littlefs_data/
│ ├── www/ The dashboard (8 pages, vanilla JS)
│ └── certs/ Development TLS certificate and key
├── test/ On-target Unity test app
├── tools/ Certificates, provisioning, fonts, SD validation
├── docs/ Pointer stubs into this site
├── safi-docs/ This documentation site
├── partitions.csv The 16 MB flash layout
├── sdkconfig.defaults Baseline configuration
├── sdkconfig.production Hardened profile
└── build.sh / flash.sh / monitor.sh

The component convention

Every component follows the same shape, which is the ESP-IDF standard plus two house rules:

components/audio_player/
├── include/ Public API: what other components may call
│ ├── audio_player.h
│ └── ...
├── src/ Private implementation
├── idf_component.yml Managed third-party dependencies, if any
└── CMakeLists.txt

House rules:

  1. Components talk through public headers or the event bus, nothing else. If a component needs another's internals, that is a missing API, not an invitation to include a private header.
  2. UI rendering happens only inside display_driver. Other components publish events; the display decides what they look like. This kept a whole class of cross-task LVGL corruption impossible by construction (see display UI).

Third-party code

Managed dependencies are declared per component in idf_component.yml and fetched by the build into managed_components/ (never edited, never committed):

DependencyUsed byFor
lvgl/lvgl 9.xdisplay_driverThe UI toolkit
espressif/esp_lvgl_portdisplay_driverLVGL-to-esp_lcd glue and task
espressif/esp_audio_codecaudio_playerAAC and HE-AAC decoding
garag/esp-idf-pn532rfid_managerPN532 driver
joltwallet/littlefsmain/web_serverThe dashboard filesystem

One vendored exception: minimp3 is a single header in components/audio_player/include/. It is vendored rather than managed because the wrapper depends on its exact frame-boundary semantics (the subject of a war story), so upgrades must be deliberate.

Where things happen, by question

QuestionLook in
Which pin is X on?main/app_config.h, or the pinout reference
Where do REST endpoints live?components/web_server/src/rest_api_*.c, one file per domain
Where is a setting persisted?components/state_manager/src/nvs_manager.c, schema in NVS
What happens at boot, in order?main/system_init.c, then main/main.c
Where are the screens drawn?components/display_driver/src/ui_screens.c
What events exist?components/event_bus/include/event_bus.h, or the events reference