Sobre o projeto

O b166er é o robô móvel de manipulação no centro da minha pesquisa de doutorado: uma base móvel Pioneer 3-AT combinada com um braço robótico Mitsubishi RV-M2, integrados sob um único mestre ROS Noetic. Cada parte chegou com seu próprio controlador, pensado para operar isolado — a integração é o trabalho de fazer os dois conversarem em tempo real, no mesmo robô.
A arquitetura usa duas máquinas. A Shiroi é o notebook de desenvolvimento, de onde saem a programação e a visualização em RViz. A NUC (Intel NUC 5i5RYH, identificada na rede como b166er-nuc.local) é o computador embarcado no robô: hospeda o mestre ROS e fala diretamente com o hardware — a base Pioneer por USB-serial, o braço Mitsubishi via Arduino com rosserial, e uma IMU Sparton AHRS-8, também por USB-serial. O ROS Noetic em ambas as máquinas é fornecido pelo RoboStack, não por uma instalação nativa de Ubuntu — uma escolha que evita travar o projeto numa única versão de sistema operacional.
Lições aprendidas
Nem toda lição aprendida nessa integração é sobre o robô em si. Boa parte é sobre o ambiente de desenvolvimento que sustenta o projeto — e quando uma dessas lições cresce o suficiente para virar um tutorial completo, ela ganha uma página técnica própria, linkada a partir daqui:
- RoboStack: instale o ROS1 sem Docker, em qualquer sistema — como o b166er usa RoboStack para rodar ROS1 Noetic via Conda, em Linux e fora dele, sem Docker e sem dual-boot.
- ZeroTier: acesse o robô de qualquer lugar sem VPN dedicada — como conectar o notebook de desenvolvimento ao NUC embarcado via ROS, independente de estar no laboratório.
04 Ago 2026
O Bot de Review Quebrado e o Billing Separado do Claude Pro
O repositório do b166er tem um workflow de GitHub Actions que dispara uma revisão automática de código em toda PR — funcionou de forma consistente 14 vezes seguidas ao longo de julho, sempre em 30-45 segundos, e então quebrou do nada na PR que fechava o trabalho de ontem, com um erro genérico: Claude execution failed: result is_error:true. O log padrão do workflow não dizia mais que isso, e a primeira hipótese razoável — algo na configuração do plugin de revisão — não batia com a evidência: o log mostrava o plugin instalando com sucesso, a falha acontecia depois, durante a execução em si.
Em vez de trocar configuração às cegas, o caminho foi instrumentar antes de corrigir: liguei show_full_output temporariamente no workflow (numa PR própria, separada, para não misturar debug de CI com o trabalho de controle do robô) e rodei de novo. O log completo revelou a causa real — "error": "authentication_failed", HTTP 401, API key is invalid — depois de nove tentativas de autenticação ao longo de 3 minutos e meio antes de desistir. A chave de API configurada como secret do repositório, funcional havia um mês, simplesmente parou de autenticar.
Troquei a chave. O erro mudou, mas não sumiu: agora "error": "billing_error", HTTP 400, Credit balance is too low. Isso expôs uma confusão legítima — o Claude Pro (a assinatura usada para conversar comigo) e a API da Anthropic (o que esse workflow de CI efetivamente consome) são dois produtos com contas e billing completamente independentes. Ter uma assinatura Pro ativa não dá nenhum crédito de API; são dois medidores diferentes, cobrando por coisas diferentes. As 14 execuções anteriores provavelmente foram cobertas por um crédito de teste inicial da conta de API, esgotado silenciosamente sem nenhum aviso até a chave girar (ou ser revogada) e o próximo erro aparecer como autenticação, não como billing — dois sintomas em sequência escondendo a mesma causa raiz.
Com a conta de API carregada com crédito de verdade, uma execução chegou a custar US$1,39 em tokens — confirmação de que a chamada estava acontecendo de fato, não mais uma tentativa fantasma. Mas surgiu um terceiro comportamento, esse sem solução simples: o plugin de revisão dispara múltiplos sub-agentes em paralelo para escanear bugs, e na PR mais volumosa do lote (seis commits tocando vários pacotes de uma vez) esses sub-agentes ainda estavam rodando quando o tempo do job esgotou — sem erro, sem comentário publicado, um silêncio inconclusivo. PRs menores e de assunto único continuaram funcionando normalmente. A correção definitiva foi adicionar continue-on-error: true ao step do bot: uma ferramenta consultiva de revisão nunca deveria travar o pipeline de CI, seja qual for o motivo da falha — o repositório nem tem proteção de branch configurada, então esse erro nunca bloqueou um merge, mas o ruído de um check vermelho sem explicação clara custa atenção que não precisa ser gasta.
Fechei o dia documentando algo que nunca tinha sido escrito: o papel da branch local-state neste projeto. Ela não é uma feature branch de uso único — é reaproveitada sessão após sessão, sincronizada com main entre rodadas de trabalho em vez de recriada, já responsável por mais de 20 PRs ao longo do projeto. Uma convenção que sempre existiu na prática, mas só hoje ganhou uma linha no README.
03 Ago 2026
Bancada Bloqueada, Manobra Girar-Avançar-Girar em Simulação
A sessão começou tentando retomar de onde a bancada tinha parado — e esbarrou em dois bloqueios físicos antes de qualquer linha de código. A NUC não respondeu nem por mDNS (b166er-nuc.local) nem pela rota de fallback via ZeroTier: sem acesso, sem diagnóstico possível além do que já estava registrado. E o problema elétrico na Joint 1 (motor não gira mesmo com comunicação e firmware validados) segue exigindo multímetro na bancada, não isso aqui. Diante dos dois bloqueios, o caminho certo era migrar o trabalho para onde ainda havia terreno produtivo: a simulação, especificamente a lacuna que a Fase 3 tinha deixado propositalmente em aberto — o robô sem encoder de junta precisa de uma solução Fuzzy que lide com a base não-holonômica, e essa solução nunca tinha sido testada contra um cenário fiel ao hardware real.
Isso expôs um bug que estava dormindo desde a decisão arquitetural de 01 de julho. O state_estimator (que estima os ângulos de junta via IK a partir da T265 e da odometria do Pioneer, sem encoder) tinha um atalho: usar /joint_states real como semente do IK quando disponível, pensado como otimização inofensiva de Gazebo. O comentário no código dizia que, em hardware, esse tópico simplesmente “não existe” — só que isso deixou de ser verdade quando o modo hardware do launch unificado passou a subir o movemaster_control/state_publisher, o mesmo nó cujo pulse_count a bancada de ontem provou ser ruído sem encoder algum. O state_estimator estaria aceitando esse ruído como semente de alta confiança. A correção: o atalho vira opt-in (use_joint_states_seed, default false), e sem ele o sistema sempre usa sua própria estimativa recursiva do ciclo anterior — a mesma informação que o robô real vai ter disponível. Validado em Gazebo: convergência estável, sem regressão.
Com a estimativa honesta, sobrava encarar a limitação lateral em si: um alvo que exige movimento perpendicular ao heading do Pioneer nunca converge, porque a Jacobiana resolve a base como se ela pudesse se mover livremente em x e y (holonômica), e só depois da resolução uma projeção descarta a componente que a base skid-steer não consegue realizar de verdade. Entre as três correções possíveis — relaxar a orientação transitoriamente, empurrar o heading pelo espaço nulo, ou uma manobra discreta de girar-avançar-girar — a escolhida foi a manobra discreta, motivada por um requisito de projeto ainda mais à frente: tarefas futuras vão exigir o braço aplicar força contra resistência mecânica (romper uma trava), e só a manobra discreta cria um ponto de parada determinístico antes do contato — as outras duas corrigem de forma contínua, sem nenhum instante em que se possa afirmar “agora está alinhado o suficiente para empurrar”.
A implementação revelou dois bugs que só apareceram testando contra a física real do Gazebo, não em teoria. A primeira versão usava a direção crua da solução da Jacobiana como referência de giro — instável, porque o braço parado durante o giro continua preso rigidamente à base: girar a base faz o braço orbitar em torno do centro de giro, deslocando a “direção ótima” a cada ciclo de controle. O controlador perseguia um alvo que se movia por causa do próprio giro, sem nunca convergir. A correção trocou essa referência pelo rumo (bearing) até a posição XY fixa do alvo, geometricamente estável enquanto a base só gira no lugar. Mesmo assim, um segundo problema apareceu com alvos bem laterais: girar “no lugar” numa base skid-steer não é perfeito — o atrito de contato do motor de física do Gazebo desliza o suficiente para deslocar a posição real durante giros longos, e o rumo até o alvo muda mais rápido do que o giro consegue alcançar. A correção foi um teto de tempo: se o alinhamento não converge dentro de 8 segundos, desiste e libera o avanço em vez de girar para sempre — o mesmo efeito físico deve se repetir em hardware real, então o teto não é só uma gambiarra de simulação.
Enquanto validava a manobra visualmente, uma lacuna separada apareceu: nada publicava a pose real do Pioneer como TF, então o RViz sempre desenhava o robô plantado na origem, mesmo com a base andando de verdade no Gazebo — só o braço, via /joint_states, animava. A fonte do dado já existia (gazebo_sensor_sim já lia a pose ground-truth do /gazebo/model_states para montar /pioneer/pose); faltava só transmitir essa mesma pose como TF. Corrigido, e junto veio uma conversa sobre segurança que rendeu um item novo de ROADMAP: romper uma trava com o braço muda o perfil de risco do robô para tombamento, e existe um protótipo órfão no repositório (stability_controller.py, IMU → fator de estabilidade → parada de emergência) que nunca foi integrado ao stack atual — e mesmo integrado, não resolveria tudo, porque o robô não tem nenhum sensor de força: aplicar força contra uma trava hoje só pode ser feito às cegas.
07 Jul 2026
Primeira Tentativa de Calibração Hand-Eye na Bancada
O primeiro obstáculo do dia nem era do robô — era do ambiente. Subir o bringup da bancada (braço + T265 + detector de AprilTag) matava os três nós Arduino_1/2/3.py na largada, com ModuleNotFoundError: No module named 'imp'. A causa: o pacote conda ros-noetic-rosserial-python (canal robostack-noetic) ainda faz import imp, um módulo removido de vez do Python 3.12 — mesma classe de problema da correção do gazebo-ros de uma semana atrás, mas dessa vez sem build corrigido disponível no canal para eu simplesmente atualizar a versão. A solução foi um shim mínimo: o pacote só usa uma função do módulo (imp.find_module, só para checar se um pacote existe antes de importar), então um arquivo imp.py de 20 linhas reimplementando só essa função via importlib.util.find_spec, carregado na frente do PYTHONPATH, bastou.
Com o ambiente de pé, o objetivo do dia era começar a calibração hand-eye entre a T265 (montada no flange do braço) e o RV-M2 — o passo que destrava tanto a medição de erro real quanto o ajuste fino de PID e do Fuzzy em hardware. Escrevi um nó de coleta assumindo o desenho mais natural: perturbar o braço em pequenos passos em torno da pose atual, usar a TF Base→L5 do robot_state_publisher como a pose do flange, e o /b166er/tag_pose do detector de AprilTag como a pose da câmera — os dois lados do problema clássico AX=XB (Tsai-Lenz). Na bancada, esse desenho não sobreviveu ao primeiro contato com o hardware real.
O RV-M2 não tem mais encoder de junta — decisão de arquitetura já documentada aqui em 01 de julho, quando a estimativa de estado passou a vir da T265 e da odometria do Pioneer via IK, não de sensor de junta. O que eu não tinha medido ainda era a consequência prática dessa ausência para o firmware original de cada junta (src/arduino/Joints*.ino, fora do workspace catkin — não são pacotes ROS, só sketches). O controlador de cada placa calcula error = encoder_count - setpoint; sem encoder real alimentando encoder_count, esse erro nunca converge, o IsDone published nunca fica confiável, e a TF que o robot_state_publisher deriva do pulse_count publicado passou a refletir ruído de um pino flutuando, não a pose real do braço — inviabilizando ao mesmo tempo o assentamento por IsDone e a FK usada como verdade de calibração.
Ler o firmware por inteiro valeu a pena por outro motivo: toda placa dispara sozinha, na primeira conexão rosserial, uma rotina de homing (GoHome()) limitada por fim-de-curso físico — sem isso documentado em lugar nenhum, um fim-de-curso ausente teria significado o motor sendo empurrado indefinidamente atrás de um sensor inexistente. Confirmados os fins-de-curso presentes e funcionando, o homing automático é seguro. A leitura também expôs uma discrepância que ainda não tem resposta: as constantes HOME_X do firmware não são todas zero (HOME_2 = 106, HOME_3 = -123, em grau bruto de firmware), e não está validado se essa convenção bate com o q cinemático de kinematics.py usado pelo arm_vel_integrator — item que o ROADMAP já listava como não validado em hardware, e que segue em aberto.
A estratégia de coleta foi replanejada em campo: sem posição intermediária confiável, os únicos pontos com verdade física são o HOME (confirmado visualmente pelo operador contra as marcas de posicionamento do próprio manipulador, não só pela rotina automática do firmware) e cada fim-de-curso mecânico, saturando o setpoint muito além do curso da junta e assumindo o ângulo de catálogo já presente em JOINT_LOWER/JOINT_UPPER. A FK desses pontos de ancoragem passa a ser calculada direto de kinematics.py, não mais da TF. O dia fechou sem a coleta em si: religada a alimentação dos motores (uma bateria efetivamente descarregada explicou o primeiro round de “nada se move”), o firmware voltou a responder normalmente — mensagem chegando, malha de controle publicando status a ~19,5 Hz, erro calculado corretamente — mas a Joint 1 não girou fisicamente num comando real. Comunicação e lógica validadas de ponta a ponta isolam o problema na camada elétrica (driver, alimentação dos motores ou pino de enable), debug de bancada com multímetro que fica para a próxima sessão.
06 Jul 2026
Fase 3 Concluída e AprilTag Validado com a T265 Real
O braço estava tremendo. Depois do warm-start, o RV-M2 nunca parava de oscilar em torno do equilíbrio — um sintoma que parecia de ganho mal ajustado, mas a causa era mais fundamental: os PIDs de posição do ros_control não têm nenhuma noção de gravidade. Eles corrigem erro de posição, e só. Um braço horizontal ou inclinado tem torque gravitacional constante puxando cada junta para baixo, e um controlador que só reage ao erro depois que ele aparece está sempre um passo atrás da física. A correção foi um feedforward: gravity_torque_arm(q) calcula o torque gravitacional em cada junta a partir das massas do manual Mitsubishi, e o gazebo_arm_bridge soma esse torque ao comando de posição antes de mandar para o PID — o controlador deixa de brigar com a gravidade e passa a compensá-la preventivamente. Ajustar os ganhos por junta e resolver uma sequência load → unpause → switch que travava em deadlock quando o Gazebo estava pausado fechou essa frente.
O segundo sintoma era visual: no RViz, as quatro rodas do Pioneer apareciam colapsadas na origem, como um disco branco sobre o chassi. A causa raiz era boba e reveladora ao mesmo tempo — o código chamava rospy.WallRate, uma classe que não existe no ROS 1 (existe rospy.Rate e existe rospy.Duration, mas não essa combinação). O erro matava o nó pioneer_wheel_state_pub silenciosamente depois da primeira publicação, e o robot_state_publisher nunca mais recebia TF novo para as rodas. Reescrever o loop com time.sleep() em wall-time — imune a pausas do use_sim_time — resolveu de vez.
O terceiro problema foi o mais difícil de diagnosticar: o robô inteiro deslizava sozinho pelo Gazebo, sem nenhum comando de velocidade sendo enviado. O diagnóstico ao vivo, olhando /gazebo/link_states em tempo real, revelou um ciclo-limite de contato: a rigidez do contato das rodas estava subamortecida (kd=100 para um chassi de ~41 kg, resultando em ζ ≈ 1,6%), o robô entrava numa queda-e-recuperação perpétua contra o chão (vz ≈ -0,08 m/s de oscilação vertical), e o braço horizontal retificava essa vibração vertical em avanço horizontal contínuo — como um motor de vibração transformando ruído em deslocamento líquido. Subir o amortecimento (kd 100 → 1e4) quebrou o ciclo. Com isso resolvido, um piso de velocidade mínima da base (10 mm/s, contra micro-patinagem no contato ODE) e uma histerese no critério de parada do Fuzzy fecharam o ajuste fino. A validação final: amplitude de oscilação residual de 0,0002 rad/s, IK convergindo por mais de 60 segundos contínuos sem warning, e erro de rastreamento do end-effector de 4,7 mm contra um alvo estático — dentro do critério de 5 mm. A PR #20 foi mesclada, e a Fase 3 está oficialmente concluída, com uma limitação documentada e propositalmente deixada em aberto: alvos com erro puramente lateral estacionam a ~99 mm, porque a projeção não-holonômica da base não gera esterçamento para corrigir erro perpendicular ao heading — a estratégia de manobra fica para uma decisão futura de projeto.
Com a Fase 3 fechada, o ROADMAP das fases seguintes ganhou forma mais concreta: a Fase 4 absorveu o bringup completo da T265 (odometria + visão), a Fase 5 detalhou o Hokuyo UST-05LX para navegação, e ficou registrada a decisão de manter o Gazebo como simulador principal do projeto, com o PyBullet reservado como bancada auxiliar de sintonia rápida — não uma substituição, mas uma ferramenta complementar para iterar ganhos sem pagar o custo de subir o stack ROS completo a cada teste.
A peça que faltava para fechar a Fase 4 de visão era escolher a fonte do erro visual da servovisão. A T265 é a única RealSense do robô e não tem câmera de profundidade — só duas fisheye de ampla abertura (163° de FOV) e uma IMU. A resposta foi construir o pacote b166er_vision: um núcleo puro NumPy/OpenCV, testável sem ROS, com um nó fino por cima. A primeira armadilha apareceu na retificação: a função padrão do OpenCV para gerar uma câmera pinhole virtual a partir de uma fisheye (estimateNewCameraMatrixForUndistortRectify) degenera com o FOV da T265 — o fx calculado colapsava de 285 para 58, encolhendo os AprilTags na imagem até ficarem indetectáveis. A correção foi construir a pinhole virtual manualmente a partir do fx real da fisheye, sem deixar o OpenCV “otimizar” o resultado. Com a retificação estável, o detector de AprilTag 36h11 resolve a pose via solvePnPGeneric (IPPE_SQUARE), escolhendo entre as duas soluções possíveis a de menor erro de reprojeção e expondo a razão entre elas como pose_ambiguity — um sinal de alerta para quando a tag está quase de frente para a câmera, caso em que a posição continua confiável mas a orientação pode vir do ramo errado da ambiguidade. Seis testes sintéticos (renderizando uma tag em pose 3D conhecida e sintetizando a distorção fisheye inversa) validaram o pipeline inteiro sem precisar de ROS nem de hardware: erro de posição menor que 2% e de orientação menor que 3° em três poses distintas.
O teste que importava, porém, era com a câmera de verdade. Bancada montada no shiroi: T265 real, tag 36h11 de 132 mm impresso e exibido em tela, trena para medir a distância de referência. No ponto de calibração — tag centralizado no eixo óptico, a 33,8 cm — o pipeline estimou 33,3 cm: um erro de −1,5%, dentro da própria incerteza da trena, sem viés sistemático de escala. A 60 cm o erro subiu para +2,0%; a 147 cm, para +5,7%, com frames começando a se perder (a taxa caiu de 30 Hz para ~24 Hz). O padrão revelou uma regra prática que não estava óbvia antes do teste: o erro de escala é dominado pela resolução da tag em pixels, não pela distância em si — quanto menor a tag na imagem, maior o viés de fração de pixel nos cantos detectados (agravado pelo bloom da tela de exibição em uma câmera monocromática). A regra que ficou registrada no ROADMAP: lado da tag ≥ d/11, onde d é a distância de operação — um tag de 13 cm serve até ~1 m; para 1,5 m, é preciso imprimir 20 cm ou mais.
01 Jul 2026
IK Sincronizado, Braço Sem Encoders Documentado e Fase 2 Concluída
O dia começou com uma falsa esperança: o controlador whole-body subia, o Gazebo rodava, o end-effector se movia — mas a IK que estima os ângulos de junta a partir da câmera T265 não convergia. O resíduo de posição era de 0,24 metros. Não um erro numérico pequeno que tolerância mais generosa resolveria: 24 centímetros é uma falha de diagnóstico.
O culpado foi um problema de sincronização de timestamps que só existe em simulação. O nó gazebo_sensor_sim calcula a cinemática direta FK(q_t1) e publica a pose do T265 com o stamp t1 da mensagem de joint_states que usou. Enquanto isso, o state_estimator mantinha apenas o q mais recente na memória — que pode ter chegado em t2 > t1, com o braço já em outra posição. O IK tentava resolver a cinemática inversa de uma pose que correspondia a q_t1, mas usava como ponto de partida q_t2. Com o braço oscilando, a diferença |q_t1 - q_t2| era grande o suficiente para o algoritmo DLS divergir.
A correção foi cirúrgica: um buffer deque(maxlen=50) de pares (timestamp_em_segundos, q) no estimador. A cada ciclo, em vez de usar o q mais recente, o sistema procura no buffer o q cujo stamp é mais próximo do stamp do T265. Em simulação, onde o sensor_sim usa exatamente o stamp do joint_states, a diferença é zero — o IK recebe o seed exato e converge em zero iterações. O resíduo caiu de 0,24 m para 0,0015 m.
O segundo bug era mais visível: o RViz mostrava o Pioneer com apenas uma roda abaixo da plataforma, como se o modelo estivesse incompleto. O culpado era o pioneer_wheel_state_pub, que publicava mensagens antes do relógio de simulação estar ativo. Com use_sim_time=true, rospy.Time.now() retorna zero até a primeira mensagem chegar no tópico /clock. O robot_state_publisher recebia transformadas com stamp t=0 em loop, descartava tudo com o aviso TF_REPEATED_DATA, e as quatro rodas nunca apareciam. A correção: substituir o sleep de ROS por time.sleep() da stdlib Python — que usa o relógio real do sistema operacional independente do tempo de simulação — e aguardar até que rospy.Time.now() retorne um valor não-zero antes de começar a publicar.
Com a IK estável, a primeira tentativa de enviar um alvo revelou um terceiro problema: a pose z=0.35 m no frame odom colocava o end-effector quase 40 cm abaixo do ombro do braço, que fica a 0,74 m de altura. O braço precisaria cruzar o chassi do Pioneer para chegar lá — e tentava. O espaço de trabalho seguro começa em z ≥ 0,60 m. Para evitar que o braço caísse dentro do chassi nos 0,5 segundos que os controladores ros_control levam para carregar após o spawn, o launch passou a iniciar com J2=0.5 rad — ombro já elevado, esperando os controladores.
Com esses três ajustes, o pipeline end-to-end convergiu de forma limpa: o ee_target entra, o estimador de estado retorna q_est, o controlador Fuzzy calcula cmd_vel e arm_vel_cmd, o bridge integra velocidade em posição, e o Gazebo move o braço sem warnings. A PR #18 foi mesclada.
O restante do dia foi documentação. Escrevi um documento técnico completo explicando como o braço RV-M2 opera sem encoders de junta — o mecanismo de fechar a malha via T265 no espaço cartesiano, a IK com seed sincronizado por timestamp, a lei de controle whole-body com pseudoinversa amortecida e o diagrama de blocos completo do sistema, com o Fuzzy Mamdani como peça central. O README recebeu a tabela de pacotes, o launch unificado e a arquitetura resumida. O ROADMAP foi reescrito com seis fases claras, incluindo a redução do shaking do braço como próxima etapa — um problema esperado porque os PIDs de posição do ros_control não têm compensação de gravidade.
30 Jun 2026
Whole-Body Control: Pioneer e RV-M2 como um Único Sistema
A pergunta que organizou o trabalho de hoje foi simples mas consequente: o que significa controlar um manipulador móvel? A resposta óbvia — e errada — é sequencial: a base navega até perto do objeto, para, e então o braço age. Isso existe em abundância na literatura e em produtos comerciais. Não é o que estou construindo. O que me interessa é o caso em que a base e o braço se movem ao mesmo tempo, coordenados por um único planejador que trata os dois como partes de um mesmo sistema, sem distinção de quem navega e quem manipula. O objetivo da tarefa é a pose do end-effector no mundo — como chegar lá, usando as oito juntas disponíveis (três da base, cinco do braço), é problema do controlador, não meu.
O problema de controle que emerge dessa escolha tem um nome: whole-body control. O sistema tem oito graus de liberdade para satisfazer uma tarefa de seis (posição e orientação do end-effector), o que o torna redundante — sobram dois graus de liberdade que podem ser usados para otimizar objetivos secundários, como manter as juntas longe dos seus limites mecânicos. A lei de controle que implementei resolve isso com álgebra linear padrão: calcula a pseudoinversa amortecida da Jacobiana completa do sistema (6×8) para encontrar a velocidade das juntas que corrige o erro do end-effector, e projeta um objetivo secundário no espaço nulo para usar a redundância com propósito.
O ponto mais delicado foi estimar o estado do braço sem nenhum encoder — os motores do RV-M2 não retornam posição. A solução veio da combinação de dois sensores que já estão no robô: a T265 no end-effector diz onde o end-effector está no mundo; a odometria do Pioneer diz onde a base está. Com os dois, é possível calcular onde o braço precisa estar para que a cinemática seja consistente — o que é exatamente um problema de cinemática inversa. Ao custo de uma IK numérica a cada ciclo de controle (convergência típica abaixo de 0,1 mm), o sistema obtém uma estimativa dos ângulos de junta sem precisar de encoder algum.
O ganho do controlador foi substituído por um sistema de inferência Fuzzy do tipo Mamdani com três entradas (erro de posição, erro de orientação e variação do erro) e três saídas (ganho de posição, ganho de orientação e amortecimento da pseudoinversa). O efeito prático é que o robô se aproxima rápido quando está longe do alvo e desacelera suavemente ao chegar — e quando o erro começa a aumentar em vez de diminuir, o controlador eleva automaticamente o amortecimento para estabilizar o movimento antes de tentar de novo. Ao final do dia, o stack completo — estimador de estado, planejador whole-body e controlador Fuzzy — sobe com um único comando em dois modos: simulação com RViz e hardware real.
30 Jun 2026
Sensores no Lugar Certo e RViz Sem NUC
O URDF é o modelo que o ROS usa como verdade sobre a geometria do robô — é a partir dele que o sistema calcula onde cada sensor está, o que cada câmera vê e como as juntas do braço se relacionam com o chassi da base. Se os sensores estiverem nas posições erradas nesse arquivo, todo o raciocínio espacial do robô fica comprometido antes mesmo de começar. Hoje o trabalho foi exatamente esse: confrontar o modelo com uma foto do robô real e corrigir o que estava errado.
Três posições estavam imprecisas. A IMU Sparton AHRS-8 estava posicionada dentro do chassi do Pioneer — uma interpretação errada da descrição inicial. A foto mostrou que ela fica na face traseira do primeiro elo do braço, entre a cintura e o ombro, aparafusada diretamente no paralelepípedo azul que é o L1 do RV-M2. O Hokuyo estava flutuando alguns centímetros acima do top_plate, como se houvesse um espaçador inexistente; a correção o colocou assentado diretamente sobre a superfície preta do Pioneer. O braço em si estava deslocado em relação ao centro dos eixos das rodas — a plataforma foi estendida 10 cm na traseira para acomodar a bateria, e o braço fica na plataforma original, não na extensão. O processo foi iterativo: ajustar o arquivo Xacro, relançar o RViz, checar a posição visualmente, repetir.
Isso levantou um problema prático: o RViz só funcionava conectado à NUC, porque o script de ambiente apontava para ela como ROS master. Para desenvolver o URDF localmente, sem cabo e sem laboratório, foi preciso resolver dois obstáculos independentes. O primeiro era uma sessão antiga de Gazebo que deixou o parâmetro use_sim_time ativo no servidor de parâmetros do ROS — isso fazia o robot_state_publisher parar de publicar as transformações, esperando por um relógio de simulação que nunca chegava. A correção entrou direto no launch file do RViz. O segundo era mais fundo: a biblioteca de renderização 3D do RViz (OGRE) procura seus plugins num diretório do ambiente base do conda, mas no ambiente de ROS eles estão em outro lugar. A solução foi criar um link simbólico entre os dois caminhos — uma linha de terminal, mas que exigiu entender por que o OGRE ignora variáveis de ambiente e procura os plugins pelo caminho compilado dentro da biblioteca.
24 Jun 2026
Simulação Unificada e a Caça ao Bug do Gazebo
Até hoje, a simulação do b166er era um Frankenstein: um modelo combinado feito do zero com caixas e cilindros genéricos, sem nenhum sensor, desconectado das malhas 3D reais do Pioneer e do braço RV-M2 que já existiam espalhadas pelo repositório — resquício de tentativas anteriores que nunca foram unificadas. O trabalho de hoje foi exatamente isso: montar um único arquivo Xacro que reaproveita os modelos reais (com a geometria de cada peça do braço e os sonares do Pioneer) em vez de recriar tudo do zero, e plugar nele os três sensores que faltavam — a câmera Intel RealSense T265, o laser Hokuyo UST-05LX e a IMU Sparton AHRS-8.
A posição exata de cada sensor não foi chute: usei fotos do robô real para confirmar onde cada peça está montada de fato. A primeira tentativa colocou a T265 no chassi do Pioneer, pensando nela como sensor de navegação — errado. As fotos mostraram que ela fica no end-effector do braço, do lado do gripper, junto com um suporte que parece ter sido desenhado para até duas câmeras (só uma está em uso). A IMU, por sua vez, fica fixada na base do primeiro elo do braço, logo depois da torre giratória. O Hokuyo é o único que realmente fica em cima do Pioneer, na base superior. No caminho, também limpei seis sensores fictícios — quatro câmeras RGB simuladas e dois lasers de brinquedo — que vieram de um template antigo e nunca corresponderam a nada do robô real.
Com a versão cinemática validada no RViz, o passo natural era testar a física de verdade no Gazebo. Só que o simulador morria assim que abria, com um erro de baixo nível do glibc dentro de uma trava de mutex — nada relacionado ao robô. Isolei o problema removendo uma peça por vez até sobrar só o essencial: o gzserver puro funcionava, e o crash só acontecia quando o plugin que conecta o Gazebo ao ROS era carregado. Era um bug de empacotamento, não do modelo. A correção exigia uma versão mais nova desse plugin, que por sua vez exigia atualizar todo o ambiente Python de 3.11 para 3.12 — uma migração de centenas de pacotes, não um simples upgrade. Testei tudo num ambiente descartável antes de tocar no ambiente real, confirmei que o robô sobe e fica de pé no Gazebo com física rodando, e só então promovi a mudança — guardando uma cópia do ambiente antigo como rede de segurança, já que a validação final com o hardware físico só é possível no laboratório.
23 Jun 2026
T265 Validada e Acesso Remoto via ZeroTier
A T265 estava instalada desde a sessão anterior, mas instalada não é o mesmo que funcionando. Hoje o sensor passou pelo teste que importa: rodando em hardware real, publicando dados consistentes, com a pose mudando ao mover a câmera. O caminho até lá começou com um erro enganoso — RS2_USB_STATUS_ACCESS, que soa como permissão de sistema operacional, mas era outra coisa. A regra udev que dá acesso ao dispositivo USB tinha o VID (identificador de fabricante) errado: 8086 em vez do 8087 real da T265. Com o VID corrigido e o launch file convertido de um nó que não existia mais para o nodelet oficial do driver (rs_t265.launch), os números saíram dentro do esperado: odometria a 199.6 Hz, acelerômetro a 62.6 Hz, giroscópio a 199.5 Hz, e a transformação t265_odom_frame → t265_pose_frame atualizando em tempo real no RViz.
O segundo avanço do dia foi de infraestrutura, não de robótica: a conexão com a NUC do laboratório agora funciona por ZeroTier, uma rede privada virtual que conecta as máquinas como se estivessem na mesma LAN, independente de onde cada uma esteja fisicamente. Até aqui, o notebook só falava com a NUC quando os dois estavam na mesma rede local. Isso significa que o trabalho de integração não depende mais de estar no laboratório, ao lado do robô — útil em qualquer cenário onde o acesso físico é interrompido, da viagem à simples queda de energia. O script ros_env.sh foi atualizado para tentar ZeroTier primeiro e cair para mDNS (a descoberta automática usada quando notebook e NUC estão no mesmo Wi-Fi) como alternativa.
Por fim, o roadmap da integração foi formalizado em dois pilares que organizam o restante da tese: navegação autônoma da base Pioneer usando a T265 como fonte de odometria com move_base, e servovisão do braço Mitsubishi RV-M2 também apoiada na T265, sem depender dos encoders originais do braço.
22 Jun 2026
NUC Autônoma e Início da T265
Um robô que depende de intervenção manual para ligar não é um robô autônomo — é um eletrodoméstico glorificado. Hoje o b166er cruzou essa linha: a NUC agora inicializa completamente sozinha, sem teclado, sem senha, sem terminal aberto. O usuário liga a tomada e, em menos de dois minutos, o ROS já está no ar.
Para chegar lá, foi preciso resolver dois problemas independentes. O primeiro era o login: a NUC roda um ambiente gráfico (GDM), que por padrão exige senha na tela. Configurar o auto-login direto no GDM eliminou essa barreira — o usuário robo entra automaticamente na área de trabalho. O segundo era o roscore: o nó mestre do ROS precisa estar rodando antes que qualquer outro nó do sistema possa se comunicar. Em vez de depender de uma sessão de terminal aberta ou de um script iniciado manualmente, o roscore foi registrado como serviço do systemd — o gerenciador de processos do Linux — e agora sobe junto com o sistema operacional, logo após a rede e o mDNS estarem prontos.
O dia também marcou o início da integração da câmera Intel RealSense T265, responsável pela odometria visual do robô. A T265 usa duas câmeras fisheye e uma IMU interna para estimar a posição e orientação do robô no espaço sem depender de GPS ou de marcadores externos. As regras de dispositivo USB foram instaladas e o launch file do ROS foi criado — o teste com a câmera física fica para a próxima sessão.
21 Jun 2026
Integração via NUC
Minha tese investiga manipuladores móveis: robôs que combinam uma base capaz de se locomover livremente com um braço capaz de manipular objetos no espaço ao seu redor. Na maioria dos projetos de robótica, essas duas competências são tratadas como problemas separados — navegação de um lado, manipulação do outro. O ponto da pesquisa é justamente o que acontece quando elas precisam operar juntas, no mesmo robô, em tempo real.
O estágio atual é essa integração de fato. A plataforma móvel é uma base Pioneer 3-AT; o manipulador é um braço Mitsubishi RV-M2. Os dois nunca foram projetados para conversar entre si — cada um veio com seu próprio controlador, pensado para operar isolado. A ponte entre eles é um Intel NUC embarcado no robô, rodando ROS Noetic, responsável por sincronizar a navegação da base com o controle do braço numa arquitetura única, em vez de dois sistemas que nunca se falaram.