sakura.io で rotronic 温湿度プローブ HC2 を使ってみました 2020/11/24, 25
・2020/11/25 HTML ファイルにモジュール ID の表示を追加
ロトロニック社 温湿度プローブ HC2 (現行型番:HC2A-S)は、単体で計測データを UART(シリアル通信)で取得できます。
データロガーの機能も備えており、 カタログ上の精度は、±0.8 %RH at 23℃、±0.1 ℃ と高精度。メーカーの校正も受けられます。
参考価格は、こちら。
Arduino nano で HC2 の UART データを読み出し、sakura.io 通信モジュールに送信します。
sakura.io はLTE 回線を使っているため、ほぼ携帯電話の通信圏内であれば、通信可能です。
sakurai oプラットフォームに届いたデータは、インターネット環境にあるパソコン、携帯端末であれば、WebSocket を使って取得できます。
sakura.io は 8 バイト×16ch のデータ送受信が可能ですが、今回は 送信 4ch のみを使っています。
接続図はこちらです。
通信料ですが、5 分に 1 回程度の更新であれば、通信モジュール使用料 66 円/月 に含まれます。
30 秒間隔: 66 円 +420 円/月。5 秒間隔: 66 円 +2,800 円/月 程度になるようです。
■iPhone でのスクリーンショット↓
HTML ファイルをレンタルサーバーに置いているので、どこからでも使用可能です。
(1 秒ごとの更新とかはサーバーに負担がかかるので、レンタルサーバーの場合、控えたほうが良いでしょう)
HTML ファイルをパソコンに置いた場合は、Wi-Fi が届く範囲で使用可能です。
■サーバーレスでの運用
sakura.io の通信モジュールをもう 1 個用意すると、「デバイス間通信」を使ってサーバレスでの運用が可能になり、
送信側の LCD 表示と同じ内容を受信側の LCD に表示できるようになります。
ただ、初期費用(機器代)、通信料が 2 倍になります。
/* * sakuraHC2.ino * Rotronic HC2 * 2020/11/24 f.izawa */ #include <SoftwareSerial.h> #include <SakuraIO.h> #include <LiquidCrystal_I2C.h> SakuraIO_I2C sakuraIO; LiquidCrystal_I2C lcd(0x27, 16, 2); const byte rxPin = 2; const byte txPin = 3; SoftwareSerial mySerial (rxPin, txPin); void setup() { Serial.begin(19200); // HC2 通信設定 pinMode(rxPin, INPUT); pinMode(txPin, OUTPUT); mySerial.begin(19200); mySerial.setTimeout(100); lcd.init(); lcd.backlight(); lcdSetup(); // 外字登録 int ret = sakuraIO.getConnectionStatus();// 127 0x7F Serial.println(ret); lcd.setCursor(0, 0); lcd.print("Waiting to"); lcd.setCursor(5, 1); lcd.print("come online"); // sakura.io 接続 for( ;; ){ if((sakuraIO.getConnectionStatus() & 0x80) == 0x80) break; delay( 1000 ); } } void loop() { // float <-> 4 byte 変換(未使用) union data{ byte buf[4]; float value; }f; char buf[10]; uint8_t signalQuality = sakuraIO.getSignalQuality(); dispAntenaLevel(13, 0, signalQuality, 0); // アンテナマーク // 以下のデータは未使用 uint8_t connectionStatus = sakuraIO.getConnectionStatus(); Serial.print("connectionStatus=");Serial.println(connectionStatus); uint32_t unixTime = (uint32_t)(sakuraIO.getUnixtime()/1000UL); Serial.print("unixTime="); Serial.println(unixTime); uint16_t productID = sakuraIO.getProductID(); Serial.print("PID=");Serial.println(productID); uint8_t response[33] = {}; sakuraIO.getUniqueID((char *)response);// モジュールのシリアル番号 Serial.print("UID=");Serial.println((char *)response); // HC2 ReadData mySerial.print("{ 99RDD}\r"); mySerial.flush(); int st, ed; //{F00rdd 001; 63.68;%rh;000;=; 26.42;⸮C;000;=;nc;---.- ;⸮C;000; ;001;V2.0-2;0061174567;HC2 ;000;X String str = mySerial.readStringUntil(0x0d); // Humi st = str.indexOf(';'); ed = str.indexOf(';', st + 1); float fHumi = str.substring(st + 1, ed).toFloat(); Serial.print("[%RH]="); Serial.print(fHumi, 1); dtostrf(fHumi, 5, 1, buf); lcd.setCursor(1, 0); lcd.print(buf); lcd.print(" %RH"); st = str.indexOf(';', ed + 7); ed = str.indexOf(';', st + 1); String markHumi = str.substring(st + 1, ed); Serial.println(markHumi); // "+", "-", "=", " " lcd.setCursor(0, 0); lcd.write(markHC2(markHumi.c_str()[0])); // Temp st = ed; ed = str.indexOf(';', st + 1); float fTemp = str.substring(st + 1, ed).toFloat(); Serial.print("[°C]="); Serial.print(fTemp, 1); dtostrf(fTemp, 5, 1, buf); lcd.setCursor(1, 1); lcd.print(buf);lcd.print(" C"); lcd.setCursor(7, 1);lcd.write(0x06); st = str.indexOf(';', ed + 7); ed = str.indexOf(';', st + 1); String markTemp = str.substring(st + 1, ed); Serial.println(markTemp); // "+", "-", "=", " " lcd.setCursor(0, 1); lcd.write(markHC2(markTemp.c_str()[0])); Serial.print("[°CDP]="); Serial.println(calcDp(fTemp, fHumi)); float fDp = calcDp(fTemp, fHumi); // 露点温度計算 dtostrf(fDp, 5, 1, buf); lcd.setCursor(9, 1); lcd.print(buf);lcd.print("dp"); Serial.println(); // float(4byte) をそのまま格納 sakuraIO.enqueueTx(0, fHumi); sakuraIO.enqueueTx(1, fTemp); sakuraIO.enqueueTx(2, fDp); // バイトデータで格納 byte chBuf[8]={}; chBuf[0] = markHumi.c_str()[0]; chBuf[1] = markTemp.c_str()[0]; //chBuf[2] = markDp.c_str()[0]; sakuraIO.enqueueTx(3, chBuf); // 送信 sakuraIO.send(); delay(5000); } byte markHC2(byte mark){ switch (mark){ case 0x2b: //'+' return 0x5e; // '^' case 0x2d:// '-' return 0x07; // 'v'(外字) case 0x3d:// '=' return 0x3a; // ':' default : return 0x20; // > ' ' } } // 水の飽和水蒸気圧 es (Pa) float calcEs(float fTemp){ return exp(1.809378 + 7.266115E-2 * fTemp - 3.003879E-4 * fTemp * fTemp + 1.181765E-6 * fTemp * fTemp * fTemp + - 3.863083E-9 * fTemp * fTemp * fTemp * fTemp) * 100.0; } // 露点温度 dp (°CDP) float calcDp(float temp, float humi){ float es = calcEs(temp); // 飽和水蒸気圧 Pa float e = es * humi / 100.0; // 水蒸気圧 Pa return 237.3 / (7.5 / log10((e / 100.0) / 6.1078) -1.0); } // 外字登録(8個まで) void lcdSetup() { byte charData[ 8 ] = {}; // DOWN charData[ 0 ] = B00000; charData[ 1 ] = B00000; charData[ 2 ] = B00000; charData[ 3 ] = B00000; charData[ 4 ] = B10001; charData[ 5 ] = B01010; charData[ 6 ] = B00100; charData[ 7 ] = B00000; lcd.createChar( 0x07, charData ); // DEG charData[ 0 ] = B00011; charData[ 1 ] = B00011; charData[ 2 ] = B00000; charData[ 3 ] = B00000; charData[ 4 ] = B00000; charData[ 5 ] = B00000; charData[ 6 ] = B00000; charData[ 7 ] = B00000; lcd.createChar( 0x06, charData ); // ANT LV0 charData[ 0 ] = B11100; charData[ 1 ] = B01000; charData[ 2 ] = B01000; charData[ 3 ] = B00000; charData[ 4 ] = B00000; charData[ 5 ] = B00000; charData[ 6 ] = B00000; charData[ 7 ] = B00000; lcd.createChar( 0x00, charData ); // ANT LV1 charData[ 0 ] = B11100; charData[ 1 ] = B01000; charData[ 2 ] = B01000; charData[ 3 ] = B00000; charData[ 4 ] = B00000; charData[ 5 ] = B11000; charData[ 6 ] = B11000; charData[ 7 ] = B00000; lcd.createChar( 0x01, charData ); // ANT LV2 charData[ 0 ] = B11100; charData[ 1 ] = B01000; charData[ 2 ] = B01000; charData[ 3 ] = B00000; charData[ 4 ] = B00011; charData[ 5 ] = B11011; charData[ 6 ] = B11011; charData[ 7 ] = B00000; lcd.createChar( 0x02, charData ); // ANT LV3 charData[ 0 ] = B00000; charData[ 1 ] = B00000; charData[ 2 ] = B00000; charData[ 3 ] = B11000; charData[ 4 ] = B11000; charData[ 5 ] = B11000; charData[ 6 ] = B11000; charData[ 7 ] = B00000; lcd.createChar( 0x03, charData ); // ANT LV4 charData[ 0 ] = B00000; charData[ 1 ] = B00000; charData[ 2 ] = B00011; charData[ 3 ] = B11011; charData[ 4 ] = B11011; charData[ 5 ] = B11011; charData[ 6 ] = B11011; charData[ 7 ] = B00000; lcd.createChar( 0x04, charData ); // ANT LV5 charData[ 0 ] = B00000; charData[ 1 ] = B11000; charData[ 2 ] = B11000; charData[ 3 ] = B11000; charData[ 4 ] = B11000; charData[ 5 ] = B11000; charData[ 6 ] = B11000; charData[ 7 ] = B00000; lcd.createChar( 0x05, charData ); } void dispAntenaLevel(byte col, byte row, byte antLevel, byte stAddr) { switch (antLevel) { case 0: lcd.setCursor(col, row); lcd.write(stAddr + 0x00); lcd.setCursor(col + 1, row); lcd.write(0x20); lcd.setCursor(col + 2, row); lcd.write(0x20); break; case 1: lcd.setCursor(col, row); lcd.write(stAddr + 0x01); lcd.setCursor(col + 1, row); lcd.write(0x20); lcd.setCursor(col + 2, row); lcd.write(0x20); break; case 2: lcd.setCursor(col, row); lcd.write(stAddr + 0x02); lcd.setCursor(col + 1, row); lcd.write(0x20); lcd.setCursor(col + 2, row); lcd.write(0x20); break; case 3: lcd.setCursor(col, row); lcd.write(stAddr + 0x02); lcd.setCursor(col + 1, row); lcd.write(stAddr + 0x03); lcd.setCursor(col + 2, row); lcd.write(0x20); break; case 4: lcd.setCursor(col, row); lcd.write(stAddr + 0x02); lcd.setCursor(col + 1, row); lcd.write(stAddr + 0x04); lcd.setCursor(col + 2, row); lcd.write(0x20); break; case 5: lcd.setCursor(col, row); lcd.write(stAddr + 0x02); lcd.setCursor(col + 1, row); lcd.write(stAddr + 0x04); lcd.setCursor(col + 2, row); lcd.write(stAddr + 0x05); } }
<!-- sakura.io HC2 受信用 送信側仕様: ch0:相対湿度 float ch1:温度 float ch2:露点温度 float ch3:トレンドマーク(湿度 1 byte、温度 1 byte) 2020/11/24 f.izawa 2020/11/25 モジュールIDの表示を追加 --> <!-- { "channels": [ { "channel": 0, "type": "f", "value": 35.16, "datetime": "2020-11-24T03:39:30.98414333Z" }, { "channel": 1, "type": "f", "value": 24.21, "datetime": "2020-11-24T03:39:30.99614333Z" }, { "channel": 2, "type": "f", "value": 7.8509927, "datetime": "2020-11-24T03:39:31.00814333Z" }, { "channel": 3, "type": "b", "value": "2d2d000000000000", "datetime": "2020-11-24T03:39:31.02014333Z" } ] } --> <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <style> table.hc2{ border-collapse: collapse; } table.hc2, table.hc2 th, table.hc2 td{ border: 2px #0000ff solid; } </style> <script type="text/javascript"> // sakura.io WebSocket URL const url = "wss://api.sakura.io/ws/v1/xxxx-yyyy-zzzz"; function readstart() { var output = document.getElementById('txtarea'); // WebSocket var client = new WebSocket(url); client.onopen = function() { // 接続開始 document.getElementById('humi').innerHTML = '--.-'; document.getElementById('temp').innerHTML = '--.-'; document.getElementById('dewpoint').innerHTML = '--.-'; document.getElementById('mhumi').innerHTML = ' '; document.getElementById('mtemp').innerHTML = ' '; document.getElementById('mdewpoint').innerHTML = ' '; } client.onerror = function(error) { // エラー alert(error); } client.onmessage = function(e) { // JSON データを受け取る var data = JSON.parse(e.data); // output.innerHTML = output.innerHTML + data.type + '\n'; // data.type には 'keepalive' が含まれる // 'keepalive': sakura.io から時刻だけが送られてくる // デバイスからの受信データ if (data.type == 'channels'){ // モジュール ID : sakura.io 登録時に付けられた ID document.getElementById('module').innerHTML = data.module; //2020-11-25T11:54:35.999067961Z //document.getElementById('module').innerHTML = data.datetime; // GMT ISO8061形式 //var ch0val = data['payload']['channels'][ 0 ]['value']; var ch0val = data.payload.channels[0].value; // も同じ var ch1val = data['payload']['channels'][1]['value']; var ch2val = data['payload']['channels'][2]['value']; document.getElementById('humi').innerHTML = ch0val.toFixed(1); document.getElementById('temp').innerHTML = ch1val.toFixed(1); document.getElementById('dewpoint').innerHTML = ch2val.toFixed(1); var date = new Date(data.datetime);// data['datetime'] も同じ var y = date.getFullYear(); var m = date.getMonth() + 1; var d = date.getDate(); var hour = date.getHours(); var min = date.getMinutes(); var sec = date.getSeconds(); document.getElementById('dtime').innerHTML = y + '/' + ('00' + m).slice(-2) + '/' + ('00' + d).slice(-2) + ' ' + ('00' + hour).slice(-2) + ':' + ('00' + min).slice(-2) + ':' + ('00' + sec).slice(-2); if (data.payload.channels.length >= 4){ var ch3val = data.payload.channels[3].value; document.getElementById('mhumi').innerHTML = markHC2(parseInt(ch3val.substr(0, 2), 16)); document.getElementById('mtemp').innerHTML = markHC2(parseInt(ch3val.substr(2, 2), 16)); //String.fromCharCode(parseInt(ch3val.substr 2, 2), 16)); document.getElementById('mdewpoint').innerHTML = ''; } } } } function markHC2(imark){ switch (imark){ case 0x2b: // '+' return '▲'; case 0x2d: // '-' return '▼'; case 0x3d: // '=' return '■'; default: return ''; } } </script> </head> <body> <h1 style="font-size:50px;">sakura.io HC2</h1> <div style="font-size:50px; font-weight:bold" id="module"></div> <div style="font-size:50px; font-weight:bold" id="dtime"></div> <table border="1" class="hc2" width="100%"> <tr align="center" bgcolor="black" style="color:white; font-size:50px; font-weight:bold"> <td width="33%">%RH</td> <td width="33%">°C</td> <td width="33%">°CDP</td> </tr> <tr align="center" style="font-size:120px; font-weight:bold"> <td><div id="humi"></div></td> <td><div id="temp"></div></td> <td><div id="dewpoint"></div></td> </tr> <tr align="center" style="font-size:50px; font-weight:bold"> <td><div id="mhumi"></div></td> <td><div id="mtemp"></div></td> <td><div id="mdewpoint"></div></td> </tr> </table> <br> <!-- <textarea id ="txtarea" cols="100" rows="10"></textarea> <br> --> <input type="button" onclick="readstart();" value="START" style="WIDTH: 200px; HEIGHT: 100px; font-size:30px;"> </body> </html>