A minimalist barbell percentage + plate calculator for Pebble, written in C.
Enter your training max for an exercise and pl8s shows descending percentages with the exact weight to load and a glyph of the plates needed per side (default 90..50 in 10% steps, or 90..60 in 5% steps — selectable in settings). Large color displays (Pebble Time 2 / agate) draw each plate as a thick disc with the weight printed horizontally in bold on its face and a gym-standard color per size (55 red, 45 blue, 35 yellow, 25 green, 15 pink, 10 white, 5 cyan, 2.5 grey); compact displays keep the thin side-view bars.
- Percentage rows with the loadable weight and per-side plate glyphs (bold horizontal weight labels on large color displays). Default 10% step shows 90..50; switch to 5% steps in settings to see 90, 85, 80, 75, 70, 65, 60 (7 rows)
- Max entry: UP/DN nudge the max in 5 lb steps (hold to repeat), SELECT opens a NumberWindow for direct entry (45-995 lb); confirming ✓ returns to the results
- Settings wizard (hold SELECT): first set the percent step (5% or 10%), then step through each plate size (55/45/35/25/15/10/5/2.5 lb) and set how many the gym has: 0-10 per side, or 99 for unlimited (the default)
- Phone settings page: the Pebble/Rebble phone app shows a gear icon next to pl8s (the app declares
capabilities: ["configurable"]); tapping it opens a hosted webpage with the same settings, and saves push them to the watch over AppMessage. The watch keeps the on-device wizard too - Always loadable weights: percentages are snapped to the nearest weight the configured inventory can build exactly. With unlimited plates every 2.5 lb step is reachable; with a limited inventory the weight snaps DOWN to the heaviest buildable weight, never showing a load you can't make
- Backlight stays on while the app is in the foreground (returns to automatic control on exit); the user-facing "backlight timeout" watch setting (
Settings > Display > Backlight) is a separate system control - Persistent: max, percent step, and plate counts survive app exit
- 45 lb Olympic bar assumed
![]() |
![]() |
![]() |
![]() |
| 10% steps: 5 rows, 90 down to 50 | 5% steps: seven rows, 90 down to 60 | Max entry (NumberWindow) | Wizard (hold SEL): plate counts, 99 = unlimited |
These are the appstore assets: every platform has its own screenshot set and a 720x320 banner under appstore/, regenerated with tools/appstore_shots.py and tools/make_appstore_assets.py.
The config page is a single self-contained file: config.html. It must be hosted at an http (s) URL the phone can open — the page location is set in src/pkjs/index.js (CONFIG_URL):
var CONFIG_URL = 'https://cmalec.github.io/pl8s/config.html';GitHub Pages works out of the box for this repo. The page talks to the watch via the standard Pebble config protocol, and it mirrors everything the on-watch settings wizard can change — percent step, exercise max, and per-plate counts — prefilled with the current values:
- The watch pushes its current settings when asked:
src/pkjs/index.jssends aSYNCAppMessage on JS ready and on every gear tap;src/c/main.creplies withSTEP,MAX_LB, and allPLATE_*values. The JS side caches them in localStorage and appends them to the config URL as query params. - The phone app's gear fires
showConfiguration, which opensCONFIG_URL + '?step=..&max=..&p55=..&p2p5=..';config.htmlprefills the form from those params. - Save redirects to
pebblejs://close#<json>; the phone decodes it and fireswebviewclosedwith the settings. src/pkjs/index.jsforwards them to the watch withPebble.sendAppMessage, keyed by themessageKeysinpackage.json(STEP,MAX_LB,PLATE_*).src/c/main.c(inbox_received_handler) validates, persists, and redraws.
The watch-only wizard (hold SEL) and the phone page can be used interchangeably; the watch pushes its state after the wizard closes too, so the page always opens showing current values.
Message keys are shared between the C build (MESSAGE_KEY_* from
build/include/message_keys.auto.h) and the JS runtime (message_keys.json), both generated from messageKeys in package.json.
pebble login
pebble build # build for all targetPlatforms
pebble install --emulator emery # install on the emery emulator
pebble install --cloudpebble # install to a paired phone
npm test # host unit test of the plate math
npm run test:emulator # drive the wizard on the emulatorTesting phone-side pushes from the emulator. --int takes the numeric message keys the SDK assigns, listed in build/js/message_keys.json after a build, and all pairs go after one --int:
pebble send-app-message --emulator emery --int 10000=5 # STEP
pebble send-app-message --emulator emery --int 10001=315 10009=3 # MAX_LB, PLATE_2P5Built for emery (Pebble Time 2) first, with layout adaptations for the smaller rect displays (basalt/diorite/flint/aplite) and round displays (chalk/gabbro). On the round displays the rows step inward with the circular mask: each row's text and its plate glyph are inset to the widest chord that row has, so nothing is cut off by the glass.
src/c/ C source for the watchapp (plate_math.c is SDK-free: math only)
src/pkjs/ Phone-side JavaScript (config page bridge)
config.html Self-contained phone settings page (host it on GitHub Pages)
test/ Host unit tests (C), run by npm test
tools/ Emulator test, screenshot OCR, asset generators (Python)
resources/ Images, fonts, and other bundled resources
package.json Project metadata (UUID, platforms, resources, message keys)
wscript Build rules - usually no need to edit
The plate math is plain C with no Pebble SDK dependency, so it is unit tested on the host against the same source the watchapp links:
npm testtest/test_plate_math.c compares every row against an independent oracle (a memoized exhaustive search over plate multisets, deliberately a different algorithm from the app's reachable-load bitset) across the whole max range, both step modes, curated and random limited inventories, and every inventory with 0..2 of each size. It also pins the plates the glyph draws, and the rows that a limited inventory has to snap down.
The app itself only really runs on the watch, so the wizard, persistence and rendering are checked on the emulator:
pebble build
npm run test:emulatorThat restarts the emery emulator, drives the settings wizard with button presses and reads the weights back off the screen with tools/ocr_weights.py, so a wrong weight fails the run instead of just changing the picture.
Full SDK docs, tutorials, and API reference: https://developer.repebble.com




