foxglove-sdk 0.15.3__cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.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,321 @@
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
+ class Client:
31
+ """
32
+ A client that is connected to a running websocket server.
33
+ """
34
+
35
+ id: int = ...
36
+
37
+ class ChannelView:
38
+ """
39
+ Information about a channel.
40
+ """
41
+
42
+ id: int = ...
43
+ topic: str = ...
44
+
45
+ class ClientChannel:
46
+ """
47
+ Information about a channel advertised by a client.
48
+ """
49
+
50
+ id: int = ...
51
+ topic: str = ...
52
+ encoding: str = ...
53
+ schema_name: str = ...
54
+ schema_encoding: str | None = ...
55
+ schema: bytes | None = ...
56
+
57
+ class ConnectionGraph:
58
+ """
59
+ A graph of connections between clients.
60
+ """
61
+
62
+ def __init__(self) -> None: ...
63
+ def set_published_topic(self, topic: str, publisher_ids: list[str]) -> None:
64
+ """
65
+ Set a published topic and its associated publisher ids. Overwrites any existing topic with
66
+ the same name.
67
+
68
+ :param topic: The topic name.
69
+ :param publisher_ids: The set of publisher ids.
70
+ """
71
+ ...
72
+
73
+ def set_subscribed_topic(self, topic: str, subscriber_ids: list[str]) -> None:
74
+ """
75
+ Set a subscribed topic and its associated subscriber ids. Overwrites any existing topic with
76
+ the same name.
77
+
78
+ :param topic: The topic name.
79
+ :param subscriber_ids: The set of subscriber ids.
80
+ """
81
+ ...
82
+
83
+ def set_advertised_service(self, service: str, provider_ids: list[str]) -> None:
84
+ """
85
+ Set an advertised service and its associated provider ids Overwrites any existing service
86
+ with the same name.
87
+
88
+ :param service: The service name.
89
+ :param provider_ids: The set of provider ids.
90
+ """
91
+ ...
92
+
93
+ class MessageSchema:
94
+ """
95
+ A service request or response schema.
96
+ """
97
+
98
+ encoding: str
99
+ schema: Schema
100
+
101
+ def __init__(
102
+ self,
103
+ *,
104
+ encoding: str,
105
+ schema: Schema,
106
+ ) -> None: ...
107
+
108
+ class Parameter:
109
+ """
110
+ A parameter which can be sent to a client.
111
+
112
+ :param name: The parameter name.
113
+ :type name: str
114
+ :param value: Optional value, represented as a native python object, or a ParameterValue.
115
+ :type value: None|bool|int|float|str|bytes|list|dict|ParameterValue
116
+ :param type: Optional parameter type. This is automatically derived when passing a native
117
+ python object as the value.
118
+ :type type: ParameterType|None
119
+ """
120
+
121
+ name: str
122
+ type: ParameterType | None
123
+ value: AnyParameterValue | None
124
+
125
+ def __init__(
126
+ self,
127
+ name: str,
128
+ *,
129
+ value: AnyNativeParameterValue | None = None,
130
+ type: ParameterType | None = None,
131
+ ) -> None: ...
132
+ def get_value(self) -> AnyNativeParameterValue | None:
133
+ """Returns the parameter value as a native python object."""
134
+ ...
135
+
136
+ class ParameterType(Enum):
137
+ """
138
+ The type of a parameter.
139
+ """
140
+
141
+ ByteArray = ...
142
+ """A byte array."""
143
+
144
+ Float64 = ...
145
+ """A floating-point value that can be represented as a `float64`."""
146
+
147
+ Float64Array = ...
148
+ """An array of floating-point values that can be represented as `float64`s."""
149
+
150
+ class ParameterValue:
151
+ """
152
+ A parameter value.
153
+ """
154
+
155
+ class Integer:
156
+ """An integer value."""
157
+
158
+ def __init__(self, value: int) -> None: ...
159
+
160
+ class Bool:
161
+ """A boolean value."""
162
+
163
+ def __init__(self, value: bool) -> None: ...
164
+
165
+ class Float64:
166
+ """A floating-point value."""
167
+
168
+ def __init__(self, value: float) -> None: ...
169
+
170
+ class String:
171
+ """
172
+ A string value.
173
+
174
+ For parameters of type :py:attr:ParameterType.ByteArray, this is a
175
+ base64 encoding of the byte array.
176
+ """
177
+
178
+ def __init__(self, value: str) -> None: ...
179
+
180
+ class Array:
181
+ """An array of parameter values."""
182
+
183
+ def __init__(self, value: list[AnyParameterValue]) -> None: ...
184
+
185
+ class Dict:
186
+ """An associative map of parameter values."""
187
+
188
+ def __init__(self, value: dict[str, AnyParameterValue]) -> None: ...
189
+
190
+ class ServiceRequest:
191
+ """
192
+ A websocket service request.
193
+ """
194
+
195
+ service_name: str
196
+ client_id: int
197
+ call_id: int
198
+ encoding: str
199
+ payload: bytes
200
+
201
+ class Service:
202
+ """
203
+ A websocket service.
204
+ """
205
+
206
+ name: str
207
+ schema: ServiceSchema
208
+ handler: ServiceHandler
209
+
210
+ def __init__(
211
+ self,
212
+ *,
213
+ name: str,
214
+ schema: ServiceSchema,
215
+ handler: ServiceHandler,
216
+ ): ...
217
+
218
+ class ServiceSchema:
219
+ """
220
+ A websocket service schema.
221
+ """
222
+
223
+ name: str
224
+ request: MessageSchema | None
225
+ response: MessageSchema | None
226
+
227
+ def __init__(
228
+ self,
229
+ *,
230
+ name: str,
231
+ request: MessageSchema | None = None,
232
+ response: MessageSchema | None = None,
233
+ ): ...
234
+
235
+ class StatusLevel(Enum):
236
+ """A level for `WebSocketServer.publish_status`"""
237
+
238
+ Info = ...
239
+ Warning = ...
240
+ Error = ...
241
+
242
+ class WebSocketServer:
243
+ """
244
+ A websocket server for live visualization.
245
+ """
246
+
247
+ def __init__(self) -> None: ...
248
+ @property
249
+ def port(self) -> int:
250
+ """Get the port on which the server is listening."""
251
+ ...
252
+
253
+ def app_url(
254
+ self,
255
+ *,
256
+ layout_id: str | None = None,
257
+ open_in_desktop: bool = False,
258
+ ) -> str | None:
259
+ """
260
+ Returns a web app URL to open the websocket as a data source.
261
+
262
+ Returns None if the server has been stopped.
263
+
264
+ :param layout_id: An optional layout ID to include in the URL.
265
+ :param open_in_desktop: Opens the foxglove desktop app.
266
+ """
267
+ ...
268
+
269
+ def stop(self) -> None:
270
+ """Explicitly stop the server."""
271
+ ...
272
+
273
+ def clear_session(self, session_id: str | None = None) -> None:
274
+ """
275
+ Sets a new session ID and notifies all clients, causing them to reset their state.
276
+ If no session ID is provided, generates a new one based on the current timestamp.
277
+ If the server has been stopped, this has no effect.
278
+ """
279
+ ...
280
+
281
+ def broadcast_time(self, timestamp_nanos: int) -> None:
282
+ """
283
+ Publishes the current server timestamp to all clients.
284
+ If the server has been stopped, this has no effect.
285
+ """
286
+ ...
287
+
288
+ def publish_parameter_values(self, parameters: list[Parameter]) -> None:
289
+ """Publishes parameter values to all subscribed clients."""
290
+ ...
291
+
292
+ def publish_status(
293
+ self, message: str, level: StatusLevel, id: str | None = None
294
+ ) -> None:
295
+ """
296
+ Send a status message to all clients. If the server has been stopped, this has no effect.
297
+ """
298
+ ...
299
+
300
+ def remove_status(self, ids: list[str]) -> None:
301
+ """
302
+ Remove status messages by id from all clients. If the server has been stopped, this has no
303
+ effect.
304
+ """
305
+ ...
306
+
307
+ def add_services(self, services: list[Service]) -> None:
308
+ """Add services to the server."""
309
+ ...
310
+
311
+ def remove_services(self, names: list[str]) -> None:
312
+ """Removes services that were previously advertised."""
313
+ ...
314
+
315
+ def publish_connection_graph(self, graph: ConnectionGraph) -> None:
316
+ """
317
+ Publishes a connection graph update to all subscribed clients. An update is published to
318
+ clients as a difference from the current graph to the replacement graph. When a client first
319
+ subscribes to connection graph updates, it receives the current graph.
320
+ """
321
+ ...
@@ -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)