Files
flipper-digital-alarm-clock/digital_alarm_clock.c
Jon df7fc97cd0 Add app icon and asset support
- clock.png (10x10) set as fap_icon for app browser
- app_icon.png (47x39) compiled as I_app_icon_47x39 asset for in-app use
- Include digital_alarm_clock_icons.h in source

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYNXvRg4xjHHE9oPH9kYYU
2026-06-29 15:04:06 +01:00

486 lines
15 KiB
C
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include <furi.h>
#include <furi_hal.h>
#include <digital_alarm_clock_icons.h>
#include <gui/gui.h>
#include <gui/view_port.h>
#include <input/input.h>
#include <notification/notification.h>
#include <notification/notification_messages.h>
#include <storage/storage.h>
#include <stdio.h>
#include <string.h>
#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;
}