oscura 0.7.0__py3-none-any.whl → 0.10.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.
Files changed (175) hide show
  1. oscura/__init__.py +19 -19
  2. oscura/analyzers/__init__.py +2 -0
  3. oscura/analyzers/digital/extraction.py +2 -3
  4. oscura/analyzers/digital/quality.py +1 -1
  5. oscura/analyzers/digital/timing.py +1 -1
  6. oscura/analyzers/eye/__init__.py +5 -1
  7. oscura/analyzers/eye/generation.py +501 -0
  8. oscura/analyzers/jitter/__init__.py +6 -6
  9. oscura/analyzers/jitter/timing.py +419 -0
  10. oscura/analyzers/patterns/__init__.py +94 -0
  11. oscura/analyzers/patterns/reverse_engineering.py +991 -0
  12. oscura/analyzers/power/__init__.py +35 -12
  13. oscura/analyzers/power/basic.py +3 -3
  14. oscura/analyzers/power/soa.py +1 -1
  15. oscura/analyzers/power/switching.py +3 -3
  16. oscura/analyzers/signal_classification.py +529 -0
  17. oscura/analyzers/signal_integrity/sparams.py +3 -3
  18. oscura/analyzers/statistics/__init__.py +4 -0
  19. oscura/analyzers/statistics/basic.py +152 -0
  20. oscura/analyzers/statistics/correlation.py +47 -6
  21. oscura/analyzers/validation.py +1 -1
  22. oscura/analyzers/waveform/__init__.py +2 -0
  23. oscura/analyzers/waveform/measurements.py +329 -163
  24. oscura/analyzers/waveform/measurements_with_uncertainty.py +91 -35
  25. oscura/analyzers/waveform/spectral.py +498 -54
  26. oscura/api/dsl/commands.py +15 -6
  27. oscura/api/server/templates/base.html +137 -146
  28. oscura/api/server/templates/export.html +84 -110
  29. oscura/api/server/templates/home.html +248 -267
  30. oscura/api/server/templates/protocols.html +44 -48
  31. oscura/api/server/templates/reports.html +27 -35
  32. oscura/api/server/templates/session_detail.html +68 -78
  33. oscura/api/server/templates/sessions.html +62 -72
  34. oscura/api/server/templates/waveforms.html +54 -64
  35. oscura/automotive/__init__.py +1 -1
  36. oscura/automotive/can/session.py +1 -1
  37. oscura/automotive/dbc/generator.py +638 -23
  38. oscura/automotive/dtc/data.json +102 -17
  39. oscura/automotive/uds/decoder.py +99 -6
  40. oscura/cli/analyze.py +8 -2
  41. oscura/cli/batch.py +36 -5
  42. oscura/cli/characterize.py +18 -4
  43. oscura/cli/export.py +47 -5
  44. oscura/cli/main.py +2 -0
  45. oscura/cli/onboarding/wizard.py +10 -6
  46. oscura/cli/pipeline.py +585 -0
  47. oscura/cli/visualize.py +6 -4
  48. oscura/convenience.py +400 -32
  49. oscura/core/config/loader.py +0 -1
  50. oscura/core/measurement_result.py +286 -0
  51. oscura/core/progress.py +1 -1
  52. oscura/core/schemas/device_mapping.json +8 -2
  53. oscura/core/schemas/packet_format.json +24 -4
  54. oscura/core/schemas/protocol_definition.json +12 -2
  55. oscura/core/types.py +300 -199
  56. oscura/correlation/multi_protocol.py +1 -1
  57. oscura/export/legacy/__init__.py +11 -0
  58. oscura/export/legacy/wav.py +75 -0
  59. oscura/exporters/__init__.py +19 -0
  60. oscura/exporters/wireshark.py +809 -0
  61. oscura/hardware/acquisition/file.py +5 -19
  62. oscura/hardware/acquisition/saleae.py +10 -10
  63. oscura/hardware/acquisition/socketcan.py +4 -6
  64. oscura/hardware/acquisition/synthetic.py +1 -5
  65. oscura/hardware/acquisition/visa.py +6 -6
  66. oscura/hardware/security/side_channel_detector.py +5 -508
  67. oscura/inference/message_format.py +686 -1
  68. oscura/jupyter/display.py +2 -2
  69. oscura/jupyter/magic.py +3 -3
  70. oscura/loaders/__init__.py +17 -12
  71. oscura/loaders/binary.py +1 -1
  72. oscura/loaders/chipwhisperer.py +1 -2
  73. oscura/loaders/configurable.py +1 -1
  74. oscura/loaders/csv_loader.py +2 -2
  75. oscura/loaders/hdf5_loader.py +1 -1
  76. oscura/loaders/lazy.py +6 -1
  77. oscura/loaders/mmap_loader.py +0 -1
  78. oscura/loaders/numpy_loader.py +8 -7
  79. oscura/loaders/preprocessing.py +3 -5
  80. oscura/loaders/rigol.py +21 -7
  81. oscura/loaders/sigrok.py +2 -5
  82. oscura/loaders/tdms.py +3 -2
  83. oscura/loaders/tektronix.py +38 -32
  84. oscura/loaders/tss.py +20 -27
  85. oscura/loaders/vcd.py +13 -8
  86. oscura/loaders/wav.py +1 -6
  87. oscura/pipeline/__init__.py +76 -0
  88. oscura/pipeline/handlers/__init__.py +165 -0
  89. oscura/pipeline/handlers/analyzers.py +1045 -0
  90. oscura/pipeline/handlers/decoders.py +899 -0
  91. oscura/pipeline/handlers/exporters.py +1103 -0
  92. oscura/pipeline/handlers/filters.py +891 -0
  93. oscura/pipeline/handlers/loaders.py +640 -0
  94. oscura/pipeline/handlers/transforms.py +768 -0
  95. oscura/reporting/__init__.py +88 -1
  96. oscura/reporting/automation.py +348 -0
  97. oscura/reporting/citations.py +374 -0
  98. oscura/reporting/core.py +54 -0
  99. oscura/reporting/formatting/__init__.py +11 -0
  100. oscura/reporting/formatting/measurements.py +320 -0
  101. oscura/reporting/html.py +57 -0
  102. oscura/reporting/interpretation.py +431 -0
  103. oscura/reporting/summary.py +329 -0
  104. oscura/reporting/templates/enhanced/protocol_re.html +504 -503
  105. oscura/reporting/visualization.py +542 -0
  106. oscura/side_channel/__init__.py +38 -57
  107. oscura/utils/builders/signal_builder.py +5 -5
  108. oscura/utils/comparison/compare.py +7 -9
  109. oscura/utils/comparison/golden.py +1 -1
  110. oscura/utils/filtering/convenience.py +2 -2
  111. oscura/utils/math/arithmetic.py +38 -62
  112. oscura/utils/math/interpolation.py +20 -20
  113. oscura/utils/pipeline/__init__.py +4 -17
  114. oscura/utils/progressive.py +1 -4
  115. oscura/utils/triggering/edge.py +1 -1
  116. oscura/utils/triggering/pattern.py +2 -2
  117. oscura/utils/triggering/pulse.py +2 -2
  118. oscura/utils/triggering/window.py +3 -3
  119. oscura/validation/hil_testing.py +11 -11
  120. oscura/visualization/__init__.py +47 -284
  121. oscura/visualization/batch.py +160 -0
  122. oscura/visualization/plot.py +542 -53
  123. oscura/visualization/styles.py +184 -318
  124. oscura/workflows/__init__.py +2 -0
  125. oscura/workflows/batch/advanced.py +1 -1
  126. oscura/workflows/batch/aggregate.py +7 -8
  127. oscura/workflows/complete_re.py +251 -23
  128. oscura/workflows/digital.py +27 -4
  129. oscura/workflows/multi_trace.py +136 -17
  130. oscura/workflows/waveform.py +788 -0
  131. {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/METADATA +59 -79
  132. {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/RECORD +135 -149
  133. oscura/side_channel/dpa.py +0 -1025
  134. oscura/utils/optimization/__init__.py +0 -19
  135. oscura/utils/optimization/parallel.py +0 -443
  136. oscura/utils/optimization/search.py +0 -532
  137. oscura/utils/pipeline/base.py +0 -338
  138. oscura/utils/pipeline/composition.py +0 -248
  139. oscura/utils/pipeline/parallel.py +0 -449
  140. oscura/utils/pipeline/pipeline.py +0 -375
  141. oscura/utils/search/__init__.py +0 -16
  142. oscura/utils/search/anomaly.py +0 -424
  143. oscura/utils/search/context.py +0 -294
  144. oscura/utils/search/pattern.py +0 -288
  145. oscura/utils/storage/__init__.py +0 -61
  146. oscura/utils/storage/database.py +0 -1166
  147. oscura/visualization/accessibility.py +0 -526
  148. oscura/visualization/annotations.py +0 -371
  149. oscura/visualization/axis_scaling.py +0 -305
  150. oscura/visualization/colors.py +0 -451
  151. oscura/visualization/digital.py +0 -436
  152. oscura/visualization/eye.py +0 -571
  153. oscura/visualization/histogram.py +0 -281
  154. oscura/visualization/interactive.py +0 -1035
  155. oscura/visualization/jitter.py +0 -1042
  156. oscura/visualization/keyboard.py +0 -394
  157. oscura/visualization/layout.py +0 -400
  158. oscura/visualization/optimization.py +0 -1079
  159. oscura/visualization/palettes.py +0 -446
  160. oscura/visualization/power.py +0 -508
  161. oscura/visualization/power_extended.py +0 -955
  162. oscura/visualization/presets.py +0 -469
  163. oscura/visualization/protocols.py +0 -1246
  164. oscura/visualization/render.py +0 -223
  165. oscura/visualization/rendering.py +0 -444
  166. oscura/visualization/reverse_engineering.py +0 -838
  167. oscura/visualization/signal_integrity.py +0 -989
  168. oscura/visualization/specialized.py +0 -643
  169. oscura/visualization/spectral.py +0 -1226
  170. oscura/visualization/thumbnails.py +0 -340
  171. oscura/visualization/time_axis.py +0 -351
  172. oscura/visualization/waveform.py +0 -454
  173. {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/WHEEL +0 -0
  174. {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/entry_points.txt +0 -0
  175. {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,1246 +0,0 @@
1
- """Protocol decoder visualization functions.
2
-
3
- This module provides visualization functions for decoded protocol packets,
4
- creating timing diagrams with multi-level annotations for protocol analysis.
5
-
6
- Example:
7
- >>> from oscura.analyzers.protocols.uart import UARTDecoder
8
- >>> from oscura.visualization.protocols import plot_protocol_decode
9
- >>>
10
- >>> decoder = UARTDecoder(baudrate=115200)
11
- >>> packets = list(decoder.decode(trace))
12
- >>> fig = plot_protocol_decode(packets, trace=trace, title="UART Decode")
13
-
14
- References:
15
- - Protocol visualization best practices
16
- - Wavedrom-style timing diagrams
17
- - sigrok annotation system
18
- """
19
-
20
- from __future__ import annotations
21
-
22
- from typing import TYPE_CHECKING, Any, Literal
23
-
24
- import numpy as np
25
-
26
- if TYPE_CHECKING:
27
- from matplotlib.axes import Axes
28
- from matplotlib.figure import Figure
29
- from numpy.typing import NDArray
30
-
31
- from oscura.core.types import DigitalTrace, ProtocolPacket
32
-
33
- try:
34
- import matplotlib.pyplot as plt
35
- from matplotlib import patches
36
-
37
- HAS_MATPLOTLIB = True
38
- except ImportError:
39
- HAS_MATPLOTLIB = False
40
-
41
-
42
- def plot_protocol_decode(
43
- packets: list[ProtocolPacket],
44
- *,
45
- trace: DigitalTrace | None = None,
46
- trace_channel: str | None = None,
47
- annotation_levels: list[str] | Literal["all"] = "all",
48
- time_range: tuple[float, float] | None = None,
49
- time_unit: str = "auto",
50
- show_data: bool = True,
51
- show_errors: bool = True,
52
- colorize: bool = True,
53
- figsize: tuple[float, float] | None = None,
54
- title: str | None = None,
55
- ) -> Figure:
56
- """Plot decoded protocol packets with multi-level annotations.
57
-
58
- Creates a timing diagram showing the original waveform (if provided)
59
- and annotation rows for decoded protocol data at different levels
60
- (bits, bytes, fields, packets, messages).
61
-
62
- Args:
63
- packets: List of decoded protocol packets to visualize.
64
- trace: Optional digital trace to plot alongside annotations.
65
- trace_channel: Name of trace channel (default: protocol name).
66
- annotation_levels: Which annotation levels to display ("all" or list of level names).
67
- time_range: Time range to plot (t_min, t_max) in seconds. None = auto from packets.
68
- time_unit: Time unit for x-axis ("s", "ms", "us", "ns", "auto").
69
- show_data: Show decoded data values in annotations.
70
- show_errors: Highlight packets with errors.
71
- colorize: Use color coding for different packet types.
72
- figsize: Figure size (width, height). Auto-calculated if None.
73
- title: Plot title.
74
-
75
- Returns:
76
- Matplotlib Figure object.
77
-
78
- Raises:
79
- ImportError: If matplotlib is not available.
80
- ValueError: If packets list is empty.
81
-
82
- Example:
83
- >>> decoder = UARTDecoder(baudrate=9600)
84
- >>> packets = list(decoder.decode(rx_trace))
85
- >>> fig = plot_protocol_decode(
86
- ... packets,
87
- ... trace=rx_trace,
88
- ... time_unit="ms",
89
- ... title="UART Communication"
90
- ... )
91
-
92
- References:
93
- VIS-030: Protocol Decode Visualization
94
- """
95
- _validate_plot_inputs(packets)
96
- protocol = packets[0].protocol
97
- t_min, t_max, time_mult, time_unit = _calculate_time_parameters(packets, time_range, time_unit)
98
- fig, axes = _create_figure_layout(trace, figsize)
99
- ax_idx = _plot_waveform_if_present(
100
- axes, trace, trace_channel, protocol, t_min, t_max, time_mult
101
- )
102
- _plot_packet_timeline(
103
- axes[ax_idx], packets, protocol, t_min, t_max, time_mult, show_data, show_errors, colorize
104
- )
105
- _finalize_plot_layout(axes, t_min, t_max, time_mult, time_unit, title)
106
-
107
- return fig
108
-
109
-
110
- def _validate_plot_inputs(packets: list[ProtocolPacket]) -> None:
111
- """Validate plot inputs.
112
-
113
- Args:
114
- packets: List of protocol packets.
115
-
116
- Raises:
117
- ImportError: If matplotlib not available.
118
- ValueError: If packets list is empty.
119
- """
120
- if not HAS_MATPLOTLIB:
121
- raise ImportError("matplotlib is required for visualization")
122
- if len(packets) == 0:
123
- raise ValueError("packets list cannot be empty")
124
-
125
-
126
- def _calculate_time_parameters(
127
- packets: list[ProtocolPacket],
128
- time_range: tuple[float, float] | None,
129
- time_unit: str,
130
- ) -> tuple[float, float, float, str]:
131
- """Calculate time range and multiplier for plotting.
132
-
133
- Args:
134
- packets: List of packets for auto time range.
135
- time_range: User-specified time range or None.
136
- time_unit: Time unit string.
137
-
138
- Returns:
139
- Tuple of (t_min, t_max, time_mult, time_unit).
140
- """
141
- if time_range is None:
142
- t_min = min(p.timestamp for p in packets)
143
- t_max = max(p.end_timestamp if p.end_timestamp else p.timestamp for p in packets)
144
- padding = (t_max - t_min) * 0.1
145
- t_min -= padding
146
- t_max += padding
147
- else:
148
- t_min, t_max = time_range
149
-
150
- if time_unit == "auto":
151
- time_range_val = t_max - t_min
152
- if time_range_val < 1e-6:
153
- time_unit = "ns"
154
- time_mult = 1e9
155
- elif time_range_val < 1e-3:
156
- time_unit = "us"
157
- time_mult = 1e6
158
- elif time_range_val < 1:
159
- time_unit = "ms"
160
- time_mult = 1e3
161
- else:
162
- time_unit = "s"
163
- time_mult = 1.0
164
- else:
165
- time_mult = {"s": 1.0, "ms": 1e3, "us": 1e6, "ns": 1e9}.get(time_unit, 1.0)
166
-
167
- return t_min, t_max, time_mult, time_unit
168
-
169
-
170
- def _create_figure_layout(
171
- trace: DigitalTrace | None, figsize: tuple[float, float] | None
172
- ) -> tuple[Figure, list[Axes]]:
173
- """Create figure and axes layout.
174
-
175
- Args:
176
- trace: Optional trace for determining row count.
177
- figsize: Figure size or None for auto.
178
-
179
- Returns:
180
- Tuple of (figure, axes_list).
181
- """
182
- n_rows = 1 if trace is None else 2
183
-
184
- if figsize is None:
185
- width = 14
186
- height = max(4, n_rows * 1.5 + 1)
187
- figsize = (width, height)
188
-
189
- fig, axes = plt.subplots(
190
- n_rows,
191
- 1,
192
- figsize=figsize,
193
- sharex=True,
194
- gridspec_kw={"hspace": 0.15, "height_ratios": [1] * n_rows},
195
- )
196
-
197
- if n_rows == 1:
198
- axes = [axes]
199
-
200
- return fig, axes
201
-
202
-
203
- def _plot_waveform_if_present(
204
- axes: list[Axes],
205
- trace: DigitalTrace | None,
206
- trace_channel: str | None,
207
- protocol: str,
208
- t_min: float,
209
- t_max: float,
210
- time_mult: float,
211
- ) -> int:
212
- """Plot waveform trace if provided.
213
-
214
- Args:
215
- axes: List of axes to plot on.
216
- trace: Optional digital trace.
217
- trace_channel: Channel name override.
218
- protocol: Protocol name for default label.
219
- t_min: Minimum time value.
220
- t_max: Maximum time value.
221
- time_mult: Time unit multiplier.
222
-
223
- Returns:
224
- Index of next available axis.
225
- """
226
- if trace is None:
227
- return 0
228
-
229
- ax = axes[0]
230
- trace_time = trace.time_vector * time_mult
231
- trace_data = trace.data.astype(float)
232
-
233
- mask = (trace_time >= t_min * time_mult) & (trace_time <= t_max * time_mult)
234
- trace_time = trace_time[mask]
235
- trace_data = trace_data[mask]
236
-
237
- _plot_digital_waveform(ax, trace_time, trace_data)
238
-
239
- channel_name = trace_channel if trace_channel else protocol
240
- ax.set_ylabel(channel_name, rotation=0, ha="right", va="center", fontsize=10)
241
- ax.set_ylim(-0.2, 1.3)
242
- ax.set_yticks([])
243
- ax.grid(True, axis="x", alpha=0.3, linestyle=":")
244
-
245
- return 1
246
-
247
-
248
- def _plot_packet_timeline(
249
- ax: Axes,
250
- packets: list[ProtocolPacket],
251
- protocol: str,
252
- t_min: float,
253
- t_max: float,
254
- time_mult: float,
255
- show_data: bool,
256
- show_errors: bool,
257
- colorize: bool,
258
- ) -> None:
259
- """Plot packet timeline on axis.
260
-
261
- Args:
262
- ax: Matplotlib axis.
263
- packets: List of packets to plot.
264
- protocol: Protocol name.
265
- t_min: Minimum time value.
266
- t_max: Maximum time value.
267
- time_mult: Time unit multiplier.
268
- show_data: Show data annotations.
269
- show_errors: Highlight errors.
270
- colorize: Use color coding.
271
- """
272
- for packet in packets:
273
- if packet.timestamp < t_min or packet.timestamp > t_max:
274
- continue
275
-
276
- start = packet.timestamp * time_mult
277
- end = (
278
- packet.end_timestamp if packet.end_timestamp else packet.timestamp + 0.001
279
- ) * time_mult
280
-
281
- color = _determine_packet_color(packet, protocol, show_errors, colorize)
282
-
283
- rect = patches.Rectangle(
284
- (start, 0.1),
285
- end - start,
286
- 0.8,
287
- facecolor=color,
288
- edgecolor="black",
289
- linewidth=0.8,
290
- alpha=0.7,
291
- )
292
- ax.add_patch(rect)
293
-
294
- if show_data and packet.data:
295
- _add_packet_annotation(ax, packet, start, end, show_errors)
296
-
297
- if show_errors and packet.errors:
298
- ax.plot(start, 1.1, "rx", markersize=8, markeredgewidth=2)
299
-
300
- ax.set_ylabel(f"{protocol}\nPackets", rotation=0, ha="right", va="center", fontsize=10)
301
- ax.set_ylim(0, 1.2)
302
- ax.set_yticks([])
303
- ax.grid(True, axis="x", alpha=0.3, linestyle=":")
304
-
305
-
306
- def _determine_packet_color(
307
- packet: ProtocolPacket, protocol: str, show_errors: bool, colorize: bool
308
- ) -> str:
309
- """Determine packet rectangle color.
310
-
311
- Args:
312
- packet: Protocol packet.
313
- protocol: Protocol name.
314
- show_errors: Whether to highlight errors.
315
- colorize: Whether to use protocol colors.
316
-
317
- Returns:
318
- Color string.
319
- """
320
- if show_errors and packet.errors:
321
- return "#ff6b6b"
322
- elif colorize:
323
- return _get_packet_color(packet, protocol)
324
- else:
325
- return "#4ecdc4"
326
-
327
-
328
- def _add_packet_annotation(
329
- ax: Axes, packet: ProtocolPacket, start: float, end: float, show_errors: bool
330
- ) -> None:
331
- """Add data annotation to packet.
332
-
333
- Args:
334
- ax: Matplotlib axis.
335
- packet: Protocol packet.
336
- start: Start time in scaled units.
337
- end: End time in scaled units.
338
- show_errors: Whether errors are highlighted.
339
- """
340
- data_str = _format_packet_data(packet)
341
- mid_time = (start + end) / 2
342
- text_color = "white" if not (show_errors and packet.errors) else "black"
343
- ax.text(
344
- mid_time,
345
- 0.5,
346
- data_str,
347
- ha="center",
348
- va="center",
349
- fontsize=8,
350
- fontweight="bold",
351
- color=text_color,
352
- )
353
-
354
-
355
- def _finalize_plot_layout(
356
- axes: list[Axes],
357
- t_min: float,
358
- t_max: float,
359
- time_mult: float,
360
- time_unit: str,
361
- title: str | None,
362
- ) -> None:
363
- """Finalize plot layout with labels and title.
364
-
365
- Args:
366
- axes: List of axes.
367
- t_min: Minimum time value.
368
- t_max: Maximum time value.
369
- time_mult: Time unit multiplier.
370
- time_unit: Time unit string.
371
- title: Optional plot title.
372
- """
373
- axes[-1].set_xlabel(f"Time ({time_unit})", fontsize=11)
374
- axes[-1].set_xlim(t_min * time_mult, t_max * time_mult)
375
-
376
- fig = axes[0].get_figure()
377
- if fig and hasattr(fig, "tight_layout"):
378
- if title:
379
- fig.suptitle(title, fontsize=14, y=0.98)
380
- fig.tight_layout()
381
-
382
-
383
- def plot_uart_decode(
384
- packets: list[ProtocolPacket],
385
- *,
386
- rx_trace: DigitalTrace | None = None,
387
- tx_trace: DigitalTrace | None = None,
388
- time_range: tuple[float, float] | None = None,
389
- time_unit: str = "auto",
390
- show_parity_errors: bool = True,
391
- show_framing_errors: bool = True,
392
- figsize: tuple[float, float] | None = None,
393
- title: str = "UART Communication",
394
- ) -> Figure:
395
- """Plot UART decoded packets with RX/TX lanes.
396
-
397
- Specialized visualization for UART showing separate RX and TX channels
398
- with decoded bytes and error highlighting.
399
-
400
- Args:
401
- packets: List of UART packets.
402
- rx_trace: Optional RX digital trace.
403
- tx_trace: Optional TX digital trace.
404
- time_range: Time range to plot (t_min, t_max) in seconds.
405
- time_unit: Time unit for x-axis ("s", "ms", "us", "ns", "auto").
406
- show_parity_errors: Highlight parity errors.
407
- show_framing_errors: Highlight framing errors.
408
- figsize: Figure size (width, height).
409
- title: Plot title.
410
-
411
- Returns:
412
- Matplotlib Figure object.
413
-
414
- Raises:
415
- ImportError: If matplotlib is not installed.
416
- ValueError: If packets list is empty.
417
-
418
- Example:
419
- >>> decoder = UARTDecoder(baudrate=115200, parity="even")
420
- >>> packets = list(decoder.decode(rx_trace))
421
- >>> fig = plot_uart_decode(packets, rx_trace=rx_trace, time_unit="ms")
422
- """
423
- if not HAS_MATPLOTLIB:
424
- raise ImportError("matplotlib is required for visualization")
425
-
426
- if len(packets) == 0:
427
- raise ValueError("packets list cannot be empty")
428
-
429
- # If we have both RX and TX, create dual-channel visualization
430
- if rx_trace is not None and tx_trace is not None:
431
- return _plot_dual_channel_uart(
432
- packets,
433
- rx_trace=rx_trace,
434
- tx_trace=tx_trace,
435
- time_range=time_range,
436
- time_unit=time_unit,
437
- show_parity_errors=show_parity_errors,
438
- show_framing_errors=show_framing_errors,
439
- figsize=figsize,
440
- title=title,
441
- )
442
-
443
- # Single-channel view using generic decode plot
444
- return plot_protocol_decode(
445
- packets,
446
- trace=rx_trace or tx_trace,
447
- trace_channel="RX" if rx_trace else "TX",
448
- show_errors=show_parity_errors or show_framing_errors,
449
- time_range=time_range,
450
- time_unit=time_unit,
451
- figsize=figsize,
452
- title=title,
453
- )
454
-
455
-
456
- def _plot_dual_channel_uart(
457
- packets: list[ProtocolPacket],
458
- *,
459
- rx_trace: DigitalTrace,
460
- tx_trace: DigitalTrace,
461
- time_range: tuple[float, float] | None = None,
462
- time_unit: str = "auto",
463
- show_parity_errors: bool = True,
464
- show_framing_errors: bool = True,
465
- figsize: tuple[float, float] | None = None,
466
- title: str = "UART Communication",
467
- ) -> Figure:
468
- """Create dual-channel UART visualization with separate RX/TX rows.
469
-
470
- Args:
471
- packets: List of UART packets (may include both RX and TX).
472
- rx_trace: RX digital trace.
473
- tx_trace: TX digital trace.
474
- time_range: Time range to plot (t_min, t_max) in seconds.
475
- time_unit: Time unit for x-axis.
476
- show_parity_errors: Highlight parity errors.
477
- show_framing_errors: Highlight framing errors.
478
- figsize: Figure size (width, height).
479
- title: Plot title.
480
-
481
- Returns:
482
- Matplotlib Figure object.
483
- """
484
- # Calculate time parameters
485
- t_min, t_max, time_mult, time_unit = _determine_time_params(packets, time_range, time_unit)
486
-
487
- # Create figure with 4 rows
488
- fig, axes = _create_dual_uart_figure(figsize)
489
-
490
- # Separate packets by channel
491
- rx_packets, tx_packets = _separate_uart_packets(packets)
492
- show_errors = show_parity_errors or show_framing_errors
493
-
494
- # Plot all four rows
495
- _plot_uart_channel_pair(
496
- axes[0], axes[1], rx_trace, rx_packets, "RX", t_min, t_max, time_mult, show_errors
497
- )
498
- _plot_uart_channel_pair(
499
- axes[2], axes[3], tx_trace, tx_packets, "TX", t_min, t_max, time_mult, show_errors
500
- )
501
-
502
- # Finalize plot
503
- _finalize_uart_plot(fig, axes, t_min, t_max, time_mult, time_unit, title)
504
-
505
- return fig
506
-
507
-
508
- def _create_dual_uart_figure(
509
- figsize: tuple[float, float] | None,
510
- ) -> tuple[Figure, list[Axes]]:
511
- """Create figure for dual-channel UART plot.
512
-
513
- Args:
514
- figsize: Figure size or None for auto-calculation.
515
-
516
- Returns:
517
- Tuple of (figure, axes_list).
518
- """
519
- n_rows = 4
520
-
521
- if figsize is None:
522
- width = 14
523
- height = max(6, n_rows * 1.2 + 1)
524
- figsize = (width, height)
525
-
526
- fig, axes = plt.subplots(
527
- n_rows,
528
- 1,
529
- figsize=figsize,
530
- sharex=True,
531
- gridspec_kw={"hspace": 0.1, "height_ratios": [1, 0.8, 1, 0.8]},
532
- )
533
-
534
- return fig, axes
535
-
536
-
537
- def _separate_uart_packets(
538
- packets: list[ProtocolPacket],
539
- ) -> tuple[list[ProtocolPacket], list[ProtocolPacket]]:
540
- """Separate UART packets by channel (RX vs TX).
541
-
542
- Args:
543
- packets: List of UART packets.
544
-
545
- Returns:
546
- Tuple of (rx_packets, tx_packets).
547
- """
548
- rx_packets = []
549
- tx_packets = []
550
-
551
- for packet in packets:
552
- channel = getattr(packet, "channel", None)
553
- if channel is None and hasattr(packet, "metadata"):
554
- channel = packet.metadata.get("channel") if isinstance(packet.metadata, dict) else None
555
-
556
- if channel == "TX":
557
- tx_packets.append(packet)
558
- else:
559
- rx_packets.append(packet)
560
-
561
- # If no channel info, put all packets on RX
562
- if not rx_packets and not tx_packets:
563
- rx_packets = packets
564
-
565
- return rx_packets, tx_packets
566
-
567
-
568
- def _plot_uart_channel_pair(
569
- ax_wave: Axes,
570
- ax_packets: Axes,
571
- trace: DigitalTrace,
572
- packets: list[ProtocolPacket],
573
- label: str,
574
- t_min: float,
575
- t_max: float,
576
- time_mult: float,
577
- show_errors: bool,
578
- ) -> None:
579
- """Plot waveform and packet row for a single UART channel.
580
-
581
- Args:
582
- ax_wave: Axis for waveform plot.
583
- ax_packets: Axis for packet annotations.
584
- trace: Digital trace for the channel.
585
- packets: Packets for this channel.
586
- label: Channel label (e.g., "RX" or "TX").
587
- t_min: Minimum time value.
588
- t_max: Maximum time value.
589
- time_mult: Time unit multiplier.
590
- show_errors: Whether to highlight errors.
591
- """
592
- # Plot waveform
593
- trace_time = trace.time_vector * time_mult
594
- trace_data = trace.data.astype(float)
595
- mask = (trace_time >= t_min * time_mult) & (trace_time <= t_max * time_mult)
596
- _plot_digital_waveform(ax_wave, trace_time[mask], trace_data[mask])
597
- ax_wave.set_ylabel(label, rotation=0, ha="right", va="center", fontsize=10)
598
- ax_wave.set_ylim(-0.2, 1.3)
599
- ax_wave.set_yticks([])
600
- ax_wave.grid(True, axis="x", alpha=0.3, linestyle=":")
601
-
602
- # Plot packets
603
- _plot_packet_row(ax_packets, packets, t_min, t_max, time_mult, show_errors)
604
- ax_packets.set_ylabel(f"{label}\nData", rotation=0, ha="right", va="center", fontsize=9)
605
-
606
-
607
- def _finalize_uart_plot(
608
- fig: Figure,
609
- axes: list[Axes],
610
- t_min: float,
611
- t_max: float,
612
- time_mult: float,
613
- time_unit: str,
614
- title: str | None,
615
- ) -> None:
616
- """Add final formatting to UART plot.
617
-
618
- Args:
619
- fig: Matplotlib figure.
620
- axes: List of axes.
621
- t_min: Minimum time value.
622
- t_max: Maximum time value.
623
- time_mult: Time multiplier.
624
- time_unit: Time unit string.
625
- title: Plot title or None.
626
- """
627
- axes[-1].set_xlabel(f"Time ({time_unit})", fontsize=11)
628
- axes[-1].set_xlim(t_min * time_mult, t_max * time_mult)
629
-
630
- if title:
631
- fig.suptitle(title, fontsize=14, y=0.98)
632
-
633
- fig.tight_layout()
634
-
635
-
636
- def _plot_packet_row(
637
- ax: Axes,
638
- packets: list[ProtocolPacket],
639
- t_min: float,
640
- t_max: float,
641
- time_mult: float,
642
- show_errors: bool,
643
- ) -> None:
644
- """Plot a single row of packets on the given axes."""
645
- for packet in packets:
646
- if packet.timestamp < t_min or packet.timestamp > t_max:
647
- continue
648
-
649
- start = packet.timestamp * time_mult
650
- end = (
651
- packet.end_timestamp if packet.end_timestamp else packet.timestamp + 0.001
652
- ) * time_mult
653
-
654
- # Determine packet color
655
- if show_errors and packet.errors:
656
- color = "#ff6b6b" # Red for errors
657
- else:
658
- color = "#4ecdc4" # Teal
659
-
660
- # Draw packet rectangle
661
- rect = patches.Rectangle(
662
- (start, 0.1),
663
- end - start,
664
- 0.8,
665
- facecolor=color,
666
- edgecolor="black",
667
- linewidth=0.8,
668
- alpha=0.7,
669
- )
670
- ax.add_patch(rect)
671
-
672
- # Add data annotation
673
- if packet.data:
674
- data_str = _format_packet_data(packet)
675
- mid_time = (start + end) / 2
676
- ax.text(
677
- mid_time,
678
- 0.5,
679
- data_str,
680
- ha="center",
681
- va="center",
682
- fontsize=7,
683
- fontweight="bold",
684
- color="white" if not (show_errors and packet.errors) else "black",
685
- )
686
-
687
- # Add error markers
688
- if show_errors and packet.errors:
689
- ax.plot(start, 1.1, "rx", markersize=6, markeredgewidth=2)
690
-
691
- ax.set_ylim(0, 1.2)
692
- ax.set_yticks([])
693
- ax.grid(True, axis="x", alpha=0.3, linestyle=":")
694
-
695
-
696
- def plot_spi_decode(
697
- packets: list[ProtocolPacket],
698
- *,
699
- clk_trace: DigitalTrace | None = None,
700
- mosi_trace: DigitalTrace | None = None,
701
- miso_trace: DigitalTrace | None = None,
702
- cs_trace: DigitalTrace | None = None,
703
- time_range: tuple[float, float] | None = None,
704
- time_unit: str = "auto",
705
- show_mosi: bool = True,
706
- show_miso: bool = True,
707
- figsize: tuple[float, float] | None = None,
708
- title: str = "SPI Transaction",
709
- ) -> Figure:
710
- """Plot SPI decoded packets with CLK, MOSI, MISO, CS signals.
711
-
712
- Specialized visualization for SPI showing all relevant signals
713
- and decoded words on MOSI/MISO channels.
714
-
715
- Args:
716
- packets: List of SPI packets.
717
- clk_trace: Optional clock signal trace.
718
- mosi_trace: Optional MOSI (Master Out Slave In) trace.
719
- miso_trace: Optional MISO (Master In Slave Out) trace.
720
- cs_trace: Optional chip select trace.
721
- time_range: Time range to plot (t_min, t_max) in seconds.
722
- time_unit: Time unit for x-axis.
723
- show_mosi: Show MOSI decoded data.
724
- show_miso: Show MISO decoded data.
725
- figsize: Figure size.
726
- title: Plot title.
727
-
728
- Returns:
729
- Matplotlib Figure object.
730
-
731
- Raises:
732
- ImportError: If matplotlib is not installed.
733
- ValueError: If packets list is empty.
734
-
735
- Example:
736
- >>> decoder = SPIDecoder(cpol=0, cpha=0, word_size=8)
737
- >>> packets = list(decoder.decode(clk=clk, mosi=mosi, miso=miso))
738
- >>> fig = plot_spi_decode(packets, clk_trace=clk, mosi_trace=mosi)
739
- """
740
- if not HAS_MATPLOTLIB:
741
- raise ImportError("matplotlib is required for visualization")
742
-
743
- if len(packets) == 0:
744
- raise ValueError("packets list cannot be empty")
745
-
746
- # If we have multiple traces, create multi-channel visualization
747
- traces_available = sum(
748
- 1 for t in [clk_trace, mosi_trace, miso_trace, cs_trace] if t is not None
749
- )
750
-
751
- if traces_available >= 2:
752
- return _plot_multi_channel_spi(
753
- packets,
754
- clk_trace=clk_trace,
755
- mosi_trace=mosi_trace,
756
- miso_trace=miso_trace,
757
- cs_trace=cs_trace,
758
- time_range=time_range,
759
- time_unit=time_unit,
760
- show_mosi=show_mosi,
761
- show_miso=show_miso,
762
- figsize=figsize,
763
- title=title,
764
- )
765
-
766
- # Single-channel view using generic decode plot
767
- return plot_protocol_decode(
768
- packets,
769
- trace=mosi_trace,
770
- trace_channel="MOSI",
771
- time_range=time_range,
772
- time_unit=time_unit,
773
- figsize=figsize,
774
- title=title,
775
- )
776
-
777
-
778
- def _plot_multi_channel_spi(
779
- packets: list[ProtocolPacket],
780
- *,
781
- clk_trace: DigitalTrace | None = None,
782
- mosi_trace: DigitalTrace | None = None,
783
- miso_trace: DigitalTrace | None = None,
784
- cs_trace: DigitalTrace | None = None,
785
- time_range: tuple[float, float] | None = None,
786
- time_unit: str = "auto",
787
- show_mosi: bool = True,
788
- show_miso: bool = True,
789
- figsize: tuple[float, float] | None = None,
790
- title: str = "SPI Transaction",
791
- ) -> Figure:
792
- """Create multi-channel SPI visualization with separate rows for each signal.
793
-
794
- Args:
795
- packets: List of SPI packets.
796
- clk_trace: Optional clock signal trace.
797
- mosi_trace: Optional MOSI trace.
798
- miso_trace: Optional MISO trace.
799
- cs_trace: Optional chip select trace.
800
- time_range: Time range to plot.
801
- time_unit: Time unit for x-axis.
802
- show_mosi: Show MOSI decoded data row.
803
- show_miso: Show MISO decoded data row.
804
- figsize: Figure size.
805
- title: Plot title.
806
-
807
- Returns:
808
- Matplotlib Figure object.
809
- """
810
- t_min, t_max, time_mult, time_unit = _determine_time_params(packets, time_range, time_unit)
811
- rows = _build_spi_row_list(cs_trace, clk_trace, mosi_trace, miso_trace, show_mosi, show_miso)
812
-
813
- if len(rows) == 0:
814
- return plot_protocol_decode(
815
- packets, time_range=(t_min, t_max), time_unit=time_unit, figsize=figsize, title=title
816
- )
817
-
818
- fig, axes = _create_spi_figure(rows, figsize)
819
- mosi_packets, miso_packets = _separate_spi_packets(packets)
820
- _render_spi_rows(axes, rows, t_min, t_max, time_mult, mosi_packets, miso_packets)
821
- _finalize_spi_plot(axes, t_min, t_max, time_mult, time_unit, title)
822
-
823
- return fig
824
-
825
-
826
- def _determine_time_params(
827
- packets: list[ProtocolPacket],
828
- time_range: tuple[float, float] | None,
829
- time_unit: str,
830
- ) -> tuple[float, float, float, str]:
831
- """Determine time range and multiplier for SPI plot.
832
-
833
- Args:
834
- packets: List of SPI packets for time range calculation.
835
- time_range: User-specified time range or None for auto.
836
- time_unit: Time unit ("auto" or specific unit).
837
-
838
- Returns:
839
- Tuple of (t_min, t_max, time_mult, time_unit).
840
- """
841
- if time_range is None:
842
- t_min = min(p.timestamp for p in packets)
843
- t_max = max(p.end_timestamp if p.end_timestamp else p.timestamp for p in packets)
844
- padding = (t_max - t_min) * 0.1
845
- t_min -= padding
846
- t_max += padding
847
- else:
848
- t_min, t_max = time_range
849
-
850
- if time_unit == "auto":
851
- time_range_val = t_max - t_min
852
- if time_range_val < 1e-6:
853
- time_unit = "ns"
854
- time_mult = 1e9
855
- elif time_range_val < 1e-3:
856
- time_unit = "us"
857
- time_mult = 1e6
858
- elif time_range_val < 1:
859
- time_unit = "ms"
860
- time_mult = 1e3
861
- else:
862
- time_unit = "s"
863
- time_mult = 1.0
864
- else:
865
- time_mult = {"s": 1.0, "ms": 1e3, "us": 1e6, "ns": 1e9}.get(time_unit, 1.0)
866
-
867
- return t_min, t_max, time_mult, time_unit
868
-
869
-
870
- def _build_spi_row_list(
871
- cs_trace: DigitalTrace | None,
872
- clk_trace: DigitalTrace | None,
873
- mosi_trace: DigitalTrace | None,
874
- miso_trace: DigitalTrace | None,
875
- show_mosi: bool,
876
- show_miso: bool,
877
- ) -> list[dict[str, Any]]:
878
- """Build list of row specifications for SPI multi-channel plot.
879
-
880
- Args:
881
- cs_trace: Chip select trace.
882
- clk_trace: Clock trace.
883
- mosi_trace: MOSI trace.
884
- miso_trace: MISO trace.
885
- show_mosi: Whether to show MOSI data row.
886
- show_miso: Whether to show MISO data row.
887
-
888
- Returns:
889
- List of row dictionaries specifying type, trace, label, channel.
890
- """
891
- rows: list[dict[str, Any]] = []
892
-
893
- if cs_trace is not None:
894
- rows.append({"type": "waveform", "trace": cs_trace, "label": "CS"})
895
- if clk_trace is not None:
896
- rows.append({"type": "waveform", "trace": clk_trace, "label": "CLK"})
897
- if mosi_trace is not None:
898
- rows.append({"type": "waveform", "trace": mosi_trace, "label": "MOSI"})
899
- if show_mosi:
900
- rows.append({"type": "packets", "label": "MOSI\nData", "channel": "MOSI"})
901
- if miso_trace is not None:
902
- rows.append({"type": "waveform", "trace": miso_trace, "label": "MISO"})
903
- if show_miso:
904
- rows.append({"type": "packets", "label": "MISO\nData", "channel": "MISO"})
905
-
906
- return rows
907
-
908
-
909
- def _create_spi_figure(
910
- rows: list[dict[str, Any]],
911
- figsize: tuple[float, float] | None,
912
- ) -> tuple[Figure, list[Axes]]:
913
- """Create matplotlib figure and axes for SPI plot.
914
-
915
- Args:
916
- rows: Row specifications from _build_spi_row_list.
917
- figsize: Figure size or None for auto-calculation.
918
-
919
- Returns:
920
- Tuple of (figure, axes_list).
921
- """
922
- height_ratios = [1.0 if row["type"] == "waveform" else 0.6 for row in rows]
923
- n_rows = len(rows)
924
-
925
- if figsize is None:
926
- width = 14
927
- height = max(4, sum(height_ratios) * 1.2 + 1)
928
- figsize = (width, height)
929
-
930
- fig, axes = plt.subplots(
931
- n_rows,
932
- 1,
933
- figsize=figsize,
934
- sharex=True,
935
- gridspec_kw={"hspace": 0.1, "height_ratios": height_ratios},
936
- )
937
- if n_rows == 1:
938
- axes = [axes]
939
-
940
- return fig, axes
941
-
942
-
943
- def _separate_spi_packets(
944
- packets: list[ProtocolPacket],
945
- ) -> tuple[list[ProtocolPacket], list[ProtocolPacket]]:
946
- """Separate packets by channel (MOSI vs MISO).
947
-
948
- Args:
949
- packets: List of SPI packets.
950
-
951
- Returns:
952
- Tuple of (mosi_packets, miso_packets).
953
- """
954
- mosi_packets = []
955
- miso_packets = []
956
-
957
- for packet in packets:
958
- channel = getattr(packet, "channel", None)
959
- if channel is None and hasattr(packet, "metadata"):
960
- channel = packet.metadata.get("channel") if isinstance(packet.metadata, dict) else None
961
-
962
- if channel == "MISO":
963
- miso_packets.append(packet)
964
- else:
965
- mosi_packets.append(packet)
966
-
967
- if not mosi_packets and not miso_packets:
968
- mosi_packets = packets
969
-
970
- return mosi_packets, miso_packets
971
-
972
-
973
- def _render_spi_rows(
974
- axes: list[Axes],
975
- rows: list[dict[str, Any]],
976
- t_min: float,
977
- t_max: float,
978
- time_mult: float,
979
- mosi_packets: list[ProtocolPacket],
980
- miso_packets: list[ProtocolPacket],
981
- ) -> None:
982
- """Render all SPI plot rows (waveforms and packet data).
983
-
984
- Args:
985
- axes: List of matplotlib axes.
986
- rows: Row specifications.
987
- t_min: Minimum time value.
988
- t_max: Maximum time value.
989
- time_mult: Time multiplier for unit conversion.
990
- mosi_packets: MOSI channel packets.
991
- miso_packets: MISO channel packets.
992
- """
993
- for ax, row in zip(axes, rows, strict=False):
994
- if row["type"] == "waveform":
995
- _render_spi_waveform_row(ax, row, t_min, t_max, time_mult)
996
- else:
997
- _render_spi_packet_row(ax, row, t_min, t_max, time_mult, mosi_packets, miso_packets)
998
-
999
-
1000
- def _render_spi_waveform_row(
1001
- ax: Axes,
1002
- row: dict[str, Any],
1003
- t_min: float,
1004
- t_max: float,
1005
- time_mult: float,
1006
- ) -> None:
1007
- """Render a single waveform row for SPI plot.
1008
-
1009
- Args:
1010
- ax: Matplotlib axis to render on.
1011
- row: Row specification with trace and label.
1012
- t_min: Minimum time value.
1013
- t_max: Maximum time value.
1014
- time_mult: Time multiplier for unit conversion.
1015
- """
1016
- trace = row["trace"]
1017
- trace_time = trace.time_vector * time_mult
1018
- trace_data = trace.data.astype(float)
1019
- mask = (trace_time >= t_min * time_mult) & (trace_time <= t_max * time_mult)
1020
- _plot_digital_waveform(ax, trace_time[mask], trace_data[mask])
1021
- ax.set_ylabel(row["label"], rotation=0, ha="right", va="center", fontsize=10)
1022
- ax.set_ylim(-0.2, 1.3)
1023
- ax.set_yticks([])
1024
- ax.grid(True, axis="x", alpha=0.3, linestyle=":")
1025
-
1026
-
1027
- def _render_spi_packet_row(
1028
- ax: Axes,
1029
- row: dict[str, Any],
1030
- t_min: float,
1031
- t_max: float,
1032
- time_mult: float,
1033
- mosi_packets: list[ProtocolPacket],
1034
- miso_packets: list[ProtocolPacket],
1035
- ) -> None:
1036
- """Render a single packet data row for SPI plot.
1037
-
1038
- Args:
1039
- ax: Matplotlib axis to render on.
1040
- row: Row specification with channel and label.
1041
- t_min: Minimum time value.
1042
- t_max: Maximum time value.
1043
- time_mult: Time multiplier for unit conversion.
1044
- mosi_packets: MOSI channel packets.
1045
- miso_packets: MISO channel packets.
1046
- """
1047
- channel = row.get("channel", "MOSI")
1048
- pkts = mosi_packets if channel == "MOSI" else miso_packets
1049
- _plot_packet_row(ax, pkts, t_min, t_max, time_mult, show_errors=True)
1050
- ax.set_ylabel(row["label"], rotation=0, ha="right", va="center", fontsize=9)
1051
-
1052
-
1053
- def _finalize_spi_plot(
1054
- axes: list[Axes],
1055
- t_min: float,
1056
- t_max: float,
1057
- time_mult: float,
1058
- time_unit: str,
1059
- title: str | None,
1060
- ) -> None:
1061
- """Add final formatting to SPI plot.
1062
-
1063
- Args:
1064
- axes: List of matplotlib axes.
1065
- t_min: Minimum time value.
1066
- t_max: Maximum time value.
1067
- time_mult: Time multiplier for unit conversion.
1068
- time_unit: Time unit string for label.
1069
- title: Plot title or None.
1070
- """
1071
- axes[-1].set_xlabel(f"Time ({time_unit})", fontsize=11)
1072
- axes[-1].set_xlim(t_min * time_mult, t_max * time_mult)
1073
-
1074
- fig = axes[0].get_figure()
1075
- if title and fig is not None:
1076
- fig.suptitle(title, fontsize=14, y=0.98)
1077
-
1078
- if fig is not None and hasattr(fig, "tight_layout"):
1079
- fig.tight_layout()
1080
-
1081
-
1082
- def plot_i2c_decode(
1083
- packets: list[ProtocolPacket],
1084
- *,
1085
- sda_trace: DigitalTrace | None = None,
1086
- scl_trace: DigitalTrace | None = None,
1087
- time_range: tuple[float, float] | None = None,
1088
- time_unit: str = "auto",
1089
- show_addresses: bool = True,
1090
- show_ack_nack: bool = True,
1091
- figsize: tuple[float, float] | None = None,
1092
- title: str = "I2C Transaction",
1093
- ) -> Figure:
1094
- """Plot I2C decoded packets with SDA/SCL and address annotations.
1095
-
1096
- Specialized visualization for I2C showing start/stop conditions,
1097
- addresses, data bytes, and ACK/NACK bits.
1098
-
1099
- Args:
1100
- packets: List of I2C packets.
1101
- sda_trace: Optional SDA (data) signal trace.
1102
- scl_trace: Optional SCL (clock) signal trace.
1103
- time_range: Time range to plot (t_min, t_max) in seconds.
1104
- time_unit: Time unit for x-axis.
1105
- show_addresses: Highlight address bytes.
1106
- show_ack_nack: Show ACK/NACK indicators.
1107
- figsize: Figure size.
1108
- title: Plot title.
1109
-
1110
- Returns:
1111
- Matplotlib Figure object.
1112
-
1113
- Example:
1114
- >>> decoder = I2CDecoder()
1115
- >>> packets = list(decoder.decode(sda=sda, scl=scl))
1116
- >>> fig = plot_i2c_decode(packets, sda_trace=sda, scl_trace=scl)
1117
- """
1118
- return plot_protocol_decode(
1119
- packets,
1120
- trace=sda_trace,
1121
- trace_channel="SDA",
1122
- time_range=time_range,
1123
- time_unit=time_unit,
1124
- figsize=figsize,
1125
- title=title,
1126
- )
1127
-
1128
-
1129
- def plot_can_decode(
1130
- packets: list[ProtocolPacket],
1131
- *,
1132
- can_trace: DigitalTrace | None = None,
1133
- time_range: tuple[float, float] | None = None,
1134
- time_unit: str = "auto",
1135
- show_ids: bool = True,
1136
- show_data_length: bool = True,
1137
- colorize_by_id: bool = True,
1138
- figsize: tuple[float, float] | None = None,
1139
- title: str = "CAN Bus",
1140
- ) -> Figure:
1141
- """Plot CAN decoded packets with arbitration IDs and data.
1142
-
1143
- Specialized visualization for CAN bus showing arbitration IDs,
1144
- data length codes, and message data.
1145
-
1146
- Args:
1147
- packets: List of CAN packets.
1148
- can_trace: Optional CAN bus trace.
1149
- time_range: Time range to plot (t_min, t_max) in seconds.
1150
- time_unit: Time unit for x-axis.
1151
- show_ids: Show arbitration IDs in annotations.
1152
- show_data_length: Show DLC (Data Length Code).
1153
- colorize_by_id: Use different colors for different CAN IDs.
1154
- figsize: Figure size.
1155
- title: Plot title.
1156
-
1157
- Returns:
1158
- Matplotlib Figure object.
1159
-
1160
- Example:
1161
- >>> decoder = CANDecoder()
1162
- >>> packets = list(decoder.decode(can_trace))
1163
- >>> fig = plot_can_decode(packets, can_trace=can_trace, colorize_by_id=True)
1164
- """
1165
- return plot_protocol_decode(
1166
- packets,
1167
- trace=can_trace,
1168
- trace_channel="CAN",
1169
- colorize=colorize_by_id,
1170
- time_range=time_range,
1171
- time_unit=time_unit,
1172
- figsize=figsize,
1173
- title=title,
1174
- )
1175
-
1176
-
1177
- def _plot_digital_waveform(
1178
- ax: Axes,
1179
- time: NDArray[np.float64],
1180
- data: NDArray[np.float64],
1181
- ) -> None:
1182
- """Plot digital waveform with clean transitions."""
1183
- for i in range(len(time) - 1):
1184
- level = 1 if data[i] > 0.5 else 0
1185
- # Horizontal line
1186
- ax.plot(
1187
- [time[i], time[i + 1]],
1188
- [level, level],
1189
- "b-",
1190
- linewidth=1.5,
1191
- )
1192
- # Vertical transition
1193
- if i < len(time) - 1:
1194
- next_level = 1 if data[i + 1] > 0.5 else 0
1195
- if level != next_level:
1196
- ax.plot(
1197
- [time[i + 1], time[i + 1]],
1198
- [level, next_level],
1199
- "b-",
1200
- linewidth=1.5,
1201
- )
1202
-
1203
-
1204
- def _get_packet_color(packet: ProtocolPacket, protocol: str) -> str:
1205
- """Get color for packet based on protocol and type."""
1206
- # Color palette for different protocols
1207
- colors = {
1208
- "UART": "#4ecdc4", # Teal
1209
- "SPI": "#95e1d3", # Mint
1210
- "I2C": "#f38181", # Coral
1211
- "CAN": "#aa96da", # Purple
1212
- "USB": "#fcbad3", # Pink
1213
- "1-Wire": "#ffffd2", # Yellow
1214
- }
1215
-
1216
- return colors.get(protocol, "#4ecdc4")
1217
-
1218
-
1219
- def _format_packet_data(packet: ProtocolPacket) -> str:
1220
- """Format packet data for display."""
1221
- if len(packet.data) == 0:
1222
- return ""
1223
-
1224
- # For single byte, show as hex
1225
- if len(packet.data) == 1:
1226
- byte_val = packet.data[0]
1227
- # Show both hex and ASCII if printable
1228
- if 32 <= byte_val <= 126:
1229
- return f"0x{byte_val:02X} '{chr(byte_val)}'"
1230
- return f"0x{byte_val:02X}"
1231
-
1232
- # For multiple bytes, show hex string (limit to first few bytes)
1233
- if len(packet.data) <= 4:
1234
- return " ".join(f"{b:02X}" for b in packet.data)
1235
-
1236
- # For longer data, truncate
1237
- return " ".join(f"{b:02X}" for b in packet.data[:3]) + "..."
1238
-
1239
-
1240
- __all__ = [
1241
- "plot_can_decode",
1242
- "plot_i2c_decode",
1243
- "plot_protocol_decode",
1244
- "plot_spi_decode",
1245
- "plot_uart_decode",
1246
- ]