# Pebble Developer Documentation: /guides/alloy/ SDK 4.33.1. 19 pages. Index: https://developer.repebble.com/llms.txt # Getting Started with Alloy Source: https://developer.repebble.com/guides/alloy/getting-started/ This guide walks you through creating your first Alloy app for Pebble. > **Platform Support** : Alloy currently supports Emery (Pebble Time 2) and Gabbro (Pebble Round 2). See the platform support table in [_Hardware Information_](https://developer.repebble.com/guides/tools-and-resources/hardware-information.md#platform-support). ## Creating a New Project Use the `pebble` command-line tool to create a new Alloy project: ``` $ pebble new-project --alloy my-first-app ``` This creates a new directory with the following structure: ``` my-first-app/ src/ embeddedjs/ main.js # Watch app code (runs on Pebble) pkjs/ index.js # Phone code (runs on connected phone) resources/ # App resources (images, fonts, etc.) package.json # App manifest and configuration ``` ## Project Structure Alloy apps have **two JavaScript environments** : | Location | File | Runs On | Purpose | | --- | --- | --- | --- | | **embeddedjs** | `src/embeddedjs/main.js` | Watch | Your app UI and logic | | **PKJS** | `src/pkjs/index.js` | Phone | Network proxy, location, config | ### Watch Code (embeddedjs) The `src/embeddedjs/main.js` file is your watch app's entry point. This code runs directly on the Pebble watch: **⌚ Watch** (src/embeddedjs/main.js): ``` console.log("Hello, Pebble!"); ``` ### Phone Code (PKJS) The `src/pkjs/index.js` file runs on the connected phone. It's used for network requests, GPS location, and configuration: **📱 PKJS** (src/pkjs/index.js): ``` Pebble.addEventListener("ready", function(e) {console.log("PebbleKit JS ready!");}); ``` ### package.json The `package.json` file contains your app's metadata and configuration: ``` {"name": "My First App","author": "Your Name","version": "1.0.0","keywords": ["pebble-app"],"private": true,"dependencies": {},"pebble": {"displayName": "My First App","uuid": "12345678-1234-1234-1234-123456789abc","projectType": "moddable","sdkVersion": "3","enableMultiJS": true,"targetPlatforms": ["emery"],"watchapp": {"watchface": false},"messageKeys": [],"resources": {"media": []}}} ``` Key fields in the `pebble` section: | Field | Description | | --- | --- | | `displayName` | The name shown on the watch | | `uuid` | Unique identifier for your app | | `targetPlatforms` | Which Pebble platforms to build for | | `watchapp.watchface` | Set to `true` for watchfaces, `false` for apps | | `messageKeys` | Keys for watch-phone communication | ## Using ES Modules Alloy uses standard ECMAScript modules. You can split your code across multiple files and import them: **math.js** ``` export function add(a, b) {return a + b;}export function multiply(a, b) {return a \* b;} ``` **main.js** ``` import { add, multiply } from "./math";console.log("Sum: " + add(2, 3));// 5console.log("Product: " + multiply(4, 5));// 20 ``` ### Declaring Modules in manifest.json Each additional module must be listed in `src/embeddedjs/manifest.json` so the Moddable build system knows to compile it. The default manifest only includes `main.js`: ``` "modules": {"\*": "./main"} ``` To add more modules, change the value to an array of paths. The `.js` extension is optional: ``` "modules": {"\*": ["./main","./math"]} ``` If you import a module that isn't listed here, you'll get a module-not-found error at runtime. ## Important JavaScript Differences Alloy runs on the XS JavaScript engine, which has some differences from browser or Node.js JavaScript: ### Strict Mode All code runs in strict mode by default. ### Hardened JavaScript Built-in objects (primordials) are frozen and cannot be modified: ``` // This will throw an error:Array.prototype.myMethod = function() {}; ``` ### No eval() Evaluation of JavaScript source code by eval, Function and friends is not supported to keep minimize the code footprint of the JavaScript engine. ### Explicit Resource Management Since SDK 4.33, Alloy supports Explicit Resource Management: `using` declarations, `Symbol.dispose`, `DisposableStack` and `SuppressedError`. Alloy classes with a `close()` method (buttons, sensors, messages, files, network clients and more) are disposable, so they can be released automatically at the end of a scope: ``` function readScore(path) {using file = device.files.openFile({ path });const data = file.read(file.status().size, 0);return JSON.parse(String.fromArrayBuffer(data)).score;// file.close() runs automatically when the scope exits,// even if read() or JSON.parse() throws} ``` ## Debugging Output Use `console.log()` for debug output: ``` console.log("Debug message");console.log("Value: " + someVariable); ``` Output appears in the Pebble emulator console or when viewing logs from a physical watch. > **Note** : `trace()` is also available as a lower-level alternative, but requires a manual newline (`\n`) at the end of each message. `console.log()` is recommended for most use cases. ## Building and Running To build your app and run it in the emulator: ``` $ pebble build$ pebble install --emulator emery ``` To install on a physical watch: ``` $ pebble install --phone YOUR_PHONE_IP ``` ## Next Steps Now that you have a basic app running, explore the following guides: - [Piu UI Framework](https://developer.repebble.com/guides/alloy/piu-guide.md): Build declarative user interfaces - [Poco Graphics](https://developer.repebble.com/guides/alloy/poco-guide.md): Low-level drawing and graphics - [Sensors and Input](https://developer.repebble.com/guides/alloy/sensors-and-input.md): Handle buttons and read sensors - [Storage](https://developer.repebble.com/guides/alloy/storage.md): Persist data between app launches - [Networking](https://developer.repebble.com/guides/alloy/networking.md): Make HTTP requests and use WebSockets - [App Messages](https://developer.repebble.com/guides/alloy/app-messages.md): Send and receive messages between watch and phone --- # Port (Custom Drawing) Source: https://developer.repebble.com/guides/alloy/port-drawing/ The `Port` content type lets you do custom drawing within a Piu application. This combines the best of both worlds: Piu's declarative layout and component system with Poco-style direct drawing. > **Note** : All code in this guide runs on the watch in `src/embeddedjs/main.js`. ## What is Port? Port is a Piu content type that provides a canvas for custom drawing. Unlike other Piu content types that are styled with skins, Port content uses an `onDraw` callback where you can draw shapes, text, and graphics. ## Basic Port Usage ``` class MyPortBehavior extends Behavior {onDraw(port, x, y, width, height) {// Draw a blue rectangleport.fillColor("#0066cc", 0, 0, width, height);// Draw some textconst style = new Style({ font: "18px Gothic", color: "white" });port.drawString("Hello!", style, "white", 10, 10);}}const myPort = new Port(null, {top: 0, bottom: 0, left: 0, right: 0,Behavior: MyPortBehavior});const application = new Application(null, {skin: new Skin({ fill: "white" })});application.add(myPort); ``` ## Port Drawing Methods ### Filling Shapes ``` onDraw(port, x, y, width, height) {// Fill a rectangle with a colorport.fillColor("blue", 10, 10, 50, 30);// Fill with hex colorport.fillColor("#ff6600", 70, 10, 50, 30);} ``` ### Drawing Text ``` onDraw(port, x, y, width, height) {const style = new Style({font: "bold 18px Gothic",color: "black"});// Draw text at positionport.drawString("Hello World", style, "black", 10, 50);// Measure text width for alignmentconst text = "Centered";const measured = style.measure(text);const textX = (width - measured.width) / 2;port.drawString(text, style, "black", textX, 80);} ``` ### Drawing Images ``` onDraw(port, x, y, width, height) {const texture = new Texture(2);// Resource IDport.drawTexture(texture, "white", 10, 10, 0, 0, texture.width, texture.height);} ``` ## Updating Port Content Call `invalidate()` to trigger a redraw: ``` class AnimatedPortBehavior extends Behavior {onDisplaying(port) {this.value = 0;port.interval = 100;// Update every 100msport.start();}onTimeChanged(port) {this.value = (this.value + 5) % 100;port.invalidate();// Request redraw}onDraw(port, x, y, width, height) {// Clear backgroundport.fillColor("white", 0, 0, width, height);// Draw bar based on current valueconst barWidth = (width \* this.value) / 100;port.fillColor("green", 0, height / 2 - 10, barWidth, 20);}} ``` ## Complete Example: Live Bar Chart This example creates an animated bar chart with random data: ``` const GRAY = "gray";const BLUE = "#1932ab";const textStyle = new Style({font: "bold 18px Gothic"});class GraphBehavior extends Behavior {onDisplaying(port) {// Initialize data arrayconst barCount = Math.floor(screen.width / 16);this.values = new Array(barCount);this.values.fill(0);// Start animationport.interval = 100;port.start();}onTimeChanged(port) {// Shift data and add new random valuethis.values.shift();this.values.push(Math.floor(Math.random() \* 100));port.invalidate();}onDraw(port, x, y, width, height) {// Draw Y-axis labels and grid linesfor (let i = 100, yOffset = 0; i \>= 0; i -= 20) {const label = String(i);const labelWidth = textStyle.measure(label).width;port.drawString(label, textStyle, "black", 30 - labelWidth, yOffset);port.fillColor(GRAY, 35, yOffset + 10, width - 35, 1);yOffset += height / 5;}// Draw barslet xOffset = 35;for (let i = 0; i \< this.values.length; i++) {const value = this.values[i];const barHeight = Math.floor((value \* (height - 10)) / 100);port.fillColor(BLUE, xOffset, height - barHeight, 12, barHeight);xOffset += 14;}}}const graph = new Port(null, {top: 0, bottom: 0, left: 0, right: 0,Behavior: GraphBehavior});const application = new Application(null, {skin: new Skin({ fill: "white" })});application.add(graph); ``` ## Combining Port with Piu Layout Port works within Piu's layout system, so you can combine custom drawing with standard Piu components: ``` import {} from "piu/MC";const headerSkin = new Skin({ fill: "darkblue" });const headerStyle = new Style({font: "bold 18px Gothic",color: "white"});class ChartBehavior extends Behavior {onDisplaying(port) {this.data = [30, 50, 80, 45, 90, 60, 75];port.invalidate();}onDraw(port, x, y, width, height) {port.fillColor("white", 0, 0, width, height);const barWidth = width / this.data.length - 4;let xPos = 2;for (const value of this.data) {const barHeight = (value / 100) \* height;port.fillColor("#4CAF50", xPos, height - barHeight, barWidth, barHeight);xPos += barWidth + 4;}}}const DashboardApp = Application.template($ =\> ({skin: new Skin({ fill: "#f0f0f0" }),contents: [Column($, {top: 0, bottom: 0, left: 0, right: 0,contents: [// Header using standard Piu LabelLabel($, {top: 0, height: 40, left: 0, right: 0,skin: headerSkin,style: headerStyle,string: "Weekly Stats"}),// Custom chart using PortPort($, {top: 10, bottom: 10, left: 10, right: 10,Behavior: ChartBehavior})]})]}));export default new DashboardApp({}, {}); ``` ## Port vs Poco | Feature | Port | Poco | | --- | --- | --- | | Integration | Works within Piu apps | Standalone rendering | | Layout | Uses Piu's layout system | Manual positioning | | Updates | Call `invalidate()` | Call `begin()`/`end()` | | Use case | Custom drawing in Piu apps | Full-screen graphics | Use Port when you want custom drawing as part of a larger Piu UI. Use Poco when you need full control over the entire screen. ## Performance Tips 1. **Minimize invalidate() calls**: Only call when data actually changes. 2. **Draw only what's needed** : The `onDraw` callback receives bounds - use them to clip your drawing. 3. **Cache expensive calculations** : Calculate positions and sizes once, not on every draw. 4. **Use appropriate intervals** : 100ms intervals are usually sufficient for charts. Use shorter intervals only when smooth animation is needed. ## Common Patterns ### Progress Bar ``` class ProgressBehavior extends Behavior {onCreate(port, data) {this.progress = data.progress || 0;}setProgress(port, value) {this.progress = Math.max(0, Math.min(100, value));port.invalidate();}onDraw(port, x, y, width, height) {// Backgroundport.fillColor("#ddd", 0, 0, width, height);// Progress fillconst fillWidth = (width \* this.progress) / 100;port.fillColor("#4CAF50", 0, 0, fillWidth, height);}} ``` ### Gauge/Meter ``` class GaugeBehavior extends Behavior {onCreate(port, data) {this.value = data.value || 0;this.max = data.max || 100;}onDraw(port, x, y, width, height) {const centerX = width / 2;const centerY = height / 2;const radius = Math.min(width, height) / 2 - 10;// Background circle (using rectangles as approximation)port.fillColor("#eee", centerX - radius, centerY - radius,radius \* 2, radius \* 2);// Value indicatorconst percentage = this.value / this.max;const indicatorHeight = radius \* 2 \* percentage;port.fillColor("#2196F3",centerX - radius, centerY + radius - indicatorHeight,radius \* 2, indicatorHeight);}} ``` ## Examples The [Pebble Examples](https://github.com/Moddable-OpenSource/pebble-examples) repository includes a Port example: - [`hellopiu-port`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellopiu-port) - animated bar graph using a Piu Port with custom drawing --- # Advanced Networking Source: https://developer.repebble.com/guides/alloy/advanced-networking/ In addition to `fetch()` and `WebSocket`, Alloy provides low-level ECMA-419 networking APIs that offer more control over HTTP and WebSocket connections. In fact, `fetch()` and `WebSocket()` are implemented using these ECMA-419 APIs. > **Note** : Like `fetch()` and `WebSocket`, these APIs require the `@moddable/pebbleproxy` package. See below for setup. ## Proxy Setup All networking APIs require proxy code running on the phone. Install the `@moddable/pebbleproxy` package in your project directory: ``` $ pebble package install @moddable/pebbleproxy ``` Then add the proxy to your `src/pkjs/index.js`: **📱 PKJS** (src/pkjs/index.js): ``` const moddableProxy = require("@moddable/pebbleproxy");Pebble.addEventListener('ready', moddableProxy.readyReceived);Pebble.addEventListener('appmessage', moddableProxy.appMessageReceived); ``` If your app also needs to handle its own `ready` or `appmessage` events, you can call the proxy functions from your own event handlers: ``` const moddableProxy = require("@moddable/pebbleproxy");Pebble.addEventListener('ready', function(e) {moddableProxy.readyReceived(e);// Handle your own ready event here})Pebble.addEventListener('appmessage', function(e) {if (moddableProxy.appMessageReceived(e))return;// Handle your own app messages here}); ``` The `@moddable/pebbleproxy` package handles proxying for `fetch()`, `WebSocket`, `HTTPClient`, and `WebSocketClient`. ## HTTPClient The `HTTPClient` class provides streaming HTTP requests with fine-grained control over headers and response handling. ### Using HTTPClient **⌚ Watch** (src/embeddedjs/main.js): ``` import HTTPClient from "embedded:network/http/client";const http = new HTTPClient({host: "example.com"});http.request({path: "/",method: "GET",headers: new Map([["User-Agent", "Pebble App"]]),headersMask: ["content-type", "date"],onHeaders(status, headers, statusText) {console.log("Status: " + status + " " + statusText);headers.forEach((value, key) =\> {console.log(key + ": " + value);});},onReadable(count) {// Read response body in chunksconst buffer = this.read(count);console.log(String.fromArrayBuffer(buffer));},onComplete() {console.log("Request complete");},onError(error) {console.log("Error: " + error);}}); ``` ### HTTPClient Options | Option | Description | | --- | --- | | `host` | Target hostname | | `port` | Port number (optional) | ### Request Options | Option | Description | | --- | --- | | `path` | URL path (e.g., `/api/data`) | | `method` | HTTP method (default: `GET`) | | `headers` | Map of request headers | | `headersMask` | Array of header names to include in response | | `body` | Request body (for POST/PUT) | ### Request Callbacks | Callback | Description | | --- | --- | | `onHeaders(status, headers, statusText)` | Response headers received | | `onReadable(count)` | Response body data available (`count` is the number of bytes available to read) | | `onComplete()` | Request completed successfully | | `onError(error)` | Request failed | ## WebSocketClient The `WebSocketClient` class provides a low-level WebSocket connection with streaming support. ### Using WebSocketClient **⌚ Watch** (src/embeddedjs/main.js): ``` import WebSocketClient from "embedded:network/websocket/client";const ws = new WebSocketClient({...device.network.ws,// use device.network.wss for secure (wss://) connectionshost: "websockets.chilkat.io",path: "/wsChilkatEcho.ashx",onReadable(count, options) {console.log("Received " + count + " bytes, binary: " + options.binary);const data = this.read();console.log(String.fromArrayBuffer(data));},onWritable(count) {console.log("Ready to write " + count + " bytes");this.write(ArrayBuffer.fromString("Hello!"), { binary: false });},onClose() {console.log("Connection closed");},onError() {console.log("Connection error");}}); ``` ### WebSocketClient Callbacks | Callback | Description | | --- | --- | | `onReadable(count, options)` | Data received from server | | `onWritable(count)` | Ready to send data | | `onClose()` | Connection closed | | `onError()` | Connection error occurred | ## HTTPClient vs fetch() | Feature | HTTPClient | fetch() | | --- | --- | --- | | API style | ECMA-419 callbacks | Promise-based | | Streaming | Yes | No | | Header control | Fine-grained with mask | Basic | | Best for | Large responses, streaming | Simple API calls | ## WebSocketClient vs WebSocket | Feature | WebSocketClient | WebSocket | | --- | --- | --- | | API style | ECMA-419 callbacks | Web standard events | | Streaming | Yes | No | | Best for | Low-level control | Simple messaging | ## Examples The [Pebble Examples](https://github.com/Moddable-OpenSource/pebble-examples) repository includes advanced networking examples: - [`hellohttpclient`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellohttpclient) - HTTP requests using the ECMA-419 `HTTPClient` - [`hellowebsocketclient`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellowebsocketclient) - WebSocket connections using the ECMA-419 `WebSocketClient` --- # Native Functions (FFI) Source: https://developer.repebble.com/guides/alloy/ffi/ The Foreign Function Interface (FFI) lets your Alloy app call native C functions directly from JavaScript. This is useful for performance-critical code, for reusing existing C libraries, or for reaching low-level functionality that isn't exposed through a JavaScript API. > **Platform Support** : Like the rest of Alloy, FFI is available on Emery (Pebble Time 2) and Gabbro (Pebble Round 2). An FFI binding has three parts: 1. The **native C source** that implements the functions. 2. An **`ffi` block** in `manifest.json` that lists the C sources and declares each function's signature. 3. The **`Natives` global** in your JavaScript, through which the functions are called. ## 1. Write the native C functions Add one or more C files alongside your `mdbl.c` (for example `src/c/add.c`). The C function names must match the names you declare in the manifest: ``` // add.c#include \int32\_t add(int32\_t a, int32\_t b) {return a + b;}int32\_t addSquares(int32\_t a, int32\_t b) {return (a \* a) + (b \* b);} ``` Functions can take and return buffers and strings as well as numbers: ``` // hello.c#include \#include \// Fill the caller-provided buffer with bytes derived from `greeting`.void hello(char \*greeting, uint8\_t \*bytes, uint32\_t length) {size\_t len = strlen(greeting);for (uint32\_t i = 0; i \< length; i++) {bytes[i] = (uint8\_t)greeting[i % len];}} ``` ## 2. Declare the functions in the manifest In `src/embeddedjs/manifest.json`, add an `ffi` block listing your C `sources` and the `functions` with their argument and return types: ``` {"include": ["$(MODDABLE)/examples/manifest\_mod.json","$(MODDABLE)/examples/manifest\_typings.json"],"modules": {"\*": "./main"},"ffi": {"sources": ["./add.c","./hello.c"],"functions": {"add": {"arguments": ["int32\_t", "int32\_t"],"returns": "int32\_t"},"addSquares": {"arguments": ["int32\_t", "int32\_t"],"returns": "int32\_t"},"hello": {"arguments": ["char \*", "uint8\_t\*", "uint32\_t"],"returns": "void"}}}} ``` ### Supported types | C type | JavaScript value | | --- | --- | | `int32_t`, `uint8_t`, `uint32_t` | `Number` | | `char*` (argument) | `String` | | `char*`, `const char*` (return) | `String` | | `uint8_t*`, `void*` | `ArrayBuffer` (pass `.buffer` from a typed array or `DataView`) | | `void` | `undefined` | Numeric arguments are passed by value. Buffers are passed by reference, so a C function can read from and write into an `ArrayBuffer` you pass in. ## 3. Make the machine FFI-aware The C entry point (`src/c/mdbl.c`) must pass the generated `fxBuildFFI` symbol when it creates the Moddable machine: ``` #include \int main(void) {Window \*w = window\_create();window\_stack\_push(w, true);moddable\_createMachine(&(ModdableCreationRecord){.recordSize = sizeof(ModdableCreationRecord),#ifdef PBL\_DEBUG.flags = kModdableCreationFlagDebug,#endif.fxBuildFFI = fxBuildFFI,});window\_destroy(w);return 0;} ``` ## 4. Call the functions from JavaScript All declared functions are available on the global `Natives` object: ``` console.log("Hello, FFI.");trace(`Natives.add(2, 3) = ${ Natives.add(2, 3) }\n`);trace(`Natives.addSquares(3, 4) = ${ Natives.addSquares(3, 4) }\n`);const bytes = new Uint8Array(5);Natives.hello("hello", bytes.buffer, bytes.length);trace(`bytes = ${ bytes }\n`); ``` Wrap calls in a `try`/`catch` to handle errors thrown across the boundary: ``` try {const result = Natives.add(2, 3);} catch (error) {console.log("FFI Error: " + error);} ``` ## Further reading The FFI mechanism comes from the Moddable SDK. See the [XS FFI documentation](https://www.moddable.com/documentation/xs/XS%20FFI) for the full set of supported types and behaviors, and the [`helloffi` example](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/helloffi) for a complete project. --- # Dictation Source: https://developer.repebble.com/guides/alloy/dictation/ The `Dictation` class lets your Alloy app capture speech from the user and receive it as text. It drives the system dictation UI and delivers the transcription to your code through callbacks. > **Platform Support** : Dictation is available on Emery (Pebble Time 2) and Gabbro (Pebble Round 2), and requires a connected phone with a working internet connection. ## Basic Usage Import the class and create an instance with `onReadable` and `onError` handlers, then call `start()` to begin listening: ``` import Dictation from "pebble/dictation";let dictation = new Dictation({onReadable() {console.log(`Transcription: ${this.read()}`);},onError(e) {console.log(`Dictation error: ${e}`);}});dictation.start(); ``` ## Methods | Method | Description | | --- | --- | | `start()` | Open the dictation UI and begin listening. | | `stop()` | Stop an in-progress dictation session. | | `read()` | Return the transcribed text. Call from `onReadable`. | | `configure(options)` | Change dictation behavior (see [Configuration](#configuration)). | | `close()` | Release the dictation instance when you're done with it. | ## Events | Event | Description | | --- | --- | | `onReadable()` | Called when a transcription is ready. Call `this.read()` to get the text. | | `onError(status)` | Called when dictation fails (for example, no connection or no speech detected). `status` is a numeric error code. | ## Configuration Pass a `byteLength` to the constructor to size the transcription buffer, and use `configure()` to control the dictation UI: ``` import Dictation from "pebble/dictation";let dictation = new Dictation({byteLength: 512,// maximum transcription size, in bytesonReadable() {console.log(`Transcription: ${this.read()}`);}});dictation.configure({confirm: false,// skip the confirmation screen after dictationerrorDialogs: true// let the system show its own error dialogs});dictation.start(); ``` | Option | Description | | --- | --- | | `confirm` | Whether to show the confirmation screen after dictation (default `true`). | | `errorDialogs` | Whether the system shows its own error UI on failure. | ## Continuous Dictation To keep listening after each transcription, start a new session from within `onReadable`. Use `setImmediate` so the current session finishes cleanly first: ``` import Dictation from "pebble/dictation";let dictation = new Dictation({onReadable() {console.log(`Transcription: ${this.read()}`);setImmediate(() =\> {console.log("Listening...");this.start();});},onError(e) {console.log(`Dictation error: ${e}`);}});console.log("Listening...");dictation.start(); ``` See the [`hellodictation` example](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellodictation) for a complete project. --- # Wakeups Source: https://developer.repebble.com/guides/alloy/wakeups/ Wakeups let your Alloy app schedule itself to launch at a future time, even if it isn't running. When the wakeup fires, the system launches your app and you can read the wakeup details to know why it was started. > **Platform Support** : Wakeups are available on Emery (Pebble Time 2) and Gabbro (Pebble Round 2). ## Scheduling a Wakeup Import the `WakeUp` class and call `schedule()` with a timestamp (in milliseconds, as returned by `Date.now()`), a numeric `cookie` of your choice, and whether the user should be notified if the wakeup is missed: ``` import WakeUp from "pebble/wakeup";// Fire 3 seconds from nowconst id = WakeUp.schedule(Date.now() + 3000, 12345678, false);console.log(`Scheduled WakeUp id ${id}`); ``` `schedule()` returns an `id` you can later use to query or cancel the wakeup. Persist the `id` (for example with `localStorage`) if you need it across launches. ## Methods | Method | Description | | --- | --- | | `WakeUp.schedule(time, cookie, notifyIfMissed)` | Schedule a wakeup at `time` (ms). `cookie` is a number passed back to your app. `notifyIfMissed` shows a missed-event notification if the wakeup couldn't fire. Returns an `id`. | | `WakeUp.cancel(id)` | Cancel a previously scheduled wakeup. | | `WakeUp.query(id)` | Return a `{ time, scheduled }` object describing the wakeup - `time` is the scheduled time in ms and `scheduled` is `true` if it is still pending - or a falsy value if it no longer exists. | ## Detecting a Wakeup Launch When your app is launched by a wakeup, `watch.wake` is set and contains the wakeup's `id` and `cookie`: ``` console.log(`Launch reason ${watch.launch.reason}, arguments ${watch.launch.arguments}`);if (watch.wake) {console.log(`Launched by wakeup id ${watch.wake.id}, cookie ${watch.wake.cookie}`);} ``` ## Receiving a Wakeup While Running If a wakeup fires while your app is already running, it is delivered as a `"wakeup"` event on `watch`: ``` watch.addEventListener("wakeup", wake =\> {console.log(`wakeup id ${wake.id} occurred while running`);WakeUp.cancel(wake.id);}); ``` ## Managing Stored Wakeups Because a wakeup `id` outlives a single launch, it's good practice to validate a stored `id` on startup and discard it if the wakeup is gone or stale: ``` const id = localStorage.getItem("wakeid");if (id) {const wakeup = WakeUp.query(id);if (!wakeup) {// The wakeup no longer existslocalStorage.removeItem("wakeid");} else if (wakeup.time \< Date.now()) {// The wakeup is in the past; cancel itWakeUp.cancel(id);localStorage.removeItem("wakeid");}} ``` See the [`hellowakeup` example](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellowakeup) for a complete project. --- # Vibration Source: https://developer.repebble.com/guides/alloy/vibration/ The `Vibes` class drives the watch's vibration motor, letting your Alloy app provide haptic feedback. It offers a set of standard pulses as well as custom vibration patterns. > **Platform Support** : Vibration is available on Emery (Pebble Time 2) and Gabbro (Pebble Round 2). ## Standard Pulses ``` import Vibes from "pebble/vibes";Vibes.shortPulse();Vibes.longPulse();Vibes.doublePulse(); ``` | Method | Description | | --- | --- | | `Vibes.shortPulse()` | A single short vibration. | | `Vibes.longPulse()` | A single long vibration. | | `Vibes.doublePulse()` | Two short vibrations. | ## Custom Patterns Pass an array of millisecond durations to `pattern()`. Durations alternate between _on_ and _off_, starting with _on_: ``` import Vibes from "pebble/vibes";// on 100ms, off 100ms, on 150ms, off 50ms, on 50ms, off 150ms, on 1000msVibes.pattern([100, 100, 150, 50, 50, 150, 1000]); ``` ## Cancelling Stop any ongoing vibration immediately: ``` Vibes.cancel(); ``` > **Note** : Vibration draws power and can be disruptive. Use it sparingly, and respect the user's Quiet Time settings for non-essential feedback. See the [`hellovibes` example](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellovibes) for a complete project. --- # Device Info and App Events Source: https://developer.repebble.com/guides/alloy/device-info/ Alloy exposes information about the watch and screen through the `watch`, `screen`, and `device` globals, delivers app lifecycle events through `watch`, and lets you control the backlight with `watch.light()`. > **Platform Support** : These APIs are available on Emery (Pebble Time 2) and Gabbro (Pebble Round 2). ## Watch Information The `watch` global describes the device and the user's preferences: ``` console.log(`watch.model ${watch.model}`);console.log(`firmware ${watch.firmwareVersion.major}.${watch.firmwareVersion.minor}.${watch.firmwareVersion.patch}`);console.log(`user prefers ${watch.hour12 ? "12" : "24"} hour display`); ``` | Property | Description | | --- | --- | | `watch.model` | The watch model identifier. | | `watch.firmwareVersion` | Object with `major`, `minor`, and `patch` numbers. | | `watch.hour12` | `true` if the user prefers a 12-hour clock, `false` for 24-hour. | | `watch.launch` | How the app was launched: `reason` and `arguments`. | | `watch.wake` | Present when launched by a wakeup; see [_Wakeups_](https://developer.repebble.com/guides/alloy/wakeups.md). | ## Screen Properties The `screen` global describes the display: ``` console.log(`screen ${screen.width} x ${screen.height}`);console.log(`round ${screen.round}, color ${screen.color}`); ``` | Property | Description | | --- | --- | | `screen.width` | Display width in pixels. | | `screen.height` | Display height in pixels. | | `screen.round` | `true` on round displays (Gabbro). | | `screen.color` | `true` on color displays. | ## Device Sensors The `device.sensor` object reports which sensors are available, so you can feature-detect before using one: ``` const hasTouch = device.sensor.Touch ? true : false;console.log(`touch available ${hasTouch}`); ``` ## App Lifecycle and Display Events Subscribe to app events on `watch` with `addEventListener`: ``` watch.addEventListener("willFocus", inFocus =\> {console.log(`willFocus with inFocus ${inFocus}`);});watch.addEventListener("didFocus", inFocus =\> {console.log(`didFocus with inFocus ${inFocus}`);});watch.addEventListener("resize", progress =\> {console.log(`screen resized to ${screen.width} x ${screen.height} at progress ${progress}`);}); ``` | Event | Description | | --- | --- | | `"willFocus"` | The app is about to gain or lose focus. The handler receives `inFocus`. | | `"didFocus"` | The app has gained or lost focus. The handler receives `inFocus`. | | `"resize"` | The drawable area is changing size (for example during an animation). The handler receives the animation `progress`. | The `watch` global also delivers `"secondchange"`/`"minutechange"` time events (see [_Watchfaces_](https://developer.repebble.com/guides/alloy/watchfaces.md)), a `"wakeup"` event (see [_Wakeups_](https://developer.repebble.com/guides/alloy/wakeups.md)), and `"health"` events when new health data arrives (see [_Health_](https://developer.repebble.com/guides/alloy/health.md)). ## Controlling the Backlight Use `watch.light()` to control the backlight: ``` watch.light(true);// force the backlight onwatch.light(false);// turn the backlight offwatch.light();// turn on temporarily, as if the user interacted ``` Calling `watch.light()` with no argument turns the backlight on for the standard auto-off interval, just like a button press or shake would. Passing a boolean forces the backlight on or off. > **Note** : Holding the backlight on draws significantly more power than letting it auto-off. Only force it on for short, deliberate interactions. See the [`helloinfo`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/helloinfo), [`helloappevents`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/helloappevents), and [`hellolight`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellolight) examples for complete projects. --- # Health Source: https://developer.repebble.com/guides/alloy/health/ The `Health` class gives Alloy apps access to Pebble Health: current and historical step counts, sleep, calories, distance, detected activities, and heart rate. It was added in SDK 4.33 and requires firmware 4.32 or later. > **Note** : Health data is only available while the user has Pebble Health enabled in the mobile app, and heart rate metrics additionally require a watch with a heart rate monitor (Pebble Time 2). Well-behaved apps check [availability](#checking-availability) before reading. All times passed to and returned by the Health API are JavaScript milliseconds — pass `Date` objects or millisecond timestamps. ## Reading Current Values `Health.metric.get()` returns the current (today's) value of a metric: ``` import Health from "pebble/health";console.log(`Steps today: ${Health.metric.get("step count")}`);console.log(`Heart rate: ${Health.metric.get("heart rate")} BPM`); ``` | Metric | Description | | --- | --- | | `"step count"` | Steps taken today | | `"active seconds"` | Seconds spent active today | | `"walked distance"` | Distance walked today, in meters | | `"sleep seconds"` | Seconds slept | | `"sleep restful seconds"` | Seconds of restful (deep) sleep | | `"resting calories"` | Resting kilocalories burned today | | `"active calories"` | Active kilocalories burned today | | `"heart rate"` | Filtered heart rate, in BPM | | `"heart rate raw"` | Raw, unfiltered heart rate, in BPM | ## Checking Availability Not every metric is available on every watch, for every time range, or with the user's current permissions. `Health.metric.accessible()` returns a bitmask of the flags in `Health.access`: ``` import Health from "pebble/health";const now = Date.now();const access = Health.metric.accessible({metric: "heart rate",start: now,end: now});if (access & Health.access.available) {console.log(`Heart rate: ${Health.metric.get("heart rate")} BPM`);} ``` | Flag | Meaning | | --- | --- | | `Health.access.available` | Values can be read for the requested range | | `Health.access.permission` | The user has granted this app access to health data | | `Health.access.supported` | The watch supports this metric | | `Health.access.data` | Samples exist in the requested range | ## Querying History `Health.metric.query()` reads sums and averages over a time range. With no `start` and `end`, it returns today's total: ``` import Health from "pebble/health";// Today's total (same as Health.metric.get for cumulative metrics)const today = Health.metric.query({ metric: "step count" });// Average daily steps over the past weekconst WEEK = 7 \* 24 \* 60 \* 60 \* 1000;const weeklyAverage = Health.metric.query({metric: "step count",start: Date.now() - WEEK,end: Date.now(),aggregation: "average",scope: "daily"});console.log(`Today: ${today}, daily average: ${weeklyAverage}`); ``` | Option | Values | Description | | --- | --- | --- | | `metric` | See [metric table](#reading-current-values) | The metric to query. Required. | | `start`, `end` | `Date` or milliseconds | Time range. Provide both or neither — with neither, the query covers today. | | `aggregation` | `"sum"` (default), `"average"`, `"min"`, `"max"` | How samples in the range are combined. `min`/`max` are only stored for recent history. | | `scope` | `"once"` (default), `"daily"`, `"weekly"`, `"weekday or weekend"` | Which days contribute: every day in the range at once, or averaged per-day, per-week, or by weekday/weekend. | ## Health Events Subscribe to the `"health"` event on the `watch` global to be told when new data arrives. The listener receives a string describing what changed: ``` watch.addEventListener("health", what =\> {if (what === "movement")console.log(`Steps now: ${Health.metric.get("step count")}`);}); ``` | Subtype | Meaning | | --- | --- | | `"significant"` | All data should be considered outdated (e.g. day rollover) — re-read everything | | `"movement"` | Step, distance or active-time values changed | | `"sleep"` | Sleep values changed | | `"metric"` | A [metric alert](#metric-alerts) threshold was crossed | | `"heart rate"` | A new heart rate reading is available | | `"heart rate variability"` | A new heart rate variability reading is available | ## Metric Alerts Instead of polling, register a threshold with `Health.metric.Alert`. When the metric crosses the threshold, a `"metric"` health event fires: ``` import Health from "pebble/health";const alert = new Health.metric.Alert({metric: "heart rate",threshold: 120});watch.addEventListener("health", what =\> {if (what === "metric")console.log("Heart rate above 120!");}); ``` Call `alert.close()` to cancel the alert. Alerts are also disposable, so a `using` declaration closes them automatically at the end of the scope: ``` using alert = new Health.metric.Alert({ metric: "step count", threshold: 10000 }); ``` ## Activities Pebble Health detects activities like sleeping, walking and running. `Health.activity.get()` returns a bitmask of what the user is doing right now, and `Health.activity.iterate()` walks through past (or upcoming scheduled) activity sessions: ``` import Health from "pebble/health";// What is the user doing right now?if (Health.activity.get() & Health.activity.run)console.log("Running!");// List today's walks and runsconst DAY = 24 \* 60 \* 60 \* 1000;Health.activity.iterate({activities: Health.activity.walk | Health.activity.run,start: Date.now() - DAY,end: Date.now(),callback(activity, start, end) {const minutes = Math.round((end - start) / 60000);const name = (activity === Health.activity.run) ? "Run" : "Walk";console.log(`${name}: ${minutes} minutes`);return true;// keep iterating; return false to stop}}); ``` The available activity constants are `Health.activity.sleep`, `restfulSleep`, `walk`, `run` and `openWorkout`. Use `Health.activity.accessible({activities, start, end})` to check access the same way as for metrics. ## Heart Rate Sampling By default the system samples heart rate automatically, more often during intense activity and less often at rest. An app that needs more frequent readings can request a shorter sample period (in milliseconds): ``` import Health from "pebble/health";// Request a heart rate sample every secondHealth.heartRate.samplePeriod = 1000;// ...and restore automatic sampling when doneHealth.heartRate.samplePeriod = 0; ``` The system treats the request as a suggestion, and shared use by other apps may result in a different effective rate. Always set the period back to `0` when your app no longer needs fast sampling. If the app exits without doing so, the request lingers for a while — `Health.heartRate.samplePeriodExpiration` reports how long (in milliseconds) it remains in effect. ## Minute-by-Minute History `Health.history.byMinute()` returns detailed per-minute records: ``` import Health from "pebble/health";const HOUR = 60 \* 60 \* 1000;const minutes = Health.history.byMinute({length: 60,start: Date.now() - HOUR,end: Date.now()});for (const record of minutes) {if (!record)continue;// minutes with no valid data are left undefinedconsole.log(`Steps: ${record.steps}, HR: ${record.heartRate}`);} ``` Each record has `steps`, `orientation`, `vmc` (vector magnitude counts, a measure of movement intensity), `light` (ambient light level) and `heartRate`. The returned array also carries `start` and `end` properties with the actual time range covered. ## Measurement System To display distances in the user's preferred units, ask which measurement system they chose for a metric in the mobile app: ``` const system = Health.displayMeasurementSystem("walked distance");// "metric", "imperial", or undefined if the user hasn't chosen ``` ## See Also - [_Device Info & App Lifecycle_](https://developer.repebble.com/guides/alloy/device-info.md) — the other `watch` events - [_Sensors and Input_](https://developer.repebble.com/guides/alloy/sensors-and-input.md) — live accelerometer and battery data --- # Alloy Reference Source: https://developer.repebble.com/guides/alloy/reference/ Alloy is built from three layers: the XS JavaScript engine and the Moddable SDK modules (Piu, Poco, the ECMA-419 sensor and network classes), the Pebble modules that expose the watch (`pebble/button`, `pebble/message`, `pebble/vibes` and so on), and the Pebble firmware underneath. Documentation is spread across the same three places. ## Where the Documentation Is - The guides in this section describe the Pebble modules and how to use Piu and Poco on the watch. Start with [_Getting Started with Alloy_](https://developer.repebble.com/guides/alloy/getting-started.md). - The Moddable SDK documentation at [moddable.com/documentation](https://www.moddable.com/documentation/) covers Piu, Poco (under Commodetto), the XS engine, the `manifest.json` format and the ECMA-419 classes. Alloy ships Moddable SDK 8.3; features added later are not available. - The TypeScript declarations in the SDK are the most precise reference for the Pebble modules. They are at `toolchain/moddable/typings/pebble/` inside the SDK directory (`~/Library/Application Support/Pebble SDK/SDKs/current` on macOS, `~/.pebble-sdk/SDKs/current` on Linux), one `.d.ts` file per module. - The [pebble-examples](https://github.com/Moddable-OpenSource/pebble-examples) repository has a small example for each API. Its README lists the JavaScript features that are left out of the XS build and what the "dead strip" error means. ## System Fonts Poco and Piu load the watch's built-in fonts by family name and size. Poco takes them directly: ``` const font = new render.Font("Gothic-Bold", 24); ``` Piu uses a CSS-style string. The weight becomes part of the family name, so `"bold 24px Gothic"` selects `Gothic-Bold` at 24 and `"black 30px Bitham"` selects `Bitham-Black` at 30. A string with no weight selects the `-Regular` variant. Both look the name and size up in a fixed table in the firmware. The size must match exactly; there is no scaling. If nothing matches, the constructor throws `font not found`. The table maps to the C `FONT_KEY_*` constants as follows: | Alloy family | Sizes | C constant | | --- | --- | --- | | `Gothic-Regular` | 9, 14, 18, 24, 28 | `FONT_KEY_GOTHIC_09` ... `FONT_KEY_GOTHIC_28` | | `Gothic-Bold` | 14, 18, 24, 28 | `FONT_KEY_GOTHIC_14_BOLD` ... `FONT_KEY_GOTHIC_28_BOLD` | | `Bitham-Black` | 30 | `FONT_KEY_BITHAM_30_BLACK` | | `Bitham-Bold` | 42 | `FONT_KEY_BITHAM_42_BOLD` | | `Bitham-Light` | 42, 34, 18 | `FONT_KEY_BITHAM_42_LIGHT`, `FONT_KEY_BITHAM_34_LIGHT_SUBSET`, `FONT_KEY_BITHAM_18_LIGHT_SUBSET` | | `Bitham-Medium` | 42, 34 | `FONT_KEY_BITHAM_42_MEDIUM_NUMBERS`, `FONT_KEY_BITHAM_34_MEDIUM_NUMBERS` | | `Roboto-Condensed` | 21 | `FONT_KEY_ROBOTO_CONDENSED_21` | | `Roboto-Bold` | 49 | `FONT_KEY_ROBOTO_BOLD_SUBSET_49` | | `DroidSerif-Bold` | 28 | `FONT_KEY_DROID_SERIF_28_BOLD` | | `Leco-Bold` | 20, 26, 32, 36, 38 | `FONT_KEY_LECO_20_BOLD_NUMBERS`, `FONT_KEY_LECO_26_BOLD_NUMBERS_AM_PM`, `FONT_KEY_LECO_32_BOLD_NUMBERS`, `FONT_KEY_LECO_36_BOLD_NUMBERS`, `FONT_KEY_LECO_38_BOLD_NUMBERS` | | `Leco-Regular` | 42 | `FONT_KEY_LECO_42_NUMBERS` | | `Leco-Light` | 28 | `FONT_KEY_LECO_28_LIGHT_NUMBERS` | The firmware table also lists `Gothic-Regular` and `Gothic-Bold` at size 36, which have no `FONT_KEY_*` constant in the C SDK. The Leco fonts and the `SUBSET` Bitham fonts contain digits and a few symbols only; see [_System Fonts_](https://developer.repebble.com/guides/app-resources/system-fonts.md) for the glyph sets. There is no Alloy name for `FONT_KEY_LECO_60_NUMBERS_AM_PM` or `FONT_KEY_LECO_60_BOLD_NUMBERS_AM_PM`. Fonts from the project's resources are loaded by resource ID instead of by name, with the same constructor: `new render.Font(3)`. ## Resource IDs `Poco.PebbleBitmap(id)`, `Poco.PebbleDrawCommandImage(id)` and `Poco.PebbleDrawCommandSequence(id)` take a number: the resource ID the build assigned. IDs start at 1 and follow the order of the `resources.media` array in `package.json`, counting only the resources built for the current platform: ``` "resources": {"media": [{ "type": "png", "name": "BACKGROUND", "file": "images/background.png" },{ "type": "raw", "name": "HOURS", "file": "pdc/hours.pdc" }]} ``` With this manifest `new Poco.PebbleBitmap(1)` loads `background.png` and `new Poco.PebbleDrawCommandImage(2)` loads `hours.pdc`. Inserting a resource in the middle of the list shifts every ID after it, and so do these cases: - A resource with `targetPlatforms` that excludes the current platform is skipped on that platform, so the IDs after it differ between platforms. - When `publishedMedia` is present, the build reserves ID 1 for the timeline lookup table and the first media resource becomes ID 2. - The compiled JavaScript is added as a resource named `MOD` after the project's media, and resources from Pebble Packages come after it. The build writes the final assignment to `build//src/resource_ids.auto.h` as `RESOURCE_ID_` defines. Check that file after changing `package.json` rather than counting by hand. The names in `package.json` are not available to the JavaScript at runtime. To avoid hard-coded numbers, keep one module that defines the IDs in the same order as the manifest and import it everywhere: ``` // resources.jsexport default {BACKGROUND: 1,HOURS: 2,}; ``` Piu's `Texture` and `SVGImage` classes accept either a number, which is a Pebble resource ID as above, or a string path, which is a Moddable resource listed in the `resources` section of `src/embeddedjs/manifest.json`. ## Modules and manifest.json Every JavaScript file that is imported must be listed in `src/embeddedjs/manifest.json`. The template contains: ``` {"include": ["$(MODDABLE)/examples/manifest\_mod.json","$(MODDABLE)/examples/manifest\_typings.json"],"modules": {"\*": "./main"}} ``` Under `"*"` each file becomes a module named after its file name, so `"*": ["./main", "./clock"]` makes `import Clock from "clock"` work. A key other than `"*"` gives the module a different specifier, for example `"piu/Controller": "./modules/piuController"`, and a path ending in `/*` includes every file in a directory. A `"platforms"` block selects modules per platform, with the keys `"pebble/emery"` and `"pebble/gabbro"`. Each module costs memory at startup. Prefer a few larger modules over many small ones. ## The connected Event `watch.addEventListener("connected", handler)` fires when the connection to the phone changes, in either direction. It does not fire when the listener is added, and the handler receives no argument. Read `watch.connected.app` and `watch.connected.pebblekit` for the current state, once at startup and again inside the handler: ``` function update() {console.log(`phone connected: ${watch.connected.app}`);}watch.addEventListener("connected", update);update(); ``` The firmware can take 15 to 30 seconds to report a dropped connection. ## Memory The JavaScript engine runs inside the app's RAM. By default the template's `src/c/mdbl.c` passes `NULL` to `moddable_createMachine()`, which gives the engine a 32 kB static pool that it divides between the slot heap (objects and values), the chunk heap (strings and buffers) and the stack, growing as needed. When the engine cannot allocate, the app exits and the watch shows a dialog titled _Alloy: Fatal Error_ with the engine's message, most often `memory full`. The same message appears in the app logs as `fxAbort memory full`. Other messages in that dialog are `JavaScript stack overflow`, `not enough keys` and `dead strip`, the last one meaning the code used a JavaScript feature that is left out of the XS build. Apps that need more than the default pass a `ModdableCreationRecord`: ``` #include \int main(void) {Window \*w = window\_create();window\_stack\_push(w, true);moddable\_createMachine(&(ModdableCreationRecord){.recordSize = sizeof(ModdableCreationRecord),.stack = 5120,.slot = 31744,.chunk = 19456});window\_destroy(w);return 0;} ``` All three sizes are in bytes, and either all three or none must be set. When their total exceeds 32 kB the engine uses exactly these sizes and does not grow further, so size them for the app's peak use. The watch's app RAM is 128 kB on both emery and gabbro, shared with the Pebble UI library and the native part of the app. ## TypeScript Name the source files `.ts` instead of `.js` and list them in `manifest.json` as usual. The build runs `tsc`, which must be installed and on the `PATH`; the SDK does not include it. The `manifest_typings.json` include in the template adds the Pebble and Moddable declarations to the compiler's configuration, so no `tsconfig.json` is needed. The [hellotypescript](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellotypescript) example is the smallest working project. ## Examples and SDK Versions The examples in pebble-examples target SDK 4.33.1, with these exceptions: - `piu/apps/words` and `piu/watchfaces/wallpaper` import `embedded:storage/key-value` as a module. The released firmware rejects this import, so both fail to load. Use `device.keyValue.open()` as shown in [_Storage_](https://developer.repebble.com/guides/alloy/storage.md). - `piu/watchfaces/cupertino`, `helsinki`, `redmond` and `zurich` list `flint` in `targetPlatforms`. Alloy does not run on Pebble 2 Duo, so build and install them for emery or gabbro only. - `piu/watchfaces/wallpaper` lists `gabbro` but its `manifest.json` only has modules for emery, so the gabbro build fails. It also needs a local WebSocket server to download images from. --- # Piu UI Framework Source: https://developer.repebble.com/guides/alloy/piu-guide/ Piu is a declarative UI framework for building user interfaces in Alloy apps. It provides a component-based architecture with automatic layout, styling, and animation support. > **Note** : All code in this guide runs on the watch in `src/embeddedjs/main.js`. ## Getting Started with Piu For apps using advanced Piu features (behaviors, templates, containers), include the Piu runtime: ``` import {} from "piu/MC"; ``` A basic Piu application creates an `Application` object that fills the screen: ``` const application = new Application(null, {skin: new Skin({ fill: "white" })}); ``` ## Core Concepts ### Content Objects Content objects are the basic building blocks of Piu UIs. The main types are: | Type | Description | | --- | --- | | `Content` | Basic rectangular element (base class for most UI objects) | | `Container` | Holds other content objects | | `Column` | Vertical layout container | | `Row` | Horizontal layout container | | `Label` | Single line of text | | `Text` | Multi-line formatted text | | `Application` | Root container (one per app) | ### Skins Skins define the visual appearance of content objects: ``` // Solid color skinconst whiteSkin = new Skin({ fill: "white" });// Skin with stroke/borderconst borderedSkin = new Skin({fill: "transparent",stroke: "black",borders: { left: 2, right: 2, top: 2, bottom: 2 }});// Texture-based skin (for images)const ballTexture = new Texture("balls.png");const ballSkin = new Skin({texture: ballTexture,x: 0, y: 0,width: 30, height: 30,variants: 30// for sprite sheets}); ``` ### Styles Styles define text appearance. Styles cascade, similar to CSS - you can create a base style and let child content objects inherit from it, making it easy to maintain a consistent look: ``` const textStyle = new Style({font: "OpenSans-Regular-15",color: "black",horizontal: "center",// "left", "right", "center", "justify"left: 10, right: 10,top: 15, bottom: 15});// Style with state colors (normal, active)const buttonStyle = new Style({font: "bold 18px Gothic",color: ["black", "gray"]// [normal, active]}); ``` ## Positioning and Layout Content objects are positioned using constraints: ``` // Absolute positioningContent($, { left: 20, top: 20, width: 80, height: 80 });// Anchored to edgesContent($, { right: 20, bottom: 20, width: 80, height: 80 });// Fill available spaceContent($, { left: 0, right: 0, top: 0, bottom: 0 });// Centered (no position constraints)Content($, { width: 80, height: 80 }); ``` ## Behaviors Behaviors add interactivity and logic to content objects. They are essential for handling events and updating the UI: ``` class BallBehavior extends Behavior {onCreate(ball, delta) {// delta is the data passed to the contentthis.dx = delta;this.dy = delta;}onDisplaying(ball) {// Store initial position and boundsthis.x = ball.x;this.y = ball.y;this.width = ball.container.width - ball.width;this.height = ball.container.height - ball.height;ball.start();// Start time-based updates}onTimeChanged(ball) {// Move the ballball.moveBy(this.dx, this.dy);// Update position and check boundsthis.x += this.dx;this.y += this.dy;// Bounce off wallsif (this.x \< 0 || this.x \> this.width) this.dx = -this.dx;if (this.y \< 0 || this.y \> this.height) this.dy = -this.dy;}}// Attach behavior to content - the first argument (6) is passed to onCreateContent(6, {left: 0, top: 0,width: 30, height: 30,skin: ballSkin,variant: 0,Behavior: BallBehavior}); ``` ### Common Behavior Events | Event | Description | | --- | --- | | `onCreate(content, data)` | Content created with data | | `onDisplaying(content)` | Content added to display | | `onTimeChanged(content)` | Animation frame (after `start()`) | | `onFinished(content)` | Animation/duration completed | Pebble button presses are handled using the `Button` class rather than Behavior events. See the [Sensors and Input](https://developer.repebble.com/guides/alloy/sensors-and-input.md) guide for details on button handling. Piu also uses the `Button` class internally for button input. ### Touch Events On platforms with a touchscreen (Emery, Gabbro), Piu delivers touch events to Behaviors on the content under the user's finger. Enable touch input by setting `touchCount` on the Application when it is constructed — this is the number of simultaneous contacts Piu will track (Pebble hardware supports `1`): ``` new MyApplication({}, {displayListLength: 2048,touchCount: 1,pixels: screen.width \* 4}); ``` With `touchCount: 0` (the default) Piu does not poll the touch sensor and no touch events fire. Once enabled, Behaviors receive `onTouchBegan` and `onTouchEnded` events. Hit-testing is handled by Piu — the events are delivered to the innermost active content under the touch. Set `active: true` on a container to make it receive touches: ``` class TTTSquareBehavior {onTouchBegan(container) {container.state = 1;// visual press feedback via skin variants}onTouchEnded(container) {container.state = 0;container.bubble("onTap", container);}}const TTTSquare = Container.template($ =\> ({width: 67, height: 67, skin: squareSkin, active: true,Behavior: TTTSquareBehavior,contents: [/\* ... \*/]})); ``` Use `container.bubble(event, ...args)` from inside `onTouchEnded` to forward higher-level events (tap, long-press, etc.) up the content tree, where an Application or container Behavior can handle them. | Event | Description | | --- | --- | | `onTouchBegan(content, id, x, y, ticks)` | Finger pressed on the content | | `onTouchEnded(content, id, x, y, ticks)` | Finger lifted from the content | For free-form touch input outside of Piu's hit-testing (for example, dragging in a Poco-rendered app), use the lower-level [`device.sensor.Touch`](https://developer.repebble.com/guides/alloy/sensors-and-input.md#touch) driver instead. ## Templates Templates create reusable component definitions. They are most useful when you need to create multiple instances of the same component: ``` // Define a templateconst Square = Content.template($ =\> ({width: 80, height: 80,skin: new Skin({ fill: $ })// $ is the data passed to template}));// Use the templateconst redSquare = new Square("red", { left: 20, top: 20 });const blueSquare = new Square("blue", { right: 20, bottom: 20 });application.add(redSquare);application.add(blueSquare); ``` > **Note** : For one-off content, create instances directly rather than defining a template first. Templates use more code and RAM than direct instantiation when you only need a single instance. ## Building an Application Here's a complete example with multiple components: ``` const backgroundSkin = new Skin({ fill: "silver" });const headerSkin = new Skin({ fill: "white" });const headerStyle = new Style({ font: "bold 18px Gothic", color: "black" });class HeaderBehavior extends Behavior {onDisplaying(label) {label.string = "My App";}}const application = new Application(null, {skin: backgroundSkin,contents: [new Column(null, {top: 0, bottom: 0, left: 0, right: 0,contents: [new Label(null, {top: 0, height: 30, left: 0, right: 0,skin: headerSkin,style: headerStyle,Behavior: HeaderBehavior}),new Content(null, {top: 10, bottom: 10, left: 10, right: 10,skin: new Skin({ fill: "gray" })})]})]}); ``` ## Anchors and References Use anchors to reference content objects from behaviors: ``` const MyApp = Application.template($ =\> ({contents: [Label($, {anchor: "TITLE",// Creates $.TITLE referencestring: "Hello"})],Behavior: class extends Behavior {onCreate(app, data) {this.data = data;}updateTitle(app, newTitle) {this.data.TITLE.string = newTitle;}}})); ``` ## Animations with Timeline The Timeline class creates smooth animations: ``` import Timeline from "piu/Timeline";class AnimatedBehavior extends Behavior {onDisplaying(content) {let timeline = new Timeline();// Animate 'y' property over 750mstimeline.to(content, { y: 100 }, 750, Math.quadEaseOut, 0);content.duration = timeline.duration;timeline.seekTo(0);content.time = 0;content.start();}onTimeChanged(content) {this.timeline.seekTo(content.time);}} ``` ### Easing Functions Available easing functions on the `Math` object: - `Math.backEaseIn` / `Math.backEaseOut` - `Math.bounceEaseIn` / `Math.bounceEaseOut` - `Math.circularEaseIn` / `Math.circularEaseOut` - `Math.cubicEaseIn` / `Math.cubicEaseOut` - `Math.elasticEaseIn` / `Math.elasticEaseOut` - `Math.exponentialEaseIn` / `Math.exponentialEaseOut` - `Math.quadEaseIn` / `Math.quadEaseOut` - `Math.quartEaseIn` / `Math.quartEaseOut` - `Math.quintEaseIn` / `Math.quintEaseOut` - `Math.sineEaseIn` / `Math.sineEaseOut` For more animation techniques including chaining and looping, see the [Animations](https://developer.repebble.com/guides/alloy/animations.md) guide. ## Displaying Images Load and display images using Texture and Skin: ``` // Load a texture from resourcesconst logoTexture = new Texture("logo.png");// Create a skin from the textureconst logoSkin = new Skin({texture: logoTexture,x: 0, y: 0,width: 64, height: 64});// Display the imageapplication.add(new Content(null, {skin: logoSkin})); ``` ## Text Display ### Single Line (Label) ``` application.add(new Label(null, {left: 0, right: 0, top: 50,style: new Style({ font: "bold 24px Gothic", color: "black" }),string: "Hello, World!"})); ``` ### Multi-line (Text) ``` application.add(new Text(null, {left: 10, right: 10, top: 10,style: textStyle,blocks: [{ spans: "First paragraph of text." },{ spans: ["Second paragraph with ",{ style: boldStyle, spans: "bold text" }," inline."]}]})); ``` ## Using Pebble Fonts Access built-in Pebble fonts with CSS-like syntax: ``` // Format: "[style] [size]px [family]"const headerStyle = new Style({font: "bold 14px Gothic",color: "black"});const titleStyle = new Style({font: "black 30px Bitham",color: "blue"});const bodyStyle = new Style({font: "24px Leco",color: "white"}); ``` Available Pebble fonts include: - `Gothic` - Regular and Bold weights - `Bitham` - Black and Bold weights - `Roboto` - Condensed weight - `Leco` - Regular weight (great for numbers) - `Droid` - Serif style The available sizes are limited for each font family. See the [System Fonts](https://developer.repebble.com/guides/app-resources/system-fonts.md) reference for the full table of all fonts, styles, and sizes. ## Loading Bitmaps from Resources Load images from Pebble resources by ID: ``` // Load texture by resource IDconst backgroundTexture = new Texture(2);// Resource ID 2// Create a tiled background skinconst backgroundSkin = new Skin({texture: backgroundTexture,x: 0, y: 0,width: 40, height: 40,tiles: { left: 0, right: 0, top: 0, bottom: 0 }});// Load and display an imageconst iconTexture = new Texture(3);application.add(new Content(null, {skin: new Skin({texture: iconTexture,width: iconTexture.width,height: iconTexture.height})})); ``` ## PDC (Pebble Draw Command) Images PDC images are vector graphics converted from SVG. In Piu, use the `SVGImage` class to display and transform PDC files. Place `.pdc` files in your `src/embeddedjs/assets/` directory and reference them by filename: ``` const application = new Application(null, {skin: new Skin({ fill: "gray" }),contents: [SVGImage(null, { path: "icon.pdc" })]}); ``` The `path` property specifies the `.pdc` filename from your assets directory. ### SVGImage Transforms SVGImage supports rotation, scaling, and translation through built-in properties: | Property | Description | | --- | --- | | `r` | Rotation angle in radians | | `s` | Uniform scale factor | | `sx`, `sy` | Non-uniform scale (horizontal, vertical) | | `tx`, `ty` | Translation offset (x, y) | | `cx`, `cy` | Center of rotation | In the [gravity example](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/piu/apps/gravity), a PDC star is continuously rotated and moved based on accelerometer readings. The rotation portion of its Behavior updates the `r` property each frame: ``` let degree = this.degree + 1;if (degree \> 360)degree -= 360;this.degree = degree;star.r = (this.degree / 180) \* Math.PI; ``` Unlike Poco's `clone().rotate()` approach, Piu transforms the SVGImage in place through its properties - no cloning needed. ### Animated SVGImage with Timeline Use `duration`, `fraction`, and `start()` to animate SVGImage transforms with easing: ``` const graySkin = new Skin({ fill: "gray" });class ImageBehavior extends Behavior {onAnimate(image) {image.duration = 1500;image.time = 0;image.start();}onDisplaying(image) {this.delta = (image.container.width \>\> 1) + image.width;image.tx = this.delta;}onFinished(image) {image.r = 0;image.tx = this.delta;let next = image.next ?? image.container.first;next.delegate("onAnimate");}onTimeChanged(image) {let fraction = image.fraction;if (fraction \< 0.4) {fraction = Math.quadEaseOut(0.4 - fraction);image.r = fraction \* Math.PI;image.tx = this.delta \* fraction;}else if (fraction \> 0.6) {fraction = Math.quadEaseOut(0.6 - fraction);image.r = fraction \* Math.PI;image.tx = this.delta \* fraction;}else {image.r = 0;image.tx = 0;}}}class TestApplicationBehavior {onDisplaying(application) {application.duration = 1000;application.first.delegate("onAnimate");}}const TestApplication = Application.template($ =\> ({Behavior: TestApplicationBehavior, skin: graySkin,contents: [SVGImage(6, { path: "Pebble\_80x80\_Incoming\_call\_centered.pdc", Behavior: ImageBehavior }),SVGImage(5, { path: "Pebble\_80x80\_Scheduled\_event.pdc", Behavior: ImageBehavior }),SVGImage(4, { path: "Pebble\_80x80\_Outgoing\_call.pdc", Behavior: ImageBehavior }),]}));export default new TestApplication({}, {displayListLength: 4096, touchCount: 0, pixels: screen.width \* 4,}); ``` The `fraction` property returns a value from 0 to 1 representing progress through the `duration`. Combine it with easing functions from `Math` for smooth motion. ### PDC Sequences PDC sequences are animated vector graphics containing multiple frames. Use `SVGImage` with a sequence `.pdc` file and control playback through Behavior events: ``` const graySkin = new Skin({ fill: "gray" });class ClockBehavior extends Behavior {onDisplaying(image) {image.start();}onFinished(image) {image.time = 0;image.start();}onTimeChanged(image) {let fraction = image.fraction;if (fraction \< 0.5)fraction = 1 + fraction;elsefraction = 2 - fraction;image.sx = image.sy = fraction;}}class TestApplicationBehavior {}const TestApplication = Application.template($ =\> ({Behavior: TestApplicationBehavior, skin: graySkin,contents: [SVGImage($, { bottom: 20, path: "clock\_sequence.pdc", Behavior: ClockBehavior })]}));export default new TestApplication({}, {displayListLength: 2048, touchCount: 0, pixels: screen.width \* 4,}); ``` ### Clock Hands with SVGImage A common pattern is using SVGImage for watchface clock hands. Each hand is a separate PDC file, rotated based on the current time: ``` const scale = Math.min(screen.width, screen.height) / 240;class FaceHandBehavior {onFractionChanged(content, fraction) {const angle = ((-fraction \* 2) - 1) \* Math.PI;content.r = angle;}onClockResized(content) {const container = content.container;content.x = (container.width \>\> 1) - content.cx;content.y = (container.height \>\> 1) - content.cy;}}class FaceHoursBehavior extends FaceHandBehavior {onDisplaying(content) {content.cx = 7;content.cy = 14;content.s = scale;this.onClockResized(content);}onClockChanged(content, clock) {this.onFractionChanged(content,(clock.hours % 12 + clock.minutes / 60) / 12);}}class FaceMinutesBehavior extends FaceHandBehavior {onDisplaying(content) {content.cx = 5;content.cy = 20;content.s = scale;this.onClockResized(content);}onClockChanged(content, clock) {this.onFractionChanged(content, clock.minutes / 60);}} ``` The `cx` and `cy` properties set the rotation center, which should match the pivot point of the clock hand in the original SVG. The `scale` factor adapts the hand size to the screen. In the application template, layer the hands from bottom to top: ``` class FaceApplicationBehavior {onDisplaying(application) {watch.addEventListener("secondchange", (clock) =\> {const date = clock.date;application.distribute("onClockChanged", {date,hours: date.getHours(),minutes: date.getMinutes(),seconds: date.getSeconds(),});});}onResize(application) {application.distribute("onClockResized");}}const FaceApplication = Application.template($ =\> ({Behavior: FaceApplicationBehavior,contents: [Content($, { skin: new Skin({ texture: new Texture("dial.png"),width: screen.width, height: screen.height }) }),SVGImage($, { left: 0, width: 14, top: 0, height: 79,path: "hours.pdc", Behavior: FaceHoursBehavior }),SVGImage($, { left: 0, width: 10, top: 0, height: 100,path: "minutes.pdc", Behavior: FaceMinutesBehavior }),]}));export default new FaceApplication(null, {displayListLength: 2048, touchCount: 0, pixels: screen.width \* 4,}); ``` Use `distribute()` to send clock updates to all hands simultaneously. ## Complete Example: Bouncing Balls Here's a complete animated app with multiple bouncing balls: ``` import {} from "piu/MC";const backgroundSkin = new Skin({ fill: "silver" });const ballTexture = new Texture("balls.png");const ballSkin = new Skin({texture: ballTexture,x: 0, y: 0,width: 30, height: 30,variants: 30// sprite sheet with 30px variants});class BallBehavior extends Behavior {onCreate(ball, delta) {this.dx = delta;this.dy = delta;}onDisplaying(ball) {this.x = ball.x;this.y = ball.y;this.width = ball.container.width - ball.width;this.height = ball.container.height - ball.height;ball.start();}onTimeChanged(ball) {ball.moveBy(this.dx, this.dy);this.x += this.dx;this.y += this.dy;if (this.x \< 0 || this.x \> this.width) this.dx = -this.dx;if (this.y \< 0 || this.y \> this.height) this.dy = -this.dy;}}const BallApplication = Application.template($ =\> ({skin: backgroundSkin,contents: [Content(6, { left: 0, top: 0, skin: ballSkin, variant: 0, Behavior: BallBehavior }),Content(5, { right: 0, top: 0, skin: ballSkin, variant: 1, Behavior: BallBehavior }),Content(4, { right: 0, bottom: 0, skin: ballSkin, variant: 2, Behavior: BallBehavior }),Content(3, { left: 0, bottom: 0, skin: ballSkin, variant: 3, Behavior: BallBehavior }),]}));export default new BallApplication(null, { pixels: screen.width \* 4 }); ``` ## Related Guides - [Animations](https://developer.repebble.com/guides/alloy/animations.md) - Advanced animation techniques with Timeline and easing functions - [Port (Custom Drawing)](https://developer.repebble.com/guides/alloy/port-drawing.md) - Combine Piu layout with custom Poco-style drawing - [Poco Graphics](https://developer.repebble.com/guides/alloy/poco-guide.md) - Low-level graphics alternative to Piu - [Converting SVG to PDC](https://developer.repebble.com/guides/app-resources/converting-svg-to-pdc.md) - How to create PDC files from SVG - [Vector Graphics](https://developer.repebble.com/guides/graphics-and-animations/vector-graphics.md) - Overview of vector graphics on Pebble ## Examples The [Pebble Examples](https://github.com/Moddable-OpenSource/pebble-examples) repository includes several Piu examples: - [`hellopiu-balls`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellopiu-balls) - animated bouncing balls with Behavior and texture variants - [`hellopiu-coloredsquares`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellopiu-coloredsquares) - basic layout with colored skins - [`hellopiu-gbitmap`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellopiu-gbitmap) - displaying Pebble GBitmap PNG images - [`hellopiu-jsicon`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellopiu-jsicon) - displaying a Moddable SDK bitmap from a PNG resource - [`hellopiu-text`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellopiu-text) - dynamic text layout with different fonts and alignment - [`hellopiu-pebbletext`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellopiu-pebbletext) - text rendering with Pebble's built-in fonts - [`piu/apps/pdc-images`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/piu/apps/pdc-images) - moving and rotating PDC (SVG) images - [`piu/apps/pdc-sequence`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/piu/apps/pdc-sequence) - playback of animated PDC sequences - [`piu/apps/compass`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/piu/apps/compass) - compass visualization with rotating PDC image - [`piu/apps/gravity`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/piu/apps/gravity) - accelerometer-driven PDC star animation - [`piu/apps/tic-tac-toe`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/piu/apps/tic-tac-toe) - touch input via `onTouchBegan`/`onTouchEnded` Behavior events - [`piu/watchfaces/minato`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/piu/watchfaces/minato) - watchface with PDC clock hands --- # Poco Graphics Source: https://developer.repebble.com/guides/alloy/poco-guide/ Poco is a low-level graphics rendering framework that gives you direct control over pixel drawing. It's ideal for custom graphics, games, and watchfaces where you need precise control over rendering. > **Note** : All code in this guide runs on the watch in `src/embeddedjs/main.js`. ## Getting Started Import Poco and create a renderer instance: ``` import Poco from "commodetto/Poco";const render = new Poco(screen); ``` The `screen` global provides access to the Pebble display. ## Basic Drawing All drawing happens between `begin()` and `end()` calls: ``` render.begin();// Drawing commands hererender.end(); ``` You can also specify a clip region to update only part of the screen: ``` render.begin(x, y, width, height);// Only draws within the specified rectanglerender.end(); ``` ## Colors Create colors using `makeColor()` with RGB values (0-255): ``` const black = render.makeColor(0, 0, 0);const white = render.makeColor(255, 255, 255);const red = render.makeColor(255, 0, 0);const gray = render.makeColor(128, 128, 128);const lightBlue = render.makeColor(0x80, 0xc0, 0xff); ``` ## Drawing Shapes ### Rectangles ``` // Filled rectanglerender.fillRectangle(color, x, y, width, height);// Example: fill the entire screen whiterender.fillRectangle(white, 0, 0, render.width, render.height); ``` ### Rounded Rectangles ``` // Rounded rectangle with radius// corners is a bitmask: 0b0001=TL, 0b0010=TR, 0b0100=BL, 0b1000=BRrender.drawRoundRect(x, y, width, height, color, radius, corners);// All corners roundedrender.drawRoundRect(10, 10, 100, 50, black, 8);// Only top corners roundedrender.drawRoundRect(10, 10, 100, 50, black, 8, 0b0011);// Frame (outline) of rounded rectanglerender.frameRoundRect(x, y, width, height, color, radius); ``` ### Lines ``` // Draw a line with specified thicknessrender.drawLine(x1, y1, x2, y2, color, thickness);// Example: diagonal linesrender.drawLine(0, 0, render.width, render.height, gray, 4);render.drawLine(0, render.height, render.width, 0, gray, 4); ``` ### Circles ``` // Draw a circle or arc// startAngle and endAngle are in degreesrender.drawCircle(color, centerX, centerY, radius, startAngle, endAngle);// Full circlerender.drawCircle(black, 90, 90, 30, 0, 360);// Arc (quarter circle)render.drawCircle(blue, 90, 90, 30, 0, 90); ``` ## Text Rendering ### Using Pebble Built-in Fonts ``` import Poco from "commodetto/Poco";const render = new Poco(screen);const font = new render.Font("Bitham-Black", 30);const black = render.makeColor(0, 0, 0);render.begin();render.drawText("Hello!", font, black, 10, 50);render.end(); ``` Available Pebble fonts: - `Gothic-Regular` (various sizes) - `Gothic-Bold` - `Bitham-Black` - `Bitham-Bold` - `Roboto-Condensed` - `Leco-Regular` - `Droid-Serif` The available sizes are limited for each font family. See the [System Fonts](https://developer.repebble.com/guides/app-resources/system-fonts.md) reference for the full table of all fonts, styles, and sizes. ### Centering Text Use `getTextWidth()` to measure text for centering: ``` const msg = "Hello!";const width = render.getTextWidth(msg, font);const x = (render.width - width) / 2;const y = (render.height - font.height) / 2;render.drawText(msg, font, black, x, y); ``` ### Using Custom Fonts (Moddable SDK Fonts) For custom fonts bundled with your app, use `parseBMF` and `parseRLE`: ``` import Poco from "commodetto/Poco";import parseBMF from "commodetto/parseBMF";import parseRLE from "commodetto/parseRLE";const render = new Poco(screen);const black = render.makeColor(0, 0, 0);// Load custom font from resourcesfunction getFont(name, size) {const font = parseBMF(new Resource(`${name}-${size}.fnt`));font.bitmap = parseRLE(new Resource(`${name}-${size}-alpha.bm4`));return font;}const openSans = getFont("OpenSans-Regular", 21);const noto = getFont("NotoSansJP-Regular", 24);render.begin();render.drawText("Hello!", openSans, black, 10, 10);render.drawText("新宿出口", noto, black, 10, 40);// Japanese textrender.end(); ``` Custom fonts allow Unicode support and unique typography not available in built-in Pebble fonts. ## Drawing Images ### Bitmaps Load bitmaps from resources using `PebbleBitmap`: ``` import Poco from "commodetto/Poco";const render = new Poco(screen);// Load bitmap by resource IDconst bitmap = new Poco.PebbleBitmap(2);render.begin();// Center the bitmapconst x = (render.width - bitmap.width) / 2;const y = (render.height - bitmap.height) / 2;render.drawBitmap(bitmap, x, y);render.end(); ``` Bitmaps have `width` and `height` properties for positioning. ### PDC (Pebble Draw Command) Images PDC images are vector graphics converted from SVG: ``` import Poco from "commodetto/Poco";const render = new Poco(screen);// Load PDC image by resource IDconst pdc = new Poco.PebbleDrawCommandImage(2);render.begin();// Center the PDC imageconst x = (render.width - pdc.width) / 2;const y = (render.height - pdc.height) / 2;render.drawDCI(pdc, x, y);render.end(); ``` ### PDC Sequences (Animations) PDC sequences are animated vector graphics. Use the `time` property to advance through frames: ``` import Poco from "commodetto/Poco";const render = new Poco(screen);const gray = render.makeColor(128, 128, 128);const dcs = new Poco.PebbleDrawCommandSequence(2);console.log("Sequence dimensions: " + dcs.width + " x " + dcs.height);console.log("Sequence duration: " + dcs.duration + " ms");function draw() {render.begin();render.fillRectangle(gray, 0, 0, render.width, render.height);render.drawDCI(dcs,(render.width - dcs.width) / 2,(render.height - dcs.height) / 2);render.end();// Advance to next frameconst frameDuration = dcs.frameDuration;dcs.time += frameDuration;setTimeout(draw, frameDuration);}draw(); ``` PDC sequences have these properties: - `width`, `height` - Dimensions of the sequence - `duration` - Total animation duration in milliseconds - `frameDuration` - Duration of current frame - `time` - Current playback position (set this to advance) ## Animation Use `setInterval()` for frame-based animations. The interval is a request - if Pebble OS can't render fast enough, the actual frame rate will be lower. Smoothest results come from requesting a frame rate that can be consistently achieved: ``` import Poco from "commodetto/Poco";const render = new Poco(screen);const white = render.makeColor(255, 255, 255);const black = render.makeColor(0, 0, 0);let x = 0;setInterval(() =\> {render.begin();// Clear screenrender.fillRectangle(white, 0, 0, render.width, render.height);// Draw moving circlerender.drawCircle(black, x, 90, 20, 0, 360);render.end();x = (x + 2) % render.width;}, 50);// ~20 fps ``` ### Rotating PDC Images PDC images can be transformed. Use `clone()` and `rotate()` for rotation: ``` import Poco from "commodetto/Poco";const render = new Poco(screen);const gray = render.makeColor(128, 128, 128);const dci = new Poco.PebbleDrawCommandImage(2);console.log("PDC dimensions: " + dci.width + " x " + dci.height);let angle = 0;setInterval(() =\> {render.begin();render.fillRectangle(gray, 0, 0, render.width, render.height);// Clone, rotate around center, then drawrender.drawDCI(dci.clone().rotate(angle, dci.width / 2, dci.height / 2),(render.width - dci.width) / 2,(render.height - dci.height) / 2);angle += Math.PI / 30;render.end();}, 50);// ~20fps ``` The `rotate()` method takes angle in radians and pivot point coordinates. ### Scaling PDC Images PDC images can also be scaled dynamically: ``` import Poco from "commodetto/Poco";const render = new Poco(screen);const gray = render.makeColor(128, 128, 128);const dci = new Poco.PebbleDrawCommandImage(2);console.log("PDC size: " + dci.width + " x " + dci.height);const start = Date.now();setInterval(() =\> {// Animate scale with elastic easingconst progress = ((Date.now() - start) % 1500) / 1500;const scale = Math.elasticEaseOut(progress);// Center the scaled imageconst cx = (render.width - dci.width \* scale) / 2;const cy = (render.height - dci.height \* scale) / 2;render.begin();render.fillRectangle(gray, 0, 0, render.width, render.height);render.drawDCI(dci.clone().scale(scale), cx, cy);render.end();}, 50); ``` The `scale()` method takes a scale factor (1.0 = original size). ### PDC Transformation Methods | Method | Description | | --- | --- | | `clone()` | Create a copy for transformation | | `rotate(angle, pivotX, pivotY)` | Rotate by angle (radians) around pivot | | `scale(factor)` | Scale uniformly by factor | Always call `clone()` before transforming to preserve the original. ## Complete Example: Animated Display ``` import Poco from "commodetto/Poco";console.log("Starting animation demo");const render = new Poco(screen);const black = render.makeColor(0, 0, 0);const white = render.makeColor(255, 255, 255);const gray = render.makeColor(128, 128, 128);let angle = 0;setInterval(() =\> {render.begin(0, 0, render.width, render.height);// Backgroundrender.fillRectangle(white, 0, 0, render.width, render.height);// Diagonal linesrender.drawLine(0, 0, render.width, render.height, gray, 4);render.drawLine(0, render.height, render.width, 0, gray, 4);// Rounded rectangle frameconst margin = 30;render.drawRoundRect(margin, margin,render.width - (margin \* 2),render.height - (margin \* 2),black, 8);// Rotating arcconst cx = render.width / 2;const cy = render.height / 2;render.drawCircle(gray, cx, cy, 15, angle, angle + 270);render.end();angle = (angle + 10) % 360;}, 50); ``` ## Screen Properties The `render` object provides useful screen information: | Property | Description | | --- | --- | | `render.width` | Screen width in pixels | | `render.height` | Screen height in pixels | ## Performance Tips 1. **Minimize draw area** : Use `begin(x, y, w, h)` to only redraw changed regions 2. **Batch drawing** : Do all drawing between a single `begin()`/`end()` pair 3. **Reuse colors** : Create colors once and store them in variables 4. **Preload fonts** : Create font objects once at startup 5. **Use PDC for complex graphics** : Vector graphics scale better and use less memory than bitmaps ## Poco vs Piu | Feature | Poco | Piu | | --- | --- | --- | | Approach | Procedural | Declarative | | Control | Low-level, pixel-perfect | High-level, automatic layout | | Best for | Games, watchfaces, custom graphics | Apps with standard UI patterns | | Learning curve | Simpler concepts | More abstraction to learn | | Performance | More control over optimization | Framework handles optimization | Use Poco when you need precise control over every pixel. Use Piu when you want automatic layout and a component-based architecture. ## Examples The [Pebble Examples](https://github.com/Moddable-OpenSource/pebble-examples) repository includes several Poco examples: - [`hellopoco-gbitmap`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellopoco-gbitmap) - rendering Pebble GBitmap bitmaps - [`hellopoco-text`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellopoco-text) - text rendering with Moddable SDK fonts - [`hellopoco-pebbletext`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellopoco-pebbletext) - text rendering with Pebble's built-in fonts - [`hellopoco-pebblegraphics`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellopoco-pebblegraphics) - lines, rounded rectangles, and circles - [`hellopoco-pdc`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellopoco-pdc) - rendering PDC (Draw Command) SVG images - [`hellopoco-pdc-rotate`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellopoco-pdc-rotate) - rotating a PDC image - [`hellopoco-pdc-scale`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellopoco-pdc-scale) - scaling a PDC image with easing - [`hellopoco-pdc-sequence`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellopoco-pdc-sequence) - animated PDC image sequences --- # Sensors and Input Source: https://developer.repebble.com/guides/alloy/sensors-and-input/ Alloy provides access to Pebble hardware sensors and button input through simple JavaScript APIs. Sensors follow the ECMA-419 Sensor Class Pattern, while the Button class is a Pebble-specific API. > **Note** : All code in this guide runs on the watch in `src/embeddedjs/main.js`. ## Button Input Handle button presses using the `Button` class: ``` import Button from "pebble/button";new Button({types: ["select", "up", "down", "back"],onPush(down, type) {console.log((down ? "press " : "release ") + type);}}); ``` > **Note** : If your app includes `"back"` in the `types` array, the back button no longer exits the app automatically - press and hold back to exit instead. If you don't include `"back"`, pressing back exits as usual. ### Button Types | Type | Description | | --- | --- | | `"select"` | Middle button (center right) | | `"up"` | Top button | | `"down"` | Bottom button | | `"back"` | Back button (left side) | ### Click Recognizers By default a `Button` reports _raw_ presses and releases, as above. Since SDK 4.33, buttons can instead (or additionally) use the system's click recognizers, which take care of repeat-while-held, long presses and multi-clicks for you. Enable them with extra options in the constructor: ``` import Button from "pebble/button";new Button({types: ["select", "up", "down"],single: { repeat: 200 },// clicks, repeating every 200ms while heldlong: { delay: 700 },// long presses after 700msonPush(down, type, recognizer, count, repeat) {if (recognizer === "long") {console.log(down ? ("long press " + type) : "long release");} else if (down) {console.log("click " + type + (repeat ? " (held)" : ""));}}}); ``` | Option | Value | Behavior | | --- | --- | --- | | `single` | `true` or `{ repeat }` | Fires once per click with `down = 1`. With a `repeat` interval (ms), the click fires repeatedly while the button is held, with `repeat = true` on repeats. | | `long` | `true` or `{ delay }` | Fires with `down = 1` once the button has been held for `delay` ms (default is the system delay), and again with `down = 0` on release. | | `multi` | `true` or `{ min, max, lastOnly, timeout }` | Recognizes sequences of `min` to `max` clicks arriving within `timeout` ms of each other. Fires with `down = 1` and `count` set to the number of clicks; with `lastOnly` it fires only for the final count. | | `raw` | `true` | Plain press (`down = 1`) and release (`down = 0`) events. This is the default when no recognizer option is given. | Recognizer options apply to all buttons in the `types` array, and multiple recognizers can be combined on the same button. Two restrictions apply to the back button: it does not support `long` (the system reserves press-and-hold back for exiting the app), and its `raw` events are emulated, delivering the press and release back-to-back on click. ### Button Events The `onPush` callback receives five parameters: - `down` - `1` when pressed, `0` when released (use it as a truthy/falsy value) - `type` - String: which button was pressed - `recognizer` - String: which recognizer fired (`"single"`, `"long"`, `"multi"` or `"raw"`) - `count` - Number of clicks counted (e.g. for `multi` sequences) - `repeat` - `true` when this is a repeating `single` click from holding the button Callbacks that only declare `(down, type)` keep working unchanged. ## Accelerometer Read motion data from the accelerometer: ``` import Accelerometer from "embedded:sensor/Accelerometer";const accel = new Accelerometer({onSample() {const sample = this.sample();console.log("accel " + sample.x + ", " + sample.y + ", " + sample.z);},onTap(direction) {console.log("single tap " + direction);},onDoubleTap(direction) {console.log("double tap " + direction);}});// Configure sample rate (Hz)accel.configure({ hz: 10 }); ``` ### Accelerometer Sample Data The `sample()` method returns an object with: | Property | Description | | --- | --- | | `x` | Acceleration on X axis | | `y` | Acceleration on Y axis | | `z` | Acceleration on Z axis | ### Accelerometer Events | Event | Description | | --- | --- | | `onSample()` | Called when new sensor data is available | | `onTap(direction)` | Called on single tap gesture | | `onDoubleTap(direction)` | Called on double tap gesture | ### Configuration Options ``` accel.configure({hz: 10// Sample rate in Hz (samples per second)}); ``` ## Compass Read the magnetic heading from the compass: ``` import Compass from "embedded:sensor/Compass";const compass = new Compass({onSample() {const sample = this.sample();console.log("heading " + sample.heading);}}); ``` ### Compass Sample Data | Property | Description | | --- | --- | | `heading` | Magnetic heading in degrees (0-360) | ## Location Get the user's GPS location via the phone: ``` import Location from "embedded:sensor/Location";const location = new Location({onSample() {const sample = this.sample();console.log("Location: " + sample.latitude + ", " + sample.longitude);this.close();}}); ``` The Location sensor follows the same ECMA-419 pattern as other sensors but has a few differences: - **One-shot** - call `this.close()` after receiving the sample. Location is a request, not a continuous monitor. - **Requires the phone** - location data comes from the phone's GPS via the `@moddable/pebbleproxy` package. - **Requires capability** - add `"location"` to the `capabilities` array in `package.json`. ### Location Sample Data | Property | Type | Description | | --- | --- | --- | | `latitude` | Number | Latitude in decimal degrees | | `longitude` | Number | Longitude in decimal degrees | ### Configuration Options You can optionally configure the location request immediately after creating the instance: ``` location.configure({enableHighAccuracy: false,timeout: 5000,maximumAge: 0}); ``` | Option | Description | | --- | --- | | `enableHighAccuracy` | Request high-accuracy GPS (uses more battery) | | `timeout` | Maximum time in ms to wait for a location | | `maximumAge` | Accept a cached location up to this many ms old | ### PKJS Setup for Location The Location sensor requires the `@moddable/pebbleproxy` package. Set up `src/pkjs/index.js`: ``` const moddableProxy = require("@moddable/pebbleproxy");Pebble.addEventListener('ready', moddableProxy.readyReceived);Pebble.addEventListener('appmessage', moddableProxy.appMessageReceived); ``` No custom location code is needed in PKJS - the proxy handles GPS lookup automatically. ## Touch On platforms with a touchscreen (Emery, Gabbro), read raw touch points using the `Touch` sensor exposed as `device.sensor.Touch`: ``` const touch = new device.sensor.Touch({onSample() {const points = this.sample();if (!points) return;if (points.length === 0) {console.log("lift-off");return;}const { x, y } = points[0];console.log("touch " + x + ", " + y);}}); ``` ### Touch Sample Data `sample()` returns either a falsy value (no new data since the last call) or an array of current contact points. An empty array signals that the last finger has lifted off the screen. | Property | Description | | --- | --- | | `x` | X coordinate in screen pixels | | `y` | Y coordinate in screen pixels | ### Tracking Touch State `onSample` only tells you the current set of contact points, not when a touch started or ended. Track that state yourself to get began/moved/ended semantics: ``` const touch = new device.sensor.Touch({onSample() {const points = this.sample();if (!points) return;const last = this.points[0];if (points.length === 0) {if (last) {console.log("ended at " + last.x + ", " + last.y);this.points[0] = undefined;}return;}const { x, y } = points[0];if (last) {last.x = x;last.y = y;console.log("moved to " + x + ", " + y);} else {this.points[0] = { x, y };console.log("began at " + x + ", " + y);}}});touch.points = new Array(1); ``` > **Note** : For Piu apps, you generally don't need this sensor directly - Piu delivers `onTouchBegan`/`onTouchEnded` events to container Behaviors when `touchCount` is set on the Application. See the [Piu guide](https://developer.repebble.com/guides/alloy/piu-guide.md) for details. ## Battery Status Monitor battery level and charging state: ``` import Battery from "embedded:sensor/Battery";const battery = new Battery({onSample() {const sample = this.sample();console.log("battery " + sample.percent + "%, charging " +sample.charging + ", plugged " + sample.plugged);}});// Get current state immediatelyconst status = battery.sample();console.log("battery " + status.percent + "%"); ``` ### Battery Sample Data | Property | Type | Description | | --- | --- | --- | | `percent` | Number | Battery level (0-100) | | `charging` | Boolean | Whether battery is charging | | `plugged` | Boolean | Whether charger is connected | ## Connection Status Monitor connection to the phone: ``` function logConnected() {console.log("App connected: " + watch.connected.app);console.log("PebbleKitJS connected: " + watch.connected.pebblekit);}watch.addEventListener('connected', logConnected);logConnected(); ``` The `watch.connected` object has two properties: | Property | Description | | --- | --- | | `app` | Whether the Pebble app is connected | | `pebblekit` | Whether PebbleKit JS is ready for messaging | ## Complete Example: Sensor Dashboard Here's a complete example that displays sensor data: ``` import Poco from "commodetto/Poco";import Button from "pebble/button";import Accelerometer from "embedded:sensor/Accelerometer";import Battery from "embedded:sensor/Battery";const render = new Poco(screen);const font = new render.Font("Gothic-Regular", 18);const black = render.makeColor(0, 0, 0);const white = render.makeColor(255, 255, 255);let accelData = { x: 0, y: 0, z: 0 };let batteryPercent = 0;let lastButton = "none";// Setup accelerometerconst accel = new Accelerometer({onSample() {accelData = this.sample();draw();}});accel.configure({ hz: 10 });// Setup battery monitorconst battery = new Battery({onSample() {batteryPercent = this.sample().percent;draw();}});batteryPercent = battery.sample().percent;// Setup button handlernew Button({types: ["select", "up", "down"],onPush(down, type) {if (down) {lastButton = type;draw();}}});function draw() {render.begin();render.fillRectangle(white, 0, 0, render.width, render.height);render.drawText("Battery: " + batteryPercent + "%", font, black, 10, 10);render.drawText("Button: " + lastButton, font, black, 10, 35);render.drawText("Accel X: " + accelData.x.toFixed(2), font, black, 10, 60);render.drawText("Accel Y: " + accelData.y.toFixed(2), font, black, 10, 85);render.drawText("Accel Z: " + accelData.z.toFixed(2), font, black, 10, 110);render.end();}draw(); ``` ## Testing in the Emulator When using the Pebble emulator, you can simulate sensor input: ### Battery ``` $ pebble emu-battery --percent 20 --charging --qemu localhost:12344 ``` ### Accelerometer ``` $ pebble emu-accel tilt-left --qemu localhost:12344 ``` ## Best Practices 1. **Clean up resources** : Sensors continue to consume power while active. Stop them when not needed. 2. **Appropriate sample rates** : Higher accelerometer sample rates use more power. Use the lowest rate that meets your needs. 3. **Handle disconnection gracefully** : Check connection status before attempting network operations. ## Examples The [Pebble Examples](https://github.com/Moddable-OpenSource/pebble-examples) repository includes sensor and input examples: - [`hellobutton`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellobutton) - subscribing to button press and release events - [`helloaccelerometer`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/helloaccelerometer) - reading accelerometer data and detecting taps - [`hellobattery`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellobattery) - monitoring battery level and charging state - [`hellolocation`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellolocation) - getting GPS location from the phone - [`hellopoco-drag`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellopoco-drag) - draggable UI built on the `device.sensor.Touch` driver - [`piu/apps/gravity`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/piu/apps/gravity) - visualizes accelerometer readings with an animated display - [`piu/apps/compass`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/piu/apps/compass) - visualizes compass readings with a rotating compass rose --- # Storage Source: https://developer.repebble.com/guides/alloy/storage/ Alloy provides several ways to persist data on the watch: the Web standard `localStorage` API, the ECMA-419 Key-Value Storage API, and a file system API. > **Note** : All code in this guide runs on the watch in `src/embeddedjs/main.js`. ## localStorage (Recommended) The `localStorage` API provides a simple, familiar way to store string data: **⌚ Watch** : ``` // Store a valuelocalStorage.setItem("username", "Alice");// Retrieve a valueconst username = localStorage.getItem("username");console.log("username: " + username);// "Alice"// Remove a valuelocalStorage.removeItem("username"); ``` ### localStorage Methods | Method | Description | | --- | --- | | `setItem(key, value)` | Store a value | | `getItem(key)` | Retrieve a value (returns `null` if not found) | | `removeItem(key)` | Delete a value | | `clear()` | Remove all stored values | ### Example: Persistent Counter ``` // Get current count or start at 0let counter = localStorage.getItem("counter");if (counter === null) {console.log("initializing counter");counter = 1;} else {counter = Number(counter) + 1;}if (counter \< 5) {console.log("save counter value " + counter);localStorage.setItem("counter", counter);} else {console.log("reset counter");localStorage.removeItem("counter");} ``` ### Storing Objects Since `localStorage` only stores strings, serialize objects with JSON: ``` // Store an objectconst settings = {theme: "dark",notifications: true,fontSize: 14};localStorage.setItem("settings", JSON.stringify(settings));// Retrieve the objectconst stored = localStorage.getItem("settings");if (stored) {const settings = JSON.parse(stored);console.log("theme: " + settings.theme);// "dark"} ``` ## Key-Value Storage (ECMA-419) For more control, and to be able to store binary data as well as strings, use the ECMA-419 Key-Value Storage API: ``` // Open a storage fileconst store = device.keyValue.open({path: "mysettings",format: "string"});// Write a valuestore.write("counter", "42");// Read a valueconst value = store.read("counter");console.log("value: " + value);// "42"// Delete a valuestore.delete("counter");// Close when donestore.close(); ``` ### device.keyValue.open() Options | Option | Description | | --- | --- | | `path` | Name of the storage file (required) | | `format` | Data format: `"string"` or `"buffer"` | ### Store Methods | Method | Description | | --- | --- | | `write(key, value)` | Store a value | | `read(key)` | Retrieve a value (returns `undefined` if not found) | | `delete(key)` | Remove a value | | `close()` | Close the storage file | ### Example: Settings with Key-Value Storage ``` const store = device.keyValue.open({ path: "appsettings", format: "string" });// Save settingsfunction saveSettings(settings) {store.write("settings", JSON.stringify(settings));}// Load settings with defaultsfunction loadSettings() {const stored = store.read("settings");if (stored) {return JSON.parse(stored);}return {vibrate: true,brightness: 50};}const settings = loadSettings();console.log("Vibrate: " + settings.vibrate);// Modify and savesettings.brightness = 75;saveSettings(settings);store.close(); ``` ## Files (ECMA-419) For storing larger data, use the file system API via `device.files`: ``` const path = "example.json";// Write a fileconst jsonData = { name: "Alice", score: 1500 };const data = ArrayBuffer.fromString(JSON.stringify(jsonData));const save = device.files.openFile({ path, mode: "r+", size: data.byteLength });save.write(data, 0);save.close();// Read a fileconst load = device.files.openFile({ path });const loaded = load.read(load.status().size, 0);load.close();const parsed = JSON.parse(String.fromArrayBuffer(loaded));console.log(parsed.name);// "Alice"// Delete a filedevice.files.delete(path); ``` ### openFile() Options | Option | Description | | --- | --- | | `path` | File name (required) | | `mode` | `"r"` for read-only (default), `"r+"` for read-write | | `size` | File size in bytes (required when creating with `"r+"`) | ### File Methods | Method | Description | | --- | --- | | `write(buffer, offset)` | Write an ArrayBuffer at the given byte offset | | `read(count, offset)` | Read `count` bytes from the given byte offset | | `status()` | Returns an object with `size` property | | `close()` | Close the file | | `device.files.delete(path)` | Delete a file | Files are disposable, so instead of calling `close()` manually you can open them with a `using` declaration (SDK 4.33 and later) and they close automatically at the end of the scope: ``` using file = device.files.openFile({ path });const loaded = file.read(file.status().size, 0); ``` ## Choosing Between APIs | Feature | localStorage | Key-Value Storage | Files | | --- | --- | --- | --- | | API Style | Web standard | ECMA-419 | ECMA-419 | | Simplicity | Simpler | Moderate | More verbose | | Global access | Yes | Yes (`device.keyValue`) | Yes (`device.files`) | | Multiple stores | No | Yes (different paths) | Yes (different paths) | | Binary data | No (strings only) | Yes (buffer format) | Yes (ArrayBuffer) | | Random access | No | No | Yes (read/write at offset) | **Recommendation** : Use `localStorage` for most cases. Use Key-Value Storage when you need multiple isolated stores or binary key-value pairs. Use Files when you need random access or large binary data. ## Storage Limits Pebble has limited storage space. Keep stored data minimal: - Store only essential data - Clean up old or unused data - Avoid storing large objects - Consider compressing data if storing significant amounts ## Data Persistence Data stored with all three APIs persists across: - App restarts - Watch reboots - App updates (unless the app is uninstalled) Data is deleted when: - The app is uninstalled - The user performs a factory reset - You explicitly delete it with `removeItem()` or `delete()` ## Error Handling The storage APIs can fail if storage is full or corrupted: ``` try {localStorage.setItem("key", "value");} catch (e) {console.log("Failed to save data");} ``` For Key-Value Storage: ``` try {const store = device.keyValue.open({ path: "settings", format: "string" });store.write("key", "value");store.close();} catch (e) {console.log("Storage error: " + e.message);} ``` ## Complete Example: High Score Tracker ``` console.log("High Score Tracker");// Load existing high scoresfunction loadHighScores() {const stored = localStorage.getItem("highscores");if (stored) {return JSON.parse(stored);}return [];}// Save high scoresfunction saveHighScores(scores) {localStorage.setItem("highscores", JSON.stringify(scores));}// Add a new scorefunction addScore(name, score) {const scores = loadHighScores();scores.push({ name, score, date: Date.now() });// Keep only top 10scores.sort((a, b) =\> b.score - a.score);scores.splice(10);saveHighScores(scores);console.log("Added score: " + name + " - " + score);}// Display high scoresfunction showHighScores() {const scores = loadHighScores();console.log("=== HIGH SCORES ===");scores.forEach((entry, i) =\> {console.log((i + 1) + ". " + entry.name + ": " + entry.score);});}// Example usageaddScore("Alice", 1500);addScore("Bob", 2000);addScore("Charlie", 1750);showHighScores(); ``` ## Examples The [Pebble Examples](https://github.com/Moddable-OpenSource/pebble-examples) repository includes storage examples: - [`hellolocalstorage`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellolocalstorage) - persisting strings with the `localStorage` API - [`hellokeyvalue`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellokeyvalue) - key-value storage with string and binary data - [`hellofiles`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellofiles) - reading and writing JSON data using `device.files` --- # Networking Source: https://developer.repebble.com/guides/alloy/networking/ Alloy apps can communicate with the internet by proxying network requests through PebbleKit JS (PKJS) running on the phone. ## Understanding Watch vs Phone Code Alloy apps have two JavaScript environments: | Location | File Path | Runs On | Purpose | | --- | --- | --- | --- | | **embeddedjs** | `src/embeddedjs/main.js` | Watch | Your app UI and logic | | **PKJS** | `src/pkjs/index.js` | Phone | Network proxy, location, config | Code examples in this guide are labeled with **📱 PKJS** or **⌚ Watch** to indicate where they run. ## Setting Up the Network Proxy To use `fetch()` or `WebSocket` on the watch, install the `@moddable/pebbleproxy` package: ``` $ pebble package install @moddable/pebbleproxy ``` Then set up your `src/pkjs/index.js`: **📱 PKJS** (src/pkjs/index.js): ``` const moddableProxy = require("@moddable/pebbleproxy");Pebble.addEventListener('ready', moddableProxy.readyReceived);Pebble.addEventListener('appmessage', moddableProxy.appMessageReceived); ``` If your app also needs to handle its own events, call the proxy functions from your handlers: ``` const moddableProxy = require("@moddable/pebbleproxy");Pebble.addEventListener('ready', function(e) {moddableProxy.readyReceived(e);// Handle your own ready event here});Pebble.addEventListener('appmessage', function(e) {if (moddableProxy.appMessageReceived(e))return;// Handle your own app messages here}); ``` ## HTTP Requests with fetch() Once the proxy is set up, use `fetch()` in your watch code: **⌚ Watch** (src/embeddedjs/main.js): ``` async function fetchData() {const url = new URL("http://api.open-meteo.com/v1/forecast");url.search = new URLSearchParams({latitude: 37.7749,longitude: -122.4194,current: "temperature\_2m"});const response = await fetch(url);const data = await response.json();console.log("Temperature: " + data.current.temperature\_2m);} ``` ### Important Notes - **Wait for the proxy** - network requests only work after the proxy signals it is ready. Listen for `watch.addEventListener("connected", ...)` before calling `fetch()` or opening a `WebSocket` ### Response Methods | Method | Description | | --- | --- | | `response.json()` | Parse response as JSON | | `response.text()` | Get response as string | | `response.ok` | Boolean: true if status 200-299 | | `response.status` | HTTP status code | ## WebSockets WebSockets are also handled by the `@moddable/pebbleproxy` package - no additional proxy setup needed. **⌚ Watch** (src/embeddedjs/main.js): ``` const ws = new WebSocket("ws://websockets.chilkat.io/wsChilkatEcho.ashx");ws.binaryType = "arraybuffer";ws.addEventListener("open", event =\> {console.log("WebSocket connected");ws.send("Hello from Pebble!");ws.send(Uint8Array.of(0, 1, 2, 3, 4, 5));});ws.addEventListener("message", event =\> {let data = event.data;if (data instanceof ArrayBuffer) {console.log("Received binary data");data = new Uint8Array(data);} else {console.log("Received: " + data);if (data === "Goodbye") ws.close(1000, "Done");}});ws.addEventListener("close", event =\> {console.log("Closed: " + event.code + " " + event.reason);}); ``` ## Connection Status Check if the phone is connected from watch code: **⌚ Watch** (src/embeddedjs/main.js): ``` function logConnected() {console.log("App connected: " + watch.connected.app);console.log("PebbleKitJS connected: " + watch.connected.pebblekit);}watch.addEventListener('connected', logConnected);logConnected(); ``` Network requests only work once the proxy is ready. Wait until `watch.connected.pebblekit` is `true` before calling `fetch()` or opening a `WebSocket`. ## Best Practices 1. **Install `@moddable/pebbleproxy`** - required for `fetch()` and `WebSocket` 2. **Wait for the proxy** - network requests only work after the proxy is ready; listen for the `connected` event or check `watch.connected.pebblekit` 3. **Handle errors** - network requests can fail 4. **Minimize data** - request only what you need 5. **Cache responses** - use `localStorage` to reduce requests ## Advanced Networking For more control over HTTP and WebSocket connections, Alloy also provides low-level ECMA-419 networking APIs: - `HTTPClient` - streaming HTTP client with fine-grained header control - `WebSocketClient` - low-level WebSocket with callback-based API, provides streaming support These are also handled by `@moddable/pebbleproxy`. See the [Advanced Networking](https://developer.repebble.com/guides/alloy/advanced-networking.md) guide for details. ## Examples The [Pebble Examples](https://github.com/Moddable-OpenSource/pebble-examples) repository includes networking examples: - [`hellofetch`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellofetch) - HTTP requests using the `fetch()` API - [`hellowebsocket`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellowebsocket) - WebSocket connections using the `WebSocket` API --- # App Messages Source: https://developer.repebble.com/guides/alloy/app-messages/ The Message API provides direct communication between watch code and PebbleKit JS (PKJS) on the phone. Unlike [networking](https://developer.repebble.com/guides/alloy/networking.md), app messages don't require the `@moddable/pebbleproxy` package - they use Pebble's built-in messaging system. Code examples in this guide are labeled with **📱 PKJS** or **⌚ Watch** to indicate where they run. ## Watch-Phone Messaging Use the `Message` class to send and receive structured data between the watch and PKJS: **⌚ Watch** (src/embeddedjs/main.js): ``` import Message from "pebble/message";const message = new Message({keys: ["COMMAND", "DATA", "RESULT"],onReadable() {const msg = this.read();msg.forEach((value, key) =\> {console.log(key + ": " + value);});},onWritable() {console.log("Ready to send messages");},onSuspend() {console.log("Messages suspended");}}); ``` The `keys` array lists the message key names used for communication. These must match the keys defined in `package.json`. The `onWritable` callback fires when the `Message` instance is ready to send messages. The `onSuspend` callback is the opposite - it fires when sending is temporarily unavailable (e.g., the PKJS connection is lost or another `Message` instance is sending). A new `Message` instance begins in the suspended state. ## Sending Messages from Watch **⌚ Watch** (src/embeddedjs/main.js): ``` message.write(new Map([["COMMAND", 1],["DATA", 42]])); ``` ## Message Keys in package.json Message keys must be defined in `package.json` as an array: ``` {"pebble": {"messageKeys": ["COMMAND","DATA","RESULT"]}} ``` ## Receiving Messages in PKJS **📱 PKJS** (src/pkjs/index.js): ``` Pebble.addEventListener('appmessage', function(e) {console.log("Received from watch: " + JSON.stringify(e.payload));// Send a response back to watchPebble.sendAppMessage({'RESULT': 123});}); ``` ## Combining with the Network Proxy If your app uses both app messages and the network proxy, set up the PKJS to handle both: **📱 PKJS** (src/pkjs/index.js): ``` const moddableProxy = require("@moddable/pebbleproxy");Pebble.addEventListener('ready', moddableProxy.readyReceived);Pebble.addEventListener('appmessage', function(e) {if (moddableProxy.appMessageReceived(e))return;// Handle your own app messages hereconsole.log("Received from watch: " + JSON.stringify(e.payload));}); ``` The proxy's `appMessageReceived()` returns `true` if the message was for the proxy (e.g., a `fetch()` request). If it returns `false`, the message is one of your own and you can handle it. ## Getting GPS Location For GPS location, use the `Location` sensor instead of app messages. See the [Sensors and Input](https://developer.repebble.com/guides/alloy/sensors-and-input.md) guide for details on the Location sensor, or the [watchface tutorial Part 4](https://developer.repebble.com/tutorials/alloy-watchface-tutorial/part4.md) for a complete weather example. ## Examples The [Pebble Examples](https://github.com/Moddable-OpenSource/pebble-examples) repository includes app message examples: - [`hellomessage`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellomessage) - sending and receiving messages between watch and phone - [`helloconnected`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/helloconnected) - monitoring watch-phone connection status --- # Watchfaces Source: https://developer.repebble.com/guides/alloy/watchfaces/ Watchfaces are a special type of Pebble app that displays the current time. Alloy makes it easy to create custom watchfaces using either Poco or Piu. > **Note** : All code in this guide runs on the watch in `src/embeddedjs/main.js` unless otherwise noted. ## Configuration To create a watchface, set `watchface` to `true` in your `package.json`: ``` {"pebble": {"watchapp": {"watchface": true}}} ``` ## Time Events Subscribe to time change events using the `watch` global. The event callback receives a `Date` instance for the current time: ``` // Called every secondwatch.addEventListener("secondchange", e =\> {updateDisplay(e.date);});// Called every minutewatch.addEventListener("minutechange", e =\> {updateDisplay(e.date);}); ``` Use `secondchange` for watchfaces that show seconds, and `minutechange` for those that only show hours and minutes (saves battery). The callback is also invoked immediately when the event listener is installed, so there is no need to call your display function separately at startup. ## Basic Watchface Here's a simple digital watchface: ``` import Poco from "commodetto/Poco";const render = new Poco(screen);const font = new render.Font("Bitham-Black", 30);const black = render.makeColor(0, 0, 0);const white = render.makeColor(255, 255, 255);function draw(e) {render.begin();render.fillRectangle(white, 0, 0, render.width, render.height);// Get current time as HH:MM:SSconst time = e.date.toTimeString().slice(0, 8);// Center the textconst width = render.getTextWidth(time, font);const x = (render.width - width) / 2;const y = (render.height - font.height) / 2;render.drawText(time, font, black, x, y);render.end();}watch.addEventListener("secondchange", draw); ``` ## Getting Time Components Use the `Date` provided by the event to get individual time components: ``` function getTimeComponents(date) {return {hours: date.getHours(),// 0-23minutes: date.getMinutes(),// 0-59seconds: date.getSeconds(),// 0-59day: date.getDate(),// 1-31month: date.getMonth(),// 0-11year: date.getFullYear(),// e.g., 2025dayOfWeek: date.getDay()// 0-6 (Sunday=0)};} ``` ### Formatting Time ``` function formatTime(hours, minutes) {const h = String(hours).padStart(2, "0");const m = String(minutes).padStart(2, "0");return `${h}:${m}`;}function format12Hour(hours, minutes) {const period = hours \>= 12 ? "PM" : "AM";const h = hours % 12 || 12;const m = String(minutes).padStart(2, "0");return `${h}:${m} ${period}`;} ``` ## Watchface with Date ``` import Poco from "commodetto/Poco";const render = new Poco(screen);const timeFont = new render.Font("Leco-Regular", 42);const dateFont = new render.Font("Gothic-Regular", 18);const black = render.makeColor(0, 0, 0);const white = render.makeColor(255, 255, 255);const DAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun","Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];function draw(e) {const now = e.date;render.begin();render.fillRectangle(white, 0, 0, render.width, render.height);// Time (HH:MM)const hours = String(now.getHours()).padStart(2, "0");const minutes = String(now.getMinutes()).padStart(2, "0");const timeStr = `${hours}:${minutes}`;let width = render.getTextWidth(timeStr, timeFont);render.drawText(timeStr, timeFont, black,(render.width - width) / 2,(render.height - timeFont.height) / 2 - 15);// Date (e.g., "Mon, Jan 15")const dayName = DAYS[now.getDay()];const monthName = MONTHS[now.getMonth()];const dateStr = `${dayName}, ${monthName} ${now.getDate()}`;width = render.getTextWidth(dateStr, dateFont);render.drawText(dateStr, dateFont, black,(render.width - width) / 2,(render.height - dateFont.height) / 2 + 30);render.end();}watch.addEventListener("minutechange", draw); ``` ## Analog Watchface Concepts For analog watchfaces, calculate hand positions based on time: ``` function getHandAngles(now) {const hours = now.getHours() % 12;const minutes = now.getMinutes();const seconds = now.getSeconds();// Angles in degrees (0 = 12 o'clock)const secondAngle = seconds \* 6;// 360/60 = 6 degrees per secondconst minuteAngle = minutes \* 6 + seconds \* 0.1;const hourAngle = hours \* 30 + minutes \* 0.5; // 360/12 = 30 degrees per hourreturn { hourAngle, minuteAngle, secondAngle };} ``` ### Drawing Clock Hands ``` function drawHand(cx, cy, angle, length, color, thickness) {// Convert angle to radians (0 degrees = up)const radians = (angle - 90) \* Math.PI / 180;const endX = cx + Math.cos(radians) \* length;const endY = cy + Math.sin(radians) \* length;render.drawLine(cx, cy, endX, endY, color, thickness);}function drawAnalogClock(e) {const cx = render.width / 2;const cy = render.height / 2;const { hourAngle, minuteAngle, secondAngle } = getHandAngles(e.date);render.begin();render.fillRectangle(white, 0, 0, render.width, render.height);// Hour handdrawHand(cx, cy, hourAngle, 35, black, 4);// Minute handdrawHand(cx, cy, minuteAngle, 55, black, 3);// Second handdrawHand(cx, cy, secondAngle, 60, red, 1);// Center dotrender.drawCircle(black, cx, cy, 4, 0, 360);render.end();} ``` ## Using Piu for Watchfaces You can also build watchfaces with the Piu framework: ``` import {} from "piu/MC";const backgroundSkin = new Skin({ fill: "white" });const timeStyle = new Style({ font: "42px Bitham-Black", color: "black" });class ClockBehavior extends Behavior {onDisplaying(label) {this.updateTime(label);}updateTime(label, date) {const h = String(date.getHours()).padStart(2, "0");const m = String(date.getMinutes()).padStart(2, "0");label.string = `${h}:${m}`;}}const WatchfaceApp = Application.template($ =\> ({skin: backgroundSkin,contents: [Label($, {anchor: "TIME",style: timeStyle,Behavior: ClockBehavior})],Behavior: class extends Behavior {onCreate(app, data) {this.data = data;watch.addEventListener("minutechange", e =\> {this.data.TIME.delegate("updateTime", e.date);});}}}));export default new WatchfaceApp({}, {}); ``` ## Battery and Connection Status Good watchfaces show battery and connection status: ``` import Battery from "embedded:sensor/Battery";let batteryPercent = 100;let isConnected = false;// Battery monitoringconst battery = new Battery({onSample() {batteryPercent = this.sample().percent;updateDisplay();}});batteryPercent = battery.sample().percent;// Connection monitoringfunction checkConnection() {isConnected = watch.connected.app;console.log("Connected: " + isConnected);updateDisplay();}watch.addEventListener('connected', checkConnection);checkConnection();function drawStatusBar() {// Battery indicatorconst batteryText = batteryPercent + "%";render.drawText(batteryText, smallFont, gray, 10, 5);// Connection indicator (show X when disconnected)if (!isConnected) {render.drawText("X", smallFont, gray, render.width - 20, 5);}} ``` ## Performance Tips 1. **Use minutechange when possible** : Updating every minute instead of every second significantly improves battery life. 2. **Minimize redraw area** : Use `render.begin(x, y, w, h)` to only update changed portions of the screen. 3. **Precompute values** : Calculate font sizes, positions, and colors once at startup. 4. **Avoid complex calculations in draw** : Do math outside the render begin/end block when possible. 5. **Use the provided Date** : Use the `Date` instance from the event instead of creating your own. ## Complete Example: Minimal Watchface ``` import Poco from "commodetto/Poco";import Battery from "embedded:sensor/Battery";const render = new Poco(screen);const timeFont = new render.Font("Leco-Regular", 42);const dateFont = new render.Font("Gothic-Regular", 18);const black = render.makeColor(0, 0, 0);const white = render.makeColor(255, 255, 255);const gray = render.makeColor(128, 128, 128);const DAYS = ["Sunday", "Monday", "Tuesday", "Wednesday","Thursday", "Friday", "Saturday"];let batteryPercent = 100;const battery = new Battery({onSample() {batteryPercent = this.sample().percent;}});batteryPercent = battery.sample().percent;function draw(e) {const now = e.date;render.begin();render.fillRectangle(black, 0, 0, render.width, render.height);// Timeconst h = String(now.getHours()).padStart(2, "0");const m = String(now.getMinutes()).padStart(2, "0");const timeStr = `${h}:${m}`;let width = render.getTextWidth(timeStr, timeFont);render.drawText(timeStr, timeFont, white,(render.width - width) / 2,(render.height / 2) - timeFont.height);// Day of weekconst dayStr = DAYS[now.getDay()];width = render.getTextWidth(dayStr, dateFont);render.drawText(dayStr, dateFont, gray,(render.width - width) / 2,(render.height / 2) + 10);// Batteryconst batteryStr = `${batteryPercent}%`;width = render.getTextWidth(batteryStr, dateFont);render.drawText(batteryStr, dateFont, gray,(render.width - width) / 2,render.height - dateFont.height - 10);render.end();}watch.addEventListener("minutechange", draw); ``` ## Examples The [Pebble Examples](https://github.com/Moddable-OpenSource/pebble-examples) repository includes watchface examples: - [`hellowatchface`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellowatchface) - simple digital watchface using Poco - [`piu/watchfaces/cupertino`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/piu/watchfaces/cupertino) - classic macOS cursor clock - [`piu/watchfaces/london`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/piu/watchfaces/london) - Big Ben-inspired analog watchface - [`piu/watchfaces/helsinki`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/piu/watchfaces/helsinki) - Nokia style digital watchface - [`piu/watchfaces/redmond`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/piu/watchfaces/redmond) - classic Windows clock - [`piu/watchfaces/zurich`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/piu/watchfaces/zurich) - Swiss train station clock --- # Animations Source: https://developer.repebble.com/guides/alloy/animations/ Alloy provides powerful animation capabilities through the Timeline class and built-in easing functions. These work with both Piu UI components and Poco graphics. > **Note** : All code in this guide runs on the watch in `src/embeddedjs/main.js`. ## Timeline Basics The Timeline class creates smooth, coordinated animations: ``` import Timeline from "piu/Timeline";class AnimatedBehavior extends Behavior {onDisplaying(content) {const timeline = this.timeline = new Timeline();// Animate 'y' property from current to 100 over 750mstimeline.to(content, { y: 100 }, 750, Math.quadEaseOut, 0);// Setup playbackcontent.duration = timeline.duration;timeline.seekTo(0);content.time = 0;content.start();}onTimeChanged(content) {this.timeline.seekTo(content.time);}onFinished(content) {console.log("Animation complete!");}} ``` ### Timeline Methods | Method | Description | | --- | --- | | `to(target, properties, duration, easing, delay)` | Animate properties to new values | | `from(target, properties, duration, easing, delay)` | Animate properties from values | | `on(target, properties, duration, easing, delay)` | Keyframe animation | | `seekTo(time)` | Jump to specific time in animation | ### Timeline Properties | Property | Description | | --- | --- | | `duration` | Total duration of all animations | ## Easing Functions Easing functions control the rate of change during animations. All easing functions are available on the `Math` object: ### Available Easing Functions The [`hellopiu-timeline`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellopiu-timeline) example demonstrates most of these easing equations in action. | Ease In | Ease Out | Description | | --- | --- | --- | | `Math.backEaseIn` | `Math.backEaseOut` | Overshoots then returns | | `Math.bounceEaseIn` | `Math.bounceEaseOut` | Bouncing effect | | `Math.circularEaseIn` | `Math.circularEaseOut` | Circular curve | | `Math.cubicEaseIn` | `Math.cubicEaseOut` | Cubic curve (smooth) | | `Math.elasticEaseIn` | `Math.elasticEaseOut` | Spring/elastic effect | | `Math.exponentialEaseIn` | `Math.exponentialEaseOut` | Exponential curve | | `Math.quadEaseIn` | `Math.quadEaseOut` | Quadratic curve | | `Math.quartEaseIn` | `Math.quartEaseOut` | Quartic curve | | `Math.quintEaseIn` | `Math.quintEaseOut` | Quintic curve | | `Math.sineEaseIn` | `Math.sineEaseOut` | Sinusoidal curve | ### Using Easing Functions ``` // Linear (no easing) - define your ownfunction linearEase(fraction) {return fraction;}// Use built-in easingtimeline.to(content, { y: 200 }, 500, Math.bounceEaseOut, 0);// Ease in then ease outtimeline.to(content, { x: 100 }, 300, Math.quadEaseIn, 0);timeline.to(content, { x: 200 }, 300, Math.quadEaseOut, 300); ``` ### Easing Function Characteristics - **EaseIn** : Starts slow, ends fast - **EaseOut** : Starts fast, ends slow - **Bounce** : Creates a bouncing effect at the end - **Elastic** : Creates a spring-like overshoot - **Back** : Pulls back before moving forward ## Animating in Poco For Poco graphics, use easing functions with `setInterval`: ``` import Poco from "commodetto/Poco";const render = new Poco(screen);const gray = render.makeColor(128, 128, 128);const blue = render.makeColor(0, 100, 255);const start = Date.now();const duration = 1500;setInterval(() =\> {// Calculate animation progress (0 to 1)const elapsed = (Date.now() - start) % duration;const progress = elapsed / duration;// Apply easing functionconst eased = Math.elasticEaseOut(progress);// Animate size based on eased valueconst size = 20 + (60 \* eased);const x = (render.width - size) / 2;const y = (render.height - size) / 2;render.begin();render.fillRectangle(gray, 0, 0, render.width, render.height);render.fillRectangle(blue, x, y, size, size);render.end();}, 50);// ~20fps ``` ## Scaling PDC Images Animate PDC image scaling with easing: ``` import Poco from "commodetto/Poco";const render = new Poco(screen);const gray = render.makeColor(128, 128, 128);const dci = new Poco.PebbleDrawCommandImage(2);console.log("PDC size: " + dci.width + " x " + dci.height);const start = Date.now();setInterval(() =\> {// Calculate scale with elastic easingconst progress = ((Date.now() - start) % 1500) / 1500;const scale = Math.elasticEaseOut(progress);// Calculate centered position for scaled imageconst cx = (render.width - dci.width \* scale) / 2;const cy = (render.height - dci.height \* scale) / 2;render.begin();render.fillRectangle(gray, 0, 0, render.width, render.height);render.drawDCI(dci.clone().scale(scale), cx, cy);render.end();}, 50); ``` ## Complete Piu Animation Example Here's a complete example showing Timeline animations with multiple easing functions: ``` import {} from "piu/MC";import Timeline from "piu/Timeline";const WHITE = "white";const BLACK = "black";const GRAY = "gray";const backgroundSkin = new Skin({ fill: BLACK });const headerSkin = new Skin({ fill: WHITE });const boxSkin = new Skin({ fill: GRAY });const borderSkin = new Skin({fill: "transparent",stroke: WHITE,borders: { left: 2, right: 2, top: 2, bottom: 2 }});const headerStyle = new Style({font: "bold 18px Gothic",color: BLACK});// List of easing functions to demonstrateconst easingFunctions = [{ name: "quadEase", out: Math.quadEaseOut, in: Math.quadEaseIn },{ name: "bounceEase", out: Math.bounceEaseOut, in: Math.bounceEaseIn },{ name: "elasticEase", out: Math.elasticEaseOut, in: Math.elasticEaseIn },{ name: "backEase", out: Math.backEaseOut, in: Math.backEaseIn },];class AnimationBehavior extends Behavior {onCreate(container) {this.index = 0;}onDisplaying(container) {this.startAnimation(container);}startAnimation(container) {const easing = easingFunctions[this.index];const box = container.first;const bottom = container.height - box.height - 4;// Animate downconst timeline = this.timeline = new Timeline();timeline.to(box, { y: bottom }, 750, easing.out, 0);timeline.to(box, { y: 4 }, 750, easing.in, 250);container.duration = timeline.duration + 500;timeline.seekTo(0);container.time = 0;container.start();}onTimeChanged(container) {this.timeline.seekTo(container.time);}onFinished(container) {// Move to next easing functionthis.index = (this.index + 1) % easingFunctions.length;this.startAnimation(container);}}const AnimationApp = Application.template($ =\> ({skin: backgroundSkin,contents: [Column($, {top: 0, bottom: 0, left: 0, right: 0,contents: [Label($, {top: 0, height: 30, left: 0, right: 0,skin: headerSkin,style: headerStyle,string: "Animation Demo"}),Container($, {top: 10, bottom: 10, left: 10, right: 10,skin: borderSkin,contents: [Content($, {top: 4, height: 25, width: 25,skin: boxSkin})],Behavior: AnimationBehavior})]})]}));export default new AnimationApp({}, {}); ``` ## Animation Best Practices 1. **Choose appropriate easing** : Use `quadEaseOut` for UI transitions, `bounceEaseOut` for playful effects, `elasticEaseOut` for attention-grabbing animations. 2. **Keep animations short** : 200-500ms for UI transitions, up to 1000ms for decorative animations. 3. **Use appropriate frame rates** : 30fps (33ms interval) is usually sufficient. 60fps (17ms interval) for smoother animations at higher battery cost. Note that these are requests - if Pebble OS can't keep up, the actual frame rate will be lower. Smoothest results tend to come from requesting a frame rate that can be consistently achieved. 4. **Clean up animations** : Stop intervals and timelines when no longer needed. 5. **Consider battery life** : Frequent screen updates drain battery. Use animations sparingly on watchfaces. ## Combining Animations Chain multiple animations using Timeline delays: ``` const timeline = new Timeline();// First animation: move righttimeline.to(content, { x: 100 }, 300, Math.quadEaseOut, 0);// Second animation: move down (starts after 300ms)timeline.to(content, { y: 100 }, 300, Math.quadEaseOut, 300);// Third animation: fade (starts after 600ms)timeline.to(content, { state: 1 }, 200, Math.quadEaseOut, 600); ``` ## Looping Animations Create looping animations by restarting in `onFinished`: ``` class LoopingBehavior extends Behavior {onDisplaying(content) {this.setupAnimation(content);}setupAnimation(content) {const timeline = this.timeline = new Timeline();timeline.to(content, { y: 100 }, 500, Math.sineEaseOut, 0);timeline.to(content, { y: 0 }, 500, Math.sineEaseIn, 500);content.duration = timeline.duration;timeline.seekTo(0);content.time = 0;content.start();}onTimeChanged(content) {this.timeline.seekTo(content.time);}onFinished(content) {// Restart the animationthis.setupAnimation(content);}} ``` ## Examples The [Pebble Examples](https://github.com/Moddable-OpenSource/pebble-examples) repository includes an animation example: - [`hellopiu-timeline`](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellopiu-timeline) - demonstrates various easing equations with Timeline animation --- # Alloy Source: https://developer.repebble.com/guides/alloy/ Alloy is a JavaScript framework for building Pebble apps, based on the [Moddable SDK](https://www.moddable.com/). Alloy provides developers a mix of standard Web APIs, standard Embedded JavaScript APIs from ECMA-419, and dedicated Pebble OS APIs. It allows you to write apps using modern JavaScript (ES2025, ES6++) with powerful UI frameworks and access to Pebble hardware features. > **Platform Support** : Alloy currently supports Emery (Pebble Time 2) and Gabbro (Pebble Round 2). See the platform support table in [_Hardware Information_](https://developer.repebble.com/guides/tools-and-resources/hardware-information.md#platform-support). ## Key Features - **Modern JavaScript** : Write apps using ES modules, classes, async/await, and other modern JavaScript features - **Two UI Frameworks** : Choose between Piu (declarative, component-based) or Poco (procedural, low-level graphics) - **Hardware Access** : Full access to accelerometer, battery, buttons, compass, and other Pebble sensors - **Network Communication** : Built-in support for HTTP requests, WebSockets, and watch-phone messaging - **Persistent Storage** : Simple APIs for storing data locally using `localStorage`, key-value storage, or files ## Getting Started To create a new Alloy project: ``` $ pebble new-project --alloy my-app ``` This creates a new project with the standard Alloy structure: ``` my-app/ src/ embeddedjs/ main.js # Watch code (runs on Pebble) pkjs/ index.js # Phone code (for networking, location) c/ mdbl.c # C code as entry point for embeddedjs (usually no need to modify) resources/ # App resources (images, fonts) package.json # App manifest ``` Alloy apps have two JavaScript environments: **embeddedjs** runs on the watch, while **pkjs** runs on the connected phone for network and location services. ## Hello World The simplest Alloy app: ``` console.log("Hello, Pebble!"); ``` ## Rocky.js Rocky.js is no longer part of the firmware or the SDK. Alloy replaces it. ## Guides - [**Getting Started with Alloy**](https://developer.repebble.com/guides/alloy/getting-started.md) - Learn how to create your first Alloy app for Pebble. - [**Piu UI Framework**](https://developer.repebble.com/guides/alloy/piu-guide.md) - Build declarative user interfaces with the Piu framework. - [**Poco Graphics**](https://developer.repebble.com/guides/alloy/poco-guide.md) - Low-level graphics rendering with the Poco framework. - [**Sensors and Input**](https://developer.repebble.com/guides/alloy/sensors-and-input.md) - Access Pebble sensors and handle button input in Alloy apps. - [**Storage**](https://developer.repebble.com/guides/alloy/storage.md) - Persist data between app launches using localStorage, key-value storage, or files. - [**Networking**](https://developer.repebble.com/guides/alloy/networking.md) - Make HTTP requests and use WebSockets via the phone proxy. - [**App Messages**](https://developer.repebble.com/guides/alloy/app-messages.md) - Send and receive messages between the watch and the phone. - [**Watchfaces**](https://developer.repebble.com/guides/alloy/watchfaces.md) - Build custom watchfaces with Alloy. - [**Animations**](https://developer.repebble.com/guides/alloy/animations.md) - Create smooth animations using Timeline and easing functions. - [**Port (Custom Drawing)**](https://developer.repebble.com/guides/alloy/port-drawing.md) - Combine Piu's declarative UI with custom drawing using Port. - [**Advanced Networking**](https://developer.repebble.com/guides/alloy/advanced-networking.md) - Low-level HTTP and WebSocket clients for advanced networking needs. - [**Native Functions (FFI)**](https://developer.repebble.com/guides/alloy/ffi.md) - Call native C functions directly from JavaScript in your Alloy app using the Foreign Function Interface. - [**Dictation**](https://developer.repebble.com/guides/alloy/dictation.md) - Capture speech as text in Alloy apps using the dictation API. - [**Wakeups**](https://developer.repebble.com/guides/alloy/wakeups.md) - Schedule your Alloy app to launch at a future time using wakeups. - [**Vibration**](https://developer.repebble.com/guides/alloy/vibration.md) - Provide haptic feedback in Alloy apps using the vibration motor. - [**Device Info and App Events**](https://developer.repebble.com/guides/alloy/device-info.md) - Query watch and screen properties, respond to app lifecycle events, and control the backlight in Alloy apps. - [**Health**](https://developer.repebble.com/guides/alloy/health.md) - Read steps, sleep, activities and heart rate data from Pebble Health in Alloy apps. - [**Alloy Reference**](https://developer.repebble.com/guides/alloy/reference.md) - Where the Alloy APIs are documented, and the details that are not obvious from the other guides: font names, resource IDs, modules, memory and TypeScript. ## Example Apps The [Moddable Pebble Examples](https://github.com/Moddable-OpenSource/pebble-examples) repository contains a collection of examples covering all aspects of Alloy development. Here's a categorized overview: ### Fundamentals | Example | Description | | --- | --- | | [hellopebble](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellopebble) | "Hello, world" - the simplest starting point | | [hellotimer](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellotimer) | Using `setTimeout` | | [hellomodule](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellomodule) | Loading multiple modules | | [hellotypescript](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellotypescript) | TypeScript with the Pebble `Button` class | ### Storage | Example | Description | | --- | --- | | [hellokeyvalue](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellokeyvalue) | ECMA-419 Key-Value Storage for persistent data | | [hellolocalstorage](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellolocalstorage) | Web standard `localStorage` for persisting strings | | [hellofiles](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellofiles) | File system storage | ### Sensors & Input | Example | Description | | --- | --- | | [helloaccelerometer](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/helloaccelerometer) | Subscribing to accelerometer readings | | [hellobattery](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellobattery) | Battery and charging status | | [hellolocation](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellolocation) | GPS location via phone | | [hellobutton](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellobutton) | Pebble button events | | [hellopoco-drag](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellopoco-drag) | Touch events with the ECMA-419 touch driver | ### Piu UI Framework | Example | Description | | --- | --- | | [hellopiu-text](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellopiu-text) | Dynamic text layout with different fonts | | [hellopiu-pebbletext](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellopiu-pebbletext) | Text using Pebble built-in fonts | | [hellopiu-balls](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellopiu-balls) | Classic bouncing balls demo | | [hellopiu-coloredsquares](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellopiu-coloredsquares) | Drawing colored squares | | [hellopiu-gbitmap](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellopiu-gbitmap) | Pebble GBitmap PNG images as Piu textures | | [hellopiu-jsicon](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellopiu-jsicon) | Moddable SDK bitmaps as Piu textures | | [hellopiu-port](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellopiu-port) | Animated graph using Piu Port | | [hellopiu-timeline](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellopiu-timeline) | Easing equations with Timeline animation | ### Piu Watchfaces | Example | Description | | --- | --- | | [cupertino](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/piu/watchfaces/cupertino) | Classic macOS watch cursor as a watchface | | [london](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/piu/watchfaces/london) | Big Ben (color watches only) | | [helsinki](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/piu/watchfaces/helsinki) | Minimal design with per-model assets | | [redmond](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/piu/watchfaces/redmond) | Classic Windows clock | | [zurich](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/piu/watchfaces/zurich) | Iconic Swiss railway clock | ### Poco Renderer | Example | Description | | --- | --- | | [hellopoco-text](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellopoco-text) | Text rendering with Moddable SDK fonts | | [hellopoco-pebbletext](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellopoco-pebbletext) | Text rendering with Pebble built-in fonts | | [hellopoco-gbitmap](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellopoco-gbitmap) | Rendering GBitmap resources | | [hellopoco-pebblegraphics](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellopoco-pebblegraphics) | Lines, round rectangles, and circles | | [hellopoco-qrcode](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellopoco-qrcode) | Dynamic QR code generation | | [hellopoco-pdc](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellopoco-pdc) | Rendering PDC (SVG) images | | [hellopoco-pdc-rotate](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellopoco-pdc-rotate) | Spinning a PDC image | | [hellopoco-pdc-scale](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellopoco-pdc-scale) | Animated PDC scaling with easing | | [hellopoco-pdc-sequence](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellopoco-pdc-sequence) | PDC image sequence animation | | [hellowatchface](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellowatchface) | Simple watchface app | ### Communication | Example | Description | | --- | --- | | [hellomessage](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellomessage) | Watch-phone messaging via app\_message | | [helloconnected](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/helloconnected) | Phone connection status notifications | | [hellofetch](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellofetch) | HTTP requests using the `fetch()` API | | [hellohttpclient](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellohttpclient) | HTTP requests using ECMA-419 HTTP Client | | [hellowebsocket](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellowebsocket) | WebSocket using the Web standard API | | [hellowebsocketclient](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellowebsocketclient) | WebSocket using ECMA-419 WebSocket Client | ### Sensor Visualizations (Piu) | Example | Description | | --- | --- | | [compass](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/piu/apps/compass) | Compass visualization (Emery only) | | [gravity](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/piu/apps/gravity) | Accelerometer visualization (Emery only) | | [tic-tac-toe](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/piu/apps/tic-tac-toe) | Touch game using Piu Behavior touch events | ### Device, Events & Feedback | Example | Description | | --- | --- | | [helloinfo](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/helloinfo) | Watch, screen, and device information | | [helloappevents](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/helloappevents) | App focus and display resize events | | [hellowakeup](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellowakeup) | Scheduling the app to launch later with wakeups | | [hellodictation](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellodictation) | Capturing speech as text with dictation | | [hellovibes](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellovibes) | Haptic feedback with the vibration motor | | [hellolight](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/hellolight) | Controlling the backlight | ### Native Code | Example | Description | | --- | --- | | [helloffi](https://github.com/Moddable-OpenSource/pebble-examples/tree/main/helloffi) | Calling native C functions from JavaScript (FFI) | ## Additional Resources - [Moddable SDK Documentation](https://www.moddable.com/documentation/readme) - Comprehensive documentation for Piu, Poco, and other modules