1use 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#[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#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
42pub struct Datatype {
43 pub field: Field,
47 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#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
86pub enum Field {
87 Real,
89 Complex,
91}
92
93#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
98pub enum SampleFormat {
99 F32(Endianness),
101 F64(Endianness),
103 I32(Endianness),
105 I16(Endianness),
107 U32(Endianness),
109 U16(Endianness),
111 I8,
113 U8,
115}
116
117#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
122pub enum Endianness {
123 Le,
125 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#[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 pub fn from_lat_lon(latitude: f64, longitude: f64) -> Result<GeoJsonPoint> {
180 GeoJsonPoint::from_lat_lon_alt_option(latitude, longitude, None)
181 }
182
183 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 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 pub fn latitude(&self) -> f64 {
220 self.latitude
221 }
222
223 pub fn longitude(&self) -> f64 {
225 self.longitude
226 }
227
228 pub fn altitude(&self) -> Option<f64> {
233 self.altitude
234 }
235
236 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 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 pub fn datatype(&self) -> Datatype {
274 self.datatype
275 }
276
277 pub fn set_datatype(&mut self, datatype: Datatype) {
279 self.datatype = datatype;
280 }
281
282 pub fn sample_rate(&self) -> f64 {
284 self.sample_rate
285 }
286
287 pub fn set_sample_rate(&mut self, sample_rate: f64) {
289 self.sample_rate = sample_rate;
290 }
291
292 pub fn description(&self) -> &str {
294 &self.description
295 }
296
297 pub fn set_description(&mut self, description: &str) {
299 self.description.replace_range(.., description);
300 }
301
302 pub fn author(&self) -> &str {
304 &self.author
305 }
306
307 pub fn set_author(&mut self, author: &str) {
309 self.author.replace_range(.., author);
310 }
311
312 pub fn frequency(&self) -> f64 {
314 self.frequency
315 }
316
317 pub fn geolocation(&self) -> Option<GeoJsonPoint> {
319 self.geolocation
320 }
321
322 pub fn set_frequency(&mut self, frequency: f64) {
324 self.frequency = frequency;
325 }
326
327 pub fn datetime(&self) -> DateTime<Utc> {
329 self.datetime
330 }
331
332 pub fn set_datetime(&mut self, datetime: DateTime<Utc>) {
334 self.datetime = datetime;
335 }
336
337 pub fn set_datetime_now(&mut self) {
339 self.set_datetime(Utc::now());
340 }
341
342 pub fn set_geolocation(&mut self, geolocation: GeoJsonPoint) {
344 self.geolocation = Some(geolocation);
345 }
346
347 pub fn remove_geolocation(&mut self) {
349 self.geolocation = None;
350 }
351
352 pub fn set_geolocation_optional(&mut self, geolocation: Option<GeoJsonPoint>) {
357 self.geolocation = geolocation;
358 }
359
360 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'); s
368 }
369
370 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}