Node.js(5) three.js

前回作ったNode.jsベースのWebアプリで、Raspberry PiをWebサーバにしてデバイスの情報をブラウザに表示する目的はほぼ実現できた。
JavaScriptにはブラウザで各種データをビジュアルに表示するライブラリが豊富にあり、手軽に使えることもわかったので仕上げとしてそのあたりを試してみる。

使用したのは3Dライブラリのthree.js。WebGLをJavaScriptで手軽に使うことができる。

とりあえずテクスチャを貼った立方体を表示させて、Raspberry Piから送られてくる加速度センサの値でそれを動かすようにしてみた。
スクリーンショット 2015-05-30 14.23.50
ブラウザ側でマウスによってカメラを動かせるようにしたので、複数のクライアントから接続した場合、立方体の動きは全く同一になるが、表示される角度や大きさはクライアント側で個別のものとなる。

ソースコード一式はこちら

gitでソースをクローンしたら、以下のようにnpmでモジュールをインストールして動かすことができる。

$ git clone https://github.com/tokoya/node_three.git  
$ cd node_three  
$ npm install  
$ npm start  

ブラウザからは前回と同じように「Raspberry PiのIPアドレス:3000/users」へアクセス。

three.jsは実行時にサイトからダウンロードする事も可能なようだが、ローカルなネットワーク環境下でも動かせるように/public/javascripts/にthree.min.jsとTrackballControls.jsを予め置いてある。

データの3D処理は完全にブラウザ側で行われるので、Raspberry Piの処理能力とは無関係にリッチな表現ができるのは面白い。かなり高度な表現ができるライブラリも各種存在しているので、Raspberry Piとの組み合わせは表現の幅が広がるのではないかと思う。

広告

Node.js(4) I2C

Webサーバとクライアント(ブラウザ)間でコネクションを維持したまま通信を行うsocket.ioライブラリを、Node.jsで使用してWebアプリを作成するのは思ったよりも簡単に出来た。socket.ioを使用するとブラウザ側からリクエストを送出する事無く、サーバ側から自動的に配信を行うPUSH型のアプリを作成できるので、今回はそれを試してみる。

Raspberry PiのGPIOポートにスイッチを付けてその状態を配信するだけでも良いのだが、シンプル過ぎて面白く無いので以前Pythonで使ってみた加速度センサモジュールを今回も使用した。加速度センサモジュールについてはここを参照。

前回と同じようにexpressフレームワークが生成したコードのpackage.jsonに、必要なライブラリの定義を追加。Raspberry Pi用のI2Cパッケージはいくつか存在するようだが、今回はこれを使用した。

{
  "name": "i2c_acc",
  "version": "0.0.0",
  "private": true,
  "scripts": {
    "start": "node ./bin/www"
  },
  "dependencies": {
    "body-parser": "~1.12.0",
    "cookie-parser": "~1.3.4",
    "debug": "~2.1.1",
    "ejs": "~2.3.1",
    "express": "~4.12.2",
    "morgan": "~1.5.1",
    "serve-favicon": "~2.2.0",
    "socket.io": "^1.3.5",
    "i2c": "~0.2.1"
  }
}

加速度センサからのデータ読み出しと、socket.ioによる通信はアプリケーションのエントリポイント”/bin/www”で、サーバオブジェクト生成後に呼び出されるapp_i2c.jsに記述。

var express = require('express');
var router = express.Router();
var i2c = require('i2c');
var addr = 0x1d;
var wire = new i2c(addr, {device: '/dev/i2c-1'});

function signed(n) {
  return (n < 128) ? n : n - 256; 
}

function app_i2c(server) {
  var io = require('socket.io')(server);
  wire.writeBytes(0x16, [0x05], function(err, res){});
  wire.writeBytes(0x10, [0,0,0,0,0,0], function(err, res){});
  setInterval(function() {
    wire.readBytes(0x06, 3, function(err, res){
      io.sockets.emit('event', {
        x: signed(res[0]),
        y: signed(res[1]),
        z: signed(res[2]),
      });
    });
  }, 100);
}

module.exports = app_i2c;

加速度センサを初期化した後、100msec周期でx,y,z値を読み出し、接続されているすべてのクライアントに対してその値を送信している。
続きを読む »

Pressure Sensor

大型の台風18号と19号が連続で発生して、それに合わせたわけでは無いと思うが秋月電子通商で大気圧センサをDIP基板化したモジュールが発売されたので、台風19号の上陸前に入手して試してみた。

インターフェースはI2CとSPIが選択可能で、Raspberry Piでは不要だがI2C使用時はジャンパーでプルアップ抵抗も有効にできる。LEDも載っていて通電すると光るのだが、これは不要な気がする。コマンドからの応答が無い時に、通電してないのか他の要因なのかが識別できる程度のメリットしか無い。まあこのモジュールをそのまま製品に使う事は無いだろうし、ブレッドボードで実験するだけなら消費電力が僅かに増えるのも気にはならないだろう。

使用方法はデータシートに記載されている通り、最初にパワーダウンモードから復帰させてあとは24ビットのデータを3つのレジスタから読み出せば良い。読み出した値を4096で割ったものが気圧(ヘクトパスカル)になる。

プログラムは以下の通り

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import quick2wire.i2c as i2c
import time
import sys

LPS25H = 0x5c

def i2c_read(addr, reg):
    with i2c.I2CMaster() as bus:
        data = bus.transaction(
                    i2c.writing_bytes(addr, reg),
                    i2c.reading(addr, 1))
        return data[0][0]

def i2c_write(addr, reg, data):
    with i2c.I2CMaster() as bus:
        bus.transaction(i2c.writing_bytes(addr, reg, data))

def get_press(addr):
    xl = i2c_read(addr, 0x28)
    l  = i2c_read(addr, 0x29)
    h  = i2c_read(addr, 0x2A)
    p = h >> 16 | l >> 8 | xl
    return round(p / 4096, 1)

def get_temp(addr):
    l = i2c_read(addr, 0x2B)
    h = i2c_read(addr, 0x2C)
    t = h >> 8 | l
    if t & 0x8000:
        t = -((t - 1) ^ 0xFFFF)
    return round(42.5 + (t / 480.0), 2)

if __name__ == '__main__':
    whoami = i2c_read(LPS25H, 0x0F)
    if whoami != 0xbd:
        print('device incorrect.')
        sys.exit()
    i2c_write(LPS25H, 0x20, 0x90)
    time.sleep(1)
    try:
        while True:
            pha = get_press(LPS25H)
            print(pha, "hPa")
            temp = get_temp(LPS25H)
            print(temp, "C")
            time.sleep(1)
    except KeyboardInterrupt:
        print('\nbreak')            

実際に台風19号が関東上空を通過している時にこのプログラムを動かして見ていると、グングン気圧が下がっていったのが面白かった。気象庁が一時間おきに発表している東京都の計測データと照らし合わせると1hPa程度低い値になっていたが、これは観測地点の差によるものだろう。

ちなみにこのモジュール内には温度センサも内蔵されていて、同時に読み出してみるとADT7410を使用した温度センサモジュールよりも0.5℃ほど低い値を示した。正確な温度計が無いのでどちらが正しいのかは分からないのだが、大気圧センサに内蔵されている温度センサは気圧の補正用途で、あまり精度は高くないらしい。温度センサの値はだいたいの目安程度に考えたほうが良いだろう。

Quick2Wire(3)

Quick2WireでI2C通信を行うのは簡単で、基本的にはI2CMasterオブジェクトのtransactionメソッドを使用するだけで済む。このメソッドの引数としてI2Cデバイスに対する読み書きを記述する。

説明するよりも実際のコードを見たほうがたぶん早い。お馴染みの温度センサーADT7410をRaspberry PiのI2Cポートに接続して、温度を読み出すプログラムは以下のようになる。

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from quick2wire import i2c
from time import sleep

ADT_ADDR = 0x48

def get_temp(addr):
    with i2c.I2CMaster() as bus:
        data = bus.transaction(
                    i2c.writing_bytes(addr, 0x00),
                    i2c.reading(addr, 2))
        h = data[0][0]
        l = data[0][1]
        t = (h << 8 | l) >> 3
        if t & 0x1000:
            t = -((t - 1) ^ 0x1FFF)
        return round(t / 16.0, 2)

if __name__ == '__main__':
    try:
        while True:
            print("current temperature -> %7.2f" % get_temp(ADT_ADDR))
            sleep(1)
    except IOError as err:
        print('\nIOError: {0}'.format(err))            
    except KeyboardInterrupt:
        print('\nbreak')

with文でI2CMasterオブジェクトを作成して、transactionメソッドのパラメータとしてI2CデバイスへのRead/Writeを記述する。これは複数並べて書くことができるので、戻り値は必ず二次元配列(リスト)になる。アドレスが異なるデバイスへのアクセスを記述することもできる。たとえば

with i2c.I2CMaster() as bus:
    data = bus.transaction(
                i2c.writing_bytes(0x48, 0x00),
                i2c.reading(0x48, 2),
                i2c.writing_bytes(0x5c, 0x28),
                i2c.reading(0x5c, 1),
                i2c.writing_bytes(0x5c, 0x29),
                i2c.reading(0x5c, 1),
                i2c.writing_bytes(0x5c, 0x2A),
                i2c.reading(0x5c, 1))

このように書くと、以下のようなデータが帰ってくる

0x48のレジスタ0x00の値 0x48のレジスタ0x01の値
0x5cのレジスタ0x28の値 無効
0x5cのレジスタ0x29の値 無効
0x5cのレジスタ0x2Aの値 無効

複数のI2Cデバイスがぶら下がっているような場合や、一つのI2Cデバイスでも複数のレジスタに同時にアクセスしなければならないような場合に、まとめて記述する事ができるので便利ではないかと思う。ただし、その場合はエラー処理で注意が必要になるかも知れない。

Quick2Wire

Raspberry PiはLinuxベースのPCでありながら、I/Oポートに接続されたセンサから値を取得したり、モータやサーボを動かしたりというマイコンボード的な事が手軽にできるのが特徴で、Python言語からGPIOやI2Cを操作する手段も何種類か用意されている。

初期の頃から存在しているI2Cデバイス制御用モジュールsmbusは手軽にpythonから使えるのだが、実行時に管理者権限が必要となる。ユーザと管理者が通常は同一なRaspberry Piではあまり問題にはならないと思うが、リモートデスクトップ等の環境でどうしても一般ユーザ権限で動かさなければならないという場合に困る。

調べてみるとQuick2WireモジュールならI2Cの操作に管理者権限が不要という事なので、インストールして試してみる事にした。
apt-getではインストール出来ないので、少しばかり手順が煩雑になる。

Quick2Wireのドキュメントではvirtualenvを使用した仮想環境での使用が推奨されているので、virtualenvがインストールされていない場合は最初にその環境をセットアップ。ここでは仮想環境の名前を「python32env」とした。

$ sudo apt-get install python-pip
$ sudo apt-get install python-virtualenv
$ virtualenv -p python3 python32env

python32envディレクトリに移動してから

$ source bin/activate

で仮想環境を起動。

次にQuick2Wire-Gpio-Adminをインストール

$ git clone https://github.com/quick2wire/quick2wire-gpio-admin.git
$ cd quick2wire-gpio-admin/
$ make
$ sudo make install
$ cd ..

Quick2Wire-Python-APIをインストール

$ git clone https://github.com/quick2wire/quick2wire-python-api.git
$ cd quick2wire-python-api
$ export QUICK2WIRE_API_HOME=`pwd`
$ export PYTHONPATH=$PYTHONPATH:$QUICK2WIRE_API_HOME
$ git checkout
$ python3 setup.py install
$ cd ..

Quick2Wireを使うためにはユーザがgpioとi2cグループに所属している必要があるので、所属していない場合は以下のコマンドを実行してから再起動する。

$ sudo adduser $USER gpio
$ sudo adduser $USER i2c

pythonを起動して、Quick2WireモジュールがインポートできればOK

~/python32env $ python
Python 3.2.3 (default, Mar  1 2013, 11:53:50)
[GCC 4.6.3] on linux2 Type "help", "copyright", "credits" or "license" for more information.
>>> import quick2wire
>>>

Quick2Wireの使用法については次回に。

Temperature Sensor(3)

秋月電子通商温度センサーモジュールがRaspberry Piのsmbusモジュールで正しく読み出せないので、Perl用のHiPiモジュールを使ってみたが、その後色々と試して限定的ではあるがPythonのsmbusモジュールを使用しても温度を読み出すことが可能である事が判った。

レジスタを指定して1byteだけ読み出す方法では、どのレジスタを指定してもレジスタ0x00の値が返ってきてしまう。しかし、レジスタ0x00から複数バイトを読み出す方法なら、4バイト目までは正しく読み出せている。

i2cdumpを使って読み出した場合と、pythonプログラムで0x00~0x0bまでを読み出した場合の結果は、以下のようになる。

$ i2cdump -y 1 0x48
No size specified (using byte-data access)
0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f    0123456789abcdef
00: 0e 0e 0e 0e 0e 0e 0e 0e 0e 0e 0e 0e 0e 0e 0e 0e    ????????????????
10: 0e 0e 0e 0e 0e 0e 0e 0e 0e 0e 0e 0e 0e 0e 0e 0e    ????????????????
20: 0e 0e 0e 0e 0e 0e 0e 0e 0e 0e 0e 0e 0e 0e 0e 00    ???????????????.
30: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
40: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
50: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
60: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
70: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
90: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
a0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
b0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
c0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
d0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
e0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
$ python ADT7410test.py
Register 00  0e
Register 01  a8
Register 02  80
Register 03  00
Register 04  0e
Register 05  a8
Register 06  80
Register 07  00
Register 08  0e
Register 09  a8
Register 0a  80
Register 0b  00
current temperature -> 29.31

i2cdumpでは00x~0x2eまで同じ値が返ってきているが、プログラムで0x00~0x0bまで読み出した場合、0x00~0x03の値がくり返し返ってきている。同じプログラムでもレジスタを個別に指定して読み出した場合は、i2cdumpと同じ結果になってしまう。

$ i2cdump -y 1 0x48
No size specified (using byte-data access)
0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f    0123456789abcdef
00: 0e 0e 0e 0e 0e 0e 0e 0e 0e 0e 0e 0e 0e 0e 0e 0e    ????????????????
10: 0e 0e 0e 0e 0e 0e 0e 0e 0e 0e 0e 0e 0e 0e 0e 0e    ????????????????
20: 0e 0e 0e 0e 0e 0e 0e 0e 0e 0e 0e 0e 0e 0e 0e 00    ???????????????.
30: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
40: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
50: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
60: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
70: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
90: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
a0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
b0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
c0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
d0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
e0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
$ python ADT7410test2.py
Register 00  0e
Register 01  0e
Register 02  0e
Register 03  0e
Register 04  0e
Register 05  0e
Register 06  0e
Register 07  0e
Register 08  0e
Register 09  0e
Register 0a  0e
Register 0b  0e
current temperature -> 28.06

この違いが何故起きるのかは判らないが、ADT7410のデータシートを読むと幸いなことに基本的なコントロールはアドレス0x00~0x03のレジスタだけで行う事ができる。0x04以上のレジスタは設定温度範囲を越えたときに割り込み信号を発生させる設定値レジスタなので、現在の温度を読み出す用途だけならそのレジスタを使う必要が無い。

レジスタ0x0bがデバイスの製造番号とリビジョン番号のレジスタで、それも正しく読み出せないのだが実際に必要になる場面はあまり無いだろう。

テストプログラムは以下の通り。

#!/usr/bin/env python
# -*- coding: utf-8 -*-
 
import smbus
import time
 
class i2c:
    def __init__(self, bus, addr):
        self.b = smbus.SMBus(bus)
        self.addr = addr
    def put(self, cmd, data):
        self.b.write_byte_data(self.addr, cmd, data)
    def get(self, cmd):
        return self.b.read_byte_data(self.addr, cmd)
    def getblock(self, cmd, len):
        return self.b.read_i2c_block_data(self.addr, cmd, len)

if __name__ == '__main__':
    try:
        dev = i2c(1, 0x48)
        while True:
            result = dev.getblock(0x00, 12)
            for i in range(0, 12):
                print "Register %02x" % i, " %02x" % result[i]
            temp = (result[0] << 8 | result[1]) >> 3
            if(temp >= 4096):
                temp -= 8192;
            print("current temperature -> %7.2f" % (temp / 16.0))
            print
            time.sleep(1)
    except KeyboardInterrupt:
        print '\nbreak'

ちなみにレジスタ0x02と0x03に書き込んだ値も正しく読み出せているので、0x00~0x03までは正常に読み書きできているようだ。ただしこれは今現在の話で、将来的にモジュールのバージョンが上がった場合にどうなるかは判らないので要注意。

Temperature Sensor(2)

Raspberry Piで正しく扱えないI2Cデバイスが存在する問題、ドライバのソフトウェア的な問題だと思っていたのだが、その後色々と調べてみるとBroadcomのBCM2835チップのI2Cにバグがあるという話が見つかった。Raspberry Piに使われているのが、まさにそのBCM2835だ。

I2C Broadcom bug workaround.
Fixing the Repeated Start Condition of the I2C Bus on the Raspberry Pi by Hardware

思ったよりも根が深い問題のようだ。

チップのバグならドライバではどうしようも無いが、Perl用のHiPiモジュールではsmbusでダメなI2Cデバイスでも正しく読み書きできているように思える。HiPiのI2Cツールの実行が妙に遅いことを考えると、こちらはGPIO直叩きで読み書きしているのかも知れない。

BCM2835チップのバグについて議論されているスレッドでは、カーネルにGPIO直叩きI2C(Bitbang I2Cと書かれていた)を実装して、そちらを使うという案が出されていたので、対処する方法が無いわけでは無いようだ。チップのバグなら新しいリビジョンで修正されるかも知れないが、既存の基板が交換されるとは思えないので何らかの対処は行われる事になるのだろう。

そうでないと困る。

今のところPerlのHiPiでなら使えることが判っているので、全くダメな状態よりはまだマシ。前回作った温度読み出しのテストプログラムにI2C液晶の表示を付け加えて、温度計にしてみた。R0012031

プログラムは以下の通り。もちろんHiPiがインストールされている必要がある。

#!/usr/bin/perl

# ADT7410 I2C Temperature Sensor & I2C LCD

use strict;
use warnings;
use HiPi::BCM2835::I2C qw( :all );

my $adt = HiPi::BCM2835::I2C->new(
            peripheral => BB_I2C_PERI_1,
            address    => 0x48
        );
my $lcd = HiPi::BCM2835::I2C->new(
            peripheral => BB_I2C_PERI_1,
            address    => 0x3e
        );
# init LCD
$lcd->i2c_write(0x00, 0x38, 0x39, 0x14, 0x7a, 0x56, 0x6c);
$lcd->delay(200);
$lcd->i2c_write(0x00, 0x38, 0x0c, 0x01, 0x06);
$lcd->delay(200);
$lcd->i2c_write(0x00, 0x38);
# main
my @val;
my $tmp;
my @param;
while(1)
{
    @val = $adt->i2c_read_register_rs(0x00, 2);
    #$val[0] = 0xff; # check value
    #$val[1] = 0xff;
    $tmp = ($val[0] << 8 | $val[1]) >> 3;
    if($tmp >= 4096)
    {
        $tmp = ($tmp - 8192);
    }
    my $out = sprintf("Temp.[%7.2f%cC]", $tmp / 16, 0xdf);
    $lcd->i2c_write(0x00, 0x38, 0x80);
    @param = (0x40, unpack("C*", $out));
    $lcd->i2c_write(@param);
    sleep(1);
}

HiPiが/dev/memにアクセスするため、実行にはsudo権限が必要。

温度センサから読み出したデータをそのまま液晶に表示しているだけで、AVRやPICのような8ビットマイコンでも同じ事ができてしまうからわざわざRaspberry Piを使う必要は無いのだが、Raspberry Piならデータをネットワークで送ったり、データベースに蓄積したりといった応用が比較的簡単にできるので、そういった用途であれば意味があるだろう。