485 lines
15 KiB
C
485 lines
15 KiB
C
#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;
|
||
}
|