Node Playground

Use a code editor sandbox to publish pseudo-ROS topics internally to Studio. Manipulate, reduce, and filter existing ROS messages and output them in a way that is useful to you.

Getting started

Node Playground uses TypeScript to typecheck messages coming in and out of your nodes.

Learning TypeScript

TypeScript is a superset of JavaScript, so you can Google syntactic questions (e.g. how to manipulate arrays, or access object properties) using JavaScript terms, and semantic questions (e.g. how to make an object property optional) using TypeScript terms.

Resources to get yourself ramped up:

Writing your first node

Every node must declare 3 exports that determine how it should execute:

  • Inputs array of topic names.
  • Output topic with an enforced prefix: /studio_node/.
  • Publisher function that takes messages from input topics and publishes messages under your output topic.

Here is a basic node that echoes its input:

import { Input, Messages } from "ros";

export const inputs = ["/rosout"]; export const output = "/studio_node/echo";

const publisher = (message: Input<"/rosout">): Messages.rosgraph_msgs/Log => { return message.message; };

export default publisher;

If you drag in a .bag file, you should now be able to subscribe to the /studio_node/echo topic in the Raw Messages panel.

But let’s say you want to render some markers in the 3D panel. When you create a new node, you’ll be presented with some boilerplate:

import { Input, Messages } from "ros";

type Output = {}; type GlobalVariables = { id: number };

export const inputs = []; export const output = "/studio_node/";

// Populate 'Input' with a parameter to properly type your inputs, e.g. 'Input<"/your_input_topic">' const publisher = (message: Input<>, globalVars: GlobalVariables): Output => { return {}; };

export default publisher;

You’ll notice a few things:

  • The types Input and Messages are being imported from the ros module.
  • The type Output has no properties.
  • The type GlobalVariables is declared for convenience.

Input is a generic type, meaning that it takes a parameter in order to be used. It is left empty on purpose as you'll need to populate it with the name of your input topic, e.g. Input<"/rosout">.

As for the Output type, you can either manually type out your output with the properties you care about or use one of the dynamically generated types from the Messages type imported above. For instance, if you want to publish an array of markers, you can return the type Messages.visualization_msgs\MarkerArray.

The GlobalVariables type is used to specify the types of any variables you'd like to access in your node. It is not required.

Strictly typing your nodes will help you debug issues at compile time rather than at runtime. It's not always obvious how message properties are affecting the visualized output, and so the more you strictly type your nodes, the less likely you will make mistakes.

With that said, you can disable Typescript checks while getting a rough draft of your node working by adding // @ts-expect-error on the line above the one you want to ignore.

Using multiple input topics

In some cases, you will want to define multiple input topics:

import { Input, Messages } from "ros";

export const inputs = ["/rosout", "/tf"]; export const output = "/studio_node/echo";

const publisher = (message: Input<"/rosout"> | Input<"/tf">): { data: number[] } => { if (message.topic === "/rosout") { // type now refined to /rosout - can safely use message.message.pose } else { // type now refined to /tf - can safely use message.message.transforms }

return { data: [] }; };

export default publisher;

This snippet uses union types to assert that the message in the publisher function can take either a /rosout or /tf topic. Use an if/else clause to differentiate between incoming topic datatypes when manipulating messages. To combine messages from multiple topics, create a variable in your node's global scope to reference every time your publisher function is invoked. Check timestamps to make sure you are not publishing out-of-sync data.

import { Input, Messages, Time } from "ros";

export const inputs = ["/rosout", "/tf"]; export const output = "/studio_node/echo";

let lastReceiveTime: Time = { sec: 0, nsec: 0 }; const myScope: { tf?: Messages.tf2_msgs/TFMessage; rosout?: Messages.rosgraph_msgs/Log; } = {};

const publisher = (message: Input<"/rosout"> | Input<"/tf">): { data: number[] } | undefined => { const { receiveTime } = message; let inSync = true;

if (receiveTime.sec !== lastReceiveTime.sec || receiveTime.nsec !== lastReceiveTime.nsec) { lastReceiveTime = receiveTime; inSync = false; }

if (message.topic === "/rosout") { myScope.rosout = message.message; } else { = message.message; }

if (!inSync) { return { data: [] }; } };

export default publisher;

Using global variables

The publisher function will receive all of the variables as an object every time it is called. If the variables change, the publisher function will automatically re-run with the new values:

import { Input, Messages } from "ros";

type Output = {}; type GlobalVariables = { someNumericaVar: number };

export const inputs = []; export const output = "/studio_node/";

const publisher = (message: Input<"/foo_marker">, globalVars: GlobalVariables): Output => { if ( === globalVars.someNumericaVar) { // Message's id matches $someNumericaVar }

return { data: [] }; };

export default publisher;


For easier debugging, invoke log(someValue) anywhere in your node code to print values to the Logs section at the bottom of the panel. The only value you cannot log() is one that is, or contains, a function definition. You can also log multiple values at once, e.g. log(someValue, anotherValue, yetAnotherValue).

The following log statements will not produce any errors:

const addNums = (a: number, b: number): number => a + b;
log(50, "ABC", null, undefined, { abc: 2, def: false });
log(1 + 2, addNums(1, 2));

But these statements containing function definitions will:

log(() => {});
log({ subtractNums: (a: number, b: number): number => a - b });

Invoking log() outside your publisher function will invoke it once, when your node is registered. Invoking log() inside your publisher function will log that value every time your publisher function is called. Note that if your topic publishes at a high rate (tick information for instance) using log will significantly slow down Node Playground.


What if I don't want to produce a message every time publish is called?

All you need to do is do an early (or late) return in your function body that is hit when you don't want to publish. For instance, let's say you only wanted to publish messages when a constant in the input is not 3:

import { Input } from "ros";

export const inputs = ["/state"]; export const output = "/studio_node/manual_metrics";

const publisher = (msg: Input<"/state">): { metrics: number } | undefined => { if (msg.message.constant === 3) { return; } return { // Your data here }; };

export default publisher;

Note the union return type in the publisher definition. We've indicated to Typescript that this function can return undefined, and we do so within the conditional block (In Typescript, if you return without a value, it will implicitly return undefined). When this code path is hit, we don't publish any message.

Can I return arbitrary JSON data in a message?

Yes! Node Playground supports the json type. You can import it from the "ros" module:

import { Input, json } from "ros";

export const inputs = ["/state"]; export const output = "/studio_node/json_data";

const publisher = (msg: Input<"/state">): { data: json } => { return { data: { foo: 123, bar: "string", nestedData: { foo: [1, 2, 3], bar: true, }, }, }; };

export default publisher;

Node Playground utilities

This directory includes a number of utility functions that can be used in any node playground. You can import them into your node like this: import { compare } from "./time.ts".


Can I use 3rd-party packages? Not at the moment, but please let us know if that would be useful to you! Be sure to specify which package(s) you would like and why you want them.

Can I use other Studio code here? Studio has many utilities that you are free to copy and paste into this directory, as long as you also provide relevant unit testing.

Feel free to reach out to the Studio team with any questions!

Are these utilities versioned? Not currently. Please let us know if this is something you'd be interested in!

Panel settings

  • Auto-format on save – Auto-format the code in your Node Playground node on save