ROBOT FUTBOLISTA
// ===== TRANSMISOR ESP-NOW
// LED de enlace: parpadea hasta detectar al receptor, luego se APAGA.
// Recibe del receptor un latido ('P') para saber que está conectado.
//
// Al arrancar mide el centro de cada eje: NO toques los sticks durante
// el primer arranque. Los 4 ejes están en ADC1 (32-35), que convive bien
// con WiFi/ESP-NOW (ADC2 queda inutilizable, pero aquí no se usa).
#include <esp_now.h>
#include <WiFi.h>
// --- Pines joystick izquierdo ---
const int PIN_IZQ_X = 32; // ADC1_CH4
const int PIN_IZQ_Y = 33; // ADC1_CH5
const int PIN_IZQ_BTN = 25; // Digital con pull-up interno
// --- Pines joystick derecho ---
const int PIN_DER_X = 34; // ADC1_CH7 (solo entrada)
const int PIN_DER_Y = 35; // ADC1_CH6 (solo entrada)
const int PIN_DER_BTN = 21; // Digital con pull-up interno
// --- LED indicador de enlace ---
// AJUSTA este pin al del LED de tu transmisor.
// Por defecto uso GPIO2 (LED azul integrado en muchas placas ESP32 DevKit).
#define LED_TX 2
// --- Parámetros de ajuste ---
const int N_CALIB = 200; // Muestras para calcular el centro al arrancar
const int N_FILTRO = 8; // Lecturas promediadas por eje (suaviza ruido)
const int UMBRAL = 600; // Cuentas que hay que desviarse del centro para
// que un eje genere comando. Súbelo si se mueve
// solo; bájalo si cuesta activarlo.
const unsigned long INTERVALO_TX = 50; // ms entre envíos (también sirve de "latido"
// para el failsafe del robot)
// --- Detección de enlace con el receptor ---
const unsigned long TIMEOUT_RX = 1000; // ms sin latido del receptor -> desconectado
volatile unsigned long ultimoLatidoRx = 0;
volatile bool hayLatido = false;
unsigned long ultimoParpadeoTx = 0;
bool ledEstadoTx = false;
// --- ESP-NOW ---
uint8_t broadcastAddress[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
esp_now_peer_info_t peerInfo;
// Estructura del paquete de control (debe ser IDÉNTICA en el receptor)
typedef struct {
char comando;
} PaqueteControl;
PaqueteControl paquete;
// --- Centros calculados en la calibración ---
int centroIzqX, centroIzqY;
int centroDerX, centroDerY;
unsigned long ultimoTx = 0;
char ultimoImpreso = 0;
// ============================================================
// CALLBACK ESP-NOW DE RECEPCIÓN (latido del receptor)
// Compatible con core 2.x y 3.x
// ============================================================
#if ESP_ARDUINO_VERSION_MAJOR >= 3
void onLatidoRecv(const esp_now_recv_info_t *info, const uint8_t *data, int len) {
#else
void onLatidoRecv(const uint8_t *mac, const uint8_t *data, int len) {
#endif
if (len >= 1 && data[0] == 'P') { // 'P' = receptor presente
ultimoLatidoRx = millis();
hayLatido = true;
}
}
// Lee un eje promediando N_FILTRO muestras para reducir el ruido del ADC
int leerEjeFiltrado(int pin) {
long suma = 0;
for (int i = 0; i < N_FILTRO; i++) {
suma += analogRead(pin);
}
return suma / N_FILTRO;
}
// Mide el centro de los cuatro ejes con los sticks en reposo
void calibrarCentros() {
long sIzqX = 0, sIzqY = 0, sDerX = 0, sDerY = 0;
Serial.println("Calibrando... NO toques los joysticks");
for (int i = 0; i < N_CALIB; i++) {
sIzqX += analogRead(PIN_IZQ_X);
sIzqY += analogRead(PIN_IZQ_Y);
sDerX += analogRead(PIN_DER_X);
sDerY += analogRead(PIN_DER_Y);
delay(5);
}
centroIzqX = sIzqX / N_CALIB;
centroIzqY = sIzqY / N_CALIB;
centroDerX = sDerX / N_CALIB;
centroDerY = sDerY / N_CALIB;
Serial.println("Centros calculados:");
Serial.print(" IZQ centroX:"); Serial.print(centroIzqX);
Serial.print(" centroY:"); Serial.println(centroIzqY);
Serial.print(" DER centroX:"); Serial.print(centroDerX);
Serial.print(" centroY:"); Serial.println(centroDerY);
Serial.println();
}
// Decide el comando a enviar.
// Orden de prioridad: botón izq (G) > botón der (H) > eje con mayor desviación.
// Mapeo de ejes según tus mediciones:
// X (vertical): arriba=0 -> 'F' abajo=4095 -> 'B'
// Y (horizontal): derecha=0 -> 'R' izquierda=4095 -> 'L'
char decidirComando() {
// Botones (LOW = presionado por el pull-up interno)
if (digitalRead(PIN_IZQ_BTN) == LOW) return 'G';
if (digitalRead(PIN_DER_BTN) == LOW) return 'H';
// Lecturas filtradas
int ix = leerEjeFiltrado(PIN_IZQ_X);
int iy = leerEjeFiltrado(PIN_IZQ_Y);
int dx = leerEjeFiltrado(PIN_DER_X);
int dy = leerEjeFiltrado(PIN_DER_Y);
// Desviación respecto al centro
int dIX = ix - centroIzqX;
int dIY = iy - centroIzqY;
int dDX = dx - centroDerX;
int dDY = dy - centroDerY;
int absIX = abs(dIX), absIY = abs(dIY);
int absDX = abs(dDX), absDY = abs(dDY);
// El eje que más se aleja del centro manda
int maxAbs = max(max(absIX, absIY), max(absDX, absDY));
if (maxAbs <= UMBRAL) return 'S'; // todos dentro de la zona muerta
// Empates: se resuelven en este orden (X antes que Y)
if (maxAbs == absIX) return (dIX < 0) ? 'F' : 'B';
if (maxAbs == absDX) return (dDX < 0) ? 'F' : 'B';
if (maxAbs == absIY) return (dIY < 0) ? 'R' : 'L';
return (dDY < 0) ? 'R' : 'L';
}
void setup() {
Serial.begin(115200);
delay(300);
analogReadResolution(12); // Rango 0..4095
pinMode(PIN_IZQ_BTN, INPUT_PULLUP);
pinMode(PIN_DER_BTN, INPUT_PULLUP);
pinMode(LED_TX, OUTPUT);
digitalWrite(LED_TX, LOW);
// --- ESP-NOW ---
WiFi.mode(WIFI_STA);
if (esp_now_init() != ESP_OK) {
Serial.println("Error al iniciar ESP-NOW");
return;
}
// Escuchar el latido del receptor
esp_now_register_recv_cb(onLatidoRecv);
// Registrar el peer broadcast
memcpy(peerInfo.peer_addr, broadcastAddress, 6);
peerInfo.channel = 0; // 0 = canal actual
peerInfo.encrypt = false;
if (esp_now_add_peer(&peerInfo) != ESP_OK) {
Serial.println("Error al registrar el peer broadcast");
return;
}
calibrarCentros();
Serial.println("Transmisor listo");
}
void loop() {
unsigned long ahora = millis();
// ── Estado de enlace con el receptor ──
bool conectado = hayLatido && (ahora - ultimoLatidoRx < TIMEOUT_RX);
// ── LED de enlace: parpadea sin receptor, se APAGA al conectarse ──
if (conectado) {
digitalWrite(LED_TX, LOW); // conectado -> apagado
} else {
if (ahora - ultimoParpadeoTx >= 500) { // sin receptor -> parpadeo
ultimoParpadeoTx = ahora;
ledEstadoTx = !ledEstadoTx;
digitalWrite(LED_TX, ledEstadoTx);
}
}
// ── Envío de comandos ──
if (ahora - ultimoTx >= INTERVALO_TX) {
ultimoTx = ahora;
paquete.comando = decidirComando();
esp_now_send(broadcastAddress, (uint8_t *)&paquete, sizeof(paquete));
// Imprime solo cuando cambia, para no saturar el monitor
if (paquete.comando != ultimoImpreso) {
Serial.print("Comando enviado: ");
Serial.println(paquete.comando);
ultimoImpreso = paquete.comando;
}
}
}
//RECEPTOR CON RETORNO DE SEÑAL AL EMISOR
//LED: parpadea sin enlace, se APAGA al conectarse. Emite latido hacia el transmisor.
//Canal WiFi fijado para que la comunicación funcione en ambos sentidos.
#include <esp_now.h>
#include <WiFi.h>
#include <esp_wifi.h> // para fijar el canal WiFi
#include <Adafruit_GFX.h>
#include <Adafruit_ST7789.h>
#include <SPI.h>
// ───────────────── Canal WiFi (DEBE ser el mismo en transmisor y receptor) ─────────────────
#define WIFI_CANAL 1
// ───────────────── Polaridad del LED indicador ─────────────────
// Por defecto: LED que enciende con nivel ALTO (lo más común).
// Si tu LED está cableado al revés (enciende con BAJO), intercambia estos dos valores.
#define LED_ENCENDIDO HIGH
#define LED_APAGADO LOW
// ───────────────── ESP-NOW ─────────────────
char z;
int estado1 = 0; // 0 stop, 1 avanza, 2 retrocede, 3 derecha, 4 izquierda
// Estructura del paquete de control (debe ser IDÉNTICA al transmisor)
typedef struct {
char comando;
} PaqueteControl;
// Lo que llega del transmisor (se actualiza en el callback)
volatile char comandoRx = 'S';
volatile unsigned long ultimaRecepcion = 0;
volatile bool hayDatos = false; // se activa al recibir el primer paquete
const unsigned long TIMEOUT_ENLACE = 1000; // ms sin datos -> "SIN CONTROL" + LED parpadea
const unsigned long TIMEOUT_FAILSAFE = 500; // ms sin datos -> el robot se detiene
// ───────────────── Latido (heartbeat) hacia el transmisor ─────────────────
// El receptor emite por broadcast un paquetito periódico para que el
// transmisor sepa que está vivo y apague su LED.
uint8_t broadcastAddress[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
esp_now_peer_info_t peerInfo;
typedef struct {
char latido; // 'P' = presente
} PaqueteLatido;
PaqueteLatido latidoTx;
const unsigned long INTERVALO_LATIDO = 250; // ms entre latidos
unsigned long ultimoLatido = 0;
// Estado especial de pantalla cuando no hay enlace
#define VISTA_DESCONECTADO (-2)
int estadoVista = 0; // lo que se muestra en pantalla (estado1 o desconectado)
// ───────────────── Motores (TB6612) ─────────────────
#define STBY 27
#define AIN1 13
#define AIN2 5
#define PWMB 33
#define PWMA 14
#define BIN1 26
#define BIN2 25
#define LED 22 // LED indicador
#define PINBOTON 19 // BOTON (sin uso en este firmware; coincide con MISO del SPI por hardware)
// ───────────────── Pantalla ST7789 1.9" (170x320) ─────────────────
#define TFT_CS 15
#define TFT_RST 4
#define TFT_DC 2
#define TFT_BL 32 // Backlight
Adafruit_ST7789 tft = Adafruit_ST7789(TFT_CS, TFT_DC, TFT_RST);
// ───────────────── LED parpadeo ─────────────────
unsigned long ultimoParpadeo = 0;
bool ledEstado = false;
// ───────────────── Control de animación ─────────────────
int estadoMostrado = -1; // fuerza el primer dibujado
unsigned long ultimaAnim = 0;
int faseAnim = 0;
const unsigned long INTERVALO_ANIM = 120; // ms entre cuadros (movimiento)
bool blinkStop = false;
// ============================================================
// CALLBACK ESP-NOW (compatible con core 2.x y 3.x)
// ============================================================
#if ESP_ARDUINO_VERSION_MAJOR >= 3
void onDataRecv(const esp_now_recv_info_t *info, const uint8_t *incomingData, int len) {
#else
void onDataRecv(const uint8_t *mac, const uint8_t *incomingData, int len) {
#endif
if (len >= (int)sizeof(PaqueteControl)) {
PaqueteControl p;
memcpy(&p, incomingData, sizeof(p));
comandoRx = p.comando;
ultimaRecepcion = millis();
hayDatos = true;
}
}
// ============================================================
// HELPERS DE PANTALLA
// ============================================================
// Texto centrado horizontalmente (tomado de tu propio código)
void centerText(const char *text, int y) {
int16_t x1, y1;
uint16_t w, h;
tft.getTextBounds(text, 0, 0, &x1, &y1, &w, &h);
int x = (tft.width() - w) / 2;
tft.setCursor(x, y);
tft.print(text);
}
// Genera un color escalando su brillo (k entre 0.0 y 1.0)
uint16_t tono(uint8_t r, uint8_t g, uint8_t b, float k) {
if (k < 0) k = 0;
if (k > 1) k = 1;
return tft.color565((uint8_t)(r * k), (uint8_t)(g * k), (uint8_t)(b * k));
}
// Flechas sólidas (triángulos rellenos) en las 4 direcciones
void chevronUp(int cx, int cy, int hw, int h, uint16_t color) {
tft.fillTriangle(cx, cy, cx - hw, cy + h, cx + hw, cy + h, color);
}
void chevronDown(int cx, int cy, int hw, int h, uint16_t color) {
tft.fillTriangle(cx, cy, cx - hw, cy - h, cx + hw, cy - h, color);
}
void chevronRight(int cx, int cy, int hw, int h, uint16_t color) {
tft.fillTriangle(cx, cy, cx - h, cy - hw, cx - h, cy + hw, color);
}
void chevronLeft(int cx, int cy, int hw, int h, uint16_t color) {
tft.fillTriangle(cx, cy, cx + h, cy - hw, cx + h, cy + hw, color);
}
// Brillo de cada flecha según la fase, para crear el efecto de flujo
float brilloChevron(int i, int total) {
int head = faseAnim % total;
int dist = (head - i + total) % total; // 0 = la más brillante
float k = 1.0 - dist * 0.30;
if (k < 0.18) k = 0.18;
return k;
}
// Dibuja el fondo y el título cuando cambia el estado mostrado
void dibujarBase(int estado) {
tft.fillScreen(ST77XX_BLACK);
tft.setTextSize(3);
switch (estado) {
case 1: tft.setTextColor(ST77XX_GREEN); centerText("AVANZA", 12); break;
case 2: tft.setTextColor(ST77XX_ORANGE); centerText("RETROCEDE", 12); break;
case 3: tft.setTextColor(ST77XX_YELLOW); centerText("DERECHA", 12); break;
case 4: tft.setTextColor(ST77XX_YELLOW); centerText("IZQUIERDA", 12); break;
case VISTA_DESCONECTADO:
tft.setTextColor(ST77XX_RED); centerText("SIN CONTROL", 12); break;
default: tft.setTextColor(ST77XX_RED); centerText("STOP", 12); break;
}
}
// Dibuja un cuadro de la animación correspondiente al estado
void animar(int estado) {
const int N = 4;
switch (estado) {
case 1: { // AVANZA -> flechas hacia arriba, el brillo sube
int cx = 160, yBase = 144, paso = 26, hw = 42, h = 22;
for (int i = 0; i < N; i++) {
int cy = yBase - i * paso; // i mayor = más arriba
chevronUp(cx, cy, hw, h, tono(0, 255, 0, brilloChevron(i, N)));
}
} break;
case 2: { // RETROCEDE -> flechas hacia abajo, el brillo baja
int cx = 160, yTop = 58, paso = 26, hw = 42, h = 22;
for (int i = 0; i < N; i++) {
int cy = yTop + i * paso; // i mayor = más abajo
chevronDown(cx, cy, hw, h, tono(255, 120, 0, brilloChevron(i, N)));
}
} break;
case 3: { // DERECHA -> flechas hacia la derecha
int cy = 100, xLeft = 100, paso = 34, hw = 26, h = 26;
for (int i = 0; i < N; i++) {
int cx = xLeft + i * paso;
chevronRight(cx, cy, hw, h, tono(255, 255, 0, brilloChevron(i, N)));
}
} break;
case 4: { // IZQUIERDA -> flechas hacia la izquierda
int cy = 100, xRight = 220, paso = 34, hw = 26, h = 26;
for (int i = 0; i < N; i++) {
int cx = xRight - i * paso;
chevronLeft(cx, cy, hw, h, tono(255, 255, 0, brilloChevron(i, N)));
}
} break;
case VISTA_DESCONECTADO: { // SIN CONTROL -> círculo naranja parpadeante
blinkStop = !blinkStop;
uint16_t c = blinkStop ? tft.color565(255, 90, 0) : tft.color565(50, 15, 0);
tft.fillCircle(160, 105, 44, c);
} break;
default: { // STOP -> círculo rojo parpadeante con texto
blinkStop = !blinkStop;
uint16_t c = blinkStop ? tft.color565(255, 0, 0) : tft.color565(70, 0, 0);
int cx = 160, cy = 100, r = 46;
tft.fillCircle(cx, cy, r, c);
tft.setTextSize(3);
tft.setTextColor(ST77XX_WHITE, c); // fondo = color del círculo para sobrescribir limpio
centerText("STOP", cy - 10);
} break;
}
}
// Decide cuándo redibujar el fondo y cuándo avanzar la animación
void actualizarPantalla() {
if (estadoVista != estadoMostrado) {
dibujarBase(estadoVista);
estadoMostrado = estadoVista;
faseAnim = 0;
ultimaAnim = 0; // fuerza animar de inmediato
}
unsigned long ahora = millis();
// STOP y SIN CONTROL parpadean más lento que las flechas de movimiento
unsigned long intervalo = (estadoVista == 0 || estadoVista == VISTA_DESCONECTADO)
? 400 : INTERVALO_ANIM;
if (ahora - ultimaAnim >= intervalo) {
ultimaAnim = ahora;
animar(estadoVista);
faseAnim++;
}
}
// ============================================================
// SETUP
// ============================================================
void setup() {
// ── Pantalla ──
pinMode(TFT_BL, OUTPUT);
digitalWrite(TFT_BL, HIGH); // encender backlight
tft.init(170, 320); // 1.9" 170x320
tft.setRotation(3); // horizontal -> width()=320, height()=170
tft.fillScreen(ST77XX_BLACK);
// Bienvenida con tu branding
tft.setTextSize(3); tft.setTextColor(ST77XX_YELLOW); centerText("DRONACTIVOS", 35);
tft.setTextSize(3); tft.setTextColor(ST77XX_RED); centerText("ESP - 32", 80);
tft.setTextSize(2); tft.setTextColor(ST77XX_GREEN); centerText("EL PROFE ROYERO", 125);
delay(1500);
tft.fillScreen(ST77XX_BLACK);
// ── Serial y motores ──
Serial.begin(115200);
pinMode(AIN1, OUTPUT);
pinMode(AIN2, OUTPUT);
pinMode(BIN1, OUTPUT);
pinMode(BIN2, OUTPUT);
pinMode(LED, OUTPUT);
pinMode(PINBOTON, INPUT_PULLUP);
digitalWrite(LED, LED_APAGADO);
pinMode(STBY, OUTPUT);
digitalWrite(STBY, HIGH);
// ── ESP-NOW ──
WiFi.mode(WIFI_STA);
if (esp_now_init() != ESP_OK) {
Serial.println("Error al iniciar ESP-NOW");
return;
}
// Fijar el canal WiFi (clave para que el enlace funcione en ambos sentidos)
esp_wifi_set_channel(WIFI_CANAL, WIFI_SECOND_CHAN_NONE);
esp_now_register_recv_cb(onDataRecv);
// Registrar el peer broadcast para poder ENVIAR el latido
memcpy(peerInfo.peer_addr, broadcastAddress, 6);
peerInfo.channel = WIFI_CANAL; // mismo canal fijo
peerInfo.encrypt = false;
if (esp_now_add_peer(&peerInfo) != ESP_OK) {
Serial.println("Error al registrar el peer broadcast");
}
Serial.println("Receptor listo (ESP-NOW)");
delay(300);
}
// ============================================================
// LOOP
// ============================================================
void loop() {
unsigned long ahora = millis();
// ── Latido periódico hacia el transmisor ──
if (ahora - ultimoLatido >= INTERVALO_LATIDO) {
ultimoLatido = ahora;
latidoTx.latido = 'P';
esp_now_send(broadcastAddress, (uint8_t *)&latidoTx, sizeof(latidoTx));
}
// El enlace solo se considera activo si YA llegó al menos un paquete
bool enlaceActivo = hayDatos && (ahora - ultimaRecepcion < TIMEOUT_ENLACE);
// ── Control del LED según el enlace ESP-NOW ──
// Sin enlace -> parpadea ; con enlace -> se APAGA
if (enlaceActivo) {
digitalWrite(LED, LED_APAGADO); // enlace activo -> LED apagado
} else {
if (ahora - ultimoParpadeo >= 500) { // sin enlace -> parpadeo no bloqueante
ultimoParpadeo = ahora;
ledEstado = !ledEstado;
digitalWrite(LED, ledEstado ? LED_ENCENDIDO : LED_APAGADO);
}
}
// ── Comando recibido, o failsafe si no hay datos recientes ──
if (!hayDatos || (ahora - ultimaRecepcion > TIMEOUT_FAILSAFE)) {
z = 'S'; // sin señal -> detener
} else {
z = comandoRx;
}
if (z == 'F') estado1 = 1;
if (estado1 == 1) avanza();
if (z == 'S') estado1 = 0;
if (estado1 == 0) stop();
if (z == 'B') estado1 = 2;
if (estado1 == 2) retrocede();
if (z == 'R') estado1 = 3;
if (estado1 == 3) derecha();
if (z == 'L') estado1 = 4;
if (estado1 == 4) izquierda();
// ── Qué mostrar en pantalla ──
// Sin enlace -> "SIN CONTROL"; con enlace -> el estado actual del robot.
if (!enlaceActivo) estadoVista = VISTA_DESCONECTADO;
else estadoVista = estado1;
actualizarPantalla();
}
// ============================================================
// FUNCIONES DE MOVIMIENTO
// (derecha/izquierda intercambiadas para corregir el giro invertido)
// ============================================================
void stop() {
analogWrite(PWMA, 0);
analogWrite(PWMB, 0);
digitalWrite(AIN1, LOW);
digitalWrite(AIN2, LOW);
digitalWrite(BIN1, LOW);
digitalWrite(BIN2, LOW);
}
void avanza() {
analogWrite(PWMA, 200);
analogWrite(PWMB, 200);
digitalWrite(AIN1, HIGH);
digitalWrite(AIN2, LOW);
digitalWrite(BIN1, HIGH);
digitalWrite(BIN2, LOW);
}
void avanza_izq() {
analogWrite(PWMA, 100);
analogWrite(PWMB, 200);
digitalWrite(AIN1, HIGH);
digitalWrite(AIN2, LOW);
digitalWrite(BIN1, HIGH);
digitalWrite(BIN2, LOW);
}
void retrocede() {
analogWrite(PWMA, 200);
analogWrite(PWMB, 200);
digitalWrite(AIN1, LOW);
digitalWrite(AIN2, HIGH);
digitalWrite(BIN1, LOW);
digitalWrite(BIN2, HIGH);
}
void derecha() {
analogWrite(PWMA, 200);
analogWrite(PWMB, 200);
digitalWrite(AIN1, LOW);
digitalWrite(AIN2, LOW);
digitalWrite(BIN1, HIGH);
digitalWrite(BIN2, LOW);
}
void izquierda() {
analogWrite(PWMA, 200);
analogWrite(PWMB, 200);
digitalWrite(AIN1, HIGH);
digitalWrite(AIN2, LOW);
digitalWrite(BIN1, LOW);
digitalWrite(BIN2, LOW);
}

Comentarios
Publicar un comentario