概要
Braveridge社の"Sizuka THA"は温度・湿度・気圧センサーを搭載したLinking対応のBLEデバイスです。2,300円と手頃な価格で購入できます。
Linkingとはdocomoを中心とした16社が策定したBLE機器をスマートフォンで利用するためのAPIです。Linkingの仕様にはセンサー情報をLinking Beaconとしてブロードキャスする方法もあり、今回はこれを利用してNode-REDでセンサー情報を取得しました。次に、温度データを利用しECHONET Liteの温度センサーを作成しました。通常のノードだけでなく送信専用ノードとしても使えます。さらにGoogle ChartsのGaugeを利用した表示部をWeb Applicationとして作成しました。
今回の記事の内容はMacおよびラズパイ上にインストールしたNode-red(Node.js: v6.9.1、Node-RED: v0.15.1)で動作確認をしました。
準備
- Node-redはインストール済みとして話を始めます
- BLEのライブラリのnobleをローカルにinstallします
cd
npm install noble
- nobleをsudoなしで実行できるようにするため、以下を実行します
sudo setcap cap_net_raw+eip $(eval readlink -f `which node`)
- Node-REDでライブラリを使えるようにするためにsetting.jsを修正します
cd .node-red
nano settings.js
以下の行を追加します。
noble:require('noble'),
functionGlobalContext: {
os:require('os'),
noble:require('noble'),
// octalbonescript:require('octalbonescript'),
// jfive:require("johnny-five"),
// j5board:require("johnny-five").Board({repl:false})
},
- Node-redにnode-red-contrib-nobleをinstallします
npm install node-red-contrib-noble
node-redを再起動するとSCAN BLEsというnodeが現れます
センサのデータフォーマット
詳細は「Linkingアドバタイズ情報フォーマット規定.pdf」に記述されています。
ここでは概要を説明します。
まずは、以下のnobleのsampleプログラムを"nobleSample.js"として保存し、試してみてください。
$ node nobleSample.js
var noble = require('noble');
noble.on('stateChange', function(state) {
if (state === 'poweredOn') {
noble.startScanning();
} else {
noble.stopScanning();
}
});
noble.on('discover', function(peripheral) {
console.log('peripheral discovered (' + peripheral.id +
' with address <' + peripheral.address + ', ' + peripheral.addressType + '>,' +
' connectable ' + peripheral.connectable + ',' +
' RSSI ' + peripheral.rssi + ':');
console.log('\thello my local name is:');
console.log('\t\t' + peripheral.advertisement.localName);
console.log('\tcan I interest you in any of the following advertised services:');
console.log('\t\t' + JSON.stringify(peripheral.advertisement.serviceUuids));
var serviceData = peripheral.advertisement.serviceData;
if (serviceData && serviceData.length) {
console.log('\there is my service data:');
for (var i in serviceData) {
console.log('\t\t' + JSON.stringify(serviceData[i].uuid) + ': ' + JSON.stringify(serviceData[i].data.toString('hex')));
}
}
if (peripheral.advertisement.manufacturerData) {
console.log('\there is my manufacturer data:');
console.log('\t\t' + JSON.stringify(peripheral.advertisement.manufacturerData.toString('hex')));
}
if (peripheral.advertisement.txPowerLevel !== undefined) {
console.log('\tmy TX power level is:');
console.log('\t\t' + peripheral.advertisement.txPowerLevel);
}
console.log();
});
するとコンソールにlocal nameやmanufacture dataが表示されます。このうち、local nameとしてSizuku_thaと表示される場合のmanufacture dataを見てみます。
例:
local name: Sizuku_tha0141188
manufacture data: 8byte, sample: e2020002277d2c91
Sizukaのmanufacture dataは8バイトで、Data-0, 1, 2, ... 7 とすると
最後の2バイト(Data-6, 7)にセンサーデータがあります。
例:2c91, 3cb7, 2c7c, 15d3
このうち、先頭の4bitでセンサーデータの種類を示します。
4bit: ビーコンサービスID, 1:温度(℃), 2:湿度(%), 3:気圧(hPa)
残りの12bitを使い浮動小数点データとしてそれぞれのデータを表しています。
12bit: ビーコンサービスデータ
温度:1bit(sign), 4bit(指数部), 7bit(仮数部)
湿度:0bit(sign), 4bit(指数部), 8bit(仮数部)
気圧:0bit(sign), 5bit(指数部), 7bit(仮数部)
計算例
気温 (1/4/7)
Data-6, 7 = 15d3 の場合
上位4bitが「1」なので気温。残りの12bitsをバイナリー表記
HEX: 5 d 3
BIN: 0101 1101 0011
1bit(sign), 4bit(指数部), 7bit(仮数部)に分解
符号: 0b0
指数部: 0b1011 = 11 (DEC)
仮数部: 0b1010011
(1/2)x1 + (1/4)x0 + (1/8)x1 + (1/16)x0 + (1/32)x0 + (1/64)x1 + (1/128)x1 = 0.648
(1+0.648)x2^(11-7) = 26.3℃
湿度 (0/4/8)
Data-6, 7 = 2c7c の場合
上位4bitが「2」なので湿度。残りの12bitsをバイナリー表記
HEX: c 7 c
BIN: 1100 0111 1100
0bit(sign), 4bit(指数部), 8bit(仮数部)に分解
符号: 0b0
指数部: 0xC = 12 (DEC)
仮数部: 0b01111100 = 0x7C
(1/2)x0 + (1/4)x1 + (1/8)x1 + (1/16)x1 + (1/32)x1 + (1/64)x1 + (1/128)x0+ (1/256)x0 = 0.484
(1+0.484)x2^(12-7) = 47.5%
気圧 (0/5/7)
Data-6, 7 = 3c7b の場合
上位4bitが「3」なので気圧。残りの12bitsをバイナリー表記
HEX: c 7 b
BIN: 1100 0111 1011
0bit(sign), 5bit(指数部), 7bit(仮数部)に分解
符号: 0b0
指数部: 0b11000 = 0x18 = 24(DEC)
仮数部: 0b1111011
(1/2)x1 + (1/4)x1 + (1/8)x1 + (1/16)x1 + (1/32)x0 + (1/64)x1 + (1/128)x1 = 0.960
(1+0.960)x2^(24-15) = 1003.52hPa
センサーデータを取得し固定小数点データに変換するFlow
センサーからデータを取得して、global変数にセットするFlowはこのようになります。
Scan BLEs nodeは各機器からアドバタイズデータを受信すると自動では更新しません。そこで20秒に一度Scanを止めて、1秒後にScanを再開しています。これにより、センサデータの更新を行うことができます。このsensorは10秒毎に温度、湿度、気圧のデータをLinking Beaconとして順次ブロードキャストするので、10秒毎にScanをリセットするのが効率的です。しかし10秒ではBeaconを受信できない場合があるので、今回は20秒に一度Scanをリセットしています。
msg.payload = {"scan": false };
return msg;
msg.payload = {"scan": true };
return msg;
取得したセンサデータは「Upload sensor data」nodeでそれぞれ固定小数点データに変換したのち、global変数にsetしています。「Show global data」をトリガーすると温度、湿度、気圧データがデバッグエリアに表示されます。「Clear global data」をトリガーするとglobal変数がクリアされます。
tmp = msg.advertisement;
var mask1 = 0b00000000000000001111000000000000; // Beacon service ID
var mask2 = 0b00000000000000000000100000000000; // sign bit of temperature
var mask3 = 0b00000000000000000000011110000000; // exponentData of temperature
var mask4 = 0b00000000000000000000000001111111; // fixedPointData of temperature
var mask5 = 0b00000000000000000000111100000000; // exponentData of humidity
var mask6 = 0b00000000000000000000000011111111; // fixedPointData of humidity
var mask7 = 0b00000000000000000000111110000000; // exponentData of pressure
if (typeof tmp !== "undefined") {
localName = tmp.localName;
if (typeof localName !== "undefined") {
// console.log(localName);
if (localName.indexOf("Sizuku") != -1) {
console.log("Found Sizuku_tha");
manufacturerData = tmp.manufacturerData;
sensorData = manufacturerData.readUInt16BE(6);
console.log('sensorData = ' + sensorData);
var sensorType = (sensorData & mask1) >>> 12; // 12bit right shift wo sign bit
if (sensorType == 1) { // Temperature: 1-4-7
signCode = ((sensorData & mask2) === 0) ? 1 : -1;
exponentData = (sensorData & mask3) >>> 7;
fixedPointData = (sensorData & mask4) << 1;
temperature = signCode*(1 + fixedPointData/256)*Math.pow(2, exponentData - 7);
global.set("temperature", temperature);
} else if (sensorType == 2) { // Humidity: 0-4-8
exponentData = (sensorData & mask5) >>> 8;
fixedPointData = (sensorData & mask6);
humidity = (1 + fixedPointData/256)*Math.pow(2, exponentData - 7);
global.set("humidity", humidity);
} else if (sensorType == 3) { // Air Pressure: 0-5-7
exponentData = (sensorData & mask7) >>> 7;
fixedPointData = (sensorData & mask4) << 1;
pressure = (1 + fixedPointData/256)*Math.pow(2, exponentData - 15);
global.set("pressure", pressure);
} else {
console.log('Not Sensor Data');
}
msg.data = manufacturerData;
}
}
}
return msg;
###ECHONET Lite対応
センサから取得した温度データを利用して、ECHONET Lite対応温度センサーを作成しました。ECHONET Liteの仕様で必須とされているEPCは全て実装しています。GET(EPC=E0)で温度データが取得できるだけでなく、定期的にINFで温度データを送信する「送信専用ノード」も実装しました。
FLOW
「Parse EL1」nodeで受信したUDPデータをパースしています。「Node Profile」nodeはNode Profileに対するコマンドを受信した場合の対応が記述されています。「Temperature Sensor」nodeは機器オブジェクトに対するコマンドを受信した場合の対応が記述されています。EPC=E0(温度計測値)のGETを受信した場合は、global変数の温度データをgetし、温度の値をECHONET Liteで規定された形式に変換してEDTにsetしています。「Send EL1」nodeはInt形式で設定されたECHONET LiteのデータをUDPで送信するためのバイナリーデータに変換します。
const buffer = msg.payload;
if (buffer.length >= 14) {
const ehd_buffer = buffer.slice(0, 2);
const tid_buffer = buffer.slice(2, 4);
const seoj_device_buffer = buffer.slice(4, 6);
const seoj_instance_buffer = buffer.slice(6, 7);
const deoj_device_buffer = buffer.slice(7, 9);
const deoj_instance_buffer = buffer.slice(9, 10);
const esv_buffer = buffer.slice(10, 11);
const opc_buffer = buffer.slice(11, 12);
const epc_buffer = buffer.slice(12, 13);
const pdc_buffer = buffer.slice(13, 14);
const ehd = ehd_buffer.readUInt16BE(0);
const tid = tid_buffer.readUInt16BE(0);
const seoj_device = seoj_device_buffer.readUInt16BE(0);
const seoj_instance = seoj_instance_buffer.readUInt8(0);
const seoj = [seoj_device, seoj_instance];
const deoj_device = deoj_device_buffer.readUInt16BE(0);
const deoj_instance = deoj_instance_buffer.readUInt8(0);
const deoj = [deoj_device, deoj_instance];
const esv = esv_buffer.readUInt8(0);
const opc = opc_buffer.readUInt8(0);
const epc = epc_buffer.readUInt8(0);
const pdc = pdc_buffer.readUInt8(0);
let edt = [];
if (buffer.length >= 15) {
const edt_buffer = buffer.slice(14);
for (var i = 0; i < edt_buffer.length; i++) {
edt[i] = edt_buffer.readUInt8(i);
}
}
// ECHONET Lite header check
if (ehd == 0x1081) {
msg.elr = {
"ip" : msg.ip,
"ehd" : ehd,
"tid" : tid,
"seoj" : seoj,
"deoj" : deoj,
"esv" : esv,
"opc" : opc,
"epc" : epc,
"pdc" : pdc,
"edt" : edt
};
} else {
return;
}
} else {
return;
}
return msg;
var esv = 0x72;
var edt = [0x30];
// deojがnode(0x0ef0)?
if (msg.elr.deoj[0] == 0x0ef0) {
// ESVがGET?
if (msg.elr.esv == 0x62) {
switch(msg.elr.epc){
case 0x80:
edt = [0x30];
break;
case 0x82:
edt = [0x01, 0x0c, 0x01, 0x00]; // V1.12
break;
case 0x83:
edt = [0xfe, 0x00, 0x00, 0x77, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c];
break;
case 0x8a:
edt = [0x00, 0x00, 0x77];
break;
case 0x9d:
edt = [0x00];
break;
case 0x9e:
edt = [0x00];
break;
case 0x9f:
edt = [0x0a, 0x80, 0x82, 0x83, 0x8a, 0x9d, 0x9e, 0x9f, 0xd3, 0xd4, 0xd6];
break;
case 0xd3:
edt = [0x00, 0x00, 0x01];
break;
case 0xd4:
edt = [0x00, 0x02];
break;
case 0xd6:
edt = [0x01, 0x00, 0x11, 0x01];
break;
case 0xd7:
edt = [0x01, 0x00, 0x11];
break;
default: // SNA
esv = 0x52;
edt = [];
break;
}
msg.els = {
"ip" : msg.elr.ip,
"tid" : msg.elr.tid,
"seoj" : msg.elr.deoj,
"deoj" : msg.elr.seoj,
"esv" : esv,
"epc" : msg.elr.epc,
"edt" : edt
};
} else {
return;
}
} else {
return;
}
return msg;
var esv = 0x72;
var edt = [0x30];
// deojが温度センサ(0x0011)?
if (msg.elr.deoj[0] == 0x0011) {
// ESVがGET?
if (msg.elr.esv == 0x62) {
switch(msg.elr.epc){
case 0x80:
edt = [0x30];
break;
case 0x81:
edt = [0x08];
break;
case 0x82:
edt = [0x00, 0x00, 0x47, 0x00];
break;
case 0x88:
edt = [0x42];
break;
case 0x8a:
edt = [0x00, 0x00, 0x77];
break;
case 0x9d:
edt = [0x00];
break;
case 0x9e:
edt = [0x00];
break;
case 0x9f:
edt = [0x0a, 0x80, 0x81, 0x82, 0x83, 0x88, 0x8a, 0x9d, 0x9e, 0x9f];
break;
case 0xe0:
var temperature = global.get("temperature");
var tempInt = Math.round(temperature * 10);
// Int32のデータから下2バイトを取り出しEDTへ set
var buf = new Buffer(4);
buf.writeInt32BE(tempInt, 0);
var c = buf.readUInt8(2);
var d = buf.readUInt8(3);
edt = [c, d];
break;
default: // SNA
esv = 0x52;
edt = [];
break;
}
msg.els = {
"ip" : msg.elr.ip,
"tid" : msg.elr.tid,
"seoj" : msg.elr.deoj,
"deoj" : msg.elr.seoj,
"esv" : esv,
"epc" : msg.elr.epc,
"edt" : edt
};
} else {
return;
}
} else {
return;
}
return msg;
const tid_buffer = Buffer.allocUnsafe(2);
let seoj_buffer = Buffer.allocUnsafe(3);
const seoj = msg.els.seoj;
let deoj_buffer = Buffer.allocUnsafe(3);
const deoj = msg.els.deoj;
const esv_buffer = Buffer.allocUnsafe(1);
const opc_buffer = Buffer.from([1]);
const epc_buffer = Buffer.allocUnsafe(1);
const pdc_buffer = Buffer.allocUnsafe(1);
let edt = msg.els.edt;
tid_buffer.writeUInt16BE(msg.els.tid, 0);
esv_buffer.writeUInt8(msg.els.esv, 0);
epc_buffer.writeUInt8(msg.els.epc, 0);
seoj_buffer.writeUInt16BE(seoj[0], 0);
seoj_buffer.writeUInt8(seoj[1], 2);
deoj_buffer.writeUInt16BE(deoj[0], 0);
deoj_buffer.writeUInt8(deoj[1], 2);
// edt[]からedt_bufferを作成
if ((typeof edt) == "undefined") {
edt = [];
}
const edt_buffer = Buffer.allocUnsafe(edt.length);
for (var i = 0; i < edt.length; i++) {
edt_buffer.writeUInt8(edt[i], i);
}
pdc_buffer.writeUInt8(edt_buffer.length, 0);
// Create ECHONET Lite packet
const el_buffer = Buffer.from([
0x10, 0x81,
tid_buffer[0], tid_buffer[1],
seoj_buffer[0], seoj_buffer[1], seoj_buffer[2],
deoj_buffer[0], deoj_buffer[1], deoj_buffer[2],
esv_buffer[0], opc_buffer[0], epc_buffer[0], pdc_buffer[0]]);
// add EDT data
if (edt_buffer.length !== 0) {
msg.payload = Buffer.concat([el_buffer, edt_buffer],
(el_buffer.length + edt_buffer.length));
} else {
msg.payload = el_buffer;
}
msg.ip = msg.els.ip;
return msg;
データ表示UI
Node-REDはデータ表示のUIを持ちません。そこでセンサーデータを表示するWeb ApplicationをNode-REDで作成してみました。Google ChartsのGauge Chartsを利用しています。iPhoneのSafariで表示した場合はこのようになります。この例では、Web Browserで以下のURLにアクセスしています。
FLOW
Web BrowserでNode-Redサーバーの/sensorをアクセスすると、「Gauge」nodeがhtmlを返し、Browserがメーターを表示します。html内のJavascriptが2秒に一度 /sensor/data にアクセス(XMLHttpRequest)し、温度・湿度・気圧の数値を取得します。Browserはその値を元に画面を再描画します。
<html>
<head>
<meta charset="UTF-8">
<title>Sensor Data Visualization</title>
<script type="text/Javascript" src="https://www.gstatic.com/charts/loader.js"></script>
<script type="text/Javascript">
google.charts.load('current', {'packages':['gauge']});
google.charts.setOnLoadCallback(drawChart);
function drawChart() {
var data1 = google.visualization.arrayToDataTable([
['Label', 'Value'],
['温度', 0]
]);
var data2 = google.visualization.arrayToDataTable([
['Label', 'Value'],
['湿度', 0]
]);
var data3 = google.visualization.arrayToDataTable([
['Label', 'Value'],
['気圧', 900]
]);
var options1 = {
width: 250, height: 250,
max: 40, min: -10,
redFrom: 35, redTo: 40,
yellowFrom:-10, yellowTo: 0,
majorTicks: ["-10", "0", "10", "20", "30", "40"],
minorTicks: 10
};
var options2 = {
width: 250, height: 250,
max: 100, min: 0,
redFrom: 80, redTo: 100,
yellowFrom:-0, yellowTo: 20,
majorTicks: ["0", "20", "40", "60", "80", "100"],
minorTicks: 10
};
var options3 = {
width: 250, height: 250,
max: 1100, min: 900,
redFrom: 900, redTo: 1013,
greenFrom:1013, greenTo: 1100,
majorTicks: ["900", "950", "1000", "1050", "1100"],
minorTicks: 5
};
var chart1 = new google.visualization.Gauge(document.getElementById('chart_div1'));
var chart2 = new google.visualization.Gauge(document.getElementById('chart_div2'));
var chart3 = new google.visualization.Gauge(document.getElementById('chart_div3'));
chart1.draw(data1, options1);
chart2.draw(data2, options2);
chart3.draw(data3, options3);
// get sensor data
var request = new XMLHttpRequest();
var sensorData = {"temperature":23,"humidity":44.625,"pressure":1004}
var temperature = ""
var humidity = ""
var pressure = ""
// repeat every 2000 msec
setInterval(function(temperature) {
// get sensor data, blocking
request.open('GET', '/sensor/data', false);
request.send();
if (request.status === 200) {
console.log(request.responseText);
}
sensorData = JSON.parse(request.responseText)
temperature = sensorData.temperature;
humidity = sensorData.humidity;
pressure = sensorData.pressure;
console.log(sensorData);
console.log(temperature);
console.log(humidity);
console.log(pressure);
data1.setValue(0, 1, Math.round(temperature*10)/10);
data2.setValue(0, 1, Math.round(humidity*10)/10);
data3.setValue(0, 1, Math.round(pressure));
chart1.draw(data1, options1);
chart2.draw(data2, options2);
chart3.draw(data3, options3);
}, 2000);
}
</script>
</head>
<body>
<div class="gauge" style="width: 800px; height: 200px;">
<div id="chart_div1" style="width: 250px; height: 250px; float:left;"></div>
<div id="chart_div2" style="width: 250px; height: 250px; float:left"></div>
<div id="chart_div3" style="width: 250px; height: 250px; float:left"></div>
</div>
</body>
</html>
- reply sensor data
temperature = global.get("temperature");
humidity = global.get("humidity");
pressure = global.get("pressure");
msg.payload = {"temperature":temperature, "humidity":humidity, "pressure":pressure};
return msg;
全体のFLOW
[{"id":"fa02f69b.698178","type":"scan ble","z":"a5a7466f.6ff35","uuids":"","duplicates":false,"name":"","x":498,"y":136,"wires":[["6e60f163.84967","3ae2b441.5f819c"]]},{"id":"6e60f163.84967","type":"debug","z":"a5a7466f.6ff35","name":"","active":false,"console":"false","complete":"advertisement","x":715,"y":173,"wires":[]},{"id":"3f3e0fdb.252758","type":"inject","z":"a5a7466f.6ff35","name":"every 20 sec","topic":"","payload":"","payloadType":"date","repeat":"20","crontab":"","once":true,"x":133,"y":134,"wires":[["79c5ec3b.5c236c","b0a644da.3bb928"]]},{"id":"79c5ec3b.5c236c","type":"function","z":"a5a7466f.6ff35","name":"stop","func":"msg.payload = {\"scan\": false };\nreturn msg;","outputs":1,"noerr":0,"x":325.5,"y":133,"wires":[["fa02f69b.698178"]]},{"id":"8d14aa05.5db948","type":"function","z":"a5a7466f.6ff35","name":"start","func":"msg.payload = {\"scan\": true };\nreturn msg;","outputs":1,"noerr":0,"x":324.5,"y":181,"wires":[["fa02f69b.698178"]]},{"id":"b0a644da.3bb928","type":"delay","z":"a5a7466f.6ff35","name":"","pauseType":"delay","timeout":"1","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":150,"y":181,"wires":[["8d14aa05.5db948"]]},{"id":"27dc79ab.e6a206","type":"inject","z":"a5a7466f.6ff35","name":"Show global data","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"x":335,"y":262,"wires":[["3632fb36.ea4ec4"]]},{"id":"d897f9ab.4739a8","type":"inject","z":"a5a7466f.6ff35","name":"Clear global data","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"x":337,"y":226,"wires":[["e9480c43.28db7"]]},{"id":"e9480c43.28db7","type":"function","z":"a5a7466f.6ff35","name":"Clear sensor data","func":"global.set(\"temperature\", \"\");\nglobal.set(\"humidity\", \"\");\nglobal.set(\"pressure\", \"\");\nglobal.set(\"data\", \"\");\nreturn msg;","outputs":1,"noerr":0,"x":573,"y":225,"wires":[[]]},{"id":"6db2366.bb17bc8","type":"comment","z":"a5a7466f.6ff35","name":"On/Off scan","info":"","x":100,"y":97,"wires":[]},{"id":"3ae2b441.5f819c","type":"function","z":"a5a7466f.6ff35","name":"Upload sensor data","func":"tmp = msg.advertisement;\nvar mask1 = 0b00000000000000001111000000000000; // Beacon service ID\nvar mask2 = 0b00000000000000000000100000000000; // sign bit of temperature\nvar mask3 = 0b00000000000000000000011110000000; // exponentData of temperature\nvar mask4 = 0b00000000000000000000000001111111; // fixedPointData of temperature\nvar mask5 = 0b00000000000000000000111100000000; // exponentData of humidity\nvar mask6 = 0b00000000000000000000000011111111; // fixedPointData of humidity\nvar mask7 = 0b00000000000000000000111110000000; // exponentData of pressure\n\nif (typeof tmp !== \"undefined\") {\n localName = tmp.localName;\n if (typeof localName !== \"undefined\") {\n // console.log(localName);\n if (localName.indexOf(\"Sizuku\") != -1) {\n console.log(\"Found Sizuku_tha\");\n manufacturerData = tmp.manufacturerData;\n sensorData = manufacturerData.readUInt16BE(6);\n console.log('sensorData = ' + sensorData);\n\n var sensorType = (sensorData & mask1) >>> 12; // 12bit right shift wo sign bit\n if (sensorType == 1) {\t// Temperature: 1-4-7\n signCode = ((sensorData & mask2) === 0) ? 1 : -1;\n\t exponentData = (sensorData & mask3) >>> 7;\n\t fixedPointData = (sensorData & mask4) << 1;\n \ttemperature = signCode*(1 + fixedPointData/256)*Math.pow(2, exponentData - 7);\n global.set(\"temperature\", temperature);\n } else if (sensorType == 2) {\t// Humidity: 0-4-8\n \texponentData = (sensorData & mask5) >>> 8;\n\t fixedPointData = (sensorData & mask6);\n \thumidity = (1 + fixedPointData/256)*Math.pow(2, exponentData - 7);\n global.set(\"humidity\", humidity);\n } else if (sensorType == 3) {\t// Air Pressure: 0-5-7\n \texponentData = (sensorData & mask7) >>> 7;\n\t fixedPointData = (sensorData & mask4) << 1;\n \tpressure = (1 + fixedPointData/256)*Math.pow(2, exponentData - 15);\n global.set(\"pressure\", pressure);\n } else {\n\t console.log('Not Sensor Data');\n }\n msg.data = manufacturerData;\n }\n }\n}\nreturn msg;","outputs":1,"noerr":0,"x":715.5,"y":135,"wires":[[]]},{"id":"3632fb36.ea4ec4","type":"function","z":"a5a7466f.6ff35","name":"get sensor data","func":"temperature = global.get(\"temperature\");\nhumidity = global.get(\"humidity\");\npressure = global.get(\"pressure\");\n\nmsg.payload = {\"temperature\":temperature, \"humidity\":humidity, \"pressure\":pressure};\nreturn msg;","outputs":1,"noerr":0,"x":564.5,"y":260,"wires":[["957dbdcb.8a39d8"]]},{"id":"2916c755.cd2168","type":"comment","z":"a5a7466f.6ff35","name":"Browse sensor data","info":"","x":120,"y":225,"wires":[]},{"id":"957dbdcb.8a39d8","type":"debug","z":"a5a7466f.6ff35","name":"","active":true,"console":"false","complete":"false","x":799,"y":260,"wires":[]},{"id":"24adaf0b.b7f95","type":"comment","z":"a5a7466f.6ff35","name":"linking 対応センサーを利用したEL温度センサー","info":"Receive BLE broadcast from Braveride's sensor","x":210,"y":61,"wires":[]},{"id":"2e0b2a7b.f03ce6","type":"inject","z":"a5a7466f.6ff35","name":"every 60 sec","topic":"","payload":"","payloadType":"date","repeat":"60","crontab":"","once":true,"x":151,"y":484,"wires":[["8ba075b.8f3ec08"]]},{"id":"88d874e6.8d5dd8","type":"debug","z":"a5a7466f.6ff35","name":"","active":false,"console":"false","complete":"false","x":958.5,"y":438,"wires":[]},{"id":"2d2a82d0.1d5a6e","type":"comment","z":"a5a7466f.6ff35","name":"温度センサ 送信専用ノード","info":"","x":155.5,"y":444,"wires":[]},{"id":"f045ca95.9dc76","type":"comment","z":"a5a7466f.6ff35","name":"EL: GETへの対応","info":"","x":114.5,"y":312,"wires":[]},{"id":"8d351c15.5f38f8","type":"function","z":"a5a7466f.6ff35","name":"Send EL1","func":"// Send ECHONET Lite packet\n// Assumption: OPC = 1\n//\n// Usage: set following data\n// msg.els.ip ip address (String)\n// msg.els.tid TID (Int)\n// msg.els.seoj SEOJ ([Int:device, Int:instance])\n// msg.els.deoj DEOJ ([Int:device, Int:instance])\n// msg.els.esv ESV (Int)\n// msg.els.epc EPC (Int)\n// msg.els.edt EDT ([Int]) or []\n\nconst tid_buffer = Buffer.allocUnsafe(2);\nlet seoj_buffer = Buffer.allocUnsafe(3);\nconst seoj = msg.els.seoj;\nlet deoj_buffer = Buffer.allocUnsafe(3);\nconst deoj = msg.els.deoj;\nconst esv_buffer = Buffer.allocUnsafe(1);\nconst opc_buffer = Buffer.from([1]);\nconst epc_buffer = Buffer.allocUnsafe(1);\nconst pdc_buffer = Buffer.allocUnsafe(1);\nlet edt = msg.els.edt;\n\ntid_buffer.writeUInt16BE(msg.els.tid, 0);\nesv_buffer.writeUInt8(msg.els.esv, 0);\nepc_buffer.writeUInt8(msg.els.epc, 0);\n\nseoj_buffer.writeUInt16BE(seoj[0], 0);\nseoj_buffer.writeUInt8(seoj[1], 2);\ndeoj_buffer.writeUInt16BE(deoj[0], 0);\ndeoj_buffer.writeUInt8(deoj[1], 2);\n\n// edt[]からedt_bufferを作成\nif ((typeof edt) == \"undefined\") {\n edt = [];\n}\n\nconst edt_buffer = Buffer.allocUnsafe(edt.length);\nfor (var i = 0; i < edt.length; i++) {\n edt_buffer.writeUInt8(edt[i], i);\n}\n\npdc_buffer.writeUInt8(edt_buffer.length, 0);\n\n// Create ECHONET Lite packet\nconst el_buffer = Buffer.from([\n\t0x10, 0x81,\n\ttid_buffer[0], tid_buffer[1],\n\tseoj_buffer[0], seoj_buffer[1], seoj_buffer[2],\n\tdeoj_buffer[0], deoj_buffer[1], deoj_buffer[2],\n\tesv_buffer[0], opc_buffer[0], epc_buffer[0], pdc_buffer[0]]);\n\n// add EDT data\nif (edt_buffer.length !== 0) {\n msg.payload = Buffer.concat([el_buffer, edt_buffer], \n (el_buffer.length + edt_buffer.length));\n} else {\n msg.payload = el_buffer;\n}\n\nmsg.ip = msg.els.ip;\nreturn msg;","outputs":1,"noerr":0,"x":764,"y":389,"wires":[["88d874e6.8d5dd8","e5323cd1.5c7d88"]]},{"id":"8ba075b.8f3ec08","type":"function","z":"a5a7466f.6ff35","name":"温度センサー:Inf 温度計測値","func":"// msg.els.ip_string // ip address (String)\n// msg.els.tid // TID (Int)\n// msg.els.seoj // SEOJ ([Int:device, Int:instance])\n// msg.els.deoj // DEOJ ([Int:device, Int:instance])\n// msg.els.esv // ESV (Int)\n// msg.els.epc // EPC (Int)\n// msg.els.edt_int // EDT ([Int])\n\nvar temperature = global.get(\"temperature\");\nvar tempInt = Math.round(temperature * 10);\n\n// Int32のデータから下2バイトを取り出しEDTへ set\nconst buf = Buffer.allocUnsafe(4);\nbuf.writeInt32BE(tempInt, 0);\n\nvar c = buf.readUInt8(2);\nvar d = buf.readUInt8(3);\n\n// from Temperature sensor to Controller, INF, EPC=0xE0\nmsg.els = {\n \"ip\" : \"224.0.23.0\",\n \"tid\" : 5,\n \"seoj\" : [0x0011,1],\n \"deoj\" : [0x05ff,1],\n \"esv\" : 0x73,\n \"epc\" : 0xe0,\n \"edt\" : [c, d]\n};\n\nreturn msg;","outputs":1,"noerr":0,"x":463,"y":483,"wires":[["8d351c15.5f38f8"]]},{"id":"44ef9300.591c04","type":"function","z":"a5a7466f.6ff35","name":"Parse EL1","func":"// Parse ECHONET Lite packet\n// Assumption: OPC = 1\n\n// OUTPUT:\n// msg.elr.ip // ip address (String)\n// msg.elr.ehd // EHD (Int)\n// msg.elr.tid // TID (Int)\n// msg.elr.seoj // SEOJ ([Int:device, Int:instance])\n// msg.elr.deoj // DEOJ ([Int:device, Int:instance])\n// msg.elr.esv // ESV (Int)\n// msg.elr.opc // OPC (Int)\n// msg.elr.epc // EPC (Int)\n// msg.elr.pdc // PDC (Int)\n// msg.elr.edt // EDT ([Int]) or [](no EDT data)\n\nconst buffer = msg.payload;\n\nif (buffer.length >= 14) {\n const ehd_buffer = buffer.slice(0, 2);\n const tid_buffer = buffer.slice(2, 4);\n const seoj_device_buffer = buffer.slice(4, 6);\n const seoj_instance_buffer = buffer.slice(6, 7);\n const deoj_device_buffer = buffer.slice(7, 9);\n const deoj_instance_buffer = buffer.slice(9, 10);\n const esv_buffer = buffer.slice(10, 11);\n const opc_buffer = buffer.slice(11, 12);\n const epc_buffer = buffer.slice(12, 13);\n const pdc_buffer = buffer.slice(13, 14);\n\n const ehd = ehd_buffer.readUInt16BE(0);\n const tid = tid_buffer.readUInt16BE(0);\n const seoj_device = seoj_device_buffer.readUInt16BE(0);\n const seoj_instance = seoj_instance_buffer.readUInt8(0);\n const seoj = [seoj_device, seoj_instance];\n const deoj_device = deoj_device_buffer.readUInt16BE(0);\n const deoj_instance = deoj_instance_buffer.readUInt8(0);\n const deoj = [deoj_device, deoj_instance];\n const esv = esv_buffer.readUInt8(0);\n const opc = opc_buffer.readUInt8(0);\n const epc = epc_buffer.readUInt8(0);\n const pdc = pdc_buffer.readUInt8(0);\n let edt = [];\n \n if (buffer.length >= 15) {\n const edt_buffer = buffer.slice(14);\n for (var i = 0; i < edt_buffer.length; i++) {\n edt[i] = edt_buffer.readUInt8(i);\n }\n }\n\n // ECHONET Lite header check\n if (ehd == 0x1081) {\n msg.elr = {\n \"ip\" : msg.ip,\n \"ehd\" : ehd,\n \"tid\" : tid,\n \"seoj\" : seoj,\n \"deoj\" : deoj,\n \"esv\" : esv,\n \"opc\" : opc,\n \"epc\" : epc,\n \"pdc\" : pdc,\n \"edt\" : edt\n };\n } else {\n return;\n }\n} else {\n return;\n}\n\nreturn msg;","outputs":1,"noerr":0,"x":273,"y":350,"wires":[["69f16559.b757cc","f3a01c1.0cf6ee"]]},{"id":"4ad93e8d.eb7358","type":"udp in","z":"a5a7466f.6ff35","name":"EL receive","iface":"","port":"3610","ipv":"udp4","multicast":"true","group":"224.0.23.0","datatype":"buffer","x":99,"y":347,"wires":[["44ef9300.591c04"]]},{"id":"e5323cd1.5c7d88","type":"udp out","z":"a5a7466f.6ff35","name":"EL send","addr":"","iface":"","port":"3610","ipv":"udp4","outport":"","base64":false,"multicast":"false","x":948,"y":390,"wires":[]},{"id":"1fa8d51b.adfefb","type":"http in","z":"a5a7466f.6ff35","name":"","url":"/sensor","method":"get","swaggerDoc":"","x":120.5,"y":610,"wires":[["657a886d.825b7"]]},{"id":"657a886d.825b7","type":"template","z":"a5a7466f.6ff35","name":"Gauge","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"<html>\n <head>\n <meta charset=\"UTF-8\">\n <title>Sensor Data Visualization</title>\n <script type=\"text/Javascript\" src=\"https://www.gstatic.com/charts/loader.js\"></script>\n <script type=\"text/Javascript\">\n google.charts.load('current', {'packages':['gauge']});\n google.charts.setOnLoadCallback(drawChart);\n function drawChart() {\n\n var data1 = google.visualization.arrayToDataTable([\n ['Label', 'Value'],\n ['温度', 0]\n ]);\n var data2 = google.visualization.arrayToDataTable([\n ['Label', 'Value'],\n ['湿度', 0]\n ]);\n var data3 = google.visualization.arrayToDataTable([\n ['Label', 'Value'],\n ['気圧', 900]\n ]);\n\n var options1 = {\n width: 250, height: 250,\n max: 40, min: -10,\n redFrom: 35, redTo: 40,\n yellowFrom:-10, yellowTo: 0,\n majorTicks: [\"-10\", \"0\", \"10\", \"20\", \"30\", \"40\"],\n minorTicks: 10\n };\n var options2 = {\n width: 250, height: 250,\n max: 100, min: 0,\n redFrom: 80, redTo: 100,\n yellowFrom:-0, yellowTo: 20,\n majorTicks: [\"0\", \"20\", \"40\", \"60\", \"80\", \"100\"],\n minorTicks: 10\n };\n var options3 = {\n width: 250, height: 250,\n max: 1100, min: 900,\n redFrom: 900, redTo: 1013,\n greenFrom:1013, greenTo: 1100,\n majorTicks: [\"900\", \"950\", \"1000\", \"1050\", \"1100\"],\n minorTicks: 5\n };\n\n var chart1 = new google.visualization.Gauge(document.getElementById('chart_div1'));\n var chart2 = new google.visualization.Gauge(document.getElementById('chart_div2'));\n var chart3 = new google.visualization.Gauge(document.getElementById('chart_div3'));\n\n chart1.draw(data1, options1);\n chart2.draw(data2, options2);\n chart3.draw(data3, options3);\n \n // get sensor data\n var request = new XMLHttpRequest();\n var sensorData = {\"temperature\":23,\"humidity\":44.625,\"pressure\":1004}\n var temperature = \"\"\n var humidity = \"\"\n var pressure = \"\"\n \n // repeat every 2000 msec\n setInterval(function(temperature) {\n // get sensor data, blocking\n request.open('GET', '/sensor/data', false);\n request.send();\n if (request.status === 200) {\n console.log(request.responseText);\n }\n sensorData = JSON.parse(request.responseText)\n temperature = sensorData.temperature;\n humidity = sensorData.humidity;\n pressure = sensorData.pressure;\n \n console.log(sensorData);\n console.log(temperature);\n console.log(humidity);\n console.log(pressure);\n \n data1.setValue(0, 1, Math.round(temperature*10)/10);\n data2.setValue(0, 1, Math.round(humidity*10)/10);\n data3.setValue(0, 1, Math.round(pressure));\n chart1.draw(data1, options1);\n chart2.draw(data2, options2);\n chart3.draw(data3, options3); \n }, 2000);\n }\n </script>\n </head>\n <body>\n <div class=\"gauge\" style=\"width: 800px; height: 200px;\">\n <div id=\"chart_div1\" style=\"width: 250px; height: 250px; float:left;\"></div>\n <div id=\"chart_div2\" style=\"width: 250px; height: 250px; float:left\"></div>\n <div id=\"chart_div3\" style=\"width: 250px; height: 250px; float:left\"></div>\n </div>\n </body>\n</html>","x":333.5,"y":611,"wires":[["f90370d5.7104f8"]]},{"id":"f90370d5.7104f8","type":"http response","z":"a5a7466f.6ff35","name":"","x":554.5,"y":608,"wires":[]},{"id":"b86cc6e2.4a4c38","type":"http in","z":"a5a7466f.6ff35","name":"","url":"/sensor/data","method":"get","swaggerDoc":"","x":130.5,"y":645,"wires":[["57bd9b3f.4329a4"]]},{"id":"57bd9b3f.4329a4","type":"function","z":"a5a7466f.6ff35","name":"reply sensor data","func":"temperature = global.get(\"temperature\");\nhumidity = global.get(\"humidity\");\npressure = global.get(\"pressure\");\n\nmsg.payload = {\"temperature\":temperature, \"humidity\":humidity, \"pressure\":pressure};\nreturn msg;","outputs":1,"noerr":0,"x":370.5,"y":645,"wires":[["f90370d5.7104f8"]]},{"id":"717333a4.74e66c","type":"comment","z":"a5a7466f.6ff35","name":"気温、湿度、気圧 表示","info":"ブラウザーで /sensorにアクセスすると、\n気温、湿度、気圧を表示する。\n\n\nNote: Gauge functionの中に、\nip address がhard codingされている","x":144.5,"y":569,"wires":[]},{"id":"3b85e8f.a4c2498","type":"comment","z":"a5a7466f.6ff35","name":"2016.11.30_13:00","info":"","x":900.5,"y":64,"wires":[]},{"id":"69f16559.b757cc","type":"function","z":"a5a7466f.6ff35","name":"Node Profile","func":"// msg.elr.ip // ip address (String)\n// msg.elr.ehd // EHD (Int)\n// msg.elr.tid // TID (Int)\n// msg.elr.seoj // SEOJ ([Int:device, Int:instance])\n// msg.elr.deoj // DEOJ ([Int:device, Int:instance])\n// msg.elr.esv // ESV (Int)\n// msg.elr.opc // OPC (Int)\n// msg.elr.epc // EPC (Int)\n// msg.elr.pdc // PDC (Int)\n// msg.elr.edt // EDT ([Int]) or [](no EDT data)\n\nvar esv = 0x72;\nvar edt = [0x30];\n\n// deojがnode(0x0ef0)?\nif (msg.elr.deoj[0] == 0x0ef0) {\n // ESVがGET?\n if (msg.elr.esv == 0x62) {\n \tswitch(msg.elr.epc){\n \t\tcase 0x80:\n \t\t\tedt = [0x30];\n \t\t\tbreak; \n \t\tcase 0x82:\n \t\t\tedt = [0x01, 0x0c, 0x01, 0x00];\t// V1.12\n \t\t\tbreak; \n \t\tcase 0x83:\n \t\t\tedt = [0xfe, 0x00, 0x00, 0x77, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c];\n \t\t\tbreak; \n \t\tcase 0x8a:\n \t\t\tedt = [0x00, 0x00, 0x77];\n \t\t\tbreak; \n \t\tcase 0x9d:\n \t\t\tedt = [0x00];\n \t\t\tbreak; \n \t\tcase 0x9e:\n \t\t\tedt = [0x00];\n \t\t\tbreak; \n \t\tcase 0x9f:\n \t\t\tedt = [0x0a, 0x80, 0x82, 0x83, 0x8a, 0x9d, 0x9e, 0x9f, 0xd3, 0xd4, 0xd6];\n \t\t\tbreak; \n \t\tcase 0xd3:\n \t\t\tedt = [0x00, 0x00, 0x01];\n \t\t\tbreak; \n \t\tcase 0xd4:\n \t\t\tedt = [0x00, 0x02];\n \t\t\tbreak; \n \t\tcase 0xd6:\n \t\t\tedt = [0x01, 0x00, 0x11, 0x01];\n \t\t\tbreak; \n \t\tcase 0xd7:\n \t\t\tedt = [0x01, 0x00, 0x11];\n \t\t\tbreak; \n \t\tdefault:\t// SNA\n \t\t\tesv = 0x52;\n \t\t\tedt = [];\n \t\t\tbreak; \n\t}\n\n\tmsg.els = {\n\t \"ip\" : msg.elr.ip,\n\t \"tid\" : msg.elr.tid,\n\t \"seoj\" : msg.elr.deoj,\n\t \"deoj\" : msg.elr.seoj,\n\t \"esv\" : esv,\n\t \"epc\" : msg.elr.epc,\n\t \"edt\" : edt\n\t};\n } else {\n return;\n }\n} else {\n return;\n}\n\nreturn msg;","outputs":1,"noerr":0,"x":468,"y":326,"wires":[["8d351c15.5f38f8"]]},{"id":"f3a01c1.0cf6ee","type":"function","z":"a5a7466f.6ff35","name":"Temperature Sensor","func":"// msg.elr.ip // ip address (String)\n// msg.elr.ehd // EHD (Int)\n// msg.elr.tid // TID (Int)\n// msg.elr.seoj // SEOJ ([Int:device, Int:instance])\n// msg.elr.deoj // DEOJ ([Int:device, Int:instance])\n// msg.elr.esv // ESV (Int)\n// msg.elr.opc // OPC (Int)\n// msg.elr.epc // EPC (Int)\n// msg.elr.pdc // PDC (Int)\n// msg.elr.edt // EDT ([Int]) or [](no EDT data)\n\nvar esv = 0x72;\nvar edt = [0x30];\n\n// deojが温度センサ(0x0011)?\nif (msg.elr.deoj[0] == 0x0011) {\n // ESVがGET?\n if (msg.elr.esv == 0x62) {\n \tswitch(msg.elr.epc){\n \t\tcase 0x80:\n \t\t\tedt = [0x30];\n \t\t\tbreak; \n \t\tcase 0x81:\n \t\t\tedt = [0x08];\n \t\t\tbreak; \n \t\tcase 0x82:\n \t\t\tedt = [0x00, 0x00, 0x47, 0x00];\n \t\t\tbreak; \n \t\tcase 0x88:\n \t\t\tedt = [0x42];\n \t\t\tbreak; \n \t\tcase 0x8a:\n \t\t\tedt = [0x00, 0x00, 0x77];\n \t\t\tbreak; \n \t\tcase 0x9d:\n \t\t\tedt = [0x00];\n \t\t\tbreak; \n \t\tcase 0x9e:\n \t\t\tedt = [0x00];\n \t\t\tbreak; \n \t\tcase 0x9f:\n \t\t\tedt = [0x0a, 0x80, 0x81, 0x82, 0x83, 0x88, 0x8a, 0x9d, 0x9e, 0x9f];\n \t\t\tbreak;\n \t\tcase 0xe0:\n \t\t var temperature = global.get(\"temperature\");\n var tempInt = Math.round(temperature * 10);\n // Int32のデータから下2バイトを取り出しEDTへ set\n var buf = new Buffer(4);\n buf.writeInt32BE(tempInt, 0);\n var c = buf.readUInt8(2);\n var d = buf.readUInt8(3);\n \t\t\tedt = [c, d];\n \t\t\tbreak; \n \t\tdefault:\t// SNA\n \t\t\tesv = 0x52;\n \t\t\tedt = [];\n \t\t\tbreak; \n\t}\n\n\tmsg.els = {\n\t \"ip\" : msg.elr.ip,\n\t \"tid\" : msg.elr.tid,\n\t \"seoj\" : msg.elr.deoj,\n\t \"deoj\" : msg.elr.seoj,\n\t \"esv\" : esv,\n\t \"epc\" : msg.elr.epc,\n\t \"edt\" : edt\n\t};\n } else {\n return;\n }\n} else {\n return;\n}\n\nreturn msg;","outputs":1,"noerr":0,"x":497,"y":375,"wires":[["8d351c15.5f38f8"]]}]