Foxglove SDK: A comprehensive quick start tutorial.

This tutorial provides a detailed overview of the Foxglove SDK, enabling efficient understanding and utilization for both theoretical and practical applications.

Overview.

The Foxglove SDK facilitates:

  • Live Data Streaming: Real-time visualization of robot data in the Foxglove app.
  • Data Logging: Recording data to MCAP files for offline analysis.  

Available in C++, Python, and Rust, the SDK is open-source under the MIT license.  

Core concepts.

Messages

A message is a single timestamped log entry, which can be sent to the Foxglove app or written to an MCAP file.  

Schemas

A schema describes the structure of a message’s contents. Foxglove defines several schemas with visualization support, and users can define custom schemas using supported encodings.  

Channels

A channel provides a way to log related messages sharing the same schema. Each channel is identified by a unique topic name. For Foxglove schemas, the SDK offers type-safe channels for logging messages with a known, matching schema (not yet supported in C++).  

Sinks

A sink is a destination for logged messages, such as a WebSocket client or an MCAP writer. Without a configured sink, log messages are dropped. Multiple sinks can be configured and managed dynamically at runtime.  

MCAP

MCAP is a container file format for multimodal log data, supported by Foxglove.  

Installation.

Rust

Install via crates.io:

cargo add foxglove

Python

Install via PyPI:

pip install foxglove-sdk

C++

The C++ SDK is a wrapper around a C library. To build it:

  1. Download the library, source, and header files from the SDK release assets.
  2. Link the library and compile the SDK source as part of your build process.
  3. Ensure your project uses C++17 or newer.  

Quickstart example.

This example demonstrates logging to an MCAP file and streaming to the Foxglove app using the SDK.

Python

import math
import time

import foxglove
from foxglove import Channel
from foxglove.channels import SceneUpdateChannel
from foxglove.schemas import (
    Color,
    CubePrimitive,
    SceneEntity,
    SceneUpdate,
    Vector3,
)

foxglove.set_log_level("DEBUG")

# Our example logs data on a couple of different topics, so we'll create a
# channel for each. We can use a channel like SceneUpdateChannel to log
# Foxglove schemas, or a generic Channel to log custom data.
scene_channel = SceneUpdateChannel("/scene")
size_channel = Channel("/size", message_encoding="json")

# We'll log to both an MCAP file, and to a running Foxglove app via a server.
file_name = "quickstart-python.mcap"
writer = foxglove.open_mcap(file_name)
server = foxglove.start_server()

while True:
    size = abs(math.sin(time.time())) + 1

    # Log messages on both channels until interrupted. By default, each message
    # is stamped with the current time.
    size_channel.log({"size": size})
    scene_channel.log(
        SceneUpdate(
            entities=[
                SceneEntity(
                    cubes=[
                        CubePrimitive(
                            size=Vector3(x=size, y=size, z=size),
                            color=Color(r=1.0, g=0, b=0, a=1.0),
                        )
                    ],
                ),
            ]
        )
    )

    time.sleep(0.033)

Rust

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");
}

C++

#include <foxglove/channel.hpp>
#include <foxglove/mcap.hpp>
#include <foxglove/server.hpp>

#include <atomic>
#include <chrono>
#include <csignal>
#include <functional>
#include <iostream>
#include <thread>

using namespace std::chrono_literals;

// This example logs custom data on an "example" topic. Open Foxglove and connect to the running
// server. Then add a Raw Message panel, and choose the "example" topic.
int main(int argc, const char* argv[]) {
  static std::function<void()> sigintHandler;

  std::signal(SIGINT, [](int) {
    if (sigintHandler) {
      sigintHandler();
    }
  });

  // We'll log to both an MCAP file, and to a running Foxglove app.
  foxglove::McapWriterOptions mcap_options = {};
  mcap_options.path = "quickstart-cpp.mcap";
  auto writerResult = foxglove::McapWriter::create(mcap_options);
  if (!writerResult.has_value()) {
    std::cerr << "Failed to create writer: " << foxglove::strerror(writerResult.error()) << '\n';
    return 1;
  }
  auto writer = std::move(writerResult.value());

  // Start a server to communicate with the Foxglove app.
  foxglove::WebSocketServerOptions ws_options;
  ws_options.host = "127.0.0.1";
  ws_options.port = 8765;
  auto serverResult = foxglove::WebSocketServer::create(std::move(ws_options));
  if (!serverResult.has_value()) {
    std::cerr << "Failed to create server: " << foxglove::strerror(serverResult.error()) << '\n';
    return 1;
  }
  auto server = std::move(serverResult.value());
  std::cerr << "Server listening on port " << server.port() << '\n';

  std::atomic_bool done = false;
  sigintHandler = [&] {
    done = true;
  };

  // Our example logs custom data on an "example" topic, so we'll create a channel for that.
  foxglove::Schema schema;
  schema.encoding = "jsonschema";
  std::string schemaData = R"({
    "type": "object",
    "properties": {
      "val": { "type": "number" }
    }
  })";
  schema.data = reinterpret_cast<const std::byte*>(schemaData.data());
  schema.dataLen = schemaData.size();
  auto channelResult = foxglove::Channel::create("example", "json", std::move(schema));
  if (!channelResult.has_value()) {
    std::cerr << "Failed to create channel: " << foxglove::strerror(channelResult.error()) << '\n';
    return 1;
  }
  auto channel = std::move(channelResult.value());

  while (!done) {
    // Log messages on the channel until interrupted. By default, each message
    // is stamped with the current time.
    std::this_thread::sleep_for(33ms);
    auto now = std::chrono::system_clock::now().time_since_epoch().count();
    std::string msg = "{\"val\": " + std::to_string(now) + "}";
    channel.log(reinterpret_cast<const std::byte*>(msg.data()), msg.size());
  }

  return 0;
}

The C++ example follows a similar structure, utilizing the SDK’s C++ API to log data and stream to the Foxglove app.

Visualization in Foxglove App.

To view the live visualization:

  1. Open the Foxglove app.
  2. Click “Open connection…” and establish a 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.  

Custom schemas.

Foxglove supports defining custom schemas using various encodings:

  • Protobuf
  • JSON Schema
  • FlatBuffers
  • ROS 1/2
  • OMG IDL  

For Protobuf, include the .proto files in your project and use them to publish data via a live Foxglove WebSocket connection or log to an MCAP file.  

Additional resources.

Start building with Foxglove.

Get started