Módulo 2.1: Programação com Python e ROS 2
Módulo 2.1: Programação com Python e ROS 2
Duração estimada: 12-15 horas Nível: Intermediário Pré-requisito: Nível 1 concluído
Objetivos de Aprendizado
Ao final deste módulo, você será capaz de:
- Programar em Python aplicado à robótica
- Configurar ambiente de desenvolvimento Python
- Compreender os conceitos fundamentais de ROS 2
- Criar e executar nodes ROS 2
- Publicar e subscrever topics
- Integrar ROS 2 com simuladores (Webots)
- Controlar um robô humanoide via código Python
Parte 1: Python para Robótica (4-5h)
1.1 Por que Python?
Python é a linguagem dominante em robótica e IA por várias razões:
- Sintaxe simples: Fácil de ler e escrever
- Bibliotecas poderosas: NumPy, OpenCV, TensorFlow, PyTorch
- Comunidade ativa: Muitos exemplos e suporte
- Integração: ROS 2, simuladores, frameworks de IA
1.2 Instalação do Ambiente Python
Windows
# 1. Baixe Python 3.10+ em python.org
# 2. Durante instalação, marque "Add Python to PATH"
# 3. Verifique instalação
python --version
pip --version
# 4. Instale virtualenv
pip install virtualenv
# 5. Crie ambiente virtual para robótica
cd C:\robotica
python -m venv robo_env
# 6. Ative ambiente virtual
robo_env\Scripts\activate
# 7. Instale bibliotecas essenciais
pip install numpy matplotlib scipy
Linux/Mac
# 1. Python já vem instalado, mas instale pip
sudo apt update
sudo apt install python3-pip python3-venv
# 2. Crie ambiente virtual
mkdir -p ~/robotica
cd ~/robotica
python3 -m venv robo_env
# 3. Ative ambiente virtual
source robo_env/bin/activate
# 4. Instale bibliotecas
pip install numpy matplotlib scipy
1.3 Python Básico - Revisão Rápida
Variáveis e Tipos
# Variáveis básicas
velocidade = 0.5 # float - velocidade do robô em m/s
angulo = 90 # int - ângulo em graus
nome_robo = "Bumi" # string
esta_ativo = True # boolean
# Listas - coleções ordenadas
posicao = [0.0, 0.0, 0.5] # [x, y, z]
sensores = ["camera", "lidar", "imu"]
# Dicionários - pares chave-valor
robo_config = {
"nome": "Bumi",
"altura": 1.45,
"sensores": ["camera", "lidar"],
"ativo": True
}
print(f"Robô {robo_config['nome']} tem altura de {robo_config['altura']}m")
Funções
def calcular_distancia(x1, y1, x2, y2):
"""
Calcula distância euclidiana entre dois pontos 2D
Args:
x1, y1: Coordenadas do ponto 1
x2, y2: Coordenadas do ponto 2
Returns:
float: Distância entre os pontos
"""
import math
dx = x2 - x1
dy = y2 - y1
return math.sqrt(dx**2 + dy**2)
# Uso
distancia = calcular_distancia(0, 0, 3, 4)
print(f"Distância: {distancia}m") # Output: 5.0m
Classes - Programação Orientada a Objetos
class RoboHumanoide:
"""Classe para representar um robô humanoide"""
def __init__(self, nome, altura):
"""Construtor - inicializa o robô"""
self.nome = nome
self.altura = altura
self.posicao = [0.0, 0.0, 0.0] # [x, y, z]
self.velocidade = 0.0
self.ativo = False
def ligar(self):
"""Liga o robô"""
self.ativo = True
print(f"{self.nome} foi ligado!")
def andar(self, velocidade):
"""Define velocidade de caminhada"""
if not self.ativo:
print("Erro: Robô está desligado!")
return
self.velocidade = velocidade
print(f"{self.nome} andando a {velocidade}m/s")
def mover_para(self, x, y, z):
"""Move robô para posição específica"""
self.posicao = [x, y, z]
print(f"{self.nome} moveu para posição {self.posicao}")
def status(self):
"""Retorna status atual do robô"""
return {
"nome": self.nome,
"ativo": self.ativo,
"posicao": self.posicao,
"velocidade": self.velocidade
}
# Uso da classe
bumi = RoboHumanoide("Bumi", 1.45)
bumi.ligar()
bumi.andar(0.5)
bumi.mover_para(1.0, 2.0, 0.0)
print(bumi.status())
1.4 NumPy - Matemática para Robótica
import numpy as np
# Arrays - estruturas eficientes para cálculos
posicao = np.array([1.0, 2.0, 0.5]) # Posição 3D
velocidade = np.array([0.5, 0.0, 0.0]) # Velocidade em x
# Operações vetoriais
nova_posicao = posicao + velocidade * 1.0 # dt = 1 segundo
print(f"Nova posição: {nova_posicao}")
# Matrizes de rotação
angulo = np.radians(45) # 45 graus em radianos
matriz_rotacao = np.array([
[np.cos(angulo), -np.sin(angulo), 0],
[np.sin(angulo), np.cos(angulo), 0],
[0, 0, 1]
])
# Rotacionar vetor
vetor_original = np.array([1, 0, 0])
vetor_rotacionado = matriz_rotacao @ vetor_original # @ = multiplicação matricial
print(f"Vetor rotacionado: {vetor_rotacionado}")
# Funções úteis
distancias = np.array([0.5, 1.2, 0.8, 2.1])
print(f"Distância mínima: {np.min(distancias)}")
print(f"Distância média: {np.mean(distancias)}")
print(f"Índice do mínimo: {np.argmin(distancias)}")
1.5 Exercício Prático 1: Simulador de Caminhada
Crie um programa que simula a caminhada do robô:
import numpy as np
import matplotlib.pyplot as plt
class SimuladorCaminhada:
"""Simula caminhada de robô humanoide"""
def __init__(self):
self.posicao = np.array([0.0, 0.0])
self.historico = [self.posicao.copy()]
def dar_passo(self, direcao, tamanho_passo=0.3):
"""
Simula um passo
Args:
direcao: Ângulo em graus (0=frente, 90=esquerda)
tamanho_passo: Comprimento do passo em metros
"""
angulo_rad = np.radians(direcao)
dx = tamanho_passo * np.cos(angulo_rad)
dy = tamanho_passo * np.sin(angulo_rad)
self.posicao += np.array([dx, dy])
self.historico.append(self.posicao.copy())
def caminhar_reta(self, num_passos):
"""Caminha em linha reta para frente"""
for _ in range(num_passos):
self.dar_passo(0) # 0 graus = frente
def girar_e_caminhar(self, angulo, num_passos):
"""Gira e caminha"""
for _ in range(num_passos):
self.dar_passo(angulo)
def plotar_trajetoria(self):
"""Visualiza trajetória do robô"""
historico = np.array(self.historico)
plt.figure(figsize=(10, 10))
plt.plot(historico[:, 0], historico[:, 1], 'b-', linewidth=2, label='Trajetória')
plt.plot(historico[0, 0], historico[0, 1], 'go', markersize=10, label='Início')
plt.plot(historico[-1, 0], historico[-1, 1], 'ro', markersize=10, label='Fim')
plt.xlabel('X (metros)')
plt.ylabel('Y (metros)')
plt.title('Trajetória do Robô')
plt.grid(True)
plt.legend()
plt.axis('equal')
plt.show()
# Teste o simulador
sim = SimuladorCaminhada()
sim.caminhar_reta(10) # 10 passos para frente
sim.girar_e_caminhar(90, 5) # Gira 90° e dá 5 passos
sim.girar_e_caminhar(0, 5) # Volta para frente, 5 passos
sim.plotar_trajetoria()
print(f"Posição final: {sim.posicao}")
Desafio: Modifique o código para fazer o robô andar em um quadrado perfeito!
Parte 2: ROS 2 - Robot Operating System (5-6h)
2.1 O que é ROS 2?
ROS 2 (Robot Operating System 2) é um framework para desenvolvimento de software de robótica. Pense nele como um “sistema nervoso” que conecta todas as partes do robô.
Principais conceitos:
┌─────────────────────────────────────────────┐
│ ROS 2 SYSTEM │
│ │
│ ┌───────┐ Topic ┌───────┐ │
│ │ Node │ ──────────────> │ Node │ │
│ │ A │ /cmd_vel │ B │ │
│ └───────┘ └───────┘ │
│ │ │ │
│ │ │ │
│ └─────── Service ──────────┘ │
│ /start_walk │
└─────────────────────────────────────────────┘
- Node: Processo independente (ex: controle de perna, processamento de câmera)
- Topic: Canal de comunicação unidirecional (ex: velocidade do robô)
- Message: Dados trafegados (ex: velocidade, imagem, posição)
- Service: Requisição/resposta síncrona (ex: “ligue o motor”)
2.2 Instalação ROS 2 Humble
Ubuntu 22.04 (recomendado)
# 1. Setup sources
sudo apt update
sudo apt install software-properties-common
sudo add-apt-repository universe
# 2. Adicionar ROS 2 apt repository
sudo apt update && sudo apt install curl -y
sudo curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key -o /usr/share/keyrings/ros-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/ros-archive-keyring.gpg] http://packages.ros.org/ros2/ubuntu $(. /etc/os-release && echo $UBUNTU_CODENAME) main" | sudo tee /etc/apt/sources.list.d/ros2.list > /dev/null
# 3. Instalar ROS 2 Humble
sudo apt update
sudo apt upgrade
sudo apt install ros-humble-desktop python3-colcon-common-extensions
# 4. Setup ambiente
echo "source /opt/ros/humble/setup.bash" >> ~/.bashrc
source ~/.bashrc
# 5. Verificar instalação
ros2 --help
Windows (WSL2 ou Docker)
Recomendamos usar WSL2 (Windows Subsystem for Linux) e seguir as instruções Ubuntu acima.
2.3 Primeiro Node ROS 2
Crie um workspace ROS 2:
# Criar workspace
mkdir -p ~/ros2_ws/src
cd ~/ros2_ws/src
# Criar pacote Python
ros2 pkg create --build-type ament_python meu_robo_py
cd meu_robo_py
Edite meu_robo_py/primeiro_node.py:
#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
class PrimeiroNode(Node):
"""
Nosso primeiro ROS 2 Node!
"""
def __init__(self):
# Inicializa o node com nome 'primeiro_node'
super().__init__('primeiro_node')
# Cria timer que chama callback a cada 1 segundo
self.timer = self.create_timer(1.0, self.timer_callback)
self.contador = 0
self.get_logger().info('Primeiro Node inicializado!')
def timer_callback(self):
"""Função chamada periodicamente pelo timer"""
self.contador += 1
self.get_logger().info(f'Olá do ROS 2! Contador: {self.contador}')
def main(args=None):
# Inicializa ROS 2
rclpy.init(args=args)
# Cria instância do node
node = PrimeiroNode()
# Mantém node rodando
try:
rclpy.spin(node)
except KeyboardInterrupt:
pass
# Cleanup
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
Configurar setup.py:
entry_points={
'console_scripts': [
'primeiro_node = meu_robo_py.primeiro_node:main',
],
},
Build e execute:
cd ~/ros2_ws
colcon build --packages-select meu_robo_py
source install/setup.bash
ros2 run meu_robo_py primeiro_node
2.4 Publisher e Subscriber
Publisher - Publicando velocidade do robô
#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from geometry_msgs.msg import Twist
class RoboPublisher(Node):
"""
Publica comandos de velocidade para o robô
"""
def __init__(self):
super().__init__('robo_publisher')
# Cria publisher no topic /cmd_vel
self.publisher = self.create_publisher(Twist, '/cmd_vel', 10)
# Timer para publicar a cada 0.5 segundos
self.timer = self.create_timer(0.5, self.publicar_velocidade)
self.velocidade_linear = 0.0
self.velocidade_angular = 0.0
self.get_logger().info('Publisher de velocidade iniciado!')
def publicar_velocidade(self):
"""Publica mensagem de velocidade"""
msg = Twist()
# Define velocidades
msg.linear.x = self.velocidade_linear # Frente/trás
msg.linear.y = 0.0 # Esquerda/direita (robô humanoide não usa)
msg.linear.z = 0.0 # Cima/baixo
msg.angular.x = 0.0
msg.angular.y = 0.0
msg.angular.z = self.velocidade_angular # Rotação
self.publisher.publish(msg)
self.get_logger().info(f'Publicado: linear={msg.linear.x:.2f}, angular={msg.angular.z:.2f}')
# Varia velocidade (exemplo)
self.velocidade_linear += 0.1
if self.velocidade_linear > 1.0:
self.velocidade_linear = 0.0
def main(args=None):
rclpy.init(args=args)
node = RoboPublisher()
try:
rclpy.spin(node)
except KeyboardInterrupt:
pass
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
Subscriber - Lendo dados de sensor
#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from sensor_msgs.msg import LaserScan
class SensorSubscriber(Node):
"""
Subscreve dados de LIDAR e detecta obstáculos
"""
def __init__(self):
super().__init__('sensor_subscriber')
# Subscreve ao topic /scan (LIDAR)
self.subscription = self.create_subscription(
LaserScan,
'/scan',
self.callback_lidar,
10
)
self.get_logger().info('Subscriber de LIDAR iniciado!')
def callback_lidar(self, msg):
"""
Callback chamado quando nova mensagem LIDAR chega
Args:
msg: LaserScan message com dados do LIDAR
"""
# Pega distâncias do LIDAR
ranges = msg.ranges
# Encontra menor distância (obstáculo mais próximo)
min_dist = min([r for r in ranges if r > 0]) # Ignora 0s (inválidos)
if min_dist < 0.5: # Menos de 50cm
self.get_logger().warn(f'OBSTÁCULO PRÓXIMO! Distância: {min_dist:.2f}m')
else:
self.get_logger().info(f'Caminho livre. Mínimo: {min_dist:.2f}m')
def main(args=None):
rclpy.init(args=args)
node = SensorSubscriber()
try:
rclpy.spin(node)
except KeyboardInterrupt:
pass
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
2.5 Exercício Prático 2: Controle Básico
Crie um node que integra publisher e subscriber para controle reativo:
#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from geometry_msgs.msg import Twist
from sensor_msgs.msg import LaserScan
class ControleReativo(Node):
"""
Controle reativo: robô anda e desvia de obstáculos
"""
def __init__(self):
super().__init__('controle_reativo')
# Publisher de velocidade
self.cmd_vel_pub = self.create_publisher(Twist, '/cmd_vel', 10)
# Subscriber de LIDAR
self.scan_sub = self.create_subscription(
LaserScan,
'/scan',
self.callback_scan,
10
)
# Timer para controle
self.timer = self.create_timer(0.1, self.controlar) # 10 Hz
self.obstaculo_proximo = False
self.distancia_minima = float('inf')
self.get_logger().info('Controle reativo iniciado!')
def callback_scan(self, msg):
"""Processa dados do LIDAR"""
# Pega apenas leituras frontais (ex: 30 graus para cada lado)
num_leituras = len(msg.ranges)
angulo_frontal = 30 # graus
# Calcula índices das leituras frontais
indice_inicio = num_leituras // 2 - angulo_frontal
indice_fim = num_leituras // 2 + angulo_frontal
leituras_frente = msg.ranges[indice_inicio:indice_fim]
# Filtra leituras válidas
leituras_validas = [r for r in leituras_frente if r > 0]
if leituras_validas:
self.distancia_minima = min(leituras_validas)
self.obstaculo_proximo = self.distancia_minima < 1.0 # 1 metro
else:
self.obstaculo_proximo = False
def controlar(self):
"""Lógica de controle"""
msg = Twist()
if self.obstaculo_proximo:
# Obstáculo detectado - para e gira
msg.linear.x = 0.0
msg.angular.z = 0.5 # Gira para esquerda
self.get_logger().warn(f'Obstáculo a {self.distancia_minima:.2f}m - Girando!')
else:
# Caminho livre - anda para frente
msg.linear.x = 0.3 # 0.3 m/s
msg.angular.z = 0.0
self.get_logger().info('Caminho livre - Andando...')
self.cmd_vel_pub.publish(msg)
def main(args=None):
rclpy.init(args=args)
node = ControleReativo()
try:
rclpy.spin(node)
except KeyboardInterrupt:
pass
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
Parte 3: Integração ROS 2 + Webots (3-4h)
3.1 Instalação Webots com ROS 2
# Instalar Webots
sudo apt install wget
wget https://github.com/cyberbotics/webots/releases/download/R2023b/webots_2023b_amd64.deb
sudo apt install ./webots_2023b_amd64.deb
# Instalar pacotes ROS 2 para Webots
sudo apt install ros-humble-webots-ros2
3.2 Primeiro Robô no Webots com ROS 2
Crie arquivo meu_mundo.wbt (mundo Webots):
#VRML_SIM R2023b utf8
EXTERNPROTO "https://raw.githubusercontent.com/cyberbotics/webots/R2023b/projects/objects/backgrounds/protos/TexturedBackground.proto"
EXTERNPROTO "https://raw.githubusercontent.com/cyberbotics/webots/R2023b/projects/objects/floors/protos/Floor.proto"
WorldInfo {
}
Viewpoint {
position 0 -3 2
orientation 0.5 0.5 0.7 1.8
}
TexturedBackground {
}
Floor {
size 10 10
}
Robot {
name "meu_robo"
translation 0 0 0.5
children [
Shape {
geometry Box {
size 0.3 0.3 0.5
}
}
HingeJoint {
device [
RotationalMotor {
name "motor_esquerdo"
}
]
}
HingeJoint {
device [
RotationalMotor {
name "motor_direito"
}
]
}
]
controller "<extern>"
}
Controller ROS 2:
#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from geometry_msgs.msg import Twist
from webots_ros2_driver.webots_controller import WebotsController
class WebotsRoboController(WebotsController):
"""Controlador Webots integrado com ROS 2"""
def __init__(self):
super().__init__('webots_controller')
# Subscreve comandos de velocidade
self.cmd_vel_sub = self.create_subscription(
Twist,
'/cmd_vel',
self.callback_cmd_vel,
10
)
# Pega referências dos motores Webots
self.motor_esq = self.robot.getDevice('motor_esquerdo')
self.motor_dir = self.robot.getDevice('motor_direito')
# Configura motores para controle de velocidade
self.motor_esq.setPosition(float('inf'))
self.motor_dir.setPosition(float('inf'))
self.motor_esq.setVelocity(0.0)
self.motor_dir.setVelocity(0.0)
self.get_logger().info('Webots controller iniciado!')
def callback_cmd_vel(self, msg):
"""Converte Twist para velocidades de motor"""
linear = msg.linear.x
angular = msg.angular.z
# Cinemática diferencial simplificada
vel_esq = linear - angular
vel_dir = linear + angular
self.motor_esq.setVelocity(vel_esq)
self.motor_dir.setVelocity(vel_dir)
self.get_logger().info(f'Motores: E={vel_esq:.2f}, D={vel_dir:.2f}')
def main(args=None):
rclpy.init(args=args)
controller = WebotsRoboController()
try:
rclpy.spin(controller)
except KeyboardInterrupt:
pass
controller.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
3.3 Executando
# Terminal 1: Inicia Webots com mundo
webots meu_mundo.wbt
# Terminal 2: Inicia controller ROS 2
source ~/ros2_ws/install/setup.bash
ros2 run meu_robo_py webots_controller
# Terminal 3: Publica comandos de velocidade
ros2 topic pub /cmd_vel geometry_msgs/msg/Twist "{linear: {x: 0.5}, angular: {z: 0.0}}"
Mini-Projeto: Robô que Segue Parede
Crie um node que faz o robô seguir uma parede usando LIDAR:
#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from geometry_msgs.msg import Twist
from sensor_msgs.msg import LaserScan
import numpy as np
class SeguidorParede(Node):
"""
Robô que segue parede usando LIDAR
"""
def __init__(self):
super().__init__('seguidor_parede')
self.cmd_vel_pub = self.create_publisher(Twist, '/cmd_vel', 10)
self.scan_sub = self.create_subscription(LaserScan, '/scan', self.callback_scan, 10)
self.distancia_desejada = 0.5 # 50cm da parede
self.velocidade_linear = 0.3
self.get_logger().info('Seguidor de parede iniciado!')
def callback_scan(self, msg):
"""Processa LIDAR e calcula controle"""
ranges = np.array(msg.ranges)
# Divide LIDAR em regiões
num_leituras = len(ranges)
# Lado direito (assumimos parede à direita)
lado_dir = ranges[0:num_leituras//4]
# Frente
frente = ranges[num_leituras//2 - 10:num_leituras//2 + 10]
# Calcula distâncias médias
dist_direita = np.mean([r for r in lado_dir if 0 < r < 10])
dist_frente = np.min([r for r in frente if 0 < r < 10])
# Lógica de controle
msg_vel = Twist()
# Controle proporcional para seguir parede
erro = dist_direita - self.distancia_desejada
kp = 2.0 # Ganho proporcional
if dist_frente < 0.7:
# Obstáculo à frente - gira à esquerda
msg_vel.linear.x = 0.0
msg_vel.angular.z = 0.5
self.get_logger().warn('Obstáculo à frente!')
else:
# Segue parede
msg_vel.linear.x = self.velocidade_linear
msg_vel.angular.z = -kp * erro # Negativo para corrigir
self.get_logger().info(f'Distância direita: {dist_direita:.2f}m, Erro: {erro:.2f}')
self.cmd_vel_pub.publish(msg_vel)
def main(args=None):
rclpy.init(args=args)
node = SeguidorParede()
try:
rclpy.spin(node)
except KeyboardInterrupt:
pass
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
Recursos Adicionais
Documentação
Tutoriais
Comunidade
Troubleshooting - Erros Comuns
Erro: “colcon: command not found”
# Solução
sudo apt install python3-colcon-common-extensions
Erro: “Package not found” ao fazer colcon build
# Solução: limpar workspace e rebuildar
cd ~/ros2_ws
rm -rf build install log
colcon build
Erro: Topic não aparece com ros2 topic list
# Verificar se node está rodando
ros2 node list
# Verificar se domínio ROS_DOMAIN_ID é o mesmo
echo $ROS_DOMAIN_ID
Erro: “LIDAR não retorna dados”
Verifique se o sensor está habilitado no mundo Webots e se o topic name está correto.
Checklist de Conclusão
- Ambiente Python configurado com virtualenv
- ROS 2 Humble instalado e funcionando
- Primeiro node ROS 2 criado e executado
- Publisher de velocidade implementado
- Subscriber de sensor implementado
- Integração Webots + ROS 2 funcionando
- Mini-projeto “Seguidor de Parede” concluído
- Código versionado no GitHub
Próximo Módulo
No Módulo 2.2: Sensores e Controle, você vai:
- Trabalhar com diferentes tipos de sensores
- Processar imagens com OpenCV
- Implementar controle PID
- Criar comportamentos mais complexos
Dúvidas? Participe da comunidade FTH no Discord ou abra uma issue no GitHub.
Última atualização: 2025-10-29