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}