Vorweg
Ein SDK (Software Development Kit) an sich hat erst mal nicht zwingend viel mit Open Source zu tun. Pebble (bzw. Core Devices) bekennt sich jedoch auch klar dazu; PebbleOS sowie die Pebble Smartphone App sind beide Open Source. Daher denke ich, dass das Thema Pebble im kleinen Erfahrungsbericht zur Pebble Time 2 als auch hier mit der Programmierung eines Watchfaces bei GnuLinux.ch recht gut aufgehoben ist.
Das Pebble SDK
Wer ein Watchface für die Pebble Uhren erstellen möchte, hat mehrere Möglichkeiten. Zum einen gibt es den klassischen Ansatz mit dem C SDK. Damit kann man für alle Pebblemodelle entwickeln. Möchte man sich auf die neueren Modelle Time 2 und Round 2 konzentrieren, kann man sich auch für eine reine JavaScript-Variante entscheiden. Egal wofür man sich entscheidet, man kann dann noch wählen, ob man die Entwicklung klassisch auf dem lokalen Rechner machen möchte, oder ob alles in der Cloud (CloudPebble) passieren soll.
Vielleicht liegt es an meinem Alter, aber ich habe mich für das C SDK und die lokale Entwicklung entschieden. 🙂 Mit JavaScript werden wir es trotzdem noch zu tun bekommen.
Die Installation des SDKs ist recht gut beschrieben. Abgesehen von ein paar Paketen (je nach Betriebssystem/Distribution), darunter der Python Paketmanager uv, genügt die Eingabe (als normaler User) des folgenden Befehls:
uv tool install pebble-tool --python 3.13
Danach muss man mit pebble sdk install latest noch das SDK an sich installieren und schon kann es losgehen.
Das Watchface
Das pebble-tool und das SDK sind also bereit. Aber was wollen wir für ein Watchface bauen? Natürlich kann es sich hier nur ein möglichst einfaches Projekt handeln, ein Hauch an Mehrwehrt schadet aber auch nicht. Ich dachte also an ein GNU/Linux.ch Watchface, das folgende Komponenten enthält:
- GNU/Linux-Logo
- Uhrzeit
- Datum
- Titel des zuletzt veröffentlichten Artikels
Der Artikel-Titel soll aus dem RSS-Feed extrahiert und stündlich (immer zu Minute 5) aktualisiert werden.
Dabei verzichten wir bewusst auf eine vernünftige Fehlerbehandlung oder das Abfangen von Eventualitäten. Wenn es keine Bluetooth-Verbindung zum Smartphone gibt oder das Smartphone keine Internetverbindung hat, dann kann natürlich auch nicht auf den RSS-Feed zugegriffen werden. Man könnte darauf reagieren, ggf. den Zugriffsversuch wiederholen etc. Das lassen wir aber mal alles beiseite.
Erste Schritte
Im Folgenden werde ich aus Platzgründen nicht immer den kompletten Code zeigen können. Wer sich den Code in Gänze anschauen möchte, kann ihn hier herunterladen.
Wir halten uns eng an das Watchface Tutorial von Pebble, wo man vieles auch noch einmal genauer nachlesen kann.
Zunächst erstellen wir mit pebble new-project --simple gnulinuxch ein neues Projekt, was folgende Verzeichnisstruktur erstellt:
└── gnulinuxch
├── package.json
├── README.md
├── src
│ └── c
│ └── gnulinuxch.c
└── wscript
Die Datei package.json enthält die wesentlichen Projektinformationen. Da ein Watchface, also eine Ziffernblatt-App, erstellt werden soll, müssen wir "watchface": false in "watchface": true abändern. Außerdem werden hier auch Ressourcen definiert. In unserem Fall ist das die Grafik mit dem Gnu und dem Pinguin. Gekürzt sieht die Datei so aus (MessageKeys benötigen wir später):
{
"name": "gnulinuxch",
[…]
"pebble": {
"displayName": "gnulinuxch",
[…]
"watchapp": {
"watchface": true
},
"messageKeys": [
"RssTitle"
],
"resources": {
"media": [{
"type": "bitmap",
"file": "gnulinuxch.png",
"name": "GLCH_IMAGE"
}]
}
}
}
Von der Grundstruktur her ist ein Watchface recht überschaubar. Nach einer Initialisierung wird auf Systemevents gehorcht. Und zum Schluss müssen die Ressourcen wieder freigegeben werden. GNU/Linux.ch könnte also zunächst so aussehen (wobei da noch nichts passiert):
#include <pebble.h>
static void init() {
// TODO
}
static void deinit() {
// TODO
}
int main(void) {
init();
app_event_loop();
deinit();
}
Von Windows und Layern
In init() erstellen wir als Top-Level Element ein Window, das die eigentlichen Layer enthält. Diese Layer dienen zur Darstellung der Daten, also der Texte, Grafiken usw. Damit also das Laden/Freigeben dieser Layer beim Betreten/Verlassen des Windows durchgeführt wird, packen wir diese Logik in zwei Handler-Funktionen und registrieren diese per window_set_window_handlers(). Ist das gemacht, zeigen wir die aktuelle Zeit mit der eigenen Funktion update_time() an und lassen diese durch einen TickHandler regelmäßig aufrufen. Damit sieht init() etwa so aus:
static void init() {
s_main_window = window_create();
window_set_background_color(s_main_window, GColorWhite);
window_set_window_handlers(
s_main_window,
(WindowHandlers){.load = main_window_load, .unload = main_window_unload});
window_stack_push(s_main_window, true);
update_time();
tick_timer_service_subscribe(MINUTE_UNIT, tick_handler);
}
Um die WindowHandler main_window_load und main_window_unload kümmern wir uns gleich. update_time() und tick_handler() sind überschaubar und weitgehend selbsterklärend. Wir fügen die Funktionen oberhalb der bisherigen Funktionen ein.
static void update_time() {
time_t temp = time(NULL);
struct tm *tick_time = localtime(&temp);
static char s_time_buffer[8];
strftime(s_time_buffer, sizeof(s_time_buffer),
clock_is_24h_style() ? "%H:%M" : "%I:%M", tick_time);
text_layer_set_text(s_time_layer, s_time_buffer);
static char s_date_buffer[16];
strftime(s_date_buffer, sizeof(s_date_buffer), "%a %b %d", tick_time);
text_layer_set_text(s_date_layer, s_date_buffer);
}
static void tick_handler(struct tm *tick_time, TimeUnits units_changed) {
update_time();
}
update_time()holt also die aktuelle Zeit und aktualisiert entsprechende TextLayer damit. Diese und alle anderen Elemente werden übrigens direkt nach den #include Anweisungen in der Datei deklariert. Der tick_handler(), der alle Minute vom System ausgeführt wird, ruft seinerseits lediglich unsere Funktion update_time() auf, sodass Datum und Uhrzeit aktualisiert werden.
Was noch fehlt, ist das Erstellen und Platzieren des Grafik- und des Textlayers. Das macht, wie zuvor erwähnt, der main_window_load Handler. Da wir eine Grafik und vier Texte (Uhrzeit, Datum, Label und Artikel-Titel) haben, ist diese Funktion etwas umfangreicher. Um den Artikel nicht zu sprengen, zeige ich hier nur den Grafik- und den ersten TextLayer. Der Rest sieht ganz ähnlich aus und kann dem Quellcode-Archiv entnommen werden.
static void main_window_load(Window *window) {
// Get information about the Window
Layer *window_layer = window_get_root_layer(window);
GRect bounds = layer_get_bounds(window_layer);
s_glch_bitmap = gbitmap_create_with_resource(RESOURCE_ID_GLCH_IMAGE);
// Create the bitmap layer
s_glch_layer = bitmap_layer_create(
GRect((bounds.size.w / 2) - 50, PBL_IF_ROUND_ELSE(15, 10), 100, 58));
bitmap_layer_set_bitmap(s_glch_layer, s_glch_bitmap);
layer_add_child(window_layer, bitmap_layer_get_layer(s_glch_layer));
// Create the time TextLayer
s_time_layer =
text_layer_create(GRect(0, PBL_IF_ROUND_ELSE(68, 65), bounds.size.w, 50));
text_layer_set_background_color(s_time_layer, GColorClear);
text_layer_set_text_color(s_time_layer, GColorBlack);
text_layer_set_font(s_time_layer,
fonts_get_system_font(FONT_KEY_BITHAM_42_BOLD));
text_layer_set_text_alignment(s_time_layer, GTextAlignmentCenter);
layer_add_child(window_layer, text_layer_get_layer(s_time_layer));
[…]
Die Grafik wird über RESOURCE_ID_GLCH_IMAGE angesprochen und bezieht sich auf unsere PNG-Datei gnulinuxch.png. Diese Ressourcen-ID setzt sich zusammen aus RESOURCE_ID_ und dem Namen, den wir der Grafik in package.json gegeben haben, also GLCH_IMAGE.
Und schließlich müssen die Ressourcen beim Verlassen des Windows (Watchfaces) auch wieder freigegeben werden. deinit() und der verbliebene Handler main_window_unload machen genau das:
static void main_window_unload(Window *window) {
text_layer_destroy(s_time_layer);
text_layer_destroy(s_date_layer);
text_layer_destroy(s_rss_title_layer);
text_layer_destroy(s_rss_label_layer);
gbitmap_destroy(s_glch_bitmap);
bitmap_layer_destroy(s_glch_layer);
}
static void deinit() { window_destroy(s_main_window); }
RSS-Feed und Artikel-Titel
Jetzt kommt der spannende Teil: Wie kommt man an den aktuellen Artikel-Titel von GNULinux.ch? Die Pebble-Uhr selbst kann nicht auf das Internet zugreifen. Hier muss man den Umweg über die JavaScript-Umgebung PebbleKit JS (PKJS) machen, die auf dem Smartphone läuft. Anfragen werden also von der Pebble in JavaScript an die PKJS auf dem Smartphone gestellt. Die Antwort kann dann als AppMessage an den C-Code weitergereicht werden.
C-seitig sieht das recht überschaubar aus. Wir erstellen eine Handler-Funktion, die so eine AppMessage entgegennimmt und den entsprechenden TextLayer damit füttert:
static void inbox_received_handler(DictionaryIterator *iter, void *context) {
Tuple *title_tuple = dict_find(iter, MESSAGE_KEY_RssTitle);
if (title_tuple) {
text_layer_set_text(s_rss_title_layer, title_tuple->value->cstring);
}
}
In init() müssen wir den Handler natürlich noch registieren und fügen folgende Zeilen ein:
app_message_register_inbox_received(inbox_received_handler);
app_message_open(1024, 1024);
Jetzt fehlt noch der JavaScript-Teil. Dafür erstellen wir die Datei src/pkjs/index.js. Sobald die Uhr so weit ist (und das ready-Event gesendet hat), wollen wir sofort und dann zur nächsten „Minute 5“ den RSS-Feed von GNULinux.ch abrufen. Wenn es also 13:31 Uhr ist, soll dies jetzt und dann um 14:05 Uhr passieren. Danach dann um 15:05, 16:05 und so weiter.
function scheduleNextRun() {
var now = new Date();
var nextRun = new Date(now.getFullYear(), now.getMonth(), now.getDate(), now.getHours(), 5, 0, 0);
if (now >= nextRun) {
nextRun.setHours(nextRun.getHours() + 1);
}
var msUntilNextRun = nextRun - now;
setTimeout(function() {
loadRssFeed();
scheduleNextRun();
}, msUntilNextRun);
}
Pebble.addEventListener('ready', function(e) {
loadRssFeed();
scheduleNextRun();
});
Was macht loadRssFeed()? Es setzt einen GET Request an https://gnulinux.ch/rss.xml ab. Im Erfolgsfall (Status 200) wird der erste Titel aus der Antwort extrahiert.
function loadRssFeed() {
var request = new XMLHttpRequest();
request.open('GET', 'https://gnulinux.ch/rss.xml', true);
request.onload = function() {
if (request.status == "200") {
var rssFeed = request.responseText;
var title = extractFirstTitle(rssFeed);
if (title) {
Pebble.sendAppMessage({'RssTitle': title});
} else {
Pebble.sendAppMessage({'RssTitle': 'Kein Titel gefunden.'});
}
} else {
Pebble.sendAppMessage({'RssTitle': 'Fehler beim Laden des Feeds.'});
}
};
request.onerror = function() {
Pebble.sendAppMessage({'RssTitle': 'Netzwerkfehler.'})
};
request.send();
};
Pebble.sendAppMessage() übermittelt den Titel des Artikels an den C-Code. Dabei übergeben wir zwingend den in package.json angegebenen massageKey RssTitle. Im C-Code (siehe oben) fragen wir entsprechend nach der ID MESSAGE_KEY_RssTitle.
Das Extrahieren des Titels übernimmt die Funktion extractFirstTitle() und verwendet hierfür einen einfachen regulären Ausdruck.
function extractFirstTitle(rssFeed) {
const regex = /- \s*
/;
var match = rssFeed.match(regex);
if (!match) return null;
var titleStart = match.index + match[0].length;
var titleEnd = rssFeed.indexOf(' ', titleStart);
if (titleEnd === -1) return null;
return rssFeed.substring(titleStart, titleEnd).trim();
}
Ausprobieren
Das war’s! Unser, zugegeben etwas rudimentäres, GNULinux.ch-Watchface ist fertig. pebble build erstellt uns im build-Verzeichnis die Watchface-Datei gnulinuxch.pbw.
Man benötigt übrigens keine Pebble, um das Watchface ausprobieren zu können. Dafür gibt es nämlich einen Emulator, den wir direkt mit einem Pebble-Kommando aufrufen können. Um das Watchface auf einer Time 2 zu emulieren, schreibt man pebble install --emulator emery build/gnulinuxch.pbw. emery steht hier für das Uhrenmodell. gabbro würde das Watchface auf einer Round 2 anzeigen. Übrigens findet sich im Code öfter das Makro PBL_IF_ROUND_ELSE. Damit kann das Watchface unterscheiden, ob es auf einer rechteckigen oder einer runden Pebble läuft; entsprechend wird der Layer etwas anders platziert.

Unser GNULinux.ch Watchface im Emulator. Links Time 2 (emery), rechts Round 2 (gabbro)
Fazit
Das war eine Menge Code. Vielleicht erschließt es sich am besten, wenn man das Quellcode-Archiv zur Hand nimmt und die C- und JavaScript-Dateien in Gänze vor sich hat. Nach etwas Einarbeitungszeit kommt man sicherlich zu dem Schluss, dass das Erstellen eines Pebble Watchfaces gar nicht so schwer ist. Mit etwas Fantasie fallen einem sicherlich nützliche Anwendungsfälle ein. Es muss auch nicht unbedingt ein Watchface sein; es gibt etwa die World Cup 2026 App, die man im Pebble-Menü aufrufen kann und die einen dann über sämtliche Spieltermine und -ergebnisse der aktuellen Fußballweltmeisterschaft informiert.
