diff --git a/application.fam b/application.fam new file mode 100644 index 0000000..39da37d --- /dev/null +++ b/application.fam @@ -0,0 +1,13 @@ +App( + appid="digital_alarm_clock", + name="Digital Alarm Clock", + apptype=FlipperAppType.EXTERNAL, + entry_point="digital_alarm_clock_app", + stack_size=4 * 1024, + fap_category="Tools", + fap_version="1.0", + fap_description="Nightstand-style clock with configurable alarm, vibrate, and buzzer.", + fap_author="", + fap_weburl="", + fap_icon_assets="assets", +) diff --git a/assets/app_icon.png b/assets/app_icon.png new file mode 100644 index 0000000..5641160 Binary files /dev/null and b/assets/app_icon.png differ diff --git a/digital_alarm_clock.c b/digital_alarm_clock.c new file mode 100644 index 0000000..81024ec --- /dev/null +++ b/digital_alarm_clock.c @@ -0,0 +1,484 @@ +#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; +}