UNPKG

rclnodejs

Version:
162 lines (127 loc) 6.41 kB
# rclnodejs/web — Browser SDK guide > Talk to ROS 2 from a web app — typed, allow-listed, `curl`-able. `rclnodejs/web` is the browser-side of `rclnodejs`: a compact ESM module plus a server runtime that together expose a declarative subset of your ROS 2 graph over WebSocket **and** plain HTTP. The browser API is three verbs — `call`, `publish`, `subscribe` — typed end-to-end from your ROS 2 message and service types. For runnable code see [`demo/web/`](../demo/web/): | Demo | Pick this if you… | | ------------------------------------------------- | --------------------------------------------------------------------------- | | [`demo/web/javascript/`](../demo/web/javascript/) | want a single static page — no build tools, no `npm install` for the page | | [`demo/web/typescript/`](../demo/web/typescript/) | already have a Vite / Next / React / Vue / Svelte project, want full typing | ## 1. Server side: stand up the runtime > `-p rclnodejs` tells npx the `rclnodejs-web` binary lives inside the > `rclnodejs` package; drop it once `rclnodejs` is already installed in > the current project. ```bash source /opt/ros/<distro>/setup.bash npx -p rclnodejs rclnodejs-web \ --port 9000 --http-port 9001 \ --call /add_two_ints=example_interfaces/srv/AddTwoInts \ --publish /chatter=std_msgs/msg/String \ --subscribe /scan=sensor_msgs/msg/LaserScan # rclnodejs/web listening on ws://localhost:9000/capability (3 capabilities) # also http://localhost:9001/capability (call/publish only) ``` Or feed the same allow-list from `web.json`: ```json { "port": 9000, "http": { "port": 9001 }, "expose": { "call": { "/add_two_ints": "example_interfaces/srv/AddTwoInts" }, "publish": { "/chatter": "std_msgs/msg/String" }, "subscribe": { "/scan": "sensor_msgs/msg/LaserScan" } } } ``` ```bash npx -p rclnodejs rclnodejs-web web.json ``` > The `expose` block is the **public API** your browser depends on. > Anything not listed is rejected with `code: 'not_exposed'` before > any ROS 2 API runs. Keep it narrow. ## 2. Client side: talk to it from the browser ### Connect ```ts import { connect } from 'rclnodejs/web'; // or via esm.sh in a <script type="module"> ``` `connect()` accepts three URL shapes — the SDK picks transport(s) from the scheme: | You want… | Pass | | ---------------------------------- | --------------------------------------------------------------- | | WebSocket only | `'ws://host:9000/capability'` | | HTTP + WS behind one reverse proxy | `'http://host:9001'` | | HTTP + WS on different ports | `{ http: 'http://host:9001', ws: 'ws://host:9000/capability' }` | | HTTP only (no `subscribe()`) | `{ http: 'http://host:9001' }` | A bare `http://` URL auto-derives the WS sibling at the same origin (`/capability` path); the `{ http }`-only form disables WS entirely and `subscribe()` rejects with `transport_unavailable`. ```ts const ros = await connect({ http: 'http://localhost:9001', ws: 'ws://localhost:9000/capability', }); ``` ### The verb API The snippet below is **TypeScript** — the `<'pkg/.../Type'>` generic in angle brackets is what drives end-to-end typing of the payload and reply from your ROS 2 message types (no codegen, no shared types module). From plain JavaScript, drop the generic and the calls behave identically. ```ts // Service call — '7n' / '35n' are the string forms of BigInt 7n / 35n; // ROS 2 64-bit integers round-trip as strings to survive JSON. const reply = await ros.call<'example_interfaces/srv/AddTwoInts'>( '/add_two_ints', { a: '7n', b: '35n' } ); reply.sum; // typed as `${number}n`, runtime value '42n' // Publish — resolves to undefined on success await ros.publish<'std_msgs/msg/String'>('/chatter', { data: 'hello' }); // Subscribe — always uses WebSocket const sub = await ros.subscribe<'std_msgs/msg/String'>('/chatter', (msg) => console.log(msg.data) ); await sub.close(); ``` ### Lifecycle and cleanup Each `subscribe()` returns a handle with its own `close()`; the top-level `ros.close()` cancels every active subscription and shuts down both transports. ```ts const sub = await ros.subscribe('/chatter', handler); // … sub.close(); // drop just this subscription await ros.close(); // tear down the whole connection // Typical browser cleanup: window.addEventListener('beforeunload', () => ros.close()); ``` ## 3. curl recipes (no JavaScript at all) When `--http-port` is on, every `call` / `publish` is reachable from any HTTP client — curl, Postman, AI-agent tool-use, no SDK required. Subscribe stays on WebSocket. ```bash # Service call curl -sS -X POST http://localhost:9001/capability/call/add_two_ints \ -H 'content-type: application/json' \ -d '{"a":"7n","b":"35n"}' # => {"sum":"42n"} # Publish (returns 204 No Content) curl -sS -X POST http://localhost:9001/capability/publish/chatter \ -H 'content-type: application/json' \ -d '{"data":"hi from curl"}' ``` ## 4. `rclnodejs/web` vs. `rosbridge` + `roslibjs` `rosbridge` + `roslibjs` is the standard browser-side ROS 2 stack of the past decade. Both stacks target the same job (talk to ROS 2 from a web app over WebSocket + JSON) and both keep the browser facing topics/services rather than inventing a higher-level abstraction. What differs is **what's exposed to the browser, how strongly it's typed, and whether plain HTTP works**: | | **`rclnodejs/web`** | `rosbridge` + `roslibjs` | | --------------------------- | -------------------------------------------------------------------- | --------------------------------- | | **Public API surface** | **`web.json` allow-list — reviewable artifact** | The whole live ROS graph | | **TypeScript types** | One ROS 2 type name → fully typed request/response/message | `any`; bolt-on community packages | | **HTTP `call` / `publish`** | ✅ — `curl`, Postman, AI-agent tool-use just work | ❌ (WebSocket only) |