name: pebble-watchface description: Generate complete Pebble smartwatch watchfaces, build PBW artifacts, and test in QEMU emulator. Use when creating watchfaces, Pebble apps, animated displays, clock faces. Produces ready-to-install PBW files and runs them in emulator.
Pebble Watchface Generator
Generate complete, buildable Pebble watchfaces with full PBW artifact output and QEMU testing.
Default target platform: Emery (Pebble Time 2, 200x228 color rectangular display).
CRITICAL: End-to-End Delivery
This skill MUST produce a final .pbw file, test it, AND visually verify it looks correct.
Every watchface request follows this complete flow:
- Research → [SUBAGENT] Gather requirements, study samples and tutorials
- Design → [SUBAGENT] Plan architecture and visuals
- Implement → Write ALL project files
- Build → Run
pebble buildto generate PBW - Test → Run in QEMU, capture screenshot, visually verify with Read tool
- Iterate → Fix issues until screenshot looks good
- Generate Assets → Capture screenshots and rollover GIFs using
pebble screenshot - Deliver → Report PBW location with verified screenshots and GIFs
- Publish → (Optional) Publish to Pebble App Store with
pebble publish
Never stop until:
- PBW is built successfully
- Screenshots captured from QEMU for emery platform
- Visual verification confirms it looks correct
- User receives the final artifacts
CRITICAL: Battery Efficiency
ALWAYS use MINUTE_UNIT for tick_timer_service_subscribe(). NEVER use SECOND_UNIT unless the user explicitly requests a seconds display. SECOND_UNIT causes the watchface to redraw every second, which drastically reduces battery life. Design all watchfaces to update on minute boundaries.
For animated watchfaces that use app_timer_register(), only run animations briefly (e.g., on a tap event or for a few seconds after minute change), then stop the timer. Continuous animation is acceptable only when the user explicitly requests it.
Platform Reference
| Platform | Model | Resolution | Shape | Colors |
|---|---|---|---|---|
| emery | Pebble Time 2 | 200x228 | Rect | 64-color |
| gabbro | Pebble Round 2 | 260x260 | Round | 64-color |
| basalt | Pebble Time | 144x168 | Rect | 64-color |
| chalk | Pebble Time Round | 180x180 | Round | 64-color |
| aplite | Pebble Classic | 144x168 | Rect | B&W |
| diorite | Pebble 2 | 144x168 | Rect | B&W |
| flint | Pebble 2 Duo | 144x168 | Rect | 64-color |
Emery is the default and exclusive target. If the user wants gabbro (round) support, that should be a second pass after the emery version is finalized.
Phase 1: Research [USE SUBAGENT]
Spawn a research subagent using Agent tool with subagent_type: "Explore" to:
Gather Requirements
Ask the user (use AskUserQuestion if unclear):
- Type: Digital, analog, animated, or artistic?
- Elements: Time, date, battery, weather, custom graphics?
- Animation: Static, subtle, or complex animations?
- Weather/Web data: Does it need weather or other internet data?
Study Existing Code
The subagent should read and analyze:
samples/aqua-pbw/src/c/main.c— animated watchface patternstutorials/c-watchface-tutorial/part1/— basic time + datetutorials/c-watchface-tutorial/part4/— weather via AppMessage + pkjs
Key patterns to extract:
- Data structures for animated elements
- Animation loop structure
- Drawing functions
- Memory management patterns
- Battery-aware throttling
- Weather/AppMessage communication (if needed)
Also have subagent read relevant reference docs:
reference/pebble-api-reference.mdreference/animation-patterns.mdreference/drawing-guide.md
Phase 2: Design [USE SUBAGENT]
Spawn a planning subagent using Agent tool with subagent_type: "Plan" to:
Create Design Specification
- Screen layout for emery (200x228) rectangular display
- Element positions and sizes
- Animation behavior and timing (intervals, speeds)
- Color scheme (64-color palette)
- Data structures needed
- Layer hierarchy
- Whether weather/web data is needed (requires pkjs)
CRITICAL: Layout Planning to Prevent Cropping
You MUST calculate exact pixel positions to ensure nothing is cropped.
For emery (200x228):
Available space: X: 0-199, Y: 0-227
Safe margins: 2-5 pixels from edges
For each visual element, calculate:
- Y position: Where does it start vertically?
- Element height: How tall is it?
- Bottom edge: Y_position + height must be < 228 (with margin)
Example layout calculation for emery:
SCREEN_WIDTH = 200, SCREEN_HEIGHT = 228
Time text: Y=60, height=50 → bottom at 110 ✓
Date text: Y=115, height=26 → bottom at 141 ✓
Weather: Y=145, height=24 → bottom at 169 ✓
Battery bar: Y=0, height=3 → bottom at 3 ✓
FAIL CONDITIONS to check in design:
- Element bottom edge >= SCREEN_HEIGHT (228 for emery)
- Element right edge >= SCREEN_WIDTH (200 for emery)
- GPath points with negative offsets that extend beyond anchor point
- Elements positioned relative to SCREEN_HEIGHT without accounting for element size
GPath Positioning Guide
GPaths use relative coordinates from an anchor point. Calculate carefully:
// GPath points are RELATIVE to where you move_to
static GPoint castle_points[] = {
{-35, 0}, // 35px LEFT of anchor, AT anchor Y
{-35, -40}, // 35px left, 40px ABOVE anchor
{35, 0}, // 35px RIGHT of anchor
};
// Anchor positioning calculation:
// If castle_points go from Y=0 to Y=-40 (40px tall, extending UP)
// And you want bottom of castle at Y=223 (5px margin from 228)
// Then anchor Y = 223 (the base of the castle)
gpath_move_to(castle_path, GPoint(SCREEN_WIDTH/2, 223));
Architecture Planning
- What structs are needed?
- How many animated elements?
- Update interval (MINUTE_UNIT for tick, 50ms for brief animations)
- Memory pre-allocation strategy
- Does it need pkjs for weather/web data?
Phase 3: Implementation
Do this directly (not a subagent) — write all files:
Create Project Directory
mkdir -p /path/to/watchface-name/src/c
mkdir -p /path/to/watchface-name/resources
Write ALL Required Files
1. package.json (REQUIRED)
{
"name": "watchface-name",
"author": "Author Name",
"version": "1.0.0",
"keywords": ["pebble-app"],
"private": true,
"dependencies": {},
"pebble": {
"displayName": "Watchface Display Name",
"uuid": "GENERATE-NEW-UUID-HERE",
"sdkVersion": "3",
"enableMultiJS": true,
"targetPlatforms": ["emery"],
"watchapp": { "watchface": true },
"resources": { "media": [] }
}
}
Generate UUID: python3 -c "import uuid; print(uuid.uuid4())"
If weather/web data is needed, add to the pebble section:
{
"capabilities": ["location"],
"messageKeys": ["TEMPERATURE", "CONDITIONS", "REQUEST_WEATHER"]
}
2. wscript (REQUIRED)
import os.path
top = '.'
out = 'build'
def options(ctx):
ctx.load('pebble_sdk')
def configure(ctx):
ctx.load('pebble_sdk')
def build(ctx):
ctx.load('pebble_sdk')
build_worker = os.path.exists('worker_src')
binaries = []
cached_env = ctx.env
for platform in ctx.env.TARGET_PLATFORMS:
ctx.env = ctx.all_envs[platform]
ctx.set_group(ctx.env.PLATFORM_NAME)
app_elf = '{}/pebble-app.elf'.format(ctx.env.BUILD_DIR)
ctx.pbl_build(source=ctx.path.ant_glob('src/c/**/*.c'),
target=app_elf, bin_type='app')
if build_worker:
worker_elf = '{}/pebble-worker.elf'.format(ctx.env.BUILD_DIR)
binaries.append({'platform': platform, 'app_elf': app_elf,
'worker_elf': worker_elf})
ctx.pbl_build(source=ctx.path.ant_glob('worker_src/c/**/*.c'),
target=worker_elf, bin_type='worker')
else:
binaries.append({'platform': platform, 'app_elf': app_elf})
ctx.env = cached_env
ctx.set_group('bundle')
ctx.pbl_bundle(binaries=binaries,
js=ctx.path.ant_glob(['src/pkjs/**/*.js',
'src/pkjs/**/*.json']),
js_entry_file='src/pkjs/index.js')
3. src/c/main.c (REQUIRED) Write complete watchface code following the design from Phase 2.
Use templates as starting points:
- templates/animated-watchface.c — animated watchfaces
- templates/static-watchface.c — static/analog watchfaces
- templates/weather-watchface.c — watchfaces with weather data
4. src/pkjs/index.js (REQUIRED if weather/web data needed) Use templates/pkjs-weather.js as starting point.
The pkjs file runs on the phone and handles:
- GPS location via
navigator.geolocation.getCurrentPosition() - HTTP requests via
XMLHttpRequestto web APIs - Sending data to watch via
Pebble.sendAppMessage() - Receiving requests from watch via
appmessageevent
Open-Meteo API (free, no API key):
https://api.open-meteo.com/v1/forecast?latitude=LAT&longitude=LON¤t=temperature_2m,weather_code
Code Requirements
#include <pebble.h>- Implement
main(),init(),deinit() - Window with load/unload handlers
tick_timer_service_subscribe(MINUTE_UNIT, tick_handler)— ALWAYS MINUTE_UNIT- For brief animations:
app_timer_register()with 50ms interval - Pre-allocate GPath in window_load
- Destroy all resources in unload handlers
- Fixed-point math only (sin_lookup/cos_lookup)
- Use
layer_get_bounds()for screen dimensions — don't hardcode sizes - Register AppMessage callbacks BEFORE calling
app_message_open()
Phase 4: Build PBW
Run the Build
cd /path/to/watchface-name
pebble build
Verify Build Success
ls -la build/*.pbw
Handle Build Errors
If build fails:
- Read error message
- Fix C code (syntax, types, missing includes)
- Run
pebble buildagain - Repeat until successful
Phase 5: Test in QEMU Emulator
REQUIRED — Must test AND visually verify before delivering.
Step 1: Launch Emulator and Install
# Primary test - emery (Pebble Time 2, 200x228 color)
pebble install --emulator emery
Wait a few seconds for the watchface to load and render.
Step 2: Capture Screenshot (MANDATORY)
pebble screenshot --no-open --emulator emery screenshot_emery.png
Step 3: Visual Verification (MANDATORY)
Use the Read tool to view the screenshot image:
Read tool: screenshot_emery.png
CRITICAL: Perform thorough visual verification using this detailed checklist.
A. Cropping Check (FAIL if any element is cut off)
- All visual elements fully visible — No element should be cut off at screen edges
- Key graphics not clipped — Main visual elements must be 100% within 200x228 bounds
- No overflow at bottom — Elements near y=228 must have margin
- No overflow at sides — Elements near x=0 or x=200 must have margin
- Text not truncated — All text fits within its designated area
B. Positioning Check (FAIL if layout doesn't match design)
- Time in correct position — Matches the designed location
- Visual elements properly placed — Each element appears where designed
- Proportional spacing — Elements have appropriate margins
- Center alignment — Centered elements are actually centered (x=100 center)
C. Color Scheme Check (FAIL if colors don't match design)
- Primary colors correct — Main colors match design spec
- Contrast sufficient — Text and elements are readable
D. Design Intent Check (FAIL if doesn't match user request)
- Theme recognizable — Watchface represents the requested theme
- Key features prominent — Main visual features are visible
- Overall composition balanced — Layout looks intentional
STOP AND FIX if ANY check fails. Do not proceed to delivery with visual issues.
Step 4: Fix Issues and Re-test
If visual verification fails:
Fixing Cropping Issues
- Bottom cropping: Reduce Y coordinates, use
bounds.size.h - H - marginformula - Side cropping: Use
bounds.size.w / 2 - element_width / 2for centering - Common mistake: Hardcoding 144x168 values instead of using
layer_get_bounds()
Iteration Process:
- Identify which check(s) failed
- Apply the specific fix
- Rebuild:
pebble build - Reinstall:
pebble install --emulator emery - New screenshot:
pebble screenshot --no-open --emulator emery screenshot_emery.png - Re-verify with Read tool
- Repeat until ALL checks pass
Step 5: Check Logs for Errors
pebble logs --emulator emery
Look for:
- APP_LOG errors
- Crashes or exceptions
- Memory warnings
Phase 6: Generate Assets
After the watchface passes visual verification, generate marketing assets.
Capture Screenshots
pebble screenshot --no-open --emulator emery screenshot_emery.png
Generate App Icons
python3 /path/to/skills/pebble-watchface/scripts/create_app_icons.py .
Creates icon_80x80.png and icon_144x144.png from the screenshot.
Generate Preview GIFs (for animated watchfaces)
python3 /path/to/skills/pebble-watchface/scripts/create_preview_gif.py . --frames 8 --delay 400
Creates preview_emery.gif (and other platforms if their emulators are running).
Phase 7: Deliver
Report to User
After successful build AND visual verification:
-
PBW Location:
build/watchface-name.pbw -
Verified Screenshots: Show the captured screenshots
screenshot_emery.png— Primary emery display
-
Preview GIFs (if animated):
preview_emery.gifin project root -
Visual Confirmation: Describe what the watchface shows
-
Install Commands:
- Emulator:
pebble install --emulator emery - Device:
pebble install --cloudpebble
- Emulator:
-
Publish: Offer to publish to the Pebble App Store (see Phase 8)
-
Gabbro support: Suggest a second pass for round display (260x260) if the user wants it
Phase 8: Publish to Pebble App Store (Optional)
If the user wants to publish, use the built-in pebble publish command.
Prerequisites
# Login first (opens browser for Firebase OAuth)
pebble login
# Check login status
pebble login --status
Interactive Publish (Recommended)
pebble publish
This will:
- Build the PBW
- Prompt for screenshot/GIF capture method
- For new apps: prompt for name, description, category, icons
- Upload to the Pebble App Store
Non-Interactive Publish (CI)
pebble publish --non-interactive \
--description "Your watchface description" \
--release-notes "Initial release"
Publish Flags
| Flag | Description |
|---|---|
--release-notes TEXT | Release notes for this version |
--is-published | Make release immediately visible |
--gif-all-platforms | Capture rollover GIFs before upload (default: on) |
--no-gif-all-platforms | Skip GIF capture |
--non-interactive | No prompts, use flags/defaults |
--name NAME | App name (new apps only) |
--description DESC | Short description (required for new non-interactive) |
--category CAT | Category: daily, tools, notifications, remotes, health, games |
Weather Watchface Architecture
When a watchface needs weather or other web data, use the AppMessage + PebbleKit JS pattern:
Watch (C code) ←AppMessage→ Phone (PebbleKit JS) ←HTTP→ Web API
Required Files
- src/c/main.c — C code with AppMessage handlers
- src/pkjs/index.js — JavaScript running on phone
- wscript — Must use
pbl_build(..., bin_type='app')withjs_entry_file='src/pkjs/index.js'(thepbl_programpattern does NOT work with pkjs). Use the wscript template.
Required package.json Changes
{
"pebble": {
"enableMultiJS": true,
"capabilities": ["location"],
"messageKeys": ["TEMPERATURE", "CONDITIONS", "REQUEST_WEATHER"]
}
}
C Side Pattern
// In init(), register callbacks BEFORE opening:
app_message_register_inbox_received(inbox_received_callback);
app_message_open(128, 128);
// Receive weather data:
static void inbox_received_callback(DictionaryIterator *iterator, void *context) {
Tuple *temp_tuple = dict_find(iterator, MESSAGE_KEY_TEMPERATURE);
Tuple *cond_tuple = dict_find(iterator, MESSAGE_KEY_CONDITIONS);
// Update display...
}
// Request refresh every 30 minutes from tick_handler:
if (tick_time->tm_min % 30 == 0) {
DictionaryIterator *iter;
AppMessageResult result = app_message_outbox_begin(&iter);
if (result == APP_MSG_OK) {
dict_write_uint8(iter, MESSAGE_KEY_REQUEST_WEATHER, 1);
app_message_outbox_send();
}
}
JS Side Pattern (src/pkjs/index.js)
// Use Open-Meteo API (free, no API key)
function getWeather() {
navigator.geolocation.getCurrentPosition(function(pos) {
var url = 'https://api.open-meteo.com/v1/forecast?' +
'latitude=' + pos.coords.latitude +
'&longitude=' + pos.coords.longitude +
'¤t=temperature_2m,weather_code';
// Fetch and send via Pebble.sendAppMessage()...
});
}
Pebble.addEventListener('ready', function() { getWeather(); });
Pebble.addEventListener('appmessage', function(e) {
if (e.payload['REQUEST_WEATHER']) getWeather();
});
See tutorials/c-watchface-tutorial/part4/ for a complete working example.
Visual Weather Reactions (C Side)
The pkjs sends weather as human-readable strings ("Clear", "Cloudy", "Rain", etc.). To change visuals based on weather (sky color, particles, accessories), reverse-map the string to a numeric code on the C side:
static int s_weather_code = -1; // -1 = no data yet
// In inbox_received_callback, after reading CONDITIONS:
const char *c = cond_tuple->value->cstring;
if (strcmp(c, "Clear") == 0) s_weather_code = 0;
else if (strcmp(c, "Cloudy") == 0) s_weather_code = 2;
else if (strcmp(c, "Rain") == 0 || strcmp(c, "Showers") == 0) s_weather_code = 63;
else if (strcmp(c, "Snow") == 0) s_weather_code = 73;
else if (strcmp(c, "Fog") == 0) s_weather_code = 45;
else if (strcmp(c, "T-Storm") == 0) s_weather_code = 95;
else s_weather_code = 2;
// Then in draw functions, branch on s_weather_code:
if (s_weather_code == 0) { /* draw sun, blue sky */ }
else if (s_weather_code >= 61) { /* draw rain drops */ }
else if (s_weather_code >= 71) { /* draw snowflakes, white ground */ }
Battery-Efficient Visual Variety
Even with MINUTE_UNIT updates (no animation timer), you can create visual variety by using deterministic math tied to the minute counter. Each minute tick increments a frame counter, and drawing functions use it to offset positions:
static int s_frame = 0; // incremented in tick_handler
// In draw function — "animated" rain/snow without a timer:
int rx = (i * 37 + s_frame * 7) % bounds.size.w;
int ry = 40 + (i * 23 + s_frame * 11) % sky_height;
This gives a different scene each minute without burning battery on sub-second redraws.
Tutorial Reference
Complete working tutorial examples are in tutorials/c-watchface-tutorial/:
| Part | What It Teaches |
|---|---|
| part1 | Basic time + date display with system fonts |
| part4 | Weather via AppMessage + PebbleKit JS + Open-Meteo API |
| part6 | User settings via Clay configuration framework |
These are sourced from coredevices/c-watchface-tutorial.
Subagent Summary
| Phase | Subagent Type | Purpose |
|---|---|---|
| Research | Explore | Read samples, tutorials, extract patterns |
| Design | Plan | Create implementation plan for emery (200x228) |
| Implement | Direct | Write all project files |
| Build | Direct | Run pebble build |
| Test | Direct | Run in QEMU, screenshot, verify with Read tool |
| Iterate | Direct | Fix code until screenshot looks correct |
| Assets | Direct | Run create_app_icons.py and create_preview_gif.py |
| Deliver | Direct | Report PBW + screenshots + GIFs to user |
| Publish | Direct | Run pebble publish if user requests |
Quick Reference
Emery Screen Dimensions (Default Target)
| Property | Value |
|---|---|
| Width | 200 px |
| Height | 228 px |
| Shape | Rectangular |
| Colors | 64-color |
| Center X | 100 |
| Center Y | 114 |
All Platform Dimensions
| Platform | Resolution | Shape | Color |
|---|---|---|---|
| emery | 200x228 | Rect | 64-color |
| gabbro | 260x260 | Round | 64-color |
| basalt | 144x168 | Rect | 64-color |
| chalk | 180x180 | Round | 64-color |
| aplite | 144x168 | Rect | B&W |
| diorite | 144x168 | Rect | B&W |
| flint | 144x168 | Rect | 64-color |
Key APIs
// Drawing
graphics_fill_circle(ctx, center, radius);
graphics_draw_line(ctx, start, end);
graphics_fill_rect(ctx, rect, corner_radius, corners);
graphics_draw_arc(ctx, rect, scale_mode, angle_start, angle_end);
graphics_fill_radial(ctx, rect, scale_mode, inset, angle_start, angle_end);
// Fixed-point trig (NO FLOATS!)
sin_lookup(angle); // 0 to TRIG_MAX_ANGLE (65536)
cos_lookup(angle); // returns -TRIG_MAX_RATIO to +TRIG_MAX_RATIO
DEG_TO_TRIGANGLE(degrees); // macro for conversion
// Time — ALWAYS USE MINUTE_UNIT
tick_timer_service_subscribe(MINUTE_UNIT, tick_handler);
// Screen dimensions — use dynamically, don't hardcode
Layer *window_layer = window_get_root_layer(window);
GRect bounds = layer_get_bounds(window_layer);
// bounds.size.w = 200 on emery, bounds.size.h = 228 on emery
// AppMessage (for weather/web data)
app_message_register_inbox_received(callback);
app_message_open(128, 128);
Build & Test Commands
pebble build # Build PBW
pebble install --emulator emery # Test in QEMU
pebble logs --emulator emery # View logs
pebble screenshot --no-open --emulator emery # Capture screen
python3 scripts/create_preview_gif.py . --frames 8 # Capture preview GIFs
python3 scripts/create_app_icons.py . # Generate app icons
pebble install --cloudpebble # Deploy to device
pebble login # Login for publishing
pebble publish # Publish to App Store
Emulator Interaction Commands
Button Presses (pebble emu-button):
pebble emu-button click select --emulator emery
pebble emu-button click back --duration 2000 --emulator emery
Accelerometer Tap (pebble emu-tap):
pebble emu-tap --emulator emery
Constraints
- No Floating Point — Use sin_lookup/cos_lookup only
- Pre-allocate Memory — Create GPath in window_load for static shapes (clock hands, fixed elements). Small dynamic shapes that change position each frame (e.g. character silhouettes at computed coordinates) can use create/destroy in draw functions — this is acceptable for paths with ~3-6 points
- MINUTE_UNIT Only — Never use SECOND_UNIT unless explicitly requested
- Clean Resources — Destroy in unload handlers
- NULL Checks — Verify pointers before use
- Overflow Protection — Use modulo on counters
- Dynamic Bounds — Use
layer_get_bounds()not hardcoded screen sizes - Register Before Open — AppMessage callbacks must be registered before
app_message_open()
File Checklist
Before building:
-
package.jsonwith valid UUID and"targetPlatforms": ["emery"] -
wscriptwith build config -
src/c/main.cwith complete code -
src/pkjs/index.js(if weather/web data needed) -
resources/directory exists
Build: pebble build
Test: pebble install --emulator emery
Screenshot: pebble screenshot --no-open --emulator emery
Output: build/[name].pbw