/* * 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_DATA_KEY 210 #define MSG_KEY_DATA_LINE 220 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 char global_buffer[1024]; 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 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 last_key = (now + 59) / 60; int32_t key_span = last_key - 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 (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); } #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; } 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, data->steps, yaw, pitch, data->vmc, (int)data->light, activity_mask); 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; update_progress(); } 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)) { 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); minute_index = 0; minute_data_size = 0; minute_last = ikey ? (ikey + 1) * 60 : 0; set_modal_mode(false); send_next_line(); } static void inbox_received_handler(DictionaryIterator *iterator, void *context) { Tuple *tuple; (void)context; tuple = dict_find(iterator, MSG_KEY_LAST_SENT); if (tuple) handle_last_sent (tuple); tuple = dict_find(iterator, MSG_KEY_MODAL_MESSAGE); if (tuple) { 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); } } tuple = dict_find(iterator, MSG_KEY_UPLOAD_DONE); if (tuple) { web.current_key = tuple_uint(tuple); if (!web.first_key) web.first_key = web.current_key; update_progress(); } } 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 init(void) { 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); } static void deinit(void) { window_destroy(window); } int main(void) { init(); app_event_loop(); deinit(); }