# Pebble Developer Documentation: /guides/ SDK 4.33.1. 109 pages. Index: https://developer.repebble.com/llms.txt # Animated Images Source: https://developer.repebble.com/guides/app-resources/animated-images/ The Pebble SDK allows animated images to be played inside an app using the [`GBitmapSequence`](https://developer.repebble.com/docs/c/Graphics/Graphics_Types.md#GBitmapSequence "GBitmapSequence") API, which takes [APNG](https://en.wikipedia.org/wiki/APNG) images as input files. APNG files are similar to well-known `.gif` files, which are not supported directly but can be converted to APNG. A similar effect can be achieved with multiple image resources, a [`BitmapLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/BitmapLayer.md "BitmapLayer") and an [`AppTimer`](https://developer.repebble.com/docs/c/Foundation/Timer.md#AppTimer "AppTimer"), but would require a lot more code. The [`GBitmapSequence`](https://developer.repebble.com/docs/c/Graphics/Graphics_Types.md#GBitmapSequence "GBitmapSequence") API handles the reading, decompression, and frame duration/count automatically. ## Converting GIF to APNG A `.gif` file can be converted to the APNG `.png` format with [gif2apng](http://gif2apng.sourceforge.net/) and the `-z0` flag: ``` ./gif2apng -z0 animation.gif ``` > Note: The file extension must be `.png`, **not** `.apng`. ## Adding an APNG Include the APNG file in the `resources` array in `package.json` as a `raw` resource: ``` "resources": {"media": [{"type":"raw","name":"ANIMATION","file":"images/animation.png"}]} ``` ## Displaying APNG Frames The [`GBitmapSequence`](https://developer.repebble.com/docs/c/Graphics/Graphics_Types.md#GBitmapSequence "GBitmapSequence") will use a [`GBitmap`](https://developer.repebble.com/docs/c/Graphics/Graphics_Types.md#GBitmap "GBitmap") as a container and update its contents each time a new frame is read from the APNG file. This means that the first step is to create a blank [`GBitmap`](https://developer.repebble.com/docs/c/Graphics/Graphics_Types.md#GBitmap "GBitmap") to be this container. Declare file-scope variables to hold the data: ``` static GBitmapSequence \*s\_sequence;static GBitmap \*s\_bitmap; ``` Load the APNG from resources into the [`GBitmapSequence`](https://developer.repebble.com/docs/c/Graphics/Graphics_Types.md#GBitmapSequence "GBitmapSequence") variable, and use the frame size to create the blank [`GBitmap`](https://developer.repebble.com/docs/c/Graphics/Graphics_Types.md#GBitmap "GBitmap") frame container: ``` // Create sequences\_sequence = gbitmap\_sequence\_create\_with\_resource(RESOURCE\_ID\_ANIMATION);// Create blank GBitmap using APNG frame sizeGSize frame\_size = gbitmap\_sequence\_get\_bitmap\_size(s\_sequence);s\_bitmap = gbitmap\_create\_blank(frame\_size, GBitmapFormat8Bit); ``` Once the app is ready to begin playing the animated image, advance each frame using an [`AppTimer`](https://developer.repebble.com/docs/c/Foundation/Timer.md#AppTimer "AppTimer") until the end of the sequence is reached. Loading the next APNG frame is handled for you and written to the container [`GBitmap`](https://developer.repebble.com/docs/c/Graphics/Graphics_Types.md#GBitmap "GBitmap"). Declare a [`BitmapLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/BitmapLayer.md "BitmapLayer") variable to display the current frame, and set it up as described under [_Displaying An Image_](https://developer.repebble.com/guides/app-resources/images.md#displaying-an-image). ``` static BitmapLayer \*s\_bitmap\_layer; ``` Create the callback to be used when the [`AppTimer`](https://developer.repebble.com/docs/c/Foundation/Timer.md#AppTimer "AppTimer") has elapsed, and the next frame should be displayed. This will occur in a loop until there are no more frames, and [`gbitmap_sequence_update_bitmap_next_frame()`](https://developer.repebble.com/docs/c/Graphics/Graphics_Types.md#gbitmap_sequence_update_bitmap_next_frame "gbitmap\_sequence\_update\_bitmap\_next\_frame") returns `false`: ``` static void timer\_handler(void \*context) {uint32\_t next\_delay;// Advance to the next APNG frame, and get the delay for this frameif(gbitmap\_sequence\_update\_bitmap\_next\_frame(s\_sequence, s\_bitmap, &next\_delay)) {// Set the new frame into the BitmapLayerbitmap\_layer\_set\_bitmap(s\_bitmap\_layer, s\_bitmap);layer\_mark\_dirty(bitmap\_layer\_get\_layer(s\_bitmap\_layer));// Timer for that frame's delayapp\_timer\_register(next\_delay, timer\_handler, NULL);}} ``` When appropriate, schedule the first frame advance with an [`AppTimer`](https://developer.repebble.com/docs/c/Foundation/Timer.md#AppTimer "AppTimer"): ``` uint32\_t first\_delay\_ms = 10;// Schedule a timer to advance the first frameapp\_timer\_register(first\_delay\_ms, timer\_handler, NULL); ``` When the app exits or the resource is no longer required, destroy the [`GBitmapSequence`](https://developer.repebble.com/docs/c/Graphics/Graphics_Types.md#GBitmapSequence "GBitmapSequence") and the container [`GBitmap`](https://developer.repebble.com/docs/c/Graphics/Graphics_Types.md#GBitmap "GBitmap"): ``` gbitmap\_sequence\_destroy(s\_sequence);gbitmap\_destroy(s\_bitmap); ``` --- # App Assets Source: https://developer.repebble.com/guides/app-resources/app-assets/ This guide contains some resources available for developers to use in their apps to improve consistency, as well as for convenience. For example, most [`ActionBarLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/ActionBarLayer.md "ActionBarLayer") implementations will require at least one of the common icons given below. ## Example PDC icon SVG Files Many of the system PDC animations are available for use in watchfaces and watchapps as part of the [`pdc-sequence`](https://github.com/pebble-examples/pdc-sequence/tree/master/resources) example project. ## Example Action Bar Icons There is a set of example icons for developers to use for common actions. Each icon is shown below as a preview, along with a short description about its suggested usage. | Preview | Description | | --- | --- | | ![](https://developer.repebble.com/assets/images/guides/design-and-interaction/icons/action_bar_icon_check.png) | Check mark for confirmation actions. | | ![](https://developer.repebble.com/assets/images/guides/design-and-interaction/icons/action_bar_icon_dismiss.png) | Cross mark for dismiss, cancel, or decline actions. | | ![](https://developer.repebble.com/assets/images/guides/design-and-interaction/icons/action_bar_icon_up.png) | Up arrow for navigating or scrolling upwards. | | ![](https://developer.repebble.com/assets/images/guides/design-and-interaction/icons/action_bar_icon_down.png) | Down arrow for navigating or scrolling downwards. | | ![](https://developer.repebble.com/assets/images/guides/design-and-interaction/icons/action_bar_icon_edit.png) | Pencil icon for edit actions. | | ![](https://developer.repebble.com/assets/images/guides/design-and-interaction/icons/action_bar_icon_delete.png) | Trash can icon for delete actions. | | ![](https://developer.repebble.com/assets/images/guides/design-and-interaction/icons/action_bar_icon_snooze.png) | Stylized 'zzz' for snooze actions. | | ![](https://developer.repebble.com/assets/images/guides/design-and-interaction/icons/music_icon_ellipsis.png) | Ellipsis to suggest further information or actions are available. | | ![](https://developer.repebble.com/assets/images/guides/design-and-interaction/icons/music_icon_play.png) | Common icon for play actions. | | ![](https://developer.repebble.com/assets/images/guides/design-and-interaction/icons/music_icon_pause.png) | Common icon for pause actions. | | ![](https://developer.repebble.com/assets/images/guides/design-and-interaction/icons/music_icon_skip_forward.png) | Common icon for skip forward actions. | | ![](https://developer.repebble.com/assets/images/guides/design-and-interaction/icons/music_icon_skip_backward.png) | Common icon for skip backward actions. | | ![](https://developer.repebble.com/assets/images/guides/design-and-interaction/icons/music_icon_volume_up.png) | Common icon for raising volume. | | ![](https://developer.repebble.com/assets/images/guides/design-and-interaction/icons/music_icon_volume_down.png) | Common icon for lowering volume. | --- # App Resources Source: https://developer.repebble.com/guides/app-resources/ The Pebble SDK allows apps to include extra files as app resources. These files can include images, animated images, vector images, custom fonts, and raw data files. These resources are stored in flash memory and loaded when required by the SDK. Apps that use a large number of resources should consider only keeping in memory those that are immediately required. **Notice** The maximum number of resources an app can include is **256**. In addition, the maximum size of all resources bundled into a built app is **128 kB** on the Aplite platform, and **256 kB** on the Basalt and Chalk platforms. These limits include resources used by included Pebble Packages. App resources are included in a project by being listed in the `media` property of `package.json`, and are converted into suitable firmware-compatible formats at build time. Examples of this are shown in each type of resource's respective guide. ## Contents - [**Animated Images**](https://developer.repebble.com/guides/app-resources/animated-images.md) - How to add animated image resources to a project in the APNG format, and display them in your app. - [**App Assets**](https://developer.repebble.com/guides/app-resources/app-assets.md) - A collection of assets for use as resources in Pebble apps. - [**Converting SVG to PDC**](https://developer.repebble.com/guides/app-resources/converting-svg-to-pdc.md) - How to create compatible SVG files using Inkscape and Illustrator. - [**Fonts**](https://developer.repebble.com/guides/app-resources/fonts.md) - How to use built-in system fonts, or add your own font resources to a project. - [**Images**](https://developer.repebble.com/guides/app-resources/images.md) - How to add image resources to a project and display them in your app. - [**Pebble Draw Command File Format**](https://developer.repebble.com/guides/app-resources/pdc-format.md) - The binary file format description for Pebble Draw Command Frames, Images and Sequences. - [**Platform-specific Resources**](https://developer.repebble.com/guides/app-resources/platform-specific.md) - How to include different resources for different platforms, as well as how to include a resource only on a particular platform. - [**Raw Data Files**](https://developer.repebble.com/guides/app-resources/raw-data-files.md) - How to add raw data resources to a project and read them in your app. - [**System Fonts**](https://developer.repebble.com/guides/app-resources/system-fonts.md) - A complete list of all the system fonts available for use in Pebble projects. --- # Converting SVG to PDC Source: https://developer.repebble.com/guides/app-resources/converting-svg-to-pdc/ [Pebble Draw Commands](https://developer.repebble.com/docs/c/Graphics/Draw_Commands.md "Draw Commands") (PDC) are a powerful method of creating vector images and icons that can be transformed and manipulated at runtime. These can be used as a low-cost alternative to APNGs or bitmap sequences. Currently the only simple way to create PDC files is to use the [`svg2pdc.py`](https://github.com/pebble-examples/cards-example/blob/master/tools/svg2pdc.py) tool. However, as noted in [_Vector Animations_](https://developer.repebble.com/tutorials/advanced/vector-animations.md#creating-compatible-files) there are a some limitations to the nature of the input SVG file: > The `svg2pdc` tool currently supports SVG files that use **only** the following elements: `g`, `layer`, `path`, `rect`, `polyline`, `polygon`, `line`, `circle`. Fortunately, steps can be taken when creating SVG files in popular graphics packages to avoid these limitations and ensure the output file is compatible with `svg2pdc.py`. In this guide, we will be creating compatible PDC files using an example SVG - this [pencil icon](https://upload.wikimedia.org/wikipedia/commons/a/ac/Black_pencil.svg). ![pencil icon](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/pencil.svg) ## Using Inkscape - First, open the SVG in [Inkscape](https://inkscape.org/en/): ![inkscape-open](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/inkscape-open.png) - Resize the viewport with _File_, _Document Properties_, _Page_, _Resize Page to Drawing_: ![inkscape-resize-page](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/inkscape-resize-page.png) - Select the layer, then resize the image to fit Pebble (50 x 50 pixels in this example) with _Object_, _Transform_: ![inkscape-resize-pebble](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/inkscape-resize-pebble.png) - Now that the image has been resized, shrink the viewport again with _File_, _Document Properties_, _Page_, _Resize Page to Drawing_: - Remove groupings with _Edit_, _Select All_, then _Object_, _Ungroup_ until no groups remain: ![inkscape-ungroup](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/inkscape-ungroup.png) - Disable relative move in _Object_, _Transform_. Hit _Apply_: ![inkscape-relative](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/inkscape-relative.png) - Finally, save the image as a 'Plain SVG': ![inkscape-plain](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/inkscape-plain.png) ## Using Illustrator - First, open the SVG in Illustrator: ![illustrator-open](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/illustrator-open.png) - Resize the image to fit Pebble (50 x 50 pixels in this example) by entering in the desired values in the 'W' and 'H' fields of the _Transform_ panel: ![illustrator-resize](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/illustrator-resize.png) - Ungroup all items with _Select_, _All_, followed by _Object_, _Ungroup_ until no groups remain: ![illustrator-ungroup](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/illustrator-ungroup.png) - Shrink the image bounds with _Object_, _Artboards_, _Fit to Selected Art_: ![illustrator-fit](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/illustrator-fit.png) - Save the SVG using _File_, _Save As_ with the _SVG Tiny 1.1_ profile and 1 decimal places: ![illustrator-settings](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/illustrator-settings.png) ## Using the PDC Files Once the compatible SVG files have been created, it's time to use `svg2pdc.py` to convert into PDC resources, which will contain all the vector information needed to draw the image in the correct Pebble binary format. The command is shown below, with the Inkscape output SVG used as an example: ``` $ python svg2pdc.py pencil-inkscape.svg# Use python 2.x! ``` > If a coordinate value's precision value isn't supported, a warning will be printed and the nearest compatible value will be used instead: > > ``` > Invalid point: (9.4, 44.5). Closest supported coordinate: (9.5, 44.5) > ``` To use the PDC file in a Pebble project, read [_Drawing a PDC Image_](https://developer.repebble.com/tutorials/advanced/vector-animations.md#drawing-a-pdc-image). The result should look near-identical on Pebble: ![svg-output](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/svg-output.png) ## Example Output For reference, compatible output files are listed below: - Inkscape: [SVG](https://developer.repebble.com/assets/other/pdc/pencil-inkscape.svg) | [PDC](https://developer.repebble.com/assets/other/pdc/pencil-inkscape.pdc) - Illustrator: [SVG](https://developer.repebble.com/assets/other/pdc/pencil-illustrator.svg) | [PDC](https://developer.repebble.com/assets/other/pdc/pencil-illustrator.pdc) --- # Fonts Source: https://developer.repebble.com/guides/app-resources/fonts/ ## Using Fonts Text drawn in a Pebble app can be drawn using a variety of built-in fonts or a custom font specified as a project resource. Custom font resources must be in the `.ttf` (TrueType font) format. When the app is built, the font file is processed by the SDK according to the `compatibility` (See [_Font Compatibility_](#font-compatibility)) and `characterRegex` fields (see [_Choosing Font Characters_](#choosing-font-characters)), the latter of which is a standard Python regex describing the character set of the resulting font. ## System Fonts All of the built-in system fonts are available to use with [`fonts_get_system_font()`](https://developer.repebble.com/docs/c/Graphics/Fonts.md#fonts_get_system_font "fonts\_get\_system\_font"). See [_System Fonts_](https://developer.repebble.com/guides/app-resources/system-fonts.md) for a complete list with sample images. Examples of using a built-in system font in code are [shown below](#using-a-system-font). ### Limitations There are limitations to the Bitham, Roboto, Droid and LECO fonts, owing to the memory space available on Pebble, which only contain a subset of the default character set. - Roboto 49 Bold Subset - contains digits and a colon. - Bitham 34/42 Medium Numbers - contain digits and a colon. - Bitham 18/34 Light Subset - only contains a few characters and is not suitable for displaying general text. - LECO Number sets - suitable for number-only usage. ## Using a System Font Using a system font is the easiest choice when displaying simple text. For more advanced cases, a custom font may be advantageous. A system font can be obtained at any time, and the developer is not responsible for destroying it when they are done with it. Fonts can be used in two modes: ``` // Use a system font in a TextLayertext\_layer\_set\_font(s\_text\_layer, fonts\_get\_system\_font(FONT\_KEY\_GOTHIC\_24)); ``` ``` // Use a system font when drawing text manuallygraphics\_draw\_text(ctx, text, fonts\_get\_system\_font(FONT\_KEY\_GOTHIC\_24), bounds,GTextOverflowModeWordWrap, GTextAlignmentCenter, NULL); ``` ## Adding a Custom Font After placing the font file in the project's `resources` directory, the custom font can be added to a project as `font` `type` item in the `media` array in `package.json`. The `name` field's contents will be made available at compile time with `RESOURCE_ID_` at the front, and must end with the desired font size. For example: ``` "resources": {"media": [{"type": "font","name": "EXAMPLE\_FONT\_20","file": "example\_font.ttf"}]} ``` **Important** The maximum recommended font size is 48. ## Using a Custom Font Unlike a system font, a custom font must be loaded and unloaded by the developer. Once this has been done, the font can easily be used in a similar manner. When the app initializes, load the font from resources using the generated `RESOURCE_ID`: ``` // Declare a file-scope variablestatic GFont s\_font; ``` ``` // Load the custom fonts\_font = fonts\_load\_custom\_font(resource\_get\_handle(RESOURCE\_ID\_EXAMPLE\_FONT\_20)); ``` The font can now be used in two modes - with a [`TextLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/TextLayer.md "TextLayer"), or when drawing text manually in a [`LayerUpdateProc`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#LayerUpdateProc "LayerUpdateProc"): ``` // Use a custom font in a TextLayertext\_layer\_set\_font(s\_text\_layer, s\_font); ``` ``` // Use a custom font when drawing text manuallygraphics\_draw\_text(ctx, text, s\_font, bounds, GTextOverflowModeWordWrap,GTextAlignmentCenter, NULL); ``` ## Font Compatibility The font rendering process was improved in SDK 2.8. However, in some cases this may cause the appearance of custom fonts to change slightly. To revert to the old rendering process, add `"compatibility": "2.7"` to your font's object in the `media` array (shown above) in `package.json`. ## Choosing Font Characters By default, the maximum number of supported characters is generated for a font resource. In most cases this will be far too many, and can bloat the size of the app. To optimize the size of your font resources you can use a standard regular expression (or 'regex') string to limit the number of characters to only those you require. The table below outlines some example regular expressions to use for limiting font character sets in common watchapp scenarios: | Expression | Result | | --- | --- | | `[-~]` | ASCII characters only. | | `[0-9]` | Numbers only. | | `[0-9]` | Numbers and spaces only. | | `[a-zA-Z]` | Letters only. | | `[a-zA-Z]` | Letters and spaces only. | | `[0-9:APM]` | Time strings only (e.g.: "12:45 AM"). | | `[0-9:A-Za-z]` | Time and date strings (e.g.: "12:43 AM Wednesday 3rd March 2015". | | `[0-9:A-Za-z°]` | Time, date, and degree symbol for temperature gauges. | | `[0-9°CF]` | Numbers and degree symbol with 'C' and 'F' for temperature gauges. | Add the `characterRegex` key to any font objects in `package.json`'s `media` array. ``` "media": [{"characterRegex": "[:0-9]","type": "font","name": "EXAMPLE\_FONT","file": "example\_font.ttf"}] ``` Check out [regular-expressions.info](http://www.regular-expressions.info/tutorial.html) to learn more about how to use regular expressions. --- # Images Source: https://developer.repebble.com/guides/app-resources/images/ Images can be displayed in a Pebble app by adding them as a project resource. They are stored in memory as a [`GBitmap`](https://developer.repebble.com/docs/c/Graphics/Graphics_Types.md#GBitmap "GBitmap") while the app is running, and can be displayed either in a [`BitmapLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/BitmapLayer.md "BitmapLayer") or by using [`graphics_draw_bitmap_in_rect()`](https://developer.repebble.com/docs/c/Graphics/Drawing_Primitives.md#graphics_draw_bitmap_in_rect "graphics\_draw\_bitmap\_in\_rect"). ## Creating an Image In order to be compatible with Pebble, the image should be saved as a PNG file, ideally in a palettized format (see below for palette files) with the appropriate number of colors. The number of colors available on each platform is shown below: | Platform | Number of Colors | | --- | --- | | Aplite | 2 (black and white) | | Basalt | 64 colors | | Chalk | 64 colors | | Diorite | 2 (black and white) | | Emery | 64 colors | | Flint | 2 (black and white) | ## Color Palettes Palette files for popular graphics packages that contain the 64 supported colors are available below. Use these when creating color image resources: - [Photoshop `.act`](https://developer.repebble.com/assets/other/pebble_colors_64.act) - [Aseprite (raw colors, for watch displays) `.aseprite`](https://developer.repebble.com/assets/other/pebble_colors_uncorrected.aseprite) - [Aseprite (Sunlight, color-corrected for HD displays) `.aseprite`](https://developer.repebble.com/assets/other/pebble_colors_sunlight.aseprite) - [Illustrator `.ai`](https://developer.repebble.com/assets/other/pebble_colors_64.ai) - [GIMP `.pal`](https://developer.repebble.com/assets/other/pebble_colors_64.pal) - [ImageMagick `.gif`](https://developer.repebble.com/assets/other/pebble_colors_64.gif) ## Import the Image After placing the image in the project's `resources` directory, add an entry to the `resources` item in `package.json`. Specify the `type` as `bitmap`, choose a `name` (to be used in code) and supply the path relative to the project's `resources` directory. Below is an example: ``` "resources": {"media": [{"type": "bitmap","name": "EXAMPLE\_IMAGE","file": "background.png"}]}, ``` ## Specifying an Image Resource Image resources are used in a Pebble project when they are listed using the `bitmap` resource type. Resources of this type can be optimized using additional attributes: | Attribute | Description | Values | | --- | --- | --- | | `memoryFormat` | Optional. Determines the bitmap type. Reflects values in the `GBitmapFormat` `enum`. | `Smallest`, `SmallestPalette`, `1Bit`, `8Bit`, `1BitPalette`, `2BitPalette`, or `4BitPalette`. | | `storageFormat` | Optional. Determines the file format used for storage. Using `spaceOptimization` instead is preferred. | `pbi` or `png`. | | `spaceOptimization` | Optional. Determines whether the output resource is optimized for low runtime memory or low resource space usage. | `storage` or `memory`. | An example usage of these attributes in `package.json` is shown below: ``` {"type": "bitmap","name": "IMAGE\_EXAMPLE","file": "images/example\_image.png""memoryFormat": "Smallest","spaceOptimization": "memory"} ``` On all platforms `memoryFormat` will default to `Smallest`. On Aplite `spaceOptimization` will default to `memory`, and `storage` on all other platforms. > If you specify a combination of attributes that is not supported, such as a `1Bit` unpalettized PNG, the build will fail. Palettized 1-bit PNGs are supported. When compared to using image resources in previous SDK versions: - `png` is equivalent to `bitmap` with no additional specifiers. - `pbi` is equivalent to `bitmap` with `"memoryFormat": "1Bit"`. - `pbi8` is equivalent to `bitmap` with `"memoryFormat": "8Bit"` and `"storageFormat": "pbi"`. Continuing to use the `png` resource type will result in a `bitmap` resource with `"storageFormat": "png"`, which is not optimized for memory usage on the Aplite platform due to less memory available in total, and is not encouraged. ## Specifying Resources Per Platform To save resource space, it is possible to include only certain image resources when building an app for specific platforms. For example, this is useful for the Aplite platform, which requires only black and white versions of images, which can be significantly smaller in size. Resources can also be selected according to platform and display shape. Read [_Platform-specific Resources_](https://developer.repebble.com/guides/app-resources/platform-specific.md) to learn more about how to do this. ## Displaying an Image Declare a [`GBitmap`](https://developer.repebble.com/docs/c/Graphics/Graphics_Types.md#GBitmap "GBitmap") pointer. This will be the object type the image data is stored in while the app is running: ``` static GBitmap \*s\_bitmap; ``` Create the [`GBitmap`](https://developer.repebble.com/docs/c/Graphics/Graphics_Types.md#GBitmap "GBitmap"), specifying the `name` chosen earlier, prefixed with `RESOURCE_ID_`. This will manage the image data: ``` s\_bitmap = gbitmap\_create\_with\_resource(RESOURCE\_ID\_EXAMPLE\_IMAGE); ``` Declare a [`BitmapLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/BitmapLayer.md "BitmapLayer") pointer: ``` static BitmapLayer \*s\_bitmap\_layer; ``` Create the [`BitmapLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/BitmapLayer.md "BitmapLayer") and set it to show the [`GBitmap`](https://developer.repebble.com/docs/c/Graphics/Graphics_Types.md#GBitmap "GBitmap"). Make sure to supply the correct width and height of your image in the [`GRect`](https://developer.repebble.com/docs/c/Graphics/Graphics_Types.md#GRect "GRect"), as well as using [`GCompOpSet`](https://developer.repebble.com/docs/c/Graphics/Graphics_Types.md#GCompOpSet "GCompOpSet") to ensure color transparency is correctly applied: ``` s\_bitmap\_layer = bitmap\_layer\_create(GRect(5, 5, 48, 48));bitmap\_layer\_set\_compositing\_mode(s\_bitmap\_layer, GCompOpSet);bitmap\_layer\_set\_bitmap(s\_bitmap\_layer, s\_bitmap); ``` Add the [`BitmapLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/BitmapLayer.md "BitmapLayer") as a child layer to the [`Window`](https://developer.repebble.com/docs/c/User_Interface/Window.md "Window"): ``` layer\_add\_child(window\_get\_root\_layer(window), bitmap\_layer\_get\_layer(s\_bitmap\_layer)); ``` Destroy both the [`GBitmap`](https://developer.repebble.com/docs/c/Graphics/Graphics_Types.md#GBitmap "GBitmap") and [`BitmapLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/BitmapLayer.md "BitmapLayer") when the app exits: ``` gbitmap\_destroy(s\_bitmap);bitmap\_layer\_destroy(s\_bitmap\_layer); ``` ## Menu Icon in the Launcher The new launcher in firmware 4.0+ allows developers to provide a custom icon for their watchapps and watchfaces. ![Launcher Icon](https://developer.repebble.com/assets/images/blog/2016-08-19-pikachu-icon.png) ![Launcher](https://developer.repebble.com/assets/images/blog/2016-08-19-pikachu-launcher.png) You can add a 25x25 `png` to the `resources.media` section of the`package.json` file, and set `"menuIcon": true`. Please note that icons that are larger will be rejected by the SDK. ``` "resources": {"media": [{"type": "bitmap","name": "MENU\_ICON","file": "images/icon.png","menuIcon": true}]} ``` There are four different menu icon display modes, each of which is automatically used based on the platform, type of app, and the way you specify the icon resource. 1. Inversion mode is supported on the black & white platforms, for both apps and watchfaces. This is the mode used by all system icons. In this mode, the icon gets inverted depending on the background on which it is placed. Aplite requires the resource to specify 1Bit memory format and the icon to contain only black and white; transparency and color is not handled. Diorite and Flint don't require the 1Bit memory format to be set for apps (but do for watchfaces), treat any transparency in the file as white, and automatically convert any color to black & white. 2. Non-inverting transparent black & white is an optional mode only for Diorite & Flint watchfaces. The icon is never inverted, and transparency works. If you provide a color icon, it is automatically converted to black & white based on luminance. This mode is enabled when the memory format is not 1Bit. 3. Non-inverting transparent greyscale is the only mode for apps on color platforms. If you provide a color icon, it will be automatically converted to greyscale based on luminance. 4. Non-inverting transparent color is the only mode for watchfaces on color platforms (of course, your icon doesn't have to contain color). Therefore, to support all platforms with the fewest different icons, one option is to provide two files `icon~bw.png` and `icon~color.png` (see [_Platform-specific Resources_](https://developer.repebble.com/guides/app-resources/platform-specific.md)) and add the following to platform.json: ``` "resources": {"media": [{"file": "icon.png","name": "APP\_ICON","type": "bitmap","memoryFormat": "1Bit","menuIcon": true,"targetPlatforms": ["aplite", "diorite", "flint"]},{"file": "icon.png","name": "APP\_ICON","type": "bitmap","targetPlatforms": ["basalt", "chalk", "emery", "gabbro"]},]} ``` If you wish to have a non-inverting transparent icon for Diorite and Flint (watchfaces only) you could specify those two platforms to use a different (non-1Bit) icon than Aplite or, if the automatic black & white conversion is sufficient, to use the same resource as the color platforms. The app icons specified during submission to the appstore are independent of this image resource (used in other places such as the mobile app and the appstore) - the `menuIcon` specified will always be used in the watch launcher list. --- # Pebble Draw Command File Format Source: https://developer.repebble.com/guides/app-resources/pdc-format/ Pebble [`Draw Commands`](https://developer.repebble.com/docs/c/Graphics/Draw_Commands.md "Draw Commands") (PDCs) are vector image files that consist of a binary resource containing the instructions for each stroke, fill, etc. that makes up the image. The byte format of all these components are described in tabular form below. > **Important** : All fields are in the little-endian format! An example implementation with some [usage limitations](https://developer.repebble.com/tutorials/advanced/vector-animations.md#creating-compatible-files) can be seen in [`svg2pdc.py`](https://github.com/pebble-examples/cards-example/blob/master/tools/svg2pdc.py). ## Component Types A PDC binary file consists of the following key components, in ascending order of abstraction: - [Draw Command](#pebble-draw-command) - an instruction for a single line or path to be drawn. - [Draw Command List](#pebble-draw-command-list) - a set of Draw Commands that make up a shape. - [Draw Command Frame](#pebble-draw-command-frame) - a Draw Command List with configurable duration making up one animation frame. Many of these are used in a Draw Command Sequence. - [Draw Command Image](#pebble-draw-command-image) - A single vector image. - [Draw Command Sequence](#pebble-draw-command-sequence) - A set of Draw Command Frames that make up an animated sequence of vector images. ## Versions | PDC Format Version | Implemented | | --- | --- | | 1 | Firmware 3.0 | ## File Format Components ### Point | Field | Offset (bytes) | Size (bytes) | Description | | --- | --- | --- | --- | | X | 0 | 2 | X axis coordinate. Has one of two formats depending on the Draw Command type (see below): Path/Circle type: signed integer. Precise path type: 13.3 fixed point. | | Y | 2 | 2 | Y axis coordinate. Has one of two formats depending on the Draw Command type (see below): Path/Circle type: signed integer. Precise path type: 13.3 fixed point. | ### View Box | Field | Offset (bytes) | Size (bytes) | Description | | --- | --- | --- | --- | | Width | 0 | 2 | Width of the view box (signed integer). | | Height | 2 | 2 | Height of the view box (signed integer). | ### Pebble Draw Command | Field | Offset (bytes) | Size (bytes) | Description | | --- | --- | --- | --- | | Type | 0 | 1 | Draw command type. Possible values are: `0` - Invalid `1` - Path `2` - Circle `3` - Precise path | | Flags | 1 | 1 | Bit 0: Hidden (Draw Command should not be drawn). Bits 1-7: Reserved. | | Stroke color | 2 | 1 | Pebble color (integer). | | Stroke width | 3 | 1 | Stroke width (unsigned integer). | | Fill color | 4 | 1 | Pebble color (integer). | | Path open/radius | 5 | 2 | Path/Precise path type: Bit 0 indicates whether or not the path is drawn open (`1`) or closed (`0`). Circle type: radius of the circle. | | Number of points | 7 | 2 | Number of points (n) in the point array. See below. | | Point array | 9 | n x 4 | The number of points (n) points. | ### Pebble Draw Command List | Field | Offset (bytes) | Size (bytes) | Description | | --- | --- | --- | --- | | Number of commands | 0 | 2 | Number of Draw Commands in this Draw Command List. (`0` is invalid). | | Draw Command array | 2 | n x size of Draw Command | List of Draw Commands in the format [specified above](#pebble-draw-command). | ### Pebble Draw Command Frame | Field | Offset (bytes) | Size (bytes) | Description | | --- | --- | --- | --- | | Duration | 0 | 2 | Duration of the frame in milliseconds. If `0`, the frame will not be shown at all (unless it is the last frame in a sequence). | | Command list | 2 | Size of Draw Command List | Pebble Draw Command List in the format [specified above](#pebble-draw-command-list). | ### Pebble Draw Command Image | Field | Offset (bytes) | Size (bytes) | Description | | --- | --- | --- | --- | | Version | 8 | 1 | File version. | | Reserved | 9 | 1 | Reserved field. Must be `0`. | | [View box](#view-box) | 10 | 4 | Bounding box of the image. All Draw Commands are drawn relative to the top left corner of the view box. | | Command list | 14 | Size of Draw Command List | Pebble Draw Command List in the format [specified above](#pebble-draw-command-list). | ### Pebble Draw Command Sequence | Field | Offset (bytes) | Size (bytes) | Description | | --- | --- | --- | --- | | Version | 8 | 1 | File version. | | Reserved | 9 | 1 | Reserved field. Must be `0`. | | [View box](#view-box) | 10 | 4 | Bounding box of the sequence. All Draw Commands are drawn relative to the top left corner of the view box. | | Play count | 14 | 2 | Number of times to repeat the sequence. A value of `0` will result in no playback at all, whereas a value of `0xFFFF` will repeat indefinitely. | | Frame count | 16 | 2 | Number of frames in the sequence. `0` is invalid. | | Frame list | 18 | n x size of Draw Command Frame | Array of Draw Command Frames in the format [specified above](#pebble-draw-command-frame). | ## File Formats ### Pebble Draw Command Image File | Field | Offset (bytes) | Size (bytes) | Description | | --- | --- | --- | --- | | Magic word | 0 | 4 | ASCII characters spelling "PDCI". | | Image size | 4 | 4 | Size of the Pebble Draw Command Image (in bytes). | | Image | 8 | Size of Pebble Draw Command Image. | The Draw Command Image in the format [specified above](#pebble-draw-command-image). | ### Pebble Draw Command Sequence File | Field | Offset (bytes) | Size (bytes) | Description | | --- | --- | --- | --- | | Magic word | 0 | 4 | ASCII characters spelling "PDCS". | | Sequence size | 4 | 4 | Size of the Pebble Draw Command Sequence (in bytes). | | Sequence | 8 | Size of Draw Command Sequence | The Draw Command Sequence in the format [specified above](#pebble-draw-command-sequence). | --- # Platform-specific Resources Source: https://developer.repebble.com/guides/app-resources/platform-specific/ You may want to use different versions of a resource on one or more of the Aplite, Basalt or Chalk platforms. To enable this, it is now possible to "tag" resource files with the attributes that make them relevant to a given platform. The follows tags exist for each platform: | Aplite | Basalt | Chalk | Diorite | Emery | Flint | | --- | --- | --- | --- | --- | --- | | rect | rect | round | rect | rect | rect | | bw | color | color | bw | color | bw | | aplite | basalt | chalk | diorite | emery | flint | | 144w | 144w | 180w | 144w | 200w | 144w | | 168h | 168h | 180h | 168h | 228h | 168h | | compass | compass | compass | | compass | compass | | | mic | mic | mic | mic | mic | | | strap | strap | strap | strap | | | | strappower | strappower | | strappower | | | | health | health | health | health | health | To tag a resource, add the tags after the file's using tildes (`~`) — for instance, `example-image~color.png` to use the resource on only color platforms, or `example-image~color~round.png` to use the resource on only platforms with round, color displays. All tags must match for the file to be used. If no file matches for a platform, a compilation error will occur. If the correct file for a platform is ambiguous, an error will occur at compile time. You cannot, for instance, have both `example~color.png` and `example~round.png`, because it is unclear which image to use when building for Chalk. Instead, use `example~color~rect.png` and `example~round.png`. If multiple images could match, the one with the most tags wins. We recommend avoiding the platform specific tags (aplite, basalt etc). When we release new platforms in the future, you will need to create new files for that platform. However, if you use the descriptive tags we will automatically use them as appropriate. It is also worth noting that the platform tags are _not_ special: if you have `example~basalt.png` and `example~rect.png`, that is ambiguous (they both match Basalt) and will cause a compilation error. An example file structure is shown below. ``` my-project/ resources/ images/ example-image~bw.png example-image~color~rect.png example-image~color~round.png src/ main.c package.json wscript ``` This resource will appear in `package.json` as shown below. ``` "resources": {"media": [{"type": "bitmap","name": "EXAMPLE\_IMAGE","file": "images/example-image.png"}]} ``` **Single-platform Resources** If you want to only include a resource on a **specific** platform, you can add a `targetPlatforms` field to the resource's entry in the `media` array in `package.json`. For example, the resource shown below will only be included for the Basalt build. ``` "resources": {"media": [{"type": "bitmap","name": "BACKGROUND\_IMAGE","file": "images/background.png","targetPlatforms": ["basalt"]}]} ``` --- # Raw Data Files Source: https://developer.repebble.com/guides/app-resources/raw-data-files/ Some kinds of apps will require extra data that is not a font or an image. In these cases, the file can be included in a Pebble project as a raw resource. When a file is included as a raw resource, it is not modified in any way from the original when the app is built. Applications of this resource type can be found in the Pebble SDK for APIs such as [`GDrawCommand`](https://developer.repebble.com/docs/c/Graphics/Draw_Commands.md#GDrawCommand "GDrawCommand") and [`GBitmapSequence`](https://developer.repebble.com/docs/c/Graphics/Graphics_Types.md#GBitmapSequence "GBitmapSequence"), which both use raw resources as input files. Other possible applications include localized string dictionaries, CSV data files, etc. ## Adding Raw Data Files To add a file as a raw resource, specify its `type` as `raw` in `package.json`. An example is shown below: ``` "resources": {"media": [{"type": "raw","name": "EXAMPLE\_DATA\_FILE","file": "data.bin"}]} ``` ## Reading Bytes and Byte Ranges Once a raw resource has been added to a project, it can be loaded at runtime in a manner similar to other resources types: ``` // Get resource handleResHandle handle = resource\_get\_handle(RESOURCE\_ID\_DATA); ``` With a handle to the resource now available in the app, the size of the resource can be determined: ``` // Get size of the resource in bytessize\_t res\_size = resource\_size(handle); ``` To read bytes from the resource, create an appropriate byte buffer and copy data into it: ``` // Create a buffer the exact size of the raw resourceuint8\_t \*s\_buffer = (uint8\_t\*)malloc(res\_size); ``` The example below copies the entire resource into a `uint8_t` buffer: ``` // Copy all bytes to a bufferresource\_load(handle, s\_buffer, res\_size); ``` It is also possible to read a specific range of bytes from a given offset into the buffer: ``` // Read the second set of 8 bytesresource\_load\_byte\_range(handle, 8, s\_buffer, 8); ``` --- # System Fonts Source: https://developer.repebble.com/guides/app-resources/system-fonts/ The tables below show all the system font identifiers available in the Pebble SDK, sorted by family. A sample of each is also shown. ## Available System Fonts ### Raster Gothic Available Font Keys | Basalt Preview | Emery Preview || `FONT_KEY_GOTHIC_14` | ![](https://developer.repebble.com/assets/images/guides/app-resources/fonts/gothic_14_preview.png) | ![](https://developer.repebble.com/assets/images/guides/app-resources/fonts/gothic_14_emery.png) | | `FONT_KEY_GOTHIC_14_BOLD` | ![](https://developer.repebble.com/assets/images/guides/app-resources/fonts/gothic_14_bold_preview.png) | ![](https://developer.repebble.com/assets/images/guides/app-resources/fonts/gothic_14_bold_emery.png) | | `FONT_KEY_GOTHIC_18` | ![](https://developer.repebble.com/assets/images/guides/app-resources/fonts/gothic_18_preview.png) | ![](https://developer.repebble.com/assets/images/guides/app-resources/fonts/gothic_18_emery.png) | | `FONT_KEY_GOTHIC_18_BOLD` | ![](https://developer.repebble.com/assets/images/guides/app-resources/fonts/gothic_18_bold_preview.png) | ![](https://developer.repebble.com/assets/images/guides/app-resources/fonts/gothic_18_bold_emery.png) | | `FONT_KEY_GOTHIC_24` | ![](https://developer.repebble.com/assets/images/guides/app-resources/fonts/gothic_24_preview.png) | ![](https://developer.repebble.com/assets/images/guides/app-resources/fonts/gothic_24_emery.png) | | `FONT_KEY_GOTHIC_24_BOLD` | ![](https://developer.repebble.com/assets/images/guides/app-resources/fonts/gothic_24_bold_preview.png) | ![](https://developer.repebble.com/assets/images/guides/app-resources/fonts/gothic_24_bold_emery.png) | | `FONT_KEY_GOTHIC_28` | ![](https://developer.repebble.com/assets/images/guides/app-resources/fonts/gothic_28_preview.png) | ![](https://developer.repebble.com/assets/images/guides/app-resources/fonts/gothic_28_emery.png) | | `FONT_KEY_GOTHIC_28_BOLD` | ![](https://developer.repebble.com/assets/images/guides/app-resources/fonts/gothic_28_bold_preview.png) | ![](https://developer.repebble.com/assets/images/guides/app-resources/fonts/gothic_28_bold_emery.png) | ### Bitham Available Font Keys | Basalt Preview | Emery Preview || `FONT_KEY_BITHAM_30_BLACK` | ![](https://developer.repebble.com/assets/images/guides/app-resources/fonts/bitham_30_black_preview.png) | ![](https://developer.repebble.com/assets/images/guides/app-resources/fonts/bitham_30_black_emery.png) | | `FONT_KEY_BITHAM_34_MEDIUM_NUMBERS` | ![](https://developer.repebble.com/assets/images/guides/app-resources/fonts/bitham_34_medium_numbers_preview.png) | ![](https://developer.repebble.com/assets/images/guides/app-resources/fonts/bitham_34_medium_numbers_emery.png) | | `FONT_KEY_BITHAM_42_BOLD` | ![](https://developer.repebble.com/assets/images/guides/app-resources/fonts/bitham_42_bold_preview.png) | ![](https://developer.repebble.com/assets/images/guides/app-resources/fonts/bitham_42_bold_emery.png) | | `FONT_KEY_BITHAM_42_LIGHT` | ![](https://developer.repebble.com/assets/images/guides/app-resources/fonts/bitham_42_light_preview.png) | ![](https://developer.repebble.com/assets/images/guides/app-resources/fonts/bitham_42_light_emery.png) | | `FONT_KEY_BITHAM_42_MEDIUM_NUMBERS` | ![](https://developer.repebble.com/assets/images/guides/app-resources/fonts/bitham_42_medium_numbers_preview.png) | ![](https://developer.repebble.com/assets/images/guides/app-resources/fonts/bitham_42_medium_numbers_emery.png) | ### Roboto/Droid Serif Available Font Keys | Basalt Preview | Emery Preview || `FONT_KEY_ROBOTO_CONDENSED_21` | ![](https://developer.repebble.com/assets/images/guides/app-resources/fonts/roboto_21_condensed_preview.png) | ![](https://developer.repebble.com/assets/images/guides/app-resources/fonts/roboto_21_condensed_emery.png) | | `FONT_KEY_ROBOTO_BOLD_SUBSET_49` | ![](https://developer.repebble.com/assets/images/guides/app-resources/fonts/roboto_49_bold_subset_preview.png) | ![](https://developer.repebble.com/assets/images/guides/app-resources/fonts/roboto_49_bold_subset_emery.png) | | `FONT_KEY_DROID_SERIF_28_BOLD` | ![](https://developer.repebble.com/assets/images/guides/app-resources/fonts/droid_28_bold_preview.png) | ![](https://developer.repebble.com/assets/images/guides/app-resources/fonts/droid_28_bold_emery.png) | ### LECO Available Font Keys | Basalt Preview | Emery Preview || `FONT_KEY_LECO_20_BOLD_NUMBERS` | ![](https://developer.repebble.com/assets/images/guides/app-resources/fonts/leco_20_bold_preview.png) | ![](https://developer.repebble.com/assets/images/guides/app-resources/fonts/leco_20_bold_emery.png) | | `FONT_KEY_LECO_26_BOLD_NUMBERS_AM_PM` | ![](https://developer.repebble.com/assets/images/guides/app-resources/fonts/leco_26_bold_preview.png) | ![](https://developer.repebble.com/assets/images/guides/app-resources/fonts/leco_26_bold_emery.png) | | `FONT_KEY_LECO_28_LIGHT_NUMBERS` | ![](https://developer.repebble.com/assets/images/guides/app-resources/fonts/leco_28_light_preview.png) | ![](https://developer.repebble.com/assets/images/guides/app-resources/fonts/leco_28_light_emery.png) | | `FONT_KEY_LECO_32_BOLD_NUMBERS` | ![](https://developer.repebble.com/assets/images/guides/app-resources/fonts/leco_32_bold_preview.png) | ![](https://developer.repebble.com/assets/images/guides/app-resources/fonts/leco_32_bold_emery.png) | | `FONT_KEY_LECO_36_BOLD_NUMBERS` | ![](https://developer.repebble.com/assets/images/guides/app-resources/fonts/leco_36_bold_preview.png) | ![](https://developer.repebble.com/assets/images/guides/app-resources/fonts/leco_36_bold_emery.png) | | `FONT_KEY_LECO_38_BOLD_NUMBERS` | ![](https://developer.repebble.com/assets/images/guides/app-resources/fonts/leco_38_bold_preview.png) | ![](https://developer.repebble.com/assets/images/guides/app-resources/fonts/leco_38_bold_emery.png) | | `FONT_KEY_LECO_42_NUMBERS` | ![](https://developer.repebble.com/assets/images/guides/app-resources/fonts/leco_42_preview.png) | ![](https://developer.repebble.com/assets/images/guides/app-resources/fonts/leco_42_emery.png) | | `FONT_KEY_LECO_60_NUMBERS_AM_PM` | _Emery and newer only_ | ![](https://developer.repebble.com/assets/images/guides/app-resources/fonts/leco_60_emery.png) | | `FONT_KEY_LECO_60_BOLD_NUMBERS_AM_PM` | _Emery and newer only_ | ![](https://developer.repebble.com/assets/images/guides/app-resources/fonts/leco_60_bold_emery.png) | ## Obtaining System Font Files The following system fonts are available to developers in the SDK can be found online for use in design mockups: - [Raster Gothic](http://www.marksimonson.com/) - By Mark Simonson - [Gotham (Bitham)](http://www.typography.com/fonts/gotham/overview/) - Available from Typography.com - [Droid Serif](https://www.google.com/fonts/specimen/Droid+Serif) - Available from Google Fonts - [LECO 1976](https://www.myfonts.com/fonts/carnoky/leco-1976/) - Available from Myfonts.com ## Using Emoji Fonts A subset of the built-in system fonts support the use of a set of emoji characters. These are the Gothic 24, Gothic 24 Bold, Gothic 18, and Gothic 18 Bold fonts, but do not include the full range. To print an emoji on Pebble, specify the code in a character string like the one shown below when using a [`TextLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/TextLayer.md "TextLayer"), or [`graphics_draw_text()`](https://developer.repebble.com/docs/c/Graphics/Drawing_Text.md#graphics_draw_text "graphics\_draw\_text"): ``` text\_layer\_set\_text(s\_layer, "Smiley face: \U0001F603"); ``` An app containing a [`TextLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/TextLayer.md "TextLayer") displaying the above string will look similar to this: ![emoji-screenshot](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-screenshot.png) The supported characters are listed below with their corresponding Unicode codepoints, alongside how each one is rendered on Pebble. _Jumbomoji_ shows the 80×80 SVG that PebbleOS uses when a notification body is just that one emoji. | Codepoint | Symbol | Pebble (small) | Pebble (large) | Jumbomoji | Name | | --- | --- | --- | --- | --- | --- | | `U+231A` | ⌚ | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+231A.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+231A.png) | | Watch _(new)_ | | `U+263A` | ☺ | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+263A.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+263A.png) | | White Smiling Face | | `U+2620` | ☠ | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+2620.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+2620.png) | | Skull and Crossbones _(new)_ | | `U+26A7` | ⚧ | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+26A7.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+26A7.png) | | Transgender Symbol _(new)_ | | `U+2705` | ✅ | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+2705.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+2705.png) | | Check Mark Button _(new)_ | | `U+270B` | ✋ | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+270B.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+270B.png) | | Raised Hand | | `U+270C` | ✌ | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+270C.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+270C.png) | | Victory Hand | | `U+2728` | ✨ | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+2728.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+2728.png) | | Sparkles _(new)_ | | `U+274E` | ❎ | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+274E.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+274E.png) | | Cross Mark Button _(new)_ | | `U+2757` | ❗ | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+2757.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+2757.png) | | Red Exclamation Mark _(new)_ | | `U+2763` | ❣ | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+2763.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+2763.png) | | Heavy Heart Exclamation _(new)_ | | `U+2764` | ❤ | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+2764.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+2764.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/jumbo/Pebble_80x80_Emoji_heart.svg) | Heavy Black Heart | | `U+2B50` | ⭐ | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+2B50.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+2B50.png) | | Star _(new)_ | | `U+1F319` | 🌙 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F319.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F319.png) | | Crescent Moon _(new)_ | | `U+1F31F` | 🌟 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F31F.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F31F.png) | | Glowing Star _(new)_ | | `U+1F337` | 🌷 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F337.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F337.png) | | Tulip _(new)_ | | `U+1F338` | 🌸 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F338.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F338.png) | | Cherry Blossom _(new)_ | | `U+1F33A` | 🌺 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F33A.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F33A.png) | | Hibiscus _(new)_ | | `U+1F340` | 🍀 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F340.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F340.png) | | Four Leaf Clover _(new)_ | | `U+1F37A` | 🍺 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F37A.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F37A.png) | | Beer Mug | | `U+1F37B` | 🍻 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F37B.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F37B.png) | | Clinking Beer Mugs | | `U+1F389` | 🎉 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F389.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F389.png) | | Party Popper | | `U+1F3B6` | 🎶 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F3B6.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F3B6.png) | | Musical Notes _(new)_ | | `U+1F3F3` | 🏳 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F3F3.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F3F3.png) | | Waving White Flag _(new)_ | | `U+1F425` | 🐥 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F425.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F425.png) | | Front-Facing Baby Chick | | `U+1F440` | 👀 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F440.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F440.png) | | Eyes _(new)_ | | `U+1F44D` | 👍 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F44D.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F44D.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/jumbo/Pebble_80x80_Emoji_thumbs_up.svg) | Thumbs Up | | `U+1F44E` | 👎 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F44E.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F44E.png) | | Thumbs Down | | `U+1F480` | 💀 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F480.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F480.png) | | Skull _(new)_ | | `U+1F493` | 💓 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F493.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F493.png) | | Beating Heart | | `U+1F494` | 💔 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F494.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F494.png) | | Broken Heart | | `U+1F495` | 💕 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F495.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F495.png) | | Two Hearts | | `U+1F496` | 💖 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F496.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F496.png) | | Sparkling Heart | | `U+1F497` | 💗 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F497.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F497.png) | | Growing Heart | | `U+1F498` | 💘 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F498.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F498.png) | | Heart with Arrow | | `U+1F499` | 💙 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F499.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F499.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/jumbo/Pebble_80x80_Emoji_heart.svg) | Blue Heart | | `U+1F49A` | 💚 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F49A.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F49A.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/jumbo/Pebble_80x80_Emoji_heart.svg) | Green Heart | | `U+1F49B` | 💛 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F49B.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F49B.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/jumbo/Pebble_80x80_Emoji_heart.svg) | Yellow Heart | | `U+1F49C` | 💜 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F49C.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F49C.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/jumbo/Pebble_80x80_Emoji_heart.svg) | Purple Heart | | `U+1F49D` | 💝 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F49D.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F49D.png) | | Heart with Ribbon | | `U+1F49E` | 💞 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F49E.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F49E.png) | | Revolving Hearts | | `U+1F49F` | 💟 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F49F.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F49F.png) | | Heart Decoration | | `U+1F4A1` | 💡 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F4A1.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F4A1.png) | | Light Bulb _(new)_ | | `U+1F4A3` | 💣 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F4A3.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F4A3.png) | | Bomb _(new)_ | | `U+1F4A5` | 💥 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F4A5.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F4A5.png) | | Collision _(new)_ | | `U+1F4A9` | 💩 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F4A9.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F4A9.png) | | Pile of Poo | | `U+1F4AF` | 💯 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F4AF.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F4AF.png) | | Hundred Points _(new)_ | | `U+1F5A4` | 🖤 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F5A4.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F5A4.png) | | Black Heart _(new)_ | | `U+1F600` | 😀 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F600.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F600.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/jumbo/Pebble_80x80_Emoji_big_open_smile.svg) | Grinning Face | | `U+1F601` | 😁 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F601.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F601.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/jumbo/Pebble_80x80_Emoji_smiling_with_teeth.svg) | Beaming Face with Smiling Eyes | | `U+1F602` | 😂 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F602.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F602.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/jumbo/Pebble_80x80_Emoji_laughing_with_tears.svg) | Face with Tears of Joy | | `U+1F603` | 😃 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F603.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F603.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/jumbo/Pebble_80x80_Emoji_big_open_smile.svg) | Smiling Face with Open Mouth | | `U+1F604` | 😄 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F604.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F604.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/jumbo/Pebble_80x80_Emoji_big_smile.svg) | Smiling Face with Smiling Eyes | | `U+1F605` | 😅 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F605.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F605.png) | | Smiling Face with Cold Sweat | | `U+1F606` | 😆 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F606.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F606.png) | | Smiling Face with Closed Eyes | | `U+1F607` | 😇 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F607.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F607.png) | | Smiling Face with Halo | | `U+1F608` | 😈 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F608.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F608.png) | | Smiling Face with Horns | | `U+1F609` | 😉 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F609.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F609.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/jumbo/Pebble_80x80_Emoji_wink.svg) | Winking Face | | `U+1F60A` | 😊 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F60A.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F60A.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/jumbo/Pebble_80x80_Emoji_smiling_blush.svg) | Smiling Face with Smiling Eyes | | `U+1F60B` | 😋 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F60B.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F60B.png) | | Face Savoring Food | | `U+1F60C` | 😌 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F60C.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F60C.png) | | Relieved Face | | `U+1F60D` | 😍 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F60D.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F60D.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/jumbo/Pebble_80x80_Emoji_smiling_hearts.svg) | Smiling Face with Heart-Eyes | | `U+1F60E` | 😎 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F60E.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F60E.png) | | Smiling Face with Sunglasses | | `U+1F60F` | 😏 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F60F.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F60F.png) | | Smirking Face | | `U+1F610` | 😐 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F610.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F610.png) | | Neutral Face | | `U+1F611` | 😑 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F611.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F611.png) | | Expressionless Face | | `U+1F612` | 😒 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F612.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F612.png) | | Unamused Face | | `U+1F613` | 😓 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F613.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F613.png) | | Face with Cold Sweat | | `U+1F614` | 😔 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F614.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F614.png) | | Pensive Face | | `U+1F615` | 😕 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F615.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F615.png) | | Confused Face | | `U+1F616` | 😖 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F616.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F616.png) | | Confounded Face | | `U+1F617` | 😗 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F617.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F617.png) | | Kissing Face | | `U+1F618` | 😘 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F618.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F618.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/jumbo/Pebble_80x80_Emoji_kissing_with_heart.svg) | Face Blowing a Kiss | | `U+1F619` | 😙 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F619.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F619.png) | | Kissing Face with Smiling Eyes | | `U+1F61A` | 😚 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F61A.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F61A.png) | | Kissing Face with Closed Eyes | | `U+1F61B` | 😛 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F61B.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F61B.png) | | Face with Tongue | | `U+1F61C` | 😜 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F61C.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F61C.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/jumbo/Pebble_80x80_Emoji_wink_tongue.svg) | Winking Face with Tongue | | `U+1F61D` | 😝 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F61D.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F61D.png) | | Squinting Face with Tongue | | `U+1F61E` | 😞 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F61E.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F61E.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/jumbo/Pebble_80x80_Emoji_sad.svg) | Disappointed Face | | `U+1F61F` | 😟 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F61F.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F61F.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/jumbo/Pebble_80x80_Emoji_sad.svg) | Worried Face | | `U+1F620` | 😠 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F620.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F620.png) | | Angry Face | | `U+1F621` | 😡 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F621.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F621.png) | | Pouting Face | | `U+1F622` | 😢 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F622.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F622.png) | | Crying Face | | `U+1F623` | 😣 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F623.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F623.png) | | Persevering Face | | `U+1F624` | 😤 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F624.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F624.png) | | Face with Steam from Nose | | `U+1F625` | 😥 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F625.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F625.png) | | Sad but Relieved Face | | `U+1F626` | 😦 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F626.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F626.png) | | Frowning Face with Open Mouth | | `U+1F627` | 😧 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F627.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F627.png) | | Anguished Face | | `U+1F628` | 😨 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F628.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F628.png) | | Fearful Face | | `U+1F629` | 😩 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F629.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F629.png) | | Weary Face | | `U+1F62A` | 😪 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F62A.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F62A.png) | | Sleepy Face | | `U+1F62B` | 😫 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F62B.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F62B.png) | | Tired Face | | `U+1F62C` | 😬 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F62C.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F62C.png) | | Grimacing Face | | `U+1F62D` | 😭 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F62D.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F62D.png) | | Loudly Crying Face | | `U+1F62E` | 😮 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F62E.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F62E.png) | | Face with Open Mouth | | `U+1F62F` | 😯 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F62F.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F62F.png) | | Hushed Face | | `U+1F630` | 😰 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F630.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F630.png) | | Anxious Face with Sweat | | `U+1F631` | 😱 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F631.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F631.png) | | Face Screaming in Fear | | `U+1F632` | 😲 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F632.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F632.png) | | Astonished Face | | `U+1F633` | 😳 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F633.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F633.png) | | Flushed Face | | `U+1F634` | 😴 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F634.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F634.png) | | Sleeping Face | | `U+1F635` | 😵 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F635.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F635.png) | | Dizzy Face | | `U+1F636` | 😶 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F636.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F636.png) | | Face Without Mouth | | `U+1F637` | 😷 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F637.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F637.png) | | Face with Medical Mask | | `U+1F643` | 🙃 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F643.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F643.png) | | Upside-Down Face _(new)_ | | `U+1F644` | 🙄 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F644.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F644.png) | | Face with Rolling Eyes _(new)_ | | `U+1F64F` | 🙏 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F64F.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F64F.png) | | Folded Hands | | `U+1F917` | 🤗 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F917.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F917.png) | | Hugging Face _(new)_ | | `U+1F918` | 🤘 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F918.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F918.png) | | Sign of the Horns _(new)_ | | `U+1F91D` | 🤝 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F91D.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F91D.png) | | Handshake _(new)_ | | `U+1F923` | 🤣 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F923.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F923.png) | | Rolling on the Floor Laughing _(new)_ | | `U+1F924` | 🤤 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F924.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F924.png) | | Drooling Face _(new)_ | | `U+1F929` | 🤩 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F929.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F929.png) | | Star-Struck _(new)_ | | `U+1F92A` | 🤪 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F92A.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F92A.png) | | Zany Face _(new)_ | | `U+1F92C` | 🤬 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F92C.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F92C.png) | | Face with Symbols on Mouth _(new)_ | | `U+1F92E` | 🤮 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F92E.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F92E.png) | | Face Vomiting _(new)_ | | `U+1F970` | 🥰 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F970.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F970.png) | | Smiling Face with Hearts _(new)_ | | `U+1F97A` | 🥺 | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/small/U+1F97A.png) | ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-glyphs/large/U+1F97A.png) | | Pleading Face _(new)_ | ### Deprecated Emoji Symbols The following emoji characters are no longer available on the Aplite platform. ![](https://developer.repebble.com/assets/images/guides/pebble-apps/resources/emoji-unsupported.png) --- # Benefits of Design Guidelines Source: https://developer.repebble.com/guides/design-and-interaction/benefits/ ## What are Design Guidelines? Design guidelines are a set of concepts and rules used to create an app's user interface. These define how the layout on-screen at any one time should be used to maximize the efficiency of presenting data to the user, as well as quickly informing them how to choose their next action. An app creator may look to other popular apps to determine how they have helped their users understand their app's purpose, either through the use of iconography or text highlighting. They may then want to use that inspiration to enable users of the inspiring app to easily use their own app. If many apps use the same visual cues, then future users will already be trained in their use when they discover them. ## What are Interaction Patterns? Similar to design guidelines, interaction patterns define how to implement app interactivity to maximize its efficiency. If a user can predict how the app will behave when they take a certain action, or be able to determine which action fits that which they want to achieve without experimentation, then an intuitive and rewarding experience will result. In addition to purely physical actions such as button presses and accelerometer gestures, the virtual navigation flow should also be considered at the design stage. It should be intuitive for the user to move around the app screens to access the information or execute the commands as they would expect on a first guess. An easy way to achieve this is to use a menu with the items clearly labelling their associated actions. An alternative is to use explicit icons to inform the user implicitly of their purpose without needing to label them all. ## Why are They Needed? Design guidelines and interaction patterns exist to help the developer help the user by ensuring user interface consistency across applications on the platform. It is often the case that the developer will have no problem operating their own watchapp because they have been intimately familiar with how it is supposed to work since its inception. When such an app is given to users, they may receive large amounts of feedback from confused users who feel they do not know if an app supports the functionality they though it did, or even how to find it. By considering a novice user from the beginning of the UI design and implementation, this problem can be avoided. A Pebble watchapp experience is at its best when it can be launched, used for its purpose in the smallest amount of time, and then closed back to the watchface. If the user must spend a long time navigating the app's UI to get to the information they want, or the information takes a while to arrive on every launch, the app efficiency suffers. To avoid this problem, techniques such as implementing a list of the most commonly used options in an app (according to the user or the whole user base) to aid fast navigation, or caching remotely fetched data which may still be relevant from the last update will improve the user experience. From an interaction pattern point of view, a complex layout filled with abstract icons may confuse a first-time user as to what each of them represents. Apps can mitigate this problem by using icons that have pre-established meanings across languages, such as the 'Play/Pause' icon or the 'Power' icon, seen on many forms of devices. ## What Are the Benefits? The main benefits of creating and following design guidelines and common interaction patterns are summarized as follows: - User interface consistency, which breeds familiarity and predictability. - Clarity towards which data is most important and hence visible and usable. - Reduced user confusion and frustration, leading to improved perception of apps. - No need to include explicit usage instructions in every app to explain how it must be used. - Apps that derive design from the system apps can benefit from any learned behavior all Pebble users may develop in using their watches out the box. - Clearer, more efficient and better looking apps! ## Using Existing Affordances Developers can use concepts and interaction patterns already employed in system apps and popular 3rd party apps to lend those affordances to your own apps. An example of this in mobile apps is the common 'swipe down to refresh' action. By using this action in their app, many mobile app makers can benefit from users who have already been trained to perform this action, and can free up their app's UI for a cleaner look, or use the space that would have been used by a 'refresh' button to add an additional feature. In a similar vein, knowing that the Back button always exits the current [`Window`](https://developer.repebble.com/docs/c/User_Interface/Window.md "Window") in a Pebble app, a user does not have to worry about knowing how to navigate out of it. Similarly, developers do not have to repeatedly implement exiting an app, as this action is a single, commonly understood pattern - just press the Back button! On the other hand, if a developer overrides this action a user may be confused or frustrated when the app fails to exit as they would expect, and this could mean a negative opinion that could have been avoided. ## What's Next? Read [_Core Experience Design_](https://developer.repebble.com/guides/design-and-interaction/core-experience.md) to learn how design guidelines helped shape the core Pebble system experience. --- # Core Experience Design Source: https://developer.repebble.com/guides/design-and-interaction/core-experience/ The core Pebble experience includes several built-in system apps that use repeatable design and interaction concepts in their implementation. These are: - Apps are designed with a single purpose in mind, and they do it well. - Fast animations are used to draw attention to changing or updating information. - Larger, bolder fonts to highlight important data. - A preference for displaying multiple data items in a paginated format rather than many 'menus within menus'. This is called the 'card' pattern and is detail in [_Display Data Sets Using Cards_](https://developer.repebble.com/guides/design-and-interaction/recommended.md#display-data-sets-using-cards). - Colors are used to enhance the app's look and feel, and are small in number. Colors are also sometimes used to indicate state, such as the temperature in a weather app, or to differentiate between different areas of a layout. ## System Experience Design The core system design and navigation model is a simple metaphor for time on the user's wrist - a linear representation of the past, present, and future. The first two are presented using the timeline design, with a press of the Up button displaying past events (also known as pins), and a press of the Down button displaying future events. As was the case for previous versions of the system experience, pressing the Select button opens the app menu. This contains the system apps, such as Music and Notifications, as well as all the 3rd party apps the user has installed in their locker. ![system-navigation](https://developer.repebble.com/assets/images/guides/design-and-interaction/system-navigation.png) Evidence of the concepts outlined above in action can be seen within the core system apps in firmware 3.x. These are described in detail in the sections below. ### Music ![music](https://developer.repebble.com/assets/images/guides/design-and-interaction/music.png) The Music app is designed with a singular purpose in mind - display the current song and allow control over playback. To achieve this, the majority of the screen space is devoted to the most important data such as the song title and artist. The remainder of the space is largely used to display the most immediate controls for ease of interaction in an action bar UI element. The 'previous track' and 'next track' icons on the action bar are ones with pre-existing affordances which do not require specific instruction for new users thanks to their universal usaging other media applications. The use of the '...' icon is used as an additional commonly understood action to indicate more functionality is available. By single-pressing this action, the available actions change from preview/next to volume up/volume down, reverting on a timeout. This is preferable to a long-press, which is typically harder to discover without an additional prompt included in the UI. A press of the Back button returns the user to the appface menu, where the Music appface displays the name and artist of the currently playing track, in a scrolling 'marquee' manner. If no music is playing, no information is shown here. ### Notifications ![notifications](https://developer.repebble.com/assets/images/guides/design-and-interaction/notifications.png) The system Notifications app allows a user to access all their past received notifications in one place. Due to the fact that the number of notifications received can be either small or large, the main view of the app is implemented as a menu, with each item showing each notification's icon, title and the first line of the body content. In this way it is easy for a user to quickly scroll down the list and identify the notification they are looking for based on these first hints. The first item in the menu is a 'Clear All' option, which when selected prompts the user to confirm this action using a dialog. This dialog uses the action bar component to give the user the opportunity to confirm this action with the Select button, or to cancel it with the Back button. Once the desired item has been found, a press of the Select button opens a more detailed view, where the complete notification content can be read, scrolling down if needed. The fact that there is more content available to view is hinted at using the arrow marker and overlapping region at the bottom of the layout. ### Alarms ![alarms](https://developer.repebble.com/assets/images/guides/design-and-interaction/alarms.png) The Alarms app is the most complex of the system apps, with multiple screens dedicated to input collection from the user. Like the Watchfaces and Music apps, the appface in the system menu shows the time of the next upcoming scheduled alarm, if any. Also in keeping with other system apps, the main screen is presented using a menu, with each item representing a scheduled alarm. Each alarm is treated as a separate item, containing different settings and values. A press of the Select button on an existing item will open the action menu containing a list of possible actions, such as 'Delete' or 'Disable'. Pressing Select on the top item ('+') will add a new item to the list, using multiple subsequent screens to collect data about the alarm the user wishes to schedule. Using multiple screens avoids the need for one screen to contain a lot of input components and clutter up the display. In the time selection screen, the current selection is marked using a green highlight. The Up and Down buttons are used to increase and decrease the currently selected field respectively. Once a time and recurring frequency has been chosen by the user, the new alarm is added to the main menu list. The default state is enabled, marked by the word 'ON' to the right hand side, but can be disabled in which case 'OFF' is displayed instead. ### Watchfaces ![watchfaces](https://developer.repebble.com/assets/images/guides/design-and-interaction/watchfaces.png) The Watchfaces system app is similar to the Notifications app, in that it uses a menu as its primary means of navigation. Each watchface available in the user's locker is shown as a menu item, with a menu icon if one has been included by the watchface developer. The currently active watchface is indicated by the presence of 'Active' as that item's subtitle. Once the user has selected a new watchface, they are shown a confirmation dialog to let them know their choice was successful. If the watchface is not currently loaded on the watch, a progress bar is shown briefly while the data is loaded. Once this is done the newly chosen watchface is displayed. ### Settings ![settings](https://developer.repebble.com/assets/images/guides/design-and-interaction/settings.png) The Settings app uses the system appface to display the date, the battery charge level, and the Bluetooth connection without the need to open the app proper. If the user does open the app, they are greeted with a menu allowing a choice of settings category. This approach saves the need for a single long list of settings that would require a lot of scrolling. Once a category has been chosen, the app displays another menu filled with interactive menu rows that change various settings. Each item shows the name of the setting in bold as the item title, with the current state of the setting shown as the subtitle. When the user presses Select, the state of the currently selected setting is changed, usually in a binary rotation of On -\> Off states. If the setting does not operate with a binary state (two states), or has more than two options, an action menu window is displayed with the available actions, allowing the user to select one with the Select button. A press of the Back button from a category screen returns the user to the category list, where they can make another selection, or press Back again to return to the app menu. ### Sports API #### aplite #### basalt #### chalk ![](https://developer.repebble.com/assets/images/guides/design-and-interaction/sports~aplite.png) ![](https://developer.repebble.com/assets/images/guides/design-and-interaction/sports~basalt.png) ![](https://developer.repebble.com/assets/images/guides/design-and-interaction/sports~chalk.png) The Sports API app is designed around displaying the most immediate relevant data of a particular sporting activity, such as running or cycling. A suitable Android or iOS companion app pushes data to this app using the [_PebbleKit Sports API_](https://developer.repebble.com/guides/communication/using-the-sports-api.md) at regular intervals. This API enables third-party sports app developers to easily add support for Pebble without needing to create and maintain their own watchapp. The high contrast choice of colors makes the information easy to read at a glance in a wide variety of lighting conditions, ideal for use in outdoor activities. The action bar is also used to present the main action available to the user - the easily recognizable 'pause' action to suspend the current activity for a break. This is replaced by the equally recognizable 'play' icon, the action now used to resume the activity. This API also contains a separate Golf app for PebbleKit-compatible apps to utilize in tracking the user's golf game. #### aplite #### basalt #### chalk ![](https://developer.repebble.com/assets/images/guides/design-and-interaction/golf~aplite.png) ![](https://developer.repebble.com/assets/images/guides/design-and-interaction/golf~basalt.png) ![](https://developer.repebble.com/assets/images/guides/design-and-interaction/golf~chalk.png) The Golf app uses a similar design style with larger fonts for important numbers in the center of the layout, with the action bar reserved for additional input, such as moving between holes. Being an app that is intended to be in use for long periods of time, the status bar is used to display the current time for quick reference without needing to exit back to the watchface. ## Timeline Experience Design Evidence of guided design can also be found in other aspects of the system, most noticeably the timeline view. With pins containing possibly a large variety of types of information, it is important to display a view which caters for as many types as possible. As detailed in [_Creating Pins_](https://developer.repebble.com/guides/pebble-timeline/pin-structure.md), pins can be shown in two different ways; the event time, icon and title on two lines or a single line, depending on how the user is currently navigating the timeline. When a pin is highlighted, as much information is shown to the user as possible. ![timeline](https://developer.repebble.com/assets/images/guides/design-and-interaction/timeline.png) Users access the timeline view using Up and Down buttons from the watchface to go into the past and future respectively. The navigation of the timeline also uses animations for moving elements that gives life to the user interface, and elements such as moving the pin icon between windows add cohesion between the different screens. A press of the Select button opens the pin to display all the information it contains, and is only one click away. A further press of the Select button opens the pin's action menu, containing a list of all the actions a user may take. These actions are directly related to the pin, and can be specified when it is created. The system provides two default actions: 'Remove' to remove the pin from the user's timeline, and 'Mute [Name]' to mute all future pins from that source. This gives the user control over which pins they see in their personal timeline. Mute actions can be reversed later in the mobile app's 'Apps/Timeline' screen. ## What's Next? Read [_Recommended Guidelines and Patterns_](https://developer.repebble.com/guides/design-and-interaction/recommended.md) for tips on creating an intuitive app experience. --- # One Click Actions Source: https://developer.repebble.com/guides/design-and-interaction/one-click-actions/ One click actions are set to revolutionize the way users interact with their Pebble by providing instant access to their favorite one click watchapps, directly from the new system launcher. Want to unlock your front door? Call an Uber? Or perhaps take an instant voice note? With one click actions, the user is able to instantly perform a single action by launching an app, and taking no further action. ![Lockitron](https://developer.repebble.com/assets/images/guides/design-and-interaction/lockitron.png) ### The One Click Flow It’s important to develop your one click application with a simple and elegant flow. You need to simplify the process of your application by essentially creating an application which serves a single purpose. The typical flow for a one click application would be as follows: 1. Application is launched 2. Application performs action 3. Application displays status to user 4. Application automatically exits to watchface if the action was successful, or displays status message and does not exit if the action failed If we were creating an instant voice note watchapp, the flow could be as follows: 1. Application launched 2. Application performs action (take a voice note) 1. Start listening for dictation 2. Accept dictation response 3. Application displays a success message 4. Exit to watchface In the case of a one click application for something like Uber, we would need to track the state of any existing booking to prevent ordering a second car. We would also want to update the [`App Glance`](https://developer.repebble.com/docs/c/Foundation/App_Glance.md "App Glance") as the status of the booking changes. 1. Application launched 2. If a booking exists: 1. Refresh booking status 2. Update [`App Glance`](https://developer.repebble.com/docs/c/Foundation/App_Glance.md "App Glance") with new status 3. Exit to watchface 3. Application performs action (create a booking) 1. Update AppGlance: “Your Uber is on it’s way” 2. Application displays a success message 3. Exit to watchface ### Building a One Click Application For this example, we’re going to build a one click watchapp which will lock or unlock the front door of our virtual house. We’re going to use a virtual [Lockitron](https://lockitron.com/), or a real one if you’re lucky enough to have one. Our flow will be incredibly simple: 1. Launch the application 2. Take an action (toggle the state of the lock) 3. Update the [`App Glance`](https://developer.repebble.com/docs/c/Foundation/App_Glance.md "App Glance") to indicate the new lock state 4. Display a success message 5. Exit to watchface For the sake of simplicity in our example, we will not know if someone else has locked or unlocked the door using a different application. You can investigate the [Lockitron API](http://api.lockitron.com) if you want to develop this idea further. In order to control our Lockitron, we need the UUID of the lock and an access key. You can generate your own virtual lockitron UUID and access code on the [Lockitron website](https://api.lockitron.com/v1/getting_started/virtual_locks). ``` #define LOCKITRON\_LOCK\_UUID "95c22a11-4c9e-4420-adf0-11f1b36575f2"#define LOCKITRON\_ACCESS\_TOKEN "99e75a775fe737bb716caf88f161460bb623d283c3561c833480f0834335668b" ``` > Never publish your actual Lockitron access token in the appstore, unless you want strangers unlocking your door! Ideally you would make these fields configurable using [Clay for Pebble](https://github.com/pebble-dev/clay). We’re going to need a simple enum for the state of our lock, where 0 is unlocked, 1 is locked and anything else is unknown. ``` typedef enum {LOCKITRON\_UNLOCKED,LOCKITRON\_LOCKED,LOCKITRON\_UNKNOWN} LockitronLockState; ``` We’re also going to use a static variable to keep track of the state of our lock. ``` static LockitronLockState s\_lockitron\_state; ``` When our application launches, we’re going to initialize [`AppMessage`](https://developer.repebble.com/docs/c/Foundation/AppMessage.md "AppMessage") and then wait for PebbleKit JS to tell us it’s ready. ``` static void prv\_init(void) {app\_message\_register\_inbox\_received(prv\_inbox\_received\_handler);app\_message\_open(256, 256);s\_window = window\_create();window\_stack\_push(s\_window, false);}static void prv\_inbox\_received\_handler(DictionaryIterator \*iter, void \*context) {Tuple \*ready\_tuple = dict\_find(iter, MESSAGE\_KEY\_APP\_READY);if (ready\_tuple) {// PebbleKit JS is ready, toggle the Lockitron!prv\_lockitron\_toggle\_state();return;}// ...} ``` In order to toggle the state of the Lockitron, we’re going to send an [`AppMessage`](https://developer.repebble.com/docs/c/Foundation/AppMessage.md "AppMessage") to PebbleKit JS, containing our UUID and our access key. ``` static void prv\_lockitron\_toggle\_state() {DictionaryIterator \*out;AppMessageResult result = app\_message\_outbox\_begin(&out);dict\_write\_cstring(out, MESSAGE\_KEY\_LOCK\_UUID, LOCKITRON\_LOCK\_UUID);dict\_write\_cstring(out, MESSAGE\_KEY\_ACCESS\_TOKEN, LOCKITRON\_ACCESS\_TOKEN);result = app\_message\_outbox\_send();} ``` PebbleKit JS will handle this request and make the relevant ajax request to the Lockitron API. It will then return the current state of the lock and tell our application to exit back to the default watchface using [`AppExitReason`](https://developer.repebble.com/docs/c/Foundation/Exit_Reason.md#AppExitReason "AppExitReason"). See the [full example](https://github.com/pebble-examples/one-click-action-example) for the actual Javascript implementation. ``` static void prv\_inbox\_received\_handler(DictionaryIterator \*iter, void \*context) {// ...Tuple \*lock\_state\_tuple = dict\_find(iter, MESSAGE\_KEY\_LOCK\_STATE);if (lock\_state\_tuple) {// Lockitron state has changeds\_lockitron\_state = (LockitronLockState)lock\_state\_tuple-\>value-\>int32;// App will exit to default watchfaceapp\_exit\_reason\_set(APP\_EXIT\_ACTION\_PERFORMED\_SUCCESSFULLY);// Exit the application by unloading the only windowwindow\_stack\_remove(s\_window, false);}} ``` Before our application terminates, we need to update the [`App Glance`](https://developer.repebble.com/docs/c/Foundation/App_Glance.md "App Glance") with the current state of our lock. We do this by passing our current lock state into the [`app_glance_reload`](https://developer.repebble.com/docs/c/Foundation/App_Glance.md#app_glance_reload "app\_glance\_reload") method. ``` static void prv\_deinit(void) {window\_destroy(s\_window);// Before the application terminates, setup the AppGlanceapp\_glance\_reload(prv\_update\_app\_glance, &s\_lockitron\_state);} ``` We only need a single [`AppGlanceSlice`](https://developer.repebble.com/docs/c/Foundation/App_Glance.md#AppGlanceSlice "AppGlanceSlice") for our [`App Glance`](https://developer.repebble.com/docs/c/Foundation/App_Glance.md "App Glance"), but it’s worth noting you can have multiple slices with varying expiration times. ``` static void prv\_update\_app\_glance(AppGlanceReloadSession \*session, size\_t limit, void \*context) {// Check we haven't exceeded system limit of AppGlancesif (limit \< 1) return;// Retrieve the current Lockitron state from contextLockitronLockState \*lockitron\_state = context;// Generate a friendly message for the current Lockitron statechar \*str = prv\_lockitron\_status\_message(lockitron\_state);APP\_LOG(APP\_LOG\_LEVEL\_INFO, "STATE: %s", str);// Create the AppGlanceSlice (no icon, no expiry)const AppGlanceSlice entry = (AppGlanceSlice) {.layout = {.template\_string = str},.expiration\_time = time(NULL)+3600};// Add the slice, and check the resultconst AppGlanceResult result = app\_glance\_add\_slice(session, entry);if (result != APP\_GLANCE\_RESULT\_SUCCESS) {APP\_LOG(APP\_LOG\_LEVEL\_ERROR, "AppGlance Error: %d", result);}} ``` ### Handling Launch Reasons In the example above, we successfully created an application that will automatically execute our One Click Action when the application is launched. But we also need to be aware of some additional launch reasons where it would not be appropriate to perform the action. By using the [`launch_reason()`](https://developer.repebble.com/docs/c/Foundation/Launch_Reason.md#launch_reason "launch\_reason") method, we can detect why our application was started and prevent the One Click Action from firing unnecessarily. A common example, would be to detect if the application was actually started by the user, from either the launcher, or quick launch. ``` if(launch\_reason() == APP\_LAUNCH\_USER || launch\_reason() == APP\_LAUNCH\_QUICK\_LAUNCH) {// Perform One Click} else {// Display a message} ``` When the launch reason is [`APP_LAUNCH_USER`](https://developer.repebble.com/docs/c/Foundation/Launch_Reason.md#APP_LAUNCH_USER "APP\_LAUNCH\_USER") or [`APP_LAUNCH_QUICK_LAUNCH`](https://developer.repebble.com/docs/c/Foundation/Launch_Reason.md#APP_LAUNCH_QUICK_LAUNCH "APP\_LAUNCH\_QUICK\_LAUNCH"), [`launch_button()`](https://developer.repebble.com/docs/c/Foundation/Launch_Reason.md#launch_button "launch\_button") returns the [`ButtonId`](https://developer.repebble.com/docs/c/User_Interface/Clicks.md#ButtonId "ButtonId") that launched the app. When it is [`APP_LAUNCH_QUICK_LAUNCH`](https://developer.repebble.com/docs/c/Foundation/Launch_Reason.md#APP_LAUNCH_QUICK_LAUNCH "APP\_LAUNCH\_QUICK\_LAUNCH"), [`launch_get_quick_launch_action()`](https://developer.repebble.com/docs/c/Foundation/Launch_Reason.md#launch_get_quick_launch_action "launch\_get\_quick\_launch\_action") tells whether the user held a single button ([`APP_QUICK_LAUNCH_ACTION_HOLD`](https://developer.repebble.com/docs/c/Foundation/Launch_Reason.md#APP_QUICK_LAUNCH_ACTION_HOLD "APP\_QUICK\_LAUNCH\_ACTION\_HOLD")), tapped one ([`APP_QUICK_LAUNCH_ACTION_TAP`](https://developer.repebble.com/docs/c/Foundation/Launch_Reason.md#APP_QUICK_LAUNCH_ACTION_TAP "APP\_QUICK\_LAUNCH\_ACTION\_TAP")) or held a button combination ([`APP_QUICK_LAUNCH_ACTION_COMBO`](https://developer.repebble.com/docs/c/Foundation/Launch_Reason.md#APP_QUICK_LAUNCH_ACTION_COMBO "APP\_QUICK\_LAUNCH\_ACTION\_COMBO")); for any other launch reason it returns [`APP_QUICK_LAUNCH_ACTION_NONE`](https://developer.repebble.com/docs/c/Foundation/Launch_Reason.md#APP_QUICK_LAUNCH_ACTION_NONE "APP\_QUICK\_LAUNCH\_ACTION\_NONE"). An app assigned to more than one Quick Launch slot can use this to perform a different action for each button: ``` if(launch\_reason() == APP\_LAUNCH\_QUICK\_LAUNCH) {switch(launch\_button()) {case BUTTON\_ID\_UP:start\_timer(); break;case BUTTON\_ID\_DOWN: stop\_timer(); break;default: break;}} ``` Both functions are available on Pebble 2 Duo (flint), Pebble Time 2 (emery) and Pebble Round 2 (gabbro). On older platforms they compile to stubs that return [`BUTTON_ID_BACK`](https://developer.repebble.com/docs/c/User_Interface/Clicks.md#BUTTON_ID_BACK "BUTTON\_ID\_BACK") and [`APP_QUICK_LAUNCH_ACTION_NONE`](https://developer.repebble.com/docs/c/Foundation/Launch_Reason.md#APP_QUICK_LAUNCH_ACTION_NONE "APP\_QUICK\_LAUNCH\_ACTION\_NONE"). ### Conclusion As you can see, it’s a relatively small amount of code to create one click watchapps and we hope this inspires you to build your own! We recommend that you check out the complete [Lockitron sample](https://github.com/pebble-examples/one-click-action-example) application and also the [`App Glance`](https://developer.repebble.com/docs/c/Foundation/App_Glance.md "App Glance") and [`AppExitReason`](https://developer.repebble.com/docs/c/Foundation/Exit_Reason.md#AppExitReason "AppExitReason") guides for further information. --- # Recommended Guidelines and Patterns Source: https://developer.repebble.com/guides/design-and-interaction/recommended/ This page contains recommendations for things to consider when designing an app's visual styles and interaction patterns. The aim here is to encourage efficiency through optional conformity to common ideas and concepts, and to breed a consistent experience for users across apps. Developers can also find suggested interface styles to use when building the navigation of apps to best display the information contained within. ## Tips for UI Design To achieve an effective, clear, and intuitive design developers should: - Keep layouts **simple** , with only as much information displayed as is **immediately required**. This encourages quick usage of the app, distracting the user for the minimal amount of time necessary for the app to achieve its goals. - Give priority to the **most commonly used/main purpose** functionality of the app in the UI. Make it easy to use the biggest features, but still easy to find additional functionality that would otherwise be hidden. - Use **larger fonts** to highlight the **most important data** to be read at a glance. Consider font size 28 for larger items, and a minimum of 18 for smaller ones. - Take advantage of colors to convey additional **information without any text** if they are already associated as such, such as green for a task complete. - Try to avoid any colors used in places they may have a **pre-conceived meaning** which does not apply, such as red text when there are no errors. - Use animations to embody the layout with **character** and flourish, as well as to **draw the eye** to updated or changing information. - Ensure the UI gives **direct feedback** to the user's input, or else they may think their button presses are having no effect. ## Tips for UI Interaction - Avoid using the Pebble buttons for actions **not** already associated with them, unless clearly marked in the UI using an [`ActionBarLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/ActionBarLayer.md "ActionBarLayer") or similar method. When using the Up and Down buttons for 'previous item' and 'next item' respectively, there may be no need for a visual cue. - Use iconography with **pre-existing visual associations** (such as a 'stop' icon) to take advantage of the inherent behavior the user will have when seeing it. This can avoid the need for explicit instructions to train the user for an app's special case. - Ensure the navigation between app [`Window`](https://developer.repebble.com/docs/c/User_Interface/Window.md "Window")s is **logical** with the input given and the information displayed. For example, an app showing bus timetables should use higher level screens for quickly navigating to a particular area/station, with lower level views reserved for scrolling through the actual timetable data. - If possible, **preserve the state of the app** and/or previous navigation if the app is commonly used for a repetitive task. In the bus timetable example, the user may only look up two stations for a to-from work journey. Learn this behavior and store it with the [Persistent Storage API](https://developer.repebble.com/docs/c/Foundation/Storage.md "Storage") to intelligently adapt the UI on subsequent launches to show the relevant information. This helps the user avoid navigating through multiple menus every time they launch the app. ## Common Design Styles The following are common design styles that have been successfully used in system and 3rd party apps, and are recommended for use in the correct manner. ### Display Data Sets Using Cards ![card](https://developer.repebble.com/assets/images/guides/design-and-interaction/card.gif) The 'card' style aims to reduce the number of menu levels needed to access as much relevant information as possible. Instead of a menu to select a data set leading to a menu to explore each item in that set, a single [`Window`](https://developer.repebble.com/docs/c/User_Interface/Window.md "Window") is designed that displays an entire data set. This view then uses the Pebble Up and Down buttons to scroll through complete data sets in an array of many sets. An example of this is the [_cards-example_](https://developer.repebble.com/guides/design-and-interaction/implementation.md#cards-example-weather) example app, which displays all weather data in a single view and pages through sets of data for separate locations with the Up and Down buttons. This style of UI design allows access to lots of information without navigating through several menus to view it. ### List Options with a Menu #### aplite #### basalt #### chalk ![](https://developer.repebble.com/assets/images/guides/design-and-interaction/list~aplite.png) ![](https://developer.repebble.com/assets/images/guides/design-and-interaction/list~basalt.png) ![](https://developer.repebble.com/assets/images/guides/design-and-interaction/list~chalk.png) The style is one of the most basic, tried and true styles. Using the [`MenuLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/MenuLayer.md "MenuLayer") UI component, the user may choose between multiple app functions by scrolling with the Up and Down buttons, an interaction pattern afforded to the developer by the core system experience. Using a menu, a user can navigate straight to the part of the app or specific action they want. ### Execute Actions with an ActionBarLayer #### aplite #### basalt #### chalk ![](https://developer.repebble.com/assets/images/guides/design-and-interaction/actionbar~aplite.png) ![](https://developer.repebble.com/assets/images/guides/design-and-interaction/actionbar~basalt.png) ![](https://developer.repebble.com/assets/images/guides/design-and-interaction/actionbar~chalk.png) The [`ActionBarLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/ActionBarLayer.md "ActionBarLayer") allows easy association of app functionality with the Pebble buttons. By setting icons to each of the three positions, a user can see which actions they can perform at a glance and execture them with a single button press. When pressed, the icon is animated to provide immediate visual feedback. An example of this is the system Music app, that uses the [`ActionBarLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/ActionBarLayer.md "ActionBarLayer") to inform the user that the Up and Down buttons skip tracks. In this case, the Select button is displayed with elipses, indicating further actions are available. A press of this button changes the set of actions on the Up and Down buttons, enabling them to modify the playback volume instead. A collection of icons for common actions is available for use by developers, and can be found in the [_App Assets_](https://developer.repebble.com/guides/app-resources/app-assets.md) guide. ### Allow Extended Options with an Action Menu ![actionmenu](https://developer.repebble.com/assets/images/guides/design-and-interaction/actionmenu.png) If an app screen demands a larger range of available actions than the [`ActionBarLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/ActionBarLayer.md "ActionBarLayer") will allow, present these as a list that slides into view with a press of the Select button using an action menu. This menu contains all the available options, and can contain multiple sub-menus, providing levels. The user can keep track of which level they are currently looking at using the breadcrumb dots on the left-hand side of the screen when the action menu is displayed. Once an action has been chosen, the user should be informed of the success or failure of their choice using a new alert dialog window. In the system action menus, these screens use an eye-catching animation and bold label to convey the result of the action. This feedback is important to prevent the user from getting frustrated if they perceive their input has no result, as well as to reassure them that their action has succeeded without a problem. ### Get User Input with a Form ![list](https://developer.repebble.com/assets/images/guides/design-and-interaction/alarm-list-config.png) Apps such as the system Alarm app make use of a list of configurable items, with each active alarm treated as a menu item with properties. The status of each item is displayed in a menu, with the Select button initiating configuration of that item. When an item is being configured, the data requried to create the item should be obtained from the user through the use of a form, with manipulable elements. In the Alarms example, each integer required to schedule an alarm is obtained with a number field that can have its value incrememted or decremented using the intuitive Up and Down buttons. The current form element is highlighted with color, and advancing to the next element is done with the Select button, doubling as a 'Confirm' action when the end of the form is reached. ### Prompting User Action on the Phone In some applications, user input is required in the app's configuration page (as detailed in [_App Configuration_](https://developer.repebble.com/guides/user-interfaces/app-configuration.md)) before the app can perform its task. An example of this is a feed reader app, that will need the user to input the URL of their preferred news feed before it can fetch the feed content. In this case, the watchapp should display a prominent (full- screen if possible) dialog telling the user that input to the phone app for configuration is required. ![action-required](https://developer.repebble.com/assets/images/guides/design-and-interaction/action-required.png) Once the user has performed the required action on the phone, the [`webviewclosed`](https://developer.repebble.com/guides/communication/using-pebblekit-js.md) event should signify that the app can proceed, and that the required data is now available. It should not be the case that this action is required every time the app is started. In most cases, the input data from the user can be stored with [Peristent Storage](https://developer.repebble.com/docs/c/Foundation/Storage.md "Storage") on the watch, or [`localStorage`](https://developer.repebble.com/guides/communication/using-pebblekit-js.md) on the phone. If the app must get input on every launch (such as a mode selection), this should be done through a form or menu on the watch, so as to avoid needing to use the phone. ### Show Time and Other Data with the Status Bar #### aplite #### basalt #### chalk ![](https://developer.repebble.com/assets/images/guides/design-and-interaction/alarm-list~aplite.png) ![](https://developer.repebble.com/assets/images/guides/design-and-interaction/alarm-list~basalt.png) ![](https://developer.repebble.com/assets/images/guides/design-and-interaction/alarm-list~chalk.png) Under SDK 2.x, the status bar was displayed to users in all menus and watchapps except watchfaces, or where the developer had explicitly disabled it. This was useful for showing the time and battery level, but arguably not essential all the time. In SDK 3.x, only apps that are designed to be running for extended periods of time (such as Music and the Sports API app) show the time, using the [`StatusBarLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/StatusBarLayer.md "StatusBarLayer") UI component. The battery level can easily be seen from the Settings appface, and so it not necessary to be always visible. Another instance where the status bar is neccessary is in the Alarms app (shown above), where the user may need to compare with the current time when setting an alarm. If a constant, minimalistic display of app data is required, the [`StatusBarLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/StatusBarLayer.md "StatusBarLayer") can be used to perform this task. It provides a choice of separator mode and foreground/background colors, and can also be made transparent. Since is it just another [`Layer`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#Layer "Layer"), it can be easily extended with additional text, icons, or other data. For example, the [`cards-example`](https://github.com/pebble-examples/cards-example) app uses an extention of the status bar to display the currently selected 'card' (a set of displayed data). Another example is the progress bar component example from the [`ui-patterns`](https://github.com/pebble-examples/ui-patterns) app, which builds upon the dotted separator mode to become a thin progress bar. When used in conjunction with the [`ActionBarLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/ActionBarLayer.md "ActionBarLayer") (for example, in the Music system app), the width of the underlying layer should be adjusted such that the time displayed is shown in the new center of the app area (excluding that taken up by the action bar itself). ### Show Alerts and Get Decisions with Modal Windows ![dialog-message](https://developer.repebble.com/assets/images/guides/design-and-interaction/dialog-message.gif) When a significant event occurs while using an app, it should be made visible to the user through the use of a full-screen model dialog. In a similar way that notifications and reminders alert the user to events, these layouts consist of only the important information and an associated icon telling the user the source of the alert, or the reason for its occurrence. This pattern should also be used to quickly and efficently tell the user that an app-related error has occured, including steps on how to fix any potential problems. These alerts can also take the form of requests for important decisions to be made by the user, such as to remember a choice as the default preference: #### aplite #### basalt #### chalk ![](https://developer.repebble.com/assets/images/guides/design-and-interaction/dialog-choice-window~aplite.png) ![](https://developer.repebble.com/assets/images/guides/design-and-interaction/dialog-choice-window~basalt.png) ![](https://developer.repebble.com/assets/images/guides/design-and-interaction/dialog-choice-window~chalk.png) In this way the decision can be passed to the user with an immediately obvious and actionable set of choices. One the choice has been made, the modal window is dismissed, and a confirmation of the choice displayed. The user should then be returned to the previous window to resume their use of the app where they left off. ### Using Vibrations and Haptic Feedback The Pebble SDK allows the use of the vibration motor to deliver haptic feedback to the user. This can take the form of short, long, double pulses or more detailed vibration sequences, allowing a lot of customization as well as variation between apps. To encourage a consistent experience for users, the [`Vibes`](https://developer.repebble.com/docs/c/User_Interface/Vibes.md "Vibes") API should be used with the following points in mind: - A short pulse should be used to alert the user to the end of a long-running in-app event, such as a download completing, preferably when they are not looking at the watch. - A long pulse should be used to alert the user to a failure or error that requires attention and some interaction. - Custom vibration patterns should be used to allow the user to customize haptic feedback for different events inside the app. When the app is open and being actively interacted with no vibration or haptic feedback should be neccessary on top of the existing visual feedback. However, some exceptions may occur, such as for visually-impaired users. In these cases haptic feedback may be very useful in boosting app accessibility. ### Handling Connection Problems When a watchapp is running, there is no guarantee that the phone connection will be available at any one time. Most apps will function without this connection, but if PebbleKit JS, Android, or iOS is required, the user must be informed of the reason for failure so that they can correct the problem. This check can be performed at any time using [`connection_service_peek_pebble_app_connection()`](https://developer.repebble.com/docs/c/Foundation/Event_Service/ConnectionService.md#connection_service_peek_pebble_app_connection "connection\_service\_peek\_pebble\_app\_connection"). An example alert layout is shown below. #### aplite #### basalt #### chalk ![](https://developer.repebble.com/assets/images/guides/design-and-interaction/no-bt-connection~aplite.png) ![](https://developer.repebble.com/assets/images/guides/design-and-interaction/no-bt-connection~basalt.png) ![](https://developer.repebble.com/assets/images/guides/design-and-interaction/no-bt-connection~chalk.png) A similar situation arises if an app that requires information or responses from a remote web service attempts to do so, but the phone has no Internet connection. This may be because the user has opted to disable their data connections, or they may be out of range. Another example alert layout is shown below for this situation. #### aplite #### basalt #### chalk ![](https://developer.repebble.com/assets/images/guides/design-and-interaction/no-inet-connection~aplite.png) ![](https://developer.repebble.com/assets/images/guides/design-and-interaction/no-inet-connection~basalt.png) ![](https://developer.repebble.com/assets/images/guides/design-and-interaction/no-inet-connection~chalk.png) If these kinds of situations can cause problems for the operation of the app, consider using the [`Persistent Storage`](https://developer.repebble.com/docs/c/Foundation/Storage.md "Storage") API to cache the most recently loaded data (such as weather, sports scores or news items) from the last successful launch, and display this to the user (while making them aware of the data's age) until new data can be obtained. ### Hiding Timeline-only Apps Watchapps that consist only of a [_Pebble timeline_](https://developer.repebble.com/guides/pebble-timeline.md) experience will only need to be launched when configured by the user to select topic subscriptions. In these cases, developers should hide their app from the launcher menu to prevent the user needlessly launching it. To find out how to do this using the `hiddenApp` property, see [_App Metadata_](https://developer.repebble.com/guides/tools-and-resources/app-metadata.md). ## Consistent App Configuration Watchapps and watchfaces that include user configuration normally include a web page hosted by the app developer, allowing the user to choose from a set of options and apply them to the app. Such options include aesthetic options such as color schemes, larger font sizes, replacement images, data source choices, and others. Traditionally the design of these pages has been left entirely to the developer on a per-app basis, and this is reflected in the resulting design consistency. Read [_App Configuration_](https://developer.repebble.com/guides/user-interfaces/app-configuration.md) to learn more about configuration page design and implementation. ## What's Next? Read [_Round App Design_](https://developer.repebble.com/guides/design-and-interaction/in-the-round.md) to read tips and guidance on designing apps that work well on a round display. --- # Round App Design Source: https://developer.repebble.com/guides/design-and-interaction/in-the-round/ > This guide is about designing round apps. For advice on implementing a round design in code, read [_Round App UI_](https://developer.repebble.com/guides/user-interfaces/round-app-ui.md). With the release of the Chalk [platform](https://developer.repebble.com/faqs.md#pebble-sdk), developers must take new features and limitations into account when designing their apps. New and existing apps that successfully adapt their layout and colors for both Aplite and Basalt should also endeavor to do so for the Chalk platform. Pebble Round 2 (the Gabbro platform) has a 260 x 260 round display, compared to the 180 x 180 display of Pebble Time Round. The advice in this guide applies to both. Designs that are drawn relative to the display bounds scale between them; designs made from fixed-size bitmaps need a second set of assets for Gabbro. ## Center of Attention With the round Chalk display, apps no longer have the traditional constant amount of horizontal space available. This particularly affects the use of the [`MenuLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/MenuLayer.md "MenuLayer"). To compensate for this, menus are now always centered on the highlighted item. Use this to display additional information in the cell with the most space available, while showing reduced content previews in the unhighlighted cells. ![centered](https://developer.repebble.com/assets/images/guides/design-and-interaction/center-layout~chalk.png) Menus built using the standard cell drawing functions will automatically adopt this behavior. If performing custom cell drawing, new APIs are available to help implement this behavior. For more information, look at the [`Graphics`](https://developer.repebble.com/docs/c/Graphics.md "Graphics") documentation, as well as the [`menu_layer_set_center_focused()`](https://developer.repebble.com/docs/c/User_Interface/Layers/MenuLayer.md#menu_layer_set_center_focused "menu\_layer\_set\_center\_focused") and [`menu_layer_is_index_selected()`](https://developer.repebble.com/docs/c/User_Interface/Layers/MenuLayer.md#menu_layer_is_index_selected "menu\_layer\_is\_index\_selected") to help with conditional drawing. ## Pagination Another key concept to bear in mind when designing for a round display is text flow. In traditional Pebble apps, text in [`ScrollLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/ScrollLayer.md "ScrollLayer") or [`TextLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/TextLayer.md "TextLayer") elements could be freely moved and scrolled with per-pixel increments without issue. However, with a round display each row of text can have a different width, depending on its vertical position. If such text was reflowed while moving smoothly down the window, the layout would reflow so often the text would be very difficult to read. ![center-layout](https://developer.repebble.com/assets/images/guides/design-and-interaction/scrolling-with-text-flow.gif) The solution to this problem is to scroll through text in pages, a technique known as pagination. By moving through the text in discrete sections, the text is only reflowed once per 'page', and remains easily readable as the user is navigating through it. The [`ScrollLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/ScrollLayer.md "ScrollLayer") has been updated to implement this on Chalk. To inform the user that more content is available, the Chalk platform allows use of the [`ContentIndicator`](https://developer.repebble.com/docs/c/User_Interface/Layers/ScrollLayer.md#ContentIndicator "ContentIndicator") UI component. This facilitates the display of two arrows at the top and bottom of the display, similar to those seen in the system UI. ![content-indicator](https://developer.repebble.com/assets/images/guides/design-and-interaction/content-indicator.png) A [`ContentIndicator`](https://developer.repebble.com/docs/c/User_Interface/Layers/ScrollLayer.md#ContentIndicator "ContentIndicator") can be created from scratch and manually managed to determine when the arrows should be shown, or a built-in instance can be obtained from a [`ScrollLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/ScrollLayer.md "ScrollLayer"). ## Platform-Specific Designs Sometimes a design that made sense on a rectangular display does not make sense on a circular one, or could be improved. Be open to creating a new UI for the Chalk platform, and selecting which to use based on the display shape. For example, in the screenshot below the linear track display was incompatible with the round display and center-focused menus, leading to a completely different design on Chalk that shows the same information. #### basalt #### chalk ![](https://developer.repebble.com/assets/images/guides/design-and-interaction/caltrain-stops~basalt.png) ![](https://developer.repebble.com/assets/images/guides/design-and-interaction/caltrain-stops~chalk.png) ## What's Next? Read [_Example Implementations_](https://developer.repebble.com/guides/design-and-interaction/implementation.md) to learn how to use and implement the UI components and patterns encouraged in SDK 3.x apps. --- # Example Implementations Source: https://developer.repebble.com/guides/design-and-interaction/implementation/ This guide contains resources and links to code examples that may help developers implement UI designs and interaction patterns recommended in the other guides in this section. ## UI Components and Patterns Developers can make use of the many UI components available in SDK 3.x in combination with the [_Common Design Styles_](https://developer.repebble.com/guides/design-and-interaction/recommended.md#common-design-styles) to ensure the user experience is consistent and intuitive. The following components and patterns are used in the Pebble experience, and listed in the table below. Some are components available for developers to use in the SDK, or are example implementations designed for adaptation and re-use. | Pattern | Screenshot | Description | | --- | --- | --- | | [`Menu Layer`](https://developer.repebble.com/docs/c/User_Interface/Layers/MenuLayer.md "MenuLayer") | ![](https://developer.repebble.com/assets/images/guides/design-and-interaction/menulayer.png) | Show many items in a list, allow scrolling between them, and choose an option. | | [`Status Bar`](https://developer.repebble.com/docs/c/User_Interface/Layers/StatusBarLayer.md "StatusBarLayer") | ![](https://developer.repebble.com/assets/images/guides/design-and-interaction/alarm-list~basalt.png) | Display the time at the top of the Window, optionally extended with additional data. | | [`Radio Button List`](https://github.com/pebble-examples/ui-patterns/blob/master/src/windows/radio_button_window.c) | ![](https://developer.repebble.com/assets/images/guides/design-and-interaction/radio-button.png) | Allow the user to specify one choice out of a list. | | [`Checkbox List`](https://github.com/pebble-examples/ui-patterns/blob/master/src/windows/checkbox_window.c) | ![](https://developer.repebble.com/assets/images/guides/design-and-interaction/checkbox-list.png) | Allow the user to choose multiple different options from a list. | | [`List Message`](https://github.com/pebble-examples/ui-patterns/blob/master/src/windows/list_message_window.c) | ![](https://developer.repebble.com/assets/images/guides/design-and-interaction/list-message.png) | Provide a hint to help the user choose from a list of options. | | [`Message Dialog`](https://github.com/pebble-examples/ui-patterns/blob/master/src/windows/dialog_message_window.c) | ![](https://developer.repebble.com/assets/images/guides/design-and-interaction/dialog-message.gif) | Show an important message using a bold fullscreen alert. | | [`Choice Dialog`](https://github.com/pebble-examples/ui-patterns/blob/master/src/windows/dialog_choice_window.c) | ![](https://developer.repebble.com/assets/images/guides/design-and-interaction/dialog-choice-patterns.png) | Present the user with an important choice, using the action bar and icons to speed up decision making. | | [`PIN Entry`](https://github.com/pebble-examples/ui-patterns/blob/master/src/windows/pin_window.c) | ![](https://developer.repebble.com/assets/images/guides/design-and-interaction/pin.png) | Enable the user to input integer data. | | [`Text Animation`](https://github.com/pebble-examples/ui-patterns/blob/master/src/windows/text_animation_window.c) | ![](https://developer.repebble.com/assets/images/guides/design-and-interaction/text-change-anim.gif) | Example animation to highlight a change in a text field. | | [`Progress Bar`](https://github.com/pebble-examples/ui-patterns/blob/master/src/windows/progress_bar_window.c) | ![](https://developer.repebble.com/assets/images/guides/design-and-interaction/progress-bar.gif) | Example progress bar implementation on top of a [`StatusBarLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/StatusBarLayer.md "StatusBarLayer"). | | [`Progress Layer`](https://github.com/pebble-examples/ui-patterns/blob/master/src/windows/progress_layer_window.c) | ![](https://developer.repebble.com/assets/images/guides/design-and-interaction/progresslayer.gif) | Example implementation of the system progress bar layer. | ## Example Apps Developers can look at existing apps to begin to design (or improve) their user interface and interaction design. Many of these apps can be found on the appstore with links to their source code, and can be used as inspiration. ### Cards Example (Weather) The weather [`cards-example`](https://github.com/pebble-examples/cards-example) embodies the 'card' design pattern. Consisting of a single layout, it displays all the crucial weather-related data in summary without the need for further layers of navigation. Instead, the buttons are reserved for scrolling between whole sets of data pertaining to different cities. The number of 'cards' is shown in the top-right hand corner to let the user know that there is more data present to be scrolled through, using the pre-existing Up and Down button action affordances the user has already learned. This helps avoid implementing a novel navigation pattern, which saves time for both the user and the developer. ![weather](https://developer.repebble.com/assets/images/guides/design-and-interaction/weather.gif) When the user presses the appropriate buttons to scroll through sets of data, the changing information is animated with fast, snappy, and highly visible animations to reinforce the idea of old data moving out of the layout and being physically replaced by new data. --- # Design and Interaction Source: https://developer.repebble.com/guides/design-and-interaction/ Interaction guides are intended to help developers design their apps to maximize user experience through effective, consistent visual design and user interactions on the Pebble platform. Readers can be non-programmers and programmers alike: All material is explained conceptually and no code must be understood. For code examples, see [_Example Implementations_](https://developer.repebble.com/guides/design-and-interaction/implementation.md). By designing apps using a commonly understood and easy to understand visual language users can get the best experience with the minimum amount of effort expended - learning how they work, how to operate them or what other behavior is required. This can help boost how efficiently any given app is used as well as help reinforce the underlying patterns for similar apps. For example, the layout design should make it immediately obvious which part of the UI contains the vital information the user should glance at first. In addition to consistent visual design, implementing a common interaction pattern helps an app respond to users as they would expect. This allows them to correctly predict how an app will respond to their input without having to experiment to find out. To get a feel for how to approach good UI design for smaller devices, read other examples of developer design guidelines such as Google's [Material Design](http://www.google.com/design/spec/material-design/introduction.html) page or Apple's [iOS Human Interface Guidelines](https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/MobileHIG/). ## Contents - [**Benefits of Design Guidelines**](https://developer.repebble.com/guides/design-and-interaction/benefits.md) - Learn the main concepts of design guidelines, why they are needed, and how they can help developers. - [**Core Experience Design**](https://developer.repebble.com/guides/design-and-interaction/core-experience.md) - How design guidelines shape the core Pebble app experience. - [**One Click Actions**](https://developer.repebble.com/guides/design-and-interaction/one-click-actions.md) - Details about how to create One Click Action watchapps - [**Recommended Guidelines and Patterns**](https://developer.repebble.com/guides/design-and-interaction/recommended.md) - Pebble's recommended guidelines for creating awesome app experiences. - [**Round App Design**](https://developer.repebble.com/guides/design-and-interaction/in-the-round.md) - Tips and advice for designing apps that take advantage of the Pebble Time Round display - [**Example Implementations**](https://developer.repebble.com/guides/design-and-interaction/implementation.md) - Resources and code samples to implement common design and UI patterns. --- # Animations Source: https://developer.repebble.com/guides/graphics-and-animations/animations/ The [`Animation`](https://developer.repebble.com/docs/c/User_Interface/Animation.md "Animation") API allows a variety of different types of value to be smoothly animated from an initial value to a new value over time. Animations can also use built-in easing curves to affect how the transition behaves. ## Using PropertyAnimations The most common use of animations is to move a [`Layer`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#Layer "Layer") (or similar) around the display. For example, to show or hide some information or animate the time changing in a watchface. The simplest method of animating a [`Layer`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#Layer "Layer") (such as a [`TextLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/TextLayer.md "TextLayer")) is to use a [`PropertyAnimation`](https://developer.repebble.com/docs/c/User_Interface/Animation/PropertyAnimation.md "PropertyAnimation"), which animates a property of the target object. In this example, the target is the frame property, which is a [`GRect`](https://developer.repebble.com/docs/c/Graphics/Graphics_Types.md#GRect "GRect") To animate the this property, [`property_animation_create_layer_frame()`](https://developer.repebble.com/docs/c/User_Interface/Animation/PropertyAnimation.md#property_animation_create_layer_frame "property\_animation\_create\_layer\_frame") is used, which is a convenience [`PropertyAnimation`](https://developer.repebble.com/docs/c/User_Interface/Animation/PropertyAnimation.md "PropertyAnimation") implementation provided by the SDK. ``` static Layer \*s\_layer; ``` Create the Layer during [`Window`](https://developer.repebble.com/docs/c/User_Interface/Window.md "Window") initialization: ``` // Create the Layers\_layer = layer\_create(some\_bounds); ``` Determine the start and end values of the [`Layer`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#Layer "Layer")'s frame. These are the 'from' and 'to' locations and sizes of the [`Layer`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#Layer "Layer") before and after the animation takes place: ``` // The start and end frames - move the Layer 40 pixels to the rightGRect start = GRect(10, 10, 20, 20);GRect finish = GRect(50, 10, 20, 20); ``` At the appropriate time, create a [`PropertyAnimation`](https://developer.repebble.com/docs/c/User_Interface/Animation/PropertyAnimation.md "PropertyAnimation") to animate the [`Layer`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#Layer "Layer"), specifying the `start` and `finish` values as parameters: ``` // Animate the LayerPropertyAnimation \*prop\_anim = property\_animation\_create\_layer\_frame(s\_layer, &start, &finish); ``` Configure the attributes of the [`Animation`](https://developer.repebble.com/docs/c/User_Interface/Animation.md "Animation"), such as the delay before starting, and total duration (in milliseconds): ``` // Get the AnimationAnimation \*anim = property\_animation\_get\_animation(prop\_anim);// Choose parametersconst int delay\_ms = 1000;const int duration\_ms = 500;// Configure the Animation's curve, delay, and durationanimation\_set\_curve(anim, AnimationCurveEaseOut);animation\_set\_delay(anim, delay\_ms);animation\_set\_duration(anim, duration\_ms); ``` Finally, schedule the [`Animation`](https://developer.repebble.com/docs/c/User_Interface/Animation.md "Animation") to play at the next possible opportunity (usually immediately): ``` // Play the animationanimation\_schedule(anim); ``` If the app requires knowledge of the start and end times of an [`Animation`](https://developer.repebble.com/docs/c/User_Interface/Animation.md "Animation"), it is possible to register [`AnimationHandlers`](https://developer.repebble.com/docs/c/User_Interface/Animation.md#AnimationHandlers "AnimationHandlers") to be notified of these events. The handlers should be created with the signature of these examples shown below: ``` static void anim\_started\_handler(Animation \*animation, void \*context) {APP\_LOG(APP\_LOG\_LEVEL\_DEBUG, "Animation started!");}static void anim\_stopped\_handler(Animation \*animation, bool finished, void \*context) {APP\_LOG(APP\_LOG\_LEVEL\_DEBUG, "Animation stopped!");} ``` Register the handlers with an optional third context parameter **before** scheduling the [`Animation`](https://developer.repebble.com/docs/c/User_Interface/Animation.md "Animation"): ``` // Set some handlersanimation\_set\_handlers(anim, (AnimationHandlers) {.started = anim\_started\_handler,.stopped = anim\_stopped\_handler}, NULL); ``` With the handlers registered, the start and end times of the [`Animation`](https://developer.repebble.com/docs/c/User_Interface/Animation.md "Animation") can be detected by the app and used as appropriate. ### Other Types of PropertyAnimation In addition to [`property_animation_create_layer_frame()`](https://developer.repebble.com/docs/c/User_Interface/Animation/PropertyAnimation.md#property_animation_create_layer_frame "property\_animation\_create\_layer\_frame"), it is also possible to animate the origin of a [`Layer`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#Layer "Layer")'s bounds using [`property_animation_create_bounds_origin()`](https://developer.repebble.com/docs/c/User_Interface/Animation/PropertyAnimation.md#property_animation_create_bounds_origin "property\_animation\_create\_bounds\_origin"). Animation of more types of data can be achieved using custom implementations and one the following provided update implementations and the associated [getters and setters](https://developer.repebble.com/docs/c/User_Interface/Animation/PropertyAnimation.md#property_animation_update_int16 "property\_animation\_update\_int16"): - [`property_animation_update_int16`](https://developer.repebble.com/docs/c/User_Interface/Animation/PropertyAnimation.md#property_animation_update_int16 "property\_animation\_update\_int16") - Animate an `int16`. - [`property_animation_update_uint32`](https://developer.repebble.com/docs/c/User_Interface/Animation/PropertyAnimation.md#property_animation_update_uint32 "property\_animation\_update\_uint32") - Animate a `uint32`. - [`property_animation_update_gpoint`](https://developer.repebble.com/docs/c/User_Interface/Animation/PropertyAnimation.md#property_animation_update_gpoint "property\_animation\_update\_gpoint") - Animate a [`GPoint`](https://developer.repebble.com/docs/c/Graphics/Graphics_Types.md#GPoint "GPoint"). - [`property_animation_update_grect`](https://developer.repebble.com/docs/c/User_Interface/Animation/PropertyAnimation.md#property_animation_update_grect "property\_animation\_update\_grect") - Animate a [`GRect`](https://developer.repebble.com/docs/c/Graphics/Graphics_Types.md#GRect "GRect") - [`property_animation_update_gcolor8`](https://developer.repebble.com/docs/c/User_Interface/Animation/PropertyAnimation.md#property_animation_update_gcolor8 "property\_animation\_update\_gcolor8") - Animate a [`GColor8`](https://developer.repebble.com/docs/c/Graphics/Graphics_Types.md#GColor8 "GColor8"). ## Custom Animation Implementations Beyond the convenience functions provided by the SDK, apps can implement their own [`Animation`](https://developer.repebble.com/docs/c/User_Interface/Animation.md "Animation") by using custom callbacks for each stage of the animation playback process. A [`PropertyAnimation`](https://developer.repebble.com/docs/c/User_Interface/Animation/PropertyAnimation.md "PropertyAnimation") is an example of such an implementation. The callbacks to implement are the `.setup`, `.update`, and `.teardown` members of an [`AnimationImplementation`](https://developer.repebble.com/docs/c/User_Interface/Animation.md#AnimationImplementation "AnimationImplementation") object. Some example implementations are shown below. It is in the `.update` callback where the value of `progress` can be used to modify the custom target of the animation. For example, some percentage of completion: ``` static void implementation\_setup(Animation \*animation) {APP\_LOG(APP\_LOG\_LEVEL\_INFO, "Animation started!");}static void implementation\_update(Animation \*animation, const AnimationProgress progress) {// Animate some completion variables\_animation\_percent = ((int)progress \* 100) / ANIMATION\_NORMALIZED\_MAX;APP\_LOG(APP\_LOG\_LEVEL\_INFO, "Animation progress: %d%%", s\_animation\_percent);}static void implementation\_teardown(Animation \*animation) {APP\_LOG(APP\_LOG\_LEVEL\_INFO, "Animation finished!");}// This needs to exist while the event loop runsstatic const AnimationImplementation s\_implementation = {.setup = implementation\_setup,.update = implementation\_update,.teardown = implementation\_teardown}; ``` Once these are in place, create a new [`Animation`](https://developer.repebble.com/docs/c/User_Interface/Animation.md "Animation") , specifying the new custom implementation as a `const` object pointer at the appropriate time: ``` // Create a new AnimationAnimation \*animation = animation\_create();animation\_set\_delay(animation, 1000);animation\_set\_duration(animation, 1000);// Create the AnimationImplementationanimation\_set\_implementation(animation, &s\_implementation);// Play the Animationanimation\_schedule(animation); ``` The output of the example above will look like the snippet shown below (edited for brevity). Note the effect of the easing [`AnimationCurve`](https://developer.repebble.com/docs/c/User_Interface/Animation.md#AnimationCurve "AnimationCurve") on the progress value: ``` [13:42:33] main.c:11> Animation started! [13:42:34] main.c:19> Animation progress: 0% [13:42:34] main.c:19> Animation progress: 0% [13:42:34] main.c:19> Animation progress: 0% [13:42:34] main.c:19> Animation progress: 2% [13:42:34] main.c:19> Animation progress: 3% [13:42:34] main.c:19> Animation progress: 5% [13:42:34] main.c:19> Animation progress: 7% [13:42:34] main.c:19> Animation progress: 10% [13:42:34] main.c:19> Animation progress: 14% [13:42:35] main.c:19> Animation progress: 17% [13:42:35] main.c:19> Animation progress: 21% [13:42:35] main.c:19> Animation progress: 26% ... [13:42:35] main.c:19> Animation progress: 85% [13:42:35] main.c:19> Animation progress: 88% [13:42:35] main.c:19> Animation progress: 91% [13:42:35] main.c:19> Animation progress: 93% [13:42:35] main.c:19> Animation progress: 95% [13:42:35] main.c:19> Animation progress: 97% [13:42:35] main.c:19> Animation progress: 98% [13:42:35] main.c:19> Animation progress: 99% [13:42:35] main.c:19> Animation progress: 99% [13:42:35] main.c:19> Animation progress: 100% [13:42:35] main.c:23> Animation finished! ``` ## Timers [`AppTimer`](https://developer.repebble.com/docs/c/Foundation/Timer.md "Timer") objects can be used to schedule updates to variables and objects at a later time. They can be used to implement frame-by-frame animations as an alternative to using the [`Animation`](https://developer.repebble.com/docs/c/User_Interface/Animation.md "Animation") API. They can also be used in a more general way to schedule events to occur at some point in the future (such as UI updates) while the app is open. A thread-blocking alternative for small pauses is [`psleep()`](https://developer.repebble.com/docs/c/Foundation/Timer.md#psleep "psleep"), but this is **not** recommended for use in loops updating UI (such as a counter), or for scheduling [`AppMessage`](https://developer.repebble.com/docs/c/Foundation/AppMessage.md "AppMessage") messages, which rely on the event loop to do their work. > Note: To create timed events in the future that persist after an app is closed, check out the [`Wakeup`](https://developer.repebble.com/docs/c/Foundation/Wakeup.md "Wakeup") API. When a timer elapses, it will call a developer-defined [`AppTimerCallback`](https://developer.repebble.com/docs/c/Foundation/Timer.md#AppTimerCallback "AppTimerCallback"). This is where the code to be executed after the timed interval should be placed. The callback will only be called once, so use this opportunity to re-register the timer if it should repeat. ``` static void timer\_callback(void \*context) {APP\_LOG(APP\_LOG\_LEVEL\_INFO, "Timer elapsed!");} ``` Schedule the timer with a specific `delay` interval, the name of the callback to fire, and an optional context pointer: ``` const int delay\_ms = 5000;// Schedule the timerapp\_timer\_register(delay\_ms, timer\_callback, NULL); ``` If the timer may need to be cancelled or rescheduled at a later time, ensure a reference to it is kept for later use: ``` static AppTimer \*s\_timer; ``` ``` // Register the timer, and keep a handle to its\_timer = app\_timer\_register(delay\_ms, timer\_callback, NULL); ``` If the timer needs to be cancelled, use the previous reference. If it has already elapsed, nothing will occur: ``` // Cancel the timerapp\_timer\_cancel(s\_timer); ``` ## Sequence and Spawn Animations The Pebble SDK also includes the capability to build up composite animations built from other [`Animation`](https://developer.repebble.com/docs/c/User_Interface/Animation.md "Animation") objects. There are two types: a sequence animation and a spawn animation. - A sequence animation is a set of two or more other animations that are played out in **series** (one after another). For example, a pair of timed animations to show and hide a [`Layer`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#Layer "Layer"). - A spawn animation is a set of two or more other animations that are played out in **parallel**. A spawn animation acts the same as creating and starting two or more animations at the same time, but has the advantage that it can be included as part of a sequence animation. > Note: Composite animations can be composed of other composite animations. ### Important Considerations When incorporating an [`Animation`](https://developer.repebble.com/docs/c/User_Interface/Animation.md "Animation") into a sequence or spawn animation, there are a couple of points to note: - Any single animation cannot appear more than once in the list of animations used to create a more complex animation. - A composite animation assumes ownership of its component animations once it has been created. - Once an animation has been added to a composite animation, it becomes immutable. This means it can only be read, and not written to. Attempts to modify such an animation after it has been added to a composite animation will fail. - Once an animation has been added to a composite animation, it cannot then be used to build a different composite animation. ### Creating a Sequence Animation To create a sequence animation, first create the component [`Animation`](https://developer.repebble.com/docs/c/User_Interface/Animation.md "Animation") objects that will be used to build it. ``` // Create the first AnimationPropertyAnimation \*prop\_anim = property\_animation\_create\_layer\_frame(s\_layer, &start, &finish);Animation \*animation\_a = property\_animation\_get\_animation(prop\_anim);// Set some propertiesanimation\_set\_delay(animation\_a, 1000);animation\_set\_duration(animation\_a, 500);// Clone the first, modify the duration and reverse it.Animation \*animation\_b = animation\_clone(animation\_a);animation\_set\_reverse(animation\_b, true);animation\_set\_duration(animation\_b, 1000); ``` Use these component animations to create the sequence animation. You can either specify the components as a list or pass an array. Both approaches are shown below. #### Using a List You can specify up to 20 [`Animation`](https://developer.repebble.com/docs/c/User_Interface/Animation.md "Animation") objects as parameters to `animation_sequence_create()`. The list must always be terminated with `NULL` to mark the end. ``` // Create the sequenceAnimation \*sequence = animation\_sequence\_create(animation\_a, animation\_b, NULL);// Play the sequenceanimation\_schedule(sequence); ``` #### Using an Array You can also specify the component animations using a dynamically allocated array. Give this to `animation_sequence_create_from_array()` along with the size of the array. ``` const uint32\_t array\_length = 2;// Create the arrayAnimation \*\*arr = (Animation\*\*)malloc(array\_length \* sizeof(Animation\*));arr[0] = animation\_a;arr[1] = animation\_b;// Create the sequence, set to loop foreverAnimation \*sequence = animation\_sequence\_create\_from\_array(arr, array\_length);animation\_set\_play\_count(sequence, ANIMATION\_DURATION\_INFINITE);// Play the sequenceanimation\_schedule(sequence);// Destroy the arrayfree(arr); ``` ### Creating a Spawn Animation Creating a spawn animation is done in a very similiar way to a sequence animation. The animation is built up from component animations which are then all started at the same time. This simplifies the task of precisely timing animations that are designed to coincide. The first step is the same as for sequence animations, which is to create a number of component animations to be spawned together. ``` // Create the first animationAnimation \*animation\_a = animation\_create();animation\_set\_duration(animation\_a, 1000);// Clone the first, modify the duration and reverse it.Animation \*animation\_b = animation\_clone(animation\_a);animation\_set\_reverse(animation\_b, true);animation\_set\_duration(animation\_b, 300); ``` Next, the spawn animation is created in a similar manner to the sequence animation with a `NULL` terminated list of parameters: ``` // Create the spawn animationAnimation \*spawn = animation\_spawn\_create(animation\_a, animation\_b, NULL);// Play the animationanimation\_schedule(spawn); ``` Alternatively the spawn animation can be created with an array of [`Animation`](https://developer.repebble.com/docs/c/User_Interface/Animation.md "Animation") objects. ``` const uint32\_t array\_length = 2;// Create the arrayAnimation \*\*arr = (Animation\*\*)malloc(array\_length \* sizeof(Animation\*));arr[0] = animation\_a;arr[1] = animation\_b;// Create the sequence and set the play count to 3Animation \*spawn = animation\_spawn\_create\_from\_array(arr, array\_length);animation\_set\_play\_count(spawn, 3);// Play the spawn animationanimation\_schedule(spawn);// Destroy the arrayfree(arr); ``` --- # Drawing Primitives, Images and Text Source: https://developer.repebble.com/guides/graphics-and-animations/drawing-primitives-images-and-text/ While [`Layer`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#Layer "Layer") types such as [`TextLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/TextLayer.md "TextLayer") and [`BitmapLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/BitmapLayer.md "BitmapLayer") allow easy rendering of text and bitmaps, more precise drawing can be achieved through the use of the [`Graphics Context`](https://developer.repebble.com/docs/c/Graphics/Graphics_Context.md "Graphics Context") APIs. Custom drawing of primitive shapes such as line, rectangles, and circles is also supported. Clever use of these functions can remove the need to pre-prepare bitmap images for many UI elements and icons. ## Obtaining a Drawing Context All custom drawing requires a [`GContext`](https://developer.repebble.com/docs/c/Graphics/Graphics_Types.md#GContext "GContext") instance. These cannot be created, and are only available inside a [`LayerUpdateProc`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#LayerUpdateProc "LayerUpdateProc"). This update procedure is simply a function that is called when a [`Layer`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#Layer "Layer") is to be rendered, and is defined by the developer as opposed to the system. For example, a [`BitmapLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/BitmapLayer.md "BitmapLayer") is simply a [`Layer`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#Layer "Layer") with a [`LayerUpdateProc`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#LayerUpdateProc "LayerUpdateProc") abstracted away for convenience by the SDK. First, create the [`Layer`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#Layer "Layer") that will have a custom drawing procedure: ``` static Layer \*s\_canvas\_layer; ``` Allocate the [`Layer`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#Layer "Layer") during [`Window`](https://developer.repebble.com/docs/c/User_Interface/Window.md "Window") creation: ``` GRect bounds = layer\_get\_bounds(window\_get\_root\_layer(window));// Create canvas layers\_canvas\_layer = layer\_create(bounds); ``` Next, define the [`LayerUpdateProc`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#LayerUpdateProc "LayerUpdateProc") according to the function specification: ``` static void canvas\_update\_proc(Layer \*layer, GContext \*ctx) {// Custom drawing happens here!} ``` Assign this procedure to the canvas layer and add it to the [`Window`](https://developer.repebble.com/docs/c/User_Interface/Window.md "Window") to make it visible: ``` // Assign the custom drawing procedurelayer\_set\_update\_proc(s\_canvas\_layer, canvas\_update\_proc);// Add to Windowlayer\_add\_child(window\_get\_root\_layer(window), s\_canvas\_layer); ``` From now on, every time the [`Layer`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#Layer "Layer") needs to be redrawn (for example, if other layer geometry changes), the [`LayerUpdateProc`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#LayerUpdateProc "LayerUpdateProc") will be called to allow the developer to draw it. It can also be explicitly marked for redrawing at the next opportunity: ``` // Redraw this as soon as possiblelayer\_mark\_dirty(s\_canvas\_layer); ``` ## Drawing Primitive Shapes The [`Graphics Context`](https://developer.repebble.com/docs/c/Graphics/Graphics_Context.md "Graphics Context") API allows drawing and filling of lines, rectangles, circles, and arbitrary paths. For each of these, the colors of the output can be set using the appropriate function: ``` // Set the line colorgraphics\_context\_set\_stroke\_color(ctx, GColorRed);// Set the fill colorgraphics\_context\_set\_fill\_color(ctx, GColorBlue); ``` In addition, the stroke width and antialiasing mode can also be changed: ``` // Set the stroke width (must be an odd integer value)graphics\_context\_set\_stroke\_width(ctx, 5);// Disable antialiasing (enabled by default where available)graphics\_context\_set\_antialiased(ctx, false); ``` ### Lines Drawing a simple line requires only the start and end positions, expressed as [`GPoint`](https://developer.repebble.com/docs/c/Graphics/Graphics_Types.md#GPoint "GPoint") values: ``` GPoint start = GPoint(10, 10);GPoint end = GPoint(40, 60);// Draw a linegraphics\_draw\_line(ctx, start, end); ``` ### Rectangles Drawing a rectangle requires a bounding [`GRect`](https://developer.repebble.com/docs/c/Graphics/Graphics_Types.md#GRect "GRect"), as well as other parameters if it is to be filled: ``` GRect rect\_bounds = GRect(10, 10, 40, 60);// Draw a rectanglegraphics\_draw\_rect(ctx, rect\_bounds);// Fill a rectangle with rounded cornersint corner\_radius = 10;graphics\_fill\_rect(ctx, rect\_bounds, corner\_radius, GCornersAll); ``` It is also possible to draw a rounded unfilled rectangle: ``` // Draw outline of a rounded rectanglegraphics\_draw\_round\_rect(ctx, rect\_bounds, corner\_radius); ``` ### Circles Drawing a circle requries its center [`GPoint`](https://developer.repebble.com/docs/c/Graphics/Graphics_Types.md#GPoint "GPoint") and radius: ``` GPoint center = GPoint(25, 25);uint16\_t radius = 50;// Draw the outline of a circlegraphics\_draw\_circle(ctx, center, radius);// Fill a circlegraphics\_fill\_circle(ctx, center, radius); ``` In addition, it is possble to draw and fill arcs. In these cases, the [`GOvalScaleMode`](https://developer.repebble.com/docs/c/Graphics/Drawing_Primitives.md#GOvalScaleMode "GOvalScaleMode") determines how the shape is adjusted to fill the rectangle, and the cartesian angle values are transformed to preserve accuracy: ``` int32\_t angle\_start = DEG\_TO\_TRIGANGLE(0);int32\_t angle\_end = DEG\_TO\_TRIGANGLE(45);// Draw an arcgraphics\_draw\_arc(ctx, rect\_bounds, GOvalScaleModeFitCircle, angle\_start, angle\_end); ``` Lastly, a filled circle with a sector removed can also be drawn in a similar manner. The value of `inset_thickness` determines the inner inset size that is removed from the full circle: ``` uint16\_t inset\_thickness = 10; // Fill a radial section of a circlegraphics\_fill\_radial(ctx, rect\_bounds, GOvalScaleModeFitCircle, inset\_thickness,angle\_start, angle\_end); ``` For more guidance on using round elements in apps, watch the presentation given at the 2015 Developer Retreat on [developing for Pebble Time Round](https://www.youtube.com/watch?v=3a1V4n9HDvY). ## Bitmaps Manually drawing [`GBitmap`](https://developer.repebble.com/docs/c/Graphics/Graphics_Types.md#GBitmap "GBitmap") images with the [`Graphics Context`](https://developer.repebble.com/docs/c/Graphics/Graphics_Context.md "Graphics Context") API is a simple task, and has much in common with the alternative approach of using a [`BitmapLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/BitmapLayer.md "BitmapLayer") (which provides additional convenience funcionality). The first step is to load the image data from resources (read [_Images_](https://developer.repebble.com/guides/app-resources/images.md) to learn how to include images in a Pebble project): ``` static GBitmap \*s\_bitmap; ``` ``` // Load the image datas\_bitmap = gbitmap\_create\_with\_resource(RESOURCE\_ID\_EXAMPLE\_IMAGE); ``` When the appropriate [`LayerUpdateProc`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#LayerUpdateProc "LayerUpdateProc") is called, draw the image inside the desired rectangle: > Note: Unlike [`BitmapLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/BitmapLayer.md "BitmapLayer"), the image will be drawn relative to the [`Layer`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#Layer "Layer")'s origin, and not centered. ``` // Get the bounds of the imageGRect bitmap\_bounds = gbitmap\_get\_bounds(s\_bitmap);// Set the compositing mode (GCompOpSet is required for transparency)graphics\_context\_set\_compositing\_mode(ctx, GCompOpSet);// Draw the imagegraphics\_draw\_bitmap\_in\_rect(ctx, s\_bitmap, bitmap\_bounds); ``` Once the image is no longer needed (i.e.: the app is exiting), free the data: ``` // Destroy the image datagbitmap\_destroy(s\_bitmap); ``` ## Drawing Text Similar to the [`TextLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/TextLayer.md "TextLayer") UI component, a [`LayerUpdateProc`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#LayerUpdateProc "LayerUpdateProc") can also be used to draw text. Advantages can include being able to draw in multiple fonts with only one [`Layer`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#Layer "Layer") and combining text with other drawing operations. The first operation to perform inside the [`LayerUpdateProc`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#LayerUpdateProc "LayerUpdateProc") is to get or load the font to be used for drawing and set the text's color: ``` // Load the fontGFont font = fonts\_get\_system\_font(FONT\_KEY\_GOTHIC\_24\_BOLD);// Set the colorgraphics\_context\_set\_text\_color(ctx, GColorBlack); ``` Next, determine the bounds that will guide the text's position and overflow behavior. This can either be the size of the [`Layer`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#Layer "Layer"), or a more precise bounds of the text itself. This information can be useful for drawing multiple text items after one another with automatic spacing. ``` char \*text = "Example test string for the Developer Website guide!";// Determine a reduced bounding boxGRect layer\_bounds = layer\_get\_bounds(layer);GRect bounds = GRect(layer\_bounds.origin.x, layer\_bounds.origin.y,layer\_bounds.size.w / 2, layer\_bounds.size.h);// Calculate the size of the text to be drawn, with restricted spaceGSize text\_size = graphics\_text\_layout\_get\_content\_size(text, font, bounds,GTextOverflowModeWordWrap, GTextAlignmentCenter); ``` Finally, the text can be drawn into the appropriate bounding rectangle: ``` // Draw the textgraphics\_draw\_text(ctx, text, font, bounds, GTextOverflowModeWordWrap, GTextAlignmentCenter, NULL); ``` --- # Framebuffer Graphics Source: https://developer.repebble.com/guides/graphics-and-animations/framebuffer-graphics/ In the context of a Pebble app, the framebuffer is the data region used to store the contents of the what is shown on the display. Using the [`Graphics Context`](https://developer.repebble.com/docs/c/Graphics/Graphics_Context.md "Graphics Context") API allows developers to draw primitive shapes and text, but at a slower speed and with a restricted set of drawing patterns. Getting direct access to the framebuffer allows arbitrary transforms, special effects, and other modifications to be applied to the display contents, and allows drawing at a much greater speed than standard SDK APIs. ## Accessing the Framebuffer Access to the framebuffer can only be obtained during a [`LayerUpdateProc`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#LayerUpdateProc "LayerUpdateProc"), when redrawing is taking place. When the time comes to update the associated [`Layer`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#Layer "Layer"), the framebuffer can be obtained as a [`GBitmap`](https://developer.repebble.com/docs/c/Graphics/Graphics_Types.md#GBitmap "GBitmap"): ``` static void layer\_update\_proc(Layer \*layer, GContext \*ctx) {// Get the framebufferGBitmap \*fb = graphics\_capture\_frame\_buffer(ctx);// Manipulate the image data...// Finally, release the framebuffergraphics\_release\_frame\_buffer(ctx, fb);} ``` > Note: Once obtained, the framebuffer **must** be released back to the app so that it may continue drawing. The format of the data returned will vary by platform, as will the representation of a single pixel, shown in the table below. | Platform | Framebuffer Bitmap Format | Pixel Format | | --- | --- | --- | | Aplite | [`GBitmapFormat1Bit`](https://developer.repebble.com/docs/c/Graphics/Graphics_Types.md#GBitmapFormat1Bit "GBitmapFormat1Bit") | One bit (black or white) | | Basalt | [`GBitmapFormat8Bit`](https://developer.repebble.com/docs/c/Graphics/Graphics_Types.md#GBitmapFormat8Bit "GBitmapFormat8Bit") | One byte (two bits per color) | | Chalk | [`GBitmapFormat8BitCircular`](https://developer.repebble.com/docs/c/Graphics/Graphics_Types.md#GBitmapFormat8BitCircular "GBitmapFormat8BitCircular") | One byte (two bits per color) | | Diorite | [`GBitmapFormat1Bit`](https://developer.repebble.com/docs/c/Graphics/Graphics_Types.md#GBitmapFormat1Bit "GBitmapFormat1Bit") | One bit (black or white) | | Flint | [`GBitmapFormat1Bit`](https://developer.repebble.com/docs/c/Graphics/Graphics_Types.md#GBitmapFormat1Bit "GBitmapFormat1Bit") | One bit (black or white) | | Emery | [`GBitmapFormat8Bit`](https://developer.repebble.com/docs/c/Graphics/Graphics_Types.md#GBitmapFormat8Bit "GBitmapFormat8Bit") | One byte (two bits per color) | | Gabbro | [`GBitmapFormat8BitCircular`](https://developer.repebble.com/docs/c/Graphics/Graphics_Types.md#GBitmapFormat8BitCircular "GBitmapFormat8BitCircular") | One byte (two bits per color) | Gabbro reports [`GBitmapFormat8BitCircular`](https://developer.repebble.com/docs/c/Graphics/Graphics_Types.md#GBitmapFormat8BitCircular "GBitmapFormat8BitCircular") like Chalk, but its rows are not packed: the framebuffer is a full 260 x 260 byte rectangle, every row starts 260 bytes after the previous one, and only the `min_x` to `max_x` range of each row is inside the circle. Apps built for Chalk and run on Gabbro get the packed 180 x 180 Chalk layout instead. On both platforms [`gbitmap_get_bytes_per_row()`](https://developer.repebble.com/docs/c/Graphics/Graphics_Types.md#gbitmap_get_bytes_per_row "gbitmap\_get\_bytes\_per\_row") returns 0 for this format, so always use [`gbitmap_get_data_row_info()`](https://developer.repebble.com/docs/c/Graphics/Graphics_Types.md#gbitmap_get_data_row_info "gbitmap\_get\_data\_row\_info") to address pixels rather than a fixed stride. ## Modifying the Framebuffer Data Once the framebuffer has been captured, the underlying data can be manipulated on a row-by-row or even pixel-by-pixel basis. This data region can be obtained using [`gbitmap_get_data()`](https://developer.repebble.com/docs/c/Graphics/Graphics_Types.md#gbitmap_get_data "gbitmap\_get\_data"), but the recommended approach is to make use of [`gbitmap_get_data_row_info()`](https://developer.repebble.com/docs/c/Graphics/Graphics_Types.md#gbitmap_get_data_row_info "gbitmap\_get\_data\_row\_info") objects to cater for framebuffer formats (such as [`GBitmapFormat8BitCircular`](https://developer.repebble.com/docs/c/Graphics/Graphics_Types.md#GBitmapFormat8BitCircular "GBitmapFormat8BitCircular") on Chalk) where not every row is of the same width. The [`GBitmapDataRowInfo`](https://developer.repebble.com/docs/c/Graphics/Graphics_Types.md#GBitmapDataRowInfo "GBitmapDataRowInfo") object helps with this by providing a `min_x` and `max_x` value for each `y` used to build it. Using it on platforms with a regular rectangular framebuffer is still safe — `min_x` and `max_x` will simply span the full row width. To iterate over all rows and columns, safely avoiding those with irregular start and end indices, use two nested loops as shown below. The implementation of `set_pixel_color()` is shown in [_Getting and Setting Pixels_](#getting-and-setting-pixels): > Note: it is only necessary to call [`gbitmap_get_data_row_info()`](https://developer.repebble.com/docs/c/Graphics/Graphics_Types.md#gbitmap_get_data_row_info "gbitmap\_get\_data\_row\_info") once per row. Calling it more often (such as for every pixel) will incur a sigificant speed penalty. ``` GRect bounds = layer\_get\_bounds(layer);// Iterate over all rowsfor(int y = 0; y \< bounds.size.h; y++) {// Get this row's range and dataGBitmapDataRowInfo info = gbitmap\_get\_data\_row\_info(fb, y);// Iterate over all visible columnsfor(int x = info.min\_x; x \<= info.max\_x; x++) {// Manipulate the pixel at x,y...const GColor random\_color = (GColor){ .argb = rand() % 255 };// ...to be a random colorset\_pixel\_color(info, GPoint(x, y), random\_color);}} ``` ## Getting and Setting Pixels To modify a pixel's value, simply set a new value at the appropriate position in the `data` field of that row's [`GBitmapDataRowInfo`](https://developer.repebble.com/docs/c/Graphics/Graphics_Types.md#GBitmapDataRowInfo "GBitmapDataRowInfo") object. This will modify the underlying data, and update the display once the frame buffer is released. This process will be different depending on the [`GBitmapFormat`](https://developer.repebble.com/docs/c/Graphics/Graphics_Types.md#GBitmapFormat "GBitmapFormat") of the captured framebuffer. On a color platform, each pixel is stored as a single byte. However, on black and white platforms this will be one bit per byte. Using [`memset()`](https://developer.repebble.com/docs/c/Standard_C/Memory.md#memset "memset") to read or modify the correct pixel on a black and white display requires a bit more logic, shown below: ``` static GColor get\_pixel\_color(GBitmapDataRowInfo info, GPoint point) {#if defined(PBL\_COLOR)// Read the single byte color pixelreturn (GColor){ .argb = info.data[point.x] };#elif defined(PBL\_BW)// Read the single bit of the correct byteuint8\_t byte = point.x / 8;uint8\_t bit = point.x % 8; return byte\_get\_bit(&info.data[byte], bit) ? GColorWhite : GColorBlack;#endif} ``` Setting a pixel value is achieved in much the same way, with different logic depending on the format of the framebuffer on each platform: ``` static void set\_pixel\_color(GBitmapDataRowInfo info, GPoint point, GColor color) {#if defined(PBL\_COLOR)// Write the pixel's byte colormemset(&info.data[point.x], color.argb, 1);#elif defined(PBL\_BW)// Find the correct byte, then set the appropriate bituint8\_t byte = point.x / 8;uint8\_t bit = point.x % 8; byte\_set\_bit(&info.data[byte], bit, gcolor\_equal(color, GColorWhite) ? 1 : 0);#endif} ``` The `byte_get_bit()` and `byte_set_bit()` implementations are shown here for convenience: ``` static bool byte\_get\_bit(uint8\_t \*byte, uint8\_t bit) {return ((\*byte) \>\> bit) & 1;}static void byte\_set\_bit(uint8\_t \*byte, uint8\_t bit, uint8\_t value) {\*byte ^= (-value ^ \*byte) & (1 \<\< bit);} ``` ## Learn More To see an example of what can be achieved with direct access to the framebuffer and learn more about the underlying principles, watch the [talk given at the 2014 Developer Retreat](https://www.youtube.com/watch?v=lYoHh19RNy4). --- # Graphics and Animations Source: https://developer.repebble.com/guides/graphics-and-animations/ The Pebble SDK allows drawing of many types of graphics in apps. Using the [`Graphics Context`](https://developer.repebble.com/docs/c/Graphics/Graphics_Context.md "Graphics Context"), [`GBitmap`](https://developer.repebble.com/docs/c/Graphics/Graphics_Types.md#GBitmap "GBitmap"), [`GDrawCommand`](https://developer.repebble.com/docs/c/Graphics/Draw_Commands.md#GDrawCommand "GDrawCommand") and [`Framebuffer`](https://developer.repebble.com/docs/c/Graphics/Drawing_Primitives.md#graphics_capture_frame_buffer "graphics\_capture\_frame\_buffer") APIs gives developers complete control over the contents of the display, and also can be used to complement UI created with the various types of [`Layer`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#Layer "Layer") available to enable more complex UI layouts. Both image-based and property-based animations are available, as well as scheduling regular updates to UI components with [`Timer`](https://developer.repebble.com/docs/c/Foundation/Timer.md "Timer") objects. Creative use of [`Animation`](https://developer.repebble.com/docs/c/User_Interface/Animation.md "Animation") and [`PropertyAnimation`](https://developer.repebble.com/docs/c/User_Interface/Animation/PropertyAnimation.md "PropertyAnimation") can help to delight and engage users, as well as highlight important information inside apps. ## Contents - [**Animations**](https://developer.repebble.com/guides/graphics-and-animations/animations.md) - How to use Animations and Timers to add life to your app. - [**Drawing Primitives, Images and Text**](https://developer.repebble.com/guides/graphics-and-animations/drawing-primitives-images-and-text.md) - How to draw primitive shapes, image, and text onto the Graphics Context. - [**Framebuffer Graphics**](https://developer.repebble.com/guides/graphics-and-animations/framebuffer-graphics.md) - How to perform advanced drawing using direct framebuffer access. - [**Vector Graphics**](https://developer.repebble.com/guides/graphics-and-animations/vector-graphics.md) - How to draw simple images using vector images, instead of bitmaps. --- # Vector Graphics Source: https://developer.repebble.com/guides/graphics-and-animations/vector-graphics/ This is an overview of drawing vector images using Pebble Draw Command files. See the [_Vector Animations_](https://developer.repebble.com/tutorials/advanced/vector-animations.md) tutorial for more information. ## Vector Graphics on Pebble As opposed to bitmaps which contain data for every pixel to be drawn, a vector file contains only instructions about points contained in the image and how to draw lines connecting them up. Instructions such as fill color, stroke color, and stroke width are also included. Vector images on Pebble are implemented using the [`Draw Commands`](https://developer.repebble.com/docs/c/Graphics/Draw_Commands.md "Draw Commands") API, which allows apps to load and display PDC (Pebble Draw Command) images and sequences that contain sets of these instructions. An example is the weather icon used in weather timeline pins. The benefit of using vector graphics for this icon is that is allows the image to stretch in the familiar manner as it moves between the timeline view and the pin detail view: ![weather](https://developer.repebble.com/assets/images/tutorials/advanced/weather.png) The main benefits of vectors over bitmaps for simple images and icons are: - Smaller resource size - instructions for joining points are less memory expensive than per-pixel bitmap data. - Flexible rendering - vector images can be rendered as intended, or manipulated at runtime to move the individual points around. This allows icons to appear more organic and life-like than static PNG images. Scaling and distortion is also made possible. However, there are also some drawbacks to choosing vector images in certain cases: - Vector files require more specialized tools to create than bitmaps, and so are harder to produce. - Complicated vector files may take more time to render than if they were simply drawn per-pixel as a bitmap, depending on the drawing implementation. ## Creating Compatible Files The file format of vector image files on Pebble is the PDC (Pebble Draw Command) format, which includes all the instructions necessary to allow drawing of vectors. These files are created from compatible SVG (Scalar Vector Graphics) files. Read [_Converting SVG to PDC_](https://developer.repebble.com/guides/app-resources/converting-svg-to-pdc.md) for more information. Pebble Draw Command files can only be used from app resources, and cannot be created at runtime. ## Drawing Vector Graphics Add the PDC file to the project resources in `package.json` with the 'type' field to `raw`: ``` "media": [{"type": "raw","name": "EXAMPLE\_IMAGE","file": "example\_image.pdc"}] ``` Drawing a Pebble Draw Command image is just as simple as drawing a normal PNG image to a graphics context, requiring only one draw call. First, load the `.pdc` file from resources as shown below. First, declare a pointer of type [`GDrawCommandImage`](https://developer.repebble.com/docs/c/Graphics/Draw_Commands.md#GDrawCommandImage "GDrawCommandImage") at the top of the file: ``` static GDrawCommandImage \*s\_command\_image; ``` Create and assign the [`GDrawCommandImage`](https://developer.repebble.com/docs/c/Graphics/Draw_Commands.md#GDrawCommandImage "GDrawCommandImage") in `init()`, before calling `window_stack_push()`: ``` // Create the object from resource files\_command\_image = gdraw\_command\_image\_create\_with\_resource(RESOURCE\_ID\_EXAMPLE\_IMAGE); ``` Next, define the [`LayerUpdateProc`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#LayerUpdateProc "LayerUpdateProc") that will be used to draw the PDC image: ``` static void update\_proc(Layer \*layer, GContext \*ctx) {// Set the origin offset from the context for drawing the imageGPoint origin = GPoint(10, 20);// Draw the GDrawCommandImage to the GContextgdraw\_command\_image\_draw(ctx, s\_command\_image, origin);} ``` Next, create a [`Layer`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#Layer "Layer") to display the image: ``` static Layer \*s\_canvas\_layer; ``` Assign the [`LayerUpdateProc`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#LayerUpdateProc "LayerUpdateProc") that will do the rendering to the canvas [`Layer`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#Layer "Layer") and add it to the desired [`Window`](https://developer.repebble.com/docs/c/User_Interface/Window.md "Window") during `window_load()`: ``` // Create the canvas Layers\_canvas\_layer = layer\_create(GRect(30, 30, bounds.size.w, bounds.size.h));// Set the LayerUpdateProclayer\_set\_update\_proc(s\_canvas\_layer, update\_proc);// Add to parent Windowlayer\_add\_child(window\_layer, s\_canvas\_layer); ``` Finally, don't forget to free the memory used by the sub-components of the [`Window`](https://developer.repebble.com/docs/c/User_Interface/Window.md "Window") in `main_window_unload()`: ``` // Destroy the canvas Layerlayer\_destroy(s\_canvas\_layer);// Destroy the PDC imagegdraw\_command\_image\_destroy(s\_command\_image); ``` When run, the PDC image will be loaded, and rendered in the [`LayerUpdateProc`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#LayerUpdateProc "LayerUpdateProc"). To put the image into contrast, optionally change the [`Window`](https://developer.repebble.com/docs/c/User_Interface/Window.md "Window") background color after `window_create()`: ``` window\_set\_background\_color(s\_main\_window, GColorBlueMoon); ``` The result will look similar to the example shown below. ![weather-image](https://developer.repebble.com/assets/images/tutorials/advanced/weather-image.png) --- # App Configuration Source: https://developer.repebble.com/guides/user-interfaces/app-configuration/ Many watchfaces and watchapps in the Pebble appstore include the ability to customize their behavior or appearance through the use of a configuration page. [Clay for Pebble](https://github.com/pebble-dev/clay) is the recommended approach for creating configuration pages, and is what will be covered in this guide. If you need to host your own configuration pages, please follow our [_Manual Setup_](https://developer.repebble.com/guides/user-interfaces/app-configuration-static.md) guide. ![Clay Sample](https://developer.repebble.com/assets/images/guides/user-interfaces/app-configuration/clay-sample.png) Clay for Pebble dramatically simplifies the process of creating a configuration page, by allowing developers to define their application settings using a simple [JSON](https://en.wikipedia.org/wiki/JSON) file. Clay processes the JSON file and then dynamically generates a configuration page which matches the existing style of the Pebble mobile application, and it even works without an Internet connection. ## Enabling Configuration For an app to be configurable, it must include the 'configurable' item in `package.json`. ``` "capabilities": ["configurable"] ``` The presence of this value tells the mobile app to display the gear icon that is associated with the ability to launch the config page next to the app itself. ## Installing Clay Clay is available as a [_Pebble Package_](https://developer.repebble.com/guides/pebble-packages.md), so it takes minimal effort to install. Within your project folder, just type: ``` $ pebble package install @rebble/clay ``` ## Choosing messageKeys When passing data between the configuration page and the watch application, we define `messageKeys` to help us easily identify the different values. In this example, we're going to allow users to control the background color, foreground color, whether the watchface ticks on seconds and whether any animations are displayed. We define `messageKeys` in the `package.json` file for each configuration setting in our application: ``` "messageKeys": ["BackgroundColor","ForegroundColor","SecondTick","Animations"] ``` ## Creating the Clay Configuration Create the Clay configuration file as `src/pkjs/config.js`. It is not part of the project template, so `pebble new-project` and CloudPebble do not create it. It defines each type of HTML form entity that is required. These types include: - [Section](https://github.com/pebble-dev/clay#section) - [Heading](https://github.com/pebble-dev/clay#heading) - [Text](https://github.com/pebble-dev/clay#text) - [Input](https://github.com/pebble-dev/clay#input) - [Toggle](https://github.com/pebble-dev/clay#toggle) - [Select](https://github.com/pebble-dev/clay#select) - [Color Picker](https://github.com/pebble-dev/clay#color-picker) - [Radio Group](https://github.com/pebble-dev/clay#radio-group) - [Checkbox Group](https://github.com/pebble-dev/clay#checkbox-group) - [Generic Button](https://github.com/pebble-dev/clay#generic-button) - [Range Slider](https://github.com/pebble-dev/clay#range-slider) - [Submit Button](https://github.com/pebble-dev/clay#submit) In our example configuration page, we will add some introductory text, and group our fields into two sections. All configuration pages must have a submit button at the end, which is used to send the JSON data back to the watch. ![Clay](https://developer.repebble.com/assets/images/guides/user-interfaces/app-configuration/clay-actual.png) Now start populating the configuration file with the sections you require, then add the required elements to each section. Be sure to assign the correct `messageKey` to each field. ``` module.exports = [{"type": "heading","defaultValue": "App Configuration"},{"type": "text","defaultValue": "Here is some introductory text."},{"type": "section","items": [{"type": "heading","defaultValue": "Colors"},{"type": "color","messageKey": "BackgroundColor","defaultValue": "0x000000","label": "Background Color"},{"type": "color","messageKey": "ForegroundColor","defaultValue": "0xFFFFFF","label": "Foreground Color"}]},{"type": "section","items": [{"type": "heading","defaultValue": "More Settings"},{"type": "toggle","messageKey": "SecondTick","label": "Enable Seconds","defaultValue": false},{"type": "toggle","messageKey": "Animations","label": "Enable Animations","defaultValue": false}]},{"type": "submit","defaultValue": "Save Settings"}]; ``` ## Initializing Clay To initialize Clay, all you need to do is add the following JavaScript into your `index.js` file. ``` // Import the Clay packagevar Clay = require('@rebble/clay');// Load our Clay configuration filevar clayConfig = require('./config');// Initialize Clayvar clay = new Clay(clayConfig); ``` > When using the local SDK, it is possible to use a pure JSON configuration file (`config.json`). If this is the case, you must not include the `module.exports = []` in your configuration file, and you need to `var clayConfig = require('./config.json');` ## Receiving Config Data Within our watchapp we need to open a connection with [`AppMessage`](https://developer.repebble.com/docs/c/Foundation/AppMessage.md "AppMessage") to begin listening for data from Clay, and also provide a handler to process the data once it has been received. ``` void prv\_init(void) {// ...// Open AppMessage connectionapp\_message\_register\_inbox\_received(prv\_inbox\_received\_handler);app\_message\_open(128, 128);// ...} ``` Once triggered, our handler will receive a [`DictionaryIterator`](https://developer.repebble.com/docs/c/Foundation/Dictionary.md#DictionaryIterator "DictionaryIterator") containing [`Tuple`](https://developer.repebble.com/docs/c/Foundation/Dictionary.md#Tuple "Tuple") objects for each `messageKey`. Note that the key names need to be prefixed with `MESSAGE_KEY_`. ``` static void prv\_inbox\_received\_handler(DictionaryIterator \*iter, void \*context) {// Read color preferencesTuple \*bg\_color\_t = dict\_find(iter, MESSAGE\_KEY\_BackgroundColor);if(bg\_color\_t) {GColor bg\_color = GColorFromHEX(bg\_color\_t-\>value-\>int32);}Tuple \*fg\_color\_t = dict\_find(iter, MESSAGE\_KEY\_ForegroundColor);if(fg\_color\_t) {GColor fg\_color = GColorFromHEX(fg\_color\_t-\>value-\>int32);}// Read boolean preferencesTuple \*second\_tick\_t = dict\_find(iter, MESSAGE\_KEY\_SecondTick);if(second\_tick\_t) {bool second\_ticks = second\_tick\_t-\>value-\>int32 == 1;}Tuple \*animations\_t = dict\_find(iter, MESSAGE\_KEY\_Animations);if(animations\_t) {bool animations = animations\_t-\>value-\>int32 == 1;}} ``` ## Persisting Settings By default, Clay will persist your settings in localStorage within the mobile application. It is common practice to also save settings within the persistent storage on the watch. This creates a seamless experience for users launching your application, as their settings can be applied on startup. This means there isn't an initial delay while the settings are loaded from the phone. You could save each individual value within the persistent storage, or you could create a struct to hold all of your settings, and save that entire object. This has the benefit of simplicity, and because writing to persistent storage is slow, it also provides improved performance. ``` // Persistent storage key#define SETTINGS\_KEY 1// Define our settings structtypedef struct ClaySettings {GColor BackgroundColor;GColor ForegroundColor;bool SecondTick;bool Animations;} ClaySettings;// An instance of the structstatic ClaySettings settings;// AppMessage receive handlerstatic void prv\_inbox\_received\_handler(DictionaryIterator \*iter, void \*context) {// Assign the values to our structTuple \*bg\_color\_t = dict\_find(iter, MESSAGE\_KEY\_BackgroundColor);if (bg\_color\_t) {settings.BackgroundColor = GColorFromHEX(bg\_color\_t-\>value-\>int32);}// ...prv\_save\_settings();}// Save the settings to persistent storagestatic void prv\_save\_settings() {persist\_write\_data(SETTINGS\_KEY, &settings, sizeof(settings));} ``` You can see a complete implementation of persisting a settings struct in the [Pebble Clay Example](https://github.com/pebble-examples/clay-example). ## What's Next If you're thinking that Clay won't be as flexible as hand crafting your own configuration pages, you're mistaken. Developers can extend the functionality of Clay in a number of ways: - Define a [custom function](https://github.com/pebble-dev/clay#custom-function) to enhance the interactivity of the page. - [Override events](https://github.com/pebble-dev/clay#handling-the-showconfiguration-and-webviewclosed-events-manually) and transform the format of the data before it's transferred to the watch. - Create and share your own [custom components](https://github.com/pebble-dev/clay#custom-components). Why not find out more about [Clay for Pebble](https://github.com/pebble-dev/clay) and perhaps even [contribute](https://github.com/pebble-dev/clay/blob/master/CONTRIBUTING.md) to the project, it's open source! --- # App Exit Reason Source: https://developer.repebble.com/guides/user-interfaces/app-exit-reason/ Introduced in SDK v4.0, the [`AppExitReason`](https://developer.repebble.com/docs/c/Foundation/Exit_Reason.md#AppExitReason "AppExitReason") API allows developers to provide a reason when terminating their application. The system uses these reasons to determine where the user should be sent when the current application terminates. At present, there are only two [`AppExitReason`](https://developer.repebble.com/docs/c/Foundation/Exit_Reason.md#AppExitReason "AppExitReason") states when exiting an application, but this may change in future updates. ### APP\_EXIT\_NOT\_SPECIFIED This is the default state and when the current watchapp terminates. The user is returned to their previous location. If you do not specify an [`AppExitReason`](https://developer.repebble.com/docs/c/Foundation/Exit_Reason.md#AppExitReason "AppExitReason"), this state will be used automatically. ``` static void prv\_deinit() {// Optional, default behavior// App will exit to the previous location in the systemexit\_reason\_set(APP\_EXIT\_NOT\_SPECIFIED);} ``` ### APP\_EXIT\_ACTION\_PERFORMED\_SUCCESSFULLY This state is primarily provided for developers who are creating one click action applications. When the current watchapp terminates, the user is returned to the default watchface. ``` static void prv\_deinit() {// App will exit to default watchfaceexit\_reason\_set(APP\_EXIT\_ACTION\_PERFORMED\_SUCCESSFULLY);} ``` --- # AppGlance C API Source: https://developer.repebble.com/guides/user-interfaces/appglance-c/ An app's "glance" is the visual representation of a watchapp in the launcher and provides glanceable information to the user. The [`App Glance`](https://developer.repebble.com/docs/c/Foundation/App_Glance.md "App Glance") API, added in SDK 4.0, enables developers to programmatically set the icon and subtitle that appears alongside their app in the launcher. > The [`App Glance`](https://developer.repebble.com/docs/c/Foundation/App_Glance.md "App Glance") API is only applicable to watchapps, it is not supported by watchfaces. ## Glances and AppGlanceSlices An app's glance can change over time, and is defined by zero or more [`AppGlanceSlice`](https://developer.repebble.com/docs/c/Foundation/App_Glance.md#AppGlanceSlice "AppGlanceSlice") each consisting of a layout (including a subtitle and icon), as well as an expiration time. AppGlanceSlices are displayed in the order they were added, and will persist until their expiration time, or another call to [`app_glance_reload()`](https://developer.repebble.com/docs/c/Foundation/App_Glance.md#app_glance_reload "app\_glance\_reload"). > To create an [`AppGlanceSlice`](https://developer.repebble.com/docs/c/Foundation/App_Glance.md#AppGlanceSlice "AppGlanceSlice") with no expiration time, use [`APP_GLANCE_SLICE_NO_EXPIRATION`](https://developer.repebble.com/docs/c/Foundation/App_Glance.md#APP_GLANCE_SLICE_NO_EXPIRATION "APP\_GLANCE\_SLICE\_NO\_EXPIRATION") Developers can change their watchapp’s glance by calling the [`app_glance_reload()`](https://developer.repebble.com/docs/c/Foundation/App_Glance.md#app_glance_reload "app\_glance\_reload") method, which first clears any existing app glance slices, and then loads zero or more [`AppGlanceSlice`](https://developer.repebble.com/docs/c/Foundation/App_Glance.md#AppGlanceSlice "AppGlanceSlice") as specified by the developer. The [`app_glance_reload()`](https://developer.repebble.com/docs/c/Foundation/App_Glance.md#app_glance_reload "app\_glance\_reload") method is invoked with two parameters: a pointer to an [`AppGlanceReloadCallback`](https://developer.repebble.com/docs/c/Foundation/App_Glance.md#AppGlanceReloadCallback "AppGlanceReloadCallback") that will be invoked after the existing app glance slices have been cleared, and a pointer to context data. Developers can add new [`AppGlanceSlice`](https://developer.repebble.com/docs/c/Foundation/App_Glance.md#AppGlanceSlice "AppGlanceSlice") to their app's glance in the [`AppGlanceReloadCallback`](https://developer.repebble.com/docs/c/Foundation/App_Glance.md#AppGlanceReloadCallback "AppGlanceReloadCallback"). ``` // ...// app\_glance\_reload callbackstatic void prv\_update\_app\_glance(AppGlanceReloadSession \*session,size\_t limit, void \*context) {// Create and add app glance slices...}static void prv\_deinit() {// deinit code// ...// Reload the watchapp's app glanceapp\_glance\_reload(prv\_update\_app\_glance, NULL);} ``` ## The app\_glance\_reload Callback The [`app_glance_reload()`](https://developer.repebble.com/docs/c/Foundation/App_Glance.md#app_glance_reload "app\_glance\_reload") is invoked with 3 parameters, a pointer to an [`AppGlanceReloadSession`](https://developer.repebble.com/docs/c/Foundation/App_Glance.md#AppGlanceReloadSession "AppGlanceReloadSession") (which is used when invoking [`app_glance_add_slice()`](https://developer.repebble.com/docs/c/Foundation/App_Glance.md#app_glance_add_slice "app\_glance\_add\_slice")) , the maximum number of slices you are able to add (as determined by the system at run time), and a pointer to the context data that was passed into [`app_glance_reload()`](https://developer.repebble.com/docs/c/Foundation/App_Glance.md#app_glance_reload "app\_glance\_reload"). The context data should contain all the information required to build the [`AppGlanceSlice`](https://developer.repebble.com/docs/c/Foundation/App_Glance.md#AppGlanceSlice "AppGlanceSlice"), and is typically cast to a specific type before being used. > The `limit` is currently set to 8 app glance slices per watchapp, though there is no guarantee that this value will remain static, and developers should always ensure they are not adding more slices than the limit. ![Hello World](https://developer.repebble.com/assets/images/guides/appglance-c/hello-world-app-glance.png) In this example, we’re passing the string we would like to set as the subtitle, by using the context parameter. The full code for this example can be found in the [AppGlance-Hello-World](https://github.com/pebble-examples/app-glance-hello-world) repository. ``` static void prv\_update\_app\_glance(AppGlanceReloadSession \*session,size\_t limit, void \*context) {// This should never happen, but developers should always ensure they are// not adding more slices than are availableif (limit \< 1) return;// Cast the context object to a stringconst char \*message = context;// Create the AppGlanceSlice// NOTE: When .icon is not set, the app's default icon is usedconst AppGlanceSlice entry = (AppGlanceSlice) {.layout = {.icon = APP\_GLANCE\_SLICE\_DEFAULT\_ICON,.subtitle\_template\_string = message},.expiration\_time = APP\_GLANCE\_SLICE\_NO\_EXPIRATION};// Add the slice, and check the resultconst AppGlanceResult result = app\_glance\_add\_slice(session, entry);if (result != APP\_GLANCE\_RESULT\_SUCCESS) {APP\_LOG(APP\_LOG\_LEVEL\_ERROR, "AppGlance Error: %d", result);}} ``` The `layout` member is an [`AppGlanceSliceLayout`](https://developer.repebble.com/docs/c/Foundation/App_Glance.md#AppGlanceSliceLayout "AppGlanceSliceLayout"). Its `icon` is the published resource ID of the bitmap to show, or [`APP_GLANCE_SLICE_DEFAULT_ICON`](https://developer.repebble.com/docs/c/Foundation/App_Glance.md#APP_GLANCE_SLICE_DEFAULT_ICON "APP\_GLANCE\_SLICE\_DEFAULT\_ICON") for the app's own icon, and its `subtitle_template_string` is the text shown under the app name, or `NULL` for no text. > **NOTE:** When an [`AppGlanceSlice`](https://developer.repebble.com/docs/c/Foundation/App_Glance.md#AppGlanceSlice "AppGlanceSlice") is loaded with the [`app_glance_add_slice()`](https://developer.repebble.com/docs/c/Foundation/App_Glance.md#app_glance_add_slice "app\_glance\_add\_slice") method, the slice's `layout.subtitle_template_string` is copied to the app's glance, meaning the string does not need to persist after the call to [`app_glance_add_slice()`](https://developer.repebble.com/docs/c/Foundation/App_Glance.md#app_glance_add_slice "app\_glance\_add\_slice") is made. ## Using Custom Icons In order to use custom icons within an [`AppGlanceSlice`](https://developer.repebble.com/docs/c/Foundation/App_Glance.md#AppGlanceSlice "AppGlanceSlice"), you need to use the new `publishedMedia` entry in the `package.json` file. - Create your images as 25px x 25px PNG files. - Add your images as media resources in the `package.json`. - Then add the `publishedMedia` declaration. You should end up with something like this: ``` "resources": {"media": [{"name": "WEATHER\_HOT\_ICON\_TINY","type": "bitmap","file": "hot\_tiny.png"}],"publishedMedia": [{"name": "WEATHER\_HOT","id": 1,"glance": "WEATHER\_HOT\_ICON\_TINY"}]} ``` Then you can reference the `icon` by `name` in your [`AppGlanceSlice`](https://developer.repebble.com/docs/c/Foundation/App_Glance.md#AppGlanceSlice "AppGlanceSlice"). You must use the prefix `PUBLISHED_ID_`. E.g. `PUBLISHED_ID_WEATHER_HOT`. ## Subtitle Template Strings The `subtitle_template_string` field provides developers with a string formatting language for app glance subtitles. Developers can create a single app glance slice which updates automatically based upon a timestamp. For example, the template can be used to create a countdown until a timestamp (`time_until`), or the duration since a timestamp (`time_since`). The result from the timestamp evaluation can be output in various different time-format's, such as: - It's 50 days until New Year - Your Uber will arrive in 5 minutes - You are 15515 days old ### Template Structure The template string has the following structure: `{evaluation(timestamp)|format(parameters)}` Let's take a look at a simple countdown example: `Your Uber will arrive in 1 hr 10 min 4 sec` In this example, we need to know the time until our timestamp: `time_until(1467834606)`, then output the duration using an abbreviated time-format: `%aT`. `Your Uber will arrive in {time_until(1467834606)|format('%aT')}` ### Format Parameters Each format parameter is comprised of an optional predicate, and a time-format, separated by a colon. The time-format parameter is only output if the predicate evaluates to true. If a predicate is not supplied, the time-format is output by default. `format(predicate:'time-format')` #### Predicate The predicates are composed of a comparator and time value. For example, the difference between `now` and the timestamp evaluation is: - `>1d` Greater than 1 day - `<12m` Less than 12 months - `>=6m` Greater than or equal to 6 months - `<=1d12h` Less than or equal to 1 day, 12 hours. The supported time units are: - `d` (Day) - `H` (Hour) - `M` (Minute) - `S` (Second) #### Time Format The time-format is a single quoted string, comprised of a percent sign and an optional format flag, followed by a time unit. For example: `'%aT'` Abbreviated time. e.g. 1 hr 10 min 4 sec The optional format flags are: - `a` Adds abbreviated units (translated and with proper pluralization) (overrides 'u' flag) - `u` Adds units (translated and with proper pluralization) (overrides 'a' flag) - `-` Negates the input for this format specifier - `0` Pad value to the "expected" number of digits with zeros - `f` Do not modulus the value The following table demonstrates sample output for each time unit, and the effects of the format flags. | Time Unit | No flag | 'u' flag | 'a' flag | '0' flag | 'f' flag | | --- | --- | --- | --- | --- | --- | | **y** | \ | \ year(s) | \ yr(s) | \ | \ | | output: | 4 | 4 years | 4 yr | 04 | 4 | | **m** | \ | \ month(s) | \ mo(s) | \ | \ | | output: | 8 | 8 months | 8 mo | 08 | 16 | | **d** | \ | \ days | \ d | \ | \ | | output: | 7 | 7 days | 7 d | 07 | 38 | | **H** | \ | \ hour(s) | \ hr | \ | \ | | output: | 1 | 1 hour | 1 hr | 01 | 25 | | **M** | \ | \ minute(s) | \ min | \ | \ | | output: | 22 | 22 minutes | 22 min | 22 | 82 | | **S** | \ | \ second(s) | \ sec | \ | \ | | output: | 5 | 5 seconds | 5 sec | 05 | 65 | | **T** | %H:%0M:%0S (if \>= 1hr) * * * %M:%0S (if \>= 1m) * * * %S (otherwise) | %uH, %uM, and %uS * * * %uM, and %uS * * * %uS | %aH %aM %aS * * * %aM %aS * * * %aS | %0H:%0M:%0S (always) | %fH:%0M:%0S * * * %M:%0S * * * %S | | output: | 1:53:20 * * * 53:20 * * * 20 | 1 hour, 53 minutes, and 20 seconds * * * 53 minutes, and 20 seconds * * * 20 seconds | 1 hr 53 min 20 sec * * * 53 min 20 sec * * * 20 sec | 01:53:20 * * * 00:53:20 * * * 00:00:20 | 25:53:20 * * * 53:20 * * * 20 | | **R** | %H:%0M (if \>= 1hr) * * * %M (otherwise) | %uH, and %uM * * * %uM | %aH %aM * * * %aM | %0H:%0M (always) | %fH:%0M * * * %M | | output: | 23:04 * * * 15 | 23 hours, and 4 minutes * * * 15 minutes | 23 hr 4 min * * * 15 min | 23:04 * * * 00:15 | 47:04 * * * 15 | > Note: The time units listed above are not all available for use as predicates, but can be used with format flags. #### Advanced Usage We've seen how to use a single parameter to generate our output, but for more advanced cases, we can chain multiple parameters together. This allows for a single app glance slice to produce different output as each parameter evaluates successfully, from left to right. `format(predicate:'time-format', predicate:'time-format', predicate:'time-format')` For example, we can generate a countdown which displays different output before, during and after the event: - 100 days left - 10 hr 5 min 20 sec left - It's New Year! - 10 days since New Year To produce this output we could use the following template: `{time_until(1483228800)|format(>=1d:'%ud left',>0S:'%aT left',>-1d:\"It's New Year!\", '%-ud since New Year')}` ## Adding Multiple Slices An app's glance can change over time, with the slices being displayed in the order they were added, and removed after the `expiration_time`. In order to add multiple app glance slices, we simply need to create and add multiple [`AppGlanceSlice`](https://developer.repebble.com/docs/c/Foundation/App_Glance.md#AppGlanceSlice "AppGlanceSlice") instances, with increasing expiration times. ![Virtual Pet](https://developer.repebble.com/assets/images/guides/appglance-c/virtual-pet-app-glance.png) In the following example, we create a basic virtual pet that needs to be fed (by opening the app) every 12 hours, or else it runs away. When the app closes, we update the app glance to display a new message and icon every 3 hours until the virtual pet runs away. The full code for this example can be found in the [AppGlance-Virtual-Pet](https://github.com/pebble-examples/app-glance-virtual-pet) repository. ``` // How often pet needs to be fed (12 hrs)#define PET\_FEEDING\_FREQUENCY 3600\*12// Number of states to show in the launcher#define NUM\_STATES 4// Icons associated with each stateconst uint32\_t icons[NUM\_STATES] = {PUBLISHED\_ID\_ICON\_FROG\_HAPPY,PUBLISHED\_ID\_ICON\_FROG\_HUNGRY,PUBLISHED\_ID\_ICON\_FROG\_VERY\_HUNGRY,PUBLISHED\_ID\_ICON\_FROG\_MISSING};// Message associated with each stateconst char \*messages[NUM\_STATES] = {"Mmm, that was delicious!!","I'm getting hungry..","I'm so hungry!! Please feed me soon..","Your pet ran away :("};static void prv\_update\_app\_glance(AppGlanceReloadSession \*session,size\_t limit, void \*context) {// Ensure we have sufficient slicesif (limit \< NUM\_STATES) {APP\_LOG(APP\_LOG\_LEVEL\_DEBUG, "Error: app needs %d slices (%zu available)",NUM\_STATES, limit);}time\_t expiration\_time = time(NULL);// Build and add NUM\_STATES slicesfor (int i = 0; i \< NUM\_STATES; i++) {// Increment the expiration\_time of the slice on each passexpiration\_time += PET\_FEEDING\_FREQUENCY / NUM\_STATES;// Set it so the last slice never expiresif (i == (NUM\_STATES - 1)) expiration\_time = APP\_GLANCE\_SLICE\_NO\_EXPIRATION;// Create the sliceconst AppGlanceSlice slice = {.layout = {.icon = icons[i],.subtitle\_template\_string = messages[i]},.expiration\_time = expiration\_time};// add the slice, and check the resultAppGlanceResult result = app\_glance\_add\_slice(session, slice);if (result != APP\_GLANCE\_RESULT\_SUCCESS) {APP\_LOG(APP\_LOG\_LEVEL\_ERROR, "Error adding AppGlanceSlice: %d", result);}}}static void prv\_deinit() {app\_glance\_reload(prv\_update\_app\_glance, NULL);}void main() {app\_event\_loop();prv\_deinit();} ``` --- # AppGlance in PebbleKit JS Source: https://developer.repebble.com/guides/user-interfaces/appglance-pebblekit-js/ > Note: `Pebble.appGlanceReload()` is not supported in the Pebble mobile app and its callbacks never fire. Use the [_C AppGlance API_](https://developer.repebble.com/guides/user-interfaces/appglance-c.md) from the watchapp instead. The [_AppGlance C API_](https://developer.repebble.com/guides/user-interfaces/appglance-c.md) guide describes how to set the icon and subtitle shown alongside an app in the launcher with [`app_glance_reload()`](https://developer.repebble.com/docs/c/Foundation/App_Glance.md#app_glance_reload "app\_glance\_reload"), including the slice format, system and published media icons, and the subtitle template string language. --- # Content Size Source: https://developer.repebble.com/guides/user-interfaces/content-size/ The [ContentSize](https://developer.repebble.com/docs/c/User_Interface/Preferences.md#preferred_content_size) API, added in SDK 4.2, allows developers to dynamically adapt their watchface and watchapp design based upon the system `Text Size` preference (_Settings \> Notifications \> Text Size_). While this allows developers to create highly accessible designs, it also serves to provide a mechanism for creating designs which are less focused upon screen size, and more focused upon content size. ![ContentSize](https://developer.repebble.com/assets/images/guides/content-size/anim.gif) The `Text Size` setting displays the following options on all platforms: - Small - Medium - Large Whereas, the [ContentSize](https://developer.repebble.com/docs/c/User_Interface/Preferences.md#preferred_content_size) API will return different content sizes based on the `Text Size` setting, varying by platform. The list of content sizes is: - Small - Medium - Large - Extra Large An example of the varying content sizes: - `Text Size`: `small` on `Basalt` is `ContentSize`: `small` - `Text Size`: `small` on `Emery` is `ContentSize`: `medium` The following table describes the relationship between `Text Size`, `Platform` and `ContentSize`: | Platform | Text Size: Small | Text Size: Medium | Text Size: Large | | --- | --- | --- | --- | | Aplite, Basalt, Chalk, Diorite, Flint | ContentSize: Small | ContentSize: Medium | ContentSize: Large | | Emery, Gabbro | ContentSize: Medium | ContentSize: Large | ContentSize: Extra Large | > _At present the Text Size setting only affects notifications and some system UI components, but other system UI components will be updated to support ContentSize in future versions._ We highly recommend that developers begin to build and update their applications with consideration for [ContentSize](https://developer.repebble.com/docs/c/User_Interface/Preferences.md#preferred_content_size) to provide the best experience to users. ## Detecting ContentSize In order to detect the current [ContentSize](https://developer.repebble.com/docs/c/User_Interface/Preferences.md#preferred_content_size) developers can use the [`preferred_content_size()`](https://developer.repebble.com/docs/c/User_Interface/Preferences.md#preferred_content_size "preferred\_content\_size") function. The [ContentSize](https://developer.repebble.com/docs/c/User_Interface/Preferences.md#preferred_content_size) will never change during runtime, so it's perfectly acceptable to check this once during `init()`. ``` static PreferredContentSize s\_content\_size;void init() {s\_content\_size = preferred\_content\_size();// ...} ``` ## Adapting Layouts There are a number of different approaches to adapting the screen layout based upon content size. You could change font sizes, show or hide design elements, or even present an entirely different UI. In the following example, we will change font sizes based on the [ContentSize](https://developer.repebble.com/docs/c/User_Interface/Preferences.md#preferred_content_size) ``` static TextLayer \*s\_text\_layer;static PreferredContentSize s\_content\_size;void init() {s\_content\_size = preferred\_content\_size();// ...switch (s\_content\_size) {case PreferredContentSizeMedium:// Use a medium fonttext\_layer\_set\_font(s\_text\_layer, fonts\_get\_system\_font(FONT\_KEY\_GOTHIC\_18\_BOLD));break;case PreferredContentSizeLarge:case PreferredContentSizeExtraLarge:// Use a large fonttext\_layer\_set\_font(s\_text\_layer, fonts\_get\_system\_font(FONT\_KEY\_GOTHIC\_28\_BOLD));break;default:// Use a small fonttext\_layer\_set\_font(s\_text\_layer, fonts\_get\_system\_font(FONT\_KEY\_GOTHIC\_14\_BOLD));break;}// ...} ``` ## Additional Considerations When developing an application which dynamically adjusts based on the [ContentSize](https://developer.repebble.com/docs/c/User_Interface/Preferences.md#preferred_content_size) setting, try to avoid using fixed widths and heights. Calculate coordinates and dimensions based upon the size of the root layer, [`UnobstructedArea`](https://developer.repebble.com/docs/c/User_Interface/UnobstructedArea.md "UnobstructedArea") and [ContentSize](https://developer.repebble.com/docs/c/User_Interface/Preferences.md#preferred_content_size) --- # Layers Source: https://developer.repebble.com/guides/user-interfaces/layers/ The [`Layer`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#Layer "Layer") and associated subclasses (such as [`TextLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/TextLayer.md "TextLayer") and [`BitmapLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/BitmapLayer.md "BitmapLayer")) form the foundation of the UI for every Pebble watchapp or watchface, and are added to a [`Window`](https://developer.repebble.com/docs/c/User_Interface/Window.md "Window") to construct the UI's design. Each [`Layer`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#Layer "Layer") type contains at least three basic elements: - Frame - contains the position and dimensions of the [`Layer`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#Layer "Layer"), relative to the parent object. - Bounds - contains the drawable bounding box within the frame. This allows only a portion of the layer to be visible, and is relative to the [`Layer`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#Layer "Layer") frame. - Update procedure - the function that performs the drawing whenever the [`Layer`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#Layer "Layer") is rendered. The subclasses implement a convenience update procedure with additional data to achieve their specialization. ## Layer Hierarchy Every app must consist of at least one [`Window`](https://developer.repebble.com/docs/c/User_Interface/Window.md "Window") in order to successfully launch. Mutiple [`Layer`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#Layer "Layer") objects are added as children of the [`Window`](https://developer.repebble.com/docs/c/User_Interface/Window.md "Window"), which itself contains a [`Layer`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#Layer "Layer") known as the 'root layer'. When the [`Window`](https://developer.repebble.com/docs/c/User_Interface/Window.md "Window") is rendered, each child [`Layer`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#Layer "Layer") is rendered in the order in which they were added. For example: ``` static Window \*s\_main\_window;static BitmapLayer \*s\_background\_layer;static TextLayer \*s\_time\_layer; ``` ``` // Get the Window's root layerLayer \*root\_layer = window\_get\_root\_layer(s\_main\_window);/\* set up BitmapLayer and TextLayer \*/// Add the background layer first, so that it is drawn behind the timelayer\_add\_child(root\_layer, bitmap\_layer\_get\_layer(s\_background\_layer));// Add the time layer secondlayer\_add\_child(root\_layer, text\_layer\_get\_layer(s\_time\_layer)); ``` Once added to a [`Window`](https://developer.repebble.com/docs/c/User_Interface/Window.md "Window"), the ordering of each [`Layer`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#Layer "Layer") cannot be modified, but one can be placed at the front by removing and re-adding it to the hierarchy: ``` // Bring a layer to the frontlayer\_remove\_from\_parent(s\_some\_layer);layer\_add\_child(root\_layer, s\_some\_layer); ``` ## Update Procedures For creating custom drawing implementations, the basic [`Layer`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#Layer "Layer") update procedure can be reassigned to one created by a developer. This takes the form of a [`LayerUpdateProc`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#LayerUpdateProc "LayerUpdateProc"), and provides a [`GContext`](https://developer.repebble.com/docs/c/Graphics/Graphics_Context.md "Graphics Context") object which can be used for drawing primitive shapes, paths, text, and images. > Note: See [_Graphics and Animations_](https://developer.repebble.com/guides/graphics-and-animations.md) for more information on drawing with the graphics context. ``` static void layer\_update\_proc(Layer \*layer, GContext \*ctx) {// Custom drawing happens here} ``` This function must then be assigned to the [`Layer`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#Layer "Layer") that will be drawn with it: ``` // Set this Layer's update procedurelayer\_set\_update\_proc(s\_some\_layer, layer\_update\_proc); ``` The update procedure will be called every time the [`Layer`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#Layer "Layer") must be redrawn. This is typically when any other [`Layer`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#Layer "Layer") requests a redraw, the [`Window`](https://developer.repebble.com/docs/c/User_Interface/Window.md "Window") is shown/hidden, the hierarchy changes, or a modal (such as a notification) appears. The [`Layer`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#Layer "Layer") can also be manually marked as 'dirty', and will be redrawn at the next opportunity (usually immediately): ``` // Request a redrawlayer\_mark\_dirty(s\_some\_layer); ``` ## Layer Subclasses For convenience, there are multiple subclasses of [`Layer`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#Layer "Layer") included in the Pebble SDK to allow developers to easily construct their app's UI. Each should be created when the [`Window`](https://developer.repebble.com/docs/c/User_Interface/Window.md "Window") is loading (using the `.load` [`WindowHandler`](https://developer.repebble.com/docs/c/User_Interface/Window.md#WindowHandler "WindowHandler")) and destroyed when it is unloading (using the `.unload` [`WindowHandler`](https://developer.repebble.com/docs/c/User_Interface/Window.md#WindowHandler "WindowHandler")). These are briefly outlined below, alongside a simple usage example split into three code snippets - the element declarations, the setup procedure, and the teardown procedure. ### TextLayer The [`TextLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/TextLayer.md "TextLayer") is the most commonly used subclass of [`Layer`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#Layer "Layer"), and allows apps to render text using any available font, with built-in behavior to handle text color, line wrapping, alignment, etc. ``` static TextLayer \*s\_text\_layer; ``` ``` // Create a TextLayers\_text\_layer = text\_layer\_create(bounds);// Set some propertiestext\_layer\_set\_text\_color(s\_text\_layer, GColorWhite);text\_layer\_set\_background\_color(s\_text\_layer, GColorBlack);text\_layer\_set\_overflow\_mode(s\_text\_layer, GTextOverflowModeWordWrap);text\_layer\_set\_alignment(s\_text\_layer, GTextAlignmentCenter);// Set the text showntext\_layer\_set\_text(s\_text\_layer, "Hello, World!");// Add to the Windowlayer\_add\_child(root\_layer, text\_layer\_get\_layer(s\_text\_layer)); ``` ``` // Destroy the TextLayertext\_layer\_destroy(s\_text\_layer); ``` ### BitmapLayer The [`BitmapLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/BitmapLayer.md "BitmapLayer") provides an easy way to show images loaded into [`GBitmap`](https://developer.repebble.com/docs/c/Graphics/Graphics_Types.md#GBitmap "GBitmap") objects from an image resource. Images shown using a [`BitmapLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/BitmapLayer.md "BitmapLayer") are automatically centered within the bounds provided to [`bitmap_layer_create()`](https://developer.repebble.com/docs/c/User_Interface/Layers/BitmapLayer.md#bitmap_layer_create "bitmap\_layer\_create"). Read [_Images_](https://developer.repebble.com/guides/app-resources/images.md) to learn more about using image resources in apps. > Note: PNG images with transparency should use `bitmap` resource type, and use the [`GCompOpSet`](https://developer.repebble.com/docs/c/Graphics/Graphics_Types.md#GCompOpSet "GCompOpSet") compositing mode when being displayed, as shown below. ``` static BitmapLayer \*s\_bitmap\_layer;static GBitmap \*s\_bitmap; ``` ``` // Load the images\_bitmap = gbitmap\_create\_with\_resource(RESOURCE\_ID\_EXAMPLE\_IMAGE);// Create a BitmapLayers\_bitmap\_layer = bitmap\_layer\_create(bounds);// Set the bitmap and compositing modebitmap\_layer\_set\_bitmap(s\_bitmap\_layer, s\_bitmap);bitmap\_layer\_set\_compositing\_mode(s\_bitmap\_layer, GCompOpSet);// Add to the Windowlayer\_add\_child(root\_layer, bitmap\_layer\_get\_layer(s\_bitmap\_layer)); ``` ``` // Destroy the BitmapLayerbitmap\_layer\_destroy(s\_bitmap\_layer); ``` ### StatusBarLayer If a user needs to see the current time inside an app (instead of exiting to the watchface), the [`StatusBarLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/StatusBarLayer.md "StatusBarLayer") component can be used to display this information at the top of the [`Window`](https://developer.repebble.com/docs/c/User_Interface/Window.md "Window"). Colors and separator display style can be customized. ``` static StatusBarLayer \*s\_status\_bar; ``` ``` // Create the StatusBarLayers\_status\_bar = status\_bar\_layer\_create();// Set propertiesstatus\_bar\_layer\_set\_colors(s\_status\_bar, GColorBlack, GColorBlueMoon);status\_bar\_layer\_set\_separator\_mode(s\_status\_bar, StatusBarLayerSeparatorModeDotted);// Add to Windowlayer\_add\_child(root\_layer, status\_bar\_layer\_get\_layer(s\_status\_bar)); ``` ``` // Destroy the StatusBarLayerstatus\_bar\_layer\_destroy(s\_status\_bar); ``` ### MenuLayer The [`MenuLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/MenuLayer.md "MenuLayer") allows the user to scroll a list of options using the Up and Down buttons, and select an option to trigger an action using the Select button. It differs from the other [`Layer`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#Layer "Layer") subclasses in that it makes use of a number of [`MenuLayerCallbacks`](https://developer.repebble.com/docs/c/User_Interface/Layers/MenuLayer.md#MenuLayerCallbacks "MenuLayerCallbacks") to allow the developer to fully control how it renders and behaves. Some minimum example callbacks are shown below: ``` static MenuLayer \*s\_menu\_layer; ``` ``` static uint16\_t get\_num\_rows\_callback(MenuLayer \*menu\_layer, uint16\_t section\_index, void \*context) {const uint16\_t num\_rows = 5;return num\_rows;}static void draw\_row\_callback(GContext \*ctx, const Layer \*cell\_layer, MenuIndex \*cell\_index, void \*context) {static char s\_buff[16];snprintf(s\_buff, sizeof(s\_buff), "Row %d", (int)cell\_index-\>row);// Draw this row's indexmenu\_cell\_basic\_draw(ctx, cell\_layer, s\_buff, NULL, NULL);}static int16\_t get\_cell\_height\_callback(struct MenuLayer \*menu\_layer, MenuIndex \*cell\_index, void \*context) {const int16\_t cell\_height = 44;return cell\_height;}static void select\_callback(struct MenuLayer \*menu\_layer, MenuIndex \*cell\_index, void \*context) {// Do something in response to the button press} ``` ``` // Create the MenuLayers\_menu\_layer = menu\_layer\_create(bounds);// Let it receive click eventsmenu\_layer\_set\_click\_config\_onto\_window(s\_menu\_layer, window);// Set the callbacks for behavior and renderingmenu\_layer\_set\_callbacks(s\_menu\_layer, NULL, (MenuLayerCallbacks) {.get\_num\_rows = get\_num\_rows\_callback,.draw\_row = draw\_row\_callback,.get\_cell\_height = get\_cell\_height\_callback,.select\_click = select\_callback,});// Add to the Windowlayer\_add\_child(root\_layer, menu\_layer\_get\_layer(s\_menu\_layer)); ``` ``` // Destroy the MenuLayermenu\_layer\_destroy(s\_menu\_layer); ``` ### ScrollLayer The [`ScrollLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/ScrollLayer.md "ScrollLayer") provides an easy way to use the Up and Down buttons to scroll large content that does not all fit onto the screen at the same time. The usage of this type differs from the others in that the [`Layer`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#Layer "Layer") objects that are scrolled are added as children of the [`ScrollLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/ScrollLayer.md "ScrollLayer"), which is then in turn added as a child of the [`Window`](https://developer.repebble.com/docs/c/User_Interface/Window.md "Window"). The [`ScrollLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/ScrollLayer.md "ScrollLayer") frame is the size of the 'viewport', while the content size determines how far the user can scroll in each direction. The example below shows a [`ScrollLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/ScrollLayer.md "ScrollLayer") scrolling some long text, the total size of which is calculated with [`graphics_text_layout_get_content_size()`](https://developer.repebble.com/docs/c/Graphics/Drawing_Text.md#graphics_text_layout_get_content_size "graphics\_text\_layout\_get\_content\_size") and used as the [`ScrollLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/ScrollLayer.md "ScrollLayer") content size. > Note: The scrolled [`TextLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/TextLayer.md "TextLayer") frame is relative to that of its parent, the [`ScrollLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/ScrollLayer.md "ScrollLayer"). ``` static TextLayer \*s\_text\_layer;static ScrollLayer \*s\_scroll\_layer; ``` ``` GFont font = fonts\_get\_system\_font(FONT\_KEY\_GOTHIC\_28\_BOLD);// Find the bounds of the scrolling textGRect shrinking\_rect = GRect(0, 0, bounds.size.w, 2000);char \*text = "Example text that is really really really really really \ really really really really really really long";GSize text\_size = graphics\_text\_layout\_get\_content\_size(text, font, shrinking\_rect, GTextOverflowModeWordWrap, GTextAlignmentLeft);GRect text\_bounds = bounds;text\_bounds.size.h = text\_size.h;// Create the TextLayers\_text\_layer = text\_layer\_create(text\_bounds);text\_layer\_set\_overflow\_mode(s\_text\_layer, GTextOverflowModeWordWrap);text\_layer\_set\_font(s\_text\_layer, font);text\_layer\_set\_text(s\_text\_layer, text);// Create the ScrollLayers\_scroll\_layer = scroll\_layer\_create(bounds);// Set the scrolling content sizescroll\_layer\_set\_content\_size(s\_scroll\_layer, text\_size);// Let the ScrollLayer receive click eventsscroll\_layer\_set\_click\_config\_onto\_window(s\_scroll\_layer, window);// Add the TextLayer as a child of the ScrollLayerscroll\_layer\_add\_child(s\_scroll\_layer, text\_layer\_get\_layer(s\_text\_layer));// Add the ScrollLayer as a child of the Windowlayer\_add\_child(root\_layer, scroll\_layer\_get\_layer(s\_scroll\_layer)); ``` ``` // Destroy the ScrollLayer and TextLayerscroll\_layer\_destroy(s\_scroll\_layer);text\_layer\_destroy(s\_text\_layer); ``` ### ActionBarLayer The [`ActionBarLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/ActionBarLayer.md "ActionBarLayer") allows apps to use the familiar black right-hand bar, featuring icons denoting the action that will occur when each button on the right hand side is pressed. For example, 'previous track', 'more actions', and 'next track' in the built-in Music app. For three or fewer actions, the [`ActionBarLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/ActionBarLayer.md "ActionBarLayer") can be more appropriate than a [`MenuLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/MenuLayer.md "MenuLayer") for presenting the user with a list of actionable options. Each action's icon must also be loaded into a [`GBitmap`](https://developer.repebble.com/docs/c/Graphics/Graphics_Types.md#GBitmap "GBitmap") object from app resources. The example below demonstrates show to set up an [`ActionBarLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/ActionBarLayer.md "ActionBarLayer") showing an up, down, and checkmark icon for each of the buttons. ``` static ActionBarLayer \*s\_action\_bar;static GBitmap \*s\_up\_bitmap, \*s\_down\_bitmap, \*s\_check\_bitmap; ``` ``` // Load icon bitmapss\_up\_bitmap = gbitmap\_create\_with\_resource(RESOURCE\_ID\_UP\_ICON);s\_down\_bitmap = gbitmap\_create\_with\_resource(RESOURCE\_ID\_DOWN\_ICON);s\_check\_bitmap = gbitmap\_create\_with\_resource(RESOURCE\_ID\_CHECK\_ICON);// Create ActionBarLayers\_action\_bar = action\_bar\_layer\_create();action\_bar\_layer\_set\_click\_config\_provider(s\_action\_bar, click\_config\_provider);// Set the iconsaction\_bar\_layer\_set\_icon(s\_action\_bar, BUTTON\_ID\_UP, s\_up\_bitmap);action\_bar\_layer\_set\_icon(s\_action\_bar, BUTTON\_ID\_DOWN, s\_down\_bitmap);action\_bar\_layer\_set\_icon(s\_action\_bar, BUTTON\_ID\_SELECT, s\_check\_bitmap);// Add to Windowaction\_bar\_layer\_add\_to\_window(s\_action\_bar, window); ``` ``` // Destroy the ActionBarLayeraction\_bar\_layer\_destroy(s\_action\_bar);// Destroy the icon GBitmapsgbitmap\_destroy(s\_up\_bitmap);gbitmap\_destroy(s\_down\_bitmap);gbitmap\_destroy(s\_check\_bitmap); ``` --- # Round App UI Source: https://developer.repebble.com/guides/user-interfaces/round-app-ui/ **PLATFORM NOTICE** This guide only applies to apps built to run on the Chalk and Gabbro platforms (Pebble Time Round and Pebble Round 2). > This guide is about creating round apps in code. For advice on designing a round app, read [_Round App Design_](https://developer.repebble.com/guides/design-and-interaction/in-the-round.md). With the addition of Pebble Time Round (the Chalk platform) to the Pebble family, developers face a new challenge - circular apps! With this display shape, traditional layouts will not display properly due to the obscuring of the corners. Another potential issue is the increased display resolution. Any UI elements that were not previously centered correctly (or drawn with hardcoded coordinates) will also display incorrectly. Pebble Round 2 (the Gabbro platform) is also round, with a 260 x 260 display instead of Chalk's 180 x 180. Everything in this guide applies to both. Use the `PBL_ROUND` define and the bounds of the root layer rather than the Chalk dimensions, so that one layout serves both round watches. The [`ACTION_BAR_WIDTH`](https://developer.repebble.com/docs/c/User_Interface/Layers/ActionBarLayer.md#ACTION_BAR_WIDTH "ACTION\_BAR\_WIDTH") constant is 40 pixels on both round platforms. However, the Pebble SDK provides additions and functionality to help developers cope with this way of thinking. In many cases, a round display can be an aesthetic advantage. An example of this is the traditional circular dial watchface, which has been emulated on Pebble many times, but also wastes corner space. With a round display, these watchfaces can look better than ever. ![time-dots](https://developer.repebble.com/assets/images/guides/pebble-apps/display-animations/time-dots.png) ## Detecting Display Shape The first step for any app wishing to correctly support both display shapes is to use the available compiler directives to conditionally create the UI. This can be done as shown below: ``` #if defined(PBL\_RECT)printf("This code is run on a rectangular display!");/\* Rectangular UI code \*/#elif defined(PBL\_ROUND)printf("This code is run on a round display!");/\* Round UI code \*/#endif ``` Another approach for single value selection is the [`PBL_IF_RECT_ELSE()`](https://developer.repebble.com/docs/c/Graphics/Graphics_Types.md#PBL_IF_RECT_ELSE "PBL\_IF\_RECT\_ELSE") and [`PBL_IF_ROUND_ELSE()`](https://developer.repebble.com/docs/c/Graphics/Graphics_Types.md#PBL_IF_ROUND_ELSE "PBL\_IF\_ROUND\_ELSE") macros, which accept two parameters for each of the respective round and rectangular cases. For example, [`PBL_IF_RECT_ELSE()`](https://developer.repebble.com/docs/c/Graphics/Graphics_Types.md#PBL_IF_RECT_ELSE "PBL\_IF\_RECT\_ELSE") will compile the first parameter on a rectangular display, and the second one otherwise: ``` // Conditionally print out the shape of the displayprintf("This is a %s display!", PBL\_IF\_RECT\_ELSE("rectangular", "round")); ``` ## Circular Drawing In addition to the older [`graphics_draw_circle()`](https://developer.repebble.com/docs/c/Graphics/Drawing_Primitives.md#graphics_draw_circle "graphics\_draw\_circle") and [`graphics_fill_circle()`](https://developer.repebble.com/docs/c/Graphics/Drawing_Primitives.md#graphics_fill_circle "graphics\_fill\_circle") functions, the Pebble SDK for the chalk platform contains additional functions to help draw shapes better suited for a round display. These include: - [`graphics_draw_arc()`](https://developer.repebble.com/docs/c/Graphics/Drawing_Primitives.md#graphics_draw_arc "graphics\_draw\_arc") - Draws a line arc clockwise between two angles within a given [`GRect`](https://developer.repebble.com/docs/c/Graphics/Graphics_Types.md#GRect "GRect") area, where 0° is the top of the circle. - [`graphics_fill_radial()`](https://developer.repebble.com/docs/c/Graphics/Drawing_Primitives.md#graphics_fill_radial "graphics\_fill\_radial") - Fills a circle clockwise between two angles within a given [`GRect`](https://developer.repebble.com/docs/c/Graphics/Graphics_Types.md#GRect "GRect") area, with adjustable inner inset radius allowing the creation of 'doughnut-esque' shapes. - [`gpoint_from_polar()`](https://developer.repebble.com/docs/c/Graphics/Drawing_Primitives.md#gpoint_from_polar "gpoint\_from\_polar") - Returns a [`GPoint`](https://developer.repebble.com/docs/c/Graphics/Graphics_Types.md#GPoint "GPoint") object describing a point given by a specified angle within a centered [`GRect`](https://developer.repebble.com/docs/c/Graphics/Graphics_Types.md#GRect "GRect"). In the Pebble SDK angles between `0` and `360` degrees are specified as values scaled between `0` and [`TRIG_MAX_ANGLE`](https://developer.repebble.com/docs/c/Foundation/Math.md#TRIG_MAX_ANGLE "TRIG\_MAX\_ANGLE") to preserve accuracy and avoid floating point math. These are most commonly used when dealing with drawing circles. To help with this conversion, developers can use the [`DEG_TO_TRIGANGLE()`](https://developer.repebble.com/docs/c/Foundation/Math.md#DEG_TO_TRIGANGLE "DEG\_TO\_TRIGANGLE") macro. An example function to draw the letter 'C' in a yellow color is shown below for use in a [`LayerUpdateProc`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#LayerUpdateProc "LayerUpdateProc"). ``` static void draw\_letter\_c(GRect bounds, GContext \*ctx) {GRect frame = grect\_inset(bounds, GEdgeInsets(30));graphics\_context\_set\_fill\_color(ctx, GColorYellow);graphics\_fill\_radial(ctx, frame, GOvalScaleModeFitCircle, 30,DEG\_TO\_TRIGANGLE(-225), DEG\_TO\_TRIGANGLE(45));} ``` This produces the expected result, drawn with a smooth antialiased filled circle arc between the specified angles. ![letter-c](https://developer.repebble.com/assets/images/guides/pebble-apps/display-animations/letter-c.png) ## Adaptive Layouts With not only a difference in display shape, but also in resolution, it is very important that an app's layout not be created using hardcoded coordinates. Consider the examples below, designed to create a child [`Layer`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#Layer "Layer") to fill the size of the parent layer. ``` // Bad - only works on Aplite and Basalt rectangular displaysLayer \*layer = layer\_create(GRect(0, 0, 144, 168));// Better - uses the native display sizeGRect bounds = layer\_get\_bounds(parent\_layer);Layer \*layer = layer\_create(bounds); ``` Using this style, the child layer will always fill the parent layer, regardless of its actual dimensions. In a similar vein, when working with a round display it can be important that the layout is centered correctly. A set of layout values that are in the center of the classic 144 x 168 pixel display will not be centered when displayed on a 180 x 180 or 260 x 260 display. The undesirable effect of this can be seen in the example shown below: ![cut-corners](https://developer.repebble.com/assets/images/guides/pebble-apps/display-animations/cut-corners.png) By using the technique described above, the layout's [`GRect`](https://developer.repebble.com/docs/c/Graphics/Graphics_Types.md#GRect "GRect") objects can specify their `origin` and `size` as a function of the dimensions of the layer they are drawn into, solving this problem. ![centered](https://developer.repebble.com/assets/images/guides/pebble-apps/display-animations/centered.png) ## Text Flow and Pagination A chief concern when working with a circular display is the rendering of large amounts of text. As demonstrated by an animation in [_Round App Design_](https://developer.repebble.com/guides/design-and-interaction/in-the-round.md#pagination), continuous reflowing of text makes it much harder to read. A solution to this problem is to render text while flowing within the constraints of the shape of the display, and to scroll/animate it one page at a time. There are three approaches to this available to developers, which are detailed below. For full examples of each, see the [`text-flow-techniques`](https://github.com/pebble-examples/text-flow-techniques) example app. ### Using TextLayer Additions to the [`TextLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/TextLayer.md "TextLayer") API allow text rendered within it to be automatically flowed according to the curve of the display, and paged correctly when the layer is moved or animated further. After a [`TextLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/TextLayer.md "TextLayer") is created in the usual way, text flow can then be enabled: ``` // Create TextLayerTextLayer \*s\_text\_layer = text\_layer\_create(bounds);/\* other properties set up \*/// Add to parent Windowlayer\_add\_child(window\_layer, text\_layer\_get\_layer(s\_text\_layer));// Enable paging and text flow with an inset of 5 pixelstext\_layer\_enable\_screen\_text\_flow\_and\_paging(s\_text\_layer, 5); ``` > Note: The [`text_layer_enable_screen_text_flow_and_paging()`](https://developer.repebble.com/docs/c/User_Interface/Layers/TextLayer.md#text_layer_enable_screen_text_flow_and_paging "text\_layer\_enable\_screen\_text\_flow\_and\_paging") function must be called **after** the [`TextLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/TextLayer.md "TextLayer") is added to the view hierarchy (i.e.: after using [`layer_add_child()`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#layer_add_child "layer\_add\_child")), or else it will have no effect. An example of two [`TextLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/TextLayer.md "TextLayer") elements flowing their text within the constraints of the display shape is shown below: ![text-flow](https://developer.repebble.com/assets/images/guides/pebble-apps/display-animations/text-flow.png) ### Using ScrollLayer The [`ScrollLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/ScrollLayer.md "ScrollLayer") UI component also contains round-friendly functionality, allowing it to scroll its child [`Layer`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#Layer "Layer") elements in pages of the same height as its frame (usually the size of the parent [`Window`](https://developer.repebble.com/docs/c/User_Interface/Window.md "Window")). This allows consuming long content to be a more consistent experience, whether it is text, images, or some other kind of information. ``` // Enable ScrollLayer pagingscroll\_layer\_set\_paging(s\_scroll\_layer, true); ``` When combined with a [`TextLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/TextLayer.md "TextLayer") as the main child layer, it becomes easy to display long pieces of textual content on a round display. The [`TextLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/TextLayer.md "TextLayer") can be set up to handle the reflowing of text to follow the display shape, and the [`ScrollLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/ScrollLayer.md "ScrollLayer") handles the paginated scrolling. ``` // Add the TextLayer and ScrollLayer to the view hierarchyscroll\_layer\_add\_child(s\_scroll\_layer, text\_layer\_get\_layer(s\_text\_layer));layer\_add\_child(window\_layer, scroll\_layer\_get\_layer(s\_scroll\_layer));// Set the ScrollLayer's content size to the total size of the textscroll\_layer\_set\_content\_size(s\_scroll\_layer,text\_layer\_get\_content\_size(s\_text\_layer));// Enable TextLayer text flow and pagingconst int inset\_size = 2;text\_layer\_enable\_screen\_text\_flow\_and\_paging(s\_text\_layer, inset\_size);// Enable ScrollLayer pagingscroll\_layer\_set\_paging(s\_scroll\_layer, true); ``` ### Manual Text Drawing The drawing of text into a [`Graphics Context`](https://developer.repebble.com/docs/c/Graphics/Drawing_Text.md "Drawing Text") can also be performed with awareness of text flow and paging preferences. This can be used to emulate the behavior of the two previous approaches, but with more flexibility. This approach involves the use of the [`GTextAttributes`](https://developer.repebble.com/docs/c/Graphics/Drawing_Text.md#GTextAttributes "GTextAttributes") object, which is given to the Graphics API to allow it to flow text and paginate when being animated. When initializing the [`Window`](https://developer.repebble.com/docs/c/User_Interface/Window.md "Window") that will do the drawing: ``` // Create the attributes object used for text renderingGTextAttributes \*s\_attributes = graphics\_text\_attributes\_create();// Enable text flow with an inset of 5 pixelsgraphics\_text\_attributes\_enable\_screen\_text\_flow(s\_attributes, 5);// Enable pagination with a fixed reference point and bounds, used for animatinggraphics\_text\_attributes\_enable\_paging(s\_attributes, bounds.origin, bounds); ``` When drawing some text in a [`LayerUpdateProc`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#LayerUpdateProc "LayerUpdateProc"): ``` static void update\_proc(Layer \*layer, GContext \*ctx) {GRect bounds = layer\_get\_bounds(layer);// Calculate size of the text to be drawn with current attribute settingsGSize text\_size = graphics\_text\_layout\_get\_content\_size\_with\_attributes(s\_sample\_text, fonts\_get\_system\_font(FONT\_KEY\_GOTHIC\_24\_BOLD), bounds,GTextOverflowModeWordWrap, GTextAlignmentCenter, s\_attributes);// Draw the text in this box with the current attribute settingsgraphics\_context\_set\_text\_color(ctx, GColorBlack);graphics\_draw\_text(ctx, s\_sample\_text, fonts\_get\_system\_font(FONT\_KEY\_GOTHIC\_24\_BOLD),GRect(bounds.origin.x, bounds.origin.y, text\_size.w, text\_size.h),GTextOverflowModeWordWrap, GTextAlignmentCenter, s\_attributes);} ``` Once this setup is complete, the text will display correctly when moved or scrolled via a [`PropertyAnimation`](https://developer.repebble.com/docs/c/User_Interface/Animation/PropertyAnimation.md "PropertyAnimation"), such as one that moves the [`Layer`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#Layer "Layer") that draws the text upwards, and at the same time extending its height to display subsequent pages. An example animation is shown below: ``` GRect window\_bounds = layer\_get\_bounds(window\_get\_root\_layer(s\_main\_window));const int duration\_ms = 1000;// Animate the Layer upwards, lengthening it to allow the next page to be drawnGRect start = layer\_get\_frame(s\_layer);GRect finish = GRect(start.origin.x, start.origin.y - window\_bounds.size.h,start.size.w, start.size.h \* 2);// Create and scedule the PropertyAnimationPropertyAnimation \*prop\_anim = property\_animation\_create\_layer\_frame(s\_layer, &start, &finish);Animation \*animation = property\_animation\_get\_animation(prop\_anim);animation\_set\_duration(animation, duration\_ms);animation\_schedule(animation); ``` ## Working With a Circular Framebuffer The traditional rectangular Pebble app framebuffer is a single continuous memory segment that developers could access with [`gbitmap_get_data()`](https://developer.repebble.com/docs/c/Graphics/Graphics_Types.md#gbitmap_get_data "gbitmap\_get\_data"). With a round display, Pebble saves memory by clipping sections of each line of difference between the display area and the rectangle it occupies. The resulting masking pattern looks like this: ![mask](https://developer.repebble.com/assets/images/guides/pebble-apps/display-animations/mask.png) > Download this mask by saving the PNG image above, or get it as a [Photoshop PSD layer](https://developer.repebble.com/assets/images/guides/pebble-apps/display-animations/round-mask-layer.psd). This has an important implication - the memory segment of the framebuffer can no longer be accessed using classic `y * row_width + x` formulae. Instead, developers should use the [`gbitmap_get_data_row_info()`](https://developer.repebble.com/docs/c/Graphics/Graphics_Types.md#gbitmap_get_data_row_info "gbitmap\_get\_data\_row\_info") API. Gabbro stores its framebuffer with a fixed row stride but reports the same [`GBitmapFormat8BitCircular`](https://developer.repebble.com/docs/c/Graphics/Graphics_Types.md#GBitmapFormat8BitCircular "GBitmapFormat8BitCircular") format, so the same code works on both round platforms. Read [_Framebuffer Graphics_](https://developer.repebble.com/guides/graphics-and-animations/framebuffer-graphics.md) for details. When used with a given y coordinate, this will return a [`GBitmapDataRowInfo`](https://developer.repebble.com/docs/c/Graphics/Graphics_Types.md#GBitmapDataRowInfo "GBitmapDataRowInfo") object containing a pointer to the row's data, as well as values for the minumum and maximum visible values of x coordinate on that row. For example: ``` static void round\_update\_proc(Layer \*layer, GContext \*ctx) {// Get framebufferGBitmap \*fb = graphics\_capture\_frame\_buffer(ctx);GRect bounds = layer\_get\_bounds(layer);// Write a value to all visible pixelsfor(int y = 0; y \< bounds.size.h; y++) {// Get the min and max x values for this rowGBitmapDataRowInfo info = gbitmap\_get\_data\_row\_info(fb, y);// Iterate over visible pixels in that rowfor(int x = info.min\_x; x \< info.max\_x; x++) {// Set the pixel to blackmemset(&info.data[x], GColorBlack.argb, 1);}}// Release framebuffergraphics\_release\_frame\_buffer(ctx, fb);} ``` ## Displaying More Content When more content is available than fits on the screen at any one time, the user should be made aware using visual clues. The best way to do this is to use the [`ContentIndicator`](https://developer.repebble.com/docs/c/User_Interface/Layers/ScrollLayer.md#ContentIndicator "ContentIndicator") UI component. ![content-indicator](https://developer.repebble.com/assets/images/guides/design-and-interaction/content-indicator.png) A [`ContentIndicator`](https://developer.repebble.com/docs/c/User_Interface/Layers/ScrollLayer.md#ContentIndicator "ContentIndicator") can be obtained in two ways. It can be created from scratch with [`content_indicator_create()`](https://developer.repebble.com/docs/c/User_Interface/Layers/ScrollLayer.md#content_indicator_create "content\_indicator\_create") and manually managed to determine when the arrows should be shown, or a built-in instance can be obtained from a [`ScrollLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/ScrollLayer.md "ScrollLayer"), as shown below: ``` // Get the ContentIndicator from the ScrollLayers\_indicator = scroll\_layer\_get\_content\_indicator(s\_scroll\_layer); ``` In order to draw the arrows indicating more information in each direction, the [`ContentIndicator`](https://developer.repebble.com/docs/c/User_Interface/Layers/ScrollLayer.md#ContentIndicator "ContentIndicator") must be supplied with two new [`Layer`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#Layer "Layer") elements that will be used to do the drawing. These should also be added as children to the main [`Window`](https://developer.repebble.com/docs/c/User_Interface/Window.md "Window") root [`Layer`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#Layer "Layer") such that they are visible on top of all other [`Layer`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#Layer "Layer") elements: ``` static void window\_load(Window \*window) {Layer \*window\_layer = window\_get\_root\_layer(window);GRect bounds = layer\_get\_bounds(window\_layer);/\* ... \*/// Create two Layers to draw the arrowss\_indicator\_up\_layer = layer\_create(GRect(0, 0, bounds.size.w, STATUS\_BAR\_LAYER\_HEIGHT));s\_indicator\_down\_layer = layer\_create(GRect(0, bounds.size.h - STATUS\_BAR\_LAYER\_HEIGHT,bounds.size.w, STATUS\_BAR\_LAYER\_HEIGHT));/\* ... \*/// Add these Layers as children after all other components to appear belowlayer\_add\_child(window\_layer, s\_indicator\_up\_layer);layer\_add\_child(window\_layer, s\_indicator\_down\_layer);} ``` Once the indicator [`Layer`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#Layer "Layer") elements have been created, each of the up and down directions for conventional vertical scrolling must be configured with data to control its behavior. Aspects such as the color of the arrows and background, whether or not the arrows time out after being brought into view, and the alignment of the drawn arrow within the [`Layer`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#Layer "Layer") itself are configured with a `const` [`ContentIndicatorConfig`](https://developer.repebble.com/docs/c/User_Interface/Layers/ScrollLayer.md#ContentIndicatorConfig "ContentIndicatorConfig") object when each direction is being configured: ``` // Configure the properties of each indicatorconst ContentIndicatorConfig up\_config = (ContentIndicatorConfig) {.layer = s\_indicator\_up\_layer,.times\_out = false,.alignment = GAlignCenter,.colors = {.foreground = GColorBlack,.background = GColorWhite}};content\_indicator\_configure\_direction(s\_indicator, ContentIndicatorDirectionUp,&up\_config);const ContentIndicatorConfig down\_config = (ContentIndicatorConfig) {.layer = s\_indicator\_down\_layer,.times\_out = false,.alignment = GAlignCenter,.colors = {.foreground = GColorBlack,.background = GColorWhite}};content\_indicator\_configure\_direction(s\_indicator, ContentIndicatorDirectionDown,&down\_config); ``` Unless the [`ContentIndicator`](https://developer.repebble.com/docs/c/User_Interface/Layers/ScrollLayer.md#ContentIndicator "ContentIndicator") has been retrieved from another [`Layer`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#Layer "Layer") type that includes an instance, it should be destroyed along with its parent [`Window`](https://developer.repebble.com/docs/c/User_Interface/Window.md "Window"): ``` // Destroy a manually created ContentIndicatorcontent\_indicator\_destroy(s\_indicator); ``` For layouts that use the [`StatusBarLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/StatusBarLayer.md "StatusBarLayer"), the [`ContentIndicatorDirectionUp`](https://developer.repebble.com/docs/c/User_Interface/Layers/ScrollLayer.md#ContentIndicatorDirectionUp "ContentIndicatorDirectionUp")`.layer` in the [`ContentIndicatorConfig`](https://developer.repebble.com/docs/c/User_Interface/Layers/ScrollLayer.md#ContentIndicatorConfig "ContentIndicatorConfig") object can be given the status bar's [`Layer`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#Layer "Layer") with [`status_bar_layer_get_layer()`](https://developer.repebble.com/docs/c/User_Interface/Layers/StatusBarLayer.md#status_bar_layer_get_layer "status\_bar\_layer\_get\_layer"), and the drawing routines for each will be managed automatically. --- # Unobstructed Area Source: https://developer.repebble.com/guides/user-interfaces/unobstructed-area/ The [`UnobstructedArea`](https://developer.repebble.com/docs/c/User_Interface/UnobstructedArea.md "UnobstructedArea") API, added in SDK 4.0, allows developers to dynamically adapt their watchface design when an area of the screen is partially obstructed by a system overlay. Currently, the Timeline Quick View feature is the only system overlay. Developers are not required to adjust their designs to cater for such system overlays, but by using the [`UnobstructedArea`](https://developer.repebble.com/docs/c/User_Interface/UnobstructedArea.md "UnobstructedArea") API they can detect changes to the available screen real-estate and then move, scale, or hide their layers to achieve an optimal layout while the screen is partially obscured. ![Unobstructed-watchfaces](https://developer.repebble.com/assets/images/guides/user-interfaces/unobstructed-area/01-unobstructed-watchfaces.jpg) Sample watchfaces with Timeline Quick View overlay ![Obstructed-watchfaces](https://developer.repebble.com/assets/images/guides/user-interfaces/unobstructed-area/02-obstructed-watchfaces.jpg) Potential versions of sample watchfaces using the UnobstructedArea API ### Determining the Unobstructed Bounds Prior to SDK 4.0, when displaying layers on screen you would calculate the size of the display using [`layer_get_bounds()`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#layer_get_bounds "layer\_get\_bounds") and then scale and position your layers accordingly. Developers can now calculate the size of a layer, excluding system obstructions, using the new [`layer_get_unobstructed_bounds()`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#layer_get_unobstructed_bounds "layer\_get\_unobstructed\_bounds"). ``` static Layer \*s\_window\_layer;static TextLayer \*s\_text\_layer;static void main\_window\_load(Window \*window) {s\_window\_layer = window\_get\_root\_layer(window);GRect unobstructed\_bounds = layer\_get\_unobstructed\_bounds(s\_window\_layer);s\_text\_layer = text\_layer\_create(GRect(0, unobstructed\_bounds.size.h / 4, unobstructed\_bounds.size.w, 50));} ``` If you still want a fullscreen entities such as a background image, regardless of any obstructions, just combine both techniques as follows: ``` static Layer \*s\_window\_layer;static BitmapLayer \*s\_image\_layer;static TextLayer \*s\_text\_layer;static void main\_window\_load(Window \*window) {s\_window\_layer = window\_get\_root\_layer(window);GRect full\_bounds = layer\_get\_bounds(s\_window\_layer);GRect unobstructed\_bounds = layer\_get\_unobstructed\_bounds(s\_window\_layer);s\_image\_layer = bitmap\_layer\_create(full\_bounds);s\_text\_layer = text\_layer\_create(GRect(0, unobstructed\_bounds.size.h / 4, unobstructed\_bounds.size.w, 50));} ``` The approach outlined above is perfectly fine to use when your watchface is initially launched, but you’re also responsible for handling the obstruction appearing and disappearing while your watchface is running. ### Rendering with LayerUpdateProc If your application controls its own rendering process using a [`LayerUpdateProc`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#LayerUpdateProc "LayerUpdateProc") you can just dynamically adjust your rendering each time your layer updates. In this example, we use [`layer_get_unobstructed_bounds()`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#layer_get_unobstructed_bounds "layer\_get\_unobstructed\_bounds") instead of [`layer_get_bounds()`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#layer_get_bounds "layer\_get\_bounds"). The graphics are then positioned or scaled based upon the available screen real-estate, instead of the screen dimensions. > You must ensure you fill the entire window, not just the unobstructed area, when drawing the screen - failing to do so may cause unexpected graphics to be drawn behind the quick view, during animations. ``` static void hands\_update\_proc(Layer \*layer, GContext \*ctx) {GRect bounds = layer\_get\_unobstructed\_bounds(layer);GPoint center = grect\_center\_point(&bounds);const int16\_t second\_hand\_length = (bounds.size.w / 2);time\_t now = time(NULL);struct tm \*t = localtime(&now);int32\_t second\_angle = TRIG\_MAX\_ANGLE \* t-\>tm\_sec / 60;GPoint second\_hand = {.x = (int16\_t)(sin\_lookup(second\_angle) \* (int32\_t)second\_hand\_length / TRIG\_MAX\_RATIO) + center.x,.y = (int16\_t)(-cos\_lookup(second\_angle) \* (int32\_t)second\_hand\_length / TRIG\_MAX\_RATIO) + center.y,};// second handgraphics\_context\_set\_stroke\_color(ctx, GColorWhite);graphics\_draw\_line(ctx, second\_hand, center);// minute/hour handgraphics\_context\_set\_fill\_color(ctx, GColorWhite);graphics\_context\_set\_stroke\_color(ctx, GColorBlack);gpath\_rotate\_to(s\_minute\_arrow, TRIG\_MAX\_ANGLE \* t-\>tm\_min / 60);gpath\_draw\_filled(ctx, s\_minute\_arrow);gpath\_draw\_outline(ctx, s\_minute\_arrow);gpath\_rotate\_to(s\_hour\_arrow, (TRIG\_MAX\_ANGLE \* (((t-\>tm\_hour % 12) \* 6) +(t-\>tm\_min / 10))) / (12 \* 6));gpath\_draw\_filled(ctx, s\_hour\_arrow);gpath\_draw\_outline(ctx, s\_hour\_arrow);// dot in the middlegraphics\_context\_set\_fill\_color(ctx, GColorBlack);graphics\_fill\_rect(ctx, GRect(bounds.size.w / 2 - 1, bounds.size.h / 2 - 1, 3,3), 0, GCornerNone);} ``` ### Using Unobstructed Area Handlers If you are not overriding the default rendering of a [`Layer`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#Layer "Layer"), you will need to subscribe to one or more of the [`UnobstructedAreaHandlers`](https://developer.repebble.com/docs/c/User_Interface/UnobstructedArea.md#UnobstructedAreaHandlers "UnobstructedAreaHandlers") to adjust the sizes and positions of layers. There are 3 events available using [`UnobstructedAreaHandlers`](https://developer.repebble.com/docs/c/User_Interface/UnobstructedArea.md#UnobstructedAreaHandlers "UnobstructedAreaHandlers"). These events will notify you when the unobstructed area is: _about to change_, _is currently changing_, or _has finished changing_. You can use these handlers to perform any necessary alterations to your layout. `.will_change` - an event to inform you that the unobstructed area size is about to change. This provides a [`GRect`](https://developer.repebble.com/docs/c/Graphics/Graphics_Types.md#GRect "GRect") which lets you know the size of the screen after the change has finished. `.change` - an event to inform you that the unobstructed area size is currently changing. This event is called several times during the animation of an obstruction appearing or disappearing. [`AnimationProgress`](https://developer.repebble.com/docs/c/User_Interface/Animation.md#AnimationProgress "AnimationProgress") is provided to let you know the percentage of progress towards completion. `.did_change` - an event to inform you that the unobstructed area size has finished changing. This is useful for deinitializing or destroying anything created or allocated in the will\_change handler. These handlers are optional, but at least one must be specified for a valid subscription. In the following example, we subscribe to two of the three available handlers. > **NOTE** : You must construct the [`UnobstructedAreaHandlers`](https://developer.repebble.com/docs/c/User_Interface/UnobstructedArea.md#UnobstructedAreaHandlers "UnobstructedAreaHandlers") object _before_ passing it to the [`unobstructed_area_service_subscribe()`](https://developer.repebble.com/docs/c/User_Interface/UnobstructedArea.md#unobstructed_area_service_subscribe "unobstructed\_area\_service\_subscribe") method. ``` UnobstructedAreaHandlers handlers = {.will\_change = prv\_unobstructed\_will\_change,.did\_change = prv\_unobstructed\_did\_change};unobstructed\_area\_service\_subscribe(handlers, NULL); ``` #### Hiding Layers In this example, we’re going to hide a [`TextLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/TextLayer.md "TextLayer") containing the current date, while the screen is obstructed. Just before the Timeline Quick View appears, we’re going to hide the [`TextLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/TextLayer.md "TextLayer") and we’ll show it again after the Timeline Quick View disappears. ``` static Window \*s\_main\_window;static Layer \*s\_window\_layer;static TextLayer \*s\_date\_layer; ``` Subscribe to the `.did_change` and `.will_change` events: ``` static void main\_window\_load(Window \*window) {// Keep a handle on the root layers\_window\_layer = window\_get\_root\_layer(window);// Subscribe to the will\_change and did\_change eventsUnobstructedAreaHandlers handlers = {.will\_change = prv\_unobstructed\_will\_change,.did\_change = prv\_unobstructed\_did\_change};unobstructed\_area\_service\_subscribe(handlers, NULL);} ``` The `will_change` event fires before the size of the unobstructed area changes, so we need to establish whether the screen is already obstructed, or about to become obstructed. If there isn’t a current obstruction, that means the obstruction must be about to appear, so we’ll need to hide our data layer. ``` static void prv\_unobstructed\_will\_change(GRect final\_unobstructed\_screen\_area,void \*context) {// Get the full size of the screenGRect full\_bounds = layer\_get\_bounds(s\_window\_layer);if (!grect\_equal(&full\_bounds, &final\_unobstructed\_screen\_area)) {// Screen is about to become obstructed, hide the datelayer\_set\_hidden(text\_layer\_get\_layer(s\_date\_layer), true);}} ``` The `did_change` event fires after the unobstructed size changes, so we can perform the same check to see whether the screen is already obstructed, or about to become obstructed. If the screen isn’t obstructed when this event fires, then the obstruction must have just cleared and we’ll need to display our date layer again. ``` static void prv\_unobstructed\_did\_change(void \*context) {// Get the full size of the screenGRect full\_bounds = layer\_get\_bounds(s\_window\_layer);// Get the total available screen real-estateGRect bounds = layer\_get\_unobstructed\_bounds(s\_window\_layer);if (grect\_equal(&full\_bounds, &bounds)) {// Screen is no longer obstructed, show the datelayer\_set\_hidden(text\_layer\_get\_layer(s\_date\_layer), false);}} ``` #### Animating Layer Positions The `.change` event will fire several times while the unobstructed area is changing size. This allows us to use this event to make our layers appear to slide-in or slide-out of their initial positions. In this example, we’re going to use percentages to position two text layers vertically. One layer at the top of the screen and one layer at the bottom. When the screen is obstructed, these two layers will shift to be closer together. Because we’re using percentages, it doesn’t matter if the unobstructed area is increasing or decreasing, our text layers will always be relatively positioned in the available space. ``` static const uint8\_t s\_offset\_top\_percent = 33;static const uint8\_t s\_offset\_bottom\_percent = 10; ``` A simple helper function to simulate percentage based coordinates: ``` uint8\_t relative\_pixel(int16\_t percent, int16\_t max) {return (max \* percent) / 100;} ``` Subscribe to the change event: ``` static void main\_window\_load(Window \*window) {UnobstructedAreaHandlers handler = {.change = prv\_unobstructed\_change};unobstructed\_area\_service\_subscribe(handler, NULL);} ``` Move the text layer each time the unobstructed area size changes: ``` static void prv\_unobstructed\_change(AnimationProgress progress, void \*context) {// Get the total available screen real-estateGRect bounds = layer\_get\_unobstructed\_bounds(s\_window\_layer);// Get the current position of our top text layerGRect frame = layer\_get\_frame(text\_layer\_get\_layer(s\_top\_text\_layer));// Shift the Y coordinateframe.origin.y = relative\_pixel(s\_offset\_top\_percent, bounds.size.h);// Apply the new locationlayer\_set\_frame(text\_layer\_get\_layer(s\_top\_text\_layer), frame);// Get the current position of our bottom text layerGRect frame2 = layer\_get\_frame(text\_layer\_get\_layer(s\_top\_text\_layer));// Shift the Y coordinateframe2.origin.y = relative\_pixel(s\_offset\_bottom\_percent, bounds.size.h);// Apply the new positionlayer\_set\_frame(text\_layer\_get\_layer(s\_bottom\_text\_layer), frame2);} ``` ### Toggling Timeline Quick View The `pebble` tool which shipped as part of SDK 4.0, allows developers to enable and disable Timeline Quick View, which is incredibly useful for debugging purposes. ![Unobstructed animation](https://developer.repebble.com/assets/images/guides/user-interfaces/unobstructed-area/unobstructed-animation.gif) To enable Timeline Quick View, you can use: ``` $ pebble emu-set-timeline-quick-view on ``` To disable Timeline Quick View, you can use: ``` $ pebble emu-set-timeline-quick-view off ``` ### Additional Considerations If you're scaling or moving layers based on the unobstructed area, you must ensure you fill the entire window, not just the unobstructed area. Failing to do so may cause unexpected graphics to be drawn behind the quick view, during animations. At present, Timeline Quick View is not currently planned for the Chalk platform. For design reference, the height of the Timeline Quick View overlay will be _51px_ in total, which includes a 2px border, but this may vary on newer platforms and and the height should always be calculated at runtime. ``` // Calculate the actual height of the Timeline Quick Views\_window\_layer = window\_get\_root\_layer(window);GRect fullscreen = layer\_get\_bounds(s\_window\_layer);GRect unobstructed\_bounds = layer\_get\_unobstructed\_bounds(s\_window\_layer);int16\_t obstruction\_height = fullscreen.size.h - unobstructed\_bounds.size.h; ``` --- # User Interfaces Source: https://developer.repebble.com/guides/user-interfaces/ The User Intefaces section of the developer guide contains information on using other the Pebble SDK elements that contribute to interface with the user in some way, shape, or form. For example, [`Layer`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#Layer "Layer") objects form the foundation of all app user interfaces, while a configuration page asks a user for their input in terms of preferences. Graphics-specific UI elements and resources are discussed in the [_Graphics and Animations_](https://developer.repebble.com/guides/graphics-and-animations.md) and [_App Resources_](https://developer.repebble.com/guides/app-resources.md) sections. ## Contents - [**App Configuration**](https://developer.repebble.com/guides/user-interfaces/app-configuration.md) - How to allow users to customize an app with a configuration page. - [**App Exit Reason**](https://developer.repebble.com/guides/user-interfaces/app-exit-reason.md) - Details on how to use the AppExitReason API - [**AppGlance C API**](https://developer.repebble.com/guides/user-interfaces/appglance-c.md) - How to programatically update an app's app glance. - [**AppGlance in PebbleKit JS**](https://developer.repebble.com/guides/user-interfaces/appglance-pebblekit-js.md) - How to update an app's glance using PebbleKit JS. - [**Content Size**](https://developer.repebble.com/guides/user-interfaces/content-size.md) - Details on how to use the ContentSize API to adapt your watchface layout based on user text size preferences. - [**Layers**](https://developer.repebble.com/guides/user-interfaces/layers.md) - How to use standard Layer components to build an app's UI. - [**Round App UI**](https://developer.repebble.com/guides/user-interfaces/round-app-ui.md) - Details on how to use the Pebble SDK to create layouts specifically for round displays. - [**Unobstructed Area**](https://developer.repebble.com/guides/user-interfaces/unobstructed-area.md) - Details on how to use the UnobstructedArea API to adapt your watchface layout when the screen is partially obstructed by a system overlay. --- # Getting Started with Alloy Source: https://developer.repebble.com/guides/alloy/getting-started/ This guide walks you through creating your first Alloy app for Pebble. > **Platform Support** : Alloy currently supports Emery (Pebble Time 2) and Gabbro (Pebble Round 2). See the platform support table in [_Hardware Information_](https://developer.repebble.com/guides/tools-and-resources/hardware-information.md#platform-support). ## Creating a New Project Use the `pebble` command-line tool to create a new Alloy project: ``` $ pebble new-project --alloy my-first-app ``` This creates a new directory with the following structure: ``` my-first-app/ src/ embeddedjs/ main.js # Watch app code (runs on Pebble) pkjs/ index.js # Phone code (runs on connected phone) resources/ # App resources (images, fonts, etc.) package.json # App manifest and configuration ``` ## Project Structure Alloy apps have **two JavaScript environments** : | Location | File | Runs On | Purpose | | --- | --- | --- | --- | | **embeddedjs** | `src/embeddedjs/main.js` | Watch | Your app UI and logic | | **PKJS** | `src/pkjs/index.js` | Phone | Network proxy, location, config | ### Watch Code (embeddedjs) The `src/embeddedjs/main.js` file is your watch app's entry point. This code runs directly on the Pebble watch: **⌚ Watch** (src/embeddedjs/main.js): ``` console.log("Hello, Pebble!"); ``` ### Phone Code (PKJS) The `src/pkjs/index.js` file runs on the connected phone. It's used for network requests, GPS location, and configuration: **📱 PKJS** (src/pkjs/index.js): ``` Pebble.addEventListener("ready", function(e) {console.log("PebbleKit JS ready!");}); ``` ### package.json The `package.json` file contains your app's metadata and configuration: ``` {"name": "My First App","author": "Your Name","version": "1.0.0","keywords": ["pebble-app"],"private": true,"dependencies": {},"pebble": {"displayName": "My First App","uuid": "12345678-1234-1234-1234-123456789abc","projectType": "moddable","sdkVersion": "3","enableMultiJS": true,"targetPlatforms": ["emery"],"watchapp": {"watchface": false},"messageKeys": [],"resources": {"media": []}}} ``` Key fields in the `pebble` section: | Field | Description | | --- | --- | | `displayName` | The name shown on the watch | | `uuid` | Unique identifier for your app | | `targetPlatforms` | Which Pebble platforms to build for | | `watchapp.watchface` | Set to `true` for watchfaces, `false` for apps | | `messageKeys` | Keys for watch-phone communication | ## Using ES Modules Alloy uses standard ECMAScript modules. You can split your code across multiple files and import them: **math.js** ``` export function add(a, b) {return a + b;}export function multiply(a, b) {return a \* b;} ``` **main.js** ``` import { add, multiply } from "./math";console.log("Sum: " + add(2, 3));// 5console.log("Product: " + multiply(4, 5));// 20 ``` ### Declaring Modules in manifest.json Each additional module must be listed in `src/embeddedjs/manifest.json` so the Moddable build system knows to compile it. The default manifest only includes `main.js`: ``` "modules": {"\*": "./main"} ``` To add more modules, change the value to an array of paths. The `.js` extension is optional: ``` "modules": {"\*": ["./main","./math"]} ``` If you import a module that isn't listed here, you'll get a module-not-found error at runtime. ## Important JavaScript Differences Alloy runs on the XS JavaScript engine, which has some differences from browser or Node.js JavaScript: ### Strict Mode All code runs in strict mode by default. ### Hardened JavaScript Built-in objects (primordials) are frozen and cannot be modified: ``` // This will throw an error:Array.prototype.myMethod = function() {}; ``` ### No eval() Evaluation of JavaScript source code by eval, Function and friends is not supported to keep minimize the code footprint of the JavaScript engine. ### Explicit Resource Management Since SDK 4.33, Alloy supports Explicit Resource Management: `using` declarations, `Symbol.dispose`, `DisposableStack` and `SuppressedError`. Alloy classes with a `close()` method (buttons, sensors, messages, files, network clients and more) are disposable, so they can be released automatically at the end of a scope: ``` function readScore(path) {using file = device.files.openFile({ path });const data = file.read(file.status().size, 0);return JSON.parse(String.fromArrayBuffer(data)).score;// file.close() runs automatically when the scope exits,// even if read() or JSON.parse() throws} ``` ## Debugging Output Use `console.log()` for debug output: ``` console.log("Debug message");console.log("Value: " + someVariable); ``` Output appears in the Pebble emulator console or when viewing logs from a physical watch. > **Note** : `trace()` is also available as a lower-level alternative, but requires a manual newline (`\n`) at the end of each message. `console.log()` is recommended for most use cases. ## Building and Running To build your app and run it in the emulator: ``` $ pebble build$ pebble install --emulator emery ``` To install on a physical watch: ``` $ pebble install --phone YOUR_PHONE_IP ``` ## Next Steps Now that you have a basic app running, explore the following guides: - [Piu UI Framework](https://developer.repebble.com/guides/alloy/piu-guide.md): Build declarative user interfaces - [Poco Graphics](https://developer.repebble.com/guides/alloy/poco-guide.md): Low-level drawing and graphics - [Sensors and Input](https://developer.repebble.com/guides/alloy/sensors-and-input.md): Handle buttons and read sensors - [Storage](https://developer.repebble.com/guides/alloy/storage.md): Persist data between app launches - [Networking](https://developer.repebble.com/guides/alloy/networking.md): Make HTTP requests and use WebSockets - [App Messages](https://developer.repebble.com/guides/alloy/app-messages.md): Send and receive messages between watch and phone --- # Port (Custom Drawing) Source: https://developer.repebble.com/guides/alloy/port-drawing/ The `Port` content type lets you do custom drawing within a Piu application. This combines the best of both worlds: Piu's declarative layout and component system with Poco-style direct drawing. > **Note** : All code in this guide runs on the watch in `src/embeddedjs/main.js`. ## What is Port? Port is a Piu content type that provides a canvas for custom drawing. Unlike other Piu content types that are styled with skins, Port content uses an `onDraw` callback where you can draw shapes, text, and graphics. ## Basic Port Usage ``` class MyPortBehavior extends Behavior {onDraw(port, x, y, width, height) {// Draw a blue rectangleport.fillColor("#0066cc", 0, 0, width, height);// Draw some textconst style = new Style({ font: "18px Gothic", color: "white" });port.drawString("Hello!", style, "white", 10, 10);}}const myPort = new Port(null, {top: 0, bottom: 0, left: 0, right: 0,Behavior: MyPortBehavior});const application = new Application(null, {skin: new Skin({ fill: "white" })});application.add(myPort); ``` ## Port Drawing Methods ### Filling Shapes ``` onDraw(port, x, y, width, height) {// Fill a rectangle with a colorport.fillColor("blue", 10, 10, 50, 30);// Fill with hex colorport.fillColor("#ff6600", 70, 10, 50, 30);} ``` ### Drawing Text ``` onDraw(port, x, y, width, height) {const style = new Style({font: "bold 18px Gothic",color: "black"});// Draw text at positionport.drawString("Hello World", style, "black", 10, 50);// Measure text width for alignmentconst text = "Centered";const measured = style.measure(text);const textX = (width - measured.width) / 2;port.drawString(text, style, "black", textX, 80);} ``` ### Drawing Images ``` onDraw(port, x, y, width, height) {const texture = new Texture(2);// Resource IDport.drawTexture(texture, "white", 10, 10, 0, 0, texture.width, texture.height);} ``` ## Updating Port Content Call `invalidate()` to trigger a redraw: ``` class AnimatedPortBehavior extends Behavior {onDisplaying(port) {this.value = 0;port.interval = 100;// Update every 100msport.start();}onTimeChanged(port) {this.value = (this.value + 5) % 100;port.invalidate();// Request redraw}onDraw(port, x, y, width, height) {// Clear backgroundport.fillColor("white", 0, 0, width, height);// Draw bar based on current valueconst barWidth = (width \* this.value) / 100;port.fillColor("green", 0, height / 2 - 10, barWidth, 20);}} ``` ## Complete Example: Live Bar Chart This example creates an animated bar chart with random data: ``` const GRAY = "gray";const BLUE = "#1932ab";const textStyle = new Style({font: "bold 18px Gothic"});class GraphBehavior extends Behavior {onDisplaying(port) {// Initialize data arrayconst barCount = Math.floor(screen.width / 16);this.values = new Array(barCount);this.values.fill(0);// Start animationport.interval = 100;port.start();}onTimeChanged(port) {// Shift data and add new random valuethis.values.shift();this.values.push(Math.floor(Math.random() \* 100));port.invalidate();}onDraw(port, x, y, width, height) {// Draw Y-axis labels and grid linesfor (let i = 100, yOffset = 0; i \>= 0; i -= 20) {const label = String(i);const labelWidth = textStyle.measure(label).width;port.drawString(label, textStyle, "black", 30 - labelWidth, yOffset);port.fillColor(GRAY, 35, yOffset + 10, width - 35, 1);yOffset += height / 5;}// Draw barslet xOffset = 35;for (let i = 0; i \< this.values.length; i++) {const value = this.values[i];const barHeight = Math.floor((value \* (height - 10)) / 100);port.fillColor(BLUE, xOffset, height - barHeight, 12, barHeight);xOffset += 14;}}}const graph = new Port(null, {top: 0, bottom: 0, left: 0, right: 0,Behavior: GraphBehavior});const application = new Application(null, {skin: new Skin({ fill: "white" })});application.add(graph); ``` ## Combining Port with Piu Layout Port works within Piu's layout system, so you can combine custom drawing with standard Piu components: ``` import {} from "piu/MC";const headerSkin = new Skin({ fill: "darkblue" });const headerStyle = new Style({font: "bold 18px Gothic",color: "white"});class ChartBehavior extends Behavior {onDisplaying(port) {this.data = [30, 50, 80, 45, 90, 60, 75];port.invalidate();}onDraw(port, x, y, width, height) {port.fillColor("white", 0, 0, width, height);const barWidth = width / this.data.length - 4;let xPos = 2;for (const value of this.data) {const barHeight = (value / 100) \* height;port.fillColor("#4CAF50", xPos, height - barHeight, barWidth, barHeight);xPos += barWidth + 4;}}}const DashboardApp = Application.template($ =\> ({skin: new Skin({ fill: "#f0f0f0" }),contents: [Column($, {top: 0, bottom: 0, left: 0, right: 0,contents: [// Header using standard Piu LabelLabel($, {top: 0, height: 40, left: 0, right: 0,skin: headerSkin,style: headerStyle,string: "Weekly Stats"}),// Custom chart using PortPort($, {top: 10, bottom: 10, left: 10, right: 10,Behavior: ChartBehavior})]})]}));export default new DashboardApp({}, {}); ``` ## Port vs Poco | Feature | Port | Poco | | --- | --- | --- | | Integration | Works within Piu apps | Standalone rendering | | Layout | Uses Piu's layout system | Manual positioning | | Updates | Call `invalidate()` | Call `begin()`/`end()` | | Use case | Custom drawing in Piu apps | Full-screen graphics | Use Port when you want custom drawing as part of a larger Piu UI. Use Poco when you need full control over the entire screen. ## Performance Tips 1. **Minimize invalidate() calls**: Only call when data actually changes. 2. **Draw only what's needed** : The `onDraw` callback receives bounds - use them to clip your drawing. 3. **Cache expensive calculations** : Calculate positions and sizes once, not on every draw. 4. **Use appropriate intervals** : 100ms intervals are usually sufficient for charts. Use shorter intervals only when smooth animation is needed. ## Common Patterns ### Progress Bar ``` class ProgressBehavior extends Behavior {onCreate(port, data) {this.progress = data.progress || 0;}setProgress(port, value) {this.progress = Math.max(0, Math.min(100, value));port.invalidate();}onDraw(port, x, y, width, height) {// Backgroundport.fillColor("#ddd", 0, 0, width, height);// Progress fillconst fillWidth = (width \* this.progress) / 100;port.fillColor("#4CAF50", 0, 0, fillWidth, height);}} ``` ### Gauge/Meter ``` class GaugeBehavior extends Behavior {onCreate(port, data) {this.value = data.value || 0;this.max = data.max || 100;}onDraw(port, x, y, width, height) {const centerX = width / 2;const centerY = height / 2;const radius = Math.min(width, height) / 2 - 10;// Background circle (using rectangles as approximation)port.fillColor("#eee", centerX - radius, centerY - radius,radius \* 2, radius \* 2);// Value indicatorconst percentage = this.value / this.max;const indicatorHeight = radius \* 2 \* percentage;port.fillColor("#2196F3",centerX - radius, centerY + radius - indicatorHeight,radius \* 2, indicatorHeight);}} ``` ## Examples The [Pebble Examples](https://github.com/Moddable-OpenSource/pebble-examples) repository includes a Port example: - [`hellopiu-port`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellopiu-port) - animated bar graph using a Piu Port with custom drawing --- # Advanced Networking Source: https://developer.repebble.com/guides/alloy/advanced-networking/ In addition to `fetch()` and `WebSocket`, Alloy provides low-level ECMA-419 networking APIs that offer more control over HTTP and WebSocket connections. In fact, `fetch()` and `WebSocket()` are implemented using these ECMA-419 APIs. > **Note** : Like `fetch()` and `WebSocket`, these APIs require the `@moddable/pebbleproxy` package. See below for setup. ## Proxy Setup All networking APIs require proxy code running on the phone. Install the `@moddable/pebbleproxy` package in your project directory: ``` $ pebble package install @moddable/pebbleproxy ``` Then add the proxy to your `src/pkjs/index.js`: **📱 PKJS** (src/pkjs/index.js): ``` const moddableProxy = require("@moddable/pebbleproxy");Pebble.addEventListener('ready', moddableProxy.readyReceived);Pebble.addEventListener('appmessage', moddableProxy.appMessageReceived); ``` If your app also needs to handle its own `ready` or `appmessage` events, you can call the proxy functions from your own event handlers: ``` const moddableProxy = require("@moddable/pebbleproxy");Pebble.addEventListener('ready', function(e) {moddableProxy.readyReceived(e);// Handle your own ready event here})Pebble.addEventListener('appmessage', function(e) {if (moddableProxy.appMessageReceived(e))return;// Handle your own app messages here}); ``` The `@moddable/pebbleproxy` package handles proxying for `fetch()`, `WebSocket`, `HTTPClient`, and `WebSocketClient`. ## HTTPClient The `HTTPClient` class provides streaming HTTP requests with fine-grained control over headers and response handling. ### Using HTTPClient **⌚ Watch** (src/embeddedjs/main.js): ``` import HTTPClient from "embedded:network/http/client";const http = new HTTPClient({host: "example.com"});http.request({path: "/",method: "GET",headers: new Map([["User-Agent", "Pebble App"]]),headersMask: ["content-type", "date"],onHeaders(status, headers, statusText) {console.log("Status: " + status + " " + statusText);headers.forEach((value, key) =\> {console.log(key + ": " + value);});},onReadable(count) {// Read response body in chunksconst buffer = this.read(count);console.log(String.fromArrayBuffer(buffer));},onComplete() {console.log("Request complete");},onError(error) {console.log("Error: " + error);}}); ``` ### HTTPClient Options | Option | Description | | --- | --- | | `host` | Target hostname | | `port` | Port number (optional) | ### Request Options | Option | Description | | --- | --- | | `path` | URL path (e.g., `/api/data`) | | `method` | HTTP method (default: `GET`) | | `headers` | Map of request headers | | `headersMask` | Array of header names to include in response | | `body` | Request body (for POST/PUT) | ### Request Callbacks | Callback | Description | | --- | --- | | `onHeaders(status, headers, statusText)` | Response headers received | | `onReadable(count)` | Response body data available (`count` is the number of bytes available to read) | | `onComplete()` | Request completed successfully | | `onError(error)` | Request failed | ## WebSocketClient The `WebSocketClient` class provides a low-level WebSocket connection with streaming support. ### Using WebSocketClient **⌚ Watch** (src/embeddedjs/main.js): ``` import WebSocketClient from "embedded:network/websocket/client";const ws = new WebSocketClient({...device.network.ws,// use device.network.wss for secure (wss://) connectionshost: "websockets.chilkat.io",path: "/wsChilkatEcho.ashx",onReadable(count, options) {console.log("Received " + count + " bytes, binary: " + options.binary);const data = this.read();console.log(String.fromArrayBuffer(data));},onWritable(count) {console.log("Ready to write " + count + " bytes");this.write(ArrayBuffer.fromString("Hello!"), { binary: false });},onClose() {console.log("Connection closed");},onError() {console.log("Connection error");}}); ``` ### WebSocketClient Callbacks | Callback | Description | | --- | --- | | `onReadable(count, options)` | Data received from server | | `onWritable(count)` | Ready to send data | | `onClose()` | Connection closed | | `onError()` | Connection error occurred | ## HTTPClient vs fetch() | Feature | HTTPClient | fetch() | | --- | --- | --- | | API style | ECMA-419 callbacks | Promise-based | | Streaming | Yes | No | | Header control | Fine-grained with mask | Basic | | Best for | Large responses, streaming | Simple API calls | ## WebSocketClient vs WebSocket | Feature | WebSocketClient | WebSocket | | --- | --- | --- | | API style | ECMA-419 callbacks | Web standard events | | Streaming | Yes | No | | Best for | Low-level control | Simple messaging | ## Examples The [Pebble Examples](https://github.com/Moddable-OpenSource/pebble-examples) repository includes advanced networking examples: - [`hellohttpclient`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellohttpclient) - HTTP requests using the ECMA-419 `HTTPClient` - [`hellowebsocketclient`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellowebsocketclient) - WebSocket connections using the ECMA-419 `WebSocketClient` --- # Native Functions (FFI) Source: https://developer.repebble.com/guides/alloy/ffi/ The Foreign Function Interface (FFI) lets your Alloy app call native C functions directly from JavaScript. This is useful for performance-critical code, for reusing existing C libraries, or for reaching low-level functionality that isn't exposed through a JavaScript API. > **Platform Support** : Like the rest of Alloy, FFI is available on Emery (Pebble Time 2) and Gabbro (Pebble Round 2). An FFI binding has three parts: 1. The **native C source** that implements the functions. 2. An **`ffi` block** in `manifest.json` that lists the C sources and declares each function's signature. 3. The **`Natives` global** in your JavaScript, through which the functions are called. ## 1. Write the native C functions Add one or more C files alongside your `mdbl.c` (for example `src/c/add.c`). The C function names must match the names you declare in the manifest: ``` // add.c#include \int32\_t add(int32\_t a, int32\_t b) {return a + b;}int32\_t addSquares(int32\_t a, int32\_t b) {return (a \* a) + (b \* b);} ``` Functions can take and return buffers and strings as well as numbers: ``` // hello.c#include \#include \// Fill the caller-provided buffer with bytes derived from `greeting`.void hello(char \*greeting, uint8\_t \*bytes, uint32\_t length) {size\_t len = strlen(greeting);for (uint32\_t i = 0; i \< length; i++) {bytes[i] = (uint8\_t)greeting[i % len];}} ``` ## 2. Declare the functions in the manifest In `src/embeddedjs/manifest.json`, add an `ffi` block listing your C `sources` and the `functions` with their argument and return types: ``` {"include": ["$(MODDABLE)/examples/manifest\_mod.json","$(MODDABLE)/examples/manifest\_typings.json"],"modules": {"\*": "./main"},"ffi": {"sources": ["./add.c","./hello.c"],"functions": {"add": {"arguments": ["int32\_t", "int32\_t"],"returns": "int32\_t"},"addSquares": {"arguments": ["int32\_t", "int32\_t"],"returns": "int32\_t"},"hello": {"arguments": ["char \*", "uint8\_t\*", "uint32\_t"],"returns": "void"}}}} ``` ### Supported types | C type | JavaScript value | | --- | --- | | `int32_t`, `uint8_t`, `uint32_t` | `Number` | | `char*` (argument) | `String` | | `char*`, `const char*` (return) | `String` | | `uint8_t*`, `void*` | `ArrayBuffer` (pass `.buffer` from a typed array or `DataView`) | | `void` | `undefined` | Numeric arguments are passed by value. Buffers are passed by reference, so a C function can read from and write into an `ArrayBuffer` you pass in. ## 3. Make the machine FFI-aware The C entry point (`src/c/mdbl.c`) must pass the generated `fxBuildFFI` symbol when it creates the Moddable machine: ``` #include \int main(void) {Window \*w = window\_create();window\_stack\_push(w, true);moddable\_createMachine(&(ModdableCreationRecord){.recordSize = sizeof(ModdableCreationRecord),#ifdef PBL\_DEBUG.flags = kModdableCreationFlagDebug,#endif.fxBuildFFI = fxBuildFFI,});window\_destroy(w);return 0;} ``` ## 4. Call the functions from JavaScript All declared functions are available on the global `Natives` object: ``` console.log("Hello, FFI.");trace(`Natives.add(2, 3) = ${ Natives.add(2, 3) }\n`);trace(`Natives.addSquares(3, 4) = ${ Natives.addSquares(3, 4) }\n`);const bytes = new Uint8Array(5);Natives.hello("hello", bytes.buffer, bytes.length);trace(`bytes = ${ bytes }\n`); ``` Wrap calls in a `try`/`catch` to handle errors thrown across the boundary: ``` try {const result = Natives.add(2, 3);} catch (error) {console.log("FFI Error: " + error);} ``` ## Further reading The FFI mechanism comes from the Moddable SDK. See the [XS FFI documentation](https://www.moddable.com/documentation/xs/XS%20FFI) for the full set of supported types and behaviors, and the [`helloffi` example](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/helloffi) for a complete project. --- # Dictation Source: https://developer.repebble.com/guides/alloy/dictation/ The `Dictation` class lets your Alloy app capture speech from the user and receive it as text. It drives the system dictation UI and delivers the transcription to your code through callbacks. > **Platform Support** : Dictation is available on Emery (Pebble Time 2) and Gabbro (Pebble Round 2), and requires a connected phone with a working internet connection. ## Basic Usage Import the class and create an instance with `onReadable` and `onError` handlers, then call `start()` to begin listening: ``` import Dictation from "pebble/dictation";let dictation = new Dictation({onReadable() {console.log(`Transcription: ${this.read()}`);},onError(e) {console.log(`Dictation error: ${e}`);}});dictation.start(); ``` ## Methods | Method | Description | | --- | --- | | `start()` | Open the dictation UI and begin listening. | | `stop()` | Stop an in-progress dictation session. | | `read()` | Return the transcribed text. Call from `onReadable`. | | `configure(options)` | Change dictation behavior (see [Configuration](#configuration)). | | `close()` | Release the dictation instance when you're done with it. | ## Events | Event | Description | | --- | --- | | `onReadable()` | Called when a transcription is ready. Call `this.read()` to get the text. | | `onError(status)` | Called when dictation fails (for example, no connection or no speech detected). `status` is a numeric error code. | ## Configuration Pass a `byteLength` to the constructor to size the transcription buffer, and use `configure()` to control the dictation UI: ``` import Dictation from "pebble/dictation";let dictation = new Dictation({byteLength: 512,// maximum transcription size, in bytesonReadable() {console.log(`Transcription: ${this.read()}`);}});dictation.configure({confirm: false,// skip the confirmation screen after dictationerrorDialogs: true// let the system show its own error dialogs});dictation.start(); ``` | Option | Description | | --- | --- | | `confirm` | Whether to show the confirmation screen after dictation (default `true`). | | `errorDialogs` | Whether the system shows its own error UI on failure. | ## Continuous Dictation To keep listening after each transcription, start a new session from within `onReadable`. Use `setImmediate` so the current session finishes cleanly first: ``` import Dictation from "pebble/dictation";let dictation = new Dictation({onReadable() {console.log(`Transcription: ${this.read()}`);setImmediate(() =\> {console.log("Listening...");this.start();});},onError(e) {console.log(`Dictation error: ${e}`);}});console.log("Listening...");dictation.start(); ``` See the [`hellodictation` example](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellodictation) for a complete project. --- # Wakeups Source: https://developer.repebble.com/guides/alloy/wakeups/ Wakeups let your Alloy app schedule itself to launch at a future time, even if it isn't running. When the wakeup fires, the system launches your app and you can read the wakeup details to know why it was started. > **Platform Support** : Wakeups are available on Emery (Pebble Time 2) and Gabbro (Pebble Round 2). ## Scheduling a Wakeup Import the `WakeUp` class and call `schedule()` with a timestamp (in milliseconds, as returned by `Date.now()`), a numeric `cookie` of your choice, and whether the user should be notified if the wakeup is missed: ``` import WakeUp from "pebble/wakeup";// Fire 3 seconds from nowconst id = WakeUp.schedule(Date.now() + 3000, 12345678, false);console.log(`Scheduled WakeUp id ${id}`); ``` `schedule()` returns an `id` you can later use to query or cancel the wakeup. Persist the `id` (for example with `localStorage`) if you need it across launches. ## Methods | Method | Description | | --- | --- | | `WakeUp.schedule(time, cookie, notifyIfMissed)` | Schedule a wakeup at `time` (ms). `cookie` is a number passed back to your app. `notifyIfMissed` shows a missed-event notification if the wakeup couldn't fire. Returns an `id`. | | `WakeUp.cancel(id)` | Cancel a previously scheduled wakeup. | | `WakeUp.query(id)` | Return a `{ time, scheduled }` object describing the wakeup - `time` is the scheduled time in ms and `scheduled` is `true` if it is still pending - or a falsy value if it no longer exists. | ## Detecting a Wakeup Launch When your app is launched by a wakeup, `watch.wake` is set and contains the wakeup's `id` and `cookie`: ``` console.log(`Launch reason ${watch.launch.reason}, arguments ${watch.launch.arguments}`);if (watch.wake) {console.log(`Launched by wakeup id ${watch.wake.id}, cookie ${watch.wake.cookie}`);} ``` ## Receiving a Wakeup While Running If a wakeup fires while your app is already running, it is delivered as a `"wakeup"` event on `watch`: ``` watch.addEventListener("wakeup", wake =\> {console.log(`wakeup id ${wake.id} occurred while running`);WakeUp.cancel(wake.id);}); ``` ## Managing Stored Wakeups Because a wakeup `id` outlives a single launch, it's good practice to validate a stored `id` on startup and discard it if the wakeup is gone or stale: ``` const id = localStorage.getItem("wakeid");if (id) {const wakeup = WakeUp.query(id);if (!wakeup) {// The wakeup no longer existslocalStorage.removeItem("wakeid");} else if (wakeup.time \< Date.now()) {// The wakeup is in the past; cancel itWakeUp.cancel(id);localStorage.removeItem("wakeid");}} ``` See the [`hellowakeup` example](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellowakeup) for a complete project. --- # Vibration Source: https://developer.repebble.com/guides/alloy/vibration/ The `Vibes` class drives the watch's vibration motor, letting your Alloy app provide haptic feedback. It offers a set of standard pulses as well as custom vibration patterns. > **Platform Support** : Vibration is available on Emery (Pebble Time 2) and Gabbro (Pebble Round 2). ## Standard Pulses ``` import Vibes from "pebble/vibes";Vibes.shortPulse();Vibes.longPulse();Vibes.doublePulse(); ``` | Method | Description | | --- | --- | | `Vibes.shortPulse()` | A single short vibration. | | `Vibes.longPulse()` | A single long vibration. | | `Vibes.doublePulse()` | Two short vibrations. | ## Custom Patterns Pass an array of millisecond durations to `pattern()`. Durations alternate between _on_ and _off_, starting with _on_: ``` import Vibes from "pebble/vibes";// on 100ms, off 100ms, on 150ms, off 50ms, on 50ms, off 150ms, on 1000msVibes.pattern([100, 100, 150, 50, 50, 150, 1000]); ``` ## Cancelling Stop any ongoing vibration immediately: ``` Vibes.cancel(); ``` > **Note** : Vibration draws power and can be disruptive. Use it sparingly, and respect the user's Quiet Time settings for non-essential feedback. See the [`hellovibes` example](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellovibes) for a complete project. --- # Device Info and App Events Source: https://developer.repebble.com/guides/alloy/device-info/ Alloy exposes information about the watch and screen through the `watch`, `screen`, and `device` globals, delivers app lifecycle events through `watch`, and lets you control the backlight with `watch.light()`. > **Platform Support** : These APIs are available on Emery (Pebble Time 2) and Gabbro (Pebble Round 2). ## Watch Information The `watch` global describes the device and the user's preferences: ``` console.log(`watch.model ${watch.model}`);console.log(`firmware ${watch.firmwareVersion.major}.${watch.firmwareVersion.minor}.${watch.firmwareVersion.patch}`);console.log(`user prefers ${watch.hour12 ? "12" : "24"} hour display`); ``` | Property | Description | | --- | --- | | `watch.model` | The watch model identifier. | | `watch.firmwareVersion` | Object with `major`, `minor`, and `patch` numbers. | | `watch.hour12` | `true` if the user prefers a 12-hour clock, `false` for 24-hour. | | `watch.launch` | How the app was launched: `reason` and `arguments`. | | `watch.wake` | Present when launched by a wakeup; see [_Wakeups_](https://developer.repebble.com/guides/alloy/wakeups.md). | ## Screen Properties The `screen` global describes the display: ``` console.log(`screen ${screen.width} x ${screen.height}`);console.log(`round ${screen.round}, color ${screen.color}`); ``` | Property | Description | | --- | --- | | `screen.width` | Display width in pixels. | | `screen.height` | Display height in pixels. | | `screen.round` | `true` on round displays (Gabbro). | | `screen.color` | `true` on color displays. | ## Device Sensors The `device.sensor` object reports which sensors are available, so you can feature-detect before using one: ``` const hasTouch = device.sensor.Touch ? true : false;console.log(`touch available ${hasTouch}`); ``` ## App Lifecycle and Display Events Subscribe to app events on `watch` with `addEventListener`: ``` watch.addEventListener("willFocus", inFocus =\> {console.log(`willFocus with inFocus ${inFocus}`);});watch.addEventListener("didFocus", inFocus =\> {console.log(`didFocus with inFocus ${inFocus}`);});watch.addEventListener("resize", progress =\> {console.log(`screen resized to ${screen.width} x ${screen.height} at progress ${progress}`);}); ``` | Event | Description | | --- | --- | | `"willFocus"` | The app is about to gain or lose focus. The handler receives `inFocus`. | | `"didFocus"` | The app has gained or lost focus. The handler receives `inFocus`. | | `"resize"` | The drawable area is changing size (for example during an animation). The handler receives the animation `progress`. | The `watch` global also delivers `"secondchange"`/`"minutechange"` time events (see [_Watchfaces_](https://developer.repebble.com/guides/alloy/watchfaces.md)), a `"wakeup"` event (see [_Wakeups_](https://developer.repebble.com/guides/alloy/wakeups.md)), and `"health"` events when new health data arrives (see [_Health_](https://developer.repebble.com/guides/alloy/health.md)). ## Controlling the Backlight Use `watch.light()` to control the backlight: ``` watch.light(true);// force the backlight onwatch.light(false);// turn the backlight offwatch.light();// turn on temporarily, as if the user interacted ``` Calling `watch.light()` with no argument turns the backlight on for the standard auto-off interval, just like a button press or shake would. Passing a boolean forces the backlight on or off. > **Note** : Holding the backlight on draws significantly more power than letting it auto-off. Only force it on for short, deliberate interactions. See the [`helloinfo`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/helloinfo), [`helloappevents`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/helloappevents), and [`hellolight`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellolight) examples for complete projects. --- # Health Source: https://developer.repebble.com/guides/alloy/health/ The `Health` class gives Alloy apps access to Pebble Health: current and historical step counts, sleep, calories, distance, detected activities, and heart rate. It was added in SDK 4.33 and requires firmware 4.32 or later. > **Note** : Health data is only available while the user has Pebble Health enabled in the mobile app, and heart rate metrics additionally require a watch with a heart rate monitor (Pebble Time 2). Well-behaved apps check [availability](#checking-availability) before reading. All times passed to and returned by the Health API are JavaScript milliseconds — pass `Date` objects or millisecond timestamps. ## Reading Current Values `Health.metric.get()` returns the current (today's) value of a metric: ``` import Health from "pebble/health";console.log(`Steps today: ${Health.metric.get("step count")}`);console.log(`Heart rate: ${Health.metric.get("heart rate")} BPM`); ``` | Metric | Description | | --- | --- | | `"step count"` | Steps taken today | | `"active seconds"` | Seconds spent active today | | `"walked distance"` | Distance walked today, in meters | | `"sleep seconds"` | Seconds slept | | `"sleep restful seconds"` | Seconds of restful (deep) sleep | | `"resting calories"` | Resting kilocalories burned today | | `"active calories"` | Active kilocalories burned today | | `"heart rate"` | Filtered heart rate, in BPM | | `"heart rate raw"` | Raw, unfiltered heart rate, in BPM | ## Checking Availability Not every metric is available on every watch, for every time range, or with the user's current permissions. `Health.metric.accessible()` returns a bitmask of the flags in `Health.access`: ``` import Health from "pebble/health";const now = Date.now();const access = Health.metric.accessible({metric: "heart rate",start: now,end: now});if (access & Health.access.available) {console.log(`Heart rate: ${Health.metric.get("heart rate")} BPM`);} ``` | Flag | Meaning | | --- | --- | | `Health.access.available` | Values can be read for the requested range | | `Health.access.permission` | The user has granted this app access to health data | | `Health.access.supported` | The watch supports this metric | | `Health.access.data` | Samples exist in the requested range | ## Querying History `Health.metric.query()` reads sums and averages over a time range. With no `start` and `end`, it returns today's total: ``` import Health from "pebble/health";// Today's total (same as Health.metric.get for cumulative metrics)const today = Health.metric.query({ metric: "step count" });// Average daily steps over the past weekconst WEEK = 7 \* 24 \* 60 \* 60 \* 1000;const weeklyAverage = Health.metric.query({metric: "step count",start: Date.now() - WEEK,end: Date.now(),aggregation: "average",scope: "daily"});console.log(`Today: ${today}, daily average: ${weeklyAverage}`); ``` | Option | Values | Description | | --- | --- | --- | | `metric` | See [metric table](#reading-current-values) | The metric to query. Required. | | `start`, `end` | `Date` or milliseconds | Time range. Provide both or neither — with neither, the query covers today. | | `aggregation` | `"sum"` (default), `"average"`, `"min"`, `"max"` | How samples in the range are combined. `min`/`max` are only stored for recent history. | | `scope` | `"once"` (default), `"daily"`, `"weekly"`, `"weekday or weekend"` | Which days contribute: every day in the range at once, or averaged per-day, per-week, or by weekday/weekend. | ## Health Events Subscribe to the `"health"` event on the `watch` global to be told when new data arrives. The listener receives a string describing what changed: ``` watch.addEventListener("health", what =\> {if (what === "movement")console.log(`Steps now: ${Health.metric.get("step count")}`);}); ``` | Subtype | Meaning | | --- | --- | | `"significant"` | All data should be considered outdated (e.g. day rollover) — re-read everything | | `"movement"` | Step, distance or active-time values changed | | `"sleep"` | Sleep values changed | | `"metric"` | A [metric alert](#metric-alerts) threshold was crossed | | `"heart rate"` | A new heart rate reading is available | | `"heart rate variability"` | A new heart rate variability reading is available | ## Metric Alerts Instead of polling, register a threshold with `Health.metric.Alert`. When the metric crosses the threshold, a `"metric"` health event fires: ``` import Health from "pebble/health";const alert = new Health.metric.Alert({metric: "heart rate",threshold: 120});watch.addEventListener("health", what =\> {if (what === "metric")console.log("Heart rate above 120!");}); ``` Call `alert.close()` to cancel the alert. Alerts are also disposable, so a `using` declaration closes them automatically at the end of the scope: ``` using alert = new Health.metric.Alert({ metric: "step count", threshold: 10000 }); ``` ## Activities Pebble Health detects activities like sleeping, walking and running. `Health.activity.get()` returns a bitmask of what the user is doing right now, and `Health.activity.iterate()` walks through past (or upcoming scheduled) activity sessions: ``` import Health from "pebble/health";// What is the user doing right now?if (Health.activity.get() & Health.activity.run)console.log("Running!");// List today's walks and runsconst DAY = 24 \* 60 \* 60 \* 1000;Health.activity.iterate({activities: Health.activity.walk | Health.activity.run,start: Date.now() - DAY,end: Date.now(),callback(activity, start, end) {const minutes = Math.round((end - start) / 60000);const name = (activity === Health.activity.run) ? "Run" : "Walk";console.log(`${name}: ${minutes} minutes`);return true;// keep iterating; return false to stop}}); ``` The available activity constants are `Health.activity.sleep`, `restfulSleep`, `walk`, `run` and `openWorkout`. Use `Health.activity.accessible({activities, start, end})` to check access the same way as for metrics. ## Heart Rate Sampling By default the system samples heart rate automatically, more often during intense activity and less often at rest. An app that needs more frequent readings can request a shorter sample period (in milliseconds): ``` import Health from "pebble/health";// Request a heart rate sample every secondHealth.heartRate.samplePeriod = 1000;// ...and restore automatic sampling when doneHealth.heartRate.samplePeriod = 0; ``` The system treats the request as a suggestion, and shared use by other apps may result in a different effective rate. Always set the period back to `0` when your app no longer needs fast sampling. If the app exits without doing so, the request lingers for a while — `Health.heartRate.samplePeriodExpiration` reports how long (in milliseconds) it remains in effect. ## Minute-by-Minute History `Health.history.byMinute()` returns detailed per-minute records: ``` import Health from "pebble/health";const HOUR = 60 \* 60 \* 1000;const minutes = Health.history.byMinute({length: 60,start: Date.now() - HOUR,end: Date.now()});for (const record of minutes) {if (!record)continue;// minutes with no valid data are left undefinedconsole.log(`Steps: ${record.steps}, HR: ${record.heartRate}`);} ``` Each record has `steps`, `orientation`, `vmc` (vector magnitude counts, a measure of movement intensity), `light` (ambient light level) and `heartRate`. The returned array also carries `start` and `end` properties with the actual time range covered. ## Measurement System To display distances in the user's preferred units, ask which measurement system they chose for a metric in the mobile app: ``` const system = Health.displayMeasurementSystem("walked distance");// "metric", "imperial", or undefined if the user hasn't chosen ``` ## See Also - [_Device Info & App Lifecycle_](https://developer.repebble.com/guides/alloy/device-info.md) — the other `watch` events - [_Sensors and Input_](https://developer.repebble.com/guides/alloy/sensors-and-input.md) — live accelerometer and battery data --- # Alloy Reference Source: https://developer.repebble.com/guides/alloy/reference/ Alloy is built from three layers: the XS JavaScript engine and the Moddable SDK modules (Piu, Poco, the ECMA-419 sensor and network classes), the Pebble modules that expose the watch (`pebble/button`, `pebble/message`, `pebble/vibes` and so on), and the Pebble firmware underneath. Documentation is spread across the same three places. ## Where the Documentation Is - The guides in this section describe the Pebble modules and how to use Piu and Poco on the watch. Start with [_Getting Started with Alloy_](https://developer.repebble.com/guides/alloy/getting-started.md). - The Moddable SDK documentation at [moddable.com/documentation](https://www.moddable.com/documentation/) covers Piu, Poco (under Commodetto), the XS engine, the `manifest.json` format and the ECMA-419 classes. Alloy ships Moddable SDK 8.3; features added later are not available. - The TypeScript declarations in the SDK are the most precise reference for the Pebble modules. They are at `toolchain/moddable/typings/pebble/` inside the SDK directory (`~/Library/Application Support/Pebble SDK/SDKs/current` on macOS, `~/.pebble-sdk/SDKs/current` on Linux), one `.d.ts` file per module. - The [pebble-examples](https://github.com/Moddable-OpenSource/pebble-examples) repository has a small example for each API. Its README lists the JavaScript features that are left out of the XS build and what the "dead strip" error means. ## System Fonts Poco and Piu load the watch's built-in fonts by family name and size. Poco takes them directly: ``` const font = new render.Font("Gothic-Bold", 24); ``` Piu uses a CSS-style string. The weight becomes part of the family name, so `"bold 24px Gothic"` selects `Gothic-Bold` at 24 and `"black 30px Bitham"` selects `Bitham-Black` at 30. A string with no weight selects the `-Regular` variant. Both look the name and size up in a fixed table in the firmware. The size must match exactly; there is no scaling. If nothing matches, the constructor throws `font not found`. The table maps to the C `FONT_KEY_*` constants as follows: | Alloy family | Sizes | C constant | | --- | --- | --- | | `Gothic-Regular` | 9, 14, 18, 24, 28 | `FONT_KEY_GOTHIC_09` ... `FONT_KEY_GOTHIC_28` | | `Gothic-Bold` | 14, 18, 24, 28 | `FONT_KEY_GOTHIC_14_BOLD` ... `FONT_KEY_GOTHIC_28_BOLD` | | `Bitham-Black` | 30 | `FONT_KEY_BITHAM_30_BLACK` | | `Bitham-Bold` | 42 | `FONT_KEY_BITHAM_42_BOLD` | | `Bitham-Light` | 42, 34, 18 | `FONT_KEY_BITHAM_42_LIGHT`, `FONT_KEY_BITHAM_34_LIGHT_SUBSET`, `FONT_KEY_BITHAM_18_LIGHT_SUBSET` | | `Bitham-Medium` | 42, 34 | `FONT_KEY_BITHAM_42_MEDIUM_NUMBERS`, `FONT_KEY_BITHAM_34_MEDIUM_NUMBERS` | | `Roboto-Condensed` | 21 | `FONT_KEY_ROBOTO_CONDENSED_21` | | `Roboto-Bold` | 49 | `FONT_KEY_ROBOTO_BOLD_SUBSET_49` | | `DroidSerif-Bold` | 28 | `FONT_KEY_DROID_SERIF_28_BOLD` | | `Leco-Bold` | 20, 26, 32, 36, 38 | `FONT_KEY_LECO_20_BOLD_NUMBERS`, `FONT_KEY_LECO_26_BOLD_NUMBERS_AM_PM`, `FONT_KEY_LECO_32_BOLD_NUMBERS`, `FONT_KEY_LECO_36_BOLD_NUMBERS`, `FONT_KEY_LECO_38_BOLD_NUMBERS` | | `Leco-Regular` | 42 | `FONT_KEY_LECO_42_NUMBERS` | | `Leco-Light` | 28 | `FONT_KEY_LECO_28_LIGHT_NUMBERS` | The firmware table also lists `Gothic-Regular` and `Gothic-Bold` at size 36, which have no `FONT_KEY_*` constant in the C SDK. The Leco fonts and the `SUBSET` Bitham fonts contain digits and a few symbols only; see [_System Fonts_](https://developer.repebble.com/guides/app-resources/system-fonts.md) for the glyph sets. There is no Alloy name for `FONT_KEY_LECO_60_NUMBERS_AM_PM` or `FONT_KEY_LECO_60_BOLD_NUMBERS_AM_PM`. Fonts from the project's resources are loaded by resource ID instead of by name, with the same constructor: `new render.Font(3)`. ## Resource IDs `Poco.PebbleBitmap(id)`, `Poco.PebbleDrawCommandImage(id)` and `Poco.PebbleDrawCommandSequence(id)` take a number: the resource ID the build assigned. IDs start at 1 and follow the order of the `resources.media` array in `package.json`, counting only the resources built for the current platform: ``` "resources": {"media": [{ "type": "png", "name": "BACKGROUND", "file": "images/background.png" },{ "type": "raw", "name": "HOURS", "file": "pdc/hours.pdc" }]} ``` With this manifest `new Poco.PebbleBitmap(1)` loads `background.png` and `new Poco.PebbleDrawCommandImage(2)` loads `hours.pdc`. Inserting a resource in the middle of the list shifts every ID after it, and so do these cases: - A resource with `targetPlatforms` that excludes the current platform is skipped on that platform, so the IDs after it differ between platforms. - When `publishedMedia` is present, the build reserves ID 1 for the timeline lookup table and the first media resource becomes ID 2. - The compiled JavaScript is added as a resource named `MOD` after the project's media, and resources from Pebble Packages come after it. The build writes the final assignment to `build//src/resource_ids.auto.h` as `RESOURCE_ID_` defines. Check that file after changing `package.json` rather than counting by hand. The names in `package.json` are not available to the JavaScript at runtime. To avoid hard-coded numbers, keep one module that defines the IDs in the same order as the manifest and import it everywhere: ``` // resources.jsexport default {BACKGROUND: 1,HOURS: 2,}; ``` Piu's `Texture` and `SVGImage` classes accept either a number, which is a Pebble resource ID as above, or a string path, which is a Moddable resource listed in the `resources` section of `src/embeddedjs/manifest.json`. ## Modules and manifest.json Every JavaScript file that is imported must be listed in `src/embeddedjs/manifest.json`. The template contains: ``` {"include": ["$(MODDABLE)/examples/manifest\_mod.json","$(MODDABLE)/examples/manifest\_typings.json"],"modules": {"\*": "./main"}} ``` Under `"*"` each file becomes a module named after its file name, so `"*": ["./main", "./clock"]` makes `import Clock from "clock"` work. A key other than `"*"` gives the module a different specifier, for example `"piu/Controller": "./modules/piuController"`, and a path ending in `/*` includes every file in a directory. A `"platforms"` block selects modules per platform, with the keys `"pebble/emery"` and `"pebble/gabbro"`. Each module costs memory at startup. Prefer a few larger modules over many small ones. ## The connected Event `watch.addEventListener("connected", handler)` fires when the connection to the phone changes, in either direction. It does not fire when the listener is added, and the handler receives no argument. Read `watch.connected.app` and `watch.connected.pebblekit` for the current state, once at startup and again inside the handler: ``` function update() {console.log(`phone connected: ${watch.connected.app}`);}watch.addEventListener("connected", update);update(); ``` The firmware can take 15 to 30 seconds to report a dropped connection. ## Memory The JavaScript engine runs inside the app's RAM. By default the template's `src/c/mdbl.c` passes `NULL` to `moddable_createMachine()`, which gives the engine a 32 kB static pool that it divides between the slot heap (objects and values), the chunk heap (strings and buffers) and the stack, growing as needed. When the engine cannot allocate, the app exits and the watch shows a dialog titled _Alloy: Fatal Error_ with the engine's message, most often `memory full`. The same message appears in the app logs as `fxAbort memory full`. Other messages in that dialog are `JavaScript stack overflow`, `not enough keys` and `dead strip`, the last one meaning the code used a JavaScript feature that is left out of the XS build. Apps that need more than the default pass a `ModdableCreationRecord`: ``` #include \int main(void) {Window \*w = window\_create();window\_stack\_push(w, true);moddable\_createMachine(&(ModdableCreationRecord){.recordSize = sizeof(ModdableCreationRecord),.stack = 5120,.slot = 31744,.chunk = 19456});window\_destroy(w);return 0;} ``` All three sizes are in bytes, and either all three or none must be set. When their total exceeds 32 kB the engine uses exactly these sizes and does not grow further, so size them for the app's peak use. The watch's app RAM is 128 kB on both emery and gabbro, shared with the Pebble UI library and the native part of the app. ## TypeScript Name the source files `.ts` instead of `.js` and list them in `manifest.json` as usual. The build runs `tsc`, which must be installed and on the `PATH`; the SDK does not include it. The `manifest_typings.json` include in the template adds the Pebble and Moddable declarations to the compiler's configuration, so no `tsconfig.json` is needed. The [hellotypescript](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellotypescript) example is the smallest working project. ## Examples and SDK Versions The examples in pebble-examples target SDK 4.33.1, with these exceptions: - `piu/apps/words` and `piu/watchfaces/wallpaper` import `embedded:storage/key-value` as a module. The released firmware rejects this import, so both fail to load. Use `device.keyValue.open()` as shown in [_Storage_](https://developer.repebble.com/guides/alloy/storage.md). - `piu/watchfaces/cupertino`, `helsinki`, `redmond` and `zurich` list `flint` in `targetPlatforms`. Alloy does not run on Pebble 2 Duo, so build and install them for emery or gabbro only. - `piu/watchfaces/wallpaper` lists `gabbro` but its `manifest.json` only has modules for emery, so the gabbro build fails. It also needs a local WebSocket server to download images from. --- # Piu UI Framework Source: https://developer.repebble.com/guides/alloy/piu-guide/ Piu is a declarative UI framework for building user interfaces in Alloy apps. It provides a component-based architecture with automatic layout, styling, and animation support. > **Note** : All code in this guide runs on the watch in `src/embeddedjs/main.js`. ## Getting Started with Piu For apps using advanced Piu features (behaviors, templates, containers), include the Piu runtime: ``` import {} from "piu/MC"; ``` A basic Piu application creates an `Application` object that fills the screen: ``` const application = new Application(null, {skin: new Skin({ fill: "white" })}); ``` ## Core Concepts ### Content Objects Content objects are the basic building blocks of Piu UIs. The main types are: | Type | Description | | --- | --- | | `Content` | Basic rectangular element (base class for most UI objects) | | `Container` | Holds other content objects | | `Column` | Vertical layout container | | `Row` | Horizontal layout container | | `Label` | Single line of text | | `Text` | Multi-line formatted text | | `Application` | Root container (one per app) | ### Skins Skins define the visual appearance of content objects: ``` // Solid color skinconst whiteSkin = new Skin({ fill: "white" });// Skin with stroke/borderconst borderedSkin = new Skin({fill: "transparent",stroke: "black",borders: { left: 2, right: 2, top: 2, bottom: 2 }});// Texture-based skin (for images)const ballTexture = new Texture("balls.png");const ballSkin = new Skin({texture: ballTexture,x: 0, y: 0,width: 30, height: 30,variants: 30// for sprite sheets}); ``` ### Styles Styles define text appearance. Styles cascade, similar to CSS - you can create a base style and let child content objects inherit from it, making it easy to maintain a consistent look: ``` const textStyle = new Style({font: "OpenSans-Regular-15",color: "black",horizontal: "center",// "left", "right", "center", "justify"left: 10, right: 10,top: 15, bottom: 15});// Style with state colors (normal, active)const buttonStyle = new Style({font: "bold 18px Gothic",color: ["black", "gray"]// [normal, active]}); ``` ## Positioning and Layout Content objects are positioned using constraints: ``` // Absolute positioningContent($, { left: 20, top: 20, width: 80, height: 80 });// Anchored to edgesContent($, { right: 20, bottom: 20, width: 80, height: 80 });// Fill available spaceContent($, { left: 0, right: 0, top: 0, bottom: 0 });// Centered (no position constraints)Content($, { width: 80, height: 80 }); ``` ## Behaviors Behaviors add interactivity and logic to content objects. They are essential for handling events and updating the UI: ``` class BallBehavior extends Behavior {onCreate(ball, delta) {// delta is the data passed to the contentthis.dx = delta;this.dy = delta;}onDisplaying(ball) {// Store initial position and boundsthis.x = ball.x;this.y = ball.y;this.width = ball.container.width - ball.width;this.height = ball.container.height - ball.height;ball.start();// Start time-based updates}onTimeChanged(ball) {// Move the ballball.moveBy(this.dx, this.dy);// Update position and check boundsthis.x += this.dx;this.y += this.dy;// Bounce off wallsif (this.x \< 0 || this.x \> this.width) this.dx = -this.dx;if (this.y \< 0 || this.y \> this.height) this.dy = -this.dy;}}// Attach behavior to content - the first argument (6) is passed to onCreateContent(6, {left: 0, top: 0,width: 30, height: 30,skin: ballSkin,variant: 0,Behavior: BallBehavior}); ``` ### Common Behavior Events | Event | Description | | --- | --- | | `onCreate(content, data)` | Content created with data | | `onDisplaying(content)` | Content added to display | | `onTimeChanged(content)` | Animation frame (after `start()`) | | `onFinished(content)` | Animation/duration completed | Pebble button presses are handled using the `Button` class rather than Behavior events. See the [Sensors and Input](https://developer.repebble.com/guides/alloy/sensors-and-input.md) guide for details on button handling. Piu also uses the `Button` class internally for button input. ### Touch Events On platforms with a touchscreen (Emery, Gabbro), Piu delivers touch events to Behaviors on the content under the user's finger. Enable touch input by setting `touchCount` on the Application when it is constructed — this is the number of simultaneous contacts Piu will track (Pebble hardware supports `1`): ``` new MyApplication({}, {displayListLength: 2048,touchCount: 1,pixels: screen.width \* 4}); ``` With `touchCount: 0` (the default) Piu does not poll the touch sensor and no touch events fire. Once enabled, Behaviors receive `onTouchBegan` and `onTouchEnded` events. Hit-testing is handled by Piu — the events are delivered to the innermost active content under the touch. Set `active: true` on a container to make it receive touches: ``` class TTTSquareBehavior {onTouchBegan(container) {container.state = 1;// visual press feedback via skin variants}onTouchEnded(container) {container.state = 0;container.bubble("onTap", container);}}const TTTSquare = Container.template($ =\> ({width: 67, height: 67, skin: squareSkin, active: true,Behavior: TTTSquareBehavior,contents: [/\* ... \*/]})); ``` Use `container.bubble(event, ...args)` from inside `onTouchEnded` to forward higher-level events (tap, long-press, etc.) up the content tree, where an Application or container Behavior can handle them. | Event | Description | | --- | --- | | `onTouchBegan(content, id, x, y, ticks)` | Finger pressed on the content | | `onTouchEnded(content, id, x, y, ticks)` | Finger lifted from the content | For free-form touch input outside of Piu's hit-testing (for example, dragging in a Poco-rendered app), use the lower-level [`device.sensor.Touch`](https://developer.repebble.com/guides/alloy/sensors-and-input.md#touch) driver instead. ## Templates Templates create reusable component definitions. They are most useful when you need to create multiple instances of the same component: ``` // Define a templateconst Square = Content.template($ =\> ({width: 80, height: 80,skin: new Skin({ fill: $ })// $ is the data passed to template}));// Use the templateconst redSquare = new Square("red", { left: 20, top: 20 });const blueSquare = new Square("blue", { right: 20, bottom: 20 });application.add(redSquare);application.add(blueSquare); ``` > **Note** : For one-off content, create instances directly rather than defining a template first. Templates use more code and RAM than direct instantiation when you only need a single instance. ## Building an Application Here's a complete example with multiple components: ``` const backgroundSkin = new Skin({ fill: "silver" });const headerSkin = new Skin({ fill: "white" });const headerStyle = new Style({ font: "bold 18px Gothic", color: "black" });class HeaderBehavior extends Behavior {onDisplaying(label) {label.string = "My App";}}const application = new Application(null, {skin: backgroundSkin,contents: [new Column(null, {top: 0, bottom: 0, left: 0, right: 0,contents: [new Label(null, {top: 0, height: 30, left: 0, right: 0,skin: headerSkin,style: headerStyle,Behavior: HeaderBehavior}),new Content(null, {top: 10, bottom: 10, left: 10, right: 10,skin: new Skin({ fill: "gray" })})]})]}); ``` ## Anchors and References Use anchors to reference content objects from behaviors: ``` const MyApp = Application.template($ =\> ({contents: [Label($, {anchor: "TITLE",// Creates $.TITLE referencestring: "Hello"})],Behavior: class extends Behavior {onCreate(app, data) {this.data = data;}updateTitle(app, newTitle) {this.data.TITLE.string = newTitle;}}})); ``` ## Animations with Timeline The Timeline class creates smooth animations: ``` import Timeline from "piu/Timeline";class AnimatedBehavior extends Behavior {onDisplaying(content) {let timeline = new Timeline();// Animate 'y' property over 750mstimeline.to(content, { y: 100 }, 750, Math.quadEaseOut, 0);content.duration = timeline.duration;timeline.seekTo(0);content.time = 0;content.start();}onTimeChanged(content) {this.timeline.seekTo(content.time);}} ``` ### Easing Functions Available easing functions on the `Math` object: - `Math.backEaseIn` / `Math.backEaseOut` - `Math.bounceEaseIn` / `Math.bounceEaseOut` - `Math.circularEaseIn` / `Math.circularEaseOut` - `Math.cubicEaseIn` / `Math.cubicEaseOut` - `Math.elasticEaseIn` / `Math.elasticEaseOut` - `Math.exponentialEaseIn` / `Math.exponentialEaseOut` - `Math.quadEaseIn` / `Math.quadEaseOut` - `Math.quartEaseIn` / `Math.quartEaseOut` - `Math.quintEaseIn` / `Math.quintEaseOut` - `Math.sineEaseIn` / `Math.sineEaseOut` For more animation techniques including chaining and looping, see the [Animations](https://developer.repebble.com/guides/alloy/animations.md) guide. ## Displaying Images Load and display images using Texture and Skin: ``` // Load a texture from resourcesconst logoTexture = new Texture("logo.png");// Create a skin from the textureconst logoSkin = new Skin({texture: logoTexture,x: 0, y: 0,width: 64, height: 64});// Display the imageapplication.add(new Content(null, {skin: logoSkin})); ``` ## Text Display ### Single Line (Label) ``` application.add(new Label(null, {left: 0, right: 0, top: 50,style: new Style({ font: "bold 24px Gothic", color: "black" }),string: "Hello, World!"})); ``` ### Multi-line (Text) ``` application.add(new Text(null, {left: 10, right: 10, top: 10,style: textStyle,blocks: [{ spans: "First paragraph of text." },{ spans: ["Second paragraph with ",{ style: boldStyle, spans: "bold text" }," inline."]}]})); ``` ## Using Pebble Fonts Access built-in Pebble fonts with CSS-like syntax: ``` // Format: "[style] [size]px [family]"const headerStyle = new Style({font: "bold 14px Gothic",color: "black"});const titleStyle = new Style({font: "black 30px Bitham",color: "blue"});const bodyStyle = new Style({font: "24px Leco",color: "white"}); ``` Available Pebble fonts include: - `Gothic` - Regular and Bold weights - `Bitham` - Black and Bold weights - `Roboto` - Condensed weight - `Leco` - Regular weight (great for numbers) - `Droid` - Serif style The available sizes are limited for each font family. See the [System Fonts](https://developer.repebble.com/guides/app-resources/system-fonts.md) reference for the full table of all fonts, styles, and sizes. ## Loading Bitmaps from Resources Load images from Pebble resources by ID: ``` // Load texture by resource IDconst backgroundTexture = new Texture(2);// Resource ID 2// Create a tiled background skinconst backgroundSkin = new Skin({texture: backgroundTexture,x: 0, y: 0,width: 40, height: 40,tiles: { left: 0, right: 0, top: 0, bottom: 0 }});// Load and display an imageconst iconTexture = new Texture(3);application.add(new Content(null, {skin: new Skin({texture: iconTexture,width: iconTexture.width,height: iconTexture.height})})); ``` ## PDC (Pebble Draw Command) Images PDC images are vector graphics converted from SVG. In Piu, use the `SVGImage` class to display and transform PDC files. Place `.pdc` files in your `src/embeddedjs/assets/` directory and reference them by filename: ``` const application = new Application(null, {skin: new Skin({ fill: "gray" }),contents: [SVGImage(null, { path: "icon.pdc" })]}); ``` The `path` property specifies the `.pdc` filename from your assets directory. ### SVGImage Transforms SVGImage supports rotation, scaling, and translation through built-in properties: | Property | Description | | --- | --- | | `r` | Rotation angle in radians | | `s` | Uniform scale factor | | `sx`, `sy` | Non-uniform scale (horizontal, vertical) | | `tx`, `ty` | Translation offset (x, y) | | `cx`, `cy` | Center of rotation | In the [gravity example](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/piu/apps/gravity), a PDC star is continuously rotated and moved based on accelerometer readings. The rotation portion of its Behavior updates the `r` property each frame: ``` let degree = this.degree + 1;if (degree \> 360)degree -= 360;this.degree = degree;star.r = (this.degree / 180) \* Math.PI; ``` Unlike Poco's `clone().rotate()` approach, Piu transforms the SVGImage in place through its properties - no cloning needed. ### Animated SVGImage with Timeline Use `duration`, `fraction`, and `start()` to animate SVGImage transforms with easing: ``` const graySkin = new Skin({ fill: "gray" });class ImageBehavior extends Behavior {onAnimate(image) {image.duration = 1500;image.time = 0;image.start();}onDisplaying(image) {this.delta = (image.container.width \>\> 1) + image.width;image.tx = this.delta;}onFinished(image) {image.r = 0;image.tx = this.delta;let next = image.next ?? image.container.first;next.delegate("onAnimate");}onTimeChanged(image) {let fraction = image.fraction;if (fraction \< 0.4) {fraction = Math.quadEaseOut(0.4 - fraction);image.r = fraction \* Math.PI;image.tx = this.delta \* fraction;}else if (fraction \> 0.6) {fraction = Math.quadEaseOut(0.6 - fraction);image.r = fraction \* Math.PI;image.tx = this.delta \* fraction;}else {image.r = 0;image.tx = 0;}}}class TestApplicationBehavior {onDisplaying(application) {application.duration = 1000;application.first.delegate("onAnimate");}}const TestApplication = Application.template($ =\> ({Behavior: TestApplicationBehavior, skin: graySkin,contents: [SVGImage(6, { path: "Pebble\_80x80\_Incoming\_call\_centered.pdc", Behavior: ImageBehavior }),SVGImage(5, { path: "Pebble\_80x80\_Scheduled\_event.pdc", Behavior: ImageBehavior }),SVGImage(4, { path: "Pebble\_80x80\_Outgoing\_call.pdc", Behavior: ImageBehavior }),]}));export default new TestApplication({}, {displayListLength: 4096, touchCount: 0, pixels: screen.width \* 4,}); ``` The `fraction` property returns a value from 0 to 1 representing progress through the `duration`. Combine it with easing functions from `Math` for smooth motion. ### PDC Sequences PDC sequences are animated vector graphics containing multiple frames. Use `SVGImage` with a sequence `.pdc` file and control playback through Behavior events: ``` const graySkin = new Skin({ fill: "gray" });class ClockBehavior extends Behavior {onDisplaying(image) {image.start();}onFinished(image) {image.time = 0;image.start();}onTimeChanged(image) {let fraction = image.fraction;if (fraction \< 0.5)fraction = 1 + fraction;elsefraction = 2 - fraction;image.sx = image.sy = fraction;}}class TestApplicationBehavior {}const TestApplication = Application.template($ =\> ({Behavior: TestApplicationBehavior, skin: graySkin,contents: [SVGImage($, { bottom: 20, path: "clock\_sequence.pdc", Behavior: ClockBehavior })]}));export default new TestApplication({}, {displayListLength: 2048, touchCount: 0, pixels: screen.width \* 4,}); ``` ### Clock Hands with SVGImage A common pattern is using SVGImage for watchface clock hands. Each hand is a separate PDC file, rotated based on the current time: ``` const scale = Math.min(screen.width, screen.height) / 240;class FaceHandBehavior {onFractionChanged(content, fraction) {const angle = ((-fraction \* 2) - 1) \* Math.PI;content.r = angle;}onClockResized(content) {const container = content.container;content.x = (container.width \>\> 1) - content.cx;content.y = (container.height \>\> 1) - content.cy;}}class FaceHoursBehavior extends FaceHandBehavior {onDisplaying(content) {content.cx = 7;content.cy = 14;content.s = scale;this.onClockResized(content);}onClockChanged(content, clock) {this.onFractionChanged(content,(clock.hours % 12 + clock.minutes / 60) / 12);}}class FaceMinutesBehavior extends FaceHandBehavior {onDisplaying(content) {content.cx = 5;content.cy = 20;content.s = scale;this.onClockResized(content);}onClockChanged(content, clock) {this.onFractionChanged(content, clock.minutes / 60);}} ``` The `cx` and `cy` properties set the rotation center, which should match the pivot point of the clock hand in the original SVG. The `scale` factor adapts the hand size to the screen. In the application template, layer the hands from bottom to top: ``` class FaceApplicationBehavior {onDisplaying(application) {watch.addEventListener("secondchange", (clock) =\> {const date = clock.date;application.distribute("onClockChanged", {date,hours: date.getHours(),minutes: date.getMinutes(),seconds: date.getSeconds(),});});}onResize(application) {application.distribute("onClockResized");}}const FaceApplication = Application.template($ =\> ({Behavior: FaceApplicationBehavior,contents: [Content($, { skin: new Skin({ texture: new Texture("dial.png"),width: screen.width, height: screen.height }) }),SVGImage($, { left: 0, width: 14, top: 0, height: 79,path: "hours.pdc", Behavior: FaceHoursBehavior }),SVGImage($, { left: 0, width: 10, top: 0, height: 100,path: "minutes.pdc", Behavior: FaceMinutesBehavior }),]}));export default new FaceApplication(null, {displayListLength: 2048, touchCount: 0, pixels: screen.width \* 4,}); ``` Use `distribute()` to send clock updates to all hands simultaneously. ## Complete Example: Bouncing Balls Here's a complete animated app with multiple bouncing balls: ``` import {} from "piu/MC";const backgroundSkin = new Skin({ fill: "silver" });const ballTexture = new Texture("balls.png");const ballSkin = new Skin({texture: ballTexture,x: 0, y: 0,width: 30, height: 30,variants: 30// sprite sheet with 30px variants});class BallBehavior extends Behavior {onCreate(ball, delta) {this.dx = delta;this.dy = delta;}onDisplaying(ball) {this.x = ball.x;this.y = ball.y;this.width = ball.container.width - ball.width;this.height = ball.container.height - ball.height;ball.start();}onTimeChanged(ball) {ball.moveBy(this.dx, this.dy);this.x += this.dx;this.y += this.dy;if (this.x \< 0 || this.x \> this.width) this.dx = -this.dx;if (this.y \< 0 || this.y \> this.height) this.dy = -this.dy;}}const BallApplication = Application.template($ =\> ({skin: backgroundSkin,contents: [Content(6, { left: 0, top: 0, skin: ballSkin, variant: 0, Behavior: BallBehavior }),Content(5, { right: 0, top: 0, skin: ballSkin, variant: 1, Behavior: BallBehavior }),Content(4, { right: 0, bottom: 0, skin: ballSkin, variant: 2, Behavior: BallBehavior }),Content(3, { left: 0, bottom: 0, skin: ballSkin, variant: 3, Behavior: BallBehavior }),]}));export default new BallApplication(null, { pixels: screen.width \* 4 }); ``` ## Related Guides - [Animations](https://developer.repebble.com/guides/alloy/animations.md) - Advanced animation techniques with Timeline and easing functions - [Port (Custom Drawing)](https://developer.repebble.com/guides/alloy/port-drawing.md) - Combine Piu layout with custom Poco-style drawing - [Poco Graphics](https://developer.repebble.com/guides/alloy/poco-guide.md) - Low-level graphics alternative to Piu - [Converting SVG to PDC](https://developer.repebble.com/guides/app-resources/converting-svg-to-pdc.md) - How to create PDC files from SVG - [Vector Graphics](https://developer.repebble.com/guides/graphics-and-animations/vector-graphics.md) - Overview of vector graphics on Pebble ## Examples The [Pebble Examples](https://github.com/Moddable-OpenSource/pebble-examples) repository includes several Piu examples: - [`hellopiu-balls`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellopiu-balls) - animated bouncing balls with Behavior and texture variants - [`hellopiu-coloredsquares`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellopiu-coloredsquares) - basic layout with colored skins - [`hellopiu-gbitmap`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellopiu-gbitmap) - displaying Pebble GBitmap PNG images - [`hellopiu-jsicon`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellopiu-jsicon) - displaying a Moddable SDK bitmap from a PNG resource - [`hellopiu-text`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellopiu-text) - dynamic text layout with different fonts and alignment - [`hellopiu-pebbletext`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellopiu-pebbletext) - text rendering with Pebble's built-in fonts - [`piu/apps/pdc-images`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/piu/apps/pdc-images) - moving and rotating PDC (SVG) images - [`piu/apps/pdc-sequence`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/piu/apps/pdc-sequence) - playback of animated PDC sequences - [`piu/apps/compass`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/piu/apps/compass) - compass visualization with rotating PDC image - [`piu/apps/gravity`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/piu/apps/gravity) - accelerometer-driven PDC star animation - [`piu/apps/tic-tac-toe`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/piu/apps/tic-tac-toe) - touch input via `onTouchBegan`/`onTouchEnded` Behavior events - [`piu/watchfaces/minato`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/piu/watchfaces/minato) - watchface with PDC clock hands --- # Poco Graphics Source: https://developer.repebble.com/guides/alloy/poco-guide/ Poco is a low-level graphics rendering framework that gives you direct control over pixel drawing. It's ideal for custom graphics, games, and watchfaces where you need precise control over rendering. > **Note** : All code in this guide runs on the watch in `src/embeddedjs/main.js`. ## Getting Started Import Poco and create a renderer instance: ``` import Poco from "commodetto/Poco";const render = new Poco(screen); ``` The `screen` global provides access to the Pebble display. ## Basic Drawing All drawing happens between `begin()` and `end()` calls: ``` render.begin();// Drawing commands hererender.end(); ``` You can also specify a clip region to update only part of the screen: ``` render.begin(x, y, width, height);// Only draws within the specified rectanglerender.end(); ``` ## Colors Create colors using `makeColor()` with RGB values (0-255): ``` const black = render.makeColor(0, 0, 0);const white = render.makeColor(255, 255, 255);const red = render.makeColor(255, 0, 0);const gray = render.makeColor(128, 128, 128);const lightBlue = render.makeColor(0x80, 0xc0, 0xff); ``` ## Drawing Shapes ### Rectangles ``` // Filled rectanglerender.fillRectangle(color, x, y, width, height);// Example: fill the entire screen whiterender.fillRectangle(white, 0, 0, render.width, render.height); ``` ### Rounded Rectangles ``` // Rounded rectangle with radius// corners is a bitmask: 0b0001=TL, 0b0010=TR, 0b0100=BL, 0b1000=BRrender.drawRoundRect(x, y, width, height, color, radius, corners);// All corners roundedrender.drawRoundRect(10, 10, 100, 50, black, 8);// Only top corners roundedrender.drawRoundRect(10, 10, 100, 50, black, 8, 0b0011);// Frame (outline) of rounded rectanglerender.frameRoundRect(x, y, width, height, color, radius); ``` ### Lines ``` // Draw a line with specified thicknessrender.drawLine(x1, y1, x2, y2, color, thickness);// Example: diagonal linesrender.drawLine(0, 0, render.width, render.height, gray, 4);render.drawLine(0, render.height, render.width, 0, gray, 4); ``` ### Circles ``` // Draw a circle or arc// startAngle and endAngle are in degreesrender.drawCircle(color, centerX, centerY, radius, startAngle, endAngle);// Full circlerender.drawCircle(black, 90, 90, 30, 0, 360);// Arc (quarter circle)render.drawCircle(blue, 90, 90, 30, 0, 90); ``` ## Text Rendering ### Using Pebble Built-in Fonts ``` import Poco from "commodetto/Poco";const render = new Poco(screen);const font = new render.Font("Bitham-Black", 30);const black = render.makeColor(0, 0, 0);render.begin();render.drawText("Hello!", font, black, 10, 50);render.end(); ``` Available Pebble fonts: - `Gothic-Regular` (various sizes) - `Gothic-Bold` - `Bitham-Black` - `Bitham-Bold` - `Roboto-Condensed` - `Leco-Regular` - `Droid-Serif` The available sizes are limited for each font family. See the [System Fonts](https://developer.repebble.com/guides/app-resources/system-fonts.md) reference for the full table of all fonts, styles, and sizes. ### Centering Text Use `getTextWidth()` to measure text for centering: ``` const msg = "Hello!";const width = render.getTextWidth(msg, font);const x = (render.width - width) / 2;const y = (render.height - font.height) / 2;render.drawText(msg, font, black, x, y); ``` ### Using Custom Fonts (Moddable SDK Fonts) For custom fonts bundled with your app, use `parseBMF` and `parseRLE`: ``` import Poco from "commodetto/Poco";import parseBMF from "commodetto/parseBMF";import parseRLE from "commodetto/parseRLE";const render = new Poco(screen);const black = render.makeColor(0, 0, 0);// Load custom font from resourcesfunction getFont(name, size) {const font = parseBMF(new Resource(`${name}-${size}.fnt`));font.bitmap = parseRLE(new Resource(`${name}-${size}-alpha.bm4`));return font;}const openSans = getFont("OpenSans-Regular", 21);const noto = getFont("NotoSansJP-Regular", 24);render.begin();render.drawText("Hello!", openSans, black, 10, 10);render.drawText("新宿出口", noto, black, 10, 40);// Japanese textrender.end(); ``` Custom fonts allow Unicode support and unique typography not available in built-in Pebble fonts. ## Drawing Images ### Bitmaps Load bitmaps from resources using `PebbleBitmap`: ``` import Poco from "commodetto/Poco";const render = new Poco(screen);// Load bitmap by resource IDconst bitmap = new Poco.PebbleBitmap(2);render.begin();// Center the bitmapconst x = (render.width - bitmap.width) / 2;const y = (render.height - bitmap.height) / 2;render.drawBitmap(bitmap, x, y);render.end(); ``` Bitmaps have `width` and `height` properties for positioning. ### PDC (Pebble Draw Command) Images PDC images are vector graphics converted from SVG: ``` import Poco from "commodetto/Poco";const render = new Poco(screen);// Load PDC image by resource IDconst pdc = new Poco.PebbleDrawCommandImage(2);render.begin();// Center the PDC imageconst x = (render.width - pdc.width) / 2;const y = (render.height - pdc.height) / 2;render.drawDCI(pdc, x, y);render.end(); ``` ### PDC Sequences (Animations) PDC sequences are animated vector graphics. Use the `time` property to advance through frames: ``` import Poco from "commodetto/Poco";const render = new Poco(screen);const gray = render.makeColor(128, 128, 128);const dcs = new Poco.PebbleDrawCommandSequence(2);console.log("Sequence dimensions: " + dcs.width + " x " + dcs.height);console.log("Sequence duration: " + dcs.duration + " ms");function draw() {render.begin();render.fillRectangle(gray, 0, 0, render.width, render.height);render.drawDCI(dcs,(render.width - dcs.width) / 2,(render.height - dcs.height) / 2);render.end();// Advance to next frameconst frameDuration = dcs.frameDuration;dcs.time += frameDuration;setTimeout(draw, frameDuration);}draw(); ``` PDC sequences have these properties: - `width`, `height` - Dimensions of the sequence - `duration` - Total animation duration in milliseconds - `frameDuration` - Duration of current frame - `time` - Current playback position (set this to advance) ## Animation Use `setInterval()` for frame-based animations. The interval is a request - if Pebble OS can't render fast enough, the actual frame rate will be lower. Smoothest results come from requesting a frame rate that can be consistently achieved: ``` import Poco from "commodetto/Poco";const render = new Poco(screen);const white = render.makeColor(255, 255, 255);const black = render.makeColor(0, 0, 0);let x = 0;setInterval(() =\> {render.begin();// Clear screenrender.fillRectangle(white, 0, 0, render.width, render.height);// Draw moving circlerender.drawCircle(black, x, 90, 20, 0, 360);render.end();x = (x + 2) % render.width;}, 50);// ~20 fps ``` ### Rotating PDC Images PDC images can be transformed. Use `clone()` and `rotate()` for rotation: ``` import Poco from "commodetto/Poco";const render = new Poco(screen);const gray = render.makeColor(128, 128, 128);const dci = new Poco.PebbleDrawCommandImage(2);console.log("PDC dimensions: " + dci.width + " x " + dci.height);let angle = 0;setInterval(() =\> {render.begin();render.fillRectangle(gray, 0, 0, render.width, render.height);// Clone, rotate around center, then drawrender.drawDCI(dci.clone().rotate(angle, dci.width / 2, dci.height / 2),(render.width - dci.width) / 2,(render.height - dci.height) / 2);angle += Math.PI / 30;render.end();}, 50);// ~20fps ``` The `rotate()` method takes angle in radians and pivot point coordinates. ### Scaling PDC Images PDC images can also be scaled dynamically: ``` import Poco from "commodetto/Poco";const render = new Poco(screen);const gray = render.makeColor(128, 128, 128);const dci = new Poco.PebbleDrawCommandImage(2);console.log("PDC size: " + dci.width + " x " + dci.height);const start = Date.now();setInterval(() =\> {// Animate scale with elastic easingconst progress = ((Date.now() - start) % 1500) / 1500;const scale = Math.elasticEaseOut(progress);// Center the scaled imageconst cx = (render.width - dci.width \* scale) / 2;const cy = (render.height - dci.height \* scale) / 2;render.begin();render.fillRectangle(gray, 0, 0, render.width, render.height);render.drawDCI(dci.clone().scale(scale), cx, cy);render.end();}, 50); ``` The `scale()` method takes a scale factor (1.0 = original size). ### PDC Transformation Methods | Method | Description | | --- | --- | | `clone()` | Create a copy for transformation | | `rotate(angle, pivotX, pivotY)` | Rotate by angle (radians) around pivot | | `scale(factor)` | Scale uniformly by factor | Always call `clone()` before transforming to preserve the original. ## Complete Example: Animated Display ``` import Poco from "commodetto/Poco";console.log("Starting animation demo");const render = new Poco(screen);const black = render.makeColor(0, 0, 0);const white = render.makeColor(255, 255, 255);const gray = render.makeColor(128, 128, 128);let angle = 0;setInterval(() =\> {render.begin(0, 0, render.width, render.height);// Backgroundrender.fillRectangle(white, 0, 0, render.width, render.height);// Diagonal linesrender.drawLine(0, 0, render.width, render.height, gray, 4);render.drawLine(0, render.height, render.width, 0, gray, 4);// Rounded rectangle frameconst margin = 30;render.drawRoundRect(margin, margin,render.width - (margin \* 2),render.height - (margin \* 2),black, 8);// Rotating arcconst cx = render.width / 2;const cy = render.height / 2;render.drawCircle(gray, cx, cy, 15, angle, angle + 270);render.end();angle = (angle + 10) % 360;}, 50); ``` ## Screen Properties The `render` object provides useful screen information: | Property | Description | | --- | --- | | `render.width` | Screen width in pixels | | `render.height` | Screen height in pixels | ## Performance Tips 1. **Minimize draw area** : Use `begin(x, y, w, h)` to only redraw changed regions 2. **Batch drawing** : Do all drawing between a single `begin()`/`end()` pair 3. **Reuse colors** : Create colors once and store them in variables 4. **Preload fonts** : Create font objects once at startup 5. **Use PDC for complex graphics** : Vector graphics scale better and use less memory than bitmaps ## Poco vs Piu | Feature | Poco | Piu | | --- | --- | --- | | Approach | Procedural | Declarative | | Control | Low-level, pixel-perfect | High-level, automatic layout | | Best for | Games, watchfaces, custom graphics | Apps with standard UI patterns | | Learning curve | Simpler concepts | More abstraction to learn | | Performance | More control over optimization | Framework handles optimization | Use Poco when you need precise control over every pixel. Use Piu when you want automatic layout and a component-based architecture. ## Examples The [Pebble Examples](https://github.com/Moddable-OpenSource/pebble-examples) repository includes several Poco examples: - [`hellopoco-gbitmap`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellopoco-gbitmap) - rendering Pebble GBitmap bitmaps - [`hellopoco-text`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellopoco-text) - text rendering with Moddable SDK fonts - [`hellopoco-pebbletext`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellopoco-pebbletext) - text rendering with Pebble's built-in fonts - [`hellopoco-pebblegraphics`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellopoco-pebblegraphics) - lines, rounded rectangles, and circles - [`hellopoco-pdc`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellopoco-pdc) - rendering PDC (Draw Command) SVG images - [`hellopoco-pdc-rotate`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellopoco-pdc-rotate) - rotating a PDC image - [`hellopoco-pdc-scale`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellopoco-pdc-scale) - scaling a PDC image with easing - [`hellopoco-pdc-sequence`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellopoco-pdc-sequence) - animated PDC image sequences --- # Sensors and Input Source: https://developer.repebble.com/guides/alloy/sensors-and-input/ Alloy provides access to Pebble hardware sensors and button input through simple JavaScript APIs. Sensors follow the ECMA-419 Sensor Class Pattern, while the Button class is a Pebble-specific API. > **Note** : All code in this guide runs on the watch in `src/embeddedjs/main.js`. ## Button Input Handle button presses using the `Button` class: ``` import Button from "pebble/button";new Button({types: ["select", "up", "down", "back"],onPush(down, type) {console.log((down ? "press " : "release ") + type);}}); ``` > **Note** : If your app includes `"back"` in the `types` array, the back button no longer exits the app automatically - press and hold back to exit instead. If you don't include `"back"`, pressing back exits as usual. ### Button Types | Type | Description | | --- | --- | | `"select"` | Middle button (center right) | | `"up"` | Top button | | `"down"` | Bottom button | | `"back"` | Back button (left side) | ### Click Recognizers By default a `Button` reports _raw_ presses and releases, as above. Since SDK 4.33, buttons can instead (or additionally) use the system's click recognizers, which take care of repeat-while-held, long presses and multi-clicks for you. Enable them with extra options in the constructor: ``` import Button from "pebble/button";new Button({types: ["select", "up", "down"],single: { repeat: 200 },// clicks, repeating every 200ms while heldlong: { delay: 700 },// long presses after 700msonPush(down, type, recognizer, count, repeat) {if (recognizer === "long") {console.log(down ? ("long press " + type) : "long release");} else if (down) {console.log("click " + type + (repeat ? " (held)" : ""));}}}); ``` | Option | Value | Behavior | | --- | --- | --- | | `single` | `true` or `{ repeat }` | Fires once per click with `down = 1`. With a `repeat` interval (ms), the click fires repeatedly while the button is held, with `repeat = true` on repeats. | | `long` | `true` or `{ delay }` | Fires with `down = 1` once the button has been held for `delay` ms (default is the system delay), and again with `down = 0` on release. | | `multi` | `true` or `{ min, max, lastOnly, timeout }` | Recognizes sequences of `min` to `max` clicks arriving within `timeout` ms of each other. Fires with `down = 1` and `count` set to the number of clicks; with `lastOnly` it fires only for the final count. | | `raw` | `true` | Plain press (`down = 1`) and release (`down = 0`) events. This is the default when no recognizer option is given. | Recognizer options apply to all buttons in the `types` array, and multiple recognizers can be combined on the same button. Two restrictions apply to the back button: it does not support `long` (the system reserves press-and-hold back for exiting the app), and its `raw` events are emulated, delivering the press and release back-to-back on click. ### Button Events The `onPush` callback receives five parameters: - `down` - `1` when pressed, `0` when released (use it as a truthy/falsy value) - `type` - String: which button was pressed - `recognizer` - String: which recognizer fired (`"single"`, `"long"`, `"multi"` or `"raw"`) - `count` - Number of clicks counted (e.g. for `multi` sequences) - `repeat` - `true` when this is a repeating `single` click from holding the button Callbacks that only declare `(down, type)` keep working unchanged. ## Accelerometer Read motion data from the accelerometer: ``` import Accelerometer from "embedded:sensor/Accelerometer";const accel = new Accelerometer({onSample() {const sample = this.sample();console.log("accel " + sample.x + ", " + sample.y + ", " + sample.z);},onTap(direction) {console.log("single tap " + direction);},onDoubleTap(direction) {console.log("double tap " + direction);}});// Configure sample rate (Hz)accel.configure({ hz: 10 }); ``` ### Accelerometer Sample Data The `sample()` method returns an object with: | Property | Description | | --- | --- | | `x` | Acceleration on X axis | | `y` | Acceleration on Y axis | | `z` | Acceleration on Z axis | ### Accelerometer Events | Event | Description | | --- | --- | | `onSample()` | Called when new sensor data is available | | `onTap(direction)` | Called on single tap gesture | | `onDoubleTap(direction)` | Called on double tap gesture | ### Configuration Options ``` accel.configure({hz: 10// Sample rate in Hz (samples per second)}); ``` ## Compass Read the magnetic heading from the compass: ``` import Compass from "embedded:sensor/Compass";const compass = new Compass({onSample() {const sample = this.sample();console.log("heading " + sample.heading);}}); ``` ### Compass Sample Data | Property | Description | | --- | --- | | `heading` | Magnetic heading in degrees (0-360) | ## Location Get the user's GPS location via the phone: ``` import Location from "embedded:sensor/Location";const location = new Location({onSample() {const sample = this.sample();console.log("Location: " + sample.latitude + ", " + sample.longitude);this.close();}}); ``` The Location sensor follows the same ECMA-419 pattern as other sensors but has a few differences: - **One-shot** - call `this.close()` after receiving the sample. Location is a request, not a continuous monitor. - **Requires the phone** - location data comes from the phone's GPS via the `@moddable/pebbleproxy` package. - **Requires capability** - add `"location"` to the `capabilities` array in `package.json`. ### Location Sample Data | Property | Type | Description | | --- | --- | --- | | `latitude` | Number | Latitude in decimal degrees | | `longitude` | Number | Longitude in decimal degrees | ### Configuration Options You can optionally configure the location request immediately after creating the instance: ``` location.configure({enableHighAccuracy: false,timeout: 5000,maximumAge: 0}); ``` | Option | Description | | --- | --- | | `enableHighAccuracy` | Request high-accuracy GPS (uses more battery) | | `timeout` | Maximum time in ms to wait for a location | | `maximumAge` | Accept a cached location up to this many ms old | ### PKJS Setup for Location The Location sensor requires the `@moddable/pebbleproxy` package. Set up `src/pkjs/index.js`: ``` const moddableProxy = require("@moddable/pebbleproxy");Pebble.addEventListener('ready', moddableProxy.readyReceived);Pebble.addEventListener('appmessage', moddableProxy.appMessageReceived); ``` No custom location code is needed in PKJS - the proxy handles GPS lookup automatically. ## Touch On platforms with a touchscreen (Emery, Gabbro), read raw touch points using the `Touch` sensor exposed as `device.sensor.Touch`: ``` const touch = new device.sensor.Touch({onSample() {const points = this.sample();if (!points) return;if (points.length === 0) {console.log("lift-off");return;}const { x, y } = points[0];console.log("touch " + x + ", " + y);}}); ``` ### Touch Sample Data `sample()` returns either a falsy value (no new data since the last call) or an array of current contact points. An empty array signals that the last finger has lifted off the screen. | Property | Description | | --- | --- | | `x` | X coordinate in screen pixels | | `y` | Y coordinate in screen pixels | ### Tracking Touch State `onSample` only tells you the current set of contact points, not when a touch started or ended. Track that state yourself to get began/moved/ended semantics: ``` const touch = new device.sensor.Touch({onSample() {const points = this.sample();if (!points) return;const last = this.points[0];if (points.length === 0) {if (last) {console.log("ended at " + last.x + ", " + last.y);this.points[0] = undefined;}return;}const { x, y } = points[0];if (last) {last.x = x;last.y = y;console.log("moved to " + x + ", " + y);} else {this.points[0] = { x, y };console.log("began at " + x + ", " + y);}}});touch.points = new Array(1); ``` > **Note** : For Piu apps, you generally don't need this sensor directly - Piu delivers `onTouchBegan`/`onTouchEnded` events to container Behaviors when `touchCount` is set on the Application. See the [Piu guide](https://developer.repebble.com/guides/alloy/piu-guide.md) for details. ## Battery Status Monitor battery level and charging state: ``` import Battery from "embedded:sensor/Battery";const battery = new Battery({onSample() {const sample = this.sample();console.log("battery " + sample.percent + "%, charging " +sample.charging + ", plugged " + sample.plugged);}});// Get current state immediatelyconst status = battery.sample();console.log("battery " + status.percent + "%"); ``` ### Battery Sample Data | Property | Type | Description | | --- | --- | --- | | `percent` | Number | Battery level (0-100) | | `charging` | Boolean | Whether battery is charging | | `plugged` | Boolean | Whether charger is connected | ## Connection Status Monitor connection to the phone: ``` function logConnected() {console.log("App connected: " + watch.connected.app);console.log("PebbleKitJS connected: " + watch.connected.pebblekit);}watch.addEventListener('connected', logConnected);logConnected(); ``` The `watch.connected` object has two properties: | Property | Description | | --- | --- | | `app` | Whether the Pebble app is connected | | `pebblekit` | Whether PebbleKit JS is ready for messaging | ## Complete Example: Sensor Dashboard Here's a complete example that displays sensor data: ``` import Poco from "commodetto/Poco";import Button from "pebble/button";import Accelerometer from "embedded:sensor/Accelerometer";import Battery from "embedded:sensor/Battery";const render = new Poco(screen);const font = new render.Font("Gothic-Regular", 18);const black = render.makeColor(0, 0, 0);const white = render.makeColor(255, 255, 255);let accelData = { x: 0, y: 0, z: 0 };let batteryPercent = 0;let lastButton = "none";// Setup accelerometerconst accel = new Accelerometer({onSample() {accelData = this.sample();draw();}});accel.configure({ hz: 10 });// Setup battery monitorconst battery = new Battery({onSample() {batteryPercent = this.sample().percent;draw();}});batteryPercent = battery.sample().percent;// Setup button handlernew Button({types: ["select", "up", "down"],onPush(down, type) {if (down) {lastButton = type;draw();}}});function draw() {render.begin();render.fillRectangle(white, 0, 0, render.width, render.height);render.drawText("Battery: " + batteryPercent + "%", font, black, 10, 10);render.drawText("Button: " + lastButton, font, black, 10, 35);render.drawText("Accel X: " + accelData.x.toFixed(2), font, black, 10, 60);render.drawText("Accel Y: " + accelData.y.toFixed(2), font, black, 10, 85);render.drawText("Accel Z: " + accelData.z.toFixed(2), font, black, 10, 110);render.end();}draw(); ``` ## Testing in the Emulator When using the Pebble emulator, you can simulate sensor input: ### Battery ``` $ pebble emu-battery --percent 20 --charging --qemu localhost:12344 ``` ### Accelerometer ``` $ pebble emu-accel tilt-left --qemu localhost:12344 ``` ## Best Practices 1. **Clean up resources** : Sensors continue to consume power while active. Stop them when not needed. 2. **Appropriate sample rates** : Higher accelerometer sample rates use more power. Use the lowest rate that meets your needs. 3. **Handle disconnection gracefully** : Check connection status before attempting network operations. ## Examples The [Pebble Examples](https://github.com/Moddable-OpenSource/pebble-examples) repository includes sensor and input examples: - [`hellobutton`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellobutton) - subscribing to button press and release events - [`helloaccelerometer`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/helloaccelerometer) - reading accelerometer data and detecting taps - [`hellobattery`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellobattery) - monitoring battery level and charging state - [`hellolocation`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellolocation) - getting GPS location from the phone - [`hellopoco-drag`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellopoco-drag) - draggable UI built on the `device.sensor.Touch` driver - [`piu/apps/gravity`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/piu/apps/gravity) - visualizes accelerometer readings with an animated display - [`piu/apps/compass`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/piu/apps/compass) - visualizes compass readings with a rotating compass rose --- # Storage Source: https://developer.repebble.com/guides/alloy/storage/ Alloy provides several ways to persist data on the watch: the Web standard `localStorage` API, the ECMA-419 Key-Value Storage API, and a file system API. > **Note** : All code in this guide runs on the watch in `src/embeddedjs/main.js`. ## localStorage (Recommended) The `localStorage` API provides a simple, familiar way to store string data: **⌚ Watch** : ``` // Store a valuelocalStorage.setItem("username", "Alice");// Retrieve a valueconst username = localStorage.getItem("username");console.log("username: " + username);// "Alice"// Remove a valuelocalStorage.removeItem("username"); ``` ### localStorage Methods | Method | Description | | --- | --- | | `setItem(key, value)` | Store a value | | `getItem(key)` | Retrieve a value (returns `null` if not found) | | `removeItem(key)` | Delete a value | | `clear()` | Remove all stored values | ### Example: Persistent Counter ``` // Get current count or start at 0let counter = localStorage.getItem("counter");if (counter === null) {console.log("initializing counter");counter = 1;} else {counter = Number(counter) + 1;}if (counter \< 5) {console.log("save counter value " + counter);localStorage.setItem("counter", counter);} else {console.log("reset counter");localStorage.removeItem("counter");} ``` ### Storing Objects Since `localStorage` only stores strings, serialize objects with JSON: ``` // Store an objectconst settings = {theme: "dark",notifications: true,fontSize: 14};localStorage.setItem("settings", JSON.stringify(settings));// Retrieve the objectconst stored = localStorage.getItem("settings");if (stored) {const settings = JSON.parse(stored);console.log("theme: " + settings.theme);// "dark"} ``` ## Key-Value Storage (ECMA-419) For more control, and to be able to store binary data as well as strings, use the ECMA-419 Key-Value Storage API: ``` // Open a storage fileconst store = device.keyValue.open({path: "mysettings",format: "string"});// Write a valuestore.write("counter", "42");// Read a valueconst value = store.read("counter");console.log("value: " + value);// "42"// Delete a valuestore.delete("counter");// Close when donestore.close(); ``` ### device.keyValue.open() Options | Option | Description | | --- | --- | | `path` | Name of the storage file (required) | | `format` | Data format: `"string"` or `"buffer"` | ### Store Methods | Method | Description | | --- | --- | | `write(key, value)` | Store a value | | `read(key)` | Retrieve a value (returns `undefined` if not found) | | `delete(key)` | Remove a value | | `close()` | Close the storage file | ### Example: Settings with Key-Value Storage ``` const store = device.keyValue.open({ path: "appsettings", format: "string" });// Save settingsfunction saveSettings(settings) {store.write("settings", JSON.stringify(settings));}// Load settings with defaultsfunction loadSettings() {const stored = store.read("settings");if (stored) {return JSON.parse(stored);}return {vibrate: true,brightness: 50};}const settings = loadSettings();console.log("Vibrate: " + settings.vibrate);// Modify and savesettings.brightness = 75;saveSettings(settings);store.close(); ``` ## Files (ECMA-419) For storing larger data, use the file system API via `device.files`: ``` const path = "example.json";// Write a fileconst jsonData = { name: "Alice", score: 1500 };const data = ArrayBuffer.fromString(JSON.stringify(jsonData));const save = device.files.openFile({ path, mode: "r+", size: data.byteLength });save.write(data, 0);save.close();// Read a fileconst load = device.files.openFile({ path });const loaded = load.read(load.status().size, 0);load.close();const parsed = JSON.parse(String.fromArrayBuffer(loaded));console.log(parsed.name);// "Alice"// Delete a filedevice.files.delete(path); ``` ### openFile() Options | Option | Description | | --- | --- | | `path` | File name (required) | | `mode` | `"r"` for read-only (default), `"r+"` for read-write | | `size` | File size in bytes (required when creating with `"r+"`) | ### File Methods | Method | Description | | --- | --- | | `write(buffer, offset)` | Write an ArrayBuffer at the given byte offset | | `read(count, offset)` | Read `count` bytes from the given byte offset | | `status()` | Returns an object with `size` property | | `close()` | Close the file | | `device.files.delete(path)` | Delete a file | Files are disposable, so instead of calling `close()` manually you can open them with a `using` declaration (SDK 4.33 and later) and they close automatically at the end of the scope: ``` using file = device.files.openFile({ path });const loaded = file.read(file.status().size, 0); ``` ## Choosing Between APIs | Feature | localStorage | Key-Value Storage | Files | | --- | --- | --- | --- | | API Style | Web standard | ECMA-419 | ECMA-419 | | Simplicity | Simpler | Moderate | More verbose | | Global access | Yes | Yes (`device.keyValue`) | Yes (`device.files`) | | Multiple stores | No | Yes (different paths) | Yes (different paths) | | Binary data | No (strings only) | Yes (buffer format) | Yes (ArrayBuffer) | | Random access | No | No | Yes (read/write at offset) | **Recommendation** : Use `localStorage` for most cases. Use Key-Value Storage when you need multiple isolated stores or binary key-value pairs. Use Files when you need random access or large binary data. ## Storage Limits Pebble has limited storage space. Keep stored data minimal: - Store only essential data - Clean up old or unused data - Avoid storing large objects - Consider compressing data if storing significant amounts ## Data Persistence Data stored with all three APIs persists across: - App restarts - Watch reboots - App updates (unless the app is uninstalled) Data is deleted when: - The app is uninstalled - The user performs a factory reset - You explicitly delete it with `removeItem()` or `delete()` ## Error Handling The storage APIs can fail if storage is full or corrupted: ``` try {localStorage.setItem("key", "value");} catch (e) {console.log("Failed to save data");} ``` For Key-Value Storage: ``` try {const store = device.keyValue.open({ path: "settings", format: "string" });store.write("key", "value");store.close();} catch (e) {console.log("Storage error: " + e.message);} ``` ## Complete Example: High Score Tracker ``` console.log("High Score Tracker");// Load existing high scoresfunction loadHighScores() {const stored = localStorage.getItem("highscores");if (stored) {return JSON.parse(stored);}return [];}// Save high scoresfunction saveHighScores(scores) {localStorage.setItem("highscores", JSON.stringify(scores));}// Add a new scorefunction addScore(name, score) {const scores = loadHighScores();scores.push({ name, score, date: Date.now() });// Keep only top 10scores.sort((a, b) =\> b.score - a.score);scores.splice(10);saveHighScores(scores);console.log("Added score: " + name + " - " + score);}// Display high scoresfunction showHighScores() {const scores = loadHighScores();console.log("=== HIGH SCORES ===");scores.forEach((entry, i) =\> {console.log((i + 1) + ". " + entry.name + ": " + entry.score);});}// Example usageaddScore("Alice", 1500);addScore("Bob", 2000);addScore("Charlie", 1750);showHighScores(); ``` ## Examples The [Pebble Examples](https://github.com/Moddable-OpenSource/pebble-examples) repository includes storage examples: - [`hellolocalstorage`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellolocalstorage) - persisting strings with the `localStorage` API - [`hellokeyvalue`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellokeyvalue) - key-value storage with string and binary data - [`hellofiles`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellofiles) - reading and writing JSON data using `device.files` --- # Networking Source: https://developer.repebble.com/guides/alloy/networking/ Alloy apps can communicate with the internet by proxying network requests through PebbleKit JS (PKJS) running on the phone. ## Understanding Watch vs Phone Code Alloy apps have two JavaScript environments: | Location | File Path | Runs On | Purpose | | --- | --- | --- | --- | | **embeddedjs** | `src/embeddedjs/main.js` | Watch | Your app UI and logic | | **PKJS** | `src/pkjs/index.js` | Phone | Network proxy, location, config | Code examples in this guide are labeled with **📱 PKJS** or **⌚ Watch** to indicate where they run. ## Setting Up the Network Proxy To use `fetch()` or `WebSocket` on the watch, install the `@moddable/pebbleproxy` package: ``` $ pebble package install @moddable/pebbleproxy ``` Then set up your `src/pkjs/index.js`: **📱 PKJS** (src/pkjs/index.js): ``` const moddableProxy = require("@moddable/pebbleproxy");Pebble.addEventListener('ready', moddableProxy.readyReceived);Pebble.addEventListener('appmessage', moddableProxy.appMessageReceived); ``` If your app also needs to handle its own events, call the proxy functions from your handlers: ``` const moddableProxy = require("@moddable/pebbleproxy");Pebble.addEventListener('ready', function(e) {moddableProxy.readyReceived(e);// Handle your own ready event here});Pebble.addEventListener('appmessage', function(e) {if (moddableProxy.appMessageReceived(e))return;// Handle your own app messages here}); ``` ## HTTP Requests with fetch() Once the proxy is set up, use `fetch()` in your watch code: **⌚ Watch** (src/embeddedjs/main.js): ``` async function fetchData() {const url = new URL("http://api.open-meteo.com/v1/forecast");url.search = new URLSearchParams({latitude: 37.7749,longitude: -122.4194,current: "temperature\_2m"});const response = await fetch(url);const data = await response.json();console.log("Temperature: " + data.current.temperature\_2m);} ``` ### Important Notes - **Wait for the proxy** - network requests only work after the proxy signals it is ready. Listen for `watch.addEventListener("connected", ...)` before calling `fetch()` or opening a `WebSocket` ### Response Methods | Method | Description | | --- | --- | | `response.json()` | Parse response as JSON | | `response.text()` | Get response as string | | `response.ok` | Boolean: true if status 200-299 | | `response.status` | HTTP status code | ## WebSockets WebSockets are also handled by the `@moddable/pebbleproxy` package - no additional proxy setup needed. **⌚ Watch** (src/embeddedjs/main.js): ``` const ws = new WebSocket("ws://websockets.chilkat.io/wsChilkatEcho.ashx");ws.binaryType = "arraybuffer";ws.addEventListener("open", event =\> {console.log("WebSocket connected");ws.send("Hello from Pebble!");ws.send(Uint8Array.of(0, 1, 2, 3, 4, 5));});ws.addEventListener("message", event =\> {let data = event.data;if (data instanceof ArrayBuffer) {console.log("Received binary data");data = new Uint8Array(data);} else {console.log("Received: " + data);if (data === "Goodbye") ws.close(1000, "Done");}});ws.addEventListener("close", event =\> {console.log("Closed: " + event.code + " " + event.reason);}); ``` ## Connection Status Check if the phone is connected from watch code: **⌚ Watch** (src/embeddedjs/main.js): ``` function logConnected() {console.log("App connected: " + watch.connected.app);console.log("PebbleKitJS connected: " + watch.connected.pebblekit);}watch.addEventListener('connected', logConnected);logConnected(); ``` Network requests only work once the proxy is ready. Wait until `watch.connected.pebblekit` is `true` before calling `fetch()` or opening a `WebSocket`. ## Best Practices 1. **Install `@moddable/pebbleproxy`** - required for `fetch()` and `WebSocket` 2. **Wait for the proxy** - network requests only work after the proxy is ready; listen for the `connected` event or check `watch.connected.pebblekit` 3. **Handle errors** - network requests can fail 4. **Minimize data** - request only what you need 5. **Cache responses** - use `localStorage` to reduce requests ## Advanced Networking For more control over HTTP and WebSocket connections, Alloy also provides low-level ECMA-419 networking APIs: - `HTTPClient` - streaming HTTP client with fine-grained header control - `WebSocketClient` - low-level WebSocket with callback-based API, provides streaming support These are also handled by `@moddable/pebbleproxy`. See the [Advanced Networking](https://developer.repebble.com/guides/alloy/advanced-networking.md) guide for details. ## Examples The [Pebble Examples](https://github.com/Moddable-OpenSource/pebble-examples) repository includes networking examples: - [`hellofetch`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellofetch) - HTTP requests using the `fetch()` API - [`hellowebsocket`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellowebsocket) - WebSocket connections using the `WebSocket` API --- # App Messages Source: https://developer.repebble.com/guides/alloy/app-messages/ The Message API provides direct communication between watch code and PebbleKit JS (PKJS) on the phone. Unlike [networking](https://developer.repebble.com/guides/alloy/networking.md), app messages don't require the `@moddable/pebbleproxy` package - they use Pebble's built-in messaging system. Code examples in this guide are labeled with **📱 PKJS** or **⌚ Watch** to indicate where they run. ## Watch-Phone Messaging Use the `Message` class to send and receive structured data between the watch and PKJS: **⌚ Watch** (src/embeddedjs/main.js): ``` import Message from "pebble/message";const message = new Message({keys: ["COMMAND", "DATA", "RESULT"],onReadable() {const msg = this.read();msg.forEach((value, key) =\> {console.log(key + ": " + value);});},onWritable() {console.log("Ready to send messages");},onSuspend() {console.log("Messages suspended");}}); ``` The `keys` array lists the message key names used for communication. These must match the keys defined in `package.json`. The `onWritable` callback fires when the `Message` instance is ready to send messages. The `onSuspend` callback is the opposite - it fires when sending is temporarily unavailable (e.g., the PKJS connection is lost or another `Message` instance is sending). A new `Message` instance begins in the suspended state. ## Sending Messages from Watch **⌚ Watch** (src/embeddedjs/main.js): ``` message.write(new Map([["COMMAND", 1],["DATA", 42]])); ``` ## Message Keys in package.json Message keys must be defined in `package.json` as an array: ``` {"pebble": {"messageKeys": ["COMMAND","DATA","RESULT"]}} ``` ## Receiving Messages in PKJS **📱 PKJS** (src/pkjs/index.js): ``` Pebble.addEventListener('appmessage', function(e) {console.log("Received from watch: " + JSON.stringify(e.payload));// Send a response back to watchPebble.sendAppMessage({'RESULT': 123});}); ``` ## Combining with the Network Proxy If your app uses both app messages and the network proxy, set up the PKJS to handle both: **📱 PKJS** (src/pkjs/index.js): ``` const moddableProxy = require("@moddable/pebbleproxy");Pebble.addEventListener('ready', moddableProxy.readyReceived);Pebble.addEventListener('appmessage', function(e) {if (moddableProxy.appMessageReceived(e))return;// Handle your own app messages hereconsole.log("Received from watch: " + JSON.stringify(e.payload));}); ``` The proxy's `appMessageReceived()` returns `true` if the message was for the proxy (e.g., a `fetch()` request). If it returns `false`, the message is one of your own and you can handle it. ## Getting GPS Location For GPS location, use the `Location` sensor instead of app messages. See the [Sensors and Input](https://developer.repebble.com/guides/alloy/sensors-and-input.md) guide for details on the Location sensor, or the [watchface tutorial Part 4](https://developer.repebble.com/tutorials/alloy-watchface-tutorial/part4.md) for a complete weather example. ## Examples The [Pebble Examples](https://github.com/Moddable-OpenSource/pebble-examples) repository includes app message examples: - [`hellomessage`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellomessage) - sending and receiving messages between watch and phone - [`helloconnected`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/helloconnected) - monitoring watch-phone connection status --- # Watchfaces Source: https://developer.repebble.com/guides/alloy/watchfaces/ Watchfaces are a special type of Pebble app that displays the current time. Alloy makes it easy to create custom watchfaces using either Poco or Piu. > **Note** : All code in this guide runs on the watch in `src/embeddedjs/main.js` unless otherwise noted. ## Configuration To create a watchface, set `watchface` to `true` in your `package.json`: ``` {"pebble": {"watchapp": {"watchface": true}}} ``` ## Time Events Subscribe to time change events using the `watch` global. The event callback receives a `Date` instance for the current time: ``` // Called every secondwatch.addEventListener("secondchange", e =\> {updateDisplay(e.date);});// Called every minutewatch.addEventListener("minutechange", e =\> {updateDisplay(e.date);}); ``` Use `secondchange` for watchfaces that show seconds, and `minutechange` for those that only show hours and minutes (saves battery). The callback is also invoked immediately when the event listener is installed, so there is no need to call your display function separately at startup. ## Basic Watchface Here's a simple digital watchface: ``` import Poco from "commodetto/Poco";const render = new Poco(screen);const font = new render.Font("Bitham-Black", 30);const black = render.makeColor(0, 0, 0);const white = render.makeColor(255, 255, 255);function draw(e) {render.begin();render.fillRectangle(white, 0, 0, render.width, render.height);// Get current time as HH:MM:SSconst time = e.date.toTimeString().slice(0, 8);// Center the textconst width = render.getTextWidth(time, font);const x = (render.width - width) / 2;const y = (render.height - font.height) / 2;render.drawText(time, font, black, x, y);render.end();}watch.addEventListener("secondchange", draw); ``` ## Getting Time Components Use the `Date` provided by the event to get individual time components: ``` function getTimeComponents(date) {return {hours: date.getHours(),// 0-23minutes: date.getMinutes(),// 0-59seconds: date.getSeconds(),// 0-59day: date.getDate(),// 1-31month: date.getMonth(),// 0-11year: date.getFullYear(),// e.g., 2025dayOfWeek: date.getDay()// 0-6 (Sunday=0)};} ``` ### Formatting Time ``` function formatTime(hours, minutes) {const h = String(hours).padStart(2, "0");const m = String(minutes).padStart(2, "0");return `${h}:${m}`;}function format12Hour(hours, minutes) {const period = hours \>= 12 ? "PM" : "AM";const h = hours % 12 || 12;const m = String(minutes).padStart(2, "0");return `${h}:${m} ${period}`;} ``` ## Watchface with Date ``` import Poco from "commodetto/Poco";const render = new Poco(screen);const timeFont = new render.Font("Leco-Regular", 42);const dateFont = new render.Font("Gothic-Regular", 18);const black = render.makeColor(0, 0, 0);const white = render.makeColor(255, 255, 255);const DAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun","Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];function draw(e) {const now = e.date;render.begin();render.fillRectangle(white, 0, 0, render.width, render.height);// Time (HH:MM)const hours = String(now.getHours()).padStart(2, "0");const minutes = String(now.getMinutes()).padStart(2, "0");const timeStr = `${hours}:${minutes}`;let width = render.getTextWidth(timeStr, timeFont);render.drawText(timeStr, timeFont, black,(render.width - width) / 2,(render.height - timeFont.height) / 2 - 15);// Date (e.g., "Mon, Jan 15")const dayName = DAYS[now.getDay()];const monthName = MONTHS[now.getMonth()];const dateStr = `${dayName}, ${monthName} ${now.getDate()}`;width = render.getTextWidth(dateStr, dateFont);render.drawText(dateStr, dateFont, black,(render.width - width) / 2,(render.height - dateFont.height) / 2 + 30);render.end();}watch.addEventListener("minutechange", draw); ``` ## Analog Watchface Concepts For analog watchfaces, calculate hand positions based on time: ``` function getHandAngles(now) {const hours = now.getHours() % 12;const minutes = now.getMinutes();const seconds = now.getSeconds();// Angles in degrees (0 = 12 o'clock)const secondAngle = seconds \* 6;// 360/60 = 6 degrees per secondconst minuteAngle = minutes \* 6 + seconds \* 0.1;const hourAngle = hours \* 30 + minutes \* 0.5; // 360/12 = 30 degrees per hourreturn { hourAngle, minuteAngle, secondAngle };} ``` ### Drawing Clock Hands ``` function drawHand(cx, cy, angle, length, color, thickness) {// Convert angle to radians (0 degrees = up)const radians = (angle - 90) \* Math.PI / 180;const endX = cx + Math.cos(radians) \* length;const endY = cy + Math.sin(radians) \* length;render.drawLine(cx, cy, endX, endY, color, thickness);}function drawAnalogClock(e) {const cx = render.width / 2;const cy = render.height / 2;const { hourAngle, minuteAngle, secondAngle } = getHandAngles(e.date);render.begin();render.fillRectangle(white, 0, 0, render.width, render.height);// Hour handdrawHand(cx, cy, hourAngle, 35, black, 4);// Minute handdrawHand(cx, cy, minuteAngle, 55, black, 3);// Second handdrawHand(cx, cy, secondAngle, 60, red, 1);// Center dotrender.drawCircle(black, cx, cy, 4, 0, 360);render.end();} ``` ## Using Piu for Watchfaces You can also build watchfaces with the Piu framework: ``` import {} from "piu/MC";const backgroundSkin = new Skin({ fill: "white" });const timeStyle = new Style({ font: "42px Bitham-Black", color: "black" });class ClockBehavior extends Behavior {onDisplaying(label) {this.updateTime(label);}updateTime(label, date) {const h = String(date.getHours()).padStart(2, "0");const m = String(date.getMinutes()).padStart(2, "0");label.string = `${h}:${m}`;}}const WatchfaceApp = Application.template($ =\> ({skin: backgroundSkin,contents: [Label($, {anchor: "TIME",style: timeStyle,Behavior: ClockBehavior})],Behavior: class extends Behavior {onCreate(app, data) {this.data = data;watch.addEventListener("minutechange", e =\> {this.data.TIME.delegate("updateTime", e.date);});}}}));export default new WatchfaceApp({}, {}); ``` ## Battery and Connection Status Good watchfaces show battery and connection status: ``` import Battery from "embedded:sensor/Battery";let batteryPercent = 100;let isConnected = false;// Battery monitoringconst battery = new Battery({onSample() {batteryPercent = this.sample().percent;updateDisplay();}});batteryPercent = battery.sample().percent;// Connection monitoringfunction checkConnection() {isConnected = watch.connected.app;console.log("Connected: " + isConnected);updateDisplay();}watch.addEventListener('connected', checkConnection);checkConnection();function drawStatusBar() {// Battery indicatorconst batteryText = batteryPercent + "%";render.drawText(batteryText, smallFont, gray, 10, 5);// Connection indicator (show X when disconnected)if (!isConnected) {render.drawText("X", smallFont, gray, render.width - 20, 5);}} ``` ## Performance Tips 1. **Use minutechange when possible** : Updating every minute instead of every second significantly improves battery life. 2. **Minimize redraw area** : Use `render.begin(x, y, w, h)` to only update changed portions of the screen. 3. **Precompute values** : Calculate font sizes, positions, and colors once at startup. 4. **Avoid complex calculations in draw** : Do math outside the render begin/end block when possible. 5. **Use the provided Date** : Use the `Date` instance from the event instead of creating your own. ## Complete Example: Minimal Watchface ``` import Poco from "commodetto/Poco";import Battery from "embedded:sensor/Battery";const render = new Poco(screen);const timeFont = new render.Font("Leco-Regular", 42);const dateFont = new render.Font("Gothic-Regular", 18);const black = render.makeColor(0, 0, 0);const white = render.makeColor(255, 255, 255);const gray = render.makeColor(128, 128, 128);const DAYS = ["Sunday", "Monday", "Tuesday", "Wednesday","Thursday", "Friday", "Saturday"];let batteryPercent = 100;const battery = new Battery({onSample() {batteryPercent = this.sample().percent;}});batteryPercent = battery.sample().percent;function draw(e) {const now = e.date;render.begin();render.fillRectangle(black, 0, 0, render.width, render.height);// Timeconst h = String(now.getHours()).padStart(2, "0");const m = String(now.getMinutes()).padStart(2, "0");const timeStr = `${h}:${m}`;let width = render.getTextWidth(timeStr, timeFont);render.drawText(timeStr, timeFont, white,(render.width - width) / 2,(render.height / 2) - timeFont.height);// Day of weekconst dayStr = DAYS[now.getDay()];width = render.getTextWidth(dayStr, dateFont);render.drawText(dayStr, dateFont, gray,(render.width - width) / 2,(render.height / 2) + 10);// Batteryconst batteryStr = `${batteryPercent}%`;width = render.getTextWidth(batteryStr, dateFont);render.drawText(batteryStr, dateFont, gray,(render.width - width) / 2,render.height - dateFont.height - 10);render.end();}watch.addEventListener("minutechange", draw); ``` ## Examples The [Pebble Examples](https://github.com/Moddable-OpenSource/pebble-examples) repository includes watchface examples: - [`hellowatchface`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellowatchface) - simple digital watchface using Poco - [`piu/watchfaces/cupertino`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/piu/watchfaces/cupertino) - classic macOS cursor clock - [`piu/watchfaces/london`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/piu/watchfaces/london) - Big Ben-inspired analog watchface - [`piu/watchfaces/helsinki`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/piu/watchfaces/helsinki) - Nokia style digital watchface - [`piu/watchfaces/redmond`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/piu/watchfaces/redmond) - classic Windows clock - [`piu/watchfaces/zurich`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/piu/watchfaces/zurich) - Swiss train station clock --- # Animations Source: https://developer.repebble.com/guides/alloy/animations/ Alloy provides powerful animation capabilities through the Timeline class and built-in easing functions. These work with both Piu UI components and Poco graphics. > **Note** : All code in this guide runs on the watch in `src/embeddedjs/main.js`. ## Timeline Basics The Timeline class creates smooth, coordinated animations: ``` import Timeline from "piu/Timeline";class AnimatedBehavior extends Behavior {onDisplaying(content) {const timeline = this.timeline = new Timeline();// Animate 'y' property from current to 100 over 750mstimeline.to(content, { y: 100 }, 750, Math.quadEaseOut, 0);// Setup playbackcontent.duration = timeline.duration;timeline.seekTo(0);content.time = 0;content.start();}onTimeChanged(content) {this.timeline.seekTo(content.time);}onFinished(content) {console.log("Animation complete!");}} ``` ### Timeline Methods | Method | Description | | --- | --- | | `to(target, properties, duration, easing, delay)` | Animate properties to new values | | `from(target, properties, duration, easing, delay)` | Animate properties from values | | `on(target, properties, duration, easing, delay)` | Keyframe animation | | `seekTo(time)` | Jump to specific time in animation | ### Timeline Properties | Property | Description | | --- | --- | | `duration` | Total duration of all animations | ## Easing Functions Easing functions control the rate of change during animations. All easing functions are available on the `Math` object: ### Available Easing Functions The [`hellopiu-timeline`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellopiu-timeline) example demonstrates most of these easing equations in action. | Ease In | Ease Out | Description | | --- | --- | --- | | `Math.backEaseIn` | `Math.backEaseOut` | Overshoots then returns | | `Math.bounceEaseIn` | `Math.bounceEaseOut` | Bouncing effect | | `Math.circularEaseIn` | `Math.circularEaseOut` | Circular curve | | `Math.cubicEaseIn` | `Math.cubicEaseOut` | Cubic curve (smooth) | | `Math.elasticEaseIn` | `Math.elasticEaseOut` | Spring/elastic effect | | `Math.exponentialEaseIn` | `Math.exponentialEaseOut` | Exponential curve | | `Math.quadEaseIn` | `Math.quadEaseOut` | Quadratic curve | | `Math.quartEaseIn` | `Math.quartEaseOut` | Quartic curve | | `Math.quintEaseIn` | `Math.quintEaseOut` | Quintic curve | | `Math.sineEaseIn` | `Math.sineEaseOut` | Sinusoidal curve | ### Using Easing Functions ``` // Linear (no easing) - define your ownfunction linearEase(fraction) {return fraction;}// Use built-in easingtimeline.to(content, { y: 200 }, 500, Math.bounceEaseOut, 0);// Ease in then ease outtimeline.to(content, { x: 100 }, 300, Math.quadEaseIn, 0);timeline.to(content, { x: 200 }, 300, Math.quadEaseOut, 300); ``` ### Easing Function Characteristics - **EaseIn** : Starts slow, ends fast - **EaseOut** : Starts fast, ends slow - **Bounce** : Creates a bouncing effect at the end - **Elastic** : Creates a spring-like overshoot - **Back** : Pulls back before moving forward ## Animating in Poco For Poco graphics, use easing functions with `setInterval`: ``` import Poco from "commodetto/Poco";const render = new Poco(screen);const gray = render.makeColor(128, 128, 128);const blue = render.makeColor(0, 100, 255);const start = Date.now();const duration = 1500;setInterval(() =\> {// Calculate animation progress (0 to 1)const elapsed = (Date.now() - start) % duration;const progress = elapsed / duration;// Apply easing functionconst eased = Math.elasticEaseOut(progress);// Animate size based on eased valueconst size = 20 + (60 \* eased);const x = (render.width - size) / 2;const y = (render.height - size) / 2;render.begin();render.fillRectangle(gray, 0, 0, render.width, render.height);render.fillRectangle(blue, x, y, size, size);render.end();}, 50);// ~20fps ``` ## Scaling PDC Images Animate PDC image scaling with easing: ``` import Poco from "commodetto/Poco";const render = new Poco(screen);const gray = render.makeColor(128, 128, 128);const dci = new Poco.PebbleDrawCommandImage(2);console.log("PDC size: " + dci.width + " x " + dci.height);const start = Date.now();setInterval(() =\> {// Calculate scale with elastic easingconst progress = ((Date.now() - start) % 1500) / 1500;const scale = Math.elasticEaseOut(progress);// Calculate centered position for scaled imageconst cx = (render.width - dci.width \* scale) / 2;const cy = (render.height - dci.height \* scale) / 2;render.begin();render.fillRectangle(gray, 0, 0, render.width, render.height);render.drawDCI(dci.clone().scale(scale), cx, cy);render.end();}, 50); ``` ## Complete Piu Animation Example Here's a complete example showing Timeline animations with multiple easing functions: ``` import {} from "piu/MC";import Timeline from "piu/Timeline";const WHITE = "white";const BLACK = "black";const GRAY = "gray";const backgroundSkin = new Skin({ fill: BLACK });const headerSkin = new Skin({ fill: WHITE });const boxSkin = new Skin({ fill: GRAY });const borderSkin = new Skin({fill: "transparent",stroke: WHITE,borders: { left: 2, right: 2, top: 2, bottom: 2 }});const headerStyle = new Style({font: "bold 18px Gothic",color: BLACK});// List of easing functions to demonstrateconst easingFunctions = [{ name: "quadEase", out: Math.quadEaseOut, in: Math.quadEaseIn },{ name: "bounceEase", out: Math.bounceEaseOut, in: Math.bounceEaseIn },{ name: "elasticEase", out: Math.elasticEaseOut, in: Math.elasticEaseIn },{ name: "backEase", out: Math.backEaseOut, in: Math.backEaseIn },];class AnimationBehavior extends Behavior {onCreate(container) {this.index = 0;}onDisplaying(container) {this.startAnimation(container);}startAnimation(container) {const easing = easingFunctions[this.index];const box = container.first;const bottom = container.height - box.height - 4;// Animate downconst timeline = this.timeline = new Timeline();timeline.to(box, { y: bottom }, 750, easing.out, 0);timeline.to(box, { y: 4 }, 750, easing.in, 250);container.duration = timeline.duration + 500;timeline.seekTo(0);container.time = 0;container.start();}onTimeChanged(container) {this.timeline.seekTo(container.time);}onFinished(container) {// Move to next easing functionthis.index = (this.index + 1) % easingFunctions.length;this.startAnimation(container);}}const AnimationApp = Application.template($ =\> ({skin: backgroundSkin,contents: [Column($, {top: 0, bottom: 0, left: 0, right: 0,contents: [Label($, {top: 0, height: 30, left: 0, right: 0,skin: headerSkin,style: headerStyle,string: "Animation Demo"}),Container($, {top: 10, bottom: 10, left: 10, right: 10,skin: borderSkin,contents: [Content($, {top: 4, height: 25, width: 25,skin: boxSkin})],Behavior: AnimationBehavior})]})]}));export default new AnimationApp({}, {}); ``` ## Animation Best Practices 1. **Choose appropriate easing** : Use `quadEaseOut` for UI transitions, `bounceEaseOut` for playful effects, `elasticEaseOut` for attention-grabbing animations. 2. **Keep animations short** : 200-500ms for UI transitions, up to 1000ms for decorative animations. 3. **Use appropriate frame rates** : 30fps (33ms interval) is usually sufficient. 60fps (17ms interval) for smoother animations at higher battery cost. Note that these are requests - if Pebble OS can't keep up, the actual frame rate will be lower. Smoothest results tend to come from requesting a frame rate that can be consistently achieved. 4. **Clean up animations** : Stop intervals and timelines when no longer needed. 5. **Consider battery life** : Frequent screen updates drain battery. Use animations sparingly on watchfaces. ## Combining Animations Chain multiple animations using Timeline delays: ``` const timeline = new Timeline();// First animation: move righttimeline.to(content, { x: 100 }, 300, Math.quadEaseOut, 0);// Second animation: move down (starts after 300ms)timeline.to(content, { y: 100 }, 300, Math.quadEaseOut, 300);// Third animation: fade (starts after 600ms)timeline.to(content, { state: 1 }, 200, Math.quadEaseOut, 600); ``` ## Looping Animations Create looping animations by restarting in `onFinished`: ``` class LoopingBehavior extends Behavior {onDisplaying(content) {this.setupAnimation(content);}setupAnimation(content) {const timeline = this.timeline = new Timeline();timeline.to(content, { y: 100 }, 500, Math.sineEaseOut, 0);timeline.to(content, { y: 0 }, 500, Math.sineEaseIn, 500);content.duration = timeline.duration;timeline.seekTo(0);content.time = 0;content.start();}onTimeChanged(content) {this.timeline.seekTo(content.time);}onFinished(content) {// Restart the animationthis.setupAnimation(content);}} ``` ## Examples The [Pebble Examples](https://github.com/Moddable-OpenSource/pebble-examples) repository includes an animation example: - [`hellopiu-timeline`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellopiu-timeline) - demonstrates various easing equations with Timeline animation --- # Alloy Source: https://developer.repebble.com/guides/alloy/ Alloy is a JavaScript framework for building Pebble apps, based on the [Moddable SDK](https://www.moddable.com/). Alloy provides developers a mix of standard Web APIs, standard Embedded JavaScript APIs from ECMA-419, and dedicated Pebble OS APIs. It allows you to write apps using modern JavaScript (ES2025, ES6++) with powerful UI frameworks and access to Pebble hardware features. > **Platform Support** : Alloy currently supports Emery (Pebble Time 2) and Gabbro (Pebble Round 2). See the platform support table in [_Hardware Information_](https://developer.repebble.com/guides/tools-and-resources/hardware-information.md#platform-support). ## Key Features - **Modern JavaScript** : Write apps using ES modules, classes, async/await, and other modern JavaScript features - **Two UI Frameworks** : Choose between Piu (declarative, component-based) or Poco (procedural, low-level graphics) - **Hardware Access** : Full access to accelerometer, battery, buttons, compass, and other Pebble sensors - **Network Communication** : Built-in support for HTTP requests, WebSockets, and watch-phone messaging - **Persistent Storage** : Simple APIs for storing data locally using `localStorage`, key-value storage, or files ## Getting Started To create a new Alloy project: ``` $ pebble new-project --alloy my-app ``` This creates a new project with the standard Alloy structure: ``` my-app/ src/ embeddedjs/ main.js # Watch code (runs on Pebble) pkjs/ index.js # Phone code (for networking, location) c/ mdbl.c # C code as entry point for embeddedjs (usually no need to modify) resources/ # App resources (images, fonts) package.json # App manifest ``` Alloy apps have two JavaScript environments: **embeddedjs** runs on the watch, while **pkjs** runs on the connected phone for network and location services. ## Hello World The simplest Alloy app: ``` console.log("Hello, Pebble!"); ``` ## Rocky.js Rocky.js is no longer part of the firmware or the SDK. Alloy replaces it. ## Guides - [**Getting Started with Alloy**](https://developer.repebble.com/guides/alloy/getting-started.md) - Learn how to create your first Alloy app for Pebble. - [**Piu UI Framework**](https://developer.repebble.com/guides/alloy/piu-guide.md) - Build declarative user interfaces with the Piu framework. - [**Poco Graphics**](https://developer.repebble.com/guides/alloy/poco-guide.md) - Low-level graphics rendering with the Poco framework. - [**Sensors and Input**](https://developer.repebble.com/guides/alloy/sensors-and-input.md) - Access Pebble sensors and handle button input in Alloy apps. - [**Storage**](https://developer.repebble.com/guides/alloy/storage.md) - Persist data between app launches using localStorage, key-value storage, or files. - [**Networking**](https://developer.repebble.com/guides/alloy/networking.md) - Make HTTP requests and use WebSockets via the phone proxy. - [**App Messages**](https://developer.repebble.com/guides/alloy/app-messages.md) - Send and receive messages between the watch and the phone. - [**Watchfaces**](https://developer.repebble.com/guides/alloy/watchfaces.md) - Build custom watchfaces with Alloy. - [**Animations**](https://developer.repebble.com/guides/alloy/animations.md) - Create smooth animations using Timeline and easing functions. - [**Port (Custom Drawing)**](https://developer.repebble.com/guides/alloy/port-drawing.md) - Combine Piu's declarative UI with custom drawing using Port. - [**Advanced Networking**](https://developer.repebble.com/guides/alloy/advanced-networking.md) - Low-level HTTP and WebSocket clients for advanced networking needs. - [**Native Functions (FFI)**](https://developer.repebble.com/guides/alloy/ffi.md) - Call native C functions directly from JavaScript in your Alloy app using the Foreign Function Interface. - [**Dictation**](https://developer.repebble.com/guides/alloy/dictation.md) - Capture speech as text in Alloy apps using the dictation API. - [**Wakeups**](https://developer.repebble.com/guides/alloy/wakeups.md) - Schedule your Alloy app to launch at a future time using wakeups. - [**Vibration**](https://developer.repebble.com/guides/alloy/vibration.md) - Provide haptic feedback in Alloy apps using the vibration motor. - [**Device Info and App Events**](https://developer.repebble.com/guides/alloy/device-info.md) - Query watch and screen properties, respond to app lifecycle events, and control the backlight in Alloy apps. - [**Health**](https://developer.repebble.com/guides/alloy/health.md) - Read steps, sleep, activities and heart rate data from Pebble Health in Alloy apps. - [**Alloy Reference**](https://developer.repebble.com/guides/alloy/reference.md) - Where the Alloy APIs are documented, and the details that are not obvious from the other guides: font names, resource IDs, modules, memory and TypeScript. ## Example Apps The [Moddable Pebble Examples](https://github.com/Moddable-OpenSource/pebble-examples) repository contains a collection of examples covering all aspects of Alloy development. Here's a categorized overview: ### Fundamentals | Example | Description | | --- | --- | | [hellopebble](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellopebble) | "Hello, world" - the simplest starting point | | [hellotimer](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellotimer) | Using `setTimeout` | | [hellomodule](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellomodule) | Loading multiple modules | | [hellotypescript](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellotypescript) | TypeScript with the Pebble `Button` class | ### Storage | Example | Description | | --- | --- | | [hellokeyvalue](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellokeyvalue) | ECMA-419 Key-Value Storage for persistent data | | [hellolocalstorage](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellolocalstorage) | Web standard `localStorage` for persisting strings | | [hellofiles](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellofiles) | File system storage | ### Sensors & Input | Example | Description | | --- | --- | | [helloaccelerometer](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/helloaccelerometer) | Subscribing to accelerometer readings | | [hellobattery](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellobattery) | Battery and charging status | | [hellolocation](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellolocation) | GPS location via phone | | [hellobutton](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellobutton) | Pebble button events | | [hellopoco-drag](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellopoco-drag) | Touch events with the ECMA-419 touch driver | ### Piu UI Framework | Example | Description | | --- | --- | | [hellopiu-text](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellopiu-text) | Dynamic text layout with different fonts | | [hellopiu-pebbletext](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellopiu-pebbletext) | Text using Pebble built-in fonts | | [hellopiu-balls](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellopiu-balls) | Classic bouncing balls demo | | [hellopiu-coloredsquares](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellopiu-coloredsquares) | Drawing colored squares | | [hellopiu-gbitmap](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellopiu-gbitmap) | Pebble GBitmap PNG images as Piu textures | | [hellopiu-jsicon](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellopiu-jsicon) | Moddable SDK bitmaps as Piu textures | | [hellopiu-port](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellopiu-port) | Animated graph using Piu Port | | [hellopiu-timeline](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellopiu-timeline) | Easing equations with Timeline animation | ### Piu Watchfaces | Example | Description | | --- | --- | | [cupertino](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/piu/watchfaces/cupertino) | Classic macOS watch cursor as a watchface | | [london](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/piu/watchfaces/london) | Big Ben (color watches only) | | [helsinki](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/piu/watchfaces/helsinki) | Minimal design with per-model assets | | [redmond](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/piu/watchfaces/redmond) | Classic Windows clock | | [zurich](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/piu/watchfaces/zurich) | Iconic Swiss railway clock | ### Poco Renderer | Example | Description | | --- | --- | | [hellopoco-text](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellopoco-text) | Text rendering with Moddable SDK fonts | | [hellopoco-pebbletext](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellopoco-pebbletext) | Text rendering with Pebble built-in fonts | | [hellopoco-gbitmap](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellopoco-gbitmap) | Rendering GBitmap resources | | [hellopoco-pebblegraphics](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellopoco-pebblegraphics) | Lines, round rectangles, and circles | | [hellopoco-qrcode](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellopoco-qrcode) | Dynamic QR code generation | | [hellopoco-pdc](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellopoco-pdc) | Rendering PDC (SVG) images | | [hellopoco-pdc-rotate](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellopoco-pdc-rotate) | Spinning a PDC image | | [hellopoco-pdc-scale](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellopoco-pdc-scale) | Animated PDC scaling with easing | | [hellopoco-pdc-sequence](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellopoco-pdc-sequence) | PDC image sequence animation | | [hellowatchface](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellowatchface) | Simple watchface app | ### Communication | Example | Description | | --- | --- | | [hellomessage](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellomessage) | Watch-phone messaging via app\_message | | [helloconnected](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/helloconnected) | Phone connection status notifications | | [hellofetch](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellofetch) | HTTP requests using the `fetch()` API | | [hellohttpclient](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellohttpclient) | HTTP requests using ECMA-419 HTTP Client | | [hellowebsocket](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellowebsocket) | WebSocket using the Web standard API | | [hellowebsocketclient](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellowebsocketclient) | WebSocket using ECMA-419 WebSocket Client | ### Sensor Visualizations (Piu) | Example | Description | | --- | --- | | [compass](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/piu/apps/compass) | Compass visualization (Emery only) | | [gravity](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/piu/apps/gravity) | Accelerometer visualization (Emery only) | | [tic-tac-toe](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/piu/apps/tic-tac-toe) | Touch game using Piu Behavior touch events | ### Device, Events & Feedback | Example | Description | | --- | --- | | [helloinfo](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/helloinfo) | Watch, screen, and device information | | [helloappevents](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/helloappevents) | App focus and display resize events | | [hellowakeup](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellowakeup) | Scheduling the app to launch later with wakeups | | [hellodictation](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellodictation) | Capturing speech as text with dictation | | [hellovibes](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellovibes) | Haptic feedback with the vibration motor | | [hellolight](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellolight) | Controlling the backlight | ### Native Code | Example | Description | | --- | --- | | [helloffi](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/helloffi) | Calling native C functions from JavaScript (FFI) | ## Additional Resources - [Moddable SDK Documentation](https://www.moddable.com/documentation/readme) - Comprehensive documentation for Piu, Poco, and other modules --- # Advanced Communication Source: https://developer.repebble.com/guides/communication/advanced-communication/ Many types of connected Pebble watchapps and watchfaces perform common tasks such as the ones discussed here. Following these best practices can increase the quality of the implementation of each one, and avoid common bugs. ## Waiting for PebbleKit JS Any app that wishes to send data from the watch to the phone via [_PebbleKit JS_](https://developer.repebble.com/guides/communication/using-pebblekit-js.md) **must** wait until the JavaScript `ready` event has occured, indicating that the phone has loaded the JavaScript component of the launching app. If this JavaScript code implements the `appmessage` event listsner, it is ready to receive data. > An watchapp that only _receives_ data from PebbleKit JS does not have to wait for the `ready` event. In addition, Android companion apps do not have to wait for such an event thanks to the `Intent` system. iOS companion apps must wait for `-watchDidConnect:`. A simple method is to define a key in `package.json` that will be interpreted by the watchapp to mean that the JS environment is ready for exchange data: ``` "messageKeys": ["JSReady"] ``` The watchapp should implement a variable that describes if the `ready` event has occured. An example is shown below: ``` static bool s\_js\_ready; ``` This can be exported in a header file for other parts of the app to check. Any parts of the app that are waiting should call this as part of a [retry](#timeouts-and-retries) mechanism. ``` bool comm\_is\_js\_ready() {return s\_js\_ready;} ``` The state of this variable will be `false` until set to `true` when the `ready` event causes the key to be transmitted: ``` Pebble.addEventListener('ready', function() {console.log('PebbleKit JS ready.');// Update s\_js\_ready on watchPebble.sendAppMessage({'JSReady': 1});}); ``` This key should be interpreted in the app's [`AppMessageInboxReceived`](https://developer.repebble.com/docs/c/Foundation/AppMessage.md#AppMessageInboxReceived "AppMessageInboxReceived") implementation: ``` static void inbox\_received\_handler(DictionaryIterator \*iter, void \*context) {Tuple \*ready\_tuple = dict\_find(iter, MESSAGE\_KEY\_JSReady);if(ready\_tuple) {// PebbleKit JS is ready! Safe to send messagess\_js\_ready = true;}} ``` ## Timeouts and Retries Due to the wireless and stateful nature of the Bluetooth connection, some messages sent between the watch and phone may fail. A tried-and-tested method for dealing with these failures is to implement a 'timeout and retry' mechanism. Under such a scheme: - A message is sent and a timer started. - If the message is sent successfully (and optionally a reply received), the timer is cancelled. - If the timer elapses before the message can be sent successfully, the message is reattempted. Depending on the nature of the failure, a suitable retry interval (such as a few seconds) is used to avoid saturating the connection. The interval chosen before a timeout occurs and the message is resent may vary depending on the circumstances. The first failure should be reattempted fairly quickly (one second), with the interval increasing as successive failures occurs. If the connection is not available the timer interval should be [even longer](https://en.wikipedia.org/wiki/Exponential_backoff), or wait until the connection is restored. ### Using a Timeout Timer The example below shows the sending of a message and scheduling a timeout timer. The first step is to declare a handle for the timeout timer: ``` static AppTimer \*s\_timeout\_timer; ``` When the message is sent, the timer should be scheduled: ``` static void send\_with\_timeout(int key, int value) {// Construct and send the messageDitionaryIterator \*iter;if(app\_message\_outbox\_begin(&iter) == APP\_MSG\_OK) {dict\_write\_int(iter, key, &value, sizeof(int), true);app\_message\_outbox\_send();}// Schedule the timeout timerconst int interval\_ms = 1000;s\_timout\_timer = app\_timer\_register(interval\_ms, timout\_timer\_handler, NULL);} ``` If the [`AppMessageOutboxSent`](https://developer.repebble.com/docs/c/Foundation/AppMessage.md#AppMessageOutboxSent "AppMessageOutboxSent") is called, the message was a success, and the timer should be cancelled: ``` static void outbox\_sent\_handler(DictionaryIterator \*iter, void \*context) {// Successful message, the timeout is not needed anymore for this messageapp\_timer\_cancel(s\_timout\_timer);} ``` ### Retry a Failed Message However, if the timeout timer elapses before the message's success can be determined or an expected reply is not received, the callback to `timout_timer_handler()` should be used to inform the user of the failure, and schedule another attempt and retry the message: ``` static void timout\_timer\_handler(void \*context) {// The timer elapsed because no success was reportedtext\_layer\_set\_text(s\_status\_layer, "Failed. Retrying...");// Retry the messagesend\_with\_timeout(some\_key, some\_value);} ``` Alternatively, if the [`AppMessageOutboxFailed`](https://developer.repebble.com/docs/c/Foundation/AppMessage.md#AppMessageOutboxFailed "AppMessageOutboxFailed") is called the message failed to send, sometimes immediately. The timeout timer should be cancelled and the message reattempted after an additional delay (the 'retry interval') to avoid saturating the channel: ``` static void outbox\_failed\_handler(DictionaryIterator \*iter,AppMessageResult reason, void \*context) {// Message failed before timer elapsed, reschedule for laterif(s\_timout\_timer) {app\_timer\_cancel(s\_timout\_timer);}// Inform the user of the failuretext\_layer\_set\_text(s\_status\_layer, "Failed. Retrying...");// Use the timeout handler to perform the same action - resend the messageconst int retry\_interval\_ms = 500;app\_timer\_register(retry\_interval\_ms, timout\_timer\_handler, NULL);} ``` > Note: All eventualities where a message fails must invoke a resend of the message, or the purpose of an automated 'timeout and retry' mechanism is defeated. However, the number of attempts made and the interval between them is for the developer to decide. ## Sending Lists Until SDK 3.8, the size of [`AppMessage`](https://developer.repebble.com/docs/c/Foundation/AppMessage.md "AppMessage") buffers did not facilitate sending large amounts of data in one message. With the current buffer sizes of up to 8k for each an outbox the need for efficient transmission of multiple sequential items of data is lessened, but the technique is still important. For instance, to transmit sensor data as fast as possible requires careful scheduling of successive messages. Because there is no guarantee of how long a message will take to transmit, simply using timers to schedule multiple messages after one another is not reliable. A much better method is to make good use of the callbacks provided by the [`AppMessage`](https://developer.repebble.com/docs/c/Foundation/AppMessage.md "AppMessage") API. ### Sending a List to the Phone For instance, the [`AppMessageOutboxSent`](https://developer.repebble.com/docs/c/Foundation/AppMessage.md#AppMessageOutboxSent "AppMessageOutboxSent") callback can be used to safely schedule the next message to the phone, since the previous one has been acknowledged by the other side at that time. Here is an example array of items: ``` static int s\_data[] = { 2, 4, 8, 16, 32, 64 };#define NUM\_ITEMS sizeof(s\_data); ``` A variable can be used to keep track of the current list item index that should be transmitted next: ``` static int s\_index = 0; ``` When a message has been sent, this index is used to construct the next message: > Note: A useful key scheme is to use the item's array index as the key. For PebbleKit JS that number of keys will have to be declared in `package.json`, like so: `someArray[6]` ``` static void outbox\_sent\_handler(DictionaryIterator \*iter, void \*context) {// Increment the indexs\_index++;if(s\_index \< NUM\_ITEMS) {// Send the next itemDictionaryIterator \*iter;if(app\_message\_outbox\_begin(&iter) == APP\_MSG\_OK) {dict\_write\_int(iter, MESSAGE\_KEY\_someArray + s\_index, &s\_data[s\_index], sizeof(int), true);app\_message\_outbox\_send();}} else {// We have reached the end of the sequenceAPP\_LOG(APP\_LOG\_LEVEL\_INFO, "All transmission complete!");}} ``` This results in a callback loop that repeats until the last data item has been transmitted, and the index becomes equal to the total number of items. This technique can be combined with a timeout and retry mechanism to reattempt a particular item if transmission fails. This is a good way to avoid gaps in the received data items. On the phone side, the data items are received in the same order. An analogous `index` variable is used to keep track of which item has been received. This process will look similar to the example shown below: ``` var NUM\_ITEMS = 6;var keys = require('message\_keys');var data = [];var index = 0;Pebble.addEventListener('appmessage', function(e) {// Store this data itemdata[index] = e.payload[keys.someArray + index];// Increment index for next messageindex++;if(index == NUM\_ITEMS) {console.log('Received all data items!');}}); ``` ### Sending a List to Pebble Conversely, the `success` callback of `Pebble.sendAppMessage()` in PebbleKit JS is the equivalent safe time to send the next message to the watch. An example implementation that achieves this is shown below. After the message is sent with `Pebble.sendAppMessage()`, the `success` callback calls the `sendNextItem()` function repeatedly until the index is larger than that of the last list item to be sent, and transmission will be complete. Again, an index variable is maintained to keep track of which item is being transmitted: ``` var keys = require('message\_keys');function sendNextItem(items, index) {// Build messagevar key = keys.someArray + index;var dict = {};dict[key] = items[index];// Send the messagePebble.sendAppMessage(dict, function() {// Use success callback to increment indexindex++;if(index \< items.length) {// Send next itemsendNextItem(items, index);} else {console.log('Last item sent!');}}, function() {console.log('Item transmission failed at index: ' + index);});}function sendList(items) {var index = 0;sendNextItem(items, index);}function onDownloadComplete(responseText) {// Some web response containing a JSON object arrayvar json = JSON.parse(responseText);// Begin transmission loopsendList(json.items);} ``` On the watchapp side, the items are received in the same order in the [`AppMessageInboxReceived`](https://developer.repebble.com/docs/c/Foundation/AppMessage.md#AppMessageInboxReceived "AppMessageInboxReceived") handler: ``` #define NUM\_ITEMS 6static int s\_index;static int s\_data[NUM\_ITEMS]; ``` ``` static void inbox\_received\_handler(DictionaryIterator \*iter, void \*context) {Tuple \*data\_t = dict\_find(iter, MESSAGE\_KEY\_someArray + s\_index);if(data\_t) {// Store this items\_data[index] = (int)data\_t-\>value-\>int32;// Increment index for next items\_index++;}if(s\_index == NUM\_ITEMS) {// We have reached the end of the sequenceAPP\_LOG(APP\_LOG\_LEVEL\_INFO, "All transmission complete!");}} ``` This sequence of events is demonstrated for PebbleKit JS, but the same technique can be applied exactly to either and Android or iOS companion app wishing to transmit many data items to Pebble. Get the complete source code for this example from the [`list-items-example`](https://github.com/pebble-examples/list-items-example) repository on GitHub. ## Sending Image Data A common task developers want to accomplish is display a dynamically loaded image resource (for example, showing an MMS photo or a news item thumbnail pulled from a webservice). Because some images could be larger than the largest buffer size available to the app, the techniques shown above for sending lists also prove useful here, as the image is essentially a list of color byte values. ### Image Data Format There are two methods available for displaying image data downloaded from the web: 1. Download a `png` image, transmit the compressed data, and decompress using [`gbitmap_create_from_png_data()`](https://developer.repebble.com/docs/c/Graphics/Graphics_Types.md#gbitmap_create_from_png_data "gbitmap\_create\_from\_png\_data"). This involves sending less data, but can be prone to failure depending on the exact format of the image. The image must be in a compatible palette (1, 2, 4, or 8-bit) and small enough such that there is enough memory for a compessed copy, an uncompressed copy, and ~2k overhead when it is being processed. 2. Download a `png` image, decompress in the cloud or in PebbleKit JS into an array of image pixel bytes, transmit the pixel data into a blank [`GBitmap`](https://developer.repebble.com/docs/c/Graphics/Graphics_Types.md#GBitmap "GBitmap")'s `data` member. Each byte must be in the compatible Pebble color format (2 bits per ARGB). This process can be simplified by pre-formatting the image to be dowloaded, as resizing or palette-reduction is difficult to do locally. ### Sending Compressed PNG Data As the fastest and least complex of the two methods described above, an example of how to display a compressed PNG image will be discussed here. The image that will be displayed is [the HTML 5 logo](https://www.w3.org/html/logo/): ![The HTML5 logo.](https://developer.repebble.com/assets/images/guides/pebble-apps/communications/html5-logo-small.png) > Note: The above image has been resized and palettized for compatibility. To download this image in PebbleKit JS, use an `XmlHttpRequest` object. It is important to specify the `responseType` as 'arraybuffer' to obtain the image data in the correct format: ``` function downloadImage() {var url = 'https://developer.repebble.com/assets/images/guides/pebble-apps/communications/html5-logo-small.png';var request = new XMLHttpRequest();request.onload = function() {processImage(this.response);};request.responseType = "arraybuffer";request.open("GET", url);request.send();} ``` When the response has been received, `processImage()` will be called. The received data must be converted into an array of unsigned bytes, which is achieved through the use of a `Uint8Array`. This process is shown below (see the [`png-download-example`](https://github.com/pebble-examples/png-download-example) repository for the full example): ``` function processImage(responseData) {// Convert to a arrayvar byteArray = new Uint8Array(responseData);var array = [];for(var i = 0; i \< byteArray.byteLength; i++) {array.push(byteArray[i]);}// Send chunks to PebbletransmitImage(array);} ``` Now that the image data has been converted, the transmission to Pebble can begin. At a high level, the JS side transmits the image data in chunks, using an incremental array index to coordinate saving of data on the C side in a mirror array. In downloading the image data, the following keys are used for the specified purposes: | Key | Purpose | | --- | --- | | `Index` | The array index that the current chunk should be stored at. This gets larger as each chunk is transmitted. | | `DataLength` | This length of the entire data array to be downloaded. As the image is compressed, this is _not_ the product of the width and height of the image. | | `DataChunk` | The chunk's image data. | | `ChunkSize` | The size of this chunk. | | `Complete` | Used to signify that the image transfer is complete. | The first message in the sequence should tell the C side how much memory to allocate to store the compressed image data: ``` function transmitImage(array) {var index = 0;var arrayLength = array.length;// Transmit the length for array allocationPebble.sendAppMessage({'DataLength': arrayLength}, function(e) {// Success, begin sending chunkssendChunk(array, index, arrayLength);}, function(e) {console.log('Failed to initiate image transfer!');})} ``` If this message is successful, the transmission of actual image data commences with the first call to `sendChunk()`. This function calculates the size of the next chunk (the smallest of either the size of the `AppMessage` inbox buffer, or the remainder of the data) and assembles the dictionary containing the index in the array it is sliced from, the length of the chunk, and the actual data itself: ``` function sendChunk(array, index, arrayLength) {// Determine the next chunk sizevar chunkSize = BUFFER\_SIZE;if(arrayLength - index \< BUFFER\_SIZE) {// Resize to fit just the remaining data itemschunkSize = arrayLength - index;}// Prepare the dictionaryvar dict = {'DataChunk': array.slice(index, index + chunkSize),'ChunkSize': chunkSize,'Index': index};// Send the chunkPebble.sendAppMessage(dict, function() {// Successindex += chunkSize;if(index \< arrayLength) {// Send the next chunksendChunk(array, index, arrayLength);} else {// Complete!Pebble.sendAppMessage({'Complete': 0});}}, function(e) {console.log('Failed to send chunk with index ' + index);});} ``` After each chunk is sent, the index is incremented with the size of the chunk that was just sent, and compared to the total length of the array. If the index exceeds the size of the array, the loop has sent all the data (this could be just a single chunk if the array is smaller than the maximum message size). The `AppKeyComplete` key is sent to inform the C side that the image is complete and ready for display. ### Receiving Compressed PNG Data In the previous section, the process for using PebbleKit JS to download and transmit an image to the C side was discussed. The process for storing and displaying this data is discussed here. Only when both parts work in harmony can an image be successfully shown from the web. The majority of the process takes place within the watchapp's [`AppMessageInboxReceived`](https://developer.repebble.com/docs/c/Foundation/AppMessage.md#AppMessageInboxReceived "AppMessageInboxReceived") handler, with the presence of each key being detected and the appropriate actions taken to reconstruct the image. The first item expected is the total size of the data to be transferred. This is recorded (for later use with [`gbitmap_create_from_png_data()`](https://developer.repebble.com/docs/c/Graphics/Graphics_Types.md#gbitmap_create_from_png_data "gbitmap\_create\_from\_png\_data")) and the buffer used to store the chunks is allocated to this exact size: ``` static uint8\_t \*s\_img\_data;static int s\_img\_size; ``` ``` // Get the received image chunkTuple \*img\_size\_t = dict\_find(iter, MESSAGE\_KEY\_DataLength);if(img\_size\_t) {s\_img\_size = img\_size\_t-\>value-\>int32;// Allocate buffer for image dataimg\_data = (uint8\_t\*)malloc(s\_img\_size \* sizeof(uint8\_t));} ``` When the message containing the data size is acknowledged, the JS side begins sending chunks with `sendChunk()`. When one of these subsequent messages is received, the three keys (`DataChunk`, `ChunkSize`, and `Index`) are used to store that chunk of data at the correct offset in the array: ``` // An image chunkTuple \*chunk\_t = dict\_find(iter, MESSAGE\_KEY\_DataChunk);if(chunk\_t) {uint8\_t \*chunk\_data = chunk\_t-\>value-\>data;Tuple \*chunk\_size\_t = dict\_find(iter, MESSAGE\_KEY\_ChunkSize);int chunk\_size = chunk\_size\_t-\>value-\>int32;Tuple \*index\_t = dict\_find(iter, MESSAGE\_KEY\_Index);int index = index\_t-\>value-\>int32;// Save the chunkmemcpy(&s\_img\_data[index], chunk\_data, chunk\_size);} ``` Finally, once the array index exceeds the size of the data array on the JS side, the `AppKeyComplete` key is transmitted, triggering the data to be transformed into a [`GBitmap`](https://developer.repebble.com/docs/c/Graphics/Graphics_Types.md#GBitmap "GBitmap"): ``` static BitmapLayer \*s\_bitmap\_layer;static GBitmap \*s\_bitmap; ``` ``` // Complete?Tuple \*complete\_t = dict\_find(iter, MESSAGE\_KEY\_Complete);if(complete\_t) {// Create new GBitmap from downloaded PNG datas\_bitmap = gbitmap\_create\_from\_png\_data(s\_img\_data, s\_img\_size);// Show the imageif(s\_bitmap) {bitmap\_layer\_set\_bitmap(s\_bitmap\_layer, s\_bitmap);} else {APP\_LOG(APP\_LOG\_LEVEL\_ERROR, "Error creating GBitmap from PNG data!");}} ``` The final result is a compressed PNG image downloaded from the web displayed in a Pebble watchapp. ![watch](https://developer.repebble.com/assets/images/guides/pebble-apps/communications/html-5-watch.png) Get the complete source code for this example from the [`png-download-example`](https://github.com/pebble-examples/png-download-example) repository on GitHub. --- # Communication Source: https://developer.repebble.com/guides/communication/ All Pebble watchapps and watchfaces have the ability to communicate with the outside world through its connection to the user's phone. The PebbleKit collection of libraries (see below) is available to facilitate this communication between watchapps and phone apps. Examples of additional functionality made possible through PebbleKit include, but are not limited to apps that can: - Display weather, news, stocks, etc. - Communicate with other web services. - Read and control platform APIs and features of the connected phone. ## Contents - [**Advanced Communication**](https://developer.repebble.com/guides/communication/advanced-communication.md) - Details of communication tips and best practices for more advanced scenarios. - [**Datalogging**](https://developer.repebble.com/guides/communication/datalogging.md) - Information on how to collect data batches using the Datalogging API. - [**PebbleKit Android**](https://developer.repebble.com/guides/communication/using-pebblekit-android.md) - How to use PebbleKit to communicate with a watchapp on Android. - [**PebbleKit JS**](https://developer.repebble.com/guides/communication/using-pebblekit-js.md) - How to use PebbleKit JS to communicate with the connected phone's JS environment, and how the Pebble mobile app runs it on Android and iOS. - [**PebbleKit iOS**](https://developer.repebble.com/guides/communication/using-pebblekit-ios.md) - How to use PebbleKit to communicate with a watchapp on iOS. - [**Sending and Receiving Data**](https://developer.repebble.com/guides/communication/sending-and-receiving-data.md) - How to send and receive data between your watchapp and phone. - [**Sports API**](https://developer.repebble.com/guides/communication/using-the-sports-api.md) - How to use the PebbleKit Sports API to integrate your mobile sports app with Pebble. ## Communication Model Pebble communicates with the connected phone via the Bluetooth connection, which is the same connection that delivers notifications and other alerts in normal use. Developers can leverage this connection to send and receive arbitrary data using the [`AppMessage`](https://developer.repebble.com/docs/c/Foundation/AppMessage.md "AppMessage") API. Depending on the requirements of the app, there are three possible ways to receive data sent from Pebble on the connected phone: - [_PebbleKit JS_](https://developer.repebble.com/guides/communication/using-pebblekit-js.md) - A JavaScript environment running within the official Pebble mobile app with web, geolocation, and extended storage access. - [_PebbleKit Android_](https://developer.repebble.com/guides/communication/using-pebblekit-android.md) - A library available to use in Android companion apps that allows them to interact with standard Android platform APIs. - [_PebbleKit iOS_](https://developer.repebble.com/guides/communication/using-pebblekit-ios.md) - As above, but for iOS companion apps. **Important** PebbleKit JS cannot be used in conjunction with PebbleKit Android or PebbleKit iOS. All messages sent from a Pebble watchapp or watchface will be delivered to the appropriate phone app depending on the layout of the developer's project: - If at least an `index.js` file is present in `src/pkjs/`, the message will be handled by PebbleKit JS. - If there is no valid JS file present (at least an `index.js`) in the project, the message will be delivered to the official Pebble mobile app. If there is a companion app installed that has registered a listener with the same UUID as the watchapp, the message will be forwarded to that app via PebbleKit Android/iOS. --- # Datalogging Source: https://developer.repebble.com/guides/communication/datalogging/ In addition to the more realtime [`AppMessage`](https://developer.repebble.com/docs/c/Foundation/AppMessage.md "AppMessage") API, the Pebble SDK also includes the [`Datalogging`](https://developer.repebble.com/docs/c/Foundation/DataLogging.md "DataLogging") API. This is useful for applications where data can be sent in batches at time intervals that make the most sense (for example, to save battery power by allowing the watch to spend more time sleeping). Datalogging also allows upto 640 kB of data to be buffered on the watch until a connection is available, instead of requiring a connection be present at all times. If data is logged while the watch is disconnected, it will be transferred to the Pebble mobile app in batches for processing at the next opportunity. The data is then passed on to any [_PebbleKit Android_](https://developer.repebble.com/guides/communication/using-pebblekit-android.md) or [_PebbleKit iOS_](https://developer.repebble.com/guides/communication/using-pebblekit-ios.md) companion app that wishes to process it. ## Collecting Data Datalogging can capture any values that are compatible with one of the [`DataLoggingItemType`](https://developer.repebble.com/docs/c/Foundation/DataLogging.md#DataLoggingItemType "DataLoggingItemType") `enum` (byte array, unsigned integer, and integer) values, with common sources including accelerometer data or compass data. ### Creating a Session Data is logged to a 'session' with a unique identifier or tag, which allows a single app to have multiple data logs for different types of data. First, define the identifier(s) that should be used where appropriate: ``` // The log's ID. Only one required in this example#define TIMESTAMP\_LOG 1 ``` Next, a session must first be created before any data can be logged to it. This should be done during app initialization, or just before the first time an app needs to log some data: ``` // The session reference variablestatic DataLoggingSessionRef s\_session\_ref; ``` ``` static void init() {// Begin the sessions\_session\_ref = data\_logging\_create(TIMESTAMP\_LOG, DATA\_LOGGING\_INT, sizeof(int), true);/\* ... \*/} ``` > Note: The final parameter of [`data_logging_create()`](https://developer.repebble.com/docs/c/Foundation/DataLogging.md#data_logging_create "data\_logging\_create") allows a previous log session to be continued, instead of starting from screatch on each app launch. ### Logging Data Once the log has been created or resumed, data collection can proceed. Each call to [`data_logging_log()`](https://developer.repebble.com/docs/c/Foundation/DataLogging.md#data_logging_log "data\_logging\_log") will add a new entry to the log indicated by the [`DataLoggingSessionRef`](https://developer.repebble.com/docs/c/Foundation/DataLogging.md#DataLoggingSessionRef "DataLoggingSessionRef") variable provided. The success of each logging operation can be checked using the [`DataLoggingResult`](https://developer.repebble.com/docs/c/Foundation/DataLogging.md#DataLoggingResult "DataLoggingResult") returned: ``` const int value = 16;const uint32\_t num\_values = 1;// Log a single valueDataLoggingResult result = data\_logging\_log(s\_session\_ref, &value, num\_values);// Was the value successfully stored? If it failed, print the reasonif(result != DATA\_LOGGING\_SUCCESS) {APP\_LOG(APP\_LOG\_LEVEL\_ERROR, "Error logging data: %d", (int)result);} ``` ### Finishing a Session Once all data has been logged or the app is exiting, the session must be finished to signify that the data is to be either transferred to the connected phone (if available), or stored for later transmission. ``` // Finish the session and sync data if appropriatedata\_logging\_finish(s\_session\_ref); ``` > Note: Once a session has been finished, data cannot be logged to its [`DataLoggingSessionRef`](https://developer.repebble.com/docs/c/Foundation/DataLogging.md#DataLoggingSessionRef "DataLoggingSessionRef") until it is resumed or began anew. ## Receiving Data > Note: Datalogging data cannot be received via PebbleKit JS. Data collected with the [`Datalogging`](https://developer.repebble.com/docs/c/Foundation/DataLogging.md "DataLogging") API can be received and processed in a mobile companion app using PebbleKit Android or PebbleKit iOS. This enables it to be used in a wide range of general applications, such as detailed analysis of accelerometer data for health research, or transmission to a third-party web service. ### With PebbleKit Android PebbleKit Android allows collection of data by registering a `PebbleDataLogReceiver` within your `Activity` or `Service`. When creating a receiver, be careful to provide the correct UUID to match that of the watchapp that is doing that data collection. For example: ``` // The UUID of the watchappprivate UUID APP\_UUID = UUID.fromString("64fcb54f-76f0-418a-bd7d-1fc1c07c9fc1"); ``` Use the following overridden methods to collect data and determine when the session has been finished by the watchapp. In the example below, each new integer received represents the uptime of the watchapp, and is displayed in an Android `TextView`: ``` // Create a receiver to collect logged dataPebbleKit.PebbleDataLogReceiver dataLogReceiver = new PebbleKit.PebbleDataLogReceiver(APP\_UUID) {@Overridepublic void receiveData(Context context, UUID logUuid, Long timestamp, Long tag, int data) {// super() (removed from IDE-generated stub to avoid exception)Log.i(TAG, "New data for session " + tag + "!");// Cumulatively add the new data item to a TextView's current textString current = dataView.getText().toString();current += timestamp.toString() + ": " + data + "s since watchapp launch.\n";dataView.setText(current);}@Overridepublic void onFinishSession(Context context, UUID logUuid, Long timestamp, Long tag) {Log.i(TAG, "Session " + tag + " finished!");}};// Register the receiverPebbleKit.registerDataLogReceiver(getApplicationContext(), dataLogReceiver); ``` **Important** If your Java IDE automatically adds a line of code to call super() when you create the method, the code will result in an UnsupportedOperationException. Ensure you remove this line to avoid the exception. Once the `Activity` or `Service` is closing, you should attempt to unregister the receiver. However, this is not always required (and will cause an exception to be thrown if invoked when not required), so use a `try, catch` statement: ``` @Overrideprotected void onPause() {super.onPause();try {unregisterReceiver(dataLogReceiver);} catch(Exception e) {Log.w(TAG, "Receiver did not need to be unregistered");}} ``` ### With PebbleKit iOS The process of collecing data via a PebbleKit iOS companion mobile app is similar to that of using PebbleKit Android. Once your app is a delegate of [`PBDataLoggingServiceDelegate`](https://developer.repebble.com/docs/pebblekit-ios/Protocols/PBDataLoggingServiceDelegate.md "PBDataLoggingServiceDelegate") (see [_PebbleKit iOS_](https://developer.repebble.com/guides/communication/using-pebblekit-ios.md) for details), simply register the class as a datalogging delegate: ``` // Get datalogging data by becoming the delegate[[PBPebbleCentral defaultCentral] dataLoggingServiceForAppUUID:myAppUUID].delegate = self; ``` Being a datalogging delegate allows the class to receive two additional [callbacks](https://developer.repebble.com/docs/pebblekit-ios/Protocols/PBDataLoggingServiceDelegate.md) for when new data is available, and when the session has been finished by the watch. Implement these callbacks to read the new data: ``` -(BOOL)dataLoggingService:(PBDataLoggingService \*)servicehasSInt32s:(const SInt32 [])datanumberOfItems:(UInt16)numberOfItemsforDataLog:(PBDataLoggingSessionMetadata \*)log {NSLog(@"New data received!");// Append newest data to displayed stringNSString \*current = self.dataView.text;NSString \*newString = [NSString stringWithFormat:@"New item: %d", data[0]];current = [current stringByAppendingString:newString];self.dataView.text = current;return YES;}-(void)dataLoggingService:(PBDataLoggingService \*)servicelogDidFinish:(PBDataLoggingSessionMetadata \*)log {NSLog(@"Finished data log: %@", log);} ``` ### Special Considerations for iOS Apps - The logic to deal with logs with the same type of data (i.e., the same tag/type) but from different sessions (different timestamps) must be created by the developer using the delegate callbacks. - To check whether the data belongs to the same log or not, use `-isEqual:` on `PBDataLoggingSessionMetadata`. For convenience, `PBDataLoggingSessionMetadata` can be serialized using `NSCoding`. - Using multiple logs in parallel (for example to transfer different kinds of information) will require extra logic to re-associate the data from these different logs, which must also be implemented by the developer. --- # PebbleKit Android Source: https://developer.repebble.com/guides/communication/using-pebblekit-android/ # PebbleKit Android 2 Currently recommended way of communicating with the watch is to use PebbleKit Android 2. See [its documentation](https://github.com/pebble-dev/PebbleKitAndroid2) for more information. # Legacy PebbleKit Android 1 [PebbleKit Android](https://github.com/pebble/pebble-android-sdk/) is a Java library that works with the Pebble SDK and can be embedded in any Android application. Using the classes and methods in this library, an Android companion app can find and exchange data with a Pebble watch. This section assumes that the reader is familiar with basic Android development and Android Studio as an integrated development environment. Refer to the [Android Documentation](http://developer.android.com/sdk/index.html) for more information on installing the Android SDK. Most PebbleKit Android methods require a `Context` parameter. An app can use `getApplicationContext()`, which is available from any `Activity` implementation. ### Setting Up PebbleKit Android Add PebbleKit Android to an Android Studio project in the `app/build.gradle` file: ``` dependencies { compile 'com.getpebble:pebblekit:4.0.1' } ``` ### Sending Messages from Android Since Android apps are built separately from their companion Pebble apps, there is no way for the build system to automatically create matching appmessage keys. You must therefore manually specify them in `package.json`, like so: ``` {"ContactName": 0,"Age": 1} ``` These numeric values can then be used as appmessage keys in your Android app. Messages are constructed with the `PebbleDictionary` class and sent to a C watchapp or watchface using the `PebbleKit` class. The first step is to create a `PebbleDictionary` object: ``` // Create a new dictionaryPebbleDictionary dict = new PebbleDictionary(); ``` Data items are added to the [`PebbleDictionary`](https://developer.repebble.com/docs/pebblekit-android/com/getpebble/android/kit/util/PebbleDictionary.md) using key-value pairs with the methods made available by the object, such as `addString()` and `addInt32()`. An example is shown below: ``` // The key representing a contact name is being transmittedfinal int AppKeyContactName = 0;final int AppKeyAge = 1;// Get data from the appfinal String contactName = getContact();final int age = getAge();// Add data to the dictionarydict.addString(AppKeyContactName, contactName);dict.addInt32(AppKeyAge, age); ``` Finally, the dictionary is sent to the C app by calling `sendDataToPebble()` with a UUID matching that of the C app that will receive the data: ``` final UUID appUuid = UUID.fromString("EC7EE5C6-8DDF-4089-AA84-C3396A11CC95");// Send the dictionaryPebbleKit.sendDataToPebble(getApplicationContext(), appUuid, dict); ``` Once delivered, this dictionary will be available in the C app via the [`AppMessageInboxReceived`](https://developer.repebble.com/docs/c/Foundation/AppMessage.md#AppMessageInboxReceived "AppMessageInboxReceived") callback, as detailed in [_Sending and Receiving Data_](https://developer.repebble.com/guides/communication/sending-and-receiving-data.md#inbox-received). ### Receiving Messages on Android Receiving messages from Pebble in a PebbleKit Android app requires a listener to be registered in the form of a `PebbleDataReceiver` object, which extends `BroadcastReceiver`: ``` // Create a new receiver to get AppMessages from the C appPebbleDataReceiver dataReceiver = new PebbleDataReceiver(appUuid) {@Overridepublic void receiveData(Context context, int transaction\_id,PebbleDictionary dict) {// A new AppMessage was received, tell PebblePebbleKit.sendAckToPebble(context, transaction\_id);}}; ``` **Important** PebbleKit apps **must** manually send an acknowledgement (Ack) to Pebble to inform it that the message was received successfully. Failure to do this will cause timeouts. Once created, this receiver should be registered in `onResume()`, overridden from `Activity`: ``` @Overridepublic void onResume() {super.onResume();// Register the receiverPebbleKit.registerReceivedDataHandler(getApplicationContext(), dataReceiver);} ``` > Note: To avoid getting callbacks after the `Activity` or `Service` has exited, apps should attempt to unregister the receiver in `onPause()` with `unregisterReceiver()`. With a receiver in place, data can be read from the provided [`PebbleDictionary`](https://developer.repebble.com/docs/pebblekit-android/com/getpebble/android/kit/util/PebbleDictionary.md) using analogous methods such as `getString()` and `getInteger()`. Before reading the value of a key, the app should first check that it exists using a `!= null` check. The example shown below shows how to read an integer from the message, in the scenario that the watch is sending an age value to the Android companion app: ``` @Overridepublic void receiveData(Context context, int transaction\_id,PebbleDictionary dict) {// If the tuple is present...Long ageValue = dict.getInteger(AppKeyAge);if(ageValue != null) {// Read the integer valueint age = ageValue.intValue();}} ``` ### Other Capabilities In addition to sending and receiving messages, PebbleKit Android also allows more intricate interactions with Pebble. See the [PebbleKit Android Documentation](https://developer.repebble.com/docs/pebblekit-android/com/getpebble/android/kit/PebbleKit.md) for a complete list of available methods. Some examples are shown below of what is possible: - Checking if the watch is connected: - Registering for connection events with `registerPebbleConnectedReceiver()` and `registerPebbleDisconnectedReceiver()`, and a suitable `BroadcastReceiver`. - Registering for Ack/Nack events with `registerReceivedAckHandler()` and `registerReceivedNackHandler()`. - Launching and killing the watchapp with `startAppOnPebble()` and `closeAppOnPebble()`. --- # PebbleKit JS Source: https://developer.repebble.com/guides/communication/using-pebblekit-js/ PebbleKit JS allows a JavaScript component (run in a sandbox inside the official Pebble mobile app) to be added to any watchapp or watchface in order to extend the functionality of the app beyond what can be accomplished on the watch itself. Extra features available to an app using PebbleKit JS include: - Access to extended storage with [`localStorage`](#using-localstorage). - Internet access using [`XMLHttpRequest`](#using-xmlhttprequest). - Location data using [`geolocation`](#using-geolocation). - The ability to show a configuration page to allow users to customize how the app behaves. This is discussed in detail in [_App Configuration_](https://developer.repebble.com/guides/user-interfaces/app-configuration.md). ## Setting Up PebbleKit JS can be set up by creating the `index.js` file in the project's `src/pkjs/` directory. Code in this file will be executed when the associated watchapp is launched, and will stop once that app exits. The basic JS code required to begin using PebbleKit JS is shown below. An event listener is created to listen for the `ready` event - fired when the watchapp has been launched and the JS environment is ready to receive messages. This callback must return within a short space of time (a few seconds) or else it will timeout and be killed by the phone. ``` Pebble.addEventListener('ready', function() {// PebbleKit JS is ready!console.log('PebbleKit JS ready!');}); ``` **Important** A watchapp or watchface **must** wait for the `ready` event before attempting to send messages to the connected phone. See [_Advanced Communication_](https://developer.repebble.com/guides/communication/advanced-communication.md#waiting-for-pebblekit-js) to learn how to do this. ## Defining Keys Before any messages can be sent or received, the keys to be used to store the data items in the dictionary must be declared. The watchapp side uses exclusively integer keys, whereas the JavaScript side may use the same integers or named string keys declared in `package.json`. Any string key not declared beforehand will not be transmitted to/from Pebble. > Note: This requirement is true of PebbleKit JS **only** , and not PebbleKit Android or iOS. Keys are declared in the project's `package.json` file in the `messageKeys` object, which is inside the `pebble` object. Example keys are shown as equivalents to the ones used in the hypothetical weather app example in [_Sending and Receiving Data_](https://developer.repebble.com/guides/communication/sending-and-receiving-data.md#choosing-keys). ``` "messageKeys": ["Temperature","WindSpeed","WindDirection","RequestData","LocationName"] ``` The names chosen here will be injected into your C code prefixed with `MESSAGE_KEY_`, like `MESSAGE_KEY_Temperature`. As such, they must be legal C identifiers. If you want to emulate an array by attaching multiple "keys" to a name, you can specify the size of the array by adding it in square brackets: for instance, `"LapTimes[10]`" would create a key called `LapTimes` and leave nine empty keys after it which can be accessed by arithmetic, e.g. `MESSAGE_KEY_LapTimes + 3`. ## Sending Messages from JS Messages are sent to the C watchapp or watchface using `Pebble.sendAppMessage()`, which accepts a standard JavaScript object containing the keys and values to be transmitted. The keys used **must** be identical to the ones declared earlier. An example is shown below: ``` // Assemble data objectvar dict = {'Temperature': 29,'LocationName': 'London, UK'};// Send the objectPebble.sendAppMessage(dict, function() {console.log('Message sent successfully: ' + JSON.stringify(dict));}, function(e) {console.log('Message failed: ' + JSON.stringify(e));}); ``` It is also possible to read the numeric values of the keys by `require`ing `message_keys`, which is necessary to use the array feature. For instance: ``` // Require the keys' numeric values.var keys = require('message\_keys');// Build a dictionary.var dict = {}dict[keys.LapTimes] = 42dict[keys.LapTimes+1] = 51// Send the objectPebble.sendAppMessage(dict, function() {console.log('Message sent successfully: ' + JSON.stringify(dict));}, function(e) {console.log('Message failed: ' + JSON.stringify(e));}); ``` ### Type Conversion Depending on the type of the item in the object to be sent, the C app will be able to read the value (from the [`Tuple.value` union](https://developer.repebble.com/guides/communication/sending-and-receiving-data.md#data-types)) according to the table below: | JS Type | Union member | | --- | --- | | String | cstring | | Number | int32 | | Array | data | | Boolean | int16 | ## Receiving Messages in JS When a message is received from the C watchapp or watchface, the `appmessage` event is fired in the PebbleKit JS app. To receive these messages, register the appropriate event listener: ``` // Get AppMessage eventsPebble.addEventListener('appmessage', function(e) {// Get the dictionary from the messagevar dict = e.payload;console.log('Got message: ' + JSON.stringify(dict));}); ``` Data can be read from the dictionary by reading the value if it is present. A suggested best practice involves first checking for the presence of each key within the callback using an `if()` statement. ``` if(dict['RequestData']) {// The RequestData key is present, read the valuevar value = dict['RequestData'];} ``` ## Using LocalStorage In addition to the storage available on the watch itself through the [`Storage`](https://developer.repebble.com/docs/c/Foundation/Storage.md "Storage") API, apps can take advantage of the larger storage on the connected phone through the use of the HTML 5 [`localStorage`](http://www.w3.org/TR/webstorage/) API. Data stored here will persist across app launches, and so can be used to persist latest data, app settings, and other data. PebbleKit JS `localStorage` is: - Associated with the application UUID and cannot be shared between apps. - Persisted when the user uninstalls and then reinstalls an app. - Persisted when the user upgrades an app. To store a value: ``` var color = '#FF0066';// Store some datalocalStorage.setItem('backgroundColor', color); ``` To read the data back: ``` var color = localStorage.getItem('backgroundColor'); ``` > Note: Keys used with `localStorage` should be Strings. ## Using XMLHttpRequest A PebbleKit JS-equipped app can access the internet and communicate with web services or download data using the standard [`XMLHttpRequest`](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest) object. To communicate with the web, create an `XMLHttpRequest` object and send it, specifying the HTTP method and URL to be used, as well as a callback for when it is successfully completed: ``` var method = 'GET';var url = 'http://example.com';// Create the requestvar request = new XMLHttpRequest();// Specify the callback for when the request is completedrequest.onload = function() {// The request was successfully completed!console.log('Got response: ' + this.responseText);};// Send the requestrequest.open(method, url);request.send(); ``` If the response is expected to be in the JSON format, data items can be easily read after the `responseText` is converted into a JSON object: ``` request.onload = function() {try {// Transform in to JSONvar json = JSON.parse(this.responseText);// Read datavar temperature = json.main.temp;} catch(err) {console.log('Error parsing JSON response!');}}; ``` ## Using Geolocation PebbleKit JS provides access to the location services provided by the phone through the [`navigator.geolocation`](http://dev.w3.org/geo/api/spec-source.html) object. Declare that the app will be using the `geolocation` API by adding the string `location` in the `capabilities` array in `package.json`: ``` "capabilities": ["location"] ``` Below is an example showing how to get a single position value from the `geolocation` API using the [`getCurrentPosition()`](https://developer.mozilla.org/en-US/docs/Web/API/Geolocation/getCurrentPosition) method: ``` function success(pos) {console.log('lat= ' + pos.coords.latitude + ' lon= ' + pos.coords.longitude);}function error(err) {console.log('location error (' + err.code + '): ' + err.message);}/\* ... \*/// Choose options about the data returnedvar options = {enableHighAccuracy: true,maximumAge: 10000,timeout: 10000};// Request current positionnavigator.geolocation.getCurrentPosition(success, error, options); ``` Location permission is given by the user to the Pebble application for all Pebble apps. The app should gracefully handle the `PERMISSION DENIED` error and fallback to a default value or manual configuration when the user has denied location access to Pebble apps. ``` function error(err) {if(err.code == err.PERMISSION\_DENIED) {console.log('Location access was denied by the user.');} else {console.log('location error (' + err.code + '): ' + err.message);}} ``` The `geolocation` API also provides a mechanism to receive callbacks when the user's position changes to avoid the need to manually poll at regular intervals. This is achieved by using [`watchPosition()`](https://developer.mozilla.org/en-US/docs/Web/API/Geolocation/watchPosition) in a manner similar to the example below: ``` // An ID to store to later clear the watchvar watchId;function success(pos) {console.log('Location changed!');console.log('lat= ' + pos.coords.latitude + ' lon= ' + pos.coords.longitude);}function error(err) {console.log('location error (' + err.code + '): ' + err.message);}/\* ... \*/var options = {enableHighAccuracy: true,maximumAge: 0,timeout: 5000};// Get location updateswatchId = navigator.geolocation.watchPosition(success, error, options); ``` To cancel the update callbacks, use the `watchId` variable received when the watch was registered with the [`clearWatch()`](https://developer.mozilla.org/en-US/docs/Web/API/Geolocation/clearWatch) method: ``` // Clear the watch and stop receiving updatesnavigator.geolocation.clearWatch(watchId); ``` ## Account Token PebbleKit JS provides a unique account token that is associated with the Pebble account of the current user, accessible using `Pebble.getAccountToken()`: ``` // Get the account tokenconsole.log('Pebble Account Token: ' + Pebble.getAccountToken()); ``` The token is a string with the following properties: - From the developer's perspective, the account token of a user is identical across platforms and across all the developer's watchapps. - If the user is not logged in, the token will be an empty string (''). ## Watch Token PebbleKit JS also provides a unique token that can be used to identify a Pebble device. It works in a similar way to `Pebble.getAccountToken()`: ``` // Get the watch tokenconsole.log('Pebble Watch Token: ' + Pebble.getWatchToken()); ``` The token is a string derived from the watch and the developer ID of an appstore app, or the UUID of a sideloaded app, so apps from the same developer see the same watch token and apps from different developers do not. **Important** The watch token is dependent on the watch's serial number, and therefore **should not** be used to store sensitive user information in case the watch changes ownership. If the app wishes to track a specific user _and_ watch, use a combination of the watch and account token. ## Showing a Notification A PebbleKit JS app can send a notification to the watch. This uses the standard system notification layout with customizable `title` and `body` fields: ``` var title = 'Update Available';var body = 'Version 1.5 of this app is now available from the appstore!';// Show the notificationPebble.showSimpleNotificationOnPebble(title, body); ``` > Note: PebbleKit Android/iOS applications cannot directly invoke a notification, and should instead leverage the respective platform notification APIs. These will be passed on to Pebble unless the user has turned them off in the mobile app. ## Getting Watch Information Use `Pebble.getActiveWatchInfo()` to return an object of data about the connected Pebble. This API is currently only available for SDK 3.0 and above. Do not assume that this function exists, so test that it is available before attempting to use it using the code shown below. ``` var watch = Pebble.getActiveWatchInfo ? Pebble.getActiveWatchInfo() : null;if(watch) {// Information is available!} else {// Not available, handle gracefully} ``` > Note: If there is no active watch available, `null` will be returned. The table below details the fields of the returned object and the information available. | Field | Type | Description | Values | | --- | --- | --- | --- | | `platform` | String | Hardware platform name. | `aplite`, `basalt`, `chalk`. | | `model` | String | Watch model name including color. | `pebble_black`, `pebble_grey`, `pebble_white`, `pebble_red`, `pebble_orange`, `pebble_blue`, `pebble_green`, `pebble_pink`, `pebble_steel_silver`, `pebble_steel_black`, `pebble_time_red`, `pebble_time_white`, `pebble_time_black`, `pebble_time_steel_black`, `pebble_time_steel_silver`, `pebble_time_steel_gold`, `pebble_time_round_silver_14mm`, `pebble_time_round_black_14mm`, `pebble_time_round_rose_gold_14mm`, `pebble_time_round_silver_20mm`, `pebble_time_round_black_20mm`, `qemu_platform_aplite`, `qemu_platform_basalt`, `qemu_platform_chalk`. | | `language` | String | Language currently selected on the watch. | E.g.: `en_GB`. See the [_Internationalization_](https://developer.repebble.com/guides/tools-and-resources/internationalization.md#locales-supported-by-pebble) for more information. | | `firmware` | Object | The firmware version running on the watch. | See below for sub-fields. | | `firmware.major` | Number | Major firmware version. | E.g.: `2` | | `firmware.minor` | Number | Minor firmware version. | E.g.: `8` | | `firmware.patch` | Number | Patch firmware version. | E.g.: `1` | | `firmware.suffix` | String | Any additional firmware versioning. | E.g.: `beta3` | ## Differences in the Pebble Mobile App The Pebble mobile app runs PebbleKit JS differently on each phone platform. On Android the JavaScript runs in a hidden WebView, so the full set of browser APIs is available. On iOS it runs in JavaScriptCore with a small set of APIs added by the app: `XMLHttpRequest`, `WebSocket`, `setTimeout` and `setInterval`, `localStorage`, `atob` and `btoa`, `navigator.geolocation`, and `console`. Anything else that a browser provides, such as `fetch`, `Blob` or the DOM, does not exist on iOS. Code that follows the sections above works on both. The sections below list the behaviour that differs from the original Pebble apps, or between Android and iOS, so that existing apps can be checked against it. The source is at [github.com/coredevices/mobileapp](https://github.com/coredevices/mobileapp). ### Lifecycle The JavaScript for an app starts when the app starts running on the watch and stops when the app exits. Launching the app again, or reconnecting the watch, starts a fresh JavaScript context, so state that must survive belongs in `localStorage`. Only one app's JavaScript runs at a time. On iOS the `ready` event fires as soon as the app's script has been evaluated. On Android it fires once the script has loaded in the WebView. Messages from the watch that arrive before `ready` are held for up to six seconds. On iOS the phone suspends the Pebble mobile app when it is in the background and no Bluetooth activity is pending. Timers do not fire while the app is suspended and run late when it resumes, so do not rely on `setInterval` for work that must happen while the user is not looking at the phone. ### Network Requests Use `XMLHttpRequest`. It is the WebView's own implementation on Android and the app's implementation on iOS, where these limits apply: - Event handlers receive an event with only a `type` property. Read the result from the request object, not from `event.target`. - `responseType` can be `""`, `"text"`, `"json"` or `"arraybuffer"`. `"blob"` and `"document"` throw. An `arraybuffer` response is delivered as a `Uint8Array`. - There is no `timeout` property and there are no progress events. - Synchronous requests (`async` set to `false` in `open()`) still deliver their response asynchronously, so the response is not available when `send()` returns. Always use asynchronous requests. `fetch()` is only available on Android. Apps that use it fail on iOS with `fetch is not defined`. `WebSocket` is available on both platforms. On iOS `binaryType` is always `"arraybuffer"`. ### Storage `localStorage` is separate for each app, keyed by the app's UUID, and keeps its contents across app launches and phone app restarts. Values are stored as strings, as in a browser. On Android, values written through the `localStorage` object are saved when the app's JavaScript stops. Use `localStorage.setItem()` rather than assigning properties so that writes are not lost if the phone app is killed. ### Geolocation `navigator.geolocation.getCurrentPosition()` and `watchPosition()` work on both platforms with these differences from a browser: - `maximumAge` (default 0) and `timeout` (default 15 seconds) are honoured. When the timeout passes and a previous position is known, that position is returned instead of an error. - `enableHighAccuracy` only affects `watchPosition()`. - The position object has `coords` with `latitude`, `longitude`, `accuracy`, `altitude`, `heading` and `speed`, and no `timestamp`. - Errors always have `code` set to `1` and a `message` string. ### Tokens `Pebble.getAccountToken()` and `Pebble.getWatchToken()` are derived from the signed-in Pebble account (or, for the watch token, the watch serial) together with the developer ID of an appstore app or the UUID of a sideloaded app. Both tokens therefore change when an app that was tested by sideloading is installed from the appstore. Treat data stored against a token from a sideloaded build as test data. `Pebble.getTimelineToken()` returns the token the appstore issued for the app. A sideloaded app gets a placeholder token while the _Emulate Timeline Webservice_ setting is on, which it is by default, and the failure callback when it is off. Neither token can be used with a server: the Pebble mobile app does not sync pins, so use [_local pins_](https://developer.repebble.com/guides/pebble-timeline/timeline-local-pins.md) instead. ### Watch Information `Pebble.getActiveWatchInfo()` returns `platform`, `model`, `language` and `firmware` (`major`, `minor`, `patch`, `suffix`). The `platform` value is the connected watch's platform only when that platform is in the app's `targetPlatforms`. When the watch is running a build for an older platform, `platform` reports the platform of the build that is installed: | Connected watch | Reported when not a target | | --- | --- | | flint (Pebble 2 Duo) | diorite if targeted, otherwise aplite | | emery (Pebble Time 2) | basalt | | gabbro (Pebble Round 2) | chalk | Add the new platforms to `targetPlatforms` and rebuild to receive their real names. ### AppMessage `Pebble.sendAppMessage()` follows these rules: - Keys must be names declared in `messageKeys` in `package.json`, or numeric strings. Keys that are not declared are dropped without an error. - Values can be strings, integers, booleans (sent as 0 or 1) and arrays of numbers (sent as a byte array). Fractional numbers are truncated to integers. Nested objects are not supported and stop the message from being sent. - The call fails with `"Failed to connect to Pebble"` if the watch does not accept the message within five seconds. If the watch does not acknowledge the message within ten seconds, the failure callback receives an error of `"nack"`. ### Configuration Pages `Pebble.openURL()` is only honoured in response to the `showConfiguration` event, and must be called within ten seconds of it. The page opens in a WebView inside the Pebble mobile app, not in the system browser. `data:` URLs are accepted, so a configuration page can be embedded in the app's JavaScript. The page returns its result by navigating to `pebblejs://close#`. `pebblejs://close/?` and `pebblejs://close/` are also accepted; `pebblejs://close?data=...` is not. The data is percent-decoded once and passed to the `webviewclosed` handler as `e.response`. It is not parsed, so call `JSON.parse(e.response)` as before. If the page is closed without data, or the user leaves it with the back button, `webviewclosed` does not fire. Read [_App Configuration_](https://developer.repebble.com/guides/user-interfaces/app-configuration.md) for the full flow. ### Plugins (Preview) The `Pebble` object has four functions for the plugins preview. They do nothing useful until the preview is turned on in the Pebble mobile app, which is off by default, and the API will change; read [_Plugins (Preview)_](https://developer.repebble.com/guides/plugins.md) before using them. - `Pebble.enumeratePlugins()` returns an array of the installed plugins with their sources and actions. - `Pebble.subscribeToSource(config)` subscribes to a category and item and delivers updates to `config.onData`. - `Pebble.invokeAction(config)` runs one action on one plugin and returns a Promise that resolves with the result. - `Pebble.sendConfigMessage(message)` pushes a message to the app's own settings page while it is open. ### Other Differences - `Pebble.showToast()` shows a toast on Android and does nothing on iOS. - `Pebble.getVersionCode()` is not available. Calling it throws. - `Pebble.timelineSubscribe()`, `timelineUnsubscribe()`, `timelineSubscriptions()` and `appGlanceReload()` do nothing and never call back. Server-pushed pins and glances are not supported. - `console.log()` output appears in `pebble logs` prefixed with the app's name and the line number. The Pebble mobile app's own log, the one included in bug reports, replaces lines containing words such as "token" or "location" with `` when the setting that obfuscates log content is on; the Developer Connection log is not redacted. --- # PebbleKit iOS Source: https://developer.repebble.com/guides/communication/using-pebblekit-ios/ > **Important: PebbleKit iOS has been discontinued.** Apple has made it increasingly difficult for third-party smartwatches to provide a competitive experience on iOS. Over the years, Apple has systematically restricted capabilities that were essential to Pebble's functionality, including the ability to reply to notifications, send messages, enable inter-app communication, and properly function when the companion app is in the background. These restrictions have made it nearly impossible to build a smartwatch experience on iOS that matches the capabilities available on Android. We encourage developers to focus on Android development where Pebble can deliver its full potential without artificial platform limitations. Learn more about Apple's restrictions and their impact on third-party wearables in [this blog post](https://ericmigi.com/blog/apple-restricts-pebble-from-being-awesome-with-iphones). This section assumes that the reader has a basic knowledge of Objective-C, Xcode as an IDE, and the delegate and block patterns. > PebbleKit iOS should be compatible if your app uses Swift. The framework itself is written in Objective-C to avoid the requirement of the Swift runtime in pure Objective-C apps, and to improve the backwards and forwards compatibility. ### Setting Up PebbleKit iOS If the project is using [CocoaPods](http://cocoapods.org/) (which is the recommended approach), just add `pod 'PebbleKit'` to the `Podfile` and execute `pod install`. After installing PebbleKit iOS in the project, perform these final steps: - If the iOS app needs to run in the background, you should update your target’s “Capabilities” in Xcode. Enable “Background Modes” and select both “Uses Bluetooth LE accessories” and “Acts as a Bluetooth LE accessory”. This should add the keys `bluetooth-peripheral` (“App shares data using CoreBluetooth”) and `bluetooth-central` (“App communicates using CoreBluetooth”) to your target’s `Info.plist` file. - If you are using Xcode 8 or greater (and recommended for previous versions), you must also add the key `NSBluetoothPeripheralUsageDescription` (“Privacy - Bluetooth Peripheral Usage Description”) to your `Info.plist`. > To add PebbleKit iOS manually, or some other alternatives follow the steps in the [repository](https://github.com/pebble/pebble-ios-sdk/). The documentation might also include more information that might be useful. Read it carefully. ### Targeting a Companion App Before an iOS companion app can start communicating or exchange messages with a watchapp on Pebble, it needs to give PebbleKit a way to identify the watchapp. The UUID of your watchapp is used for this purpose. Set the app UUID associated with the PBPebbleCentral instance. A simple way to create a UUID in standard representation to `NSUUID` is shown here: ``` // Set UUID of watchappNSUUID \*myAppUUID = [[NSUUID alloc] initWithUUIDString:@"226834ae-786e-4302-a52f-6e7efc9f990b"];[PBPebbleCentral defaultCentral].appUUID = myAppUUID; ``` If you are trying to communicate with the built-in Sports or Golf apps, their UUID are available as part of PebbleKit with `PBSportsUUID` and `PBGolfUUID`. You must register those UUID if you intend to communicate with those apps. ### Becoming a Delegate To communicate with a Pebble watch, the class must implement `PBPebbleCentralDelegate`: ``` @interfaceViewController () \ ``` The `PBPebbleCentral` class should not be instantiated directly. Instead, always use the singleton provided by `[PBPebbleCentral defaultCentral]`. An example is shown below, with the Golf app UUID: ``` central = [PBPebbleCentral defaultCentral];central.appUUID = myAppUUID;[central run]; ``` Once this is done, set the class to be the delegate: ``` [PBPebbleCentral defaultCentral].delegate = self; ``` This delegate will get two callbacks: `pebbleCentral:watchDidConnect:isNew:` and `pebbleCentral:watchDidDisconnect:` every time a Pebble connects or disconnects. The app won't get connection callbacks if the Pebble is already connected when the delegate is set. Implement these to receive the associated connection/disconnection events: ``` -(void)pebbleCentral:(PBPebbleCentral \*)central watchDidConnect:(PBWatch \*)watch isNew:(BOOL)isNew {NSLog(@"Pebble connected: %@", watch.name);// Keep a reference to this watchself.connectedWatch = watch;}-(void)pebbleCentral:(PBPebbleCentral \*)central watchDidDisconnect:(PBWatch \*)watch {NSLog(@"Pebble disconnected: %@", watch.name);// If this was the recently connected watch, forget itif ([watch isEqual:self.connectedWatch]) {self.connectedWatch = nil;}} ``` ### Initiating Bluetooth Communication Once the iOS app is correctly set up to communicate with Pebble, the final step is to actually begin communication. No communication can take place until the following is called: ``` [[PBPebbleCentral defaultCentral] run]; ``` > Once this occurs, the user _may_ be shown a dialog asking for confirmation that they want the app to communicate. This means the app should not call `run:` until the appropriate moment in the UI. ### Sending Messages from iOS Since iOS apps are built separately from their companion Pebble apps, there is no way for the build system to automatically create matching app message keys. You must therefore manually specify them in `package.json`, like so: ``` {"Temperature": 0,"WindSpeed": 1,"WindDirection": 2,"RequestData": 3,"LocationName": 4} ``` These numeric values can then be used as app message keys in your iOS app. Messages are constructed with the `NSDictionary` class and sent to the C watchapp or watchface by the `PBPebbleCentralDelegate` when the `appMessagesPushUpdate:` function is invoked. To send a message, prepare an `NSDictionary` object with the data to be sent to the C watchapp. Data items are added to the [`NSDictionary`](https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSDictionary_Class/) using key-value pairs of standard data types. An example containing a string and an integer is shown below: ``` NSDictionary \*update = @{ @(0):[NSNumber pb\_numberWithUint8:42],@(1):@"a string" }; ``` Send this dictionary to the watchapp using `appMessagesPushUpdate:`. The first argument is the update dictionary to send and the second argument is a callback block that will be invoked when the data has been acknowledged by the watch (or if an error occurs). ``` [self.connectedWatch appMessagesPushUpdate:update onSent:^(PBWatch \*watch, NSDictionary \*update, NSError \*error) {if (!error) {NSLog(@"Successfully sent message.");} else {NSLog(@"Error sending message: %@", error);}}]; ``` Once delivered, this dictionary will be available in the C app via the [`AppMessageInboxReceived`](https://developer.repebble.com/docs/c/Foundation/AppMessage.md#AppMessageInboxReceived "AppMessageInboxReceived") callback, as detailed in [_Sending and Receiving Data_](https://developer.repebble.com/guides/communication/sending-and-receiving-data.md#inbox-received). ### Receiving Messages on iOS To receive messages from a watchapp, register a receive handler (a block) with `appMessagesAddReceiveUpdateHandler:`. This block will be invoked with two parameters - a pointer to a `PBWatch` object describing the Pebble that sent the message and an `NSDictionary` with the message received. ``` [self.connectedWatch appMessagesAddReceiveUpdateHandler:^BOOL(PBWatch \*watch, NSDictionary \*update) {NSLog(@"Received message: %@", update);// Send Ack to Pebblereturn YES;}]; ``` > Always return `YES` in the handler. This instructs PebbleKit to automatically send an ACK to Pebble, to avoid the message timing out. Data can be read from the `NSDictionary` by first testing for each key's presence using a `!= nil` check, and reading the value if it is present: ``` NSNumber \*key = @1;// If the key is present in the received dictionaryif (update[key]) {// Read the integer valueint value = [update[key] intValue];} ``` ### Other Capabilities In addition to sending and receiving messages, PebbleKit iOS also allows more intricate interactions with Pebble. See the [PebbleKit iOS Documentation](https://developer.repebble.com/docs/pebblekit-ios.md) for more information. Some examples are shown below of what is possible: - Checking if the watch is connected using the `connected` property of a `PBWatch`. ``` BOOL isConnected = self.watch.connected; ``` - Receiving `watchDidConnect` and `watchDidDisconnect` events through being a `PBDataloggingServiceDelegate`. ### Limitations of PebbleKit on iOS The iOS platform imposes some restrictions on what apps can do with accessories. It also limits the capabilities of apps that are in the background. It is critical to understand these limitations when developing an app that relies on PebbleKit iOS. On iOS, all communication between a mobile app and Pebble is managed through a communication session. This communication session is a protocol specific to iOS, with notable limitations that the reader should know and understand when developing an iOS companion app for Pebble. #### Bluetooth Low Energy (BLE) Connections For Pebble apps that communicate with Pebble in BLE mode, a session can be created for each app that requires one. This removes the 'one session only' restriction, but only for these BLE apps. Currently, there are several BLE only devices, such as Pebble Time Round, and Pebble 2, but all the devices using a firmware 3.8 or greater can use BLE to communicate with PebbleKit. For BLE apps, the 'phone must launch' restriction is removed. The iOS companion app can be restarted by the watchapp if it stops working if user force-quits iOS app, or it crashes. Note that the app will not work after rebooting iOS device, which requires it be launched by the iPhone user once after boot. #### Communication with firmware older than 3.0 PebbleKit iOS 3.1.1 is the last PebbleKit that supports communication with firmwares older than 3.0. PebbleKit iOS 4.0.0 can only communicate with Pebble devices with firmware newer than 3.0. For newer devices like Pebble Time, Pebble Time Steel, Pebble Time Round, and Pebble 2 there should be no problem. For previous generation devices like Pebble and Pebble Steel it means that their users should upgrade their firmware to the latest firmware available for their devices using the new apps. This change allows better compatibility and new features to be developed by 3rd parties. --- # Sending and Receiving Data Source: https://developer.repebble.com/guides/communication/sending-and-receiving-data/ Before using [`AppMessage`](https://developer.repebble.com/docs/c/Foundation/AppMessage.md "AppMessage"), a Pebble C app must set up the buffers used for the inbox and outbox. These are used to store received messages that have not yet been processed, and sent messages that have not yet been transmitted. In addition, callbacks may be registered to allow an app to respond to any success or failure events that occur when dealing with messages. Doing all of this is discussed in this guide. ## Message Structure Every message sent or received using the [`AppMessage`](https://developer.repebble.com/docs/c/Foundation/AppMessage.md "AppMessage") API is stored in a [`DictionaryIterator`](https://developer.repebble.com/docs/c/Foundation/Dictionary.md#DictionaryIterator "DictionaryIterator") structure, which is essentially a list of [`Tuple`](https://developer.repebble.com/docs/c/Foundation/Dictionary.md#Tuple "Tuple") objects. Each [`Tuple`](https://developer.repebble.com/docs/c/Foundation/Dictionary.md#Tuple "Tuple") contains a key used to 'label' the value associated with that key. When a message is sent, a [`DictionaryIterator`](https://developer.repebble.com/docs/c/Foundation/Dictionary.md#DictionaryIterator "DictionaryIterator") is filled with a [`Tuple`](https://developer.repebble.com/docs/c/Foundation/Dictionary.md#Tuple "Tuple") for each item of outgoing data. Conversely, when a message is received the [`DictionaryIterator`](https://developer.repebble.com/docs/c/Foundation/Dictionary.md#DictionaryIterator "DictionaryIterator") provided by the callback is examined for the presence of each key. If a key is present, the value associated with it can be read. ## Data Types The [`Tuple.value`](https://developer.repebble.com/docs/c/Foundation/Dictionary.md#Tuple "Tuple") union allows multiple data types to be stored in and read from each received message. These are detailed below: | Name | Type | Size in Bytes | Signed? | | --- | --- | --- | --- | | uint8 | `uint8_t` | 1 | No | | uint16 | `uint16_t` | 2 | No | | uint32 | `uint32_t` | 4 | No | | int8 | `int8_t` | 1 | Yes | | int16 | `int16_t` | 2 | Yes | | int32 | `int32_t` | 4 | Yes | | cstring | `char[]` | Variable length array | N/A | | data | `uint8_t[]` | Variable length array | N/A | ## Buffer Sizes The size of each of the outbox and inbox buffers must be set chosen such that the largest message that the app will ever send or receive will fit. Incoming messages that exceed the size of the inbox buffer, and outgoing messages that exceed that size of the outbox buffer will be dropped. These sizes are specified when the [`AppMessage`](https://developer.repebble.com/docs/c/Foundation/AppMessage.md "AppMessage") system is 'opened', allowing communication to occur: ``` // Largest expected inbox and outbox message sizesconst uint32\_t inbox\_size = 64;const uint32\_t outbox\_size = 256;// Open AppMessageapp\_message\_open(inbox\_size, outbox\_size); ``` Each of these buffers is allocated at this moment, and comes out of the app's memory budget, so the sizes of the inbox and outbox should be conservative. Calculate the size of the buffer you require by summing the sizes of all the keys and values in the larges message the app will handle. For example, a message containing three integer keys and values will work with a 32 byte buffer size. ## Choosing Keys For each message sent and received, the contents are accessible using keys-value pairs in a [`Tuple`](https://developer.repebble.com/docs/c/Foundation/Dictionary.md#Tuple "Tuple"). This allows each piece of data in the message to be uniquely identifiable using its key, and also allows many different data types to be stored inside a single message. Each possible piece of data that may be transmitted should be assigned a unique key value, used to read the associated value when it is found in a received message. An example for a weather app is shown below:: - Temperature - WindSpeed - WindDirection - RequestData - LocationName These values will be made available in any file that includes `pebble.h` prefixed with `MESSAGE_KEY_`, such as `MESSAGE_KEY_Temperature` and `MESSAGE_KEY_WindSpeed`. Examples of how these key values would be used in the phone-side app are shown in [_PebbleKit JS_](https://developer.repebble.com/guides/communication/using-pebblekit-js.md), [_PebbleKit iOS_](https://developer.repebble.com/guides/communication/using-pebblekit-ios.md), and [_PebbleKit Android_](https://developer.repebble.com/guides/communication/using-pebblekit-android.md). ## Using Callbacks Like many other aspects of the Pebble C API, the [`AppMessage`](https://developer.repebble.com/docs/c/Foundation/AppMessage.md "AppMessage") system makes use of developer-defined callbacks to allow an app to gracefully handle all events that may occur, such as successfully sent or received messages as well as any errors that may occur. These callback types are discussed below. Each is used by first creating a function that matches the signature of the callback type, and then registering it with the [`AppMessage`](https://developer.repebble.com/docs/c/Foundation/AppMessage.md "AppMessage") system to be called when that event type occurs. Good use of callbacks to drive the app's UI will result in an improved user experience, especially when errors occur that the user can be guided in fixing. ### Inbox Received The [`AppMessageInboxReceived`](https://developer.repebble.com/docs/c/Foundation/AppMessage.md#AppMessageInboxReceived "AppMessageInboxReceived") callback is called when a new message has been received from the connected phone. This is the moment when the contents can be read and used to drive what the app does next, using the provided [`DictionaryIterator`](https://developer.repebble.com/docs/c/Foundation/Dictionary.md#DictionaryIterator "DictionaryIterator") to read the message. An example is shown below under [_Reading an Incoming Message_](#reading-an-incoming-message): ``` static void inbox\_received\_callback(DictionaryIterator \*iter, void \*context) {// A new message has been successfully received} ``` Register this callback so that it is called at the appropriate time: ``` // Register to be notified about inbox received eventsapp\_message\_register\_inbox\_received(inbox\_received\_callback); ``` ### Inbox Dropped The [`AppMessageInboxDropped`](https://developer.repebble.com/docs/c/Foundation/AppMessage.md#AppMessageInboxDropped "AppMessageInboxDropped") callback is called when a message was received, but it was dropped. A common cause of this is that the message was too big for the inbox. The reason for failure can be determined using the [`AppMessageResult`](https://developer.repebble.com/docs/c/Foundation/AppMessage.md#AppMessageResult "AppMessageResult") provided by the callback: ``` static void inbox\_dropped\_callback(AppMessageResult reason, void \*context) {// A message was received, but had to be droppedAPP\_LOG(APP\_LOG\_LEVEL\_ERROR, "Message dropped. Reason: %d", (int)reason);} ``` Register this callback so that it is called at the appropriate time: ``` // Register to be notified about inbox dropped eventsapp\_message\_register\_inbox\_dropped(inbox\_dropped\_callback); ``` ### Outbox Sent The [`AppMessageOutboxSent`](https://developer.repebble.com/docs/c/Foundation/AppMessage.md#AppMessageOutboxSent "AppMessageOutboxSent") callback is called when a message sent from Pebble has been successfully delivered to the connected phone. The provided [`DictionaryIterator`](https://developer.repebble.com/docs/c/Foundation/Dictionary.md#DictionaryIterator "DictionaryIterator") can be optionally used to inspect the contents of the message just sent. > When sending multiple messages in a short space of time, it is **strongly** recommended to make use of this callback to wait until the previous message has been sent before sending the next. ``` static void outbox\_sent\_callback(DictionaryIterator \*iter, void \*context) {// The message just sent has been successfully delivered} ``` Register this callback so that it is called at the appropriate time: ``` // Register to be notified about outbox sent eventsapp\_message\_register\_outbox\_sent(outbox\_sent\_callback); ``` ### Outbox Failed The [`AppMessageOutboxFailed`](https://developer.repebble.com/docs/c/Foundation/AppMessage.md#AppMessageOutboxFailed "AppMessageOutboxFailed") callback is called when a message just sent failed to be successfully delivered to the connected phone. The reason can be determined by reading the value of the provided [`AppMessageResult`](https://developer.repebble.com/docs/c/Foundation/AppMessage.md#AppMessageResult "AppMessageResult"), and the contents of the failed message inspected with the provided [`DictionaryIterator`](https://developer.repebble.com/docs/c/Foundation/Dictionary.md#DictionaryIterator "DictionaryIterator"). Use of this callback is strongly encouraged, since it allows an app to detect a failed message and either retry its transmission, or inform the user of the failure so that they can attempt their action again. ``` static void outbox\_failed\_callback(DictionaryIterator \*iter,AppMessageResult reason, void \*context) {// The message just sent failed to be deliveredAPP\_LOG(APP\_LOG\_LEVEL\_ERROR, "Message send failed. Reason: %d", (int)reason);} ``` Register this callback so that it is called at the appropriate time: ``` // Register to be notified about outbox failed eventsapp\_message\_register\_outbox\_failed(outbox\_failed\_callback); ``` ## Constructing an Outgoing Message A message is constructed and sent from the C app via [`AppMessage`](https://developer.repebble.com/docs/c/Foundation/AppMessage.md "AppMessage") using a [`DictionaryIterator`](https://developer.repebble.com/docs/c/Foundation/Dictionary.md#DictionaryIterator "DictionaryIterator") object and the [`Dictionary`](https://developer.repebble.com/docs/c/Foundation/Dictionary.md "Dictionary") APIs. Ensure that [`app_message_open()`](https://developer.repebble.com/docs/c/Foundation/AppMessage.md#app_message_open "app\_message\_open") has been called before sending or receiving any messages. The first step is to begin an outgoing message by preparing a [`DictionaryIterator`](https://developer.repebble.com/docs/c/Foundation/Dictionary.md#DictionaryIterator "DictionaryIterator") pointer, used to keep track of the state of the dictionary being constructed: ``` // Declare the dictionary's iteratorDictionaryIterator \*out\_iter;// Prepare the outbox buffer for this messageAppMessageResult result = app\_message\_outbox\_begin(&out\_iter); ``` The [`AppMessageResult`](https://developer.repebble.com/docs/c/Foundation/AppMessage.md#AppMessageResult "AppMessageResult") should be checked to make sure the outbox was successfully prepared: ``` if(result == APP\_MSG\_OK) {// Construct the message} else {// The outbox cannot be used right nowAPP\_LOG(APP\_LOG\_LEVEL\_ERROR, "Error preparing the outbox: %d", (int)result);} ``` If the result is [`APP_MSG_OK`](https://developer.repebble.com/docs/c/Foundation/AppMessage.md#APP_MSG_OK "APP\_MSG\_OK"), the message construction can continue. Data is now written to the dictionary according to data type using the [`Dictionary`](https://developer.repebble.com/docs/c/Foundation/Dictionary.md "Dictionary") APIs. An example from the hypothetical weather app is shown below: ``` if(result == APP\_MSG\_OK) {// A dummy valueint value = 0;// Add an item to ask for weather datadict\_write\_int(out\_iter, MESSAGE\_KEY\_RequestData, &value, sizeof(int), true);} ``` After all desired data has been written to the dictionary, the message may be sent: ``` // Send this messageresult = app\_message\_outbox\_send();// Check the resultif(result != APP\_MSG\_OK) {APP\_LOG(APP\_LOG\_LEVEL\_ERROR, "Error sending the outbox: %d", (int)result);} ``` **Important** Any app that wishes to send data from the watch to the phone via PebbleKit JS must wait until the `ready` event has occured, indicating that the phone has loaded the JavaScript for the app and it is ready to receive data. See [_Advanced Communication_](https://developer.repebble.com/guides/communication/advanced-communication.md#waiting-for-pebblekit-js) for more information. Once the message send operation has been completed, either the [`AppMessageOutboxSent`](https://developer.repebble.com/docs/c/Foundation/AppMessage.md#AppMessageOutboxSent "AppMessageOutboxSent") or [`AppMessageOutboxFailed`](https://developer.repebble.com/docs/c/Foundation/AppMessage.md#AppMessageOutboxFailed "AppMessageOutboxFailed") callbacks will be called (if they have been registered), depending on either a success or failure outcome. ### Example Outgoing Message Construction A complete example of assembling an outgoing message is shown below: ``` // Declare the dictionary's iteratorDictionaryIterator \*out\_iter;// Prepare the outbox buffer for this messageAppMessageResult result = app\_message\_outbox\_begin(&out\_iter);if(result == APP\_MSG\_OK) {// Add an item to ask for weather dataint value = 0;dict\_write\_int(out\_iter, MESSAGE\_KEY\_RequestData, &value, sizeof(int), true);// Send this messageresult = app\_message\_outbox\_send();if(result != APP\_MSG\_OK) {APP\_LOG(APP\_LOG\_LEVEL\_ERROR, "Error sending the outbox: %d", (int)result);}} else {// The outbox cannot be used right nowAPP\_LOG(APP\_LOG\_LEVEL\_ERROR, "Error preparing the outbox: %d", (int)result);} ``` ## Reading an Incoming Message When a message is received from the connected phone the [`AppMessageInboxReceived`](https://developer.repebble.com/docs/c/Foundation/AppMessage.md#AppMessageInboxReceived "AppMessageInboxReceived") callback is called, and the message's contents can be read using the provided [`DictionaryIterator`](https://developer.repebble.com/docs/c/Foundation/Dictionary.md#DictionaryIterator "DictionaryIterator"). This should be done by looking for the presence of each expectd `Tuple` key value, and using the associated value as required. Most apps will deal with integer values or strings to pass signals or some human-readable information respectively. These common use cases are discussed below. ### Reading an Integer **From JS** ``` var dict= {'Temperature': 29}; ``` **In C** ``` static void inbox\_received\_callback(DictionaryIterator \*iter, void \*context) {// A new message has been successfully received// Does this message contain a temperature value?Tuple \*temperature\_tuple = dict\_find(iter, MESSAGE\_KEY\_Temperature);if(temperature\_tuple) {// This value was stored as JS Number, which is stored here as int32\_tint32\_t temperature = temperature\_tuple-\>value-\>int32;}} ``` ### Reading a String A common use of transmitted strings is to display them in a [`TextLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/TextLayer.md "TextLayer"). Since the displayed text is required to be long-lived, a `static` `char` buffer can be used when the data is received: **From JS** ``` var dict = {'LocationName': 'London, UK'}; ``` **In C** ``` static void inbox\_received\_callback(DictionaryIterator \*iter, void \*context) {// Is the location name inside this message?Tuple \*location\_tuple = dict\_find(iter, MESSAGE\_KEY\_LocationName);if(location\_tuple) {// This value was stored as JS String, which is stored here as a char stringchar \*location\_name = location\_tuple-\>value-\>cstring;// Use a static buffer to store the string for displaystatic char s\_buffer[MAX\_LENGTH];snprintf(s\_buffer, sizeof(s\_buffer), "Location: %s", location\_name);// Display in the TextLayertext\_layer\_set\_text(s\_text\_layer, s\_buffer);}} ``` ### Reading Binary Data Apps that deal in packed binary data can send this data and pack/unpack as required on either side: **From JS** ``` var dict = {'Data': [1, 2, 4, 8, 16, 32, 64]}; ``` **In C** ``` static void inbox\_received\_callback(DictionaryIterator \*iter, void \*context) {// Expected length of the binary dataconst int length = 32;// Does this message contain the data tuple?Tuple \*data\_tuple = dict\_find(iter, MESSAGE\_KEY\_Data);if(data\_tuple) {// Read the binary data valueuint8\_t \*data = data\_tuple-\>value-\>data;// Inspect the first byte, for exampleuint8\_t byte\_zero = data[0];// Store into an app-defined buffermemcpy(s\_buffer, data, length);} ``` --- # Sports API Source: https://developer.repebble.com/guides/communication/using-the-sports-api/ Every Pebble watch has two built-in system watchapps called the Sports app, and the Golf app. These apps are hidden from the launcher until launched via PebbleKit Android or PebbleKit iOS. Both are designed to be generic apps that display sports-related data in common formats. The goal is to allow fitness and golf mobile apps to integrate with Pebble to show the wearer data about their activity without needing to create and maintain an additional app for Pebble. An example of a popular app that uses this approach is the [Runkeeper](http://apps.repebble.com/application/52e05bd5d8561de307000039) app. The Sports and Golf apps are launched, closed, and controlled by PebbleKit in an Android or iOS app, shown by example in each section below. In both cases, the data fields that are available to be populated are different, but data is pushed in the same way. ## Available Data Fields ### Sports #### aplite #### basalt #### chalk ![](https://developer.repebble.com/assets/images/guides/design-and-interaction/sports~aplite.png) ![](https://developer.repebble.com/assets/images/guides/design-and-interaction/sports~basalt.png) ![](https://developer.repebble.com/assets/images/guides/design-and-interaction/sports~chalk.png) The sports app displays activity duration and distance which can apply to a wide range of sports, such as cycling or running. A configurable third field is also available that displays pace or speed, depending on the app's preference. The Sports API also allows the app to be configured to display the labels of each field in metric (the default) or imperial units. The action bar is used to prompt the user to use the Select button to pause and resume their activity session. The companion app is responsible for listening for these events and implementing the pause/resume operation as appropriate. ### Golf #### aplite #### basalt #### chalk ![](https://developer.repebble.com/assets/images/guides/design-and-interaction/golf~aplite.png) ![](https://developer.repebble.com/assets/images/guides/design-and-interaction/golf~basalt.png) ![](https://developer.repebble.com/assets/images/guides/design-and-interaction/golf~chalk.png) The Golf app is specialized to displaying data relevant to golf games, including par and hole numbers, as well as front, mid, and rear yardage. Similar to the Sports app, the action bar is used to allow appropriate feedback to the companion app. In this case the actions are an 'up', 'ball' and 'down' events which the companion should handle as appropriate. ## With PebbleKit Android Once an Android app has set up [_PebbleKit Android_](https://developer.repebble.com/guides/communication/using-pebblekit-android.md), the Sports and Golf apps can be launched and customized as appropriate. ### Launching Sports and Golf To launch one of the Sports API apps, simply call `startAppOnPebble()` and supply the UUID from the `Constants` class: ``` // Launch the Sports appPebbleKit.startAppOnPebble(getApplicationContext(), Constants.SPORTS\_UUID); ``` ### Customizing Sports To choose which unit type is used, construct and send a `PebbleDictionary` containing the desired value from the `Constants` class. Either `SPORTS_UNITS_IMPERIAL` or `SPORTS_UNITS_METRIC` can be used: ``` PebbleDictionary dict = new PebbleDictionary();// Display imperial unitsdict.addUint8(Constants.SPORTS\_UNITS\_KEY, Constants.SPORTS\_UNITS\_IMPERIAL);PebbleKit.sendDataToPebble(getApplicationContext(), Constants.SPORTS\_UUID, dict); ``` To select between 'pace' or 'speed' as the label for the third field, construct and send a `PebbleDictionary` similar to the example above. This can be done in the same message as unit selection: ``` PebbleDictionary dict = new PebbleDictionary();// Display speed instead of pacedict.addUint8(Constants.SPORTS\_LABEL\_KEY, Constants.SPORTS\_DATA\_SPEED);PebbleKit.sendDataToPebble(getApplicationContext(), Constants.SPORTS\_UUID, dict); ``` > Note: The Golf app does not feature any customizable fields. ### Displaying Data Data about the current activity can be sent to either of the Sports API apps using a `PebbleDictionary`. For example, to show a value for duration and distance in the Sports app: ``` PebbleDictionary dict = new PebbleDictionary();// Show a value for duration and distancedict.addString(Constants.SPORTS\_TIME\_KEY, "12:52");dict.addString(Constants.SPORTS\_DISTANCE\_KEY, "23.8");PebbleKit.sendDataToPebble(getApplicationContext(), Constants.SPORTS\_UUID, dict); ``` Read the [`Constants`](https://developer.repebble.com/docs/pebblekit-android/com/getpebble/android/kit/Constants.md) documentation to learn about all the available parameters that can be used for customization. ### Handling Button Events When a button event is generated from one of the Sports API apps, a message is sent to the Android companion app, which can be processed using a `PebbleDataReceiver`. For example, to listen for a change in the state of the Sports app, search for `Constants.SPORTS_STATE_KEY` in the received `PebbleDictionary`. The user is notified in the example below through the use of an Android [`Toast`](http://developer.android.com/guide/topics/ui/notifiers/toasts.html): ``` // Create a receiver for when the Sports app state changesPebbleDataReceiver reciever = new PebbleKit.PebbleDataReceiver(Constants.SPORTS\_UUID) {@Overridepublic void receiveData(Context context, int id, PebbleDictionary data) {// Always ACKnowledge the last message to prevent timeoutsPebbleKit.sendAckToPebble(getApplicationContext(), id);// Get action and display as ToastLong value = data.getUnsignedIntegerAsLong(Constants.SPORTS\_STATE\_KEY);if(value != null) {int state = value.intValue();String text = (state == Constants.SPORTS\_STATE\_PAUSED)? "Resumed!" : "Paused!";Toast.makeText(getApplicationContext(), text, Toast.LENGTH\_SHORT).show();}}};// Register the receiverPebbleKit.registerReceivedDataHandler(getApplicationContext(), receiver); ``` ## With PebbleKit iOS Once an iOS app has set up [_PebbleKit iOS_](https://developer.repebble.com/guides/communication/using-pebblekit-ios.md), the Sports and Golf apps can be launched and customized as appropriate. The companion app should set itself as a delegate of `PBPebbleCentralDelegate`, and assign a `PBWatch` property once `watchDidConnect:` has fired. This `PBWatch` object will then be used to manipulate the Sports API apps. Read _Becoming a Delegate_ in the [_PebbleKit iOS_](https://developer.repebble.com/guides/communication/using-pebblekit-ios.md) guide to see how this is done. ### Launching Sports and Golf To launch one of the Sports API apps, simply call `sportsAppLaunch:` or `golfAppLaunch:` as appropriate: ``` [self.watch sportsAppLaunch:^(PBWatch \* \_Nonnull watch,NSError \* \_Nullable error) {NSLog(@"Sports app was launched");}]; ``` ### Customizing Sports To choose which unit type is used, call `sportsAppSetMetric:` with the desired `isMetric` `BOOL`: ``` BOOL isMetric = YES;[self.watch sportsAppSetMetric:isMetric onSent:^(PBWatch \* \_Nonnull watch,NSError \* \_Nonnull error) {if (!error) {NSLog(@"Successfully sent message.");} else {NSLog(@"Error sending message: %@", error);}}]; ``` To select between 'pace' or 'speed' as the label for the third field, call `sportsAppSetLabel:` with the desired `isPace` `BOOL`: ``` BOOL isPace = YES;[self.watch sportsAppSetLabel:isPace onSent:^(PBWatch \* \_Nonnull watch,NSError \* \_Nullable error) {if (!error) {NSLog(@"Successfully sent message.");} else {NSLog(@"Error sending message: %@", error);}}]; ``` > Note: The Golf app does not feature any customizable fields. ### Displaying Data Data about the current activity can be sent to either the Sports or Golf app using `sportsAppUpdate:` or `golfAppUpdate:`. For example, to show a value for duration and distance in the Sports app: ``` // Construct a dictionary of dataNSDictionary \*update = @{ PBSportsTimeKey: @"12:34",PBSportsDistanceKey: @"6.23" };// Send the data to the Sports app[self.watch sportsAppUpdate:update onSent:^(PBWatch \* \_Nonnull watch,NSError \* \_Nullable error) {if (!error) {NSLog(@"Successfully sent message.");} else {NSLog(@"Error sending message: %@", error);}}]; ``` Read the [`PBWatch`](https://developer.repebble.com/docs/pebblekit-ios/Classes/PBWatch.md) documentation to learn about all the available methods and values for customization. ### Handling Button Events When a button event is generated from one of the Sports API apps, a message is sent to the Android companion app, which can be processed using `sportsAppAddReceiveUpdateHandler` and supplying a block to be run when a message is received. For example, to listen for change in state of the Sports app, check the value of the provided `SportsAppActivityState`: ``` // Register to get state updates from the Sports app[self.watch sportsAppAddReceiveUpdateHandler:^BOOL(PBWatch \*watch,SportsAppActivityState state) {// Display the new state of the watchappswitch (state) {case SportsAppActivityStateRunning:NSLog(@"Watchapp now running.");break;case SportsAppActivityStatePaused:NSLog(@"Watchapp now paused.");break;default: break;}// Finallyreturn YES;}]; ``` --- # Accelerometer Source: https://developer.repebble.com/guides/events-and-services/accelerometer/ The acceleromter sensor is included in every Pebble watch, and allows collection of acceleration and orientation data in watchapps and watchfaces. Data is available in two ways, each suitable to different types of watchapp: - Taps events - Fires an event whenever a significant tap or shake of the watch occurs. Useful to 'shake to view' features. - Data batches - Allows collection of data in batches at specific intervals. Useful for general accelerometer data colleciton. As a significant source of regular callbacks, the accelerometer should be used as sparingly as possible to allow the watch to sleep and conserve power. For example, receiving data in batches once per second is more power efficient than receiving a single sample 25 times per second. ## About the Pebble Accelerometer The Pebble accelerometer is oriented according to the diagram below, showing the direction of each of the x, y, and z axes. ![accel-axes](https://developer.repebble.com/assets/images/guides/pebble-apps/sensors/accel.png) In the API, each axis value contained in an [`AccelData`](https://developer.repebble.com/docs/c/Foundation/Event_Service/AccelerometerService.md#AccelData "AccelData") sample is measured in milli-Gs. The accelerometer is calibrated to measure a maximum acceleration of ±4G. Therefore, the range of possible values for each axis is -4000 to +4000. The [`AccelData`](https://developer.repebble.com/docs/c/Foundation/Event_Service/AccelerometerService.md#AccelData "AccelData") sample object also contains a `did_vibrate` field, set to `true` if the vibration motor was active during the sample collection. This could possibly contaminate those samples due to onboard vibration, so they should be discarded. Lastly, the `timestamp` field allows tracking of obtained accelerometer data over time. ## Using Taps Adding a subscription to tap events allows a developer to react to any time the watch is tapped or experiences a shake along one of three axes. Tap events are received by registering an [`AccelTapHandler`](https://developer.repebble.com/docs/c/Foundation/Event_Service/AccelerometerService.md#AccelTapHandler "AccelTapHandler") function, such as the one below: ``` static void accel\_tap\_handler(AccelAxisType axis, int32\_t direction) {// A tap event occured} ``` The `axis` parameter describes which axis the tap was detected along. The `direction` parameter is set to `1` for the positive direction, and `-1` for the negative direction. A subscription can be added or removed at any time. While subscribed, `accel_tap_handler` will be called whenever a tap event is fired by the accelerometer. Adding a subscription is simple: ``` // Subscribe to tap eventsaccel\_tap\_service\_subscribe(accel\_tap\_handler); ``` ``` // Unsubscribe from tap eventsaccel\_tap\_service\_unsubscribe(); ``` ## Using Data Batches Accelerometer data can be received in batches at a chosen sampling rate by subscribing to the Accelerometer Data Service at any time: ``` uint32\_t num\_samples = 3;// Number of samples per batch/callback// Subscribe to batched data eventsaccel\_data\_service\_subscribe(num\_samples, accel\_data\_handler); ``` The [`AccelDataHandler`](https://developer.repebble.com/docs/c/Foundation/Event_Service/AccelerometerService.md#AccelDataHandler "AccelDataHandler") function (called `accel_data_handler` in the example above) is called when a new batch of data is ready for consumption by the watchapp. The rate at which these occur is dictated by two things: - The sampling rate - The number of samples the accelerometer device measures per second. One value chosen from the [`AccelSamplingRate`](https://developer.repebble.com/docs/c/Foundation/Event_Service/AccelerometerService.md#AccelSamplingRate "AccelSamplingRate") `enum`. - The number of samples per batch. Some simple math will determine how often the callback will occur. For example, at the [`ACCEL_SAMPLING_50HZ`](https://developer.repebble.com/docs/c/Foundation/Event_Service/AccelerometerService.md#ACCEL_SAMPLING_50HZ "ACCEL\_SAMPLING\_50HZ") sampling rate, and specifying 10 samples per batch will result in five calls per second. When an event occurs, the acceleromater data can be read from the [`AccelData`](https://developer.repebble.com/docs/c/Foundation/Event_Service/AccelerometerService.md#AccelData "AccelData") pointer provided in the callback. An example reading the first set of values is shown below: ``` static void accel\_data\_handler(AccelData \*data, uint32\_t num\_samples) {// Read sample 0's x, y, and z valuesint16\_t x = data[0].x;int16\_t y = data[0].y;int16\_t z = data[0].z;// Determine if the sample occured during vibration, and when it occuredbool did\_vibrate = data[0].did\_vibrate;uint64\_t timestamp = data[0].timestamp;if(!did\_vibrate) {// Print it outAPP\_LOG(APP\_LOG\_LEVEL\_INFO, "t: %llu, x: %d, y: %d, z: %d",timestamp, x, y, z);} else {// Discard with a warningAPP\_LOG(APP\_LOG\_LEVEL\_WARNING, "Vibration occured during collection");}} ``` The code above will output the first sample in each batch to app logs, which will look similar to the following: ``` [15:33:18] -data-service.c:21> t: 1449012797098, x: -111, y: -280, z: -1153 [15:33:18] -data-service.c:21> t: 1449012797305, x: -77, y: 40, z: -1014 [15:33:18] -data-service.c:21> t: 1449012797507, x: -60, y: 4, z: -1080 [15:33:19] -data-service.c:21> t: 1449012797710, x: -119, y: -55, z: -921 [15:33:19] -data-service.c:21> t: 1449012797914, x: 628, y: 64, z: -506 ``` --- # Background Worker Source: https://developer.repebble.com/guides/events-and-services/background-worker/ In addition to the main foreground task that every Pebble app implements, a second background worker task can also be created. This worker is capable of running even when the foreground task is closed, and is useful for tasks that must continue for long periods of time. For example, apps that log sensor data. There are several important points to note about the capabilities of this worker when compared to those of the foreground task: - The worker is constrained to 10.5 kB of memory. - Some APIs are not available to the worker. See the [_Available APIs_](#available-apis) section below for more information. - There can only be one background worker active at a time. In the event that a second one attempts to launch from another watchapp, the user will be asked to choose whether the new worker can replace the existing one. - The user can determine which app's worker is running by checking the 'Background App' section of the Settings menu. Workers can also be launched from there. - The worker can launch the foreground app using [`worker_launch_app()`](https://developer.repebble.com/docs/c/Worker.md#worker_launch_app "worker\_launch\_app"). This means that the foreground app should be prepared to be launched at any time that the worker is running. > Note: This API should not be used to build background timers; use the [`Wakeup`](https://developer.repebble.com/docs/c/Foundation/Wakeup.md "Wakeup") API instead. ## Adding a Worker The background worker's behavior is determined by code written in a separate C file to the foreground app, created in the `/worker_src` project directory. This project structure can also be generated using the [`pebble` tool](https://developer.repebble.com/guides/tools-and-resources/pebble-tool.md) with the `--worker` flag as shown below: ``` $ pebble new-project --worker project_name ``` The worker C file itself has a basic structure similar to a regular Pebble app, but with a couple of minor changes, as shown below: ``` #include \static void prv\_init() {// Initialize the worker here}static void prv\_deinit() {// Deinitialize the worker here}int main(void) {prv\_init();worker\_event\_loop();prv\_deinit();} ``` ## Launching the Worker To launch the worker from the foreground app, use [`app_worker_launch()`](https://developer.repebble.com/docs/c/Foundation/AppWorker.md#app_worker_launch "app\_worker\_launch"): ``` // Launch the background workerAppWorkerResult result = app\_worker\_launch(); ``` The [`AppWorkerResult`](https://developer.repebble.com/docs/c/Foundation/AppWorker.md#AppWorkerResult "AppWorkerResult") returned will indicate any errors encountered as a result of attempting to launch the worker. Possible result values include: | Result | Value | Description | | --- | --- | --- | | [`APP_WORKER_RESULT_SUCCESS`](https://developer.repebble.com/docs/c/Foundation/AppWorker.md#APP_WORKER_RESULT_SUCCESS "APP\_WORKER\_RESULT\_SUCCESS") | `0` | The worker launch was successful, but may not start running immediately. Use [`app_worker_is_running()`](https://developer.repebble.com/docs/c/Foundation/AppWorker.md#app_worker_is_running "app\_worker\_is\_running") to determine when the worker has started running. | | [`APP_WORKER_RESULT_NO_WORKER`](https://developer.repebble.com/docs/c/Foundation/AppWorker.md#APP_WORKER_RESULT_NO_WORKER "APP\_WORKER\_RESULT\_NO\_WORKER") | `1` | No worker found for the current app. | | [`APP_WORKER_RESULT_ALREADY_RUNNING`](https://developer.repebble.com/docs/c/Foundation/AppWorker.md#APP_WORKER_RESULT_ALREADY_RUNNING "APP\_WORKER\_RESULT\_ALREADY\_RUNNING") | `4` | The worker is already running. | | [`APP_WORKER_RESULT_ASKING_CONFIRMATION`](https://developer.repebble.com/docs/c/Foundation/AppWorker.md#APP_WORKER_RESULT_ASKING_CONFIRMATION "APP\_WORKER\_RESULT\_ASKING\_CONFIRMATION") | `5` | The user will be asked for confirmation. To determine whether the worker was given permission to launch, use [`app_worker_is_running()`](https://developer.repebble.com/docs/c/Foundation/AppWorker.md#app_worker_is_running "app\_worker\_is\_running") for a short period after receiving this result. | ## Communicating Between Tasks There are three methods of passing data between the foreground and background worker tasks: - Save the data using the [`Storage`](https://developer.repebble.com/docs/c/Foundation/Storage.md "Storage") API, then read it in the other task. - Send the data to a companion phone app using the [`DataLogging`](https://developer.repebble.com/docs/c/Foundation/DataLogging.md "DataLogging") API. Details on how to do this are available in [_Datalogging_](https://developer.repebble.com/guides/communication/datalogging.md). - Pass the data directly while the other task is running, using an [`AppWorkerMessage`](https://developer.repebble.com/docs/c/Foundation/AppWorker.md#AppWorkerMessage "AppWorkerMessage"). These messages can be sent bi-directionally by creating an `AppWorkerMessageHandler` in each task. The handler will fire in both the foreground and the background tasks, so you must identify the source of the message using the `type` parameter. ## Managing the Worker The current running state of the background worker can be determined using the [`app_worker_is_running()`](https://developer.repebble.com/docs/c/Foundation/AppWorker.md#app_worker_is_running "app\_worker\_is\_running") function: ``` // Check to see if the worker is currently activebool running = app\_worker\_is\_running(); ``` The user can tell whether the worker is running by checking the system 'Background App' settings. Any installed workers with be listed there. The worker can be stopped using [`app_worker_kill()`](https://developer.repebble.com/docs/c/Foundation/AppWorker.md#app_worker_kill "app\_worker\_kill"): ``` // Stop the background workerAppWorkerResult result = app\_worker\_kill(); ``` Possible `result` values when attempting to kill the worker are as follows: | Result | Value | Description | | --- | --- | --- | | [`APP_WORKER_RESULT_SUCCESS`](https://developer.repebble.com/docs/c/Foundation/AppWorker.md#APP_WORKER_RESULT_SUCCESS "APP\_WORKER\_RESULT\_SUCCESS") | `0` | The worker launch was killed successfully. | | [`APP_WORKER_RESULT_DIFFERENT_APP`](https://developer.repebble.com/docs/c/Foundation/AppWorker.md#APP_WORKER_RESULT_DIFFERENT_APP "APP\_WORKER\_RESULT\_DIFFERENT\_APP") | `2` | A worker from a different app is running, and cannot be killed by this app. | | [`APP_WORKER_RESULT_NOT_RUNNING`](https://developer.repebble.com/docs/c/Foundation/AppWorker.md#APP_WORKER_RESULT_NOT_RUNNING "APP\_WORKER\_RESULT\_NOT\_RUNNING") | `3` | The worker is not currently running. | ## Available APIs Background workers do not have access to the UI APIs. They also cannot use the [`AppMessage`](https://developer.repebble.com/docs/c/Foundation/AppMessage.md "AppMessage") API or load resources. Most other APIs are available including (but not limited to) [`AccelerometerService`](https://developer.repebble.com/docs/c/Foundation/Event_Service/AccelerometerService.md "AccelerometerService"), [`CompassService`](https://developer.repebble.com/docs/c/Foundation/Event_Service/CompassService.md "CompassService"), [`DataLogging`](https://developer.repebble.com/docs/c/Foundation/DataLogging.md "DataLogging"), [`HealthService`](https://developer.repebble.com/docs/c/Foundation/Event_Service/HealthService.md "HealthService"), [`ConnectionService`](https://developer.repebble.com/docs/c/Foundation/Event_Service/ConnectionService.md "ConnectionService"), [`BatteryStateService`](https://developer.repebble.com/docs/c/Foundation/Event_Service/BatteryStateService.md "BatteryStateService"), [`TickTimerService`](https://developer.repebble.com/docs/c/Foundation/Event_Service/TickTimerService.md "TickTimerService") and [`Storage`](https://developer.repebble.com/docs/c/Foundation/Storage.md "Storage"). The compiler will throw an error if the developer attempts to use an API unsupported by the worker. For a definitive list of available APIs, check `pebble_worker.h` in the SDK bundle for the presence of the desired API. --- # Backlight Source: https://developer.repebble.com/guides/events-and-services/light/ The Light API gives an app limited control over the watch backlight. An app can ask the system to flash the backlight as if the user had performed a wrist gesture, or force it on for as long as it needs. On hardware with an RGB backlight, the same API can also tint that backlight any color the user can render in their UI. All of these calls operate on top of the user's normal backlight settings (auto on-shake, brightness, etc.), so apps don't need to worry about overriding the user's preferences permanently. ## Triggering the Backlight The simplest and most common use of the Light API is to trigger the backlight exactly as a wrist flick would. The light comes on, stays on for the system auto-off interval, and then fades out: ``` // Turn the backlight on for the standard auto-off intervallight\_enable\_interaction(); ``` This is the recommended entry point for most apps. It respects the user's backlight setting (it does nothing if the user has the backlight disabled) and does not require a follow-up call to turn the light back off. ## Forcing the Backlight On If an app needs the backlight on for an extended interaction - for example, during a flashlight feature or while the user is doing something with both hands - it can pin it on: ``` // Force the backlight onlight\_enable(true); ``` The backlight stays on until the app explicitly returns control to the automatic system: ``` // Hand control back to the system's auto-off behaviorlight\_enable(false); ``` > Holding the backlight on draws significantly more power than letting it auto-off. The system also dims the backlight automatically when the battery is low to protect the user's remaining runtime, so a forced-on backlight is not a guarantee of full brightness. To check whether the backlight is currently on, use: ``` if (light\_is\_on()) {// The backlight is currently lit} ``` ## Reacting to Backlight Changes The [`BacklightService`](https://developer.repebble.com/docs/c/Foundation/Event_Service/BacklightService.md "BacklightService") tells an app when the backlight turns on or off, whether the user flicked their wrist, pressed a button or the auto-off timer ran out. A watchface can use it to switch to a brighter color scheme while the light is on, or to pause an animation when the display is no longer being looked at: ``` static void backlight\_handler(bool on) {// on is true when the backlight has just turned on,// false when it has turned fully offlayer\_mark\_dirty(s\_canvas\_layer);}// Subscribebacklight\_service\_subscribe(backlight\_handler);// Later, stop receiving eventsbacklight\_service\_unsubscribe(); ``` The handler is called once per transition. Any state other than fully off, including the fade-out, counts as on. Use [`light_is_on()`](https://developer.repebble.com/docs/c/User_Interface/Light.md#light_is_on "light\_is\_on") to read the current state without subscribing. [`backlight_service_subscribe()`](https://developer.repebble.com/docs/c/Foundation/Event_Service/BacklightService.md#backlight_service_subscribe "backlight\_service\_subscribe") is available on Pebble 2 Duo (flint), Pebble Time 2 (emery) and Pebble Round 2 (gabbro). On aplite, basalt, chalk and diorite the SDK compiles it to a no-op, so the handler is never called. ## Tinting the Backlight (RGB Hardware Only) On hardware with an RGB backlight, the backlight color can be set to any [`GColor`](https://developer.repebble.com/docs/c/Graphics/Graphics_Types.md#GColor "GColor") value. The color persists for as long as the app is in the foreground, then resets to the user's default when the app exits or is preempted (for example, by an incoming notification modal). ``` // Tint the backlight redlight\_set\_color(GColorRed); ``` `light_set_color()` takes a [`GColor`](https://developer.repebble.com/docs/c/Graphics/Graphics_Types.md#GColor "GColor"), which on current hardware quantizes to the 64-color palette the UI uses. For more control over the exact tint - for example, to match a brand color or blend between two palette entries - use `light_set_color_rgb888()`, which takes a 24-bit RGB value packed into a `uint32_t` as `0x00RRGGBB`: ``` // Tint the backlight with an exact RGB color (teal)light\_set\_color\_rgb888(0x0088CC); ``` Once set, the tint persists for the rest of the app's foreground session - every subsequent backlight trigger will use that color until the app changes it again or exits. To return to the user's chosen default mid-session, call: ``` // Restore the user's default backlight colorlight\_set\_system\_color(); ``` This is important if the app uses different colors for different states (for example, red for an alert and the user default the rest of the time): without an explicit `light_set_system_color()` call the alert color will stick around for every subsequent backlight flash. The system does reset the color automatically when the app exits or is preempted, so there's no need to call `light_set_system_color()` purely on the way out. On hardware without an RGB backlight these two calls are no-ops and can be left in unconditionally; there's no need to wrap them in a preprocessor guard just to compile against other platforms. For cases where a whole block of color-specific logic should be excluded on non-RGB platforms, the `PBL_RGB_BACKLIGHT` compile-time define is present on platforms that have an RGB backlight: ``` #if defined(PBL\_RGB\_BACKLIGHT)// Use a different tint per app statelight\_set\_color(is\_alert ? GColorRed : GColorWhite);#endiflight\_enable\_interaction(); ``` Tinting only changes the _color_ of the backlight; the backlight still needs to be on for the user to see it. Combine `light_set_color()` with `light_enable_interaction()` or `light_enable(true)` so the user actually sees the new color: ``` // Flash the backlight red for the system auto-off intervallight\_set\_color(GColorRed);light\_enable\_interaction(); ``` ## Battery Considerations The backlight is one of the most power-hungry components on the watch. Triggering it through `light_enable_interaction()` for short user interactions is generally fine, but holding it on for long periods with `light_enable(true)` will noticeably reduce battery life. Use the forced-on mode only when the feature genuinely requires it, and make sure to release control with `light_enable(false)` as soon as the user is done. --- # Buttons Source: https://developer.repebble.com/guides/events-and-services/buttons/ Button [`Clicks`](https://developer.repebble.com/docs/c/User_Interface/Clicks.md "Clicks") are the primary input method on Pebble. All Pebble watches come with the same buttons available, shown in the diagram below for Pebble Time: ![button-layout](https://developer.repebble.com/assets/images/guides/sensors-and-input/button-layout.png) These buttons are used in a logical fashion throughout the system: - Back - Navigates back one [`Window`](https://developer.repebble.com/docs/c/User_Interface/Window.md "Window") until the watchface is reached. - Up - Navigates to the previous item in a list, or opens the past timeline when pressed from the watchface. - Select - Opens the app launcher from the watchface, accepts a selected option or list item, or launches the next [`Window`](https://developer.repebble.com/docs/c/User_Interface/Window.md "Window"). - Down - Navigates to the next item in a list, or opens the future timeline when pressed from the watchface. Developers are highly encouraged to follow these patterns when using button clicks in their watchapps, since users will already have an idea of what each button will do to a reasonable degree, thus avoiding the need for lengthy usage instructions for each app. Watchapps that wish to use each button for a specific action should use the [`ActionBarLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/ActionBarLayer.md "ActionBarLayer") or [`ActionMenu`](https://developer.repebble.com/docs/c/User_Interface/Window/ActionMenu.md "ActionMenu") to give hints about what each button will do. > Note: Watchfaces do not receive button clicks. A [`ClickConfigProvider`](https://developer.repebble.com/docs/c/User_Interface/Clicks.md#ClickConfigProvider "ClickConfigProvider") set on a watchface [`Window`](https://developer.repebble.com/docs/c/User_Interface/Window.md "Window") is still called, but the system handles the button events itself: by default Up opens Health and Down opens the timeline (both can be reassigned in the watch's Quick Launch settings), Select opens the launcher and Back is used for Quick Launch, so the subscribed handlers never run. Build a watchapp instead, or read [_App Exit Reason_](https://developer.repebble.com/guides/user-interfaces/app-exit-reason.md) and the launch reasons in [_One Click Actions_](https://developer.repebble.com/guides/design-and-interaction/one-click-actions.md) for ways to open an app from the watchface. ## Listening for Button Clicks Button clicks are received via a subscription to one of the types of button click events listed below. Each [`Window`](https://developer.repebble.com/docs/c/User_Interface/Window.md "Window") that wishes to receive button click events must provide a [`ClickConfigProvider`](https://developer.repebble.com/docs/c/User_Interface/Clicks.md#ClickConfigProvider "ClickConfigProvider") that performs these subscriptions. The first step is to create the [`ClickConfigProvider`](https://developer.repebble.com/docs/c/User_Interface/Clicks.md#ClickConfigProvider "ClickConfigProvider") function: ``` static void click\_config\_provider(void \*context) {// Subcribe to button click events here} ``` The second step is to register the [`ClickConfigProvider`](https://developer.repebble.com/docs/c/User_Interface/Clicks.md#ClickConfigProvider "ClickConfigProvider") with the current [`Window`](https://developer.repebble.com/docs/c/User_Interface/Window.md "Window"), typically after [`window_create()`](https://developer.repebble.com/docs/c/User_Interface/Window.md#window_create "window\_create"): ``` // Use this provider to add button click subscriptionswindow\_set\_click\_config\_provider(window, click\_config\_provider); ``` The final step is to write a [`ClickHandler`](https://developer.repebble.com/docs/c/User_Interface/Clicks.md#ClickHandler "ClickHandler") function for each different type of event subscription required by the watchapp. An example for a single click event is shown below: ``` static void select\_click\_handler(ClickRecognizerRef recognizer, void \*context) {// A single click has just occured} ``` ## Types of Click Events There are five types of button click events that apps subscribe to, enabling virtually any combination of up/down/click events to be utilized in a watchapp. The usage of each of these is explained below: ### Single Clicks Most apps will use this type of click event, which occurs whenever the button specified is pressed and then immediately released. Use [`window_single_click_subscribe()`](https://developer.repebble.com/docs/c/User_Interface/Window.md#window_single_click_subscribe "window\_single\_click\_subscribe") from a [`ClickConfigProvider`](https://developer.repebble.com/docs/c/User_Interface/Clicks.md#ClickConfigProvider "ClickConfigProvider") function, supplying the [`ButtonId`](https://developer.repebble.com/docs/c/User_Interface/Clicks.md#ButtonId "ButtonId") value for the chosen button and the name of the [`ClickHandler`](https://developer.repebble.com/docs/c/User_Interface/Clicks.md#ClickHandler "ClickHandler") that will receive the events: ``` static void click\_config\_provider(void \*context) {ButtonId id = BUTTON\_ID\_SELECT;// The Select buttonwindow\_single\_click\_subscribe(id, select\_click\_handler);} ``` ### Single Repeating Clicks Similar to the single click event, the single repeating click event allows repeating events to be received at a specific interval if the chosen button is held down for a longer period of time. This makes the task of scrolling through many list items or incrementing a value significantly easier for the user, and uses fewer button clicks. ``` static void click\_config\_provider(void \*context) {ButtonId id = BUTTON\_ID\_DOWN;// The Down buttonuint16\_t repeat\_interval\_ms = 200;// Fire every 200 ms while held downwindow\_single\_repeating\_click\_subscribe(id, repeat\_interval\_ms,down\_repeating\_click\_handler);} ``` After an initial press (but not release) of the button `id` subscribed to, the callback will be called repeatedly with an interval of `repeat_interval_ms` until it is then released. Developers can determine if the button is still held down after the first callback by using [`click_recognizer_is_repeating()`](https://developer.repebble.com/docs/c/User_Interface/Clicks.md#click_recognizer_is_repeating "click\_recognizer\_is\_repeating"), as well as get the number of callbacks counted so far with [`click_number_of_clicks_counted()`](https://developer.repebble.com/docs/c/User_Interface/Clicks.md#click_number_of_clicks_counted "click\_number\_of\_clicks\_counted"): ``` static void down\_repeating\_click\_handler(ClickRecognizerRef recognizer,void \*context) {// Is the button still held down?bool is\_repeating = click\_recognizer\_is\_repeating(recognizer);// How many callbacks have been recorded so far?uint8\_t click\_count = click\_number\_of\_clicks\_counted(recognizer);} ``` > Single click and single repeating click subscriptions conflict, and cannot be registered for the same button. ### Multiple Clicks A multi click event will call the [`ClickHandler`](https://developer.repebble.com/docs/c/User_Interface/Clicks.md#ClickHandler "ClickHandler") after a specified number of single clicks has been recorded. A good example of usage is to detect a double or triple click gesture: ``` static void click\_config\_provider(void \*context) {ButtonId id = BUTTON\_ID\_SELECT;// The Select buttonuint8\_t min\_clicks = 2;// Fire after at least two clicksuint8\_t max\_clicks = 3;// Don't fire after three clicksuint16\_t timeout = 300;// Wait 300ms before firingbool last\_click\_only = true;// Fire only after the last clickwindow\_multi\_click\_subscribe(id, min\_clicks, max\_clicks, timeout,last\_click\_only, multi\_select\_click\_handler);} ``` Similar to the single repeating click event, the [`ClickRecognizerRef`](https://developer.repebble.com/docs/c/User_Interface/Clicks.md#ClickRecognizerRef "ClickRecognizerRef") can be used to determine how many clicks triggered this multi click event using [`click_number_of_clicks_counted()`](https://developer.repebble.com/docs/c/User_Interface/Clicks.md#click_number_of_clicks_counted "click\_number\_of\_clicks\_counted"). ### Long Clicks A long click event is fired after a button is held down for the specified amount of time. The event also allows two [`ClickHandler`](https://developer.repebble.com/docs/c/User_Interface/Clicks.md#ClickHandler "ClickHandler")s to be registered - one for when the button is pressed, and another for when the button is released. Only one of these is required. ``` static void click\_config\_provider(void \*context) {ButtonId id = BUTTON\_ID\_SELECT;// The select buttonuint16\_t delay\_ms = 500;// Minimum time pressed to firewindow\_long\_click\_subscribe(id, delay\_ms, long\_down\_click\_handler,long\_up\_click\_handler);} ``` ### Raw Clicks The last type of button click subcsription is used to track raw button click events. Like the long click event, two [`ClickHandler`](https://developer.repebble.com/docs/c/User_Interface/Clicks.md#ClickHandler "ClickHandler")s may be supplied to receive each of the pressed and depressed events. ``` static void click\_config\_provider(void \*context) {ButtonId id = BUTTON\_ID\_SELECT;// The select buttonwindow\_raw\_click\_subscribe(id, raw\_down\_click\_handler, raw\_up\_click\_handler,NULL);} ``` > The last parameter is an optional pointer to a context object to be passed to the callback, and is set to `NULL` if not used. --- # Compass Source: https://developer.repebble.com/guides/events-and-services/compass/ The [`CompassService`](https://developer.repebble.com/docs/c/Foundation/Event_Service/CompassService.md "CompassService") combines data from Pebble's accelerometer and magnetometer to automatically calibrate the compass and produce a [`CompassHeading`](https://developer.repebble.com/docs/c/Foundation/Event_Service/CompassService.md#CompassHeading "CompassHeading"), containing an angle measured relative to magnetic north. The compass service provides magnetic north and information about its status and accuracy through the [`CompassHeadingData`](https://developer.repebble.com/docs/c/Foundation/Event_Service/CompassService.md#CompassHeadingData "CompassHeadingData") structure. ## Calibration The compass service requires an initial calibration before it can return accurate results. Calibration is performed automatically by the system when first required. The [`compass_status`](https://developer.repebble.com/docs/c/Foundation/Event_Service/CompassService.md#CompassHeadingData "CompassHeadingData") field indicates whether the compass service is calibrating. To help the calibration process, the app should show a message to the user asking them to move their wrists in different directions. Refer to the [compass example](https://github.com/pebble-examples/feature-compass) for an example of how to implement this screen. Calibration only runs while at least one app is subscribed to the compass service. If all apps unsubscribe before calibration is complete, the calibration is discarded and will start again from scratch the next time an app subscribes. Once calibration succeeds, it is saved for future reuse by all apps without recalibration. Putting the pebble on charge deletes any saved calibration. ## Magnetic North and True North Depending on the user's location on Earth, the measured heading towards magnetic north and true north can significantly differ. This is due to magnetic variation, also known as 'declination'. Pebble does not automatically correct the magnetic heading to return a true heading, but the API is designed so that this feature can be added in the future and the app will be able to automatically take advantage of it. For a more precise heading, use the `magnetic_heading` field of [`CompassHeadingData`](https://developer.repebble.com/docs/c/Foundation/Event_Service/CompassService.md#CompassHeadingData "CompassHeadingData") and use a webservice to retrieve the declination at the user's current location. Otherwise, use the `true_heading` field. This field will contain the `magnetic_heading` if declination is not available, or the true heading if declination is available. The field `is_declination_valid` will be true when declination is available. Use this information to tell the user whether the app is showing magnetic north or true north. ![Declination illustrated](https://developer.repebble.com/assets/images/guides/pebble-apps/sensors/declination.gif) > To see the true extent of declination, see how declination has [changed over time](http://maps.ngdc.noaa.gov/viewers/historical_declination/). ## Battery Considerations Using the compass will turn on both Pebble's magnetometer and accelerometer. Those two devices will have a slight impact on battery life. A much more significant battery impact will be caused by redrawing the screen too often or performing CPU-intensive work every time the compass heading is updated. Use [`compass_service_subscribe()`](https://developer.repebble.com/docs/c/Foundation/Event_Service/CompassService.md#compass_service_subscribe "compass\_service\_subscribe") if the app only needs to update its UI when new compass data is available, or else use [`compass_service_peek()`](https://developer.repebble.com/docs/c/Foundation/Event_Service/CompassService.md#compass_service_peek "compass\_service\_peek") if this happens much less frequently. ## Defining "Up" on Pebble Compass readings are always relative to the current orientation of Pebble. Using the accelerometer, the compass service figures out which direction the user is facing. ![Compass Orientation](https://developer.repebble.com/assets/images/guides/pebble-apps/sensors/compass-orientation.png) The best orientation to encourage users to adopt while using a compass-enabled watchapp is with the top of the watch parallel to the ground. If the watch is raised so that the screen is facing the user, the plane will now be perpedicular to the screen, but still parallel to the ground. ## Angles and Degrees The magnetic heading value is presented as a number between 0 and TRIG\_MAX\_ANGLE (65536). This range is used to give a higher level of precision for drawing commands, which is preferable to using only 360 degrees. If you imagine an analogue clock face on your Pebble, the angle 0 is always at the 12 o'clock position, and the magnetic heading angle is calculated in a counter clockwise direction from 0. This can be confusing to grasp at first, as it’s opposite of how direction is measured on a compass, but it's simple to convert the values into a clockwise direction: ``` int clockwise\_angle = TRIG\_MAX\_ANGLE - heading\_data.magnetic\_heading; ``` Once you have an angle relative to North, you can convert that to degrees using the helper function [`TRIGANGLE_TO_DEG()`](https://developer.repebble.com/docs/c/Foundation/Math.md#TRIGANGLE_TO_DEG "TRIGANGLE\_TO\_DEG"): ``` int degrees = TRIGANGLE\_TO\_DEG(TRIG\_MAX\_ANGLE - heading\_data.magnetic\_heading); ``` ## Subscribing to Compass Data Compass heading events can be received in a watchapp by subscribing to the [`CompassService`](https://developer.repebble.com/docs/c/Foundation/Event_Service/CompassService.md "CompassService"): ``` // Subscribe to compass heading updatescompass\_service\_subscribe(compass\_heading\_handler); ``` The provided [`CompassHeadingHandler`](https://developer.repebble.com/docs/c/Foundation/Event_Service/CompassService.md#CompassHeadingHandler "CompassHeadingHandler") function (called `compass_heading_handler` above) can be used to read the state of the compass, and the current heading if it is available. This value is given in the range of `0` to [`TRIG_MAX_ANGLE`](https://developer.repebble.com/docs/c/Foundation/Math.md#TRIG_MAX_ANGLE "TRIG\_MAX\_ANGLE") to preserve precision, and so it can be converted using the [`TRIGANGLE_TO_DEG()`](https://developer.repebble.com/docs/c/Foundation/Math.md#TRIGANGLE_TO_DEG "TRIGANGLE\_TO\_DEG") macro: ``` static void compass\_heading\_handler(CompassHeadingData heading\_data) {// Is the compass calibrated?switch(heading\_data.compass\_status) {case CompassStatusDataInvalid:APP\_LOG(APP\_LOG\_LEVEL\_INFO, "Not yet calibrated.");break;case CompassStatusCalibrating:APP\_LOG(APP\_LOG\_LEVEL\_INFO, "Calibration in progress. Heading is %ld",TRIGANGLE\_TO\_DEG(TRIG\_MAX\_ANGLE - heading\_data.magnetic\_heading));break;case CompassStatusCalibrated:APP\_LOG(APP\_LOG\_LEVEL\_INFO, "Calibrated! Heading is %ld",TRIGANGLE\_TO\_DEG(TRIG\_MAX\_ANGLE - heading\_data.magnetic\_heading));break;}} ``` By default, the callback will be triggered whenever the heading changes by one degree. To reduce the frequency of updates, change the threshold for heading changes by setting a heading filter: ``` // Only notify me when the heading has changed by more than 5 degrees.compass\_service\_set\_heading\_filter(DEG\_TO\_TRIGANGLE(5)); ``` ## Unsubscribing From Compass Data When the app is done using the compass, stop receiving callbacks by unsubscribing: ``` compass\_service\_unsubscribe(); ``` ## Peeking at Compass Data To fetch a compass heading, simply peek to get a single sample: ``` // Peek to get dataCompassHeadingData data;compass\_service\_peek(&data); ``` If not already subscribed, this temporarily subscribes the app to the compass service without a handler. WARNING: Regardless of if you were already subscribed with a handler or not, calling `compass_service_peek()` sets a timer to unsubscribe after 11 seconds (refreshed on each call). > Similar to the subscription-provided data, the app should examine the peeked [`CompassHeadingData`](https://developer.repebble.com/docs/c/Foundation/Event_Service/CompassService.md#CompassHeadingData "CompassHeadingData") to determine if it is valid (i.e. the compass is calibrated). Note that as calibration requires an active subscription, and calibration can take longer than 11 seconds, to calibrate while only using peek you must keep calling `compass_service_peek()` over time to refresh its unsubscribe timer until calibration is successful. --- # Dictation Source: https://developer.repebble.com/guides/events-and-services/dictation/ **PLATFORM NOTICE** This guide does not apply to apps built to run on the Aplite platform (Pebble Classic and Pebble Steel). On hardware [platforms](https://developer.repebble.com/faqs.md#pebble-sdk) supporting a microphone, the [`Dictation`](https://developer.repebble.com/docs/c/Foundation/Dictation.md "Dictation") API can be used to gather arbitrary text input from a user. This approach is much faster than any previous button-based text input system (such as [tertiary text](https://github.com/vgmoose/tertiary_text)), and includes the ability to allow users to re-attempt dictation if there are any errors in the returned transcription. > Note: Apps running on multiple hardware platforms that may or may not include a microphone should use the `PBL_MICROPHONE` compile-time define (as well as checking API return values) to gracefully handle when it is not available. ## How the Dictation API Works The [`Dictation`](https://developer.repebble.com/docs/c/Foundation/Dictation.md "Dictation") API invokes the same UI that is shown to the user when responding to notifications via the system menu, with events occuring in the following order: - The user initiates transcription and the dictation UI is displayed. - The user dictates the phrase they would like converted into text. - The audio is transmitted via the Pebble phone application to a 3rd party service and translated into text. - When the text is returned, the user is given the opportunity to review the result of the transcription. At this time they may elect to re-attempt the dictation by pressing the Back button and speaking clearer. - When the user is happy with the transcription, the text is provided to the app by pressing the Select button. - If an error occurs in the transcription attempt, the user is automatically allowed to re-attempt the dictation. - The user can retry their dictation by rejecting a successful transcription, but only if confirmation dialogs are enabled. ## Beginning a Dictation Session To get voice input from a user, an app must first create a [`DictationSession`](https://developer.repebble.com/docs/c/Foundation/Dictation.md#DictationSession "DictationSession") that contains data relating to the status of the dictation service, as well as an allocated buffer to store the result of any transcriptions. This should be declared in the file-global scope (as `static`), so it can be used at any time (in button click handlers, for example). ``` static DictationSession \*s\_dictation\_session; ``` A callback of type [`DictationSessionStatusCallback`](https://developer.repebble.com/docs/c/Foundation/Dictation.md#DictationSessionStatusCallback "DictationSessionStatusCallback") is also required to notify the developer to the status of any dictation requests and transcription results. This is called at any time the dictation UI exits, which can be for any of the following reasons: - The user accepts a transcription result. - A transcription is successful but the confirmation dialog is disabled. - The user exits the dictation UI with the Back button. - When any error occurs and the error dialogs are disabled. - Too many transcription errors occur. ``` static void dictation\_session\_callback(DictationSession \*session, DictationSessionStatus status,char \*transcription, void \*context) {// Print the results of a transcription attemptAPP\_LOG(APP\_LOG\_LEVEL\_INFO, "Dictation status: %d", (int)status);} ``` At the end of this callback the `transcription` pointer becomes invalid - if the text is required later it should be copied into a separate buffer provided by the app. The size of this dictation buffer is chosen by the developer, and should be large enough to accept all expected input. Any transcribed text longer than the length of the buffer will be truncated. ``` // Declare a buffer for the DictationSessionstatic char s\_last\_text[512]; ``` Finally, create the [`DictationSession`](https://developer.repebble.com/docs/c/Foundation/Dictation.md#DictationSession "DictationSession") and supply the size of the buffer and the [`DictationSessionStatusCallback`](https://developer.repebble.com/docs/c/Foundation/Dictation.md#DictationSessionStatusCallback "DictationSessionStatusCallback"). This session may be used as many times as requires for multiple transcriptions. A context pointer may also optionally be provided. ``` // Create new dictation sessions\_dictation\_session = dictation\_session\_create(sizeof(s\_last\_text),dictation\_session\_callback, NULL); ``` ## Obtaining Dictated Text After creating a [`DictationSession`](https://developer.repebble.com/docs/c/Foundation/Dictation.md#DictationSession "DictationSession"), the developer can begin a dictation attempt at any time, providing that one is not already in progress. ``` // Start dictation UIdictation\_session\_start(s\_dictation\_session); ``` The dictation UI will be displayed and the user will speak their desired input. ![listening](https://developer.repebble.com/assets/images/guides/pebble-apps/sensors/listening.png) It is recommended to provide visual guidance on the format of the expected input before the [`dictation_session_start()`](https://developer.repebble.com/docs/c/Foundation/Dictation.md#dictation_session_start "dictation\_session\_start") is called. For example, if the user is expected to speak a location that should be a city name, they should be briefed as such before being asked to provide input. When the user exits the dictation UI, the developer's [`DictationSessionStatusCallback`](https://developer.repebble.com/docs/c/Foundation/Dictation.md#DictationSessionStatusCallback "DictationSessionStatusCallback") will be called. The `status` parameter provided will inform the developer as to whether or not the transcription was successful using a [`DictationSessionStatus`](https://developer.repebble.com/docs/c/Foundation/Dictation.md#DictationSessionStatus "DictationSessionStatus") value. It is useful to check this value, as there are multiple reasons why a dictation request may not yield a successful result. These values are described below under [_DictationSessionStatus Values_](#dictationsessionstatus-values). If the value of `status` is equal to [`DictationSessionStatusSuccess`](https://developer.repebble.com/docs/c/Foundation/Dictation.md#DictationSessionStatusSuccess "DictationSessionStatusSuccess"), the transcription was successful. The user's input can be read from the `transcription` parameter for evaluation and storage for later use if required. Note that once the callback returns, `transcription` will no longer be valid. For example, a [`TextLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/TextLayer.md "TextLayer") in the app's UI with variable name `s_output_layer` may be used to show the status of an attempted transcription: ``` if(status == DictationSessionStatusSuccess) {// Display the dictated textsnprintf(s\_last\_text, sizeof(s\_last\_text), "Transcription:\n\n%s", transcription);text\_layer\_set\_text(s\_output\_layer, s\_last\_text);} else {// Display the reason for any errorstatic char s\_failed\_buff[128];snprintf(s\_failed\_buff, sizeof(s\_failed\_buff), "Transcription failed.\n\nReason:\n%d",(int)status);text\_layer\_set\_text(s\_output\_layer, s\_failed\_buff);} ``` The confirmation mechanism allowing review of the transcription result can be disabled if it is not needed. An example of such a scenario may be to speed up a 'yes' or 'no' decision where the two expected inputs are distinct and different. ``` // Disable the confirmation screendictation\_session\_enable\_confirmation(s\_dictation\_session, false); ``` It is also possible to disable the error dialogs, if so desired. This will disable the dialogs that appear when a transcription attempt fails, as well as disabling the ability to retry the dictation if a failure occurs. ``` // Disable error dialogsdictation\_session\_enable\_error\_dialogs(s\_dictation\_session, false); ``` ### DictationSessionStatus Values These are the possible values provided by a [`DictationSessionStatusCallback`](https://developer.repebble.com/docs/c/Foundation/Dictation.md#DictationSessionStatusCallback "DictationSessionStatusCallback"), and should be used to handle transcription success or failure for any of the following reasons. | Status | Value | Description | | --- | --- | --- | | [`DictationSessionStatusSuccess`](https://developer.repebble.com/docs/c/Foundation/Dictation.md#DictationSessionStatusSuccess "DictationSessionStatusSuccess") | `0` | Transcription successful, with a valid result. | | [`DictationSessionStatusFailureTranscriptionRejected`](https://developer.repebble.com/docs/c/Foundation/Dictation.md#DictationSessionStatusFailureTranscriptionRejected "DictationSessionStatusFailureTranscriptionRejected") | `1` | User rejected transcription and dismissed the dictation UI. | | [`DictationSessionStatusFailureTranscriptionRejectedWithError`](https://developer.repebble.com/docs/c/Foundation/Dictation.md#DictationSessionStatusFailureTranscriptionRejectedWithError "DictationSessionStatusFailureTranscriptionRejectedWithError") | `2` | User exited the dictation UI after a transcription error. | | [`DictationSessionStatusFailureSystemAborted`](https://developer.repebble.com/docs/c/Foundation/Dictation.md#DictationSessionStatusFailureSystemAborted "DictationSessionStatusFailureSystemAborted") | `3` | Too many errors occurred during transcription and the dictation UI exited. | | [`DictationSessionStatusFailureNoSpeechDetected`](https://developer.repebble.com/docs/c/Foundation/Dictation.md#DictationSessionStatusFailureNoSpeechDetected "DictationSessionStatusFailureNoSpeechDetected") | `4` | No speech was detected and the dictation UI exited. | | [`DictationSessionStatusFailureConnectivityError`](https://developer.repebble.com/docs/c/Foundation/Dictation.md#DictationSessionStatusFailureConnectivityError "DictationSessionStatusFailureConnectivityError") | `5` | No Bluetooth or Internet connection available. | | [`DictationSessionStatusFailureDisabled`](https://developer.repebble.com/docs/c/Foundation/Dictation.md#DictationSessionStatusFailureDisabled "DictationSessionStatusFailureDisabled") | `6` | Voice transcription disabled for this user. This can occur if the user has disabled sending 'Usage logs' in the Pebble mobile app. | | [`DictationSessionStatusFailureInternalError`](https://developer.repebble.com/docs/c/Foundation/Dictation.md#DictationSessionStatusFailureInternalError "DictationSessionStatusFailureInternalError") | `7` | Voice transcription failed due to an internal error. | | [`DictationSessionStatusFailureRecognizerError`](https://developer.repebble.com/docs/c/Foundation/Dictation.md#DictationSessionStatusFailureRecognizerError "DictationSessionStatusFailureRecognizerError") | `8` | Cloud recognizer failed to transcribe speech (only possible if error dialogs are disabled). | --- # Event Services Source: https://developer.repebble.com/guides/events-and-services/events/ All Pebble apps are executed in three phases, which are summarized below: - Initialization - all code from the beginning of `main()` is run to set up all the components of the app. - Event Loop - the app waits for and responds to any event services it has subscribed to. - Deinitialization - when the app is exiting (i.e.: the user has pressed Back from the last [`Window`](https://developer.repebble.com/docs/c/User_Interface/Window.md "Window") in the stack) [`app_event_loop()`](https://developer.repebble.com/docs/c/Foundation/App.md#app_event_loop "app\_event\_loop") returns, and all deinitialization code is run before the app exits. Once [`app_event_loop()`](https://developer.repebble.com/docs/c/Foundation/App.md#app_event_loop "app\_event\_loop") is called, execution of `main()` pauses and all further activities are performed when events from various [`Event Service`](https://developer.repebble.com/docs/c/Foundation/Event_Service.md "Event Service") types occur. This continues until the app is exiting, and is typically handled in the following pattern: ``` static void init() {// Initialization code here}static void deinit() {// Deinitialization code here}int main(void) {init();app\_event\_loop();deinit();} ``` ## Types of Events There are multiple types of events an app can receive from various event services. These are described in the table below, along with their handler signature and a brief description of what they do: | Event Service | Handler(s) | Description | | --- | --- | --- | | [`TickTimerService`](https://developer.repebble.com/docs/c/Foundation/Event_Service/TickTimerService.md "TickTimerService") | [`TickHandler`](https://developer.repebble.com/docs/c/Foundation/Event_Service/TickTimerService.md#TickHandler "TickHandler") | Most useful for watchfaces. Allows apps to be notified when a second, minute, hour, day, month or year ticks by. | | [`ConnectionService`](https://developer.repebble.com/docs/c/Foundation/Event_Service/ConnectionService.md "ConnectionService") | [`ConnectionHandler`](https://developer.repebble.com/docs/c/Foundation/Event_Service/ConnectionService.md#ConnectionHandler "ConnectionHandler") | Allows apps to know when the Bluetooth connection with the phone connects and disconnects. | | [`AccelerometerService`](https://developer.repebble.com/docs/c/Foundation/Event_Service/AccelerometerService.md "AccelerometerService") | [`AccelDataHandler`](https://developer.repebble.com/docs/c/Foundation/Event_Service/AccelerometerService.md#AccelDataHandler "AccelDataHandler") [`AccelTapHandler`](https://developer.repebble.com/docs/c/Foundation/Event_Service/AccelerometerService.md#AccelTapHandler "AccelTapHandler") | Allows apps to receive raw data or tap events from the onboard accelerometer. | | [`BatteryStateService`](https://developer.repebble.com/docs/c/Foundation/Event_Service/BatteryStateService.md "BatteryStateService") | [`BatteryStateHandler`](https://developer.repebble.com/docs/c/Foundation/Event_Service/BatteryStateService.md#BatteryStateHandler "BatteryStateHandler") | Allows apps to read the state of the battery, as well as whether the watch is plugged in and charging. | | [`HealthService`](https://developer.repebble.com/docs/c/Foundation/Event_Service/HealthService.md "HealthService") | [`HealthEventHandler`](https://developer.repebble.com/docs/c/Foundation/Event_Service/HealthService.md#HealthEventHandler "HealthEventHandler") | Allows apps to be notified to changes in various [`HealthMetric`](https://developer.repebble.com/docs/c/Foundation/Event_Service/HealthService.md#HealthMetric "HealthMetric") values as the user performs physical activities. | | [`AppFocusService`](https://developer.repebble.com/docs/c/Foundation/Event_Service/AppFocusService.md "AppFocusService") | [`AppFocusHandler`](https://developer.repebble.com/docs/c/Foundation/Event_Service/AppFocusService.md#AppFocusHandler "AppFocusHandler") | Allows apps to know when they are obscured by another window, such as when a notification modal appears. | | [`CompassService`](https://developer.repebble.com/docs/c/Foundation/Event_Service/CompassService.md "CompassService") | [`CompassHeadingHandler`](https://developer.repebble.com/docs/c/Foundation/Event_Service/CompassService.md#CompassHeadingHandler "CompassHeadingHandler") | Allows apps to read a compass heading, including calibration status of the sensor. | | [`BacklightService`](https://developer.repebble.com/docs/c/Foundation/Event_Service/BacklightService.md "BacklightService") | [`BacklightHandler`](https://developer.repebble.com/docs/c/Foundation/Event_Service/BacklightService.md#BacklightHandler "BacklightHandler") | Allows apps to know when the backlight turns on or off. See [_Backlight_](https://developer.repebble.com/guides/events-and-services/light.md). | | [`AlarmService`](https://developer.repebble.com/docs/c/Foundation/Event_Service/AlarmService.md "AlarmService") | None | Lets apps peek at the next alarm the user has set. | In addition, many other APIs also operate through the use of various callbacks including [`MenuLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/MenuLayer.md "MenuLayer"), [`AppMessage`](https://developer.repebble.com/docs/c/Foundation/AppMessage.md "AppMessage"), [`Timer`](https://developer.repebble.com/docs/c/Foundation/Timer.md "Timer"), and [`Wakeup`](https://developer.repebble.com/docs/c/Foundation/Wakeup.md "Wakeup"), but these are not considered to be 'event services' in the same sense. ## Using Event Services The event services described in this guide are all used in the same manner - the app subscribes an implementation of one or more handlers, and is notified by the system when an event of that type occurs. In addition, most also include a 'peek' style API to read a single data item or status value on demand. This can be useful to determine the initial service state when a watchapp starts. Apps can subscribe to as many of these services as they require, and can also unsubscribe at any time to stop receiving events. Each event service is briefly discussed below with multiple snippets - handler implementation example, subscribing to the service, and any 'peek' API. ### Tick Timer Service The [`TickTimerService`](https://developer.repebble.com/docs/c/Foundation/Event_Service/TickTimerService.md "TickTimerService") allows an app to be notified when different units of time change. This is decided based upon the [`TimeUnits`](https://developer.repebble.com/docs/c/Foundation/Event_Service/TickTimerService.md#TimeUnits "TimeUnits") value specified when a subscription is added. The [`struct tm`](http://www.cplusplus.com/reference/ctime/tm/) pointer provided in the handler is a standard C object that contains many data fields describing the current time. This can be used with [`strftime()`](http://www.cplusplus.com/reference/ctime/strftime/) to obtain a human-readable string. ``` static void tick\_handler(struct tm \*tick\_time, TimeUnits changed) {static char s\_buffer[8];// Read time into a string bufferstrftime(s\_buffer, sizeof(s\_buffer), "%H:%M", tick\_time);APP\_LOG(APP\_LOG\_LEVEL\_INFO, "Time is now %s", s\_buffer);} ``` ``` // Get updates when the current minute changestick\_timer\_service\_subscribe(MINUTE\_UNIT, tick\_handler); ``` > The [`TickTimerService`](https://developer.repebble.com/docs/c/Foundation/Event_Service/TickTimerService.md "TickTimerService") has no 'peek' API, but a similar effect can be achieved using the [`time()`](https://developer.repebble.com/docs/c/Standard_C/Time.md "Time") and [`localtime()`](https://developer.repebble.com/docs/c/Standard_C/Time.md#localtime "localtime") APIs. ### Connection Service The [`ConnectionService`](https://developer.repebble.com/docs/c/Foundation/Event_Service/ConnectionService.md "ConnectionService") uses a handler for each of two connection types: - `pebble_app_connection_handler` - the connection to the Pebble app on the phone, analogous with the bluetooth connection state. - `pebblekit_connection_handler` - the connection to an iOS companion app, if applicable. Will never occur on Android. Either one is optional, but at least one must be specified for a valid subscription. ``` static void app\_connection\_handler(bool connected) {APP\_LOG(APP\_LOG\_LEVEL\_INFO, "Pebble app %sconnected", connected ? "" : "dis");}static void kit\_connection\_handler(bool connected) {APP\_LOG(APP\_LOG\_LEVEL\_INFO, "PebbleKit %sconnected", connected ? "" : "dis"); } ``` ``` connection\_service\_subscribe((ConnectionHandlers) {.pebble\_app\_connection\_handler = app\_connection\_handler,.pebblekit\_connection\_handler = kit\_connection\_handler}); ``` ``` // Peek at either the Pebble app or PebbleKit connectionsbool app\_connection = connection\_service\_peek\_pebble\_app\_connection();bool kit\_connection = connection\_service\_peek\_pebblekit\_connection(); ``` ### Accelerometer Service The [`AccelerometerService`](https://developer.repebble.com/docs/c/Foundation/Event_Service/AccelerometerService.md "AccelerometerService") can be used in two modes - tap events and raw data events. [`AccelTapHandler`](https://developer.repebble.com/docs/c/Foundation/Event_Service/AccelerometerService.md#AccelTapHandler "AccelTapHandler") and [`AccelDataHandler`](https://developer.repebble.com/docs/c/Foundation/Event_Service/AccelerometerService.md#AccelDataHandler "AccelDataHandler") are used for each of these respective use cases. See the [_Accelerometer_](https://developer.repebble.com/guides/events-and-services/accelerometer.md) guide for more information. **Data Events** ``` static void accel\_data\_handler(AccelData \*data, uint32\_t num\_samples) {APP\_LOG(APP\_LOG\_LEVEL\_INFO, "Got %d new samples", (int)num\_samples);} ``` ``` const int num\_samples = 10;// Subscribe to data eventsaccel\_data\_service\_subscribe(num\_samples, accel\_data\_handler); ``` ``` // Peek at the last readingAccelData data;accel\_service\_peek(&data); ``` **Tap Events** ``` static void accel\_tap\_handler(AccelAxisType axis, int32\_t direction) {APP\_LOG(APP\_LOG\_LEVEL\_INFO, "Tap event received");} ``` ``` // Subscribe to tap eventsaccel\_tap\_service\_subscribe(accel\_tap\_handler); ``` ### Battery State Service The [`BatteryStateService`](https://developer.repebble.com/docs/c/Foundation/Event_Service/BatteryStateService.md "BatteryStateService") allows apps to examine the state of the battery, and whether or not is is plugged in and charging. ``` static void battery\_state\_handler(BatteryChargeState charge) {// Report the current charge percentageAPP\_LOG(APP\_LOG\_LEVEL\_INFO, "Battery charge is %d%%", (int)charge.charge\_percent);} ``` ``` // Get battery state updatesbattery\_state\_service\_subscribe(battery\_state\_handler); ``` ``` // Peek at the current battery stateBatteryChargeState state = battery\_state\_service\_peek(); ``` ### Health Service The [`HealthService`](https://developer.repebble.com/docs/c/Foundation/Event_Service/HealthService.md "HealthService") uses the [`HealthEventHandler`](https://developer.repebble.com/docs/c/Foundation/Event_Service/HealthService.md#HealthEventHandler "HealthEventHandler") to notify a subscribed app when new data pertaining to a [`HealthMetric`](https://developer.repebble.com/docs/c/Foundation/Event_Service/HealthService.md#HealthMetric "HealthMetric") is available. See the [_Pebble Health_](https://developer.repebble.com/guides/events-and-services/health.md) guide for more information. ``` static void health\_handler(HealthEventType event, void \*context) {if(event == HealthEventMovementUpdate) {APP\_LOG(APP\_LOG\_LEVEL\_INFO, "New health movement event");}} ``` ``` // Subscribe to health-related eventshealth\_service\_events\_subscribe(health\_handler, NULL); ``` ### App Focus Service The [`AppFocusService`](https://developer.repebble.com/docs/c/Foundation/Event_Service/AppFocusService.md "AppFocusService") operates in two modes - basic and complete. **Basic Subscription** A basic subscription involves only one handler which will be fired when the app is moved in or out of focus, and any animated transition has completed. ``` static void focus\_handler(bool in\_focus) {APP\_LOG(APP\_LOG\_LEVEL\_INFO, "App is %s in focus", in\_focus ? "now" : "not");} ``` ``` // Add a basic subscriptionapp\_focus\_service\_subscribe(focus\_handler); ``` **Complete Subscription** A complete subscription will notify the app with more detail about changes in focus using two handlers in an [`AppFocusHandlers`](https://developer.repebble.com/docs/c/Foundation/Event_Service/AppFocusService.md#AppFocusHandlers "AppFocusHandlers") object: - `.will_focus` - represents a change in focus that is _about_ to occur, such as the start of a transition animation to or from a modal window. `will_focus` will be `true` if the app will be in focus at the end of the transition. - `.did_focus` - represents the end of a transition. `did_focus` will be `true` if the app is now completely in focus and the animation has finished. ``` void will\_focus\_handler(bool will\_focus) {APP\_LOG(APP\_LOG\_LEVEL\_INFO, "Will %s focus", will\_focus ? "gain" : "lose");}void did\_focus\_handler(bool did\_focus) {APP\_LOG(APP\_LOG\_LEVEL\_INFO, "%s focus", did\_focus ? "Gained" : "Lost");} ``` ``` // Subscribe to both types of eventsapp\_focus\_service\_subscribe\_handlers((AppFocusHandlers) {.will\_focus = will\_focus\_handler,.did\_focus = did\_focus\_handler}); ``` ### Alarm Service The [`AlarmService`](https://developer.repebble.com/docs/c/Foundation/Event_Service/AlarmService.md "AlarmService") has no handler. [`alarm_service_peek_next()`](https://developer.repebble.com/docs/c/Foundation/Event_Service/AlarmService.md#alarm_service_peek_next "alarm\_service\_peek\_next") reports whether at least one enabled alarm is scheduled and writes the UTC time it fires, which is enough for a watchface to show an alarm indicator: ``` time\_t next\_alarm;if (alarm\_service\_peek\_next(&next\_alarm)) {struct tm \*t = localtime(&next\_alarm);strftime(s\_alarm\_buffer, sizeof(s\_alarm\_buffer), "%H:%M", t);text\_layer\_set\_text(s\_alarm\_layer, s\_alarm\_buffer);} else {text\_layer\_set\_text(s\_alarm\_layer, "");} ``` Alarms cannot be created or changed from an app. The function is available on Pebble 2 Duo, Pebble Time 2 and Pebble Round 2; on older platforms it compiles to a stub that returns `false`. ### Compass Service The [`CompassService`](https://developer.repebble.com/docs/c/Foundation/Event_Service/CompassService.md "CompassService") provides access to regular updates about the watch's magnetic compass heading, if it is calibrated. See the [_Compass_](https://developer.repebble.com/guides/events-and-services/compass.md) guide for more information. ``` static void compass\_heading\_handler(CompassHeadingData heading\_data) {// Is the compass calibrated?if(heading\_data.compass\_status == CompassStatusCalibrated) {APP\_LOG(APP\_LOG\_LEVEL\_INFO, "Calibrated! Heading is %ld",TRIGANGLE\_TO\_DEG(heading\_data.magnetic\_heading));}} ``` ``` // Subscribe to compass heading updatescompass\_service\_subscribe(compass\_heading\_handler); ``` ``` // Peek the compass heading dataCompassHeadingData data;compass\_service\_peek(&data); ``` --- # Events and Services Source: https://developer.repebble.com/guides/events-and-services/ All Pebble watches contain a collection of sensors than can be used as input devices for apps. Available sensors include four buttons, an accelerometer, and a magnetometer (accessible via the [`CompassService`](https://developer.repebble.com/docs/c/Foundation/Event_Service/CompassService.md "CompassService") API). In addition, the Basalt and Chalk platforms also include a microphone (accessible via the [`Dictation`](https://developer.repebble.com/docs/c/Foundation/Dictation.md "Dictation") API) and access to Pebble Health data sets. Read [_Hardware Information_](https://developer.repebble.com/guides/tools-and-resources/hardware-information.md) for more information on sensor availability per platform. While providing more interactivity, excessive regular use of these sensors will stop the watch's CPU from sleeping and result in faster battery drain, so use them sparingly. An alternative to constantly reading accelerometer data is to obtain data in batches, allowing sleeping periods in between. Read [_Conserving Battery Life_](https://developer.repebble.com/guides/best-practices/conserving-battery-life.md) for more information. ## Contents - [**Accelerometer**](https://developer.repebble.com/guides/events-and-services/accelerometer.md) - How to use data and simple tap gestures from the onboard accelerometer. - [**Background Worker**](https://developer.repebble.com/guides/events-and-services/background-worker.md) - Using the Background Worker to do work in the background, such as activity tracking. - [**Backlight**](https://developer.repebble.com/guides/events-and-services/light.md) - How to trigger the backlight, force it on, and - on hardware with an RGB backlight - tint it. - [**Buttons**](https://developer.repebble.com/guides/events-and-services/buttons.md) - How to react to button presses in your app. - [**Compass**](https://developer.repebble.com/guides/events-and-services/compass.md) - How to use data from the Compass API to determine direction. - [**Dictation**](https://developer.repebble.com/guides/events-and-services/dictation.md) - How to use the Dictation API to get voice-to-text input in watchapps. - [**Event Services**](https://developer.repebble.com/guides/events-and-services/events.md) - How to use the various asynchronous event services to power app features. - [**Heart Rate Monitor**](https://developer.repebble.com/guides/events-and-services/hrm.md) - Information on using the HealthService API to obtain information from the Heart Rate Monitor. - [**Pebble Health**](https://developer.repebble.com/guides/events-and-services/health.md) - Information on using the HealthService API to incorporate multiple types of health data into your apps. - [**Persistent Storage**](https://developer.repebble.com/guides/events-and-services/persistent-storage.md) - Using persistent storage to improve your app's UX. - [**Speaker**](https://developer.repebble.com/guides/events-and-services/speaker.md) - How to play tones, melodies, polyphonic tracks, and PCM streams through the speaker. - [**Touch**](https://developer.repebble.com/guides/events-and-services/touch.md) - How to subscribe to touch events on platforms with a touchscreen. - [**Wakeups**](https://developer.repebble.com/guides/events-and-services/wakeups.md) - Using the Wakeup API to launch an app at some future time. --- # Heart Rate Monitor Source: https://developer.repebble.com/guides/events-and-services/hrm/ The Pebble Time 2 and Pebble 2 (excluding SE model) [_devices_](https://developer.repebble.com/guides/tools-and-resources/hardware-information.md) include a heart rate monitor. This guide will demonstrate how to use the [`HealthService`](https://developer.repebble.com/docs/c/Foundation/Event_Service/HealthService.md "HealthService") API to retrieve information about the user's current, and historical heart rates. See the platform support table in [_Hardware Information_](https://developer.repebble.com/guides/tools-and-resources/hardware-information.md#platform-support) for which watches have a heart rate monitor. If you aren't already familiar with the [`HealthService`](https://developer.repebble.com/docs/c/Foundation/Event_Service/HealthService.md "HealthService"), we recommended that you read the [_Health guide_](https://developer.repebble.com/guides/events-and-services/health.md) before proceeding. ## Enable Health Data Before your application is able to access the heart rate information, you will need to add `heath` to the `capabilities` array in your applications `package.json` file. ``` {..."pebble": {..."capabilities": ["health"],...}} ``` ## Data Quality Heart rate sensors aren't perfect, and their readings can be affected by improper positioning, environmental factors and excessive movement. The raw data from the HRM sensor contains a metric to indicate the quality of the readings it receives. The HRM API provides a raw BPM reading ([`HealthMetricHeartRateRawBPM`](https://developer.repebble.com/docs/c/Foundation/Event_Service/HealthService.md#HealthMetricHeartRateRawBPM "HealthMetricHeartRateRawBPM")) and a filtered reading ([`HealthMetricHeartRateBPM`](https://developer.repebble.com/docs/c/Foundation/Event_Service/HealthService.md#HealthMetricHeartRateBPM "HealthMetricHeartRateBPM")). This filtered value minimizes the effect of hand movement and improper sensor placement, by removing the bad quality readings. This filtered data makes it easy for developers to integrate in their applications, without needing to filter the data themselves. ## Obtaining the Current Heart Rate To obtain the current heart rate, you should first check whether the [`HealthMetricHeartRateBPM`](https://developer.repebble.com/docs/c/Foundation/Event_Service/HealthService.md#HealthMetricHeartRateBPM "HealthMetricHeartRateBPM") is available by using the [`health_service_metric_accessible`](https://developer.repebble.com/docs/c/Foundation/Event_Service/HealthService.md#health_service_metric_accessible "health\_service\_metric\_accessible") method. Then you can obtain the current heart rate using the [`health_service_peek_current_value`](https://developer.repebble.com/docs/c/Foundation/Event_Service/HealthService.md#health_service_peek_current_value "health\_service\_peek\_current\_value") method: ``` HealthServiceAccessibilityMask hr = health\_service\_metric\_accessible(HealthMetricHeartRateBPM, time(NULL), time(NULL));if (hr & HealthServiceAccessibilityMaskAvailable) {HealthValue val = health\_service\_peek\_current\_value(HealthMetricHeartRateBPM);if(val \> 0) {// Display HRM valuestatic char s\_hrm\_buffer[8];snprintf(s\_hrm\_buffer, sizeof(s\_hrm\_buffer), "%lu BPM", (uint32\_t)val);text\_layer\_set\_text(s\_hrm\_layer, s\_hrm\_buffer);}} ``` > **Note** this value is averaged from readings taken over the past minute, but due to the [sampling rate](#heart-rate-sample-periods) and our data filters, this value could be several minutes old. Use `HealthMetricHeartRateRawBPM` for the raw, unfiltered value. ## Subscribing to Heart Rate Updates The user's heart rate can also be monitored via an event subscription, in a similar way to the other health metrics. If you wanted your watchface to update the displayed heart rate every time the HRM takes a new reading, you could use the [`health_service_events_subscribe`](https://developer.repebble.com/docs/c/Foundation/Event_Service/HealthService.md#health_service_events_subscribe "health\_service\_events\_subscribe") method. ``` static void prv\_on\_health\_data(HealthEventType type, void \*context) {// If the update was from the Heart Rate Monitor, query itif (type == HealthEventHeartRateUpdate) {HealthValue value = health\_service\_peek\_current\_value(HealthMetricHeartRateBPM);// Display the heart rate}}static void prv\_init(void) {// Subscribe to health event handlerhealth\_service\_events\_subscribe(prv\_on\_health\_data, NULL);// ...} ``` > **Note** The frequency of these updates does not directly correlate to the sensor sampling rate. ## Heart Rate Sample Periods The default sample period is 10 minutes, but the system automatically controls the HRM sample rate based on the level of user activity. It increases the sampling rate during intense activity and reduces it again during inactivity. This aims to provide the optimal battery usage. ### Battery Considerations Like all active sensors, accelerometer, backlight etc, the HRM sensor will have a negative affect on battery life. It's important to consider this when using the APIs within your application. By default the system will automatically control the heart rate sampling period for the optimal balance between update frequency and battery usage. In addition, the APIs have been designed to allow developers to retrieve values for the most common use cases with minimal impact on battery life. ### Altering the Sampling Period Developers can request a specific sampling rate using the [`health_service_set_heart_rate_sample_period`](https://developer.repebble.com/docs/c/Foundation/Event_Service/HealthService.md#health_service_set_heart_rate_sample_period "health\_service\_set\_heart\_rate\_sample\_period") method. The system will use this value as a suggestion, but does not guarantee that value will be used. The actual sampling period may be greater or less due to other apps that require input from the sensor, or data quality issues. The shortest period you can currently specify is `1` second, and the longest period you can specify is `600` seconds (10 minutes). In this example, we will sample the heart rate monitor every second: ``` // Set the heart rate monitor to sample every secondbool success = health\_service\_set\_heart\_rate\_sample\_period(1); ``` > **Note** This does not mean that you can peek the current value every second, only that the sensor will capture more samples. ### Resetting the Sampling Period Developers **must** reset the heart rate sampling period when their application exits. Failing to do so may result in the heart rate monitor continuing at the increased rate for a period of time, even after your application closes. This is fundamentally different to other Pebble sensors and was designed so that applications which a reliant upon high sampling rates can be temporarily interupted for notifications, or music, without affecting the sensor data. ``` // Reset the heart rate sampling period to automatichealth\_service\_set\_heart\_rate\_sample\_period(0); ``` ## Heart Rate Variability Since firmware 4.32, watches whose heart rate sensor supports it (currently the Pebble Time 2) can also report heart rate variability (HRV) as peak-to-peak intervals (PPI). Unlike the BPM metrics, HRV data is only collected while an app explicitly requests it. To start collecting HRV readings, request an HRV sample period with `health_service_set_hrv_sample_period`. While any app holds an HRV sample period, the sensor collects HRV alongside heart rate, and a [`HealthEventHRVUpdate`](https://developer.repebble.com/docs/c/Foundation/Event_Service/HealthService.md#HealthEventHRVUpdate "HealthEventHRVUpdate") event is delivered to health service subscribers each time a new reading is available. Read the most recent interval with `health_service_peek_hrv_ppi_ms`, which returns the PPI in milliseconds, or `0` if no reading is available yet: ``` static void prv\_on\_health\_data(HealthEventType type, void \*context) {if (type == HealthEventHRVUpdate) {uint16\_t ppi = health\_service\_peek\_hrv\_ppi\_ms();if (ppi \> 0) {// Display the peak-to-peak interval}}}static void prv\_init(void) {// Subscribe to health event handlerhealth\_service\_events\_subscribe(prv\_on\_health\_data, NULL);// Collect an HRV reading every 10 secondshealth\_service\_set\_hrv\_sample\_period(10);} ``` The HRV and heart rate sample periods are independent requests that share the app's single sensor subscription: both may be active at once, the sensor is driven at the shorter of the two periods, and setting one period to `0` clears only that request. As with the heart rate sample period, reset the HRV sample period when your application no longer needs it: ``` // Stop requesting HRV samplinghealth\_service\_set\_hrv\_sample\_period(0); ``` ## Obtaining Historical Data If your application is using heart rate information, it may also want to obtain historical data to compare it against. In this section we'll look at how you can use the `health_service_aggregate` functions to obtain relevant historic data. Before requesting historic/aggregate data for a specific time period, you should ensure that it is available using the [`health_service_metric_accessible`](https://developer.repebble.com/docs/c/Foundation/Event_Service/HealthService.md#health_service_metric_accessible "health\_service\_metric\_accessible") method. Then we'll use the [`health_service_aggregate_averaged`](https://developer.repebble.com/docs/c/Foundation/Event_Service/HealthService.md#health_service_aggregate_averaged "health\_service\_aggregate\_averaged") method to obtain the average daily heart rate over the last 7 days. ``` // Obtain history for last 7 daystime\_t end\_time = time(NULL);time\_t start\_time = end\_time - (7 \* SECONDS\_PER\_DAY);HealthServiceAccessibilityMask hr = health\_service\_metric\_accessible(HealthMetricHeartRateBPM, start\_time, end\_time);if (hr & HealthServiceAccessibilityMaskAvailable) {uint32\_t weekly\_avg\_hr = health\_service\_aggregate\_averaged(HealthMetricHeartRateBPM,start\_time, end\_time,HealthAggregationAvg, HealthServiceTimeScopeDaily);} ``` You can also query the average `min` and `max` heart rates, but only within the past two hours (maximum). This limitation is due to very limited storage capacity on the device, but the implementation may change in the future. ``` // Obtain history for last 1 hourtime\_t end\_time = time(NULL);time\_t start\_time = end\_time - SECONDS\_PER\_HOUR;HealthServiceAccessibilityMask hr = health\_service\_metric\_accessible(HealthMetricHeartRateBPM, start\_time, end\_time);if (hr & HealthServiceAccessibilityMaskAvailable) {uint32\_t min\_hr = health\_service\_aggregate\_averaged(HealthMetricHeartRateBPM,start\_time, end\_time,HealthAggregationMin, HealthServiceTimeScopeOnce);uint32\_t max\_hr = health\_service\_aggregate\_averaged(HealthMetricHeartRateBPM,start\_time, end\_time,HealthAggregationMax, HealthServiceTimeScopeOnce);} ``` ## Read Per-Minute History The [`HealthMinuteData`](https://developer.repebble.com/docs/c/Foundation/Event_Service/HealthService.md#HealthMinuteData "HealthMinuteData") structure contains multiple types of activity-related data that are recorded in a minute-by-minute fashion. Although this structure now contains HRM readings, it does not contain information about the quality of those readings. > **Note** Please refer to the [_Health Guide_](https://developer.repebble.com/guides/events-and-services/health.md#read-per-minute-history) for futher information. ## Next Steps This guide covered the basics of how to interact with realtime and historic heart information. We encourage you to further explore the [`HealthService`](https://developer.repebble.com/docs/c/Foundation/Event_Service/HealthService.md "HealthService") documentation, and integrate it into your next project. --- # Pebble Health Source: https://developer.repebble.com/guides/events-and-services/health/ **PLATFORM NOTICE** This guide does not apply to apps built to run on the Aplite platform (Pebble Classic and Pebble Steel). [Pebble Health](https://blog.getpebble.com/2015/12/15/health/) provides builtin health data tracking to allow users to improve their activity and sleep habits. With SDK 3.9, the [`HealthService`](https://developer.repebble.com/docs/c/Foundation/Event_Service/HealthService.md "HealthService") API opens this data up to developers to include and use within their apps. For example, a watchface could display a brief summary of the user's activity for the day. ## API Availability In order to use the [`HealthService`](https://developer.repebble.com/docs/c/Foundation/Event_Service/HealthService.md "HealthService") (and indeed Pebble Health), the user must enable the 'Pebble Health' app in the 'Apps/Timeline' view of the official Pebble mobile app. If this is not enabled health data will not be available to apps, and API calls will return values to reflect this. In addition, any app using the [`HealthService`](https://developer.repebble.com/docs/c/Foundation/Event_Service/HealthService.md "HealthService") API must declare the 'health' capability in order to be accepted by the [developer dashboard](https://developer.repebble.com/dashboard). This can be done in `package.json` in the SDK: ``` "capabilities": ["health"] ``` Since Pebble Health is not available on the Aplite platform, developers should check the API return values and hence the lack of [`HealthService`](https://developer.repebble.com/docs/c/Foundation/Event_Service/HealthService.md "HealthService") on that platform gracefully. In addition, the `PBL_HEALTH` define and `PBL_IF_HEALTH_ELSE()` macro can be used to selectively omit affected code. ## Available Metrics The [`HealthMetric`](https://developer.repebble.com/docs/c/Foundation/Event_Service/HealthService.md#HealthMetric "HealthMetric") `enum` lists the types of data (or 'metrics') that can be read using the API. These are described below: | Metric | Description | | --- | --- | | `HealthMetricStepCount` | The user's step count. | | `HealthMetricActiveSeconds` | Duration of time the user was considered 'active'. | | `HealthMetricWalkedDistanceMeters` | Estimation of the distance travelled in meters. | | `HealthMetricSleepSeconds` | Duration of time the user was considered asleep. | | `HealthMetricSleepRestfulSeconds` | Duration of time the user was considered in deep restful sleep. | | `HealthMetricRestingKCalories` | The number of kcal (thousand calories) burned due to resting metabolism. | | `HealthMetricActiveKCalories` | The number of kcal (thousand calories) burned due to activity. | | `HealthMetricHeartRateBPM` | The heart rate, in beats per minute. | ## Subscribing to HealthService Events Like other Event Services, an app can subscribe a handler function to receive a callback when new health data is available. This is useful for showing near-realtime activity updates. The handler must be a suitable implementation of [`HealthEventHandler`](https://developer.repebble.com/docs/c/Foundation/Event_Service/HealthService.md#HealthEventHandler "HealthEventHandler"). The `event` parameter describes the type of each update, and is one of the following from the [`HealthEventType`](https://developer.repebble.com/docs/c/Foundation/Event_Service/HealthService.md#HealthEventType "HealthEventType") `enum`: | Event Type | Value | Description | | --- | --- | --- | | `HealthEventSignificantUpdate` | `0` | All data is considered as outdated, apps should re-read all health data. This can happen on a change of the day or in other cases that significantly change the underlying data. | | `HealthEventMovementUpdate` | `1` | Recent values around `HealthMetricStepCount`, `HealthMetricActiveSeconds`, `HealthMetricWalkedDistanceMeters`, and `HealthActivityMask` changed. | | `HealthEventSleepUpdate` | `2` | Recent values around `HealthMetricSleepSeconds`, `HealthMetricSleepRestfulSeconds`, `HealthActivitySleep`, and `HealthActivityRestfulSleep` changed. | | `HealthEventHeartRateUpdate` | `4` | The value of `HealthMetricHeartRateBPM` has changed. | | `HealthEventHRVUpdate` | `5` | A new heart rate variability peak-to-peak interval reading is available. See the [_Heart Rate Monitor guide_](https://developer.repebble.com/guides/events-and-services/hrm.md#heart-rate-variability). | A simple example handler is shown below, which outputs to app logs the type of event that fired the callback: ``` static void health\_handler(HealthEventType event, void \*context) {// Which type of event occurred?switch(event) {case HealthEventSignificantUpdate:APP\_LOG(APP\_LOG\_LEVEL\_INFO,"New HealthService HealthEventSignificantUpdate event");break;case HealthEventMovementUpdate:APP\_LOG(APP\_LOG\_LEVEL\_INFO,"New HealthService HealthEventMovementUpdate event");break;case HealthEventSleepUpdate:APP\_LOG(APP\_LOG\_LEVEL\_INFO,"New HealthService HealthEventSleepUpdate event");break;case HealthEventHeartRateUpdate:APP\_LOG(APP\_LOG\_LEVEL\_INFO,"New HealthService HealthEventHeartRateUpdate event");break;}} ``` The subscription is then registered in the usual way, optionally providing a `context` parameter that is relayed to each event callback. The return value should be used to determine whether the subscription was successful: ``` #if defined(PBL\_HEALTH)// Attempt to subscribeif(!health\_service\_events\_subscribe(health\_handler, NULL)) {APP\_LOG(APP\_LOG\_LEVEL\_ERROR, "Health not available!");}#elseAPP\_LOG(APP\_LOG\_LEVEL\_ERROR, "Health not available!");#endif ``` ## Reading Health Data Health data is collected in the background as part of Pebble Health regardless of the state of the app using the [`HealthService`](https://developer.repebble.com/docs/c/Foundation/Event_Service/HealthService.md "HealthService") API, and is available to apps through various [`HealthService`](https://developer.repebble.com/docs/c/Foundation/Event_Service/HealthService.md "HealthService") API functions. Before reading any health data, it is recommended to check that data is available for the desired time range, if applicable. In addition to the [`HealthServiceAccessibilityMask`](https://developer.repebble.com/docs/c/Foundation/Event_Service/HealthService.md#HealthServiceAccessibilityMask "HealthServiceAccessibilityMask") value, health-related code can be conditionally compiled using `PBL_HEALTH`. For example, to check whether any data is available for a given time range: ``` #if defined(PBL\_HEALTH)// Use the step count metricHealthMetric metric = HealthMetricStepCount;// Create timestamps for midnight (the start time) and now (the end time)time\_t start = time\_start\_of\_today();time\_t end = time(NULL);// Check step data is availableHealthServiceAccessibilityMask mask = health\_service\_metric\_accessible(metric,start, end);bool any\_data\_available = mask & HealthServiceAccessibilityMaskAvailable;#else// Health data is not available herebool any\_data\_available = false;#endif ``` Most applications will want to read the sum of a metric for the current day's activity. This is the simplest method for accessing summaries of users' health data, and is shown in the example below: ``` HealthMetric metric = HealthMetricStepCount;time\_t start = time\_start\_of\_today();time\_t end = time(NULL);// Check the metric has data available for todayHealthServiceAccessibilityMask mask = health\_service\_metric\_accessible(metric,start, end);if(mask & HealthServiceAccessibilityMaskAvailable) {// Data is available!APP\_LOG(APP\_LOG\_LEVEL\_INFO, "Steps today: %d",(int)health\_service\_sum\_today(metric));} else {// No data recorded yet todayAPP\_LOG(APP\_LOG\_LEVEL\_ERROR, "Data unavailable!");} ``` For more specific data queries, the API also allows developers to request data records and sums of metrics from a specific time range. If data is available, it can be read as a sum of all values recorded between that time range. You can use the convenience constants from [`Time`](https://developer.repebble.com/docs/c/Standard_C/Time.md "Time"), such as [`SECONDS_PER_HOUR`](https://developer.repebble.com/docs/c/Standard_C/Time.md#SECONDS_PER_HOUR "SECONDS\_PER\_HOUR") to adjust a timestamp relative to the current moment returned by [`time()`](https://developer.repebble.com/docs/c/Standard_C/Time.md "Time"). > Note: The value returned will be an average since midnight, weighted for the length of the specified time range. This may change in the future. An example of this process is shown below: ``` // Make a timestamp for nowtime\_t end = time(NULL);// Make a timestamp for the last hour's worth of datatime\_t start = end - SECONDS\_PER\_HOUR;// Check data is availableHealthServiceAccessibilityMask result =health\_service\_metric\_accessible(HealthMetricStepCount, start, end);if(result & HealthServiceAccessibilityMaskAvailable) {// Data is available! Read itHealthValue steps = health\_service\_sum(HealthMetricStepCount, start, end);APP\_LOG(APP\_LOG\_LEVEL\_INFO, "Steps in the last hour: %d", (int)steps);} else {APP\_LOG(APP\_LOG\_LEVEL\_ERROR, "No data available!");} ``` ## Representing Health Data Depending on the locale of the user, the conventional measurement system used to represent distances may vary between metric and imperial. For this reason it is recommended to query the user's preferred [`MeasurementSystem`](https://developer.repebble.com/docs/c/Foundation/Event_Service/HealthService.md#MeasurementSystem "MeasurementSystem") before formatting distance data from the [`HealthService`](https://developer.repebble.com/docs/c/Foundation/Event_Service/HealthService.md "HealthService"): > Note: This API is currently only meaningful when querying the [`HealthMetricWalkedDistanceMeters`](https://developer.repebble.com/docs/c/Foundation/Event_Service/HealthService.md#HealthMetricWalkedDistanceMeters "HealthMetricWalkedDistanceMeters") metric. [`MeasurementSystemUnknown`](https://developer.repebble.com/docs/c/Foundation/Event_Service/HealthService.md#MeasurementSystemUnknown "MeasurementSystemUnknown") will be returned for all other queries. ``` const HealthMetric metric = HealthMetricWalkedDistanceMeters;const HealthValue distance = health\_service\_sum\_today(metric);// Get the preferred measurement systemMeasurementSystem system = health\_service\_get\_measurement\_system\_for\_display(metric);// Format accordinglystatic char s\_buffer[32];switch(system) {case MeasurementSystemMetric:snprintf(s\_buffer, sizeof(s\_buffer), "Walked %d meters", (int)distance);break;case MeasurementSystemImperial: {// Convert to imperial firstint feet = (int)((float)distance \* 3.28F);snprintf(s\_buffer, sizeof(s\_buffer), "Walked %d feet", (int)feet);} break;case MeasurementSystemUnknown:default:APP\_LOG(APP\_LOG\_LEVEL\_INFO, "MeasurementSystem unknown or does not apply");}// Display to user in correct unitstext\_layer\_set\_text(s\_some\_layer, s\_buffer); ``` ## Obtaining Averaged Data The [`HealthService`](https://developer.repebble.com/docs/c/Foundation/Event_Service/HealthService.md "HealthService") also allows developers to read average values of a particular [`HealthMetric`](https://developer.repebble.com/docs/c/Foundation/Event_Service/HealthService.md#HealthMetric "HealthMetric") with varying degrees of scope. This is useful for apps that wish to display an average value (e.g.: as a goal for the user) alongside a summed value. In this context, the `start` and `end` parameters specify the time period to be used for the daily average calculation. For example, a start time of midnight and an end time ten hours later will return the average value for the specified metric measured until 10 AM on average across the days specified by the scope. The [`HealthServiceTimeScope`](https://developer.repebble.com/docs/c/Foundation/Event_Service/HealthService.md#HealthServiceTimeScope "HealthServiceTimeScope") specified when querying for averaged data over a given time range determines how the average is calculated, as detailed in the table below: | Scope Type | Description | | --- | --- | | `HealthServiceTimeScopeOnce` | No average computed. The result is the same as calling [`health_service_sum()`](https://developer.repebble.com/docs/c/Foundation/Event_Service/HealthService.md#health_service_sum "health\_service\_sum"). | | `HealthServiceTimeScopeWeekly` | Compute average using the same day from each week (up to four weeks). For example, every Monday if the provided time range falls on a Monday. | | `HealthServiceTimeScopeDailyWeekdayOrWeekend` | Compute average using either weekdays (Monday to Friday) or weekends (Saturday and Sunday), depending on which day the provided time range falls. | | `HealthServiceTimeScopeDaily` | Compute average across all days of the week. | > Note: If the difference between the start and end times is greater than one day, an average will be returned that takes both days into account. Similarly, if the time range crosses between scopes (such as including weekend days and weekdays with [`HealthServiceTimeScopeDailyWeekdayOrWeekend`](https://developer.repebble.com/docs/c/Foundation/Event_Service/HealthService.md#HealthServiceTimeScopeDailyWeekdayOrWeekend "HealthServiceTimeScopeDailyWeekdayOrWeekend")), the start time will be used to determine which days are used. Reading averaged data values works in a similar way to reading sums. The example below shows how to read an average step count across all days of the week for a given time range: ``` // Define query parametersconst HealthMetric metric = HealthMetricStepCount;const HealthServiceTimeScope scope = HealthServiceTimeScopeDaily;// Use the average daily value from midnight to the current timeconst time\_t start = time\_start\_of\_today();const time\_t end = time(NULL);// Check that an averaged value is accessibleHealthServiceAccessibilityMask mask =health\_service\_metric\_averaged\_accessible(metric, start, end, scope);if(mask & HealthServiceAccessibilityMaskAvailable) {// Average is available, read itHealthValue average = health\_service\_sum\_averaged(metric, start, end, scope);APP\_LOG(APP\_LOG\_LEVEL\_INFO, "Average step count: %d steps", (int)average);} ``` ## Detecting Activities It is possible to detect when the user is sleeping using a [`HealthActivityMask`](https://developer.repebble.com/docs/c/Foundation/Event_Service/HealthService.md#HealthActivityMask "HealthActivityMask") value. A useful application of this information could be to disable a watchface's animations or tick at a reduced rate once the user is asleep. This is done by checking certain bits of the returned value: ``` // Get an activities maskHealthActivityMask activities = health\_service\_peek\_current\_activities();// Determine which bits are set, and hence which activity is activeif(activities & HealthActivitySleep) {APP\_LOG(APP\_LOG\_LEVEL\_INFO, "The user is sleeping.");} else if(activities & HealthActivityRestfulSleep) {APP\_LOG(APP\_LOG\_LEVEL\_INFO, "The user is sleeping peacefully.");} else {APP\_LOG(APP\_LOG\_LEVEL\_INFO, "The user is not currently sleeping.");} ``` ## Read Per-Minute History The [`HealthMinuteData`](https://developer.repebble.com/docs/c/Foundation/Event_Service/HealthService.md#HealthMinuteData "HealthMinuteData") structure contains multiple types of activity-related data that are recorded in a minute-by-minute fashion. This style of data access is best suited to those applications requiring more granular detail (such as creating a new fitness algorithm). Up to seven days worth of data is available with this API. > See [_Notes on Minute-level Data_](#notes-on-minute-level-data) below for more information on minute-level data. The data items contained in the [`HealthMinuteData`](https://developer.repebble.com/docs/c/Foundation/Event_Service/HealthService.md#HealthMinuteData "HealthMinuteData") structure are summarized below: | Item | Type | Description | | --- | --- | --- | | `steps` | `uint8_t` | Number of steps taken in this minute. | | `orientation` | `uint8_t` | Quantized average orientation, encoding the x-y plane (the "yaw") in the lower 4 bits (360 degrees linearly mapped to 1 of 16 values) and the z axis (the "pitch") in the upper 4 bits. | | `vmc` | `uint16_t` | Vector Magnitude Counts (VMC). This is a measure of the total amount of movement seen by the watch. More vigorous movement yields higher VMC values. | | `is_invalid` | `bool` | `true` if the item doesn't represent actual data, and should be ignored. | | `heart_rate_bpm` | `uint8_t` | Heart rate in beats per minute (if available). | These data items can be obtained in the following manner, similar to obtaining a sum. ``` // Create an array to store dataconst uint32\_t max\_records = 60;HealthMinuteData \*minute\_data = (HealthMinuteData\*)malloc(max\_records \* sizeof(HealthMinuteData));// Make a timestamp for 15 minutes ago and an hour before thattime\_t end = time(NULL) - (15 \* SECONDS\_PER\_MINUTE);time\_t start = end - SECONDS\_PER\_HOUR;// Obtain the minute-by-minute recordsuint32\_t num\_records = health\_service\_get\_minute\_history(minute\_data,max\_records, &start, &end);APP\_LOG(APP\_LOG\_LEVEL\_INFO, "num\_records: %d", (int)num\_records);// Print the number of steps for each minutefor(uint32\_t i = 0; i \< num\_records; i++) {APP\_LOG(APP\_LOG\_LEVEL\_INFO, "Item %d steps: %d", (int)i,(int)minute\_data[i].steps);} ``` Don't forget to free the array once the data is finished with: ``` // Free the arrayfree(minute\_data); ``` ### Notes on Minute-level Data Missing minute-level records can occur if the watch is reset, goes into low power (watch-only) mode due to critically low battery, or Pebble Health is disabled during the time period requested. [`health_service_get_minute_history()`](https://developer.repebble.com/docs/c/Foundation/Event_Service/HealthService.md#health_service_get_minute_history "health\_service\_get\_minute\_history") will return as many **consecutive** minute-level records that are available after the provided `start` timestamp, skipping any missing records until one is found. This API behavior enables one to easily continue reading data after a previous query encountered a missing minute. If there are some minutes with missing data, the API will return all available records up to the last available minute, and no further. Conversely, records returned will begin with the first available record after the provided `start` timestamp, skipping any missing records until one is found. This can be used to continue reading data after a previous query encountered a missing minute. The code snippet below shows an example function that packs a provided [`HealthMinuteData`](https://developer.repebble.com/docs/c/Foundation/Event_Service/HealthService.md#HealthMinuteData "HealthMinuteData") array with all available values in a time range, up to an arbitrary maximum number. Any missing minutes are collapsed, so that as much data can be returned as is possible for the allocated array size and time range requested. > This example shows querying up to 60 records. More can be obtained, but this increases the heap allocation required as well as the time taken to process the query. ``` static uint32\_t get\_available\_records(HealthMinuteData \*array, time\_t query\_start,time\_t query\_end, uint32\_t max\_records) {time\_t next\_start = query\_start;time\_t next\_end = query\_end;uint32\_t num\_records\_found = 0;// Find more records until no more are returnedwhile (num\_records\_found \< max\_records) {int ask\_num\_records = max\_records - num\_records\_found;uint32\_t ret\_val = health\_service\_get\_minute\_history(&array[num\_records\_found],ask\_num\_records, &next\_start, &next\_end);if (ret\_val == 0) {// a 0 return value means no more data is availablereturn num\_records\_found;}num\_records\_found += ret\_val;next\_start = next\_end;next\_end = query\_end;}return num\_records\_found;}static void print\_last\_hours\_steps() {// Query for the last hour, max 60 minute-level records// (except the last 15 minutes)const time\_t query\_end = time(NULL) - (15 \* SECONDS\_PER\_MINUTE);const time\_t query\_start = query\_end - SECONDS\_PER\_HOUR;const uint32\_t max\_records = (query\_end - query\_start) / SECONDS\_PER\_MINUTE;HealthMinuteData \*data =(HealthMinuteData\*)malloc(max\_records \* sizeof(HealthMinuteData));// Populate the arraymax\_records = get\_available\_records(data, query\_start, query\_end, max\_records);// Print the resultsfor(uint32\_t i = 0; i \< max\_records; i++) {if(!data[i].is\_invalid) {APP\_LOG(APP\_LOG\_LEVEL\_INFO, "Record %d contains %d steps.", (int)i,(int)data[i].steps);} else {APP\_LOG(APP\_LOG\_LEVEL\_INFO, "Record %d was not valid.", (int)i);}}free(data);} ``` --- # Persistent Storage Source: https://developer.repebble.com/guides/events-and-services/persistent-storage/ Developers can use the [`Storage`](https://developer.repebble.com/docs/c/Foundation/Storage.md "Storage") API to persist multiple types of data between app launches, enabling apps to remember information previously entered by the user. A common use-case of this API is to enable the app to remember configuration data chosen in an app's configuration page, removing the tedious need to enter the information on the phone every time the watchapp is launched. Other use cases include to-to lists, stat trackers, game highscores etc. Read [_App Configuration_](https://developer.repebble.com/guides/user-interfaces/app-configuration.md) for more information on implementing an app configuration page. ## Persistent Storage Model Every app is allocated a fixed amount of persistent storage space and can write values to storage using a key, similar to [`AppMessage`](https://developer.repebble.com/docs/c/Foundation/AppMessage.md "AppMessage") dictionaries or the web `localStorage` API. To recall values, the app simply queries the API using the associated key. Keys are specified in the `uint32_t` type, and each value can have a size up to [`PERSIST_DATA_MAX_LENGTH`](https://developer.repebble.com/docs/c/Foundation/Storage.md#PERSIST_DATA_MAX_LENGTH "PERSIST\_DATA\_MAX\_LENGTH") (currently 256 bytes). The total space depends on the firmware. Call [`persist_get_max_size()`](https://developer.repebble.com/docs/c/Foundation/Storage.md#persist_get_max_size "persist\_get\_max\_size") to read the limit in bytes at runtime rather than assuming a value: ``` size\_t max\_bytes = persist\_get\_max\_size(); ``` On Pebble 2 Duo, Pebble Time 2 and Pebble Round 2 the limit is 1 MB per app. The storage file starts at 4 kB and grows as values are written. On aplite, basalt, chalk and diorite the SDK compiles [`persist_get_max_size()`](https://developer.repebble.com/docs/c/Foundation/Storage.md#persist_get_max_size "persist\_get\_max\_size") to the constant 4096, the 4 kB limit documented for those platforms. [`persist_get_max_size()`](https://developer.repebble.com/docs/c/Foundation/Storage.md#persist_get_max_size "persist\_get\_max\_size") was added in SDK 4.9.172; with an older SDK, assume 4 kB. When an app is updated the values saved using the [`Storage`](https://developer.repebble.com/docs/c/Foundation/Storage.md "Storage") API will be persisted, but if it is uninstalled they will be removed. Apps that make large use of the [`Storage`](https://developer.repebble.com/docs/c/Foundation/Storage.md "Storage") API may experience small pauses due to underlying housekeeping operations. Therefore it is recommended to read and write values when an app is launching or exiting, or during a time the user is waiting for some other action to complete. ## Types of Data Values can be stored as boolean, integer, string, or arbitrary data structure types. Before retrieving a value, the app should check that it has been previously persisted. If it has not, a default value should be chosen as appropriate. ``` uint32\_t key = 0;int num\_items = 0;if (persist\_exists(key)) {// Read persisted valuenum\_items = persist\_read\_int(key);} else {// Choose a default valuenum\_items = 10;// Remember the default value until the user chooses their own valuepersist\_write\_int(key, num\_items);} ``` The API provides a 'read' and 'write' function for each of these types, with builtin data types retrieved through assignment, and complex ones into a buffer provided by the app. Examples of each are shown below. ### Booleans ``` uint32\_t key = 0;bool large\_font\_size = true; ``` ``` // Write a boolean valuepersist\_write\_bool(key, large\_font\_size); ``` ``` // Read the boolean valuebool large\_font\_size = persist\_read\_bool(key); ``` ### Integers ``` uint32\_t key = 1;int highscore = 432; ``` ``` // Write an integerpersist\_write\_int(key, highscore); ``` ``` // Read the integer valueint highscore = persist\_read\_int(key); ``` ### Strings ``` uint32\_t key = 2;char \*string = "Remember this!"; ``` ``` // Write the stringpersist\_write\_string(key, string); ``` ``` // Read the stringchar buffer[32];persist\_read\_string(key, buffer, sizeof(buffer)); ``` ### Data Structures ``` typedef struct {int a;int b;} Data;uint32\_t key = 3;Data data = (Data) {.a = 32,.b = 45}; ``` ``` // Write the data structurepersist\_write\_data(key, &data, sizeof(Data)); ``` ``` // Read the data structurepersist\_read\_data(key, &data, sizeof(Data)); ``` > Note: If a persisted data structure's field layout changes between app versions, the data read may no longer be compatible (see below). ## Versioning Persisted Data As already mentioned, automatic app updates will persist data between app versions. However, if the format of persisted data changes in a new app version (or keys change), developers should version their storage scheme and correctly handle version changes appropriately. One way to do this is to use an extra persisted integer as the storage scheme's version number. If the scheme changes, simply update the version number and migrate existing data as required. If old data cannot be migrated it should be deleted and replaced with fresh data in the correct scheme from the user. An example is shown below: ``` const uint32\_t storage\_version\_key = 786;const int current\_storage\_version = 2; ``` ``` // Store the current storage scheme version numberpersist\_write\_int(storage\_version\_key, current\_storage\_version); ``` In this example, data stored in a key of `12` is now stored in a key of `13` due to a new key being inserted higher up the list of key values. ``` // The scheme has changed, increment the version numberconst int current\_storage\_version = 3; ``` ``` static void migrate\_storage\_data() {// Check the last storage scheme version the app usedint last\_storage\_version = persist\_read\_int(storage\_version\_key);if (last\_storage\_version == current\_storage\_version) {// No migration necessaryreturn;}// Migrate dataswitch(last\_storage\_version) {case 0:// ...break;case 1:// ...break;case 2: {uint32\_t old\_highscore\_key = 12;uint32\_t new\_highscore\_key = 13;// Migrate to scheme version 3int highscore = persist\_read\_int(old\_highscore\_key);persist\_write\_int(new\_highscore\_key, highscore);// Delete old datapersist\_delete(old\_highscore\_key);break;}// Migration is complete, store the current storage scheme version numberpersist\_write\_int(storage\_version\_key, current\_storage\_version);} ``` ## Alternative Method In addition to the [`Storage`](https://developer.repebble.com/docs/c/Foundation/Storage.md "Storage") API, data can also be persisted using the `localStorage` API in PebbleKit JS, and communicated with the watch over [`AppMessage`](https://developer.repebble.com/docs/c/Foundation/AppMessage.md "AppMessage") when the app is lanched. However, this method uses more power and fails if the watch is not connected to the phone. --- # Speaker Source: https://developer.repebble.com/guides/events-and-services/speaker/ On hardware platforms with a built-in speaker, the Speaker API gives apps four different ways to make sound (see the platform support table in [_Hardware Information_](https://developer.repebble.com/guides/tools-and-resources/hardware-information.md#platform-support) for which watches have a speaker): - A **one-shot tone** , for short beeps and confirmation sounds. - A **note sequence** , for monophonic melodies that the system synthesizes from a chosen waveform. - **Polyphonic tracks** , where up to four voices are mixed together, each optionally backed by a pitch-shifted PCM sample. - **PCM streaming** , for arbitrary audio that the app generates or decodes on the fly. All four paths share the same volume control, status reporting, and finished-callback mechanism, so an app can mix and match them as it needs to. ## Volume Every speaker call takes a volume argument in the range `0`–`100`. The global volume can also be changed at any time, including while audio is playing: ``` speaker\_set\_volume(60); ``` For note sequences, individual notes can override the global volume by setting their `velocity` field (`1`–`127`); a velocity of `0` means "use the global volume." ## Playing a One-Shot Tone The simplest path is `speaker_play_tone()`, which plays a single frequency with a chosen waveform for up to ten seconds. It's well suited to UI beeps, alarms, and confirmation sounds: ``` // Play a 440 Hz sine wave for 250 ms at 80% volumespeaker\_play\_tone(440, 250, 80, SpeakerWaveformSine); ``` The available waveforms are: | Waveform | Description | | --- | --- | | [`SpeakerWaveformSine`](https://developer.repebble.com/docs/c/User_Interface/Speaker.md#SpeakerWaveformSine "SpeakerWaveformSine") | Smooth sine wave; the gentlest option. | | [`SpeakerWaveformSquare`](https://developer.repebble.com/docs/c/User_Interface/Speaker.md#SpeakerWaveformSquare "SpeakerWaveformSquare") | Hollow, retro square wave. | | [`SpeakerWaveformTriangle`](https://developer.repebble.com/docs/c/User_Interface/Speaker.md#SpeakerWaveformTriangle "SpeakerWaveformTriangle") | Brighter than a sine, softer than a square. | | [`SpeakerWaveformSawtooth`](https://developer.repebble.com/docs/c/User_Interface/Speaker.md#SpeakerWaveformSawtooth "SpeakerWaveformSawtooth") | Buzzy, harmonically rich sawtooth. | `speaker_play_tone()` returns `true` if playback started, or `false` if the duration is out of range or the speaker is busy with a different request. ## Playing a Note Sequence For monophonic melodies, build an array of [`SpeakerNote`](https://developer.repebble.com/docs/c/User_Interface/Speaker.md#SpeakerNote "SpeakerNote") values and hand it to `speaker_play_notes()`. Each note carries a MIDI note number, a waveform, a duration in milliseconds (capped at 10000), and an optional per-note velocity: ``` // A short C-major arpeggiostatic const SpeakerNote s\_arpeggio[] = {{ .midi\_note = 60, .waveform = SpeakerWaveformSine,.duration\_ms = 200 }, // C4{ .midi\_note = 64, .waveform = SpeakerWaveformSine,.duration\_ms = 200 }, // E4{ .midi\_note = 67, .waveform = SpeakerWaveformSine,.duration\_ms = 200 }, // G4{ .midi\_note = 72, .waveform = SpeakerWaveformTriangle, .duration\_ms = 400 }, // C5};speaker\_play\_notes(s\_arpeggio, ARRAY\_LENGTH(s\_arpeggio), 80); ``` A `midi_note` of `0` is treated as a rest of the given duration. MIDI note `60` is middle C (C4), and the standard MIDI note numbering applies. ## Playing Polyphonic Tracks `speaker_play_tracks()` mixes up to four [`SpeakerTrack`](https://developer.repebble.com/docs/c/User_Interface/Speaker.md#SpeakerTrack "SpeakerTrack") voices together. Each track is its own monophonic note sequence, so four tracks together can play a four-voice harmony. Each track may also point at a [`SpeakerSample`](https://developer.repebble.com/docs/c/User_Interface/Speaker.md#SpeakerSample "SpeakerSample"), which causes the synthesizer to pitch-shift that PCM sample to the requested note instead of generating a waveform: ``` static const SpeakerNote s\_melody[] = {{ .midi\_note = 72, .waveform = SpeakerWaveformSine, .duration\_ms = 250 },{ .midi\_note = 74, .waveform = SpeakerWaveformSine, .duration\_ms = 250 },{ .midi\_note = 76, .waveform = SpeakerWaveformSine, .duration\_ms = 500 },};static const SpeakerNote s\_bass[] = {{ .midi\_note = 48, .waveform = SpeakerWaveformTriangle, .duration\_ms = 500 },{ .midi\_note = 50, .waveform = SpeakerWaveformTriangle, .duration\_ms = 500 },};static const SpeakerTrack s\_tracks[] = {{ .notes = s\_melody, .num\_notes = ARRAY\_LENGTH(s\_melody) },{ .notes = s\_bass,.num\_notes = ARRAY\_LENGTH(s\_bass)},};speaker\_play\_tracks(s\_tracks, ARRAY\_LENGTH(s\_tracks), 80); ``` To use a sample instead of a synthesized waveform, fill in a [`SpeakerSample`](https://developer.repebble.com/docs/c/User_Interface/Speaker.md#SpeakerSample "SpeakerSample") and point the track at it. The sample's `base_midi_note` specifies which note plays the sample at its original pitch; other notes in the track are produced by pitch-shifting the same sample. Setting `loop` to `true` lets a short sample sustain through longer notes: ``` static const SpeakerSample s\_kick = {.data= kick\_pcm\_data,.num\_bytes= sizeof(kick\_pcm\_data),.format= SpeakerPcmFormat\_16kHz\_16bit,.base\_midi\_note = 36,// C2.loop= false,};static const SpeakerTrack s\_drum\_track = {.notes= s\_drum\_notes,.num\_notes = ARRAY\_LENGTH(s\_drum\_notes),.sample= &s\_kick,}; ``` Pass between 1 and 4 tracks to `speaker_play_tracks()`. Tracks beyond the fourth are not supported. ## Streaming PCM For arbitrary audio - synthesised on the fly, decoded from a downloaded file, or generated procedurally - open a PCM stream, push bytes into it, and close it when done. Open a stream by choosing one of the supported PCM formats and a starting volume: ``` if (!speaker\_stream\_open(SpeakerPcmFormat\_16kHz\_16bit, 80)) {// Speaker was busy or unavailablereturn;} ``` The supported formats are all mono, signed PCM: | Format | Bytes / sample | Sample rate | | --- | --- | --- | | [`SpeakerPcmFormat_8kHz_8bit`](https://developer.repebble.com/docs/c/User_Interface/Speaker.md#SpeakerPcmFormat_8kHz_8bit "SpeakerPcmFormat\_8kHz\_8bit") | 1 | 8 kHz | | [`SpeakerPcmFormat_16kHz_8bit`](https://developer.repebble.com/docs/c/User_Interface/Speaker.md#SpeakerPcmFormat_16kHz_8bit "SpeakerPcmFormat\_16kHz\_8bit") | 1 | 16 kHz | | [`SpeakerPcmFormat_8kHz_16bit`](https://developer.repebble.com/docs/c/User_Interface/Speaker.md#SpeakerPcmFormat_8kHz_16bit "SpeakerPcmFormat\_8kHz\_16bit") | 2 (little-endian) | 8 kHz | | [`SpeakerPcmFormat_16kHz_16bit`](https://developer.repebble.com/docs/c/User_Interface/Speaker.md#SpeakerPcmFormat_16kHz_16bit "SpeakerPcmFormat\_16kHz\_16bit") | 2 (little-endian) | 16 kHz | Then push samples into the stream. `speaker_stream_write()` returns the number of bytes it actually accepted, which may be less than the amount requested if the internal buffer is full. Loop and retry the remainder: ``` const uint8\_t \*cursor = buffer;uint32\_t remaining = buffer\_size;while (remaining \> 0) {uint32\_t written = speaker\_stream\_write(cursor, remaining);cursor+= written;remaining -= written;if (written == 0) {// Buffer is full - yield briefly and try againpsleep(5);}} ``` When all of the audio has been written, close the stream. `speaker_stream_close()` drains anything still buffered before it stops the speaker, so it's safe to call as soon as the last byte has been written: ``` speaker\_stream\_close(); ``` ## Stopping Playback Early Any playback path can be stopped immediately with: ``` speaker\_stop(); ``` This applies equally to tones, note sequences, polyphonic tracks, and PCM streams. If a finish callback is registered, it will fire with the [`SpeakerFinishReasonStopped`](https://developer.repebble.com/docs/c/User_Interface/Speaker.md#SpeakerFinishReasonStopped "SpeakerFinishReasonStopped") reason. ## Knowing When Playback Ends To find out when playback finishes - naturally or otherwise - register a finish callback before starting playback: ``` static void speaker\_finished(SpeakerFinishReason reason, void \*ctx) {switch (reason) {case SpeakerFinishReasonDone:APP\_LOG(APP\_LOG\_LEVEL\_INFO, "Playback completed");break;case SpeakerFinishReasonStopped:APP\_LOG(APP\_LOG\_LEVEL\_INFO, "Stopped by the app");break;case SpeakerFinishReasonPreempted:APP\_LOG(APP\_LOG\_LEVEL\_INFO, "Preempted by the system");break;case SpeakerFinishReasonError:APP\_LOG(APP\_LOG\_LEVEL\_ERROR, "Playback error");break;}}speaker\_set\_finish\_callback(speaker\_finished, NULL);speaker\_play\_notes(s\_arpeggio, ARRAY\_LENGTH(s\_arpeggio), 80); ``` The callback runs on the app task, so it's safe to update UI or kick off follow-up playback from inside it. The current state can also be polled at any time with `speaker_get_status()`, which returns one of: | Status | Meaning | | --- | --- | | [`SpeakerStatusIdle`](https://developer.repebble.com/docs/c/User_Interface/Speaker.md#SpeakerStatusIdle "SpeakerStatusIdle") | Nothing is playing. | | [`SpeakerStatusPlaying`](https://developer.repebble.com/docs/c/User_Interface/Speaker.md#SpeakerStatusPlaying "SpeakerStatusPlaying") | A tone, sequence, track set, or stream is actively playing. | | [`SpeakerStatusDraining`](https://developer.repebble.com/docs/c/User_Interface/Speaker.md#SpeakerStatusDraining "SpeakerStatusDraining") | Playback has stopped accepting new input and is finishing the buffered audio. | ## Detecting Speaker Support Not every platform has a built-in speaker. There are two ways to handle this: At compile time, the `PBL_SPEAKER` preprocessor define is present on platforms that have a speaker, so speaker-specific code can be excluded entirely from builds for platforms that don't: ``` #if defined(PBL\_SPEAKER)speaker\_play\_tone(440, 250, 80, SpeakerWaveformSine);#elsevibes\_short\_pulse();#endif ``` At runtime, `speaker_play_tone()`, `speaker_play_notes()`, `speaker_play_tracks()`, and `speaker_stream_open()` all return `false` on platforms without a speaker (or when the speaker is busy with another request), so apps that always call through the API can just check the return value and fall back to vibration or a visual cue. ## Battery Considerations Driving the speaker is one of the most power-hungry things an app can do. Long PCM streams in particular keep the audio hardware powered and the CPU busy moving samples into the buffer. Use the speaker for short, intentional sounds - confirmations, alarms, brief melodies - and prefer `speaker_play_tone()` or `speaker_play_notes()` over PCM streaming whenever a synthesized sound would do the job. --- # Touch Source: https://developer.repebble.com/guides/events-and-services/touch/ On hardware platforms with a touchscreen, the `TouchService` lets an app receive touchdown, lift-off, and position updates as the user moves their finger across the display. See the platform support table in [_Hardware Information_](https://developer.repebble.com/guides/tools-and-resources/hardware-information.md#platform-support) for which watches have a touchscreen. This is the same low-level event stream the system itself uses, so apps can build draggable UI or free-form input on top of it. Apps that want gestures rather than raw touches can use the built-in [gesture recognizers](#gesture-recognizers), and apps that just want their menus to scroll by touch can opt in to [touch navigation](#touch-navigation) without handling any touch events at all. **Important** Touch input is currently **not supported in watchfaces**. While we work out how we want to expose it, touch is intentionally restricted to watchapps - it is easier to allow it later than to take it away once apps depend on it. For now, only use the `TouchService` from a watchapp. ## Detecting Touch Support A touchscreen is not present on every platform, and even when it is the user can disable touch input from _Settings → Display → Touch_. Apps should call [`touch_service_is_enabled()`](https://developer.repebble.com/docs/c/Foundation/Event_Service/TouchService.md#touch_service_is_enabled "touch\_service\_is\_enabled") before relying on touch input - typically from the window's `appear` handler - and gracefully degrade if it returns `false`: ``` static void main\_window\_appear(Window \*window) {if (!touch\_service\_is\_enabled()) {text\_layer\_set\_text(s\_status\_layer,"Touch is disabled. Enable it in Settings → Display.");return;}// Touch is available - subscribe and start the touch UItouch\_service\_subscribe(touch\_handler, NULL);} ``` `touch_service_is_enabled()` returns `false` on platforms without a touchscreen, so a single check covers both the "no hardware" and the "user-disabled" cases. For code that should only be compiled on platforms with a touchscreen at all - for example, an entire gesture recognizer that has no equivalent on button-only hardware - use the `PBL_TOUCH` compile-time define: ``` #if defined(PBL\_TOUCH)touch\_service\_subscribe(touch\_handler, NULL);#else// Fall back to a button-based UIwindow\_set\_click\_config\_provider(window, click\_config\_provider);#endif ``` ## Subscribing to Touch Events Touch events are delivered through a [`TouchServiceHandler`](https://developer.repebble.com/docs/c/Foundation/Event_Service/TouchService.md#TouchServiceHandler "TouchServiceHandler") callback. The handler receives a pointer to a [`TouchEvent`](https://developer.repebble.com/docs/c/Foundation/Event_Service/TouchService.md#TouchEvent "TouchEvent") describing what happened, and the context pointer that was registered with the subscription: ``` static void touch\_handler(const TouchEvent \*event, void \*context) {switch (event-\>type) {case TouchEvent\_Touchdown:APP\_LOG(APP\_LOG\_LEVEL\_DEBUG, "Touchdown at %d, %d", event-\>x, event-\>y);break;case TouchEvent\_PositionUpdate:APP\_LOG(APP\_LOG\_LEVEL\_DEBUG, "Move to %d, %d", event-\>x, event-\>y);break;case TouchEvent\_Liftoff:APP\_LOG(APP\_LOG\_LEVEL\_DEBUG, "Liftoff at %d, %d", event-\>x, event-\>y);break;}} ``` Subscribing to the service powers on the touch sensor; it stays on as long as at least one app is subscribed and is automatically disabled again once the last subscriber drops: ``` // Receive touch eventstouch\_service\_subscribe(touch\_handler, NULL); ``` When the app no longer needs touch input - for example, when its main window disappears - unsubscribe: ``` touch\_service\_unsubscribe(); ``` ## Event Types The [`TouchEventType`](https://developer.repebble.com/docs/c/Foundation/Event_Service/TouchService.md#TouchEventType "TouchEventType") field on each event identifies what the user just did: | Event Type | Description | | --- | --- | | [`TouchEvent_Touchdown`](https://developer.repebble.com/docs/c/Foundation/Event_Service/TouchService.md#TouchEvent_Touchdown "TouchEvent\_Touchdown") | The user has just placed a finger on the screen. `x` and `y` are the initial contact position. | | [`TouchEvent_PositionUpdate`](https://developer.repebble.com/docs/c/Foundation/Event_Service/TouchService.md#TouchEvent_PositionUpdate "TouchEvent\_PositionUpdate") | An existing touch has moved. `x` and `y` are the new position. | | [`TouchEvent_Liftoff`](https://developer.repebble.com/docs/c/Foundation/Event_Service/TouchService.md#TouchEvent_Liftoff "TouchEvent\_Liftoff") | The user has just lifted their finger. `x` and `y` are the final position before lift-off. | Coordinates are in the same screen-relative pixel space used everywhere else in the UI, so they can be passed directly to drawing routines or compared against [`Layer`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#Layer "Layer") bounds. A typical touch interaction starts with a single `TouchEvent_Touchdown`, followed by zero or more `TouchEvent_PositionUpdate` events as the finger moves, and ends with a single `TouchEvent_Liftoff`. Apps that want to track gestures (taps, drags, swipes) can do this by hand - store the touchdown position, watch the position updates, and decide what happened on lift-off - but in most cases the built-in [gesture recognizers](#gesture-recognizers) below do that work already. Each [`TouchEvent`](https://developer.repebble.com/docs/c/Foundation/Event_Service/TouchService.md#TouchEvent "TouchEvent") also carries a `non_navigational` flag. It is set on touches that arrive while no _interaction session_ is active (see [Touch Navigation](#touch-navigation) below) — the user is touching the screen without having woken the watch first. Raw subscribers still receive these events and can decide for themselves whether to honor them. ## Touch Navigation Since firmware 4.32, the system can translate touches into the button-based navigation model: tapping and swiping in a [`MenuLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/MenuLayer.md "MenuLayer") scrolls it and activates rows, taps on an [`ActionBarLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/ActionBarLayer.md "ActionBarLayer") are zoned into up/select/down button events, and [`ActionMenu`](https://developer.repebble.com/docs/c/User_Interface/Window/ActionMenu.md "ActionMenu") items activate on tap. As of firmware 4.33 this _touch navigation_ is enabled by default, and it is gated on an **interaction session** : the user must press a button or wake the watch with a gesture before touches navigate. This prevents accidental navigation from brushing against an idle watchface. Watchapps are **opted out** of touch navigation by default, so existing apps behave exactly as before. To let the system drive your menus and scroll views by touch, opt in once at startup: ``` app\_touch\_navigation\_enable(true); ``` With touch navigation enabled for the app, taps and swipes that don't hit a touch-aware widget are bridged to button click events, so a plain `click_config_provider` keeps working without any touch-specific code. If one particular [`Window`](https://developer.repebble.com/docs/c/User_Interface/Window.md "Window") handles its own touch input - with a raw subscription or with recognizers - take it out of the touch bridge so the system doesn't consume its touches: ``` window\_set\_touch\_bridge\_disabled(window, true); ``` ## Gesture Recognizers Rather than tracking touchdown/move/lift-off sequences by hand, apps can create _recognizers_ that watch the touch stream for one specific gesture and report progress through a callback. Three recognizers are available: | Recognizer | Constructor | Recognizes | | --- | --- | --- | | Tap | [`tap_recognizer_create()`](https://developer.repebble.com/docs/c/User_Interface/Gesture_Recognizers.md#tap_recognizer_create "tap\_recognizer\_create") | A single tap. Read the location with [`tap_recognizer_get_tap_point()`](https://developer.repebble.com/docs/c/User_Interface/Gesture_Recognizers.md#tap_recognizer_get_tap_point "tap\_recognizer\_get\_tap\_point"). | | Pan | [`pan_recognizer_create()`](https://developer.repebble.com/docs/c/User_Interface/Gesture_Recognizers.md#pan_recognizer_create "pan\_recognizer\_create") | A drag locked to one axis ([`PanAxis_Horizontal`](https://developer.repebble.com/docs/c/User_Interface/Gesture_Recognizers.md#PanAxis_Horizontal "PanAxis\_Horizontal") or [`PanAxis_Vertical`](https://developer.repebble.com/docs/c/User_Interface/Gesture_Recognizers.md#PanAxis_Vertical "PanAxis\_Vertical")). Read movement with [`pan_recognizer_get_total_delta()`](https://developer.repebble.com/docs/c/User_Interface/Gesture_Recognizers.md#pan_recognizer_get_total_delta "pan\_recognizer\_get\_total\_delta"), [`pan_recognizer_get_delta_since_start()`](https://developer.repebble.com/docs/c/User_Interface/Gesture_Recognizers.md#pan_recognizer_get_delta_since_start "pan\_recognizer\_get\_delta\_since\_start"), [`pan_recognizer_get_delta_since_prev()`](https://developer.repebble.com/docs/c/User_Interface/Gesture_Recognizers.md#pan_recognizer_get_delta_since_prev "pan\_recognizer\_get\_delta\_since\_prev") and [`pan_recognizer_get_velocity()`](https://developer.repebble.com/docs/c/User_Interface/Gesture_Recognizers.md#pan_recognizer_get_velocity "pan\_recognizer\_get\_velocity"). | | Swipe | [`swipe_recognizer_create()`](https://developer.repebble.com/docs/c/User_Interface/Gesture_Recognizers.md#swipe_recognizer_create "swipe\_recognizer\_create") | A fast, straight flick in one of the directions in the given [`SwipeDirection`](https://developer.repebble.com/docs/c/User_Interface/Gesture_Recognizers.md#SwipeDirection "SwipeDirection") mask. Read the result with [`swipe_recognizer_get_direction()`](https://developer.repebble.com/docs/c/User_Interface/Gesture_Recognizers.md#swipe_recognizer_get_direction "swipe\_recognizer\_get\_direction") and [`swipe_recognizer_get_velocity()`](https://developer.repebble.com/docs/c/User_Interface/Gesture_Recognizers.md#swipe_recognizer_get_velocity "swipe\_recognizer\_get\_velocity"). | Each recognizer reports [`RecognizerEvent_Started`](https://developer.repebble.com/docs/c/User_Interface/Gesture_Recognizers.md#RecognizerEvent_Started "RecognizerEvent\_Started"), [`RecognizerEvent_Updated`](https://developer.repebble.com/docs/c/User_Interface/Gesture_Recognizers.md#RecognizerEvent_Updated "RecognizerEvent\_Updated"), [`RecognizerEvent_Completed`](https://developer.repebble.com/docs/c/User_Interface/Gesture_Recognizers.md#RecognizerEvent_Completed "RecognizerEvent\_Completed") and [`RecognizerEvent_Cancelled`](https://developer.repebble.com/docs/c/User_Interface/Gesture_Recognizers.md#RecognizerEvent_Cancelled "RecognizerEvent\_Cancelled") events to its [`RecognizerEventCb`](https://developer.repebble.com/docs/c/User_Interface/Gesture_Recognizers.md#RecognizerEventCb "RecognizerEventCb"). Recognizers are attached to a window with [`window_attach_recognizer()`](https://developer.repebble.com/docs/c/User_Interface/Window.md#window_attach_recognizer "window\_attach\_recognizer"). The window takes ownership and destroys attached recognizers when it unloads, so in the common case there is nothing to clean up manually ([`recognizer_destroy()`](https://developer.repebble.com/docs/c/User_Interface/Gesture_Recognizers.md#recognizer_destroy "recognizer\_destroy") exists for recognizers that were never attached). Remember to also disable the window's touch bridge, or the system will consume the touches before your recognizers see them. This example scrolls a custom [`ScrollLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/ScrollLayer.md "ScrollLayer") by finger — something a bare `ScrollLayer` does not do on its own: ``` static ScrollLayer \*s\_scroll;static int16\_t s\_base;// content offset committed on Completestatic void pan\_handler(const Recognizer \*recognizer, RecognizerEvent event) {switch (event) {case RecognizerEvent\_Updated: {// delta\_since\_start is (0, 0) at Start, so the content does not jumpGPoint d = pan\_recognizer\_get\_delta\_since\_start(recognizer);scroll\_layer\_set\_content\_offset(s\_scroll, GPoint(0, s\_base + d.y), false);break;}case RecognizerEvent\_Completed:// Commit the new offsets\_base = scroll\_layer\_get\_content\_offset(s\_scroll).y;break;case RecognizerEvent\_Cancelled:// Roll back to the last committed offsetscroll\_layer\_set\_content\_offset(s\_scroll, GPoint(0, s\_base), true);break;default:break;}}static void main\_window\_load(Window \*window) {Layer \*root = window\_get\_root\_layer(window);s\_scroll = scroll\_layer\_create(layer\_get\_bounds(root));scroll\_layer\_set\_content\_size(s\_scroll,GSize(layer\_get\_bounds(root).size.w, total\_content\_height));// Add custom row layers as children of s\_scroll herelayer\_add\_child(root, scroll\_layer\_get\_layer(s\_scroll));// Handle this window's touches ourselves instead of the system bridgewindow\_set\_touch\_bridge\_disabled(window, true);// The window owns the recognizer and destroys it when the window unloadsRecognizer \*pan = pan\_recognizer\_create(pan\_handler, NULL, PanAxis\_Vertical);window\_attach\_recognizer(window, pan);} ``` When several recognizers watch the same window, they are evaluated exclusively by default: the first one to recognize its gesture wins. Use [`recognizer_set_simultaneous_with()`](https://developer.repebble.com/docs/c/User_Interface/Gesture_Recognizers.md#recognizer_set_simultaneous_with "recognizer\_set\_simultaneous\_with") to allow two recognizers to evaluate at the same time, or [`recognizer_set_fail_after()`](https://developer.repebble.com/docs/c/User_Interface/Gesture_Recognizers.md#recognizer_set_fail_after "recognizer\_set\_fail\_after") to hold one back until another has failed (e.g. only treat a touch as a tap once the swipe recognizer has given up). Note that on touch hardware a [`MenuLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/MenuLayer.md "MenuLayer") already scrolls and activates by touch when the app has opted in to touch navigation. Recognizers are for interactions the built-in widgets don't provide, such as custom menus, drags, or acting on raw taps and swipes. ## Backlight Behavior Each touch event triggers the backlight the same way a wrist flick or button press would - the light flashes on for the system auto-off interval and then fades out. This keeps the screen lit naturally while the user is actively interacting, without keeping the backlight pinned on between taps. There is no need to call the [Light API](https://developer.repebble.com/guides/events-and-services/light.md) manually to achieve this; subsequent touches will re-trigger the backlight on their own. ## Battery Considerations The touch sensor is an active component and draws power continuously while enabled. Subscribe to the touch service only while the app's UI actually needs touch input, and unsubscribe as soon as it doesn't - for example, in the window `disappear` handler, or when navigating to a screen that uses buttons instead. --- # Wakeups Source: https://developer.repebble.com/guides/events-and-services/wakeups/ The [`Wakeup`](https://developer.repebble.com/docs/c/Foundation/Wakeup.md "Wakeup") API allows developers to schedule an app launch in the future, even if the app itself is closed in the meantime. A wakeup event is scheduled in a similar manner to a [`Timer`](https://developer.repebble.com/docs/c/Foundation/Timer.md "Timer") with a future timestamp calculated beforehand. ## Calculating Timestamps To schedule a wakeup event, first determine the timestamp of the desired wakeup time as a `time_t` variable. Most uses of the [`Wakeup`](https://developer.repebble.com/docs/c/Foundation/Wakeup.md "Wakeup") API will fall into three distinct scenarios discussed below. ### A Future Time Call [`time()`](https://developer.repebble.com/docs/c/Standard_C/Time.md "Time") and add the offset, measured in seconds. For example, for 30 minutes in the future: ``` // 30 minutes from nowtime\_t timestamp = time(NULL) + (30 \* SECONDS\_PER\_MINUTE); ``` ### A Specific Time Use [`clock_to_timestamp()`](https://developer.repebble.com/docs/c/Foundation/Wall_Time.md#clock_to_timestamp "clock\_to\_timestamp") to obtain a `time_t` timestamp by specifying a day of the week and hours and minutes (in 24 hour format). For example, for the next occuring Monday at 5 PM: ``` // Next occuring monday at 17:00 time\_t timestamp = clock\_to\_timestamp(MONDAY, 17, 0); ``` ### Using a Timestamp Provided by a Web Service The timestamp will need to be translated using the [`getTimezoneOffset()`](http://www.w3schools.com/jsref/jsref_gettimezoneoffset.asp) method available in PebbleKit JS or with any timezone information given by the web service. ## Scheduling a Wakeup Once a `time_t` timestamp has been calculated, the wakeup event can be scheduled: ``` // Let the timestamp be 30 minutes from nowconst time\_t future\_timestamp = time() + (30 \* SECONDS\_PER\_MINUTE);// Choose a 'cookie' value representing the reason for the wakeupconst int cookie = 0;// If true, the user will be notified if they missed the wakeup // (i.e. their watch was off)const bool notify\_if\_missed = true;// Schedule wakeup eventWakeupId id = wakeup\_schedule(future\_timestamp, cookie, notify\_if\_missed);// Check the scheduling was successfulif(id \>= 0) {// Persist the ID so that a future launch can query itconst wakeup\_id\_key = 43;persist\_write\_int(wakeup\_id\_key, id);} ``` After scheduling a wakeup event it is possible to perform some interaction with it. For example, reading the timestamp for when the event will occur using the [`WakeupId`](https://developer.repebble.com/docs/c/Foundation/Wakeup.md#WakeupId "WakeupId") with [`wakeup_query()`](https://developer.repebble.com/docs/c/Foundation/Wakeup.md#wakeup_query "wakeup\_query"), and then perform simple arithmetic to get the time remaining: ``` // This will be set by wakeup\_query()time\_t wakeup\_timestamp = 0;// Is the wakeup still scheduled?if(wakeup\_query(id, &wakeup\_timestamp)) {// Get the time remainingint seconds\_remaining = wakeup\_timestamp - time(NULL);APP\_LOG(APP\_LOG\_LEVEL\_INFO, "%d seconds until wakeup", seconds\_remaining);} ``` To cancel a scheduled event, use the [`WakeupId`](https://developer.repebble.com/docs/c/Foundation/Wakeup.md#WakeupId "WakeupId") obtained when it was scheduled: ``` // Cancel a wakeupwakeup\_cancel(id); ``` To cancel all scheduled wakeup events: ``` // Cancel all wakeupswakeup\_cancel\_all(); ``` ## Limitations There are three limitations that should be taken into account when using the Wakeup API: - There can be no more than 8 scheduled wakeup events per app at any one time. - Wakeup events cannot be scheduled within 30 seconds of the current time. - Wakeup events are given a one minute window either side of the wakeup time. In this time no app may schedule an event. The return [`StatusCode`](https://developer.repebble.com/docs/c/Foundation/Storage.md#StatusCode "StatusCode") of [`wakeup_schedule()`](https://developer.repebble.com/docs/c/Foundation/Wakeup.md#wakeup_schedule "wakeup\_schedule") should be checked to determine whether the scheduling of the new event should be reattempted. A negative value indicates that the wakeup could not be scheduled successfully. The possible [`StatusCode`](https://developer.repebble.com/docs/c/Foundation/Storage.md#StatusCode "StatusCode") values are detailed below: | StatusCode | Value | Description | | --- | --- | --- | | `E_RANGE` | `-8` | The wakeup event cannot be scheduled due to another event in that period. | | `E_INVALID_ARGUMENT` | `-4` | The time requested is in the past. | | `E_OUT_OF_RESOURCES` | `-7` | The application has already scheduled all 8 wakeup events. | | `E_INTERNAL` | `-3` | A system error occurred during scheduling. | ## Handling Wakeup Events A wakeup event can occur at two different times - when the app is closed, and when it is already launched and in the foreground. If the app is launched due to a previously scheduled wakeup event, check the [`AppLaunchReason`](https://developer.repebble.com/docs/c/Foundation/Launch_Reason.md#AppLaunchReason "AppLaunchReason") and load the app accordingly: ``` static void init() {if(launch\_reason() == APP\_LAUNCH\_WAKEUP) {// The app was started by a wakeup event.WakeupId id = 0;int32\_t reason = 0;// Get details and handle the event appropriatelywakeup\_get\_launch\_event(&id, &reason);}/\* other init code \*/} ``` If the app is expecting a wakeup to occur while it is open, use a subscription to the wakeup service to be notified of such events: ``` static void wakeup\_handler(WakeupId id, int32\_t reason) {// A wakeup event has occurred while the app was already open} ``` ``` // Subscribe to wakeup servicewakeup\_service\_subscribe(wakeup\_handler); ``` The two approaches can also be combined for a unified response to being woken up, not depenent on the state of the app: ``` // Get details of the wakeupwakeup\_get\_launch\_event(&id, &reason);// Manually handle using the handlerwakeup\_handler(id, reason); ``` --- # Creating Pins Source: https://developer.repebble.com/guides/pebble-timeline/pin-structure/ A timeline pin contains all the information required to be displayed on the watch, and is written in the JSON object format. It can contain basic information such as title and times, or more advanced data such as notifications, reminders, or actions that can be used out from the pin view. ## Pin Overview The table below details the pin object fields and their function within the object. Those marked in **bold** are required. | Field | Type | Function | | --- | --- | --- | | **`id`** | String (max. 64 chars) | Developer-implemented identifier for this pin event, which cannot be re-used. This means that any pin that was previously deleted cannot then be re-created with the same `id`. | | **`time`** | String ([ISO date-time](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString)) | The start time of the event the pin represents, such as the beginning of a meeting. | | `duration` | Integer number | The duration of the event the pin represents, in minutes. | | `createNotification` | [Notification object](#notification-object) | The notification shown when the event is first created. | | `updateNotification` | [Notification object](#notification-object) | The notification shown when the event is updated but already exists. | | **`layout`** | [Layout object](#layout-object) | Description of the values to populate the layout when the user views the pin. | | `reminders` | [Reminder object](#reminder-object) array (Max. 3) | Collection of event reminders to display before an event starts. | | `actions` | [Action object](#action-object) array | Collection of event actions that can be executed by the user. | ### Notification Object The notification objects used for `createNotification` and `updateNotification` contain only one common field: a Layout object describing the visual properties of the notification. The other field (`time`) is used only in an `updateNotification` object. The `createNotification` type does **not** require a `time` attribute. | Field | Type | Function | | --- | --- | --- | | `layout` | [Layout object](#layout-object) | The layout that will be used to display this notification. | | `time` | String (ISO date-time) | The new time of the pin update. | The `createNotification` is shown when the pin is first delivered to the watch. The `updateNotification` is shown when the pin already existed and is being updated on the watch. It will only be shown if the `updateNotification.time` is newer than the last `updateNotification.time` received by the watch. Using these fields, developers can build a great experience for their users when live updating pins. For example, when sending updates about a sports game the app could use the `createNotification` to tell the user that "The game has just been added to your timeline" and the `updateNotification` to tell them that "The game starting time has just been updated in your timeline.". ### Layout Object The Layout object is used to describe any message shown in a customizable layout. This includes a pin in the timeline, a notification, and also reminders. Developers can choose between different layout types and customize them with attributes. Required fields are shown in **bold**. Some layout types have additional required fields not shown below, but listed in their dedicated sections. Values for icon URIs can be found below under [Pin Icons](#pin-icons), although not all icons are available at all sizes. | Field | Type | Function | | --- | --- | --- | | **`type`** | String | The type of layout the pin will use. See [_Pin Layouts_](#pin-layouts) for a list of available types. | | `title` | String | The title of the pin when viewed. | | `subtitle` | String | Shorter subtitle for details. | | `body` | String | The body text of the pin. Maximum of 512 characters. | | `tinyIcon` | String | URI of the pin's tiny icon. | | `smallIcon` | String | URI of the pin's small icon. | | `largeIcon` | String | URI of the pin's large icon. | The following attributes are also available for all pin layout types **(excluding notifications and reminders)**. | Field | Type | Function | | --- | --- | --- | | `primaryColor` | String | Six-digit color hexadecimal string or case-insensitive SDK constant (e.g.: "665566" or "mintgreen"), describing the primary text color. | | `secondaryColor` | String | Similar to `primaryColor`, except applies to the layout's secondary-colored elements. | | `backgroundColor` | String | Similar to `primaryColor`, except applies to the layout's background color. | | `headings` | Array of Strings | List of section headings in this layout. The list must be less than 128 characters in length, including the underlying delimiters (one byte) between each item. Longer items will be truncated with an ellipsis ('...'). | | `paragraphs` | Array of Strings | List of paragraphs in this layout. **Must equal the number of `headings`**. The list must be less than 1024 characters in length, including the underlying delimiters (one byte) between each item. Longer items will be truncated with an ellipsis ('...'). | | `lastUpdated` | ISO date-time | Timestamp of when the pin’s data (e.g: weather forecast or sports score) was last updated. | ### Reminder Object Reminders are synchronized to the watch and will be shown at the precise time set in the reminder. They work even when Pebble is disconnected from the user's mobile phone. | Field | Type | Function | | --- | --- | --- | | `time` | String (ISO date-time) | The time the reminder is scheduled to be shown. | | `layout` | [Layout object](#layout-object) | The layout of the reminder. | ### Action Object | Field | Type | Function | | --- | --- | --- | | `title` | String | The name of the action that appears on the watch. | | `type` | String | The type of action this will execute. See [_Pin Actions_](#pin-actions) for a list of available actions. | ## Minimal Pin Example The example pin object shown below includes only the required fields for a generic pin. ``` {"id": "example-pin-generic-1","time": "2015-03-19T18:00:00Z","layout": {"type": "genericPin","title": "News at 6 o'clock","tinyIcon": "system://images/NOTIFICATION\_FLAG"}} ``` ## Complete Pin Example Below is a more advanced example pin object: ``` {"id": "meeting-453923","time": "2015-03-19T15:00:00Z","duration": 60,"createNotification": {"layout": {"type": "genericNotification","title": "New Item","tinyIcon": "system://images/NOTIFICATION\_FLAG","body": "A new appointment has been added to your calendar at 4pm."}},"updateNotification": {"time": "2015-03-19T16:00:00Z","layout": {"type": "genericNotification","tinyIcon": "system://images/NOTIFICATION\_FLAG","title": "Reminder","body": "The meeting has been rescheduled to 4pm."}},"layout": {"title": "Client Meeting","type": "genericPin","tinyIcon": "system://images/TIMELINE\_CALENDAR","body": "Meeting in Kepler at 4:00pm. Topic: discuss pizza toppings for party."},"reminders": [{"time": "2015-03-19T14:45:00Z","layout": {"type": "genericReminder","tinyIcon": "system://images/TIMELINE\_CALENDAR","title": "Meeting in 15 minutes"}},{"time": "2015-03-19T14:55:00Z","layout": {"type": "genericReminder","tinyIcon": "system://images/TIMELINE\_CALENDAR","title": "Meeting in 5 minutes"}}],"actions": [{"title": "View Schedule","type": "openWatchApp","launchCode": 15},{"title": "Show Directions","type": "openWatchApp","launchCode": 22}]} ``` ## View Modes When viewing pins in the timeline, they can be displayed in two different ways. | State | Preview | Details | | --- | --- | --- | | Selected | ![](https://developer.repebble.com/assets/images/guides/timeline/timeline-selected.png) | Three lines of text shown from the title, location and sender. | | Not selected | ![](https://developer.repebble.com/assets/images/guides/timeline/timeline-one-line.png) | Time, short title, and icon are shown. | ## Pin Icons The tables below detail the available icons provided by the system. Each icon can be used when pushing a pin in the following manner: ``` "layout": {"type": "genericNotification","title": "Example Pin","tinyIcon": "system://images/NOTIFICATION\_FLAG"} ``` ### Notifications | Preview | Name | Description | | --- | --- | --- | | ![](https://developer.repebble.com/assets/images/guides/timeline/NOTIFICATION_GENERIC.svg) | `NOTIFICATION_GENERIC` | Generic notification | | ![](https://developer.repebble.com/assets/images/guides/timeline/NOTIFICATION_REMINDER.svg) | `NOTIFICATION_REMINDER` | Reminder notification | | ![](https://developer.repebble.com/assets/images/guides/timeline/NOTIFICATION_FLAG.svg) | `NOTIFICATION_FLAG` | Generic notification flag | | ![](https://developer.repebble.com/assets/images/guides/timeline/NOTIFICATION_LIGHTHOUSE.svg) | `NOTIFICATION_LIGHTHOUSE` | Generic lighthouse | ### Generic | Preview | Name | Description | | --- | --- | --- | | ![](https://developer.repebble.com/assets/images/guides/timeline/GENERIC_EMAIL.svg) | `GENERIC_EMAIL` | Generic email | | ![](https://developer.repebble.com/assets/images/guides/timeline/GENERIC_SMS.svg) | `GENERIC_SMS` | Generic SMS icon | | ![](https://developer.repebble.com/assets/images/guides/timeline/GENERIC_WARNING.svg) | `GENERIC_WARNING` | Generic warning icon | | ![](https://developer.repebble.com/assets/images/guides/timeline/GENERIC_CONFIRMATION.svg) | `GENERIC_CONFIRMATION` | Generic confirmation icon | | ![](https://developer.repebble.com/assets/images/guides/timeline/GENERIC_QUESTION.svg) | `GENERIC_QUESTION` | Generic question icon | ### Weather | Preview | Name | Description | | --- | --- | --- | | ![](https://developer.repebble.com/assets/images/guides/timeline/PARTLY_CLOUDY.svg) | `PARTLY_CLOUDY` | Partly cloudy weather | | ![](https://developer.repebble.com/assets/images/guides/timeline/CLOUDY_DAY.svg) | `CLOUDY_DAY` | Cloudy weather | | ![](https://developer.repebble.com/assets/images/guides/timeline/LIGHT_SNOW.svg) | `LIGHT_SNOW` | Light snow weather | | ![](https://developer.repebble.com/assets/images/guides/timeline/LIGHT_RAIN.svg) | `LIGHT_RAIN` | Light rain weather | | ![](https://developer.repebble.com/assets/images/guides/timeline/HEAVY_RAIN.svg) | `HEAVY_RAIN` | Heavy rain weather icon | | ![](https://developer.repebble.com/assets/images/guides/timeline/HEAVY_SNOW.svg) | `HEAVY_SNOW` | Heavy snow weather icon | | ![](https://developer.repebble.com/assets/images/guides/timeline/TIMELINE_WEATHER.svg) | `TIMELINE_WEATHER` | Generic weather icon | | ![](https://developer.repebble.com/assets/images/guides/timeline/TIMELINE_SUN.svg) | `TIMELINE_SUN` | Sunny weather icon | | ![](https://developer.repebble.com/assets/images/guides/timeline/RAINING_AND_SNOWING.svg) | `RAINING_AND_SNOWING` | Raining and snowing weather icon | | ![](https://developer.repebble.com/assets/images/guides/timeline/SUNRISE.svg) | `SUNRISE` | Sunrise weather icon | | ![](https://developer.repebble.com/assets/images/guides/timeline/SUNSET.svg) | `SUNSET` | Sunset weather icon | ### Timeline | Preview | Name | Description | | --- | --- | --- | | ![](https://developer.repebble.com/assets/images/guides/timeline/TIMELINE_MISSED_CALL.svg) | `TIMELINE_MISSED_CALL` | Generic missed call icon | | ![](https://developer.repebble.com/assets/images/guides/timeline/TIMELINE_CALENDAR.svg) | `TIMELINE_CALENDAR` | Generic calendar event icon | | ![](https://developer.repebble.com/assets/images/guides/timeline/TIMELINE_SPORTS.svg) | `TIMELINE_SPORTS` | Generic sports icon | ### Sports | Preview | Name | Description | | --- | --- | --- | | ![](https://developer.repebble.com/assets/images/guides/timeline/TIMELINE_BASEBALL.svg) | `TIMELINE_BASEBALL` | Baseball sports icon | | ![](https://developer.repebble.com/assets/images/guides/timeline/AMERICAN_FOOTBALL.svg) | `AMERICAN_FOOTBALL` | American football sports icon | | ![](https://developer.repebble.com/assets/images/guides/timeline/BASKETBALL.svg) | `BASKETBALL` | Basketball sports icon | | ![](https://developer.repebble.com/assets/images/guides/timeline/CRICKET_GAME.svg) | `CRICKET_GAME` | Cricket sports icon | | ![](https://developer.repebble.com/assets/images/guides/timeline/SOCCER_GAME.svg) | `SOCCER_GAME` | Soccer sports icon | | ![](https://developer.repebble.com/assets/images/guides/timeline/HOCKEY_GAME.svg) | `HOCKEY_GAME` | Hockey sports icon | ### Action Results | Preview | Name | Description | | --- | --- | --- | | ![](https://developer.repebble.com/assets/images/guides/timeline/RESULT_DISMISSED.svg) | `RESULT_DISMISSED` | Dismissed event | | ![](https://developer.repebble.com/assets/images/guides/timeline/RESULT_DELETED.svg) | `RESULT_DELETED` | Deleted event | | ![](https://developer.repebble.com/assets/images/guides/timeline/RESULT_MUTE.svg) | `RESULT_MUTE` | Mute event | | ![](https://developer.repebble.com/assets/images/guides/timeline/RESULT_SENT.svg) | `RESULT_SENT` | Generic message sent event | | ![](https://developer.repebble.com/assets/images/guides/timeline/RESULT_FAILED.svg) | `RESULT_FAILED` | Generic failure event | ### Events | Preview | Name | Description | | --- | --- | --- | | ![](https://developer.repebble.com/assets/images/guides/timeline/STOCKS_EVENT.svg) | `STOCKS_EVENT` | Stocks icon | | ![](https://developer.repebble.com/assets/images/guides/timeline/MUSIC_EVENT.svg) | `MUSIC_EVENT` | Music event | | ![](https://developer.repebble.com/assets/images/guides/timeline/BIRTHDAY_EVENT.svg) | `BIRTHDAY_EVENT` | Birthday event | | ![](https://developer.repebble.com/assets/images/guides/timeline/NEWS_EVENT.svg) | `NEWS_EVENT` | Generic news story event | | ![](https://developer.repebble.com/assets/images/guides/timeline/SCHEDULED_EVENT.svg) | `SCHEDULED_EVENT` | Generic scheduled event | | ![](https://developer.repebble.com/assets/images/guides/timeline/MOVIE_EVENT.svg) | `MOVIE_EVENT` | Generic movie icon | | ![](https://developer.repebble.com/assets/images/guides/timeline/NO_EVENTS.svg) | `NO_EVENTS` | No events icon | ### Miscellaneous | Preview | Name | Description | | --- | --- | --- | | ![](https://developer.repebble.com/assets/images/guides/timeline/PAY_BILL.svg) | `PAY_BILL` | Pay bill event | | ![](https://developer.repebble.com/assets/images/guides/timeline/HOTEL_RESERVATION.svg) | `HOTEL_RESERVATION` | Hotel event | | ![](https://developer.repebble.com/assets/images/guides/timeline/TIDE_IS_HIGH.svg) | `TIDE_IS_HIGH` | High tide event | | ![](https://developer.repebble.com/assets/images/guides/timeline/INCOMING_PHONE_CALL.svg) | `INCOMING_PHONE_CALL` | Incoming phone call event | | ![](https://developer.repebble.com/assets/images/guides/timeline/DURING_PHONE_CALL.svg) | `DURING_PHONE_CALL` | Phone call event | | ![](https://developer.repebble.com/assets/images/guides/timeline/DURING_PHONE_CALL_CENTERED.svg) | `DURING_PHONE_CALL_CENTERED` | Phone call event centered | | ![](https://developer.repebble.com/assets/images/guides/timeline/DISMISSED_PHONE_CALL.svg) | `DISMISSED_PHONE_CALL` | Phone call dismissed event | | ![](https://developer.repebble.com/assets/images/guides/timeline/CHECK_INTERNET_CONNECTION.svg) | `CHECK_INTERNET_CONNECTION` | Check Internet connection event | | ![](https://developer.repebble.com/assets/images/guides/timeline/GLUCOSE_MONITOR.svg) | `GLUCOSE_MONITOR` | Sensor monitor event | | ![](https://developer.repebble.com/assets/images/guides/timeline/ALARM_CLOCK.svg) | `ALARM_CLOCK` | Alarm clock event | | ![](https://developer.repebble.com/assets/images/guides/timeline/CAR_RENTAL.svg) | `CAR_RENTAL` | Generic car rental event | | ![](https://developer.repebble.com/assets/images/guides/timeline/DINNER_RESERVATION.svg) | `DINNER_RESERVATION` | Dinner reservation event | | ![](https://developer.repebble.com/assets/images/guides/timeline/RADIO_SHOW.svg) | `RADIO_SHOW` | Radio show event | | ![](https://developer.repebble.com/assets/images/guides/timeline/AUDIO_CASSETTE.svg) | `AUDIO_CASSETTE` | Audio cassette icon | | ![](https://developer.repebble.com/assets/images/guides/timeline/SCHEDULED_FLIGHT.svg) | `SCHEDULED_FLIGHT` | Scheduled flight event | | ![](https://developer.repebble.com/assets/images/guides/timeline/REACHED_FITNESS_GOAL.svg) | `REACHED_FITNESS_GOAL` | Reached fitness goal event | | ![](https://developer.repebble.com/assets/images/guides/timeline/DAY_SEPARATOR.svg) | `DAY_SEPARATOR` | Day separator icon | | ![](https://developer.repebble.com/assets/images/guides/timeline/WATCH_DISCONNECTED.svg) | `WATCH_DISCONNECTED` | Watch disconnected event | | ![](https://developer.repebble.com/assets/images/guides/timeline/TV_SHOW.svg) | `TV_SHOW` | Generic TV show icon | | ![](https://developer.repebble.com/assets/images/guides/timeline/LOCATION.svg) | `LOCATION` | Generic location icon | | ![](https://developer.repebble.com/assets/images/guides/timeline/SETTINGS.svg) | `SETTINGS` | Generic settings icon | ### Custom Icons Custom icons were introduced in SDK 4.0. They allow you to use custom images for timeline pins, by utilizing the [_Published Media_](https://developer.repebble.com/guides/tools-and-resources/app-metadata.md#published-media)`name`. E.g. `app://images/*name*` ## Pin Layouts Developers can customize how pins, reminders and notifications are shown to the user using different layouts. The Pebble SDK includes layouts appropriate for a broad set of apps. Each layout has different customization options, called the layout attributes. Most layouts also offer the option of showing an icon, which must be one of the standard system provided icons, listed under [_Pin Icons_](#pin-icons) above. The sub-sections below detail the available layouts and the fields they will display. Required attributes are shown in **bold**. ### Generic Layout Generic layout for generic pins of no particular type. **Timeline view** #### aplite #### basalt #### chalk ![](https://developer.repebble.com/assets/images/guides/timeline/generic-pin~aplite.png) ![](https://developer.repebble.com/assets/images/guides/timeline/generic-pin~basalt.png) ![](https://developer.repebble.com/assets/images/guides/timeline/generic-pin~chalk.png) **Detail view** #### aplite #### basalt #### chalk ![](https://developer.repebble.com/assets/images/guides/timeline/generic-layout~aplite.png) ![](https://developer.repebble.com/assets/images/guides/timeline/generic-layout~basalt.png) ![](https://developer.repebble.com/assets/images/guides/timeline/generic-layout~chalk.png) **Standard Attributes** **`title`** , **`tinyIcon`** , `subtitle`, `body`. **Color Elements** | Layout Property | Applies To | | --- | --- | | `primaryColor` | Time, body | | `secondaryColor` | Title | | `backgroundColor` | Background | **Example JSON** ``` {"id": "pin-generic-1","time": "2015-09-22T16:30:00Z","layout": {"type": "genericPin","title": "This is a genericPin!","tinyIcon": "system://images/NOTIFICATION\_FLAG","primaryColor": "#FFFFFF","secondaryColor": "#666666","backgroundColor": "#5556FF"}} ``` ### Calendar Layout Standard layout for pins displaying calendar events. **Timeline view** #### aplite #### basalt #### chalk ![](https://developer.repebble.com/assets/images/guides/timeline/calendar-pin~aplite.png) ![](https://developer.repebble.com/assets/images/guides/timeline/calendar-pin~basalt.png) ![](https://developer.repebble.com/assets/images/guides/timeline/calendar-pin~chalk.png) **Detail view** #### aplite #### basalt #### chalk ![](https://developer.repebble.com/assets/images/guides/timeline/calendar-layout~aplite.png) ![](https://developer.repebble.com/assets/images/guides/timeline/calendar-layout~basalt.png) ![](https://developer.repebble.com/assets/images/guides/timeline/calendar-layout~chalk.png) **Standard Attributes** **`title`** , `body`. **Special Attributes** | Field | Type | Function | | --- | --- | --- | | `locationName` | String | Name of the location of this pin event. Used if `shortSubtitle` is not present on the list view, and always in the detail view. | **Color Elements** | Layout Property | Applies To | | --- | --- | | `primaryColor` | Times, body | | `secondaryColor` | Title | | `backgroundColor` | Background | **Example JSON** ``` {"id": "pin-calendar-1","time": "2015-03-18T15:45:00Z","duration": 60,"layout": {"type": "calendarPin","title": "Pin Layout Meeting","locationName": "Conf Room 1","body": "Discuss layout types with Design Team."}} ``` ### Sports Layout Generic layout for displaying sports game pins including team ranks, scores and records. **Timeline view** #### aplite #### basalt #### chalk ![](https://developer.repebble.com/assets/images/guides/timeline/sport-pin~aplite.png) ![](https://developer.repebble.com/assets/images/guides/timeline/sport-pin~basalt.png) ![](https://developer.repebble.com/assets/images/guides/timeline/sport-pin~chalk.png) **Detail view** #### aplite #### basalt #### chalk ![](https://developer.repebble.com/assets/images/guides/timeline/sport-layout~aplite.png) ![](https://developer.repebble.com/assets/images/guides/timeline/sport-layout~basalt.png) ![](https://developer.repebble.com/assets/images/guides/timeline/sport-layout~chalk.png) **Standard Attributes** **`title`** (name of the game), `subtitle` (friendly name of the period), `body` (game description), **`tinyIcon`** , `largeIcon`, `lastUpdated`. **Special Attributes** > Note: The `rankAway` and `rankHome` fields will be shown before the event begins, otherwise `scoreAway` and `scoreHome` will be shown. | Field | Type | Function | | --- | --- | --- | | `rankAway` | String (~2 characters) | The rank of the away team. | | `rankHome` | String (~2 characters) | The rank of the home team. | | `nameAway` | String (Max 4 characters) | Short name of the away team. | | `nameHome` | String (Max 4 characters) | Short name of the home team. | | `recordAway` | String (~5 characters) | Record of the away team (wins-losses). | | `recordHome` | String (~5 characters) | Record of the home team (wins-losses). | | `scoreAway` | String (~2 characters) | Score of the away team. | | `scoreHome` | String (~2 characters) | Score of the home team. | | `sportsGameState` | String | `in-game` for in game or post game, `pre-game` for pre game. | **Color Elements** | Layout Property | Applies To | | --- | --- | | `primaryColor` | Text body | | `secondaryColor` | Team names and scores | | `backgroundColor` | Background | **Example JSON** ``` {"id": "pin-sports-1","time": "2015-03-18T19:00:00Z","layout": {"type": "sportsPin","title": "Bulls at Bears","subtitle": "Halftime","body": "Game of the Century","tinyIcon": "system://images/AMERICAN\_FOOTBALL","largeIcon": "system://images/AMERICAN\_FOOTBALL","lastUpdated": "2015-03-18T18:45:00Z","rankAway": "03","rankHome": "08","nameAway": "POR","nameHome": "LAC","recordAway": "39-19","recordHome": "39-21","scoreAway": "54","scoreHome": "49","sportsGameState": "in-game"}} ``` ### Weather Layout Standard layout for pins displaying the weather. **Timeline view** #### aplite #### basalt #### chalk ![](https://developer.repebble.com/assets/images/guides/timeline/weather-pin~aplite.png) ![](https://developer.repebble.com/assets/images/guides/timeline/weather-pin~basalt.png) ![](https://developer.repebble.com/assets/images/guides/timeline/weather-pin~chalk.png) **Detail view** #### aplite #### basalt #### chalk ![](https://developer.repebble.com/assets/images/guides/timeline/weather-layout~aplite.png) ![](https://developer.repebble.com/assets/images/guides/timeline/weather-layout~basalt.png) ![](https://developer.repebble.com/assets/images/guides/timeline/weather-layout~chalk.png) **Standard Attributes** **`title`** (part of the day), **`tinyIcon`** , `largeIcon`, `body` (shortcast), `lastUpdated`. **Special Attributes** | Field | Type | Function | | --- | --- | --- | | `shortTitle` | String | Used instead of `title` in the main timeline view unless it is not specified. | | `subtitle` | String | Show high/low temperatures. Note: currently only numbers and the degree symbol (°) are supported. | | `shortSubtitle` | String | Used instead of `subtitle` in the main timeline view unless it is not specified. | | **`locationName`** | String | Name of the location of this pin event. | | `displayTime` | String | Use a value of 'pin' to display the pin's time in title of the detail view and description, or 'none' to not show the time. Defaults to 'pin' if not specified. | **Color Elements** | Layout Property | Applies To | | --- | --- | | `primaryColor` | All text | | `backgroundColor` | Background | **Example JSON** ``` {"id": "pin-weather-1","time": "2015-03-18T19:00:00Z","layout": {"type": "weatherPin","title": "Nice day","subtitle": "40/65","tinyIcon": "system://images/TIMELINE\_SUN","largeIcon": "system://images/TIMELINE\_SUN","locationName": "Palo Alto","body": "Sunny with a chance of rain.","lastUpdated": "2015-03-18T18:00:00Z"}} ``` ### Generic Reminder Generic layout for pin reminders, which can be set at various times before an event is due to occur to remind the user ahead of time. #### aplite #### basalt #### chalk ![](https://developer.repebble.com/assets/images/guides/timeline/generic-reminder~aplite.png) ![](https://developer.repebble.com/assets/images/guides/timeline/generic-reminder~basalt.png) ![](https://developer.repebble.com/assets/images/guides/timeline/generic-reminder~chalk.png) **Standard Attributes** **`title`** , **`tinyIcon`**. **Special Attributes** | Field | Type | Function | | --- | --- | --- | | `locationName` | String | Name of the location of this pin event. | **Example JSON** ``` {"id": "pin-generic-reminder-1","time": "2015-03-18T23:00:00Z","layout": {"type": "genericPin","title": "This is a genericPin!","subtitle": "With a reminder!.","tinyIcon": "system://images/NOTIFICATION\_FLAG"},"reminders": [{"time": "2015-03-18T22:55:00Z","layout": {"type": "genericReminder","title": "Reminder!","locationName": "Conf Rm 1","tinyIcon": "system://images/ALARM\_CLOCK"}}]} ``` ### Generic Notification Generic notification layout which can be used with `createNotification` and `updateNotification` to alert the user to a new pin being created on their timeline. #### aplite #### basalt #### chalk ![](https://developer.repebble.com/assets/images/guides/timeline/generic-notification-layout~aplite.png) ![](https://developer.repebble.com/assets/images/guides/timeline/generic-notification-layout~basalt.png) ![](https://developer.repebble.com/assets/images/guides/timeline/generic-notification-layout~chalk.png) **Standard Attributes** **`title`** , **`tinyIcon`** , `body`. **Color Elements** | Layout Property | Applies To | | --- | --- | | `primaryColor` | Title | | `backgroundColor` | Banner background | **Example JSON** ``` {"id": "pin-generic-createmessage-1","time": "2015-04-30T23:45:00Z","layout": {"type": "genericPin","title": "This is a genericPin!","subtitle": "With a notification","tinyIcon": "system://images/NOTIFICATION\_FLAG"},"createNotification": {"layout": {"type": "genericNotification","title": "Notification!","tinyIcon": "system://images/NOTIFICATION\_FLAG","body": "A new genericPin has appeared!"}}} ``` ## Pin Actions Pins can be further customized by adding actions to them. This allows bi- directional interactivity for pin-based apps. These apps can have multiple actions associated with them, allowing different launch behavior depending on how the user interacts with the pin. The table below shows the available actions that can be added to a pin. Required attributes are shown in **bold**. | Action `type` | Description | Attributes | | --- | --- | --- | | `openWatchApp` | Launch the watchapp associated with this pin. The `launchCode` field of this action object will be passed to the watchapp and can be obtained with [`launch_get_args()`](https://developer.repebble.com/docs/c/Foundation/Launch_Reason.md#launch_get_args "launch\_get\_args"). | **`title`** , **`launchCode`**. | | `http` | Execute an HTTP request that invokes this action on the remote service. | See [_HTTP Actions_](#http-actions) for full attribute details. | ### Using a Launch Code Launch codes can be used to pass a single integer value from a specific timeline pin to the app associated with it when it is lauched from that pin. This mechanism allows the context to be given to the app to allow it to change behavior based on the action chosen. For example, a pin could have two actions associated with an app for making restaurant table reservations that allowed the user to cancel the reservation or review the restaurant. To set up these actions, add them to the pin when it is pushed to the timeline API. ``` "actions": [{"title": "Cancel Table","type": "openWatchApp","launchCode": 15},{"title": "Leave Rating","type": "openWatchApp","launchCode": 22}] ``` ### Reading the Launch Code When the user sees the pin and opens the action menu, they can select one of these actions which will launch the watchapp (as dictated by the `openWatchApp` pin action `type`). When the app launches, use [`launch_get_args()`](https://developer.repebble.com/docs/c/Foundation/Launch_Reason.md#launch_get_args "launch\_get\_args") to read the value of the `launchCode` associated with the chosen action, and react accordingly. An example is shown below; ``` if(launch\_reason() == APP\_LAUNCH\_TIMELINE\_ACTION) {uint32\_t arg = launch\_get\_args();switch(arg) {case LAUNCH\_ARG\_CANCEL:// Cancel table UI...break;case LAUNCH\_ARG\_REVIEW:// Leave a review UI...break;}} ``` ### HTTP Actions With the `http` pin action `type`, pins can include actions that carry out an arbitrary HTTP request. This makes it possible for a web service to be used purely by pushed pins with actions that respond to those events. The table below details the attributes of this type of pin action object. Items shown in **bold** are required. | Attribute | Type | Default | Description | | --- | --- | --- | --- | | **`title`** | String | _mandatory_ | The title of the action. | | **`url`** | String | _mandatory_ | The URL of the remote service to send the request to. | | `method` | String | `POST` | The request method, such as `GET`, `POST`, `PUT` or `DELETE`. | | `headers` | Object | `{}` | Dictionary of key-value pairs of headers (`Content-Type` is implied by using `bodyJSON`) as required by the remote service. | | `bodyText` | String | `''` | The data body of the request in String format. | | `bodyJSON` | Object | _unspecified_ | The data body of the request in JSON object format. | | `successText` | String | "Done!" | The string to display if the action is successful. | | `successIcon` | Pin Icon URL | `system://images/GENERIC_CONFIRMATION` | The icon to display if the action is successful. | | `failureText` | String | "Failed!" | The string to display if the action is unsuccessful. | | `failureIcon` | Pin Icon URL | `system://images/RESULT_FAILED` | The icon to display if the action is unsuccessful. | > Note: `bodyText` and `bodyJSON` are mutually exclusive fields (they cannot be used together in the same request). You should choose that which is most convenient for your implementation. > > Note: Do not include a body with HTTP methods that do not support one. This means that `bodyText` and `bodyJSON` cannot be used with `GET` or `DELETE` requests. The following is an example action, using the `http` action `type` to confirm attendance at a meeting managed by a fictitious meeting scheduling service. ``` "actions": [{"type": "http","title": "Confirm Meeting","url": "http://some-meeting-service.com/api/v1/meetings/46146717","method": "PUT","headers": {"X-Request-Source": "pebble-timeline","Content-Type": "application/x-www-form-urlencoded"},"bodyText": "type=confirm&value=1","successIcon": "system://images/GENERIC\_CONFIRMATION","successText": "Confirmed!"}] ``` Alternatively, pins can use the `bodyJSON` field to encode a JSON object. Include this data using the `bodyJSON` field. ``` "actions": [{"type": "http","title": "Confirm Meeting","url": "http://some-meeting-service.com/api/v1/meetings/46146717","method": "PUT","headers": {"X-Request-Source": "pebble-timeline"},"bodyJSON": {"type": "confirm","value": true},"successIcon": "system://images/GENERIC\_CONFIRMATION","successText": "Confirmed!"}] ``` ### Included Headers When using the `http` action, the request will also include the following additional headers. Developers can use these to personalize the timeline experience to each individual user. | Header Key | Value | | --- | --- | | `X-Pebble-Account-Token` | Same as [`Pebble.getAccountToken()`](https://developer.repebble.com/guides/communication/using-pebblekit-js.md) | | `X-Pebble-Watch-Token` | Same as [`Pebble.getWatchToken()`](https://developer.repebble.com/guides/communication/using-pebblekit-js.md) | ## Testing Pins It is also possible to push new timeline pins using the `pebble`[_Command Line Tool_](https://developer.repebble.com/guides/tools-and-resources/pebble-tool.md). Prepare your pin in a JSON file, such as `example-pin.json` shown below: ``` {"id": "pin-generic-1","time": "2015-03-18T15:45:00Z","layout": {"type": "genericPin","title": "This is a genericPin!","tinyIcon": "system://images/NOTIFICATION\_FLAG"}} ``` Push this pin to your emulator to preview how it will appear for users. ``` $ pebble insert-pin example-pin.json ``` The pin will appear as shown below: ![pin-preview](https://developer.repebble.com/assets/images/guides/timeline/generic-pin~basalt.png) It is possible to delete the pin in a similar manner, making sure the `id` is the same as the pin to be removed: ``` $ pebble delete-pin --id pin-generic-1 ``` --- # Local Pins Source: https://developer.repebble.com/guides/pebble-timeline/timeline-local-pins/ PebbleKit JS can insert pins into the timeline directly from the phone. Local pins are created by the Pebble mobile app and synced to the watch, which means: - No timeline token, API key, or appstore listing is needed, so sideloaded apps can use them. - Pins only exist on the phone that created them - they are not shared with the user's other phones, and cannot be created while your app's JS is not running. > The Pebble mobile app does not support the timeline web API, so pins can no longer be pushed to users from a web server. Local pins are the only way to add pins to the timeline from an app. ## Inserting a Pin Pass a pin object (or a JSON string) in the format described in [_Creating Pins_](https://developer.repebble.com/guides/pebble-timeline/pin-structure.md): ``` Pebble.insertTimelinePin({id: 'local-pin-1',time: new Date().toISOString(),layout: {type: 'genericPin',title: 'Local pin',tinyIcon: 'system://images/NOTIFICATION\_FLAG'}}); ``` The `id` is chosen by you and only needs to be unique within your app. Inserting a pin with an `id` that already exists updates that pin, rather than creating a second one. ## Deleting a Pin Pins are removed using the same `id` they were inserted with: ``` Pebble.deleteTimelinePin('local-pin-1'); ``` ## Existing Timeline Web API Code Apps written against the timeline web API do not need to be rewritten to keep working. Requests made from PebbleKit JS to `timeline-api.rebble.io` or `timeline-api.getpebble.com` under `/v1/user/pins` are intercepted by the phone and turned into local pins instead of being sent to a server - a `PUT` or `POST` inserts the pin in the request body, and a `DELETE` removes the pin named in the URL. The request completes with a `200` response and an empty body. Only requests made by your app's JS are intercepted. Pins your backend pushes to the timeline web API will never reach the watch. > Interception can be turned off by the user with the 'Emulate Timeline Webservice' setting in the Pebble mobile app, so new apps should call `Pebble.insertTimelinePin()` directly. ## Unsupported Fields Local pins use the pin format described in [_Creating Pins_](https://developer.repebble.com/guides/pebble-timeline/pin-structure.md), with the following exceptions: | Field | Behavior | | --- | --- | | `createNotification` | Ignored - no notification is shown when the pin is created. | | `updateNotification` | Ignored - no notification is shown when the pin is updated. | | `actions` | Ignored. Every local pin gets a 'Remove' action. | | `primaryColor`, `secondaryColor`, `backgroundColor` | Ignored. | All other fields, including `duration` and `reminders`, are supported. --- # Pebble Timeline Source: https://developer.repebble.com/guides/pebble-timeline/ The Pebble timeline is a system-level display of chronological events that apps can insert data into to deliver user-specific data, events, notifications and reminders. These items are called pins and are accessible outside the running app, but are deeply associated with an app the user has installed on their watch. Every user can view their personal list of pins from the main watchface by pressing Up for the past and Down for the future. The Pebble mobile app inserts pins for weather, calendar events and missed calls, and apps insert their own pins, such as sports scores or news items, from PebbleKit JS. ## Contents - [**Creating Pins**](https://developer.repebble.com/guides/pebble-timeline/pin-structure.md) - How to create timeline pins with reminders, actions, and layouts. - [**Local Pins**](https://developer.repebble.com/guides/pebble-timeline/timeline-local-pins.md) - How to insert pins into the timeline directly from PebbleKit JS, without a web server or timeline API keys. - [**Subscriptions and Tokens**](https://developer.repebble.com/guides/pebble-timeline/timeline-js.md) - The PebbleKit JS timeline subscription and token APIs, and what to use instead. ## Adding Pins Apps add pins from PebbleKit JS with `Pebble.insertTimelinePin()` and remove them with `Pebble.deleteTimelinePin()`. The Pebble mobile app creates the pin and syncs it to the watch. No timeline token, API key or appstore listing is needed, so sideloaded apps can add pins. See [_Local Pins_](https://developer.repebble.com/guides/pebble-timeline/timeline-local-pins.md) for the API and [_Creating Pins_](https://developer.repebble.com/guides/pebble-timeline/pin-structure.md) for the pin format. > Note: The timeline web API is no longer available. The Pebble mobile app does not sync pins from a server, and `Pebble.timelineSubscribe()`, `Pebble.timelineUnsubscribe()` and `Pebble.timelineSubscriptions()` do not work. Use [_local pins_](https://developer.repebble.com/guides/pebble-timeline/timeline-local-pins.md) instead. --- # Service Architecture Source: https://developer.repebble.com/guides/pebble-timeline/timeline-architecture/ > Note: The timeline web API is no longer available. The Pebble mobile app does not sync pins from a server. Use [_local pins_](https://developer.repebble.com/guides/pebble-timeline/timeline-local-pins.md) instead. Every item on the timeline is called a 'pin'. A pin can have information attached to it which is used to show it to the user, such as layout, title, start time and actions. When the user is viewing their timeline, they can use the information provided about the immediate past and future to make decisions about how they plan the rest of their day and how to respond to any missed notifications or events. The system and the Pebble mobile app populate the user's timeline with items such as calendar appointments, missed calls and weather. Apps add their own pins from PebbleKit JS. For example, a sports app could show a pin representing an upcoming match so the user can see how long remains until it starts. Pins are created on the phone and sent to the watch. There is no server in the path, so no timeline token, API key or appstore listing is needed. The pin format is described in [_Creating Pins_](https://developer.repebble.com/guides/pebble-timeline/pin-structure.md) and the PebbleKit JS API in [_Local Pins_](https://developer.repebble.com/guides/pebble-timeline/timeline-local-pins.md). ## Architecture Overview ![diagram](https://developer.repebble.com/assets/images/guides/3.0/timeline-architecture.png) The diagram shows the original timeline service. The 'Pebble Timeline API' cloud service and the developer's server no longer exist; today pins travel only between the phone and the watch. ### Watch Timeline The watch stores the pins and shows them when the user presses Up for the past or Down for the future from the watchface. Alarm pins are inserted directly on the watch. Pins carry their own layout, reminders and actions, so the watch does not need to contact the phone to display them. ### Pebble Mobile App The Pebble mobile app inserts system pins for the user's upcoming calendar events, missed calls and weather, and syncs them to the watch. It also runs the PebbleKit JS component of each installed app and forwards the pins that component inserts. ## Sources of Pins ### System Pins Calendar, missed call and weather pins are created by the Pebble mobile app. Alarm pins are created by the watch. Apps cannot change these pins. ### Local Pins Apps insert pins from PebbleKit JS with `Pebble.insertTimelinePin()` and remove them with `Pebble.deleteTimelinePin()`. Each pin is created on the phone that runs the app's JavaScript, so pins are not shared with the user's other phones and are only inserted while the app's JavaScript is running. Developers can use PebbleKit JS to combine the user's preferences from a configuration page (detailed in [_App Configuration_](https://developer.repebble.com/guides/user-interfaces/app-configuration.md)) with their location and data from web services to decide which pins to insert. --- # Subscriptions and Tokens Source: https://developer.repebble.com/guides/pebble-timeline/timeline-js/ > Note: The timeline web API is no longer available. The Pebble mobile app does not sync pins from a server, and `Pebble.timelineSubscribe()`, `Pebble.timelineUnsubscribe()` and `Pebble.timelineSubscriptions()` do not work. Use [_local pins_](https://developer.repebble.com/guides/pebble-timeline/timeline-local-pins.md) instead. `Pebble.getTimelineToken()` still returns a token: the appstore token for an installed app, or a placeholder for a sideloaded app while the _Emulate Timeline Webservice_ setting is on (the default). The token cannot be used to push pins, since there is no server to push them to. See [_Using PebbleKit JS_](https://developer.repebble.com/guides/communication/using-pebblekit-js.md#account-token) for the tokens that identify a user or a watch. Apps add pins from PebbleKit JS with `Pebble.insertTimelinePin()` and remove them with `Pebble.deleteTimelinePin()`. See [_Local Pins_](https://developer.repebble.com/guides/pebble-timeline/timeline-local-pins.md) for the API and [_Creating Pins_](https://developer.repebble.com/guides/pebble-timeline/pin-structure.md) for the pin format. --- # Pebble Plugins Preview Source: https://developer.repebble.com/guides/plugins/preview/ **Notice** This page is generated from [`plugins.md`](https://github.com/coredevices/mobileapp/blob/ec2156cc38a850e5458addba2a2fb8efa34396df/test-apps/plugins.md) in the [Pebble mobile app repository](https://github.com/coredevices/mobileapp) at commit [ec2156cc3](https://github.com/coredevices/mobileapp/commit/ec2156cc38a850e5458addba2a2fb8efa34396df). Relative paths refer to that directory. Changes belong in that repository. Plugins are nearly here (finally). A plugin is a small piece of JavaScript that runs on the phone and: - **provides data** : (_sources_) that any Pebble watchface/app (and soon Index) can read - **takes actions** : turn a light off, add a task, that a watchface/app (and soon Index) can trigger It has a generic API, designed to allow any type of data to be exposed, in multiple formats. This doc lives here while plugins are under active development, and will move once they're stable. Right now they're supported in the Pebble mobile app (v1.14.0), but are experimental and disabled by default. ### Why? - A watchface that shows the weather shouldn't need its own weather API and key. The Pebble app already fetches weather for the user's saved locations; as a plugin, any app can display that. - If you have an interesting data source (say, a blood sugar monitor, or next-bus data), users can show it on whichever watchface they like instead of the one design you made. - Plugins let us expose data the Pebble app has but developers couldn't get at before very easily: music, notifications, phone state, and more. - Actions let a watch (and, later, Index) control things like lights or tasks through a plugin, without every app integrating with every service. - Once Index can use plugins, adding a to-do list service (like the Todoist, TickTick and Notion demos) is just a plugin, with no change to the Pebble app. ## What can I try now? - Try the example plugins, watchface and app. - Build a watchface or app that uses plugin data. - Build a plugin. First, enable plugins in the mobile app (v1.14.0 or above): **Settings → Debug → Show debug options** , then **Use experimental plugins**. The demo apps below are installed automatically, and a **Plugins** tab appears in the Apps screen listing plugins and their settings pages. ### Try the demo apps and plugins Source for each is in this folder: - **`weather-face`** ("Plugin Demo: Weather Face"): a watchface built entirely from a weather plugin: any installed one (the built-in one, `met-weather`, or yours), picked on its settings page. - **`met-weather`** : weather for the phone's current location from MET Norway (api.met.no), an alternative to the built-in weather plugin. Shows `navigator.geolocation` and drawing condition images in JS. - **`plugin-test`** ("Plugin Demo: Dashboard"): a watchapp with eight tiles, each set from its settings page to any reading from any installed plugin. This shows what data is available through the example/built-in plugins. You can also configure taps to take actions, or make a tile a button for any action that takes no arguments (e.g. "Back", the default, goes to the previous app). A tile can also show the date, from the watch's own clock. - **`stocks`** : share prices for tickers you pick on its settings page. - **`hue`** : Control Philips Hue lights on your local bridge (pair from its settings page). - **`notion`** , **`ticktick`** , **`todoist`** : examples of plugins that sign in with OAuth. ### What can't I do yet? **Don't ship anything that uses/offers plugins to the app store yet.** The API is unstable and will change before plugins are released to everyone. We want your feedback on it first. See [Future](#future) for what's planned but not built yet. ## Concepts ### Sources and actions A **source** is something an app can read from a plugin, addressed in layers: ``` category home item room_lights the kind of thing instance "3" one particular room (its name property: "Living room") property brightness one reading of it shape numericValue -> { value: 64, unit: "%", min: 0, max: 100 } ``` You can subscribe to an _item_ and get every property of every instance of it, and updates when they change. Instances have a stable `instanceId`; `name` is the property to display for one. An **action** is something a plugin can do (`set_on`, `add_task`). It takes JSON arguments described by a JSON Schema in the manifest, and can say which sources it affects (`targets`), so a tile showing a light knows which actions apply to it. ### Categories Apps ask for a category and item, not a plugin (`weather/location`, not "AcmeWeather"). The host serves it from whichever installed plugin provides it. Category, item and property names are free-form, so a plugin can offer any kind of source or action. But swapping one plugin for another only works if plugins serving the same category use the same names. So if an existing category fits, like weather, match its item and property names rather than inventing your own. We'll publish the names/shapes for common categories. Examples now: | Category | Items | From | | --- | --- | --- | | `weather` | `location`, `hour` | built-in | | `calendar` | `event` | built-in | | `phone` | `phone_state` | built-in | | `music` | `track` | built-in (Android) | | `notifications` | `notification` | built-in (Android) | | `watch` | `switch_setting`, `number_setting`, `text_setting`, `app_setting` | built-in | | `home` | `home_lights`, `room_lights`, `light` | Hue demo | | `finance` | `stock` | Stocks demo | | `notes` | `destination`, `configured` | Notion demo | | `reminders` | `list`, `configured` | Todoist, TickTick demos | A plugin is offered as a notes / reminders destination for the Index agent when it declares a `create_note` / `create_reminder` action _and_ serves `notes/configured` / `reminders/configured`: one instance whose `configured` property is a `boolean` that is true once the user has signed in and set everything the action needs. While it reads false the destination shows a Connect button that opens the plugin's settings page. An action without its `configured` source is not offered. The built-in Watch State plugin has no sources, only actions, both needing `LaunchApps`: `launch_app` (`{ uuid }`) opens an app or watchface in the user's locker, and `launch_previous_app` goes back to whatever ran before the current app. ### Shapes A property isn't typed as "a number" or "a string". It offers one or more **shapes** : fixed types that a consuming app can be confident of being able to render. The plugin sends every shape it can, and you pick one. | Shape | For | Payload | | --- | --- | --- | | `shortText` | up to ~7 characters | `{ text: "72°F" }` | | `longText` | a line of text | `{ text: "Partly cloudy, breezy" }` | | `numericValue` | a number, with a range if it has one | `{ value: 84, unit: "%", min: 0, max: 100 }` | | `timestamp` | a moment, in epoch seconds UTC | `{ value: 1716940800 }` | | `boolean` | on/off state | `{ value: true }` | | `icon` | a monochrome glyph | `{ pixels, palette, width, height }` | | `image` | a colour bitmap | `{ pixels, palette, width, height }` | - Some shapes carry optional metadata (unit, min, max) so an app can render the data in context. - `icon` and `image` are 4-bpp palettised bitmaps (base64), only produced if you ask for them with `iconPixelSize`. A missing property or shape means the plugin can't serve it right now. Check before drawing. Shapes are designed so you can build for a _slot in your design_ rather than for specific data. A face with a gauge widget can offer the user anything that has a `numericValue` with a range: battery, UV index, a light's brightness. A small text slot can take any `shortText`, whatever it came from. An `icon` suits a small monochrome mark beside a reading, and an `image` suits a full-colour one. ### Permissions Plugins introduce a fine-grained permission system, covering your plugin and your app's PKJS. This ships in v1.15 of the mobile app, and is only in place when plugins are enabled. #### Declaring Declare what your app and its plugin need once, in the `pebble` block of `package.json`: ``` "pebble": {"usesPermissions": ["Location","LocalNetwork",{ "name": "Internet", "parameters": { "domains": ["api.example.com"] } }]} ``` The built-in permissions are `Internet`, `LocalNetwork`, `Location`, `Notifications`, `Calendar`, `Contacts`, `Timeline`, `ReadWatchSettings`/`WriteWatchSettings`, `ReadMusic`/`ControlMusic`, `LaunchApps`, `LegacyPebbleKit` and `Health`. - Each `Internet` domain is its own permission and covers its subdomains. `LocalNetwork` covers devices on the LAN. - `LegacyPebbleKit` lets your watchapp talk to a companion app over PebbleKit Classic. A PebbleKit 2 companion doesn't need declaring: each package your appinfo lists is asked about on its own. - `Health` isn't enforced yet: health data is read on the watch, so enforcing it needs permissions synced to the watch, which isn't built yet. Declare it anyway if you use health data. The [packaging script](#packaging-and-sideloading) marks the pbw as using permissions (`manifestVersion: 2` in its appinfo). Without that it's a legacy app, which keeps what apps had before permissions: internet access, and what its `capabilities` declare. Sources and actions guard themselves with **caller permissions** : what an app must hold to use them. `calendar/event` needs `Calendar`, `notifications/notification` needs `Notifications`, `weather/*` needs `Location`. A plugin can also define its own permissions, to guard its sources and actions: ``` "plugin": {"definesPermissions": [{ "name": "HomeControl", "label": "Control your lights", "kind": "write" }],"actions": [{ "name": "set\_on", "callerPermissions": ["HomeControl"], ... }]} ``` `kind` is `read`, `write` or `exfiltrating`, and decides when users are asked, as for a built-in permission. The name is scoped to your plugin, and users see your label with where it came from ("Control your lights (via Hue)"). Actions get one more check: if your plugin can send data off the phone, an app invoking its actions needs the user's permission for that too (`InvokeAction`), since the arguments go wherever your plugin can send them. To use another plugin, declare its permissions in your `usesPermissions` with its uuid, so users can approve them when installing your app (if that plugin is already installed): ``` "usesPermissions": [{ "name": "HomeControl", "plugin": "\" },{ "name": "InvokeAction", "plugin": "\" }] ``` #### Approval Network access and companion apps let data leave the phone, so they're what make reading someone's data risky. Those, and permissions that change things (`Timeline`, `WriteWatchSettings`, `ControlMusic`, `LaunchApps`), need the user's approval unless they've set apps to never ask. Reading is up to a setting; by default: - **An app that can send data off the phone** has the user approve each permission: the ones it declares when it's installed, and anything else (an undeclared permission, or a new domain) when it first uses it. - **Any other app** can read without asking, since the data can't go anywhere. While your app is running (or, for a plugin, while something reads from it), a request shows the user a notification and a card in the Pebble app. Until they answer, it fails with `PERMISSION_DENIED` (a blocked `fetch` rejects like a network error), possibly after waiting a few seconds. Try again later rather than treating it as permanent. Users can change any permission on the app's page, and choose in settings when apps have to ask. Declare what you need up front, and keep `Internet` to the domains you use: fewer prompts, and users can trust you. An app holding `Location` whose only network access is `api.openweathermap.org` can't send the user's location anywhere else. ## Writing a watchface or app that uses plugins Your app uses plugins from its PKJS (`pebble-js-app.js`). ### Three ways to use plugins 1. **You know exactly what you want.** Hard-code the category, item and properties, as the `weather-face` demo does with `weather/location`. Whichever weather plugin the user has, the face works. It uses the condition `image` the plugin provides - but a face that wants its own artwork could read the `condition_code` property instead (`sun`, `partly_cloudy`, `heavy_rain`, ...) and draw its own. 2. **Let the user choose.** Use `Pebble.enumeratePlugins()` to see everything available, and a settings page for the user to pick what goes where, as the `plugin-test` dashboard does. Filter by the shapes your design can draw (see [Shapes](#shapes)). 3. **Use what the user already prefers** _(not built yet)_. Users will pick their favourite readings in the Pebble app: e.g. a user might select weather for home location first, then phone battery, then the Google stock price. An API will expose that ordered list, so any face with widget slots can display those items with no configuration required. **Reading a source:** ``` const subscription = Pebble.subscribeToSource({category: "weather",item: "location",properties: ["name", "temperature", "condition"],// optional hint; omit for everythingiconPixelSize: { w: 48, h: 48 },// optional; ask only if you want bitmapsonData: ({ instances }) =\> {const here = instances[0];// first saved location (can be multiple if supportsMultiple==true)if (!here) return;const p = here.properties;send(p.name.shortText.text, p.temperature.shortText.text);},onError: (err) =\> console.log(err.code, err.message),});subscription.unsubscribe();// or let it end with the app ``` Every update is an envelope: ``` {pluginUuid: "...",validUntilMs: 1716940000000,instances: [{instanceId: "3",properties: {name:{ shortText: { text: "Kitchen" } },brightness: { numericValue: { value: 64, unit: "%", min: 0, max: 100 },shortText:{ text: "64%" } },},},],} ``` **Reading from a specific plugin:** pass `plugin: ""`, for example your own pbw's plugin. This is a preference. If that plugin isn't installed or doesn't serve the item, another plugin serving it is used. Check `pluginUuid` on the envelope if you need to know which one answered. **Invoking an action** : `plugin` is required here, and never falls back to another plugin: ``` const result = await Pebble.invokeAction({plugin: "\",action: "set\_on",args: { item: "light", instanceId: "3", on: false },});// { ok: true, text: "Kitchen off.", refreshed: ["home/light"] }// { ok: false, code: "AUTH\_REQUIRED", message: "..." } ``` **Discovering what's installed:** `Pebble.enumeratePlugins()` returns every installed plugin with its sources (items, properties and shapes) and actions. Error codes: `PLUGIN_UNAVAILABLE`, `PERMISSION_DENIED`, `RATE_LIMITED`, `TIMEOUT`, `INVALID_REQUEST`, `UNKNOWN`, plus `AUTH_REQUIRED` and `INVALID_ARGS` for actions. ## Writing a plugin A plugin ships in a `.pbw`: either alongside a watchapp/watchface, or on its own as a **plugin-only** pbw (no watch binary). It uses the pbw's uuid and name. The simplest example to copy is `stocks/`. **If your app fetches data from a service and displays it, consider splitting it into a plugin plus the app, in one pbw.** The app reads from its own plugin (pass your pbw's uuid as `plugin`), and behaves the same as it does now. But the data is also available to every other watchface and app, and later to Index - and users can show it in a design other than yours. ### The manifest Add a `plugin` block to the `pebble` section of `package.json`: ``` "pebble": {"displayName": "Stocks","uuid": "2c4b7f10-9a3d-4e6b-8f21-5d0c7e9a1b34","sdkVersion": "3","configPage": "config.html",// optional settings page (a file in the pbw, or a URL)"usesPermissions": [{ "name": "Internet", "parameters": { "domains": ["query1.finance.yahoo.com"] } }],"plugin": {"description": "Share prices for the tickers the user picked.",// also read by index MCP"script": "plugin.js","sources": [{"category": "finance","items": ["stock"],"properties": {"name":["shortText"],"price": ["shortText", "longText", "numericValue"]},"supportsMultiple": true,"suggestedRefreshIntervalSec": 60}],"actions": [{"name": "add\_stock",// illustrative; the real Stocks plugin has no actions"description": "Start tracking a ticker symbol.","parameters": {// JSON Schema"type": "object","properties": { "symbol": { "type": "string" } },"required": ["symbol"]},"targets": ["finance/stock"]}]}} ``` ### The script A plugin only runs when it's needed: for a subscription, an action, or while its settings page is open. It doesn't stay running in the background. While an app is subscribed, the plugin is called regularly for fresh data (currently every `suggestedRefreshIntervalSec`, but this may change). ``` // plugin.jsPebble.registerSourceHandler(async (request, respond) =\> {// request: { category, item, properties?, iconPixelSize? }if (request.category === "finance" && request.item === "stock") {const quotes = await loadQuotes();respond.data({validUntilMs: Date.now() + 60\_000,instances: quotes.map((q) =\> ({instanceId: q.symbol,properties: {name:{ shortText: { text: q.name } },price: { shortText: { text: `$${q.price}` }, numericValue: { value: q.price, unit: "$" } },},})),});return;}respond.error("PLUGIN\_UNAVAILABLE");});Pebble.registerActionHandler(async (request, respond) =\> {// request: { action, args }try {await addStock(request.args.symbol);respond.ok({ text: "Added.", refreshed: ["finance/stock"] });} catch (e) {respond.error("UNKNOWN", e.message);}}); ``` What a plugin has to work with: - **`fetch`** for HTTP, to hosts the user has allowed (see [Approval](#approval)). A blocked host rejects like a network error. Plain-HTTP LAN devices are reachable with `LocalNetwork`. - **`WebSocket`** under the same rules: a blocked host fires `error` then `close`. Binary frames arrive as `ArrayBuffer`. Sockets are closed when the session ends, so one only lives for the request that opened it. These aren't intended for long-running connections. - **`navigator.geolocation`** (`getCurrentPosition`, `watchPosition`, `clearWatch`), as in PKJS. Needs `Location`; refused, it calls your error callback. Watches end with the session. - **`localStorage`** for state, shared with your app's PKJS. A plugin is started fresh for each request, so don't keep state in variables. - **`Pebble.secureStorage`** for credentials (tokens, API keys): `await getItem(key)`, `setItem(key, value)` and `removeItem(key)`, encrypted by the phone's keystore. Private to your plugin (PKJS can't read it) and deleted when the app is removed. - **`Pebble.refreshSources(["finance/stock"])`** when data changes outside a request (for example, the user added a ticker on the settings page). - **`Pebble.oauth`** for sign-in with OAuth providers, without the plugin ever holding a client secret. Declare the connector in the manifest (`"oauth": { "todoist": {} }`), then `await Pebble.oauth.authorize("todoist")` for a token and `Pebble.oauth.refresh("todoist", broker_refresh)` to renew it; keep both in `Pebble.secureStorage`. The connector has to be registered with the Pebble app store (see the `todoist/` example). - Nothing else: unlike PKJS, a plugin has no WebView APIs. Return every property and shape you can; text shapes cost nothing once you have the data, and the app picks. Only produce `icon`/`image` when the request has `iconPixelSize`. ### Settings pages Until now, a PKJS settings page had to be hosted somewhere (or generated with Clay), and could only pass data through its URL + when it closed. Settings pages can now be bundled in the pbw, and can message your plugin/PKJS code in real-time while it's open. Set `configPage` in `package.json` to an HTML file in the project (bundled into the pbw) or a URL. If it's set, it's used instead of the legacy `showConfiguration` flow. ``` // in the pagePebble.addEventListener("ready", (e) =\> { /\* e.target ('plugin' or 'pkjs') is up \*/ });const reply = await Pebble.sendMessage("plugin", { type: "listTickers" });// in plugin.jsPebble.registerConfigHandler((message, respond) =\> respond({ tickers: tickers() }));// in pebble-js-app.jsPebble.addEventListener("configmessage", (e) =\> e.respond({ theme: theme() })); ``` Wait for `ready` before sending: the plugin is started when the page opens. The `plugin-test` dashboard shows what this makes possible: its settings page asks the app's PKJS for the catalogue of everything installed (built from `enumeratePlugins`) to fill its dropdowns. Each change is sent straight to PKJS, so the watch updates as the user edits, with no save button. PKJS pushes the live readings back, so each option is labelled with what it currently reads (for example "temperature: 21°C"), and each tile shows its current value. ### Packaging and sideloading The standard `pebble build` doesn't put `plugin.js` or `config.html` in the pbw (or support plugin-only PBWs), so use [`scripts/pack-plugin-pbw.py`](https://github.com/coredevices/mobileapp/blob/ec2156cc38a850e5458addba2a2fb8efa34396df/scripts/pack-plugin-pbw.py): ``` scripts/pack-plugin-pbw.py path/to/my-plugin ``` It works out what to do from the project. If there's a watchapp (`src/c`), it runs `pebble build`, then adds the plugin files. Otherwise it builds a plugin-only pbw directly. Either way it marks the pbw as using [permissions](#declaring). Sideloading plugins is supported from v1.14.0 of the mobile app - but the manifest format changed in v1.15.0 (along with several other parts of the API). ## Future We're planning: - Index integration (plugins as MCP tools). - Tooling support (the Pebble tool, CloudPebble). - Permission metadata in the app store, so users can review an app's permissions before adding it. - Choosing favourite readings in the app, and a preferred plugin when several serve the same thing (e.g. weather), plus an API to read those choices. - More extensive documentation. - A watch-side (C / Alloy) API, so apps can use plugins without any phone-side JS. - A way to cache images on the watch (see the Weather demo face which needs a phone connection to display images after restarting, right now) - PebbleKit-Android plugin support. - Plugins calling other plugins. - App store support, including a Plugins section for discovery. - More built-in plugins exposing more things. --- # Plugins (Preview) Source: https://developer.repebble.com/guides/plugins/ A plugin is JavaScript in a `.pbw` that runs on the phone, inside the Pebble mobile app, and offers two things to other apps: _sources_, data that any watchface or watchapp can read, and _actions_, operations such as turning a light off or adding a task that any watchapp can trigger. A watchface that shows the weather no longer needs its own weather API and key; it subscribes to `weather/location` and whichever weather plugin the user has installed answers. The Pebble mobile app exposes some of its own data as built-in plugins, and the [Index 01](https://developer.repebble.com/index-01.md) assistant will call plugin actions as tools. **Important** Plugins are a preview. The API will change before plugins are released to all users, and the Pebble mobile app ships with them turned off. Do not publish apps that use or provide plugins to the appstore yet. The documentation is maintained alongside the implementation and the example plugins in the [Pebble mobile app repository](https://github.com/coredevices/mobileapp/tree/master/test-apps), and is reproduced here. ## Contents - [**Pebble Plugins Preview**](https://developer.repebble.com/guides/plugins/preview.md) - What plugins are, how to try the preview in the Pebble mobile app, how a watchapp reads plugin data from PebbleKit JS, and how to write a plugin. --- # Building with AI Coding Agents Source: https://developer.repebble.com/guides/ai-agents/ AI coding agents such as Claude Code, Cursor and Codex can write Pebble apps when they can read the SDK documentation, know how the project is set up and can run the result in the emulator. This guide describes what the developer site and the `pebble` tool provide for each of these. ## Building a Watchface with an Agent The quickest way to see what an agent can do is to point it at the [Pebble agent skill](#the-pebble-agent-skill) and ask for a watchface. With the Pebble SDK installed, start Claude Code, Codex or Cursor in an empty directory and use a prompt like this: ``` Install the Pebble agent skill from https://github.com/coredevices/pebble-watchface-agent-skill (follow its README), then use it to build a watchface for Pebble Time 2 that shows the time in large digits with the date underneath. Build it, install it in the emulator and show me a screenshot. ``` The agent clones the skill, copies it into the project, then creates the watchface, runs `pebble build`, installs the result in the emulator with `pebble install --emulator emery`, takes a screenshot with `pebble screenshot` and compares it with the request before reporting back. Ask for changes in the same session ("make the digits bold", "add a battery meter") and it repeats the build and screenshot. To install on a watch, see [_Developer Connection_](https://developer.repebble.com/guides/tools-and-resources/developer-connection.md). ## Documentation for Agents Most guide and reference pages on this site have a Markdown version. Replace the trailing `/` or `/index.html` of the page URL with `.md`. For example, the Markdown version of `https://developer.repebble.com/guides/events-and-services/buttons/` is `https://developer.repebble.com/guides/events-and-services/buttons.md`. The Markdown version has no navigation or scripts, and its links point to the Markdown versions of the other pages. [`/llms.txt`](https://developer.repebble.com/llms.txt) is an index of every page with a one-line description, grouped by section. It starts with a short block that states the current SDK version, the target platforms and what the SDK no longer supports. Give an agent this URL and tell it to read the `.md` version of any page it needs. Larger files are available for agents that read a whole section at once: | File | Contents | | --- | --- | | [`/llms-full.txt`](https://developer.repebble.com/llms-full.txt) | Every page | | [`/guides/llms-full.txt`](https://developer.repebble.com/guides/llms-full.txt) | All guides | | [`/guides/alloy/llms-full.txt`](https://developer.repebble.com/guides/alloy/llms-full.txt) | The Alloy guides | | [`/docs/c/llms-full.txt`](https://developer.repebble.com/docs/c/llms-full.txt) | The C API reference | | [`/docs/pebblekit-js/llms-full.txt`](https://developer.repebble.com/docs/pebblekit-js/llms-full.txt) | The PebbleKit JS reference | [`/api-index.json`](https://developer.repebble.com/api-index.json) lists every C and PebbleKit JS symbol with its kind, signature, brief description, the platforms it is available on and the page that documents it. Agents can look up a function there instead of searching the reference pages. Add a line like the following to a prompt or an instruction file. For example: ``` Pebble SDK docs: https://developer.repebble.com/llms.txt. Fetch the .md version of any page before using an API. ``` ## Agent Instruction Files Most agents read an instruction file from the project directory before they start. `pebble new-project` writes these files when given the `--ai` flag: ``` $ pebble new-project --ai myproject ``` The command writes `CLAUDE.md` for Claude Code and `.cursor/rules/pebble.mdc` for Cursor from the same template. The files describe the project layout, the target platforms, the build, install and screenshot commands, how to use the emulator without a display, and where the documentation is. The flag also works with `--alloy` for an Alloy project. To add the files to an existing project, run the command in another directory and copy the files into the project. Other agents read other file names. The content is the same, so copy `CLAUDE.md` to the name the agent expects and edit the parts that describe the project. ## The Pebble Agent Skill [pebble-watchface-agent-skill](https://github.com/coredevices/pebble-watchface-agent-skill) is a skill for Claude Code that builds complete watchfaces and watchapps from a description. It chooses between a watchface and a watchapp and between C and Alloy, writes the project files, runs `pebble build`, installs the result in the emulator, takes a screenshot, checks the screenshot against the request and repeats until it matches. It then generates the app icons and preview images, and can publish the app with `pebble publish`. The skill targets emery (Pebble Time 2) by default and supports the other platforms as a second pass. Alloy projects are limited to emery and gabbro. To install it, clone the repository and follow its README, which lists the agents it supports. ## Working with the Emulator An agent can check its own work when it can install the app and see the screen. These commands do that without a physical watch: ``` $ pebble build$ pebble install --emulator emery$ pebble screenshot --no-open --emulator emery screenshot.png$ pebble emu-button click select --emulator emery$ pebble logs --emulator emery ``` `pebble screenshot --no-open` saves the image without opening it in a viewer, so the agent can read the file. `pebble emu-button` presses `back`, `up`, `select` or `down`; use `--duration` for a long press and `--repeat` to scroll. `pebble logs` shows `APP_LOG()` output from the running app. In an environment without a display, such as a container or CI, add `--vnc` to every command that uses `--emulator`. If the emulator gets into a strange state, such as a screenshot that shows a different app or an install that times out, `pebble kill` stops it and `pebble wipe` resets its storage; then install again. > Note: `pebble screenshot` does not create directories. Create the output directory before taking a screenshot into it. ## Target Platforms and the SDK Version State the SDK version and the target platforms in the instruction file or the prompt, together with the following: - The target platforms, as listed in `targetPlatforms` in `package.json`. The current platforms are aplite, basalt, chalk, diorite, emery, flint and gabbro. See [_Hardware Information_](https://developer.repebble.com/guides/tools-and-resources/hardware-information.md) for screen sizes and colors, and [_Building for Every Pebble_](https://developer.repebble.com/guides/best-practices/building-for-every-pebble.md) for the compile-time defines. - The SDK version. The current version is 4.33.1; `pebble sdk list` lists the available SDK versions. - Alloy runs on emery and gabbro only. A project that targets older watches has to be written in C. - The timeline web API is no longer available. The Pebble mobile app does not sync pins from a server. Use [_local pins_](https://developer.repebble.com/guides/pebble-timeline/timeline-local-pins.md) instead. --- # Preparing a Submission Source: https://developer.repebble.com/guides/appstore-publishing/preparing-a-submission/ An appstore listing is built from the `.pbw` file, some text fields and a set of images. This guide lists what is required so that everything is ready before the submission starts. All three publishing routes described in [_Publishing an App_](https://developer.repebble.com/guides/appstore-publishing/publishing-an-app.md) use the same listing fields and the same image sizes. ## The `.pbw` File The appstore reads the app's type (watchface or watchapp), UUID, version and target platforms from the `.pbw` file. These values come from `package.json`, so check them before building the release: - `watchapp.watchface` decides whether the listing is a watchface or a watchapp. This cannot be changed in the Developer Dashboard. - `uuid` must be unique across the whole appstore. If another app already uses it, the submission is rejected. Each app keeps its UUID for all later releases, since the UUID is how a new release is matched to the existing listing. - `version` must contain only numbers and dots, and each segment must be 255 or less. Each release of an app must have a version that has not been used before. - `targetPlatforms` decides which platforms the `.pbw` contains binaries for. The appstore reads the platform list from `targetPlatforms` when it is present and otherwise from the binaries in the `.pbw`, and offers each binary to the watches that can run it: an aplite binary also serves basalt, diorite, flint and emery, a basalt binary also serves emery, a diorite binary also serves flint and emery, and a chalk binary also serves gabbro. Build for the newer platforms directly to use their full display. Read [_App Metadata_](https://developer.repebble.com/guides/tools-and-resources/app-metadata.md) for details. The `.pbw` file can be at most 4.4 MB. ## Listing Fields | Field | Watchface | Watchapp | Notes | | --- | --- | --- | --- | | App name | Required | Required | Defaults to the name in the `.pbw`. | | Description | Required | Required | Plain text. | | Category | - | Required | One of Daily, Tools & Utilities, Notifications, Remotes, Health & Fitness, Games. Watchfaces are listed under Faces automatically. | | Website URL | Optional | Optional | | | Source code URL | Optional | Optional | | | Release notes | Optional | Optional | Per release. | | Companion app | - | Optional | Name, Play Store URL and a 144×144 icon for an Android companion app. | > Note: The appstore does not support the timeline web API. Apps that use [_local pins_](https://developer.repebble.com/guides/pebble-timeline/timeline-local-pins.md) work as normal. ## Images All images are PNG, JPEG or GIF, at most 4.4 MB each. GIFs keep their animation. Screenshots must have exactly the dimensions of the platform's display and must not be framed in a watch outline. | Image | Size | Notes | | --- | --- | --- | | Screenshots for aplite, basalt, diorite, flint | 144×168 | Up to 5 per platform. | | Screenshots for chalk | 180×180 | Up to 5. | | Screenshots for emery | 200×228 | Up to 5. | | Screenshots for gabbro | 260×260 | Up to 5. | | Banner | 720×320 | Optional. One per platform. Shown at the top of the listing. | | Small icon | 80×80 | Watchapps only. | | Large icon | 144×144 | Watchapps only. | Icons are optional. When one or both icons are missing, the appstore generates them from the app name. Screenshots can be taken from the emulator with the `pebble` tool. The `--all-platforms` and `--gif-all-platforms` flags of `pebble screenshot` save one image per target platform under `screenshots/` in the project directory, already at the correct size: ``` $ pebble screenshot --all-platforms ``` Read [_pebble screenshot_](https://developer.repebble.com/guides/tools-and-resources/pebble-tool.md#screenshot) for the other options. ## Visibility Every app has one of three visibility settings. It is chosen when the app is submitted and can be changed at any time from the Developer Dashboard. | Setting | Search and browsing | Direct link and install | | --- | --- | --- | | Listed | Yes | Yes | | Unlisted | No | Yes | | Hidden | No | No | Apps submitted from the Developer Dashboard, the `pebble` tool or CloudPebble start as Listed. Use Unlisted for a beta that is shared by link only, and Hidden to take an app offline. Hiding an app does not remove it from watches where it is already installed, and cached appstore pages can take up to ten minutes to disappear. Visibility applies to the app as a whole. Each release also has its own published or draft state, and only published releases can be installed. --- # Publishing an App Source: https://developer.repebble.com/guides/appstore-publishing/publishing-an-app/ There are three ways to put an app in the Pebble appstore. The [Developer Dashboard](https://developer.repebble.com/dashboard) is a web form and is the only place where a listing can be edited after it is created. The `pebble` tool and CloudPebble build the app, capture screenshots and upload the result in one step. All three create the same listing, and an app submitted from one can be updated from another. Before starting, read [_Preparing a Submission_](https://developer.repebble.com/guides/appstore-publishing/preparing-a-submission.md) for the fields and image sizes that are required. Apps go live as soon as they are submitted. There is no review queue. Published listings can be browsed at [apps.repebble.com](https://apps.repebble.com) to see how other developers present their apps. ## Signing In The Developer Dashboard, the `pebble` tool and CloudPebble all use the same Pebble account. Sign in with Google, GitHub or Apple. A developer account is created on first sign-in. ![](https://developer.repebble.com/assets/images/guides/appstore-publishing/dashboard-signin.png) Use the same sign-in method each time. If the same email address was already used with a different method, the dashboard asks for a sign-in with the original method and then links the new one to the account. ## Using the Developer Dashboard ### Submitting a New App - Open the [Developer Dashboard](https://developer.repebble.com/dashboard) and click _New_. - Upload the `.pbw` file. The dashboard reads the app type, UUID, version and target platforms from it. Check the _Version Number_ and add _Release Notes_ if wanted. - Enter the _App Name_ and _Description_. For a watchapp, choose a _Category_. - For a watchapp, upload the _Small Icon_ and _Large Icon_, or leave them empty to have them generated. - Upload up to five screenshots for each target platform, and optionally a _Banner Image_ per platform. Screenshots are optional in the dashboard, but listings without them show nothing in the appstore apart from the icon and the text. - Add the _Website URL_, _Source Code URL_ and an Android companion app if there is one. - Choose the _App visibility_ and click _Submit App_. The app appears in the dashboard's app list with a public link of the form `https://apps.repebble.com/`. Share this link, or search for the app in the Pebble mobile app. ### Adding a New Release - Open the app in the dashboard and click _New Release_. - Upload the new `.pbw`. Its UUID must match the app, and its version must be one that has not been used before. - Add _Release Notes_. Leave _Publish immediately_ checked unless the release should stay as a draft. - Click _Create Release_. Each release in the app's release list has a _Publish_ or _Mark Draft_ toggle, so a release can be withdrawn without hiding the whole app. The appstore offers only the newest published release, and uses its binaries to decide which watches the app is compatible with. Older releases are not served, so dropping a platform from a new release makes the app unavailable on that hardware. ### Editing a Listing Click _Edit_ on the app's page to change the name, description, URLs, companion app, screenshots, banner and icons, and to change the _App visibility_ between Listed, Unlisted and Hidden. Changes are live after saving, apart from appstore pages that are cached for up to ten minutes. ## Using the `pebble` Tool The `publish` command builds the project, captures screenshots from the emulator and uploads everything. Run it from the project directory: ``` $ pebble login$ pebble publish ``` If the project's UUID is not in the appstore yet, the tool asks for the app name, version, description, source URL and, for a watchapp, the category and icons. If the UUID already belongs to one of the developer's apps, the tool uploads a new release of that app and only asks for screenshots. Screenshots are captured for every target platform with a rollover GIF by default. Pass `--no-gif-all-platforms` to skip the GIF and `--all-platforms` to capture static images instead, or choose _Select local screenshot/GIF files_ at the prompt to upload existing files. Local filenames must start with the platform name followed by an underscore, for example `emery_main.png` or `gabbro_menu.gif`. The files written by `pebble screenshot --all-platforms` already follow this pattern. Use `--release-notes` to set the release notes without being prompted: ``` $ pebble publish --release-notes "Fix the date on the first of the month" ``` ### Publishing from CI With `--non-interactive` the tool does not prompt. Everything a new app needs is passed as flags, and the sign-in token comes from the `PEBBLE_FIREBASE_ID_TOKEN` environment variable or the `--firebase-id-token` flag instead of a stored `pebble login` session: ``` $ export PEBBLE_FIREBASE_ID_TOKEN=...$ pebble publish --non-interactive \ --name "My Watchface" --description "A clean analog face." \ --source https://github.com/example/my-watchface \ --screenshots screenshots/emery_1.png screenshots/gabbro_1.png ``` For a watchapp add `--category` and, if generated icons are not wanted, `--icon-small` and `--icon-large`. For an existing app only `--release-notes`, `--version` and `--screenshots` are used. Without `--screenshots` the tool captures from the emulator, which needs a working emulator on the CI machine. A new app must have at least one screenshot. Releases made with `pebble publish` are published immediately and new apps are Listed. Change the visibility afterwards in the Developer Dashboard if needed. ## Using CloudPebble CloudPebble has a _Publish_ entry in the project sidebar for C and JavaScript SDK projects. It uses the account that is signed in to CloudPebble, so no extra sign-in is needed. - Build the project. Publishing uses the most recent successful build. - Open _Publish_. For a new app, fill in _App name_, _Version_, _Description_ and optionally _Source URL_. For a watchapp choose a _Category_, and either upload icons or leave _Auto-generate icons_ selected. - Under _Screenshots_, click _Auto-generate_ next to each platform to capture a screenshot and a short GIF from the emulator, click _Upload_ to use your own files, or _Auto-generate All_ to do every platform at once. - Click _Publish to App Store_. When the project's UUID is already in the appstore, the pane shows _Publish Update_ instead. Only the version, release notes and new screenshots are needed. If the version has already been used, CloudPebble reports _Version x.y already exists_ with a link to the project settings to change it. The success page links to the public appstore listing and to the Developer Dashboard for further edits. --- # Importing Apps from Rebble Source: https://developer.repebble.com/guides/appstore-publishing/importing-from-rebble/ Apps that were published through the Rebble Dev Portal can be imported into the Pebble appstore from the [Developer Dashboard](https://developer.repebble.com/dashboard). Importing copies the listing, releases and screenshots and keeps the app's original ID, so existing links keep working. After an import the app is managed from the Developer Dashboard like any other. ## Importing - Open the Developer Dashboard and click _Import_. - Choose _Import by Developer ID_ and enter your Pebble developer ID. The ID is in the URL of your developer page on the Pebble appstore and on the Rebble Dev Portal profile page. Click _Load All My Apps_. - To import single apps instead, choose _Import by App IDs_ and paste one app ID per line. The app ID is the last part of an appstore URL such as `apps.repebble.com/app/583f347dca3094d3b00006d3`. - Click _Import_ next to an app, or _Import All_. Apps are imported as Listed. Importing an app a second time refreshes its listing from Rebble and keeps the visibility that was set in the Developer Dashboard. ## Verifying Ownership The import checks the developer ID that Rebble has for each app against the ID of the signed-in account. Since the Pebble developer account is new, they never match on a first import, and the dashboard opens the _Verify App Ownership_ page. Until verification is approved the imported apps keep their original developer ID. They are listed on the verification page, not in the account's app list, and cannot be edited. - Check the list of apps submitted for verification. - Tick _I confirm that I am the original developer of these apps_ and click _Submit for Verification_. - An email arrives asking for proof. Reply with a screenshot of the apps and their IDs in the Rebble Dev Portal. Links to the source repositories or the original appstore submission emails help if the screenshot is not conclusive. Verification is done by a person and takes two to three business days. Once it is approved, the apps move to your developer account, and the account's developer ID becomes the original one so that the developer name shown in the appstore stays the same. ## Keeping Apps in Sync Apps that are still updated through the Rebble Dev Portal can be kept in sync instead of being moved. Open _Sync_ in the dashboard and turn it on per app. Synced apps are re-imported from Rebble once a day. Rebble is the source of truth for them, and the sync is skipped when the Pebble appstore already has a newer release than Rebble. --- # Appstore Publishing Source: https://developer.repebble.com/guides/appstore-publishing/ When an app is feature-complete and stable, the compiled `.pbw` file can be uploaded to the Pebble appstore so that users can find and install it from the Pebble mobile app. Listings are managed in the [Developer Dashboard](https://developer.repebble.com/dashboard). The `pebble` tool and CloudPebble can also upload apps and new releases directly. Published apps and watchfaces can be browsed at [apps.repebble.com](https://apps.repebble.com). Look at a few listings there to see how the icon, screenshots and description appear before preparing a submission. ![](https://developer.repebble.com/assets/images/guides/appstore-publishing/apps-repebble-com.png) To be listed in the Pebble appstore an app must: - Be built with a released SDK. - Use a UUID that is not already in use by another app in the appstore. - Have a title and a description, and a category if it is a watchapp. - Comply with the [legal agreements](https://developer.repebble.com/legal.md). Apps that were published in the Rebble appstore can be imported into the Developer Dashboard so that the same developer keeps managing them. ## Contents - [**Preparing a Submission**](https://developer.repebble.com/guides/appstore-publishing/preparing-a-submission.md) - What a Pebble appstore listing is made of, and the sizes and formats required for screenshots, icons and banners. - [**Publishing an App**](https://developer.repebble.com/guides/appstore-publishing/publishing-an-app.md) - How to submit an app and later releases from the Developer Dashboard, the `pebble` tool or CloudPebble. - [**Importing Apps from Rebble**](https://developer.repebble.com/guides/appstore-publishing/importing-from-rebble.md) - How to bring apps published in the Rebble appstore into the Developer Dashboard and prove ownership of them. --- # Best Practices Source: https://developer.repebble.com/guides/best-practices/ In order to get the most out of the Pebble SDK, there are numerous opportunities for optimization that can allow apps to use power more efficiently, display correctly on all display shapes and sizes, and help keep large projects maintainable. Information on these topics is contained in this collection of guides. Pebble recommends that developers try and incorporate as many of these practices into their apps as possible, to give themselves and users the best experience of their app. ## Contents - [**Building for Every Pebble**](https://developer.repebble.com/guides/best-practices/building-for-every-pebble.md) - How to write one app compatible with all Pebble smartwatches. - [**Conserving Battery Life**](https://developer.repebble.com/guides/best-practices/conserving-battery-life.md) - How to write an app to consume power as efficiently as possible. - [**Modular App Architecture**](https://developer.repebble.com/guides/best-practices/modular-app-architecture.md) - How to break up a complex app into smaller pieces for managablilty, modularity and reusability. --- # Building for Every Pebble Source: https://developer.repebble.com/guides/best-practices/building-for-every-pebble/ The difference in capabilities between the various Pebble hardware platforms are listed in [_Hardware Information_](https://developer.repebble.com/guides/tools-and-resources/hardware-information.md). For example, the Basalt, Chalk and Emery platforms support 64 colors, whereas the Aplite, Diorite and Flint 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`](https://developer.repebble.com/docs/c/Foundation/Dictation.md "Dictation") API should be excluded on platforms with no microphone: ``` #if defined(PBL\_MICROPHONE)// Start dictation UIdictation\_session\_start(s\_dictation\_session);#else// Microphone is not availabletext\_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: ``` #if defined(PBL\_COLOR)text\_layer\_set\_text\_color(s\_text\_layer, GColorRed);text\_layer\_set\_background\_color(s\_text\_layer, GColorChromeYellow);#elsetext\_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()`](https://developer.repebble.com/docs/c/Graphics/Graphics_Types.md#PBL_IF_COLOR_ELSE "PBL\_IF\_COLOR\_ELSE") macro. ``` 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_COMPASS` | None | Running on hardware that includes a compass. | | `PBL_MICROPHONE` | `PBL_IF_MICROPHONE_ELSE()` | Running on hardware that includes a microphone. | | `PBL_HEALTH` | `PBL_IF_HEALTH_ELSE()` | Running on hardware that supports Pebble Health and the `HealthService` API. | | `PBL_RGB_BACKLIGHT` | None | Running on hardware that has an RGB backlight | | `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_SMARTSTRAP` | `PBL_IF_SMARTSTRAP_ELSE()` | Running on hardware with smartstrap support. | | `PBL_SMARTSTRAP_POWER` | None | Running on hardware that includes a smartstrap connector capable of supplying power. | | `PBL_SPEAKER` | None | Running on hardware that includes a speaker. | | `PBL_TOUCH` | None | Running on hardware that includes a touch screen. | | `PBL_RGB_BACKLIGHT` | None | Running on hardware with an RGB backlight. | | `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_PLATFORM_FLINT` | None | Built for Pebble 2 Duo. | | `PBL_PLATFORM_GABBRO` | None | Built for Pebble Round 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. ## Selecting Values by Platform [`PBL_PLATFORM_SWITCH()`](https://developer.repebble.com/docs/c/Foundation/Platform.md#PBL_PLATFORM_SWITCH "PBL\_PLATFORM\_SWITCH") picks one of seven values by platform. The [`PlatformType`](https://developer.repebble.com/docs/c/Foundation/Platform.md#PlatformType "PlatformType") enum names every platform and [`PBL_PLATFORM_TYPE_CURRENT`](https://developer.repebble.com/docs/c/Foundation/Platform.md#PBL_PLATFORM_TYPE_CURRENT "PBL\_PLATFORM\_TYPE\_CURRENT") is the platform of the current build: ``` // Width of the action bar on each platformint width = PBL\_PLATFORM\_SWITCH(PBL\_PLATFORM\_TYPE\_CURRENT,/\*aplite\*/ 30, /\*basalt\*/ 30, /\*chalk\*/ 40, /\*diorite\*/ 30,/\*emery\*/ 34, /\*flint\*/ 30, /\*gabbro\*/ 40); ``` The SDK defines [`ACTION_BAR_WIDTH`](https://developer.repebble.com/docs/c/User_Interface/Layers/ActionBarLayer.md#ACTION_BAR_WIDTH "ACTION\_BAR\_WIDTH") in exactly this way. The macro expands to a chain of comparisons, so with [`PBL_PLATFORM_TYPE_CURRENT`](https://developer.repebble.com/docs/c/Foundation/Platform.md#PBL_PLATFORM_TYPE_CURRENT "PBL\_PLATFORM\_TYPE\_CURRENT") as the first argument the compiler resolves it at compile time and the result is the same as the `PBL_IF_*` macros above. It only becomes a runtime choice when the first argument is a variable, for example a [`PlatformType`](https://developer.repebble.com/docs/c/Foundation/Platform.md#PlatformType "PlatformType") the app has derived from a [`WatchInfoModel`](https://developer.repebble.com/docs/c/Foundation/WatchInfo.md#WatchInfoModel "WatchInfoModel") to lay out content for a different watch. The first argument is evaluated once per comparison, so pass a variable rather than a function call. [`PBL_PLATFORM_SWITCH_DEFAULT()`](https://developer.repebble.com/docs/c/Foundation/Platform.md#PBL_PLATFORM_SWITCH_DEFAULT "PBL\_PLATFORM\_SWITCH\_DEFAULT") takes an extra default value that is returned for a platform the app does not know about. ## 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: ``` #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: ``` static void window\_load(Window \*window) {// Create a full-screen Layer - BADs\_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, Diorite and Flint, but not on Chalk or Emery. This kind of screen size-dependant calculation should use the [`UnobstructedArea`](https://developer.repebble.com/docs/c/User_Interface/UnobstructedArea.md "UnobstructedArea") bounds of the [`Window`](https://developer.repebble.com/docs/c/User_Interface/Window.md "Window") itself: ``` static void window\_load(Window \*window) {// Get the unobstructed bounds of the WindowLayer window\_layer = window\_get\_root\_layer(window);GRect window\_bounds = layer\_get\_unobstructed\_bounds(window\_layer);// Properly create a full-screen Layer - GOODs\_some\_layer = layer\_create(window\_bounds);} ``` Another common use of this sort of construction is to make a [`Layer`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#Layer "Layer") that is half the unobstructed screen height. This can also be correctly achieved using the [`Window`](https://developer.repebble.com/docs/c/User_Interface/Window.md "Window") unobstructed bounds: ``` GRect layer\_bounds = window\_bounds;layer\_bounds.size.h /= 2;// Create a Layer that is half the screen heights\_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`](https://developer.repebble.com/docs/c/User_Interface/Window.md "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. ``` #if PBL\_DISPLAY\_HEIGHT == 228uint8\_t offset\_y = 100;#elif PBL\_DISPLAY\_HEIGHT == 180uint8\_t offset\_y = 80;#elseuint8\_t offset\_y = 60;#endif ``` | Platform | Display | Shape | | --- | --- | --- | | aplite, basalt, diorite, flint | 144 × 168 | Rectangular | | chalk | 180 × 180 | Round | | emery | 200 × 228 | Rectangular | | gabbro | 260 × 260 | Round | > 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`](https://developer.repebble.com/docs/c/Foundation/WatchInfo.md "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`](https://developer.repebble.com/docs/c/Foundation/WatchInfo.md#WatchInfoModel "WatchInfoModel"). ``` 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 parametersswitch(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 frameGRect layer\_frame = GRect(0, y\_offset, window\_bounds.size.w, button\_height);// Create the Layers\_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`](https://developer.repebble.com/docs/c/Foundation/WatchInfo.md#WatchInfoColor "WatchInfoColor") values to theme an app for each available color of Pebble. ``` static void window\_load(Window \*window) {GColor text\_color, background\_color;// Choose different theme colors per watch colorswitch(watch\_info\_get\_color()) {case WATCH\_INFO\_COLOR\_RED:// Red themetext\_color = GColorWhite;background\_color = GColorRed;break;case WATCH\_INFO\_COLOR\_BLUE:// Blue themetext\_color = GColorBlack;background\_color = GColorVeryLightBlue;break;/\* Other cases \*/default:text\_color = GColorBlack;background\_color = GColorWhite;break;}// Use the conditionally set valuetext\_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. ``` // Get the watch infovar info = Pebble.getActiveWatchInfo();console.log('Pebble model: ' + info.model); ``` ## Detecting Platform-specific JS Features A number of features in PebbleKit JS (such as `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()`: ``` 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, Diorite or Flint 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 [_Platform-specific Resources_](https://developer.repebble.com/guides/app-resources/platform-specific.md). ## 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: ``` #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()`](https://developer.repebble.com/docs/c/Graphics/Graphics_Types.md#PBL_IF_RECT_ELSE "PBL\_IF\_RECT\_ELSE") and [`PBL_IF_ROUND_ELSE()`](https://developer.repebble.com/docs/c/Graphics/Graphics_Types.md#PBL_IF_ROUND_ELSE "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: ``` // Conditionally print out the shape of the displayprintf("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`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#Layer "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. --- # Conserving Battery Life Source: https://developer.repebble.com/guides/best-practices/conserving-battery-life/ One of Pebble's strengths is its long battery life. This is due in part to using a low-power display technology, conservative use of the backlight, and allowing the processor to sleep whenever possible. It therefore follows that apps which misuse high-power APIs or prevent power-saving mechanisms from working will detract from the user's battery life. Several common causes of battery drain in apps are discussed in this guide, alongside suggestions to help avoid them. ## Time Awake Because the watch tries to sleep as much as possible to conserve power, any app that keeps the watch awake will incur significant a battery penalty. Examples of such apps include those that frequently use animations, sensors, Bluetooth communications, and vibrations. ### Animations and Display Updates A common cause of such a drain are long-running animations that cause frequent display updates. For example, a watchface that plays a half-second [`Animation`](https://developer.repebble.com/docs/c/User_Interface/Animation.md "Animation") for every second that ticks by will drain the battery faster than one that does so only once per minute. The latter approach will allow a lot more time for the watch to sleep. ``` static void tick\_handler(struct tm \*tick\_time, TimeUnits changed) {// Update timeset\_time\_digits(tick\_time);// Only update once a minuteif(tick\_time-\>tm\_sec == 0) {play\_animation();}} ``` This also applies to apps that make use of short-interval [`Timer`](https://developer.repebble.com/docs/c/Foundation/Timer.md "Timer")s, which is another method of creating animations. Consider giving users the option to reduce or disable animations to further conserve power, as well as removing or shortening animations that are not essential to the app's function or aesthetic. However, not all animations are bad. Efficient use of the battery can be maintained if the animations are played at more intelligent times. For example, when the user is holding their arm to view the screen (see [`pebble_glancing_demo`](https://github.com/pebble-hacks/pebble_glancing_demo)) or only when a tap or wrist shake is detected: ``` static void accel\_tap\_handler(AccelAxisType axis, int32\_t direction) {// Animate when the user flicks their wristplay\_animation();} ``` ``` accel\_tap\_service\_subscribe(tap\_handler); ``` ### Tick Updates Many watchfaces unecessarily tick once a second by using the [`SECOND_UNIT`](https://developer.repebble.com/docs/c/Foundation/Event_Service/TickTimerService.md#SECOND_UNIT "SECOND\_UNIT") constant value with the [`TickTimerService`](https://developer.repebble.com/docs/c/Foundation/Event_Service/TickTimerService.md "TickTimerService"), when they only update the display once a minute. By using the [`MINUTE_UNIT`](https://developer.repebble.com/docs/c/Foundation/Event_Service/TickTimerService.md#MINUTE_UNIT "MINUTE\_UNIT") instead, the amount of times the watch is woken up per minute is reduced. ``` // Only tick once a minute, much more time asleeptick\_timer\_service\_subscribe(MINUTE\_UNIT, tick\_handler); ``` If possible, give users the choice to disable the second hand tick and/or animation to further save power. Extremely minimal watchfaces may also use the [`HOUR_UNIT`](https://developer.repebble.com/docs/c/Foundation/Event_Service/TickTimerService.md#HOUR_UNIT "HOUR\_UNIT") value to only be updated once per hour. This factor is especially important for Pebble Time Round users. On this platform the reduced battery capacity means that a watchface with animations that play every second could reduce this to one day or less. Consider offering configuration options to reducing tick updates on this platform to save power where it at a premium. ### Sensor Usage Apps that make frequent usage of Pebble's onboard accelerometer and compass sensors will also prevent the watch from going to sleep and consume more battery power. The [`AccelerometerService`](https://developer.repebble.com/docs/c/Foundation/Event_Service/AccelerometerService.md "AccelerometerService") API features the ability to configure the sampling rate and number of samples received per update, allowing batching of data into less frequent updates. By receiving updates less frequently, the battery will last longer. ``` // Batch samples into sets of 10 per callbackconst uint32\_t num\_samples = 10;// Sample at 10 Hzaccel\_service\_set\_sampling\_rate(ACCEL\_SAMPLING\_10HZ);// With this combination, only wake up the app once per second!accel\_data\_service\_subscribe(num\_samples, accel\_data\_handler); ``` Similarly, the [`CompassService`](https://developer.repebble.com/docs/c/Foundation/Event_Service/CompassService.md "CompassService") API allows a filter to be set on the heading updates, allowing an app to only be notified per every 45 degree angle change, for example. ``` // Only update if the heading changes significantlycompass\_service\_set\_heading\_filter(TRIG\_MAX\_ANGLE / 36); ``` In addition, making frequent use of the [`Dictation`](https://developer.repebble.com/docs/c/Foundation/Dictation.md "Dictation") API will also keep the watch awake, and also incur a penalty for keeping the Bluetooth connection alive. Consider using the [`Storage`](https://developer.repebble.com/docs/c/Foundation/Storage.md "Storage") API to remember previous user input and instead present a list of previous inputs if appropriate to reduce usage of this API. ``` static void dictation\_session\_callback(DictationSession \*session, DictationSessionStatus status,char \*transcription, void \*context) {if(status == DictationSessionStatusSuccess) {// Display the dictated textsnprintf(s\_last\_text, sizeof(s\_last\_text), "Transcription:\n\n%s",transcription);text\_layer\_set\_text(s\_output\_layer, s\_last\_text);// Save for later!const int last\_text\_key = 0;persist\_write\_string(last\_text\_key, s\_last\_text);}} ``` ### Bluetooth Usage Hinted at above, frequent use of the [`AppMessage`](https://developer.repebble.com/docs/c/Foundation/AppMessage.md "AppMessage") API to send and recieve data will cause the Bluetooth connection to enter a more responsive state, which consumes much more power. A small time after a message is sent, the connection will return back to a low-power state. The 'sniff interval' determines how often the API checks for new messages from the phone, and should be let in the default [`SNIFF_INTERVAL_NORMAL`](https://developer.repebble.com/docs/c/Foundation/App_Communication.md#SNIFF_INTERVAL_NORMAL "SNIFF\_INTERVAL\_NORMAL") state as much as possible. Consider how infrequent communication activities can be to save power and maintain functionality, and how data obtained over the Bluetooth connection can be cached using the [`Storage`](https://developer.repebble.com/docs/c/Foundation/Storage.md "Storage") API to reduce the frequency of updates (for example, weather information in watchface). If the reduced sniff state must be used to transfer large amounts of data quickly, be sure to return to the low-power state as soon as the transfer is complete: ``` // Return to low power Bluetooth stateapp\_comm\_set\_sniff\_interval(SNIFF\_INTERVAL\_NORMAL); ``` ## Backlight Usage The backlight LED is another large consumer of battery power. System-level backlight settings may see the backlight turn on for a few seconds every time a button is pressed. While this setting is out of the hands of developers, apps can work to reduce the backlight on-time by minimizing the number of button presses required to operate them. For example, use an [`ActionBarLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/ActionBarLayer.md "ActionBarLayer") to execute common actions with one button press instead of a long scrolling [`MenuLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/MenuLayer.md "MenuLayer"). While the [`Light`](https://developer.repebble.com/docs/c/User_Interface/Light.md "Light") API is available to manually turn the backlight on, it should not be used for more than very short periods, if at all. Apps that keep the backlight on all the time will not last more than a few hours. If the backlight must be kept on for an extended period, make sure to return to the automatic mode as soon as possible: ``` // Return to automatic backlight controllight\_enable(false); ``` ## Vibration Motor Usage As a physical converter of electrical to mechanical energy, the vibration motor also consumes a lot of power. Users can elect to use Quiet Time or turn off vibration for notifications to save power, but apps can also contribute to this effort. Try and keep the use of the [`Vibes`](https://developer.repebble.com/docs/c/User_Interface/Vibes.md "Vibes") API to a minimum and giving user the option to disable any vibrations the app emits. Another method to reduce vibrator power consumtion is to shorten the length of any custom sequences used. ## Learn More To learn more about power consumtion on Pebble and how battery life can be extended through app design choices, watch the presentation below given at the 2014 Developer Retreat. --- # Modular App Architecture Source: https://developer.repebble.com/guides/best-practices/modular-app-architecture/ Most Pebble projects (such as a simple watchface) work fine as a single-file project. This means that all the code is located in one `.c` file. However, as the size of a single-file Pebble project increases, it can become harder to keep track of where all the different components are located, and to track down how they interact with each other. For example, a hypothetical app may have many [`Window`](https://developer.repebble.com/docs/c/User_Interface/Window.md "Window")s, perform communication over [`AppMessage`](https://developer.repebble.com/docs/c/Foundation/AppMessage.md "AppMessage") with many types of data items, store and persist a large number of data items, or include components that may be valuable in other projects. As a first example, the Pebble SDK is already composed of separate modules such as [`Window`](https://developer.repebble.com/docs/c/User_Interface/Window.md "Window"), [`Layer`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#Layer "Layer"), [`AppMessage`](https://developer.repebble.com/docs/c/Foundation/AppMessage.md "AppMessage") etc. The implementation of each is separate from the rest and the interface for developers to use in each module is clearly defined and will rarely change. This guide aims to provide techniques that can be used to break up such an app. The advantages of a modular approach include: - App [`Window`](https://developer.repebble.com/docs/c/User_Interface/Window.md "Window")s can be kept separate and are easier to work on. - A clearly defined interface between components ensures internal changes do not affect other modules. - Modules can be re-used in other projects, or even made into sharable libraries. - Inter-component variable dependencies do not occur, which can otherwise cause problems if their type or size changes. - Sub-component complexity is hidden in each module. - Simpler individual files promote maintainability. - Modules can be more easily tested. ## A Basic Project A basic Pebble project starts life with the `new-project` command: ``` $ pebble new-project modular-project ``` This new project will contain the following default file structure. The `modular-project.c` file will contain the entire app, including `main()`, `init()` and `deinit()`, as well as a [`Window`](https://developer.repebble.com/docs/c/User_Interface/Window.md "Window") and a child [`TextLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/TextLayer.md "TextLayer"). ``` modular-project/ resources/ src/ c/ modular-project.c package.json wscript ``` The `wscript` generated by `new-project` compiles every `.c` file under `src/c/`, including subdirectories, so new source files only need to be placed there to be built. Projects created with older SDKs that keep their sources in `src/` directly must either move them to `src/c/` or change the `ant_glob()` pattern in `wscript` to include `src/c/**/*.c` as well. For most projects, this structure is perfectly adequate. When the `.c` file grows to several hundred lines long and incorporates several sub-components with many points of interaction with each other through shared variables, the complexity reaches a point where some new techniques are needed. ## Creating a Module In this context, a 'module' can be thought of as a C header and source file 'pair', a `.h` file describing the module's interface and a `.c` file containing the actual logic and code. The header contains standard statements to prevent redefinition from being `#include`d multiple times, as well as all the function prototypes the module makes available for other modules to use. By making a sub-component of the app into a module, the need for messy global variables is removed and a clear interface between them is defined. The files themselves are located in a `modules` directory inside the project's `src/c` directory, keeping them in a separate location to other components of the app. Thus the structure of the project with a `data` module added (and explained below) is now this: ``` modular-project/ resources/ src/ c/ modules/ data.h data.c modular-project.c package.json wscript ``` The example module's pair of files is shown below. It manages a dynamically allocated array of integers, and includes an interface to setting and getting values from the array. The array itself is private to the module thanks for the [`static`](https://en.wikipedia.org/wiki/Static_(keyword)) keyword. This technique allows other components of the app to call the 'getters' and 'setters' with the correct parameters as per the module's interface, without worrying about the implementation details. `src/c/modules/data.h` ``` #pragma once // Prevent errors by being included multiple times#include \ // Pebble SDK symbolsvoid data\_init(int array\_length);void data\_deinit();void data\_set\_array\_value(int index, int new\_value);int data\_get\_array\_value(int index); ``` `src/c/modules/data.c` ``` #include "data.h"static int\* s\_array;void data\_init(int array\_length) {if(!s\_array) {s\_array = (int\*)malloc(array\_length \* sizeof(int));}}void data\_deinit() {if(s\_array) {free(s\_array);s\_array = NULL;}}void data\_set\_array\_value(int index, int new\_value) {s\_array[index] = new\_value;}int data\_get\_array\_value(int index) {return s\_array[index];} ``` ## Keep Multiple Windows Separate The [`Window Stack`](https://developer.repebble.com/docs/c/User_Interface/Window_Stack.md "Window Stack") lifecycle makes the task of keeping each [`Window`](https://developer.repebble.com/docs/c/User_Interface/Window.md "Window") separate quite easy. Each one has a `.load` and `.unload` handler which should be used to create and destroy its UI components and other data. The first step to modularizing the new app is to keep each [`Window`](https://developer.repebble.com/docs/c/User_Interface/Window.md "Window") in its own module. The first [`Window`](https://developer.repebble.com/docs/c/User_Interface/Window.md "Window")'s code can be moved out of `src/c/modular-project.c` into a new module in `src/c/windows/` called 'main\_window': `src/c/windows/main_window.h` ``` #pragma once#include \void main\_window\_push(); ``` `src/c/windows/main_window.c` ``` #include "main\_window.h"static Window \*s\_window;static void window\_load(Window \*window) {Layer \*window\_layer = window\_get\_root\_layer(window);GRect bounds = layer\_get\_bounds(window\_layer);}static void window\_unload(Window \*window) {window\_destroy(s\_window);}void main\_window\_push() {if(!s\_window) {s\_window = window\_create();window\_set\_window\_handlers(s\_window, (WindowHandlers) {.load = window\_load,.unload = window\_unload,});}window\_stack\_push(s\_window, true);} ``` ## Keeping Main Clear After moving the [`Window`](https://developer.repebble.com/docs/c/User_Interface/Window.md "Window") code out of the main `.c` file, it can be safely renamed `main.c` to reflect its contents. This allows the main `.c` file to show a high-level overview of the app as a whole. Simply `#include` the required modules and windows to initialize and deinitialize the rest of the app as necessary: `src/c/main.c` ``` #include \#include "modules/data.h"#include "windows/main\_window.h"static void init() {const int array\_size = 16;data\_init(array\_size);main\_window\_push();}static void deinit() {data\_deinit();}int main() {init();app\_event\_loop();deinit();} ``` Thus the structure of the project is now: ``` modular-project/ resources/ src/ c/ modules/ data.h data.c windows/ main_window.h main_window.c main.c package.json wscript ``` With this structured approach to organizing the different functional components of an app, the maintainability of the project will not suffer as it grows in size and complexity. A useful module can even be shared and reused as a library, which is preferrable to pasting chunks of code that may have other messy dependencies elsewhere in the project. --- # Common Runtime Errors Source: https://developer.repebble.com/guides/debugging/common-runtime-errors/ Whether just beginning to create apps for the Pebble platform or are creating a more complex app, the output from app logs can be very useful in tracking down problems with an app's code. Some examples of common problems are explored here, including some examples to help gain familiarity with compiler output. In contrast with syntactical errors in written code (See [_Common Syntax Errors_](https://developer.repebble.com/guides/debugging/common-syntax-errors.md)), there can also be problems that only occur when the app is actually run on a Pebble. The reason for this is that perfectly valid C code can sometimes cause improper behavior that is incompatible with the hardware. These problems can manifest themselves as an app crashing and very little other information available as to what the cause was, which means that they can take an abnormally long time to diagnose and fix. One option to help track down the offending lines is to begin at the start of app initialization and use a call to [`APP_LOG()`](https://developer.repebble.com/docs/c/Foundation/Logging.md#app_log "app\_log") to establish where execution stops. If the message in the log call appears, then execution has at least reached that point. Move the call further through logical execution until it ceases to appear, as it will then be after the point where the app crashes. ## Null Pointers The Pebble SDK uses a dynamic memory allocation model, meaning that all the SDK objects and structures available for use by developers are allocated as and when needed. This model has the advantage that only the immediately needed data and objects can be kept in memory and unloaded when they are not needed, increasing the scale and capability of apps that can be created. In this paradigm a structure is first declared as a pointer (which may be given an initial value of `NULL`) before being fully allocated a structure later in the app's initialization. Therefore one of the most common problems that can arise is that of the developer attempting to use an unallocated structure or data item. For example, the following code segment will cause a crash: ``` Window \*main\_window;static void init() {// Attempting to push an uninitialized Window!window\_stack\_push(main\_window, true);} ``` The compiler will not report this, but when run the app will crash before the [`Window`](https://developer.repebble.com/docs/c/User_Interface/Window.md "Window") can be displayed, with an error message sent to the console output along the following lines: ``` [INFO] E ault_handling.c:77 App fault! {f23aecb8-bdb5-4d6b-b270-602a1940575e} PC: 0x8016716 LR: 0x8016713 [WARNING] Program Counter (PC): 0x8016716 ??? [WARNING] Link Register (LR): 0x8016713 ??? ``` When possible, the pebble tool will tell the developer the PC (Program Counter, or which statement is currently being executed) and LR (Link Register, address to return to when the current function scope ends) addresses and line numbers at the time of the crash, which may help indicate the source of the problem. This problem can be fixed by ensuring that any data structures declared as pointers are properly allocated using the appropriate `_create()` SDK functions before they are used as arguments: ``` Window \*main\_window;static void init(void) {main\_window = window\_create();window\_stack\_push(main\_window, true);} ``` In situations where available heap space is limited, `_create()` functions may return `NULL`, and the object will not be allocated. Apps can detect this situation as follows: ``` Window \*main\_window;static void init(void) {main\_window = window\_create();if(main\_window != NULL) {// Allocation was successful!window\_stack\_push(main\_window, true);} else {// The Window could not be allocated! // Tell the user that the operation could not be completedtext\_layer\_set\_text(s\_output\_layer, "Unable to use this feature at the moment.");}} ``` This `NULL` pointer error can also occur to any dynamically allocated structure or variable of the developer's own creation outside the SDK. For example, a typical dynamically allocated array will cause a crash if it is used before it is allocated: ``` char \*array;// Array is still NULL!array[0] = 'a'; ``` This problem can be fixed in a similar manner as before by making sure the array is properly allocated before it is used: ``` char \*array = (char\*)malloc(8 \* sizeof(char));array[0] = 'a'; ``` As mentioned above for [`window_create()`](https://developer.repebble.com/docs/c/User_Interface/Window.md#window_create "window\_create"), be sure also check the [return value](http://pubs.opengroup.org/onlinepubs/009695399/functions/malloc.html) of `malloc()` to determine whether the memory allocation requested was completed successfully: ``` array = (char\*)malloc(8 \* sizeof(char));// Check the malloc() was successfulif(array != NULL) {array[0] = 'a';} else {// Gracefully handle the failed situation} ``` ## Outside Array Bounds Another problem that can look OK to the compiler, but cause an error at runtime is improper use of arrays, such as attempting to access an array index outside the array's bounds. This can occur when a loop is set up to iterate through an array, but the size of the array is reduced or the loop conditions change. For example, the array iteration below will not cause a crash, but includes the use of 'magic numbers' that can make a program brittle and prone to errors when these numbers change: ``` int \*array;static void init(void) {array = (int\*)malloc(8 \* sizeof(int));for(int i = 0; i \< 8; i++) {array[i] = i \* i;}} ``` If the size of the allocated array is reduced, the app will crash when the iterative loop goes outside the array bounds: ``` int \*array;static void init(void) {array = (int\*)malloc(4 \* sizeof(int));for(int i = 0; i \< 8; i++) {array[i] = i \* i;// Crash when i == 4!}} ``` Since the number of loop iterations is linked to the size of the array, this problem can be avoided by defining the size of the array in advance in one place, and then using that value everywhere the size of the array is needed: ``` #define ARRAY\_SIZE 4int \*array;static void init(void) {array = (int\*)malloc(ARRAY\_SIZE \* sizeof(int));for(int i = 0; i \< ARRAY\_SIZE; i++) {array[i] = i \* i;}} ``` An alternative solution to the above is to use either the `ARRAY_LENGTH()` macro or the `sizeof()` function to programmatically determine the size of the array to be looped over. --- # Common Syntax Errors Source: https://developer.repebble.com/guides/debugging/common-syntax-errors/ If a developer is relatively new to writing Pebble apps (or new to the C language in general), there may be times when problems with an app's code will cause compilation errors. Some types of errors with the code itself can be detected by the compiler and this helps reduce the number that cause problems when the code is run on Pebble. These are problems with how app code is written, as opposed to runtime errors (discussed in [_Common Runtime Errors_](https://developer.repebble.com/guides/debugging/common-runtime-errors.md)), which may include breaking the rules of the C language or bad practices that the compiler is able to detect and show as an error. The following are some examples. ### Undeclared Variables This error means that a variable that has been referenced is not available in the current scope. ``` ../src/main.c: In function 'toggle_logging': ../src/main.c:33:6: error: 'is_now_logging' undeclared (first use in this function) if(is_now_logging == true) { ^ ``` In the above example, the symbol `is_now_logging` has been used in the `toggle_logging` function, but it was not first declared there. This could be because the declaring line has been deleted, or it was expected to be available globally, but isn't. To fix this, consider where else the symbol is required. If it is needed in other functions, move the declaration to a global scope (outside any function). If it is needed only for this function, declare it before the offending line (here line `33`). ### Undeclared Functions Another variant of the above problem can occur when declaring new functions in a code file. Due to the nature of C compilation, any function a developer attempts to call must have been previously encountered by the compiler in order to be visible. This can be done through [forward declaration](http://en.wikipedia.org/wiki/Forward_declaration). For example, the code segment below will not compile: ``` static void window\_load(Window \*window) {my\_function();}void my\_function() {// Some code here} ``` The compiler will report this with an 'implicit declaration' error, as the app has implied the function's existence by calling it, even though the compiler has not seen it previously: ``` ../src/function-visibility.c: In function 'window_load': ../src/function-visibility.c:6:3: error: implicit declaration of function 'my_function' [-Werror=implicit-function-declaration] my_function(); ^ ``` This is because the _declaration_ of `my_function()` occurs after it is called in `window_load()`. There are two options to fix this. - Move the function declaration above any calls to it, so it has been encountered by the compiler: ``` void my\_function() {// Some code here}static void window\_load(Window \*window) {my\_function();} ``` - Declare the function by prototype before it is called, and provide the implementation later: ``` void my\_function();static void window\_load(Window \*window) {my\_function();}void my\_function() {// Some code here} ``` ### Too Few Arguments When creating functions with argument lists, sometimes the requirements of the function change and the developer forgets to update the places where it is called. ``` ../src/main.c: In function 'select_click_handler': ../src/main.c:57:3: error: too few arguments to function 'toggle_logging' toggle_logging(); ^ ../src/main.c:32:13: note: declared here static void toggle_logging(bool will_log) { ^ ``` The example above reports that the app tried to call the `toggle_logging()` function in `select_click_handler()` on line 57, but did not supply enough arguments. The argument list expected in the function definition is shown in the second part of the output message, which here exists on line 32 and expects an extra value of type `bool`. To fix this, establish which version of the function is required, and update either the calls or the declaration to match. ### Incorrect Callback Implementations In the Pebble SDK there are many instances where the developer must implement a function signature required for callbacks, such as for a [`WindowHandlers`](https://developer.repebble.com/docs/c/User_Interface/Window.md#WindowHandlers "WindowHandlers") object. This means that when implementing the handler the developer-defined callback must match the return type and argument list specified in the API documentation. For example, the [`WindowHandler`](https://developer.repebble.com/docs/c/User_Interface/Window.md#WindowHandler "WindowHandler") callback (used for the `load` and `unload` events in a [`Window`](https://developer.repebble.com/docs/c/User_Interface/Window.md "Window")'s lifecycle) has the following signature: ``` typedef void(\* WindowHandler)(struct Window \*window) ``` This specifies a return type of `void` and a single argument: a pointer of type [`Window`](https://developer.repebble.com/docs/c/User_Interface/Window.md "Window"). Therefore the implemented callback should look like this: ``` void window\_load(Window \*window) {} ``` If the developer does not specify the correct return type and argument list in their callback implementation, the compiler will let them know with an error like the following, stating that the type of function passed by the developer does not match that which is expected: ``` ../src/main.c: In function 'init': ../src/main.c:82:5: error: initialization from incompatible pointer type [-Werror] .load = main_window_load, ^ ../src/main.c:82:5: error: (near initialization for '(anonymous).load') [-Werror] ``` To fix this, double check that the implementation provided has the same return type and argument list as specified in the API documentation. --- # Debugging Source: https://developer.repebble.com/guides/debugging/ When writing apps, everyone makes mistakes. Sometimes a simple typo or omission can lead to all kinds of mysterious behavior or crashes. The guides in this section are aimed at trying to help developers identify and fix a variety of issues that can arise when writing C code (compile-time) or running the compiled app on Pebble (runtime). There are also a few strategies outlined here, such as app logging and other features of the `pebble` [_Command Line Tool_](https://developer.repebble.com/guides/tools-and-resources/pebble-tool.md) that can indicate the source of a problem in the vast majority of cases. ## Contents - [**Common Runtime Errors**](https://developer.repebble.com/guides/debugging/common-runtime-errors.md) - Examples of commonly encountered runtime problems that cannot be detected at compile time and can usually be fixed by logical thought and experimentation. - [**Common Syntax Errors**](https://developer.repebble.com/guides/debugging/common-syntax-errors.md) - Details of common problems encountered when writing C apps for Pebble, and how to resolve them. - [**Debugging Alloy Apps with xsbug**](https://developer.repebble.com/guides/debugging/debugging-alloy-with-xsbug.md) - Use the xsbug JavaScript debugger to set breakpoints and inspect your Alloy app while it runs on the watch. - [**Debugging with App Logs**](https://developer.repebble.com/guides/debugging/debugging-with-app-logs.md) - How to use the app logs to debug problems with an app, as well as tips on interpreting common run time errors. - [**Debugging with GDB**](https://developer.repebble.com/guides/debugging/debugging-with-gdb.md) - How to use GDB to debug a Pebble app in the emulator. --- # Debugging Alloy Apps with xsbug Source: https://developer.repebble.com/guides/debugging/debugging-alloy-with-xsbug/ Alloy apps run on Moddable's XS JavaScript engine, which ships with a graphical source-level debugger called **xsbug**. With xsbug you can set breakpoints, step through JavaScript, inspect variables, and watch `console.log` / `trace` output while your app runs on a watch or in the emulator. > **Platform Support** : Alloy and xsbug debugging are available on Emery (Pebble Time 2) and Gabbro (Pebble Round 2). This requires `pebble-tool` 5.0.38 or later. ## Building a Debug Build xsbug can only attach to an app that was built in debug mode. Build your project with the `--debug` flag: ``` $ pebble build --debug ``` A debug build: - compiles your C code without optimizations (`-O0`) for easier stepping; - defines `PBL_DEBUG`, which the Alloy entry point (`src/c/mdbl.c`) uses to set `kModdableCreationFlagDebug` and enable the xsbug connection; and - produces a separate `_debug.pbw` bundle, so your debug and release builds can coexist. > **Note** : A debug build is larger and runs more slowly than a release build, and may not fit on memory-constrained apps. Build without `--debug` for normal testing and release. ## Launching the Debugger When you install a `--debug` build, the Pebble tool automatically launches xsbug, opens your `src/embeddedjs` folder so breakpoints map to your source, and bridges the JavaScript debugger over the connection _before_ the app boots - so the debugger is attached as soon as your code starts running: ``` $ pebble install --emulator emery Launching xsbug JavaScript debugger... ``` The same happens for `pebble logs`, which also prefers the `_debug` bundle when one is present. xsbug ships in the active SDK's `moddable-tools` directory and is started for you. If the tool can't find it, it prints a warning - launch xsbug manually and re-run the install so the debugger can connect. ## Using xsbug Once connected, xsbug shows your running app. From its interface you can: - set and clear **breakpoints** by clicking in the gutter of a source file; - **step** over, into, and out of functions, and continue execution; - inspect the **call stack** , **local variables** , and **global** state; and - view **`console.log`** and **`trace`** output in the messages pane. For a full tour of the debugger, see the [Moddable xsbug documentation](https://www.moddable.com/documentation/xs/xsbug). ## See Also - [_Debugging with App Logs_](https://developer.repebble.com/guides/debugging/debugging-with-app-logs.md) - viewing log output - [_Getting Started with Alloy_](https://developer.repebble.com/guides/alloy/getting-started.md) - getting started with Alloy --- # Debugging with App Logs Source: https://developer.repebble.com/guides/debugging/debugging-with-app-logs/ When apps in development do not behave as expected the developer can use app logs to find out what is going wrong. The C SDK and PebbleKit JS can both output messages and values to the console to allow developers to get realtime information on the state of their app. This guide describes how to log information from both the C and JS parts of a watchapp or watchface and also how to read that information for debugging purposes. ## Logging in C The C SDK includes the [`APP_LOG()`](https://developer.repebble.com/docs/c/Foundation/Logging.md#app_log "app\_log") macro function which allows an app to log a string containing information to the console: ``` static int s\_buffer[5];for(int i = 0; i \< 10; i++) {// Store loop value in arrays\_buffer[i] = i;APP\_LOG(APP\_LOG\_LEVEL\_DEBUG, "Loop index now %d", i);} ``` This will result in the following output before crashing: ``` [INFO] D main.c:20 Loop index now 0 [INFO] D main.c:20 Loop index now 1 [INFO] D main.c:20 Loop index now 2 [INFO] D main.c:20 Loop index now 3 [INFO] D main.c:20 Loop index now 4 ``` In this way it will be possible to tell the state of the loop index value if the app encounters a problem and crashes (such as going out of array bounds in the above example). ## Logging in JS Information can be logged in PebbleKit JS and Pebble.js using the standard JavaScript console, which will then be passed on to the log output view. An example of this is to use the optional callbacks when using `Pebble.sendAppMessage()` to know if a message was sent successfully to the watch: ``` console.log('Sending data to Pebble...');Pebble.sendAppMessage({'KEY': value}, function(e) {console.log('Send successful!');}, function(e) {console.log('Send FAILED!');}); ``` ## Viewing Log Data When viewing app logs, both the C and JS files' output are shown in the same view. The `pebble` [_Command Line Tool_](https://developer.repebble.com/guides/tools-and-resources/pebble-tool.md) will output any logs from C and JS files after executing the `pebble logs` command and supplying the phone's IP address: ``` pebble logs --phone=192.168.1.25 ``` > Note: You can also use `pebble install --logs' to combine both of these operations into one command. ## Memory Usage Information In addition to the log output from developer apps, statistics about memory usage are also included in the C app logs when an app exits: ``` [INFO] process_manager.c:289: Heap Usage for App compass-ex: Total Size <22980B> Used <164B> Still allocated <0B> ``` This piece of information reports the total heap size of the app, the amount of memory allocated as a result of execution, and the amount of memory still allocated when it exited. This last number can alert any forgotten deallocations (for example, forgetting [`window_destroy()`](https://developer.repebble.com/docs/c/User_Interface/Window.md#window_destroy "window\_destroy") after [`window_create()`](https://developer.repebble.com/docs/c/User_Interface/Window.md#window_create "window\_create")). A small number such as `28B` is acceptable, provided it remains the same after subsequent executions. If it increases after each app exit it may indicate a memory leak. For more information on system memory usage, checkout the [Size presentation from the 2014 Developer Retreat](https://www.youtube.com/watch?v=8tOhdUXcSkw). ## Alloy Instrumentation Logging Alloy apps can enable instrumentation logging to get detailed output from the JavaScript runtime. This is useful for profiling and understanding what the XS engine is doing under the hood. To enable instrumentation logging, modify the `src/c/mdbl.c` file in your Alloy project. Instead of passing `NULL` to `moddable_createMachine()`, pass a [`ModdableCreationRecord`](https://developer.repebble.com/docs/c/Foundation/Alloy.md#ModdableCreationRecord "ModdableCreationRecord") with the `kModdableCreationFlagLogInstrumentation` flag: ``` #include \int main(void) {Window \*w = window\_create();window\_stack\_push(w, true);ModdableCreationRecord creation = {.recordSize = sizeof(ModdableCreationRecord),.flags = kModdableCreationFlagLogInstrumentation,};moddable\_createMachine(&creation);window\_destroy(w);} ``` The key changes from the default `mdbl.c` are: 1. Create a [`ModdableCreationRecord`](https://developer.repebble.com/docs/c/Foundation/Alloy.md#ModdableCreationRecord "ModdableCreationRecord") struct, setting `recordSize` to `sizeof(ModdableCreationRecord)` for version compatibility. 2. Set `flags` to `kModdableCreationFlagLogInstrumentation`. 3. Pass a pointer to the creation record to `moddable_createMachine()` instead of `NULL`. The instrumentation output will appear alongside regular app logs. > **Note** : Instrumentation logging adds overhead and produces verbose output. Disable it for release builds by reverting to `moddable_createMachine(NULL)`. ## Avoid Excessive Logging As noted in the [API documentation](https://developer.repebble.com/docs/c/Foundation/Logging.md "Logging"), logging over Bluetooth can be a power-hungry operation if an end user has the Developer Connection enabled and is currently viewing app logs. In addition, frequent (multiple times per second) logging can interfere with frequent use of [`AppMessage`](https://developer.repebble.com/docs/c/Foundation/AppMessage.md "AppMessage"), as the two mechanisms share the same channel for communication. If an app is logging sent/received AppMessage events or values while doing this sending, it could experience slow or dropped messages. Be sure to disable this logging when frequently sending messages. --- # Debugging with GDB Source: https://developer.repebble.com/guides/debugging/debugging-with-gdb/ As of SDK 3.10 (and [Pebble Tool](https://developer.repebble.com/guides/tools-and-resources/pebble-tool.md) 4.2), developers can use the powerful [GDB](https://www.gnu.org/software/gdb/) debugging tool to find and fix errors in Pebble apps while they are running in an emulator. GDB allows the user to observe the state of the app at any point in time, including the value of global and local variables, as well as current function parameters and a backtrace. Strategically placing breakpoints and observing these values can quickly reveal the source of a bug. **Notice** GDB cannot be used to debug an app running on a real watch. ## Starting GDB To begin using GDB, start an emulator and install an app: ``` $ pebble install --emulator basalt ``` Once the app is installed, begin using GDB: ``` $ pebble gdb --emulator basalt ``` Once the `(gdb)` prompt appears, the app is paused by GDB for observation. To resume execution, use the `continue` (or `c`) command. Similarly, the app can be paused for debugging by pressing `control + c`. ``` (gdb) c Continuing. ``` A short list of useful commands (many more are available) can be also be obtained from the `pebble` tool. Read the [_Emulator Interaction_](https://developer.repebble.com/guides/tools-and-resources/pebble-tool.md#gdb) section of the [_Command Line Tool_](https://developer.repebble.com/guides/tools-and-resources/pebble-tool.md) guide for more details on this list. ``` $ pebble gdb --help ``` ## Observing App State To see the value of variables and parameters at any point, set a breakpoint by using the `break` (or `b`) command and specifying either a function name, or file name with a line number. For example, the snippet below shows a typical [`TickHandler`](https://developer.repebble.com/docs/c/Foundation/Event_Service/TickTimerService.md#TickHandler "TickHandler") implementation with line numbers in comments: ``` /\* 58 \*/static void tick\_handler(struct tm \*tick\_time, TimeUnits changed) {/\* 59 \*/int hours = tick\_time-\>tm\_hour;/\* 60 \*/int mins = tick\_time-\>tm\_min;/\* 61 \*//\* 62 \*/if(hours \< 10) {/\* 63 \*//\* other code \*//\* 64 \*/}/\* 65 \*/} ``` To observe the values of `hours` and `mins`, a breakpoint is set in this file at line 61: ``` (gdb) b main.c:61 Breakpoint 2 at 0x200204d6: file ../src/main.c, line 61. ``` > Use `info break` to see a list of all breakpoints currently registered. Each can be deleted with `delete n`, where `n` is the breakpoint number. With this breakpoint set, use the `c` command to let the app continue until it encounters the breakpoint: ``` $ c Continuing. ``` When execution arrives at the breakpoint, the next line will be displayed along with the state of the function's parameters: ``` Breakpoint 2, tick_handler (tick_time=0x20018770, units_changed=(SECOND_UNIT | MINUTE_UNIT)) at ../src/main.c:62 62 if(hours < 10) { ``` The value of `hours` and `mins` can be found using the `info locals` command: ``` (gdb) info locals hours = 13 mins = 23 ``` GDB can be further used here to view the state of variables using the `p` command, such as other parts of the `tm` object beyond those being used to assign values to `hours` and `mins`. For example, the day of the month: ``` (gdb) p tick_time->tm_mday $2 = 14 ``` A backtrace can be generated that describes the series of function calls that got the app to the breakpoint using the `bt` command: ``` (gdb) bt #0 segment_logic (this=0x200218a0) at ../src/drawable/segment.c:18 #1 0x2002033c in digit_logic (this=0x20021858) at ../src/drawable/digit.c:141 #2 0x200204c4 in pge_logic () at ../src/main.c:29 #3 0x2002101a in draw_frame_update_proc (layer=, ctx=) at ../src/pge/pge.c:190 #4 0x0802627c in ?? () #5 0x0805ecaa in ?? () #6 0x0801e1a6 in ?? () #7 0x0801e24c in app_event_loop () #8 0x2002108a in main () at ../src/pge/pge.c:34 #9 0x080079de in ?? () #10 0x00000000 in ?? () ``` > Lines that include '??' denote a function call in the firmware. Building the app with `pebble build --debug` will disable some optimizations and can produce more readable output from GDB. However, this can increase code size which may break apps that are pushing the heap space limit. ## Fixing a Crash When an app is paused for debugging, the developer can manually advance each statement and precisely follow the path taken through the code and observe how the state of each variable changes over time. This is very useful for tracking down bugs caused by unusual input to functions that do not adequately check them. For example, a `NULL` pointer. The app code below demonstrates a common cause of an app crash, caused by a misunderstanding of how the [`Window`](https://developer.repebble.com/docs/c/User_Interface/Window.md "Window") stack works. The [`TextLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/TextLayer.md "TextLayer") is created in the `.load` handler, but this is not called until the [`Window`](https://developer.repebble.com/docs/c/User_Interface/Window.md "Window") is pushed onto the stack. The attempt to set the time to the [`TextLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/TextLayer.md "TextLayer") by calling `update_time()` before it is displayed will cause the app to crash. ``` #include \static Window \*s\_window;static TextLayer \*s\_time\_layer;static void window\_load(Window \*window) {Layer \*window\_layer = window\_get\_root\_layer(window);GRect bounds = layer\_get\_bounds(window\_layer);s\_time\_layer = text\_layer\_create(bounds);text\_layer\_set\_text(s\_time\_layer, "00:00");text\_layer\_set\_text\_alignment(s\_time\_layer, GTextAlignmentCenter);layer\_add\_child(window\_layer, text\_layer\_get\_layer(s\_time\_layer));}static void update\_time() {time\_t now = time(NULL);struct tm \*tick\_time = localtime(&now);static char s\_buffer[8];strftime(s\_buffer, sizeof(s\_buffer), "%H:%M", tick\_time);text\_layer\_set\_text(s\_time\_layer, s\_buffer);}static void init() {s\_window = window\_create();window\_set\_window\_handlers(s\_window, (WindowHandlers) {.load = window\_load});update\_time();window\_stack\_push(s\_window, true);}static void deinit() {window\_destroy(s\_window);}int main() {init();app\_event\_loop();deinit();} ``` Supposing the cause of this crash was not obvious from the order of execution, GDB can be used to identify the cause of the crash with ease. It is known that the app crashes on launch, so the first breakpoint is placed at the beginning of `init()`. After continuing execution, the app will pause at this location: ``` (gdb) b init Breakpoint 2 at 0x2002010c: file ../src/main.c, line 26. (gdb) c Continuing. Breakpoint 2, main () at ../src/main.c:41 41 init(); ``` Using the `step` command (or Enter key), the developer can step through all the statements that occur during app initialization until the crash is found (and the `app_crashed` breakpoint is encountered. Alternatively, `bt full` can be used after the crash occurs to inspect the local variables at the time of the crash: ``` (gdb) c Continuing. Breakpoint 1, 0x0804af6c in app_crashed () (gdb) bt full #0 0x0804af6c in app_crashed () No symbol table info available. #1 0x0800bfe2 in ?? () No symbol table info available. #2 0x0800c078 in ?? () No symbol table info available. #3 0x0804c306 in ?? () No symbol table info available. #4 0x080104f0 in ?? () No symbol table info available. #5 0x0804c5c0 in ?? () No symbol table info available. #6 0x0805e6ea in text_layer_set_text () No symbol table info available. #7 0x20020168 in update_time () at ../src/main.c:22 now = 2076 tick_time = s_buffer = "10:38\000\000" #8 init () at ../src/main.c:31 No locals. #9 main () at ../src/main.c:41 No locals. #10 0x080079de in ?? () No symbol table info available. #11 0x00000000 in ?? () No symbol table info available. ``` The last statement to be executed before the crash is a call to `text_layer_set_text()`, which implies that one of its input variables was bad. It is easy to determine which by printing local variable values with the `p` command: ``` Breakpoint 4, update_time () at ../src/main.c:22 22 text_layer_set_text(s_time_layer, s_buffer); (gdb) p s_time_layer $1 = (TextLayer *) 0x0 <__pbl_app_info> ``` In this case, GDB displays `0x0` (`NULL`) for the value of `s_time_layer`, which shows it has not yet been allocated, and so will cause `text_layer_set_text()` to crash. And thus, the source of the crash has been methodically identified. A simple fix here is to swap `update_time()` and [`window_stack_push()`](https://developer.repebble.com/docs/c/User_Interface/Window_Stack.md#window_stack_push "window\_stack\_push") around so that `init()` now becomes: ``` static void init() {// Create a Windows\_window = window\_create();window\_set\_window\_handlers(s\_window, (WindowHandlers) {.load = window\_load});// Display the Windowwindow\_stack\_push(s\_window, true);// Set the timeupdate\_time();} ``` In this new version of the code the [`Window`](https://developer.repebble.com/docs/c/User_Interface/Window.md "Window") will be pushed onto the stack, calling its `.load` handler in the process, and the [`TextLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/TextLayer.md "TextLayer") will be allocated and available for use once execution subsequently reaches `update_time()`. --- # Creating Pebble Packages Source: https://developer.repebble.com/guides/pebble-packages/creating-packages/ ## Getting Started To get started creating a package, run `pebble new-package some-name`. Make `some-name` something meaningful and unique; it is what you'll be publishing it under. You can check if it's taken on [npm](https://npmjs.org). If you'll be including a JavaScript component, you can add `--javascript` for a sample javascript file. ## Components of a Package ### C code **Notice** **Tip** : If you want to use an [`Event Service`](https://developer.repebble.com/docs/c/Foundation/Event_Service.md "Event Service"), you should use the [pebble-events](https://www.npmjs.com/package/pebble-events) package to handle subscriptions from multiple packages. Packages can export C functions to their consumers, and the default package exports `somelib_find_truth` as an example. To export a function, it simply has to be declared in a C file in `src/c/`, and declared in a header file in `include/`. For instance: `src/c/somelib.c`: ``` #include \#include "somelib.h"bool somelib\_find\_truth(void) {return true;} ``` `include/somelib.h`: ``` #pragma oncebool somelib\_find\_truth(void); ``` Notice that `include/` is already in the include path when building packages, so include files there can be included easily. By convention, packages should prefix their non-static functions with their name (in this case `somelib`) in order to avoid naming conflicts. If you don't want to export a function, don't include it in a file in `includes/`; you can instead use `src/c/`. You should still prefix any non-static symbols with your library name to avoid conflicts. Once the package is imported by a consumer — either an app or package — its include files will be in a directory named for the package, and so can be included with `#include `. There is no limit on the number or structure of files in the `include` directory. ### JavaScript code Packages can export JavaScript code for use in PebbleKit JS. The default JavaScript entry point for packages is always in `src/js/index.js`. However, files can also be required directly, according to [standard node `require` rules](https://nodejs.org/api/modules.html). In either case they are looked up relative to their root in `src/js/`. JavaScript code can export functions by attaching them to the global `exports` object: `src/js/index.js`: ``` exports.addNumbers = function(a, b) {return a + b;}; ``` Because JavaScript code is scoped and namespaced already, there is no need to use any naming convention. ### Resources Packages can include resources in the same way as apps, and those resources can then be used by both the package and the app. They are included in `package.json` in [the same manner as they are for apps](https://developer.repebble.com/guides/app-resources.md). To avoid naming conflicts, packages should prefix their resource names with the package name, e.g. `SOMELIB_IMAGE_LYRA`. It's best practice to define an image resource using the package name as a prefix on the resource `name`: ``` "resources": {"media": [{"name": "MEDIA\_PACKAGE\_IMAGE\_01\_TINY","type": "bitmap","file": "images/01-tiny.png"},//...]} ``` Create a `publishedMedia` entry if you want to make the images available for [_AppGlance slices_](https://developer.repebble.com/guides/user-interfaces/appglance-c.md) or [_Timeline pins_](https://developer.repebble.com/guides/pebble-timeline/pin-structure.md). ``` "resources": {//..."publishedMedia": [{"name": "MEDIA\_PACKAGE\_IMAGE\_01","glance": "MEDIA\_PACKAGE\_IMAGE\_01\_TINY","timeline": {"tiny": "MEDIA\_PACKAGE\_IMAGE\_01\_TINY","small": "MEDIA\_PACKAGE\_IMAGE\_01\_SMALL","large": "MEDIA\_PACKAGE\_IMAGE\_01\_LARGE"}}]} ``` > Note: Do NOT assign an `id` when defining `publishedMedia` within packages, see [_Using Packages_](https://developer.repebble.com/guides/pebble-packages/using-packages.md). Resource IDs are not assigned until the package has been linked with an app and compiled, so `RESOURCE_ID_*` constants cannot be used as constant initializers in packages. To work around this, either assign them at runtime or reference them directly. It is also no longer valid to try iterating over the resource id numbers directly; you must use the name defined for you by the SDK. ### AppMessage Keys Libraries can use AppMessage keys to reduce friction when creating a package that needs to communicate with the phone or internet, such as a weather package. A list of key names can be included in `package.json`, under `pebble.messageKeys`. These keys will be allocated numbers at app build time. We will inject them into your C code with the prefix `MESSAGE_KEY_`, e.g. `MESSAGE_KEY_CURRENT_TEMP`. If you want to use multiple keys as an 'array', you can specify a name like `ELEMENTS[6]`. This will create a single key, `ELEMENTS`, but leave five empty spaces after it, for a total of six available keys. You can then use arithmetic to access the additional keys, such as `MESSAGE_KEY_ELEMENTS + 5`. To use arrays in JavaScript you will need to know the actual numeric values of your keys. They will exist as keys on an object you can access via `require('message_keys')`. For instance: ``` var keys = require('message\_keys');var elements = ['honesty', 'generosity', 'loyalty', 'kindness', 'laughter', 'magic'];var dict = {}for (var i = 0; i \< 6; ++i) {dict[keys.ELEMENTS + i] = elements[i];}Pebble.sendAppMessage(dict, successCallback, failureCallback); ``` ## Building and testing Run `pebble build` to build your package. You can install it in a test project using `pebble package install ../path/to/package`. Note that you will have to repeat both steps when you change the package. If you try symlinking the package, you are likely to run into problems building your app. ## Publishing Publishing a Pebble Package requires you to have an npm account. For your convenience, you can create or log in to one using `pebble package login` (as distinct from `pebble login`). Having done this once, you can use `pebble package publish` to publish a package. Remember to document your package! It isn't any use to anyone if they can't figure out how to use it. README.md is a good place to include some documentation. Adding extra metadata to package.json is also worthwhile. In particular, it is worth specifying: - `repository`: if your package is open source, the repo you can find it in. If it's hosted on github, `"username/repo-name"` works; otherwise a git URL. - `license`: the license the package is licensed under. If you're not sure, try [choosealicense.com](http://choosealicense.com). You will also want to include a copy of the full license text in a file called LICENSE. - `description`: a one-sentence description of the package. For more details on package.json, check out [npm's package.json documentation](https://docs.npmjs.com/getting-started/using-a-package.json). --- # Pebble Packages Source: https://developer.repebble.com/guides/pebble-packages/ It is very common to want to use some piece of common functionality that we do not provide directly in our SDK: for instance, show the weather or swap our colors in a bitmap. To provide this functionality, developers can create _Pebble Packages_, which provide developers with easy ways to share their code. ## Contents - [**Creating Pebble Packages**](https://developer.repebble.com/guides/pebble-packages/creating-packages.md) - How to create Pebble Packages - [**Using Pebble Packages**](https://developer.repebble.com/guides/pebble-packages/using-packages.md) - How to use Pebble Packages --- # Using Pebble Packages Source: https://developer.repebble.com/guides/pebble-packages/using-packages/ ## Getting started Using pebble packages is easy: 1. Find a package. We will have a searchable listing soon, but for now you can [browse the pebble-package keyword on npm](https://www.npmjs.com/browse/keyword/pebble-package). 2. Run `pebble package install pebble-somelib` to install pebble-somelib. 3. Use the package. It is possible to use _some_ standard npm packages. However, packages that depend on being run in node, or in a real web browser, are likely to fail. If you install an npm package, you can use it in the usual manner, as described below. ### C code Packages should document their specific usage. However, in general, for C packages you can include their headers and call them like so: ``` #include \int main() {somelib\_do\_the\_thing();} ``` All of the package's include files will be in a folder named after the package. Packages may have any structure inside that folder, so you are advised to read their documentation. **Notice** **Tip** : If you want to use an [`Event Service`](https://developer.repebble.com/docs/c/Foundation/Event_Service.md "Event Service"), you should use the [pebble-events](https://www.npmjs.com/package/pebble-events) package to avoid conflicting with handlers registered by packages. ### JavaScript code JavaScript packages are used via the `require` function. In most cases you can just `require` the package by name: ``` var somelib = require('pebble-somelib');somelib.doTheThing(); ``` ### Resources If the package you are using has included image resources, you can reference them directly using their `RESOURCE_ID_*` identifiers. ``` static GBitmap \*s\_image\_01;s\_image\_01 = gbitmap\_create\_with\_resource(RESOURCE\_ID\_MEDIA\_PACKAGE\_IMAGE\_01\_TINY); ``` ### Published Media If the package you are using has defined `publishedMedia` resources, you can either reference the resources using their resource identifier (as above), or you can create an alias within the `package.json`. The `name` you specify in your own project can be used to reference that `publishedMedia` item for AppGlances and Timeline pins, eg. `PUBLISHED_ID_` For example, if the package exposes the following `publishedMedia`: ``` "resources": {//..."publishedMedia": [{"name": "MEDIA\_PACKAGE\_IMAGE\_01","glance": "MEDIA\_PACKAGE\_IMAGE\_01\_TINY","timeline": {"tiny": "MEDIA\_PACKAGE\_IMAGE\_01\_TINY","small": "MEDIA\_PACKAGE\_IMAGE\_01\_SMALL","large": "MEDIA\_PACKAGE\_IMAGE\_01\_LARGE"}}]} ``` You could define the following `name` and `alias` with a unique `id` in your `package.json`: ``` "resources": {//..."publishedMedia": [{"name": "SHARED\_IMAGE\_01","id": 1,"alias": "MEDIA\_PACKAGE\_IMAGE\_01"}]} ``` You can then proceed to use that `name`, prefixed with `PUBLISHED_ID_`, within your code: ``` const AppGlanceSlice entry = (AppGlanceSlice) {.layout = {.icon = PUBLISHED\_ID\_SHARED\_IMAGE\_01,.subtitle\_template\_string = "message"}}; ``` --- # App Metadata Source: https://developer.repebble.com/guides/tools-and-resources/app-metadata/ ## Project Defaults After creating a new local SDK Pebble project, additional setup of the project behavior and metadata can be performed in the `package.json` file. By default, the `pebble new-project` command will create a template `package.json` metadata file, which can then be modified as required. The generated `package.json` metadata looks like this: ``` {"name": "example-app","author": "MakeAwesomeHappen","version": "1.0.0","keywords": ["pebble-app"],"private": true,"dependencies": {},"pebble": {"displayName": "example-app","uuid": "5e5b3966-60b3-453a-a83b-591a13ae47d5","sdkVersion": "3","enableMultiJS": true,"targetPlatforms": ["aplite","basalt","chalk"],"watchapp": {"watchface": false},"messageKeys": ["dummy"],"resources": {"media": []}}} ``` **Notice** Older projects might have an `appinfo.json` file instead of a `package.json`. You can run `pebble convert-project` to update these projects. > Note: The `uuid` (Universally Unique IDentifier) field should be unique (as the name implies) and always properly generated. To do this on Mac OS X or Linux, use the `uuidgen` tool or a web service. Due to format requirements, these should never be manually modified. ## Available Properties Options defined in `package.json` (Required fields shown in **bold** ) are listed in the table below: | Property | Type | Default Value | Description | | --- | --- | --- | --- | | **`pebble.uuid`** | UUID | Developer-defined | Unique identifier [(UUID v4)](http://en.wikipedia.org/wiki/Universally_unique_identifier#Version_4_.28random.29) for the app. Generated by `pebble new-project`. | | **`name`** | String | Developer-defined | App's short name. This is only used by npm and can't contain any non-URL-safe characters. | | **`pebble.displayName`** | String | Developer-defined | App's long name. This is what the app will appear as on the appstore, phone and watch. | | **`author`** | String | `"MakeAwesomeHappen"` | Name of the app's developer. | | **`version`** | String | `"1.0.0"` | Version label for the app. Must use the format `major.minor.0`. | | **`pebble.sdkVersion`** | String | `"3"` | The major version of the SDK that this app is being written for. | | `pebble.targetPlatforms` | Array of strings | `["aplite", "basalt", "chalk"]` | Specify which platforms to build this app for. Read [_Hardware Information_](https://developer.repebble.com/guides/tools-and-resources/hardware-information.md) for a list of available platforms. Defaults to all if omitted. | | `pebble.enableMultiJS` | Boolean | `true` | Enables [use of multiple JS files](https://developer.repebble.com/blog/2016/01/29/Multiple-JavaScript-Files.md) in a project with a CommonJS-style `require` syntax. | | **`pebble.watchapp`** | Object | N/A | Used to configure the app behavior on the watch. See below for an example object. | | **`.watchface`** | Boolean | `false` | Field of `watchapp`. Set to `false` to behave as a watchapp. | | `.hiddenApp` | Boolean | `false` | Field of `watchapp`. Set to `true` to prevent the app from appearing in the system menu. | | `.onlyShownOnCommunication` | Boolean | `false` | Field of `watchapp`. Set to `true` to hide the app unless communicated with from a companion app. | | `pebble.capabilities` | Array of strings | None | List of capabilities that the app requires. The supported capabilities are `location`, `configurable` (detailed in [_PebbleKit JS_](https://developer.repebble.com/guides/communication/using-pebblekit-js.md)), and `health` (detailed in [_Pebble Health_](https://developer.repebble.com/guides/events-and-services/health.md)). | | `pebble.messageKeys` | Object | `["dummy"]` | Keys used for [`AppMessage`](https://developer.repebble.com/docs/c/Foundation/AppMessage.md "AppMessage") and [`AppSync`](https://developer.repebble.com/docs/c/Foundation/AppSync.md "AppSync"). This is either a list of mapping of [`AppMessage`](https://developer.repebble.com/docs/c/Foundation/AppMessage.md "AppMessage") keys. See [_PebbleKit JS_](https://developer.repebble.com/guides/communication/using-pebblekit-js.md) for more information. | | `pebble.resources.media` | Object | `[]` | Contains an array of all of the media resources to be bundled with the app (Maximum 256 per app). See [_App Resources_](https://developer.repebble.com/guides/app-resources.md) for more information. | | `pebble.resources.publishedMedia` | Object | | Used for [_AppGlance Slices_](https://developer.repebble.com/guides/user-interfaces/appglance-c.md) and [_Timeline Pins_](https://developer.repebble.com/guides/pebble-timeline/pin-structure.md). See [Published Media](#published-media) for more information. | > Note: `hiddenApp` and `onlyShownOnCommunication` are mutually exclusive. `hiddenApp` will always take preference. ## Hidden Watchapp The example below shows an example `watchapp` object for a watchapp that is hidden until communicated with from a companion app. ``` "watchapp": {"watchface": false,"hiddenApp": false,"onlyShownOnCommunication": true} ``` ## Published Media Introduced in SDK 4.0, `publishedMedia` provides a mechanism for applications to specify image resources which can be used within [_AppGlance Slices_](https://developer.repebble.com/guides/user-interfaces/appglance-c.md) or [_Timeline Pins_](https://developer.repebble.com/guides/pebble-timeline/pin-structure.md). The `publishedMedia` object allows your watchapp, Pebble mobile application and Pebble web services to determine which image resources to use, based on a static ID. Without this lookup table, the components would not be able to establish the correct resources to use after an update to the watchapp. Let's take a look at the structure of `publishedMedia` in more detail. We'll start by declaring some image resources which we can reference in `publishedMedia` later. In this example we have 'hot' and 'cold' icons, in 3 different sizes. ``` "resources": {"media": [{"name": "WEATHER\_HOT\_ICON\_TINY","type": "bitmap","file": "hot\_tiny.png"},{"name": "WEATHER\_HOT\_ICON\_SMALL","type": "bitmap","file": "hot\_small.png"},{"name": "WEATHER\_HOT\_ICON\_LARGE","type": "bitmap","file": "hot\_large.png"},{"name": "WEATHER\_COLD\_ICON\_TINY","type": "bitmap","file": "cold\_tiny.png"},{"name": "WEATHER\_COLD\_ICON\_SMALL","type": "bitmap","file": "cold\_small.png"},{"name": "WEATHER\_COLD\_ICON\_LARGE","type": "bitmap","file": "cold\_large.png"}]} ``` Next we declare the `publishedMedia`: ``` "resources": {// ....."publishedMedia": [{"name": "WEATHER\_HOT","id": 1,"glance": "WEATHER\_HOT\_ICON\_TINY","timeline": {"tiny": "WEATHER\_HOT\_ICON\_TINY","small": "WEATHER\_HOT\_ICON\_SMALL","large": "WEATHER\_HOT\_ICON\_LARGE"}}, {"name": "WEATHER\_COLD","id": 2,"glance": "WEATHER\_COLD\_ICON\_TINY","timeline": {"tiny": "WEATHER\_COLD\_ICON\_TINY","small": "WEATHER\_COLD\_ICON\_SMALL","large": "WEATHER\_COLD\_ICON\_LARGE"}}]} ``` As you can see above, we can declare multiple entries in `publishedMedia`. Let's look at the properties in more detail: - `name` - This must be a unique alias to reference each entry of the `publishedMedia` object. We use the `name` for the AppGlanceSlice icon or Timeline Pin icon, and the system will load the appropriate resource when required. The naming convention varies depending on where it's being used. For example, the AppGlance C API prefixes the name: `PUBLISHED_ID_WEATHER_HOT` but the other APIs use: `app://images/WEATHER_HOT`. - `id` - This must be a unique number within the set of `publishedMedia`. It must be greater than 0. - `glance` - Optional. This references the `resource` image which will be displayed if this `publishedMedia` is used as the `icon` of an [`AppGlanceSlice`](https://developer.repebble.com/docs/c/Foundation/App_Glance.md#AppGlanceSlice "AppGlanceSlice"). Size: 25x25. - `timeline` - Optional. This references the `resource` images which will be displayed if this `publishedMedia` is used for the various sizes of a timeline icon. All three sizes must be specified. Sizes: Tiny 25x25, Small 50x50, Large 80x80. > Your `publishedMedia` entry must include either `glance`, `timeline`, or both. If you specify `glance` and `timeline`, the `glance` must be the same resource as `timeline.tiny`. If you remove a `publishedMedia` entry, you must not re-use the ID. --- # Color Picker Tool Source: https://developer.repebble.com/guides/tools-and-resources/color-picker/ Click a hexagon in the color map to see its SDK constant and how to use it in your Pebble app. [Uncorrected](javascript:void(0)) [Sunlight](javascript:void(0)) | Name: | | | Sample: | | | HTML code: | `` | | Uncorrected HTML code: | `` | | SDK Constant: | `` | | Code (RGB): | `` | | Code (Hex): | `` | #### Example Code Segment --- # Command Line Tool Source: https://developer.repebble.com/guides/tools-and-resources/pebble-tool/ The Pebble SDK includes a command line tool called `pebble`. This tool allows developers to create new projects, build projects, install apps on a watch and debug them. ## Enabling the Developer Connection Most `pebble` commands allow interaction with a Pebble watch. This relies on a communication channel opened between the `pebble` tool on a computer and the Pebble mobile application on the phone. To use a physical watch, enable the [_Developer Connection_](https://developer.repebble.com/guides/tools-and-resources/developer-connection.md) in the Pebble mobile application. This is not required when using the emulator. ## Connecting to a Pebble There are several connection types possible with the command line tool. These can be used with any command that involves a watch, for example `install`. Connect to a physical watch through the CloudPebble connection. This works from any network, and requires signing in with the same Pebble account used in the Pebble mobile application (see [`login`](#login)): ``` $ pebble login$ pebble install --cloudpebble ``` Using `--phone` without an IP address also uses the CloudPebble connection. Connect to a physical watch connected to a phone on the same Wi-Fi network, where `IP` is the address shown in the Pebble mobile application when the [LAN connection](https://developer.repebble.com/guides/tools-and-resources/developer-connection.md#using-a-lan-connection) is enabled: ``` $ pebble install --phone IP ``` Connect to an already running QEMU instance available on the `HOST` and `PORT` provided. Add `--pypkjs` with `--platform PLATFORM` to also start the PebbleKit JS runtime for that platform: ``` $ pebble install --qemu HOST:PORT$ pebble install --qemu HOST:PORT --pypkjs --platform basalt ``` Connect directly to a watch using a local Bluetooth connection, where `SERIAL` is the path to the device file. On OS X, this is similar to `/dev/cu.PebbleTimeXXXX-SerialPo` or `/dev/cu.PebbleXXXX-SerialPortSe`: > Note: Replace 'XXXX' with the actual value for the watch in question. ``` $ pebble install --serial SERIAL ``` ## Configure the Pebble Tool Use the CloudPebble connection by default: ``` $ export PEBBLE_CLOUDPEBBLE=1 ``` Or save the IP address of the phone for the LAN connection in an environment variable: ``` $ export PEBBLE_PHONE=192.168.1.42 ``` > Note: Only set one of `PEBBLE_CLOUDPEBBLE` or `PEBBLE_PHONE`. When switching between the CloudPebble and LAN connections, `unset` the other variable, as it can take priority over the `--phone` or `--cloudpebble` argument. Save the default choice of emulator platform: ``` $ export PEBBLE_EMULATOR=aplite ``` Save the default instance of QEMU: ``` $ export PEBBLE_QEMU=localhost:12344 ``` ## Debugging Output Get up to four levels of `pebble` tool log verbosity. This is available for all commands: ``` # Minimum level of verbosity$ pebble install -v # Maximum logging verbosity$ pebble install -vvvv ``` ## Installing Watchapps In addition to interacting with a physical Pebble watch, the Pebble emulator included in the local SDK can be used to run and debug apps without any physical hardware. Build an app, then use `pebble install` with the `--emulator` flag to choose an emulator platform: `aplite`, `basalt`, `chalk`, `diorite`, `emery`, `flint` or `gabbro`. This allows developers to test out apps on all platforms before publishing to the Pebble appstore. For example, to emulate Pebble Time or Pebble Time Steel: ``` $ pebble install --emulator basalt ``` > Note: If no `--emulator` parameter is specified, the running emulator will be used. Two options apply to `--emulator`: `--sdk VERSION` launches the emulator with an installed SDK other than the active one, and `--vnc` starts a VNC server for the emulator display. With a suitable color app installed, the emulator will load and display it: ![](https://developer.repebble.com/assets/images/guides/publishing-tools/emulator.png) The Pebble physical buttons are emulated with the following mapping: | Button | Key | | --- | --- | | Back | `Q`, left arrow | | Up | `W`, up arrow | | Select | `S`, right arrow | | Down | `X`, down arrow | ## Pebble Timeline in the Emulator With the emulator, it is also possible to view any past and future pins on the Pebble timeline by pressing Up or Down from the main watchface screen: | ![](https://developer.repebble.com/assets/images/guides/publishing-tools/timeline-past.png) | ![](https://developer.repebble.com/assets/images/guides/publishing-tools/timeline-future.png) | | Timeline, past view | Timeline, future view | ## Commands This section summarizes the available commands that can be used in conjunction with the `pebble` tool, on the applicable SDK versions. Most are designed to interact with a watch with `--phone` or the emulator with `--emulator`. ### Project Management #### new-project ``` $ pebble new-project [--c | --alloy] [--simple] [--javascript] [--worker] [--ai] NAME ``` Create a new project called `NAME`. This will create a directory in the location the command is used, containing all essential project files. There are also a number of optional parameters that allow developers to choose features of the generated project to be created automatically: - `--simple` - Initializes the main `.c` file with a minimal app template. - `--javascript` - Creates a new application including a `./src/pkjs/index.js` file to quickly start an app with a PebbleKit JS component. Read [_PebbleKit JS_](https://developer.repebble.com/guides/communication/using-pebblekit-js.md) for more information. - `--worker` - Creates a new application including a ` ./worker_src/NAME_worker.c` file to quickly start an app with a background worker. Read [_Background Worker_](https://developer.repebble.com/guides/events-and-services/background-worker.md) for more information. - `--ai` - Adds instruction files for Claude Code and Cursor to the project, so an AI coding agent can build and test it. - `--alloy` - Creates a new [_Alloy_](https://developer.repebble.com/guides/alloy.md) project, with JavaScript that runs on the watch in `src/embeddedjs/`. The C-specific options above do not apply. - `--c` - Creates a C project. This is the default. #### new-package ``` $ pebble new-package [--javascript] NAME ``` Create a new Pebble package called `NAME`, for publishing a library to npm rather than an app. `--javascript` adds a `js` directory. Read [_Pebble Packages_](https://developer.repebble.com/guides/pebble-packages.md) for more information. #### build ``` $ pebble build [--debug] ``` Compile and build the project into a `.pbw` file that can be installed on a watch or the emulator. `--debug` builds without optimizations for easier debugging with `pebble gdb`. Any further arguments are passed to `waf`. #### install ``` $ pebble install [FILE] [--logs] [--force] [--throttle [SECONDS]] [--qemu_logs] ``` Install the current project to the watch connected to the phone with the given `IP` address, or to the emulator on the `PLATFORM`. See [_Hardware Information_](https://developer.repebble.com/guides/tools-and-resources/hardware-information.md) for a list of available platforms. For example, the Aplite platform is specified as follows: ``` $ pebble install --emulator aplite ``` It is also possible to use this command to install a pre-compiled `.pbw` `FILE`. In this case, the specified file will be installed instead of the current project. > Note: A `FILE` parameter is not required if running `pebble install` from inside a buildable project directory. In this case, the `.pbw` package is found automatically in `./build`. - `--logs` - Display app logs after installing, as `pebble logs` does. - `--force` - Install even if the `.pbw` does not support the connected platform. - `--throttle [SECONDS]` - Slow down the transfer to avoid QEMU timeouts with large apps. Defaults to 0.004 seconds between packets when given without a value. - `--qemu_logs` - Show the QEMU serial log while installing on the emulator. #### clean ``` $ pebble clean ``` Delete all build files in `./build` to prepare for a clean build, which can resolve some state inconsistencies that may prevent the project from building. #### convert-project ``` $ pebble convert-project ``` Convert an existing Pebble project to the current SDK. > Note: This will only convert the project, the source code will still have to updated to match any new APIs. #### analyze-size ``` $ pebble analyze-size [ELF] [--summary] [--verbose] ``` Print the size of each section of the built app, to see how much of the platform's app memory the code and static data use. `ELF` defaults to the build output of the current project. `--summary` prints one line per section and `--verbose` adds a per-symbol breakdown. #### package ``` $ pebble package install [PACKAGE]$ pebble package uninstall PACKAGE$ pebble package login$ pebble package publish ``` Manage the [_Pebble Packages_](https://developer.repebble.com/guides/pebble-packages.md) a project depends on. `install` adds an npm package to `package.json` and installs it, `uninstall` removes one, `login` signs in to npm and `publish` publishes the current package to npm. #### publish ``` $ pebble publish [--release-notes TEXT] [--is-published] [--non-interactive] [--all-platforms] [--no-gif-all-platforms] ``` Build the project and upload it as a new release to the app's listing on the [Developer Dashboard](https://developer.repebble.com/dashboard). Requires `pebble login`. If the app does not have a listing yet, the command prompts for the details to create one. `--non-interactive` uses flags (`--name`, `--version`, `--description`, `--source`, `--category`, `--icon-small`, `--icon-large`, `--screenshots FILE...`) instead of prompts, for CI. By default the command captures a rollover GIF on the emulator for each platform the app supports before uploading; `--no-gif-all-platforms` skips this and `--all-platforms` captures static screenshots as well. Uploaded releases are published immediately; `--is-published` is accepted but has no effect in the current version. ### SDK Management #### sdk ``` $ pebble sdk list$ pebble sdk install VERSION$ pebble sdk activate VERSION$ pebble sdk uninstall [--keep-data] VERSION$ pebble sdk set-channel CHANNEL$ pebble sdk include-path PLATFORM [--sdk VERSION] ``` Manage the installed SDKs. `list` shows the installed SDKs and the versions available for download, `install` downloads and installs a version (`latest` for the newest), `activate` selects which installed SDK `pebble build` and the emulator use, and `uninstall` removes one, keeping its emulator data with `--keep-data`. `set-channel` selects the release channel that `list` and `install` read from. `include-path` prints the directory of SDK headers for `PLATFORM`, for editor and language server configuration. Several SDK versions can be installed side by side. The SDK version (4.x) is separate from the version of the `pebble` tool itself (5.x). ### Pebble Interaction #### logs ``` $ pebble logs ``` Continuously display app logs from the watch connected to the phone with the given `IP` address, or from a running emulator. App log output is colorized according to log level. This behavior can be forced on or off by specifying `--color` or `--no-color` respectively. #### screenshot ``` $ pebble screenshot [FILENAME] [--no-correction] [--no-open]$ pebble screenshot --all-platforms$ pebble screenshot --gif-all-platforms [--gif-fps FPS] ``` Take a screenshot of the watch connected to the phone with the given `IP` address, or from any running emulator. If provided, the output is saved to `FILENAME`. Color correction may be disabled by specifying `--no-correction`. The auto-opening of screenshots may also be disabled by specifying `--no-open`. `--all-platforms` takes a screenshot on the emulator for each platform the current project supports, building the project first only if `build/` has no `.pbw` yet; run `pebble build` before it to capture current code. `--gif-all-platforms` does the same but records a rollover GIF of the app on each platform, capped at `--gif-fps` frames per second (default 30). These are the images `pebble publish` uploads. #### ping ``` $ pebble ping ``` Send a `ping` to the watch connected to the phone with the given `IP` address, or to any running emulator. #### repl ``` $ pebble repl ``` Launch an interactive python shell with a `pebble` object to execute methods on, using the watch connected to the phone with the given `IP` address, or to any running emulator. #### send-app-message ``` $ pebble send-app-message [--int KEY=VALUE ...] [--uint KEY=VALUE ...] [--string KEY=VALUE ...] [--bytes KEY=HEX ...] [--bytes-file KEY=FILE ...] [--app-uuid UUID] ``` Send an [`AppMessage`](https://developer.repebble.com/docs/c/Foundation/AppMessage.md "AppMessage") dictionary to the running watchapp, to test its inbox handler without a PebbleKit JS or companion app. Keys are integers. Each option takes one or more `KEY=VALUE` entries, for example `--int 1=42 2=43 --string 3=hello`. Give each option once; a repeated option replaces the earlier entries. `--app-uuid` selects the target app when it is not the current project. #### fw ``` $ pebble fw install FILE [--slot {0,1}]$ pebble fw install-lang FILE$ pebble fw coredump [FILENAME] [--fresh]$ pebble fw flash-logs --board BOARD$ pebble fw enter-prf ``` Firmware management for a connected watch. `install` installs a `.pbz` firmware bundle (`--slot` selects the slot for multi-slot bundles), `install-lang` installs a language pack, `coredump` downloads the most recent coredump from the watch (`--fresh` requires one that has not been read yet), `flash-logs` dumps the firmware's flash log for the given board, and `enter-prf` reboots the watch into the recovery firmware. #### data-logging ##### list ``` $ pebble data-logging list ``` List the current data logging sessions on the watch. ##### download ``` $ pebble data-logging download --session-id SESSION-ID FILENAME ``` Downloads the contents of the data logging session with the given ID (as given by `pebble data-logging list`) to the given filename. ##### disable-sends ``` pebble data-logging disable-sends ``` Disables automatically sending data logging to the phone. This enables downloading data logging information without the phone's interference. It will also suspend any other app depending on data logging, which includes some firmware functionality. You should remember to enable it once you are done testing. ##### enable-sends ``` pebble data-logging enable-sends ``` Re-enables automatically sending data logging information to the phone. ##### get-sends-enabled ``` pebble data-logging get-sends-enabled ``` Indicates whether data logging is automatically sent to the phone. Change state with `enable-sends` and `disable-sends`. ### Emulator Interaction > Note: If the emulator does not boot, keeps apps that were uninstalled or stays on one screen, run `pebble kill` to stop it and then `pebble wipe` to delete its stored data. The next `pebble install` starts the emulator with a fresh flash image. #### gdb ``` $ pebble gdb ``` Use [GDB](https://www.gnu.org/software/gdb/) to step through and debug an app running in an emulator. Some useful commands are displayed in a cheat sheet when run with `--help` and summarized in the table below: > Note: Emulator commands that rely on interaction with the emulator (e.g. `emu-app-config`) will not work while execution is paused with GDB. | Command | Description | | --- | --- | | ctrl-C | Pause app execution. | | `continue` or `c` | Continue app execution. The app is paused while a `(gdb)` prompt is available. | | ctrl-D or `quit` | Quit gdb. | | `break` or `b` | Set a breakpoint. This can be either a symbol or a position: - `b function_name` to break when entering a function. - `b file_name.c:45` to break on line 45 of `file_name.c`. | | `step` or `s` | Step forward one line. | | `next` or `n` | Step _over_ the current line, avoiding stopping for any functions it calls into. | | `finish` | Run forward until exiting the current stack frame. | | `backtrace` or `bt` | Print out the current call stack. | | `p [expression]` | Print the result of evaluating the given `expression`. | | `info args` | Show the values of arguments to the current function. | | `info locals` | Show local variables in the current frame. | | `bt full` | Show all local variables in all stack frames. | | `info break` | List break points (#1 is ``, and is inserted by the `pebble` tool). | | `delete [n]` | Delete breakpoint #n. | Read [_Debugging with GDB_](https://developer.repebble.com/guides/debugging/debugging-with-gdb.md) for help on using GDB to debug app. #### emu-control ``` $ pebble emu-control [--port PORT] ``` Send near real-time accelerometer and compass readings from a phone, tablet, or computer to the emulator. When run, a QR code will be displayed in the terminal containing the IP address the device should connect to. Scanning the code will open this address in the device's browser. The IP address itself is also printed in the terminal for manual entry. `PORT` may be specified in order to make bookmarking the page easier. ![qr-code](https://developer.repebble.com/assets/images/guides/publishing-tools/qr-code.png) After connecting, the device's browser will display a UI with two modes of operation. ![accel-ui](https://developer.repebble.com/assets/images/guides/publishing-tools/accel-ui.png) The default mode will transmit sensor readings to the emulator as they are recorded. By unchecking the 'Use built-in sensors?' checkbox, manual values for the compass bearing and each of the accelerometer axes can be transmitted to the emulator by manipulating the compass rose or axis sliders. Using these UI elements will automatically uncheck the aforementioned checkbox. The values shown in bold are the Pebble SDK values, scaled relative to [`TRIG_MAX_ANGLE`](https://developer.repebble.com/docs/c/Foundation/Math.md#TRIG_MAX_ANGLE "TRIG\_MAX\_ANGLE") in the case of the compass bearing value, and in the range of +/-4000 (+/- 4g) for the accelerometer values. The physical measurements with associated units are shown beside in parentheses. #### emu-app-config ``` $ pebble emu-app-config [--file FILE] ``` Open the app configuration page associated with the running app, if any. Uses the HTML configuration `FILE` if provided. See [_App Configuration_](https://developer.repebble.com/guides/user-interfaces/app-configuration.md) to learn about emulator- specific configuration behavior. #### emu-tap ``` $ pebble emu-tap [--direction DIRECTION] ``` Send an accelerometer tap event to any running emulator. `DIRECTION` can be one of `x+`, `x-`, `y+`, `y-`, `z+`, or `z-`. #### emu-bt-connection ``` $ pebble emu-bt-connection --connected STATE ``` Send a Bluetooth connection event to any running emulator. `STATE` can be either `yes` or `no` for connected and disconnected events respectively. > Note: The disconnected event may take a few seconds to occur. #### emu-compass ``` $ pebble emu-compass --heading BEARING [--uncalibrated | --calibrating | --calibrated] ``` Send a compass update event to any running emulator. `BEARING` must be a number between `0` and `359` to be the desired compass bearing. Use any of `--uncalibrated`, `--calibrating`, or `--calibrated` to set the calibration state. #### emu-battery ``` $ pebble emu-battery [--percent LEVEL] [--charging] ``` Send a battery update event to any running emulator. `LEVEL` must be a number between `0` and `100` to represent the new battery level. The presence of `--charging` will determine whether the charging cable is plugged in. #### emu-accel ``` $ pebble emu-accel DIRECTION [FILE] ``` Send accelerometer data events to any running emulator. `DIRECTION` can be any of `tilt-left`, `tilt-right`, `tilt-forward`, `tilt-back`, `gravity+x`, `gravity-x`, `gravity+y`, `gravity-y`, `gravity+z`, `gravity-z`, `none` and `custom`. If `custom` is selected, give a `FILE` with one line per reading of comma-separated x, y and z values. #### transcribe ``` $ pebble transcribe [message] [--error {connectivity,disabled,no-speech-detected}] ``` Run a server that will act as a transcription service. Run it before invoking the [`Dictation`](https://developer.repebble.com/docs/c/Foundation/Dictation.md "Dictation") service in an app. If `message` is provided, the dictation will be successful and that message will be provided. If `--error` is provided, the dictation will fail with the given error. Exactly one of `message` and `--error` is required. ``` $ pebble transcribe "Hello, Pebble!" ``` #### kill ``` $ pebble kill [--force] ``` Kill both the Pebble emulator and phone simulator. `--force` sends `SIGKILL`. #### emu-time-format ``` pebble emu-time-format --format FORMAT ``` Set the time format of the emulator to either 12-hour or 24-hour format, with a `FORMAT` value of `12h` or `24h` respectively. #### emu-set-timeline-quick-view ``` $ pebble emu-set-timeline-quick-view STATE ``` Show or hide the Timeline Quick View system overlay. STATE can be `on` or `off`. #### emu-set-content-size ``` $ pebble emu-set-content-size SIZE ``` Set the system content size on the emulator, as the user does in _Settings_. `SIZE` can be `small`, `medium`, `large` or `x-large`. Read [_Content Size_](https://developer.repebble.com/guides/user-interfaces/content-size.md) for how apps respond to it. #### emu-set-time ``` $ pebble emu-set-time TIME [--utc] ``` Set the emulator's clock. `TIME` is either `HH:MM:SS` for a time today, or a Unix timestamp in seconds. `--utc` interprets `HH:MM:SS` as UTC rather than local time. #### emu-button ``` $ pebble emu-button {click,push,release} [BUTTON ...] [--duration MS] [--repeat N] [--interval MS] ``` Press buttons on the emulator from the command line. `click` presses and releases, `push` holds a button down and `release` lets it go. `BUTTON` is one or more of `back`, `up`, `select` and `down`; several buttons are pressed together. `--duration` sets the click length in milliseconds (default 100), `--repeat` the number of clicks (default 1) and `--interval` the gap between them (default 200). #### wipe ``` $ pebble wipe ``` Delete the data stored for the emulator of every platform in the current SDK, including the flash image that holds installed apps and settings and the phone simulator's PebbleKit JS storage, but not the logged in Pebble account. Run `pebble kill` first so that a running emulator does not keep the old data. To wipe **all** data for every SDK version and log out, specify `--everything`. ### Pebble Account Management #### login ``` $ pebble login [--status] [--no-open-browser] ``` Sign in to your Pebble account. The command opens a browser window to complete the sign in; `--no-open-browser` prints the URL instead, for use over SSH. Signing in is required for the CloudPebble connection (`--cloudpebble`) and for `pebble publish`. `--status` prints whether you are signed in and whether the account is linked to a Developer Dashboard account, then exits. #### logout ``` $ pebble logout ``` Logs out the currently logged in Pebble account from this instance of the command line tool. ### Pebble Timeline Interaction #### insert-pin ``` $ pebble insert-pin FILE [--id ID] [--app-uuid UUID] ``` Insert a JSON pin from `FILE` into the timeline of the connected watch or emulator. `-` reads the pin from standard input. `--id` supplies the pin `id` when the file has none; if both are given they must match. `--app-uuid` sets the pin's parent app when it is not the current project. #### delete-pin ``` $ pebble delete-pin --id ID ``` Delete a pin previously inserted with `insert-pin`, specifying the same pin `ID`. --- # Developer Connection Source: https://developer.repebble.com/guides/tools-and-resources/developer-connection/ In order to install apps and view logs on a physical watch from CloudPebble or the `pebble` tool, the Pebble mobile app must be set up to allow a connection from the computer to the watch. This is called the Developer Connection, and it speeds up development by letting you install apps directly from your development environment. The Developer Connection can work in two ways: - **CloudPebble connection** (default) - The phone connects to the CloudPebble proxy using your Pebble account. CloudPebble and the `pebble` tool reach the phone through the proxy by signing in to the same account. This works from anywhere, and the phone and computer do not need to be on the same network. - **LAN connection** - The phone runs a server on port 9000 that the `pebble` tool connects to directly using the phone's IP address. This requires the phone and computer to be on the same Wi-Fi network. The steps below are the same in the Android and iOS versions of the Pebble app. ## Enabling the Developer Connection In the Pebble mobile app: - Make sure you are signed in with your Pebble account. If not, go to _Settings_ \> _General_ and tap _Sign In - Pebble Account_. - Make sure your watch is connected, then open the _Devices_ tab. - Tap the three dot icon on your watch to open its menu, then enable the _Dev Connection_ toggle. - The menu will show _Connected to CloudPebble_ once the connection is enabled. ![](https://developer.repebble.com/assets/images/guides/publishing-tools/dev-connection-cloudpebble.png) While the Developer Connection is enabled, a small developer icon is shown next to the watch's model name in the _Devices_ tab. > Note: The _Dev Connection_ option is only shown while the watch is connected. A watch that is out of range, in a low-power state or still reconnecting does not show it; wait for the _Devices_ tab to report the watch as connected and open the menu again. It cannot be enabled while signed out, unless the LAN connection is turned on (see [below](#using-a-lan-connection)). The Developer Connection always goes through the phone. The watch's charging cable does not carry data, so apps cannot be installed or debugged over it. ## Using with CloudPebble - Sign in to [CloudPebble](https://cloudpebble.repebble.com) with the same Pebble account you use in the Pebble mobile app. - Open your project and open _Build & Run_ in the sidebar. The _Phone_ tab appears once the project has been built successfully. - Under _Run On_, select the _Phone_ tab and choose _New Cloud Dev Connection_. - Click _Install and Run_. Use _View app logs_ to see the logs from your app, and _Screenshot_ to take a screenshot of the watch. The _Phone_ tab offers three connection types. All three reach the phone through the CloudPebble proxy; they differ in how CloudPebble signs in to it. | Option | Signs in with | Use it when | | --- | --- | --- | | _New Cloud Dev Connection_ | Your Pebble account, the one CloudPebble is signed in with | Always, with the current Pebble mobile app. | | _Cloud Dev Connection_ | A GitHub account linked under _Settings_ \> _GitHub Integrations_ | Only with earlier versions of the Pebble mobile app, which signed in to the Developer Connection with GitHub. | | _Local Dev Connection_ | The token of a legacy Pebble account sign-in | Not with the current Pebble mobile app. | CloudPebble cannot connect to the phone's IP address directly, so the LAN connection described below is for the `pebble` tool only. ## Using with the `pebble` Tool - Sign in to your Pebble account. This opens a browser window to complete the sign in: ``` $ pebble login ``` - Use the `--cloudpebble` argument with any command that interacts with a watch. The tool will wait for the phone to connect to the CloudPebble proxy before continuing: ``` $ pebble install --cloudpebble $ pebble logs --cloudpebble ``` Using `--phone` without an IP address does the same thing. To use the CloudPebble connection by default, set the `PEBBLE_CLOUDPEBBLE` environment variable: ``` $ export PEBBLE_CLOUDPEBBLE=1 ``` ## Using a LAN Connection The LAN connection lets the `pebble` tool connect to the phone directly over your local network, without signing in. CloudPebble does not offer a way to enter the phone's IP address. Its _Local Dev Connection_ option still goes through the cloud proxy, so use the `pebble` tool for the LAN connection. > Note: The LAN connection is not authenticated. Anyone on the same network can install apps on your watch while it is enabled, so only use it on trusted networks. In the Pebble mobile app: - Open the _Settings_ tab, select _Phone_ at the top, and tap _Connectivity_. ![](https://developer.repebble.com/assets/images/guides/publishing-tools/dev-connection-settings.png) - Enable _Use LAN developer connection_. ![](https://developer.repebble.com/assets/images/guides/publishing-tools/dev-connection-lan-setting.png) - Go back to the _Devices_ tab, open your watch's menu and enable the _Dev Connection_ toggle. If it was already enabled, toggle it off and on again. - Make note of the _IPv4_ address shown under the toggle. ![](https://developer.repebble.com/assets/images/guides/publishing-tools/dev-connection-lan.png) Use the IP address with the `--phone` argument: ``` $ pebble install --phone 192.168.1.42$ pebble logs --phone 192.168.1.42 ``` The server always uses port 9000. To avoid typing the IP address each time, save it in an environment variable: ``` $ export PEBBLE_PHONE=192.168.1.42 ``` > Note: Only set one of `PEBBLE_CLOUDPEBBLE` or `PEBBLE_PHONE`. When switching between the CloudPebble and LAN connections, `unset` the other variable, as it can take priority over the `--phone` or `--cloudpebble` argument. ### Connecting over USB on Android On Android, the LAN connection can also be used over USB with `adb`, which works even when the phone and computer are on different networks. With the phone connected over USB and the Pebble app running, enable the LAN connection and forward port 9000 to the computer: ``` $ adb shell am broadcast -a coredevices.coreapp.DEV_CONNECTION \ -n coredevices.coreapp/coredevices.coreapp.debug.DevConnectionReceiver$ adb forward tcp:9000 tcp:9000$ pebble install --phone 127.0.0.1 ``` ## Working Without Internet Access The CloudPebble connection needs the phone and the computer to reach the proxy on the internet. Without internet access, use the LAN connection with `--phone` and the phone's IP address. The phone and computer only need to be on the same Wi-Fi network, for example a hotspot started on the phone. On Android, the USB method above works with no network at all. The `--serial` argument of the `pebble` tool connects to a watch over a Bluetooth serial port on the computer. It only works with watches that support Bluetooth Classic. Pebble 2 Duo, Pebble Time 2 and Pebble Round 2 use Bluetooth LE only, so `--serial` cannot be used with them. ## Limitations Inserting timeline pins with `pebble insert-pin` is not currently supported over the Developer Connection. --- # Hardware Information Source: https://developer.repebble.com/guides/tools-and-resources/hardware-information/ The Pebble watch family comprises of multiple generations of hardware, each with unique sets of features and capabilities. Developers wishing to reach the maximum number of users will want to account for these differences when developing their apps. The table below details the differences between hardware platforms: | Company | Pebble Technology Corporation | Core Devices | | --- | --- | --- | | Model | Classic, Steel | Time, Time Steel | Time Round | Pebble 2 | Pebble 2 Duo | Pebble Time 2\* | Pebble Round 2 | | --- | --- | --- | --- | --- | --- | --- | --- | | Platform | Aplite | Basalt | Chalk | Diorite | Flint | Emery | Gabbro | | SOC | [STMicro STM32F205RE](https://www.st.com/en/microcontrollers-microprocessors/stm32f205re.html) | [STMicro STM32F411](https://www.st.com/en/microcontrollers-microprocessors/stm32f411.html) | [Nordic nRF52840](https://www.nordicsemi.com/Products/nRF52840) | [SiFli SF32LB52J](https://www.sifli.com/en/node/31) | | CPU | Cortex-M3 64 MHz | Cortex-M4 100 MHz | Cortex-M4 64 MHz | [Star-MC1](https://www.armchina.com/mountain?infoId=160&name=%22%E6%98%9F%E8%BE%B0%22%20STAR-MC1&CategoryStr=%22%E6%98%9F%E8%BE%B0%22&pStatus=) (Cortex-M33-like) 240 MHz | | Max Resource Size | 96k | 256k | | Max App Size (code + heap) | 24k | 64k | 128k | | Display Shape | Rectangle | Round | Rectangle | Round | | Display Size | 1.26" | 1.25" | 1" | 1.26" | 1.5" | 1.3" | | Display Resolution | 144 x 168 | 180 x 180 | 144 x 168 | 200 x 228 | 260 x 260 | | Display PPI | 175 | 182 | 175 | 202 | 200 | | Display Colors | 2 _(B/W)_ | 64 _(Color)_ | 2 _(B/W)_ | 64 _(Color)_ | | Display Manufacturer | Sharp _(LS013B7DH05)_ | JDI _(Color)_ | JDI _(Color)_ | Sharp _(LS013B7DH05)_ | JDI _(LPM015M135A)_ | Sharp _(LS013B7DD02)_ | | Touch Screen | No | Yes | | Backlight | White LED | Multicolor RGB LED | White LED | | Heart Rate Monitor | No | No _(possible with smartstrap)_ | Yes _(except SE model)_ | No | Yes | No | | Microphone | No | Yes | Yes _(with 2nd Mic for ENC\*1)_ | | Speaker | No | Yes | No | | Sensors | Accelerometer, Compass | Accelerometer | 6-axis IMU, Compass, Barometer | 6-axis IMU, Compass | 3-axis IMU, Compass | | Buttons | 4 | | Vibration | ERM (eccentric rotating mass) | LRA (linear resonant actuator) | | Case Material | Polycarbonate _(P, PT)_ Stainless Steel 316 _(PS, PTS)_ | Stainless Steel 316 | Polycarbonate | Stainless Steel 316 | | Lens Material | Polycarbonate | Gorilla Glass _(curved)_ | Gorilla Glass _(flat)_ | Hardened Glass _(flat)_ | Hardened Glass _(curved)_ | | Charging Port | Power only | Smart accessory port | Smart accessory port _(data only)_ | Power only | | Battery Life\*4 | ~7 days | ~7 days (Pebble Time), ~10 days (Pebble Time Steel) | ~2 days | ~7 days | ~30 days | ~14 days | | Water Resistance\*3 | 5 ATM\*2 _(50 m)_ | 3 ATM\*2 _(30 m)_ | None _(Splash Resistant)_ | 3 ATM\*2 _(30 m)_ | 20 m | TBD _(target IPX8)_ | TBD _(target 30m)_ | | Model | Classic, Steel | Time, Time Steel | Time Round | Pebble 2 | Pebble 2 Duo | Pebble Time 2\* | Pebble Round 2 | | --- | --- | --- | --- | --- | --- | --- | --- | \*1 ENC = Environmental Noise Cancellation \*2 Please note regarding water resistance: - 3 ATM → Safe for hand-washing, rain, splashes, and brief immersion, but not for swimming/diving. - 5 ATM → Safe for swimming in shallow water, showering, and snorkeling, but not for diving. \*3Rated waterproofing is applicable to new, sealed hardware. Age or repairs may compromise water resistance. \*4Rated battery life is applicable to new hardware in typical usage conditions. Heavy use or use of power intensive apps and watchfaces may lead to a lower battery life. Battery life will reduce as the watch ages. Aftermarket replacement batteries may have a lower capacity. See [_Available Defines and Macros_](https://developer.repebble.com/guides/best-practices/building-for-every-pebble.md#available-defines-and-macros) for a complete list of compile-time defines available. ## Platform Support Each Pebble watch is identified by a platform name in the SDK: | Platform | Watches | | --- | --- | | `aplite` | Pebble, Pebble Steel | | `basalt` | Pebble Time, Pebble Time Steel | | `chalk` | Pebble Time Round | | `diorite` | Pebble 2 | | `flint` | Pebble 2 Duo | | `emery` | Pebble Time 2 | | `gabbro` | Pebble Round 2 | Not every language and feature is available on every platform: | Feature | Platforms | Notes | | --- | --- | --- | | C SDK | All | | | Alloy (JavaScript on the watch) | `emery`, `gabbro` | The firmware includes the Alloy runtime on `emery` and `gabbro` only, and the `pebble` tool templates target those two. CloudPebble also lists `flint`, but Alloy apps do not run on `flint` yet. `aplite`, `basalt`, `chalk` and `diorite` are not supported. See [_Alloy_](https://developer.repebble.com/guides/alloy.md). | | PebbleKit JS | All | Runs on the phone, in the Pebble mobile app. | | Touch | `emery`, `gabbro` | Watchapps only. Watchfaces cannot receive touch events. See [_Touch_](https://developer.repebble.com/guides/events-and-services/touch.md). | | Heart rate monitor | `diorite` (except Pebble 2 SE), `emery` | See [_Heart Rate Monitor_](https://developer.repebble.com/guides/events-and-services/hrm.md). | | Speaker | `flint`, `emery` | See [_Speaker_](https://developer.repebble.com/guides/events-and-services/speaker.md). | | Microphone | All except `aplite` | See [_Dictation_](https://developer.repebble.com/guides/events-and-services/dictation.md). | Watchfaces cannot receive button or touch events on any platform. --- # Internationalization Source: https://developer.repebble.com/guides/tools-and-resources/internationalization/ When distributing an app in the Pebble appstore, it can be downloaded by Pebble users all over the world. Depending on the app, this means that developers may want to internationalize their apps for a varierty of different locales. The locale is the region in the world in which the user may be using an app. The strings used in the app to convey information should be available as translations in locales where the app is used by speakers of a different language. This is the process of localization. For example, users in France may prefer to see an app in French, if the translation is available. ## Internationalization on Pebble The Pebble SDK allows an app to be localized with the [`Internationalization`](https://developer.repebble.com/docs/c/Foundation/Internationalization.md "Internationalization") API. Developers can use this to adjust how their app displays and behaves to cater for users in non-English speaking countries. By default all apps are assumed to be in English. Choose the user selected locale using [`i18n_get_system_locale()`](https://developer.repebble.com/docs/c/Foundation/Internationalization.md#i18n_get_system_locale "i18n\_get\_system\_locale") or to force one using [`setlocale()`](https://developer.repebble.com/docs/c/Standard_C/Locale.md#setlocale "setlocale"). > Note: The app's locale also affects [`strftime()`](https://developer.repebble.com/docs/c/Standard_C/Time.md#strftime "strftime"), which will output different strings for different languages. Bear in mind that these strings may be different lengths and use different character sets, so the app's layout should scale accordingly. ## Locales Supported By Pebble This is the set of locales that are currently supported: | Language | Region | Value returned by [`i18n_get_system_locale()`](https://developer.repebble.com/docs/c/Foundation/Internationalization.md#i18n_get_system_locale "i18n\_get\_system\_locale") | | --- | --- | --- | | English | United States | `"en_US"` | | French | France | `"fr_FR"` | | German | Germany | `"de_DE"` | | Spanish | Spain | `"es_ES"` | | Italian | Italy | `"it_IT"` | | Portuguese | Portugal | `"pt_PT"` | | Chinese | China | `"en_CN"` | | Chinese | Taiwan | `"en_TW"` | When the user's preferred language is set using the Pebble Time mobile app, the required characters will be loaded onto Pebble and will be used by the compatible system fonts. **This means that any custom fonts used should include the necessary locale-specific characters in order to display correctly.** ## Translating an App By calling `setlocale(LC_ALL, "")`, the app can tell the system that it supports internationalization. Use the value returned by [`setlocale()`](https://developer.repebble.com/docs/c/Standard_C/Locale.md#setlocale "setlocale") to translate any app strings. The implementation follows the [libc convention](https://www.gnu.org/software/libc/manual/html_node/Setting-the-Locale.html#Setting-the-Locale). Users may expect text strings, images, time display formats, and numbers to change if they use an app in a certain language. Below is a simple approach to providing a translation: ``` // Use selocale() to obtain the system locale for translationchar \*sys\_locale = setlocale(LC\_ALL, "");// Set the TextLayer's contents depending on localeif (strcmp("fr\_FR", sys\_locale) == 0) {text\_layer\_set\_text(s\_output\_layer, "Bonjour tout le monde!");} else if ( /\* Next locale string comparison \*/ ) {/\* other language cases... \*/} else {// Fall back to Englishtext\_layer\_set\_text(s\_output\_layer, "Hello, world!");} ``` The above method is most suitable for a few strings in a small number of languages, but may become unwieldly if an app uses many strings. A more streamlined approach for these situations is given below in [_Using Locale Dictionaries_](#using-locale-dictionaries). ## Using the Locale in JavaScript Determine the language used by the user's phone in PebbleKit JS using the standard [`navigator`](http://www.w3schools.com/jsref/prop_nav_language.asp) property: ``` console.log('Phone language is ' + navigator.language); ``` ``` [INFO] js-i18n-example__1.0/index.js:19 Phone language is en-US ``` This will reflect the user's choice of 'Language & Input' -\> 'Language' on Android and 'General' -\> 'Language & Region' -\> 'i[Pod or Phone] Language' on iOS. > Note: Changing the language on these platforms will require a force close of the Pebble app or device restart before changes take effect. ## Choosing the App's Locale It is also possible to explicitly set an app's locale using [`setlocale()`](https://developer.repebble.com/docs/c/Standard_C/Locale.md#setlocale "setlocale"). This is useful for providing a language selection option for users who wish to do so. This function takes two arguments; the `category` and the `locale` string. To set localization **only** for the current time, use `LC_TIME` as the `category`, else use `LC_ALL`. The `locale` argument accepts the ISO locale code, such as `"fr_FR"`. Alternatively, set `locale` to `NULL` to query the current locale or `""` (an empty string) to set the app's locale to the system default. ``` // Force the app's locale to Frenchsetlocale(LC\_ALL, "fr\_FR"); ``` To adapt the way an application displays time, call `setlocale(LC_TIME, "")`. This will change the locale used to display time and dates in [`strftime()`](https://developer.repebble.com/docs/c/Standard_C/Time.md#strftime "strftime") without translating any strings. ## Effect of Setting a Locale Besides returning the value of the current system locale, calling [`setlocale()`](https://developer.repebble.com/docs/c/Standard_C/Locale.md#setlocale "setlocale") will have a few other effects: - Translate month and day names returned by [`strftime()`](https://developer.repebble.com/docs/c/Standard_C/Time.md#strftime "strftime") `%a`, `%A`, `%b`, and `%B`. - Translate the date and time representation returned by [`strftime()`](https://developer.repebble.com/docs/c/Standard_C/Time.md#strftime "strftime") `%c`. - Translate the date representation returned by [`strftime()`](https://developer.repebble.com/docs/c/Standard_C/Time.md#strftime "strftime") `%x`. Note that currently the SDK will not change the decimal separator added by the `printf()` family of functions when the locale changes; it will always be a `.` regardless of the currently selected locale. ## Using Locale Dictionaries A more advanced method to include multiple translations of strings in an app (without needing a large number of `if`, `else` statements) is to use the [Locale Framework](https://github.com/pebble-hacks/locale_framework). This framework works by wrapping every string in the project's `src` directory in the `_()` and replacing it with with a hashed value, which is then used with the current locale to look up the translated string from a binary resource file for that language. Instructions for using this framework are as follows: - Add `hash.h`, `localize.h` and `localize.c` to the project. This will be the `src` directory in the native SDK. - Include `localize.h` in any file to be localized: ``` #include "localize.h" ``` - Initalize the framework at the start of the app's execution: ``` int main(void) {// Init locale frameworklocale\_init();/\*\* Other app setup code \*\*/} ``` - For every string in each source file to be translated, wrap it in `_()`: ``` text\_layer\_set\_text(some\_layer, \_("Meal application")); ``` - Save all source files, then run `get_dict.py` from the project root directory to get a JSON file containing the hashed strings to be translated. This file will look like the one below: ``` { "1204784839": "Breakfast Time", "1383429240": "Meal application", "1426781285": "A fine meal with family", "1674248270": "Lunch Time", "1753964326": "Healthy in a hurry", "1879903262": "Start your day right", "2100983732": "Dinner Time" } ``` - Create a copy of the JSON file and perform translation of each string, keeping the equivalent hash values the same. Name the file according to the language, such as `locale_french.json`. - Convert both JSON files into app binary resource files: ``` $ python dict2bin.py locale_english.json $ python dict2bin.py locale_french.json ``` - Add the output binary files as project resources as described in [_Raw Data Files_](https://developer.repebble.com/guides/app-resources/raw-data-files.md). When the app is compiled and run, the `_()` macro will be replaced with calls to `locale_str()`, which will use the app's locale to load the translated string out of the binary resource files. > Note: If the translation lookup fails, the framework will fall back to the English binary resource file. ## Publishing Localized Apps The appstore does not yet support localizing an app's resources (such as name, description, images etc), so use this guide to prepare the app itself. To cater for users viewing appstore listings in a different locale, include a list of supported languages in the listing description, which itself can be written in multiple languages. The format for a multi-lingual appstore description should display the supported languages at the top and a include a copy in each language, as shown in the example below: > Now supports - French, German and Spanish. > > This compass app allows you to navigate in any directions from your wrist! Simply open the app and tilt to show the layout you prefer. > > Français > > Cette application de la boussole vous permet de naviguer dans toutes les directions à partir de votre poignet! Il suffit d'ouvrir l'application et de l'inclinaison pour montrer la disposition que vous préférez. > > Deutsch > > Dieser Kompass App ermöglicht es Ihnen, in jeder Richtung vom Handgelenk zu navigieren! Öffnen Sie einfach die App und kippen, um das Layout Sie bevorzugen zu zeigen. > > Español > > Esta aplicación brújula le permite navegar en cualquier dirección de la muñeca ! Basta con abrir la aplicación y la inclinación para mostrar el diseño que prefiera. --- # Testing with the Pebble Mobile App Source: https://developer.repebble.com/guides/tools-and-resources/testing-with-the-mobile-app/ The `pebble` tool runs its own PebbleKit JS environment next to the emulator, which is enough for most development. Some things only the Pebble mobile app does: the phone-side settings screen that opens configuration pages, the appstore locker, timeline pins created by PebbleKit JS and the real JavaScript runtimes described in [_PebbleKit JS_](https://developer.repebble.com/guides/communication/using-pebblekit-js.md#differences-in-the-pebble-mobile-app). To test those without a watch, the Pebble mobile app can connect to a PebbleOS emulator over TCP in place of a Bluetooth watch. This needs the PebbleOS emulator built from source, since the Pebble mobile app connects to the emulator's Pebble Protocol port and only one client can use that port at a time. The emulator started by `pebble install --emulator` is already attached to the `pebble` tool's own PebbleKit JS runtime, so it cannot be used. ## Running the PebbleOS Emulator Follow the build instructions in the [PebbleOS repository](https://github.com/coredevices/pebbleos) to build one of the `qemu_emery`, `qemu_flint` or `qemu_gabbro` boards, then start it: ``` $ pbl configure --board=qemu_emery$ pbl build$ pbl qemu ``` The emulator serves the Pebble Protocol on TCP port 12344 of the computer. The `pebble` tool connects to this port with `--qemu`, so apps can still be installed and their logs read from the command line while the phone is connected: ``` $ pebble install --qemu localhost:12344$ pebble logs --qemu localhost:12344 ``` > Note: Only one client can be attached to the emulator's port at a time. When the Pebble mobile app is connected, disconnect it before using `--qemu` from the `pebble` tool, or install apps from the phone instead. ## iOS Simulator The iOS version of the Pebble mobile app accepts the emulator address as a launch argument. This is only possible from `simctl` or Xcode, so it works in the iOS Simulator and not on an iPhone. Build the app from the [mobileapp repository](https://github.com/coredevices/mobileapp), install it in a booted simulator and launch it with: ``` $ xcrun simctl launch booted coredevices.coreapp -qemu 127.0.0.1:12344 ``` The simulator runs on the same computer as the emulator, so `127.0.0.1` is the emulator's address. ## Android On Android the emulator can be reached from a device or an emulator image connected with `adb`. Forward the emulator's port to the phone, then tell the app to add the emulator as a watch: ``` $ adb reverse tcp:12344 tcp:12344$ adb shell am broadcast -a coredevices.coreapp.ADD_QEMU_WATCH \ -n coredevices.coreapp/coredevices.coreapp.debug.QemuSetupReceiver \ --es host 127.0.0.1 --ei port 12344 ``` The broadcast is only accepted from `adb`; another app on the phone cannot send it. After the broadcast the emulator appears in the _Devices_ tab like a watch, and the app connects to it. ### Faking a Location Apps that use `navigator.geolocation` in PebbleKit JS need a location on the phone. On an Android device without GPS reception, or an Android emulator image, set a test location with `adb`. Repeat the last command periodically so the fix does not go stale: ``` $ adb shell appops set com.android.shell android:mock_location allow$ for p in gps fused; do adb shell cmd location providers add-test-provider $p adb shell cmd location providers set-test-provider-enabled $p true adb shell cmd location providers set-test-provider-location $p \ --location 37.4438,-122.1650 done ``` ## Sideloading Apps Opening a `.pbw` file on the phone installs it on the connected watch. On Android the Pebble mobile app registers for `.pbw`, `.pbl` and `.pbz` files, so a file received by email, downloaded in the browser or shared from another app opens in the Pebble mobile app. On iOS the app declares the same document types, so a `.pbw` can be opened from the Files app, Mail, AirDrop or a share sheet. The app is added to the locker with the version marked _(sideloaded)_, and it is installed from there like an appstore app. Sideloaded apps have no appstore listing, so `Pebble.getTimelineToken()` returns nothing, and `Pebble.getAccountToken()` and `Pebble.getWatchToken()` return different values from the ones the same app gets once it is published. Read [_Tokens_](https://developer.repebble.com/guides/communication/using-pebblekit-js.md#tokens) for details. The simplest way to get a `.pbw` onto a phone during development is still the [_Developer Connection_](https://developer.repebble.com/guides/tools-and-resources/developer-connection.md), which installs the build directly from the `pebble` tool or CloudPebble. --- # Tools and Resources Source: https://developer.repebble.com/guides/tools-and-resources/ This section of the developer guides contains information and resources surrounding management of SDK projects themselves, as well as additional information on using the Pebble emulator and the local SDK command-line utility. Developers looking to add internationalization support can also find information and tools for that purpose here. ## Contents - [**App Metadata**](https://developer.repebble.com/guides/tools-and-resources/app-metadata.md) - Details of the metadata that describes the app, such as its name, resources and capabilities. - [**Color Picker Tool**](https://developer.repebble.com/guides/tools-and-resources/color-picker.md) - Preview all the colors available on Pebble with the associated SDK contants and HTML codes. - [**Command Line Tool**](https://developer.repebble.com/guides/tools-and-resources/pebble-tool.md) - How to use the Pebble command line tool to build, debug, and emulate apps. - [**Developer Connection**](https://developer.repebble.com/guides/tools-and-resources/developer-connection.md) - How to enable and use the Pebble Developer Connection to install and debug apps directly from a computer. - [**Hardware Information**](https://developer.repebble.com/guides/tools-and-resources/hardware-information.md) - Details of the the capabilities of the various Pebble hardware platforms. - [**Internationalization**](https://developer.repebble.com/guides/tools-and-resources/internationalization.md) - How to localize an app to multiple languages. - [**Testing with the Pebble Mobile App**](https://developer.repebble.com/guides/tools-and-resources/testing-with-the-mobile-app.md) - How to connect the Pebble mobile app to the emulator to test PebbleKit JS, configuration pages and timeline pins without a watch, and how to sideload a .pbw file. --- # App Configuration (manual setup) Source: https://developer.repebble.com/guides/user-interfaces/app-configuration-static/ > This guide provides the steps to manually create an app configuration page. The preferred approach is to use [_Clay for Pebble_](https://developer.repebble.com/guides/user-interfaces/app-configuration.md) instead. Many watchfaces and apps in the Pebble appstore include the ability to customize their behavior or appearance through the use of a configuration page. This mechanism consists of an HTML form that passes the user's chosen configuration data to PebbleKit JS, which in turn relays it to the watchface or watchapp. The HTML page created needs to be hosted online, so that it is accessible to users via the Pebble application. If you do not want to host your own HTML page, you should follow the [_Clay guide_](https://developer.repebble.com/guides/user-interfaces/app-configuration.md) to create a local config page. App configuration pages are powered by PebbleKit JS. To find out more about PebbleKit JS, [_read the guide_](https://developer.repebble.com/guides/communication/using-pebblekit-js.md). ## Adding Configuration For an app to be configurable, it must marked as 'configurable' in the app's [_`package.json`_](https://developer.repebble.com/guides/tools-and-resources/app-metadata.md)`capabilities` array. The presence of this value tells the mobile app to display a gear icon next to the app, allowing users to access the configuration page. ``` "capabilities": ["configurable"] ``` ## Choosing Key Values Since the config page must transmit the user's preferred options to the watchapp, the first step is to decide upon the [`AppMessage`](https://developer.repebble.com/docs/c/Foundation/AppMessage.md "AppMessage") keys defined in `package.json` that will be used to represent the chosen value for each option on the config page: ``` "messageKeys": ["BackgroundColor","ForegroundColor","SecondTick","Animations"] ``` These keys will automatically be available both in C on the watch and in PebbleKit JS on the phone. Each of these keys will apply to the appropriate input element on the config page, with the user's chosen value transmitted to the watchapp's [`AppMessageInboxReceived`](https://developer.repebble.com/docs/c/Foundation/AppMessage.md#AppMessageInboxReceived "AppMessageInboxReceived") handler once the page is submitted. ## Showing the Config Page Once an app is marked as `configurable`, the PebbleKit JS component must implement `Pebble.openURL()` in the `showConfiguration` event handler in `index.js` to present the developer's HTML page when the user wants to configure the app: ``` Pebble.addEventListener('showConfiguration', function() {var url = 'http://example.com/config.html';Pebble.openURL(url);}); ``` ## Creating the Config Page The basic structure of an HTML config page begins with a template HTML file: > Note: This page will be plain and unstyled. CSS styling must be performed separately, and is not covered here. ``` \\\\Example Configuration\\\\This is an example HTML forms configuration page.\\\ ``` The various UI elements the user will interact with to choose their preferences must be placed within the `body` tag, and will most likely take the form of HTML `input` elements. For example, a text input field for each of the example color options will look like the following: ``` \Background Color\\Foreground Color\ ``` Other components include checkboxes, such as the two shown below for each of the example boolean options: ``` \Enable Second Ticks\\Show Animations\ ``` The final element should be the 'Save' button, used to trigger the sending of the user's preferences back to PebbleKit JS. ``` \ ``` ## Submitting Config Data Once the 'Save' button is pressed, the values of all the input elements should be encoded and included in the return URL as shown below: ``` \// Get a handle to the button's HTML elementvar submitButton = document.getElementById('submit\_button');// Add a 'click' listenersubmitButton.addEventListener('click', function() {// Get the config data from the UI elementsvar backgroundColor = document.getElementById('background\_color\_input');var foregroundColor = document.getElementById('foreground\_color\_input');var secondTickCheckbox = document.getElementById('second\_tick\_checkbox');var animationsCheckbox = document.getElementById('animations\_checkbox');// Make a data object to be sent, coercing value types to integersvar options = {'background\_color': parseInt(backgroundColor.value, 16),'foreground\_color': parseInt(foregroundColor.value, 16),'second\_ticks': secondTickCheckbox.checked == 'true' ? 1 : 0,'animations': animationsCheckbox.checked == 'true' ? 1 : 0};// Determine the correct return URL (emulator vs real watch)function getQueryParam(variable, defaultValue) {var query = location.search.substring(1);var vars = query.split('&');for (var i = 0; i \< vars.length; i++) {var pair = vars[i].split('=');if (pair[0] === variable) {return decodeURIComponent(pair[1]);}}return defaultValue || false;}var return\_to = getQueryParam('return\_to', 'pebblejs://close#');// Encode and send the data when the page closesdocument.location = return\_to + encodeURIComponent(JSON.stringify(options));});\ ``` > Note: Remember to use `encodeURIComponent()` and `decodeURIComponent()` to ensure the JSON data object is transmitted without error. ## Hosting the Config Page In order for users to access your configuration page, it needs to be hosted online somewhere. One potential free service to host your configuration page is Github Pages: [Github Pages](https://pages.github.com/) allow you to host your HTML, CSS and JavaScript files and directly access them from a special branch within your Github repo. This also has the added advantage of encouraging the use of version control. ## Relaying Data through PebbleKit JS When the user submits the HTML form, the page will close and the result is passed to the `webviewclosed` event handler in the PebbleKit JS `index.js` file: ``` Pebble.addEventListener('webviewclosed', function(e) {// Decode the user's preferencesvar configData = JSON.parse(decodeURIComponent(e.response));} ``` The data from the config page should be converted to the appropriate keys and value types expected by the watchapp, and sent via [`AppMessage`](https://developer.repebble.com/docs/c/Foundation/AppMessage.md "AppMessage"): ``` // Send to the watchapp via AppMessagevar dict = {'BackgroundColor': configData.background\_color,'ForegroundColor': configData.foreground\_color,'SecondTick': configData.second\_ticks,'Animations': configData.animations};// Send to the watchappPebble.sendAppMessage(dict, function() {console.log('Config data sent successfully!');}, function(e) {console.log('Error sending config data!');}); ``` ## Receiving Config Data Once the watchapp has called [`app_message_open()`](https://developer.repebble.com/docs/c/Foundation/AppMessage.md#app_message_open "app\_message\_open") and registered an [`AppMessageInboxReceived`](https://developer.repebble.com/docs/c/Foundation/AppMessage.md#AppMessageInboxReceived "AppMessageInboxReceived") handler, that handler will be called once the data has arrived on the watch. This occurs once the user has pressed the submit button. To obtain the example keys and values shown in this guide, simply look for and read the keys as [`Tuple`](https://developer.repebble.com/docs/c/Foundation/Dictionary.md#Tuple "Tuple") objects using the [`DictionaryIterator`](https://developer.repebble.com/docs/c/Foundation/Dictionary.md#DictionaryIterator "DictionaryIterator") provided: ``` static void inbox\_received\_handler(DictionaryIterator \*iter, void \*context) {// Read color preferencesTuple \*bg\_color\_t = dict\_find(iter, MESSAGE\_KEY\_BackgroundColor);if(bg\_color\_t) {GColor bg\_color = GColorFromHEX(bg\_color\_t-\>value-\>int32);}Tuple \*fg\_color\_t = dict\_find(iter, MESSAGE\_KEY\_ForegroundColor);if(fg\_color\_t) {GColor fg\_color = GColorFromHEX(fg\_color\_t-\>value-\>int32);}// Read boolean preferencesTuple \*second\_tick\_t = dict\_find(iter, MESSAGE\_KEY\_SecondTick);if(second\_tick\_t) {bool second\_ticks = second\_tick\_t-\>value-\>int32 == 1;}Tuple \*animations\_t = dict\_find(iter, MESSAGE\_KEY\_Animations);if(animations\_t) {bool animations = animations\_t-\>value-\>int32 == 1;}// App should now update to take the user's preferences into accountreload\_config();} ``` Read the [_Communication_](https://developer.repebble.com/guides/communication.md) guides for more information about using the [`AppMessage`](https://developer.repebble.com/docs/c/Foundation/AppMessage.md "AppMessage") API. If you're looking for a simpler option, we recommend using [_Clay for Pebble_](https://developer.repebble.com/guides/user-interfaces/app-configuration.md) instead. --- # Developer Guides Source: https://developer.repebble.com/guides/ This is the main page for the Developer Guides, containing a large number of resources for developing all kinds of Pebble watchapps. New and experienced developers can use these resources to help build their skill set when writing for Pebble. ## What's Here? To help guide towards the most relevant information, the developer guides are split up into the following sections. A complete list of every available guide is available in the [Table of Contents](https://developer.repebble.com/guides/toc.md). ### Create beautiful and engaging apps - [**App Resources**](https://developer.repebble.com/guides/app-resources.md) - Information on the many kinds of files that can be used inside Pebble apps. - [**Design and Interaction**](https://developer.repebble.com/guides/design-and-interaction.md) - Information on creating the best app user experiences and layout designs. - [**Graphics and Animations**](https://developer.repebble.com/guides/graphics-and-animations.md) - Information on using animations and drawing shapes, text, and images, as well as more advanced techniques. - [**User Interfaces**](https://developer.repebble.com/guides/user-interfaces.md) - How to build effective user interfaces. Includes information on events, persistent storage, background worker, wakeups and app configuration. ### Create apps that interact with the world - [**Alloy**](https://developer.repebble.com/guides/alloy.md) - Information on creating apps with JavaScript using the Alloy framework, based on Moddable. - [**Communication**](https://developer.repebble.com/guides/communication.md) - How to talk to the phone via PebbleKit with JavaScript and on Android or iOS. - [**Events and Services**](https://developer.repebble.com/guides/events-and-services.md) - How to get data from the onboard sensors and services including the accelerometer, compass, and microphone. - [**Pebble Timeline**](https://developer.repebble.com/guides/pebble-timeline.md) - How to use Pebble timeline to bring timely information to app users outside the app itself. - [**Plugins (Preview)**](https://developer.repebble.com/guides/plugins.md) - How a .pbw's JavaScript can provide data sources and actions to other apps through the Pebble mobile app, and how a watchapp reads them from PebbleKit JS. ### Build, refine, and debug apps - [**Building with AI Agents**](https://developer.repebble.com/guides/ai-agents.md) - How to give an AI coding agent the documentation, project instructions and emulator commands it needs to build a Pebble app. - [**Appstore Publishing**](https://developer.repebble.com/guides/appstore-publishing.md) - How to publish apps in the Pebble appstore from the Developer Dashboard, the pebble tool or CloudPebble. - [**Best Practices**](https://developer.repebble.com/guides/best-practices.md) - Information to help optimize apps and ensure a good user experience. - [**Debugging**](https://developer.repebble.com/guides/debugging.md) - How to find and fix common compilation and runtime problems in apps. - [**Pebble Packages**](https://developer.repebble.com/guides/pebble-packages.md) - How to create and use Pebble Packages - [**Tools and Resources**](https://developer.repebble.com/guides/tools-and-resources.md) - Information on all the software tools available when writing Pebble apps, as well as other resources. --- # Guides Table of Contents Source: https://developer.repebble.com/guides/toc/ ## [1. Alloy](https://developer.repebble.com/guides/alloy.md) Information on creating apps with JavaScript using the Alloy framework, based on Moddable. #### [Getting Started with Alloy](https://developer.repebble.com/guides/alloy/getting-started.md) Learn how to create your first Alloy app for Pebble. #### [Piu UI Framework](https://developer.repebble.com/guides/alloy/piu-guide.md) Build declarative user interfaces with the Piu framework. #### [Poco Graphics](https://developer.repebble.com/guides/alloy/poco-guide.md) Low-level graphics rendering with the Poco framework. #### [Sensors and Input](https://developer.repebble.com/guides/alloy/sensors-and-input.md) Access Pebble sensors and handle button input in Alloy apps. #### [Storage](https://developer.repebble.com/guides/alloy/storage.md) Persist data between app launches using localStorage, key-value storage, or files. #### [Networking](https://developer.repebble.com/guides/alloy/networking.md) Make HTTP requests and use WebSockets via the phone proxy. #### [App Messages](https://developer.repebble.com/guides/alloy/app-messages.md) Send and receive messages between the watch and the phone. #### [Watchfaces](https://developer.repebble.com/guides/alloy/watchfaces.md) Build custom watchfaces with Alloy. #### [Animations](https://developer.repebble.com/guides/alloy/animations.md) Create smooth animations using Timeline and easing functions. #### [Port (Custom Drawing)](https://developer.repebble.com/guides/alloy/port-drawing.md) Combine Piu's declarative UI with custom drawing using Port. #### [Advanced Networking](https://developer.repebble.com/guides/alloy/advanced-networking.md) Low-level HTTP and WebSocket clients for advanced networking needs. #### [Native Functions (FFI)](https://developer.repebble.com/guides/alloy/ffi.md) Call native C functions directly from JavaScript in your Alloy app using the Foreign Function Interface. #### [Dictation](https://developer.repebble.com/guides/alloy/dictation.md) Capture speech as text in Alloy apps using the dictation API. #### [Wakeups](https://developer.repebble.com/guides/alloy/wakeups.md) Schedule your Alloy app to launch at a future time using wakeups. #### [Vibration](https://developer.repebble.com/guides/alloy/vibration.md) Provide haptic feedback in Alloy apps using the vibration motor. #### [Device Info and App Events](https://developer.repebble.com/guides/alloy/device-info.md) Query watch and screen properties, respond to app lifecycle events, and control the backlight in Alloy apps. #### [Health](https://developer.repebble.com/guides/alloy/health.md) Read steps, sleep, activities and heart rate data from Pebble Health in Alloy apps. #### [Alloy Reference](https://developer.repebble.com/guides/alloy/reference.md) Where the Alloy APIs are documented, and the details that are not obvious from the other guides: font names, resource IDs, modules, memory and TypeScript. ## [2. Building with AI Agents](https://developer.repebble.com/guides/ai-agents.md) How to give an AI coding agent the documentation, project instructions and emulator commands it needs to build a Pebble app. ## [3. App Resources](https://developer.repebble.com/guides/app-resources.md) Information on the many kinds of files that can be used inside Pebble apps. #### [Animated Images](https://developer.repebble.com/guides/app-resources/animated-images.md) How to add animated image resources to a project in the APNG format, and display them in your app. #### [App Assets](https://developer.repebble.com/guides/app-resources/app-assets.md) A collection of assets for use as resources in Pebble apps. #### [Converting SVG to PDC](https://developer.repebble.com/guides/app-resources/converting-svg-to-pdc.md) How to create compatible SVG files using Inkscape and Illustrator. #### [Fonts](https://developer.repebble.com/guides/app-resources/fonts.md) How to use built-in system fonts, or add your own font resources to a project. #### [Images](https://developer.repebble.com/guides/app-resources/images.md) How to add image resources to a project and display them in your app. #### [Pebble Draw Command File Format](https://developer.repebble.com/guides/app-resources/pdc-format.md) The binary file format description for Pebble Draw Command Frames, Images and Sequences. #### [Platform-specific Resources](https://developer.repebble.com/guides/app-resources/platform-specific.md) How to include different resources for different platforms, as well as how to include a resource only on a particular platform. #### [Raw Data Files](https://developer.repebble.com/guides/app-resources/raw-data-files.md) How to add raw data resources to a project and read them in your app. #### [System Fonts](https://developer.repebble.com/guides/app-resources/system-fonts.md) A complete list of all the system fonts available for use in Pebble projects. ## [4. Appstore Publishing](https://developer.repebble.com/guides/appstore-publishing.md) How to publish apps in the Pebble appstore from the Developer Dashboard, the pebble tool or CloudPebble. #### [Preparing a Submission](https://developer.repebble.com/guides/appstore-publishing/preparing-a-submission.md) What a Pebble appstore listing is made of, and the sizes and formats required for screenshots, icons and banners. #### [Publishing an App](https://developer.repebble.com/guides/appstore-publishing/publishing-an-app.md) How to submit an app and later releases from the Developer Dashboard, the `pebble` tool or CloudPebble. #### [Importing Apps from Rebble](https://developer.repebble.com/guides/appstore-publishing/importing-from-rebble.md) How to bring apps published in the Rebble appstore into the Developer Dashboard and prove ownership of them. ## [5. Best Practices](https://developer.repebble.com/guides/best-practices.md) Information to help optimize apps and ensure a good user experience. #### [Building for Every Pebble](https://developer.repebble.com/guides/best-practices/building-for-every-pebble.md) How to write one app compatible with all Pebble smartwatches. #### [Conserving Battery Life](https://developer.repebble.com/guides/best-practices/conserving-battery-life.md) How to write an app to consume power as efficiently as possible. #### [Modular App Architecture](https://developer.repebble.com/guides/best-practices/modular-app-architecture.md) How to break up a complex app into smaller pieces for managablilty, modularity and reusability. ## [6. Communication](https://developer.repebble.com/guides/communication.md) How to talk to the phone via PebbleKit with JavaScript and on Android or iOS. #### [Advanced Communication](https://developer.repebble.com/guides/communication/advanced-communication.md) Details of communication tips and best practices for more advanced scenarios. #### [Datalogging](https://developer.repebble.com/guides/communication/datalogging.md) Information on how to collect data batches using the Datalogging API. #### [PebbleKit Android](https://developer.repebble.com/guides/communication/using-pebblekit-android.md) How to use PebbleKit to communicate with a watchapp on Android. #### [PebbleKit JS](https://developer.repebble.com/guides/communication/using-pebblekit-js.md) How to use PebbleKit JS to communicate with the connected phone's JS environment, and how the Pebble mobile app runs it on Android and iOS. #### [PebbleKit iOS](https://developer.repebble.com/guides/communication/using-pebblekit-ios.md) How to use PebbleKit to communicate with a watchapp on iOS. #### [Sending and Receiving Data](https://developer.repebble.com/guides/communication/sending-and-receiving-data.md) How to send and receive data between your watchapp and phone. #### [Sports API](https://developer.repebble.com/guides/communication/using-the-sports-api.md) How to use the PebbleKit Sports API to integrate your mobile sports app with Pebble. ## [7. Debugging](https://developer.repebble.com/guides/debugging.md) How to find and fix common compilation and runtime problems in apps. #### [Common Runtime Errors](https://developer.repebble.com/guides/debugging/common-runtime-errors.md) Examples of commonly encountered runtime problems that cannot be detected at compile time and can usually be fixed by logical thought and experimentation. #### [Common Syntax Errors](https://developer.repebble.com/guides/debugging/common-syntax-errors.md) Details of common problems encountered when writing C apps for Pebble, and how to resolve them. #### [Debugging Alloy Apps with xsbug](https://developer.repebble.com/guides/debugging/debugging-alloy-with-xsbug.md) Use the xsbug JavaScript debugger to set breakpoints and inspect your Alloy app while it runs on the watch. #### [Debugging with App Logs](https://developer.repebble.com/guides/debugging/debugging-with-app-logs.md) How to use the app logs to debug problems with an app, as well as tips on interpreting common run time errors. #### [Debugging with GDB](https://developer.repebble.com/guides/debugging/debugging-with-gdb.md) How to use GDB to debug a Pebble app in the emulator. ## [8. Design and Interaction](https://developer.repebble.com/guides/design-and-interaction.md) Information on creating the best app user experiences and layout designs. #### [Benefits of Design Guidelines](https://developer.repebble.com/guides/design-and-interaction/benefits.md) Learn the main concepts of design guidelines, why they are needed, and how they can help developers. #### [Core Experience Design](https://developer.repebble.com/guides/design-and-interaction/core-experience.md) How design guidelines shape the core Pebble app experience. #### [One Click Actions](https://developer.repebble.com/guides/design-and-interaction/one-click-actions.md) Details about how to create One Click Action watchapps #### [Recommended Guidelines and Patterns](https://developer.repebble.com/guides/design-and-interaction/recommended.md) Pebble's recommended guidelines for creating awesome app experiences. #### [Round App Design](https://developer.repebble.com/guides/design-and-interaction/in-the-round.md) Tips and advice for designing apps that take advantage of the Pebble Time Round display #### [Example Implementations](https://developer.repebble.com/guides/design-and-interaction/implementation.md) Resources and code samples to implement common design and UI patterns. ## [9. Events and Services](https://developer.repebble.com/guides/events-and-services.md) How to get data from the onboard sensors and services including the accelerometer, compass, and microphone. #### [Accelerometer](https://developer.repebble.com/guides/events-and-services/accelerometer.md) How to use data and simple tap gestures from the onboard accelerometer. #### [Background Worker](https://developer.repebble.com/guides/events-and-services/background-worker.md) Using the Background Worker to do work in the background, such as activity tracking. #### [Backlight](https://developer.repebble.com/guides/events-and-services/light.md) How to trigger the backlight, force it on, and - on hardware with an RGB backlight - tint it. #### [Buttons](https://developer.repebble.com/guides/events-and-services/buttons.md) How to react to button presses in your app. #### [Compass](https://developer.repebble.com/guides/events-and-services/compass.md) How to use data from the Compass API to determine direction. #### [Dictation](https://developer.repebble.com/guides/events-and-services/dictation.md) How to use the Dictation API to get voice-to-text input in watchapps. #### [Event Services](https://developer.repebble.com/guides/events-and-services/events.md) How to use the various asynchronous event services to power app features. #### [Heart Rate Monitor](https://developer.repebble.com/guides/events-and-services/hrm.md) Information on using the HealthService API to obtain information from the Heart Rate Monitor. #### [Pebble Health](https://developer.repebble.com/guides/events-and-services/health.md) Information on using the HealthService API to incorporate multiple types of health data into your apps. #### [Persistent Storage](https://developer.repebble.com/guides/events-and-services/persistent-storage.md) Using persistent storage to improve your app's UX. #### [Speaker](https://developer.repebble.com/guides/events-and-services/speaker.md) How to play tones, melodies, polyphonic tracks, and PCM streams through the speaker. #### [Touch](https://developer.repebble.com/guides/events-and-services/touch.md) How to subscribe to touch events on platforms with a touchscreen. #### [Wakeups](https://developer.repebble.com/guides/events-and-services/wakeups.md) Using the Wakeup API to launch an app at some future time. ## [10. Graphics and Animations](https://developer.repebble.com/guides/graphics-and-animations.md) Information on using animations and drawing shapes, text, and images, as well as more advanced techniques. #### [Animations](https://developer.repebble.com/guides/graphics-and-animations/animations.md) How to use Animations and Timers to add life to your app. #### [Drawing Primitives, Images and Text](https://developer.repebble.com/guides/graphics-and-animations/drawing-primitives-images-and-text.md) How to draw primitive shapes, image, and text onto the Graphics Context. #### [Framebuffer Graphics](https://developer.repebble.com/guides/graphics-and-animations/framebuffer-graphics.md) How to perform advanced drawing using direct framebuffer access. #### [Vector Graphics](https://developer.repebble.com/guides/graphics-and-animations/vector-graphics.md) How to draw simple images using vector images, instead of bitmaps. ## [11. Pebble Packages](https://developer.repebble.com/guides/pebble-packages.md) How to create and use Pebble Packages #### [Creating Pebble Packages](https://developer.repebble.com/guides/pebble-packages/creating-packages.md) How to create Pebble Packages #### [Using Pebble Packages](https://developer.repebble.com/guides/pebble-packages/using-packages.md) How to use Pebble Packages ## [12. Pebble Timeline](https://developer.repebble.com/guides/pebble-timeline.md) How to use Pebble timeline to bring timely information to app users outside the app itself. #### [Creating Pins](https://developer.repebble.com/guides/pebble-timeline/pin-structure.md) How to create timeline pins with reminders, actions, and layouts. #### [Local Pins](https://developer.repebble.com/guides/pebble-timeline/timeline-local-pins.md) How to insert pins into the timeline directly from PebbleKit JS, without a web server or timeline API keys. #### [Subscriptions and Tokens](https://developer.repebble.com/guides/pebble-timeline/timeline-js.md) The PebbleKit JS timeline subscription and token APIs, and what to use instead. ## [13. Plugins (Preview)](https://developer.repebble.com/guides/plugins.md) How a .pbw's JavaScript can provide data sources and actions to other apps through the Pebble mobile app, and how a watchapp reads them from PebbleKit JS. #### [Pebble Plugins Preview](https://developer.repebble.com/guides/plugins/preview.md) What plugins are, how to try the preview in the Pebble mobile app, how a watchapp reads plugin data from PebbleKit JS, and how to write a plugin. ## [14. Tools and Resources](https://developer.repebble.com/guides/tools-and-resources.md) Information on all the software tools available when writing Pebble apps, as well as other resources. #### [App Metadata](https://developer.repebble.com/guides/tools-and-resources/app-metadata.md) Details of the metadata that describes the app, such as its name, resources and capabilities. #### [Color Picker Tool](https://developer.repebble.com/guides/tools-and-resources/color-picker.md) Preview all the colors available on Pebble with the associated SDK contants and HTML codes. #### [Command Line Tool](https://developer.repebble.com/guides/tools-and-resources/pebble-tool.md) How to use the Pebble command line tool to build, debug, and emulate apps. #### [Developer Connection](https://developer.repebble.com/guides/tools-and-resources/developer-connection.md) How to enable and use the Pebble Developer Connection to install and debug apps directly from a computer. #### [Hardware Information](https://developer.repebble.com/guides/tools-and-resources/hardware-information.md) Details of the the capabilities of the various Pebble hardware platforms. #### [Internationalization](https://developer.repebble.com/guides/tools-and-resources/internationalization.md) How to localize an app to multiple languages. #### [Testing with the Pebble Mobile App](https://developer.repebble.com/guides/tools-and-resources/testing-with-the-mobile-app.md) How to connect the Pebble mobile app to the emulator to test PebbleKit JS, configuration pages and timeline pins without a watch, and how to sideload a .pbw file. ## [15. User Interfaces](https://developer.repebble.com/guides/user-interfaces.md) How to build effective user interfaces. Includes information on events, persistent storage, background worker, wakeups and app configuration. #### [App Configuration](https://developer.repebble.com/guides/user-interfaces/app-configuration.md) How to allow users to customize an app with a configuration page. #### [App Exit Reason](https://developer.repebble.com/guides/user-interfaces/app-exit-reason.md) Details on how to use the AppExitReason API #### [AppGlance C API](https://developer.repebble.com/guides/user-interfaces/appglance-c.md) How to programatically update an app's app glance. #### [AppGlance in PebbleKit JS](https://developer.repebble.com/guides/user-interfaces/appglance-pebblekit-js.md) How to update an app's glance using PebbleKit JS. #### [Content Size](https://developer.repebble.com/guides/user-interfaces/content-size.md) Details on how to use the ContentSize API to adapt your watchface layout based on user text size preferences. #### [Layers](https://developer.repebble.com/guides/user-interfaces/layers.md) How to use standard Layer components to build an app's UI. #### [Round App UI](https://developer.repebble.com/guides/user-interfaces/round-app-ui.md) Details on how to use the Pebble SDK to create layouts specifically for round displays. #### [Unobstructed Area](https://developer.repebble.com/guides/user-interfaces/unobstructed-area.md) Details on how to use the UnobstructedArea API to adapt your watchface layout when the screen is partially obstructed by a system overlay. --- # Smartstraps Source: https://developer.repebble.com/guides/smartstraps/ > Note: Smartstraps are not supported on Pebble 2 Duo, Pebble Time 2, Pebble Round 2 or newer watches, and support will not be added. Smartstraps were accessories that connected to the smart accessory port on the back of Pebble Time, Pebble Time Steel, Pebble Time Round and Pebble 2. The port carried power and a one-wire serial bus, and the `Smartstrap` C API let an app exchange data with the strap. Pebble 2 Duo, Pebble Time 2, Pebble Round 2 and newer watches do not have the accessory port, and the `Smartstrap` API is not part of the current SDK reference. This page is kept as a historical record. The original guides on smartstrap hardware, the serial protocol and the C API remain in the [repository history](https://github.com/coredevices/sdk-docs/tree/c4ef713%5E/source/_guides/smartstraps).