// =====================================================================================
// === PROYECTO MAESTRO (CON BLUETOOTH Y TIMEOUT AUTÓNOMO) - CORREGIDO ===
// =====================================================================================
#include <Wire.h>
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <ESP32Encoder.h>
#include <math.h>
#include <BluetoothSerial.h>
// --- CONFIGURACIÓN DE BLUETOOTH ---
BluetoothSerial SerialBT;
// ... (El resto de las configuraciones y pines son idénticos y se omiten por brevedad) ...
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
#define SCREEN_ADDRESS 0x3C
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
#define VOL_CLK_PIN 12
#define VOL_DT_PIN 13
#define GAIN_CLK_PIN 1
#define GAIN_DT_PIN 3
#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
#define VOL_CS_PIN 15
#define GAIN_CS_PIN 5
#define HIGH_CS_PIN 17
#define MID_HIGH_CS_PIN 16
#define MID_LOW_CS_PIN 4
#define LOW_CS_PIN 2
#define VOLTAGE_IN_L 36
#define VOLTAGE_IN_R 39
#define SAMPLES_AVG 64
#define ADC_MAX_VALUE 4095
#define ADC_REF_0DB 1106.0
#define DB_MIN -30.0
#define DB_MAX 11.4
#define PEAK_HOLD_MS 1500
#define PEAK_DECAY_RATE 0.35
#define DISPLAY_TIMEOUT_MS 3000
const int POT_MAX_STEPS = 255;
#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;
ESP32Encoder encoderVol;
ESP32Encoder encoderGain;
ESP32Encoder encoderHigh;
ESP32Encoder encoderMidHigh;
ESP32Encoder encoderMidLow;
ESP32Encoder encoderLow;
int currentVolDB = 0;
int currentGainDB = 0;
int currentHighDB = 0;
int currentMidHighDB = 0;
int currentMidLowDB = 0;
int currentLowDB = 0;
double peakL = DB_MIN;
double peakR = DB_MIN;
unsigned long lastPeakTimeL = 0;
unsigned long lastPeakTimeR = 0;
// Variables para gestionar el timeout y el estado de funcionamiento
const unsigned long CONNECTION_TIMEOUT_MS = 5000; // 5 segundos de espera
bool initialWaitOver = false; // Indica si ya pasó la fase inicial de espera
unsigned long setupCompleteTime = 0; // Guardará el tiempo cuando termine el setup
enum DisplayMode { MODE_VUMETER, MODE_PARAM }; // Ya no necesitamos MODE_WAITING
DisplayMode currentMode = MODE_VUMETER; // Empezamos asumiendo que iremos al vúmetro
unsigned long lastInteractionTime = 0;
bool clientConnected = false;
// --- PROTOTIPOS DE FUNCIONES ---
void writeToPot(int csPin, byte address, byte value);
void setPotValue(int csPin, int dbValue, int dbMin, int dbMax);
void actualizarPantallaParametro(const char *paramName, int currentValue, int minValue, int maxValue);
void drawVUMeterBar(int x_start, int y_start, double dbValue, double peakDbValue);
void actualizarVUMeter();
void handleBluetoothCommands();
void showWaitingScreen();
void showStandaloneModeScreen();
// =====================================================================================
// === SETUP ===
// =====================================================================================
void setup() {
SPI.begin();
Wire.begin();
display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS);
SerialBT.begin("PatroDJ_Master");
showWaitingScreen(); // Muestra la pantalla de espera
// ... (El resto de la inicialización es la misma) ...
pinMode(VOL_CS_PIN, OUTPUT); digitalWrite(VOL_CS_PIN, HIGH);
pinMode(GAIN_CS_PIN, OUTPUT); digitalWrite(GAIN_CS_PIN, HIGH);
pinMode(HIGH_CS_PIN, OUTPUT); digitalWrite(HIGH_CS_PIN, HIGH);
pinMode(MID_HIGH_CS_PIN, OUTPUT); digitalWrite(MID_HIGH_CS_PIN, HIGH);
pinMode(MID_LOW_CS_PIN, OUTPUT); digitalWrite(MID_LOW_CS_PIN, HIGH);
pinMode(LOW_CS_PIN, OUTPUT); digitalWrite(LOW_CS_PIN, HIGH);
encoderVol.attachHalfQuad(VOL_DT_PIN, VOL_CLK_PIN);
encoderVol.setCount(VOL_MIN * ENCODER_STEPS_PER_DB);
currentVolDB = VOL_MIN;
encoderGain.attachHalfQuad(GAIN_DT_PIN, GAIN_CLK_PIN);
encoderGain.setCount(0);
encoderHigh.attachHalfQuad(HIGH_DT_PIN, HIGH_CLK_PIN);
encoderHigh.setCount(0);
encoderMidHigh.attachHalfQuad(MID_HIGH_DT_PIN, MID_HIGH_CLK_PIN);
encoderMidHigh.setCount(0);
encoderMidLow.attachHalfQuad(MID_LOW_DT_PIN, MID_LOW_CLK_PIN);
encoderMidLow.setCount(0);
encoderLow.attachHalfQuad(LOW_DT_PIN, LOW_CLK_PIN);
encoderLow.setCount(0);
setPotValue(VOL_CS_PIN, currentVolDB, VOL_MIN, VOL_MAX);
setPotValue(GAIN_CS_PIN, 0, GAIN_MIN, GAIN_MAX);
setPotValue(HIGH_CS_PIN, 0, EQ_MIN, EQ_MAX);
setPotValue(MID_HIGH_CS_PIN, 0, EQ_MIN, EQ_MAX);
setPotValue(MID_LOW_CS_PIN, 0, EQ_MIN, EQ_MAX);
setPotValue(LOW_CS_PIN, 0, EQ_MIN, EQ_MAX);
// Guardar el tiempo al final del setup para iniciar el contador del timeout
setupCompleteTime = millis();
}
// =====================================================================================
// === MAIN LOOP ===
// =====================================================================================
void loop() {
// <<-- CORRECCIÓN: CAMBIO ESTRUCTURAL PRINCIPAL
// Primero, gestionamos la fase de espera. Si aún no ha terminado, no hacemos nada más.
// Si ya terminó, pasamos directamente a la lógica de funcionamiento normal.
if (!initialWaitOver) {
// --- FASE DE ESPERA INICIAL ---
if (SerialBT.hasClient()) {
// Se conectó un cliente dentro del tiempo de espera
clientConnected = true;
initialWaitOver = true; // Finaliza la espera
display.clearDisplay();
display.setTextSize(2);
display.setTextColor(SSD1306_WHITE);
display.setCursor(1, 24);
display.println("ESCLAVO");
display.setCursor(1, 40);
display.println("CONECTADO!");
display.display();
delay(2000);
lastInteractionTime = millis(); // Iniciar el temporizador de la pantalla
} else if (millis() - setupCompleteTime > CONNECTION_TIMEOUT_MS) {
// Se agotó el tiempo de espera, pasamos a modo autónomo
clientConnected = false;
initialWaitOver = true; // Finaliza la espera
showStandaloneModeScreen();
delay(2000);
lastInteractionTime = millis(); // Iniciar el temporizador de la pantalla
}
// Si ninguna de las condiciones anteriores se cumple, el loop() simplemente
// terminará y volverá a empezar, quedándose en esta sección hasta que
// se conecte un cliente o se agote el tiempo. NO se ejecuta el resto del código.
} else {
// --- FASE DE FUNCIONAMIENTO NORMAL (Solo se ejecuta después de la espera inicial) ---
// Gestionar desconexiones y reconexiones sin detener el programa
if (clientConnected && !SerialBT.hasClient()) {
clientConnected = false;
// Opcional: mostrar un mensaje temporal de "Esclavo Desconectado"
}
if (!clientConnected && SerialBT.hasClient()) {
clientConnected = true;
// Opcional: mostrar un mensaje temporal de "Esclavo Reconectado"
}
bool interaction = false;
// --- 0. PROCESAR COMANDOS BLUETOOTH (Solo si hay un cliente conectado) ---
if (clientConnected && SerialBT.available()) {
handleBluetoothCommands();
interaction = true;
}
// --- 1. LEER ENCODERS LOCALES Y ACTUALIZAR PARÁMETROS ---
long newVol = encoderVol.getCount() / ENCODER_STEPS_PER_DB;
if (newVol != currentVolDB) {
currentVolDB = constrain(newVol, VOL_MIN, VOL_MAX);
encoderVol.setCount(currentVolDB * ENCODER_STEPS_PER_DB);
actualizarPantallaParametro("VOLUMEN", currentVolDB, VOL_MIN, VOL_MAX);
setPotValue(VOL_CS_PIN, currentVolDB, VOL_MIN, VOL_MAX);
if (clientConnected) { // Enviar por Bluetooth solo si está conectado
SerialBT.printf("V%d;", currentVolDB);
}
interaction = true;
}
long newGain = encoderGain.getCount() / ENCODER_STEPS_PER_DB;
if (newGain != currentGainDB) {
currentGainDB = constrain(newGain, GAIN_MIN, GAIN_MAX);
encoderGain.setCount(currentGainDB * ENCODER_STEPS_PER_DB);
actualizarPantallaParametro("GAIN", currentGainDB, GAIN_MIN, GAIN_MAX);
setPotValue(GAIN_CS_PIN, currentGainDB, GAIN_MIN, GAIN_MAX);
if (clientConnected) {
SerialBT.printf("G%d;", currentGainDB);
}
interaction = true;
}
long newHigh = encoderHigh.getCount() / ENCODER_STEPS_PER_DB;
if (newHigh != currentHighDB) {
currentHighDB = constrain(newHigh, EQ_MIN, EQ_MAX);
encoderHigh.setCount(currentHighDB * ENCODER_STEPS_PER_DB);
actualizarPantallaParametro("HIGH", currentHighDB, EQ_MIN, EQ_MAX);
setPotValue(HIGH_CS_PIN, currentHighDB, EQ_MIN, EQ_MAX);
if (clientConnected) SerialBT.printf("H%d;", currentHighDB);
interaction = true;
}
long newMidHigh = encoderMidHigh.getCount() / ENCODER_STEPS_PER_DB;
if (newMidHigh != currentMidHighDB) {
currentMidHighDB = constrain(newMidHigh, EQ_MIN, EQ_MAX);
encoderMidHigh.setCount(currentMidHighDB * ENCODER_STEPS_PER_DB);
actualizarPantallaParametro("MID HIGH", currentMidHighDB, EQ_MIN, EQ_MAX);
setPotValue(MID_HIGH_CS_PIN, currentMidHighDB, EQ_MIN, EQ_MAX);
if (clientConnected) SerialBT.printf("M%d;", currentMidHighDB);
interaction = true;
}
long newMidLow = encoderMidLow.getCount() / ENCODER_STEPS_PER_DB;
if (newMidLow != currentMidLowDB) {
currentMidLowDB = constrain(newMidLow, EQ_MIN, EQ_MAX);
encoderMidLow.setCount(currentMidLowDB * ENCODER_STEPS_PER_DB);
actualizarPantallaParametro("MID LOW", currentMidLowDB, EQ_MIN, EQ_MAX);
setPotValue(MID_LOW_CS_PIN, currentMidLowDB, EQ_MIN, EQ_MAX);
if (clientConnected) SerialBT.printf("N%d;", currentMidLowDB);
interaction = true;
}
long newLow = encoderLow.getCount() / ENCODER_STEPS_PER_DB;
if (newLow != currentLowDB) {
currentLowDB = constrain(newLow, EQ_MIN, EQ_MAX);
encoderLow.setCount(currentLowDB * ENCODER_STEPS_PER_DB);
actualizarPantallaParametro("LOW", currentLowDB, EQ_MIN, EQ_MAX);
setPotValue(LOW_CS_PIN, currentLowDB, EQ_MIN, EQ_MAX);
if (clientConnected) SerialBT.printf("L%d;", currentLowDB);
interaction = true;
}
// --- 2. GESTIONAR EL MODO DE LA PANTALLA ---
if (interaction) {
lastInteractionTime = millis();
currentMode = MODE_PARAM;
} else {
if (currentMode == MODE_PARAM && (millis() - lastInteractionTime > DISPLAY_TIMEOUT_MS)) {
currentMode = MODE_VUMETER;
}
}
// --- 3. DIBUJAR EN PANTALLA SEGÚN EL MODO ---
if (currentMode == MODE_VUMETER) {
actualizarVUMeter();
}
// <<-- CORRECCIÓN: Un pequeño delay general para estabilizar el loop
delay(20);
} // Fin del 'else' de la fase de funcionamiento normal
}
// ... (El resto de funciones auxiliares no necesitan cambios) ...
void showWaitingScreen() {
display.clearDisplay();
display.setTextSize(2);
display.setTextColor(SSD1306_WHITE);
display.setCursor(20, 10);
display.println("MAESTRO");
display.setTextSize(1);
display.setCursor(5, 40);
display.print("Esperando Esclavo...");
display.display();
}
void showStandaloneModeScreen() {
display.clearDisplay();
display.setTextSize(2);
display.setTextColor(SSD1306_WHITE);
display.setCursor(20, 10);
display.println("MAESTRO");
display.setTextSize(1);
display.setCursor(10, 40);
display.print("Modo Autonomo ON");
display.display();
}
void handleBluetoothCommands() {
String message = SerialBT.readStringUntil(';');
if (message.length() > 1) {
char command = message.charAt(0);
int value = message.substring(1).toInt();
switch (command) {
case 'V':
currentVolDB = constrain(value, VOL_MIN, VOL_MAX);
encoderVol.setCount(currentVolDB * ENCODER_STEPS_PER_DB);
setPotValue(VOL_CS_PIN, currentVolDB, VOL_MIN, VOL_MAX);
actualizarPantallaParametro("VOLUMEN", currentVolDB, VOL_MIN, VOL_MAX);
break;
case 'G':
currentGainDB = constrain(value, GAIN_MIN, GAIN_MAX);
encoderGain.setCount(currentGainDB * ENCODER_STEPS_PER_DB);
setPotValue(GAIN_CS_PIN, currentGainDB, GAIN_MIN, GAIN_MAX);
actualizarPantallaParametro("GAIN", currentGainDB, GAIN_MIN, GAIN_MAX);
break;
case 'H':
currentHighDB = constrain(value, EQ_MIN, EQ_MAX);
encoderHigh.setCount(currentHighDB * ENCODER_STEPS_PER_DB);
setPotValue(HIGH_CS_PIN, currentHighDB, EQ_MIN, EQ_MAX);
actualizarPantallaParametro("HIGH", currentHighDB, EQ_MIN, EQ_MAX);
break;
case 'M': // Mid High
currentMidHighDB = constrain(value, EQ_MIN, EQ_MAX);
encoderMidHigh.setCount(currentMidHighDB * ENCODER_STEPS_PER_DB);
setPotValue(MID_HIGH_CS_PIN, currentMidHighDB, EQ_MIN, EQ_MAX);
actualizarPantallaParametro("MID HIGH", currentMidHighDB, EQ_MIN, EQ_MAX);
break;
case 'N': // Mid Low
currentMidLowDB = constrain(value, EQ_MIN, EQ_MAX);
encoderMidLow.setCount(currentMidLowDB * ENCODER_STEPS_PER_DB);
setPotValue(MID_LOW_CS_PIN, currentMidLowDB, EQ_MIN, EQ_MAX);
actualizarPantallaParametro("MID LOW", currentMidLowDB, EQ_MIN, EQ_MAX);
break;
case 'L': // Low
currentLowDB = constrain(value, EQ_MIN, EQ_MAX);
encoderLow.setCount(currentLowDB * ENCODER_STEPS_PER_DB);
setPotValue(LOW_CS_PIN, currentLowDB, EQ_MIN, EQ_MAX);
actualizarPantallaParametro("LOW", currentLowDB, EQ_MIN, EQ_MAX);
break;
}
}
}
void writeToPot(int csPin, byte address, byte value) {
SPI.beginTransaction(SPISettings(1000000, MSBFIRST, SPI_MODE0));
digitalWrite(csPin, LOW);
SPI.transfer(address);
SPI.transfer(value);
digitalWrite(csPin, HIGH);
SPI.endTransaction();
}
void setPotValue(int csPin, int dbValue, int dbMin, int dbMax) {
long potStep = map(dbValue, dbMin, dbMax, POT_MAX_STEPS, 0);
potStep = constrain(potStep, 0, POT_MAX_STEPS);
writeToPot(csPin, 0x00, (byte)potStep);
writeToPot(csPin, 0x10, (byte)potStep);
}
void actualizarPantallaParametro(const char *paramName, int currentValue, int minValue, int maxValue) {
display.clearDisplay();
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.getTextBounds(dbStr, 0, 0, &x1, &y1, &w, &h);
display.setCursor((SCREEN_WIDTH - w) / 2, 20);
display.print(dbStr);
int barWidth = map(currentValue, minValue, maxValue, 0, SCREEN_WIDTH);
display.drawRect(0, 42, SCREEN_WIDTH, 16, SSD1306_WHITE);
if (minValue < 0 && maxValue > 0) {
int zeroPoint = map(0, minValue, maxValue, 0, SCREEN_WIDTH);
display.drawFastVLine(zeroPoint, 40, 20, SSD1306_WHITE);
if (barWidth > zeroPoint) {
display.fillRect(zeroPoint, 42, barWidth - zeroPoint, 16, SSD1306_WHITE);
} else {
display.fillRect(barWidth, 42, zeroPoint - barWidth, 16, SSD1306_WHITE);
}
} else {
display.fillRect(0, 42, barWidth, 16, SSD1306_WHITE);
}
display.display();
}
void drawVUMeterBar(int x_start, int y_start, double dbValue, double peakDbValue) {
#define BAR_LENGTH 100
#define BAR_HEIGHT 20
#define SEGMENT_WIDTH 3
#define SEGMENT_SPACING 1
int totalSegments = BAR_LENGTH / (SEGMENT_WIDTH + SEGMENT_SPACING);
display.drawRect(x_start - 1, y_start - 1, BAR_LENGTH + 2, BAR_HEIGHT + 2, SSD1306_WHITE);
int activeSegments = map(dbValue, DB_MIN, DB_MAX, 0, totalSegments);
activeSegments = constrain(activeSegments, 0, totalSegments);
for (int i = 0; i < activeSegments; i++) {
int xPos = x_start + (i * (SEGMENT_WIDTH + SEGMENT_SPACING));
display.fillRect(xPos, y_start, SEGMENT_WIDTH, BAR_HEIGHT, SSD1306_WHITE);
}
int peakSegment = map(peakDbValue, DB_MIN, DB_MAX, 0, totalSegments);
peakSegment = constrain(peakSegment, 0, totalSegments);
int peakXPos = x_start + (peakSegment * (SEGMENT_WIDTH + SEGMENT_SPACING));
if (peakXPos < x_start + BAR_LENGTH) {
display.fillRect(peakXPos, y_start, SEGMENT_WIDTH, BAR_HEIGHT, SSD1306_WHITE);
}
}
void actualizarVUMeter() {
unsigned long totalL = 0;
unsigned long totalR = 0;
for (int i = 0; i < SAMPLES_AVG; i++) {
totalL += analogRead(VOLTAGE_IN_L);
totalR += analogRead(VOLTAGE_IN_R);
}
double dbL = (totalL == 0) ? DB_MIN : 20 * log10((double)(totalL / SAMPLES_AVG) / ADC_REF_0DB);
double dbR = (totalR == 0) ? DB_MIN : 20 * log10((double)(totalR / SAMPLES_AVG) / ADC_REF_0DB);
unsigned long currentTime = millis();
if (dbL >= peakL) {
peakL = dbL;
lastPeakTimeL = currentTime;
} else if (currentTime - lastPeakTimeL > PEAK_HOLD_MS) {
peakL -= PEAK_DECAY_RATE;
if (peakL < dbL) peakL = dbL;
if (peakL < DB_MIN) peakL = DB_MIN;
}
if (dbR >= peakR) {
peakR = dbR;
lastPeakTimeR = currentTime;
} else if (currentTime - lastPeakTimeR > PEAK_HOLD_MS) {
peakR -= PEAK_DECAY_RATE;
if (peakR < dbR) peakR = dbR;
if (peakR < DB_MIN) peakR = DB_MIN;
}
display.clearDisplay();
int label_x = 9;
int bar_x_start = 26;
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
drawVUMeterBar(bar_x_start, 4, dbL, peakL);
display.setCursor(1, 12);
int roundedDbL = round(dbL);
if (roundedDbL > 0) display.print("+");
display.print(roundedDbL);
display.setCursor(label_x, 3);
display.print("L");
display.setCursor(6, 20);
display.print("dB");
drawVUMeterBar(bar_x_start, 39, dbR, peakR);
display.setCursor(1, 47);
int roundedDbR = round(dbR);
if (roundedDbR > 0) display.print("+");
display.print(roundedDbR);
display.setCursor(label_x, 37);
display.print("R");
display.setCursor(6, 55);
display.print("dB");
display.setCursor(52, 28);
display.print("PATRO DJ");
display.display();
}