battery-minus.c (18997B)
1 /* 2 * Copyright (c) 2015-2016, Natacha Porté 3 * 4 * Permission to use, copy, modify, and distribute this software for any 5 * purpose with or without fee is hereby granted, provided that the above 6 * copyright notice and this permission notice appear in all copies. 7 * 8 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 */ 16 17 #include <inttypes.h> 18 #include <pebble.h> 19 #include "dict_tools.h" 20 #include "simple_dialog.h" 21 #include "storage.h" 22 23 #undef DISPLAY_TEST_DATA 24 25 static Window *window; 26 static SimpleMenuLayer *menu_layer; 27 static SimpleMenuSection menu_section; 28 static SimpleMenuItem menu_items[PAGE_LENGTH + 2]; 29 30 static struct event current_page[PAGE_LENGTH]; 31 static const char *titles[PAGE_LENGTH]; 32 static const char *dates[PAGE_LENGTH]; 33 static int cfg_wakeup_time = -1; 34 static char send_status[64]; 35 36 static void 37 do_start_worker(int index, void *context); 38 39 static void 40 do_stop_worker(int index, void *context); 41 42 /************* 43 * UTILITIES * 44 *************/ 45 46 static void 47 close_app(void) { 48 window_stack_pop_all(true); 49 } 50 51 static unsigned 52 first_index(struct event *page, size_t page_length) { 53 unsigned j; 54 55 for (j = 1; 56 j < page_length && page[j - 1].time < page[j].time; 57 j += 1); 58 if (j >= page_length || !page[j].time) j = 0; 59 60 return j; 61 } 62 63 static void 64 mark_menu_dirty(void) { 65 if (!menu_layer) return; 66 layer_mark_dirty(simple_menu_layer_get_layer(menu_layer)); 67 } 68 69 /********************** 70 * DATA UPLOAD TO WEB * 71 **********************/ 72 73 #define MSG_KEY_LAST_SENT 110 74 #define MSG_KEY_LAST_POSTED 120 75 #define MSG_KEY_DATA_TIME 210 76 #define MSG_KEY_DATA_LINE 220 77 #define MSG_KEY_CFG_WAKEUP_TIME 320 78 79 static unsigned sent_index; 80 static unsigned sent_done; 81 static time_t sent_last_key; 82 83 static const char keyword_anomalous[] = "error"; 84 static const char keyword_charge_start[] = "charge"; 85 static const char keyword_charge_stop[] = "dischg"; 86 static const char keyword_charging[] = "+"; 87 static const char keyword_discharging[] = "-"; 88 static const char keyword_unknown[] = "unknown"; 89 static const char keyword_start[] = "start"; 90 static const char keyword_start_charging[] = "start+"; 91 static const char keyword_stop[] = "stop"; 92 static const char keyword_stop_charging[] = "stop+"; 93 94 static bool 95 event_csv_image(char *buffer, size_t size, struct event *event) { 96 struct tm *tm; 97 size_t ret; 98 int i; 99 const char *keyword; 100 uint8_t int_1, int_2; 101 bool has_int_2; 102 103 if (!buffer || !event) return false; 104 105 tm = gmtime(&event->time); 106 if (!tm) { 107 APP_LOG(APP_LOG_LEVEL_ERROR, "event_csv_image: " 108 "Unable to get UTC time for %" PRIi32, event->time); 109 return false; 110 } 111 112 ret = strftime(buffer, size, "%FT%TZ", tm); 113 if (!ret) { 114 APP_LOG(APP_LOG_LEVEL_ERROR, "event_csv_image: " 115 "Unable to build RFC-3339 representation of %" PRIi32, 116 event->time); 117 return false; 118 } 119 120 if (ret >= size) { 121 APP_LOG(APP_LOG_LEVEL_ERROR, "event_csv_image: " 122 "Unexpected returned value %zu of strftime on buffer %zu", 123 ret, size); 124 return false; 125 } 126 127 switch (event->before) { 128 case UNKNOWN: 129 keyword = keyword_unknown; 130 int_1 = event->after; 131 has_int_2 = false; 132 break; 133 134 case APP_STARTED: 135 keyword = (event->after & 0x80) 136 ? keyword_start_charging : keyword_start; 137 int_1 = event->after & 0x7f; 138 has_int_2 = false; 139 break; 140 141 case APP_CLOSED: 142 keyword = (event->after & 0x80) 143 ? keyword_stop_charging : keyword_stop; 144 int_1 = event->after & 0x7f; 145 has_int_2 = false; 146 break; 147 148 case ANOMALOUS_VALUE: 149 keyword = keyword_anomalous; 150 int_1 = event->after; 151 has_int_2 = false; 152 break; 153 154 default: 155 keyword = (event->before & 0x80) 156 ? ((event->after & 0x80) 157 ? keyword_charging : keyword_charge_stop) 158 : ((event->after & 0x80) 159 ? keyword_charge_start : keyword_discharging); 160 int_1 = event->after & 0x7f; 161 int_2 = event->before & 0x7f; 162 has_int_2 = true; 163 break; 164 } 165 166 if (has_int_2) 167 i = snprintf(buffer + ret, size - ret, 168 ",%s,%" PRIu8 ",%" PRIu8, keyword, int_1, int_2); 169 else 170 i = snprintf(buffer + ret, size - ret, 171 ",%s,%" PRIu8, keyword, int_1); 172 173 if (i <= 0) { 174 APP_LOG(APP_LOG_LEVEL_ERROR, "event_csv_image: " 175 "Unexpected return value %d from snprintf", i); 176 return false; 177 } 178 179 return true; 180 } 181 182 static bool 183 send_event(struct event *event) { 184 AppMessageResult msg_result; 185 DictionaryIterator *iter; 186 DictionaryResult dict_result; 187 char buffer[64]; 188 bool result = true; 189 190 if (!event) return false; 191 192 if (!event_csv_image(buffer, sizeof buffer, event)) 193 return false; 194 195 msg_result = app_message_outbox_begin(&iter); 196 if (msg_result) { 197 APP_LOG(APP_LOG_LEVEL_ERROR, 198 "send_event: app_message_outbox_begin returned %d", 199 (int)msg_result); 200 return false; 201 } 202 203 dict_result = dict_write_int(iter, MSG_KEY_DATA_TIME, 204 &event->time, sizeof event->time, true); 205 if (dict_result != DICT_OK) { 206 APP_LOG(APP_LOG_LEVEL_ERROR, 207 "send_event: [%d] unable to add data time %" PRIi32, 208 (int)dict_result, event->time); 209 result = false; 210 } 211 212 dict_result = dict_write_cstring(iter, MSG_KEY_DATA_LINE, buffer); 213 if (dict_result != DICT_OK) { 214 APP_LOG(APP_LOG_LEVEL_ERROR, 215 "send_event: [%d] unable to add data line \"%s\"", 216 (int)dict_result, buffer); 217 result = false; 218 } 219 220 msg_result = app_message_outbox_send(); 221 if (msg_result) { 222 APP_LOG(APP_LOG_LEVEL_ERROR, 223 "send_event: app_mesage_outbox_send returned %d", 224 (int)msg_result); 225 result = false; 226 } 227 228 return result; 229 } 230 231 static void 232 handle_nothing_to_do(void) { 233 if (launch_reason() == APP_LAUNCH_WAKEUP) 234 close_app(); 235 else { 236 snprintf(send_status, sizeof send_status, "Done (0)"); 237 mark_menu_dirty(); 238 } 239 } 240 241 static void 242 handle_last_sent(Tuple *tuple) { 243 time_t t = tuple_int(tuple); 244 245 sent_index = first_index(current_page, PAGE_LENGTH); 246 247 if (!current_page[sent_index].time) { 248 /* empty page */ 249 handle_nothing_to_do(); 250 return; 251 } 252 253 if (t) 254 while (current_page[sent_index].time <= t) { 255 unsigned next_index = (sent_index + 1) % PAGE_LENGTH; 256 if (current_page[sent_index].time 257 > current_page[next_index].time) { 258 /* end of page reached without match */ 259 handle_nothing_to_do(); 260 return; 261 } 262 sent_index = next_index; 263 } 264 265 snprintf(send_status, sizeof send_status, "0 sent"); 266 mark_menu_dirty(); 267 268 send_event(current_page + sent_index); 269 } 270 271 static void 272 inbox_received_handler(DictionaryIterator *iterator, void *context) { 273 Tuple *tuple; 274 (void)context; 275 276 for (tuple = dict_read_first(iterator); 277 tuple; 278 tuple = dict_read_next(iterator)) { 279 switch (tuple->key) { 280 case MSG_KEY_LAST_SENT: 281 handle_last_sent(tuple); 282 break; 283 284 case MSG_KEY_LAST_POSTED: 285 if (tuple_int(tuple) == sent_last_key 286 && launch_reason() == APP_LAUNCH_WAKEUP) { 287 close_app(); 288 } 289 break; 290 291 case MSG_KEY_CFG_WAKEUP_TIME: 292 cfg_wakeup_time = tuple_int(tuple); 293 persist_write_int(MSG_KEY_CFG_WAKEUP_TIME, 294 cfg_wakeup_time + 1); 295 break; 296 297 default: 298 APP_LOG(APP_LOG_LEVEL_ERROR, 299 "Unknown key %" PRIu32 " in received message", 300 tuple->key); 301 break; 302 } 303 } 304 } 305 306 static void 307 outbox_sent_handler(DictionaryIterator *iterator, void *context) { 308 unsigned next_index = (sent_index + 1) % PAGE_LENGTH; 309 (void)iterator; 310 (void)context; 311 312 sent_done += 1; 313 314 if (current_page[sent_index].time <= current_page[next_index].time) { 315 sent_index = next_index; 316 send_event(current_page + next_index); 317 snprintf(send_status, sizeof send_status, "%u sent", 318 sent_done); 319 } else { 320 sent_last_key = current_page[sent_index].time; 321 snprintf(send_status, sizeof send_status, "Done (%u)", 322 sent_done); 323 mark_menu_dirty(); 324 } 325 } 326 327 static void 328 outbox_failed_handler(DictionaryIterator *iterator, AppMessageResult reason, 329 void *context) { 330 (void)iterator; 331 (void)context; 332 APP_LOG(APP_LOG_LEVEL_ERROR, "Outbox failed: 0x%x", (unsigned)reason); 333 if (launch_reason() == APP_LAUNCH_WAKEUP) 334 close_app(); 335 snprintf(send_status, sizeof send_status, "Outbox failed 0x%x", 336 (unsigned)reason); 337 } 338 339 /************* 340 * MENU ITEM * 341 *************/ 342 343 #define SET_BUF(dest, src) (strcpy(dest, src ""), (int)(sizeof src) - 1) 344 345 static const char * 346 strdup(char *buffer) { 347 size_t length = strlen(buffer); 348 char *result = malloc(length + 1); 349 if (result) memcpy(result, buffer, length + 1); 350 return result; 351 } 352 353 static void 354 init_strings(void) { 355 char buffer[256]; 356 int ret; 357 struct tm *tm; 358 unsigned j = first_index(current_page, PAGE_LENGTH); 359 360 for (unsigned i = 0; i < PAGE_LENGTH; i += 1) { 361 if (!current_page[j].time) { 362 titles[i] = dates[i] = 0; 363 continue; 364 } 365 366 tm = localtime(¤t_page[j].time); 367 ret = strftime(buffer, sizeof buffer, "%Y-%m-%d %H:%M:%S", tm); 368 dates[i] = ret ? strdup(buffer) : 0; 369 370 switch (current_page[j].before) { 371 case UNKNOWN: 372 snprintf(buffer, sizeof buffer, 373 "%u%%%c", 374 (unsigned)(current_page[j].after & 0x7f), 375 (current_page[j].after & 0x80) ? '+' : '-'); 376 break; 377 378 case APP_STARTED: 379 snprintf(buffer, sizeof buffer, 380 "Start %u%%%c", 381 (unsigned)(current_page[j].after & 0x7f), 382 (current_page[j].after & 0x80) ? '+' : '-'); 383 break; 384 385 case APP_CLOSED: 386 snprintf(buffer, sizeof buffer, 387 "Close %u%%%c", 388 (unsigned)(current_page[j].after & 0x7f), 389 (current_page[j].after & 0x80) ? '+' : '-'); 390 break; 391 392 case ANOMALOUS_VALUE: 393 snprintf(buffer, sizeof buffer, 394 "Anomalous %u", 395 (unsigned)(current_page[j].after)); 396 break; 397 398 default: 399 if ((current_page[j].before & 0x80) 400 == (current_page[j].after & 0x80)) { 401 snprintf(buffer, sizeof buffer, 402 "%u%% %c> %u%%", 403 (unsigned)(current_page[j].before & 0x7f), 404 (current_page[j].after & 0x80) ? '+' : '-', 405 (unsigned)(current_page[j].after & 0x7f)); 406 break; 407 } 408 409 if ((current_page[j].before & 0x7f) 410 == (current_page[j].after & 0x7f)) 411 snprintf (buffer, sizeof buffer, 412 "%s %u%%", 413 (current_page[j].after & 0x80) 414 ? "Charge" : "Discharge", 415 (unsigned)(current_page[j].after & 0x7f)); 416 else 417 snprintf (buffer, sizeof buffer, 418 "%s %u%% -> %u%%", 419 (current_page[j].after & 0x80) 420 ? "Chg" : "Disch", 421 (unsigned)(current_page[j].before & 0x7f), 422 (unsigned)(current_page[j].after & 0x7f)); 423 break; 424 } 425 426 titles[i] = strdup(buffer); 427 428 j = (j + 1) % PAGE_LENGTH; 429 } 430 } 431 432 static time_t 433 latest_event(void) { 434 time_t result = current_page[0].time; 435 for (unsigned j = 1; j < PAGE_LENGTH; j += 1) { 436 if (result < current_page[j].time) { 437 result = current_page[j].time; 438 } 439 } 440 return result; 441 } 442 443 static void 444 rebuild_menu(void) { 445 unsigned i = PAGE_LENGTH; 446 bool is_empty = true; 447 time_t old_latest = latest_event(); 448 449 persist_read_data(1, current_page, sizeof current_page); 450 451 if (old_latest != latest_event()) { 452 for (unsigned i = 0; i < PAGE_LENGTH; i += 1) { 453 free((void *)dates[i]); 454 free((void *)titles[i]); 455 } 456 init_strings(); 457 } 458 459 menu_section.title = 0; 460 menu_section.items = menu_items; 461 menu_section.num_items = 0; 462 463 menu_items[menu_section.num_items] = (SimpleMenuItem){ 464 .title = send_status, 465 .subtitle = 0, 466 .callback = 0, 467 .icon = 0 468 }; 469 menu_section.num_items += 1; 470 471 if (app_worker_is_running()) { 472 menu_items[menu_section.num_items] = (SimpleMenuItem){ 473 .title = "Stop worker", 474 .callback = &do_stop_worker 475 }; 476 menu_section.num_items += 1; 477 } else { 478 menu_items[menu_section.num_items] = (SimpleMenuItem){ 479 .title = "Start worker", 480 .callback = &do_start_worker 481 }; 482 menu_section.num_items += 1; 483 } 484 485 while (i) { 486 i -= 1; 487 if (!titles[i]) continue; 488 menu_items[menu_section.num_items].title = titles[i]; 489 menu_items[menu_section.num_items].subtitle = dates[i]; 490 menu_items[menu_section.num_items].icon = 0; 491 menu_items[menu_section.num_items].callback = 0; 492 menu_section.num_items += 1; 493 is_empty = false; 494 } 495 496 if (is_empty) { 497 menu_items[menu_section.num_items].title = "No event recorded"; 498 menu_items[menu_section.num_items].subtitle = 0; 499 menu_items[menu_section.num_items].icon = 0; 500 menu_items[menu_section.num_items].callback = 0; 501 menu_section.num_items += 1; 502 } 503 } 504 505 /**************** 506 * MENU ACTIONS * 507 ****************/ 508 509 static void 510 do_start_worker(int index, void *context) { 511 (void)index; 512 (void)context; 513 char buffer[256]; 514 AppWorkerResult result = app_worker_launch(); 515 516 switch (result) { 517 case APP_WORKER_RESULT_SUCCESS: 518 push_simple_dialog("Worker start requested.", true); 519 break; 520 case APP_WORKER_RESULT_ALREADY_RUNNING: 521 push_simple_dialog("Worker is already running.", true); 522 break; 523 case APP_WORKER_RESULT_ASKING_CONFIRMATION: 524 APP_LOG(APP_LOG_LEVEL_INFO, 525 "Frirmware requesting confirmation, skipping UI"); 526 break; 527 default: 528 snprintf(buffer, sizeof buffer, 529 "Unexpected result %d", 530 (int)result); 531 push_simple_dialog(buffer, false); 532 } 533 } 534 535 static void 536 do_stop_worker(int index, void *context) { 537 (void)index; 538 (void)context; 539 char buffer[256]; 540 AppWorkerResult result = app_worker_kill(); 541 542 switch (result) { 543 case APP_WORKER_RESULT_SUCCESS: 544 push_simple_dialog("Worker stop requested.", true); 545 break; 546 case APP_WORKER_RESULT_NOT_RUNNING: 547 push_simple_dialog("Worker is already stopped.", true); 548 break; 549 case APP_WORKER_RESULT_DIFFERENT_APP: 550 push_simple_dialog("A different worker is running.", true); 551 break; 552 case APP_WORKER_RESULT_ASKING_CONFIRMATION: 553 APP_LOG(APP_LOG_LEVEL_INFO, 554 "Frirmware requesting confirmation, skipping UI"); 555 break; 556 default: 557 snprintf(buffer, sizeof buffer, 558 "Unexpected result %d", 559 (int)result); 560 push_simple_dialog(buffer, false); 561 } 562 } 563 564 /********************* 565 * WINDOW MANAGEMENT * 566 *********************/ 567 568 static void 569 window_appear(Window *window) { 570 rebuild_menu(); 571 } 572 573 static void 574 window_load(Window *window) { 575 Layer *window_layer = window_get_root_layer(window); 576 GRect bounds = layer_get_bounds(window_layer); 577 578 rebuild_menu(); 579 580 menu_layer = simple_menu_layer_create(bounds, window, 581 &menu_section, 1, 0); 582 simple_menu_layer_set_selected_index(menu_layer, 1, false); 583 584 layer_add_child(window_layer, simple_menu_layer_get_layer(menu_layer)); 585 } 586 587 static void 588 window_unload(Window *window) { 589 simple_menu_layer_destroy(menu_layer); 590 } 591 592 /********************************** 593 * INTIALIZATION AND FINALIZATION * 594 **********************************/ 595 596 static void 597 init(void) { 598 snprintf(send_status, sizeof send_status, "Waiting for JS"); 599 600 cfg_wakeup_time = persist_read_int(MSG_KEY_CFG_WAKEUP_TIME) - 1; 601 wakeup_cancel_all(); 602 603 persist_read_data(1, current_page, sizeof current_page); 604 605 #ifdef DISPLAY_TEST_DATA 606 current_page[0].time = 1449738000; /* 2015-12-10T10:00:00 */ 607 current_page[0].before = APP_STARTED; 608 current_page[0].after = 90; 609 current_page[1].time = 1449741600; /* 2015-12-10T11:00:00 */ 610 current_page[1].before = 90; 611 current_page[1].after = 80; 612 current_page[2].time = 1449742980; /* 2015-12-10T11:23:00 */ 613 current_page[2].before = ANOMALOUS_VALUE; 614 current_page[2].after = 131; 615 current_page[3].time = 1449743160; /* 2015-12-10T11:26:00 */ 616 current_page[3].before = UNKNOWN; 617 current_page[3].after = 70; 618 current_page[4].time = 1449743460; /* 2015-12-10T11:31:00 */ 619 current_page[4].before = 70; 620 current_page[4].after = 128 | 70; 621 current_page[5].time = 1449743940; /* 2015-12-10T11:39:00 */ 622 current_page[5].before = 128 | 70; 623 current_page[5].after = 128 | 80; 624 current_page[6].time = 1449744000; /* 2015-12-10T11:40:00 */ 625 current_page[6].before = 128 | 80; 626 current_page[6].after = 80; 627 current_page[7].time = 1449744420; /* 2015-12-10T11:47:00 */ 628 current_page[7].before = 80; 629 current_page[7].after = 128 | 70; 630 current_page[8].time = 1449744660; /* 2015-12-10T11:51:00 */ 631 current_page[8].before = 128 | 70; 632 current_page[8].after = 80; 633 current_page[9].time = 1449744660; /* 2015-12-10T11:51:00 */ 634 current_page[9].before = 80; 635 current_page[9].after = 90; 636 current_page[10].time = 1449745140; /* 2015-12-10T11:59:00 */ 637 current_page[10].before = 90; 638 current_page[10].after = 128 | 100; 639 current_page[11].time = 1449745260; /* 2015-12-10T12:01:00 */ 640 current_page[11].before = 128 | 100; 641 current_page[11].after = 128 | 80; 642 current_page[12].time = 1449745620; /* 2015-12-10T12:07:00 */ 643 current_page[12].before = 128 | 80; 644 current_page[12].after = 60; 645 current_page[13].time = 1449745800; /* 2015-12-10T12:10:00 */ 646 current_page[13].before = APP_CLOSED; 647 current_page[13].after = 60; 648 current_page[14].time = 1449846060; /* 2015-12-11T16:01:00 */ 649 current_page[14].before = APP_STARTED; 650 current_page[14].after = 128 | 40; 651 current_page[15].time = 1449846480; /* 2015-12-11T16:08:00 */ 652 current_page[15].before = 128 | 40; 653 current_page[15].after = 128 | 40; 654 current_page[16].time = 1449846660; /* 2015-12-11T16:11:00 */ 655 current_page[16].before = UNKNOWN; 656 current_page[16].after = 128 | 60; 657 current_page[17].time = 1449846780; /* 2015-12-11T16:13:00 */ 658 current_page[17].before = APP_CLOSED; 659 current_page[17].after = 128 | 60; 660 for (unsigned i = 18; i < PAGE_LENGTH; i += 1) { 661 current_page[i].time = 0; 662 current_page[i].before = 0; 663 current_page[i].after = 0; 664 } 665 #else 666 if (launch_reason() == APP_LAUNCH_WAKEUP) { 667 push_simple_dialog("Battery- Auto Sync", true); 668 app_message_register_inbox_received(inbox_received_handler); 669 app_message_register_outbox_failed(outbox_failed_handler); 670 app_message_register_outbox_sent(outbox_sent_handler); 671 app_message_open(256, 2048); 672 return; 673 } 674 #endif 675 676 init_strings(); 677 678 window = window_create(); 679 window_set_window_handlers(window, (WindowHandlers) { 680 .load = window_load, 681 .unload = window_unload, 682 .appear = window_appear, 683 }); 684 window_stack_push(window, true); 685 686 app_message_register_inbox_received(inbox_received_handler); 687 app_message_register_outbox_failed(outbox_failed_handler); 688 app_message_register_outbox_sent(outbox_sent_handler); 689 app_message_open(256, 2048); 690 } 691 692 static void 693 deinit(void) { 694 window_destroy(window); 695 696 for (unsigned i = 0; i < PAGE_LENGTH; i += 1) { 697 if (titles[i]) free((void *)titles[i]); 698 titles[i] = 0; 699 700 if (dates[i]) free((void *)dates[i]); 701 dates[i] = 0; 702 } 703 704 if (cfg_wakeup_time >= 0) { 705 WakeupId res; 706 time_t now = time(0); 707 time_t t = clock_to_timestamp(TODAY, 708 cfg_wakeup_time / 60, cfg_wakeup_time % 60); 709 710 if (t - now > 6 * 86400) 711 t -= 6 * 86400; 712 else if (t - now <= 120) 713 t += 86400; 714 715 res = wakeup_schedule(t, 0, true); 716 717 if (res < 0) 718 APP_LOG(APP_LOG_LEVEL_ERROR, 719 "wakeup_schedule(%" PRIi32 ", 0, true)" 720 " returned %" PRIi32, 721 t, res); 722 } 723 } 724 725 int 726 main(void) { 727 init(); 728 app_event_loop(); 729 deinit(); 730 }