|
|
@@ -1,6 +1,7 @@
|
|
|
#include <furi.h>
|
|
|
#include <furi_hal.h>
|
|
|
|
|
|
+#include <notification/notification_messages.h>
|
|
|
#include <gui/gui.h>
|
|
|
#include <gui/elements.h>
|
|
|
#include <input/input.h>
|
|
|
@@ -9,29 +10,104 @@
|
|
|
* Just set fap_icon_assets in application.fam and #include {APPID}_icons.h */
|
|
|
#include "flipp_pomodoro_icons.h"
|
|
|
|
|
|
+const int SECONDS_IN_MINUTE = 60;
|
|
|
+
|
|
|
+/// @brief Actions to be processed in a queue
|
|
|
typedef enum {
|
|
|
- TimerTickType,
|
|
|
+ TimerTickType = 42,
|
|
|
InputEventType,
|
|
|
} ActionType;
|
|
|
|
|
|
+/// @brief Single action contains type and payload
|
|
|
typedef struct {
|
|
|
ActionType type;
|
|
|
void* payload;
|
|
|
} Action;
|
|
|
|
|
|
-/**
|
|
|
- * Flipp Pomodoro state management
|
|
|
-*/
|
|
|
-
|
|
|
typedef enum {
|
|
|
Work,
|
|
|
Rest,
|
|
|
} PomodoroStage;
|
|
|
|
|
|
+static const NotificationSequence work_start_notification = {
|
|
|
+ &message_display_backlight_on,
|
|
|
+
|
|
|
+ &message_vibro_on,
|
|
|
+
|
|
|
+ &message_note_b5,
|
|
|
+ &message_delay_250,
|
|
|
+
|
|
|
+ &message_note_d5,
|
|
|
+ &message_delay_250,
|
|
|
+
|
|
|
+ &message_sound_off,
|
|
|
+ &message_vibro_off,
|
|
|
+
|
|
|
+ &message_green_255,
|
|
|
+ &message_delay_1000,
|
|
|
+ &message_green_0,
|
|
|
+ &message_delay_250,
|
|
|
+ &message_green_255,
|
|
|
+ &message_delay_1000,
|
|
|
+
|
|
|
+ NULL,
|
|
|
+};
|
|
|
+
|
|
|
+static const NotificationSequence rest_start_notification = {
|
|
|
+ &message_display_backlight_on,
|
|
|
+
|
|
|
+ &message_vibro_on,
|
|
|
+
|
|
|
+ &message_note_d5,
|
|
|
+ &message_delay_250,
|
|
|
+
|
|
|
+ &message_note_b5,
|
|
|
+ &message_delay_250,
|
|
|
+
|
|
|
+ &message_sound_off,
|
|
|
+ &message_vibro_off,
|
|
|
+
|
|
|
+ &message_red_255,
|
|
|
+ &message_delay_1000,
|
|
|
+ &message_red_0,
|
|
|
+ &message_delay_250,
|
|
|
+ &message_red_255,
|
|
|
+ &message_delay_1000,
|
|
|
+
|
|
|
+ NULL,
|
|
|
+};
|
|
|
+
|
|
|
+static const NotificationSequence* stage_start_notification_sequence_map[] = {
|
|
|
+ [Work] = &work_start_notification,
|
|
|
+ [Rest] = &rest_start_notification,
|
|
|
+};
|
|
|
+
|
|
|
+static char* next_stage_label[] = {
|
|
|
+ [Work] = "Get Rest",
|
|
|
+ [Rest] = "Start Work",
|
|
|
+};
|
|
|
+
|
|
|
+static const Icon* stage_background_image[] = {
|
|
|
+ [Work] = &I_flipp_pomodoro_work_64,
|
|
|
+ [Rest] = &I_flipp_pomodoro_rest_64,
|
|
|
+};
|
|
|
+
|
|
|
+static const PomodoroStage stage_rotaion_map[] = {
|
|
|
+ [Work] = Rest,
|
|
|
+ [Rest] = Work,
|
|
|
+};
|
|
|
+
|
|
|
+static const int32_t stage_duration_seconds_map[] = {
|
|
|
+ [Work] = 25 * SECONDS_IN_MINUTE,
|
|
|
+ [Rest] = 5 * SECONDS_IN_MINUTE,
|
|
|
+};
|
|
|
+
|
|
|
+const PomodoroStage default_stage = Work;
|
|
|
+
|
|
|
+/// @brief Container for a time period
|
|
|
typedef struct {
|
|
|
uint8_t seconds;
|
|
|
uint8_t minutes;
|
|
|
- uint8_t hours;
|
|
|
uint32_t total_seconds;
|
|
|
} TimeDifference;
|
|
|
|
|
|
@@ -41,62 +117,115 @@ typedef struct {
|
|
|
} FlippPomodoroState;
|
|
|
|
|
|
/// @brief Calculates difference between two provided timestamps
|
|
|
-/// @param begin
|
|
|
-/// @param end
|
|
|
-/// @return
|
|
|
+/// @param begin - start timestamp of the period
|
|
|
+/// @param end - end timestamp of the period to measure
|
|
|
+/// @return TimeDifference struct
|
|
|
static TimeDifference get_timestamp_difference_seconds(uint32_t begin, uint32_t end) {
|
|
|
const uint32_t duration_seconds = end - begin;
|
|
|
- return (TimeDifference){.total_seconds=duration_seconds};
|
|
|
+
|
|
|
+ uint32_t minutes = (duration_seconds / SECONDS_IN_MINUTE) % SECONDS_IN_MINUTE;
|
|
|
+ uint32_t seconds = duration_seconds % SECONDS_IN_MINUTE;
|
|
|
+
|
|
|
+ return (TimeDifference){.total_seconds=duration_seconds, .minutes=minutes, .seconds=seconds};
|
|
|
}
|
|
|
|
|
|
static void flipp_pomodoro__toggle_stage(FlippPomodoroState* state) {
|
|
|
- state->stage = state->stage == Work ? Rest : Work;
|
|
|
+ furi_assert(state);
|
|
|
+ state->stage = stage_rotaion_map[state->stage];
|
|
|
state->started_at_timestamp = furi_hal_rtc_get_timestamp();
|
|
|
}
|
|
|
|
|
|
static char* flipp_pomodoro__next_stage_label(FlippPomodoroState* state) {
|
|
|
- return state->stage == Work ? "To rest" : "To work";
|
|
|
+ furi_assert(state);
|
|
|
+ return next_stage_label[state->stage];
|
|
|
};
|
|
|
|
|
|
-static TimeDifference flipp_pomodoro__stage_duration(FlippPomodoroState* state) {
|
|
|
- const uint32_t now = furi_hal_rtc_get_timestamp();
|
|
|
- return get_timestamp_difference_seconds(state->started_at_timestamp, now);
|
|
|
-}
|
|
|
|
|
|
static void flipp_pomodoro__destroy(FlippPomodoroState* state) {
|
|
|
+ furi_assert(state);
|
|
|
free(state);
|
|
|
}
|
|
|
|
|
|
+static uint32_t flipp_pomodoro__stage_expires_timestamp(FlippPomodoroState* state) {
|
|
|
+ return state->started_at_timestamp + stage_duration_seconds_map[state->stage];
|
|
|
+}
|
|
|
+
|
|
|
+static TimeDifference flipp_pomodoro__stage_remaining_duration(FlippPomodoroState* state) {
|
|
|
+ const uint32_t now = furi_hal_rtc_get_timestamp();
|
|
|
+ const uint32_t stage_ends_at = flipp_pomodoro__stage_expires_timestamp(state);
|
|
|
+ return get_timestamp_difference_seconds(now, stage_ends_at);
|
|
|
+}
|
|
|
+
|
|
|
+static bool flipp_pomodoro__is_stage_expired(FlippPomodoroState* state) {
|
|
|
+ const uint32_t now = furi_hal_rtc_get_timestamp();
|
|
|
+ const uint32_t expired_by = flipp_pomodoro__stage_expires_timestamp(state);
|
|
|
+ const uint8_t seamless_change_span_seconds = 1;
|
|
|
+ return (now - seamless_change_span_seconds) >= expired_by;
|
|
|
+}
|
|
|
+
|
|
|
static FlippPomodoroState flipp_pomodoro__new() {
|
|
|
const uint32_t now = furi_hal_rtc_get_timestamp();
|
|
|
- const FlippPomodoroState new_state = {.stage=Work, .started_at_timestamp=now};
|
|
|
+ const FlippPomodoroState new_state = {.stage=default_stage, .started_at_timestamp=now};
|
|
|
return new_state;
|
|
|
}
|
|
|
|
|
|
+typedef struct {
|
|
|
+ FlippPomodoroState* state;
|
|
|
+} DrawContext;
|
|
|
+
|
|
|
// Screen is 128x64 px
|
|
|
static void app_draw_callback(Canvas* canvas, void* ctx) {
|
|
|
- // WARNING: place no side-effects into rener cycle
|
|
|
- canvas_clear(canvas);
|
|
|
- FlippPomodoroState* state = ctx;
|
|
|
+ // WARNING: place no side-effects into rener cycle!!
|
|
|
+ DrawContext* draw_context = ctx;
|
|
|
|
|
|
- FuriString* timer_string = furi_string_alloc();
|
|
|
+ const TimeDifference remaining_stage_time = flipp_pomodoro__stage_remaining_duration(draw_context->state);
|
|
|
|
|
|
- const uint32_t now = flipp_pomodoro__stage_duration(state).total_seconds;
|
|
|
-
|
|
|
- furi_string_printf(timer_string, "%lu", now);
|
|
|
+ // Format remaining stage time;
|
|
|
+ FuriString* timer_string = furi_string_alloc();
|
|
|
+ furi_string_printf(timer_string, "%02u:%02u", remaining_stage_time.minutes, remaining_stage_time.seconds);
|
|
|
+ const char* remaining_stage_time_string = furi_string_get_cstr(timer_string);
|
|
|
|
|
|
- elements_text_box(canvas, 50, 20, 30, 50, AlignCenter, AlignCenter, furi_string_get_cstr(timer_string), false);
|
|
|
- elements_button_right(canvas, flipp_pomodoro__next_stage_label(state));
|
|
|
+ // Render interface
|
|
|
+ canvas_clear(canvas);
|
|
|
|
|
|
+ canvas_draw_icon(canvas, 0, 0, stage_background_image[draw_context->state->stage]);
|
|
|
+
|
|
|
+ // Countdown section
|
|
|
+ const uint8_t countdown_box_height = canvas_height(canvas)*0.4;
|
|
|
+ const uint8_t countdown_box_width = canvas_width(canvas)*0.5;
|
|
|
+ const uint8_t countdown_box_x = canvas_width(canvas) - countdown_box_width - 2;
|
|
|
+ const uint8_t countdown_box_y = 0;
|
|
|
+
|
|
|
+ elements_bold_rounded_frame(canvas,
|
|
|
+ countdown_box_x,
|
|
|
+ countdown_box_y,
|
|
|
+ countdown_box_width,
|
|
|
+ countdown_box_height
|
|
|
+ );
|
|
|
+
|
|
|
+ canvas_set_font(canvas, FontBigNumbers);
|
|
|
+ canvas_draw_str_aligned(
|
|
|
+ canvas,
|
|
|
+ countdown_box_x + (countdown_box_width/2),
|
|
|
+ countdown_box_y + (countdown_box_height/2),
|
|
|
+ AlignCenter,
|
|
|
+ AlignCenter,
|
|
|
+ remaining_stage_time_string
|
|
|
+ );
|
|
|
+
|
|
|
+ // Draw layout
|
|
|
+ canvas_set_font(canvas, FontSecondary);
|
|
|
+ elements_button_right(canvas, flipp_pomodoro__next_stage_label(draw_context->state));
|
|
|
+
|
|
|
+
|
|
|
+ // Cleanup
|
|
|
furi_string_free(timer_string);
|
|
|
}
|
|
|
|
|
|
static void clock_tick_callback(void* ctx) {
|
|
|
-
|
|
|
furi_assert(ctx);
|
|
|
FuriMessageQueue* queue = ctx;
|
|
|
Action action = {.type = TimerTickType};
|
|
|
- // It's OK to loose this event if system overloaded
|
|
|
furi_message_queue_put(queue, &action, 0);
|
|
|
}
|
|
|
|
|
|
@@ -134,34 +263,46 @@ int32_t flipp_pomodoro_main(void* p) {
|
|
|
|
|
|
// Configure view port
|
|
|
ViewPort* view_port = view_port_alloc();
|
|
|
- view_port_draw_callback_set(view_port, app_draw_callback, &state);
|
|
|
+ DrawContext draw_context = {.state=&state};
|
|
|
+ view_port_draw_callback_set(view_port, app_draw_callback, &draw_context);
|
|
|
view_port_input_callback_set(view_port, app_input_callback, event_queue);
|
|
|
|
|
|
- FuriTimer* timer = furi_timer_alloc(clock_tick_callback, FuriTimerTypePeriodic, &event_queue);
|
|
|
+ // Initiate timer
|
|
|
+ FuriTimer* timer = furi_timer_alloc(clock_tick_callback, FuriTimerTypePeriodic, event_queue);
|
|
|
+ furi_timer_start(timer, 500);
|
|
|
+
|
|
|
+ NotificationApp* notification_app = furi_record_open(RECORD_NOTIFICATION);
|
|
|
|
|
|
// Register view port in GUI
|
|
|
Gui* gui = furi_record_open(RECORD_GUI);
|
|
|
gui_add_view_port(gui, view_port, GuiLayerFullscreen);
|
|
|
|
|
|
- furi_timer_start(timer, 500);
|
|
|
-
|
|
|
- Action action;
|
|
|
|
|
|
bool running = true;
|
|
|
while(running) {
|
|
|
- if(furi_message_queue_get(event_queue, &action, 200) == FuriStatusOk) {
|
|
|
- switch (action.type) {
|
|
|
- case InputEventType:
|
|
|
- running = input_events_reducer(&state, action.payload);
|
|
|
- break;
|
|
|
- case TimerTickType:
|
|
|
- // TODO: track time is over and make switch
|
|
|
- break;
|
|
|
- default:
|
|
|
- break;
|
|
|
- }
|
|
|
- view_port_update(view_port);
|
|
|
+ Action action;
|
|
|
+ if(furi_message_queue_get(event_queue, &action, 200) != FuriStatusOk) {
|
|
|
+ continue;
|
|
|
+ };
|
|
|
+
|
|
|
+ if(!action.type) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ switch (action.type) {
|
|
|
+ case InputEventType:
|
|
|
+ running = input_events_reducer(&state, action.payload);
|
|
|
+ break;
|
|
|
+ case TimerTickType:
|
|
|
+ if(flipp_pomodoro__is_stage_expired(&state)) {
|
|
|
+ flipp_pomodoro__toggle_stage(&state);
|
|
|
+ notification_message(notification_app, stage_start_notification_sequence_map[state.stage]);
|
|
|
+ };
|
|
|
+ break;
|
|
|
+ default:
|
|
|
+ break;
|
|
|
}
|
|
|
+ view_port_update(view_port); // Only re-draw on event
|
|
|
}
|
|
|
|
|
|
view_port_enabled_set(view_port, false);
|
|
|
@@ -171,6 +312,7 @@ int32_t flipp_pomodoro_main(void* p) {
|
|
|
furi_record_close(RECORD_GUI);
|
|
|
furi_timer_free(timer);
|
|
|
flipp_pomodoro__destroy(&state);
|
|
|
+ furi_record_close(RECORD_NOTIFICATION);
|
|
|
|
|
|
return 0;
|
|
|
}
|