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
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"""Synthetic signal generation source.
|
|
2
|
+
|
|
3
|
+
This module provides SyntheticSource, which wraps SignalBuilder to implement
|
|
4
|
+
the unified Source protocol. SyntheticSource makes synthetic signals consistent
|
|
5
|
+
with all other acquisition methods (files, hardware).
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> from oscura.acquisition import SyntheticSource
|
|
9
|
+
>>> from oscura import SignalBuilder
|
|
10
|
+
>>>
|
|
11
|
+
>>> # Create signal builder
|
|
12
|
+
>>> builder = SignalBuilder(sample_rate=1e6, duration=0.01)
|
|
13
|
+
>>> builder = builder.add_sine(frequency=1000, amplitude=1.0)
|
|
14
|
+
>>> builder = builder.add_noise(snr_db=40)
|
|
15
|
+
>>>
|
|
16
|
+
>>> # Wrap in Source for unified interface
|
|
17
|
+
>>> source = SyntheticSource(builder)
|
|
18
|
+
>>> trace = source.read()
|
|
19
|
+
>>>
|
|
20
|
+
>>> # Or one-liner
|
|
21
|
+
>>> source = SyntheticSource(
|
|
22
|
+
... SignalBuilder().sample_rate(1e6).add_sine(1000).add_noise(snr_db=40)
|
|
23
|
+
... )
|
|
24
|
+
>>> trace = source.read()
|
|
25
|
+
|
|
26
|
+
Pattern:
|
|
27
|
+
SyntheticSource bridges SignalBuilder (generator pattern) with
|
|
28
|
+
Source protocol (acquisition pattern). This enables:
|
|
29
|
+
- Polymorphic use with FileSource and HardwareSource
|
|
30
|
+
- Session management with synthetic signals
|
|
31
|
+
- Pipeline composition with generated data
|
|
32
|
+
|
|
33
|
+
Note:
|
|
34
|
+
SignalBuilder.build() returns WaveformTrace for single-channel signals.
|
|
35
|
+
SignalBuilder.build_channels() returns dict[str, WaveformTrace] for multi-channel.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
from __future__ import annotations
|
|
39
|
+
|
|
40
|
+
from collections.abc import Iterator
|
|
41
|
+
from typing import TYPE_CHECKING
|
|
42
|
+
|
|
43
|
+
if TYPE_CHECKING:
|
|
44
|
+
from oscura.builders.signal_builder import SignalBuilder
|
|
45
|
+
from oscura.core.types import Trace, WaveformTrace
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class SyntheticSource:
|
|
49
|
+
"""Synthetic signal source implementing Source protocol.
|
|
50
|
+
|
|
51
|
+
Wraps SignalBuilder to provide unified acquisition interface.
|
|
52
|
+
Enables synthetic signals to be used anywhere a Source is expected.
|
|
53
|
+
|
|
54
|
+
Attributes:
|
|
55
|
+
builder: SignalBuilder instance to generate from.
|
|
56
|
+
channel: Channel name to extract (for multi-channel signals).
|
|
57
|
+
|
|
58
|
+
Example:
|
|
59
|
+
>>> from oscura import SignalBuilder
|
|
60
|
+
>>> from oscura.acquisition import SyntheticSource
|
|
61
|
+
>>>
|
|
62
|
+
>>> # Create builder
|
|
63
|
+
>>> builder = (SignalBuilder(sample_rate=1e6, duration=0.01)
|
|
64
|
+
... .add_sine(frequency=1000)
|
|
65
|
+
... .add_noise(snr_db=40))
|
|
66
|
+
>>>
|
|
67
|
+
>>> # Wrap in source
|
|
68
|
+
>>> source = SyntheticSource(builder)
|
|
69
|
+
>>> trace = source.read()
|
|
70
|
+
>>>
|
|
71
|
+
>>> # Multi-channel
|
|
72
|
+
>>> builder = SignalBuilder().sample_rate(1e6)
|
|
73
|
+
>>> builder = builder.add_sine(1000, channel="sig")
|
|
74
|
+
>>> builder = builder.add_square(500, channel="clk")
|
|
75
|
+
>>> source = SyntheticSource(builder, channel="sig")
|
|
76
|
+
>>> trace = source.read() # Gets "sig" channel only
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
def __init__(
|
|
80
|
+
self,
|
|
81
|
+
builder: SignalBuilder,
|
|
82
|
+
*,
|
|
83
|
+
channel: str | None = None,
|
|
84
|
+
) -> None:
|
|
85
|
+
"""Initialize synthetic source.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
builder: SignalBuilder instance to generate from.
|
|
89
|
+
channel: Optional channel name for multi-channel signals.
|
|
90
|
+
If None, uses first channel (or converts multi-channel to
|
|
91
|
+
single-channel trace if possible).
|
|
92
|
+
|
|
93
|
+
Example:
|
|
94
|
+
>>> builder = SignalBuilder().sample_rate(1e6).add_sine(1000)
|
|
95
|
+
>>> source = SyntheticSource(builder)
|
|
96
|
+
>>> source = SyntheticSource(builder, channel="ch1")
|
|
97
|
+
"""
|
|
98
|
+
self.builder = builder
|
|
99
|
+
self.channel = channel
|
|
100
|
+
self._closed = False
|
|
101
|
+
self._cached_trace: WaveformTrace | None = None
|
|
102
|
+
|
|
103
|
+
def read(self) -> Trace:
|
|
104
|
+
"""Generate and return complete trace.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
WaveformTrace with generated signal data.
|
|
108
|
+
|
|
109
|
+
Raises:
|
|
110
|
+
ValueError: If source is closed or builder has no signals.
|
|
111
|
+
|
|
112
|
+
Example:
|
|
113
|
+
>>> builder = SignalBuilder().sample_rate(1e6).add_sine(1000)
|
|
114
|
+
>>> source = SyntheticSource(builder)
|
|
115
|
+
>>> trace = source.read()
|
|
116
|
+
>>> print(f"Generated {len(trace.data)} samples")
|
|
117
|
+
|
|
118
|
+
Note:
|
|
119
|
+
This method caches the generated trace to avoid regenerating
|
|
120
|
+
on multiple calls. Call close() and recreate to generate new data.
|
|
121
|
+
"""
|
|
122
|
+
if self._closed:
|
|
123
|
+
raise ValueError("Cannot read from closed source")
|
|
124
|
+
|
|
125
|
+
# Return cached trace if available
|
|
126
|
+
if self._cached_trace is not None:
|
|
127
|
+
return self._cached_trace
|
|
128
|
+
|
|
129
|
+
# Build signal
|
|
130
|
+
# Phase 0.2: build() now returns WaveformTrace directly
|
|
131
|
+
trace = self.builder.build(channel=self.channel)
|
|
132
|
+
|
|
133
|
+
self._cached_trace = trace
|
|
134
|
+
return trace
|
|
135
|
+
|
|
136
|
+
def stream(self, chunk_size: int) -> Iterator[Trace]:
|
|
137
|
+
"""Stream generated signal in chunks.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
chunk_size: Number of samples per chunk.
|
|
141
|
+
|
|
142
|
+
Yields:
|
|
143
|
+
Trace chunks.
|
|
144
|
+
|
|
145
|
+
Example:
|
|
146
|
+
>>> builder = SignalBuilder().sample_rate(1e6).duration(1.0).add_sine(1000)
|
|
147
|
+
>>> source = SyntheticSource(builder)
|
|
148
|
+
>>> for chunk in source.stream(chunk_size=10000):
|
|
149
|
+
... process_chunk(chunk)
|
|
150
|
+
|
|
151
|
+
Note:
|
|
152
|
+
Synthetic signals are generated once and sliced into chunks.
|
|
153
|
+
This is different from hardware streaming where data arrives continuously.
|
|
154
|
+
"""
|
|
155
|
+
if self._closed:
|
|
156
|
+
raise ValueError("Cannot stream from closed source")
|
|
157
|
+
|
|
158
|
+
# Generate full trace
|
|
159
|
+
trace = self.read()
|
|
160
|
+
|
|
161
|
+
# Yield chunks
|
|
162
|
+
from oscura.core.types import DigitalTrace, IQTrace, TraceMetadata, WaveformTrace
|
|
163
|
+
|
|
164
|
+
# IQTrace not supported by SyntheticSource
|
|
165
|
+
if isinstance(trace, IQTrace):
|
|
166
|
+
raise TypeError("IQTrace not supported by SyntheticSource")
|
|
167
|
+
|
|
168
|
+
n_samples = len(trace.data)
|
|
169
|
+
|
|
170
|
+
for start in range(0, n_samples, chunk_size):
|
|
171
|
+
end = min(start + chunk_size, n_samples)
|
|
172
|
+
chunk_data = trace.data[start:end]
|
|
173
|
+
|
|
174
|
+
# Create chunk metadata (same as parent)
|
|
175
|
+
chunk_metadata = TraceMetadata(
|
|
176
|
+
sample_rate=trace.metadata.sample_rate,
|
|
177
|
+
vertical_scale=trace.metadata.vertical_scale,
|
|
178
|
+
vertical_offset=trace.metadata.vertical_offset,
|
|
179
|
+
acquisition_time=trace.metadata.acquisition_time,
|
|
180
|
+
trigger_info=trace.metadata.trigger_info,
|
|
181
|
+
source_file=trace.metadata.source_file,
|
|
182
|
+
channel_name=trace.metadata.channel_name,
|
|
183
|
+
calibration_info=trace.metadata.calibration_info,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
if isinstance(trace, DigitalTrace):
|
|
187
|
+
yield DigitalTrace(data=chunk_data, metadata=chunk_metadata) # type: ignore[arg-type]
|
|
188
|
+
else:
|
|
189
|
+
yield WaveformTrace(data=chunk_data, metadata=chunk_metadata) # type: ignore[arg-type]
|
|
190
|
+
|
|
191
|
+
def close(self) -> None:
|
|
192
|
+
"""Close the source and release resources.
|
|
193
|
+
|
|
194
|
+
For synthetic sources, this clears the cached trace.
|
|
195
|
+
|
|
196
|
+
Example:
|
|
197
|
+
>>> source = SyntheticSource(builder)
|
|
198
|
+
>>> trace = source.read()
|
|
199
|
+
>>> source.close()
|
|
200
|
+
>>> # source.read() would now fail
|
|
201
|
+
"""
|
|
202
|
+
self._closed = True
|
|
203
|
+
self._cached_trace = None
|
|
204
|
+
|
|
205
|
+
def __enter__(self) -> SyntheticSource:
|
|
206
|
+
"""Context manager entry.
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
Self for use in 'with' statement.
|
|
210
|
+
|
|
211
|
+
Example:
|
|
212
|
+
>>> with SyntheticSource(builder) as source:
|
|
213
|
+
... trace = source.read()
|
|
214
|
+
"""
|
|
215
|
+
return self
|
|
216
|
+
|
|
217
|
+
def __exit__(self, *args: object) -> None:
|
|
218
|
+
"""Context manager exit.
|
|
219
|
+
|
|
220
|
+
Automatically calls close() when exiting 'with' block.
|
|
221
|
+
"""
|
|
222
|
+
self.close()
|
|
223
|
+
|
|
224
|
+
def __repr__(self) -> str:
|
|
225
|
+
"""String representation."""
|
|
226
|
+
return f"SyntheticSource(builder={self.builder!r}, channel={self.channel!r})"
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
__all__ = ["SyntheticSource"]
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
"""PyVISA oscilloscope and instrument acquisition source.
|
|
2
|
+
|
|
3
|
+
This module provides VISASource for acquiring waveforms from oscilloscopes and
|
|
4
|
+
other SCPI-compatible instruments via PyVISA. Supports USB, GPIB, Ethernet, and
|
|
5
|
+
serial connections.
|
|
6
|
+
|
|
7
|
+
The VISA source communicates with instruments using SCPI commands and acquires
|
|
8
|
+
waveform data directly from the oscilloscope, returning WaveformTrace format
|
|
9
|
+
for analysis.
|
|
10
|
+
|
|
11
|
+
Example:
|
|
12
|
+
>>> from oscura.acquisition import HardwareSource
|
|
13
|
+
>>>
|
|
14
|
+
>>> # USB oscilloscope
|
|
15
|
+
>>> with HardwareSource.visa("USB0::0x0699::0x0401::INSTR") as scope:
|
|
16
|
+
... scope.configure(channels=[1, 2], timebase=1e-6)
|
|
17
|
+
... trace = scope.read()
|
|
18
|
+
>>>
|
|
19
|
+
>>> # Ethernet oscilloscope
|
|
20
|
+
>>> with HardwareSource.visa("TCPIP::192.168.1.100::INSTR") as scope:
|
|
21
|
+
... scope.configure(channels=[1], vertical_scale=0.5)
|
|
22
|
+
... trace = scope.read()
|
|
23
|
+
>>>
|
|
24
|
+
>>> # Auto-detect
|
|
25
|
+
>>> with HardwareSource.visa() as scope:
|
|
26
|
+
... trace = scope.read()
|
|
27
|
+
|
|
28
|
+
Dependencies:
|
|
29
|
+
Requires pyvisa and pyvisa-py: pip install pyvisa pyvisa-py
|
|
30
|
+
|
|
31
|
+
Platform:
|
|
32
|
+
Cross-platform (Windows, macOS, Linux).
|
|
33
|
+
|
|
34
|
+
Supported Instruments:
|
|
35
|
+
- Tektronix oscilloscopes (DPO, MSO, MDO series)
|
|
36
|
+
- Keysight oscilloscopes (InfiniiVision, MSO-X series)
|
|
37
|
+
- Rigol oscilloscopes (DS1000Z, DS4000 series)
|
|
38
|
+
- Other SCPI-compatible instruments
|
|
39
|
+
|
|
40
|
+
References:
|
|
41
|
+
PyVISA: https://pyvisa.readthedocs.io/
|
|
42
|
+
SCPI Standard: IEEE 488.2
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
from __future__ import annotations
|
|
46
|
+
|
|
47
|
+
from collections.abc import Iterator
|
|
48
|
+
from datetime import datetime
|
|
49
|
+
from typing import TYPE_CHECKING, Any
|
|
50
|
+
|
|
51
|
+
if TYPE_CHECKING:
|
|
52
|
+
from pyvisa import Resource, ResourceManager
|
|
53
|
+
|
|
54
|
+
from oscura.core.types import Trace
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class VISASource:
|
|
58
|
+
"""PyVISA instrument acquisition source.
|
|
59
|
+
|
|
60
|
+
Acquires waveforms from oscilloscopes and other SCPI-compatible instruments
|
|
61
|
+
via PyVISA. Supports multiple connection types (USB, GPIB, Ethernet, serial).
|
|
62
|
+
|
|
63
|
+
Attributes:
|
|
64
|
+
resource: VISA resource string.
|
|
65
|
+
instrument: PyVISA instrument instance.
|
|
66
|
+
channels: Configured channel list.
|
|
67
|
+
timebase: Configured timebase in seconds/division.
|
|
68
|
+
vertical_scale: Configured vertical scale in volts/division.
|
|
69
|
+
|
|
70
|
+
Example:
|
|
71
|
+
>>> scope = VISASource("USB0::0x0699::0x0401::INSTR")
|
|
72
|
+
>>> scope.configure(channels=[1, 2], timebase=1e-6)
|
|
73
|
+
>>> trace = scope.read()
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
def __init__(
|
|
77
|
+
self,
|
|
78
|
+
resource: str | None = None,
|
|
79
|
+
**kwargs: Any,
|
|
80
|
+
) -> None:
|
|
81
|
+
"""Initialize VISA source.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
resource: VISA resource string (optional, auto-detects if None).
|
|
85
|
+
Examples: "USB0::0x0699::0x0401::INSTR", "TCPIP::192.168.1.100::INSTR"
|
|
86
|
+
**kwargs: Additional PyVISA configuration options.
|
|
87
|
+
|
|
88
|
+
Raises:
|
|
89
|
+
ImportError: If pyvisa is not installed.
|
|
90
|
+
|
|
91
|
+
Example:
|
|
92
|
+
>>> scope = VISASource("USB0::0x0699::0x0401::INSTR")
|
|
93
|
+
>>> scope = VISASource() # Auto-detect
|
|
94
|
+
"""
|
|
95
|
+
self.resource = resource
|
|
96
|
+
self.kwargs = kwargs
|
|
97
|
+
self.rm: ResourceManager | None = None
|
|
98
|
+
self.instrument: Resource | None = None
|
|
99
|
+
self._closed = False
|
|
100
|
+
|
|
101
|
+
# Configuration
|
|
102
|
+
self.channels: list[int] = [1]
|
|
103
|
+
self.timebase: float = 1e-6
|
|
104
|
+
self.vertical_scale: float = 1.0
|
|
105
|
+
self.record_length: int = 10000
|
|
106
|
+
|
|
107
|
+
def _ensure_connection(self) -> None:
|
|
108
|
+
"""Ensure connection to VISA instrument.
|
|
109
|
+
|
|
110
|
+
Raises:
|
|
111
|
+
ImportError: If pyvisa is not installed.
|
|
112
|
+
RuntimeError: If no instrument found or connection fails.
|
|
113
|
+
"""
|
|
114
|
+
if self.instrument is not None:
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
import pyvisa
|
|
119
|
+
except ImportError as e:
|
|
120
|
+
raise ImportError(
|
|
121
|
+
"VISA source requires pyvisa library. Install with: pip install pyvisa pyvisa-py"
|
|
122
|
+
) from e
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
self.rm = pyvisa.ResourceManager()
|
|
126
|
+
|
|
127
|
+
# Auto-detect if resource not specified
|
|
128
|
+
if self.resource is None:
|
|
129
|
+
resources = self.rm.list_resources()
|
|
130
|
+
if not resources:
|
|
131
|
+
raise RuntimeError("No VISA instruments found")
|
|
132
|
+
self.resource = resources[0]
|
|
133
|
+
|
|
134
|
+
self.instrument = self.rm.open_resource(self.resource, **self.kwargs)
|
|
135
|
+
|
|
136
|
+
# Query instrument identity
|
|
137
|
+
try:
|
|
138
|
+
idn = self.instrument.query("*IDN?")
|
|
139
|
+
print(f"Connected to: {idn.strip()}")
|
|
140
|
+
except Exception:
|
|
141
|
+
# Some instruments may not support *IDN?
|
|
142
|
+
pass
|
|
143
|
+
|
|
144
|
+
except Exception as e:
|
|
145
|
+
raise RuntimeError(
|
|
146
|
+
f"Failed to connect to VISA instrument '{self.resource}'. "
|
|
147
|
+
f"Ensure instrument is connected and powered on. "
|
|
148
|
+
f"Error: {e}"
|
|
149
|
+
) from e
|
|
150
|
+
|
|
151
|
+
def configure(
|
|
152
|
+
self,
|
|
153
|
+
*,
|
|
154
|
+
channels: list[int] | None = None,
|
|
155
|
+
timebase: float | None = None,
|
|
156
|
+
vertical_scale: float | None = None,
|
|
157
|
+
record_length: int | None = None,
|
|
158
|
+
) -> None:
|
|
159
|
+
"""Configure oscilloscope acquisition parameters.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
channels: List of channel numbers to acquire (e.g., [1, 2]).
|
|
163
|
+
timebase: Timebase in seconds/division (e.g., 1e-6 for 1 µs/div).
|
|
164
|
+
vertical_scale: Vertical scale in volts/division (e.g., 0.5).
|
|
165
|
+
record_length: Number of samples to acquire (e.g., 10000).
|
|
166
|
+
|
|
167
|
+
Example:
|
|
168
|
+
>>> scope = VISASource()
|
|
169
|
+
>>> scope.configure(
|
|
170
|
+
... channels=[1, 2],
|
|
171
|
+
... timebase=1e-6,
|
|
172
|
+
... vertical_scale=0.5,
|
|
173
|
+
... record_length=10000
|
|
174
|
+
... )
|
|
175
|
+
"""
|
|
176
|
+
if channels is not None:
|
|
177
|
+
self.channels = channels
|
|
178
|
+
if timebase is not None:
|
|
179
|
+
self.timebase = timebase
|
|
180
|
+
if vertical_scale is not None:
|
|
181
|
+
self.vertical_scale = vertical_scale
|
|
182
|
+
if record_length is not None:
|
|
183
|
+
self.record_length = record_length
|
|
184
|
+
|
|
185
|
+
# Apply configuration to instrument
|
|
186
|
+
self._ensure_connection()
|
|
187
|
+
|
|
188
|
+
try:
|
|
189
|
+
# Set horizontal (timebase)
|
|
190
|
+
self.instrument.write(f":TIMebase:SCALe {self.timebase}") # type: ignore[union-attr]
|
|
191
|
+
|
|
192
|
+
# Set vertical scale for each channel
|
|
193
|
+
for ch in self.channels:
|
|
194
|
+
self.instrument.write(f":CHANnel{ch}:SCALe {self.vertical_scale}") # type: ignore[union-attr]
|
|
195
|
+
self.instrument.write(f":CHANnel{ch}:DISPlay ON") # type: ignore[union-attr]
|
|
196
|
+
|
|
197
|
+
# Set record length
|
|
198
|
+
self.instrument.write(f":ACQuire:POINts {self.record_length}") # type: ignore[union-attr]
|
|
199
|
+
|
|
200
|
+
except Exception as e:
|
|
201
|
+
# SCPI commands vary by manufacturer, log but continue
|
|
202
|
+
print(f"Warning: Configuration command failed: {e}")
|
|
203
|
+
|
|
204
|
+
def read(self, channel: int | None = None) -> Trace:
|
|
205
|
+
"""Read waveform from oscilloscope.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
channel: Channel to read (uses first configured channel if None).
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
WaveformTrace containing acquired waveform.
|
|
212
|
+
|
|
213
|
+
Raises:
|
|
214
|
+
ImportError: If pyvisa is not installed.
|
|
215
|
+
RuntimeError: If acquisition fails.
|
|
216
|
+
ValueError: If source is closed.
|
|
217
|
+
|
|
218
|
+
Example:
|
|
219
|
+
>>> scope = VISASource()
|
|
220
|
+
>>> scope.configure(channels=[1, 2])
|
|
221
|
+
>>> trace = scope.read(channel=1)
|
|
222
|
+
"""
|
|
223
|
+
if self._closed:
|
|
224
|
+
raise ValueError("Cannot read from closed source")
|
|
225
|
+
|
|
226
|
+
self._ensure_connection()
|
|
227
|
+
|
|
228
|
+
import numpy as np
|
|
229
|
+
|
|
230
|
+
from oscura.core.types import CalibrationInfo, TraceMetadata, WaveformTrace
|
|
231
|
+
|
|
232
|
+
if channel is None:
|
|
233
|
+
channel = self.channels[0]
|
|
234
|
+
|
|
235
|
+
acquisition_start = datetime.now()
|
|
236
|
+
|
|
237
|
+
try:
|
|
238
|
+
# Single acquisition
|
|
239
|
+
self.instrument.write(":SINGle") # type: ignore[union-attr]
|
|
240
|
+
|
|
241
|
+
# Wait for acquisition to complete
|
|
242
|
+
import time
|
|
243
|
+
|
|
244
|
+
time.sleep(0.1)
|
|
245
|
+
|
|
246
|
+
# Set data source
|
|
247
|
+
self.instrument.write(f":DATa:SOURce CHANnel{channel}") # type: ignore[union-attr]
|
|
248
|
+
|
|
249
|
+
# Get waveform preamble (metadata)
|
|
250
|
+
preamble = self.instrument.query(":WAVeform:PREamble?") # type: ignore[union-attr]
|
|
251
|
+
preamble_parts = preamble.split(",")
|
|
252
|
+
|
|
253
|
+
# Parse preamble (format varies by manufacturer)
|
|
254
|
+
try:
|
|
255
|
+
x_increment = float(preamble_parts[4]) # Time between samples
|
|
256
|
+
sample_rate = 1.0 / x_increment
|
|
257
|
+
except (IndexError, ValueError):
|
|
258
|
+
sample_rate = 1e9 # Default 1 GSa/s
|
|
259
|
+
|
|
260
|
+
# Get waveform data
|
|
261
|
+
self.instrument.write(":WAVeform:FORMat WORD") # type: ignore[union-attr]
|
|
262
|
+
raw_data = self.instrument.query_binary_values( # type: ignore[union-attr]
|
|
263
|
+
":WAVeform:DATA?", datatype="h", is_big_endian=True
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
# Convert to voltage
|
|
267
|
+
data = np.array(raw_data, dtype=np.float64)
|
|
268
|
+
|
|
269
|
+
# Get instrument identity for calibration info
|
|
270
|
+
try:
|
|
271
|
+
idn = self.instrument.query("*IDN?").strip() # type: ignore[union-attr]
|
|
272
|
+
except Exception:
|
|
273
|
+
idn = "Unknown Instrument"
|
|
274
|
+
|
|
275
|
+
calibration_info = CalibrationInfo(
|
|
276
|
+
instrument=idn,
|
|
277
|
+
coupling="DC", # Default
|
|
278
|
+
vertical_resolution=8, # Typical for oscilloscopes
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
metadata = TraceMetadata(
|
|
282
|
+
sample_rate=sample_rate,
|
|
283
|
+
vertical_scale=self.vertical_scale,
|
|
284
|
+
acquisition_time=acquisition_start,
|
|
285
|
+
source_file=f"visa://{self.resource}",
|
|
286
|
+
channel_name=f"CH{channel}",
|
|
287
|
+
calibration_info=calibration_info,
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
return WaveformTrace(data=data, metadata=metadata)
|
|
291
|
+
|
|
292
|
+
except Exception as e:
|
|
293
|
+
raise RuntimeError(
|
|
294
|
+
f"Failed to acquire waveform from channel {channel}. Error: {e}"
|
|
295
|
+
) from e
|
|
296
|
+
|
|
297
|
+
def stream(self, duration: float = 60.0, interval: float = 1.0) -> Iterator[Trace]:
|
|
298
|
+
"""Stream waveforms at regular intervals.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
duration: Total acquisition duration in seconds (default: 60.0).
|
|
302
|
+
interval: Time between acquisitions in seconds (default: 1.0).
|
|
303
|
+
|
|
304
|
+
Yields:
|
|
305
|
+
WaveformTrace for each acquisition.
|
|
306
|
+
|
|
307
|
+
Raises:
|
|
308
|
+
ValueError: If source is closed.
|
|
309
|
+
|
|
310
|
+
Example:
|
|
311
|
+
>>> scope = VISASource()
|
|
312
|
+
>>> scope.configure(channels=[1])
|
|
313
|
+
>>> for trace in scope.stream(duration=60, interval=1):
|
|
314
|
+
... analyze(trace)
|
|
315
|
+
|
|
316
|
+
Note:
|
|
317
|
+
Acquires repeated single-shot waveforms, not continuous streaming.
|
|
318
|
+
"""
|
|
319
|
+
if self._closed:
|
|
320
|
+
raise ValueError("Cannot stream from closed source")
|
|
321
|
+
|
|
322
|
+
import time
|
|
323
|
+
|
|
324
|
+
start_time = time.time()
|
|
325
|
+
|
|
326
|
+
while time.time() - start_time < duration:
|
|
327
|
+
# Acquire waveform
|
|
328
|
+
trace = self.read()
|
|
329
|
+
yield trace
|
|
330
|
+
|
|
331
|
+
# Wait for next acquisition
|
|
332
|
+
time.sleep(interval)
|
|
333
|
+
|
|
334
|
+
def close(self) -> None:
|
|
335
|
+
"""Close connection to VISA instrument.
|
|
336
|
+
|
|
337
|
+
Example:
|
|
338
|
+
>>> scope = VISASource()
|
|
339
|
+
>>> scope.configure(channels=[1])
|
|
340
|
+
>>> trace = scope.read()
|
|
341
|
+
>>> scope.close()
|
|
342
|
+
"""
|
|
343
|
+
if self.instrument is not None:
|
|
344
|
+
self.instrument.close()
|
|
345
|
+
self.instrument = None
|
|
346
|
+
if self.rm is not None:
|
|
347
|
+
self.rm.close()
|
|
348
|
+
self.rm = None
|
|
349
|
+
self._closed = True
|
|
350
|
+
|
|
351
|
+
def __enter__(self) -> VISASource:
|
|
352
|
+
"""Context manager entry.
|
|
353
|
+
|
|
354
|
+
Returns:
|
|
355
|
+
Self for use in 'with' statement.
|
|
356
|
+
|
|
357
|
+
Example:
|
|
358
|
+
>>> with VISASource() as scope:
|
|
359
|
+
... scope.configure(channels=[1])
|
|
360
|
+
... trace = scope.read()
|
|
361
|
+
"""
|
|
362
|
+
return self
|
|
363
|
+
|
|
364
|
+
def __exit__(self, *args: object) -> None:
|
|
365
|
+
"""Context manager exit.
|
|
366
|
+
|
|
367
|
+
Automatically calls close() when exiting 'with' block.
|
|
368
|
+
"""
|
|
369
|
+
self.close()
|
|
370
|
+
|
|
371
|
+
def __repr__(self) -> str:
|
|
372
|
+
"""String representation."""
|
|
373
|
+
return f"VISASource(resource={self.resource!r})"
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
__all__ = ["VISASource"]
|
oscura/analyzers/__init__.py
CHANGED
|
@@ -9,6 +9,7 @@ Provides signal analysis functionality including:
|
|
|
9
9
|
- Jitter analysis (RJ, DJ, PJ, DDJ, bathtub curves)
|
|
10
10
|
- Eye diagram analysis (height, width, Q-factor)
|
|
11
11
|
- Signal integrity (S-parameters, equalization)
|
|
12
|
+
- Side-channel analysis (DPA, CPA, timing attacks)
|
|
12
13
|
"""
|
|
13
14
|
|
|
14
15
|
# Import measurements module as namespace for DSL compatibility
|
|
@@ -18,6 +19,7 @@ from oscura.analyzers import (
|
|
|
18
19
|
jitter,
|
|
19
20
|
measurements,
|
|
20
21
|
protocols,
|
|
22
|
+
side_channel,
|
|
21
23
|
signal_integrity,
|
|
22
24
|
statistics,
|
|
23
25
|
validation,
|
|
@@ -30,6 +32,7 @@ __all__ = [
|
|
|
30
32
|
"jitter",
|
|
31
33
|
"measurements",
|
|
32
34
|
"protocols",
|
|
35
|
+
"side_channel",
|
|
33
36
|
"signal_integrity",
|
|
34
37
|
"statistics",
|
|
35
38
|
"validation",
|