Skip to content
Draft
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 20 additions & 3 deletions python/foxglove-sdk/python/foxglove/_foxglove_py/mcap.pyi
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
from enum import Enum
from typing import Any
from typing import TYPE_CHECKING, Any

if TYPE_CHECKING:
from ..layouts import Layout

class MCAPCompression(Enum):
"""
Expand Down Expand Up @@ -116,10 +119,24 @@ class MCAPWriter:
Common uses include storing configuration files, calibration data, or other
reference material related to the recording.

:param log_time: Time at which the attachment was logged, in nanoseconds since epoch.
:param create_time: Time at which the attachment data was created, in nanoseconds since epoch.
:param log_time: Time at which the attachment was logged, in nanoseconds since
epoch.
:param create_time: Time at which the attachment data was created, in nanoseconds
since epoch.
:param name: Name of the attachment (e.g., "config.json").
:param media_type: MIME type of the attachment (e.g., "application/json").
:param data: Binary content of the attachment.
"""
...

def write_layout(self, layout: "Layout", layout_name: str) -> None:
"""
Write a layout to the MCAP file.

The layout is serialized to JSON and stored in the file's metadata
under the "foxglove.layout" key.

:param layout: A Layout object from foxglove.layouts.
:param layout_name: The name to use as the key in the metadata record.
"""
...
43 changes: 43 additions & 0 deletions python/foxglove-sdk/python/foxglove/tests/test_mcap.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import pytest
from foxglove import Channel, ChannelDescriptor, Context, open_mcap
from foxglove.layouts import Layout, MarkdownPanel
from foxglove.mcap import MCAPWriteOptions

chan = Channel("test", schema={"type": "object"})
Expand Down Expand Up @@ -223,6 +224,48 @@ def test_write_metadata(tmp_mcap: Path) -> None:
_verify_metadata_in_file(tmp_mcap, expected_metadata)


def test_write_layout(tmp_mcap: Path) -> None:
"""Test writing a layout to MCAP file."""
import json

# Create a simple layout with a MarkdownPanel
layout = Layout(
content=MarkdownPanel(
title="Test Panel",
)
)

layout_name = "my_test_layout"

with open_mcap(tmp_mcap) as writer:
writer.write_layout(layout, layout_name)

# Log some messages
for ii in range(5):
chan.log({"foo": ii})

# Verify layout was written correctly as metadata
import mcap.reader

with open(tmp_mcap, "rb") as f:
reader = mcap.reader.make_reader(f)

found_layout = False
for record in reader.iter_metadata():
if record.name == "foxglove.layout":
found_layout = True
# The layout should be stored with the layout_name as the key
assert layout_name in record.metadata
layout_json = record.metadata[layout_name]
# Verify the JSON is valid and contains the expected structure
layout_data = json.loads(layout_json)
assert layout_data.get("version") == 1
assert "content" in layout_data
assert layout_data["content"]["panelType"] == "Markdown"

assert found_layout, "Layout metadata not found in MCAP file"


def test_channel_filter(make_tmp_mcap: Callable[[], Path]) -> None:
tmp_1 = make_tmp_mcap()
tmp_2 = make_tmp_mcap()
Expand Down
25 changes: 25 additions & 0 deletions python/foxglove-sdk/src/mcap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,31 @@ impl PyMcapWriter {
}
Ok(())
}

/// Write a layout to the MCAP file.
///
/// The layout is serialized to JSON and stored in the file's metadata
/// under the "foxglove.layout" key.
///
/// :param layout: A Layout object from foxglove.layouts.
/// :param layout_name: The name to use as the key in the metadata record.
fn write_layout(&self, py: Python<'_>, layout: Py<PyAny>, layout_name: String) -> PyResult<()> {
// Call to_json() on the layout object
let json_str: String = layout.call_method0(py, "to_json")?.extract(py)?;

// Store in metadata with name "foxglove.layout" and layout_name as the key
let mut metadata = std::collections::BTreeMap::new();
metadata.insert(layout_name, json_str);

if let Some(writer) = &self.0 {
writer
.write_metadata("foxglove.layout", metadata)
.map_err(PyFoxgloveError::from)?;
} else {
return Err(PyFoxgloveError::from(foxglove::FoxgloveError::SinkClosed).into());
}
Ok(())
}
}

pub fn register_submodule(parent_module: &Bound<'_, PyModule>) -> PyResult<()> {
Expand Down