PR

ESP32-WROOM-32Eでアクセスポイントとサーバを実装する

組み込みエンジニア
本記事はプロモーションが含まれています。

こんにちは、ENGかぴです。

ESP32-WROOM-32Eはアクセスポイントの設定とサーバーの設定の2つを行うことで双方の機能を実装して使用することができます。ブラウザからアクセスポイントで接続してサーバーのIPを設定してサーバーを使用する例をまとめました。

クライアントからの接続要求に応じたレスポンスを返信するためにWebServerを使用します。下記記事にWebServerを使ってレスポンスを行う方法をまとめています。

ESP32-WROOM-32EでWebServerを実装する

サーバーで使用するのが基本でアクセスポイントはIPは固定することで設定項目が不明でサーバーに接続できなくなることを防ぐための補助のイメージです。動作確認としてBME280から取得したデータをブラウザー上に表示します。

ESP32-WROOM-32E開発ボード(秋月電子)を使用しArduino IDEで開発を行います。またAE-BME280(秋月電子)を使用しています。 アクセスポイントにはWZR-HP-G300NH(バッファロー:生産中止)を使用しています。

ESP32-WROOM-32Eを使って動作確認したことをまとめています。

ESP32-WROOM-32Eで学べるソフト開発と標準ライブラリの使い方

WiFiのアクセスポイントとサーバーを実装

アクセスポイントとサーバーを同時に使用するイメージ図
アクセスポイントとサーバーを同時に使用するイメージ図

WiFiライブラリを使用するとアクセスポイントとサーバーのそれぞれを設定するとアクセスポイントとサーバーの双方の動作が実現できます。

サーバー(ステーションモード)でアクセスポイントに接続してブラウザーからのリクエストを待ちます。アクセスポイントはスマホなどの端末から接続要求を待ちます。

アクセスポイントのIPを固定にすることでサーバー設定したIPなどの情報が不明な場合にアクセスポイントで接続してサーバー情報を確認することができるようになります。

モードの選択(参考資料)

WiFi.mode()は特に意識しなくても動作させることができますが参考として記述します。

WiFi.mode(WIFI_AP_STA);

WiFi.mode()でWIFI_APを指定するとアクセスポイントモードになります。WIFI_STAを指定するとステーションモードとなりサーバー動作となります。WIFI_AP_STAを指定するとアクセスポイント+ステーションモードになります。

サーバーとして動作させるWiFi.begin()によってステーションモードが付加されます。アクセスポイントとして動作させるWiFi.softAP()によってアクセスポイントモードが付加されるためモードの指定を行う必要はありません。

広告
マイベスト3年連続1位を獲得した実績を持つ実践型のプログラミングスクール

アクセスポイントの設定

void setup() {

  WiFi.softAP(ssidacp, passacp,1,0,1);  //SSIDとバスの設定
  WiFi.softAPConfig(ip, ip, subnet); //IP及びサブネットマスクの設定
}

WiFi.softAP()で第1引数と第2引数にアクセスポイント名とパスワードを設定します。

第3引数はチャンネル番号を1~13chで指定します。デフォルトでは1chを使用するようになっています。

第4引数はSSIDを全体に通知するかの設定を行います。デフォルトではブロードキャストになっており接続範囲であればSSIDが通知されます。SSIDを隠したい場合は0を指定します。デフォルトでは1で通知になっています。

第5引数は最大で接続できるクライアント数です。1~4を指定します。デフォルトでは4台になっています。例では1台を指定しています。

第3引数から第5引数までは変更しない場合は特に設定する必要はありません。

WiFi.softAPConfig()の第1引数にIPアドレスをしてします。第2引数はゲートウェイを指定します。ゲートウェイを使用しない場合はIPアドレスを設定します。第3引数はサブネットマスクを指定します。

広告

サーバの設定

#include <WiFi.h>

void setup() {

  FfsRead(); //フラッシュに保存したサーバー情報を読み込み
  if (ipset == "MANUAL") {
    IPAddress ip(ipadr1.toInt(), ipadr2.toInt(), ipadr3.toInt(), ipadr4.toInt()); //IPアドレスセット
    IPAddress gateway(gwadr1.toInt(), gwadr2.toInt(), gwadr3.toInt(), gwadr4.toInt()); //GATEWAYアドレスセット
    IPAddress subnet(255, 255, 255, 0); //サブネットマスクは固定
    Serial.println(ip);
    Serial.println(gateway);
    WiFi.config(ip, gateway, subnet);
  }
  delay(100);
  WiFi.begin(ssid.c_str(), pass.c_str());
  uint16_t wait = SERVER_WAIT;
  //サーバー接続待ち
  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.print(wait);
    Serial.print("..");
    if ( --wait <= 0 ) {
      //yield();
      //ESP.restart();    //設定時間を過ぎたら強制ソフトリセットを行う
      break;
    }
  }
}

WiFiライブラリをインクルードするとWiFiの設定に関するライブラリが使用できるようになります。サーバーの接続先のアクセスポイントのSSIDとパスワードの文字列をWiFi.begin()の第1引数と第2引数で指定してサーバーをスタートします。

例ではSSIDやパスワードを含めた条件をフラッシュに保存したデータを展開して設定を行っています。

ipsetはDHCPを使用するかを判断しています。IPやサブネットマスクなどの条件を固定する場合はフラッシュから展開したデータをIPAddressの型に置き換えてWiFi.config()で条件を指定します。DHCPを使用しない場合は自動でIPが割り当たられるためIPを確認する仕組みが必要です。

WiFi.begin()でサーバー動作がスタートするとサーバーの接続先のアクセスポイントに接続要求を出します。接続が確立するまで数秒間必要なので接続が確立するまで1秒おきに”.”をモニター表示して待ちます。一定時間経過しても接続されない場合はループから抜けるようにしています。

アクセスポイントに接続出来たら接続したIPをシリアルモニタに表示しています。IPアドレスを固定していない場合はアクセスポイントが割り振ったIPアドレスになります。

スポンサーリンク

サーバーの再接続

サーバーでアクセスポイントに接続していても通信環境が悪くなり接続が切れることがあります。一度接続が切れると再接続しない限り通信ができなくなるため一定間隔で接続状態を確認します。

if(WiFi.isConnected()){ //WiFi接続有り
  wifiRetry.recnt = 0;
  wifiRetry.cutflg = false;
  wifiRetry.waitcnt = 0;
  wifiRetry.disconflg = true;
}
else{ //WiFi接続無し
  if( ++wifiRetry.waitcnt >= WIFI_CHK_WAIT){
    //任意の回数経過したら再接続する
    wifiRetry.waitcnt = 0;
                
    if( WiFi.softAPgetStationNum() == 0){ 
      //アクセスポイントに接続していない場合再接続
      wifiRetry.cutflg = true;
      WiFi.reconnect();
    }
    else{
      wifiRetry.cutflg = false;
      if(wifiRetry.disconflg ){
         WiFi.disconnect(); //サーバーの接続を一旦切る
         wifiRetry.disconflg = false;
      }
    }
        
    if( wifiRetry.cutflg){
      if( ++wifiRetry.recnt >= WIFI_RESTART_MAX){
        ESP.restart();  //リスタート
      }
    }
  }
}

WiFiの接続状態を確認するためにWiFi.isConnected()関数でESP32-WROOM-32Eと接続先のアクセスポイント間の接続を確認します。接続が切れている場合は再接続のための処理を行います。

サーバーで使用することを基本としていますがESP32-WROOM-32Eをアクセスポイントとして使用している場合は再接続しないようにWiFi.softAPgetStationNum()関数でアクセスポイントが接続されていないかを確認しています。

WiFi.softAPgetStationNum()関数は接続しているアクセスポイントの台数が取得できます。アクセスポイントで使用している場合でもサーバーの再接続を行っても問題ありませんが、サーバーに接続できない状態が続くと処理が重たくなることがあったのでサーバー一旦停止して再接続しないようにしています。

アクセスポイントで使用していない場合はWiFi.reconnect()で再接続を行います。再接続には少し時間がかかるため任意の回数が経過した場合に接続の確認を行うようにしています。

再接続を複数回繰り返しても接続が復帰しない場合はモジュールをESP.restart()関数でリスタートして状態の復帰を試みます。アクセスポイントとの接続に問題がなければ通常通りWebサーバーでクライアントからの接続を待ちます。

PR:技術系の通信教育講座ならJTEX

動作確認

動作確認用の回路図
動作確認用の回路図

ESP32-WROOM-32EとAE-BME280の配線例を示しています。回路図の番号はESP32 -WROOM-32Eの左上を1ピンとした時反時計回りにピンを数えた場合の番号としています。ピン番号横の()内の番号はシルク印刷されているピンの名称です。

アクセスポイント(WZR-HP-G300NH)をルータとして動作し、ESP32-WROOM-32Eからの接続要求を待ちます。ESP32-WROOM-32Eは電源ONするとアクセスポイントに対して接続要求し、接続待機します。接続が成功すると接続待機をやめて次の処理に遷移します。

ESP32-WROOM-32Eのサーバー接続はソフトを書き込んだ初期の状態ではフラッシュにデータが保存されていないためデフォルト値でスタートするためIPが「192.168.11.2」になっています。

ESP32-WROOM-32Eのアクセスポイントで接続してサーバーの設定条件を変更します。アクセスポイントのIPは「192.168.100.1」に固定しているのでWiFiのネットワーク(アクセスポイント)を変更してアクセスポイントのIPでリクエストを送信します。

アクセスポイントでの動作
アクセスポイントでの動作
設定項目のページ
設定項目のページ

サーバーの設定の変更はIPアドレスに/settingを加えたURL(192.168.100.1/setting)指定をすると設定項目の画面に遷移します。

SSIDとPASSはお使いのアクセスポイントのものを指定します。IPとゲートウェイアドレスをデフォルト値の「192.168.11.2」から「192.168.11.4」に変更します。設定後はESP32-WROOM-32Eをリスタートすると設定項目が反映された状態でサーバーがスタートします。

次にアクセスポイント(WZR-HP-G300NH)にWiFiを接続して設定したIPアドレスでリクエストを送信します。

サーバーでの動作
サーバーでの動作

設定項目で設定したIPアドレスで接続することができています。BME280から取得した温湿度と気圧のデータが表示されています。5秒毎にページをリフレッシュするようにしていますが、データの表示が更新されているのも確認しています。

初期設定をESP32-WROOM-32Eのアクセスポイントを使ってサーバーの設定を行い以降は設定したIPを使ってサーバーで運用するような使い方をする場合に有効な方法だと思います。

スポンサーリンク

ソースコード全体

以下のソースコードはコンパイルして動作確認をしております。コメントなど細かな部分で間違っていたりやライブラリの更新などにより動作しなくなったりする可能性があります。参考としてお使いいただければと思います。

#include <FS.h>
#include <SPIFFS.h>
#include <Wire.h>
#include <WebServer.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>

#define FFS_HEAD "Setting_FLG"
#define SERVER_WAIT 60
#define TIM_BME280_WAIT 50
#define TIME_UP 0
#define TIME_OFF -1
#define BASE_CNT 10
#define WIFI_CONNECT_WAIT 100
#define WIFI_CHK_WAIT 60
#define WIFI_RESTART_MAX 60 //1時間Wifi未接続でリスタート

const char *ssidacp = "EngKapi1"; //SSID
const char *passacp = "22223333"; //password
String settings = "/settings_dat.txt"; //フラッシュに置くテキストファイル

const IPAddress ipacp(192,168,100,1); //IPアドレス
const IPAddress subnetacp(255,255,255,0); //サブネットマスク

typedef struct{
  bool  cutflg;
  bool  disconflg;
  uint16_t recnt;
  uint16_t waitcnt;
}WIFI_MNG;

/* 変数の宣言 */
WebServer Wserver(80);
String ssid;
String pass;
String ipset;
String ipadr1; String ipadr2; String ipadr3; String ipadr4;
String gwadr1; String gwadr2; String gwadr3; String gwadr4;
uint32_t timer= millis();
Adafruit_BME280 bme280;
int16_t timBme280Set;
int16_t timConnectChk;
float bme280data[3];
WIFI_MNG  wifiRetry;

/* プロトタイプ宣言 */
void mainTimer(void);
void FfsRead(void);
void FfsWrite(void);
void Connect(void);
void HtmlBme280(void);
void HtmlSetGet(void);
void HtmlSetPost(void);
void handleNotFound(void);
void Bme280Get(void);

void setup() {

  Serial.begin(115200);
  SPIFFS.begin();
  FfsRead();

  //WiFi.mode(WIFI_AP_STA);
  WiFi.disconnect();  //WiFi接続情報をクリア
  if (ipset == "MANUAL") {
    IPAddress ip(ipadr1.toInt(), ipadr2.toInt(), ipadr3.toInt(), ipadr4.toInt()); //IPアドレスセット
    IPAddress gateway(gwadr1.toInt(), gwadr2.toInt(), gwadr3.toInt(), gwadr4.toInt()); //GATEWAYアドレスセット
    IPAddress subnet(255, 255, 255, 0); //サブネットマスクは固定
    Serial.println(ip);
    Serial.println(gateway);
    WiFi.config(ip, gateway, subnet);
  }
  delay(100);
  WiFi.begin(ssid.c_str(), pass.c_str());
  uint16_t wait = SERVER_WAIT;
  //サーバー接続待ち
  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.print(wait);
    Serial.print("..");
    if ( --wait <= 0 ) {
      //yield();
      //ESP.restart();    //設定時間を過ぎたら強制ソフトリセットを行う
      break;
    }
  }

  /* アクセスポイントの設定 */
  WiFi.softAP(ssidacp, passacp,1,0,1);  //SSIDとバスの設定
  WiFi.softAPConfig(ipacp,ipacp,subnetacp);  //IPアドレス、ゲートウェイ、サブネットマスク
  Wserver.on("/",HTTP_GET,HtmlBme280);
  Wserver.on("/setting", HTTP_GET, HtmlSetGet);
  Wserver.on("/setting", HTTP_POST, HtmlSetPost);
  Wserver.onNotFound(handleNotFound);
  Wserver.begin();
  bme280.begin();
}

void loop() {
  Wserver.handleClient();
  mainTimer();

  if( timBme280Set == TIME_UP ){
    timBme280Set = TIM_BME280_WAIT;
    Bme280Get();
  }

  if( timConnectChk == TIME_UP ){
    timConnectChk = WIFI_CONNECT_WAIT;
    
    if(WiFi.isConnected()){ //WiFi接続有り
      wifiRetry.recnt = 0;
      wifiRetry.cutflg = false;
      wifiRetry.waitcnt = 0;
      wifiRetry.disconflg = true;
    }
    else{                   //WiFi接続無し
      if( ++wifiRetry.waitcnt >= WIFI_CHK_WAIT){
        wifiRetry.waitcnt = 0;
                
        if( WiFi.softAPgetStationNum() == 0){
          wifiRetry.cutflg = true;
          WiFi.reconnect();
        }
        else{
          wifiRetry.cutflg = false;
          if(wifiRetry.disconflg ){
            WiFi.disconnect();
            wifiRetry.disconflg = false;
          }
        }
        
        if( wifiRetry.cutflg){
          if( ++wifiRetry.recnt >= WIFI_RESTART_MAX){
            ESP.restart();  //1時間WiFi未接続でリスタート
          }
        }
      }
    }
  }
}
/* タイマ管理 */
void mainTimer(void){

  if( millis() - timer >= BASE_CNT ){
    timer = millis();

    if( timBme280Set > TIME_UP ){
      --timBme280Set;
    }
    if( timConnectChk > TIME_UP ){
      --timConnectChk;
    }
  }
}
/* BME280のデータ取得 */
void Bme280Get(void){

  Serial.print("Temperature = ");
  bme280data[0] = bme280.readTemperature();
  Serial.print(bme280data[0]);
  Serial.println(" °C");

  Serial.print("Pressure = ");
  bme280data[1] = bme280.readPressure() / 100.0F;
  Serial.print(bme280data[1]);
  Serial.println(" hPa");

  Serial.print("Humidity = ");
  bme280data[2] = bme280.readHumidity();
  Serial.print(bme280data[2]);
  Serial.println(" %");
  Serial.println();
}

/* FFSデータ書き込み処理 */
void FfsWrite(void){

  File fp = SPIFFS.open(settings,FILE_WRITE);
  if(fp){
    fp.println(FFS_HEAD);
    fp.println(ssid);
    fp.println(pass);
    fp.println(ipset);
    fp.println(ipadr1);
    fp.println(ipadr2);
    fp.println(ipadr3);
    fp.println(ipadr4);
    fp.println(gwadr1);
    fp.println(gwadr2);
    fp.println(gwadr3);
    fp.println(gwadr4);
    fp.close();
  }
  else{
    Serial.println("Writting Error");
  }
}
/* FFSデータ読み込み処理 */
void FfsRead(void){
  String str;
  bool initflg = false;
  
  File fp = SPIFFS.open(settings,FILE_READ);

  if(fp){
    str =fp.readStringUntil('\n');
    str.trim();
    if( strcmp(str.c_str(),FFS_HEAD) == 0 ){
      ssid = fp.readStringUntil('\n');
      pass = fp.readStringUntil('\n');
      ipset = fp.readStringUntil('\n');
      ipadr1 = fp.readStringUntil('\n');
      ipadr2 = fp.readStringUntil('\n');
      ipadr3 = fp.readStringUntil('\n');
      ipadr4 = fp.readStringUntil('\n');
      gwadr1 = fp.readStringUntil('\n');
      gwadr2 = fp.readStringUntil('\n');
      gwadr3 = fp.readStringUntil('\n');
      gwadr4 = fp.readStringUntil('\n');
    }
    else{
      initflg = true;
      Serial.println("R ERR");
    }
    fp.close();
  }
//初期値を入れる
  if(initflg){
    ssid = "engkapi3"; //使用するアクセスポイントのSSID
    pass = "11114444"; //使用するアクセスポイントのPASSWORD
    ipset = "MANUAL";   
    ipadr1 = "192";
    ipadr2 = "168";
    ipadr3 = "11";
    ipadr4 = "2";
    gwadr1 = "192";
    gwadr2 = "168";
    gwadr3 = "11";
    gwadr4 = "2";
  }
  //読込みデータのNull文字削除
  ssid.trim();
  pass.trim();
  ipset.trim();
  ipadr1.trim();
  ipadr2.trim();
  ipadr3.trim();
  ipadr4.trim();
  gwadr1.trim();
  gwadr2.trim();  
  gwadr3.trim(); 
  gwadr4.trim();
}
/* クライアントに返信するhtmlデータを生成 */
void HtmlBme280(void){
  String str = "";

  str += "<html lang=\"ja\">";
  str += "<head>";
  str += "<meta http-equiv=\"refresh\" content=\"5\">";
  str += "<meta charset=\"UTF-8\">";
  str += "<title>Sensor BME280</title>";
  str += "</head>";
  str += "<body>";
  str += "<h1>BME280温湿度気圧センサ</h1>";
  str += "<h2>WebServerライブラリを使用</h2>";
  str += "<h2>温度: ";
  str += bme280data[0];
  str += "℃";
  str += "</h2>";
  str += "<h2>湿度: ";
  str += bme280data[2];
  str += "%RH";
  str += "</h2>";
  str += "<h2>気圧: ";
  str += bme280data[1];
  str += "hPa";
  str += "</h2>";
  str += "</body>";
  str += "</html>";

  Wserver.send(200,"text/html", str); 
  //HTTPレスポンス200でhtmlデータとして送信
}

/* 設定したIPアドレスで表示するページ */
void HtmlSetGet(void) {
  String html = "";
  html += "<!DOCTYPE html>";
  html += "<html lang=\"ja\">";
  html += "<head>";
  html += "<meta charset=\"UTF-8\">";
  html += "<title>Settings</title>";
  html += "<h2>設定項目</h2>";
  html += "</head>";
  html += "<body>";
  html += "<form method='post'>";
  html += "SSID: ";   //SSID
  html += "<input type='text' name='ssid' value='" + ssid + "' placeholder='EngKapi1'style ='width:200px;font-size:20px;'>";
  html += "<br>";
  html += " PASSWORD: ";   //パスワード
  html += "<input type='text' name='pass' value='" + pass + "' placeholder='22223333'style ='width:200px;font-size:20px;'>";
  html += "<br><br>";
  //IP取得設定
  html += "IPアドレス設定方法: ";
  html += "<input type='radio' name='ipset' value='DHCP' ";
  if(ipset == "DHCP") {
  html += "checked='checked'"; }
  html += ">自動(DHCP) ";
  html += "<input type='radio' name='ipset' value='MANUAL' ";
  if(ipset == "MANUAL") {
  html += "checked='checked'"; }
  html += ">手動 ";
  html += "<br><br>";
  html += "<h4>手動の場合はIPアドレス及びゲートウェイアドレスの設定が必要です。</h4>";
  //装置IPアドレス設定
  html += "IPアドレス:  ";
  html += "<input type='text' name='ipadr1' value='" + ipadr1 + "' placeholder='192' style ='width:50px;font-size:20px;'>";
  html += ".";
  html += "<input type='text' name='ipadr2' value='" + ipadr2 + "' placeholder='168' style ='width:50px;font-size:20px;'>";
  html += ".";
  html += "<input type='text' name='ipadr3' value='" + ipadr3 + "' placeholder='11' style ='width:50px;font-size:20px;'>";
  html += ".";
  html += "<input type='text' name='ipadr4' value='" + ipadr4 + "' placeholder='2' style ='width:50px;font-size:20px;'>";
  html += "<br><br>";
  //ゲートウェイアドレス設定
  html += "ゲートウェイアドレス:  ";
  html += "<input type='text' name='gwadr1' value='" + gwadr1 +"' placeholder='192' style ='width:50px;font-size:20px;'>";
  html += ".";
  html += "<input type='text' name='gwadr2' value='" + gwadr2 +"' placeholder='168' style ='width:50px;font-size:20px;'>";
  html += ".";
  html += "<input type='text' name='gwadr3' value='" + gwadr3 +"' placeholder='11' style ='width:50px;font-size:20px;'>";
  html += ".";  
  html += "<input type='text' name='gwadr4' value='" + gwadr4 +"' placeholder='1' style ='width:50px;font-size:20px;'>";
  html += "<br><br>";
  //送信ボタン
  html += "<input type='submit' value='設定値送信' style='width:200px;padding:20px;font-size:20px;'><br>";
  html += "</form>";
  html += "</body>";
  html += "</html>";

  Wserver.send(200, "text/html", html);
}
/* 入力文字を取得*/
void HtmlSetPost(void){
  String html="";

  ssid = Wserver.arg("ssid"); //送信された値を取得
  pass = Wserver.arg("pass");
  ipset = Wserver.arg("ipset");
  ipadr1 = Wserver.arg("ipadr1");
  ipadr2 = Wserver.arg("ipadr2");
  ipadr3 = Wserver.arg("ipadr3");
  ipadr4 = Wserver.arg("ipadr4");
  gwadr1 = Wserver.arg("gwadr1");
  gwadr2 = Wserver.arg("gwadr2");
  gwadr3 = Wserver.arg("gwadr3");
  gwadr4 = Wserver.arg("gwadr4");

  FfsWrite(); //データを書き込み

  html += "<html lang=\"ja\">";
  html += "<head>";
  //html += "<meta http-equiv='refresh' content='3;'>";
  html += "<meta charset=\"UTF-8\">";
  html += "<title>Setting</title>";
  //html += "<link rel=""shortcut icon"" href=""/favicon.ico"" />";
  html += "</head>";
  html += "<body>";
  html += "<h1>システム設定中</h1>";
  html += "システム情報の変更後WiFiモジュールを再起動します。";
  html += "</body>";
  html += "</html>";
  Wserver.send(200, "text/html",html);
  delay(100);
  ESP.restart();
}

/* URLが存在しない場合の処理 */
void handleNotFound(void) {

  String message = "File Not Found\n\n";
  message += "URI: ";
  message += Wserver.uri();
  message += "\nMethod: ";
  message += (Wserver.method() == HTTP_GET) ? "GET" : "POST";
  message += "\nArguments: ";
  message += Wserver.args();
  message += "\n";

  for (uint8_t i = 0; i < Wserver.args(); i++) {
    message += " " + Wserver.argName(i) + ": " + Wserver.arg(i) + "\n";
  }
  Wserver.send(404, "text/plain", message); //テキストファイルであることを示している。
}

アクセスポイントのSSIDとパスワードはお使いのアクセスポイントのものに置き換えてください。

関連リンク

Arduinoのライブラリを使って動作確認を行ったことを下記リンクにまとめています。

Arduinoで学べるマイコンのソフト開発と標準ライブラリの使い方

Seeeduino XIAOで学べるソフト開発と標準ライブラリの使い方

ESP32-WROOM-32Eで学べるソフト開発と標準ライブラリの使い方

PR:アクセンチュアの転職なら【コンサルアクシスコンサルティング】

最後まで、読んでいただきありがとうございました。

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