foxglove-sdk 0.16.2__cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.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.

@@ -0,0 +1,85 @@
1
+ import datetime
2
+
3
+ class Duration:
4
+ """
5
+ A duration in seconds and nanoseconds
6
+ """
7
+
8
+ def __init__(
9
+ self,
10
+ sec: int,
11
+ nsec: int | None = None,
12
+ ) -> None: ...
13
+ @property
14
+ def sec(self) -> int: ...
15
+ @property
16
+ def nsec(self) -> int: ...
17
+ @staticmethod
18
+ def from_secs(secs: float) -> "Duration":
19
+ """
20
+ Creates a :py:class:`Duration` from seconds.
21
+
22
+ Raises `OverflowError` if the duration cannot be represented.
23
+
24
+ :param secs: Seconds
25
+ """
26
+ ...
27
+
28
+ @staticmethod
29
+ def from_timedelta(td: datetime.timedelta) -> "Duration":
30
+ """
31
+ Creates a :py:class:`Duration` from a timedelta.
32
+
33
+ Raises `OverflowError` if the duration cannot be represented.
34
+
35
+ :param td: Timedelta
36
+ """
37
+ ...
38
+
39
+ class Timestamp:
40
+ """
41
+ A timestamp in seconds and nanoseconds
42
+ """
43
+
44
+ def __init__(
45
+ self,
46
+ sec: int,
47
+ nsec: int | None = None,
48
+ ) -> None: ...
49
+ @property
50
+ def sec(self) -> int: ...
51
+ @property
52
+ def nsec(self) -> int: ...
53
+ @staticmethod
54
+ def from_epoch_secs(timestamp: float) -> "Timestamp":
55
+ """
56
+ Creates a :py:class:`Timestamp` from an epoch timestamp, such as is
57
+ returned by :py:func:`time.time` or :py:func:`datetime.datetime.timestamp`.
58
+
59
+ Raises `OverflowError` if the timestamp cannot be represented.
60
+
61
+ :param timestamp: Seconds since epoch
62
+ """
63
+ ...
64
+
65
+ @staticmethod
66
+ def from_datetime(dt: datetime.datetime) -> "Timestamp":
67
+ """
68
+ Creates a UNIX epoch :py:class:`Timestamp` from a datetime object.
69
+
70
+ Naive datetime objects are presumed to be in the local timezone.
71
+
72
+ Raises `OverflowError` if the timestamp cannot be represented.
73
+
74
+ :param dt: Datetime
75
+ """
76
+ ...
77
+
78
+ @staticmethod
79
+ def now() -> "Timestamp":
80
+ """
81
+ Creates a :py:class:`Timestamp` from the current system time.
82
+
83
+ Raises `OverflowError` if the timestamp cannot be represented.
84
+ """
85
+ ...
@@ -0,0 +1,394 @@
1
+ from enum import Enum
2
+
3
+ from foxglove import Schema
4
+ from foxglove.websocket import (
5
+ AnyNativeParameterValue,
6
+ AnyParameterValue,
7
+ ServiceHandler,
8
+ )
9
+
10
+ class Capability(Enum):
11
+ """
12
+ An enumeration of capabilities that the websocket server can advertise to its clients.
13
+ """
14
+
15
+ ClientPublish = ...
16
+ """Allow clients to advertise channels to send data messages to the server."""
17
+
18
+ ConnectionGraph = ...
19
+ """Allow clients to subscribe and make connection graph updates"""
20
+
21
+ Parameters = ...
22
+ """Allow clients to get & set parameters."""
23
+
24
+ Services = ...
25
+ """Allow clients to call services."""
26
+
27
+ Time = ...
28
+ """Inform clients about the latest server time."""
29
+
30
+ RangedPlayback = ...
31
+ """Indicates that the server is sending data within a fixed time range."""
32
+
33
+ class Client:
34
+ """
35
+ A client that is connected to a running websocket server.
36
+ """
37
+
38
+ id: int = ...
39
+
40
+ class ChannelView:
41
+ """
42
+ Information about a channel.
43
+ """
44
+
45
+ id: int = ...
46
+ topic: str = ...
47
+
48
+ class ClientChannel:
49
+ """
50
+ Information about a channel advertised by a client.
51
+ """
52
+
53
+ id: int = ...
54
+ topic: str = ...
55
+ encoding: str = ...
56
+ schema_name: str = ...
57
+ schema_encoding: str | None = ...
58
+ schema: bytes | None = ...
59
+
60
+ class ConnectionGraph:
61
+ """
62
+ A graph of connections between clients.
63
+ """
64
+
65
+ def __init__(self) -> None: ...
66
+ def set_published_topic(self, topic: str, publisher_ids: list[str]) -> None:
67
+ """
68
+ Set a published topic and its associated publisher ids. Overwrites any existing topic with
69
+ the same name.
70
+
71
+ :param topic: The topic name.
72
+ :param publisher_ids: The set of publisher ids.
73
+ """
74
+ ...
75
+
76
+ def set_subscribed_topic(self, topic: str, subscriber_ids: list[str]) -> None:
77
+ """
78
+ Set a subscribed topic and its associated subscriber ids. Overwrites any existing topic with
79
+ the same name.
80
+
81
+ :param topic: The topic name.
82
+ :param subscriber_ids: The set of subscriber ids.
83
+ """
84
+ ...
85
+
86
+ def set_advertised_service(self, service: str, provider_ids: list[str]) -> None:
87
+ """
88
+ Set an advertised service and its associated provider ids Overwrites any existing service
89
+ with the same name.
90
+
91
+ :param service: The service name.
92
+ :param provider_ids: The set of provider ids.
93
+ """
94
+ ...
95
+
96
+ class MessageSchema:
97
+ """
98
+ A service request or response schema.
99
+ """
100
+
101
+ encoding: str
102
+ schema: Schema
103
+
104
+ def __init__(
105
+ self,
106
+ *,
107
+ encoding: str,
108
+ schema: Schema,
109
+ ) -> None: ...
110
+
111
+ class Parameter:
112
+ """
113
+ A parameter which can be sent to a client.
114
+
115
+ :param name: The parameter name.
116
+ :type name: str
117
+ :param value: Optional value, represented as a native python object, or a ParameterValue.
118
+ :type value: None|bool|int|float|str|bytes|list|dict|ParameterValue
119
+ :param type: Optional parameter type. This is automatically derived when passing a native
120
+ python object as the value.
121
+ :type type: ParameterType|None
122
+ """
123
+
124
+ name: str
125
+ type: ParameterType | None
126
+ value: AnyParameterValue | None
127
+
128
+ def __init__(
129
+ self,
130
+ name: str,
131
+ *,
132
+ value: AnyNativeParameterValue | None = None,
133
+ type: ParameterType | None = None,
134
+ ) -> None: ...
135
+ def get_value(self) -> AnyNativeParameterValue | None:
136
+ """Returns the parameter value as a native python object."""
137
+ ...
138
+
139
+ class ParameterType(Enum):
140
+ """
141
+ The type of a parameter.
142
+ """
143
+
144
+ ByteArray = ...
145
+ """A byte array."""
146
+
147
+ Float64 = ...
148
+ """A floating-point value that can be represented as a `float64`."""
149
+
150
+ Float64Array = ...
151
+ """An array of floating-point values that can be represented as `float64`s."""
152
+
153
+ class ParameterValue:
154
+ """
155
+ A parameter value.
156
+ """
157
+
158
+ class Integer:
159
+ """An integer value."""
160
+
161
+ def __init__(self, value: int) -> None: ...
162
+
163
+ class Bool:
164
+ """A boolean value."""
165
+
166
+ def __init__(self, value: bool) -> None: ...
167
+
168
+ class Float64:
169
+ """A floating-point value."""
170
+
171
+ def __init__(self, value: float) -> None: ...
172
+
173
+ class String:
174
+ """
175
+ A string value.
176
+
177
+ For parameters of type :py:attr:ParameterType.ByteArray, this is a
178
+ base64 encoding of the byte array.
179
+ """
180
+
181
+ def __init__(self, value: str) -> None: ...
182
+
183
+ class Array:
184
+ """An array of parameter values."""
185
+
186
+ def __init__(self, value: list[AnyParameterValue]) -> None: ...
187
+
188
+ class Dict:
189
+ """An associative map of parameter values."""
190
+
191
+ def __init__(self, value: dict[str, AnyParameterValue]) -> None: ...
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
+
257
+ class ServiceRequest:
258
+ """
259
+ A websocket service request.
260
+ """
261
+
262
+ service_name: str
263
+ client_id: int
264
+ call_id: int
265
+ encoding: str
266
+ payload: bytes
267
+
268
+ class Service:
269
+ """
270
+ A websocket service.
271
+ """
272
+
273
+ name: str
274
+ schema: ServiceSchema
275
+ handler: ServiceHandler
276
+
277
+ def __init__(
278
+ self,
279
+ *,
280
+ name: str,
281
+ schema: ServiceSchema,
282
+ handler: ServiceHandler,
283
+ ): ...
284
+
285
+ class ServiceSchema:
286
+ """
287
+ A websocket service schema.
288
+ """
289
+
290
+ name: str
291
+ request: MessageSchema | None
292
+ response: MessageSchema | None
293
+
294
+ def __init__(
295
+ self,
296
+ *,
297
+ name: str,
298
+ request: MessageSchema | None = None,
299
+ response: MessageSchema | None = None,
300
+ ): ...
301
+
302
+ class StatusLevel(Enum):
303
+ """A level for `WebSocketServer.publish_status`"""
304
+
305
+ Info = ...
306
+ Warning = ...
307
+ Error = ...
308
+
309
+ class WebSocketServer:
310
+ """
311
+ A websocket server for live visualization.
312
+ """
313
+
314
+ def __init__(self) -> None: ...
315
+ @property
316
+ def port(self) -> int:
317
+ """Get the port on which the server is listening."""
318
+ ...
319
+
320
+ def app_url(
321
+ self,
322
+ *,
323
+ layout_id: str | None = None,
324
+ open_in_desktop: bool = False,
325
+ ) -> str | None:
326
+ """
327
+ Returns a web app URL to open the websocket as a data source.
328
+
329
+ Returns None if the server has been stopped.
330
+
331
+ :param layout_id: An optional layout ID to include in the URL.
332
+ :param open_in_desktop: Opens the foxglove desktop app.
333
+ """
334
+ ...
335
+
336
+ def stop(self) -> None:
337
+ """Explicitly stop the server."""
338
+ ...
339
+
340
+ def clear_session(self, session_id: str | None = None) -> None:
341
+ """
342
+ Sets a new session ID and notifies all clients, causing them to reset their state.
343
+ If no session ID is provided, generates a new one based on the current timestamp.
344
+ If the server has been stopped, this has no effect.
345
+ """
346
+ ...
347
+
348
+ def broadcast_playback_state(self, playback_state: PlaybackState) -> None:
349
+ """
350
+ Publish the current playback state to all clients.
351
+ """
352
+ ...
353
+
354
+ def broadcast_time(self, timestamp_nanos: int) -> None:
355
+ """
356
+ Publishes the current server timestamp to all clients.
357
+ If the server has been stopped, this has no effect.
358
+ """
359
+ ...
360
+
361
+ def publish_parameter_values(self, parameters: list[Parameter]) -> None:
362
+ """Publishes parameter values to all subscribed clients."""
363
+ ...
364
+
365
+ def publish_status(
366
+ self, message: str, level: StatusLevel, id: str | None = None
367
+ ) -> None:
368
+ """
369
+ Send a status message to all clients. If the server has been stopped, this has no effect.
370
+ """
371
+ ...
372
+
373
+ def remove_status(self, ids: list[str]) -> None:
374
+ """
375
+ Remove status messages by id from all clients. If the server has been stopped, this has no
376
+ effect.
377
+ """
378
+ ...
379
+
380
+ def add_services(self, services: list[Service]) -> None:
381
+ """Add services to the server."""
382
+ ...
383
+
384
+ def remove_services(self, names: list[str]) -> None:
385
+ """Removes services that were previously advertised."""
386
+ ...
387
+
388
+ def publish_connection_graph(self, graph: ConnectionGraph) -> None:
389
+ """
390
+ Publishes a connection graph update to all subscribed clients. An update is published to
391
+ clients as a difference from the current graph to the replacement graph. When a client first
392
+ subscribes to connection graph updates, it receives the current graph.
393
+ """
394
+ ...
@@ -0,0 +1,160 @@
1
+ import json
2
+ import math
3
+ import os
4
+ import struct
5
+ import time
6
+ from pathlib import Path
7
+ from typing import Generator, List
8
+
9
+ import pytest
10
+ from foxglove import Channel, open_mcap
11
+ from foxglove.channels import PointCloudChannel, SceneUpdateChannel
12
+ from foxglove.schemas import (
13
+ Color,
14
+ CubePrimitive,
15
+ Duration,
16
+ PackedElementField,
17
+ PackedElementFieldNumericType,
18
+ PointCloud,
19
+ Pose,
20
+ Quaternion,
21
+ SceneEntity,
22
+ SceneUpdate,
23
+ Vector3,
24
+ )
25
+ from pytest_benchmark.fixture import BenchmarkFixture # type: ignore
26
+
27
+
28
+ @pytest.fixture
29
+ def tmp_mcap(tmpdir: os.PathLike[str]) -> Generator[Path, None, None]:
30
+ dir = Path(tmpdir)
31
+ mcap = dir / "test.mcap"
32
+ yield mcap
33
+ mcap.unlink()
34
+ dir.rmdir()
35
+
36
+
37
+ def build_entities(entity_count: int) -> List[SceneEntity]:
38
+ assert entity_count > 0
39
+ return [
40
+ SceneEntity(
41
+ id=f"box_{i}",
42
+ frame_id="box",
43
+ lifetime=Duration(10, nsec=int(100 * 1e6)),
44
+ cubes=[
45
+ CubePrimitive(
46
+ pose=Pose(
47
+ position=Vector3(x=0.0, y=0.0, z=3.0),
48
+ orientation=Quaternion(x=0.0, y=0.0, z=0.0, w=1.0),
49
+ ),
50
+ size=Vector3(x=1.0, y=1.0, z=1.0),
51
+ color=Color(r=1.0, g=0.0, b=0.0, a=1.0),
52
+ )
53
+ ],
54
+ )
55
+ for i in range(entity_count)
56
+ ]
57
+
58
+
59
+ def write_scene_entity_mcap(
60
+ tmp_mcap: Path, channel: SceneUpdateChannel, entities: List[SceneEntity]
61
+ ) -> None:
62
+ with open_mcap(tmp_mcap, allow_overwrite=True):
63
+ for _ in range(100):
64
+ channel.log(SceneUpdate(entities=entities))
65
+
66
+
67
+ def make_point_cloud(point_count: int) -> PointCloud:
68
+ """
69
+ https://foxglove.dev/blog/visualizing-point-clouds-with-custom-colors
70
+ """
71
+ point_struct = struct.Struct("<fffBBBB")
72
+ f32 = PackedElementFieldNumericType.Float32
73
+ u32 = PackedElementFieldNumericType.Uint32
74
+
75
+ t = time.time()
76
+ count = math.ceil(math.sqrt(point_count))
77
+ points = [
78
+ (x + math.cos(t + y / 5), y, 0) for x in range(count) for y in range(count)
79
+ ]
80
+
81
+ buffer = bytearray(point_struct.size * len(points))
82
+ for i, point in enumerate(points):
83
+ x, y, z = point
84
+ r = g = b = a = 128
85
+ point_struct.pack_into(buffer, i * point_struct.size, x, y, z, b, g, r, a)
86
+
87
+ return PointCloud(
88
+ frame_id="points",
89
+ pose=Pose(
90
+ position=Vector3(x=0, y=0, z=0),
91
+ orientation=Quaternion(x=0, y=0, z=0, w=1),
92
+ ),
93
+ point_stride=16, # 4 fields * 4 bytes
94
+ fields=[
95
+ PackedElementField(name="x", offset=0, type=f32),
96
+ PackedElementField(name="y", offset=4, type=f32),
97
+ PackedElementField(name="z", offset=8, type=f32),
98
+ PackedElementField(name="rgba", offset=12, type=u32),
99
+ ],
100
+ data=bytes(buffer),
101
+ )
102
+
103
+
104
+ def write_point_cloud_mcap(
105
+ tmp_mcap: Path, channel: PointCloudChannel, point_cloud: PointCloud
106
+ ) -> None:
107
+ with open_mcap(tmp_mcap, allow_overwrite=True):
108
+ for _ in range(10):
109
+ channel.log(point_cloud)
110
+
111
+
112
+ def write_untyped_channel_mcap(
113
+ tmp_mcap: Path, channel: Channel, messages: List[bytes]
114
+ ) -> None:
115
+ with open_mcap(tmp_mcap, allow_overwrite=True):
116
+ for message in messages:
117
+ channel.log(message)
118
+
119
+
120
+ @pytest.mark.benchmark
121
+ @pytest.mark.parametrize("entity_count", [1, 2, 4, 8])
122
+ def test_write_scene_update_mcap(
123
+ benchmark: BenchmarkFixture,
124
+ entity_count: int,
125
+ tmp_mcap: Path,
126
+ ) -> None:
127
+ channel = SceneUpdateChannel(f"/scene_{entity_count}")
128
+ entities = build_entities(entity_count)
129
+ benchmark(write_scene_entity_mcap, tmp_mcap, channel, entities)
130
+
131
+
132
+ @pytest.mark.benchmark
133
+ @pytest.mark.parametrize("point_count", [100, 1000, 10000])
134
+ def test_write_point_cloud_mcap(
135
+ benchmark: BenchmarkFixture,
136
+ point_count: int,
137
+ tmp_mcap: Path,
138
+ ) -> None:
139
+ print("test_write_point_cloud_mcap")
140
+ channel = PointCloudChannel(f"/point_cloud_{point_count}")
141
+ point_cloud = make_point_cloud(point_count)
142
+ benchmark(write_point_cloud_mcap, tmp_mcap, channel, point_cloud)
143
+
144
+
145
+ @pytest.mark.benchmark
146
+ @pytest.mark.parametrize("message_count", [10, 100, 1000])
147
+ def test_write_untyped_channel_mcap(
148
+ benchmark: BenchmarkFixture,
149
+ message_count: int,
150
+ tmp_mcap: Path,
151
+ ) -> None:
152
+ channel = Channel(
153
+ f"/untyped_{message_count}",
154
+ schema={"type": "object", "additionalProperties": True},
155
+ )
156
+ messages = [
157
+ json.dumps({"message": f"hello_{i}"}).encode("utf-8")
158
+ for i in range(message_count)
159
+ ]
160
+ benchmark(write_untyped_channel_mcap, tmp_mcap, channel, messages)