Leafony ESP32 KitでGoogle Driveに画像をアップロードしてみた

# はじめに
LeafonyのESP32 Wi-Fi Kitを使って、画像をGoogle Driveにアップロードすることに成功したので、備忘録ついでに残します。

# Leafonyとは？
Leafony公式サイトより引用します(https://docs.leafony.com/docs/overview/)。
>Leafonyは、超小型、低消費電力、簡単に出来るオープンイノベーション・プラットフォームです。新しいITサービスやIoTのエッジノードなどの試作開発が、簡単に出来ます。トリリオンノード・エンジン・プロジェクト1が、このプラットフォームの研究開発を推進し、仕様などが確定したものを「Leafony」と呼びます。また、このLeafonyを製造・販売する会社をLEAFONY SYSTEMS社と言います。

個人的には、小型化されたArduinoであると感じています。
今回用いるのは、ESP32-WROOM-32が搭載されたKitを用います。これは、Wi-FiやBluetooth LEを用いることができ、IoTに最適なものになっています。このKitにはmicroSDを扱える基板(Leaf)も搭載されており、今回はこれも用います。

# アップロード方法
アップロード方法としては、Google Drive Apiを利用します。
具体的には、
1. Google APIでOAuth 2.0を設定
1. クライアントIDとクライアントシークレットからcodeを取得
2. 上3つの値からトークンを取得
2. トークンを用いてファイルをPOST

という手順でアップロードをします。
また、2.で取得したトークンは1時間を超えると有効期限が切れるので、同時に取得できるrefresh_tokenから再びトークンを取得するようにします。

# 1.Google APIでOAuth 2.0を設定
これは他のサイトを参考に設定しました。調べればいくつかサイトが出てくると思うので、ここでは省略します。(現在と一部画面が異なりますが、2.の参考サイトにも設定方法が載っています)ただ、一つ躓いた点として、OAuth同意画面の設定でテストユーザーを追加しないと、2.でコードが取得できなかったので、ADD USERSボタンをクリックして、Googleに登録されているメールアドレスを入力しました。
必要なコードとしては、__クライアントID__ と __クライアントシークレット__ となるので、この2つの値をメモってください。


# 2.クライアントIDからcodeを取得
次に、クライアントIDからcodeを取得します。方法としては、取得するためのリンクを作り、そこにブラウザからアクセスします。
リンクは次のとおりになります。「ここにクライアントIDを入力」をクライアントIDに置き換えてアクセスしてください。

https\://accounts.google.com/o/oauth2/auth?client_id=ここにクライアントIDを入力&response_type=code&redirect_uri=http\://localhost/&scope=https\://www.googleapis.com/auth/drive

アクセスすると、次のような画面が出てきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/718856/b7010d4f-7df8-394d-56de-60736c740b11.png)

ここで、1.で登録したメールアドレスをクリックします。すると、「このアプリはGoogleで確認されていません」と出るので、「続いて続行」をクリックします。すると、「(OAuth同意画面上の名前)がGoogleアカウントへのアクセスを求めています」と出るので、「Googleドライブのすべての…」のみになっていることを確認し、「続行」をクリックします。
上記のURLにおいて、redirect_uriをhttp\://localhost/としているので、ここでgoogle chromeなら「このサイトにアクセスできません」と出ます。しかし、リンクにはcodeが載っており、下記のようになっているはずです。

http\://localhost/?code=(codeの中身)&scope=http\://www.googleapis.com/auth/drive

このcodeをメモします。

参考：https://qiita.com/MypaceEngine/items/29940f509f755726e482 及び https://github.com/MypaceEngine/m5camera-arduino-googleDrive/blob/master/m5camera-arduino-googleDrive.ino

# 3. 上3つの値からトークンを取得
クライアントID、クライアントシークレット、codeの3つの値からtokenを取得します。方法としては、https\://oauth2.googleapis.com/tokenにPOSTリクエストを送信することで取得します。この際、POSTする方法は何でも良いのですが、ESP32上で取得するほうが、後々refresh_tokenを取得する際に使い回せるので、ESP32から取得します。

POSTにはWiFiClientSecureを用いました。POSTするコードを以下に記します。WiFiに接続した後にこのコードを実行します。(見やすさを重視したいため、拡張子をcとしていますが、実際にはinoです)

```getToken.c
#include <WiFiClientSecure.h>
WiFiClientSecure client;

String getToken() {
  const String client_id = "1.で取得したクライアントID";
  const String client_secret = "1.で取得したクライアントシークレット";
  const String code = "2.で取得したcode";

  //bodyの作成
  String body = "client_id=" + client_id + "&";
  body += "client_secret=" + client_secret + "&";
  body += "code=" + code + "&";
  body += "redirect_uri=http://localhost/&";
  body += "grant_type=authorization_code";

  //POSTリクエスト送信先に接続
  Serial.print("Connect to oauth2.googleapis.com");
  client.setInsecure();
  while(!client.connect("oauth2.googleapis.com", 443)) {
    delay(100);
    Serial.print(".");
  }
  Serial.println("");
  if(!client.connected()) {
    Serial.println("Failed");
    return "Failed to connect";
  }
  Serial.println("Success");

  //POSTリクエストの送信
  Serial.println(F("POST request Start"));
  //  header
  client.print("POST /token HTTP/1.1\r\n");
  client.print("Host: oauth2.googleapis.com\r\n");
  client.print("content-type: application/x-www-form-urlencoded\r\n");
  client.print("content-length: " + (String)body.length() + "\r\n");
  client.print("\r\n");
  //  body
  client.print(body);
  Serial.println(F("POST request End"));

  //jsonの受け取り
  char receivedData[2048];
  int i = 0;
  Serial.print(F("Receive Start"));
  while(i == 0) {
    while(client.available()) {
      receivedData[i] = client.read();
      //Serial.print(receivedData[i]);
      i++;
      if(i > 2047)
        break;
    }
  }
  client.stop();
  
  //json部分の抜き出し
  String json = (String)receivedData;
  json = json.substring( json.indexOf("{"), json.indexOf("}")+1 );
  return json;
}
```

参考：https://developers.google.com/identity/protocols/oauth2/web-server#httprest_1 及び https://github.com/MypaceEngine/m5camera-arduino-googleDrive/blob/master/m5camera-arduino-googleDrive.ino

このコードを実行すると、次のjsonが得られます。
```
{
  "access_token": "アクセストークン"
  ...
  "refresh_token": "リフレッシュトークン"
  ...
}
```
このうち、アクセストークンとリフレッシュトークンをメモしてください。

つまづいたところ：400 Bad Request　invalid_grantエラーが出る
これは、認証コードを取得し直して入力し直したらうまくいきました。古い認証コードを入れていたのですかね…

# 4. トークンを用いてファイルをPOST
これで、実際に画像ファイルをPOSTする準備が整いました。
microSDに入っている画像ファイルをアップロードします。

コードを以下に記します。なお、予めGoogle drive上にフォルダを作成し、その中に画像ファイルを入れるようにしてあります。フォルダのIDはGoogle drive上で開き、リンクの".../folders/"以降の部分になります。

```post.c
#include <SD.h>
#include <SPI.h>
#include <WiFiClientSecure.h>
WiFiClientSecure client;

String post() {
  const String boundary = "boundary_1234567890"; 
  const String folder_id = "入れたいフォルダのID";
  const String token = "アクセストークン";
  
  //画像ファイルの読み込み
  File file = SD.open("/test.jpg");
  String filename = file.name();

  //メタデータ作成
  String metadata = "{\r\n";
  metadata += "\"name\": \"" + filename.substring(1) + "\",\r\n"; //filenameに/が入るので取り除く
  metadata += "\"mimeType\": \"image/jpeg\",\r\n";
  metadata += "\"description\": \"Uploaded From ESP32\",\r\n";
  metadata += "\"parents\": [\"" + folder_id + "\"]\r\n";
  metadata += "}";

  //body作成
  //画像ファイルのサイズが大きく、メモリ上に展開できないので、
  //SD上にテキストファイルとして作成する
  File body = SD.open("/_body.txt", FILE_WRITE);
  if(!body){
    Serial.println(F("File cant open"));
    return "Failed to open _body.txt";
  }
  body.print("\r\n");
  body.print("--" + boundary + "\r\n");
  body.print("Content-Type: application/json; charset=UTF-8\r\n");
  body.print("\r\n");
  body.print(metadata + "\r\n");
  body.print("--" + boundary + "\r\n");
  body.print("Content-Type: image/jpeg\r\n");
  body.print("\r\n");
  while(file.available()){
    body.write(file.read());
  }
  body.print("\r\n");
  body.print("--" + boundary + "--\r\n");
  body.print("\r\n");

  body.close();
  file.close();

  //POSTリクエスト送信先に接続
  Serial.print("Connect to Google Drive");
  client.setInsecure();
  while(!client.connect("www.googleapis.com", 443)) {
    delay(100);
    Serial.print(".");
  }
  Serial.println("");
  if(!client.connected()){
    Serial.println("Failed");
    return "Failed to connect";
  }
  Serial.println("Success");

  //POSTリクエストの送信
  body = SD.open("/_body.txt");
  Serial.println(F("POST request Start"));
  //  header
  client.print("POST /upload/drive/v3/files?uploadType=multipart HTTP/1.1\r\n");
  client.print("Host: www.googleapis.com\r\n");
  client.print("authorization: Bearer " + token + "\r\n");
  client.print("content-type: multipart/form-data; boundary=" + boundary + "\r\n");
  client.print("content-length: " + (String)body.size() + "\r\n");
  client.print("\r\n");
  //  body
  while(body.available()) {
    client.write(body.read());
  }
  Serial.println(F("POST request End"));
  body.close();


  //結果の受信
  char receivedData[2048];
  int i = 0;
  Serial.print(F("Receive Start"));
  while(i == 0){
    delay(100);
    Serial.print(F("."));
    while(client.available()) {
      receivedData[i] = client.read();
      Serial.print(receivedData[i]);
      i++;
      if(i > 2047)
        break;
    } 
  }
  client.stop();

  //json部分の抜き出し
  String json = (String)receivedData;
  json = json.substring( json.indexOf("{"), json.indexOf("}")+1 );
  return json;
}
```
参考：https://developers.google.com/drive/api/guides/manage-uploads#http 及び https://github.com/MypaceEngine/m5camera-arduino-googleDrive/blob/master/m5camera-arduino-googleDrive.ino

やっていることとしては、
1. 画像の読み出し
1. metadata作成
1. body作成
1. www\.googleapis.comに接続
1. POSTリクエスト送信

となります。
microSDのルートディレクトリ(/)にtest.jpgを置き、このコードを実行したところ、正常にGoogle driveにアップロードされました。

# 感想
実際に動かしてみると、特に画像ファイルをbodyに入れてPOSTする過程で時間がかかってしまいました。これは、CPUやメモリがやはりマイコンでは貧弱な部分があると思われます。今回用意した画像は解像度320*180で、容量は45.7KBですが、それでも数十秒はかかっていました。より解像度の高く、容量の多いものではなおさら時間がかかってしまい、リアルタイムには圧倒的に不向きなものになると思われます。まあESP32でやろうとすること自体が間違っていると言われればそうかもしれませんが…
ただ、非常に小型な基板でここまで出来るのであれば上出来なほうだと思います。個人的にLeafonyには可能性を秘めていると感じます。
ここまで見てくださりありがとうございました。

# EX.リフレッシュトークンを用いてアクセストークンの再取得
アクセストークンは一定時間（60分）を超えると使えなくなります。従いまして、アクセストークンを再取得しなければならないのですが、その際にはリフレッシュトークンを用います。具体的には、3.の手順の容量で、oauth2.googleapis.com/tokenにリフレッシュトークンを含むデータをPOSTします。bodyの中身を変えるだけですので、詳細なコードは省略します。参考サイトが大いに参考になると思います。
参考：https://qiita.com/chenglin/items/f2382898a8cf85bec8dd

# 余談：自分への反省
しっかりclient.close()やFile.close()をして解放する意識を持ちましょう。