├── cntdown_timer.png ├── README.md ├── utils ├── utils.h └── utils.c ├── application.fam ├── app.h ├── views ├── countdown_view.h └── countdown_view.c └── app.c /cntdown_timer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0w0mewo/fpz_cntdown_timer/HEAD/cntdown_timer.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Simple count down timer application for flipper zero 2 | 3 | ### How to use 4 | `up/down`: set second/minute/hour value. 5 | 6 | `ok`: start/stop counting. 7 | 8 | `long press on ok`: stop counting and reset counter. 9 | 10 | `left/right`: select second/minute/hour value. 11 | 12 | -------------------------------------------------------------------------------- /utils/utils.h: -------------------------------------------------------------------------------- 1 | #ifndef __UTILS_H__ 2 | #define __UTILS_H__ 3 | #include 4 | #include 5 | 6 | void notification_beep_once(); 7 | void notification_off(); 8 | void notification_timeup(); 9 | 10 | void parse_sec_to_time_str(char *buffer, size_t len, int32_t sec); 11 | 12 | #endif // __UTILS_H__ -------------------------------------------------------------------------------- /application.fam: -------------------------------------------------------------------------------- 1 | App( 2 | appid="cntdown_tim", 3 | name="Count Down Timer", 4 | apptype=FlipperAppType.EXTERNAL, 5 | entry_point="app_main", 6 | cdefines=["APP_COUNT_DOWN_TIMER"], 7 | requires=[ 8 | "gui", 9 | ], 10 | stack_size=2 * 1024, 11 | order=20, 12 | fap_icon="cntdown_timer.png", 13 | fap_category="Tools", 14 | fap_version="1.5" 15 | ) 16 | -------------------------------------------------------------------------------- /app.h: -------------------------------------------------------------------------------- 1 | #ifndef __APP_H__ 2 | #define __APP_H__ 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | // app 9 | typedef struct { 10 | Gui* gui; // gui object 11 | ViewDispatcher* view_dispatcher; // view dispacther of the gui 12 | 13 | // views 14 | CountDownTimView* helloworld_view; 15 | 16 | } CountDownTimerApp; 17 | 18 | CountDownTimerApp* countdown_app_new(void); 19 | void countdown_app_delete(CountDownTimerApp* app); 20 | void countdown_app_run(CountDownTimerApp* app); 21 | 22 | #endif -------------------------------------------------------------------------------- /views/countdown_view.h: -------------------------------------------------------------------------------- 1 | #ifndef __COUNTDOWN_VIEW_H__ 2 | #define __COUNTDOWN_VIEW_H__ 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #define SCREEN_WIDTH 128 10 | #define SCREEN_HEIGHT 64 11 | #define SCREEN_CENTER_X (SCREEN_WIDTH / 2) 12 | #define SCREEN_CENTER_Y (SCREEN_HEIGHT / 2) 13 | 14 | #define INIT_COUNT 60 15 | 16 | typedef enum { 17 | CountDownTimerMinuteUp, 18 | CountDownTimerMinuteDown, 19 | CountDownTimerSecDown, 20 | CountDownTimerSecUp, 21 | CountDownTimerHourUp, 22 | CountDownTimerHourDown, 23 | CountDownTimerReset, 24 | CountDownTimerTimeUp, 25 | CountDownTimerToggleCounting, 26 | } CountDownViewCmd; 27 | 28 | typedef enum { 29 | CountDownTimerSelectSec, 30 | CountDownTimerSelectMinute, 31 | CountDownTimerSelectHour, 32 | } CountDownViewSelect; 33 | 34 | typedef struct { 35 | int32_t count; 36 | int32_t saved_count_setting; 37 | CountDownViewSelect select; // setting 38 | } CountDownModel; 39 | 40 | typedef struct { 41 | View* view; 42 | FuriTimer* timer; // 1Hz tick timer 43 | bool counting; 44 | 45 | } CountDownTimView; 46 | 47 | // functions 48 | // allocate helloworld view 49 | CountDownTimView* countdown_timer_view_new(); 50 | 51 | // delete helloworld view 52 | void countdown_timer_view_delete(CountDownTimView* cdv); 53 | 54 | // return view 55 | View* countdown_timer_view_get_view(CountDownTimView* cdv); 56 | 57 | void countdown_timer_view_state_reset(CountDownTimView* cdv); // set initial state 58 | void countdown_timer_state_toggle(CountDownTimView* cdv); 59 | #endif // __COUNTDOWN_VIEW_H__ -------------------------------------------------------------------------------- /utils/utils.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include "utils.h" 3 | 4 | static const NotificationSequence sequence_beep = { 5 | &message_blue_255, 6 | &message_note_d5, 7 | &message_delay_100, 8 | &message_sound_off, 9 | 10 | NULL, 11 | }; 12 | 13 | static const NotificationSequence sequence_timeup = { 14 | &message_force_display_brightness_setting_1f, 15 | &message_display_backlight_on, 16 | &message_vibro_on, 17 | 18 | &message_note_c8, 19 | &message_delay_50, 20 | &message_sound_off, 21 | &message_delay_50, 22 | &message_delay_25, 23 | 24 | &message_note_c8, 25 | &message_delay_50, 26 | &message_sound_off, 27 | &message_delay_50, 28 | &message_delay_25, 29 | 30 | &message_note_c8, 31 | &message_delay_50, 32 | &message_sound_off, 33 | &message_delay_50, 34 | &message_delay_25, 35 | 36 | &message_note_c8, 37 | &message_delay_50, 38 | &message_sound_off, 39 | &message_delay_50, 40 | &message_delay_25, 41 | 42 | &message_vibro_off, 43 | &message_display_backlight_off, 44 | &message_delay_500, 45 | 46 | NULL, 47 | }; 48 | 49 | void notification_beep_once() { 50 | notification_message(furi_record_open(RECORD_NOTIFICATION), &sequence_beep); 51 | notification_off(); 52 | } 53 | 54 | void notification_off() { 55 | furi_record_close(RECORD_NOTIFICATION); 56 | } 57 | 58 | void notification_timeup() { 59 | notification_message(furi_record_open(RECORD_NOTIFICATION), &sequence_timeup); 60 | } 61 | 62 | void parse_sec_to_time_str(char* buffer, size_t len, int32_t sec) { 63 | snprintf( 64 | buffer, 65 | len, 66 | "%02ld:%02ld:%02ld", 67 | (sec % (60 * 60 * 24)) / (60 * 60), // hour 68 | (sec % (60 * 60)) / 60, // minute 69 | sec % 60); // second 70 | } 71 | -------------------------------------------------------------------------------- /app.c: -------------------------------------------------------------------------------- 1 | #include "views/countdown_view.h" 2 | #include "app.h" 3 | 4 | static void register_view(ViewDispatcher* dispatcher, View* view, uint32_t viewid); 5 | 6 | int32_t app_main(void* p) { 7 | UNUSED(p); 8 | 9 | CountDownTimerApp* app = countdown_app_new(); 10 | 11 | countdown_app_run(app); 12 | 13 | countdown_app_delete(app); 14 | 15 | return 0; 16 | } 17 | 18 | static uint32_t view_exit(void* ctx) { 19 | furi_assert(ctx); 20 | 21 | return VIEW_NONE; 22 | } 23 | 24 | CountDownTimerApp* countdown_app_new(void) { 25 | CountDownTimerApp* app = (CountDownTimerApp*)(malloc(sizeof(CountDownTimerApp))); 26 | 27 | // 1.1 open gui 28 | app->gui = furi_record_open(RECORD_GUI); 29 | 30 | // 2.1 setup view dispatcher 31 | app->view_dispatcher = view_dispatcher_alloc(); 32 | view_dispatcher_enable_queue(app->view_dispatcher); 33 | 34 | // 2.2 attach view dispatcher to gui 35 | view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen); 36 | 37 | // 2.3 attach views to the dispatcher 38 | // helloworld view 39 | app->helloworld_view = countdown_timer_view_new(); 40 | register_view(app->view_dispatcher, countdown_timer_view_get_view(app->helloworld_view), 0xff); 41 | 42 | // 2.5 switch to default view 43 | view_dispatcher_switch_to_view(app->view_dispatcher, 0xff); 44 | 45 | return app; 46 | } 47 | 48 | void countdown_app_delete(CountDownTimerApp* app) { 49 | furi_assert(app); 50 | 51 | // delete views 52 | view_dispatcher_remove_view(app->view_dispatcher, 0xff); 53 | countdown_timer_view_delete(app->helloworld_view); // hello world view 54 | 55 | // delete view dispatcher 56 | view_dispatcher_free(app->view_dispatcher); 57 | furi_record_close(RECORD_GUI); 58 | 59 | // self 60 | free(app); 61 | } 62 | 63 | void countdown_app_run(CountDownTimerApp* app) { 64 | view_dispatcher_run(app->view_dispatcher); 65 | } 66 | 67 | static void register_view(ViewDispatcher* dispatcher, View* view, uint32_t viewid) { 68 | view_dispatcher_add_view(dispatcher, viewid, view); 69 | 70 | view_set_previous_callback(view, view_exit); 71 | } 72 | -------------------------------------------------------------------------------- /views/countdown_view.c: -------------------------------------------------------------------------------- 1 | #include "countdown_view.h" 2 | #include "../utils/utils.h" 3 | 4 | // internal 5 | static void handle_misc_cmd(CountDownTimView* hw, CountDownViewCmd cmd); 6 | static void handle_time_setting_updown(CountDownTimView* cdv, CountDownViewCmd cmd); 7 | static void handle_time_setting_select(InputKey key, CountDownTimView* cdv); 8 | static void draw_selection(Canvas* canvas, CountDownViewSelect selection); 9 | 10 | static void countdown_timer_start_counting(CountDownTimView* cdv); 11 | static void countdown_timer_pause_counting(CountDownTimView* cdv); 12 | 13 | // callbacks 14 | static void countdown_timer_view_on_enter(void* ctx); 15 | static void countdown_timer_view_on_draw(Canvas* canvas, void* ctx); 16 | static bool countdown_timer_view_on_input(InputEvent* event, void* ctx); 17 | static void timer_cb(void* ctx); 18 | 19 | CountDownTimView* countdown_timer_view_new() { 20 | CountDownTimView* cdv = (CountDownTimView*)(malloc(sizeof(CountDownTimView))); 21 | 22 | cdv->view = view_alloc(); 23 | 24 | cdv->timer = furi_timer_alloc(timer_cb, FuriTimerTypePeriodic, cdv); 25 | 26 | cdv->counting = false; 27 | 28 | view_set_context(cdv->view, cdv); 29 | 30 | view_allocate_model(cdv->view, ViewModelTypeLocking, sizeof(CountDownModel)); 31 | 32 | view_set_draw_callback(cdv->view, countdown_timer_view_on_draw); 33 | view_set_input_callback(cdv->view, countdown_timer_view_on_input); 34 | view_set_enter_callback(cdv->view, countdown_timer_view_on_enter); 35 | 36 | return cdv; 37 | } 38 | 39 | void countdown_timer_view_delete(CountDownTimView* cdv) { 40 | furi_assert(cdv); 41 | 42 | view_free(cdv->view); 43 | furi_timer_stop(cdv->timer); 44 | furi_timer_free(cdv->timer); 45 | 46 | free(cdv); 47 | } 48 | 49 | View* countdown_timer_view_get_view(CountDownTimView* cdv) { 50 | return cdv->view; 51 | } 52 | 53 | void countdown_timer_view_state_reset(CountDownTimView* cdv) { 54 | cdv->counting = false; 55 | 56 | with_view_model( 57 | cdv->view, CountDownModel * model, { model->count = model->saved_count_setting; }, true) 58 | } 59 | 60 | void countdown_timer_state_toggle(CountDownTimView* cdv) { 61 | bool on = cdv->counting; 62 | if(!on) { 63 | countdown_timer_start_counting(cdv); 64 | } else { 65 | countdown_timer_pause_counting(cdv); 66 | } 67 | 68 | cdv->counting = !on; 69 | } 70 | 71 | // on enter callback, CountDownTimView as ctx 72 | static void countdown_timer_view_on_enter(void* ctx) { 73 | furi_assert(ctx); 74 | 75 | CountDownTimView* cdv = (CountDownTimView*)ctx; 76 | 77 | // set current count to a initial value 78 | with_view_model( 79 | cdv->view, 80 | CountDownModel * model, 81 | { 82 | model->count = INIT_COUNT; 83 | model->saved_count_setting = INIT_COUNT; 84 | }, 85 | true); 86 | } 87 | 88 | // view draw callback, CountDownModel as ctx 89 | static void countdown_timer_view_on_draw(Canvas* canvas, void* ctx) { 90 | furi_assert(ctx); 91 | CountDownModel* model = (CountDownModel*)ctx; 92 | 93 | char buffer[64]; 94 | 95 | int32_t count = model->count; 96 | int32_t expected_count = model->saved_count_setting; 97 | furi_check(expected_count > 0, "expected_count < 0"); 98 | 99 | CountDownViewSelect select = model->select; 100 | 101 | // elements_frame(canvas, 0, 0, SCREEN_WIDTH, SCREEN_HEIGHT); 102 | 103 | canvas_set_font(canvas, FontBigNumbers); 104 | draw_selection(canvas, select); 105 | 106 | parse_sec_to_time_str(buffer, sizeof(buffer), count); 107 | canvas_draw_str_aligned( 108 | canvas, SCREEN_CENTER_X, SCREEN_CENTER_Y, AlignCenter, AlignCenter, buffer); 109 | 110 | elements_progress_bar(canvas, 0, 0, SCREEN_WIDTH, (1.0 * count / expected_count)); 111 | } 112 | 113 | // keys input event callback, CountDownTimView as ctx 114 | static bool countdown_timer_view_on_input(InputEvent* event, void* ctx) { 115 | furi_assert(ctx); 116 | 117 | CountDownTimView* hw = (CountDownTimView*)ctx; 118 | 119 | if(event->type == InputTypeShort || event->type == InputTypeRepeat) { 120 | switch(event->key) { 121 | case InputKeyUp: 122 | case InputKeyDown: 123 | case InputKeyRight: 124 | case InputKeyLeft: 125 | handle_time_setting_select(event->key, hw); 126 | break; 127 | 128 | case InputKeyOk: 129 | if(event->type == InputTypeShort) { 130 | handle_misc_cmd(hw, CountDownTimerToggleCounting); 131 | } 132 | break; 133 | 134 | default: 135 | break; 136 | } 137 | 138 | return true; 139 | } 140 | 141 | if(event->type == InputTypeLong) { 142 | switch(event->key) { 143 | case InputKeyOk: 144 | handle_misc_cmd(hw, CountDownTimerReset); 145 | break; 146 | 147 | case InputKeyBack: 148 | return false; 149 | break; 150 | 151 | default: 152 | break; 153 | } 154 | 155 | return true; 156 | } 157 | 158 | return false; 159 | } 160 | 161 | static void timer_cb(void* ctx) { 162 | furi_assert(ctx); 163 | 164 | CountDownTimView* cdv = (CountDownTimView*)ctx; 165 | 166 | int32_t count; 167 | bool timeup = false; 168 | 169 | // decrement counter 170 | with_view_model( 171 | cdv->view, 172 | CountDownModel * model, 173 | { 174 | count = model->count; 175 | count--; 176 | 177 | // check timeup 178 | if(count <= 0) { 179 | count = 0; 180 | timeup = true; 181 | } 182 | 183 | model->count = count; 184 | }, 185 | true); 186 | 187 | if(timeup) { 188 | handle_misc_cmd(cdv, CountDownTimerTimeUp); 189 | } 190 | } 191 | 192 | static void handle_time_setting_updown(CountDownTimView* cdv, CountDownViewCmd cmd) { 193 | int32_t count; 194 | 195 | with_view_model( 196 | cdv->view, 197 | CountDownModel * model, 198 | { 199 | count = model->count; 200 | switch(cmd) { 201 | case CountDownTimerMinuteUp: 202 | count += 60; 203 | break; 204 | case CountDownTimerMinuteDown: 205 | count -= 60; 206 | break; 207 | case CountDownTimerHourDown: 208 | count -= 3600; 209 | break; 210 | case CountDownTimerHourUp: 211 | count += 3600; 212 | break; 213 | case CountDownTimerSecUp: 214 | count++; 215 | break; 216 | case CountDownTimerSecDown: 217 | count--; 218 | break; 219 | default: 220 | break; 221 | } 222 | 223 | count = MAX(count, 1); 224 | 225 | // update count state 226 | model->count = count; 227 | 228 | // save the count time setting 229 | model->saved_count_setting = count; 230 | }, 231 | true); 232 | } 233 | 234 | static void handle_misc_cmd(CountDownTimView* hw, CountDownViewCmd cmd) { 235 | switch(cmd) { 236 | case CountDownTimerTimeUp: 237 | notification_timeup(); 238 | break; 239 | 240 | case CountDownTimerReset: 241 | furi_timer_stop(hw->timer); 242 | countdown_timer_view_state_reset(hw); 243 | notification_off(); 244 | 245 | break; 246 | 247 | case CountDownTimerToggleCounting: 248 | countdown_timer_state_toggle(hw); 249 | break; 250 | 251 | default: 252 | break; 253 | } 254 | 255 | return; 256 | } 257 | 258 | static void handle_time_setting_select(InputKey key, CountDownTimView* cdv) { 259 | bool counting = cdv->counting; 260 | CountDownViewCmd setting_cmd = CountDownTimerSecUp; 261 | CountDownViewSelect selection; 262 | 263 | if(counting) { 264 | return; 265 | } 266 | 267 | // load current selection from model context 268 | with_view_model( 269 | cdv->view, CountDownModel * model, { selection = model->select; }, false); 270 | 271 | // select 272 | switch(key) { 273 | case InputKeyUp: 274 | switch(selection) { 275 | case CountDownTimerSelectSec: 276 | setting_cmd = CountDownTimerSecUp; 277 | break; 278 | case CountDownTimerSelectMinute: 279 | setting_cmd = CountDownTimerMinuteUp; 280 | break; 281 | case CountDownTimerSelectHour: 282 | setting_cmd = CountDownTimerHourUp; 283 | break; 284 | } 285 | 286 | handle_time_setting_updown(cdv, setting_cmd); 287 | break; 288 | 289 | case InputKeyDown: 290 | switch(selection) { 291 | case CountDownTimerSelectSec: 292 | setting_cmd = CountDownTimerSecDown; 293 | break; 294 | case CountDownTimerSelectMinute: 295 | setting_cmd = CountDownTimerMinuteDown; 296 | break; 297 | case CountDownTimerSelectHour: 298 | setting_cmd = CountDownTimerHourDown; 299 | break; 300 | } 301 | 302 | handle_time_setting_updown(cdv, setting_cmd); 303 | break; 304 | 305 | case InputKeyRight: 306 | selection = (3 + selection - 1) % 3; 307 | break; 308 | 309 | case InputKeyLeft: 310 | selection = (3 + selection + 1) % 3; 311 | break; 312 | 313 | default: 314 | break; 315 | } 316 | 317 | // save selection to model context 318 | with_view_model( 319 | cdv->view, CountDownModel * model, { model->select = selection; }, false); 320 | } 321 | 322 | static void draw_selection(Canvas* canvas, CountDownViewSelect selection) { 323 | switch(selection) { 324 | case CountDownTimerSelectSec: 325 | elements_slightly_rounded_box(canvas, SCREEN_CENTER_X + 25, SCREEN_CENTER_Y + 11, 24, 2); 326 | break; 327 | case CountDownTimerSelectMinute: 328 | elements_slightly_rounded_box(canvas, SCREEN_CENTER_X - 10, SCREEN_CENTER_Y + 11, 21, 2); 329 | break; 330 | case CountDownTimerSelectHour: 331 | elements_slightly_rounded_box(canvas, SCREEN_CENTER_X - 47, SCREEN_CENTER_Y + 11, 24, 2); 332 | break; 333 | } 334 | } 335 | 336 | static void countdown_timer_start_counting(CountDownTimView* cdv) { 337 | furi_timer_start(cdv->timer, furi_kernel_get_tick_frequency() * 1); // 1s 338 | } 339 | 340 | static void countdown_timer_pause_counting(CountDownTimView* cdv) { 341 | furi_timer_stop(cdv->timer); 342 | notification_off(); 343 | } --------------------------------------------------------------------------------