ros-mobile-bridge 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,40 @@ All notable changes to `ros-mobile-bridge` will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.1.4] - 2026-06-14
8
+
9
+ ### Fixed
10
+
11
+ - **A malformed JSON control message no longer crashes the host.** A bad `advertise` / `unadvertise` / `advertiseServices` frame from a buggy or hostile bridge (for example a non-array `channels`) previously threw out of the WebSocket message handler, surfacing as an uncaughtException on Node and a fatal error on React Native release builds. The Foxglove dispatch now contains handler errors and routes them through the injected logger, and the affected handlers guard their field shapes so a half-valid frame degrades instead of throwing.
12
+ - **`FoxgloveClient.connect()` no longer hangs forever against a wrong or silent endpoint.** A socket that opened but never spoke the Foxglove protocol (the classic "rosbridge port in a Foxglove profile"), or that closed before the handshake completed, left the connect promise pending indefinitely. The connection timeout now covers the full handshake, and a pre-handshake close rejects the promise.
13
+ - **A failed initial `connect()` no longer leaves a background reconnect loop.** Both transports previously rejected the caller yet kept retrying in the background; because the manager only stores the client after a successful connect, that produced an unreachable client holding a socket nobody could disconnect. Auto-reconnect now runs only after a connection has previously succeeded, or while a reconnect cycle is already in progress; first-connect retry is the consumer's responsibility. `ProtocolManager.connect` also disconnects the client when `connect()` rejects.
14
+ - **Circuit-breaker cooldown timers no longer outlive the connection.** Connection teardown now destroys each subscription's breaker, so a tripped breaker's cooldown cannot fire into the next connection (where subscription ids are reused).
15
+ - **`RosbridgeClient` detaches its WebSocket handlers before closing**, so a late `onclose` from a stale socket cannot tear down a newer connection.
16
+ - **A pasted `wss://` or `https://` host no longer silently downgrades to plaintext.** When `secure` is unset the scheme is inferred from the host; an explicit `secure` (true or false) still wins.
17
+
18
+ ### Changed
19
+
20
+ - **`base64ToUint8` no longer uses `atob`.** It decodes with a small lookup table instead, removing a global that is outside the library's supported set and absent on older React Native (Hermes) runtimes. Only the legacy JSON service-response path is affected; the decoded bytes are unchanged.
21
+
22
+ ### Documented
23
+
24
+ - **Corrected the disconnect safety boundary.** The library publishes a zero-Twist stop on `/cmd_vel` only on an intentional disconnect while the socket is open. It cannot stop the robot on an unexpected loss of connectivity (network drop, app kill, crash) because the transport is already gone; network-loss halting requires a robot-side `cmd_vel` watchdog. Stated affirmatively in the README, both client class headers, and the `publishZeroTwist` TSDoc.
25
+ - **Documented post-reconnect behavior:** subscriptions are not re-established after an automatic reconnect, so the consumer watches `onStatusChange` and resubscribes.
26
+ - Added a SECURITY.md threat-model section: inbound size and count caps plus a fuzz harness are scheduled for a later hardening milestone, so connect only to bridges you trust until then. Removed a stale class-header line describing a keep-alive ping that was dropped in v0.1.1.
27
+ - Reordered the ROADMAP milestones (the read-only introspection surface before the Zenoh transport before the hardening milestone).
28
+
29
+ ## [0.1.3] - 2026-06-02
30
+
31
+ ### Added
32
+
33
+ - **`SubscribeOptions.dispatchMode`** controls how throttle-surviving messages reach the callback. `'immediate'` (the default, and the only prior behavior) parses and delivers every surviving message synchronously on the message-handler tick. `'latest-only'` delivers only the newest message under back-pressure: superseded messages are dropped *before* being parsed and delivery is deferred off the message-handler tick, so a high-bandwidth topic like a raw camera stream decodes only the frame you will actually render. The conflation happens upstream of the CDR/JSON decode, which an external wrapper cannot do because it only ever receives already-parsed messages. Available on both Foxglove WS and rosbridge. On a binary (CDR) topic the surviving payload is copied to outlive the deferral, so `'latest-only'` is parse-cheap but not allocation-free; on rosbridge the stashed frame is an immutable string and the stash is copy-free. It composes below the throttle (`maxFrequency` and the adaptive cap decide eligibility, then `'latest-only'` keeps the newest of those). A callback that throws is logged and never wedges the subscription; on unsubscribe, disconnect, or a circuit-breaker trip any pending message is dropped rather than delivered.
34
+ - **`materializeBytes(view: Uint8Array): Uint8Array`** returns an owned, offset-0 copy of a `Uint8Array`. `RosMessage.data`, when it is a `Uint8Array`, is a zero-copy view into the inbound WebSocket frame (v0.1.2); call this before retaining the bytes past the callback or handing them to a native binding that ignores `byteOffset` (some Skia paths, `node-canvas`, `sharp`, FFI). It always copies and never returns the input view, so the result is always safe to retain. This supersedes the v0.1.2 note that anticipated a conditional (skip-when-already-owned) copy: a full-span view can still alias the shared frame buffer, so the conditional form would have skipped the copy in exactly the unsafe case.
35
+ - **`matchesSchema(a: string, b: string): boolean`** compares two ROS schema names tolerant of the ROS 1 / ROS 2 `msg/` (and `srv/`, `action/`) asymmetry, so `sensor_msgs/msg/Image` matches `sensor_msgs/Image`. It is kind-agnostic: it strips the interface-kind segment and compares `pkg` + `Type`. A `normalizeSchema` counterpart is deliberately not exported, because a 2-part name cannot be safely expanded to canonical 3-part form (the interface kind cannot be inferred from the string).
36
+
37
+ ### Changed
38
+
39
+ - **Bundled service-schema lookup now tolerates the 2-part / 3-part name asymmetry.** A bridge advertising a well-known system service as `rcl_interfaces/ListParameters` (without the `srv/` segment) now resolves to the bundled schema, so parameter operations and goal cancellation work regardless of which form the bridge reports. Bridge-advertised schemas remain authoritative.
40
+
7
41
  ## [0.1.2] - 2026-06-01
8
42
 
9
43
  ### Performance
@@ -58,5 +92,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
58
92
 
59
93
  - `ZenohClient` ships as an unimplemented skeleton (every method throws). `ProtocolManager.connect` throws a clear "Zenoh support is planned for v0.2.0" error for `protocol: 'zenoh'`. The class is not exported from `index.ts` in v0.1.0.
60
94
 
95
+ [0.1.4]: https://github.com/AuriLabsTech/ros-mobile-bridge/releases/tag/v0.1.4
96
+ [0.1.3]: https://github.com/AuriLabsTech/ros-mobile-bridge/releases/tag/v0.1.3
97
+ [0.1.2]: https://github.com/AuriLabsTech/ros-mobile-bridge/releases/tag/v0.1.2
61
98
  [0.1.1]: https://github.com/AuriLabsTech/ros-mobile-bridge/releases/tag/v0.1.1
62
99
  [0.1.0]: https://github.com/AuriLabsTech/ros-mobile-bridge/releases/tag/v0.1.0
package/README.md CHANGED
@@ -75,6 +75,14 @@ A consumer can write against `IProtocolClient` once and pick the transport at ru
75
75
  client.publish('/cmd_vel', 'geometry_msgs/msg/Twist', zeroTwist, { priority: 'control' });
76
76
  ```
77
77
 
78
+ ### Safety: stopping the robot on disconnect
79
+
80
+ `publishZeroTwist()` and the control-priority paths send a stop on `/cmd_vel` only while the connection is open, on an **intentional** `disconnect()`, app-background, or E-Stop. They cannot stop the robot on an *unexpected* loss of connectivity (network drop, app kill, crash): the transport is already gone, so no command can leave the device. Halting on network loss must be enforced robot-side, by a `cmd_vel` timeout or watchdog on the robot that stops when commands stop arriving. This library covers intentional teardown; it is not a substitute for that watchdog.
81
+
82
+ ### Reconnection
83
+
84
+ A connection that previously succeeded auto-reconnects with exponential backoff (up to 5 attempts). A failed *initial* `connect()` rejects and does not retry in the background; first-connect retry is the consumer's call. After an automatic reconnect, prior subscriptions are not re-established: watch `onStatusChange` and resubscribe.
85
+
78
86
  ### Adaptive throttle and circuit breaker
79
87
 
80
88
  Both reliability features are observable. Read the current throttle bucket per subscription:
@@ -98,6 +106,27 @@ const unwatch = client.onBreakerStateChange('/camera/compressed', (state) => {
98
106
 
99
107
  Manual breaker controls (`breakerRetry`, `breakerDisable`) let consumers expose user-driven recovery in their UI.
100
108
 
109
+ ### High-throughput subscriptions: `dispatchMode`
110
+
111
+ `SubscribeOptions.dispatchMode` controls how messages reach your callback when they arrive faster than you can use them. The default, `'immediate'`, parses and delivers every message that survives the throttle, synchronously on the message-handler tick. For a high-bandwidth topic where only the freshest message matters (a raw camera stream feeding a viewport), `'latest-only'` delivers just the newest message and drops the rest *before* they are parsed, so you decode only the frame you actually render:
112
+
113
+ ```typescript
114
+ import { materializeBytes } from 'ros-mobile-bridge';
115
+
116
+ client.subscribe(
117
+ '/camera/raw',
118
+ (msg) => {
119
+ if (msg.data instanceof Uint8Array) {
120
+ // msg.data is a zero-copy view; copy it before retaining past the callback.
121
+ render(materializeBytes(msg.data));
122
+ }
123
+ },
124
+ { dispatchMode: 'latest-only' },
125
+ );
126
+ ```
127
+
128
+ Delivery is deferred off the message-handler tick, and the conflation happens upstream of the decode, which a wrapper around `subscribe` cannot do because it only ever sees already-parsed messages. `'latest-only'` composes below the throttle: `maxFrequency` and the adaptive cap still decide which messages are eligible, then the newest of those is delivered. For lossless-but-deferred delivery, keep your own bounded queue in the callback (the `dispatchMode` TSDoc shows the pattern); the bound and drop policy are yours to set because they depend on the device.
129
+
101
130
  ### Host-app injection
102
131
 
103
132
  Construct clients with `ProtocolClientOptions` to receive latency callbacks, route logs, and tell the throttle which mode the user picked:
package/ROADMAP.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  `ros-mobile-bridge` follows a milestone-based roadmap. Each milestone targets a concrete release. Versions are not date-bound; they ship when the criteria for the milestone are met.
4
4
 
5
- The library reached `v0.1.0` with two production-ready transports (Foxglove WebSocket v1 and rosbridge v2) extracted from a real mobile application's protocol layer. The next two milestones expand the transport set, harden the library against real-world parse-side attacks, and grow the community of consumers.
5
+ The library reached `v0.1.0` with two production-ready transports (Foxglove WebSocket v1 and rosbridge v2) extracted from a real mobile application's protocol layer. The roadmap then exposes the robot's ROS 2 graph through a read-only introspection surface, adds a third transport (Zenoh), and finishes with parser-side hardening, community growth, and the v1.0 API freeze.
6
6
 
7
7
  ## v0.1.x — Stabilization (current)
8
8
 
@@ -17,9 +17,26 @@ The `v0.1.x` series consolidates the first public release. Patch releases addres
17
17
 
18
18
  **Out of scope:** any breaking API change, any new transport.
19
19
 
20
- ## v0.2.0 — Zenoh transport
20
+ ## v0.2.0 — Programmatic introspection surface
21
21
 
22
- The `ZenohClient` skeleton already exists in `src/ZenohClient.ts` as roadmap-as-code. `v0.2.0` lands the real implementation.
22
+ `v0.2.0` exposes a robot's ROS 2 graph to programmatic consumers, including AI coding agents. It lands before the Zenoh transport on purpose: the introspection schema interface must be frozen before a third transport implements against it, and it is the lower-risk milestone that front-loads value.
23
+
24
+ Hobbyist and academic ROS 2 users increasingly drive their workflow through AI coding agents (Claude Code, Cursor, Codex, OpenCode). A natural extension is agent-driven configuration of mobile dashboards: the agent already knows the robot's topic graph because it just wrote the nodes, so it can wire widget configurations headlessly and hand the user a ready-to-use dashboard. Prior art in adjacent ecosystems (Marimo's `marimo-pair` skill for collaborative notebook editing, shipped via the cross-tool `skills` package manager) validates the pattern shape.
25
+
26
+ This milestone does **not** build an MCP server inside `ros-mobile-bridge`. Hosting an MCP server requires a runtime-specific transport (stdio on Node, HTTP on browsers / Electron) and would break the four-runtimes-from-one-build promise. Instead, the library exposes the introspection surface an MCP server would need; integrators (mobile apps, CLI tools, agent harnesses) build the transport layer themselves. The command-dispatch safety boundary lives in the integrator that owns the robot connection, not in the library.
27
+
28
+ **Deliverables:**
29
+
30
+ - `getAvailableParameters(): Promise<ParameterInfo[]>` and `getParameter(name)` on `IProtocolClient`, mapped through both Foxglove WebSocket and rosbridge wire protocols. Today the library exposes topic and service introspection but has no concept of ROS parameters; agents driving dashboard configuration may want them for parameter-display widgets.
31
+ - `getSchemaDefinition(schemaName)` returning the parsed message definition (full type information per field), complementing the existing zero-value `getSchemaTemplate`. Returns `null` for transports that do not carry schemas inline (rosbridge), matching `getSchemaTemplate`'s rosbridge fallback shape.
32
+ - Documentation: a dedicated guide page covering "building an MCP server on top of `ros-mobile-bridge`" — the introspection-only contract, the command-dispatch safety boundary (which lives in the consumer, not the library), and worked examples for stdio and HTTP transports.
33
+ - A reference adapter under `examples/agent-mcp/` showing how to wrap the introspection methods as MCP tool definitions, runtime-agnostic, leaving the actual server hosting to the integrator.
34
+
35
+ **Why this matters:** open-source plus AI-agent accessibility for non-experts is what distinguishes a serious robotics library from a thin protocol wrapper. No existing JavaScript ROS 2 client surfaces enough metadata for an agent to drive a dashboard end-to-end. `v0.2.0` ships the contract; downstream projects (including Tinca) ship the experiences on top.
36
+
37
+ ## v0.3.0 — Zenoh transport
38
+
39
+ The `ZenohClient` skeleton already exists in `src/ZenohClient.ts` as roadmap-as-code. `v0.3.0` lands the real implementation, conforming to the `IProtocolClient` interface frozen in `v0.2.0`.
23
40
 
24
41
  **Deliverables:**
25
42
 
@@ -33,13 +50,13 @@ The `ZenohClient` skeleton already exists in `src/ZenohClient.ts` as roadmap-as-
33
50
  **Open questions resolved first (a spike opens this milestone):**
34
51
 
35
52
  - **React Native support is unverified.** `@eclipse-zenoh/zenoh-ts` ships a WebAssembly module for key-expression handling. WASM runs cleanly in browsers, Node, and Electron, but React Native (Hermes) WASM support is limited. If the dependency does not run under React Native, the Zenoh transport may initially target browser / Node / Electron with React Native to follow.
36
- - **Schema acquisition differs from Foxglove WS.** `rmw_zenoh` does not distribute message definitions inline, so CDR decoding needs definitions sourced another way (bundled common interfaces, consumer-provided, or a registry). The spike scopes this.
53
+ - **Schema acquisition differs from Foxglove WS.** `rmw_zenoh` does not distribute message definitions inline, so CDR decoding needs definitions sourced another way (bundled common interfaces, consumer-provided, or a registry). The introspection surface from `v0.2.0` informs this; the spike scopes it.
37
54
 
38
- **Why this matters:** Zenoh is the recommended middleware for the next generation of ROS 2 deployments (it underlies `rmw_zenoh`, which the OSRF has positioned as a serious alternative to the default DDS-based middleware). No existing JavaScript or TypeScript library provides a ROS 2 client over Zenoh for these runtimes; `v0.2.0` aims to close that gap, with mobile-runtime support contingent on the verification above.
55
+ **Why this matters:** Zenoh is the recommended middleware for the next generation of ROS 2 deployments (it underlies `rmw_zenoh`, which the OSRF has positioned as a serious alternative to the default DDS-based middleware). No existing JavaScript or TypeScript library provides a ROS 2 client over Zenoh for these runtimes; `v0.3.0` aims to close that gap, with mobile-runtime support contingent on the verification above.
39
56
 
40
- ## v0.3.0 — Hardening and community
57
+ ## v0.4.0 — Hardening, community & v1.0
41
58
 
42
- `v0.3.0` is the security-and-community milestone. It assumes the library has been in use for several months across multiple consumers and that real-world inputs have surfaced edge cases the test suite did not anticipate.
59
+ `v0.4.0` is the security-and-community milestone, and it carries the library into the `v1.0` API freeze. It assumes the library has been in use for several months across multiple consumers and that real-world inputs have surfaced edge cases the test suite did not anticipate.
43
60
 
44
61
  **Deliverables:**
45
62
 
@@ -49,29 +66,16 @@ The `ZenohClient` skeleton already exists in `src/ZenohClient.ts` as roadmap-as-
49
66
  - Community onboarding work: contributor guide expansion, "good first issue" labels, response-time commitments documented in `CONTRIBUTING.md`, monthly maintainer triage cadence.
50
67
  - A discoverable presence in the ROS 2 ecosystem: ROS Discourse announcement, link from `index.ros.org` if accepted, listing in `awesome-ros2`.
51
68
 
52
- **Why this matters:** the library will by then be a parser of arbitrary input from untrusted bridges. Security-grade quality is what distinguishes a library a serious robotics company will adopt from a hobby project.
53
-
54
- ## v0.4.0 (provisional) — agent-driven introspection surface
55
-
56
- Hobbyist and academic ROS 2 users increasingly drive their workflow through AI coding agents (Claude Code, Cursor, Codex, OpenCode). A natural extension is agent-driven configuration of mobile dashboards: the agent already knows the robot's topic graph because it just wrote the nodes, so it can wire widget configurations headlessly and hand the user a ready-to-use dashboard. Prior art in adjacent ecosystems (Marimo's `marimo-pair` skill for collaborative notebook editing, shipped via the cross-tool `skills` package manager) validates the pattern shape.
57
-
58
- This milestone does **not** build an MCP server inside `ros-mobile-bridge`. Hosting an MCP server requires a runtime-specific transport (stdio on Node, HTTP on browsers / Electron) and would break the four-runtimes-from-one-build promise. Instead, the library exposes the introspection surface an MCP server would need; integrators (mobile apps, CLI tools, agent harnesses) build the transport layer themselves. The command-dispatch safety boundary lives in the integrator that owns the robot connection, not in the library.
59
-
60
- **Deliverables:**
69
+ > Note: individual crash and robustness fixes against malformed input ship as `v0.1.x` bug-fixes whenever a consumer surfaces them (e.g. "don't let one malformed control frame kill the host"). This milestone is the *systematic* pass: the third-party audit, the property-based fuzz harness, and the oversized-message / DoS / prototype-pollution coverage as a whole. The two are complementary, not a contradiction.
61
70
 
62
- - `getAvailableParameters(): Promise<ParameterInfo[]>` and `getParameter(name)` on `IProtocolClient`, mapped through both Foxglove WebSocket and rosbridge wire protocols. Today the library exposes topic and service introspection but has no concept of ROS parameters; agents driving dashboard configuration may want them for parameter-display widgets.
63
- - `getSchemaDefinition(schemaName)` returning the parsed message definition (full type information per field), complementing the existing zero-value `getSchemaTemplate`. Returns `null` for transports that do not carry schemas inline (rosbridge), matching `getSchemaTemplate`'s rosbridge fallback shape.
64
- - Documentation: a dedicated guide page covering "building an MCP server on top of `ros-mobile-bridge`" — the introspection-only contract, the command-dispatch safety boundary (which lives in the consumer, not the library), and worked examples for stdio and HTTP transports.
65
- - A reference adapter under `examples/agent-mcp/` showing how to wrap the introspection methods as MCP tool definitions, runtime-agnostic, leaving the actual server hosting to the integrator.
66
-
67
- **Why this matters:** open-source plus AI-agent accessibility for non-experts is what distinguishes a serious robotics library from a thin protocol wrapper. No existing JavaScript ROS 2 client surfaces enough metadata for an agent to drive a dashboard end-to-end. `v0.4.0` ships the contract; downstream projects (including Tinca) ship the experiences on top.
71
+ **Why this matters:** the library will by then be a parser of arbitrary input from untrusted bridges. Security-grade quality is what distinguishes a library a serious robotics company will adopt from a hobby project.
68
72
 
69
73
  ## Beyond v1.0
70
74
 
71
75
  `v1.0.0` is not date-bound. It represents the point at which the public API has stabilized enough that breaking changes become exceptional rather than routine. Reaching `v1.0` requires:
72
76
 
73
77
  - At least six months of `v0.x` series usage across multiple consumers without API-breaking pressure.
74
- - The security audit from `v0.3.0` completed with no unresolved high-severity findings.
78
+ - The security audit from `v0.4.0` completed with no unresolved high-severity findings.
75
79
  - Documentation site complete: every public symbol with TSDoc, every public concept with a dedicated guide page, every transport with a runnable example.
76
80
  - Stable performance characteristics documented (throughput per transport, memory footprint, supported message sizes).
77
81
 
package/SECURITY.md CHANGED
@@ -36,6 +36,14 @@ A CVE is requested when applicable.
36
36
  - Vulnerabilities in transitive dependencies that do not affect this library's runtime behavior. Report those to the relevant upstream package.
37
37
  - Misconfigurations in the host application that consumes this library.
38
38
 
39
+ ## Threat Model and Current Limitations
40
+
41
+ The library is designed to connect to bridges the consumer chooses (a robot they own), not arbitrary untrusted endpoints. With that in mind:
42
+
43
+ - **Malformed frames are contained.** A malformed control message from a buggy or hostile bridge is caught and logged rather than crashing the host application.
44
+ - **Inbound size and count limits are not yet enforced.** A hostile endpoint could stream unbounded channel or service advertisements, or a single very large frame, to exhaust memory or block the JS thread. Per-message size caps, channel-count limits, and a property-based fuzz harness over the parser entry points are scheduled for the systematic hardening milestone (v0.4.0). Until then, connect only to bridges you trust, and keep untrusted bridges behind a network boundary you control.
45
+ - **Transport encryption is opt-in.** Plaintext `ws://` is the default; pass `secure: true` (or paste a `wss://` host) for TLS. On untrusted networks, use `wss://`.
46
+
39
47
  ## Supported Versions
40
48
 
41
49
  | Version | Supported |