User Scripts

Use a code editor sandbox to write scripts 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

User Scripts uses TypeScript to typecheck messages in your scripts.

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 script

Every script must declare 3 exports:

  • Inputs array of topic names
  • Output topic name
  • Node function that takes messages from input topics and publishes messages under your output topic

Here is a basic script that echoes its input on a new output topic, /studio_script/echo:

import { Input, Message } from "./types";

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

export default function script(event: Input<"/rosout">): Message<"rosgraph_msgs/Log"> => {
  return event.message;
}

If you drag in a .bag file, you should now be able to inspect the /studio_script/echo topic in the Raw Message panel.

When you create a new script, you’ll be presented with some boilerplate:

import { Input, Message } from "./types";

type Output = {};

export const inputs = ["/topic"];
export const output = "/studio_script/output_topic";

export default function script(event: Input<"/topic">): Output {
  return {};
}

You’ll notice a few things:

  • The types Input and Message are being imported from the ./types module
  • The type Output has no properties

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 Message type imported above. For instance, if you want to publish an array of markers, you can return the type Message<"visualization_msgs/MarkerArray">.

It's not always obvious how message properties affect the visualized output – strictly typing your scripts 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 script 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, Message } from "./types";

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

export default function script(event: Input<"/rosout"> | Input<"/tf">): { data: number[] } => {
  if (event.topic === "/rosout") {
    // read event.message fields expected for /rosout messages
  } else {
    // read event.message fields expected for /tf messages
  }

  return { data: [] };
};

This snippet uses union types to assert that the message in the node function can take either a /rosout or /tf topic. Use an if/else clause to differentiate between incoming topics' schema names when manipulating messages.

To combine messages from multiple topics, create a variable in your script's global scope to reference every time your node function is invoked. Check timestamps to make sure you are not publishing out-of-sync data.

import { Input, Message, Time } from "./types";

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

let lastReceiveTime: Time = { sec: 0, nsec: 0 };
const myScope: {
  tf?: Message<"tf2_msgs/TFMessage">;
  rosout?: Message<"rosgraph_msgs/Log">;
} = {};

export default function script(event: 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 = event.message;
  } else {
    myScope.tf = event.message;
  }

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

Using global variables

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

import { Input, Message } from "./types";

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

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

export default function script(event: Input<"/foo_marker">, globalVars: GlobalVariables): Output => {
  if (event.message.id === globalVars.someNumericaVar) {
    // Message's id matches $someNumericaVar
  }

  return { data: [] };
};

Debugging

For easier debugging, invoke log(someValue) anywhere in your script 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 node function will invoke it once, when your script is registered. Invoking log() inside your node function will log that value every time your node 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 "./types";

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

export default function script(event: Input<"/state">): { metrics: number } | undefined => {
  if (event.message.constant === 3) {
    // Do not publish any message
    return;
  }
  return {
    // Your data here
  };
};

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

Can I return arbitrary JSON data in a message?

Yes! User Scripts supports the json type. You can import it from the ./types module:

import { Input } from "./types";

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

type Output = {
  data: {
    foo: number,
    bar: string,
    nestedData: {
       foo: number[],
       bar: boolean,
    }
  }
}

export default function node(event: Input<"/state">): Output => {
  return {
    data: {
      foo: 123,
      bar: "string",
      nestedData: {
        foo: [1, 2, 3],
        bar: true,
      },
    },
  };
};

Utilities and templates

The sidebar's Utilities tab includes functions that can be imported for use in any script (e.g. import { compare } from "./time.ts"). The types.ts utility file is generated from the currently loaded data source, and contains type definitions for all found schemas.

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 scripts, 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

General
Auto-format on save Auto-format the code in your script on save