commit 78c06b3dcba47e449cdbfdadb628427acae7608c Author: tmeissner Date: Sat Oct 24 17:02:50 2015 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9eaef5d --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ + +# Ignore build generated files +build + +# Ignore waf lock file +.lock-waf* diff --git a/appinfo.json b/appinfo.json new file mode 100644 index 0000000..06a1d7f --- /dev/null +++ b/appinfo.json @@ -0,0 +1,52 @@ +{ + "uuid": "3c6e0682-a917-4283-a27e-349f2de629fa", + "shortName": "create_a_c_watchface", + "longName": "create_a_c_watchface", + "companyName": "goodcleanfun", + "versionLabel": "1.0", + "sdkVersion": "3", + "targetPlatforms": ["basalt"], + "watchapp": { + "watchface": true + }, + "appKeys": { + "KEY_TEMPERATURE": 0, + "KEY_CONDITIONS": 1, + "KEY_ROOM_TEMP": 2 + }, + "capabilities": + [ "location" ], + "resources": { + "media": [ + { + "type": "font", + "name": "FONT_PERFECT_DOS_16", + "file": "fonts/perfect-dos-vga.ttf", + "compatibility":"2.7" + }, + { + "type": "font", + "name": "FONT_PERFECT_DOS_20", + "file": "fonts/perfect-dos-vga.ttf", + "compatibility":"2.7" + }, + { + "type": "font", + "name": "FONT_PERFECT_DOS_24", + "file": "fonts/perfect-dos-vga.ttf", + "compatibility":"2.7" + }, + { + "type": "font", + "name": "FONT_PERFECT_DOS_48", + "file": "fonts/perfect-dos-vga.ttf", + "compatibility":"2.7" + }, + { + "type": "png", + "name": "TUTORIAL_BACKGROUND", + "file": "images/background.png" + } + ] + } +} diff --git a/resources/fonts/perfect-dos-vga.ttf b/resources/fonts/perfect-dos-vga.ttf new file mode 100644 index 0000000..d03b1c5 Binary files /dev/null and b/resources/fonts/perfect-dos-vga.ttf differ diff --git a/resources/images/background.png b/resources/images/background.png new file mode 100644 index 0000000..1b5e78c Binary files /dev/null and b/resources/images/background.png differ diff --git a/src/js/pebble-js-app.js b/src/js/pebble-js-app.js new file mode 100644 index 0000000..b314e4d --- /dev/null +++ b/src/js/pebble-js-app.js @@ -0,0 +1,132 @@ +var xhrRequest = function (url, type, callback) { + + var xhr = new XMLHttpRequest(); + + xhr.onload = function () { + callback(this.responseText); + }; + + xhr.open(type, url); + xhr.send(); + +}; + + +function locationSuccess(pos) { + + // generic API key, has to be replaced by developers one + var api_key = 'ca82d3c964da82f54d033abf702a46a5'; + + var lat = Math.round(pos.coords.latitude*10) / 10; + var lon = Math.round(pos.coords.longitude*10) / 10; + + // Construct URL + var url = 'http://api.openweathermap.org/data/2.5/weather?lat=' + + lat + '&lon=' + lon + '&APPID=' + api_key; + + // Send request to OpenWeatherMap + xhrRequest(url, 'GET', + function(responseText) { + // responseText contains a JSON object with weather info + var json = JSON.parse(responseText); + + // Temperature in Kelvin requires adjustment + var temperature = Math.round(json.main.temp - 273.15); + console.log('Temperature is ' + temperature); + + // Conditions + var conditions = json.weather[0].main; + console.log('Conditions are ' + conditions); + + // Assemble dictionary using our keys + var dictionary = { + 'KEY_TEMPERATURE': temperature, + 'KEY_CONDITIONS': conditions + }; + + // Send to Pebble + Pebble.sendAppMessage(dictionary, + function(e) { + console.log('Weather info sent to Pebble successfully!'); + }, + function(e) { + console.log('Error sending weather info to Pebble!'); + } + ); + } + ); + +} + + +function locationError(err) { + + console.log('Error requesting location!'); + +} + + +function getWeather() { + + navigator.geolocation.getCurrentPosition( + locationSuccess, + locationError, + {timeout: 15000, maximumAge: 60000} + ); + +} + + +function getHomeTemp() { + + var url = "https://raspi.goodcleanfun.de/cgi-bin/raspiweb.py?pwd=d3Vyc3RnZXNpY2h0&period=3"; + + console.log('Try to get room temp'); + + xhrRequest(url, 'POST', + function(responseText) { + // responseText contains a JSON object with weather info + var json = JSON.parse(responseText); + + // Conditions + var room_temp = (Math.round(json.room[0]*10) / 10) + '°C @ home'; + console.log('Room temp is ' + room_temp); + + // Assemble dictionary using our keys + var dictionary = { + 'KEY_ROOM_TEMP': room_temp + }; + + // Send to Pebble + Pebble.sendAppMessage(dictionary, + function(e) { + console.log('Room temp sent to Pebble successfully!'); + }, + function(e) { + console.log('Error sending room temp to Pebble!'); + } + ); + } + ); +} + + +// Listen for when the watchface is opened +Pebble.addEventListener('ready', + function(e) { + console.log('PebbleKit JS ready!'); + // Get the initial weather + getWeather(); + getHomeTemp(); + } +); + + +// Listen for when an AppMessage is received +Pebble.addEventListener('appmessage', + function(e) { + console.log('AppMessage received!'); + getWeather(); + getHomeTemp(); + } +); diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..b1cd0c6 --- /dev/null +++ b/src/main.c @@ -0,0 +1,301 @@ +#include +#include +#include "pebble_utils.h" + + +#define KEY_TEMPERATURE 0 +#define KEY_CONDITIONS 1 +#define KEY_ROOM_TEMP 2 +#define WEATHER_TIME 0 + + +// Function signatures +static void update_time(); +static void tick_handler(struct tm *tick_time, TimeUnits units_changed); +static void main_window_load(Window *window); +static void main_window_unload(Window *window); +static void init(); +static void deinit(); +static void inbox_received_callback(DictionaryIterator *iterator, void *context); +static void inbox_dropped_callback(AppMessageResult reason, void *context); +static void outbox_failed_callback(DictionaryIterator *iterator, AppMessageResult reason, void *context); +static void outbox_sent_callback(DictionaryIterator *iterator, void *context); + + +// Global variables +static Window *s_main_window; +static TextLayer *s_time_layer; +static TextLayer *s_weather_layer; +static TextLayer *s_room_layer; +static TextLayer *s_daymonth_layer; +static GFont s_time_font; +static GFont s_weather_font; +static GFont s_date_font; +static BitmapLayer *s_background_layer; +static GBitmap *s_background_bitmap; + + + +int main(void) { + + init(); + app_event_loop(); + deinit(); + +} + + +static void update_time() { + + // Get a tm structure + const time_t temp = time(NULL); + const struct tm *tick_time = localtime(&temp); + + // Create a long-lived buffer + static char buffer[] = "00:00"; + + // Write the current hours and minutes into the buffer + if (clock_is_24h_style()) { + // Use 24 hour format + strftime(buffer, sizeof(buffer), "%H:%M", tick_time); + } else { + // Use 12 hour format + strftime(buffer, sizeof(buffer), "%I:%M", tick_time); + } + + // Display this time on the TextLayer + text_layer_set_text(s_time_layer, buffer); + +} + + +static void update_date() { + + // Get a tm structure + const time_t temp = time(NULL); + const struct tm *tick_time = localtime(&temp); + + // Create a long-lived buffer + static char buffer_daymonth[] = "00.000"; + + // Write the current hours and minutes into the buffer + strftime(buffer_daymonth, sizeof(buffer_daymonth), "%e.%b", tick_time); + + // Display this time on the TextLayer + text_layer_set_text(s_daymonth_layer, buffer_daymonth); + +} + + +static void tick_handler(struct tm *tick_time, TimeUnits units_changed) { + + update_time(); + if (units_changed & DAY_UNIT) { + update_date(); + } + + if (persist_exists(WEATHER_TIME)) { + (void) persist_delete(WEATHER_TIME); + } + + // Get weather update every 30 minutes + if (tick_time->tm_min % 30 == 0) { + // Begin dictionary + DictionaryIterator *iter; + app_message_outbox_begin(&iter); + + // Add a key-value pair + dict_write_value(iter, 0, (uint8_t)0); + + // Send the message! + app_message_outbox_send(); + } + +} + + +static void inbox_received_callback(DictionaryIterator *iterator, void *context) { + + // Store incoming information + static char temperature_buffer[8]; + static char conditions_buffer[32]; + static char room_buffer[32]; + static char weather_layer_buffer[32]; + bool got_weather = false; + bool got_room = false; + + // Read first item + Tuple *t = dict_read_first(iterator); + + // For all items + while(t != NULL) { + + // Which key was received? + switch(t->key) { + case KEY_TEMPERATURE: + snprintf(temperature_buffer, sizeof(temperature_buffer), "%d", (int)t->value->int32); + break; + + case KEY_CONDITIONS: + snprintf(conditions_buffer, sizeof(conditions_buffer), "%s", t->value->cstring); + got_weather = true; + break; + + case KEY_ROOM_TEMP: + snprintf(room_buffer, sizeof(room_buffer), "%s", t->value->cstring); + got_room = true; + break; + + default: + APP_LOG(APP_LOG_LEVEL_ERROR, "Key %d not recognized!", (int)t->key); + break; + } + + // Look for next item + t = dict_read_next(iterator); + } + + // Assemble full string and display + if (got_weather) { + snprintf(weather_layer_buffer, sizeof(weather_layer_buffer), "%s°C, %s", temperature_buffer, conditions_buffer); + text_layer_set_text(s_weather_layer, weather_layer_buffer); + } + + if (got_room) { + text_layer_set_text(s_room_layer, room_buffer); + } + +} + + +static void inbox_dropped_callback(AppMessageResult reason, void *context) { + APP_LOG(APP_LOG_LEVEL_ERROR, "Message dropped!"); +} + + +static void outbox_failed_callback(DictionaryIterator *iterator, AppMessageResult reason, void *context) { + APP_LOG(APP_LOG_LEVEL_ERROR, "Outbox send failed!"); +} + + +static void outbox_sent_callback(DictionaryIterator *iterator, void *context) { + APP_LOG(APP_LOG_LEVEL_INFO, "Outbox send success!"); +} + + +static void main_window_load(Window *window) { + + // Create GBitmap, then set to created BitmapLayer + s_background_bitmap = gbitmap_create_with_resource(RESOURCE_ID_TUTORIAL_BACKGROUND); + s_background_layer = bitmap_layer_create(GRect(0, 0, 144, 168)); + bitmap_layer_set_bitmap(s_background_layer, s_background_bitmap); + + // Create time TextLayer + s_time_layer = text_layer_create(GRect(5, 52, 139, 50)); + text_layer_set_background_color(s_time_layer, GColorClear); + text_layer_set_text_color(s_time_layer, GColorBlack); + + // Create temperature Layer + s_weather_layer = text_layer_create(GRect(0, 130, 144, 20)); + text_layer_set_background_color(s_weather_layer, GColorClear); + text_layer_set_text_color(s_weather_layer, GColorWhite); + text_layer_set_text(s_weather_layer, "Loading..."); + + // Create room temperature Layer + s_room_layer = text_layer_create(GRect(0, 150, 144, 20)); + text_layer_set_background_color(s_room_layer, GColorClear); + text_layer_set_text_color(s_room_layer, GColorWhite); + text_layer_set_text(s_room_layer, "Loading..."); + + // Create day-month TextLayer + s_daymonth_layer = text_layer_create(GRect(5, 12, 139, 30)); + text_layer_set_background_color(s_daymonth_layer, GColorBlack); + text_layer_set_text_color(s_daymonth_layer, GColorWhite); + + // Create GFont + s_time_font = fonts_load_custom_font(resource_get_handle(RESOURCE_ID_FONT_PERFECT_DOS_48)); + s_weather_font = fonts_load_custom_font(resource_get_handle(RESOURCE_ID_FONT_PERFECT_DOS_16)); + s_date_font = fonts_load_custom_font(resource_get_handle(RESOURCE_ID_FONT_PERFECT_DOS_24)); + + // Apply to TextLayer + text_layer_set_font(s_time_layer, s_time_font); + text_layer_set_font(s_weather_layer, s_weather_font); + text_layer_set_font(s_room_layer, s_weather_font); + text_layer_set_font(s_daymonth_layer, s_date_font); + + // Improve the layout to be more like a watchface + text_layer_set_text_alignment(s_time_layer, GTextAlignmentCenter); + text_layer_set_text_alignment(s_weather_layer, GTextAlignmentCenter); + text_layer_set_text_alignment(s_room_layer, GTextAlignmentCenter); + text_layer_set_text_alignment(s_daymonth_layer, GTextAlignmentCenter); + + // Add it as a child layer to the Window's root layer + layer_add_child(window_get_root_layer(window), bitmap_layer_get_layer(s_background_layer)); + layer_add_child(window_get_root_layer(window), text_layer_get_layer(s_time_layer)); + layer_add_child(window_get_root_layer(window), text_layer_get_layer(s_weather_layer)); + layer_add_child(window_get_root_layer(window), text_layer_get_layer(s_room_layer)); + layer_add_child(window_get_root_layer(window), text_layer_get_layer(s_daymonth_layer)); + +} + + +static void main_window_unload(Window *window) { + + // Destroy TextLayer + text_layer_destroy(s_time_layer); + text_layer_destroy(s_weather_layer); + text_layer_destroy(s_room_layer); + text_layer_destroy(s_daymonth_layer); + + // Unload GFont + fonts_unload_custom_font(s_time_font); + fonts_unload_custom_font(s_weather_font); + fonts_unload_custom_font(s_date_font); + + // Destroy GBitmap + gbitmap_destroy(s_background_bitmap); + + // Destroy BitmapLayer + bitmap_layer_destroy(s_background_layer); + +} + + +static void init() { + + // Create main Window element and assign to pointer + s_main_window = window_create(); + + // Set handlers to manage the elements inside the Window + window_set_window_handlers(s_main_window, (WindowHandlers){ + .load = main_window_load, + .unload = main_window_unload + }); + + // Show the Window on the watch, with animated=true + window_stack_push(s_main_window, true); + + // Register with TickTimerService + tick_timer_service_subscribe((MINUTE_UNIT | DAY_UNIT), tick_handler); + + // Register AppMessage callbacks + app_message_register_inbox_received(inbox_received_callback); + app_message_register_inbox_dropped(inbox_dropped_callback); + app_message_register_outbox_failed(outbox_failed_callback); + app_message_register_outbox_sent(outbox_sent_callback); + + // Open AppMessage + app_message_open(app_message_inbox_size_maximum(), app_message_outbox_size_maximum()); + + update_time(); + update_date(); + +} + + +static void deinit() { + + // Destroy Window + window_destroy(s_main_window); + +} diff --git a/src/pebble_utils.h b/src/pebble_utils.h new file mode 100644 index 0000000..f77e803 --- /dev/null +++ b/src/pebble_utils.h @@ -0,0 +1,26 @@ +#define GCC_VERSION (__GNUC__ * 10000 + \ + __GNUC_MINOR__ * 100 + \ + __GNUC_PATCHLEVEL__) + + +#if GCC_VERSION < 40900 + + #error Your GCC version is too old, please switch to >= 4.9 + +#endif + + +// Generic wrapper macro for the various dict_write_* functions +// Can be used with gcc >= 4.9 and --std=c11 flag +#define dict_write_value(iter, key, value, ...) _Generic((value), \ + uint8_t: dict_write_uint8, \ + uint16_t: dict_write_uint16, \ + uint32_t: dict_write_uint32, \ + unsigned: dict_write_uint32, \ + int8_t: dict_write_int8, \ + int16_t: dict_write_int16, \ + int32_t: dict_write_int32, \ + int: dict_write_int32, \ + char*: dict_write_cstring, \ + uint8_t*: dict_write_data \ + )(iter, key, value, ## __VA_ARGS__) diff --git a/wscript b/wscript new file mode 100644 index 0000000..040ab7c --- /dev/null +++ b/wscript @@ -0,0 +1,41 @@ + +# +# This file is the default set of rules to compile a Pebble project. +# +# Feel free to customize this to your needs. +# + +import os.path + +top = '.' +out = 'build' + +def options(ctx): + ctx.load('pebble_sdk') + +def configure(ctx): + ctx.load('pebble_sdk') + +def build(ctx): + 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'), + 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'), + 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/js/**/*.js'))