// =========================================================================
// === LIBRERÍAS ===
// =========================================================================
#include <Arduino.h>
#include <Wire.h>
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <ESP32Encoder.h>
#include <math.h>
#include <BluetoothSerial.h>
#include <ArduinoJson.h>
#include "Update.h"
#include "esp_ota_ops.h"
#include <arduinoFFT.h>
#include <Adafruit_NeoPixel.h>
// =========================================================================
// === *** ARRAY DEL LOGO PERSONALIZADO *** ===
// =========================================================================
// Este array de bytes representa una imagen de 128x64 píxeles.
// Fue generado con una herramienta online (image2cpp) a partir de una imagen BMP.
const unsigned char patro_dj_logo_bmp[] PROGMEM = {
/*
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
*/
// 'logo', 128x64px
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfd, 0xff, 0xff, 0xbf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xcf, 0xfe, 0x3f, 0xf7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0xf9, 0x3e, 0x3d, 0x6e, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xf7, 0x9c, 0x1e, 0x7e, 0xcd, 0xcf, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xdd, 0xdd, 0x5e, 0x3e, 0xdd, 0xf3, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0x72, 0xec, 0x7f, 0xfc, 0xb9, 0x9d, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xfe, 0xf9, 0xe7, 0x1f, 0xf1, 0xe7, 0xbf, 0x7f, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xfb, 0xfc, 0xf3, 0xff, 0xff, 0xde, 0xbf, 0xbf, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xf7, 0x3e, 0xff, 0xff, 0xff, 0xff, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xef, 0xc7, 0x7f, 0xfe, 0xff, 0xff, 0xf8, 0x77, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xda, 0xe5, 0xfe, 0x3c, 0x78, 0xff, 0xf8, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xbf, 0x57, 0xff, 0x7d, 0x7d, 0xef, 0xfc, 0xfb, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0x7f, 0x0f, 0xe7, 0xff, 0xff, 0xef, 0xfe, 0xfd, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0x00, 0x3f, 0x0e, 0x00, 0x40, 0x18, 0x03, 0xe0, 0x00, 0x00, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0x00, 0x1e, 0x0e, 0x00, 0x6f, 0xf8, 0x01, 0xe0, 0x03, 0xf0, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0x0e, 0x0e, 0x07, 0xc3, 0xff, 0xf8, 0x61, 0xe3, 0xc3, 0xf0, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0x0f, 0x0c, 0x07, 0xc7, 0xc0, 0x78, 0x71, 0xe3, 0xc3, 0xf0, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0x0f, 0x0c, 0x47, 0xc7, 0xc0, 0x78, 0x71, 0xe3, 0xc3, 0xf0, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0x0f, 0x08, 0x63, 0xc7, 0xc0, 0xf8, 0x71, 0xe3, 0xc3, 0xf0, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0x00, 0x18, 0xe3, 0xc7, 0xcf, 0xf8, 0x71, 0xe3, 0xc3, 0xf0, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0x00, 0x38, 0x01, 0xc7, 0xc9, 0xf8, 0x60, 0xe3, 0xc3, 0xf0, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0x0f, 0xf0, 0x01, 0xc7, 0xd9, 0xf8, 0x71, 0xe3, 0xc2, 0x30, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0x0f, 0xf1, 0xf0, 0xc7, 0xd9, 0x18, 0xef, 0xe0, 0x02, 0x00, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0x0f, 0xe1, 0xf0, 0xc7, 0xd8, 0x08, 0x7d, 0xe0, 0x07, 0x00, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfb, 0xfe, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0x7f, 0x6f, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xfd, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xb8, 0x77, 0xe7, 0xff, 0xff, 0xcf, 0xec, 0xfd, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xdf, 0x9b, 0xfe, 0x3c, 0x38, 0xff, 0x9e, 0x1b, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xee, 0xee, 0xff, 0x7c, 0x7d, 0xff, 0x33, 0xf7, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xf5, 0x9f, 0x3f, 0xff, 0xff, 0xf8, 0xf8, 0xef, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xfb, 0x7c, 0xe7, 0xff, 0xff, 0xc3, 0x7d, 0xdf, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xfd, 0xff, 0x7c, 0x7f, 0xfc, 0x1e, 0xbf, 0x3f, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0x76, 0x67, 0xc0, 0x01, 0xff, 0xbe, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0x99, 0xc4, 0xff, 0xff, 0xdf, 0xdb, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xe7, 0xac, 0x9e, 0xbd, 0xff, 0xe7, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xda, 0x5e, 0x3d, 0xef, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0xbe, 0x3e, 0xf8, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe3, 0xff, 0xff, 0x8f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff
};
// =========================================================================
// === CONFIGURACIONES GLOBALES ===
// =========================================================================
// --- ESTADOS DEL PROGRAMA ---
enum MainProgramMode { MODE_PREAMP, MODE_VISUALIZER };
MainProgramMode mainProgramMode = MODE_PREAMP; // El Vúmetro/Preamp es el modo principal por defecto
int visualizerSubMode = 0; // Para los efectos secundarios (0-3)
const int TOTAL_VISUALIZER_MODES = 4;
unsigned long lastVisualizerActivity = 0;
#define VISUALIZER_TIMEOUT_MS 5000 // Vuelve al modo Preamp después de 5 segundos de inactividad
enum OledParamState { OLED_SHOWING_MAIN, OLED_SHOWING_PARAM };
OledParamState oledParamState = OLED_SHOWING_MAIN;
// --- CONFIGURACIÓN DEL BOTÓN BOOT (GPIO 0) ---
#define BOOT_BUTTON_PIN 0 // BOOT BUTTOM
#define LONG_PRESS_HOLD_MS 3000
unsigned long bootButtonPressTime = 0;
bool longPressTriggered = false;
#define BUILTIN_LED 2
// --- BLUETOOTH ---
BluetoothSerial SerialBT;
const char* BT_DEVICE_NAME = "ESP32_Master";
// --- PANTALLA OLED (I2C) ---
// D21 = SDA - D22 = SCL
#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);
unsigned long lastParamInteractionTime = 0;
#define OLED_PARAM_TIMEOUT_MS 3000
// --- TIRA LED ---
#define LED_PIN 5
#define NUM_LEDS 30
#define LED_BRIGHTNESS 180
// --- ENCODERS ---
// Encoder de Volumen (no multiplexado)
#define VOL_CLK_PIN 12
#define VOL_DT_PIN 13
// Encoder de Ganancia (no multiplexado)
#define GAIN_CLK_PIN 34
#define GAIN_DT_PIN 35
// --- NUEVO: CONFIGURACIÓN DEL MULTIPLEXOR TMUX4052 ---
// Pines de selección del MUX
#define MUX_S0_PIN 25
#define MUX_S1_PIN 26
// Pines de entrada comunes desde el MUX al ESP32
#define MUX_COMMON_CLK_PIN 32 // Conectar al pin Y0 del TMUX4052
#define MUX_COMMON_DT_PIN 33 // Conectar al pin Y1 del TMUX4052
/*
* CONEXIONES DE LOS ENCODERS AL TMUX4052:
* - Encoder HIGH: CLK -> A0, DT -> B0
* - Encoder MID_HIGH: CLK -> A1, DT -> B1
* - Encoder MID_LOW: CLK -> A2, DT -> B2
* - Encoder LOW: CLK -> A3, DT -> B3
*/
// --- REGISTRO DE DESPLAZAMIENTO 74HC595 ---
#define SHIFT_REG_DATA_PIN 19
#define SHIFT_REG_CLOCK_PIN 15
#define SHIFT_REG_LATCH_PIN 4
const byte POT_VOL_BIT = (1 << 0);
const byte POT_GAIN_BIT = (1 << 1);
const byte POT_HIGH_BIT = (1 << 2);
const byte POT_MID_HIGH_BIT = (1 << 3);
const byte POT_MID_LOW_BIT = (1 << 4);
const byte POT_LOW_BIT = (1 << 5);
const byte POT_DBASS_BIT = (1 << 6); // Bit para el potenciómetro DBASS
const byte DESELECT_ALL_POTS = 0xFF;
// --- ENTRADAS DE AUDIO (COMPARTIDAS) ---
#define AUDIO_IN_L 36 //VP
#define AUDIO_IN_R 39 //VN
// --- PARÁMETROS VÚMETRO ---
#define VUMETER_SAMPLES_AVG 64
#define VUMETER_ADC_REF_0DB 1106.0
#define VUMETER_DB_MIN -30.0
#define VUMETER_DB_MAX 11.4
#define VUMETER_PEAK_HOLD_MS 500
#define VUMETER_PEAK_DECAY_RATE 0.75
// --- PARÁMETROS FFT (PARA VISUALIZADORES) ---
#define FFT_SAMPLES 256
#define FFT_SAMPLING_FREQUENCY 40000.0
#define FFT_AMPLITUDE 900.0
const int VIS_PEAK_FALL_DELAY = 50;
// --- PARÁMETROS DE FUNCIONAMIENTO (DE PREAMP) ---
const int POT_MAX_STEPS = 255;
#define ENCODER_STEPS_PER_DB 2
const int VOL_MIN = -70, VOL_MAX = 10, GAIN_MIN = -20, GAIN_MAX = 20, EQ_MIN = -15, EQ_MAX = 15;
const int DBASS_MIN = -50, DBASS_MAX = 0; // Rango para el nuevo potenciómetro DBASS
const int DBASS_WIPER_MAX = 192; // --- NUEVO: Valor máximo del wiper para DBASS
// --- IDENTIFICADORES JSON ---
const char* PARAM_M_VOL = "m_vol"; const char* PARAM_S_VOL = "s_vol";
const char* PARAM_M_GAIN = "m_gain"; const char* PARAM_S_GAIN = "s_gain";
const char* PARAM_M_HIGH = "m_high"; const char* PARAM_S_HIGH = "s_high";
const char* PARAM_M_MID_HIGH = "m_mid_high"; const char* PARAM_S_MID_HIGH = "s_mid_high";
const char* PARAM_M_MID_LOW = "m_mid_low"; const char* PARAM_S_MID_LOW = "s_mid_low";
const char* PARAM_M_LOW = "m_low"; const char* PARAM_S_LOW = "s_low";
const char* PARAM_M_DBASS = "m_dbass"; const char* PARAM_S_DBASS = "s_dbass"; // Identificadores para DBASS
// =========================================================================
// === OBJETOS Y VARIABLES GLOBALES ===
// =========================================================================
ESP32Encoder encoderVol, encoderGain;
ESP32Encoder muxEncoder; // Un único encoder para los 4 EQs multiplexados
int currentVolDB = 0, currentGainDB = 0, currentHighDB = 0, currentMidHighDB = 0, currentMidLowDB = 0, currentLowDB = 0;
int currentDbassDB = 0; // Variable de estado para DBASS
// --- NUEVO: Variables para almacenar el conteo crudo de cada encoder de EQ ---
long encoderCountHigh = 0, encoderCountMidHigh = 0, encoderCountMidLow = 0, encoderCountLow = 0;
double peakL = VUMETER_DB_MIN, peakR = VUMETER_DB_MIN;
unsigned long lastPeakTimeL = 0, lastPeakTimeR = 0;
Adafruit_NeoPixel strip(NUM_LEDS, LED_PIN, NEO_GRB + NEO_KHZ800);
double vRealL[FFT_SAMPLES], vImagL[FFT_SAMPLES], vRealR[FFT_SAMPLES], vImagR[FFT_SAMPLES];
ArduinoFFT<double> fftL = ArduinoFFT<double>(vRealL, vImagL, FFT_SAMPLES, FFT_SAMPLING_FREQUENCY);
ArduinoFFT<double> fftR = ArduinoFFT<double>(vRealR, vImagR, FFT_SAMPLES, FFT_SAMPLING_FREQUENCY);
int vis_peakL = 0, vis_peakR = 0;
unsigned long last_vis_peak_time = 0;
// =========================================================================
// === PROTOTIPOS DE FUNCIONES ===
// =========================================================================
void handleBootButton();
void handlePreampControls();
void runPreampDisplay();
void runVisualizer();
void switchToOtaUpdater();
void updateParamDisplay(const char *paramName, int currentValue, int minValue, int maxValue);
void updateVumeterDisplay();
void drawVUMeterBar(int x_start, int y_start, double dbValue, double peakDbValue);
void updateOledVisualizerModeDisplay();
void selectPotentiometer(byte potSelectByte);
void writeToPot(byte potBit, byte address, byte value);
void setPotValue(byte potBit, int dbValue, int dbMin, int dbMax);
void sendEncoderUpdate(const char* param, int value);
void handleIncomingData();
void performFFT();
void visualizerSpectrum();
void visualizerVUMeter();
void visualizerBassPulse();
void visualizerPeakMeter();
void printInitialValues();
void updateParameter(const char* param, int value, bool isRemote);
void selectMuxChannel(byte channel); // --- NUEVO: Prototipo para la función de selección del MUX
// =========================================================================
// === SETUP ===
// =========================================================================
void setup() {
Serial.begin(115200);
Serial.println("Monitor Serial Iniciado. Mostrando valores de potenciometros...");
Wire.begin(); SPI.begin();
pinMode(BOOT_BUTTON_PIN, INPUT);
pinMode(SHIFT_REG_DATA_PIN, OUTPUT); pinMode(SHIFT_REG_CLOCK_PIN, OUTPUT); pinMode(SHIFT_REG_LATCH_PIN, OUTPUT);
selectPotentiometer(DESELECT_ALL_POTS);
// --- NUEVO: Configurar los pines de selección del MUX como salida ---
pinMode(MUX_S0_PIN, OUTPUT);
pinMode(MUX_S1_PIN, OUTPUT);
currentVolDB = VOL_MIN;
currentDbassDB = DBASS_MIN;
encoderVol.attachHalfQuad(VOL_DT_PIN, VOL_CLK_PIN);
encoderVol.setCount(VOL_MIN * ENCODER_STEPS_PER_DB);
encoderGain.attachHalfQuad(GAIN_DT_PIN, GAIN_CLK_PIN);
encoderGain.setCount(0 * ENCODER_STEPS_PER_DB);
currentGainDB = 0;
// --- NUEVO: Inicializar el encoder multiplexado en los pines comunes ---
muxEncoder.attachHalfQuad(MUX_COMMON_DT_PIN, MUX_COMMON_CLK_PIN);
// Inicializar valores de EQ a 0 dB y sus contadores crudos a 0
currentHighDB = 0; encoderCountHigh = 0;
currentMidHighDB = 0; encoderCountMidHigh = 0;
currentMidLowDB = 0; encoderCountMidLow = 0;
currentLowDB = 0; encoderCountLow = 0;
muxEncoder.setCount(0); // Inicia el contador del mux a 0
// --- MODIFICADO: Ajuste inicial de DBASS con su rango de wiper personalizado ---
long initialDbassWiper = map(currentDbassDB, DBASS_MIN, DBASS_MAX, DBASS_WIPER_MAX, 0);
writeToPot(POT_DBASS_BIT, 0x00, (byte)initialDbassWiper);
writeToPot(POT_DBASS_BIT, 0x10, (byte)initialDbassWiper);
setPotValue(POT_VOL_BIT, currentVolDB, VOL_MIN, VOL_MAX);
setPotValue(POT_GAIN_BIT, currentGainDB, GAIN_MIN, GAIN_MAX);
setPotValue(POT_HIGH_BIT, currentHighDB, EQ_MIN, EQ_MAX);
setPotValue(POT_MID_HIGH_BIT, currentMidHighDB, EQ_MIN, EQ_MAX);
setPotValue(POT_MID_LOW_BIT, currentMidLowDB, EQ_MIN, EQ_MAX);
setPotValue(POT_LOW_BIT, currentLowDB, EQ_MIN, EQ_MAX);
/*display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS);
display.clearDisplay();
display.setTextSize(2);
display.setTextColor(SSD1306_WHITE);
display.setCursor(15, 15);
display.println("PATRO DJ");
display.setCursor(25, 35);
display.println("MASTER");
display.display();
delay(1000);
*/
display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS);
display.clearDisplay();
display.drawBitmap(0, 0, patro_dj_logo_bmp, 128, 64, SSD1306_WHITE);
display.display();
delay(2000);
pinMode(BUILTIN_LED, OUTPUT); digitalWrite(BUILTIN_LED, LOW); //Pin 2 esp32
SerialBT.begin(BT_DEVICE_NAME);
strip.begin(); strip.setBrightness(LED_BRIGHTNESS); strip.show();
printInitialValues();
}
// =========================================================================
// === LOOP ===
// =========================================================================
void loop() {
handleBootButton();
if (longPressTriggered) { delay(100); return; }
handlePreampControls();
switch(mainProgramMode) {
case MODE_PREAMP:
runPreampDisplay();
break;
case MODE_VISUALIZER:
runVisualizer();
break;
}
}
// =========================================================================
// === FUNCIÓN PARA IMPRIMIR VALORES INICIALES ===
// =========================================================================
void printInitialValues() {
Serial.println("--- Valores Iniciales ---");
long potStep;
// --- MODIFICADO: Usa el rango de wiper personalizado para DBASS
potStep = constrain(map(currentDbassDB, DBASS_MIN, DBASS_MAX, DBASS_WIPER_MAX, 0), 0, DBASS_WIPER_MAX);
Serial.print("DBASS: "); Serial.print(currentDbassDB); Serial.print(" dB");
Serial.print(" (Wiper: "); Serial.print(potStep); Serial.println(")");
potStep = constrain(map(currentVolDB, VOL_MIN, VOL_MAX, POT_MAX_STEPS, 0), 0, POT_MAX_STEPS);
Serial.print("VOLUMEN: "); Serial.print(currentVolDB); Serial.print(" dB");
Serial.print(" (Wiper: "); Serial.print(potStep); Serial.println(")");
potStep = constrain(map(currentGainDB, GAIN_MIN, GAIN_MAX, POT_MAX_STEPS, 0), 0, POT_MAX_STEPS);
Serial.print("GAIN: "); Serial.print(currentGainDB); Serial.print(" dB");
Serial.print(" (Wiper: "); Serial.print(potStep); Serial.println(")");
potStep = constrain(map(currentHighDB, EQ_MIN, EQ_MAX, POT_MAX_STEPS, 0), 0, POT_MAX_STEPS);
Serial.print("HIGH: "); Serial.print(currentHighDB); Serial.print(" dB");
Serial.print(" (Wiper: "); Serial.print(potStep); Serial.println(")");
potStep = constrain(map(currentMidHighDB, EQ_MIN, EQ_MAX, POT_MAX_STEPS, 0), 0, POT_MAX_STEPS);
Serial.print("MID HIGH: "); Serial.print(currentMidHighDB); Serial.print(" dB");
Serial.print(" (Wiper: "); Serial.print(potStep); Serial.println(")");
potStep = constrain(map(currentMidLowDB, EQ_MIN, EQ_MAX, POT_MAX_STEPS, 0), 0, POT_MAX_STEPS);
Serial.print("MID LOW: "); Serial.print(currentMidLowDB); Serial.print(" dB");
Serial.print(" (Wiper: "); Serial.print(potStep); Serial.println(")");
potStep = constrain(map(currentLowDB, EQ_MIN, EQ_MAX, POT_MAX_STEPS, 0), 0, POT_MAX_STEPS);
Serial.print("LOW: "); Serial.print(currentLowDB); Serial.print(" dB");
Serial.print(" (Wiper: "); Serial.print(potStep); Serial.println(")");
Serial.println("-------------------------");
Serial.println("Gire un encoder para ver actualizaciones...");
}
// =========================================================================
// === FUNCIONES DE CONTROL PRINCIPAL ===
// =========================================================================
void handleBootButton() {
if (digitalRead(BOOT_BUTTON_PIN) == LOW) {
if (bootButtonPressTime == 0) { bootButtonPressTime = millis(); }
else if (millis() - bootButtonPressTime > LONG_PRESS_HOLD_MS && !longPressTriggered) {
longPressTriggered = true; switchToOtaUpdater();
}
} else {
if (bootButtonPressTime > 0 && !longPressTriggered) {
if (mainProgramMode == MODE_PREAMP) {
mainProgramMode = MODE_VISUALIZER;
visualizerSubMode = 0;
vis_peakL = 0; vis_peakR = 0;
updateOledVisualizerModeDisplay();
lastVisualizerActivity = millis();
} else {
visualizerSubMode++;
if (visualizerSubMode >= TOTAL_VISUALIZER_MODES) {
mainProgramMode = MODE_PREAMP;
strip.clear(); strip.show();
} else {
vis_peakL = 0; vis_peakR = 0;
updateOledVisualizerModeDisplay();
lastVisualizerActivity = millis();
}
}
delay(200);
}
bootButtonPressTime = 0; longPressTriggered = false;
}
}
void handlePreampControls() {
if (SerialBT.connected()) {
digitalWrite(BUILTIN_LED, HIGH); handleIncomingData();
} else {
digitalWrite(BUILTIN_LED, LOW);
}
// --- Leer encoders no multiplexados (Volumen y Ganancia) ---
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) {
updateParameter(PARAM_M_VOL, newVol, false); // false = no es remoto
}
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) {
updateParameter(PARAM_M_GAIN, newGain, false);
}
// --- NUEVO: Leer encoders de EQ multiplexados ---
long* encoderCounts[4] = {&encoderCountHigh, &encoderCountMidHigh, &encoderCountMidLow, &encoderCountLow};
int* currentDBs[4] = {¤tHighDB, ¤tMidHighDB, ¤tMidLowDB, ¤tLowDB};
const char* paramNames[4] = {PARAM_M_HIGH, PARAM_M_MID_HIGH, PARAM_M_MID_LOW, PARAM_M_LOW};
for (int i = 0; i < 4; i++) {
// 1. Seleccionar el canal del encoder en el MUX
selectMuxChannel(i);
// 2. Establecer el contador del objeto encoder al valor guardado para este canal
muxEncoder.setCount(*encoderCounts[i]);
// 3. Dar un respiro muy breve para que la lectura se estabilice (puede no ser necesario)
yield();
// 4. Leer el nuevo valor crudo del encoder
long newRawCount = muxEncoder.getCount();
// 5. Si ha cambiado, procesar la actualización
if (newRawCount != *encoderCounts[i]) {
// 6. Guardar el nuevo valor crudo
*encoderCounts[i] = newRawCount;
// 7. Convertir el valor crudo a dB
long newDbValue = newRawCount / ENCODER_STEPS_PER_DB;
// 8. Limitar el valor dentro del rango de EQ
if (newDbValue > EQ_MAX) {
newDbValue = EQ_MAX;
*encoderCounts[i] = newDbValue * ENCODER_STEPS_PER_DB;
muxEncoder.setCount(*encoderCounts[i]);
}
if (newDbValue < EQ_MIN) {
newDbValue = EQ_MIN;
*encoderCounts[i] = newDbValue * ENCODER_STEPS_PER_DB;
muxEncoder.setCount(*encoderCounts[i]);
}
// 9. Actualizar el estado general (variable dB, pot, pantalla, BT)
if (newDbValue != *currentDBs[i]) {
updateParameter(paramNames[i], newDbValue, false);
}
}
}
}
void runPreampDisplay() {
if (oledParamState == OLED_SHOWING_PARAM && millis() - lastParamInteractionTime > OLED_PARAM_TIMEOUT_MS) {
oledParamState = OLED_SHOWING_MAIN;
}
if (oledParamState == OLED_SHOWING_MAIN) {
updateVumeterDisplay();
}
delay(20);
}
void runVisualizer() {
if (millis() - lastVisualizerActivity > VISUALIZER_TIMEOUT_MS) {
mainProgramMode = MODE_PREAMP;
strip.clear(); strip.show();
return;
}
switch (visualizerSubMode) {
case 0: visualizerSpectrum(); break;
case 1: visualizerVUMeter(); break;
case 2: visualizerBassPulse(); break;
case 3: visualizerPeakMeter(); break;
}
}
void switchToOtaUpdater() {
display.clearDisplay(); display.setTextSize(1); display.setCursor(14, 25); display.println("REINICIANDO....."); display.display();
delay(1000);
const esp_partition_t* update_partition = esp_ota_get_next_update_partition(NULL);
if (update_partition == NULL) {
display.clearDisplay(); display.setTextSize(1); display.setCursor(0, 10);
display.println("ERROR: No se encontro"); display.println("la particion OTA.");
display.display(); delay(3000); ESP.restart(); return;
}
esp_err_t err = esp_ota_set_boot_partition(update_partition);
if (err != ESP_OK) {
display.clearDisplay(); display.setTextSize(1); display.setCursor(0, 10);
display.println("ERROR: Fallo al"); display.println("cambiar particion.");
display.display(); delay(3000);
}
ESP.restart();
}
// =========================================================================
// === FUNCIONES DE PANTALLA OLED ===
// =========================================================================
void updateParamDisplay(const char *paramName, int currentValue, int minValue, int maxValue) {
oledParamState = OLED_SHOWING_PARAM;
lastParamInteractionTime = millis();
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 updateVumeterDisplay() {
unsigned long totalL = 0; unsigned long totalR = 0;
for (int i = 0; i < VUMETER_SAMPLES_AVG; i++) {
totalL += analogRead(AUDIO_IN_L); totalR += analogRead(AUDIO_IN_R);
}
double dbL = (totalL == 0) ? VUMETER_DB_MIN : 20 * log10((double)(totalL / VUMETER_SAMPLES_AVG) / VUMETER_ADC_REF_0DB);
double dbR = (totalR == 0) ? VUMETER_DB_MIN : 20 * log10((double)(totalR / VUMETER_SAMPLES_AVG) / VUMETER_ADC_REF_0DB);
unsigned long currentTime = millis();
if (dbL >= peakL) { peakL = dbL; lastPeakTimeL = currentTime;
} else if (currentTime - lastPeakTimeL > VUMETER_PEAK_HOLD_MS) {
peakL -= VUMETER_PEAK_DECAY_RATE; if (peakL < dbL) peakL = dbL; if (peakL < VUMETER_DB_MIN) peakL = VUMETER_DB_MIN;
}
if (dbR >= peakR) { peakR = dbR; lastPeakTimeR = currentTime;
} else if (currentTime - lastPeakTimeR > VUMETER_PEAK_HOLD_MS) {
peakR -= VUMETER_PEAK_DECAY_RATE; if (peakR < dbR) peakR = dbR; if (peakR < VUMETER_DB_MIN) peakR = VUMETER_DB_MIN;
}
display.clearDisplay();
int label_x = 2; int bar_x_start = 12;
display.setTextSize(1); display.setTextColor(SSD1306_WHITE);
drawVUMeterBar(bar_x_start, 4, dbL, peakL);
display.setCursor(label_x, 12); display.print("L");
drawVUMeterBar(bar_x_start, 39, dbR, peakR);
display.setCursor(label_x, 47); display.print("R");
const int MARKER_Y_START = 24; const int MARKER_HEIGHT = 14; const double BAR_LENGTH_D = 110.0;
int x_marker_neg30 = bar_x_start + (int)(((-30.0 - VUMETER_DB_MIN) / (VUMETER_DB_MAX - VUMETER_DB_MIN)) * BAR_LENGTH_D);
int x_marker_0 = bar_x_start + (int)(((0.0 - VUMETER_DB_MIN) / (VUMETER_DB_MAX - VUMETER_DB_MIN)) * BAR_LENGTH_D);
int x_marker_10 = bar_x_start + (int)(((10.0 - VUMETER_DB_MIN) / (VUMETER_DB_MAX - VUMETER_DB_MIN)) * BAR_LENGTH_D);
display.drawFastVLine(x_marker_neg30, MARKER_Y_START, MARKER_HEIGHT, SSD1306_WHITE);
display.drawFastVLine(x_marker_0, MARKER_Y_START, MARKER_HEIGHT, SSD1306_WHITE);
display.drawFastVLine(x_marker_10, MARKER_Y_START, MARKER_HEIGHT, SSD1306_WHITE);
display.setCursor(15, 28);
display.print("-30dB 0dB 10dB");
display.display();
}
void drawVUMeterBar(int x_start, int y_start, double dbValue, double peakDbValue) {
#define BAR_LENGTH 110
#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, VUMETER_DB_MIN, VUMETER_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, VUMETER_DB_MIN, VUMETER_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 updateOledVisualizerModeDisplay() {
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
const char* modeName;
switch (visualizerSubMode) {
case 0: modeName = "ESPECTRO"; break;
case 1: modeName = "VUMETRO"; break;
case 2: modeName = "BASS PULSE"; break;
case 3: modeName = "PEAK METER"; break;
default: modeName = "MODO ???"; break;
}
display.setTextSize(2);
display.setCursor(2, 28);
int16_t x1, y1; uint16_t w, h;
display.getTextBounds(modeName, 0, 0, &x1, &y1, &w, &h);
display.setCursor((SCREEN_WIDTH - w) / 2, display.getCursorY());
display.println(modeName);
display.display();
}
// =========================================================================
// === FUNCIONES DE POTENCIÓMETROS Y COMUNICACIÓN BT ===
// =========================================================================
void sendEncoderUpdate(const char* param, int value) {
JsonDocument doc; doc["param"] = param; doc["value"] = value;
String output; serializeJson(doc, output); SerialBT.println(output);
}
void handleIncomingData() {
if (SerialBT.available()) {
String incomingData = SerialBT.readStringUntil('\n');
JsonDocument doc; DeserializationError error = deserializeJson(doc, incomingData);
if (error) return;
const char* param = doc["param"];
int value = doc["value"];
updateParameter(param, value, true); // true = es remoto
}
}
void updateParameter(const char* param, int value, bool isRemote) {
long potStep;
const char* displayName = "";
byte potBit = 0;
int minVal=0, maxVal=0;
if (strcmp(param, PARAM_M_VOL) == 0 || strcmp(param, PARAM_S_VOL) == 0) {
currentVolDB = value;
displayName = "VOLUMEN";
potBit = POT_VOL_BIT;
minVal = VOL_MIN; maxVal = VOL_MAX;
potStep = constrain(map(value, minVal, maxVal, POT_MAX_STEPS, 0), 0, POT_MAX_STEPS);
encoderVol.setCount(value * ENCODER_STEPS_PER_DB);
int oldDbassDB = currentDbassDB;
if (currentVolDB <= -10) {
currentDbassDB = map(currentVolDB, VOL_MIN, -10, DBASS_MIN, DBASS_MAX);
} else {
currentDbassDB = DBASS_MAX;
}
currentDbassDB = constrain(currentDbassDB, DBASS_MIN, DBASS_MAX);
if (currentDbassDB != oldDbassDB) {
long dbassPotStep = constrain(map(currentDbassDB, DBASS_MIN, DBASS_MAX, DBASS_WIPER_MAX, 0), 0, DBASS_WIPER_MAX);
Serial.print("DBASS: "); Serial.print(currentDbassDB); Serial.print(" dB");
Serial.print(" (Wiper: "); Serial.print(dbassPotStep); Serial.print(")");
if (isRemote) { Serial.println(" - [Slave]"); } else { Serial.println(" - [Master]"); }
writeToPot(POT_DBASS_BIT, 0x00, (byte)dbassPotStep);
writeToPot(POT_DBASS_BIT, 0x10, (byte)dbassPotStep);
if (!isRemote && SerialBT.connected()) {
sendEncoderUpdate(PARAM_M_DBASS, currentDbassDB);
}
}
} else if (strcmp(param, PARAM_M_DBASS) == 0 || strcmp(param, PARAM_S_DBASS) == 0) {
currentDbassDB = value;
displayName = "DBASS";
potBit = POT_DBASS_BIT;
minVal = DBASS_MIN; maxVal = DBASS_MAX;
potStep = constrain(map(value, minVal, maxVal, DBASS_WIPER_MAX, 0), 0, DBASS_WIPER_MAX);
} else if (strcmp(param, PARAM_M_GAIN) == 0 || strcmp(param, PARAM_S_GAIN) == 0) {
currentGainDB = value;
displayName = "GAIN";
potBit = POT_GAIN_BIT;
minVal = GAIN_MIN; maxVal = GAIN_MAX;
potStep = constrain(map(value, minVal, maxVal, POT_MAX_STEPS, 0), 0, POT_MAX_STEPS);
encoderGain.setCount(value * ENCODER_STEPS_PER_DB);
} else if (strcmp(param, PARAM_M_HIGH) == 0 || strcmp(param, PARAM_S_HIGH) == 0) {
currentHighDB = value;
displayName = "HIGH";
potBit = POT_HIGH_BIT;
minVal = EQ_MIN; maxVal = EQ_MAX;
potStep = constrain(map(value, minVal, maxVal, POT_MAX_STEPS, 0), 0, POT_MAX_STEPS);
// --- NUEVO: Actualiza la variable de conteo crudo en lugar de un objeto encoder ---
encoderCountHigh = value * ENCODER_STEPS_PER_DB;
} else if (strcmp(param, PARAM_M_MID_HIGH) == 0 || strcmp(param, PARAM_S_MID_HIGH) == 0) {
currentMidHighDB = value;
displayName = "MID HIGH";
potBit = POT_MID_HIGH_BIT;
minVal = EQ_MIN; maxVal = EQ_MAX;
potStep = constrain(map(value, minVal, maxVal, POT_MAX_STEPS, 0), 0, POT_MAX_STEPS);
// --- NUEVO: Actualiza la variable de conteo crudo ---
encoderCountMidHigh = value * ENCODER_STEPS_PER_DB;
} else if (strcmp(param, PARAM_M_MID_LOW) == 0 || strcmp(param, PARAM_S_MID_LOW) == 0) {
currentMidLowDB = value;
displayName = "MID LOW";
potBit = POT_MID_LOW_BIT;
minVal = EQ_MIN; maxVal = EQ_MAX;
potStep = constrain(map(value, minVal, maxVal, POT_MAX_STEPS, 0), 0, POT_MAX_STEPS);
// --- NUEVO: Actualiza la variable de conteo crudo ---
encoderCountMidLow = value * ENCODER_STEPS_PER_DB;
} else if (strcmp(param, PARAM_M_LOW) == 0 || strcmp(param, PARAM_S_LOW) == 0) {
currentLowDB = value;
displayName = "LOW";
potBit = POT_LOW_BIT;
minVal = EQ_MIN; maxVal = EQ_MAX;
potStep = constrain(map(value, minVal, maxVal, POT_MAX_STEPS, 0), 0, POT_MAX_STEPS);
// --- NUEVO: Actualiza la variable de conteo crudo ---
encoderCountLow = value * ENCODER_STEPS_PER_DB;
} else {
return;
}
Serial.print(displayName); Serial.print(": "); Serial.print(value); Serial.print(" dB");
Serial.print(" (Wiper: "); Serial.print(potStep); Serial.print(")");
if (isRemote) { Serial.println(" - [Slave]"); } else { Serial.println(" - [Master]"); }
if (strcmp(param, PARAM_M_DBASS) != 0 && strcmp(param, PARAM_S_DBASS) != 0) {
updateParamDisplay(displayName, value, minVal, maxVal);
}
writeToPot(potBit, 0x00, (byte)potStep);
writeToPot(potBit, 0x10, (byte)potStep);
if (!isRemote && SerialBT.connected()) {
sendEncoderUpdate(param, value);
}
}
void selectPotentiometer(byte potSelectByte) {
digitalWrite(SHIFT_REG_LATCH_PIN, LOW);
shiftOut(SHIFT_REG_DATA_PIN, SHIFT_REG_CLOCK_PIN, MSBFIRST, ~potSelectByte);
digitalWrite(SHIFT_REG_LATCH_PIN, HIGH);
}
void writeToPot(byte potBit, byte address, byte value) {
selectPotentiometer(potBit);
SPI.beginTransaction(SPISettings(1000000, MSBFIRST, SPI_MODE0));
SPI.transfer(address); SPI.transfer(value); SPI.endTransaction();
selectPotentiometer(DESELECT_ALL_POTS);
}
// Para los pot que usan el rango completo 0-255.
void setPotValue(byte potBit, int dbValue, int dbMin, int dbMax) {
long potStep = map(dbValue, dbMin, dbMax, POT_MAX_STEPS, 0);
potStep = constrain(potStep, 0, POT_MAX_STEPS);
writeToPot(potBit, 0x00, (byte)potStep);
writeToPot(potBit, 0x10, (byte)potStep);
}
// --- NUEVA FUNCIÓN: Selecciona el canal en el multiplexor TMUX4052 ---
void selectMuxChannel(byte channel) {
// El TMUX4052 usa lógica binaria estándar para sus pines de selección S0 y S1
// channel 0: S1=0, S0=0
// channel 1: S1=0, S0=1
// channel 2: S1=1, S0=0
// channel 3: S1=1, S0=1
digitalWrite(MUX_S0_PIN, (channel & 1) ? HIGH : LOW);
digitalWrite(MUX_S1_PIN, (channel & 2) ? HIGH : LOW);
}
// =========================================================================
// === FUNCIONES DE VISUALIZACIÓN (LED Y FFT) ===
// =========================================================================
void performFFT() {
for (int i = 0; i < FFT_SAMPLES; i++) {
vRealL[i] = (double)analogRead(AUDIO_IN_L) - 2048.0; vImagL[i] = 0.0;
vRealR[i] = (double)analogRead(AUDIO_IN_R) - 2048.0; vImagR[i] = 0.0;
delayMicroseconds(20);
}
fftL.windowing(FFT_WIN_TYP_HAMMING, FFT_FORWARD); fftL.compute(FFT_FORWARD); fftL.complexToMagnitude();
fftR.windowing(FFT_WIN_TYP_HAMMING, FFT_FORWARD); fftR.compute(FFT_FORWARD); fftR.complexToMagnitude();
}
void visualizerSpectrum() {
performFFT(); strip.clear();
int leds_per_channel = NUM_LEDS / 2;
for (int i = 0; i < leds_per_channel; i++) {
int bin = map(i, 0, leds_per_channel - 1, 2, FFT_SAMPLES / 4);
int brightnessL = constrain((int)(vRealL[bin] / FFT_AMPLITUDE * 255.0), 0, 255);
uint16_t hueL = map(i, 0, leds_per_channel - 1, 22000, 0);
strip.setPixelColor(i, strip.ColorHSV(hueL, 255, brightnessL));
int brightnessR = constrain((int)(vRealR[bin] / FFT_AMPLITUDE * 255.0), 0, 255);
uint16_t hueR = map(i, 0, leds_per_channel - 1, 22000, 0);
strip.setPixelColor(i + leds_per_channel, strip.ColorHSV(hueR, 255, brightnessR));
}
strip.show();
}
void visualizerVUMeter() {
performFFT(); strip.clear();
int leds_per_channel = NUM_LEDS / 2;
double totalMagL = 0, totalMagR = 0;
for(int i = 2; i < FFT_SAMPLES / 2; i++) { totalMagL += vRealL[i]; totalMagR += vRealR[i]; }
int heightL = constrain(map((long)totalMagL, 0, FFT_AMPLITUDE * 40, 0, leds_per_channel), 0, leds_per_channel);
int heightR = constrain(map((long)totalMagR, 0, FFT_AMPLITUDE * 40, 0, leds_per_channel), 0, leds_per_channel);
for(int i = 0; i < leds_per_channel; i++) {
if (i < heightL) strip.setPixelColor(i, strip.ColorHSV(map(i, 0, leds_per_channel - 1, 22000, 0), 255, 255));
if (i < heightR) strip.setPixelColor(i + leds_per_channel, strip.ColorHSV(map(i, 0, leds_per_channel - 1, 22000, 0), 255, 255));
}
strip.show();
}
void visualizerBassPulse() {
performFFT(); strip.clear();
double bassL = 0, bassR = 0;
for (int i = 2; i < 7; i++) { bassL += vRealL[i]; bassR += vRealR[i]; }
int brightnessL = constrain(map((long)bassL, 0, FFT_AMPLITUDE * 5, 0, 255), 0, 255);
int brightnessR = constrain(map((long)bassR, 0, FFT_AMPLITUDE * 5, 0, 255), 0, 255);
uint32_t colorL = strip.Color(0, 0, brightnessL);
uint32_t colorR = strip.Color(brightnessR, 0, 0);
for(int i = 0; i < NUM_LEDS / 2; i++) {
strip.setPixelColor(i, colorL); strip.setPixelColor(i + (NUM_LEDS / 2), colorR);
}
strip.show();
}
void visualizerPeakMeter() {
performFFT();
strip.clear();
int leds_per_channel = NUM_LEDS / 2;
double totalMagL = 0, totalMagR = 0;
for(int i = 2; i < FFT_SAMPLES / 2; i++) { totalMagL += vRealL[i]; totalMagR += vRealR[i]; }
int heightL = constrain(map((long)totalMagL, 0, FFT_AMPLITUDE * 40, 0, leds_per_channel), 0, leds_per_channel);
int heightR = constrain(map((long)totalMagR, 0, FFT_AMPLITUDE * 40, 0, leds_per_channel), 0, leds_per_channel);
if (heightL > vis_peakL) {
vis_peakL = heightL;
}
if (heightR > vis_peakR) {
vis_peakR = heightR;
}
for(int i = 0; i < leds_per_channel; i++) {
if (i < heightL) strip.setPixelColor(i, strip.ColorHSV(map(i, 0, leds_per_channel - 1, 22000, 0), 255, 255));
if (i < heightR) strip.setPixelColor(i + leds_per_channel, strip.ColorHSV(map(i, 0, leds_per_channel - 1, 22000, 0), 255, 255));
}
if (vis_peakL > 0) {
strip.setPixelColor(vis_peakL - 1, strip.Color(255, 255, 255));
}
if (vis_peakR > 0) {
strip.setPixelColor((vis_peakR - 1) + leds_per_channel, strip.Color(255, 255, 255));
}
strip.show();
if (millis() - last_vis_peak_time > VIS_PEAK_FALL_DELAY) {
if (vis_peakL > 0) vis_peakL--;
if (vis_peakR > 0) vis_peakR--;
last_vis_peak_time = millis();
}
}