oscura 0.7.0__py3-none-any.whl → 0.10.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- oscura/__init__.py +19 -19
- oscura/analyzers/__init__.py +2 -0
- oscura/analyzers/digital/extraction.py +2 -3
- oscura/analyzers/digital/quality.py +1 -1
- oscura/analyzers/digital/timing.py +1 -1
- oscura/analyzers/eye/__init__.py +5 -1
- oscura/analyzers/eye/generation.py +501 -0
- oscura/analyzers/jitter/__init__.py +6 -6
- oscura/analyzers/jitter/timing.py +419 -0
- oscura/analyzers/patterns/__init__.py +94 -0
- oscura/analyzers/patterns/reverse_engineering.py +991 -0
- oscura/analyzers/power/__init__.py +35 -12
- oscura/analyzers/power/basic.py +3 -3
- oscura/analyzers/power/soa.py +1 -1
- oscura/analyzers/power/switching.py +3 -3
- oscura/analyzers/signal_classification.py +529 -0
- oscura/analyzers/signal_integrity/sparams.py +3 -3
- oscura/analyzers/statistics/__init__.py +4 -0
- oscura/analyzers/statistics/basic.py +152 -0
- oscura/analyzers/statistics/correlation.py +47 -6
- oscura/analyzers/validation.py +1 -1
- oscura/analyzers/waveform/__init__.py +2 -0
- oscura/analyzers/waveform/measurements.py +329 -163
- oscura/analyzers/waveform/measurements_with_uncertainty.py +91 -35
- oscura/analyzers/waveform/spectral.py +498 -54
- oscura/api/dsl/commands.py +15 -6
- oscura/api/server/templates/base.html +137 -146
- oscura/api/server/templates/export.html +84 -110
- oscura/api/server/templates/home.html +248 -267
- oscura/api/server/templates/protocols.html +44 -48
- oscura/api/server/templates/reports.html +27 -35
- oscura/api/server/templates/session_detail.html +68 -78
- oscura/api/server/templates/sessions.html +62 -72
- oscura/api/server/templates/waveforms.html +54 -64
- oscura/automotive/__init__.py +1 -1
- oscura/automotive/can/session.py +1 -1
- oscura/automotive/dbc/generator.py +638 -23
- oscura/automotive/dtc/data.json +102 -17
- oscura/automotive/uds/decoder.py +99 -6
- oscura/cli/analyze.py +8 -2
- oscura/cli/batch.py +36 -5
- oscura/cli/characterize.py +18 -4
- oscura/cli/export.py +47 -5
- oscura/cli/main.py +2 -0
- oscura/cli/onboarding/wizard.py +10 -6
- oscura/cli/pipeline.py +585 -0
- oscura/cli/visualize.py +6 -4
- oscura/convenience.py +400 -32
- oscura/core/config/loader.py +0 -1
- oscura/core/measurement_result.py +286 -0
- oscura/core/progress.py +1 -1
- oscura/core/schemas/device_mapping.json +8 -2
- oscura/core/schemas/packet_format.json +24 -4
- oscura/core/schemas/protocol_definition.json +12 -2
- oscura/core/types.py +300 -199
- oscura/correlation/multi_protocol.py +1 -1
- oscura/export/legacy/__init__.py +11 -0
- oscura/export/legacy/wav.py +75 -0
- oscura/exporters/__init__.py +19 -0
- oscura/exporters/wireshark.py +809 -0
- oscura/hardware/acquisition/file.py +5 -19
- oscura/hardware/acquisition/saleae.py +10 -10
- oscura/hardware/acquisition/socketcan.py +4 -6
- oscura/hardware/acquisition/synthetic.py +1 -5
- oscura/hardware/acquisition/visa.py +6 -6
- oscura/hardware/security/side_channel_detector.py +5 -508
- oscura/inference/message_format.py +686 -1
- oscura/jupyter/display.py +2 -2
- oscura/jupyter/magic.py +3 -3
- oscura/loaders/__init__.py +17 -12
- oscura/loaders/binary.py +1 -1
- oscura/loaders/chipwhisperer.py +1 -2
- oscura/loaders/configurable.py +1 -1
- oscura/loaders/csv_loader.py +2 -2
- oscura/loaders/hdf5_loader.py +1 -1
- oscura/loaders/lazy.py +6 -1
- oscura/loaders/mmap_loader.py +0 -1
- oscura/loaders/numpy_loader.py +8 -7
- oscura/loaders/preprocessing.py +3 -5
- oscura/loaders/rigol.py +21 -7
- oscura/loaders/sigrok.py +2 -5
- oscura/loaders/tdms.py +3 -2
- oscura/loaders/tektronix.py +38 -32
- oscura/loaders/tss.py +20 -27
- oscura/loaders/vcd.py +13 -8
- oscura/loaders/wav.py +1 -6
- oscura/pipeline/__init__.py +76 -0
- oscura/pipeline/handlers/__init__.py +165 -0
- oscura/pipeline/handlers/analyzers.py +1045 -0
- oscura/pipeline/handlers/decoders.py +899 -0
- oscura/pipeline/handlers/exporters.py +1103 -0
- oscura/pipeline/handlers/filters.py +891 -0
- oscura/pipeline/handlers/loaders.py +640 -0
- oscura/pipeline/handlers/transforms.py +768 -0
- oscura/reporting/__init__.py +88 -1
- oscura/reporting/automation.py +348 -0
- oscura/reporting/citations.py +374 -0
- oscura/reporting/core.py +54 -0
- oscura/reporting/formatting/__init__.py +11 -0
- oscura/reporting/formatting/measurements.py +320 -0
- oscura/reporting/html.py +57 -0
- oscura/reporting/interpretation.py +431 -0
- oscura/reporting/summary.py +329 -0
- oscura/reporting/templates/enhanced/protocol_re.html +504 -503
- oscura/reporting/visualization.py +542 -0
- oscura/side_channel/__init__.py +38 -57
- oscura/utils/builders/signal_builder.py +5 -5
- oscura/utils/comparison/compare.py +7 -9
- oscura/utils/comparison/golden.py +1 -1
- oscura/utils/filtering/convenience.py +2 -2
- oscura/utils/math/arithmetic.py +38 -62
- oscura/utils/math/interpolation.py +20 -20
- oscura/utils/pipeline/__init__.py +4 -17
- oscura/utils/progressive.py +1 -4
- oscura/utils/triggering/edge.py +1 -1
- oscura/utils/triggering/pattern.py +2 -2
- oscura/utils/triggering/pulse.py +2 -2
- oscura/utils/triggering/window.py +3 -3
- oscura/validation/hil_testing.py +11 -11
- oscura/visualization/__init__.py +47 -284
- oscura/visualization/batch.py +160 -0
- oscura/visualization/plot.py +542 -53
- oscura/visualization/styles.py +184 -318
- oscura/workflows/__init__.py +2 -0
- oscura/workflows/batch/advanced.py +1 -1
- oscura/workflows/batch/aggregate.py +7 -8
- oscura/workflows/complete_re.py +251 -23
- oscura/workflows/digital.py +27 -4
- oscura/workflows/multi_trace.py +136 -17
- oscura/workflows/waveform.py +788 -0
- {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/METADATA +59 -79
- {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/RECORD +135 -149
- oscura/side_channel/dpa.py +0 -1025
- oscura/utils/optimization/__init__.py +0 -19
- oscura/utils/optimization/parallel.py +0 -443
- oscura/utils/optimization/search.py +0 -532
- oscura/utils/pipeline/base.py +0 -338
- oscura/utils/pipeline/composition.py +0 -248
- oscura/utils/pipeline/parallel.py +0 -449
- oscura/utils/pipeline/pipeline.py +0 -375
- oscura/utils/search/__init__.py +0 -16
- oscura/utils/search/anomaly.py +0 -424
- oscura/utils/search/context.py +0 -294
- oscura/utils/search/pattern.py +0 -288
- oscura/utils/storage/__init__.py +0 -61
- oscura/utils/storage/database.py +0 -1166
- oscura/visualization/accessibility.py +0 -526
- oscura/visualization/annotations.py +0 -371
- oscura/visualization/axis_scaling.py +0 -305
- oscura/visualization/colors.py +0 -451
- oscura/visualization/digital.py +0 -436
- oscura/visualization/eye.py +0 -571
- oscura/visualization/histogram.py +0 -281
- oscura/visualization/interactive.py +0 -1035
- oscura/visualization/jitter.py +0 -1042
- oscura/visualization/keyboard.py +0 -394
- oscura/visualization/layout.py +0 -400
- oscura/visualization/optimization.py +0 -1079
- oscura/visualization/palettes.py +0 -446
- oscura/visualization/power.py +0 -508
- oscura/visualization/power_extended.py +0 -955
- oscura/visualization/presets.py +0 -469
- oscura/visualization/protocols.py +0 -1246
- oscura/visualization/render.py +0 -223
- oscura/visualization/rendering.py +0 -444
- oscura/visualization/reverse_engineering.py +0 -838
- oscura/visualization/signal_integrity.py +0 -989
- oscura/visualization/specialized.py +0 -643
- oscura/visualization/spectral.py +0 -1226
- oscura/visualization/thumbnails.py +0 -340
- oscura/visualization/time_axis.py +0 -351
- oscura/visualization/waveform.py +0 -454
- {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/WHEEL +0 -0
- {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/entry_points.txt +0 -0
- {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -20,6 +20,7 @@ from typing import TYPE_CHECKING, Any
|
|
|
20
20
|
|
|
21
21
|
import numpy as np
|
|
22
22
|
|
|
23
|
+
from oscura.core.measurement_result import make_measurement
|
|
23
24
|
from oscura.core.types import WaveformTrace
|
|
24
25
|
|
|
25
26
|
if TYPE_CHECKING:
|
|
@@ -171,6 +172,75 @@ def weighted_mean(
|
|
|
171
172
|
return float(np.average(data, weights=weights))
|
|
172
173
|
|
|
173
174
|
|
|
175
|
+
def weighted_std(
|
|
176
|
+
trace: WaveformTrace | NDArray[np.floating[Any]],
|
|
177
|
+
weights: NDArray[np.floating[Any]] | None = None,
|
|
178
|
+
*,
|
|
179
|
+
ddof: int = 0,
|
|
180
|
+
) -> float:
|
|
181
|
+
"""Compute weighted standard deviation.
|
|
182
|
+
|
|
183
|
+
Uses the reliability weights formula for weighted variance.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
trace: Input trace or numpy array.
|
|
187
|
+
weights: Weight array (same length as data). If None, equal weights (unweighted std).
|
|
188
|
+
ddof: Delta degrees of freedom for bias correction (default 0).
|
|
189
|
+
- ddof=0: Maximum likelihood estimate (biased)
|
|
190
|
+
- ddof=1: Sample standard deviation (unbiased for normal distribution)
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
Weighted standard deviation.
|
|
194
|
+
|
|
195
|
+
Raises:
|
|
196
|
+
ValueError: If weights and data have different lengths.
|
|
197
|
+
ValueError: If weights contain negative values.
|
|
198
|
+
|
|
199
|
+
Example:
|
|
200
|
+
>>> weights = np.linspace(0.5, 1.0, len(trace.data))
|
|
201
|
+
>>> wstd = weighted_std(trace, weights)
|
|
202
|
+
>>> print(f"Weighted std: {wstd:.6f}")
|
|
203
|
+
|
|
204
|
+
>>> # Sample standard deviation (Bessel's correction)
|
|
205
|
+
>>> wstd_unbiased = weighted_std(trace, weights, ddof=1)
|
|
206
|
+
|
|
207
|
+
References:
|
|
208
|
+
Wikipedia: Weighted arithmetic mean
|
|
209
|
+
https://en.wikipedia.org/wiki/Weighted_arithmetic_mean#Weighted_sample_variance
|
|
210
|
+
"""
|
|
211
|
+
data = trace.data if isinstance(trace, WaveformTrace) else trace
|
|
212
|
+
|
|
213
|
+
if weights is None:
|
|
214
|
+
return float(np.std(data, ddof=ddof))
|
|
215
|
+
|
|
216
|
+
if len(weights) != len(data):
|
|
217
|
+
raise ValueError(f"Weights and data must have same length: {len(weights)} != {len(data)}")
|
|
218
|
+
|
|
219
|
+
if np.any(weights < 0):
|
|
220
|
+
raise ValueError("Weights must be non-negative")
|
|
221
|
+
|
|
222
|
+
# Handle edge cases
|
|
223
|
+
if len(data) == 0:
|
|
224
|
+
return float("nan")
|
|
225
|
+
|
|
226
|
+
if len(data) == 1:
|
|
227
|
+
return 0.0
|
|
228
|
+
|
|
229
|
+
# Compute weighted mean
|
|
230
|
+
w_sum = np.sum(weights)
|
|
231
|
+
if w_sum <= 0:
|
|
232
|
+
return float("nan")
|
|
233
|
+
|
|
234
|
+
w_mean = np.sum(weights * data) / w_sum
|
|
235
|
+
|
|
236
|
+
# Compute weighted variance with bias correction
|
|
237
|
+
# Reliability weights formula: var = sum(w * (x - mean)^2) / (sum(w) - ddof)
|
|
238
|
+
weighted_sq_deviations = weights * (data - w_mean) ** 2
|
|
239
|
+
variance = np.sum(weighted_sq_deviations) / (w_sum - ddof) if w_sum > ddof else 0.0
|
|
240
|
+
|
|
241
|
+
return float(np.sqrt(max(0.0, variance)))
|
|
242
|
+
|
|
243
|
+
|
|
174
244
|
def running_stats(
|
|
175
245
|
trace: WaveformTrace | NDArray[np.floating[Any]],
|
|
176
246
|
window_size: int,
|
|
@@ -253,11 +323,93 @@ def summary_stats(
|
|
|
253
323
|
return basic
|
|
254
324
|
|
|
255
325
|
|
|
326
|
+
def measure(
|
|
327
|
+
trace: WaveformTrace | NDArray[np.floating[Any]],
|
|
328
|
+
*,
|
|
329
|
+
parameters: list[str] | None = None,
|
|
330
|
+
include_units: bool = True,
|
|
331
|
+
) -> dict[str, Any]:
|
|
332
|
+
"""Compute statistical measurements with consistent format.
|
|
333
|
+
|
|
334
|
+
Unified function matching the API pattern of waveform.measure() and spectral.measure().
|
|
335
|
+
Returns MeasurementResult format with applicability tracking and formatting.
|
|
336
|
+
|
|
337
|
+
Args:
|
|
338
|
+
trace: Input trace or numpy array.
|
|
339
|
+
parameters: List of measurement names to compute. If None, compute all.
|
|
340
|
+
Valid names: mean, variance, std, min, max, range, count, p1, p5, p25, p50, p75, p95, p99
|
|
341
|
+
include_units: If True, return MeasurementResult format. If False, return flat values.
|
|
342
|
+
|
|
343
|
+
Returns:
|
|
344
|
+
Dictionary mapping measurement names to MeasurementResults (if include_units=True)
|
|
345
|
+
or raw values (if include_units=False).
|
|
346
|
+
|
|
347
|
+
Example:
|
|
348
|
+
>>> from oscura.analyzers.statistics import measure
|
|
349
|
+
>>> results = measure(trace)
|
|
350
|
+
>>> if results['mean']['applicable']:
|
|
351
|
+
... print(f"Mean: {results['mean']['display']}")
|
|
352
|
+
|
|
353
|
+
>>> # Get specific measurements only
|
|
354
|
+
>>> results = measure(trace, parameters=["mean", "std"])
|
|
355
|
+
|
|
356
|
+
>>> # Get flat values (legacy compatibility)
|
|
357
|
+
>>> results = measure(trace, include_units=False)
|
|
358
|
+
>>> mean_value = results["mean"] # Just the float
|
|
359
|
+
"""
|
|
360
|
+
data = trace.data if isinstance(trace, WaveformTrace) else trace
|
|
361
|
+
|
|
362
|
+
# Define unit mappings for statistical measurements
|
|
363
|
+
# For generic signals we use voltage units, but this could be parameterized
|
|
364
|
+
unit_map = {
|
|
365
|
+
"mean": "V",
|
|
366
|
+
"variance": "V²",
|
|
367
|
+
"std": "V",
|
|
368
|
+
"min": "V",
|
|
369
|
+
"max": "V",
|
|
370
|
+
"range": "dimensionless",
|
|
371
|
+
"count": "samples",
|
|
372
|
+
"p1": "dimensionless",
|
|
373
|
+
"p5": "dimensionless",
|
|
374
|
+
"p25": "dimensionless",
|
|
375
|
+
"p50": "dimensionless",
|
|
376
|
+
"p75": "dimensionless",
|
|
377
|
+
"p95": "dimensionless",
|
|
378
|
+
"p99": "dimensionless",
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
# Get basic stats
|
|
382
|
+
basic = basic_stats(trace)
|
|
383
|
+
|
|
384
|
+
# Get percentiles
|
|
385
|
+
percentile_values = percentiles(data, [1, 5, 25, 50, 75, 95, 99])
|
|
386
|
+
|
|
387
|
+
# Combine into single dict
|
|
388
|
+
all_measurements = {**basic, **percentile_values}
|
|
389
|
+
|
|
390
|
+
# Select requested measurements or all
|
|
391
|
+
if parameters is not None:
|
|
392
|
+
all_measurements = {k: v for k, v in all_measurements.items() if k in parameters}
|
|
393
|
+
|
|
394
|
+
# Format results
|
|
395
|
+
if include_units:
|
|
396
|
+
results = {}
|
|
397
|
+
for name, value in all_measurements.items():
|
|
398
|
+
unit = unit_map.get(name, "")
|
|
399
|
+
# All statistical measurements are always applicable (never NaN from valid data)
|
|
400
|
+
results[name] = make_measurement(value, unit)
|
|
401
|
+
return results
|
|
402
|
+
else:
|
|
403
|
+
return all_measurements
|
|
404
|
+
|
|
405
|
+
|
|
256
406
|
__all__ = [
|
|
257
407
|
"basic_stats",
|
|
408
|
+
"measure",
|
|
258
409
|
"percentiles",
|
|
259
410
|
"quartiles",
|
|
260
411
|
"running_stats",
|
|
261
412
|
"summary_stats",
|
|
262
413
|
"weighted_mean",
|
|
414
|
+
"weighted_std",
|
|
263
415
|
]
|
|
@@ -338,24 +338,48 @@ def cross_correlation(
|
|
|
338
338
|
def correlation_coefficient(
|
|
339
339
|
trace1: WaveformTrace | NDArray[np.floating[Any]],
|
|
340
340
|
trace2: WaveformTrace | NDArray[np.floating[Any]],
|
|
341
|
+
*,
|
|
342
|
+
method: Literal["pearson", "spearman", "kendall"] = "pearson",
|
|
341
343
|
) -> float:
|
|
342
|
-
"""Compute
|
|
344
|
+
"""Compute correlation coefficient between two signals.
|
|
343
345
|
|
|
344
|
-
|
|
346
|
+
Supports Pearson (linear), Spearman (monotonic), and Kendall (rank) correlations.
|
|
345
347
|
|
|
346
348
|
Args:
|
|
347
349
|
trace1: First input trace or numpy array.
|
|
348
350
|
trace2: Second input trace or numpy array.
|
|
351
|
+
method: Correlation method to use:
|
|
352
|
+
- "pearson": Linear correlation (default, parametric)
|
|
353
|
+
- "spearman": Monotonic correlation (non-parametric, robust to outliers)
|
|
354
|
+
- "kendall": Rank correlation (non-parametric, tau-b coefficient)
|
|
349
355
|
|
|
350
356
|
Returns:
|
|
351
357
|
Correlation coefficient in range [-1, 1].
|
|
352
358
|
|
|
359
|
+
Raises:
|
|
360
|
+
ValueError: If method is not one of the supported types.
|
|
361
|
+
|
|
353
362
|
Example:
|
|
363
|
+
>>> # Linear correlation (default)
|
|
354
364
|
>>> r = correlation_coefficient(trace1, trace2)
|
|
355
|
-
>>> print(f"
|
|
365
|
+
>>> print(f"Pearson correlation: {r:.3f}")
|
|
366
|
+
|
|
367
|
+
>>> # Monotonic correlation (robust to outliers)
|
|
368
|
+
>>> rho = correlation_coefficient(trace1, trace2, method="spearman")
|
|
369
|
+
>>> print(f"Spearman correlation: {rho:.3f}")
|
|
370
|
+
|
|
371
|
+
>>> # Rank correlation (best for ordinal data)
|
|
372
|
+
>>> tau = correlation_coefficient(trace1, trace2, method="kendall")
|
|
373
|
+
>>> print(f"Kendall correlation: {tau:.3f}")
|
|
374
|
+
|
|
375
|
+
References:
|
|
376
|
+
Pearson, K. (1895). Correlation coefficient
|
|
377
|
+
Spearman, C. (1904). Rank correlation
|
|
378
|
+
Kendall, M. G. (1938). Tau rank correlation
|
|
356
379
|
"""
|
|
357
|
-
|
|
380
|
+
from scipy import stats as sp_stats
|
|
358
381
|
|
|
382
|
+
data1 = trace1.data if isinstance(trace1, WaveformTrace) else trace1
|
|
359
383
|
data2 = trace2.data if isinstance(trace2, WaveformTrace) else trace2
|
|
360
384
|
|
|
361
385
|
# Ensure same length
|
|
@@ -363,8 +387,25 @@ def correlation_coefficient(
|
|
|
363
387
|
data1 = data1[:n]
|
|
364
388
|
data2 = data2[:n]
|
|
365
389
|
|
|
366
|
-
# Compute correlation
|
|
367
|
-
|
|
390
|
+
# Compute correlation based on method
|
|
391
|
+
if method == "pearson":
|
|
392
|
+
# Pearson linear correlation (parametric)
|
|
393
|
+
return float(np.corrcoef(data1, data2)[0, 1])
|
|
394
|
+
|
|
395
|
+
elif method == "spearman":
|
|
396
|
+
# Spearman rank correlation (non-parametric, monotonic)
|
|
397
|
+
corr, _p_value = sp_stats.spearmanr(data1, data2)
|
|
398
|
+
return float(corr)
|
|
399
|
+
|
|
400
|
+
elif method == "kendall":
|
|
401
|
+
# Kendall tau-b rank correlation (non-parametric)
|
|
402
|
+
corr, _p_value = sp_stats.kendalltau(data1, data2)
|
|
403
|
+
return float(corr)
|
|
404
|
+
|
|
405
|
+
else:
|
|
406
|
+
raise ValueError(
|
|
407
|
+
f"Unknown correlation method: {method}. Available: 'pearson', 'spearman', 'kendall'"
|
|
408
|
+
)
|
|
368
409
|
|
|
369
410
|
|
|
370
411
|
def _extract_periodicity_data(
|
oscura/analyzers/validation.py
CHANGED
|
@@ -64,7 +64,7 @@ def is_suitable_for_frequency_measurement(trace: WaveformTrace) -> tuple[bool, s
|
|
|
64
64
|
|
|
65
65
|
# Check period consistency (is it periodic?)
|
|
66
66
|
if len(rising_edges) >= 3:
|
|
67
|
-
edge_times = rising_edges
|
|
67
|
+
edge_times = rising_edges / trace.metadata.sample_rate
|
|
68
68
|
periods = np.diff(edge_times)
|
|
69
69
|
period_cv = np.std(periods) / np.mean(periods) if np.mean(periods) > 0 else float("inf")
|
|
70
70
|
|
|
@@ -4,6 +4,7 @@ Provides timing and amplitude measurements for analog waveforms.
|
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
from oscura.analyzers.waveform.measurements import (
|
|
7
|
+
MEASUREMENT_METADATA,
|
|
7
8
|
amplitude,
|
|
8
9
|
duty_cycle,
|
|
9
10
|
fall_time,
|
|
@@ -20,6 +21,7 @@ from oscura.analyzers.waveform.measurements import (
|
|
|
20
21
|
)
|
|
21
22
|
|
|
22
23
|
__all__ = [
|
|
24
|
+
"MEASUREMENT_METADATA",
|
|
23
25
|
"amplitude",
|
|
24
26
|
"duty_cycle",
|
|
25
27
|
"fall_time",
|