Mini Robot Tanque con ESP32

Autor: Oscar Gonzalez

Mini Robot Tanque con ESP32

Tiempo de lectura: 11 minutos

Pequeño robot tanque con control remoto basado en ESP32 ATOM S3 de M5Stack

Mini Robot Tanque con ESP32

  • 29

0 Principiante

Programación ESP32

La parte de la programación se divide en dos partes.

La primera es el programa que va en el propio tanque y aunque parezca un programa muy largo, es bien sencillo. Lo primero que hace es inicializar diversas funciones del ATOM S3, el sensor de distancia y también el sistema ESP-NOW que será el que nos va a permitir controlar el tanque a distancia.

¿Qué es ESP-NOW?

ESP-NOW es un protocolo de comunicación de corto alcance y baja potencia que permite que múltiples dispositivos se comuniquen sin Wi-Fi. Este protocolo es similar a la conexión inalámbrica de 2,4 GHz y los dispositivos se emparejan antes de comunicarse mediante la dirección MAC.

La segunda parte del código será la que se cargará en el mando de control, que no será otra cosa que otro ESP32. En mi caso, he usado un M5 Core, aunque puedes utilizar otra placa de control si quieres. El M5 Stack Core es muy práctico para controlar el tanque, ya que tiene una pantalla LCD junto con unos pulsadores que usaré para ir hacia delante y girar. Todo eso en un encapsulado listo para utilizar.

El programa principal del tanque lo puedes ver aquí abajo. Está hecho con PlatformIO y puedes descargarlo completo desde GitHub aquí.

Como podrás observar, hay dos modos de funcionamiento, ya que la pantalla LCD del ATOM S3 sirve de pulsador. El modo por defecto es ser controlado por el mando, pero si pulsas la pantalla, se activará el modo de sensor de distancia.

Ese modo es muy sencillo y consiste en que cuando se detecta un obstáculo delante, el tanque intentará mantener la distancia activando los motores hacia delante o atrás.

Es una función muy sencilla que estoy seguro, se puede sacar mucho provecho, pero da una idea de cómo interpretar la distancia usando el sensor de distancia. Lo propio es hacer un control PID, pero para no complicar demasiado el programa, por el momento se queda así. 

¡Por supuesto, siéntete libre de modificarlo a tu gusto!

Programa del tanque

/*
  M5Tank - Oscar Gonzalez - Abril 2023
  www-Bricogeek.com
*/
#include <m5unified.h>
#include <esp_now.h>
#include <WiFi.h>
#include <M5GFX.h>
#include <Adafruit_VL6180X.h>
#include <math.h>
#include <Arduino.h>
#include <WiFi.h>
#include <Wire.h>

// ATOM S3 Tank Mac Address: DC:54:75:D0:9A:08
const int MOTOR_LEFT = 5;
const int MOTOR_RIGHT = 6;

const int MLEFT = 0;
const int MRIGHT = 1;

// setting PWM properties
const int pwmfreq1 = 400;
const int pwmResolution1 = 8;

const int pwmfreq2 = 400;
const int pwmResolution2 = 8;

int mode = 0; // 0: tank 1: distancia

Adafruit_VL6180X vl = Adafruit_VL6180X();

// Structure example to receive data
// Must match the sender structure
typedef struct struct_message {
    bool left;
    bool run;
    bool right;
    float x;
    float y;
    float z;
} struct_message;

// Create a struct_message called myData
struct_message rfData;

// callback function that will be executed when data is received
void OnDataRecv(const uint8_t * mac, const uint8_t *incomingData, int len) {
  memcpy(&rfData, incomingData, sizeof(rfData));
}

//==========================================================================================
void setup() {

    M5.begin();
    M5.Power.begin();      
    Serial.begin (115200);      

    // espera al dispositivo USB
    while (!Serial) {
      delay(1);
    }    

    Serial.println("Serial OK");          

    Wire.begin(38, 39);

    // Borramos la pantalla
    M5.Lcd.fillScreen(TFT_BLACK);

    // Activamos Wifi
    WiFi.mode(WIFI_STA);

    // Iniciamos ESP-NOW
    if (esp_now_init() != ESP_OK) {
      M5.Lcd.setTextSize(2);
      M5.Lcd.setCursor(5, 5);
      M5.Lcd.setTextColor(TFT_RED);
      M5.Lcd.println("ESP-NOW Error");
      return;
    }
    
    // Función que recibe los datos ESP-NOW desde el emisor    
    esp_now_register_recv_cb(OnDataRecv);    

    // M5Tank!
    M5.Lcd.setTextSize(3);
    M5.Lcd.setCursor(5, 5);
    M5.Lcd.println("M5Tank");

    // Mostramos la dirección MAC    
    //M5.Lcd.setTextSize(1);
    //M5.Lcd.setCursor(5, 50);
    //M5.Lcd.setTextColor(TFT_BLUE);
    //M5.Lcd.println(WiFi.macAddress());

    // Datos IMU recibidos
    M5.Lcd.setTextColor(TFT_GREEN);
    M5.Lcd.setTextSize(2);
    M5.Lcd.setCursor(5, 50);      
    M5.Lcd.println("X: 0.0");    
    M5.Lcd.setCursor(5, 68);      
    M5.Lcd.println("Y: 0.0");    
    M5.Lcd.setCursor(5, 86);      
    M5.Lcd.println("Z: 0.0");            

    // Inicializa el sensor laser
    if (!vl.begin()) {
      M5.Lcd.setTextColor(TFT_RED);
      M5.Lcd.setTextSize(1);
      M5.Lcd.setCursor(5, 30);      
      M5.Lcd.println("VL6180 error!");      
    }    
    else
    {
      M5.Lcd.setTextColor(TFT_GREEN);
      M5.Lcd.setTextSize(1);
      M5.Lcd.setCursor(5, 30);    
      M5.Lcd.println("VL6180 OK");    
    }
    
    // configure LED PWM functionalitites
    ledcSetup(MLEFT, pwmfreq1, pwmResolution1);
    ledcSetup(MRIGHT, pwmfreq2, pwmResolution2);
    
    // attach the channel to the GPIO to be controlled
    ledcAttachPin(MOTOR_LEFT, MLEFT);    
    ledcAttachPin(MOTOR_RIGHT, MRIGHT);    

}

// MLEFT or MRIGHT
// Speed: 0-255
void setMotorSpeed(int motor, int speed) { ledcWrite(motor, speed); }

//==========================================================================================
void loop() {

    M5.update();

    M5.Lcd.fillRect(0, 50, 128, 50, TFT_BLUE);
 
    // Datos IMU recibidos
    M5.Lcd.setTextColor(TFT_GREEN);
    M5.Lcd.setTextSize(2);
    M5.Lcd.setCursor(5, 50);      
    M5.Lcd.print("LEFT: ");    
    M5.Lcd.println(rfData.left);    

    M5.Lcd.setCursor(5, 68);      
    M5.Lcd.print("RIGHT: ");    
    M5.Lcd.println(rfData.right);

    M5.Lcd.setCursor(5, 86);      
    M5.Lcd.print("RUN: ");            
    M5.Lcd.println(rfData.run);

    // Cambiamos modo
    if (M5.BtnA.wasClicked())
    {
      if (mode == 0) { mode = 1; }
      else { mode = 0; }    
    }    

    M5.Lcd.fillRect(0, 100, 128, 28, TFT_BLACK);    
  
    if (mode == 0)
    {
      if (rfData.run == 1)
      {    
        
        setMotorSpeed(MLEFT, 50);
        setMotorSpeed(MRIGHT, 200);      

      }
      else if (rfData.left == 1)
      {
        setMotorSpeed(MLEFT, 50);
        setMotorSpeed(MRIGHT, 50);
      }
      else if (rfData.right == 1)
      {
        setMotorSpeed(MLEFT, 200);
        setMotorSpeed(MRIGHT, 200);
      }  
      else {
        setMotorSpeed(MLEFT, 0);
        setMotorSpeed(MRIGHT, 0);    
      }
    }
  else { // Mode 1
      // Leemos distancia
      uint8_t range = vl.readRange();
      uint8_t status = vl.readRangeStatus();  

      int distancia = 0;
      if (status == VL6180X_ERROR_NONE) {
        distancia = range;
      }  

      // Barra de distancia
      M5.Lcd.fillRect(0, 110, distancia, 18, TFT_RED);

      if (distancia > 0)
      {
        if (distancia > 90)
        {
          setMotorSpeed(MLEFT, 50);
          setMotorSpeed(MRIGHT, 200);      
        }
        else
        {
          if (distancia < 110)
          {
            setMotorSpeed(MLEFT, 200);
            setMotorSpeed(MRIGHT, 50);      
          }
        }        
      }
      else
      {
        setMotorSpeed(MLEFT, 0);
        setMotorSpeed(MRIGHT, 0);            
      }

    }

    if (mode == 0) { M5.Lcd.fillCircle(120, 10, 8, TFT_GREEN); }
    else { M5.Lcd.fillCircle(120, 10, 8, TFT_RED); }    

    delay(10);    

}
//==========================================================================================

Programa del mando de control

Ahora solo queda el programa del mando de control que puedes ver aquí abajo. Recuerda que el programa completo para PlatformIO lo puedes descargar también desde nuestro GitHub.

Detalle de la interfaz del mando de control del tanque

Detalle de la interfaz del mando de control del tanque

/*
  M5Tank Remote - Oscar Gonzalez - Abril 2023
  www-Bricogeek.com
*/
#include <Arduino.h>
#include <M5Stack.h>
#include <esp_now.h>
#include <WiFi.h>

// Dirección MAC del tanque (IMPORTANTE: NO COPIES ESTA, PON LA TUYA!)
uint8_t tankMAC[] = { 0xDC, 0x54, 0x75, 0xD0, 0x9A, 0x08 };

typedef struct struct_message {
    bool left;
    bool run;
    bool right;
    float x;
    float y;
    float z;
} struct_message;

// Create a struct_message called myData
struct_message rfData;

// Create peer interface
esp_now_peer_info_t peerInfo;

float pitch, roll, yaw;
float x, y, z;

// callback when data is sent
void OnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
  //Serial.print("rnLast Packet Send Status:t");
  //Serial.println(status == ESP_NOW_SEND_SUCCESS ? "Delivery Success" : "Delivery Fail");
}

void setup() {
  // put your setup code here, to run once:
  M5.begin();
  M5.Power.begin();

  M5.Imu.Init();
  M5.Imu.getAhrsData(&pitch, &roll, &yaw);

  // Init Serial Monitor
  Serial.begin(115200);

  // Set device as a Wi-Fi Station
  WiFi.mode(WIFI_STA);

  // Init ESP-NOW
  if (esp_now_init() != ESP_OK) {
    Serial.println("Error initializing ESP-NOW");
    return;
  }

  // Once ESPNow is successfully Init, we will register for Send CB to
  // get the status of Trasnmitted packet
  esp_now_register_send_cb(OnDataSent);
  
  // Register peer
  memcpy(peerInfo.peer_addr, tankMAC, sizeof(tankMAC));
  peerInfo.channel = 0;  
  peerInfo.encrypt = false;
  
  // Add peer        
  if (esp_now_add_peer(&peerInfo) != ESP_OK){
    Serial.println("Failed to add peer");
    return;
  }  

  M5.Lcd.clear(BLACK);
  M5.Lcd.setTextColor(YELLOW);
  M5.Lcd.setTextSize(3);
  M5.Lcd.setCursor(55, 0);
  M5.Lcd.println("M5Tank Remote");
  M5.Lcd.setTextColor(WHITE);
  M5.Lcd.setCursor(35, 25);
  M5.Lcd.println("Tanks everwhere! ;)");  

  M5.Lcd.setTextSize(2);
  M5.Lcd.setTextColor(TFT_GREEN);
  M5.Lcd.setCursor(0, 200);
  M5.Lcd.println("   LEFT     RUN    RIGHT");        
  
}

void loop() {
  // put your main code here, to run repeatedly:

  //M5.Imu.getAhrsData(&pitch, &roll, &yaw);
  M5.Imu.getAccelData(&x, &y, &z);

  rfData.left = false;
  rfData.right = false;
  rfData.run = false;

  rfData.x = 0;
  rfData.y = 0;
  rfData.z = 0;

  if (M5.BtnA.isPressed())
  {    
    rfData.left = true;
    M5.Lcd.setTextSize(5);
    M5.Lcd.setCursor(100, 100);
    M5.Lcd.setTextColor(TFT_YELLOW);
    M5.Lcd.println("LEFT");      
  }
  else if (M5.BtnB.isPressed())
  {    
    rfData.run = true;
    M5.Lcd.setTextSize(5);
    M5.Lcd.setCursor(100, 100);
    M5.Lcd.setTextColor(TFT_YELLOW);
    M5.Lcd.println("RUN");      
  }
  else if (M5.BtnC.isPressed())
  {   
    rfData.right = true;
    M5.Lcd.setTextSize(5);
    M5.Lcd.setCursor(100, 100);
    M5.Lcd.setTextColor(TFT_YELLOW);
    M5.Lcd.println("RIGHT");      
  }
  else
  {
    M5.Lcd.fillRect(0, 50, 320, 130, TFT_BLUE);
  }

  // Datos IMU
  M5.Lcd.fillRect(0, 50, 0, 10, TFT_WHITE);
  M5.Lcd.setTextSize(2);
  M5.Lcd.setTextColor(TFT_PINK);
  M5.Lcd.setCursor(0, 50);
  M5.Lcd.println(x);  

  // Send message via ESP-NOW
  esp_err_t result = esp_now_send(tankMAC, (uint8_t *) &rfData, sizeof(rfData));
   
  if (result == ESP_OK) {
    //Serial.println("Sent with success");
  }
  else {
    //Serial.println("Error sending the data");
  }  

  M5.update();

  delay(10);

}

¿Cómo saber la dirección MAC del tanque?

Lo más importante para que el mando pueda comunicarse con el tanque, es saber la dirección MAC del ATOM S3.

Para eso, he dejado comentado unas líneas en el código del tanque que muestran en pantalla la dirección para que puedas anotarla. Dado que están comentadas, no se ejecutan. Por lo tanto, debes des comentar estas líneas quitando el símbolo "/" de cada una de ellas.

    // Mostramos la dirección MAC    
    M5.Lcd.setTextSize(1);
    M5.Lcd.setCursor(5, 50);
    M5.Lcd.setTextColor(TFT_BLUE);
    M5.Lcd.println(WiFi.macAddress());

Cuando lo hagas, compila y carga el programa en el tanque y anota bien la dirección que se muestra en la pantalla LCD. Luego puedes volver a comentar las líneas y cargar de nuevo el programa, ya que no lo vas a necesitar.

Ahora vamos al código del mando, en este caso un M5Stack Core y debes localizar ésta línea:

// Dirección MAC del tanque
uint8_t tankMAC[] = { 0xDC, 0x54, 0x75, 0xD0, 0x9A, 0x08 };

Aquí es donde debes poner la dirección MAC del tanque para que se pueda comunicar con él.

Ten en cuenta que la dirección que te ha mostrado el tanque en la pantalla solo tiene 2 dígitos y debes añadirle al principio "0x" para que funcione.

Por ejemplo, si la dirección más que muestra el tanque es: A0, 23, 42, D0, 9B, 02

La dirección que debes indicar en el mando es:  0xA0, 0x23, 0x42, 0xD0, 0x9B, 0x02

Fíjate que es lo mismo, pero simplemente añadir "0x" antes de cada número.

Cuando lo tengas todo listo, carga el código en el mando y quedará todo listo.

Ajuste de los motores

Llegado a este punto, ya tienes el tanque casi listo para funcionar y cuando lo hagas, es posible que cuando intentes ir recto (RUN) el tanque no vaya recto del todo. Eso es principalmente debido a las tolerancias de fabricación de los diferentes servos.

Para conseguir que vaya lo más recto posible, debes ajustar la velocidad de cada motor cuando debe ir recto, ya que para un mismo valor, puede que vayan más rápido o despacio y gire en lugar de ir bien recto.

Para realizar ese ajuste, localiza las líneas de código de aquí abajo. Verás que yo he utilizado valores de 50 y 200 en mi caso, pero jugando con esos valores, podrás hacer que avance bien recto. Puedes subir o bajar de 5 o 10 unidades de cada vez y vas probando hasta que consigas que vaya recto.

      if (rfData.run == 1)
      {            
        setMotorSpeed(MLEFT, 50);
        setMotorSpeed(MRIGHT, 200);      
      }