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
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,10 @@ how much glue you want to write.
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`._
`curl -X POST http://<host>/capability/call/<name>`, with `subscribe`
streaming as Server-Sent Events (`GET .../capability/subscribe/<name>`) —
so shell scripts, Postman, AI-agent tool-use, and even a bare browser
`fetch()` / `EventSource` (CORS-enabled) just work.

```ts
import { connect } from 'rclnodejs/web';
Expand All @@ -135,7 +136,6 @@ how much glue you want to write.
- **[`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
Expand Down
17 changes: 16 additions & 1 deletion bin/rclnodejs-web.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
parseArgv,
loadConfigFile,
mergeConfig,
validateConfig,
HELP,
} from '../lib/runtime/cli-config.js';

Expand Down Expand Up @@ -51,6 +52,11 @@ const argv = process.argv.slice(2);
let cfg;
try {
cfg = mergeConfig(loadConfigFile(parsed.configPath), parsed.partial);
// Re-validate the merged result: loadConfigFile only validates the
// JSON file, so values supplied purely via flags (e.g.
// `--http-sse-keep-alive foo` → NaN, or a non-numeric `--http-port`)
// would otherwise reach the transport unchecked.
validateConfig(cfg, 'options');
} catch (e) {
fail(e);
}
Expand Down Expand Up @@ -91,6 +97,12 @@ const argv = process.argv.slice(2);
port: cfg.http.port,
host: cfg.http.host || cfg.host,
basePath: cfg.http.basePath || cfg.path,
sse: cfg.http.sse,
sseKeepAliveMs:
cfg.http.sseKeepAliveMs != null
? cfg.http.sseKeepAliveMs
: undefined,
cors: cfg.http.cors,
})
);
}
Expand Down Expand Up @@ -118,8 +130,11 @@ const argv = process.argv.slice(2);
if (httpTransport) {
const httpHost = displayHost(cfg.http.host || cfg.host);
const httpBase = cfg.http.basePath || cfg.path;
const httpKinds = cfg.http.sse
? 'call/publish + subscribe (SSE)'
: 'call/publish only';
process.stdout.write(
` also http://${httpHost}:${httpTransport.port}${httpBase} (call/publish only)\n`
` also http://${httpHost}:${httpTransport.port}${httpBase} (${httpKinds})\n`
);
}
}
Expand Down
101 changes: 82 additions & 19 deletions demo/web/javascript/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@ 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 + 1 Hz
`/web_demo_tick` publisher so every panel has live data.
`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/`](../../../web/) folder so the page can `import`
Expand All @@ -45,7 +47,7 @@ Open <http://localhost:8080/> in any modern browser. Runtime in shell
const reply = await ros.call('/add_two_ints', { a: '2n', b: '40n' });
console.log(reply.sum); // '42n'

await ros.subscribe('/web_demo_tick', (msg) => render(msg.data));
await ros.subscribe('/web_demo_chatter', (msg) => render(msg.data));
await ros.publish('/web_demo_chatter', { data: 'hi' });
</script>
```
Expand All @@ -55,34 +57,95 @@ can flip the SDK between the two without restarting.

## Same capability, no SDK

Every `call` / `publish` is also reachable as plain HTTP — drive the
runtime from `curl`, Postman, or an AI agent without any JavaScript:
Every `call` / `publish` is reachable as plain HTTP — drive the runtime
from `curl`, Postman, or an AI agent, no JavaScript required:

```bash
curl -sS -X POST http://localhost:9001/capability/call/add_two_ints \
-H 'content-type: application/json' \
-d '{"a":"7n","b":"35n"}'
-H 'content-type: application/json' -d '{"a":"7n","b":"35n"}'
# => {"sum":"42n"}
```

Subscribe stays on WebSocket.
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:

```bash
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:

```js
// 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.

### Pair it with your own publisher

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`:

```bash
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"}
```

## Without the bundled `runtime.mjs`

`runtime.mjs` bundles the rclnodejs/web runtime and the demo's sample
ROS 2 nodes (the `/add_two_ints` service + the `/web_demo_tick`
publisher) into one process so the demo runs out of the box. In a
real project you already have those ROS 2 nodes running elsewhere,
so you only need the runtime. **Replace shell 1's `node runtime.mjs`
with the CLI** — shell 2 (`node static.mjs`) and the browser code are
unchanged:
`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):

```bash
# shell 1 (instead of `node runtime.mjs`); the `-p rclnodejs` tells npx
# the `rclnodejs-web` binary lives inside the `rclnodejs` package:
# the `-p rclnodejs` tells npx the binary lives in the rclnodejs package:
npx -p rclnodejs rclnodejs-web web.json

# the publisher / service the demo expects:
# plus the service the demo expects (and any std_msgs/String publisher
# on /web_demo_chatter):
ros2 run demo_nodes_cpp add_two_ints_server
# (and a publisher of std_msgs/String on /web_demo_tick from any source)
```

> The bundled `runtime.mjs` enables SSE + CORS via
> `new HttpTransport({ sse: true, cors: true })`. The CLI does the same
> with `--http-sse` / `--http-cors` (or `"http": { "sse": true, "cors":
> "*" }` in `web.json`).
Loading
Loading