diff --git a/package.json b/package.json new file mode 100644 index 0000000..570782b --- /dev/null +++ b/package.json @@ -0,0 +1,94 @@ +{ + "author": "pebble@schlotzz.com", + "dependencies": { + "pebble-clay": "^1.0.4" + }, + "keywords": [], + "name": "octowatch2", + "pebble": { + "capabilities": [ + "configurable" + ], + "displayName": "OctoWatch2", + "enableMultiJS": true, + "messageKeys": [ + "ready", + "filename", + "remaining_time", + "state", + "progress", + "temp0", + "temp1", + "tempbed" + ], + "projectType": "native", + "resources": { + "media": [ + { + "file": "images/icon_connect.png", + "name": "CONNECT_ICON", + "targetPlatforms": null, + "type": "bitmap" + }, + { + "file": "images/icon_cancel.png", + "name": "CANCEL_ICON", + "targetPlatforms": null, + "type": "bitmap" + }, + { + "file": "images/icon_nozzle.png", + "name": "NOZZLE_ICON", + "targetPlatforms": null, + "type": "bitmap" + }, + { + "file": "images/icon_bed.png", + "name": "BED_ICON", + "targetPlatforms": null, + "type": "bitmap" + }, + { + "file": "images/icon_play.png", + "name": "PLAY_ICON", + "targetPlatforms": null, + "type": "bitmap" + }, + { + "file": "images/icon_pause.png", + "name": "PAUSE_ICON", + "targetPlatforms": null, + "type": "bitmap" + }, + { + "file": "images/icon_menu.png", + "menuIcon": true, + "name": "MENU_ICON", + "targetPlatforms": null, + "type": "bitmap" + }, + { + "file": "images/icon_ellipsis.png", + "name": "ELLIPSIS_ICON", + "targetPlatforms": null, + "type": "bitmap" + }, + { + "file": "images/icon_dismiss.png", + "name": "DISMISS_ICON", + "targetPlatforms": null, + "type": "bitmap" + } + ] + }, + "sdkVersion": "3", + "targetPlatforms": [ + "basalt" + ], + "uuid": "605e1126-d933-433e-8334-e519995a1ac0", + "watchapp": { + "watchface": false + } + }, + "version": "2.0.0" +} diff --git a/resources/images/icon_bed.png b/resources/images/icon_bed.png new file mode 100644 index 0000000..eb40e24 Binary files /dev/null and b/resources/images/icon_bed.png differ diff --git a/resources/images/icon_cancel.png b/resources/images/icon_cancel.png new file mode 100644 index 0000000..6f59b64 Binary files /dev/null and b/resources/images/icon_cancel.png differ diff --git a/resources/images/icon_connect.png b/resources/images/icon_connect.png new file mode 100644 index 0000000..6da45b8 Binary files /dev/null and b/resources/images/icon_connect.png differ diff --git a/resources/images/icon_dismiss.png b/resources/images/icon_dismiss.png new file mode 100644 index 0000000..3710933 Binary files /dev/null and b/resources/images/icon_dismiss.png differ diff --git a/resources/images/icon_ellipsis.png b/resources/images/icon_ellipsis.png new file mode 100644 index 0000000..901a4ac Binary files /dev/null and b/resources/images/icon_ellipsis.png differ diff --git a/resources/images/icon_menu.png b/resources/images/icon_menu.png new file mode 100644 index 0000000..c22b8ac Binary files /dev/null and b/resources/images/icon_menu.png differ diff --git a/resources/images/icon_nozzle.png b/resources/images/icon_nozzle.png new file mode 100644 index 0000000..2a26a5a Binary files /dev/null and b/resources/images/icon_nozzle.png differ diff --git a/resources/images/icon_pause.png b/resources/images/icon_pause.png new file mode 100644 index 0000000..64b6471 Binary files /dev/null and b/resources/images/icon_pause.png differ diff --git a/resources/images/icon_play.png b/resources/images/icon_play.png new file mode 100644 index 0000000..0a403ef Binary files /dev/null and b/resources/images/icon_play.png differ diff --git a/src/c/app_glance.c b/src/c/app_glance.c new file mode 100644 index 0000000..222d3d3 --- /dev/null +++ b/src/c/app_glance.c @@ -0,0 +1,92 @@ +// OctoWatch2 +// A Pebble watch app for monitoring and basic controlling of 3D printers via Octoprint +// +// Licence: CC BY-SA 3.0, http://creativecommons.org/licenses/by-sa/3.0/ +// Author: Dominik Scholz , go4u.de Webdesign + +#include +#include "app_glance.h" +#include "printer.h" + +#define APP_GLANCE_DEFAULT_TIMEOUT 30*60 // 30 minutes after last update + + +static int s_app_glance_timeout = APP_GLANCE_SLICE_NO_EXPIRATION; + + +// add the app glance +static void app_glance_update(AppGlanceReloadSession *session, size_t limit, void *context) { + // return, if no app glances allowed + if (limit < 1) return; + + // cast the context object to a string + const char *message = (char *)context; + + // create the slice + const AppGlanceSlice slice = (AppGlanceSlice) { + .layout = { + //.icon = APP_GLANCE_SLICE_DEFAULT_ICON, + .subtitle_template_string = message + }, + .expiration_time = s_app_glance_timeout + }; + + APP_LOG(APP_LOG_LEVEL_DEBUG, "app_glance_update time is: %u", s_app_glance_timeout); + + // Add the slice, and check the result + const AppGlanceResult result = app_glance_add_slice(session, slice); + if (result != APP_GLANCE_RESULT_SUCCESS) { + APP_LOG(APP_LOG_LEVEL_ERROR, "app_glance_update Error: %d", result); + } +} + + +// do the app glance +void app_glance_destroy(void) { + static char s_message[128] = "Unknown printer state"; + s_app_glance_timeout = time(0) + APP_GLANCE_DEFAULT_TIMEOUT; + + // choose proper message + switch (printer_get_state()) { + + // clear app glance if still loading + case LOADING: + app_glance_reload(NULL, NULL); + return; + + // printer is offline + case OFFLINE: + snprintf(s_message, sizeof(s_message), "Printer offline"); + break; + + // connecting to printer + case CONNECTING: + snprintf(s_message, sizeof(s_message), "Connecting"); + break; + + // printer finished or is ready/online + case OPERATIONAL: + if (printer_get_progress() == 100) snprintf(s_message, sizeof(s_message), "Finished %s", printer_get_filename()); + else if (printer_get_finish_timestamp() > 0) snprintf(s_message, sizeof(s_message), "Ready for %s", printer_get_filename()); + else snprintf(s_message, sizeof(s_message), "Printer online"); + break; + + // job is being printed / has been completed + case PRINTING: + snprintf(s_message, sizeof(s_message), "{time_until(%u)|format(>0S:'Printing, %%aT left','Print completed')}", (uint)printer_get_finish_timestamp()); + s_app_glance_timeout = printer_get_finish_timestamp() + APP_GLANCE_DEFAULT_TIMEOUT; + break; + + // printer has been paused + case PAUSED: + snprintf(s_message, sizeof(s_message), "Print paused"); + break; + + // an error occured + case ERROR: + snprintf(s_message, sizeof(s_message), "Print error occured"); + break; + } + + app_glance_reload(app_glance_update, s_message); +} diff --git a/src/c/app_glance.h b/src/c/app_glance.h new file mode 100644 index 0000000..0e9ae87 --- /dev/null +++ b/src/c/app_glance.h @@ -0,0 +1,4 @@ +#pragma once + +void app_glance_destroy(void); +//static void app_glance_update(AppGlanceReloadSession *, size_t, void *); \ No newline at end of file diff --git a/src/c/main.c b/src/c/main.c new file mode 100644 index 0000000..12c08a8 --- /dev/null +++ b/src/c/main.c @@ -0,0 +1,33 @@ +// OctoWatch2 +// A Pebble watch app for monitoring and basic controlling of 3D printers via Octoprint +// +// Licence: CC BY-SA 3.0, http://creativecommons.org/licenses/by-sa/3.0/ +// Author: Dominik Scholz , go4u.de Webdesign + +#include +#include "window_main.h" +#include "messaging.h" +#include "app_glance.h" + + +// do init +static void init(void) { + window_main_init(); + messaging_init(); +} + + +// do deinit +static void deinit(void) { + messaging_destroy(); + window_main_destroy(); + app_glance_destroy(); +} + + +// main +int main(void) { + init(); + app_event_loop(); + deinit(); +} diff --git a/src/c/messaging.c b/src/c/messaging.c new file mode 100644 index 0000000..6c6a57b --- /dev/null +++ b/src/c/messaging.c @@ -0,0 +1,118 @@ +// OctoWatch2 +// A Pebble watch app for monitoring and basic controlling of 3D printers via Octoprint +// +// Licence: CC BY-SA 3.0, http://creativecommons.org/licenses/by-sa/3.0/ +// Author: Dominik Scholz , go4u.de Webdesign + +#include +#include "messaging.h" +#include "window_main.h" +#include "printer.h" + + +// inbox success handler +static void messaging_inbox_received(DictionaryIterator *iter, void *context) { + // read tuples + Tuple *tuple_filename = dict_find(iter, 1); + Tuple *tuple_remain = dict_find(iter, 2); + Tuple *tuple_state = dict_find(iter, 3); + Tuple *tuple_progress = dict_find(iter, 4); + Tuple *tuple_secs = dict_find(iter, 5); + Tuple *tuple_temp0 = dict_find(iter, 6); + Tuple *tuple_temp1 = dict_find(iter, 7); + Tuple *tuple_tempbed = dict_find(iter, 8); + + // received filename + if (tuple_filename) { + APP_LOG(APP_LOG_LEVEL_DEBUG, "setting filename to: %s", tuple_filename->value->cstring); + printer_set_filename(tuple_filename->value->cstring); + window_main_set_filename(printer_get_filename()); + } + + // received remaining time, human readable + if (tuple_remain) { + APP_LOG(APP_LOG_LEVEL_DEBUG, "setting time remaining to: %s", tuple_remain->value->cstring); + printer_set_remaining(tuple_remain->value->cstring); + window_main_set_time_remaing_counter(printer_get_remaining()); + } + + // received state + if (tuple_state) { + APP_LOG(APP_LOG_LEVEL_DEBUG, "setting state to: %s", tuple_state->value->cstring); + printer_set_state_by_char(tuple_state->value->cstring); + window_main_set_state(printer_get_state_text()); + } + + // received progress + if (tuple_progress) { + APP_LOG(APP_LOG_LEVEL_DEBUG, "setting progress to: %u", (uint)tuple_progress->value->uint8); + printer_set_progress(tuple_progress->value->uint8); + window_main_set_progress(printer_get_progress()); + } + + // received remaining time, seconds + if (tuple_secs) { + APP_LOG(APP_LOG_LEVEL_DEBUG, "setting secs to: %u", (uint)tuple_secs->value->uint32); + printer_set_finish_timestamp(tuple_secs->value->uint32); + } + + // received nozzle0 temp + if (tuple_temp0) { + APP_LOG(APP_LOG_LEVEL_DEBUG, "setting nozzle0 temp to: %u", (uint)tuple_temp0->value->uint16); + printer_set_temp0(tuple_temp0->value->uint16); + window_main_set_nozzle(printer_get_temp0(), printer_get_temp1()); + } + + // received nozzle1 temp + if (tuple_temp1) { + APP_LOG(APP_LOG_LEVEL_DEBUG, "setting nozzle1 temp to: %u", (uint)tuple_temp1->value->uint16); + printer_set_temp1(tuple_temp1->value->uint16); + window_main_set_nozzle(printer_get_temp0(), printer_get_temp1()); + } + + // received bed temp + if (tuple_tempbed) { + APP_LOG(APP_LOG_LEVEL_DEBUG, "setting bed temp to: %u", tuple_tempbed->value->uint16); + printer_set_tempbed(tuple_tempbed->value->uint16); + window_main_set_bed(printer_get_tempbed()); + } +} + + +// inbox failure handler +static void messaging_inbox_dropped(AppMessageResult reason, void *context) { + APP_LOG(APP_LOG_LEVEL_DEBUG, "App Message Dropped %d", (int)reason); + vibes_double_pulse(); +} + + +// send an outgoing message +void messaging_outbox_send(const char *value) { + DictionaryIterator *iter; + app_message_outbox_begin(&iter); + + if (!iter) { + // Error creating outbound message + APP_LOG(APP_LOG_LEVEL_ERROR, "messaging_outbox_send: cannot send app message"); + return; + } + + dict_write_cstring(iter, 0, value); + dict_write_end(iter); + + app_message_outbox_send(); +} + + +// setup app message inbox +void messaging_init(void) { + app_message_register_inbox_received(messaging_inbox_received); + app_message_register_inbox_dropped(messaging_inbox_dropped); + app_message_open(256, 64); +} + + +// destroy message handlers +void messaging_destroy(void) { + app_message_deregister_callbacks(); +} diff --git a/src/c/messaging.h b/src/c/messaging.h new file mode 100644 index 0000000..208b37a --- /dev/null +++ b/src/c/messaging.h @@ -0,0 +1,7 @@ +#pragma once + +//static void messaging_inbox_received(DictionaryIterator *, void *); +//static void messaging_inbox_dropped(AppMessageResult, void *); +void messaging_outbox_send(const char *); +void messaging_init(void); +void messaging_destroy(void); diff --git a/src/c/printer.c b/src/c/printer.c new file mode 100644 index 0000000..7399f6c --- /dev/null +++ b/src/c/printer.c @@ -0,0 +1,134 @@ +// OctoWatch2 +// A Pebble watch app for monitoring and basic controlling of 3D printers via Octoprint +// +// Licence: CC BY-SA 3.0, http://creativecommons.org/licenses/by-sa/3.0/ +// Author: Dominik Scholz , go4u.de Webdesign + +#include +#include "printer.h" + +static enum PRINTERSTATE s_state = LOADING; +static char s_state_text[16] = "loading..."; +static uint32_t s_finish_timestamp = 0; +static char s_remaining[8] = "--:--"; +static char s_filename[64] = "no file loaded"; +static uint8_t s_progress = 0; +static uint16_t s_temp0 = 0; +static uint16_t s_temp1 = 0; +static uint16_t s_tempbed = 0; + + +// get printer state +enum PRINTERSTATE printer_get_state(void) { + return s_state; +} + + +// set printer state +void printer_set_state(enum PRINTERSTATE state) { + s_state = state; +} + + +// set printer state by name +void printer_set_state_by_char(const char *name) { + if (0 == strcmp(name, "Loading")) s_state = LOADING; + else if (0 == strcmp(name, "Offline")) s_state = OFFLINE; + else if (0 == strcmp(name, "Connecting")) s_state = CONNECTING; + else if (0 == strcmp(name, "Operational")) s_state = OPERATIONAL; + else if (0 == strcmp(name, "Printing")) s_state = PRINTING; + else if (0 == strcmp(name, "Paused")) s_state = PAUSED; + else if (0 == strcmp(name, "Error")) s_state = ERROR; + else APP_LOG(APP_LOG_LEVEL_DEBUG, "unknown printerstate: %s", name); + snprintf(s_state_text, sizeof(s_state_text), "%s", name); +} + + +// get printer state text +char *printer_get_state_text(void) { + return s_state_text; +} + + +// get remaining seconds +uint32_t printer_get_finish_timestamp(void) { + return s_finish_timestamp; +} + + +// set remaining seconds +void printer_set_finish_timestamp(uint32_t timestamp) { + s_finish_timestamp = timestamp; +} + + +// get remaining +char *printer_get_remaining(void) { + return s_remaining; +} + + +// set remaining +void printer_set_remaining(const char *remaining) { + snprintf(s_remaining, sizeof(s_remaining), "%s", remaining); +} + + +// get filename +char *printer_get_filename(void) { + return s_filename; +} + + +// set filename +void printer_set_filename(const char *filename) { + snprintf(s_filename, sizeof(s_filename), "%s", filename); +} + + +// get progress +uint8_t printer_get_progress(void) { + return s_progress; +} + + +// set progress +void printer_set_progress(uint8_t progress) { + s_progress = progress; +} + + +// get nozzle0 temp +uint16_t printer_get_temp0(void) { + return s_temp0; +} + + +// set nozzle0 temp +void printer_set_temp0(uint16_t temp) { + s_temp0 = temp; +} + + +// get nozzle1 temp +uint16_t printer_get_temp1(void) { + return s_temp1; +} + + +// set nozzle1 temp +void printer_set_temp1(uint16_t temp) { + s_temp1 = temp; +} + + +// get bed temp +uint16_t printer_get_tempbed(void) { + return s_tempbed; +} + + +// set bed temp +void printer_set_tempbed(uint16_t temp) { + s_tempbed = temp; +} diff --git a/src/c/printer.h b/src/c/printer.h new file mode 100644 index 0000000..2dfd362 --- /dev/null +++ b/src/c/printer.h @@ -0,0 +1,31 @@ +#pragma once + +enum PRINTERSTATE { + LOADING, + OFFLINE, + CONNECTING, + OPERATIONAL, + PRINTING, + PAUSED, + ERROR +}; + + +enum PRINTERSTATE printer_get_state(void); +void printer_set_state(enum PRINTERSTATE); +void printer_set_state_by_char(const char *); +char *printer_get_state_text(void); +uint32_t printer_get_finish_timestamp(void); +void printer_set_finish_timestamp(uint32_t); +char *printer_get_remaining(void); +void printer_set_remaining(const char *); +char *printer_get_filename(void); +void printer_set_filename(const char *); +uint8_t printer_get_progress(void); +void printer_set_progress(uint8_t); +uint16_t printer_get_temp0(void); +void printer_set_temp0(uint16_t); +uint16_t printer_get_temp1(void); +void printer_set_temp1(uint16_t); +uint16_t printer_get_tempbed(void); +void printer_set_tempbed(uint16_t); diff --git a/src/c/progress_layer.c b/src/c/progress_layer.c new file mode 100644 index 0000000..dccc91a --- /dev/null +++ b/src/c/progress_layer.c @@ -0,0 +1,105 @@ +// OctoWatch2 +// A Pebble watch app for monitoring and basic controlling of 3D printers via Octoprint +// +// Licence: CC BY-SA 3.0, http://creativecommons.org/licenses/by-sa/3.0/ +// Author: Dominik Scholz , go4u.de Webdesign + +// progress_layer +// taken from https://github.com/pebble-examples/ui-patterns/ (MIT License) +// +// Licence: The MIT License (MIT) +// Author: C-D-Lewis + +#include +#include "progress_layer.h" + +#define MIN(a,b) (((a)<(b))?(a):(b)) + +typedef struct { + int16_t progress_percent; + int16_t corner_radius; + GColor foreground_color; + GColor background_color; +} ProgressLayerData; + + +static int16_t scale_progress_bar_width_px(unsigned int progress_percent, int16_t rect_width_px) { + return ((progress_percent * (rect_width_px)) / 100); +} + + +static void progress_layer_update_proc(ProgressLayer* progress_layer, GContext* ctx) { + ProgressLayerData *data = (ProgressLayerData *)layer_get_data(progress_layer); + GRect bounds = layer_get_bounds(progress_layer); + + int16_t progress_bar_width_px = scale_progress_bar_width_px(data->progress_percent, bounds.size.w); + GRect progress_bar = GRect(bounds.origin.x, bounds.origin.y, progress_bar_width_px, bounds.size.h); + + graphics_context_set_fill_color(ctx, data->background_color); + graphics_fill_rect(ctx, bounds, data->corner_radius, GCornersAll); + + graphics_context_set_fill_color(ctx, data->foreground_color); + graphics_fill_rect(ctx, progress_bar, data->corner_radius, GCornersAll); + +#ifdef PBL_PLATFORM_APLITE + graphics_context_set_stroke_color(ctx, data->background_color); + graphics_draw_rect(ctx, progress_bar); +#endif +} + + +ProgressLayer* progress_layer_create(GRect frame) { + ProgressLayer *progress_layer = layer_create_with_data(frame, sizeof(ProgressLayerData)); + layer_set_update_proc(progress_layer, progress_layer_update_proc); + layer_mark_dirty(progress_layer); + + ProgressLayerData *data = (ProgressLayerData *)layer_get_data(progress_layer); + data->progress_percent = 0; + data->corner_radius = 1; + data->foreground_color = GColorBlack; + data->background_color = GColorWhite; + + return progress_layer; +} + + +void progress_layer_destroy(ProgressLayer* progress_layer) { + if (progress_layer) { + layer_destroy(progress_layer); + } +} + + +void progress_layer_increment_progress(ProgressLayer* progress_layer, int16_t progress) { + ProgressLayerData *data = (ProgressLayerData *)layer_get_data(progress_layer); + data->progress_percent = MIN(100, data->progress_percent + progress); + layer_mark_dirty(progress_layer); +} + + +void progress_layer_set_progress(ProgressLayer* progress_layer, int16_t progress_percent) { + ProgressLayerData *data = (ProgressLayerData *)layer_get_data(progress_layer); + data->progress_percent = MIN(100, progress_percent); + layer_mark_dirty(progress_layer); +} + + +void progress_layer_set_corner_radius(ProgressLayer* progress_layer, uint16_t corner_radius) { + ProgressLayerData *data = (ProgressLayerData *)layer_get_data(progress_layer); + data->corner_radius = corner_radius; + layer_mark_dirty(progress_layer); +} + + +void progress_layer_set_foreground_color(ProgressLayer* progress_layer, GColor color) { + ProgressLayerData *data = (ProgressLayerData *)layer_get_data(progress_layer); + data->foreground_color = color; + layer_mark_dirty(progress_layer); +} + + +void progress_layer_set_background_color(ProgressLayer* progress_layer, GColor color) { + ProgressLayerData *data = (ProgressLayerData *)layer_get_data(progress_layer); + data->background_color = color; + layer_mark_dirty(progress_layer); +} \ No newline at end of file diff --git a/src/c/progress_layer.h b/src/c/progress_layer.h new file mode 100644 index 0000000..6b5d103 --- /dev/null +++ b/src/c/progress_layer.h @@ -0,0 +1,13 @@ +#pragma once + +#include + +typedef Layer ProgressLayer; + +ProgressLayer* progress_layer_create(GRect frame); +void progress_layer_destroy(ProgressLayer* progress_layer); +void progress_layer_increment_progress(ProgressLayer* progress_layer, int16_t progress); +void progress_layer_set_progress(ProgressLayer* progress_layer, int16_t progress_percent); +void progress_layer_set_corner_radius(ProgressLayer* progress_layer, uint16_t corner_radius); +void progress_layer_set_foreground_color(ProgressLayer* progress_layer, GColor color); +void progress_layer_set_background_color(ProgressLayer* progress_layer, GColor color); \ No newline at end of file diff --git a/src/c/window_main.c b/src/c/window_main.c new file mode 100644 index 0000000..72e530b --- /dev/null +++ b/src/c/window_main.c @@ -0,0 +1,312 @@ +// OctoWatch2 +// A Pebble watch app for monitoring and basic controlling of 3D printers via Octoprint +// +// Licence: CC BY-SA 3.0, http://creativecommons.org/licenses/by-sa/3.0/ +// Author: Dominik Scholz , go4u.de Webdesign + +#include +#include "window_main.h" +#include "printer.h" +#include "messaging.h" +#include "progress_layer.h" + +#define TIMER_INTERVAL 5*1000 + +static Window *s_window_main; + +static ActionBarLayer *s_actionbar; + +static GBitmap *image_connect; +static GBitmap *image_refresh; +static GBitmap *image_pause; +static GBitmap *image_start; +static GBitmap *image_cancel; +static GBitmap *image_nozzle; +static GBitmap *image_bed; + +static Layer *s_bg_layer; + +static ProgressLayer *s_progress_layer; + +static TextLayer *s_layer_time_remaining_counter; +static TextLayer *s_layer_filename; +static TextLayer *s_layer_nozzle; +static TextLayer *s_layer_bed; +static TextLayer *s_layer_state; + +static BitmapLayer *s_bitmaplayer_nozzle; +static BitmapLayer *s_bitmaplayer_bed; + + +// handle click on up button +static void window_main_actionbar_up(ClickRecognizerRef recognizer, void *context) { + messaging_outbox_send("pause"); +} + + +// handle click on select button +static void window_main_actionbar_select(ClickRecognizerRef recognizer, void *context) { + messaging_outbox_send("update"); +} + + +// handle click on down button +static void window_main_actionbar_down(ClickRecognizerRef recognizer, void *context) { + messaging_outbox_send("cancel"); +} + + +// action bar config +void window_main_actionbar_config(void *context) { + window_single_click_subscribe(BUTTON_ID_UP, (ClickHandler) window_main_actionbar_up); + window_single_click_subscribe(BUTTON_ID_SELECT, (ClickHandler) window_main_actionbar_select); + window_single_click_subscribe(BUTTON_ID_DOWN, (ClickHandler) window_main_actionbar_down); +} + + +// render background layer +static void window_main_draw_background_layer(Layer *layer, GContext *context) { + GRect bounds = layer_get_bounds(layer); + + GColor8 s_state_colors[] = { + PBL_IF_COLOR_ELSE(GColorDarkGray, GColorWhite), // LOADING + PBL_IF_COLOR_ELSE(GColorRed, GColorWhite), // OFFLINE + PBL_IF_COLOR_ELSE(GColorOrange, GColorWhite), // CONNECTING + PBL_IF_COLOR_ELSE(GColorCyan, GColorWhite), // OPERATIONAL + PBL_IF_COLOR_ELSE(GColorBrightGreen, GColorWhite), // PRINTING + PBL_IF_COLOR_ELSE(GColorYellow, GColorWhite), // PAUSED + PBL_IF_COLOR_ELSE(GColorFolly, GColorWhite) // ERROR + }; + + graphics_context_set_fill_color(context, GColorBlack); + + // time remaining container + graphics_fill_rect(context, GRect(0, 0, bounds.size.w, 46), 4, GCornersAll); + + // progress bar + graphics_fill_rect(context, GRect(0, 48, bounds.size.w, 10), 4, GCornersAll); + + // filename container + graphics_fill_rect(context, GRect(0, 65, bounds.size.w, 51), 4, GCornersAll); + //graphics_draw_round_rect(context, GRect(0, 77, bounds.size.w, 36), 4); + + // nozzle/bed container + graphics_fill_rect(context, GRect(0, 118, bounds.size.w, 22), 4, GCornersAll); + + // status container + graphics_context_set_fill_color(context, s_state_colors[printer_get_state()]); + graphics_fill_rect(context, GRect(0, bounds.size.h - 22, bounds.size.w, 22), 4, GCornersAll); +} + + +// load window handler +static void window_main_load_handler(Window *window) { + // create window layer + Layer *window_layer = window_get_root_layer(window); + GRect bounds = layer_get_bounds(window_layer); + + // load images + image_connect = gbitmap_create_with_resource(RESOURCE_ID_CONNECT_ICON); + image_refresh = gbitmap_create_with_resource(RESOURCE_ID_ELLIPSIS_ICON); + image_start = gbitmap_create_with_resource(RESOURCE_ID_PLAY_ICON); + image_pause = gbitmap_create_with_resource(RESOURCE_ID_PAUSE_ICON); + image_cancel = gbitmap_create_with_resource(RESOURCE_ID_CANCEL_ICON); + image_nozzle = gbitmap_create_with_resource(RESOURCE_ID_NOZZLE_ICON); + image_bed = gbitmap_create_with_resource(RESOURCE_ID_BED_ICON); + + // create action bar + s_actionbar = action_bar_layer_create(); + action_bar_layer_add_to_window(s_actionbar, window); + action_bar_layer_set_click_config_provider(s_actionbar, window_main_actionbar_config); + action_bar_layer_set_icon(s_actionbar, BUTTON_ID_UP, image_connect); + action_bar_layer_set_icon(s_actionbar, BUTTON_ID_SELECT, image_refresh); + action_bar_layer_set_icon(s_actionbar, BUTTON_ID_DOWN, image_cancel); + + // background layer + s_bg_layer = layer_create(GRect(2, 2, (bounds.size.w - ACTION_BAR_WIDTH - 4), bounds.size.h - 4)); + layer_set_update_proc(s_bg_layer, window_main_draw_background_layer); + layer_add_child(window_layer, s_bg_layer); + GRect bg_bounds = layer_get_bounds(s_bg_layer); + + // create time remaining counter + s_layer_time_remaining_counter = text_layer_create(GRect(0, 0, bg_bounds.size.w, 46)); + text_layer_set_text_color(s_layer_time_remaining_counter, PBL_IF_COLOR_ELSE(GColorYellow, GColorWhite)); + text_layer_set_background_color(s_layer_time_remaining_counter, GColorClear); + text_layer_set_font(s_layer_time_remaining_counter, fonts_get_system_font(FONT_KEY_BITHAM_34_MEDIUM_NUMBERS)); + text_layer_set_text_alignment(s_layer_time_remaining_counter, GTextAlignmentCenter); + text_layer_set_text(s_layer_time_remaining_counter, "--:--"); + layer_add_child(s_bg_layer, text_layer_get_layer(s_layer_time_remaining_counter)); + + // create progress layer + s_progress_layer = progress_layer_create(GRect(2, 50, bg_bounds.size.w - 4, 6)); + progress_layer_set_corner_radius(s_progress_layer, 2); + progress_layer_set_foreground_color(s_progress_layer, PBL_IF_COLOR_ELSE(GColorWhite, GColorWhite)); + progress_layer_set_background_color(s_progress_layer, PBL_IF_COLOR_ELSE(GColorDarkGray, GColorBlack)); + layer_add_child(s_bg_layer, s_progress_layer); + + // create filename + s_layer_filename = text_layer_create(GRect(4, 65, bg_bounds.size.w - 8, 45)); + text_layer_set_text_color(s_layer_filename, PBL_IF_COLOR_ELSE(GColorWhite, GColorWhite)); + text_layer_set_background_color(s_layer_filename, GColorClear); + text_layer_set_font(s_layer_filename, fonts_get_system_font(FONT_KEY_GOTHIC_14)); + text_layer_set_text_alignment(s_layer_filename, GTextAlignmentLeft); + text_layer_set_overflow_mode(s_layer_filename, GTextOverflowModeWordWrap); + text_layer_set_text(s_layer_filename, "loading..."); + layer_add_child(s_bg_layer, text_layer_get_layer(s_layer_filename)); + + // create nozzle temp + s_layer_nozzle = text_layer_create(GRect(13, 119, 60, 14)); + text_layer_set_text_color(s_layer_nozzle, PBL_IF_COLOR_ELSE(GColorWhite, GColorWhite)); + text_layer_set_background_color(s_layer_nozzle, GColorClear); + text_layer_set_font(s_layer_nozzle, fonts_get_system_font(FONT_KEY_GOTHIC_14)); + text_layer_set_text_alignment(s_layer_nozzle, GTextAlignmentLeft); + text_layer_set_overflow_mode(s_layer_nozzle, GTextOverflowModeFill); + text_layer_set_text(s_layer_nozzle, "---/---"); + layer_add_child(s_bg_layer, text_layer_get_layer(s_layer_nozzle)); + + // create bed temp + s_layer_bed = text_layer_create(GRect(83, 119, 30, 14)); + text_layer_set_text_color(s_layer_bed, PBL_IF_COLOR_ELSE(GColorWhite, GColorWhite)); + text_layer_set_background_color(s_layer_bed, GColorClear); + text_layer_set_font(s_layer_bed, fonts_get_system_font(FONT_KEY_GOTHIC_14)); + text_layer_set_text_alignment(s_layer_bed, GTextAlignmentLeft); + text_layer_set_text(s_layer_bed, "---"); + layer_add_child(s_bg_layer, text_layer_get_layer(s_layer_bed)); + + // create state + s_layer_state = text_layer_create(GRect(4, bg_bounds.size.h - 24, bg_bounds.size.w - 8, 24)); + text_layer_set_text_color(s_layer_state, PBL_IF_COLOR_ELSE(GColorDarkGray, GColorWhite)); + text_layer_set_background_color(s_layer_state, GColorClear); + text_layer_set_font(s_layer_state, fonts_get_system_font(FONT_KEY_GOTHIC_18)); + text_layer_set_text_alignment(s_layer_state, GTextAlignmentLeft); + text_layer_set_overflow_mode(s_layer_filename, GTextOverflowModeTrailingEllipsis); + text_layer_set_text(s_layer_state, "loading..."); + layer_add_child(s_bg_layer, text_layer_get_layer(s_layer_state)); + + // create bitmaplayer nozzle + s_bitmaplayer_nozzle = bitmap_layer_create(GRect(4, 123, 7, 11)); + bitmap_layer_set_compositing_mode(s_bitmaplayer_nozzle, GCompOpSet); + bitmap_layer_set_bitmap(s_bitmaplayer_nozzle, image_nozzle); + layer_add_child(s_bg_layer, bitmap_layer_get_layer(s_bitmaplayer_nozzle)); + + // create bitmaplayer bed + s_bitmaplayer_bed = bitmap_layer_create(GRect(74, 123, 7, 11)); + bitmap_layer_set_compositing_mode(s_bitmaplayer_bed, GCompOpSet); + bitmap_layer_set_bitmap(s_bitmaplayer_bed, image_bed); + layer_add_child(s_bg_layer, bitmap_layer_get_layer(s_bitmaplayer_bed)); + + // start timer to auto-update + tick_timer_service_subscribe(MINUTE_UNIT, window_main_timer_fired); +} + + +// unload window handler +static void window_main_unload_handler(Window *window) { + // destroy action bar + action_bar_layer_remove_from_window(s_actionbar); + action_bar_layer_destroy(s_actionbar); + + // destroy images + gbitmap_destroy(image_connect); + gbitmap_destroy(image_refresh); + gbitmap_destroy(image_pause); + gbitmap_destroy(image_start); + gbitmap_destroy(image_cancel); + gbitmap_destroy(image_nozzle); + gbitmap_destroy(image_bed); + + // destroy progress layer + progress_layer_destroy(s_progress_layer); + + // destroy text layers + text_layer_destroy(s_layer_time_remaining_counter); + text_layer_destroy(s_layer_filename); + text_layer_destroy(s_layer_nozzle); + text_layer_destroy(s_layer_bed); + text_layer_destroy(s_layer_state); + + // destroy bitmap layers + bitmap_layer_destroy(s_bitmaplayer_nozzle); + bitmap_layer_destroy(s_bitmaplayer_bed); + + // destroy background layer + layer_destroy(s_bg_layer); +} + + +// set time remaining counter +void window_main_set_time_remaing_counter(const char *value) { + text_layer_set_text(s_layer_time_remaining_counter, value); +} + + +// set filename +void window_main_set_filename(const char *value) { + text_layer_set_text(s_layer_filename, value); +} + + +// set progress +void window_main_set_progress(const uint8_t progress) { + progress_layer_set_progress(s_progress_layer, progress); +} + + +// set nozzle0 temp +void window_main_set_nozzle(const uint16_t temp0, const uint16_t temp1) { + static char msg[14] = ""; + char t0[4] = "---"; + char t1[4] = "---"; + + if (temp0 > 1) snprintf(t0, sizeof(t0), "%u", (uint)temp0); + if (temp1 > 1) snprintf(t1, sizeof(t1), "%u", (uint)temp1); + + if (temp1 > 0) snprintf(msg, sizeof(msg), "%s°/%s°", t0, t1); + else snprintf(msg, sizeof(msg), "%s°", t0); + + text_layer_set_text(s_layer_nozzle, msg); +} + + +// set bed temp +void window_main_set_bed(const uint16_t temp) { + static char msg[8] = "---"; + if (temp > 1) snprintf(msg, sizeof(msg), "%u°", temp); + text_layer_set_text(s_layer_bed, msg); +} + + +//set state +void window_main_set_state(const char *value) { + text_layer_set_text(s_layer_state, value); + text_layer_set_text_color(s_layer_state, GColorBlack); + + if (printer_get_state() == OFFLINE) action_bar_layer_set_icon(s_actionbar, BUTTON_ID_UP, image_connect); + else if (printer_get_state() == PRINTING) action_bar_layer_set_icon(s_actionbar, BUTTON_ID_UP, image_pause); + else action_bar_layer_set_icon(s_actionbar, BUTTON_ID_UP, image_start); +} + + +// update data +void window_main_timer_fired(struct tm *tick_time, TimeUnits units_changed) { + APP_LOG(APP_LOG_LEVEL_DEBUG, "timer fired, reloading"); + messaging_outbox_send("update"); +} + + +// create new window +void window_main_init(void) { + s_window_main = window_create(); + window_set_background_color(s_window_main, PBL_IF_COLOR_ELSE(GColorDarkGreen, GColorWhite)); + window_set_window_handlers(s_window_main, (WindowHandlers) { + .load = window_main_load_handler, + .unload = window_main_unload_handler + }); + window_stack_push(s_window_main, true); +} + + +// destroy window +void window_main_destroy(void) { + window_destroy(s_window_main); +} diff --git a/src/c/window_main.h b/src/c/window_main.h new file mode 100644 index 0000000..2b836c7 --- /dev/null +++ b/src/c/window_main.h @@ -0,0 +1,17 @@ +#pragma once + +//static void window_main_actionbar_up(ClickRecognizerRef, void *); +//static void window_main_actionbar_select(ClickRecognizerRef, void *); +//static void window_main_actionbar_down(ClickRecognizerRef, void *); +void window_main_actionbar_config(void *); +//static void window_main_load_handler(Window *); +//static void window_main_unload_handler(Window *); +void window_main_set_time_remaing_counter(const char *); +void window_main_set_filename(const char *); +void window_main_set_nozzle(const uint16_t, const uint16_t); +void window_main_set_bed(const uint16_t); +void window_main_set_state(const char *); +void window_main_set_progress(const uint8_t); +void window_main_timer_fired(struct tm *, TimeUnits); +void window_main_init(void); +void window_main_destroy(void); diff --git a/src/pkjs/ajax.js b/src/pkjs/ajax.js new file mode 100644 index 0000000..7f99e7f --- /dev/null +++ b/src/pkjs/ajax.js @@ -0,0 +1,81 @@ +var Ajax = { + + usessl: localStorage.getItem('octoprintusessl') > 0, + host: localStorage.getItem('octoprinthost'), + port: localStorage.getItem('octoprintport'), + apikey: localStorage.getItem('octoprintapikey'), + usebasicauth: localStorage.getItem('octoprintusebasicauth'), + auth: { + user: localStorage.getItem('octoprintuser'), + password: localStorage.getItem('octoprintpassword') + }, + + + // send ajax call + send: function(method, path, params, onSuccess, onError) { + + var url = 'http' + (Ajax.usessl ? 's' : '') + '://' + Ajax.host + ':' + Ajax.port + path; + console.log("calling " + url); + + var req = new XMLHttpRequest(), + mergedParams = [], + isMethodPost = method.toLowerCase() === 'post'; + + // add apikey to params, if get + if (!isMethodPost) + params.apikey = Ajax.apikey; + + // merge params + for (var key in params) + mergedParams.push(key + '=' + encodeURIComponent(params[key])); + + // open request + var mergedUrl = url + (isMethodPost ? '' : ('?' + mergedParams.join('&'))); + req.open(method.toUpperCase(), mergedUrl, true); + + // if credentials given, use http basic auth + if (Ajax.usebasicauth) + req.setRequestHeader('Authorization', 'Basic ' + Base64.encode(Ajax.auth.user + ':' + Ajax.auth.password)); + + // set proper header for post + if (isMethodPost) { + req.setRequestHeader('Content-Type', 'application/json'); + req.setRequestHeader('X-API-KEY', Ajax.apikey); + } + + // register callback handlers + req.onload = function () { + if (req.readyState === 4) { + if (req.status === 204) { + console.log('ajaxCall: got empty response!'); + onSuccess({}); + } + else if (req.status === 200) { + console.log('ajaxCall: got data!'); + try { + onSuccess(JSON.parse(req.responseText)); + } + catch(e) { + console.log('ajaxCall: could not parse data or call success callback function!' + req.responseText); + } + } + else if (req.status === 401) { + console.log('ajaxCall: could not authenticate!', req.responseText); + onError(req.responseText); + } + else { + console.log('ajaxCall: error receiving data!', req.responseText); + onError(req.responseText); + } + } + }; + + // send request + console.log('ajaxCall: sent data with ' + method.toLowerCase() + ' method'); + req.send(isMethodPost ? JSON.stringify(params) : null); + } +}; + +module.exports = Ajax; + +var Base64 = require('./base64.js'); \ No newline at end of file diff --git a/src/pkjs/base64.js b/src/pkjs/base64.js new file mode 100644 index 0000000..2ffe17c --- /dev/null +++ b/src/pkjs/base64.js @@ -0,0 +1,50 @@ +var Base64 = { + _keyStr : "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=", + encode : function (input) { + var output = ""; + var chr1, chr2, chr3, enc1, enc2, enc3, enc4; + var i = 0; + input = Base64._utf8_encode(input); + while (i < input.length) { + chr1 = input.charCodeAt(i++); + chr2 = input.charCodeAt(i++); + chr3 = input.charCodeAt(i++); + enc1 = chr1 >> 2; + enc2 = ((chr1 & 3) << 4) | (chr2 >> 4); + enc3 = ((chr2 & 15) << 2) | (chr3 >> 6); + enc4 = chr3 & 63; + if (isNaN(chr2)) { + enc3 = enc4 = 64; + } else if (isNaN(chr3)) { + enc4 = 64; + } + output = output + + this._keyStr.charAt(enc1) + this._keyStr.charAt(enc2) + + this._keyStr.charAt(enc3) + this._keyStr.charAt(enc4); + } + return output; + }, + + _utf8_encode : function (string) { + string = string.replace(/\r\n/g,"\n"); + var utftext = ""; + for (var n = 0; n < string.length; n++) { + var c = string.charCodeAt(n); + if (c < 128) { + utftext += String.fromCharCode(c); + } + else if((c > 127) && (c < 2048)) { + utftext += String.fromCharCode((c >> 6) | 192); + utftext += String.fromCharCode((c & 63) | 128); + } + else { + utftext += String.fromCharCode((c >> 12) | 224); + utftext += String.fromCharCode(((c >> 6) & 63) | 128); + utftext += String.fromCharCode((c & 63) | 128); + } + } + return utftext; + } +}; + +module.exports = Base64; \ No newline at end of file diff --git a/src/pkjs/config.js b/src/pkjs/config.js new file mode 100644 index 0000000..2a33761 --- /dev/null +++ b/src/pkjs/config.js @@ -0,0 +1,81 @@ +module.exports = [ + { + "type": "heading", + "defaultValue": "OctoWatch2 Configuration" + }, + { + "type": "text", + "defaultValue": "Please enter your Octoprint configuration." + }, + { + "type": "section", + "items": [ + { + "type": "heading", + "defaultValue": "Basic Settings" + }, + { + "type": "toggle", + "messageKey": "usessl", + "label": "Use HTTPS", + "defaultValue": localStorage.getItem('octoprintusessl') == '1' || false, + "description": "Activate this function, if you have enabled https on your Octoprint setup and want to encrypt the network traffic." + }, + { + "type": "input", + "messageKey": "host", + "label": "Host", + "defaultValue": localStorage.getItem('octoprinthost') || "octopi.local", + "description": "Enter the IP or network name of your Octoprint server. Default is octopi.local" + }, + { + "type": "input", + "messageKey": "port", + "label": "Port", + "defaultValue": localStorage.getItem('octoprintport') || "80", + "description": "Enter the port number of your Octoprint server. Default is 80" + }, + { + "type": "input", + "messageKey": "apikey", + "label": "APIKEY", + "defaultValue": localStorage.getItem('octoprintapikey') || "", + "description": "Enter the APIKEY of your Octoprint server. Must be generated in Octoprint settings before." + } + ] + }, + { + "type": "section", + "items": [ + { + "type": "heading", + "defaultValue": "Advanced Settings" + }, + { + "type": "toggle", + "messageKey": "usebasicauth", + "label": "Use HTTP Basic Auth", + "defaultValue": localStorage.getItem('octoprintusebasicauth') == '1' || false, + "description": "Activate this, if your Octoprint server is tunneled through HAProxy and you need to use HTTP Basic Auth." + }, + { + "type": "input", + "messageKey": "user", + "label": "User", + "defaultValue": localStorage.getItem('octoprintuser') || "", + "description": "Enter the username of your HTTP Basic Auth." + }, + { + "type": "input", + "messageKey": "password", + "label": "Password", + "defaultValue": localStorage.getItem('octoprintpassword') || "", + "description": "Enter the password of your HTTP Basic Auth." + } + ] + }, + { + "type": "submit", + "defaultValue": "Save Settings" + } +]; \ No newline at end of file diff --git a/src/pkjs/index.js b/src/pkjs/index.js new file mode 100644 index 0000000..989f5f5 --- /dev/null +++ b/src/pkjs/index.js @@ -0,0 +1,45 @@ +var Clay = require('pebble-clay'), + clayConfig = require('./config'), + clay = new Clay(clayConfig, null, {autoHandleEvents: false}), + Octowatch = require('./octowatch.js'), + Octoprint = require('./octoprint.js'); + + +// on app loaded +Pebble.addEventListener('ready', function (e) { + console.log('connect!'); + Octoprint.fetchStatus(); +}); + + +// on received message +Pebble.addEventListener('appmessage', function (e) { + console.log('received data from pebble: ' + JSON.stringify(e.payload)); + Octowatch.handleCommand(e.payload); +}); + + +// configuration opened +Pebble.addEventListener('showConfiguration', function(e) { + console.log('show configuration!'); + Pebble.openURL(clay.generateUrl()); +}); + + +// on app closed +Pebble.addEventListener('webviewclosed', function (e) { + console.log('webview closed!'); + if (e && !e.response) return; + + var dict = clay.getSettings(e.response, false), + value; + + for (var key in dict) { + value = dict[key].value; + if (value === true) value = 1; + if (value === false) value = 0; + console.log("save setting " + key + ": " + value); + localStorage.setItem('octoprint' + key, value); + } + +}); diff --git a/src/pkjs/octoprint.js b/src/pkjs/octoprint.js new file mode 100644 index 0000000..4c80285 --- /dev/null +++ b/src/pkjs/octoprint.js @@ -0,0 +1,139 @@ +var Octoprint = { + + path: { + job: '/api/job', + printer: '/api/printer', + connection: '/api/connection' + }, + + + // fetch status + fetchStatus: function() { + Octoprint.fetchJobStatus(); + Octoprint.fetchPrinterStatus(); + }, + + + // fetch current job status + fetchJobStatus: function() { + Ajax.send( + 'GET', + Octoprint.path.job, + {}, + // got response + function(data) { + console.log('fetchJobStatus: received data'); + + // save previous state + Octowatch.data.previousState = Octowatch.data.state; + + // get data + Octowatch.data.state = data.state.split(':', 1).join(''); + Octowatch.data.progress = data.progress.completion; + + if (!data.job.file.name) { + Octowatch.data.filename = 'no file selected'; + Octowatch.data.remainingTime = 0; + } + else { + Octowatch.data.filename = data.job.file.name; + Octowatch.data.remainingTime = Math.round(data.progress.printTimeLeft ? data.progress.printTimeLeft : data.job.estimatedPrintTime); + } + + Octowatch.updateJobState(); + }, + // ajax call failed + function(data) { + console.log('fetchJobStatus: call failed ' + data); + } + ); + }, + + + // fetch current printer status + fetchPrinterStatus: function() { + Ajax.send( + 'GET', + Octoprint.path.printer, + {}, + // got response + function(data) { + console.log('fetchPrinterStatus: received data'); + + // get data + if (data.temperature.tool0 && data.temperature.tool0.actual) Octowatch.data.temp0 = Math.round(data.temperature.tool0.actual); + if (data.temperature.tool1 && data.temperature.tool1.actual) Octowatch.data.temp1 = Math.round(data.temperature.tool1.actual); + if (data.temperature.bed && data.temperature.bed.actual) Octowatch.data.tempbed = Math.round(data.temperature.bed.actual); + + Octowatch.updatePrinterState(); + }, + // ajax call failed + function(data) { + console.log('fetchPrinterStatus: call failed ' + data); + } + ); + }, + + + // pause or resume current job + pauseResumeJob: function() { + Ajax.send( + 'POST', + Octoprint.path.job, + {command: (Octowatch.data.state != 'Printing' && Octowatch.data.state != 'Paused') ? 'start' : 'pause'}, + // got response + function(data) { + console.log('pauseResumeJob: received data'); + Octoprint.fetchJobStatus(); + }, + // ajax call failed + function(data) { + console.log('pauseResumeJob: call failed ' + data); + } + ); + }, + + + // pause or resume current job + cancelJob: function() { + Ajax.send( + 'POST', + Octoprint.path.job, + {command: 'cancel'}, + // got response + function(data) { + console.log('cancelJob: received data'); + Octoprint.fetchJobStatus(); + }, + // ajax call failed + function(data) { + console.log('cancelJob: call failed ' + data); + } + ); + }, + + + // connect to printer + connectPrinter: function() { + Ajax.send( + 'POST', + Octoprint.path.connection, + {command: 'connect'}, + // got response + function(data) { + console.log('connectPrinter: received data'); + Octoprint.fetchJobStatus(); + }, + // ajax call failed + function(data) { + console.log('connectPrinter: call failed ' + data); + } + ); + } + +}; + +module.exports = Octoprint; + +var Ajax = require('./ajax.js'), + Octowatch = require('./octowatch.js'); \ No newline at end of file diff --git a/src/pkjs/octowatch.js b/src/pkjs/octowatch.js new file mode 100644 index 0000000..8cdb32a --- /dev/null +++ b/src/pkjs/octowatch.js @@ -0,0 +1,95 @@ +var Octowatch = { + + // data + data: { + filename: 'no file selected', + timeRemaining: '--:--', + state: 'Loading', + previousState: 'Loading', + progress: 0, + temp0: 0, + temp1: 0, + tempbed: 0 + }, + + + // handle an incoming command + handleCommand: function(data) { + switch(data[0]) { + // update printer status + case 'update': + Octoprint.fetchStatus(); + break; + + // pause/resume print job + case 'pause': + if (Octowatch.data.state == 'Offline') Octoprint.connectPrinter(); + else Octoprint.pauseResumeJob(); + break; + + // cancel print job + case 'cancel': + Octoprint.cancelJob(); + break; + } + }, + + + // update job state + updateJobState: function() { + Octowatch.updatePebble({ + '1': Octowatch.data.filename, + '2': Octowatch.data.remainingTime >= 0 ? Octowatch.secondsToReadableTime(Octowatch.data.remainingTime) : '--:--', + '3': Octowatch.data.state, + '4': Math.floor(Octowatch.data.progress), + '5': Octowatch.data.remainingTime >= 0 ? Math.floor(new Date().getTime() / 1000 + parseInt(Octowatch.data.remainingTime)) : 0 + }); + }, + + + // update printer state + updatePrinterState: function() { + Octowatch.updatePebble({ + '6': Octowatch.data.temp0, + '7': Octowatch.data.temp1, + '8': Octowatch.data.tempbed + }); + }, + + + // update data on pebble watch + updatePebble: function(data) { + Pebble.sendAppMessage( + data, + // sent data to pebble successfully + function() { + console.log('updatePebble: sent data successfully'); + }, + // some error occured + function() { + console.log('updatePebble: sending data failed'); + } + ); + + // show notification + if (Octowatch.data.previousState === 'Printing' && Octowatch.data.state === 'Operational' && Octowatch.data.progress === 100.0) { + var d = new Date(); + Pebble.showSimpleNotificationOnPebble('Print completed', Octowatch.data.filename + ' finished printing at ' + d); + } + }, + + + // convert seconds to readable format HH:MM + secondsToReadableTime: function(seconds) { + var hours = Math.floor(seconds / 3600), + minutes = Math.ceil(seconds % 3600 / 60); + if (hours < 10) hours = '0' + hours; + if (minutes < 10) minutes = '0' + minutes; + return hours + ':' + minutes; + } + +}; + +module.exports = Octowatch; + +var Octoprint = require('./octoprint.js'); \ No newline at end of file diff --git a/wscript b/wscript new file mode 100644 index 0000000..9282b27 --- /dev/null +++ b/wscript @@ -0,0 +1,52 @@ +# +# This file is the default set of rules to compile a Pebble project. +# +# Feel free to customize this to your needs. +# + +import os.path +try: + from sh import CommandNotFound, jshint, cat, ErrorReturnCode_2 + hint = jshint +except (ImportError, CommandNotFound): + hint = None + +top = '.' +out = 'build' + + +def options(ctx): + ctx.load('pebble_sdk') + + +def configure(ctx): + ctx.load('pebble_sdk') + + +def build(ctx): + if False and hint is not None: + try: + hint([node.abspath() for node in ctx.path.ant_glob("src/**/*.js")], _tty_out=False) # no tty because there are none in the cloudpebble sandbox. + except ErrorReturnCode_2 as e: + ctx.fatal("\nJavaScript linting failed (you can disable this in Project Settings):\n" + e.stdout) + + ctx.load('pebble_sdk') + + build_worker = os.path.exists('worker_src') + binaries = [] + + for p in ctx.env.TARGET_PLATFORMS: + ctx.set_env(ctx.all_envs[p]) + ctx.set_group(ctx.env.PLATFORM_NAME) + app_elf = '{}/pebble-app.elf'.format(ctx.env.BUILD_DIR) + ctx.pbl_program(source=ctx.path.ant_glob('src/c/**/*.c'), target=app_elf) + + if build_worker: + worker_elf = '{}/pebble-worker.elf'.format(ctx.env.BUILD_DIR) + binaries.append({'platform': p, 'app_elf': app_elf, 'worker_elf': worker_elf}) + ctx.pbl_worker(source=ctx.path.ant_glob('worker_src/c/**/*.c'), target=worker_elf) + else: + binaries.append({'platform': p, 'app_elf': app_elf}) + + ctx.set_group('bundle') + ctx.pbl_bundle(binaries=binaries, js=ctx.path.ant_glob(['src/pkjs/**/*.js', 'src/pkjs/**/*.json']), js_entry_file='src/pkjs/index.js')