Un tutorial introductorio de programación de robots

Publicado: 2022-03-11
Nota del editor: el 16 de octubre de 2018, este artículo se revisó para que funcione con las últimas tecnologías.

Seamos realistas, los robots son geniales. También van a dirigir el mundo algún día y, con suerte, en ese momento se apiadarán de sus pobres y carnosos creadores (también conocidos como desarrolladores de robótica) y nos ayudarán a construir una utopía espacial llena de abundancia. Estoy bromeando, por supuesto, pero solo un poco.

En mi ambición de tener una pequeña influencia sobre el tema, tomé un curso de teoría de control de robots autónomos el año pasado, que culminó con la construcción de un simulador robótico basado en Python que me permitió practicar la teoría de control en un robot simple, móvil y programable. .

En este artículo, mostraré cómo usar un marco de trabajo de Python para desarrollar software de control, describiré el esquema de control que desarrollé para mi robot simulado, ilustraré cómo interactúa con su entorno y logra sus objetivos, y discutiré algunos de los desafíos fundamentales de la programación de robótica que encontré en el camino.

Para poder seguir este tutorial sobre programación de robótica para principiantes, debes tener un conocimiento básico de dos cosas:

  • Matemáticas: usaremos algunas funciones trigonométricas y vectores
  • Python, dado que Python se encuentra entre los lenguajes de programación de robots básicos más populares, haremos uso de las bibliotecas y funciones básicas de Python.

Los fragmentos de código que se muestran aquí son solo una parte del simulador completo, que se basa en clases e interfaces, por lo que para leer el código directamente, es posible que necesite algo de experiencia en Python y programación orientada a objetos.

Finalmente, los temas opcionales que lo ayudarán a seguir mejor este tutorial son saber qué es una máquina de estado y cómo funcionan los sensores y codificadores de rango.

El desafío del robot programable: percepción versus realidad y la fragilidad del control

El desafío fundamental de toda robótica es este: es imposible conocer el verdadero estado del medio ambiente. El software de control de robots solo puede adivinar el estado del mundo real en función de las mediciones devueltas por sus sensores. Solo puede intentar cambiar el estado del mundo real a través de la generación de señales de control.

Este gráfico demuestra la interacción entre un robot físico y los controles de la computadora cuando se practica la programación de robots Python.

El software de control de robots solo puede adivinar el estado del mundo real en función de las mediciones devueltas por sus sensores.

Por lo tanto, uno de los primeros pasos en el diseño de control es generar una abstracción del mundo real, conocida como modelo , con la cual interpretar las lecturas de nuestros sensores y tomar decisiones. Siempre que el mundo real se comporte de acuerdo con los supuestos del modelo, podemos hacer buenas conjeturas y ejercer el control. Sin embargo, tan pronto como el mundo real se desvíe de estas suposiciones, ya no podremos hacer buenas conjeturas y se perderá el control. A menudo, una vez que se pierde el control, nunca se puede recuperar. (A menos que alguna fuerza externa benévola lo restaure).

Esta es una de las razones clave por las que la programación robótica es tan difícil. A menudo vemos videos del último robot de investigación en el laboratorio, realizando fantásticas hazañas de destreza, navegación o trabajo en equipo, y estamos tentados a preguntar: "¿Por qué no se usa esto en el mundo real?" Bueno, la próxima vez que vea un video así, observe cuán altamente controlado está el entorno del laboratorio. En la mayoría de los casos, estos robots solo pueden realizar estas impresionantes tareas siempre que las condiciones ambientales permanezcan dentro de los estrechos límites de su modelo interno. Por lo tanto, una clave para el avance de la robótica es el desarrollo de modelos más complejos, flexibles y robustos, y dicho avance está sujeto a los límites de los recursos computacionales disponibles.

Una clave para el avance de la robótica es el desarrollo de modelos más complejos, flexibles y robustos.

[Nota al margen: tanto los filósofos como los psicólogos notarían que los seres vivos también sufren de dependencia de su propia percepción interna de lo que les dicen sus sentidos. Muchos avances en robótica provienen de observar criaturas vivas y ver cómo reaccionan ante estímulos inesperados. Piénsalo. ¿Cuál es tu modelo interno del mundo? ¿Es diferente al de una hormiga, y al de un pez? (Con suerte.) Sin embargo, como la hormiga y el pez, es probable que simplifique en exceso algunas realidades del mundo. Cuando sus suposiciones sobre el mundo no son correctas, puede correr el riesgo de perder el control de las cosas. A veces llamamos a esto “peligro”. De la misma manera que nuestro pequeño robot lucha por sobrevivir contra el universo desconocido, todos lo hacemos. Esta es una idea poderosa para los robóticos.]

El simulador de robot programable

El simulador que construí está escrito en Python y muy inteligentemente llamado Sobot Rimulator . Puede encontrar v1.0.0 en GitHub. No tiene muchas campanas y silbatos, pero está diseñado para hacer una cosa muy bien: proporcionar una simulación precisa de un robot móvil y brindar a un aspirante a roboticista un marco simple para practicar la programación de software de robots. Si bien siempre es mejor tener un robot real para jugar, un buen simulador de robot de Python es mucho más accesible y es un excelente lugar para comenzar.

En los robots del mundo real, se requiere que el software que genera las señales de control (el "controlador") funcione a una velocidad muy alta y realice cálculos complejos. Esto afecta la elección de qué lenguajes de programación de robots son mejores para usar: por lo general, C ++ se usa para este tipo de escenarios, pero en aplicaciones de robótica más simples, Python es un muy buen compromiso entre la velocidad de ejecución y la facilidad de desarrollo y prueba.

El software que escribí simula un robot de investigación de la vida real llamado Khepera, pero se puede adaptar a una gama de robots móviles con diferentes dimensiones y sensores. Dado que traté de programar el simulador lo más similar posible a las capacidades del robot real, la lógica de control se puede cargar en un robot Khepera real con una refactorización mínima y funcionará igual que el robot simulado. Las características específicas implementadas se refieren al Khepera III, pero se pueden adaptar fácilmente al nuevo Khepera IV.

En otras palabras, programar un robot simulado es similar a programar un robot real. Esto es crítico si el simulador va a ser de alguna utilidad para desarrollar y evaluar diferentes enfoques de software de control.

En este tutorial, describiré la arquitectura del software de control de robots que viene con la versión 1.0.0 de Sobot Rimulator y proporcionaré fragmentos de la fuente de Python (con ligeras modificaciones para mayor claridad). Sin embargo, te animo a sumergirte en la fuente y perder el tiempo. El simulador se ha bifurcado y utilizado para controlar diferentes robots móviles, incluido un Roomba2 de iRobot. Del mismo modo, siéntase libre de bifurcar el proyecto y mejorarlo.

La lógica de control del robot está restringida a estas clases/archivos de Python:

  • models/supervisor.py esta clase es responsable de la interacción entre el mundo simulado que rodea al robot y el propio robot. Evoluciona nuestra máquina de estado de robot y activa los controladores para calcular el comportamiento deseado.
  • models/supervisor_state_machine.py esta clase representa los diferentes estados en los que puede estar el robot, según su interpretación de los sensores.
  • Los archivos en el directorio de models/controllers : estas clases implementan diferentes comportamientos del robot dado un estado conocido del entorno. En particular, se selecciona un controlador específico dependiendo de la máquina de estado.

La meta

Los robots, como las personas, necesitan un propósito en la vida. El objetivo de nuestro software para controlar este robot será muy simple: intentará llegar a un punto de destino predeterminado. Esta suele ser la característica básica que debe tener cualquier robot móvil, desde coches autónomos hasta robots aspiradores. Las coordenadas del objetivo se programan en el software de control antes de que se active el robot, pero podrían generarse desde una aplicación Python adicional que supervisa los movimientos del robot. Por ejemplo, imagínelo conduciendo a través de múltiples puntos intermedios.

Sin embargo, para complicar las cosas, el entorno del robot puede estar plagado de obstáculos. El robot NO PUEDE chocar con un obstáculo en su camino hacia la meta. Por lo tanto, si el robot se encuentra con un obstáculo, tendrá que orientarse para poder continuar su camino hacia la meta.

El robot programable

Cada robot viene con diferentes capacidades y preocupaciones de control. Familiaricémonos con nuestro robot programable simulado.

Lo primero a tener en cuenta es que, en esta guía, nuestro robot será un robot móvil autónomo . Esto significa que se moverá libremente en el espacio y que lo hará bajo su propio control. Esto contrasta con, por ejemplo, un robot de control remoto (que no es autónomo) o un brazo robótico de fábrica (que no es móvil). Nuestro robot debe descubrir por sí mismo cómo lograr sus objetivos y sobrevivir en su entorno. Esto demuestra ser un desafío sorprendentemente difícil para los programadores de robótica novatos.

Entradas de control: Sensores

Hay muchas maneras diferentes en que un robot puede estar equipado para monitorear su entorno. Estos pueden incluir cualquier cosa, desde sensores de proximidad, sensores de luz, parachoques, cámaras, etc. Además, los robots pueden comunicarse con sensores externos que les brindan información que ellos mismos no pueden observar directamente.

Nuestro robot de referencia está equipado con nueve sensores infrarrojos (el modelo más nuevo tiene ocho sensores infrarrojos y cinco ultrasónicos de proximidad) dispuestos en una "falda" en todas las direcciones. Hay más sensores en la parte delantera del robot que en la trasera porque, por lo general, es más importante para el robot saber qué hay delante que qué hay detrás.

Además de los sensores de proximidad, el robot tiene un par de indicadores de rueda que rastrean el movimiento de la rueda. Estos le permiten realizar un seguimiento de cuántas rotaciones hace cada rueda, con un giro completo hacia adelante de una rueda de 2,765 tics. Los giros en la dirección opuesta cuentan hacia atrás, disminuyendo el recuento de ticks en lugar de aumentarlo. No tienes que preocuparte por números específicos en este tutorial porque el software que escribiremos usa la distancia recorrida expresada en metros. Más adelante, le mostraré cómo calcularlo a partir de ticks con una sencilla función de Python.

Salidas de control: Movilidad

Algunos robots se mueven sobre piernas. Algunos ruedan como una pelota. Algunos incluso se deslizan como una serpiente.

Nuestro robot es un robot de accionamiento diferencial, lo que significa que rueda sobre dos ruedas. Cuando ambas ruedas giran a la misma velocidad, el robot se mueve en línea recta. Cuando las ruedas se mueven a diferentes velocidades, el robot gira. Por lo tanto, controlar el movimiento de este robot se reduce a controlar adecuadamente las velocidades a las que gira cada una de estas dos ruedas.

API

En Sobot Rimulator, la separación entre la "computadora" del robot y el mundo físico (simulado) está representada por el archivo robot_supervisor_interface.py , que define toda la API para interactuar con los sensores y motores del "robot real":

  • read_proximity_sensors() devuelve una matriz de nueve valores en el formato nativo de los sensores
  • read_wheel_encoders() devuelve una matriz de dos valores que indican el total de ticks desde el inicio
  • set_wheel_drive_rates( v_l, v_r ) toma dos valores (en radianes por segundo) y establece la velocidad izquierda y derecha de las ruedas en esos dos valores

Esta interfaz utiliza internamente un objeto robot que proporciona los datos de los sensores y la posibilidad de mover motores o ruedas. Si desea crear un robot diferente, simplemente debe proporcionar una clase de robot Python diferente que pueda ser utilizada por la misma interfaz, y el resto del código (controladores, supervisor y simulador) funcionará de forma inmediata.

el simulador

Como usaría un robot real en el mundo real sin prestar demasiada atención a las leyes de la física involucradas, puede ignorar cómo se simula el robot y pasar directamente a cómo se programa el software del controlador, ya que será casi lo mismo. entre el mundo real y una simulación. Pero si tiene curiosidad, lo presentaré brevemente aquí.

El archivo world.py es una clase de Python que representa el mundo simulado, con robots y obstáculos dentro. La función de paso dentro de esta clase se encarga de hacer evolucionar nuestro mundo simple al:

  • Aplicar las reglas de la física a los movimientos del robot.
  • Consideración de colisiones con obstáculos
  • Proporcionando nuevos valores para los sensores del robot.

Al final, llama a los supervisores de robots responsables de ejecutar el software del cerebro del robot.

La función de paso se ejecuta en un bucle para que robot.step_motion() mueva el robot utilizando la velocidad de la rueda calculada por el supervisor en el paso de simulación anterior.

 # step the simulation through one time interval def step( self ): dt = self.dt # step all the robots for robot in self.robots: # step robot motion robot.step_motion( dt ) # apply physics interactions self.physics.apply_physics() # NOTE: The supervisors must run last to ensure they are observing the "current" world # step all of the supervisors for supervisor in self.supervisors: supervisor.step( dt ) # increment world time self.world_time += dt

La función apply_physics() actualiza internamente los valores de los sensores de proximidad del robot para que el supervisor pueda estimar el entorno en el paso de simulación actual. Los mismos conceptos se aplican a los codificadores.

un modelo sencillo

Primero, nuestro robot tendrá un modelo muy simple. Hará muchas suposiciones sobre el mundo. Algunos de los más importantes incluyen:

  • El terreno es siempre llano y parejo.
  • Los obstáculos nunca son redondos
  • Las ruedas nunca resbalan
  • Nada va a empujar nunca al robot
  • Los sensores nunca fallan ni dan lecturas falsas.
  • Las ruedas siempre giran cuando se les dice que lo hagan.

Aunque la mayoría de estas suposiciones son razonables dentro de un entorno similar a una casa, podrían estar presentes obstáculos redondos. Nuestro software para evitar obstáculos tiene una implementación simple y sigue el borde de los obstáculos para sortearlos. Daremos pistas a los lectores sobre cómo mejorar el marco de control de nuestro robot con una verificación adicional para evitar obstáculos circulares.

El bucle de control

Ahora entraremos en el núcleo de nuestro software de control y explicaremos los comportamientos que queremos programar dentro del robot. Se pueden agregar comportamientos adicionales a este marco, ¡y debe probar sus propias ideas después de terminar de leer! El software de robótica basada en el comportamiento se propuso hace más de 20 años y sigue siendo una herramienta poderosa para la robótica móvil. Como ejemplo, en 2007 se utilizó un conjunto de comportamientos en el DARPA Urban Challenge, ¡la primera competencia para autos autónomos!

Un robot es un sistema dinámico. El estado del robot, las lecturas de sus sensores y los efectos de sus señales de control están en constante cambio. Controlar la forma en que se desarrollan los eventos implica los siguientes tres pasos:

  1. Aplicar señales de control.
  2. Medir los resultados.
  3. Generar nuevas señales de control calculadas para acercarnos a nuestro objetivo.

Estos pasos se repiten una y otra vez hasta que hayamos logrado nuestro objetivo. Cuantas más veces podamos hacer esto por segundo, mejor control tendremos sobre el sistema. El robot Sobot Rimulator repite estos pasos 20 veces por segundo (20 Hz), pero muchos robots deben hacerlo miles o millones de veces por segundo para tener un control adecuado. Recuerde nuestra introducción anterior sobre diferentes lenguajes de programación de robots para diferentes sistemas de robótica y requisitos de velocidad.

En general, cada vez que nuestro robot toma medidas con sus sensores, utiliza estas medidas para actualizar su estimación interna del estado del mundo, por ejemplo, la distancia desde su objetivo. Compara este estado con un valor de referencia de lo que quiere que sea el estado (para la distancia, quiere que sea cero) y calcula el error entre el estado deseado y el estado real. Una vez que se conoce esta información, la generación de nuevas señales de control puede reducirse a un problema de minimizar el error que eventualmente moverá al robot hacia la meta.

Un truco ingenioso: simplificar el modelo

Para controlar el robot que queremos programar, tenemos que enviar una señal a la rueda izquierda diciéndole qué tan rápido debe girar, y una señal separada a la rueda derecha diciéndole qué tan rápido debe girar. Llamemos a estas señales v L y v R . Sin embargo, pensar constantemente en términos de v L y v R es muy engorroso. En lugar de preguntar: "¿Qué tan rápido queremos que gire la rueda izquierda y qué tan rápido queremos que gire la rueda derecha?" es más natural preguntar: "¿Qué tan rápido queremos que avance el robot y qué tan rápido queremos que gire o cambie su rumbo?" Llamemos a estos parámetros velocidad v y velocidad angular (rotacional) ω (léase “omega”). Resulta que podemos basar todo nuestro modelo en v y ω en lugar de v L y v R , y solo una vez que hayamos determinado cómo queremos que se mueva nuestro robot programado, transformamos matemáticamente estos dos valores en la v L y v R que necesitamos para controlar realmente las ruedas del robot. Esto se conoce como modelo de control monociclo .

En la programación de robótica, es importante comprender la diferencia entre los modelos monociclo y diferencial.

Aquí está el código de Python que implementa la transformación final en supervisor.py . Tenga en cuenta que si ω es 0, ambas ruedas girarán a la misma velocidad:

 # generate and send the correct commands to the robot def _send_robot_commands( self ): # ... v_l, v_r = self._uni_to_diff( v, omega ) self.robot.set_wheel_drive_rates( v_l, v_r ) def _uni_to_diff( self, v, omega ): # v = translational velocity (m/s) # omega = angular velocity (rad/s) R = self.robot_wheel_radius L = self.robot_wheel_base_length v_l = ( (2.0 * v) - (omega*L) ) / (2.0 * R) v_r = ( (2.0 * v) + (omega*L) ) / (2.0 * R) return v_l, v_r

Estado de estimación: robot, conócete a ti mismo

Usando sus sensores, el robot debe tratar de estimar el estado del entorno, así como su propio estado. Estas estimaciones nunca serán perfectas, pero deben ser bastante buenas porque el robot basará todas sus decisiones en estas estimaciones. Usando solo sus sensores de proximidad y sus indicadores de rueda, debe intentar adivinar lo siguiente:

  • La dirección a los obstáculos.
  • La distancia de los obstáculos
  • La posición del robot
  • El rumbo del robot.

Las dos primeras propiedades están determinadas por las lecturas del sensor de proximidad y son bastante sencillas. La función de API read_proximity_sensors() devuelve una matriz de nueve valores, uno para cada sensor. Sabemos de antemano que la séptima lectura, por ejemplo, corresponde al sensor que apunta 75 grados a la derecha del robot.

Así, si este valor muestra una lectura correspondiente a 0,1 metros de distancia, sabemos que hay un obstáculo a 0,1 metros, 75 grados a la izquierda. Si no hay ningún obstáculo, el sensor devolverá una lectura de su rango máximo de 0,2 metros. Así, si leemos 0,2 metros en el sensor siete, supondremos que en realidad no hay ningún obstáculo en esa dirección.

Debido a la forma en que funcionan los sensores de infrarrojos (que miden la reflexión de infrarrojos), los números que devuelven son una transformación no lineal de la distancia real detectada. Así, la función de Python para determinar la distancia indicada debe convertir estas lecturas en metros. Esto se hace en supervisor.py de la siguiente manera:

 # update the distances indicated by the proximity sensors def _update_proximity_sensor_distances( self ): self.proximity_sensor_distances = [ 0.02-( log(readval/3960.0) )/30.0 for readval in self.robot.read_proximity_sensors() ]

Una vez más, tenemos un modelo de sensor específico en este marco de trabajo de robot de Python, mientras que en el mundo real, los sensores vienen con un software que los acompaña que debería proporcionar funciones de conversión similares de valores no lineales a metros.

Determinar la posición y el rumbo del robot (conocidos en conjunto como la pose en la programación de robótica) es algo más desafiante. Nuestro robot utiliza la odometría para estimar su pose. Aquí es donde entran los indicadores de las ruedas. Al medir cuánto ha girado cada rueda desde la última iteración del ciclo de control, es posible obtener una buena estimación de cómo ha cambiado la pose del robot, pero solo si el cambio es pequeño .

Esta es una de las razones por las que es importante iterar el bucle de control con mucha frecuencia en un robot del mundo real, donde los motores que mueven las ruedas pueden no ser perfectos. Si esperamos demasiado para medir los tickers de las ruedas, ambas ruedas podrían haber hecho mucho y será imposible estimar dónde hemos terminado.

Dado nuestro simulador de software actual, podemos permitirnos ejecutar el cálculo de la odometría a 20 Hz, la misma frecuencia que los controladores. Pero podría ser una buena idea tener un subproceso de Python separado que se ejecute más rápido para captar movimientos más pequeños de los teletipos.

A continuación se muestra la función de odometría completa en supervisor.py que actualiza la estimación de la pose del robot. Tenga en cuenta que la pose del robot se compone de las coordenadas x e y , y el rumbo theta , que se mide en radianes desde el eje X positivo. La x positiva está al este y la y positiva está al norte. Por lo tanto, un encabezado de 0 indica que el robot está mirando directamente hacia el este. El robot siempre asume que su pose inicial es (0, 0), 0 .

 # update the estimated position of the robot using it's wheel encoder readings def _update_odometry( self ): R = self.robot_wheel_radius N = float( self.wheel_encoder_ticks_per_revolution ) # read the wheel encoder values ticks_left, ticks_right = self.robot.read_wheel_encoders() # get the difference in ticks since the last iteration d_ticks_left = ticks_left - self.prev_ticks_left d_ticks_right = ticks_right - self.prev_ticks_right # estimate the wheel movements d_left_wheel = 2*pi*R*( d_ticks_left / N ) d_right_wheel = 2*pi*R*( d_ticks_right / N ) d_center = 0.5 * ( d_left_wheel + d_right_wheel ) # calculate the new pose prev_x, prev_y, prev_theta = self.estimated_pose.scalar_unpack() new_x = prev_x + ( d_center * cos( prev_theta ) ) new_y = prev_y + ( d_center * sin( prev_theta ) ) new_theta = prev_theta + ( ( d_right_wheel - d_left_wheel ) / self.robot_wheel_base_length ) # update the pose estimate with the new values self.estimated_pose.scalar_update( new_x, new_y, new_theta ) # save the current tick count for the next iteration self.prev_ticks_left = ticks_left self.prev_ticks_right = ticks_right

Ahora que nuestro robot puede generar una buena estimación del mundo real, usemos esta información para lograr nuestros objetivos.

Relacionado: Tutorial de física de videojuegos - Detección de colisiones para objetos sólidos

Métodos de programación de robots de Python: comportamiento de ir a la meta

El propósito supremo en la existencia de nuestro pequeño robot en este tutorial de programación es llegar al punto de destino. Entonces, ¿cómo hacemos que las ruedas giren para llegar allí? Comencemos por simplificar un poco nuestra visión del mundo y supongamos que no hay obstáculos en el camino.

Esto entonces se convierte en una tarea simple y se puede programar fácilmente en Python. Si avanzamos de cara a la meta, llegaremos allí. Gracias a nuestra odometría, sabemos cuáles son nuestras coordenadas y rumbo actuales. También sabemos cuáles son las coordenadas de la meta porque fueron preprogramadas. Por lo tanto, usando un poco de álgebra lineal, podemos determinar el vector desde nuestra ubicación hasta el objetivo, como en go_to_goal_controller.py :

 # return a go-to-goal heading vector in the robot's reference frame def calculate_gtg_heading_vector( self ): # get the inverse of the robot's pose robot_inv_pos, robot_inv_theta = self.supervisor.estimated_pose().inverse().vector_unpack() # calculate the goal vector in the robot's reference frame goal = self.supervisor.goal() goal = linalg.rotate_and_translate_vector( goal, robot_inv_theta, robot_inv_pos ) return goal

Tenga en cuenta que estamos llevando el vector a la meta en el marco de referencia del robot , y NO en coordenadas mundiales. Si el objetivo está en el eje X en el marco de referencia del robot, eso significa que está directamente frente al robot. Por lo tanto, el ángulo de este vector desde el eje X es la diferencia entre nuestro rumbo y el rumbo en el que queremos estar. En otras palabras, es el error entre nuestro estado actual y lo que queremos que sea nuestro estado actual. Por lo tanto, queremos ajustar nuestra tasa de giro ω para que el ángulo entre nuestro rumbo y la meta cambie hacia 0. Queremos minimizar el error:

 # calculate the error terms theta_d = atan2( self.gtg_heading_vector[1], self.gtg_heading_vector[0] ) # calculate angular velocity omega = self.kP * theta_d

self.kP en el fragmento anterior de la implementación del controlador Python es una ganancia de control. Es un coeficiente que determina la rapidez con la que giramos en proporción a la distancia de la meta a la que nos enfrentamos. Si el error en nuestro rumbo es 0 , entonces la tasa de giro también es 0 . En la función real de Python dentro del archivo go_to_goal_controller.py , verá ganancias más similares, ya que usamos un controlador PID en lugar de un coeficiente proporcional simple.

Ahora que tenemos nuestra velocidad angular ω , ¿cómo determinamos nuestra velocidad de avance v ? Una buena regla general es una que probablemente conozca instintivamente: si no estamos girando, podemos avanzar a toda velocidad, y cuanto más rápido estemos girando, más debemos reducir la velocidad. Esto generalmente nos ayuda a mantener nuestro sistema estable y actuando dentro de los límites de nuestro modelo. Por lo tanto, v es una función de ω . En go_to_goal_controller.py la ecuación es:

 # calculate translational velocity # velocity is v_max when omega is 0, # drops rapidly to zero as |omega| rises v = self.supervisor.v_max() / ( abs( omega ) + 1 )**0.5

Una sugerencia para profundizar en esta fórmula es considerar que solemos reducir la velocidad cuando estamos cerca de la meta para alcanzarla con velocidad cero. ¿Cómo cambiaría esta fórmula? Tiene que incluir de alguna manera un reemplazo de v_max() con algo proporcional a la distancia. Bien, casi hemos completado un solo ciclo de control. Lo único que queda por hacer es transformar estos dos parámetros del modelo de monociclo en velocidades diferenciales de las ruedas y enviar las señales a las ruedas. Aquí hay un ejemplo de la trayectoria del robot bajo el controlador ir a la meta, sin obstáculos:

Este es un ejemplo de la trayectoria del robot programado.

Como podemos ver, el vector a la meta es una referencia efectiva para que podamos basar nuestros cálculos de control. Es una representación interna de “dónde queremos ir”. Como veremos, la única gran diferencia entre ir a la meta y otros comportamientos es que a veces ir hacia la meta es una mala idea, por lo que debemos calcular un vector de referencia diferente.

Métodos de programación de robots Python: Comportamiento para evitar obstáculos

Ir hacia la meta cuando hay un obstáculo en esa dirección es un buen ejemplo. En lugar de chocarnos de cabeza con las cosas que se interponen en nuestro camino, intentemos programar una ley de control que haga que el robot las evite.

Para simplificar el escenario, olvidémonos por completo del objetivo y hagamos nuestro objetivo el siguiente: cuando no haya obstáculos frente a nosotros, avancemos. Cuando encuentre un obstáculo, aléjese de él hasta que ya no esté frente a nosotros.

En consecuencia, cuando no hay obstáculos frente a nosotros, queremos que nuestro vector de referencia simplemente apunte hacia adelante. Entonces ω será cero y v será la velocidad máxima. Sin embargo, tan pronto como detectamos un obstáculo con nuestros sensores de proximidad, queremos que el vector de referencia apunte en cualquier dirección que se aleje del obstáculo. Esto hará que ω se dispare para alejarnos del obstáculo, y hará que v baje para asegurarnos de que no nos topemos accidentalmente con el obstáculo en el proceso.

Una buena forma de generar nuestro vector de referencia deseado es convertir nuestras nueve lecturas de proximidad en vectores y tomar una suma ponderada. Cuando no se detecten obstáculos, los vectores se sumarán simétricamente, dando como resultado un vector de referencia que apunta directamente hacia adelante como se desee. Pero si un sensor en, digamos, el lado derecho detecta un obstáculo, contribuirá con un vector más pequeño a la suma, y ​​el resultado será un vector de referencia que se desplaza hacia la izquierda.

Para un robot general con una ubicación diferente de los sensores, se puede aplicar la misma idea pero puede requerir cambios en los pesos y/o cuidado adicional cuando los sensores son simétricos en la parte delantera y trasera del robot, ya que la suma ponderada podría volverse cero. .

Cuando se programa correctamente, el robot puede evitar estos complejos obstáculos.

Aquí está el código que hace esto en avoid_obstacles_controller.py :

 # sensor gains (weights) self.sensor_gains = [ 1.0+( (0.4*abs(p.theta)) / pi ) for p in supervisor.proximity_sensor_placements() ] # ... # return an obstacle avoidance vector in the robot's reference frame # also returns vectors to detected obstacles in the robot's reference frame def calculate_ao_heading_vector( self ): # initialize vector obstacle_vectors = [ [ 0.0, 0.0 ] ] * len( self.proximity_sensor_placements ) ao_heading_vector = [ 0.0, 0.0 ] # get the distances indicated by the robot's sensor readings sensor_distances = self.supervisor.proximity_sensor_distances() # calculate the position of detected obstacles and find an avoidance vector robot_pos, robot_theta = self.supervisor.estimated_pose().vector_unpack() for i in range( len( sensor_distances ) ): # calculate the position of the obstacle sensor_pos, sensor_theta = self.proximity_sensor_placements[i].vector_unpack() vector = [ sensor_distances[i], 0.0 ] vector = linalg.rotate_and_translate_vector( vector, sensor_theta, sensor_pos ) obstacle_vectors[i] = vector # store the obstacle vectors in the robot's reference frame # accumulate the heading vector within the robot's reference frame ao_heading_vector = linalg.add( ao_heading_vector, linalg.scale( vector, self.sensor_gains[i] ) ) return ao_heading_vector, obstacle_vectors

Usando el ao_heading_vector resultante como nuestra referencia para que el robot intente igualar, estos son los resultados de ejecutar el software del robot en simulación usando solo el controlador para evitar obstáculos, ignorando el punto objetivo por completo. El robot rebota sin rumbo, pero nunca choca con un obstáculo, e incluso se las arregla para navegar en espacios muy reducidos:

Este robot está evitando con éxito los obstáculos dentro del simulador de robot Python.

Métodos de programación de robots de Python: Autómatas híbridos (Máquina de estado de comportamiento)

Hasta ahora hemos descrito dos comportamientos (ir a la meta y evitar obstáculos) de forma aislada. Ambos cumplen su función de manera admirable, pero para alcanzar con éxito la meta en un entorno lleno de obstáculos, necesitamos combinarlos.

La solución que desarrollaremos radica en una clase de máquinas que tiene la designación de autómatas híbridos que suena sumamente genial. Un autómata híbrido está programado con varios comportamientos o modos diferentes, así como una máquina de estado de supervisión. La máquina de estado de supervisión cambia de un modo a otro en tiempos discretos (cuando se logran los objetivos o el entorno cambia demasiado repentinamente), mientras que cada comportamiento usa sensores y ruedas para reaccionar continuamente a los cambios del entorno. La solución se denominó híbrida porque evoluciona tanto de manera discreta como continua.

Nuestro marco de robot de Python implementa la máquina de estado en el archivo supervisor_state_machine.py .

Equipado con nuestros dos prácticos comportamientos, se sugiere una lógica simple: cuando no se detecte ningún obstáculo, use el comportamiento ir a la meta. Cuando se detecta un obstáculo, cambie al comportamiento de evitar obstáculos hasta que ya no se detecte el obstáculo.

Sin embargo, resulta que esta lógica producirá muchos problemas. Lo que este sistema tiende a hacer cuando encuentra un obstáculo es alejarse de él, luego, tan pronto como se haya alejado de él, dar la vuelta y toparse con él nuevamente. El resultado es un ciclo interminable de cambios rápidos que inutiliza el robot. In the worst case, the robot may switch between behaviors with every iteration of the control loop—a state known as a Zeno condition .

There are multiple solutions to this problem, and readers that are looking for deeper knowledge should check, for example, the DAMN software architecture.

What we need for our simple simulated robot is an easier solution: One more behavior specialized with the task of getting around an obstacle and reaching the other side.

Python Robot Programming Methods: Follow-Wall Behavior

Here's the idea: When we encounter an obstacle, take the two sensor readings that are closest to the obstacle and use them to estimate the surface of the obstacle. Then, simply set our reference vector to be parallel to this surface. Keep following this wall until A) the obstacle is no longer between us and the goal, and B) we are closer to the goal than we were when we started. Then we can be certain we have navigated the obstacle properly.

With our limited information, we can't say for certain whether it will be faster to go around the obstacle to the left or to the right. To make up our minds, we select the direction that will move us closer to the goal immediately. To figure out which way that is, we need to know the reference vectors of the go-to-goal behavior and the avoid-obstacle behavior, as well as both of the possible follow-wall reference vectors. Here is an illustration of how the final decision is made (in this case, the robot will choose to go left):

Utilizing a few types of behaviors, the programmed robot avoids obstacles and continues onward.

Determining the follow-wall reference vectors turns out to be a bit more involved than either the avoid-obstacle or go-to-goal reference vectors. Take a look at the Python code in follow_wall_controller.py to see how it's done.

Final Control Design

The final control design uses the follow-wall behavior for almost all encounters with obstacles. However, if the robot finds itself in a tight spot, dangerously close to a collision, it will switch to pure avoid-obstacles mode until it is a safer distance away, and then return to follow-wall. Once obstacles have been successfully negotiated, the robot switches to go-to-goal. Here is the final state diagram, which is programmed inside the supervisor_state_machine.py :

This diagram illustrates the switching between robotics programming behaviors to achieve a goal and avoid obstacles.

Here is the robot successfully navigating a crowded environment using this control scheme:

The robot simulator has successfully allowed the robot software to avoid obstacles and achieve its original purpose.

An additional feature of the state machine that you can try to implement is a way to avoid circular obstacles by switching to go-to-goal as soon as possible instead of following the obstacle border until the end (which does not exist for circular objects!)

Tweak, Tweak, Tweak: Trial and Error

The control scheme that comes with Sobot Rimulator is very finely tuned. It took many hours of tweaking one little variable here, and another equation there, to get it to work in a way I was satisfied with. Robotics programming often involves a great deal of plain old trial-and-error. Robots are very complex and there are few shortcuts to getting them to behave optimally in a robot simulator environment…at least, not much short of outright machine learning, but that's a whole other can of worms.

Robotics often involves a great deal of plain old trial-and-error.

I encourage you to play with the control variables in Sobot Rimulator and observe and attempt to interpret the results. Changes to the following all have profound effects on the simulated robot's behavior:

  • The error gain kP in each controller
  • The sensor gains used by the avoid-obstacles controller
  • The calculation of v as a function of ω in each controller
  • The obstacle standoff distance used by the follow-wall controller
  • The switching conditions used by supervisor_state_machine.py
  • Pretty much anything else

When Programmable Robots Fail

We've done a lot of work to get to this point, and this robot seems pretty clever. Yet, if you run Sobot Rimulator through several randomized maps, it won't be long before you find one that this robot can't deal with. Sometimes it drives itself directly into tight corners and collides. Sometimes it just oscillates back and forth endlessly on the wrong side of an obstacle. Occasionally it is legitimately imprisoned with no possible path to the goal. After all of our testing and tweaking, sometimes we must come to the conclusion that the model we are working with just isn't up to the job, and we have to change the design or add functionality.

In the mobile robot universe, our little robot's “brain” is on the simpler end of the spectrum. Many of the failure cases it encounters could be overcome by adding some more advanced software to the mix. More advanced robots make use of techniques such as mapping , to remember where it's been and avoid trying the same things over and over; heuristics , to generate acceptable decisions when there is no perfect decision to be found; and machine learning , to more perfectly tune the various control parameters governing the robot's behavior.

A Sample of What's to Come

Robots are already doing so much for us, and they are only going to be doing more in the future. While even basic robotics programming is a tough field of study requiring great patience, it is also a fascinating and immensely rewarding one.

In this tutorial, we learned how to develop reactive control software for a robot using the high-level programming language Python. But there are many more advanced concepts that can be learned and tested quickly with a Python robot framework similar to the one we prototyped here. I hope you will consider getting involved in the shaping of things to come!


Acknowledgement: I would like to thank Dr. Magnus Egerstedt and Jean-Pierre de la Croix of the Georgia Institute of Technology for teaching me all this stuff, and for their enthusiasm for my work on Sobot Rimulator.

Related: OpenCV Tutorial: Real-time Object Detection Using MSER in iOS