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}