「#4-5 WiFiインターネット通信」では、ESP32をFTDI(USB⇔UART変換モジュール)でPCと接続、Olive+のスクリプトから制御してみました。今回は、ちゃんとArduinoとシリアル接続して、Arduinoを使ったWiFi接続をトライしてみます。
ESP32のデフォルト設定では、シリアルユニットの通信速度は115,200baudに設定されています。よって、ESP32と通信するためには、少なくとも、最初の一回は115,200baudで接続してください。
実は、通信速度やフレームフォーマット(ビット数、ストップビット、パリティなど)の設定は、変更することができます。ただ、115,200baudで接続してから、他の通信速度に切り替えるなんて手間は無意味です。そこで、ESP32では、シリアルユニットの通信設定を不揮発メモリに記憶させて、次回起動時にその設定値で起動するように設定することができるようになっています。
でも、待ってください。万一、コマンドを打つ時にうっかり間違えて、違うフォーマットを指定してしまったら?そのまま不揮発メモリに書き込まれてしまったら?もう二度と、シリアル接続を確立できなくなってしまうかもしれません。
筆者は、設定を変更して不揮発メモリに記憶させる事に、抵抗(拒否反応と言っても良い)があります。
※espressif (ESP32のメーカ)のHPを探したら、ESP32の設定を工場設定値に初期化するツールくらいあるかもしれませんが……(未確認)。
接続相手のArduino UNO(に搭載されたATmega328)には、シリアルユニットは1本しか搭載されておらず、その1本はご存知の通り、USB⇔UART変換チップとの通信に使われていて、他の用途に転用できません。
Arduino IDEで使用可能なライブラリの中に、「ソフトウェアシリアル」と言うライブラリがあって、普通はそう言うライブラリを使って、外部の非同期シリアル通信機器と通信するのだそうです。しかし、このソフトウェアシリアルライブラリは、最速でも57,600baud程度までしか使えない様です。115,200baudとは接続できません。
※使用してみたことが無いので、ソフトウェアシリアルの最高通信速度は、よくわかりません……(1^^;)。何しろ、ソフトウェアで受信するので、57,600baudですら、「本当に平気?安定して通信できる?」と、疑ってしまいます。「安定して通信できるのは9,600baudくらいまで」と、言っている方もいらっしゃいました。
回避策は以下:
拒否反応を起こさないように、そして、安定・高速に通信することを第一として、筆者は(2)の回避策を取ります。
プログラムのデバッグ中は、Pro Miniだと、「プログラムを書き込むときにはFTDIとの接続」 ⇔ 「実行する時はESP32との接続」と、繋ぎ変える必要があって面倒なので、Microを使ってデバッグすることにします。
以下の様にボード設定を判定して、SerialESP32というマクロを定義すると、Pro MiniとMicro両方の機体に対応できるようにプログラムすることができます。
#ifdef ARDUINO_AVR_LEONARDO
#define SerialESP32 Serial1
#elif ARDUINO_AVR_PRO
#define SerialESP32 Serial
#endif // ARDUINO_AVR_LEONARDO, ARDUINO_AVR_PRO
#elifを増やして、UNOだった場合にはソフトウェアシリアルを使うような設定追加も可能だと思います(必要に応じて各自対応願います)。
さくっと接続して試してみたところ、時々、データの取りこぼしが発生しました。通信速度115,200baudは、結構速いんですよね。1ビット分の転送時間が約8.7μsですから。
いや、さっきのソフトウェアシリアルの議論の様に、ビットレートが速すぎて取りこぼしているのでは無いですよ!さすがにそこはハードウェアシリアルですから。追いつかないのはプログラムの方で、受信したデータの処理が間に合わず、バッファがオーバフロウして受信データをロストしてしまっているようです。
ArduinoのSerialクラスは、受信バッファサイズがSERIAL_RX_BUFFER_SIZE(=64バイトがデフォルト)で規定されています。
非同期シリアル通信では、1バイトのデータを送受信するのに、スタート/ストップビットを加えた計10ビットの通信が行われます。連続してデータを受信し続けた場合、SERIAL_RX_BUFFER_SIZEの受信バッファがいっぱいになるまで、どのくらいの時間(RXBUF_OVF_TIME)がかかるでしょうか?計算してみましょう。
RXBUF_OVF_TIME=SERIAL_RX_BUFFER_SIZE×10[ビット/フレーム]/115,200baud
=5,555μs (≒5.6ms)
長いようで、短い時間ですね(delay(6);の間にバッファフルになる)。ちょっと気を抜くと、すぐにバッファがオーバフロウして、データを取りこぼしてしまうはずです。
簡単な例では、例えば、ESP32(115,200baud)から受信したデータを、シリアルモニタ(9,600baud)へリダイレクトする場合はどうでしょう?通信速度が明らかに違うので、いずれ、バッファがオーバフロウするのは自明です。
コンパイルオプションなどで、SERIAL_RX_BUFFER_SIZEを大きな値に変更してコンパイルするのも安易な解法のひとつではあります。しかし、メモリ(RAM)に余裕のないArduino Micro/Pro Miniで、安易にバッファサイズを大きくするのは賢くありませんし、そもそも、サイズを大きくしたところで、大量のデータが一気に書き込みされてきた場合、結局どこかで取りこぼしが発生することは避けられません。
普通は、64バイトもバッファを持っていたら十分なはずなので、もう少し工夫してみます。
ESP32のATコマンドデータシートを見ていたところ、「AT+UART_CUR」コマンドで、ハードウェアフロウ制御(RTS,CTS)が選択できるようになっていることが分かりました。
※似たコマンドで、「AT+UART_DEF」は、最初に敬遠した、不揮発メモリ領域に設定を書き込んでしまうコマンドなので、間違って使ってしまわないように注意。勿論、意図して使う場合はご随意に。
※「AT+UART_CUR」の方は、ダイナミックに設定を変更しますが、不揮発領域に設定値を書き込まないので、リセットすれば、出荷時設定に戻ります。逆に言うと、毎回起動後に再設定することが必要なコマンドとなります。
フロウ制御と言うのは、データをロストすることが無い様、互いにデータを受信できない時に、相手に対して送信を保留するようにリクエストするハンドシェイクのことです。
残念ながら、Arduinoのシリアルユニットには、フロウ制御の機能が無いので、バッファフルになって、これ以上受け取れない状態になっても、それを伝える手段が無く、結果としてデータをロストしてしまっています。
ESP32のフロウ制御に使われる信号は、RTSとCTSの2本です。
2つの機器間で非同期シリアル通信する時、RXとTXの信号線を、それぞれクロスして繋ぎますよね?
こちら側 | 接続 | 相手側 |
RX | → | TX |
TX | ← | RX |
同じ様に、RTSとCTSも、クロスして繋ぎます。
こちら側 | 接続 | 相手側 |
RTS | → | CTS |
CTS | ← | RTS |
ESP32側にはフロウ制御の機能が備わっていることが分かったので、ArduinoのI/Oポートを使って、手動(プログラム)でフロウ制御を実施することができれば、データのロストを回避できるはずです。今やりたいのは、ESP32からの送信を一時保留してもらって、その間にバッファに溜まったデータの処理を進めたいのだから、ArduinoからRTS信号を出力して、ESP32のCTSに接続します。
ESP32は、デフォルトでRTSを出力していますが、CTS入力が無効になっています。以下のATコマンドを実行すると、CTSを有効にして、フロウ制御を実行する様になります。
AT+UART_CUR=115200,8,1,0,2
最後の"2"がCTS有効にする設定値です(今回、ESP32のRTS出力は使わないので、代わりに無効にしてあります)。ESP32のATコマンドデータシートによると、RTSがIO14、CTSがIO15に割り当たっています。
最後に、ESP32のI/Oポートは5Vトレラントになっていないので、レベルシフタを通してArduinoと接続します。接続回路図は以下の通りとなります。
※後日アップデートした回路図となります。更新内容については、本ページの末尾に記載してあります。
筆者が購入したレベルシフタは、プルアップ+FETタイプの回路構成で、自動的に双方向のレベルシフトが可能です。その代わり、プッシュプルドライバタイプのレベルシフタと異なり、配線負荷次第では、信号伝搬遅延が大きくなる傾向にあるので、高速通信に使用する場合は注意が必要です。
データシートからは読み取ることができなかったのですが、ちょっと繋いでみて試したところ、どうやらESP32のRTSとCTSは負論理になっているようです。つまり、
です。
RTS,CTS信号は、「レベル」信号なので、アサートしている間、ずっと「データを送信して良いですよ」と言う状態を示しています。従って、Arduinoで、RTS出力として使用するI/Oポートを出力設定して、digitalWrite()関数でLOWを出力するとアサート、HIGHを出力するとネゲートとなります。
const int pin_RTS=7;
digitalWrite (pin_RTS, LOW); // アサート
pinMode (pin_RTS, OUTPUT);
今現在、どのくらいのデータを受信したか?は、SerialESP32.available()メソッドの返り値で知ることができます。つまり、SerialESP32.available()を監視し、SERIAL_RX_BUFFER_SIZE(=64)と比べてどの程度のデータを受信しているか?を知り、受信バッファがオーバフロウする前にRTSをネゲートすれば、そこでESP32からの送信を一時停止できるので、データをロストしなくて済むことになります。
以下の様な関数群を作って、手動/自動できめ細かく制御します。
void esp_negate_rts (void)
{
// negate rts (not request to send)
if (esp_rts_state)
{
digitalWrite (pin_RTS, HIGH);
esp_rts_state=false;
}
}
void esp_assert_rts (void)
{
// assert rts (request to send)
if (!esp_rts_state)
{
digitalWrite (pin_RTS, LOW);
esp_rts_state=true;
}
}
void esp_auto_rts (
int navl=SERIAL_RX_BUFFER_SIZE/2)
{
// auto control rts
// navl: assert or negate threshold for Serial.available()
if (SerialESP32.available ()<navl)
{
// assert rts
esp_assert_rts ();
}
else
{
// negate rts
esp_negate_rts ();
}
}
以下の様な動作をします。
プログラム中で時々使っている、「esp_auto_rts (1);」という処理は、1バイトも受信していなければアサート、1バイトでも受信していればネゲートと言う動作になります。
SerialESP32.available()は、ESP32からデータが送られているかどうか?を知るためにも使用しているので、無条件にESP32からの送信を止めてしまう(RTSネゲート)と、今、ESP32が送信したいデータを持っているかどうか?を、知る術がなくなってしまいます。そこで、データを受信していないときにはアサートしておき、1バイトでも受信していたら一旦ネゲートする、と言う動作が必要になります。
今後、Arduino側の処理が複雑になってきて、SerialESP32.available()の監視がおろそかになってしまうと、結局、RTSのネゲート処理が間に合わずに、データをロストしてしまいかねません。そこで、「#4-8 タイマ割り込み」で解説したタイマ割り込みを使い、一定期間毎にSerialESP32.available()を監視、RTSを制御するようにしておきます。
ISR (TIMER1_COMPA_vect)
{
// OCI1A interrupt service routine
if (esp_rts_state && SerialESP32.available ()>0)
esp_negate_rts ();
}
割り込み間隔(TINT_INTERVAL)は、受信バッファがいっぱいになるまでの最短時間RXBUF_OVF_TIMEの1/2に設定しておきます。
const uint32 TINT_INTERVAL=RXBUF_OVF_TIME/2;
本ページ冒頭で計算した通り、RXBUF_OVF_TIME=5,555μsですので、TINT_INTERVAL=2,777μs(≒2.8ms)となります。だいぶ短い間隔で割り込みがかかりますが、上のコード程度の処理ですので、それほどオーバヘッドは無いはずです。
それでもオーバヘッドが気になる場合には、以下の様に制御することも可能です。ご検討ください。
いつも、筆者がシリアルモニタからコマンド等のデータを受信するために使用している、get_line()と言う関数があります。今回は、データを受け取るSerialクラスがSerialESP32に代わるだけですので、ちょっと修正して、esp_get_line()関数を作成しました。
ただし、ESP32が非同期に送ってくる、"+IPD"で始まる応答だけは、ちょっと注意して、別扱いにする必要があります。"+IPD"で始まる応答は、以下の様な書式になっています。
今回はESP32でTELNETサーバを立ち上げますので、外部機器(クライアント)からTELNET接続してきます。すると、外部機器の都合に従って、こちら(ESP32+Arduino)の都合とは無関係(非同期)に、データが飛んでくるため、"+IPD"応答が、いつ、どのタイミングで返るか、全く予想できません。
※通常は、こちら(Arduino)側からESP32へATコマンドを投げると、応答が返ってくる、という同期的動作になっています。裏返すと、「ATコマンドを投げない限り、応答は返ってこない」ので、そういう前提でプログラムを組めたわけです。サーバを立ち上げた場合は、非同期にデータを受信するので、こちらがATコマンドを投げていないときでも、ESP32からのデータを受信し続ける必要がある、と言うことです。
そこで、ESP32からの応答を受信する関数esp_get_line()内で"+IPD"応答を監視し、"+IPD"応答を検知したら、通常のテキストデータを保持するバッファesp_lbufとは別な専用バッファesp_ipdに、"+IPD"データ(<data>)を保持することにします。
"+IPD"用のバッファesp_ipdには、バイナリデータも受信できるように、PASCAL文字列形式を採用します。バイナリデータだと言っているのに、「文字列」って?と、思うかもしれませんが、PASCAL文字列は、以下図のように、先頭バイトがデータ数、その後ろにデータが続く形になっているので、テキスト(文字列)に限らず、バイナリデータを保持することも可能な構造になっています。
先頭のバイトカウントが1バイトの符号無整数なので、最大データ長は255バイトとなります。
比較してあるC文字列では、文字列の終端コードとして0x00('\0')を使ってしまうため、0x00を含むバイナリデータを保持できません。
※完成版プログラム(telnet_server_esp.ino)では、万一、255バイトを超えるデータを受信したときには、先頭の255バイトを取り込み、それ以降のデータは破棄しています。256バイト以上のデータを受信する必要のある場合には、必要に応じて修正してお使いください。
実際のコーディングに関しては、完成版プログラム(telnet_server_esp.ino)でご確認ください。
「#4-7 ENC28J60を使う」では、ENC28J60という、有線(10BASE-T)EthernetモジュールとUIPEthernetライブラリを使って、TELNETサーバを構築しましたが、今回はESP32を使って構築します。
以下、目標仕様です。
今回も、「TELNET(もどき?)サーバwith ESP32」と言う命名にしておきます(1^−^;)っ。
早速、TELNETサーバを立ち上げるためのATコマンドから見ていきましょう。
送信 | AT+RST |
ESP32をリスタートする。 Arduino側にリセットがかかっても、ESP32側にはリセットがかからないので、毎回同じ状態でセットアップできるよう、Arduino起動時にESP32をリセットする。 |
受信 |
OK ready WIFI CONNECTED WIFI GOT IP |
リセットがかかると、「AT+CWJAP」で指定したAPへ自動的に接続することを期待。 自動接続に設定していない場合は、ここに「AT+CWJAP」を使って接続するコマンドを挿入する必要がある。 |
送信 | AT+UART_CUR=115200,8,1,0,2 | フロウ制御CTSを有効にする。 |
受信 | OK | |
送信 | AT+CIPMUX=1 |
マルチコネクションモードを設定する。 ESP32では、サーバを立てるためにマルチコネクションモードにする必要がある。 |
受信 | OK | |
送信 | AT+CIPSERVER=1,23 |
ポート23に対して、サーバを起動(リスン開始)します。 23は、TELNETで使われるポート名です。他のポートを使う場合は変更してください。 |
受信 | OK |
ESP32とは、SerialESP32でシリアル接続されているので、例えば以下の様にすると、所望のATコマンドを送信することが可能です。
SerialESP32.println ("AT+RST");
簡単でしょう?でも、ちょっと面倒なのが、ESP32からの応答処理です。ATコマンドを投げると、一々応答メッセージが返ってくるので、毎回これを受け取ってあげないといけません。コマンドによっては、エラーを返す物もあるし、色々な書式で情報を返してくるものもあります。
そこで、いくつか(3つにしてあります)、応答メッセージの候補を与えて、いずれかの応答が返ってくるまでESP32からデータを受信し続ける関数を用意しておきます。
int esp_wait_response (
const char *res0,
const char *res1=NULL,
const char *res2=NULL)
{
// wait until receive specified response
// res#: waiting response (forward match)
// return: res# / ErrRecv
int rtncod;
int len0;
int len1;
int len2;
uint32 msec;
len0=(res0==NULL)? 0: strlen (res0);
len1=(res1==NULL)? 0: strlen (res1);
len2=(res2==NULL)? 0: strlen (res2);
rtncod=ErrRecv;
msec=millis ();
while (millis () - msec<esp_msec_tout)
{
esp_get_line ();
if (res0!=NULL && strncmp (esp_lbuf,res0,len0)==0)
{
rtncod=0;
break;
}
if (res1!=NULL && strncmp (esp_lbuf,res1,len1)==0)
{
rtncod=1;
break;
}
if (res2!=NULL && strncmp (esp_lbuf,res2,len2)==0)
{
rtncod=2;
break;
}
}
return (rtncod);
}
例えば、「esp_wait_response ("OK", "ERROR");」の様に使うと、応答メッセージが「OK」または「ERROR」を返すまでESP32から受信し続け、どちらかの応答が見つかった時に、その番号(「OK」なら0、「ERROR」なら1)を返すようにしてあります。
※完全に想定外の応答が返ってきた場合にプログラムがハングしないように、タイムアウトesp_msec_tout(=20秒)を設定してあります。
応答候補文字列は、前方一致で判定します。PINGを実行する「AT+PING」コマンドを例に取って、使ってみましょう。
送信 | AT+PING="192.168.0.1" | 指定したIPアドレスに対してPINGを実行する。 |
受信 |
+PING:<time> OK または、 +PING:TIMEOUT ERROR |
成功した場合と失敗した場合で応答が異なる。 |
この場合、欲しいのは"+PING:"で始まる応答行ですので、以下の様に取得します。
// ping
int errcod;
char png[16];
SerialESP32.println ("AT+PING=\"192.168.0.1\"");
SerialESP32.flush ();
png[0]='\0';
errcod=esp_wait_response ("OK","ERROR","+PING:");
if (errcod==2)
{
strcpy (png,esp_lbuf);
errcod=esp_wait_response ("OK","ERROR");
}
// response
Serial.println (png);
実行結果は、"+PING:"で始まる行だけがシリアルモニタにリダイレクトされて表示されます。文字列の「前方一致」比較が利いています(1^−^)d。
残りの部分は、完成版プログラムでご確認ください。
完成版プログラム:telnet_server_esp.ino
完成版プログラムをコンパイルしてArduino MicroまたはPro Miniに書き込んでください。デバッグ時は、シリアルモニタで確認可能なMicroの使用を推奨します。
「#4-7 ENC28J60を使う」では、UIPEthernetライブラリの物量(Flash/RAMの使用量)が大きくて、注意喚起しましたが、今回はどうでしょうか?
ターゲット | プログラム | Flash容量 | RAM容量 | 備考 |
Pro or Pro Mini | telnet_server_esp.ino | 7,128バイト | 517バイト | ESP32版 |
Leonardo (Micro) | 12,346バイト | 753バイト | ||
UNO | telnet_server_enc.ino | 20,836バイト | 1,460バイト |
UIPEthernetライブラリ +ENC28J60版 |
それぞれ、表記したターゲットとプログラムをコンパイルしたときの、FlashとRAMの使用量です。
完全に同じ機能のプログラムではないので、アンフェアなのですが、イメージは伝わるかと……。UIPEthernetライブラリ部分の物量が、ごっそりESP32側に押し出されているので、ATmegaベースのArduinoでも、余裕をもってアプリケーションを組み込むことができそうです。
上図は、実験中の様子です。ESP32は、ピンヘッダがはんだ付けされた状態で届いた(「#4-1 中国から配送されるネットショッピング」参照)のですが、斜めにはんだ付けされている関係で、ブレッドボードには挿さりません。しかし、今回使用する端子(3V3, GND, TX2, RX2, IO15(CTS))は、片側1列にまとまっていたため、写真の様に一列だけブレッドボードに挿して回路を組むことができました。小さくて見にくいですが、ブレッドボードには3.3V⇔5Vレベルシフタもつながっています。
起動したら、まずは、ESP32のIPアドレスを調べます。Microであれば、シリアルモニタからコマンドを投げれば確認可能です。
実験サンプル:【 】内はシリアルモニタからの送信データ
【AT+CIFSR】
+CIFSR:STAIP,"192.168.0.102" …… DHCPの割り当てたIPアドレス
OK
Arduino Pro Miniの場合は、……確認できないので、「AT+CIPSTA」コマンドで固定IPにするのも手です。
どうしても、DHCPで使いたい場合は、FTDIをESP32に接続してIPアドレスを確認してください。DHCPで割り当てられるIPアドレスはMACアドレスに対して割り当てられるので、DHCPのIPアドレス貸与期間中であれば、FTDIで確認したIPアドレスは、Pro Miniに繋ぎ変えても変わりません。
※I/Oポートには空きがあるので、Pro Miniを使うときのデバッグ用出力を「ソフトウェアシリアル」にして、ソフトウェアシリアルの出力端子にFTDIを接続すると、Arduino IDEのシリアルモニタが使用可能です。
「#4-7 ENC28J60を使う」でも使った、通信ソフトウェアTera Termを使って、192.168.0.102:23に対してTELNET接続してみてください。今回も、特にプロンプトを表示していないので、「無言」ですが、以下の様なコマンドを打って動作を確認してみましょう。
実験サンプル:【 】内はTELNET端末からの送信データ
【who】
OK: telnet_server_esp32 ver.1.00
【ping,192.168.0.1】
+PING:7
【time】
+CIPSNTPTIME:Sat Mar 16 11:55:14 2019
一応、最大5つのクライアントと接続できるようになっています。Tera Termで複数のセッションを開き、同時にコマンドを送ってみたところ、どちらか一方にしか応答が返らないケースがありました。"+IPD"の受信バッファを1段しか持っていないため、ほぼ同時に受信してしまうと、後に受信したデータが、先に受信したデータを上書きしてしまうためです。"+IPD"データの受信バッファを多段にすれば回避可能ですが、今回は「仕様制限」として、そこまでのコーディングはしていません。
同様に、クライアント側から非同期に飛んでくる受信データのハンドリングが完全ではない可能性があるので(デバッグ不足)、時々変な挙動をするかも??筆者が使ってみた感じでは問題なさそうでしたが、注意して使ってみてください。
※「AT+CIPSERVERMAXCONN=1」を設定すると、クライアントとの最大接続数を1に限定することができます。それでも、短い間隔でコマンドを連続して送信した場合には、同様に、後に届いた方が、先に届いたデータを上書きしてしまうので、同じことが起こり得ます。
最後に、ちゃんとArduino Pro Miniでも実行できることを確認しておきます。Pro Miniを購入したとき(「#4-1 中国から配送されるネットショッピング」参照)、MicroとPro Miniのピン配置が似ている、と言う指摘をしました。
今回使用する端子(ジャンパ線5本分)については完全に一致しており、ブレッドボードの配線そのままで、Microを外して同じ場所にPro Miniを挿したら換装終了でした。
もっとも、Pro Miniの場合はUSBから電源を供給することができませんので、電源を別途供給する必要があります。プログラムを転送するのにFTDIを接続する必要があるので、転送後もVccとGNDだけは繋いでおいて、FTDIから電源を供給しました。TXとRXについて、ESP32⇔FTDIの繋ぎ変えが必要なのは本ページ最初に述べた通りです。
ぱっと見、少し上で示した実験中写真と同じに見えますが、Micro→Pro Miniになっていますので、FTDIが接続されていることがわかります。想定通り、Pro Miniのハードウェアシリアル(Serial)を使って、ESP32を接続することができました。
ESP32には、ちゃんと登録されたMACアドレスがついているので、安心してインターネットにぶら下がることが可能です。すなわち、インターネットを使って、Arduinoの遠隔ワイヤレス操作・制御が可能となったわけです。
さあ、あなたなら何に使いますか?
後日、「#5-2 高速ソフトウェアシリアル(後編)」で作成した高速ソフトウェアシリアルライブラリを使って、Arduino UNOでもESP32と接続できるようにプログラムをアップデートしました。以下にリンクを張っておきますが、上の本文中でリンクしたオリジナルプログラムを更新したので、どちらからダウンロードしても、同じ(更新版)プログラムを入手できます。
更新版プログラム:telnet_server_esp.ino
HsswSerialクラスライブラリ:HsswSerial.zip
Arduino UNOでのコンパイルおよび実行には、高速ソフトウェアシリアルライブラリ(HsswSerial)のインストールが必要になります。インストール方法については、「#5-2 高速ソフトウェアシリアル(後編)」を参照してください。
「#5-3 高速ソフトウェアシリアル(続編)」でHsswSerialクラスライブラリをアップデートしました(ver.1.12)。これに伴い、本ページ中の回路図やプログラム(telnet_server_esp.ino)も変更しました。
※本文中の記述は変更していませんが、一部図やリンクしたプログラムは更新されていることに、ご注意ください。