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

Entradas populares de este blog

CLASIFICADOR DE COLORES CON SENSOR TCS3472 (I2C) CON ARDUINO NANO

PROTOTIPO - CONTROL DE ACCESO CON RFID + TECLADO CON ALMACENAMIENTO EN MICRO SD

CONTROL DE ACCESO CON RFID YMODULO RTC DS3231