LoginSignup
9

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-11-01

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

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

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

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

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

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 するサンプル

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

〇URLの分解

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

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

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 には乱数か何か適当な文字列を入れておきます。

#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.printclient.flushclient.write を使って送信します。

〇レスポンスの解析

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

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;
}

サンプル

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

#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;
}

以上

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
9