#本記事の内容
- M5StackでGoogleスプレッドシートとデータのやり取りをするために、Google App Scriptを用いて連携することにしました。ESP32に初期搭載されていたWiFiClientSecureでは、Google App ScriptのWeb Appの仕様(※1)に対応できておらず、上手くいきませんでした。この仕様に対応しているHTTPSRedirectというライブラリーを利用することで上手く行ったので、どのように対応したか本記事で記載します。
※1 HTTP通信のレスポンスを一度別のURLにリダイレクトして返却する
Googleの公式ドキュメントに記載のあるこちらの仕様のことです。
Redirects
For security reasons, content returned by the Content service isn't served from script.google.com, but instead redirected to a one-time URL at script.googleusercontent.com.
This means that if you use the Content service to return data to another application, you must ensure that the HTTP client is configured to follow redirects.
セキュリティ対策として、レスポンスはリクエスト時に指定した"script.google.com"ドメインではなく、"script.googleusercontent.com"ドメインの一時的なURLから返却されるようです。ESP32に搭載されているWiFiClientSecureは、リダイレクト非対応のためこちらの仕様に沿ってリダイレクトされるとリダイレクトに追随できずレスポンスが取得できなかった。
[この連携を実現したい背景]
この記事(【コロナ対策】ラズパイとCO2センサーでポータブルIoTシステムを作ってみた)で書いたポータブルIoTシステムで蓄積したデータを閲覧できるデバイスを作りたく、M5Stackを活用することを思いつきました。M5StackとGoogleスプレッドシートを連携させることで実現します。
以下に実施したことを書いていきます。
#実施環境
- Windows 10
- Arduino 1.8.16
- M5Stack Gray
- Google スプレッドシート
- Google App Script
- HTTPS Redirectライブラリー
#手順の流れ
- WiFiClientSecure通信時に起こったことの説明
- HTTPS Redirectライブラリーのダウンロード
- Arduino IDEへHTTPS Redirectライブラリーのインポート
- HTTPS Redirectライブラリーの設定
- Arduino IDEでの実装
- M5Stackでの実施
1. WiFiClientSecure通信時に起こったことの説明
WiFiClientSecureでGoogle App Scriptにリクエストを出したところ、以下のログが出た。シリアルモニタはトラブル時の解析に役立つ。コンソールでのログの確認の仕方は
こちらのサイト"Arduinoからデバッグ用ログ出力"を参考にさせて頂きました。
HTTP/1.1 302 Moved Temporarily
Content-Type: text/html; charset=UTF-8
Access-Control-Allow-Origin: *
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: Mon, 01 Jan 1990 00:00:00 GMT
Date: Thu, 30 Sep 2021 08:14:53 GMT
Location: https://script.googleusercontent.com/macros/echo?user_content_key=-OUK~省略~
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Content-Security-Policy: frame-ancestors 'self'
X-XSS-Protection: 1; mode=block
Server: GSE
Alt-Svc: ~省略~
Accept-Ranges: none
Vary: Accept-Encoding
Connection: close
Transfer-Encoding: chunked
コードが想定に動かず、いろいろ調べたところ”302 Moved Temporarily”が発生していた。これは全く予想外でした。やはりトラブル時のデバッグはとっても大事だと痛感しました。
2. HTTPS Redirectライブラリーのダウンロード
HTTPS Redirectのライブラリーを公開してくださっているこちらのページ"electronicsguy/HTTPSRedirect"
にアクセス。"Download ZIP"をクリックし、HTTPSRedirectのライブラリーのzipファイルをダウンロードする。
3. Arduino IDEへHTTPS Redirectライブラリーのインポート
Arduinoツールを起動し、スケッチ > ライブラリーをインクルード > .zip形式のライブラリーをインストール ...
のメニューを開く。
ダウンロードしたHTTPSRedirectのライブラリーのzipファイルを指定する。
4. HTTPS Redirectライブラリーの設定
こちらの設定を行わずに初期状態のままGoogle App Scriptとの通信を行うとHTTPS通信中にハングして通信が止まる。
M5Stack initializing...OK
[D][WiFiGeneric.cpp:374] _eventCallback(): Event: 0 - WIFI_READY
[D][WiFiGeneric.cpp:374] _eventCallback(): Event: 2 - STA_START
[D][WiFiGeneric.cpp:374] _eventCallback(): Event: 4 - STA_CONNECTED
[D][WiFiGeneric.cpp:374] _eventCallback(): Event: 7 - STA_GOT_IP
[D][WiFiGeneric.cpp:419] _eventCallback(): STA IP: 192.168.3.30, MASK: 255.255.255.0, GW: 192.168.3.1
[V][ssl_client.cpp:59] start_ssl_client(): Free internal heap before TLS 249156
[V][ssl_client.cpp:65] start_ssl_client(): Starting socket
[V][ssl_client.cpp:104] start_ssl_client(): Seeding the random number generator
[V][ssl_client.cpp:113] start_ssl_client(): Setting up the SSL/TLS structure...
[I][ssl_client.cpp:127] start_ssl_client(): WARNING: Skipping SSL Verification. INSECURE!
[V][ssl_client.cpp:197] start_ssl_client(): Setting hostname for TLS session...
[V][ssl_client.cpp:212] start_ssl_client(): Performing the SSL/TLS handshake...
[V][ssl_client.cpp:233] start_ssl_client(): Verifying peer X.509 certificate...
[V][ssl_client.cpp:242] start_ssl_client(): Certificate verified.
[V][ssl_client.cpp:257] start_ssl_client(): Free internal heap after TLS 199828
[V][ssl_client.cpp:295] send_ssl_data(): Writing HTTP request with 154 bytes...
[V][ssl_client.cpp:59] start_ssl_client(): Free internal heap before TLS 197876
[V][ssl_client.cpp:65] start_ssl_client(): Starting socket
[V][ssl_client.cpp:104] start_ssl_client(): Seeding the random number generator
[V][ssl_client.cpp:113] start_ssl_client(): Setting up the SSL/TLS structure...
[I][ssl_client.cpp:127] start_ssl_client(): WARNING: Skipping SSL Verification. INSECURE!
[V][ssl_client.cpp:197] start_ssl_client(): Setting hostname for TLS session...
[V][ssl_client.cpp:212] start_ssl_client(): Performing the SSL/TLS handshake...
[V][ssl_client.cpp:233] start_ssl_client(): Verifying peer X.509 certificate...
[V][ssl_client.cpp:242] start_ssl_client(): Certificate verified.
[V][ssl_client.cpp:257] start_ssl_client(): Free internal heap after TLS 161540
[V][ssl_client.cpp:295] send_ssl_data(): Writing HTTP request with 401 bytes...
★★エラー [E][ssl_client.cpp:36] _handle_error(): [data_to_read():287]: (-29184) SSL - An invalid SSL record was received
[V][ssl_client.cpp:265] stop_ssl_socket(): Cleaning SSL connection.
調べたところHTTPSRedirectライブラリー内に含まれる「HTTPSRedirect.cpp」のソースの修正を行うことでエラーを回避できる。
インポートしたライブラリは、"スケッチブックの保存場所\Arduino\libraries\HTTPSRedirect-main"に保存されており、該当のファイルもそのディレクトリに格納されている。
スケッチブックの保存場所は、Arduino IDEのファイル > 環境設定
のメニューを開くことで確認できる。
以下が「HTTPSRedirect.cpp」の修正前。
case 301:
case 302:
{
// Get re-direction URL from the 'Location' field in the header
if (getLocationURL()){
// stop(); // may not be required
以下が修正後。stopのコメントアウトを外すだけで修正は完了。
case 301:
case 302:
{
// Get re-direction URL from the 'Location' field in the header
if (getLocationURL()){
stop(); // may not be required
以上で修正は完了する。
5. Arduino IDEでの実装
様々なソースを参考に以下の様に実装しました。こちらはM5Stackのソース。
#include <M5Stack.h>
#include <WiFiClientSecure.h>
#include <HTTPSRedirect.h>
//****** WI-Fi通信設定 ******
const char* wifi_ssid = "XXXXXXXX";
const char* wifi_password = "YYYYYYYY";
//****** Google App Scriptとの通信設定 ******
const char* httphost = "script.google.com";
String exec_url = "/macros/s/~省略~/exec";
//****** NTP通信設定 ******
const char* ntpServerDomain = "ntp.nict.jp";
const long gmtOffset_sec = 3600 * 9;
const int daylightOffset_sec = 0;
//****** 変数関連 ******
String body = "";
struct tm timeInfo;
char now[20];
HTTPSRedirect* client = nullptr;
//ディスプレイの初期化メソッド
void LcdInit(){
M5.Lcd.clear(BLACK);
M5.Lcd.setTextSize(2);
M5.Lcd.setTextColor(WHITE,BLACK);
M5.Lcd.setCursor(0, 0);
}
//Google App Scriptとの通信
void getValues() {
M5.Lcd.println("get values process start...");
// HTTP通信設定
client = new HTTPSRedirect(443);
client->setInsecure();
client->setPrintResponseBody(true);
if (client->connect(httphost, 443)){
LcdInit();
M5.Lcd.println("http request start...");
client->GET(exec_url, httphost);
delay(1000);
body = client->getResponseBody();
M5.Lcd.println("http request finish...");
delete client;
client = nullptr;
M5.Lcd.setTextColor(0xd7f4);
M5.Lcd.setTextSize(3);
M5.Lcd.println("Body Data");
M5.Lcd.println("");
M5.Lcd.println(body);
M5.Lcd.setTextColor(WHITE);
M5.Lcd.setTextSize(2);
M5.Lcd.print("Http Success");
}
else {
M5.Lcd.print("Http error");
}
M5.Lcd.println("get values process finish...");
}
//WiFi設定
void connectingWiFi(){
boolean WiFiState;
int attempted_count, max_trial_count = 30;
attempted_count = 0;
LcdInit();
M5.Lcd.print("Connecting to WiFi");
WiFi.mode(WIFI_STA);
WiFi.begin(wifi_ssid, wifi_password);
//WiFi接続確立するまで繰り返し
while (WiFi.status() != WL_CONNECTED && attempted_count < max_trial_count) {
M5.Lcd.print(".");
attempted_count++;
delay(1000);
}
LcdInit();
//WiFi接続確立
if (WiFi.status() == WL_CONNECTED)
{
//WiFi設定の表示
WiFiState = true;
M5.Lcd.println("WiFi connection is established");
M5.Lcd.println("Connected to:" + String(wifi_ssid));
M5.Lcd.print("IP:");
M5.Lcd.println(WiFi.localIP());
}
else
{
WiFiState = false;
M5.Lcd.print("WiFi connect failed");
}
delay(5000);
}
//起動時の設定
void setup() {
M5.begin();
//ディスプレイの初期化
LcdInit();
M5.Lcd.print("Get Google data process start..\n");
delay(1500);
//Wi-Fi設定
connectingWiFi();
//NTP設定
configTime(gmtOffset_sec, daylightOffset_sec, ntpServerDomain);
}
//ループ設定
void loop() {
LcdInit();
getLocalTime(&timeInfo);
sprintf(now, "%04d/%02d/%02d %02d:%02d:%02d",
timeInfo.tm_year + 1900,
timeInfo.tm_mon + 1,
timeInfo.tm_mday,
timeInfo.tm_hour,
timeInfo.tm_min,
timeInfo.tm_sec
);
M5.Lcd.print("Current time");
M5.Lcd.println("");
M5.Lcd.println(now);
getValues();
delay(60000);
}
尚、「String exec_url = "/macros/s/~省略~/exec"」の部分のURLは、Google App ScriptのWeb Appで公開しているURLを記載しています。
こちらはGoogle App Script側のソース。Google App ScriptのWeb Appの公開方法は別の記事にまとめます。(現在、準備中・・。)
function doGet() {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('data');
var lastRow = sheet.getLastRow();
var co2 = null;
var date_time = null;
var date_time_tmp = null;
var return_string = null;
var range = sheet.getRange(sheet.getLastRow(), 1, sheet.getLastRow(), 2);
date_time_tmp = range.getCell(1,1).getValue();
date_time = Utilities.formatDate(date_time_tmp, 'JST', 'yyyy/M/d HH:mm:ss')
co2 = range.getCell(1,2).getValue();
return_string = ""+ date_time +","+co2+"ppm";
var output = ContentService.createTextOutput(return_string);
return output;
}
Google App Scriptのソースは、こちらのGoogleスプレッドシートの一番下の値をレスポンスとして返す。最新のco2濃度の測定データを返却します。
6. M5Stackでの実施
以下のアイコンをクリックし実行しコンパイルする。
エラーなく実行できボードへの書き込みが完了したメッセージが出力され、Google App Script経由でGoogleスプレッドシートのデータを取得することができた。
取得のイメージは、Twitterの動画の通りです。
M5StackにてGoogle App Script経由で、Googleスプレッドシートのデータを取得を試みた。いくつか躓くポイントがあったが、無事実現できて嬉しい。HTTPS通信にWiFiClientSecureではなく、HTTPSRedirectを利用するのがポイントでした。 pic.twitter.com/3IJXVWtXmD
— kaname kun (@kanamekunkun) October 1, 2021
ディスプレイでの見栄えは、この次に改善しようと思います!フォントも改善したい!
手順は以上。
参考文献
本記事の作成に当たり、以下の情報を参考にさせて頂きました。
- Google Apps Script Document
- Arduinoからデバッグ用ログ出力
- GASにリクエストしたら「Moved Temporarily」が返ってくるときの対処法
- ESP8266からGoogle Apps Scriptを叩くのはちょっと面倒
- electronicsguy/HTTPSRedirect
- GASで作る貧乏IoT(1) ESP32からPOSTして、レスポンスを取得する。
- 電池駆動したESP8266(ESPr Developer)の3.3vピンの電圧をHTTPSRedirectを使って1分ごとにGoogle Spread Sheetに書き込んだ話
- M5StackからGoogleスプレッドシートに送信