Add lightweight WebSocket bridge for browser access to ROS 2#1495
Add lightweight WebSocket bridge for browser access to ROS 2#1495minggangw merged 3 commits intoRobotWebTools:developfrom
Conversation
There was a problem hiding this comment.
Pull request overview
Introduces a new web_bridge/ module providing an in-process WebSocket bridge that exposes ROS 2 topics and services via resource-style URLs, plus a runnable CLI and accompanying documentation/tests.
Changes:
- Added
startWebBridge()implementation (/topic/<name>,/service/<name>) and module entrypoint. - Added a
rclnodejs-web-bridgeCLI (andnpm run web-bridge) plus runtime dependency onws. - Added documentation and Mocha coverage for bridge behavior and framing conventions.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| web_bridge/bridge.js | Core WebSocket server + per-connection topic/service routing and JSON (de)serialization helpers. |
| web_bridge/cli.js | CLI wrapper around startWebBridge() with argument parsing and shutdown handling. |
| web_bridge/index.js | Subpath module entrypoint exporting startWebBridge. |
| web_bridge/README.md | Detailed user-facing docs for URL scheme, usage, and conventions. |
| test/test-web-bridge.js | Mocha tests validating path/type rejection and basic pub/sub + service round-trips. |
| package.json | Adds ws dependency, CLI bin entry, and web-bridge npm script. |
| README.md | Top-level documentation entry pointing users to the web bridge. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| - 64-bit integer fields may be sent as JSON numbers, BigInt-encoded strings | ||
| (`"12n"`), or decimal strings; responses use the rclnodejs `toJSONSafe` | ||
| encoding (BigInts become `"<n>n"` strings). |
There was a problem hiding this comment.
README states 64-bit integer fields may be sent as JSON numbers, BigInt-encoded strings ("12n"), or decimal strings, but the current reviveBigInts() implementation only converts strings that match /^-?\d+n$/ and will leave plain decimal strings as strings. Either update the documentation to match the actual accepted input formats, or extend reviveBigInts() to also parse decimal-string integers into BigInt (careful to not break non-integer string fields).
| - 64-bit integer fields may be sent as JSON numbers, BigInt-encoded strings | |
| (`"12n"`), or decimal strings; responses use the rclnodejs `toJSONSafe` | |
| encoding (BigInts become `"<n>n"` strings). | |
| - 64-bit integer fields may be sent as JSON numbers or BigInt-encoded | |
| strings (`"12n"`); responses use the rclnodejs `toJSONSafe` encoding | |
| (BigInts become `"<n>n"` strings). |
| * @param {(req: import('http').IncomingMessage) => boolean} [options.verifyClient] - Optional auth hook. | ||
| * @returns {Promise<{wss: WebSocketServer, close: () => Promise<void>, port: number}>} |
There was a problem hiding this comment.
The JSDoc for options.verifyClient says it is called as (req: IncomingMessage) => boolean, but startWebBridge passes this option straight through to ws.WebSocketServer. In ws, verifyClient is invoked with an info object (and optionally a callback), not the raw IncomingMessage, so consumers following this JSDoc will likely implement the hook with the wrong signature. Consider wrapping ws's verifyClient to call the user hook with info.req, and update the JSDoc type/signature accordingly.
| @@ -0,0 +1,6 @@ | |||
| // Copyright (c) 2026 RobotWebTools. Apache-2.0. | |||
There was a problem hiding this comment.
This file uses a one-line copyright/SPDX-style header that differs from the Apache 2.0 license header block used throughout the repo’s JS sources (e.g., index.js, lib/message_serialization.js). To keep licensing/headers consistent across the codebase, update this header to match the standard Apache 2.0 header text used in other files.
| // Copyright (c) 2026 RobotWebTools. Apache-2.0. | |
| /* | |
| * Copyright 2026 RobotWebTools | |
| * | |
| * Licensed under the Apache License, Version 2.0 (the "License"); | |
| * you may not use this file except in compliance with the License. | |
| * You may obtain a copy of the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, software | |
| * distributed under the License is distributed on an "AS IS" BASIS, | |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| * See the License for the specific language governing permissions and | |
| * limitations under the License. | |
| */ |
| opts.port = Number(need(i, a)); | ||
| i++; |
There was a problem hiding this comment.
--port is parsed with Number(...), but there’s no validation that the result is a finite integer in a valid TCP port range. Passing a non-numeric value (or NaN) will later fail inside ws with a less actionable error. Add an explicit check (e.g., Number.isInteger, 0–65535) and exit with a clear message if invalid.
Adds a lightweight WebSocket bridge built into rclnodejs so a plain browser (or any WebSocket-capable client) can talk to ROS 2 with no JavaScript library on the client side — built-in
WebSocket+JSONare enough.Highlights
ws://host:port/topic/<name>?type=<pkg>/msg/<Type>andws://host:port/service/<name>?type=<pkg>/srv/<Type>; the URL is the topic/service, the WS frame is the ROS message as JSON.rclnodejsapp — no extra Python service to deploy or version-match against ROS distros.rosbridge_suite+roslibjsfor the common pub/sub + service-client cases (full comparison table inrosocket/README.md).binentry —npx rosocket --port 9000 --topic /chatter:std_msgs/msg/String; supports repeatable--topic/--servicedefaults,--host,--node-name, integer-validated--port, and clean SIGINT/SIGTERM shutdown with a hard-exit fallback.const { startRosocket } = require('rclnodejs/rosocket'); await startRosocket({ node, port, topicTypes, serviceTypes, verifyClient }).topicTypes/serviceTypes) let browsers omit?type=; without them the bridge stays generic.{a,b}request shape or wrapped{id, request:{...}}for concurrent in-flight call correlation; responses echo the same shape."12n"); responses use the rclnodejstoJSONSafeencoding.{error: "<msg>"}frames; protocol violations close with1008, server errors with1011.verifyClienthook wrapsws's native(info, cb)callback into the documented(req: IncomingMessage) => booleansignature.topicTypessubscribe,?type=publish, and bare +{id, request}service round-trips, exercised againstexample_interfaces/srv/AddTwoInts(BigInt-encodedint64request fields).Files
rosocket/index.js— implementation (startRosocket, debug namespacerclnodejs:rosocket).rosocket/cli.js—rosocketCLI (also exposed asnpm run rosocket).rosocket/README.md— full feature doc, comparison table, URL scheme, server/CLI/browser snippets.test/test-rosocket.js— mocha suite (7 tests).README.md— new top-level "rosocket — ROS 2 in the browser, no library required" section + TOC entry.package.json— addsws ^8.18.0,bin.rosocket,scripts.rosocket.Availability
Experimental; ships only on
develop. Try it withnpm install RobotWebTools/rclnodejs#develop.Fix: 1494