1
1
#include <furi.h>
2
2
#include <furi_hal.h>
3
3
4
+ #include <notification/notification_messages.h>
4
5
#include <gui/gui.h>
5
6
#include <gui/elements.h>
6
7
#include <input/input.h>
9
10
* Just set fap_icon_assets in application.fam and #include {APPID}_icons.h */
10
11
#include "flipp_pomodoro_icons.h"
11
12
13
+ const int SECONDS_IN_MINUTE = 60 ;
14
+
15
+ /// @brief Actions to be processed in a queue
12
16
typedef enum {
13
- TimerTickType ,
17
+ TimerTickType = 42 ,
14
18
InputEventType ,
15
19
} ActionType ;
16
20
21
+ /// @brief Single action contains type and payload
17
22
typedef struct {
18
23
ActionType type ;
19
24
void * payload ;
20
25
} Action ;
21
26
22
- /**
23
- * Flipp Pomodoro state management
24
- */
25
-
26
27
typedef enum {
27
28
Work ,
28
29
Rest ,
29
30
} PomodoroStage ;
30
31
32
+ static const NotificationSequence work_start_notification = {
33
+ & message_display_backlight_on ,
34
+
35
+ & message_vibro_on ,
36
+
37
+ & message_note_b5 ,
38
+ & message_delay_250 ,
39
+
40
+ & message_note_d5 ,
41
+ & message_delay_250 ,
42
+
43
+ & message_sound_off ,
44
+ & message_vibro_off ,
45
+
46
+ & message_green_255 ,
47
+ & message_delay_1000 ,
48
+ & message_green_0 ,
49
+ & message_delay_250 ,
50
+ & message_green_255 ,
51
+ & message_delay_1000 ,
52
+
53
+ NULL ,
54
+ };
55
+
56
+ static const NotificationSequence rest_start_notification = {
57
+ & message_display_backlight_on ,
58
+
59
+ & message_vibro_on ,
60
+
61
+ & message_note_d5 ,
62
+ & message_delay_250 ,
63
+
64
+ & message_note_b5 ,
65
+ & message_delay_250 ,
66
+
67
+ & message_sound_off ,
68
+ & message_vibro_off ,
69
+
70
+ & message_red_255 ,
71
+ & message_delay_1000 ,
72
+ & message_red_0 ,
73
+ & message_delay_250 ,
74
+ & message_red_255 ,
75
+ & message_delay_1000 ,
76
+
77
+ NULL ,
78
+ };
79
+
80
+ static const NotificationSequence * stage_start_notification_sequence_map [] = {
81
+ [Work ] = & work_start_notification ,
82
+ [Rest ] = & rest_start_notification ,
83
+ };
84
+
85
+ static char * next_stage_label [] = {
86
+ [Work ] = "Get Rest" ,
87
+ [Rest ] = "Start Work" ,
88
+ };
89
+
90
+ static const Icon * stage_background_image [] = {
91
+ [Work ] = & I_flipp_pomodoro_work_64 ,
92
+ [Rest ] = & I_flipp_pomodoro_rest_64 ,
93
+ };
94
+
95
+ static const PomodoroStage stage_rotaion_map [] = {
96
+ [Work ] = Rest ,
97
+ [Rest ] = Work ,
98
+ };
99
+
100
+ static const int32_t stage_duration_seconds_map [] = {
101
+ [Work ] = 25 * SECONDS_IN_MINUTE ,
102
+ [Rest ] = 5 * SECONDS_IN_MINUTE ,
103
+ };
104
+
105
+ const PomodoroStage default_stage = Work ;
106
+
107
+ /// @brief Container for a time period
31
108
typedef struct {
32
109
uint8_t seconds ;
33
110
uint8_t minutes ;
34
- uint8_t hours ;
35
111
uint32_t total_seconds ;
36
112
} TimeDifference ;
37
113
@@ -41,62 +117,115 @@ typedef struct {
41
117
} FlippPomodoroState ;
42
118
43
119
/// @brief Calculates difference between two provided timestamps
44
- /// @param begin
45
- /// @param end
46
- /// @return
120
+ /// @param begin - start timestamp of the period
121
+ /// @param end - end timestamp of the period to measure
122
+ /// @return TimeDifference struct
47
123
static TimeDifference get_timestamp_difference_seconds (uint32_t begin , uint32_t end ) {
48
124
const uint32_t duration_seconds = end - begin ;
49
- return (TimeDifference ){.total_seconds = duration_seconds };
125
+
126
+ uint32_t minutes = (duration_seconds / SECONDS_IN_MINUTE ) % SECONDS_IN_MINUTE ;
127
+ uint32_t seconds = duration_seconds % SECONDS_IN_MINUTE ;
128
+
129
+ return (TimeDifference ){.total_seconds = duration_seconds , .minutes = minutes , .seconds = seconds };
50
130
}
51
131
52
132
static void flipp_pomodoro__toggle_stage (FlippPomodoroState * state ) {
53
- state -> stage = state -> stage == Work ? Rest : Work ;
133
+ furi_assert (state );
134
+ state -> stage = stage_rotaion_map [state -> stage ];
54
135
state -> started_at_timestamp = furi_hal_rtc_get_timestamp ();
55
136
}
56
137
57
138
static char * flipp_pomodoro__next_stage_label (FlippPomodoroState * state ) {
58
- return state -> stage == Work ? "To rest" : "To work" ;
139
+ furi_assert (state );
140
+ return next_stage_label [state -> stage ];
59
141
};
60
142
61
- static TimeDifference flipp_pomodoro__stage_duration (FlippPomodoroState * state ) {
62
- const uint32_t now = furi_hal_rtc_get_timestamp ();
63
- return get_timestamp_difference_seconds (state -> started_at_timestamp , now );
64
- }
65
143
66
144
static void flipp_pomodoro__destroy (FlippPomodoroState * state ) {
145
+ furi_assert (state );
67
146
free (state );
68
147
}
69
148
149
+ static uint32_t flipp_pomodoro__stage_expires_timestamp (FlippPomodoroState * state ) {
150
+ return state -> started_at_timestamp + stage_duration_seconds_map [state -> stage ];
151
+ }
152
+
153
+ static TimeDifference flipp_pomodoro__stage_remaining_duration (FlippPomodoroState * state ) {
154
+ const uint32_t now = furi_hal_rtc_get_timestamp ();
155
+ const uint32_t stage_ends_at = flipp_pomodoro__stage_expires_timestamp (state );
156
+ return get_timestamp_difference_seconds (now , stage_ends_at );
157
+ }
158
+
159
+ static bool flipp_pomodoro__is_stage_expired (FlippPomodoroState * state ) {
160
+ const uint32_t now = furi_hal_rtc_get_timestamp ();
161
+ const uint32_t expired_by = flipp_pomodoro__stage_expires_timestamp (state );
162
+ const uint8_t seamless_change_span_seconds = 1 ;
163
+ return (now - seamless_change_span_seconds ) >= expired_by ;
164
+ }
165
+
70
166
static FlippPomodoroState flipp_pomodoro__new () {
71
167
const uint32_t now = furi_hal_rtc_get_timestamp ();
72
- const FlippPomodoroState new_state = {.stage = Work , .started_at_timestamp = now };
168
+ const FlippPomodoroState new_state = {.stage = default_stage , .started_at_timestamp = now };
73
169
return new_state ;
74
170
}
75
171
172
+ typedef struct {
173
+ FlippPomodoroState * state ;
174
+ } DrawContext ;
175
+
76
176
// Screen is 128x64 px
77
177
static void app_draw_callback (Canvas * canvas , void * ctx ) {
78
- // WARNING: place no side-effects into rener cycle
79
- canvas_clear (canvas );
80
- FlippPomodoroState * state = ctx ;
178
+ // WARNING: place no side-effects into rener cycle!!
179
+ DrawContext * draw_context = ctx ;
81
180
82
- FuriString * timer_string = furi_string_alloc ( );
181
+ const TimeDifference remaining_stage_time = flipp_pomodoro__stage_remaining_duration ( draw_context -> state );
83
182
84
- const uint32_t now = flipp_pomodoro__stage_duration (state ).total_seconds ;
85
-
86
- furi_string_printf (timer_string , "%lu" , now );
183
+ // Format remaining stage time;
184
+ FuriString * timer_string = furi_string_alloc ();
185
+ furi_string_printf (timer_string , "%02u:%02u" , remaining_stage_time .minutes , remaining_stage_time .seconds );
186
+ const char * remaining_stage_time_string = furi_string_get_cstr (timer_string );
87
187
88
- elements_text_box ( canvas , 50 , 20 , 30 , 50 , AlignCenter , AlignCenter , furi_string_get_cstr ( timer_string ), false);
89
- elements_button_right (canvas , flipp_pomodoro__next_stage_label ( state ) );
188
+ // Render interface
189
+ canvas_clear (canvas );
90
190
191
+ canvas_draw_icon (canvas , 0 , 0 , stage_background_image [draw_context -> state -> stage ]);
192
+
193
+ // Countdown section
194
+ const uint8_t countdown_box_height = canvas_height (canvas )* 0.4 ;
195
+ const uint8_t countdown_box_width = canvas_width (canvas )* 0.5 ;
196
+ const uint8_t countdown_box_x = canvas_width (canvas ) - countdown_box_width - 2 ;
197
+ const uint8_t countdown_box_y = 0 ;
198
+
199
+ elements_bold_rounded_frame (canvas ,
200
+ countdown_box_x ,
201
+ countdown_box_y ,
202
+ countdown_box_width ,
203
+ countdown_box_height
204
+ );
205
+
206
+ canvas_set_font (canvas , FontBigNumbers );
207
+ canvas_draw_str_aligned (
208
+ canvas ,
209
+ countdown_box_x + (countdown_box_width /2 ),
210
+ countdown_box_y + (countdown_box_height /2 ),
211
+ AlignCenter ,
212
+ AlignCenter ,
213
+ remaining_stage_time_string
214
+ );
215
+
216
+ // Draw layout
217
+ canvas_set_font (canvas , FontSecondary );
218
+ elements_button_right (canvas , flipp_pomodoro__next_stage_label (draw_context -> state ));
219
+
220
+
221
+ // Cleanup
91
222
furi_string_free (timer_string );
92
223
}
93
224
94
225
static void clock_tick_callback (void * ctx ) {
95
-
96
226
furi_assert (ctx );
97
227
FuriMessageQueue * queue = ctx ;
98
228
Action action = {.type = TimerTickType };
99
- // It's OK to loose this event if system overloaded
100
229
furi_message_queue_put (queue , & action , 0 );
101
230
}
102
231
@@ -134,34 +263,46 @@ int32_t flipp_pomodoro_main(void* p) {
134
263
135
264
// Configure view port
136
265
ViewPort * view_port = view_port_alloc ();
137
- view_port_draw_callback_set (view_port , app_draw_callback , & state );
266
+ DrawContext draw_context = {.state = & state };
267
+ view_port_draw_callback_set (view_port , app_draw_callback , & draw_context );
138
268
view_port_input_callback_set (view_port , app_input_callback , event_queue );
139
269
140
- FuriTimer * timer = furi_timer_alloc (clock_tick_callback , FuriTimerTypePeriodic , & event_queue );
270
+ // Initiate timer
271
+ FuriTimer * timer = furi_timer_alloc (clock_tick_callback , FuriTimerTypePeriodic , event_queue );
272
+ furi_timer_start (timer , 500 );
273
+
274
+ NotificationApp * notification_app = furi_record_open (RECORD_NOTIFICATION );
141
275
142
276
// Register view port in GUI
143
277
Gui * gui = furi_record_open (RECORD_GUI );
144
278
gui_add_view_port (gui , view_port , GuiLayerFullscreen );
145
279
146
- furi_timer_start (timer , 500 );
147
-
148
- Action action ;
149
280
150
281
bool running = true;
151
282
while (running ) {
152
- if (furi_message_queue_get (event_queue , & action , 200 ) == FuriStatusOk ) {
153
- switch (action .type ) {
154
- case InputEventType :
155
- running = input_events_reducer (& state , action .payload );
156
- break ;
157
- case TimerTickType :
158
- // TODO: track time is over and make switch
159
- break ;
160
- default :
161
- break ;
162
- }
163
- view_port_update (view_port );
283
+ Action action ;
284
+ if (furi_message_queue_get (event_queue , & action , 200 ) != FuriStatusOk ) {
285
+ continue ;
286
+ };
287
+
288
+ if (!action .type ) {
289
+ continue ;
290
+ }
291
+
292
+ switch (action .type ) {
293
+ case InputEventType :
294
+ running = input_events_reducer (& state , action .payload );
295
+ break ;
296
+ case TimerTickType :
297
+ if (flipp_pomodoro__is_stage_expired (& state )) {
298
+ flipp_pomodoro__toggle_stage (& state );
299
+ notification_message (notification_app , stage_start_notification_sequence_map [state .stage ]);
300
+ };
301
+ break ;
302
+ default :
303
+ break ;
164
304
}
305
+ view_port_update (view_port ); // Only re-draw on event
165
306
}
166
307
167
308
view_port_enabled_set (view_port , false);
@@ -171,6 +312,7 @@ int32_t flipp_pomodoro_main(void* p) {
171
312
furi_record_close (RECORD_GUI );
172
313
furi_timer_free (timer );
173
314
flipp_pomodoro__destroy (& state );
315
+ furi_record_close (RECORD_NOTIFICATION );
174
316
175
317
return 0 ;
176
318
}
0 commit comments