Google Calendar API の Push Notification 機能を使うと、Google カレンダー側の更新をリアルタイムに自分のアプリケーションに反映することができるらしいので試してみました。

Push Notification 機能を使うためには、通知を受け取る URL を Google 側に事前に登録しておく必要があるのですが、手順はドキュメントに書いてあるため省略します。

今回、クライアント用のライブラリには google-api-client を使用します。

# Gemfile
gem 'google-api-client'

Channel を作成する

Push 通知を受け取るために、まず Channel と呼ばれるものを作成する必要があります。

変更の通知を受け取りたいカレンダーを指定して Channel を作成することで、そのカレンダー内のイベントが変更されるたびに、Google 側から HTTPS でリクエストが飛んでくるようになります。

ちなみに Channel は変更の通知を受け取りたいカレンダーごとに作成する必要があるようです。

require 'google/api_client'

client = Google::APIClient.new

client.authorization.client_id = ''
client.authorization.client_secret = ''
client.authorization.refresh_token = ''
client.authorization.fetch_access_token!

service = client.discovered_api('calendar', 'v3')

client.execute!(
  api_method: service.events.watch,
  parameters: { calendarId: '' },
  body_object: {
    id: '',
    type: 'web_hook',
    address: ''
  }
)

<CLIENT_ID><CLIENT_SECRET>Google Cloud Console で確認できます。

<YOUR_REFRESH_TOKEN>OAuth 2.0 認証後に取得できる refresh_token を指定します。

<YOUR_CALENDAR_ID> に変更の通知を受け取りたいカレンダー ID を指定します。
ちなみに、ここで自分のカレンダー ID を調べることができます。

<CHANNEL_ID>には任意の文字列を指定します。
複数の Channel を作成する場合は Channel 間でユニークになっているほうがいいでしょう。
ちなみに、自分はSecureRandom.uuid()を使っています。

<YOUR_RECEIVING_URL> には通知を受け取りたい URL を指定しておきます。

リクエストに問題がなければ、以下のようなレスポンスが返ってきます。

{
  "kind": "api#channel",
  "id": "4a0d04ac-b5df-4a4d-ba03-65d121fbc100",
  "resourceId": "ROKWIDXHB6wV6KdPqFWmo_lTTLF",
  "resourceUri": "https://www.googleapis.com/calendar/v3/calendars/sei@me.com/events?alt=json",
  "expiration": "1384317818000"
}

ここで大事なのはidexpirationです。

idは先ほど指定した<CHANNEL_ID>の値が入ってきます。

expirationは Channel の有効期限で、これが切れると通知が飛んでこなくなります。
もし、引き続き通知を受け取りたい場合は、期限が切れる前に Channel を作成し直す必要があります。

Push Notification を受け取る

Channel 作成後、カレンダーに変更があると以下のようなヘッダー付きのリクエストが飛んできます。

HTTP_X_GOOG_CHANNEL_EXPIRATION: Sun, 10 Nov 2013 11:08:46 GMT
HTTP_X_GOOG_CHANNEL_ID: 4a0d04ac-b5df-4a4d-ba03-65d121fbc100
HTTP_X_GOOG_MESSAGE_NUMBER: 235226900
HTTP_X_GOOG_RESOURCE_ID: ROKWIDXHB6wV6KdPqFWmo_lTTLF
HTTP_X_GOOG_RESOURCE_STATE: exists
HTTP_X_GOOG_RESOURCE_URI: https://www.googleapis.com/calendar/v3/calendars/sei@me.com/events?alt=json

HTTP_X_GOOG_CHANNEL_IDは先ほど登録した<CHANNEL_ID>です。

通知では変更の内容までは教えてくれないので、通知が来る度に API を使って更新された内容を取得する必要があります。

更新されたイベントを取得する

<YOUR_CALENDAR_ID>にカレンダー ID を指定して更新があったイベントを取得します。
複数の Channel をもつアプリケーションを構築する場合は、通知内のHTTP_X_GOOG_CHANNEL_IDの値をもとにカレンダー ID を参照できるようにしておく必要があります。

require 'google/api_client'

client = Google::APIClient.new

client.authorization.client_id = ''
client.authorization.client_secret = ''
client.authorization.refresh_token = ''
client.authorization.fetch_access_token!

service = client.discovered_api('calendar', 'v3')

client.execute!(
  api_method: service.events.list,
  parameters: {
    calendarId: '',
    updatedMin: 1.minute.ago.to_datetime.rfc3339
  }
)

変更があったイベントをピンポイントで取得できるわけではないので、直近で変更があったイベントを検索する形になります。

ヘタすると取りこぼしの危険があるので、多めに 1 分前以降の変更を取得するようにしていますが、もっといい方法があればなぁ、という感じです。
(Google さん、変更があった時刻も教えてくれるとうれしいですっ!)

null-ly:

グローバルオブジェクトをガンガン汚染してる得体の知れない HTML をメンテせざるを得ないこともあったりします。(ちょっと韻踏んでる)

これでグローバルリークしているプロパティを探せます。ちなみに Tumblr のダッシュボードはこんな感じ。

["ENVIRONMENT","Tumblr","tinyMCE","_sf_startpt","language_for_tinymce","l10n_str","localized_str","i","localized_str_ajax_error","localized_str_loading","localized_str_remove_tag","localized_str_promote_this_post_in","localized_str_promote","localized_str_promote_warning_none","localized_str_promote_warning_singular","localized_str_promote_warning_plural","localized_str_markdown","localized_str_html_enabled","localized_str_bold","localized_str_italic","localized_str_strikethrough","localized_str_enter_the_url","localized_str_insert_link","localized_str_adding_tags","localized_str_removing_tags","localized_str_only_100_posts","localized_str_select_posts_to_edit","localized_str_select_posts_to_delete","localized_str_enter_tags_to_add","localized_str_select_posts_to_tag","localized_str_wait_for_last_operation","localized_str_confirm_delete_selected_posts","localized_str_my_posts","localized_str_search_tumblr","localized_str_my_dashboard","localized_str_search_posts","localized_str_search_help","localized_str_search_by_tag","localized_str_search","localized_str_this_tumblelog","localized_str_you_answered","localized_str_thank_you","localized_str_confirm_block_this_person","localized_str_cancel","localized_str_reply","localized_str_250_max","localized_str_confirm_block","localized_str_new_posts","localized_str_over_max_file_size_mb","localized_str_empty_query","localized_str_image_upload","localized_str_old_password","localized_str_password_mismatch","localized_str_confirm_password","localized_str_valid_email","localized_str_unsaved_changes","$","jQuery","_","Backbone","video_thumbnail_hover","load_typekit","select_field","get_cookie","set_cookie","unset_cookie","trackable_follow","toggle_video_embed","cycle_video_thumbnails","pano_iframe_preloader","flashVersion","renderVideo","replaceIfFlash","Ajax","Spinner","AutoPaginator","loading_next_page","BeforeAutoPaginationQueue","AfterAutoPaginationQueue","add_to_image_queue","process_image_queue","start_processing_image_queue","increment_note_count","decrement_note_count","GIF","jQuery110205483331584837288","_gaq","tumblr_custom_tracking_url","_gat","gaGlobal","_qevents","_comscore","__qc","quantserve","uh","YAHOO","__","udm_","ns_p","COMSCORE","dialog_translations","audiojs","audiojsInstance","tinymce","data-mce-expando","Markdown","mejs","JpegMeta","MediaElement","createSetter","createGetter","vdata1384174873883","_V_","VideoJS","default_search_text","next_page","toast_translations","img_obj"]

多すぎ!

null-ly:

いつの間にか Homebrew から OpenCV が消えている模様。コミットログを見ると別のリポジトリに動いているっぽい。

brew tap コマンドでリポジトリを追加すればインストールできるようになる。

$ brew tap homebrew/science
$ brew install opencv
==> Downloading ...

null-ly:

Heroku 使ってますか?便利なので使いましょう。Rails で有名な PaaS ですが、ここでは静的サイト公開用に特化して使う方法を紹介します。

記事の内容と被りますが、スクリーンキャストも参考にして下さい。

まず、Heroku のアカウントを持っていない人はサインアップしましょう。

サクッと以下のコマンドを入力して、ひな形からサイトを作ります。

$ git clone ...

null-ly:

Socket.IO でちょっとしたアクション系のゲームを作ることになり特にレイテンシがシビアになりそうなので調査。

テストした条件などはこんな感じ。

  • アプリは Node.js 0.10.15 + Socket.IO 0.9.16 + Express 3.3.4 で実装
  • ディストリビューションは Amazon Linux AMI 2013.3
  • m1.{xlarge,large,medium,small} の 4 種類のインスタンスタイプを使用
  • クライアントは専用の m1.xlarge のインスタンスから接続
  • 同時接続数は 1, 10, 50, 100, 500, 1000,…

ブロック(ス)はすばらC〜!!!

そんなすばらC〜!!! ブロック(ス)はと何かというと、

Apple が C/C++/Objective-C 向けに独自実装したクロージャみたいなものだよ。

boost がなくても、C++11 でなくても、クロージャがなにも考えずに使える!!!

 

すばらC〜!!!

typedef void (^blk_t)();

class Test {
	private :
		blk_t _callback;
	public :
		Test() { _callback = ^() { printf("%p\n",this); }; }
		blk_t callback() { return _callback; }
};

int main(int argc, char *argv[]) {
	@autoreleasepool {
		Test *t1 = new Test();
		printf("%p\n",t1);
		t1->callback()();
	}
	return 0;
}

とかいて実行すると、、、

同じ値コンソールに、表示された!!!

うれC〜!!!

以上テスト投稿でした〜



X-CTUがWindowsしか対応してない、という状況はMacなMakerにとって非常にストレスフルなことである(多分)。
でも、もう大丈夫です。以下、MacでX-CTUを起動してXBeeのファームを書き込む手順。

1.CrossOver Macをインストール

http://www.codeweavers.com/products/ からダウンロード。
自分は何かのキャンペーンの際にタダでもらいました。Trial版でも大丈夫だと思うけど制限とかあるかもしれない。
とりあえず言われるがままにインストールします。

2.X-CTUをインストール

http://www.digi.com/support/productdetail?pid=3352&type=firmware からダウンロード。 (“Diagnostics, Utilities and MIBs” のタブをクリックするとリストが出てきます) .exeがダウンロードされる訳ですが、↑のインストールが完了していれば恐らくCrossOverのアイコンになっていると思います。
そのままダブルクリックするとCrossOverが起動して「対応してるかわかんないけどインストールする?」みたいな感じで聞かれるのをOKしてインストール。

この時点で一応インストールは出来てます。特に指定していなければ ~/Applicatons/CrossOver/Digi/X-CTU.app という感じで入っているはず。
ただ、MacにはCOMポートが無いのでXBeeを挿してもそのままでは動きません。

3.COMポートを作成

そこでこちらの手順でCOMポートを作ります。


$ cd ~/Library/Application Support/CrossOver/Bottles//dosdevices
$ ln -s /dev/tty.usbserial-AXXXXXX COM1


何のことはない、ただのシンボリックリンクなんだけど結構目から鱗でした。 COM2, COM3…って感じで、必要に応じて増やしていきます。

4.X-CTUを起動してCOMポートを登録

“Add User Com Port”というセクションがあるので、そこにさっきリンクを貼ったポート(“COM1” なら “1”)を入力してAddを押します。



任意のボーレートを設定して”Test/Query”を押すとレスポンスが返ってきます。嬉しいですね〜。



5.ファームのRead/Write

さて早速ファームのアップデートをしたいのですが、どうもWeb経由の更新がうまく動きません。仕方ないのでローカルのファイルから読み込みます。
ここで最新のファームが配布されているので、”XBee / XBee-PRO ZB firmware ver. 2xA7”というのをダウンロード。
X-CTUに戻って “Download new versions…” を押し、ダイアログの “File…” を押してダウンロードしたzipを開きます。



成功すると現在対応しているファームの一覧が読み込まれるので、最新のものを選択して”Write”を押します。



成功しました。嬉しいですね〜。これでもう現場で枕を濡らすこともありません。
今回はたまたまCrossOverを使いましたが、多分普通のWineでもいけるはず。

文字をSVGに書き出そうとして、最初は FontForge -script 使おうとしていたのだけれど、スクリプトを覚える気がないからどうも小回りが効かなく FreeType でなんとかならないかなと思い、適当に日本語で検索したら、「flashなどで利用するためにフォントのグリフを取得する方法」を見つけたので、この資料を参考に、どうせ UCS-2 の範囲で事足りるし iconv を使わずに NSString で代用して Objective-C++ でコードを書いてみた。


#import <Cocoa/Cocoa.h>
#import "string"
#import "sstream"
#import <ft2build.h>
#import <freetype/freetype.h>

#define WEIGHT 0.015625
template <class T> static std::string str(const T &value) { std::ostringstream out; out<<value; return out.str(); }

void dumptype(NSString *text,std::string otf,int size=200,int dpi=72) {
    FT_Library  lib;
    FT_Face     face;
    std::string dir=[[([[NSBundle mainBundle] bundlePath]) stringByDeletingLastPathComponent] UTF8String];
    FT_Init_FreeType(&lib);
    
    if(FT_New_Face(lib,(dir+"/data/"+otf).c_str(),0,&face)) ;
    else {
        FT_Set_Char_Size(face,size<<6,size<<6,dpi,dpi);
        FT_Select_Charmap(face,FT_ENCODING_MS_SJIS);
        
        for(int k=0; k<[text length]; k++) {
            char *chars=(char *)[[text substringWithRange:NSMakeRange(k,1)] cStringUsingEncoding:NSUTF16BigEndianStringEncoding];
            char tmp[6];
            sprintf(tmp,"U+%02X%02X",(unsigned char)chars[0],(unsigned char)chars[1]);
            std::string svg=("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n");
            svg+=("<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n");
            svg+=("<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\" width=\""+str(size)+"px\" height=\""+str(size)+"px\" viewBox=\"0 0 "+str(size)+" "+str(size)+"\" xml:space=\"preserve\">");
            if(!FT_Load_Glyph(face,FT_Get_Char_Index(face,((unsigned char)chars[0])<<8|((unsigned char)chars[1])),FT_LOAD_DEFAULT)){
                FT_Vector *p=face->glyph->outline.points;
                char *t=face->glyph->outline.tags;
                int n=face->glyph->outline.n_contours;
                int b=0;
                svg+=("<path d=\"");
                for(int i=0; i<n; i++){
                    if(i) b=face->glyph->outline.contours[i-1]+1;
                    int e=face->glyph->outline.contours[i]+1;
                    svg+=((i)?"Z M ":"M ")+str(p[b].x*WEIGHT)+" "+str(size-p[b].y*WEIGHT);
                    double px1=p[b].x,py1=-p[b].y;
                    for(int j=b; j<e; j++){
                        if(FT_CURVE_TAG(t[j])==FT_CURVE_TAG_ON){
                            if(px1!=(p[j].x)||py1!=-(p[j].y)) svg+=" L "+str(p[j].x*WEIGHT)+" "+str(size-p[j].y*WEIGHT);
                            px1=p[b].x;
                            py1=-p[b].y;
                        }
                        else{
                            int i1=j-1,i2=j+1;
                            if(j==0) i1=e-1;
                            if(i2>=e) i2=b;
                            
                            if(FT_CURVE_TAG(t[i1])==FT_CURVE_TAG_CUBIC) {
                                double px2=p[i2].x,py2=-p[i2].y;
                                double cx1=p[i1].x,cy1=-p[i1].y;
                                double cx2=p[j].x,cy2=-p[j].y;
                                svg+=" C "+str(cx1*WEIGHT)+" "+str(size+cy1*WEIGHT)+" "+str(cx2*WEIGHT)+" "+str(size+cy2*WEIGHT)+" "+str(px2*WEIGHT)+" "+str(size+py2*WEIGHT);
                                px1=px2;
                                py1=py2;
                            }
                        }
                    }
                }
                svg+=" Z\"/>";
            }
            svg+="</svg>";
            NSError  *error;
            NSString *dst=[NSString stringWithUTF8String:svg.c_str()];
            [dst writeToFile:[NSString stringWithUTF8String:(dir+"/data/"+tmp+".svg").c_str()] atomically:YES encoding:NSUTF8StringEncoding error:&error];
        }
    }
    FT_Done_FreeType(lib);
}

dumptype(@"おっぱいさわり隊","uniba.otf");

/data フォルダにオープンタイプフォントを入れて実行すると U+304A.svg, U+308A.svg, U+308F.svg, U+968A.svg, U+3044.svg, U+3055.svg, U+3063.svg, U+3071.svg を吐き出してくれるので、うれしい。

※ この文章はテスト投稿となります。

割とまじめにgeo系をやる必要が出てきたのでメモ。

d3.js の geometry 変換を THREE.js 上のレンダラで利用できないかいろいろ調べていて、

d3で地図を描く時、標準になっている GeoJSONという地図データを表現するためのJSONフォーマットが良さそうという事に。*1

この記事を参考に、GeoJSONを読んでTHREE.jsのマテリアルとして表示させてみた。

記事にあるように、このライブラリで一度 GeoJSONを読み込んで、 d3 の geometry 変換で経緯座標系をメルカトル地図に変換、 SVG Pathにする。地図座標 -> デカルト座標系への変換。

それをTHREE Meshマテリアルのパスにして3Dオブジェクトとして生成する。

demo

国内の地図データが必要なときは、国土交通省国土政策局GISHPのサイトで国土数値情報のデータが公開されているので、

ここから shape 形式のデータが利用できる。

shap形式のデータを GeoJSON に直すには、GDAL というコマンドラインのツール や QGIS というpython製の素敵なツールがある。

こちらの記事がとても詳しく紹介されている。

需要があるか分からないが、SuperColliderでDMX制御についてメモ。

インターフェースとしてはENTTEC USB Proがメジャーな様。
SuperColliderではExtensionでquarksにdmxというのが存在する。結局はSerialPortでパケット整形してあげてるようだ。

これでサクっとできると思ったのだが、DMX512を送るのに baud rate 57600で、シリアルパケットが詰まるので結局oscを受けてシリアルパケットを整形する部分をPythonでする事にした。
https://github.com/c0z3n/pySimpleDMX この辺りのコードが参考になった。

null-ly:

Grunt と組み合わせて Node.js で Web アプリ作るときに、node とは別に grunt を起動するのが面倒なので、child_process を使って Node のアプリを立ち上げる時に grunt watch するようにしてみた。

var express = require('express');
var path = require('path');
var cp = require('child_process'); var app = express();
app.set('port', process.env.PORT || 3000); // grunt...

ofxiPhone のアプリで環境設定でユーザー設定を設けたくなるときがあると思います。

これはCcocoaTouchの時と同じように、プロジェクトにSettings.bundleを追加して、testApp::setup()時にNSUserDefaultsを呼び出せばできます。

1) Settings.bundleの追加

XCodeのインスペクタでxcodeprojファイルを左クリック->New Group でSettings のようなグループを作成

作成したグループを左クリック->New File で iOS / Resource の Settings Bundle を選択してプロジェクトのディレクトリに作成

前々から気になっていた ofxiPhone で Cocoa UIKit にアクセスする方法。

rootViewController になっている ofxiPhoneViewController を NavigationController に
むりやりねじ込む方法をやってみた。

ofxiPhoneViewController は setup では早すぎてまだ生成されてないので、
draw の中でdispatch_once している。もっといい方法はないのかな…。

XBee Wi-Fi を手に入れてほったらかしにしてあったので、いい加減動かしてみる。

以前やったAPIモードとADサンプリングで三軸加速度センサーを作った物を今度はWi-Fi経由で

PCに送るまでをやってみた…。

回路は以前のものと全く同じで、ボタン電池で駆動、D0~D2ピンに加速度のXYZ軸のピンをそれぞれアサインさせるもの。

XBeeはホストとして、サンプリングしたデータをアドレス指定したクライアントへUDP経由でたれ流す。