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.
- Rust
- Python
- C++
let server = foxglove::WebSocketServer::new()
.start_blocking()
.expect("Server failed to start");
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.
import foxglove
server = foxglove.start_server()
#include <foxglove/server.hpp>
foxglove::WebSocketServerOptions ws_options;
auto server_result = foxglove::WebSocketServer::create(std::move(ws_options));
if (!server_result.has_value()) {
std::cerr << "Failed to create server: " << foxglove::strerror(server_result.error()) << '\n';
return 1;
}
auto server = std::move(server_result.value());
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.
- Rust
- Python
- C++
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
import foxglove
server = foxglove.start_server(host="192.168.0.101", port=0)
For more options, see the start_server reference
#include <foxglove/server.hpp>
foxglove::WebSocketServerOptions ws_options;
ws_options.host = "192.168.0.101";
ws_options.port = 0;
auto server_result = foxglove::WebSocketServer::create(std::move(ws_options));
if (!server_result.has_value()) {
std::cerr << "Failed to create server: " << foxglove::strerror(server_result.error()) << '\n';
return 1;
}
auto server = std::move(server_result.value());
For more options, see the WebSocketServerOptions 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.
- Rust
- Python
- C++
server.clear_session(None);
server.clear_session()
auto error = server.clearSession();
if (error != foxglove::FoxgloveError::Ok) {
std::cerr << "Failed to clear session: " << foxglove::strerror(error) << '\n';
}
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.
- Rust
- Python
- C++
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.
from foxglove.websocket import StatusLevel
server.publish_status("Error!", StatusLevel.Error)
If you provide a status ID when publishing, you can later remove the status from the Problems panel by calling remove_status.
server.publishStatus(foxglove::WebSocketServerStatusLevel::Error, "Error!")
If you provide a status ID when publishing, you can later remove the status from the Problems panel by calling removeStatus.
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.
- Rust
- Python
- C++
use foxglove::websocket::Capability;
let server = foxglove::WebSocketServer::new()
.capabilities([Capability::Time, Capability::ClientPublish])
.start_blocking();
from foxglove import start_server
from foxglove.websocket import Capability
server = start_server(capabilities=[Capability.Time, Capability.ClientPublish])
foxglove::WebSocketServerOptions options;
options.capabilities = {foxglove::WebSocketServerCapabilities::Time &
foxglove::WebSocketServerCapabilities::ClientPublish};
auto server = foxglove::WebSocketServer::create(std::move(options)).value();
// Error checking omitted for brevity
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.
- Rust
- Python
- C++
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.
from foxglove import start_server
from foxglove.websocket import (Capability, ServerListener)
class ExampleListener(ServerListener):
def on_message_data(
self,
client: Client,
client_channel_id: int,
data: bytes,
) -> None:
logging.info(f"Message from client {client.id} on channel {client_channel_id}")
logging.info(f"Data: {data!r}")
listener = ExampleListener()
server = start_server(
capabilities=[Capability.ClientPublish],
supported_encodings=["json"],
server_listener=listener,
)
The ServerListener interface provides ways to track advertisements and subscriptions. For
more detail, see the live-visualization example.
#include <cstddef>
foxglove::WebSocketServerOptions options;
options.capabilities = {foxglove::WebSocketServerCapabilities::ClientPublish};
options.supported_encodings = {"json"};
options.callbacks.onMessageData =
[&](uint32_t client_id [[maybe_unused]],
uint32_t client_channel_id [[maybe_unused]], const std::byte *data,
size_t data_len) {
std::cout << "Received message from client " << client_id
<< " on channel " << client_channel_id << std::endl;
};
auto server = foxglove::WebSocketServer::create(std::move(options)).value();
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_timecan optionally be overridden when using thelogfunction. 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.
You must add the Time capability to your server. See advertising capabilities.
- Rust
- Python
- C++
server.broadcast_time(42);
server.broadcast_time(42)
server.broadcastTime(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:
- Implement a server listener that receives playback control requests from Foxglove, handles them, and returns the updated playback state.
- Set up the server by declaring the start and end times of your data, enabling the
Timecapability, and registering your listener. - 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.
- 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.
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.
- Rust
- Python
- C++
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),
})
}
}
from foxglove.websocket import (
PlaybackCommand, PlaybackControlRequest,
PlaybackState, PlaybackStatus, ServerListener
)
class ExampleListener(ServerListener):
def __init__(self, start_time_ns: int, end_time_ns: int) -> None:
self.start_time_ns = start_time_ns
self.end_time_ns = end_time_ns
# ... other machinery needed for playback ...
def on_playback_control_request(
self, playback_control_request: PlaybackControlRequest
) -> PlaybackState:
# For illustration, this example echoes request fields. In a real application,
# update your playback machinery before returning a PlaybackState.
seek_time = playback_control_request.seek_time
return PlaybackState(
status=(
PlaybackStatus.Paused
if playback_control_request.playback_command is PlaybackCommand.Pause
else PlaybackStatus.Playing
),
current_time=seek_time if seek_time is not None else self.start_time_ns,
playback_speed=playback_control_request.playback_speed,
did_seek=playback_control_request.seek_time is not None,
request_id=playback_control_request.request_id,
)
#include <foxglove/server.hpp>
std::optional<foxglove::PlaybackState> onPlaybackControlRequest(
const foxglove::PlaybackControlRequest& request
) {
// For illustration, this example echoes request fields. In a real application,
// update your playback machinery before returning a PlaybackState.
foxglove::PlaybackState playback_state{};
playback_state.status = request.playback_command == foxglove::PlaybackCommand::Pause
? foxglove::PlaybackStatus::Paused
: foxglove::PlaybackStatus::Playing;
playback_state.current_time = request.seek_time.value_or(0ULL);
playback_state.playback_speed = request.playback_speed;
playback_state.did_seek = request.seek_time.has_value();
playback_state.request_id = request.request_id;
return playback_state;
}
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.
- Rust
- Python
- C++
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");
from foxglove import start_server
from foxglove.websocket import Capability
listener = ExampleListener(start_time_ns=0, end_time_ns=10000000000)
server = start_server(
capabilities=[Capability.PlaybackControl, Capability.Time],
playback_time_range=(listener.start_time_ns, listener.end_time_ns),
server_listener=listener,
)
// ... Load your data and determine its time bounds ...
uint64_t start_time_ns = 0ULL;
uint64_t end_time_ns = 1000000000ULL;
foxglove::WebSocketServerOptions ws_options;
ws_options.capabilities = {
foxglove::WebSocketServerCapabilities::PlaybackControl |
foxglove::WebSocketServerCapabilities::Time};
ws_options.playback_time_range = std::make_pair(start_time_ns, end_time_ns);
ws_options.callbacks.onPlaybackControlRequest = onPlaybackControlRequest;
auto server_result = foxglove::WebSocketServer::create(std::move(ws_options));
if (!server_result.has_value()) {
std::cerr << "Error in creating Foxglove WebSocketServer: "
<< foxglove::strerror(server_result.error()) << std::endl;
exit(1);
}
auto server = std::move(server_result.value());
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.
- Rust
- Python
- C++
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();
}
from foxglove.channels import LocationFixChannel
channel = LocationFixChannel("/location")
while True:
# 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.
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()
auto channel = foxglove::schemas::LocationFixChannel::create("/location").value();
while (true) {
// Wait while playback is paused.
waitWhilePaused();
// Load the next message from your data source.
// If there are no more messages, broadcast the Ended state as shown below.
auto message = loadNextMessage();
// Broadcast the current playback time to keep the Foxglove playback bar in sync.
server.broadcastTime(message->timestamp);
// Log the message to the appropriate channel.
channel.log(message->data);
// Sleep to maintain the requested playback speed.
sleepUntilNextMessage();
}
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.
- Rust
- Python
- C++
server.broadcast_playback_state(PlaybackState {
status: PlaybackStatus::Ended,
current_time: end_time_ns,
playback_speed: 1.0,
did_seek: false,
request_id: None,
});
server.broadcast_playback_state(
PlaybackState(
status=PlaybackStatus.Ended,
current_time=end_time_ns,
playback_speed=1.0,
did_seek=False,
request_id=None,
)
)
foxglove::PlaybackState playback_state{};
playback_state.status = foxglove::PlaybackStatus::Ended;
playback_state.current_time = end_time_ns;
playback_state.playback_speed = 1.0F;
playback_state.did_seek = false;
playback_state.request_id = std::nullopt;
server.broadcastPlaybackState(playback_state);