arrow-righthamburgerlogo-marksocial-facebooksocial-githubsocial-twitter
2019.01.31

NefryBTからGoogleDriveにデータをアップロードする方法

黒田

Nefry BT
   このエントリーをはてなブックマークに追加  

こんにちは、NefryBTきっかけでIoTの世界に入った黒田と言います。Webとどう接していけばいいのか分からないときにNefryBTと出会い、意外と難しくない(何とかなる)ことを知りました。

わみさん、ありがとうございます。

今回は、NefryBTからGoogleDriveにデータをアップロード出来たので紹介したいと思います。

おそらくESP32マイコン搭載モジュールであれば、同じ考えで出来ると思います。

アップロードまでの手順

  1. リフレッシュトークンなど必要な情報を取得する(最初の一度だけ)
  2. リフレッシュトークンから認証用のアクセストークンを取得する
  3. http POSTリクエストでデータをアップロードする

はい、とてもシンプルです。

httpを全く分からない私としてはここまでたどり着くまでかなり苦労しました・・・先人の方々が様々な実績を残してくれているのを本当に感謝しています。)

もう少し詳しく説明します。

Google Drive REST APIのサイトを読みますと、まずGoogleDriveへアクセスするには認証用のアクセストークンが必要なこと、データをアップロードするときのPOSTリクエストの書き方が決まっていることが分かります。

そのためアクセストークンで認証を行いつつPOSTリクエストを投げれば、NefryBTからでもデータをアップロード出来ます。

ここで一つトラップがあります。 アクセストークンの有効期限は3600秒(1時間)です。当初これを知らなかったので、時間が経つとなぜか認証エラーになる問題に悩まされました。

回避策としてリフレッシュトークンからアクセストークンを取得するようにします。すると、アクセストークンが新しくなるので、認証もばっちり通ります。 つまり、アクセストークンよりも再発行用のリフレッシュトークンが欲しいのです。

ではでは、具体的な手順を説明していきたいと思います。

[手順1] リフレッシュトークンなど必要な情報を取得する(最初の一度だけ)

今回はNode.jsで取得します。

公式サイトのNode.js Quickstartに従って進めていきます。

公式サイトにある「ENABLE THE DRIVE API」を押します。

OAuth0.PNG

するとOAuth2.0クライアントIDを作ってくれます。 続けて「DOWNLOAD CLIENT CONFIGURATION」を押してcredentials.jsonを取得します。

OAuth1.PNG


(補足) 例えばcredentials.jsonを削除してしまって、もう一度取得したい場合・・・ Google Cloud PlatformのAPI認証情報からダウンロードできます。

左上のメニューを押して「APIとサービス」を押します。

OAuth2-1.PNG

認証情報の中に先ほど作成したクライアントIDがありますので、ダウンロードします。

OAuth2-2.PNG

※ファイル名は「clientsecret***.json」となっていますので「credentials.json」に変更します。


次にNode.jsでアクセストークンなどの情報を取得します。(Node.jsやnpmが動く環境は用意されているものとします。)

npm install [email protected] --save
[変更前]
const SCOPES = ['https://www.googleapis.com/auth/drive.metadata.readonly'];
[変更後]
const SCOPES = ['https://www.googleapis.com/auth/drive'];

スコープについて詳しくはAbout Authorizationを参照ください。

1. credentials.json、index.jsなど一式入っているフォルダです。

AccessToken_0.PNG

2. nodeを実行します。

AccessToken_1.PNG

3. URLが表示されるのでアクセスします。

AccessToken_2.PNG

4. 紐づけるGoogleアカウントを選択し、GoogleDriveへのアクセスを許可します。

AccessToken_3.PNG

AccessToken_4.PNG

5. 最後に表示されるコードを上記3.の続きに貼り付けます。 AccessToken_5.PNG

AccessToken_6.PNG

6.無事token.jsonを取得できました。

AccessToken_7.PNG

[手順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

POSTリクエストを投げるとレスポンスがjson形式で返ってきます。

{
  "access_token": "***",
  "expires_in": 3600,
  "scope": "https://www.googleapis.com/auth/drive",
  "token_type": "Bearer"
}

access_token:***が取得したいアクセストークンになります。


上記の内容をNefryBTで書くと次のようになります。

プログラムソースのうち主要な部分を抜粋しています。

      String postData = "";
      postData += "refresh_token=" + refresh_token;
      postData += "&client_id=" + client_id;
      postData += "&client_secret=" + client_secret;
      postData += "&grant_type=" + String("refresh_token");
      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");
      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;
    }
    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”の場合、アップロードしたファイル名は全て”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--

まとめますと、プログラムを書くとき毎回設定しなければならないのは次の4つです。

テキストファイルをアップロードする

上記の仕様を踏まえてテキストファイルをアップロードする例を挙げます。この次に紹介するJpegファイルのアップロードと比べるとシンプルで分かりやすいと思います。

  1. POSTリクエストのファイルの指定周り(--foo_bar_baz部分)を設定します。
  2. 上記1.のあとPOSTリクエスト全体のサイズが分かるのでヘッダー(POST https://部分)を設定します。
  3. 上記1.と2.をPOSTします。
  4. データの中身をPOSTします。
  5. 最後の’–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/***」の***の部分です。

GoogleDrive_Text_0.PNG

2. NefryBTのセットアップが完了するのを待ちます。

DSC_0003.JPG

DSC_0005.JPG

3. セットアップが完了したらNefryBTについているボタンを押します。

DSC_0006.JPG

4. 成功しました!

DSC_0007.JPG

5. GoogleDriveを見てみるとしっかりとアップロードされています!

GoogleDrive_Text_1.PNG

GoogleDrive_Text_2.PNG

Jpegファイルをアップロードする

続いてJpegファイルをアップロードする方法です。ここからかなり込み入った内容になります。 今回JpegファイルにしてアップロードしたものはNefryBTに接続したカメラモジュールで撮った画像となります。そのためカメラモジュールの話とGoogleDriveへアップロードする話が混在しますのでご了承ください・・・

カメラモジュールについて

ArduCAM.jpg

使用したカメラモジュールはArducam Miniモジュール(2メガピクセル)です。ESP32向けのサンプルがあったので動かせるだろうと思って選びました。 (NefryBTのことを考えればGroveのカメラでも良かったとあとで気づきました・・・)

初期設定やカメラモジュールからデータを取得するところはプログラムソースを参照ください。

厄介なのは取得したデータ全てがJpegデータではないことです。このカメラモジュールの場合、1バイト目に0x00が入っています。これ、要らないデータなのです。きちんとJpegファイルにあたるデータをアップロードしないと正しく表示されません。

Jpegファイルは開始位置(0xFFD8)と終了位置(0xFFD9)が決まっています。この範囲のデータだけをアップロードすればJpegファイルとして正しく表示されます。

プログラムのポイント

カメラモジュールの仕様を踏まえてアップロードします。

プログラムソースを抜粋します。

// 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. アップロード先のフォルダはテキストファイルと同じところにします。

GoogleDrive_Jpeg_0.PNG

2. カメラで撮ってみます。

DSC_0015.JPG

3. NefryBTのセットアップが完了するのを待ちます。

DSC_0010.JPG DSC_0012.JPG

4. セットアップが完了したらNefryBTについているボタンを押します。

DSC_0013.JPG

5. 成功しました!

DSC_0014.JPG

6. GoogleDriveを見てみるとしっかりとアップロードされています!

GoogleDrive_Jpeg_1.PNG

GoogleDrive_Jpeg_2.PNG

参考にしたサイト

大変助かりました。

*jalmeroth/ESP8266-OAUTH2

*Google APIを使用するためにGoogle OAuth認証をしようよ

*時間が立つとGoogle APIのOAuth認証に失敗する

*ESP-SensorCam

まとめ

NefryBTから直接GoogleDriveにアクセスできたときは感動しました!こんな小さなボードがネットに繋がるなんて!

またGmailやスプレッドシートなどの他のアプリケーションにもアクセスできると思います。面白い組み合わせがあるかもしれません。

みなさんのIoTライフの参考になれば幸いです。ではでは。

   このエントリーをはてなブックマークに追加