Diário de pesquisa

Manipuladores Móveis

Diário da minha pesquisa de doutorado em robótica: integração entre plataformas móveis e manipuladores.

Sobre o projeto

O robô b166er, base Pioneer 3-AT com braço Mitsubishi RV-M2

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:


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.

Por que um erro de autenticação (401) pode ser, na real, um erro de saldo esgotado?

Nem toda credencial inválida significa "senha errada". No fluxo de billing pré-pago de uma API, uma chave pode deixar de autenticar por vários motivos que não têm nada a ver com a chave em si estar certa ou errada: ela pode ter sido rotacionada manualmente, revogada automaticamente por uma política de segurança da plataforma, ou — no caso mais comum para contas novas — a conta associada pode ter sido suspensa depois que um crédito de teste inicial se esgotou, sem que isso apareça explicitamente como "billing" na primeira mensagem de erro. O sintoma que chega primeiro (401, "chave inválida") pode não ser a causa raiz — só o próximo obstáculo na fila depois que o anterior (saldo zerado) já tinha acontecido silenciosamente. É por isso que vale a pena olhar o histórico completo antes de assumir que o primeiro erro reportado é o problema de fato: às vezes são dois problemas em sequência, e resolver só o primeiro só revela o segundo.

Por que uma ferramenta de CI consultiva nunca deveria poder travar o pipeline?

Existe uma diferença importante entre uma checagem que precisa passar para o código ser seguro de mergear (testes automatizados, linters de segurança) e uma checagem que só sugere melhorias (um bot de revisão que comenta possíveis problemas, mas cuja ausência de comentário não significa nada de errado). Tratar as duas categorias da mesma forma — deixando qualquer uma travar o CI se falhar — cria um incentivo perverso: quando a ferramenta consultiva quebra por um motivo qualquer (billing, rede, um bug interno dela mesma), ela passa a bloquear trabalho real por um problema que não tem relação nenhuma com a qualidade do código que está sendo revisado. A flag continue-on-error resolve isso na raiz: deixa a ferramenta rodar, reportar o que conseguir, e nunca decidir sozinha que o pipeline inteiro deve parar. Falhas de ferramentas consultivas devem ser visíveis — para quem for investigar — mas nunca bloqueantes.

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.

Por que tratar a base como holonômica na Jacobiana quebra a correção de erro lateral?

Uma base skid-steer como o Pioneer 3-AT tem só dois graus de liberdade de controle — avançar/recuar e girar — mas fisicamente não consegue "escorregar de lado" como um carrinho holonômico poderia. A Jacobiana whole-body do b166er, porém, modela a contribuição da base ao movimento do efetuador como se ela tivesse três graus de liberdade livres (x, y, θ), porque essa é a forma matematicamente conveniente de combinar base e braço numa única resolução por mínimos quadrados. Quando o erro pede uma correção majoritariamente lateral, essa resolução escolhe a solução mais barata do ponto de vista puramente matemático — "escorregar" — sem saber que essa direção é fisicamente inacessível. Só depois, numa etapa separada, a velocidade calculada é projetada de volta no que a base realmente consegue fazer (avanço alinhado ao heading + giro), e é exatamente aí que a componente lateral desaparece sem deixar rastro: nada no sistema convertia esse descarte em um comando de giro que resolvesse o problema pela raiz. A manobra discreta existe para preencher esse buraco — trata o giro como uma decisão explícita, não como um resíduo perdido de uma resolução matemática que nunca conheceu as restrições físicas da base.

Por que um sensor de tombamento (IMU) não é suficiente para uma tarefa de força?

Um IMU mede orientação — o quanto o chassi está inclinado em relação à gravidade — e é uma boa fonte de alarme depois que algo já começou a dar errado: se o robô está tombando, o IMU percebe a inclinação crescendo e pode disparar uma parada de emergência a tempo. O problema é que "romper uma trava" não é uma tarefa que só arrisca tombamento — é uma tarefa de força de contato, e a força que o braço está exercendo contra um obstáculo não aparece na orientação do chassi até o exato momento em que essa força já é grande o suficiente para desequilibrar o robô inteiro. Sem nenhum sensor de força no punho (nem sequer encoder de junta, como esta pesquisa já documentou), o robô hoje não tem como saber, de forma antecipada, que está empurrando forte demais contra algo que não vai ceder — só saberia depois, pelo efeito colateral de quase tombar. Uma arquitetura de segurança completa para esse tipo de tarefa provavelmente precisa de um proxy indireto de força (por exemplo, o quanto o PWM do motor está saturado tentando vencer uma resistência) combinado com o IMU, não o IMU sozinho.

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.

Por que um módulo do Python inteiro pode sumir de uma hora para outra?

O Python tem um processo formal de descontinuação: uma funcionalidade é primeiro marcada como "deprecated" (ainda funciona, mas avisa que vai sumir), e só é removida de fato várias versões depois — o módulo imp, substituído pelo importlib, ficou depreciado desde o Python 3.4 (2014) e só foi removido no 3.12 (2023), quase dez anos de aviso. O problema é que pacotes de terceiros nem sempre acompanham esse calendário — o rosserial_python é um pacote do ROS Noetic (2020), escrito quando imp ainda era normal, e a distribuição conda que o traz para Python 3.12 preservou o código-fonte original sem revisar essa única linha. Resultado: o pacote continua funcionalmente correto, só que com uma importação que aponta para algo que não existe mais no intérprete usado para rodá-lo. Um shim resolve porque o Python resolve import percorrendo o sys.path em ordem — bastou um arquivo chamado imp.py aparecer antes do site-packages nesse caminho, implementando só a única função que o pacote realmente chama.

Por que a calibração hand-eye não pode confiar na própria estimativa de estado do robô?

O b166er já resolve a posição do braço sem encoder — por IK, a partir de onde a T265 diz que o end-effector está no mundo. Seria tentador usar essa mesma estimativa como uma das duas metades da calibração hand-eye (a transformação fixa entre o flange do braço e a câmera). O problema é circular: essa estimativa de estado já assume a transformação T265→flange para converter "a câmera está na pose P" em "logo o flange está em P vezes o inverso dessa transformação". Usar o resultado para calibrar a própria premissa não converge para nada confiável — é medir uma régua com ela mesma. Por isso a calibração precisa de uma fonte de verdade totalmente independente da T265: no caso do b166er, isso significa abrir mão de posições intermediárias e ancorar apenas nos pontos onde a posição do braço é conhecida por outro motivo — o fim-de-curso mecânico e a marca física de home, nenhum dos dois depende de câmera nem de encoder.

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.

Por que o `estimateNewCameraMatrixForUndistortRectify` do OpenCV falha com o FOV da T265?

Uma câmera fisheye captura um ângulo de visão muito maior que uma câmera convencional — a T265 chega a 163°, contra os 60-90° típicos de uma webcam. O modelo matemático que descreve essa distorção (Kannala-Brandt, ou "equidistant") mapeia pontos 3D para pixels de forma não-linear, diferente do modelo pinhole padrão usado pela maioria dos algoritmos de visão computacional, incluindo detectores de marcadores como o AprilTag. Para usar esses algoritmos, é preciso primeiro "desfazer" a distorção fisheye e sintetizar uma imagem pinhole virtual. A função do OpenCV que tenta automatizar essa escolha (`estimateNewCameraMatrixForUndistortRectify`) assume implicitamente FOVs moderados; em 163°, a matemática interna da função degenera e ela devolve uma distância focal artificialmente pequena, o que comprime a imagem sintética e encolhe qualquer objeto nela — inclusive os AprilTags, que passam a ocupar poucos pixels demais para serem detectados de forma confiável. Construir a câmera pinhole virtual manualmente, a partir da distância focal real da fisheye, evita essa degeneração.

O que é a ambiguidade de pose IPPE e por que ela não afeta a posição?

Estimar a pose 3D (posição + orientação) de um objeto plano — como um AprilTag — a partir de uma única imagem é um problema matematicamente ambíguo quando o objeto está quase de frente para a câmera: existem duas soluções de orientação diferentes que explicam quase igualmente bem os quatro cantos detectados na imagem, um fenômeno conhecido como ambiguidade IPPE (Infinitesimal Plane-based Pose Estimation). A posição do centro da tag é bem determinada nos dois casos — é a orientação (para onde a tag está "olhando") que pode escorregar para o ramo errado. O `pose_ambiguity` exposto pelo detector é a razão entre o erro de reprojeção das duas soluções: perto de 1,0 significa que as duas soluções são quase indistinguíveis e a orientação não deve ser confiada; próximo de 0 (ou bem menor que 1) significa que uma solução é claramente melhor que a outra, e a orientação é confiável.

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.

Por que a sincronização de timestamps importa tanto para a IK?

A Cinemática Inversa numérica é um problema de otimização iterativo: partindo de um ângulo de junta inicial (o "seed"), cada iteração move o seed em direção à solução até que o erro caia abaixo de uma tolerância. Se o seed já estiver próximo da solução real, o algoritmo converge em poucas iterações; se estiver longe, pode não convergir dentro do limite máximo. No b166er, o "seed ideal" é o ângulo de junta que o braço tinha no mesmo instante em que a T265 mediu a pose. Com o seed correto, a IK converge em zero iterações — a resposta já estava no ponto de partida. Sem sincronização, o seed é o estado do braço em um instante diferente, e com um braço oscilando a 20 Hz, essa diferença pode ser grande o suficiente para o algoritmo perder a solução.

O que é o workspace seguro do braço e como ele foi definido?

O Pioneer é spawned no Gazebo com z=0.05 m. A base do braço fica a z=0.344 m, e o ombro (junta J1) a z=0.744 m. O end-effector precisa ficar acima de z=0.60 m para que o braço não precise cruzar o chassi do Pioneer. Abaixo desse limite, a cinemática inversa encontra soluções matematicamente válidas, mas o braço físico colide com a estrutura da base — o Gazebo tenta resolver isso com forças de contato, o que gera movimentos erráticos e pode travar o controlador. O alvo de teste validado foi position: {x: 0.55, y: 0.10, z: 0.70} no frame odom.

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.

O que é a Jacobiana whole-body e por que ela tem 6×8?

A Jacobiana de um sistema robótico relaciona velocidades de junta com velocidade do end-effector. Para o b166er, as "juntas" são as três velocidades generalizadas da base (translação em x, translação em y, rotação em torno de z) mais os cinco ângulos do braço RV-M2 — oito no total. O end-effector tem seis graus de liberdade (três de posição, três de orientação). A Jacobiana whole-body é então uma matriz 6×8: cada coluna descreve como uma pequena variação naquela junta específica move o end-effector. A pseudoinversa amortecida dessa matriz (o método de mínimos quadrados com amortecimento) encontra o conjunto de velocidades de junta que melhor corrige o erro do end-effector, distribuindo o movimento entre base e braço de forma automática.

Por que usar Fuzzy em vez de um ganho PID clássico?

Um controlador PID com ganho fixo aplica a mesma "força" de correção independente do tamanho do erro. Perto do alvo, isso pode causar oscilação; longe, pode resultar em movimentos lentos desnecessários. O Fuzzy resolve isso com regras linguísticas: "se o erro é grande e está diminuindo, aplique ganho alto e amortecimento baixo; se o erro é pequeno e está aumentando, reduza o ganho e eleve o amortecimento". Não há equações — só regras que codificam o julgamento que um operador experiente usaria. O resultado é uma curva de ganho não-linear e adaptativa que não precisa ser derivada analiticamente.

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.

Por que o URDF precisa ter as posições certas dos sensores?

No ROS, cada sensor publica seus dados em relação ao seu próprio frame de referência — um sistema de coordenadas centrado nele. Para que esses dados façam sentido no contexto do robô inteiro (por exemplo, para fundir a leitura do laser com a odometria visual da T265), o sistema precisa saber como o frame do sensor se relaciona com o frame do robô. Essa relação vem do URDF: se o arquivo diz que o Hokuyo está 5 cm acima do top_plate quando ele está assentado diretamente nele, o laser vai "enxergar" o chão num ângulo errado, e o mapa que ele constrói estará inclinado em relação ao mundo real.

O que é use_sim_time e por que ele trava o sistema?

O ROS tem dois modos de tempo: o tempo do sistema operacional (relógio real) e o tempo de simulação (publicado pelo Gazebo no tópico /clock). Quando use_sim_time=true, todos os nós ignoram o relógio do sistema e esperam por mensagens em /clock. Se o Gazebo não estiver rodando — como acontece ao usar o RViz isolado para visualizar o URDF — essa espera nunca termina, e nós como o robot_state_publisher ficam paralisados sem publicar nada. O parâmetro ficou ativo no servidor de parâmetros de uma sessão anterior de Gazebo e não foi limpo quando o simulador foi fechado.

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.

O que é Xacro e por que reaproveitar modelos é melhor que recriar?

Xacro é uma extensão do URDF (o formato XML que descreve a geometria de um robô para o ROS) que permite usar variáveis, macros e includes — como uma linguagem de programação para descrição de robôs. Isso permite combinar arquivos separados (o modelo do Pioneer, o modelo do braço) num único robô final, reaproveitando malhas 3D e juntas já validadas em vez de redesenhar a geometria do zero com formas primitivas, que nunca representam o robô real com fidelidade.

Por que um bug de mutex no glibc derruba o Gazebo inteiro?

Um mutex é uma trava que garante que só uma thread (linha de execução) acesse um recurso compartilhado por vez. O glibc — a biblioteca C padrão do Linux — tem verificações internas de consistência nessas travas; quando elas detectam um estado inconsistente (por exemplo, software compilado para uma versão da biblioteca sendo executado contra outra), o processo é abortado imediatamente para evitar corrupção silenciosa de memória. Isso é exatamente o que aconteceu: o plugin que conecta o Gazebo ao ROS foi compilado contra uma combinação de bibliotecas que não correspondia ao que estava disponível em tempo de execução.

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.

O que é ZeroTier e por que ele importa aqui?

ZeroTier cria uma rede privada virtual (VPN) entre máquinas que pode atravessar a internet pública: cada dispositivo recebe um IP fixo dentro dessa rede e os dois passam a se enxergar como se estivessem fisicamente conectados ao mesmo switch, mesmo que um esteja no laboratório e o outro em outro lugar qualquer. Para um robô que depende de uma NUC embarcada como ROS master, isso remove a exigência de estar na mesma rede Wi-Fi do robô para depurar, lançar nós ou simplesmente checar se algo está vivo.

VID errado: por que um número de fabricante derrubava a câmera?

Todo dispositivo USB se anuncia ao sistema operacional com um par de identificadores: o VID (Vendor ID, quem fabricou) e o PID (Product ID, qual produto). As regras `udev` do Linux usam esse par para decidir quais permissões liberar para qual dispositivo. Se o VID na regra não corresponde ao VID real que a câmera envia, o sistema operacional nunca aplica a permissão — e o driver da câmera falha tentando abrir um dispositivo ao qual não tem acesso, mesmo que o cabo esteja bem conectado e o dispositivo apareça no barramento USB.

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.

Por que o roscore precisa de um serviço systemd?

O ROS 1 exige que um nó central chamado roscore esteja rodando antes de qualquer comunicação entre nós. Sem ele, nenhum publisher ou subscriber consegue se conectar. Até hoje, o roscore era iniciado manualmente em uma sessão tmux. Isso funciona, mas é frágil: um reboot inesperado ou uma queda de energia exige intervenção humana para religar tudo. Registrando o roscore como serviço systemd, o próprio sistema operacional passa a ser responsável por iniciá-lo — e por reiniciá-lo automaticamente em caso de falha.

O que é a Intel RealSense T265?

A T265 é uma câmera de rastreamento visual-inercial: combina duas câmeras fisheye com uma IMU (acelerômetro + giroscópio) para estimar a pose 6-DOF do robô — posição (x, y, z) e orientação (roll, pitch, yaw) — sem nenhuma infraestrutura externa. Essa técnica é chamada de odometria visual-inercial (VIO). No contexto do b166er, ela substitui ou complementa a odometria de rodas da base Pioneer, que acumula erro em superfícies irregulares ou durante manobras com o braço estendido.

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.

O que é um manipulador móvel?

É a combinação de uma base móvel (rodas, esteiras ou pernas) com um braço robótico montado sobre ela. A base resolve "chegar até lá"; o braço resolve "fazer algo quando chegar". Separados, os dois problemas já são bem estudados — navegação autônoma de um lado, cinemática e controle de manipuladores do outro. Juntos, a dificuldade nasce da interação: cada movimento do braço desloca o centro de massa do conjunto, o que afeta a estabilidade e a própria navegação da base.

Por que um Intel NUC?

Tanto a base quanto o braço têm controladores originais pensados para operar de forma isolada. Um NUC — um mini-computador x86 compacto o suficiente para ir a bordo do robô — assume o papel de unificar os dois: roda o ROS Noetic, traduz comandos entre os sistemas, e dá ao projeto um único ponto de processamento e decisão.