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 +111 -1
- foxglove/_foxglove_py/__init__.pyi +64 -4
- foxglove/_foxglove_py/cloud.pyi +9 -0
- foxglove/_foxglove_py/mcap.pyi +41 -0
- foxglove/_foxglove_py/websocket.pyi +74 -1
- foxglove/_foxglove_py.cpython-310-s390x-linux-gnu.so +0 -0
- foxglove/cloud.py +61 -0
- foxglove/notebook/__init__.py +0 -0
- foxglove/notebook/foxglove_widget.py +100 -0
- foxglove/notebook/notebook_buffer.py +114 -0
- foxglove/tests/test_context.py +10 -0
- foxglove/tests/test_logging.py +46 -0
- foxglove/tests/test_mcap.py +363 -2
- foxglove/tests/test_server.py +33 -2
- foxglove/websocket.py +22 -7
- foxglove_sdk-0.16.3.dist-info/METADATA +53 -0
- foxglove_sdk-0.16.3.dist-info/RECORD +32 -0
- foxglove_sdk-0.16.3.dist-info/WHEEL +5 -0
- foxglove_sdk-0.14.2.dist-info/METADATA +0 -29
- foxglove_sdk-0.14.2.dist-info/RECORD +0 -26
- foxglove_sdk-0.14.2.dist-info/WHEEL +0 -4
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
|
|
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
|
-
|
|
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
|
|
foxglove/_foxglove_py/mcap.pyi
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
Binary file
|
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 = []
|