Skip to main content

sensorviz_session/
lib.rs

1//! Tag logs opened and viewed through sensorViz's C++ core.
2//!
3//! [`Session`] is a safe wrapper over the C interface in
4//! `host/libraries/tagcore-c/sensorviz_c.h`, which wraps the Qt-free
5//! `sensorviz::Session` -- the code the Qt sensorViz runs. So a Rust program
6//! shows a log exactly as sensorViz does: the same log reader, display
7//! transforms (altitude, IMU magnitudes, compass), plot policy, hover text and
8//! preference file.
9//!
10//! ```no_run
11//! use sensorviz_session::{Preferences, Session};
12//! let preferences = Preferences::new()?;
13//! let mut session = Session::new()?;
14//! session.open("log.db3", Some(&preferences))?;
15//! let spec = session.spec()?;
16//! for trace in &spec.traces {
17//!     let envelope = session.envelope(trace.stream, f64::NEG_INFINITY, f64::INFINITY, 1200,
18//!                                     spec.break_at_gaps);
19//!     // draw envelope.time / envelope.value
20//! }
21//! let changes = session.set_stream_range("imu_pressure", 950.0, 1010.0);
22//! if changes.contains(sensorviz_session::Changes::RANGES) {
23//!     // redraw
24//! }
25//! # Ok::<(), sensorviz_session::Error>(())
26//! ```
27//!
28//! Descriptions -- the catalog, the view state, the plot spec, the
29//! parameters -- cross as JSON and are parsed into the types here; samples
30//! cross as arrays. Times are seconds in the log's time domain: since the Unix
31//! epoch, or since the start of collection for IMUTag.
32//!
33//! # Linking
34//!
35//! As for `taglogwriter` and `calibsession`: `tagcore-c-sys` links
36//! `libtagcore_c`, found through `TAGCORE_C_LIB_DIR`, statically on macOS and
37//! shared on Linux and Windows, and a program may use this crate beside them.
38//!
39//! # Safety model
40//!
41//! All `unsafe` is in this file. A [`Session`] or [`Preferences`] owns exactly
42//! one C handle, never null, freed exactly once in `Drop`. Every pointer
43//! passed in points at Rust memory that lives until the call returns, sized
44//! as the header requires. A string the library returns belongs to the handle
45//! and stays valid until the next call on it, so it is copied before any
46//! other call. Each handle may move between threads but not be shared: both
47//! types are `Send` and not `Sync`, and every call that changes or reads
48//! through the handle's string buffer takes `&mut self`.
49//!
50//! # Maintaining
51//!
52//! The declarations are `tagcore_c_sys::svz`
53//! (`host/libraries/tagcore-c-sys/src/svz.rs`), written by hand from
54//! `sensorviz_c.h`. When the header changes, change the declaration and the
55//! safe method over it. If a signature, meaning or JSON schema changed, the
56//! header raises `SVZ_ABI_VERSION`, and `svz::SVZ_ABI_VERSION` must follow;
57//! [`Session::new`] refuses a library built from another version. The JSON
58//! types here mirror the writers in `sensorviz_c.cc`; a field added there is
59//! ignored here until it is added.
60
61use std::collections::BTreeMap;
62use std::ffi::{CStr, CString, c_char, c_int};
63use std::fmt;
64use std::path::Path;
65use std::ptr::NonNull;
66
67use serde::Deserialize;
68use tagcore_c_sys::svz as ffi;
69
70/// Why a call failed.
71#[derive(Debug, Clone, PartialEq)]
72pub enum Error {
73    /// The library implements another version of the interface.
74    AbiMismatch {
75        /// The version this crate was written against.
76        expected: u32,
77        /// The version the library reports.
78        found: u32,
79    },
80    /// The library could not allocate a handle.
81    OutOfMemory,
82    /// A string argument held a NUL byte, which C cannot take.
83    Nul,
84    /// The library's own message: a log that cannot be read, a preference
85    /// file refused, a transform that failed to remake its outputs.
86    Library(String),
87    /// The library returned JSON this crate could not parse: a version skew
88    /// the ABI version did not catch.
89    Json(String),
90}
91
92impl fmt::Display for Error {
93    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94        match self {
95            Error::AbiMismatch { expected, found } => write!(
96                f,
97                "tagcore_c implements sensorviz interface version {found}, this crate needs {expected}"
98            ),
99            Error::OutOfMemory => write!(f, "out of memory creating a sensorviz handle"),
100            Error::Nul => write!(f, "a string argument contains a NUL byte"),
101            Error::Library(message) => write!(f, "{message}"),
102            Error::Json(message) => write!(f, "unexpected JSON from tagcore_c: {message}"),
103        }
104    }
105}
106
107impl std::error::Error for Error {}
108
109/// What a call changed, as the `SVZ_CHANGE_*` bits: what a front end redraws.
110#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
111pub struct Changes(pub u32);
112
113impl Changes {
114    /// Streams were added, replaced or removed.
115    pub const STREAMS: Changes = Changes(0x001);
116    /// Which streams are shown changed.
117    pub const VISIBILITY: Changes = Changes(0x002);
118    /// Custom y ranges changed.
119    pub const RANGES: Changes = Changes(0x004);
120    /// Colors or axis sides changed.
121    pub const APPEARANCE: Changes = Changes(0x008);
122    /// Title text or visibility changed.
123    pub const TITLE: Changes = Changes(0x010);
124    /// The UTC offset changed.
125    pub const UTC_OFFSET: Changes = Changes(0x020);
126    /// Cursor visibility changed.
127    pub const CURSORS: Changes = Changes(0x040);
128    /// The gap mode changed.
129    pub const GAPS: Changes = Changes(0x080);
130    /// A transform parameter changed.
131    pub const PARAMETERS: Changes = Changes(0x100);
132
133    /// True when nothing changed.
134    pub fn is_empty(self) -> bool {
135        self.0 == 0
136    }
137    /// True when every bit of `other` is set here.
138    pub fn contains(self, other: Changes) -> bool {
139        self.0 & other.0 == other.0
140    }
141}
142
143impl std::ops::BitOr for Changes {
144    type Output = Changes;
145    fn bitor(self, other: Changes) -> Changes {
146        Changes(self.0 | other.0)
147    }
148}
149
150/// A time domain.
151#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
152#[serde(rename_all = "lowercase")]
153pub enum TimeDomain {
154    /// Seconds since the Unix epoch.
155    Epoch,
156    /// Seconds since the start of collection (IMUTag).
157    Elapsed,
158}
159
160/// A side of the plot.
161#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
162#[serde(rename_all = "lowercase")]
163pub enum Side {
164    /// Left of the data area.
165    Left,
166    /// Right of the data area.
167    Right,
168}
169
170/// A multi-column table that transforms read, such as `compass_raw`.
171#[derive(Debug, Clone, PartialEq, Deserialize)]
172#[serde(rename_all = "camelCase")]
173pub struct RecordSet {
174    /// Stable id.
175    pub id: String,
176    /// User-facing name.
177    pub label: String,
178    /// SQLite table.
179    pub table: String,
180    /// Domain of its times.
181    pub time_domain: TimeDomain,
182    /// Column names.
183    pub columns: Vec<String>,
184}
185
186/// Discrete events drawn as vertical lines.
187#[derive(Debug, Clone, PartialEq, Deserialize)]
188#[serde(rename_all = "camelCase")]
189pub struct EventMarker {
190    /// Stable id, e.g. `imu_resync`.
191    pub id: String,
192    /// User-facing name.
193    pub label: String,
194    /// Domain of `times`.
195    pub time_domain: TimeDomain,
196    /// Event times, seconds.
197    pub times: Vec<f64>,
198}
199
200/// The newest compass calibration in the log.
201#[derive(Debug, Clone, PartialEq, Deserialize)]
202#[serde(rename_all = "camelCase")]
203pub struct CompassCalibration {
204    /// Whether a usable calibration was read.
205    pub present: bool,
206    /// Its Calibration row's epoch, seconds.
207    pub epoch: i64,
208    /// Why it is absent or suspect; empty when there is nothing to say.
209    pub warning: String,
210    /// Hard-iron offset, µT.
211    pub hard_iron: [f64; 3],
212    /// Soft-iron matrix, row-major.
213    pub soft_iron: [f64; 9],
214    /// Accelerometer zero-g offset, mg.
215    pub accel_offset: [f64; 3],
216}
217
218/// A transform that failed when the log was opened.
219#[derive(Debug, Clone, PartialEq, Deserialize)]
220pub struct TransformFailure {
221    /// The transform's id, e.g. `compass`.
222    pub transform: String,
223    /// Why.
224    pub error: String,
225}
226
227/// The open log: what it is and what it holds besides its streams.
228#[derive(Debug, Clone, PartialEq, Deserialize)]
229#[serde(rename_all = "camelCase")]
230pub struct Catalog {
231    /// The file, as opened; empty when none is.
232    pub path: String,
233    /// The `info` table's tag type; may be empty.
234    pub tag_type: String,
235    /// A display name for the tag family.
236    pub profile_name: String,
237    /// The key its preferences are stored under.
238    pub preference_key: String,
239    /// The `info` table.
240    pub info: BTreeMap<String, String>,
241    /// The plot's time domain.
242    pub time_domain: TimeDomain,
243    /// The absolute start of collection, ms since the Unix epoch, when known.
244    pub collection_start_epoch_ms: Option<i64>,
245    /// Record sets.
246    pub record_sets: Vec<RecordSet>,
247    /// Event markers.
248    pub event_markers: Vec<EventMarker>,
249    /// The compass calibration.
250    pub compass_calibration: CompassCalibration,
251    /// Where the compass samples came from, for File Info; empty without a compass.
252    pub compass_source: String,
253    /// Transforms that failed when the log was opened.
254    pub transform_failures: Vec<TransformFailure>,
255}
256
257/// One stream: what it is, how it is drawn, and its plot data.
258#[derive(Debug, Clone, PartialEq, Deserialize)]
259#[serde(rename_all = "camelCase")]
260pub struct Stream {
261    /// Stable id, e.g. `imu_ax`.
262    pub id: String,
263    /// User-facing name.
264    pub label: String,
265    /// Units; may be empty.
266    pub units: String,
267    /// Axis group; empty for a stream with its own axis.
268    pub group: String,
269    /// The group's user-facing name.
270    pub group_label: String,
271    /// Default color, `#rrggbb`.
272    pub color: String,
273    /// The color it is drawn in: the user's, else the default.
274    pub effective_color: String,
275    /// Derived streams have no visibility of their own.
276    pub derived: bool,
277    /// Visible on load.
278    pub default_visible: bool,
279    /// Default axis side.
280    pub axis_side: Side,
281    /// The side it is drawn on: the user's, else the default.
282    pub effective_axis_side: Side,
283    /// Default fixed y range.
284    pub axis_range: Option<[f64; 2]>,
285    /// Domain of its times.
286    pub time_domain: TimeDomain,
287    /// Its plot-data handle, for [`Session::envelope`].
288    pub stream: u64,
289    /// Whether it is shown.
290    pub visible: bool,
291}
292
293/// A y range the user set, or one linked to such a range.
294#[derive(Debug, Clone, Copy, PartialEq, Deserialize)]
295pub struct CustomRange {
296    /// Lower bound.
297    pub lower: f64,
298    /// Upper bound.
299    pub upper: f64,
300    /// Set directly, rather than through a linked stream.
301    pub explicit: bool,
302}
303
304/// The view state: the streams and every choice about them.
305#[derive(Debug, Clone, PartialEq, Deserialize)]
306#[serde(rename_all = "camelCase")]
307pub struct View {
308    /// Every stream, in View-menu order.
309    pub streams: Vec<Stream>,
310    /// The shown streams, in drawing order.
311    pub visible_stream_ids: Vec<String>,
312    /// Custom y ranges by stream id.
313    pub custom_ranges: BTreeMap<String, CustomRange>,
314    /// Graph title.
315    pub title: String,
316    /// Whether the title is shown.
317    pub title_visible: bool,
318    /// Hours added to UTC for epoch times.
319    pub utc_offset_hours: i32,
320    /// Whether the time cursors are shown.
321    pub cursors_visible: bool,
322    /// Whether lines break at gaps.
323    pub break_at_gaps: bool,
324    /// Every transform parameter by id; toggles as 1 or 0.
325    pub parameters: BTreeMap<String, f64>,
326}
327
328/// One drawn trace.
329#[derive(Debug, Clone, PartialEq, Deserialize)]
330#[serde(rename_all = "camelCase")]
331pub struct Trace {
332    /// Stream id.
333    pub id: String,
334    /// Legend name.
335    pub name: String,
336    /// Plot-data handle, for [`Session::envelope`].
337    pub stream: u64,
338    /// Line color, `#rrggbb`.
339    pub color: String,
340    /// Line width, pixels.
341    pub pen_width: i32,
342    /// Index into [`Spec::axes`].
343    pub axis: usize,
344}
345
346/// How an axis's range is chosen.
347#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
348#[serde(rename_all = "lowercase")]
349pub enum AxisMode {
350    /// From the data, padded, and widened to include the range it showed
351    /// before (the renderer keeps that history).
352    Auto,
353    /// The padded union of an axis group's data ranges.
354    Linked,
355    /// Exactly `range`.
356    Fixed,
357}
358
359/// One y axis.
360#[derive(Debug, Clone, PartialEq, Deserialize)]
361#[serde(rename_all = "camelCase")]
362pub struct Axis {
363    /// Which side.
364    pub side: Side,
365    /// Title.
366    pub label: String,
367    /// Axis, tick and label color, `#rrggbb`.
368    pub color: String,
369    /// How the range is chosen.
370    pub mode: AxisMode,
371    /// The axis group when linked.
372    pub linked_group: String,
373    /// Indices into [`Spec::traces`].
374    pub traces: Vec<usize>,
375    /// The range per `mode`; none for an auto axis without finite data.
376    pub range: Option<[f64; 2]>,
377}
378
379/// What the plot shows: everything a renderer needs besides the samples.
380#[derive(Debug, Clone, PartialEq, Deserialize)]
381#[serde(rename_all = "camelCase")]
382pub struct Spec {
383    /// The time axis.
384    pub time_domain: TimeDomain,
385    /// Hours added to UTC for epoch tick labels.
386    pub utc_offset_hours: i32,
387    /// Earliest and latest sample over all traces; none when nothing is drawn.
388    pub x_data_range: Option<[f64; 2]>,
389    /// Traces in drawing order.
390    pub traces: Vec<Trace>,
391    /// Axes, the first outermost.
392    pub axes: Vec<Axis>,
393    /// Vertical marker lines.
394    pub event_marker_times: Vec<f64>,
395    /// Lines of the metadata box; empty hides it.
396    pub metadata_rows: Vec<String>,
397    /// Title; empty hides it.
398    pub title: String,
399    /// Lines break at gaps.
400    pub break_at_gaps: bool,
401}
402
403/// A transform parameter, described for building its control.
404#[derive(Debug, Clone, PartialEq, Deserialize)]
405pub struct Parameter {
406    /// Id, for [`Session::set_parameter`].
407    pub id: String,
408    /// Dialog title.
409    pub title: String,
410    /// Field label.
411    pub prompt: String,
412    /// Units; may be empty.
413    pub units: String,
414    /// `number` or `toggle`.
415    pub kind: String,
416    /// Smallest value (number).
417    pub minimum: f64,
418    /// Largest value (number).
419    pub maximum: f64,
420    /// Decimals shown (number).
421    pub decimals: i32,
422    /// The current value; a toggle as 1 or 0.
423    pub value: f64,
424}
425
426/// One stream's M4 envelope: at most four points per column, plus gap markers.
427#[derive(Debug, Clone, PartialEq, Default)]
428pub struct Envelope {
429    /// Point times, seconds.
430    pub time: Vec<f64>,
431    /// Point values as stored; NaN at a gap marker.
432    pub value: Vec<f32>,
433    /// Sample index of each point; `None` at a gap marker.
434    pub index: Vec<Option<u64>>,
435}
436
437/// One compass orientation sample.
438#[derive(Debug, Clone, Copy, PartialEq)]
439pub struct OrientationSample {
440    /// Time, seconds.
441    pub time: f64,
442    /// Magnetic-frame orientation, `[w, x, y, z]`.
443    pub q: [f32; 4],
444    /// Magnetic dip, degrees.
445    pub dip: f32,
446    /// Field magnitude, µT.
447    pub field: f32,
448    /// Acceleration magnitude, mg.
449    pub mg: f32,
450}
451
452/// Checks the library's interface version.
453fn check_abi() -> Result<(), Error> {
454    // SAFETY: takes nothing and returns a number.
455    let found = unsafe { ffi::svz_abi_version() };
456    if found == ffi::SVZ_ABI_VERSION {
457        Ok(())
458    } else {
459        Err(Error::AbiMismatch {
460            expected: ffi::SVZ_ABI_VERSION,
461            found,
462        })
463    }
464}
465
466/// Copies a library string; null reads as empty.
467///
468/// # Safety
469///
470/// `text` is null or a NUL-terminated string that stays valid for the call.
471unsafe fn copy(text: *const c_char) -> String {
472    if text.is_null() {
473        return String::new();
474    }
475    // SAFETY: non-null and NUL-terminated, as the caller promises.
476    unsafe { CStr::from_ptr(text) }
477        .to_string_lossy()
478        .into_owned()
479}
480
481/// `text` as a C string.
482fn c_string(text: &str) -> Result<CString, Error> {
483    CString::new(text).map_err(|_| Error::Nul)
484}
485
486/// Strings as C strings, and the pointer array the library takes.
487fn c_strings(items: &[&str]) -> Result<(Vec<CString>, Vec<*const c_char>), Error> {
488    let owned = items
489        .iter()
490        .map(|item| c_string(item))
491        .collect::<Result<Vec<_>, _>>()?;
492    let pointers = owned.iter().map(|item| item.as_ptr()).collect();
493    Ok((owned, pointers))
494}
495
496/// Parses JSON from the library.
497fn parse<T: for<'a> Deserialize<'a>>(json: &str) -> Result<T, Error> {
498    serde_json::from_str(json).map_err(|e| Error::Json(e.to_string()))
499}
500
501/// The per-tag display preferences: colors, axis sides, visibility and gaps
502/// that follow a tag type, and their JSON file. Outlives sessions.
503pub struct Preferences {
504    /// The C handle; never null.
505    handle: NonNull<ffi::SvzPreferences>,
506}
507
508// SAFETY: the handle is used from one thread at a time, which `&mut self`
509// on every call that touches its buffers ensures; it may move between threads.
510unsafe impl Send for Preferences {}
511
512impl Preferences {
513    /// An empty store.
514    ///
515    /// # Errors
516    ///
517    /// [`Error::AbiMismatch`] for a library of another interface version,
518    /// [`Error::OutOfMemory`] when it cannot allocate.
519    pub fn new() -> Result<Self, Error> {
520        check_abi()?;
521        // SAFETY: takes nothing; returns a new handle or null.
522        let handle = unsafe { ffi::svz_preferences_new() };
523        NonNull::new(handle)
524            .map(|handle| Preferences { handle })
525            .ok_or(Error::OutOfMemory)
526    }
527
528    /// Replaces every entry with those of a preference file.
529    ///
530    /// # Errors
531    ///
532    /// [`Error::Library`] with the reason when the file is refused, leaving
533    /// the store as it was; [`Error::Nul`] when `source` holds a NUL.
534    pub fn load(&mut self, json: &str, source: &str) -> Result<(), Error> {
535        let source = c_string(source)?;
536        // SAFETY: a live handle; `json` is `json.len()` bytes and `source`
537        // NUL-terminated, both live for the call; the library copies them.
538        let ok = unsafe {
539            ffi::svz_preferences_load(
540                self.handle.as_ptr(),
541                json.as_ptr().cast(),
542                json.len(),
543                source.as_ptr(),
544            )
545        };
546        if ok == 1 {
547            Ok(())
548        } else {
549            // SAFETY: a live handle; the message is copied before the next call.
550            Err(Error::Library(unsafe {
551                copy(ffi::svz_preferences_last_error(self.handle.as_ptr()))
552            }))
553        }
554    }
555
556    /// The preference file, byte for byte as the Qt sensorViz writes it.
557    pub fn save(&mut self) -> String {
558        // SAFETY: a live handle; the text is copied before the next call.
559        unsafe { copy(ffi::svz_preferences_save(self.handle.as_ptr())) }
560    }
561}
562
563impl Drop for Preferences {
564    fn drop(&mut self) {
565        // SAFETY: the handle is live and freed only here.
566        unsafe { ffi::svz_preferences_free(self.handle.as_ptr()) }
567    }
568}
569
570/// One opened log: its catalog, the view state, the transforms, and what the
571/// plot and readout show.
572pub struct Session {
573    /// The C handle; never null.
574    handle: NonNull<ffi::SvzSession>,
575}
576
577// SAFETY: as for Preferences.
578unsafe impl Send for Session {}
579
580impl Session {
581    /// A session with no log.
582    ///
583    /// # Errors
584    ///
585    /// [`Error::AbiMismatch`] for a library of another interface version,
586    /// [`Error::OutOfMemory`] when it cannot allocate.
587    pub fn new() -> Result<Self, Error> {
588        check_abi()?;
589        // SAFETY: takes nothing; returns a new handle or null.
590        let handle = unsafe { ffi::svz_session_new() };
591        NonNull::new(handle)
592            .map(|handle| Session { handle })
593            .ok_or(Error::OutOfMemory)
594    }
595
596    /// The handle, for a call.
597    fn raw(&self) -> *mut ffi::SvzSession {
598        self.handle.as_ptr()
599    }
600
601    /// The library's last error on this session.
602    fn last_error(&self) -> Error {
603        // SAFETY: a live handle; the message is copied at once.
604        Error::Library(unsafe { copy(ffi::svz_last_error(self.raw())) })
605    }
606
607    /// Copies a string the library returned for this session.
608    fn returned(&mut self, text: *const c_char) -> String {
609        // SAFETY: the string belongs to this live handle and is valid until
610        // its next call, which cannot happen while `&mut self` is held here.
611        unsafe { copy(text) }
612    }
613
614    /// Opens a log and does everything sensorViz's File > Load does: reads
615    /// it, resets the view, runs the transforms and applies `preferences`'
616    /// entry for its tag.
617    ///
618    /// # Errors
619    ///
620    /// [`Error::Library`] when the log cannot be read, leaving the session as
621    /// it was; [`Error::Nul`] when the path holds a NUL.
622    pub fn open(
623        &mut self,
624        path: impl AsRef<Path>,
625        preferences: Option<&Preferences>,
626    ) -> Result<(), Error> {
627        let path = c_string(&path.as_ref().to_string_lossy())?;
628        let preferences = preferences.map_or(std::ptr::null(), |p| p.handle.as_ptr().cast_const());
629        // SAFETY: a live handle, a NUL-terminated path living for the call,
630        // and a live or null preference handle, read only.
631        let ok = unsafe { ffi::svz_open(self.raw(), path.as_ptr(), preferences) };
632        if ok == 1 {
633            Ok(())
634        } else {
635            Err(self.last_error())
636        }
637    }
638
639    /// Applies `preferences`' entry for the open log's tag, or the defaults.
640    pub fn apply_preferences(&mut self, preferences: &Preferences) -> Changes {
641        // SAFETY: two live handles; the store is only read.
642        Changes(unsafe { ffi::svz_apply_preferences(self.raw(), preferences.handle.as_ptr()) })
643    }
644
645    /// Stores the open log's display choices in `preferences` under its tag.
646    pub fn remember_preferences(&self, preferences: &mut Preferences) {
647        // SAFETY: two live handles; the session is only read.
648        unsafe { ffi::svz_remember_preferences(self.raw(), preferences.handle.as_ptr()) }
649    }
650
651    /// Removes `preferences`' entry for the open log's tag.
652    pub fn forget_preferences(&self, preferences: &mut Preferences) {
653        // SAFETY: two live handles; the session is only read.
654        unsafe { ffi::svz_forget_preferences(self.raw(), preferences.handle.as_ptr()) }
655    }
656
657    /// The open log.
658    ///
659    /// # Errors
660    ///
661    /// [`Error::Json`] for JSON this crate does not understand.
662    pub fn catalog(&mut self) -> Result<Catalog, Error> {
663        // SAFETY: a live handle.
664        let text = unsafe { ffi::svz_catalog_json(self.raw()) };
665        parse(&self.returned(text))
666    }
667
668    /// The view state.
669    ///
670    /// # Errors
671    ///
672    /// [`Error::Json`] for JSON this crate does not understand.
673    pub fn view(&mut self) -> Result<View, Error> {
674        // SAFETY: a live handle.
675        let text = unsafe { ffi::svz_view_json(self.raw()) };
676        parse(&self.returned(text))
677    }
678
679    /// The plot spec.
680    ///
681    /// # Errors
682    ///
683    /// [`Error::Json`] for JSON this crate does not understand.
684    pub fn spec(&mut self) -> Result<Spec, Error> {
685        // SAFETY: a live handle.
686        let text = unsafe { ffi::svz_spec_json(self.raw()) };
687        parse(&self.returned(text))
688    }
689
690    /// The JSON texts of the catalog, view and spec, as the library writes
691    /// them: for comparing two sessions exactly.
692    pub fn state_json(&mut self) -> [String; 3] {
693        // SAFETY: a live handle; each text is copied before the next call.
694        let catalog = unsafe { ffi::svz_catalog_json(self.raw()) };
695        let catalog = self.returned(catalog);
696        // SAFETY: as above.
697        let view = unsafe { ffi::svz_view_json(self.raw()) };
698        let view = self.returned(view);
699        // SAFETY: as above.
700        let spec = unsafe { ffi::svz_spec_json(self.raw()) };
701        let spec = self.returned(spec);
702        [catalog, view, spec]
703    }
704
705    /// The parameters of the transforms that apply to the open log, with
706    /// their current values.
707    ///
708    /// # Errors
709    ///
710    /// [`Error::Json`] for JSON this crate does not understand.
711    pub fn parameters(&mut self) -> Result<Vec<Parameter>, Error> {
712        // SAFETY: a live handle.
713        let text = unsafe { ffi::svz_parameters_json(self.raw()) };
714        parse(&self.returned(text))
715    }
716
717    /// The hover readout at `time`: the time, then each visible stream's
718    /// `label: value units`.
719    ///
720    /// # Errors
721    ///
722    /// [`Error::Json`] for JSON this crate does not understand.
723    pub fn hover(&mut self, time: f64) -> Result<Vec<String>, Error> {
724        // SAFETY: a live handle.
725        let text = unsafe { ffi::svz_hover_json(self.raw(), time) };
726        parse(&self.returned(text))
727    }
728
729    /// `time` as the readout shows it.
730    pub fn format_time(&mut self, time: f64) -> String {
731        // SAFETY: a live handle.
732        let text = unsafe { ffi::svz_format_time(self.raw(), time) };
733        self.returned(text)
734    }
735
736    /// The range the range dialog starts from for stream `id`; `None` for an
737    /// unknown stream.
738    pub fn range_for_dialog(&self, id: &str) -> Option<(f64, f64)> {
739        let id = c_string(id).ok()?;
740        let (mut lower, mut upper) = (0.0, 0.0);
741        // SAFETY: a live handle, a NUL-terminated id, and two doubles to write.
742        let ok =
743            unsafe { ffi::svz_range_for_dialog(self.raw(), id.as_ptr(), &mut lower, &mut upper) };
744        (ok == 1).then_some((lower, upper))
745    }
746
747    /// One stream's M4 envelope over `[t0, t1]` (infinities for the whole
748    /// stream) at `columns`, breaking lines at gaps when `break_gaps`.
749    pub fn envelope(
750        &mut self,
751        stream: u64,
752        t0: f64,
753        t1: f64,
754        columns: i32,
755        break_gaps: bool,
756    ) -> Envelope {
757        let call = |time: *mut f64, value: *mut f32, index: *mut u64, capacity: usize| {
758            // SAFETY: a live handle; each array holds `capacity` elements (or
759            // is null with capacity 0), and the library writes no more.
760            unsafe {
761                ffi::svz_envelope(
762                    self.raw(),
763                    stream,
764                    t0,
765                    t1,
766                    columns,
767                    c_int::from(break_gaps),
768                    time,
769                    value,
770                    index,
771                    capacity,
772                )
773            }
774        };
775        let count = call(
776            std::ptr::null_mut(),
777            std::ptr::null_mut(),
778            std::ptr::null_mut(),
779            0,
780        );
781        let mut time = vec![0.0; count];
782        let mut value = vec![0.0f32; count];
783        let mut index = vec![0u64; count];
784        let written = call(
785            time.as_mut_ptr(),
786            value.as_mut_ptr(),
787            index.as_mut_ptr(),
788            count,
789        );
790        if written != count {
791            return Envelope::default();
792        }
793        Envelope {
794            time,
795            value,
796            index: index
797                .into_iter()
798                .map(|i| (i != ffi::SVZ_GAP_MARKER).then_some(i))
799                .collect(),
800        }
801    }
802
803    /// The compass orientation samples; empty without a compass.
804    pub fn compass_samples(&mut self) -> Vec<OrientationSample> {
805        // SAFETY: a live handle; null arrays with capacity 0 ask for the count.
806        let count = unsafe {
807            ffi::svz_compass_samples(self.raw(), std::ptr::null_mut(), std::ptr::null_mut(), 0)
808        };
809        let mut time = vec![0.0; count];
810        let mut values = vec![0.0f32; 7 * count];
811        // SAFETY: a live handle; `time` holds `count` and `values` 7 * `count`.
812        let written = unsafe {
813            ffi::svz_compass_samples(self.raw(), time.as_mut_ptr(), values.as_mut_ptr(), count)
814        };
815        if written != count {
816            return Vec::new();
817        }
818        let (chunks, _) = values.as_chunks::<7>();
819        time.iter()
820            .zip(chunks)
821            .map(|(&time, v)| OrientationSample {
822                time,
823                q: [v[0], v[1], v[2], v[3]],
824                dip: v[4],
825                field: v[5],
826                mg: v[6],
827            })
828            .collect()
829    }
830
831    /// Takes the open log through sensorViz's state-dump script and returns
832    /// the dump; it changes the session, so use one opened for the purpose.
833    pub fn state_dump(&mut self) -> String {
834        // SAFETY: a live handle.
835        let text = unsafe { ffi::svz_state_dump(self.raw()) };
836        self.returned(text)
837    }
838
839    /// Shows or hides stream `id`.
840    pub fn set_stream_visible(&mut self, id: &str, visible: bool) -> Changes {
841        let Ok(id) = c_string(id) else {
842            return Changes::default();
843        };
844        // SAFETY: a live handle and a NUL-terminated id living for the call.
845        Changes(unsafe {
846            ffi::svz_set_stream_visible(self.raw(), id.as_ptr(), c_int::from(visible))
847        })
848    }
849
850    /// Shows exactly the streams `ids`.
851    ///
852    /// # Errors
853    ///
854    /// [`Error::Nul`] when an id holds a NUL.
855    pub fn set_visible_streams(&mut self, ids: &[&str]) -> Result<Changes, Error> {
856        let (_owned, pointers) = c_strings(ids)?;
857        // SAFETY: a live handle; `pointers` holds `ids.len()` NUL-terminated
858        // strings, kept alive by `_owned` for the call.
859        Ok(Changes(unsafe {
860            ffi::svz_set_visible_streams(self.raw(), pointers.as_ptr(), pointers.len())
861        }))
862    }
863
864    /// Sets a y range the user chose, and the ranges linked to it.
865    pub fn set_stream_range(&mut self, id: &str, lower: f64, upper: f64) -> Changes {
866        let Ok(id) = c_string(id) else {
867            return Changes::default();
868        };
869        // SAFETY: a live handle and a NUL-terminated id living for the call.
870        Changes(unsafe { ffi::svz_set_stream_range(self.raw(), id.as_ptr(), lower, upper) })
871    }
872
873    /// Reset Zoom: clears every custom range.
874    pub fn reset_zoom(&mut self) -> Changes {
875        // SAFETY: a live handle.
876        Changes(unsafe { ffi::svz_reset_zoom(self.raw()) })
877    }
878
879    /// Replaces the color overrides: stream id and `0xRRGGBB`.
880    ///
881    /// # Errors
882    ///
883    /// [`Error::Nul`] when an id holds a NUL.
884    pub fn set_colors(&mut self, colors: &[(&str, u32)]) -> Result<Changes, Error> {
885        let ids: Vec<&str> = colors.iter().map(|(id, _)| *id).collect();
886        let rgb: Vec<u32> = colors.iter().map(|(_, rgb)| *rgb).collect();
887        let (_owned, pointers) = c_strings(&ids)?;
888        // SAFETY: a live handle; both arrays hold `colors.len()` items, the
889        // strings kept alive by `_owned` for the call.
890        Ok(Changes(unsafe {
891            ffi::svz_set_colors(self.raw(), pointers.as_ptr(), rgb.as_ptr(), rgb.len())
892        }))
893    }
894
895    /// Replaces the axis-side overrides.
896    ///
897    /// # Errors
898    ///
899    /// [`Error::Nul`] when an id holds a NUL.
900    pub fn set_axis_sides(&mut self, sides: &[(&str, Side)]) -> Result<Changes, Error> {
901        let ids: Vec<&str> = sides.iter().map(|(id, _)| *id).collect();
902        let right: Vec<c_int> = sides
903            .iter()
904            .map(|(_, side)| c_int::from(*side == Side::Right))
905            .collect();
906        let (_owned, pointers) = c_strings(&ids)?;
907        // SAFETY: a live handle; both arrays hold `sides.len()` items, the
908        // strings kept alive by `_owned` for the call.
909        Ok(Changes(unsafe {
910            ffi::svz_set_axis_sides(self.raw(), pointers.as_ptr(), right.as_ptr(), right.len())
911        }))
912    }
913
914    /// Sets the title text.
915    ///
916    /// # Errors
917    ///
918    /// [`Error::Nul`] when the title holds a NUL.
919    pub fn set_title(&mut self, title: &str) -> Result<Changes, Error> {
920        let title = c_string(title)?;
921        // SAFETY: a live handle and a NUL-terminated title living for the call.
922        Ok(Changes(unsafe {
923            ffi::svz_set_title(self.raw(), title.as_ptr())
924        }))
925    }
926
927    /// Shows or hides the title.
928    pub fn set_title_visible(&mut self, visible: bool) -> Changes {
929        // SAFETY: a live handle.
930        Changes(unsafe { ffi::svz_set_title_visible(self.raw(), c_int::from(visible)) })
931    }
932
933    /// Sets the UTC offset for epoch times, hours.
934    pub fn set_utc_offset(&mut self, hours: i32) -> Changes {
935        // SAFETY: a live handle.
936        Changes(unsafe { ffi::svz_set_utc_offset(self.raw(), hours) })
937    }
938
939    /// Shows or hides the time cursors.
940    pub fn set_cursors_visible(&mut self, visible: bool) -> Changes {
941        // SAFETY: a live handle.
942        Changes(unsafe { ffi::svz_set_cursors_visible(self.raw(), c_int::from(visible)) })
943    }
944
945    /// Breaks lines at gaps, or joins them.
946    pub fn set_break_at_gaps(&mut self, break_gaps: bool) -> Changes {
947        // SAFETY: a live handle.
948        Changes(unsafe { ffi::svz_set_break_at_gaps(self.raw(), c_int::from(break_gaps)) })
949    }
950
951    /// Sets a transform parameter (an id from [`Session::parameters`]) and
952    /// remakes what it affects; a toggle is on when `value` is nonzero.
953    ///
954    /// # Errors
955    ///
956    /// [`Error::Library`] when the transform failed to remake its outputs;
957    /// [`Error::Nul`] when the id holds a NUL.
958    pub fn set_parameter(&mut self, id: &str, value: f64) -> Result<Changes, Error> {
959        let id = c_string(id)?;
960        let mut changes = 0u32;
961        // SAFETY: a live handle, a NUL-terminated id living for the call, and
962        // one u32 to write.
963        let ok = unsafe { ffi::svz_set_parameter(self.raw(), id.as_ptr(), value, &mut changes) };
964        if ok == 1 {
965            Ok(Changes(changes))
966        } else {
967            Err(self.last_error())
968        }
969    }
970}
971
972impl Drop for Session {
973    fn drop(&mut self) {
974        // SAFETY: the handle is live and freed only here.
975        unsafe { ffi::svz_session_free(self.handle.as_ptr()) }
976    }
977}