Skip to main content

WebSocket Server

The SDK integrates with the Foxglove app using a WebSocket server.

For an introduction to basic logging with the WebSocket server, see the earlier example. This guide describes more of the features that the SDK and app support.

Connecting

By default, the server listens on 127.0.0.1, port 8765, which matches the app's default connection string. In the app, choose "Open connection" from the dashboard to connect to a server — see Connecting to data for details.

Here, we'll start a server in the SDK with the default options.

let server = foxglove::WebSocketServer::new()
.start_blocking()
.expect("Server failed to start");
note

The examples here use .start_blocking(), but if you're in an async context (such as a tokio.rs runtime), then you can use start().await instead.

You can change the host or port (and other options) when creating the server. If port is 0, an available port will be automatically selected.

let server = foxglove::WebSocketServer::new()
.bind("127.0.0.1", 0)
.start_blocking()
.expect("Server failed to start");

For more options, see the WebSocketServer reference

Sessions

A session describes an instance of a running WebSocket server. The server provides the connected app with a session ID, which allows the app to know whether it is connecting to a new server, or re-connecting to an existing one. You can provide a session ID explicitly when starting the server; by default, it will generate one based on the current time.

Clearing a session

During playback, you may need to reset the visualization state. For example, if you're streaming data from multiple simulation runs, you'll want to clear out data from previous runs. You can do this by clearing the running session.

server.clear_session(None);

When clearing the session, you can optionally provide a new session ID to use instead of the default time-based ID.

Status Messages

You can publish a status message to the app, which will be displayed in the Problems panel. Use this to communicate warnings and errors within the app.

server.publish_status(Status::error("Error!"));

If you provide a status ID when publishing, you can later remove the status from the Problems panel by calling remove_status.

Capabilities

You can augment the SDK's WebSocket server with additional capabilities by implementing interfaces which unlock more features in the app.

The following sections describe the additional capabilities you can build into your server.

Advertising capabilities

In some cases, as noted below, you'll have to specifically advertise these capabilities to the app so that it knows when they're available. To do this, provide one or more capabilities when constructing your server. In this example, we augment the Connecting example above to advertise Time and ClientPublish capabilities.

use foxglove::websocket::Capability;

let server = foxglove::WebSocketServer::new()
.capabilities([Capability::Time, Capability::ClientPublish])
.start_blocking();

Handling messages from the app

The Publish panel can be used send messages to the server from the app.

In addition to advertising ClientPublish support, you'll declare the encodings you support and implement at least one callback function to receive the messages.

This example uses serde_json to parse messages from the client:

cargo add serde_json
use foxglove::websocket::{Capability, Client, ClientChannel, ServerListener};

struct ExampleCallbackHandler;
impl ServerListener for ExampleCallbackHandler {
fn on_message_data(&self, client: Client, channel: &ClientChannel, message: &[u8]) {
let json: serde_json::Value =
serde_json::from_slice(message).expect("Failed to parse message");
println!(
"Client {} published to channel {}: {json}",
client.id(),
channel.id
);
}
}


let server = foxglove::WebSocketServer::new()
.capabilities([Capability::ClientPublish])
.supported_encodings(["json"])
.listener(Arc::new(ExampleCallbackHandler))
.start_blocking();

The ServerListener interface provides ways to track advertisements and subscriptions. For more detail, see the client-publish example.

Timestamp handling

When using WebSocket connections, it's important to understand how timestamps work:

  • Log time: For WebSocket connections, log time is set to when the message is received by the WebSocket client. In the SDK, log_time can optionally be overridden when using the log function. Log time ensures consistent playback ordering.
  • Message timestamps: Your messages can contain their own timestamp fields (like header stamps in ROS messages), which represent when the data was originally captured or created.
  • Playback: The Foxglove app will order messages by log time for playback, but panels like Plot can visualize data using alternative timestamp fields from within your messages.

Broadcasting time

You can broadcast timestamps (nanoseconds since epoch) from the server to inform many panels in the app of the server time, in cases where message timestamps disagree with wall time.

note

You must add the Time capability to your server. See advertising capabilities.

server.broadcast_time(42);

Playback control

Use the PlaybackControl capability when you want Foxglove's UI to control playback when your application streams data from a fixed time range over WebSocket.

To see this capability in action, you can check out a full MCAP player example that implements PlaybackControl. You can use this example as a starting point to build a custom player application using your own data format.

Data flow overview

Requirements

To support PlaybackControl, your server will need to:

  1. Implement a server listener that receives playback control requests from Foxglove, handles them, and returns the updated playback state.
  2. Set up the server by declaring the start and end times of your data, enabling the Time capability, and registering your listener.
  3. Implement a main playback loop that loads messages from your data source, logs them to channels, broadcasts time with the current playback time, and sleeps as needed to maintain playback speed.
  4. When playback ends, or any time playback state changes for a reason other than a request from Foxglove, broadcast the updated playback state using broadcast_playback_state.

Create a server listener for handling playback control requests

Implement a server listener that receives playback control requests from Foxglove, handles them as appropriate in your application, and returns the updated playback state. If handling a request results in a significant jump in time that should reset the panels in Foxglove, set did_seek to true.

note

Even if your application can't fully handle the playback control request, return the playback state that most accurately reflects how your server is actually replaying data so the Foxglove UI stays in sync.

use foxglove::websocket::{
PlaybackCommand, PlaybackControlRequest, PlaybackState, PlaybackStatus, ServerListener,
};

struct ExampleListener {
start_time_ns: u64,
end_time_ns: u64,
}
impl ServerListener for ExampleListener {
fn on_playback_control_request(
&self,
request: PlaybackControlRequest,
) -> Option<PlaybackState> {
// For illustration, this example echoes request fields. In a real application,
// update your playback machinery before returning a PlaybackState.
Some(PlaybackState {
status: match request.playback_command {
PlaybackCommand::Pause => {
PlaybackStatus::Paused
}
_ => PlaybackStatus::Playing,
},
current_time: request.seek_time.unwrap_or(self.start_time_ns),
playback_speed: request.playback_speed,
did_seek: request.seek_time.is_some(),
request_id: Some(request.request_id),
})
}
}

Set up the server

When you construct the server, pass your listener and enable the Time capability, since time broadcasts from the server drive the Foxglove playback bar.

Declare the inclusive start and end timestamps (in nanoseconds since epoch) for the data you plan to replay. This enables the PlaybackControl capability.

use foxglove::websocket::Capability;
use std::sync::Arc;

let listener = Arc::new(ExampleListener {
start_time_ns: 0,
end_time_ns: 10000000000,
});

let server = foxglove::WebSocketServer::new()
.capabilities([Capability::PlaybackControl, Capability::Time])
.playback_time_range(listener.start_time_ns, listener.end_time_ns)
.listener(listener)
.start_blocking()
.expect("Server failed to start");

Implement the playback loop

Your application needs a main playback loop that loads messages from your data source, logs them to the appropriate channels, and sleeps as needed to maintain the requested playback speed. Each iteration through the loop, call broadcast_time() with the current playback time (in nanoseconds since epoch). This keeps the Foxglove playback bar synchronized with your replayed data.

static CHANNEL: foxglove::LazyChannel<foxglove::schemas::LocationFix> =
foxglove::LazyChannel::new("/location");

loop {
// Wait while playback is paused.
wait_while_paused();

// Load the next message from your data source.
// If there are no more messages, broadcast the Ended state as shown below.
let message = load_next_message();

// Broadcast the current playback time to keep the Foxglove playback bar in sync.
server.broadcast_time(message.timestamp);

// Log the message to the appropriate channel.
CHANNEL.log(&message.data);

// Sleep to maintain the requested playback speed.
sleep_until_next_message();
}

Broadcast playback state changes

If playback changes for any reason other than a request from Foxglove (for example, hitting the end of the time range or if an external UI also controls playback), broadcast the updated playback state with broadcast_playback_state() so the Foxglove UI stays in sync.

server.broadcast_playback_state(PlaybackState {
status: PlaybackStatus::Ended,
current_time: end_time_ns,
playback_speed: 1.0,
did_seek: false,
request_id: None,
});