maia_httpd/
sigmf.rs

1//! SigMF format.
2//!
3//! This module contains a minimal implementation of [SigMF](https://github.com/gnuradio/SigMF/).
4
5use anyhow::Result;
6use chrono::prelude::*;
7use serde_json::json;
8
9const SIGMF_VERSION: &str = "1.2.3";
10const SIGMF_RECORDER: &str = concat!("Maia SDR v", env!("CARGO_PKG_VERSION"));
11
12/// SigMF metadata.
13///
14/// This structure can be used to create and edit SigMF metadata, and convert it
15/// to JSON format for its storage in a `.sigmf-meta` file.
16///
17/// # Examples
18/// ```
19/// use maia_httpd::sigmf::{Datatype, Field, Metadata, SampleFormat};
20/// let datatype = Datatype { field: Field::Complex, format: SampleFormat::I8 };
21/// let sample_rate = 1e6; // 1 Msps
22/// let frequency = 100e6; // 100 MHz
23/// let metadata = Metadata::new(datatype, sample_rate, frequency);
24/// println!("{}", metadata.to_json());
25/// ```
26#[derive(Debug, Clone, PartialEq)]
27pub struct Metadata {
28    datatype: Datatype,
29    sample_rate: f64,
30    description: String,
31    author: String,
32    frequency: f64,
33    datetime: DateTime<Utc>,
34    geolocation: Option<GeoJsonPoint>,
35}
36
37/// SigMF datatype.
38///
39/// A datatype is formed by a field, which can be either real or complex, and a
40/// sample format.
41#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
42pub struct Datatype {
43    /// Datatype field.
44    ///
45    /// This indicates if the signal is complex (IQ) or real.
46    pub field: Field,
47    /// Datatype sample format.
48    ///
49    /// The sample format indicates the width and format (floating point or
50    /// integer) of the samples.
51    pub format: SampleFormat,
52}
53
54impl std::fmt::Display for Datatype {
55    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
56        let field = match self.field {
57            Field::Real => "r",
58            Field::Complex => "c",
59        };
60        let (format, endianness) = match self.format {
61            SampleFormat::F32(e) => ("f32", Some(e)),
62            SampleFormat::F64(e) => ("f64", Some(e)),
63            SampleFormat::I32(e) => ("i32", Some(e)),
64            SampleFormat::I16(e) => ("i16", Some(e)),
65            SampleFormat::U32(e) => ("u32", Some(e)),
66            SampleFormat::U16(e) => ("u16", Some(e)),
67            SampleFormat::I8 => ("i8", None),
68            SampleFormat::U8 => ("u8", None),
69        };
70        let endianness = match endianness {
71            Some(e) => match e {
72                Endianness::Le => "_le",
73                Endianness::Be => "_be",
74            },
75            None => "",
76        };
77        write!(f, "{field}{format}{endianness}")
78    }
79}
80
81/// Datatype field.
82///
83/// A datatype [field](https://en.wikipedia.org/wiki/Field_(mathematics)) is used
84/// to indicate if the signal is complex (IQ) or real.
85#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
86pub enum Field {
87    /// Real field.
88    Real,
89    /// Complex field.
90    Complex,
91}
92
93/// Sample format.
94///
95/// The sample format indicates the width and type (floating point or integer)
96/// of the numbers used to represent the signal samples.
97#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
98pub enum SampleFormat {
99    /// 32-bit IEEE 754 floating point.
100    F32(Endianness),
101    /// 64-bit IEEE 754 floating point.
102    F64(Endianness),
103    /// 32-bit signed integer.
104    I32(Endianness),
105    /// 16-bit signed integer.
106    I16(Endianness),
107    /// 32-bit unsigned integer.
108    U32(Endianness),
109    /// 16-bit unsigned integer.
110    U16(Endianness),
111    /// 8-bit signed integer.
112    I8,
113    /// 8-bit unsigned integer.
114    U8,
115}
116
117/// Endianness.
118///
119/// The endianness indicates the order of the bytes forming a multi-byte number
120/// in memory or in a file.
121#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
122pub enum Endianness {
123    /// Little-endian.
124    Le,
125    /// Big-endian.
126    Be,
127}
128
129impl From<maia_json::RecorderMode> for Datatype {
130    fn from(value: maia_json::RecorderMode) -> Datatype {
131        match value {
132            maia_json::RecorderMode::IQ8bit => Datatype {
133                field: Field::Complex,
134                format: SampleFormat::I8,
135            },
136            maia_json::RecorderMode::IQ12bit | maia_json::RecorderMode::IQ16bit => Datatype {
137                field: Field::Complex,
138                format: SampleFormat::I16(Endianness::Le),
139            },
140        }
141    }
142}
143
144/// GeoJSON point.
145///
146/// This struct represents a GeoJSON point, which contains a latitude and
147/// longitude with respect to the WGS84 ellipsoid, and an optional altitude.
148#[derive(Debug, Copy, Clone, PartialEq)]
149pub struct GeoJsonPoint {
150    latitude: f64,
151    longitude: f64,
152    altitude: Option<f64>,
153}
154
155impl TryFrom<maia_json::Geolocation> for GeoJsonPoint {
156    type Error = anyhow::Error;
157
158    fn try_from(value: maia_json::Geolocation) -> Result<GeoJsonPoint> {
159        GeoJsonPoint::from_lat_lon_alt_option(value.latitude, value.longitude, value.altitude)
160    }
161}
162
163impl From<GeoJsonPoint> for maia_json::Geolocation {
164    fn from(value: GeoJsonPoint) -> maia_json::Geolocation {
165        maia_json::Geolocation {
166            altitude: value.altitude,
167            latitude: value.latitude,
168            longitude: value.longitude,
169        }
170    }
171}
172
173impl GeoJsonPoint {
174    /// Creates a GeoJSON point from a latitude and longitude.
175    ///
176    /// The latitude is given in degrees, between -90 and 90. The longitude is
177    /// given in degrees, between -180 and 180. An error is returned if the
178    /// values are out of range.
179    pub fn from_lat_lon(latitude: f64, longitude: f64) -> Result<GeoJsonPoint> {
180        GeoJsonPoint::from_lat_lon_alt_option(latitude, longitude, None)
181    }
182
183    /// Creates a GeoJSON point from a latitude, longitude and altitude.
184    ///
185    /// The latitude is given in degrees, between -90 and 90. The longitude is
186    /// given in degrees, between -180 and 180. The altitude is given in
187    /// meters. An error is returned if the values are out of range.
188    pub fn from_lat_lon_alt(latitude: f64, longitude: f64, altitude: f64) -> Result<GeoJsonPoint> {
189        GeoJsonPoint::from_lat_lon_alt_option(latitude, longitude, Some(altitude))
190    }
191
192    /// Creates a GeoJSON point from a latitude, longitude and an optional
193    /// altitude.
194    ///
195    /// The latitude is given in degrees, between -90 and 90. The longitude is
196    /// given in degrees, between -180 and 180. The altitude is given in
197    /// meters. An error is returned if the values are out of range.
198    pub fn from_lat_lon_alt_option(
199        latitude: f64,
200        longitude: f64,
201        altitude: Option<f64>,
202    ) -> Result<GeoJsonPoint> {
203        anyhow::ensure!(
204            (-90.0..=90.0).contains(&latitude),
205            "latitude is not between -90 and +90 degrees"
206        );
207        anyhow::ensure!(
208            (-180.0..=180.0).contains(&longitude),
209            "longitude is not between -180 and +180 degrees"
210        );
211        Ok(GeoJsonPoint {
212            latitude,
213            longitude,
214            altitude,
215        })
216    }
217
218    /// Gives the latitude of the GeoJSON point in degrees.
219    pub fn latitude(&self) -> f64 {
220        self.latitude
221    }
222
223    /// Gives the longitude of the GeoJSON point in degrees.
224    pub fn longitude(&self) -> f64 {
225        self.longitude
226    }
227
228    /// Gives the altitude of the GeoJSON point.
229    ///
230    /// The altitude is returned in meters, or `None` if the point does not
231    /// contain an altitude.
232    pub fn altitude(&self) -> Option<f64> {
233        self.altitude
234    }
235
236    /// Returns a JSON [`serde_json::Value`] that represents the GeoJSON point in JSON.
237    ///
238    /// The formatting of the JSON is compliant with the SigMF standard.
239    pub fn to_json_value(&self) -> serde_json::Value {
240        if let Some(altitude) = self.altitude {
241            json!({
242                "type": "Point",
243                "coordinates": [self.longitude, self.latitude, altitude]
244            })
245        } else {
246            json!({
247                "type": "Point",
248                "coordinates": [self.longitude, self.latitude]
249            })
250        }
251    }
252}
253
254impl Metadata {
255    /// Creates a new SigMF metadata object.
256    ///
257    /// The datatype, sample rate and frequency are mandatory parameters. The
258    /// datetime field is set to the current time. The description and author
259    /// fields are initialized to empty strings.
260    pub fn new(datatype: Datatype, sample_rate: f64, frequency: f64) -> Metadata {
261        Metadata {
262            datatype,
263            sample_rate,
264            description: String::new(),
265            author: String::new(),
266            frequency,
267            datetime: Utc::now(),
268            geolocation: None,
269        }
270    }
271
272    /// Gives the value of the datatype field.
273    pub fn datatype(&self) -> Datatype {
274        self.datatype
275    }
276
277    /// Sets the value datatype field.
278    pub fn set_datatype(&mut self, datatype: Datatype) {
279        self.datatype = datatype;
280    }
281
282    /// Gives the value of the sample rate field (in samples per second).
283    pub fn sample_rate(&self) -> f64 {
284        self.sample_rate
285    }
286
287    /// Sets the value of the sample rate field.
288    pub fn set_sample_rate(&mut self, sample_rate: f64) {
289        self.sample_rate = sample_rate;
290    }
291
292    /// Gives the value of the description field.
293    pub fn description(&self) -> &str {
294        &self.description
295    }
296
297    /// Sets the value of the description field.
298    pub fn set_description(&mut self, description: &str) {
299        self.description.replace_range(.., description);
300    }
301
302    /// Gives the value of the author field.
303    pub fn author(&self) -> &str {
304        &self.author
305    }
306
307    /// Sets the value of the author field.
308    pub fn set_author(&mut self, author: &str) {
309        self.author.replace_range(.., author);
310    }
311
312    /// Gives the value of the frequency field (in Hz).
313    pub fn frequency(&self) -> f64 {
314        self.frequency
315    }
316
317    /// Gives the value of the geolocation field.
318    pub fn geolocation(&self) -> Option<GeoJsonPoint> {
319        self.geolocation
320    }
321
322    /// Sets the value of the frequency field.
323    pub fn set_frequency(&mut self, frequency: f64) {
324        self.frequency = frequency;
325    }
326
327    /// Gives the value of the datetime field.
328    pub fn datetime(&self) -> DateTime<Utc> {
329        self.datetime
330    }
331
332    /// Sets the value of the datetime field.
333    pub fn set_datetime(&mut self, datetime: DateTime<Utc>) {
334        self.datetime = datetime;
335    }
336
337    /// Sets the datetime field to the current time.
338    pub fn set_datetime_now(&mut self) {
339        self.set_datetime(Utc::now());
340    }
341
342    /// Sets the value of the geolocation field.
343    pub fn set_geolocation(&mut self, geolocation: GeoJsonPoint) {
344        self.geolocation = Some(geolocation);
345    }
346
347    /// Removes the geolocation field.
348    pub fn remove_geolocation(&mut self) {
349        self.geolocation = None;
350    }
351
352    /// Sets or removes the value of the geolocation field.
353    ///
354    /// If `geolocation` is `Some`, then the value of the geolocation field is
355    /// set. Otherwise, the value is cleared.
356    pub fn set_geolocation_optional(&mut self, geolocation: Option<GeoJsonPoint>) {
357        self.geolocation = geolocation;
358    }
359
360    /// Returns a string that represents the metadata in JSON.
361    ///
362    /// The formatting of the JSON is compliant with the SigMF standard.
363    pub fn to_json(&self) -> String {
364        let json = self.to_json_value();
365        let mut s = serde_json::to_string_pretty(&json).unwrap();
366        s.push('\n'); // to_string_pretty does not include a final \n
367        s
368    }
369
370    /// Returns a JSON [`serde_json::Value`] that represents the metadata in JSON.
371    ///
372    /// The formatting of the JSON is compliant with the SigMF standard.
373    pub fn to_json_value(&self) -> serde_json::Value {
374        let mut global = json!({
375            "core:datatype": self.datatype.to_string(),
376            "core:version": SIGMF_VERSION,
377            "core:sample_rate": self.sample_rate,
378            "core:description": self.description,
379            "core:author": self.author,
380            "core:recorder": SIGMF_RECORDER
381        });
382        if let Some(geolocation) = self.geolocation() {
383            global
384                .as_object_mut()
385                .unwrap()
386                .insert("core:geolocation".to_string(), geolocation.to_json_value());
387        }
388        json!({
389            "global": global,
390            "captures": [
391                {
392                    "core:sample_start": 0,
393                    "core:frequency": self.frequency,
394                    "core:datetime": self.datetime.to_rfc3339_opts(SecondsFormat::Millis, true)
395                }
396            ],
397            "annotations": []
398        })
399    }
400}
401
402#[cfg(test)]
403mod test {
404    use super::*;
405
406    #[test]
407    fn to_json() {
408        let meta = Metadata {
409            datatype: Datatype {
410                field: Field::Complex,
411                format: SampleFormat::I16(Endianness::Le),
412            },
413            sample_rate: 30.72e6,
414            description: "Test SigMF dataset".to_string(),
415            author: "Tester".to_string(),
416            frequency: 2400e6,
417            datetime: Utc.with_ymd_and_hms(2022, 11, 1, 0, 0, 0).unwrap(),
418            geolocation: None,
419        };
420        let json = meta.to_json();
421        let expected = [
422            r#"{
423  "annotations": [],
424  "captures": [
425    {
426      "core:datetime": "2022-11-01T00:00:00.000Z",
427      "core:frequency": 2400000000.0,
428      "core:sample_start": 0
429    }
430  ],
431  "global": {
432    "core:author": "Tester",
433    "core:datatype": "ci16_le",
434    "core:description": "Test SigMF dataset",
435    "core:recorder": ""#,
436            SIGMF_RECORDER,
437            r#"",
438    "core:sample_rate": 30720000.0,
439    "core:version": ""#,
440            SIGMF_VERSION,
441            r#""
442  }
443}
444"#,
445        ]
446        .join("");
447        assert_eq!(json, expected);
448    }
449
450    #[test]
451    fn to_json_with_geolocation() {
452        let meta = Metadata {
453            datatype: Datatype {
454                field: Field::Complex,
455                format: SampleFormat::I16(Endianness::Le),
456            },
457            sample_rate: 30.72e6,
458            description: "Test SigMF dataset with geolocation".to_string(),
459            author: "Tester".to_string(),
460            frequency: 2400e6,
461            datetime: Utc.with_ymd_and_hms(2022, 11, 1, 0, 0, 0).unwrap(),
462            geolocation: Some(
463                GeoJsonPoint::from_lat_lon_alt(34.0787916, -107.6183682, 2120.0).unwrap(),
464            ),
465        };
466        let json = meta.to_json();
467        let expected = [
468            r#"{
469  "annotations": [],
470  "captures": [
471    {
472      "core:datetime": "2022-11-01T00:00:00.000Z",
473      "core:frequency": 2400000000.0,
474      "core:sample_start": 0
475    }
476  ],
477  "global": {
478    "core:author": "Tester",
479    "core:datatype": "ci16_le",
480    "core:description": "Test SigMF dataset with geolocation",
481    "core:geolocation": {
482      "coordinates": [
483        -107.6183682,
484        34.0787916,
485        2120.0
486      ],
487      "type": "Point"
488    },
489    "core:recorder": ""#,
490            SIGMF_RECORDER,
491            r#"",
492    "core:sample_rate": 30720000.0,
493    "core:version": ""#,
494            SIGMF_VERSION,
495            r#""
496  }
497}
498"#,
499        ]
500        .join("");
501        assert_eq!(json, expected);
502    }
503}