diff --git a/source/_includes/guides/owm-api-key-notice.html b/source/_includes/guides/owm-api-key-notice.html index be6b517..15cf4d0 100644 --- a/source/_includes/guides/owm-api-key-notice.html +++ b/source/_includes/guides/owm-api-key-notice.html @@ -17,5 +17,5 @@
API KEY REQUIRED

As of October 2015, an API key is required to fetch OpenWeatherMap data. - These can be freely obtained from OpenWeatherMap.org.

+ These can be freely obtained from OpenWeatherMap.org.

\ No newline at end of file diff --git a/source/mobilenav.html b/source/mobilenav.html index 10e907e..85d6b53 100644 --- a/source/mobilenav.html +++ b/source/mobilenav.html @@ -26,45 +26,26 @@
  • More C Concepts
  • -
  • Create a C Watchface +
  • Build a Watchface in C
  • -
  • Create a JS Watchapp +
  • Build a Watchface in JS
  • -
  • Add More Features - -
  • -
  • Go Beyond +
  • Advanced Tutorials
  • -
  • PebbleKit Android Tutorial - -
  • PebbleKit iOS Tutorial - -
  • Get the SDK
  • diff --git a/source/tutorials/index.html b/source/tutorials/index.html index 2117619..c9eafd3 100644 --- a/source/tutorials/index.html +++ b/source/tutorials/index.html @@ -31,8 +31,8 @@

    Build a Watchface

    Learn how to create your first watchface. This tutorial will cover basic Pebble concepts, and is the recommended starting point for new developers.

    - Build with JS - Build with C + Build with C + Build with JS
    @@ -48,7 +48,7 @@

    Build a Watchface

    Build a One Click Action

    Learn how to create your first one click action watchapp. This guide explains how to create a watchapp that will makes a web request upon launch and display the result.

    - Build with C + Build with C
    @@ -76,7 +76,7 @@

    Learn C with Pebble

    Publish Your App

    Learn how to publish your watchface or watchapp on Pebble's appstore.

    - Publish an App + Publish an App
    diff --git a/source/tutorials/js-watchface-tutorial/part1.md b/source/tutorials/js-watchface-tutorial/part1.md index 992031e..1636b19 100644 --- a/source/tutorials/js-watchface-tutorial/part1.md +++ b/source/tutorials/js-watchface-tutorial/part1.md @@ -58,13 +58,14 @@ structure required for a basic Rocky.js application. ## Watchface Basics -Watchface are essentially long running applications that update the display at +Watchfaces are essentially long running applications that update the display at a regular interval (typically once a minute, or when specific events occur). By minimizing the frequency that the screen is updated, we help to conserve battery life on the watch. The main entry point for the watchface is `/src/rocky/index.js`, so we'll -start by editing this file. +start by editing this file. You can remove the sample content provided for the +code given below. The very first thing we must do is include the Rocky.js library, which gives us access to the APIs we need to create a Pebble watchface. @@ -198,7 +199,9 @@ Another minute with your Pebble! ``` > Note: You should prevent execution of the log statements by commenting the -code, if you aren't using them. e.g. `//console.log();` +> code, if you aren't using them. e.g. `// console.log();` before publishing the +> watchface. + ## Creating an Analog Watchface @@ -221,7 +224,8 @@ representing the minute hand. We need to implement a function to draw the hands, to prevent duplicating the same drawing code for hours and minutes. We're going to use a series of -``CanvasRenderingContext2D`` methods to accomplish the desired effect. +``CanvasRenderingContext2D`` methods to accomplish the desired effect in the +`draw` callback we added earlier. First we need to find the center point in our display: @@ -269,6 +273,9 @@ ctx.stroke(); ### Putting It All Together +This is the complete code assembled so far, including a helpful function for +drawing the hands and converting a time fraction to a Radian angle. + ```js var rocky = require('rocky'); @@ -421,8 +428,7 @@ output, it probably means there is an issue in the preceding code. ### I'm still having problems! If you've tried the steps above and you're still having problems, there are -plenty of places to get help. You can post your question and code on the -[Pebble Forums](https://forums.pebble.com/c/development) or join our +plenty of places to get help. You can post your question and code on our [Discord Server]({{ site.links.discord_invite }}) and ask for assistance. @@ -438,9 +444,7 @@ watchface using JavaScript! To do this we: 5. Used drawing commands to draw text and lines on the display. If you have problems with your code, check it against the sample source code -provided using the button below. - -[View Source Code >{center,bg-lightblue,fg-white}](https://github.com/pebble-examples/rocky-watchface-tutorial-part1) +provided above. ## What's Next diff --git a/source/tutorials/js-watchface-tutorial/part2.md b/source/tutorials/js-watchface-tutorial/part2.md index 36074ca..dd811c6 100644 --- a/source/tutorials/js-watchface-tutorial/part2.md +++ b/source/tutorials/js-watchface-tutorial/part2.md @@ -69,7 +69,8 @@ To send a message from the smartwatch to the mobile device, use the object: ```js -// rocky index.js +// File: rocky/index.js + var rocky = require('rocky'); // Send a message from the smartwatch @@ -80,7 +81,7 @@ To send a message from the mobile device to the smartwatch, use the ``Pebble.postMessage`` method: ```js -// pkjs index.js +// File: pkjs/index.js // Send a message from the mobile device Pebble.postMessage({'test': 'hello from mobile device'}); @@ -92,7 +93,7 @@ We can create a message listener in our smartwatch code using the ``rocky.on`` method: ```js -// rocky index.js +// File: rocky/index.js // On the smartwatch, begin listening for a message from the mobile device rocky.on('message', function(event) { @@ -105,7 +106,7 @@ We can also create a message listener in our `pkjs` code using the ``Pebble.on`` method: ```js -// pkjs index.js +// File: pkjs/index.js // On the phone, begin listening for a message from the smartwatch Pebble.on('message', function(event) { @@ -126,12 +127,12 @@ In order to use this functionality, your application must include the array of your `package.json` file. ```js -// file: package.json -// ... - "pebble": { - "capabilities": ["location"] - } -// ... +// File: package.json + +"pebble": { + // ... + "capabilities": ["location"] +} ``` Once we've added the `location` flag, we can access GPS coordinates using the @@ -140,7 +141,7 @@ In this example, we're going to request the user's location when we receive the "fetch" message from the smartwatch. ```js -// pkjs index.js +// File: pkjs/index.js Pebble.on('message', function(event) { // Get the message that was passed @@ -166,14 +167,14 @@ services. In this tutorial, we will interface with [Open Weather Map](http://openweathermap.org/) – a common weather API used by -the [Pebble Developer Community](https://forums.pebble.com/c/development). +many Pebble watchfaces. The `XMLHttpRequest` object is quite powerful, but can be intimidating to get started with. To make things a bit simpler, we'll wrap the object with a helper function which makes the request, then raises a callback: ```js -// pkjs index.js +// File: pkjs/index.js function request(url, type, callback) { var xhr = new XMLHttpRequest(); @@ -199,7 +200,8 @@ followed by the API key: {% include guides/owm-api-key-notice.html %} ```js -var myAPIKey = '1234567'; +var myAPIKey = 'your own key here!'; + var url = 'http://api.openweathermap.org/data/2.5/weather' + '?lat=' + pos.coords.latitude + '&lon=' + pos.coords.longitude + @@ -209,9 +211,9 @@ var url = 'http://api.openweathermap.org/data/2.5/weather' + All together, our message handler should now look like the following: ```js -// pkjs index.js +// File: pkjs/index.js -var myAPIKey = '1234567'; +var myAPIKey = 'your own key here!'; Pebble.on('message', function(event) { // Get the message that was passed @@ -242,7 +244,7 @@ Once we receive the weather data from OpenWeatherMap, we need to send it to the smartwatch using ``Pebble.postMessage``: ```js -// pkjs index.js +// File: pkjs/index.js // ... request(url, 'GET', function(respText) { @@ -263,9 +265,12 @@ On the smartwatch, we'll need to create a message handler to listen for a `weather` message, and store the information so it can be drawn on screen. ```js -// rocky index.js +// File: rocky/index.js + var rocky = require('rocky'); +// ... + // Global object to store weather data var weather; @@ -287,13 +292,13 @@ We also need to send the 'fetch' command from the smartwatch to ask for weather data when the application starts, then every hour: ```js -// rocky index.js +// File: rocky/index.js // ... rocky.on('hourchange', function(event) { // Send a message to fetch the weather information (on startup and every hour) - rocky.postMessage({'fetch': true}); + rocky.postMessage({ fetch: true }); }); ``` @@ -301,7 +306,7 @@ Finally, we'll need some new code in our Rocky `draw` handler to display the temperature and conditions: ```js -// rocky index.js +// File: rocky/index.js var rocky = require('rocky'); // ... @@ -334,6 +339,171 @@ rocky.on('draw', function(event) { }); ``` +Once it is compiled and run, it should look something like the preview at the +start of the tutorial section: + +![rocky >{pebble-screenshot,pebble-screenshot--time-red}](/images/tutorials/js-watchface-tutorial/tictoc-weather.png) + + +### Putting It All Together + +This is the complete code for this tutorial, separated by JS file. Compare it +to yours if you have problems with any part of it or it is not working as +expected. + +```js +// File: rocky/index.js + +var rocky = require('rocky'); + +// Global object to store weather data +var weather; + +function fractionToRadian(fraction) { + return fraction * 2 * Math.PI; +} + +function drawHand(ctx, cx, cy, angle, length, color) { + // Find the end points + var x2 = cx + Math.sin(angle) * length; + var y2 = cy - Math.cos(angle) * length; + + // Configure how we want to draw the hand + ctx.lineWidth = 8; + ctx.strokeStyle = color; + + // Begin drawing + ctx.beginPath(); + + // Move to the center point, then draw the line + ctx.moveTo(cx, cy); + ctx.lineTo(x2, y2); + + // Stroke the line (output to display) + ctx.stroke(); +} + +function drawWeather(ctx, weather) { + // Create a string describing the weather + //var weatherString = weather.celcius + 'ºC, ' + weather.desc; + var weatherString = weather.fahrenheit + 'ºF, ' + weather.desc; + + // Draw the text, top center + ctx.fillStyle = 'lightgray'; + ctx.textAlign = 'center'; + ctx.font = '14px Gothic'; + ctx.fillText(weatherString, ctx.canvas.unobstructedWidth / 2, 2); +} + +rocky.on('draw', function(event) { + var ctx = event.context; + var d = new Date(); + + // Clear the screen + ctx.clearRect(0, 0, ctx.canvas.clientWidth, ctx.canvas.clientHeight); + + // Draw the conditions (before clock hands, so it's drawn underneath them) + if (weather) { + drawWeather(ctx, weather); + } + + // Determine the width and height of the display + var w = ctx.canvas.unobstructedWidth; + var h = ctx.canvas.unobstructedHeight; + + // Determine the center point of the display + // and the max size of watch hands + var cx = w / 2; + var cy = h / 2; + + // -20 so we're inset 10px on each side + var maxLength = (Math.min(w, h) - 20) / 2; + + // Calculate the minute hand angle + var minuteFraction = (d.getMinutes()) / 60; + var minuteAngle = fractionToRadian(minuteFraction); + + // Draw the minute hand + drawHand(ctx, cx, cy, minuteAngle, maxLength, "white"); + + // Calculate the hour hand angle + var hourFraction = (d.getHours() % 12 + minuteFraction) / 12; + var hourAngle = fractionToRadian(hourFraction); + + // Draw the hour hand + drawHand(ctx, cx, cy, hourAngle, maxLength * 0.6, "lightblue"); +}); + +rocky.on('minutechange', function(event) { + // Request the screen to be redrawn on next pass + rocky.requestDraw(); +}); + +rocky.on('hourchange', function(event) { + // Send a message to fetch the weather information (on startup and every hour) + rocky.postMessage({ fetch: true }); +}); + +rocky.on('message', function(event) { + // Receive a message from the mobile device (pkjs) + var message = event.data; + + if (message.weather) { + // Save the weather data + weather = message.weather; + + // Request a redraw so we see the information + rocky.requestDraw(); + } +}); +``` + +```js +// File: pkjs/index.js + +var myAPIKey = 'your own key here!'; + +function request(url, type, callback) { + var xhr = new XMLHttpRequest(); + xhr.onload = function () { + callback(this.responseText); + }; + xhr.open(type, url); + xhr.send(); +} + +Pebble.on('message', function(event) { + // Get the message that was passed + var message = event.data; + + if (message.fetch) { + navigator.geolocation.getCurrentPosition(function(pos) { + var url = 'http://api.openweathermap.org/data/2.5/weather' + + '?lat=' + pos.coords.latitude + + '&lon=' + pos.coords.longitude + + '&appid=' + myAPIKey; + + request(url, 'GET', function(respText) { + var weatherData = JSON.parse(respText); + + Pebble.postMessage({ + 'weather': { + // Convert from Kelvin + 'celcius': Math.round(weatherData.main.temp - 273.15), + 'fahrenheit': Math.round((weatherData.main.temp - 273.15) * 9 / 5 + 32), + 'desc': weatherData.weather[0].main + } + }); + }); + }, function(err) { + console.error('Error getting location'); + }, + { timeout: 15000, maximumAge: 60000 }); + } +}); +``` + + ## Conclusion So there we have it, we successfully added web content to our JavaScript @@ -352,15 +522,9 @@ request the weather data when the application starts and every hour. 8. Then finally we drew the weather conditions on the screen as text. If you have problems with your code, check it against the sample source code -provided using the button below. - -[View Source Code >{center,bg-lightblue,fg-white}](https://github.com/pebble-examples/rocky-watchface-tutorial-part2) +provided above. ## What's Next We hope you enjoyed this tutorial and that it inspires you to make something awesome! - -Why not let us know what you've created by tweeting -[@pebbledev](https://twitter.com/pebbledev), or join our epic developer -community on [Discord]({{ site.links.discord_invite }}). diff --git a/source/tutorials/watchface-tutorial/part1.md b/source/tutorials/watchface-tutorial/part1.md index a5f21f0..733f654 100644 --- a/source/tutorials/watchface-tutorial/part1.md +++ b/source/tutorials/watchface-tutorial/part1.md @@ -70,21 +70,22 @@ this: The main difference between the two kinds are that watchfaces serve as the default display on the watch, with the Up and Down buttons allowing use of the -Pebble timeline. This means that these buttons are not available for custom -behavior (Back and Select are also not available to watchfaces). In contrast, -watchapps are launched from the Pebble system menu. These have more capabilities -such as button clicks and menu elements, but we will come to those later. +Pebble timeline and Pebble Health (though these are customisable). This means +that these buttons are not available for custom behavior (Back and Select are +also not available to watchfaces). In contrast, watchapps are launched from the +Pebble system menu. These can have more capabilities such as button clicks and +menu elements, but we will come to those later. -Finally, set a value for `companyName` and we can start to write some code! +Now we can start to write some code! ## Watchface Basics Our first source file is already created for you by the `pebble` command line tool and lives in the project's `src` directory. By default, this file -contains sample code which you can safely remove, since we will be starting from -scratch. Alternatively, you can avoid this by using the `--simple` flag when -creating the project. +contains sample code which you can safely remove completely, since we will be +starting from scratch. Alternatively, you can avoid this by using the `--simple` +flag when creating the project. Let's add the basic code segments which are required by every watchapp. The first of these is the main directive to use the Pebble SDK at the top of the @@ -94,7 +95,7 @@ file like so: #include ``` -After this first line, we must begin with the recommended app structure, +After this first line, we should begin with the recommended app structure, specifically a standard C `main()` function and two other functions to help us organize the creation and destruction of all the Pebble SDK elements. This helps make the task of managing memory allocation and deallocation as simple as @@ -191,8 +192,7 @@ valid after each iterative change, so let's do this now. ## First Compilation and Installation To compile the watchface, make sure you have saved your project files and -then run `pebble build` from the project's root directory. The installable -`.pbw` file will be deposited in the `build` directory. After a successful +then run `pebble build` from the project's root directory. After a successful compile you will see a message reading `'build' finished successfully`. If there are any problems with your code, the compiler will tell you which lines are in error so you can fix them. @@ -202,12 +202,25 @@ In order to install your watchface on your Pebble, first Make sure you are using the latest version of the Pebble app. Install the watchapp by running `pebble install`, supplying your phone's IP -address with the `--phone` flag. For example: `pebble install ---phone 192.168.1.78`. +address with the `--phone` flag. For example: + +``` +pebble install --phone 192.168.1.78 +``` > Instead of using the --phone flag every time you install, set the PEBBLE_PHONE environment variable: > `export PEBBLE_PHONE=192.168.1.78` and simply use `pebble install`. +You can also use the emulator by specifying the platform you want to use, for +example the Pebble Time platform `basalt`: + +``` +pebble install --emulator basalt +``` + +> Use of the emulator may require more dependencies to be installed depending +> on your system and configuration. + Congratulations! You should see that you have a new item in the watchface menu, but it is entirely blank! @@ -292,8 +305,7 @@ static void main_window_unload(Window *window) { ``` This completes the setup of the basic watchface layout. If you run `pebble -build && pebble install` (with your phone's IP address) for the new build, you -should now see the following: +build && pebble install` for the new build, you should now see the following: {% screenshot_viewer %} { @@ -339,8 +351,9 @@ tick_timer_service_subscribe(MINUTE_UNIT, tick_handler); ``` The logic to update the time ``TextLayer`` will be created in a function called -`update_time()`, enabling us to call it both from the ``TickHandler`` as well as -`main_window_load()` to ensure it is showing a time from the very beginning. +`update_time()`, using a newly created time object, enabling us to call it both +from the ``TickHandler`` as well as `main_window_load()` to ensure it is showing +a time from the very beginning. This function will use `strftime()` ([See here for formatting](http://www.cplusplus.com/reference/ctime/strftime/)) @@ -417,9 +430,93 @@ watchface! To do this we: these to a buffer for display in the ``TextLayer``. If you have problems with your code, check it against the sample source code -provided using the button below. +provided below. + +
    +View source code +{% markdown %} +```c +#include -[View Source Code >{center,bg-lightblue,fg-white}](https://gist.github.com/9b9d50b990d742a3ae34) +static Window *s_main_window; +static TextLayer *s_time_layer; + +static void update_time() { + // Get a tm structure + time_t temp = time(NULL); + struct tm *tick_time = localtime(&temp); + + // Write the current hours and minutes into a buffer + static char s_buffer[8]; + strftime(s_buffer, sizeof(s_buffer), clock_is_24h_style() ? + "%H:%M" : "%I:%M", tick_time); + + // Display this time on the TextLayer + text_layer_set_text(s_time_layer, s_buffer); +} + +static void main_window_load(Window *window) { + // Get information about the Window + Layer *window_layer = window_get_root_layer(window); + GRect bounds = layer_get_bounds(window_layer); + + // Create the TextLayer with specific bounds + s_time_layer = text_layer_create( + GRect(0, PBL_IF_ROUND_ELSE(58, 52), bounds.size.w, 50)); + + // Improve the layout to be more like a watchface + text_layer_set_background_color(s_time_layer, GColorClear); + text_layer_set_text_color(s_time_layer, GColorBlack); + text_layer_set_text(s_time_layer, "00:00"); + text_layer_set_font(s_time_layer, fonts_get_system_font(FONT_KEY_BITHAM_42_BOLD)); + text_layer_set_text_alignment(s_time_layer, GTextAlignmentCenter); + + // Add it as a child layer to the Window's root layer + layer_add_child(window_layer, text_layer_get_layer(s_time_layer)); +} + +static void main_window_unload(Window *window) { + // Destroy TextLayer + text_layer_destroy(s_time_layer); +} + +static void tick_handler(struct tm *tick_time, TimeUnits units_changed) { + update_time(); +} + +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, tick_handler); + + // Make sure the time is displayed from the start + update_time(); +} + +static void deinit() { + // Destroy Window + window_destroy(s_main_window); +} + +int main(void) { + init(); + app_event_loop(); + deinit(); +} +``` +{% endmarkdown %} +
    ## What's Next? diff --git a/source/tutorials/watchface-tutorial/part2.md b/source/tutorials/watchface-tutorial/part2.md index 2611e99..aa8cd38 100644 --- a/source/tutorials/watchface-tutorial/part2.md +++ b/source/tutorials/watchface-tutorial/part2.md @@ -53,9 +53,8 @@ done, you should end up with a watchface looking like this: ## First Steps To continue from the last part, you can either modify your existing Pebble -project or create a new one, using the code from that project's main `.c` file -as a starting template. For reference, that should look -[something like this](https://gist.github.com/pebble-gists/9b9d50b990d742a3ae34). +project or create a new one, using the code from the end of the last tutorial +as a starting point. Don't forget also to include changes to `package.json`. The result of the first part should look something like this - a basic time display: @@ -77,23 +76,26 @@ Let's improve it! App resources (fonts and images etc.) are managed in the `package.json` file in the project's root directory, as detailed in -[*App Resources*](/guides/app-resources/). All image files and fonts must -reside in subfolders of the `/resources` folder of your project. Below is an -example entry in the `media` array: +[*App Resources*](/guides/app-resources/). Below is an example entry in the +`resources` section: ```json -"media": [ - { - "type": "font", - "name": "FONT_PERFECT_DOS_48", - "file": "fonts/perfect-dos-vga.ttf", - "compatibility":"2.7" - } -] +"resources": { + "media": [ + { + "type": "font", + "name": "FONT_PERFECT_DOS_48", + "file": "fonts/perfect-dos-vga.ttf", + "compatibility":"2.7" + } + ] +} ``` -In the example above, we would place our `perfect-dos-vga.ttf` file in the -`/resources/fonts/` folder of our project. +All image files and fonts must reside in subfolders of the `/resources` folder +of your project. In the example above, we would place our `perfect-dos-vga.ttf` +file in the `/resources/fonts/` folder of our project. So, create this if it +doesn't already exist. A custom font file must be a [TrueType](http://en.wikipedia.org/wiki/TrueType) font in the `.ttf` file format. @@ -103,7 +105,8 @@ A custom font file must be a Now we will substitute the system font used before (`FONT_KEY_BITHAM_42_BOLD`) for our newly imported one. -To do this, we will declare a ``GFont`` globally. +To do this, we will declare a ``GFont`` globally near the top of the source +file. ```c // Declare globally @@ -121,7 +124,7 @@ void main_window_load() { // Create GFont s_time_font = fonts_load_custom_font(resource_get_handle(RESOURCE_ID_FONT_PERFECT_DOS_48)); - // Apply to TextLayer + // Apply custom font to TextLayer text_layer_set_font(s_time_layer, s_time_font); // ... } @@ -157,11 +160,12 @@ An example screenshot is shown below: ## Adding a Bitmap -The Pebble SDK also allows you to use a 2-color (black and white) bitmap image -in your watchface project. You can ensure that you meet this requirement by -checking the export settings in your graphics package, or by purely using only -white (`#FFFFFF`) and black (`#000000`) in the image's creation. Another -alternative is to use a dithering tool such as +The Pebble SDK also allows you to use images in your project and handles the +process of preparing them for the watch. In this tutorial we will use a 2-color +(black and white) bitmap image as an example. You can ensure that you meet this +requirement by checking the export settings in your graphics package, or by +purely using only white (`#FFFFFF`) and black (`#000000`) in the image's +creation. Another alternative is to use a dithering tool such as [HyperDither](http://2002-2010.tinrocket.com/software/hyperdither/index.html). This will be loaded from the watchface's resources into a ``GBitmap`` data structure before being displayed using a ``BitmapLayer`` element. These two @@ -181,7 +185,7 @@ object will have a `type` of `bitmap`. Below is an example: ``` As before, here is an example bitmap we have created for you to use, which looks -like this: +as shown below. Be sure to save it to a `resources/images` directory. [![background](/images/getting-started/watchface-tutorial/background.png "background")]({{ site.asset_path }}/images/getting-started/watchface-tutorial/background.png) @@ -219,17 +223,18 @@ As always, the final step should be to ensure we free up the memory consumed by these new elements in `main_window_unload()`: ```c -// Destroy GBitmap -gbitmap_destroy(s_background_bitmap); - // Destroy BitmapLayer bitmap_layer_destroy(s_background_layer); + +// Destroy GBitmap +gbitmap_destroy(s_background_bitmap); ``` The final step is to set the background color of the main ``Window`` to match -the background image. Do this in `init()`: +the background image. Do this in `init()` after `window_create()`: ```c +// Change the background color window_set_background_color(s_main_window, GColorBlack); ``` @@ -259,9 +264,125 @@ the same way as the time display one to show the current date (hint: look at the available for `strftime()`!) As with last time, you can compare your own code to the example source code -using the button below. +provided below. + +
    +View source code +{% markdown %} +```c +#include + +static Window *s_main_window; +static TextLayer *s_time_layer; +static BitmapLayer *s_background_layer; + +static GFont s_time_font; +static GBitmap *s_background_bitmap; + +static void update_time() { + // Get a tm structure + time_t temp = time(NULL); + struct tm *tick_time = localtime(&temp); + + // Write the current hours and minutes into a buffer + static char s_buffer[8]; + strftime(s_buffer, sizeof(s_buffer), clock_is_24h_style() ? + "%H:%M" : "%I:%M", tick_time); + + // Display this time on the TextLayer + text_layer_set_text(s_time_layer, s_buffer); +} + +static void main_window_load(Window *window) { + // Get information about the Window + Layer *window_layer = window_get_root_layer(window); + GRect bounds = layer_get_bounds(window_layer); + + // Create GBitmap + s_background_bitmap = gbitmap_create_with_resource(RESOURCE_ID_IMAGE_BACKGROUND); + + // Create BitmapLayer to display the GBitmap + s_background_layer = bitmap_layer_create(bounds); + + // Set the bitmap onto the layer and add to the window + bitmap_layer_set_bitmap(s_background_layer, s_background_bitmap); + layer_add_child(window_layer, bitmap_layer_get_layer(s_background_layer)); + + // Create GFont + s_time_font = fonts_load_custom_font(resource_get_handle(RESOURCE_ID_FONT_PERFECT_DOS_48)); -[View Source Code >{center,bg-lightblue,fg-white}](https://gist.github.com/d216d9e0b840ed296539) + // Create the TextLayer with specific bounds + s_time_layer = text_layer_create( + GRect(0, PBL_IF_ROUND_ELSE(58, 52), bounds.size.w, 50)); + + // Improve the layout to be more like a watchface + text_layer_set_background_color(s_time_layer, GColorClear); + text_layer_set_text_color(s_time_layer, GColorBlack); + text_layer_set_text(s_time_layer, "00:00"); + text_layer_set_font(s_time_layer, fonts_get_system_font(FONT_KEY_BITHAM_42_BOLD)); + text_layer_set_text_alignment(s_time_layer, GTextAlignmentCenter); + + // Apply custom font to TextLayer + text_layer_set_font(s_time_layer, s_time_font); + + // Add it as a child layer to the Window's root layer + layer_add_child(window_layer, text_layer_get_layer(s_time_layer)); +} + +static void main_window_unload(Window *window) { + // Destroy TextLayer + text_layer_destroy(s_time_layer); + + // Unload GFont + fonts_unload_custom_font(s_time_font); + + // Destroy BitmapLayer + bitmap_layer_destroy(s_background_layer); + + // Destroy GBitmap + gbitmap_destroy(s_background_bitmap); +} + +static void tick_handler(struct tm *tick_time, TimeUnits units_changed) { + update_time(); +} + +static void init() { + // Create main Window element and assign to pointer + s_main_window = window_create(); + + // Change the background color + window_set_background_color(s_main_window, GColorBlack); + + // 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, tick_handler); + + // Make sure the time is displayed from the start + update_time(); +} + +static void deinit() { + // Destroy Window + window_destroy(s_main_window); +} + +int main(void) { + init(); + app_event_loop(); + deinit(); +} +``` +{% endmarkdown %} +
    ## What's Next? diff --git a/source/tutorials/watchface-tutorial/part3.md b/source/tutorials/watchface-tutorial/part3.md index f46980f..724d283 100644 --- a/source/tutorials/watchface-tutorial/part3.md +++ b/source/tutorials/watchface-tutorial/part3.md @@ -52,9 +52,8 @@ all its customized glory: {% endscreenshot_viewer %} To continue from the last part, you can either modify your existing Pebble -project or create a new one, using the code from that project's main `.c` file -as a starting template. For reference, that should look -[something like this](https://gist.github.com/pebble-gists/d216d9e0b840ed296539). +project or create a new one, using the code from the end of the last tutorial +as a starting point. Don't forget also to include changes to `package.json`. ## Preparing the Watchface Layout @@ -73,11 +72,9 @@ elements. Here is the ``TextLayer`` setup; this should all be familiar to you from the previous two tutorial parts: ```c -// Create temperature Layer +// Create weather Layer s_weather_layer = text_layer_create( GRect(0, PBL_IF_ROUND_ELSE(125, 120), bounds.size.w, 25)); - -// Style the text text_layer_set_background_color(s_weather_layer, GColorClear); text_layer_set_text_color(s_weather_layer, GColorWhite); text_layer_set_text_alignment(s_weather_layer, GTextAlignmentCenter); @@ -101,9 +98,9 @@ field to `_20` or similar. Below is an example showing both fonts: { "type":"font", "name":"FONT_PERFECT_DOS_20", - "file":"perfect-dos-vga.ttf", + "file":"fonts/perfect-dos-vga.ttf", "compatibility": "2.7" - }, + } ] ``` @@ -212,7 +209,7 @@ the inbox and outbox size (in bytes): ```c // Open AppMessage -const int inbox_size = 128; +const int inbox_size = 256; const int outbox_size = 128; app_message_open(inbox_size, outbox_size); ``` @@ -239,21 +236,18 @@ This template is shown below for you to start your JS file: ```js // Listen for when the watchface is opened -Pebble.addEventListener('ready', - function(e) { - console.log('PebbleKit JS ready!'); - } -); +Pebble.addEventListener('ready', function(e) { + console.log('PebbleKit JS ready!'); +}); // Listen for when an AppMessage is received -Pebble.addEventListener('appmessage', - function(e) { - console.log('AppMessage received!'); - } -); +Pebble.addEventListener('appmessage', function(e) { + console.log('AppMessage received!'); +}); ``` -After compiling and installing the watchface, open the app logs. +After compiling and installing the watchface to the watch or an emulator, open +the app logs. You can listen for app logs by running `pebble logs`, supplying your phone's IP address with the `--phone` switch. For example: @@ -265,7 +259,7 @@ pebble logs --phone 192.168.1.78 You can also combine these two commands into one: ``` -pebble install --logs --phone 192.168.1.78 +pebble install --phone 192.168.1.78 --logs ``` You should see a message matching that set to appear using `console.log()` in @@ -302,6 +296,8 @@ the data: ```js function locationSuccess(pos) { + console.log(JSON.stringify(pos)); + // We will request the weather here } @@ -318,14 +314,12 @@ function getWeather() { } // Listen for when the watchface is opened -Pebble.addEventListener('ready', - function(e) { - console.log('PebbleKit JS ready!'); - - // Get the initial weather - getWeather(); - } -); +Pebble.addEventListener('ready', function(e) { + console.log('PebbleKit JS ready!'); + + // Get the initial weather + getWeather(); +}); ``` Notice that when the `ready` event occurs, `getWeather()` is called, which in @@ -353,40 +347,35 @@ The three arguments we have to provide when calling `xhrRequest()` are the URL, the type of request (`GET` or `POST`, for example) and a callback for when the response is received. The URL is specified on the OpenWeatherMap API page, and contains the coordinates supplied by `getCurrentPosition()`, the latitude and -longitude encoded at the end: +longitude encoded at the end. {% include guides/owm-api-key-notice.html %} -```js -var url = 'http://api.openweathermap.org/data/2.5/weather?lat=' + - pos.coords.latitude + '&lon=' + pos.coords.longitude + '&appid=' + myAPIKey; -``` - The type of the XHR will be a 'GET' request, to *get* information from the service. We will incorporate the callback into the function call for readability, and the full code snippet is shown below: ```js +var myAPIKey = 'your own key here!'; + function locationSuccess(pos) { // Construct URL var url = 'http://api.openweathermap.org/data/2.5/weather?lat=' + pos.coords.latitude + '&lon=' + pos.coords.longitude + '&appid=' + myAPIKey; - + // 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); - } - ); + 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); + }); } ``` @@ -396,18 +385,16 @@ conditions obtained. To discover the structure of the JSON object we can use `console.log(responseText)` to see its contents. To see how we arrived at some of the statements above, such as -`json.weather[0].main`, here is an -[example response](https://gist.github.com/pebble-gists/216e6d5a0f0bd2328509#file-example-response-json) -for London, UK. We can see that by following the JSON structure from our -variable called `json` (which represents the root of the structure) we can +`json.weather[0].main`, we can see that by following the JSON structure from our +variable called `json` (which represents the top level of the structure) we can access any of the data items. So to get the wind speed we would access `json.wind.speed`, and so on. ## Showing Weather on Pebble -The final JS step is to send the weather data back to the watch. To do this we must -pick some appmessage keys to send back. Since we want to display the temperature -and current conditions, we'll create one key for each of those. +The final JS step is to send the weather data back to the watch. To do this we +must pick some ``AppMessage`` keys to send back. Since we want to display the +temperature and current conditions, we'll create one key for each of those. You can add your ``AppMessage`` keys in the `messageKeys` object in `package.json` as shown below for the example keys: @@ -415,7 +402,7 @@ You can add your ``AppMessage`` keys in the `messageKeys` object in ```json "messageKeys": [ "TEMPERATURE", - "CONDITIONS", + "CONDITIONS" ] ``` @@ -431,14 +418,11 @@ var dictionary = { }; // 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!'); - } -); +Pebble.sendAppMessage(dictionary, function(e) { + console.log('Weather info sent to Pebble successfully!'); +}, function(e) { + console.log('Error sending weather info to Pebble!'); +}); ``` While we are here, let's add another call to `getWeather()` in the `appmessage` @@ -447,12 +431,12 @@ from the watch to achieve this: ```js // Listen for when an AppMessage is received -Pebble.addEventListener('appmessage', - function(e) { - console.log('AppMessage received!'); - getWeather(); - } -); +Pebble.addEventListener('appmessage', function(e) { + console.log('AppMessage received!'); + + // Get updated weather now + getWeather(); +}); ``` The final step on the Pebble side is to act on the information received from @@ -460,16 +444,17 @@ PebbleKit JS and show the weather data in the ``TextLayer`` we created for this very purpose. To do this, go back to your C code file and find your ``AppMessageInboxReceived`` implementation (such as our `inbox_received_callback()` earlier). This will now be modified to process the -received data. When the watch receives an ``AppMessage`` message from the JS +received data. + +When the watch receives an ``AppMessage`` message from the JS part of the watchface, this callback will be called and we will be provided a dictionary of data in the form of a `DictionaryIterator` object, as seen in the callback signature. `MESSAGE_KEY_TEMPERATURE` and `MESSAGE_KEY_CONDITIONS` -will be automatically provided as we specified them in `package.json`. +will be automatically defined as we specified them in `package.json`. -Before examining the dictionary we add three character -buffers; one each for the temperature and conditions and the other for us to -assemble the entire string. Remember to be generous with the buffer sizes to -prevent overruns: +Before examining the dictionary we add three character buffers; one each for the +temperature and conditions and the other for us to assemble the entire string. +Remember to be generous with the buffer sizes to prevent overruns: ```c // Store incoming information @@ -487,7 +472,7 @@ Tuple *temp_tuple = dict_find(iterator, MESSAGE_KEY_TEMPERATURE); Tuple *conditions_tuple = dict_find(iterator, MESSAGE_KEY_CONDITIONS); // If all data is available, use it -if(temp_tuple && conditions_tuple) { +if (temp_tuple && conditions_tuple) { snprintf(temperature_buffer, sizeof(temperature_buffer), "%dC", (int)temp_tuple->value->int32); snprintf(conditions_buffer, sizeof(conditions_buffer), "%s", conditions_tuple->value->cstring); } @@ -530,7 +515,7 @@ weather updates every 30 minutes by adding the following code to the end of ```c // Get weather update every 30 minutes -if(tick_time->tm_min % 30 == 0) { +if (tick_time->tm_min % 30 == 0) { // Begin dictionary DictionaryIterator *iter; app_message_outbox_begin(&iter); @@ -561,10 +546,261 @@ Whew! That was quite a long tutorial, but here's all you've learned: Using all this it is possible to `GET` and `POST` to a huge number of web services to display data and control these services. -As usual, you can compare your code to the example code provided using the button -below. +As usual, you can compare your code to the example code provided below. + +
    +View C code +{% markdown %} +```c +#include + +static Window *s_main_window; +static TextLayer *s_time_layer; +static BitmapLayer *s_background_layer; +static TextLayer *s_weather_layer; + +static GFont s_time_font; +static GFont s_weather_font; +static GBitmap *s_background_bitmap; + +static void update_time() { + // Get a tm structure + time_t temp = time(NULL); + struct tm *tick_time = localtime(&temp); + + // Write the current hours and minutes into a buffer + static char s_buffer[8]; + strftime(s_buffer, sizeof(s_buffer), clock_is_24h_style() ? + "%H:%M" : "%I:%M", tick_time); + + // Display this time on the TextLayer + text_layer_set_text(s_time_layer, s_buffer); +} + +static void main_window_load(Window *window) { + // Get information about the Window + Layer *window_layer = window_get_root_layer(window); + GRect bounds = layer_get_bounds(window_layer); + + // Create GBitmap + s_background_bitmap = gbitmap_create_with_resource(RESOURCE_ID_IMAGE_BACKGROUND); + + // Create BitmapLayer to display the GBitmap + s_background_layer = bitmap_layer_create(bounds); + bitmap_layer_set_bitmap(s_background_layer, s_background_bitmap); + layer_add_child(window_layer, bitmap_layer_get_layer(s_background_layer)); + + // Create GFont + s_time_font = fonts_load_custom_font(resource_get_handle(RESOURCE_ID_FONT_PERFECT_DOS_48)); + + // Create the TextLayer with specific bounds + s_time_layer = text_layer_create( + GRect(0, PBL_IF_ROUND_ELSE(58, 52), bounds.size.w, 50)); + text_layer_set_background_color(s_time_layer, GColorClear); + text_layer_set_text_color(s_time_layer, GColorBlack); + text_layer_set_text(s_time_layer, "00:00"); + text_layer_set_font(s_time_layer, fonts_get_system_font(FONT_KEY_BITHAM_42_BOLD)); + text_layer_set_text_alignment(s_time_layer, GTextAlignmentCenter); + text_layer_set_font(s_time_layer, s_time_font); + layer_add_child(window_layer, text_layer_get_layer(s_time_layer)); + + // Create weather Layer + s_weather_layer = text_layer_create( + GRect(0, PBL_IF_ROUND_ELSE(125, 120), bounds.size.w, 25)); + text_layer_set_background_color(s_weather_layer, GColorClear); + text_layer_set_text_color(s_weather_layer, GColorWhite); + text_layer_set_text_alignment(s_weather_layer, GTextAlignmentCenter); + text_layer_set_text(s_weather_layer, "Loading..."); + + // Create second custom font, apply it and add to Window + s_weather_font = fonts_load_custom_font(resource_get_handle(RESOURCE_ID_FONT_PERFECT_DOS_20)); + text_layer_set_font(s_weather_layer, s_weather_font); + layer_add_child(window_get_root_layer(window), text_layer_get_layer(s_weather_layer)); +} + +static void main_window_unload(Window *window) { + // Destroy TextLayer + text_layer_destroy(s_time_layer); + + // Unload GFont + fonts_unload_custom_font(s_time_font); + + // Destroy BitmapLayer + bitmap_layer_destroy(s_background_layer); + + // Destroy GBitmap + gbitmap_destroy(s_background_bitmap); + + // Destroy weather elements + text_layer_destroy(s_weather_layer); + fonts_unload_custom_font(s_weather_font); +} + +static void tick_handler(struct tm *tick_time, TimeUnits units_changed) { + update_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_uint8(iter, 0, 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 weather_layer_buffer[32]; + + // Read tuples for data + Tuple *temp_tuple = dict_find(iterator, MESSAGE_KEY_TEMPERATURE); + Tuple *conditions_tuple = dict_find(iterator, MESSAGE_KEY_CONDITIONS); + + // If all data is available, use it + if (temp_tuple && conditions_tuple) { + snprintf(temperature_buffer, sizeof(temperature_buffer), "%dC", (int)temp_tuple->value->int32); + snprintf(conditions_buffer, sizeof(conditions_buffer), "%s", conditions_tuple->value->cstring); + } + + // Assemble full string and display + snprintf(weather_layer_buffer, sizeof(weather_layer_buffer), "%s, %s", temperature_buffer, conditions_buffer); + text_layer_set_text(s_weather_layer, weather_layer_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 init() { + s_main_window = window_create(); + window_set_background_color(s_main_window, GColorBlack); + window_set_window_handlers(s_main_window, (WindowHandlers) { + .load = main_window_load, + .unload = main_window_unload + }); + window_stack_push(s_main_window, true); + + tick_timer_service_subscribe(MINUTE_UNIT, tick_handler); + update_time(); + + // Register 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 + const int inbox_size = 256; + const int outbox_size = 128; + app_message_open(inbox_size, outbox_size); +} + +static void deinit() { + window_destroy(s_main_window); +} + +int main(void) { + init(); + app_event_loop(); + deinit(); +} +``` +{% endmarkdown %} +
    + +
    +View JS code +{% markdown %} +```js +var myAPIKey = 'your own key here!'; + +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) { + // Construct URL + var url = 'http://api.openweathermap.org/data/2.5/weather?lat=' + + pos.coords.latitude + '&lon=' + pos.coords.longitude + '&appid=' + myAPIKey; + + // 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 = { + 'TEMPERATURE': temperature, + '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} + ); +} -[View Source Code >{center,bg-lightblue,fg-white}](https://gist.github.com/216e6d5a0f0bd2328509) +// Listen for when the watchface is opened +Pebble.addEventListener('ready', function(e) { + console.log('PebbleKit JS ready!'); + + // Get the initial weather + getWeather(); +}); + +// Listen for when an AppMessage is received +Pebble.addEventListener('appmessage', function(e) { + console.log('AppMessage received!'); + + // Get updated weather now + getWeather(); +}); +``` +{% endmarkdown %} +
    ## What's Next? diff --git a/source/tutorials/watchface-tutorial/part4.md b/source/tutorials/watchface-tutorial/part4.md index 10fd9b1..10d4306 100644 --- a/source/tutorials/watchface-tutorial/part4.md +++ b/source/tutorials/watchface-tutorial/part4.md @@ -30,9 +30,9 @@ glance. This is typically implemented as the classic 'battery icon' that fills up according to the current charge level, but some watchfaces favor the more minimal approach, which will be implemented here. -This section continues from -[*Part 3*](/tutorials/watchface-tutorial/part3/), so be sure to re-use -your code or start with that finished project. +To continue from the last part, you can either modify your existing Pebble +project or create a new one, using the code from the end of the last tutorial +as a starting point. Don't forget also to include changes to `package.json`. The state of the battery is obtained using the ``BatteryStateService``. This service offers two modes of usage - 'peeking' at the current level, or @@ -47,7 +47,8 @@ static int s_battery_level; As with all the Event Services, to receive an event when new battery information is available, a callback must be registered. Create this callback using the signature of ``BatteryStateHandler``, and use the provided -``BatteryChargeState`` parameter to store the current charge percentage: +``BatteryChargeState`` parameter to store the current charge percentage. Place +it before `init()`, such as after `tick_handler()`: ```c static void battery_callback(BatteryChargeState state) { @@ -136,7 +137,8 @@ called manually in `init()` to display an inital value: battery_callback(battery_state_service_peek()); ``` -Don't forget to free the memory used by the new battery meter: +Don't forget to free the memory used by the new battery meter in +`main_window_unload()` as with the other ``Layer`` objects: ```c layer_destroy(s_battery_layer); @@ -149,9 +151,235 @@ existing design style. ![battery-level >{pebble-screenshot,pebble-screenshot--steel-black}](/images/tutorials/intermediate/battery-level.png) +## Conclusion + +Now our watchface shows the watch's remaining battery level! It's discreet, +but very useful. + +As usual, you can compare your code to the example code provided below. + +> The JS code file remains unchanged from the last part of the tutorial. + +
    +View C code +{% markdown %} +```c +#include + +static Window *s_main_window; +static TextLayer *s_time_layer; +static BitmapLayer *s_background_layer; +static TextLayer *s_weather_layer; +static Layer *s_battery_layer; + +static GFont s_time_font; +static GFont s_weather_font; +static GBitmap *s_background_bitmap; + +static int s_battery_level; + +static void update_time() { + // Get a tm structure + time_t temp = time(NULL); + struct tm *tick_time = localtime(&temp); + + // Write the current hours and minutes into a buffer + static char s_buffer[8]; + strftime(s_buffer, sizeof(s_buffer), clock_is_24h_style() ? + "%H:%M" : "%I:%M", tick_time); + + // Display this time on the TextLayer + text_layer_set_text(s_time_layer, s_buffer); +} + +static void battery_update_proc(Layer *layer, GContext *ctx) { + GRect bounds = layer_get_bounds(layer); + + // Find the width of the bar (total width = 114px) + int width = (s_battery_level * 114) / 100; + + // Draw the background + graphics_context_set_fill_color(ctx, GColorBlack); + graphics_fill_rect(ctx, bounds, 0, GCornerNone); + + // Draw the bar + graphics_context_set_fill_color(ctx, GColorWhite); + graphics_fill_rect(ctx, GRect(0, 0, width, bounds.size.h), 0, GCornerNone); +} + +static void main_window_load(Window *window) { + // Get information about the Window + Layer *window_layer = window_get_root_layer(window); + GRect bounds = layer_get_bounds(window_layer); + + // Create GBitmap + s_background_bitmap = gbitmap_create_with_resource(RESOURCE_ID_IMAGE_BACKGROUND); + + // Create BitmapLayer to display the GBitmap + s_background_layer = bitmap_layer_create(bounds); + bitmap_layer_set_bitmap(s_background_layer, s_background_bitmap); + layer_add_child(window_layer, bitmap_layer_get_layer(s_background_layer)); + + // Create GFont + s_time_font = fonts_load_custom_font(resource_get_handle(RESOURCE_ID_FONT_PERFECT_DOS_48)); + + // Create the TextLayer with specific bounds + s_time_layer = text_layer_create( + GRect(0, PBL_IF_ROUND_ELSE(58, 52), bounds.size.w, 50)); + text_layer_set_background_color(s_time_layer, GColorClear); + text_layer_set_text_color(s_time_layer, GColorBlack); + text_layer_set_text(s_time_layer, "00:00"); + text_layer_set_font(s_time_layer, fonts_get_system_font(FONT_KEY_BITHAM_42_BOLD)); + text_layer_set_text_alignment(s_time_layer, GTextAlignmentCenter); + text_layer_set_font(s_time_layer, s_time_font); + layer_add_child(window_layer, text_layer_get_layer(s_time_layer)); + + // Create weather Layer + s_weather_layer = text_layer_create( + GRect(0, PBL_IF_ROUND_ELSE(125, 120), bounds.size.w, 25)); + text_layer_set_background_color(s_weather_layer, GColorClear); + text_layer_set_text_color(s_weather_layer, GColorWhite); + text_layer_set_text_alignment(s_weather_layer, GTextAlignmentCenter); + text_layer_set_text(s_weather_layer, "Loading..."); + + // Create second custom font, apply it and add to Window + s_weather_font = fonts_load_custom_font(resource_get_handle(RESOURCE_ID_FONT_PERFECT_DOS_20)); + text_layer_set_font(s_weather_layer, s_weather_font); + layer_add_child(window_get_root_layer(window), text_layer_get_layer(s_weather_layer)); + + // Create battery meter Layer + s_battery_layer = layer_create(GRect(14, 53, 115, 2)); + layer_set_update_proc(s_battery_layer, battery_update_proc); + + // Add to Window + layer_add_child(window_get_root_layer(window), s_battery_layer); +} + +static void main_window_unload(Window *window) { + // Destroy TextLayer + text_layer_destroy(s_time_layer); + + // Unload GFont + fonts_unload_custom_font(s_time_font); + + // Destroy BitmapLayer + bitmap_layer_destroy(s_background_layer); + + // Destroy GBitmap + gbitmap_destroy(s_background_bitmap); + + // Destroy weather elements + text_layer_destroy(s_weather_layer); + fonts_unload_custom_font(s_weather_font); + + layer_destroy(s_battery_layer); +} + +static void tick_handler(struct tm *tick_time, TimeUnits units_changed) { + update_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_uint8(iter, 0, 0); + + // Send the message! + app_message_outbox_send(); + } +} + +static void battery_callback(BatteryChargeState state) { + // Record the new battery level + s_battery_level = state.charge_percent; + + // Update meter + layer_mark_dirty(s_battery_layer); +} + +static void inbox_received_callback(DictionaryIterator *iterator, void *context) { + // Store incoming information + static char temperature_buffer[8]; + static char conditions_buffer[32]; + static char weather_layer_buffer[32]; + + // Read tuples for data + Tuple *temp_tuple = dict_find(iterator, MESSAGE_KEY_TEMPERATURE); + Tuple *conditions_tuple = dict_find(iterator, MESSAGE_KEY_CONDITIONS); + + // If all data is available, use it + if (temp_tuple && conditions_tuple) { + snprintf(temperature_buffer, sizeof(temperature_buffer), "%dC", (int)temp_tuple->value->int32); + snprintf(conditions_buffer, sizeof(conditions_buffer), "%s", conditions_tuple->value->cstring); + } + + // Assemble full string and display + snprintf(weather_layer_buffer, sizeof(weather_layer_buffer), "%s, %s", temperature_buffer, conditions_buffer); + text_layer_set_text(s_weather_layer, weather_layer_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 init() { + s_main_window = window_create(); + window_set_background_color(s_main_window, GColorBlack); + window_set_window_handlers(s_main_window, (WindowHandlers) { + .load = main_window_load, + .unload = main_window_unload + }); + window_stack_push(s_main_window, true); + + tick_timer_service_subscribe(MINUTE_UNIT, tick_handler); + update_time(); + + // Register 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 + const int inbox_size = 256; + const int outbox_size = 128; + app_message_open(inbox_size, outbox_size); + + // Register for battery level updates + battery_state_service_subscribe(battery_callback); + + // Ensure battery level is displayed from the start + battery_callback(battery_state_service_peek()); +} + +static void deinit() { + window_destroy(s_main_window); +} + +int main(void) { + init(); + app_event_loop(); + deinit(); +} +``` +{% endmarkdown %} +
    + + ## What's Next? -In the next, and final, section of this tutorial, we'll use the Connection Service +In the next and final section of this tutorial, we'll use the Connection Service to notify the user when their Pebble smartwatch disconnects from their phone. [Go to Part 5 → >{wide,bg-dark-red,fg-white}](/tutorials/watchface-tutorial/part5/) diff --git a/source/tutorials/watchface-tutorial/part5.md b/source/tutorials/watchface-tutorial/part5.md index ce70212..1f3b09c 100644 --- a/source/tutorials/watchface-tutorial/part5.md +++ b/source/tutorials/watchface-tutorial/part5.md @@ -30,9 +30,9 @@ when their watch connects or disconnects. This can be useful to know when the watch is out of range and notifications will not be received, or to let the user know that they might have walked off somewhere without their phone. -This section continues from -[*Part 4*](/tutorials/watchface-tutorial/part4), so be sure to -re-use your code or start with that finished project. +To continue from the last part, you can either modify your existing Pebble +project or create a new one, using the code from the end of the last tutorial +as a starting point. Don't forget also to include changes to `package.json`. In a similar manner to both the ``TickTimerService`` and ``BatteryStateService``, the events associated with the Bluetooth connection are @@ -60,16 +60,16 @@ hidden when reconnected. Save the image below for use in this project: -Add this icon to your project by copying the above icon image to the `resources` -project directory, and adding a new JSON object to the `media` array in -`package.json` such as the following: +Add this icon to your project by copying the above icon image to the +`/resources/images` project directory, and adding a new JSON object to the +`media` array in `package.json` such as the following: ```js { "type": "bitmap", "name": "IMAGE_BT_ICON", - "file": "bt-icon.png" -}, + "file": "images/bt-icon.png" +} ``` This icon will be loaded into the app as a ``GBitmap`` for display in a @@ -122,7 +122,7 @@ static void bluetooth_callback(bool connected) { Upon initialization, the app will display the icon unless a re-connection event occurs, and the current state is evaluated. Manually call the handler in -`main_window_load()` to display the correct initial state: +`init()` to display the correct initial state: ```c // Show the correct state of the BT connection from the start @@ -131,15 +131,248 @@ bluetooth_callback(connection_service_peek_pebble_app_connection()); With this last feature in place, running the app and disconnecting the Bluetooth connection will cause the new indicator to appear, and the watch to vibrate -twice. +twice. It may take a few seconds for the watch to register the disconnection. ![bt >{pebble-screenshot,pebble-screenshot--steel-black}](/images/tutorials/intermediate/bt.png) -You can see the finished project source code in -[this GitHub Gist](https://gist.github.com/pebble-gists/ddd15cbe8b0986fda407). +## Conclusion + +Now our watchface shows the watch's remaining battery level! It's discreet, +but very useful. + +As usual, you can compare your code to the example code provided below. + +> The JS code file remains unchanged from the last part of the tutorial. + +
    +View C code +{% markdown %} +```c +#include + +static Window *s_main_window; +static TextLayer *s_time_layer; +static BitmapLayer *s_background_layer, *s_bt_icon_layer; +static TextLayer *s_weather_layer; +static Layer *s_battery_layer; + +static GFont s_time_font; +static GFont s_weather_font; +static GBitmap *s_background_bitmap, *s_bt_icon_bitmap; + +static int s_battery_level; + +static void update_time() { + // Get a tm structure + time_t temp = time(NULL); + struct tm *tick_time = localtime(&temp); + + // Write the current hours and minutes into a buffer + static char s_buffer[8]; + strftime(s_buffer, sizeof(s_buffer), clock_is_24h_style() ? + "%H:%M" : "%I:%M", tick_time); + + // Display this time on the TextLayer + text_layer_set_text(s_time_layer, s_buffer); +} + +static void battery_update_proc(Layer *layer, GContext *ctx) { + GRect bounds = layer_get_bounds(layer); + + // Find the width of the bar (total width = 114px) + int width = (s_battery_level * 114) / 100; + + // Draw the background + graphics_context_set_fill_color(ctx, GColorBlack); + graphics_fill_rect(ctx, bounds, 0, GCornerNone); + + // Draw the bar + graphics_context_set_fill_color(ctx, GColorWhite); + graphics_fill_rect(ctx, GRect(0, 0, width, bounds.size.h), 0, GCornerNone); +} -## What's Next? +static void main_window_load(Window *window) { + // Get information about the Window + Layer *window_layer = window_get_root_layer(window); + GRect bounds = layer_get_bounds(window_layer); -Now that you've successfully built a feature rich watchface, it's time to -[publish it](/guides/appstore-publishing/publishing-an-app/)! + // Create GBitmap + s_background_bitmap = gbitmap_create_with_resource(RESOURCE_ID_IMAGE_BACKGROUND); + + // Create BitmapLayer to display the GBitmap + s_background_layer = bitmap_layer_create(bounds); + bitmap_layer_set_bitmap(s_background_layer, s_background_bitmap); + layer_add_child(window_layer, bitmap_layer_get_layer(s_background_layer)); + + // Create GFont + s_time_font = fonts_load_custom_font(resource_get_handle(RESOURCE_ID_FONT_PERFECT_DOS_48)); + + // Create the TextLayer with specific bounds + s_time_layer = text_layer_create( + GRect(0, PBL_IF_ROUND_ELSE(58, 52), bounds.size.w, 50)); + text_layer_set_background_color(s_time_layer, GColorClear); + text_layer_set_text_color(s_time_layer, GColorBlack); + text_layer_set_text(s_time_layer, "00:00"); + text_layer_set_font(s_time_layer, fonts_get_system_font(FONT_KEY_BITHAM_42_BOLD)); + text_layer_set_text_alignment(s_time_layer, GTextAlignmentCenter); + text_layer_set_font(s_time_layer, s_time_font); + layer_add_child(window_layer, text_layer_get_layer(s_time_layer)); + + // Create weather Layer + s_weather_layer = text_layer_create( + GRect(0, PBL_IF_ROUND_ELSE(125, 120), bounds.size.w, 25)); + text_layer_set_background_color(s_weather_layer, GColorClear); + text_layer_set_text_color(s_weather_layer, GColorWhite); + text_layer_set_text_alignment(s_weather_layer, GTextAlignmentCenter); + text_layer_set_text(s_weather_layer, "Loading..."); + + // Create second custom font, apply it and add to Window + s_weather_font = fonts_load_custom_font(resource_get_handle(RESOURCE_ID_FONT_PERFECT_DOS_20)); + text_layer_set_font(s_weather_layer, s_weather_font); + layer_add_child(window_get_root_layer(window), text_layer_get_layer(s_weather_layer)); + + // Create battery meter Layer + s_battery_layer = layer_create(GRect(14, 53, 115, 2)); + layer_set_update_proc(s_battery_layer, battery_update_proc); + layer_add_child(window_get_root_layer(window), s_battery_layer); + + // Create the Bluetooth icon GBitmap + s_bt_icon_bitmap = gbitmap_create_with_resource(RESOURCE_ID_IMAGE_BT_ICON); + + // Create the BitmapLayer to display the GBitmap + s_bt_icon_layer = bitmap_layer_create(GRect(59, 12, 30, 30)); + bitmap_layer_set_bitmap(s_bt_icon_layer, s_bt_icon_bitmap); + layer_add_child(window_get_root_layer(window), bitmap_layer_get_layer(s_bt_icon_layer)); +} + +static void main_window_unload(Window *window) { + text_layer_destroy(s_time_layer); + bitmap_layer_destroy(s_background_layer); + bitmap_layer_destroy(s_bt_icon_layer); + text_layer_destroy(s_weather_layer); + layer_destroy(s_battery_layer); + + fonts_unload_custom_font(s_weather_font); + fonts_unload_custom_font(s_time_font); + + gbitmap_destroy(s_background_bitmap); + gbitmap_destroy(s_bt_icon_bitmap); +} + +static void tick_handler(struct tm *tick_time, TimeUnits units_changed) { + update_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_uint8(iter, 0, 0); + + // Send the message! + app_message_outbox_send(); + } +} + +static void battery_callback(BatteryChargeState state) { + // Record the new battery level + s_battery_level = state.charge_percent; + + // Update meter + layer_mark_dirty(s_battery_layer); +} + +static void bluetooth_callback(bool connected) { + // Show icon if disconnected + layer_set_hidden(bitmap_layer_get_layer(s_bt_icon_layer), connected); + + if(!connected) { + // Issue a vibrating alert + vibes_double_pulse(); + } +} + +static void inbox_received_callback(DictionaryIterator *iterator, void *context) { + // Store incoming information + static char temperature_buffer[8]; + static char conditions_buffer[32]; + static char weather_layer_buffer[32]; + + // Read tuples for data + Tuple *temp_tuple = dict_find(iterator, MESSAGE_KEY_TEMPERATURE); + Tuple *conditions_tuple = dict_find(iterator, MESSAGE_KEY_CONDITIONS); + + // If all data is available, use it + if (temp_tuple && conditions_tuple) { + snprintf(temperature_buffer, sizeof(temperature_buffer), "%dC", (int)temp_tuple->value->int32); + snprintf(conditions_buffer, sizeof(conditions_buffer), "%s", conditions_tuple->value->cstring); + } + + // Assemble full string and display + snprintf(weather_layer_buffer, sizeof(weather_layer_buffer), "%s, %s", temperature_buffer, conditions_buffer); + text_layer_set_text(s_weather_layer, weather_layer_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 init() { + s_main_window = window_create(); + window_set_background_color(s_main_window, GColorBlack); + window_set_window_handlers(s_main_window, (WindowHandlers) { + .load = main_window_load, + .unload = main_window_unload + }); + window_stack_push(s_main_window, true); + + tick_timer_service_subscribe(MINUTE_UNIT, tick_handler); + update_time(); + + // Register 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 + const int inbox_size = 256; + const int outbox_size = 128; + app_message_open(inbox_size, outbox_size); + + // Register for battery level updates + battery_state_service_subscribe(battery_callback); + battery_callback(battery_state_service_peek()); + + // Register for Bluetooth connection updates + connection_service_subscribe((ConnectionHandlers) { + .pebble_app_connection_handler = bluetooth_callback + }); + + // Show the correct state of the BT connection from the start + bluetooth_callback(connection_service_peek_pebble_app_connection()); +} + +static void deinit() { + window_destroy(s_main_window); +} + +int main(void) { + init(); + app_event_loop(); + deinit(); +} +``` +{% endmarkdown %} +