Main Loop and State Machine
The main task coordinates all subsystems through a finite state machine. It receives events from queues, transitions between states, and dispatches commands to the display and audio tasks.
State Machine
State Definitions
| State | Display screen | Audio | Buttons active |
|---|---|---|---|
| IDLE | SCREEN_CLOCK | Off | SELECT (long-press for menu) |
| ALARM_FIRING | SCREEN_ALARM_FIRING | Playing, looping, fade-in | SNOOZE, DISMISS |
| SNOOZED | SCREEN_SNOOZED | Off | DISMISS |
| MENU | SCREEN_MENU | Off | UP, DOWN, SELECT, BACK |
| ALARM_SET | SCREEN_ALARM_SET | Off | UP, DOWN, SELECT, BACK |
Event Sources
The main task receives events from multiple sources via a unified event loop:
typedef enum {
EVT_BUTTON, // from button_event_queue
EVT_ALARM_FIRE, // from alarm_event_queue
EVT_SNOOZE_TIMER, // from snooze_timer_queue
EVT_NTP_SYNCED, // from ntp event group bit
EVT_TICK_60S, // from periodic timer
} event_type_t;
typedef struct {
event_type_t type;
union {
button_event_t button;
alarm_t alarm;
};
} main_event_t;
Event Loop
void main_task(void *arg) {
app_state_t state = STATE_IDLE;
main_event_t evt;
for (;;) {
// Collect events from all sources with a short timeout
if (collect_next_event(&evt, pdMS_TO_TICKS(1000))) {
state = state_machine_step(state, &evt);
}
// Periodic: update clock display, check alarms
check_alarm_tick();
update_clock_display_if_minute_changed();
}
}
State Transition Function
app_state_t state_machine_step(app_state_t state, const main_event_t *evt) {
switch (state) {
case STATE_IDLE:
if (evt->type == EVT_ALARM_FIRE) {
current_alarm = evt->alarm;
audio_play(current_alarm.audio_file);
display_set_screen(SCREEN_ALARM_FIRING);
arm_auto_dismiss_timer(10 * 60); // 10 min
return STATE_ALARM_FIRING;
}
if (evt->type == EVT_BUTTON
&& evt->button.id == BTN_SELECT
&& evt->button.type == BTN_EVENT_LONG_PRESS) {
display_set_screen(SCREEN_MENU);
arm_inactivity_timer(30);
return STATE_MENU;
}
return STATE_IDLE;
case STATE_ALARM_FIRING:
if (evt->type == EVT_BUTTON && evt->button.id == BTN_SNOOZE) {
audio_stop();
alarm_snooze();
display_set_screen(SCREEN_SNOOZED);
return STATE_SNOOZED;
}
if (evt->type == EVT_BUTTON && evt->button.id == BTN_DISMISS) {
audio_stop();
disarm_auto_dismiss_timer();
display_set_screen(SCREEN_CLOCK);
return STATE_IDLE;
}
if (evt->type == EVT_SNOOZE_TIMER) { // auto-dismiss after 10 min
audio_stop();
display_set_screen(SCREEN_CLOCK);
return STATE_IDLE;
}
return STATE_ALARM_FIRING;
case STATE_SNOOZED:
if (evt->type == EVT_ALARM_FIRE) { // snooze timer expired
audio_play(current_alarm.audio_file);
display_set_screen(SCREEN_ALARM_FIRING);
arm_auto_dismiss_timer(10 * 60);
return STATE_ALARM_FIRING;
}
if (evt->type == EVT_BUTTON && evt->button.id == BTN_DISMISS) {
alarm_snooze_cancel();
display_set_screen(SCREEN_CLOCK);
return STATE_IDLE;
}
return STATE_SNOOZED;
case STATE_MENU:
if (evt->type == EVT_BUTTON) {
menu_handle(evt->button); // returns new state if item selected
}
return STATE_MENU; // menu_handle may call transition_to() directly
default:
return STATE_IDLE;
}
}
Startup Sequence
void app_main(void) {
// 1. Init NVS flash
nvs_flash_init();
// 2. Init hardware drivers (order matters)
lfs_init(); // needed by audio
rtc_init(); // needed by timekeeping
buttons_init(); // needed by state machine
display_init(); // shows splash screen
frontlight_set(128); // 50% brightness
// 3. Set system clock from RTC
sync_system_clock_from_rtc();
// 4. Start tasks
xTaskCreatePinnedToCore(display_task, "display", 8192, NULL, 4, NULL, 0);
xTaskCreatePinnedToCore(audio_task, "audio", 4096, NULL, 5, NULL, 1);
xTaskCreate(button_task, "buttons", 2048, NULL, 6, NULL);
// 5. Connect to WiFi and start NTP (non-blocking)
wifi_init_sta_from_nvs();
// 6. Show clock
display_set_screen(SCREEN_CLOCK);
// 7. Main event loop
main_task(NULL);
}