use anyhow::Result;
use chrono::prelude::*;
use serde_json::json;
const SIGMF_VERSION: &str = "1.2.3";
const SIGMF_RECORDER: &str = concat!("Maia SDR v", env!("CARGO_PKG_VERSION"));
#[derive(Debug, Clone, PartialEq)]
pub struct Metadata {
datatype: Datatype,
sample_rate: f64,
description: String,
author: String,
frequency: f64,
datetime: DateTime<Utc>,
geolocation: Option<GeoJsonPoint>,
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct Datatype {
pub field: Field,
pub format: SampleFormat,
}
impl std::fmt::Display for Datatype {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
let field = match self.field {
Field::Real => "r",
Field::Complex => "c",
};
let (format, endianness) = match self.format {
SampleFormat::F32(e) => ("f32", Some(e)),
SampleFormat::F64(e) => ("f64", Some(e)),
SampleFormat::I32(e) => ("i32", Some(e)),
SampleFormat::I16(e) => ("i16", Some(e)),
SampleFormat::U32(e) => ("u32", Some(e)),
SampleFormat::U16(e) => ("u16", Some(e)),
SampleFormat::I8 => ("i8", None),
SampleFormat::U8 => ("u8", None),
};
let endianness = match endianness {
Some(e) => match e {
Endianness::Le => "_le",
Endianness::Be => "_be",
},
None => "",
};
write!(f, "{field}{format}{endianness}")
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum Field {
Real,
Complex,
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum SampleFormat {
F32(Endianness),
F64(Endianness),
I32(Endianness),
I16(Endianness),
U32(Endianness),
U16(Endianness),
I8,
U8,
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum Endianness {
Le,
Be,
}
impl From<maia_json::RecorderMode> for Datatype {
fn from(value: maia_json::RecorderMode) -> Datatype {
match value {
maia_json::RecorderMode::IQ8bit => Datatype {
field: Field::Complex,
format: SampleFormat::I8,
},
maia_json::RecorderMode::IQ12bit | maia_json::RecorderMode::IQ16bit => Datatype {
field: Field::Complex,
format: SampleFormat::I16(Endianness::Le),
},
}
}
}
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct GeoJsonPoint {
latitude: f64,
longitude: f64,
altitude: Option<f64>,
}
impl TryFrom<maia_json::Geolocation> for GeoJsonPoint {
type Error = anyhow::Error;
fn try_from(value: maia_json::Geolocation) -> Result<GeoJsonPoint> {
GeoJsonPoint::from_lat_lon_alt_option(value.latitude, value.longitude, value.altitude)
}
}
impl From<GeoJsonPoint> for maia_json::Geolocation {
fn from(value: GeoJsonPoint) -> maia_json::Geolocation {
maia_json::Geolocation {
altitude: value.altitude,
latitude: value.latitude,
longitude: value.longitude,
}
}
}
impl GeoJsonPoint {
pub fn from_lat_lon(latitude: f64, longitude: f64) -> Result<GeoJsonPoint> {
GeoJsonPoint::from_lat_lon_alt_option(latitude, longitude, None)
}
pub fn from_lat_lon_alt(latitude: f64, longitude: f64, altitude: f64) -> Result<GeoJsonPoint> {
GeoJsonPoint::from_lat_lon_alt_option(latitude, longitude, Some(altitude))
}
pub fn from_lat_lon_alt_option(
latitude: f64,
longitude: f64,
altitude: Option<f64>,
) -> Result<GeoJsonPoint> {
anyhow::ensure!(
(-90.0..=90.0).contains(&latitude),
"latitude is not between -90 and +90 degrees"
);
anyhow::ensure!(
(-180.0..=180.0).contains(&longitude),
"longitude is not between -180 and +180 degrees"
);
Ok(GeoJsonPoint {
latitude,
longitude,
altitude,
})
}
pub fn latitude(&self) -> f64 {
self.latitude
}
pub fn longitude(&self) -> f64 {
self.longitude
}
pub fn altitude(&self) -> Option<f64> {
self.altitude
}
pub fn to_json_value(&self) -> serde_json::Value {
if let Some(altitude) = self.altitude {
json!({
"type": "Point",
"coordinates": [self.longitude, self.latitude, altitude]
})
} else {
json!({
"type": "Point",
"coordinates": [self.longitude, self.latitude]
})
}
}
}
impl Metadata {
pub fn new(datatype: Datatype, sample_rate: f64, frequency: f64) -> Metadata {
Metadata {
datatype,
sample_rate,
description: String::new(),
author: String::new(),
frequency,
datetime: Utc::now(),
geolocation: None,
}
}
pub fn datatype(&self) -> Datatype {
self.datatype
}
pub fn set_datatype(&mut self, datatype: Datatype) {
self.datatype = datatype;
}
pub fn sample_rate(&self) -> f64 {
self.sample_rate
}
pub fn set_sample_rate(&mut self, sample_rate: f64) {
self.sample_rate = sample_rate;
}
pub fn description(&self) -> &str {
&self.description
}
pub fn set_description(&mut self, description: &str) {
self.description.replace_range(.., description);
}
pub fn author(&self) -> &str {
&self.author
}
pub fn set_author(&mut self, author: &str) {
self.author.replace_range(.., author);
}
pub fn frequency(&self) -> f64 {
self.frequency
}
pub fn geolocation(&self) -> Option<GeoJsonPoint> {
self.geolocation
}
pub fn set_frequency(&mut self, frequency: f64) {
self.frequency = frequency;
}
pub fn datetime(&self) -> DateTime<Utc> {
self.datetime
}
pub fn set_datetime(&mut self, datetime: DateTime<Utc>) {
self.datetime = datetime;
}
pub fn set_datetime_now(&mut self) {
self.set_datetime(Utc::now());
}
pub fn set_geolocation(&mut self, geolocation: GeoJsonPoint) {
self.geolocation = Some(geolocation);
}
pub fn remove_geolocation(&mut self) {
self.geolocation = None;
}
pub fn set_geolocation_optional(&mut self, geolocation: Option<GeoJsonPoint>) {
self.geolocation = geolocation;
}
pub fn to_json(&self) -> String {
let json = self.to_json_value();
let mut s = serde_json::to_string_pretty(&json).unwrap();
s.push('\n'); s
}
pub fn to_json_value(&self) -> serde_json::Value {
let mut global = json!({
"core:datatype": self.datatype.to_string(),
"core:version": SIGMF_VERSION,
"core:sample_rate": self.sample_rate,
"core:description": self.description,
"core:author": self.author,
"core:recorder": SIGMF_RECORDER
});
if let Some(geolocation) = self.geolocation() {
global
.as_object_mut()
.unwrap()
.insert("core:geolocation".to_string(), geolocation.to_json_value());
}
json!({
"global": global,
"captures": [
{
"core:sample_start": 0,
"core:frequency": self.frequency,
"core:datetime": self.datetime.to_rfc3339_opts(SecondsFormat::Millis, true)
}
],
"annotations": []
})
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn to_json() {
let meta = Metadata {
datatype: Datatype {
field: Field::Complex,
format: SampleFormat::I16(Endianness::Le),
},
sample_rate: 30.72e6,
description: "Test SigMF dataset".to_string(),
author: "Tester".to_string(),
frequency: 2400e6,
datetime: Utc.with_ymd_and_hms(2022, 11, 1, 0, 0, 0).unwrap(),
geolocation: None,
};
let json = meta.to_json();
let expected = [
r#"{
"annotations": [],
"captures": [
{
"core:datetime": "2022-11-01T00:00:00.000Z",
"core:frequency": 2400000000.0,
"core:sample_start": 0
}
],
"global": {
"core:author": "Tester",
"core:datatype": "ci16_le",
"core:description": "Test SigMF dataset",
"core:recorder": ""#,
SIGMF_RECORDER,
r#"",
"core:sample_rate": 30720000.0,
"core:version": ""#,
SIGMF_VERSION,
r#""
}
}
"#,
]
.join("");
assert_eq!(json, expected);
}
#[test]
fn to_json_with_geolocation() {
let meta = Metadata {
datatype: Datatype {
field: Field::Complex,
format: SampleFormat::I16(Endianness::Le),
},
sample_rate: 30.72e6,
description: "Test SigMF dataset with geolocation".to_string(),
author: "Tester".to_string(),
frequency: 2400e6,
datetime: Utc.with_ymd_and_hms(2022, 11, 1, 0, 0, 0).unwrap(),
geolocation: Some(
GeoJsonPoint::from_lat_lon_alt(34.0787916, -107.6183682, 2120.0).unwrap(),
),
};
let json = meta.to_json();
let expected = [
r#"{
"annotations": [],
"captures": [
{
"core:datetime": "2022-11-01T00:00:00.000Z",
"core:frequency": 2400000000.0,
"core:sample_start": 0
}
],
"global": {
"core:author": "Tester",
"core:datatype": "ci16_le",
"core:description": "Test SigMF dataset with geolocation",
"core:geolocation": {
"coordinates": [
-107.6183682,
34.0787916,
2120.0
],
"type": "Point"
},
"core:recorder": ""#,
SIGMF_RECORDER,
r#"",
"core:sample_rate": 30720000.0,
"core:version": ""#,
SIGMF_VERSION,
r#""
}
}
"#,
]
.join("");
assert_eq!(json, expected);
}
}