Módulo 11

Proyecto mecatrónica: plataforma IoT con robot móvil y telemetría

Aplicaciones en Mecatrónica

ESP32 Mecatrónica IoT UNAM

Introducción: Plataforma IoT Mecatrónica con Robot Móvil

Este proyecto integra los conceptos fundamentales de la robótica móvil autónoma con las tecnologías modernas de Internet de las Cosas (IoT) y sistemas de telemetría en tiempo real. Utilizando el ESP32 como núcleo computacional, desarrollaremos una plataforma integral que combina navegación autónoma, recolección de datos sensoriales y comunicación inalámbrica avanzada.

La mecatrónica moderna en el contexto de la Industria 4.0 requiere sistemas que no solo funcionen de manera autónoma, sino que también proporcionen información valiosa para la toma de decisiones en tiempo real. Este proyecto aborda esta necesidad mediante:

Robótica Móvil Autónoma
  • Sistemas de navegación por sensores
  • Algoritmos de evasión de obstáculos
  • Control diferencial de motores
  • Mapeo y localización simultánea (SLAM)
Telemetría IoT Avanzada
  • Comunicación MQTT bidireccional
  • Dashboard web en tiempo real
  • Análisis de datos sensoriales
  • Control remoto inteligente

Aplicaciones en la Industria 4.0

Las aplicaciones reales de esta tecnología incluyen:

  • Inspección industrial automatizada: Robots móviles que navegan por plantas industriales recolectando datos de temperatura, vibración y calidad del aire
  • Logística inteligente: Sistemas de transporte autónomo con seguimiento en tiempo real de inventarios
  • Monitoreo ambiental: Plataformas móviles que mapean condiciones ambientales en tiempo real
  • Agricultura de precisión: Robots agrícolas que monitorizan cultivos y optimizan recursos

Arquitectura Técnica del Sistema

La arquitectura del sistema se basa en una topología distribuida que integra múltiples subsistemas especializados trabajando en conjunto para lograr navegación autónoma y telemetría avanzada.

Núcleo Computacional ESP32

El ESP32 actúa como el cerebro del sistema, coordinando:

  • Core 0: Manejo de comunicaciones Wi-Fi/Bluetooth y telemetría
  • Core 1: Procesamiento de sensores y algoritmos de navegación
  • Co-procesador ULP: Monitoreo continuo de sensores críticos
  • Memoria PSRAM: Buffer de datos sensoriales y mapas de navegación

Subsistemas Integrados

  • Sistema de Navegación: Sensores ultrasónicos, IMU y encoders
  • Control de Motores: Drivers PWM con retroalimentación
  • Adquisición de Datos: Múltiples sensores ambientales
  • Comunicación IoT: Protocolos MQTT, HTTP y WebSocket

Diseño del Chasis y Control de Motores

C++ - Sistema de Control Diferencial
#include <ESP32Servo.h>
#include <WiFi.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>

// Definición de pines para motores
#define MOTOR_LEFT_PWM    18
#define MOTOR_LEFT_DIR1   19
#define MOTOR_LEFT_DIR2   21
#define MOTOR_RIGHT_PWM   22
#define MOTOR_RIGHT_DIR1  23
#define MOTOR_RIGHT_DIR2  25

// Encoders para odometría
#define ENCODER_LEFT_A    26
#define ENCODER_LEFT_B    27
#define ENCODER_RIGHT_A   32
#define ENCODER_RIGHT_B   33

class DifferentialDrive {
private:
    volatile long leftEncoder = 0;
    volatile long rightEncoder = 0;
    float wheelBase = 0.15; // Distancia entre ruedas en metros
    float wheelRadius = 0.03; // Radio de rueda en metros
    int countsPerRevolution = 360;
    
public:
    struct RobotPose {
        float x, y, theta;
    } pose = {0, 0, 0};
    
    void init() {
        // Configurar pines de motores
        pinMode(MOTOR_LEFT_PWM, OUTPUT);
        pinMode(MOTOR_LEFT_DIR1, OUTPUT);
        pinMode(MOTOR_LEFT_DIR2, OUTPUT);
        pinMode(MOTOR_RIGHT_PWM, OUTPUT);
        pinMode(MOTOR_RIGHT_DIR1, OUTPUT);
        pinMode(MOTOR_RIGHT_DIR2, OUTPUT);
        
        // Configurar interrupciones para encoders
        pinMode(ENCODER_LEFT_A, INPUT_PULLUP);
        pinMode(ENCODER_LEFT_B, INPUT_PULLUP);
        pinMode(ENCODER_RIGHT_A, INPUT_PULLUP);
        pinMode(ENCODER_RIGHT_B, INPUT_PULLUP);
        
        attachInterrupt(digitalPinToInterrupt(ENCODER_LEFT_A), 
                       [](){encoderISR(true);}, CHANGE);
        attachInterrupt(digitalPinToInterrupt(ENCODER_RIGHT_A), 
                       [](){encoderISR(false);}, CHANGE);
    }
    
    void setMotorSpeeds(int leftSpeed, int rightSpeed) {
        // Control motor izquierdo
        if (leftSpeed > 0) {
            digitalWrite(MOTOR_LEFT_DIR1, HIGH);
            digitalWrite(MOTOR_LEFT_DIR2, LOW);
        } else if (leftSpeed < 0) {
            digitalWrite(MOTOR_LEFT_DIR1, LOW);
            digitalWrite(MOTOR_LEFT_DIR2, HIGH);
        } else {
            digitalWrite(MOTOR_LEFT_DIR1, LOW);
            digitalWrite(MOTOR_LEFT_DIR2, LOW);
        }
        analogWrite(MOTOR_LEFT_PWM, abs(leftSpeed));
        
        // Control motor derecho
        if (rightSpeed > 0) {
            digitalWrite(MOTOR_RIGHT_DIR1, HIGH);
            digitalWrite(MOTOR_RIGHT_DIR2, LOW);
        } else if (rightSpeed < 0) {
            digitalWrite(MOTOR_RIGHT_DIR1, LOW);
            digitalWrite(MOTOR_RIGHT_DIR2, HIGH);
        } else {
            digitalWrite(MOTOR_RIGHT_DIR1, LOW);
            digitalWrite(MOTOR_RIGHT_DIR2, LOW);
        }
        analogWrite(MOTOR_RIGHT_PWM, abs(rightSpeed));
    }
    
    void updateOdometry() {
        static long lastLeftEncoder = 0;
        static long lastRightEncoder = 0;
        static unsigned long lastTime = millis();
        
        unsigned long currentTime = millis();
        float dt = (currentTime - lastTime) / 1000.0;
        
        long deltaLeft = leftEncoder - lastLeftEncoder;
        long deltaRight = rightEncoder - lastRightEncoder;
        
        // Convertir pulsos a distancia
        float leftDistance = (deltaLeft * 2 * PI * wheelRadius) / countsPerRevolution;
        float rightDistance = (deltaRight * 2 * PI * wheelRadius) / countsPerRevolution;
        
        // Calcular odometría diferencial
        float deltaDistance = (leftDistance + rightDistance) / 2.0;
        float deltaTheta = (rightDistance - leftDistance) / wheelBase;
        
        pose.x += deltaDistance * cos(pose.theta + deltaTheta/2.0);
        pose.y += deltaDistance * sin(pose.theta + deltaTheta/2.0);
        pose.theta += deltaTheta;
        
        lastLeftEncoder = leftEncoder;
        lastRightEncoder = rightEncoder;
        lastTime = currentTime;
    }
    
    static void encoderISR(bool isLeft) {
        // Implementación de la rutina de interrupción para encoders
        // Se ejecuta en el contexto de interrupción
    }
};

Integración de Sensores y Fusión de Datos

C++ - Sistema Multi-Sensor
#include <Wire.h>
#include <MPU6050.h>
#include <NewPing.h>
#include <DHT.h>

class MultiSensorSystem {
private:
    MPU6050 imu;
    NewPing sonar[4] = {
        NewPing(2, 3, 200),   // Frente
        NewPing(4, 5, 200),   // Derecha
        NewPing(6, 7, 200),   // Atrás
        NewPing(8, 9, 200)    // Izquierda
    };
    DHT dht(10, DHT22);
    
public:
    struct SensorData {
        // IMU Data
        float accel_x, accel_y, accel_z;
        float gyro_x, gyro_y, gyro_z;
        float temp_imu;
        
        // Sensores de distancia
        float distances[4]; // [frente, derecha, atrás, izquierda]
        
        // Sensores ambientales
        float temperature;
        float humidity;
        
        // Timestamp
        unsigned long timestamp;
    } data;
    
    void init() {
        Wire.begin();
        imu.initialize();
        dht.begin();
        
        if (!imu.testConnection()) {
            Serial.println("Error: IMU no detectado");
        }
        
        // Calibrar sensores
        calibrateSensors();
    }
    
    void readAllSensors() {
        data.timestamp = millis();
        
        // Leer IMU
        int16_t ax, ay, az;
        int16_t gx, gy, gz;
        imu.getMotion6(&ax, &ay, &az, &gx, &gy, &gz);
        
        data.accel_x = ax / 16384.0; // Convertir a g
        data.accel_y = ay / 16384.0;
        data.accel_z = az / 16384.0;
        data.gyro_x = gx / 131.0;    // Convertir a deg/s
        data.gyro_y = gy / 131.0;
        data.gyro_z = gz / 131.0;
        
        // Leer sensores ultrasónicos
        for (int i = 0; i < 4; i++) {
            data.distances[i] = sonar[i].ping_cm();
            if (data.distances[i] == 0) data.distances[i] = 200; // Máxima distancia
            delay(50); // Evitar interferencias
        }
        
        // Leer sensores ambientales
        data.temperature = dht.readTemperature();
        data.humidity = dht.readHumidity();
        
        // Aplicar filtros de ruido
        applyFilters();
    }
    
    void applyFilters() {
        // Filtro paso bajo para IMU
        static float alpha = 0.1;
        static float filtered_accel[3] = {0};
        
        filtered_accel[0] = alpha * data.accel_x + (1 - alpha) * filtered_accel[0];
        filtered_accel[1] = alpha * data.accel_y + (1 - alpha) * filtered_accel[1];
        filtered_accel[2] = alpha * data.accel_z + (1 - alpha) * filtered_accel[2];
        
        data.accel_x = filtered_accel[0];
        data.accel_y = filtered_accel[1];
        data.accel_z = filtered_accel[2];
    }
    
    bool isObstacleDetected(float threshold = 30.0) {
        return (data.distances[0] < threshold || 
                data.distances[1] < threshold || 
                data.distances[3] < threshold);
    }
    
    void calibrateSensors() {
        Serial.println("Iniciando calibración de sensores...");
        
        // Calibrar IMU (mantener inmóvil durante 5 segundos)
        float accel_offset[3] = {0};
        float gyro_offset[3] = {0};
        
        for (int i = 0; i < 100; i++) {
            int16_t ax, ay, az, gx, gy, gz;
            imu.getMotion6(&ax, &ay, &az, &gx, &gy, &gz);
            
            accel_offset[0] += ax;
            accel_offset[1] += ay;
            accel_offset[2] += az;
            gyro_offset[0] += gx;
            gyro_offset[1] += gy;
            gyro_offset[2] += gz;
            
            delay(50);
        }
        
        // Aplicar offsets de calibración
        for (int i = 0; i < 3; i++) {
            accel_offset[i] /= 100.0;
            gyro_offset[i] /= 100.0;
        }
        
        Serial.println("Calibración completada");
    }
};

Sistema de Comunicación IoT

C++ - Telemetría MQTT Avanzada
#include <WiFi.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
#include <WebSocketsClient.h>

class IoTCommunication {
private:
    WiFiClient wifiClient;
    PubSubClient mqttClient;
    WebSocketsClient webSocket;
    
    const char* mqtt_server = "broker.hivemq.com";
    const int mqtt_port = 1883;
    const char* device_id = "ESP32_Robot_001";
    
    // Tópicos MQTT
    const char* topic_telemetry = "robot/telemetry";
    const char* topic_control = "robot/control";
    const char* topic_status = "robot/status";
    
    unsigned long lastTelemetry = 0;
    const unsigned long telemetryInterval = 1000; // 1 segundo
    
public:
    struct ControlCommand {
        String action;
        float value1, value2;
        bool emergency_stop = false;
    } command;
    
    IoTCommunication() : mqttClient(wifiClient) {}
    
    void init(const char* ssid, const char* password) {
        // Conectar a Wi-Fi
        WiFi.begin(ssid, password);
        Serial.print("Conectando a Wi-Fi");
        
        while (WiFi.status() != WL_CONNECTED) {
            delay(500);
            Serial.print(".");
        }
        
        Serial.println();
        Serial.println("Wi-Fi conectado!");
        Serial.print("IP: ");
        Serial.println(WiFi.localIP());
        
        // Configurar MQTT
        mqttClient.setServer(mqtt_server, mqtt_port);
        mqttClient.setCallback([this](char* topic, byte* payload, unsigned int length) {
            this->mqttCallback(topic, payload, length);
        });
        
        // Configurar WebSocket
        webSocket.begin("192.168.1.100", 8080, "/ws");
        webSocket.onEvent([this](WStype_t type, uint8_t * payload, size_t length) {
            this->webSocketEvent(type, payload, length);
        });
        
        connectMQTT();
    }
    
    void loop() {
        if (!mqttClient.connected()) {
            connectMQTT();
        }
        mqttClient.loop();
        webSocket.loop();
        
        // Enviar telemetría
        if (millis() - lastTelemetry > telemetryInterval) {
            sendTelemetry();
            lastTelemetry = millis();
        }
    }
    
    void sendTelemetry() {
        DynamicJsonDocument telemetryDoc(1024);
        
        // Datos del robot
        telemetryDoc["device_id"] = device_id;
        telemetryDoc["timestamp"] = millis();
        telemetryDoc["uptime"] = millis() / 1000;
        
        // Posición y orientación
        JsonObject position = telemetryDoc.createNestedObject("position");
        position["x"] = robot.pose.x;
        position["y"] = robot.pose.y;
        position["theta"] = robot.pose.theta;
        
        // Datos de sensores
        JsonObject sensors = telemetryDoc.createNestedObject("sensors");
        sensors["temperature"] = sensors_system.data.temperature;
        sensors["humidity"] = sensors_system.data.humidity;
        
        JsonArray distances = sensors.createNestedArray("distances");
        for (int i = 0; i < 4; i++) {
            distances.add(sensors_system.data.distances[i]);
        }
        
        JsonObject imu = sensors.createNestedObject("imu");
        imu["accel_x"] = sensors_system.data.accel_x;
        imu["accel_y"] = sensors_system.data.accel_y;
        imu["accel_z"] = sensors_system.data.accel_z;
        imu["gyro_x"] = sensors_system.data.gyro_x;
        imu["gyro_y"] = sensors_system.data.gyro_y;
        imu["gyro_z"] = sensors_system.data.gyro_z;
        
        // Estado del sistema
        JsonObject status = telemetryDoc.createNestedObject("status");
        status["battery_voltage"] = analogRead(A0) * 3.3 / 4095.0 * 2; // Divisor de voltaje
        status["wifi_rssi"] = WiFi.RSSI();
        status["free_heap"] = ESP.getFreeHeap();
        status["obstacle_detected"] = sensors_system.isObstacleDetected();
        
        // Convertir a string y enviar
        String telemetryString;
        serializeJson(telemetryDoc, telemetryString);
        
        // Enviar por MQTT
        mqttClient.publish(topic_telemetry, telemetryString.c_str());
        
        // Enviar por WebSocket
        webSocket.sendTXT(telemetryString);
        
        Serial.println("Telemetría enviada: " + telemetryString);
    }
    
    void mqttCallback(char* topic, byte* payload, unsigned int length) {
        String message;
        for (int i = 0; i < length; i++) {
            message += (char)payload[i];
        }
        
        Serial.println("Mensaje MQTT recibido: " + message);
        
        if (String(topic) == topic_control) {
            processControlCommand(message);
        }
    }
    
    void webSocketEvent(WStype_t type, uint8_t * payload, size_t length) {
        switch(type) {
            case WStype_DISCONNECTED:
                Serial.println("WebSocket desconectado");
                break;
                
            case WStype_CONNECTED:
                Serial.println("WebSocket conectado");
                break;
                
            case WStype_TEXT:
                Serial.printf("Mensaje WebSocket: %s\n", payload);
                processControlCommand(String((char*)payload));
                break;
                
            default:
                break;
        }
    }
    
    void processControlCommand(String message) {
        DynamicJsonDocument commandDoc(512);
        deserializeJson(commandDoc, message);
        
        command.action = commandDoc["action"].as();
        command.value1 = commandDoc["value1"] | 0.0;
        command.value2 = commandDoc["value2"] | 0.0;
        command.emergency_stop = commandDoc["emergency_stop"] | false;
        
        Serial.println("Comando procesado: " + command.action);
        
        // Enviar confirmación
        DynamicJsonDocument responseDoc(256);
        responseDoc["device_id"] = device_id;
        responseDoc["response"] = "command_received";
        responseDoc["command"] = command.action;
        responseDoc["timestamp"] = millis();
        
        String response;
        serializeJson(responseDoc, response);
        mqttClient.publish(topic_status, response.c_str());
    }
    
private:
    void connectMQTT() {
        while (!mqttClient.connected()) {
            Serial.print("Conectando a MQTT...");
            
            if (mqttClient.connect(device_id)) {
                Serial.println("conectado");
                mqttClient.subscribe(topic_control);
                
                // Enviar mensaje de conexión
                DynamicJsonDocument connectDoc(256);
                connectDoc["device_id"] = device_id;
                connectDoc["status"] = "online";
                connectDoc["timestamp"] = millis();
                connectDoc["ip"] = WiFi.localIP().toString();
                
                String connectMessage;
                serializeJson(connectDoc, connectMessage);
                mqttClient.publish(topic_status, connectMessage.c_str());
                
            } else {
                Serial.print("falló, rc=");
                Serial.print(mqttClient.state());
                Serial.println(" reintentando en 5 segundos");
                delay(5000);
            }
        }
    }
};

Ejercicios Prácticos de Implementación

1

Construcción del Robot Móvil Básico

Básico 60 min Hardware

Objetivo: Construir el chasis básico del robot con sistema de tracción diferencial y encoders ópticos.

Materiales Necesarios:
  • ESP32 DevKit v1
  • 2x Motores DC con encoder (JGA25-371)
  • Driver de motores L298N
  • Chasis acrílico o impreso 3D
  • Ruedas de 65mm de diámetro
  • Batería LiPo 7.4V 2200mAh
  • Regulador de voltaje Buck (7.4V a 5V)
  • Protoboard y cables

Procedimiento:

  1. Montar los motores en el chasis con separación de 15cm
  2. Conectar el driver L298N según el esquema de pines
  3. Implementar el sistema de encoders para odometría
  4. Calibrar las velocidades de ambos motores
  5. Programar movimientos básicos: adelante, atrás, giros
2

Integración Multi-Sensor y Navegación

Intermedio 90 min Sensores

Objetivo: Implementar sistema completo de sensores para navegación autónoma con evasión de obstáculos.

Sensores Adicionales:
  • 4x Sensores ultrasónicos HC-SR04
  • MPU6050 (acelerómetro + giroscopio)
  • Sensor DHT22 (temperatura/humedad)
  • Sensor de voltaje para batería
  • Buzzer para alertas sonoras
  • LED RGB WS2812 para indicadores

Algoritmos a Implementar:

  • Navegación reactiva: Algoritmo de campos potenciales
  • Fusión sensorial: Filtro de Kalman para IMU
  • Mapeo local: Grid de ocupación simple
  • Planificación: Algoritmo A* simplificado
3

Sistema de Telemetría IoT

Avanzado 120 min IoT

Objetivo: Desarrollar sistema completo de telemetría con dashboard web y control remoto bidireccional.

Componentes de Software:
  • Broker MQTT (Mosquitto o HiveMQ)
  • Servidor web Node.js con Express
  • WebSocket para tiempo real
  • Base de datos InfluxDB para métricas
  • Dashboard con Chart.js
  • App móvil con React Native

Características del Dashboard:

  • Mapa de navegación en tiempo real
  • Gráficas de telemetría de sensores
  • Control remoto del robot
  • Alertas y notificaciones
  • Registro histórico de misiones
4

Navegación Autónoma Avanzada

Experto 180 min IA

Objetivo: Implementar algoritmos avanzados de navegación con SLAM (Simultaneous Localization and Mapping).

Algoritmos Avanzados:
  • SLAM con FastSLAM 2.0
  • Control PID para seguimiento de trayectorias
  • Planificación RRT* (Rapidly-exploring Random Tree)
  • Máquina de estados finitos para comportamientos
  • Filtro de partículas para localización

Misiones Autónomas:

  • Exploración: Mapeo autónomo de un área desconocida
  • Patrullaje: Recorrido cíclico con detección de anomalías
  • Seguimiento: Persecución de un objeto en movimiento
  • Inspección: Recolección de datos en puntos específicos

Proyecto Aplicado: Robot de Inspección Industrial Autónomo

Especificaciones del Proyecto Final

Desarrollar un robot móvil autónomo capaz de realizar inspecciones industriales programadas con telemetría avanzada y análisis de datos en tiempo real.

Capacidades Mecatrónicas
  • Navegación autónoma con evasión de obstáculos
  • Mapeo SLAM de instalaciones industriales
  • Recolección de datos multi-sensor
  • Control remoto y supervisión humana
  • Planificación de misiones automatizada
Análisis IoT Avanzado
  • Detección de anomalías con machine learning
  • Predicción de mantenimiento preventivo
  • Generación automática de reportes
  • Alertas inteligentes por criticidad
  • Integración con sistemas ERP/MES

Implementación del Algoritmo SLAM

C++ - SLAM con FastSLAM 2.0
#include <vector>
#include <random>
#include <cmath>

class FastSLAM {
private:
    struct Particle {
        struct Pose {
            float x, y, theta;
        } pose;
        
        std::vector<struct Landmark> landmarks;
        float weight;
        
        struct Landmark {
            float x, y;
            float covariance[2][2];
            int id;
            int observed_count;
        };
    };
    
    std::vector<Particle> particles;
    int num_particles = 100;
    std::random_device rd;
    std::mt19937 gen;
    
public:
    FastSLAM() : gen(rd()) {
        initializeParticles();
    }
    
    void initializeParticles() {
        particles.resize(num_particles);
        for (auto& particle : particles) {
            particle.pose = {0.0f, 0.0f, 0.0f};
            particle.weight = 1.0f / num_particles;
        }
    }
    
    void predict(float linear_vel, float angular_vel, float dt) {
        std::normal_distribution<float> noise_linear(0.0, 0.1);
        std::normal_distribution<float> noise_angular(0.0, 0.05);
        
        for (auto& particle : particles) {
            // Modelo de movimiento con ruido
            float noisy_linear = linear_vel + noise_linear(gen);
            float noisy_angular = angular_vel + noise_angular(gen);
            
            // Actualizar pose de la partícula
            particle.pose.x += noisy_linear * cos(particle.pose.theta) * dt;
            particle.pose.y += noisy_linear * sin(particle.pose.theta) * dt;
            particle.pose.theta += noisy_angular * dt;
            
            // Normalizar ángulo
            while (particle.pose.theta > M_PI) particle.pose.theta -= 2 * M_PI;
            while (particle.pose.theta < -M_PI) particle.pose.theta += 2 * M_PI;
        }
    }
    
    void update(const std::vector<struct Observation>& observations) {
        struct Observation {
            float range, bearing;
            int landmark_id;
        };
        
        for (auto& particle : particles) {
            particle.weight = 1.0f;
            
            for (const auto& obs : observations) {
                updateLandmark(particle, obs);
            }
        }
        
        normalize_weights();
        resample();
    }
    
    void updateLandmark(Particle& particle, const struct Observation& obs) {
        // Buscar landmark existente
        auto landmark_it = std::find_if(particle.landmarks.begin(), 
                                       particle.landmarks.end(),
                                       [&obs](const auto& lm) {
                                           return lm.id == obs.landmark_id;
                                       });
        
        if (landmark_it == particle.landmarks.end()) {
            // Nuevo landmark
            Particle::Landmark new_landmark;
            new_landmark.id = obs.landmark_id;
            
            // Inicializar posición usando observación
            new_landmark.x = particle.pose.x + obs.range * cos(particle.pose.theta + obs.bearing);
            new_landmark.y = particle.pose.y + obs.range * sin(particle.pose.theta + obs.bearing);
            
            // Inicializar covarianza
            float sigma_range = 0.1f;
            float sigma_bearing = 0.05f;
            
            new_landmark.covariance[0][0] = sigma_range * sigma_range;
            new_landmark.covariance[1][1] = sigma_range * sigma_range;
            new_landmark.covariance[0][1] = 0.0f;
            new_landmark.covariance[1][0] = 0.0f;
            
            new_landmark.observed_count = 1;
            
            particle.landmarks.push_back(new_landmark);
            
        } else {
            // Actualizar landmark existente usando Extended Kalman Filter
            ekfUpdate(*landmark_it, particle.pose, obs);
            landmark_it->observed_count++;
        }
    }
    
    void ekfUpdate(Particle::Landmark& landmark, const Particle::Pose& pose, 
                   const struct Observation& obs) {
        // Predicción
        float dx = landmark.x - pose.x;
        float dy = landmark.y - pose.y;
        float predicted_range = sqrt(dx*dx + dy*dy);
        float predicted_bearing = atan2(dy, dx) - pose.theta;
        
        // Normalizar bearing predicho
        while (predicted_bearing > M_PI) predicted_bearing -= 2 * M_PI;
        while (predicted_bearing < -M_PI) predicted_bearing += 2 * M_PI;
        
        // Jacobiano de la observación
        float H[2][2];
        float range_inv = 1.0f / predicted_range;
        
        H[0][0] = dx * range_inv;  // d(range)/dx
        H[0][1] = dy * range_inv;  // d(range)/dy
        H[1][0] = -dy * (range_inv * range_inv);  // d(bearing)/dx
        H[1][1] = dx * (range_inv * range_inv);   // d(bearing)/dy
        
        // Matriz de ruido de observación
        float R[2][2] = {{0.1*0.1, 0}, {0, 0.05*0.05}};
        
        // Ganancia de Kalman
        float S[2][2]; // H * P * H^T + R
        matrixMultiply(H, landmark.covariance, S);
        matrixAdd(S, R, S);
        
        float S_inv[2][2];
        matrixInverse(S, S_inv);
        
        float K[2][2]; // P * H^T * S^(-1)
        float HT[2][2] = {{H[0][0], H[1][0]}, {H[0][1], H[1][1]}}; // H transpose
        matrixMultiply(landmark.covariance, HT, K);
        matrixMultiply(K, S_inv, K);
        
        // Actualización del estado
        float innovation[2] = {
            obs.range - predicted_range,
            obs.bearing - predicted_bearing
        };
        
        // Normalizar innovación de bearing
        while (innovation[1] > M_PI) innovation[1] -= 2 * M_PI;
        while (innovation[1] < -M_PI) innovation[1] += 2 * M_PI;
        
        landmark.x += K[0][0] * innovation[0] + K[0][1] * innovation[1];
        landmark.y += K[1][0] * innovation[0] + K[1][1] * innovation[1];
        
        // Actualización de covarianza: P = (I - K*H) * P
        float I_KH[2][2] = {
            {1 - K[0][0]*H[0][0] - K[0][1]*H[1][0], -K[0][0]*H[0][1] - K[0][1]*H[1][1]},
            {-K[1][0]*H[0][0] - K[1][1]*H[1][0], 1 - K[1][0]*H[0][1] - K[1][1]*H[1][1]}
        };
        
        float new_cov[2][2];
        matrixMultiply(I_KH, landmark.covariance, new_cov);
        
        landmark.covariance[0][0] = new_cov[0][0];
        landmark.covariance[0][1] = new_cov[0][1];
        landmark.covariance[1][0] = new_cov[1][0];
        landmark.covariance[1][1] = new_cov[1][1];
        
        // Actualizar peso de la partícula basado en la innovación
        float det_S = S[0][0] * S[1][1] - S[0][1] * S[1][0];
        float mahalanobis = innovation[0] * (S_inv[0][0] * innovation[0] + S_inv[0][1] * innovation[1]) +
                           innovation[1] * (S_inv[1][0] * innovation[0] + S_inv[1][1] * innovation[1]);
        
        particle.weight *= exp(-0.5 * mahalanobis) / sqrt(2 * M_PI * det_S);
    }
    
    void normalize_weights() {
        float total_weight = 0.0f;
        for (const auto& particle : particles) {
            total_weight += particle.weight;
        }
        
        if (total_weight > 0) {
            for (auto& particle : particles) {
                particle.weight /= total_weight;
            }
        }
    }
    
    void resample() {
        // Implementar remuestreo sistemático
        std::vector<Particle> new_particles;
        new_particles.reserve(num_particles);
        
        std::uniform_real_distribution<float> dist(0.0, 1.0 / num_particles);
        float r = dist(gen);
        float c = particles[0].weight;
        int i = 0;
        
        for (int m = 0; m < num_particles; m++) {
            float u = r + m * (1.0f / num_particles);
            while (u > c) {
                i++;
                c += particles[i].weight;
            }
            new_particles.push_back(particles[i]);
            new_particles.back().weight = 1.0f / num_particles;
        }
        
        particles = std::move(new_particles);
    }
    
    struct MapState {
        struct Pose pose;
        std::vector<struct Landmark> landmarks;
    };
    
    MapState getBestEstimate() {
        // Encontrar partícula con mayor peso
        auto best_it = std::max_element(particles.begin(), particles.end(),
                                       [](const auto& a, const auto& b) {
                                           return a.weight < b.weight;
                                       });
        
        MapState result;
        result.pose = best_it->pose;
        result.landmarks = best_it->landmarks;
        
        return result;
    }
    
private:
    void matrixMultiply(float A[2][2], float B[2][2], float C[2][2]) {
        C[0][0] = A[0][0] * B[0][0] + A[0][1] * B[1][0];
        C[0][1] = A[0][0] * B[0][1] + A[0][1] * B[1][1];
        C[1][0] = A[1][0] * B[0][0] + A[1][1] * B[1][0];
        C[1][1] = A[1][0] * B[0][1] + A[1][1] * B[1][1];
    }
    
    void matrixAdd(float A[2][2], float B[2][2], float C[2][2]) {
        C[0][0] = A[0][0] + B[0][0];
        C[0][1] = A[0][1] + B[0][1];
        C[1][0] = A[1][0] + B[1][0];
        C[1][1] = A[1][1] + B[1][1];
    }
    
    void matrixInverse(float A[2][2], float inv[2][2]) {
        float det = A[0][0] * A[1][1] - A[0][1] * A[1][0];
        if (abs(det) < 1e-6) return; // Matriz singular
        
        inv[0][0] = A[1][1] / det;
        inv[0][1] = -A[0][1] / det;
        inv[1][0] = -A[1][0] / det;
        inv[1][1] = A[0][0] / det;
    }
};

Dashboard Web de Monitoreo

HTML/JavaScript - Dashboard Interactivo
<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Dashboard Robot Industrial IoT</title>
    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/mqtt/4.3.7/mqtt.min.js"></script>
    <style>
        body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; margin: 0; background: #f5f5f5; }
        .dashboard { display: grid; grid-template-columns: 300px 1fr; height: 100vh; }
        .sidebar { background: #2c3e50; color: white; padding: 20px; overflow-y: auto; }
        .main-content { display: grid; grid-template-rows: auto 1fr; }
        .header { background: white; padding: 20px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
        .content { padding: 20px; display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
        .widget { background: white; border-radius: 10px; padding: 20px; box-shadow: 0 4px 15px rgba(0,0,0,0.1); }
        .status-indicator { display: inline-block; width: 12px; height: 12px; border-radius: 50%; margin-right: 8px; }
        .online { background: #27ae60; }
        .offline { background: #e74c3c; }
        .map-container { height: 400px; background: #ecf0f1; border-radius: 8px; position: relative; }
        .robot-position { position: absolute; width: 20px; height: 20px; background: #3498db; border-radius: 50%; }
        .obstacle { position: absolute; width: 15px; height: 15px; background: #e74c3c; border-radius: 3px; }
        .control-panel { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-top: 20px; }
        .control-btn { padding: 15px; border: none; border-radius: 8px; cursor: pointer; font-weight: bold; }
        .control-btn.forward { background: #3498db; color: white; }
        .control-btn.stop { background: #e74c3c; color: white; }
        .control-btn.backward { background: #95a5a6; color: white; }
    </style>
</head>
<body>
    <div class="dashboard">
        <div class="sidebar">
            <h2>🤖 Control Robot</h2>
            <div class="robot-status">
                <p><span id="status-indicator" class="status-indicator offline"></span>
                   Estado: <span id="robot-status">Desconectado</span></p>
                <p>Batería: <span id="battery-level">--</span>%</p>
                <p>Wi-Fi: <span id="wifi-signal">--</span> dBm</p>
                <p>Memoria: <span id="memory-free">--</span> KB</p>
            </div>
            
            <h3>Misiones Activas</h3>
            <div id="active-missions">
                <p>Exploración automática</p>
                <p>Recolección de datos</p>
            </div>
            
            <h3>Alertas</h3>
            <div id="alerts">
                <p style="color: #f39c12;">⚠️ Batería baja</p>
                <p style="color: #27ae60;">✅ Conexión estable</p>
            </div>
        </div>
        
        <div class="main-content">
            <div class="header">
                <h1>Dashboard Robot Industrial IoT</h1>
                <p>Monitoreo en tiempo real - Última actualización: <span id="last-update">--</span></p>
            </div>
            
            <div class="content">
                <div class="widget">
                    <h3>Mapa de Navegación</h3>
                    <div class="map-container" id="navigation-map">
                        <div class="robot-position" id="robot-marker" style="left: 50%; top: 50%;"></div>
                    </div>
                    <div class="control-panel">
                        <button class="control-btn forward" onclick="sendCommand('move', 100, 0)">▲</button>
                        <button class="control-btn" onclick="sendCommand('turn', 0, 50)">◀</button>
                        <button class="control-btn" onclick="sendCommand('turn', 0, -50)">▶</button>
                        <button class="control-btn backward" onclick="sendCommand('move', -100, 0)">▼</button>
                        <button class="control-btn stop" onclick="sendCommand('stop', 0, 0)">⏹</button>
                        <button class="control-btn" onclick="sendCommand('auto', 0, 0)">🤖</button>
                    </div>
                </div>
                
                <div class="widget">
                    <h3>Sensores Ambientales</h3>
                    <canvas id="environmental-chart" width="400" height="200"></canvas>
                </div>
                
                <div class="widget">
                    <h3>Datos IMU (Acelerómetro/Giroscopio)</h3>
                    <canvas id="imu-chart" width="400" height="200"></canvas>
                </div>
                
                <div class="widget">
                    <h3>Distancias Ultrasonicas</h3>
                    <canvas id="distance-chart" width="400" height="200"></canvas>
                </div>
            </div>
        </div>
    </div>

    <script>
        // Configuración MQTT
        const client = mqtt.connect('ws://localhost:8080/mqtt');
        let robotData = {};
        
        // Configurar gráficas
        const envChart = new Chart(document.getElementById('environmental-chart'), {
            type: 'line',
            data: {
                labels: [],
                datasets: [{
                    label: 'Temperatura (°C)',
                    borderColor: '#e74c3c',
                    data: [],
                    fill: false
                }, {
                    label: 'Humedad (%)',
                    borderColor: '#3498db',
                    data: [],
                    fill: false
                }]
            },
            options: {
                responsive: true,
                scales: {
                    x: { type: 'time' },
                    y: { beginAtZero: true }
                }
            }
        });
        
        const imuChart = new Chart(document.getElementById('imu-chart'), {
            type: 'line',
            data: {
                labels: [],
                datasets: [{
                    label: 'Accel X',
                    borderColor: '#e74c3c',
                    data: [],
                    fill: false
                }, {
                    label: 'Accel Y',
                    borderColor: '#27ae60',
                    data: [],
                    fill: false
                }, {
                    label: 'Accel Z',
                    borderColor: '#3498db',
                    data: [],
                    fill: false
                }]
            },
            options: {
                responsive: true,
                scales: {
                    x: { type: 'time' },
                    y: { min: -2, max: 2 }
                }
            }
        });
        
        const distanceChart = new Chart(document.getElementById('distance-chart'), {
            type: 'radar',
            data: {
                labels: ['Frente', 'Derecha', 'Atrás', 'Izquierda'],
                datasets: [{
                    label: 'Distancia (cm)',
                    data: [0, 0, 0, 0],
                    backgroundColor: 'rgba(52, 152, 219, 0.2)',
                    borderColor: '#3498db',
                    pointBackgroundColor: '#3498db'
                }]
            },
            options: {
                responsive: true,
                scale: {
                    ticks: { min: 0, max: 200 }
                }
            }
        });
        
        // Eventos MQTT
        client.on('connect', () => {
            console.log('Conectado al broker MQTT');
            client.subscribe('robot/telemetry');
            client.subscribe('robot/status');
        });
        
        client.on('message', (topic, message) => {
            const data = JSON.parse(message.toString());
            
            if (topic === 'robot/telemetry') {
                updateDashboard(data);
            } else if (topic === 'robot/status') {
                updateStatus(data);
            }
        });
        
        function updateDashboard(data) {
            robotData = data;
            
            // Actualizar indicadores de estado
            document.getElementById('last-update').textContent = new Date().toLocaleTimeString();
            
            if (data.status) {
                document.getElementById('battery-level').textContent = 
                    Math.round(data.status.battery_voltage / 8.4 * 100);
                document.getElementById('wifi-signal').textContent = data.status.wifi_rssi;
                document.getElementById('memory-free').textContent = 
                    Math.round(data.status.free_heap / 1024);
            }
            
            // Actualizar posición del robot
            if (data.position) {
                const robot = document.getElementById('robot-marker');
                robot.style.left = (50 + data.position.x * 10) + '%';
                robot.style.top = (50 - data.position.y * 10) + '%';
                robot.style.transform = `rotate(${data.position.theta * 180 / Math.PI}deg)`;
            }
            
            // Actualizar gráficas
            const now = new Date();
            
            if (data.sensors) {
                // Gráfica ambiental
                envChart.data.labels.push(now);
                envChart.data.datasets[0].data.push(data.sensors.temperature);
                envChart.data.datasets[1].data.push(data.sensors.humidity);
                
                // Limitar a últimos 50 puntos
                if (envChart.data.labels.length > 50) {
                    envChart.data.labels.shift();
                    envChart.data.datasets[0].data.shift();
                    envChart.data.datasets[1].data.shift();
                }
                envChart.update();
                
                // Gráfica IMU
                if (data.sensors.imu) {
                    imuChart.data.labels.push(now);
                    imuChart.data.datasets[0].data.push(data.sensors.imu.accel_x);
                    imuChart.data.datasets[1].data.push(data.sensors.imu.accel_y);
                    imuChart.data.datasets[2].data.push(data.sensors.imu.accel_z);
                    
                    if (imuChart.data.labels.length > 50) {
                        imuChart.data.labels.shift();
                        imuChart.data.datasets.forEach(dataset => dataset.data.shift());
                    }
                    imuChart.update();
                }
                
                // Gráfica de distancias
                if (data.sensors.distances) {
                    distanceChart.data.datasets[0].data = data.sensors.distances;
                    distanceChart.update();
                }
                
                // Actualizar obstáculos en el mapa
                updateObstacles(data.sensors.distances);
            }
        }
        
        function updateObstacles(distances) {
            const map = document.getElementById('navigation-map');
            
            // Limpiar obstáculos previos
            const oldObstacles = map.querySelectorAll('.obstacle');
            oldObstacles.forEach(obs => obs.remove());
            
            // Agregar nuevos obstáculos
            distances.forEach((distance, index) => {
                if (distance < 50) { // Obstáculo detectado
                    const obstacle = document.createElement('div');
                    obstacle.className = 'obstacle';
                    
                    let x = 50, y = 50;
                    switch(index) {
                        case 0: y = 50 - distance; break; // Frente
                        case 1: x = 50 + distance; break; // Derecha
                        case 2: y = 50 + distance; break; // Atrás
                        case 3: x = 50 - distance; break; // Izquierda
                    }
                    
                    obstacle.style.left = x + '%';
                    obstacle.style.top = y + '%';
                    map.appendChild(obstacle);
                }
            });
        }
        
        function updateStatus(data) {
            const statusIndicator = document.getElementById('status-indicator');
            const robotStatus = document.getElementById('robot-status');
            
            if (data.status === 'online') {
                statusIndicator.className = 'status-indicator online';
                robotStatus.textContent = 'Conectado';
            } else {
                statusIndicator.className = 'status-indicator offline';
                robotStatus.textContent = 'Desconectado';
            }
        }
        
        function sendCommand(action, value1 = 0, value2 = 0) {
            const command = {
                action: action,
                value1: value1,
                value2: value2,
                timestamp: Date.now()
            };
            
            client.publish('robot/control', JSON.stringify(command));
            console.log('Comando enviado:', command);
        }
        
        // Simular datos para demostración
        setInterval(() => {
            const mockData = {
                device_id: "ESP32_Robot_001",
                timestamp: Date.now(),
                position: {
                    x: Math.sin(Date.now() / 5000) * 5,
                    y: Math.cos(Date.now() / 5000) * 3,
                    theta: Date.now() / 1000 % (2 * Math.PI)
                },
                sensors: {
                    temperature: 25 + Math.random() * 10,
                    humidity: 60 + Math.random() * 20,
                    distances: [
                        50 + Math.random() * 150,
                        30 + Math.random() * 170,
                        40 + Math.random() * 160,
                        60 + Math.random() * 140
                    ],
                    imu: {
                        accel_x: Math.random() * 0.4 - 0.2,
                        accel_y: Math.random() * 0.4 - 0.2,
                        accel_z: 1 + Math.random() * 0.2 - 0.1
                    }
                },
                status: {
                    battery_voltage: 7.2 + Math.random() * 1.2,
                    wifi_rssi: -60 - Math.random() * 20,
                    free_heap: 150000 + Math.random() * 50000
                }
            };
            
            updateDashboard(mockData);
        }, 1000);
    </script>
</body>
</html>

Troubleshooting y Evaluación del Sistema

Problemas Comunes y Soluciones

🔧 Hardware
  • Motores no responden: Verificar conexiones del driver L298N y alimentación
  • Sensores ultrasónicos erróneos: Revisar interferencias y tiempos de eco
  • IMU deriva constantemente: Recalibrar sensores y verificar filtros
  • Batería se agota rápido: Optimizar ciclos de trabajo y usar modo deep sleep
📡 Comunicaciones
  • Pérdida de conexión Wi-Fi: Implementar reconexión automática
  • MQTT desconecta frecuentemente: Ajustar keep-alive y QoS
  • Latencia alta en dashboard: Optimizar frecuencia de telemetría
  • WebSocket no funciona: Verificar firewall y proxy
🤖 Software
  • Robot se mueve en círculos: Calibrar velocidades de motores
  • SLAM diverge: Aumentar número de partículas
  • Memoria insuficiente: Optimizar estructuras de datos
  • Watchdog reset: Reducir tiempo de procesamiento en loop principal

Criterios de Evaluación

🔩 Implementación Mecatrónica (25%)
  • Diseño y construcción del chasis
  • Integración de sensores y actuadores
  • Calibración y puesta en marcha
  • Robustez del sistema mecánico
💻 Programación y Algoritmos (30%)
  • Calidad del código C++ en ESP32
  • Implementación de algoritmos de navegación
  • Sistema multitarea con FreeRTOS
  • Manejo de errores y robustez
🌐 Conectividad IoT (25%)
  • Configuración de protocolos de comunicación
  • Dashboard web funcional
  • Sistema de telemetría en tiempo real
  • Seguridad en comunicaciones
🎯 Funcionalidad y Performance (20%)
  • Navegación autónoma efectiva
  • Precisión de sensores y actuadores
  • Tiempo de respuesta del sistema
  • Confiabilidad y estabilidad

Proyectos de Extensión Avanzados

Para estudiantes que deseen profundizar en el proyecto, se proponen las siguientes extensiones:

Inteligencia Artificial
  • Implementar redes neuronales para clasificación de objetos
  • Algoritmos de aprendizaje por refuerzo para optimización de rutas
  • Procesamiento de imágenes con OpenCV
Cloud Computing
  • Integración con AWS IoT Core
  • Analytics avanzado con AWS Lambda
  • Machine Learning en la nube
Sistemas Multi-Robot
  • Coordinación de múltiples robots
  • Algoritmos de consenso distribuido
  • Exploración colaborativa

Referencias y Recursos Adicionales

📚 Libros Especializados
  • "Robotics: Modelling, Planning and Control" - Siciliano et al.
  • "Probabilistic Robotics" - Thrun, Burgard, Fox
  • "Internet of Things: A Hands-On Approach" - Bahga & Madisetti
  • "ESP32 Programming and IoT Projects" - Dogan Ibrahim
🔬 Papers de Investigación
  • "FastSLAM 2.0: An Improved Particle Filtering Algorithm"
  • "RRT*: Asymptotically Optimal Path Planning"
  • "IoT-Based Robotic Systems for Industrial Applications"
💻 Herramientas y Frameworks
  • ROS2: Robot Operating System para desarrollo avanzado
  • Gazebo: Simulador 3D para robótica
  • Node-RED: Herramienta visual para IoT
  • InfluxDB + Grafana: Base de datos temporal y visualización
🌐 Recursos Online