Skip to main content

Quickstart example

In this example, we'll learn how to log a scene update for an animated cube to an MCAP file and visualize it live in the Foxglove app using the SDK. You can see the full code example below.

Let's setup a new project called "quickstart". Note the commands here are for Linux and will vary by platform, especially on Windows, if not using WSL.

Create a new project and add the Foxglove SDK

With cargo installed:

cargo new quickstart --bin
cd quickstart
cargo add foxglove
# Optional to configure debug logs for the SDK
cargo add env_logger

Create an MCAP file for writing

The SDK logs to sinks (log destinations). If you do not configure a sink, log messages will simply be dropped without being recorded. You can configure multiple sinks, and you can create or destroy them dynamically at runtime. The SDK comes with two types of sinks:

  • An MCAP sink, which directs log output to an MCAP file.
  • A WebSocket server which transmits logs over a WebSocket connection to the Foxglove app.

Here's how we can create a sink that writes logs to an MCAP file:

Open src/main.rs and replace it with:

use foxglove::{McapWriter, WebSocketServer};

const FILE_NAME: &str = "quickstart-rust.mcap";

fn main() {
// This doesn't affect what gets logged to the MCAP file, this is for troubleshooting the SDK integration
let env = env_logger::Env::default().default_filter_or("debug");
env_logger::init_from_env(env);

// Open an MCAP file for logging
let mcap = McapWriter::new()
.create_new_buffered_file(FILE_NAME)
.expect("Failed to start mcap writer");

// Calling close is optional; it will be closed when the reference is dropped.
// Closing it explicitly lets us check for errors.
mcap.close().expect("error closing mcap writer");
}

Run with:

cargo run

Live visualization

We can also add a second log destination, to send them via WebSocket connection to the Foxglove app, for live visualization and debugging. See running the app below for how to connect to it. If there are no connected clients, this adds very little overhead.

Open src/main.rs and replace it with:

use foxglove::{McapWriter, WebSocketServer};

const FILE_NAME: &str = "quickstart-rust.mcap";

fn main() {
let env = env_logger::Env::default().default_filter_or("debug");
env_logger::init_from_env(env);

// We'll log to both an MCAP file, and to a running Foxglove app via a server.
let mcap = McapWriter::new()
.create_new_buffered_file(FILE_NAME)
.expect("Failed to start mcap writer");

// Start a server to communicate with the Foxglove app.
// This will run indefinitely even if references are dropped.
let server = WebSocketServer::new()
.start_blocking()
.expect("Server failed to start");

// If you need to close it before the process ends, use the stop method.
// This returns a handle that can be used to gracefully shutdown.
// Dropping the handle means all client tasks will be immediately aborted.
server.stop();
}

Create channels for logging

The SDK logs data via channels. A channel has a topic name and some optional type information. Let's create a channel /size for logging some JSON and another named /scene for logging structured SceneUpdates that we can visualize in the Foxglove app. You can log to channels from any thread, the channels and sinks are thread-safe.

SceneUpdate is one of the Foxglove well-known schemas. It updates the entities displayed in a 3D scene. We'll use it to display a 3D animated cube.

use foxglove::schemas::{SceneUpdate};
use foxglove::{LazyChannel, LazyRawChannel, McapWriter, WebSocketServer};

const FILE_NAME: &str = "quickstart-rust.mcap";

// Our example logs data on a couple of different topics, so we'll create a
// channel for each. We can use a channel like Channel<SceneUpdate> to log
// Foxglove schemas, or a generic RawChannel to log custom data.
static SCENE: LazyChannel<SceneUpdate> = LazyChannel::new("/scene");
static SIZE: LazyRawChannel = LazyRawChannel::new("/size", "json");

fn main() {
let env = env_logger::Env::default().default_filter_or("debug");
env_logger::init_from_env(env);

// We'll log to both an MCAP file, and to a running Foxglove app via a server.
// This will be closed when the reference is dropped.
let mcap = McapWriter::new()
.create_new_buffered_file(FILE_NAME)
.expect("Failed to start mcap writer");

// Start a server to communicate with the Foxglove app.
// This will run indefinitely even if references are dropped.
WebSocketServer::new()
.start_blocking()
.expect("Server failed to start");
}

Use the channels to log some data

Let's create the SceneUpdate message, and update it at 30 frames a second for 10 seconds (every 33ms).

use foxglove::schemas::{Color, CubePrimitive, SceneEntity, SceneUpdate, Vector3};
use foxglove::{LazyChannel, LazyRawChannel, McapWriter, WebSocketServer};

const FILE_NAME: &str = "quickstart-rust.mcap";

// Our example logs data on a couple of different topics, so we'll create a
// channel for each. We can use a channel like Channel<SceneUpdate> to log
// Foxglove schemas, or a generic RawChannel to log custom data.
static SCENE: LazyChannel<SceneUpdate> = LazyChannel::new("/scene");
static SIZE: LazyRawChannel = LazyRawChannel::new("/size", "json");

fn main() {
let env = env_logger::Env::default().default_filter_or("debug");
env_logger::init_from_env(env);

// We'll log to both an MCAP file, and to a running Foxglove app via a server.
// This will be closed when the reference is dropped.
let mcap = McapWriter::new()
.create_new_buffered_file(FILE_NAME)
.expect("Failed to start mcap writer");

// Start a server to communicate with the Foxglove app.
// This will run indefinitely even if references are dropped.
WebSocketServer::new()
.start_blocking()
.expect("Server failed to start");

for _ in 0..10*30 {
let size = 1.0 + std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs_f64()
.sin()
.abs();

// Log messages on the channel until interrupted. By default, each message
// is stamped with the current time.
SIZE.log(format!("{{\"size\": {size}}}").as_bytes());
SCENE.log(&SceneUpdate {
deletions: vec![],
entities: vec![SceneEntity {
id: "box".to_string(),
cubes: vec![CubePrimitive {
size: Some(Vector3 {
x: size,
y: size,
z: size,
}),
color: Some(Color {
r: 1.0,
g: 0.0,
b: 0.0,
a: 1.0,
}),
..Default::default()
}],
..Default::default()
}],
});

std::thread::sleep(std::time::Duration::from_millis(33));
}
}
tip

You can now open the generated .mcap file with the Foxglove app. To quickly inspect the data you've logged to the MCAP file, you can also use the MCAP CLI.

mcap info <file.mcap>

Some context

You may have wondered how the data gets logged from the channels above to the sinks we setup at the start of main. The answer is they're linked together by a Context object that acts like a kind of a namespace/container for sinks and channels. All channels in a context are logged to all sinks for that same context. A channel and a sink can only be associated with a single context. In the example above, we don't specify the context, so the smae global default context is used for everything, which is how they ended up connected.

We could have made the context explicit like this:

use foxglove::schemas::{Color, CubePrimitive, SceneEntity, SceneUpdate, Vector3};
use foxglove::{LazyContext, LazyChannel, LazyRawChannel, McapWriter, WebSocketServer};

const FILE_NAME: &str = "quickstart-rust.mcap";

static CTX_A: LazyContext = LazyContext::new();

// Our example logs data on a couple of different topics, so we'll create a
// channel for each. We can use a channel like Channel<SceneUpdate> to log
// Foxglove schemas, or a generic RawChannel to log custom data.
static SCENE: LazyChannel<SceneUpdate> = CTX_A.channel("/scene");
static SIZE: LazyRawChannel = CTX_A.raw_channel("/size", "json");

fn main() {
let env = env_logger::Env::default().default_filter_or("debug");
env_logger::init_from_env(env);

// We'll log to both an MCAP file, and to a running Foxglove app via a server.
// This will be closed when the reference is dropped.
let mcap = CTX_A.mcap_writer()
.create_new_buffered_file(FILE_NAME)
.expect("Failed to start mcap writer");

// Start a server to communicate with the Foxglove app.
// This will run indefinitely even if references are dropped.
CTX_A.websocket_server()
.start_blocking()
.expect("Server failed to start");
}

Example code

Here's the full example. We've modified the loop to continue until explicit exit via Ctrl+C interrupt.

use std::ops::Add;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::SystemTime;

use foxglove::schemas::{Color, CubePrimitive, SceneEntity, SceneUpdate, Vector3};
use foxglove::{LazyChannel, LazyRawChannel, McapWriter};

const FILE_NAME: &str = "quickstart-rust.mcap";

// Our example logs data on a couple of different topics, so we'll create a
// channel for each. We can use a channel like Channel<SceneUpdate> to log
// Foxglove schemas, or a generic RawChannel to log custom data.
static SCENE: LazyChannel<SceneUpdate> = LazyChannel::new("/scene");
static SIZE: LazyRawChannel = LazyRawChannel::new("/size", "json");

fn main() {
let env = env_logger::Env::default().default_filter_or("debug");
env_logger::init_from_env(env);

let done = Arc::new(AtomicBool::default());
ctrlc::set_handler({
let done = done.clone();
move || {
done.store(true, Ordering::Relaxed);
}
})
.expect("Failed to set SIGINT handler");

// We'll log to both an MCAP file, and to a running Foxglove app via a server.
let mcap = McapWriter::new()
.create_new_buffered_file(FILE_NAME)
.expect("Failed to start mcap writer");

// Start a server to communicate with the Foxglove app. This will run indefinitely, even if
// references are dropped.
foxglove::WebSocketServer::new()
.start_blocking()
.expect("Server failed to start");

while !done.load(Ordering::Relaxed) {
let size = SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs_f64()
.sin()
.abs()
.add(1.0);

// Log messages on the channel until interrupted. By default, each message
// is stamped with the current time.
SIZE.log(format!("{{\"size\": {size}}}").as_bytes());
SCENE.log(&SceneUpdate {
deletions: vec![],
entities: vec![SceneEntity {
id: "box".to_string(),
cubes: vec![CubePrimitive {
size: Some(Vector3 {
x: size,
y: size,
z: size,
}),
color: Some(Color {
r: 1.0,
g: 0.0,
b: 0.0,
a: 1.0,
}),
..Default::default()
}],
..Default::default()
}],
});

std::thread::sleep(std::time::Duration::from_millis(33));
}

mcap.close().expect("Failed to close mcap writer");
}

For more examples, see the source examples and reference documentation.

Running the app

Now that you're running the above example, let's view the live visualization.

  1. Open the Foxglove app or visit https://app.foxglove.dev/.
  2. Click "Open connection..." and open a Foxglove WebSocket connection with the default URL.
  3. Add a 3D panel to your layout.
  4. Subscribe to the "/scene" topic by toggling its visibility in the panel settings sidebar.

Foxglove WebSocket dialog

See the Live data documentation for help connecting to the app, and the 3D panel documentation for help configuring the scene.