Robot seguidor de líneas con IA en Arduino Nano
Componentes y materiales
| Componente | Cantidad |
|---|---|
| Arduino Nano | 1 |
| Transceptor Bluetooth RF Maestro/Esclavo (Seeed) | 1 |
| Módulo sensor IR reflectivo fotoeléctrico (Amazon Web Services) | 1 |
| Servo rotación continua (Sony) | 1 |
| Batería 4.8V (Android) | 1 |
Aplicaciones y servicios en línea
- Arduino IDE
Sobre el proyecto
Tras desarrollar un robot Pick & Place con Arduino, la idea era crear un robot seguidor de líneas basado en Inteligencia Artificial (IA). Un seguidor de líneas simple es bueno para principiantes; solo hacía falta añadir sensores infrarrojos en lugar de servos en mi proyecto anterior. "La idea era poner a prueba mis habilidades a nivel avanzado."
Aunque fue una tarea difícil, la investigación online me ayudó mucho.
¿Por qué un robot seguidor de líneas con IA?
La IA es tecnología reciente con potencial para cambiar el mundo y mejorar la vida humana. El sueño de dotar a robots de inteligencia para entender y resolver problemas reales solo puede hacerse realidad mediante Inteligencia Artificial; por eso todos hablan de ella.
- Uso en líneas de montaje/producción.
- Autobuses de recogida de pasajeros.
Mi prototipo tiene algo de inteligencia; es solo el inicio de mi camino hacia la IA. Explicaré paso a paso el desarrollo en lenguaje sencillo. Por falta de recursos no construiré el robot físico, pero explicaré cómo hacerlo investigando. Proporcionaré códigos precisos que funcionen en Arduino IDE para que desarrolles tu primer robot IA fácilmente.
Apóyame en Patreon: https://bit.ly/31NfQ6A
"Encuentras los componentes más baratos en EE. UU. en la descripción del vídeo, solo haz clic en el vídeo."
Uso de sensores infrarrojos
El proyecto usa 7 sensores IR como muestra el diagrama:
- 5 para control PID.
- 1 para detección izquierda.
- 1 para detección derecha.
Rol de los 5 sensores PID: generan salida digital HIGH/LOW (1/0). Solo el sensor centrado sobre la línea negra da HIGH (1). Posibles patrones:
10000 11000 01000 01100 00100 00110 00010 00011 00001 00000 11111
Sensores laterales (izq/der): salida analógica HIGH/LOW.
Array para 5 sensores: int LFSensor[5]={1,1,1,1,1};
Variables para laterales: int farleft_sensor=0;
Lectura:
LFSensor[0] = digitalRead(lineFollowSensor0);
LFSensor[1] = digitalRead(lineFollowSensor1);
LFSensor[2] = digitalRead(lineFollowSensor2);
LFSensor[3] = digitalRead(lineFollowSensor3);
LFSensor[4] = digitalRead(lineFollowSensor4);
farRightSensor = analogRead(farRightSensorPin);
farLeftSensor = analogRead(farLeftSensorPin);
Regla de la mano izquierda en laberintos
Mira este vídeo para entender la regla de la mano izquierda en laberintos.
En resumen: coloca tu mano izquierda en la pared, avanza, mantén la mano tocando la pared izquierda en cada intersección. Llegarás al final (no necesariamente por el camino más corto).
Clave: identificar intersecciones y decidir según la regla. En laberinto 2D hay 8 tipos de intersecciones (ver imagen). Acciones posibles:
- Cruce (+): Izquierda, Derecha, Recto.
- T: Izquierda, Derecha.
- Solo Derecha: Derecha.
- Solo Izquierda: Izquierda.
- Recto o Izquierda: Izquierda, Recto.
- Recto o Derecha: Derecha, Recto.
- Callejón sin salida: Vuelta 180°.
- Fin de laberinto: Parar.
Aplicando la regla de la mano izquierda, las acciones se reducen a una por intersección:
- Cruce → Izquierda
- T → Izquierda
- Solo Derecha → Derecha
- Solo Izquierda → Izquierda
- Recto o Izquierda → Izquierda
- Recto o Derecha → Recto
- Callejón sin salida → Vuelta 180°
- Fin de laberinto → Parar
Identificar "Callejón sin salida" y "Fin de laberinto" es fácil (no hay ambigüedad). El problema surge con "LÍNEA" (puede ser Cruce o T) y "GIRO IZQ/DER" (puede ser giro simple o opción de recto). Para saber exactamente el tipo, el robot debe avanzar un "paso extra" y ver qué hay después (ver imagen 2).
Flujo de acciones:
- Callejón sin salida: Vuelta 180°.
- LÍNEA: Avanza paso extra → si hay línea = Cruce → Izquierda; si no hay = T → Izquierda; si hay otra línea = Fin → Parar.
- GIRO DERECHA: Avanza paso extra → si hay línea = Recto o Derecha → Recto; si no = Solo Derecha → Derecha.
- GIRO IZQUIERDA: Avanza paso extra → si hay línea = Recto o Izquierda → Izquierda; si no = Solo Izquierda → Izquierda. (En realidad, en giro izquierda siempre vas a la izquierda, así que el test se puede omitir; lo dejo por claridad).
Posibles problemas
Tema relacionado: Resolución de problemas de driver Arduino (ver vídeo).
Código
El código está modularizado en varios archivos:
- RoBot_Maze_Solve_2 (principal)
- Functions
- GeneralFunctions
- sensorFunctions
- RobotDefines.h
- MotorFunctions
RoBot_Maze_Solve_2 (C/C++)
/*------------------------------------------------------------------
7 sensors Smart Robot - Maze Solver and Line Follower
with programmable PID controller via BT
=> Basic movement based on Nano Mouse Robot, Michael Backus (https://www.akrobotnerd.com/)
=> Line follow based on https://samvrit.tk/tutorials/pid-control-arduino-line-follower-robot/
=> CREDIT to Patrick McCabe for path Solving Code, visit patrickmccabemakes.com!!
Marcelo Jose Rovai - 23 April, 2016 - Visit: https://mjrobot.org
------------------------------------------------------------------*/
#include <Servo.h>
#include "robotDefines.h"
String command, device;
#include <SoftwareSerial.h>
SoftwareSerial BT1(10, 11); // Rx=10, Tx=11
void setup() {
Serial.begin(9600); BT1.begin(9600);
pinMode(ledPin, OUTPUT);
pinMode(buttonPin, INPUT_PULLUP);
pinMode(lineFollowSensor0, INPUT);
pinMode(lineFollowSensor1, INPUT);
pinMode(lineFollowSensor2, INPUT);
pinMode(lineFollowSensor3, INPUT);
pinMode(lineFollowSensor4, INPUT);
leftServo.attach(5); rightServo.attach(3);
BT1.print("check the PID constants to be sent to Robot");
Serial.print("check the PID constants to be sent to Robot");
BT1.println('\n');
while (digitalRead(buttonPin) && !mode) {
checkBTcmd(); manualCmd(); command = "";
}
checkPIDvalues();
mode = STOPPED; status = 0; // 1st pass
}
void loop() {
ledBlink(1);
BT1.println("Start First Pass"); Serial.println("Start First Pass");
readLFSsensors(); mazeSolve(); // Primera pasada: resolver laberinto
ledBlink(2);
BT1.println("End First Pass"); Serial.println("End First Pass");
while (digitalRead(buttonPin) && !mode) {
checkBTcmd(); manualCmd(); command = "";
}
BT1.println("Starting 2nd Pass"); Serial.println("Starting 2nd Pass");
pathIndex = 0; status = 0;
mazeOptimization(); // Segunda pasada: recorrer óptimo
ledBlink(3);
BT1.println("End 2nd Pass"); Serial.println("End 2nd Pass");
while (digitalRead(buttonPin) && !mode) {
checkBTcmd(); manualCmd(); command = "";
}
mode = STOPPED; status = 0; pathIndex = 0; pathLength = 0;
}
Functions (C/C++)
void mazeSolve(void) {
while (!status) {
readLFSsensors();
switch (mode) {
case NO_LINE: motorStop(); goAndTurn(LEFT, 180); recIntersection('B'); break;
case CONT_LINE:
runExtraInch(); readLFSsensors();
if (mode != CONT_LINE) { goAndTurn(LEFT, 90); recIntersection('L'); }
else mazeEnd();
break;
case RIGHT_TURN:
runExtraInch(); readLFSsensors();
if (mode == NO_LINE) { goAndTurn(RIGHT, 90); recIntersection('R'); }
else recIntersection('S');
break;
case LEFT_TURN:
goAndTurn(LEFT, 90); recIntersection('L'); break;
case FOLLOWING_LINE:
followingLine(); break;
}
}
}
void recIntersection(char direction) {
path[pathLength] = direction; pathLength++; simplifyPath();
}
void mazeEnd(void) {
motorStop();
BT1.print("The End ==> Path: "); for(int i=0;i "); Serial.println(pathLength);
status = 1; mode = STOPPED;
}
void followingLine(void) { calculatePID(); motorPIDcontrol(); }
void simplifyPath() {
if (pathLength < 3 || path[pathLength-2] != 'B') return;
int totalAngle = 0;
for (int i=1; i<=3; i++) {
switch(path[pathLength-i]) {
case 'R': totalAngle += 90; break;
case 'L': totalAngle += 270; break;
case 'B': totalAngle += 180; break;
}
}
totalAngle %= 360;
switch(totalAngle) {
case 0: path[pathLength-3] = 'S'; break;
case 90: path[pathLength-3] = 'R'; break;
case 180: path[pathLength-3] = 'B'; break;
case 270: path[pathLength-3] = 'L'; break;
}
pathLength -= 2;
}
void mazeOptimization(void) {
while (!status) {
readLFSsensors();
switch (mode) {
case FOLLOWING_LINE: followingLine(); break;
case CONT_LINE:
case LEFT_TURN:
case RIGHT_TURN:
if (pathIndex >= pathLength) mazeEnd();
else { mazeTurn(path[pathIndex]); pathIndex++; }
break;
}
}
}
void mazeTurn(char dir) {
switch(dir) {
case 'L': goAndTurn(LEFT, 90); break;
case 'R': goAndTurn(RIGHT, 90); break;
case 'B': goAndTurn(RIGHT, 800); break;
case 'S': runExtraInch(); break;
}
}
GeneralFunctions (C/C++)
void ledBlink(int times) { for(int i=0;i 0) {
Serial.print("Command received from BT ==> "); Serial.println(device);
command = device; device = ""; BT1.flush();
}
}
void manualCmd() {
switch (command[0]) {
case 'g': mode = FOLLOWING_LINE; break;
case 's': motorStop(); break;
case 'f': motorForward(); break;
case 'r': motorTurn(RIGHT, 30); motorStop(); break;
case 'l': motorTurn(LEFT, 30); motorStop(); break;
case 'b': motorBackward(); break;
case 'p': Kp = command[2]; break;
case 'i': Ki = command[2]; break;
case 'd': Kd = command[2]; break;
}
}
void sendBTdata(int data) {
digitalWrite(ledPin, HIGH);
BT1.print("Data from Arduino"); BT1.print(data); BT1.print(" xx"); BT1.println('\n');
digitalWrite(ledPin, LOW);
}
void calculatePID() {
P = error; I = I + error; D = error - previousError;
PIDvalue = (Kp*P) + (Ki*I) + (Kd*D);
previousError = error;
}
void checkPIDvalues() {
BT1.print("PID: "); BT1.print(Kp); BT1.print(" - "); BT1.print(Ki); BT1.print(" - "); BT1.println(Kd);
Serial.print("PID: "); Serial.print(Kp); Serial.print(" - "); Serial.print(Ki); Serial.print(" - "); Serial.println(Kd);
}
void testLineFollowSensors() {
int LFS0 = digitalRead(lineFollowSensor0), LFS1 = digitalRead(lineFollowSensor1);
int LFS2 = digitalRead(lineFollowSensor2), LFS3 = digitalRead(lineFollowSensor3);
int LFS4 = digitalRead(lineFollowSensor4);
Serial.print("LFS: L 0 1 2 3 4 R ==> "); Serial.print(LFS0); Serial.print(" "); Serial.print(LFS1); Serial.print(" ");
Serial.print(LFS2); Serial.print(" "); Serial.print(LFS3); Serial.print(" "); Serial.print(LFS4);
Serial.print(" ==> P: "); Serial.print(P); Serial.print(" I: "); Serial.print(I); Serial.print(" D: "); Serial.print(D);
Serial.print(" PID: "); Serial.println(PIDvalue);
}
sensorFunctions (C/C++)
void readLFSsensors() {
LFSensor[0] = digitalRead(lineFollowSensor0);
LFSensor[1] = digitalRead(lineFollowSensor1);
LFSensor[2] = digitalRead(lineFollowSensor2);
LFSensor[3] = digitalRead(lineFollowSensor3);
LFSensor[4] = digitalRead(lineFollowSensor4);
farRightSensor = analogRead(farRightSensorPin);
farLeftSensor = analogRead(farLeftSensorPin);
if ((LFSensor[0]==1)&&(LFSensor[1]==1)&&(LFSensor[2]==1)&&(LFSensor[3]==1)&&(LFSensor[4]==1)) { mode = CONT_LINE; error = 0; }
else if((LFSensor[0]==0) && (farRightSensor < THRESHOLD)) { mode = RIGHT_TURN; error = 0; }
else if((farLeftSensor < THRESHOLD) && (LFSensor[4]==0)) { mode = LEFT_TURN; error = 0; }
else if((LFSensor[0]==0)&&(LFSensor[1]==0)&&(LFSensor[2]==0)&&(LFSensor[3]==0)&&(LFSensor[4]==0)) { mode = NO_LINE; error = 0; }
else if((LFSensor[0]==0)&&(LFSensor[1]==0)&&(LFSensor[2]==0)&&(LFSensor[3]==0)&&(LFSensor[4]==1)) { mode = FOLLOWING_LINE; error = 4; }
else if((LFSensor[0]==0)&&(LFSensor[1]==0)&&(LFSensor[2]==0)&&(LFSensor[3]==1)&&(LFSensor[4]==1)) { mode = FOLLOWING_LINE; error = 3; }
else if((LFSensor[0]==0)&&(LFSensor[1]==0)&&(LFSensor[2]==0)&&(LFSensor[3]==1)&&(LFSensor[4]==0)) { mode = FOLLOWING_LINE; error = 2; }
else if((LFSensor[0]==0)&&(LFSensor[1]==0)&&(LFSensor[2]==1)&&(LFSensor[3]==1)&&(LFSensor[4]==0)) { mode = FOLLOWING_LINE; error = 1; }
else if((LFSensor[0]==0)&&(LFSensor[1]==0)&&(LFSensor[2]==1)&&(LFSensor[3]==0)&&(LFSensor[4]==0)) { mode = FOLLOWING_LINE; error = 0; }
else if((LFSensor[0]==0)&&(LFSensor[1]==1)&&(LFSensor[2]==1)&&(LFSensor[3]==0)&&(LFSensor[4]==0)) { mode = FOLLOWING_LINE; error = -1; }
else if((LFSensor[0]==0)&&(LFSensor[1]==1)&&(LFSensor[2]==0)&&(LFSensor[3]==0)&&(LFSensor[4]==0)) { mode = FOLLOWING_LINE; error = -2; }
else if((LFSensor[0]==1)&&(LFSensor[1]==1)&&(LFSensor[2]==0)&&(LFSensor[3]==0)&&(LFSensor[4]==0)) { mode = FOLLOWING_LINE; error = -3; }
else if((LFSensor[0]==1)&&(LFSensor[1]==0)&&(LFSensor[2]==0)&&(LFSensor[3]==0)&&(LFSensor[4]==0)) { mode = FOLLOWING_LINE; error = -4; }
Serial.print(farLeftSensor); Serial.print(" <== LEFT RIGH==> "); Serial.print(farRightSensor);
Serial.print(" mode: "); Serial.print(mode); Serial.print(" error:"); Serial.println(error);
}
void readLateralSensors(void) { }
RobotDefines.h (C/C++)
int mode = 0;
#define STOPPED 0
#define FOLLOWING_LINE 1
#define NO_LINE 2
#define CONT_LINE 3
#define POS_LINE 4
#define RIGHT_TURN 5
#define LEFT_TURN 6
const int power = 250;
const int iniMotorPower = 250;
const int adj = 0;
float adjTurn = 8;
int extraInch = 200;
int adjGoAndTurn = 800;
const int ledPin = 13;
const int buttonPin = 9;
const int lineFollowSensor0 = 12;
const int lineFollowSensor1 = 18;
const int lineFollowSensor2 = 17;
const int lineFollowSensor3 = 16;
const int lineFollowSensor4 = 19;
const int farRightSensorPin = 0; // A0
const int farLeftSensorPin = 1; // A1
const int THRESHOLD = 150;
int farRightSensor = 0, farLeftSensor = 0;
int LFSensor[5] = {0,0,0,0,0};
float Kp=50, Ki=0, Kd=0;
float error=0, P=0, I=0, D=0, PIDvalue=0;
float previousError=0, previousI=0;
#define RIGHT 1
#define LEFT -1
Servo leftServo, rightServo;
unsigned char dir;
char path[100] = "";
unsigned char pathLength = 0;
int pathIndex = 0;
unsigned int status = 0; // 0=resolviendo, 1=fin alcanzado
MotorFunctions (C/C++)
void motorStop() {
leftServo.writeMicroseconds(1500); rightServo.writeMicroseconds(1500); delay(200);
}
void motorForward() {
leftServo.writeMicroseconds(1500 - (power+adj)); rightServo.writeMicroseconds(1500 + power);
}
void motorBackward() {
leftServo.writeMicroseconds(1500 + power); rightServo.writeMicroseconds(1500 - power);
}
void motorFwTime(unsigned int time) { motorForward(); delay(time); motorStop(); }
void motorBwTime(unsigned int time) { motorBackward(); delay(time); motorStop(); }
void motorTurn(int direction, int degrees) {
leftServo.writeMicroseconds(1500 - (iniMotorPower+adj)*direction);
rightServo.writeMicroseconds(1500 - iniMotorPower*direction);
delay(round(adjTurn*degrees+20)); motorStop();
}
void motorPIDcontrol() {
int leftMotorSpeed = 1500 - (iniMotorPower+adj) - PIDvalue;
int rightMotorSpeed = 1500 + iniMotorPower - PIDvalue;
constrain(leftMotorSpeed, 1000, 2000);
constrain(rightMotorSpeed, 1000, 2000);
leftServo.writeMicroseconds(leftMotorSpeed);
rightServo.writeMicroseconds(rightMotorSpeed);
}
void runExtraInch(void) { motorPIDcontrol(); delay(extraInch); motorStop(); }
void goAndTurn(int direction, int degrees) { motorPIDcontrol(); delay(adjGoAndTurn); motorTurn(direction, degrees); }
Esquemático
Diagrama de conexiones (imagen referenciada).
Proceso de manufactura
- Construye un robot que evita paredes con Raspberry Pi – Guía paso a paso
- Robot móvil que evita obstáculos basado en visión en Raspberry Pi
- Robot calígrafo de Star Automation brilla en NPE2018
- Wittmann Battenfeld lanza el WX153: Línea avanzada de robots para automatización de inyección de alta fuerza
- Bosch presenta solución de fabricación impulsada por IA en Hannover Messe
- FANUC amplía su línea de robots SCARA: seis modelos, cargas de 3‑20 kg, alcances de 400‑1100 mm
- Robótica Impulsada por IA: Máquinas Inteligentes de Vanguardia
- La IA permite a robots identificar más de 90 objetos mediante el tacto
- IA: Beneficios, riesgos e impacto en la industria
- Big Data vs IA: La sinergia detrás de la transformación digital
- Inteligencia artificial en retail: beneficios probados, no una moda pasajera