MyTetra Share
Делитесь знаниями!
Время создания: 23.10.2025 10:08
Автор: alensav
Текстовые метки: WIFE RADIO v4.0.0
Раздел: Монтаж_ WifiRadio_23-1--25
Запись: alensav/MyTetra2/main/base/1761203321lbq1uuse5n/text.html на raw.githubusercontent.com

/*==========================================================================================

* WIFE RADIO v4.0.0 — Полностью функциональное радио с веб-интерфейсом

* ----------------------------------------------------------------------------------------

* Веб-интерфейс соответствует скриншоту:

* - Статус VS1053 и SD

* - IP, Wi-Fi, RSSI, громкость

* - Кнопки: Play, Stop, Volume +/-, Record, Next, Scan, Info, Test

*

* Платформа: ESP32-MAX-V3.0

* FRAM: MB85RC256V через TCA9548A

* I2C мультиплексор: TCA9548A (адрес 0x70)

* VS1053 Breakout v4

* TFT ST7735 1.8"

* Энкодер KY-040

*========================================================================================*/


#include <Arduino.h>

#include <Adafruit_GFX.h>

#include <Adafruit_ST7735.h>

#include <SPI.h>

#include <Wire.h>

#include <WiFi.h>

#include <WebServer.h>

#include <ESPmDNS.h>

#include <ArduinoOTA.h>

#include <NTPClient.h>

#include <WiFiUdp.h>

#include <SD.h>

#include <Adafruit_VS1053.h>


// === ПИНЫ ===

#define TFT_CS 15

#define TFT_DC 2

#define TFT_RST 0


#define VS1053_CS 5

#define VS1053_DCS 17

#define VS1053_DREQ 16

#define VS1053_RESET 4

#define SD_CS 25


#define ENCODER_CLK 12

#define ENCODER_DT 14

#define ENCODER_SW 13


#define I2C_SDA 21

#define I2C_SCL 22

#define TCA9548A_ADDR 0x70

#define FRAM_ADDR 0x50


// === ГЛОБАЛЬНЫЕ ОБЪЕКТЫ ===

Adafruit_ST7735 tft = Adafruit_ST7735(TFT_CS, TFT_DC, TFT_RST);

Adafruit_VS1053_FilePlayer player = Adafruit_VS1053_FilePlayer(

VS1053_RESET, VS1053_CS, VS1053_DCS, VS1053_DREQ

);

WebServer server(80);

WiFiUDP ntpUDP;

NTPClient timeClient(ntpUDP, "pool.ntp.org", 10800);


// === СТАТУС УСТРОЙСТВ ===

bool vs1053Available = false;

bool sdCardAvailable = false;


// === Wi-Fi сети ===

struct WiFiNetwork {

const char* ssid;

const char* password;

};

WiFiNetwork wifiNetworks[] = {

{"Keenetic-6391", "U7Y6hMeB"},

{"ASUS", "as485127sav"},

{"Galaxy_A12", "sang7164"},

{"MobileHotspot", "12345678"}

};

const uint8_t WIFI_NETWORK_COUNT = 4;

String connectedSSID = "Не подключено";


// === Радиостанции ===

struct Station {

char name[32];

char url[128];

};

Station stations[10] = {

{"Радио Джаз", "http://jazz-wr06.ice.infomaniak.ch/jazz-wr06-128.mp3"},

{"Классический рок", "http://listen.181fm.com/181-classicrock_128k.mp3"},

{"BBC World", "http://stream.live.vc.bbcmedia.co.uk/bbc_world_service"},

{"", ""}

};

uint8_t stationCount = 3;

uint8_t currentStation = 0;


// === Настройки ===

uint8_t volume = 50;

bool isPlaying = false;

String currentMetadata = "Ожидание...";

bool isRecording = false;

File currentFile;


// === Функции ===

void selectTCA9548AChannel(uint8_t channel);

bool framWriteBytes(uint16_t addr, uint8_t* data, uint8_t len);

bool framReadBytes(uint16_t addr, uint8_t* data, uint8_t len);

void saveSettingsToFRAM();

void loadSettingsFromFRAM();

void connectToBestWiFi();

void setupWebServer();

void handleEncoder();

void playStation(uint8_t index);

void playSDFile(const char* filename);

void startRecording();

void stopPlaying();

void updateDisplay();

bool waitForDREQ(unsigned long timeout = 500);


// === SETUP ===

void setup() {

Serial.begin(115200);

delay(500);

Serial.println("\n\n=== [WIFE RADIO v4.0.0 — ESP32-MAX-V3.0] ===");


// TFT

tft.initR(INITR_BLACKTAB);

tft.setRotation(3);

tft.fillScreen(ST7735_BLACK);

tft.setTextColor(ST7735_YELLOW);

tft.println("Загрузка...");


// Энкодер

pinMode(ENCODER_CLK, INPUT_PULLUP);

pinMode(ENCODER_DT, INPUT_PULLUP);

pinMode(ENCODER_SW, INPUT_PULLUP);


// SPI

SPI.begin();


// VS1053

if (player.begin()) {

vs1053Available = true;

player.setVolume(volume, volume);

Serial.println("✅ VS1053 найден");

} else {

vs1053Available = false;

Serial.println("❌ VS1053 не найден");

}


// SD карта

if (SD.begin(SD_CS)) {

sdCardAvailable = true;

Serial.println("✅ SD готова");

} else {

sdCardAvailable = false;

Serial.println("❌ SD не найдена");

}


// I2C

Wire.begin(I2C_SDA, I2C_SCL);

Wire.setClock(400000);


// FRAM через TCA9548A

selectTCA9548AChannel(0);

Wire.beginTransmission(FRAM_ADDR);

if (Wire.endTransmission() == 0) {

Serial.println("✅ FRAM обнаружена");

loadSettingsFromFRAM();

} else {

Serial.println("⚠️ FRAM не найдена");

}


// Wi-Fi

connectToBestWiFi();


// NTP

timeClient.begin();


// Веб-сервер

setupWebServer();


// OTA

ArduinoOTA.setHostname("wife-radio");

ArduinoOTA.onStart([]() {

Serial.println("Начало OTA...");

tft.fillScreen(ST7735_RED);

tft.setTextColor(ST7735_WHITE);

tft.println("OTA Обновление...");

});

ArduinoOTA.begin();

Serial.println("OTA готов");


// mDNS

if (MDNS.begin("wife-radio")) {

MDNS.addService("http", "tcp", 80);

Serial.println("mDNS: http://wife-radio.local");

}


Serial.println("ГОТОВО! 📻");

}


// === LOOP ===

void loop() {

static uint32_t lastUpdate = 0;


ArduinoOTA.handle();

handleEncoder();

timeClient.update();


if (millis() - lastUpdate > 200) {

updateDisplay();

lastUpdate = millis();

}


if (WiFi.status() != WL_CONNECTED) {

connectToBestWiFi();

delay(2000);

}


server.handleClient();

delay(1);

}


// === TCA9548A ===

void selectTCA9548AChannel(uint8_t channel) {

if (channel > 7) return;

Wire.beginTransmission(TCA9548A_ADDR);

Wire.write(1 << channel);

Wire.endTransmission();

}


// === FRAM ===

bool framWriteBytes(uint16_t addr, uint8_t* data, uint8_t len) {

selectTCA9548AChannel(0);

Wire.beginTransmission(FRAM_ADDR);

Wire.write((addr >> 8) & 0xFF);

Wire.write(addr & 0xFF);

for (uint8_t i = 0; i < len; i++) Wire.write(data[i]);

return (Wire.endTransmission() == 0);

}


bool framReadBytes(uint16_t addr, uint8_t* data, uint8_t len) {

selectTCA9548AChannel(0);

Wire.beginTransmission(FRAM_ADDR);

Wire.write((addr >> 8) & 0xFF);

Wire.write(addr & 0xFF);

if (Wire.endTransmission() != 0) return false;

Wire.requestFrom((uint8_t)FRAM_ADDR, (uint8_t)len);

uint8_t i = 0;

while (Wire.available() && i < len) data[i++] = Wire.read();

return (i == len);

}


void saveSettingsToFRAM() {

uint8_t data[2] = {currentStation, volume};

framWriteBytes(0, data, 2);

}


void loadSettingsFromFRAM() {

uint8_t data[2];

if (framReadBytes(0, data, 2)) {

currentStation = data[0];

volume = data[1];

if (currentStation >= stationCount) currentStation = 0;

if (volume > 100) volume = 50;

}

}


// === Wi-Fi ===

void connectToBestWiFi() {

WiFi.disconnect();

delay(100);

WiFi.mode(WIFI_STA);


int n = WiFi.scanNetworks();

if (n <= 0) {

connectedSSID = "Сети не найдены";

return;

}


int bestRSSI = -100;

uint8_t bestIndex = 0;

bool found = false;


for (int i = 0; i < n; i++) {

String scanned = WiFi.SSID(i);

int rssi = WiFi.RSSI(i);

for (uint8_t j = 0; j < WIFI_NETWORK_COUNT; j++) {

if (scanned == wifiNetworks[j].ssid && rssi > bestRSSI) {

bestRSSI = rssi;

bestIndex = j;

found = true;

}

}

}


if (!found) {

connectedSSID = "Сеть не найдена";

return;

}


WiFi.begin(wifiNetworks[bestIndex].ssid, wifiNetworks[bestIndex].password);

uint8_t attempts = 0;

while (WiFi.status() != WL_CONNECTED && attempts++ < 20) delay(500);


if (WiFi.status() == WL_CONNECTED) {

connectedSSID = wifiNetworks[bestIndex].ssid;

} else {

connectedSSID = "Ошибка подключения";

}

}


// === АУДИО ===

void playStation(uint8_t index) {

if (index >= stationCount || WiFi.status() != WL_CONNECTED) return;

stopPlaying();

delay(100);


const char* url = stations[index].url;

if (strncmp(url, "http://", 7) != 0) return;


const char* host = url + 7;

const char* path = strchr(host, '/');

if (!path) path = host + strlen(host);


char hostBuf[64];

size_t len = min((size_t)(path - host), sizeof(hostBuf) - 1);

strncpy(hostBuf, host, len);

hostBuf[len] = '\0';


WiFiClient client;

if (!client.connect(hostBuf, 80)) {

currentMetadata = "Ошибка подключения";

return;

}


client.print(String("GET ") + (path[0] ? path : "/") + " HTTP/1.1\r\n"

"Host: " + hostBuf + "\r\n"

"User-Agent: ESP32/WifeRadio\r\n"

"Icy-MetaData: 1\r\n"

"Connection: close\r\n\r\n");


while (client.connected()) {

String line = client.readStringUntil('\n');

if (line == "\r") break;

}


isPlaying = true;

currentStation = index;

currentMetadata = "Подключение...";


uint8_t buffer[32];

while (client.connected() && isPlaying) {

if (client.available()) {

int bytes = client.read(buffer, sizeof(buffer));

if (bytes > 0 && waitForDREQ(10)) {

player.playData(buffer, bytes);

}

}

if (!isPlaying) break;

delay(1);

}

client.stop();

stopPlaying();

}


void playSDFile(const char* filename) {

stopPlaying();

currentFile = SD.open(filename);

if (!currentFile) return;


isPlaying = true;

currentMetadata = String("SD: ") + filename;


while (currentFile.available() && isPlaying) {

if (waitForDREQ(10)) {

uint8_t buffer[32];

size_t bytes = currentFile.read(buffer, sizeof(buffer));

player.playData(buffer, bytes);

}

delay(1);

}

currentFile.close();

stopPlaying();

}


void startRecording() {

if (isRecording) return;

isRecording = true;

isPlaying = false;

currentMetadata = "Запись...";


String filename = "/rec_";

filename += millis();

filename += ".mp3";

currentFile = SD.open(filename.c_str(), FILE_WRITE);

if (!currentFile) {

currentMetadata = "Ошибка записи";

isRecording = false;

return;

}


player.startRecordOgg(true);

delay(100);


while (isRecording) {

if (player.recordedWordsWaiting()) {

uint16_t buffer[32];

uint16_t words = player.recordedRead(buffer, 32);

currentFile.write((uint8_t*)buffer, words * 2);

}

delay(10);

}


player.stopRecord();

currentFile.close();

currentMetadata = "Запись завершена";

}


void stopPlaying() {

player.stopPlaying();

isPlaying = false;

isRecording = false;

if (currentFile) currentFile.close();

currentMetadata = "Остановлено";

}


bool waitForDREQ(unsigned long timeout) {

unsigned long start = millis();

while (!digitalRead(VS1053_DREQ)) {

if (millis() - start > timeout) return false;

delay(1);

}

return true;

}


// === ЭНКОДЕР ===

void handleEncoder() {

static int lastCLK = digitalRead(ENCODER_CLK);

int currentCLK = digitalRead(ENCODER_CLK);


if (currentCLK != lastCLK) {

bool clockwise = (digitalRead(ENCODER_DT) != currentCLK);

if (clockwise) {

if (volume < 100) volume += 5;

else {

currentStation = (currentStation + 1) % stationCount;

if (isPlaying) playStation(currentStation);

}

} else {

if (volume > 0) volume -= 5;

else {

if (currentStation == 0) currentStation = stationCount - 1;

else currentStation--;

if (isPlaying) playStation(currentStation);

}

}

player.setVolume(volume, volume);

saveSettingsToFRAM();

lastCLK = currentCLK;

}


if (digitalRead(ENCODER_SW) == LOW) {

delay(50);

if (digitalRead(ENCODER_SW) == LOW) {

if (isRecording) {

isRecording = false;

} else if (isPlaying) {

stopPlaying();

} else {

playStation(currentStation);

}

while (digitalRead(ENCODER_SW) == LOW) delay(10);

}

}

}


// === ДИСПЛЕЙ ===

void updateDisplay() {

tft.fillScreen(ST7735_BLACK);

tft.setCursor(0, 0);

tft.setTextSize(1);


// Источник

tft.setTextColor(ST7735_WHITE);

tft.println("Источник: Радио");


// Станция

tft.setTextColor(ST7735_YELLOW);

tft.println(stations[currentStation].name);


// Метаданные

tft.setTextColor(ST7735_CYAN);

tft.println(currentMetadata.substring(0, 32));


// Статус

tft.setTextColor(ST7735_WHITE);

tft.println(isPlaying ? "▶ Воспроизведение" : isRecording ? "● Запись" : "⏸ Остановлено");


// Громкость

tft.setTextColor(ST7735_GREEN);

tft.print("Громкость: ");

tft.print(volume);

tft.println("%");


// Сеть

tft.setTextColor(ST7735_BLUE);

tft.print("Wi-Fi: ");

tft.println(connectedSSID.substring(0, 20));


// IP и время

tft.setTextColor(ST7735_MAGENTA);

tft.print("IP: ");

tft.println(WiFi.localIP());

tft.setTextColor(ST7735_RED);

tft.print("Время: ");

tft.println(timeClient.getFormattedTime());

}


// === ВЕБ-СЕРВЕР ===

void setupWebServer() {

server.on("/", HTTP_GET, []() {

String html = "<!DOCTYPE html><html><head><title>Wife Radio</title>";

html += "<style>body{font-family:Arial;margin:20px;background:#f5f5f5;} .status{padding:10px;margin:10px 0;border-radius:5px;}";

html += ".ok{background:#d4edda;color:#155724;border:1px solid #c3e6cb;} .error{background:#f8d7da;color:#721c24;border:1px solid #f5c6cb;}</style></head><body>";


// Device Status

html += "<div class='status ";

if (vs1053Available) html += "ok'>✅ VS1053: Available"; else html += "error'>❌ VS1053: Not Available";

html += "</div>";

html += "<div class='status ";

if (sdCardAvailable) html += "ok'>✅ SD Card: Available"; else html += "error'>❌ SD Card: Not Available";

html += "</div>";


// System Status

html += "<div style='background:#e3f2fd;padding:15px;margin:20px 0;border-radius:5px;'>";

html += "<h3>📊 System Status</h3>";

html += "<p>📶 WiFi: " + connectedSSID + "</p>";

html += "<p>🌐 IP Address: " + WiFi.localIP().toString() + "</p>";

html += "<p>📡 Signal: " + String(WiFi.RSSI()) + " dBm</p>";

html += "<p>🔊 Volume: " + String(volume) + "%</p>";

html += "<p>📡 Status: " + (vs1053Available ? "Ready" : "VS1053 Not Found") + "</p>";

html += "<p>🎵 Source: Radio</p>";

html += "</div>";


// Radio Stations

html += "<h3>📻 Radio Stations</h3>";

for (int i = 0; i < stationCount; i++) {

html += "<button onclick=\"location.href='/station?idx=" + String(i) + "'\" style='margin:5px;padding:10px;'>Station " + String(i+1) + "</button>";

}


// Controls

html += "<h3>🕹 Controls</h3>";

html += "<button onclick=\"location.href='/play'\" style='background:#ffc107;margin:5px;padding:10px;'>▶ Play Radio</button>";

html += "<button onclick=\"location.href='/stop'\" style='background:#007bff;margin:5px;padding:10px;'>⏸ Stop</button>";

html += "<button onclick=\"location.href='/volup'\" style='background:#28a745;margin:5px;padding:10px;'>🔊 Volume +</button>";

html += "<button onclick=\"location.href='/voldn'\" style='background:#007bff;margin:5px;padding:10px;'>🔉 Volume -</button><br>";

html += "<button onclick=\"location.href='/record'\" style='background:#ffc107;margin:5px;padding:10px;'>● Start Record</button>";

html += "<button onclick=\"location.href='/stoprecord'\" style='background:#dc3545;margin:5px;padding:10px;'>■ Stop Record</button>";

html += "<button onclick=\"location.href='/next'\" style='background:#ffc107;margin:5px;padding:10px;'>⏭ Next Station</button><br>";

html += "<button onclick=\"location.href='/scan'\" style='background:#007bff;margin:5px;padding:10px;'>🔍 Scan Networks</button>";

html += "<button onclick=\"location.href='/info'\" style='background:#007bff;margin:5px;padding:10px;'>ℹ️ System Info</button>";

html += "<button onclick=\"location.href='/test'\" style='background:#007bff;margin:5px;padding:10px;'>🔧 Test Functions</button>";


html += "</body></html>";

server.send(200, "text/html", html);

});


server.on("/play", HTTP_GET, []() {

if (!isPlaying && !isRecording) playStation(currentStation);

server.sendHeader("Location", "/");

server.send(303);

});


server.on("/stop", HTTP_GET, []() {

stopPlaying();

server.sendHeader("Location", "/");

server.send(303);

});


server.on("/volup", HTTP_GET, []() {

if (volume < 100) volume += 5;

player.setVolume(volume, volume);

saveSettingsToFRAM();

server.sendHeader("Location", "/");

server.send(303);

});


server.on("/voldn", HTTP_GET, []() {

if (volume > 0) volume -= 5;

player.setVolume(volume, volume);

saveSettingsToFRAM();

server.sendHeader("Location", "/");

server.send(303);

});


server.on("/record", HTTP_GET, []() {

if (!isRecording && !isPlaying) startRecording();

server.sendHeader("Location", "/");

server.send(303);

});


server.on("/stoprecord", HTTP_GET, []() {

isRecording = false;

stopPlaying();

server.sendHeader("Location", "/");

server.send(303);

});


server.on("/next", HTTP_GET, []() {

currentStation = (currentStation + 1) % stationCount;

if (isPlaying) playStation(currentStation);

saveSettingsToFRAM();

server.sendHeader("Location", "/");

server.send(303);

});


server.on("/station", HTTP_GET, []() {

String idxStr = server.arg("idx");

int idx = idxStr.toInt();

if (idx >= 0 && idx < stationCount) {

currentStation = idx;

if (isPlaying) playStation(currentStation);

saveSettingsToFRAM();

}

server.sendHeader("Location", "/");

server.send(303);

});


server.on("/scan", HTTP_GET, []() {

connectToBestWiFi();

server.sendHeader("Location", "/");

server.send(303);

});


server.on("/info", HTTP_GET, []() {

String html = "<h1>System Info</h1>";

html += "<p>ESP32 Chip ID: " + String(ESP.getEfuseMac(), HEX) + "</p>";

html += "<p>Free Heap: " + String(ESP.getFreeHeap()) + " bytes</p>";

html += "<p>Sketch Size: " + String(ESP.getSketchSize()) + " bytes</p>";

html += "<p>SDK Version: " + String(ESP.getSdkVersion()) + "</p>";

server.send(200, "text/html", html);

});


server.on("/test", HTTP_GET, []() {

String html = "<h1>Test Functions</h1>";

html += "<p>Testing VS1053: " + (vs1053Available ? "OK" : "FAIL") + "</p>";

html += "<p>Testing SD: " + (sdCardAvailable ? "OK" : "FAIL") + "</p>";

html += "<p>Testing FRAM: " + (framReadBytes(0, new uint8_t[2], 2) ? "OK" : "FAIL") + "</p>";

server.send(200, "text/html", html);

});


server.begin();

}

 
MyTetra Share v.0.67
Яндекс индекс цитирования