Um tutorial introdutório de programação de robôs

Publicados: 2022-03-11
Nota do editor: em 16 de outubro de 2018, este artigo foi revisado para funcionar com as tecnologias mais recentes.

Vamos enfrentá-lo, os robôs são legais. Eles também vão dominar o mundo algum dia, e espero que, nesse momento, eles tenham pena de seus pobres criadores carnudos e macios (também conhecidos como desenvolvedores de robótica) e nos ajudem a construir uma utopia espacial cheia de abundância. Estou brincando, é claro, mas só mais ou menos.

Na minha ambição de ter alguma pequena influência sobre o assunto, fiz um curso de teoria de controle de robôs autônomos no ano passado, que culminou na construção de um simulador robótico baseado em Python que me permitiu praticar a teoria de controle em um robô simples, móvel e programável. .

Neste artigo, vou mostrar como usar um framework de robô Python para desenvolver software de controle, descrever o esquema de controle que desenvolvi para meu robô simulado, ilustrar como ele interage com seu ambiente e atinge seus objetivos e discutir alguns dos desafios fundamentais da programação robótica que encontrei ao longo do caminho.

Para seguir este tutorial sobre programação robótica para iniciantes, você deve ter um conhecimento básico de duas coisas:

  • Matemática—vamos usar algumas funções trigonométricas e vetores
  • Python—já que Python está entre as linguagens básicas de programação de robôs mais populares—vamos usar bibliotecas e funções básicas de Python

Os trechos de código mostrados aqui são apenas uma parte de todo o simulador, que depende de classes e interfaces, portanto, para ler o código diretamente, você pode precisar de alguma experiência em Python e programação orientada a objetos.

Por fim, tópicos opcionais que ajudarão você a seguir melhor este tutorial são saber o que é uma máquina de estado e como funcionam os sensores e codificadores de alcance.

O desafio do robô programável: percepção versus realidade e a fragilidade do controle

O desafio fundamental de toda a robótica é este: é impossível conhecer o verdadeiro estado do ambiente. O software de controle do robô só pode adivinhar o estado do mundo real com base nas medições retornadas por seus sensores. Ele só pode tentar mudar o estado do mundo real através da geração de sinais de controle.

Este gráfico demonstra a interação entre um robô físico e controles de computador ao praticar a programação de robôs Python.

O software de controle do robô só pode adivinhar o estado do mundo real com base nas medições retornadas por seus sensores.

Assim, um dos primeiros passos no projeto de controle é criar uma abstração do mundo real, conhecida como modelo , com a qual interpretar nossas leituras de sensores e tomar decisões. Desde que o mundo real se comporte de acordo com as suposições do modelo, podemos fazer boas suposições e exercer controle. Assim que o mundo real se desviar dessas suposições, no entanto, não seremos mais capazes de fazer boas suposições e o controle será perdido. Muitas vezes, uma vez que o controle é perdido, ele nunca pode ser recuperado. (A menos que alguma força externa benevolente o restaure.)

Esta é uma das principais razões pelas quais a programação robótica é tão difícil. Muitas vezes vemos vídeos do mais recente robô de pesquisa no laboratório, realizando feitos fantásticos de destreza, navegação ou trabalho em equipe, e somos tentados a perguntar: “Por que isso não é usado no mundo real?” Bem, da próxima vez que você assistir a um vídeo desse tipo, dê uma olhada em como o ambiente de laboratório é altamente controlado. Na maioria dos casos, esses robôs só são capazes de realizar essas tarefas impressionantes enquanto as condições ambientais permanecerem dentro dos limites estreitos de seu modelo interno. Assim, uma chave para o avanço da robótica é o desenvolvimento de modelos mais complexos, flexíveis e robustos – e tal avanço está sujeito aos limites dos recursos computacionais disponíveis.

Uma chave para o avanço da robótica é o desenvolvimento de modelos mais complexos, flexíveis e robustos.

[Nota lateral: Filósofos e psicólogos notariam que as criaturas vivas também sofrem de dependência de sua própria percepção interna do que seus sentidos estão lhes dizendo. Muitos avanços na robótica vêm da observação de criaturas vivas e de como elas reagem a estímulos inesperados. Pense nisso. Qual é o seu modelo interno do mundo? É diferente do de uma formiga e do de um peixe? (Esperamos.) No entanto, como a formiga e o peixe, é provável que simplifique demais algumas realidades do mundo. Quando suas suposições sobre o mundo não estão corretas, você corre o risco de perder o controle das coisas. Às vezes chamamos isso de “perigo”. Da mesma forma que nosso pequeno robô luta para sobreviver contra o universo desconhecido, todos nós também. Esta é uma visão poderosa para roboticistas.]

O simulador de robô programável

O simulador que construí foi escrito em Python e muito inteligentemente apelidado de Sobot Rimulator . Você pode encontrar a v1.0.0 no GitHub. Ele não tem muitos sinos e assobios, mas é construído para fazer uma coisa muito bem: fornecer uma simulação precisa de um robô móvel e dar a um aspirante a roboticista uma estrutura simples para praticar a programação de software de robô. Embora seja sempre melhor ter um robô real para brincar, um bom simulador de robô Python é muito mais acessível e é um ótimo lugar para começar.

Em robôs do mundo real, o software que gera os sinais de controle (o “controlador”) precisa rodar em alta velocidade e fazer cálculos complexos. Isso afeta a escolha de quais linguagens de programação de robôs são melhores para usar: Normalmente, C++ é usado para esses tipos de cenários, mas em aplicativos de robótica mais simples, Python é um compromisso muito bom entre velocidade de execução e facilidade de desenvolvimento e teste.

O software que escrevi simula um robô de pesquisa da vida real chamado Khepera, mas pode ser adaptado a uma variedade de robôs móveis com diferentes dimensões e sensores. Como tentei programar o simulador o mais semelhante possível às capacidades do robô real, a lógica de controle pode ser carregada em um robô Khepera real com refatoração mínima e ele executará o mesmo que o robô simulado. As características específicas implementadas referem-se ao Khepera III, mas podem ser facilmente adaptadas ao novo Khepera IV.

Em outras palavras, programar um robô simulado é análogo a programar um robô real. Isso é crítico se o simulador for útil para desenvolver e avaliar diferentes abordagens de software de controle.

Neste tutorial, descreverei a arquitetura do software de controle do robô que vem com a v1.0.0 do Sobot Rimulator e fornecerei trechos da fonte Python (com pequenas modificações para maior clareza). No entanto, eu encorajo você a mergulhar na fonte e mexer. O simulador foi bifurcado e usado para controlar diferentes robôs móveis, incluindo um Roomba2 da iRobot. Da mesma forma, sinta-se à vontade para bifurcar o projeto e melhorá-lo.

A lógica de controle do robô é restrita a essas classes/arquivos Python:

  • models/supervisor.py — essa classe é responsável pela interação entre o mundo simulado ao redor do robô e o próprio robô. Ele evolui nossa máquina de estado do robô e aciona os controladores para calcular o comportamento desejado.
  • models/supervisor_state_machine.py —esta classe representa os diferentes estados em que o robô pode estar, dependendo de sua interpretação dos sensores.
  • Os arquivos no diretório models/controllers —essas classes implementam diferentes comportamentos do robô dado um estado conhecido do ambiente. Em particular, um controlador específico é selecionado dependendo da máquina de estado.

O objetivo

Os robôs, como as pessoas, precisam de um propósito na vida. O objetivo do nosso software de controlar este robô será muito simples: ele tentará chegar a um ponto de meta predeterminado. Esta é geralmente a característica básica que qualquer robô móvel deve ter, desde carros autônomos até aspiradores de pó robóticos. As coordenadas da meta são programadas no software de controle antes que o robô seja ativado, mas podem ser geradas a partir de um aplicativo Python adicional que supervisiona os movimentos do robô. Por exemplo, pense nisso dirigindo por vários pontos de passagem.

No entanto, para complicar as coisas, o ambiente do robô pode estar repleto de obstáculos. O robô NÃO PODE colidir com um obstáculo em seu caminho para o gol. Portanto, se o robô encontrar um obstáculo, ele terá que se orientar para poder continuar seu caminho até o objetivo.

O Robô Programável

Cada robô vem com diferentes capacidades e preocupações de controle. Vamos nos familiarizar com nosso robô programável simulado.

A primeira coisa a notar é que, neste guia, nosso robô será um robô móvel autônomo . Isso significa que ele se moverá livremente no espaço e o fará sob seu próprio controle. Isso contrasta com, digamos, um robô de controle remoto (que não é autônomo) ou um braço robótico de fábrica (que não é móvel). Nosso robô deve descobrir por si mesmo como atingir seus objetivos e sobreviver em seu ambiente. Isso prova ser um desafio surpreendentemente difícil para programadores de robótica iniciantes.

Entradas de Controle: Sensores

Há muitas maneiras diferentes de um robô ser equipado para monitorar seu ambiente. Isso pode incluir qualquer coisa, desde sensores de proximidade, sensores de luz, pára-choques, câmeras e assim por diante. Além disso, os robôs podem se comunicar com sensores externos que fornecem informações que eles mesmos não podem observar diretamente.

Nosso robô de referência está equipado com nove sensores infravermelhos – o modelo mais novo possui oito sensores infravermelhos e cinco sensores de proximidade ultrassônicos – dispostos em uma “saia” em todas as direções. Existem mais sensores voltados para a frente do robô do que para trás, porque geralmente é mais importante para o robô saber o que está na frente dele do que o que está atrás dele.

Além dos sensores de proximidade, o robô possui um par de marcadores de roda que rastreiam o movimento da roda. Isso permite que você rastreie quantas rotações cada roda faz, com uma volta completa para frente de uma roda sendo 2.765 ticks. As voltas na direção oposta contam para trás, diminuindo a contagem de ticks em vez de aumentá-la. Você não precisa se preocupar com números específicos neste tutorial porque o software que escreveremos usa a distância percorrida expressa em metros. Mais tarde, mostrarei como calculá-lo a partir de ticks com uma função Python fácil.

Saídas de controle: Mobilidade

Alguns robôs se movem sobre as pernas. Alguns rolam como uma bola. Alguns até deslizam como uma cobra.

Nosso robô é um robô de acionamento diferencial, o que significa que ele rola sobre duas rodas. Quando ambas as rodas giram na mesma velocidade, o robô se move em linha reta. Quando as rodas se movem em velocidades diferentes, o robô gira. Assim, controlar o movimento desse robô se resume a controlar adequadamente as taxas em que cada uma dessas duas rodas gira.

API

No Sobot Rimulator, a separação entre o “computador” do robô e o mundo físico (simulado) é incorporada pelo arquivo robot_supervisor_interface.py , que define toda a API para interagir com os sensores e motores do “robô real”:

  • read_proximity_sensors() retorna uma matriz de nove valores no formato nativo dos sensores
  • read_wheel_encoders() retorna uma matriz de dois valores indicando o total de ticks desde o início
  • set_wheel_drive_rates( v_l, v_r ) recebe dois valores (em radianos por segundo) e define a velocidade esquerda e direita das rodas para esses dois valores

Esta interface utiliza internamente um objeto robô que fornece os dados dos sensores e a possibilidade de movimentar motores ou rodas. Se você quiser criar um robô diferente, basta fornecer uma classe de robô Python diferente que possa ser usada pela mesma interface, e o restante do código (controladores, supervisor e simulador) funcionará imediatamente!

O Simulador

Como você usaria um robô real no mundo real sem prestar muita atenção às leis da física envolvidas, você pode ignorar como o robô é simulado e pular diretamente para como o software do controlador é programado, pois será quase o mesmo entre o mundo real e uma simulação. Mas se você está curioso, vou apresentá-lo brevemente aqui.

O arquivo world.py é uma classe Python que representa o mundo simulado, com robôs e obstáculos dentro. A função step dentro desta classe cuida da evolução do nosso mundo simples:

  • Aplicando regras físicas aos movimentos do robô
  • Considerando colisões com obstáculos
  • Fornecendo novos valores para os sensores do robô

No final, chama os supervisores do robô responsáveis ​​pela execução do software do cérebro do robô.

A função step é executada em um loop para que robot.step_motion() mova o robô usando a velocidade da roda calculada pelo supervisor na etapa de simulação 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

A função apply_physics() atualiza internamente os valores dos sensores de proximidade do robô para que o supervisor possa estimar o ambiente na etapa atual da simulação. Os mesmos conceitos se aplicam aos codificadores.

Um modelo simples

Primeiro, nosso robô terá um modelo muito simples. Fará muitas suposições sobre o mundo. Alguns dos mais importantes incluem:

  • O terreno é sempre plano e uniforme
  • Obstáculos nunca são redondos
  • As rodas nunca escorregam
  • Nada nunca vai empurrar o robô ao redor
  • Os sensores nunca falham ou dão leituras falsas
  • As rodas sempre giram quando

Embora a maioria dessas suposições seja razoável dentro de um ambiente semelhante a uma casa, obstáculos redondos podem estar presentes. Nosso software de prevenção de obstáculos tem uma implementação simples e segue a borda dos obstáculos para contorná-los. Daremos dicas aos leitores sobre como melhorar a estrutura de controle do nosso robô com uma verificação adicional para evitar obstáculos circulares.

O circuito de controle

Vamos agora entrar no núcleo do nosso software de controle e explicar os comportamentos que queremos programar dentro do robô. Comportamentos adicionais podem ser adicionados a essa estrutura, e você deve tentar suas próprias ideias depois de terminar de ler! O software de robótica baseado em comportamento foi proposto há mais de 20 anos e ainda é uma ferramenta poderosa para robótica móvel. Como exemplo, em 2007, um conjunto de comportamentos foi usado no DARPA Urban Challenge – a primeira competição para carros autônomos!

Um robô é um sistema dinâmico. O estado do robô, as leituras de seus sensores e os efeitos de seus sinais de controle estão em constante fluxo. Controlar a forma como os eventos se desenrolam envolve as três etapas a seguir:

  1. Aplique sinais de controle.
  2. Meça os resultados.
  3. Gere novos sinais de controle calculados para nos aproximar do nosso objetivo.

Essas etapas são repetidas várias vezes até atingirmos nosso objetivo. Quanto mais vezes pudermos fazer isso por segundo, melhor controle teremos sobre o sistema. O robô Sobot Rimulator repete essas etapas 20 vezes por segundo (20 Hz), mas muitos robôs devem fazer isso milhares ou milhões de vezes por segundo para ter controle adequado. Lembre-se de nossa introdução anterior sobre diferentes linguagens de programação de robôs para diferentes sistemas de robótica e requisitos de velocidade.

Em geral, cada vez que nosso robô faz medições com seus sensores, ele usa essas medições para atualizar sua estimativa interna do estado do mundo – por exemplo, a distância de seu objetivo. Ele compara esse estado com um valor de referência do que deseja que o estado seja (para a distância, ele quer que seja zero) e calcula o erro entre o estado desejado e o estado real. Uma vez que esta informação é conhecida, a geração de novos sinais de controle pode ser reduzida a um problema de minimização do erro que eventualmente irá mover o robô em direção ao objetivo.

Um truque bacana: simplificando o modelo

Para controlar o robô que queremos programar, temos que enviar um sinal para a roda esquerda informando o quão rápido ele deve girar e um sinal separado para a roda direita informando o quão rápido ele deve girar. Vamos chamar esses sinais de v L e v R . No entanto, pensar constantemente em termos de v L e v R é muito complicado. Em vez de perguntar: “Com que rapidez queremos que a roda esquerda gire e com que rapidez queremos que a roda direita gire?” é mais natural perguntar: “Com que velocidade queremos que o robô avance e com que rapidez queremos que ele gire ou mude seu rumo?” Vamos chamar esses parâmetros de velocidade v e velocidade angular (rotacional) ω (leia-se “ômega”). Acontece que podemos basear nosso modelo inteiro em v e ω em vez de v L e v R , e somente depois de determinarmos como queremos que nosso robô programado se mova, transformar matematicamente esses dois valores nos v L e v R que precisamos para realmente controlar as rodas do robô. Isso é conhecido como um modelo monociclo de controle.

Na programação de robótica, é importante entender a diferença entre os modelos de acionamento monociclo e diferencial.

Aqui está o código Python que implementa a transformação final em supervisor.py . Observe que se ω for 0, ambas as rodas irão girar com a mesma velocidade:

 # 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

Estimando o estado: Robô, conheça a si mesmo

Usando seus sensores, o robô deve tentar estimar o estado do ambiente, bem como seu próprio estado. Essas estimativas nunca serão perfeitas, mas devem ser razoavelmente boas porque o robô baseará todas as suas decisões nessas estimativas. Usando apenas seus sensores de proximidade e marcadores de roda, ele deve tentar adivinhar o seguinte:

  • A direção dos obstáculos
  • A distância dos obstáculos
  • A posição do robô
  • O título do robô

As duas primeiras propriedades são determinadas pelas leituras do sensor de proximidade e são bastante diretas. A função da API read_proximity_sensors() retorna uma matriz de nove valores, um para cada sensor. Sabemos de antemão que a sétima leitura, por exemplo, corresponde ao sensor que aponta 75 graus para a direita do robô.

Assim, se este valor apresentar uma leitura correspondente a 0,1 metros de distância, sabemos que existe um obstáculo a 0,1 metros de distância, 75 graus à esquerda. Se não houver nenhum obstáculo, o sensor retornará uma leitura de seu alcance máximo de 0,2 metros. Assim, se lermos 0,2 metros no sensor sete, assumiremos que não há realmente nenhum obstáculo nessa direção.

Devido à maneira como os sensores infravermelhos funcionam (medindo a reflexão infravermelha), os números que eles retornam são uma transformação não linear da distância real detectada. Assim, a função Python para determinar a distância indicada deve converter essas leituras em metros. Isso é feito em supervisor.py da seguinte forma:

 # 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() ]

Novamente, temos um modelo de sensor específico nesta estrutura de robô Python, enquanto no mundo real, os sensores vêm com um software que deve fornecer funções de conversão semelhantes de valores não lineares para medidores.

Determinar a posição e a direção do robô (juntos conhecidos como pose na programação robótica) é um pouco mais desafiador. Nosso robô usa odometria para estimar sua pose. É aqui que entram os marcadores de roda. Ao medir o quanto cada roda girou desde a última iteração do loop de controle, é possível obter uma boa estimativa de como a pose do robô mudou - mas somente se a mudança for pequena .

Esta é uma razão pela qual é importante iterar o circuito de controle com muita frequência em um robô do mundo real, onde os motores que movem as rodas podem não ser perfeitos. Se esperássemos muito tempo para medir os tickers das rodas, ambas as rodas poderiam ter feito muito, e será impossível estimar onde chegamos.

Dado nosso simulador de software atual, podemos executar o cálculo de odometria em 20 Hz - a mesma frequência dos controladores. Mas pode ser uma boa ideia ter um thread Python separado rodando mais rápido para capturar movimentos menores dos tickers.

Abaixo está a função de odometria completa em supervisor.py que atualiza a estimativa de pose do robô. Observe que a pose do robô é composta pelas coordenadas x e y e pela direção theta , que é medida em radianos a partir do eixo X positivo. Positivo x é para o leste e positivo y é para o norte. Assim, uma direção de 0 indica que o robô está voltado diretamente para o leste. O robô sempre assume que sua pose inicial é (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

Agora que nosso robô é capaz de gerar uma boa estimativa do mundo real, vamos usar essa informação para atingir nossos objetivos.

Relacionado: Tutorial de Física de Videogames - Detecção de Colisão para Objetos Sólidos

Métodos de programação do robô Python: comportamento Go-to-Goal

O propósito supremo na existência do nosso pequeno robô neste tutorial de programação é chegar ao ponto de meta. Então, como fazemos as rodas girarem para chegar lá? Vamos começar simplificando um pouco nossa visão de mundo e assumir que não há obstáculos no caminho.

Isso então se torna uma tarefa simples e pode ser facilmente programada em Python. Se avançarmos encarando o gol, chegaremos lá. Graças à nossa odometria, sabemos quais são nossas coordenadas e rumo atuais. Também sabemos quais são as coordenadas do gol porque elas foram pré-programadas. Portanto, usando um pouco de álgebra linear, podemos determinar o vetor de nossa localização até o objetivo, como em 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

Observe que estamos levando o vetor para o objetivo no referencial do robô e NÃO nas coordenadas mundiais. Se o objetivo estiver no eixo X no quadro de referência do robô, isso significa que está diretamente na frente do robô. Assim, o ângulo desse vetor em relação ao eixo X é a diferença entre nossa direção e a direção em que queremos estar. Em outras palavras, é o erro entre nosso estado atual e o que queremos que seja nosso estado atual. Portanto, queremos ajustar nossa taxa de giro ω para que o ângulo entre nossa direção e o objetivo mude para 0. Queremos minimizar o erro:

 # 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 no trecho acima da implementação do controlador Python é um ganho de controle. É um coeficiente que determina o quão rápido nos viramos em proporção à distância do objetivo que estamos enfrentando. Se o erro em nosso rumo for 0 , então a taxa de giro também será 0 . Na função Python real dentro do arquivo go_to_goal_controller.py , você verá ganhos mais semelhantes, já que usamos um controlador PID ao invés de um simples coeficiente proporcional.

Agora que temos nossa velocidade angular ω , como determinamos nossa velocidade para frente v ? Uma boa regra geral é aquela que você provavelmente conhece instintivamente: se não estivermos fazendo uma curva, podemos avançar a toda velocidade e, quanto mais rápido estivermos, mais devemos desacelerar. Isso geralmente nos ajuda a manter nosso sistema estável e agindo dentro dos limites do nosso modelo. Assim, v é uma função de ω . Em go_to_goal_controller.py a equação é:

 # 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

Uma sugestão para elaborar essa fórmula é considerar que geralmente desaceleramos quando estamos perto da meta para alcançá-la com velocidade zero. Como essa fórmula mudaria? Tem que incluir de alguma forma uma substituição de v_max() por algo proporcional à distância. OK, quase completamos um único loop de controle. A única coisa que resta a fazer é transformar esses dois parâmetros do modelo monociclo em velocidades diferenciais das rodas e enviar os sinais para as rodas. Aqui está um exemplo da trajetória do robô sob o controlador go-to-goal, sem obstáculos:

Este é um exemplo da trajetória do robô programado.

Como podemos ver, o vetor para a meta é uma referência efetiva para basearmos nossos cálculos de controle. É uma representação interna de “para onde queremos ir”. Como veremos, a única grande diferença entre go-to-goal e outros comportamentos é que, às vezes, ir em direção ao objetivo é uma má ideia, então devemos calcular um vetor de referência diferente.

Métodos de programação de robôs Python: comportamento de evitar obstáculos

Ir em direção à meta quando há um obstáculo nessa direção é um exemplo. Em vez de nos precipitarmos contra as coisas em nosso caminho, vamos tentar programar uma lei de controle que faça o robô evitá-las.

Para simplificar o cenário, vamos agora esquecer completamente o ponto de meta e apenas fazer o seguinte nosso objetivo: Quando não houver obstáculos à nossa frente, avance. Quando um obstáculo for encontrado, afaste-se dele até que ele não esteja mais à nossa frente.

Assim, quando não há nenhum obstáculo à nossa frente, queremos que nosso vetor de referência simplesmente aponte para frente. Então ω será zero e v será a velocidade máxima. No entanto, assim que detectamos um obstáculo com nossos sensores de proximidade, queremos que o vetor de referência aponte em qualquer direção que esteja longe do obstáculo. Isso fará com que ω suba para nos afastar do obstáculo e faça com que v caia para garantir que não colidiremos acidentalmente com o obstáculo no processo.

Uma maneira elegante de gerar nosso vetor de referência desejado é transformar nossas nove leituras de proximidade em vetores e obter uma soma ponderada. Quando não houver obstáculos detectados, os vetores serão somados simetricamente, resultando em um vetor de referência que aponta para frente conforme desejado. Mas se um sensor, digamos, do lado direito pegar um obstáculo, ele contribuirá com um vetor menor para a soma, e o resultado será um vetor de referência que é deslocado para a esquerda.

Para um robô geral com um posicionamento diferente de sensores, a mesma ideia pode ser aplicada, mas pode exigir alterações nos pesos e/ou cuidados adicionais quando os sensores são simétricos na frente e na traseira do robô, pois a soma ponderada pode se tornar zero .

Quando programado corretamente, o robô pode evitar esses obstáculos complexos.

Aqui está o código que faz isso em 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 o ao_heading_vector resultante como nossa referência para o robô tentar corresponder, aqui estão os resultados da execução do software do robô na simulação usando apenas o controlador de evitar obstáculos, ignorando completamente o ponto de meta. O robô salta sem rumo, mas nunca colide com um obstáculo e até consegue navegar em alguns espaços muito apertados:

Este robô está evitando obstáculos com sucesso no simulador de robô Python.

Métodos de programação de robôs Python: autômatos híbridos (máquina de estado de comportamento)

Até agora descrevemos dois comportamentos – ir para a meta e evitar obstáculos – isoladamente. Ambos cumprem sua função de forma admirável, mas para atingir o objetivo com sucesso em um ambiente cheio de obstáculos, precisamos combiná-los.

A solução que desenvolveremos está em uma classe de máquinas que tem a designação extremamente legal de autômatos híbridos . Um autômato híbrido é programado com vários comportamentos ou modos diferentes, bem como uma máquina de estado de supervisão. A máquina de estado de supervisão muda de um modo para outro em tempos discretos (quando os objetivos são alcançados ou o ambiente muda muito repentinamente), enquanto cada comportamento usa sensores e rodas para reagir continuamente às mudanças do ambiente. A solução foi chamada de híbrida porque evolui de forma discreta e contínua.

Nossa estrutura de robô Python implementa a máquina de estado no arquivo supervisor_state_machine.py .

Equipado com nossos dois comportamentos práticos, uma lógica simples se sugere: Quando não houver nenhum obstáculo detectado, use o comportamento go-to-goal. Quando um obstáculo for detectado, mude para o comportamento de evitar obstáculos até que o obstáculo não seja mais detectado.

Como se vê, no entanto, essa lógica produzirá muitos problemas. O que este sistema tende a fazer quando encontra um obstáculo é se afastar dele, então, assim que ele se afastar dele, virar de volta e correr para ele novamente. O resultado é um ciclo interminável de comutação rápida que torna o robô inútil. 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