pebble/devsite/source/_guides/best-practices/building-for-every-pebble.md

374 lines
13 KiB
Markdown
Raw Permalink Normal View History

---
# 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.
title: Building for Every Pebble
description: How to write one app compatible with all Pebble smartwatches.
guide_group: best-practices
order: 0
---
The difference in capabilities between the various Pebble hardware platforms are
listed in
{% guide_link tools-and-resources/hardware-information %}. For example, the
Basalt, Chalk and Emery platforms support 64 colors, whereas the Aplite and
Diorite platforms only support two colors. This can make developing apps with
rich color layouts difficult when considering compatibility with other non-color
hardware. Another example is using platform specific APIs such as Health or
Dictation.
To make life simple for users, developers should strive to write one app that
can be used on all platforms. To help make this task simpler for developers, the
Pebble SDK provides numerous methods to accommodate different hardware
capabilities in code.
## Preprocessor Directives
It is possible to specify certain blocks of code to be compiled for specific
purposes by using the `#ifdef` preprocessor statement. For example, the
``Dictation`` API should be excluded on platforms with no microphone:
```c
#if defined(PBL_MICROPHONE)
// Start dictation UI
dictation_session_start(s_dictation_session);
#else
// Microphone is not available
text_layer_set_text(s_some_layer, "Dictation not available!");
#endif
```
When designing UI layouts, any use of colors on compatible platforms can be
adapted to either black or white on non-color platforms. The `PBL_COLOR` and
`PBL_BW` symbols will be defined at compile time when appropriate capabilities
are available:
```c
#if defined(PBL_COLOR)
text_layer_set_text_color(s_text_layer, GColorRed);
text_layer_set_background_color(s_text_layer, GColorChromeYellow);
#else
text_layer_set_text_color(s_text_layer, GColorWhite);
text_layer_set_background_color(s_text_layer, GColorBlack);
#endif
```
This is useful for blocks of multiple statements that change depending on the
availability of color support. For single statements, this can also be achieved
by using the ``PBL_IF_COLOR_ELSE()`` macro.
```c
window_set_background_color(s_main_window, PBL_IF_COLOR_ELSE(GColorJaegerGreen, GColorBlack));
```
See below for a complete list of defines and macros available.
## Available Defines and Macros
The tables below show a complete summary of all the defines and associated
macros available to conditionally compile or omit feature-dependant code. The
macros are well-suited for individual value selection, whereas the defines are
better used to select an entire block of code.
| Define | MACRO |Available |
|--------|-------|----------|
| `PBL_BW` | `PBL_IF_BW_ELSE()` | Running on hardware that supports only black and white. |
| `PBL_COLOR` | `PBL_IF_COLOR_ELSE()` | Running on hardware that supports 64 colors. |
| `PBL_MICROPHONE` | `PBL_IF_MICROPHONE_ELSE()` | Running on hardware that includes a microphone. |
| `PBL_COMPASS` | None | Running on hardware that includes a compass. |
| `PBL_SMARTSTRAP` | `PBL_IF_SMARTSTRAP_ELSE()` | Running on hardware that includes a smartstrap connector, but does not indicate that the connector is capable of supplying power. |
| `PBL_SMARTSTRAP_POWER` | None | Running on hardware that includes a smartstrap connector capable of supplying power. |
| `PBL_HEALTH` | `PBL_IF_HEALTH_ELSE()` | Running on hardware that supports Pebble Health and the `HealthService` API. |
| `PBL_RECT` | `PBL_IF_RECT_ELSE()` | Running on hardware with a rectangular display. |
| `PBL_ROUND` | `PBL_IF_ROUND_ELSE()` | Running on hardware with a round display. |
| `PBL_DISPLAY_WIDTH` | None | Determine the screen width in pixels. |
| `PBL_DISPLAY_HEIGHT` | None | Determine the screen height in pixels. |
| `PBL_PLATFORM_APLITE` | None | Built for Pebble/Pebble Steel. |
| `PBL_PLATFORM_BASALT` | None | Built for Pebble Time/Pebble Time Steel. |
| `PBL_PLATFORM_CHALK` | None | Built for Pebble Time Round. |
| `PBL_PLATFORM_DIORITE` | None | Built for Pebble 2. |
| `PBL_PLATFORM_EMERY` | None | Built for Pebble Time 2. |
| `PBL_SDK_2` | None | Compiling with SDK 2.x (deprecated). |
| `PBL_SDK_3` | None | Compiling with SDK 3.x. or 4.x. |
> Note: It is strongly recommended to conditionally compile code using
> applicable feature defines instead of `PBL_PLATFORM` defines to be as specific
> as possible.
## API Detection
In addition to platform and capabilities detection, we now provide API
detection to detect if a specific API method is available. This approach could
be considered future-proof, since platforms and capabilities may come and go.
Let's take a look at a simple example:
```c
#if PBL_API_EXISTS(health_service_peek_current_value)
// Do something if specific Health API exists
#endif
```
## Avoid Hardcoded Layout Values
With the multiple display shapes and resolutions available, developers should
try and avoid hardcoding layout values. Consider the example
below:
```c
static void window_load(Window *window) {
// Create a full-screen Layer - BAD
s_some_layer = layer_create(GRect(0, 0, 144, 168));
}
```
The hardcoded width and height of this layer will cover the entire screen on
Aplite, Basalt and Diorite, but not on Chalk or Emery. This kind of screen
size-dependant calculation should use the ``UnobstructedArea`` bounds of the
``Window`` itself:
```c
static void window_load(Window *window) {
// Get the unobstructed bounds of the Window
Layer window_layer = window_get_root_layer(window);
GRect window_bounds = layer_get_unobstructed_bounds(window_layer);
// Properly create a full-screen Layer - GOOD
s_some_layer = layer_create(window_bounds);
}
```
Another common use of this sort of construction is to make a ``Layer`` that is
half the unobstructed screen height. This can also be correctly achieved using
the ``Window`` unobstructed bounds:
```c
GRect layer_bounds = window_bounds;
layer_bounds.size.h /= 2;
// Create a Layer that is half the screen height
s_some_layer = layer_create(layer_bounds);
```
This approach is also advantageous in simplifying updating an app for a future
new screen size, as proportional layout values will adapt as appropriate when
the ``Window`` unobstructed bounds change.
## Screen Sizes
To ease the introduction of the Emery platform, the Pebble SDK introduced new
compiler directives to allow developers to determine the screen width and
height. This is preferable to using platform detection, since multiple platforms
share the same screen width and height.
```c
#if PBL_DISPLAY_HEIGHT == 228
uint8_t offset_y = 100;
#elif PBL_DISPLAY_HEIGHT == 180
uint8_t offset_y = 80;
#else
uint8_t offset_y = 60;
#endif
```
> Note: Although this method is preferable to platform detection, it is better
to dynamically calculate the display width and height based on the unobstructed
bounds of the root layer.
## Pebble C WatchInfo
The ``WatchInfo`` API can be used to determine exactly which Pebble model and
color an app is running on. Apps can use this information to dynamically
modify their layout or behavior depending on which Pebble the user is wearing.
For example, the display on Pebble Steel is located at a different vertical
position relative to the buttons than on Pebble Time. Any on-screen button hints
can be adjusted to compensate for this using ``WatchInfoModel``.
```c
static void window_load(Window *window) {
Layer window_layer = window_get_root_layer(window);
GRect window_bounds = layer_get_bounds(window_layer);
int button_height, y_offset;
// Conditionally set layout parameters
switch(watch_info_get_model()) {
case WATCH_INFO_MODEL_PEBBLE_STEEL:
y_offset = 64;
button_height = 44;
break;
case WATCH_INFO_MODEL_PEBBLE_TIME:
y_offset = 58;
button_height = 56;
break;
/* Other cases */
default:
y_offset = 0;
button_height = 0;
break;
}
// Set the Layer frame
GRect layer_frame = GRect(0, y_offset, window_bounds.size.w, button_height);
// Create the Layer
s_label_layer = text_layer_create(layer_frame);
layer_add_child(window_layer, text_layer_get_layer(s_label_layer));
/* Other UI code */
}
```
Developers can also use ``WatchInfoColor`` values to theme an app for each
available color of Pebble.
```c
static void window_load(Window *window) {
GColor text_color, background_color;
// Choose different theme colors per watch color
switch(watch_info_get_color()) {
case WATCH_INFO_COLOR_RED:
// Red theme
text_color = GColorWhite;
background_color = GColorRed;
break;
case WATCH_INFO_COLOR_BLUE:
// Blue theme
text_color = GColorBlack;
background_color = GColorVeryLightBlue;
break;
/* Other cases */
default:
text_color = GColorBlack;
background_color = GColorWhite;
break;
}
// Use the conditionally set value
text_layer_set_text_color(s_label_layer, text_color);
text_layer_set_background_color(s_label_layer, background_color);
/* Other UI code */
}
```
## PebbleKit JS Watch Info
Similar to [*Pebble C WatchInfo*](#pebble-c-watchinfo) above, the PebbleKit JS
``Pebble.getActiveWatchInfo()`` method allows developers to determine
which model and color of Pebble the user is wearing, as well as the firmware
version running on it. For example, to obtain the model of the watch:
> Note: See the section below to avoid problem using this function on older app
> version.
```js
// Get the watch info
var info = Pebble.getActiveWatchInfo();
console.log('Pebble model: ' + info.model);
```
## Detecting Platform-specific JS Features
A number of features in PebbleKit JS (such as ``Pebble.timelineSubscribe()`` and
``Pebble.getActiveWatchInfo()``) exist on SDK 3.x. If an app tries to use any of
these on an older Pebble mobile app version where they are not available, the JS
app will crash.
To prevent this, be sure to check for the availability of the function before
calling it. For example, in the case of ``Pebble.getActiveWatchInfo()``:
```js
if (Pebble.getActiveWatchInfo) {
// Available.
var info = Pebble.getActiveWatchInfo();
console.log('Pebble model: ' + info.model);
} else {
// Gracefully handle no info available
}
```
## Platform-specific Resources
With the availability of color support on Basalt, Chalk and Emery, developers
may wish to include color versions of resources that had previously been
pre-processed for Pebble's black and white display. Including both versions of
the resource is expensive from a resource storage perspective, and lays the
burden of packing redundant color resources in an Aplite or Diorite app when
built for multiple platforms.
To solve this problem, the Pebble SDK allows developers to specify which version
of an image resource is to be used for each display type, using `~bw` or
`~color` appended to a file name. Resources can also be bundled only with
specific platforms using the `targetPlatforms` property for each resource.
For more details about packaging resources specific to each platform, as well as
more tags available similar to `~color`, read
{% guide_link app-resources/platform-specific %}.
## Multiple Display Shapes
With the introduction of the Chalk platform, a new round display type is
available with increased pixel resolution. To distinguish between the two
possible shapes of display, developers can use defines to conditionally
include code segments:
```c
#if defined(PBL_RECT)
printf("This is a rectangular display!");
#elif defined(PBL_ROUND)
printf("This is a round display!");
#endif
```
Another approach to this conditional compilation is to use the
``PBL_IF_RECT_ELSE()`` and ``PBL_IF_ROUND_ELSE()`` macros, allowing values to be
inserted into expressions that might otherwise require a set of `#define`
statements similar to the previous example. This would result in needless
verbosity of four extra lines of code when only one is actually needed. These
are used in the following manner:
```c
// Conditionally print out the shape of the display
printf("This is a %s display!", PBL_IF_RECT_ELSE("rectangular", "round"));
```
This mechanism is best used with window bounds-derived layout size and position
value. See the [*Avoid Hardcoded Layout Values*](#avoid-hardcoded-layout-values)
section above for more information. Making good use of the builtin ``Layer``
types will also help safeguard apps against display shape and size changes.
Another thing to consider is rendering text on a round display. Due to the
rounded corners, each horizontal line of text will have a different available
width, depending on its vertical position.