Added files

This commit is contained in:
Jon
2026-06-29 14:56:39 +01:00
parent ad6078d226
commit 40249e7864
3 changed files with 497 additions and 0 deletions

13
application.fam Normal file
View File

@@ -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",
)

BIN
assets/app_icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 B

484
digital_alarm_clock.c Normal file
View File

@@ -0,0 +1,484 @@
#include <furi.h>
#include <furi_hal.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;
}