From 7c688e258b6738fcc5ea81772dc1ecb1fca92a7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Fejoz?= Date: Fri, 1 Mar 2024 09:04:35 +0100 Subject: [PATCH] Support for SigMF (#11) Introduce initial support for SigMF. Add `sigmf-hash` and `sigmf-col` utility to manipulate SigMF files. --- .vscode/launch.json | 328 ++++++++++++ Cargo.toml | 21 +- Makefile | 7 + crates/sigmf-utilities/Cargo.toml | 42 ++ crates/sigmf-utilities/README.md | 39 ++ crates/sigmf-utilities/src/sigmf_col.rs | 102 ++++ crates/sigmf-utilities/src/sigmf_convert.rs | 92 ++++ crates/sigmf-utilities/src/sigmf_hash.rs | 94 ++++ crates/sigmf/Cargo.toml | 36 ++ crates/sigmf/Makefile | 9 + crates/sigmf/README.md | 9 + crates/sigmf/quickcheck-test.sh | 9 + crates/sigmf/samples/index.sigmf-meta | 11 + crates/sigmf/samples/test1.sigmf-data | Bin 0 -> 2048 bytes crates/sigmf/samples/test1.sigmf-meta | 21 + crates/sigmf/src/annotation.rs | 30 ++ crates/sigmf/src/antenna_extension.rs | 18 + crates/sigmf/src/capture.rs | 41 ++ crates/sigmf/src/collection.rs | 41 ++ crates/sigmf/src/dataset_format.rs | 546 ++++++++++++++++++++ crates/sigmf/src/description.rs | 248 +++++++++ crates/sigmf/src/errors.rs | 17 + crates/sigmf/src/extension.rs | 9 + crates/sigmf/src/global.rs | 124 +++++ crates/sigmf/src/lib.rs | 35 ++ crates/sigmf/src/recording.rs | 118 +++++ crates/sigmf/tests/dataset_format.rs | 54 ++ crates/sigmf/tests/description.rs | 34 ++ crates/sigmf/tests/sigmf_meta.rs | 182 +++++++ rust-toolchain.toml | 2 + src/lib.rs | 5 + src/serde_pmt/deserialiser.rs | 447 ++++++++++++++++ src/serde_pmt/error.rs | 39 ++ src/serde_pmt/mod.rs | 24 + src/serde_pmt/serialiser.rs | 479 +++++++++++++++++ src/sigmf/mod.rs | 131 +++++ src/sigmf/sigmf_sink.rs | 267 ++++++++++ src/sigmf/sigmf_source.rs | 257 +++++++++ src/type_converters.rs | 66 ++- structure.png | Bin 0 -> 17394 bytes tests/serde_pmt.rs | 160 ++++++ tests/sigmf/mod.rs | 3 + tests/sigmf/sigmf_graph.rs | 172 ++++++ tests/sigmf/sigmf_sink.rs | 1 + tests/sigmf/sigmf_source.rs | 104 ++++ tests/tests.rs | 2 + 46 files changed, 4463 insertions(+), 13 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 Makefile create mode 100644 crates/sigmf-utilities/Cargo.toml create mode 100644 crates/sigmf-utilities/README.md create mode 100644 crates/sigmf-utilities/src/sigmf_col.rs create mode 100644 crates/sigmf-utilities/src/sigmf_convert.rs create mode 100644 crates/sigmf-utilities/src/sigmf_hash.rs create mode 100644 crates/sigmf/Cargo.toml create mode 100644 crates/sigmf/Makefile create mode 100644 crates/sigmf/README.md create mode 100755 crates/sigmf/quickcheck-test.sh create mode 100644 crates/sigmf/samples/index.sigmf-meta create mode 100644 crates/sigmf/samples/test1.sigmf-data create mode 100644 crates/sigmf/samples/test1.sigmf-meta create mode 100644 crates/sigmf/src/annotation.rs create mode 100644 crates/sigmf/src/antenna_extension.rs create mode 100644 crates/sigmf/src/capture.rs create mode 100644 crates/sigmf/src/collection.rs create mode 100644 crates/sigmf/src/dataset_format.rs create mode 100644 crates/sigmf/src/description.rs create mode 100644 crates/sigmf/src/errors.rs create mode 100644 crates/sigmf/src/extension.rs create mode 100644 crates/sigmf/src/global.rs create mode 100644 crates/sigmf/src/lib.rs create mode 100644 crates/sigmf/src/recording.rs create mode 100644 crates/sigmf/tests/dataset_format.rs create mode 100644 crates/sigmf/tests/description.rs create mode 100644 crates/sigmf/tests/sigmf_meta.rs create mode 100644 rust-toolchain.toml create mode 100644 src/serde_pmt/deserialiser.rs create mode 100644 src/serde_pmt/error.rs create mode 100644 src/serde_pmt/mod.rs create mode 100644 src/serde_pmt/serialiser.rs create mode 100644 src/sigmf/mod.rs create mode 100644 src/sigmf/sigmf_sink.rs create mode 100644 src/sigmf/sigmf_source.rs create mode 100644 structure.png create mode 100644 tests/serde_pmt.rs create mode 100644 tests/sigmf/mod.rs create mode 100644 tests/sigmf/sigmf_graph.rs create mode 100644 tests/sigmf/sigmf_sink.rs create mode 100644 tests/sigmf/sigmf_source.rs diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..6c5498d --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,328 @@ +{ + // Utilisez IntelliSense pour en savoir plus sur les attributs possibles. + // Pointez pour afficher la description des attributs existants. + // Pour plus d'informations, visitez : https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in library 'fsdr-blocks'", + "cargo": { + "args": [ + "test", + "--no-run", + "--lib", + "--package=fsdr-blocks" + ], + "filter": { + "name": "fsdr-blocks", + "kind": "lib" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug integration test 'type_converters'", + "cargo": { + "args": [ + "test", + "--no-run", + "--test=type_converters", + "--package=fsdr-blocks" + ], + "filter": { + "name": "type_converters", + "kind": "test" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug integration test 'tests'", + "cargo": { + "args": [ + "test", + "--no-run", + "--test=tests", + "--package=fsdr-blocks" + ], + "filter": { + "name": "tests", + "kind": "test" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug benchmark 'crossbeam_sink'", + "cargo": { + "args": [ + "test", + "--no-run", + "--bench=crossbeam_sink", + "--package=fsdr-blocks" + ], + "filter": { + "name": "crossbeam_sink", + "kind": "bench" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug benchmark 'crossbeam_source'", + "cargo": { + "args": [ + "test", + "--no-run", + "--bench=crossbeam_source", + "--package=fsdr-blocks" + ], + "filter": { + "name": "crossbeam_source", + "kind": "bench" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug benchmark 'cw_to_char'", + "cargo": { + "args": [ + "test", + "--no-run", + "--bench=cw_to_char", + "--package=fsdr-blocks" + ], + "filter": { + "name": "cw_to_char", + "kind": "bench" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug benchmark 'bb_to_cw'", + "cargo": { + "args": [ + "test", + "--no-run", + "--bench=bb_to_cw", + "--package=fsdr-blocks" + ], + "filter": { + "name": "bb_to_cw", + "kind": "bench" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug benchmark 'shared'", + "cargo": { + "args": [ + "test", + "--no-run", + "--bench=shared", + "--package=fsdr-blocks" + ], + "filter": { + "name": "shared", + "kind": "bench" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in library 'sigmf'", + "cargo": { + "args": [ + "test", + "--no-run", + "--lib", + "--package=sigmf" + ], + "filter": { + "name": "sigmf", + "kind": "lib" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug integration test 'description'", + "cargo": { + "args": [ + "test", + "--no-run", + "--test=description", + "--package=sigmf" + ], + "filter": { + "name": "description", + "kind": "test" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug integration test 'dataset_format'", + "cargo": { + "args": [ + "test", + "--no-run", + "--test=dataset_format", + "--package=sigmf" + ], + "filter": { + "name": "dataset_format", + "kind": "test" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug integration test 'sigmf_meta'", + "cargo": { + "args": [ + "test", + "--no-run", + "--test=sigmf_meta", + "--package=sigmf" + ], + "filter": { + "name": "sigmf_meta", + "kind": "test" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug executable 'sigmf-hash'", + "cargo": { + "args": [ + "build", + "--bin=sigmf-hash", + "--package=sigmf-utilities" + ], + "filter": { + "name": "sigmf-hash", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug executable 'sigmf-col'", + "cargo": { + "args": [ + "build", + "--bin=sigmf-col", + "--package=sigmf-utilities" + ], + "filter": { + "name": "sigmf-col", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in executable 'sigmf-col'", + "cargo": { + "args": [ + "test", + "--no-run", + "--bin=sigmf-col", + "--package=sigmf-utilities" + ], + "filter": { + "name": "sigmf-col", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug executable 'sigmf-convert'", + "cargo": { + "args": [ + "build", + "--bin=sigmf-convert", + "--package=sigmf-samples" + ], + "filter": { + "name": "sigmf-convert", + "kind": "bin" + } + }, + "args": ["../crates/sigmf/samples/test1.sigmf-meta", "rf32_le", "./test2"], + "cwd": "${workspaceFolder}/crates/sigmf-utilities" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in executable 'sigmf-convert'", + "cargo": { + "args": [ + "test", + "--no-run", + "--bin=sigmf-convert", + "--package=sigmf-samples" + ], + "filter": { + "name": "sigmf-convert", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + } + ] +} \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index d5a3513..5aa0254 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,27 +12,34 @@ categories = ["asynchronous", "concurrency", "hardware-support", "science"] readme = "README.md" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[workspace] +members = [ + ".", + "crates/*", + # "examples/*", +] + [dependencies] futuresdr = { git = "https://github.com/FutureSDR/FutureSDR", branch = "main" } -#futuresdr = { version="0.0.32" } +#futuresdr = { path = "../FutureSDR" } async-trait = "0.1.68" crossbeam-channel = { version = "0.5.8", optional = true } bimap = { version = "0.6.3", optional = true } +sigmf = { version = "0.1.0", path = "crates/sigmf" } +async-fs = "2.1.0" +serde = "1.0.193" [dev-dependencies] criterion = { version = "0.4.0", features = ["html_reports"] } rand = { version = "0.8.5" } +quickcheck_macros = "1" +serde_json = "1.0.108" [features] default = [] crossbeam = ["dep:crossbeam-channel"] cw = ["dep:bimap"] -[workspace] -members = [ - ".", -] - [[bench]] name = "crossbeam_sink" path = "benches/channel/crossbeam_sink.rs" @@ -61,4 +68,4 @@ required-features = ["cw"] name = "shared" path = "benches/cw/shared.rs" harness = false -required-features = ["cw"] \ No newline at end of file +required-features = ["cw"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..b777ae6 --- /dev/null +++ b/Makefile @@ -0,0 +1,7 @@ +structure.png: + cargo structure --local -m | dot -Tpng > structure.png + +check: + ./check.sh + +.PHONY: structure.png \ No newline at end of file diff --git a/crates/sigmf-utilities/Cargo.toml b/crates/sigmf-utilities/Cargo.toml new file mode 100644 index 0000000..4b84677 --- /dev/null +++ b/crates/sigmf-utilities/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "sigmf-utilities" +version = "0.1.0" +edition = "2021" +authors = ["FutureSDR Contributors ", "Loïc Fejoz "] +license = "Apache-2.0 or MIT" +repository = "https://github.com/futuresdr/fsdr-blocks/" +description = "command-line tools to manipulate SigMF files" +keywords = ["sdr", "radio", "dsp", "sigmf", "fileformat"] +categories = ["science"] +readme = "README.md" +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +serde = "^1.0" +serde_derive = "^1.0" +serde_json = "^1.0" +thiserror = "1.0.50" +clap = { version = "4.4.7", features = ["derive"] } +anyhow = { version = "1.0.75"} +sigmf = {path="../sigmf"} +walkdir = "2.4.0" +fsdr-blocks = {path="../../"} +futuresdr = { git = "https://github.com/FutureSDR/FutureSDR", branch = "main" } +#futuresdr = {path="../../../FutureSDR"} + +[build-dependencies] +rustc_version = "0.4.0" + +[dev-dependencies] + +[[bin]] +name = "sigmf-hash" +path = "src/sigmf_hash.rs" + +[[bin]] +name = "sigmf-col" +path = "src/sigmf_col.rs" + +[[bin]] +name = "sigmf-convert" +path = "src/sigmf_convert.rs" diff --git a/crates/sigmf-utilities/README.md b/crates/sigmf-utilities/README.md new file mode 100644 index 0000000..2f25d14 --- /dev/null +++ b/crates/sigmf-utilities/README.md @@ -0,0 +1,39 @@ +# SigMF Utilities + +Some command-line utilities to manipulate SigMF compliant files: + +* [sigmf-col](#sigmf-collection) +* [sigmf-hash](#sigmf-hash) + +## SigMF Hash + +Check and update hashes on sigmf files + +Usage: ```sigmf-hash ..``` + +Commands: + +* check Verify the hash of a dataset +* update Recompute and update the hash of a dataset + +Examples: + +```sigmf-hash check samples/test1``` + +```sigmf-hash update samples/test1``` + +## SigMF Collection + +Create and updates collection of SigMF records + +Usage: ```sigmf-col ``` + +Commands: + +* create Create a collection from given SigMF files +* ~~update Update a collection~~ +* help Print this message or the help of the given subcommand(s) + +Examples: + +```sigmf-col create -o samples/index.sigmf-meta samples/*.sigmf-data``` diff --git a/crates/sigmf-utilities/src/sigmf_col.rs b/crates/sigmf-utilities/src/sigmf_col.rs new file mode 100644 index 0000000..231c608 --- /dev/null +++ b/crates/sigmf-utilities/src/sigmf_col.rs @@ -0,0 +1,102 @@ +use anyhow::{Context, Result}; +use clap::{arg, Parser, Subcommand}; +use sigmf::{DescriptionBuilder, RecordingBuilder}; +use std::path::PathBuf; + +#[derive(Parser)] +#[command(author, version, about="Create and updates collection of SigMF records", long_about = None)] +struct Cli { + #[command(subcommand)] + command: Commands, +} + +impl Cli { + pub fn execute(self) -> Result<()> { + self.command.execute() + } +} + +#[derive(Subcommand)] +enum Commands { + #[command(about="Create a collection from given SigMF files", long_about = None)] + Create { + #[arg(value_name = "FILE", short, long)] + output: Option, + // #[arg(value_name = "AUTHOR", long)] + // author: Option, + #[arg(value_name = "FILE", required = true)] + files: Vec, + }, + #[command(about="Update a collection", long_about = None)] + Update { + #[arg(value_name = "FILE")] + files: Vec, + }, +} + +impl Commands { + // pub fn author(self) -> Option { + // use Commands::*; + // match self { + // Create { author, .. } => author, + // _ => None, + // } + // } + + pub fn files(&self) -> Result<&Vec> { + use Commands::*; + match self { + Create { files, .. } => Ok(files), + _ => unreachable!(), + } + } + + fn output(&self) -> &PathBuf { + use Commands::*; + match self { + Create { output, .. } => { + if let Some(output) = output { + output + } else { + //PathBuf::from("index.sigmf-meta"); + unimplemented!() + } + } + _ => unreachable!(), + } + } + + pub fn execute(self) -> Result<()> { + use Commands::*; + match self { + Create { .. } => self.create_collection(), + _ => todo!("Not yet implemented"), + } + } + + fn create_collection(&self) -> Result<()> { + let mut collec = DescriptionBuilder::collection(); + + for a_file in self.files()? { + println!("Adding {:?}", a_file); + let record = RecordingBuilder::from(a_file).compute_sha512()?.build(); + collec.add_stream(record)?; + } + + let output = self.output(); + // output.set_extension("sigmf-meta"); + collec + // .author(self.author()) + .build()? + .create_pretty(output) + .with_context(|| format!("Error writing to {}", &output.display()))?; + Ok(()) + } +} + +fn main() { + let cli = Cli::parse(); + if let Err(err) = cli.execute() { + eprintln!("{:#}", err); + } +} diff --git a/crates/sigmf-utilities/src/sigmf_convert.rs b/crates/sigmf-utilities/src/sigmf_convert.rs new file mode 100644 index 0000000..b87363a --- /dev/null +++ b/crates/sigmf-utilities/src/sigmf_convert.rs @@ -0,0 +1,92 @@ +use clap::{arg, Parser}; +use fsdr_blocks::sigmf::DatasetFormat; +use fsdr_blocks::sigmf::DatasetFormat::*; +use fsdr_blocks::{ + sigmf::{SigMFSinkBuilder, SigMFSourceBuilder}, + type_converters::TypeConvertersBuilder, +}; +use futuresdr::blocks::TagDebug; +use futuresdr::macros::connect; +use futuresdr::{ + anyhow::{anyhow, Result}, + blocks::Apply, + runtime::{Flowgraph, Runtime}, +}; +use std::path::PathBuf; + +#[derive(Parser)] +#[command(author, version, about="Lossly Convert the type of data by going through float32", long_about = None)] +struct Cli { + #[arg(value_name = "INPUT", required = true)] + input: PathBuf, + #[arg(value_name = "DATATYPE", required = true)] + target: DatasetFormat, + #[arg(value_name = "OUTPUT", required = true)] + output: PathBuf, +} + +impl Cli { + pub async fn execute(self) -> Result<()> { + let mut fg = Flowgraph::new(); + + let mut src = SigMFSourceBuilder::from(&self.input); + let src = src.build::().await?; + let src = fg.add_block(src); + + let snk = SigMFSinkBuilder::from(self.output); + + let (conv, snk) = match self.target { + RI8 => ( + fg.add_block(TypeConvertersBuilder::lossy_scale_convert_f32_i8().build()), + fg.add_block(snk.datatype(self.target).build::().await?), + ), + RU8 => ( + fg.add_block(TypeConvertersBuilder::lossy_scale_convert_f32_u8().build()), + fg.add_block(snk.datatype(self.target).build::().await?), + ), + Rf32Be | Rf32Le => ( + fg.add_block(Apply::new(|x: &f32| *x)), + fg.add_block(snk.datatype(self.target).build::().await?), + ), + Rf64Be | Rf64Le => ( + fg.add_block(TypeConvertersBuilder::convert::().build()), + fg.add_block(snk.datatype(self.target).build::().await?), + ), + Ri16Be | Ri16Le => ( + fg.add_block(TypeConvertersBuilder::lossy_scale_convert_f32_i16().build()), + fg.add_block(snk.datatype(self.target).build::().await?), + ), + // Ri32Be | Ri32Le => ( + // fg.add_block(TypeConvertersBuilder::lossy_scale_convert_f32_i32().build()), + // fg.add_block(snk.datatype(self.target).build::().await?), + // ), + // Ru16Be | Ru16Le => { + // fg.add_block(TypeConvertersBuilder::convert::().build()) + // } + // Ru32Be | Ru32Le => { + // fg.add_block(TypeConvertersBuilder::convert::().build()) + // } + _ => return Err(anyhow!("Unsupported target type: {}", self.target)), + }; + connect!(fg, src > conv > snk); + // fg.connect_stream(src, "out", conv, "in") + // .with_context(|| "src->conv")?; + // fg.connect_stream(conv, "out", snk, "in") + // .with_context(|| "conv->snk")?; + + let tag_dbg = TagDebug::::new("debugger"); + let tag_dbg = fg.add_block(tag_dbg); + // fg.connect_stream(src, "out", tag_dbg, "in")?; + connect!(fg, src > tag_dbg); + + Runtime::new().run(fg)?; + Ok(()) + } +} + +fn main() { + let cli = Cli::parse(); + if let Err(err) = futuresdr::futures::executor::block_on(cli.execute()) { + eprintln!("{:#}", err); + } +} diff --git a/crates/sigmf-utilities/src/sigmf_hash.rs b/crates/sigmf-utilities/src/sigmf_hash.rs new file mode 100644 index 0000000..0aeaf0f --- /dev/null +++ b/crates/sigmf-utilities/src/sigmf_hash.rs @@ -0,0 +1,94 @@ +use anyhow::{Context, Result}; +use clap::{arg, Parser, Subcommand}; +use sigmf::RecordingBuilder; +use std::path::PathBuf; + +#[derive(Parser)] +#[command(author, version, about="Check and update hashes on sigmf files", long_about = None)] +struct Cli { + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand)] +enum Commands { + #[command(about="Verify the hash of a dataset", long_about = None)] + Check { + #[arg(value_name = "FILE", required = true)] + files: Vec, + }, + #[command(about="Recompute and update the hash of a dataset", long_about = None)] + Update { + #[arg(value_name = "FILE")] + files: Vec, + }, +} + +fn main() { + let cli = Cli::parse(); + use Commands::*; + match cli { + Cli { + command: Check { files }, + } => check(files), + Cli { + command: Update { files }, + } => update(files), + } +} + +fn check(files: Vec) { + for a_file in files { + if let Err(err) = check_sigmf(a_file) { + eprintln!("{:#}", err); + } + } +} + +fn check_sigmf(basename: PathBuf) -> Result<()> { + let mut record = RecordingBuilder::from(&basename) + .compute_sha512() + .with_context(|| format!("Computing sha512 of {}", basename.display()))? + .build(); + let computed_sha512 = record.hash()?.clone(); + let desc = record.load_description()?; + let expected_sha512 = desc.global()?.sha512.as_ref().expect("sha512 not present"); + if expected_sha512.eq(&computed_sha512) { + println!("Hash match"); + } else { + println!("{}", expected_sha512); + println!("{}", computed_sha512); + println!("Hash doesn't match"); + } + Ok(()) +} + +fn update(files: Vec) { + for a_file in files { + if let Err(err) = update_sigmf(a_file) { + eprintln!("{:#}", err); + } + } +} + +fn update_sigmf(basename: PathBuf) -> Result<()> { + let mut record = RecordingBuilder::from(&basename) + .compute_sha512() + .with_context(|| format!("While computing sha512 of {}", basename.display()))? + .build(); + let computed_sha512 = record.hash()?.clone(); + let mut desc = record.load_description()?; + let expected_sha512 = desc.global()?.sha512.as_ref(); + let mut need_update = true; + if let Some(expected_sha512) = expected_sha512 { + need_update = !expected_sha512.eq(&computed_sha512); + } + if need_update { + let mut basename = basename; + basename.set_extension("sigmf-meta"); + desc.global_mut()?.sha512 = Some(computed_sha512); + desc.create_pretty(&basename) + .with_context(|| format!("Error writing to {}", &basename.display()))?; + } + Ok(()) +} diff --git a/crates/sigmf/Cargo.toml b/crates/sigmf/Cargo.toml new file mode 100644 index 0000000..bf5d12f --- /dev/null +++ b/crates/sigmf/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "sigmf" +version = "0.1.0" +edition = "2021" +authors = ["FutureSDR Contributors ", "Loïc Fejoz "] +license = "Apache-2.0 or MIT" +repository = "https://github.com/futuresdr/fsdr-blocks/" +description = "Crate for interfacing to SigMF files" +keywords = ["sdr", "radio", "dsp", "sigmf", "fileformat"] +categories = ["science"] +readme = "README.md" +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[features] +default = ["quickcheck"] +quickcheck=['dep:quickcheck'] + +[dependencies] +serde = "^1.0" +serde_derive = "^1.0" +serde_json = "^1.0" +thiserror = "1.0.50" +quickcheck = {version = "1.0.3", optional = true} +uuid = {version = "1.5.0", features = ["serde"]} +sha2 = { version = "0.10.8"} +hex = { version = "0.4.3"} + +[build-dependencies] +rustc_version = "0.4.0" + +[dev-dependencies] +quickcheck_macros = "1" + +[lib] +name = "sigmf" +path = "src/lib.rs" diff --git a/crates/sigmf/Makefile b/crates/sigmf/Makefile new file mode 100644 index 0000000..25460fe --- /dev/null +++ b/crates/sigmf/Makefile @@ -0,0 +1,9 @@ +test: cargo-test quickcheck + +cargo-test: + cargo test + +quickcheck: + ./quickcheck-test.sh + +.PHONY: quickcheck cargo-test \ No newline at end of file diff --git a/crates/sigmf/README.md b/crates/sigmf/README.md new file mode 100644 index 0000000..a7c2381 --- /dev/null +++ b/crates/sigmf/README.md @@ -0,0 +1,9 @@ +SigMF Crate +=========== + +## Useful links + +* https://github.com/sigmf/SigMF +* https://www.iqengine.org/ +* https://github.com/sigmf/libsigmf/ +* https://github.com/skysafe/gr-sigmf \ No newline at end of file diff --git a/crates/sigmf/quickcheck-test.sh b/crates/sigmf/quickcheck-test.sh new file mode 100755 index 0000000..bbcdcdc --- /dev/null +++ b/crates/sigmf/quickcheck-test.sh @@ -0,0 +1,9 @@ +#!/usr/bin/bash + +while true +do + cargo test qc_ + if [[ x$? != x0 ]] ; then + exit $? + fi +done diff --git a/crates/sigmf/samples/index.sigmf-meta b/crates/sigmf/samples/index.sigmf-meta new file mode 100644 index 0000000..7781f9b --- /dev/null +++ b/crates/sigmf/samples/index.sigmf-meta @@ -0,0 +1,11 @@ +{ + "collection": { + "core:version": "1.0.0", + "core:streams": [ + { + "name": "../samples/test1.sigmf-data", + "hash": "d739c3803852bd27203d3638ef3605404a1afbbf6ab7caa04458913f3e54c2086c091e98720a5634b1b5fc3fd79cd72413581aa2934489edec5e4c6c640269b0" + } + ] + } +} \ No newline at end of file diff --git a/crates/sigmf/samples/test1.sigmf-data b/crates/sigmf/samples/test1.sigmf-data new file mode 100644 index 0000000000000000000000000000000000000000..9006ca6e292526e057d743462dfd1504b216bffc GIT binary patch literal 2048 zcmV+b2>+uHiN;IFJ2%5>?XO*nmW$nlX@2texZ7hh^4 zA2POz;jIYVb)g1fz81tYM6coI2qvX|TO26%gxPhwTEB_X zVUnyP{ExoC^yY1`S0(^mRLX#X5Q&B+I$9?FNNd@PYCd)|OJ7Jm5A^=Ks7@11VR<`g zhWs2xD`yt20b_2xRBo`O|@X4{(JB#>E!{*I{XJO9XB3G|BSfI>06`_OWkmxr*&R*eaeEx)2 zL)y?lZivyeL?O&E(nMEFGUVLu2%GPBIAX_;D@pv|?sE4m+Hgg!7wj$&k+(Gs@ov#J z3>+38UE4pntYZFu+XocXRZ=emgXL!3;IwRVN)1?&&D;)FEM-{)`k*gTLXmhnehZoVegVe3-T4QCxVNvI%>&% zTDD}AcON)Sa|rPm&I-#4queV-H{069dGRnelc)YyJ z!oWH$>PCLfEEp;zMu#`PPNFG7I8`E)ahDnbM8G4(UH)%9Qo6GjQYDQdOJvscv5sU71O|fH zZ4R@MygsXK62q_{rnn&3OzBKJH?4eHX}r+EMa^U!XWR%`1M?9KyuGRnxZ#Ei_H60Q z>*#ZeY8xbK2Pu&v<$>}QgQTu8CSL*Bu|2B+FDC;i$sUo_H{|ia;JSEST+p?i$%C-6 zc5gjpK#I-OBfD6`(mA5QjS+U81f_b{2rS8+{OooflVf2b^|_iv%7DFMu=$z0-NE0f z^W70q2spwvC?VxGc>S1ft}=ec)CF8@3#}I>>6(^H2ob^YrNi$?T9OQSqa=VpXD`i5!o}1N)LB31OU2e!E59DRbXmKMSU} zX5llY-nE-P0AqxQSV*RJ^X9B+6rxLhQ1bH>hf3QZ!9T(k0XMZAHP{6`IX#TVP5RGtY zdo-}o9>R1!r@`F@6WernXdpr5>%9VNS_ayDm%Q9y321Vq-wdd*GLy>pC=AK4L-mH>LZ#5=l7W zQwp#QDNlf^%a;EjmMS=nZ69wCh$CPCkv4m3ljoG6ix_Wy0&DNJJkANU8mzQ!(pB)_ z_t_|#;-rs+PN8SdFiTpfilR3eZ{Pv-tmKzNa=9$i3E;o!cT3wi7w3hab};(otI_w~ zB9C>@lu24Yips)~4g7J=pj(GeOF20ruiFV()bm~le_HOU`v7#~d2yIN6(%JE=Rc|mt&%9d%6fm5d$%mOL7VIXf`(Jc}J66u* zk3qDQt^nrR&U$LoaZ&!eOShZ4VY^cNoPHHx5RJRNd)s^R*K*s4?CGGs4^MHgOV-mc zx}upjxBM0b2HC$5m<33scm;VdmsnZ}`*@sc>9KY(XX1t}A+hVJwa_QeK zZ<^^Z13?h}}h(SsCN*ER7 zUd9?FX0+h*a{mX{>IXd{UCE|7QCLQayDj!{$a=1T-Jhr3C1o#t) zZwJmDQHgiSF&>Wor6bu1viUo2S)T2@CrYVr;gep(@o#)yF$7<5&ph$PJ;MxQ#m*uE ea?vo7qe?wjJd=dRjR(Z5cG~~qyKtr|7`I|G#`8}A literal 0 HcmV?d00001 diff --git a/crates/sigmf/samples/test1.sigmf-meta b/crates/sigmf/samples/test1.sigmf-meta new file mode 100644 index 0000000..c96a1b9 --- /dev/null +++ b/crates/sigmf/samples/test1.sigmf-meta @@ -0,0 +1,21 @@ +{ + "global": { + "core:datatype": "ru8", + "core:version": "1.0.0", + "core:sha512": "d739c3803852bd27203d3638ef3605404a1afbbf6ab7caa04458913f3e54c2086c091e98720a5634b1b5fc3fd79cd72413581aa2934489edec5e4c6c640269b0" + }, + "captures": [], + "annotations": [ + { + "core:sample_start": 0, + "core:sample_count": 512, + "core:label": "first 512 random bytes", + "core:comment": "some comment" + }, + { + "core:sample_start": 512, + "core:sample_count": 512, + "core:label": "second part of 512 random bytes" + } + ] +} \ No newline at end of file diff --git a/crates/sigmf/src/annotation.rs b/crates/sigmf/src/annotation.rs new file mode 100644 index 0000000..52f2d98 --- /dev/null +++ b/crates/sigmf/src/annotation.rs @@ -0,0 +1,30 @@ +use serde_json::Value; +use std::collections::HashMap; + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Default)] +pub struct Annotation { + #[serde(rename = "core:sample_start")] + pub sample_start: Option, + #[serde(rename = "core:sample_count", skip_serializing_if = "Option::is_none")] + pub sample_count: Option, + #[serde( + rename = "core:freq_lower_edge", + skip_serializing_if = "Option::is_none" + )] + pub freq_lower_edge: Option, + #[serde( + rename = "core:freq_upper_edge", + skip_serializing_if = "Option::is_none" + )] + pub freq_upper_edge: Option, + #[serde(rename = "core:label", skip_serializing_if = "Option::is_none")] + pub label: Option, + #[serde(rename = "core:generator", skip_serializing_if = "Option::is_none")] + pub generator: Option, + #[serde(rename = "core:comment", skip_serializing_if = "Option::is_none")] + pub comment: Option, + #[serde(rename = "core:uuid", skip_serializing_if = "Option::is_none")] + pub uuid: Option, + #[serde(flatten)] + pub extra: HashMap, +} diff --git a/crates/sigmf/src/antenna_extension.rs b/crates/sigmf/src/antenna_extension.rs new file mode 100644 index 0000000..65b18ee --- /dev/null +++ b/crates/sigmf/src/antenna_extension.rs @@ -0,0 +1,18 @@ +use crate::errors::SigMFError; + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Default)] +pub struct AntennaExtension { + #[serde(rename = "antenna:model", skip_serializing_if = "Option::is_none")] + pub model: Option, // Mandatory but required by the way we handle extension + #[serde(rename = "antenna:type", skip_serializing_if = "Option::is_none")] + pub r#type: Option, +} + +impl AntennaExtension { + pub fn model(&self) -> Result<&String, SigMFError> { + if let Some(model) = &self.model { + return Ok(model); + } + Err(SigMFError::MissingMandatoryField("model")) + } +} diff --git a/crates/sigmf/src/capture.rs b/crates/sigmf/src/capture.rs new file mode 100644 index 0000000..9fcc1d5 --- /dev/null +++ b/crates/sigmf/src/capture.rs @@ -0,0 +1,41 @@ +use serde_json::Value; +use std::collections::HashMap; + +#[cfg(feature = "quickcheck")] +use quickcheck::{empty_shrinker, Arbitrary, Gen}; + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Default)] +pub struct Capture { + #[serde(rename = "core:sample_start")] + pub sample_start: Option, + #[serde(rename = "core:global_index", skip_serializing_if = "Option::is_none")] + pub global_index: Option, + #[serde(rename = "core:frequency", skip_serializing_if = "Option::is_none")] + pub frequency: Option, + #[serde(rename = "core:datetime", skip_serializing_if = "Option::is_none")] + pub datetime: Option, + #[serde(rename = "core:header_bytes", skip_serializing_if = "Option::is_none")] + pub headers_bytes: Option, + #[serde(flatten)] + pub extra: HashMap, +} + +#[cfg(feature = "quickcheck")] +impl Arbitrary for Capture { + fn arbitrary(g: &mut Gen) -> Self { + let mut cap = Capture::default(); + for _ in 1..u8::arbitrary(g) { + let key = String::arbitrary(g); + let value = String::arbitrary(g); + cap.extra.insert(key, Value::String(value)); + } + cap + } + + fn shrink(&self) -> Box> { + if self.extra.is_empty() { + return empty_shrinker(); + } + empty_shrinker() //TODO better + } +} diff --git a/crates/sigmf/src/collection.rs b/crates/sigmf/src/collection.rs new file mode 100644 index 0000000..7d6422c --- /dev/null +++ b/crates/sigmf/src/collection.rs @@ -0,0 +1,41 @@ +use crate::{Extension, Recording}; +use serde_json::Value; +use std::collections::HashMap; + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct Collection { + #[serde(rename = "core:version")] + pub version: Option, + #[serde(rename = "core:description", skip_serializing_if = "Option::is_none")] + pub description: Option, + #[serde(rename = "core:author", skip_serializing_if = "Option::is_none")] + pub author: Option, + #[serde( + rename = "core:collection_doi", + skip_serializing_if = "Option::is_none" + )] + pub collection_doi: Option, + #[serde(rename = "core:license", skip_serializing_if = "Option::is_none")] + pub license: Option, + #[serde(rename = "core:extensions", skip_serializing_if = "Option::is_none")] + pub extensions: Option>, + #[serde(rename = "core:streams", skip_serializing_if = "Option::is_none")] + pub streams: Option>, + #[serde(flatten)] + pub extra: HashMap, +} + +impl Default for Collection { + fn default() -> Self { + Self { + version: Some("1.0.0".to_string()), + streams: Some(Vec::new()), + extra: HashMap::new(), + description: None, + author: None, + collection_doi: None, + license: None, + extensions: None, + } + } +} diff --git a/crates/sigmf/src/dataset_format.rs b/crates/sigmf/src/dataset_format.rs new file mode 100644 index 0000000..e55b55c --- /dev/null +++ b/crates/sigmf/src/dataset_format.rs @@ -0,0 +1,546 @@ +use std::{fmt, marker::PhantomData}; + +#[cfg(feature = "quickcheck")] +use quickcheck::{empty_shrinker, single_shrinker, Arbitrary, Gen}; + +use crate::SigMFError; + +#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] +pub enum DatasetFormat { + #[serde(rename = "rf64_le")] + Rf64Le, + #[serde(rename = "rf64_be")] + Rf64Be, + #[serde(rename = "cf64_le")] + Cf64Le, + #[serde(rename = "cf64_be")] + Cf64Be, + #[serde(rename = "rf32_le")] + Rf32Le, + #[serde(rename = "rf32_be")] + Rf32Be, + #[serde(rename = "cf32_le")] + Cf32Le, + #[serde(rename = "cf32_be")] + Cf32Be, + #[serde(rename = "ri32_le")] + Ri32Le, + #[serde(rename = "ri32_be")] + Ri32Be, + #[serde(rename = "ci32_le")] + Ci32Le, + #[serde(rename = "ci32_be")] + Ci32Be, + #[serde(rename = "ri16_le")] + Ri16Le, + #[serde(rename = "ri16_be")] + Ri16Be, + #[serde(rename = "ci16_le")] + Ci16Le, + #[serde(rename = "ci16_be")] + Ci16Be, + #[serde(rename = "ru32_le")] + Ru32Le, + #[serde(rename = "ru32_be")] + Ru32Be, + #[serde(rename = "cu32_le")] + Cu32Le, + #[serde(rename = "cu32_be")] + Cu32Be, + #[serde(rename = "ru16_le")] + Ru16Le, + #[serde(rename = "ru16_be")] + Ru16Be, + #[serde(rename = "cu16_le")] + Cu16Le, + #[serde(rename = "cu16_be")] + Cu16Be, + #[serde(rename = "ri8")] + RI8, + #[serde(rename = "ru8")] + RU8, + #[serde(rename = "ci8")] + CI8, + #[serde(rename = "cu8")] + CU8, +} + +impl DatasetFormat { + /// The size in bits + pub fn bits(&self) -> usize { + use DatasetFormat::*; + match self { + Cf64Le | Cf64Be => 2 * 64, + Rf64Le | Rf64Be => 64, + + Rf32Le | Rf32Be => 32, + Cf32Le | Cf32Be => 2 * 32, + + Ri32Le | Ri32Be => 32, + Ci32Le | Ci32Be => 2 * 32, + + Ri16Le | Ri16Be => 16, + Ci16Le | Ci16Be => 2 * 16, + + Ru32Le | Ru32Be => 32, + Cu32Le | Cu32Be => 2 * 32, + + Ru16Le | Ru16Be => 16, + Cu16Le | Cu16Be => 2 * 16, + + CI8 => 2 * 8, + CU8 => 2 * 8, + RI8 => 8, + RU8 => 8, + } + } + + /// The size in bytes + pub fn size(&self) -> usize { + self.bits() / 8 + } + + pub fn is_real(&self) -> bool { + use DatasetFormat::*; + match &self { + Cf64Le | Cf64Be | Cf32Le | Cf32Be | Ci32Le | Ci32Be | Ci16Le | Ci16Be | Cu32Le + | Cu32Be | Cu16Le | Cu16Be | CI8 | CU8 => false, + + Rf64Le | Rf64Be | Rf32Le | Rf32Be | Ri32Le | Ri32Be | Ri16Le | Ri16Be | Ru32Le + | Ru32Be | Ru16Le | Ru16Be | RI8 | RU8 => true, + } + } + + pub fn is_complex(&self) -> bool { + !self.is_real() + } + + pub fn is_signed(&self) -> bool { + use DatasetFormat::*; + match self { + Rf64Le | Rf64Be | Cf64Le | Cf64Be | Rf32Le | Rf32Be | Cf32Le | Cf32Be | Ri32Le + | Ri32Be | Ci32Le | Ci32Be | Ri16Le | Ri16Be | Ci16Le | Ci16Be | RI8 | CI8 => true, + + Ru32Le | Ru32Be | Cu32Le | Cu32Be | Ru16Le | Ru16Be | Cu16Le | Cu16Be | RU8 | CU8 => { + false + } + } + } + + pub fn is_unsigned(&self) -> bool { + !self.is_signed() + } + + pub fn is_little_endian(&self) -> bool { + use DatasetFormat::*; + matches!( + self, + Rf64Le + | Cf64Le + | Rf32Le + | Cf32Le + | Ri32Le + | Ci32Le + | Ri16Le + | Ci16Le + | Ru32Le + | Cu32Le + | Ru16Le + | Cu16Le + ) + } + + pub fn is_big_endian(&self) -> bool { + use DatasetFormat::*; + matches!( + self, + Rf64Be + | Cf64Be + | Rf32Be + | Cf32Be + | Ri32Be + | Ci32Be + | Ri16Be + | Ci16Be + | Ru32Be + | Cu32Be + | Ru16Be + | Cu16Be + ) + } + + pub fn is_float(&self) -> bool { + use DatasetFormat::*; + matches!( + self, + Rf64Le | Rf64Be | Cf64Le | Cf64Be | Rf32Le | Rf32Be | Cf32Le | Cf32Be + ) + } + + pub fn is_integer(&self) -> bool { + !self.is_float() + } + + pub fn is_byte(&self) -> bool { + use DatasetFormat::*; + matches!(self, RI8 | CU8 | CI8 | RU8) + } + + pub const fn all() -> [&'static DatasetFormat; 28] { + use DatasetFormat::*; + [ + &Rf64Le, &Rf64Be, &Cf64Le, &Cf64Be, &Rf32Le, &Rf32Be, &Cf32Le, &Cf32Be, &Ri32Le, + &Ri32Be, &Ci32Le, &Ci32Be, &Ri16Le, &Ri16Be, &Ci16Le, &Ci16Be, &Ru32Le, &Ru32Be, + &Cu32Le, &Cu32Be, &Ru16Le, &Ru16Be, &Cu16Le, &Cu16Be, &CI8, &CU8, &RI8, &RU8, + ] + } +} + +impl fmt::Display for DatasetFormat { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + use DatasetFormat::*; + match self { + Rf64Le => write!(f, "rf64_le"), + Rf64Be => write!(f, "rf64_be"), + Cf64Le => write!(f, "cf64_le"), + Cf64Be => write!(f, "cf64_be"), + Rf32Le => write!(f, "rf32_le"), + Rf32Be => write!(f, "rf32_be"), + Cf32Le => write!(f, "cf32_le"), + Cf32Be => write!(f, "cf32_be"), + Ri32Le => write!(f, "ri32_le"), + Ri32Be => write!(f, "ri32_be"), + Ci32Le => write!(f, "ci32_le"), + Ci32Be => write!(f, "ci32_be"), + Ri16Le => write!(f, "ri16_le"), + Ri16Be => write!(f, "ri16_be"), + Ci16Le => write!(f, "ci16_le"), + Ci16Be => write!(f, "ci16_be"), + Ru32Le => write!(f, "ru32_le"), + Ru32Be => write!(f, "ru32_be"), + Cu32Le => write!(f, "cu32_le"), + Cu32Be => write!(f, "cu32_be"), + Ru16Le => write!(f, "ru16_le"), + Ru16Be => write!(f, "ru16_be"), + Cu16Le => write!(f, "cu16_le"), + Cu16Be => write!(f, "cu16_be"), + CI8 => write!(f, "ci8"), + CU8 => write!(f, "cu8"), + RI8 => write!(f, "ri8"), + RU8 => write!(f, "ru8"), + } + } +} + +impl std::str::FromStr for DatasetFormat { + type Err = SigMFError; + fn from_str(s: &str) -> ::core::result::Result { + use DatasetFormat::*; + match s.to_lowercase().as_str() { + "rf64_le" => Ok(Rf64Le), + "rf64_be" => Ok(Rf64Be), + "cf64_le" => Ok(Cf64Le), + "cf64_be" => Ok(Cf64Be), + "rf32_le" => Ok(Rf32Le), + "rf32_be" => Ok(Rf32Be), + "cf32_le" => Ok(Cf32Le), + "cf32_be" => Ok(Cf32Be), + "ri32_le" => Ok(Ri32Le), + "ri32_be" => Ok(Ri32Be), + "ci32_le" => Ok(Ci32Le), + "ci32_be" => Ok(Ci32Be), + "ri16_le" => Ok(Ri16Le), + "ri16_be" => Ok(Ri16Be), + "ci16_le" => Ok(Ci16Le), + "ci16_be" => Ok(Ci16Be), + "ru32_le" => Ok(Ru32Le), + "ru32_be" => Ok(Ru32Be), + "cu32_le" => Ok(Cu32Le), + "cu32_be" => Ok(Cu32Be), + "ru16_le" => Ok(Ru16Le), + "ru16_be" => Ok(Ru16Be), + "cu16_le" => Ok(Cu16Le), + "cu16_be" => Ok(Cu16Be), + "ri8" => Ok(RI8), + "ru8" => Ok(RU8), + "ci8" => Ok(CI8), + "cu8" => Ok(CU8), + _ => Err(SigMFError::UnknownDatasetFormat(s.to_string())), + } + } +} + +#[cfg(feature = "quickcheck")] +impl Arbitrary for DatasetFormat { + fn arbitrary(g: &mut Gen) -> DatasetFormat { + **g.choose(&DatasetFormat::all()).unwrap() + } + + fn shrink(&self) -> Box> { + use DatasetFormat::*; + match self { + Rf64Le => single_shrinker(Rf32Le), + Rf64Be => single_shrinker(Rf32Be), + Cf64Le => single_shrinker(Cf32Le), + Cf64Be => single_shrinker(Cf32Be), + Rf32Le => single_shrinker(Ri32Le), + Rf32Be => single_shrinker(Rf32Le), + Cf32Le => single_shrinker(Rf32Le), + Cf32Be => single_shrinker(Rf32Be), + Ri32Le => single_shrinker(Ri16Le), + Ri32Be => single_shrinker(Ri16Be), + Ci32Le => single_shrinker(Ci16Le), + Ci32Be => single_shrinker(Ci16Be), + Ri16Le => single_shrinker(RI8), + Ri16Be => single_shrinker(RI8), + Ci16Le => single_shrinker(CI8), + Ci16Be => single_shrinker(CI8), + Ru32Le => single_shrinker(Ru16Le), + Ru32Be => single_shrinker(Ru16Be), + Cu32Le => single_shrinker(Ru32Le), + Cu32Be => single_shrinker(Ru32Be), + Ru16Le => single_shrinker(RU8), + Ru16Be => single_shrinker(RU8), + Cu16Le => single_shrinker(CU8), + Cu16Be => single_shrinker(CU8), + CI8 => single_shrinker(CU8), + CU8 => single_shrinker(RU8), + RI8 => single_shrinker(RU8), + RU8 => empty_shrinker(), + } + } +} + +#[derive(Debug, Clone, Copy)] +pub struct DatasetFormatBuilder { + underlying_type: PhantomData, + complex: bool, + little_endian: bool, +} + +impl DatasetFormatBuilder +where + T: Sized, +{ + pub fn complex() -> DatasetFormatBuilder { + DatasetFormatBuilder { + underlying_type: PhantomData::, + complex: true, + little_endian: true, + } + } + + pub fn real() -> DatasetFormatBuilder { + DatasetFormatBuilder { + underlying_type: PhantomData::, + complex: false, + little_endian: true, + } + } + + pub fn little_endian(mut self) -> DatasetFormatBuilder { + self.little_endian = true; + self + } + + pub fn big_endian(mut self) -> DatasetFormatBuilder { + self.little_endian = false; + self + } +} + +impl DatasetFormatBuilder { + pub fn build(self) -> DatasetFormat { + match self { + DatasetFormatBuilder { + underlying_type: _, + complex: true, + little_endian: true, + } => DatasetFormat::Cu32Le, + DatasetFormatBuilder { + underlying_type: _, + complex: true, + little_endian: false, + } => DatasetFormat::Cu32Be, + DatasetFormatBuilder { + underlying_type: _, + complex: false, + little_endian: true, + } => DatasetFormat::Ru32Le, + DatasetFormatBuilder { + underlying_type: _, + complex: false, + little_endian: false, + } => DatasetFormat::Ru32Be, + } + } +} + +impl DatasetFormatBuilder { + pub fn build(self) -> DatasetFormat { + match self { + DatasetFormatBuilder { + underlying_type: _, + complex: true, + little_endian: true, + } => DatasetFormat::Ci32Le, + DatasetFormatBuilder { + underlying_type: _, + complex: true, + little_endian: false, + } => DatasetFormat::Ci32Be, + DatasetFormatBuilder { + underlying_type: _, + complex: false, + little_endian: true, + } => DatasetFormat::Ri32Le, + DatasetFormatBuilder { + underlying_type: _, + complex: false, + little_endian: false, + } => DatasetFormat::Ri32Be, + } + } +} + +impl DatasetFormatBuilder { + pub fn build(self) -> DatasetFormat { + match self { + DatasetFormatBuilder { + underlying_type: _, + complex: true, + little_endian: true, + } => DatasetFormat::Cu16Le, + DatasetFormatBuilder { + underlying_type: _, + complex: true, + little_endian: false, + } => DatasetFormat::Cu16Be, + DatasetFormatBuilder { + underlying_type: _, + complex: false, + little_endian: true, + } => DatasetFormat::Ru16Le, + DatasetFormatBuilder { + underlying_type: _, + complex: false, + little_endian: false, + } => DatasetFormat::Ru16Be, + } + } +} + +impl DatasetFormatBuilder { + pub fn build(self) -> DatasetFormat { + match self { + DatasetFormatBuilder { + underlying_type: _, + complex: true, + little_endian: true, + } => DatasetFormat::Ci16Le, + DatasetFormatBuilder { + underlying_type: _, + complex: true, + little_endian: false, + } => DatasetFormat::Ci16Be, + DatasetFormatBuilder { + underlying_type: _, + complex: false, + little_endian: true, + } => DatasetFormat::Ri16Le, + DatasetFormatBuilder { + underlying_type: _, + complex: false, + little_endian: false, + } => DatasetFormat::Ri16Be, + } + } +} + +impl DatasetFormatBuilder { + pub fn build(self) -> DatasetFormat { + match self { + DatasetFormatBuilder { + underlying_type: _, + complex: true, + little_endian: _, + } => DatasetFormat::CU8, + DatasetFormatBuilder { + underlying_type: _, + complex: false, + little_endian: _, + } => DatasetFormat::RU8, + } + } +} + +impl DatasetFormatBuilder { + pub fn build(self) -> DatasetFormat { + match self { + DatasetFormatBuilder { + underlying_type: _, + complex: true, + little_endian: _, + } => DatasetFormat::CI8, + DatasetFormatBuilder { + underlying_type: _, + complex: false, + little_endian: _, + } => DatasetFormat::RI8, + } + } +} + +impl DatasetFormatBuilder { + pub fn build(self) -> DatasetFormat { + match self { + DatasetFormatBuilder { + underlying_type: _, + complex: true, + little_endian: true, + } => DatasetFormat::Cf32Le, + DatasetFormatBuilder { + underlying_type: _, + complex: true, + little_endian: false, + } => DatasetFormat::Cf32Be, + DatasetFormatBuilder { + underlying_type: _, + complex: false, + little_endian: true, + } => DatasetFormat::Rf32Le, + DatasetFormatBuilder { + underlying_type: _, + complex: false, + little_endian: false, + } => DatasetFormat::Rf32Be, + } + } +} + +impl DatasetFormatBuilder { + pub fn build(self) -> DatasetFormat { + match self { + DatasetFormatBuilder { + underlying_type: _, + complex: true, + little_endian: true, + } => DatasetFormat::Cf64Le, + DatasetFormatBuilder { + underlying_type: _, + complex: true, + little_endian: false, + } => DatasetFormat::Cf64Be, + DatasetFormatBuilder { + underlying_type: _, + complex: false, + little_endian: true, + } => DatasetFormat::Rf64Le, + DatasetFormatBuilder { + underlying_type: _, + complex: false, + little_endian: false, + } => DatasetFormat::Rf64Be, + } + } +} diff --git a/crates/sigmf/src/description.rs b/crates/sigmf/src/description.rs new file mode 100644 index 0000000..d404d60 --- /dev/null +++ b/crates/sigmf/src/description.rs @@ -0,0 +1,248 @@ +use crate::Recording; +use std::{ + fs::File, + io::{self, BufReader}, + path::Path, +}; + +#[cfg(feature = "quickcheck")] +use quickcheck::{empty_shrinker, Arbitrary, Gen}; + +use crate::{Annotation, Capture, Collection, DatasetFormat, Extension, Global, SigMFError}; + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct Description { + #[serde(rename = "global", skip_serializing_if = "Option::is_none")] + pub global: Option, + #[serde(rename = "captures", skip_serializing_if = "Option::is_none")] + pub captures: Option>, + #[serde(rename = "annotations", skip_serializing_if = "Option::is_none")] + pub annotations: Option>, + #[serde(rename = "collection", skip_serializing_if = "Option::is_none")] + pub collection: Option, +} + +impl Description { + pub fn global(&self) -> Result<&Global, SigMFError> { + if let Some(global) = &self.global { + return Ok(global); + } + Err(SigMFError::MissingMandatoryField("global")) + } + + pub fn global_mut(&mut self) -> Result<&mut Global, SigMFError> { + if let Some(global) = &mut self.global { + return Ok(global); + } + Err(SigMFError::MissingMandatoryField("global")) + } + + pub fn annotations(&self) -> Result<&Vec, SigMFError> { + if let Some(annotations) = &self.annotations { + return Ok(annotations); + } + Err(SigMFError::MissingMandatoryField("annotations")) + } + + pub fn annotations_mut(&mut self) -> Result<&mut Vec, SigMFError> { + if let Some(annotations) = &mut self.annotations { + return Ok(annotations); + } + Err(SigMFError::MissingMandatoryField("annotations")) + } + + pub fn captures(&self) -> Result<&Vec, SigMFError> { + if let Some(captures) = &self.captures { + return Ok(captures); + } + Err(SigMFError::MissingMandatoryField("captures")) + } + + pub fn captures_mut(&mut self) -> Result<&Vec, SigMFError> { + if let Some(captures) = &mut self.captures { + return Ok(captures); + } + Err(SigMFError::MissingMandatoryField("captures")) + } + + pub fn to_writer(&self, writer: W) -> Result<(), SigMFError> + where + W: io::Write, + { + Ok(serde_json::to_writer(writer, self)?) + } + + pub fn to_writer_pretty(&self, writer: W) -> Result<(), SigMFError> + where + W: io::Write, + { + Ok(serde_json::to_writer_pretty(writer, self)?) + } + + pub fn create

(&self, path: P) -> Result<(), SigMFError> + where + P: AsRef, + { + let f = File::create(path)?; + self.to_writer(f) + } + + pub fn create_pretty

(&self, path: P) -> Result<(), SigMFError> + where + P: AsRef, + { + let f = File::create(path)?; + self.to_writer_pretty(f) + } + + pub fn from_reader(reader: R) -> Result + where + R: io::Read, + { + let desc: Result = serde_json::from_reader(reader); + Ok(desc?) + } + + pub fn open

(path: P) -> Result + where + P: AsRef, + { + let meta_file = File::open(path)?; + let rdr = BufReader::new(meta_file); + Description::from_reader(rdr) + } +} + +impl Default for Description { + fn default() -> Self { + Self { + global: Some(Global::default()), + annotations: Some(Vec::new()), + captures: Some(Vec::new()), + collection: None, + } + } +} + +#[cfg(feature = "quickcheck")] +impl Arbitrary for Description { + fn arbitrary(g: &mut Gen) -> Self { + let global = Global::arbitrary(g); + let mut desc = DescriptionBuilder::from(global); + if bool::arbitrary(g) { + let caps = Vec::::arbitrary(g); + desc.captures(caps); + } + desc.build() + .expect("arbitrary shall build valid description") + } + + fn shrink(&self) -> Box> { + if *self == Description::default() { + return empty_shrinker(); + } + empty_shrinker() + } +} + +#[derive(Debug, Default)] +pub struct DescriptionBuilder(Description); + +impl DescriptionBuilder { + pub fn collection() -> DescriptionBuilder { + DescriptionBuilder(Description { + collection: Some(Collection::default()), + global: None, + captures: None, + annotations: None, + }) + } + + pub fn sample_rate(&mut self, sample_rate: f64) -> Result<&mut DescriptionBuilder, SigMFError> { + if sample_rate.is_nan() || !(0.0..=1e251).contains(&sample_rate) { + return Err(SigMFError::BadSampleRate()); + } + let global = self.0.global.as_mut().unwrap(); + global.sample_rate = Some(sample_rate); + Ok(self) + } + + pub fn extension( + &mut self, + name: &str, + version: &str, + optional: bool, + ) -> &mut DescriptionBuilder { + let global = self.0.global.as_mut().unwrap(); + let new_ext = Extension { + name: name.to_string(), + version: version.to_string(), + optional, + }; + if let Some(extensions) = &mut global.extensions { + extensions.push(new_ext); + } else { + global.extensions = Some(vec![new_ext]); + } + self + } + + pub fn captures(&mut self, captures: Vec) -> &mut DescriptionBuilder { + self.0.captures = Some(captures); + self + } + + pub fn build(&self) -> Result { + // TODO checks for mandatory fields + Ok(self.0.clone()) + } + + pub fn open

(path: P) -> Result + where + P: AsRef, + { + let desc = Description::open(path)?; + Ok(DescriptionBuilder(desc)) + } + + pub fn add_stream(&mut self, stream: Recording) -> Result<&mut Self, SigMFError> { + self.0 + .collection + .as_mut() + .expect("") + .streams + .as_mut() + .expect("msg") + .push(stream); + Ok(self) + } + + pub fn add_annotation(&mut self, annot: Annotation) -> Result<&mut Self, SigMFError> { + if let Some(annotations) = &mut self.0.annotations { + annotations.push(annot); + } else { + self.0.annotations = Some(vec![annot]); + } + Ok(self) + } +} + +impl From for DescriptionBuilder { + fn from(value: DatasetFormat) -> Self { + let mut desc = DescriptionBuilder::default(); + let global = Global { + datatype: Some(value), + ..Default::default() + }; + desc.0.global = Some(global); + desc + } +} + +impl From for DescriptionBuilder { + fn from(value: Global) -> Self { + let mut desc = DescriptionBuilder::default(); + desc.0.global = Some(value); + desc + } +} diff --git a/crates/sigmf/src/errors.rs b/crates/sigmf/src/errors.rs new file mode 100644 index 0000000..6235a70 --- /dev/null +++ b/crates/sigmf/src/errors.rs @@ -0,0 +1,17 @@ +use std::io; + +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum SigMFError { + #[error("Mandatory field is missing")] + MissingMandatoryField(&'static str), + #[error("JSON malformed or ")] + JsonError(#[from] serde_json::Error), + #[error("Unknown DatasetFormat")] + UnknownDatasetFormat(String), + #[error("io error")] + IoError(#[from] io::Error), + #[error("Sample rate must be positive and less than 1e250")] + BadSampleRate(), +} diff --git a/crates/sigmf/src/extension.rs b/crates/sigmf/src/extension.rs new file mode 100644 index 0000000..73d7aaa --- /dev/null +++ b/crates/sigmf/src/extension.rs @@ -0,0 +1,9 @@ +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct Extension { + #[serde(rename = "name")] + pub name: String, + #[serde(rename = "version")] + pub version: String, + #[serde(rename = "optional")] + pub optional: bool, +} diff --git a/crates/sigmf/src/global.rs b/crates/sigmf/src/global.rs new file mode 100644 index 0000000..c078e7b --- /dev/null +++ b/crates/sigmf/src/global.rs @@ -0,0 +1,124 @@ +use crate::{errors::SigMFError, AntennaExtension, DatasetFormat, Extension}; +use serde_json::Value; +use std::collections::HashMap; + +#[cfg(feature = "quickcheck")] +use quickcheck::{empty_shrinker, Arbitrary, Gen}; + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct Global { + #[serde(rename = "core:datatype")] + pub datatype: Option, // It is mandatory but we want to be lax in parsing + #[serde(rename = "core:version", skip_serializing_if = "Option::is_none")] + pub version: Option, // It is mandatory but we want to be lax in parsing + #[serde(rename = "core:sample_rate", skip_serializing_if = "Option::is_none")] + pub sample_rate: Option, + #[serde(rename = "core:num_channels", skip_serializing_if = "Option::is_none")] + pub num_channels: Option, + #[serde(rename = "core:sha512", skip_serializing_if = "Option::is_none")] + pub sha512: Option, + #[serde(rename = "core:offset", skip_serializing_if = "Option::is_none")] + pub offset: Option, + #[serde(rename = "core:description", skip_serializing_if = "Option::is_none")] + pub description: Option, + #[serde(rename = "core:author", skip_serializing_if = "Option::is_none")] + pub author: Option, + #[serde(rename = "core:meta_doi", skip_serializing_if = "Option::is_none")] + pub meta_doi: Option, + #[serde(rename = "core:data_doi", skip_serializing_if = "Option::is_none")] + pub data_doi: Option, + #[serde(rename = "core:recorder", skip_serializing_if = "Option::is_none")] + pub recorder: Option, + #[serde(rename = "core:license", skip_serializing_if = "Option::is_none")] + pub license: Option, + #[serde(rename = "core:hw", skip_serializing_if = "Option::is_none")] + pub hw: Option, + #[serde(rename = "core:collection", skip_serializing_if = "Option::is_none")] + pub collection: Option, + #[serde(rename = "core:metadata_only", skip_serializing_if = "Option::is_none")] + pub metadata_only: Option, + #[serde(rename = "core:dataset", skip_serializing_if = "Option::is_none")] + pub dataset: Option, + #[serde( + rename = "core:trailing_bytes", + skip_serializing_if = "Option::is_none" + )] + pub trailing_bytes: Option, + #[serde(rename = "core:extensions", skip_serializing_if = "Option::is_none")] + pub extensions: Option>, + #[serde(flatten)] + pub antenna: AntennaExtension, + + #[serde(flatten)] + pub extra: HashMap, +} + +impl Global { + pub fn version(&self) -> Result<&String, SigMFError> { + if let Some(version) = &self.version { + return Ok(version); + } + Err(SigMFError::MissingMandatoryField("version")) + } + + pub fn datatype(&self) -> Result<&DatasetFormat, SigMFError> { + if let Some(datatype) = &self.datatype { + return Ok(datatype); + } + Err(SigMFError::MissingMandatoryField("datatype")) + } +} + +impl Default for Global { + fn default() -> Self { + Self { + datatype: Some(DatasetFormat::Cf32Le), + version: Some("1.0.0".to_string()), + sample_rate: None, + num_channels: None, + sha512: None, + offset: None, + description: None, + author: None, + meta_doi: None, + data_doi: None, + recorder: None, + license: None, + hw: None, + collection: None, + metadata_only: None, + dataset: None, + trailing_bytes: None, + extensions: None, + antenna: AntennaExtension::default(), + extra: HashMap::new(), + } + } +} + +#[cfg(feature = "quickcheck")] +impl Arbitrary for Global { + fn arbitrary(g: &mut Gen) -> Global { + let dataset: DatasetFormat = DatasetFormat::arbitrary(g); + let mut global: Global = Global { + datatype: Some(dataset), + ..Global::default() + }; + if bool::arbitrary(g) { + let sample_rate = f64::arbitrary(g); + let sample_rate = ((sample_rate % 1e15) * 100.0).trunc() / 100.0; + if !sample_rate.is_nan() { + global.sample_rate = Some(sample_rate.abs()) + } + } + // if bool::arbitrary(g) {} + global + } + + fn shrink(&self) -> Box> { + if *self == Global::default() { + return empty_shrinker(); + } + empty_shrinker() + } +} diff --git a/crates/sigmf/src/lib.rs b/crates/sigmf/src/lib.rs new file mode 100644 index 0000000..c917016 --- /dev/null +++ b/crates/sigmf/src/lib.rs @@ -0,0 +1,35 @@ +#[macro_use] +extern crate serde_derive; + +extern crate serde; +extern crate serde_json; + +mod errors; +pub use errors::SigMFError; + +mod annotation; +pub use annotation::Annotation; + +mod antenna_extension; +pub use antenna_extension::AntennaExtension; + +mod capture; +pub use capture::Capture; + +mod collection; +pub use collection::Collection; + +mod dataset_format; +pub use dataset_format::{DatasetFormat, DatasetFormatBuilder}; + +mod description; +pub use description::{Description, DescriptionBuilder}; + +mod extension; +pub use extension::Extension; + +mod global; +pub use global::Global; + +mod recording; +pub use recording::{Recording, RecordingBuilder}; diff --git a/crates/sigmf/src/recording.rs b/crates/sigmf/src/recording.rs new file mode 100644 index 0000000..76050f9 --- /dev/null +++ b/crates/sigmf/src/recording.rs @@ -0,0 +1,118 @@ +use crate::{Description, SigMFError}; +use sha2::{Digest, Sha512}; +use std::io::Read; +use std::path::Path; +use std::{fs::File, path::PathBuf}; + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct Recording { + #[serde(rename = "name")] + pub name: Option, + #[serde(rename = "hash", skip_serializing_if = "Option::is_none")] + pub hash: Option, +} + +impl Recording { + pub fn hash(&self) -> Result<&String, SigMFError> { + if let Some(hash) = &self.hash { + return Ok(hash); + } + Err(SigMFError::MissingMandatoryField("hash")) + } + + pub fn sigmf_data(&mut self) -> Result<&Path, SigMFError> { + if let Some(basename) = &mut self.name { + basename.set_extension("sigmf-data"); + return Ok(basename.as_path()); + } + Err(SigMFError::MissingMandatoryField("name")) + } + + pub fn sigmf_meta(&mut self) -> Result<&Path, SigMFError> { + if let Some(basename) = &mut self.name { + basename.set_extension("sigmf-meta"); + return Ok(basename.as_path()); + } + Err(SigMFError::MissingMandatoryField("name")) + } + + pub fn compute_sha512(&mut self) -> Result { + let path = self.sigmf_data()?; + let mut data_file = File::open(path)?; + let mut hasher = Sha512::new(); + let mut buffer = [0; 1024]; + + loop { + let count = data_file.read(&mut buffer)?; + if count == 0 { + break; + } + hasher.update(&buffer[..count]); + } + let computed_sha512 = hasher.finalize(); + let computed_sha512 = hex::encode(computed_sha512); + Ok(computed_sha512) + } + + pub fn load_description(&mut self) -> Result { + let path = self.sigmf_meta()?; + let desc = Description::open(path)?; + Ok(desc) + } +} + +pub struct RecordingBuilder(Recording); + +impl From for RecordingBuilder { + fn from(value: PathBuf) -> Self { + RecordingBuilder(Recording { + name: Some(value), + hash: None, + }) + } +} + +impl From<&PathBuf> for RecordingBuilder { + fn from(value: &PathBuf) -> Self { + RecordingBuilder(Recording { + name: Some(value.to_path_buf()), + hash: None, + }) + } +} + +impl From<&Path> for RecordingBuilder { + fn from(value: &Path) -> Self { + RecordingBuilder(Recording { + name: Some(value.to_path_buf()), + hash: None, + }) + } +} + +impl RecordingBuilder { + pub fn build(&self) -> Recording { + self.0.clone() + } + + /// Load the .sigmf-meta file and copy the sha512 hash if any + pub fn load_description(&mut self) -> Result<(Self, Description), SigMFError> { + let desc = self.0.load_description()?; + let mut new_hash = self.0.hash.clone(); + if let Some(hash) = &desc.global()?.sha512 { + new_hash = Some((*hash).clone()); + } + let res = RecordingBuilder(Recording { + name: self.0.name.clone(), + hash: new_hash, + }); + Ok((res, desc)) + } + + /// Load the .sigmf-data and compute the sha512 hash + pub fn compute_sha512(&mut self) -> Result<&mut Self, SigMFError> { + let hash = self.0.compute_sha512()?; + self.0.hash = Some(hash); + Ok(self) + } +} diff --git a/crates/sigmf/tests/dataset_format.rs b/crates/sigmf/tests/dataset_format.rs new file mode 100644 index 0000000..b71e9ac --- /dev/null +++ b/crates/sigmf/tests/dataset_format.rs @@ -0,0 +1,54 @@ +#[cfg(test)] +#[macro_use(quickcheck)] +extern crate quickcheck_macros; + +use sigmf::{DatasetFormat, DatasetFormatBuilder, SigMFError}; + +#[quickcheck] +fn qc_little_endian_ends_with_le(dataset: DatasetFormat) -> bool { + !dataset.is_little_endian() | dataset.to_string().ends_with("_le") +} + +#[quickcheck] +fn qc_big_endian_ends_with_be(dataset: DatasetFormat) -> bool { + !dataset.is_big_endian() | dataset.to_string().ends_with("_be") +} + +#[quickcheck] +fn qc_complex_starts_with_c(dataset: DatasetFormat) -> bool { + !dataset.is_complex() | dataset.to_string().starts_with('c') +} + +#[quickcheck] +fn qc_real_starts_with_r(dataset: DatasetFormat) -> bool { + !dataset.is_real() | dataset.to_string().starts_with('r') +} + +#[quickcheck] +fn qc_bits_in_label(dataset: DatasetFormat) -> bool { + let mut nb_bits = dataset.bits(); + if dataset.is_complex() { + nb_bits /= 2; + } + let nb_bits = nb_bits.to_string().clone(); + let label = dataset.to_string(); + label.contains(nb_bits.as_str()) +} + +#[quickcheck] +fn qc_parse_string_is_identity(dataset: DatasetFormat) -> bool { + let dataset_repr = dataset.to_string(); + let parsed = dataset_repr.parse::(); + parsed.is_err() || parsed.unwrap() == dataset +} + +#[test] +fn test_dataset_builder() -> Result<(), SigMFError> { + let datatype = DatasetFormatBuilder::::complex() + .little_endian() + .build(); + assert_eq!("cu32_le", datatype.to_string()); + let datatype = DatasetFormatBuilder::::real().big_endian().build(); + assert_eq!("rf32_be", datatype.to_string()); + Ok(()) +} diff --git a/crates/sigmf/tests/description.rs b/crates/sigmf/tests/description.rs new file mode 100644 index 0000000..9b6d4ad --- /dev/null +++ b/crates/sigmf/tests/description.rs @@ -0,0 +1,34 @@ +#[cfg(test)] +#[macro_use(quickcheck)] +extern crate quickcheck_macros; + +use std::io::Cursor; + +use sigmf::{DatasetFormat, Description, DescriptionBuilder, SigMFError}; + +#[quickcheck] +fn qc_write_read(desc_input: Description) -> bool { + let mut buffer = Vec::::new(); + if desc_input.to_writer(&mut buffer).is_err() { + return false; + } + let buffer = Cursor::new(buffer); + let desc_output = Description::from_reader(buffer).expect(""); + assert_eq!( + desc_input.global().expect("").sample_rate, + desc_output.global().expect("").sample_rate + ); + desc_input == desc_output +} + +#[test] +fn create_desc_high_sample_rate() -> Result<(), SigMFError> { + let mut desc = DescriptionBuilder::from(DatasetFormat::Cf32Le); + let setter_ok = desc.sample_rate(2.7350335256693894e251); + assert!(setter_ok.is_err()); + let setter_ok: Result<&mut DescriptionBuilder, SigMFError> = desc.sample_rate(f64::NAN); + assert!(setter_ok.is_err()); + let setter_ok = desc.sample_rate(2_000_000.0); + assert!(setter_ok.is_ok()); + Ok(()) +} diff --git a/crates/sigmf/tests/sigmf_meta.rs b/crates/sigmf/tests/sigmf_meta.rs new file mode 100644 index 0000000..0a7ad0b --- /dev/null +++ b/crates/sigmf/tests/sigmf_meta.rs @@ -0,0 +1,182 @@ +use sigmf::{DatasetFormatBuilder, Description, DescriptionBuilder, SigMFError}; + +#[test] +fn parse_mandatory() -> Result<(), SigMFError> { + let metadata = r#"{ + "global": { + "core:datatype": "cu8", + "core:version": "1.0.0" + }, + "captures": [], + "annotations": [] +} +"#; + let description: Description = serde_json::from_str(metadata)?; + let global = description.global()?; + assert_eq!("1.0.0", global.version()?); + assert_eq!("cu8", global.datatype()?.to_string()); + assert_eq!( + *global.datatype()?, + DatasetFormatBuilder::::complex().build() + ); + assert_eq!(0, description.annotations()?.len()); + assert_eq!(0, description.captures()?.len()); + Ok(()) +} + +#[test] +fn parse_example_from_spec() -> Result<(), SigMFError> { + let metadata = r#" +{ + "global": { + "core:datatype": "ru8", + "core:version": "1.0.0", + "core:dataset": "non-conforming-dataset-01.dat" + }, + "captures": [ + { + "core:sample_start": 0, + "core:header_bytes": 4 + }, + { + "core:sample_start": 500, + "core:header_bytes": 4 + } + ], + "annotations": [] +}"#; + let description: Description = serde_json::from_str(metadata)?; + let global = description.global()?; + assert_eq!("1.0.0", global.version()?); + assert_eq!("ru8", global.datatype()?.to_string()); + assert_eq!( + *global.datatype()?, + DatasetFormatBuilder::::real().build() + ); + assert_eq!(0, description.annotations()?.len()); + assert_eq!(2, description.captures()?.len()); + Ok(()) +} + +// { +// "global": { +// "core:datatype": "cf32_le", +// "core:sample_rate": 2000000, +// "core:hw": "HachRF(tm) One with bi-bands double J antenna", +// "core:author": "Loïc Fejoz", +// "core:version": "1.0.0", +// "core:description": "GQRX recording of VHF APRS" +// }, +// "captures": [ +// { +// "core:sample_start": 0, +// "core:frequency": 145171400, +// "core:datetime": "2023-11-04T10:17:25Z" +// } +// ], +// "annotations": [] +// } + +#[test] +fn create_simple_description() -> Result<(), SigMFError> { + let sample_rate = 2_000_000.0; + let datatype = DatasetFormatBuilder::::complex() + .little_endian() + .build(); + let desc = DescriptionBuilder::from(datatype) + .sample_rate(sample_rate)? + .build()?; + let expected = r#" + { + "global": { + "core:datatype": "cu32_le", + "core:version": "1.0.0", + "core:sample_rate": 2000000.0 + }, + "captures": [], + "annotations": [] + }"#; + let mut expected = expected.to_string(); + expected.retain(|c| !c.is_whitespace()); + let expected_desc: Description = serde_json::from_str(expected.as_str())?; + assert_eq!(expected_desc, desc); + let json = serde_json::to_string(&desc)?; + assert_eq!(expected, json); + let global = desc.global()?; + assert_eq!(Some(sample_rate), global.sample_rate); + assert_eq!(None, global.hw); + assert_eq!(0, desc.annotations()?.len()); + assert_eq!(0, desc.captures()?.len()); + Ok(()) +} + +#[test] +fn create_description_with_extensions() -> Result<(), SigMFError> { + let sample_rate = 2_000_000.0; + let datatype = DatasetFormatBuilder::::complex() + .little_endian() + .build(); + let desc = DescriptionBuilder::from(datatype) + .sample_rate(sample_rate)? + .extension("extension-01", "0.0.5", true) + .build()?; + let expected = r#" + { + "global": { + "core:datatype": "cu32_le", + "core:version": "1.0.0", + "core:sample_rate": 2000000.0, + "core:extensions" : [ + { + "name": "extension-01", + "version": "0.0.5", + "optional": true + } + ] + }, + "captures": [], + "annotations": [] + }"#; + let mut expected = expected.to_string(); + expected.retain(|c| !c.is_whitespace()); + let expected_desc: Description = serde_json::from_str(expected.as_str())?; + assert_eq!(expected_desc, desc); + let json = serde_json::to_string(&desc)?; + assert_eq!(expected, json); + let global = desc.global()?; + assert_eq!(Some(sample_rate), global.sample_rate); + assert_eq!(None, global.hw); + assert_eq!(0, desc.annotations()?.len()); + assert_eq!(0, desc.captures()?.len()); + Ok(()) +} + +#[test] +fn parse_antenna() -> Result<(), SigMFError> { + let metadata = r#"{ + "global": { + "core:datatype": "cu8", + "core:version": "1.0.0", + "core:extensions" : [ + { + "name": "antenna", + "version": "1.0.0", + "optional": false + } + ], + "antenna:model": "ARA CSB-16" + }, + "captures": [], + "annotations": [] +} +"#; + let description: Description = serde_json::from_str(metadata)?; + let global = description.global()?; + assert_eq!("1.0.0", global.version()?); + assert_eq!("cu8", global.datatype()?.to_string()); + assert_eq!(0, description.annotations()?.len()); + assert_eq!(0, description.captures()?.len()); + let antenna_desc = &global.antenna; + assert_eq!("ARA CSB-16", antenna_desc.model()?); + Ok(()) +} diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..5d56faf --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "nightly" diff --git a/src/lib.rs b/src/lib.rs index 26232bd..2392668 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,8 @@ //! This library acts as a toolbox on top of [FutureSDR][`futuresdr`] to easily build your own flowgraph. //! It is made by the community for the community. +// #![feature(async_fn_in_trait)] + #[macro_use] pub extern crate async_trait; @@ -11,6 +13,9 @@ pub mod channel; pub mod cw; pub mod math; +pub mod sigmf; pub mod stdinout; pub mod stream; pub mod type_converters; + +pub mod serde_pmt; diff --git a/src/serde_pmt/deserialiser.rs b/src/serde_pmt/deserialiser.rs new file mode 100644 index 0000000..9456f0b --- /dev/null +++ b/src/serde_pmt/deserialiser.rs @@ -0,0 +1,447 @@ +use std::{borrow::Cow, collections::HashMap}; + +use super::error::{Error, Result}; +use futuresdr::runtime::Pmt; +use serde::{ + de::{DeserializeSeed, Expected, MapAccess, Unexpected, Visitor}, + forward_to_deserialize_any, Deserializer, +}; + +pub struct PmtDist(Pmt); + +impl From for PmtDist { + fn from(value: Pmt) -> Self { + PmtDist(value) + } +} + +impl PmtDist { + #[cold] + fn invalid_type(&self, exp: &dyn Expected) -> E + where + E: serde::de::Error, + { + serde::de::Error::invalid_type(self.unexpected(), exp) + } + + #[cold] + fn unexpected(&self) -> Unexpected { + match &self.0 { + Pmt::Null => Unexpected::Unit, + Pmt::Bool(b) => Unexpected::Bool(*b), + //Pmt::U32(n) => n.unexpected(), + Pmt::String(s) => Unexpected::Str(s), + Pmt::VecF32(_) => Unexpected::Seq, + Pmt::MapStrPmt(_) => Unexpected::Map, + _ => Unexpected::Unit, //TODO + } + } +} + +impl<'de> serde::Deserializer<'de> for PmtDist { + type Error = Error; + + fn deserialize_any(self, visitor: V) -> std::prelude::v1::Result + where + V: serde::de::Visitor<'de>, + { + match self.0 { + Pmt::Bool(v) => visitor.visit_bool(v), + Pmt::F32(v) => visitor.visit_f32(v), + Pmt::F64(v) => visitor.visit_f64(v), + Pmt::U32(v) => visitor.visit_u32(v), + Pmt::U64(v) => visitor.visit_u64(v), + Pmt::Null => visitor.visit_unit(), + Pmt::String(v) => visitor.visit_string(v), + Pmt::Usize(v) => visitor.visit_u64(v as u64), + _ => Err(self::Error::Message("Not yet implemented".to_string())), + } + } + + fn deserialize_bool(self, visitor: V) -> std::prelude::v1::Result + where + V: serde::de::Visitor<'de>, + { + match self.0 { + Pmt::Bool(v) => visitor.visit_bool(v), + _ => Err(self.invalid_type(&visitor)), + } + } + + fn deserialize_i8(self, _visitor: V) -> std::prelude::v1::Result + where + V: serde::de::Visitor<'de>, + { + todo!() + } + + fn deserialize_i16(self, _visitor: V) -> std::prelude::v1::Result + where + V: serde::de::Visitor<'de>, + { + todo!() + } + + fn deserialize_i32(self, _visitor: V) -> std::prelude::v1::Result + where + V: serde::de::Visitor<'de>, + { + todo!() + } + + fn deserialize_i64(self, _visitor: V) -> std::prelude::v1::Result + where + V: serde::de::Visitor<'de>, + { + todo!() + } + + fn deserialize_u8(self, _visitor: V) -> std::prelude::v1::Result + where + V: serde::de::Visitor<'de>, + { + todo!() + } + + fn deserialize_u16(self, _visitor: V) -> std::prelude::v1::Result + where + V: serde::de::Visitor<'de>, + { + todo!() + } + + fn deserialize_u32(self, visitor: V) -> std::prelude::v1::Result + where + V: serde::de::Visitor<'de>, + { + match self.0 { + Pmt::U32(v) => visitor.visit_u32(v), + _ => Err(self.invalid_type(&visitor)), + } + } + + fn deserialize_u64(self, visitor: V) -> std::prelude::v1::Result + where + V: serde::de::Visitor<'de>, + { + match self.0 { + Pmt::U64(v) => visitor.visit_u64(v), + _ => Err(self.invalid_type(&visitor)), + } + } + + fn deserialize_f32(self, _visitor: V) -> std::prelude::v1::Result + where + V: serde::de::Visitor<'de>, + { + todo!() + } + + fn deserialize_f64(self, _visitor: V) -> std::prelude::v1::Result + where + V: serde::de::Visitor<'de>, + { + todo!() + } + + fn deserialize_char(self, _visitor: V) -> std::prelude::v1::Result + where + V: serde::de::Visitor<'de>, + { + todo!() + } + + fn deserialize_str(self, _visitor: V) -> std::prelude::v1::Result + where + V: serde::de::Visitor<'de>, + { + todo!() + } + + fn deserialize_string(self, visitor: V) -> std::prelude::v1::Result + where + V: serde::de::Visitor<'de>, + { + match self.0 { + Pmt::String(v) => visitor.visit_string(v), + _ => Err(self.invalid_type(&visitor)), + } + } + + fn deserialize_bytes(self, _visitor: V) -> std::prelude::v1::Result + where + V: serde::de::Visitor<'de>, + { + todo!() + } + + fn deserialize_byte_buf(self, _visitor: V) -> std::prelude::v1::Result + where + V: serde::de::Visitor<'de>, + { + todo!() + } + + fn deserialize_option(self, visitor: V) -> std::prelude::v1::Result + where + V: serde::de::Visitor<'de>, + { + match self.0 { + Pmt::Null => visitor.visit_unit(), + _ => visitor.visit_some(self), + } + } + + fn deserialize_unit(self, visitor: V) -> std::prelude::v1::Result + where + V: serde::de::Visitor<'de>, + { + match self.0 { + Pmt::Null => visitor.visit_unit(), + _ => Err(self.invalid_type(&visitor)), + } + } + + fn deserialize_unit_struct( + self, + _name: &'static str, + _visitor: V, + ) -> std::prelude::v1::Result + where + V: serde::de::Visitor<'de>, + { + todo!() + } + + fn deserialize_newtype_struct( + self, + _name: &'static str, + _visitor: V, + ) -> std::prelude::v1::Result + where + V: serde::de::Visitor<'de>, + { + todo!() + } + + fn deserialize_seq(self, _visitor: V) -> std::prelude::v1::Result + where + V: serde::de::Visitor<'de>, + { + todo!() + } + + fn deserialize_tuple( + self, + _len: usize, + _visitor: V, + ) -> std::prelude::v1::Result + where + V: serde::de::Visitor<'de>, + { + todo!() + } + + fn deserialize_tuple_struct( + self, + _name: &'static str, + _len: usize, + _visitor: V, + ) -> std::prelude::v1::Result + where + V: serde::de::Visitor<'de>, + { + todo!() + } + + fn deserialize_map(self, visitor: V) -> std::prelude::v1::Result + where + V: serde::de::Visitor<'de>, + { + match self.0 { + Pmt::MapStrPmt(v) => visit_object(v, visitor), + _ => Err(self.invalid_type(&visitor)), + } + } + + fn deserialize_struct( + self, + _name: &'static str, + _fields: &'static [&'static str], + _visitor: V, + ) -> std::prelude::v1::Result + where + V: serde::de::Visitor<'de>, + { + todo!() + } + + fn deserialize_enum( + self, + _name: &'static str, + _variants: &'static [&'static str], + _visitor: V, + ) -> std::prelude::v1::Result + where + V: serde::de::Visitor<'de>, + { + todo!() + } + + fn deserialize_identifier( + self, + _visitor: V, + ) -> std::prelude::v1::Result + where + V: serde::de::Visitor<'de>, + { + todo!() + } + + fn deserialize_ignored_any( + self, + _visitor: V, + ) -> std::prelude::v1::Result + where + V: serde::de::Visitor<'de>, + { + todo!() + } +} + +fn visit_object<'de, V>(object: HashMap, visitor: V) -> Result +where + V: Visitor<'de>, +{ + let len = object.len(); + let mut deserializer = PmtMapDeserializer::new(object); + let map = visitor.visit_map(&mut deserializer)?; + let remaining = deserializer.iter.len(); + if remaining == 0 { + Ok(map) + } else { + Err(serde::de::Error::invalid_length( + len, + &"fewer elements in map", + )) + } + // Err(serde::de::Error::custom("not yet implemented")) +} + +struct PmtMapDeserializer { + iter: as IntoIterator>::IntoIter, + value: Option, +} + +impl PmtMapDeserializer { + fn new(map: HashMap) -> Self { + PmtMapDeserializer { + iter: map.into_iter(), + value: None, + } + } +} + +impl<'de> MapAccess<'de> for PmtMapDeserializer { + type Error = Error; + + fn next_key_seed(&mut self, seed: T) -> core::result::Result, Error> + where + T: DeserializeSeed<'de>, + { + match self.iter.next() { + Some((key, value)) => { + self.value = Some(value); + let key_de = MapKeyDeserializer { + key: Cow::Owned(key), + }; + seed.deserialize(key_de).map(Some) + } + None => Ok(None), + } + } + + fn next_value_seed(&mut self, seed: T) -> core::result::Result + where + T: DeserializeSeed<'de>, + { + match self.value.take() { + Some(value) => seed.deserialize(PmtDist(value)), + None => Err(serde::de::Error::custom("value is missing")), + } + } + + fn size_hint(&self) -> Option { + match self.iter.size_hint() { + (lower, Some(upper)) if lower == upper => Some(upper), + _ => None, + } + } +} + +struct MapKeyDeserializer<'de> { + key: Cow<'de, str>, +} + +impl<'de> serde::Deserializer<'de> for MapKeyDeserializer<'de> { + type Error = Error; + + fn deserialize_any(self, visitor: V) -> core::result::Result + where + V: Visitor<'de>, + { + BorrowedCowStrDeserializer::new(self.key).deserialize_any(visitor) + } + + forward_to_deserialize_any! { + bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 char str string + bytes byte_buf option unit unit_struct newtype_struct seq tuple + tuple_struct map struct identifier ignored_any enum + } +} + +struct BorrowedCowStrDeserializer<'de> { + value: Cow<'de, str>, +} + +impl<'de> BorrowedCowStrDeserializer<'de> { + fn new(value: Cow<'de, str>) -> Self { + BorrowedCowStrDeserializer { value } + } +} + +impl<'de> Deserializer<'de> for BorrowedCowStrDeserializer<'de> { + type Error = Error; + + fn deserialize_any(self, visitor: V) -> core::result::Result + where + V: Visitor<'de>, + { + match self.value { + Cow::Borrowed(string) => visitor.visit_borrowed_str(string), + Cow::Owned(string) => visitor.visit_string(string), + } + } + + // fn deserialize_enum( + // self, + // _name: &str, + // _variants: &'static [&'static str], + // visitor: V, + // ) -> core::result::Result + // where + // V: Visitor<'de>, + // { + // visitor.visit_enum(self) + // } + + forward_to_deserialize_any! { + bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 char str string + bytes byte_buf option unit unit_struct newtype_struct seq tuple + tuple_struct map struct identifier ignored_any enum + } + + fn is_human_readable(&self) -> bool { + true + } +} diff --git a/src/serde_pmt/error.rs b/src/serde_pmt/error.rs new file mode 100644 index 0000000..83a5dd3 --- /dev/null +++ b/src/serde_pmt/error.rs @@ -0,0 +1,39 @@ +use std; +use std::fmt::{self, Display}; + +use serde::{de, ser}; + +pub type Result = std::result::Result; + +#[derive(Debug)] +pub enum Error { + Message(String), + Eof, + KeyMustBeAString, + FloatKeyMustBeFinite, +} + +impl ser::Error for Error { + fn custom(msg: T) -> Self { + Error::Message(msg.to_string()) + } +} + +impl de::Error for Error { + fn custom(msg: T) -> Self { + Error::Message(msg.to_string()) + } +} + +impl Display for Error { + fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + match self { + Error::Message(msg) => formatter.write_str(msg), + Error::Eof => formatter.write_str("unexpected end of input"), + Error::KeyMustBeAString => formatter.write_str("key must be a string"), + Error::FloatKeyMustBeFinite => formatter.write_str("Float key must be finite"), + } + } +} + +impl std::error::Error for Error {} diff --git a/src/serde_pmt/mod.rs b/src/serde_pmt/mod.rs new file mode 100644 index 0000000..94bddc9 --- /dev/null +++ b/src/serde_pmt/mod.rs @@ -0,0 +1,24 @@ +pub mod error; + +mod serialiser; +use futuresdr::runtime::Pmt; +use serde::{de::DeserializeOwned, Serialize}; +pub use serialiser::Serializer; + +use self::deserialiser::PmtDist; +mod deserialiser; + +pub fn to_pmt(value: &T) -> error::Result +where + T: Serialize + ?Sized, +{ + let mut serializer = Serializer {}; + value.serialize(&mut serializer) +} + +pub fn from_pmt(value: Pmt) -> error::Result +where + T: DeserializeOwned, +{ + T::deserialize(PmtDist::from(value)) +} diff --git a/src/serde_pmt/serialiser.rs b/src/serde_pmt/serialiser.rs new file mode 100644 index 0000000..fd7cafc --- /dev/null +++ b/src/serde_pmt/serialiser.rs @@ -0,0 +1,479 @@ +use core::fmt::Display; +use futuresdr::runtime::Pmt; +use serde::{ + ser::{self, Impossible}, + Serialize, +}; +use std::collections::HashMap; + +use super::{ + error::{Error, Result}, + to_pmt, +}; + +pub struct Serializer {} + +impl<'a> serde::Serializer for &'a mut Serializer { + type Ok = Pmt; + + type Error = Error; + + type SerializeSeq = Impossible; // TODO + + type SerializeTuple = Impossible; // TODO + + type SerializeTupleStruct = Impossible; // TODO + + type SerializeTupleVariant = Impossible; // TODO + + type SerializeMap = SerializeMap; + + type SerializeStruct = SerializeMap; + + type SerializeStructVariant = Impossible; // TODO + + fn serialize_bool(self, v: bool) -> Result { + Ok(Pmt::Bool(v)) + } + + fn serialize_i8(self, v: i8) -> Result { + self.serialize_f32(v as f32) + } + + fn serialize_i16(self, v: i16) -> Result { + self.serialize_f32(v as f32) + } + + fn serialize_i32(self, v: i32) -> Result { + self.serialize_f32(v as f32) + } + + fn serialize_i64(self, v: i64) -> Result { + self.serialize_f32(v as f32) + } + + fn serialize_u8(self, v: u8) -> Result { + self.serialize_u32(v as u32) + } + + fn serialize_u16(self, v: u16) -> Result { + self.serialize_u32(v as u32) + } + + fn serialize_u32(self, v: u32) -> Result { + Ok(Pmt::U32(v)) + } + + fn serialize_u64(self, v: u64) -> Result { + Ok(Pmt::U64(v)) + } + + fn serialize_f32(self, v: f32) -> Result { + Ok(Pmt::F32(v)) + } + + fn serialize_f64(self, v: f64) -> Result { + Ok(Pmt::F64(v)) + } + + fn serialize_char(self, v: char) -> Result { + let mut s = String::new(); + s.push(v); + Ok(Pmt::String(s)) + } + + fn serialize_str(self, v: &str) -> Result { + Ok(Pmt::String(v.to_string())) + } + + fn serialize_bytes(self, v: &[u8]) -> Result { + let vec = v.iter().map(|&b| Pmt::U32(b.into())).collect(); + Ok(Pmt::VecPmt(vec)) + } + + fn serialize_none(self) -> Result { + Ok(Pmt::Null) + } + + fn serialize_some(self, value: &T) -> Result + where + T: Serialize, + { + value.serialize(self) + } + + fn serialize_unit(self) -> Result { + Ok(Pmt::Null) + } + + fn serialize_unit_struct(self, _name: &'static str) -> Result { + self.serialize_unit() + } + + fn serialize_unit_variant( + self, + _name: &'static str, + _variant_index: u32, + variant: &'static str, + ) -> std::prelude::v1::Result { + self.serialize_str(variant) + } + + fn serialize_newtype_struct( + self, + _name: &'static str, + value: &T, + ) -> std::prelude::v1::Result + where + T: Serialize, + { + value.serialize(self) + } + + fn serialize_newtype_variant( + self, + _name: &'static str, + _variant_index: u32, + variant: &'static str, + value: &T, + ) -> std::prelude::v1::Result + where + T: Serialize + ?Sized, + { + let mut values = HashMap::::new(); + values.insert(String::from(variant), to_pmt(value)?); + Ok(Pmt::MapStrPmt(values)) + } + + fn serialize_seq( + self, + _len: Option, + ) -> std::prelude::v1::Result { + // Ok(SerializeVec { + // vec: Vec::with_capacity(len.unwrap_or(0)), + // }) + todo!() + } + + fn serialize_tuple( + self, + len: usize, + ) -> std::prelude::v1::Result { + self.serialize_seq(Some(len)) + } + + fn serialize_tuple_struct( + self, + _name: &'static str, + len: usize, + ) -> std::prelude::v1::Result { + self.serialize_seq(Some(len)) + } + + fn serialize_tuple_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _len: usize, + ) -> std::prelude::v1::Result { + // Ok(SerializeTupleVariant { + // name: String::from(variant), + // vec: Vec::with_capacity(len), + // }) + todo!() + } + + fn serialize_map( + self, + _len: Option, + ) -> std::prelude::v1::Result { + Ok(SerializeMap { + map: HashMap::new(), + next_key: None, + }) + } + + fn serialize_struct( + self, + _name: &'static str, + len: usize, + ) -> std::prelude::v1::Result { + self.serialize_map(Some(len)) + } + + fn serialize_struct_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _len: usize, + ) -> std::prelude::v1::Result { + // Ok(SerializeStructVariant { + // name: String::from(variant), + // map: Map::new(), + // }) + todo!() + } + + fn serialize_i128(self, v: i128) -> std::prelude::v1::Result { + let _ = v; + Err(ser::Error::custom("i128 is not supported")) + } + + fn serialize_u128(self, v: u128) -> std::prelude::v1::Result { + let _ = v; + Err(ser::Error::custom("u128 is not supported")) + } + + fn collect_str(self, value: &T) -> Result + where + T: ?Sized + Display, + { + Ok(Pmt::String(value.to_string())) + } +} + +pub struct SerializeMap { + map: HashMap, + next_key: Option, +} + +impl serde::ser::SerializeMap for SerializeMap { + type Ok = Pmt; + type Error = Error; + + fn serialize_key(&mut self, key: &T) -> Result<()> + where + T: ?Sized + Serialize, + { + self.next_key = Some(key.serialize(MapKeySerializer)?); + Ok(()) + } + + fn serialize_value(&mut self, value: &T) -> Result<()> + where + T: ?Sized + Serialize, + { + let key = self.next_key.take(); + // Panic because this indicates a bug in the program rather than an + // expected failure. + let key = key.expect("serialize_value called before serialize_key"); + self.map.insert(key, to_pmt(value)?); + Ok(()) + } + + fn end(self) -> Result { + Ok(Pmt::MapStrPmt(self.map)) + } +} + +struct MapKeySerializer; + +fn key_must_be_a_string() -> Error { + Error::KeyMustBeAString +} + +fn float_key_must_be_finite() -> Error { + Error::FloatKeyMustBeFinite +} + +impl serde::Serializer for MapKeySerializer { + type Ok = String; + type Error = Error; + + type SerializeSeq = Impossible; + type SerializeTuple = Impossible; + type SerializeTupleStruct = Impossible; + type SerializeTupleVariant = Impossible; + type SerializeMap = Impossible; + type SerializeStruct = Impossible; + type SerializeStructVariant = Impossible; + + #[inline] + fn serialize_unit_variant( + self, + _name: &'static str, + _variant_index: u32, + variant: &'static str, + ) -> Result { + Ok(variant.to_owned()) + } + + #[inline] + fn serialize_newtype_struct(self, _name: &'static str, value: &T) -> Result + where + T: ?Sized + Serialize, + { + value.serialize(self) + } + + fn serialize_bool(self, value: bool) -> Result { + Ok(value.to_string()) + } + + fn serialize_i8(self, value: i8) -> Result { + Ok(value.to_string()) + } + + fn serialize_i16(self, value: i16) -> Result { + Ok(value.to_string()) + } + + fn serialize_i32(self, value: i32) -> Result { + Ok(value.to_string()) + } + + fn serialize_i64(self, value: i64) -> Result { + Ok(value.to_string()) + } + + fn serialize_u8(self, value: u8) -> Result { + Ok(value.to_string()) + } + + fn serialize_u16(self, value: u16) -> Result { + Ok(value.to_string()) + } + + fn serialize_u32(self, value: u32) -> Result { + Ok(value.to_string()) + } + + fn serialize_u64(self, value: u64) -> Result { + Ok(value.to_string()) + } + + fn serialize_f32(self, value: f32) -> Result { + if value.is_finite() { + Ok(format!("{:?}", value)) + } else { + Err(float_key_must_be_finite()) + } + } + + fn serialize_f64(self, value: f64) -> Result { + if value.is_finite() { + Ok(format!("{:?}", value)) + } else { + Err(float_key_must_be_finite()) + } + } + + #[inline] + fn serialize_char(self, value: char) -> Result { + Ok({ + let mut s = String::new(); + s.push(value); + s + }) + } + + #[inline] + fn serialize_str(self, value: &str) -> Result { + Ok(value.to_owned()) + } + + fn serialize_bytes(self, _value: &[u8]) -> Result { + Err(key_must_be_a_string()) + } + + fn serialize_unit(self) -> Result { + Err(key_must_be_a_string()) + } + + fn serialize_unit_struct(self, _name: &'static str) -> Result { + Err(key_must_be_a_string()) + } + + fn serialize_newtype_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _value: &T, + ) -> Result + where + T: ?Sized + Serialize, + { + Err(key_must_be_a_string()) + } + + fn serialize_none(self) -> Result { + Err(key_must_be_a_string()) + } + + fn serialize_some(self, _value: &T) -> Result + where + T: ?Sized + Serialize, + { + Err(key_must_be_a_string()) + } + + fn serialize_seq(self, _len: Option) -> Result { + Err(key_must_be_a_string()) + } + + fn serialize_tuple(self, _len: usize) -> Result { + Err(key_must_be_a_string()) + } + + fn serialize_tuple_struct( + self, + _name: &'static str, + _len: usize, + ) -> Result { + Err(key_must_be_a_string()) + } + + fn serialize_tuple_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _len: usize, + ) -> Result { + Err(key_must_be_a_string()) + } + + fn serialize_map(self, _len: Option) -> Result { + Err(key_must_be_a_string()) + } + + fn serialize_struct(self, _name: &'static str, _len: usize) -> Result { + Err(key_must_be_a_string()) + } + + fn serialize_struct_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _len: usize, + ) -> Result { + Err(key_must_be_a_string()) + } + + fn collect_str(self, value: &T) -> Result + where + T: ?Sized + Display, + { + Ok(value.to_string()) + } +} + +impl serde::ser::SerializeStruct for SerializeMap { + type Ok = Pmt; + type Error = Error; + + fn serialize_field(&mut self, key: &'static str, value: &T) -> Result<()> + where + T: ?Sized + Serialize, + { + serde::ser::SerializeMap::serialize_entry(self, key, value) + } + + fn end(self) -> Result { + serde::ser::SerializeMap::end(self) + } +} diff --git a/src/sigmf/mod.rs b/src/sigmf/mod.rs new file mode 100644 index 0000000..5482982 --- /dev/null +++ b/src/sigmf/mod.rs @@ -0,0 +1,131 @@ +mod sigmf_source; +use futuresdr::num_complex::Complex; +pub use sigmf_source::{SigMFSource, SigMFSourceBuilder}; +mod sigmf_sink; +pub use sigmf::*; +pub use sigmf_sink::{SigMFSink, SigMFSinkBuilder}; + +use crate::type_converters::ScaledConverterBuilder; + +pub trait BytesConveter +where + T: Sized, +{ + fn convert(self, bytes: &[u8]) -> T; +} + +impl BytesConveter for DatasetFormat { + fn convert(self, bytes: &[u8]) -> f32 { + use DatasetFormat::*; + match self { + Rf64Le => f64::from_le_bytes(bytes[0..8].try_into().unwrap()) as f32, + Rf64Be => f64::from_ne_bytes(bytes[0..8].try_into().unwrap()) as f32, + // Cf64Le => write!(f, "cf64_le"), + // Cf64Be => write!(f, "cf64_be"), + Rf32Le => f32::from_le_bytes(bytes[0..4].try_into().unwrap()), + Rf32Be => f32::from_be_bytes(bytes[0..4].try_into().unwrap()), + // Cf32Le => write!(f, "cf32_le"), + // Cf32Be => write!(f, "cf32_be"), + Ri32Le => ScaledConverterBuilder::::convert(&i32::from_le_bytes( + bytes[0..4].try_into().unwrap(), + )), + Ri32Be => ScaledConverterBuilder::::convert(&i32::from_be_bytes( + bytes[0..4].try_into().unwrap(), + )), + // Ci32Le => write!(f, "ci32_le"), + // Ci32Be => write!(f, "ci32_be"), + Ri16Le => ScaledConverterBuilder::::convert(&i16::from_le_bytes( + bytes[0..2].try_into().unwrap(), + )), + Ri16Be => ScaledConverterBuilder::::convert(&i16::from_be_bytes( + bytes[0..2].try_into().unwrap(), + )), + // Ci16Le => write!(f, "ci16_le"), + // Ci16Be => write!(f, "ci16_be"), + Ru32Le => ScaledConverterBuilder::::convert(&u32::from_le_bytes( + bytes[0..4].try_into().unwrap(), + )), + Ru32Be => ScaledConverterBuilder::::convert(&u32::from_be_bytes( + bytes[0..4].try_into().unwrap(), + )), + // Cu32Le => write!(f, "cu32_le"), + // Cu32Be => write!(f, "cu32_be"), + Ru16Le => ScaledConverterBuilder::::convert(&u16::from_le_bytes( + bytes[0..2].try_into().unwrap(), + )), + Ru16Be => ScaledConverterBuilder::::convert(&u16::from_be_bytes( + bytes[0..2].try_into().unwrap(), + )), + // Cu16Le => write!(f, "cu16_le"), + // Cu16Be => write!(f, "cu16_be"), + // CI8 => write!(f, "ci8"), + // CU8 => write!(f, "cu8"), + RI8 => ScaledConverterBuilder::::convert(&i8::from_ne_bytes( + bytes[0..1].try_into().unwrap(), + )), + RU8 => ScaledConverterBuilder::::convert(&(bytes[0])), + _ => todo!("not yet implemented"), + } + } +} + +impl BytesConveter for DatasetFormat { + fn convert(self, bytes: &[u8]) -> u8 { + use DatasetFormat::*; + match self { + RU8 => bytes[0], + _ => todo!("not yet implemented"), + } + } +} + +impl BytesConveter for DatasetFormat { + fn convert(self, bytes: &[u8]) -> i8 { + use DatasetFormat::*; + match self { + RI8 => bytes[0] as i8, + _ => todo!("not yet implemented"), + } + } +} + +impl BytesConveter for DatasetFormat { + fn convert(self, bytes: &[u8]) -> u16 { + use DatasetFormat::*; + match self { + Ru16Le => u16::from_le_bytes(bytes[0..2].try_into().unwrap()), + Ru16Be => u16::from_be_bytes(bytes[0..2].try_into().unwrap()), + _ => todo!("not yet implemented"), + } + } +} + +impl BytesConveter for DatasetFormat { + fn convert(self, bytes: &[u8]) -> u32 { + use DatasetFormat::*; + match self { + Ru32Le => u32::from_le_bytes(bytes[0..4].try_into().unwrap()), + Ru32Be => u32::from_be_bytes(bytes[0..4].try_into().unwrap()), + _ => todo!("not yet implemented"), + } + } +} + +impl BytesConveter> for DatasetFormat { + fn convert(self, bytes: &[u8]) -> Complex { + use DatasetFormat::*; + match self { + Cu16Be => Complex::new( + u16::from_be_bytes(bytes[0..2].try_into().unwrap()), + u16::from_be_bytes(bytes[2..4].try_into().unwrap()), + ), + Cu16Le => Complex::new( + u16::from_le_bytes(bytes[0..2].try_into().unwrap()), + u16::from_le_bytes(bytes[2..4].try_into().unwrap()), + ), + Ru16Be => Complex::new(u16::from_be_bytes(bytes[0..2].try_into().unwrap()), 0u16), + Ru16Le => Complex::new(u16::from_le_bytes(bytes[0..2].try_into().unwrap()), 0u16), + _ => todo!("not yet implemented"), + } + } +} diff --git a/src/sigmf/sigmf_sink.rs b/src/sigmf/sigmf_sink.rs new file mode 100644 index 0000000..4d38092 --- /dev/null +++ b/src/sigmf/sigmf_sink.rs @@ -0,0 +1,267 @@ +use std::ffi::OsStr; +use std::io::Write; +use std::path::PathBuf; + +use futuresdr::anyhow::Result; +use futuresdr::runtime::BlockMeta; +use futuresdr::runtime::BlockMetaBuilder; +use futuresdr::runtime::Kernel; +use futuresdr::runtime::MessageIo; +use futuresdr::runtime::MessageIoBuilder; +use futuresdr::runtime::StreamIo; +use futuresdr::runtime::StreamIoBuilder; +use futuresdr::runtime::WorkIo; +use futuresdr::runtime::{Block, Pmt, Tag}; + +use sigmf::Annotation; +use sigmf::{DatasetFormat, DescriptionBuilder}; + +use crate::serde_pmt::from_pmt; + +/// Write samples from a SigMF file. +/// +/// # Inputs +/// +/// `in`: input samples with tags annotations +/// +/// # Outputs +/// +/// None +/// +/// # Usage +/// ```no_run +/// use fsdr_blocks::sigmf::SigMFSinkBuilder; +/// use futuresdr::runtime::Flowgraph; +/// +/// let mut fg = Flowgraph::new(); +/// +/// let mut builder = SigMFSinkBuilder::from("my_filename"); +/// let sink = builder.build::(); +/// ``` +#[cfg_attr(docsrs, doc(cfg(not(target_arch = "wasm32"))))] +pub struct SigMFSink +where + T: Send + 'static + Sized, + W: Write, + M: Write, +{ + pub writer: W, + pub meta_writer: M, + pub description: DescriptionBuilder, + // global_index: usize, + // sample_index: usize, + _sample_type: std::marker::PhantomData, + _writer_type: std::marker::PhantomData, + _meta_writer_type: std::marker::PhantomData, +} + +impl SigMFSink +where + T: Send + 'static + Sized + std::marker::Sync, + W: Write + std::marker::Send + 'static, // + std::marker::Sync + std::marker::Send + std::marker::Unpin, + M: Write + std::marker::Send + 'static, //std::io::Write, // + Send + std::marker::Sync, +{ + /// Create FileSink block + #[allow(clippy::new_ret_no_self)] + pub fn new(writer: W, description: DescriptionBuilder, meta_writer: M) -> Block { + Block::new( + BlockMetaBuilder::new("SigMFSink").build(), + StreamIoBuilder::new().add_input::("in").build(), + MessageIoBuilder::new().build(), + SigMFSink:: { + writer, + meta_writer, + description, + // global_index: 0, + // sample_index: 0, + _sample_type: std::marker::PhantomData, + _writer_type: std::marker::PhantomData, + _meta_writer_type: std::marker::PhantomData, + }, + ) + } +} + +pub fn convert_pmt_to_annotation(value: &Pmt) -> Option { + let annot: crate::serde_pmt::error::Result = from_pmt(value.clone()); + if let Ok(annot) = annot { + //TODO check if at least one field has been deserialized + Some(annot) + } else { + None + } + // match value { + // Pmt::MapStrPmt(dict) => { + // let mut annot = Annotation::default(); + // let mut is_some = false; + // if let Some(Pmt::String(label)) = dict.get("label") { + // annot.label = Some(label.to_owned()); + // is_some = true; + // } + // if let Some(Pmt::String(label)) = dict.get("core:label") { + // annot.label = Some(label.to_owned()); + // is_some = true; + // } + // if let Some(Pmt::Usize(annot_sample_start)) = dict.get("sample_start") { + // annot.sample_start = Some(*annot_sample_start); + // is_some = true; + // } + // if let Some(Pmt::Usize(annot_sample_start)) = dict.get("core:sample_start") { + // annot.sample_start = Some(*annot_sample_start); + // is_some = true; + // } + // if let Some(Pmt::Usize(annot_sample_count)) = dict.get("sample_count") { + // annot.sample_count = Some(*annot_sample_count); + // is_some = true; + // } + // if let Some(Pmt::Usize(annot_sample_count)) = dict.get("core:sample_count") { + // annot.sample_count = Some(*annot_sample_count); + // is_some = true; + // } + // if is_some { + // Some(annot) + // } else { + // None + // } + // } + // _ => None, + // } +} + +#[doc(hidden)] +#[async_trait] +impl Kernel for SigMFSink +where + T: Send + 'static + Sized + std::marker::Sync, + W: Write + Send + 'static, + M: Write + Send, //std::io::Write + Send + std::marker::Sync, +{ + async fn work( + &mut self, + io: &mut WorkIo, + sio: &mut StreamIo, + _mio: &mut MessageIo, + _meta: &mut BlockMeta, + ) -> Result<()> { + let i = sio.input(0).slice_unchecked::(); + + let item_size = std::mem::size_of::(); + let items = i.len() / item_size; + + if items > 0 { + let i = &i[..items * item_size]; + let _ = self.writer.write_all(i)?; + } + for item in sio.input(0).tags() { + // let index = item.index; + #[allow(clippy::single_match)] // Because of todo!() + match &item.tag { + Tag::Data(pmt) => { + if let Some(annot) = convert_pmt_to_annotation(pmt) { + self.description.add_annotation(annot)?; + } + } + _ => { + todo!("Automate other pmt to annotation") + } + } + } + + if sio.input(0).finished() { + io.finished = true; + } + + sio.input(0).consume(items); + Ok(()) + } + + // async fn init( + // &mut self, + // _sio: &mut StreamIo, + // _mio: &mut MessageIo, + // _meta: &mut BlockMeta, + // ) -> Result<()> { + // Ok(()) + // } + + async fn deinit( + &mut self, + _sio: &mut StreamIo, + _mio: &mut MessageIo, + _meta: &mut BlockMeta, + ) -> Result<()> { + let desc = self.description.build()?; + desc.to_writer_pretty(&mut self.meta_writer)?; + Ok(()) + } +} + +pub struct SigMFSinkBuilder { + basename: PathBuf, + datatype: DatasetFormat, +} + +impl SigMFSinkBuilder { + pub fn datatype(self, data: DatasetFormat) -> Self { + SigMFSinkBuilder { + basename: self.basename, + datatype: data, + } + } +} + +impl From<&PathBuf> for SigMFSinkBuilder { + fn from(value: &PathBuf) -> Self { + SigMFSinkBuilder { + basename: value.to_path_buf(), + datatype: DatasetFormat::Cf32Le, + } + } +} + +impl From for SigMFSinkBuilder { + fn from(value: PathBuf) -> Self { + SigMFSinkBuilder { + basename: value.to_path_buf(), + datatype: DatasetFormat::Cf32Le, + } + } +} + +impl From for SigMFSinkBuilder { + fn from(value: String) -> Self { + SigMFSinkBuilder { + basename: PathBuf::from(value), + datatype: DatasetFormat::Cf32Le, + } + } +} + +impl From<&OsStr> for SigMFSinkBuilder { + fn from(value: &OsStr) -> Self { + SigMFSinkBuilder { + basename: PathBuf::from(value), + datatype: DatasetFormat::Cf32Le, + } + } +} + +impl From<&str> for SigMFSinkBuilder { + fn from(value: &str) -> Self { + SigMFSinkBuilder { + basename: PathBuf::from(value), + datatype: DatasetFormat::Cf32Le, + } + } +} + +impl SigMFSinkBuilder { + pub async fn build(&mut self) -> Result { + let desc = DescriptionBuilder::from(self.datatype); + self.basename.set_extension("sigmf-data"); + let actual_file = std::fs::File::create(&self.basename)?; + self.basename.set_extension("sigmf-meta"); + let meta_file = std::fs::File::create(&self.basename)?; + Ok(SigMFSink::::new(actual_file, desc, meta_file)) + } +} diff --git a/src/sigmf/sigmf_source.rs b/src/sigmf/sigmf_source.rs new file mode 100644 index 0000000..f1999bf --- /dev/null +++ b/src/sigmf/sigmf_source.rs @@ -0,0 +1,257 @@ +use std::ffi::OsStr; +use std::path::PathBuf; + +use futuresdr::anyhow::Result; +use futuresdr::futures::AsyncRead; +use futuresdr::futures::AsyncReadExt; +use futuresdr::runtime::BlockMeta; +use futuresdr::runtime::BlockMetaBuilder; +use futuresdr::runtime::Kernel; +use futuresdr::runtime::MessageIo; +use futuresdr::runtime::MessageIoBuilder; +use futuresdr::runtime::StreamIo; +use futuresdr::runtime::StreamIoBuilder; +use futuresdr::runtime::WorkIo; +use futuresdr::runtime::{Block, Tag}; + +use sigmf::RecordingBuilder; +use sigmf::{Annotation, Description}; + +use crate::serde_pmt; + +use super::BytesConveter; + +/// Read samples from a SigMF file. +/// +/// # Inputs +/// +/// No inputs. +/// +/// # Outputs +/// +/// `out`: Output samples +/// +/// # Usage +/// ```no_run +/// use fsdr_blocks::sigmf::SigMFSourceBuilder; +/// use futuresdr::runtime::Flowgraph; +/// +/// let mut fg = Flowgraph::new(); +/// +/// // Loads samples as unsigned 16-bits integer from the file `my_filename.sigmf-data` with +/// // conversion applied depending on the data type actually described in `my_filename.sigmf-meta` +/// let mut builder = SigMFSourceBuilder::from("my_filename"); +/// let source = builder.build::(); +/// ``` +#[cfg_attr(docsrs, doc(cfg(not(target_arch = "wasm32"))))] +pub struct SigMFSource +where + T: Send + 'static + Sized, + R: AsyncRead, + F: FnMut(&[u8]) -> T + Send + 'static, +{ + reader: R, + annotations: Vec, + // captures: Vec, + // global_index: usize, + sample_index: usize, + _sample_type: std::marker::PhantomData, + _reader_type: std::marker::PhantomData, + converter: F, + item_size: usize, +} + +impl SigMFSource +where + T: Send + 'static + Sized + std::marker::Sync, + R: AsyncRead + std::marker::Sync + std::marker::Send + std::marker::Unpin + 'static, + F: FnMut(&[u8]) -> T + Send + 'static, +{ + /// Create FileSource block + #[allow(clippy::new_ret_no_self)] + pub fn new(reader: R, desc: Description, converter: F) -> Result { + let global = desc.global()?; + let datatype = *global.datatype()?; + let annotations = if let Some(annot) = desc.annotations { + annot + } else { + vec![] + }; + // let captures = if let Some(capts) = desc.captures { + // capts + // } else { + // vec![] + // }; + Ok(Block::new( + BlockMetaBuilder::new("SigMFFileSource").build(), + StreamIoBuilder::new().add_output::("out").build(), + MessageIoBuilder::new().build(), + SigMFSource:: { + reader, + annotations, + // captures, + // global_index: 0, + sample_index: 0, + _sample_type: std::marker::PhantomData, + _reader_type: std::marker::PhantomData, + converter, + item_size: datatype.size(), + }, + )) + } +} + +#[doc(hidden)] +#[async_trait] +impl Kernel for SigMFSource +where + T: Send + 'static + Sized + std::marker::Sync, + R: AsyncRead + std::marker::Send + std::marker::Sync + std::marker::Unpin, + F: FnMut(&[u8]) -> T + Send + 'static, +{ + async fn work( + &mut self, + io: &mut WorkIo, + sio: &mut StreamIo, + _mio: &mut MessageIo, + _meta: &mut BlockMeta, + ) -> Result<()> { + let o = sio.output(0).slice::(); + + let mut out = [0u8; 2048]; + let mut i = 0; + // let max_produce = o.len(); + // while i < max_produce { + match self.reader.read(&mut out[i..]).await { + Ok(0) => { + io.finished = true; + // break; + } + Ok(written) => { + for (v, r) in out.chunks_exact(self.item_size).zip(o) { + *r = (self.converter)(v); + } + i += written / self.item_size; + } + Err(e) => panic!("SigMFSource: Error reading data: {e:?}"), + } + // } + + while let Some(annot) = self.annotations.first() { + if let Some(annot_sample_start) = annot.sample_start { + let upper_sample_index = self.sample_index + i; + if (self.sample_index..upper_sample_index).contains(&annot_sample_start) { + let tag = serde_pmt::to_pmt(annot)?; + let tag = Tag::Data(tag); + sio.output(0) + .add_tag(annot_sample_start - self.sample_index, tag); + + self.annotations.remove(0); + } else { + break; + } + } else { + // Skip all annotations without sample_start + self.annotations.remove(0); + } + } + + // println!("written: {:?}", i); + sio.output(0).produce(i); + self.sample_index += i; + + Ok(()) + } + + // async fn init( + // &mut self, + // _sio: &mut StreamIo, + // _mio: &mut MessageIo, + // _meta: &mut BlockMeta, + // ) -> Result<()> { + // Ok(()) + // } +} + +pub struct SigMFSourceBuilder { + basename: PathBuf, +} + +pub struct SigMFSourceBuilderFromReader { + data: R, + desc: Description, +} + +impl From<&PathBuf> for SigMFSourceBuilder { + fn from(value: &PathBuf) -> Self { + SigMFSourceBuilder { + basename: value.to_path_buf(), + } + } +} + +impl From for SigMFSourceBuilder { + fn from(value: PathBuf) -> Self { + SigMFSourceBuilder { + basename: value.to_path_buf(), + } + } +} + +impl From for SigMFSourceBuilder { + fn from(value: String) -> Self { + SigMFSourceBuilder { + basename: PathBuf::from(value), + } + } +} + +impl From<&OsStr> for SigMFSourceBuilder { + fn from(value: &OsStr) -> Self { + SigMFSourceBuilder { + basename: PathBuf::from(value), + } + } +} + +impl From<&str> for SigMFSourceBuilder { + fn from(value: &str) -> Self { + SigMFSourceBuilder { + basename: PathBuf::from(value), + } + } +} + +impl SigMFSourceBuilder { + pub fn with_data_and_description( + reader: R, + desc: Description, + ) -> SigMFSourceBuilderFromReader { + SigMFSourceBuilderFromReader { data: reader, desc } + } + + pub async fn build(&mut self) -> Result + where + sigmf::DatasetFormat: BytesConveter, + { + let mut record = RecordingBuilder::from(&self.basename); + let (_, desc) = record.load_description()?; + let datatype = desc.global()?.datatype()?.to_owned(); + self.basename.set_extension("sigmf-data"); + let actual_file = async_fs::File::open(&self.basename).await?; + SigMFSource::::new(actual_file, desc, move |bytes| datatype.convert(bytes)) + } +} + +impl SigMFSourceBuilderFromReader +where + R: AsyncRead + std::marker::Send + std::marker::Sync + std::marker::Unpin + 'static, +{ + pub async fn build(self) -> Result + where + sigmf::DatasetFormat: BytesConveter, + { + let datatype = *self.desc.global()?.datatype()?; + SigMFSource::::new(self.data, self.desc, move |bytes| datatype.convert(bytes)) + } +} diff --git a/src/type_converters.rs b/src/type_converters.rs index bae12d6..44bc5a7 100644 --- a/src/type_converters.rs +++ b/src/type_converters.rs @@ -106,36 +106,90 @@ where impl ScaledConverterBuilder { pub fn build(self) -> Block { - Apply::new(|i: &u8| -> f32 { (*i as f32) / ((u8::MAX as f32) / 2.0) - 1.0 }) + Apply::new(|i: &u8| -> f32 { ScaledConverterBuilder::::convert(i) }) + } + + pub fn convert(i: &u8) -> f32 { + (*i as f32) / ((u8::MAX as f32) / 2.0) - 1.0 + } +} + +impl ScaledConverterBuilder { + pub fn build(self) -> Block { + Apply::new(|i: &u16| -> f32 { ScaledConverterBuilder::::convert(i) }) + } + + pub fn convert(i: &u16) -> f32 { + (*i as f32) / ((u16::MAX as f32) / 2.0) - 1.0 + } +} + +impl ScaledConverterBuilder { + pub fn build(self) -> Block { + Apply::new(|i: &u32| -> f32 { ScaledConverterBuilder::::convert(i) }) + } + + pub fn convert(i: &u32) -> f32 { + (*i as f32) / ((u32::MAX as f32) / 2.0) - 1.0 } } impl ScaledConverterBuilder { pub fn build(self) -> Block { - Apply::new(|i: &i8| -> f32 { (*i as f32) / ((i8::MAX as f32) / 2.0) - 1.0 }) + Apply::new(|i: &i8| -> f32 { ScaledConverterBuilder::::convert(i) }) + } + + pub fn convert(i: &i8) -> f32 { + (*i as f32) / ((i8::MAX as f32) / 2.0) - 1.0 } } impl ScaledConverterBuilder { pub fn build(self) -> Block { - Apply::new(|i: &i16| -> f32 { (*i as f32) / ((i16::MAX as f32) / 2.0) - 1.0 }) + Apply::new(|i: &i16| -> f32 { ScaledConverterBuilder::::convert(i) }) + } + + pub fn convert(i: &i16) -> f32 { + (*i as f32) / ((i16::MAX as f32) / 2.0) - 1.0 + } +} + +impl ScaledConverterBuilder { + pub fn build(self) -> Block { + Apply::new(|i: &i32| -> f32 { ScaledConverterBuilder::::convert(i) }) + } + + pub fn convert(i: &i32) -> f32 { + (*i as f32) / ((i32::MAX as f32) / 2.0) - 1.0 } } impl ScaledConverterBuilder { pub fn build(self) -> Block { - Apply::new(|i: &f32| -> u8 { (*i * (u8::MAX as f32) * 0.5 + 128.0) as u8 }) + Apply::new(|i: &f32| -> u8 { ScaledConverterBuilder::::convert(i) }) + } + + pub fn convert(i: &f32) -> u8 { + (*i * (u8::MAX as f32) * 0.5 + 128.0) as u8 } } impl ScaledConverterBuilder { pub fn build(self) -> Block { - Apply::new(|i: &f32| -> i8 { (*i * (i8::MAX as f32)) as i8 }) + Apply::new(|i: &f32| -> i8 { ScaledConverterBuilder::::convert(i) }) + } + + pub fn convert(i: &f32) -> i8 { + (*i * (i8::MAX as f32)) as i8 } } impl ScaledConverterBuilder { pub fn build(self) -> Block { - Apply::new(|i: &f32| -> i16 { (*i * (i16::MAX as f32)) as i16 }) + Apply::new(|i: &f32| -> i16 { ScaledConverterBuilder::::convert(i) }) + } + + pub fn convert(i: &f32) -> i16 { + (*i * (i16::MAX as f32)) as i16 } } diff --git a/structure.png b/structure.png new file mode 100644 index 0000000000000000000000000000000000000000..d23caf964650954ccc6722d56fc4948ff673a602 GIT binary patch literal 17394 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 literal 0 HcmV?d00001 diff --git a/tests/serde_pmt.rs b/tests/serde_pmt.rs new file mode 100644 index 0000000..83da886 --- /dev/null +++ b/tests/serde_pmt.rs @@ -0,0 +1,160 @@ +use std::collections::HashMap; + +use futuresdr::anyhow::Result; +use futuresdr::runtime::Pmt; + +use fsdr_blocks::serde_pmt::{from_pmt, to_pmt}; + +#[test] +fn test_pmt_uint32() -> Result<()> { + assert_eq!(Pmt::U32(42u32), to_pmt(&42u32)?); + assert_eq!(Pmt::U32(u32::MAX), to_pmt(&u32::MAX)?); + assert_eq!(Pmt::U32(u32::MIN), to_pmt(&u32::MIN)?); + Ok(()) +} + +#[test] +fn test_u32_pmt() -> Result<()> { + let v: u32 = from_pmt(Pmt::U32(10u32))?; + assert_eq!(10u32, v); + Ok(()) +} + +#[test] +fn test_pmt_none() -> Result<()> { + assert_eq!(Pmt::Null, to_pmt(&Option::::None)?); + Ok(()) +} + +#[test] +fn test_none_pmt() -> Result<()> { + let v: Option = from_pmt(Pmt::Null)?; + assert_eq!(Option::None, v); + Ok(()) +} + +#[test] +fn test_pmt_option_u32() -> Result<()> { + assert_eq!(Pmt::U32(10u32), to_pmt(&Some(10u32))?); + Ok(()) +} + +#[test] +fn test_option_u32_pmt() -> Result<()> { + let v: Option = from_pmt(Pmt::U32(10u32))?; + assert_eq!(Some(10u32), v); + Ok(()) +} + +#[test] +fn test_pmt_string() -> Result<()> { + assert_eq!(Pmt::String("a string".to_string()), to_pmt(&"a string")?); + assert_eq!( + Pmt::String("a string".to_string()), + to_pmt(&("a string".to_string()))? + ); + assert_eq!(Pmt::String("".to_string()), to_pmt(&"")?); + Ok(()) +} + +#[test] +fn test_string_pmt() -> Result<()> { + let v: String = from_pmt(Pmt::String("foo bar".to_string()))?; + assert_eq!("foo bar", v); + Ok(()) +} + +#[test] +fn test_pmt_bool() -> Result<()> { + assert_eq!(Pmt::Bool(true), to_pmt(&true)?); + assert_eq!(Pmt::Bool(false), to_pmt(&false)?); + Ok(()) +} + +#[test] +fn test_pmt_f32() -> Result<()> { + assert_eq!(Pmt::F32(42.3f32), to_pmt(&42.3f32)?); + assert_eq!(Pmt::F32(f32::MAX), to_pmt(&f32::MAX)?); + assert_eq!(Pmt::F32(f32::MIN), to_pmt(&f32::MIN)?); + Ok(()) +} + +#[test] +fn test_pmt_f64() -> Result<()> { + assert_eq!(Pmt::F64(42.3f64), to_pmt(&42.3f64)?); + assert_eq!(Pmt::F64(f64::MAX), to_pmt(&f64::MAX)?); + assert_eq!(Pmt::F64(f64::MIN), to_pmt(&f64::MIN)?); + Ok(()) +} + +#[test] +fn test_pmt_i16() -> Result<()> { + assert_eq!(Pmt::F32(-3f32), to_pmt(&-3i16)?); + assert_eq!(Pmt::F32(5f32), to_pmt(&5i16)?); + assert_eq!(Pmt::F32(i16::MIN as f32), to_pmt(&i16::MIN)?); + Ok(()) +} + +#[test] +fn test_pmt_char() -> Result<()> { + assert_eq!(Pmt::String("a".to_string()), to_pmt(&'a')?); + Ok(()) +} + +// TODO +// #[test] +// fn test_pmt_slice_u8() -> Result<()> { +// let v = [8u8, 0, 5, 45, 255]; +// let expected: Vec = v.iter().map(|x| *x).collect(); +// assert_eq!(Pmt::Blob(expected), to_pmt(&v)?); + +// let v = vec![8u8, 9, 45, 26, 255, 0]; +// let expected = v.clone(); +// let v = &v[..]; +// assert_eq!(Pmt::Blob(expected), to_pmt(v)?); +// Ok(()) +// } + +#[test] +fn test_pmt_option_char() -> Result<()> { + assert_eq!(Pmt::String("a".to_string()), to_pmt(&Some('a'))?); + assert_eq!(Pmt::Null, to_pmt(&Option::::None)?); + Ok(()) +} + +#[test] +fn test_pmt_sigmf_annot() -> Result<()> { + let mut annot = sigmf::Annotation { + sample_start: Some(0), + ..Default::default() + }; + let mut expected = HashMap::new(); + expected.insert("core:sample_start".to_string(), Pmt::U64(0)); + assert_eq!(Pmt::MapStrPmt(expected.clone()), to_pmt(&annot)?); + + annot.comment = Some("a comment".to_string()); + expected.insert("core:comment".to_string(), to_pmt("a comment")?); + assert_eq!(Pmt::MapStrPmt(expected.clone()), to_pmt(&annot)?); + + annot.extra.insert( + "some_ext:some_field".to_string(), + serde_json::to_value(456)?, + ); + expected.insert("some_ext:some_field".to_string(), to_pmt(&456u64)?); + assert_eq!(Pmt::MapStrPmt(expected.clone()), to_pmt(&annot)?); + Ok(()) +} + +#[test] +fn test_sigmf_annot_pmt() -> Result<()> { + let mut annot = sigmf::Annotation { + sample_start: Some(0), + ..Default::default() + }; + annot.sample_start = Some(0); + + let mut value = HashMap::new(); + value.insert("core:sample_start".to_string(), Pmt::U64(0)); + assert_eq!(annot, from_pmt(Pmt::MapStrPmt(value))?); + Ok(()) +} diff --git a/tests/sigmf/mod.rs b/tests/sigmf/mod.rs new file mode 100644 index 0000000..bc5cb61 --- /dev/null +++ b/tests/sigmf/mod.rs @@ -0,0 +1,3 @@ +pub mod sigmf_graph; +pub mod sigmf_sink; +pub mod sigmf_source; diff --git a/tests/sigmf/sigmf_graph.rs b/tests/sigmf/sigmf_graph.rs new file mode 100644 index 0000000..a721524 --- /dev/null +++ b/tests/sigmf/sigmf_graph.rs @@ -0,0 +1,172 @@ +use futuresdr::blocks::VectorSink; + +use fsdr_blocks::sigmf::{BytesConveter, SigMFSink, SigMFSourceBuilder}; +use futuresdr::{ + anyhow::Result, + blocks::{VectorSinkBuilder, VectorSource}, + macros::connect, + runtime::{Flowgraph, Runtime}, +}; + +use futuresdr::futures::io::BufReader; +use futuresdr::futures::io::Cursor; +use sigmf::{Annotation, DatasetFormat, DescriptionBuilder}; + +/// Write the data into a SigMF file, +/// then read it back again +/// and compare data +fn sigmf_write_read(datatype: DatasetFormat, data: Vec) -> Result<()> +where + T: Sized + + 'static + + Clone + + std::marker::Send + + std::marker::Sync + + std::fmt::Debug + + std::cmp::PartialEq, + DatasetFormat: BytesConveter, +{ + let mut fg = Flowgraph::new(); + + let src1 = VectorSource::new(data.clone()); + let data_file_content: Vec = vec![]; + let meta_file_content: Vec = vec![]; + let data_file = std::io::Cursor::new(data_file_content); + let meta_file = std::io::Cursor::new(meta_file_content); + let desc = DescriptionBuilder::from(datatype); + let snk1 = SigMFSink::::new(data_file, desc, meta_file); + connect!(fg, + src1 > snk1; + ); + fg = Runtime::new().run(fg)?; + let snk1 = fg + .kernel::>, std::io::Cursor>>>(snk1) + .unwrap(); + let desc = snk1.description.build()?; + let mut fg = Flowgraph::new(); + let data_file = snk1.writer.to_owned().into_inner(); + let data_file = futuresdr::futures::io::Cursor::new(data_file); + let src2 = futuresdr::futures::executor::block_on( + SigMFSourceBuilder::with_data_and_description(data_file, desc).build::(), + )?; + let snk2 = VectorSinkBuilder::::new().build(); + connect!(fg, + src2 > snk2; + ); + fg = Runtime::new().run(fg)?; + let snk2 = fg.kernel::>(snk2).unwrap().items(); + assert_eq!(data.len(), snk2.len()); + for (o, i) in data.iter().zip(snk2) { + assert_eq!(*o, *i); + } + Ok(()) +} + +#[test] +fn sigmf_write_read_ru8() -> Result<()> { + let data = vec![6u8, 8, 10, 12]; + let datatype = DatasetFormat::RU8; + sigmf_write_read(datatype, data) +} + +#[test] +fn sigmf_write_read_ri8() -> Result<()> { + let data = vec![6i8, 8, -10, 0, 12]; + let datatype = DatasetFormat::RI8; + sigmf_write_read(datatype, data) +} + +#[test] +fn sigmf_read_write_annotation() -> Result<()> { + let data = vec![6u8; 45]; + let datatype = DatasetFormat::RU8; + let mut fg = Flowgraph::new(); + + let mut desc = DescriptionBuilder::from(datatype); + let annot = Annotation { + label: Some("abc".to_string()), + comment: Some("the comment".to_string()), + sample_start: Some(10), + sample_count: Some(20), + ..Annotation::default() + }; + desc.add_annotation(annot)?; + let annot = Annotation { + label: Some("the annot".to_string()), + comment: Some("another comment".to_string()), + sample_start: Some(15), + sample_count: Some(25), + ..Annotation::default() + }; + desc.add_annotation(annot)?; + let desc = desc.build()?; + + let actual_file = Cursor::new(data); + let actual_file = BufReader::new(actual_file); + let src1 = futuresdr::futures::executor::block_on( + SigMFSourceBuilder::with_data_and_description(actual_file, desc).build::(), + )?; + + let data_file_content: Vec = vec![]; + let meta_file_content: Vec = vec![]; + let data_file = std::io::Cursor::new(data_file_content); + let meta_file = std::io::Cursor::new(meta_file_content); + let tgt_desc = DescriptionBuilder::from(datatype); + let snk1 = SigMFSink::::new(data_file, tgt_desc, meta_file); + + // Direct from source to sinkflowgraph + connect!(fg, + src1 > snk1; + ); + // Now run the flowgraph + fg = Runtime::new().run(fg)?; + + // Time to verify + let snk1 = fg + .kernel::>, std::io::Cursor>>>(snk1) + .unwrap(); + let tgt_desc = snk1.description.build()?; + let annotations = tgt_desc.annotations()?; + assert_eq!(2, annotations.len()); + let annot1 = annotations + .first() + .expect("the annotation should have been recreated"); + assert_eq!( + "the comment", + annot1 + .comment + .as_ref() + .expect("comment should have been copied") + .as_str() + ); + assert_eq!( + "abc", + annot1 + .label + .as_ref() + .expect("label should have been copied") + .as_str() + ); + + let annot1 = annotations + .first() + .expect("the annotation should have been recreated"); + assert_eq!( + "the comment", + annot1 + .comment + .as_ref() + .expect("comment should have been copied") + .as_str() + ); + assert_eq!( + "abc", + annot1 + .label + .as_ref() + .expect("label should have been copied") + .as_str() + ); + + Ok(()) +} diff --git a/tests/sigmf/sigmf_sink.rs b/tests/sigmf/sigmf_sink.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/tests/sigmf/sigmf_sink.rs @@ -0,0 +1 @@ + diff --git a/tests/sigmf/sigmf_source.rs b/tests/sigmf/sigmf_source.rs new file mode 100644 index 0000000..0bf4723 --- /dev/null +++ b/tests/sigmf/sigmf_source.rs @@ -0,0 +1,104 @@ +use fsdr_blocks::sigmf::BytesConveter; +use fsdr_blocks::sigmf::SigMFSourceBuilder; +use futuresdr::anyhow::Result; +use futuresdr::blocks::VectorSink; +use futuresdr::blocks::VectorSinkBuilder; +use futuresdr::futures::io::BufReader; +use futuresdr::futures::io::Cursor; +use futuresdr::macros::connect; +use futuresdr::num_complex::Complex; +use futuresdr::runtime::Flowgraph; +use futuresdr::runtime::Runtime; +use sigmf::DatasetFormat; +use sigmf::DescriptionBuilder; + +pub fn test_no_conversion(data: &[u8], datatype: DatasetFormat) -> Result> +where + T: Sized + + std::marker::Send + + std::marker::Sync + + std::marker::Copy + + std::fmt::Debug + + 'static, + DatasetFormat: BytesConveter, +{ + let mut fg = Flowgraph::new(); + let desc = DescriptionBuilder::from(datatype).build()?; + + let actual_file = Cursor::new(Vec::from(data)); + let actual_file = BufReader::new(actual_file); + let src = futuresdr::futures::executor::block_on( + SigMFSourceBuilder::with_data_and_description(actual_file, desc).build::(), + )?; + let snk = VectorSinkBuilder::::new().build(); + + connect!(fg, + src > snk; + ); + + fg = Runtime::new().run(fg)?; + + let snk = fg.kernel::>(snk).unwrap(); + Ok(snk.items().clone()) +} + +#[test] +fn sigmf_source_u8_u8() -> Result<()> { + let data = [6u8, 8, 10, 12]; + let datatype = DatasetFormat::RU8; + let snk = test_no_conversion::(&data, datatype)?; + let expected = [6u8, 8, 10, 12]; + assert_eq!(snk.len(), expected.len()); + for (o, i) in expected.iter().zip(snk) { + assert_eq!(*o, i); + } + Ok(()) +} + +#[test] +fn sigmf_source_u16_u16() -> Result<()> { + let data = [6u8, 8, 10, 12]; + #[cfg(target_endian = "big")] + let datatype = DatasetFormat::Ru16Be; + #[cfg(target_endian = "little")] + let datatype = DatasetFormat::Ru16Le; + let snk = test_no_conversion::(&data, datatype)?; + let expected = [2054, 3082]; + assert_eq!(expected.len(), snk.len()); + for (expected, actual) in expected.iter().zip(snk) { + assert_eq!(*expected, actual); + } + Ok(()) +} + +#[test] +fn sigmf_source_u32_u32() -> Result<()> { + let data = [6u8, 8, 10, 12].repeat(10); + #[cfg(target_endian = "big")] + let datatype = DatasetFormat::Ru32Be; + #[cfg(target_endian = "little")] + let datatype = DatasetFormat::Ru32Le; + let snk = test_no_conversion::(&data, datatype)?; + let expected = [201984006].repeat(10); + assert_eq!(expected.len(), snk.len()); + for (o, i) in expected.iter().zip(snk) { + assert_eq!(*o, i); + } + Ok(()) +} + +#[test] +fn sigmf_source_cu16_cu16() -> Result<()> { + let data = [6u8, 8, 10, 12].repeat(4); + #[cfg(target_endian = "big")] + let datatype = DatasetFormat::Cu16Be; + #[cfg(target_endian = "little")] + let datatype = DatasetFormat::Cu16Le; + let snk = test_no_conversion::>(&data, datatype)?; + let expected = [Complex:: { re: 2054, im: 3082 }].repeat(4); + assert_eq!(expected.len(), snk.len()); + for (o, i) in expected.iter().zip(snk) { + assert_eq!(*o, i); + } + Ok(()) +} diff --git a/tests/tests.rs b/tests/tests.rs index 24a7df5..29e78a1 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -4,4 +4,6 @@ mod channel; mod cw; mod math; +mod serde_pmt; +mod sigmf; mod stream;