@fibril/react-roslib
v0.1.0
Published
React hooks for roslib 2.x (rosbridge): connections, topics, services, actions.
Readme
@fibril/react-roslib
React hooks for roslib 2.x, for robot web UIs talking to ROS 2 through rosbridge.
- Several robots at once, each with its own auto-reconnecting connection
- Topics, services and ROS 2 actions as hooks built on
useSyncExternalStore - Throttling done by rosbridge (
throttle_rate), not in the browser - Pure conversion functions (quaternion ⇄ yaw, odometry → pose, diagnostics, gamepad → Joy) exported alongside the hooks
vp add @fibril/react-roslib roslibPeer dependencies: react 18.3+ or 19, roslib 2.1+.
Connecting
import { RosProvider, useRosStatus } from "@fibril/react-roslib";
function App() {
return (
<RosProvider name="r1" url="ws://192.168.1.10:9090">
<RosProvider name="r2" url="ws://192.168.1.11:9090">
<Dashboard />
</RosProvider>
</RosProvider>
);
}
function Dashboard() {
const r1 = useRosStatus("r1"); // "idle" | "connecting" | "connected" | "reconnecting" | "closed"
const r2 = useRosStatus("r2");
// ...
}Hooks take { ros: "r1" } to pick a connection; without it they use the nearest provider.
Changing url closes the old connection and opens a new one.
Storing the URL (e.g. in localStorage) is left to the app; toRosbridgeUrl("192.168.1.10") turns a host into ws://192.168.1.10:9090.
RosClient is the connection itself and does not depend on React. Pass one with <RosProvider client={client}> to manage it yourself.
Topics
Define each topic once, with its name, interface type and TypeScript type:
import { defineTopic, withNamespace } from "@fibril/react-roslib";
import type { geometry_msgs, nav_msgs } from "@fibril/react-roslib/msgs";
export const odom = defineTopic<nav_msgs.Odometry>("odometry/global", "nav_msgs/msg/Odometry");
export const cmdVel = defineTopic<geometry_msgs.Twist>("/r1/cmd_vel", "geometry_msgs/msg/Twist");
const r1Odom = withNamespace(odom, "/r1"); // "/r1/odometry/global"import { usePublisher, useSubscription, useTopic, useTopicHistory } from "@fibril/react-roslib";
import { odometryToPose2D, twist2D } from "@fibril/react-roslib/msgs";
const msg = useTopic(r1Odom, { ros: "r1", throttleRate: 100 }); // latest message or undefined
const pose = msg && odometryToPose2D(msg);
const last50 = useTopicHistory(r1Odom, 50, { ros: "r1" }); // oldest first
useSubscription(r1Odom, (m) => log(m), { ros: "r1" }); // no re-render
const publish = usePublisher(cmdVel, { ros: "r1" }); // stable function
publish(twist2D(0.5, 0, 0)); // returns false (dropped) while disconnectedusePublisher drops messages while disconnected by default, because roslib would otherwise queue them and replay them all on reconnect.
Services and actions
import { defineAction, defineService, useAction, useServiceClient } from "@fibril/react-roslib";
import type { std_srvs } from "@fibril/react-roslib/msgs";
const reset = defineService<std_srvs.TriggerRequest, std_srvs.TriggerResponse>(
"/r1/reset",
"std_srvs/srv/Trigger",
);
const call = useServiceClient(reset, { ros: "r1", timeout: 2 });
const res = await call({}); // rejects with RosNotConnectedError / RosServiceError
const navigate = defineAction<Goal, Feedback, Result>("/r1/navigate", "my_msgs/action/Navigate");
const { state, send, cancel } = useAction(navigate, { ros: "r1" });
// state.status: "idle" | "active" | "succeeded" | "failed", with feedback / result / errorOutside React, use callService(client, def, request) and sendActionGoal(client, def, goal).
Graph
useTopicList(), useNodeList() and useServiceList() fetch through rosapi on every (re)connection and return { data, error, loading, refresh }.
getTopics(ros), getNodes(ros) and getServices(ros) are the promise versions.
Extras
| Import | Contents |
| ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| @fibril/react-roslib/msgs | Types for common interfaces (std_msgs, geometry_msgs, nav_msgs, sensor_msgs, diagnostic_msgs, std_srvs) and pure conversions: quaternionToYaw, yawToQuaternion, poseToPose2D, odometryToPose2D, pose2DToPoseWithCovarianceStamped, createHeader, msToTime, timeToMs, diagonalCovariance, twist2D |
| @fibril/react-roslib/diagnostics | useDiagnostics(topic), and normalizeDiagnostics, sortDiagnostics, mergeDiagnostics, summarizeDiagnostics |
| @fibril/react-roslib/gamepad | useGamepadJoyPublisher(topic), useGamepadCount(), and standardGamepadToJoy, gamepadToJoy, readGamepad |
Development
vp install
vp check # format, lint, type check
vp test # unit tests with an in-memory rosbridge transport
vp pack # build dist/Releases: bump version in package.json, merge, then push a v<version> tag. .github/workflows/release.yml publishes to npm with trusted publishing (OIDC).
License
MIT
