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で送信しますが、マルチパート形式で送信する必要があるため、ちょっと面倒です。
以下の記事を参考にさせていただきました。
面倒な理由で一番大きいのが、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.print
、client.flush
、client.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;
}
以上