pebbleで天気を表示してみよう

pebbleで天気を表示してみましょう。

イメージとしては以下の様な感じです。

  1. pebbleから天気情報を取得するメッセージをPebbleKit JavaScript Frameworkに投げる
  2. PebbleKit JavaScript Frameworkで緯度経度を取得する
  3. 緯度経度を使って天気予報を教えてくれるWebAPIにアクセスして天気予報を取得する
  4. pebbleに天気予報をメッセージで返す
  5. pebbleで天気予報を表示

やることは多いですが、この形さえ覚えてしまえば、その他のWebAPIも同じ方法で取得することができます。

pebble-sdk-examples

Pebbleの公式サンプルとして、 pebble/pebble-sdk-examples というのがあります。
ここのpebblekit-js/weatherが天気を表示するサンプルそのものです。

今回説明する内容はこのサンプルよりも更に簡略化したものですので、それなりに分かる人はpebble-sdk-examplesを動かした方が早いです。

Cloud Pebbleでプロジェクトを作成

CloudPebbleの機能を使いながら少しずつ作っていきましょう。

プロジェクトの作成

まずは、空のプロジェクトを作成します。

Screen Shot 2014 10 04 at 15 01 54

Window layoutの作成

Windowのレイアウトプログラムを作成します。

SOURCE FILESの隣のADD NEWを押します。

Screen Shot 2014 10 04 at 15 03 07

次に、Window Layout、WIndowNameを mainにします。

Screen Shot 2014 10 04 at 15 03 19

CREATEを押すとファイルが追加され、ウィンドウをレイアウトするための画面が表示されます。

Screen Shot 2014 10 04 at 15 03 30

今回はここに、TOOLKITからText Layerを押して、TextLayerを追加します。

Screen Shot 2014 10 04 at 15 04 27

IDを textlayer_weather にしてください。
あとは好きなように位置、大きさ、フォントを変更して、右側にある保存ボタン(上から2つ目のアイコン)を押します。

main関数の追加

保存したら、保存ボタンのもう1つ下の定規と鉛筆のアイコンを押します。

プログラムのコードが表示されるので、ここにプログラムを書いていきます。

ただし、BEGIN AUTO-GENERATED…から END AUTO_GENERATED…の間には書かないでください。
ここは先程のウィンドウレイアウトを変更して保存すると、上書きされてしまいます。

プログラムのコードの一番最後のところに、main関数を追加して下さい。

[c]
int main() {
show_main();

app_event_loop();

hide_main();
}
[/c]

ビルドとインストールをしてみよう

追加ができたら、実行ボタン(右側のアイコンの一番上のアイコン)を押します。

textlayer_weatherに設定しているテキストが表示されるだけのwatchappが起動したと思います。

Pebble screenshot 2014 10 04 16 56 31

天気予報を取得して文字を表示するプログラム

ここから一気に作ります。というか、分けて説明しにくい><

main.cの int main関数あたりを変更します。

先ほどのmain関数も含めて、次のように変更します。

[c]

void hide_main(void) {
window_stack_remove(s_window, true);
}

// ここから追加

enum WeatherKey {
MESSAGE_WEATHER_KEY = 0x01 // TUPLE_CSTRING
};

static AppSync sync;
static uint8_t sync_buffer[64];

static void sync_error_callback(DictionaryResult dict_error, AppMessageResult app_message_error, void *context) {
APP_LOG(APP_LOG_LEVEL_DEBUG, "App Message Sync Error: %d", app_message_error);
}

static void sync_tuple_changed_callback(const uint32_t key, const Tuple* new_tuple, const Tuple* old_tuple, void* context) {
switch (key) {
case MESSAGE_WEATHER_KEY:
text_layer_set_text(textlayer_weather, new_tuple->value->cstring);
break;

}
}

static void send_cmd(void) {
Tuplet value = TupletInteger(1, 1);

DictionaryIterator *iter;
app_message_outbox_begin(&iter);

if (iter == NULL) {
return;
}

dict_write_tuplet(iter, &value);
dict_write_end(iter);

app_message_outbox_send();
}

int main() {
show_main();

const int inbound_size = 64;
const int outbound_size = 64;
app_message_open(inbound_size, outbound_size);

Tuplet initial_values[] = {
TupletCString(MESSAGE_WEATHER_KEY, ""),
};

app_sync_init(&sync, sync_buffer, sizeof(sync_buffer), initial_values, ARRAY_LENGTH(initial_values),
sync_tuple_changed_callback, sync_error_callback, NULL);

send_cmd();

app_event_loop();

hide_main();
}

[/c]

JavaScriptのファイルを作ります

SOURCE FILESのADD NEWからJavaScriptのファイルを追加します。

Screen Shot 2014 10 04 at 15 39 55

app.jsを開き次のコードを入力します。

[javascript]

/*
* openweathermap.orgから天気予報を取得して情報をPebbleに送る処理
*/
function fetchWeather(latitude, longitude) {
var req = new XMLHttpRequest();

req.open(‘GET’, "http://api.openweathermap.org/data/2.5/weather?" +
"lat=" + latitude + "&lon=" + longitude + "&cnt=1", true);

req.onload = function(e) {
if (req.readyState == 4) {
if(req.status == 200) {

var response = JSON.parse(req.responseText);

if (response && response.weather.length > 0) {
var weather = response.weather[0].main;

Pebble.sendAppMessage({
"weather": weather
});
}

} else {
console.log("Error");
}
}
};

req.send(null);
}

/*
* getCurrentPositionが成功したときの処理
*/
function locationSuccess(pos) {
console.log("Location success");
var coordinates = pos.coords;
fetchWeather(coordinates.latitude, coordinates.longitude);
}

/*
* getCurrentPositionが失敗したときの処理
*/
function locationError(err) {
console.log("Location error");
}

// getCurrentPositionにオプションの設定
var locationOptions = { "timeout": 15000, "maximumAge": 60000 };

// Pebbleのイベント appmessage が発生した時の処理を登録
Pebble.addEventListener("appmessage",
function(e) {
console.log("appmessage");
window.navigator.geolocation.getCurrentPosition(locationSuccess, locationError, locationOptions);
});

[/javascript]

SETTINGSでMESSAGE KEYSを設定する

Key Nameにweather、Key IDに1を入力します。

Screen Shot 2014 10 04 at 16 39 19

入力したら、左下の SAVE CHANGESを押します。

実行します

これで実行すると、画面におおざっぱな天気が文字列で表示されると思います。

説明 Cの部分

メッセージをやり取りするための値の定義

enumは列挙型と呼ばれる型の定義で、WeatherKeyという名前でMESSAGE_WEATHER_KEY を 0x01として定義しています。

[c]
enum WeatherKey {
MESSAGE_WEATHER_KEY = 0x01 // TUPLE_CSTRING
};
[/c]

複数定義したい場合には、カンマで区切って増やしていくことができます。

[c]
enum WeatherKey {
MESSAGE_WEATHER_KEY = 0x01, // TUPLE_CSTRING
MESSAGE_CITY_KEY = 0x02 // TUPLE_CSTRING
};
[/c]

スラッシュ2つの後に書いているTUPLE_CSTRINGというのは特に意味はなく、このキーで渡されるデータはTUPLE_CSTRINGだよ、というメモ書きです。
Cではプログラムと関係のないメモを残したい場合に、スラッシュ2つ連続で書いて始めることでコメントを書くことができます。

[c]
static AppSync sync;
static uint8_t sync_buffer[64];
[/c]

上の行でAppSync という型のsyncという変数を宣言しています。
staticはここで宣言したsyncという変数をこのファイルからしかアクセスできなくするための指定です。

下の行でuint8_t型のsync_bufferという配列を大きさ64で宣言しています。これもstatic指定ですね。
unit8_tは8bitのunsinged intです。unsinged intは符号なしの整数を表す型です。マイナスがない整数ということです。というわけで、uint8_tと指定すると格納できる値は0から255の間の数字になります。

メッセージをもらったときの処理

sync_error_callbackとsync_tuple_changed_callbackはメッセージをやりとりするときに必要となる処理です。
sync_error_callbackはエラーが発生した時の処理で、今回はログにエラーメッセージを出力するだけです。
sync_tuple_changed_callbackは、メッセージの内容が変更されるたびに呼び出される関数です。

[c]
static void sync_error_callback(DictionaryResult dict_error, AppMessageResult app_message_error, void *context) {
APP_LOG(APP_LOG_LEVEL_DEBUG, "App Message Sync Error: %d", app_message_error);
}

static void sync_tuple_changed_callback(const uint32_t key, const Tuple* new_tuple, const Tuple* old_tuple, void* context) {
switch (key) {
case MESSAGE_WEATHER_KEY:
text_layer_set_text(textlayer_weather, new_tuple->value->cstring);
break;

}
}
[/c]

[c]
….

app_sync_init(&sync, sync_buffer, sizeof(sync_buffer), initial_values, ARRAY_LENGTH(initial_values),
sync_tuple_changed_callback, sync_error_callback, NULL);


[/c]

sync_error_callbackの方は当分特に触ることは無いと思うのでこのままでOKです。

sync_tuple_changed_callbackの方は、引数としてkeyにどのメッセージが変更されたかを示す値、new_tupleに変更後の値、old_tupleに変更前の値、contextにはapp_sync_initで設定したcontextが渡されます(今回はapp_sync_initでNULLを指定しているのでNULLです)。

ちなみにNULLは何も無いことを表す値です。
JavaScriptの場合には、nullです(小文字)。

ちなみに小文字と書きましたが、CやJavaScriptでは大文字と小文字は区別されまるので、aとAは違う変数や関数になります。

もう1つちなみに、sync_tuple_changed_callbackで使われているswitchですが、これはスイッチ文と呼ばれるもので、switch(A) で指定したAの内容によって、処理を分岐させることができます。

[c]
switch(A) {
case 1:
// 1の時の処理
break;
case 2:
// 2の時の処理
break;
default:
// 上記以外の時の処理
break;
}
[/c]
breakはswitchやforループを抜けるときの文で、breakが呼ばれると以降の処理を行わずに、switch文を抜けます。

さて話は戻り、app_sync_init関数ですが、どういった引数を渡せばよいのかはAPI DOCに書いてあります。

Pebble SDK: AppSync

Screen Shot 2014 10 05 at 12 36 36

ココを見ると、sync_tuple_changed_callbackはAppSyncTupleChangedCallbackという関数型と同じ定義をすればいいことがわかります。

[c]
typedef void(* AppSyncTupleChangedCallback)(const uint32_t key, const Tuple *new_tuple, const Tuple *old_tuple, void *context)
[/c]

API DOCを見てコールバックを作る場合には、ここの定義をコピペして、

[c]
void callback_func(const uint32_t key, const Tuple *new_tuple, const Tuple *old_tuple, void *context) {
// 何かの処理
}
[/c]

こんな感じで作ればOKです。

メッセージを送る処理

メッセージを送る処理ですが、今回は特に意味のある値を送るというわけではなく、送ったタイミングで処理すればいいだけなので、適当な数値としてTupletInteger(1, 1)を送ります。

[c]
static void send_cmd(void) {
Tuplet value = TupletInteger(1, 1);

DictionaryIterator *iter;
app_message_outbox_begin(&iter);

if (iter == NULL) {
return;
}

dict_write_tuplet(iter, &value);
dict_write_end(iter);

app_message_outbox_send();
}
[/c]

これをmain関数の中で呼び出しています。

main関数

[c]
int main() {
show_main();

const int inbound_size = 64;
const int outbound_size = 64;
app_message_open(inbound_size, outbound_size);

Tuplet initial_values[] = {
TupletCString(MESSAGE_WEATHER_KEY, ""),
};

app_sync_init(&sync, sync_buffer, sizeof(sync_buffer), initial_values, ARRAY_LENGTH(initial_values),
sync_tuple_changed_callback, sync_error_callback, NULL);

send_cmd();

app_event_loop();

hide_main();
}
[/c]

app_message_openはJavaScriptとのメッセージのやり取りを開始するための宣言です。
最初の64はinbound(もらうメッセージ)で必要となるサイズを指定します。後ろの64はoutbound(送るメッセージ)で必要となるサイズを指定します。
ここで指定したサイズのメモリを確保できないと、戻り値としてAPP_MSG_OUT_OF_MEMORYが戻ってきます。
厳密にプログラムを作る場合、ここでAPP_MSG_OUT_OF_MEMORYが戻ってきたら、通信処理を行わないようにするのがベストだと思います。

initial_valuesはメッセージのやり取りが始まる際の一番最初に渡される値です。
メッセージのやり取りは「メッセージをもらったときにイベントが発生する」というよりは「やり取りに使う変数に入っている値が変わった時にsync_tuple_changed_callbackが呼ばれる」というイメージです。
そのため、このinitial_valuesに入れた値でsync_tuple_changed_callbackが呼ばれることで初期化を行います。

app_sync_initは先程も説明をしましたが、メッセージをやり取りする機能に初期値やコールバックを登録しています。

そして最後にsend_cmdを呼んでJavaScript側にメッセージを送り、app_event_loopを開始しています。

説明 JavaScriptの部分

addEventListener

Pebble.addEventListenerは発生するイベント(ここではappmessage)に対して、処理を登録するメソッドです。

[javascript]
// Pebbleのイベント appmessage が発生した時の処理を登録
Pebble.addEventListener("appmessage",
function(e) {
console.log("appmessage");
window.navigator.geolocation.getCurrentPosition(locationSuccess, locationError, locationOptions);
});
[/javascript]

appmessageはpebbleのCのプログラム側からsend_cmdのapp_message_outbox_sendでメッセージが送られてきたタイミングで発生するイベントです。
ここでは、window.navigator.geolocation.getCurrentPositionで、スマートフォンのGPSを使って緯度経度を取得しています。

window.navigator.geolocation.getCurrentPositionはHTML5の Geolocation API です。
getCurrentPositionでは、第1引数に取得に成功した場合のコールバック関数、第2引数にエラー時のコールバック関数、第3引数に取得時のオプションを指定します。第2、第3引数は省略することができます。
ここではそれぞれlocationSuccess, locationError, locationOptionsを指定しています。

緯度軽度の取得に成功した時の処理

緯度経度の取得に成功した時はlocationSuccessが呼ばれます。
locationSuccessでは渡される Position から 座標情報 をもらい、fetchWeather関数に渡しています。

[javascript]
/*
* getCurrentPositionが成功したときの処理
*/
function locationSuccess(pos) {
console.log("Location success");
var coordinates = pos.coords;
fetchWeather(coordinates.latitude, coordinates.longitude);
}
[/javascript]

天気予報の取得 fetchWeather

fetchWeatherでは緯度経度を使って、api.openweathermap.orgから天気情報を取得してPebbleにメッセージを送ります。

[javascript]
/*
* openweathermap.orgから天気予報を取得して情報をPebbleに送る処理
*/
function fetchWeather(latitude, longitude) {
var req = new XMLHttpRequest();

req.open(‘GET’, "http://api.openweathermap.org/data/2.5/weather?" +
"lat=" + latitude + "&lon=" + longitude + "&cnt=1", true);

req.onload = function(e) {
if (req.readyState == 4) {
if(req.status == 200) {

var response = JSON.parse(req.responseText);

if (response && response.weather.length > 0) {
var weather = response.weather[0].main;

Pebble.sendAppMessage({
"weather": weather
});
}

} else {
console.log("Error");
}
}
};

req.send(null);
}
[/javascript]

XMLHttpRequest は指定するURLにアクセスしてデータを取得するためのオブジェクトです。

req.openでアクセスするURLを指定します。
ここでは、http://api.openweathermap.org/data/2.5/weather?lat=43.50&lon=141.14&cnt=1 というようなURLを作成して設定しています。
latとlonが緯度経度になっています。
openweathermapではここで指定する緯度経度に近い地点の天気を教えてくれます。

ちなみにURLの指定の仕方は、http://api.openweathermap.org/data/2.5/weather?q=sapporo-shi,jp といった都市名を指定しても大丈夫です。
pebbleで天気を表示する時計の設定画面を見てみると、GPS情報から取得するか、都市名を指定するかを選べる時計が多いです。

req.onloadでURLからデータの取得が完了した時に行う処理を指定します。

req.sendで処理を開始します。

onload時の処理

まず、reqの状態を確認しています。

readyStateの値は、4がリクエストの動作完了を示しています。それ以外の場合はまだ準備完了されていないときです。詳しくは XMLHttpRequest に書いてあります。

次に req.statusが200かどうか確認していますが、これはHTTPのコードで、200はリクエストが成功した時に返されます。HTTPステータスコード – Wikipedia
404 Not Foundとか、403 Forbiddenなどは見かけたこともあるのではないかと思いますが、その類のコードです。

次からやっとリクエストが成功した場合の処理を書きます。
まず、取得した文字列をJavaScriptで扱えるようデータに変換します。

[javascript]
var response = JSON.parse(req.responseText);
[/javascript]

アクセスした時に取得できるデータは次のようなデータになります。
[javascript]
{
"cod": 200,
"name": "Sapporo-shi",
"id": 2128295,
"coord": {
"lat": 43.08,
"lon": 141.36
},
"sys": {
"sunset": 1412410270,
"sunrise": 1412368520,
"country": "JP",
"message": 0.0304,
"id": 7527,
"type": 1
},
"weather": [
{
"icon": "09d",
"description": "light intensity shower rain",
"main": "Rain",
"id": 520
}
],
"base": "cmc stations",
"main": {
"temp_max": 286.15,
"temp_min": 285.15,
"humidity": 81,
"pressure": 1015,
"temp": 285.69
},
"wind": {
"gust": 13.9,
"deg": 300,
"speed": 8.2
},
"clouds": {
"all": 75
},
"dt": 1412405100
}
[/javascript]

weatherの部分が天気情報で、idが詳細な天気です。idの意味は Weather Conditions を確認して下さい。

pebble-sdk-examples/pebble-js-app.js ではこのidを見て表示する天気の画像を変えています。

今回は Rain というデータを貰っている response.weather[0].main を取得してPebble側に送ります。

ちなみに、JSONデータは、連想配列(オブジェクト)と配列が組み合わさったデータで、{}が連想配列と呼ばれるデータ構造でキーとなる文字列と値が組み合わさったデータです。

[javascript]
var data = {
"key1": "value",
"key2": 123
};

console.log(data["key1"]); // value
console.log(data["key2"]); // 123
[/javascript]

“key1″には、key1と書いても大丈夫です。微妙に色々な書き方ができます。 JavaScript の配列と連想配列の違い – IT戦記 あたりを参考に。

次に配列です。
1つの変数ではなく沢山の変数を一気に用意して添字・インデックスでアクセスできるようにしたものです。

[javascript]
var data = [ "value", 123 ];

console.log(data[0]); // value
console.log(data[1]); // 123
[/javascript]

pebbleへのメッセージ送信

取得した天気の情報をPebble.sendAppMessageで送信します。

[javascript]
Pebble.sendAppMessage({
"weather": weather
});
[/javascript]

ここで指定している”weather”は、CloudPebbleのSETTINGSのPEBBLEKIT JS MESSAGE KEYSで指定している文字列です。
Pebbleの時計側が受け取るとき(かJavaScript側から送られるとき)に設定したKeyIDに変換されます。

なので、最初から 1 を指定しても動作します。

ただ、意味のある文字列で指定したほうが、プログラムを読む時に何をしているかがわかりやすいので、なるべく文字列で指定するようにしましょう。

まとめ

PebbleアプリとPebbleKit JavaScript Frameworkとの通信や天気を取得する方法を説明しましたが、中途半端感満載ですね。

でも、まぁ・・・・・ないよりはマシだろう!