Import the pebble dev site into devsite/

This commit is contained in:
Katharine Berry 2025-02-17 17:02:33 -08:00
parent 3b92768480
commit 527858cf4c
1359 changed files with 265431 additions and 0 deletions

View 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/
---

View 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:
![weather >{pebble-screenshot,pebble-screenshot--time-red}](/images/tutorials/advanced/weather.png)
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:
![action-completed >{pebble-screenshot,pebble-screenshot--time-red}](/images/tutorials/advanced/action-completed.gif)
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.
![weather-image >{pebble-screenshot,pebble-screenshot--time-red}](/images/tutorials/advanced/weather-image.png)
## 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:
![pdcs-example >{pebble-screenshot,pebble-screenshot--time-red}](/images/tutorials/advanced/pdcs-example.gif)
## 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!

View 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>

View 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/
---

View 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:
![rocky >{pebble-screenshot,pebble-screenshot--time-red}](/images/tutorials/js-watchface-tutorial/tictoc.png)
## 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.
![rocky >{pebble-screenshot,pebble-screenshot--time-red}](/images/tutorials/js-watchface-tutorial/rocky-time.png)
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 &rarr; >{wide,bg-dark-red,fg-white}](/tutorials/js-watchface-tutorial/part2/)

View 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.
![rocky >{pebble-screenshot,pebble-screenshot--time-red}](/images/tutorials/js-watchface-tutorial/tictoc-weather.png)
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 }}).

View 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/
---

View 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 &rarr; >{wide,bg-dark-red,fg-white}](/tutorials/watchface-tutorial/part2/)

View 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:
[![background](/images/getting-started/watchface-tutorial/background.png "background")]({{ 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 &rarr; >{wide,bg-dark-red,fg-white}](/tutorials/watchface-tutorial/part3/)

View 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 &rarr; >{wide,bg-dark-red,fg-white}](/tutorials/watchface-tutorial/part4/)

View 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.
![battery-level >{pebble-screenshot,pebble-screenshot--steel-black}](/images/tutorials/intermediate/battery-level.png)
## 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 &rarr; >{wide,bg-dark-red,fg-white}](/tutorials/watchface-tutorial/part5/)

View 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.
![bt >{pebble-screenshot,pebble-screenshot--steel-black}](/images/tutorials/intermediate/bt.png)
^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