Hardware tests produce waveforms, not just single numbers. A power supply ripple test captures thousands of voltage samples over time. A vibration test records acceleration spectra across frequency bands. Storing these natively lets you trend and compare waveforms across your production.
Single Values vs. Time-Series
Most test systems store one number per measurement: "output voltage = 3.31V." But the full story is in the waveform. That 3.31V might be a clean DC signal or a noisy mess that happens to average out to 3.31V.
| Data type | Example | What it reveals |
|---|---|---|
| Single value | Vout = 3.31V | Average output level |
| Time series | 1000 samples over 10ms | Ripple, noise, transient behavior |
| Frequency spectrum | FFT of output voltage | Switching noise frequency content |
| Multi-axis | X/Y/Z acceleration | Vibration in all directions |
Dimensioned measurements handle all of these.
Capturing Time-Series Data
With OpenHTF
Use dimensioned measurements to store time-series data.
ripple_test_openhtf.py35 lines
import openhtf as htffrom tofupilot.openhtf import upload@htf.measures( htf.Measurement("output_ripple_mv") .with_dimensions("time_us") .with_units("mV"), htf.Measurement("ripple_pk_pk_mv") .in_range(0, 50) .with_units("mV"),)def ripple_test(test): # Capture 1000 samples at 100kHz (10us spacing) waveform = capture_oscilloscope(channel=1, samples=1000, rate_hz=100000) for i, sample in enumerate(waveform): test.measurements.output_ripple_mv[i * 10] = sample # time in microseconds # Also store the scalar summary test.measurements.ripple_pk_pk_mv = max(waveform) - min(waveform)def main(): test = htf.Test( ripple_test, procedure_id="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", # procedure UUID from the dashboard part_number="PSU-100", ) test.add_output_callbacks(upload()) test.execute(lambda: "PSU-2025-0099")if __name__ == "__main__": main()Install it with pip install "tofupilot[openhtf]". The upload() callback reads TOFUPILOT_API_KEY from the environment.
Note that the pass/fail limit is on the scalar (ripple_pk_pk_mv), not on the waveform. That split is deliberate and worth keeping: the scalar decides the verdict and drives trending, while the waveform is what you open when the scalar tells you something is wrong.
With the Python SDK
If you are not running OpenHTF, create the run directly:
ripple_test_client.py36 lines
import osfrom datetime import datetime, timedelta, timezoneimport numpy as npfrom tofupilot.v2 import TofuPilotstarted = datetime.now(timezone.utc) - timedelta(seconds=5)ended = datetime.now(timezone.utc)waveform = capture_oscilloscope(channel=1, samples=1000, rate_hz=100000)pk_pk = float(np.max(waveform) - np.min(waveform))outcome = "PASS" if pk_pk < 50 else "FAIL"with TofuPilot(api_key=os.getenv("TOFUPILOT_API_KEY")) as client: client.runs.create( procedure_id="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", serial_number="PSU-2025-0099", part_number="PSU-100", outcome=outcome, started_at=started, ended_at=ended, phases=[{ "name": "Output Ripple", "outcome": outcome, "started_at": started, "ended_at": ended, "measurements": [{ "name": "Ripple Peak to Peak", "measured_value": pk_pk, "units": "mV", "validators": [ {"operator": "<=", "expected_value": 50}, ], }], }], )This uploads the extracted scalar rather than all 1000 samples. If you need the raw capture kept alongside the result, attach it as a file rather than expanding it into measurements.
Types of Time-Series Test Data
Voltage/Current Waveforms
Captured from oscilloscopes or DAQs during power-on sequences, ripple tests, or transient response tests. Store the raw waveform plus scalar summaries (peak-to-peak, RMS, rise time).
Temperature Profiles
Recorded during thermal cycling, burn-in, or heat dissipation tests. Multiple sensors produce multi-channel time-series data over minutes or hours.
# Thermal test with 4 temperature sensors sampled every second for 30 minutessensors = ["junction", "ambient", "heatsink", "case"]duration_s = 1800samples_per_sensor = duration_s # 1 sample/secondfor sensor_name in sensors: readings = read_thermal_sensor(sensor_name, samples=samples_per_sensor) measurements.append({ "name": f"temp_{sensor_name}", "measured_value": max(readings), # peak temperature is the gating value "units": "degC", "validators": [ {"operator": "<=", "expected_value": 85}, ], })Four sensors at 1800 samples each is 7200 points per unit. At production volume, store the features that gate the result (peak, time above threshold, final steady-state) and keep the full profile as an attachment.
Vibration Spectra
FFT data from accelerometers during vibration testing. Frequency on one axis, amplitude on the other.
Pressure/Flow Curves
Time-series pressure and flow measurements during leak tests, pneumatic tests, or hydraulic validation.
Analyzing Time-Series Across Production
The power of storing waveforms rather than only scalars is comparison across units.
Waveform Overlay
Compare ripple waveforms from 100 units. If 99 look the same and one has an extra spike, that unit has a problem the peak-to-peak measurement alone might not catch.
Statistical Bounds
Calculate the mean and standard deviation of your waveform at each time point across all units. This gives you an envelope of normal behavior. Any unit whose waveform falls outside the envelope is flagged.
import numpy as np# All waveforms from production (each is a list of 1000 samples)all_waveforms = np.array(waveforms_from_production) # shape: (N_units, 1000)mean_waveform = np.mean(all_waveforms, axis=0)std_waveform = np.std(all_waveforms, axis=0)upper_bound = mean_waveform + 3 * std_waveformlower_bound = mean_waveform - 3 * std_waveform# Check if a new unit's waveform is within boundsnew_waveform = np.array(new_unit_data)is_anomalous = np.any(new_waveform > upper_bound) or np.any(new_waveform < lower_bound)Two cautions before using this as a gate. With 1000 sample points and a 3-sigma band, roughly 3 points per unit will fall outside by chance alone, so np.any will flag a large share of good units. Require a run of consecutive out-of-band points, or compare against a tolerance band rather than a per-point sigma. And the envelope must be built from a period when the process was stable, otherwise you encode a drift as normal.
Trend Analysis on Waveform Features
Extract features from each waveform (rise time, settling time, overshoot, RMS) and trend them over production. A gradual increase in rise time across units suggests a component or process drift.
This is usually the highest-value analysis of the three, and the cheapest to store: a handful of numbers per unit that trend and alert like any other measurement.
Best Practices
| Practice | Why |
|---|---|
| Store both raw waveform and scalar summaries | Waveforms for deep analysis, scalars for dashboards and trending |
| Use consistent sample rates | Comparing waveforms requires the same number of points and timing |
| Include time axis metadata | Sample rate or time stamps so the waveform can be reconstructed |
| Set limits on scalar summaries | Scalars drive pass/fail, waveforms drive root cause analysis |
| Limit waveform size | Keep arrays under 10,000 points per measurement for practical storage |
| Capture raw traces on failures or a sample | Every unit at full rate outgrows its usefulness quickly |
