NefryBTからGoogleDriveにデータをアップロードする方法
こんにちは、NefryBTきっかけでIoTの世界に入った黒田と言います。Webとどう接していけばいいのか分からないときにNefryBTと出会い、意外と難しくない(何とかなる)ことを知りました。
わみさん、ありがとうございます。
今回は、NefryBTからGoogleDriveにデータをアップロード出来たので紹介したいと思います。
おそらくESP32マイコン搭載モジュールであれば、同じ考えで出来ると思います。
アップロードまでの手順
- リフレッシュトークンなど必要な情報を取得する(最初の一度だけ)
- リフレッシュトークンから認証用のアクセストークンを取得する
- http POSTリクエストでデータをアップロードする
はい、とてもシンプルです。
httpを全く分からない私としてはここまでたどり着くまでかなり苦労しました・・・先人の方々が様々な実績を残してくれているのを本当に感謝しています。)
もう少し詳しく説明します。
Google Drive REST APIのサイトを読みますと、まずGoogleDriveへアクセスするには認証用のアクセストークンが必要なこと、データをアップロードするときのPOSTリクエストの書き方が決まっていることが分かります。
そのためアクセストークンで認証を行いつつPOSTリクエストを投げれば、NefryBTからでもデータをアップロード出来ます。
ここで一つトラップがあります。 アクセストークンの有効期限は3600秒(1時間)です。当初これを知らなかったので、時間が経つとなぜか認証エラーになる問題に悩まされました。
回避策としてリフレッシュトークンからアクセストークンを取得するようにします。すると、アクセストークンが新しくなるので、認証もばっちり通ります。 つまり、アクセストークンよりも再発行用のリフレッシュトークンが欲しいのです。
ではでは、具体的な手順を説明していきたいと思います。
[手順1] リフレッシュトークンなど必要な情報を取得する(最初の一度だけ)
今回はNode.jsで取得します。
公式サイトのNode.js Quickstartに従って進めていきます。
公式サイトにある「ENABLE THE DRIVE API」を押します。
するとOAuth2.0クライアントIDを作ってくれます。 続けて「DOWNLOAD CLIENT CONFIGURATION」を押してcredentials.jsonを取得します。
(補足) 例えばcredentials.jsonを削除してしまって、もう一度取得したい場合・・・ Google Cloud PlatformのAPI認証情報からダウンロードできます。
左上のメニューを押して「APIとサービス」を押します。
認証情報の中に先ほど作成したクライアントIDがありますので、ダウンロードします。
※ファイル名は「clientsecret***.json」となっていますので「credentials.json」に変更します。
次にNode.jsでアクセストークンなどの情報を取得します。(Node.jsやnpmが動く環境は用意されているものとします。)
アクセストークンを取得するフォルダを用意して以下のモジュールをインストールします。
npm install googleapis@27 --save
credentials.jsonも同じフォルダに移動させます。
index.jsを作成します。公式サイトのままではスコープがReadOnlyになっているのでアップロードできるスコープに変更します。それ以外は全く同じです。
[変更前] const SCOPES = ['https://www.googleapis.com/auth/drive.metadata.readonly']; [変更後] const SCOPES = ['https://www.googleapis.com/auth/drive'];
スコープについて詳しくはAbout Authorizationを参照ください。
- 準備は整ったのでNode.jsを実行します。
1. credentials.json、index.jsなど一式入っているフォルダです。
2. nodeを実行します。
3. URLが表示されるのでアクセスします。
4. 紐づけるGoogleアカウントを選択し、GoogleDriveへのアクセスを許可します。
5. 最後に表示されるコードを上記3.の続きに貼り付けます。
6.無事token.jsonを取得できました。
[手順2] リフレッシュトークンから認証用のアクセストークンを取得する
アップロードに必要な情報は揃ったので、あとはNefryBTだけ触っていきます。
リフレッシュトークンからアクセストークンを取得するhttp POSTリクエストは次の通りとなります。
[ヘッダー]
POST /oauth2/v4/token HTTP/1.1
Host: www.googleapis.com:443
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: [POSTするデータの長さ]
[POSTするデータ]
refresh_token=[リフレッシュトークン]&client_id=[クライアントID]&client_secret=[クライアントシークレット]&grant_type=refresh_token
- クライアントIDとクライアントシークレットは「credentials.json」に書いてあります。もしくはGoogle Cloud PlatformのAPIの認証情報でも確認できます。
- リフレッシュトークンは「token.json」に書いてあります。
POSTリクエストを投げるとレスポンスがjson形式で返ってきます。
{
"access_token": "***",
"expires_in": 3600,
"scope": "https://www.googleapis.com/auth/drive",
"token_type": "Bearer"
}
access_token:***が取得したいアクセストークンになります。
上記の内容をNefryBTで書くと次のようになります。
※プログラムソースのうち主要な部分を抜粋しています。
リフレッシュトークン・クライアントID・クライアントシークレットは個人情報なので、直接プログラムソースに記載するのではなくNefryBTのDataStoreに登録します。
POSTするデータを設定します。
String postData = ""; postData += "refresh_token=" + refresh_token; postData += "&client_id=" + client_id; postData += "&client_secret=" + client_secret; postData += "&grant_type=" + String("refresh_token");
ヘッダーを設定します。 データの長さはPOSTするデータ(postData)から算出します。
String postHeader = ""; postHeader += ("POST " + token_uri + " HTTP/1.1\r\n"); postHeader += ("Host: " + String(host) + ":" + String(httpsPort) + "\r\n"); postHeader += ("Connection: close\r\n"); postHeader += ("Content-Type: application/x-www-form-urlencoded\r\n"); postHeader += ("Content-Length: "); postHeader += (postData.length()); postHeader += ("\r\n\r\n");
POSTリクエストを投げてレスポンスを取得します。更にレスポンスからアクセストークンを取得します。
String result = postRequest(host, postHeader, postData); //取得したjsonデータからAccessTokenを取得する const int BUFFER_SIZE = JSON_OBJECT_SIZE(4) + JSON_ARRAY_SIZE(1); StaticJsonBuffer<BUFFER_SIZE> jsonBuffer; char json[result.length() + 1]; result.toCharArray(json, sizeof(json)); JsonObject& root = jsonBuffer.parseObject(json); const char* tmp = root["access_token"]; token = tmp; return token; }
POSTリクエストを行っているソース(
postRequest
)です。正常に処理が終わればレスポンスを返します。String postRequest(const char* server, String header, String data) { String result = ""; // Use WiFiClientSecure class to create TLS connection WiFiClientSecure client; Serial.print("Connecting to: "); Serial.println(server); if (!client.connect(server, httpsPort)) { Serial.println("connection failed"); return result; } Serial.println("certificate matches"); Serial.print("post: "); Serial.println(header + data); client.print(header + data); Serial.println("Receiving response"); if (client.connected()) { if (client.find("HTTP/1.1 ")) { String status_code = client.readStringUntil('\r'); Serial.print("Status code: "); Serial.println(status_code); if (status_code != "200 OK") { Serial.println("There was an error"); } } if (client.find("\r\n\r\n")) { Serial.println(F("[Read Data]")); } String line = client.readStringUntil('\r'); Serial.println(line); result += line; } Serial.println("closing connection"); return result; }
だいたいこんな感じで無事アクセストークンを取得できました。
[手順3] http POSTリクエストでデータをアップロードする
いよいよデータをアップロードしてみます。今回はテキストファイルとJpegファイルのアップロード方法を紹介します。 公式サイトのUploading Filesによりますと、アップロード方法は3パターンあります。
- Simple upload
- Multipart upload
- Resumable upload
“Simple upload”の場合、アップロードしたファイル名は全て”untitled”となってしまい、具合がよろしくありません。 “Multipart upload”の場合、ファイルに様々な設定を付加できます。今回はファイル名・親フォルダの指定・コメントを設定しました。 (“Resumable upload”はどんなものか調べていません・・・)
Multipart uploadのPOSTリクエストは次の通りです。
POST https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart HTTP/1.1
Authorization: Bearer [YOUR_AUTH_TOKEN]
Content-Type: multipart/related; boundary=foo_bar_baz
Content-Length: [NUMBER_OF_BYTES_IN_ENTIRE_REQUEST_BODY]
--foo_bar_baz
Content-Type: application/json; charset=UTF-8
{
"name": "myObject"
}
--foo_bar_baz
Content-Type: image/jpeg
[JPEG_DATA]
--foo_bar_baz--
- [YOUR_AUTH_TOKEN]はアクセストークンを設定します。
- [NUMBER_OF_BYTES_IN_ENTIRE_REQUEST_BODY]は ヘッダーから最後のデータまですべてのサイズ となります。つまり最初のPOSTから最後の–foo_bar_baz–まで全てのサイズを設定します。これを間違えると正しくPOSTリクエストを投げることが出来ません。
"name": "myObject"
の部分がアップロードするファイルの設定を記載する部分となります。 何を設定できるかは公式ページの”Files”を参照ください。- その下の
Content-Type: image/jpeg
がアップロードするデータの形式となり、その次にデータの中身を記述していきます。 - 最後に
--foo_bar_baz--
で閉めます。
まとめますと、プログラムを書くとき毎回設定しなければならないのは次の4つです。
- アクセストークン
- POSTリクエスト全体のサイズ
- ファイルの設定(ファイル名・親フォルダ・コメント)
- ファイルの中身
テキストファイルをアップロードする
上記の仕様を踏まえてテキストファイルをアップロードする例を挙げます。この次に紹介するJpegファイルのアップロードと比べるとシンプルで分かりやすいと思います。
- POSTリクエストのファイルの指定周り(
--foo_bar_baz
部分)を設定します。 - 上記1.のあとPOSTリクエスト全体のサイズが分かるのでヘッダー(
POST https://
部分)を設定します。 - 上記1.と2.をPOSTします。
- データの中身をPOSTします。
最後の’–foo_bar_baz–‘をPOSTします。
void postDrive_Text(String _fileName, String _textData, String _comment) { uint8_t DataSize = _textData.length(); uint8_t postData[DataSize]; for (int i = 0; i < DataSize; i++) { postData[i] = (uint8_t)_textData[i]; } 1. POSTリクエストのファイルの指定周りを設定 String start_request = getStartRequest_Text(_fileName, _comment); 2. ヘッダーを設定 uint16_t full_length; full_length = start_request.length() + DataSize + end_request.length(); String postHeader = getPostHeader(full_length); String result = ""; // Use WiFiClientSecure class to create TLS connection WiFiClientSecure client; if (!client.connect(host, httpsPort)) { Serial.println("connection failed"); return ; } 3. 1.と2.をPOST client.print(postHeader + start_request); 4. データの中身をPOST client.write(&postData[0] , DataSize); 5. --foo_bar_baz--をPOST client.println(end_request); if (client.connected()) { if (client.find("HTTP/1.1 ")) { String status_code = client.readStringUntil('\r'); Serial.print("Status code: "); Serial.println(status_code); if (status_code != "200 OK") { Serial.println("There was an error"); } } if (client.find("\r\n\r\n")) { Serial.println(F("[Read Data]")); } else { Serial.println(F("[WARNING] Response Data is Nothing")); } String line = client.readStringUntil('\r'); Serial.println(line); result += line; } }
実際にテキストファイルをアップロードしている様子
1. アップロード先のフォルダを作成しておきます。親フォルダIDは「https://drive.google.com/drive/folders/***
」の***の部分です。
2. NefryBTのセットアップが完了するのを待ちます。
3. セットアップが完了したらNefryBTについているボタンを押します。
4. 成功しました!
5. GoogleDriveを見てみるとしっかりとアップロードされています!
Jpegファイルをアップロードする
続いてJpegファイルをアップロードする方法です。ここからかなり込み入った内容になります。 今回JpegファイルにしてアップロードしたものはNefryBTに接続したカメラモジュールで撮った画像となります。そのためカメラモジュールの話とGoogleDriveへアップロードする話が混在しますのでご了承ください・・・
カメラモジュールについて
使用したカメラモジュールはArducam Miniモジュール(2メガピクセル)です。ESP32向けのサンプルがあったので動かせるだろうと思って選びました。 (NefryBTのことを考えればGroveのカメラでも良かったとあとで気づきました・・・)
初期設定やカメラモジュールからデータを取得するところはプログラムソースを参照ください。
厄介なのは取得したデータ全てがJpegデータではないことです。このカメラモジュールの場合、1バイト目に0x00が入っています。これ、要らないデータなのです。きちんとJpegファイルにあたるデータをアップロードしないと正しく表示されません。
Jpegファイルは開始位置(0xFFD8)と終了位置(0xFFD9)が決まっています。この範囲のデータだけをアップロードすればJpegファイルとして正しく表示されます。
プログラムのポイント
カメラモジュールの仕様を踏まえてアップロードします。
- Jpegファイルのデータサイズは何十万バイトと膨大なので全て変数に入れて一括でPOSTすることはできません。そのためデータを分割してカメラモジュールから取得、POSTを繰り返します。
- POSTリクエスト全体のサイズとJpegファイルのサイズが異なるので、POSTリクエスト全体のサイズに足りない分を最後に0x00をPOSTすることで整合性を合わせています。
プログラムソースを抜粋します。
// ReadSizeはカメラモジュールで取得したデータサイズ
//GoogleDriveへポスト
String start_request = api.getStartRequest_Jpeg("Capture", "From ArduCam");
String end_request = api.getEndRequest();
uint32_t full_length;
full_length = start_request.length() + ReadSize + end_request.length();
String postHeader = api.getPostHeader(full_length);
(中略)
client.print(postHeader + start_request);
//JPEGデータ
static const size_t bufferSize = 2048;
static uint8_t buffer[bufferSize] = {0xFF};
uint32_t index = 0;
uint32_t sizeCnt = 0;
uint8_t now = 0;
uint8_t prev = 0;
myCAM.CS_LOW();
myCAM.set_fifo_burst();
カメラモジュールのデータをPOSTしている部分
bool isHeader = false;
while (ReadSize--) {
prev = now;
now = SPI.transfer(0x00);
//ヘッダーを探す(0xFF,0xD8)
if (!isHeader) {
if (prev == 0xFF && now == 0xD8) {
Serial.println(F("JPEG First Data is Found"));
buffer[0] = 0xFF;
buffer[1] = 0xD8;
index = 2;
sizeCnt = 2;
isHeader = true;
}
continue;
}
//ヘッダーが見つかったあと
sizeCnt++;
// JPEGファイルの最後を検出したら終了(0xFF,0xD9)
if (prev == 0xFF && now == 0xD9) {
Serial.println(F("JPEG Last Data is Found"));
buffer[index++] = now;
client.write(&buffer[0], index);
myCAM.CS_HIGH();
break;
}
if (index < bufferSize) {
buffer[index] = now;
index++;
} else {
if (!client.connected()) break;
client.write(&buffer[0], bufferSize);
index = 0;
buffer[index++] = now;
}
}
myCAM.CS_HIGH();
ReadSize += 1;
Serial.print(F("JPEG Data Size: ")); Serial.println(sizeCnt);
Serial.print(F("Remaining Data Size: ")); Serial.println(ReadSize);
client.println(end_request);
POSTリクエスト全体のサイズを調整している部分
//バッファーメモリサイズと画像サイズが異なるため、full_lengthに達していない。
//足りない分の帳尻を合わせる
uint8_t tmpbuf[ReadSize] = {0x00};
client.write(&tmpbuf[0], ReadSize);
(以下、省略)
}
実際にJpegファイルをアップロードしている様子
1. アップロード先のフォルダはテキストファイルと同じところにします。
2. カメラで撮ってみます。
3. NefryBTのセットアップが完了するのを待ちます。
4. セットアップが完了したらNefryBTについているボタンを押します。
5. 成功しました!
6. GoogleDriveを見てみるとしっかりとアップロードされています!
参考にしたサイト
大変助かりました。
*Google APIを使用するためにGoogle OAuth認証をしようよ
*時間が立つとGoogle APIのOAuth認証に失敗する
まとめ
NefryBTから直接GoogleDriveにアクセスできたときは感動しました!こんな小さなボードがネットに繋がるなんて!
またGmailやスプレッドシートなどの他のアプリケーションにもアクセスできると思います。面白い組み合わせがあるかもしれません。
みなさんのIoTライフの参考になれば幸いです。ではでは。