maia_httpd/
spectrometer.rs

1//! Spectrometer.
2//!
3//! This module is used for the control of the spectrometer included in the Maia
4//! SDR FPGA IP core.
5
6use crate::{app::AppState, fpga::InterruptWaiter};
7use anyhow::Result;
8use bytes::Bytes;
9use maia_json::SpectrometerMode;
10use std::sync::Mutex;
11use tokio::sync::broadcast;
12
13// Used to obtain values in dB which are positive
14const BASE_SCALE: f32 = 4e6;
15
16/// Spectrometer.
17///
18/// This struct waits for interrupts from the spectrometer in the FPGA IP core,
19/// reads the spectrum data, transforms it from `u64` to `f32` format, and sends
20/// it (serialized into [`Bytes`]) into a [`tokio::sync::broadcast::Sender`].
21#[derive(Debug)]
22pub struct Spectrometer {
23    state: AppState,
24    sender: broadcast::Sender<Bytes>,
25    interrupt: InterruptWaiter,
26}
27
28/// Spectrometer configuration setter.
29///
30/// This struct gives shared access to getters and setters for the spectrometer
31/// sample rate and mode. It is used to update the sample rate and mode from
32/// other parts of the code.
33#[derive(Debug)]
34pub struct SpectrometerConfig(Mutex<Config>);
35
36#[derive(Debug, Clone)]
37struct Config {
38    samp_rate: f32,
39    mode: SpectrometerMode,
40}
41
42impl Spectrometer {
43    /// Creates a new spectrometer struct.
44    ///
45    /// The `interrupt` parameter should correspond to the [`InterruptWaiter`]
46    /// corresponding to the spectrometer. Each spectra received from the FPGA
47    /// is sent to the `sender`.
48    pub fn new(
49        state: AppState,
50        interrupt: InterruptWaiter,
51        sender: broadcast::Sender<Bytes>,
52    ) -> Spectrometer {
53        Spectrometer {
54            state,
55            interrupt,
56            sender,
57        }
58    }
59
60    /// Runs the spectrometer.
61    ///
62    /// This function only returns if there is an error. The function should be
63    /// run concurrently with the rest of the application for the spectrometer
64    /// to work.
65    #[tracing::instrument(name = "spectrometer", skip_all)]
66    pub async fn run(self) -> Result<()> {
67        loop {
68            self.interrupt.wait().await;
69            let (samp_rate, mode) = self.state.spectrometer_config().samp_rate_mode();
70            let mut ip_core = self.state.ip_core().lock().unwrap();
71            let num_integrations = ip_core.spectrometer_number_integrations() as f32;
72            let scale = match mode {
73                SpectrometerMode::Average => BASE_SCALE / (num_integrations * samp_rate),
74                SpectrometerMode::PeakDetect => BASE_SCALE / samp_rate,
75            };
76            tracing::trace!(
77                last_buffer = ip_core.spectrometer_last_buffer(),
78                samp_rate,
79                num_integrations,
80                scale
81            );
82            // TODO: potential optimization: do not hold the mutex locked while
83            // we iterate over the buffers.
84            for buffer in ip_core.get_spectrometer_buffers() {
85                if self.sender.receiver_count() > 0 {
86                    // It is ok if send returns Err, because there might be
87                    // no receiver handles in this moment.
88                    let _ = self.sender.send(Self::buffer_u64fp_to_f32(buffer, scale));
89                }
90            }
91        }
92    }
93
94    fn buffer_u64fp_to_f32(buffer: &[u64], scale: f32) -> Bytes {
95        // The spectrometer output is in "floating point" format with an
96        // exponent that occupies the 8 MSBs of the 64 value and represents
97        // powers of 4, and a mantissa that occupies the LSBs. The way to parse
98        // this representation is to separate the exponent and the mantissa and
99        // to shift left the mantissa by 2 times the exponent places.
100
101        // TODO: optimize using Neon
102        buffer
103            .iter()
104            .flat_map(|&x| {
105                let exponent = (x >> 56) as u8;
106                let value = x & ((1u64 << 56) - 1);
107                let y = value << (2 * exponent);
108                let z = y as f32 * scale;
109                z.to_ne_bytes().into_iter()
110            })
111            .collect()
112    }
113}
114
115impl SpectrometerConfig {
116    /// Creates a new spectrometer configuration object.
117    fn new() -> SpectrometerConfig {
118        SpectrometerConfig(Mutex::new(Config {
119            samp_rate: 0.0,
120            mode: SpectrometerMode::Average,
121        }))
122    }
123
124    /// Returns the spectrometer sample rate.
125    ///
126    /// The units are samples per second.
127    pub fn samp_rate(&self) -> f32 {
128        self.0.lock().unwrap().samp_rate
129    }
130
131    /// Returns the spectrometer mode.
132    pub fn mode(&self) -> SpectrometerMode {
133        self.0.lock().unwrap().mode
134    }
135
136    /// Returns the spectrometer sample rate and mode
137    pub fn samp_rate_mode(&self) -> (f32, SpectrometerMode) {
138        let conf = self.0.lock().unwrap();
139        (conf.samp_rate, conf.mode)
140    }
141
142    /// Sets the spectrometer sample rate.
143    ///
144    /// Updates the spectrometer sample rate to the value give, in units of
145    /// samples per second.
146    pub fn set_samp_rate(&self, samp_rate: f32) {
147        self.0.lock().unwrap().samp_rate = samp_rate;
148    }
149
150    /// Sets the spectrometer mode.
151    pub fn set_mode(&self, mode: SpectrometerMode) {
152        self.0.lock().unwrap().mode = mode;
153    }
154
155    /// Sets the spectrometer sample rate and mode.
156    pub fn set_samp_rate_mode(&self, samp_rate: f32, mode: SpectrometerMode) {
157        let mut conf = self.0.lock().unwrap();
158        conf.samp_rate = samp_rate;
159        conf.mode = mode;
160    }
161}
162
163impl Default for SpectrometerConfig {
164    fn default() -> SpectrometerConfig {
165        SpectrometerConfig::new()
166    }
167}