//https://github.com/MagicBulletPro/esp32-bluetooth-slave
//https://github.com/MagicBulletPro/esp32-bluetooth-master
// =========================================================================
// --- LIBRERÍAS COMUNES ---
// =========================================================================
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <ESP32Encoder.h>
#include <arduinoFFT.h>
// =========================================================================
// --- CONFIGURACIONES GLOBALES DE HARDWARE Y PANTALLA ---
// =========================================================================
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
#define SCREEN_ADDRESS 0x3C // Usar un solo nombre para la dirección
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
// --- PINES DE ENCODERS ---
#define VOL_CLK_PIN 12
#define VOL_DT_PIN 13
#define GAIN_CLK_PIN 1 //TX
#define GAIN_DT_PIN 3 //RX
#define HIGH_CLK_PIN 34
#define HIGH_DT_PIN 35
#define MID_HIGH_CLK_PIN 32
#define MID_HIGH_DT_PIN 33
#define MID_LOW_CLK_PIN 25
#define MID_LOW_DT_PIN 26
#define LOW_CLK_PIN 27
#define LOW_DT_PIN 14
// --- PIN DE ENTRADA DE AUDIO ---
#define AUDIO_IN_PIN 36 // ADC1_CH0 en ESP32
// =========================================================================
// --- PARÁMETROS DE FUNCIONAMIENTO ---
// =========================================================================
// Para los encoders
#define ENCODER_STEPS_PER_DB 2
const int VOL_MIN = -80, VOL_MAX = 10;
const int GAIN_MIN = -20, GAIN_MAX = 20;
const int EQ_MIN = -15, EQ_MAX = 15;
// Para el Analizador de Espectro
#define SAMPLES 128
#define SAMPLING_FREQUENCY 40000.0
#define AMPLITUDE 1000 // Sensibilidad: un número más bajo = barras más altas
#define MAX_FREQ 15000.0
const double FREQ_RESOLUTION = SAMPLING_FREQUENCY / SAMPLES;
const int MIN_BIN_TO_DISPLAY = 1;
const int MAX_BIN_TO_DISPLAY = (int)(MAX_FREQ / FREQ_RESOLUTION);
#define SCALE_HEIGHT 10
#define GRAPH_HEIGHT (SCREEN_HEIGHT - SCALE_HEIGHT)
// Para la lógica de la interfaz de usuario
const unsigned long SCREEN_TIMEOUT_MS = 3000; // 3 segundos para volver al analizador
// ---> FUSIÓN: Se añade el estado del ANALIZADOR al enumerador.
enum DisplayState {
STATE_ANALYZER, // Esta será la pantalla por defecto
STATE_VOLUMEN,
STATE_GAIN,
STATE_HIGH,
STATE_MID_HIGH,
STATE_MID_LOW,
STATE_LOW
};
// =========================================================================
// --- OBJETOS Y VARIABLES GLOBALES ---
// =========================================================================
// --- Objetos ---
ESP32Encoder encoderVol, encoderGain, encoderHigh, encoderMidHigh, encoderMidLow, encoderLow;
double vReal[SAMPLES], vImag[SAMPLES];
ArduinoFFT<double> fft = ArduinoFFT<double>(vReal, vImag, SAMPLES, SAMPLING_FREQUENCY);
// --- Variables de Estado ---
int currentVolDB = 0, currentGainDB = 0, currentHighDB = 0;
int currentMidHighDB = 0, currentMidLowDB = 0, currentLowDB = 0;
// ---> FUSIÓN: El estado inicial ahora es el analizador.
DisplayState currentDisplayState = STATE_ANALYZER;
unsigned long lastActivityTimestamp = 0;
bool needsDisplayUpdate = true; // Para forzar actualización de pantallas de parámetros
// =========================================================================
// --- PROTOTIPOS DE FUNCIONES ---
// =========================================================================
void drawParameterScreen();
void drawSingleParameter(const char *paramName, int currentValue, int minValue, int maxValue);
void drawSpectrumAnalyzer();
void drawFrequencyScale();
// =========================================================================
// --- SETUP ---
// =========================================================================
void setup() {
Wire.begin();
// --- Inicialización de Encoders ---
encoderVol.attachHalfQuad(VOL_DT_PIN, VOL_CLK_PIN);
encoderVol.setCount(VOL_MIN * ENCODER_STEPS_PER_DB);
currentVolDB = VOL_MIN;
// ... (el resto de inicializaciones de encoders es idéntico)
encoderGain.attachHalfQuad(GAIN_DT_PIN, GAIN_CLK_PIN);
encoderGain.setCount(0 * ENCODER_STEPS_PER_DB);
encoderHigh.attachHalfQuad(HIGH_DT_PIN, HIGH_CLK_PIN);
encoderHigh.setCount(0 * ENCODER_STEPS_PER_DB);
encoderMidHigh.attachHalfQuad(MID_HIGH_DT_PIN, MID_HIGH_CLK_PIN);
encoderMidHigh.setCount(0 * ENCODER_STEPS_PER_DB);
encoderMidLow.attachHalfQuad(MID_LOW_DT_PIN, MID_LOW_CLK_PIN);
encoderMidLow.setCount(0 * ENCODER_STEPS_PER_DB);
encoderLow.attachHalfQuad(LOW_DT_PIN, LOW_CLK_PIN);
encoderLow.setCount(0 * ENCODER_STEPS_PER_DB);
// --- Inicialización de la pantalla OLED y Splash Screen ---
display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS);
display.clearDisplay();
display.setTextSize(2);
display.setTextColor(SSD1306_WHITE);
display.setCursor(15, 24);
display.println("PATRO DJ");
display.display();
delay(1500);
}
// =========================================================================
// --- LOOP PRINCIPAL ---
// =========================================================================
void loop() {
bool activityDetected = false;
// --- 1. LEER ENCODERS Y DETECTAR ACTIVIDAD ---
// (El código de lectura es el mismo, solo se añade la detección de actividad)
long newVol = encoderVol.getCount() / ENCODER_STEPS_PER_DB;
if (newVol > VOL_MAX) { newVol = VOL_MAX; encoderVol.setCount(newVol * ENCODER_STEPS_PER_DB); }
if (newVol < VOL_MIN) { newVol = VOL_MIN; encoderVol.setCount(newVol * ENCODER_STEPS_PER_DB); }
if (newVol != currentVolDB) {
currentVolDB = newVol;
currentDisplayState = STATE_VOLUMEN;
activityDetected = true;
}
// Repetir para los demás encoders...
long newGain = encoderGain.getCount() / ENCODER_STEPS_PER_DB;
if (newGain > GAIN_MAX) { newGain = GAIN_MAX; encoderGain.setCount(newGain * ENCODER_STEPS_PER_DB); }
if (newGain < GAIN_MIN) { newGain = GAIN_MIN; encoderGain.setCount(newGain * ENCODER_STEPS_PER_DB); }
if (newGain != currentGainDB) {
currentGainDB = newGain;
currentDisplayState = STATE_GAIN;
activityDetected = true;
}
long newHigh = encoderHigh.getCount() / ENCODER_STEPS_PER_DB;
if (newHigh > EQ_MAX) { newHigh = EQ_MAX; encoderHigh.setCount(newHigh * ENCODER_STEPS_PER_DB); }
if (newHigh < EQ_MIN) { newHigh = EQ_MIN; encoderHigh.setCount(newHigh * ENCODER_STEPS_PER_DB); }
if (newHigh != currentHighDB) {
currentHighDB = newHigh;
currentDisplayState = STATE_HIGH;
activityDetected = true;
}
long newMidHigh = encoderMidHigh.getCount() / ENCODER_STEPS_PER_DB;
if (newMidHigh > EQ_MAX) { newMidHigh = EQ_MAX; encoderMidHigh.setCount(newMidHigh * ENCODER_STEPS_PER_DB); }
if (newMidHigh < EQ_MIN) { newMidHigh = EQ_MIN; encoderMidHigh.setCount(newMidHigh * ENCODER_STEPS_PER_DB); }
if (newMidHigh != currentMidHighDB) {
currentMidHighDB = newMidHigh;
currentDisplayState = STATE_MID_HIGH;
activityDetected = true;
}
long newMidLow = encoderMidLow.getCount() / ENCODER_STEPS_PER_DB;
if (newMidLow > EQ_MAX) { newMidLow = EQ_MAX; encoderMidLow.setCount(newMidLow * ENCODER_STEPS_PER_DB); }
if (newMidLow < EQ_MIN) { newMidLow = EQ_MIN; encoderMidLow.setCount(newMidLow * ENCODER_STEPS_PER_DB); }
if (newMidLow != currentMidLowDB) {
currentMidLowDB = newMidLow;
currentDisplayState = STATE_MID_LOW;
activityDetected = true;
}
long newLow = encoderLow.getCount() / ENCODER_STEPS_PER_DB;
if (newLow > EQ_MAX) { newLow = EQ_MAX; encoderLow.setCount(newLow * ENCODER_STEPS_PER_DB); }
if (newLow < EQ_MIN) { newLow = EQ_MIN; encoderLow.setCount(newLow * ENCODER_STEPS_PER_DB); }
if (newLow != currentLowDB) {
currentLowDB = newLow;
currentDisplayState = STATE_LOW;
activityDetected = true;
}
// --- 2. GESTIONAR TEMPORIZADOR Y ESTADO DE PANTALLA ---
if (activityDetected) {
lastActivityTimestamp = millis(); // Reinicia el temporizador
needsDisplayUpdate = true; // Marca que la pantalla de parámetro necesita ser dibujada
}
// Si no estamos en el analizador y ha pasado el tiempo de timeout...
if (currentDisplayState != STATE_ANALYZER && (millis() - lastActivityTimestamp > SCREEN_TIMEOUT_MS)) {
currentDisplayState = STATE_ANALYZER; // ...vuelve al analizador
}
// --- 3. DIBUJAR EN PANTALLA SEGÚN EL ESTADO ACTUAL ---
if (currentDisplayState == STATE_ANALYZER) {
// El analizador se dibuja continuamente en cada ciclo del loop para ser "en tiempo real"
drawSpectrumAnalyzer();
} else {
// Las pantallas de parámetros solo se dibujan una vez cuando hay un cambio
if (needsDisplayUpdate) {
drawParameterScreen();
needsDisplayUpdate = false; // Evita redibujar hasta la próxima actividad
}
}
// No es necesario un delay aquí, ya que el muestreo y FFT controlan el ritmo.
}
// =========================================================================
// --- FUNCIONES DE DIBUJO ---
// =========================================================================
// ---> FUSIÓN: Esta función actúa como el antiguo 'updateDisplay'
// Dibuja la pantalla de parámetro que corresponda
void drawParameterScreen() {
switch (currentDisplayState) {
case STATE_VOLUMEN:
drawSingleParameter("VOLUMEN", currentVolDB, VOL_MIN, VOL_MAX);
break;
case STATE_GAIN:
drawSingleParameter("GAIN", currentGainDB, GAIN_MIN, GAIN_MAX);
break;
case STATE_HIGH:
drawSingleParameter("HIGH", currentHighDB, EQ_MIN, EQ_MAX);
break;
case STATE_MID_HIGH:
drawSingleParameter("MID HIGH", currentMidHighDB, EQ_MIN, EQ_MAX);
break;
case STATE_MID_LOW:
drawSingleParameter("MID LOW", currentMidLowDB, EQ_MIN, EQ_MAX);
break;
case STATE_LOW:
drawSingleParameter("LOW", currentLowDB, EQ_MIN, EQ_MAX);
break;
default: // No hacer nada si el estado es ANALYZER
break;
}
}
// ---> FUSIÓN: El antiguo 'actualizarPantallaParametro' renombrado para mayor claridad
// Dibuja una pantalla de parámetro individual con su barra
void drawSingleParameter(const char *paramName, int currentValue, int minValue, int maxValue) {
display.clearDisplay();
// 1. Dibuja el nombre y el valor numérico
display.setTextSize(2);
display.setTextColor(SSD1306_WHITE);
int16_t x1, y1;
uint16_t w, h;
display.getTextBounds(paramName, 0, 0, &x1, &y1, &w, &h);
display.setCursor((SCREEN_WIDTH - w) / 2, 2);
display.println(paramName);
char dbStr[10];
sprintf(dbStr, "%+d dB", currentValue);
display.setTextSize(2); // Asegurar tamaño
display.getTextBounds(dbStr, 0, 0, &x1, &y1, &w, &h);
display.setCursor((SCREEN_WIDTH - w) / 2, 24); // Posición ajustada
display.print(dbStr);
// 2. Dibuja la barra de progreso
int barWidth = map(currentValue, minValue, maxValue, 0, SCREEN_WIDTH-1);
display.drawRect(0, 50, SCREEN_WIDTH, 14, SSD1306_WHITE);
if (minValue < 0 && maxValue > 0) { // Barra bipolar
int zeroPoint = map(0, minValue, maxValue, 0, SCREEN_WIDTH-1);
display.drawFastVLine(zeroPoint, 48, 18, SSD1306_WHITE);
if (barWidth > zeroPoint) {
display.fillRect(zeroPoint, 50, barWidth - zeroPoint, 14, SSD1306_WHITE);
} else {
display.fillRect(barWidth, 50, zeroPoint - barWidth, 14, SSD1306_WHITE);
}
} else { // Barra unipolar
display.fillRect(0, 50, barWidth, 14, SSD1306_WHITE);
}
display.display();
}
// ---> FUSIÓN: Toda la lógica del analizador de espectro dentro de una función
void drawSpectrumAnalyzer() {
// 1. Muestreo de la señal de audio
for (int i = 0; i < SAMPLES; i++) {
vReal[i] = (double)analogRead(AUDIO_IN_PIN) - 2048.0;
vImag[i] = 0.0;
}
// 2. Procesamiento FFT
fft.windowing(FFT_WIN_TYP_HAMMING, FFT_FORWARD);
fft.compute(FFT_FORWARD);
fft.complexToMagnitude();
// 3. Visualización en la Pantalla OLED
display.clearDisplay();
for (int x = 0; x < SCREEN_WIDTH; x++) {
int bin = map(x, 0, SCREEN_WIDTH - 1, MIN_BIN_TO_DISPLAY, MAX_BIN_TO_DISPLAY);
int magnitude = vReal[bin] / AMPLITUDE;
magnitude = constrain(magnitude, 0, GRAPH_HEIGHT);
display.drawLine(x, GRAPH_HEIGHT - 1, x, GRAPH_HEIGHT - 1 - magnitude, WHITE);
}
drawFrequencyScale();
display.display();
}
// Función de ayuda para dibujar la escala de frecuencia (sin cambios)
void drawFrequencyScale() {
display.setTextSize(1);
display.setTextColor(WHITE);
int textY = SCREEN_HEIGHT - 7;
display.drawFastHLine(0, GRAPH_HEIGHT, SCREEN_WIDTH, WHITE);
int bin_1k = (int)(1000 / FREQ_RESOLUTION);
int x_1k = map(bin_1k, MIN_BIN_TO_DISPLAY, MAX_BIN_TO_DISPLAY, 0, SCREEN_WIDTH - 1);
display.setCursor(x_1k, textY);
display.print("1k");
display.drawFastVLine(x_1k, GRAPH_HEIGHT, 3, WHITE);
int bin_5k = (int)(5000 / FREQ_RESOLUTION);
int x_5k = map(bin_5k, MIN_BIN_TO_DISPLAY, MAX_BIN_TO_DISPLAY, 0, SCREEN_WIDTH - 1);
display.setCursor(x_5k + 2, textY);
display.print("5k");
display.drawFastVLine(x_5k, GRAPH_HEIGHT, 3, WHITE);
int bin_10k = (int)(10000 / FREQ_RESOLUTION);
int x_10k = map(bin_10k, MIN_BIN_TO_DISPLAY, MAX_BIN_TO_DISPLAY, 0, SCREEN_WIDTH - 1);
display.setCursor(x_10k - 4, textY);
display.print("10k");
display.drawFastVLine(x_10k, GRAPH_HEIGHT, 3, WHITE);
display.setCursor(SCREEN_WIDTH - 18, textY);
display.print("15k");
display.drawFastVLine(SCREEN_WIDTH - 1, GRAPH_HEIGHT, 3, WHITE);
}