1use serde::{Deserialize, Serialize};
4
5use crate::adapter::AdapterKind;
6use crate::ids::{FrameId, SessionId, SourceId};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
10pub enum ValidationStatus {
11 Pending,
14 Accepted,
16 Degraded,
18 Rejected,
20 Recovered,
22}
23
24impl ValidationStatus {
25 #[inline]
27 pub fn is_exposable(self) -> bool {
28 matches!(
29 self,
30 ValidationStatus::Accepted | ValidationStatus::Degraded | ValidationStatus::Recovered
31 )
32 }
33}
34
35#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
43pub struct CsiFrame {
44 pub frame_id: FrameId,
46 pub session_id: SessionId,
48 pub source_id: SourceId,
50 pub adapter_kind: AdapterKind,
52 pub timestamp_ns: u64,
54 pub channel: u16,
56 pub bandwidth_mhz: u16,
58 pub rssi_dbm: Option<i16>,
60 pub noise_floor_dbm: Option<i16>,
62 pub antenna_index: Option<u8>,
64 pub tx_chain: Option<u8>,
66 pub rx_chain: Option<u8>,
68 pub subcarrier_count: u16,
70 pub i_values: Vec<f32>,
72 pub q_values: Vec<f32>,
74 pub amplitude: Vec<f32>,
76 pub phase: Vec<f32>,
78 pub validation: ValidationStatus,
80 pub quality_score: f32,
82 #[serde(default, skip_serializing_if = "Vec::is_empty")]
84 pub quality_reasons: Vec<String>,
85 pub calibration_version: Option<String>,
87}
88
89impl CsiFrame {
90 #[allow(clippy::too_many_arguments)]
96 pub fn from_iq(
97 frame_id: FrameId,
98 session_id: SessionId,
99 source_id: SourceId,
100 adapter_kind: AdapterKind,
101 timestamp_ns: u64,
102 channel: u16,
103 bandwidth_mhz: u16,
104 i_values: Vec<f32>,
105 q_values: Vec<f32>,
106 ) -> Self {
107 let n = i_values.len();
108 let mut amplitude = Vec::with_capacity(n);
109 let mut phase = Vec::with_capacity(n);
110 for (i, q) in i_values.iter().zip(q_values.iter()) {
111 amplitude.push((i * i + q * q).sqrt());
112 phase.push(q.atan2(*i));
113 }
114 CsiFrame {
115 frame_id,
116 session_id,
117 source_id,
118 adapter_kind,
119 timestamp_ns,
120 channel,
121 bandwidth_mhz,
122 rssi_dbm: None,
123 noise_floor_dbm: None,
124 antenna_index: None,
125 tx_chain: None,
126 rx_chain: None,
127 subcarrier_count: n as u16,
128 i_values,
129 q_values,
130 amplitude,
131 phase,
132 validation: ValidationStatus::Pending,
133 quality_score: 0.0,
134 quality_reasons: Vec::new(),
135 calibration_version: None,
136 }
137 }
138
139 pub fn with_rssi(mut self, rssi_dbm: i16) -> Self {
141 self.rssi_dbm = Some(rssi_dbm);
142 self
143 }
144
145 pub fn with_noise_floor(mut self, noise_floor_dbm: i16) -> Self {
147 self.noise_floor_dbm = Some(noise_floor_dbm);
148 self
149 }
150
151 pub fn with_chains(mut self, antenna: Option<u8>, tx: Option<u8>, rx: Option<u8>) -> Self {
153 self.antenna_index = antenna;
154 self.tx_chain = tx;
155 self.rx_chain = rx;
156 self
157 }
158
159 pub fn mean_amplitude(&self) -> f32 {
161 if self.amplitude.is_empty() {
162 0.0
163 } else {
164 self.amplitude.iter().sum::<f32>() / self.amplitude.len() as f32
165 }
166 }
167
168 pub fn is_exposable(&self) -> bool {
170 self.validation.is_exposable()
171 }
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177
178 fn sample() -> CsiFrame {
179 CsiFrame::from_iq(
180 FrameId(0),
181 SessionId(0),
182 SourceId::from("test"),
183 AdapterKind::File,
184 1_000,
185 6,
186 20,
187 vec![3.0, 0.0, -1.0],
188 vec![4.0, 2.0, 0.0],
189 )
190 }
191
192 #[test]
193 fn derives_amplitude_and_phase() {
194 let f = sample();
195 assert_eq!(f.subcarrier_count, 3);
196 assert!((f.amplitude[0] - 5.0).abs() < 1e-6); assert!((f.amplitude[1] - 2.0).abs() < 1e-6);
198 assert!((f.phase[0] - (4.0f32).atan2(3.0)).abs() < 1e-6);
199 assert_eq!(f.validation, ValidationStatus::Pending);
200 assert_eq!(f.quality_score, 0.0);
201 }
202
203 #[test]
204 fn builder_setters_and_mean() {
205 let f = sample().with_rssi(-55).with_noise_floor(-92).with_chains(Some(0), None, Some(1));
206 assert_eq!(f.rssi_dbm, Some(-55));
207 assert_eq!(f.noise_floor_dbm, Some(-92));
208 assert_eq!(f.antenna_index, Some(0));
209 assert_eq!(f.rx_chain, Some(1));
210 assert!((f.mean_amplitude() - (5.0 + 2.0 + 1.0) / 3.0).abs() < 1e-6);
211 }
212
213 #[test]
214 fn exposability_rules() {
215 assert!(!ValidationStatus::Pending.is_exposable());
216 assert!(!ValidationStatus::Rejected.is_exposable());
217 assert!(ValidationStatus::Accepted.is_exposable());
218 assert!(ValidationStatus::Degraded.is_exposable());
219 assert!(ValidationStatus::Recovered.is_exposable());
220 }
221
222 #[test]
223 fn frame_json_roundtrips() {
224 let f = sample().with_rssi(-60);
225 let json = serde_json::to_string(&f).unwrap();
226 let back: CsiFrame = serde_json::from_str(&json).unwrap();
227 assert_eq!(f, back);
228 }
229}