ESP32でバイナリファイルのダウンロード・アップロード

ESP32でバイナリファイルのダウンロードとアップロードをします。

Arduinoを使っていますが、HTTPClientやらWifiClientやら、マルチパートやら、ややこしいことがあったので、一度関数にまとめておこうと思いました。

#バイナリファイルダウンロード

バイナリファイルダウンロードには、HTTPClientを使います。
メソッドはHTTP Getを利用します。

細かな処理は、HTTPClientのクラスがやってくれます。
http.GET()を呼び出して、あとは、stream->readBytes()を呼び出すだけです。

```c
long doHttpGet(String url, uint8_t *p_buffer, unsigned long *p_len){
  HTTPClient http;

  Serial.print("[HTTP] GET begin...\n");
  // configure traged server and url
  http.begin(url);

  Serial.print("[HTTP] GET...\n");
  // start connection and send HTTP header
  int httpCode = http.GET();
  unsigned long index = 0;

  // httpCode will be negative on error
  if(httpCode > 0) {
      // HTTP header has been send and Server response header has been handled
      Serial.printf("[HTTP] GET... code: %d\n", httpCode);

      // file found at server
      if(httpCode == HTTP_CODE_OK) {
        // get tcp stream
        WiFiClient * stream = http.getStreamPtr();

        // get lenght of document (is -1 when Server sends no Content-Length header)
        int len = http.getSize();
        Serial.printf("[HTTP] Content-Length=%d\n", len);
        if( len != -1 && len > *p_len ){
          Serial.printf("[HTTP] buffer size over\n");
          http.end();
          return -1;
        }

        // read all data from server
        while(http.connected() && (len > 0 || len == -1)) {
            // get available data size
            size_t size = stream->available();

            if(size > 0) {
                // read up to 128 byte
                if( (index + size ) > *p_len){
                  Serial.printf("[HTTP] buffer size over\n");
                  http.end();
                  return -1;
                }
                int c = stream->readBytes(&p_buffer[index], size);

                index += c;
                if(len > 0) {
                    len -= c;
                }
            }
            delay(1);
        }
      }else{
        http.end();
        return -1;
      }
  } else {
    http.end();
    Serial.printf("[HTTP] GET... failed, error: %s\n", http.errorToString(httpCode).c_str());
    return -1;
  }

  http.end();
  *p_len = index;

  return 0;
}
```

#バイナリファイルアップロード

HTTP Postで送信しますが、マルチパート形式で送信する必要があるため、ちょっと面倒です。

以下の記事を参考にさせていただきました。

　[ESP32(Arduino)で画像をPOST するサンプル](https://qiita.com/t_furu/items/d4f7213f70c54a8ba51d)

面倒な理由で一番大きいのが、HTTPClientを使えないことです。ですので、WifiClientを使ってマルチパートを一から実装する必要があります。

〇URLの分解

WifiClientは、URI形式を認識してくれず、スキーム名・ホスト名・ポート番号、それ以降のパスを区別する必要があります。

そこで以下のような関数を作成しました。
URLを入力して、スキーム名、ホスト名、ポート番号、パスを抽出してくれます。

```c
typedef struct{
	char scheme;
	char host[32];
	unsigned short port;
	const char *p_path;
} URL_INFO;

URL_INFO url_parse(const char *p_url){
  URL_INFO url_info;
	const char *p_ptr = p_url;
	
	url_info.scheme = -1;
	if( strncmp(p_ptr, "http://", 7 ) == 0 ){
    url_info.scheme = 0;
		p_ptr += 7;
	}else if( strncmp( p_ptr, "https://", 8 ) == 0 ){
		url_info.scheme = 1;
		p_ptr += 8;
	}
	
	const char *p_host = p_ptr;
	
	memset( url_info.host, '\0', sizeof(url_info.host));
	
	url_info.port = 80;	
	while( *p_ptr != '\0' ){
		if( *p_ptr == ':' ){
			strncat(url_info.host, p_host, p_ptr - p_host);
			p_ptr++;
			url_info.port = atoi(p_ptr);
			while( isdigit(*p_ptr) ) p_ptr++;
			break;
		}else if( *p_ptr == '/' ){
			strncat(url_info.host, p_ptr, p_ptr - p_host);
			break;
		}
		p_ptr++;
	}
	
	url_info.p_path = p_ptr;
	
	return url_info;
}
```

〇マルチパートの生成

マルチパートを自作する必要があります。
基本的には以下に上げる文字列を生成して、つなげて送信する必要があります。
MUTIPART_BOUNDARY には乱数か何か適当な文字列を入れておきます。

```c
#define MUTIPART_BOUNDARY  "f9492bf35a2126aca30c9ca8525"

#define PAYLOAD_PARAM_HEADER_FORMAT ""\
                     "--%s\r\n"\
                     "Content-Disposition: form-data; name=\"%s\"\r\n"\
                     "\r\n"

#define PAYLOAD_FILE_HEADER_FORMAT ""\
                     "--%s\r\n"\
                     "Content-Disposition: form-data; name=\"%s\"; filename=\"%s\"\r\n"\
                     "Content-Type: %s\r\n"\
                     "Content-Transfer-Encoding: binary\r\n"\
                     "\r\n"

#define MULTIPART_CONTENT_TYPE_FORMAT "multipart/form-data; boundary=%s"
#define MULTIPART_DELIMITER "\r\n"
#define PAYLOAD_FOOTER_FORMAT "--%s--\r\n"
```

ほかにも、それらの結合後のContent-Lengthを計算したりする必要があります。

〇HTTP送信

```client.connect(url_info.host, url_info.port)``` で接続先サーバに接続したのち、```client.print```、```client.flush```、```client.write``` を使って送信します。

〇レスポンスの解析

WifiClientを使って生のデータを扱っているため、HTTPレスポンスの解析をする必要があります。
```\r\n```を受信するまでは、HTTPヘッダー情報です。```client.readStringUntil('\n')```で読み進めます。
その中に、```Content-Length```があるのでそれを探します。
あとは、そのサイズ分だけ、```client.readBytes```を呼び出します。

```c
long doHttpPostFile(const char *p_url, const unsigned char *p_data, unsigned long data_len, const char *p_content_type, const char *p_bin_name, const char *p_bin_filename, 
    const char *p_param_name, const char *p_param_data, uint8_t *p_buffer, unsigned long *p_len){

  Serial.print("[HTTP] POST begin...\n");

  URL_INFO url_info = url_parse(p_url);
  WiFiClient client = espClient;
  if( url_info.scheme == 1 )
    client = espClientSecure;

  char contentType[120];
  sprintf(contentType, MULTIPART_CONTENT_TYPE_FORMAT, MUTIPART_BOUNDARY);
  
  char payloadParamHeader[120] = {0};
  if( p_param_name != NULL && p_param_data != NULL ){
    sprintf(payloadParamHeader,
                PAYLOAD_PARAM_HEADER_FORMAT,
                MUTIPART_BOUNDARY,
                p_param_name);
  }

  char payloadHeader[220] = {0};
  sprintf(payloadHeader,
              PAYLOAD_FILE_HEADER_FORMAT,
              MUTIPART_BOUNDARY,
              p_bin_name, p_bin_filename, p_content_type);

  char payloadFooter[70] = {0};
  sprintf(payloadFooter, PAYLOAD_FOOTER_FORMAT, MUTIPART_BOUNDARY);
  
  long contentLength = strlen(payloadHeader) + data_len + strlen(MULTIPART_DELIMITER) + strlen(payloadFooter);
  if( p_param_name != NULL && p_param_data != NULL )
    contentLength += strlen(payloadParamHeader) + strlen(p_param_data) + strlen(MULTIPART_DELIMITER);

  Serial.printf("[HTTP] request Content-Length=%ld\n", contentLength);

  Serial.print("[HTTP] POST...\n");
  if (!client.connect(url_info.host, url_info.port)){
    Serial.println("can not connect");
    return -3;
  }
  
  client.printf("POST %s HTTP/1.1\n", url_info.p_path);
  client.print(F("Host: "));client.println(url_info.host);
  client.println(F("Accept: application/json"));
  client.println(F("Connection: close"));
  client.print(F("Content-Type: "));client.println(contentType);
  client.print(F("Content-Length: "));client.println(contentLength);        
  client.println();
  client.print(payloadHeader);
  client.flush();
  
  client.write(p_data, data_len);
  client.flush();
  client.print(MULTIPART_DELIMITER);
  if( p_param_name != NULL && p_param_data != NULL ){
    client.print(payloadParamHeader);
    client.print(p_param_data);
    client.print(MULTIPART_DELIMITER);
  }
  client.print(payloadFooter);
  client.flush();

  bool trimed = false;
  long length = -1;
  long received = 0;

  while(client.connected() || client.available()) {
    int size = client.available();
    if( size <= 0 ){
      delay(1);
      continue;
    }

    if( !trimed ){
      String line = client.readStringUntil('\n');
      line.toLowerCase();
      if( line.startsWith("content-length:") ){
        length = atoi(&(line.c_str()[15]));
        Serial.printf("[HTTP] response Content-Length=%ld\n", length);
      }else if( line == "\r" ){
        trimed = true;
      }
    }else if( trimed ){
      if( received + size > *p_len ){
        client.stop();
        Serial.println("receive buffer over");
        return -1;
      }
      int len = client.readBytes(&p_buffer[received], size);
      received += len;
      if( length >= 0 && received >= length )
        break;
    }
  }

  client.stop();

  if( !trimed )
    return -2;

  *p_len = received;

  return 0;
}
```

#サンプル

利用サンプルも示しておきます。
後ろに前述の関数群もくっつけておきます。

```c
#include <WiFi.h>
#include "M5Lite.h"
#include <HTTPClient.h>

WiFiClient espClient;
WiFiClientSecure espClientSecure;

const char* wifi_ssid = "【WiFiアクセスポイントのSSID】";
const char* wifi_password = "【WiFiアクセスポイントのパスワード】";

long doHttpGet(String url, uint8_t *p_buffer, unsigned long *p_len);
long doHttpPostFile(const char *p_url, const unsigned char *p_data, unsigned long data_len, const char *p_content_type, const char *p_bin_name, const char *p_bin_filename, 
    const char *p_param_name, const char *p_param_data, uint8_t *p_buffer, unsigned long *p_len);

void wifi_connect(void){
  Serial.println("");
  Serial.print("WiFi Connenting");
  M5Lite.Lcd.print("Connecting");
  
  WiFi.begin(wifi_ssid, wifi_password);
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print(".");
    M5Lite.Lcd.print(".");
    delay(1000);
  }
  Serial.println("");
  Serial.print("Connected : ");
  Serial.println(WiFi.localIP());

  M5Lite.Lcd.fillScreen(BLACK);
  M5Lite.Lcd.setCursor(0, 0);
  M5Lite.Lcd.println(WiFi.localIP());
}

void setup() {
  M5Lite.begin();
  Serial.begin(9600);

  M5Lite.Lcd.setRotation(3);
  M5Lite.Lcd.fillScreen(BLACK);
  M5Lite.Lcd.setTextColor(WHITE, BLACK);
  M5Lite.Lcd.println("[M5StickC]");

  wifi_connect();
}

unsigned char buffer[50000];
unsigned char data[10000];

void loop() {
  M5Lite.update();

  if (M5Lite.BtnB.wasReleased()) {
    Serial.println("M5Lite.BtnA.wasReleased()");

    unsigned long len;
    len = sizeof(buffer);
    long ret = doHttpGet("【取得したいファイルのURL】", buffer, &len);

    Serial.printf("end ret=%d, size=%d\n", ret, len);
  }
  if( M5Lite.BtnA.wasReleased()){
    unsigned long len;
    len = sizeof(buffer);
    long ret = doHttpPostFile("【送信先のURL】", data, sizeof(data), "【送信ファイルのコンテンツタイプ】", "【送信ファイルのID】", "【送信ファイル名】", "【追加パラメータのID】", "【追加パラメータの値】", buffer, &len);
    Serial.printf("ret=%d\n", ret);
    Serial.printf("response=%s\n", buffer);
  }
}

typedef struct{
	char scheme;
	char host[32];
	unsigned short port;
	const char *p_path;
} URL_INFO;

URL_INFO url_parse(const char *p_url){
  URL_INFO url_info;
	const char *p_ptr = p_url;
	
	url_info.scheme = -1;
	if( strncmp(p_ptr, "http://", 7 ) == 0 ){
    url_info.scheme = 0;
		p_ptr += 7;
	}else if( strncmp( p_ptr, "https://", 8 ) == 0 ){
		url_info.scheme = 1;
		p_ptr += 8;
	}
	
	const char *p_host = p_ptr;
	
	memset( url_info.host, '\0', sizeof(url_info.host));
	
	url_info.port = 80;	
	while( *p_ptr != '\0' ){
		if( *p_ptr == ':' ){
			strncat(url_info.host, p_host, p_ptr - p_host);
			p_ptr++;
			url_info.port = atoi(p_ptr);
			while( isdigit(*p_ptr) ) p_ptr++;
			break;
		}else if( *p_ptr == '/' ){
			strncat(url_info.host, p_ptr, p_ptr - p_host);
			break;
		}
		p_ptr++;
	}
	
	url_info.p_path = p_ptr;
	
	return url_info;
}

long doHttpGet(String url, uint8_t *p_buffer, unsigned long *p_len){
  HTTPClient http;

  Serial.print("[HTTP] GET begin...\n");
  // configure traged server and url
  http.begin(url);

  Serial.print("[HTTP] GET...\n");
  // start connection and send HTTP header
  int httpCode = http.GET();
  unsigned long index = 0;

  // httpCode will be negative on error
  if(httpCode > 0) {
      // HTTP header has been send and Server response header has been handled
      Serial.printf("[HTTP] GET... code: %d\n", httpCode);

      // file found at server
      if(httpCode == HTTP_CODE_OK) {
        // get tcp stream
        WiFiClient * stream = http.getStreamPtr();

        // get lenght of document (is -1 when Server sends no Content-Length header)
        int len = http.getSize();
        Serial.printf("[HTTP] Content-Length=%d\n", len);
        if( len != -1 && len > *p_len ){
          Serial.printf("[HTTP] buffer size over\n");
          http.end();
          return -1;
        }

        // read all data from server
        while(http.connected() && (len > 0 || len == -1)) {
            // get available data size
            size_t size = stream->available();

            if(size > 0) {
                // read up to 128 byte
                if( (index + size ) > *p_len){
                  Serial.printf("[HTTP] buffer size over\n");
                  http.end();
                  return -1;
                }
                int c = stream->readBytes(&p_buffer[index], size);

                index += c;
                if(len > 0) {
                    len -= c;
                }
            }
            delay(1);
        }
      }
  } else {
    http.end();
    Serial.printf("[HTTP] GET... failed, error: %s\n", http.errorToString(httpCode).c_str());
    return -1;
  }

  http.end();
  *p_len = index;

  return 0;
}

#define MUTIPART_BOUNDARY  "f9492bf35a2126aca30c9ca8525"

#define PAYLOAD_PARAM_HEADER_FORMAT ""\
                     "--%s\r\n"\
                     "Content-Disposition: form-data; name=\"%s\"\r\n"\
                     "\r\n"

#define PAYLOAD_FILE_HEADER_FORMAT ""\
                     "--%s\r\n"\
                     "Content-Disposition: form-data; name=\"%s\"; filename=\"%s\"\r\n"\
                     "Content-Type: %s\r\n"\
                     "Content-Transfer-Encoding: binary\r\n"\
                     "\r\n"

#define MULTIPART_CONTENT_TYPE_FORMAT "multipart/form-data; boundary=%s"
#define MULTIPART_DELIMITER "\r\n"
#define PAYLOAD_FOOTER_FORMAT "--%s--\r\n"

long doHttpPostFile(const char *p_url, const unsigned char *p_data, unsigned long data_len, const char *p_content_type, const char *p_bin_name, const char *p_bin_filename, 
    const char *p_param_name, const char *p_param_data, uint8_t *p_buffer, unsigned long *p_len){

  Serial.print("[HTTP] POST begin...\n");

  URL_INFO url_info = url_parse(p_url);
  WiFiClient client = espClient;
  if( url_info.scheme == 1 )
    client = espClientSecure;

  char contentType[120];
  sprintf(contentType, MULTIPART_CONTENT_TYPE_FORMAT, MUTIPART_BOUNDARY);
  
  char payloadParamHeader[120] = {0};
  sprintf(payloadParamHeader,
              PAYLOAD_PARAM_HEADER_FORMAT,
              MUTIPART_BOUNDARY,
              p_param_name);

  char payloadHeader[220] = {0};
  sprintf(payloadHeader,
              PAYLOAD_FILE_HEADER_FORMAT,
              MUTIPART_BOUNDARY,
              p_bin_name, p_bin_filename, p_content_type);

  char payloadFooter[70] = {0};
  sprintf(payloadFooter, PAYLOAD_FOOTER_FORMAT, MUTIPART_BOUNDARY);
  
  long contentLength = strlen(payloadHeader) + data_len + strlen(MULTIPART_DELIMITER) + strlen(payloadFooter);
  if( p_param_name != NULL && p_param_data != NULL )
    contentLength += strlen(payloadParamHeader) + strlen(p_param_data) + strlen(MULTIPART_DELIMITER);

  Serial.printf("[HTTP] request Content-Length=%ld\n", contentLength);

  Serial.print("[HTTP] POST...\n");
  if (!client.connect(url_info.host, url_info.port)){
    Serial.println("can not connect");
    return -3;
  }
  
  client.printf("POST %s HTTP/1.1\n", url_info.p_path);
  client.print(F("Host: "));client.println(url_info.host);
  client.println(F("Accept: application/json"));
  client.println(F("Connection: close"));
  client.print(F("Content-Type: "));client.println(contentType);
  client.print(F("Content-Length: "));client.println(contentLength);        
  client.println();
  client.print(payloadHeader);
  client.flush();
  
  client.write(p_data, data_len);
  client.flush();
  client.print(MULTIPART_DELIMITER);
  if( p_param_name != NULL && p_param_data != NULL ){
    client.print(payloadParamHeader);
    client.print(p_param_data);
    client.print(MULTIPART_DELIMITER);
  }
  client.print(payloadFooter);
  client.flush();

  bool trimed = false;
  long length = -1;
  long received = 0;

  while(client.connected() || client.available()) {
    int size = client.available();
    if( size <= 0 ){
      delay(1);
      continue;
    }

    if( !trimed ){
      String line = client.readStringUntil('\n');
      line.toLowerCase();
      if( line.startsWith("content-length:") ){
        length = atoi(&(line.c_str()[15]));
        Serial.printf("[HTTP] response Content-Length=%ld\n", length);
      }else if( line == "\r" ){
        trimed = true;
      }
    }else if( trimed ){
      if( received + size > *p_len ){
        client.stop();
        Serial.println("receive buffer over");
        return -1;
      }
      int len = client.readBytes(&p_buffer[received], size);
      received += len;
      if( length >= 0 && received >= length )
        break;
    }
  }

  client.stop();

  if( !trimed )
    return -2;

  *p_len = received;

  return 0;
}
```

以上