A single static HTML page that talks to a real ROS 2 graph — just
<script type="module"> and the SDK's ESM file. No bundler, no
npm install for the page itself.
cd demo/web/javascriptShell 1 — runtime + the demo's ROS 2 nodes:
source /opt/ros/<distro>/setup.bash
node runtime.mjs
# rclnodejs/web : ws://localhost:9000/capability
# also http://localhost:9001/capability (call/publish, curl-able)
# also http://localhost:9001/capability/subscribe/<name> (SSE)runtime.mjs exposes a tiny /add_two_ints service and the shared
/web_demo_chatter talker/listener topic (publish from one panel,
receive in the others).
Shell 2 — static-file server (hosts index.html + maps /sdk/* to
the in-repo web/ folder so the page can import
the SDK from a plain URL):
node static.mjs
# Static files : http://localhost:8080/Open http://localhost:8080/ in any modern browser. Runtime in shell
1, page server in shell 2 — so you can swap in nginx / a CDN /
python3 -m http.server 8080 for shell 2 without touching shell 1.
<script type="module">
import { connect } from '/sdk/index.js';
const ros = await connect('ws://localhost:9000/capability');
const reply = await ros.call('/add_two_ints', { a: '2n', b: '40n' });
console.log(reply.sum); // '42n'
await ros.subscribe('/web_demo_chatter', (msg) => render(msg.data));
await ros.publish('/web_demo_chatter', { data: 'hi' });
</script>The page also has a transport toggle (WebSocket vs. HTTP) so you can flip the SDK between the two without restarting.
Every call / publish is reachable as plain HTTP — drive the runtime
from curl, Postman, or an AI agent, no JavaScript required:
curl -sS -X POST http://localhost:9001/capability/call/add_two_ints \
-H 'content-type: application/json' -d '{"a":"7n","b":"35n"}'
# => {"sum":"42n"}The demo also enables SSE (new HttpTransport({ sse: true })), so
subscribe works over HTTP as a text/event-stream — handy for clients
that can't hold a WebSocket open:
curl -N http://localhost:9001/capability/subscribe/web_demo_chatter
# event: ready
# data: {"capability":"/web_demo_chatter","subId":"sse"}
#
# event: message
# data: {"data":"hi from curl"}The page's native EventSource panel (section 6) reads this same
stream — no SDK, no WebSocket. It works cross-origin (:8080 → :9001)
because the demo also enables CORS (new HttpTransport({ sse: true, cors: true })); in production, pass your site's origin instead of true.
The same is true for call / publish from the browser itself: the
native fetch() panel (section 7) hits these endpoints directly —
the curl commands above translate one-to-one to fetch() (same method,
URL, headers and JSON body), again cross-origin thanks to CORS:
// service call → 200 + JSON reply
const res = await fetch(
'http://localhost:9001/capability/call/add_two_ints',
{
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ a: '7n', b: '35n' }),
},
);
console.log((await res.json()).sum); // '42n'
// topic publish → 204 No Content
await fetch('http://localhost:9001/capability/publish/web_demo_chatter', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ data: 'hi from fetch()' }),
});For browser apps, prefer the WebSocket transport for
subscribe— one connection multiplexes every topic. SSE targets the curl / AI-agent / server-side persona.
The runtime also exposes /topic, so you can feed the demo from any ROS 2
node instead of the in-page publisher. Run the stock publisher example in
a third shell, then point the EventSource panel (or curl) at /topic:
source /opt/ros/<distro>/setup.bash
node ../../../example/topics/publisher/publisher-example.mjs
# Publishing message: Hello ROS 0, 1, 2, …
curl -N http://localhost:9001/capability/subscribe/topic
# event: message
# data: {"data":"Hello ROS 0"}runtime.mjs bundles the runtime and the demo's sample nodes into one
process so it runs out of the box. In a real project those nodes already
run elsewhere, so you only need the runtime — replace shell 1 with the
CLI (shell 2 and the browser code are unchanged):
# the `-p rclnodejs` tells npx the binary lives in the rclnodejs package:
npx -p rclnodejs rclnodejs-web web.json
# plus the service the demo expects (and any std_msgs/String publisher
# on /web_demo_chatter):
ros2 run demo_nodes_cpp add_two_ints_serverThe bundled
runtime.mjsenables SSE + CORS vianew HttpTransport({ sse: true, cors: true }). The CLI does the same with--http-sse/--http-cors(or"http": { "sse": true, "cors": "*" }inweb.json).