#include #include #include #include #include #include /* Magic happens here -- this file is generated by fbt. * 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 = 42, InputEventType, } ActionType; /// @brief Single action contains type and payload typedef struct { ActionType type; void* payload; } Action; 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; uint32_t total_seconds; } TimeDifference; typedef struct { PomodoroStage stage; uint32_t started_at_timestamp; } FlippPomodoroState; /// @brief Calculates difference between two provided timestamps /// @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; 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) { 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) { furi_assert(state); return next_stage_label[state->stage]; }; 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=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!! DrawContext* draw_context = ctx; const TimeDifference remaining_stage_time = flipp_pomodoro__stage_remaining_duration(draw_context->state); // 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); // 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}; furi_message_queue_put(queue, &action, 0); } static void app_input_callback(InputEvent* input_event, void* ctx) { furi_assert(ctx); Action action = {.type=InputEventType, .payload=input_event}; FuriMessageQueue* event_queue = ctx; furi_message_queue_put(event_queue, &action, FuriWaitForever); }; static bool input_events_reducer (FlippPomodoroState* state, InputEvent* input_event) { bool keep_running = true; if((input_event->type == InputTypePress) || (input_event->type == InputTypeRepeat)) { switch(input_event->key) { case InputKeyRight: flipp_pomodoro__toggle_stage(state); break; case InputKeyBack: keep_running = false; break; default: break; } } return keep_running; } int32_t flipp_pomodoro_main(void* p) { UNUSED(p); FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(Action)); FlippPomodoroState state = flipp_pomodoro__new(); // Configure view port ViewPort* view_port = view_port_alloc(); 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); // 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); bool running = true; while(running) { 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); gui_remove_view_port(gui, view_port); view_port_free(view_port); furi_message_queue_free(event_queue); furi_record_close(RECORD_GUI); furi_timer_free(timer); flipp_pomodoro__destroy(&state); furi_record_close(RECORD_NOTIFICATION); return 0; }