Skip to main content

sigmf/
capture.rs

1//! Capture segments: entries of the SigMF `captures` array.
2//!
3//! Each [`Capture`] describes a contiguous run of samples in the dataset,
4//! beginning at `core:sample_start` and continuing until the next capture's
5//! start (or the end of the dataset). Captures carry per-segment parameters
6//! such as center frequency (`core:frequency`), wall-clock time
7//! (`core:datetime`), and the number of non-sample header bytes that precede
8//! the segment in the data file (`core:header_bytes`).
9//!
10//! Fields not defined by the SigMF core namespace (for example extension keys
11//! like `"vendor:gain"`) are preserved verbatim and are reachable through
12//! [`Capture::extra_fields`] and [`Capture::insert_extra`].
13
14use super::{Integer, Number};
15use geojson::GeoJson;
16use serde_derive::{Deserialize, Serialize};
17use serde_json::{Map, Value};
18
19/// A single capture segment from the SigMF `captures` array.
20///
21/// Serializes to and from a JSON object keyed by SigMF field names. Optional
22/// fields set to `None` are omitted from the output; unknown keys are
23/// preserved in a flattened map of extension fields.
24///
25/// # Examples
26///
27/// ```rust
28/// use sigmf::capture::Capture;
29///
30/// let mut capture = Capture::new(0);
31/// capture.set_frequency(915e6);
32/// capture.set_datetime("2026-01-01T00:00:00Z".to_string());
33///
34/// let json = serde_json::to_value(&capture).unwrap();
35/// assert_eq!(json["core:sample_start"], 0);
36/// assert_eq!(json["core:frequency"], 915e6);
37/// assert!(json.get("core:header_bytes").is_none());
38/// ```
39#[derive(Debug, Deserialize, Serialize, Clone)]
40pub struct Capture {
41    // Required fields
42    /// Index of the first sample of this segment within the dataset
43    /// (`core:sample_start`); required.
44    #[serde(rename = "core:sample_start")]
45    pub sample_start: Integer,
46
47    // Optional fields
48    /// ISO 8601 timestamp of the segment's first sample (`core:datetime`).
49    #[serde(rename = "core:datetime", skip_serializing_if = "Option::is_none")]
50    pub datetime: Option<String>,
51    /// Center frequency of the signal in Hz (`core:frequency`).
52    #[serde(rename = "core:frequency", skip_serializing_if = "Option::is_none")]
53    pub frequency: Option<Number>,
54    /// Sample index of this segment's first sample in a larger, global
55    /// sample stream such as an SDR's internal counter (`core:global_index`).
56    #[serde(rename = "core:global_index", skip_serializing_if = "Option::is_none")]
57    pub global_index: Option<Integer>,
58    /// Number of bytes of non-sample header data preceding this segment in
59    /// the dataset file (`core:header_bytes`).
60    #[serde(rename = "core:header_bytes", skip_serializing_if = "Option::is_none")]
61    pub header_bytes: Option<Integer>,
62    /// GeoJSON `Point` giving the recording location for this segment
63    /// (`core:geolocation`).
64    #[serde(rename = "core:geolocation", skip_serializing_if = "Option::is_none")]
65    pub geolocation: Option<GeoJson>,
66    #[serde(flatten)]
67    extra_fields: Map<String, Value>,
68}
69
70impl Capture {
71    /// Creates a capture starting at `sample_start` with every optional field
72    /// unset and no extension fields.
73    pub fn new(sample_start: Integer) -> Self {
74        Capture {
75            sample_start,
76            datetime: None,
77            frequency: None,
78            global_index: None,
79            header_bytes: None,
80            geolocation: None,
81            extra_fields: Map::new(),
82        }
83    }
84
85    /// Returns the non-core (extension) fields preserved on this capture,
86    /// keyed by their full JSON name (for example `"vendor:gain"`).
87    pub fn extra_fields(&self) -> &Map<String, Value> {
88        &self.extra_fields
89    }
90
91    pub(crate) fn extension_fields_mut(&mut self) -> &mut Map<String, Value> {
92        &mut self.extra_fields
93    }
94
95    /// Inserts or replaces an extension field on this capture.
96    ///
97    /// Returns the previous value stored under `key`, if any. The key is
98    /// stored verbatim; no namespace or extension-declaration check is
99    /// performed here.
100    ///
101    /// # Errors
102    ///
103    /// Returns [`crate::SigmfError::InvalidMetadata`] if `key` names a core
104    /// capture field modeled by [`Capture`] (`core:sample_start`,
105    /// `core:datetime`, `core:frequency`, `core:global_index`,
106    /// `core:header_bytes`, or `core:geolocation`); set those through the
107    /// typed fields or setters instead.
108    pub fn insert_extra(
109        &mut self,
110        key: impl Into<String>,
111        value: Value,
112    ) -> Result<Option<Value>, crate::SigmfError> {
113        let key = key.into();
114        if matches!(
115            key.as_str(),
116            "core:sample_start"
117                | "core:datetime"
118                | "core:frequency"
119                | "core:global_index"
120                | "core:header_bytes"
121                | "core:geolocation"
122        ) {
123            return Err(crate::SigmfError::InvalidMetadata(format!(
124                "cannot insert known field {key:?} as an extension"
125            )));
126        }
127        Ok(self.extra_fields.insert(key, value))
128    }
129
130    // Setters
131    /// Sets `core:datetime`, the ISO 8601 timestamp of the first sample.
132    ///
133    /// The string is stored as given and is not validated.
134    pub fn set_datetime(&mut self, datetime: String) {
135        self.datetime = Some(datetime);
136    }
137
138    /// Sets `core:frequency`, the center frequency in Hz.
139    pub fn set_frequency(&mut self, frequency: Number) {
140        self.frequency = Some(frequency);
141    }
142
143    /// Sets `core:global_index`, the segment's index in a global sample
144    /// stream.
145    pub fn set_global_index(&mut self, global_index: Integer) {
146        self.global_index = Some(global_index);
147    }
148
149    /// Sets `core:header_bytes`, the number of header bytes preceding this
150    /// segment in the dataset file.
151    pub fn set_header_bytes(&mut self, header_bytes: Integer) {
152        self.header_bytes = Some(header_bytes);
153    }
154
155    /// Sets `core:geolocation` to the given GeoJSON value.
156    pub fn set_geolocation(&mut self, geolocation: GeoJson) {
157        self.geolocation = Some(geolocation);
158    }
159
160    // Getters
161    /// Returns `core:sample_start`, the index of this segment's first sample.
162    pub fn get_sample_start(&self) -> Integer {
163        self.sample_start
164    }
165
166    /// Returns `core:datetime`, if set.
167    pub fn get_datetime(&self) -> Option<&String> {
168        self.datetime.as_ref()
169    }
170
171    /// Returns `core:frequency` in Hz, if set.
172    pub fn get_frequency(&self) -> Option<&Number> {
173        self.frequency.as_ref()
174    }
175
176    /// Returns `core:global_index`, if set.
177    pub fn get_global_index(&self) -> Option<&Integer> {
178        self.global_index.as_ref()
179    }
180
181    /// Returns `core:header_bytes`, if set.
182    pub fn get_header_bytes(&self) -> Option<&Integer> {
183        self.header_bytes.as_ref()
184    }
185}