SyntaxHighlighter

2016年11月16日水曜日

Node.js+WebSocketでEnOceanセンサを扱う

目的

Raspberry PiでEnOceanセンサデータを受信し、ブラウザ(localhost)上でリアルタイムに見れるようにしたい。
WebSocketを扱う場合、PythonよりもNode.jsの方が簡単そうだったので実装してみた。

Node.jsのバージョンはv6.9.1。今のところ、MacOS 10.12.1(Sierra)で動作を確認している。
EnOceanの受信にはnode-enoceanを利用。今回使っているのは以下4種類のセンサ。
  • ドア開閉 (STM431J or CS-EO429J)
  • 人感 (HM92-01WHC)
  • 温度 (STM429J or CS-EO429J)
  • ロッカースイッチ (ESM210R)

実行例

EnOceanのセンサが動作すると、リアルタイムにdatetimeとstate欄の表示が更新される。

ソースコード(サーバ側)

USBドングルのデバイス名(ここでは/dev/tty.usbserial-FTYKW2P5)とセンサIDは要変更。
また、EnOceanプロトコルについては以前の記事を参照。
// execute at node.js v6.9.1

// サーバ設定
var fs = require("fs");
var http = require("http");
var socketio = require("socket.io");
var server = http.createServer( function(req, res) {
    res.writeHead(200, {"Content-Type": "text/html" } );
    var output = fs.readFileSync("index.html", "utf-8");
    res.end(output);
}).listen(3000);
var io = socketio.listen(server);

// EnOcean設定
require("date-utils");
var enocean = require("node-enocean")();
enocean.listen("/dev/tty.usbserial-FTYKW2P5")

// サーバからデータ送信
io.sockets.on("connection", function (socket) {
    // EnOceanの電文を受信
    enocean.on("data", function (data) {
        console.log("============================");
        var dt = new Date();
        var datetime = dt.toFormat("YYYY/MM/DD HH24:MI:SS");
        console.log(datetime);
        // 電文からdata部を抽出し、センサIDを取得
        console.log("raw   : "+data["rawByte"]); // 電文本体
        body_length = parseInt(data["rawByte"].slice(4,6),16); // data部のbyte数
        body = data["rawByte"].slice(12,12+body_length*2); // data部
        sid = body.slice(2,10); // センサID
        console.log("data  : "+body);
        console.log("sid   : "+sid);
    
        // センサの種類を識別して状態を取得
        // ドア開閉 (STM431J or CS-EO429J)
        if(sid=="0a007cd0"){
            sType = "door";
            if(body.slice(10,12)=="08") state = "open";
            else if(body.slice(10,12)=="09") state = "close";
        // 人感 (HM92-01WHC)
        } else if(sid=="040150a5"){
            sType = "motion";
            if(body.slice(14,16)=="ff") state = "on";
            else if(body.slice(14,16)=="00") state = "off";
        // 温度 (STM429J or CS-EO429J)
        } else if(sid=="040177db"){
            sType = "temp";
            val = parseInt(body.slice(14,16),16);
            state = (255.0-val)/255.0*40.0;
        // ロッカースイッチ (ESM210R)
        } else if(sid=="002c865a"){
            sType = "SW";
            if(body.slice(10,12)=="84") state = "SW1-on";
            else if(body.slice(10,12)=="88") state = "SW2-on";
            else if(body.slice(10,12)=="04") state = "SW1-off";
            else if(body.slice(10,12)=="00") state = "SW2-off";
        } else {
            sType = "unknown";
            state = null
        }
        console.log("state : "+state);

        // 送信用JSON作成
        sensorStr = '{"datetime":"'+datetime+'","type":"'+sType+'","state":"'+state+'"}';
        sensorJson = JSON.parse(sensorStr);
        
        // WebSocketでJSONを送信
        io.sockets.emit("sendLog", sensorJson);

    });
});

ソースコード(クライアント側)

<!DOCTYPE HTML>
<html>
<head>
    <meta charset="UTF-8">
    <title>WebSocket Sample</title>
    <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script>
    <script src="/socket.io/socket.io.js"></script>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
</head>
<body>

<table class="table">
    <tr>
        <th>type</th>
        <th>datetime</th>
        <th>state</th>
    </tr>
    <tr>
        <td>door</td>
        <td><div id="door-dt"></div></td>
        <td><div id="door-state"></div></td>
    </tr>
    <tr>
        <td>motion</td>
        <td><div id="motion-dt"></div></td>
        <td><div id="motion-state"></div></td>
    </tr>
    <tr>
        <td>temp</td>
        <td><div id="temp-dt"></div></td>
        <td><div id="temp-state"></div></td>
    </tr>
    <tr>
        <td>SW</td>
        <td><div id="SW-dt"></div></td>
        <td><div id="SW-state"></div></td>
    </tr>
</table>

<script type="text/javascript">
var s = io.connect();
s.on("sendLog", function (data) {
    sType = data["type"];
    if(sType == "door"){
        tgtId = "#door";
    } else if(sType == "motion"){
        tgtId = "#motion";
    } else if(sType == "temp"){
        tgtId = "#temp";
    } else if(sType == "SW"){
        tgtId = "#SW";
    }
    $(tgtId+"-dt").html(data["datetime"]);
    $(tgtId+"-state").html(data["state"]);
});
</script>

</body>
</html>

参考にしたサイト

2016年6月21日火曜日

scikit-learnで作成した分類器をエクスポートする

目的

scikit-learnで作成した分類器(決定木/ランダムフォレスト)を、外部ファイルとしてエクスポートしたい。 つまり、他のプログラムで読み込める、scikit-learnやpythonを使わずとも実装できる形式で出力したい。

決定木

作成した分類器をそのままexport_graphvizするだけ。
from sklearn.datasets import load_iris
from sklearn import tree

clf = tree.DecisionTreeClassifier()
iris = load_iris()
clf = clf.fit(iris.data, iris.target)
tree.export_graphviz(clf, out_file='tree.dot')

ランダムフォレスト

そのままexport_graphvizすることはできず、一手間必要。
ランダムフォレストで作成した分類器のclf.estimators_で決定木のリストを取得できるので、それぞれのリストに対して上記同様にexport_graphvizする。
この場合、木の本数分dotファイルが生成される。下記の例では100本の木を作成するので、tree_0.dot~tree_99.dotが出力される。
from sklearn.datasets import load_iris
from sklearn import tree
from sklearn.ensemble import RandomForestClassifier

clf = RandomForestClassifier(n_estimators=100)
iris = load_iris()
clf = clf.fit(iris.data, iris.target)      
for i,val in enumerate(clf.estimators_):
    tree.export_graphviz(clf.estimators_[i], out_file='tree_%d.dot'%i)

出力例

digraph Tree {
0 [label="X[3] <= 0.8000\ngini = 0.666666666667\nsamples = 150", shape="box"] ;
1 [label="gini = 0.0000\nsamples = 50\nvalue = [ 50.   0.   0.]", shape="box"] ;
0 -> 1 ;
2 [label="X[3] <= 1.7500\ngini = 0.5\nsamples = 100", shape="box"] ;
0 -> 2 ;
3 [label="X[2] <= 4.9500\ngini = 0.168038408779\nsamples = 54", shape="box"] ;
2 -> 3 ;
4 [label="X[3] <= 1.6500\ngini = 0.0407986111111\nsamples = 48", shape="box"] ;
3 -> 4 ;
5 [label="gini = 0.0000\nsamples = 47\nvalue = [  0.  47.   0.]", shape="box"] ;
4 -> 5 ;
6 [label="gini = 0.0000\nsamples = 1\nvalue = [ 0.  0.  1.]", shape="box"] ;
4 -> 6 ;
7 [label="X[3] <= 1.5500\ngini = 0.444444444444\nsamples = 6", shape="box"] ;
3 -> 7 ;
8 [label="gini = 0.0000\nsamples = 3\nvalue = [ 0.  0.  3.]", shape="box"] ;
7 -> 8 ;
9 [label="X[2] <= 5.4500\ngini = 0.444444444444\nsamples = 3", shape="box"] ;
7 -> 9 ;
10 [label="gini = 0.0000\nsamples = 2\nvalue = [ 0.  2.  0.]", shape="box"] ;
9 -> 10 ;
11 [label="gini = 0.0000\nsamples = 1\nvalue = [ 0.  0.  1.]", shape="box"] ;
9 -> 11 ;
12 [label="X[2] <= 4.8500\ngini = 0.0425330812854\nsamples = 46", shape="box"] ;
2 -> 12 ;
13 [label="X[1] <= 3.1000\ngini = 0.444444444444\nsamples = 3", shape="box"] ;
12 -> 13 ;
14 [label="gini = 0.0000\nsamples = 2\nvalue = [ 0.  0.  2.]", shape="box"] ;
13 -> 14 ;
15 [label="gini = 0.0000\nsamples = 1\nvalue = [ 0.  1.  0.]", shape="box"] ;
13 -> 15 ;
16 [label="gini = 0.0000\nsamples = 43\nvalue = [  0.   0.  43.]", shape="box"] ;
12 -> 16 ;
}

参考リンク

2016年3月4日金曜日

EnOceanセンサ記録(ドア開閉・人感・温度)

前回の記事ではマグネットセンサSTM429Jと温度センサSTM431Jを読み取る例を載せた。
先日、サイミックス社製のEnOcean人感センサHM92-01WHCを手に入れたので、上記と合わせ3つのセンサを家庭内(自宅ではないが)に配置してセンサログを取ってみることにした。
マグネットと人感センサは玄関に、温度センサはリビングに配置した。

数日間の記録をpythonのmatplotlibを用いて可視化した結果が以下のとおり。


温度のヒートマップに、ドア開と人感の時刻を無理やり載せた。
右のカラーバーは気温を表しており、それとは別にドア開は青、人感は赤で表示している。
縦軸が時刻、横軸が日付。つまり縦1列が1日を表している。

マグネットセンサの配置がまずかったのかドア開閉の欠落が多々あったものの、こうやって見ると、かなり詳しいレベルで生活パターンがわかってしまう。
特に、冬ということもあって温度で活動の有無がはっきりと判別できる。取り扱い注意である。

2015年12月14日月曜日

PythonでEnOceanの電文を読み取る

太陽光で動作するEnOceanマグネットセンサSTM429J、温度センサSTM431Jを利用する際のメモ。
Python2.7.9+pySerialで実装。

ESP:EnOcean Serial Protocol

詳しいドキュメントはEnOceanのサイトからDLできる

EnOceanの電文は上記の図(ドキュメントから引用)のようになっている。
  1. Sync. Byte: 同期用の信号で、常に0x55で固定。
  2. header: 4byte固定。2番目のbyteにdataの、3番目にoptional dataの長さが書かれている。
  3. CRC8H: チェックサム。
  4. data: センサIDや動作内容が記載されている。
    • センサid: dataの2~5番目に書かれている4byte。
    • マグネットセンサ(STM429J): dataの6番目のbyteが0x08なら開、0x09なら閉。
    • 温度センサ(STM431J): dataの8番目のbyteを元に算出する。例えば5dなら25.4℃。
      temp = (255.0-val)/255.0*40.0
  5. optional data: 長さはheaderに書かれている。
  6. CRC8D: チェックサム。

Pythonのソースコード

CentOS環境でPython2.7.9/pySerialにより実装した例。
シリアル通信で1byteずつ読み取る。
電文の先頭はかならず0x55になるので、それを起点としてカウントする。
# coding: UTF-8
from serial import *
from sys import exit
from datetime import datetime

port = '/dev/ttyUSB0'

# シリアルポートを開く
try:
    ser = Serial(port, 57600)
    print('open serial port: %s' % port)
except:
    print('cannot open serial port: %s' % port)
    exit(1)

# 初期化
cnt,dataLen,optLen = 0,0,0
telegraph,headList,dataList,optList = [],[],[],[]
ready = True # 電文開始のフラグ管理

# データの解釈とログの記録
while True:
    s = ser.read().encode('hex') # 1byteずつ読み込み
    if s == '55' and ready: # 電文開始
        # 変数のリセット
        cnt,dataLen,optLen = 0,0,0
        telegraph,headList,dataList,optList = [],[],[],[]
        ready = False
        print '=========='
    cnt += 1
    telegraph.append(s)
    if 2 <= cnt <= 5: # header
        headList.append(s)
    if cnt == 5: # header終了, data length取得
        dataLen = int(headList[1],16)
        optLen  = int(headList[2],16)
    if 7 <= cnt <= (6+dataLen): # data
        dataList.append(s)
    if (7+dataLen) <= cnt <= (6+dataLen+optLen): # optional data
        optList.append(s)
    if cnt == (6+dataLen+optLen+1): # 電文終了
        ready = True
        dt = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
        # ログ出力
        print dt
        print ':'.join(telegraph)
        print 'head...', ':'.join(headList)
        print 'data...', ':'.join(dataList), '(length=%d)' % dataLen
        print 'opt ...', ':'.join(optList),  '(length=%d)' % optLen
        sensorId = ':'.join(dataList[1:5]) # センサID取得
        print 'sid ...', sensorId
        # マグネットセンサ
        if sensorId == '04:00:03:df':
            if   dataList[5] == '08':
                action = 'open'
            elif dataList[5] == '09':
                action = 'close'
            print 'door...', action
        # 温度センサ
        elif sensorId == '04:00:7a:fc':
            val = int(dataList[7],16)
            temp = (255.0-val)/255.0*40.0
            print 'temp...', temp
        # 上記以外のセンサIDは無視
        else:
            continue
出力結果は以下のとおり。
今のところ変な挙動もなく、ドア開閉や気温をきちんと読み取れている。
open serial port: /dev/ttyUSB0
==========
2015-12-14 16:12:58
55:00:07:02:0a:0a:21:04:00:03:df:08:61:01:3a:b3
head... 00:07:02:0a
data... 21:04:00:03:df:08:61 (length=7)
opt ... 01:3a (length=2)
sid ... 04:00:03:df
door... open
==========
2015-12-14 16:13:05
55:00:07:02:0a:0a:21:04:00:03:df:09:66:01:3a:b3
head... 00:07:02:0a
data... 21:04:00:03:df:09:66 (length=7)
opt ... 01:3a (length=2)
sid ... 04:00:03:df
door... close
==========
2015-12-14 16:17:32
55:00:0a:02:0a:9b:22:04:00:7a:fc:00:00:5d:08:3f:01:37:90
head... 00:0a:02:0a
data... 22:04:00:7a:fc:00:00:5d:08:3f (length=10)
opt ... 01:37 (length=2)
sid ... 04:00:7a:fc
temp... 25.4117647059
当初、pySerialのreadline()で1行ずつ受信しようとしたら上手くいかなかった。
面倒でもread()で1byteずつ読み取って、ヘッダ部分からデータ長など読み取りつつ処理する必要がある。

----
2016/6/21追記:プログラムにバグがあるという指摘を受けて修正。Bool型変数readyによる、電文開始フラグに関する処理を追加した。

2015年9月8日火曜日

赤外線センサをArduino経由のシリアル通信で受信する

Arduinoに赤外線センサSE-10を接続し、Raspberry Piで読み取ってみた。

今回使ったもの

  • Arduino UNO
  • 人感センサSE-10
  • 抵抗器:330Ω,10kΩ
  • 赤色LED
  • ピンヘッダ (3pin):SE-10をブレッドボードに繋ぐため
  • Raspberry Pi

配線図

fritzingで配線図を描いてみる。
SE-10側に10kΩ、LEDに330Ωの抵抗を繋ぐ。

ソースコード

Arduino

センサが反応すると1を出力。
反応後5秒間LEDが点灯し、その間は不感時間とする(チャタリング防止のため)。
const int ledPin = 13;
const int pirPin = 2;

void setup() {
  Serial.begin(9600);
  pinMode(ledPin, OUTPUT);
}

void loop() {
  if (digitalRead(pirPin) == LOW) {
    digitalWrite(ledPin, HIGH);
    Serial.println("1");
    delay(5000); //不感帯を5秒に設定
  } else {
    digitalWrite(ledPin, LOW);
  }
  delay(100);
}

Python

pySerial
ArduinoのUSBデバイス名は/dev/ttyACM0。これはdmesg等で確認できる。
センサが感知すると、時刻と共に出力される。
# coding: UTF-8
from serial import *
from sys import exit
from datetime import datetime

# ArduinoのUSBデバイス名
port = '/dev/ttyACM0'

# シリアルポートを開く
try:
    ser = Serial(port, 9600)
    print('open port: %s' % port)
except:
    print('cannot open port: %s' % port)
    exit(1)

# 読み出しと出力
while True:
    # 1ライン単位で読み出し、末尾の改行コードを削除
    line = ser.readline().rstrip() 
    try:
        print(datetime.now(),line)
    except Exception as e:
        print(e.message)

参考

秋月電子の焦電型赤外線センサユニット(SE-10)を使う

2015年8月13日木曜日

加速度センサTWE-Lite-2525AのOTA(無線)による設定変更手順

ToCoStickがあれば、加速度センサTWE-Lite-2525Aが検出する動作の種類(1回叩く・2回叩く・落とす・動かす)や感度を無線経由で設定できる。TWE-Lite Rを使えば有線で設定することも可能だが、配線が手間なので無線の方が簡単だ。
ただ、メーカーの説明がややわかりづらいので、作業手順をメモしておく。

用意するもの

  • TeraTermが動作するWindows PC
  • 親機:ToCoStick
  • 子機:TWE-Lite-2525A

設定に必要なソフトウェア(バージョンは投稿時点のもの)

  1. ToCoStick標準アプリケーション
    • App_ToCoStick.exe
    • GUIでセンサの受信を確認できるWindows用アプリケーション
  2. TWE-Liteプログラマ
    • TWE-Programmer.exe
    • ToCoStickのアプリを切り替えるためのWindows用アプリケーション
  3. OTA設定用アプリ
    • Samp_Monitor_EndDevice_Input_JN5164_CNFMST_1_5_3.bin
    • 無線経由でTWE-Lite-2525Aの設定を変更するためのToCoStick用プログラム
  4. 超簡単!TWE標準アプリ
    • App_TweLite_Master_JN5164_TOCOSTICK_1_6_6.bin
    • ToCoStickにデフォルトでインストールされている、センサ受信用プログラム

メーカー情報

OTA設定手順

  1. ToCoStickをPCのUSBポートに挿す
  2. 「TWE-Liteプログラマ」を使ってOTA設定用アプリ(Samp_Monitor_EndDevice_Input_JN5164_CNFMST_1_5_3.bin)をToCoStickへ書き込む
  3. 「TWE-Liteのリセット」ボタンを押す
  4. TeraTermでシリアル接続(設定>シリアルポートからボー・レートを115200に設定)
  5. Enterキーを押すとメニューが出る
    --- CONFIG/Samp_Monitor V1-05-3/SID=xxxxxxxxx/LID=0x00/RC=10000 ---
    a: set Application ID (0x67726305)
    i: set Device ID (--)
    c: set Channels (15)
    x: set Tx Power (13)
    d: set Sleep Dur (500)
    w: set Sensor Wait Dur (0)
    m: set Sensor Mode (0x35)
    p: set Sensor Parameter (16)
    P: set Sensor Parameter2 ()
    k: set Enc Key (0xA5A5A5A5)
    o: set Option Bits (0x00000011)
    ---
    S: save Configuration
    R: reset to Defaults
    *** POWER ON END DEVICE NEAR THIS CONFIGURATOR ***
  6. 例として、動作モードをActive/Inactive(動かした時・静止した時)のみ検出するように設定。 pを押してSensor Parameterを8と入力、SHIFT+sで記録。一瞬だけ下記のようなメッセージが出る。
    !INF FlashWrite Success
    !INF RESET SYSTEM...
  7. Active/Inactiveモードの感度調整。
    大文字のP (Sensor Parameter2) を選んでTHA(動作のしきい値1~15000mg、初期値は2000)とTHI(静止のしきい値1~15000mg、初期値は1938)を指定。
    例えばそれぞれ1000,950ならTHA=1000,THI=950と入力。同様にSHIFT+sで記録。
  8. 設定の入力を終えたら、電池を抜いたTWE-Lite-2525AをToCoStickに近づけ、電池を入れる。すると下記メッセージが出てTWE-Lite-2525Aへの設定が完了。
    !INF REQUEST CONF FR 81020950
    >>> TxCmp Ok(tick=35040,req=#0) <<<
    !INF ACK CONF FR 81020950
    SUCCESS 81020950
  9. ToCoStickに再度「超簡単!TWE標準アプリ(App_TweLite_Master_JN5164_TOCOSTICK_1_6_6.bin)」を書き込み、リセット
あとは前回の記事にしたがってシリアル通信を行えば、指定した動作のみ受信できるようになる。

しかし、手順7に載せた感度調整パラメータを指定すると、何故か動作を認識しなくなってしまう。
理由は不明だが、以下のtweetによればプログラムに問題があるようで、現状ではデフォルトの感度のまま利用するしかなさそうだ。