M5Cameraから画像をPOSTする

[前の投稿](https://qiita.com/dinosauria123/items/c8ce027f1c23dc916609)の課題だった、M5Cameraから画像をPOSTして他のサーバで画像表示ができた。

M5Cameraのソースは
https://github.com/igrr/esp32-cam-demo
を改造した。

POSTの部分は、
https://github.com/espressif/esp-idf/blob/master/examples/protocols/esp_http_client/main/esp_http_client_example.c#L88
をコピペしている。

M5カメラのフレームバッファのデータをBase64でエンコードして画像表示用のサーバにポストする。
画像表示用のサーバはNode-REDをもちいた。

M5Cameraに書き込んだファームウエアのソースは以下になる。

```C:app_main.c
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.


#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "mbedtls/base64.h"

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include "freertos/event_groups.h"

#include "esp_system.h"
#include "esp_wifi.h"
#include "esp_event_loop.h"
#include "esp_log.h"
#include "esp_err.h"
#include "nvs_flash.h"
#include "esp_http_client.h"

#include "driver/gpio.h"
#include "camera.h"
#include "http_server.h"
#include "lwip/netdb.h"

static esp_err_t event_handler(void *ctx, system_event_t *event);
static void initialise_wifi(void);
static void http_post_task();

static const char* TAG = "camera_demo";
static EventGroupHandle_t s_wifi_event_group;
static ip4_addr_t s_ip_addr;
static camera_pixelformat_t s_pixel_format;

#define CAMERA_FRAME_SIZE CAMERA_FS_VGA　　　<-画像サイズ（QVGA, etc.）

// The IP address that we want our device to have.
#define DEVICE_IP          "192.168.0.250"   <-M5Cameraのアドレス（固定）

// The Gateway address where we wish to send packets.
// This will commonly be our access point.
#define DEVICE_GW          "192.168.11.1"　　<-Wifiルーターのアドレス

// The netmask specification.
#define DEVICE_NETMASK     "255.255.255.0"

#define DEFAULT_PS_MODE WIFI_PS_NONE

/* The event group allows multiple bits for each event,
but we only care about one event - are we connected
to the AP with an IP? */
const int CONNECTED_BIT = BIT0;

/* Constants that aren't configurable in menuconfig */
#define WEB_URL "http://35.231.0.1:1880/up/"  <-POSTするサーバのアドレス

void app_main()
{
    esp_log_level_set("wifi", ESP_LOG_WARN);
    esp_log_level_set("gpio", ESP_LOG_WARN);

    esp_err_t err = nvs_flash_init();
    if (err != ESP_OK) {
        ESP_ERROR_CHECK( nvs_flash_erase() );
        ESP_ERROR_CHECK( nvs_flash_init() );
    }

    ESP_ERROR_CHECK(gpio_install_isr_service(0));

    camera_config_t camera_config = {
        .ledc_channel = LEDC_CHANNEL_0,
        .ledc_timer = LEDC_TIMER_0,
        .pin_d0 = CONFIG_D0,
        .pin_d1 = CONFIG_D1,
        .pin_d2 = CONFIG_D2,
        .pin_d3 = CONFIG_D3,
        .pin_d4 = CONFIG_D4,
        .pin_d5 = CONFIG_D5,
        .pin_d6 = CONFIG_D6,
        .pin_d7 = CONFIG_D7,
        .pin_xclk = CONFIG_XCLK,
        .pin_pclk = CONFIG_PCLK,
        .pin_vsync = CONFIG_VSYNC,
        .pin_href = CONFIG_HREF,
        .pin_sscb_sda = CONFIG_SDA,
        .pin_sscb_scl = CONFIG_SCL,
        .pin_reset = CONFIG_RESET,
        .xclk_freq_hz = CONFIG_XCLK_FREQ,
    };

    camera_model_t camera_model;
    err = camera_probe(&camera_config, &camera_model);
    if (err != ESP_OK) {
        ESP_LOGE(TAG, "Camera probe failed with error 0x%x", err);
        return;
    }

  if (camera_model == CAMERA_OV2640) { 
       ESP_LOGI(TAG, "Detected OV2640 camera, using JPEG format");
        s_pixel_format = CAMERA_PF_JPEG;
        camera_config.frame_size = CAMERA_FRAME_SIZE;
        camera_config.jpeg_quality = 20;　　<-小さいほど画質が良い
    } else {
        ESP_LOGE(TAG, "Camera not supported");
        return;
    }

    camera_config.pixel_format = s_pixel_format;
    err = camera_init(&camera_config);
    if (err != ESP_OK) {
        ESP_LOGE(TAG, "Camera init failed with error 0x%x", err);
        return;
    }

    initialise_wifi();

    ESP_LOGI(TAG, "Free heap: %u", xPortGetFreeHeapSize());
    ESP_LOGI(TAG, "Camera demo ready");

    camera_run();

    xTaskCreate(&http_post_task, "http_post_task", 100000, NULL, 5, NULL);

    vTaskDelay(2000/portTICK_RATE_MS);　　<-2秒待つ

    ESP_LOGI(TAG, "DeepSleep....zzzz");
    esp_deep_sleep(8000000);　　　　　　　  <-8秒待って復帰
}

static esp_err_t event_handler(void *ctx, system_event_t *event)
{
    switch (event->event_id) {
        case SYSTEM_EVENT_STA_START:
            esp_wifi_connect();
            break;
        case SYSTEM_EVENT_STA_GOT_IP:
            xEventGroupSetBits(s_wifi_event_group, CONNECTED_BIT);
            s_ip_addr = event->event_info.got_ip.ip_info.ip;
            break;
        case SYSTEM_EVENT_STA_DISCONNECTED:
            esp_wifi_connect();
            xEventGroupClearBits(s_wifi_event_group, CONNECTED_BIT);
            break;
        default:
            break;
    }
    return ESP_OK;
}

static void initialise_wifi(void)
{
    tcpip_adapter_init();
    tcpip_adapter_dhcpc_stop(TCPIP_ADAPTER_IF_STA); // Don't run a DHCP client
    tcpip_adapter_ip_info_t ipInfo;

    inet_pton(AF_INET, DEVICE_IP, &ipInfo.ip);
    inet_pton(AF_INET, DEVICE_GW, &ipInfo.gw);
    inet_pton(AF_INET, DEVICE_NETMASK, &ipInfo.netmask);
  
    tcpip_adapter_set_ip_info(TCPIP_ADAPTER_IF_STA, &ipInfo);

    s_wifi_event_group = xEventGroupCreate();
    ESP_ERROR_CHECK( esp_event_loop_init(event_handler, NULL) );
    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    ESP_ERROR_CHECK( esp_wifi_init(&cfg) );
    ESP_ERROR_CHECK( esp_wifi_set_storage(WIFI_STORAGE_RAM) );
    wifi_config_t wifi_config = {
        .sta = {
            .ssid = CONFIG_WIFI_SSID,
            .password = CONFIG_WIFI_PASSWORD,
        },
    };
    ESP_ERROR_CHECK( esp_wifi_set_mode(WIFI_MODE_STA) );
    ESP_ERROR_CHECK( esp_wifi_set_config(WIFI_IF_STA, &wifi_config) );
    ESP_ERROR_CHECK( esp_wifi_start() );
    ESP_ERROR_CHECK( esp_wifi_set_ps(WIFI_PS_NONE) );
    ESP_LOGI(TAG, "Connecting to \"%s\"", wifi_config.sta.ssid);
    xEventGroupWaitBits(s_wifi_event_group, CONNECTED_BIT, false, true, portMAX_DELAY);
    ESP_LOGI(TAG, "Connected");
}


static void http_post_task()
{

   esp_http_client_config_t config = {
       .url = WEB_URL, 
   };

     esp_http_client_handle_t client = esp_http_client_init(&config);

     unsigned char image0[40000];
     char image[40000];
     size_t olen;

     mbedtls_base64_encode(image0, sizeof(image0), &olen, camera_get_fb(), camera_get_data_size());

    static char *HEADER = "text=";

    snprintf(image, sizeof(image0)+5, "%s%s", HEADER, image0);

    const char *post_data = (const char*)image;

    esp_http_client_set_method(client, HTTP_METHOD_POST);
    esp_http_client_set_post_field(client, post_data, strlen(post_data));
    esp_err_t  err = esp_http_client_perform(client);

    if (err == ESP_OK) {
        ESP_LOGI(TAG, "HTTP POST Status = %d, content_length = %d",
                esp_http_client_get_status_code(client),
                esp_http_client_get_content_length(client));
    } else {
        ESP_LOGE(TAG, "HTTP POST request failed: %s", esp_err_to_name(err));
    }
    while(1);
}

```

POSTするとBase64エンコードの中の”＋”がスペースに化ける。
対策がわからなかったので、Node-REDのサーバーの側でスペースを
”＋”に戻して画像を得ることができた。

Node-RED側の画像の受信は以下のソースとなる。

```
var data0=msg.payload.text;
var data = data0.replace(/\s/g,'+');

msg.payload = `<img src="data:image/jpeg;base64,${data}">`;

global.set("html_up",msg.payload);

```
全フローはこちら。
![Screenshot from 2018-10-24 15-07-34.png](https://qiita-image-store.s3.amazonaws.com/0/200022/f6751297-072d-1f93-9ac2-bd31117a4ba3.png)

```json:imageserver.json
[{"id":"f8716dce.c0c578","type":"tab","label":"フロー 3","disabled":false,"info":""},{"id":"3a3988a7.2a2ba8","type":"http in","z":"f8716dce.c0c578","name":"","url":"/mobile2","method":"get","upload":false,"swaggerDoc":"","x":118,"y":343,"wires":[["b854189d.bf9df"]]},{"id":"a66fea47.d45308","type":"http response","z":"f8716dce.c0c578","name":"","statusCode":"","headers":{},"x":598,"y":343,"wires":[]},{"id":"b854189d.bf9df","type":"function","z":"f8716dce.c0c578","name":"画像表示","func":"msg.payload=\"Last Update : \"+global.get(\"time1\")+\"</br>\";\nmsg.payload=msg.payload+global.get(\"html_up\")+\"</br>\"\nreturn msg;","outputs":1,"noerr":0,"x":371,"y":343,"wires":[["a66fea47.d45308"]]},{"id":"56d6e2b6.5e48fc","type":"http response","z":"f8716dce.c0c578","name":"","statusCode":"","headers":{},"x":674,"y":219,"wires":[]},{"id":"b410b859.619578","type":"function","z":"f8716dce.c0c578","name":"画像データ生成","func":"var data0=msg.payload.text;\nvar data = data0.replace(/\\s/g,'+');\n\nmsg.payload = `<img src=\"data:image/jpeg;base64,${data}\">`;\n\nglobal.set(\"html_up\",msg.payload);\n\n\nvar getCurrentTime = function () {\n    var date = new Date();\n    date.setHours(date.getHours() + 9);\n    var d = date.getFullYear() + '-';\n    d += ('0' + (date.getMonth() + 1)).slice(-2) + '-';\n    d += ('0' + date.getDate()).slice(-2) + ' ';\n    d += ('0' + date.getHours()).slice(-2) + ':';\n    d += ('0' + date.getMinutes()).slice(-2) + ':';\n    d += ('0' + date.getSeconds()).slice(-2);\n    return d;\n};\n\nmsg.payload =  {\n    \"timestamp\": getCurrentTime()\n};\n\nglobal.set(\"time1\",msg.payload.timestamp);\n\nreturn msg;","outputs":1,"noerr":0,"x":339,"y":218,"wires":[["56d6e2b6.5e48fc"]]},{"id":"b1bd4bbb.d5e98","type":"debug","z":"f8716dce.c0c578","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","x":591,"y":432,"wires":[]},{"id":"a86c8dfe.8c2eb","type":"http in","z":"f8716dce.c0c578","name":"","url":"/up","method":"post","upload":true,"swaggerDoc":"","x":104,"y":219,"wires":[["b410b859.619578"]]}]
```

ファームウエアで指定した画像サーバのアドレス/mobile2
にアクセスするとM5Cameraから送られた画像が見えるはずである。

このファームウエアでは約10秒おきにdeepsleepしてカメラの画像をPOSTする。このことにより、M5Camera基板の発熱が軽減する。
ダイソーの300円モバイルバッテリーに満充電でつないだところ、約20時間動作した。
送信間隔を一分程度にすれば数日間画像を送信し続けることが期待できる。

あまり出来は良くないが、とりあえず動いたのでよしとしよう。