Skip to content
Merged
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
1 change: 1 addition & 0 deletions .npmignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ tutorials/
electron_demo/
ts_demo/
tools/
demo/
.github/
.nyc_output/
.vscode/
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,10 +144,10 @@ Create rich, interactive desktop applications using Electron and web technologie

| Demo | Description | Screenshot |
| :-----------------------------------------------: | :-----------------------------------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------: |
| **🐢 [turtle_tf2](./electron_demo/turtle_tf2)** | Real-time coordinate frame visualization with turtle control. Features TF2 transforms, keyboard control, and dynamic frame updates. | ![turtle_tf2](./electron_demo/turtle_tf2/turtle-tf2-demo.png) |
| **🦾 [manipulator](./electron_demo/manipulator)** | Interactive two-joint robotic arm simulation. Features 3D joint visualization, manual/automatic control, and visual movement markers. | ![manipulator](./electron_demo/manipulator/manipulator-demo.png) |
| **🐢 [turtle_tf2](./demo/electron/turtle_tf2)** | Real-time coordinate frame visualization with turtle control. Features TF2 transforms, keyboard control, and dynamic frame updates. | ![turtle_tf2](./demo/electron/turtle_tf2/turtle-tf2-demo.png) |
| **🦾 [manipulator](./demo/electron/manipulator)** | Interactive two-joint robotic arm simulation. Features 3D joint visualization, manual/automatic control, and visual movement markers. | ![manipulator](./demo/electron/manipulator/manipulator-demo.png) |

Explore more examples in [electron_demo](https://github.com/RobotWebTools/rclnodejs/tree/develop/electron_demo).
Explore more examples in [demo/electron](https://github.com/RobotWebTools/rclnodejs/tree/develop/demo/electron).

## Using rclnodejs with TypeScript

Expand Down Expand Up @@ -175,7 +175,7 @@ rclnodejs.init().then(() => {
});
```

See [TypeScript demos](https://github.com/RobotWebTools/rclnodejs/tree/develop/ts_demo) for more examples.
See [TypeScript demos](https://github.com/RobotWebTools/rclnodejs/tree/develop/demo/typescript) for more examples.

## Observable Subscriptions

Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ An interactive Electron application demonstrating a two-joint robotic manipulato
1. **Navigate to the demo directory**:

```bash
cd rclnodejs/electron_demo/manipulator
cd rclnodejs/demo/electron/manipulator
```

2. **Install dependencies**:
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ A minimal Electron application demonstrating basic ROS2 topic communication usin
1. **Navigate to the demo directory**:

```bash
cd rclnodejs/electron_demo/topics
cd rclnodejs/demo/electron/topics
```

2. **Source your ROS 2 environment**:
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ The demo uses the following key dependencies:
1. **Navigate to the demo directory**:

```bash
cd electron_demo/turtle_tf2
cd demo/electron/turtle_tf2
```

2. **Install dependencies**:
Expand Down
File renamed without changes.
93 changes: 93 additions & 0 deletions demo/rosocket/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# rosocket demo (browser ↔ ROS 2)

A minimal end-to-end example of the
[`rosocket`](../../rosocket/README.md) WebSocket bridge. 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.

## What it shows

- Subscribe to and publish on `/chatter` (`std_msgs/msg/String`).
- Call `/add_two_ints` (`example_interfaces/srv/AddTwoInts`) — the
service implementation lives in
[`example/services/service/service-example.js`](../../example/services/service/service-example.js)
and is launched in a second terminal.

## Layout

- `server.js` — `rclnodejs` node + `startRosocket` bridge only.
- `index.html` — single-file browser client using only built-in
`WebSocket` and `JSON`.

## Run the bridge

```bash
# 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
node demo/rosocket/server.js
# [rosocket-demo] listening on ws://localhost:9000 (bind=0.0.0.0)

# 3. Terminal B — start the AddTwoInts service so the browser has
# something to call
node example/services/service/service-example.js
```

The server binds to `0.0.0.0:9000` so it is reachable from any host
that can route to the machine running it.

## Open the page

Open `demo/rosocket/index.html` in any modern browser (double-click,
or `File > Open`). Leave the bridge field as `ws://localhost:9000`
when the browser and the bridge run on the same machine, and click
**Subscribe**, then **Publish**, then **Call**.

If the browser is on a different machine than the bridge (remote ROS
box, container, VM, WSL → host browser, etc.), change the bridge
field to point at the bridge host, e.g. `ws://192.0.2.10:9000` or
`ws://my-ros-host.local:9000`, and reconnect.

> WSL note: WSL2 normally forwards `localhost` to Windows, so
> `ws://localhost:9000` works as-is. On WSL1 or with custom/mirrored
> networking, use the WSL IP (`hostname -I | awk '{print $1}'`)
> instead.

### Verify from the ROS 2 side (optional)

```bash
# Always works (explicit type), even before any browser tab is connected:
ros2 topic echo /chatter std_msgs/msg/String

# Auto-discovery form — only works after a browser tab is connected to
# ws://.../topic/chatter (the bridge creates the subscription on demand)
# and after a publisher exists on the topic (browser Publish, or the
# `ros2 topic pub` command below):
ros2 topic echo /chatter

ros2 topic pub /chatter std_msgs/msg/String "{data: 'hi from ros2'}"
ros2 service call /add_two_ints example_interfaces/srv/AddTwoInts "{a: 7, b: 5}"
```

> If you see `WARNING: topic [/chatter] does not appear to be published yet
> / Could not determine the type for the passed topic`, it just means no
> publisher exists on `/chatter` yet — use the explicit-type form above,
> or click **Subscribe** + **Publish** in the browser first.

Anything you publish from the browser shows up in `ros2 topic echo`,
and any `ros2 topic pub` to `/chatter` shows up in the browser
subscription log.

## URL scheme reminder

```
ws://<host>:<port>/topic/<name>?type=<pkg>/msg/<Type>
ws://<host>:<port>/service/<name>?type=<pkg>/srv/<Type>
```

This demo pre-declares both types server-side via `topicTypes` /
`serviceTypes`, so the browser can omit `?type=`. See
[`rosocket/README.md`](../../rosocket/README.md) for the full
protocol reference.
220 changes: 220 additions & 0 deletions demo/rosocket/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
<!doctype html>
<!--
rosocket browser demo. Open this file directly in a browser (File > Open,
or drag-and-drop). Talks to the Node server in this folder over plain
WebSocket — no JS library required.
-->
<html lang="en">
<head>
<meta charset="utf-8" />
<title>rosocket demo</title>
<style>
body {
font-family: system-ui, sans-serif;
max-width: 720px;
margin: 2em auto;
padding: 0 1em;
}
h1 {
margin-bottom: 0;
}
h2 {
margin-top: 1.5em;
}
input,
button {
font-size: 1em;
padding: 0.3em 0.5em;
}
input[type='text'],
input[type='number'] {
width: 14em;
}
pre {
background: #f4f4f4;
padding: 0.5em;
max-height: 12em;
overflow: auto;
white-space: pre-wrap;
}
.log {
background: #f4f4f4;
padding: 0.5em;
max-height: 12em;
overflow: auto;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 0.9em;
}
.demo {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1em;
align-items: start;
}
.demo > .controls pre {
max-height: 10em;
}
.demo > .code {
margin: 0;
background: #1e1e1e;
color: #e6e6e6;
padding: 0.75em;
border-radius: 4px;
font-size: 0.85em;
line-height: 1.35;
max-height: none;
overflow: auto;
}
.code .kw {
color: #569cd6;
}
.code .str {
color: #ce9178;
}
.code .com {
color: #6a9955;
font-style: italic;
}
@media (max-width: 720px) {
.demo {
grid-template-columns: 1fr;
}
}
.ok {
color: #096;
}
.err {
color: #c33;
}
</style>
</head>
<body>
<h1>rosocket demo</h1>
<p>
Browser ↔ ROS 2 over plain WebSocket. Adjust the bridge URL if your WSL
IP differs from <code>localhost</code>.
</p>
<p>
Bridge:
<input id="bridge" type="text" value="ws://localhost:9000" />
</p>

<h2>Topic: /chatter (std_msgs/msg/String)</h2>
<div class="demo">
<div class="controls">
<p>
<button id="subBtn">Subscribe</button>
<button id="unsubBtn" disabled>Unsubscribe</button>
</p>
<p>
<input id="pubText" type="text" value="hello from browser" />
<button id="pubBtn">Publish</button>
</p>
<div class="log" id="topicLog"></div>
</div>
<pre class="code"><span class="com">// Subscribe — every published message arrives as onmessage.</span>
<span class="kw">const</span> sub = <span class="kw">new</span> WebSocket(<span class="str">'ws://localhost:9000/topic/chatter'</span>);
sub.onmessage = (ev) =&gt; console.log(<span class="str">'recv:'</span>, ev.data);
<span class="com">// e.g. {"data":"hi"}</span>

<span class="com">// Publish — open, send JSON, close.</span>
<span class="kw">const</span> ws = <span class="kw">new</span> WebSocket(<span class="str">'ws://localhost:9000/topic/chatter'</span>);
ws.onopen = () =&gt; {
ws.send(JSON.stringify({ data: <span class="str">'hello from browser'</span> }));
setTimeout(() =&gt; ws.close(), <span class="str">200</span>); <span class="com">// let it flush</span>
};</pre>
</div>

<h2>Service: /add_two_ints (example_interfaces/srv/AddTwoInts)</h2>
<div class="demo">
<div class="controls">
<p>
a: <input id="a" type="number" value="2" /> b:
<input id="b" type="number" value="3" />
<button id="callBtn">Call</button>
</p>
<div class="log" id="serviceLog"></div>
</div>
<pre class="code"><span class="com">// Service call — send {id, request}, await one response, close.</span>
<span class="kw">const</span> ws = <span class="kw">new</span> WebSocket(<span class="str">'ws://localhost:9000/service/add_two_ints'</span>);
ws.onopen = () =&gt; ws.send(JSON.stringify({
id: <span class="str">1</span>,
request: { a: <span class="str">'2n'</span>, b: <span class="str">'3n'</span> }, <span class="com">// int64 → "Nn" string</span>
}));
ws.onmessage = (ev) =&gt; {
console.log(<span class="str">'resp:'</span>, ev.data);
<span class="com">// {"id":1,"response":{"sum":"5n"}}</span>
ws.close();
};</pre>
</div>

<script>
const $ = (id) => document.getElementById(id);
const bridge = () => $('bridge').value.replace(/\/+$/, '');

function log(el, msg, cls) {
const line = document.createElement('div');
if (cls) line.className = cls;
const ts = new Date().toLocaleTimeString();
line.textContent = `[${ts}] ${msg}`;
el.prepend(line);
Comment on lines +156 to +160
}

// --- Topic subscription ---
let subSocket = null;
$('subBtn').onclick = () => {
if (subSocket) return;
subSocket = new WebSocket(`${bridge()}/topic/chatter`);
subSocket.onopen = () => {
log($('topicLog'), 'subscribed to /chatter', 'ok');
$('subBtn').disabled = true;
$('unsubBtn').disabled = false;
};
subSocket.onmessage = (ev) => {
log($('topicLog'), `recv: ${ev.data}`);
};
subSocket.onerror = () => log($('topicLog'), 'socket error', 'err');
subSocket.onclose = () => {
log($('topicLog'), 'subscription closed');
subSocket = null;
$('subBtn').disabled = false;
$('unsubBtn').disabled = true;
};
};
$('unsubBtn').onclick = () => subSocket && subSocket.close();

// --- Topic publish (separate ephemeral connection) ---
$('pubBtn').onclick = () => {
const ws = new WebSocket(`${bridge()}/topic/chatter`);
ws.onopen = () => {
const msg = { data: $('pubText').value };
ws.send(JSON.stringify(msg));
log($('topicLog'), `pub: ${JSON.stringify(msg)}`, 'ok');
// Give the publisher a moment to flush before closing.
setTimeout(() => ws.close(), 200);
};
ws.onerror = () => log($('topicLog'), 'publish error', 'err');
};

// --- Service call ---
$('callBtn').onclick = () => {
const ws = new WebSocket(`${bridge()}/service/add_two_ints`);
const a = $('a').value;
const b = $('b').value;
ws.onopen = () => {
// AddTwoInts.a/b are int64; the bridge expects BigInt-encoded
// strings ("12n") for 64-bit integer fields. Plain JSON numbers
// are not coerced because the bridge has no schema info.
const request = { a: `${a}n`, b: `${b}n` };
ws.send(JSON.stringify({ id: 1, request }));
log($('serviceLog'), `call: a=${a} b=${b}`, 'ok');
};
ws.onmessage = (ev) => {
log($('serviceLog'), `resp: ${ev.data}`);
ws.close();
};
ws.onerror = () => log($('serviceLog'), 'service error', 'err');
};
</script>
</body>
</html>
Loading
Loading