Added files
This commit is contained in:
484
digital_alarm_clock.c
Normal file
484
digital_alarm_clock.c
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user