Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 65 additions & 80 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

**rclnodejs** is a Node.js client library for [ROS 2](https://www.ros.org/) that provides comprehensive JavaScript and TypeScript APIs for developing ROS 2 solutions.

**Key features:** Topics, Services, Actions, Parameters, Lifecycle Nodes, TypeScript support, RxJS Observables, Electron integration, browser ↔ ROS 2 WebSocket bridge (rosocket), and prebuilt binaries for Linux x64/arm64.
**Key features:** Topics, Services, Actions, Parameters, Lifecycle Nodes, TypeScript support, RxJS Observables, Electron integration, ROS 2 in the browser (typed Web SDK + thin WebSocket gateway — `rclnodejs/web`, `rosocket`), and prebuilt binaries for Linux x64/arm64.

```javascript
const rclnodejs = require('rclnodejs');
Expand All @@ -30,11 +30,11 @@ This example assumes your ROS 2 environment is already sourced.
## Documentation

- Get started:
[Installation](#installation), [Quick Start](#quick-start), [Tutorials](./tutorials/)
[Installation](#installation), [Quick Start](#quick-start), [Web SDK guide](./web/README.md), [Tutorials](./tutorials/)
- Reference:
[API Documentation](https://robotwebtools.github.io/rclnodejs/docs/index.html), [Using TypeScript](#using-rclnodejs-with-typescript), [ROS 2 Interface Message Generation](#ros-2-interface-message-generation)
- Features and examples:
[rosocket](#rosocket--browser--ros-2-bridge), [Observable Subscriptions](#observable-subscriptions), [Electron-based Visualization](#electron-based-visualization), [Performance Benchmarks](#performance-benchmarks), [rclnodejs-cli](#rclnodejs-cli)
[API Documentation](https://robotwebtools.github.io/rclnodejs/docs/index.html), [ROS 2 Interface Message Generation](#ros-2-interface-message-generation), [Using TypeScript](#using-rclnodejs-with-typescript)
- Features:
[ROS 2 in the browser](#ros-2-in-the-browser), [Observable Subscriptions](#observable-subscriptions), [Electron-based Visualization](#electron-based-visualization)
- Project docs:
[Efficient Usage Tips](./docs/EFFICIENCY.md), [FAQ and Known Issues](./docs/FAQ.md), [Building from Scratch](./docs/BUILDING.md), [Contributing](./docs/CONTRIBUTING.md)

Expand Down Expand Up @@ -73,8 +73,6 @@ npm install RobotWebTools/rclnodejs#<branch>

> **Docker:** For containerized development, see the included [Dockerfile](./Dockerfile) for building and testing with different ROS distributions and Node.js versions.

See the [features](./docs/FEATURES.md) and try the [examples](https://github.com/RobotWebTools/rclnodejs/tree/develop/example) to get started.

### Prebuilt Binaries

rclnodejs ships with prebuilt native binaries for common Linux configurations, so most installs skip compilation.
Expand Down Expand Up @@ -112,6 +110,62 @@ node example/topics/publisher/publisher-example.js

More runnable examples in [example/](https://github.com/RobotWebTools/rclnodejs/tree/develop/example) and step-by-step guides in [tutorials/](./tutorials/).

## ROS 2 in the browser

`rclnodejs` ships **two** ways to reach ROS 2 from the browser — pick one based on
how much glue you want to write.

- **[`rclnodejs/web`](./web/README.md)** — **typed, allow-listed,
curl-able** ROS 2 in the browser. A `web.json` file is your public API;
the browser SDK types `call` / `publish` / `subscribe` end-to-end
from your ROS 2 message types; and every capability
is also a plain HTTP endpoint —
`curl -X POST http://<host>/capability/call/<name>` — so shell
scripts, Postman, and AI-agent tool-use just work.
_New in `2.0.0-beta.0`._

```ts
import { connect } from 'rclnodejs/web';
const ros = await connect('ws://host:9000/capability');
const reply = await ros.call<'example_interfaces/srv/AddTwoInts'>(
'/add_two_ints', { a: '2n', b: '40n' }
); // reply.sum is typed as `${number}n`
```

- **[`rosocket`](./rosocket/README.md)** — thin WebSocket gateway,
zero browser dependencies (just built-in `WebSocket` + `JSON`).
Best for quick prototypes and `roslibjs`-style apps.
_New in `2.0.0-beta.0`._

```bash
npx rosocket --port 9000 --topic /chatter:std_msgs/msg/String
```

## Observable Subscriptions

rclnodejs supports [RxJS](https://rxjs.dev/) Observable subscriptions for reactive programming with ROS 2 messages. Use operators like `throttleTime()`, `debounceTime()`, `map()`, and `combineLatest()` to build declarative message processing pipelines.

```javascript
const { throttleTime, map } = require('rxjs');

const obsSub = node.createObservableSubscription(
'sensor_msgs/msg/LaserScan',
'/scan'
);
obsSub.observable
.pipe(
throttleTime(200),
map((msg) => msg.ranges)
)
.subscribe((ranges) => console.log('Ranges:', ranges.length));
```

See the [Observable Subscriptions Tutorial](./tutorials/observable-subscriptions.md) for more details.

## Electron-based Visualization

Build desktop ROS 2 apps with Electron + Three.js, packaged for Windows/macOS/Linux via **Electron Forge**. Featured demo: 🦾 **[manipulator](./demo/electron/manipulator)** — a two-joint arm with manual/automatic control. More in [demo/electron](./demo/electron/).

## ROS 2 Interface Message Generation

rclnodejs auto-generates JavaScript bindings and TypeScript declarations for every ROS 2 `.msg`, `.srv`, and `.action` interface available in your sourced ROS 2 environment. This happens during `npm install`, so in most projects you do not need to run anything by hand.
Expand All @@ -136,13 +190,7 @@ Generated files are written to `<your-project>/node_modules/rclnodejs/generated/

### IDL Message Generation

In addition to the standard ROS 2 message generation (`.msg`, `.srv`, `.action`), rclnodejs can also generate JavaScript message files directly from IDL (Interface Definition Language) files. This is useful for custom IDL files or when you need finer control over the generation process.

To generate messages from IDL files:

```bash
npm run generate-messages-idl
```
For custom `.idl` files (Interface Definition Language), this repo also exposes `npm run generate-messages-idl`. See [docs/BUILDING.md](./docs/BUILDING.md) for when you'd need it.

## Using rclnodejs with TypeScript

Expand All @@ -160,73 +208,10 @@ TypeScript declaration files are included in the package and exposed through the

Then `import * as rclnodejs from 'rclnodejs'` works the same as the JavaScript example at the top of this README. See [TypeScript demos](https://github.com/RobotWebTools/rclnodejs/tree/develop/demo/typescript) for more.

## rosocket — Browser ↔ ROS 2 bridge

> A tiny WebSocket gateway to ROS 2 — built into `rclnodejs`. _New in `2.0.0-beta.0`._

**rosocket** exposes ROS 2 topics/services as plain WebSocket URLs — a
**lightweight** alternative to the rosbridge + roslibjs stack. Zero browser
code, one Node.js process; browsers use only built-in `WebSocket` + `JSON`,
no JavaScript library required.

```bash
npx rosocket --port 9000 --topic /chatter:std_msgs/msg/String
```

```js
const ws = new WebSocket('ws://host:9000/topic/chatter');
ws.onmessage = (e) => console.log(JSON.parse(e.data).data);
ws.onopen = () => ws.send(JSON.stringify({ data: 'hi' }));
```

See [rosocket/README.md](./rosocket/README.md) for the URL scheme, service calls, and the programmatic `startRosocket()` API.

## Observable Subscriptions

rclnodejs supports [RxJS](https://rxjs.dev/) Observable subscriptions for reactive programming with ROS 2 messages. Use operators like `throttleTime()`, `debounceTime()`, `map()`, and `combineLatest()` to build declarative message processing pipelines.

```javascript
const { throttleTime, map } = require('rxjs');

const obsSub = node.createObservableSubscription(
'sensor_msgs/msg/LaserScan',
'/scan'
);
obsSub.observable
.pipe(
throttleTime(200),
map((msg) => msg.ranges)
)
.subscribe((ranges) => console.log('Ranges:', ranges.length));
```

See the [Observable Subscriptions Tutorial](./tutorials/observable-subscriptions.md) for more details.

## Electron-based Visualization

Build interactive desktop ROS 2 apps with Electron + Three.js, packaged for Windows/macOS/Linux via **Electron Forge**. Featured demo: 🦾 **[manipulator](./demo/electron/manipulator)** — a two-joint arm with manual/automatic control.

<p align="left">
<a href="./demo/electron/manipulator"><img src="./demo/electron/manipulator/manipulator-demo.png" alt="manipulator demo" width="320"></a>
</p>

More in [demo/electron](https://github.com/RobotWebTools/rclnodejs/tree/develop/demo/electron).

## Performance Benchmarks

Benchmark results for 1000 iterations with 1024 KB messages (Ubuntu 24.04 WSL2, i7-1185G7):

| Library | Topic (ms) | Service (ms) |
| ----------------------- | ---------: | -----------: |
| **rclcpp** (C++) | 168 | 627 |
| **rclnodejs** (Node.js) | 744 | 927 |
| **rclpy** (Python) | 1,618 | 15,380 |

See [benchmark/README.md](./benchmark/README.md) for the full setup and methodology.

## rclnodejs-cli
## More

[rclnodejs-cli](https://github.com/RobotWebTools/rclnodejs-cli/) is a companion project providing command-line tooling for scaffolding rclnodejs application skeletons and working with launch files for multi-node orchestration.
- **Performance** — faster than `rclpy` and competitive with `rclcpp` for both topic and service round-trips. Full benchmarks in [benchmark/README.md](./benchmark/README.md).
- **Companion CLI** — [`rclnodejs-cli`](https://github.com/RobotWebTools/rclnodejs-cli/) scaffolds rclnodejs application skeletons and orchestrates launch files for multi-node setups.

## Contributing

Expand Down
144 changes: 86 additions & 58 deletions bin/rclnodejs-web.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,68 +64,96 @@ const argv = process.argv.slice(2);
HttpTransport,
} = require('../lib/runtime');

await rclnodejs.init();
const node = rclnodejs.createNode(cfg.node);
rclnodejs.spin(node);

// Always start the WebSocket transport. Add HTTP only when the user
// configured an http.port (via --http-port or in the config file).
const transports = [
new WebSocketTransport({
port: cfg.port,
host: cfg.host,
path: cfg.path,
}),
];
const httpEnabled =
cfg.http && cfg.http.port !== null && cfg.http.port !== undefined;
if (httpEnabled) {
transports.push(
new HttpTransport({
port: cfg.http.port,
host: cfg.http.host || cfg.host,
basePath: cfg.http.basePath || cfg.path,
})
);
}
const runtime = createRuntime({ node, transports });
runtime.expose(cfg.expose);
await runtime.start();

// After start(), each transport reports the actual bound port
// (matters for `--port 0` / `--http-port 0` ephemeral modes).
const wsTransport = runtime.transports[0];
const httpTransport = httpEnabled ? runtime.transports[1] : null;

if (!parsed.quiet) {
const displayHost = (h) =>
['0.0.0.0', '::'].includes(h) ? 'localhost' : h;
const list = runtime.registry.list();
const totals =
Object.keys(list.call).length +
Object.keys(list.publish).length +
Object.keys(list.subscribe).length;
process.stdout.write(
`rclnodejs/web listening on ws://${displayHost(cfg.host)}:${wsTransport.port}${cfg.path} (${totals} capabilities)\n`
);
if (httpTransport) {
const httpHost = displayHost(cfg.http.host || cfg.host);
const httpBase = cfg.http.basePath || cfg.path;
// Track partial init so the catch block can clean up native handles before
// process.exit() — without this, a startup failure (e.g. EADDRINUSE on the
// WS port) leaves rclnodejs's native spin loop running and segfaults on exit.
let rclInitialized = false;
let runtime = null;

try {
await rclnodejs.init();
rclInitialized = true;

const node = rclnodejs.createNode(cfg.node);
rclnodejs.spin(node);

// Always start the WebSocket transport. Add HTTP only when the user
// configured an http.port (via --http-port or in the config file).
const transports = [
new WebSocketTransport({
port: cfg.port,
host: cfg.host,
path: cfg.path,
}),
];
const httpEnabled =
cfg.http && cfg.http.port !== null && cfg.http.port !== undefined;
if (httpEnabled) {
transports.push(
new HttpTransport({
port: cfg.http.port,
host: cfg.http.host || cfg.host,
basePath: cfg.http.basePath || cfg.path,
})
);
}
runtime = createRuntime({ node, transports });
runtime.expose(cfg.expose);
await runtime.start();

// After start(), each transport reports the actual bound port
// (matters for `--port 0` / `--http-port 0` ephemeral modes).
const wsTransport = runtime.transports[0];
const httpTransport = httpEnabled ? runtime.transports[1] : null;

if (!parsed.quiet) {
const displayHost = (h) =>
['0.0.0.0', '::'].includes(h) ? 'localhost' : h;
const list = runtime.registry.list();
const totals =
Object.keys(list.call).length +
Object.keys(list.publish).length +
Object.keys(list.subscribe).length;
const noun = totals === 1 ? 'capability' : 'capabilities';
process.stdout.write(
` also http://${httpHost}:${httpTransport.port}${httpBase} (call/publish only)\n`
`rclnodejs/web listening on ws://${displayHost(cfg.host)}:${wsTransport.port}${cfg.path} (${totals} ${noun})\n`
);
if (httpTransport) {
const httpHost = displayHost(cfg.http.host || cfg.host);
const httpBase = cfg.http.basePath || cfg.path;
process.stdout.write(
` also http://${httpHost}:${httpTransport.port}${httpBase} (call/publish only)\n`
);
}
}
}

const stop = async () => {
if (!parsed.quiet) process.stdout.write('\nstopping…\n');
await runtime.stop();
rclnodejs.shutdown();
process.exit(0);
};
process.once('SIGINT', stop);
process.once('SIGTERM', stop);
})().catch((err) => fail(err));
const stop = async () => {
if (!parsed.quiet) process.stdout.write('\nstopping…\n');
await runtime.stop();
rclnodejs.shutdown();
process.exit(0);
};
process.once('SIGINT', stop);
process.once('SIGTERM', stop);
} catch (err) {
// Best-effort native cleanup so rclnodejs doesn't segfault on dirty exit.
if (runtime) {
try {
await runtime.stop();
} catch (_) {
/* ignore — we're already failing */
}
}
if (rclInitialized) {
try {
rclnodejs.shutdown();
} catch (_) {
/* ignore — we're already failing */
}
}
fail(err);
}
})();

function fail(err) {
if (err && err.cli) {
Expand Down
6 changes: 3 additions & 3 deletions demo/rosocket/README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# rosocket demo (browser ↔ ROS 2)
# rosocket demo ROS 2 in the browser

A minimal end-to-end example of the
[`rosocket`](../../rosocket/README.md) WebSocket bridge. The Node
[`rosocket`](../../rosocket/README.md) WebSocket gateway. The Node
server runs anywhere ROS 2 is sourced; the HTML page runs in any
modern browser and talks to it over plain `WebSocket` — no client
library required.
Expand All @@ -26,7 +26,7 @@ library required.
# 1. Source your ROS 2 distro (humble / jazzy / kilted / lyrical / rolling)
source /opt/ros/$ROS_DISTRO/setup.bash

# 2. Terminal A — start the WebSocket bridge
# 2. Terminal A — start the WebSocket gateway
node demo/rosocket/server.js
# [rosocket-demo] listening on ws://localhost:9000 (bind=0.0.0.0)

Expand Down
Loading
Loading