How to publish and subscribe ROS 2 custom messages using Node.js
Quem sou eu?
Hey! I’m Diego, I’m a researcher, work right now as a web developer and have a growing enthusiasm for robotics. Having recently graduated, my final project to get my degree were ROS related and for a while now I wanted to publish a tutorial like this, now I decided the the time has come.
Introdução
Esse tutorial visa mostar como realizar a publicação ou subscrição de um tópico ROS2 (ROS) utilizando Node.js. Abordando conceitos básicos de ROS como, criar nós, criar um tópico, subscrever-se a um tópico e publicar em um tópico.
Pré-requisitos
Node.js: >= 16.20.2 — Utilizado por mim 22.18.0
ROS 2 (instalado e configurado ): Versões suportadas Rolling, Kilted, Jazzy, Humble - utilizei instalado e compilado na minha máquina via código fonte
Esse projeto pode ser realizado utilizando diferentes distros ROS, porém destaco que a versão que utilizei foi compilada da fonte.
Linux Distribution: Nesse projeto foi utilizado o Ubuntu 22.04 LTS (embora possível a utilização de ROS 2 no windows, esse projeto foi desenvolvido pensando no ambiente linux).
Setting up the environment
Para faciliar o desenvolvimento eu deixo disponível um repositório que contém um workspace ROS2 configurado com um publisher e uma mensagem customizada, ambos serão utilizados para o projeto. No repositório, você vai encontrar também a aplicação final desse projeto.
Todas a implementação do publisher e mensagem customizada foram feitas seguindo o tutorial oficial do ROS Humble, em caso de dúvidas, consulte o material Writing a Simple Python Publisher and Subscriber.
Para fins didáticos eu recomendo seguir as etapas do tutorial ao invés de se aventurar na implementação final em
/final-app/.
Utilize o comando abaixo para clonar o repositório:
# HTTPS
git clone https://github.com/Daliego/pub-sub-ros-nodejs-application.git
# SSH
git clone git@github.com:Daliego/pub-sub-ros-nodejs-application.git
No repositório, você vai encontrar a seguinte estrutura de pastas:
pub-sub-ros-nodejs-application/
├── ros-ws-app/ # Ambiente ROS2 configurado
├── final-app/ # Aplicação Node.js com a implementação final desse tutorial
├── tutorial-implementation/ # Pasta onde você vai implementar o tutorial descrito aqui
├── setup_ros.sh/ # Script para configurar o ambiente ROS2
└── README.md
A pasta
tutorial-implementationfoi deixada no repositório pra que possa ser utilizada para a implementação do tutorial descrito aqui. E a pastafinal-terminal-appé a aplicação final que esse tutorial está descrevendo.
O script setup_ros.sh foi deixado aqui para facilitar o carregamento das variáveis de ambiente do ROS e carregar o workspace local (custom_interface e count_publisher_python). Para utilizar o script basta executar o comando source setup_ros.sh. No script, a linha 12 (source ~/ros2_humble/install/setup.bash) deve ser alterada para o path do seu ROS2 installation.
Executando o publisher python
A primeira etapa do tutorial visa mostar como escutar mensagens do ROS em terminal Node.js.
Desse modo, antes de qualquer configuração do ambiente Node.js, vamos realizar a execução do publisher python utilizando ROS 2.
Acesse o workspace /ros-ws-app, compile e execute o projeto com os comandos abaixo:
# Carregue as variáveis de ambiente
source setup_ros.sh
# Em caso de ainda não ter realizado a substituição do path para a sua instalação do ROS2, você pode executar o comando:
# `source $ROS2_PATH/install/setup.bash`
# Tenha certeza de estar na raiz do repositório clonado e navegue até a pasta `ros-ws-app`
cd ros-ws-app
# Compile o workspace
colcon build
# Tendo o projeto compilado, carregue as variáveis novamente com o setup_ros.sh e execute o publisher python
source ../setup_ros.sh
# Execute o publisher python
ros2 run count_publisher_python talker
Você deve ver o seguinte output no terminal:

A imagem acima mostra o output do o publisher python está publicando no tópico count_messages. Ele esta enviado mensagen no formato de um objeto
O que o publisher python faz é criar um nó ROS chamado de count_publisher, esse nó (bloco de código ROS configurado para realizar uma ação específica) publicará mensagens no tópico chamado de count_messages com a mensagem customizada Count. Todo o código resposável por isso pode ser encontrado no arquivo publisher_member_function.py do repositório.
O formato de mensagem customizada Count é definido no arquivo Count.msg, ele é composto por um único campo value do tipo float64.
Exemplo de mensagem publicada:
{
// OBS: Os valores são float 64
"value": 11.0000000
}
Escutando mensagens do ROS com Node.js
Agora, com o objetivo de escutar as mensagens do tópico count_messages, precisaremos criar um novo nó ROS, esse vai ser reponsável por sobscrever-se no tópico. Nesse sentido, ao invés de criarmos um subscriber python ou C/C++ executado com ROS2, vamos utilizar o Node.js com a biblioteca rclnodejs.
Instalando as dependências do Node.js
Antes de começarmos a implementar, precisamos configurar o ambiente Node.js com as dependências necessárias. Acesse a pasta tutorial-implementation e inicialize o projeto:
# Acesse a pasta tutorial-implementation
cd tutorial-implementation
# Inicialize o projeto Node.js
npm init -y
# Instale o rclnodejs e as dependências de desenvolvimento
npm install rclnodejs
npm install --save-dev typescript @types/node ts-node
Configurando o ambiente TypeScript
O projeto inteiro foi desenvolvido utilizando TypeScript, simplesmente para facilitar a leitura e manutenção do código. E porque eu gosto muito de ter a garantia de tipagem do TypeScript.
Crie um arquivo tsconfig.json na pasta tutorial-implementation com a seguinte configuração:
// nano tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"outDir": "./dist",
"rootDir": "./",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true
},
"include": ["**/*.ts"],
"exclude": ["node_modules"]
}
Nota importante: O
rclnodejsrequer que o ambiente ROS2 esteja configurado antes de ser executado. Certifique-se de ter executado osource setup_ros.shou o source do seu ROS2 antes de executar qualquer código Node.js que utilize orclnodejs.
Criando o arquivo publisher.ts
Agora vamos criar o arquivo publisher.ts onde implementaremos o primeiro nó ROS:
# Ainda dentro da pasta tutorial-implementation
touch publisher.ts
No arquivo publisher.ts, vamos começar com a estrutura básica de um nó ROS, que é a inicialização da classe ROS e a criação do nó:
import ROS, { type MessagesMap, type TypeClass } from "rclnodejs";
ROS.init().then(() => {
const node = new ROS.Node("node_publisher_typescript");
// O tipo nao existe em runtime, por isso o as unknown as TypeClass<keyof MessagesMap>
const countTypeMessage = "custom_interface/msg/Count" as unknown as TypeClass<
keyof MessagesMap
>;
node.spin();
});
Este código inicial:
- Importa o módulo
rclnodejse os tipos necessários - Inicializa o ROS com
ROS.init() - Cria um novo nó ROS chamado
node_publisher_typescript - Define o tipo de mensagem que será utilizado (
custom_interface/msg/Count) - Mantém o nó ativo e processando mensagens com o
node.spin()
Etapa 1 - Criando um subscriber para receber as mensagens do tópico count_messages
Agora pode ser implementada a primeira funcionalidade do sistema, que é utilizar o nó criado para se subscrever ao tópico count_messages que está sendo publicado pelo publisher Python.
Atualize o arquivo publisher.ts com o seguinte código:
import ROS, { type MessagesMap, type TypeClass } from "rclnodejs";
ROS.init().then(() => {
const node = new ROS.Node("node_publisher_typescript");
const countTypeMessage = "custom_interface/msg/Count" as unknown as TypeClass<
keyof MessagesMap
>;
// Etapa 1 - Criar um subscriber para receber os mensagens do tópico `count_messages`
node.createSubscription(countTypeMessage, "count_messages", (message) => {
console.log("[count_messages] - Received count value: ", message);
});
node.spin();
});
O método createSubscription recebe três parâmetros:
- Tipo da mensagem:
countTypeMessage- define o formato da mensagem que será recebida - Nome do tópico:
"count_messages"- o tópico ao qual queremos nos subscrever - Callback: função que será executada sempre que uma nova mensagem for recebida no tópico
Execute o código, utilizando os comandos abaixo:
# Certifique-se de estar na pasta tutorial-implementation
# E que o ambiente ROS2 está configurado
source ../setup_ros.sh
# Execute o código
npx ts-node publisher.ts
Você deve começar a receber no terminal as mensagens publicadas no tópico:

Ainda, ao executar o rqt_graph é possível ver de maneira visual os nós e tópicos ativos no ROS 2.
# Em outro terminal, com o ambiente ROS2 configurado
# Nao esqueça de realizar o source do setup_ros.sh
ros2 run rqt_graph rqt_graph
O output da execução do rqt_graph é o seguinte:

A visualização acima mostra que existem dois nós ativos:
count_publisher: o nó Python que está publicando no tópicocount_messagesnode_publisher_typescript: o nó Node.js que está subscrevendo-se ao tópicocount_messages
O tópico count_messages é utilizado para realizar a comunicação entre os dois nós, mostrando que ambos estão publicando mensagens e recebendo mensagens.
Etapa 2 - Criando um publisher para publicar em um novo tópico
Agora duas coisas serão realizadas, a criação de um novo tópico e a publicação de mensagens dentro dele.
import ROS, { type MessagesMap, type TypeClass } from "rclnodejs";
interface Count {
value: number;
}
ROS.init().then(() => {
const node = new ROS.Node("node_publisher_typescript");
const countTypeMessage = "custom_interface/msg/Count" as unknown as TypeClass<
keyof MessagesMap
>;
// Etapa 1 - Criar um subscriber para receber os mensagens do tópico `count_messages`
node.createSubscription(countTypeMessage, "count_messages", (message) => {
console.log("[count_messages] - Received count value: ", message);
});
// Etapa 2 - Criar um novo tópico que vai receber mensagens de node_publisher_typescript
const publisher = node.createPublisher(
countTypeMessage,
"random_values_topic"
);
// Configuração para publicar a cada 10 segundos
const timer = node.createTimer(BigInt(10000000000), () => {
publisher.publish({ value: Math.random() * 100 });
});
node.spin();
});
O método createPublisher vai ser utilizado para criar um novo tópico, esse tópico vai receber o mesmo tipo de mensagem do tópico count_messages.
A variável publisher é um instância do objeto Publisher do rclnodejs, ela contém diversos métodos para manipular esse novo tópico, como publicar mensagens, acessar o nome do tópico, verificar se o tópico ainda está ativo, etc.
Para que as mensagens aleatórias que estão sendo geradas se mantenham sendo publicadas no tópico random_values_topic, algum tipo de loop precisa ser utilizado.
O método createTimer do rclnodejs é uma opção para manter a publicação por intervalo de tempo, inclusive é uma alternativa ao setTimeout nativo do javascript.
Para verificar o funcionamento do timer, podemos executar o comando abaixo no terminal:
# Em outro terminal, com o ambiente ROS2 configurado
ros2 topic echo /random_values_topic
Um output como o abaixo poderá ser visto:

Etapa 3 - Criando um subscriber para o novo tópico
O objetivo dessa etapa é criar um segundo nó, que se sobscreva no novo tópico random_values_topic e log as mensagens recebidas. Para fins de organização, eu vou criar um novo arquivo para esse novo nó. Com isso, é possível executar o novo nó em um terminal separado.
# Dentro da pasta tutorial-implementation
touch subscriber.ts
No arquivo subscriber.ts, adicione:
import ROS, { type MessagesMap, type TypeClass } from "rclnodejs";
interface Count {
value: number;
}
ROS.init().then(() => {
const nodeSubsriber = new ROS.Node("node_subscriber_typescript");
const countTypeMessage = "custom_interface/msg/Count" as unknown as TypeClass<
keyof MessagesMap
>;
// Etapa 3 - Criar um subscriber para receber os mensagens do novo tópico
nodeSubsriber.createSubscription(
countTypeMessage,
"random_values_topic",
(message) => {
const messageFormated = message as unknown as Count;
console.log(
"[node_subscriber_typescript] - From:[random_values_topic] - Value: ",
messageFormated.value
);
}
);
nodeSubsriber.spin();
});
Para executar o código, utilize os comandos abaixo:
# Terminal 1 - com o ambiente ROS2 configurado
source ../setup_ros.sh
npx ts-node publisher.ts
# Terminal 2 - com o ambiente ROS2 configurado
source ../setup_ros.sh
npx ts-node subscriber.ts
Você verá no terminal do subscriber.ts as mensagens sendo recebidas do tópico random_values_topic:

Se você estiver como os três terminais abertos, você verá o seguinte output:

Visualize novamente com o rqt_graph para observar a organização dos nós e tópicos:
ros2 run rqt_graph rqt_graph

Conclusão
Neste tutorial, você aprendeu como utilizar Node.js para se comunicar com a infraestrutura ROS 2, criando publishers e subscribers que podem trocar mensagens customizadas através de tópicos.
Os conceitos abordados aqui como criar nós, subscrever-se a tópicos e publicar mensagens são fundamentais para qualquer aplicação ROS 2. Com o rclnodejs, você pode facilmente integrar aplicações Node.js com sistemas robóticos baseados em ROS 2, ou utilizar o Node.js simplesmente para manipular alguma parte específica do sistema robótico.
Future projects
Eu espero trazer no futuro alguma outra aplicação utilizando a mesma abordagem de comunicação pub-sub, utilizando talvez outra biblioteca ou trazendo uma implementação mais complexa.
Recursos
- ROS 2 Installation Guide (Ubuntu Development Setup)
- Writing a Simple Python Publisher and Subscriber
- rclnodejs - ROS 2 Client Library for Node.js
- Repositório do Projeto
- ROS 2 Standard Interfaces (Messages)
Se este tutorial foi útil para você, considere dar uma estrela no repositório e seguir para mais conteúdo sobre ROS 2 e Node.js! Also, you can actually buy me a coffee!