Arduino HTTPClientでファイルのバイナリ送信

# 1. はじめに

ESP32のプログラムをArudino-IDEで開発するのはとても楽です．しかし，一つ踏み込むと出来ない事も出てきます．例えば，バイナリファイルをhttp postで送信というのも，arduino-IDE上では簡単でない事の1つです．
選択肢としてはいくつかありますが，思い着くのは次ではないでしょうか？
- Arduino-IDEではなくESP-IDFで開発にする
- WiFiClientで書く
- HTTPClientを書き換える

ESP32でhttp postする方法を検索すると，上2つが多いです．
私としては最後のHTTPClientライブラリ書き換えがよさそうな選択肢だと思っているのですが，汎用性が無くなるので，現時点では諦めます．その代わり，HTTPClientで出来ないかを模索してみました．

利用したバージョン (古いライブラリでは動かないことがあります）
Arduino-IDE : ver 1.8.13
Arduino core for the ESP32 : ver 1.0.4 (2019.10月版)

# 2. ESP32からのファイル送信

ESP32からサーバ側にバイナリファイルを送信する手段は色々ありそうです．例を挙げます．
- ftp (もしくは sftp)
- メール添付 (SMTP)
- http post
- UDPで独自実装
- TCPで独自実装

独自実装すれば何でもいけそうですが，とても大変だろうと思います．ftpはシンプルで良いかもしれません．メール添付はファイルが蓄積されていく点でメリットがありそうです．ただ，数秒間隔など短い間隔で投げるには，ftpやメールは，少々重いように感じます．
そこで候補に挙がったのが，http postでした．

# 3. http postとは

http通信にはgetやpostといったメソッドがあり，ファイルを送信するには，http postが都合良いです．

詳細は割愛します．
- httpのメソッドを知りたい方は，[http メソッド](https://www.google.com/search?q=http+メソッド) で検索してみてください．
- httpとは何かを知りたい方は，[http 通信](https://www.google.com/search?q=http+通信) で検索してみてください．
- プロトコルって何かを知りたい方は，[プロトコル IP](https://www.google.com/search?q=プロトコル+IP) で検索してみてください．

サーバ側がhttp postで受信するメリットは以下があると思います．
- 受信をトリガにした処理を作るのが容易
- 任意のファイルを受け付けることができる
- Arduinoに限らず，様々な言語でライブラリが存在するので検証が容易


# 4. Arduino用HTTPClientライブラリについて

ESP32用ArduinoライブラリとしてHTTPClientがあり，postメソッドがあります．

```c++
#include <HTTPClient.h>
HTTPClient client;

uint8_t payload={ FILE CONTENT };
size_t size=sizeof(payload);
 :
client.post(payload, size);
 :
```

これだけ見ると，payloadにバイナリを入れれば完成に見えます．しかし，それでは正しく送信されません．それは，このpostメソッドに，マルチパート化する処理が実装されてないからです．

# 5. マルチパート

## 5.1. httpにおけるマルチパート
マルチパートというと，ファイル添付したメールを連想する方が多いと思います．区切りの文字列で分離できる形で，上が本文，下がファイルです．
![mail_multipart.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/698531/2175dc42-8310-e4a5-640b-d83daf585487.png)


http postでも同じで，ファイルはマルチパート化して送信する必要があります．
実際にhttp postでファイルを送信しているパケットをキャプチャすると，マルチパートであることが判ります．

![post_capt_packet.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/698531/3ec87ec3-2acf-3cfb-c60b-5d3b5c1fd763.png)


この部分を保存してテキストエディタで開くとこんな感じです．区切り文字をハイライトしています．
![post_capt.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/698531/4bdd6419-f23f-1c98-3c4e-2d2014624368.png)


メールの場合，ファイル内容はbase64等でエンコードしますが，http postの場合，バイナリのままでOKです．

## 5.2. マルチパートの作り方

http postでバイナリを送信しているパケットを参考にします．
見ると，次の文字列があることに気付きます．
```
区切り文字(改行)
Content-Disposition: form-data; name="uploadFile"; filename="FILENAME.png"(改行)
Content-Type: image/png(改行)
(改行)
```
この次に，ファイルのバイナリが入ってます．バイナリが終わった後は次の形に文字列があります．
```
(改行)
区切り文字(改行)
(改行)
```

## 5.3. postを行うまでの手順

http postで中身を送る前に，手続を踏む必要があります．パケットキャプチャした内容を見ると，postを実行する前に通信が一往復しています．
![post_preamble.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/698531/93baffff-14b9-f485-7999-60044402c8b1.png)


WiFiClientで実装する場合は，この手続をコーディングする必要があります．
しかし，HTTPClientは，このための処理が実装済みなので，特別に実装する必要はありません．
（ここがHTTPClientを使うメリット）

ただし，一つヘッダに必要な文字列があります．
```
Content-Type: multipart/form-data; boundary=区切り文字
```
この文字列を入れるには次のように記述すると良いです．

```c++
client.addHeader("Content-Type","multipart/form-data; boundary=区切り文字")
```

## 5.4. 対向サーバ

サーバ側は，node-redでhttp inとhttp responseのノードだけで十分です． メッセージを出力したい場合はtemplateノードを入れます．
![node-red_nodes.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/698531/78f5b091-d9a8-bb9a-9c8e-08f13dc19aac.png)

http inの設定は，メソッドをpostにして，ファイルのアップロードにチェックを入れます．
![httpinnode.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/698531/1bcff4b1-c589-f634-8902-0358bf9cfd47.png)


# 6. 最終的なコード

WiFiの環境に合わせてSSIDやPASSPHRASEを入れ，サーバのアドレスを入れます．URL2で,node-redのポート番号(1880)を付け，postを受け付けるURLを構成しています．

アップロードする画像は
![a.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/698531/c6cf584a-1ea2-3173-82a7-1335bfbffc2f.png)
です．
画像ファイルをCSV化し，ソースに埋め込んでいます．

```c++
#include <WiFi.h>
#include <HTTPClient.h>

HTTPClient myHttp;

char ssidC="SSID";
char passC="PASSPHRESE"; 
char servC="192.168.1.2";//server address

#define URL1 "http://"
#define URL2 ":1880/posttst"

#define STRING_BOUNDARY "123456789000000000000987654321"
#define STRING_MULTIHEAD02 "Content-Disposition: form-data; name=\"uploadFile\"; filename=\"./a.png\""
#define STRING_MULTIHEAD03 "Content-Type: image/png"

uint8_t postData[] = {137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 80, 0, 0, 0, 80, 8, 2, 0, 0, 0, 1, 115, 101, 250, 0, 0, 0, 1, 115, 82, 71, 66, 0, 174, 206, 28, 233, 0, 0, 0, 4, 103, 65, 77, 65, 0, 0, 177, 143, 11, 252, 97, 5, 0, 0, 0, 9, 112, 72, 89, 115, 0, 0, 14, 195, 0, 0, 14, 195, 1, 199, 111, 168, 100, 0, 0, 0, 111, 73, 68, 65, 84, 120, 94, 237, 207, 1, 13, 0, 32, 12, 192, 48, 252, 43, 187, 157, 43, 0, 31, 163, 73, 13, 244, 220, 157, 175, 8, 215, 9, 215, 9, 215, 9, 215, 9, 215, 9, 215, 9, 215, 9, 215, 9, 215, 9, 215, 9, 215, 9, 215, 9, 215, 9, 215, 9, 215, 9, 215, 9, 215, 9, 215, 9, 215, 9, 215, 9, 215, 9, 215, 9, 215, 9, 215, 9, 215, 9, 215, 9, 215, 9, 215, 9, 215, 9, 215, 9, 215, 9, 215, 9, 215, 9, 215, 9, 215, 9, 215, 9, 215, 9, 215, 9, 215, 125, 22, 222, 121, 107, 34, 251, 238, 159, 135, 120, 202, 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130};

int32_t httppost( uint8_t * ui8Buf, uint32_t iNumDat ){
  String stMyURL="";
  stMyURL+=URL1;
  stMyURL+=String(servC);
  stMyURL+=URL2;
  myHttp.begin(stMyURL);
    
  String stConType ="";
  stConType +="multipart/form-data; boundary=";
  stConType +=STRING_BOUNDARY;
  myHttp.addHeader("Content-Type", stConType);

  String stMHead="";
  stMHead += "--";
  stMHead += STRING_BOUNDARY;
  stMHead += "\r\n";
  stMHead += STRING_MULTIHEAD02;
  stMHead += "\r\n";
  stMHead += STRING_MULTIHEAD03;
  stMHead += "\r\n";
  stMHead += "\r\n";
  uint32_t iNumMHead = stMHead.length();

  String stMTail="";
  stMTail += "\r\n";
  stMTail += "--";
  stMTail += STRING_BOUNDARY;
  stMTail += "--";
  stMTail += "\r\n";
  stMTail += "\r\n";  
  uint32_t iNumMTail = stMTail.length();

  uint32_t iNumTotalLen = iNumMHead + iNumMTail + iNumDat;

  uint8_t *uiB = (uint8_t *)malloc(sizeof(uint8_t)*iNumTotalLen);

  for(int uilp=0;uilp<iNumMHead;uilp++){
    uiB[0+uilp]=stMHead[uilp];
  }
  for(int uilp=0;uilp<iNumDat;uilp++){
    uiB[iNumMHead+uilp]=ui8BufJpg[uilp];
  }
  for(int uilp=0;uilp<iNumMTail;uilp++){
    uiB[iNumMHead+iNumDat+uilp]=stMTail[uilp];
  }

  int32_t httpResponseCode = (int32_t)myHttp.POST(uiB,iNumTotalLen);
  myHttp.end();
  free(uiB);
  return (httpResponseCode);
}

void initWifiClient(void){
  Serial.print("Connecting to ");
  Serial.println(ssidC);

  WiFi.begin( ssidC, passC);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("WiFi connected.");
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());
}

void setup() {
  Serial.begin(115200);
  initWifiClient();
  myHttp.setReuse(true);
  int iRetHttp = httppost( postData , sizeof(postData));
}

void loop() {}
```

# 付録
http postを実験したpythonスクリプト

```python
import requests

fnameL='a.png'
MIMETYPE = 'image/png'
url = 'http://127.0.0.1:1880/post200915'

fcontent = open(fnameL, 'rb').read()
messWithFile = {'uploadFile': (fnameL, fcontent, MIMETYPE)}
retCode = requests.post(url, files=messWithFile,timeout=10.1)
print(retCode.status_code)
print(retCode.text)
```