oscura 0.0.1__py3-none-any.whl → 0.1.1__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.1.dist-info/METADATA +300 -0
- oscura-0.1.1.dist-info/RECORD +463 -0
- oscura-0.1.1.dist-info/entry_points.txt +2 -0
- {oscura-0.0.1.dist-info → oscura-0.1.1.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.1.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,691 @@
|
|
|
1
|
+
"""Configurable multi-bit parallel bus decoding.
|
|
2
|
+
|
|
3
|
+
This module provides configurable bus decoding for parallel digital signals,
|
|
4
|
+
supporting various bit orderings, active-low signaling, and clock-based
|
|
5
|
+
or interval-based sampling strategies.
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
Example:
|
|
9
|
+
>>> import numpy as np
|
|
10
|
+
>>> from oscura.analyzers.digital.bus import BusConfig, BusDecoder
|
|
11
|
+
>>> # Define 8-bit bus configuration
|
|
12
|
+
>>> config = BusConfig(name="data_bus", width=8, bit_order='lsb_first')
|
|
13
|
+
>>> config.bits = [{'channel': i, 'bit': i, 'name': f'D{i}'} for i in range(8)]
|
|
14
|
+
>>> # Create decoder
|
|
15
|
+
>>> decoder = BusDecoder(config, sample_rate=100e6)
|
|
16
|
+
>>> # Decode bus values from bit traces
|
|
17
|
+
>>> bit_traces = {i: np.random.randint(0, 2, 1000, dtype=np.uint8) for i in range(8)}
|
|
18
|
+
>>> transactions = decoder.decode_bus(bit_traces)
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from dataclasses import dataclass, field
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
26
|
+
|
|
27
|
+
import numpy as np
|
|
28
|
+
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
from numpy.typing import NDArray
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class BusConfig:
|
|
35
|
+
"""Configuration for parallel bus decoding.
|
|
36
|
+
|
|
37
|
+
Attributes:
|
|
38
|
+
name: Descriptive name for the bus.
|
|
39
|
+
width: Number of bits in the bus.
|
|
40
|
+
bit_order: Bit ordering, 'lsb_first' or 'msb_first'.
|
|
41
|
+
active_low: Whether signals are active-low (inverted).
|
|
42
|
+
bits: List of bit definitions with channel mapping.
|
|
43
|
+
|
|
44
|
+
Example:
|
|
45
|
+
>>> config = BusConfig(name="addr_bus", width=12, bit_order='lsb_first')
|
|
46
|
+
>>> config.bits = [{'channel': i, 'bit': i} for i in range(12)]
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
name: str
|
|
50
|
+
width: int # Number of bits
|
|
51
|
+
bit_order: Literal["lsb_first", "msb_first"] = "lsb_first"
|
|
52
|
+
active_low: bool = False
|
|
53
|
+
bits: list[dict[str, Any]] = field(
|
|
54
|
+
default_factory=list
|
|
55
|
+
) # [{channel: 0, bit: 0, name: 'D0'}, ...]
|
|
56
|
+
|
|
57
|
+
def __post_init__(self) -> None:
|
|
58
|
+
"""Validate bus configuration."""
|
|
59
|
+
if self.width <= 0:
|
|
60
|
+
raise ValueError(f"Bus width must be positive, got {self.width}")
|
|
61
|
+
if self.bit_order not in ["lsb_first", "msb_first"]:
|
|
62
|
+
raise ValueError(f"Invalid bit_order: {self.bit_order}")
|
|
63
|
+
|
|
64
|
+
@classmethod
|
|
65
|
+
def from_yaml(cls, path: str | Path) -> BusConfig:
|
|
66
|
+
"""Load bus configuration from YAML file.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
path: Path to YAML configuration file.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
BusConfig instance loaded from file.
|
|
73
|
+
|
|
74
|
+
Raises:
|
|
75
|
+
ImportError: If PyYAML is not installed.
|
|
76
|
+
FileNotFoundError: If file does not exist.
|
|
77
|
+
"""
|
|
78
|
+
try:
|
|
79
|
+
import yaml
|
|
80
|
+
except ImportError as e:
|
|
81
|
+
raise ImportError(
|
|
82
|
+
"PyYAML is required for YAML loading. Install with: pip install pyyaml"
|
|
83
|
+
) from e
|
|
84
|
+
|
|
85
|
+
path = Path(path)
|
|
86
|
+
if not path.exists():
|
|
87
|
+
raise FileNotFoundError(f"Configuration file not found: {path}")
|
|
88
|
+
|
|
89
|
+
with open(path) as f:
|
|
90
|
+
config_dict = yaml.safe_load(f)
|
|
91
|
+
|
|
92
|
+
return cls.from_dict(config_dict)
|
|
93
|
+
|
|
94
|
+
@classmethod
|
|
95
|
+
def from_dict(cls, config: dict[str, Any]) -> BusConfig:
|
|
96
|
+
"""Create bus configuration from dictionary.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
config: Dictionary with bus configuration parameters.
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
BusConfig instance created from dictionary.
|
|
103
|
+
|
|
104
|
+
Example:
|
|
105
|
+
>>> config_dict = {
|
|
106
|
+
... 'name': 'data_bus',
|
|
107
|
+
... 'width': 8,
|
|
108
|
+
... 'bit_order': 'lsb_first',
|
|
109
|
+
... 'bits': [{'channel': i, 'bit': i} for i in range(8)]
|
|
110
|
+
... }
|
|
111
|
+
>>> config = BusConfig.from_dict(config_dict)
|
|
112
|
+
"""
|
|
113
|
+
return cls(
|
|
114
|
+
name=config.get("name", "bus"),
|
|
115
|
+
width=config["width"],
|
|
116
|
+
bit_order=config.get("bit_order", "lsb_first"),
|
|
117
|
+
active_low=config.get("active_low", False),
|
|
118
|
+
bits=config.get("bits", []),
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@dataclass
|
|
123
|
+
class ParallelBusConfig:
|
|
124
|
+
"""Configuration for parallel bus decoding with simplified interface.
|
|
125
|
+
|
|
126
|
+
This is a convenience class for tests that provides a simpler interface
|
|
127
|
+
than the full BusConfig.
|
|
128
|
+
|
|
129
|
+
Attributes:
|
|
130
|
+
data_width: Number of data bits in the bus.
|
|
131
|
+
bit_order: Bit ordering, 'lsb_first' or 'msb_first'.
|
|
132
|
+
has_clock: Whether the bus uses a clock signal.
|
|
133
|
+
address_width: Optional number of address bits.
|
|
134
|
+
active_low: Whether signals are active-low.
|
|
135
|
+
|
|
136
|
+
Example:
|
|
137
|
+
>>> config = ParallelBusConfig(data_width=8, bit_order='lsb_first')
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
data_width: int
|
|
141
|
+
bit_order: Literal["lsb_first", "msb_first"] = "lsb_first"
|
|
142
|
+
has_clock: bool = False
|
|
143
|
+
address_width: int | None = None
|
|
144
|
+
active_low: bool = False
|
|
145
|
+
|
|
146
|
+
def __post_init__(self) -> None:
|
|
147
|
+
"""Validate configuration."""
|
|
148
|
+
if self.data_width <= 0:
|
|
149
|
+
raise ValueError(f"data_width must be positive, got {self.data_width}")
|
|
150
|
+
if self.address_width is not None and self.address_width <= 0:
|
|
151
|
+
raise ValueError(f"address_width must be positive, got {self.address_width}")
|
|
152
|
+
|
|
153
|
+
def to_bus_config(self, name: str = "parallel_bus") -> BusConfig:
|
|
154
|
+
"""Convert to BusConfig.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
name: Name for the bus configuration.
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
BusConfig instance.
|
|
161
|
+
"""
|
|
162
|
+
return BusConfig(
|
|
163
|
+
name=name,
|
|
164
|
+
width=self.data_width,
|
|
165
|
+
bit_order=self.bit_order,
|
|
166
|
+
active_low=self.active_low,
|
|
167
|
+
bits=[{"channel": i, "bit": i} for i in range(self.data_width)],
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@dataclass
|
|
172
|
+
class BusTransaction:
|
|
173
|
+
"""A decoded bus transaction.
|
|
174
|
+
|
|
175
|
+
Attributes:
|
|
176
|
+
timestamp: Time in seconds when transaction occurred.
|
|
177
|
+
sample_index: Sample index in the original traces.
|
|
178
|
+
value: Decoded bus value as integer.
|
|
179
|
+
raw_bits: Individual bit values (after active-low inversion if applicable).
|
|
180
|
+
transaction_type: Optional transaction type label.
|
|
181
|
+
address: Optional address field if this is an address bus.
|
|
182
|
+
data: Optional data field if this is a data bus.
|
|
183
|
+
"""
|
|
184
|
+
|
|
185
|
+
timestamp: float # Time in seconds
|
|
186
|
+
sample_index: int
|
|
187
|
+
value: int # Decoded bus value
|
|
188
|
+
raw_bits: list[int] # Individual bit values
|
|
189
|
+
transaction_type: str = "" # 'read', 'write', etc.
|
|
190
|
+
address: int | None = None # If address bus present
|
|
191
|
+
data: int | None = None # If data value
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class BusDecoder:
|
|
195
|
+
"""Decode multi-bit parallel buses from individual bit traces.
|
|
196
|
+
|
|
197
|
+
Supports configurable bit ordering, active-low signaling, and various
|
|
198
|
+
sampling strategies (clock-based or interval-based).
|
|
199
|
+
|
|
200
|
+
Attributes:
|
|
201
|
+
config: Bus configuration specifying width, ordering, etc.
|
|
202
|
+
sample_rate: Sample rate of input traces in Hz.
|
|
203
|
+
|
|
204
|
+
Example:
|
|
205
|
+
>>> config = BusConfig(name="data", width=8, bit_order='lsb_first')
|
|
206
|
+
>>> decoder = BusDecoder(config, sample_rate=100e6)
|
|
207
|
+
>>> bit_traces = {i: trace_data for i in range(8)}
|
|
208
|
+
>>> transactions = decoder.decode_bus(bit_traces)
|
|
209
|
+
"""
|
|
210
|
+
|
|
211
|
+
def __init__(
|
|
212
|
+
self,
|
|
213
|
+
config: BusConfig | ParallelBusConfig,
|
|
214
|
+
sample_rate: float = 1.0,
|
|
215
|
+
):
|
|
216
|
+
"""Initialize decoder with configuration.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
config: Bus configuration (BusConfig or ParallelBusConfig).
|
|
220
|
+
sample_rate: Sample rate of input traces in Hz.
|
|
221
|
+
|
|
222
|
+
Raises:
|
|
223
|
+
ValueError: If sample rate is invalid.
|
|
224
|
+
"""
|
|
225
|
+
if sample_rate <= 0:
|
|
226
|
+
raise ValueError(f"Sample rate must be positive, got {sample_rate}")
|
|
227
|
+
|
|
228
|
+
# Handle ParallelBusConfig
|
|
229
|
+
self._parallel_config: ParallelBusConfig | None
|
|
230
|
+
if isinstance(config, ParallelBusConfig):
|
|
231
|
+
self._parallel_config = config
|
|
232
|
+
self.config = config.to_bus_config()
|
|
233
|
+
else:
|
|
234
|
+
self._parallel_config = None
|
|
235
|
+
self.config = config
|
|
236
|
+
|
|
237
|
+
self.sample_rate = sample_rate
|
|
238
|
+
self._time_base = 1.0 / sample_rate
|
|
239
|
+
|
|
240
|
+
def decode_bus(
|
|
241
|
+
self,
|
|
242
|
+
bit_traces: dict[int, NDArray[np.uint8]], # channel_index -> trace data
|
|
243
|
+
clock_trace: NDArray[np.uint8] | None = None,
|
|
244
|
+
clock_edge: Literal["rising", "falling"] = "rising",
|
|
245
|
+
) -> list[BusTransaction]:
|
|
246
|
+
"""Decode bus values from individual bit traces.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
bit_traces: Dictionary mapping channel index to trace data (boolean or 0/1).
|
|
250
|
+
clock_trace: Optional clock signal for synchronous sampling.
|
|
251
|
+
clock_edge: Which clock edge to sample on ('rising' or 'falling').
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
List of BusTransaction objects with decoded values.
|
|
255
|
+
|
|
256
|
+
Raises:
|
|
257
|
+
ValueError: If bit traces don't match configuration.
|
|
258
|
+
|
|
259
|
+
Example:
|
|
260
|
+
>>> bit_traces = {0: np.array([0,1,1,0]), 1: np.array([1,1,0,0])}
|
|
261
|
+
>>> transactions = decoder.decode_bus(bit_traces)
|
|
262
|
+
"""
|
|
263
|
+
if not bit_traces:
|
|
264
|
+
raise ValueError("bit_traces cannot be empty")
|
|
265
|
+
|
|
266
|
+
# Use clock-based or interval-based sampling
|
|
267
|
+
if clock_trace is not None:
|
|
268
|
+
return self.sample_at_clock(bit_traces, clock_trace, clock_edge)
|
|
269
|
+
else:
|
|
270
|
+
# Sample every point (could be optimized with interval sampling)
|
|
271
|
+
_trace_length = len(next(iter(bit_traces.values())))
|
|
272
|
+
return self.sample_at_intervals(bit_traces, interval_samples=1)
|
|
273
|
+
|
|
274
|
+
def decode_parallel(
|
|
275
|
+
self,
|
|
276
|
+
channels: list[NDArray[np.uint8]],
|
|
277
|
+
) -> list[int]:
|
|
278
|
+
"""Decode parallel bus values from channel list.
|
|
279
|
+
|
|
280
|
+
Simplified interface for parallel bus decoding without clock.
|
|
281
|
+
|
|
282
|
+
Args:
|
|
283
|
+
channels: List of channel data arrays, indexed by bit position.
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
List of decoded integer values (one per sample).
|
|
287
|
+
|
|
288
|
+
Example:
|
|
289
|
+
>>> channels = [ch0, ch1, ch2, ch3] # 4-bit bus
|
|
290
|
+
>>> values = decoder.decode_parallel(channels)
|
|
291
|
+
"""
|
|
292
|
+
if not channels:
|
|
293
|
+
return []
|
|
294
|
+
|
|
295
|
+
trace_length = len(channels[0])
|
|
296
|
+
width = len(channels)
|
|
297
|
+
bit_order = self.config.bit_order
|
|
298
|
+
|
|
299
|
+
values = []
|
|
300
|
+
for sample_idx in range(trace_length):
|
|
301
|
+
value = 0
|
|
302
|
+
for bit_idx in range(width):
|
|
303
|
+
bit_val = int(bool(channels[bit_idx][sample_idx]))
|
|
304
|
+
if self.config.active_low:
|
|
305
|
+
bit_val = 1 - bit_val
|
|
306
|
+
|
|
307
|
+
if bit_order == "lsb_first":
|
|
308
|
+
if bit_val:
|
|
309
|
+
value |= 1 << bit_idx
|
|
310
|
+
else: # msb_first
|
|
311
|
+
if bit_val:
|
|
312
|
+
value |= 1 << (width - 1 - bit_idx)
|
|
313
|
+
|
|
314
|
+
values.append(value)
|
|
315
|
+
|
|
316
|
+
return values
|
|
317
|
+
|
|
318
|
+
def decode_with_clock(
|
|
319
|
+
self,
|
|
320
|
+
channels: list[NDArray[np.uint8]],
|
|
321
|
+
clock: NDArray[np.uint8],
|
|
322
|
+
edge: Literal["rising", "falling"] = "rising",
|
|
323
|
+
) -> list[int]:
|
|
324
|
+
"""Decode parallel bus values at clock edges.
|
|
325
|
+
|
|
326
|
+
Args:
|
|
327
|
+
channels: List of channel data arrays, indexed by bit position.
|
|
328
|
+
clock: Clock signal trace (boolean or 0/1).
|
|
329
|
+
edge: Which edge to sample on ('rising' or 'falling').
|
|
330
|
+
|
|
331
|
+
Returns:
|
|
332
|
+
List of decoded integer values (one per clock edge).
|
|
333
|
+
|
|
334
|
+
Example:
|
|
335
|
+
>>> values = decoder.decode_with_clock(channels, clock, 'rising')
|
|
336
|
+
"""
|
|
337
|
+
if not channels:
|
|
338
|
+
return []
|
|
339
|
+
|
|
340
|
+
# Convert clock to boolean
|
|
341
|
+
clock_bool = np.asarray(clock, dtype=bool)
|
|
342
|
+
|
|
343
|
+
# Find edges
|
|
344
|
+
if edge == "rising":
|
|
345
|
+
edges = np.where(np.diff(clock_bool.astype(int)) > 0)[0] + 1
|
|
346
|
+
else:
|
|
347
|
+
edges = np.where(np.diff(clock_bool.astype(int)) < 0)[0] + 1
|
|
348
|
+
|
|
349
|
+
width = len(channels)
|
|
350
|
+
bit_order = self.config.bit_order
|
|
351
|
+
|
|
352
|
+
values = []
|
|
353
|
+
for edge_idx in edges:
|
|
354
|
+
value = 0
|
|
355
|
+
for bit_idx in range(width):
|
|
356
|
+
if edge_idx < len(channels[bit_idx]):
|
|
357
|
+
bit_val = int(bool(channels[bit_idx][edge_idx]))
|
|
358
|
+
else:
|
|
359
|
+
bit_val = 0
|
|
360
|
+
|
|
361
|
+
if self.config.active_low:
|
|
362
|
+
bit_val = 1 - bit_val
|
|
363
|
+
|
|
364
|
+
if bit_order == "lsb_first":
|
|
365
|
+
if bit_val:
|
|
366
|
+
value |= 1 << bit_idx
|
|
367
|
+
else: # msb_first
|
|
368
|
+
if bit_val:
|
|
369
|
+
value |= 1 << (width - 1 - bit_idx)
|
|
370
|
+
|
|
371
|
+
values.append(value)
|
|
372
|
+
|
|
373
|
+
return values
|
|
374
|
+
|
|
375
|
+
def decode_transactions(
|
|
376
|
+
self,
|
|
377
|
+
address_channels: list[NDArray[np.uint8]],
|
|
378
|
+
data_channels: list[NDArray[np.uint8]],
|
|
379
|
+
clock: NDArray[np.uint8],
|
|
380
|
+
edge: Literal["rising", "falling"] = "rising",
|
|
381
|
+
) -> list[dict[str, int]]:
|
|
382
|
+
"""Decode bus transactions with address and data.
|
|
383
|
+
|
|
384
|
+
Args:
|
|
385
|
+
address_channels: List of address channel data arrays.
|
|
386
|
+
data_channels: List of data channel data arrays.
|
|
387
|
+
clock: Clock signal trace.
|
|
388
|
+
edge: Which clock edge to sample on.
|
|
389
|
+
|
|
390
|
+
Returns:
|
|
391
|
+
List of transaction dictionaries with 'address' and 'data' keys.
|
|
392
|
+
|
|
393
|
+
Example:
|
|
394
|
+
>>> transactions = decoder.decode_transactions(
|
|
395
|
+
... address_channels=addr_ch,
|
|
396
|
+
... data_channels=data_ch,
|
|
397
|
+
... clock=clk
|
|
398
|
+
... )
|
|
399
|
+
"""
|
|
400
|
+
# Convert clock to boolean
|
|
401
|
+
clock_bool = np.asarray(clock, dtype=bool)
|
|
402
|
+
|
|
403
|
+
# Find edges
|
|
404
|
+
if edge == "rising":
|
|
405
|
+
edges = np.where(np.diff(clock_bool.astype(int)) > 0)[0] + 1
|
|
406
|
+
else:
|
|
407
|
+
edges = np.where(np.diff(clock_bool.astype(int)) < 0)[0] + 1
|
|
408
|
+
|
|
409
|
+
addr_width = len(address_channels)
|
|
410
|
+
data_width = len(data_channels)
|
|
411
|
+
bit_order = self.config.bit_order
|
|
412
|
+
|
|
413
|
+
transactions = []
|
|
414
|
+
for edge_idx in edges:
|
|
415
|
+
# Decode address
|
|
416
|
+
address = 0
|
|
417
|
+
for bit_idx in range(addr_width):
|
|
418
|
+
if edge_idx < len(address_channels[bit_idx]):
|
|
419
|
+
bit_val = int(bool(address_channels[bit_idx][edge_idx]))
|
|
420
|
+
else:
|
|
421
|
+
bit_val = 0
|
|
422
|
+
|
|
423
|
+
if bit_order == "lsb_first":
|
|
424
|
+
if bit_val:
|
|
425
|
+
address |= 1 << bit_idx
|
|
426
|
+
else:
|
|
427
|
+
if bit_val:
|
|
428
|
+
address |= 1 << (addr_width - 1 - bit_idx)
|
|
429
|
+
|
|
430
|
+
# Decode data
|
|
431
|
+
data = 0
|
|
432
|
+
for bit_idx in range(data_width):
|
|
433
|
+
if edge_idx < len(data_channels[bit_idx]):
|
|
434
|
+
bit_val = int(bool(data_channels[bit_idx][edge_idx]))
|
|
435
|
+
else:
|
|
436
|
+
bit_val = 0
|
|
437
|
+
|
|
438
|
+
if bit_order == "lsb_first":
|
|
439
|
+
if bit_val:
|
|
440
|
+
data |= 1 << bit_idx
|
|
441
|
+
else:
|
|
442
|
+
if bit_val:
|
|
443
|
+
data |= 1 << (data_width - 1 - bit_idx)
|
|
444
|
+
|
|
445
|
+
transactions.append(
|
|
446
|
+
{
|
|
447
|
+
"address": address,
|
|
448
|
+
"data": data,
|
|
449
|
+
"sample_index": int(edge_idx),
|
|
450
|
+
}
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
return transactions
|
|
454
|
+
|
|
455
|
+
def sample_at_clock(
|
|
456
|
+
self,
|
|
457
|
+
bit_traces: dict[int, NDArray[np.uint8]],
|
|
458
|
+
clock_trace: NDArray[np.uint8],
|
|
459
|
+
edge: Literal["rising", "falling"] = "rising",
|
|
460
|
+
) -> list[BusTransaction]:
|
|
461
|
+
"""Sample bus at clock edges.
|
|
462
|
+
|
|
463
|
+
Args:
|
|
464
|
+
bit_traces: Dictionary mapping channel index to trace data.
|
|
465
|
+
clock_trace: Clock signal trace (boolean or 0/1).
|
|
466
|
+
edge: Which edge to sample on ('rising' or 'falling').
|
|
467
|
+
|
|
468
|
+
Returns:
|
|
469
|
+
List of BusTransaction objects sampled at clock edges.
|
|
470
|
+
|
|
471
|
+
Example:
|
|
472
|
+
>>> clock = np.array([0,1,0,1,0,1], dtype=bool)
|
|
473
|
+
>>> transactions = decoder.sample_at_clock(bit_traces, clock, 'rising')
|
|
474
|
+
"""
|
|
475
|
+
# Convert clock to boolean
|
|
476
|
+
clock_bool = np.asarray(clock_trace, dtype=bool)
|
|
477
|
+
|
|
478
|
+
# Find edges
|
|
479
|
+
if edge == "rising":
|
|
480
|
+
# Rising edge: 0->1 transition
|
|
481
|
+
edges = np.where(np.diff(clock_bool.astype(int)) > 0)[0] + 1
|
|
482
|
+
else:
|
|
483
|
+
# Falling edge: 1->0 transition
|
|
484
|
+
edges = np.where(np.diff(clock_bool.astype(int)) < 0)[0] + 1
|
|
485
|
+
|
|
486
|
+
transactions = []
|
|
487
|
+
|
|
488
|
+
for edge_idx in edges:
|
|
489
|
+
# Sample all bits at this edge
|
|
490
|
+
bit_values = []
|
|
491
|
+
for bit_def in self.config.bits:
|
|
492
|
+
channel = bit_def.get("channel", bit_def.get("bit", 0))
|
|
493
|
+
if channel in bit_traces:
|
|
494
|
+
trace = bit_traces[channel]
|
|
495
|
+
if edge_idx < len(trace):
|
|
496
|
+
bit_val = int(bool(trace[edge_idx]))
|
|
497
|
+
bit_values.append(bit_val)
|
|
498
|
+
else:
|
|
499
|
+
bit_values.append(0)
|
|
500
|
+
else:
|
|
501
|
+
bit_values.append(0)
|
|
502
|
+
|
|
503
|
+
# Apply active-low inversion if needed
|
|
504
|
+
if self.config.active_low:
|
|
505
|
+
bit_values = self._apply_active_low(bit_values)
|
|
506
|
+
|
|
507
|
+
# Reconstruct bus value
|
|
508
|
+
value = self._reconstruct_value(bit_values)
|
|
509
|
+
|
|
510
|
+
# Create transaction
|
|
511
|
+
transaction = BusTransaction(
|
|
512
|
+
timestamp=edge_idx * self._time_base,
|
|
513
|
+
sample_index=int(edge_idx),
|
|
514
|
+
value=value,
|
|
515
|
+
raw_bits=bit_values,
|
|
516
|
+
)
|
|
517
|
+
transactions.append(transaction)
|
|
518
|
+
|
|
519
|
+
return transactions
|
|
520
|
+
|
|
521
|
+
def sample_at_intervals(
|
|
522
|
+
self, bit_traces: dict[int, NDArray[np.uint8]], interval_samples: int
|
|
523
|
+
) -> list[BusTransaction]:
|
|
524
|
+
"""Sample bus at regular intervals.
|
|
525
|
+
|
|
526
|
+
Args:
|
|
527
|
+
bit_traces: Dictionary mapping channel index to trace data.
|
|
528
|
+
interval_samples: Number of samples between each bus sample.
|
|
529
|
+
|
|
530
|
+
Returns:
|
|
531
|
+
List of BusTransaction objects sampled at intervals.
|
|
532
|
+
|
|
533
|
+
Raises:
|
|
534
|
+
ValueError: If interval_samples is not positive.
|
|
535
|
+
|
|
536
|
+
Example:
|
|
537
|
+
>>> transactions = decoder.sample_at_intervals(bit_traces, interval_samples=10)
|
|
538
|
+
"""
|
|
539
|
+
if interval_samples <= 0:
|
|
540
|
+
raise ValueError(f"interval_samples must be positive, got {interval_samples}")
|
|
541
|
+
|
|
542
|
+
# Determine trace length
|
|
543
|
+
trace_length = len(next(iter(bit_traces.values())))
|
|
544
|
+
|
|
545
|
+
transactions = []
|
|
546
|
+
|
|
547
|
+
for sample_idx in range(0, trace_length, interval_samples):
|
|
548
|
+
# Sample all bits at this index
|
|
549
|
+
bit_values = []
|
|
550
|
+
for bit_def in self.config.bits:
|
|
551
|
+
channel = bit_def.get("channel", bit_def.get("bit", 0))
|
|
552
|
+
if channel in bit_traces:
|
|
553
|
+
trace = bit_traces[channel]
|
|
554
|
+
if sample_idx < len(trace):
|
|
555
|
+
bit_val = int(bool(trace[sample_idx]))
|
|
556
|
+
bit_values.append(bit_val)
|
|
557
|
+
else:
|
|
558
|
+
bit_values.append(0)
|
|
559
|
+
else:
|
|
560
|
+
bit_values.append(0)
|
|
561
|
+
|
|
562
|
+
# Apply active-low inversion if needed
|
|
563
|
+
if self.config.active_low:
|
|
564
|
+
bit_values = self._apply_active_low(bit_values)
|
|
565
|
+
|
|
566
|
+
# Reconstruct bus value
|
|
567
|
+
value = self._reconstruct_value(bit_values)
|
|
568
|
+
|
|
569
|
+
# Create transaction
|
|
570
|
+
transaction = BusTransaction(
|
|
571
|
+
timestamp=sample_idx * self._time_base,
|
|
572
|
+
sample_index=sample_idx,
|
|
573
|
+
value=value,
|
|
574
|
+
raw_bits=bit_values,
|
|
575
|
+
)
|
|
576
|
+
transactions.append(transaction)
|
|
577
|
+
|
|
578
|
+
return transactions
|
|
579
|
+
|
|
580
|
+
def _reconstruct_value(self, bit_values: list[int]) -> int:
|
|
581
|
+
"""Reconstruct bus value from individual bits.
|
|
582
|
+
|
|
583
|
+
Args:
|
|
584
|
+
bit_values: List of bit values (0 or 1) in config order.
|
|
585
|
+
|
|
586
|
+
Returns:
|
|
587
|
+
Integer value reconstructed from bits.
|
|
588
|
+
"""
|
|
589
|
+
if not bit_values:
|
|
590
|
+
return 0
|
|
591
|
+
|
|
592
|
+
value = 0
|
|
593
|
+
|
|
594
|
+
if self.config.bit_order == "lsb_first":
|
|
595
|
+
# LSB is first in list, MSB is last
|
|
596
|
+
for i, bit_val in enumerate(bit_values):
|
|
597
|
+
if bit_val:
|
|
598
|
+
value |= 1 << i
|
|
599
|
+
else: # msb_first
|
|
600
|
+
# MSB is first in list, LSB is last
|
|
601
|
+
n_bits = len(bit_values)
|
|
602
|
+
for i, bit_val in enumerate(bit_values):
|
|
603
|
+
if bit_val:
|
|
604
|
+
value |= 1 << (n_bits - 1 - i)
|
|
605
|
+
|
|
606
|
+
return value
|
|
607
|
+
|
|
608
|
+
def _apply_active_low(self, bit_values: list[int]) -> list[int]:
|
|
609
|
+
"""Apply active-low inversion if configured.
|
|
610
|
+
|
|
611
|
+
Args:
|
|
612
|
+
bit_values: List of bit values (0 or 1).
|
|
613
|
+
|
|
614
|
+
Returns:
|
|
615
|
+
Inverted bit values if active_low is True, otherwise unchanged.
|
|
616
|
+
"""
|
|
617
|
+
if self.config.active_low:
|
|
618
|
+
return [1 - bit for bit in bit_values]
|
|
619
|
+
return bit_values
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
# Convenience functions
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
def decode_bus(
|
|
626
|
+
bit_traces: dict[int, NDArray[np.uint8]],
|
|
627
|
+
config: BusConfig | str | Path,
|
|
628
|
+
sample_rate: float,
|
|
629
|
+
clock_trace: NDArray[np.uint8] | None = None,
|
|
630
|
+
clock_edge: Literal["rising", "falling"] = "rising",
|
|
631
|
+
) -> list[BusTransaction]:
|
|
632
|
+
"""Decode bus from bit traces.
|
|
633
|
+
|
|
634
|
+
Convenience function for quick bus decoding without creating a decoder instance.
|
|
635
|
+
|
|
636
|
+
Args:
|
|
637
|
+
bit_traces: Dictionary mapping channel index to trace data.
|
|
638
|
+
config: BusConfig instance or path to YAML config file.
|
|
639
|
+
sample_rate: Sample rate of traces in Hz.
|
|
640
|
+
clock_trace: Optional clock signal for synchronous sampling.
|
|
641
|
+
clock_edge: Which clock edge to sample on.
|
|
642
|
+
|
|
643
|
+
Returns:
|
|
644
|
+
List of BusTransaction objects.
|
|
645
|
+
|
|
646
|
+
Example:
|
|
647
|
+
>>> transactions = decode_bus(bit_traces, 'bus_config.yaml', 100e6)
|
|
648
|
+
"""
|
|
649
|
+
if isinstance(config, str | Path):
|
|
650
|
+
config = BusConfig.from_yaml(config)
|
|
651
|
+
|
|
652
|
+
decoder = BusDecoder(config, sample_rate)
|
|
653
|
+
return decoder.decode_bus(bit_traces, clock_trace, clock_edge)
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
def sample_at_clock(
|
|
657
|
+
bit_traces: dict[int, NDArray[np.uint8]],
|
|
658
|
+
clock_trace: NDArray[np.uint8],
|
|
659
|
+
config: BusConfig,
|
|
660
|
+
sample_rate: float,
|
|
661
|
+
edge: Literal["rising", "falling"] = "rising",
|
|
662
|
+
) -> list[BusTransaction]:
|
|
663
|
+
"""Sample bus at clock edges.
|
|
664
|
+
|
|
665
|
+
Convenience function for clock-based bus sampling.
|
|
666
|
+
|
|
667
|
+
Args:
|
|
668
|
+
bit_traces: Dictionary mapping channel index to trace data.
|
|
669
|
+
clock_trace: Clock signal trace.
|
|
670
|
+
config: Bus configuration.
|
|
671
|
+
sample_rate: Sample rate of traces in Hz.
|
|
672
|
+
edge: Which clock edge to sample on.
|
|
673
|
+
|
|
674
|
+
Returns:
|
|
675
|
+
List of BusTransaction objects.
|
|
676
|
+
|
|
677
|
+
Example:
|
|
678
|
+
>>> transactions = sample_at_clock(bit_traces, clock, config, 100e6, 'rising')
|
|
679
|
+
"""
|
|
680
|
+
decoder = BusDecoder(config, sample_rate)
|
|
681
|
+
return decoder.sample_at_clock(bit_traces, clock_trace, edge)
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
__all__ = [
|
|
685
|
+
"BusConfig",
|
|
686
|
+
"BusDecoder",
|
|
687
|
+
"BusTransaction",
|
|
688
|
+
"ParallelBusConfig",
|
|
689
|
+
"decode_bus",
|
|
690
|
+
"sample_at_clock",
|
|
691
|
+
]
|