oscura 0.3.0__py3-none-any.whl → 0.5.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 +1 -7
- oscura/acquisition/__init__.py +147 -0
- oscura/acquisition/file.py +255 -0
- oscura/acquisition/hardware.py +186 -0
- oscura/acquisition/saleae.py +340 -0
- oscura/acquisition/socketcan.py +315 -0
- oscura/acquisition/streaming.py +38 -0
- oscura/acquisition/synthetic.py +229 -0
- oscura/acquisition/visa.py +376 -0
- oscura/analyzers/__init__.py +3 -0
- oscura/analyzers/digital/__init__.py +48 -0
- oscura/analyzers/digital/clock.py +9 -1
- oscura/analyzers/digital/edges.py +1 -1
- oscura/analyzers/digital/extraction.py +195 -0
- oscura/analyzers/digital/ic_database.py +498 -0
- oscura/analyzers/digital/timing.py +41 -11
- oscura/analyzers/digital/timing_paths.py +339 -0
- oscura/analyzers/digital/vintage.py +377 -0
- oscura/analyzers/digital/vintage_result.py +148 -0
- oscura/analyzers/protocols/__init__.py +22 -1
- oscura/analyzers/protocols/parallel_bus.py +449 -0
- oscura/analyzers/side_channel/__init__.py +52 -0
- oscura/analyzers/side_channel/power.py +690 -0
- oscura/analyzers/side_channel/timing.py +369 -0
- oscura/analyzers/signal_integrity/sparams.py +1 -1
- oscura/automotive/__init__.py +4 -2
- oscura/automotive/can/patterns.py +3 -1
- oscura/automotive/can/session.py +277 -78
- oscura/automotive/can/state_machine.py +5 -2
- oscura/builders/__init__.py +9 -11
- oscura/builders/signal_builder.py +99 -191
- oscura/core/exceptions.py +5 -1
- oscura/export/__init__.py +12 -0
- oscura/export/wavedrom.py +430 -0
- oscura/exporters/json_export.py +47 -0
- oscura/exporters/vintage_logic_csv.py +247 -0
- oscura/loaders/__init__.py +1 -0
- oscura/loaders/chipwhisperer.py +393 -0
- oscura/loaders/touchstone.py +1 -1
- oscura/reporting/__init__.py +7 -0
- oscura/reporting/vintage_logic_report.py +523 -0
- oscura/session/session.py +54 -46
- oscura/sessions/__init__.py +70 -0
- oscura/sessions/base.py +323 -0
- oscura/sessions/blackbox.py +640 -0
- oscura/sessions/generic.py +189 -0
- oscura/utils/autodetect.py +5 -1
- oscura/visualization/digital_advanced.py +718 -0
- oscura/visualization/figure_manager.py +156 -0
- {oscura-0.3.0.dist-info → oscura-0.5.0.dist-info}/METADATA +86 -5
- {oscura-0.3.0.dist-info → oscura-0.5.0.dist-info}/RECORD +54 -33
- oscura/automotive/dtc/data.json +0 -2763
- oscura/schemas/bus_configuration.json +0 -322
- oscura/schemas/device_mapping.json +0 -182
- oscura/schemas/packet_format.json +0 -418
- oscura/schemas/protocol_definition.json +0 -363
- {oscura-0.3.0.dist-info → oscura-0.5.0.dist-info}/WHEEL +0 -0
- {oscura-0.3.0.dist-info → oscura-0.5.0.dist-info}/entry_points.txt +0 -0
- {oscura-0.3.0.dist-info → oscura-0.5.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -52,9 +52,17 @@ from oscura.analyzers.digital.edges import (
|
|
|
52
52
|
from oscura.analyzers.digital.extraction import (
|
|
53
53
|
LOGIC_FAMILIES,
|
|
54
54
|
detect_edges,
|
|
55
|
+
detect_logic_family,
|
|
56
|
+
detect_open_collector,
|
|
55
57
|
get_logic_threshold,
|
|
56
58
|
to_digital,
|
|
57
59
|
)
|
|
60
|
+
from oscura.analyzers.digital.ic_database import (
|
|
61
|
+
IC_DATABASE,
|
|
62
|
+
ICTiming,
|
|
63
|
+
identify_ic,
|
|
64
|
+
validate_ic_timing,
|
|
65
|
+
)
|
|
58
66
|
from oscura.analyzers.digital.quality import (
|
|
59
67
|
Glitch,
|
|
60
68
|
NoiseMarginResult,
|
|
@@ -97,13 +105,37 @@ from oscura.analyzers.digital.timing import (
|
|
|
97
105
|
skew,
|
|
98
106
|
slew_rate,
|
|
99
107
|
)
|
|
108
|
+
from oscura.analyzers.digital.timing_paths import (
|
|
109
|
+
ICStage,
|
|
110
|
+
SetupHoldAnalysis,
|
|
111
|
+
TimingPathResult,
|
|
112
|
+
analyze_setup_hold,
|
|
113
|
+
analyze_timing_path,
|
|
114
|
+
calculate_timing_budget,
|
|
115
|
+
find_critical_paths,
|
|
116
|
+
)
|
|
117
|
+
from oscura.analyzers.digital.vintage import (
|
|
118
|
+
REPLACEMENT_DATABASE,
|
|
119
|
+
analyze_vintage_logic,
|
|
120
|
+
)
|
|
121
|
+
from oscura.analyzers.digital.vintage_result import (
|
|
122
|
+
BOMEntry,
|
|
123
|
+
ICIdentificationResult,
|
|
124
|
+
ModernReplacementIC,
|
|
125
|
+
VintageLogicAnalysisResult,
|
|
126
|
+
)
|
|
100
127
|
|
|
101
128
|
__all__ = [
|
|
129
|
+
# IC Database
|
|
130
|
+
"IC_DATABASE",
|
|
102
131
|
# Extraction
|
|
103
132
|
"LOGIC_FAMILIES",
|
|
133
|
+
"REPLACEMENT_DATABASE",
|
|
104
134
|
"AdaptiveThresholdResult",
|
|
105
135
|
# Adaptive Thresholds (RE-THR-001)
|
|
106
136
|
"AdaptiveThresholder",
|
|
137
|
+
# Vintage Logic Analysis
|
|
138
|
+
"BOMEntry",
|
|
107
139
|
# Clock Recovery (DSP-002)
|
|
108
140
|
"BaudRateResult",
|
|
109
141
|
# Bus Decoding (DSP-003)
|
|
@@ -125,6 +157,10 @@ __all__ = [
|
|
|
125
157
|
"EdgeTimingViolation",
|
|
126
158
|
# Quality
|
|
127
159
|
"Glitch",
|
|
160
|
+
"ICIdentificationResult",
|
|
161
|
+
"ICStage",
|
|
162
|
+
"ICTiming",
|
|
163
|
+
"ModernReplacementIC",
|
|
128
164
|
# Multi-Level Logic (RE-THR-002)
|
|
129
165
|
"MultiLevelDetector",
|
|
130
166
|
"MultiLevelResult",
|
|
@@ -132,18 +168,25 @@ __all__ = [
|
|
|
132
168
|
# Signal Quality (DSP-005)
|
|
133
169
|
"NoiseMargins",
|
|
134
170
|
"ParallelBusConfig",
|
|
171
|
+
"SetupHoldAnalysis",
|
|
135
172
|
"SignalIntegrityReport",
|
|
136
173
|
"SignalQualityAnalyzer",
|
|
137
174
|
"SimpleQualityMetrics",
|
|
138
175
|
"ThresholdConfig",
|
|
139
176
|
"TimingConstraint",
|
|
177
|
+
"TimingPathResult",
|
|
140
178
|
"TimingViolation",
|
|
141
179
|
"TransitionMetrics",
|
|
180
|
+
"VintageLogicAnalysisResult",
|
|
142
181
|
"Violation",
|
|
143
182
|
"align_by_trigger",
|
|
183
|
+
"analyze_setup_hold",
|
|
144
184
|
"analyze_signal_integrity",
|
|
185
|
+
"analyze_timing_path",
|
|
186
|
+
"analyze_vintage_logic",
|
|
145
187
|
"apply_adaptive_threshold",
|
|
146
188
|
"calculate_threshold_snr",
|
|
189
|
+
"calculate_timing_budget",
|
|
147
190
|
"check_timing_constraints",
|
|
148
191
|
"classify_edge_quality",
|
|
149
192
|
"correlate_channels",
|
|
@@ -153,10 +196,14 @@ __all__ = [
|
|
|
153
196
|
"detect_edges",
|
|
154
197
|
"detect_edges_advanced",
|
|
155
198
|
"detect_glitches",
|
|
199
|
+
"detect_logic_family",
|
|
156
200
|
"detect_multi_level",
|
|
201
|
+
"detect_open_collector",
|
|
157
202
|
"detect_violations",
|
|
203
|
+
"find_critical_paths",
|
|
158
204
|
"get_logic_threshold",
|
|
159
205
|
"hold_time",
|
|
206
|
+
"identify_ic",
|
|
160
207
|
"interpolate_edge_time",
|
|
161
208
|
"measure_clock_jitter",
|
|
162
209
|
"measure_edge_timing",
|
|
@@ -174,4 +221,5 @@ __all__ = [
|
|
|
174
221
|
"skew",
|
|
175
222
|
"slew_rate",
|
|
176
223
|
"to_digital",
|
|
224
|
+
"validate_ic_timing",
|
|
177
225
|
]
|
|
@@ -24,7 +24,6 @@ from dataclasses import dataclass
|
|
|
24
24
|
from typing import TYPE_CHECKING, Any, ClassVar, Literal
|
|
25
25
|
|
|
26
26
|
import numpy as np
|
|
27
|
-
from scipy import signal
|
|
28
27
|
|
|
29
28
|
from oscura.core.exceptions import InsufficientDataError, ValidationError
|
|
30
29
|
|
|
@@ -275,6 +274,9 @@ class ClockRecovery:
|
|
|
275
274
|
# Use PLL tracking for robust recovery
|
|
276
275
|
return self._pll_track(data_trace, freq)
|
|
277
276
|
else:
|
|
277
|
+
# Lazy import to avoid loading scipy at module import time
|
|
278
|
+
from scipy import signal
|
|
279
|
+
|
|
278
280
|
# Generate ideal square wave at detected frequency
|
|
279
281
|
_period_samples = self.sample_rate / freq
|
|
280
282
|
n_samples = len(data_trace)
|
|
@@ -498,6 +500,9 @@ class ClockRecovery:
|
|
|
498
500
|
Raises:
|
|
499
501
|
ValidationError: If sample rate is not set.
|
|
500
502
|
"""
|
|
503
|
+
# Lazy import to avoid loading scipy at module import time
|
|
504
|
+
from scipy import signal
|
|
505
|
+
|
|
501
506
|
# Remove DC component
|
|
502
507
|
trace_ac = trace - np.mean(trace)
|
|
503
508
|
|
|
@@ -539,6 +544,9 @@ class ClockRecovery:
|
|
|
539
544
|
Raises:
|
|
540
545
|
ValidationError: If no periodic pattern detected or sample rate not set.
|
|
541
546
|
"""
|
|
547
|
+
# Lazy import to avoid loading scipy at module import time
|
|
548
|
+
from scipy import signal
|
|
549
|
+
|
|
542
550
|
# Remove mean
|
|
543
551
|
trace_centered = trace - np.mean(trace)
|
|
544
552
|
|
|
@@ -25,6 +25,7 @@ if TYPE_CHECKING:
|
|
|
25
25
|
# Standard logic family threshold constants
|
|
26
26
|
# Reference: Various IC manufacturer datasheets
|
|
27
27
|
LOGIC_FAMILIES: dict[str, dict[str, float]] = {
|
|
28
|
+
# Modern logic families (5V)
|
|
28
29
|
"TTL": {
|
|
29
30
|
"VIL_max": 0.8, # Maximum input low voltage
|
|
30
31
|
"VIH_min": 2.0, # Minimum input high voltage
|
|
@@ -74,6 +75,63 @@ LOGIC_FAMILIES: dict[str, dict[str, float]] = {
|
|
|
74
75
|
"VOH_min": 1.1,
|
|
75
76
|
"VCC": 1.2,
|
|
76
77
|
},
|
|
78
|
+
# Vintage logic families (1960s-1970s)
|
|
79
|
+
"ECL": {
|
|
80
|
+
"VIL_max": -1.475, # ECL 10K series
|
|
81
|
+
"VIH_min": -1.105,
|
|
82
|
+
"VOL_max": -1.630, # Typical -1.63V
|
|
83
|
+
"VOH_min": -0.980, # Typical -0.98V
|
|
84
|
+
"VCC": 0.0, # Ground-referenced
|
|
85
|
+
"VEE": -5.2, # Negative supply
|
|
86
|
+
"differential": True, # Differential signaling
|
|
87
|
+
},
|
|
88
|
+
"ECL_100K": {
|
|
89
|
+
"VIL_max": -1.810, # ECL 100K series (faster)
|
|
90
|
+
"VIH_min": -1.620,
|
|
91
|
+
"VOL_max": -1.950,
|
|
92
|
+
"VOH_min": -1.650,
|
|
93
|
+
"VCC": 0.0,
|
|
94
|
+
"VEE": -5.2,
|
|
95
|
+
"differential": True,
|
|
96
|
+
},
|
|
97
|
+
"RTL": {
|
|
98
|
+
"VIL_max": 0.4, # Resistor-Transistor Logic
|
|
99
|
+
"VIH_min": 0.9,
|
|
100
|
+
"VOL_max": 0.2,
|
|
101
|
+
"VOH_min": 3.6, # Typical 3.6V high output
|
|
102
|
+
"VCC": 3.6, # 3.6V supply common
|
|
103
|
+
},
|
|
104
|
+
"DTL": {
|
|
105
|
+
"VIL_max": 0.5, # Diode-Transistor Logic
|
|
106
|
+
"VIH_min": 2.0,
|
|
107
|
+
"VOL_max": 0.4,
|
|
108
|
+
"VOH_min": 4.0, # Typical 4V high output
|
|
109
|
+
"VCC": 5.0,
|
|
110
|
+
},
|
|
111
|
+
"MOS": {
|
|
112
|
+
"VIL_max": -3.0, # P-channel MOS (negative logic)
|
|
113
|
+
"VIH_min": -10.0,
|
|
114
|
+
"VOL_max": -0.5,
|
|
115
|
+
"VOH_min": -11.5,
|
|
116
|
+
"VCC": 0.0, # Ground
|
|
117
|
+
"VDD": -12.0, # Negative supply
|
|
118
|
+
},
|
|
119
|
+
"PMOS": {
|
|
120
|
+
"VIL_max": -3.0, # PMOS (4000 series at -12V)
|
|
121
|
+
"VIH_min": -9.0,
|
|
122
|
+
"VOL_max": -0.5,
|
|
123
|
+
"VOH_min": -11.5,
|
|
124
|
+
"VCC": 0.0,
|
|
125
|
+
"VDD": -12.0,
|
|
126
|
+
},
|
|
127
|
+
"NMOS": {
|
|
128
|
+
"VIL_max": 1.5, # NMOS (positive logic, 12V)
|
|
129
|
+
"VIH_min": 8.0,
|
|
130
|
+
"VOL_max": 0.5,
|
|
131
|
+
"VOH_min": 11.5,
|
|
132
|
+
"VCC": 12.0,
|
|
133
|
+
"VSS": 0.0,
|
|
134
|
+
},
|
|
77
135
|
}
|
|
78
136
|
|
|
79
137
|
|
|
@@ -405,9 +463,146 @@ def get_logic_threshold(
|
|
|
405
463
|
raise ValueError(f"Unknown threshold_type: {threshold_type}")
|
|
406
464
|
|
|
407
465
|
|
|
466
|
+
def detect_logic_family(
|
|
467
|
+
trace: WaveformTrace,
|
|
468
|
+
*,
|
|
469
|
+
confidence_threshold: float = 0.8,
|
|
470
|
+
) -> tuple[str, float]:
|
|
471
|
+
"""Auto-detect logic family from signal voltage levels.
|
|
472
|
+
|
|
473
|
+
Analyzes the signal to determine which logic family it most likely represents
|
|
474
|
+
based on voltage levels.
|
|
475
|
+
|
|
476
|
+
Args:
|
|
477
|
+
trace: Input analog waveform trace.
|
|
478
|
+
confidence_threshold: Minimum confidence (0-1) to return a match.
|
|
479
|
+
|
|
480
|
+
Returns:
|
|
481
|
+
Tuple of (family_name, confidence_score).
|
|
482
|
+
Returns ("unknown", 0.0) if no match above threshold.
|
|
483
|
+
|
|
484
|
+
Example:
|
|
485
|
+
>>> family, conf = detect_logic_family(trace)
|
|
486
|
+
>>> print(f"Detected: {family} ({conf*100:.1f}% confidence)")
|
|
487
|
+
"""
|
|
488
|
+
if len(trace.data) < 10:
|
|
489
|
+
return ("unknown", 0.0)
|
|
490
|
+
|
|
491
|
+
data = np.asarray(trace.data)
|
|
492
|
+
|
|
493
|
+
# Find voltage levels using percentiles
|
|
494
|
+
p10, p90 = np.percentile(data, [10, 90])
|
|
495
|
+
vlow = p10
|
|
496
|
+
vhigh = p90
|
|
497
|
+
|
|
498
|
+
# Calculate midpoint and swing
|
|
499
|
+
vmid = (vlow + vhigh) / 2.0
|
|
500
|
+
vswing = vhigh - vlow
|
|
501
|
+
|
|
502
|
+
if vswing < 0.1: # Insufficient signal
|
|
503
|
+
return ("unknown", 0.0)
|
|
504
|
+
|
|
505
|
+
# Score each logic family
|
|
506
|
+
scores: dict[str, float] = {}
|
|
507
|
+
|
|
508
|
+
for family_name, levels in LOGIC_FAMILIES.items():
|
|
509
|
+
# Check if voltage levels match this family
|
|
510
|
+
vil_max = levels["VIL_max"]
|
|
511
|
+
vih_min = levels["VIH_min"]
|
|
512
|
+
vcc = levels.get("VCC", 5.0)
|
|
513
|
+
|
|
514
|
+
# Expected midpoint for this family
|
|
515
|
+
expected_mid = (vil_max + vih_min) / 2.0
|
|
516
|
+
|
|
517
|
+
# Score based on:
|
|
518
|
+
# 1. Midpoint proximity
|
|
519
|
+
# 2. High level proximity to VOH
|
|
520
|
+
# 3. Low level proximity to VOL
|
|
521
|
+
|
|
522
|
+
mid_error = abs(vmid - expected_mid)
|
|
523
|
+
high_error = abs(vhigh - levels.get("VOH_min", vih_min))
|
|
524
|
+
low_error = abs(vlow - levels.get("VOL_max", vil_max))
|
|
525
|
+
|
|
526
|
+
# Normalize errors (handle VCC=0 for ECL/PMOS)
|
|
527
|
+
if vcc != 0:
|
|
528
|
+
mid_score = max(0, 1.0 - mid_error / abs(vcc))
|
|
529
|
+
high_score = max(0, 1.0 - high_error / abs(vcc))
|
|
530
|
+
low_score = max(0, 1.0 - low_error / abs(vcc))
|
|
531
|
+
else:
|
|
532
|
+
# For VCC=0 families (ECL), use voltage range instead
|
|
533
|
+
voltage_range = abs(vih_min - vil_max)
|
|
534
|
+
if voltage_range > 0:
|
|
535
|
+
mid_score = max(0, 1.0 - mid_error / voltage_range)
|
|
536
|
+
high_score = max(0, 1.0 - high_error / voltage_range)
|
|
537
|
+
low_score = max(0, 1.0 - low_error / voltage_range)
|
|
538
|
+
else:
|
|
539
|
+
mid_score = high_score = low_score = 0.0
|
|
540
|
+
|
|
541
|
+
# Combined score (weighted average)
|
|
542
|
+
total_score = mid_score * 0.5 + high_score * 0.25 + low_score * 0.25
|
|
543
|
+
scores[family_name] = total_score
|
|
544
|
+
|
|
545
|
+
# Find best match
|
|
546
|
+
if not scores:
|
|
547
|
+
return ("unknown", 0.0)
|
|
548
|
+
|
|
549
|
+
best_family = max(scores.items(), key=lambda x: x[1])
|
|
550
|
+
|
|
551
|
+
if best_family[1] < confidence_threshold:
|
|
552
|
+
return ("unknown", best_family[1])
|
|
553
|
+
|
|
554
|
+
return best_family
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
def detect_open_collector(
|
|
558
|
+
trace: WaveformTrace,
|
|
559
|
+
*,
|
|
560
|
+
asymmetry_threshold: float = 3.0,
|
|
561
|
+
) -> tuple[bool, float]:
|
|
562
|
+
"""Detect open-collector or open-drain output.
|
|
563
|
+
|
|
564
|
+
Open-collector outputs have slow rise times (limited by pull-up resistor)
|
|
565
|
+
and fast fall times (active transistor).
|
|
566
|
+
|
|
567
|
+
Args:
|
|
568
|
+
trace: Input analog waveform trace.
|
|
569
|
+
asymmetry_threshold: Minimum rise/fall ratio to indicate open-collector.
|
|
570
|
+
|
|
571
|
+
Returns:
|
|
572
|
+
Tuple of (is_open_collector, asymmetry_ratio).
|
|
573
|
+
|
|
574
|
+
Example:
|
|
575
|
+
>>> is_oc, ratio = detect_open_collector(trace)
|
|
576
|
+
>>> if is_oc:
|
|
577
|
+
... print(f"Open-collector detected (rise/fall = {ratio:.1f})")
|
|
578
|
+
"""
|
|
579
|
+
from oscura.analyzers.waveform.measurements import fall_time, rise_time
|
|
580
|
+
|
|
581
|
+
if len(trace.data) < 10:
|
|
582
|
+
return (False, 1.0)
|
|
583
|
+
|
|
584
|
+
# Measure rise and fall times
|
|
585
|
+
tr = rise_time(trace, ref_levels=(0.1, 0.9))
|
|
586
|
+
tf = fall_time(trace, ref_levels=(0.9, 0.1))
|
|
587
|
+
|
|
588
|
+
# Check for valid measurements
|
|
589
|
+
if np.isnan(tr) or np.isnan(tf) or tf == 0:
|
|
590
|
+
return (False, 1.0)
|
|
591
|
+
|
|
592
|
+
# Calculate asymmetry ratio
|
|
593
|
+
asymmetry = tr / tf
|
|
594
|
+
|
|
595
|
+
# Open-collector has slow rise, fast fall
|
|
596
|
+
is_oc = bool(asymmetry >= asymmetry_threshold)
|
|
597
|
+
|
|
598
|
+
return (is_oc, float(asymmetry))
|
|
599
|
+
|
|
600
|
+
|
|
408
601
|
__all__ = [
|
|
409
602
|
"LOGIC_FAMILIES",
|
|
410
603
|
"detect_edges",
|
|
604
|
+
"detect_logic_family",
|
|
605
|
+
"detect_open_collector",
|
|
411
606
|
"get_logic_threshold",
|
|
412
607
|
"to_digital",
|
|
413
608
|
]
|