2015年5月31日日曜日

Qt Creator で Qt Quick アプリを作るときの落とし穴回避 & TIPS

接続した Android タブレットが「互換性のないデバイス」と表示されてしまう

その Android が armeabi-v7a アーキテクチャのときに Android for x86 などのキットを使うと互換性がない。

プロジェクトのプロパティ画面で、対象のアーキテクチャ用のキットを追加する。
目的のアーキテクチャ用のキットが見当たらない場合は maintenancetool.exe で追加インストールする。
maintenancetool.exe はスタートメニューの Qt の中にショートカットがあるはず。Qt のインストールディレクトリから直接起動してもよい。

QML ファイルを追加したけど、変更しても反映されないことがある

QML ファイルが Makefile の依存関係に取り込まれていない可能性があるので「ビルド」 >「qmake の実行」で Makefile を更新してみる。

2015年4月5日日曜日

読みやすいソースコードを書く方法

読みやすいソースを書くために気をつけていることを軽くまとめてみます。ソースコード例は Python ですが、特に言語に関係なく通用する話題です。業務などでコーディングする際の参考になれば幸いです。

良い名前を付ける

最優先で気をつけるのはこれです。「プログラミングは『名前』が9割」と言われることもあるくらい、名前付けには時間を割くべきだと思います。良い名前を付ければプログラムの見通しが良くなり全体像を把握しやすくなりますし、他人(または未来の自分)が保守しやすいソースコードになります。

では良い名前とはなんでしょうか?自分なりにまとめてみました。

名前と内容を一致させる

関数名なら処理内容を端的に表す名前にすべきですし、逆に名前から想像できない処理はしてはいけません。

私は実際に、副作用を持った operator +() の実装を見たことがあります。内部でメンバ変数を書き換えていたのです。 operator +() は C++ の機能で、自作クラスに関する加算(a + b)を独自に定義できるのですが、加算演算子ですからもちろん(外から見える)副作用を持つべきではありません。これは、関数が名前から想像できない処理をしてしまっている例です。

動的型付け言語ではもっと注意が必要なことがあります。例えば get_host() という関数を見たとき、これが Host クラスのインスタンスを返すのか、ホスト名を文字列として返すのか、新人さんは迷うはずです。ただ get_host() 自体は適切な関数名ですから、これを変えるのは難しいです。それより、ホスト名を扱う変数は常に host_name という名前にする、 get_host() の戻り値を host_obj という変数で受け取るなど、ホスト名と Host インスタンスを区別するように意識しましょう。

boolを返す関数はcheck…にしない

例えば hoge.check_available() だと hoge が有効なのが戻り値が真のときか偽のときか分かりません。 hoge.is_available() であれば紛れはありません。if 文の条件に書いたときに英文として自然になる名前を選ぶようにすると良いでしょう。

is, has など英語の三人称単数形を使うと適切なことが多いですが、敢えて動詞を使わないこともあります。例えば C++ のコンテナには、要素が空かどうかを調べる関数 empty() があります。これは形容詞だけの名前ですが、下手に check_empty() とするより分かりやすい関数名ですね。

コメントを付ける

これも基本中の基本でしょう。コメントはソースコードの理解を大幅に助けてくれますし、自分が後で読み返すときも、ソースコード断片を一つのかたまりとして思い出すのに大いに役立ちます。

ただ、闇雲にコメントを書けばよいかというとそうでもありません。多くの入門書にも書いてあることですが、コメントにも悪いコメントと良いコメントがあります。一般に、ソースコードから一瞬で分かることをコメントで書いてはいけません。関数コメントの例で言えば、get_size() に対して「サイズを返す」とコメントするのは単なるタイプ数の無駄というものです。関数名だけで十分な情報は伝わっていますから。一方で get_host() に対し「Host クラスのインスタンスを返す」とコメントするのは価値があります。この関数がホスト名でなく Host クラスのインスタンスを返すことが分かるからです。

一見して分かる情報はコメントしないという一般原則を頭に置きつつ、以下ではコメントのパワーが最大に発揮できる場面を考えていきましょう。

関数コメント

関数コメントは本当に役立ちますので、是非書いて下さい。関数コメントは普通、その関数の利用者に向けて書くものですが、関数の実装者にとってもその関数をより良いものにするために役立ちます。容易くコメントが書ける関数は、利用者にとっても直感的に使える関数になりやすく、長く複雑なコメントを書かねばならない関数は、利用も困難な関数になりがちです。関数の利用しやすさを図る尺度として、関数コメントが役立つのです。

関数コメントには最低限、その関数が何をするかという一言を書いて下さい。簡単な関数ならそれだけで十分です。少し大きな関数には、引数と戻り値の意味をコメントします。実践に則した関数の使用例があるとなお良いです。Python なら関数コメントに実行例を含めて python -m doctest hoge.py などと自動テストができますから、そのテストに合格するように書くと完璧です。

def fetch_hosts(conn, klass):
    '''class が `klass` であるホストオブジェクトすべてをテーブルから読み込んで返す。

    conn    - DB コネクションオブジェクト
    klass   - Host クラスのサブクラスオブジェクト

    >>> conn = MySQLdb.connect(host='127.0.0.1', db='hostdb')
    >>> hosts = fetch_hosts(conn, AppHost)
    >>> conn.close()
    '''

    # do something

ソースコードコメント

ソースコード断片に対してコメントを書くことも可読性の向上に役立ちます。ソースコードを意味のまとまりごとに空行を入れて分け、それぞれのソースコード断片に対して1行から数行のコメントを書くと調度良い感じです。コメントは、その断片が何をしようとしているのか、意図を書くようにします。例えば、注文履歴をもとに各商品が何個売れたかを計算するプログラムにコメントを書いてみます。

# 昨日の注文履歴をすべて取得する。
begin_dt = date.today() - timedelta(days=1)
end_dt = date.today()
sells = fetch_sells(conn, begin_dt, end_dt)

# 商品ごとの注文数をカウントする。 { 商品名 : 注文数 }
item_sells = defaultdict(int)
for sell in sells:
    item_sells[sell.item_name] += sell.count

このように、それぞれのソースコード断片が何を目的としているかを簡単に書いておくと、他人がソースコードを理解するときにも、自分が後で処理の全体像を読み返すときにも役立ちます。プログラムを単に日本語に変換しただけのような、ソースコードを読めば分かるコメントにならないように注意してください。

グローバル変数を使わない

グローバル変数やシングルトンなど、どこからでも参照できる変数があるとソースコードが読みにくく、保守しにくくなる場合が多いです。あるソースコード断片が上手く動くかどうかがその変数の状態に依存する場合、分かりにくいバグの原因となります。例えば、DBに接続してユーザ一覧を取得するプログラムを考えます。

setup_db()
user_db = UserDB.instance()
users = user_db.get_users()

setup_db() は UserDB.instance() を初期化してデータベースに接続する関数です。User.instance() はシングルトンの実装になっていますので setup_db() はあるプロセスの中で1回だけ呼び出せば良く、他の場所でユーザ一覧が欲しいと思えば以下のように書くことになります。

# setup_db() ?
user_db = UserDB.instance()
users = user_db.get_users()

ここで、プログラマは setup_db() を呼び出すべきかどうかを考えます。もし、この部分より前で setup_db() が呼び出されるなら、基本的にはここで呼ぶ必要はありません。しかし、もしこの2行が直前の setup_db() の呼び出しと別プロセスで動くなら、再度 setup_db() を呼ぶ必要があります。別プロセスで動くかどうかはこの2行からは判断できず、プログラム全体のアーキテクチャを理解する必要があります。この例では、グローバル変数の使用が保守性を著しく低下させています。

2015年1月31日土曜日

bossac で Arduino Due に書き込む

Mac OS X Mavericks のターミナルで Arduino Due にプログラムを書き込む方法です。
getting-started_flash.bin をフラッシュメモリに書き込んでみます。

stty -f /dev/cu.usbmodem1411 1200
/Applications/Arduino.app/Contents/Java/hardware/tools/bossac -U false -p cu.usbmodem1411 -e -w -v -b getting-started_flash.bin -R

bossac で Arduino Due に書き込む前に stty コマンドで通信速度を 1200 bps に設定するのがミソです。これをやらないと "No device found on cu.usbmodem1411" というエラーが出てしまいます。

サンプルプログラムの準備

ちなみに、書き込むための bin ファイルは ASF (Atmel Software Framework) に付属のサンプルプログラムを GCC でビルドしました。ダウンロードページに "ASF standalone archive for GCC (makefile-based) ..." というリンクがありますので、そこからダウンロードして展開します(執筆時点での最新版が 3.20.1 でした。以下 ~/Downloads/xdk-asf-3.20.1 に展開したとします)。

$ export PATH=$PATH:/Applications/Arduino.app/Contents/Java/hardware/tools/gcc-arm-none-eabi-4.8.3-2014q1/bin/
$ cd ~/Downloads/xdk-asf-3.20.1/sam/applications/getting-started/sam3x8e_arduino_due_x/gcc
$ make
/Applications/Xcode.app/Contents/Developer/usr/bin/make all PROJECT_TYPE=flash
CC      common/services/clock/sam3x/sysclk.o
MKDIR   common/services/serial/
CC      common/services/serial/usart_serial.o
MKDIR   common/utils/interrupt/
...
$ ls *.bin
getting-started_flash.bin getting-started_sram.bin

まず Arduino IDE 付属の GCC コマンドへのパスを通します。
パスを通したら展開したソースコードの sam/applications/getting-started/sam3x8e_arduino_due_x/gcc に移動します。
最後に make を実行すると、カレントディレクトリに getting-started_flash.bin が出力されるはずです。

ASF には Arduino Due で使えるサンプルプログラムが幾つか入っています。以下のように探せます。

$ cd ~/Downloads/xdk-asf-3.20.1/sam
$ find . -name Makefile | grep arduino
./applications/getting-started/sam3x8e_arduino_due_x/gcc/Makefile
./drivers/adc/adc_example/sam3x8e_arduino_due_x/gcc/Makefile
./drivers/adc/adc_temp_sensor_example/sam3x8e_arduino_due_x/gcc/Makefile
./drivers/adc/adc_threshold_wakeup_example/sam3x8e_arduino_due_x/gcc/Makefile
./drivers/chipid/chipid_example/sam3x8e_arduino_due_x/gcc/Makefile
./drivers/dacc/sinewave_example/sam3x8e_arduino_due_x/gcc/Makefile
./drivers/gpbr/unit_tests/sam3x8e_arduino_due_x/gcc/Makefile
...

2015年1月19日月曜日

同じval++;でもaddになったりincになったり。

最近GCCのビルドをたくさんやってるなかで気づきました。
int val = 0;
val++;
という単純なC言語プログラムでさえ、i386向けのGCCとi686向けのGCCで出力されるバイナリが違うことを。

違うのは変数のインクリメントでした。i386向けGCCは
inc dword [ebp-4]
を出力しました。一方でi686向けGCCは
add dword [ebp-4], 1
を出力したのです。

addの方がバイナリが大きくなるのですが、なぜi686向けはそちらを選択したのか。僕はその理由を知りません。不思議です。

2015/01/20追記
Twitterで良い記事を紹介していただきました。
この記事によれば、inc/dec命令でストールが発生するアーキテクチャがあるそうな。そこで、そういうアーキテクチャではincの代わりにaddを使うんだそうです。
x86アーキテクチャの闇は深い。

2015年1月16日金曜日

Python の exit(), sys.exit(), os._exit() の違い

Python には 3つの似たようなプログラム終了用の関数があります。
exit(), sys.exit(), os._exit()です。
これらの違いを簡単に調べてまとめてみました。

ものぐさな人へ


  • インタラクティブシェルを終了するには exit()
  • スクリプトの中でメインプロセスを終了するには sys.exit()
  • fork() した子プロセスを終了するには os._exit()


exit([code=None])

site モジュールにより、プログラムの起動時に自動的に追加される定数。
exit 自体が表示されると画面に "Use quit() or Ctrl-D (i.e. EOF) to exit" のようなメッセージを表示し、
exit() と呼び出すと、終了コードを伴って SystemExit 例外を投げる不思議なオブジェクト。

sys.exit() にも言えるが、例外を投げるだけなので、その外側で例外を捕捉してシステムを終了させないようにすることも可能。

sys.exit([arg])

終了コードを伴って SystemExit 例外を投げる関数。
つまり、上で紹介した exit() と呼び出し時の挙動は同じ。

os._exit(n)

終了ステータス n でプロセスを終了する。
これは exit(), sys.exit() と違って、例外を投げるのではなくプロセスを終了させる。
os._exit() は fork() された後の子プロセスを終了させるときに使うのが標準的。

2014年7月21日月曜日

PCで音声認識してmbedを制御する(解説編)

この記事はPCで音声認識してmbedを制御するに対する解説記事です。



全体構成

システム全体の構成を以下に示します。大きく4つの部分から構成しています。

mbed

パソコンのシリアルポートと接続し、パソコンからJSON形式の命令を受け取って動作します。
実際にはmbedとパソコンをUSBケーブルで繋ぐと、仮想的なRS-232ポートとして認識されます。

Webサーバー

Webブラウザとmbedの橋渡しをします。Webブラウザから受け取ったGET要求を解釈し、シリアルポートにJSON形式の命令を送ります。
JavaScriptからシリアルポートを直接扱えませんので、このサーバーを作成して橋渡しさせます。

index.html

マイクの音声をWeb Speech APIで文字列に変換し、その文字列を元にWebサーバーに命令を送ります。
XMLHttpRequestを使い、http://localhost:8080/mbed?cmd=right
のようなURLにGET要求を送ります。

音声認識サーバー

Web Speech APIが内部的に呼び出すサーバーです。いや、もしかしたらWeb Speech APIは外部サーバーを使わないで実装されてるかもしれませんが、まあほぼ、Googleのサーバーを使っていると思うわけです。なんせこの記事の執筆時点では、Google ChromeでしかWeb Speech APIが使えませんので。
Web Speech APIの利用側からは音声認識サーバーを意識する必要はありません。

動作

自作Webサーバーに置いてあるindex.htmlをGoogle Chromeで開き、パソコンのマイクに向かってしゃべります。「みぎ!」とか「ひだり!」とか。
次にWeb Speech APIを使い、その音声をテキストに変換します。「右」とか「左」という文字列が帰ってきます。
その結果を基にして、自作WebサーバーにGETリクエストを送ります。http://localhost:8000/mbed?cmd=right
という感じのURLに対してXMLHttpRequestを使ってGETすればOKです。

GETリクエストを受け取ったWebサーバーは、pySerialライブラリを使ってmbedにJSON形式の命令を送ります。
コマンドは{"cmd"="right"}みたいな文字列を送ります。rightの部分には、GETリクエストのcmd引数の値を埋め込みます。

mbedは常にシリアルポートを監視しており、1文字来る度にpicojsonライブラリで解析します。JSON形式の数値、文字列、オブジェクトのいずれか一つ受信するとpicojsonから処理が帰ってきますので、picojsonから得られたJSONオブジェクトを利用し、mbedに付いてるLEDを制御します。例えば以下のように。
if (obj["cmd"] == "right") {
    led1 = 1;
}

詳細説明

picojsonとmbed

picojsonは色々な使い方ができます。std::istreamの>>演算子を使う、文字列のバッファを与える、入力イテレータを与える、という3つの方法がありますが、今回は一番最後の入力イテレータの方法を使います。
シリアルポートからは標準入力とか文字列のバッファと違い、データが1文字ずつ送られて来ますし、JSONオブジェクトの区切りもpicojsonに自動でやって欲しいからです。

ここでちょっと問題がありました。std::istream_iteratorを使うと、picojsonの実装の関係で文字が1文字遅れてしまうのです。この問題を詳しく書くのは別の機会に譲るとして、次のようなデータをシリアルポートに送ると、最後の{が受信された時点で、初めて1つのJSONオブジェクトがpicojsonから返されます。
{"cmd"="right"}{
これはまずいです。mbedを制御しようとしてPythonから命令を送ってもすぐには反応せず、次の命令を送ったときに初めて前の命令が実行される、という動作になってしまうのです。
これを解決するのに、自作の入力イテレータを実装しました。それが次のクラス。


template <class T, class CharT = char,
          class Traits = std::char_traits<CharT>,
          class Distance = std::ptrdiff_t>
class NoDelayIstreamIterator
{
    mutable std::istream_iterator<T, CharT, Traits, Distance> iter_;
    mutable bool next_;
public:
    typedef typename std::istream_iterator<T, CharT, Traits, Distance>::istream_type istream_type;

    NoDelayIstreamIterator()
        : iter_(), next_(false)
    {}
    NoDelayIstreamIterator(istream_type& s)
        : iter_(s), next_(false)
    {}
    NoDelayIstreamIterator(const NoDelayIstreamIterator& x)
        : iter_(x.iter_), next_(x.next_)
    {}

    const T& operator*() const
    {
        if (next_)
        {
            ++iter_;
            next_ = false;
        }
        return *iter_;
    }

    NoDelayIstreamIterator<T, CharT, Traits, Distance>& operator++()
    {
        next_ = true;
        return *this;
    }

    NoDelayIstreamIterator<T, CharT, Traits, Distance> operator++(int)
    {
        NoDelayIstreamIterator<T, CharT, Traits, Distance> temp(*this);
        temp.next_ = true;
        return temp;
    }

    bool operator==(const NoDelayIstreamIterator& y) const
    {
        return iter_ == y.iter_ && next_ == y.next_;
    }

    bool operator!=(const NoDelayIstreamIterator& y) const
    {
        return ! (*this == y);
    }
};

operator++では実際にはデータを読み込まず、続くoperator*の実行でデータを読み取るのがポイントです。picojsonでは++の後は必ず*が呼ばれますので、ちょっと実装をサボっています。連続で++を呼び出してはいけません。

利用側は次のようなコードになります。

int main() {
    picojson::value json;
    picojson::default_parse_context ctx(&json);
    NoDelayIstreamIterator<char> input(cin);
    string err;
    
    for (;;)
    {
        input = picojson::_parse(ctx, input, NoDelayIstreamIterator<char>(), &err);
        if (!err.empty()) {
            cerr << err << endl;
        }
        
        picojson::value::object& o = json.get<picojson::value::object>();
        std::string cmd = o["cmd"].get<std::string>();

後はmbedのLEDをDigitalOut - ピン出力で紹介されているような方法を使うなどして、LEDをピカピカさせましょう!(筆者は4つのLEDを左右へ順番に光らせるプログラムを書いたのですが、ソースコードを消してしまったので掲載できませんでした。ごめんなさい)

PythonとpySerial

mbed側でJSON形式の文字列をシリアルポート経由で受け取り、LEDを制御するまで解説しました。ここではパソコン側の実装を説明します。パソコン側は、Pythonを用いてWebサーバーを作り、その上でindex.htmlを動かし、またシリアルポートへJSON文字列を送信します。

まずWebサーバーです。PythonでWebサーバーというと幾つかライブラリがあります。今回はTornadoを使いました。

その前にWebサーバーの解説

Webサーバーと言われてピンと来ない方のために少し解説します。Webサーバーとは超大雑把に言えば、ファイルを配信するコンピュータのことです。皆さんは普段、インターネットで情報を見るのにInternet ExplorerとかFirefoxとかGoogle ChromeとかのWebブラウザーを使っていると思います。現にこのブログを見てくださっているということは、ほぼ確実に何らかのWebブラウザーを使っているはずです。そして、皆さんが見ている情報は、Webサーバーが配信してくれています。
このブログの記事も、例にもれずBloggerさんが保持するWebサーバー(以降、Bloggerサーバー)上に保管されています。Webブラウザーを使ってBloggerサーバーへ「uchanoteブログの2014年5月『PCで音声認識してmbedを制御する』の記事をくれ!」と要求すると、Bloggerサーバーが該当の記事の内容を送り返してくれるのです。

その要求を具体的に表すのがURLです。「uchanoteブログの2014年5月『PCで音声認識してmbedを制御する』の記事」を要求する場合、次のようなURLになります。WebブラウザーにこのURLを打ち込むと該当の記事を読めます。

http://uchanote.blogspot.jp/2014/05/pcmbed.html

URLには構造があります。大きく以下の3つの部分に別れています。

  • "http://":通信方式を表します。以降の話には関係ないので、詳しくは書きません。
  • "uchanote.blogspot.jp":Webサーバーを識別する名前です。他のサーバーと重複しない名前になっています。
  • "/2014/05/pcmbed.html":実際に内容を要求するファイルの名前です。2014年の5月のpcmbed.htmlという記事を要求しています。

という感じです。このURLをWebブラウザーのURL欄に貼り付けますと、内部的に
GET /2014/05/pcmbed.html HTTP/1.1
という命令に変換され、Webサーバーへと送信されます。Webサーバー側はこの文字列を読み込んで解析し、「ふむふむ。Webブラウザー君はuchanote.blogspot.jp上の/2014/05/pcmbed.htmlというファイルをGETしたいのだな」と理解します。

Webサーバーが送り返すのはファイルでなくてもいい

で、ここが大切なのですが、/2014/05/pcmbed.htmlの部分は実際のファイル名である必要がありません。Webサーバーが理解できる何かの名前であればいいのです。そこにファイル名を書けば、Webブラウザーへ送るべきデータをファイルとして保管しておけて楽だ、というだけなのです。
例えば
GET /happy_birthday HTTP/1.1
という命令を受けて、実際には/2014/05/pcmbed.htmlのファイルを送り返してもいいですし、そもそも実在するファイルの中身を送り返す必要もありません。Webブラウザーは、Webサーバー側に本当に/happy_birthdayという名のファイルが有るのか、それともWebサーバーがその場限りの適当なデータをでっち上げているのかは判断できません。

WebサーバーはWebブラウザーから文字列を受け取ると

  1. 文字列を解釈してどんな命令かを理解し
  2. 命令に基いて何か作業をし(普通は、送り返すためのデータを準備する)
  3. Webブラウザーにデータを送り返す
という動作をします。Webサーバーは本来、3番目が最終的な目的なのですが、今回は2番目が主役となります。Webブラウザーからの命令に基いてmbedを制御する、これこそが今回やりたかったことです。Webブラウザーに送り返すデータなんて、「Success」のような固定の文字列で十分です。(何も送り返さなくてもいいくらいです)

PythonでWebサーバー

ということで、今回作るWebサーバーは
GET /なんちゃら HTTP/1.1
の「/なんちゃら」の部分をmbed制御用指令だと解釈し、mbedに信号を送るものにします。こうすることで、WebブラウザーのURL欄に「http://localhost/なんちゃら」と入力すると、mbedが「なんちゃら」の通りの動きをしてくれます。ということで作ったWebサーバーのソースコードを以下に示します。全体でこれだけです。

import argparse
import serial
import tornado.ioloop
import tornado.web

class IndexHandler(tornado.web.RequestHandler):
    def get(self):
        f = open('index.html')
        self.write(f.read())

class MainHandler(tornado.web.RequestHandler):
    def get(self):
        cmd = self.get_argument('cmd', default=None)
        if cmd is not None:
            s = '{{"cmd":"{0}"}}'.format(cmd)
            serial_port.write(s.encode('ascii'))
            self.write('Sent command to serial port: ' + s)
            print('Sent command to serial port: ' + s)
        else:
            self.write('cmd parameter must be specified')

app = tornado.web.Application([
    (r'/', IndexHandler),
    (r'/mbed', MainHandler),
])

if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Fujimin Web Server')
    parser.add_argument('--serial-device', default='/dev/tty.usbmodem1422')
    args = parser.parse_args()

    global serial_port
    serial_port = serial.Serial(args.serial_device, 9600)
    app.listen(8080)
    tornado.ioloop.IOLoop.instance().start()

「/なんちゃら」の部分は
app = tornado.web.Application([
    (r'/', IndexHandler),
    (r'/mbed', MainHandler),
])
に対応します。「/」をGETするとIndexHandlerが、「/mbed」をGETするとMainHandlerが起動します。詳しく言うと、
GET / HTTP/1.1
というGET要求についてWebブラウザーに送るデータを準備するのがIndexHandler、
GET /mbed HTTP/1.1
というGET要求についてWebブラウザーに送るデータを準備するのがMainHandlerということです。

IndexHandlerクラスの中のgetメソッドを見てみると、index.htmlファイルを開き(open)、そのファイルの中身を全部読み込んで(read)、それをWebブラウザーに送ります(self.write)。簡単ですね。普通のWebサーバーの動作です。
ちなみに、index.htmlファイルにはWeb Speech APIを利用した音声認識の仕組みと、音声認識の結果を使って/mbedにGET要求を発行する仕組みが入っています。詳しくは後で説明します。

MainHandlerクラスの中のgetメソッドが大切な部分です。この中の
serial_port.write(s.encode('ascii'))
という行が、実際にmbedに指令を送っている行です。mbedはパソコンのシリアルポートにつながっているので、そのシリアルポートに対して送りたい文字列をwriteします。

cmd = self.get_argument('cmd', default=None)
という行で、GET要求の「cmd引数の値」を取得してきます。上では解説しませんでしたが、GET要求の「/なんちゃら」は詳しく見ると構造を持っているのです。

GET /mbed?cmd=right HTTP/1.1
などと書くと、/mbedというファイル(本当はファイルではありませんが、Webブラウザーからはファイルに見えています)をGETするときに、追加の情報をWebサーバーに送ることができます。それが引数です。この例では「?cmd=right」の部分が引数ですね。

で、その引数を取得するのがself.get_argumentです。
cmd = self.get_argument('cmd', default=None)
と書けば、「cmdという引数名」に設定された値を取ってきます。今回の例では「right」が取得されます。もし、GET要求にcmd引数がなかったり、cmd引数に値が設定されていない場合、Noneとなります。
要するに
GET /mbed?cmd=hoge HTTP/1.1
というGET要求がくれば、変数cmdには"hoge"という文字列が入ります。
WebブラウザーのURL欄に「http://localhost/mbed?cmd=hoge」と入力することでこのGET要求を発行することができます。

後は取得した引数の値を使ってmbedマイコンが解釈できる指令を作成し、mbedに送るだけです。今回作成したmbedマイコンのプログラムはJSON形式の指令を受け付けるので、
'{{"cmd":"{0}"}}'.format(cmd)
と書いてJSON形式の文字列を生成しています。.format(cmd)は{0}にcmdを埋め込む命令なので、結果として「{"cmd":"hoge"}」という文字列になります。({{が{に、}}が}になってるのは.formatの機能です。誤植ではありません)

pySerial

Webサーバーで残るはシリアルポートの説明です。

PythonでRS-232を使うにはpySerialを使います。Windows、Mac、Linuxで同じようにシリアルポートの制御プログラムを作れます。

mbedマイコンは実際にはUSBでパソコンと繋がりますが、mbed側にUSBとシリアル通信の変換チップが載っているため、プログラムからはあたかもシリアルポートで接続されているように見えます。ということで、pySerialで簡単に制御できます。

実際にpySerialを使っている部分は、Webサーバーのプログラムに組み込まれています。まずシリアルポートを開きます。
serial_port = serial.Serial(args.serial_device, 9600)
args.serial_device という名前のシリアルポートを、通信速度 9600bps で開きます。args.serial_device にはWebサーバーの起動時に指定したシリアルポート名、例えば "/dev/tty.usbmodem1422"が入ります。Windowsだと"COM3"とかになるでしょう。

シリアルポートを開いたら、後はシリアルポートに対してデータを送ったり、今回はやっていませんがシリアルポートからデータを受け取ったりできます。今回のWebサーバーのプログラムでデータを送っているのが以下の行。
s = '{{"cmd":"{0}"}}'.format(cmd)
serial_port.write(s.encode('ascii'))
先ほど開いたシリアルポート「serial_port」に対して、変数sの中身を送ります。変数sには「{"cmd":"right"}」のような文字列が格納されていますので、それをそのままmbedに送ればいい…と思いきや、pySerialのwriteメソッドは、引数に「文字列」を渡せません。文字列を「バイト列」に変換してから渡しましょう。

この辺りの話を詳しく書こうとすると本が一冊書けてしまいますので、表面的な説明のみに留めます。Python(特にバージョン3)では、文字列とバイト列を明確に区別します。文字列とは、そのままの意味で0個以上の文字の列です。文字列「cmd」は「c」と「m」と「d」の3文字の列です。
で、この文字列をシリアルポートに送るには、文字の列ではなくバイトの列に変換する必要があります。文字列に対して「文字列.encode('文字コード')」と書けば、文字列をバイト列に変換できます。ある文字をどんなバイト列に変換するか(例えば文字「c」を16進数で「0x63」に変換する)というルールが文字コードです。今回はASCII文字コードを使います。「"cmd".encode('ascii')」と書けば「0x63」「0x6d」「0x64」の3バイトの列になります。

ということで、sに「{"cmd":"right"}」が入っているときにs.encode('ascii')と書けば、「7b, 22, 63, 6d, 64, 22, 3a, 22, 72, 69, 67, 68, 74, 22, 7d」という15バイトの列になります。それをserial_portにwriteすることでmbedに送信します。

index.html

パソコンのマイクから音声を受け取り、Web Speech APIで文字列に変換し、自作WebサーバーにGET要求を発行するためのHTMLファイルです。ファイル全体を以下に示します。
<!DOCTYPE HTML>
<html lang="ja">
  <head>
    <meta charset="UTF-8">
    <title>Web Speech API Test</title>
  </head>
  <body onload="recognition.start();">
    <form>
      <input type="text" id="speech_result" value="認識結果"></input>
    </form>
    <script>
      if(typeof(String.prototype.trim) === "undefined")
      {
        String.prototype.trim = function() 
        {
          return String(this).replace(/^\s+|\s+$/g, '');
        };
      }
      var recognition = new webkitSpeechRecognition();
      recognition.continuous = true;
      recognition.lang = 'ja-JP';
      recognition.onsoundstart = function(event) {
        console.log(event);
      }
      recognition.onresult = function(event) {
        console.log(event);
        var word = event.results[event.results.length - 1][0].transcript;
        word = word.trim();
        document.getElementById('speech_result').value = word;
        var cmd = null;
        if (word === '右') {
          cmd = 'right';
          } else if (word === '左') {
          cmd = 'left';
          } else if (word === '止まれ') {
          cmd = 'off';
        }
        if (cmd != null) {
          var request = new XMLHttpRequest();
          request.open('GET', '/mbed?cmd=' + cmd);
          request.send();
          console.log(cmd)
        }
      }
    </script>
  </body>
</html>

Web Speech APIを使う準備が以下の部分です。
      var recognition = new webkitSpeechRecognition();
      recognition.continuous = true;
      recognition.lang = 'ja-JP';
      recognition.onsoundstart = function(event) {
        console.log(event);
      }
      recognition.onresult = function(event) {
        ...
      }

Web Speech APIについて詳しくはWebアプリに高機能な音声認識を追加するWeb Speech API - Kesin's diaryなどを参考にしてください。
var recognition = new webkitSpeechRecognition();
でWeb Speech APIインスタンスを作り、各種設定をします。

  • continuous = true : 連続変換モードを有効にする。マイクに向かって喋るたびに、自動で音声認識が行われます。
  • lang = 'ja-JP' : マイクに入る音声が日本語であることを示します。
  • onsoundstart = function(event) : 音声認識が始まったときに呼ばれる関数を設定します。今回はデバッグ用にJavaScriptコンソールにeventを表示しています。
  • onresult = function(event) : 音声認識が完了したときに呼ばれる関数を設定します。この関数の中身が、index.htmlファイルの中で最も重要な処理です。以下で説明していきます。

Web Speech APIで変換した結果を、デバッグ用に画面に表示する処理が以下です。Web Speech APIの変換結果はevent.resultsに書き込まれていますので、そこから希望の文字列を取得し、speech_resultテキストボックスに書き込みます。画面へ表示せずともmbedは制御できますが、Web Speech APIがどのように変換してくれたかを確認したいので表示しています。
        var word = event.results[event.results.length - 1][0].transcript;
        word = word.trim();
        document.getElementById('speech_result').value = word;
文字列を表示する場所として、inputタグを用いて1行テキストボックスを作っています。document.getElementByIdで探せるように、id="speech_result"としておきます。
    <form>
      <input type="text" id="speech_result" value="認識結果"></input>
    </form> 

自作WebサーバーにGET要求を送るのが以下です。
          var request = new XMLHttpRequest();
          request.open('GET', '/mbed?cmd=' + cmd);
          request.send();
XMLHttpRequestを使うと、同じWebサーバー内に置いてある別のページに対しGET要求を送れます。ここでは/mbedページに対し、?cmd=rightのような引数を付けたGET要求を生成し、発行します。すると、自作WebサーバーのMainHandlerが起動する、という仕組みです。

まとめ

以上で今回作ったシステムの全体を解説しました。index.htmlがユーザーとの接点となり、Web Speech APIでマイクからの音声をテキストに直し、そのテキストを元にWebサーバーへGET要求を発行し、Webサーバーがmbedにシリアル通信で指令を出します。
長文となってしまいましたが、この記事が何かのお役にたてれば幸いです。

2014年5月6日火曜日

PCで音声認識してmbedを制御する

PCのマイクで拾った音声の認識結果を用い、USB接続のmbedを制御しました。
今は詳しく書く気力がないので、作ったものを軽く紹介するだけにします。

使った技術


  • Web Speech API
    • HTML5で策定された仕様。まだGoogle Chromeでしか使えないけど、マイクからの音声を認識して、テキストとして取得できます。
  • Tornado
    •  HTTPサーバを構築するためのPython用ライブラリ。Web Speech APIを動かす基盤として、またmbedの操作窓口として使っています。
  • pySerial
    • Pythonでシリアルポートを使うためのライブラリ。シリアルポート経由でmbedと通信するために使います。
  • mbed
    • お手軽マイコン。PCからの命令を受け取ってLEDをチカチカさせます。

結果


「右」「左」「止まれ」の3つの声を認識し、mbedのLEDを制御できました。
最終的にはmbedにロボットを接続して、声で動かす予定です。