oscura 0.1.2__py3-none-any.whl → 0.4.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/clock.py +9 -1
- oscura/analyzers/digital/edges.py +1 -1
- oscura/analyzers/digital/timing.py +41 -11
- oscura/analyzers/packet/payload_extraction.py +2 -4
- oscura/analyzers/packet/stream.py +5 -5
- oscura/analyzers/patterns/__init__.py +4 -3
- oscura/analyzers/patterns/clustering.py +3 -1
- oscura/analyzers/power/ac_power.py +0 -2
- oscura/analyzers/power/basic.py +0 -2
- oscura/analyzers/power/ripple.py +0 -2
- 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/embedding.py +0 -2
- oscura/analyzers/signal_integrity/sparams.py +28 -206
- oscura/analyzers/spectral/fft.py +0 -2
- oscura/analyzers/statistical/__init__.py +3 -3
- oscura/analyzers/statistical/checksum.py +2 -0
- oscura/analyzers/statistical/classification.py +2 -0
- oscura/analyzers/statistical/entropy.py +11 -9
- oscura/analyzers/statistical/ngrams.py +4 -2
- oscura/api/fluent.py +2 -2
- oscura/automotive/__init__.py +4 -4
- oscura/automotive/can/__init__.py +0 -2
- oscura/automotive/can/patterns.py +3 -1
- oscura/automotive/can/session.py +277 -78
- oscura/automotive/can/state_machine.py +5 -2
- oscura/automotive/dbc/__init__.py +0 -2
- oscura/automotive/dtc/__init__.py +0 -2
- oscura/automotive/dtc/data.json +2763 -0
- oscura/automotive/dtc/database.py +37 -2769
- oscura/automotive/j1939/__init__.py +0 -2
- oscura/automotive/loaders/__init__.py +0 -2
- oscura/automotive/loaders/asc.py +0 -2
- oscura/automotive/loaders/blf.py +0 -2
- oscura/automotive/loaders/csv_can.py +0 -2
- oscura/automotive/obd/__init__.py +0 -2
- oscura/automotive/uds/__init__.py +0 -2
- oscura/automotive/uds/models.py +0 -2
- oscura/builders/__init__.py +9 -11
- oscura/builders/signal_builder.py +99 -191
- oscura/cli/main.py +0 -2
- oscura/cli/shell.py +0 -2
- oscura/config/loader.py +0 -2
- oscura/core/backend_selector.py +1 -1
- oscura/core/correlation.py +0 -2
- oscura/core/exceptions.py +61 -3
- oscura/core/lazy.py +5 -3
- oscura/core/memory_limits.py +0 -2
- oscura/core/numba_backend.py +5 -7
- oscura/core/uncertainty.py +3 -3
- oscura/dsl/interpreter.py +2 -0
- oscura/dsl/parser.py +8 -6
- oscura/exploratory/error_recovery.py +3 -3
- oscura/exploratory/parse.py +2 -0
- oscura/exploratory/recovery.py +2 -0
- oscura/exploratory/sync.py +2 -0
- oscura/export/wireshark/generator.py +1 -1
- oscura/export/wireshark/type_mapping.py +2 -0
- oscura/exporters/hdf5.py +1 -3
- oscura/extensibility/templates.py +0 -8
- oscura/inference/active_learning/lstar.py +2 -4
- oscura/inference/active_learning/observation_table.py +0 -2
- oscura/inference/active_learning/oracle.py +3 -1
- oscura/inference/active_learning/teachers/simulator.py +1 -3
- oscura/inference/alignment.py +2 -0
- oscura/inference/message_format.py +2 -0
- oscura/inference/protocol_dsl.py +7 -5
- oscura/inference/sequences.py +12 -14
- oscura/inference/state_machine.py +2 -0
- oscura/integrations/llm.py +3 -1
- oscura/jupyter/display.py +0 -2
- oscura/loaders/__init__.py +68 -51
- oscura/loaders/chipwhisperer.py +393 -0
- oscura/loaders/pcap.py +1 -1
- oscura/loaders/touchstone.py +221 -0
- oscura/math/arithmetic.py +0 -2
- oscura/optimization/parallel.py +9 -6
- oscura/pipeline/composition.py +0 -2
- oscura/plugins/cli.py +0 -2
- oscura/reporting/comparison.py +0 -2
- oscura/reporting/config.py +1 -1
- oscura/reporting/formatting/emphasis.py +2 -0
- oscura/reporting/formatting/numbers.py +0 -2
- oscura/reporting/output.py +1 -3
- oscura/reporting/sections.py +0 -2
- oscura/search/anomaly.py +2 -0
- oscura/session/session.py +91 -16
- 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/testing/synthetic.py +2 -0
- oscura/ui/formatters.py +4 -2
- oscura/utils/buffer.py +2 -2
- oscura/utils/lazy.py +5 -5
- oscura/utils/memory_advanced.py +2 -2
- oscura/utils/memory_extensions.py +2 -2
- oscura/visualization/colors.py +0 -2
- oscura/visualization/power.py +2 -0
- oscura/workflows/multi_trace.py +2 -0
- {oscura-0.1.2.dist-info → oscura-0.4.0.dist-info}/METADATA +122 -20
- {oscura-0.1.2.dist-info → oscura-0.4.0.dist-info}/RECORD +116 -98
- {oscura-0.1.2.dist-info → oscura-0.4.0.dist-info}/WHEEL +0 -0
- {oscura-0.1.2.dist-info → oscura-0.4.0.dist-info}/entry_points.txt +0 -0
- {oscura-0.1.2.dist-info → oscura-0.4.0.dist-info}/licenses/LICENSE +0 -0
oscura/__init__.py
CHANGED
|
@@ -46,7 +46,7 @@ Example:
|
|
|
46
46
|
For more information, see https://github.com/oscura-re/oscura
|
|
47
47
|
"""
|
|
48
48
|
|
|
49
|
-
__version__ = "0.
|
|
49
|
+
__version__ = "0.4.0"
|
|
50
50
|
__author__ = "Oscura Contributors"
|
|
51
51
|
|
|
52
52
|
# Core types
|
|
@@ -157,9 +157,7 @@ from oscura.analyzers.waveform.spectral import (
|
|
|
157
157
|
|
|
158
158
|
# Signal builders (top-level convenience access)
|
|
159
159
|
from oscura.builders import (
|
|
160
|
-
GeneratedSignal,
|
|
161
160
|
SignalBuilder,
|
|
162
|
-
SignalMetadata,
|
|
163
161
|
)
|
|
164
162
|
|
|
165
163
|
# Comparison and limit testing
|
|
@@ -518,8 +516,6 @@ __all__ = [
|
|
|
518
516
|
"FieldSpec",
|
|
519
517
|
"FilterResult",
|
|
520
518
|
"FormatError",
|
|
521
|
-
# Signal builders
|
|
522
|
-
"GeneratedSignal",
|
|
523
519
|
# Signal quality (QUAL-005)
|
|
524
520
|
"Glitch",
|
|
525
521
|
"GoldenReference",
|
|
@@ -571,8 +567,6 @@ __all__ = [
|
|
|
571
567
|
"SignalBuilder",
|
|
572
568
|
# Discovery
|
|
573
569
|
"SignalCharacterization",
|
|
574
|
-
# Signal builders
|
|
575
|
-
"SignalMetadata",
|
|
576
570
|
"SmartDefaults",
|
|
577
571
|
# Convenience functions
|
|
578
572
|
"SpectralMetrics",
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""Unified acquisition layer for Oscura.
|
|
2
|
+
|
|
3
|
+
This module provides the Source protocol - a unified interface for acquiring
|
|
4
|
+
signal data from any source (files, hardware, synthetic generation).
|
|
5
|
+
|
|
6
|
+
The Source protocol enables polymorphic data acquisition:
|
|
7
|
+
- FileSource: Load from oscilloscope file formats
|
|
8
|
+
- HardwareSource: Acquire from live hardware (SocketCAN, Saleae, PyVISA)
|
|
9
|
+
- SyntheticSource: Generate synthetic test signals
|
|
10
|
+
|
|
11
|
+
All sources implement the same interface, making them interchangeable:
|
|
12
|
+
|
|
13
|
+
Example:
|
|
14
|
+
>>> from oscura.acquisition import FileSource, HardwareSource, SyntheticSource
|
|
15
|
+
>>> from oscura import SignalBuilder
|
|
16
|
+
>>>
|
|
17
|
+
>>> # All sources use the same interface
|
|
18
|
+
>>> file_src = FileSource("capture.wfm")
|
|
19
|
+
>>> hw_src = HardwareSource.socketcan("can0", bitrate=500000) # Future
|
|
20
|
+
>>> synth_src = SyntheticSource(SignalBuilder().sine(freq=1000))
|
|
21
|
+
>>>
|
|
22
|
+
>>> # Polymorphic consumption
|
|
23
|
+
>>> def analyze_from_source(source: Source):
|
|
24
|
+
... trace = source.read()
|
|
25
|
+
... return analyze(trace)
|
|
26
|
+
>>>
|
|
27
|
+
>>> # Works with any source
|
|
28
|
+
>>> analyze_from_source(file_src)
|
|
29
|
+
>>> analyze_from_source(synth_src)
|
|
30
|
+
|
|
31
|
+
Pattern Decision:
|
|
32
|
+
- Use Source.read() for one-shot acquisition (complete trace)
|
|
33
|
+
- Use Source.stream() for chunked/streaming acquisition (large files or hardware)
|
|
34
|
+
- All sources are context managers (use 'with' for resource cleanup)
|
|
35
|
+
|
|
36
|
+
References:
|
|
37
|
+
Architecture Plan Phase 0.1: Unified Acquisition Layer
|
|
38
|
+
docs/architecture/api-patterns.md: When to use Source vs load()
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
from __future__ import annotations
|
|
42
|
+
|
|
43
|
+
from collections.abc import Iterator
|
|
44
|
+
from typing import TYPE_CHECKING, Protocol, runtime_checkable
|
|
45
|
+
|
|
46
|
+
if TYPE_CHECKING:
|
|
47
|
+
from oscura.core.types import Trace
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@runtime_checkable
|
|
51
|
+
class Source(Protocol):
|
|
52
|
+
"""Unified acquisition interface for all data sources.
|
|
53
|
+
|
|
54
|
+
This protocol defines the contract that all acquisition sources must implement.
|
|
55
|
+
Sources can be files, hardware devices, or synthetic signal generators.
|
|
56
|
+
|
|
57
|
+
Methods:
|
|
58
|
+
read: Read complete trace (one-shot acquisition)
|
|
59
|
+
stream: Stream trace in chunks (for large files or continuous acquisition)
|
|
60
|
+
close: Release resources (e.g., file handles, device connections)
|
|
61
|
+
|
|
62
|
+
Example:
|
|
63
|
+
>>> class CustomSource:
|
|
64
|
+
... def read(self) -> Trace:
|
|
65
|
+
... # Load/acquire complete trace
|
|
66
|
+
... return trace
|
|
67
|
+
...
|
|
68
|
+
... def stream(self, chunk_size: int) -> Iterator[Trace]:
|
|
69
|
+
... # Yield trace chunks
|
|
70
|
+
... while has_data:
|
|
71
|
+
... yield chunk
|
|
72
|
+
...
|
|
73
|
+
... def close(self) -> None:
|
|
74
|
+
... # Clean up resources
|
|
75
|
+
... pass
|
|
76
|
+
...
|
|
77
|
+
... def __enter__(self):
|
|
78
|
+
... return self
|
|
79
|
+
...
|
|
80
|
+
... def __exit__(self, *args):
|
|
81
|
+
... self.close()
|
|
82
|
+
|
|
83
|
+
Protocol Compliance:
|
|
84
|
+
Any class implementing these methods can be used as a Source, even
|
|
85
|
+
without explicit inheritance. This enables duck typing and flexibility.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
def read(self) -> Trace:
|
|
89
|
+
"""Read complete trace (one-shot acquisition).
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
Complete trace from source (WaveformTrace, DigitalTrace, or IQTrace).
|
|
93
|
+
|
|
94
|
+
Raises:
|
|
95
|
+
LoaderError: If acquisition fails.
|
|
96
|
+
FileNotFoundError: If source file doesn't exist (FileSource).
|
|
97
|
+
|
|
98
|
+
Example:
|
|
99
|
+
>>> source = FileSource("capture.wfm")
|
|
100
|
+
>>> trace = source.read()
|
|
101
|
+
>>> print(f"Loaded {len(trace.data)} samples")
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
def stream(self, chunk_size: int) -> Iterator[Trace]:
|
|
105
|
+
"""Stream trace in chunks (for large files or continuous acquisition).
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
chunk_size: Number of samples per chunk.
|
|
109
|
+
|
|
110
|
+
Yields:
|
|
111
|
+
Trace chunks (each chunk is a complete Trace object).
|
|
112
|
+
|
|
113
|
+
Example:
|
|
114
|
+
>>> source = FileSource("huge_capture.wfm")
|
|
115
|
+
>>> for chunk in source.stream(chunk_size=10000):
|
|
116
|
+
... process_chunk(chunk)
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
def close(self) -> None:
|
|
120
|
+
"""Release resources (file handles, device connections, etc.).
|
|
121
|
+
|
|
122
|
+
Called automatically when using source as context manager.
|
|
123
|
+
|
|
124
|
+
Example:
|
|
125
|
+
>>> with FileSource("capture.wfm") as source:
|
|
126
|
+
... trace = source.read()
|
|
127
|
+
... # close() called automatically
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
def __enter__(self) -> Source:
|
|
131
|
+
"""Context manager entry."""
|
|
132
|
+
|
|
133
|
+
def __exit__(self, *args: object) -> None:
|
|
134
|
+
"""Context manager exit."""
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
# Import concrete implementations
|
|
138
|
+
from oscura.acquisition.file import FileSource
|
|
139
|
+
from oscura.acquisition.hardware import HardwareSource
|
|
140
|
+
from oscura.acquisition.synthetic import SyntheticSource
|
|
141
|
+
|
|
142
|
+
__all__ = [
|
|
143
|
+
"FileSource",
|
|
144
|
+
"HardwareSource",
|
|
145
|
+
"Source",
|
|
146
|
+
"SyntheticSource",
|
|
147
|
+
]
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"""File-based signal acquisition.
|
|
2
|
+
|
|
3
|
+
This module provides FileSource, which wraps existing file loaders to implement
|
|
4
|
+
the unified Source protocol. FileSource makes file loading consistent with all
|
|
5
|
+
other acquisition methods (hardware, synthetic).
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> from oscura.acquisition import FileSource
|
|
9
|
+
>>>
|
|
10
|
+
>>> # Basic usage
|
|
11
|
+
>>> source = FileSource("capture.wfm")
|
|
12
|
+
>>> trace = source.read()
|
|
13
|
+
>>>
|
|
14
|
+
>>> # Context manager (recommended)
|
|
15
|
+
>>> with FileSource("capture.wfm") as source:
|
|
16
|
+
... trace = source.read()
|
|
17
|
+
... # Process trace
|
|
18
|
+
... # Automatic cleanup
|
|
19
|
+
>>>
|
|
20
|
+
>>> # Streaming for large files
|
|
21
|
+
>>> with FileSource("huge_capture.wfm") as source:
|
|
22
|
+
... for chunk in source.stream(chunk_size=10000):
|
|
23
|
+
... process_chunk(chunk)
|
|
24
|
+
>>>
|
|
25
|
+
>>> # Format override
|
|
26
|
+
>>> source = FileSource("data.bin", format="tektronix")
|
|
27
|
+
>>> trace = source.read()
|
|
28
|
+
|
|
29
|
+
Pattern:
|
|
30
|
+
FileSource is a thin wrapper around the existing load() function.
|
|
31
|
+
It provides the Source interface for consistency and composition.
|
|
32
|
+
|
|
33
|
+
Backward Compatibility:
|
|
34
|
+
The existing oscura.load() function continues to work unchanged.
|
|
35
|
+
FileSource is the new preferred pattern for explicit acquisition.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
from __future__ import annotations
|
|
39
|
+
|
|
40
|
+
from collections.abc import Iterator
|
|
41
|
+
from pathlib import Path
|
|
42
|
+
from typing import TYPE_CHECKING, Any
|
|
43
|
+
|
|
44
|
+
if TYPE_CHECKING:
|
|
45
|
+
from os import PathLike
|
|
46
|
+
|
|
47
|
+
from oscura.core.types import Trace
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class FileSource:
|
|
51
|
+
"""File-based signal source implementing Source protocol.
|
|
52
|
+
|
|
53
|
+
Wraps existing file loaders to provide unified acquisition interface.
|
|
54
|
+
Supports all file formats that oscura.load() supports:
|
|
55
|
+
- Tektronix WFM
|
|
56
|
+
- Rigol WFM
|
|
57
|
+
- CSV, HDF5, NumPy
|
|
58
|
+
- Sigrok, VCD, PCAP
|
|
59
|
+
- WAV, TDMS, Touchstone
|
|
60
|
+
|
|
61
|
+
Attributes:
|
|
62
|
+
path: Path to the file.
|
|
63
|
+
format: Optional format override (auto-detected if None).
|
|
64
|
+
kwargs: Additional loader arguments.
|
|
65
|
+
|
|
66
|
+
Example:
|
|
67
|
+
>>> # Auto-detect format
|
|
68
|
+
>>> source = FileSource("capture.wfm")
|
|
69
|
+
>>> trace = source.read()
|
|
70
|
+
>>>
|
|
71
|
+
>>> # Override format
|
|
72
|
+
>>> source = FileSource("data.bin", format="tektronix")
|
|
73
|
+
>>> trace = source.read()
|
|
74
|
+
>>>
|
|
75
|
+
>>> # Specify channel for multi-channel files
|
|
76
|
+
>>> source = FileSource("multi.wfm", channel=1)
|
|
77
|
+
>>> trace = source.read()
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
def __init__(
|
|
81
|
+
self,
|
|
82
|
+
path: str | PathLike[str],
|
|
83
|
+
*,
|
|
84
|
+
format: str | None = None,
|
|
85
|
+
**kwargs: Any,
|
|
86
|
+
) -> None:
|
|
87
|
+
"""Initialize file source.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
path: Path to file to load.
|
|
91
|
+
format: Optional format override (e.g., "tektronix", "rigol").
|
|
92
|
+
**kwargs: Additional arguments passed to loader.
|
|
93
|
+
|
|
94
|
+
Example:
|
|
95
|
+
>>> source = FileSource("capture.wfm")
|
|
96
|
+
>>> source = FileSource("data.bin", format="tektronix", channel=1)
|
|
97
|
+
"""
|
|
98
|
+
self.path = Path(path)
|
|
99
|
+
self.format = format
|
|
100
|
+
self.kwargs = kwargs
|
|
101
|
+
self._closed = False
|
|
102
|
+
|
|
103
|
+
def read(self) -> Trace:
|
|
104
|
+
"""Read complete trace from file.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
Complete trace (WaveformTrace, DigitalTrace, or IQTrace).
|
|
108
|
+
|
|
109
|
+
Raises:
|
|
110
|
+
FileNotFoundError: If file doesn't exist.
|
|
111
|
+
UnsupportedFormatError: If file format not recognized.
|
|
112
|
+
LoaderError: If file cannot be loaded.
|
|
113
|
+
|
|
114
|
+
Example:
|
|
115
|
+
>>> source = FileSource("capture.wfm")
|
|
116
|
+
>>> trace = source.read()
|
|
117
|
+
>>> print(f"Loaded {len(trace.data)} samples")
|
|
118
|
+
"""
|
|
119
|
+
if self._closed:
|
|
120
|
+
raise ValueError("Cannot read from closed source")
|
|
121
|
+
|
|
122
|
+
# Import here to avoid circular dependency
|
|
123
|
+
from oscura.loaders import load
|
|
124
|
+
|
|
125
|
+
return load(self.path, format=self.format, **self.kwargs)
|
|
126
|
+
|
|
127
|
+
def stream(self, chunk_size: int) -> Iterator[Trace]:
|
|
128
|
+
"""Stream trace in chunks for large files.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
chunk_size: Number of samples per chunk.
|
|
132
|
+
|
|
133
|
+
Yields:
|
|
134
|
+
Trace chunks.
|
|
135
|
+
|
|
136
|
+
Raises:
|
|
137
|
+
FileNotFoundError: If file doesn't exist.
|
|
138
|
+
LoaderError: If file cannot be loaded.
|
|
139
|
+
|
|
140
|
+
Example:
|
|
141
|
+
>>> source = FileSource("huge_capture.wfm")
|
|
142
|
+
>>> for chunk in source.stream(chunk_size=10000):
|
|
143
|
+
... metrics = analyze(chunk)
|
|
144
|
+
... print(f"Chunk: {metrics}")
|
|
145
|
+
|
|
146
|
+
Note:
|
|
147
|
+
Currently uses load_trace_chunks for chunked loading.
|
|
148
|
+
For formats without native chunking support, loads full file
|
|
149
|
+
and yields slices.
|
|
150
|
+
"""
|
|
151
|
+
if self._closed:
|
|
152
|
+
raise ValueError("Cannot stream from closed source")
|
|
153
|
+
|
|
154
|
+
# Try lazy/chunked loading if available
|
|
155
|
+
try:
|
|
156
|
+
from oscura.streaming import load_trace_chunks
|
|
157
|
+
|
|
158
|
+
# load_trace_chunks expects Path-like and chunk_size
|
|
159
|
+
yield from load_trace_chunks(self.path, chunk_size=chunk_size)
|
|
160
|
+
except ImportError:
|
|
161
|
+
# Fallback: load full trace and yield chunks
|
|
162
|
+
trace = self.read()
|
|
163
|
+
|
|
164
|
+
# Import here to avoid circular dependency
|
|
165
|
+
from oscura.core.types import IQTrace
|
|
166
|
+
|
|
167
|
+
# Get sample count based on trace type
|
|
168
|
+
if isinstance(trace, IQTrace):
|
|
169
|
+
n_samples = len(trace.i_data)
|
|
170
|
+
else:
|
|
171
|
+
n_samples = len(trace.data)
|
|
172
|
+
|
|
173
|
+
for start in range(0, n_samples, chunk_size):
|
|
174
|
+
end = min(start + chunk_size, n_samples)
|
|
175
|
+
# Create chunk trace with sliced data
|
|
176
|
+
if isinstance(trace, IQTrace):
|
|
177
|
+
# IQTrace doesn't have .data attribute
|
|
178
|
+
chunk_data = None # Will be handled separately below
|
|
179
|
+
else:
|
|
180
|
+
chunk_data = trace.data[start:end]
|
|
181
|
+
|
|
182
|
+
# Import here to avoid circular dependency
|
|
183
|
+
from oscura.core.types import (
|
|
184
|
+
DigitalTrace,
|
|
185
|
+
TraceMetadata,
|
|
186
|
+
WaveformTrace,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# Create appropriate trace type
|
|
190
|
+
chunk_metadata = TraceMetadata(
|
|
191
|
+
sample_rate=trace.metadata.sample_rate,
|
|
192
|
+
vertical_scale=trace.metadata.vertical_scale,
|
|
193
|
+
vertical_offset=trace.metadata.vertical_offset,
|
|
194
|
+
acquisition_time=trace.metadata.acquisition_time,
|
|
195
|
+
trigger_info=trace.metadata.trigger_info,
|
|
196
|
+
source_file=str(self.path),
|
|
197
|
+
channel_name=trace.metadata.channel_name,
|
|
198
|
+
calibration_info=trace.metadata.calibration_info,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
if isinstance(trace, WaveformTrace):
|
|
202
|
+
yield WaveformTrace(data=chunk_data, metadata=chunk_metadata) # type: ignore[arg-type]
|
|
203
|
+
elif isinstance(trace, DigitalTrace):
|
|
204
|
+
yield DigitalTrace(
|
|
205
|
+
data=chunk_data, # type: ignore[arg-type]
|
|
206
|
+
metadata=chunk_metadata,
|
|
207
|
+
)
|
|
208
|
+
elif isinstance(trace, IQTrace):
|
|
209
|
+
# Handle I/Q separately
|
|
210
|
+
chunk_i = trace.i_data[start:end]
|
|
211
|
+
chunk_q = trace.q_data[start:end]
|
|
212
|
+
yield IQTrace(
|
|
213
|
+
i_data=chunk_i,
|
|
214
|
+
q_data=chunk_q,
|
|
215
|
+
metadata=chunk_metadata,
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
def close(self) -> None:
|
|
219
|
+
"""Close the source and release resources.
|
|
220
|
+
|
|
221
|
+
For file sources, this is mostly a no-op since Python handles
|
|
222
|
+
file cleanup. Included for protocol compliance.
|
|
223
|
+
|
|
224
|
+
Example:
|
|
225
|
+
>>> source = FileSource("capture.wfm")
|
|
226
|
+
>>> trace = source.read()
|
|
227
|
+
>>> source.close()
|
|
228
|
+
"""
|
|
229
|
+
self._closed = True
|
|
230
|
+
|
|
231
|
+
def __enter__(self) -> FileSource:
|
|
232
|
+
"""Context manager entry.
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
Self for use in 'with' statement.
|
|
236
|
+
|
|
237
|
+
Example:
|
|
238
|
+
>>> with FileSource("capture.wfm") as source:
|
|
239
|
+
... trace = source.read()
|
|
240
|
+
"""
|
|
241
|
+
return self
|
|
242
|
+
|
|
243
|
+
def __exit__(self, *args: object) -> None:
|
|
244
|
+
"""Context manager exit.
|
|
245
|
+
|
|
246
|
+
Automatically calls close() when exiting 'with' block.
|
|
247
|
+
"""
|
|
248
|
+
self.close()
|
|
249
|
+
|
|
250
|
+
def __repr__(self) -> str:
|
|
251
|
+
"""String representation."""
|
|
252
|
+
return f"FileSource({self.path!r}, format={self.format!r})"
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
__all__ = ["FileSource"]
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""Hardware-based signal acquisition sources.
|
|
2
|
+
|
|
3
|
+
This module provides the base HardwareSource class and factory methods for
|
|
4
|
+
creating hardware-based acquisition sources. All hardware sources implement
|
|
5
|
+
the unified Source protocol, making them interchangeable with FileSource and
|
|
6
|
+
SyntheticSource.
|
|
7
|
+
|
|
8
|
+
Supported Hardware:
|
|
9
|
+
- SocketCAN: Linux CAN bus interface (requires python-can)
|
|
10
|
+
- Saleae Logic: Logic analyzer (requires saleae library)
|
|
11
|
+
- PyVISA: Oscilloscopes and instruments (requires pyvisa)
|
|
12
|
+
|
|
13
|
+
Example:
|
|
14
|
+
>>> from oscura.acquisition import HardwareSource
|
|
15
|
+
>>>
|
|
16
|
+
>>> # SocketCAN source
|
|
17
|
+
>>> with HardwareSource.socketcan("can0", bitrate=500000) as source:
|
|
18
|
+
... trace = source.read()
|
|
19
|
+
>>>
|
|
20
|
+
>>> # Saleae Logic source
|
|
21
|
+
>>> with HardwareSource.saleae() as source:
|
|
22
|
+
... source.configure(sample_rate=1e6, duration=10)
|
|
23
|
+
... trace = source.read()
|
|
24
|
+
>>>
|
|
25
|
+
>>> # PyVISA oscilloscope
|
|
26
|
+
>>> with HardwareSource.visa("USB0::0x0699::0x0401::INSTR") as scope:
|
|
27
|
+
... scope.configure(channels=[1, 2], timebase=1e-6)
|
|
28
|
+
... trace = scope.read()
|
|
29
|
+
|
|
30
|
+
Pattern:
|
|
31
|
+
Each hardware type has its own module (socketcan.py, saleae.py, visa.py)
|
|
32
|
+
containing implementation classes. HardwareSource provides factory methods
|
|
33
|
+
for convenient creation.
|
|
34
|
+
|
|
35
|
+
Dependencies:
|
|
36
|
+
Hardware sources require optional dependencies. Install with:
|
|
37
|
+
- SocketCAN: pip install oscura[automotive] (includes python-can)
|
|
38
|
+
- Saleae: pip install saleae
|
|
39
|
+
- PyVISA: pip install pyvisa pyvisa-py
|
|
40
|
+
|
|
41
|
+
References:
|
|
42
|
+
Architecture Plan Phase 2: Hardware Integration
|
|
43
|
+
docs/architecture/api-patterns.md: Source Protocol
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
from __future__ import annotations
|
|
47
|
+
|
|
48
|
+
from typing import TYPE_CHECKING, Any
|
|
49
|
+
|
|
50
|
+
if TYPE_CHECKING:
|
|
51
|
+
from oscura.acquisition.saleae import SaleaeSource
|
|
52
|
+
from oscura.acquisition.socketcan import SocketCANSource
|
|
53
|
+
from oscura.acquisition.visa import VISASource
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class HardwareSource:
|
|
57
|
+
"""Factory for creating hardware acquisition sources.
|
|
58
|
+
|
|
59
|
+
This class provides static methods for creating hardware-based acquisition
|
|
60
|
+
sources. Each method returns a specific hardware source implementation that
|
|
61
|
+
follows the Source protocol.
|
|
62
|
+
|
|
63
|
+
Methods:
|
|
64
|
+
socketcan: Create Linux SocketCAN interface source
|
|
65
|
+
saleae: Create Saleae Logic analyzer source
|
|
66
|
+
visa: Create PyVISA instrument source
|
|
67
|
+
|
|
68
|
+
Example:
|
|
69
|
+
>>> # Create SocketCAN source
|
|
70
|
+
>>> can = HardwareSource.socketcan("can0", bitrate=500000)
|
|
71
|
+
>>> trace = can.read()
|
|
72
|
+
>>>
|
|
73
|
+
>>> # Create Saleae source
|
|
74
|
+
>>> logic = HardwareSource.saleae()
|
|
75
|
+
>>> logic.configure(sample_rate=1e6, duration=10)
|
|
76
|
+
>>> trace = logic.read()
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
@staticmethod
|
|
80
|
+
def socketcan(interface: str, *, bitrate: int = 500000, **kwargs: Any) -> SocketCANSource:
|
|
81
|
+
"""Create SocketCAN hardware source for Linux CAN interfaces.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
interface: SocketCAN interface name (e.g., "can0", "vcan0").
|
|
85
|
+
bitrate: CAN bitrate in bps (default: 500000).
|
|
86
|
+
**kwargs: Additional arguments passed to python-can Bus.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
SocketCANSource instance ready for acquisition.
|
|
90
|
+
|
|
91
|
+
Raises:
|
|
92
|
+
ImportError: If python-can is not installed.
|
|
93
|
+
OSError: If interface doesn't exist or permissions denied.
|
|
94
|
+
|
|
95
|
+
Example:
|
|
96
|
+
>>> # Physical CAN interface
|
|
97
|
+
>>> can = HardwareSource.socketcan("can0", bitrate=500000)
|
|
98
|
+
>>> trace = can.read()
|
|
99
|
+
>>>
|
|
100
|
+
>>> # Virtual CAN for testing
|
|
101
|
+
>>> vcan = HardwareSource.socketcan("vcan0")
|
|
102
|
+
>>> with vcan:
|
|
103
|
+
... for chunk in vcan.stream(duration=60):
|
|
104
|
+
... process(chunk)
|
|
105
|
+
|
|
106
|
+
Note:
|
|
107
|
+
Requires python-can library: pip install oscura[automotive]
|
|
108
|
+
Linux only - uses SocketCAN kernel module.
|
|
109
|
+
"""
|
|
110
|
+
from oscura.acquisition.socketcan import SocketCANSource
|
|
111
|
+
|
|
112
|
+
return SocketCANSource(interface=interface, bitrate=bitrate, **kwargs)
|
|
113
|
+
|
|
114
|
+
@staticmethod
|
|
115
|
+
def saleae(device_id: str | None = None, **kwargs: Any) -> SaleaeSource:
|
|
116
|
+
"""Create Saleae Logic analyzer source.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
device_id: Saleae device ID (optional, auto-detects if None).
|
|
120
|
+
**kwargs: Additional configuration options.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
SaleaeSource instance ready for acquisition.
|
|
124
|
+
|
|
125
|
+
Raises:
|
|
126
|
+
ImportError: If saleae library is not installed.
|
|
127
|
+
RuntimeError: If no Saleae device found.
|
|
128
|
+
|
|
129
|
+
Example:
|
|
130
|
+
>>> # Auto-detect device
|
|
131
|
+
>>> logic = HardwareSource.saleae()
|
|
132
|
+
>>> logic.configure(sample_rate=1e6, duration=10)
|
|
133
|
+
>>> trace = logic.read()
|
|
134
|
+
>>>
|
|
135
|
+
>>> # Specify device
|
|
136
|
+
>>> logic = HardwareSource.saleae(device_id="ABC123")
|
|
137
|
+
>>> logic.configure(digital_channels=[0, 1, 2, 3])
|
|
138
|
+
>>> with logic:
|
|
139
|
+
... trace = logic.read()
|
|
140
|
+
|
|
141
|
+
Note:
|
|
142
|
+
Requires saleae library: pip install saleae
|
|
143
|
+
Supports Logic 8, Logic Pro 8, Logic Pro 16.
|
|
144
|
+
"""
|
|
145
|
+
from oscura.acquisition.saleae import SaleaeSource
|
|
146
|
+
|
|
147
|
+
return SaleaeSource(device_id=device_id, **kwargs)
|
|
148
|
+
|
|
149
|
+
@staticmethod
|
|
150
|
+
def visa(resource: str | None = None, **kwargs: Any) -> VISASource:
|
|
151
|
+
"""Create PyVISA instrument source (oscilloscopes, etc.).
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
resource: VISA resource string (optional, auto-detects if None).
|
|
155
|
+
Examples: "USB0::0x0699::0x0401::INSTR", "TCPIP::192.168.1.100::INSTR"
|
|
156
|
+
**kwargs: Additional PyVISA configuration options.
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
VISASource instance ready for acquisition.
|
|
160
|
+
|
|
161
|
+
Raises:
|
|
162
|
+
ImportError: If pyvisa is not installed.
|
|
163
|
+
RuntimeError: If no VISA resource found.
|
|
164
|
+
|
|
165
|
+
Example:
|
|
166
|
+
>>> # Auto-detect instrument
|
|
167
|
+
>>> scope = HardwareSource.visa()
|
|
168
|
+
>>> scope.configure(channels=[1, 2], timebase=1e-6)
|
|
169
|
+
>>> trace = scope.read()
|
|
170
|
+
>>>
|
|
171
|
+
>>> # Specific instrument
|
|
172
|
+
>>> scope = HardwareSource.visa("USB0::0x0699::0x0401::INSTR")
|
|
173
|
+
>>> scope.configure(channels=[1], vertical_scale=0.5)
|
|
174
|
+
>>> with scope:
|
|
175
|
+
... trace = scope.read()
|
|
176
|
+
|
|
177
|
+
Note:
|
|
178
|
+
Requires pyvisa and pyvisa-py: pip install pyvisa pyvisa-py
|
|
179
|
+
Supports Tektronix, Keysight, Rigol, and other SCPI oscilloscopes.
|
|
180
|
+
"""
|
|
181
|
+
from oscura.acquisition.visa import VISASource
|
|
182
|
+
|
|
183
|
+
return VISASource(resource=resource, **kwargs)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
__all__ = ["HardwareSource"]
|