mirror of
https://github.com/google/pebble.git
synced 2025-06-03 16:53:11 +00:00
Import the pebble dev site into devsite/
This commit is contained in:
parent
3b92768480
commit
527858cf4c
1359 changed files with 265431 additions and 0 deletions
18
devsite/source/tutorials/advanced/index.md
Normal file
18
devsite/source/tutorials/advanced/index.md
Normal file
|
@ -0,0 +1,18 @@
|
|||
---
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
layout: utils/redirect_permanent
|
||||
redirect_to: /tutorials/advanced/vector-animations/
|
||||
---
|
466
devsite/source/tutorials/advanced/vector-animations.md
Normal file
466
devsite/source/tutorials/advanced/vector-animations.md
Normal file
|
@ -0,0 +1,466 @@
|
|||
---
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
layout: tutorials/tutorial
|
||||
tutorial: advanced
|
||||
tutorial_part: 1
|
||||
|
||||
title: Vector Animations
|
||||
description: |
|
||||
How to use vector images in icons and animations.
|
||||
permalink: /tutorials/advanced/vector-animations/
|
||||
generate_toc: true
|
||||
platform_choice: true
|
||||
platforms:
|
||||
- basalt
|
||||
- chalk
|
||||
- diorite
|
||||
- emery
|
||||
---
|
||||
|
||||
Some of the best Pebble apps make good use of the ``Animation`` and the
|
||||
[`Graphics Context`](``Graphics``) to create beautiful and eye-catching user
|
||||
interfaces that look better than those created with just the standard ``Layer``
|
||||
types.
|
||||
|
||||
Taking a good design a step further may involve using the ``Draw Commands`` API
|
||||
to load vector icons and images, and to animate them on a point-by-point basis
|
||||
at runtime. An additional capability of the ``Draw Commands`` API is the draw
|
||||
command sequence, allowing multiple frames to be incorporated into a single
|
||||
resource and played out frame by frame.
|
||||
|
||||
This tutorial will guide you through the process of using these types of image
|
||||
files in your own projects.
|
||||
|
||||
|
||||
## What Are Vector Images?
|
||||
|
||||
As opposed to bitmaps which contain data for every pixel to be drawn, a vector
|
||||
file contains only instructions about points contained in the image and how to
|
||||
draw lines connecting them up. Instructions such as fill color, stroke color,
|
||||
and stroke width are also included.
|
||||
|
||||
Vector images on Pebble are implemented using the ``Draw Commands`` APIs, which
|
||||
load and display PDC (Pebble Draw Command) images and sequences that contain
|
||||
sets of these instructions. An example is the weather icon used in weather
|
||||
timeline pins. The benefit of using vector graphics for this icon is that is
|
||||
allows the image to stretch in the familiar manner as it moves between the
|
||||
timeline view and the pin detail view:
|
||||
|
||||

|
||||
|
||||
By including two or more vector images in a single file, an animation can be
|
||||
created to enable fast and detailed animated sequences to be played. Examples
|
||||
can be seen in the Pebble system UI, such as when an action is completed:
|
||||
|
||||

|
||||
|
||||
The main benefits of vectors over bitmaps for simple images and icons are:
|
||||
|
||||
* Smaller resource size - instructions for joining points are less memory
|
||||
expensive than per-pixel bitmap data.
|
||||
|
||||
* Flexible rendering - vector images can be rendered as intended, or manipulated
|
||||
at runtime to move the individual points around. This allows icons to appear
|
||||
more organic and life-like than static PNG images. Scaling and distortion is
|
||||
also made possible.
|
||||
|
||||
* Longer animations - a side benefit of taking up less space is the ability to
|
||||
make animations longer.
|
||||
|
||||
However, there are also some drawbacks to choosing vector images in certain
|
||||
cases:
|
||||
|
||||
* Vector files require more specialized tools to create than bitmaps, and so are
|
||||
harder to produce.
|
||||
|
||||
* Complicated vector files may take more time to render than if they were simply
|
||||
drawn per-pixel as a bitmap, depending on the drawing implementation.
|
||||
|
||||
|
||||
## Creating Compatible Files
|
||||
|
||||
The file format of vector image files on Pebble is the PDC (Pebble Draw Command)
|
||||
format, which includes all the instructions necessary to allow drawing of
|
||||
vectors. These files are created from compatible SVG (Scalar Vector Graphics)
|
||||
files using the
|
||||
[`svg2pdc`]({{site.links.examples_org}}/cards-example/blob/master/tools/svg2pdc.py)
|
||||
tool.
|
||||
|
||||
<div class="alert alert--fg-white alert--bg-dark-red">
|
||||
Pebble Draw Command files can only be used from app resources, and cannot be
|
||||
created at runtime.
|
||||
</div>
|
||||
|
||||
To convert an SVG file to a PDC image of the same name:
|
||||
|
||||
```bash
|
||||
$ python svg2pdc.py image.svg
|
||||
```
|
||||
|
||||
To create a PDCS (Pebble Draw Command Sequence) from individual SVG frames,
|
||||
specify the directory containing the frames with the `--sequence` flag when
|
||||
running `svg2pdc`:
|
||||
|
||||
```bash
|
||||
$ ls frames/
|
||||
1.svg 2.svg 3.svg
|
||||
4.svg 5.svg
|
||||
|
||||
$ python svg2pdc.py --sequence frames/
|
||||
```
|
||||
|
||||
In the example above, this will create an output file in the `frames` directory
|
||||
called `frames.pdc` that contains draw command data for the complete animation.
|
||||
|
||||
<div class="alert alert--fg-white alert--bg-dark-red">
|
||||
{% markdown %}
|
||||
**Limitations**
|
||||
|
||||
The `svg2pdc` tool currently supports SVG files that use **only** the following
|
||||
elements: `g`, `layer`, `path`, `rect`, `polyline`, `polygon`, `line`, `circle`.
|
||||
|
||||
We recommend using Adobe Illustrator to create compatible SVG icons and images.
|
||||
{% endmarkdown %}
|
||||
</div>
|
||||
|
||||
For simplicity, compatible image and sequence files will be provided for you to
|
||||
use in your own project.
|
||||
|
||||
|
||||
### PDC icons
|
||||
|
||||
Example PDC image files are available for the icons listed in
|
||||
[*App Assets*](/guides/app-resources/app-assets/).
|
||||
These are ideal for use in many common types of apps, such as notification or
|
||||
weather apps.
|
||||
|
||||
[Download PDC icon files >{center,bg-lightblue,fg-white}]({{ site.links.s3_assets }}/assets/other/pebble-timeline-icons-pdc.zip)
|
||||
|
||||
|
||||
## Getting Started
|
||||
|
||||
^CP^ Begin a new [CloudPebble]({{ site.links.cloudpebble }}) project using the
|
||||
blank template and add code only to push an initial ``Window``, such as the
|
||||
example below:
|
||||
|
||||
^LC^ Begin a new project using `pebble new-project` and create a simple app that
|
||||
pushes a blank ``Window``, such as the example below:
|
||||
|
||||
```c
|
||||
#include <pebble.h>
|
||||
|
||||
static Window *s_main_window;
|
||||
|
||||
static void main_window_load(Window *window) {
|
||||
Layer *window_layer = window_get_root_layer(window);
|
||||
GRect bounds = layer_get_bounds(window_layer);
|
||||
|
||||
}
|
||||
|
||||
static void main_window_unload(Window *window) {
|
||||
|
||||
}
|
||||
|
||||
static void init() {
|
||||
s_main_window = window_create();
|
||||
window_set_window_handlers(s_main_window, (WindowHandlers) {
|
||||
.load = main_window_load,
|
||||
.unload = main_window_unload,
|
||||
});
|
||||
window_stack_push(s_main_window, true);
|
||||
}
|
||||
|
||||
static void deinit() {
|
||||
window_destroy(s_main_window);
|
||||
}
|
||||
|
||||
int main() {
|
||||
init();
|
||||
app_event_loop();
|
||||
deinit();
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Drawing a PDC Image
|
||||
|
||||
For this tutorial, use the example
|
||||
[`weather_image.pdc`](/assets/other/weather_image.pdc) file provided.
|
||||
|
||||
^CP^ Add the PDC file as a project resource using the 'Add new' under
|
||||
'Resources' on the left-hand side of the CloudPebble editor, with an
|
||||
'Identifier' of `WEATHER_IMAGE`, and a type of 'raw binary blob'. The file is
|
||||
assumed to be called `weather_image.pdc`.
|
||||
|
||||
^LC^ Add the PDC file to your project resources in `package.json` as shown
|
||||
below. Set the 'name' field to `WEATHER_IMAGE`, and the 'type' field to `raw`.
|
||||
The file is assumed to be called `weather_image.pdc`:
|
||||
|
||||
<div class="platform-specific" data-sdk-platform="local">
|
||||
{% highlight {} %}
|
||||
"media": [
|
||||
{
|
||||
"type": "raw",
|
||||
"name": "WEATHER_IMAGE",
|
||||
"file": "weather_image.pdc"
|
||||
}
|
||||
]
|
||||
{% endhighlight %}
|
||||
</div>
|
||||
|
||||
^LC^ Drawing a Pebble Draw Command image is just as simple as drawing a normal PNG
|
||||
image to a graphics context, requiring only one draw call. First, load the
|
||||
`.pdc` file from resources, for example with the `name` defined as
|
||||
`WEATHER_IMAGE`, as shown below.
|
||||
|
||||
^CP^ Drawing a Pebble Draw Command image is just as simple as drawing a normal
|
||||
PNG image to a graphics context, requiring only one draw call. First, load the
|
||||
`.pdc` file from resources, for example with the 'Identifier' defined as
|
||||
`WEATHER_IMAGE`. This will be available in code as `RESOURCE_ID_WEATHER_IMAGE`,
|
||||
as shown below.
|
||||
|
||||
Declare a pointer of type ``GDrawCommandImage`` at the top of the file:
|
||||
|
||||
```c
|
||||
static GDrawCommandImage *s_command_image;
|
||||
```
|
||||
|
||||
Create and assign the ``GDrawCommandImage`` in `init()`, before calling
|
||||
`window_stack_push()`:
|
||||
|
||||
```nc|c
|
||||
static void init() {
|
||||
/* ... */
|
||||
|
||||
// Create the object from resource file
|
||||
s_command_image = gdraw_command_image_create_with_resource(RESOURCE_ID_WEATHER_IMAGE);
|
||||
|
||||
/* ... */
|
||||
}
|
||||
```
|
||||
|
||||
Next, define the ``LayerUpdateProc`` that will be used to draw the PDC image:
|
||||
|
||||
```c
|
||||
static void update_proc(Layer *layer, GContext *ctx) {
|
||||
// Set the origin offset from the context for drawing the image
|
||||
GPoint origin = GPoint(10, 20);
|
||||
|
||||
// Draw the GDrawCommandImage to the GContext
|
||||
gdraw_command_image_draw(ctx, s_command_image, origin);
|
||||
}
|
||||
```
|
||||
|
||||
Next, create a ``Layer`` to display the image:
|
||||
|
||||
```c
|
||||
static Layer *s_canvas_layer;
|
||||
```
|
||||
|
||||
Next, set the ``LayerUpdateProc`` that will do the rendering and add it to the
|
||||
desired ``Window``:
|
||||
|
||||
```c
|
||||
static void main_window_load(Window *window) {
|
||||
|
||||
/* ... */
|
||||
|
||||
// Create the canvas Layer
|
||||
s_canvas_layer = layer_create(GRect(30, 30, bounds.size.w, bounds.size.h));
|
||||
|
||||
// Set the LayerUpdateProc
|
||||
layer_set_update_proc(s_canvas_layer, update_proc);
|
||||
|
||||
// Add to parent Window
|
||||
layer_add_child(window_layer, s_canvas_layer);
|
||||
}
|
||||
```
|
||||
|
||||
Finally, don't forget to free the memory used by the ``Window``'s sub-components
|
||||
in `main_window_unload()`:
|
||||
|
||||
```c
|
||||
static void main_window_unload(Window *window) {
|
||||
layer_destroy(s_canvas_layer);
|
||||
gdraw_command_image_destroy(s_command_image);
|
||||
}
|
||||
```
|
||||
|
||||
When run, the PDC image will be loaded, and rendered in the ``LayerUpdateProc``.
|
||||
To put the image into contrast, we will finally change the ``Window`` background
|
||||
color after `window_create()`:
|
||||
|
||||
```c
|
||||
window_set_background_color(s_main_window, GColorBlueMoon);
|
||||
```
|
||||
|
||||
The result will look similar to the example shown below.
|
||||
|
||||

|
||||
|
||||
|
||||
## Playing a PDC Sequence
|
||||
|
||||
The ``GDrawCommandSequence`` API allows developers to use vector graphics as
|
||||
individual frames in a larger animation. Just like ``GDrawCommandImage``s, each
|
||||
``GDrawCommandFrame`` is drawn to a graphics context in a ``LayerUpdateProc``.
|
||||
|
||||
For this tutorial, use the example
|
||||
[`clock_sequence.pdc`](/assets/other/clock_sequence.pdc) file provided.
|
||||
|
||||
Begin a new app, with a C file containing the [template](#getting-started) provided above.
|
||||
|
||||
^CP^ Next, add the file as a `raw` resource in the same way as for a PDC image,
|
||||
for example with an `Identifier` specified as `CLOCK_SEQUENCE`.
|
||||
|
||||
^LC^ Next, add the file as a `raw` resource in the same way as for a PDC image,
|
||||
for example with the `name` field specified in `package.json` as
|
||||
`CLOCK_SEQUENCE`.
|
||||
|
||||
<div class="platform-specific" data-sdk-platform="local">
|
||||
{% highlight {} %}
|
||||
"media": [
|
||||
{
|
||||
"type": "raw",
|
||||
"name": "CLOCK_SEQUENCE",
|
||||
"file": "clock_sequence.pdc"
|
||||
}
|
||||
]
|
||||
{% endhighlight %}
|
||||
</div>
|
||||
|
||||
Load the PDCS in your app by first declaring a ``GDrawCommandSequence`` pointer:
|
||||
|
||||
```c
|
||||
static GDrawCommandSequence *s_command_seq;
|
||||
```
|
||||
|
||||
Next, initialize the object in `init()` before calling `window_stack_push()`:
|
||||
|
||||
```nc|c
|
||||
static void init() {
|
||||
/* ... */
|
||||
|
||||
// Load the sequence
|
||||
s_command_seq = gdraw_command_sequence_create_with_resource(RESOURCE_ID_CLOCK_SEQUENCE);
|
||||
|
||||
/* ... */
|
||||
}
|
||||
```
|
||||
|
||||
Get the next frame and draw it in the ``LayerUpdateProc``. Then register a timer
|
||||
to draw the next frame:
|
||||
|
||||
```c
|
||||
// Milliseconds between frames
|
||||
#define DELTA 13
|
||||
|
||||
static int s_index = 0;
|
||||
|
||||
/* ... */
|
||||
|
||||
static void next_frame_handler(void *context) {
|
||||
// Draw the next frame
|
||||
layer_mark_dirty(s_canvas_layer);
|
||||
|
||||
// Continue the sequence
|
||||
app_timer_register(DELTA, next_frame_handler, NULL);
|
||||
}
|
||||
|
||||
static void update_proc(Layer *layer, GContext *ctx) {
|
||||
// Get the next frame
|
||||
GDrawCommandFrame *frame = gdraw_command_sequence_get_frame_by_index(s_command_seq, s_index);
|
||||
|
||||
// If another frame was found, draw it
|
||||
if (frame) {
|
||||
gdraw_command_frame_draw(ctx, s_command_seq, frame, GPoint(0, 30));
|
||||
}
|
||||
|
||||
// Advance to the next frame, wrapping if neccessary
|
||||
int num_frames = gdraw_command_sequence_get_num_frames(s_command_seq);
|
||||
s_index++;
|
||||
if (s_index == num_frames) {
|
||||
s_index = 0;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Next, create a new ``Layer`` to utilize the ``LayerUpdateProc`` and add it to the
|
||||
desired ``Window``.
|
||||
|
||||
Create the `Window` pointer:
|
||||
|
||||
```c
|
||||
static Layer *s_canvas_layer;
|
||||
```
|
||||
|
||||
Next, create the ``Layer`` and assign it to the new pointer. Set its update
|
||||
procedure and add it to the ``Window``:
|
||||
|
||||
```c
|
||||
static void main_window_load(Window *window) {
|
||||
// Get Window information
|
||||
Layer *window_layer = window_get_root_layer(window);
|
||||
GRect bounds = layer_get_bounds(window_layer);
|
||||
|
||||
// Create the canvas Layer
|
||||
s_canvas_layer = layer_create(GRect(30, 30, bounds.size.w, bounds.size.h));
|
||||
|
||||
// Set the LayerUpdateProc
|
||||
layer_set_update_proc(s_canvas_layer, update_proc);
|
||||
|
||||
// Add to parent Window
|
||||
layer_add_child(window_layer, s_canvas_layer);
|
||||
}
|
||||
```
|
||||
|
||||
Start the animation loop using a timer at the end of initialization:
|
||||
|
||||
```c
|
||||
// Start the animation
|
||||
app_timer_register(DELTA, next_frame_handler, NULL);
|
||||
```
|
||||
|
||||
Finally, remember to destroy the ``GDrawCommandSequence`` and ``Layer`` in
|
||||
`main_window_unload()`:
|
||||
|
||||
```c
|
||||
static void main_window_unload(Window *window) {
|
||||
layer_destroy(s_canvas_layer);
|
||||
gdraw_command_sequence_destroy(s_command_seq);
|
||||
}
|
||||
```
|
||||
|
||||
When run, the animation will be played by the timer at a framerate dictated by
|
||||
`DELTA`, looking similar to the example shown below:
|
||||
|
||||

|
||||
|
||||
|
||||
## What's Next?
|
||||
|
||||
You have now learned how to add vector images and animations to your apps.
|
||||
Complete examples for these APIs are available under the `pebble-examples`
|
||||
GitHub organization:
|
||||
|
||||
* [`pdc-image`]({{site.links.examples_org}}/pdc-image) - Example
|
||||
implementation of a Pebble Draw Command Image.
|
||||
|
||||
* [`pdc-sequence`]({{site.links.examples_org}}/pdc-sequence) - Example
|
||||
implementation of a Pebble Draw Command Sequence animated icon.
|
||||
|
||||
|
||||
More advanced tutorials will be added here in the future, so keep checking back!
|
106
devsite/source/tutorials/index.html
Normal file
106
devsite/source/tutorials/index.html
Normal file
|
@ -0,0 +1,106 @@
|
|||
---
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
layout: default
|
||||
title: Tutorials
|
||||
description: |
|
||||
Get started with Pebble development!
|
||||
menu_section: tutorials
|
||||
---
|
||||
<div class="row no-gutter full-height--m full-height--l getting-started">
|
||||
<div class="col-m-7 full-height">
|
||||
|
||||
<div class="bigbox bigbox--green bigbox--half">
|
||||
<div class="row full-height">
|
||||
|
||||
<div class="col-m-4 hidden-xs hidden-s title-image watchface-image"></div>
|
||||
<div class="col-m-8 col-s-12 full-height">
|
||||
<div class="vcenter--wrapper">
|
||||
<div class="vcenter">
|
||||
<h3>Build a Watchface</h3>
|
||||
<p>Learn how to create your first watchface. This tutorial will cover basic Pebble concepts, and is the recommended starting point for new developers.</p>
|
||||
<a href="/tutorials/js-watchface-tutorial/" target="_blank" class="btn btn--fg-green btn--bg-white">Build with JS</a>
|
||||
<a href="/tutorials/watchface-tutorial/" target="_blank" class="btn btn--fg-green btn--bg-white">Build with C</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bigbox bigbox--dark-blue bigbox--half">
|
||||
<div class="row full-height">
|
||||
|
||||
<div class="col-m-8 col-s-12 full-height">
|
||||
<div class="vcenter--wrapper">
|
||||
<div class="vcenter">
|
||||
<h3>Build a One Click Action</h3>
|
||||
<p>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.</p>
|
||||
<a href="/guides/design-and-interaction/one-click-actions/" target="_blank" class="btn btn--fg-lightblue btn--bg-white">Build with C</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-m-4 hidden-xs hidden-s title-image one-click-action-image"></div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="col-m-5 full-height">
|
||||
|
||||
<div class="bigbox bigbox--darkgray bigbox--third">
|
||||
<div class="vcenter--wrapper">
|
||||
<div class="vcenter">
|
||||
<h3>Learn C with Pebble</h3>
|
||||
<p>A community driven, open source textbook that teaches the fundamentals of C through the scope of Pebble application development.</p>
|
||||
<a href="https://pebble.gitbooks.io/learning-c-with-pebble/content/" target="_blank" class="btn btn--fg-gray-02 btn--bg-white">Read the Book</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bigbox bigbox--lightgray bigbox--third">
|
||||
<div class="vcenter--wrapper">
|
||||
<div class="vcenter">
|
||||
<h3>Publish Your App</h3>
|
||||
<p>Learn how to publish your watchface or watchapp on Pebble's appstore.</p>
|
||||
<a href="/guides/appstore-publishing/publishing-an-app" target="_blank" class="btn btn--fg-lightblue btn--bg-dawk">Publish an App</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bigbox bigbox--darkgray bigbox--third">
|
||||
<div class="vcenter--wrapper">
|
||||
<div class="vcenter">
|
||||
<h3>Go Beyond</h3>
|
||||
<p>If you're looking to take the next step with your Pebble development, we encourage you to checkout the following resources:</p>
|
||||
<p>
|
||||
<ul style="align: left">
|
||||
<li><a href="/tutorials/advanced/">Advanced Tutorials</a></li>
|
||||
<li><a href="/guides/pebble-packages/creating-packages/">Create and Publish a Pebble Packages</a></li>
|
||||
<li><a href="/guides/pebble-packages/using-packages/">Integrate Pebble Packages into your project</a></li>
|
||||
<li><a href="/guides/events-and-services/health/">Integrate with Pebble Health</a></li>
|
||||
<li><a href="/guides/events-and-services/dictation/">Use Pebble Dictation Service</a></li>
|
||||
<li><a href="/guides/user-interfaces/app-configuration/">Create Configuration Pages</a></li>
|
||||
<li><a href="/guides/communication/using-pebblekit-android/">Build an Android Companion Apps</a></li>
|
||||
<li><a href="/guides/communication/using-pebblekit-ios/">Build an iOS Companion Apps</a></li>
|
||||
</ul>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
18
devsite/source/tutorials/js-watchface-tutorial/index.html
Normal file
18
devsite/source/tutorials/js-watchface-tutorial/index.html
Normal file
|
@ -0,0 +1,18 @@
|
|||
---
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
layout: utils/redirect_permanent
|
||||
redirect_to: /tutorials/js-watchface-tutorial/part1/
|
||||
---
|
491
devsite/source/tutorials/js-watchface-tutorial/part1.md
Normal file
491
devsite/source/tutorials/js-watchface-tutorial/part1.md
Normal file
|
@ -0,0 +1,491 @@
|
|||
---
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
layout: tutorials/tutorial
|
||||
tutorial: js-watchface
|
||||
tutorial_part: 1
|
||||
|
||||
title: Build a Watchface in JavaScript using Rocky.js
|
||||
description: A guide to making a new Pebble watchface with Rocky.js
|
||||
permalink: /tutorials/js-watchface-tutorial/part1/
|
||||
menu_section: tutorials
|
||||
generate_toc: true
|
||||
platform_choice: true
|
||||
---
|
||||
|
||||
{% include tutorials/rocky-js-warning.html %}
|
||||
|
||||
In this tutorial we'll cover the basics of writing a simple watchface with
|
||||
Rocky.js, Pebble's JavaScript API. Rocky.js enables developers to create
|
||||
beautiful and feature-rich watchfaces with a modern programming language.
|
||||
|
||||
Rocky.js should not be confused with Pebble.js which also allowed developers to
|
||||
write applications in JavaScript. Unlike Pebble.js, Rocky.js runs natively on
|
||||
the watch and is now the only offically supported method for developing
|
||||
JavaScript applications for Pebble smartwatches.
|
||||
|
||||
We're going to start with some basics, then create a simple digital watchface
|
||||
and finally create an analog clock which looks just like this:
|
||||
|
||||

|
||||
|
||||
## First Steps
|
||||
|
||||
^CP^ Go to [CloudPebble]({{ site.links.cloudpebble }}) and click
|
||||
'Get Started' to log in using your Pebble account, or create a new one if you do
|
||||
not already have one. Once you've logged in, click 'Create' to create a new
|
||||
project. Give your project a suitable name, such as 'Tutorial 1' and set the
|
||||
'Project Type' as 'Rocky.js (beta)'. This will create a completely empty
|
||||
project, so before you continue, you will need to click the 'Add New' button in
|
||||
the left menu to create a new Rocky.js JavaScript file.
|
||||
|
||||
^CP^ Next we need to change our project from a watchapp to a watchface. Click
|
||||
'Settings' in the left menu, then change the 'APP KIND' to 'watchface'.
|
||||
|
||||
<div class="platform-specific" data-sdk-platform="local">
|
||||
{% markdown {} %}
|
||||
If you haven't already, head over the [SDK Page](/sdk/install/) to learn how to
|
||||
download and install the latest version of the Pebble Tool, and the latest SDK.
|
||||
|
||||
Once you've installed the Pebble Tool and SDK 4.0, you can create a new Rocky.js
|
||||
project with the following command:
|
||||
|
||||
```nc|text
|
||||
$ pebble new-project --rocky helloworld
|
||||
```
|
||||
|
||||
This will create a new folder called `helloworld` and populate it with the basic
|
||||
structure required for a basic Rocky.js application.
|
||||
{% endmarkdown %}
|
||||
</div>
|
||||
|
||||
|
||||
## Watchface Basics
|
||||
|
||||
Watchface 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.
|
||||
|
||||
^CP^ We'll start by editing the `index.js` file that we created earlier. Click
|
||||
on the filename in the left menu and it will load, ready for editing.
|
||||
|
||||
^LC^ The main entry point for the watchface is `/src/rocky/index.js`, so we'll
|
||||
start by editing this file.
|
||||
|
||||
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.
|
||||
|
||||
```js
|
||||
var rocky = require('rocky');
|
||||
```
|
||||
|
||||
Next, the invocation of `rocky.on('minutechange', ...)` registers a callback
|
||||
method to the `minutechange` event - which is emitted every time the internal
|
||||
clock's minute changes (and also when the handler is registered). Watchfaces
|
||||
should invoke the ``requestDraw`` method as part of the `minutechange` event to
|
||||
redraw the screen.
|
||||
|
||||
```js
|
||||
rocky.on('minutechange', function(event) {
|
||||
rocky.requestDraw();
|
||||
});
|
||||
```
|
||||
|
||||
> **NOTE**: Watchfaces that need to update more or less frequently can also
|
||||
> register the `secondchange`, `hourchange` or `daychange` events.
|
||||
|
||||
Next we register a callback method to the `draw` event - which is emitted after
|
||||
each call to `rocky.requestDraw()`. The `event` parameter passed into the
|
||||
callback function includes a ``CanvasRenderingContext2D`` object, which is used
|
||||
to determine the display characteristics and draw text or shapes on the display.
|
||||
|
||||
```js
|
||||
rocky.on('draw', function(event) {
|
||||
// Get the CanvasRenderingContext2D object
|
||||
var ctx = event.context;
|
||||
});
|
||||
```
|
||||
|
||||
The ``RockyDrawCallback`` is where we render the smartwatch display, using the
|
||||
methods provided to us through the ``CanvasRenderingContext2D`` object.
|
||||
|
||||
> **NOTE**: The `draw` event may also be emitted at other times, such
|
||||
as when the handler is first registered.
|
||||
|
||||
## Creating a Digital Watchface
|
||||
|
||||
In order to create a simple digital watchface, we will need to do the following
|
||||
things:
|
||||
|
||||
- Subscribe to the `minutechange` event.
|
||||
- Subscribe to the `draw` event, so we can update the display.
|
||||
- Clear the display each time we draw on the screen.
|
||||
- Determine the width and height of the available content area of the screen.
|
||||
- Obtain the current date and time.
|
||||
- Set the text color to white.
|
||||
- Center align the text.
|
||||
- Display the current time, using the width and height to determine the center
|
||||
point of the screen.
|
||||
|
||||
^CP^ To create our minimal watchface which displays the current time, let's
|
||||
replace the contents of our `index.js` file with the following code:
|
||||
|
||||
^LC^ To create our minimal watchface which displays the current time, let's
|
||||
replace the contents of `/src/rocky/index.js` with the following code:
|
||||
|
||||
```js
|
||||
var rocky = require('rocky');
|
||||
|
||||
rocky.on('draw', function(event) {
|
||||
// Get the CanvasRenderingContext2D object
|
||||
var ctx = event.context;
|
||||
|
||||
// Clear the screen
|
||||
ctx.clearRect(0, 0, ctx.canvas.clientWidth, ctx.canvas.clientHeight);
|
||||
|
||||
// Determine the width and height of the display
|
||||
var w = ctx.canvas.unobstructedWidth;
|
||||
var h = ctx.canvas.unobstructedHeight;
|
||||
|
||||
// Current date/time
|
||||
var d = new Date();
|
||||
|
||||
// Set the text color
|
||||
ctx.fillStyle = 'white';
|
||||
|
||||
// Center align the text
|
||||
ctx.textAlign = 'center';
|
||||
|
||||
// Display the time, in the middle of the screen
|
||||
ctx.fillText(d.toLocaleTimeString(), w / 2, h / 2, w);
|
||||
});
|
||||
|
||||
rocky.on('minutechange', function(event) {
|
||||
// Display a message in the system logs
|
||||
console.log("Another minute with your Pebble!");
|
||||
|
||||
// Request the screen to be redrawn on next pass
|
||||
rocky.requestDraw();
|
||||
});
|
||||
```
|
||||
|
||||
## First Compilation and Installation
|
||||
|
||||
^CP^ To compile the watchface, click the 'PLAY' button on the right hand side
|
||||
of the screen. This will save your file, compile the project and launch your
|
||||
watchface in the emulator.
|
||||
|
||||
^CP^ Click the 'VIEW LOGS' button.
|
||||
|
||||
<div class="platform-specific" data-sdk-platform="local">
|
||||
{% markdown {} %}
|
||||
To compile the watchface, make sure you have saved your project files, then
|
||||
run the following command from the project's root directory:
|
||||
|
||||
```nc|text
|
||||
$ pebble build
|
||||
```
|
||||
|
||||
After a successful compilation you will see a message reading `'build' finished
|
||||
successfully`.
|
||||
|
||||
If there are any problems with your code, the compiler will tell you which lines
|
||||
contain an error, so you can fix them. See
|
||||
[Troubleshooting and Debugging](#troubleshooting-and-debugging) for further
|
||||
information.
|
||||
|
||||
Now install the watchapp and view the logs on the emulator by running:
|
||||
|
||||
```nc|text
|
||||
$ pebble install --logs --emulator basalt
|
||||
```
|
||||
{% endmarkdown %}
|
||||
</div>
|
||||
|
||||
## Congratulations!
|
||||
|
||||
You should see a loading bar as the watchface is loaded, shortly followed by
|
||||
your watchface running in the emulator.
|
||||
|
||||

|
||||
|
||||
Your logs should also be displaying the message we told it to log with
|
||||
`console.log()`.
|
||||
|
||||
```nc|text
|
||||
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();`
|
||||
|
||||
## Creating an Analog Watchface
|
||||
|
||||
In order to draw an analog watchface, we will need to do the following things:
|
||||
|
||||
- Subscribe to the `minutechange` event.
|
||||
- Subscribe to the `draw` event, so we can update the display.
|
||||
- Obtain the current date and time.
|
||||
- Clear the display each time we draw on the screen.
|
||||
- Determine the width and height of the available content area of the screen.
|
||||
- Use the width and height to determine the center point of the screen.
|
||||
- Calculate the max length of the watch hands based on the available space.
|
||||
- Determine the correct angle for minutes and hours.
|
||||
- Draw the minute and hour hands, outwards from the center point.
|
||||
|
||||
### Drawing the Hands
|
||||
|
||||
We're going to need to draw two lines, one representing the hour hand, and one
|
||||
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.
|
||||
|
||||
First we need to find the center point in our display:
|
||||
|
||||
```js
|
||||
// Determine the available width and height of the display
|
||||
var w = ctx.canvas.unobstructedWidth;
|
||||
var h = ctx.canvas.unobstructedHeight;
|
||||
|
||||
// Determine the center point of the display
|
||||
var cx = w / 2;
|
||||
var cy = h / 2;
|
||||
```
|
||||
|
||||
Now we know the starting point for the hands (`cx`, `cy`), but we still need to
|
||||
determine the end point. We can do this with a tiny bit of math:
|
||||
|
||||
```js
|
||||
var x2 = cx + Math.sin(angle) * length;
|
||||
var y2 = cy - Math.cos(angle) * length;
|
||||
```
|
||||
|
||||
Then we'll use the `ctx` parameter and configure the line width and color of
|
||||
the hand.
|
||||
|
||||
```js
|
||||
// Configure how we want to draw the hand
|
||||
ctx.lineWidth = 8;
|
||||
ctx.strokeStyle = color;
|
||||
```
|
||||
|
||||
Finally we draw the hand, starting from the center of the screen, drawing a
|
||||
straight line outwards.
|
||||
|
||||
```js
|
||||
// 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();
|
||||
```
|
||||
|
||||
### Putting It All Together
|
||||
|
||||
```js
|
||||
var rocky = require('rocky');
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
// 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();
|
||||
});
|
||||
```
|
||||
|
||||
Now compile and run your project in the emulator to see the results!
|
||||
|
||||
|
||||
## Troubleshooting and Debugging
|
||||
|
||||
If your build didn't work, you'll see the error message: `Build Failed`. Let's
|
||||
take a look at some of the common types of errors:
|
||||
|
||||
|
||||
### Rocky.js Linter
|
||||
|
||||
As part of the build process, your Rocky `index.js` file is automatically
|
||||
checked for errors using a process called
|
||||
['linting'](https://en.wikipedia.org/wiki/Lint_%28software%29).
|
||||
|
||||
The first thing to check is the 'Lint Results' section of the build output.
|
||||
|
||||
```nc|text
|
||||
========== Lint Results: index.js ==========
|
||||
|
||||
src/rocky/index.js(7,39): error TS1005: ',' expected.
|
||||
src/rocky/index.js(9,8): error TS1005: ':' expected.
|
||||
src/rocky/index.js(9,37): error TS1005: ',' expected.
|
||||
src/rocky/index.js(7,1): warning TS2346: Supplied parameters do not match any signature of call target.
|
||||
src/rocky/index.js(7,24): warning TS2304: Cannot find name 'funtion'.
|
||||
|
||||
Errors: 3, Warnings: 2
|
||||
Please fix the issues marked with 'error' above.
|
||||
```
|
||||
|
||||
In the error messages above, we see the filename which contains the error,
|
||||
followed by the line number and column number where the error occurs. For
|
||||
example:
|
||||
|
||||
```nc|text
|
||||
Filename: src/rocky/index.js
|
||||
Line number: 7
|
||||
Character: 24
|
||||
Description: Cannot find name 'funtion'.
|
||||
```
|
||||
|
||||
```javascript
|
||||
rocky.on('minutechange', funtion(event) {
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
As we can see, this error relates to a typo, 'funtion' should be 'function'.
|
||||
Once this error has been fixed and you run `pebble build` again, you should
|
||||
see:
|
||||
|
||||
```nc|text
|
||||
========== Lint Results: index.js ==========
|
||||
|
||||
Everything looks AWESOME!
|
||||
```
|
||||
|
||||
### Locating Errors Using Logging
|
||||
|
||||
So what do we do when the build is successful, but our code isn't functioning as
|
||||
expected? Logging!
|
||||
|
||||
Scatter a breadcrumb trail through your application code, that you can follow as
|
||||
your application is running. This will help to narrow down the location of
|
||||
the problem.
|
||||
|
||||
```javascript
|
||||
rocky.on('minutechange', function(event) {
|
||||
console.log('minutechange fired!');
|
||||
// ...
|
||||
});
|
||||
```
|
||||
Once you've added your logging statements, rebuild the application and view the
|
||||
logs:
|
||||
|
||||
^CP^ Click the 'PLAY' button on the right hand side of the screen, then click
|
||||
the 'VIEW LOGS' button.
|
||||
|
||||
<div class="platform-specific" data-sdk-platform="local">
|
||||
{% markdown {} %}
|
||||
```nc|text
|
||||
$ pebble build && pebble install --emulator basalt --logs
|
||||
```
|
||||
{% endmarkdown %}
|
||||
</div>
|
||||
|
||||
If you find that one of your logging statements hasn't appeared in the log
|
||||
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
|
||||
[Discord Server]({{ site.links.discord_invite }}) and ask for assistance.
|
||||
|
||||
|
||||
## Conclusion
|
||||
|
||||
So there we have it, the basic process required to create a brand new Pebble
|
||||
watchface using JavaScript! To do this we:
|
||||
|
||||
1. Created a new Rocky.js project.
|
||||
2. Included the `'rocky'` library.
|
||||
3. Subscribed to the `minutechange` event.
|
||||
4. Subscribed to the `draw` event.
|
||||
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)
|
||||
|
||||
## What's Next
|
||||
|
||||
If you successfully built and run your application, you should have seen a very
|
||||
basic watchface that closely mimics the built-in TicToc. In the next tutorial,
|
||||
we'll use `postMessage` to pass information to the mobile device, and
|
||||
request weather data from the web.
|
||||
|
||||
[Go to Part 2 → >{wide,bg-dark-red,fg-white}](/tutorials/js-watchface-tutorial/part2/)
|
380
devsite/source/tutorials/js-watchface-tutorial/part2.md
Normal file
380
devsite/source/tutorials/js-watchface-tutorial/part2.md
Normal file
|
@ -0,0 +1,380 @@
|
|||
---
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
layout: tutorials/tutorial
|
||||
tutorial: js-watchface
|
||||
tutorial_part: 2
|
||||
|
||||
title: Adding Web Content to a Rocky.js JavaScript Watchface
|
||||
description: A guide to adding web content to a JavaScript watchface
|
||||
permalink: /tutorials/js-watchface-tutorial/part2/
|
||||
menu_section: tutorials
|
||||
generate_toc: true
|
||||
platform_choice: true
|
||||
---
|
||||
|
||||
{% include tutorials/rocky-js-warning.html %}
|
||||
|
||||
In the [previous tutorial](/tutorials/js-watchface-tutorial/part1), we looked
|
||||
at the process of creating a basic watchface using Pebble's new JavaScript API.
|
||||
|
||||
In this tutorial, we'll extend the example to add weather conditions from the
|
||||
Internet to our watchface.
|
||||
|
||||

|
||||
|
||||
We'll be using the JavaScript component `pkjs`, which runs on the user's mobile
|
||||
device using [PebbleKit JS](/docs/pebblekit-js). This `pkjs` component can be
|
||||
used to access information from the Internet and process it on the phone. This
|
||||
`pkjs` environment does not have the same the hardware and memory constraints of
|
||||
the Pebble.
|
||||
|
||||
## First Steps
|
||||
|
||||
^CP^ The first thing we'll need to do is add a new JavaScript file to the
|
||||
project we created in [Part 1](/tutorials/js-watchface-tutorial/part1). Click
|
||||
'Add New' in the left menu, set the filename to `index.js` and the 'TARGET' to
|
||||
'PebbleKit JS'.
|
||||
|
||||
^LC^ The first thing we'll need to do is edit a file from the project we
|
||||
created in [Part 1](/tutorials/js-watchface-tutorial/part1). The file is
|
||||
called `/src/pkjs/index.js` and it is the entry point for the `pkjs` portion
|
||||
of the application.
|
||||
|
||||
This `pkjs` component of our application is capable of sending and receiving
|
||||
messages with the smartwatch, accessing the user's location, making web
|
||||
requests, and an assortment of other tasks that are all documented in the
|
||||
[PebbleKit JS](/docs/pebblekit-js) documentation.
|
||||
|
||||
> Although Rocky.js (watch) and `pkjs` (phone) both use JavaScript, they
|
||||
> have separate APIs and purposes. It is important to understand the differences
|
||||
> and not attempt to run your code within the wrong component.
|
||||
|
||||
## Sending and Receiving Messages
|
||||
|
||||
Before we get onto the example, it's important to understand how to send and
|
||||
receive messages between the Rocky.js component on the smartwatch, and the
|
||||
`pkjs` component on the mobile device.
|
||||
|
||||
### Sending Messages
|
||||
|
||||
To send a message from the smartwatch to the mobile device, use the
|
||||
``rocky.postMessage`` method, which allows you to send an arbitrary JSON
|
||||
object:
|
||||
|
||||
```js
|
||||
// rocky index.js
|
||||
var rocky = require('rocky');
|
||||
|
||||
// Send a message from the smartwatch
|
||||
rocky.postMessage({'test': 'hello from smartwatch'});
|
||||
```
|
||||
|
||||
To send a message from the mobile device to the smartwatch, use the
|
||||
``Pebble.postMessage`` method:
|
||||
|
||||
```js
|
||||
// pkjs index.js
|
||||
|
||||
// Send a message from the mobile device
|
||||
Pebble.postMessage({'test': 'hello from mobile device'});
|
||||
```
|
||||
|
||||
### Message Listeners
|
||||
|
||||
We can create a message listener in our smartwatch code using the ``rocky.on``
|
||||
method:
|
||||
|
||||
```js
|
||||
// rocky index.js
|
||||
|
||||
// On the smartwatch, begin listening for a message from the mobile device
|
||||
rocky.on('message', function(event) {
|
||||
// Get the message that was passed
|
||||
console.log(JSON.stringify(event.data));
|
||||
});
|
||||
```
|
||||
|
||||
We can also create a message listener in our `pkjs` code using the ``Pebble.on``
|
||||
method:
|
||||
|
||||
```js
|
||||
// pkjs index.js
|
||||
|
||||
// On the phone, begin listening for a message from the smartwatch
|
||||
Pebble.on('message', function(event) {
|
||||
// Get the message that was passed
|
||||
console.log(JSON.stringify(event.data));
|
||||
});
|
||||
```
|
||||
|
||||
## Requesting Location
|
||||
|
||||
Our `pkjs` component can access to the location of the user's smartphone. The
|
||||
Rocky.js component cannot access location information directly, it must request
|
||||
it from `pkjs`.
|
||||
|
||||
^CP^ In order to use this functionality, you must change your project settings
|
||||
in CloudPebble. Click 'SETTINGS' in the left menu, then tick 'USES LOCATION'.
|
||||
|
||||
<div class="platform-specific" data-sdk-platform="local">
|
||||
{% markdown {} %}
|
||||
In order to use this functionality, your application must include the
|
||||
`location` flag in the
|
||||
[`pebble.capabilities`](/guides/tools-and-resources/app-metadata/)
|
||||
array of your `package.json` file.
|
||||
|
||||
|
||||
```js
|
||||
// file: package.json
|
||||
// ...
|
||||
"pebble": {
|
||||
"capabilities": ["location"]
|
||||
}
|
||||
// ...
|
||||
```
|
||||
{% endmarkdown %}
|
||||
</div>
|
||||
|
||||
Once we've added the `location` flag, we can access GPS coordinates using the
|
||||
[Geolocation API](https://developer.mozilla.org/en-US/docs/Web/API/Geolocation).
|
||||
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
|
||||
|
||||
Pebble.on('message', function(event) {
|
||||
// Get the message that was passed
|
||||
var message = event.data;
|
||||
|
||||
if (message.fetch) {
|
||||
navigator.geolocation.getCurrentPosition(function(pos) {
|
||||
// TODO: fetch weather
|
||||
}, function(err) {
|
||||
console.error('Error getting location');
|
||||
},
|
||||
{ timeout: 15000, maximumAge: 60000 });
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Web Service Calls
|
||||
|
||||
The `pkjs` side of our application can also access the
|
||||
[XMLHttpRequest](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest)
|
||||
object. Using this object, developers are able to interact with external web
|
||||
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).
|
||||
|
||||
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
|
||||
|
||||
function request(url, type, callback) {
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.onload = function () {
|
||||
callback(this.responseText);
|
||||
};
|
||||
xhr.open(type, url);
|
||||
xhr.send();
|
||||
}
|
||||
```
|
||||
|
||||
The three arguments we have to provide when calling our `request()` method are
|
||||
the URL, the type of request (`GET` or `POST`) and a callback for when the
|
||||
response is received.
|
||||
|
||||
### Fetching Weather Data
|
||||
|
||||
The URL is specified on the
|
||||
[OpenWeatherMap API page](http://openweathermap.org/current), and contains the
|
||||
coordinates supplied by `getCurrentPosition()` (latitude and longitude),
|
||||
followed by the API key:
|
||||
|
||||
{% include guides/owm-api-key-notice.html %}
|
||||
|
||||
```js
|
||||
var myAPIKey = '1234567';
|
||||
var url = 'http://api.openweathermap.org/data/2.5/weather' +
|
||||
'?lat=' + pos.coords.latitude +
|
||||
'&lon=' + pos.coords.longitude +
|
||||
'&appid=' + myAPIKey;
|
||||
```
|
||||
|
||||
All together, our message handler should now look like the following:
|
||||
|
||||
```js
|
||||
// pkjs index.js
|
||||
|
||||
var myAPIKey = '1234567';
|
||||
|
||||
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);
|
||||
//TODO: Send weather to smartwatch
|
||||
});
|
||||
}, function(err) {
|
||||
console.error('Error getting location');
|
||||
},
|
||||
{ timeout: 15000, maximumAge: 60000 });
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Finishing Up
|
||||
|
||||
Once we receive the weather data from OpenWeatherMap, we need to send it to the
|
||||
smartwatch using ``Pebble.postMessage``:
|
||||
|
||||
```js
|
||||
// pkjs index.js
|
||||
|
||||
// ...
|
||||
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
|
||||
}
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
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
|
||||
var rocky = require('rocky');
|
||||
|
||||
// Global object to store weather data
|
||||
var weather;
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
// ...
|
||||
|
||||
rocky.on('hourchange', function(event) {
|
||||
// Send a message to fetch the weather information (on startup and every hour)
|
||||
rocky.postMessage({'fetch': true});
|
||||
});
|
||||
```
|
||||
|
||||
Finally, we'll need some new code in our Rocky `draw` handler to display the
|
||||
temperature and conditions:
|
||||
|
||||
```js
|
||||
// rocky index.js
|
||||
var rocky = require('rocky');
|
||||
|
||||
// ...
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
## Conclusion
|
||||
|
||||
So there we have it, we successfully added web content to our JavaScript
|
||||
watchface! To do this we:
|
||||
|
||||
1. Enabled `location` in our `package.json`.
|
||||
2. Added a `Pebble.on('message', function() {...});` listener in `pkjs`.
|
||||
3. Retrieved the users current GPS coordinates in `pkjs`.
|
||||
4. Used `XMLHttpRequest` to query OpenWeatherMap API.
|
||||
5. Sent the current weather conditions from the mobile device, to the
|
||||
smartwatch, using `Pebble.postMessage()`.
|
||||
6. On the smartwatch, we created a `rocky.on('message', function() {...});`
|
||||
listener to receive the weather data from `pkjs`.
|
||||
7. We subscribed to the `hourchange` event, to send a message to `pkjs` to
|
||||
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)
|
||||
|
||||
## 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 }}).
|
18
devsite/source/tutorials/watchface-tutorial/index.md
Normal file
18
devsite/source/tutorials/watchface-tutorial/index.md
Normal file
|
@ -0,0 +1,18 @@
|
|||
---
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
layout: utils/redirect_permanent
|
||||
redirect_to: /tutorials/watchface-tutorial/part1/
|
||||
---
|
475
devsite/source/tutorials/watchface-tutorial/part1.md
Normal file
475
devsite/source/tutorials/watchface-tutorial/part1.md
Normal file
|
@ -0,0 +1,475 @@
|
|||
---
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
layout: tutorials/tutorial
|
||||
tutorial: watchface
|
||||
tutorial_part: 1
|
||||
|
||||
title: Build Your Own Watchface in C
|
||||
description: A guide to making a new Pebble watchface with the Pebble C API
|
||||
permalink: /tutorials/watchface-tutorial/part1/
|
||||
menu_section: tutorials
|
||||
generate_toc: true
|
||||
platform_choice: true
|
||||
---
|
||||
|
||||
In this tutorial we'll cover the basics of writing a simple watchface with
|
||||
Pebble's C API. Customizability is at the heart of the Pebble philosophy, so
|
||||
we'll be sure to add some exciting features for the user!
|
||||
|
||||
When we are done this section of the tutorial, you should end up with a brand
|
||||
new basic watchface looking something like this:
|
||||
|
||||
{% screenshot_viewer %}
|
||||
{
|
||||
"image": "/images/getting-started/watchface-tutorial/1-time.png",
|
||||
"platforms": [
|
||||
{"hw": "aplite", "wrapper": "steel-black"},
|
||||
{"hw": "basalt", "wrapper": "time-red"},
|
||||
{"hw": "chalk", "wrapper": "time-round-rosegold-14"}
|
||||
]
|
||||
}
|
||||
{% endscreenshot_viewer %}
|
||||
|
||||
|
||||
## First Steps
|
||||
|
||||
So, let's get started!
|
||||
|
||||
^CP^ Go to [CloudPebble]({{ site.links.cloudpebble }}) and click 'Get Started'
|
||||
to log in using your Pebble account, or create a new one if you do not already
|
||||
have one. Next, click 'Create' to create a new project. Give your project a
|
||||
suitable name, such as 'Tutorial 1' and leave the 'Project Type' as 'Pebble C
|
||||
SDK', with a 'Template' of 'Empty project', as we will be starting from scratch
|
||||
to help maximize your understanding as we go.
|
||||
|
||||
^LC^ Before you can start the tutorial you will need to have the Pebble SDK
|
||||
installed. If you haven't done this yet, go to our [download page](/sdk) to grab
|
||||
the SDK and follow the instructions to install it on your machine. Once you've
|
||||
done that you can come back here and carry on where you left off.
|
||||
|
||||
^LC^ Once you have installed the SDK, navigate to a directory of your choosing
|
||||
and run `pebble new-project watchface` (where 'watchface' is the name of your
|
||||
new project) to start a new project and set up all the relevant files.
|
||||
|
||||
^CP^ Click 'Create' and you will see the main CloudPebble project screen. The
|
||||
left menu shows all the relevant links you will need to create your watchface.
|
||||
Click on 'Settings' and you will see the name you just supplied, along with
|
||||
several other options. As we are creating a watchface, change the 'App Kind' to
|
||||
'Watchface'.
|
||||
|
||||
^LC^ In an SDK project, all the information about how an app is configured (its
|
||||
name, author, capabilities and resource listings etc) is stored in a file in the
|
||||
project root directory called `package.json`. Since this project will be a
|
||||
watchface, you will need to modify the `watchapp` object in this file to reflect
|
||||
this:
|
||||
|
||||
<div class="platform-specific" data-sdk-platform="local">
|
||||
{% highlight {} %}
|
||||
"watchapp": {
|
||||
"watchface": true
|
||||
}
|
||||
{% endhighlight %}
|
||||
</div>
|
||||
|
||||
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.
|
||||
|
||||
^CP^ Finally, set your 'Company Name' and we can start to write some code!
|
||||
|
||||
^LC^ Finally, set a value for `companyName` and we can start to write some code!
|
||||
|
||||
|
||||
## Watchface Basics
|
||||
|
||||
^CP^ Create the first source file by clicking 'Add New' on the left menu,
|
||||
selecting 'C file' as the type and choosing a suitable name such as 'main.c'.
|
||||
Click 'Create' and you will be shown the main editor screen.
|
||||
|
||||
^LC^ 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.
|
||||
|
||||
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
|
||||
file like so:
|
||||
|
||||
```c
|
||||
#include <pebble.h>
|
||||
```
|
||||
|
||||
After this first line, we must 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
|
||||
possible. Additionally, `main()` also calls ``app_event_loop()``, which lets the
|
||||
watchapp wait for system events until it exits.
|
||||
|
||||
^CP^ The recommended structure is shown below, and you can use it as the basis
|
||||
for your own watchface file by copying it into CloudPebble:
|
||||
|
||||
^LC^ The recommended structure is shown below, and you can use it as the basis
|
||||
for your main C file:
|
||||
|
||||
```c
|
||||
#include <pebble.h>
|
||||
|
||||
static void init() {
|
||||
|
||||
}
|
||||
|
||||
static void deinit() {
|
||||
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
init();
|
||||
app_event_loop();
|
||||
deinit();
|
||||
}
|
||||
```
|
||||
|
||||
To add the first ``Window``, we first declare a static pointer to a ``Window``
|
||||
variable, so that we can access it wherever we need to, chiefly in the `init()`
|
||||
and `deinit()` functions. Add this declaration below `#include`, prefixed with
|
||||
`s_` to denote its `static` nature (`static` here means it is accessible only
|
||||
within this file):
|
||||
|
||||
```c
|
||||
static Window *s_main_window;
|
||||
```
|
||||
|
||||
The next step is to create an instance of ``Window`` to assign to this pointer,
|
||||
which we will do in `init()` using the appropriate Pebble SDK functions. In this
|
||||
process we also assign two handler functions that provide an additional layer of
|
||||
abstraction to manage the subsequent creation of the ``Window``'s sub-elements,
|
||||
in a similar way to how `init()` and `deinit()` perform this task for the
|
||||
watchapp as a whole. These two functions should be created above `init()` and
|
||||
must match the following signatures (the names may differ, however):
|
||||
|
||||
```c
|
||||
static void main_window_load(Window *window) {
|
||||
|
||||
}
|
||||
|
||||
static void main_window_unload(Window *window) {
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
With this done, we can complete the creation of the ``Window`` element, making
|
||||
reference to these two new handler functions that are called by the system
|
||||
whenever the ``Window`` is being constructed. This process is shown below, and
|
||||
takes place in `init()`:
|
||||
|
||||
```c
|
||||
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);
|
||||
}
|
||||
```
|
||||
|
||||
A good best-practice to learn at this early stage is to match every Pebble SDK
|
||||
`_create()` function call with the equivalent `_destroy()` function to make sure
|
||||
all memory used is given back to the system when the app exits. Let's do this
|
||||
now in `deinit()` for our main ``Window`` element:
|
||||
|
||||
```c
|
||||
static void deinit() {
|
||||
// Destroy Window
|
||||
window_destroy(s_main_window);
|
||||
}
|
||||
```
|
||||
|
||||
We can now compile and run this watchface, but it will not show anything
|
||||
interesting yet. It is also a good practice to check that our code is still
|
||||
valid after each iterative change, so let's do this now.
|
||||
|
||||
|
||||
## First Compilation and Installation
|
||||
|
||||
^CP^ To compile the watchface, make sure you have saved your C file by clicking
|
||||
the 'Save' icon on the right of the editor screen and then proceed to the
|
||||
'Compilation' screen by clicking the appropriate link on the left of the screen.
|
||||
Click 'Run Build' to start the compilation process and wait for the result.
|
||||
Hopefully the status should become 'Succeeded', meaning the code is valid and
|
||||
can be run on the watch.
|
||||
|
||||
^LC^ 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
|
||||
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.
|
||||
|
||||
In order to install your watchface on your Pebble, first
|
||||
[setup the Pebble Developer Connection](/guides/tools-and-resources/developer-connection/).
|
||||
Make sure you are using the latest version of the Pebble app.
|
||||
|
||||
^CP^ Click 'Install and Run' and wait for the app to install.
|
||||
|
||||
^LC^ 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`.
|
||||
|
||||
<div class="platform-specific" data-sdk-platform="local">
|
||||
{% markdown {} %}
|
||||
> 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`.
|
||||
{% endmarkdown %}
|
||||
</div>
|
||||
|
||||
Congratulations! You should see that you have a new item in the watchface menu,
|
||||
but it is entirely blank!
|
||||
|
||||
{% screenshot_viewer %}
|
||||
{
|
||||
"image": "/images/getting-started/watchface-tutorial/1-blank.png",
|
||||
"platforms": [
|
||||
{"hw": "aplite", "wrapper": "steel-black"},
|
||||
{"hw": "basalt", "wrapper": "time-red"},
|
||||
{"hw": "chalk", "wrapper": "time-round-rosegold-14"}
|
||||
]
|
||||
}
|
||||
{% endscreenshot_viewer %}
|
||||
|
||||
Let's change that with the next stage towards a basic watchface - the
|
||||
``TextLayer`` element.
|
||||
|
||||
|
||||
## Showing Some Text
|
||||
|
||||
^CP^ Navigate back to the CloudPebble code editor and open your main C file to
|
||||
continue adding code.
|
||||
|
||||
^LC^ Re-open your main C file to continue adding code.
|
||||
|
||||
The best way to show some text on a watchface or watchapp
|
||||
is to use a ``TextLayer`` element. The first step in doing this is to follow a
|
||||
similar procedure to that used for setting up the ``Window`` with a pointer,
|
||||
ideally added below `s_main_window`:
|
||||
|
||||
```c
|
||||
static TextLayer *s_time_layer;
|
||||
```
|
||||
|
||||
This will be the first element added to our ``Window``, so we will make the
|
||||
Pebble SDK function calls to create it in `main_window_load()`. After calling
|
||||
``text_layer_create()``, we call other functions with plain English names that
|
||||
describe exactly what they do, which is to help setup layout properties for the
|
||||
text shown in the ``TextLayer`` including colors, alignment and font size. We
|
||||
also include a call to ``text_layer_set_text()`` with "00:00" so that we can
|
||||
verify that the ``TextLayer`` is set up correctly.
|
||||
|
||||
The layout parameters will vary depending on the shape of the display. To easily
|
||||
specify which value of the vertical position is used on each of the round and
|
||||
rectangular display shapes we use ``PBL_IF_ROUND_ELSE()``. Thus
|
||||
`main_window_load()` becomes:
|
||||
|
||||
```c
|
||||
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));
|
||||
}
|
||||
```
|
||||
|
||||
Note the use of SDK values such as ``GColorBlack`` and `FONT_KEY_BITHAM_42_BOLD`
|
||||
which allow use of built-in features and behavior. These examples here are the
|
||||
color black and a built in system font. Later we will discuss loading a custom
|
||||
font file, which can be used to replace this value.
|
||||
|
||||
Just like with ``Window``, we must be sure to destroy each element we create. We
|
||||
will do this in `main_window_unload()`, to keep the management of the
|
||||
``TextLayer`` completely within the loading and unloading of the ``Window`` it
|
||||
is associated with. This function should now look like this:
|
||||
|
||||
```c
|
||||
static void main_window_unload(Window *window) {
|
||||
// Destroy TextLayer
|
||||
text_layer_destroy(s_time_layer);
|
||||
}
|
||||
```
|
||||
|
||||
^CP^ This completes the setup of the basic watchface layout. If you return to
|
||||
'Compilation' and install a new build, you should now see the following:
|
||||
|
||||
^LC^ 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:
|
||||
|
||||
{% screenshot_viewer %}
|
||||
{
|
||||
"image": "/images/getting-started/watchface-tutorial/1-textlayer-test.png",
|
||||
"platforms": [
|
||||
{"hw": "aplite", "wrapper": "steel-black"},
|
||||
{"hw": "basalt", "wrapper": "time-red"},
|
||||
{"hw": "chalk", "wrapper": "time-round-rosegold-14"}
|
||||
]
|
||||
}
|
||||
{% endscreenshot_viewer %}
|
||||
|
||||
The final step is to get the current time and display it using the
|
||||
``TextLayer``. This is done with the ``TickTimerService``.
|
||||
|
||||
|
||||
## Telling the Time
|
||||
|
||||
The ``TickTimerService`` is an Event Service that allows access to the current
|
||||
time by subscribing a function to be run whenever the time changes. Normally
|
||||
this may be every minute, but can also be every hour, or every second. However,
|
||||
the latter will incur extra battery costs, so use it sparingly. We can do this
|
||||
by calling ``tick_timer_service_subscribe()``, but first we must create a
|
||||
function to give the service to call whenever the time changes, and must match
|
||||
this signature:
|
||||
|
||||
```c
|
||||
static void tick_handler(struct tm *tick_time, TimeUnits units_changed) {
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
This means that whenever the time changes, we are provided with a data structure
|
||||
of type `struct tm` containing the current time
|
||||
[in various forms](http://www.cplusplus.com/reference/ctime/tm/), as well as a
|
||||
constant ``TimeUnits`` value that tells us which unit changed, to allow
|
||||
filtering of behaviour. With our ``TickHandler`` created, we can register it
|
||||
with the Event Service in `init()` like so:
|
||||
|
||||
```c
|
||||
// Register with TickTimerService
|
||||
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.
|
||||
|
||||
This function will use `strftime()`
|
||||
([See here for formatting](http://www.cplusplus.com/reference/ctime/strftime/))
|
||||
to extract the hours and minutes from the `struct tm` data structure and write
|
||||
it into a character buffer. This buffer is required by ``TextLayer`` to be
|
||||
long-lived as long as the text is to be displayed, as it is not copied into the
|
||||
``TextLayer``, but merely referenced. We achieve this by making the buffer
|
||||
`static`, so it persists across multiple calls to `update_time()`. Therefore
|
||||
this function should be created before `main_window_load()` and look like this:
|
||||
|
||||
```c
|
||||
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);
|
||||
}
|
||||
```
|
||||
|
||||
Our ``TickHandler`` follows the correct function signature and contains only a
|
||||
single call to `update_time()` to do just that:
|
||||
|
||||
```c
|
||||
static void tick_handler(struct tm *tick_time, TimeUnits units_changed) {
|
||||
update_time();
|
||||
}
|
||||
```
|
||||
|
||||
Lastly, `init()` should be modified include a call to
|
||||
`update_time()` after ``window_stack_push()`` to ensure the time is displayed
|
||||
correctly when the watchface loads:
|
||||
|
||||
```c
|
||||
// Make sure the time is displayed from the start
|
||||
update_time();
|
||||
```
|
||||
|
||||
Since we can now display the time we can remove the call to
|
||||
``text_layer_set_text()`` in `main_window_load()`, as it is no longer needed to
|
||||
test the layout.
|
||||
|
||||
Re-compile and re-install the watchface on your Pebble, and it should look like
|
||||
this:
|
||||
|
||||
{% screenshot_viewer %}
|
||||
{
|
||||
"image": "/images/getting-started/watchface-tutorial/1-time.png",
|
||||
"platforms": [
|
||||
{"hw": "aplite", "wrapper": "steel-black"},
|
||||
{"hw": "basalt", "wrapper": "time-red"},
|
||||
{"hw": "chalk", "wrapper": "time-round-rosegold-14"}
|
||||
]
|
||||
}
|
||||
{% endscreenshot_viewer %}
|
||||
|
||||
|
||||
## Conclusion
|
||||
|
||||
So there we have it, the basic process required to create a brand new Pebble
|
||||
watchface! To do this we:
|
||||
|
||||
1. Created a new Pebble project.
|
||||
2. Setup basic app structure.
|
||||
3. Setup a main ``Window``.
|
||||
4. Setup a ``TextLayer`` to display the time.
|
||||
5. Subscribed to ``TickTimerService`` to get updates on the time, and wrote
|
||||
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.
|
||||
|
||||
^CP^ [Edit in CloudPebble >{center,bg-lightblue,fg-white}]({{ site.links.cloudpebble }}ide/gist/9b9d50b990d742a3ae34)
|
||||
|
||||
^LC^ [View Source Code >{center,bg-lightblue,fg-white}](https://gist.github.com/9b9d50b990d742a3ae34)
|
||||
|
||||
## What's Next?
|
||||
|
||||
The next section of the tutorial will introduce adding custom fonts and bitmap
|
||||
images to your watchface.
|
||||
|
||||
[Go to Part 2 → >{wide,bg-dark-red,fg-white}](/tutorials/watchface-tutorial/part2/)
|
300
devsite/source/tutorials/watchface-tutorial/part2.md
Normal file
300
devsite/source/tutorials/watchface-tutorial/part2.md
Normal file
|
@ -0,0 +1,300 @@
|
|||
---
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
layout: tutorials/tutorial
|
||||
tutorial: watchface
|
||||
tutorial_part: 2
|
||||
|
||||
title: Customizing Your Watchface
|
||||
description: A guide to personalizing your new Pebble watchface
|
||||
permalink: /tutorials/watchface-tutorial/part2/
|
||||
generate_toc: true
|
||||
platform_choice: true
|
||||
---
|
||||
|
||||
In the previous page of the tutorial, you learned how to create a new Pebble
|
||||
project, set it up as a basic watchface and use ``TickTimerService`` to display
|
||||
the current time. However, the design was pretty basic, so let's improve it with
|
||||
some customization!
|
||||
|
||||
In order to do this we will be using some new Pebble SDK concepts, including:
|
||||
|
||||
- Resource management
|
||||
- Custom fonts (using ``GFont``)
|
||||
- Images (using ``GBitmap`` and ``BitmapLayer``)
|
||||
|
||||
These will allow us to completely change the look and feel of the watchface. We
|
||||
will provide some sample materials to use, but once you understand the process
|
||||
be sure to replace these with your own to truly make it your own! Once we're
|
||||
done, you should end up with a watchface looking like this:
|
||||
|
||||
{% screenshot_viewer %}
|
||||
{
|
||||
"image": "/images/getting-started/watchface-tutorial/2-final.png",
|
||||
"platforms": [
|
||||
{"hw": "aplite", "wrapper": "steel-black"},
|
||||
{"hw": "basalt", "wrapper": "time-red"},
|
||||
{"hw": "chalk", "wrapper": "time-round-rosegold-14"}
|
||||
]
|
||||
}
|
||||
{% endscreenshot_viewer %}
|
||||
|
||||
## 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).
|
||||
|
||||
^CP^ You can create a new CloudPebble project from this template by
|
||||
[clicking here]({{ site.links.cloudpebble }}ide/gist/9b9d50b990d742a3ae34).
|
||||
|
||||
The result of the first part should look something like this - a basic time
|
||||
display:
|
||||
|
||||
{% screenshot_viewer %}
|
||||
{
|
||||
"image": "/images/getting-started/watchface-tutorial/1-time.png",
|
||||
"platforms": [
|
||||
{"hw": "aplite", "wrapper": "steel-black"},
|
||||
{"hw": "basalt", "wrapper": "time-red"},
|
||||
{"hw": "chalk", "wrapper": "time-round-rosegold-14"}
|
||||
]
|
||||
}
|
||||
{% endscreenshot_viewer %}
|
||||
|
||||
Let's improve it!
|
||||
|
||||
## Adding a Custom Font
|
||||
|
||||
^CP^ To add a custom font resource to use for the time display ``TextLayer``,
|
||||
click 'Add New' on the left of the CloudPebble editor. Set the 'Resource Type'
|
||||
to 'TrueType font' and upload a font file. Choose an 'Identifier', which is the
|
||||
value we will use to refer to the font resource in the `.c` file. This must end
|
||||
with the desired font size, which must be small enough to show a wide time such
|
||||
as '23:50' in the ``TextLayer``. If it does not fit, you can always return here
|
||||
to try another size. Click save and the font will be added to your project.
|
||||
|
||||
^LC^ 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:
|
||||
|
||||
<div class="platform-specific" data-sdk-platform="local">
|
||||
{% highlight {} %}
|
||||
"media": [
|
||||
{
|
||||
"type": "font",
|
||||
"name": "FONT_PERFECT_DOS_48",
|
||||
"file": "fonts/perfect-dos-vga.ttf",
|
||||
"compatibility":"2.7"
|
||||
}
|
||||
]
|
||||
{% endhighlight %}
|
||||
</div>
|
||||
|
||||
^LC^ In the example above, we would place our `perfect-dos-vga.ttf` file in the
|
||||
`/resources/fonts/` folder of our project.
|
||||
|
||||
A custom font file must be a
|
||||
[TrueType](http://en.wikipedia.org/wiki/TrueType) font in the `.ttf` file format.
|
||||
[Here is an example font to use]({{ site.asset_path }}/fonts/getting-started/watchface-tutorial/perfect-dos-vga.ttf)
|
||||
([source](http://www.dafont.com/perfect-dos-vga-437.font)).
|
||||
|
||||
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.
|
||||
|
||||
```c
|
||||
// Declare globally
|
||||
static GFont s_time_font;
|
||||
```
|
||||
|
||||
Next, we add the creation and substitution of the new ``GFont`` in the existing
|
||||
call to ``text_layer_set_font()`` in `main_window_load()`. Shown here is an
|
||||
example identifier used when uploading the font earlier, `FONT_PERFECT_DOS_48`,
|
||||
which is always pre-fixed with `RESOURCE_ID_`:
|
||||
|
||||
```c
|
||||
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
|
||||
text_layer_set_font(s_time_layer, s_time_font);
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
And finally, safe destruction of the ``GFont`` in `main_window_unload()`:
|
||||
|
||||
```c
|
||||
void main_window_unload() {
|
||||
// ...
|
||||
// Unload GFont
|
||||
fonts_unload_custom_font(s_time_font);
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
^CP^ After re-compiling and re-installing (either by using the green 'Play'
|
||||
button to the top right of the CloudPebble editor, or by clicking 'Run Build'
|
||||
and 'Install and Run' on the 'Compilation' screen), the watchface should feature
|
||||
a much more interesting font.
|
||||
|
||||
^LC^ After re-compiling and re-installing with `pebble build && pebble install`,
|
||||
the watchface should feature a much more interesting font.
|
||||
|
||||
An example screenshot is shown below:
|
||||
|
||||
{% screenshot_viewer %}
|
||||
{
|
||||
"image": "/images/getting-started/watchface-tutorial/2-custom-font.png",
|
||||
"platforms": [
|
||||
{"hw": "aplite", "wrapper": "steel-black"},
|
||||
{"hw": "basalt", "wrapper": "time-red"},
|
||||
{"hw": "chalk", "wrapper": "time-round-rosegold-14"}
|
||||
]
|
||||
}
|
||||
{% endscreenshot_viewer %}
|
||||
|
||||
|
||||
## 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
|
||||
[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
|
||||
behave in a similar fashion to ``GFont`` and ``TextLayer``, so let's get
|
||||
started.
|
||||
|
||||
^CP^ The first step is the same as using a custom font; import the bitmap into
|
||||
CloudPebble as a resource by clicking 'Add New' next to 'Resources' on the left
|
||||
of the CloudPebble project screen. Ensure the 'Resource Type' is 'Bitmap image',
|
||||
choose an identifier for the resource and upload your file.
|
||||
|
||||
^LC^ You add a bitmap to the `package.json` file in the
|
||||
[same way](/guides/app-resources/fonts) as a font, except the new `media` array
|
||||
object will have a `type` of `bitmap`. Below is an example:
|
||||
|
||||
<div class="platform-specific" data-sdk-platform="local">
|
||||
{% highlight {} %}
|
||||
{
|
||||
"type": "bitmap",
|
||||
"name": "IMAGE_BACKGROUND",
|
||||
"file": "images/background.png"
|
||||
}
|
||||
{% endhighlight %}
|
||||
</div>
|
||||
|
||||
As before, here is an example bitmap we have created for you to use, which looks
|
||||
like this:
|
||||
|
||||
[]({{ site.asset_path }}/images/getting-started/watchface-tutorial/background.png)
|
||||
|
||||
Once this has been added to the project, return to your `.c` file and declare
|
||||
two more pointers, one each of ``GBitmap`` and ``BitmapLayer`` near the top of
|
||||
the file:
|
||||
|
||||
```c
|
||||
static BitmapLayer *s_background_layer;
|
||||
static GBitmap *s_background_bitmap;
|
||||
```
|
||||
|
||||
Now we will create both of these in `main_window_load()`. After both elements
|
||||
are created, we set the ``BitmapLayer`` to use our ``GBitmap`` and then add it
|
||||
as a child of the main ``Window`` as we did for the ``TextLayer``.
|
||||
|
||||
However, is should be noted that the ``BitmapLayer`` must be added to the
|
||||
``Window`` before the ``TextLayer``. This will ensure that the text is drawn *on
|
||||
top of* the image. Otherwise, the text will be drawn behind the image and remain
|
||||
invisible to us. Here is that process in full, to be as clear as possible:
|
||||
|
||||
```c
|
||||
// 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));
|
||||
```
|
||||
|
||||
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);
|
||||
```
|
||||
|
||||
The final step is to set the background color of the main ``Window`` to match
|
||||
the background image. Do this in `init()`:
|
||||
|
||||
```c
|
||||
window_set_background_color(s_main_window, GColorBlack);
|
||||
```
|
||||
|
||||
With all this in place, the example background image should nicely frame the
|
||||
time and match the style of the new custom font. Of course, if you have used
|
||||
your own font and bitmap (highly recommended!) then your watchface will not look
|
||||
exactly like this.
|
||||
|
||||
{% screenshot_viewer %}
|
||||
{
|
||||
"image": "/images/getting-started/watchface-tutorial/2-final.png",
|
||||
"platforms": [
|
||||
{"hw": "aplite", "wrapper": "steel-black"},
|
||||
{"hw": "basalt", "wrapper": "time-red"},
|
||||
{"hw": "chalk", "wrapper": "time-round-rosegold-14"}
|
||||
]
|
||||
}
|
||||
{% endscreenshot_viewer %}
|
||||
|
||||
|
||||
## Conclusion
|
||||
|
||||
After adding a custom font and a background image, our new watchface now looks
|
||||
much nicer. If you want to go a bit further, try adding a new ``TextLayer`` in
|
||||
the same way as the time display one to show the current date (hint: look at the
|
||||
[formatting options](http://www.cplusplus.com/reference/ctime/strftime/)
|
||||
available for `strftime()`!)
|
||||
|
||||
As with last time, you can compare your own code to the example source code
|
||||
using the button below.
|
||||
|
||||
^CP^ [Edit in CloudPebble >{center,bg-lightblue,fg-white}]({{ site.links.cloudpebble }}ide/gist/d216d9e0b840ed296539)
|
||||
|
||||
^LC^ [View Source Code >{center,bg-lightblue,fg-white}](https://gist.github.com/d216d9e0b840ed296539)
|
||||
|
||||
|
||||
## What's Next?
|
||||
|
||||
The next section of the tutorial will introduce PebbleKit JS for adding
|
||||
web-based content to your watchface.
|
||||
|
||||
[Go to Part 3 → >{wide,bg-dark-red,fg-white}](/tutorials/watchface-tutorial/part3/)
|
613
devsite/source/tutorials/watchface-tutorial/part3.md
Normal file
613
devsite/source/tutorials/watchface-tutorial/part3.md
Normal file
|
@ -0,0 +1,613 @@
|
|||
---
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
layout: tutorials/tutorial
|
||||
tutorial: watchface
|
||||
tutorial_part: 3
|
||||
|
||||
title: Adding Web Content
|
||||
description: A guide to adding web-based content your Pebble watchface
|
||||
permalink: /tutorials/watchface-tutorial/part3/
|
||||
generate_toc: true
|
||||
platform_choice: true
|
||||
---
|
||||
|
||||
In the previous tutorial parts, we created a simple watchface to tell the time
|
||||
and then improved it with a custom font and background bitmap. There's a lot you
|
||||
can do with those elements, such as add more bitmaps, an extra ``TextLayer``
|
||||
showing the date, but let's aim even higher. This part is longer than the last,
|
||||
so make sure you have a nice cup of your favourite hot beverage on hand before
|
||||
embarking!
|
||||
|
||||
In this tutorial we will add some extra content to the watchface that is fetched
|
||||
from the web using [PebbleKit JS](/guides/communication/using-pebblekit-js/).
|
||||
This part of the SDK allows you to use JavaScript to access the web as well as
|
||||
the phone's location services and storage. It even allows you to display a
|
||||
configuration screen to give users options over how they want your watchface or
|
||||
app to look and run.
|
||||
|
||||
By the end of this tutorial we will arrive at a watchface like the one below, in
|
||||
all its customized glory:
|
||||
|
||||
{% screenshot_viewer %}
|
||||
{
|
||||
"image": "/images/getting-started/watchface-tutorial/3-final.png",
|
||||
"platforms": [
|
||||
{"hw": "aplite", "wrapper": "steel-black"},
|
||||
{"hw": "basalt", "wrapper": "time-red"},
|
||||
{"hw": "chalk", "wrapper": "time-round-rosegold-14"}
|
||||
]
|
||||
}
|
||||
{% 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).
|
||||
|
||||
^CP^ You can create a new CloudPebble project from this template by
|
||||
[clicking here]({{ site.links.cloudpebble }}ide/gist/d216d9e0b840ed296539).
|
||||
|
||||
## Preparing the Watchface Layout
|
||||
|
||||
The content we will be fetching will be the current weather conditions and
|
||||
temperature from [OpenWeatherMap](http://openweathermap.org). We will need a new
|
||||
``TextLayer`` to show this extra content. Let's do that now at the top of the C
|
||||
file, as we did before:
|
||||
|
||||
```c
|
||||
static TextLayer *s_weather_layer;
|
||||
```
|
||||
|
||||
As usual, we then create it properly in `main_window_load()` after the existing
|
||||
elements. Here is the ``TextLayer`` setup; this should all be familiar to you
|
||||
from the previous two tutorial parts:
|
||||
|
||||
```c
|
||||
// Create temperature 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);
|
||||
text_layer_set_text(s_weather_layer, "Loading...");
|
||||
```
|
||||
|
||||
We will be using the same font as the time display, but at a reduced font size.
|
||||
|
||||
^CP^ To do this, we return to our uploaded font resource and click 'Another
|
||||
Font. The second font that appears below should be given an 'Identifier' with
|
||||
`_20` at the end, signifying we now want font size 20 (suitable for the example
|
||||
font provided).
|
||||
|
||||
^LC^ You can add another font in `package.json` by duplicating the first font's
|
||||
entry in the `media` array and changing the font size indicated in the `name`
|
||||
field to `_20` or similar. Below is an example showing both fonts:
|
||||
|
||||
<div class="platform-specific" data-sdk-platform="local">
|
||||
{% highlight {} %}
|
||||
"media": [
|
||||
{
|
||||
"type":"font",
|
||||
"name":"FONT_PERFECT_DOS_48",
|
||||
"file":"perfect-dos-vga.ttf",
|
||||
"compatibility": "2.7"
|
||||
},
|
||||
{
|
||||
"type":"font",
|
||||
"name":"FONT_PERFECT_DOS_20",
|
||||
"file":"perfect-dos-vga.ttf",
|
||||
"compatibility": "2.7"
|
||||
},
|
||||
]
|
||||
{% endhighlight %}
|
||||
</div>
|
||||
|
||||
Now we will load and apply that font as we did last time, beginning with a new
|
||||
``GFont`` declared at the top of the file:
|
||||
|
||||
```c
|
||||
static GFont s_weather_font;
|
||||
```
|
||||
|
||||
Next, we load the resource and apply it to the new ``TextLayer`` and then add
|
||||
that as a child layer to the main ``Window``:
|
||||
|
||||
```c
|
||||
// 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));
|
||||
```
|
||||
|
||||
Finally, as usual, we add the same destruction calls in `main_window_unload()`
|
||||
as for everything else:
|
||||
|
||||
```c
|
||||
// Destroy weather elements
|
||||
text_layer_destroy(s_weather_layer);
|
||||
fonts_unload_custom_font(s_weather_font);
|
||||
```
|
||||
|
||||
After compiling and installing, your watchface should look something like this:
|
||||
|
||||
{% screenshot_viewer %}
|
||||
{
|
||||
"image": "/images/getting-started/watchface-tutorial/3-loading.png",
|
||||
"platforms": [
|
||||
{"hw": "aplite", "wrapper": "steel-black"},
|
||||
{"hw": "basalt", "wrapper": "time-red"},
|
||||
{"hw": "chalk", "wrapper": "time-round-rosegold-14"}
|
||||
]
|
||||
}
|
||||
{% endscreenshot_viewer %}
|
||||
|
||||
|
||||
## Preparing AppMessage
|
||||
|
||||
The primary method of communication for all Pebble watchapps and watchfaces is
|
||||
the ``AppMessage`` API. This allows the construction of key-value dictionaries
|
||||
for transmission between the watch and connected phone. The standard procedure
|
||||
we will be following for enabling this communication is as follows:
|
||||
|
||||
1. Create ``AppMessage`` callback functions to process incoming messages and
|
||||
errors.
|
||||
2. Register this callback with the system.
|
||||
3. Open ``AppMessage`` to allow app communication.
|
||||
|
||||
After this process is performed any incoming messages will cause a call to the
|
||||
``AppMessageInboxReceived`` callback and allow us to react to its contents.
|
||||
Let's get started!
|
||||
|
||||
The callbacks should be placed before they are referred to in the code file, so
|
||||
a good place is above `init()` where we will be registering them. The function
|
||||
signature for ``AppMessageInboxReceived`` is shown below:
|
||||
|
||||
```c
|
||||
static void inbox_received_callback(DictionaryIterator *iterator, void *context) {
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
We will also create and register three other callbacks so we can see all
|
||||
outcomes and any errors that may occur, such as dropped messages. These are
|
||||
reported with calls to ``APP_LOG`` for now, but more detail
|
||||
[can be gotten from them](http://stackoverflow.com/questions/21150193/logging-enums-on-the-pebble-watch):
|
||||
|
||||
```c
|
||||
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!");
|
||||
}
|
||||
```
|
||||
|
||||
With this in place, we will now register the callbacks with the system in
|
||||
`init()`:
|
||||
|
||||
```c
|
||||
// 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);
|
||||
```
|
||||
|
||||
And finally the third step, opening ``AppMessage`` to allow the watchface to
|
||||
receive incoming messages, directly below
|
||||
``app_message_register_inbox_received()``. It is considered best practice to
|
||||
register callbacks before opening ``AppMessage`` to ensure that no messages are
|
||||
missed. The code snippet below shows this process using two variables to specify
|
||||
the inbox and outbox size (in bytes):
|
||||
|
||||
```c
|
||||
// Open AppMessage
|
||||
const int inbox_size = 128;
|
||||
const int outbox_size = 128;
|
||||
app_message_open(inbox_size, outbox_size);
|
||||
```
|
||||
|
||||
> Read
|
||||
> [*Buffer Sizes*](/guides/pebble-apps/communications/appmessage/#buffer-sizes)
|
||||
> to learn about using correct buffer sizes for your app.
|
||||
|
||||
## Preparing PebbleKit JS
|
||||
|
||||
The weather data itself will be downloaded by the JavaScript component of the
|
||||
watchface, and runs on the connected phone whenever the watchface is opened.
|
||||
|
||||
^CP^ To begin using PebbleKit JS, click 'Add New' in the CloudPebble editor,
|
||||
next to 'Source Files'. Select 'JavaScript file' and choose a file name.
|
||||
CloudPebble allows any normally valid file name, such as `weather.js`.
|
||||
|
||||
^LC^ To begin using PebbleKit JS, add a new file to your project at
|
||||
`src/pkjs/index.js` to contain your JavaScript code.
|
||||
|
||||
To get off to a quick start, we will provide a basic template for using the
|
||||
PebbleKit JS SDK. This template features two basic event listeners. One is for
|
||||
the 'ready' event, which fires when the JS environment on the phone is first
|
||||
available after launch. The second is for the 'appmessage' event, which fires
|
||||
when an AppMessage is sent from the watch to the phone.
|
||||
|
||||
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!');
|
||||
}
|
||||
);
|
||||
|
||||
// Listen for when an AppMessage is received
|
||||
Pebble.addEventListener('appmessage',
|
||||
function(e) {
|
||||
console.log('AppMessage received!');
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
After compiling and installing the watchface, open the app logs.
|
||||
|
||||
^CP^ Click the 'View Logs' button on the confirmation dialogue or the
|
||||
'Compilation' screen if it was already dismissed.
|
||||
|
||||
^LC^ You can listen for app logs by running `pebble logs`, supplying your
|
||||
phone's IP address with the `--phone` switch. For example:
|
||||
|
||||
<div class="platform-specific" data-sdk-platform="local">
|
||||
{% highlight {} %}
|
||||
pebble logs --phone 192.168.1.78
|
||||
{% endhighlight %}
|
||||
</div>
|
||||
|
||||
^LC^ You can also combine these two commands into one:
|
||||
|
||||
<div class="platform-specific" data-sdk-platform="local">
|
||||
{% highlight {} %}
|
||||
pebble install --logs --phone 192.168.1.78
|
||||
{% endhighlight %}
|
||||
</div>
|
||||
|
||||
You should see a message matching that set to appear using `console.log()` in
|
||||
the JS console in the snippet above! This is where any information sent using
|
||||
``APP_LOG`` in the C file or `console.log()` in the JS file will be shown, and
|
||||
is very useful for debugging!
|
||||
|
||||
|
||||
## Getting Weather Information
|
||||
|
||||
To download weather information from
|
||||
[OpenWeatherMap.org](http://openweathermap.org), we will perform three steps in
|
||||
our JS file:
|
||||
|
||||
1. Request the user's location from the phone.
|
||||
2. Perform a call to the OpenWeatherMap API using an `XMLHttpRequest` object,
|
||||
supplying the location given to us from step 1.
|
||||
3. Send the information we want from the XHR request response to the watch for
|
||||
display on our watchface.
|
||||
|
||||
^CP^ Firstly, go to 'Settings' and check the 'Uses Location' box at the bottom
|
||||
of the page. This will allow the watchapp to access the phone's location
|
||||
services.
|
||||
|
||||
^LC^ You will need to add `location` to the `capabilities` array in the
|
||||
`package.json` file. This will allow the watchapp to access the phone's location
|
||||
services. This is shown in the code segment below:
|
||||
|
||||
<div class="platform-specific" data-sdk-platform="local">
|
||||
{% highlight {} %}
|
||||
"capabilities": ["location"]
|
||||
{% endhighlight %}
|
||||
</div>
|
||||
|
||||
The next step is simple to perform, and is shown in full below. The method we
|
||||
are using requires two other functions to use as callbacks for the success and
|
||||
failure conditions after requesting the user's location. It also requires two
|
||||
other pieces of information: `timeout` of the request and the `maximumAge` of
|
||||
the data:
|
||||
|
||||
```js
|
||||
function locationSuccess(pos) {
|
||||
// We will request the weather here
|
||||
}
|
||||
|
||||
function locationError(err) {
|
||||
console.log('Error requesting location!');
|
||||
}
|
||||
|
||||
function getWeather() {
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
locationSuccess,
|
||||
locationError,
|
||||
{timeout: 15000, maximumAge: 60000}
|
||||
);
|
||||
}
|
||||
|
||||
// Listen for when the watchface is opened
|
||||
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
|
||||
turn calls `getCurrentPosition()`. When this is successful, `locationSuccess()`
|
||||
is called and provides us with a single argument: `pos`, which contains the
|
||||
location information we require to make the weather info request. Let's do that
|
||||
now.
|
||||
|
||||
The next step is to assemble and send an `XMLHttpRequest` object to make the
|
||||
request to OpenWeatherMap.org. To make this easier, we will provide a function
|
||||
that simplifies its usage. Place this before `locationSuccess()`:
|
||||
|
||||
```js
|
||||
var xhrRequest = function (url, type, callback) {
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.onload = function () {
|
||||
callback(this.responseText);
|
||||
};
|
||||
xhr.open(type, url);
|
||||
xhr.send();
|
||||
};
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
{% 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
|
||||
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);
|
||||
}
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Thus when the location is successfully obtained, `xhrRequest()` is called. When
|
||||
the response arrives, the JSON object is parsed and the temperature and weather
|
||||
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
|
||||
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.
|
||||
|
||||
^CP^ Firstly, go to the 'Settings' screen, find the 'PebbleKit JS Message Keys'
|
||||
section and enter some names, like "TEMPERATURE" and "CONDITIONS":
|
||||
|
||||
^LC^ You can add your ``AppMessage`` keys in the `messageKeys` object in
|
||||
`package.json` as shown below for the example keys:
|
||||
|
||||
<div class="platform-specific" data-sdk-platform="local">
|
||||
{% highlight {} %}
|
||||
"messageKeys": [
|
||||
"TEMPERATURE",
|
||||
"CONDITIONS",
|
||||
]
|
||||
{% endhighlight %}
|
||||
</div>
|
||||
|
||||
To send the data, we call `Pebble.sendAppMessage()` after assembling the weather
|
||||
info variables `temperature` and `conditions` into a dictionary. We can
|
||||
optionally also supply two functions as success and failure callbacks:
|
||||
|
||||
```js
|
||||
// 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!');
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
While we are here, let's add another call to `getWeather()` in the `appmessage`
|
||||
event listener for when we want updates later, and will send an ``AppMessage``
|
||||
from the watch to achieve this:
|
||||
|
||||
```js
|
||||
// Listen for when an AppMessage is received
|
||||
Pebble.addEventListener('appmessage',
|
||||
function(e) {
|
||||
console.log('AppMessage received!');
|
||||
getWeather();
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
The final step on the Pebble side is to act on the information received from
|
||||
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
|
||||
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`.
|
||||
|
||||
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
|
||||
static char temperature_buffer[8];
|
||||
static char conditions_buffer[32];
|
||||
static char weather_layer_buffer[32];
|
||||
```
|
||||
|
||||
We then store the incoming information by reading the appropriate `Tuple`s to
|
||||
the two buffers using `snprintf()`:
|
||||
|
||||
```c
|
||||
// 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);
|
||||
}
|
||||
```
|
||||
|
||||
Lastly within this `if` statement, we assemble the complete string and instruct
|
||||
the ``TextLayer`` to display it:
|
||||
|
||||
```c
|
||||
// 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);
|
||||
```
|
||||
|
||||
After re-compiling and re-installing you should be presented with a watchface
|
||||
that looks similar to the one shown below:
|
||||
|
||||
{% screenshot_viewer %}
|
||||
{
|
||||
"image": "/images/getting-started/watchface-tutorial/3-final.png",
|
||||
"platforms": [
|
||||
{"hw": "aplite", "wrapper": "steel-black"},
|
||||
{"hw": "basalt", "wrapper": "time-red"},
|
||||
{"hw": "chalk", "wrapper": "time-round-rosegold-14"}
|
||||
]
|
||||
}
|
||||
{% endscreenshot_viewer %}
|
||||
|
||||
^CP^ Remember, if the text is too large for the screen, you can reduce the font
|
||||
size in the 'Resources' section of the CloudPebble editor. Don't forget to
|
||||
change the constants in the `.c` file to match the new 'Identifier'.
|
||||
|
||||
^LC^ Remember, if the text is too large for the screen, you can reduce the font
|
||||
size in `package.json` for that resource's entry in the `media` array. Don't
|
||||
forget to change the constants in the `.c` file to match the new resource's
|
||||
`name`.
|
||||
|
||||
An extra step we will perform is to modify the C code to obtain regular weather
|
||||
updates, in addition to whenever the watchface is loaded. To do this we will
|
||||
take advantage of a timer source we already have - the ``TickHandler``
|
||||
implementation, which we have called `tick_handler()`. Let's modify this to get
|
||||
weather updates every 30 minutes by adding the following code to the end of
|
||||
`tick_handler()` in our main `.c` file:
|
||||
|
||||
```c
|
||||
// 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();
|
||||
}
|
||||
```
|
||||
|
||||
Thanks to us adding a call to `getWeather()` in the `appmessage` JS event
|
||||
handler earlier, this message send in the ``TickHandler`` will result in new
|
||||
weather data being downloaded and sent to the watch. Job done!
|
||||
|
||||
## Conclusion
|
||||
|
||||
Whew! That was quite a long tutorial, but here's all you've learned:
|
||||
|
||||
1. Managing multiple font sizes.
|
||||
2. Preparing and opening ``AppMessage``.
|
||||
3. Setting up PebbleKit JS for interaction with the web.
|
||||
4. Getting the user's current location with `navigator.getCurrentPosition()`.
|
||||
5. Extracting information from a JSON response.
|
||||
6. Sending ``AppMessage`` to and from the watch.
|
||||
|
||||
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.
|
||||
|
||||
^CP^ [Edit in CloudPebble >{center,bg-lightblue,fg-white}]({{ site.links.cloudpebble }}ide/gist/216e6d5a0f0bd2328509)
|
||||
|
||||
^LC^ [View Source Code >{center,bg-lightblue,fg-white}](https://gist.github.com/216e6d5a0f0bd2328509)
|
||||
|
||||
|
||||
## What's Next?
|
||||
|
||||
The next section of the tutorial will introduce the Battery Service, and
|
||||
demonstrate how to add a battery bar to your watchface.
|
||||
|
||||
[Go to Part 4 → >{wide,bg-dark-red,fg-white}](/tutorials/watchface-tutorial/part4/)
|
157
devsite/source/tutorials/watchface-tutorial/part4.md
Normal file
157
devsite/source/tutorials/watchface-tutorial/part4.md
Normal file
|
@ -0,0 +1,157 @@
|
|||
---
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
layout: tutorials/tutorial
|
||||
tutorial: watchface
|
||||
tutorial_part: 4
|
||||
|
||||
title: Adding a Battery Bar
|
||||
description: |
|
||||
How to add a battery level meter to your watchface.
|
||||
permalink: /tutorials/watchface-tutorial/part4/
|
||||
generate_toc: true
|
||||
---
|
||||
|
||||
Another popular feature added to a lot of watchfaces is a battery meter,
|
||||
enabling users to see the state of their Pebble's battery charge level at a
|
||||
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.
|
||||
|
||||
The state of the battery is obtained using the ``BatteryStateService``. This
|
||||
service offers two modes of usage - 'peeking' at the current level, or
|
||||
subscribing to events that take place when the battery state changes. The latter
|
||||
approach will be adopted here. The battery level percentage will be stored in an
|
||||
integer at the top of the file:
|
||||
|
||||
```c
|
||||
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:
|
||||
|
||||
```c
|
||||
static void battery_callback(BatteryChargeState state) {
|
||||
// Record the new battery level
|
||||
s_battery_level = state.charge_percent;
|
||||
}
|
||||
```
|
||||
|
||||
To enable this function to be called when the battery level changes, subscribe
|
||||
to updates in `init()`:
|
||||
|
||||
```c
|
||||
// Register for battery level updates
|
||||
battery_state_service_subscribe(battery_callback);
|
||||
```
|
||||
|
||||
With the subscription in place, the UI can be created. This will take the form
|
||||
of a ``Layer`` with a ``LayerUpdateProc`` that uses the battery level to draw a
|
||||
thin, minimalist white meter along the top of the time display.
|
||||
|
||||
Create the ``LayerUpdateProc`` that will be used to draw the battery meter:
|
||||
|
||||
```c
|
||||
static void battery_update_proc(Layer *layer, GContext *ctx) {
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
Declare this new ``Layer`` at the top of the file:
|
||||
|
||||
```c
|
||||
static Layer *s_battery_layer;
|
||||
```
|
||||
|
||||
Allocate the ``Layer`` in `main_window_load()`, assign it the ``LayerUpdateProc`` that will draw it, and
|
||||
add it as a child of the main ``Window`` to make it visible:
|
||||
|
||||
```c
|
||||
// Create battery meter Layer
|
||||
s_battery_layer = layer_create(GRect(14, 54, 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);
|
||||
```
|
||||
|
||||
To ensure the battery meter is updated every time the charge level changes, mark
|
||||
it 'dirty' (to ask the system to re-render it at the next opportunity) within
|
||||
`battery_callback()`:
|
||||
|
||||
```c
|
||||
// Update meter
|
||||
layer_mark_dirty(s_battery_layer);
|
||||
```
|
||||
|
||||
The final piece of the puzzle is the actual drawing of the battery meter, which
|
||||
takes place within the ``LayerUpdateProc``. The background of the meter is drawn
|
||||
to 'paint over' the background image, before the width of the meter's 'bar' is
|
||||
calculated using the current value as a percentage of the bar's total width
|
||||
(114px).
|
||||
|
||||
The finished version of the update procedure is shown below:
|
||||
|
||||
```c
|
||||
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);
|
||||
}
|
||||
```
|
||||
|
||||
Lastly, as with the ``TickTimerService``, the ``BatteryStateHandler`` can be
|
||||
called manually in `init()` to display an inital value:
|
||||
|
||||
```c
|
||||
// Ensure battery level is displayed from the start
|
||||
battery_callback(battery_state_service_peek());
|
||||
```
|
||||
|
||||
Don't forget to free the memory used by the new battery meter:
|
||||
|
||||
```c
|
||||
layer_destroy(s_battery_layer);
|
||||
```
|
||||
|
||||
With this new feature in place, the watchface will now display the watch's
|
||||
battery charge level in a minimalist fashion that integrates well with the
|
||||
existing design style.
|
||||
|
||||

|
||||
|
||||
|
||||
## What's Next?
|
||||
|
||||
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/)
|
159
devsite/source/tutorials/watchface-tutorial/part5.md
Normal file
159
devsite/source/tutorials/watchface-tutorial/part5.md
Normal file
|
@ -0,0 +1,159 @@
|
|||
---
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
layout: tutorials/tutorial
|
||||
tutorial: watchface
|
||||
tutorial_part: 5
|
||||
|
||||
title: Vibrate on Disconnect
|
||||
description: |
|
||||
How to add bluetooth connection alerts to your watchface.
|
||||
permalink: /tutorials/watchface-tutorial/part5/
|
||||
generate_toc: true
|
||||
platform_choice: true
|
||||
---
|
||||
|
||||
The final popular watchface addition explored in this tutorial series
|
||||
is the concept of using the Bluetooth connection service to alert the user
|
||||
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.
|
||||
|
||||
In a similar manner to both the ``TickTimerService`` and
|
||||
``BatteryStateService``, the events associated with the Bluetooth connection are
|
||||
given to developers via subscriptions, which requires an additional callback -
|
||||
the ``ConnectionHandler``. Create one of these in the format given below:
|
||||
|
||||
```c
|
||||
static void bluetooth_callback(bool connected) {
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
The subscription to Bluetooth-related events is added in `init()`:
|
||||
|
||||
```c
|
||||
// Register for Bluetooth connection updates
|
||||
connection_service_subscribe((ConnectionHandlers) {
|
||||
.pebble_app_connection_handler = bluetooth_callback
|
||||
});
|
||||
```
|
||||
|
||||
The indicator itself will take the form of the following 'Bluetooth
|
||||
disconnected' icon that will be displayed when the watch is disconnected, and
|
||||
hidden when reconnected. Save the image below for use in this project:
|
||||
|
||||
<img style="background-color: #CCCCCC;" src="/assets/images/tutorials/intermediate/bt-icon.png"</img>
|
||||
|
||||
|
||||
{% platform cloudpebble %}
|
||||
Add this icon to your project by clicking 'Add New' under 'Resources' in
|
||||
the left hand side of the editor. Specify the 'Resource Type' as 'Bitmap Image',
|
||||
upload the file for the 'File' field. Give it an 'Identifier' such as
|
||||
`IMAGE_BT_ICON` before clicking 'Save'.
|
||||
{% endplatform %}
|
||||
|
||||
{% platform local %}
|
||||
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:
|
||||
|
||||
```js
|
||||
{
|
||||
"type": "bitmap",
|
||||
"name": "IMAGE_BT_ICON",
|
||||
"file": "bt-icon.png"
|
||||
},
|
||||
```
|
||||
{% endplatform %}
|
||||
|
||||
This icon will be loaded into the app as a ``GBitmap`` for display in a
|
||||
``BitmapLayer`` above the time display. Declare both of these as pointers at the
|
||||
top of the file, in addition to the existing variables of these types:
|
||||
|
||||
```c
|
||||
static BitmapLayer *s_background_layer, *s_bt_icon_layer;
|
||||
static GBitmap *s_background_bitmap, *s_bt_icon_bitmap;
|
||||
```
|
||||
|
||||
Allocate both of the new objects in `main_window_load()`, then set the
|
||||
``BitmapLayer``'s bitmap as the new icon ``GBitmap``:
|
||||
|
||||
```c
|
||||
// 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));
|
||||
```
|
||||
|
||||
As usual, ensure that the memory allocated to create these objects is also freed
|
||||
in `main_window_unload()`:
|
||||
|
||||
```c
|
||||
gbitmap_destroy(s_bt_icon_bitmap);
|
||||
bitmap_layer_destroy(s_bt_icon_layer);
|
||||
```
|
||||
|
||||
With the UI in place, the implementation of the ``BluetoothConnectionHandler``
|
||||
can be finished. Depending on the state of the connection when an event takes
|
||||
place, the indicator icon is hidden or unhidden as required. A distinct
|
||||
vibration is also triggered if the watch becomes disconnected, to differentiate
|
||||
the feedback from that of a notification or phone call:
|
||||
|
||||
```c
|
||||
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();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
```c
|
||||
// Show the correct state of the BT connection from the start
|
||||
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.
|
||||
|
||||

|
||||
|
||||
^CP^ You can create a new CloudPebble project from the completed project by
|
||||
[clicking here]({{ site.links.cloudpebble }}ide/gist/ddd15cbe8b0986fda407).
|
||||
|
||||
^LC^ You can see the finished project source code in
|
||||
[this GitHub Gist](https://gist.github.com/pebble-gists/ddd15cbe8b0986fda407).
|
||||
|
||||
|
||||
## What's Next?
|
||||
|
||||
Now that you've successfully built a feature rich watchface, it's time to
|
||||
[publish it](/guides/appstore-publishing/publishing-an-app/)!
|
Binary file not shown.
After Width: | Height: | Size: 33 KiB |
Loading…
Add table
Add a link
Reference in a new issue