SyntaxHighlighter

ラベル EnOcean の投稿を表示しています。 すべての投稿を表示
ラベル EnOcean の投稿を表示しています。 すべての投稿を表示

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年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による、電文開始フラグに関する処理を追加した。