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>