import type { Immutable } from "./immutable"; /** Valid types for parameter data (such as rosparams) */ export type ParameterValue = | undefined | boolean | number | string | Date | Uint8Array | ParameterValue[] | { [key: string]: ParameterValue }; /** Valid types for [variables](https://docs.foxglove.dev/docs/visualization/variables) */ export type VariableValue = | undefined | boolean | number | string | VariableValue[] | { [key: string]: VariableValue }; export type VariableStruct = { [key: string]: VariableValue }; /** Valid types for application settings */ export type AppSettingValue = string | number | boolean | undefined; /** * A timestamp with nanosecond precision. * * The timestamp is often nanoseconds since the UNIX epoch, but may be * relative to another event such as system boot time or simulation start * time depending on the context. */ export type Time = { sec: number; nsec: number; }; /** * A Topic is a named channel of messages. */ export type Topic = { /** * topic name i.e. "/some/topic" */ name: string; /** * @deprecated Renamed to `schemaName`. `datatype` will be removed in a future release. */ datatype: string; /** * The schema name is an identifier for the types of messages on this topic. Typically this is the * fully-qualified name of the message schema. The fully-qualified name depends on the data source * and data loaded by the data source. * * i.e. `package.Message` in protobuf-like serialization or `pkg/Msg` in ROS systems. */ schemaName: string; /** * Lists any additional schema names available for subscribers on the topic. When subscribing to * a topic, the panel can request messages be automatically converted from schemaName into one * of the convertibleTo schemas using the {@link Subscription.convertTo} option. */ convertibleTo?: readonly string[]; }; /** * A single subscription passed to {@link PanelExtensionContext.subscribe}. * * @category Custom panels */ export type Subscription = { topic: string; /** * If a topic has additional schema names, specifying a schema name will convert messages on that * topic to the convertTo schema using a registered message converter. MessageEvents for the * subscription will contain the converted message and an originalMessageEvent field with the * original message event. */ convertTo?: string; /** * Setting preload to _true_ hints to the data source that it should attempt to load all available * messages for the topic. The default behavior is to only load messages for the current frame. * * **Only** topics with `preload: true` are available in the `allFrames` render state. * * @deprecated Please use {@link PanelExtensionContext.subscribeMessageRange} instead. */ preload?: boolean; }; /** * A MessageEvent represents a single message along with metadata about the message. * * Remember to import MessageEvent from `@foxglove/extension`. This is not the same as the DOM [MessageEvent](https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent) class. */ export type MessageEvent = { /** The topic name this message was received on, i.e. "/some/topic" */ topic: string; /** * The schema name is an identifier for the schema of the message within the message event. */ schemaName: string; /** * The time in nanoseconds this message was received. This may be set by the * local system clock or the data source, depending on the data source used * and whether time is simulated via a /clock topic or similar mechanism. * The timestamp is often nanoseconds since the UNIX epoch, but may be * relative to another event such as system boot time or simulation start * time depending on the context. */ receiveTime: Time; /** * The time in nanoseconds this message was originally published. This is * only available for some data sources. The timestamp is often nanoseconds * since the UNIX epoch, but may be relative to another event such as system * boot time or simulation start time depending on the context. */ publishTime?: Time; /** The deserialized message as a JavaScript object. */ message: T; /** * The approximate size of this message event in its deserialized form. This can be * useful for statistics tracking and cache eviction. * * @deprecated Extension authors are advised not to keep references to messages in their * extensions as this may lead to out-of-memory errors. If message retention is necessary, use * your own estimation of the message byte size for memory management rather than relying on * this property. */ sizeInBytes: number; /** * When subscribing to a topic using the `convertTo` option, the message event `message` * contains the converted message and the `originalMessageEvent` field contains the original * un-converted message event. */ originalMessageEvent?: MessageEvent; }; /** * Actions the panel may perform related to the user's current layout via {@link PanelExtensionContext.layout | context.layout}. * * @category Custom panels */ export interface LayoutActions { /** * Use `context.layout.addPanel` to add a panel adjacent to the current panel in the layout. * * The value of `position` must be set to `"sibling"`. * * The value of `type` can refer to a panel from a custom extension as `extensionname.panelname`, * where `extensionname` is the extension name from `package.json` and `panelname` is the name * provided when the extension registers a panel. * * `getState` is set to a function that returns the state (also known as panel settings) for the * new panel, or return `undefined` to use the new panel's default settings. * * ```ts * // Add new panel * context.layout.addPanel({ * position: "sibling", * type: "MyExtension.MyPanel", * getState: () => ({}), * }); * ``` */ addPanel(params: { /** * Where to position the panel. Currently, only "sibling" is supported which indicates the * new panel will be adjacent to the calling panel. */ position: "sibling"; /** * The type of panel to open. For extension panels, this `"extensionName.panelName"` where * extensionName is the `name` field from the extension's package.json, and panelName is the * name provided to `registerPanel()`. */ type: string; /** * Whether to update an existing sibling panel of the same type, if it already exists. If false * or omitted, a new panel will always be added. * * @deprecated This parameter is only supported for built-in panels at this time. */ updateIfExists?: boolean; /** * A function that returns the state for the new panel. If updating an existing panel, the * existing state will be passed in. * @see `updateIfExists` */ getState(existingState?: unknown): unknown; }): void; } /** * RenderState is the information passed to your panel's * {@link PanelExtensionContext.onRender | onRender} function. * * To receive updates for a particular part of RenderState, you must first call * {@link PanelExtensionContext.watch | watch} with the field name. For example, call * `watch("currentTime")` to receive updates for `currentTime`. * * If a field is missing from RenderState, either the value has not changed since the last call to * `onRender`, or you did not `watch` the field. * * @category Custom panels */ export type RenderState = { /** * The latest messages for the current render frame. These are new messages since the last render frame. */ currentFrame?: MessageEvent[]; /** * True if the data source performed a seek. This indicates that some data may have been skipped * (never appeared in the `currentFrame`), so panels should clear out any stale state to avoid * displaying incorrect data. */ didSeek?: boolean; /** * All available messages. Best-effort list of all available messages. * * @deprecated Please use {@link PanelExtensionContext.subscribeMessageRange} instead. */ allFrames?: MessageEvent[]; /** * Map of current parameter values. Parameters are key/value pairs associated with the data * source, and may not be available for all data sources. For example, ROS 1 live connections * support parameters through the Parameter Server . */ parameters?: Map; /** * Transient panel state shared between panels of the same type. This can be any data a * panel author wishes to share between panels. */ sharedPanelState?: Record; /** * Map of current Studio variables. Variables are key/value pairs that are globally accessible * to panels and scripts in the current layout. See * for more information. */ variables?: Map; /** * List of available topics. This list includes subscribed and unsubscribed topics. */ topics?: Topic[]; /** * A timestamp value indicating the current playback time. */ currentTime?: Time; /** * The start timestamp of the playback range for the current data source. For offline files it * is expected to be present. For live connections, the start time may or may not be present * depending on the data source. */ startTime?: Time; /** * The end timestamp of the playback range for the current data source. For offline files it * is expected to be present. For live connections, the end time may or may not be present * depending on the data source. */ endTime?: Time; /** * A seconds value indicating a preview time. The preview time is set when a user hovers * over the seek bar or when a panel sets the preview time explicitly. The preview time * is a seconds value within the playback range. * * i.e. A plot panel may set the preview time when a user is hovering over the plot to signal * to other panels where the user is currently hovering and allow them to render accordingly. */ previewTime?: number | undefined; /** The color scheme currently in use throughout the app. */ colorScheme?: "dark" | "light"; /** Application settings. This will only contain keys/values that were subscribed to using {@link @PanelExtensionContext.subscribeAppSettings} */ appSettings?: Map; }; /** * This type represents the arguments you pass to {@link PanelExtensionContext.subscribeMessageRange}. * * @category Custom panels */ export type SubscribeMessageRangeArgs = { /** * Topic to be subscribed to. */ topic: string; /** * Convert messages to this schema before delivering to the subscriber. * * MessageEvents for the subscription will contain the converted message and an * `originalMessageEvent` field with the original message event. If no `convertTo` schema is * specified, then no message converters will be used. If no message converter exists for * converting the original schema to the `convertTo` schema, then no messages are delivered for * this subscription. */ convertTo?: string; /** * `onNewRangeIterator` is a function that receives an async iterable when there is message data available on * the subscription. * * To read messages, your function should iterate through the provided async iterable. Each item * of the iterable is a batch of message events for the subscription's topic. These batches and * messages are in _log time_ order. When there are no more messages to read the iterator will * finish. * * ```typescript * async function onNewRangeIterator(batchIterator) { * for await (const batch of batchIterator) { * //... * } * } * ``` * * `onNewRangeIterator` is called again when the upstream topic data changes. I.E subscribing to a * user-script output topic and the user script changes, or subscribing to an aliased topic and * the alias changes. When topic data changes, the previous iterator will end, and its data is no * longer valid. When `onNewRangeIterator` is called, you should discard previously received data. * * If your `onNewRangeIterator` function throws an error, the iterator will end and you will not receive any * more messages until `onNewRangeIterator` is called again. Your error will appear in the problems sidebar * for user visibility. */ onNewRangeIterator: (batchIterator: AsyncIterable>) => Promise; /** * @deprecated This method has been renamed. Use `onNewRangeIterator`. */ onReset?: (batchIterator: AsyncIterable>) => Promise; }; /** * The `PanelExtensionContext` exposes properties and methods for writing a custom panel. The * context has methods to subscribe for messages, receive updates, configure your panel's settings, * and render your panel to the UI. * * The {@link ExtensionPanelRegistration.initPanel | initPanel} function used in * {@link ExtensionContext.registerPanel | registerPanel} accepts a {@link PanelExtensionContext} * argument. This argument contains properties and methods for accessing panel data and rendering UI * updates. The `initPanel` function also returns an optional cleanup function to run when the * extension `panelElement` unmounts. * * See the [Creating a custom * panel](https://docs.foxglove.dev/docs/extensions/guides/create-custom-panel) guide * for more details. * * @category Custom panels */ export type PanelExtensionContext = { /** * The root element for the panel. Add your panel elements as children under this element. */ readonly panelElement: HTMLDivElement; /** * Initial panel state */ readonly initialState: unknown; /** Actions the panel may perform related to the user's current layout. See {@link LayoutActions} for details. */ readonly layout: LayoutActions; /** * Identifies the semantics of the data being played back, such as which topics or parameters * are semantically meaningful or normalization conventions to use. This typically maps to a * shorthand identifier for a robotics framework such as "ros1", "ros2", or "ulog". See the MCAP * profiles concept at . */ readonly dataSourceProfile?: string; /** * Subscribe to updates on this field within the render state. Render will only be invoked when * this field changes. * * Use `context.watch` to indicate which fields in {@link RenderState} (e.g. `currentFrame`, * `currentTime`, `previewTime`, `parameters`, `topics`) should trigger panel re-renders when * their contained values change. * * ```ts * context.watch("topics"); * context.watch("currentFrame"); * context.watch("parameters"); * context.watch("currentTime"); * ``` */ watch(field: keyof RenderState): void; /** * Subscribe to updates on this field within the render state. Render will only be invoked when * this field changes. * * @deprecated Calling `watch` with `allFrames` is deprecated. Use {@link PanelExtensionContext.subscribeMessageRange} instead. */ watch(field: "allFrames"): void; /** * Use `context.saveState` to save an arbitrary object as persisted panel state (also known as * panel settings) in the current layout. You can view the current panel state using * [Import/export settings](https://docs.foxglove.dev/docs/visualization/panels#importexport-settings). * * ```ts * context.initialState = undefined; // your panel's initial state * * context.saveState({ myNum: 2, myBool: false, myStr: "abc" }); * ``` * * @param state The state to save. This value should be JSON serializable. */ saveState(state: Partial): void; /** * Use `context.setParameter` to set a parameter `name` to any valid `value` (i.e. primitives, dates, `Uint8Array`s, and arrays or objects containing these values). * * ```ts * context.setParameter("/param1", "value1"); * ``` * * @param name The name of the parameter to set. * @param value The new value of the parameter. */ setParameter(name: string, value: ParameterValue): void; /** * Set the transient state shared by panels of the same type as the caller of this function. * This will not be persisted in the layout. */ setSharedPanelState( state: | Record | undefined | (( prevState: Immutable> | undefined, ) => Immutable> | undefined), ): void; /** * Use `context.setVariable` to set a * [variable](https://docs.foxglove.dev/docs/visualization/variables) `name` to any valid variable * `value`. * * ```ts * context.setVariable("myVar", 55); * * context.onRender = (renderState: RenderState, done) => { * // Read variable values from the renderState * const variableValues = renderState.variables; * const myVarValue = variableValues.myVar; * * // Call done when you've rendered all the UI for this renderState. If your UI framework delays rendering, call done when rendering has actually happened. * done(); * }; * ``` * * @param name The name of the variable to set. * @param value The new value of the variable. */ setVariable(name: string, value: VariableValue): void; /** * Set the active preview time. Setting the preview time to undefined clears the preview time. */ setPreviewTime(time: number | undefined): void; /** * Seek playback to the given time. Behaves as if the user had clicked the playback bar * to seek. * * Clients can pass a number or alternatively a Time object for greater precision. * * This property may be `undefined` if the current data source does not support seeking. */ seekPlayback?(time: number | Time): void; /** * Use `context.subscribe` to indicate the topics your panel wants to receive messages for. The * messages are provided during render in {@link RenderState.currentFrame}. * * @remarks * * This method will update the current subscriptions to the new list of Subscriptions and * unsubscribe from any previously subscribed topics no longer in the Subscription list. Passing * an empty array will unsubscribe from all topics. * * ```ts * context.subscribe([{ topic: "/some/topic" }, { topic: "/another/topic" }]); * ``` * * `context.subscribe([])` will unsubscribe from all topics, and is equivalent to * `unsubscribeAll`. * * #### Range loading * * Most panels display data from the current frame; examples of built-in panels that display the * current frame are 3D, Image, and Raw Message, however some panels can display data for multiple * messages or even the entire dataset duration (Plot, Map, State Transitions). * * Subscriptions will provide only the messages for the current frame. If your panel would like to * process all the available messages on a topic, use * {@link @PanelExtensionContext.subscribeMessageRange | subscribeMessageRange} instead. * * > NOTE: Message range loading is done on a best-effort basis. If your range-loaded messages * > exceed available memory limits for the browser or desktop app, then the data may not * > represent the full dataset range. Range loading results in more data transfer and memory use * > and is recommended only for panels which require access to the entire dataset. * * #### Message converters * * Message converters can convert messages from one schema to another – for example, a user might * convert custom GPS message into * [`foxglove.LocationFix`](https://docs.foxglove.dev/docs/visualization/message-schemas/location-fix) * messages for visualization in the [Map * panel](https://docs.foxglove.dev/docs/visualization/panels/map). Users may have one or more * message converters registered. * * If your panel expects messages with specific schema names, you can leverage registered message * converters to convert from one schema to another. * * Specify the `convertTo` option to enable message conversion on a topic. When conversion is * enabled for a subscription, the {@link MessageEvent}s will contain `message` entries with the * converted message rather than the original message on the topic. The original message is * available in the `originalMessageEvent` field in the message event. * * ```ts * context.subscribe([{ topic: "/some/topic", convertTo: "foxglove.LocationFix" }]); * ``` * * The {@link Topic.convertibleTo | convertibleTo} field within {@link RenderState.topics} will * contain the names of schemas you can convert this topic into. */ subscribe(subscriptions: Subscription[]): void; /** * @deprecated Use `subscribe` with an array of Subscription objects instead. */ subscribe(topics: string[]): void; /** * Unsubscribe from all topics. * * Note: This is analogous to calling `subscribe([])` with an empty array of topics. */ unsubscribeAll(): void; /** * Subscribe to any changes in application settings for an array of setting keys. * * The keys and their corresponding values are not currently documented and are subject to change. */ subscribeAppSettings(settings: string[]): void; /** * Use `context.advertise` to indicate an intent to publish a specific datatype on a topic. A * panel must call `context.advertise` before being able to publish on the topic * (`context.publish`). Options are specific to the data source - some make use of options; others * do not. * * This property may be `undefined` if the current data source does not support publishing. * * ```ts * context.advertise("/my_image_topic", "sensor_msgs/Image"); * ``` * * `options` are specific to each data source - see documentation below for supported data * sources. * * @param topic The topic on which the extension will publish messages. * @param schemaName The name of the schema that the published messages will conform to. * @param options Options passed to the current data source for additional configuration. * * @remarks * * #### [Native (ROS 1)](https://docs.foxglove.dev/docs/connecting-to-data/frameworks/ros1#native) * * | field | type | description | * | --------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | * | datatypes | `Map` | JavaScript map of datatype names and [`MessageDefinition`](https://github.com/foxglove/message-definition/blob/main/src/types.ts#L185) definitions for a given topic | * * Common datatype definitions are available in the `@foxglove/rosmsg-msgs-common` package. * * ```ts * import { ros1 } from "@foxglove/rosmsg-msgs-common"; * * context.advertise?.(currentTopic, "sensor_msgs/Joy", { * datatypes: new Map([ * ["std_msgs/Header", ros1["std_msgs/Header"]], * ["std_msgs/Float32", ros1["std_msgs/Float32"]], * ["std_msgs/Int32", ros1["std_msgs/Int32"]], * ["sensor_msgs/Joy", ros1["sensor_msgs/Joy"]], * ]), * }); * ``` * * #### [Foxglove WebSocket](https://docs.foxglove.dev/docs/connecting-to-data/frameworks/custom#foxglove-websocket) * * `options` depend on the server implementation. * * ##### [Foxglove Bridge](https://docs.foxglove.dev/docs/connecting-to-data/ros-foxglove-bridge) * * When using the Foxglove Bridge with ROS data, use `context.dataSourceProfile` to determine the * ROS version. * * For ROS 1 data, pass datatypes the same way you would for a [native ROS 1](#native-ros-1) * connection. * * For ROS 2 data, pass datatypes using the following fields: * * | field | type | description | * | --------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | * | datatypes | `Map` | JavaScript map of datatype names and [`MessageDefinition`](https://github.com/foxglove/message-definition/blob/main/src/types.ts#L185) definitions for a given topic | * * Common datatype definitions are available in the `@foxglove/rosmsg-msgs-common` package. * * ```ts * import { ros2humble as ros2 } from "@foxglove/rosmsg-msgs-common"; * // For Galactic and lower, use: * // import { ros2galactic as ros2 } from "@foxglove/rosmsg-msgs-common"; * ``` * * ```ts * context.advertise?.(currentTopic, "sensor_msgs/Joy", { * datatypes: new Map([ * ["std_msgs/Header", ros2["std_msgs/Header"]], * ["std_msgs/Float32", ros2["std_msgs/Float32"]], * ["std_msgs/Int32", ros2["std_msgs/Int32"]], * ["sensor_msgs/Joy", ros2["sensor_msgs/Joy"]], * ]), * }); * ``` * * #### Rosbridge ([ROS 1](https://docs.foxglove.dev/docs/connecting-to-data/frameworks/ros1#rosbridge), [ROS 2](https://docs.foxglove.dev/docs/connecting-to-data/frameworks/ros2#rosbridge)) * * No `options` required. Simply publish a JSON message with fields conforming to the advertised * datatype, and the bridge node will serialize it according to the datatype. */ advertise?(topic: string, schemaName: string, options?: Record): void; /** * Indicate that you no longer want to advertise on this topic. * * ```ts * context.unadvertise("/my_image_topic"); * ``` * * This property may be `undefined` if the current data source does not support publishing. */ unadvertise?(topic: string): void; /** * Use `context.publish` to publish a message on a previously advertised topic. (You must first * call {@link PanelExtensionContext.advertise | advertise} to advertise the topic before * publishing.) If the topic is not advertised or otherwise malformed, the function will throw an * error. * * ```ts * context.advertise("/my_color_topic", "std_msgs/ColorRGBA"); * context.publish("/my_color_topic", { r: 0, g: 1, b: 0, a: 1 }); * ``` * * This property may be `undefined` if the current data source does not support publishing. * * @param topic The name of the topic to publish the message on * @param message The message to publish */ publish?(topic: string, message: unknown): void; /** * Use `context.callService` to make a service call to the specified `service` with a request payload. * * ```ts * context.callService("my_service", { foo: "bar" }); * ``` * * This property may be `undefined` if the current data source does not support services. * * @param service The name of the service to call * @param request The request payload for the service call * @returns A promise that resolves when the result is available or rejected with an error */ callService?(service: string, request: unknown): Promise; /** * Set this property to a function during your panel's * {@link ExtensionPanelRegistration.initPanel | initialization}. * * Foxglove will run `context.onRender` whenever your panel needs to re-render during playback. * The function accepts `renderState` and a `done` callback as its arguments. Render events occur * frequently (60hz, 30hz, etc). * * **Note**: Your `onRender` function **must** call `done` after rendering to indicate that the * panel is ready to render the next set of data. The exact placement of this `done` invocation * will vary between frameworks and different extensions' logic. * * ```ts * context.onRender = (renderState, done) => { * // Render your UI updates with fields from RenderState * * // Call done when you've rendered all the UI for this renderState. * // If your UI framework delays rendering, call done when rendering has actually happened. * done(); * }; * ``` */ onRender?: (renderState: Immutable, done: () => void) => void; /** * Call the `updatePanelSettingsEditor` method on your panel's {@link PanelExtensionContext} * instance to define or update its settings. * * ```ts * const panelSettings: SettingsTree = { * nodes: { ... }, * actionHandler: (action: SettingsTreeAction) => { ... } * }; * * context.updatePanelSettingsEditor(panelSettings); * ``` * * The `settings` argument must be a valid {@link SettingsTree} and include 2 mandatory properties * – `nodes` and `actionHandler`: * * - `nodes` - Hierarchical structure where each node can contain input fields, display fields, or * even other nodes * - `actionHandler` - Function that is invoked when the user interacts with the settings UI; * contains logic to process the interactions and update the panel or settings tree * * It can also include the following optional properties: * * - `enableFilter` – Whether the settings should show the filter control * - `focusedPath` – Node to scroll to (transient one-time effect) * * The example tree below has a `title` text input field inside a `General` section along with an * `actionHandler` to respond to updates for the `title` field. * * ```ts * const panelSettings: SettingsTree = { * nodes: { * general: { * label: "General", * fields: { * title: "{ * label: "Title", * input: "string", * // `panelTitle` refers to a value in your extension panel's config * value: panelTitle, * }, * }, * }, * }, * actionHandler: (action: SettingsTreeAction) => { * switch (action.action) { * case "perform-node-action": * // Handle user-defined actions for nodes in the settings tree * break; * case "reorder-children": * if (action.payload.path[0] === "rules") { * // Move the child identified by action.payload.fromKey before or after * // action.payload.toKey within the parent node at action.payload.path. * } * break; * case "update": * if (action.payload.path[0] === "general" && action.payload.path[1] === "title") { * // Read action.payload.value for the new panel title value * panelTitle = action.payload.value; * * // Update your panel's state accordingly * } * break; * } * }, * } * * context.updatePanelSettingsEditor(panelSettings); * ``` * * #### `SettingsTreeAction` * * A {@link SettingsTreeAction} is a discriminated union describing how the settings UI should * update when a user interacts with it. * * All actions include a `payload.path` that identifies the target node or field. * * - `update` corresponds to a user setting a new field value. Its path points to the field, for * example `["general", "title"]`. * - `perform-node-action` corresponds to a user invoking a custom node action from `actions`. * - `reorder-children` corresponds to a user reordering the children of a node whose * `childrenReorderable` property is enabled. Its path points to the parent node, and the payload * identifies the moved child and destination child by key. * * This union is extended additively over time. To stay forward-compatible, action handlers should * ignore unknown `action` values at runtime instead of throwing from a `default` branch. * * #### Special node properties * * There are two special {@link SettingsTreeNode} properties, `label` and `visibility`. The value * you specify for `label` will control the label displayed in the settings editor. If you set the * `renamable` node property to `true`, the user can edit the node `label` – you will receive a * `SettingsTreeAction` of `update` with a path ending in `label`. * * In addition, if you specify a boolean value for `visibility` of the node then the settings * editor will provide a button to toggle the visibility of the node and you will receive an * `update` action with `visibility` as the final element in the path. * * If you set `childrenReorderable` on a node with at least two children, the settings editor lets * users drag and drop that node's children and dispatches `reorder-children` actions when the * user changes their order. * * For an example of how to use these special properties, check out the [panel settings example * extension](https://github.com/foxglove/create-foxglove-extension/tree/main/examples/panel-settings). * * #### Input types * * In addition to the `string` input type in the example above, the panel API provides a wide * array of types for your extension panel input fields. * * Each input type has different properties that you can configure: * * - `autocomplete` * - `boolean` * - `rgb` * - `rgba` * - `gradient` * - `messagepath` * - `select` * - `string` * - `toggle` * - `vec3` * - `vec2` */ updatePanelSettingsEditor(settings: Immutable): void; /** * Use `context.setDefaultPanelTitle` to override a panel's default title. Users can always * override the default title by editing it manually. If no override or default title is set, the * panel will simply display its type (e.g. "Image"). * * ```ts * // Override the default panel title * context.setDefaultPanelTitle(`Plot of ${config.topicName}`); * ``` */ setDefaultPanelTitle(defaultTitle: string | undefined): void; /** * Subscribe to receive the entire time range of messages for a given topic for the current data source. * * See {@link SubscribeMessageRangeArgs} for more information on behavior. * * Note: This will not read messages for live sources, like foxglove_bridge, rosbridge, or ROS 1 * native connections. For those messages you will still need to use `context.subscribe()` and * `watch("currentFrame")`. * * @returns A function that will unsubscribe from the topic, cancel the active async iterator, * and prevent {@link SubscribeMessageRangeArgs.onNewRangeIterator | onNewRangeIterator} from being called again. */ subscribeMessageRange?: (args: SubscribeMessageRangeArgs) => () => void; /** * @deprecated Renamed to `subscribeMessageRange`. Please use that method instead. */ UNSTABLE_subscribeMessageRange?: (args: SubscribeMessageRangeArgs) => () => void; }; /** * This type represents the arguments you pass to {@link ExtensionContext.registerPanel}. * * @category Custom panels */ export type ExtensionPanelRegistration = { /** * Unique name of the panel within your extension * * NOTE: Panel names within your extension must be unique. The panel name identifies this panel * within a layout. Changing the panel name will cause layouts using the old name unable to load * your panel. */ name: string; /** * This function is invoked when your panel is initialized * * @returns (optional) A function which will be called when the panel is removed or replaced. * Perform any cleanup logic here to gracefully tear down your panel. */ initPanel: (context: PanelExtensionContext) => void | (() => void); }; export type MessageSchemaField = "string" | "number" | "bool" | "byte"; /** * Describes the structure of a message with field names and their types. * * @example * ```typescript * { * // Primitive fields * position: "number", * name: "string", * active: "bool", * singleByte: "byte", * * // Arrays of primitive types * coordinates: ["number"], * labels: ["string"], * flags: ["bool"], * rawData: ["byte"], * * // Nested objects * metadata: { * timestamp: "number", * source: "string" * }, * * // Arrays of objects * points: [{ * x: "number", * y: "number", * z: "number" * }] * } * ``` */ export type MessageSchemaDescription = { [key: string]: | MessageSchemaField | MessageSchemaDescription | [MessageSchemaField | MessageSchemaDescription]; }; /** * This type represents the arguments you pass to * {@link ExtensionContext.registerMessageConverter} when you want to register a topic message * converter. * * @category Message converters */ export type RegisterMessageConverterArgsTopic = { type: "topic"; inputTopics: string[]; outputTopic: string; /** Name of the schema for messages output by the converter. * * If you output well-known messages like a foxglove CompressedImage then your schema name would * be `foxglove.CompressedImage`. * * If you are creating a new custom schema you can assign any name. Avoid picking an existing name * if your data uses a different schema. * * NOTE: For ROS users, we also support names like `sensor_msgs/msg/CompressedImage`. See the * supported messages documentation for each panel to learn what kinds of schemas it can display. * */ outputSchemaName: string; /** * Describes the structure of the output messages produced by this converter. * * This optional field allows Foxglove to understand the structure of your messages, enabling * features like autocompletion for message path selection. * * The schema can include: * - Primitive types: "string", "number", "bool", "byte" * - Arrays of primitives: ["string"], ["number"], ["bool"], ["byte"] * - Nested objects with their own field definitions * - Arrays of objects (each element having the same structure) * * @example * ``` * schemaDescription: { * // Simple fields * timestamp: "number", * label: "string", * enabled: "bool", * flags: "byte", * * // Array of primitives * values: ["number"], * names: ["string"], * options: ["bool"], * data: ["byte"], * * // Nested object * position: { * x: "number", * y: "number", * z: "number" * }, * * // Array of objects * landmarks: [{ * id: "string", * position: { * x: "number", * y: "number" * } * }] * } * ``` */ outputSchemaDescription?: MessageSchemaDescription; /** @deprecated use `outputSchemaName` instead */ schemaName?: string; /** @deprecated use `outputSchemaDescription` instead */ schemaDescription?: MessageSchemaDescription; /** * Optional list of global variable names that this converter depends on. When any of these * variables change, the converter will be recreated with the updated global variable values. If * watchVariables is an empty list or undefined, the converter will not be recreated when variables * change. */ watchVariables?: string[]; /** * * The create function initializes the converter. It is called by the app to create the converter * when some panel subscribes to the output topic. This receive the global variables as an optional * first argument. When a watched variable changes, the converter will be recreated with the updated * global variable values. * Global variables do not cause re-processing of current frame data. * * ``` * create: (globalVariables) => { * const threshold = globalVariables.threshold ?? 0; * return (msgEvent: MessageEvent) => { * const msg = msgEvent.message as MySignal; * return { value: Math.abs(msg.acceleration), aboveThreshold: Math.abs(msg.acceleration) > threshold }; * }; * }, * ``` * * @returns A function that is called with a MessageEvent for every input message. It performs any * computation and returns a new message(s) matching the schema description. The function can * optionally return undefined to skip producing message(s) for the given input. */ create: (globalVariables: Readonly>) => TopicConverterReturnType; }; type TopicConverterReturnType = ( messageEvent: Immutable, ) => undefined | Record | Record[]; /** * This type represents the arguments you pass to * {@link ExtensionContext.registerMessageConverter} when you want to register a schema message * converter. * * `schema` converters allow you to leverage Foxglove's built-in visualization panels by * transforming messages to adhere to Foxglove-supported schemas — for example, you can convert * your custom GPS messages to * [`foxglove.LocationFix`](https://docs.foxglove.dev/docs/visualization/message-schemas/location-fix) * messages for visualization in the [Map * panel](https://docs.foxglove.dev/docs/visualization/panels/map). * * See the [Creating a topic * converter](https://docs.foxglove.dev/docs/extensions/guides/create-topic-converter) * guide for more details. * * @category Message converters */ export type RegisterMessageConverterArgsSchema = { type: "schema"; /** The source message schema name. This is the schema name of the original message. */ fromSchemaName: string; /** * The converted message schema name. This is the schema name of the message you will output * from the converter. */ toSchemaName: string; /** * A function which takes the original message and returns the converted message. * * If the function returns `undefined`, the output is ignored, and no message is provided to the * panel. This is useful if you want to selectively output converted messages depending on the * input messages' contents. */ converter: (msg: Src, event: Immutable>) => unknown; }; /** * @deprecated Use {@link RegisterMessageConverterArgsSchema} instead. */ export type LegacyRegisterMessageConverterArgs = { fromSchemaName: string; toSchemaName: string; converter: (msg: Src, event: Immutable>) => unknown; }; /** * This type represents the arguments you pass to {@link ExtensionContext.registerMessageConverter}. * * @category Message converters */ export type RegisterMessageConverterArgs = | LegacyRegisterMessageConverterArgs | RegisterMessageConverterArgsSchema | RegisterMessageConverterArgsTopic; /** @category Topic aliases */ export type BaseTopic = { name: string; schemaName?: string }; /** @category Topic aliases */ export type TopicAlias = { name: string; sourceTopicName: string }; /** * A TopicAliasFunction takes a list of data source topics and variables and outputs * a list of aliased topics. Register this function using {@link ExtensionContext.registerTopicAliases}. * * @category Topic aliases */ export type TopicAliasFunction = ( args: Immutable<{ topics: BaseTopic[]; globalVariables: Readonly>; }>, ) => TopicAlias[]; /** * The {@link ExtensionModule.activate | activate} function's first argument is an * `ExtensionContext` — this context allows you to extend Foxglove for your custom workflows. * * ```typescript * export function activate(extensionContext: ExtensionContext) { * // ... call methods on the extensionContext to extend Foxglove * } * ``` * * @category Entry point */ export interface ExtensionContext { /** * @deprecated This field is no longer used. * @hidden */ readonly mode: "production" | "development" | "test"; /** * `registerPanel` adds a new panel to the Foxglove interface. To register a panel you provide a * `name` and an `initPanel` function. * * The `initPanel` function accepts a {@link PanelExtensionContext} argument, which contains * properties and methods for accessing panel data and rendering UI updates. It also returns an * optional cleanup function to run when the extension `panelElement` unmounts. * * See the [Creating a custom * panel](https://docs.foxglove.dev/docs/extensions/guides/create-custom-panel) * guide for more details. */ registerPanel(params: ExtensionPanelRegistration): void; /** * `registerMessageConverter` registers converters to transform message data within Foxglove. * * You can register two kinds of converters: `schema` and `topic`. * * `schema` converters transform messages of one schema into another. Most often this is used to * turn messages using a custom or proprietary schema into a well-known Foxglove schema for * visualization in one of the built-in panels. `schema` converters allow a built-in panel which * requires well-known messages to natively support visualizing any topic for which there is a * schema converter registered. An example is converting an `acme.Gps` message to * `foxglove.LocationFix` to visualize any topics which publish `acme.Gps` messages in the * built-in map panel. * * See: {@link RegisterMessageConverterArgsSchema}. * * `topic` converters transform messages from one-or-more input topics to a new in-app topic. * Topic converters are more flexible than schema converters but require more logic and * decisions to implement. They can transform existing data into new topics for plotting, * inspecting, and visualizing. Topic converters can combine data from several input topics, * maintain state, and create messages from these multiple topics. They can also do the opposite * and take a single topic and turn it into multiple output topics by registering multiple topic * converters for the same input topic but different output topics. * * See: {@link RegisterMessageConverterArgsTopic}. */ registerMessageConverter(args: RegisterMessageConverterArgs): void; /** * @deprecated Use `registerMessageConverter` with `type: "schema"` or `type: "topic"` instead. */ registerMessageConverter(args: LegacyRegisterMessageConverterArgs): void; /** * Register a schema message converter. * * See: {@link RegisterMessageConverterArgsSchema}. */ registerMessageConverter(args: RegisterMessageConverterArgsSchema): void; /** * Register a topic message converter. * * See: {@link RegisterMessageConverterArgsTopic}. */ registerMessageConverter(args: RegisterMessageConverterArgsTopic): void; /** * `registerTopicAliases` registers a function to compute topic aliases. The provided alias * function should accept an argument with two fields – `topics` with the data source's original * topics and `globalVariables` with the current layout's variables – and return a list of aliased * topics. * * Your alias function runs whenever there are changes to the data source topics or variables. Any * aliases it returns are added to the data source topics (replacing any previously returned * aliases) and available for subscribing or use within message paths as if they were real topics. */ registerTopicAliases(aliasFunction: TopicAliasFunction): void; } /** * @inline * @hidden */ export type ExtensionActivate = (extensionContext: ExtensionContext) => void | Promise; /** * ExtensionModule describes the interface your extension module must export. This typically corresponds to your `index.ts` file. * * You may use either a `default` export or named export syntax: * * ```typescript * export function activate(context: ExtensionContext) { * // ... call methods on the extensionContext to extend Foxglove * } * ``` * * ```typescript * function activate(context: ExtensionContext) { * // ... call methods on the extensionContext to extend Foxglove * } * export default { activate }; * ``` * * The `activate` function can also return a Promise to perform async initialization before * registering panels or converters: * * ```typescript * export async function activate(context: ExtensionContext) { * // Initialize WASM or other async resources * await initializeWasm(); * * // Now register panels/converters that depend on the initialized resources * context.registerPanel({ ... }); * } * ``` * * @category Entry point */ export interface ExtensionModule { /** * This function will be called when your extension is loaded. In this function, you can register * your custom panels or other types of extension features. * * The function may return a Promise if async initialization is needed before registering * extension features. The extension will not be considered fully activated until the Promise * resolves. */ activate: ExtensionActivate; } /** * Icons that can be displayed in the settings tree. Most icon names come from the Material Design * icon set, but the exact icons displayed may change in the future. * * @category Custom panels */ export type SettingsIcon = | "Add" | "Addchart" | "Background" | "Camera" | "Cells" | "Check" | "Circle" | "Clear" | "Clock" | "Collapse" | "Cube" | "Delete" | "Expand" | "Flag" | "Folder" | "FolderOpen" | "Grid" | "Hive" | "ImageProjection" | "Map" | "Move" | "MoveDown" | "MoveUp" | "NorthWest" | "Note" | "NoteFilled" | "Points" | "PrecisionManufacturing" | "Radar" | "Settings" | "Shapes" | "Share" | "Star" | "SouthEast" | "Timeline" | "Topic" | "Tune" | "Walk" | "World"; /** * A settings tree field specifies the input type and the value of a field * in the settings editor. * * @category Custom panels */ export type SettingsTreeFieldValue = | { input: "autocomplete"; value?: string; items: string[]; /** * Optional placeholder text displayed in the field input when value is undefined */ placeholder?: string; } | { input: "boolean"; value?: boolean } | { input: "rgb"; value?: string; /** * Optional placeholder text displayed in the field input when value is undefined */ placeholder?: string; /** * Optional field that's true if the clear button should be hidden. */ hideClearButton?: boolean; } | { input: "rgba"; value?: string; /** * Optional placeholder text displayed in the field input when value is undefined */ placeholder?: string; /** * Optional field that's true if the clear button should be hidden. */ hideClearButton?: boolean; } | { input: "gradient"; value?: [string, string] } | { input: "messagepath"; value?: string; /** Type names like "string", "float64", or the special "any-timestamp" string */ validTypes?: string[]; /** Only include paths from these topics in autocomplete suggestions */ validTopics?: string[]; /** True if the input should allow message path functions like "@abs" */ supportsMessagePathFunctions?: boolean; /** Whether the input should allow Plot-only time-series functions like "@delta". Defaults to true. */ supportsTimeSeriesMessagePathFunctions?: boolean; /** * @deprecated Use `supportsMessagePathFunctions` instead. * True if the input should allow message path functions like "@abs". */ supportsMathModifiers?: boolean; } | { input: "number"; value?: number; step?: number; max?: number; min?: number; precision?: number; /** * Optional placeholder text displayed in the field input when value is undefined */ placeholder?: string; } | { input: "select"; value?: number | number[]; options: Array<{ label: string; value: undefined | number; disabled?: boolean }>; } | { input: "select"; value?: string | string[]; options: Array<{ label: string; value: undefined | string; disabled?: boolean }>; } | { input: "string"; value?: string; /** * Optional placeholder text displayed in the field input when value is undefined */ placeholder?: string; } | { input: "multiline-string"; value?: string; /** * Optional placeholder text displayed in the field input when value is undefined */ placeholder?: string; } | { input: "toggle"; value?: string; options: string[] | Array<{ label: string; value: undefined | string }>; } | { input: "toggle"; value?: number; options: number[] | Array<{ label: string; value: undefined | number }>; } | { input: "vec3"; value?: [undefined | number, undefined | number, undefined | number]; placeholder?: [undefined | string, undefined | string, undefined | string]; step?: number; precision?: number; labels?: [string, string, string]; max?: number; min?: number; } | { input: "vec2"; value?: [undefined | number, undefined | number]; placeholder?: [undefined | string, undefined | string]; step?: number; precision?: number; labels?: [string, string]; max?: number; min?: number; }; /** * A settings tree field specifies the input type and the value of a field * in the settings editor. * * @category Custom panels */ export type SettingsTreeField = SettingsTreeFieldValue & { /** * Optional caption. */ caption?: string; /** * True if the field is disabled. */ disabled?: boolean; /** * Optional help text to explain the purpose of the field. */ help?: string; /** * The label displayed alongside the field. */ label: string; /** * True if the field is readonly. */ readonly?: boolean; /** * Optional message indicating any error state for the field. */ error?: string; /** * Optional message indicating any warning state for the field. * This message is not displayed when an error message is present. */ warning?: string; }; /** * @category Custom panels */ export type SettingsTreeFields = Record; /** * @category Custom panels */ export type SettingsTreeChildren = Record; /** * An action included in the action menu for a settings node. * * @category Custom panels */ export type SettingsTreeNodeActionItem = { type: "action"; /** * A unique identifier for the action. */ id: string; /** * A descriptive label for the action. */ label: string; /** * Optional icon to display with the action. */ icon?: SettingsIcon; /** * Specifies whether the item is rendered as an inline action or as an item in the * context menu. Defaults to "menu" if not specified. Inline items will be rendered * as an icon only if their icon is specified. */ display?: "menu" | "inline"; /** * Optional semantic color for the action button. */ color?: "inherit" | "primary" | "secondary" | "error" | "warning" | "success" | "info"; /** * Whether this action is disabled or not. Defaults to false. */ disabled?: boolean; }; /** * @category Custom panels */ export type SettingsTreeNodeActionDivider = { type: "divider" }; /** * An action included in the action menu for a settings node. * * @category Custom panels */ export type SettingsTreeNodeAction = SettingsTreeNodeActionItem | SettingsTreeNodeActionDivider; /** * Drag-and-drop metadata for a settings tree node. Allows the node to be * dragged onto panels that accept topics or message paths. * * @category Custom panels */ export type SettingsTreeNodeDrag = | { type: "topic"; /** Topic name without quotes, e.g. `/my_topic`. */ topicName: string; } | { type: "message-path"; /** Full message path, e.g. `/my_topic.field` or `"/topic with spaces".field`. */ value: string; /** True if the path points to a primitive value. Required by panels like Plot. */ isLeaf?: boolean; }; /** * A node represents a single item or group of items in the settings tree. * * @category Custom panels */ export type SettingsTreeNode = { /** * An array of actions that can be performed on this node. */ actions?: SettingsTreeNodeAction[]; /** * Other settings tree nodes nested under this node. */ children?: SettingsTreeChildren; /** * Set to collapsed if the node should be initially collapsed. */ defaultExpansionState?: "collapsed" | "expanded"; /** * Optional message indicating any error state for the node. */ error?: string; /** * Optional message indicating any warning state for the node. * This message is not displayed when an error message is present. */ warning?: string; /** * Field inputs attached directly to this node. */ fields?: SettingsTreeFields; /** * Optional icon to display next to the node label. */ icon?: SettingsIcon; /** * Optional color for this node. When an icon is specified, the icon will be * rendered in this color. Otherwise, a small color swatch is displayed before * the label. Expects a valid CSS color string (e.g., "#ff0000", "rgb(255, 0, 0)", "red"). */ color?: string; /** * An optional label shown at the top of this node. */ label?: string; /** * Optional help text to explain the purpose of the node. */ help?: string; /** * True if the node label can be edited by the user. */ renamable?: boolean; /** * Set to `false` on a child of a `childrenReorderable` node to keep this node rendered in place * without drag-and-drop controls or default "Move up" / "Move down" action menu items. */ reorderable?: boolean; /** * Optional sort order to override natural object ordering. All nodes * with a sort order will be rendered before nodes all with no sort order. * * Nodes without an explicit order will be ordered according to ECMA * object ordering rules. * * https://262.ecma-international.org/6.0/#sec-ordinary-object-internal-methods-and-internal-slots-ownpropertykeys */ order?: number | string; /** * When set to `true`, the renderer treats this node's reorderable children as a * user-reorderable list with drag-and-drop controls and default "Move up" / "Move down" action * menu items when at least two reorderable children are present. Both reorder methods dispatch * "reorder-children" actions whose `path` is the path of this parent node. * * When a visibility filter or tree-wide text filter hides any of this node's children, * drag-and-drop affordances are suppressed until the filter is cleared so that the user is never * dragging against a subset of the list. The action menu items remain available because they * resolve against sibling keys in the complete list. * * When this option is enabled, child render order is driven by the order of entries in * `children`. Explicit `order` values on children are ignored and should not be used for * reorderable children. */ childrenReorderable?: | boolean | { /** * Stable id that allows children to be reordered across sibling parents that use the same * group. When omitted, reordering is limited to this node's own children. */ group: string; }; /** * An optional visibility status. If this is not undefined, the node * editor will display a visibility toggle button and send update actions * to the action handler. **/ visible?: boolean; /** * Filter Children by visibility status */ enableVisibilityFilter?: boolean; /** * Drag-and-drop configuration for this node. */ drag?: SettingsTreeNodeDrag; }; /** * Distributes Pick across all members of a union, used for extracting structured * subtypes. */ type DistributivePick = T extends unknown ? Pick : never; /** * Represents actions that can be dispatched to source of the SettingsTree to implement * edits and updates. * * @category Custom panels */ export type SettingsTreeAction = | { action: "update"; payload: { path: readonly string[] } & DistributivePick< SettingsTreeFieldValue, "input" | "value" >; } | { action: "reorder-children"; payload: { /** Path to the parent node whose children are being reordered. */ path: readonly string[]; /** * Path to the parent node the moved child came from. When omitted, it is the same as * `path`. */ sourcePath?: readonly string[]; /** * Key of the child being moved. If this key no longer exists under `path` (for example * because the list changed since the drag started), handlers should ignore the action. */ fromKey: string; /** * Key of the child currently occupying the destination slot. If this key no longer * exists under `path`, handlers should ignore the action. */ toKey: string; /** Whether the moved child should land before or after `toKey`. */ position: "before" | "after"; }; } | { action: "perform-node-action"; payload: { id: string; path: readonly string[] }; }; /** * @inline * @hidden */ export type SettingsTreeNodes = Record; /** * A settings tree is a tree of panel settings that can be displayed and edited in * the panel settings sidebar. * * Nodes and fields in the tree can be referred to by a string path, which collects * the keys of each node on the path from the root to the child node or field. * * For example, for the following tree: * * ```json * root: { * children: { * a: { * children: { * b: { * fields: { * toggleMe: { * label: "Toggle me", * input: "boolean", * value: false, * }, * }, * }, * }, * }, * }, * }, * ``` * * the path to the node at b would be `["a", "b"]` and the path to the toggleMe * field would be `["a", "b", "toggleMe"]`. These paths are used in the * actionHandler, which responds to updates to values in the tree, and also in * the focusedPath, which is used to focus the editor UI at a particular node * in the tree. * * @category Custom panels */ export type SettingsTree = { /** * Handler to process all actions on the settings tree initiated by the UI. */ actionHandler: (action: SettingsTreeAction) => void; /** * True if the settings editor should show the filter control. */ enableFilter?: boolean; /** * Setting this will have a one-time effect of scrolling the editor to the * node at the path and highlighting it. This is a transient effect so it is * not necessary to subsequently unset this. */ focusedPath?: readonly string[]; /** * The settings tree root nodes. Updates to these will automatically be * reflected in the editor UI. */ nodes: SettingsTreeNodes; };