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}