search
LoginSignup
8

More than 1 year has passed since last update.

posted at

updated at

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 メソッド で検索してみてください.
- 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)

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
What you can do with signing up
8