use std::io::{BufRead, BufReader};
use std::path::Path;
use matfile::{MatFile, NumericData};
use num_complex::Complex64;
use tekhsi_rs::TekHsiClient;
use tokio::time::timeout;
#[path = "common/config.rs"]
mod common_address;
#[path = "common/data.rs"]
mod common_csv;
use common_address::test_address;
use common_csv::csv_path_for_symbol;
use tekhsi_rs::SubscribeOptions;
use tekhsi_rs::data::{ChannelData, DigitalSamples, Waveform};
pub const ANALOG_TOLERANCE: f64 = 1e-15; const IQ_TOLERANCE: f64 = 1e-6;
fn read_csv_analog_values(path: &Path) -> Result<Vec<f64>, Box<dyn std::error::Error>> {
let file = std::fs::File::open(path)?;
let reader = BufReader::new(file);
let mut header: Option<Vec<String>> = None;
let mut value_idx = None;
let mut values = Vec::new();
for line in reader.lines() {
let line = line?;
if line.trim().is_empty() {
continue;
}
if header.is_none() {
if !line.trim_start().starts_with("TIME") {
continue;
}
let cols: Vec<String> = line.split(',').map(|col| col.trim().to_string()).collect();
let idx = cols
.iter()
.enumerate()
.find(|(_, name)| *name != "TIME" && !name.is_empty())
.map(|(idx, _)| idx)
.ok_or("missing analog data column")?;
header = Some(cols);
value_idx = Some(idx);
continue;
}
let cols: Vec<&str> = line.split(',').map(|col| col.trim()).collect();
let time = cols.get(0).and_then(|col| col.parse::<f64>().ok());
if time.is_none() {
continue;
}
let idx = value_idx.ok_or("missing analog column index")?;
if let Some(value) = cols.get(idx).and_then(|col| col.parse::<f64>().ok()) {
values.push(value);
}
}
Ok(values)
}
fn read_csv_digital_bytes(path: &Path) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let file = std::fs::File::open(path)?;
let reader = BufReader::new(file);
let mut bit_indices: Option<[usize; 8]> = None;
let mut values = Vec::new();
for line in reader.lines() {
let line = line?;
if line.trim().is_empty() {
continue;
}
if bit_indices.is_none() {
if !line.trim_start().starts_with("TIME") {
continue;
}
let cols: Vec<String> = line.split(',').map(|col| col.trim().to_string()).collect();
let mut indices = [None; 8];
for (idx, name) in cols.iter().enumerate() {
let name_upper = name.to_ascii_uppercase();
if name_upper.ends_with("_D7") {
indices[0] = Some(idx);
} else if name_upper.ends_with("_D6") {
indices[1] = Some(idx);
} else if name_upper.ends_with("_D5") {
indices[2] = Some(idx);
} else if name_upper.ends_with("_D4") {
indices[3] = Some(idx);
} else if name_upper.ends_with("_D3") {
indices[4] = Some(idx);
} else if name_upper.ends_with("_D2") {
indices[5] = Some(idx);
} else if name_upper.ends_with("_D1") {
indices[6] = Some(idx);
} else if name_upper.ends_with("_D0") {
indices[7] = Some(idx);
}
}
let mut resolved = [0usize; 8];
for (idx, entry) in indices.iter().enumerate() {
resolved[idx] = entry.ok_or("missing digital bit columns")?;
}
bit_indices = Some(resolved);
continue;
}
let cols: Vec<&str> = line.split(',').map(|col| col.trim()).collect();
let time = cols.get(0).and_then(|col| col.parse::<f64>().ok());
if time.is_none() {
continue;
}
let mut byte = 0u8;
let indices = bit_indices.ok_or("missing digital column indices")?;
for (bit_idx, col_idx) in indices.iter().enumerate() {
let bit = cols
.get(*col_idx)
.and_then(|col| col.parse::<u8>().ok())
.unwrap_or(0)
& 1;
byte |= bit << (7 - bit_idx);
}
values.push(byte);
}
Ok(values)
}
fn read_mat_iq_values(path: &Path) -> Result<Vec<Complex64>, Box<dyn std::error::Error>> {
let file = std::fs::File::open(path)?;
let mat = MatFile::parse(file)?;
let array = mat.find_by_name("IQ").ok_or("missing IQ array")?;
match array.data() {
NumericData::Double { real, imag } => {
let imag = imag.as_ref().ok_or("IQ imag data missing")?;
if real.len() != imag.len() {
return Err("IQ real/imag length mismatch".into());
}
Ok(real
.iter()
.zip(imag.iter())
.map(|(re, im)| Complex64::new(*re, *im))
.collect())
}
NumericData::Single { real, imag } => {
let imag = imag.as_ref().ok_or("IQ imag data missing")?;
if real.len() != imag.len() {
return Err("IQ real/imag length mismatch".into());
}
Ok(real
.iter()
.zip(imag.iter())
.map(|(re, im)| Complex64::new(f64::from(*re), f64::from(*im)))
.collect())
}
_ => Err("IQ array is not a floating-point complex array".into()),
}
}
async fn validate_symbol_waveform(symbol: &str) -> Result<(), Box<dyn std::error::Error>> {
let client = TekHsiClient::connect(&test_address()).await?;
let mut receiver = client.subscribe(vec![symbol.to_string()], SubscribeOptions::default())?;
let acquisition = match timeout(std::time::Duration::from_secs(5), receiver.recv()).await {
Ok(Ok(acquisition)) => acquisition,
Ok(Err(err)) => return Err(format!("subscribe failed: {err}").into()),
Err(_) => return Err("timed out waiting for acquisition".into()),
};
let channel_data = acquisition
.get_by_symbol(symbol)
.ok_or_else(|| format!("missing waveform for {symbol}"))?;
if symbol.ends_with("_iq") {
let mat_path = Path::new("tests/data").join(format!("{symbol}.mat"));
if !mat_path.exists() {
return Err(format!("missing mat for {symbol}").into());
}
let expected = read_mat_iq_values(&mat_path)?;
let actual: Vec<_> = match channel_data {
ChannelData::Waveform {
waveform: Waveform::Iq(iq),
..
} => iq.iter_normalized_values().collect(),
ChannelData::Waveform { .. } => {
return Err(format!("expected iq waveform for {symbol}").into());
}
ChannelData::DecodeError { .. } => {
return Err(format!("failed to decode waveform for {symbol}").into());
}
ChannelData::AcquisitionError { .. } => {
return Err(format!("failed to acquire waveform for {symbol}").into());
}
};
if actual.len() != expected.len() {
return Err(format!(
"iq mat length mismatch for {symbol}: mat={}, waveform={}",
expected.len(),
actual.len()
)
.into());
}
for idx in 0..expected.len() {
let expected_value = expected[idx];
let actual_value = actual[idx] / 2.0; let diff_re = (expected_value.re - actual_value.re).abs();
let diff_im = (expected_value.im - actual_value.im).abs();
if diff_re > IQ_TOLERANCE || diff_im > IQ_TOLERANCE {
return Err(format!(
"iq mat mismatch for {symbol} at {idx}: expected {:?}, got {:?}",
expected_value, actual_value
)
.into());
}
}
} else if symbol.ends_with("_dall") {
let csv_path =
csv_path_for_symbol(symbol).ok_or_else(|| format!("missing csv for {symbol}"))?;
let expected = read_csv_digital_bytes(&csv_path)?;
let actual = match channel_data {
ChannelData::Waveform {
waveform: Waveform::Digital(digital),
..
} => match &digital.y_axis_bytes {
DigitalSamples::I16(_) => digital
.y_axis_bytes
.as_scope_digital8()
.ok_or_else(|| format!("missing scope mapping for {symbol}"))?,
DigitalSamples::I8(values) => {
values.iter().map(|value| value.to_le_bytes()[0]).collect()
}
},
ChannelData::Waveform { .. } => {
return Err(format!("expected digital waveform for {symbol}").into());
}
ChannelData::DecodeError { .. } => {
return Err(format!("failed to decode waveform for {symbol}").into());
}
ChannelData::AcquisitionError { .. } => {
return Err(format!("failed to acquire waveform for {symbol}").into());
}
};
if actual.len() != expected.len() {
return Err(format!(
"digital csv length mismatch for {symbol}: csv={}, waveform={}",
expected.len(),
actual.len()
)
.into());
}
if actual != expected {
if let Some((idx, (left, right))) = actual
.iter()
.zip(expected.iter())
.enumerate()
.find(|(_, (left, right))| left != right)
{
return Err(format!(
"digital csv mismatch for {symbol} at {idx}: expected {right}, got {left}"
)
.into());
}
return Err(format!("digital csv mismatch for {symbol}").into());
}
} else {
let csv_path =
csv_path_for_symbol(symbol).ok_or_else(|| format!("missing csv for {symbol}"))?;
let expected = read_csv_analog_values(&csv_path)?;
let actual: Vec<_> = match channel_data {
ChannelData::Waveform {
waveform: Waveform::Analog(analog),
..
} => analog.iter_normalized_values().collect(),
ChannelData::Waveform { .. } => {
return Err(format!("expected analog waveform for {symbol}").into());
}
ChannelData::DecodeError { .. } => {
return Err(format!("failed to decode waveform for {symbol}").into());
}
ChannelData::AcquisitionError { .. } => {
return Err(format!("failed to acquire waveform for {symbol}").into());
}
};
if actual.len() != expected.len() {
return Err(format!(
"analog csv length mismatch for {symbol}: csv={}, waveform={}",
expected.len(),
actual.len()
)
.into());
}
for idx in 0..expected.len() {
let diff = (actual[idx] - expected[idx]).abs();
if diff > ANALOG_TOLERANCE {
return Err(format!(
"analog csv mismatch for {symbol} at {idx}: expected {}, got {}",
expected[idx], actual[idx]
)
.into());
}
}
}
client.disconnect().await?;
Ok(())
}
include!(concat!(env!("OUT_DIR"), "/generated_waveform_tests.rs"));