7
9

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-09-15

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におけるマルチパート

マルチパートというと,ファイル添付したメールを連想する方が多いと思います.区切りの文字列で分離できる形で,上が本文,下がファイルです.
mail_multipart.png

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

post_capt_packet.png

この部分を保存してテキストエディタで開くとこんな感じです.区切り文字をハイライトしています.
post_capt.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

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ノードを入れます.
node-red_nodes.png

http inの設定は,メソッドをpostにして,ファイルのアップロードにチェックを入れます.
httpinnode.png

6. 最終的なコード

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

アップロードする画像は
a.png
です.
画像ファイルを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)
7
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
9