ページ

2016年8月27日土曜日

オリジナルのPebbleアプリを作成 2 - シンプルな時計表示 -


今回もCloudPebbleを使ってシンプルな時計を表示するアプリを作成してみます。
最小プログラム

まずは一番最小となるアプリを作成してみます。
新規にプロジェクトを作成します。プロジェクト名はSAMPLE2にしてます。

  • PROJECT TYPE : Pebble C SDK
  • SDK VERSION: SDK3
  • TEMPLATE: Empty project
※今回はTEMPLATEをEmpty projectにしています。

作成後新規にソースファイルを追加します。
ファイル名はmain.cにしてます。
作成したmain.cを以下に編集します。
#include <pebble.h>

static Window *s_window;

int main(void) {
  s_window = window_create();
  window_stack_push(s_window, true);

  app_event_loop();

  window_destroy(s_window);
}
処理内容としては、
  1. Windowポインタをwindow_create()で取得
  2. 表示させる為にwindow_stack_pushでstackにpush
    この時の第二引数はアニメーションの有無です。
    ※PebbleOSはWindow Stack APIにて画面遷移など行います。
  3. app_event_loopでExitするまでメインループを実行
  4. 最後window_destroyでWindowを破棄して終了
となります。
早速エミュレータで確認してみます。
COMPILATION > EMULATOR > RUN BUILD > APLITEで実行できます。
実行した結果がこちら↓
当然ながら何も表示されてませんが、一応最小のプログラムができました。

時計表示

最小限のプログラムを改良して、シンプルな時計を表示させるアプリを作成してみます。
#include <pebble.h>

static Window *s_window;
static TextLayer *s_time_layer;
static char s_buffer[] = "00:00:00";

// Windowが読み込まれた時に呼ばれる
static void window_load(Window *window) {
  // ルートのレイヤーのサイズを取得
  Layer *window_layer = window_get_root_layer(s_window);
  GRect bounds = layer_get_bounds(window_layer);

  // 時計を表示するレイヤーの作成
  s_time_layer = text_layer_create(
      GRect(0, 0, bounds.size.w, bounds.size.h));

  // レイヤーの設定
  text_layer_set_background_color(s_time_layer, GColorPastelYellow);
  text_layer_set_text_color(s_time_layer, GColorBlack);
  text_layer_set_font(s_time_layer, fonts_get_system_font(FONT_KEY_ROBOTO_CONDENSED_21));
  text_layer_set_text_alignment(s_time_layer, GTextAlignmentCenter);

  // レイヤーの追加
  layer_add_child(window_layer, text_layer_get_layer(s_time_layer));
}

// Windowが解放された時に呼ばれる
static void window_unload(Window *window) {
  text_layer_destroy(s_time_layer);
}

// 時間更新
static void update_time() {
  // 現在時刻の取得
  time_t temp = time(NULL);
  struct tm *tick_time = localtime(&temp);

  // フォーマット指定で文字列に出力
  strftime(s_buffer, sizeof(s_buffer), "%T", tick_time);

  // ディスプレイ出力
  text_layer_set_text(s_time_layer, s_buffer);
}

// 秒毎に呼ばれるハンドラ
static void tick_handler(struct tm *tick_time, TimeUnits units_changed) {
  update_time();
}

// メイン
int main(void) {
  // ウィンドウの作成
  s_window = window_create();
  window_set_window_handlers(s_window, (WindowHandlers) {
    .load = window_load,
    .unload = window_unload
  });
  // イベント登録
  tick_timer_service_subscribe(SECOND_UNIT, tick_handler);
  // Window Stackに追加
  window_stack_push(s_window, true);
  // 一度現在時刻で表示
  update_time();

  // メインループ
  app_event_loop();
  // 終了処理
  window_destroy(s_window);
}
やってる事としては、、
  1. window_set_window_handlersでWindowがload、unloadした時のハンドライベントを設定します
  2. Windowがloadしたタイミングで時計を表示するレイヤー(s_time_layer)を作成します
    ※指定できるフォントはこちらを参考にしました
  3. tick_timer_service_subscribeで1秒毎にtick_handler関数を呼び出すように登録します
  4. update_timeで現在時刻をs_time_layerに書き出します

です。ただ今回tick_timer_service_subscribeで1秒毎(SECOND_UNIT)に
ハンドラを呼び出してますが、実機だと電池がその分消費されるので注意です。

最後にエミュレータで動作させた結果がこちらです↓


2016年8月24日水曜日

Pebbleを購入して約3週間 よく使うアプリやWatchface


最近Pebbleネタが続いてますが、、
今回は個人的にPebbleを購入して約3週間でよく使ってるアプリやWatchfaceを
紹介できればと思います。

Watchface

かなりの数のWatchfaceがあるので、
選ぶだけでかなり時間を使ってしまいますw

MIYAMOTO REDUX


最近はデザインが気に入ってずっとこのWatchfaceをしてるんですが、
時計盤がデジタル/アナログの切り替えができるのと、
時間と共に時計盤の周りが変化するので飽きないです。



MARIO TIME WATCHFACE


言わずもがなマリオです。
ユニークなWatchfaceですが、なかなか機能面でも優れてます。
バッテリーやBluetoothの接続状態
(接続が切れると振動で教えてくれるように設定できます)
気温などを表示してくれます。
ただちょっとおもちゃっぽくなってしまう感じが。。(自分だけ?)

Apps

まだまだ全然いろんなアプリを試せては無いんですが、
インストールして今も使ってるアプリを紹介したいと思います。

STOPWATCH


StopWatchかよ!!と思われるかもしれませんが、
やっぱり地味に便利ですw
カップラーメンのタイマーやらやら、直ぐ測れるのは便利です。


COMPASS


こちらは頻繁に使うわけでは無いんですが、
たまにポケモンGOしながら使ってますw


まだ3週間なのでこんなもんですが、、
また新たなWatchfaceやアプリが出てきた時は紹介したいと思います。





2016年8月21日日曜日

オリジナルのPebbleアプリを作成 1 - 環境設定 -


以前「Pebble Time Round 購入!!」という記事を書きました。
それからずっとPebbleをつけてますが、スマホを取り出さずに通知の確認や、
音楽の再生などがとても便利で活用してます。
そんなPebbleですが、Pebble上で動くアプリが結構簡単に作れるらしいので
早速試してみようと思います。
作成方法

Pebbleのアプリを作るにはCloudPebbleでWeb上で作る方法と、
SDKをダウンロードしてpythonで開発していく方法があります。
今回は前者のCloudPebbleを使ってアプリを作ってみたいと思います。
また、開発時にはiOS/Android端末とPCが同ネットワーク上にいる必要があります。
CloudPebble

まずはCloudPebbleにアクセスし、アカウントを持ってない場合は作成し、
ログインします。ログイン後の画面は以下のようになってます。
※iOS/Android端末のアプリのログインアカウントと同一である必要があります。

早速プロジェクトを作ります。右上のCREATEボタンを押します。
するとダイアログが表示されるので、作りたいプロジェクトの内容を設定します。

今回は最初なのでHelloWorldを表示するsample1プロジェクトを作成します。
作成後hello_world.cファイルがWebエディッタ上で編集できるようになってます。
では早速動かしてみたいと思います。。が、
まずは端末側の設定です、Pebbleアプリを立ち上げて、
Apps>メニューSettingsを選択し、Developer ModeをONにします。
Apps>メニューSettings>Developer(off)を選択します。
Enable Developer ConnectionsをONにします。

正常に接続できる状態になるとStatusがWaitingになります。
次にCloudPebbleに戻って、左側のメニューからCOMPILATIONを選択します。
RUN BUILDをクリックしビルドを行い、上部のタブPHONEを選択し、
INSTALL AND RUNを実行します。

無事Pebble上で実行されていれば成功です。

2016年8月13日土曜日

Rubyでdllやsoを扱うFiddleのまとめ


最近仕事でお世話になっているFiddleのまとめです

Fiddleとは?
Fiddleが何かというと、Rubyの標準ライブラリに含まれている、
Rubyからdllやsoファイルを読み込んで使えるものです。
dlと同等の機能を持つ。dl自体はRuby2.0 以降deprecated となり、
2.2.0 で削除されてます。

簡単なサンプル


まずは簡単なサンプルを作ってみたいと思います。
適当なディレクトリに以下の構成で作成します。
.
├── CMakeLists.txt
├── build ※ディレクトリ
├── sample.c
└── sample.rb
それぞれのファイルを編集・・
  • CMakeLists.txt
# CMakeのバージョン
cmake_minimum_required(VERSION 2.8)

# プロジェクト名
project(sample C)

# 共有ライブラリを作成
add_library(sample SHARED sample.c)

# MacOSの場合.dylibで作成される為、.soで作成されるように以下を追記
if (APPLE)
    set_property(TARGET sample PROPERTY PREFIX "lib")
    set_property(TARGET sample PROPERTY OUTPUT_NAME "sample.so")
    set_property(TARGET sample PROPERTY SUFFIX "")
endif()
  • sample.c
#include <stdio.h>

int add(int a, int b) {
  return a + b;
}

void print(const char *msg) {
  puts(msg);
}
  • sample.rb
# coding: utf-8
require "fiddle/import"

module M
  extend Fiddle::Importer
  dlload "build/libsample.so" # 共有ライブラリをロード
  extern "int add(int, int)" # 関数をモジュールに教えてあげる
  extern "void print(const char *)"
end

# いざ実行!!
puts M.add(1, 2) #=> 3

M.print('hoge') #=> hoge
共有ライブラリ(so)側は単純に引数を加算して返すだけのadd関数と
引数の文字列を表示するprint関数を実装。
これをsample.rbで呼び出して実行しています。
ビルドして実行します。
$ cd build && cmake .. && make && cd ..
$ ruby sample.rb
こんな感じで単純な関数を呼ぶくらいであれば、
お気軽に使うことができます。

複雑なサンプル


実際にゴリゴリ使うとなったら上のサンプルのようにはいきません
例えばこんな構造体があったとします・・・
struct Person {
  int no;
  wchar_t name[20];
};

struct Members {
  struct Person persons[3];
  struct Members *nextPtr;
};
これをrubyから扱うとかちょっと考えたくないですが、
実際にやるとこうなります。
  • sample.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <wchar.h>

struct Person {
  int no;
  wchar_t name[20];
};

struct Members {
  struct Person persons[3];
  struct Members *nextPtr;
};

void initMember(struct Members **ppMembers) {
  *ppMembers = (struct Members *)malloc(sizeof(struct Members));
  (*ppMembers)->persons[0].no = 1;
  memset((*ppMembers)->persons[0].name, 0, sizeof(wchar_t) * 20);
  wcscpy((*ppMembers)->persons[0].name, L"hogehoge1");
  (*ppMembers)->persons[1].no = 2;
  memset((*ppMembers)->persons[1].name, 0, sizeof(wchar_t) * 20);
  wcscpy((*ppMembers)->persons[1].name, L"hogehoge2");
  (*ppMembers)->persons[2].no = 3;
  memset((*ppMembers)->persons[2].name, 0, sizeof(wchar_t) * 20);
  wcscpy((*ppMembers)->persons[2].name, L"hogehoge3");

  // Next pointer
  struct Members *next = (struct Members *)malloc(sizeof(struct Members));
  next->persons[0].no = 11;
  memset(next->persons[0].name, 0, sizeof(wchar_t) * 20);
  wcscpy(next->persons[0].name, L"fugafuga1");
  next->persons[1].no = 12;
  memset(next->persons[1].name, 0, sizeof(wchar_t) * 20);
  wcscpy(next->persons[1].name, L"fugafuga2");
  next->persons[2].no = 13;
  memset(next->persons[2].name, 0, sizeof(wchar_t) * 20);
  wcscpy(next->persons[2].name, L"fugafuga3");
  (*ppMembers)->nextPtr = next;
}

void freeMember(struct Members **ppMembers) {
  free((*ppMembers)->nextPtr);
  free(*ppMembers);
}
サンプルなのでエラー等は一切考慮してません。
  • sample.rb
# coding: utf-8
require "fiddle/import"

module M
  extend Fiddle::Importer
  dlload "build/libsample.so"
  extern "void initMember(void *)"
  extern "void freeMember(void *)"

  # 構造体をruby用に定義
  # 構造体in構造体は展開して定義しないといけない
  # wchar_tはMacOSだと4byte, windowsだと2byte
  # 今回MacOSなのでintをあてがってます
  MEMBERS = struct([
    "int no1",
    "int name1[20]",
    "int no2",
    "int name2[20]",
    "int no3",
    "int name3[20]",
    "void *nextPtr"
    ]);
end

# void *分の領域を確保
pointer = Fiddle::Pointer.malloc(Fiddle::SIZEOF_VOIDP)
M.initMember(pointer)
# Members構造体へキャスト
members = M::MEMBERS.new(pointer.ptr)
puts members.no1 #=> 1
puts members.name1.pack('U*') #=> hogehoge1
puts members.no2 #=> 2
puts members.name2.pack('U*') #=> hogehoge2
puts members.no3 #=> 3
puts members.name3.pack('U*') #=> hogehoge3

# nextPtrをMembers構造体へキャスト
members2 = M::MEMBERS.new(members.nextPtr)
puts members2.no1 #=> 11
puts members2.name1.pack('U*') #=> fugafuga1
puts members2.no2 #=> 12
puts members2.name2.pack('U*') #=> fugafuga2
puts members2.no3 #=> 13
puts members2.name3.pack('U*') #=> fugafuga3

M.freeMember(pointer)
とまあ、何とも強引な感じになりますw

2016年8月11日木曜日

アクセサリ収納ケースと充電ケーブル購入


最近夜な夜なくつろぎながら、YouTube見てたりするんですが、
とあるユーチューバーの方の動画で紹介されていた
アクセサリ収納ケースと充電ケーブルが良さそうだったので購入してみました。

アクセサリ収納ケース

ELECOM アクセサリ収納 汎用ポーチ 3ポケット ストレッチ生地 軽量 ブラック BMA-GP10BK




表と裏にそれぞれ収納できるスペースがあります。
表の収納スペースがやや浅めで、裏の収納スペースが厚みがあります。

自分は今表のスペースにケーブル類、裏のスペースにモバイルバッテリーを
入れてます。
使ってみて、意外とたくさん入るな〜といった印象です。
スマホのアクセサリー収納ケースとして使ったり、PC用のアクセサリー収納として
マウスや電源ケーブルなんか入れてまとめておくと便利かなと思いました。


充電ケーブル

オウルテック 1年保証 Lightning変換アダプタ付きmicroUSBケーブル Apple認証 ホワイト 20cm



microUSBとしても、Lightningケーブルとしても使用する事ができます。


先に変換アダプタが付いており、簡単に切り替える事ができます。
今回20cmの短いケーブルを購入しました。というのもカバンの中で
モバイルバッテリーで充電する時用にいいかなと思って購入しました。


ケースも充電ケーブルもお手頃な値段の割になかなか良かったです。


2016年8月6日土曜日

Feedlyの未読記事をSlackへGASで通知するBotの作成 3


前回の続きです。
前回未読一覧を取得したので、今回は未読記事のタイトルとURLを
SlackにPostするようにして完成させたいと思います。
ID毎の記事一覧取得

前回、
例)
{
  "unreadcounts": [
    {
      "count": 508,
      "id": "feed/http://www.autoblog.com/rss.xml",
      "updated": 1367539068016
    }
  ]
}
上のようなidを取得しましたが、そのidにぶら下がる記事一覧を取得します。
試しに以下のurlを叩いてみてください。
$ curl -H 'Authorization: OAuth {トークン}' \
  https://cloud.feedly.com/v3/streams/contents?streamId={id}
実行すると以下のようなJSONが取得できたかと思います、
例)
{
  "direction": "ltr",
  "title": "The Verge -  All Posts",
  "id": "feed/http => //www.theverge.com/rss/full.xml",
  "continuation": "gRtwnDeqCDpZ42bXE9Sp7dNhm4R6NsipqFVbXn2XpDA=_13fb9d6f274:2ac9c5:f5718180",
  "updated": 1367539068016,
  "items": [
    {
      "engagement": 15,
      "published": 1367539068016,
      "crawled": 1367539068016,
      "title": "NBC's reviled sci-fi drama 'Heroes' may get a second lease on life as Xbox Live exclusive", 
      "author": "Nathan Ingraham",
      "id": "gRtwnDeqCDpZ42bXE9Sp7dNhm4R6NsipqFVbXn2XpDA=_13fb9d6f274:2ac9c5:f5718180",
      "content": {
        "direction": "ltr",
        "content": "..."
      },
      "updated": 1367539068016,
      "unread": true,
      "tags": [
        {
          "id": "user/c805fcbf-3acf-4302-a97e-d82f9d7c897f/tag/inspiration",
          "label": "inspiration"
        }
      ],
      "origin": {
        "title": "The Verge -  All Posts",
        "htmlUrl": "http://www.theverge.com/",
        "streamId": "feed/http://www.theverge.com/rss/full.xml"
      },
      "alternate": [
        {
          "href": "http://www.theverge.com/2013/4/17/4236096/nbc-heroes-may-get-a-second-lease-on-life-on-xbox-live", 
          "type": "text/html"
        }
      ],
      "categories": [
        {
          "id": "user/c805fcbf-3acf-4302-a97e-d82f9d7c897f/category/tech",
          "label": "tech"
        }
      ]
    }
  ],
  ・・・
}
この中で、SlackにtitlehrefをPostします(★が付いている所)
GASでSlackにPostするまでを実装したものが↓になります。
/**
 * Entry function.
 */
function exec() {
  FeedlySlackBot.unread();
}


/**
 * FeedlySlackBot object.
 */
var FeedlySlackBot = {

  PROFILE_URL: 'https://cloud.feedly.com/v3/profile',

  UNREAD_COUNT_URL: 'https://cloud.feedly.com/v3/markers/counts',

  STREAM_URL: 'https://cloud.feedly.com/v3/streams/contents?streamId=',

  SLACK_URL: 'https://hooks.slack.com/services/XXXXXXXXXXXXXXXXXXXXXXXXXXXXX',

  initialize: function() {
  },

  /**
   * Get auth string.
   *
   * @return {String}
   */
  auth: function() {
    return 'OAuth {トークン}';
  },

  /**
   * Log out profile info.
   */
  profile: function() {

    var auth = this.auth();
    var response = this.get(this.PROFILE_URL, auth);
    Logger.log(response.getContentText("UTF-8"));
  },

  /**
   * Get unread feeds.
   */
  unread: function() {

    var auth = this.auth();
    var response = this.get(this.UNREAD_COUNT_URL, auth);

    var feeds = [];
    // Parse json.
    var obj = JSON.parse(response.getContentText("UTF-8"));
    var unreadcounts = obj.unreadcounts;
    for (var i = 0; i < unreadcounts.length; i++) {
      var unread = obj.unreadcounts[i];
      if (unread.count > 0 && unread.id.indexOf('feed/') > -1) {
        Logger.log('unread => id : ' + unread.id + ', count : ' + unread.count);
        feeds.push(unread);
      }
    }

    if (feeds.length == 0) {
      Logger.log('Unread feed not found.');
      return;
    }

    this.stream(feeds);
  },

  /**
   * Collect unread streams.
   *
   * @param {Array} unreads
   */
   stream: function(unreads) {

     var streams = {};

     var auth = this.auth();
     for (var i = 0; i < unreads.length; i++) {
       var id = unreads[i].id;
       var response = this.get(this.STREAM_URL + id + '&count=3', auth);

       var obj = JSON.parse(response.getContentText("UTF-8"));
       var table = [];
       for (var j = 0; j < obj.items.length; j++) {
         var item = obj.items[j];
         var content = {
           'title': item.title,
           'href': item.alternate[0].href
         }
         table.push(content);
       }
       streams[id] = table;
     }

     this.postSlack(streams);
   },

  postSlack: function(streams) {

    for (key in streams) {
      var table = streams[key];
      for (var i = 0; i < table.length; i++) {
        var payload = {
          'text': table[i].title + '\n' + table[i].href,
          'channel': '#general',
          'username': 'XXXXXX',
          'icon_url': 'http://XXXXX/XXXXX.png'
        };
        var r = this.post(this.SLACK_URL, payload);
        Logger.log(r.getContentText("UTF-8"));
      }
    }
  },

  /**
   * Get response with auth.
   *
   * @param {String} url
   * @param {String} auth
   */
  get: function(url, auth) {
    var headers = {'Authorization' : auth};
    var options = {
      'method' : 'get',
      'contentType' : 'application/json;charset=utf-8',
      'headers' : headers
    };

    return UrlFetchApp.fetch(url, options);
  },

  /**
   * Post to url with payload.
   *
   * @param {String} url
   * @param {Object} payload
   */
  post: function(url, payload) {

    var options = {
      'method' : 'POST',
      'payload' : JSON.stringify(payload)
    };

    return UrlFetchApp.fetch(url, options);
  }
};
SLACK_URLにはWebHooksのURLを設定します。
URLの取得方法はこちらを参考にして下さい。
channelusernameicon_urlは好きなように設定して下さい。
また、今回は記事を最大3件分だけ取ってくるようにしています。=> &count=3
結構Feedly放置してると、とんでもない数のPostが飛んでくるので制限をかけましたw
ここまでできたら、Google Driveにアップロードしexecを実行してみて下さい。
Slackに未読記事がPostされてたら成功です。
トリガーを設定して毎日定期的にPostされるようにすればBotの完成です。
一応このプロジェクトは
に上げてます。テンプレートからトークンやらその他設定を.envに書いておけば
main.jsを作ってくれるようになってます。
興味があれる方は是非