tekhsi_rs 0.1.1

High-performance client for Tektronix TekHSI enabled oscilloscopes
Documentation
//! Tektronix TekHSI gRPC client with strict validation and waveform decoding.
//!
//! This crate connects to HSI-compatible Tektronix oscilloscopes, validates waveform headers, and decodes
//! analog/digital/IQ data into typed waveforms.
//!
//! - Tektronix TekHSI gRPC client for HSI-compatible oscilloscopes
//! - Strict waveform header validation that rejects invalid/unsupported data
//! - High-throughput acquisition with parallel download and decode loops
//! - Decodes analog (I8/I16/F32/F64), digital (I8/I16), and IQ waveforms
//! - FFT utilities with calibrated output for frequency analysis (requires `fft` feature)
//! - Streaming acquisitions via broadcast channels
//!
//! # Creating a Client
//!
//! Connect to a TekHSI server using [`TekHsiClient::connect`] with the scope's address.
//!
//! ```no_run
//! # use tekhsi_rs::TekHsiClient;
//! # async fn demo() -> Result<(), Box<dyn std::error::Error>> {
//! let client = TekHsiClient::connect("tek-scope-address:5000").await?;
//! # Ok(())
//! # }
//! ```
//!
//! - Establishes a gRPC channel to the specified address
//! - Performs a handshake with the scope's TekHSI service
//! - All Tektronix scopes tested allow a **single connection** at a time
//! - Attempting to create a second connection to the same scope returns [`errors::ConnectionError::ScopeBusy`] error
//!
//! # Listing Available Symbols
//!
//! Query the scope for available channels using [`TekHsiClient::list_available_symbols`].
//!
//! ```no_run
//! # use tekhsi_rs::TekHsiClient;
//! # async fn demo() -> Result<(), Box<dyn std::error::Error>> {
//! # let client = TekHsiClient::connect("tek-scope-address:5000").await?;
//! let symbols = client.list_available_symbols().await?;
//! println!("Available channels: {:?}", symbols);
//! # Ok(())
//! # }
//! ```
//!
//! - Returns symbols normalized to lowercase (e.g., "ch1", "ch2_iq", "ch3_dall")
//! - List of available symbols depends on which channels and modes are currently enabled on the scope
//!
//! # Subscribing
//!
//! After connecting, start streaming data with [`TekHsiClient::subscribe`].
//!
//! ```no_run
//! # use tekhsi_rs::{SubscribeOptions, TekHsiClient};
//! # async fn demo() -> Result<(), Box<dyn std::error::Error>> {
//! # let client = TekHsiClient::connect("tek-scope-address:5000").await?;
//! let symbols = client.list_available_symbols().await?;
//! let mut rx = client.subscribe(symbols, SubscribeOptions::default())?;
//! # Ok(())
//! # }
//! ```
//!
//! - Only **one subscription** can be active per client
//! - Acquisitions are delivered via an async broadcast channel
//! - Must provide at least one symbol (returns [`errors::SubscriptionUpdateError::EmptySymbols`] if empty)
//! - Attempting to start a second subscription returns [`errors::SubscriptionError::AlreadyActive`]
//!
//! # Updating Subscriptions
//!
//! Dynamically change the active symbols without reconnecting by calling [`TekHsiClient::update_symbols`].
//!
//! ```no_run
//! # use tekhsi_rs::{SubscribeOptions, TekHsiClient};
//! # async fn demo() -> Result<(), Box<dyn std::error::Error>> {
//! # let client = TekHsiClient::connect("tek-scope-address:5000").await?;
//! let symbols = vec!["ch1".to_string()];
//! let mut rx = client.subscribe(symbols, SubscribeOptions::default())?;
//! // ... receive ch1 acquisitions from `rx` ...
//! let new_symbols = vec!["ch2".to_string(), "ch3".to_string()];
//! client.update_symbols(new_symbols)?; // Update to different symbols
//! // ... receive ch2 and ch3 acquisitions from `rx` ...
//!
//! # Ok(())
//! # }
//! ```
//!
//! - Must provide at least one symbol (returns [`errors::SubscriptionUpdateError::EmptySymbols`] if empty)
//! - Requires an active subscription (returns [`errors::SubscriptionUpdateError::NotActive`] if no subscription exists)
//! - There might still be acquisitions in the channel from the previous subscription
//!
//! # Acquisitions
//!
//! Each [`data::Acquisition`] represents a single acquisition across all subscribed symbols.
//! It is recommended that you access a channel's waveform by symbol name using [`data::Acquisition::get_by_symbol`] for example:
//! ```no_run
//! # use tekhsi_rs::{SubscribeOptions, TekHsiClient};
//! # async fn demo() -> Result<(), Box<dyn std::error::Error>> {
//! # let client = TekHsiClient::connect("tek-scope-address:5000").await?;
//! let mut rx = client.subscribe(vec!["ch1".to_string()], SubscribeOptions::default())?;
//! while let Ok(acquisition) = rx.recv().await {
//!     let ch1_waveform = acquisition.get_by_symbol("ch1").unwrap();
//!     // ... use the ChannelData in ch1_waveform ...
//! }
//! # Ok(())
//! # }
//! ```
//!
//! [`data::ChannelData`] variants:
//! - [`data::ChannelData::Waveform`]: Successfully decoded waveform with acquisition ID, symbol, header, and waveform data
//! - [`data::ChannelData::DecodeError`]: Failed decoding with symbol, header, and error details
//! - [`data::ChannelData::AcquisitionError`]: Failed acquisition with symbol and error details
//!
//! [`data::Waveform`] variants:
//! - [`data::Waveform::Analog`]: Analog voltage samples (I8/I16/F32/F64) from oscilloscope analog channels
//! - [`data::Waveform::Digital`]: Digital bit samples (I8/I16) from digital inputs or logic probes
//! - [`data::Waveform::Iq`]: Complex IQ samples (I8/I16/I32) from spectrum or RF analysis
//!
//! Reading acquisition data:
//! - Each waveform type provides iterator-based `iter_normalized_*` helpers for calibrated values
//! - I highly recommend using these functions unless you have a very good reason not to
//! - There is a `as_scope_digital8()` helper for [`data::Waveform::Digital`] that returns an 8-channel logic probe bit mapping (tested with a TLP058)
//!
//! # Disconnecting
//!
//! Explicitly disconnect to stop acquisition and clean up resources:
//!
//! ```no_run
//! # use tekhsi_rs::TekHsiClient;
//! # async fn demo() -> Result<(), Box<dyn std::error::Error>> {
//! let client = TekHsiClient::connect("tek-scope-address:5000").await?;
//! // ... use client ...
//! client.disconnect().await?;
//! # Ok(())
//! # }
//! ```
//!
//! - Stops all active acquisition loops via cancellation tokens
//! - Waits 500ms for the scope to exit acquisition mode
//! - Sends a disconnect request to the scope
//!
//! The client also implements [`Drop`] for automatic cleanup
//!
//! # FFT Analysis
//!
//! The [`fft::FftWaveform`] trait is implemented on [`data::Waveform::Analog`] and [`data::Waveform::Iq`]
//! for convenience.
//!
//! Use `fft_real` or `fft_complex` to compute FFT bins and then
//! convert to dBm:
//!
//! ```no_run
//! # #[cfg(feature = "fft")]
//! # mod ensure_fft {
//! # use tracing::warn;
//! # use tekhsi_rs::{FftWaveform, SubscribeOptions, TekHsiClient, data::Waveform, data::ChannelData};
//! # use tekhsi_rs::fft::{ComplexFftWaveform, FftResult, FftWindow};
//! # async fn demo() -> Result<(), Box<dyn std::error::Error>> {
//! # let client = TekHsiClient::connect("tek-scope-address:5000").await?;
//! # let symbols = client.list_available_symbols().await?;
//! let mut rx = client.subscribe(symbols, SubscribeOptions::default())?;
//! while let Ok(acquisition) = rx.recv().await {
//!     for channel in acquisition.data.iter() {
//!         match channel {
//!             ChannelData::Waveform { waveform, .. } => match waveform {
//!                 Waveform::Analog(analog) => {
//!                     let mut fft = analog.fft_mag(FftWindow::Rectangular, None);
//!                     let _fft_dbm = fft.as_dbm_single_sided(Some(50.0));
//!                 }
//!                 Waveform::Iq(iq) => {
//!                     let mut fft = iq.fft_complex(FftWindow::Rectangular, None);
//!                     let _fft_dbm = fft.as_dbm(Some(50.0));
//!                 }
//!                 Waveform::Digital(_digital) => {}
//!             },
//!             _ => { warn!("Acquisition or decode failed!") }
//!         }
//!     }
//! }
//! # client.disconnect().await?;
//! # Ok(())
//! # }
//! # }
//! ```
//!
//! Notes:
//! - FFT functionality **requires enabling** the `fft` feature flag.
//! - If you are repeatedly computing FFTs the consider the `_cached` variants
//! - Uses the `rustfft` package for CPU-bound FFT computations (no GPU acceleration)
//! - Calibrated output requires optional impedance parameter (typically 50Ω)
//! - Provided as a convenience helper for common frequency analysis tasks

#![cfg_attr(coverage, feature(coverage_attribute))]

pub mod data;
pub mod errors;

#[cfg(feature = "fft")]
pub mod fft;

mod acquisition;
mod client;
mod decoder;
mod helpers;
mod tekscope;
mod validation;

pub use client::{SubscribeOptions, TekHsiClient};

#[cfg(feature = "fft")]
pub use fft::{ComplexFftWaveform, FftWaveform};
pub use smol_str::SmolStr;
pub use tekscope::WaveformHeader;

#[doc(hidden)]
pub use crate::decoder::decode_waveform_chunks;
#[doc(hidden)]
pub use crate::tekscope::connect_client::ConnectClient;
#[doc(hidden)]
pub use crate::tekscope::{
    ClientChannel, ConnectRequest, ConnectStatus, WfmType, new_connect_client,
};