/* * Copyright (c) 2016, Natacha Porté * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ #include #include #include "dict_tools.h" #include "progress_layer.h" #define MSG_KEY_LAST_SENT 110 #define MSG_KEY_MODAL_MESSAGE 120 #define MSG_KEY_UPLOAD_DONE 130 #define MSG_KEY_UPLOAD_START 140 #define MSG_KEY_UPLOAD_FAILED 150 #define MSG_KEY_DATA_KEY 210 #define MSG_KEY_DATA_LINE 220 #define MSG_KEY_CFG_START 301 #define MSG_KEY_CFG_END 302 #define MSG_KEY_CFG_AUTO_CLOSE 310 #define MSG_KEY_CFG_WAKEUP_TIME 320 static Window *window; static TextLayer *modal_text_layer; static char modal_text[256]; static HealthMinuteData minute_data[1440]; static HealthActivityMask minute_activity[1440]; static uint16_t minute_data_size = 0; static uint16_t minute_index = 0; static time_t minute_first = 0, minute_last = 0; static bool modal_displayed = false; static bool display_dirty = false; static char global_buffer[1024]; static bool sending_data = false; static bool cfg_auto_close = false; static bool auto_close = false; static bool configuring = false; static int cfg_wakeup_time = -1; static int32_t last_key = 0; static struct widget { char label[64]; char rate[32]; TextLayer *label_layer; TextLayer *rate_layer; ProgressLayer *progress_layer; uint32_t first_key; uint32_t current_key; time_t start_time; } phone, web; static void close_app(void) { window_stack_pop_all(true); } static void set_modal_mode(bool is_modal) { if (is_modal == modal_displayed) return; layer_set_hidden(text_layer_get_layer(modal_text_layer), !is_modal); layer_set_hidden(text_layer_get_layer(phone.label_layer), is_modal); layer_set_hidden(text_layer_get_layer(phone.rate_layer), is_modal); layer_set_hidden(phone.progress_layer, is_modal); layer_set_hidden(text_layer_get_layer(web.label_layer), is_modal); layer_set_hidden(text_layer_get_layer(web.rate_layer), is_modal); layer_set_hidden(web.progress_layer, is_modal); modal_displayed = is_modal; } static void update_half_progress(struct widget *widget) { if (!widget || !widget->current_key) return; time_t t, now = time(0); struct tm *tm; int32_t key_span = (last_key ? last_key : (now + 59) / 60) - widget->first_key; int32_t keys_done = widget->current_key - widget->first_key + 1; int32_t running_time = widget->start_time ? now - widget->start_time : 0; progress_layer_set_progress(widget->progress_layer, (keys_done * 100 + key_span / 2) / key_span); t = widget->current_key * 60; tm = localtime(&t); strftime(widget->label, sizeof widget->label, "%F %H:%M", tm); if (last_key > 0 && widget->current_key == (uint32_t)last_key) { snprintf(widget->rate, sizeof widget->rate, "DONE"); } else if (running_time > 0) { int32_t i = ((widget->current_key - widget->first_key) * 60 + running_time / 2) / running_time; snprintf(widget->rate, sizeof widget->rate, "%" PRIi32 " /min", i); } } static void update_progress(void) { update_half_progress(&phone); update_half_progress(&web); display_dirty = false; } #define PROGRESS_HEIGHT 10 #define PROGRESS_MARGIN 8 #define LABEL_MARGIN (PROGRESS_HEIGHT + PROGRESS_MARGIN) #define LABEL_HEIGHT 22 static void window_load(Window *window) { Layer *window_layer = window_get_root_layer(window); GRect bounds = layer_get_bounds(window_layer); modal_text_layer = text_layer_create(GRect(0, bounds.size.h / 3, bounds.size.w, bounds.size.h / 3)); text_layer_set_text(modal_text_layer, modal_text); text_layer_set_text_alignment(modal_text_layer, GTextAlignmentCenter); text_layer_set_font(modal_text_layer, fonts_get_system_font(FONT_KEY_GOTHIC_24_BOLD)); layer_add_child(window_layer, text_layer_get_layer(modal_text_layer)); phone.rate_layer = text_layer_create(GRect(0, bounds.size.h / 2 - LABEL_MARGIN - 2 * LABEL_HEIGHT, bounds.size.w, LABEL_HEIGHT)); text_layer_set_text(phone.rate_layer, phone.rate); text_layer_set_text_alignment(phone.rate_layer, GTextAlignmentCenter); text_layer_set_font(phone.rate_layer, fonts_get_system_font(FONT_KEY_GOTHIC_18_BOLD)); layer_add_child(window_layer, text_layer_get_layer(phone.rate_layer)); phone.label_layer = text_layer_create(GRect(0, bounds.size.h / 2 - LABEL_HEIGHT - LABEL_MARGIN, bounds.size.w, LABEL_HEIGHT)); text_layer_set_text(phone.label_layer, phone.label); text_layer_set_text_alignment(phone.label_layer, GTextAlignmentCenter); text_layer_set_font(phone.label_layer, fonts_get_system_font(FONT_KEY_GOTHIC_18_BOLD)); layer_add_child(window_layer, text_layer_get_layer(phone.label_layer)); phone.progress_layer = progress_layer_create(GRect(bounds.size.w / 4, bounds.size.h / 2 - PROGRESS_HEIGHT - PROGRESS_MARGIN / 2, bounds.size.w / 2, PROGRESS_HEIGHT)); progress_layer_set_progress(phone.progress_layer, 0); progress_layer_set_corner_radius(phone.progress_layer, 3); progress_layer_set_foreground_color(phone.progress_layer, GColorBlack); progress_layer_set_background_color(phone.progress_layer, GColorLightGray); layer_add_child(window_layer, phone.progress_layer); web.progress_layer = progress_layer_create(GRect(bounds.size.w / 4, bounds.size.h / 2 + PROGRESS_MARGIN / 2, bounds.size.w / 2, PROGRESS_HEIGHT)); progress_layer_set_progress(web.progress_layer, 0); progress_layer_set_corner_radius(web.progress_layer, 3); progress_layer_set_foreground_color(web.progress_layer, GColorBlack); progress_layer_set_background_color(web.progress_layer, GColorLightGray); layer_add_child(window_layer, web.progress_layer); web.label_layer = text_layer_create(GRect(0, bounds.size.h / 2 + LABEL_MARGIN, bounds.size.w, LABEL_HEIGHT)); text_layer_set_text(web.label_layer, web.label); text_layer_set_text_alignment(web.label_layer, GTextAlignmentCenter); text_layer_set_font(web.label_layer, fonts_get_system_font(FONT_KEY_GOTHIC_18_BOLD)); layer_add_child(window_layer, text_layer_get_layer(web.label_layer)); web.rate_layer = text_layer_create(GRect(0, bounds.size.h / 2 + LABEL_MARGIN + LABEL_HEIGHT, bounds.size.w, LABEL_HEIGHT)); text_layer_set_text(web.rate_layer, web.rate); text_layer_set_text_alignment(web.rate_layer, GTextAlignmentCenter); text_layer_set_font(web.rate_layer, fonts_get_system_font(FONT_KEY_GOTHIC_18_BOLD)); layer_add_child(window_layer, text_layer_get_layer(web.rate_layer)); set_modal_mode(true); } static void window_unload(Window *window) { text_layer_destroy(modal_text_layer); text_layer_destroy(phone.label_layer); text_layer_destroy(web.label_layer); progress_layer_destroy(phone.progress_layer); progress_layer_destroy(web.progress_layer); } static void set_modal_message(const char *msg) { strncpy(modal_text, msg, sizeof modal_text); modal_text[sizeof modal_text - 1] = 0; layer_mark_dirty(text_layer_get_layer(modal_text_layer)); } /* minute_data_image - fill a buffer with CSV data without line terminator */ /* format: RFC-3339 time, step count, yaw, pitch, vmc, ambient light */ static uint16_t minute_data_image(char *buffer, size_t size, HealthMinuteData *data, HealthActivityMask activity_mask, time_t key) { struct tm *tm; size_t ret; if (!buffer || !data) return 0; tm = gmtime(&key); if (!tm) { APP_LOG(APP_LOG_LEVEL_ERROR, "Unable to get UTC time for %" PRIi32, key); return 0; } ret = strftime(buffer, size, "%FT%TZ", tm); if (!ret) { APP_LOG(APP_LOG_LEVEL_ERROR, "Unable to build RFC-3339 representation of %" PRIi32, key); return 0; } if (ret >= size) { APP_LOG(APP_LOG_LEVEL_ERROR, "Unexpected return value %zu of strftime on buffer %zu", ret, size); return 0; } if (data->is_invalid) { int i = snprintf(buffer + ret, size - ret, ",,,,,,%" PRIu32 ",", activity_mask); if (i <= 0) { APP_LOG(APP_LOG_LEVEL_ERROR, "minute_data_image: " "Unexpected return value %d of snprintf (i)", i); return 0; } return ret + i; } uint16_t yaw = data->orientation & 0xF; uint16_t pitch = data->orientation >> 4; int i = snprintf(buffer + ret, size - ret, ",%" PRIu8 ",%" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%d,%" PRIu32 ",%" PRIu8, data->steps, yaw, pitch, data->vmc, (int)data->light, activity_mask, data->heart_rate_bpm); if (i <= 0) { APP_LOG(APP_LOG_LEVEL_ERROR, "minute_data_image: " "Unexpected return value %d of snprintf", i); return 0; } return ret + i; } /* send_minute_data - use AppMessage to send the given minute data to phone */ static void send_minute_data(HealthMinuteData *data, HealthActivityMask activity_mask, time_t key) { int32_t int_key = key / 60; if (key % 60 != 0) { APP_LOG(APP_LOG_LEVEL_WARNING, "Discarding %" PRIi32 " second from time key %" PRIi32, key % 60, int_key); } uint16_t size = minute_data_image(global_buffer, sizeof global_buffer, data, activity_mask, key); if (!size) return; AppMessageResult msg_result; DictionaryIterator *iter; msg_result = app_message_outbox_begin(&iter); if (msg_result) { APP_LOG(APP_LOG_LEVEL_ERROR, "send_minute_data: app_message_outbox_begin returned %d", (int)msg_result); return; } DictionaryResult dict_result; dict_result = dict_write_int(iter, MSG_KEY_DATA_KEY, &int_key, sizeof int_key, true); if (dict_result != DICT_OK) { APP_LOG(APP_LOG_LEVEL_ERROR, "send_minute_data: [%d] unable to add data key %" PRIi32, (int)dict_result, int_key); } dict_result = dict_write_cstring(iter, MSG_KEY_DATA_LINE, global_buffer); if (dict_result != DICT_OK) { APP_LOG(APP_LOG_LEVEL_ERROR, "send_minute_data: [%d] unable to add data line \"%s\"", (int)dict_result, global_buffer); } msg_result = app_message_outbox_send(); if (msg_result) { APP_LOG(APP_LOG_LEVEL_ERROR, "send_minute_data: app_message_outbox_send returned %d", (int)msg_result); } if (!phone.first_key) phone.first_key = int_key; phone.current_key = int_key; display_dirty = true; } static bool record_activity(HealthActivity activity, time_t start_time, time_t end_time, void *context) { uint16_t first_index, last_index; (void)context; if (start_time <= minute_first) { first_index = 0; } else { first_index = (start_time - minute_first) / 60; } if (first_index >= minute_data_size) return true; last_index = (end_time - minute_first + 59) / 60; if (last_index > minute_data_size) { last_index = minute_data_size; } for (uint16_t i = first_index; i < last_index; i += 1) { minute_activity[i] |= activity; } return true; } static bool load_minute_data_page(time_t start) { minute_first = start; minute_last = time(0); minute_data_size = health_service_get_minute_history(minute_data, ARRAY_LENGTH(minute_data), &minute_first, &minute_last); minute_index = 0; memset(minute_activity, 0, sizeof minute_activity); if (health_service_any_activity_accessible(HealthActivityMaskAll, minute_first, minute_last) == HealthServiceAccessibilityMaskAvailable) { health_service_activities_iterate(HealthActivityMaskAll, minute_first, minute_last, HealthIterationDirectionFuture, &record_activity, 0); } if (!minute_data_size) { APP_LOG(APP_LOG_LEVEL_ERROR, "health_service_get_minute_history returned 0"); minute_first = minute_last = 0; return false; } return true; } static void send_next_line(void) { if (minute_index >= minute_data_size && !load_minute_data_page(minute_last)) { sending_data = false; last_key = phone.current_key; display_dirty = true; if (auto_close && web.current_key >= phone.current_key) close_app(); return; } send_minute_data(minute_data + minute_index, minute_activity[minute_index], minute_first + 60 * minute_index); minute_index += 1; } static void handle_last_sent(Tuple *tuple) { uint32_t ikey = 0; if (tuple->length == 4 && tuple->type == TUPLE_UINT) ikey = tuple->value->uint32; else if (tuple->length == 4 && tuple->type == TUPLE_INT) ikey = tuple->value->int32; else { APP_LOG(APP_LOG_LEVEL_ERROR, "Unexpected type %d or length %" PRIu16 " for MSG_KEY_LAST_SENT", (int)tuple->type, tuple->length); return; } APP_LOG(APP_LOG_LEVEL_INFO, "received LAST_SENT %" PRIu32, ikey); phone.start_time = time(0); phone.first_key = phone.current_key = 0; web.start_time = 0; web.first_key = web.current_key = 0; minute_index = 0; minute_data_size = 0; minute_last = ikey ? (ikey + 1) * 60 : 0; set_modal_mode(false); if (!sending_data) { sending_data = true; last_key = 0; send_next_line(); } } static void handle_received_tuple(Tuple *tuple) { switch (tuple->key) { case MSG_KEY_LAST_SENT: handle_last_sent (tuple); break; case MSG_KEY_MODAL_MESSAGE: if (tuple->type != TUPLE_CSTRING) { APP_LOG(APP_LOG_LEVEL_ERROR, "Unexpected type %d for MSG_KEY_MODAL_MESSAGE", (int)tuple->type); } else { set_modal_mode(true); set_modal_message(tuple->value->cstring); } break; case MSG_KEY_UPLOAD_DONE: web.current_key = tuple_uint(tuple); if (!web.first_key) web.first_key = web.current_key; display_dirty = true; if (auto_close && !sending_data && web.current_key >= phone.current_key) close_app(); break; case MSG_KEY_UPLOAD_START: if (!web.first_key) { web.first_key = tuple_uint(tuple); web.start_time = time(0); } break; case MSG_KEY_UPLOAD_FAILED: web.start_time = 0; if (tuple->type == TUPLE_CSTRING) snprintf(web.rate, sizeof web.rate, "%s", tuple->value->cstring); break; case MSG_KEY_CFG_AUTO_CLOSE: auto_close = cfg_auto_close = (tuple_uint(tuple) != 0); persist_write_bool(MSG_KEY_CFG_AUTO_CLOSE, auto_close); if (auto_close && !sending_data && web.current_key >= phone.current_key) close_app(); break; case MSG_KEY_CFG_WAKEUP_TIME: cfg_wakeup_time = tuple_int(tuple); persist_write_int(MSG_KEY_CFG_WAKEUP_TIME, cfg_wakeup_time + 1); APP_LOG(APP_LOG_LEVEL_INFO, "wrote cfg_wakeup_time %i", cfg_wakeup_time); break; case MSG_KEY_CFG_START: APP_LOG(APP_LOG_LEVEL_INFO, "Starting configuration"); auto_close = false; configuring = true; break; case MSG_KEY_CFG_END: APP_LOG(APP_LOG_LEVEL_INFO, "End of configuration"); auto_close = cfg_auto_close; configuring = false; break; default: APP_LOG(APP_LOG_LEVEL_ERROR, "Unknown key %lu in received message", (unsigned long)tuple->key); } } static void inbox_received_handler(DictionaryIterator *iterator, void *context) { Tuple *tuple; (void)context; for (tuple = dict_read_first(iterator); tuple; tuple = dict_read_next(iterator)) handle_received_tuple(tuple); } static void outbox_sent_handler(DictionaryIterator *iterator, void *context) { (void)iterator; (void)context; send_next_line(); } static void outbox_failed_handler(DictionaryIterator *iterator, AppMessageResult reason, void *context) { (void)iterator; (void)context; APP_LOG(APP_LOG_LEVEL_ERROR, "Outbox failed: 0x%x", (unsigned)reason); } static void tick_handler(struct tm *tick_time, TimeUnits units_changed) { (void)tick_time; (void)units_changed; if (display_dirty) update_progress(); } static void init(void) { cfg_auto_close = persist_read_bool(MSG_KEY_CFG_AUTO_CLOSE); cfg_wakeup_time = persist_read_int(MSG_KEY_CFG_WAKEUP_TIME) - 1; APP_LOG(APP_LOG_LEVEL_INFO, "read cfg_wakeup_time %i", cfg_wakeup_time); auto_close = (cfg_auto_close || launch_reason() == APP_LAUNCH_WAKEUP); app_message_register_inbox_received(inbox_received_handler); app_message_register_outbox_failed(outbox_failed_handler); app_message_register_outbox_sent(outbox_sent_handler); app_message_open(256, 2048); strncpy(modal_text, "Waiting for JS part", sizeof modal_text); window = window_create(); window_set_window_handlers(window, (WindowHandlers) { .load = window_load, .unload = window_unload, }); window_stack_push(window, true); tick_timer_service_subscribe(SECOND_UNIT, &tick_handler); wakeup_cancel_all(); } static void deinit(void) { window_destroy(window); if (cfg_wakeup_time >= 0) { WakeupId res; time_t now = time(0); time_t t = clock_to_timestamp(TODAY, cfg_wakeup_time / 60, cfg_wakeup_time % 60); if (t - now > 6 * 86400) t -= 6 * 86400; else if (t - now <= 120) t += 86400; res = wakeup_schedule(t, 0, true); if (res < 0) APP_LOG(APP_LOG_LEVEL_ERROR, "wakeup_schedule(%" PRIi32 ", 0, true)" " returned %" PRIi32, t, res); } else { APP_LOG(APP_LOG_LEVEL_INFO, "No wakeup to setup"); } } int main(void) { init(); app_event_loop(); deinit(); }