foxglove-sdk 0.14.2__cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl → 0.16.3__cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of foxglove-sdk might be problematic. Click here for more details.

foxglove/__init__.py CHANGED
@@ -9,20 +9,34 @@ from __future__ import annotations
9
9
 
10
10
  import atexit
11
11
  import logging
12
+ from typing import TYPE_CHECKING
12
13
 
13
14
  from . import _foxglove_py as _foxglove
14
15
 
15
16
  # Re-export these imports
16
- from ._foxglove_py import Context, Schema, open_mcap
17
+ from ._foxglove_py import (
18
+ ChannelDescriptor,
19
+ Context,
20
+ Schema,
21
+ SinkChannelFilter,
22
+ open_mcap,
23
+ )
17
24
  from .channel import Channel, log
18
25
 
19
26
  # Deprecated. Use foxglove.mcap.MCAPWriter instead.
20
27
  from .mcap import MCAPWriter
21
28
 
29
+ if TYPE_CHECKING:
30
+ from .notebook.notebook_buffer import NotebookBuffer
31
+
22
32
  atexit.register(_foxglove.shutdown)
23
33
 
24
34
 
25
35
  try:
36
+ from ._foxglove_py.cloud import CloudSink
37
+
38
+ # from ._foxglove_py.cloud import start_cloud_sink as _start_cloud_sink
39
+ from .cloud import CloudSinkListener
26
40
  from .websocket import (
27
41
  AssetHandler,
28
42
  Capability,
@@ -43,6 +57,8 @@ try:
43
57
  asset_handler: AssetHandler | None = None,
44
58
  context: Context | None = None,
45
59
  session_id: str | None = None,
60
+ channel_filter: SinkChannelFilter | None = None,
61
+ playback_time_range: tuple[int, int] | None = None,
46
62
  ) -> WebSocketServer:
47
63
  """
48
64
  Start a websocket server for live visualization.
@@ -61,6 +77,11 @@ try:
61
77
  :param session_id: An ID which allows the client to understand if the connection is a
62
78
  re-connection or a new server instance. If None, then an ID is generated based on the
63
79
  current time.
80
+ :param channel_filter: A `Callable` that determines whether a channel should be logged to.
81
+ Return `True` to log the channel, or `False` to skip it. By default, all channels
82
+ will be logged.
83
+ :param playback_time_range: Time range of data being played back, in absolute nanoseconds.
84
+ Implies `Capability.RangedPlayback` if set.
64
85
  """
65
86
  return _foxglove.start_server(
66
87
  name=name,
@@ -73,6 +94,36 @@ try:
73
94
  asset_handler=asset_handler,
74
95
  context=context,
75
96
  session_id=session_id,
97
+ channel_filter=channel_filter,
98
+ playback_time_range=playback_time_range,
99
+ )
100
+
101
+ def start_cloud_sink(
102
+ *,
103
+ listener: CloudSinkListener | None = None,
104
+ supported_encodings: list[str] | None = None,
105
+ context: Context | None = None,
106
+ session_id: str | None = None,
107
+ ) -> CloudSink:
108
+ """
109
+ Connect to Foxglove Agent for live visualization and teleop.
110
+
111
+ Foxglove Agent must be running on the same host for this to work.
112
+
113
+ :param capabilities: A list of capabilities to advertise to the agent.
114
+ :param listener: A Python object that implements the
115
+ :py:class:`cloud.CloudSinkListener` protocol.
116
+ :param supported_encodings: A list of encodings to advertise to the agent.
117
+ :param context: The context to use for logging. If None, the global context is used.
118
+ :param session_id: An ID which allows the agent to understand if the connection is a
119
+ re-connection or a new connection instance. If None, then an ID is generated based on
120
+ the current time.
121
+ """
122
+ return _foxglove.start_cloud_sink(
123
+ listener=listener,
124
+ supported_encodings=supported_encodings,
125
+ context=context,
126
+ session_id=session_id,
76
127
  )
77
128
 
78
129
  except ImportError:
@@ -123,13 +174,72 @@ def _level_names() -> dict[str, int]:
123
174
  }
124
175
 
125
176
 
177
+ def init_notebook_buffer(context: Context | None = None) -> NotebookBuffer:
178
+ """
179
+ Create a NotebookBuffer object to manage data buffering and visualization in Jupyter
180
+ notebooks.
181
+
182
+ The NotebookBuffer object will buffer all data logged to the provided context. When you
183
+ are ready to visualize the data, you can call the :meth:`show` method to display an embedded
184
+ Foxglove visualization widget. The widget provides a fully-featured Foxglove interface
185
+ directly within your Jupyter notebook, allowing you to explore multi-modal robotics data
186
+ including 3D scenes, plots, images, and more.
187
+
188
+ Args:
189
+ context: The Context used to log the messages. If no Context is provided, the global
190
+ context will be used. Logged messages will be buffered.
191
+
192
+ Returns:
193
+ NotebookBuffer: A NotebookBuffer object that can be used to manage the data buffering
194
+ and visualization.
195
+
196
+ Raises:
197
+ Exception: If the notebook extra package is not installed. Install it
198
+ with `pip install foxglove-sdk[notebook]`.
199
+
200
+ Note:
201
+ This function is only available when the `notebook` extra package
202
+ is installed. Install it with `pip install foxglove-sdk[notebook]`.
203
+
204
+ Example:
205
+ >>> import foxglove
206
+ >>>
207
+ >>> # Create a basic viewer using the default context
208
+ >>> nb_buffer = foxglove.init_notebook_buffer()
209
+ >>>
210
+ >>> # Or use a specific context
211
+ >>> nb_buffer = foxglove.init_notebook_buffer(context=my_ctx)
212
+ >>>
213
+ >>> # ... log data as usual ...
214
+ >>>
215
+ >>> # Display the widget in the notebook
216
+ >>> nb_buffer.show()
217
+ """
218
+ try:
219
+ from .notebook.notebook_buffer import NotebookBuffer
220
+
221
+ except ImportError:
222
+ raise Exception(
223
+ "NotebookBuffer is not installed. "
224
+ 'Please install it with `pip install "foxglove-sdk[notebook]"`'
225
+ )
226
+
227
+ return NotebookBuffer(context=context)
228
+
229
+
126
230
  __all__ = [
127
231
  "Channel",
232
+ "ChannelDescriptor",
128
233
  "Context",
129
234
  "MCAPWriter",
130
235
  "Schema",
236
+ "SinkChannelFilter",
237
+ "CloudSink",
238
+ "CloudSinkListener",
239
+ "start_cloud_sink",
131
240
  "log",
132
241
  "open_mcap",
133
242
  "set_log_level",
134
243
  "start_server",
244
+ "init_notebook_buffer",
135
245
  ]
@@ -1,8 +1,27 @@
1
1
  from pathlib import Path
2
- from typing import Any
2
+ from typing import Any, BinaryIO, Callable, Protocol
3
3
 
4
4
  from foxglove.websocket import AssetHandler
5
5
 
6
+ class McapWritable(Protocol):
7
+ """A writable and seekable file-like object.
8
+
9
+ This protocol defines the minimal interface required for writing MCAP data.
10
+ """
11
+
12
+ def write(self, data: bytes | bytearray) -> int:
13
+ """Write data and return the number of bytes written."""
14
+ ...
15
+
16
+ def seek(self, offset: int, whence: int = 0) -> int:
17
+ """Seek to position and return the new absolute position."""
18
+ ...
19
+
20
+ def flush(self) -> None:
21
+ """Flush any buffered data."""
22
+ ...
23
+
24
+ from .cloud import CloudSink
6
25
  from .mcap import MCAPWriteOptions, MCAPWriter
7
26
  from .websocket import Capability, Service, WebSocketServer
8
27
 
@@ -15,7 +34,7 @@ class BaseChannel:
15
34
  self,
16
35
  topic: str,
17
36
  message_encoding: str,
18
- schema: Schema | None = None,
37
+ schema: "Schema" | None = None,
19
38
  metadata: dict[str, str] | None = None,
20
39
  ) -> None: ...
21
40
  def id(self) -> int:
@@ -115,6 +134,26 @@ class Context:
115
134
  """
116
135
  ...
117
136
 
137
+ @staticmethod
138
+ def default() -> "Context":
139
+ """
140
+ Returns the default context.
141
+ """
142
+ ...
143
+
144
+ class ChannelDescriptor:
145
+ """
146
+ Information about a channel
147
+ """
148
+
149
+ id: int
150
+ topic: str
151
+ message_encoding: str
152
+ metadata: dict[str, str]
153
+ schema: "Schema" | None
154
+
155
+ SinkChannelFilter = Callable[[ChannelDescriptor], bool]
156
+
118
157
  def start_server(
119
158
  *,
120
159
  name: str | None = None,
@@ -127,12 +166,26 @@ def start_server(
127
166
  asset_handler: AssetHandler | None = None,
128
167
  context: Context | None = None,
129
168
  session_id: str | None = None,
169
+ channel_filter: SinkChannelFilter | None = None,
170
+ playback_time_range: tuple[int, int] | None = None,
130
171
  ) -> WebSocketServer:
131
172
  """
132
173
  Start a websocket server for live visualization.
133
174
  """
134
175
  ...
135
176
 
177
+ def start_cloud_sink(
178
+ *,
179
+ listener: Any = None,
180
+ supported_encodings: list[str] | None = None,
181
+ context: Context | None = None,
182
+ session_id: str | None = None,
183
+ ) -> CloudSink:
184
+ """
185
+ Connect to Foxglove Agent for remote visualization and teleop.
186
+ """
187
+ ...
188
+
136
189
  def enable_logging(level: int) -> None:
137
190
  """
138
191
  Forward SDK logs to python's logging facility.
@@ -152,17 +205,24 @@ def shutdown() -> None:
152
205
  ...
153
206
 
154
207
  def open_mcap(
155
- path: str | Path,
208
+ path: str | Path | BinaryIO | McapWritable,
156
209
  *,
157
210
  allow_overwrite: bool = False,
158
211
  context: Context | None = None,
212
+ channel_filter: SinkChannelFilter | None = None,
159
213
  writer_options: MCAPWriteOptions | None = None,
160
214
  ) -> MCAPWriter:
161
215
  """
162
- Creates a new MCAP file for recording.
216
+ Open an MCAP writer for recording.
217
+
218
+ If a path is provided, the file will be created and must not already exist (unless
219
+ allow_overwrite is True). If a file-like object is provided, it must support write(),
220
+ seek(), and flush() methods; the allow_overwrite parameter is ignored.
163
221
 
164
222
  If a context is provided, the MCAP file will be associated with that context. Otherwise, the
165
223
  global context will be used.
224
+
225
+ You must close the writer with close() or the with statement to ensure the file is correctly finished.
166
226
  """
167
227
  ...
168
228
 
@@ -0,0 +1,9 @@
1
+ class CloudSink:
2
+ """
3
+ A cloud sink for remote visualization and teleop.
4
+ """
5
+
6
+ def __init__(self) -> None: ...
7
+ def stop(self) -> None:
8
+ """Gracefully disconnect from the cloud sink, waiting for shutdown to complete."""
9
+ ...
@@ -23,6 +23,10 @@ class MCAPWriteOptions:
23
23
  :param emit_message_indexes: Specifies whether to write message index records after each chunk.
24
24
  :param emit_chunk_indexes: Specifies whether to write chunk index records in the summary
25
25
  section.
26
+ :param disable_seeking: Specifies whether to disable seeking backwards/forwards when writing.
27
+ Use this when writing to a non-seekable file-like object (e.g. a wrapped pipe or network
28
+ socket). The seek() implementation must still support `seek(0, SEEK_CUR)` and
29
+ `seek(current_position, SEEK_SET)`.
26
30
  :param repeat_channels: Specifies whether to repeat each channel record from the data section
27
31
  in the summary section.
28
32
  :param repeat_schemas: Specifies whether to repeat each schema record from the data section in
@@ -45,6 +49,7 @@ class MCAPWriteOptions:
45
49
  emit_summary_offsets: bool = True,
46
50
  emit_message_indexes: bool = True,
47
51
  emit_chunk_indexes: bool = True,
52
+ disable_seeking: bool = False,
48
53
  repeat_channels: bool = True,
49
54
  repeat_schemas: bool = True,
50
55
  calculate_chunk_crcs: bool = True,
@@ -82,3 +87,39 @@ class MCAPWriter:
82
87
  exiting the context manager.
83
88
  """
84
89
  ...
90
+
91
+ def write_metadata(self, name: str, metadata: dict[str, str]) -> None:
92
+ """
93
+ Write metadata to the MCAP file.
94
+
95
+ Metadata consists of key-value string pairs associated with a name.
96
+ If the metadata dictionary is empty, this method does nothing.
97
+
98
+ :param name: Name identifier for this metadata record
99
+ :param metadata: Dictionary of key-value pairs to store
100
+ """
101
+ ...
102
+
103
+ def attach(
104
+ self,
105
+ *,
106
+ log_time: int,
107
+ create_time: int,
108
+ name: str,
109
+ media_type: str,
110
+ data: bytes,
111
+ ) -> None:
112
+ """
113
+ Write an attachment to the MCAP file.
114
+
115
+ Attachments are arbitrary binary data that can be stored alongside messages.
116
+ Common uses include storing configuration files, calibration data, or other
117
+ reference material related to the recording.
118
+
119
+ :param log_time: Time at which the attachment was logged, in nanoseconds since epoch.
120
+ :param create_time: Time at which the attachment data was created, in nanoseconds since epoch.
121
+ :param name: Name of the attachment (e.g., "config.json").
122
+ :param media_type: MIME type of the attachment (e.g., "application/json").
123
+ :param data: Binary content of the attachment.
124
+ """
125
+ ...
@@ -15,7 +15,7 @@ class Capability(Enum):
15
15
  ClientPublish = ...
16
16
  """Allow clients to advertise channels to send data messages to the server."""
17
17
 
18
- Connectiongraph = ...
18
+ ConnectionGraph = ...
19
19
  """Allow clients to subscribe and make connection graph updates"""
20
20
 
21
21
  Parameters = ...
@@ -27,6 +27,9 @@ class Capability(Enum):
27
27
  Time = ...
28
28
  """Inform clients about the latest server time."""
29
29
 
30
+ RangedPlayback = ...
31
+ """Indicates that the server is sending data within a fixed time range."""
32
+
30
33
  class Client:
31
34
  """
32
35
  A client that is connected to a running websocket server.
@@ -187,6 +190,70 @@ class ParameterValue:
187
190
 
188
191
  def __init__(self, value: dict[str, AnyParameterValue]) -> None: ...
189
192
 
193
+ class PlaybackCommand(Enum):
194
+ """The command for playback requested by the client player"""
195
+
196
+ Play = ...
197
+ Pause = ...
198
+
199
+ class PlaybackControlRequest:
200
+ """
201
+ A request to control playback from the client
202
+
203
+ :param playback_command: The command for playback requested by the client player
204
+ :type playback_command: PlaybackCommand
205
+ :param playback_speed: The speed of playback requested by the client player
206
+ :type playback_speed: float
207
+ :param seek_time: The time the client player is requesting to seek to, in nanoseconds. None if no seek is requested.
208
+ :type seek_time: int | None
209
+ :param request_id: Unique string identifier, used to indicate that a PlaybackState is in response to a particular request from the client.
210
+ :type request_id: str
211
+ """
212
+
213
+ playback_command: PlaybackCommand
214
+ playback_speed: float
215
+ seek_time: int | None
216
+ request_id: str
217
+
218
+ class PlaybackState:
219
+ """
220
+ The state of data playback on the server
221
+
222
+ :param status: The status of server data playback
223
+ :type status: PlaybackStatus
224
+ :param current_time: The current time of playback, in absolute nanoseconds
225
+ :type current_time: int
226
+ :param playback_speed: The speed of playback, as a factor of realtime
227
+ :type playback_speed: float
228
+ :param did_seek: Whether a seek forward or backward in time triggered this message to be emitted
229
+ :type did_seek: bool
230
+ :param request_id: If this message is being emitted in response to a PlaybackControlRequest message, the request_id from that message. Set this to an empty string if the state of playback has been changed by any other condition.
231
+ :type request_id: str | None
232
+ """
233
+
234
+ status: PlaybackStatus
235
+ current_time: int
236
+ playback_speed: float
237
+ did_seek: bool
238
+ request_id: str | None
239
+
240
+ def __init__(
241
+ self,
242
+ status: PlaybackStatus,
243
+ current_time: int,
244
+ playback_speed: float,
245
+ did_seek: bool,
246
+ request_id: str | None,
247
+ ): ...
248
+
249
+ class PlaybackStatus(Enum):
250
+ """The status of server data playback"""
251
+
252
+ Playing = ...
253
+ Paused = ...
254
+ Buffering = ...
255
+ Ended = ...
256
+
190
257
  class ServiceRequest:
191
258
  """
192
259
  A websocket service request.
@@ -278,6 +345,12 @@ class WebSocketServer:
278
345
  """
279
346
  ...
280
347
 
348
+ def broadcast_playback_state(self, playback_state: PlaybackState) -> None:
349
+ """
350
+ Publish the current playback state to all clients.
351
+ """
352
+ ...
353
+
281
354
  def broadcast_time(self, timestamp_nanos: int) -> None:
282
355
  """
283
356
  Publishes the current server timestamp to all clients.
foxglove/cloud.py ADDED
@@ -0,0 +1,61 @@
1
+ from typing import Protocol
2
+
3
+ from ._foxglove_py.websocket import (
4
+ ChannelView,
5
+ Client,
6
+ ClientChannel,
7
+ )
8
+
9
+
10
+ class CloudSinkListener(Protocol):
11
+ """
12
+ A mechanism to register callbacks for handling client message events.
13
+ """
14
+
15
+ def on_subscribe(self, client: Client, channel: ChannelView) -> None:
16
+ """
17
+ Called when a client subscribes to a channel.
18
+
19
+ :param client: The client (id) that sent the message.
20
+ :param channel: The channel (id, topic) that the message was sent on.
21
+ """
22
+ return None
23
+
24
+ def on_unsubscribe(self, client: Client, channel: ChannelView) -> None:
25
+ """
26
+ Called when a client unsubscribes from a channel or disconnects.
27
+
28
+ :param client: The client (id) that sent the message.
29
+ :param channel: The channel (id, topic) that the message was sent on.
30
+ """
31
+ return None
32
+
33
+ def on_client_advertise(self, client: Client, channel: ClientChannel) -> None:
34
+ """
35
+ Called when a client advertises a channel.
36
+
37
+ :param client: The client (id) that sent the message.
38
+ :param channel: The client channel that is being advertised.
39
+ """
40
+ return None
41
+
42
+ def on_client_unadvertise(self, client: Client, client_channel_id: int) -> None:
43
+ """
44
+ Called when a client unadvertises a channel.
45
+
46
+ :param client: The client (id) that is unadvertising the channel.
47
+ :param client_channel_id: The client channel id that is being unadvertised.
48
+ """
49
+ return None
50
+
51
+ def on_message_data(
52
+ self, client: Client, client_channel_id: int, data: bytes
53
+ ) -> None:
54
+ """
55
+ Called when a message is received from a client.
56
+
57
+ :param client: The client (id) that sent the message.
58
+ :param client_channel_id: The client channel id that the message was sent on.
59
+ :param data: The message data.
60
+ """
61
+ return None
File without changes
@@ -0,0 +1,100 @@
1
+ from __future__ import annotations
2
+
3
+ import pathlib
4
+ from typing import TYPE_CHECKING, Any, Literal
5
+
6
+ import anywidget
7
+ import traitlets
8
+
9
+ if TYPE_CHECKING:
10
+ from .notebook_buffer import NotebookBuffer
11
+
12
+
13
+ class FoxgloveWidget(anywidget.AnyWidget):
14
+ """
15
+ A widget that displays a Foxglove viewer in a notebook.
16
+
17
+ :param buffer: The NotebookBuffer object that contains the data to display in the widget.
18
+ :param layout_storage_key: The storage key of the layout to use for the widget.
19
+ :param width: The width of the widget. Defaults to "full".
20
+ :param height: The height of the widget in pixels. Defaults to 500.
21
+ :param src: The source URL of the Foxglove viewer. Defaults to "https://embed.foxglove.dev/".
22
+ """
23
+
24
+ _esm = pathlib.Path(__file__).parent / "static" / "widget.js"
25
+ width = traitlets.Union(
26
+ [traitlets.Int(), traitlets.Enum(values=["full"])], default_value="full"
27
+ ).tag(sync=True)
28
+ height = traitlets.Int(default_value=500).tag(sync=True)
29
+ src = traitlets.Unicode(default_value=None, allow_none=True).tag(sync=True)
30
+ _layout_params = traitlets.Dict(
31
+ per_key_traits={
32
+ "storage_key": traitlets.Unicode(),
33
+ "opaque_layout": traitlets.Dict(allow_none=True, default_value=None),
34
+ "force": traitlets.Bool(False),
35
+ },
36
+ allow_none=True,
37
+ default_value=None,
38
+ ).tag(sync=True)
39
+
40
+ def __init__(
41
+ self,
42
+ buffer: NotebookBuffer,
43
+ layout_storage_key: str,
44
+ width: int | Literal["full"] | None = None,
45
+ height: int | None = None,
46
+ src: str | None = None,
47
+ **kwargs: Any,
48
+ ):
49
+ super().__init__(**kwargs)
50
+ if width is not None:
51
+ self.width = width
52
+ else:
53
+ self.width = "full"
54
+ if height is not None:
55
+ self.height = height
56
+ if src is not None:
57
+ self.src = src
58
+
59
+ self.select_layout(layout_storage_key, **kwargs)
60
+
61
+ # Callback to get the data to display in the widget
62
+ self._buffer = buffer
63
+ # Keep track of when the widget is ready to receive data
64
+ self._ready = False
65
+ # Pending data to be sent when the widget is ready
66
+ self._pending_data: list[bytes] = []
67
+ self.on_msg(self._handle_custom_msg)
68
+ self.refresh()
69
+
70
+ def select_layout(self, storage_key: str, **kwargs: Any) -> None:
71
+ """
72
+ Select a layout in the Foxglove viewer.
73
+ """
74
+ opaque_layout = kwargs.get("opaque_layout", None)
75
+ force_layout = kwargs.get("force_layout", False)
76
+
77
+ self._layout_params = {
78
+ "storage_key": storage_key,
79
+ "opaque_layout": opaque_layout if isinstance(opaque_layout, dict) else None,
80
+ "force": force_layout,
81
+ }
82
+
83
+ def refresh(self) -> None:
84
+ """
85
+ Refresh the widget by getting the data from the callback function and sending it
86
+ to the widget.
87
+ """
88
+ data = self._buffer.get_data()
89
+ if not self._ready:
90
+ self._pending_data = data
91
+ else:
92
+ self.send({"type": "update-data"}, data)
93
+
94
+ def _handle_custom_msg(self, msg: dict, buffers: list[bytes]) -> None:
95
+ if msg["type"] == "ready":
96
+ self._ready = True
97
+
98
+ if len(self._pending_data) > 0:
99
+ self.send({"type": "update-data"}, self._pending_data)
100
+ self._pending_data = []