仮想通貨NEMのハーベストの状態を監視するPebbleWatchFaceアプリ

  私の初回の記事(nem Advent Calendar 2017 | 10日目) にて、"PebbleはNEMアカウントの状態監視等に使えるよね"というお話をしました。で、実際にNEMのハーベスト(委任)状態を監視するWatchFaceアプリを作ってみたので公開します。(なお、一部作成中の部分が有ります)

はじめに

 まず初めに、今回はWatchFaceの雛形として、PebbleチュートリアルPart1が済んでいる状態から始めています。Part1が済んでいる状態というのはつまり、Pebble上に現在時刻を表示させただけの状態ということです。ここがスタートになります。

動作の概要

 細かいコードの断片を載せてアレコレ説明するのも分かり辛いと思う&書くの面倒なので、動作の概要を図にして載せておきます。
20171226-1.png
 基本動作は上記図の流れです。ここに、各所例外が発生した時の対応などを盛り込んでいくことになります。

 また、今回監視するアドレスはハードコードするものとしています。設定画面を別で作ってPhone側からの操作で指定できるPebbleClayなるものがあるようで、それを使えばアドレスを設定画面から買えられるようになるようですが、今回は含みません。ここが一部作成中の部分になっています。

例外など追加要素

 通信がうまく行かなかったりした時のために以下の要素を盛り込みます。
1. AppMesageのやり取りなど、Pebbleの機能内で起きた例外への対応
2. NISとの通信がうまく行かなかった時に発生する例外への対応

 1についてはPebbleチュートリアルPart3で出てくる内容をそのまま使い回すことが出来ます。
 2については今回は10回エラー又はタイムアウトが発生すると、通信を諦めて"?"の画像を表示することとしています。また、通信を行うNISサーバーは予めいくつかのIPアドレスをプリセットとして準備しておき、その中からランダムで選択されるものとしています。

修正(加筆)

 載せたコードに根本的な誤りが有りまして、リモートの状態を監視しているだけになっていました。ハーベストをしているかどうかの確認はアカウントステータスのメタデータ内の"LOCK""UNLOCK"の部分を監視しなくてはいけません。また、委任先ノードにAPIアクセスしないとLOCKで返ってきてしまうので正確な状態が分からないということでした。(普段API叩いてないのがバレてしまった)
 
 ということで後日修正します。(まだ修正できていません)(2018/01/)修正しました。jsのコードが仕組みからごっそり変わっています。

 何となくこっちの方が良いかなと思って状態をシグナルに変換してCに送っていたので、Cの方は修正なしで、jsが修正になります。(ちょっとした差でこういう違いが出るんですねー。)

書いたもの

 乱暴ですが、以下書いたものだけ載せて説明をおわります。

main.c
#include <pebble.h>

static Window *s_main_window;
static TextLayer *s_time_layer;
static GFont s_time_font;
static BitmapLayer *s_harvmonitor_layer;
static GBitmap *s_harvest_bitmap;
//harvest_status
//   1:harvesting
//  -1:not harvest
//   0:unknown
// 100:loading
static int s_harvest_signal;

// phone側からwatch側に送られるmessageの受信に成功した時の処理
// チュートリアル内では"AppMessageInobxReceived(callback)"と呼ばれる
static void inbox_received_callback(DictionaryIterator *iterator, void *context){
  Tuple *harvest_signal_tuple = dict_find(iterator, MESSAGE_KEY_SIGNAL);
  if(harvest_signal_tuple){
    //バッファに結果を書き込み
    s_harvest_signal = (int)harvest_signal_tuple->value->int32;
    //harvest_status
    // 1:harvesting
    //-1:not harvest
    // 0:unknown
    switch(s_harvest_signal){
      case 1:
        s_harvest_bitmap = gbitmap_create_with_resource(RESOURCE_ID_IMAGE_HARV);
        break;
      case -1:
        s_harvest_bitmap = gbitmap_create_with_resource(RESOURCE_ID_IMAGE_NO_HARV);
        break;
      case 0:
        s_harvest_bitmap = gbitmap_create_with_resource(RESOURCE_ID_IMAGE_NO_DATA);
        break;
      default:
        s_harvest_bitmap = gbitmap_create_with_resource(RESOURCE_ID_IMAGE_NO);
    }
     bitmap_layer_set_bitmap(s_harvmonitor_layer, s_harvest_bitmap);
  }
}

//messageを取り漏らした時のエラー処理
static void inbox_dropped_callback(AppMessageResult reason, void *context){
  APP_LOG(APP_LOG_LEVEL_ERROR, "Message dropped!");
}

//message送信を失敗した時のエラー処理
static void outbox_failed_callback(DictionaryIterator *iterator, AppMessageResult reason, void *context){
  APP_LOG(APP_LOG_LEVEL_ERROR, "Outbox send failed!");
}

//message送信に成功した時の処理
static void outbox_sent_callback(DictionaryIterator *iterator, void *context){
  APP_LOG(APP_LOG_LEVEL_INFO, "Outbox send success!");
}

static void update_time(){
  //tmストラクチャの取得
  time_t temp = time(NULL);
  struct tm *tick_time = localtime(&temp);

  //8byteのバッファを用意してそのバッファの中に現在時刻を書き込み
  static char s_buffer[8];
  strftime(s_buffer, sizeof(s_buffer), clock_is_24h_style() ? "%H:%M" : "%I:%M", tick_time);

  text_layer_set_text(s_time_layer, s_buffer);
}

static void tick_handler(struct tm *tick_time, TimeUnits units_changed){
  update_time();

  //以下、harvestモニターをアップデートするためにphoneにappMessageを送信する処理
  //30分に1回appMessageをphone側に送信し、phoneがappMessageを受信することで、phoneのPebbleKitJS内で
  //設定したイベントリスナが起動、イベントリスナ内のgetInfo()によってappMessageがwatch側に送られ、watch側に
  //登録したcallback関数が動き、表示を更新する
  //但し、harvest_status=0(UNKNOWN) or 100(LOADING)のときは、更新頻度を10分に一回にする
  int update_min = 30;
  if(s_harvest_signal ==0 || s_harvest_signal==100){
    update_min = 10; 
  }

  if(tick_time->tm_min % update_min == 0){
    //ロード中はsignal=100,loadingアイコンにする
    s_harvest_signal = 100;
    s_harvest_bitmap = gbitmap_create_with_resource(RESOURCE_ID_IMAGE_LOAD);
    bitmap_layer_set_bitmap(s_harvmonitor_layer, s_harvest_bitmap);

    DictionaryIterator *iter;
    app_message_outbox_begin(&iter);

    dict_write_uint8(iter,0,0);

    app_message_outbox_send();
  }
}

static void main_window_load(Window *window){
  //ウィンドウに関する情報を取得
  Layer *window_layer = window_get_root_layer(window);
  GRect bounds = layer_get_bounds(window_layer);

  //TextLayerとその境界の作成
  s_time_layer = text_layer_create(GRect(0 ,
                                         PBL_IF_ROUND_ELSE(40,38) ,
                                         bounds.size.w , 50));



  //GFontの設定
  s_time_font = fonts_load_custom_font(resource_get_handle(RESOURCE_ID_FONT_CHAMPAGNE_48));

  //watchfaceとなるように各種設定を実装していく(フォントの変更含む)
  text_layer_set_background_color(s_time_layer, GColorClear);
  text_layer_set_text_color(s_time_layer, GColorWhite);
  text_layer_set_text(s_time_layer, "00:00");
  text_layer_set_font(s_time_layer, fonts_get_system_font(FONT_KEY_BITHAM_42_BOLD));
  text_layer_set_font(s_time_layer, s_time_font);
  text_layer_set_text_alignment(s_time_layer, GTextAlignmentCenter);

  //GBitmapの作成
  //初期状態は空の丸
  s_harvest_bitmap = gbitmap_create_with_resource(RESOURCE_ID_IMAGE_NO);
  //GBitmapを表示するためのBitmapLayerの作成
  s_harvmonitor_layer = bitmap_layer_create(GRect(bounds.size.w /2 -12 ,
                                                  PBL_IF_ROUND_ELSE(96,104) ,
                                                  24 , 24));
  //BitmapLayerにGBitmapをセット
  bitmap_layer_set_bitmap(s_harvmonitor_layer, s_harvest_bitmap);

  //親レイヤーにテキストとビットマップレイヤを追加
  layer_add_child(window_layer, bitmap_layer_get_layer(s_harvmonitor_layer));
  layer_add_child(window_layer, text_layer_get_layer(s_time_layer));
}

static void main_window_unload(Window *window){
  //テキストレイヤを削除
  text_layer_destroy(s_time_layer);
  //フォントを元に戻す
  fonts_unload_custom_font(s_time_font);
}

static void init(){
  //上記で作成したcallbackの登録
  app_message_register_inbox_received(inbox_received_callback);
  app_message_register_inbox_dropped(inbox_dropped_callback);
  app_message_register_outbox_failed(outbox_failed_callback);
  app_message_register_outbox_sent(outbox_sent_callback);
   //AppMessgeをオープンの状態にする
  const int inbox_size  = 128; //set buffer size
  const int outbox_size = 128; //set buffer size
  app_message_open(inbox_size,outbox_size);


  //pebble内のアプリに時間を提供してくれるユニットへアプリを登録する
  //これによってアプリを開いた時にアプリは開いた時の時間を取得することが出来る
  tick_timer_service_subscribe(MINUTE_UNIT, tick_handler);

  //新規ウィンドウ要素の追加とポインタの設定
  s_main_window = window_create();

  //ウィンドウ内の要素に対するハンドラを設定
  window_set_window_handlers(s_main_window,(WindowHandlers){
    .load   = main_window_load,
    .unload = main_window_unload
  });

  //背景の色をセット
  window_set_background_color(s_main_window, GColorDarkGray);

  //ウィンドウをpebbleに表示、2番目の引数はanimated(=true)
  window_stack_push(s_main_window , true);

  //テキストレイヤに現在時刻を書き込む
  update_time();
}

static void deinit(){
  //ウィンドウの削除(メモリの開放)
  window_destroy(s_main_window);  
}

int main(void){
  init();
  app_event_loop();
  deinit();
}

PebbleKitJS

inex.js
 //Clayの設定
var Clay = require('pebble-clay');
var clayConfig = require('./config');
var clay = new Clay(clayConfig);

var xhrRequest = function(url, type, onload_callback, error_callback){
  var xhr = new XMLHttpRequest();
  xhr.onload    = function(){onload_callback(this.responseText);};
  xhr.onerror   = function(){error_callback();};
  xhr.ontimeout = function(){error_callback();};
  xhr.timeout = 5000;
  xhr.open(type, url);
  xhr.send(null);
};


//XMLHttpRequestがエラー(又はタイムアウト)になった時に呼び出されるコールバック関数
var errorCallback = function(){
  console.log("XMLHttpRequest error!!! Send UNKNOWN signal...");
  sendSignalToWatch(0);  //UNKNOWN=0
  return;
};


// account/get APIを叩いた時のコールバック
var checkAccountStatusCB = function(responseText){
  console.log("[call checkAccountStatusCB]");
  //返信をjson形式にパースする
  var json = JSON.parse(responseText);
  var signal = 0;
  //jsonからデータを取り出す
  var status = json.status;
  //受信message確認のためにログに出力
  console.log("json message is :" + status);
  switch(status){
    //元アドレスのstatusがLOCKEDの場合、リモートステータスを確認
    //リモートステータスがACTIVEで無ければハーベストしていないことが確定
    //リモートステータスがACTIVEの場合は、まず最初にリモートアカウントを取得する
    /*
     * status:UNLOCKED => このアドレスでハーベスとしていることが確定
     *
     * status:LOCKED + remoteStatus:ACTIVE     => リモートアカウントでハーベスティング中の可能性あり
     * status:LOCKED + remoteStatus:ACTIVE以外 => ハーベストしていないことが確定
     */
    case "LOCKED":
      if(json.remoteStatus == "ACTIVE"){
        tryToGetRemoteAccount();
        return;
      }
      signal = -1;
      break;

    case "UNLOCKED":
      signal = 1;
      break;
  }
  //加工結果をログに出力
  console.log("checAccountStatusCB:" + signal);
  //watch側にハーベストの状態シグナルを送信する     
  sendSignalToWatch(signal);
};


//API通信を行うメインの関数
function requestAPI(url,onload_callback){
  console.log("[call requestAPI] url : " + url);
  xhrRequest(url,"GET",onload_callback,errorCallback);
}


// account/get/from-public-key APIを叩いた時のコールバック
//このAPIを叩くことでアドレスを取得できればそのアドレスがリモートアカウントアドレスとなる
//結果をローカルストレージに保管し、そのステータスを調べることで最終的な結果を判断する
var getAddressFromPubKeyCB = function(responseText){
  console.log("[call getAddressFromPubKeyCB]");
  //返信をjson形式にパースする
  var json = JSON.parse(responseText);
  //jsonからデータを取り出す
  var address = json.account.address;
  //APIでアドレスが取得できなければUNKNOWN
  if(!address){
    console.log("getAddressFromPubKeyCB: There is no address in API response...");
    sendSignalToWatch(0);  //UNKNOWN=0
    return;
  }
  //アドレスが取り出せた場合はローカルストレージに保存してそのアドレスのステータスを確認する
  //getInfoを再び呼び出しても良いが、なにかあると無限ループするのでやめておく
  var storage_data  = localStorage.getItem("clay-settings");
  var clay_settings = JSON.parse(storage_data);
  clay_settings.REMOTE_ACCOUNT_ADDRESS = address;
  localStorage.setItem("clay-settings",JSON.stringify(clay_settings));
  var node_ip   = getLsNodeIP();
  var url       = "http://" + node_ip + ":7890/account/status?address=" + address;
  requestAPI(url,checkAccountStatusCB);
};


// account/transfers/outgoing APIを叩いた時のコールバック
//transactionの束(最大25件)から、リモートアカウントの公開鍵を調べる
var getRemoteAccountPubKeyFromTxsCB = function(responseText){
  console.log("[call getRemoteAccountPubKeyFromTxsCB]");
  //返信をjson形式にパースする
  var json = JSON.parse(responseText);
  //jsonからデータを取り出す
  var txs = json.data;
  //取得したオブジェクト内にデータフィールドが無ければUNKNOWN
  if(!txs){
    console.log("getRemoteAccountPubKeyFromTxsCB: response text has no data field...");
    sendSignalToWatch(0);  //UNKNOWN=0
    return;
  }
  //Pebbleではfor-of使えないのでこれで
  for(var i=0; i<txs.length; i++){
    var tx = txs[i];
    //transaction.type:2049:importance transfer tx
    //transaction.mode:   1:active remote harvesting
    //この条件に最初にヒットしたものにある公開鍵がリモートアカウントのもの
    if(tx.transaction.type==2049 && tx.transaction.mode==1){
      var remote_account_pubkey = tx.transaction.remoteAccount;
      var node_ip               = getLsNodeIP();
      //公開鍵のアドレス変換はNISに任せたいのでAPIで取得する
      var url = "http://" + node_ip + ":7890/account/get/from-public-key?publicKey=" + remote_account_pubkey;
      requestAPI(url,getAddressFromPubKeyCB);
      //最新一件以外は無視すべきなのでreturn
      return;
    }
  }
}; 


//この関数が呼び出されるのは、委任ハーベスト中かつローカルストレージに
//委任先アドレスがセットされていない時なのでAPIで委任元アドレスのTxを取得し、
//委任先アドレスの割り出しを試みる(取得可能件数が最大25件なので必ず取得できるとは限らない)
function tryToGetRemoteAccount(){
  console.log("[call tryToGetRemoteAccount]");
  //URLの作成
  var address   = getLsAddress();
  var node_ip   = getLsNodeIP();
  var url = "http://" + node_ip + ":7890/account/transfers/outgoing?address=" + address; 
  requestAPI(url,getRemoteAccountPubKeyFromTxsCB);
}


function getLocalStorageData(key){
  var value;
  var storage_data = localStorage.getItem("clay-settings");
  if(!storage_data){
    value = "";
  }else{
    var clay_setting = JSON.parse(storage_data);
    value = clay_setting[key];
  }
  return value;
}


//Clayに設定したアドレスを取得する関数
function getLsAddress(){
  var address = getLocalStorageData("ADDRESS");
  if(!address){
    address = "";
  }else{
    //取り出したアドレスが"-(ハイフン)"付きであれば取り除く
    address = address.replace(/-/g,"");
  }
  console.log("[call getLsAddress]" + address);
  return address;
}


//Clayに設定したノードのIPアドレスを取得する関数
//委任先ノードからデータ取らないとダメ
function getLsNodeIP(){
  var node_ip = getLocalStorageData("NODE");
  console.log("[call getlsNodeIP]" + node_ip);
  return node_ip;
}


//Clayに設定したリモートアカウントアドレスを取得する関数
function getLsRemoteAccountAddress(){
  var r_address = getLocalStorageData("REMOTE_ACCOUNT_ADDRESS");
  console.log("[call getLsRemoteAccountAddress]" + r_address);
  return r_address;
}


//引数で指定したharvest signalをAppMessageとしてwatch側に送る関数
function sendSignalToWatch(signal){
  //watch側へはjsオブジェクトをAppMessageとして送信するため、送信用のjsオブジェクトを作成する
  var dictionary = {"SIGNAL":signal};    
  //AppMessageをwatch側へ送信
  Pebble.sendAppMessage(dictionary,
    function(e){
      console.log("info sent to Pebble Success!");
    },
    function(e){
      console.log("Error sending message to Pebble!");
    }
  );
}


//PebbleKitJSへ情報を取るようにリクエストが来るとここで一度受けてNIS-APIにリクエストを送る関数を呼び出す
function getInfo(){
  console.log("[call getInfo]");
  //URLの作成
  var address   = getLsAddress();
  var node_ip   = getLsNodeIP();
  var remoteAccountAddress = getLsRemoteAccountAddress();
  var url;
  //この時点で、address,node_ipに値がセットされていなければwatchにUNKNOWNを投げて終わる
  if(!address || !node_ip){
    console.log("Please set address and node ip address!!! Send UNKNOWN signal...");
    sendSignalToWatch(0);  //UNKNOWN=0
    return;
  }
  //もし、ローカルストレージ内にリモートアカウントアドレスがセットされていれば、委任状態確定なので、
  //委任元アドレスの状態を調べずにリモートアカウントアドレスの状態のみを調べる
  if(!remoteAccountAddress){
    url = "http://" + node_ip + ":7890/account/status?address=" + address;
  }else{
    url = "http://" + node_ip + ":7890/account/status?address=" + remoteAccountAddress; 
  }
  requestAPI(url,checkAccountStatusCB);
}


//PebbleKitJSがreadyになった時のイベントリスナを設定する
Pebble.addEventListener("ready",
  function(e){
    //PebbleKit JS ready!
    console.log("PebbleKit JS ready!");

    //initial get info
    getInfo();
  }
);


//PebbleKitJSがAppMessageを受け取った時のイベントリスナを設定する
Pebble.addEventListener("appmessage",
  function(e){
    console.log("AppMessage recieved!!");

    //AppMessageをwatchから受け取ったら、再度APIにリクエストを送る
    getInfo();
  }
);

以上です。

さいごに

 とりあえず動けばいいやと思ってやっているので、jsもcもお作法がよくわかりません。

※main.cのコードコピペミスしていました。貼り直しています。

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.