
今回はESP32-C3を自宅のWiFiに接続して、測定したデータをGoogleスプレッドシートに記録していくプログラムを作っていきます。
前回は日射計からの微弱な信号を増幅する回路を設計しました。前回の記事はこちらです。
Lang-shipさんのサイトを参考にする
ESP32からGoogleスプレッドシートにデータを記録するプログラムは、すでにLang-shipさんが公開してくれています。Lang-shipさんは、実際に自分で実験して丁寧に情報をまとめてくれているすごいサイトで、ESP32関連ではよく参考にさせていただいています。
今回はこのサイトの「GoogleスプレッドシートにESP32からデータ登録する」と、「Googleスプレッドシートのログローテーション」の2つの記事を参考にさせていただきました。詳しい内容はぜひLang-shipさんのサイトをご覧ください。
プログラムはGAS(Google Apps Script)で記述します。まず「GoogleスプレッドシートにESP32からデータ登録する」を参考に、ESP32からデータを送信するためのURLを取得しました。このURLにデータを含めてアクセスすることで、Googleスプレッドシートにデータが記録される仕組みです。
ただ、このままでは何日も記録し続けると行がどんどん増えていってしまいます。そこで「Googleスプレッドシートのログローテーション」の記事を参考に、毎月新しいファイルに記録されるようにしました。
ログローテーションのスクリプトを2箇所修正
「Googleスプレッドシートのログローテーション」のスクリプトはそのままでも動くのですが、今回の用途に合わせて2箇所修正しました。
1つ目は、月が変わったときに先月分のデータが別名で保存される際、ファイル名が現在の月になってしまう問題です。これを先月の月名でファイル名が付くように直しました。
2つ目は、データが1行も登録されていない状態で翌日新しいシートが追加されると、エラーが発生してしまう問題です。これに対処しました。
GASにはあまり詳しくないので、AIに相談しながらコードを書いていきました。
1つ目の修正:ファイル名を先月にする
まず、一つ前の年月を取得する関数を作りました。
//一つ前の年月を取得する
function getPreviousMonth() {
const oneMonthAgo = new Date();
oneMonthAgo.setMonth(oneMonthAgo.getMonth() - 1);
return Utilities.formatDate(
oneMonthAgo,
SpreadsheetApp.getActiveSpreadsheet().getSpreadsheetTimeZone(),
'yyyyMM'
);
}
これをファイル名を生成する部分に組み込みます。
// 日付でスプレッドシートを別名保存
const filename = spreadsheet.getName() + "_" + getPreviousMonth();
2つ目の修正:データが0行のときのエラー対処
最終行が2以上のときだけデータの削除を行うようにしました。
// 既存データを消す
if( ( sheet.getLastRow() - 1 ) > 0 ) {
sheet.getRange(2, 1, sheet.getLastRow() - 1, sheet.getLastColumn()).clearContent();
Logger.log(`2行目以降のデータを削除しました。`);
}
これでGoogleスプレッドシート側の設定は完了です。
ESP32側のプログラムを作る
続いてESP32側です。こちらもLang-shipさんの「GoogleスプレッドシートにESP32からデータ登録する」をベースに、他の必要な処理を追加していきます。
今回は12:00、12:10、12:20というように、10分刻みで測定値をGoogleスプレッドシートに記録していく仕様にしました。
プログラムの流れは次の通りです。
- GPIOのピン設定
- WiFi接続(接続できなかったらソフトリセット)
- NTPサーバと時刻の同期
- AD値の取得(16回測定して平均する)
- AD値を物理量へ変換
- RTCメモリ(deepsleepしても消えないメモリ)に日射量と時刻を保存
- Googleスプレッドシートにデータを送信(失敗したら5回までリトライ)
- 次の10分区切りまでの残り時間を計算
- 残り時間分だけdeepsleepする
googleスプレッドシートの応答が遅い
「Googleスプレッドシートにデータを送信」の部分は、Lang-shipさんのプログラムを流用しつつ、一部を追加しました。
というのも、Googleにアクセスしてからレスポンスが返ってくるまで、たまにかなり時間がかかることがあるのです。タイムアウト時間はデフォルトで5秒に設定されているのですが、これを超えてしまいます。
そうなるとhttp.begin()がタイムアウトしてしまい、httpCodeに-11(HTTPC_ERROR_READ_TIMEOUT)が返ってきます。この現象が一日に10回ほど発生していました。最初は-11が返ってきたらデータを再送していたのですが、-11が返ってきても実はスプレッドシートにはデータが記録されています。ただ単にレスポンスが遅いだけのようです。
そこでタイムアウト時間を20秒に延長しました。
http.setTimeout(20000); // 応答タイムアウトを20秒に設定
これで、今のところタイムアウトは発生していません。
以上でプログラムの作成は完了です。プログラム全体は付録に載せておきます。
測定テストしてみる

さっそく動かしてみると、Googleスプレッドシートに10分ごとに日射計、照度計、日射量、電池電圧が記録されていくのが確認できました。
一日測定してみた結果

一日分のデータを見てみると、なんとなくそれっぽいグラフになっています。朝7時くらいに突然グラフが立ち上がっているのは、センサーが早朝は東側に建ってる家の影に入ってしまっているからです。
ただ、照度計のグラフだけちょっと様子がおかしいことに気づきました。どうやら6万Lux程度で頭打ちになってしまっているようです。太陽の照度は最大10万Lux程度と言われているので、測定レンジが倍近く足りていません。
今回使った明るさセンサーは、暗くなったらライトを点灯させるような用途を想定したセンサーだったので、太陽光のような強い光には対応しきれなかったのかもしれません。そもそもデータシートには最大照度が書かれておらず、100Luxの時に光電流が50uAという値しか書かれていませんでした。そこで、10万Luxまで測定できそうな(でも無理かもしれない)明るさセンサーを購入して、届くのを待っているところです。10万Luxまで測れる明るさセンサーってあまりなさそうですね。
日射量の方はどうかというと、NEDOが提供している日射量データベースによると、最寄りの八王子の8月の平均日射量は4.5kWh/m²だそうです。測定したデータでは、晴れた日は6kWh/m²、晴れたり曇ったりの日は3.9kWh/m²、ほぼ曇っていた日は2.5kWh/m²でした。そう的外れな値にはなっていなさそうです。
日射量を測定する仕組みが完成しました

これで、日射量を測定する仕組みが一通り完成しました。しばらく測定を続けてデータが溜まったら、いよいよ太陽電池の発電量と比較してみようと思います。
付録
ESP32-C3で日射量を測定してgoogleスプレッドシートへ記録するプログラムです。ご参考までに。
#include <Arduino.h>
#include <WiFi.h>
#include <HTTPClient.h>
#include <time.h>
#include <sys/time.h>
#define ADAccumulations 16 //AD積算回数
#define SolarRadiationPin 0
#define LuxMeterPin 1
#define BatteryVoltagePin 3
#define BatteryMeasurementPin 5
#define SolarRadiationFactor 0.47348485f
#define LuxMeterFactor 66.66666667f
#define BatteryVoltageFactor 3.0f
#define SolarRadiationMVoffset 7.0f
#define LuxMeterMVoffset 6.0f
#define BatteryVoltageoffset 7
#define BatteryMeasurementPinON HIGH
#define BatteryMeasurementPinOFF LOW
#define SSID "SSID"
#define PASSWORD "PASSWORD"
#define WiFiConnectionRemain 120 //WiFi接続最大待ち時間[sec/2]
#define retryTimes 5 //httpCodeがOKではない時のリトライ回数
#define measermentPeriod 10 * 60 * 1000 //測定周期[ms]
RTC_DATA_ATTR float solarRadiationConsumption = 0.0f; //日射量[Wh/m2]
RTC_DATA_ATTR float lastSolarRadiation = 0.0f; //前回測定した日射強度[W/m2]
RTC_DATA_ATTR int lastDay = 0; //前回測定した日[日]
RTC_DATA_ATTR time_t lastTime = 0; //前回測定したUNIX時間
void setup() {
//GPIOピンの初期化
analogReadResolution(12);
pinMode(SolarRadiationPin, INPUT);
pinMode(LuxMeterPin, INPUT);
pinMode(BatteryVoltagePin, INPUT);
pinMode(BatteryMeasurementPin, OUTPUT);
digitalWrite(BatteryMeasurementPin, BatteryMeasurementPinOFF); //電圧測定を無効
//ADのアッテネータを-11dBに設定
analogSetAttenuation(ADC_11db); //11dB
//シリアルポートを初期化
Serial.begin(115200); //シリアル通信の初期化
delay(1000);
Serial.println("Start"); //起動時の表示
//WiFiに接続する
int connectionCountRemain = WiFiConnectionRemain;
WiFi.setAutoReconnect(true);
WiFi.begin(SSID, PASSWORD);
while (WiFi.status() != WL_CONNECTED && connectionCountRemain-- > 0) {
delay(500);
Serial.print(".");
}
if( connectionCountRemain <= 0)
{
//WiFiに接続できなかったらリセット
Serial.println("Can not connect to WiFI. Reboot");
WiFi.disconnect(true);
delay(1000);
esp_sleep_enable_timer_wakeup(5000*1000);
esp_deep_sleep_start();
}
Serial.println("");
Serial.println("WiFi connected.");
Serial.print("IP address: ");
Serial.println(WiFi.localIP()); //WiFiのIPアドレスを表示
//NTPサーバから時刻を取得
configTime( 9 * 3600L, 0, "ntp.nict.jp", "time.google.com"); //NTPサーバから時刻を取得
Serial.println("Waiting for time");
while (time(nullptr) < 1609459200) { // 2021/1/1 0:0:0
delay(500);
Serial.print(".");
}
Serial.println("Time is set");
struct tm timeinfo;
getLocalTime(&timeinfo);
Serial.println(&timeinfo, "%Y/%m/%d %H:%M:%S"); //現在時刻を表示
Serial.println(); //改行
}
void loop() {
digitalWrite(BatteryMeasurementPin, BatteryMeasurementPinON); //電圧測定を有効
//現在時刻を取得
struct tm timeinfo;
getLocalTime(&timeinfo);
//現在時刻の文字列を生成する
char dayStringBuff[50] = {0};
strftime(dayStringBuff, sizeof(dayStringBuff), "%Y/%m/%d", &timeinfo);
char timeStringBuff[50] = {0};
strftime(timeStringBuff, sizeof(timeStringBuff), "%H:%M:%S", &timeinfo);
Serial.printf("%s %s ",dayStringBuff,timeStringBuff); //現在日時を表示
//ADピンからAD値をmVで取得
int SolarRadiationMV = 0;
int LuxMeterMV = 0;
int BatteryVoltageMV = 0;
for(int i=0 ; i<ADAccumulations ; i++)
{
SolarRadiationMV += analogReadMilliVolts(SolarRadiationPin);
LuxMeterMV += analogReadMilliVolts(LuxMeterPin);
BatteryVoltageMV += analogReadMilliVolts(BatteryVoltagePin);
}
digitalWrite(BatteryMeasurementPin, BatteryMeasurementPinOFF); //電圧測定を無効
//オフセットを除去
float SolarRadiationMVf = (float)SolarRadiationMV / (float)ADAccumulations - SolarRadiationMVoffset;
if( SolarRadiationMVf < 0.0 )
{
SolarRadiationMVf = 0.0;
}
float LuxMeterMVf = (float)LuxMeterMV / (float)ADAccumulations - LuxMeterMVoffset;
if( LuxMeterMVf < 0.0 )
{
LuxMeterMVf = 0.0;
}
float BatteryVoltageMVf = BatteryVoltageMV / (float)ADAccumulations - BatteryVoltageoffset;
if( BatteryVoltageMVf < 0.0 )
{
BatteryVoltageMVf = 0.0;
}
//mVを日射強度、照度、電圧に変換
float SolarRadiation = SolarRadiationMVf * SolarRadiationFactor; //ADの値を日射強度に変換
int LuxMeter = (int)(LuxMeterMVf * LuxMeterFactor); //ADの値を照度に変換
float BatteryVoltage = BatteryVoltageMVf * BatteryVoltageFactor / 1000.0f;
Serial.printf("SolarRadiation:%.2f[W/m2](%.2f[mV]) LuxMeter:%d[Lux](%.2f[mV]) BatteryVoltage:%.2f(%.0f[mV])\n",SolarRadiation,SolarRadiationMVf,LuxMeter,LuxMeterMVf,BatteryVoltage, BatteryVoltageMVf);
//一番最初の起動時は計算しない
if(lastTime != 0)
{
//前回測定した時間との差を計算する
int diffSec = difftime(mktime(&timeinfo),lastTime);
float diffHour = diffSec / 60.0f / 60.0f; //時間に変換
Serial.printf("前回測定から%.5f時間(%d秒)経過 ",diffHour, diffSec); //経過時間表示
//前回分の日射量を計算する
float lastSolarRadiationConsumption = lastSolarRadiation * diffHour;
//日射量に加算する
solarRadiationConsumption += lastSolarRadiationConsumption;
Serial.printf("積算日射量:%.3f[Wh/m2]\n",solarRadiationConsumption);
}else{
solarRadiationConsumption = 0;
}
//前回測定した日と違かったら日射量を0にする
if( lastDay != timeinfo.tm_mday )
{
solarRadiationConsumption = 0.0f;
}
//RTCメモリに情報を保存
lastTime = mktime(&timeinfo);
lastSolarRadiation = SolarRadiation;
lastDay = timeinfo.tm_mday;
//URL生成
HTTPClient http;
String url = "https://script.google.com/macros/s/********/exec";
url += "?d1=" + String(dayStringBuff);
url += "&d2=" + String(timeStringBuff);
url += "&d3=" + String(SolarRadiation);
url += "&d4=" + String(SolarRadiationMVf);
url += "&d5=" + String(LuxMeter);
url += "&d6=" + String(LuxMeterMVf);
url += "&d7=" + String(solarRadiationConsumption);
url += "&d8=" + String(BatteryVoltage);
Serial.println(url);
http.setConnectTimeout(10000); // 接続タイムアウトを10秒に設定
http.setTimeout(20000); // 応答タイムアウトを20秒に設定
http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); //リダイレクトに対応
for( int retryCount=0 ; retryCount<=retryTimes ; retryCount++)
{
http.begin(url);
int httpCode = http.GET();
Serial.printf("httpCode:%d %s\n",httpCode, http.errorToString(httpCode).c_str());
//正常にGETできたか確認
if (httpCode == HTTP_CODE_OK) {
String payload = http.getString();
Serial.println(payload);
break;
}else{
if( retryCount < retryTimes )
{
//10秒待ってからリトライ
Serial.printf("Retry %d/%d Waiting...\n",retryCount+1,retryTimes);
delay(10000);
}else{
Serial.println("Gave up on connecting");
break;
}
}
}
http.end();
//現在時刻をmsで取得する
struct timeval tv;
gettimeofday(&tv, NULL); // 時刻構造体を取得
localtime_r(&tv.tv_sec, &timeinfo); // 秒数を日付・時刻に変換
int current_min = timeinfo.tm_min; // 分 (0-59)
int current_sec = timeinfo.tm_sec; // 秒 (0-59)
long current_ms = tv.tv_usec/1000; // ms
long current_time_ms = current_min * 60 * 1000 + current_sec * 1000 + current_ms;
//次の測定タイミングまでのmsを計算する
long sleepTime = ( measermentPeriod ) - ( current_time_ms % ( measermentPeriod ) );
//sleepTimeのmsを分、秒、ミリ秒に変換する
int sleep_min = sleepTime / (60 * 1000);
int sleep_sec = (sleepTime % (60 * 1000)) / 1000;
int sleep_ms = (sleepTime % (60 * 1000)) % 1000;
Serial.printf("Sleep time:%ld[ms](%dmin %dsec %dms)\n",sleepTime, sleep_min, sleep_sec, sleep_ms);
//指定時間DeepSleep
esp_sleep_enable_timer_wakeup(sleepTime * 1000);
esp_deep_sleep_start();
}




コメント