NFC Checkin 端末 プロトタイプを作った。

しかだよ?FourSquareのチェックインってめんどくさいですね。

  1. アプリ起動
  2. GPSとネットで周辺検索(これが遅い)
  3. 今居る場所を探す
  4. チェックイン

チェックインしたいだけなのに、こんなに手間がかかります。めんどくさいとどんどんご無沙汰になって、いつしかチェックインしなくなる・・・。

そんなのってないよ




可能な限り短くしてみました。

  1. カードをかざしてチェックイン


おしまい。
ワンステップになった。やったね。

これどうやってるの?


これはNFCという近距離通信技術を使って、皆が持ってるsuicaなどのカードでチェックインをできるようにしたものです。NFCは一部のAndroid端末で利用できます。このアプリはまだ公開していません。そのうち。

流れ
  1. NFC搭載のAndroidがカードのIDを識別
  2. FourSquareのアカウントとカードのIDが関係しているかサーバーに問い合わせる。
  3. 登録済みならばチェックイン処理を行う。

とりあえずプロトタイプはAndroidだけで完結しました。このままだと普段画面が後ろを向いてしまって使いづらいので、NFCArduinoにして、AndroidADKで接続しようかと思ってます。

アラタナ研究所 に遊びに来たら是非やってね。

んじゃまた。

ADKのサンプルを動かそう

しかだよ。
ADKのバージョンが変わったりしてて正規のサンプルが動かなかったりしますよね。
というわけで、2012/9/30 現時点で動くサンプルをご紹介します。

Android

動作環境
ソースコード

Y.A.M の 雑記帳: Android Hello ADK つくった!ソースコードをほんの一部だけ修正しています。

MainActivity.java

package com.example.adksample;

import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

import android.app.Activity;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.hardware.usb.UsbAccessory;
import android.hardware.usb.UsbManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.ParcelFileDescriptor;
import android.util.Log;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.TextView;
import android.widget.ToggleButton;

public class MainActivity extends Activity implements Runnable{

	private static final String ACTION_USB_PERMISSION = "com.example.adksample.action.USB_PERMISSION";

	private static final String TAG = "ADKSample"; 
	
	private UsbManager mUsbMng;
	private PendingIntent mPermissionIntent ;
	private boolean mPermissionRequestPending;
	private UsbAccessory mAccessory;
	
	/*input output stream */
	private ParcelFileDescriptor mFileDescriptor;
	private FileInputStream mInputStream;
	private FileOutputStream mOutputStream;
	
	/*view*/
	private ToggleButton mToggleButton;
	private TextView mLedStateView;
	private TextView mStatusView;
	
	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);

		/* initialize instance */
		mUsbMng = (UsbManager) getSystemService(USB_SERVICE);		
		mPermissionIntent = PendingIntent.getBroadcast(this, 0, new Intent(ACTION_USB_PERMISSION), 0);
		
		/* receiver */
		IntentFilter filter = new IntentFilter();
		filter.addAction(ACTION_USB_PERMISSION);
		filter.addAction(UsbManager.ACTION_USB_ACCESSORY_DETACHED);
		registerReceiver(mUsbReceiver, filter);
		
		/* view */
		setContentView(R.layout.activity_main);
		mToggleButton = (ToggleButton) findViewById(R.id.toggleBtn);
		mLedStateView = (TextView) findViewById(R.id.ledState);
		mStatusView = (TextView) findViewById(R.id.status);
		
		mToggleButton.setOnCheckedChangeListener(new OnCheckedChangeListener() {
			public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
				byte command = 0x1;
				byte value = (byte) (isChecked ? 0x1 : 0x0);
				sendCommand(command, value);
			}
		});
		
		enableControls(false);  
	}
	
	@Override
	protected void onResume() {
		super.onResume();
		if(mInputStream != null && mOutputStream != null){
			return;
		}
		
		UsbAccessory[] accessories = mUsbMng.getAccessoryList();
		UsbAccessory accessory = (accessories == null ? null : accessories[0]);
		if(accessory != null){
			if(mUsbMng.hasPermission(accessory)){
				openAccessory(accessory);
			}else{
				synchronized (mUsbReceiver) {
					if(!mPermissionRequestPending){
						mUsbMng.requestPermission(accessory, mPermissionIntent);
						mPermissionRequestPending = true;
					}
				}
			}
		}else{
			Log.d(TAG, "mAccessory is null");
		}
	}
	
	@Override
	protected void onPause() {
		super.onPause();
		closeAccessory();
	}
	
	@Override
	protected void onDestroy() {
		unregisterReceiver(mUsbReceiver);
		super.onDestroy();
	}
	
	
	private final BroadcastReceiver mUsbReceiver = new BroadcastReceiver(){

		@Override
		public void onReceive(Context context, Intent intent) {
			String action = intent.getAction();
			if(ACTION_USB_PERMISSION.equals(action)){
				synchronized (this) {
					UsbAccessory accessory = intent.getParcelableExtra(UsbManager.EXTRA_ACCESSORY);
					if(intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)){
						openAccessory(accessory);
					}else{
						Log.d("adksample", "permission denied for accessory "+ accessory);
					}
					mPermissionRequestPending = false;
				}
			}else if(UsbManager.ACTION_USB_ACCESSORY_DETACHED.equals(action)){
				UsbAccessory accessory = intent.getParcelableExtra(UsbManager.EXTRA_ACCESSORY);
				if(accessory!= null && accessory.equals(mAccessory)){
					closeAccessory();
				}
			}
			
		}
		
	};
	
	private void openAccessory(UsbAccessory accessory){
		mFileDescriptor = mUsbMng.openAccessory(accessory);
		
		if(mFileDescriptor != null){
			mAccessory = accessory;
			FileDescriptor fd = mFileDescriptor.getFileDescriptor();
			mInputStream = new FileInputStream(fd);
			mOutputStream = new FileOutputStream(fd);
			
			new Thread(null, this, "DemoKit").start();
			Log.d(TAG, "accessory opend");
			
			enableControls(true);
		}else{
			Log.d(TAG, "accessory open fail");
		}
	}
	
	private void closeAccessory(){
		enableControls(false);
		
		try{
			if(mFileDescriptor != null){
				mFileDescriptor.close();
			}
		}catch(IOException e){
		}finally{
			mFileDescriptor = null;
			mAccessory = null;
		}
	}
	
	private void enableControls(boolean enable){
		if(enable){
			mStatusView.setText("connected");
		}else{
			mStatusView.setText("not connected");
		}
		mToggleButton.setEnabled(enable);
	}
	
	private static final int MESSAGE_LED = 1;  
	
	private class LedMsg{
		private byte on;
		public LedMsg(byte on) {
			this.on = on;
		}
		public boolean isOn(){
			if(on == 0x1){
				return true;
			}else{
				return false;
			}
		}
	}
	
	public void run() {
		int ret = 0;
		byte[] buffer = new byte[16384];
		int i;
		
		while(ret==0){
			try{
				ret = mInputStream.read(buffer);
			}catch(IOException e){
				break;
			}
			
			i = 0;
			while(i < ret){
				int len = ret - i;
				switch (buffer[i]) {
				case 0x1:
					if(len >= 2){
						Message m = Message.obtain(mHandler, MESSAGE_LED);
						m.obj = new LedMsg(buffer[i+1]);
						mHandler.sendMessage(m);
					}
					i += 2;
					break;

				default:
					Log.d(TAG, "unknown msg: "+buffer[i]);
					i = len;
					break;
				}
			}
		}
	}
	
	private Handler mHandler = new Handler(){
		public void handleMessage(Message msg) {
			switch (msg.what) {
			case MESSAGE_LED:
				LedMsg o = (LedMsg) msg.obj;
				handleLedMessage(o);
				break;

			default:
				break;
			}
		};
	};
	
	private void handleLedMessage(LedMsg l){
		if(l.isOn()){
			mLedStateView.setText("ON");
		}else{
			mLedStateView.setText("OFF");
		}
	}
	
	public void sendCommand(byte command, byte value){
		byte[] buffer = new byte[2];
		
		if(value != 0x1 && value != 0x0)
			value = 0x0;
		
		buffer[0] = command;
		buffer[1] = value;
		if(mOutputStream != null){
			try{
				mOutputStream.write(buffer);
			}catch(IOException e){
				Log.e(TAG, "write failed", e);
			}
		}
	}

}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>  
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
    android:layout_width="fill_parent"  
    android:layout_height="fill_parent"  
    android:gravity="center"  
    android:orientation="vertical" >  
  
    <TextView  
        android:id="@+id/status"  
        android:layout_width="wrap_content"  
        android:layout_height="wrap_content"  
        android:layout_margin="10dip" />  
  
    <ToggleButton  
        android:id="@+id/toggleBtn"  
        android:layout_width="wrap_content"  
        android:layout_height="wrap_content"  
        android:layout_margin="10dip" />  
  
    <TextView  
        android:id="@+id/ledState"  
        android:layout_width="wrap_content"  
        android:layout_height="wrap_content"  
        android:layout_margin="10dip" />  
  
</LinearLayout>  

xml/asccessory_filter.xml
manufacturerが Arduinoの AndroidAccessory の第一引数、modelが第二引数になります。

<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:android="http://schemas.android.com/apk/res/android">
    <usb-accessory manufacturer="kojika-ya" model="ADKSample" version="1.0" />
</resources>

AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.adksample"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
        android:minSdkVersion="15"
        android:targetSdkVersion="15" />

    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        
        <activity
            android:name=".MainActivity"
            android:launchMode="singleInstance"
            android:label="@string/title_activity_main" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>

            <intent-filter>
                <action android:name="android.hardware.usb.action.USB_ACCESSORY_ATTACHED" />
            </intent-filter>

            <meta-data
                android:name="android.hardware.usb.action.USB_ACCESSORY_ATTACHED"
                android:resource="@xml/asccessory_filter"/>
        </activity>
    </application>

</manifest>

Arduino

Arduino Labs - Accessory Mode browse
から、ArduinoADK.zipをDLする。

Arduino/libraries/UsbHost を
~/Documents/Arduino/libraries/
にコピーしてUsbHostをArduinoにインストール。

ソースコード
#include <AndroidAccessory.h>

# define LED 13

AndroidAccessory acc("kojika-ya", "ADKSample");

void setup() {
  Serial.begin(115200);
  Serial.print("\r\nStart");

  pinMode(LED, OUTPUT);

  if (!acc.begin()) {
    Serial.println("OSCOKIRQ failed to assert");
    while (1); //halt
  }
}

void loop() {
  uint8_t msg[64] = {0x00};
  byte led;

  acc.refresh();
  if (!acc.isConnected()) {
    digitalWrite(LED, LOW);
    return;
  }

  uint16_t len = 0;
  while (acc.available() > 0) {
    Serial.print("available");
    msg[len] = acc.read();
    len++;
  }

  if (len < 1) {
    return;
  }

  Serial.print("msg[0]" + msg[0]);
  Serial.print("msg[1]" + msg[1]);
  if (msg[0] == 0x1) {

    if (msg[1] == 0x1) {
      digitalWrite(LED, HIGH);
      msg[0] = 0x1;
      msg[1] = 0x1;
      acc.write(msg, 2);
    } else {
      digitalWrite(LED, LOW);
      msg[0] = 0x1;
      msg[1] = 0x2;
      acc.write(msg, 2);
    }
  }

  delay(10);
}

node.jsのテンプレートエンジンExpressのroutesの書き方が一瞬わからなかったのでまとめたよ


しかだよ。
Expressのroutes配下の書き方がわからないので調べてみました。
expressのルーティング - hokaccha.hamalog v2
2012-07-14 - ZeBeVogue別館
なんかしっくりこない・・・。
なんとかなるんじゃないかなと思って、色々試してみた。それっぽいまとめ方ができたので報告。

routes配下の作り方

最初のテンプレートはこんなかんじ。
web.js

routes = require("./routes");

routes/index.js

exports.index = function(req, res) {
  return res.render("index", {
    title: "Shikajiro dayo"
  });
};

機能を増やす

index以外にaddとか追加するにはこんな感じでできた。
routes/index.js

exports.index = function(req, res) {
  return res.render("index", {
    title: "Shikajiro dayo"
  });
};
//add処理の追加
exports.add = function(req, res) {
};

階層を増やす

index、addだけでは限界がある。exports.user.index, exports.user.addとか、階層を表現できるようにしたい。
routes/index.js

exports.index = function(req, res) {
  return res.render("index", {
    title: "Shikajiro dayo"
  });
};
//【これは動かない】
exports.user.add = function(req, res) {
};

だめだった・・・。

userディレクトリを作ってその中にファイルを作る。んで、親階層のindex.jsでそれをrequireする手法をやってみる。
routes/index.js

//ちゃんと動く
exports.index = function(req, res) {
  return res.render("index", {
    title: "Shikajiro dayo"
  });
};
//子階層を指定する
exports.user = require('./user');

routes/user/index.js

exports.add = function(req, res) {
}

呼び出しはこんな感じ。
web.js

var routes = require("./routes");
app.get '/', routes.index
app.post '/user/', routes.user.add

routes/index.jsで子階層を指定するのがちょびっと冗長だけど、許せる範囲!

補足

最初間違えた事書いてたので記事消しました。この記事は訂正版になります。

Google SpreadsheetをJSON 取得専用のダミーサーバーとして利用する方法

  

しかだよ。

webサービスはスピードが正義です。(まさよしじゃないよ。)
1日でも早くサービスをリリースし、1時間でも早くサーバーを構築し、1分でも早くコードを書き、1秒でも速くカチャカチャターンしなくてはいけません。

遅れの原因

そんな中、割りと時間がかかるのが『インターフェース連携』になる部分です。インターフェースの遅れは多岐にわたります。

クライアントとサーバーの疎通もその一つ。
サーバーのapiを待ってたりすると、クライアントの実装が待機状態になり、開発者はニコ動を見始めまてしまいます。
クライアント開発者にはとりあえずJSON返すwebサーバーが必要です。でも、サーバー実装力はないので、可能な限り簡単に、そして素早くテストサーバーが欲しいのです。

シートをテーブルに見立てた

というわけで作りました。Google SpreadSheetに書いたデータをJSONで取得して、あたかもgetを叩いたかのようなテーブルの形に整形するスクリプトです。はてダだとシンタックスハイライトできないのでgistに置いてます。

spreadsheetは一般公開にする必要があるので、ほんとにダミーデータだけにしてね。

サンプル

これが

こうなる

gsで書いてる時が僕にもありました。

一生懸命gsで実装してたんですが、クライアント側でやったほうが楽 & 速い事が判明しました。apiの口とかよくわからなかったので・・・。

動いたからよしとします。んじゃ!

ツイッターのタイムラインをゆっくりに喋らせるスクリプト書いた。


ゆっくりだと思った?残念!しかだよ。
ゆっくり実況見ながら寝てるので、ゆっくりボイスを聞かないと眠れない体になってきた30歳♂独身です。ゆっくりかわいいよゆっくり。

SayKanaというライブラリを使うとターミナルからゆっくりボイスを喋らせることができる、と隣の席の「はかせ」から教えてもらったのでやってみた。
SayKana - Mac用音声合成プログラム

SayKanaは、Mac OS X上で動作する日本語音声合成ソフトウェアです。OS Xに付属の say コマンド(英語の音声合成)と同様の機能を実装しています。 AquesTalk音声合成エンジンをMac OS X 上に移植したもので、基本的に『かな』からの音声合成であり、漢字かな交じり文は読み上げられません。 商用でなければ無償でご利用いただけます。

とりあえずヘルプ見る。
$ SayKana -h

NAME
saykana - 日本語用音声合成コマンド(カナからの音声生成)Ver.1.11

SYNOPSIS
saykana [-h] [-s speed] [-v f1|m1] [-o out.aiff] [-f file | kana_string]

DESCRIPTION
日本語の音声合成を行います。
say コマンドと(ほぼ)同等の機能を実装しています。
そのまま音声出力ほかに、AIFFファイルに出力することもできます。

OPTIONS
kana_string
発声するかな文字列で指定します(UTF-8)。漢字は読めません。
アクセントの指定も可能です
詳細は「AquesTalk」の音声記号列仕様を参照してください
スペース等を含む場合は で囲んで指定してください。
-f file
入力をファイルから指定するときに指定します。
kana_stringを指定せず、且つ fileに - を指定したときは、
標準入力から入力します。
-o
AIFFファイルとして出力するときにファイル名を指定します。
指定しないときは音声出力されます。
-v f1 | m1
声種を指定します。 f1:女声(default) m1:男声
-s
発話速度を指定します。 (50-300) default:100
-h
このメッセージを表示します。

EXAMPLE
$ saykana こんにちわ
$ saykana -s 150 -v m1 -o out.aiff "ファイルに しゅつ'りょくします。"
$ ls | saykana -f -

LICENCE, etc.
ライセンス、その他につきましては、下記サイトを参照してください。
http://www.a-aquest.com/aquestalk/saykana/

---- COPYRIGHT 2009 AQUEST Corp. ----

声はゆっくり霊夢とおっさんの声しか出ないのか・・・(´・ω・`)しかたあるまい。

自分のツイッタータイムラインを延々と呟かせる

自分で喋らせても寂しいので、ツイッターをゆっくり呟かせることにしてみた。
shikajiro/yukkuri-voice-tweet

仕組み
  • 英語はつぶやけないので無視。アルファベット読みもできるけど長ったらしいので無視!
  • 漢字はYahooの漢字ひらがな変換API叩いて漢字->ひらがなに変換してます。
喋らせる

ダウンロードしたらターミナルから実行してみよう。
$ python tweet.py

サンプル動画


※動画はちょっとバージョン古いです。

これでいつでもゆっくりとゆっくりできるね。んじゃ。ノシ

追記

ニコニコ動画にうpしました!

Macでの開発効率をアップする、マウントせずにローカルのソースコードをサーバーと同期するスクリプト書いた。


しかだよ。
変更があったローカルのソースコードをサーバーのソースコードと同期しながらwebアプリを開発できるスクリプトを作りました。
shikajiro/eventsync.py

2012/07/10 追記

下で説明するソースコードは古いので、shikajiro/eventsync.pyの方を見てくださいね。

webアプリ開発の問題点

  • マウントしたりsshでサーバーに接続してwebアプリを開発してると、回線が遅くて画面が固まることがあり、ストレスフルなコーディングを強いられている。
  • ローカルのソースをgitでpushしてサーバーに反映するのは複数人の場合はいいけど、一人用の場合は毎回add commit push がめんどい。

プログラマーが楽しくプログラミングできないのはです。直ちに懺悔するか解決しましょう。

開発中のwebアプリを動かす場所を比較検討

実行場所 公開範囲 反応スピード PC負荷 柔軟性
ローカルのmac × ×
vmwareLinux × ×
レンタルサーバ ×
  • ローカルでwebアプリを動かすとレスポンスとか速いんだけど、複数環境などで柔軟性に欠ける。
  • vmwareだと柔軟性高いんだけど、メモリとCPU消費が激しくて遅い。
  • 上記2つはさらにネットに公開されていないのでテストしにくい。
  • レンサバは公開されてて扱い易いけど、直接コーディングするにはレスポンスが遅い。

今この瞬間作っているものをストレス無くサーバーで確認したいのです!
というわけで、「サーバーに常時接続せず」「ローカルで開発できて」「ソースコードに変更があったらサーバーを更新する」方法を探りました。

ソースコードの同期はrsync

コードの同期はrsyncでできました。問題は「ソースコードの追加・変更・削除」をどうやって検知するかです。

lsync

Linuxディストリビューションならlsyncを使うとファイルの変更検知ができるみたいですが、Mac OS XBSDなので、これは使えませんでした。

フォルダアクション

Mac OS X にはフォルダアクションというフォルダ監視の仕組みがあります。フォルダに変更があるとautomatorを実行出来たりします。しかしこれは「フォルダへのファイルの追加」のみ検知可能だったため断念しました。

kevent kqueue

BSDLinuxとは別の方法でファイルを監視する事が可能でした。それが kevent kqueueです。
*BSD で kqueue・kevent を使ってみよう

pythonスクリプト

んで、色んなサイトのソースコードを参考にしてできました。
shikajiro/eventsync.py

import select
import os
import sys


def kevent(file):
    ke = select.kevent(file,
        filter=select.KQ_FILTER_VNODE,
        flags=select.KQ_EV_ADD | select.KQ_EV_ENABLE | select.KQ_EV_CLEAR,
        fflags=select.KQ_NOTE_DELETE | select.KQ_NOTE_WRITE
    )
    return ke

argvs = sys.argv
argc = len(argvs)
if(argc != 3):
    print 'Usage: python {} filename.'.format(argvs[0])
    quit()
folder = argvs[1]
ssh = argvs[2]
print '{} to {}'.format(folder, ssh)
print 'file update watching...'

dirs = os.walk(folder)
l = []
for root, dirs, files in dirs:
    f = os.open(root, os.O_RDONLY)
    l.append(kevent(f))
    for a in files:
        f = os.open(root + '/' + a, os.O_RDONLY)
        l.append(kevent(f))

kq = select.kqueue()
events = kq.control(l, 0, None)
while True:
    r_events = kq.control(l, 1, None)
    for event in r_events:
        print event
        if event.fflags & select.KQ_NOTE_DELETE or event.fflags & select.KQ_NOTE_WRITE:
            print "file was updated!"
            command = 'rsync -av --delete -e ssh {} {}'.format(os.path.join(os.getcwd(), folder), ssh)
            print command
            os.system(command)

python eventsync.py watchdir/ shikajiro@hostname:/dir/
第一引数に監視するディレクトリ名、第二引数に同期するsshサーバーとそのフォルダを指定します。

起動するとこんな感じ。

hogehoge/ to root@www.kojika-ya.me:/root/hogehoge/
file update watching...

ファイルに変更などがあると

building file list ... done
deleting urls.pyc
deleting settings.pyc
deleting middleware.pyc
deleting __init__.pyc
deleting api/urls.pyc
deleting api/handlers.pyc
deleting api/__init__.pyc
deleting app/models.pyc
deleting app/admin.pyc
deleting app/__init__.pyc
./
sqlite
test
api/
app/

sent 7501 bytes received 1876 bytes 6251.33 bytes/sec
total size is 3252109 speedup is 346.82
.....

こんな感じでどんどん同期されていきます。

これできっとストレスフリーな開発ができる(はず)です。