A Gneiss Rust SDK for pebble watches
Gneiss (pronounced "nice") is a collection of crates that aim to provide a "pure rust" path to writing watchfaces and applications for the pebble watches. It's standalone, you don't need the C SDK or pebble-tool on your machine to build gneiss apps and install them on your watch.
| crate | provides |
|---|---|
| gneiss | The safe SDK: windows and layers, services, AppMessage, resources, logging. |
| gneiss-macros | #[gneiss::entry] and #[gneiss::service] |
| cargo-gneiss | Project templating, build tool for producing loadable .pbw, and installer, like pebble-tool for rust |
| gneiss-sys | FFI surface for calling pebbleOS "syscalls" without linking pebble.a, generated from the upstream SDK by the xtask |
| gneiss-types | Shared definitions of the pebble platforms, etc. no-std for use in gneiss-sys |
| gneiss-build | Build-time resource compiling and bundling, used from your app's build.rs |
Tools/Setup
cargo install cargo-gneiss (or cargo install --path crates/cargo-gneiss from a checkout) gets you cargo gneiss. It builds on stable; the apps it generates pin a nightly in their rust-toolchain.toml, which rustup fetches on the first build.
cargo gneiss new <name> -p emery,<other platforms> [watchface|application] to generate a template app, which you can build and run as is. See crates/gneiss-types/src/lib.rs if you're not sure which platform is which watch. The template depends on the gneiss version matching your cargo-gneiss; to build against a local checkout or fork instead, add to its Cargo.toml:
[]
= { = "/path/to/gneiss/crates/gneiss" }
= { = "/path/to/gneiss/crates/gneiss-build" }
cargo gneiss build <optional platform specifiers> to build your application bundle.
cargo gneiss install --phone <ip> --logs to sideload it on to your watch through the phone app (enable the developer connection in the app first), with --logs printing your app's logs until Ctrl-C. cargo gneiss install --emulator <watch> installs to an emulator that pebble-tool is already running, e.g. one started with pebble install --emulator <watch>.
gneiss passes through watchface features to gneiss_sys, which exports per-watch bindings (re-exported as gneiss::sys) to the upstream API. To declare your app supports a watch, add an emery = ["gneiss/emery"] feature-forward to your Cargo.toml. cargo-gneiss looks for these during build. You can enable your own features with these, so long as one of the things enabled is the gneiss feature.
Alloc
gneiss::entry registers gneiss::alloc::PebbleAlloc, a GlobalAlloc over the PebbleOS malloc/free/realloc. You can supply #[gneiss::entry(global_alloc = your::Allocator)] to use your own. OOMs panic!
Internally, the Allocator interface is used to specify that internal heap allocations via the syscall interface are on the PebbleAlloc heap, regardless of your global allocator. I'd strongly recommend reaching for small buffers, pre/static allocation, bump allocators/arenas, and so on within your application to keep a lid on heap usage and fragmentation, especially on old watches.
Services
The gneiss::entry will call your main(), register your services with the watch, and then hand over to the event loop.
You can annotate free-standing functions with gneiss::service(SomeService) to have the OS call your handler on that event. Some will take arguments to control what kinds of event cause a callback, and some take optional context pointers. See the service module for details.
AppMessage
Name your message keys in Cargo.toml (message_keys = ["Command", "Samples[8]"] under [package.metadata.pebble]) and they're generated as typed keys in a message_keys module, and listed in the bundle for your phone-side companion. Receive with AppMessageService, send with link::Outbox. examples/message_keys shows both directions.
UI
A Window<C, D> loads a C: Layout when it's pushed and drops it when it leaves the stack, and keeps optional persistent data D for its whole life. Layouts own layers and shared resources, Layers own what they show: TextLayer, BitmapLayer, and DataLayer<T> for your own state drawn by your own function.
ui::graphics provides GContext drawing functionality, and implements an embedded-graphics compatible framebuffer over it.
Navigation is queued on a Nav and runs once the current handler returns. Anything that could otherwise alias a window's state takes a UiGuard token: give your main a ui: &mut UiGuard argument to set up and push your first window, since an app with no window on the stack exits.
examples/resources builds a whole screen this way.
Resources
Resources must be declared in Cargo.toml; the gneiss::resource docs end with a declaration for every type. These can currently be of type:
- raw (no processing performed, copied as-is),
- bitmap (Any
imagesupported image -> .pbi bitmap image), - png (Any
imagesupported image -> indexed .png image), - pbi (already existing pebble bitmap, copied raw),
- font (freetype -> .pbf) / pbf (already existing, copied raw),
- icon (app menu icon, a special-case bitmap wired into the app header),
- vibe -> Haptics.
- pdc -> Pebble Draw Command (vector format, image or animation)
- svg -> Pebble Draw Command, compiled from an svg file (or a directory of them for an animation)
Coming soon™:
- mo -> i18n
A resource can ship a different file per watch: name a sibling of the declared file <stem>~<tag>.<ext> and it is used on the platforms carrying that tag. The tags are upstream's: the platform name, color/bw, rect/round, dimensions like 200w, and capabilities like touch. A file may carry several (image~color~rect.png). A sibling naming the platform wins, then the one matching the most tags, then the untagged file. examples/resources demos it.
Logging, panics and size
gneiss::log's macros write to the app log, which cargo gneiss install --logs prints. Panics log their message and location there too. The debug-logs feature (on by default) keeps debug!/verbose!; turn it off to strip them from release builds.
If you're tight on space, building with -Cpanic=immediate-abort drops a few KiB of panic and formatting machinery, at the cost of panics going silent, and lets you format with gneiss::ufmt instead of core::fmt.
Thanks for reading
If this project interests you, do feel free to get in touch with me, quartzshard@quartzshard.com via email or @quartzshard:ethicallysourced.yachts on matrix. I'm unlikely to accept unsolicited PRs for the early stage stuff pre-gneiss while I get an MVP together. Happy programming!
License
MPL-2.0 (see LICENSE), except the vendored headers from the pebble SDK/PebbleOS, which are Apache-2.0, see NOTICE.
On the use of LLMs
This is not a "vibe coding" project and low quality slop is not wanted or welcome. That said - use of LLM tools as research aids, documentation drafting, commit message authoring, mechanical refactors/plumbing/etc, is Fine By Me. Just be sensible in scope and application, like with any other tool. Never commit code you don't understand.