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 メソッド で検索してみてください.
- httpとは何かを知りたい方は,http 通信 で検索してみてください.
- プロトコルって何かを知りたい方は,プロトコル IP で検索してみてください.
サーバ側がhttp postで受信するメリットは以下があると思います.
- 受信をトリガにした処理を作るのが容易
- 任意のファイルを受け付けることができる
- Arduinoに限らず,様々な言語でライブラリが存在するので検証が容易
4. Arduino用HTTPClientライブラリについて
ESP32用ArduinoライブラリとしてHTTPClientがあり,postメソッドがあります.
#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におけるマルチパート
マルチパートというと,ファイル添付したメールを連想する方が多いと思います.区切りの文字列で分離できる形で,上が本文,下がファイルです.
http postでも同じで,ファイルはマルチパート化して送信する必要があります.
実際にhttp postでファイルを送信しているパケットをキャプチャすると,マルチパートであることが判ります.
この部分を保存してテキストエディタで開くとこんな感じです.区切り文字をハイライトしています.
メールの場合,ファイル内容は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を実行する前に通信が一往復しています.
WiFiClientで実装する場合は,この手続をコーディングする必要があります.
しかし,HTTPClientは,このための処理が実装済みなので,特別に実装する必要はありません.
(ここがHTTPClientを使うメリット)
ただし,一つヘッダに必要な文字列があります.
Content-Type: multipart/form-data; boundary=区切り文字
この文字列を入れるには次のように記述すると良いです.
client.addHeader("Content-Type","multipart/form-data; boundary=区切り文字")
5.4. 対向サーバ
サーバ側は,node-redでhttp inとhttp responseのノードだけで十分です. メッセージを出力したい場合はtemplateノードを入れます.
http inの設定は,メソッドをpostにして,ファイルのアップロードにチェックを入れます.
6. 最終的なコード
WiFiの環境に合わせてSSIDやPASSPHRASEを入れ,サーバのアドレスを入れます.URL2で,node-redのポート番号(1880)を付け,postを受け付けるURLを構成しています.
アップロードする画像は
です.
画像ファイルをCSV化し,ソースに埋め込んでいます.
#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スクリプト
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)