#include #include #include #include #include #include #include #include #include #include #include #define SETTINGS_DIR "/ext/apps_data/digital_alarm_clock" #define SETTINGS_FILE "/ext/apps_data/digital_alarm_clock/settings.bin" #define SETTINGS_MAGIC ((uint32_t)0xDAC10002u) /* bumped – layout changed */ #define TIMER_MS (500u) #define RING_TICKS (4u) /* repeat ring every 4 ticks = 2 sec */ /* ── types ────────────────────────────────────────────────────── */ typedef enum { EvInput, EvTick } EvType; typedef struct { EvType type; InputEvent inp; } AppEv; typedef enum { SelAlarm = 0, SelVibrate, SelBuzzer, SelHour, SelMinute, SelCount, } Sel; typedef enum { StNight, StRing } AppState; typedef struct { uint32_t magic; bool alarm_on; bool vib_on; bool buz_on; uint8_t hour; uint8_t min; } Settings; typedef struct { const char* lbl; bool val; Sel sel; } TogBtn; typedef struct { FuriMessageQueue* q; ViewPort* vp; Gui* gui; FuriTimer* timer; NotificationApp* notif; Settings cfg; AppState state; Sel sel; bool running; bool fired; uint8_t fired_min; uint32_t ticks; } App; /* ── notification sequences ───────────────────────────────────── */ static const NotificationSequence SEQ_STOP = { &message_vibro_off, &message_sound_off, NULL }; static const NotificationSequence SEQ_BOTH = { &message_vibro_on, &message_note_c7, &message_delay_250, &message_vibro_off, &message_sound_off, &message_delay_100, &message_vibro_on, &message_note_c6, &message_delay_250, &message_vibro_off, &message_sound_off, NULL }; static const NotificationSequence SEQ_BUZ = { &message_note_c7, &message_delay_250, &message_sound_off, &message_delay_100, &message_note_c6, &message_delay_250, &message_sound_off, NULL }; static const NotificationSequence SEQ_VIB = { &message_vibro_on, &message_delay_250, &message_vibro_off, &message_delay_100, &message_vibro_on, &message_delay_250, &message_vibro_off, NULL }; /* ── settings ─────────────────────────────────────────────────── */ static void cfg_defaults(Settings* s) { s->magic = SETTINGS_MAGIC; s->alarm_on = false; s->vib_on = true; s->buz_on = true; s->hour = 7; s->min = 0; } static void cfg_load(App* a) { Storage* st = furi_record_open(RECORD_STORAGE); File* f = storage_file_alloc(st); do { if(!storage_file_open(f, SETTINGS_FILE, FSAM_READ, FSOM_OPEN_EXISTING)) break; Settings tmp; if(storage_file_read(f, &tmp, sizeof tmp) != sizeof tmp) break; if(tmp.magic != SETTINGS_MAGIC) break; if(tmp.hour > 23 || tmp.min > 59) break; a->cfg = tmp; } while(false); storage_file_close(f); storage_file_free(f); furi_record_close(RECORD_STORAGE); } static void cfg_save(const App* a) { Storage* st = furi_record_open(RECORD_STORAGE); storage_common_mkdir(st, SETTINGS_DIR); File* f = storage_file_alloc(st); if(storage_file_open(f, SETTINGS_FILE, FSAM_WRITE, FSOM_CREATE_ALWAYS)) storage_file_write(f, &a->cfg, sizeof a->cfg); storage_file_close(f); storage_file_free(f); furi_record_close(RECORD_STORAGE); } /* ── alarm sound ──────────────────────────────────────────────── */ static void alarm_ring(App* a) { if(a->cfg.buz_on && a->cfg.vib_on) notification_message(a->notif, &SEQ_BOTH); else if(a->cfg.buz_on) notification_message(a->notif, &SEQ_BUZ); else if(a->cfg.vib_on) notification_message(a->notif, &SEQ_VIB); /* if both off, visual-only alarm */ } /* ── draw ─────────────────────────────────────────────────────── */ static void draw_cb(Canvas* c, void* ctx) { App* a = ctx; DateTime dt; furi_hal_rtc_get_datetime(&dt); canvas_clear(c); canvas_set_color(c, ColorBlack); /* ── ringing overlay ── */ if(a->state == StRing) { canvas_draw_box(c, 0, 0, 128, 11); canvas_set_color(c, ColorWhite); canvas_set_font(c, FontPrimary); canvas_draw_str_aligned(c, 64, 2, AlignCenter, AlignTop, "! ALARM !"); canvas_set_color(c, ColorBlack); char t[8]; snprintf(t, sizeof t, "%02d:%02d", (int)dt.hour, (int)dt.minute); canvas_set_font(c, FontBigNumbers); canvas_draw_str_aligned(c, 64, 14, AlignCenter, AlignTop, t); canvas_set_font(c, FontSecondary); canvas_draw_str_aligned(c, 64, 56, AlignCenter, AlignTop, "Press any key to stop"); return; } /* ── nightstand view ── */ /* big HH:MM, shifted left so seconds fit beside it */ char big[8]; snprintf(big, sizeof big, "%02d:%02d", (int)dt.hour, (int)dt.minute); canvas_set_font(c, FontBigNumbers); canvas_draw_str_aligned(c, 60, 0, AlignCenter, AlignTop, big); /* seconds — small, to the right */ char sec[5]; snprintf(sec, sizeof sec, ":%02d", (int)dt.second); canvas_set_font(c, FontSecondary); canvas_draw_str(c, 94, 25, sec); /* divider */ canvas_draw_line(c, 0, 28, 127, 28); if(a->sel == SelHour || a->sel == SelMinute) { /* ── alarm time edit ── */ canvas_set_font(c, FontSecondary); canvas_draw_str_aligned(c, 64, 30, AlignCenter, AlignTop, "Set Alarm Time:"); char hh[4], mm[4]; snprintf(hh, sizeof hh, "%02d", (int)a->cfg.hour); snprintf(mm, sizeof mm, "%02d", (int)a->cfg.min); /* hour box */ if(a->sel == SelHour) { canvas_draw_box(c, 42, 39, 20, 11); canvas_set_color(c, ColorWhite); canvas_draw_str_aligned(c, 52, 41, AlignCenter, AlignTop, hh); canvas_set_color(c, ColorBlack); } else { canvas_draw_frame(c, 42, 39, 20, 11); canvas_draw_str_aligned(c, 52, 41, AlignCenter, AlignTop, hh); } canvas_draw_str_aligned(c, 64, 41, AlignCenter, AlignTop, ":"); /* minute box */ if(a->sel == SelMinute) { canvas_draw_box(c, 66, 39, 20, 11); canvas_set_color(c, ColorWhite); canvas_draw_str_aligned(c, 76, 41, AlignCenter, AlignTop, mm); canvas_set_color(c, ColorBlack); } else { canvas_draw_frame(c, 66, 39, 20, 11); canvas_draw_str_aligned(c, 76, 41, AlignCenter, AlignTop, mm); } canvas_set_font(c, FontSecondary); canvas_draw_str_aligned(c, 64, 57, AlignCenter, AlignTop, "Up/Dn:adj L/R:nav Bk:exit"); } else { /* ── alarm info + toggle buttons ── */ char alm[22]; snprintf( alm, sizeof alm, "Alarm %02d:%02d [%s]", (int)a->cfg.hour, (int)a->cfg.min, a->cfg.alarm_on ? "ON" : "OFF"); canvas_set_font(c, FontSecondary); canvas_draw_str_aligned(c, 64, 30, AlignCenter, AlignTop, alm); /* three toggle buttons */ const TogBtn btns[3] = { {"ALM", a->cfg.alarm_on, SelAlarm}, {"VIB", a->cfg.vib_on, SelVibrate}, {"BUZ", a->cfg.buz_on, SelBuzzer}, }; static const uint8_t BX[3] = {2, 44, 86}; const uint8_t BW = 38, BH = 13, BY = 42; for(int i = 0; i < 3; i++) { bool selected = (a->sel == btns[i].sel); if(selected) { canvas_draw_box(c, BX[i], BY, BW, BH); canvas_set_color(c, ColorWhite); } else { canvas_draw_frame(c, BX[i], BY, BW, BH); } char lbl[9]; snprintf(lbl, sizeof lbl, "%s%s", btns[i].lbl, btns[i].val ? ":ON" : ":OFF"); canvas_set_font(c, FontSecondary); canvas_draw_str_aligned(c, BX[i] + BW / 2, BY + 3, AlignCenter, AlignTop, lbl); canvas_set_color(c, ColorBlack); } canvas_set_font(c, FontSecondary); canvas_draw_str_aligned(c, 64, 57, AlignCenter, AlignTop, "Up/Dn:val L/R:nav Bk:exit"); } } /* ── ISR-context callbacks ────────────────────────────────────── */ static void input_cb(InputEvent* ev, void* ctx) { App* a = ctx; AppEv e = {.type = EvInput, .inp = *ev}; furi_message_queue_put(a->q, &e, 0); } static void timer_cb(void* ctx) { App* a = ctx; AppEv e = {.type = EvTick}; furi_message_queue_put(a->q, &e, 0); } /* ── main-thread event handlers ───────────────────────────────── */ static void on_input(App* a, const InputEvent* ev) { /* ringing: any physical press dismisses in-app alarm */ if(a->state == StRing) { if(ev->type == InputTypePress) { notification_message(a->notif, &SEQ_STOP); a->state = StNight; view_port_update(a->vp); } return; } if(ev->type != InputTypeShort && ev->type != InputTypeRepeat) return; bool settings_changed = false; switch(ev->key) { case InputKeyBack: a->running = false; return; /* skip rtc sync — done on exit */ /* Left / Right navigate between selections */ case InputKeyLeft: a->sel = (Sel)((a->sel + SelCount - 1) % SelCount); break; case InputKeyRight: a->sel = (Sel)((a->sel + 1) % SelCount); break; /* Up increments / turns ON the current selection */ case InputKeyUp: switch(a->sel) { case SelAlarm: a->cfg.alarm_on = true; settings_changed = true; break; case SelVibrate: a->cfg.vib_on = true; settings_changed = true; break; case SelBuzzer: a->cfg.buz_on = true; settings_changed = true; break; case SelHour: a->cfg.hour = (uint8_t)((a->cfg.hour + 1) % 24); settings_changed = true; break; case SelMinute: a->cfg.min = (uint8_t)((a->cfg.min + 1) % 60); settings_changed = true; break; default: break; } break; /* Down decrements / turns OFF the current selection */ case InputKeyDown: switch(a->sel) { case SelAlarm: a->cfg.alarm_on = false; settings_changed = true; break; case SelVibrate: a->cfg.vib_on = false; settings_changed = true; break; case SelBuzzer: a->cfg.buz_on = false; settings_changed = true; break; case SelHour: a->cfg.hour = (uint8_t)((a->cfg.hour + 23) % 24); settings_changed = true; break; case SelMinute: a->cfg.min = (uint8_t)((a->cfg.min + 59) % 60); settings_changed = true; break; default: break; } break; /* OK toggles booleans; increments time fields */ case InputKeyOk: switch(a->sel) { case SelAlarm: a->cfg.alarm_on = !a->cfg.alarm_on; settings_changed = true; break; case SelVibrate: a->cfg.vib_on = !a->cfg.vib_on; settings_changed = true; break; case SelBuzzer: a->cfg.buz_on = !a->cfg.buz_on; settings_changed = true; break; case SelHour: a->cfg.hour = (uint8_t)((a->cfg.hour + 1) % 24); settings_changed = true; break; case SelMinute: a->cfg.min = (uint8_t)((a->cfg.min + 1) % 60); settings_changed = true; break; default: break; } break; default: break; } UNUSED(settings_changed); view_port_update(a->vp); } static void on_tick(App* a) { a->ticks++; view_port_update(a->vp); if(!a->cfg.alarm_on) return; DateTime dt; furi_hal_rtc_get_datetime(&dt); /* reset fired flag when the minute rolls over */ if(dt.minute != a->fired_min) { a->fired_min = dt.minute; a->fired = false; } /* trigger in-app alarm */ bool just_fired = false; if(!a->fired && a->state != StRing && dt.hour == a->cfg.hour && dt.minute == a->cfg.min) { a->fired = true; a->state = StRing; just_fired = true; } /* ring immediately on trigger, then repeat every RING_TICKS */ if(a->state == StRing && (just_fired || (a->ticks % RING_TICKS == 0))) { alarm_ring(a); } } /* ── lifecycle ────────────────────────────────────────────────── */ static App* app_alloc(void) { App* a = malloc(sizeof *a); furi_assert(a); a->q = furi_message_queue_alloc(8, sizeof(AppEv)); a->vp = view_port_alloc(); a->gui = furi_record_open(RECORD_GUI); a->notif = furi_record_open(RECORD_NOTIFICATION); a->timer = furi_timer_alloc(timer_cb, FuriTimerTypePeriodic, a); a->state = StNight; a->sel = SelAlarm; a->running = true; a->fired = false; a->fired_min = 255; /* invalid sentinel — triggers reset on first tick */ a->ticks = 0; cfg_defaults(&a->cfg); cfg_load(a); view_port_draw_callback_set(a->vp, draw_cb, a); view_port_input_callback_set(a->vp, input_cb, a); gui_add_view_port(a->gui, a->vp, GuiLayerFullscreen); return a; } static void app_free(App* a) { furi_timer_stop(a->timer); furi_timer_free(a->timer); gui_remove_view_port(a->gui, a->vp); view_port_free(a->vp); furi_record_close(RECORD_GUI); furi_record_close(RECORD_NOTIFICATION); furi_message_queue_free(a->q); free(a); } /* ── entry point ──────────────────────────────────────────────── */ int32_t digital_alarm_clock_app(void* p) { UNUSED(p); App* a = app_alloc(); furi_timer_start(a->timer, TIMER_MS); AppEv ev; while(a->running) { if(furi_message_queue_get(a->q, &ev, FuriWaitForever) == FuriStatusOk) { if(ev.type == EvInput) on_input(a, &ev.inp); else on_tick(a); } } cfg_save(a); notification_message(a->notif, &SEQ_STOP); app_free(a); return 0; }