5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

ESP8266+ADS1015でエアコンの消費電流を測定し、ESP-Now → ESP32 → Raspberry Pi → Node-RED → InfluxDB → Grafanaで可視化。

Posted at

はじめに

自宅のエアコンを最近交換したのをきっかけに、どのくらい電力を消費するのかを測定してみたくなりました。
image.png

電流をカレントトランスとADコンバータを用いて測定し、1秒間ごとにRMS値(1秒間の平均)をESP-Nowの通信で、Raspberry PiにUSB接続されたESP32に送り、Node-REDで少し加工した後、influxDBに格納し、Grafanaで可視化してみましたので投稿します。
今回の用途では、わざわざESP-Nowを使って送る必要性は特になく、WiFiで直接Raspberry Piに送っても良いのですが、WiFiの環境が変わっても 送り側のソフトを変更せずに済む効果もあるかと思い、ESP-Nowで通信してみました。

また、わざわざ、node-REDを間に介さなくても、適当なPython等のプログラムで、シリアル通信を受信して、influxDBに書き込むことも問題なく出来ますが、私の自宅IoTでは、基本的に全ての入り口と出口を node-REDで統合して管理するようにしているので、今回もnode-REDを介する事にした次第です。node-REDは各種の信号の流れを描くには、とても適したツールだと思います。

ESP-Nowは、ESP8266とESP32しか対応していないので、一旦ESP32で受けてからRaspberry Piへ送る形になっています。1秒ごとに、InfluxDBのテーブル(Measurement)に書き込み、1分毎の平均値をGrafanaでグラフ化しました。

送信側 (ESP8266: ESP-WROOM02)

image.png

AD変換

エアコンで使われる交流電流の測定のために、秋月電子で購入したカレントトランスを用いました。最大80Aまでの電流を通した時に、3000:1の比率で電流を取り出す事が出来、それを抵抗終端によって電圧に変換したものを、ADS1015というADコンバータでデジタル値に変換しました。(使用したのは、秋月電子のモジュール)。ADS1015は、差動入力が出来るため、今回のように交流電圧でプラス側・マイナス側に振れるモノを、波形のまま測定する事ができます。
image.png

トランスが3000:1という変換比率で、今回は220 Ωの終端抵抗を使いましたので、抵抗両端の電圧Vと、2次側電流 i と、1次側の電流Iとの関係は、

I(mA) = i(mA) x 3000 = V(mV)/220 * 3000 

になります。(I,i,VはRMS値)
抵抗値の誤差が、そのまま測定結果の誤差になりますので、手持ちの220 Ωの金属皮膜抵抗の中で、とりあえず最も220.0 Ωに近いものを選びました。

送信側のコード

コードは、AD関係とESP-Now関係を別ファイルに分け、三つのファイルで構成しました。

AD変換関係

ADS1015のI2C制御には、ライブラリは使用せず、直接I2C通信のコードをべた書きで作りました。特殊な事は何もしていません。I2Cのクロックは、出来るだけ高速に読み書きを行うため、400kHzに設定しています。(ただ、この時、クロック周波数を実測すると、400kHzは出ておらず、なぜか 259kHzでした。理由は分かりません・・・)

ADS1015の設定としては、ゲインの設定で 1bitが1mVとしてあります(±2.048V)。ADS1015のデータシート参照
image.png
image.png

今回の測定は、ACの波形そのものを読みますが、Wire.readで2バイトを読んで16bitに結合した後、符号も必要なため、bitシフト処理では無く、「16で割る」ことにより、符号付のまま、16bit → 12bitにしています。(16bitの状態で 2's complementになっているため)

ads1015.h
#include <Wire.h>

#define ADS1015_ADDR 0x48
#define CONVERSION_REG 0x00
#define CONFIG_REG 0x01
#define CONFIG_0H  0b01000100
#define CONFIG_1H  0b01010100
#define CONFIG_2H  0b01100100
#define CONFIG_3H  0b01110100
#define CONFIG_01H 0b00000100
#define CONFIG_23H 0b00110100
#define CONFIG_L   0b10000011


void init_ADS1015() {
  Wire.begin(); // as default SDA=4, SCL=5
  Wire.setClock(400000L);
  Wire.beginTransmission(ADS1015_ADDR);
  Wire.write(CONFIG_REG);
  Wire.write(CONFIG_01H);
  Wire.write(CONFIG_L);
  Wire.endTransmission();
}

int16_t read_ADS1015_Diff01() {
  int16_t val; //符号付き
  Wire.beginTransmission(ADS1015_ADDR);
  Wire.write(CONVERSION_REG);
  Wire.endTransmission();
  Wire.requestFrom(ADS1015_ADDR, 2);
  val = Wire.read() << 8 | Wire.read() ;
  val /= 16; //シフトでは無く、除算して符号を保持
  return (val);
}

ESP-Now関連

ESP-Now通信に必要な初期設定と、データを送信する関数を定義しています。
指定するMACアドレスは、Slave側のAPモードのMACアドレスになります。(Stationモードの時のMACアドレスとは異なりますので注意が必要です。後述するように、ESP32に受信側のコードを書き込んで走らせると、シリアルターミナルに、APモードのMACアドレスが表示されますので、その値を使います。 もし、複数のESP-Nowスレーブへ送信したい場合には、slaveMACの二次元配列に、必要な数のMACアドレスを追加し、それぞれ、Peerとして追加すれば送れます。

送信側のESP8266を、CONTROLLERと定義し、受信側の相手(peer)をSLAVEあるいはCOMBOとして設定しています。

送信時の call back関数を定義していますが、正常に通信できた場合は、OK、失敗した時は、FailedというStatusが、対応するMACアドレスとともに表示されますので、デバッグ時に便利です。

あとは、実際に送るデータ(浮動小数値)に、送り元のデバイス名を付与して、文字列を作ったうえで、sendします。送り先に NULLを指定すれば、事前に登録したPEER全てに送る事が出来ますし、ここで送り先のMACアドレスを特定すれば、送り先毎に送信内容を分けることも可能です。

#include <ESP8266WiFi.h>
#include <espnow.h>

#define WIFI_CHANNEL 1
uint8_t slaveMAC[][6] = {
  {0x00, 0x00, 0x00, 0x00, 0x00, 0x00}  //ESP32 COM16 AP
};

void printMacAddress(uint8_t* macaddr) {
  Serial.print("{");
  for (int i = 0; i < 6; i++) {
    Serial.print("0x");
    Serial.print(macaddr[i], HEX);
    if (i < 5) Serial.print(',');
  }
  Serial.print("}");
}

void send_cb(uint8_t* mac, uint8_t sendStatus) {
  printMacAddress(mac);
  Serial.print(" --> ");
  Serial.println(sendStatus == 0 ? "OK" : "Failed");
}

void init_ESP_Now() { // Initialize ESP_Now as Master (Controller)
  WiFi.mode(WIFI_STA);
  if (esp_now_init() != 0) {
    Serial.println("*** ESP-Now init failed");
  }
  esp_now_set_self_role(ESP_NOW_ROLE_CONTROLLER);
  Serial.println("add peer");
  Serial.println(esp_now_add_peer(slaveMAC[0], ESP_NOW_ROLE_COMBO, WIFI_CHANNEL, NULL, 0));
  esp_now_register_send_cb(send_cb);
}

void send_ESP_Now(double x) {
  uint8_t bs[250];
  char buf[250];
  char s_rms[8];
  char* s_device = "8266-ADS1015";

  dtostrf(x, 0, 2, s_rms);
  sprintf(buf, "%s,%s", s_device, s_rms);
  memcpy(bs, buf, strlen(buf));
  Serial.println(buf);
  esp_now_send(NULL, bs, strlen(buf)); // NULL means send to all peers
}

メイン

前述の二つのファイルをincludeして、メイン処理をこのファイルに纏めました。

出来る限り高速で(delay()を入れる事無く)AD変換を繰り返し、RMSを求めるために二乗して加算をし続け、100msec毎に平均を取ってルートを取る事でRMSを計算し、さらにそれを、10回平均して1秒に一回送信する形にしました。

digitalWriteで 16ピンに出力しているのは、周期を確認するためのデバッグ用です。正確に100msec毎に反転している事は確認済みです。

RMSで得られた値は、3000:1、220 Ω終端時の電流値になりますので、換算した後、1次側電流のRMS値として出力します。(注:終端抵抗が 220 Ωでない場合や、カレントトランスの変換比率が 3000:1以外の場合には、修正が必要です)

/*
   ADS1015 I2C control basics
   just using Wire.h (without any other library)
   send the result with ESP-Now to the Slave 2020/6/7 Y.Yasuda
*/
#include "ads1015.h"
#include "espnow_mst.h"

uint64_t t_now = 0;
uint64_t t_prev = 0;
double v_sum = 0;
int v_cnt = 0;
double rms_sum = 0;
int rms_ctr = 0;
boolean dbg_pin = false;

void setup() {
  Serial.begin(115200);
  pinMode(16, OUTPUT); //for debug purpose

  init_ESP_Now();
  init_ADS1015();
  t_now = t_prev = millis();
}

void loop() {
  int16_t val; //符号付き
  double v_now, v_rms;
  double rms_avg;

  val = read_ADS1015_Diff01(); //(int16) read ADS1015, differential mode, 0-1

  t_now = millis();
  if (t_now - t_prev >= 100) { //every 100msec
    digitalWrite(16, dbg_pin);
    dbg_pin = !dbg_pin;

    v_rms = sqrt(v_sum / v_cnt); //calc RMS value
    t_prev = t_now;
    v_cnt = 0; //reset for next RMS calculation
    v_sum = 0;

    if (rms_ctr == 10) { //every 1 sec.
      rms_avg = rms_sum / rms_ctr;
      rms_avg = rms_avg * 3000 / 220; //i=V/R=rms_avg*3000/220 ohm -> mA
      send_ESP_Now(rms_avg);
      rms_sum = 0;
      rms_ctr = 0;
    }
    rms_sum += v_rms;
    rms_ctr ++;
  } else {
    v_now = val * val; //Square
    v_sum += v_now; // sum up
    v_cnt ++; // counter
  }
}

以上が送信側のコードと、概略の説明です。

##読み取った波形の例
別のコードで実験した例ですが、出来る限り速くAD変換をして、読み取った値をそのままグラフにすると、以下のようにAC波形がそのまま出力できます。この例では、白熱電球を負荷にして測定したため、きれいな波形が出ています。

image.png

参考までに、他の波形も載せておきます。

LEDのフラットランプ ・・ もの凄くスイッチングのノイズが大きいです。
image.png

LEDバックライトのコンピュータモニタ
image.png

蛍光灯のデスクライト
image.png

受信側

ESP32: ESP-WROOM32 での ESP-Now Slaveモード

ESP-Nowの受信側には、ESP32を使いました。ESP-Nowに対応しているデバイスは、ESP8266かESP32のみですが、ESP8266での実験は以前試したことがあるので、今回は秋月電子の ESP32のDevelopmentボードを使って、ESP8266からのESP-Now通信を受信してみました。

image.png

受信側 ESP32のコード (Slave、APモード)

ESP8266で includeするファイルは、espnow.hでしたが、ESP32では、esp_now.hになります。関数の種類や引数のタイプ等が、色々違っていますので注意が必要です。

マスター側(コントローラー側)からは、スレーブのMACアドレスに対してESP-Now通信をしますので、スレーブ側のMACアドレスを最初に表示するようにしています。
MACアドレスには、STATIONモード時の値と、APモード時の値がそれぞれありますので、ここでは、APモードの値を表示して使います。

一旦、WiFi.mode(WIFI_AP)で、APモードを立ち上げる準備をしていますが、APモードでのMACアドレスを表示した後は、直ぐ、Disconnectしています。すなわち、WiFiのAPモードそのものを利用するわけでは無いので、本来のAPを立ち上げる必要はないという事です。

ESP-Nowの初期化をした後、受信バッファにデータが届いた際のcallback 関数を定義します。データが届いた際は、このcallback関数が呼ばれるので、必要な処理はこの中に書きます。 やっている事は単純で、受信バッファに入った文字列を、そのままシリアルに出力するだけです。 文字列の処理で、最後に NULLを入れて文字列を作ったうえで、Serial.printlnしています。(送信側のデータには、最後にNULLは付与されていないので) 後は、待つだけなので、loop()の中味は何もありません。

どのデバイスからどんなデータが来るのかは、このSlaveでは関与しない事にしたため、このSlaveのコードは汎用的に使えるはずです。Raspberry Piに比べ、ESP系のソフトウエア変更は、OTAを使わない限り、シリアル接続が必要になりますので、出来るだけ汎用的なコードにしておいて、書き換える頻度を減らして、メンテナンスを楽にしたいという意図があります。

#include <esp_now.h>
#include <WiFi.h>

#define WIFI_CHANNEL 1
char buf[200];

void setup() {
  Serial.begin(115200);
  WiFi.mode(WIFI_AP);
  Serial.println( WiFi.softAPmacAddress() );
  WiFi.disconnect();
  if (esp_now_init() != ESP_OK)  {
    Serial.println("ESP-Now Init Failed....");
    return;
  }
  esp_now_register_recv_cb(recv_cb);
}

// call back for receiving data
void recv_cb(const uint8_t * mac, const uint8_t *recvData, int len) {
  memcpy(&buf[0], recvData, len);
  buf[len]='\0'; //lenには文字列末のNULLが入らないので暫定的に。
  Serial.println(buf);
}

void loop() {
}

このコードを、ESP32のDevボードに書き込んで終わりです。シリアル通信のボーレートは、115200にしました。

Raspberry Piの USBポートを固定

Raspberry PiのUSBポートに、Arduinoや、ESP32、ESP8266等をシリアル接続すると、/dev/ttyUSB* というドライバーで通信する事になりますが、/dev/ttyUSB0とか /dev/ttyUSB1とかいう最後の数字が、Raspberry Piのリブート時などに、立上りの順番で入れ替わってしまう事が良くあります。入れ替わってしまうと、受信側のソフトウエアでの設定が食い違うため、正常な通信が出来なくなります。

それを回避するために、「どのデバイスには、どの番号」というルールを固定しておく必要があります。この方法は、ネットのあちこちにありますので、ここでは詳細を書きませんが、lsusbでデバイスの情報(Vendor Id, Product Id)を取得して、/etc/udev/rules.d/99-local.rulesのファイルを編集して、それぞれのシンボリックリンクを張っておくことで実現できます。
ここでは、このESP32に対しては、/dev/ttyUSB_ESP32-1 という名前を付けました。

$ ls -l /dev/ttyUSB*
crw-rw---- 1 root dialout 188, 0  6月  2 08:47 /dev/ttyUSB0
crw-rw---- 1 root dialout 188, 1  6月  7 11:17 /dev/ttyUSB1
lrwxrwxrwx 1 root root         7  6月  7 11:17 /dev/ttyUSB_ESP32-1 -> ttyUSB1
lrwxrwxrwx 1 root root         7  6月  2 08:47 /dev/ttyUSB_ESP8266-1 -> ttyUSB0
$

node-RED設定

node-REDでの読込設定で、先ほどのシンボリックリンク/dev/ttyUSB_ESP32-1を使います。レートは、115200が設定されています。下記のフローを作成し、デプロイすれば、USBに繋がった ESP32が「接続済」となり、データが入る毎に、データベースに書き込まれます。
influxdb用のノードは、標準的にインストールされたnode-REDには、入っていませんので、「パレットの管理」から "node-red-contrib-influxdb"をインストールする必要があります。バージョンは、0.4.0 と少し心配になる番号ですが、問題なく使用できています。
image.png
インストールすると、ストレージの部分に、"influxdb out", "influxdb in", "influx batch"の三つが追加されます。データ書き込み(insert)には、influxdb outを使います。
image.png

フロー図 (既にESP32が接続された状態)

対象部分のnode-REDのフロー図は以下です。 USB経由で Serial in入力を行い、functionノードで、influxdb用に書式変換を行い、influxdb outノードでデータベースへの書き込みを行うという流れです。

image.png

serial inノードで受信

image.png

functionノードで変換

functionノードで、payloadの中から、必要なデータを取出し、influxDB用に書式変換します。
image.png

// influxDBへ書き込むためのの書式へ変更
var x = msg.payload.split(',');

if (x[0] === "8266-ADS1015"){
    msg.payload = {
        "power": parseFloat(x[1]),
    }
    return msg;
}

influxdb out ノード

influxdb outノードで、データベースに書き込みます。同じRaspberry Pi上に作ってある influx_iot01というデータベースの、table1というテーブル(influxdbでは、measurementと呼ぶ)に、入力が入る毎に、timestampと一緒に書きこまれます。
image.png

###フロー

[{"id":"a803abf3.140428","type":"serial in","z":"c851379d.611068","name":"ESP32-COM16","serial":"51c49bfa.f987f4","x":180,"y":220,"wires":[["967f0572.6257f8"]]},{"id":"8680d261.609c9","type":"comment","z":"c851379d.611068","name":"ESP-Now Slave ESP32-COM16 (電力 from ESP8266 書斎エアコン)","info":"","x":350,"y":180,"wires":[]},{"id":"967f0572.6257f8","type":"function","z":"c851379d.611068","name":"電力CT3000-220","func":"// influxDBへ書き込むためのの書式へ変更\nvar x = msg.payload.split(',');\n\nif (x[0] === \"8266-ADS1015\"){\n    msg.payload = {\n        \"power\": parseFloat(x[1]),\n    }\n    return msg;\n}\n","outputs":1,"noerr":0,"x":370,"y":220,"wires":[["fb09fc73.e27a2","5259d61e.a86118"]]},{"id":"fb09fc73.e27a2","type":"debug","z":"c851379d.611068","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":590,"y":280,"wires":[]},{"id":"5259d61e.a86118","type":"influxdb out","z":"c851379d.611068","influxdb":"e2d98b52.c73258","name":"","measurement":"table1","precision":"","retentionPolicy":"","x":660,"y":220,"wires":[]},{"id":"51c49bfa.f987f4","type":"serial-port","z":"","serialport":"/dev/ttyUSB_ESP32-1","serialbaud":"115200","databits":"8","parity":"none","stopbits":"1","waitfor":"","newline":"\\n","bin":"false","out":"char","addchar":"","responsetimeout":"10000"},{"id":"e2d98b52.c73258","type":"influxdb","z":"","hostname":"127.0.0.1","port":"8086","protocol":"http","database":"influx_iot01","name":"","usetls":false,"tls":""}]

influxdbは、時系列データベースの一つであり、IoTのような「時間」によるデータの変化を記録したり、Queryしたりする際にとても便利で、使いやすいデータベースです。可視化のGrafanaツールとも非常に相性が良く、簡単に時系列をベースとした可視化が実現できるのでお勧めです。

influxDB設定

influxdbが起動している状態であれば、特別な設定は不要です。予めテーブルを作っておく必要も無ければ、あらかじめフィールドの定義をしておく必要もありません。書き込めば、自動的に対応したフィールドが作成されます。主キーは常に挿入した時刻(タイムスタンプ、UTC時刻)になります。

どのような形で、データベースに記録されているのかを、念のために確認します。influxコマンドを使って、対象のデータベース(ここでは、influx_iot01)を開き、書き込んだテーブル(ここでは、table1)から、電力のフィールド(ここでは、power・・・実際には、電流値:mAで記録されている)を表示してみました。 ちなみに、timeフィールドは、挿入時に自動的に、挿入時刻の値が付与されます(UTC時刻)

$ influx
Connected to http://localhost:8086 version 1.8.0
InfluxDB shell version: 1.8.0
> use influx_iot01
Using database influx_iot01
> select power from table1 limit 10
name: table1
time                power
----                -----
1591496377829846016 237.35
1591496378770157606 237.04
1591496379623660077 237.1
1591496380629092720 236.49
1591496381623668463 237.2
1591496382623850159 237.07
1591496383627910477 237.66
1591496384622593468 237.78
1591496385624646721 237.85
1591496386639334993 237.81
>

データベースには、正しく書き込まれているようなので、Grafanaを使って可視化してみます。

Grafana設定

ダッシュボードに、パネルを一つ作成し、"Graph"パネルから、必要なフィールドを設定してグラフ化します。特に計算等が必要で無ければ、GUIから選択していけばグラフは簡単にできます。
image.png

今回は、データベース内の電流値(mA)から、100V時の電力 P=IV (Watt)を計算してからグラフ化したかったので、得られた値 x 100V / 1000 という計算をするため、Queryを変更しました。

SELECT mean("power") /10 FROM "table1" WHERE $timeFilter GROUP BY time(1m) fill(null)

##得られたグラフ
結果として、以下のグラフが出来ました。
image.png

他の情報と併せたダッシュボードとして、可視化できました。
image.png

おわりに・・・・今後の分析のために

とりあえず、エアコンの消費電力をグラフ化出来ました。これで、エアコンの各モード(例えば、省エネモードとか)を切り替えた時に、どのように消費電力が変化するのかを調べてみるために、面白そうです。ただ、これだけでは、「見るだけ」なので、もう少し深い解析をしたり、電力料金との比較をしたり・・などの分析を進めていきたいと思っています。 もう少し、分析できたら、投稿したいと思います。

5
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?