Node Playground

Use a code editor sandbox to write nodes that 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.

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:

Writing your first node

Every node must declare 3 exports:

  • 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 on a new output topic, /studio_node/echo:

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 inspect the /studio_node/echo topic in the Raw Messages 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.

It's not always obvious how message properties affect the visualized output – strictly typing your nodes helps you debug issues at compile time rather than at runtime. With that said, you can disable Typescript checks when working on a rough draft of your node 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 {
        myScope.tf = 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 (message.message.id === globalVars.someNumericaVar) {
    // Message's id matches $someNumericaVar
  }

  return { data: [] };
};

export default publisher;

Debugging

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(addNums);
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, using log() will significantly slow down your code.

FAQ

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

Do an early (or late) return in your function body when you don't want to publish. For example, 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) {
    // Do not publish any message
    return;
  }
  return {
    // Your data here
  };
};

export default publisher;

In Typescript, if you return without a value, it will implicitly return undefined. Note the union return type for the publisher – we've indicated to Typescript that this function can return undefined.

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;

Utilities and templates

The sidebar's Utilities tab includes functions that can be imported for use in any node (e.g. import { compare } from "./time.ts"). To contribute your own utility function, open a pull request to add it to our codebase.

We currently do not allow importing 3rd-party packages, but let us know if there are packages that would be useful to you!

The Templates tab includes boilerplate for writing common nodes, like one that publishes a MarkerArray. If you have any other use cases that would work well as a template, please let us know.

Settings

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