Webラジオの「選局」を非同期サーバーを通してやってみた

　インターネットラジオの「選局」について色々考えました。以前の投稿では赤外線測距センサーを使いましたが、今回は "AsyncWebServer" を利用したものが出来たので投稿します。
![Asynchronous server.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3189926/49e1be5e-2611-d818-890b-27fa5731d0cf.jpeg)
　今回使用したボードは
![FREENOVE.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3189926/5b9803d5-4f9c-2b8e-bbda-d84c53728f76.jpeg)

:::note
FREENOVE ESP32-WROVER-DEV カメラ付き（写真では外してます）
Soc は "ESP32-WROVER-B" で、"PSRAM 8MB"を裏面に搭載（使えるのは4MBまで）
銀色の部分が縦長、さらにアンテナ外付けでワイアレス機能が強化(?)
ArduinoIDE では「ESP32 Wrover Module」としました。
:::
　目指したのは「loop()中に何も記述しない」こと、"ESPAsyncWebServer" なら可能であると知り試してみました。次の記事がとても参考になりました。

http://marchan.e5.valueserver.jp/cabin/comp/jbox/arc214/doc21403.html

正直なところ HTML   については全く知らないも同然で、私にわかる範囲で手直ししました。コードが拙いのはそのためです、ご勘弁を。
```
#include <ESPAsyncWebServer.h>
```
を使用するために github のサイト

https://github.com/me-no-dev/ESPAsyncWebServer

から "ESPAsyncWebServer-master.zip" をダウンロード、ライブラリをインクルードしました。更に、どの "Audio.h" を使用するかで IDE が混乱するようになっていたので、

https://github.com/schreibfaul1/ESP32-audioI2S

から "ESP32-audioI2S-master.zip" をダウンロードしてインクルード。

　そして実際に動作したのが、以下に示すコードです。「Arduino IDE 1.8.19」でコンパイルしました。
```c:
#include <Arduino.h>
#include <WiFi.h>
#include <Audio.h>
#include <ESPAsyncWebServer.h>

// I2S PCM5102 への出力
#define I2S_BCLK 27  // 26 work too. BCK
#define I2S_DOUT 26  // 22, 27 DIN
#define I2S_LRC  25  // LCK

String ssid = "xxxxxxxxxxx";  // your ssid
String password = "yyyyyyy";  // your password

// 非同期サーバーオブジェクト
AsyncWebServer server(80);

// Begin Of HTML ------------------------------------------------------------------------------------
const char* strHtml = R"rawliteral(
<!DOCTYPE HTML>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <style>
      html { font-family: Helvetica; display: inline-block; margin: 0px auto;text-align: center;} 
      h1 {font-size:28px;}
      body {text-align: center;} 
      table { border-collapse: collapse; margin-left:auto; margin-right:auto;}
      th { padding: 12px; background-color: #0000cd; color: white; border: solid 2px #c0c0c0;}
      tr { border: solid 2px #c0c0c0; padding: 12px;}
      td { border: solid 2px #c0c0c0; padding: 12px;}
      .value { color:black; font-weight: bold;font-size:32px; padding: 1px;}
      .btn_ChDown {width:140px; height:43px; padding;1px 1px; vertical-align: middle; text-decoration:none;
        font-size:18px; font-weight: bold; background-color: #668a88; color: yellow;
        border-bottom: solid 4px gray; border-radius: 4px;}
      .btn_ChDown:active { -webkit-transform: translateY(2px); transform: translateY(2px);
        border-bottom: none;}
      .btn_ChUp {width:140px; height:43px; padding;1px 1px; vertical-align: middle; text-decoration:none;
        font-size:18px; font-weight: bold; background-color: #668a88; color: red;
        border-bottom: solid 4px gray; border-radius: 4px;}
      .btn_ChUp:active { -webkit-transform: translateY(2px); transform: translateY(2px);
        border-bottom: none;}
    </style>
  </head>
  <body>
    <h1>Asynchronous Server</h1>
    <p><table>
      <tr><th width="100">Web Radio Channel</th></tr>
      <tr><td><span id="channel" class="value">%CHANNEL%</span></td></tr>
    </table></p>
    <button class='btn_ChDown' onclick='btnClicked_0()' id='btn_D'>Ch_Down</button>
    <button class='btn_ChUp' onclick='btnClicked_1()' id='btn_U'>Ch_Up</button>
  </body>
  <script>
    var getChannel = function () {
      var xhr = new XMLHttpRequest();
      xhr.onreadystatechange = function() {
        if (this.readyState == 4 && this.status == 200) {
          document.getElementById("channel").innerHTML = this.responseText;
        }
      };
      xhr.open("GET", "/channel", true);
      xhr.send(null);
    }
    function btnClicked_0() { 
      var xhr = new XMLHttpRequest();
      xhr.open("GET", "/update0", true);
      xhr.send(null);
    }
    function btnClicked_1() { 
      var xhr = new XMLHttpRequest();
      xhr.open("GET", "/update1", true);
      xhr.send(null);
    }
    setInterval(getChannel, 1000);
  </script>
</html>)rawliteral";
// End Of HTML --------------------------------------------------------------------------------------

String stations[] = {
  "WRTI_CLASSICAL https://playerservices.streamtheworld.com/api/livestream-redirect/WRTI_CLASSICAL.mp3",
  "WRTI_JAZZ https://playerservices.streamtheworld.com/api/livestream-redirect/WRTI_JAZZ.mp3",
  "181.fm_TranceJaz http://listen.livestreamingservice.com/181-trancejazz_64k.aac",
  "181.fm__Energy93 http://listen.livestreamingservice.com/181-energy93_64k.aac",
};


Audio audio;
int volume = 14; // 0...21

int Channel = 0;  // current station No.
int before_Channel = 0;
bool ChangeFlg = false;
int Channels = 4;

TaskHandle_t thp[1];

void setup() {
  Serial.begin(115200);

  WiFi.disconnect();
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid.c_str(), password.c_str());
  while (WiFi.status() != WL_CONNECTED) delay(1500);
  Serial.println(F("WiFi start"));
  Serial.print("  *IP address: ");　　// このアドレスにPCまたはスマホからアクセス
  Serial.println(WiFi.localIP());

  Server_Init();　　　　　　// 非同期サーバーの設定から起動までを行う

  Serial.print(F("\n\rStation : "));
  Serial.print(Channel);
  Serial.print(F(" > "));
  Serial.println(stations[Channel].c_str());

  audio.setPinout(I2S_BCLK, I2S_LRC, I2S_DOUT);  // PCM5102へのデータ出力
  audio.setVolume(volume);
  audio.connecttohost(stations[Channel].c_str());

  xTaskCreatePinnedToCore(Core0, "Core0", 4096 * 2, NULL, 3, &thp[0], 0); // core0 上で動作させる。
  // --------------------------------- if 4096, restted by WDTimer in some cases.
}

void loop() {
  audio.loop();
}

void Core0(void *args) {
  while (1) {
    delay(1);
    //サブで実行するプログラムを書く
    if (ChangeFlg == true) {
      audio.stopSong();
      delay(100);
      Serial.print(F("\n\rStation : "));
      Serial.print(Channel);
      Serial.print(F(" > "));
      Serial.println(stations[Channel].c_str());
      audio.connecttohost(stations[Channel].c_str());
      ChangeFlg = false;
    }
  }
}

void Server_Init() {
  // GETリクエストに対するハンドラーを登録して
  server.on("/", HTTP_GET, [](AsyncWebServerRequest * request) { // トップページにアクセスした時
    request->send_P(200, "text/html", strHtml, editPlaceHolder);
  });

  server.on("/channel", HTTP_GET, [](AsyncWebServerRequest * request) {
    request->send(200, "text/plain", getChannel().c_str());       // 下行との違いが解らない！
    //  request->send_P(200, "text/plain", getMessage().c_str());
                     // ステータスコード, 出力形式, 出力データ
  });

  server.on("/update0", HTTP_GET, [](AsyncWebServerRequest * request) {
    if (Channel > 0) {
      before_Channel = Channel;
      Channel--;
      ChangeFlg = true;
      Serial.print("Channel="); Serial.println(Channel);
    }
    request->send(200, "text/plain", getChannel().c_str());
    //request->send_P(200, "text/plain", getMessage().c_str());
  });

  server.on("/update1", HTTP_GET, [](AsyncWebServerRequest * request) {
    if (Channel < Channels - 1) {
      before_Channel = Channel;
      Channel++;
      ChangeFlg = true;
      Serial.print("Channel="); Serial.println(Channel);
    }
    request->send(200, "text/plain", getChannel().c_str());
    //request->send_P(200, "text/plain", getMessage().c_str());
  });

  // サーバーを開始する
  server.begin();
}

/*****************************************************************************/
/*                          'GET' Request Handlers                           */
/* プレースホルダー処理 */
String editPlaceHolder(const String& var) {
  if (var == "CHANNEL") {
    return getChannel();
  }
}
/* CHANNELを通知する */
String getChannel() {
  return String(Channel);
}
/*****************************************************************************/

// Audio optional --------------------------------------------------------------
void audio_showstation(const char *info) {
  Serial.print("station     ");
  Serial.println(info);
}
void audio_showstreamtitle(const char *info) {
  Serial.print(F("streamtitle "));
  Serial.println(info);
}
void audio_bitrate(const char *info) {
  Serial.print(F("bitrate     "));
  Serial.println(info);
}
```
　シリアル通信が教えるIPアドレスにスマホからアクセスし、ボタンクリックで選局ができるので前のものより楽チンな気がします。ボリュームの調整も出来ると良いですかね。
    　今回、最近使用している "Arduino IDE 2.0" ではなく "1.8" を使用したのには訳があって、HTML 部分を "SPIFFS" に「ESP32 Sketch Data Upload」しようと考えていたからです。しかし、やってみたところ無限パニックリセット状態になり困りました。それで断念。そこまで作り込まなくても、結果的に問題なく動作し良かったです。最後まで見ていただきありがとうございました。何かありましたらまたご教示お願いします。