futuresdr 0.7.0

An Experimental Async SDR Runtime for Heterogeneous Architectures.
Documentation
use seify::Direction;
use seify::DynDevice;
use std::collections::HashMap;

use crate::runtime::Error;
use crate::runtime::Pmt;

/// Seify Config
#[derive(Debug, Default, Clone)]
pub struct Config {
    /// Configured-channel index for targeted updates.
    pub chan: Option<usize>,
    /// Antenna
    pub antenna: Option<String>,
    /// Bandwidth
    pub bandwidth: Option<f64>,
    /// Frequency
    pub freq: Option<f64>,
    /// Gain (in dB)
    pub gain: Option<f64>,
    /// Sample Rate
    pub sample_rate: Option<f64>,
}

impl Config {
    /// Create Seify Config
    pub fn new() -> Self {
        Self::default()
    }

    /// Generate a [`Pmt`] that can be used as a "cmd" port message
    pub fn to_pmt(&self) -> Pmt {
        Pmt::Any(Box::new(self.clone()))
    }

    /// Generate a [`Pmt`] that can be serialized
    pub fn to_serializable_pmt(&self) -> Pmt {
        let mut m = HashMap::new();
        if let Some(chan) = self.chan {
            m.insert("chan".to_string(), Pmt::U64(chan as u64));
        }
        if let Some(antenna) = &self.antenna {
            m.insert("antenna".to_string(), Pmt::String(antenna.clone()));
        }
        if let Some(bandwidth) = &self.bandwidth {
            m.insert("bandwidth".to_string(), Pmt::F64(*bandwidth));
        }
        if let Some(freq) = &self.freq {
            m.insert("freq".to_string(), Pmt::F64(*freq));
        }
        if let Some(gain) = &self.gain {
            m.insert("gain".to_string(), Pmt::F64(*gain));
        }
        if let Some(sample_rate) = &self.sample_rate {
            m.insert("sample_rate".to_string(), Pmt::F64(*sample_rate));
        }
        Pmt::MapStrPmt(m)
    }

    /// Apply config to a device
    pub fn apply(&self, dev: &DynDevice, channels: &[usize], dir: Direction) -> Result<(), Error> {
        if let Some(chan) = self.selected_channel(channels)? {
            self.apply_channel(dev, dir, chan)?;
        } else {
            for &chan in channels {
                self.apply_channel(dev, dir, chan)?;
            }
        }

        Ok(())
    }

    fn apply_channel(&self, dev: &DynDevice, dir: Direction, chan: usize) -> Result<(), Error> {
        match dir {
            Direction::Rx => {
                let channel = dev.rx(chan)?;
                if let Some(ref a) = self.antenna {
                    channel.antenna().select(a)?;
                }
                if let Some(b) = self.bandwidth {
                    channel.bandwidth().set(b)?;
                }
                if let Some(f) = self.freq {
                    channel.frequency().set(f)?;
                }
                if let Some(g) = self.gain {
                    channel.gain().set(g)?;
                }
                if let Some(s) = self.sample_rate {
                    channel.sample_rate().set(s)?;
                }
            }
            Direction::Tx => {
                let channel = dev.tx(chan)?;
                if let Some(ref a) = self.antenna {
                    channel.antenna().select(a)?;
                }
                if let Some(b) = self.bandwidth {
                    channel.bandwidth().set(b)?;
                }
                if let Some(f) = self.freq {
                    channel.frequency().set(f)?;
                }
                if let Some(g) = self.gain {
                    channel.gain().set(g)?;
                }
                if let Some(s) = self.sample_rate {
                    channel.sample_rate().set(s)?;
                }
            }
        }

        Ok(())
    }

    fn selected_channel(&self, channels: &[usize]) -> Result<Option<usize>, Error> {
        self.chan
            .map(|idx| channels.get(idx).copied().ok_or(Error::InvalidParameter))
            .transpose()
    }

    /// Extracts a [`Config`] from a [`DynDevice`], [`Direction`], and channel id.
    pub fn from(dev: &DynDevice, dir: Direction, channel: usize) -> Result<Self, Error> {
        let (antenna, bandwidth, freq, gain, sample_rate) = match dir {
            Direction::Rx => {
                let channel = dev.rx(channel)?;
                (
                    Self::optional_control(channel.antenna().selected())?,
                    Self::optional_control(channel.bandwidth().value())?,
                    Self::optional_control(channel.frequency().value())?,
                    Self::optional_control(channel.gain().value())?.flatten(),
                    Self::optional_control(channel.sample_rate().value())?,
                )
            }
            Direction::Tx => {
                let channel = dev.tx(channel)?;
                (
                    Self::optional_control(channel.antenna().selected())?,
                    Self::optional_control(channel.bandwidth().value())?,
                    Self::optional_control(channel.frequency().value())?,
                    Self::optional_control(channel.gain().value())?.flatten(),
                    Self::optional_control(channel.sample_rate().value())?,
                )
            }
        };

        Ok(Config {
            chan: None,
            antenna,
            bandwidth,
            freq,
            gain,
            sample_rate,
        })
    }

    fn optional_control<T>(value: Result<T, seify::Error>) -> Result<Option<T>, Error> {
        match value {
            Ok(value) => Ok(Some(value)),
            Err(e) if e.is_unsupported() => Ok(None),
            Err(e) => Err(e.into()),
        }
    }
}

impl TryFrom<Pmt> for Config {
    type Error = Error;

    fn try_from(pmt: Pmt) -> Result<Self, Self::Error> {
        match pmt {
            Pmt::Any(a) => {
                if let Some(cfg) = a.downcast_ref::<Self>() {
                    Ok(cfg.clone())
                } else {
                    Err(Error::PmtConversionError)
                }
            }
            Pmt::MapStrPmt(mut m) => {
                let mut cfg = Config::default();
                for (n, v) in m.drain() {
                    match (n.as_str(), v) {
                        ("chan", Pmt::U32(chan)) => {
                            cfg.chan = Some(chan as usize);
                        }
                        ("chan", Pmt::U64(chan)) => {
                            cfg.chan = Some(chan as usize);
                        }
                        ("chan", Pmt::Usize(chan)) => {
                            cfg.chan = Some(chan);
                        }
                        ("antenna", Pmt::String(p)) => {
                            cfg.antenna = Some(p.to_owned());
                        }
                        ("bandwidth", p) => {
                            cfg.bandwidth = Some(p.try_into()?);
                        }
                        ("freq", p) => {
                            cfg.freq = Some(p.try_into()?);
                        }
                        ("gain", p) => {
                            cfg.gain = Some(p.try_into()?);
                        }
                        ("sample_rate", p) => {
                            cfg.sample_rate = Some(p.try_into()?);
                        }
                        // If unknown, log a warning but otherwise ignore
                        _ => warn!("unrecognized key name: {}", n),
                    }
                }
                Ok(cfg)
            }
            _ => Err(Error::PmtConversionError),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::Config;
    use crate::runtime::Error;
    use crate::runtime::Pmt;
    use std::collections::HashMap;

    #[test]
    fn serializable_pmt_includes_chan() {
        let cfg = Config {
            chan: Some(1),
            freq: Some(102e6),
            ..Default::default()
        };

        let Pmt::MapStrPmt(map) = cfg.to_serializable_pmt() else {
            panic!("expected map");
        };

        assert_eq!(map.get("chan"), Some(&Pmt::U64(1)));
        assert_eq!(map.get("freq"), Some(&Pmt::F64(102e6)));
    }

    #[test]
    fn map_pmt_parses_chan() {
        let cfg = Config::try_from(Pmt::MapStrPmt(HashMap::from([(
            "chan".to_string(),
            Pmt::U32(2),
        )])))
        .unwrap();

        assert_eq!(cfg.chan, Some(2));
    }

    #[test]
    fn selected_channel_maps_configured_index() {
        let cfg = Config {
            chan: Some(1),
            ..Default::default()
        };

        assert_eq!(cfg.selected_channel(&[2, 4]).unwrap(), Some(4));
    }

    #[test]
    fn selected_channel_rejects_out_of_range_index() {
        let cfg = Config {
            chan: Some(2),
            ..Default::default()
        };

        assert_eq!(cfg.selected_channel(&[2, 4]), Err(Error::InvalidParameter));
    }
}