import { z } from 'zod'; /** * Data types matching roslib's Vector3, Quaternion, and Transform classes. * These are simple data containers used by the TF system. */ declare class Vector3 { x: number; y: number; z: number; constructor(options?: { x?: number; y?: number; z?: number; }); } declare class Quaternion { x: number; y: number; z: number; w: number; constructor(options?: { x?: number; y?: number; z?: number; w?: number; }); } declare class Transform { translation: Vector3; rotation: Quaternion; constructor(options?: { translation?: Vector3; rotation?: Quaternion; }); static identity(): Transform; /** * Invert a rigid transform: if `this` is a→b, the result is b→a. For a unit * quaternion the inverse rotation is its conjugate, and the inverse * translation is that conjugate applied to the negated translation. */ inverse(): Transform; /** * Compose two rigid transforms: `this` (a→b) composed with `other` (b→c) * yields the transform from a→c. */ multiply(other: Transform): Transform; } /** Channel info advertised by foxglove_bridge. */ interface FoxgloveChannel { id: number; topic: string; encoding: string; schemaName: string; schema: string; schemaEncoding?: string; } /** * Service schema side (request or response) as advertised by `foxglove.sdk.v1`. * The legacy `foxglove.websocket.v1` protocol used flat `requestSchema`/ * `responseSchema` fields; the sdk.v1 protocol nests the encoding metadata * alongside the schema. */ interface FoxgloveServiceSchemaSide { encoding?: string; schemaName?: string; schemaEncoding?: string; schema: string; } /** Service info advertised by foxglove_bridge. Accepts both wire shapes. */ interface FoxgloveService { id: number; name: string; type: string; requestSchema?: string; responseSchema?: string; request?: FoxgloveServiceSchemaSide; response?: FoxgloveServiceSchemaSide; } interface ActionFeedback { action: string; id: string; values: TFeedback; feedback: TFeedback; } interface ActionResult { action: string; id: string; status: number; result: boolean; accepted: boolean; values: TResult; response: Record; } declare const GoalStatus: { readonly STATUS_UNKNOWN: 0; readonly STATUS_ACCEPTED: 1; readonly STATUS_EXECUTING: 2; readonly STATUS_CANCELING: 3; readonly STATUS_SUCCEEDED: 4; readonly STATUS_CANCELED: 5; readonly STATUS_ABORTED: 6; }; /** * Foxglove WebSocket protocol client. * * Implements the Foxglove WebSocket protocol for communicating with * foxglove_bridge. Handles channel advertisements, subscriptions, service * calls, and parameter operations. * * Two subprotocol negotiation strings exist in the wild with the same wire * format: `foxglove.websocket.v1` (legacy websocket_server-based builds) and * `foxglove.sdk.v1` (newer foxglove-sdk-cpp-based builds, including the * ros-humble-foxglove-bridge 3.2.x apt package). We advertise both so either * server accepts the handshake. * * Control-plane JSON messages (the server→client traffic handled here) are * validated via the zod schemas in `./wire-schemas` at the transport edge * so the rest of the adapter can work with properly typed values without * resorting to `as` casts. The authoritative wire format still lives in the * (archived) foxglove/ws-protocol spec, which is what ros-foxglove-bridge * 3.2.x continues to implement. */ /** Creates the WebSocket used for a Foxglove connection. */ type WebSocketFactory = (url: string, protocols: string[]) => WebSocket; /** * Ros class — drop-in replacement for roslib's ROSLIB.Ros. * * Manages the WebSocket connection to foxglove_bridge and maintains * a registry of available channels (topics) and services. Higher-level * classes (Topic, Service, Action) use this to subscribe, publish, and call. */ type RosEventName = "connection" | "error" | "close" | "channelsChanged" | "servicesChanged"; type RosLifecycleCallback = () => void; type RosErrorCallback = (error: Error) => void; interface RosOptions { url?: string; /** * Creates the connection socket. Node.js callers can use this to supply * authentication headers or custom TLS trust without replacing globals. */ webSocketFactory?: WebSocketFactory; } /** Callback for incoming topic messages (deserialized to JS objects). */ type MessageCallback = { bivarianceHack(message: T): void; }["bivarianceHack"]; declare class Ros { private readonly protocol; private readonly connectionListeners; private readonly closeListeners; private readonly channelsChangedListeners; private readonly servicesChangedListeners; private readonly errorListeners; private readonly channels; private readonly channelsByTopic; private readonly services; private readonly servicesByName; private readonly subscriptions; private readonly subscriptionsByTopic; private readonly pendingSubscribers; private readonly clientChannels; private readonly pendingServiceCalls; private readonly pendingParamRequests; private nextParamRequestId; private readonly readerCache; private readonly writerCache; isConnected: boolean; constructor(options?: RosOptions); connect(url: string): void; close(): void; on(event: "error", callback: RosErrorCallback): void; on(event: "connection" | "close" | "channelsChanged" | "servicesChanged", callback: RosLifecycleCallback): void; off(event: "error", callback: RosErrorCallback): void; off(event: "connection" | "close" | "channelsChanged" | "servicesChanged", callback: RosLifecycleCallback): void; /** * roslib-compatible bulk listener removal. Tests use this to reset the * module-level mock Ros between cases; production code does not call it. */ removeAllListeners(event?: RosEventName): void; emit(event: "error", error: Error): void; emit(event: "connection" | "close" | "channelsChanged" | "servicesChanged"): void; private emitLifecycle; private emitError; getChannel(topic: string): FoxgloveChannel | undefined; getServiceByName(name: string): FoxgloveService | undefined; getActionServers(onSuccess: (actions: string[]) => void): void; isServiceAdvertised(serviceName: string): boolean; getMissingActionEndpoints(actionName: string, options?: { requireFeedback?: boolean; requireCancel?: boolean; }): string[]; isActionAdvertised(actionName: string, options?: { requireFeedback?: boolean; requireCancel?: boolean; }): boolean; waitForService(serviceName: string, timeoutMs?: number): Promise; waitForAction(actionName: string, timeoutMs?: number, options?: { requireFeedback?: boolean; requireCancel?: boolean; }): Promise; /** * Return every advertised topic whose schema matches `messageType`. Matches * roslib's `Ros#getTopicsForType(type, success, fail)` callback API so * existing callers (e.g. `useImageTopics`) can use this adapter unchanged. */ /** * The roslib signature also accepts an `onError` callback. Foxglove resolves * synchronously from the in-memory channel registry, so the failure path is * unreachable; we drop the parameter to keep lint clean. Callers that pass * one are unaffected — extra arguments to a JS function are ignored. */ getTopicsForType(messageType: string, onSuccess: (topics: string[]) => void): void; subscribeTopic(topic: string, messageType: string, callback: MessageCallback): void; unsubscribeTopic(topic: string, callback?: MessageCallback): void; private createSubscription; publishTopic(topic: string, messageType: string, message: unknown): void; private findWriterForSchema; unpublishTopic(topic: string): void; callService(serviceName: string, serviceType: string, request: unknown): Promise>; sendActionGoal, TResult = unknown, TFeedback = unknown>(actionName: string, actionType: string, goal: TGoal, onResult?: (result: ActionResult) => void, onFeedback?: (feedback: ActionFeedback) => void, onError?: (error: Error) => void, goalId?: string): string; cancelActionGoal(actionName: string, goalId: string): Promise>; private actionSendGoalRequest; getParam(name: string): Promise; setParam(name: string, value: unknown): Promise; private setupProtocolHandlers; private processPendingSubscribers; private getReader; private getWriter; } /** * Topic class — drop-in replacement for roslib's ROSLIB.Topic. * * Provides subscribe/unsubscribe/publish matching the roslib API. * Internally delegates to the Ros class which manages foxglove subscriptions. */ interface TopicOptions { ros: Ros; name: string; messageType: string; /** * Optional runtime validator for decoded messages. When supplied, subscribed * callbacks receive only values that have passed this schema. */ messageSchema?: z.ZodType; /** * Minimum milliseconds between delivered messages. Matches rosbridge's * `throttle_rate`: leading-edge fire, no trailing catch-up. Enforced client-side * because foxglove_bridge does not expose a per-subscription rate limiter — * rosbridge delivered the rate cap on the server, foxglove_bridge delivers every * message published. * * `0` / `undefined` → no throttle; the hot path takes zero extra work. */ throttle_rate?: number; compression?: string; queue_size?: number; queue_length?: number; latch?: boolean; reconnect_on_close?: boolean; } declare class Topic> { ros: Ros; name: string; messageType: string; private readonly messageSchema; /** * Throttle window in ms. `0` disables throttling — in that case `subscribe` * skips the throttling code path entirely and registers an unwrapped callback * so the per-message hot path has no throttle-related work. */ private readonly throttleMs; /** * Original user callback → wrapped MessageCallback registered with Ros. * Keyed as `object` (all JS functions are objects) so `Topic` stays * variance-friendly for callers that pass a `Topic` into a slot typed * as `Topic` — otherwise T ends up invariant through this field * and blocks assignments that are safe at runtime. */ private readonly wrappedCallbacks; constructor(options: TopicOptions); /** * advertise() is a no-op: the foxglove adapter advertises a client channel * lazily on the first publish() and reuses it afterwards, so explicit * registration has no effect on the wire. */ advertise(): void; /** * Release the client channel allocated by the lazy advertise on first publish(). * Callers that publish once then tear down (e.g. joint-control-spinners' StoreJointState * burst) rely on this to drop the channel registration rather than leaking it for the * life of the Ros connection. */ unadvertise(): void; subscribe(callback: MessageCallback): void; unsubscribe(callback?: MessageCallback): void; publish(message: T): void; } /** * Service class — drop-in replacement for roslib's ROSLIB.Service. * * Provides callService matching the roslib API. Service calls go through the * foxglove WebSocket protocol. Client-side service advertisement is not * supported by foxglove_bridge, and the corresponding no-op shims have been * removed now that no caller depends on them. */ type ServiceResponseCallback = { bivarianceHack(response: T): void; }["bivarianceHack"]; interface ServiceOptions { ros: Ros; name: string; serviceType: string; /** * Optional runtime validator for decoded responses. When supplied, success * callbacks receive only values that have passed this schema. */ responseSchema?: z.ZodType; } declare class Service { ros: Ros; name: string; serviceType: string; private readonly responseSchema; constructor(options: ServiceOptions); /** * Call the service (roslib-compatible callback API). */ callService(request: TReq, onSuccess?: ServiceResponseCallback, onError?: (error: string) => void): void; } interface ActionClientOptions { ros: Ros; /** ROS action name, e.g. `/fibonacci` or `/do_objective`. */ name: string; /** ROS action type, e.g. `example_interfaces/action/Fibonacci`. */ actionType: string; /** Accepted for roslib compatibility; reconnect behavior is owned by Ros. */ reconnectOnClose?: boolean; } declare class ActionClient, TResult = unknown, TFeedback = unknown> { ros: Ros; name: string; actionType: string; reconnectOnClose: boolean; private readonly waiters; private readonly completedGoals; private readonly failedGoals; constructor(options: ActionClientOptions); sendGoal(goal: TGoal, onResult?: (result: ActionResult) => void, onFeedback?: (feedback: ActionFeedback) => void, onError?: (error: Error) => void): string; cancelGoal(goalId: string): Promise>; waitGoal(goalId: string, timeoutMs?: number): Promise; private resolveWaiter; private rejectWaiter; } /** * Param class — drop-in replacement for roslib's ROSLIB.Param. * * Provides get/set matching the roslib API. * Uses foxglove_bridge's parameter operations. */ interface ParamOptions { ros: Ros; name: string; } declare class Param { readonly ros: Ros; readonly name: string; constructor(options: ParamOptions); /** * Overload: callers often pass a typed callback * (e.g. `(v: string) => setState(v)`) where our actual runtime value is * `unknown`. TypeScript matches the generic overload at call sites while * the implementation handles `unknown` — runtime validation is the * caller's job, same convention as `roslib`'s original `Param.get` typing. */ get(callback: (value: T) => void): void; get(callback: (value: unknown) => void): void; set(value: unknown, onSuccess?: (value?: unknown) => void, onError?: (error: string) => void): void; } /** * ROS2TFClient — drop-in replacement for roslib's ROSLIB.ROS2TFClient. * * Subscribes to /tf and /tf_static topics, builds a parent→child transform * graph, and resolves transforms from `fixedFrame` to any requested frame by * chaining through intermediate frames. */ interface TFClientOptions { ros: Ros; fixedFrame?: string; angularThres?: number; transThres?: number; /** * Max /tf processing rate in Hz (roslib semantics). Throttled client-side to * a `1000 / rate` ms leading-edge window; 0 / undefined / non-finite disables * throttling. /tf_static is never throttled (see subscribeTF). */ rate?: number; serverName?: string; } type TFCallback = (transform: Transform) => void; /** * Notified whenever the set of frames known to the client grows. Receives the * full sorted frame list. Used by the TF visualization layer to populate the * frame-selection UI; not part of roslib's ROS2TFClient API. */ type FramesCallback = (frames: string[]) => void; declare class ROS2TFClient { private readonly ros; private readonly fixedFrame; private readonly frameCallbacks; /** child_frame_id → {parent, local transform}. One entry per child. */ private readonly directTransforms; private readonly knownFrames; private readonly framesListeners; private readonly multiParentWarned; private tfSub; private tfStaticSub; private readonly onReconnect; private readonly throttleMs; private parseWarned; private disposed; constructor(options: TFClientOptions); subscribe(frameId: string, callback: TFCallback): void; unsubscribe(frameId: string, callback?: TFCallback): void; /** * Sorted list of every frame seen so far on /tf or /tf_static (as a parent or * child). MoveIt Pro extension used to drive the TF visualization frame list. */ getFrameIds(): string[]; /** * Register a listener fired whenever new frames appear. Invoked immediately * with the current frame list so callers don't miss frames seen before they * subscribed. MoveIt Pro extension, not part of roslib's ROS2TFClient. */ addFramesListener(callback: FramesCallback): void; removeFramesListener(callback: FramesCallback): void; dispose(): void; /** Warn once if a mutating method is called on a disposed client. */ private warnIfDisposed; private subscribeTF; private dispatchTFMessage; /** * Warn once per child when it is published with a parent different from the * one already stored. A frame with two live parents is a malformed tree (tf2 * surfaces this as a multiple-authority warning). We keep last-writer-wins so * resolution still works, but a silent pick can resolve through a different * parent than tf2/RViz would, so make the misconfiguration visible. */ private warnIfReparented; private handleTFMessage; /** * Resolve the transform from `fixedFrame` to `frameId`, i.e. the pose of * `frameId` expressed in `fixedFrame`. * * tf2 (and RViz) can relate any two connected frames, not just a frame and * its descendant: it walks both frames up to their lowest common ancestor and * inverts one side. We do the same. Walking only child→parent from `frameId` * to `fixedFrame` (the previous approach) silently failed for any frame that * is an ancestor of — or in a sibling branch to — `fixedFrame`, which is the * common case for mobile-base trees where the reference frame sits below * `map`/`odom`. * * Returns null if the two frames are not connected (yet) or the tree is * malformed (cycle). * * Time semantics: this composes each edge's most recent value. Unlike tf2 it * does not interpolate edges to a common timestamp, so during fast motion with * edges published at different rates the result can briefly lead or lag tf2's. * For a visualization layer this is an accepted simplification. */ private resolveTransform; /** * Walk from `frameId` up to its tree root, returning a map from every * ancestor frame (including `frameId`) to the transform expressing `frameId` * in that ancestor's frame. Returns null on a cycle. */ private ancestorTransforms; } export { ActionClient, type ActionFeedback, type ActionResult, GoalStatus, Param, Quaternion, ROS2TFClient, Ros, type RosOptions, Service, Topic, Transform, Vector3, type WebSocketFactory };