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
|
@@ -5,7 +5,7 @@ composition of signals for test data generation, demos, and protocol testing.
|
|
|
5
5
|
|
|
6
6
|
Example:
|
|
7
7
|
>>> from oscura import SignalBuilder
|
|
8
|
-
>>>
|
|
8
|
+
>>> trace = (SignalBuilder()
|
|
9
9
|
... .sample_rate(10e6)
|
|
10
10
|
... .duration(0.01)
|
|
11
11
|
... .add_sine(frequency=1000, amplitude=1.0)
|
|
@@ -16,12 +16,11 @@ The builder supports:
|
|
|
16
16
|
- Analog waveforms (sine, square, triangle, chirp, multitone)
|
|
17
17
|
- Protocol signals (UART, SPI, I2C, CAN)
|
|
18
18
|
- Noise and impairments (gaussian, pink, jitter, quantization)
|
|
19
|
-
- Multi-channel signals
|
|
19
|
+
- Multi-channel signals (use build_channels() to get all channels)
|
|
20
20
|
"""
|
|
21
21
|
|
|
22
22
|
from __future__ import annotations
|
|
23
23
|
|
|
24
|
-
from dataclasses import dataclass, field
|
|
25
24
|
from pathlib import Path
|
|
26
25
|
from typing import Any, Literal
|
|
27
26
|
|
|
@@ -31,167 +30,6 @@ from scipy import signal as scipy_signal
|
|
|
31
30
|
from oscura.core.types import TraceMetadata, WaveformTrace
|
|
32
31
|
|
|
33
32
|
|
|
34
|
-
@dataclass
|
|
35
|
-
class SignalMetadata:
|
|
36
|
-
"""Metadata for generated signals.
|
|
37
|
-
|
|
38
|
-
Attributes:
|
|
39
|
-
sample_rate: Sample rate in Hz.
|
|
40
|
-
duration: Signal duration in seconds.
|
|
41
|
-
channel_names: List of channel names.
|
|
42
|
-
description: Human-readable description.
|
|
43
|
-
generator: Name of generator that created this signal.
|
|
44
|
-
parameters: Dictionary of generation parameters.
|
|
45
|
-
"""
|
|
46
|
-
|
|
47
|
-
sample_rate: float
|
|
48
|
-
duration: float
|
|
49
|
-
channel_names: list[str] = field(default_factory=lambda: ["ch1"])
|
|
50
|
-
description: str = ""
|
|
51
|
-
generator: str = "SignalBuilder"
|
|
52
|
-
parameters: dict[str, Any] = field(default_factory=dict)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
@dataclass
|
|
56
|
-
class GeneratedSignal:
|
|
57
|
-
"""Container for generated signal data.
|
|
58
|
-
|
|
59
|
-
Attributes:
|
|
60
|
-
data: Dictionary mapping channel names to signal arrays.
|
|
61
|
-
metadata: Signal metadata.
|
|
62
|
-
"""
|
|
63
|
-
|
|
64
|
-
data: dict[str, np.ndarray[Any, np.dtype[np.float64]]]
|
|
65
|
-
metadata: SignalMetadata
|
|
66
|
-
_time: np.ndarray[Any, np.dtype[np.float64]] | None = field(default=None, repr=False)
|
|
67
|
-
|
|
68
|
-
@property
|
|
69
|
-
def time(self) -> np.ndarray[Any, np.dtype[np.float64]]:
|
|
70
|
-
"""Get time array, computing if necessary."""
|
|
71
|
-
if self._time is None:
|
|
72
|
-
n_samples = len(next(iter(self.data.values())))
|
|
73
|
-
self._time = np.arange(n_samples) / self.metadata.sample_rate
|
|
74
|
-
return self._time
|
|
75
|
-
|
|
76
|
-
@property
|
|
77
|
-
def num_channels(self) -> int:
|
|
78
|
-
"""Number of channels in signal."""
|
|
79
|
-
return len(self.data)
|
|
80
|
-
|
|
81
|
-
@property
|
|
82
|
-
def num_samples(self) -> int:
|
|
83
|
-
"""Number of samples per channel."""
|
|
84
|
-
return len(next(iter(self.data.values())))
|
|
85
|
-
|
|
86
|
-
def get_channel(self, name: str) -> np.ndarray[Any, np.dtype[np.float64]]:
|
|
87
|
-
"""Get signal data for a specific channel.
|
|
88
|
-
|
|
89
|
-
Args:
|
|
90
|
-
name: Channel name.
|
|
91
|
-
|
|
92
|
-
Returns:
|
|
93
|
-
Signal array for the channel.
|
|
94
|
-
|
|
95
|
-
Raises:
|
|
96
|
-
KeyError: If channel name not found.
|
|
97
|
-
"""
|
|
98
|
-
if name not in self.data:
|
|
99
|
-
available = list(self.data.keys())
|
|
100
|
-
raise KeyError(f"Channel '{name}' not found. Available: {available}")
|
|
101
|
-
return self.data[name]
|
|
102
|
-
|
|
103
|
-
def to_trace(self, channel: str | None = None) -> WaveformTrace:
|
|
104
|
-
"""Convert to WaveformTrace for Oscura analysis.
|
|
105
|
-
|
|
106
|
-
Args:
|
|
107
|
-
channel: Channel name to convert. If None, uses first channel.
|
|
108
|
-
|
|
109
|
-
Returns:
|
|
110
|
-
WaveformTrace instance ready for analysis.
|
|
111
|
-
"""
|
|
112
|
-
if channel is None:
|
|
113
|
-
channel = self.metadata.channel_names[0]
|
|
114
|
-
|
|
115
|
-
data = self.get_channel(channel)
|
|
116
|
-
trace_meta = TraceMetadata(
|
|
117
|
-
sample_rate=self.metadata.sample_rate,
|
|
118
|
-
channel_name=channel,
|
|
119
|
-
)
|
|
120
|
-
return WaveformTrace(data=data, metadata=trace_meta)
|
|
121
|
-
|
|
122
|
-
def save_npz(self, path: Path | str) -> None:
|
|
123
|
-
"""Save signal to NPZ format.
|
|
124
|
-
|
|
125
|
-
Args:
|
|
126
|
-
path: Output file path.
|
|
127
|
-
"""
|
|
128
|
-
path = Path(path)
|
|
129
|
-
path.parent.mkdir(parents=True, exist_ok=True)
|
|
130
|
-
|
|
131
|
-
save_dict: dict[str, Any] = {
|
|
132
|
-
"sample_rate": self.metadata.sample_rate,
|
|
133
|
-
"duration": self.metadata.duration,
|
|
134
|
-
"channel_names": np.array(self.metadata.channel_names),
|
|
135
|
-
"description": self.metadata.description,
|
|
136
|
-
"generator": self.metadata.generator,
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
# Add channel data
|
|
140
|
-
for name, data in self.data.items():
|
|
141
|
-
save_dict[name] = data
|
|
142
|
-
|
|
143
|
-
# Add parameters as JSON-serializable
|
|
144
|
-
for key, value in self.metadata.parameters.items():
|
|
145
|
-
if isinstance(value, (int, float, str, bool)):
|
|
146
|
-
save_dict[f"param_{key}"] = value
|
|
147
|
-
|
|
148
|
-
np.savez_compressed(path, **save_dict)
|
|
149
|
-
|
|
150
|
-
@classmethod
|
|
151
|
-
def load_npz(cls, path: Path | str) -> GeneratedSignal:
|
|
152
|
-
"""Load signal from NPZ format.
|
|
153
|
-
|
|
154
|
-
Args:
|
|
155
|
-
path: Input file path.
|
|
156
|
-
|
|
157
|
-
Returns:
|
|
158
|
-
GeneratedSignal instance.
|
|
159
|
-
"""
|
|
160
|
-
path = Path(path)
|
|
161
|
-
loaded = np.load(path, allow_pickle=True)
|
|
162
|
-
|
|
163
|
-
sample_rate = float(loaded["sample_rate"])
|
|
164
|
-
duration = float(loaded["duration"])
|
|
165
|
-
channel_names = list(loaded.get("channel_names", ["ch1"]))
|
|
166
|
-
description = str(loaded.get("description", ""))
|
|
167
|
-
generator = str(loaded.get("generator", "unknown"))
|
|
168
|
-
|
|
169
|
-
# Extract channel data
|
|
170
|
-
data = {}
|
|
171
|
-
for name in channel_names:
|
|
172
|
-
if name in loaded:
|
|
173
|
-
data[name] = loaded[name]
|
|
174
|
-
|
|
175
|
-
# Extract parameters
|
|
176
|
-
parameters: dict[str, Any] = {}
|
|
177
|
-
for key in loaded.files:
|
|
178
|
-
if key.startswith("param_"):
|
|
179
|
-
param_name = key[6:] # Remove "param_" prefix
|
|
180
|
-
value = loaded[key]
|
|
181
|
-
parameters[param_name] = value.item() if value.ndim == 0 else value
|
|
182
|
-
|
|
183
|
-
metadata = SignalMetadata(
|
|
184
|
-
sample_rate=sample_rate,
|
|
185
|
-
duration=duration,
|
|
186
|
-
channel_names=channel_names,
|
|
187
|
-
description=description,
|
|
188
|
-
generator=generator,
|
|
189
|
-
parameters=parameters,
|
|
190
|
-
)
|
|
191
|
-
|
|
192
|
-
return cls(data=data, metadata=metadata)
|
|
193
|
-
|
|
194
|
-
|
|
195
33
|
class SignalBuilder:
|
|
196
34
|
"""Fluent builder for composable signal generation.
|
|
197
35
|
|
|
@@ -200,7 +38,7 @@ class SignalBuilder:
|
|
|
200
38
|
|
|
201
39
|
Example:
|
|
202
40
|
>>> # Simple sine wave with noise
|
|
203
|
-
>>>
|
|
41
|
+
>>> trace = (SignalBuilder()
|
|
204
42
|
... .sample_rate(1e6)
|
|
205
43
|
... .duration(0.01)
|
|
206
44
|
... .add_sine(frequency=1000, amplitude=1.0)
|
|
@@ -208,7 +46,7 @@ class SignalBuilder:
|
|
|
208
46
|
... .build())
|
|
209
47
|
>>>
|
|
210
48
|
>>> # UART signal with realistic characteristics
|
|
211
|
-
>>>
|
|
49
|
+
>>> uart_trace = (SignalBuilder()
|
|
212
50
|
... .sample_rate(10e6)
|
|
213
51
|
... .add_uart(baud_rate=115200, data=b"Hello Oscura!", config="8N1")
|
|
214
52
|
... .add_noise(snr_db=30)
|
|
@@ -1050,14 +888,34 @@ class SignalBuilder:
|
|
|
1050
888
|
|
|
1051
889
|
# ========== Build Methods ==========
|
|
1052
890
|
|
|
1053
|
-
def build(self) ->
|
|
1054
|
-
"""Build and return signal.
|
|
891
|
+
def build(self, channel: str | None = None) -> WaveformTrace:
|
|
892
|
+
"""Build and return signal as WaveformTrace.
|
|
893
|
+
|
|
894
|
+
This is the primary build method that returns a WaveformTrace ready
|
|
895
|
+
for analysis with Oscura. For multi-channel signals, specify which
|
|
896
|
+
channel to return, or use build_channels() to get all channels.
|
|
897
|
+
|
|
898
|
+
Args:
|
|
899
|
+
channel: Channel name to build. If None, uses first channel.
|
|
900
|
+
For single-channel signals, this parameter is optional.
|
|
1055
901
|
|
|
1056
902
|
Returns:
|
|
1057
|
-
|
|
903
|
+
WaveformTrace ready for Oscura analysis.
|
|
1058
904
|
|
|
1059
905
|
Raises:
|
|
1060
|
-
ValueError: If no signals have been added.
|
|
906
|
+
ValueError: If no signals have been added or channel doesn't exist.
|
|
907
|
+
|
|
908
|
+
Example:
|
|
909
|
+
>>> builder = SignalBuilder(sample_rate=1e6).add_sine(1000)
|
|
910
|
+
>>> trace = builder.build()
|
|
911
|
+
>>> print(f"Generated {len(trace.data)} samples")
|
|
912
|
+
|
|
913
|
+
>>> # Multi-channel
|
|
914
|
+
>>> builder = SignalBuilder(sample_rate=1e6)
|
|
915
|
+
>>> builder.add_sine(1000, channel="sig")
|
|
916
|
+
>>> builder.add_square(500, channel="clk")
|
|
917
|
+
>>> sig_trace = builder.build(channel="sig")
|
|
918
|
+
>>> clk_trace = builder.build(channel="clk")
|
|
1061
919
|
"""
|
|
1062
920
|
if not self._channels:
|
|
1063
921
|
raise ValueError("No signals added. Call add_* methods before build().")
|
|
@@ -1068,44 +926,94 @@ class SignalBuilder:
|
|
|
1068
926
|
if len(signal) < max_len:
|
|
1069
927
|
self._channels[name] = np.pad(signal, (0, max_len - len(signal)), mode="edge")
|
|
1070
928
|
|
|
1071
|
-
#
|
|
1072
|
-
|
|
929
|
+
# Determine which channel to return
|
|
930
|
+
if channel is None:
|
|
931
|
+
# Use first channel
|
|
932
|
+
channel = next(iter(self._channels.keys()))
|
|
933
|
+
elif channel not in self._channels:
|
|
934
|
+
available = list(self._channels.keys())
|
|
935
|
+
raise ValueError(f"Channel '{channel}' not found. Available: {available}")
|
|
936
|
+
|
|
937
|
+
# Get channel data
|
|
938
|
+
data = self._channels[channel]
|
|
1073
939
|
|
|
1074
|
-
|
|
940
|
+
# Build TraceMetadata
|
|
941
|
+
trace_metadata = TraceMetadata(
|
|
1075
942
|
sample_rate=self._sample_rate,
|
|
1076
|
-
|
|
1077
|
-
channel_names=list(self._channels.keys()),
|
|
1078
|
-
description=self._description,
|
|
1079
|
-
generator="SignalBuilder",
|
|
1080
|
-
parameters=self._parameters,
|
|
943
|
+
channel_name=channel,
|
|
1081
944
|
)
|
|
1082
945
|
|
|
1083
|
-
return
|
|
946
|
+
return WaveformTrace(data=data, metadata=trace_metadata)
|
|
1084
947
|
|
|
1085
|
-
def
|
|
1086
|
-
"""Build and return
|
|
948
|
+
def build_channels(self) -> dict[str, WaveformTrace]:
|
|
949
|
+
"""Build and return all channels as dictionary of WaveformTraces.
|
|
1087
950
|
|
|
1088
|
-
|
|
1089
|
-
|
|
951
|
+
Use this method when you need all channels from a multi-channel
|
|
952
|
+
signal generation.
|
|
1090
953
|
|
|
1091
954
|
Returns:
|
|
1092
|
-
|
|
955
|
+
Dictionary mapping channel names to WaveformTrace objects.
|
|
956
|
+
|
|
957
|
+
Raises:
|
|
958
|
+
ValueError: If no signals have been added.
|
|
959
|
+
|
|
960
|
+
Example:
|
|
961
|
+
>>> builder = SignalBuilder(sample_rate=1e6)
|
|
962
|
+
>>> builder.add_sine(1000, channel="sig")
|
|
963
|
+
>>> builder.add_square(500, channel="clk")
|
|
964
|
+
>>> channels = builder.build_channels()
|
|
965
|
+
>>> print(f"Generated {len(channels)} channels")
|
|
966
|
+
>>> sig_trace = channels["sig"]
|
|
967
|
+
>>> clk_trace = channels["clk"]
|
|
1093
968
|
"""
|
|
1094
|
-
|
|
1095
|
-
|
|
969
|
+
if not self._channels:
|
|
970
|
+
raise ValueError("No signals added. Call add_* methods before build().")
|
|
971
|
+
|
|
972
|
+
# Ensure all channels have same length (pad if necessary)
|
|
973
|
+
max_len = max(len(s) for s in self._channels.values())
|
|
974
|
+
for name, signal in self._channels.items():
|
|
975
|
+
if len(signal) < max_len:
|
|
976
|
+
self._channels[name] = np.pad(signal, (0, max_len - len(signal)), mode="edge")
|
|
977
|
+
|
|
978
|
+
# Build WaveformTrace for each channel
|
|
979
|
+
traces: dict[str, WaveformTrace] = {}
|
|
980
|
+
for channel_name, data in self._channels.items():
|
|
981
|
+
trace_metadata = TraceMetadata(
|
|
982
|
+
sample_rate=self._sample_rate,
|
|
983
|
+
channel_name=channel_name,
|
|
984
|
+
)
|
|
985
|
+
traces[channel_name] = WaveformTrace(data=data, metadata=trace_metadata)
|
|
986
|
+
|
|
987
|
+
return traces
|
|
1096
988
|
|
|
1097
|
-
def save_npz(self, path: Path | str) ->
|
|
989
|
+
def save_npz(self, path: Path | str, channel: str | None = None) -> WaveformTrace:
|
|
1098
990
|
"""Build and save signal to NPZ format.
|
|
1099
991
|
|
|
1100
992
|
Args:
|
|
1101
993
|
path: Output file path.
|
|
994
|
+
channel: Channel to save (for multi-channel signals).
|
|
1102
995
|
|
|
1103
996
|
Returns:
|
|
1104
|
-
|
|
997
|
+
WaveformTrace that was saved.
|
|
998
|
+
|
|
999
|
+
Example:
|
|
1000
|
+
>>> builder = SignalBuilder().sample_rate(1e6).add_sine(1000)
|
|
1001
|
+
>>> trace = builder.save_npz("signal.npz")
|
|
1105
1002
|
"""
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1003
|
+
trace = self.build(channel=channel)
|
|
1004
|
+
|
|
1005
|
+
# Save as NPZ using numpy
|
|
1006
|
+
path = Path(path)
|
|
1007
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
1008
|
+
|
|
1009
|
+
np.savez_compressed(
|
|
1010
|
+
path,
|
|
1011
|
+
data=trace.data,
|
|
1012
|
+
sample_rate=trace.metadata.sample_rate,
|
|
1013
|
+
channel_name=trace.metadata.channel_name or "ch1",
|
|
1014
|
+
)
|
|
1015
|
+
|
|
1016
|
+
return trace
|
|
1109
1017
|
|
|
1110
1018
|
# ========== Internal Methods ==========
|
|
1111
1019
|
|
oscura/core/exceptions.py
CHANGED
|
@@ -283,6 +283,7 @@ class InsufficientDataError(AnalysisError):
|
|
|
283
283
|
required: int | None = None,
|
|
284
284
|
available: int | None = None,
|
|
285
285
|
analysis_type: str | None = None,
|
|
286
|
+
fix_hint: str | None = None,
|
|
286
287
|
) -> None:
|
|
287
288
|
"""Initialize InsufficientDataError.
|
|
288
289
|
|
|
@@ -291,6 +292,7 @@ class InsufficientDataError(AnalysisError):
|
|
|
291
292
|
required: Minimum number of samples or features required.
|
|
292
293
|
available: Actual number available.
|
|
293
294
|
analysis_type: Type of analysis that failed.
|
|
295
|
+
fix_hint: Optional custom fix suggestion (overrides default).
|
|
294
296
|
"""
|
|
295
297
|
self.required = required
|
|
296
298
|
self.available = available
|
|
@@ -301,7 +303,9 @@ class InsufficientDataError(AnalysisError):
|
|
|
301
303
|
elif required is not None:
|
|
302
304
|
details = f"Minimum required: {required}"
|
|
303
305
|
|
|
304
|
-
|
|
306
|
+
# Use default fix hint if not provided
|
|
307
|
+
if fix_hint is None:
|
|
308
|
+
fix_hint = "Acquire more data or reduce analysis window."
|
|
305
309
|
|
|
306
310
|
super().__init__(
|
|
307
311
|
message,
|
oscura/export/__init__.py
CHANGED
|
@@ -19,7 +19,19 @@ Example:
|
|
|
19
19
|
|
|
20
20
|
# Import main exports
|
|
21
21
|
from . import wireshark
|
|
22
|
+
from .wavedrom import (
|
|
23
|
+
WaveDromBuilder,
|
|
24
|
+
WaveDromEdge,
|
|
25
|
+
WaveDromSignal,
|
|
26
|
+
export_wavedrom,
|
|
27
|
+
from_digital_trace,
|
|
28
|
+
)
|
|
22
29
|
|
|
23
30
|
__all__ = [
|
|
31
|
+
"WaveDromBuilder",
|
|
32
|
+
"WaveDromEdge",
|
|
33
|
+
"WaveDromSignal",
|
|
34
|
+
"export_wavedrom",
|
|
35
|
+
"from_digital_trace",
|
|
24
36
|
"wireshark",
|
|
25
37
|
]
|