foxglove 0.20.0

Foxglove SDK
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
//! The official [Foxglove] SDK.
//!
//! This crate provides support for integrating with the Foxglove platform. It can be used to log
//! events to local [MCAP] files or a local visualization server that communicates with the Foxglove
//! app.
//!
//! [Foxglove]: https://docs.foxglove.dev/
//! [MCAP]: https://mcap.dev/
//!
//! # Getting started
//!
//! The easiest way to get started is to install the `foxglove` crate with default features, which
//! will allow logging messages to the Foxglove app and to an MCAP file.
//!
//! ```bash
//! cargo add foxglove
//! ```
//!
//! The following sections illustrate how to use the SDK. For a more hands-on walk-through, see
//! <https://docs.foxglove.dev/docs/sdk/example>.
//!
//! # Recording messages
//!
//! To record messages, you need to initialize either an MCAP file writer or a WebSocket server for
//! live visualization. In this example, we create an MCAP writer, and record a
//! [`Log`](`crate::schemas::Log`) message on a topic called `/log`. We write one log message and
//! close the file.
//!
//! ```no_run
//! use foxglove::schemas::Log;
//! use foxglove::{log, McapWriter};
//!
//! // Create a new MCAP file named 'test.mcap'.
//! let mcap = McapWriter::new()
//!     .create_new_buffered_file("test.mcap")
//!     .expect("create failed");
//!
//! log!(
//!     "/log",
//!     Log {
//!         message: "Hello, Foxglove!".to_string(),
//!         ..Default::default()
//!     }
//! );
//!
//! // Flush and close the MCAP file.
//! mcap.close().expect("close failed");
//! ```
//!
//! # Concepts
//!
//! ## Context
//!
//! A [`Context`] is the binding between channels and sinks. Each channel and sink belongs to
//! exactly one context. Sinks receive advertisements about channels on the context, and can
//! optionally subscribe to receive logged messages on those channels.
//!
//! When the context goes out of scope, its corresponding channels and sinks will be disconnected
//! from one another, and logging will stop. Attempts to log further messages on the channels will
//! elicit throttled warning messages.
//!
//! Since many applications only need a single context, the SDK provides a static default context
//! for convenience. This default context is the one used in the [example above](#getting-started).
//! If we wanted to use an explicit context instead, we'd write:
//!
//! ```no_run
//! use foxglove::schemas::Log;
//! use foxglove::Context;
//!
//! // Create a new context.
//! let ctx = Context::new();
//!
//! // Create a new MCAP file named 'test.mcap'.
//! let mcap = ctx
//!     .mcap_writer()
//!     .create_new_buffered_file("test.mcap")
//!     .expect("create failed");
//!
//! // Create a new channel for the topic "/log" for `Log` messages.
//! let channel = ctx.channel_builder("/log").build();
//! channel.log(&Log {
//!     message: "Hello, Foxglove!".to_string(),
//!     ..Default::default()
//! });
//!
//! // Flush and close the MCAP file.
//! mcap.close().expect("close failed");
//! ```
//!
//! ## Channels
//!
//! A [`Channel`] gives a way to log related messages which have the same type, or [`Schema`]. Each
//! channel is instantiated with a unique "topic", or name, which is typically prefixed by a `/`. If
//! you're familiar with MCAP, it's the same concept as an [MCAP channel].
//!
//! A channel is always associated with exactly one [`Context`] throughout its lifecycle. The
//! channel remains attached to the context until it is either explicitly closed with
//! [`Channel::close`], or the context is dropped. Attempting to log a message on a closed channel
//! will elicit a throttled warning.
//!
//! [MCAP channel]: https://mcap.dev/guides/concepts#channel
//!
//! In the [example above](#getting-started), `log!` creates a `Channel<Log>` behind the scenes on
//! the first call. The example could be equivalently written as:
//!
//! ```no_run
//! use foxglove::schemas::Log;
//! use foxglove::{Channel, McapWriter};
//!
//! // Create a new MCAP file named 'test.mcap'.
//! let mcap = McapWriter::new()
//!     .create_new_buffered_file("test.mcap")
//!     .expect("create failed");
//!
//! // Create a new channel for the topic "/log" for `Log` messages.
//! let channel = Channel::new("/log");
//! channel.log(&Log {
//!     message: "Hello, Foxglove!".to_string(),
//!     ..Default::default()
//! });
//!
//! // Flush and close the MCAP file.
//! mcap.close().expect("close failed");
//! ```
//!
//! `log!` can be mixed and matched with manually created channels in the default [`Context`], as
//! long as the types are exactly the same.
//!
//! ### Well-known types
//!
//! The SDK provides [structs for well-known message types](schemas). These can be used in conjunction
//! with [`Channel`] for type-safe logging, which ensures at compile time that messages logged to a
//! channel all share a common schema.
//!
//! ### Custom data
//!
//! You can also define your own custom data types by implementing the [`Encode`] trait.
//!
//! The easiest way to do this is to enable the `derive` feature and derive the [`Encode`] trait,
//! which will generate a schema and allow you to log your struct to a channel. The underlying
//! serialization format is an implementation detail and may change across SDK versions.
//!
//! ```no_run
//! # #[cfg(feature = "derive")]
//! # {
//! #[derive(foxglove::Encode)]
//! struct Custom<'a> {
//!     msg: &'a str,
//!     count: u32,
//! }
//!
//! let channel = foxglove::Channel::new("/custom");
//! channel.log(&Custom {
//!     msg: "custom",
//!     count: 42,
//! });
//! # }
//! ```
//!
//! **Note:** The `Encode` derive macro is a convenience for getting data into Foxglove with
//! minimal friction. The generated schema is not designed for evolution — reordering, inserting,
//! or removing fields will silently break compatibility with previously recorded data. If you need
//! backwards-compatible schema evolution, maintain an explicit `.proto` file and implement
//! [`Encode`] manually for your generated types. See
//! [logging with protobuf schemas](https://docs.foxglove.dev/docs/sdk/logging-messages#logging-with-protobuf-schemas)
//! for an example.
//!
//! If you'd like to use JSON encoding for integration with particular tooling, you can enable the
//! `schemars` feature, which will provide a blanket [`Encode`] implementation for types that
//! implement [`Serialize`](serde::Serialize) and [`JsonSchema`][jsonschema-trait].
//!
//! [jsonschema-trait]: https://docs.rs/schemars/latest/schemars/trait.JsonSchema.html
//!
//! ### Lazy Channels
//!
//! A common pattern is to create the channels once as static variables, and then use them
//! throughout the application. But because channels do not have a const initializer, they must be
//! initialized lazily. [`LazyChannel`] and [`LazyRawChannel`] provide a convenient way to do this.
//!
//! Be careful when using this pattern. The channel will not be advertised to sinks until it is
//! initialized, which is guaranteed to happen when the channel is first used. If you need to ensure
//! the channel is initialized _before_ using it, you can use [`LazyChannel::init`].
//!
//! In this example, we create two lazy channels on the default context:
//!
//! ```
//! use foxglove::schemas::SceneUpdate;
//! use foxglove::{LazyChannel, LazyRawChannel};
//!
//! static BOXES: LazyChannel<SceneUpdate> = LazyChannel::new("/boxes");
//! static MSG: LazyRawChannel = LazyRawChannel::new("/msg", "json");
//! ```
//!
//! It is also possible to bind lazy channels to an explicit [`LazyContext`]:
//!
//! ```
//! use foxglove::schemas::SceneUpdate;
//! use foxglove::{LazyChannel, LazyContext, LazyRawChannel};
//!
//! static CTX: LazyContext = LazyContext::new();
//! static BOXES: LazyChannel<SceneUpdate> = CTX.channel("/boxes");
//! static MSG: LazyRawChannel = CTX.raw_channel("/msg", "json");
//! ```
//!
//! ## Sinks
//!
//! A "sink" is a destination for logged messages. If you do not configure a sink, log messages will
//! simply be dropped without being recorded. You can configure multiple sinks, and you can create
//! or destroy them dynamically at runtime.
//!
//! A sink is typically associated with exactly one [`Context`] throughout its lifecycle. Details
//! about the how the sink is registered and unregistered from the context are sink-specific.
//!
//! ### MCAP file
//!
//! Use [`McapWriter::new()`] to register a new MCAP writer. As long as the handle remains in scope,
//! events will be logged to the MCAP file. When the handle is closed or dropped, the sink will be
//! unregistered from the [`Context`], and the file will be finalized and flushed.
//!
//! ```no_run
//! let mcap = foxglove::McapWriter::new()
//!     .create_new_buffered_file("test.mcap")
//!     .expect("create failed");
//! ```
//!
//! You can override the MCAP writer's configuration using [`McapWriter::with_options`]. See
//! [`WriteOptions`](`mcap::WriteOptions`) for more detail about these parameters:
//!
//! ```no_run
//! # #[cfg(feature = "lz4")]
//! # {
//! let options = mcap::WriteOptions::default()
//!     .chunk_size(Some(1024 * 1024))
//!     .compression(Some(mcap::Compression::Lz4));
//!
//! let mcap = foxglove::McapWriter::with_options(options)
//!     .create_new_buffered_file("test.mcap")
//!     .expect("create failed");
//! # }
//! ```
//!
//! ### Live visualization server
//!
//! You can use the SDK to publish messages to the Foxglove app.
//!
//! Note: this requires the `websocket` feature, which is enabled by default.
//!
//! Use [`WebSocketServer::new`] to create a new live visualization server. By default, the server
//! listens on `127.0.0.1:8765`. Once the server is configured, call [`WebSocketServer::start`] to
//! start the server, and begin accepting websocket connections from the Foxglove app.
//!
//! Each client that connects to the websocket server is its own independent sink. The sink is
//! dynamically added to the [`Context`] associated with the server when the client connects, and
//! removed from the context when the client disconnects.
//!
//! See the ["Connect" documentation][app-connect] for how to connect the Foxglove app to your
//! running server.
//!
//! Note that the server remains running until the process exits, even if the handle is dropped. Use
//! [`stop`](`WebSocketServerHandle::stop`) to shut down the server explicitly.
//!
//! [app-connect]: https://docs.foxglove.dev/docs/connecting-to-data/frameworks/custom#connect
//!
//! ```no_run
//! # #[cfg(feature = "websocket")]
//! # async fn func() {
//! let server = foxglove::WebSocketServer::new()
//!     .name("Wall-E")
//!     .bind("127.0.0.1", 9999)
//!     .start()
//!     .await
//!     .expect("Failed to start visualization server");
//!
//! // Log stuff here.
//!
//! server.stop();
//! # }
//! ```
//!
//! # Feature flags
//!
//! The Foxglove SDK defines the following feature flags:
//!
//! - `chrono`: enables [chrono] conversions for [`Duration`][crate::schemas::Duration] and
//!   [`Timestamp`][crate::schemas::Timestamp].
//! - `derive`: enables the use of `#[derive(Encode)]` to derive the [`Encode`] trait for logging
//!   custom structs. Enabled by default.
//! - `live_visualization`: deprecated alias for `websocket`.
//! - `lz4`: enables support for the LZ4 compression algorithm for mcap files. Enabled by default.
//! - `schemars`: provides a blanket implementation of the [`Encode`] trait for types that
//!   implement [`Serialize`](serde::Serialize) and [`JsonSchema`][jsonschema-trait].
//! - `serde`: derives [`Serialize`](serde::Serialize) and [`Deserialize`](serde::Deserialize) for
//!   all [message types](crate::schemas).
//! - `unstable`: features which are under active development and likely to change in an upcoming
//!   version.
//! - `websocket`: enables the websocket server and client for live visualization. Enabled by
//!   default.
//! - `zstd`: enables support for the zstd compression algorithm for mcap files. Enabled by
//!   default.
//!
//! If you do not require websocket features, you can disable that flag to reduce the
//! compiled size of the SDK.
//!
//! # Requirements
//!
//! With the `websocket` feature (enabled by default), the Foxglove SDK depends on [tokio]
//! as its async runtime. See [`WebSocketServer`] for more information. Refer to the tokio
//! documentation for more information about how to configure your application to use tokio.
//!
//! [chrono]: https://docs.rs/chrono/latest/chrono/
//! [tokio]: https://docs.rs/tokio/latest/tokio/

#![warn(missing_docs)]
#![cfg_attr(docsrs, feature(doc_cfg))]

use thiserror::Error;

mod app_url;
mod channel;
mod channel_builder;
mod context;
pub mod convert;
mod decode;
mod encode;
pub mod library_version;
#[doc(hidden)]
pub mod log_macro;
mod log_sink_set;
mod mcap_writer;
#[doc(hidden)]
pub mod messages;
mod messages_wkt;
mod metadata;
#[doc(hidden)]
#[cfg(feature = "derive")]
pub mod protobuf;
mod schema;

/// Types implementing well-known Foxglove message types.
pub mod schemas {
    pub use crate::messages::*;
}
mod sink;
mod sink_channel_filter;

#[cfg(test)]
mod tests;
#[cfg(test)]
mod testutil;
mod throttler;
mod time;

#[cfg(feature = "stream")]
pub mod stream;

#[cfg(any(feature = "data_provider", feature = "remote_data_loader_backend"))]
pub mod remote_data_loader_backend;
#[cfg(feature = "data_provider")]
// Alias for backward compatibility
pub use remote_data_loader_backend as data_provider;

#[cfg(feature = "img2yuv-core")]
#[allow(unused)]
mod img2yuv;

pub use app_url::AppUrl;
// Re-export bytes crate for convenience when implementing the `Encode` trait
pub use bytes;
pub use channel::{Channel, ChannelDescriptor, ChannelId, LazyChannel, LazyRawChannel, RawChannel};
pub use channel_builder::ChannelBuilder;
pub use context::{Context, LazyContext};
#[doc(hidden)]
pub use decode::Decode;
pub use encode::Encode;
pub use mcap_writer::{
    McapAttachment, McapCompression, McapWriteOptions, McapWriter, McapWriterHandle,
};
pub use metadata::{Metadata, PartialMetadata, ToUnixNanos};
pub use schema::Schema;
pub use sink::{Sink, SinkId};
pub use sink_channel_filter::SinkChannelFilter;
pub use std::collections::BTreeMap;
pub(crate) use time::nanoseconds_since_epoch;

#[cfg(feature = "remote_access")]
mod api_client;
#[cfg(all(feature = "_remote_common", feature = "_protocol"))]
pub mod protocol;
#[cfg(all(feature = "_remote_common", not(feature = "_protocol")))]
mod protocol;
#[doc(hidden)]
#[cfg(feature = "remote_access")]
pub mod remote_access;
#[cfg(feature = "websocket")]
mod runtime;
#[cfg(feature = "websocket")]
pub mod websocket;
#[cfg(feature = "websocket")]
mod websocket_client;
#[cfg(feature = "websocket")]
mod websocket_server;
#[cfg(feature = "websocket")]
pub(crate) use runtime::get_runtime_handle;
#[cfg(feature = "websocket")]
pub use runtime::shutdown_runtime;
#[doc(hidden)]
#[cfg(feature = "websocket")]
pub use websocket::ws_protocol;
#[doc(hidden)]
#[cfg(feature = "websocket")]
pub use websocket_client::{WebSocketClient, WebSocketClientError};
#[cfg(feature = "websocket")]
pub use websocket_server::{WebSocketServer, WebSocketServerHandle};

#[doc(hidden)]
#[cfg(feature = "derive")]
pub use foxglove_derive::Encode;
#[doc(hidden)]
#[cfg(feature = "derive")]
pub use prost_types;

/// An error type for errors generated by this crate.
#[derive(Error, Debug)]
#[non_exhaustive]
pub enum FoxgloveError {
    /// An unspecified error.
    #[error("{0}")]
    Unspecified(#[source] Box<dyn std::error::Error + Send + Sync + 'static>),
    /// A value or argument is invalid.
    #[error("Value or argument is invalid: {0}")]
    ValueError(String),
    /// A UTF-8 error.
    #[error("{0}")]
    Utf8Error(String),
    /// The sink dropped a message because it is closed.
    #[error("Sink closed")]
    SinkClosed,
    /// A schema is required.
    #[error("Schema is required")]
    SchemaRequired,
    /// A message encoding is required.
    #[error("Message encoding is required")]
    MessageEncodingRequired,
    /// The server was already started.
    #[error("Server already started")]
    ServerAlreadyStarted,
    /// Failed to bind to the specified host and port.
    #[error("Failed to bind port: {0}")]
    Bind(std::io::Error),
    /// A service with the same name is already registered.
    #[error("Service {0} has already been registered")]
    DuplicateService(String),
    /// Neither the service nor the server declared supported encodings.
    #[error("Neither service {0} nor the server declared a supported request encoding")]
    MissingRequestEncoding(String),
    /// Services are not supported on this server instance.
    #[error("Services are not supported on this server instance")]
    ServicesNotSupported,
    /// Connection graph is not supported on this server instance.
    #[error("Connection graph is not supported on this server instance")]
    ConnectionGraphNotSupported,
    /// An I/O error.
    #[error(transparent)]
    IoError(#[from] std::io::Error),
    /// An error related to MCAP writing.
    #[error("MCAP error: {0}")]
    McapError(#[from] mcap::McapError),
    /// An error occurred while encoding a message.
    #[error("Encoding error: {0}")]
    EncodeError(String),
    /// An error related to configuration
    #[error("Configuration error: {0}")]
    ConfigurationError(String),
}

impl From<convert::RangeError> for FoxgloveError {
    fn from(err: convert::RangeError) -> Self {
        FoxgloveError::ValueError(err.to_string())
    }
}

impl From<std::string::FromUtf8Error> for FoxgloveError {
    fn from(err: std::string::FromUtf8Error) -> Self {
        FoxgloveError::Utf8Error(err.to_string())
    }
}

impl From<std::str::Utf8Error> for FoxgloveError {
    fn from(err: std::str::Utf8Error) -> Self {
        FoxgloveError::Utf8Error(err.to_string())
    }
}