/** * useRosbridge.js — React hook for ROS2 rosbridge_server WebSocket connection. * * rosbridge_server default: ws://:9090 * Provides subscribe/publish/callService helpers bound to the active connection. */ import { useState, useEffect, useRef, useCallback } from 'react'; import ROSLIB from 'roslib'; export function useRosbridge(url) { const [connected, setConnected] = useState(false); const [error, setError] = useState(null); const rosRef = useRef(null); const subscribersRef = useRef(new Map()); // topic -> ROSLIB.Topic useEffect(() => { if (!url) return; const ros = new ROSLIB.Ros({ url }); rosRef.current = ros; ros.on('connection', () => { setConnected(true); setError(null); }); ros.on('error', (err) => { setError(err?.toString() || 'Connection error'); }); ros.on('close', () => { setConnected(false); }); return () => { subscribersRef.current.forEach((topic) => topic.unsubscribe()); subscribersRef.current.clear(); ros.close(); rosRef.current = null; }; }, [url]); /** Subscribe to a ROS2 topic. Returns an unsubscribe function. */ const subscribe = useCallback((name, messageType, callback) => { if (!rosRef.current) return () => {}; const key = `${name}::${messageType}`; if (subscribersRef.current.has(key)) { subscribersRef.current.get(key).unsubscribe(); } const topic = new ROSLIB.Topic({ ros: rosRef.current, name, messageType, }); topic.subscribe(callback); subscribersRef.current.set(key, topic); return () => { topic.unsubscribe(); subscribersRef.current.delete(key); }; }, []); /** Publish a single message to a ROS2 topic. */ const publish = useCallback((name, messageType, data) => { if (!rosRef.current) return; const topic = new ROSLIB.Topic({ ros: rosRef.current, name, messageType, }); topic.publish(new ROSLIB.Message(data)); }, []); /** Call a ROS2 service. Returns a Promise resolving to the response. */ const callService = useCallback((name, serviceType, request = {}) => { return new Promise((resolve, reject) => { if (!rosRef.current) { reject(new Error('Not connected')); return; } const svc = new ROSLIB.Service({ ros: rosRef.current, name, serviceType, }); svc.callService(new ROSLIB.ServiceRequest(request), resolve, reject); }); }, []); /** Set ROS2 node parameters via rcl_interfaces/srv/SetParameters. */ const setParam = useCallback((nodeName, params) => { // params: { name: string, type: 'bool'|'int'|'double'|'string', value: any }[] const TYPE_MAP = { bool: 1, integer: 2, double: 3, string: 4, int: 2 }; const parameters = params.map(({ name, type, value }) => { const typeInt = TYPE_MAP[type] ?? 4; const valueKey = { 1: 'bool_value', 2: 'integer_value', 3: 'double_value', 4: 'string_value', }[typeInt]; return { name, value: { type: typeInt, [valueKey]: value } }; }); return callService( `/${nodeName}/set_parameters`, 'rcl_interfaces/srv/SetParameters', { parameters } ); }, [callService]); return { connected, error, subscribe, publish, callService, setParam }; }