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>