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

|
||||
|
||||
|
||||
## What's Next?
|
||||
|
||||
In the next, and final, section of this tutorial, we'll use the Connection Service
|
||||
to notify the user when their Pebble smartwatch disconnects from their phone.
|
||||
|
||||
[Go to Part 5 → >{wide,bg-dark-red,fg-white}](/tutorials/watchface-tutorial/part5/)
|
159
devsite/source/tutorials/watchface-tutorial/part5.md
Normal file
159
devsite/source/tutorials/watchface-tutorial/part5.md
Normal file
|
@ -0,0 +1,159 @@
|
|||
---
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
layout: tutorials/tutorial
|
||||
tutorial: watchface
|
||||
tutorial_part: 5
|
||||
|
||||
title: Vibrate on Disconnect
|
||||
description: |
|
||||
How to add bluetooth connection alerts to your watchface.
|
||||
permalink: /tutorials/watchface-tutorial/part5/
|
||||
generate_toc: true
|
||||
platform_choice: true
|
||||
---
|
||||
|
||||
The final popular watchface addition explored in this tutorial series
|
||||
is the concept of using the Bluetooth connection service to alert the user
|
||||
when their watch connects or disconnects. This can be useful to know when the
|
||||
watch is out of range and notifications will not be received, or to let the user
|
||||
know that they might have walked off somewhere without their phone.
|
||||
|
||||
This section continues from
|
||||
[*Part 4*](/tutorials/watchface-tutorial/part4), so be sure to
|
||||
re-use your code or start with that finished project.
|
||||
|
||||
In a similar manner to both the ``TickTimerService`` and
|
||||
``BatteryStateService``, the events associated with the Bluetooth connection are
|
||||
given to developers via subscriptions, which requires an additional callback -
|
||||
the ``ConnectionHandler``. Create one of these in the format given below:
|
||||
|
||||
```c
|
||||
static void bluetooth_callback(bool connected) {
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
The subscription to Bluetooth-related events is added in `init()`:
|
||||
|
||||
```c
|
||||
// Register for Bluetooth connection updates
|
||||
connection_service_subscribe((ConnectionHandlers) {
|
||||
.pebble_app_connection_handler = bluetooth_callback
|
||||
});
|
||||
```
|
||||
|
||||
The indicator itself will take the form of the following 'Bluetooth
|
||||
disconnected' icon that will be displayed when the watch is disconnected, and
|
||||
hidden when reconnected. Save the image below for use in this project:
|
||||
|
||||
<img style="background-color: #CCCCCC;" src="/assets/images/tutorials/intermediate/bt-icon.png"</img>
|
||||
|
||||
|
||||
{% platform cloudpebble %}
|
||||
Add this icon to your project by clicking 'Add New' under 'Resources' in
|
||||
the left hand side of the editor. Specify the 'Resource Type' as 'Bitmap Image',
|
||||
upload the file for the 'File' field. Give it an 'Identifier' such as
|
||||
`IMAGE_BT_ICON` before clicking 'Save'.
|
||||
{% endplatform %}
|
||||
|
||||
{% platform local %}
|
||||
Add this icon to your project by copying the above icon image to the `resources`
|
||||
project directory, and adding a new JSON object to the `media` array in
|
||||
`package.json` such as the following:
|
||||
|
||||
```js
|
||||
{
|
||||
"type": "bitmap",
|
||||
"name": "IMAGE_BT_ICON",
|
||||
"file": "bt-icon.png"
|
||||
},
|
||||
```
|
||||
{% endplatform %}
|
||||
|
||||
This icon will be loaded into the app as a ``GBitmap`` for display in a
|
||||
``BitmapLayer`` above the time display. Declare both of these as pointers at the
|
||||
top of the file, in addition to the existing variables of these types:
|
||||
|
||||
```c
|
||||
static BitmapLayer *s_background_layer, *s_bt_icon_layer;
|
||||
static GBitmap *s_background_bitmap, *s_bt_icon_bitmap;
|
||||
```
|
||||
|
||||
Allocate both of the new objects in `main_window_load()`, then set the
|
||||
``BitmapLayer``'s bitmap as the new icon ``GBitmap``:
|
||||
|
||||
```c
|
||||
// Create the Bluetooth icon GBitmap
|
||||
s_bt_icon_bitmap = gbitmap_create_with_resource(RESOURCE_ID_IMAGE_BT_ICON);
|
||||
|
||||
// Create the BitmapLayer to display the GBitmap
|
||||
s_bt_icon_layer = bitmap_layer_create(GRect(59, 12, 30, 30));
|
||||
bitmap_layer_set_bitmap(s_bt_icon_layer, s_bt_icon_bitmap);
|
||||
layer_add_child(window_get_root_layer(window), bitmap_layer_get_layer(s_bt_icon_layer));
|
||||
```
|
||||
|
||||
As usual, ensure that the memory allocated to create these objects is also freed
|
||||
in `main_window_unload()`:
|
||||
|
||||
```c
|
||||
gbitmap_destroy(s_bt_icon_bitmap);
|
||||
bitmap_layer_destroy(s_bt_icon_layer);
|
||||
```
|
||||
|
||||
With the UI in place, the implementation of the ``BluetoothConnectionHandler``
|
||||
can be finished. Depending on the state of the connection when an event takes
|
||||
place, the indicator icon is hidden or unhidden as required. A distinct
|
||||
vibration is also triggered if the watch becomes disconnected, to differentiate
|
||||
the feedback from that of a notification or phone call:
|
||||
|
||||
```c
|
||||
static void bluetooth_callback(bool connected) {
|
||||
// Show icon if disconnected
|
||||
layer_set_hidden(bitmap_layer_get_layer(s_bt_icon_layer), connected);
|
||||
|
||||
if(!connected) {
|
||||
// Issue a vibrating alert
|
||||
vibes_double_pulse();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Upon initialization, the app will display the icon unless a re-connection event
|
||||
occurs, and the current state is evaluated. Manually call the handler in
|
||||
`main_window_load()` to display the correct initial state:
|
||||
|
||||
```c
|
||||
// Show the correct state of the BT connection from the start
|
||||
bluetooth_callback(connection_service_peek_pebble_app_connection());
|
||||
```
|
||||
|
||||
With this last feature in place, running the app and disconnecting the Bluetooth
|
||||
connection will cause the new indicator to appear, and the watch to vibrate
|
||||
twice.
|
||||
|
||||

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