LovyanGFXとJpgLoopAnimeでM5StackとM5Cameraの全画面WiFi動画ストリーミング実験

LovyanGFXとJpgLoopAnimeを使って、M5CameraとM5Stackの動画ストリーミング実験 M5Stack

M5Stack(受信側)の設定およびスケッチ

M5Stack側の設定は今までと異なる方法で、M5Stack_JpgLoopAnime ライブラリをスケッチフォルダに取り込む作業があります。

まずは、M5Stack (受信側)のスケッチ(プログラムソースコード)を入力して、名前を付けて保存する

まず、M5Stackに以下のスケッチをArduino IDEで入力して、名前を付けて保存しておきます。

16-17行目のところでご自分のWiFiルーターのSSIDとパスワードに書き換えてください。
(※このM5Stackが第三者の手に渡った場合、SSIDとパスワードはソフトウェアによって簡単に抜き取られてしまう可能性があることをご了承ください)

そして、19行目のローカルIPアドレスは、先ほど述べたM5CameraのIPアドレスですので、それに書き換えます。

/* The MIT License (MIT)
 * License URL: https://opensource.org/licenses/mit-license.php
 * Copyright (c) 2020 Mgo-tec. All rights reserved.
 *  
 * Use Arduino core for the ESP32 stable v1.0.4  
 * Use LovyanGFX ver 0.1.15
 * Use M5Stack library ver 0.3.0
 */
#include <M5Stack.h> //ver 0.3.0
#include <WiFi.h> 
#include "MainClass.h" //M5Stack_JpgLoopAnime https://github.com/lovyan03/M5Stack_JpgLoopAnime

static LGFX lcd;
static MainClass main;

const char* ssid = "xxxxxxxxx"; //ご自分のルーターのSSIDに書き換えてください
const char* password = "xxxxxxxxx"; //ご自分のルーターのパスワードに書き換えてください

const char* host = "192.168.0.11"; //M5Camera(相手先サーバー)のアドレス

enum SelCamCtrl {
  PING80 = 0,
  CHANGE_PCLK = 10,
  CHANGE_AEC = 20,
  CHANGE_FRAMESIZE = 100,
  CHANGE_JPG_QUALITY = 110,
  STOP_STREAM = 200, START_STREAM = 201, PAUSE_STREAM = 202,
  RESET_CAM = 255,
} SelectCamCtrl = PING80;

enum StateStream{
  OFF_STREAM = 0, ON_STREAM = 1, CLOSE_CONNECTION = 2
} StatusStream = OFF_STREAM;

uint8_t pclk_div_cnt = 4;
int8_t exposure_count = 0;
const char pclk_div_c[6][6] = {"div 2", "div 4", "div 8", "div16", "div32", "div64"};
const char aec_c[6][6] = {"Auto "," - 2 "," - 1 "," +-0 "," + 1 "," + 2 "};
const char stream_c[3][6] = {"Stop ", "Start", "Intrv"};
const uint8_t max_f_size_num = 6;
char framesize_c[max_f_size_num][10] = {" 96 x 96 ", "160 x 80", "160 x 120", "192 x 144", "240 x 176", "320 x 240"};
uint8_t frame_size_num = 5; //default 240 x 176

uint32_t ping80_lasttime = 0;
bool isWiFiConnected = false;
bool isPort80Handshake = false;
bool isPort81Handshake = false;
bool canSendCamCtrl = false;
bool canDisplayLCD = false;

uint8_t quality = 10;

bool canChangeDispFsize = false;
bool canDecodeJPG = false;

typedef struct {
  uint32_t jpg_len;
  uint8_t *jpg_buf;
  bool canReceiveJpg;
  bool hasReceiveJpg;
} recv_jpg_q_t;

static QueueHandle_t sem;
BaseType_t ret;
static uint8_t qidx = 0;
const uint8_t queue_max = 2;
recv_jpg_q_t qwrites[queue_max];
uint8_t now_qidx = 0;
uint32_t now_fps = 0;

//********CPU core 1 task********************
void setup(){
  for(int i = 0; i < queue_max; i++){
    recv_jpg_q_t *q = &qwrites[i];
    q->jpg_len = 0;
    q->canReceiveJpg = true;
    q->hasReceiveJpg = false;
  }

  M5.begin();

  lcd.begin();
  lcd.setRotation(0);
  if (lcd.width() < lcd.height())
    lcd.setRotation(1);
  lcd.setFont(&fonts::Font2);
  lcd.println("WiFi begin.");

  main.setup(&lcd);

  sem = xQueueCreate(queue_max, sizeof(recv_jpg_q_t*));

  TaskHandle_t taskClientCtrl_handl, taskClientStrm_handl;
  xTaskCreatePinnedToCore(&taskClientControl, "taskClientControl", 4096, NULL, 5, &taskClientCtrl_handl, 0);
  xTaskCreatePinnedToCore(&taskClientStream, "taskClientStream", 8192, NULL, 20, &taskClientStrm_handl, 0);

  while(!isWiFiConnected){
    Serial.print('.');
    delay(500);
  }

  lcd.println("WiFi connected! OK!");

  while(!isPort81Handshake){ //ESP32サーバーとのMJPEGハンドシェイクが終わるまで待つ
    M5.update();
    if (M5.BtnA.wasReleasefor(500)) {
      changeStateStreamBtnDisp();
    }
    delay(1);
  }

  lcd.startWrite();
}

void loop(){
  static recv_jpg_q_t *q = NULL;
  static uint32_t now_fps_time = 0;
  static uint8_t fps_count = 0;

  if (xQueueReceive(sem, &q, 0)){
    if(q->hasReceiveJpg){
      if(main.drawJpg(q->jpg_buf, q->jpg_len)){
        fps_count++;
      }
    }
    q->jpg_len = 0;
    q->hasReceiveJpg = false;
    q->canReceiveJpg = true;
    canDecodeJPG = false;
  }

  if(StatusStream == ON_STREAM){
    if(millis() - now_fps_time > 1000UL){
      Serial.printf("%d (fps)\r\n", fps_count);
      fps_count = 0;
      now_fps_time = millis();
    }
  }

  M5.update();
  if (M5.BtnA.wasReleased()) {
    Serial.println("Button A was Released");
    changeFrameSizeBtnDisp();
  }else if (M5.BtnA.wasReleasefor(500)) {
    changeStateStreamBtnDisp();
  }
  if (M5.BtnB.wasReleased()) {
    Serial.println("Button B was Released");
    changeQualityBtnDisp();
  }else if (M5.BtnB.wasReleasefor(500)) {
    SelectCamCtrl = RESET_CAM;
    canSendCamCtrl = true;
    Serial.println("Send [Reset]!!!");
  }
  if (M5.BtnC.wasReleased()) {
    Serial.println("Button C was Released");
    changeAEC();
  }else if (M5.BtnC.wasReleasefor(500)) {
    pclk_div_cnt++;
    if(pclk_div_cnt > 5){
      pclk_div_cnt = 0; 
    }
    SelectCamCtrl = CHANGE_PCLK;
    canSendCamCtrl = true;
  }
}
//********CPU core 0 task********************
void taskClientControl(void *pvParameters){
  connectToWiFi();
  while(!isWiFiConnected){
    delay(1);
  }
  while(true){
    //Serial.println("80");
    WiFiClient client80;
    connectClient80(client80);
    if (client80) {
      Serial.println("new client80");
      String get_request = "GET / HTTP/1.1\r\n";
      get_request += "Host: " + String(host);
      get_request += "\r\n";
      get_request += "Connection: keep-alive\r\n\r\n";

      while (client80.connected()){
        client80.print(get_request);
        get_request = "";
        String req_str;
        while(true){
          if(client80.available()){
            req_str =client80.readStringUntil('\n');
            if(req_str.indexOf("200 OK") > 0){
              Serial.println(req_str);
              req_str = "";
              if(!receiveToBlankLine(client80)){
                delay(1); continue;
              }
              Serial.println("complete server responce!");
              Serial.println("Go stream!");
              isPort80Handshake = true;
              ping80_lasttime = millis();
              while(true){
                changeCamControl(client80);
                sendPing80(client80, 60000);
                while(client80.available()){
                  Serial.write(client80.read()); //これは必要。これが無いとコマンドを送信できない。
                  delay(1);
                }
                if(!isPort80Handshake) break;
                delay(1);
              }
              goto exit_1;
            }
          }
          delay(1);
        }
        delay(1);
      }
    }
exit_1:
    while(client80.available()){
      Serial.write(client80.read());
      delay(1);
    }
    Serial.println("!!!!!!!!!!!!!!!!!!!! client80.stop");
    delay(10);
    client80.stop();
    client80.flush();
    delay(10);
  }
}
//********CPU core 0 task********************
void taskClientStream(void *pvParameters){
  while(true){
    //Serial.println("81");
    if(isPort80Handshake){
      WiFiClient client81;
      if(!connectStreamClient(client81)){
        canSendCamCtrl = true;
        Serial.println("Loop Out... wait client81 available");
        delay(10);
        client81.stop();
        client81.flush();
        Serial.println("!!!!!!!!!!!!!!!!!!!!! client81.stop");
        delay(10);
        SelectCamCtrl = PING80;
        isPort80Handshake = false;
        StatusStream = CLOSE_CONNECTION;
      }
    }
    delay(1);
  }
}
//********************************************
bool connectStreamClient(WiFiClient &client81){
  int stream_port = 81;
  while(true){
    if (!client81.connect(host, stream_port)) {
      Serial.print(',');
    }else{
      Serial.printf("client81.connected(2) %s\r\n", host);
      break;
    }
    delay(500);
  }

  String get_request = "GET /stream HTTP/1.1\r\n";
  get_request += "Host: " + String(host);
  get_request += ":81\r\n";
  get_request += "Connection: keep-alive\r\n\r\n";

  client81.print(get_request);
  get_request = "";
  if(!receiveToBlankLine(client81)){
    Serial.println("connectStreamClient FALSE");
    return false;
  }
  Serial.println("stream header receive OK!");
  isPort81Handshake = true;
  StatusStream = ON_STREAM;
  return receiveStream(client81);
}
//*********************************************
bool receiveStream(WiFiClient &client81){
  uint32_t time_out;
  uint32_t tmp_jpg_len = 0;

  while(StatusStream == ON_STREAM){
jpg_rcv0:
    if(!receiveBoundary(client81)){
      delay(1); continue;
    }
    if(client81.available()){
      time_out = millis();
      char tmp_cstr[5] = {};
      while(StatusStream == ON_STREAM){
        if((char)client81.read() == '\n') {
          client81.read((uint8_t *)tmp_cstr, 4);
          if(!strcmp(tmp_cstr, "\r\n\r\n")){
            String res_str = client81.readStringUntil('\n');

            while(canDecodeJPG){ //安定して受信するために必要なループ
              delay(1);
            }

            tmp_jpg_len = strtol(res_str.c_str(), NULL, 16);
            if(tmp_jpg_len) {
              receiveJpgData(client81, tmp_jpg_len);
              time_out = millis();
            }
            delay(1);
            goto jpg_rcv0;
          }
        }
        if(millis() - time_out > 1000UL){
          receiveBoundary(client81);
          time_out = millis();
          delay(1);
        }
      }
    }
    delay(1);
  }
  return false;
}
//********************************************
bool receiveJpgData(WiFiClient &client81, uint32_t tmp_jpg_len){
  recv_jpg_q_t *q = &qwrites[qidx];
  uint32_t time_out = millis();

  if(!canDisplayLCD){
    if(isPort81Handshake){
      while(StatusStream == ON_STREAM){
        if(client81.available()) {   
          //Serial.printf("before receive jpg heap=%d\r\n", esp_get_free_heap_size());
          q->jpg_len = tmp_jpg_len;
          if(q->jpg_buf != NULL) {
            free(q->jpg_buf);
          }
          q->jpg_buf = (uint8_t *)malloc(sizeof(uint8_t) * q->jpg_len);

          uint32_t ptr_addrs = 0;          
          uint32_t remain_bytes = q->jpg_len;
          int tmp = 0;
          while(true){
            if(StatusStream != ON_STREAM) return false;
            if(client81.available() > 0) {
              tmp = client81.read(&(q->jpg_buf[ptr_addrs]), remain_bytes);
              //Serial.printf("tmp=%d\r\n",tmp);
              if(tmp < 0){
                //Serial.println("-1");
                delay(1); continue;
              }
              ptr_addrs += tmp;
              remain_bytes = remain_bytes - tmp;
              if(remain_bytes <= 0) break;
            }
            delay(1);
          }

          if(q->jpg_len) {
            q->hasReceiveJpg = true;
          }
          q->canReceiveJpg = false;
          canDecodeJPG = true;

          //Serial.printf("QueWaiting = %d\n", uxQueueMessagesWaiting(sem));
          //Serial.printf("QueAvailable = %d\n", uxQueueSpacesAvailable(sem));

          if(xQueueSend(sem, &q, 70)){
            qidx = (1 + qidx) % queue_max;
          }else{
            Serial.println("Failed xQueueSend");
          }
          return true;
        }
        delay(1);
      }
    }
  }
  return false;
}
//********************************************
bool receiveBoundary(WiFiClient &client81){
  char cstr[16] = {};
  while(StatusStream == ON_STREAM){
    if(client81.available()){
      if((char)client81.read() == '-'){
        if(client81.read((uint8_t*)cstr, 15)){
          //Serial.write(cstr);
          //Serial.println();
          if(strcmp(cstr, "-myboundary\r\n\r\n") == 0){
            return true;
          }
        }else{
          delay(1); continue;
        }
      }else{
        delay(1); continue;
      }
    }
    delay(1);
  }
  return false;
}
//********************************************
void connectClient80(WiFiClient &client80){
  int httpPort = 80;
  while(true){
    if(SelectCamCtrl == START_STREAM){
      if (client80.connect(host, httpPort)) {
        Serial.printf("client80.connected! %s\r\n", host);
        canDisplayLCD = false;
        break;
      }else{
        Serial.print(',');
      }
    }
    delay(1);
  }
}
//********************************************
void sendPing80(WiFiClient &client80, uint32_t interval_time){
  if((millis() - ping80_lasttime) > interval_time){
    sendCamCtrlRequest(client80, "ping80", "0");
    ping80_lasttime = millis();
  }
}
//********************************************
void changeCamControl(WiFiClient &client80){
  if(canSendCamCtrl){
    String id_str = "", val_str = "0";
    switch(SelectCamCtrl){
      case CHANGE_PCLK:
        id_str = "pclk_div", val_str = String(pclk_div_cnt);
        break;
      case CHANGE_AEC:
        id_str = "aec", val_str = String(exposure_count);
        break;
      case CHANGE_FRAMESIZE:
        id_str = "framesize";
        val_str = String(frame_size_num);
        break;
      case CHANGE_JPG_QUALITY:
        id_str = "quality", val_str = String(quality);
        break;
      case STOP_STREAM:
        id_str = "stop_stream";
        StatusStream = OFF_STREAM;
        isPort80Handshake = false;
        break;
      case START_STREAM:
        id_str = "start_stream";
        break;
      case RESET_CAM:
        id_str = "reset";
        break;
      default:
        id_str = "ping80";
        break;
    }
    sendCamCtrlRequest(client80, id_str, val_str);
    SelectCamCtrl = PING80;
  }
}
//********************************************
void sendCamCtrlRequest(WiFiClient &client80, String id_str, String val_str){
  String get_request = "GET /control?var=" + id_str;
  get_request += "&val=" + val_str;
  get_request += " HTTP/1.1\r\n";
  get_request += "Host: " + String(host);
  get_request += "\r\nConnection: keep-alive\r\n\r\n";
  client80.print(get_request);
  Serial.print(get_request);
  get_request = "";
  id_str = "";
  val_str = "";
  receiveToBlankLine(client80);
  canSendCamCtrl = false;
}
//*********************************************
bool receiveToBlankLine(WiFiClient &client){
  String req_str = "";
  uint32_t time_out = millis();
  while(StatusStream == ON_STREAM){
    if(client.available()){
      req_str =client.readStringUntil('\n');
      //Serial.println(req_str);
      if(req_str.indexOf("\r") == 0) return true;
    }
    if(millis() - time_out > 10000) {
      Serial.println("--------error Time OUT receiveToBlankLine");
      break;
    }
    delay(1);
  }
  return false;
}
//********************************************
void connectToWiFi(){
  Serial.println("Connecting to WiFi network: " + String(ssid));
  WiFi.disconnect(true, true);
  delay(1000);
  WiFi.onEvent(WiFiEvent);
  WiFi.begin(ssid, password);
  Serial.println("Waiting for WIFI connection...");
}

void WiFiEvent(WiFiEvent_t event){
  switch(event) {
    case SYSTEM_EVENT_STA_GOT_IP:
      Serial.println("WiFi connected!");
      Serial.print("My IP address: ");
      Serial.println(WiFi.localIP());
      delay(1000);
      isWiFiConnected = true;
      break;
    case SYSTEM_EVENT_STA_DISCONNECTED:
      Serial.println("WiFi lost connection");
      isWiFiConnected = false;
      break;
    default:
      break;
  }
}
//********************************************
void changeStateStreamBtnDisp(){
  if(StatusStream == OFF_STREAM || StatusStream == CLOSE_CONNECTION){
    SelectCamCtrl = START_STREAM;
    StatusStream = ON_STREAM;
  }else if(StatusStream == ON_STREAM){
    SelectCamCtrl = STOP_STREAM;
    StatusStream = CLOSE_CONNECTION;
  }
  canSendCamCtrl = true;
  Serial.printf("SelCamCtrl=%d\r\n", (uint8_t)SelectCamCtrl);
}
//********************************************
void changeQualityBtnDisp(){
  quality += 10;
  if(quality > 63) quality = 10;
  Serial.printf("Qs=%d\r\n", quality);
  SelectCamCtrl = CHANGE_JPG_QUALITY;
  canSendCamCtrl = true;
}
//********************************************
void changeFrameSizeBtnDisp(){
  frame_size_num++;
  if(frame_size_num >= max_f_size_num) frame_size_num = 0;
  SelectCamCtrl = CHANGE_FRAMESIZE;
  canSendCamCtrl = true;
  Serial.println("Send CHANGE_FRAMESIZE");
}
//********************************************
void changeAEC(){
  exposure_count++;
  if(exposure_count > 5) exposure_count = 0;
  if(exposure_count == 0) {
    Serial.println("exposure=AUTO");
  }else{
    Serial.printf("exposure=%d\r\n", exposure_count);
  }
  SelectCamCtrl = CHANGE_AEC;
  canSendCamCtrl = true;
}

M5Stack (受信側)スケッチフォルダにM5Stack_JpgLoopAnime ライブラリファイル群をコピペする

ここは今までとちょっと方法が異なります。

らびやん さん作成の M5Stack_JpgLoopAnime ライブラリを少々修正したいので、先に作成したM5Stack側スケッチのフォルダに、M5Stack_JpgLoopAnime ファイル群をコピー&ペーストします。

まず、先ほど紹介した方法でダウンロードしたZIPファイルを予め解凍して、その中の「src」フォルダの中の以下の3つのファイルをコピーします。

MainClass.h
tjpgdClass.cpp
tjpgdClass.h

そして、下図の様に、先ほど入力したM5Stack (受信側)スケッチのフォルダの中にペースト(貼り付け)します。
Arduinoスケッチフォルダのある場所は、Windows10の場合、デフォルトでは以下のパスにあります。

C:\Users\ご自分のユーザー名\Documents\Arduino\

その後、Arduino IDE でスケッチを起動すると、下図の様に3つのファイルのタブが表示されていればOKです。

MainClass.h のjpgWriteRow関数を少々変更する

M5Stackのボタン操作で、画角(フレームサイズ)を変更できるようにします。 全画面以外使わない場合、ここの設定は不要です。 これはらびやん さんに、ここを変更すれば良いと教えて頂いた箇所です。 下図の様に、MainClass.h タブを選択して、jpgWriteRow関数の箇所を見ます。 100行目あたりにあると思います。
MIT License: Copyright (c) 2019 lovyan03 )

その部分を以下のように変更します。 103~104行のところを追加変更しただけです。
  static uint32_t jpgWriteRow(TJpgD *jdec, uint32_t y, uint32_t h) {
    static int flip = 0;
    MainClass* me = (MainClass*)jdec->device;
    //-------ここから変更---------
    static uint16_t old_w = 320;
    static uint16_t old_h = 240;
    if (y == 0){
      if(old_w != jdec->width || old_h != jdec->height){
        me->_lcd->clear();
        old_w = jdec->width;
        old_h = jdec->height;
      }
      me->_lcd->setAddrWindow(me->_jpg_x, me->_jpg_y, jdec->width, jdec->height);
    }
    //-------ここまで------------
    me->_lcd->pushPixelsDMA(me->_dmabuf, jdec->width * h);

    flip = !flip;
    me->_dmabuf = me->_dmabufs[flip];
    return 1;
  }

変更したら、上書き保存しておきます。

old_w や old_h という変数の前に static修飾子が付いています。
後で詳しく述べていますが、ローカル関数内でstatic修飾子を使うと、グローバル関数を定義しなくて良いので、とっても便利です。

コンパイル書き込み実行

では、Arduino IDE のボード設定を以下のようにして、コンパイル書き込みします。

ボード:  M5Stack-Core-ESP32
Upload Speed:  921600
Flash Frequency:  80MHz
Flash Mode:  QIO
Partition Scheme:  初期値
Core Debug Level:  なし
シリアルポート: ※ご自分の M5Stack のUSBポート

 

では、次では操作方法やその他学んだことを紹介します。

コメント

  1. なかむら より:

    はじめまして。私はM5stackを楽しんでいる初心者です。
    プログラミングは苦手で、ほとんどはサンプルを少し弄る
    程度です。

    貴サイトの動画表示記事に興味があり、TimwerCameraFとM5stack(Basic)で
    チャレンジしています。
    ですが、ソースコードをarduino1.8.19にてコンパイルしているのですが
    下記のエラーになります。

    >exit status 1
    >’LGFX’ does not name a type

    エラー個所は
    >15:static LGFX lcd;
    です。

    ソースファイルと3つのヘッダファイルは同じフォルダに置いてます。

    何か原因※ありますでしょうか?。
    アドバイスを頂ければ幸いです。

    ※arduino2.0 IDEは、「ping timeout」でコンパイルが止まります。

    • mgo-tec mgo-tec より:

      なかむらさん

      記事をご覧いただき、ありがとうございます。

      初心者なのに、こんな難解なコードにチャレンジされたというのには、正直驚いています。

      今の私はESP32やM5Stackから離れて、まったく触っていませんので、かなり忘れてしまいました。
      ですから、あまり的確なアドバイスできないかも知れません。

      まず、TimwerCameraF とは、
      ESP32 PSRAM Timer Camera F (OV3660)
      のことでしょうか?
      ご存知かと思いますが、この記事はM5Camera(OV2640)を使っている為、OV3660で動くかどうかは私には分かりません。

      また、ご使用の環境が不明なことが多いので、
      Arduino core for the ESP32 のバージョンや、M5Stackライブラリ、LovyanGFXライブラリのバージョンはどれをお使いでしょうか?

      Arduino core for the ESP32は、1.0.4 で検証していますので、それを使って下さい。
      また、M5Stackライブラリのバージョンも同様に、0.3.0 を使って下さい。
      LovyanGFXライブラリも同様に 0.1.15 を使って下さい。

      ライブラリは最新バージョンでは動かない可能性が大きいです。

  2. なかむら より:

    丁寧なご回答をありがとうございます。
    説明不足で申し訳ありません。
    カメラは、ESP32 PSRAM Timer Camera F (OV3660)です。
    ご指摘の通りセンサーが異なるので、動作しなくても仕方ないです。
    ※ブラウザでは表示できていますが。

    ライブラリのバージョンは気にしていませんでした。

    ・M5Stackのライブラリは0.3.0でした。
    ・ボードマネージャ Arduino core for the ESP32
     1.0.4→これはちょっとわかりませんでした。
     
    ・LovyanLGFXのライブラリを1.1.12(最新)→0.1.15 に変更したところ
     先のエラーは消えました。
     が、新たなエラーになりました。

    >cannot declare ‘::main’ to be a global variable

    >16 static MainClass main;

    >C:\Users\chuhy\Documents\Arduino\TImerCameraF-viewer\TImerCameraF-viewer.ino: >At global scope:
    >TImerCameraF-viewer:16:18: error: cannot declare ‘::main’ to be a global >>>>variable static MainClass main;

    うーん、難しいですね。
    これ以上は自分の技量では追えそうにないので諦めます。
    お忙しい中、ありがとうございました。

    • mgo-tec mgo-tec より:

      なかむらさん

      以下の件、
      >・ボードマネージャ Arduino core for the ESP32
      >1.0.4→これはちょっとわかりませんでした。

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

      Arduino IDE の環境設定で、「追加のボードマネージャのURL」という欄にURLを貼り付けて、「OK」をクリックします。
      そして、ボードマネージャの検索欄に「esp32」と入力すれば、「esp32 by Espressif Systems」という項目が見えます。
      その「バージョンを選択」で 1.0.4 を選択してインストールし直してみて下さい。
      うまくいかなかったら、今のバージョンを一度「削除」ボタンを押して削除してから、Arduino IDEを閉じて、再起動してから、1.0.4をインストールしてみてください。

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