mirror of
https://github.com/google/pebble.git
synced 2025-06-03 16:53:11 +00:00
Import the pebble dev site into devsite/
This commit is contained in:
parent
3b92768480
commit
527858cf4c
1359 changed files with 265431 additions and 0 deletions
154
devsite/source/_guides/events-and-services/accelerometer.md
Normal file
154
devsite/source/_guides/events-and-services/accelerometer.md
Normal file
|
@ -0,0 +1,154 @@
|
|||
---
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
title: Accelerometer
|
||||
description: |
|
||||
How to use data and simple tap gestures from the onboard accelerometer.
|
||||
guide_group: events-and-services
|
||||
order: 0
|
||||
related_docs:
|
||||
- AccelerometerService
|
||||
related_examples:
|
||||
- title: Feature Accel Discs
|
||||
url: https://github.com/pebble-examples/feature-accel-discs
|
||||
---
|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
In the API, each axis value contained in an ``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`` 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`` function, such as the one below:
|
||||
|
||||
```c
|
||||
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:
|
||||
|
||||
```c
|
||||
// Subscribe to tap events
|
||||
accel_tap_service_subscribe(accel_tap_handler);
|
||||
```
|
||||
|
||||
```c
|
||||
// Unsubscribe from tap events
|
||||
accel_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:
|
||||
|
||||
```c
|
||||
uint32_t num_samples = 3; // Number of samples per batch/callback
|
||||
|
||||
// Subscribe to batched data events
|
||||
accel_data_service_subscribe(num_samples, accel_data_handler);
|
||||
```
|
||||
|
||||
The ``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`` `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`` 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``
|
||||
pointer provided in the callback. An example reading the first set of values is
|
||||
shown below:
|
||||
|
||||
```c
|
||||
static void accel_data_handler(AccelData *data, uint32_t num_samples) {
|
||||
// Read sample 0's x, y, and z values
|
||||
int16_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 occured
|
||||
bool did_vibrate = data[0].did_vibrate;
|
||||
uint64_t timestamp = data[0].timestamp;
|
||||
|
||||
if(!did_vibrate) {
|
||||
// Print it out
|
||||
APP_LOG(APP_LOG_LEVEL_INFO, "t: %llu, x: %d, y: %d, z: %d",
|
||||
timestamp, x, y, z);
|
||||
} else {
|
||||
// Discard with a warning
|
||||
APP_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:
|
||||
|
||||
```nc|text
|
||||
[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
|
||||
```
|
248
devsite/source/_guides/events-and-services/background-worker.md
Normal file
248
devsite/source/_guides/events-and-services/background-worker.md
Normal file
|
@ -0,0 +1,248 @@
|
|||
---
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
title: Background Worker
|
||||
description: |
|
||||
Using the Background Worker to do work in the background, such as activity
|
||||
tracking.
|
||||
guide_group: events-and-services
|
||||
order: 1
|
||||
related_docs:
|
||||
- Worker
|
||||
related_examples:
|
||||
- title: Background Counter
|
||||
url: https://github.com/pebble-examples/feature-background-counter
|
||||
- title: Background Worker Communication
|
||||
url: https://github.com/pebble-examples/feature-worker-message
|
||||
platform_choice: true
|
||||
---
|
||||
|
||||
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()``. 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`` API instead.
|
||||
|
||||
|
||||
## Adding a Worker
|
||||
|
||||
^CP^ The background worker's behavior is determined by code written in a
|
||||
separate C file to the foreground app. Add a new source file and set the
|
||||
'Target' field to 'Background Worker'.
|
||||
|
||||
^LC^ 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.
|
||||
|
||||
<div class="platform-specific" data-sdk-platform="local">
|
||||
{% markdown %}
|
||||
This project structure can also be generated using the
|
||||
[`pebble` tool](/guides/tools-and-resources/pebble-tool/) with the `--worker`
|
||||
flag as shown below:
|
||||
|
||||
```bash
|
||||
$ pebble new-project --worker project_name
|
||||
```
|
||||
{% endmarkdown %}
|
||||
</div>
|
||||
|
||||
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:
|
||||
|
||||
```c
|
||||
#include <pebble_worker.h>
|
||||
|
||||
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()``:
|
||||
|
||||
```c
|
||||
// Launch the background worker
|
||||
AppWorkerResult result = app_worker_launch();
|
||||
```
|
||||
|
||||
The ``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`` | `0` | The worker launch was successful, but may not start running immediately. Use ``app_worker_is_running()`` to determine when the worker has started running. |
|
||||
| ``APP_WORKER_RESULT_NO_WORKER`` | `1` | No worker found for the current app. |
|
||||
| ``APP_WORKER_RESULT_ALREADY_RUNNING`` | `4` | The worker is already running. |
|
||||
| ``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()`` 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`` API, then read it in the other task.
|
||||
|
||||
* Send the data to a companion phone app using the ``DataLogging`` API. Details
|
||||
on how to do this are available in {% guide_link communication/datalogging %}.
|
||||
|
||||
* Pass the data directly while the other task is running, using an
|
||||
``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.
|
||||
|
||||
```c
|
||||
// Used to identify the source of a message
|
||||
#define SOURCE_FOREGROUND 0
|
||||
#define SOURCE_BACKGROUND 1
|
||||
```
|
||||
|
||||
**Foreground App**
|
||||
|
||||
```c
|
||||
static int s_some_value = 1;
|
||||
static int s_another_value = 2;
|
||||
|
||||
static void worker_message_handler(uint16_t type,
|
||||
AppWorkerMessage *message) {
|
||||
if(type == SOURCE_BACKGROUND) {
|
||||
// Get the data, only if it was sent from the background
|
||||
s_some_value = message->data0;
|
||||
s_another_value = message->data1;
|
||||
}
|
||||
}
|
||||
|
||||
// Subscribe to get AppWorkerMessages
|
||||
app_worker_message_subscribe(worker_message_handler);
|
||||
|
||||
|
||||
// Construct a message to send
|
||||
AppWorkerMessage message = {
|
||||
.data0 = s_some_value,
|
||||
.data1 = s_another_value
|
||||
};
|
||||
|
||||
// Send the data to the background app
|
||||
app_worker_send_message(SOURCE_FOREGROUND, &message);
|
||||
|
||||
```
|
||||
|
||||
**Worker**
|
||||
|
||||
```c
|
||||
static int s_some_value = 3;
|
||||
static int s_another_value = 4;
|
||||
|
||||
// Construct a message to send
|
||||
AppWorkerMessage message = {
|
||||
.data0 = s_some_value,
|
||||
.data1 = s_another_value
|
||||
};
|
||||
|
||||
static void worker_message_handler(uint16_t type,
|
||||
AppWorkerMessage *message) {
|
||||
if(type == SOURCE_FOREGROUND) {
|
||||
// Get the data, if it was sent from the foreground
|
||||
s_some_value = message->data0;
|
||||
s_another_value = message->data1;
|
||||
}
|
||||
}
|
||||
|
||||
// Subscribe to get AppWorkerMessages
|
||||
app_worker_message_subscribe(worker_message_handler);
|
||||
|
||||
// Send the data to the foreground app
|
||||
app_worker_send_message(SOURCE_BACKGROUND, &message);
|
||||
```
|
||||
|
||||
|
||||
## Managing the Worker
|
||||
|
||||
The current running state of the background worker can be determined using the
|
||||
``app_worker_is_running()`` function:
|
||||
|
||||
```c
|
||||
// Check to see if the worker is currently active
|
||||
bool 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()``:
|
||||
|
||||
```c
|
||||
// Stop the background worker
|
||||
AppWorkerResult result = app_worker_kill();
|
||||
```
|
||||
|
||||
Possible `result` values when attempting to kill the worker are as follows:
|
||||
|
||||
| Result | Value | Description |
|
||||
|--------|-------|:------------|
|
||||
| ``APP_WORKER_RESULT_SUCCESS`` | `0` | The worker launch was killed successfully. |
|
||||
| ``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`` | `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`` API or load resources. Most other APIs are available including
|
||||
(but not limited to) ``AccelerometerService``, ``CompassService``,
|
||||
``DataLogging``, ``HealthService``, ``ConnectionService``,
|
||||
``BatteryStateService``, ``TickTimerService`` and ``Storage``.
|
||||
|
||||
^LC^ 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.
|
||||
|
||||
^CP^ CloudPebble users will be notified by the editor and compiler if they
|
||||
attempt to use an unavailable API.
|
214
devsite/source/_guides/events-and-services/buttons.md
Normal file
214
devsite/source/_guides/events-and-services/buttons.md
Normal file
|
@ -0,0 +1,214 @@
|
|||
---
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
title: Buttons
|
||||
description: |
|
||||
How to react to button presses in your app.
|
||||
guide_group: events-and-services
|
||||
order: 2
|
||||
related_docs:
|
||||
- Clicks
|
||||
- ClickHandler
|
||||
related_examples:
|
||||
- title: App Font Browser
|
||||
url: https://github.com/pebble-examples/app-font-browser/blob/master/src/app_font_browser.c#L168
|
||||
---
|
||||
|
||||
Button [`Clicks`](``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:
|
||||
|
||||

|
||||
|
||||
These buttons are used in a logical fashion throughout the system:
|
||||
|
||||
* Back - Navigates back one ``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``.
|
||||
|
||||
* 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`` or ``ActionMenu`` to give hints about
|
||||
what each button will do.
|
||||
|
||||
|
||||
## Listening for Button Clicks
|
||||
|
||||
Button clicks are received via a subscription to one of the types of button
|
||||
click events listed below. Each ``Window`` that wishes to receive button click
|
||||
events must provide a ``ClickConfigProvider`` that performs these subscriptions.
|
||||
|
||||
The first step is to create the ``ClickConfigProvider`` function:
|
||||
|
||||
```c
|
||||
static void click_config_provider(void *context) {
|
||||
// Subcribe to button click events here
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
The second step is to register the ``ClickConfigProvider`` with the current
|
||||
``Window``, typically after ``window_create()``:
|
||||
|
||||
```c
|
||||
// Use this provider to add button click subscriptions
|
||||
window_set_click_config_provider(window, click_config_provider);
|
||||
```
|
||||
|
||||
The final step is to write a ``ClickHandler`` function for each different type
|
||||
of event subscription required by the watchapp. An example for a single click
|
||||
event is shown below:
|
||||
|
||||
```c
|
||||
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()`` from a ``ClickConfigProvider`` function,
|
||||
supplying the ``ButtonId`` value for the chosen button and the name of the
|
||||
``ClickHandler`` that will receive the events:
|
||||
|
||||
```c
|
||||
static void click_config_provider(void *context) {
|
||||
ButtonId id = BUTTON_ID_SELECT; // The Select button
|
||||
|
||||
window_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.
|
||||
|
||||
```c
|
||||
static void click_config_provider(void *context) {
|
||||
ButtonId id = BUTTON_ID_DOWN; // The Down button
|
||||
uint16_t repeat_interval_ms = 200; // Fire every 200 ms while held down
|
||||
|
||||
window_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()``, as well as get the number
|
||||
of callbacks counted so far with ``click_number_of_clicks_counted()``:
|
||||
|
||||
```c
|
||||
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`` after a specified number of
|
||||
single clicks has been recorded. A good example of usage is to detect a double
|
||||
or triple click gesture:
|
||||
|
||||
```c
|
||||
static void click_config_provider(void *context) {
|
||||
ButtonId id = BUTTON_ID_SELECT; // The Select button
|
||||
uint8_t min_clicks = 2; // Fire after at least two clicks
|
||||
uint8_t max_clicks = 3; // Don't fire after three clicks
|
||||
uint16_t timeout = 300; // Wait 300ms before firing
|
||||
bool last_click_only = true; // Fire only after the last click
|
||||
|
||||
window_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`` can be
|
||||
used to determine how many clicks triggered this multi click event using
|
||||
``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``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.
|
||||
|
||||
```c
|
||||
static void click_config_provider(void *context) {
|
||||
ButtonId id = BUTTON_ID_SELECT; // The select button
|
||||
uint16_t delay_ms = 500; // Minimum time pressed to fire
|
||||
|
||||
window_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``s may be supplied to
|
||||
receive each of the pressed and depressed events.
|
||||
|
||||
```c
|
||||
static void click_config_provider(void *context) {
|
||||
ButtonId id = BUTTON_ID_SELECT; // The select button
|
||||
|
||||
window_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.
|
195
devsite/source/_guides/events-and-services/compass.md
Normal file
195
devsite/source/_guides/events-and-services/compass.md
Normal file
|
@ -0,0 +1,195 @@
|
|||
---
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
title: Compass
|
||||
description: |
|
||||
How to use data from the Compass API to determine direction.
|
||||
guide_group: events-and-services
|
||||
order: 3
|
||||
related_docs:
|
||||
- CompassService
|
||||
related_examples:
|
||||
- title: Feature Compass
|
||||
url: https://github.com/pebble-examples/feature-compass
|
||||
- title: Official Compass Watchapp
|
||||
url: https://github.com/pebble-hacks/pebble-compass
|
||||
---
|
||||
|
||||
The ``CompassService`` combines data from Pebble's accelerometer and
|
||||
magnetometer to automatically calibrate the compass and produce a
|
||||
``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`` 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`](``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]({{site.links.examples_org}}/feature-compass) for
|
||||
an example of how to implement this screen.
|
||||
|
||||
|
||||
## 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`` 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.
|
||||
|
||||

|
||||
|
||||
> 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()`` if the app only needs to update its UI when
|
||||
new compass data is available, or else use ``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.
|
||||
|
||||

|
||||
|
||||
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:
|
||||
|
||||
```c
|
||||
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()`:
|
||||
|
||||
```c
|
||||
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``:
|
||||
|
||||
```c
|
||||
// Subscribe to compass heading updates
|
||||
compass_service_subscribe(compass_heading_handler);
|
||||
```
|
||||
|
||||
The provided ``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`` to preserve precision, and so it can be converted
|
||||
using the ``TRIGANGLE_TO_DEG()`` macro:
|
||||
|
||||
```c
|
||||
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:
|
||||
|
||||
```c
|
||||
// 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:
|
||||
|
||||
```c
|
||||
compass_service_unsubscribe();
|
||||
```
|
||||
|
||||
|
||||
## Peeking at Compass Data
|
||||
|
||||
To fetch a compass heading without subscribing, simply peek to get a single
|
||||
sample:
|
||||
|
||||
```c
|
||||
// Peek to get data
|
||||
CompassHeadingData data;
|
||||
compass_service_peek(&data);
|
||||
```
|
||||
|
||||
> Similar to the subscription-provided data, the app should examine the peeked
|
||||
> `CompassHeadingData` to determine if it is valid (i.e. the compass is
|
||||
> calibrated).
|
216
devsite/source/_guides/events-and-services/dictation.md
Normal file
216
devsite/source/_guides/events-and-services/dictation.md
Normal file
|
@ -0,0 +1,216 @@
|
|||
---
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
title: Dictation
|
||||
description: |
|
||||
How to use the Dictation API to get voice-to-text input in watchapps.
|
||||
guide_group: events-and-services
|
||||
order: 4
|
||||
platforms:
|
||||
- basalt
|
||||
- chalk
|
||||
- diorite
|
||||
- emery
|
||||
related_docs:
|
||||
- Dictation
|
||||
related_examples:
|
||||
- title: Simple Voice Demo
|
||||
url: https://github.com/pebble-examples/simple-voice-demo
|
||||
- title: Voice Quiz
|
||||
url: https://github.com/pebble-examples/voice-quiz
|
||||
---
|
||||
|
||||
On hardware [platforms](/faqs/#pebble-sdk) supporting a microphone, the
|
||||
``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`` 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``
|
||||
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).
|
||||
|
||||
```c
|
||||
static DictationSession *s_dictation_session;
|
||||
```
|
||||
|
||||
A callback of type ``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.
|
||||
|
||||
```c
|
||||
static void dictation_session_callback(DictationSession *session, DictationSessionStatus status,
|
||||
char *transcription, void *context) {
|
||||
// Print the results of a transcription attempt
|
||||
APP_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.
|
||||
|
||||
```c
|
||||
// Declare a buffer for the DictationSession
|
||||
static char s_last_text[512];
|
||||
```
|
||||
|
||||
Finally, create the ``DictationSession`` and supply the size of the buffer and
|
||||
the ``DictationSessionStatusCallback``. This session may be used as many times
|
||||
as requires for multiple transcriptions. A context pointer may also optionally
|
||||
be provided.
|
||||
|
||||
```c
|
||||
// Create new dictation session
|
||||
s_dictation_session = dictation_session_create(sizeof(s_last_text),
|
||||
dictation_session_callback, NULL);
|
||||
```
|
||||
|
||||
|
||||
## Obtaining Dictated Text
|
||||
|
||||
After creating a ``DictationSession``, the developer can begin a dictation
|
||||
attempt at any time, providing that one is not already in progress.
|
||||
|
||||
```c
|
||||
// Start dictation UI
|
||||
dictation_session_start(s_dictation_session);
|
||||
```
|
||||
|
||||
The dictation UI will be displayed and the user will speak their desired input.
|
||||
|
||||

|
||||
|
||||
It is recommended to provide visual guidance on the format of the expected input
|
||||
before the ``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`` will be called. The `status` parameter
|
||||
provided will inform the developer as to whether or not the transcription was
|
||||
successful using a ``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``, 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`` in the app's UI with variable name `s_output_layer`
|
||||
may be used to show the status of an attempted transcription:
|
||||
|
||||
```c
|
||||
if(status == DictationSessionStatusSuccess) {
|
||||
// Display the dictated text
|
||||
snprintf(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 error
|
||||
static 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.
|
||||
|
||||
```c
|
||||
// Disable the confirmation screen
|
||||
dictation_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 dialogs
|
||||
dictation_session_enable_error_dialogs(s_dictation_session, false);
|
||||
```
|
||||
|
||||
|
||||
### DictationSessionStatus Values
|
||||
|
||||
These are the possible values provided by a ``DictationSessionStatusCallback``,
|
||||
and should be used to handle transcription success or failure for any of the
|
||||
following reasons.
|
||||
|
||||
| Status | Value | Description |
|
||||
|--------|-------|-------------|
|
||||
| ``DictationSessionStatusSuccess`` | `0` | Transcription successful, with a valid result. |
|
||||
| ``DictationSessionStatusFailureTranscriptionRejected`` | `1` | User rejected transcription and dismissed the dictation UI. |
|
||||
| ``DictationSessionStatusFailureTranscriptionRejectedWithError`` | `2` | User exited the dictation UI after a transcription error. |
|
||||
| ``DictationSessionStatusFailureSystemAborted`` | `3` | Too many errors occurred during transcription and the dictation UI exited. |
|
||||
| ``DictationSessionStatusFailureNoSpeechDetected`` | `4` | No speech was detected and the dictation UI exited. |
|
||||
| ``DictationSessionStatusFailureConnectivityError`` | `5` | No Bluetooth or Internet connection available. |
|
||||
| ``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`` | `7` | Voice transcription failed due to an internal error. |
|
||||
| ``DictationSessionStatusFailureRecognizerError`` | `8` | Cloud recognizer failed to transcribe speech (only possible if error dialogs are disabled). |
|
332
devsite/source/_guides/events-and-services/events.md
Normal file
332
devsite/source/_guides/events-and-services/events.md
Normal file
|
@ -0,0 +1,332 @@
|
|||
---
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
title: Event Services
|
||||
description: |
|
||||
How to use the various asynchronous event services to power app features.
|
||||
guide_group: events-and-services
|
||||
order: 5
|
||||
related_docs:
|
||||
- TickTimerService
|
||||
- ConnectionService
|
||||
- AccelerometerService
|
||||
- BatteryStateService
|
||||
- HealthService
|
||||
- AppFocusService
|
||||
- CompassService
|
||||
---
|
||||
|
||||
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`` in the stack) ``app_event_loop()`` returns, and all
|
||||
deinitialization code is run before the app exits.
|
||||
|
||||
Once ``app_event_loop()`` is called, execution of `main()` pauses and all
|
||||
further activities are performed when events from various ``Event Service``
|
||||
types occur. This continues until the app is exiting, and is typically handled
|
||||
in the following pattern:
|
||||
|
||||
```c
|
||||
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`` | ``TickHandler`` | Most useful for watchfaces. Allows apps to be notified when a second, minute, hour, day, month or year ticks by. |
|
||||
| ``ConnectionService`` | ``ConnectionHandler`` | Allows apps to know when the Bluetooth connection with the phone connects and disconnects. |
|
||||
| ``AccelerometerService`` | ``AccelDataHandler``<br/>``AccelTapHandler`` | Allows apps to receive raw data or tap events from the onboard accelerometer. |
|
||||
| ``BatteryStateService`` | ``BatteryStateHandler`` | Allows apps to read the state of the battery, as well as whether the watch is plugged in and charging. |
|
||||
| ``HealthService`` | ``HealthEventHandler`` | Allows apps to be notified to changes in various ``HealthMetric`` values as the user performs physical activities. |
|
||||
| ``AppFocusService`` | ``AppFocusHandler`` | Allows apps to know when they are obscured by another window, such as when a notification modal appears. |
|
||||
| ``CompassService`` | ``CompassHeadingHandler`` | Allows apps to read a compass heading, including calibration status of the sensor. |
|
||||
|
||||
In addition, many other APIs also operate through the use of various callbacks
|
||||
including ``MenuLayer``, ``AppMessage``, ``Timer``, and ``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`` allows an app to be notified when different units of
|
||||
time change. This is decided based upon the ``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.
|
||||
|
||||
```c
|
||||
static void tick_handler(struct tm *tick_time, TimeUnits changed) {
|
||||
static char s_buffer[8];
|
||||
|
||||
// Read time into a string buffer
|
||||
strftime(s_buffer, sizeof(s_buffer), "%H:%M", tick_time);
|
||||
|
||||
APP_LOG(APP_LOG_LEVEL_INFO, "Time is now %s", s_buffer);
|
||||
}
|
||||
```
|
||||
|
||||
```c
|
||||
// Get updates when the current minute changes
|
||||
tick_timer_service_subscribe(MINUTE_UNIT, tick_handler);
|
||||
```
|
||||
|
||||
> The ``TickTimerService`` has no 'peek' API, but a similar effect can be
|
||||
> achieved using the ``time()`` and ``localtime()`` APIs.
|
||||
|
||||
|
||||
### Connection Service
|
||||
|
||||
The ``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.
|
||||
|
||||
```c
|
||||
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");
|
||||
}
|
||||
```
|
||||
|
||||
```c
|
||||
connection_service_subscribe((ConnectionHandlers) {
|
||||
.pebble_app_connection_handler = app_connection_handler,
|
||||
.pebblekit_connection_handler = kit_connection_handler
|
||||
});
|
||||
```
|
||||
|
||||
```c
|
||||
// Peek at either the Pebble app or PebbleKit connections
|
||||
bool app_connection = connection_service_peek_pebble_app_connection();
|
||||
bool kit_connection = connection_service_peek_pebblekit_connection();
|
||||
```
|
||||
|
||||
|
||||
### Accelerometer Service
|
||||
|
||||
The ``AccelerometerService`` can be used in two modes - tap events and raw data
|
||||
events. ``AccelTapHandler`` and ``AccelDataHandler`` are used for each of these
|
||||
respective use cases. See the
|
||||
{% guide_link events-and-services/accelerometer %} guide for more
|
||||
information.
|
||||
|
||||
**Data Events**
|
||||
|
||||
```c
|
||||
static void accel_data_handler(AccelData *data, uint32_t num_samples) {
|
||||
APP_LOG(APP_LOG_LEVEL_INFO, "Got %d new samples", (int)num_samples);
|
||||
}
|
||||
```
|
||||
|
||||
```c
|
||||
const int num_samples = 10;
|
||||
|
||||
// Subscribe to data events
|
||||
accel_data_service_subscribe(num_samples, accel_data_handler);
|
||||
```
|
||||
|
||||
```c
|
||||
// Peek at the last reading
|
||||
AccelData data;
|
||||
accel_service_peek(&data);
|
||||
```
|
||||
|
||||
**Tap Events**
|
||||
|
||||
```c
|
||||
static void accel_tap_handler(AccelAxisType axis, int32_t direction) {
|
||||
APP_LOG(APP_LOG_LEVEL_INFO, "Tap event received");
|
||||
}
|
||||
```
|
||||
|
||||
```c
|
||||
// Subscribe to tap events
|
||||
accel_tap_service_subscribe(accel_tap_handler);
|
||||
```
|
||||
|
||||
|
||||
### Battery State Service
|
||||
|
||||
The ``BatteryStateService`` allows apps to examine the state of the battery, and
|
||||
whether or not is is plugged in and charging.
|
||||
|
||||
```c
|
||||
static void battery_state_handler(BatteryChargeState charge) {
|
||||
// Report the current charge percentage
|
||||
APP_LOG(APP_LOG_LEVEL_INFO, "Battery charge is %d%%",
|
||||
(int)charge.charge_percent);
|
||||
}
|
||||
```
|
||||
|
||||
```c
|
||||
// Get battery state updates
|
||||
battery_state_service_subscribe(battery_state_handler);
|
||||
```
|
||||
|
||||
```c
|
||||
// Peek at the current battery state
|
||||
BatteryChargeState state = battery_state_service_peek();
|
||||
```
|
||||
|
||||
|
||||
### Health Service
|
||||
|
||||
The ``HealthService`` uses the ``HealthEventHandler`` to notify a subscribed app
|
||||
when new data pertaining to a ``HealthMetric`` is available. See the
|
||||
{% guide_link events-and-services/health %} guide for more information.
|
||||
|
||||
```c
|
||||
static void health_handler(HealthEventType event, void *context) {
|
||||
if(event == HealthEventMovementUpdate) {
|
||||
APP_LOG(APP_LOG_LEVEL_INFO, "New health movement event");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```c
|
||||
// Subscribe to health-related events
|
||||
health_service_events_subscribe(health_handler, NULL);
|
||||
```
|
||||
|
||||
|
||||
### App Focus Service
|
||||
|
||||
The ``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.
|
||||
|
||||
```c
|
||||
static void focus_handler(bool in_focus) {
|
||||
APP_LOG(APP_LOG_LEVEL_INFO, "App is %s in focus", in_focus ? "now" : "not");
|
||||
}
|
||||
```
|
||||
|
||||
```c
|
||||
// Add a basic subscription
|
||||
app_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`` 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.
|
||||
|
||||
```c
|
||||
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");
|
||||
}
|
||||
```
|
||||
|
||||
```c
|
||||
// Subscribe to both types of events
|
||||
app_focus_service_subscribe_handlers((AppFocusHandlers) {
|
||||
.will_focus = will_focus_handler,
|
||||
.did_focus = did_focus_handler
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
### Compass Service
|
||||
|
||||
The ``CompassService`` provides access to regular updates about the watch's
|
||||
magnetic compass heading, if it is calibrated. See the
|
||||
{% guide_link events-and-services/compass %} guide for more information.
|
||||
|
||||
```c
|
||||
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));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```c
|
||||
// Subscribe to compass heading updates
|
||||
compass_service_subscribe(compass_heading_handler);
|
||||
```
|
||||
|
||||
```c
|
||||
// Peek the compass heading data
|
||||
CompassHeadingData data;
|
||||
compass_service_peek(&data);
|
||||
```
|
469
devsite/source/_guides/events-and-services/health.md
Normal file
469
devsite/source/_guides/events-and-services/health.md
Normal file
|
@ -0,0 +1,469 @@
|
|||
---
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
title: Pebble Health
|
||||
description: |
|
||||
Information on using the HealthService API to incorporate multiple types of
|
||||
health data into your apps.
|
||||
guide_group: events-and-services
|
||||
order: 6
|
||||
platforms:
|
||||
- basalt
|
||||
- chalk
|
||||
- diorite
|
||||
- emery
|
||||
related_docs:
|
||||
- HealthService
|
||||
related_examples:
|
||||
- title: Simple Health Example
|
||||
url: https://github.com/pebble-examples/simple-health-example
|
||||
---
|
||||
|
||||
[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`` 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`` (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`` API must declare the 'health'
|
||||
capability in order to be accepted by the
|
||||
[developer portal](https://dev-portal.getpebble.com/). This can be done in
|
||||
CloudPebble 'Settings', or in `package.json` in the local SDK:
|
||||
|
||||
```js
|
||||
"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`` 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`` `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``. The `event` parameter describes the type of each update,
|
||||
and is one of the following from the ``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. |
|
||||
|
||||
A simple example handler is shown below, which outputs to app logs the type of
|
||||
event that fired the callback:
|
||||
|
||||
```c
|
||||
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:
|
||||
|
||||
```c
|
||||
#if defined(PBL_HEALTH)
|
||||
// Attempt to subscribe
|
||||
if(!health_service_events_subscribe(health_handler, NULL)) {
|
||||
APP_LOG(APP_LOG_LEVEL_ERROR, "Health not available!");
|
||||
}
|
||||
#else
|
||||
APP_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`` API, and is available to
|
||||
apps through various ``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`` 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:
|
||||
|
||||
```c
|
||||
#if defined(PBL_HEALTH)
|
||||
// Use the step count metric
|
||||
HealthMetric 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 available
|
||||
HealthServiceAccessibilityMask mask = health_service_metric_accessible(metric,
|
||||
start, end);
|
||||
bool any_data_available = mask & HealthServiceAccessibilityMaskAvailable;
|
||||
#else
|
||||
// Health data is not available here
|
||||
bool 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:
|
||||
|
||||
```c
|
||||
HealthMetric metric = HealthMetricStepCount;
|
||||
time_t start = time_start_of_today();
|
||||
time_t end = time(NULL);
|
||||
|
||||
// Check the metric has data available for today
|
||||
HealthServiceAccessibilityMask 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 today
|
||||
APP_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``, such as ``SECONDS_PER_HOUR`` to adjust
|
||||
a timestamp relative to the current moment returned by ``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:
|
||||
|
||||
```c
|
||||
// Make a timestamp for now
|
||||
time_t end = time(NULL);
|
||||
|
||||
// Make a timestamp for the last hour's worth of data
|
||||
time_t start = end - SECONDS_PER_HOUR;
|
||||
|
||||
// Check data is available
|
||||
HealthServiceAccessibilityMask result =
|
||||
health_service_metric_accessible(HealthMetricStepCount, start, end);
|
||||
if(result & HealthServiceAccessibilityMaskAvailable) {
|
||||
// Data is available! Read it
|
||||
HealthValue 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`` before
|
||||
formatting distance data from the ``HealthService``:
|
||||
|
||||
> Note: This API is currently only meaningful when querying the
|
||||
> ``HealthMetricWalkedDistanceMeters`` metric. ``MeasurementSystemUnknown`` will
|
||||
> be returned for all other queries.
|
||||
|
||||
```c
|
||||
const HealthMetric metric = HealthMetricWalkedDistanceMeters;
|
||||
const HealthValue distance = health_service_sum_today(metric);
|
||||
|
||||
// Get the preferred measurement system
|
||||
MeasurementSystem system = health_service_get_measurement_system_for_display(
|
||||
metric);
|
||||
|
||||
// Format accordingly
|
||||
static 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 first
|
||||
int 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 units
|
||||
text_layer_set_text(s_some_layer, s_buffer);
|
||||
```
|
||||
|
||||
|
||||
## Obtaining Averaged Data
|
||||
|
||||
The ``HealthService`` also allows developers to read average values of a
|
||||
particular ``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`` 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()``. |
|
||||
| `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``), 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:
|
||||
|
||||
```c
|
||||
// Define query parameters
|
||||
const HealthMetric metric = HealthMetricStepCount;
|
||||
const HealthServiceTimeScope scope = HealthServiceTimeScopeDaily;
|
||||
|
||||
// Use the average daily value from midnight to the current time
|
||||
const time_t start = time_start_of_today();
|
||||
const time_t end = time(NULL);
|
||||
|
||||
// Check that an averaged value is accessible
|
||||
HealthServiceAccessibilityMask mask =
|
||||
health_service_metric_averaged_accessible(metric, start, end, scope);
|
||||
if(mask & HealthServiceAccessibilityMaskAvailable) {
|
||||
// Average is available, read it
|
||||
HealthValue 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`` 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:
|
||||
|
||||
```c
|
||||
// Get an activities mask
|
||||
HealthActivityMask activities = health_service_peek_current_activities();
|
||||
|
||||
// Determine which bits are set, and hence which activity is active
|
||||
if(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`` 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`` 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.
|
||||
|
||||
```c
|
||||
// Create an array to store data
|
||||
const 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 that
|
||||
time_t end = time(NULL) - (15 * SECONDS_PER_MINUTE);
|
||||
time_t start = end - SECONDS_PER_HOUR;
|
||||
|
||||
// Obtain the minute-by-minute records
|
||||
uint32_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 minute
|
||||
for(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:
|
||||
|
||||
```c
|
||||
// Free the array
|
||||
free(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()`` 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`` 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.
|
||||
|
||||
```c
|
||||
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 returned
|
||||
while (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 available
|
||||
return 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 array
|
||||
max_records = get_available_records(data, query_start, query_end, max_records);
|
||||
|
||||
// Print the results
|
||||
for(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);
|
||||
}
|
||||
```
|
253
devsite/source/_guides/events-and-services/hrm.md
Normal file
253
devsite/source/_guides/events-and-services/hrm.md
Normal file
|
@ -0,0 +1,253 @@
|
|||
---
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
title: Heart Rate Monitor
|
||||
description: |
|
||||
Information on using the HealthService API to obtain information from the
|
||||
Heart Rate Monitor.
|
||||
guide_group: events-and-services
|
||||
order: 6
|
||||
related_docs:
|
||||
- HealthService
|
||||
related_examples:
|
||||
- title: HRM Watchface
|
||||
url: https://github.com/pebble-examples/watchface-tutorial-hrm
|
||||
- title: HRM Activity
|
||||
url: https://github.com/pebble-examples/hrm-activity-example
|
||||
platform_choice: true
|
||||
---
|
||||
|
||||
The Pebble Time 2 and Pebble 2 (excluding SE model)
|
||||
{% guide_link tools-and-resources/hardware-information "devices" %} include a
|
||||
heart rate monitor. This guide will demonstrate how to use the ``HealthService``
|
||||
API to retrieve information about the user's current, and historical heart
|
||||
rates.
|
||||
|
||||
If you aren't already familiar with the ``HealthService``, we recommended that
|
||||
you read the {% guide_link events-and-services/health "Health guide" %}
|
||||
before proceeding.
|
||||
|
||||
## Enable Health Data
|
||||
|
||||
<div class="platform-specific" data-sdk-platform="local">
|
||||
{% markdown {} %}
|
||||
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.
|
||||
|
||||
```js
|
||||
{
|
||||
...
|
||||
"pebble": {
|
||||
...
|
||||
"capabilities": [ "health" ],
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
{% endmarkdown %}
|
||||
</div>
|
||||
|
||||
^CP^ Before your application is able to access heart rate information, you will
|
||||
need to enable the `USES HEALTH` option in your project settings on
|
||||
[CloudPebble]({{ site.data.links.cloudpebble }}).
|
||||
|
||||
|
||||
## 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``) and a
|
||||
filtered reading (``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`` is available by using the
|
||||
``health_service_metric_accessible`` method.
|
||||
|
||||
Then you can obtain the current heart rate using the
|
||||
``health_service_peek_current_value`` method:
|
||||
|
||||
```c
|
||||
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 value
|
||||
static 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`` method.
|
||||
|
||||
```c
|
||||
|
||||
static void prv_on_health_data(HealthEventType type, void *context) {
|
||||
// If the update was from the Heart Rate Monitor, query it
|
||||
if (type == HealthEventHeartRateUpdate) {
|
||||
HealthValue value = health_service_peek_current_value(HealthMetricHeartRateBPM);
|
||||
// Display the heart rate
|
||||
}
|
||||
}
|
||||
|
||||
static void prv_init(void) {
|
||||
// Subscribe to health event handler
|
||||
health_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`` 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:
|
||||
|
||||
```c
|
||||
// Set the heart rate monitor to sample every second
|
||||
bool 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.
|
||||
|
||||
```c
|
||||
// Reset the heart rate sampling period to automatic
|
||||
health_service_set_heart_rate_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`` method.
|
||||
|
||||
Then we'll use the ``health_service_aggregate_averaged`` method to
|
||||
obtain the average daily heart rate over the last 7 days.
|
||||
|
||||
```c
|
||||
// Obtain history for last 7 days
|
||||
time_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.
|
||||
|
||||
```c
|
||||
// Obtain history for last 1 hour
|
||||
time_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`` 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
|
||||
> {% guide_link events-and-services/health#read-per-minute-history "Health Guide" %}
|
||||
> 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``
|
||||
documentation, and integrate it into your next project.
|
44
devsite/source/_guides/events-and-services/index.md
Normal file
44
devsite/source/_guides/events-and-services/index.md
Normal file
|
@ -0,0 +1,44 @@
|
|||
---
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
title: Events and Services
|
||||
description: |
|
||||
How to get data from the onboard sensors including the accelerometer, compass,
|
||||
and microphone.
|
||||
guide_group: events-and-services
|
||||
menu: false
|
||||
permalink: /guides/events-and-services/
|
||||
generate_toc: false
|
||||
hide_comments: true
|
||||
---
|
||||
|
||||
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`` API). In addition, the
|
||||
Basalt and Chalk platforms also include a microphone (accessible via the
|
||||
``Dictation`` API) and access to Pebble Health data sets. Read
|
||||
{% guide_link tools-and-resources/hardware-information %} 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
|
||||
{% guide_link best-practices/conserving-battery-life %} for more information.
|
||||
|
||||
|
||||
## Contents
|
||||
|
||||
{% include guides/contents-group.md group=page.group_data %}
|
240
devsite/source/_guides/events-and-services/persistent-storage.md
Normal file
240
devsite/source/_guides/events-and-services/persistent-storage.md
Normal file
|
@ -0,0 +1,240 @@
|
|||
---
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
title: Persistent Storage
|
||||
description: |
|
||||
Using persistent storage to improve your app's UX.
|
||||
guide_group: events-and-services
|
||||
order: 7
|
||||
related_docs:
|
||||
- Storage
|
||||
---
|
||||
|
||||
Developers can use the ``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 {% guide_link user-interfaces/app-configuration %} for more information on
|
||||
implementing an app configuration page.
|
||||
|
||||
|
||||
## Persistent Storage Model
|
||||
|
||||
Every app is allocated 4 kB of persistent storage space and can write values to
|
||||
storage using a key, similar to ``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`` (currently 256 bytes).
|
||||
|
||||
When an app is updated the values saved using the ``Storage`` API will be
|
||||
persisted, but if it is uninstalled they will be removed.
|
||||
|
||||
Apps that make large use of the ``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.
|
||||
|
||||
```c
|
||||
uint32_t key = 0;
|
||||
int num_items = 0;
|
||||
|
||||
if (persist_exists(key)) {
|
||||
// Read persisted value
|
||||
num_items = persist_read_int(key);
|
||||
} else {
|
||||
// Choose a default value
|
||||
num_items = 10;
|
||||
|
||||
// Remember the default value until the user chooses their own value
|
||||
persist_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
|
||||
|
||||
```c
|
||||
uint32_t key = 0;
|
||||
bool large_font_size = true;
|
||||
```
|
||||
|
||||
```c
|
||||
// Write a boolean value
|
||||
persist_write_bool(key, large_font_size);
|
||||
```
|
||||
|
||||
```c
|
||||
// Read the boolean value
|
||||
bool large_font_size = persist_read_bool(key);
|
||||
```
|
||||
|
||||
|
||||
### Integers
|
||||
|
||||
```c
|
||||
uint32_t key = 1;
|
||||
int highscore = 432;
|
||||
```
|
||||
|
||||
```c
|
||||
// Write an integer
|
||||
persist_write_int(key, highscore);
|
||||
```
|
||||
|
||||
```c
|
||||
// Read the integer value
|
||||
int highscore = persist_read_int(key);
|
||||
```
|
||||
|
||||
|
||||
### Strings
|
||||
|
||||
```c
|
||||
uint32_t key = 2;
|
||||
char *string = "Remember this!";
|
||||
```
|
||||
|
||||
```c
|
||||
// Write the string
|
||||
persist_write_string(key, string);
|
||||
```
|
||||
|
||||
```c
|
||||
// Read the string
|
||||
char buffer[32];
|
||||
persist_read_string(key, buffer, sizeof(buffer));
|
||||
```
|
||||
|
||||
|
||||
### Data Structures
|
||||
|
||||
```c
|
||||
typedef struct {
|
||||
int a;
|
||||
int b;
|
||||
} Data;
|
||||
|
||||
uint32_t key = 3;
|
||||
Data data = (Data) {
|
||||
.a = 32,
|
||||
.b = 45
|
||||
};
|
||||
```
|
||||
|
||||
```c
|
||||
// Write the data structure
|
||||
persist_write_data(key, &data, sizeof(Data));
|
||||
```
|
||||
|
||||
```c
|
||||
// Read the data structure
|
||||
persist_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:
|
||||
|
||||
```c
|
||||
const uint32_t storage_version_key = 786;
|
||||
const int current_storage_version = 2;
|
||||
```
|
||||
|
||||
```c
|
||||
// Store the current storage scheme version number
|
||||
persist_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.
|
||||
|
||||
```c
|
||||
// The scheme has changed, increment the version number
|
||||
const int current_storage_version = 3;
|
||||
```
|
||||
|
||||
```c
|
||||
static void migrate_storage_data() {
|
||||
// Check the last storage scheme version the app used
|
||||
int last_storage_version = persist_read_int(storage_version_key);
|
||||
|
||||
if (last_storage_version == current_storage_version) {
|
||||
// No migration necessary
|
||||
return;
|
||||
}
|
||||
|
||||
// Migrate data
|
||||
switch(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 3
|
||||
int highscore = persist_read_int(old_highscore_key);
|
||||
persist_write_int(new_highscore_key, highscore);
|
||||
|
||||
// Delete old data
|
||||
persist_delete(old_highscore_key);
|
||||
break;
|
||||
}
|
||||
|
||||
// Migration is complete, store the current storage scheme version number
|
||||
persist_write_int(storage_version_key, current_storage_version);
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Alternative Method
|
||||
|
||||
In addition to the ``Storage`` API, data can also be persisted using the
|
||||
`localStorage` API in PebbleKit JS, and communicated with the watch over
|
||||
``AppMessage`` when the app is lanched. However, this method uses more power and
|
||||
fails if the watch is not connected to the phone.
|
||||
|
206
devsite/source/_guides/events-and-services/wakeups.md
Normal file
206
devsite/source/_guides/events-and-services/wakeups.md
Normal file
|
@ -0,0 +1,206 @@
|
|||
---
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
title: Wakeups
|
||||
description: |
|
||||
Using the Wakeup API to launch an app at some future time.
|
||||
guide_group: events-and-services
|
||||
order: 8
|
||||
related_examples:
|
||||
- title: Tea Timer
|
||||
url: https://github.com/pebble-examples/feature-app-wakeup
|
||||
related_docs:
|
||||
- Wakeup
|
||||
- AppLaunchReason
|
||||
- launch_reason
|
||||
- Timer
|
||||
---
|
||||
|
||||
The ``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`` 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`` API will fall into
|
||||
three distinct scenarios discussed below.
|
||||
|
||||
|
||||
### A Future Time
|
||||
|
||||
Call ``time()`` and add the offset, measured in seconds. For example, for 30
|
||||
minutes in the future:
|
||||
|
||||
```c
|
||||
// 30 minutes from now
|
||||
time_t timestamp = time(NULL) + (30 * SECONDS_PER_MINUTE);
|
||||
```
|
||||
|
||||
|
||||
### A Specific Time
|
||||
|
||||
Use ``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:
|
||||
|
||||
```c
|
||||
// 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:
|
||||
|
||||
```c
|
||||
// Let the timestamp be 30 minutes from now
|
||||
const time_t future_timestamp = time() + (30 * SECONDS_PER_MINUTE);
|
||||
|
||||
// Choose a 'cookie' value representing the reason for the wakeup
|
||||
const 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 event
|
||||
WakeupId id = wakeup_schedule(future_timestamp, cookie, notify_if_missed);
|
||||
|
||||
// Check the scheduling was successful
|
||||
if(id >= 0) {
|
||||
// Persist the ID so that a future launch can query it
|
||||
const 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`` with ``wakeup_query()``, and then perform simple arithmetic to get
|
||||
the time remaining:
|
||||
|
||||
```c
|
||||
// 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 remaining
|
||||
int 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`` obtained when it was
|
||||
scheduled:
|
||||
|
||||
```c
|
||||
// Cancel a wakeup
|
||||
wakeup_cancel(id);
|
||||
```
|
||||
|
||||
To cancel all scheduled wakeup events:
|
||||
|
||||
```c
|
||||
// Cancel all wakeups
|
||||
wakeup_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`` of
|
||||
``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`` 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`` and load the app accordingly:
|
||||
|
||||
```c
|
||||
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 appropriately
|
||||
wakeup_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:
|
||||
|
||||
```c
|
||||
static void wakeup_handler(WakeupId id, int32_t reason) {
|
||||
// A wakeup event has occurred while the app was already open
|
||||
}
|
||||
```
|
||||
|
||||
```c
|
||||
// Subscribe to wakeup service
|
||||
wakeup_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:
|
||||
|
||||
```c
|
||||
// Get details of the wakeup
|
||||
wakeup_get_launch_event(&id, &reason);
|
||||
|
||||
// Manually handle using the handler
|
||||
wakeup_handler(id, reason);
|
||||
```
|
Loading…
Add table
Add a link
Reference in a new issue