スマホと接続された多種の Pebble ウォッチアプリとウォッチフェイスは、ここで説明されているような共通のタスクを実行します。これらのベストプラクティスに従うことで、各タスクの実装の質を高め、一般的なバグを回避できます。
PebbleKit JS を介してウォッチから電話にデータを送信したいアプリは、JavaScript の ready イベントが発生するまで待たなければなりません。これは、電話が起動中のアプリの JavaScript コンポーネントをロードしたことを示します。この JavaScript コードが appmessage イベントリスナーを実装している場合、データを受信する準備ができています。
PebbleKit JS からデータを受信のみするウォッチアプリは、
readyイベントを待つ必要はありません。さらに、Android コンパニオンアプリはIntentシステムのおかげでこのようなイベントを待つ必要はありません。iOS コンパニオンアプリは-watchDidConnect:を待つ必要があります。
シンプルな方法は、package.json でキーを定義することです。このキーは、ウォッチアプリによって JS 環境がデータ交換の準備ができていることを意味すると解釈されます:
"messageKeys": [
"JSReady"
]
ウォッチアプリは、ready イベントが発生したかどうかを記述する変数を実装する必要があります。以下に例を示します:
static bool s_js_ready;
これはヘッダーファイルにエクスポートでき、アプリの他の部分で確認できます。待機中のアプリの部分は、retryメカニズムの一環としてこれを呼び出す必要があります。
bool comm_is_js_ready() {
return s_js_ready;
}
この変数の状態は、ready イベントがキーを送信させるときに true に設定されるまで false になります:
Pebble.addEventListener("ready", function () {
console.log("PebbleKit JS ready.");
// Update s_js_ready on watch
Pebble.sendAppMessage({ JSReady: 1 });
});
このキーは、アプリの AppMessageInboxReceived 実装で解釈される必要があります:
static void inbox_received_handler(DictionaryIterator *iter, void *context) {
Tuple *ready_tuple = dict_find(iter, MESSAGE_KEY_JSReady);
if(ready_tuple) {
// PebbleKit JS is ready! Safe to send messages
s_js_ready = true;
}
}
Bluetooth 接続のワイヤレスおよびステートフルな性質により、ウォッチと電話の間で送信される一部のメッセージは失敗する可能性があります。これらの失敗に対処するための試行錯誤された方法は、「タイムアウトとリトライ」メカニズムを実装することです。このようなスキームの下で:
メッセージが送信され、タイマーが開始されます。
メッセージが正常に送信された場合(およびオプションで返信が受信された場合)、タイマーはキャンセルされます。
メッセージが正常に送信される前にタイマーが経過した場合、メッセージは再試行されます。失敗の性質に応じて、接続を飽和させないように適切なリトライ間隔(数秒など)が使用されます。
タイムアウトが発生してメッセージが再送信されるまでの間隔は、状況によって異なる場合があります。最初の失敗はかなり迅速に(1 秒)再試行する必要があり、連続した失敗が発生するにつれて間隔が増加します。接続が利用できない場合、タイマー間隔はさらに長くする必要があるか、接続が復元されるまで待つ必要があります。
以下の例は、メッセージの送信とタイムアウトタイマーのスケジューリングを示しています。最初のステップは、タイムアウトタイマーのハンドルを宣言することです:
static AppTimer *s_timeout_timer;
メッセージが送信されたら、タイマーをスケジュールする必要があります:
static void send_with_timeout(int key, int value) {
// Construct and send the message
DitionaryIterator *iter;
if(app_message_outbox_begin(&iter) == APP_MSG_OK) {
dict_write_int(iter, key, &value, sizeof(int), true);
app_message_outbox_send();
}
// Schedule the timeout timer
const int interval_ms = 1000;
s_timout_timer = app_timer_register(interval_ms, timout_timer_handler, NULL);
}
AppMessageOutboxSent が呼び出された場合、メッセージは成功であり、タイマーはキャンセルされる必要があります:
static void outbox_sent_handler(DictionaryIterator *iter, void *context) {
// Successful message, the timeout is not needed anymore for this message
app_timer_cancel(s_timout_timer);
}
ただし、メッセージの成功を判断できる前にタイムアウトタイマーが経過するか、期待される返信が受信されない場合、timout_timer_handler() へのコールバックを使用してユーザーに失敗を通知し、別の試行をスケジュールしてメッセージを再試行する必要があります:
static void timout_timer_handler(void *context) {
// The timer elapsed because no success was reported
text_layer_set_text(s_status_layer, "Failed. Retrying...");
// Retry the message
send_with_timeout(some_key, some_value);
}
あるいは、AppMessageOutboxFailed が呼び出された場合、メッセージの送信に失敗したことを示します。場合によっては即時失敗します。タイムアウトタイマーはキャンセルし、チャネルの飽和を避けるため、追加の遅延時間(再試行間隔)を置いてメッセージを再送信する必要があります:
static void outbox_failed_handler(DictionaryIterator *iter,
AppMessageResult reason, void *context) {
// Message failed before timer elapsed, reschedule for later
if(s_timout_timer) {
app_timer_cancel(s_timout_timer);
}
// Inform the user of the failure
text_layer_set_text(s_status_layer, "Failed. Retrying...");
// Use the timeout handler to perform the same action - resend the message
const int retry_interval_ms = 500;
app_timer_register(retry_interval_ms, timout_timer_handler, NULL);
}
注記:メッセージ送信が失敗するあらゆる状況では、必ずメッセージの再送信を呼び出す必要があります。そうしなければ、自動化された「タイムアウトと再試行」メカニズムの目的が達成されません。ただし、試行回数と試行間隔については、開発者が決定します。
SDK 3.8 まで、AppMessage バッファのサイズは、1 つのメッセージで大量のデータを送信することを容易にしませんでした。各送信ボックスに最大 8k の現在のバッファサイズにより、複数の連続したデータ項目を効率的に送信する必要性は軽減されましたが、この手法は依然として重要です。たとえば、センサーデータをできるだけ速く送信するには、連続したメッセージの慎重なスケジューリングが必要です。
メッセージの送信にかかる時間の保証がないため、タイマーを使用して複数のメッセージを次々とスケジュールするだけでは信頼性がありません。はるかに良い方法は、AppMessage API によって提供されるコールバックをうまく利用することです。
たとえば、AppMessageOutboxSent コールバックを使用して、電話への次のメッセージを安全にスケジュールできます。前のメッセージがその時点で相手側によって確認されているためです。以下は項目の配列の例です:
static int s_data[] = { 2, 4, 8, 16, 32, 64 };
#define NUM_ITEMS sizeof(s_data);
変数を使用して、次に送信される現在のリスト項目インデックスを追跡できます:
static int s_index = 0;
メッセージが送信されたら、このインデックスを使用して次のメッセージを構築します:
注意: 有用なキースキームは、項目の配列インデックスをキーとして使用することです。PebbleKit JS の場合、そのキーの数を
package.jsonで宣言する必要があります。たとえば:someArray[6]
static void outbox_sent_handler(DictionaryIterator *iter, void *context) {
// Increment the index
s_index++;
if(s_index < NUM_ITEMS) {
// Send the next item
DictionaryIterator *iter;
if(app_message_outbox_begin(&iter) == APP_MSG_OK) {
dict_write_int(iter, MESSAGE_KEY_someArray + s_index, &s_data[s_index], sizeof(int), true);
app_message_outbox_send();
}
} else {
// We have reached the end of the sequence
APP_LOG(APP_LOG_LEVEL_INFO, "All transmission complete!");
}
}
これにより、最後のデータ項目が送信され、インデックスが項目の総数と等しくなるまで繰り返されるコールバックループが生成されます。この手法は、タイムアウトとリトライメカニズムと組み合わせて、送信が失敗した場合に特定の項目を再試行できます。これは、受信したデータ項目のギャップを回避する良い方法です。
電話側では、データ項目は同じ順序で受信されます。類似の index 変数を使用して、どの項目が受信されたかを追跡します。このプロセスは、以下に示す例と同様になります:
var NUM_ITEMS = 6;
var keys = require("message_keys");
var data = [];
var index = 0;
Pebble.addEventListener("appmessage", function (e) {
// Store this data item
data[index] = e.payload[keys.someArray + index];
// Increment index for next message
index++;
if (index == NUM_ITEMS) {
console.log("Received all data items!");
}
});
逆に、PebbleKit JS の Pebble.sendAppMessage() の success コールバックは、ウォッチへの次のメッセージを送信する同等の安全な時間です。
これを実現するサンプル実装を以下に示します。Pebble.sendAppMessage() でメッセージが送信された後、success コールバックは、インデックスが送信される最後のリスト項目のインデックスよりも大きくなり、送信が完了するまで、sendNextItem() 関数を繰り返し呼び出します。繰り返しになりますが、送信される項目を追跡するためにインデックス変数が維持されます:
var keys = require("message_keys");
function sendNextItem(items, index) {
// Build message
var key = keys.someArray + index;
var dict = {};
dict[key] = items[index];
// Send the message
Pebble.sendAppMessage(
dict,
function () {
// Use success callback to increment index
index++;
if (index < items.length) {
// Send next item
sendNextItem(items, index);
} else {
console.log("Last item sent!");
}
},
function () {
console.log("Item transmission failed at index: " + index);
}
);
}
function sendList(items) {
var index = 0;
sendNextItem(items, index);
}
function onDownloadComplete(responseText) {
// Some web response containing a JSON object array
var json = JSON.parse(responseText);
// Begin transmission loop
sendList(json.items);
}
ウォッチアプリ側では、項目は AppMessageInboxReceived ハンドラーで同じ順序で受信されます:
#define NUM_ITEMS 6
static int s_index;
static int s_data[NUM_ITEMS];
static void inbox_received_handler(DictionaryIterator *iter, void *context) {
Tuple *data_t = dict_find(iter, MESSAGE_KEY_someArray + s_index);
if(data_t) {
// Store this item
s_data[index] = (int)data_t->value->int32;
// Increment index for next item
s_index++;
}
if(s_index == NUM_ITEMS) {
// We have reached the end of the sequence
APP_LOG(APP_LOG_LEVEL_INFO, "All transmission complete!");
}
}
この一連のイベントは PebbleKit JS 用に実証されていますが、同じ手法は、Pebble に多くのデータ項目を送信したい Android または iOS コンパニオンアプリにも正確に適用できます。
この例の完全なソースコードは、GitHub の list-items-example リポジトリから入手できます。
開発者が達成したい一般的なタスクは、動的にロードされた画像リソースを表示することです(たとえば、MMS 写真やウェブサービスから取得したニュース項目のサムネイルを表示するなど)。一部の画像はアプリで利用可能な最大バッファサイズよりも大きい可能性があるため、上記のリスト送信に示されている手法もここで有用であることがわかります。画像は本質的に色バイト値のリストであるためです。
ウェブからダウンロードした画像データを表示するために利用可能な方法は 2 つあります:
png 画像をダウンロードし、圧縮データを送信し、gbitmap_create_from_png_data() を使用して解凍します。これには、送信するデータが少なくて済みますが、画像の正確な形式によっては失敗しやすい可能性があります。画像は互換性のあるパレット(1、2、4、または 8 ビット)である必要があり、処理されるときに圧縮されたコピー、非圧縮のコピー、および約 2k のオーバーヘッドのための十分なメモリがあるほど小さい必要があります。
png 画像をダウンロードし、クラウドまたは PebbleKit JS で画像ピクセルバイトの配列に解凍し、空白の GBitmap の data メンバーにピクセルデータを送信します。各バイトは互換性のある Pebble カラー形式(ARGB あたり 2 ビット)である必要があります。このプロセスは、ダウンロードする画像を事前にフォーマットすることで簡素化できます。リサイズやパレット削減はローカルで行うのが困難であるためです。
上記で説明した 2 つの方法の中で最も高速で複雑でない方法として、圧縮 PNG 画像を表示する方法の例をここで説明します。表示される画像は、the HTML 5 logo です:

注意: 上記の画像は、互換性のためにリサイズおよびパレット化されています。
PebbleKit JS でこの画像をダウンロードするには、XmlHttpRequest オブジェクトを使用します。画像データを正しい形式で取得するには、responseType を 'arraybuffer' として指定することが重要です:
function downloadImage() {
var url =
"http://localhost:4000/assets/images/guides/pebble-apps/communications/html5-logo-small.png";
var request = new XMLHttpRequest();
request.onload = function () {
processImage(this.response);
};
request.responseType = "arraybuffer";
request.open("GET", url);
request.send();
}
レスポンスが受信されると、processImage() が呼び出されます。受信したデータは、符号なしバイトの配列に変換する必要があります。これは Uint8Array の使用を通じて実現されます。このプロセスを以下に示します(完全な例については、png-download-example リポジトリを参照してください):
function processImage(responseData) {
// Convert to a array
var byteArray = new Uint8Array(responseData);
var array = [];
for (var i = 0; i < byteArray.byteLength; i++) {
array.push(byteArray[i]);
}
// Send chunks to Pebble
transmitImage(array);
}
画像データが変換されたので、Pebble への送信を開始できます。高レベルでは、JS 側はチャンクで画像データを送信し、増分配列インデックスを使用して C 側のミラー配列でのデータの保存を調整します。画像データのダウンロードでは、指定された目的のために次のキーが使用されます:
| Key | Purpose |
|---|---|
Index |
現在のチャンクが保存されるべき配列インデックス。各チャンクが送信されるにつれて大きくなります。 |
DataLength |
ダウンロードされるデータ配列全体の長さ。画像は圧縮されているため、これは画像の幅と高さの積ではありません。 |
DataChunk |
チャンクの画像データ。 |
ChunkSize |
このチャンクのサイズ。 |
Complete |
画像転送が完了したことを示すために使用されます。 |
シーケンスの最初のメッセージは、圧縮画像データを保存するために割り当てるメモリの量を C 側に伝える必要があります:
function transmitImage(array) {
var index = 0;
var arrayLength = array.length;
// Transmit the length for array allocation
Pebble.sendAppMessage(
{ DataLength: arrayLength },
function (e) {
// Success, begin sending chunks
sendChunk(array, index, arrayLength);
},
function (e) {
console.log("Failed to initiate image transfer!");
}
);
}
このメッセージが成功した場合、実際の画像データの送信は、sendChunk() への最初の呼び出しで開始されます。この関数は、次のチャンクのサイズ(AppMessage インボックスバッファのサイズ、またはデータの残りのいずれか小さい方)を計算し、それがスライスされた配列内のインデックス、チャンクの長さ、および実際のデータ自体を含む辞書を組み立てます:
function sendChunk(array, index, arrayLength) {
// Determine the next chunk size
var chunkSize = BUFFER_SIZE;
if (arrayLength - index < BUFFER_SIZE) {
// Resize to fit just the remaining data items
chunkSize = arrayLength - index;
}
// Prepare the dictionary
var dict = {
DataChunk: array.slice(index, index + chunkSize),
ChunkSize: chunkSize,
Index: index,
};
// Send the chunk
Pebble.sendAppMessage(
dict,
function () {
// Success
index += chunkSize;
if (index < arrayLength) {
// Send the next chunk
sendChunk(array, index, arrayLength);
} else {
// Complete!
Pebble.sendAppMessage({ Complete: 0 });
}
},
function (e) {
console.log("Failed to send chunk with index " + index);
}
);
}
各チャンクが送信された後、インデックスは送信されたばかりのチャンクのサイズで増分され、配列の総長と比較されます。インデックスが配列のサイズを超える場合、ループはすべてのデータを送信しました(配列が最大メッセージサイズよりも小さい場合、これは単一のチャンクだけである可能性があります)。AppKeyComplete キーが送信され、画像が完成して表示の準備ができていることを C 側に通知します。
前のセクションでは、PebbleKit JS を使用して画像をダウンロードして C 側に送信するプロセスについて説明しました。このデータを保存して表示するプロセスについてここで説明します。両方の部分が調和して機能する場合にのみ、ウェブから画像を正常に表示できます。
プロセスの大部分は、ウォッチアプリの AppMessageInboxReceived ハンドラー内で行われ、各キーの存在が検出され、画像を再構築するための適切なアクションが実行されます。
最初に予期される項目は、転送されるデータの合計サイズです。これは記録され(後で gbitmap_create_from_png_data() で使用するため)、チャンクを保存するために使用されるバッファがこの正確なサイズに割り当てられます:
static uint8_t *s_img_data;
static int s_img_size;
// Get the received image chunk
Tuple *img_size_t = dict_find(iter, MESSAGE_KEY_DataLength);
if(img_size_t) {
s_img_size = img_size_t->value->int32;
// Allocate buffer for image data
img_data = (uint8_t*)malloc(s_img_size * sizeof(uint8_t));
}
データサイズを含むメッセージが確認されると、JS 側は sendChunk() でチャンクの送信を開始します。これらの後続のメッセージの 1 つが受信されると、3 つのキー(DataChunk、ChunkSize、および Index)を使用して、そのチャンクのデータを配列の正しいオフセットに保存します:
// An image chunk
Tuple *chunk_t = dict_find(iter, MESSAGE_KEY_DataChunk);
if(chunk_t) {
uint8_t *chunk_data = chunk_t->value->data;
Tuple *chunk_size_t = dict_find(iter, MESSAGE_KEY_ChunkSize);
int chunk_size = chunk_size_t->value->int32;
Tuple *index_t = dict_find(iter, MESSAGE_KEY_Index);
int index = index_t->value->int32;
// Save the chunk
memcpy(&s_img_data[index], chunk_data, chunk_size);
}
最後に、配列インデックスが JS 側のデータ配列のサイズを超えると、AppKeyComplete キーが送信され、データを GBitmap に変換するトリガーになります:
static BitmapLayer *s_bitmap_layer;
static GBitmap *s_bitmap;
// Complete?
Tuple *complete_t = dict_find(iter, MESSAGE_KEY_Complete);
if(complete_t) {
// Create new GBitmap from downloaded PNG data
s_bitmap = gbitmap_create_from_png_data(s_img_data, s_img_size);
// Show the image
if(s_bitmap) {
bitmap_layer_set_bitmap(s_bitmap_layer, s_bitmap);
} else {
APP_LOG(APP_LOG_LEVEL_ERROR, "Error creating GBitmap from PNG data!");
}
}
最終的な結果は、ウェブからダウンロードされた圧縮 PNG 画像が Pebble ウォッチアプリに表示されます。

この例の完全なソースコードは、GitHub の png-download-example リポジトリから入手できます。