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
#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
#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
#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
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:
- Montar los motores en el chasis con separación de 15cm
- Conectar el driver L298N según el esquema de pines
- Implementar el sistema de encoders para odometría
- Calibrar las velocidades de ambos motores
- Programar movimientos básicos: adelante, atrás, giros
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
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
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
#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
<!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