M5StackとM5Camera でWiFi TCP/IP 動画ストリーミングする実験

M5StackとM5CameraをWiFi,TCP/IP,でMJPEG動画ストリーミングする実験 M5Stack

使ったもの

M5Stack

Espressif Systems社のWiFi & Bluetooth マイコン ESP32 を搭載し、LCD(液晶ディスプレイ)、micro SDカードスロット、ボタンスイッチ、簡易バッテリー、スピーカー、Grove端子等を搭載した全部入りマイコンモジュールです。
技適取得済みです。
M5Stackを初めて使ってみて分解してみた記事は以下を参照してください。
M5Stackを分解したり電源を入れてみて、いろいろ思ったこと
以下のどれでも使えますが、M5Stack Fireには要注意点があるので、こちらの記事を参照してください。

(※この世界情勢の為、Amazonに在庫が無いと思われます。
スイッチサイエンスウェブショップでも探してみて下さい。)

M5Stack Basic
スイッチサイエンス
¥5,203(2021/09/09 23:54時点)
M5Stack Gray(9軸IMU搭載)
スイッチサイエンス
M5Stack FIRE
スイッチサイエンス
¥7,436(2021/09/10 00:02時点)

M5Camera

Espressif Systems社のESP32-WROVERを搭載し、OmniVision社のイメージセンサ(カメラセンサ) OV2640 を搭載した、WiFiマイコンカメラモジュールです。
日本の電波法をクリアした技適取得済みです。
個人的にレビューした記事がありますので、以下を参照してみてください。
M5Camera をレビューしてみた。分解したり、Arduino IDE でスマホに映したりする実験

スイッチサイエンスさんで販売しています。
https://www.switch-science.com/catalog/5207/

micro SDHCカード

(※現在流通しているM5Stackは、16GBまでmicro SDHCカードしか使えません)

モバイルバッテリー

微弱な電流でも、オートパワーオフしない、IoT専用の超お勧めモバイルバッテリーです。
M5Cameraをモバイルで使う場合に良いです。

M5Stack用電池モジュール

M5Stack Basicにこのオプションバッテリーを使うと、2時間半くらいは動画ストリーミングできました。

M5Stack用電池モジュール
スイッチサイエンス
¥1,001(2021/09/09 23:54時点)

WiFi ルーター環境

できるだけ高速のWiFiルーター環境が必要です。
M5Stackがネットワークには入れるように、事前にファイアウォール等の設定は済ませておいてください。
MACアドレスフィルタリングを掛けている場合、M5StackやM5CameraのMACアドレスを調べる方法は以下の記事を参照してください。

ESP32-WROOM-32 チップ・メモリ・MACアドレス情報取得方法

電波到達距離は少ないですが、ホテル用ルーターでも良いかと思います。

WMR-433W2

Arduino core for the ESP32 のインストール

開発環境のバージョンは以下で動作確認しています。
私の場合はWindows 10 です。

Arduino IDE ver. 1.8.12
Arduino core for the ESP32 stable ver. 1.0.4

Arduino core for the ESP32 のインストール方法は以下の記事を参照してください。

Arduino core for the ESP32 のインストール方法

自作ライブラリのインストール

M5Stack側では、LCD(液晶ディスプレイ)表示に私の自作ライブラリを使います。
beta ver. 1.0.71
で動作確認しています。
自作ライブラリはGitHubの以下のリンクにあるので、ZIPファイルをダウンロードできます。

https://github.com/mgo-tec/ESP32_mgo_tec

ZIPファイルライブラリをArduino IDEにインストールする方法は以下の記事を参照してください。

GitHubにある ZIP形式ライブラリ のインストール方法 ( Arduino IDE )

※古いライブラリがある場合は、古いライブラリのフォルダごと削除してからインストールしてください。
ライブラリをインストールした場合は、必ずArduino IDEを再起動してください。

※Arduino標準のTimeライブラリが無い場合はコンパイルエラーが出ます。
事前にTimeライブラリをインストールしておいてください。
https://github.com/PaulStoffregen/Time

※bme280 および bme680 を使わない場合は、ライブラリ内の/Sensor/フォルダを削除してください。
Windows10の場合のファイルパスは以下のようになります。

C:\Users\ユーザー名\Documents\Arduino\libraries\ESP32_mgo_tec-master\src\ESP32_mgo_tec_bV1\Sensor

フォントを micro SDHC カードにコピーしておき、M5Stackにセットしておく

フリーの日本語漢字、東雲フォントを使っています。
そして、私の自作のUTF8→Shift_JIS変換テーブルを使っています。
以下の記事を参照すれば、以下の3つのファイルがダウンロードできると思います。
Arduino – ESP32 ( SPIFFS 又は micro SD ) 自作Fontライブラリインストール方法

Utf8Sjis.tbl (UTF8→Shift_JIS変換テーブル)
shnmk16.bdf (16×16 全角フォント)
shnm8x16.bdf  ( shnm8x16r.bdf のファイル名中の ’r’ 文字を削除しておく)

この3つのファイルを micro SDHC カードのルートに/font/フォルダを作成してコピーしておけば良いです。

M5Camera(送信側)にテスト用 MJPEG (BMP) 動画送信スケッチを入力

M5Cameraのイメージセンサ動画を扱う前に、動画ストリーミング機能だけをテストしてみたいと思います。
そこで、前回記事のプログラムを少々修正して、テスト用のMJPEG (BMP) 動画ストリーミング送信プログラムにしてみました。

概要

大まかなプログラムの構成はこんな感じです。

void setup() {
  //CPU core 0 でWiFi通信サーバー用タスク生成
  TaskHandle_t taskServer_handl;
  xTaskCreatePinnedToCore(&taskServer, "taskServer", 8192, NULL, 20, &taskServer_handl, 0);

  //イメージセンサ(Camera) OV2640 初期化
  //ESP32 DMA制御初期化
}

void loop() {
  //メインループはCPU core 1
  //画像データ配列に描画
}

void taskServer(void *pvParameters){
  connectToWiFi(); //WiFiアクセスポイント接続
  startHttpd(); //httpd開始
  while(true){
    delay(1);
  }
}

void startHttpd(){
  //httpdライブラリで、port 80 と port 81 通信設定
  //制御コマンド、動画ストリーミング等送受信
}

void connectToWiFi(){
  //WiFi アクセスポイント接続
  isWiFiConnected = true;
}

Arduino core for the ESP32 のhttpdライブラリを使っているので、比較的高速処理です。
port 80番と port 81番のタスク処理はライブラリ任せなので便利です。

詳しい解説は前回記事をご覧ください。

スケッチ(プログラムソースコード)

では、Arduino IDE にスケッチ(プログラムソースコード)を入力していきます。

基本的には前回記事と同じですが、先にも述べたように、ボタンスイッチでGETリクエストを送信してエンディアンを変更できるようにしました。
295-300行のところです。

その他、304行のところでstop_streamコマンドを受信したら、231-246行の動画ストリーミングループを脱出するようにしています。

ちなみに、このブログで何度も申し上げておりますが、私はアマチュア素人なので、ソースコードに無駄が多く、誤りがあるかも知れません。
よって動作保証できません。
でも、何かありましたらコメント投稿等でご連絡いただけると助かります。

※WiFi アクセスポイントのSSIDとパスワードは、このデバイスが第三者の手に渡ると、比較的簡単に読み取られてしまうので、扱いには十分気を付けてください。
このスケッチに関するトラブルの責任は、一切負いません。

【ソースコード】 (※無保証 ※PCの場合、ダブルクリックすればコード全体を選択できます)

/* The MIT License (MIT)
 * License URL: https://opensource.org/licenses/mit-license.php
 * Copyright (c) 2020 Mgo-tec. All rights reserved.
 * Modify app_httpd.cpp(Arduino core for the ESP32 v1.0.4).
 * Copyright 2015-2016 Espressif Systems (Shanghai) PTE LTD
 * app_httpd.cpp - Licensed under the Apache License, Version 2.0
 *     http://www.apache.org/licenses/LICENSE-2.0
 */
#include <WiFi.h>
#include <utility> //swap関数を使う場合に必要
#include <esp_http_server.h> //httpd関数を使う場合必要
 
const char* ssid = "xxxxxxxxx"; //ご自分のルーターのSSIDに書き換えてください
const char* password = "xxxxxxxxx"; //ご自分のルーターのパスワードに書き換えてください

//----------------------------------
const uint16_t disp_width_pix = 200, disp_height_pix = 148;
const uint16_t max_x = disp_width_pix - 1;
const uint16_t max_y = disp_height_pix - 1;
const uint16_t max_w_pix_buf = disp_width_pix * 2;
//----------------------------------
bool shouldClear = true;
uint16_t y_old = 0;
uint16_t draw_line_count = 0;
int16_t draw_rect_count = 0;
uint32_t draw_time = 0;
int8_t moving_width_pix = 3;
uint8_t ctrl_red = 0, ctrl_green = 0xff, ctrl_blue = 0xff;
//------Initialize bitmap data------
const uint16_t data_size = disp_width_pix * 2 * disp_height_pix;
const uint8_t data_size_lsb = (uint8_t)(0x00ff & data_size);
const uint8_t data_size_msb = (uint8_t)(data_size >> 8); 
const uint8_t bmp_head_bytes = 66;
const uint16_t file_size = bmp_head_bytes + data_size;
const uint8_t file_size_lsb = (uint8_t)(0x00ff & file_size);
const uint8_t file_size_msb = (uint8_t)(file_size >> 8);
const uint8_t info_header_size = 0x28; //情報ヘッダサイズは常に40byte = 0x28byte
const uint8_t bits_per_pixel = 16; //色ビット数=16bit(0x10)
const uint8_t compression = 3; //色ビット数が16bitの場合、マスクを設定するので3にする。
const uint8_t red_mask[2] =   {0b11111000, 0b00000000};
const uint8_t green_mask[2] = {0b00000111, 0b11100000};
const uint8_t blue_mask[2] =  {0b00000000, 0b00011111}; 
//※Bitmap file headerは全てリトルエンディアン
const uint8_t bmp_header[bmp_head_bytes]=
    {0x42, 0x4D,
     file_size_lsb, file_size_msb, 0, 0,
     0, 0, 0, 0,
     bmp_head_bytes, 0, 0, 0,
     info_header_size, 0, 0, 0,
     disp_width_pix, 0, 0, 0,
     disp_height_pix, 0, 0, 0,
     1, 0, bits_per_pixel, 0,
     compression, 0, 0, 0,
     data_size_lsb, data_size_msb, 0, 0,
     0,0,0,0,
     0,0,0,0,
     0,0,0,0,
     0,0,0,0,
     red_mask[1], red_mask[0], 0, 0,
     green_mask[1], green_mask[0], 0, 0,
     blue_mask[1], blue_mask[0], 0, 0};
//----------------------------------
uint8_t bmp_data_buf[file_size] = {};
httpd_handle_t stream_httpd = NULL;
httpd_handle_t control_httpd = NULL;
uint32_t fps_timer = 0;
uint8_t fps_count = 0;
bool canStartStream = false;
bool canSendImage = false;
bool isCloseConnection = false;
bool isLittleEndian = true;

//*********************************************
void setup() {
  Serial.begin(115200);
  Serial.println();
  delay(1000);
  memcpy(bmp_data_buf, bmp_header, bmp_head_bytes); //BMPファイル配列にヘッダ情報を書き込む
  TaskHandle_t taskServer_handl;
  if (!xTaskCreatePinnedToCore(&taskServer, "taskServer", 9216, NULL, 24, &taskServer_handl, 0)) {
    Serial.println("Failed to create taskServer");
  }
  while(!canStartStream){
    delay(1);
  }
}
 
void loop() {
  if(shouldClear){
    clearAll();
    shouldClear = false;
  }
  if(canStartStream){
    if(!canSendImage){
      if(changeDrawCount(draw_time, 0, 3000)){
        drawRectangleLine(0, 0, max_x, max_y, 0xff, 0xff, 0xff);
        drawLine(0, 0, max_x, max_y, 0xff, 0xff, 0xff);
        drawLine(max_x, 0, 0, max_y, 0xff, 0xff, 0xff);
        drawVerticalLine(100, max_y, 0, 0xff, 0, 0);
        drawHorizontalLine(0, 74, max_x, 0xff, 0xff, 0);
      }
      if(changeDrawCount(draw_time, 3000, 3500)){
        clearAll();
      }
      if(changeDrawCount(draw_time, 3500, 6000)){
        uint8_t bar_w = 25;
        drawRectangleFill(0, 0, bar_w - 1, max_y, 0xff, 0xff, 0xff);
        drawRectangleFill(bar_w, 0, bar_w * 2 - 1, max_y, 0xff, 0x00, 0x00);
        drawRectangleFill(bar_w * 2, 0, bar_w * 3 - 1, max_y, 0x00, 0xff, 0x00);
        drawRectangleFill(bar_w * 3, 0, bar_w * 4 - 1, max_y, 0x00, 0x00, 0xff);
        drawRectangleFill(bar_w * 4, 0, bar_w * 5 - 1, max_y, 0xff, 0xff, 0x00);
        drawRectangleFill(bar_w * 5, 0, bar_w * 6 - 1, max_y, 0x00, 0xff, 0xff);
        drawRectangleFill(bar_w * 6, 0, bar_w * 7 - 1, max_y, 0xff, 0x00, 0xff);
        drawRectangleFill(bar_w * 7, 0, bar_w * 8 - 1, max_y, 0x80, 0x80, 0x80);
      }
      if(changeDrawCount(draw_time, 6000, 6500)){
        clearAll();
      }
      if(changeDrawCount(draw_time, 6500, 16000)){
        drawLine(0, draw_line_count * 7, draw_line_count * 9, max_y, 0xff, 0xff, 0xff);
        drawLine(draw_line_count * 9, 0, max_x, draw_line_count * 7, ctrl_red, ctrl_green, ctrl_blue);
        draw_line_count++;
        if(draw_line_count >= 22){
          draw_line_count = 0;
        }
      }
      if(changeDrawCount(draw_time, 16000, 16500)){
        draw_line_count = 0;
        clearAll();
      }
      if(changeDrawCount(draw_time, 16500, 35000)){
        uint8_t rect_width = 20;
        uint8_t x0 = draw_rect_count;
        uint8_t x1 = draw_rect_count + rect_width;
        uint8_t y0 = 75;
        uint8_t y1 = y0 + rect_width;
        drawRectangleFill(x0, y0, x1, y1, ctrl_red, ctrl_green, ctrl_blue);
        if(moving_width_pix < 0){
          if(x1 != (max_x)){
            drawRectangleFill(x1, y0, x1 - moving_width_pix, y1, 0x00, 0x00, 0x00);
          }
        }else{
          if(draw_rect_count >= moving_width_pix){
            drawRectangleFill(x0 - moving_width_pix, y0, x0 - 1, y1, 0x00, 0x00, 0x00);
          }
        }
        draw_rect_count += moving_width_pix;
        if(draw_rect_count >= (max_x -rect_width)){
          moving_width_pix = -3;
        }else if(draw_rect_count <= 0){
          moving_width_pix = 3;
        }
      }
      if(changeDrawCount(draw_time, 35000, 35500)){
        draw_rect_count = 0;
        clearAll();
        draw_time = millis();
      }
      canSendImage = true;
    }
  }
  if(canStartStream && (millis() - fps_timer > 1000)){
    Serial.printf("%d (fps)\r\n", fps_count);
    fps_count = 0;
    fps_timer = millis();
  }
}
//*********************************************
void taskServer(void *pvParameters){
  connectToWiFi(ssid, password);
  startHttpd();
  while(true){
    delay(1);
  }
}
//****************************************
void startHttpd(){
  httpd_config_t config = HTTPD_DEFAULT_CONFIG();
 
  httpd_uri_t index_uri = {
    .uri       = "/",
    .method    = HTTP_GET,
    .handler   = index_handler,
    .user_ctx  = NULL
  };
 
  httpd_uri_t cmd_uri = {
    .uri       = "/control",
    .method    = HTTP_GET,
    .handler   = cmd_handler,
    .user_ctx  = NULL
  };
 
  httpd_uri_t stream_uri = {
    .uri       = "/stream",
    .method    = HTTP_GET,
    .handler   = stream_handler,
    .user_ctx  = NULL
  };
 
  if (httpd_start(&control_httpd, &config) == ESP_OK) {
    httpd_register_uri_handler(control_httpd, &index_uri);
    httpd_register_uri_handler(control_httpd, &cmd_uri);
  }
 
  config.server_port += 1;
  config.ctrl_port += 1;
 
  if (httpd_start(&stream_httpd, &config) == ESP_OK) {
    httpd_register_uri_handler(stream_httpd, &stream_uri);
  }
}
//****************************************
static esp_err_t stream_handler(httpd_req_t *req){
  esp_err_t res = ESP_OK;
  String res_http = "HTTP/1.1 200 OK\r\n";
      res_http += "Access-Control-Allow-Origin: *\r\n";
      res_http += "Content-type: multipart/x-mixed-replace;boundary=--myboundary\r\n";
      res_http += "\r\n";

  httpd_send(req, res_http.c_str(), res_http.length());
  res_http = "";

  String boundary_header = "--myboundary\r\n";
  boundary_header += "Access-Control-Allow-Origin: *\r\n";
  boundary_header += "Content-type: image/bmp\r\n";
  boundary_header += "Content-Length: " + String(file_size);
  boundary_header += "\r\n\r\n";
  Serial.printf("boundary_header size=%d\r\n", boundary_header.length());

  draw_time = millis();
  while(true){
    if(canStartStream){
      if(canSendImage){
        res = httpd_send(req, boundary_header.c_str(), boundary_header.length());
        res = httpd_send(req, (const char *)&bmp_data_buf[0], file_size);
        res = httpd_send(req, "\r\n", 2);
        canSendImage = false;
        fps_count++;
      }
    }
    if(isCloseConnection){
      Serial.println("Loop Out Stream!");
      break;
    }
    delay(1);
  }
  return res;
}
//******************************************
static esp_err_t cmd_handler(httpd_req_t *req){
  char*  buf;
  size_t buf_len;
  char id_txt[32] = {0,};
  char value_txt[32] = {0,};
 
  buf_len = httpd_req_get_url_query_len(req) + 1;
  if (buf_len > 1) {
    buf = (char*)malloc(buf_len);
    if(!buf){
      httpd_resp_send_500(req);
      return ESP_FAIL;
    }
    if (httpd_req_get_url_query_str(req, buf, buf_len) == ESP_OK) {
      Serial.println(buf);
      if (httpd_query_key_value(buf, "id", id_txt, sizeof(id_txt)) == ESP_OK &&
        httpd_query_key_value(buf, "value", value_txt, sizeof(value_txt)) == ESP_OK) {
      } else {
        free(buf);
        httpd_resp_send_404(req);
        return ESP_FAIL;
      }
    } else {
      Serial.println(buf);
      free(buf);
      httpd_resp_send_404(req);
      return ESP_FAIL;
    }
    free(buf);
  } else {
    httpd_resp_send_404(req);
    return ESP_FAIL;
  }
 
  uint8_t val = atoi(value_txt);
  int res = 0;
  if(!strcmp(id_txt, "red")) {
    ctrl_red = 0xff, ctrl_green = 0, ctrl_blue = 0;
    Serial.printf("%s = %d\r\n", id_txt, val);
  }else if(!strcmp(id_txt, "green")) {
    ctrl_red = 0, ctrl_green = 0xff, ctrl_blue = 0;
    Serial.printf("%s = %d\r\n", id_txt, val);
  }else if(!strcmp(id_txt, "blue")) {
    ctrl_red = 0, ctrl_green = 0, ctrl_blue = 0xff;
    Serial.printf("%s = %d\r\n", id_txt, val);
  }else if(!strcmp(id_txt, "change_endian")){
    if(isLittleEndian){
      isLittleEndian = false;
    }else{
      isLittleEndian = true;
    }
  }else if(!strcmp(id_txt, "start_stream")){
    canStartStream = true;
    isCloseConnection = false;
  }else if(!strcmp(id_txt, "stop_stream")){
    canStartStream = false;
    isCloseConnection = true;
    shouldClear = true;
  }else if(!strcmp(id_txt, "reset")){
    ESP.restart(); //ESP32強制リセット
  }else if(!strcmp(id_txt, "ping80")){
    Serial.println("---------ping receive");
  }else {
    res = -1;
  }
 
  if(res){
    return httpd_resp_send_500(req);
  }
 
  httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
  return httpd_resp_send(req, NULL, 0);
}
//****************************************
static esp_err_t index_handler(httpd_req_t *req){
  String html_body = "<!DOCTYPE html>\r\n";
          html_body += "<html><head></head><body>\r\n";
          html_body += "<img id='pic_place' width='200' height='148' style='border-style:solid; transform:scale(1, -1);'>\r\n";
          html_body += "<div>\r\n";
          html_body += "<p><button style='border-radius:25px;' onclick='startStream()'>Start Stream</button>\r\n";
          html_body += "<button style='border-radius:25px;' onclick='changeControl(\"stop_stream\",0)'>Stop Stream</button></p>\r\n";
          html_body += "<p><button style='border-radius:25px;' onclick='changeControl(\"change_endian\",0)'>Change Endian</button></p>\r\n";
          html_body += "<p><button style='border-radius:25px;' onclick='changeControl(\"red\",1)'>RED</button>\r\n";
          html_body += "<button style='border-radius:25px;' onclick='changeControl(\"green\",1)'>GREEN</button>\r\n";
          html_body += "<button style='border-radius:25px;' onclick='changeControl(\"blue\",1)'>BLUE</button></p>\r\n";
          html_body += "</div>\r\n";
          html_body += "<script>\r\n";
          html_body += "var base_url = document.location.origin;\r\n";
          html_body += "var url_stream = base_url + ':81';\r\n";
          html_body += "function startStream() {\r\n";
          html_body += "var pic = document.getElementById('pic_place');\r\n";
          html_body += "pic.src = url_stream+'/stream';\r\n";
          html_body += "changeControl('start_stream',0);};\r\n";
          html_body += "function changeControl(id_txt, value_txt){\r\n";
          html_body += "var new_url = base_url+'/control?id=';\r\n";
          html_body += "new_url += id_txt + '&';\r\n";
          html_body += "new_url += 'value=' + value_txt;\r\n";
          html_body += "fetch(new_url)\r\n";
          html_body += ".then((response) => {\r\n";
          html_body += "if(response.ok){return response.text();} \r\n";
          html_body += "else {throw new Error();}})\r\n";
          html_body += ".then((text) => console.log(text))\r\n";
          html_body += ".catch((error) => console.log(error));};\r\n";
          html_body += "</script></body></html>\r\n\r\n";
 
  httpd_resp_set_type(req, "text/html");
  httpd_resp_set_hdr(req, "Accept-Charset", "UTF-8");
  return httpd_resp_send(req, html_body.c_str(), html_body.length());
}
//*********************************************
boolean changeDrawCount(uint32_t now_time, uint32_t start_time, uint32_t stop_time){
  if((millis() - now_time > start_time) && (millis() - now_time < stop_time)){
    return true;
  }
  return false;
}
//*********************************************
void clearAll(){
  memset(bmp_data_buf + bmp_head_bytes, 0, disp_height_pix * max_w_pix_buf);
}
//*********************************************
void drawRectangleFill(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint8_t red, uint8_t green, uint8_t blue){
  uint8_t rgb565_msb = 0, rgb565_lsb = 0;
  convertRGB888toRGB565(isLittleEndian, red, green, blue, rgb565_msb, rgb565_lsb);
  for(int i = x0; i <= x1; i++){
    drawVerticalLine565(i, y0, y1, rgb565_msb, rgb565_lsb);
  }
}
//*********************************************
void drawHorizontalLine(uint16_t x0, uint16_t y0, uint16_t x1, uint8_t red, uint8_t green, uint8_t blue){
  uint8_t rgb565_msb = 0, rgb565_lsb = 0;
  convertRGB888toRGB565(isLittleEndian, red, green, blue, rgb565_msb, rgb565_lsb);
  drawHorizontalLine565(x0, y0, x1, rgb565_msb, rgb565_lsb);
}
//*********************************************
void drawHorizontalLine565(uint16_t x0, uint16_t y0, uint16_t x1, uint8_t rgb565_msb, uint8_t rgb565_lsb){
  judgeMaxPixel(x0, max_x);
  judgeMaxPixel(x1, max_x);
  judgeMaxPixel(y0, max_y);
  uint16_t p = 0;
  for(uint16_t i = x0; i <= x1; i++){
    p = bmp_head_bytes + y0 * max_w_pix_buf + i * 2;
    bmp_data_buf[p] = rgb565_lsb;
    bmp_data_buf[p + 1] = rgb565_msb;
  }
}
//*********************************************
void drawVerticalLine(uint16_t x0, uint16_t y0, uint16_t y1, uint8_t red, uint8_t green, uint8_t blue){
  uint8_t rgb565_msb = 0, rgb565_lsb = 0;
  convertRGB888toRGB565(isLittleEndian, red, green, blue, rgb565_msb, rgb565_lsb);
  drawVerticalLine565(x0, y0, y1, rgb565_msb, rgb565_lsb);
}
//*********************************************
void drawVerticalLine565(uint16_t x0, uint16_t y0, uint16_t y1, uint8_t rgb565_msb, uint8_t rgb565_lsb){
  judgeMaxPixel(x0, max_x);
  judgeMaxPixel(y0, max_y);
  judgeMaxPixel(y1, max_y);
  if(y0 > y1) std::swap(y0, y1);
  uint16_t p = 0;
  for(uint16_t i = y0; i <= y1; i++){
    p = bmp_head_bytes + i * max_w_pix_buf + x0 * 2;
    bmp_data_buf[p] = rgb565_lsb;
    bmp_data_buf[p + 1] = rgb565_msb;
  }
}
//*********************************************
void drawRectangleLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint8_t red, uint8_t green, uint8_t blue){
  uint8_t rgb565_msb = 0, rgb565_lsb = 0;
  convertRGB888toRGB565(isLittleEndian, red, green, blue, rgb565_msb, rgb565_lsb);
  drawHorizontalLine565(x0, y0, x1, rgb565_msb, rgb565_lsb);
  drawVerticalLine565(x0, y0, y1, rgb565_msb, rgb565_lsb);
  drawHorizontalLine565(x0, y1, x1, rgb565_msb, rgb565_lsb);
  drawVerticalLine565(x1, y0, y1, rgb565_msb, rgb565_lsb);
}
//*********************************************
void drawPixel(uint16_t x0, uint16_t y0, uint8_t red, uint8_t green, uint8_t blue){
  uint8_t rgb565_msb = 0, rgb565_lsb = 0;
  convertRGB888toRGB565(isLittleEndian, red, green, blue, rgb565_msb, rgb565_lsb);
  drawPixel565(x0, y0, rgb565_msb, rgb565_lsb);
}
//*********************************************
void drawPixel565(uint16_t x0, uint16_t y0, uint8_t rgb565_msb, uint8_t rgb565_lsb){
  judgeMaxPixel(x0, max_x);
  judgeMaxPixel(y0, max_y);
  uint16_t p = bmp_head_bytes + y0 * max_w_pix_buf + x0 * 2;
  bmp_data_buf[p] = rgb565_lsb;
  bmp_data_buf[p + 1] = rgb565_msb;
}
//*********************************************
void drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint8_t red, uint8_t green, uint8_t blue){
  judgeMaxPixel(x0, max_x);
  judgeMaxPixel(x1, max_x);
  judgeMaxPixel(y0, max_y);
  judgeMaxPixel(y1, max_y);
  uint8_t rgb565_msb = 0, rgb565_lsb = 0;
  convertRGB888toRGB565(isLittleEndian, red, green, blue, rgb565_msb, rgb565_lsb);
  if(x1 == x0) {
    drawVerticalLine565(x0, y0, y1, rgb565_msb, rgb565_lsb);
    return;
  }
  if(y1 == y0) {
    drawHorizontalLine565(x0, y0, x1, rgb565_msb, rgb565_lsb);
    return;
  }
  double sita = atan2((double)(y1 - y0), (double)(x1 - x0));
  int16_t y_new;
  int i = x0;
  while(true){
    y_new = (uint16_t)round((double)(i - x0) * tan(sita) + y0);
    if(y_new >= disp_height_pix) y_new = max_y;
    if(y_new - y_old > 1){
      drawVerticalLine565(i, y_old, y_new, rgb565_msb, rgb565_lsb);
    }else{
      drawPixel565(i, y_new, rgb565_msb, rgb565_lsb);
    }
    y_old = y_new;
    if(x1 > x0){
      i++;
      if(i >= disp_width_pix) break;
    }else{
      i--;
      if(i < 0) break;
    }
  }
}
//*********************************************
void convertRGB888toRGB565(bool little_endian, uint8_t red888, uint8_t green888, uint8_t blue888, uint8_t &rgb565_msb, uint8_t &rgb565_lsb){
  //RGB888をRGB565へ変換するには、下位ビットを削除するだけでOK。
  uint8_t red565 = red888 & 0b11111000;
  uint8_t green565 = green888 & 0b11111100;
  uint8_t blue565 = blue888 & 0b11111000;
  if(little_endian){
    rgb565_msb = red565 | (green565 >> 5);
    rgb565_lsb = (green565 << 3) | (blue565 >> 3);
  }else{
    rgb565_msb = (green565 << 3) | (blue565 >> 3);
    rgb565_lsb = red565 | (green565 >> 5);
  }
}
//*********************************************
void judgeMaxPixel(uint16_t &pix, uint16_t max_pix){
  if(pix > max_pix){
    //Serial.printf("Over Max pix = %d\r\n", pix);
    pix = max_pix;
  }
}
//*********************************************
void connectToWiFi(const char * ssid, const char * pwd){
  Serial.println("Connecting to WiFi network: " + String(ssid));
  WiFi.disconnect(true, true);
  delay(1000);
  WiFi.begin(ssid, password);
  Serial.println("Waiting for WIFI connection...");
  while ( WiFi.status() != WL_CONNECTED ) {
    delay(500);
    Serial.print(".");
  }
  IPAddress myIP = WiFi.localIP();
  Serial.println("WiFi connected!");
  Serial.print("My IP address: ");
  Serial.println(myIP);
  delay(1000);
}

コンパイル書き込み実行

では、まずご自分のWiFiルーターを起動し、M5StackとM5CameraがWiFi接続できるようにしておきます。

Arduino IDE の「ツール」メニューのESP32ボード設定は下図の様にします。
デフォルト設定で充分でした。

ボード:  ESP32 Dev Module
Upload Speed:  921600
CPU Frequency:  240MHz (WiFi/BT)
Flash Frequency:  80MHz
Flash Mode:  QIO
Flash Size:  4MB (32Mb)
Partition Scheme:  Default 4MB width spiffs (1.2MB APP/1.5MB SPIFFS)
Core Debug Level:  なし
PSRAM:  Disabled
シリアルポート:  ※ご自分の M5Camera のUSBポート
————————————-
書込装置: USBasp

 

次に、M5CameraとパソコンをUSB接続して、予めArduino IDE のシリアルモニターを115200bpsで起動しておきます。

そしたら、Arduino IDEでテスト用 MJPEG (BMP)動画送信スケッチをコンパイル書き込みします。
下図の様にシリアルモニターに表示されればOKです。

上図のように、WiFiネットワークのローカルIPアドレスが表示されたら、次のM5Stack側のスケッチに入力するので、メモっておきます。

では、次は受信側のM5Stackのスケッチ(プログラムソースコード)を紹介します。

コメント

タイトルとURLをコピーしました