mirror of
https://github.com/google/pebble.git
synced 2025-03-22 19:52:19 +00:00
374 lines
13 KiB
Markdown
374 lines
13 KiB
Markdown
|
---
|
||
|
# 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.
|