rustradio/
lib.rs

1// Enable `std::simd` if feature simd is enabled.
2#![cfg_attr(feature = "simd", feature(portable_simd))]
3// Enable RISC-V arch detection, if on a RISC-V arch.
4#![cfg_attr(
5    all(
6        feature = "simd",
7        any(target_arch = "riscv32", target_arch = "riscv64")
8    ),
9    feature(stdarch_riscv_feature_detection)
10)]
11#![cfg_attr(coverage_nightly, feature(coverage_attribute))]
12
13/*! This create provides a framework for running SDR (software defined
14radio) applications.
15
16It's heavily inspired by [GNURadio][gnuradio], except of course
17written in Rust.
18
19In addition to the example applications in this crate, there's also a
20[sparslog][sparslog] project using this framework, that decodes IKEA
21Sparsnäs electricity meter RF signals.
22
23# Architecture overview
24
25A RustRadio application consists of blocks that are connected by
26unidirectional streams. Each block has zero or more input streams, and
27zero or more output streams.
28
29The signal flows through the blocks from "sources" (blocks without any
30input streams) to "sinks" (blocks without any output streams.
31
32These blocks and streams are called a "graph", like the mathematical
33concept of graphs that have nodes and edges.
34
35A block does something to its input(s), and passes the result to its
36output(s).
37
38A typical graph will be something like:
39
40```text
41  [ Raw radio source ]
4243      [ Filtering ]
4445      [ Resampling ]
4647     [ Demodulation ]
4849     [ Symbol Sync ]
5051[ Packet assembly and save ]
52```
53
54Or concretely, for [sparslog][sparslog]:
55
56```text
57     [ RtlSdrSource ]
5859  [ RtlSdrDecode to convert from ]
60  [ own format to complex I/Q    ]
6162     [ FftFilter ]
6364      [ RationalResampler ]
6566      [ QuadratureDemod ]
6768  [ AddConst for frequency offset ]
6970   [ ZeroCrossing symbol sync ]
7172     [ Custom Sparsnäs decoder ]
73     [ block in the binary,    ]
74     [ not in the framework    ]
75```
76
77# Examples
78
79Here's a simple example that creates a couple of blocks, connects them
80with streams, and runs the graph.
81
82```
83use rustradio::graph::{Graph, GraphRunner};
84use rustradio::blocks::{AddConst, VectorSource, DebugSink};
85use rustradio::Complex;
86let (src, prev) = VectorSource::new(
87    vec![
88        Complex::new(10.0, 0.0),
89        Complex::new(-20.0, 0.0),
90        Complex::new(100.0, -100.0),
91    ],
92);
93let (add, prev) = AddConst::new(prev, Complex::new(1.1, 2.0));
94let sink = DebugSink::new(prev);
95let mut g = Graph::new();
96g.add(Box::new(src));
97g.add(Box::new(add));
98g.add(Box::new(sink));
99g.run()?;
100# Ok::<(), anyhow::Error>(())
101```
102
103## Links
104
105* Main repo: <https://github.com/ThomasHabets/rustradio>
106* crates.io: <https://crates.io/crates/rustradio>
107* This documentation: <https://docs.rs/rustradio/latest/rustradio/>
108
109[sparslog]: https://github.com/ThomasHabets/sparslog
110[gnuradio]: https://www.gnuradio.org/
111 */
112// Macro.
113pub use rustradio_macros;
114
115// Blocks.
116pub mod add;
117pub mod add_const;
118pub mod au;
119pub mod binary_slicer;
120pub mod burst_tagger;
121pub mod cma;
122pub mod complex_to_mag2;
123pub mod constant_source;
124pub mod convert;
125pub mod correlate_access_code;
126pub mod debug_sink;
127pub mod delay;
128pub mod descrambler;
129pub mod fft;
130pub mod fft_filter;
131pub mod fft_stream;
132pub mod file_sink;
133pub mod file_source;
134pub mod fir;
135pub mod hasher;
136pub mod hdlc_deframer;
137pub mod hilbert;
138pub mod iir_filter;
139pub mod il2p_deframer;
140pub mod multiply_const;
141pub mod nrzi;
142pub mod null_sink;
143pub mod pdu_writer;
144pub mod quadrature_demod;
145pub mod rational_resampler;
146pub mod rtlsdr_decode;
147pub mod sigmf;
148pub mod signal_source;
149pub mod single_pole_iir_filter;
150pub mod skip;
151pub mod stream_to_pdu;
152pub mod symbol_sync;
153pub mod tcp_source;
154pub mod tee;
155pub mod to_text;
156pub mod vec_to_stream;
157pub mod vector_sink;
158pub mod vector_source;
159pub mod wpcr;
160pub mod xor;
161pub mod xor_const;
162pub mod zero_crossing;
163
164#[cfg(feature = "audio")]
165pub mod audio_sink;
166
167#[cfg(feature = "rtlsdr")]
168pub mod rtlsdr_source;
169
170#[cfg(feature = "soapysdr")]
171pub mod soapysdr_sink;
172
173#[cfg(feature = "soapysdr")]
174pub mod soapysdr_source;
175
176pub mod block;
177pub mod blocks;
178pub mod circular_buffer;
179pub mod graph;
180pub mod mtgraph;
181pub mod stream;
182pub mod window;
183
184#[cfg(feature = "async")]
185pub mod agraph;
186
187/// Float type used. Usually f32, but not guaranteed.
188pub type Float = f32;
189
190/// Complex (I/Q) data.
191pub type Complex = num_complex::Complex<Float>;
192
193/// RustRadio error.
194#[derive(thiserror::Error, Debug)]
195#[non_exhaustive]
196pub enum Error {
197    /// File error annotated with a specific path.
198    #[error("IO Error on {path:?}: {source:?}")]
199    FileIo {
200        #[source]
201        source: std::io::Error,
202        path: std::path::PathBuf,
203    },
204
205    /// An error happened with a device such as SDR or audio device.
206    #[error("DeviceError: {msg:?}: {source:?}")]
207    DeviceError {
208        #[source]
209        source: Box<dyn std::error::Error + Send + Sync>,
210        msg: Option<String>,
211    },
212
213    /// An IO error without a known file associated.
214    #[error("IO Error: {0}")]
215    Io(#[from] std::io::Error),
216
217    /// An error with only a plain text message.
218    #[error("An error occurred: {0}")]
219    Plain(String),
220
221    /// A wrapper around another error.
222    #[error("{msg:?}: {source:?}")]
223    Other {
224        #[source]
225        source: Box<dyn std::error::Error + Send + Sync>,
226        msg: Option<String>,
227    },
228}
229
230impl Error {
231    /// Create error from message.
232    #[must_use]
233    pub fn msg<S: Into<String>>(msg: S) -> Self {
234        Self::Plain(msg.into())
235    }
236
237    /// Wrap an IO error also including the path.
238    #[must_use]
239    pub fn file_io<P: Into<std::path::PathBuf>>(source: std::io::Error, path: P) -> Self {
240        Self::FileIo {
241            path: path.into(),
242            source,
243        }
244    }
245
246    /// Wrap another error into an `Error::Other`.
247    ///
248    /// The underlying error is provided, as well as optional extra context.
249    #[must_use]
250    pub fn wrap<S: Into<String>>(
251        source: impl std::error::Error + Send + Sync + 'static,
252        msg: S,
253    ) -> Self {
254        let msg = msg.into();
255        Self::Other {
256            source: Box::new(source),
257            msg: if msg.is_empty() { None } else { Some(msg) },
258        }
259    }
260
261    /// Wrap an error blaming some hardware or simulated hardware.
262    ///
263    /// The underlying error is provided, as well as optional extra context.
264    #[must_use]
265    pub fn device<S: Into<String>>(
266        source: impl std::error::Error + Send + Sync + 'static,
267        msg: S,
268    ) -> Self {
269        let msg = msg.into();
270        Self::DeviceError {
271            source: Box::new(source),
272            msg: if msg.is_empty() { None } else { Some(msg) },
273        }
274    }
275}
276
277#[macro_export]
278macro_rules! error_from {
279    ($ctx:literal, $($err_ty:ty),* $(,)?) => {
280        $(
281            impl From<$err_ty> for Error {
282                fn from(e: $err_ty) -> Self {
283                    let s = if $ctx.is_empty() {
284                        format!("{}", std::any::type_name::<$err_ty>())
285                    } else {
286                        format!("{} in {}", std::any::type_name::<$err_ty>(), $ctx)
287                    };
288                    Error::wrap(e, s)
289                }
290            }
291        )*
292    };
293}
294
295#[macro_export]
296macro_rules! blockchain {
297    ($g:expr, $prev:ident, $($cons:expr),* $(,)?) => {{
298        $(
299            let (block, $prev) = $cons;
300            $g.add(Box::new(block));
301            )*
302            $prev
303    }};
304}
305
306error_from!(
307    "", // Can't attribute to a specific set of blocks.
308    std::sync::mpsc::RecvError,
309    std::sync::mpsc::TryRecvError,
310    std::string::FromUtf8Error,
311    std::array::TryFromSliceError,
312    std::num::TryFromIntError,
313);
314
315pub type Result<T> = std::result::Result<T, Error>;
316
317/// Repeat between zero and infinite times.
318#[derive(Debug)]
319pub struct Repeat {
320    repeater: Repeater,
321    count: u64,
322}
323
324impl Repeat {
325    /// Repeat finite number of times. 0 Means not even once. 1 is default.
326    #[must_use]
327    pub fn finite(n: u64) -> Self {
328        Self {
329            repeater: Repeater::Finite(n),
330            count: 0,
331        }
332    }
333
334    /// Repeat infinite number of times.
335    #[must_use]
336    pub fn infinite() -> Self {
337        Self {
338            repeater: Repeater::Infinite,
339            count: 0,
340        }
341    }
342
343    /// Register a repeat being done, and return true if we should continue.
344    #[must_use]
345    pub fn again(&mut self) -> bool {
346        self.count += 1;
347        match self.repeater {
348            Repeater::Finite(n) => {
349                self.repeater = Repeater::Finite(n - 1);
350                n > 1
351            }
352            Repeater::Infinite => true,
353        }
354    }
355
356    /// Return true if repeating is done.
357    #[must_use]
358    pub fn done(&self) -> bool {
359        match self.repeater {
360            Repeater::Finite(n) => n == 0,
361            Repeater::Infinite => false,
362        }
363    }
364
365    /// Return how many repeats have fully completed.
366    #[must_use]
367    pub fn count(&self) -> u64 {
368        self.count
369    }
370}
371
372#[derive(Debug)]
373enum Repeater {
374    Finite(u64),
375    Infinite,
376}
377
378pub struct Feature {
379    name: String,
380    build: bool,
381    detected: bool,
382}
383
384impl Feature {
385    #[must_use]
386    fn new<S: Into<String>>(name: S, build: bool, detected: bool) -> Self {
387        Self {
388            name: name.into(),
389            build,
390            detected,
391        }
392    }
393}
394
395#[must_use]
396pub fn environment_str(features: &[Feature]) -> String {
397    let mut s = "Feature   Build Detected\n".to_string();
398    for feature in features {
399        s += &format!(
400            "{:10} {:-5}    {:-5}\n",
401            feature.name, feature.build, feature.detected
402        );
403    }
404    s
405}
406
407pub fn check_environment() -> Result<Vec<Feature>> {
408    #[allow(unused_mut)]
409    let mut assumptions: Vec<Feature> = Vec::new();
410    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
411    {
412        assumptions.push(Feature::new(
413            "FMA",
414            cfg!(target_feature = "fma"),
415            is_x86_feature_detected!("fma"),
416        ));
417        assumptions.push(Feature::new(
418            "SSE",
419            cfg!(target_feature = "sse"),
420            is_x86_feature_detected!("sse"),
421        ));
422        assumptions.push(Feature::new(
423            "SSE3",
424            cfg!(target_feature = "sse3"),
425            is_x86_feature_detected!("sse3"),
426        ));
427        assumptions.push(Feature::new(
428            "AVX",
429            cfg!(target_feature = "avx"),
430            is_x86_feature_detected!("avx"),
431        ));
432        assumptions.push(Feature::new(
433            "AVX2",
434            cfg!(target_feature = "avx2"),
435            is_x86_feature_detected!("avx2"),
436        ));
437    }
438
439    // TODO: ideally we don't duplicate this test here, but reuse it from the
440    // top of the file.
441    //
442    // We check for feature `simd` here as a substitute for checking we're on
443    // nightly, where the feature stuff is allowed.
444    #[cfg(all(
445        feature = "simd",
446        any(target_arch = "riscv32", target_arch = "riscv64")
447    ))]
448    {
449        assumptions.push(Feature::new(
450            "Vector",
451            cfg!(target_feature = "v"),
452            std::arch::is_riscv_feature_detected!("v"),
453        ));
454    }
455
456    let errs: Vec<_> = assumptions
457        .iter()
458        .filter_map(|f| {
459            if f.build && !f.detected {
460                Some(format!(
461                    "Feature {} assumed by build flags but not detected",
462                    f.name
463                ))
464            } else {
465                None
466            }
467        })
468        .collect();
469    if errs.is_empty() {
470        Ok(assumptions)
471    } else {
472        Err(Error::msg(format!("{errs:?}")))
473    }
474}
475
476/// A trait all sample types must implement.
477pub trait Sample: Copy + Default + Send + Sync + 'static {
478    /// The type of the sample.
479    type Type;
480
481    /// The serialized size of one sample.
482    #[must_use]
483    fn size() -> usize;
484
485    /// Parse one sample.
486    fn parse(data: &[u8]) -> Result<Self::Type>;
487
488    /// Serialize one sample.
489    #[must_use]
490    fn serialize(&self) -> Vec<u8>;
491}
492
493impl Sample for Complex {
494    type Type = Complex;
495    fn size() -> usize {
496        std::mem::size_of::<Self>()
497    }
498    fn parse(data: &[u8]) -> Result<Self::Type> {
499        if data.len() != Self::size() {
500            panic!("TODO: Complex is wrong size");
501        }
502        let i = Float::from_le_bytes(data[0..Self::size() / 2].try_into()?);
503        let q = Float::from_le_bytes(data[Self::size() / 2..].try_into()?);
504        Ok(Complex::new(i, q))
505    }
506    fn serialize(&self) -> Vec<u8> {
507        let mut ret = Vec::new();
508        ret.extend(Float::to_le_bytes(self.re));
509        ret.extend(Float::to_le_bytes(self.im));
510        ret
511    }
512}
513
514impl Sample for Float {
515    type Type = Float;
516    fn size() -> usize {
517        std::mem::size_of::<Self>()
518    }
519    fn parse(data: &[u8]) -> Result<Self::Type> {
520        if data.len() != Self::size() {
521            panic!("TODO: Float is wrong size");
522        }
523        Ok(Float::from_le_bytes(data[0..Self::size()].try_into()?))
524    }
525    fn serialize(&self) -> Vec<u8> {
526        Float::to_le_bytes(*self).to_vec()
527    }
528}
529
530impl Sample for u8 {
531    type Type = u8;
532    fn size() -> usize {
533        std::mem::size_of::<Self>()
534    }
535    fn parse(data: &[u8]) -> Result<Self::Type> {
536        if data.len() != Self::size() {
537            panic!("TODO: u8 is wrong size");
538        }
539        Ok(data[0])
540    }
541    fn serialize(&self) -> Vec<u8> {
542        vec![*self]
543    }
544}
545
546impl Sample for u32 {
547    type Type = u32;
548    fn size() -> usize {
549        4
550    }
551    fn parse(data: &[u8]) -> Result<Self::Type> {
552        if data.len() != Self::size() {
553            panic!("TODO: Float is wrong size");
554        }
555        Ok(u32::from_le_bytes(data[0..Self::size()].try_into()?))
556    }
557    fn serialize(&self) -> Vec<u8> {
558        u32::to_le_bytes(*self).to_vec()
559    }
560}
561
562impl Sample for i32 {
563    type Type = i32;
564    fn size() -> usize {
565        std::mem::size_of::<Self>()
566    }
567    fn parse(data: &[u8]) -> Result<Self::Type> {
568        if data.len() != Self::size() {
569            panic!("TODO: Float is wrong size");
570        }
571        Ok(i32::from_le_bytes(data[0..Self::size()].try_into()?))
572    }
573    fn serialize(&self) -> Vec<u8> {
574        i32::to_le_bytes(*self).to_vec()
575    }
576}
577
578/// Trivial trait for types that have .len().
579#[allow(clippy::len_without_is_empty)]
580pub trait Len {
581    /// Get the length.
582    #[must_use]
583    fn len(&self) -> usize;
584}
585impl<T> Len for Vec<T> {
586    fn len(&self) -> usize {
587        self.len()
588    }
589}
590
591#[cfg(test)]
592#[cfg_attr(coverage_nightly, coverage(off))]
593pub mod tests {
594    //! Test helper functions.
595    use super::*;
596
597    /// For testing, assert that two slices are almost equal.
598    ///
599    /// Floating point numbers are almost never exactly equal.
600    pub fn assert_almost_equal_complex(left: &[Complex], right: &[Complex]) {
601        assert_eq!(
602            left.len(),
603            right.len(),
604            "\nleft: {left:?}\nright: {right:?}",
605        );
606        for i in 0..left.len() {
607            let dist = (left[i] - right[i]).norm_sqr().sqrt();
608            if dist > 0.001 {
609                assert_eq!(
610                    left[i], right[i],
611                    "\nElement {i}:\nleft: {left:?}\nright: {right:?}",
612                );
613            }
614        }
615    }
616
617    /// For testing, assert that two slices are almost equal.
618    ///
619    /// Floating point numbers are almost never exactly equal.
620    pub fn assert_almost_equal_float(left: &[Float], right: &[Float]) {
621        assert_eq!(
622            left.len(),
623            right.len(),
624            "\nleft: {left:?}\nright: {right:?}",
625        );
626        for i in 0..left.len() {
627            let dist = (left[i] - right[i]).sqrt();
628            if dist > 0.001 {
629                assert_eq!(left[i], right[i], "\nleft: {left:?}\nright: {right:?}");
630            }
631        }
632    }
633
634    #[test]
635    fn check_env() -> Result<()> {
636        assert!(!environment_str(&check_environment()?).is_empty());
637        Ok(())
638    }
639
640    #[test]
641    fn error_wrap() {
642        use std::error::Error as SysError;
643        let e = Error::msg("foo");
644        assert!(matches![e, Error::Plain(_)]);
645        let _e2: &dyn std::error::Error = &e;
646        let e_str = e.to_string();
647        assert_eq!(e_str, "An error occurred: foo");
648        let e3 = Error::wrap(e, "foo");
649        assert!(matches![e3, Error::Other { source: _, msg: _ }]);
650        let e4 = e3.source().unwrap();
651        assert_eq!(e_str, e4.to_string());
652        let e5 = e4.downcast_ref::<Error>().unwrap();
653        assert!(matches![e5, Error::Plain(_)]);
654    }
655}