oscura 0.8.0__py3-none-any.whl → 0.11.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/__main__.py +4 -0
- 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/ml/signal_classifier.py +6 -0
- oscura/analyzers/patterns/__init__.py +66 -0
- 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/basic.py +10 -7
- oscura/analyzers/validation.py +1 -1
- oscura/analyzers/waveform/measurements.py +200 -156
- oscura/analyzers/waveform/measurements_with_uncertainty.py +91 -35
- oscura/analyzers/waveform/spectral.py +182 -84
- 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 +17 -102
- oscura/automotive/flexray/fibex.py +9 -1
- 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/measurement_result.py +286 -0
- oscura/core/progress.py +1 -1
- oscura/core/schemas/device_mapping.json +2 -8
- oscura/core/schemas/packet_format.json +4 -24
- oscura/core/schemas/protocol_definition.json +2 -12
- oscura/core/types.py +232 -239
- 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/validation.py +17 -10
- 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/formatting/measurements.py +55 -14
- oscura/reporting/templates/enhanced/protocol_re.html +504 -503
- oscura/sessions/legacy.py +49 -1
- 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 +46 -284
- oscura/visualization/batch.py +72 -433
- oscura/visualization/plot.py +542 -53
- oscura/visualization/styles.py +184 -318
- oscura/workflows/batch/advanced.py +1 -1
- oscura/workflows/batch/aggregate.py +12 -9
- oscura/workflows/complete_re.py +251 -23
- oscura/workflows/digital.py +27 -4
- oscura/workflows/multi_trace.py +136 -17
- oscura/workflows/waveform.py +11 -6
- oscura-0.11.0.dist-info/METADATA +460 -0
- {oscura-0.8.0.dist-info → oscura-0.11.0.dist-info}/RECORD +120 -145
- 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.8.0.dist-info/METADATA +0 -661
- {oscura-0.8.0.dist-info → oscura-0.11.0.dist-info}/WHEEL +0 -0
- {oscura-0.8.0.dist-info → oscura-0.11.0.dist-info}/entry_points.txt +0 -0
- {oscura-0.8.0.dist-info → oscura-0.11.0.dist-info}/licenses/LICENSE +0 -0
oscura/visualization/digital.py
DELETED
|
@@ -1,436 +0,0 @@
|
|
|
1
|
-
"""Digital timing diagram visualization.
|
|
2
|
-
|
|
3
|
-
This module provides timing diagrams for digital signals with
|
|
4
|
-
protocol decode overlay support.
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
Example:
|
|
8
|
-
>>> from oscura.visualization.digital import plot_timing
|
|
9
|
-
>>> fig = plot_timing([clk, data, cs], names=["CLK", "DATA", "CS"])
|
|
10
|
-
>>> plt.show()
|
|
11
|
-
|
|
12
|
-
References:
|
|
13
|
-
matplotlib best practices for digital waveform visualization
|
|
14
|
-
"""
|
|
15
|
-
|
|
16
|
-
from __future__ import annotations
|
|
17
|
-
|
|
18
|
-
from typing import TYPE_CHECKING
|
|
19
|
-
|
|
20
|
-
import numpy as np
|
|
21
|
-
|
|
22
|
-
try:
|
|
23
|
-
import matplotlib.pyplot as plt
|
|
24
|
-
from matplotlib.patches import Rectangle
|
|
25
|
-
|
|
26
|
-
HAS_MATPLOTLIB = True
|
|
27
|
-
except ImportError:
|
|
28
|
-
HAS_MATPLOTLIB = False
|
|
29
|
-
|
|
30
|
-
from oscura.core.types import DigitalTrace, WaveformTrace
|
|
31
|
-
|
|
32
|
-
if TYPE_CHECKING:
|
|
33
|
-
from collections.abc import Sequence
|
|
34
|
-
|
|
35
|
-
from matplotlib.axes import Axes
|
|
36
|
-
from matplotlib.figure import Figure
|
|
37
|
-
|
|
38
|
-
from oscura.analyzers.protocols.base import Annotation
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def _validate_timing_inputs(
|
|
42
|
-
traces: Sequence[WaveformTrace | DigitalTrace],
|
|
43
|
-
names: list[str] | None,
|
|
44
|
-
) -> tuple[int, list[str]]:
|
|
45
|
-
"""Validate plot_timing inputs and generate default names.
|
|
46
|
-
|
|
47
|
-
Args:
|
|
48
|
-
traces: List of traces to validate.
|
|
49
|
-
names: Channel names or None for defaults.
|
|
50
|
-
|
|
51
|
-
Returns:
|
|
52
|
-
Tuple of (n_channels, validated_names).
|
|
53
|
-
|
|
54
|
-
Raises:
|
|
55
|
-
ValueError: If traces empty or names length mismatch.
|
|
56
|
-
"""
|
|
57
|
-
if len(traces) == 0:
|
|
58
|
-
raise ValueError("traces list cannot be empty")
|
|
59
|
-
|
|
60
|
-
n_channels = len(traces)
|
|
61
|
-
|
|
62
|
-
if names is None:
|
|
63
|
-
names = [f"CH{i + 1}" for i in range(n_channels)]
|
|
64
|
-
|
|
65
|
-
if len(names) != n_channels:
|
|
66
|
-
raise ValueError(f"names length ({len(names)}) must match traces ({n_channels})")
|
|
67
|
-
|
|
68
|
-
return n_channels, names
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
def _convert_to_digital_traces(
|
|
72
|
-
traces: Sequence[WaveformTrace | DigitalTrace],
|
|
73
|
-
threshold: float | str,
|
|
74
|
-
) -> list[DigitalTrace]:
|
|
75
|
-
"""Convert analog traces to digital using threshold.
|
|
76
|
-
|
|
77
|
-
Args:
|
|
78
|
-
traces: List of analog or digital traces.
|
|
79
|
-
threshold: Threshold for analog-to-digital conversion.
|
|
80
|
-
|
|
81
|
-
Returns:
|
|
82
|
-
List of digital traces.
|
|
83
|
-
"""
|
|
84
|
-
from oscura.analyzers.digital.extraction import to_digital
|
|
85
|
-
|
|
86
|
-
digital_traces: list[DigitalTrace] = []
|
|
87
|
-
for trace in traces:
|
|
88
|
-
if isinstance(trace, WaveformTrace):
|
|
89
|
-
digital_traces.append(to_digital(trace, threshold=threshold)) # type: ignore[arg-type]
|
|
90
|
-
else:
|
|
91
|
-
digital_traces.append(trace)
|
|
92
|
-
|
|
93
|
-
return digital_traces
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
def _select_time_unit_and_multiplier(
|
|
97
|
-
digital_traces: list[DigitalTrace],
|
|
98
|
-
time_unit: str,
|
|
99
|
-
) -> tuple[str, float]:
|
|
100
|
-
"""Select appropriate time unit based on signal duration.
|
|
101
|
-
|
|
102
|
-
Args:
|
|
103
|
-
digital_traces: List of digital traces.
|
|
104
|
-
time_unit: Time unit ("auto" or specific unit).
|
|
105
|
-
|
|
106
|
-
Returns:
|
|
107
|
-
Tuple of (time_unit, multiplier).
|
|
108
|
-
"""
|
|
109
|
-
if time_unit == "auto" and len(digital_traces) > 0:
|
|
110
|
-
ref_trace = digital_traces[0]
|
|
111
|
-
duration = len(ref_trace.data) * ref_trace.metadata.time_base
|
|
112
|
-
if duration < 1e-6:
|
|
113
|
-
time_unit = "ns"
|
|
114
|
-
elif duration < 1e-3:
|
|
115
|
-
time_unit = "us"
|
|
116
|
-
elif duration < 1:
|
|
117
|
-
time_unit = "ms"
|
|
118
|
-
else:
|
|
119
|
-
time_unit = "s"
|
|
120
|
-
|
|
121
|
-
time_multipliers = {"s": 1.0, "ms": 1e3, "us": 1e6, "ns": 1e9}
|
|
122
|
-
multiplier = time_multipliers.get(time_unit, 1.0)
|
|
123
|
-
|
|
124
|
-
return time_unit, multiplier
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
def _determine_plot_time_range(
|
|
128
|
-
digital_traces: list[DigitalTrace],
|
|
129
|
-
time_range: tuple[float, float] | None,
|
|
130
|
-
) -> tuple[float, float]:
|
|
131
|
-
"""Determine start and end times for plot.
|
|
132
|
-
|
|
133
|
-
Args:
|
|
134
|
-
digital_traces: List of digital traces.
|
|
135
|
-
time_range: User-specified time range or None for auto.
|
|
136
|
-
|
|
137
|
-
Returns:
|
|
138
|
-
Tuple of (start_time, end_time) in seconds.
|
|
139
|
-
"""
|
|
140
|
-
if time_range is not None:
|
|
141
|
-
return time_range
|
|
142
|
-
|
|
143
|
-
start_time = 0.0
|
|
144
|
-
end_time = max(trace.duration for trace in digital_traces if len(trace.data) > 0)
|
|
145
|
-
return start_time, end_time
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
def _plot_timing_channel(
|
|
149
|
-
ax: Axes,
|
|
150
|
-
trace: DigitalTrace,
|
|
151
|
-
name: str,
|
|
152
|
-
channel_index: int,
|
|
153
|
-
multiplier: float,
|
|
154
|
-
time_range: tuple[float, float] | None,
|
|
155
|
-
show_grid: bool,
|
|
156
|
-
annotations: list[Annotation] | None,
|
|
157
|
-
time_unit: str,
|
|
158
|
-
) -> None:
|
|
159
|
-
"""Plot a single channel in the timing diagram.
|
|
160
|
-
|
|
161
|
-
Args:
|
|
162
|
-
ax: Matplotlib axes to plot on.
|
|
163
|
-
trace: Digital trace to plot.
|
|
164
|
-
name: Channel name for label.
|
|
165
|
-
channel_index: Index for color selection.
|
|
166
|
-
multiplier: Time unit multiplier.
|
|
167
|
-
time_range: Optional time range to display.
|
|
168
|
-
show_grid: Show vertical grid lines.
|
|
169
|
-
annotations: Optional protocol annotations.
|
|
170
|
-
time_unit: Time unit string.
|
|
171
|
-
"""
|
|
172
|
-
time = trace.time_vector * multiplier
|
|
173
|
-
|
|
174
|
-
# Filter to time range
|
|
175
|
-
if time_range is not None:
|
|
176
|
-
start_time, end_time = time_range
|
|
177
|
-
start_idx = int(np.searchsorted(trace.time_vector, start_time))
|
|
178
|
-
end_idx = int(np.searchsorted(trace.time_vector, end_time))
|
|
179
|
-
time = time[start_idx:end_idx]
|
|
180
|
-
data_slice = trace.data[start_idx:end_idx]
|
|
181
|
-
else:
|
|
182
|
-
data_slice = trace.data
|
|
183
|
-
|
|
184
|
-
# Plot digital waveform as step function
|
|
185
|
-
ax.step(
|
|
186
|
-
time,
|
|
187
|
-
data_slice.astype(int),
|
|
188
|
-
where="post",
|
|
189
|
-
color=f"C{channel_index}",
|
|
190
|
-
linewidth=1.5,
|
|
191
|
-
)
|
|
192
|
-
|
|
193
|
-
# Set up digital signal display
|
|
194
|
-
ax.set_ylim(-0.2, 1.2)
|
|
195
|
-
ax.set_yticks([0, 1])
|
|
196
|
-
ax.set_yticklabels(["0", "1"])
|
|
197
|
-
ax.set_ylabel(name, rotation=0, ha="right", va="center", fontweight="bold")
|
|
198
|
-
|
|
199
|
-
if show_grid:
|
|
200
|
-
ax.grid(True, alpha=0.2, axis="x")
|
|
201
|
-
|
|
202
|
-
# Add protocol annotations if provided
|
|
203
|
-
if annotations:
|
|
204
|
-
_add_protocol_annotations(ax, annotations, multiplier, time_unit)
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
def plot_timing(
|
|
208
|
-
traces: Sequence[WaveformTrace | DigitalTrace],
|
|
209
|
-
*,
|
|
210
|
-
names: list[str] | None = None,
|
|
211
|
-
annotations: list[list[Annotation]] | None = None,
|
|
212
|
-
time_unit: str = "auto",
|
|
213
|
-
show_grid: bool = True,
|
|
214
|
-
figsize: tuple[float, float] | None = None,
|
|
215
|
-
title: str | None = None,
|
|
216
|
-
time_range: tuple[float, float] | None = None,
|
|
217
|
-
threshold: float | str = "auto",
|
|
218
|
-
) -> Figure:
|
|
219
|
-
"""Plot digital timing diagram with protocol decode overlay.
|
|
220
|
-
|
|
221
|
-
Creates a stacked timing diagram showing digital waveforms with
|
|
222
|
-
timing information and optional protocol decode annotations.
|
|
223
|
-
|
|
224
|
-
Args:
|
|
225
|
-
traces: List of traces to plot (analog or digital).
|
|
226
|
-
names: Channel names for labels. If None, uses CH1, CH2, etc.
|
|
227
|
-
annotations: List of protocol annotations per channel (optional).
|
|
228
|
-
time_unit: Time unit ("s", "ms", "us", "ns", "auto").
|
|
229
|
-
show_grid: Show vertical grid lines at time intervals.
|
|
230
|
-
figsize: Figure size (width, height) in inches.
|
|
231
|
-
title: Overall figure title.
|
|
232
|
-
time_range: Optional (start, end) time range to display in seconds.
|
|
233
|
-
threshold: Threshold for analog-to-digital conversion ("auto" or float).
|
|
234
|
-
|
|
235
|
-
Returns:
|
|
236
|
-
Matplotlib Figure object.
|
|
237
|
-
|
|
238
|
-
Raises:
|
|
239
|
-
ImportError: If matplotlib is not available.
|
|
240
|
-
ValueError: If traces list is empty.
|
|
241
|
-
|
|
242
|
-
Example:
|
|
243
|
-
>>> fig = plot_timing(
|
|
244
|
-
... [clk_trace, data_trace, cs_trace],
|
|
245
|
-
... names=["CLK", "DATA", "CS"],
|
|
246
|
-
... annotations=[[], uart_annotations, []]
|
|
247
|
-
... )
|
|
248
|
-
>>> plt.savefig("timing.png")
|
|
249
|
-
|
|
250
|
-
References:
|
|
251
|
-
IEEE 181-2011: Standard for Transitional Waveform Definitions
|
|
252
|
-
"""
|
|
253
|
-
if not HAS_MATPLOTLIB:
|
|
254
|
-
raise ImportError("matplotlib is required for visualization")
|
|
255
|
-
|
|
256
|
-
# Data preparation/validation
|
|
257
|
-
n_channels, names = _validate_timing_inputs(traces, names)
|
|
258
|
-
digital_traces = _convert_to_digital_traces(traces, threshold)
|
|
259
|
-
|
|
260
|
-
# Unit/scale selection
|
|
261
|
-
time_unit, multiplier = _select_time_unit_and_multiplier(digital_traces, time_unit)
|
|
262
|
-
start_time, end_time = _determine_plot_time_range(digital_traces, time_range)
|
|
263
|
-
|
|
264
|
-
# Figure/axes creation
|
|
265
|
-
if figsize is None:
|
|
266
|
-
figsize = (12, 1.5 * n_channels)
|
|
267
|
-
|
|
268
|
-
fig, axes = plt.subplots(n_channels, 1, figsize=figsize, sharex=True)
|
|
269
|
-
|
|
270
|
-
if n_channels == 1:
|
|
271
|
-
axes = [axes]
|
|
272
|
-
|
|
273
|
-
# Plotting/rendering
|
|
274
|
-
for i, (trace, name, ax) in enumerate(zip(digital_traces, names, axes, strict=False)):
|
|
275
|
-
channel_annotations = annotations[i] if annotations and i < len(annotations) else None
|
|
276
|
-
_plot_timing_channel(
|
|
277
|
-
ax, trace, name, i, multiplier, time_range, show_grid, channel_annotations, time_unit
|
|
278
|
-
)
|
|
279
|
-
|
|
280
|
-
# Remove x-axis labels except for bottom plot
|
|
281
|
-
if i < n_channels - 1:
|
|
282
|
-
ax.set_xticklabels([])
|
|
283
|
-
|
|
284
|
-
# Annotation/labeling
|
|
285
|
-
axes[-1].set_xlabel(f"Time ({time_unit})")
|
|
286
|
-
|
|
287
|
-
if title:
|
|
288
|
-
fig.suptitle(title, fontsize=14, fontweight="bold")
|
|
289
|
-
|
|
290
|
-
# Layout/formatting
|
|
291
|
-
fig.tight_layout()
|
|
292
|
-
return fig
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
def _add_protocol_annotations(
|
|
296
|
-
ax: Axes,
|
|
297
|
-
annotations: list[Annotation],
|
|
298
|
-
multiplier: float,
|
|
299
|
-
time_unit: str,
|
|
300
|
-
) -> None:
|
|
301
|
-
"""Add protocol decode annotations to timing diagram.
|
|
302
|
-
|
|
303
|
-
Args:
|
|
304
|
-
ax: Matplotlib axes to annotate.
|
|
305
|
-
annotations: List of protocol annotations.
|
|
306
|
-
multiplier: Time unit multiplier for display.
|
|
307
|
-
time_unit: Time unit string.
|
|
308
|
-
"""
|
|
309
|
-
for ann in annotations:
|
|
310
|
-
# Get annotation time range
|
|
311
|
-
start_time = ann.start_sample * multiplier if hasattr(ann, "start_sample") else 0
|
|
312
|
-
end_time = ann.end_sample * multiplier if hasattr(ann, "end_sample") else start_time
|
|
313
|
-
|
|
314
|
-
# Get annotation text and level
|
|
315
|
-
if hasattr(ann, "data"):
|
|
316
|
-
text = str(ann.data)
|
|
317
|
-
elif hasattr(ann, "value"):
|
|
318
|
-
text = str(ann.value)
|
|
319
|
-
else:
|
|
320
|
-
text = str(ann)
|
|
321
|
-
|
|
322
|
-
# Determine annotation color based on type/level
|
|
323
|
-
color = "lightblue"
|
|
324
|
-
if hasattr(ann, "level"):
|
|
325
|
-
level_str = str(ann.level).lower()
|
|
326
|
-
if "error" in level_str or "warn" in level_str:
|
|
327
|
-
color = "lightcoral"
|
|
328
|
-
elif "data" in level_str or "byte" in level_str:
|
|
329
|
-
color = "lightgreen"
|
|
330
|
-
elif "start" in level_str or "stop" in level_str:
|
|
331
|
-
color = "lightyellow"
|
|
332
|
-
|
|
333
|
-
# Draw annotation box
|
|
334
|
-
width = end_time - start_time if end_time > start_time else multiplier * 10
|
|
335
|
-
rect = Rectangle(
|
|
336
|
-
(start_time, 1.05),
|
|
337
|
-
width,
|
|
338
|
-
0.15,
|
|
339
|
-
facecolor=color,
|
|
340
|
-
edgecolor="black",
|
|
341
|
-
linewidth=0.5,
|
|
342
|
-
alpha=0.7,
|
|
343
|
-
)
|
|
344
|
-
ax.add_patch(rect)
|
|
345
|
-
|
|
346
|
-
# Add text label
|
|
347
|
-
mid_time = start_time + width / 2
|
|
348
|
-
ax.text(
|
|
349
|
-
mid_time,
|
|
350
|
-
1.125,
|
|
351
|
-
text,
|
|
352
|
-
ha="center",
|
|
353
|
-
va="center",
|
|
354
|
-
fontsize=7,
|
|
355
|
-
fontfamily="monospace",
|
|
356
|
-
)
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
def plot_logic_analyzer(
|
|
360
|
-
traces: Sequence[DigitalTrace],
|
|
361
|
-
*,
|
|
362
|
-
names: list[str] | None = None,
|
|
363
|
-
bus_groups: dict[str, list[int]] | None = None,
|
|
364
|
-
time_unit: str = "auto",
|
|
365
|
-
show_grid: bool = True,
|
|
366
|
-
figsize: tuple[float, float] | None = None,
|
|
367
|
-
title: str | None = None,
|
|
368
|
-
) -> Figure:
|
|
369
|
-
"""Plot logic analyzer style multi-channel display with bus grouping.
|
|
370
|
-
|
|
371
|
-
Creates a timing diagram optimized for logic analyzer visualization
|
|
372
|
-
with support for bus grouping (showing multi-bit buses as hex values).
|
|
373
|
-
|
|
374
|
-
Args:
|
|
375
|
-
traces: List of digital traces.
|
|
376
|
-
names: Channel names.
|
|
377
|
-
bus_groups: Dictionary mapping bus names to channel indices.
|
|
378
|
-
Example: {"DATA": [0, 1, 2, 3], "ADDR": [4, 5, 6, 7]}
|
|
379
|
-
time_unit: Time unit for display.
|
|
380
|
-
show_grid: Show vertical grid lines.
|
|
381
|
-
figsize: Figure size.
|
|
382
|
-
title: Plot title.
|
|
383
|
-
|
|
384
|
-
Returns:
|
|
385
|
-
Matplotlib Figure object.
|
|
386
|
-
|
|
387
|
-
Raises:
|
|
388
|
-
ImportError: If matplotlib is not available.
|
|
389
|
-
ValueError: If traces list is empty.
|
|
390
|
-
|
|
391
|
-
Example:
|
|
392
|
-
>>> fig = plot_logic_analyzer(
|
|
393
|
-
... traces,
|
|
394
|
-
... names=[f"D{i}" for i in range(8)],
|
|
395
|
-
... bus_groups={"DATA": [0, 1, 2, 3, 4, 5, 6, 7]}
|
|
396
|
-
... )
|
|
397
|
-
|
|
398
|
-
References:
|
|
399
|
-
Logic analyzer display conventions
|
|
400
|
-
"""
|
|
401
|
-
if not HAS_MATPLOTLIB:
|
|
402
|
-
raise ImportError("matplotlib is required for visualization")
|
|
403
|
-
|
|
404
|
-
if len(traces) == 0:
|
|
405
|
-
raise ValueError("traces list cannot be empty")
|
|
406
|
-
|
|
407
|
-
# Convert to list for plot_timing
|
|
408
|
-
traces_list: list[WaveformTrace | DigitalTrace] = list(traces)
|
|
409
|
-
|
|
410
|
-
# If no bus groups, just use regular timing diagram
|
|
411
|
-
if bus_groups is None:
|
|
412
|
-
return plot_timing(
|
|
413
|
-
traces_list,
|
|
414
|
-
names=names,
|
|
415
|
-
time_unit=time_unit,
|
|
416
|
-
show_grid=show_grid,
|
|
417
|
-
figsize=figsize,
|
|
418
|
-
title=title,
|
|
419
|
-
)
|
|
420
|
-
|
|
421
|
-
# Implementation for bus grouping would go here
|
|
422
|
-
# For MVP, delegate to plot_timing
|
|
423
|
-
return plot_timing(
|
|
424
|
-
traces_list,
|
|
425
|
-
names=names,
|
|
426
|
-
time_unit=time_unit,
|
|
427
|
-
show_grid=show_grid,
|
|
428
|
-
figsize=figsize,
|
|
429
|
-
title=title,
|
|
430
|
-
)
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
__all__ = [
|
|
434
|
-
"plot_logic_analyzer",
|
|
435
|
-
"plot_timing",
|
|
436
|
-
]
|