/* ESP32を使った「Web Radio」 -------------------------- Apr.2025 S.S */ #include //#include #include #include "WiFiMulti.h" #include "Audio.h" #include "FS.h" #include "SD.h" #include #include #include #include #include #define BUILTIN_LED 2 // SPIピン配置 #define SCK 17 #define MISO 22 #define MOSI 21 #define SS 16 // ロータリーエンコーダ接続設定 #define A_PIN 5 #define B_PIN 23 //#define DT_PIN 23 #define SW_PIN 35 // Digital I/O used # define I2S_DOUT 26 // DIN connection # define I2S_BCLK 18 // Bit clock # define I2S_LRC 19 // Left Right Clock # define VOL_UP 25 // Volume+ button # define VOL_DOWN 27 // Volume- button int cur_station = 0; int clkState = 0; int prev_clkState; int max_station; size_t volume = 10; int max_wifi = 4; volatile bool sw_change_flag = true; int cur_vol, cur_sel; // OLED(SS1306)設定 #define SCREEN_WIDTH 128 // OLED display width, in pixels #define SCREEN_HEIGHT 64 // OLED display height, in pixels #define OLED_RESET -1 #define SCREEN_ADDRESS 0x3C //#define SCREEN_ADDRESS 0x78 #define OLED_SCL 32 #define OLED_SDA 33 String ap_list[4][2]; String stations[100][2]; Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); Audio audio; WiFiMulti wifiMulti; /* スイッチ割り込み時のチャタリング防止タイマー delay()は、タイマーを使っているので、割り込みの中では使えない */ void t_chatter() { for(int j=0;j<10;j++){ // チャタリング待ち。 delayMicroseconds(1000); } } /* A相の入力が変化した時に、この割り込みが掛かる ----------------- A,B相を見ると、 A相が変化したとき、時計回りであれば、10か01となる。 反時計回りであれば、11か00となる。 */ void IRAM_ATTR pulse_counter() { if(digitalRead(A_PIN) ^ digitalRead(B_PIN)) { t_chatter(); // チャタリング終了待ち clkState++; if (clkState > max_station) clkState = max_station; } else { clkState--; if (clkState <= 0) clkState = 0; } sw_change_flag = true; // 表示を更新 } /* ボリューム+/-スイッチを割り込みで受ける */ void IRAM_ATTR up_volume(){ volume ++; if (volume>=21) volume=21; sw_change_flag = true; // 表示を更新 audio.setVolume(volume); EEPROM.put(0, cur_vol); // 音量をEEPROMに保存 EEPROM.commit(); t_chatter(); // チャタリング終了待ち } void IRAM_ATTR dwn_volume(){ volume --; if (volume<=0) volume=0; sw_change_flag = true; // 表示を更新 audio.setVolume(volume); EEPROM.put(0, cur_vol); // 音量をEEPROMに保存 EEPROM.commit(); t_chatter(); // チャタリング終了待ち } /* エンコーダのシャフト(スイッチ)が押された時に掛かる割り込み */ void IRAM_ATTR change_station() { cur_station = clkState; audio.connecttohost(stations[cur_station][1].c_str()); EEPROM.put(4, cur_sel); // 選局No.をEEPROMに保存 EEPROM.commit(); t_chatter(); // チャタリング終了待ち sw_change_flag = true; // 表示を更新 } /* SDカードから局名リストを読み出して、2次元のString配列(stations)にセットする ------------------------------------------------------------- Input : ファイルハンドラ,読み出しファイル名 Output: 読み出した配列の行数 */ int readFile_station(fs::FS &fs, const char * path){ //int readFile(fs::FS &fs, const char * path, String *ary ){ int m = 0, index = 0, i; char c; Serial.printf("Reading file: %s\n", path); File file = fs.open(path); if(!file){ Serial.println("Failed to open file for reading"); return(false); } while(file.available()){ c = file.read(); if( !(c == 0x0d || c == 0x0a || c == ',') ){ stations[index][m] = stations[index][m] + String(c); } else { if (c==',') { m = 1; } else { m = 0; index++; } } } file.close(); //* 読み出し結果の確認 Serial.printf("Station Count = %0d\n ",index); for ( i=0; i"); Serial.println(stations[i][1]); } // return( index); } /* SDカードからAPリストを読み出して、配列(ssid,pass)にセットする ------------------------------------------------------------- Input : ファイルハンドラ,読み出しファイル名 Output: 読み出したAPの数 */ int readFile_wifi(fs::FS &fs, const char * path){ //int readFile(fs::FS &fs, const char * path, String *ary ){ int m = 0, index = 0, i; char c; Serial.printf("Reading file: %s\n", path); File file = fs.open(path); if(!file){ Serial.println("Failed to open file for reading"); return(false); } while(file.available()){ c = file.read(); if( !(c == 0x0d || c == 0x0a || c == ',') ){ ap_list[index][m] = ap_list[index][m] + String(c); } else { if (c==',') { m = 1; } else { m = 0; index++; } } } file.close(); //* 読み出し結果の確認 Serial.printf("AP Count = %0d\n ",index); for ( i=0; i"); Serial.println(ap_list[i][1]); } // return( index); } /* LCD表示を更新する ---------------------- Input: 局リストの局番号 */ void updateDisplay( int n ) { display.clearDisplay(); // 画面消去 display.setCursor(0, 0); display.printf("ST:%02d/%02d VOL:%0d", n, max_station, volume); //display.setTextColor(ORANGE, WHITE); display.setCursor(0, 10); display.print(" NET RADIO "); //display.setTextColor(GREEN, BLACK); //display.printf("Station list %0d/%0d", n, max_station); display.setCursor(0, 55); // Current station display.println(stations[cur_station][0]); display.setCursor(0, 20); // Playing station display.println(stations[clkState][0]); display.display(); // Display the content on the screen } void setup() { int i, n; Serial.begin(115200); Wire.begin(OLED_SDA, OLED_SCL); SPI.begin(SCK, MISO, MOSI, SS); // EEPROMから前の状態を読み出す EEPROM.begin(8); // =4+4 volumeとチャンネル設定を保存する //EEPROM.put(0,10); //EEPROM.put(4,0); //EEPROM.commit(); EEPROM.get(0,cur_vol); EEPROM.get(4,cur_sel); Serial.printf("sel=%d vol=%d\n", cur_vol, cur_sel); // Digital I/O pinMode(BUILTIN_LED, OUTPUT); digitalWrite(BUILTIN_LED, LOW); pinMode(VOL_UP,INPUT_PULLUP); pinMode(VOL_DOWN,INPUT_PULLUP); pinMode(SS, OUTPUT); // SPIのSS(Chipselect)は予めOUTPUT Modeにしておく必要がある。 attachInterrupt(digitalPinToInterrupt(VOL_UP), up_volume, FALLING); // VOL+ボタンの割り込み attachInterrupt(digitalPinToInterrupt(VOL_DOWN), dwn_volume, FALLING); // VOL+ボタンの割り込み // エンコーダー入力ポート pinMode(A_PIN, INPUT); pinMode(B_PIN, INPUT); pinMode(SW_PIN, INPUT); //prev_clkState = digitalRead(CLK_PIN); attachInterrupt(digitalPinToInterrupt(A_PIN), pulse_counter, RISING); // A相の立ち上がりで割り込みを掛ける // OLEDの初期化 Serial.println(F("SSD1306 init start")); if(!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) { Serial.println(F("SSD1306 disconnection")); for(;;); } // Initialize Display // Show initial display buffer contents on the screen -- // the library initializes this with an Adafruit splash screen. display.display(); delay(2000); // Pause for 2 seconds display.clearDisplay(); //初期化 display.setTextColor(SSD1306_WHITE); // Draw white text display.setTextSize(1); // Set text size //display.setRotation(2); // 180度回転 display.setCursor(0, 0); // Set cursor position display.println("ESP32 WEB Radio"); display.display(); // Display the content on the screen delay(2000); // Pause for 2 seconds // SD開始 if(!SD.begin()){ Serial.println("Card Mount Failed"); return; } max_station = readFile_station( SD, "/station_list.txt" ) -1; // 局名リストをSDから読み出す Serial.println("<<<<<< Station readed >>>>>>>"); // Wi-Fi接続 max_wifi = readFile_wifi( SD, "/wifi_list.txt" ); // wi-fi アクセスポイントのリストをSDから読み出す for(i=0; i<=max_wifi; i++){ //wifiMulti.addAP("Tech-Net","susumu4sikata"); // アクセスポイントを設定 //wifiMulti.addAP("Tech-Net_Guest","pass"); wifiMulti.addAP(ap_list[i][0].c_str(),ap_list[i][1].c_str()); } while(wifiMulti.run() != WL_CONNECTED) { //display.print("."); display.setCursor(0, 20); // Set cursor position display.println("Connecting Wi-Fi..."); display.display(); // Display the content on the screen digitalWrite(BUILTIN_LED, LOW); delay(200); digitalWrite(BUILTIN_LED, HIGH); delay(200); } digitalWrite(BUILTIN_LED, HIGH); display.clearDisplay(); display.setCursor(0, 20); display.println("WIFI CONNECTED TO"); display.setCursor(0, 30); display.println(WiFi.SSID()); display.display(); // Display the content on the screen Serial.print("WIFI CONNECTED TO "); Serial.println(WiFi.SSID()); delay(1000); Serial.println(F("Audio begin...")); // Audio開始 audio.setPinout(I2S_BCLK, I2S_LRC, I2S_DOUT); //audio.setVolume(cur_vol); volume = cur_vol; // 前の音量値 audio.setVolume( volume ); // 0...21 cur_station = cur_sel; // 前の選局 //cur_station = 0; audio.connecttohost(stations[cur_station][1].c_str()); // Default station } void loop() { audio.loop(); if (digitalRead(SW_PIN) == LOW){ change_station(); } if (sw_change_flag == true){ updateDisplay( clkState ); // 表示を更新 sw_change_flag = false; } } // optional void audio_info(const char *info){ Serial.print("info "); Serial.println(info); display.setCursor(0, 30); display.println(info); } void audio_id3data(const char *info){ //id3 metadata Serial.print("id3data ");Serial.println(info); } void audio_eof_mp3(const char *info){ //end of file Serial.print("eof_mp3 ");Serial.println(info); } void audio_showstation(const char *info){ Serial.print("station ");Serial.println(info); } void audio_showstreaminfo(const char *info){ Serial.print("streaminfo ");Serial.println(info); } void audio_showstreamtitle(const char *info){ display.setCursor(0, 30); display.println(info); Serial.print("streamtitle ");Serial.println(info); } void audio_bitrate(const char *info){ Serial.print("bitrate ");Serial.println(info); } void audio_commercial(const char *info){ //duration in sec Serial.print("commercial ");Serial.println(info); } void audio_icyurl(const char *info){ //homepage Serial.print("icyurl ");Serial.println(info); } void audio_lasthost(const char *info){ //stream URL played Serial.print("lasthost ");Serial.println(info); } void audio_eof_speech(const char *info){ Serial.print("eof_speech ");Serial.println(info); } //