oscura 0.0.1__py3-none-any.whl → 0.1.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 +813 -8
- oscura/__main__.py +392 -0
- oscura/analyzers/__init__.py +37 -0
- oscura/analyzers/digital/__init__.py +177 -0
- oscura/analyzers/digital/bus.py +691 -0
- oscura/analyzers/digital/clock.py +805 -0
- oscura/analyzers/digital/correlation.py +720 -0
- oscura/analyzers/digital/edges.py +632 -0
- oscura/analyzers/digital/extraction.py +413 -0
- oscura/analyzers/digital/quality.py +878 -0
- oscura/analyzers/digital/signal_quality.py +877 -0
- oscura/analyzers/digital/thresholds.py +708 -0
- oscura/analyzers/digital/timing.py +1104 -0
- oscura/analyzers/eye/__init__.py +46 -0
- oscura/analyzers/eye/diagram.py +434 -0
- oscura/analyzers/eye/metrics.py +555 -0
- oscura/analyzers/jitter/__init__.py +83 -0
- oscura/analyzers/jitter/ber.py +333 -0
- oscura/analyzers/jitter/decomposition.py +759 -0
- oscura/analyzers/jitter/measurements.py +413 -0
- oscura/analyzers/jitter/spectrum.py +220 -0
- oscura/analyzers/measurements.py +40 -0
- oscura/analyzers/packet/__init__.py +171 -0
- oscura/analyzers/packet/daq.py +1077 -0
- oscura/analyzers/packet/metrics.py +437 -0
- oscura/analyzers/packet/parser.py +327 -0
- oscura/analyzers/packet/payload.py +2156 -0
- oscura/analyzers/packet/payload_analysis.py +1312 -0
- oscura/analyzers/packet/payload_extraction.py +236 -0
- oscura/analyzers/packet/payload_patterns.py +670 -0
- oscura/analyzers/packet/stream.py +359 -0
- oscura/analyzers/patterns/__init__.py +266 -0
- oscura/analyzers/patterns/clustering.py +1036 -0
- oscura/analyzers/patterns/discovery.py +539 -0
- oscura/analyzers/patterns/learning.py +797 -0
- oscura/analyzers/patterns/matching.py +1091 -0
- oscura/analyzers/patterns/periodic.py +650 -0
- oscura/analyzers/patterns/sequences.py +767 -0
- oscura/analyzers/power/__init__.py +116 -0
- oscura/analyzers/power/ac_power.py +391 -0
- oscura/analyzers/power/basic.py +383 -0
- oscura/analyzers/power/conduction.py +314 -0
- oscura/analyzers/power/efficiency.py +297 -0
- oscura/analyzers/power/ripple.py +356 -0
- oscura/analyzers/power/soa.py +372 -0
- oscura/analyzers/power/switching.py +479 -0
- oscura/analyzers/protocol/__init__.py +150 -0
- oscura/analyzers/protocols/__init__.py +150 -0
- oscura/analyzers/protocols/base.py +500 -0
- oscura/analyzers/protocols/can.py +620 -0
- oscura/analyzers/protocols/can_fd.py +448 -0
- oscura/analyzers/protocols/flexray.py +405 -0
- oscura/analyzers/protocols/hdlc.py +399 -0
- oscura/analyzers/protocols/i2c.py +368 -0
- oscura/analyzers/protocols/i2s.py +296 -0
- oscura/analyzers/protocols/jtag.py +393 -0
- oscura/analyzers/protocols/lin.py +445 -0
- oscura/analyzers/protocols/manchester.py +333 -0
- oscura/analyzers/protocols/onewire.py +501 -0
- oscura/analyzers/protocols/spi.py +334 -0
- oscura/analyzers/protocols/swd.py +325 -0
- oscura/analyzers/protocols/uart.py +393 -0
- oscura/analyzers/protocols/usb.py +495 -0
- oscura/analyzers/signal_integrity/__init__.py +63 -0
- oscura/analyzers/signal_integrity/embedding.py +294 -0
- oscura/analyzers/signal_integrity/equalization.py +370 -0
- oscura/analyzers/signal_integrity/sparams.py +484 -0
- oscura/analyzers/spectral/__init__.py +53 -0
- oscura/analyzers/spectral/chunked.py +273 -0
- oscura/analyzers/spectral/chunked_fft.py +571 -0
- oscura/analyzers/spectral/chunked_wavelet.py +391 -0
- oscura/analyzers/spectral/fft.py +92 -0
- oscura/analyzers/statistical/__init__.py +250 -0
- oscura/analyzers/statistical/checksum.py +923 -0
- oscura/analyzers/statistical/chunked_corr.py +228 -0
- oscura/analyzers/statistical/classification.py +778 -0
- oscura/analyzers/statistical/entropy.py +1113 -0
- oscura/analyzers/statistical/ngrams.py +614 -0
- oscura/analyzers/statistics/__init__.py +119 -0
- oscura/analyzers/statistics/advanced.py +885 -0
- oscura/analyzers/statistics/basic.py +263 -0
- oscura/analyzers/statistics/correlation.py +630 -0
- oscura/analyzers/statistics/distribution.py +298 -0
- oscura/analyzers/statistics/outliers.py +463 -0
- oscura/analyzers/statistics/streaming.py +93 -0
- oscura/analyzers/statistics/trend.py +520 -0
- oscura/analyzers/validation.py +598 -0
- oscura/analyzers/waveform/__init__.py +36 -0
- oscura/analyzers/waveform/measurements.py +943 -0
- oscura/analyzers/waveform/measurements_with_uncertainty.py +371 -0
- oscura/analyzers/waveform/spectral.py +1689 -0
- oscura/analyzers/waveform/wavelets.py +298 -0
- oscura/api/__init__.py +62 -0
- oscura/api/dsl.py +538 -0
- oscura/api/fluent.py +571 -0
- oscura/api/operators.py +498 -0
- oscura/api/optimization.py +392 -0
- oscura/api/profiling.py +396 -0
- oscura/automotive/__init__.py +73 -0
- oscura/automotive/can/__init__.py +52 -0
- oscura/automotive/can/analysis.py +356 -0
- oscura/automotive/can/checksum.py +250 -0
- oscura/automotive/can/correlation.py +212 -0
- oscura/automotive/can/discovery.py +355 -0
- oscura/automotive/can/message_wrapper.py +375 -0
- oscura/automotive/can/models.py +385 -0
- oscura/automotive/can/patterns.py +381 -0
- oscura/automotive/can/session.py +452 -0
- oscura/automotive/can/state_machine.py +300 -0
- oscura/automotive/can/stimulus_response.py +461 -0
- oscura/automotive/dbc/__init__.py +15 -0
- oscura/automotive/dbc/generator.py +156 -0
- oscura/automotive/dbc/parser.py +146 -0
- oscura/automotive/dtc/__init__.py +30 -0
- oscura/automotive/dtc/database.py +3036 -0
- oscura/automotive/j1939/__init__.py +14 -0
- oscura/automotive/j1939/decoder.py +745 -0
- oscura/automotive/loaders/__init__.py +35 -0
- oscura/automotive/loaders/asc.py +98 -0
- oscura/automotive/loaders/blf.py +77 -0
- oscura/automotive/loaders/csv_can.py +136 -0
- oscura/automotive/loaders/dispatcher.py +136 -0
- oscura/automotive/loaders/mdf.py +331 -0
- oscura/automotive/loaders/pcap.py +132 -0
- oscura/automotive/obd/__init__.py +14 -0
- oscura/automotive/obd/decoder.py +707 -0
- oscura/automotive/uds/__init__.py +48 -0
- oscura/automotive/uds/decoder.py +265 -0
- oscura/automotive/uds/models.py +64 -0
- oscura/automotive/visualization.py +369 -0
- oscura/batch/__init__.py +55 -0
- oscura/batch/advanced.py +627 -0
- oscura/batch/aggregate.py +300 -0
- oscura/batch/analyze.py +139 -0
- oscura/batch/logging.py +487 -0
- oscura/batch/metrics.py +556 -0
- oscura/builders/__init__.py +41 -0
- oscura/builders/signal_builder.py +1131 -0
- oscura/cli/__init__.py +14 -0
- oscura/cli/batch.py +339 -0
- oscura/cli/characterize.py +273 -0
- oscura/cli/compare.py +775 -0
- oscura/cli/decode.py +551 -0
- oscura/cli/main.py +247 -0
- oscura/cli/shell.py +350 -0
- oscura/comparison/__init__.py +66 -0
- oscura/comparison/compare.py +397 -0
- oscura/comparison/golden.py +487 -0
- oscura/comparison/limits.py +391 -0
- oscura/comparison/mask.py +434 -0
- oscura/comparison/trace_diff.py +30 -0
- oscura/comparison/visualization.py +481 -0
- oscura/compliance/__init__.py +70 -0
- oscura/compliance/advanced.py +756 -0
- oscura/compliance/masks.py +363 -0
- oscura/compliance/reporting.py +483 -0
- oscura/compliance/testing.py +298 -0
- oscura/component/__init__.py +38 -0
- oscura/component/impedance.py +365 -0
- oscura/component/reactive.py +598 -0
- oscura/component/transmission_line.py +312 -0
- oscura/config/__init__.py +191 -0
- oscura/config/defaults.py +254 -0
- oscura/config/loader.py +348 -0
- oscura/config/memory.py +271 -0
- oscura/config/migration.py +458 -0
- oscura/config/pipeline.py +1077 -0
- oscura/config/preferences.py +530 -0
- oscura/config/protocol.py +875 -0
- oscura/config/schema.py +713 -0
- oscura/config/settings.py +420 -0
- oscura/config/thresholds.py +599 -0
- oscura/convenience.py +457 -0
- oscura/core/__init__.py +299 -0
- oscura/core/audit.py +457 -0
- oscura/core/backend_selector.py +405 -0
- oscura/core/cache.py +590 -0
- oscura/core/cancellation.py +439 -0
- oscura/core/confidence.py +225 -0
- oscura/core/config.py +506 -0
- oscura/core/correlation.py +216 -0
- oscura/core/cross_domain.py +422 -0
- oscura/core/debug.py +301 -0
- oscura/core/edge_cases.py +541 -0
- oscura/core/exceptions.py +535 -0
- oscura/core/gpu_backend.py +523 -0
- oscura/core/lazy.py +832 -0
- oscura/core/log_query.py +540 -0
- oscura/core/logging.py +931 -0
- oscura/core/logging_advanced.py +952 -0
- oscura/core/memoize.py +171 -0
- oscura/core/memory_check.py +274 -0
- oscura/core/memory_guard.py +290 -0
- oscura/core/memory_limits.py +336 -0
- oscura/core/memory_monitor.py +453 -0
- oscura/core/memory_progress.py +465 -0
- oscura/core/memory_warnings.py +315 -0
- oscura/core/numba_backend.py +362 -0
- oscura/core/performance.py +352 -0
- oscura/core/progress.py +524 -0
- oscura/core/provenance.py +358 -0
- oscura/core/results.py +331 -0
- oscura/core/types.py +504 -0
- oscura/core/uncertainty.py +383 -0
- oscura/discovery/__init__.py +52 -0
- oscura/discovery/anomaly_detector.py +672 -0
- oscura/discovery/auto_decoder.py +415 -0
- oscura/discovery/comparison.py +497 -0
- oscura/discovery/quality_validator.py +528 -0
- oscura/discovery/signal_detector.py +769 -0
- oscura/dsl/__init__.py +73 -0
- oscura/dsl/commands.py +246 -0
- oscura/dsl/interpreter.py +455 -0
- oscura/dsl/parser.py +689 -0
- oscura/dsl/repl.py +172 -0
- oscura/exceptions.py +59 -0
- oscura/exploratory/__init__.py +111 -0
- oscura/exploratory/error_recovery.py +642 -0
- oscura/exploratory/fuzzy.py +513 -0
- oscura/exploratory/fuzzy_advanced.py +786 -0
- oscura/exploratory/legacy.py +831 -0
- oscura/exploratory/parse.py +358 -0
- oscura/exploratory/recovery.py +275 -0
- oscura/exploratory/sync.py +382 -0
- oscura/exploratory/unknown.py +707 -0
- oscura/export/__init__.py +25 -0
- oscura/export/wireshark/README.md +265 -0
- oscura/export/wireshark/__init__.py +47 -0
- oscura/export/wireshark/generator.py +312 -0
- oscura/export/wireshark/lua_builder.py +159 -0
- oscura/export/wireshark/templates/dissector.lua.j2 +92 -0
- oscura/export/wireshark/type_mapping.py +165 -0
- oscura/export/wireshark/validator.py +105 -0
- oscura/exporters/__init__.py +94 -0
- oscura/exporters/csv.py +303 -0
- oscura/exporters/exporters.py +44 -0
- oscura/exporters/hdf5.py +219 -0
- oscura/exporters/html_export.py +701 -0
- oscura/exporters/json_export.py +291 -0
- oscura/exporters/markdown_export.py +367 -0
- oscura/exporters/matlab_export.py +354 -0
- oscura/exporters/npz_export.py +219 -0
- oscura/exporters/spice_export.py +210 -0
- oscura/extensibility/__init__.py +131 -0
- oscura/extensibility/docs.py +752 -0
- oscura/extensibility/extensions.py +1125 -0
- oscura/extensibility/logging.py +259 -0
- oscura/extensibility/measurements.py +485 -0
- oscura/extensibility/plugins.py +414 -0
- oscura/extensibility/registry.py +346 -0
- oscura/extensibility/templates.py +913 -0
- oscura/extensibility/validation.py +651 -0
- oscura/filtering/__init__.py +89 -0
- oscura/filtering/base.py +563 -0
- oscura/filtering/convenience.py +564 -0
- oscura/filtering/design.py +725 -0
- oscura/filtering/filters.py +32 -0
- oscura/filtering/introspection.py +605 -0
- oscura/guidance/__init__.py +24 -0
- oscura/guidance/recommender.py +429 -0
- oscura/guidance/wizard.py +518 -0
- oscura/inference/__init__.py +251 -0
- oscura/inference/active_learning/README.md +153 -0
- oscura/inference/active_learning/__init__.py +38 -0
- oscura/inference/active_learning/lstar.py +257 -0
- oscura/inference/active_learning/observation_table.py +230 -0
- oscura/inference/active_learning/oracle.py +78 -0
- oscura/inference/active_learning/teachers/__init__.py +15 -0
- oscura/inference/active_learning/teachers/simulator.py +192 -0
- oscura/inference/adaptive_tuning.py +453 -0
- oscura/inference/alignment.py +653 -0
- oscura/inference/bayesian.py +943 -0
- oscura/inference/binary.py +1016 -0
- oscura/inference/crc_reverse.py +711 -0
- oscura/inference/logic.py +288 -0
- oscura/inference/message_format.py +1305 -0
- oscura/inference/protocol.py +417 -0
- oscura/inference/protocol_dsl.py +1084 -0
- oscura/inference/protocol_library.py +1230 -0
- oscura/inference/sequences.py +809 -0
- oscura/inference/signal_intelligence.py +1509 -0
- oscura/inference/spectral.py +215 -0
- oscura/inference/state_machine.py +634 -0
- oscura/inference/stream.py +918 -0
- oscura/integrations/__init__.py +59 -0
- oscura/integrations/llm.py +1827 -0
- oscura/jupyter/__init__.py +32 -0
- oscura/jupyter/display.py +268 -0
- oscura/jupyter/magic.py +334 -0
- oscura/loaders/__init__.py +526 -0
- oscura/loaders/binary.py +69 -0
- oscura/loaders/configurable.py +1255 -0
- oscura/loaders/csv.py +26 -0
- oscura/loaders/csv_loader.py +473 -0
- oscura/loaders/hdf5.py +9 -0
- oscura/loaders/hdf5_loader.py +510 -0
- oscura/loaders/lazy.py +370 -0
- oscura/loaders/mmap_loader.py +583 -0
- oscura/loaders/numpy_loader.py +436 -0
- oscura/loaders/pcap.py +432 -0
- oscura/loaders/preprocessing.py +368 -0
- oscura/loaders/rigol.py +287 -0
- oscura/loaders/sigrok.py +321 -0
- oscura/loaders/tdms.py +367 -0
- oscura/loaders/tektronix.py +711 -0
- oscura/loaders/validation.py +584 -0
- oscura/loaders/vcd.py +464 -0
- oscura/loaders/wav.py +233 -0
- oscura/math/__init__.py +45 -0
- oscura/math/arithmetic.py +824 -0
- oscura/math/interpolation.py +413 -0
- oscura/onboarding/__init__.py +39 -0
- oscura/onboarding/help.py +498 -0
- oscura/onboarding/tutorials.py +405 -0
- oscura/onboarding/wizard.py +466 -0
- oscura/optimization/__init__.py +19 -0
- oscura/optimization/parallel.py +440 -0
- oscura/optimization/search.py +532 -0
- oscura/pipeline/__init__.py +43 -0
- oscura/pipeline/base.py +338 -0
- oscura/pipeline/composition.py +242 -0
- oscura/pipeline/parallel.py +448 -0
- oscura/pipeline/pipeline.py +375 -0
- oscura/pipeline/reverse_engineering.py +1119 -0
- oscura/plugins/__init__.py +122 -0
- oscura/plugins/base.py +272 -0
- oscura/plugins/cli.py +497 -0
- oscura/plugins/discovery.py +411 -0
- oscura/plugins/isolation.py +418 -0
- oscura/plugins/lifecycle.py +959 -0
- oscura/plugins/manager.py +493 -0
- oscura/plugins/registry.py +421 -0
- oscura/plugins/versioning.py +372 -0
- oscura/py.typed +0 -0
- oscura/quality/__init__.py +65 -0
- oscura/quality/ensemble.py +740 -0
- oscura/quality/explainer.py +338 -0
- oscura/quality/scoring.py +616 -0
- oscura/quality/warnings.py +456 -0
- oscura/reporting/__init__.py +248 -0
- oscura/reporting/advanced.py +1234 -0
- oscura/reporting/analyze.py +448 -0
- oscura/reporting/argument_preparer.py +596 -0
- oscura/reporting/auto_report.py +507 -0
- oscura/reporting/batch.py +615 -0
- oscura/reporting/chart_selection.py +223 -0
- oscura/reporting/comparison.py +330 -0
- oscura/reporting/config.py +615 -0
- oscura/reporting/content/__init__.py +39 -0
- oscura/reporting/content/executive.py +127 -0
- oscura/reporting/content/filtering.py +191 -0
- oscura/reporting/content/minimal.py +257 -0
- oscura/reporting/content/verbosity.py +162 -0
- oscura/reporting/core.py +508 -0
- oscura/reporting/core_formats/__init__.py +17 -0
- oscura/reporting/core_formats/multi_format.py +210 -0
- oscura/reporting/engine.py +836 -0
- oscura/reporting/export.py +366 -0
- oscura/reporting/formatting/__init__.py +129 -0
- oscura/reporting/formatting/emphasis.py +81 -0
- oscura/reporting/formatting/numbers.py +403 -0
- oscura/reporting/formatting/standards.py +55 -0
- oscura/reporting/formatting.py +466 -0
- oscura/reporting/html.py +578 -0
- oscura/reporting/index.py +590 -0
- oscura/reporting/multichannel.py +296 -0
- oscura/reporting/output.py +379 -0
- oscura/reporting/pdf.py +373 -0
- oscura/reporting/plots.py +731 -0
- oscura/reporting/pptx_export.py +360 -0
- oscura/reporting/renderers/__init__.py +11 -0
- oscura/reporting/renderers/pdf.py +94 -0
- oscura/reporting/sections.py +471 -0
- oscura/reporting/standards.py +680 -0
- oscura/reporting/summary_generator.py +368 -0
- oscura/reporting/tables.py +397 -0
- oscura/reporting/template_system.py +724 -0
- oscura/reporting/templates/__init__.py +15 -0
- oscura/reporting/templates/definition.py +205 -0
- oscura/reporting/templates/index.html +649 -0
- oscura/reporting/templates/index.md +173 -0
- oscura/schemas/__init__.py +158 -0
- oscura/schemas/bus_configuration.json +322 -0
- oscura/schemas/device_mapping.json +182 -0
- oscura/schemas/packet_format.json +418 -0
- oscura/schemas/protocol_definition.json +363 -0
- oscura/search/__init__.py +16 -0
- oscura/search/anomaly.py +292 -0
- oscura/search/context.py +149 -0
- oscura/search/pattern.py +160 -0
- oscura/session/__init__.py +34 -0
- oscura/session/annotations.py +289 -0
- oscura/session/history.py +313 -0
- oscura/session/session.py +445 -0
- oscura/streaming/__init__.py +43 -0
- oscura/streaming/chunked.py +611 -0
- oscura/streaming/progressive.py +393 -0
- oscura/streaming/realtime.py +622 -0
- oscura/testing/__init__.py +54 -0
- oscura/testing/synthetic.py +808 -0
- oscura/triggering/__init__.py +68 -0
- oscura/triggering/base.py +229 -0
- oscura/triggering/edge.py +353 -0
- oscura/triggering/pattern.py +344 -0
- oscura/triggering/pulse.py +581 -0
- oscura/triggering/window.py +453 -0
- oscura/ui/__init__.py +48 -0
- oscura/ui/formatters.py +526 -0
- oscura/ui/progressive_display.py +340 -0
- oscura/utils/__init__.py +99 -0
- oscura/utils/autodetect.py +338 -0
- oscura/utils/buffer.py +389 -0
- oscura/utils/lazy.py +407 -0
- oscura/utils/lazy_imports.py +147 -0
- oscura/utils/memory.py +836 -0
- oscura/utils/memory_advanced.py +1326 -0
- oscura/utils/memory_extensions.py +465 -0
- oscura/utils/progressive.py +352 -0
- oscura/utils/windowing.py +362 -0
- oscura/visualization/__init__.py +321 -0
- oscura/visualization/accessibility.py +526 -0
- oscura/visualization/annotations.py +374 -0
- oscura/visualization/axis_scaling.py +305 -0
- oscura/visualization/colors.py +453 -0
- oscura/visualization/digital.py +337 -0
- oscura/visualization/eye.py +420 -0
- oscura/visualization/histogram.py +281 -0
- oscura/visualization/interactive.py +858 -0
- oscura/visualization/jitter.py +702 -0
- oscura/visualization/keyboard.py +394 -0
- oscura/visualization/layout.py +365 -0
- oscura/visualization/optimization.py +1028 -0
- oscura/visualization/palettes.py +446 -0
- oscura/visualization/plot.py +92 -0
- oscura/visualization/power.py +290 -0
- oscura/visualization/power_extended.py +626 -0
- oscura/visualization/presets.py +467 -0
- oscura/visualization/protocols.py +932 -0
- oscura/visualization/render.py +207 -0
- oscura/visualization/rendering.py +444 -0
- oscura/visualization/reverse_engineering.py +791 -0
- oscura/visualization/signal_integrity.py +808 -0
- oscura/visualization/specialized.py +553 -0
- oscura/visualization/spectral.py +811 -0
- oscura/visualization/styles.py +381 -0
- oscura/visualization/thumbnails.py +311 -0
- oscura/visualization/time_axis.py +351 -0
- oscura/visualization/waveform.py +367 -0
- oscura/workflow/__init__.py +13 -0
- oscura/workflow/dag.py +377 -0
- oscura/workflows/__init__.py +58 -0
- oscura/workflows/compliance.py +280 -0
- oscura/workflows/digital.py +272 -0
- oscura/workflows/multi_trace.py +502 -0
- oscura/workflows/power.py +178 -0
- oscura/workflows/protocol.py +492 -0
- oscura/workflows/reverse_engineering.py +639 -0
- oscura/workflows/signal_integrity.py +227 -0
- oscura-0.1.0.dist-info/METADATA +300 -0
- oscura-0.1.0.dist-info/RECORD +463 -0
- oscura-0.1.0.dist-info/entry_points.txt +2 -0
- {oscura-0.0.1.dist-info → oscura-0.1.0.dist-info}/licenses/LICENSE +1 -1
- oscura-0.0.1.dist-info/METADATA +0 -63
- oscura-0.0.1.dist-info/RECORD +0 -5
- {oscura-0.0.1.dist-info → oscura-0.1.0.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,932 @@
|
|
|
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
|
+
if not HAS_MATPLOTLIB:
|
|
96
|
+
raise ImportError("matplotlib is required for visualization")
|
|
97
|
+
|
|
98
|
+
if len(packets) == 0:
|
|
99
|
+
raise ValueError("packets list cannot be empty")
|
|
100
|
+
|
|
101
|
+
# Determine protocol name from first packet
|
|
102
|
+
protocol = packets[0].protocol
|
|
103
|
+
|
|
104
|
+
# Determine time range
|
|
105
|
+
if time_range is None:
|
|
106
|
+
t_min = min(p.timestamp for p in packets)
|
|
107
|
+
t_max = max(p.end_timestamp if p.end_timestamp else p.timestamp for p in packets)
|
|
108
|
+
# Add 10% padding
|
|
109
|
+
padding = (t_max - t_min) * 0.1
|
|
110
|
+
t_min -= padding
|
|
111
|
+
t_max += padding
|
|
112
|
+
else:
|
|
113
|
+
t_min, t_max = time_range
|
|
114
|
+
|
|
115
|
+
# Select time unit
|
|
116
|
+
if time_unit == "auto":
|
|
117
|
+
time_range_val = t_max - t_min
|
|
118
|
+
if time_range_val < 1e-6:
|
|
119
|
+
time_unit = "ns"
|
|
120
|
+
time_mult = 1e9
|
|
121
|
+
elif time_range_val < 1e-3:
|
|
122
|
+
time_unit = "us"
|
|
123
|
+
time_mult = 1e6
|
|
124
|
+
elif time_range_val < 1:
|
|
125
|
+
time_unit = "ms"
|
|
126
|
+
time_mult = 1e3
|
|
127
|
+
else:
|
|
128
|
+
time_unit = "s"
|
|
129
|
+
time_mult = 1.0
|
|
130
|
+
else:
|
|
131
|
+
time_mult = {"s": 1.0, "ms": 1e3, "us": 1e6, "ns": 1e9}.get(time_unit, 1.0)
|
|
132
|
+
|
|
133
|
+
# Determine number of rows
|
|
134
|
+
n_rows = 1 if trace is None else 2 # Waveform + packets
|
|
135
|
+
|
|
136
|
+
# Auto-calculate figure size
|
|
137
|
+
if figsize is None:
|
|
138
|
+
width = 14
|
|
139
|
+
height = max(4, n_rows * 1.5 + 1)
|
|
140
|
+
figsize = (width, height)
|
|
141
|
+
|
|
142
|
+
fig, axes = plt.subplots(
|
|
143
|
+
n_rows,
|
|
144
|
+
1,
|
|
145
|
+
figsize=figsize,
|
|
146
|
+
sharex=True,
|
|
147
|
+
gridspec_kw={"hspace": 0.15, "height_ratios": [1] * n_rows},
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
if n_rows == 1:
|
|
151
|
+
axes = [axes]
|
|
152
|
+
|
|
153
|
+
ax_idx = 0
|
|
154
|
+
|
|
155
|
+
# Plot waveform if provided
|
|
156
|
+
if trace is not None:
|
|
157
|
+
ax = axes[ax_idx]
|
|
158
|
+
ax_idx += 1
|
|
159
|
+
|
|
160
|
+
# Create time vector for trace
|
|
161
|
+
trace_time = trace.time_vector * time_mult
|
|
162
|
+
trace_data = trace.data.astype(float)
|
|
163
|
+
|
|
164
|
+
# Filter to time range
|
|
165
|
+
mask = (trace_time >= t_min * time_mult) & (trace_time <= t_max * time_mult)
|
|
166
|
+
trace_time = trace_time[mask]
|
|
167
|
+
trace_data = trace_data[mask]
|
|
168
|
+
|
|
169
|
+
# Plot as digital waveform
|
|
170
|
+
_plot_digital_waveform(ax, trace_time, trace_data)
|
|
171
|
+
|
|
172
|
+
channel_name = trace_channel if trace_channel else protocol
|
|
173
|
+
ax.set_ylabel(channel_name, rotation=0, ha="right", va="center", fontsize=10)
|
|
174
|
+
ax.set_ylim(-0.2, 1.3)
|
|
175
|
+
ax.set_yticks([])
|
|
176
|
+
ax.grid(True, axis="x", alpha=0.3, linestyle=":")
|
|
177
|
+
|
|
178
|
+
# Plot packets row
|
|
179
|
+
ax = axes[ax_idx]
|
|
180
|
+
|
|
181
|
+
# Plot packet timeline
|
|
182
|
+
for packet in packets:
|
|
183
|
+
if packet.timestamp < t_min or packet.timestamp > t_max:
|
|
184
|
+
continue
|
|
185
|
+
|
|
186
|
+
start = packet.timestamp * time_mult
|
|
187
|
+
end = (
|
|
188
|
+
packet.end_timestamp if packet.end_timestamp else packet.timestamp + 0.001
|
|
189
|
+
) * time_mult
|
|
190
|
+
|
|
191
|
+
# Determine packet color
|
|
192
|
+
if show_errors and packet.errors:
|
|
193
|
+
color = "#ff6b6b" # Red for errors
|
|
194
|
+
elif colorize:
|
|
195
|
+
color = _get_packet_color(packet, protocol)
|
|
196
|
+
else:
|
|
197
|
+
color = "#4ecdc4" # Default teal
|
|
198
|
+
|
|
199
|
+
# Draw packet rectangle
|
|
200
|
+
rect = patches.Rectangle(
|
|
201
|
+
(start, 0.1),
|
|
202
|
+
end - start,
|
|
203
|
+
0.8,
|
|
204
|
+
facecolor=color,
|
|
205
|
+
edgecolor="black",
|
|
206
|
+
linewidth=0.8,
|
|
207
|
+
alpha=0.7,
|
|
208
|
+
)
|
|
209
|
+
ax.add_patch(rect)
|
|
210
|
+
|
|
211
|
+
# Add data annotation
|
|
212
|
+
if show_data and packet.data:
|
|
213
|
+
data_str = _format_packet_data(packet)
|
|
214
|
+
mid_time = (start + end) / 2
|
|
215
|
+
ax.text(
|
|
216
|
+
mid_time,
|
|
217
|
+
0.5,
|
|
218
|
+
data_str,
|
|
219
|
+
ha="center",
|
|
220
|
+
va="center",
|
|
221
|
+
fontsize=8,
|
|
222
|
+
fontweight="bold",
|
|
223
|
+
color="white" if not (show_errors and packet.errors) else "black",
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
# Add error markers
|
|
227
|
+
if show_errors and packet.errors:
|
|
228
|
+
ax.plot(
|
|
229
|
+
start,
|
|
230
|
+
1.1,
|
|
231
|
+
"rx",
|
|
232
|
+
markersize=8,
|
|
233
|
+
markeredgewidth=2,
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
ax.set_ylabel(f"{protocol}\nPackets", rotation=0, ha="right", va="center", fontsize=10)
|
|
237
|
+
ax.set_ylim(0, 1.2)
|
|
238
|
+
ax.set_yticks([])
|
|
239
|
+
ax.grid(True, axis="x", alpha=0.3, linestyle=":")
|
|
240
|
+
|
|
241
|
+
# Set x-axis label
|
|
242
|
+
axes[-1].set_xlabel(f"Time ({time_unit})", fontsize=11)
|
|
243
|
+
axes[-1].set_xlim(t_min * time_mult, t_max * time_mult)
|
|
244
|
+
|
|
245
|
+
if title:
|
|
246
|
+
fig.suptitle(title, fontsize=14, y=0.98)
|
|
247
|
+
|
|
248
|
+
fig.tight_layout()
|
|
249
|
+
return fig
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def plot_uart_decode(
|
|
253
|
+
packets: list[ProtocolPacket],
|
|
254
|
+
*,
|
|
255
|
+
rx_trace: DigitalTrace | None = None,
|
|
256
|
+
tx_trace: DigitalTrace | None = None,
|
|
257
|
+
time_range: tuple[float, float] | None = None,
|
|
258
|
+
time_unit: str = "auto",
|
|
259
|
+
show_parity_errors: bool = True,
|
|
260
|
+
show_framing_errors: bool = True,
|
|
261
|
+
figsize: tuple[float, float] | None = None,
|
|
262
|
+
title: str = "UART Communication",
|
|
263
|
+
) -> Figure:
|
|
264
|
+
"""Plot UART decoded packets with RX/TX lanes.
|
|
265
|
+
|
|
266
|
+
Specialized visualization for UART showing separate RX and TX channels
|
|
267
|
+
with decoded bytes and error highlighting.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
packets: List of UART packets.
|
|
271
|
+
rx_trace: Optional RX digital trace.
|
|
272
|
+
tx_trace: Optional TX digital trace.
|
|
273
|
+
time_range: Time range to plot (t_min, t_max) in seconds.
|
|
274
|
+
time_unit: Time unit for x-axis ("s", "ms", "us", "ns", "auto").
|
|
275
|
+
show_parity_errors: Highlight parity errors.
|
|
276
|
+
show_framing_errors: Highlight framing errors.
|
|
277
|
+
figsize: Figure size (width, height).
|
|
278
|
+
title: Plot title.
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
Matplotlib Figure object.
|
|
282
|
+
|
|
283
|
+
Raises:
|
|
284
|
+
ImportError: If matplotlib is not installed.
|
|
285
|
+
ValueError: If packets list is empty.
|
|
286
|
+
|
|
287
|
+
Example:
|
|
288
|
+
>>> decoder = UARTDecoder(baudrate=115200, parity="even")
|
|
289
|
+
>>> packets = list(decoder.decode(rx_trace))
|
|
290
|
+
>>> fig = plot_uart_decode(packets, rx_trace=rx_trace, time_unit="ms")
|
|
291
|
+
"""
|
|
292
|
+
if not HAS_MATPLOTLIB:
|
|
293
|
+
raise ImportError("matplotlib is required for visualization")
|
|
294
|
+
|
|
295
|
+
if len(packets) == 0:
|
|
296
|
+
raise ValueError("packets list cannot be empty")
|
|
297
|
+
|
|
298
|
+
# If we have both RX and TX, create dual-channel visualization
|
|
299
|
+
if rx_trace is not None and tx_trace is not None:
|
|
300
|
+
return _plot_dual_channel_uart(
|
|
301
|
+
packets,
|
|
302
|
+
rx_trace=rx_trace,
|
|
303
|
+
tx_trace=tx_trace,
|
|
304
|
+
time_range=time_range,
|
|
305
|
+
time_unit=time_unit,
|
|
306
|
+
show_parity_errors=show_parity_errors,
|
|
307
|
+
show_framing_errors=show_framing_errors,
|
|
308
|
+
figsize=figsize,
|
|
309
|
+
title=title,
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
# Single-channel view using generic decode plot
|
|
313
|
+
return plot_protocol_decode(
|
|
314
|
+
packets,
|
|
315
|
+
trace=rx_trace or tx_trace,
|
|
316
|
+
trace_channel="RX" if rx_trace else "TX",
|
|
317
|
+
show_errors=show_parity_errors or show_framing_errors,
|
|
318
|
+
time_range=time_range,
|
|
319
|
+
time_unit=time_unit,
|
|
320
|
+
figsize=figsize,
|
|
321
|
+
title=title,
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _plot_dual_channel_uart(
|
|
326
|
+
packets: list[ProtocolPacket],
|
|
327
|
+
*,
|
|
328
|
+
rx_trace: DigitalTrace,
|
|
329
|
+
tx_trace: DigitalTrace,
|
|
330
|
+
time_range: tuple[float, float] | None = None,
|
|
331
|
+
time_unit: str = "auto",
|
|
332
|
+
show_parity_errors: bool = True,
|
|
333
|
+
show_framing_errors: bool = True,
|
|
334
|
+
figsize: tuple[float, float] | None = None,
|
|
335
|
+
title: str = "UART Communication",
|
|
336
|
+
) -> Figure:
|
|
337
|
+
"""Create dual-channel UART visualization with separate RX/TX rows.
|
|
338
|
+
|
|
339
|
+
Args:
|
|
340
|
+
packets: List of UART packets (may include both RX and TX).
|
|
341
|
+
rx_trace: RX digital trace.
|
|
342
|
+
tx_trace: TX digital trace.
|
|
343
|
+
time_range: Time range to plot (t_min, t_max) in seconds.
|
|
344
|
+
time_unit: Time unit for x-axis.
|
|
345
|
+
show_parity_errors: Highlight parity errors.
|
|
346
|
+
show_framing_errors: Highlight framing errors.
|
|
347
|
+
figsize: Figure size (width, height).
|
|
348
|
+
title: Plot title.
|
|
349
|
+
|
|
350
|
+
Returns:
|
|
351
|
+
Matplotlib Figure object.
|
|
352
|
+
"""
|
|
353
|
+
# Determine time range from packets
|
|
354
|
+
if time_range is None:
|
|
355
|
+
t_min = min(p.timestamp for p in packets)
|
|
356
|
+
t_max = max(p.end_timestamp if p.end_timestamp else p.timestamp for p in packets)
|
|
357
|
+
padding = (t_max - t_min) * 0.1
|
|
358
|
+
t_min -= padding
|
|
359
|
+
t_max += padding
|
|
360
|
+
else:
|
|
361
|
+
t_min, t_max = time_range
|
|
362
|
+
|
|
363
|
+
# Select time unit
|
|
364
|
+
if time_unit == "auto":
|
|
365
|
+
time_range_val = t_max - t_min
|
|
366
|
+
if time_range_val < 1e-6:
|
|
367
|
+
time_unit = "ns"
|
|
368
|
+
time_mult = 1e9
|
|
369
|
+
elif time_range_val < 1e-3:
|
|
370
|
+
time_unit = "us"
|
|
371
|
+
time_mult = 1e6
|
|
372
|
+
elif time_range_val < 1:
|
|
373
|
+
time_unit = "ms"
|
|
374
|
+
time_mult = 1e3
|
|
375
|
+
else:
|
|
376
|
+
time_unit = "s"
|
|
377
|
+
time_mult = 1.0
|
|
378
|
+
else:
|
|
379
|
+
time_mult = {"s": 1.0, "ms": 1e3, "us": 1e6, "ns": 1e9}.get(time_unit, 1.0)
|
|
380
|
+
|
|
381
|
+
# 4 rows: RX waveform, RX packets, TX waveform, TX packets
|
|
382
|
+
n_rows = 4
|
|
383
|
+
|
|
384
|
+
# Auto-calculate figure size
|
|
385
|
+
if figsize is None:
|
|
386
|
+
width = 14
|
|
387
|
+
height = max(6, n_rows * 1.2 + 1)
|
|
388
|
+
figsize = (width, height)
|
|
389
|
+
|
|
390
|
+
fig, axes = plt.subplots(
|
|
391
|
+
n_rows,
|
|
392
|
+
1,
|
|
393
|
+
figsize=figsize,
|
|
394
|
+
sharex=True,
|
|
395
|
+
gridspec_kw={"hspace": 0.1, "height_ratios": [1, 0.8, 1, 0.8]},
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
# Separate packets by channel (using metadata if available)
|
|
399
|
+
rx_packets = []
|
|
400
|
+
tx_packets = []
|
|
401
|
+
for packet in packets:
|
|
402
|
+
# Check packet metadata for channel info
|
|
403
|
+
channel = getattr(packet, "channel", None)
|
|
404
|
+
if channel is None and hasattr(packet, "metadata"):
|
|
405
|
+
channel = packet.metadata.get("channel") if isinstance(packet.metadata, dict) else None
|
|
406
|
+
|
|
407
|
+
if channel == "TX":
|
|
408
|
+
tx_packets.append(packet)
|
|
409
|
+
else:
|
|
410
|
+
# Default to RX if channel not specified
|
|
411
|
+
rx_packets.append(packet)
|
|
412
|
+
|
|
413
|
+
# If no channel info, put all packets on both (as they were before)
|
|
414
|
+
if not rx_packets and not tx_packets:
|
|
415
|
+
rx_packets = packets
|
|
416
|
+
tx_packets = []
|
|
417
|
+
|
|
418
|
+
show_errors = show_parity_errors or show_framing_errors
|
|
419
|
+
|
|
420
|
+
# Plot RX waveform (row 0)
|
|
421
|
+
ax_rx_wave = axes[0]
|
|
422
|
+
rx_time = rx_trace.time_vector * time_mult
|
|
423
|
+
rx_data = rx_trace.data.astype(float)
|
|
424
|
+
mask = (rx_time >= t_min * time_mult) & (rx_time <= t_max * time_mult)
|
|
425
|
+
_plot_digital_waveform(ax_rx_wave, rx_time[mask], rx_data[mask])
|
|
426
|
+
ax_rx_wave.set_ylabel("RX", rotation=0, ha="right", va="center", fontsize=10)
|
|
427
|
+
ax_rx_wave.set_ylim(-0.2, 1.3)
|
|
428
|
+
ax_rx_wave.set_yticks([])
|
|
429
|
+
ax_rx_wave.grid(True, axis="x", alpha=0.3, linestyle=":")
|
|
430
|
+
|
|
431
|
+
# Plot RX packets (row 1)
|
|
432
|
+
ax_rx_packets = axes[1]
|
|
433
|
+
_plot_packet_row(ax_rx_packets, rx_packets, t_min, t_max, time_mult, show_errors)
|
|
434
|
+
ax_rx_packets.set_ylabel("RX\nData", rotation=0, ha="right", va="center", fontsize=9)
|
|
435
|
+
|
|
436
|
+
# Plot TX waveform (row 2)
|
|
437
|
+
ax_tx_wave = axes[2]
|
|
438
|
+
tx_time = tx_trace.time_vector * time_mult
|
|
439
|
+
tx_data = tx_trace.data.astype(float)
|
|
440
|
+
mask = (tx_time >= t_min * time_mult) & (tx_time <= t_max * time_mult)
|
|
441
|
+
_plot_digital_waveform(ax_tx_wave, tx_time[mask], tx_data[mask])
|
|
442
|
+
ax_tx_wave.set_ylabel("TX", rotation=0, ha="right", va="center", fontsize=10)
|
|
443
|
+
ax_tx_wave.set_ylim(-0.2, 1.3)
|
|
444
|
+
ax_tx_wave.set_yticks([])
|
|
445
|
+
ax_tx_wave.grid(True, axis="x", alpha=0.3, linestyle=":")
|
|
446
|
+
|
|
447
|
+
# Plot TX packets (row 3)
|
|
448
|
+
ax_tx_packets = axes[3]
|
|
449
|
+
_plot_packet_row(ax_tx_packets, tx_packets, t_min, t_max, time_mult, show_errors)
|
|
450
|
+
ax_tx_packets.set_ylabel("TX\nData", rotation=0, ha="right", va="center", fontsize=9)
|
|
451
|
+
|
|
452
|
+
# Set x-axis label
|
|
453
|
+
axes[-1].set_xlabel(f"Time ({time_unit})", fontsize=11)
|
|
454
|
+
axes[-1].set_xlim(t_min * time_mult, t_max * time_mult)
|
|
455
|
+
|
|
456
|
+
if title:
|
|
457
|
+
fig.suptitle(title, fontsize=14, y=0.98)
|
|
458
|
+
|
|
459
|
+
fig.tight_layout()
|
|
460
|
+
return fig
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def _plot_packet_row(
|
|
464
|
+
ax: Axes,
|
|
465
|
+
packets: list[ProtocolPacket],
|
|
466
|
+
t_min: float,
|
|
467
|
+
t_max: float,
|
|
468
|
+
time_mult: float,
|
|
469
|
+
show_errors: bool,
|
|
470
|
+
) -> None:
|
|
471
|
+
"""Plot a single row of packets on the given axes."""
|
|
472
|
+
for packet in packets:
|
|
473
|
+
if packet.timestamp < t_min or packet.timestamp > t_max:
|
|
474
|
+
continue
|
|
475
|
+
|
|
476
|
+
start = packet.timestamp * time_mult
|
|
477
|
+
end = (
|
|
478
|
+
packet.end_timestamp if packet.end_timestamp else packet.timestamp + 0.001
|
|
479
|
+
) * time_mult
|
|
480
|
+
|
|
481
|
+
# Determine packet color
|
|
482
|
+
if show_errors and packet.errors:
|
|
483
|
+
color = "#ff6b6b" # Red for errors
|
|
484
|
+
else:
|
|
485
|
+
color = "#4ecdc4" # Teal
|
|
486
|
+
|
|
487
|
+
# Draw packet rectangle
|
|
488
|
+
rect = patches.Rectangle(
|
|
489
|
+
(start, 0.1),
|
|
490
|
+
end - start,
|
|
491
|
+
0.8,
|
|
492
|
+
facecolor=color,
|
|
493
|
+
edgecolor="black",
|
|
494
|
+
linewidth=0.8,
|
|
495
|
+
alpha=0.7,
|
|
496
|
+
)
|
|
497
|
+
ax.add_patch(rect)
|
|
498
|
+
|
|
499
|
+
# Add data annotation
|
|
500
|
+
if packet.data:
|
|
501
|
+
data_str = _format_packet_data(packet)
|
|
502
|
+
mid_time = (start + end) / 2
|
|
503
|
+
ax.text(
|
|
504
|
+
mid_time,
|
|
505
|
+
0.5,
|
|
506
|
+
data_str,
|
|
507
|
+
ha="center",
|
|
508
|
+
va="center",
|
|
509
|
+
fontsize=7,
|
|
510
|
+
fontweight="bold",
|
|
511
|
+
color="white" if not (show_errors and packet.errors) else "black",
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
# Add error markers
|
|
515
|
+
if show_errors and packet.errors:
|
|
516
|
+
ax.plot(start, 1.1, "rx", markersize=6, markeredgewidth=2)
|
|
517
|
+
|
|
518
|
+
ax.set_ylim(0, 1.2)
|
|
519
|
+
ax.set_yticks([])
|
|
520
|
+
ax.grid(True, axis="x", alpha=0.3, linestyle=":")
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def plot_spi_decode(
|
|
524
|
+
packets: list[ProtocolPacket],
|
|
525
|
+
*,
|
|
526
|
+
clk_trace: DigitalTrace | None = None,
|
|
527
|
+
mosi_trace: DigitalTrace | None = None,
|
|
528
|
+
miso_trace: DigitalTrace | None = None,
|
|
529
|
+
cs_trace: DigitalTrace | None = None,
|
|
530
|
+
time_range: tuple[float, float] | None = None,
|
|
531
|
+
time_unit: str = "auto",
|
|
532
|
+
show_mosi: bool = True,
|
|
533
|
+
show_miso: bool = True,
|
|
534
|
+
figsize: tuple[float, float] | None = None,
|
|
535
|
+
title: str = "SPI Transaction",
|
|
536
|
+
) -> Figure:
|
|
537
|
+
"""Plot SPI decoded packets with CLK, MOSI, MISO, CS signals.
|
|
538
|
+
|
|
539
|
+
Specialized visualization for SPI showing all relevant signals
|
|
540
|
+
and decoded words on MOSI/MISO channels.
|
|
541
|
+
|
|
542
|
+
Args:
|
|
543
|
+
packets: List of SPI packets.
|
|
544
|
+
clk_trace: Optional clock signal trace.
|
|
545
|
+
mosi_trace: Optional MOSI (Master Out Slave In) trace.
|
|
546
|
+
miso_trace: Optional MISO (Master In Slave Out) trace.
|
|
547
|
+
cs_trace: Optional chip select trace.
|
|
548
|
+
time_range: Time range to plot (t_min, t_max) in seconds.
|
|
549
|
+
time_unit: Time unit for x-axis.
|
|
550
|
+
show_mosi: Show MOSI decoded data.
|
|
551
|
+
show_miso: Show MISO decoded data.
|
|
552
|
+
figsize: Figure size.
|
|
553
|
+
title: Plot title.
|
|
554
|
+
|
|
555
|
+
Returns:
|
|
556
|
+
Matplotlib Figure object.
|
|
557
|
+
|
|
558
|
+
Raises:
|
|
559
|
+
ImportError: If matplotlib is not installed.
|
|
560
|
+
ValueError: If packets list is empty.
|
|
561
|
+
|
|
562
|
+
Example:
|
|
563
|
+
>>> decoder = SPIDecoder(cpol=0, cpha=0, word_size=8)
|
|
564
|
+
>>> packets = list(decoder.decode(clk=clk, mosi=mosi, miso=miso))
|
|
565
|
+
>>> fig = plot_spi_decode(packets, clk_trace=clk, mosi_trace=mosi)
|
|
566
|
+
"""
|
|
567
|
+
if not HAS_MATPLOTLIB:
|
|
568
|
+
raise ImportError("matplotlib is required for visualization")
|
|
569
|
+
|
|
570
|
+
if len(packets) == 0:
|
|
571
|
+
raise ValueError("packets list cannot be empty")
|
|
572
|
+
|
|
573
|
+
# If we have multiple traces, create multi-channel visualization
|
|
574
|
+
traces_available = sum(
|
|
575
|
+
1 for t in [clk_trace, mosi_trace, miso_trace, cs_trace] if t is not None
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
if traces_available >= 2:
|
|
579
|
+
return _plot_multi_channel_spi(
|
|
580
|
+
packets,
|
|
581
|
+
clk_trace=clk_trace,
|
|
582
|
+
mosi_trace=mosi_trace,
|
|
583
|
+
miso_trace=miso_trace,
|
|
584
|
+
cs_trace=cs_trace,
|
|
585
|
+
time_range=time_range,
|
|
586
|
+
time_unit=time_unit,
|
|
587
|
+
show_mosi=show_mosi,
|
|
588
|
+
show_miso=show_miso,
|
|
589
|
+
figsize=figsize,
|
|
590
|
+
title=title,
|
|
591
|
+
)
|
|
592
|
+
|
|
593
|
+
# Single-channel view using generic decode plot
|
|
594
|
+
return plot_protocol_decode(
|
|
595
|
+
packets,
|
|
596
|
+
trace=mosi_trace,
|
|
597
|
+
trace_channel="MOSI",
|
|
598
|
+
time_range=time_range,
|
|
599
|
+
time_unit=time_unit,
|
|
600
|
+
figsize=figsize,
|
|
601
|
+
title=title,
|
|
602
|
+
)
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
def _plot_multi_channel_spi(
|
|
606
|
+
packets: list[ProtocolPacket],
|
|
607
|
+
*,
|
|
608
|
+
clk_trace: DigitalTrace | None = None,
|
|
609
|
+
mosi_trace: DigitalTrace | None = None,
|
|
610
|
+
miso_trace: DigitalTrace | None = None,
|
|
611
|
+
cs_trace: DigitalTrace | None = None,
|
|
612
|
+
time_range: tuple[float, float] | None = None,
|
|
613
|
+
time_unit: str = "auto",
|
|
614
|
+
show_mosi: bool = True,
|
|
615
|
+
show_miso: bool = True,
|
|
616
|
+
figsize: tuple[float, float] | None = None,
|
|
617
|
+
title: str = "SPI Transaction",
|
|
618
|
+
) -> Figure:
|
|
619
|
+
"""Create multi-channel SPI visualization with separate rows for each signal.
|
|
620
|
+
|
|
621
|
+
Args:
|
|
622
|
+
packets: List of SPI packets.
|
|
623
|
+
clk_trace: Optional clock signal trace.
|
|
624
|
+
mosi_trace: Optional MOSI trace.
|
|
625
|
+
miso_trace: Optional MISO trace.
|
|
626
|
+
cs_trace: Optional chip select trace.
|
|
627
|
+
time_range: Time range to plot.
|
|
628
|
+
time_unit: Time unit for x-axis.
|
|
629
|
+
show_mosi: Show MOSI decoded data row.
|
|
630
|
+
show_miso: Show MISO decoded data row.
|
|
631
|
+
figsize: Figure size.
|
|
632
|
+
title: Plot title.
|
|
633
|
+
|
|
634
|
+
Returns:
|
|
635
|
+
Matplotlib Figure object.
|
|
636
|
+
"""
|
|
637
|
+
# Determine time range from packets
|
|
638
|
+
if time_range is None:
|
|
639
|
+
t_min = min(p.timestamp for p in packets)
|
|
640
|
+
t_max = max(p.end_timestamp if p.end_timestamp else p.timestamp for p in packets)
|
|
641
|
+
padding = (t_max - t_min) * 0.1
|
|
642
|
+
t_min -= padding
|
|
643
|
+
t_max += padding
|
|
644
|
+
else:
|
|
645
|
+
t_min, t_max = time_range
|
|
646
|
+
|
|
647
|
+
# Select time unit
|
|
648
|
+
if time_unit == "auto":
|
|
649
|
+
time_range_val = t_max - t_min
|
|
650
|
+
if time_range_val < 1e-6:
|
|
651
|
+
time_unit = "ns"
|
|
652
|
+
time_mult = 1e9
|
|
653
|
+
elif time_range_val < 1e-3:
|
|
654
|
+
time_unit = "us"
|
|
655
|
+
time_mult = 1e6
|
|
656
|
+
elif time_range_val < 1:
|
|
657
|
+
time_unit = "ms"
|
|
658
|
+
time_mult = 1e3
|
|
659
|
+
else:
|
|
660
|
+
time_unit = "s"
|
|
661
|
+
time_mult = 1.0
|
|
662
|
+
else:
|
|
663
|
+
time_mult = {"s": 1.0, "ms": 1e3, "us": 1e6, "ns": 1e9}.get(time_unit, 1.0)
|
|
664
|
+
|
|
665
|
+
# Build list of rows to display
|
|
666
|
+
rows: list[dict[str, Any]] = []
|
|
667
|
+
|
|
668
|
+
if cs_trace is not None:
|
|
669
|
+
rows.append({"type": "waveform", "trace": cs_trace, "label": "CS"})
|
|
670
|
+
|
|
671
|
+
if clk_trace is not None:
|
|
672
|
+
rows.append({"type": "waveform", "trace": clk_trace, "label": "CLK"})
|
|
673
|
+
|
|
674
|
+
if mosi_trace is not None:
|
|
675
|
+
rows.append({"type": "waveform", "trace": mosi_trace, "label": "MOSI"})
|
|
676
|
+
if show_mosi:
|
|
677
|
+
rows.append({"type": "packets", "label": "MOSI\nData", "channel": "MOSI"})
|
|
678
|
+
|
|
679
|
+
if miso_trace is not None:
|
|
680
|
+
rows.append({"type": "waveform", "trace": miso_trace, "label": "MISO"})
|
|
681
|
+
if show_miso:
|
|
682
|
+
rows.append({"type": "packets", "label": "MISO\nData", "channel": "MISO"})
|
|
683
|
+
|
|
684
|
+
n_rows = len(rows)
|
|
685
|
+
if n_rows == 0:
|
|
686
|
+
# Fallback to generic if no traces
|
|
687
|
+
return plot_protocol_decode(
|
|
688
|
+
packets,
|
|
689
|
+
time_range=time_range,
|
|
690
|
+
time_unit=time_unit,
|
|
691
|
+
figsize=figsize,
|
|
692
|
+
title=title,
|
|
693
|
+
)
|
|
694
|
+
|
|
695
|
+
# Calculate height ratios (waveforms get more space than data rows)
|
|
696
|
+
height_ratios = []
|
|
697
|
+
for row in rows:
|
|
698
|
+
if row["type"] == "waveform":
|
|
699
|
+
height_ratios.append(1.0)
|
|
700
|
+
else:
|
|
701
|
+
height_ratios.append(0.6)
|
|
702
|
+
|
|
703
|
+
# Auto-calculate figure size
|
|
704
|
+
if figsize is None:
|
|
705
|
+
width = 14
|
|
706
|
+
height = max(4, sum(height_ratios) * 1.2 + 1)
|
|
707
|
+
figsize = (width, height)
|
|
708
|
+
|
|
709
|
+
fig, axes = plt.subplots(
|
|
710
|
+
n_rows,
|
|
711
|
+
1,
|
|
712
|
+
figsize=figsize,
|
|
713
|
+
sharex=True,
|
|
714
|
+
gridspec_kw={"hspace": 0.1, "height_ratios": height_ratios},
|
|
715
|
+
)
|
|
716
|
+
|
|
717
|
+
if n_rows == 1:
|
|
718
|
+
axes = [axes]
|
|
719
|
+
|
|
720
|
+
# Separate packets by channel (MOSI vs MISO)
|
|
721
|
+
mosi_packets = []
|
|
722
|
+
miso_packets = []
|
|
723
|
+
for packet in packets:
|
|
724
|
+
channel = getattr(packet, "channel", None)
|
|
725
|
+
if channel is None and hasattr(packet, "metadata"):
|
|
726
|
+
channel = packet.metadata.get("channel") if isinstance(packet.metadata, dict) else None
|
|
727
|
+
|
|
728
|
+
if channel == "MISO":
|
|
729
|
+
miso_packets.append(packet)
|
|
730
|
+
else:
|
|
731
|
+
# Default to MOSI
|
|
732
|
+
mosi_packets.append(packet)
|
|
733
|
+
|
|
734
|
+
# If no channel info, use all packets for MOSI
|
|
735
|
+
if not mosi_packets and not miso_packets:
|
|
736
|
+
mosi_packets = packets
|
|
737
|
+
|
|
738
|
+
# Plot each row
|
|
739
|
+
for ax, row in zip(axes, rows, strict=False):
|
|
740
|
+
if row["type"] == "waveform":
|
|
741
|
+
trace = row["trace"]
|
|
742
|
+
trace_time = trace.time_vector * time_mult
|
|
743
|
+
trace_data = trace.data.astype(float)
|
|
744
|
+
mask = (trace_time >= t_min * time_mult) & (trace_time <= t_max * time_mult)
|
|
745
|
+
_plot_digital_waveform(ax, trace_time[mask], trace_data[mask])
|
|
746
|
+
ax.set_ylabel(row["label"], rotation=0, ha="right", va="center", fontsize=10)
|
|
747
|
+
ax.set_ylim(-0.2, 1.3)
|
|
748
|
+
ax.set_yticks([])
|
|
749
|
+
ax.grid(True, axis="x", alpha=0.3, linestyle=":")
|
|
750
|
+
else:
|
|
751
|
+
# Packet row
|
|
752
|
+
channel = row.get("channel", "MOSI")
|
|
753
|
+
pkts = mosi_packets if channel == "MOSI" else miso_packets
|
|
754
|
+
_plot_packet_row(ax, pkts, t_min, t_max, time_mult, show_errors=True)
|
|
755
|
+
ax.set_ylabel(row["label"], rotation=0, ha="right", va="center", fontsize=9)
|
|
756
|
+
|
|
757
|
+
# Set x-axis label
|
|
758
|
+
axes[-1].set_xlabel(f"Time ({time_unit})", fontsize=11)
|
|
759
|
+
axes[-1].set_xlim(t_min * time_mult, t_max * time_mult)
|
|
760
|
+
|
|
761
|
+
if title:
|
|
762
|
+
fig.suptitle(title, fontsize=14, y=0.98)
|
|
763
|
+
|
|
764
|
+
fig.tight_layout()
|
|
765
|
+
return fig
|
|
766
|
+
|
|
767
|
+
|
|
768
|
+
def plot_i2c_decode(
|
|
769
|
+
packets: list[ProtocolPacket],
|
|
770
|
+
*,
|
|
771
|
+
sda_trace: DigitalTrace | None = None,
|
|
772
|
+
scl_trace: DigitalTrace | None = None,
|
|
773
|
+
time_range: tuple[float, float] | None = None,
|
|
774
|
+
time_unit: str = "auto",
|
|
775
|
+
show_addresses: bool = True,
|
|
776
|
+
show_ack_nack: bool = True,
|
|
777
|
+
figsize: tuple[float, float] | None = None,
|
|
778
|
+
title: str = "I2C Transaction",
|
|
779
|
+
) -> Figure:
|
|
780
|
+
"""Plot I2C decoded packets with SDA/SCL and address annotations.
|
|
781
|
+
|
|
782
|
+
Specialized visualization for I2C showing start/stop conditions,
|
|
783
|
+
addresses, data bytes, and ACK/NACK bits.
|
|
784
|
+
|
|
785
|
+
Args:
|
|
786
|
+
packets: List of I2C packets.
|
|
787
|
+
sda_trace: Optional SDA (data) signal trace.
|
|
788
|
+
scl_trace: Optional SCL (clock) signal trace.
|
|
789
|
+
time_range: Time range to plot (t_min, t_max) in seconds.
|
|
790
|
+
time_unit: Time unit for x-axis.
|
|
791
|
+
show_addresses: Highlight address bytes.
|
|
792
|
+
show_ack_nack: Show ACK/NACK indicators.
|
|
793
|
+
figsize: Figure size.
|
|
794
|
+
title: Plot title.
|
|
795
|
+
|
|
796
|
+
Returns:
|
|
797
|
+
Matplotlib Figure object.
|
|
798
|
+
|
|
799
|
+
Example:
|
|
800
|
+
>>> decoder = I2CDecoder()
|
|
801
|
+
>>> packets = list(decoder.decode(sda=sda, scl=scl))
|
|
802
|
+
>>> fig = plot_i2c_decode(packets, sda_trace=sda, scl_trace=scl)
|
|
803
|
+
"""
|
|
804
|
+
return plot_protocol_decode(
|
|
805
|
+
packets,
|
|
806
|
+
trace=sda_trace,
|
|
807
|
+
trace_channel="SDA",
|
|
808
|
+
time_range=time_range,
|
|
809
|
+
time_unit=time_unit,
|
|
810
|
+
figsize=figsize,
|
|
811
|
+
title=title,
|
|
812
|
+
)
|
|
813
|
+
|
|
814
|
+
|
|
815
|
+
def plot_can_decode(
|
|
816
|
+
packets: list[ProtocolPacket],
|
|
817
|
+
*,
|
|
818
|
+
can_trace: DigitalTrace | None = None,
|
|
819
|
+
time_range: tuple[float, float] | None = None,
|
|
820
|
+
time_unit: str = "auto",
|
|
821
|
+
show_ids: bool = True,
|
|
822
|
+
show_data_length: bool = True,
|
|
823
|
+
colorize_by_id: bool = True,
|
|
824
|
+
figsize: tuple[float, float] | None = None,
|
|
825
|
+
title: str = "CAN Bus",
|
|
826
|
+
) -> Figure:
|
|
827
|
+
"""Plot CAN decoded packets with arbitration IDs and data.
|
|
828
|
+
|
|
829
|
+
Specialized visualization for CAN bus showing arbitration IDs,
|
|
830
|
+
data length codes, and message data.
|
|
831
|
+
|
|
832
|
+
Args:
|
|
833
|
+
packets: List of CAN packets.
|
|
834
|
+
can_trace: Optional CAN bus trace.
|
|
835
|
+
time_range: Time range to plot (t_min, t_max) in seconds.
|
|
836
|
+
time_unit: Time unit for x-axis.
|
|
837
|
+
show_ids: Show arbitration IDs in annotations.
|
|
838
|
+
show_data_length: Show DLC (Data Length Code).
|
|
839
|
+
colorize_by_id: Use different colors for different CAN IDs.
|
|
840
|
+
figsize: Figure size.
|
|
841
|
+
title: Plot title.
|
|
842
|
+
|
|
843
|
+
Returns:
|
|
844
|
+
Matplotlib Figure object.
|
|
845
|
+
|
|
846
|
+
Example:
|
|
847
|
+
>>> decoder = CANDecoder()
|
|
848
|
+
>>> packets = list(decoder.decode(can_trace))
|
|
849
|
+
>>> fig = plot_can_decode(packets, can_trace=can_trace, colorize_by_id=True)
|
|
850
|
+
"""
|
|
851
|
+
return plot_protocol_decode(
|
|
852
|
+
packets,
|
|
853
|
+
trace=can_trace,
|
|
854
|
+
trace_channel="CAN",
|
|
855
|
+
colorize=colorize_by_id,
|
|
856
|
+
time_range=time_range,
|
|
857
|
+
time_unit=time_unit,
|
|
858
|
+
figsize=figsize,
|
|
859
|
+
title=title,
|
|
860
|
+
)
|
|
861
|
+
|
|
862
|
+
|
|
863
|
+
def _plot_digital_waveform(
|
|
864
|
+
ax: Axes,
|
|
865
|
+
time: NDArray[np.float64],
|
|
866
|
+
data: NDArray[np.float64],
|
|
867
|
+
) -> None:
|
|
868
|
+
"""Plot digital waveform with clean transitions."""
|
|
869
|
+
for i in range(len(time) - 1):
|
|
870
|
+
level = 1 if data[i] > 0.5 else 0
|
|
871
|
+
# Horizontal line
|
|
872
|
+
ax.plot(
|
|
873
|
+
[time[i], time[i + 1]],
|
|
874
|
+
[level, level],
|
|
875
|
+
"b-",
|
|
876
|
+
linewidth=1.5,
|
|
877
|
+
)
|
|
878
|
+
# Vertical transition
|
|
879
|
+
if i < len(time) - 1:
|
|
880
|
+
next_level = 1 if data[i + 1] > 0.5 else 0
|
|
881
|
+
if level != next_level:
|
|
882
|
+
ax.plot(
|
|
883
|
+
[time[i + 1], time[i + 1]],
|
|
884
|
+
[level, next_level],
|
|
885
|
+
"b-",
|
|
886
|
+
linewidth=1.5,
|
|
887
|
+
)
|
|
888
|
+
|
|
889
|
+
|
|
890
|
+
def _get_packet_color(packet: ProtocolPacket, protocol: str) -> str:
|
|
891
|
+
"""Get color for packet based on protocol and type."""
|
|
892
|
+
# Color palette for different protocols
|
|
893
|
+
colors = {
|
|
894
|
+
"UART": "#4ecdc4", # Teal
|
|
895
|
+
"SPI": "#95e1d3", # Mint
|
|
896
|
+
"I2C": "#f38181", # Coral
|
|
897
|
+
"CAN": "#aa96da", # Purple
|
|
898
|
+
"USB": "#fcbad3", # Pink
|
|
899
|
+
"1-Wire": "#ffffd2", # Yellow
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
return colors.get(protocol, "#4ecdc4")
|
|
903
|
+
|
|
904
|
+
|
|
905
|
+
def _format_packet_data(packet: ProtocolPacket) -> str:
|
|
906
|
+
"""Format packet data for display."""
|
|
907
|
+
if len(packet.data) == 0:
|
|
908
|
+
return ""
|
|
909
|
+
|
|
910
|
+
# For single byte, show as hex
|
|
911
|
+
if len(packet.data) == 1:
|
|
912
|
+
byte_val = packet.data[0]
|
|
913
|
+
# Show both hex and ASCII if printable
|
|
914
|
+
if 32 <= byte_val <= 126:
|
|
915
|
+
return f"0x{byte_val:02X} '{chr(byte_val)}'"
|
|
916
|
+
return f"0x{byte_val:02X}"
|
|
917
|
+
|
|
918
|
+
# For multiple bytes, show hex string (limit to first few bytes)
|
|
919
|
+
if len(packet.data) <= 4:
|
|
920
|
+
return " ".join(f"{b:02X}" for b in packet.data)
|
|
921
|
+
|
|
922
|
+
# For longer data, truncate
|
|
923
|
+
return " ".join(f"{b:02X}" for b in packet.data[:3]) + "..."
|
|
924
|
+
|
|
925
|
+
|
|
926
|
+
__all__ = [
|
|
927
|
+
"plot_can_decode",
|
|
928
|
+
"plot_i2c_decode",
|
|
929
|
+
"plot_protocol_decode",
|
|
930
|
+
"plot_spi_decode",
|
|
931
|
+
"plot_uart_decode",
|
|
932
|
+
]
|