Edited at
IoTLTDay 11

IoTの台風の目!?EnOceanについて本気出して語ってみた

IoTLTアドベントカレンダー11日目です。今年のIoTLTアドベントカレンダーは3つもあるという大盛況ぶり!(凄い!)

https://qiita.com/advent-calendar/2018/iotlt

https://qiita.com/advent-calendar/2018/iotlt_neo

https://qiita.com/advent-calendar/2018/iotlt_mitz <- この記事が載ってる版


はじめに

吉野 祥之(よしの あきゆき)と申します。NTTコミュニケーションズ(株)でIoTやってます。

この1年ほど業務の傍らでIoTLTやNode-RED界隈でLT登壇などさせていただいていましたが、ご縁あってIoTLTのアドベントカレンダーに参加させていただく事となりました。宜しくお願いします。

この一年ほどIoT向けの無線プロトコルであるEnOceanを色々なところで活用してきたのですが、巷にまとまった情報源が意外となく苦労したりもしたので、これまで学んだことをまとめてみたいと思います。不正確な部分や事実誤認もあるかもしれませんが、その場合はコメント欄などでご指摘いただけると幸いです。

なお、本稿では近〜中距離無線のEnOcean1について述べ、長距離通信用のEnOcean Long Range1については触れません。両者は別物のプロトコルです。


EnOceanとは

この節は主にマーケッター向けの内容です。技術者の方や既にEnOceanについてご存知の方は次の節までスキップして構いません。

EnOcean Logo


EnOceanの概要

EnOceanとは光や温度、振動など比較的微弱なエネルギーを集めて電力に変換する「エネルギーハーベスト技術(環境発電技術)」の一つで、変換した電力で無線通信する国際規格です。「No Wires. No Battery. No Limits.」のキーワードで表される通り、無給電の無線通信という大きな特徴を持っています2。ドイツのシーメンスから独立したEnOcean GmbHによって開発され、基本特許部分もEnOcean GmbHが保持しています。

2008年にEnOceanの技術普及のためEnOcean Allianceが設立されて以降、既に400社以上の企業がメンバーとなっており、1,300種類以上の製品が出ています(※2018年現在)。

各種スイッチや窓の開閉検知、動体センサーなど、ビルオートメーションやFA(Factory Automation)の領域で採用事例を広げています。日本では介護や見守り分野での適用が先行している模様ですが、2018年になってオフィス向けソリューションなどでも事例が出始めているようです。

EnOcean Allianceの会員クラスはプロモータ(Promoter)、正会員(Participant)、準会員(Associate)の3種類です。プロモータは世界で8社おり、アジア地域ではローム社が唯一のプロモータとなっています。その他EnOcean GmbHやIBM、Honeywellなどがプロモータとして名を連ねています。なお蛇足ですが、NTTコミュニケーションズも正会員です(ニュースリリース)(※2018年現在)。


参考:



EnOceanの特徴

EnOceanの特徴は以下の3点であり、これらの組み合わせでIoT分野における大きな利点である電池レス・配線レスのセンサを実現しています。


  • 独自のエネルギーハーベスタ技術(太陽光発電や温度差発電、運動エネルギー発電など)

  • 超低消費電力回路技術(待機時電流100nA)

  • EnOceanプロトコル(ISO/IEC 14543-3-1X)

一方でエネルギーハーベスティング技術を基としていることから、あまり高頻度な通信(秒単位の通信など)には向かないと言えるかと思います。


EnOceanの技術仕様

無線の周波数はアンライセンスバンドのいわゆるサブギガ帯を利用しており、日本では928.350MHzを利用しています。北米(アメリカ/カナダ)では902.875MHz、ヨーロッパ、中国では868.300MHzです。

通信距離は屋外300m、屋内30mと言われることが多いですが利用環境によって大きく左右されます3。他の無線方式でも同様ですが、事前の現地調査は必須です。データレートは125kbpsです。

なお、2018年現在マルチホップやメッシュには未対応で、リピーターによって1段の中継を行うことのみが可能です4

EnOceanにはBLEやZigBeeのようにペアリングの機構がありません。通信は基本的に送信機からのブロードキャストです5

一方で、EnOceanでは通常送信するデータにセンサデータのプロファイル情報が含まれない6という仕様になっており、各送信機がどのプロファイルを利用しているのかを受信機側で事前に登録しておく必要があります。この事前登録のためにTeach-Inと呼ばれる仕組みが準備されています7

暗号化は長らく対応していませんでしたが、2018年になってローリングコードをAES-128で暗号化する方式に対応したチップが市場に投入され始めました。これによってEnOceanでも通信暗号化に対応可能となります。ただしこの機能を利用するにはセンサ側・受信機側の双方が新型チップに対応している必要があり、一般的に暗号化が利用可能となるのはもう少し時間がかかりそうです。


参考:



EnOceanを使ってみる


ハードウェア

EnOcean製品で一番品揃えがあるのはITストアだと思います。個人で利用可能なんだろうか。

みなさんお馴染みスイッチサイエンスでも少量ですがラインナップがあります。Amazonでも同等の品揃えのようです。

何にしても個人で試すにはお高いのがかなり疵。

EnOcean電波の受信側はローム社のUSBドングルタイプの製品(USB400J)が入手しやすく、これをRaspberry Piに挿して利用するのがもっとも手っ取り早いパターンかと思います。

usb400j.png

他の選択肢としては個人で手を出す価格レベルを軽く超えますが、ぷらっとホーム社のOpenBlocksシリーズアットマークテクノ社のArmadillo IoTシリーズではオプションとしてEnOceanモジュールを筐体に内蔵した状態で利用することもできます。


ソフトウェア

電文の中身を覗いてみるにはDolphin View Advancedが無償利用可能です。以前はDolphin View Basicが無償版でAdvancedは別のライセンスになっていたのですが、Advancedに一本化されたとのことです。


EnOceanプロトコル詳細

この節からは技術者向けの内容です。

packetImage.png

出展:EnOcean仕様書を元に独自作成

EnOceanの技術仕様はEnOcean Allianceのサイト及びEnOcean GmbHのサイトから全てダウンロード可能です。以前はGeneric Profile仕様など一部未公開仕様もありましたが、2018年に入り広く公開されるようになりました。


参考:


EnOceanには以下の4種類のプロトコルが存在します。



  • ESP3(EnOcean Serial Protocol 3)


  • ERP2(EnOcean Radio Protocol 2)


  • EEP(EnOcean Equipment Profiles 2.6.X)


  • GP(Generic Profiles 1.0)

各センサごとのデータモデル定義はEEPもしくはGPで行われており、これらのデータモデルをネットワーク経由でやりとりするためにERP2、ESP3でパケットがカプセル化されます。

ERP2、ESP3はEnOcean GmbHの定めた規格であり、EEP、GPはEnOcean Allianceの定めた規格となります。

以下、各プロトコルの中身を詳しく見ていきます。


ESP3

EnOceanモジュールと外部コンピュータを接続する時のシリアル通信に関する仕様です。

USBドングルタイプのアンテナモジュールもUSB経由でESP3パケットが検出されます。

ESPパケット部で読み取るべき主なデータは以下の通りです。



  • データ長(Data Length)


  • オプションデータ長(Optional Length)


  • パケットタイプ -> 市場製品では基本的にERP2(0x0A)


  • チェックサム(CRC8H/CRC8D)-> ヘッダー部、ボディ部の2箇所ある


  • オプションデータ(以下パケットタイプがERP2の場合)



    • サブ電文番号(SubTelNum)


    • 電波強度(dBm)



ESPパケット構造(パケットタイプがERP2の場合)

ESPパケット構造

出展:EnOcean Serial Protocol 3, pp.76

その他、ESP3に定義されている内容として注意すべきポイントを記載します。



  • UART通信関連(1.5節、1.6.4節)



    • ボーレート (baud rate) : 57600


    • データビット (data bits) : 8bit


    • 終了ビット (stop bit) : 1bit


    • パリティビット (parity bit) : なし


    • タイムアウト (time out) : 100msec




  • パケット検知(1.6節、2.4節)


    • パケットの取り出しについて例が記載されています。実際のところはUART通信のタイムアウト100msecで電文を区切りつつ、取得した電文の中身を1つずつパースしてパケットを取り出して行くことになります。

    • スイッチセンサなどを連打していると複数パケットを含む電文を受信することがあったりします。




  • チェックサム(2.3節)


    • 巡回冗長検査と呼ばれるCRC8という方法で計算されます。生成多項式は X^8 + X^2 + X^1 + 1 で、CRC8-ATMと呼ばれるものを採用しています。




ERP2

EnOceanモジュール(受信機/送信機)を無線接続する時の通信に関する仕様です。OSIでいうL1〜L3(物理層、データリンク層、ネットワーク層)のあたりが定義されています。

EEPを利用する場合意識すべきところは少ないですが、GPを利用している場合やリピーターを挟む場合などは拡張ヘッダが挿入される(4.5節を参照)などのケースがあるため注意が必要です。

ERPで読み取るべき主なデータは以下の通りです。ESP3でパケットタイプがERP2(0x0A)の場合は、ERPパケット構造に含まれるLengthは省略されます



  • ERPヘッダ(1byte)



    • 送信元ID(Originator-ID)/ 送信先ID(Destination-ID)の形式(3bit)


    • 拡張ヘッダの有無(1bit)


    • 電文タイプ(Telegram Type, R-ORG)(4bit)




  • 拡張ヘッダ(1byte、省略可)



    • リピーターカウント(4bit) -> リピーター無しの場合は拡張ヘッダは省略。リピーターは1段まで許容。




  • 送信元ID(3byte/4byte/6byte、ERPヘッダ内の値により変化)


  • 送信先ID(4byte、省略可)​


ERPパケット構造

ERPパケット構造

出展:EnOcean Radio Protocol 2, pp.16

その他、ERP2に定義されている内容としては以下があります。


  • サブ電文(4.2節)


    • EnOcean通信同士のコリジョンを避けるための仕組みです。

    • データ送信側(TX)は1つのデータを送る際に3回同じデータを送信します。この3回の送信は25msec以内に実施されます。


    • データ受信側(RX)は1つ目のデータを受信してから100msec以内に受け取った同じ電文については同一のデータであるとみなします。


    • 上記はEnOceanのアンテナモジュール内部で実行されるため、USBドングルなどを使ってアプリケーション側でデータを抽出する際には意識する必要がありません。




EEP

EnOceanプロトコルの中核をなす部分です。各センサごとのデータフォーマット定義集であり、現在200を超えるデータモデル(プロファイル)が登録されています。が、よく使われるのは大体10〜20程度と思われます。

上記参考サイトで「EEP」に書かれている 「A5-04-01」 というのがEEP上のデータモデル定義を示す番号です。EEP番号の意味するところは以下の通りです。



  • 「A5」 : RORG、データの通信方式を表す(4BS->4Byteのデータを送信、VLD->可変長データを送信、等)


  • 「04」 : FUNC、データの基本的な機能を表す(Switch Buttons, Light Sensor, Temperature Sensors 等)



  • 「01」 : TYPE、デバイス種別ごとの特徴を表す(温度の例でいうと、-40°C〜0°C、0°C〜40°C、等)

前述しましたが、EEP番号はEnOceanで通常送信する電文内に含まれません。よって、通常EnOceanを利用するアプリケーションは各センサのIDとEEP番号の対応付けを独自に実装する必要があります8

その他、Teach-inプロセスについても本書に記載があります(1.7節、3.1節、3.6節)。


GP

EEPに相当するプロファイルです。EEPの追加に関してはEnOcean Allianceへの申請が必要ですが、GPはメーカが独自にデータモデルを定義して実装できることになっています。ただし市中製品でGP対応品はまだ数が少なくこれからといった印象です。

GPではセンサデータを汎用的な表現で扱うための工夫がなされており、Teach-inによってデータ定義の内容(データの種別や取りうる値の範囲など)を受信機に伝えることができるよう設計されています。実際のセンサ値はTeach-Inで共有された内容に応じてデータ解析を行う必要があります。


EnOceanセンサのデータ解析例


センサ

CO2センサ

https://www.nissha.com/products/dev/ersrhs0000009f8i-att/D-GS1A-A01WH-WA06.pdf


  • EEP: A5-09-04


元の電文

55000a020a9b220400f2a32c80760e50014fff


データ解析


プロトコル
フィールド
説明

0x55
ESP3
Sync Byte

0x000a

Data Length
データ長:10byte

0x02

Optional Length
オプションデータ長:2byte

0x0a

Packet Type
パケットタイプ:10(RADIO_ERP2)

0x9b

CRC8H
チェックサム(ヘッダー4バイト分)

0x22
ERP2
Header
ビット列:00100010 から以下を読み解く

Address Control:001 (Originator-ID 32bit, no Destination-ID)

拡張ヘッダの有無:0 (No extended header)

電文タイプ:0010 (4BS telegram(0xA5))

0x0400f2a3

Originator-ID
送信元ID:0400F2A3

0x2c
EEP
Humidity
湿度:(2 * 16 + 12) / 200 * 100 = 22 %

0x80

Concentration
CO2濃度:(8 * 16 + 0) / 255 * 2550 = 1280 ppm

0x76

Temperature
温度:(7 * 16 + 6) / 255 * 51 = 23.6 °C

0x0e

ビット列:00001110 から以下を読み解く

LRN bit:1 (Data Telegram)

H-Sensor
HSN bit:1 (Humidity Sensor available)

T-Sensor
TSN bit:1 (Temperature Sensor available)

0x50
ERP2
CRC
チェックサム

0x01
ESP3
SubTelNum
サブ電文番号:1 (1回目の電文を受信)

0x4f

dBm
電波強度:(4 * 16 + 15) * -1 = -79 dBm

0xff

CRC8D
チェックサム(データ部+オプションデータ部)


Node-REDでの実装例

スイッチセンサ(RPS: F6-02-04)のデータを解析するパーサの例をNode-REDで実装してみました。

Teach-Inは未対応です。

enocean_flow.png


シリアルポート設定

EnOceanのデータ入力にはnode-red-node-serialportを使います。

serialport

シリアルポートはお使いの端末にあったCOMポートを設定します。上記はOpenBlocks IoT EX1のEnOcean内蔵モジュールを利用する際の設定値です。

設定はESP3に記載の値を利用し、入力タイムアウト 100msecとします。稀に複数のパケットが1電文として届くこともあるので、後段でパケットごとに分割する処理を入れています。データはバイナリバッファとして出力します。


送信元IDとEEPの対応付け

list

EEPの解説で述べた通り、センサのIDとEEP番号の対応付けは独自に定義する必要があります。今回はfunctionノード内に連想配列の配列として持たせました。


flows.json

あくまで検証用に作ったものなので味見程度にご利用いただければと。

Node.js: v10.14.1Node-RED: v0.19.5 で動作確認しています。

[{"id":"b5dfb5cf.391048","type":"debug","z":"f122e4d6.d73148","name":"[DEBUG] EnOceanセンサデータ受信確認","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","x":760,"y":600,"wires":[]},{"id":"e25f4002.dad6a","type":"function","z":"f122e4d6.d73148","name":"Split msg to 2 output  (first and left)","func":"let _data = msg.payload;\n\nconst CONST = global.get(\"CONST\");\n\n// check Start Bit\nlet startBit = _data[0];\n\nif (startBit == CONST.STARTBIT){\n    let len = {\n        main:  (_data[1] << 8) + _data[2],\n        option: _data[3]\n    };\n    \n    let packetLen = CONST.HEADERLEN + len.main + len.option + CONST.CRCLEN;\n    \n    let first = JSON.parse(JSON.stringify(msg)), \n        left  = JSON.parse(JSON.stringify(msg));\n    \n    first.payload = _data.slice(0, packetLen),\n    left.payload = _data.slice(packetLen);\n    \n    if (left.payload.length === 0) {\n        return [first, null];\n    } else {\n        return [first, left];\n    }\n} else {\n    return [null, null] ;\n}","outputs":2,"noerr":0,"x":180,"y":180,"wires":[["633f838b.6fd29c"],["872b5d30.43ab"]]},{"id":"872b5d30.43ab","type":"function","z":"f122e4d6.d73148","name":" Recursion","func":"\nreturn msg;","outputs":1,"noerr":0,"x":160,"y":220,"wires":[["e25f4002.dad6a"]]},{"id":"9a9801eb.53d32","type":"function","z":"f122e4d6.d73148","name":"CRC8Hチェック","func":"let _data = msg.payload;\n\nlet crc = global.get(\"crc\");\n\n// crc8HとESPヘッダを切り出し\nlet espHeader = _data.slice(1,5),\n    crc8h     = _data[5];\n\nif(crc.checkCRC(espHeader, crc8h)){\n    return msg;    \n}else{\n    node.error(\"パケットが破損しています\");\n}","outputs":1,"noerr":0,"x":130,"y":300,"wires":[["1628b1d3.79b81e"]]},{"id":"41aa3d9b.9c47f4","type":"function","z":"f122e4d6.d73148","name":"ESPパース","func":"let _data = msg.payload;\n\nconst CONST = global.get(\"CONST\");\n\nlet dataLen = (_data[1] << 8) + _data[2];\n\nlet esp = {\n    dataLen   : dataLen,\n    optionLen : _data[3],\n    packetType: _data[4],\n    subTelNum : _data[CONST.HEADERLEN+dataLen],\n    dBm       : _data[CONST.HEADERLEN+dataLen+1] * -1\n};\n\nmsg.eo.esp = esp;\n\nreturn msg;","outputs":1,"noerr":0,"x":490,"y":300,"wires":[["73fff203.7bc2ac"]]},{"id":"e41c8e88.5df62","type":"comment","z":"f122e4d6.d73148","name":"ESP","info":"","x":90,"y":260,"wires":[]},{"id":"a72e29f2.311478","type":"comment","z":"f122e4d6.d73148","name":"ERP(RADIO_ERP2前提)","info":"","x":150,"y":360,"wires":[]},{"id":"970ac7a2.9c2ef8","type":"function","z":"f122e4d6.d73148","name":"ERPヘッダ抽出","func":"let _data = msg.payload;\n\nlet dataLen = msg.eo.esp.dataLen;\nconst CONST = global.get(\"CONST\");\nconst BIT = global.get(\"BIT\");\n\nlet _erp  = _data.slice(CONST.HEADERLEN, CONST.HEADERLEN+dataLen-1);\n\n/**\n * ERPヘッダ\n */\n\nlet erp = {\n    extHeader: (_erp[0] & BIT.EXTHEADER) ? true : false,\n    addCtrl  : (_erp[0] & BIT.ADDCTRL) >> 5,\n    rorg     : (_erp[0] & BIT.RORG)\n};\n\n// 読み終えたバイトを消して行く\n_erp = _erp.slice(1);\n\n/**\n * 拡張ヘッダ\n */\nif(erp.extHeader){\n    // リピーターカウントを抽出(頭4bitなので4bitシフト)\n    erp.repeaterCount = (_erp[1] & BIT.REPEATERCOUNT) >> 4;\n    // ERPオプションデータ長を抽出\n    erp.optionLen = (_erp[1] & BIT.OPTIONLEN);\n    // 拡張ヘッダを消す\n    _erp = _erp.slice(1);    \n}\n\n/**\n * 拡張テレグラムタイプ\n */\nif(erp.rorg === 7){\n    // ERP拡張テレグラムタイプは0x08〜0xFFがFuture setなので、この範囲の値はエラー。\n    if(_erp[0] < 0 || _erp[0] > 8){\n        node.error('ERP Extended Telegram Typeが不正です', msg);\n    }\n    erp.extTelegramType = _erp[0];\n    // 拡張テレグラムタイプを消す\n    _erp = _erp.slice(1);    \n}\n\n/**\n * 送信元ID / 送信先ID\n */\n \nlet idLen = {\n    origId: 0,\n    destId: 0\n};\n\nswitch(erp.addCtrl){\n    case 0:\n        idLen.origId = 3;\n        idLen.destId = 0;\n        break;\n        \n    case 1:\n        idLen.origId = 4;\n        idLen.destId = 0;\n        break;\n    \n    case 2:\n        idLen.origId = 4;\n        idLen.destId = 4;\n        break;\n    \n    case 3:\n        idLen.origId = 6;\n        idLen.destId = 0;\n        break;\n        \n    default:\n        node.error(\"Address Contrlの値が不正です\");\n}\n\n// 送信元ID取得\nerp.originatorId = _erp.slice(0, idLen.origId).toString('hex');\n_erp = _erp.slice(idLen.origId); // 送信元ID削除\n\n// 送信先ID取得、送信先IDの長さが0の時は処理スキップ\nif(idLen.destId !== 0){\n    erp.destinationId = _erp.slice(0, idLen.destId).toString('hex');\n    _erp = _erp.slice(idLen.destId); // 送信先ID削除\n}\n\nmsg.eo.erp = erp;\n\n// eepのデータ部分を抽出しておく\nmsg.eo.eep.raw = _erp;\n\nreturn msg;","outputs":1,"noerr":0,"x":360,"y":400,"wires":[["726de5db.3f3ecc"]]},{"id":"c68911ac.08b14","type":"comment","z":"f122e4d6.d73148","name":"パケットを抽出","info":"","x":120,"y":140,"wires":[]},{"id":"3dfc6a25.2b1006","type":"function","z":"f122e4d6.d73148","name":"dummy","func":"let array = [85,0,7,2,10,10,33,4,1,24,110,8,99,1,80,162,85,0,10,2,10,155,34,4,0,252,97,133,2,37,8,215,1,70,192];\nmsg.payload = new Buffer(array);\nreturn msg;\n\n\n","outputs":1,"noerr":0,"x":520,"y":80,"wires":[["e25f4002.dad6a"]]},{"id":"9ca552c8.baf8e","type":"inject","z":"f122e4d6.d73148","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":380,"y":80,"wires":[["3dfc6a25.2b1006"]]},{"id":"633f838b.6fd29c","type":"function","z":"f122e4d6.d73148","name":"情報格納用","func":"msg.eo = {\n    erp: {},\n    esp: {},\n    eep: {}\n};\n\nreturn msg;","outputs":1,"noerr":0,"x":430,"y":180,"wires":[["9a9801eb.53d32"]]},{"id":"8966d271.2cb3f","type":"inject","z":"f122e4d6.d73148","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":true,"onceDelay":0.1,"x":770,"y":80,"wires":[["b3a9dccc.493e1"]]},{"id":"1628b1d3.79b81e","type":"function","z":"f122e4d6.d73148","name":"CRC8Dチェック","func":"let _data = msg.payload;\n\nlet crc = global.get(\"crc\");\nconst CONST = global.get(\"CONST\");\n\n// CRC8DとESPデータを切り出し\nlet espData = _data.slice(CONST.HEADERLEN, _data.length-1),\n    crc8d     = _data[_data.length-1];\n\nif(crc.checkCRC(espData, crc8d)){\n    return msg;    \n}else{\n    node.error(\"パケットが破損しています\");\n}","outputs":1,"noerr":0,"x":320,"y":300,"wires":[["41aa3d9b.9c47f4"]]},{"id":"73fff203.7bc2ac","type":"function","z":"f122e4d6.d73148","name":"CRC8(ERP)チェック","func":"let _data = msg.payload;\n\nlet crc = global.get(\"crc\"),\n    esp = msg.eo.esp;\nconst CONST = global.get(\"CONST\");\n\n// CRC8とERPデータを切り出し\nlet erpData = _data.slice(CONST.HEADERLEN, CONST.HEADERLEN+esp.dataLen-1),\n    crc8    = _data[CONST.HEADERLEN+esp.dataLen-1];\n\nif(crc.checkCRC(erpData, crc8)){\n    return msg;    \n}else{\n    node.error(\"パケットが破損しています\");\n}","outputs":1,"noerr":0,"x":150,"y":400,"wires":[["970ac7a2.9c2ef8"]]},{"id":"726de5db.3f3ecc","type":"function","z":"f122e4d6.d73148","name":"プロファイル存在確認","func":"let originatorId = msg.eo.erp.originatorId.toLowerCase(),\n    profiles     = global.get(\"profiles\");\n\nfor(var i = 0; i<profiles.length; i++){\n    if(originatorId in profiles[i]) {\n        msg.eo.profile = profiles[i][originatorId].toLowerCase();\n    }\n}\n\nreturn msg;","outputs":"1","noerr":0,"x":140,"y":500,"wires":[["b4207029.f4a2f"]]},{"id":"9b718abe.5708b8","type":"comment","z":"f122e4d6.d73148","name":"送信元IDとプロファイルのリスト","info":"","x":830,"y":40,"wires":[]},{"id":"e1c5085e.4dfe38","type":"comment","z":"f122e4d6.d73148","name":"EEP(プロファイルごとにParser準備)","info":"- Parserは以下のプロパティを持つこととする\n    - getData: function(raw){}\n\n\n","x":190,"y":560,"wires":[]},{"id":"b4207029.f4a2f","type":"switch","z":"f122e4d6.d73148","name":"","property":"eo.profile","propertyType":"msg","rules":[{"t":"eq","v":"f60204","vt":"str"}],"checkall":"true","repair":false,"outputs":1,"x":90,"y":600,"wires":[["f01955ba.a26fa8"]]},{"id":"f01955ba.a26fa8","type":"function","z":"f122e4d6.d73148","name":"Parser: F6-02-04","func":"let f60204 = {\n    CONST: {\n        EBO: 128, // Bit for State of the energy bow\n        BC : 64, // Bit for Signalization of button coding\n        BI : 8,\n        B0 : 4,\n        AI : 2,\n        A0 : 1\n    },\n    \n    getData: function(raw){\n        return {\n            EBO: (raw[0] & this.CONST.EBO) ? 1 : 0,\n            BC : (raw[0] & this.CONST.BC) ? 1 : 0,\n            BI : (raw[0] & this.CONST.BI) ? 1 : 0,\n            B0 : (raw[0] & this.CONST.B0) ? 1 : 0,\n            AI : (raw[0] & this.CONST.AI) ? 1 : 0,\n            A0 : (raw[0] & this.CONST.A0) ? 1 : 0\n        };\n    }\n}\n\nmsg.eo.parser = f60204;\n\nreturn msg;","outputs":1,"noerr":0,"x":270,"y":600,"wires":[["3854cf13.5d773"]]},{"id":"b3a9dccc.493e1","type":"function","z":"f122e4d6.d73148","name":"要編集:デバイスIDとプロファイルの対応","func":"// key/valueはLowerCaseで記載\nlet profiles = [\n    {\"0401186e\": \"f60204\"},\n    {\"002cbd7c\": \"f60204\"}\n];\n\nglobal.set(\"profiles\", profiles);\n\nmsg.payload = \"Set sensor profiles.\";\n\nreturn msg;","outputs":1,"noerr":0,"x":1030,"y":80,"wires":[[]]},{"id":"eca27449.fbd618","type":"function","z":"f122e4d6.d73148","name":"CRCクラス","func":"const crc = {\n    \n    crcTable: [\n        0x00, 0x07, 0x0e, 0x09, 0x1c, 0x1b, 0x12, 0x15, \n    \t0x38, 0x3f, 0x36, 0x31, 0x24, 0x23, 0x2a, 0x2d,\n    \t0x70, 0x77, 0x7e, 0x79, 0x6c, 0x6b, 0x62, 0x65, \n        0x48, 0x4f, 0x46, 0x41, 0x54, 0x53, 0x5a, 0x5d,\n    \t0xe0, 0xe7, 0xee, 0xe9, 0xfc, 0xfb, 0xf2, 0xf5, \n    \t0xd8, 0xdf, 0xd6, 0xd1, 0xc4, 0xc3, 0xca, 0xcd,\n        0x90, 0x97, 0x9e, 0x99, 0x8c, 0x8b, 0x82, 0x85, \n    \t0xa8, 0xaf, 0xa6, 0xa1, 0xb4, 0xb3, 0xba, 0xbd,\n    \t0xc7, 0xc0, 0xc9, 0xce, 0xdb, 0xdc, 0xd5, 0xd2, \n        0xff, 0xf8, 0xf1, 0xf6, 0xe3, 0xe4, 0xed, 0xea,\n    \t0xb7, 0xb0, 0xb9, 0xbe, 0xab, 0xac, 0xa5, 0xa2, \n    \t0x8f, 0x88, 0x81, 0x86, 0x93, 0x94, 0x9d, 0x9a,\n        0x27, 0x20, 0x29, 0x2e, 0x3b, 0x3c, 0x35, 0x32, \n    \t0x1f, 0x18, 0x11, 0x16, 0x03, 0x04, 0x0d, 0x0a,\n    \t0x57, 0x50, 0x59, 0x5e, 0x4b, 0x4c, 0x45, 0x42, \n        0x6f, 0x68, 0x61, 0x66, 0x73, 0x74, 0x7d, 0x7a,\n    \t0x89, 0x8e, 0x87, 0x80, 0x95, 0x92, 0x9b, 0x9c, \n    \t0xb1, 0xb6, 0xbf, 0xb8, 0xad, 0xaa, 0xa3, 0xa4,\n        0xf9, 0xfe, 0xf7, 0xf0, 0xe5, 0xe2, 0xeb, 0xec, \n    \t0xc1, 0xc6, 0xcf, 0xc8, 0xdd, 0xda, 0xd3, 0xd4,\n    \t0x69, 0x6e, 0x67, 0x60, 0x75, 0x72, 0x7b, 0x7c, \n        0x51, 0x56, 0x5f, 0x58, 0x4d, 0x4a, 0x43, 0x44,\n    \t0x19, 0x1e, 0x17, 0x10, 0x05, 0x02, 0x0b, 0x0c, \n    \t0x21, 0x26, 0x2f, 0x28, 0x3d, 0x3a, 0x33, 0x34,\n        0x4e, 0x49, 0x40, 0x47, 0x52, 0x55, 0x5c, 0x5b, \n    \t0x76, 0x71, 0x78, 0x7f, 0x6A, 0x6d, 0x64, 0x63,\n    \t0x3e, 0x39, 0x30, 0x37, 0x22, 0x25, 0x2c, 0x2b, \n        0x06, 0x01, 0x08, 0x0f, 0x1a, 0x1d, 0x14, 0x13,\n    \t0xae, 0xa9, 0xa0, 0xa7, 0xb2, 0xb5, 0xbc, 0xbb, \n    \t0x96, 0x91, 0x98, 0x9f, 0x8a, 0x8D, 0x84, 0x83,\n        0xde, 0xd9, 0xd0, 0xd7, 0xc2, 0xc5, 0xcc, 0xcb, \n    \t0xe6, 0xe1, 0xe8, 0xef, 0xfa, 0xfd, 0xf4, 0xf3\n    ],\n    /**\n     * バイナリバッファとチェックサムを与え、バイナリバッファから計算した\n     * CRC8の値がチェックサムと一致していればtrue、一致しなければfalseを返す\n     * @param {buffer} data CRC8計算用のバイナリバッファ\n     * @param {buffer} checksum チェックサム\n     * @return {boolean} 真偽値\n     */\n    checkCRC: function(data, checksum){\n        let crc = 0;\n\n        for (var i = 0; i < data.length; i++){\n\t        crc = this.crcTable[(crc ^ data[i])];\n        }\n\n        if(crc != checksum){\n            return false;\n        }else{\n            return true;\n        }    \n    }\n}\n\nglobal.set(\"crc\", crc);\n\nmsg.payload = \"set crc object.\"\n\nreturn msg;","outputs":1,"noerr":0,"x":930,"y":160,"wires":[[]]},{"id":"a3c72ad0.ee8868","type":"inject","z":"f122e4d6.d73148","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":true,"onceDelay":0.1,"x":770,"y":160,"wires":[["eca27449.fbd618","978ca83f.2be438"]]},{"id":"978ca83f.2be438","type":"function","z":"f122e4d6.d73148","name":"定数","func":"const CONST = {\n    STARTBIT : 85,\n    HEADERLEN: 6,\n    CRCLEN   : 1\n};\n\n// Bit Flag\nconst BIT = {\n    ADDCTRL      : 224, // 11100000\n    EXTHEADER    : 16,  // 00010000\n    RORG         : 15,  // 00001111\n    REPEATERCOUNT: 240, // 11110000\n    OPTIONLEN    : 15   // 00001111\n}\n\n\nglobal.set(\"CONST\", CONST);\nglobal.set(\"BIT\", BIT);\n\nmsg.payload = \"set constants.\";\n\nreturn msg;","outputs":1,"noerr":0,"x":910,"y":200,"wires":[[]]},{"id":"8cd93135.dd6a4","type":"comment","z":"f122e4d6.d73148","name":"送信元IDとプロファイルの突合","info":"","x":170,"y":460,"wires":[]},{"id":"8406a89e.f468b8","type":"serial in","z":"f122e4d6.d73148","name":"","serial":"d11dd259.150ba","x":110,"y":80,"wires":[["e25f4002.dad6a"]]},{"id":"ce22cc5f.5e72b","type":"comment","z":"f122e4d6.d73148","name":"共通関数系","info":"","x":760,"y":120,"wires":[]},{"id":"969c264b.224118","type":"comment","z":"f122e4d6.d73148","name":"デバッグ用のテストデータ","info":"","x":410,"y":40,"wires":[]},{"id":"a94ece37.bb844","type":"comment","z":"f122e4d6.d73148","name":"シリアルポート設定","info":"","x":130,"y":40,"wires":[]},{"id":"3854cf13.5d773","type":"function","z":"f122e4d6.d73148","name":"データパース","func":"msg.eo.eep.data = msg.eo.parser.getData(msg.eo.eep.raw);\n\nreturn msg;","outputs":1,"noerr":0,"x":480,"y":600,"wires":[["b5dfb5cf.391048"]]},{"id":"d11dd259.150ba","type":"serial-port","z":"","serialport":"/dev/ttyEX2","serialbaud":"57600","databits":"8","parity":"none","stopbits":"1","newline":"100","bin":"bin","out":"time","addchar":false}]


EnOceanセンサの活用例

EnOcean Application

akidoko_system.png

NTTコミュニケーションズの本社オフィス(大手町プレイス)で、会議卓の利用状況の見える化 / トイレ利用状況の見える化 をEnOceanセンサで実現しました。導入したセンサの合計数は数百に及んでおり、この規模になると電池切れ対応もかなりの稼働が発生することが見込まれたため、EnOceanの活用でかなりの運用コスト削減が図れたのではないかと思います。

今後もこれらの事例に限らず様々な展開を図っていきたいと考えています。


まとめ

EnOceanは無給電という特徴がよく取り上げられますが、EEPに見られるようにアプリケーションのレイヤでもかなり使い勝手の良いよくできたプロトコルとなっています。

来年以降も引き続きIoTのメインプロトコルの1つとして活用事例が広がっていくことが期待されます。ご興味を持たれた方はぜひお声がけいただければと思います。





  1. 開発コードに由来して、旧来のEnOceanをDolphin、Long RangeをOrcaと呼ぶ人もいるとかいないとか 



  2. 一部には電池併用型のセンサもあります。ただし後述の低消費電力という特徴から、他の無線通信規格よりも電池消費がかなり少なく長持ちする傾向にはあるようです。 



  3. EnOcean Long Rangeは屋外で数km飛ぶと言われています。 



  4. ただしEnOceanのデータを特定小電力無線やLPWAに変換して通信するGWなどもあります。 



  5. Smart-ACKなどの双方向通信の規定はありますが、寡聞にして筆者は対応製品に触れたことがありません。リレースイッチとかその辺なんだろうか。  



  6. 正確にはEEPのFUNC / TYPEが含まれません。EEPのプロファイルを確定するには RORG / FUNC / TYPEの3情報が必要です。また、GPではそもそもデータフォーマットが各機器ごとメーカ独自定義となります。これらはデータ通信における情報量を少しでも減らそうとしたのだと思われます。 



  7. 仕様として定義されているのはTeach-In信号送信の手続き及びデータフォーマットだけで、Teach-Inを利用した事前登録の仕組みはEnOceanを利用するアプリケーション側の実装範囲となります。  



  8. EnOcean Link というEnOcean GmbHの提供するミドルウェア(有償)を利用することでこのあたりの実装を意識する必要がなくなるようです。産業用ですが、このソフトウェアを標準搭載したIoT-GWも発表されています(参考:EnOcean対応PoCキットリリース)。