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 more common methods and properties are described below. For full type docs on the PanelExtensionContext
and RenderState
types, see @foxglove/studio
.
The
initPanel
function used in theregisterPanel
method accepts aPanelExtensionContext
argument. This argument contains properties and methods for accessing panel data and rendering UI updates. TheinitPanel
function also returns an optional cleanup function to run when the extensionpanelElement
unmounts.
panelElement
panelElement: HTMLDivElement
The root DOM element for your panel. Add any panel UI to this DOM element or pass this element as the root of your UI framework.
onRender
onRender(renderState: Readonly<RenderState>, done: () => void)
Flora will run context.onRender
whenever your panel needs to re-render during playback. The function accepts renderState
and a done
callback as its arguments.
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.
context.onRender = (renderState: 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 actaully happened.
done();
};
watch
watch(field: keyof RenderState)
Use context.watch
to indicate which fields in renderState
(e.g. allFrames
, appSettings
, currentFrame
, currentTime
, previewTime
, parameters
, topics
) should trigger panel re-renders when their contained values change.
context.watch("topics");
context.watch("currentFrame");
context.watch("parameters");
context.watch("currentTime");
subscribe
subscribe(subscriptions: { topic: string, preload?: boolean, convertTo?: string }[])
Use context.subscribe
to indicate the topics your panel wants to receive messages for. The messages are provided during render in renderState.currentFrame
and renderState.allFrames
.
context.subscribe([{ topic: "/some/topic" }, { topic: "/another/topic", preload: true }]);
context.subscribe([]);
will unsubscribe from all topics.
Preloading
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
).
By default, subscriptions will provide only the messages for the current frame. If your panel would like to receive all the available messages on a topic, set the preload
option to true
. Preloaded messages are available in renderState.allFrames
.
NOTE: Message preloading is done on a best-effort basis. If your preloaded messages exceed available memory limits for the browser or desktop app, then the preloaded data may not represent the full dataset range. Preloading results in more data transfer and memory use and is recommended only for panels which should display the entire dataset (e.g. the Plot panel) rather than the current frame.
Message converters
Message converters can convert messages from one schema to another – for example, a user might convert custom GPS message into foxglove.LocationFix
messages for visualization in the Map panel. 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 currentFrame
and allFrame
message events 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.
context.subscribe([{ topic: "/some/topic", convertTo: "foxglove.LocationFix" }]);
The
convertibleTo
field withinrenderState.topics
will contain the names of schemas you can convert this topic into.
advertise
advertise(topic: string, datatype: string, options?: Record<string, unknown>)
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.
context.advertise("/my_image_topic", "sensor_msgs/Image");
options
are specific to each data source - see documentation below for supported data sources.
Native (ROS 1)
field | type | description |
---|---|---|
datatypes | Map<string, T> | JavaScript map of datatype names and MessageDefinition definitions for a given topic |
Common datatype definitions are available in the @foxglove/rosmsg-msgs-common
package.
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"]],
]),
});
Flora WebSocket
options
depend on the server implementation.
Flora Bridge
When using the Flora 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 connection.
For ROS 2 data, pass datatypes using the following fields:
field | type | description |
---|---|---|
datatypes | Map<string, T> | JavaScript map of datatype names and MessageDefinition definitions for a given topic |
Common datatype definitions are available in the @foxglove/rosmsg-msgs-common
package.
import { ros2humble as ros2 } from "@foxglove/rosmsg-msgs-common";
// For Galactic and lower, use:
// import { ros2galactic as ros2 } from "@foxglove/rosmsg-msgs-common";
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, ROS 2)
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.
unadvertise
unadvertise(topic: string)
Use context.unadvertise
to remove advertising for a topic.
context.unadvertise("/my_image_topic");
publish
publish(topic: string, message: Record<string, unknown>)
Use context.publish
to publish a message on a previously advertised topic. If the topic is not advertised or otherwise malformed, the function will throw an error.
context.advertise("/my_color_topic", "std_msgs/ColorRGBA");
context.publish("/my_color_topic", { r: 0, g: 1, b: 0, a: 1 });
setParameter
setParameter: (name: string, value: ParameterValue)
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).
context.setParameter("/param1", "value1");
setVariable
setVariable: (name: string, value: VariableValue)
Use context.setVariable
to set a variable name
to any valid variable value
.
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 actaully happened.
done();
};
callService
callService: (service: string, request: unknown) => Promise<unknown>
Use context.callService
to make a service
call to the specified service with a request payload.
context.callService("my_service", { foo: "bar" });
saveState
saveState(state: Partial<unknown>)
Use context.saveState
to save an arbitrary object as persisted panel state in the Flora layout.
context.initialState = undefined; // your panel's initial state
context.saveState({ myNum: 2, myBool: false, myStr: "abc" });
setDefaultPanelTitle
setDefaultPanelTitle("My panel title")
Use context.setDefaultPanelTitle
to override a panel's default title, usually determined by its configured message path.
If no override or default title is set, the panel will simply display its type (e.g. "Image").
// Override the default panel title
context.setDefaultPanelTitle("Camera Calibration");
addPanel
addPanel({ position: "sibling", type: string, updateIfExists: boolean, getState(existingState?: unknown): unknown; })
Use context.layout.addPanel
to add a sibling panel of a given panel type (e.g. "RawMessages"
) to the layout.
Use extensionname.panelname
as the type
to open a panel installed via an Extension. extensionname
is the extension name from package.json
and panelname
is the name provided when the extension registers a panel.
If updateIfExists
is set to true, this will update a sibling panel of the same panel type, if it already exists in the layout.
updateIfExists
is supported for the following panel types:
3D
Data Source Info
Diagnostics (ROS)
Gauge
Image
Indicator
Log
Map
Parameters
Plot
Publish
Raw Messages
Service Call
State Transitions
Tab
Table
Teleop
Topic Graph
User Scripts
Variable Slider
getState
is set to a function that returns the state for the added or updated panel. When updating an existing panel, the existing state will be passed in as getState
's first argument.
// Add new panel
context.layout.addPanel({
position: "sibling",
type: "RawMessages",
updateIfExists: false,
getState: () => {},
});
// Update existing panel
context.layout.addPanel({
position: "sibling",
type: "RawMessages",
updateIfExists: true,
getState: ((existingState) => { topicPath: "/some_new_path", ...existingState }),
});