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,461 @@
|
|
|
1
|
+
"""Stimulus-response mapping for CAN bus reverse engineering.
|
|
2
|
+
|
|
3
|
+
This module helps identify which CAN messages and signals change in response
|
|
4
|
+
to user actions, enabling rapid identification of relevant data during reverse
|
|
5
|
+
engineering work.
|
|
6
|
+
|
|
7
|
+
The primary use case is comparing:
|
|
8
|
+
1. Baseline capture (no actions)
|
|
9
|
+
2. Stimulus capture (button press, pedal movement, etc.)
|
|
10
|
+
|
|
11
|
+
This allows answering questions like:
|
|
12
|
+
- "What messages change when I press the brake?"
|
|
13
|
+
- "Which signals react to throttle position?"
|
|
14
|
+
- "What initializes when I turn the key?"
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from dataclasses import dataclass, field
|
|
20
|
+
from typing import TYPE_CHECKING
|
|
21
|
+
|
|
22
|
+
import numpy as np
|
|
23
|
+
from scipy import stats
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from oscura.automotive.can.models import CANMessage
|
|
27
|
+
from oscura.automotive.can.session import CANSession
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"ByteChange",
|
|
31
|
+
"FrequencyChange",
|
|
32
|
+
"StimulusResponseAnalyzer",
|
|
33
|
+
"StimulusResponseReport",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class ByteChange:
|
|
39
|
+
"""Detected change in a specific byte position.
|
|
40
|
+
|
|
41
|
+
Attributes:
|
|
42
|
+
byte_position: Byte position (0-7 for CAN 2.0).
|
|
43
|
+
baseline_values: Set of values observed in baseline.
|
|
44
|
+
stimulus_values: Set of values observed in stimulus.
|
|
45
|
+
change_magnitude: Normalized change magnitude (0.0-1.0).
|
|
46
|
+
value_range_change: Change in value range (max - min).
|
|
47
|
+
mean_change: Change in mean value.
|
|
48
|
+
new_values: Values that appear only in stimulus.
|
|
49
|
+
disappeared_values: Values that appear only in baseline.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
byte_position: int
|
|
53
|
+
baseline_values: set[int]
|
|
54
|
+
stimulus_values: set[int]
|
|
55
|
+
change_magnitude: float
|
|
56
|
+
value_range_change: float
|
|
57
|
+
mean_change: float
|
|
58
|
+
new_values: set[int] = field(default_factory=set)
|
|
59
|
+
disappeared_values: set[int] = field(default_factory=set)
|
|
60
|
+
|
|
61
|
+
def __post_init__(self) -> None:
|
|
62
|
+
"""Calculate derived fields."""
|
|
63
|
+
self.new_values = self.stimulus_values - self.baseline_values
|
|
64
|
+
self.disappeared_values = self.baseline_values - self.stimulus_values
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass
|
|
68
|
+
class FrequencyChange:
|
|
69
|
+
"""Detected frequency change for a message ID.
|
|
70
|
+
|
|
71
|
+
Attributes:
|
|
72
|
+
message_id: CAN arbitration ID.
|
|
73
|
+
baseline_hz: Frequency in baseline (Hz).
|
|
74
|
+
stimulus_hz: Frequency in stimulus (Hz).
|
|
75
|
+
change_ratio: Ratio of stimulus to baseline frequency.
|
|
76
|
+
change_type: Type of change ('increased', 'decreased', 'appeared', 'disappeared').
|
|
77
|
+
significance: Statistical significance (0.0-1.0).
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
message_id: int
|
|
81
|
+
baseline_hz: float
|
|
82
|
+
stimulus_hz: float
|
|
83
|
+
change_ratio: float
|
|
84
|
+
change_type: str
|
|
85
|
+
significance: float
|
|
86
|
+
|
|
87
|
+
def __repr__(self) -> str:
|
|
88
|
+
"""Human-readable representation."""
|
|
89
|
+
return (
|
|
90
|
+
f"FrequencyChange(0x{self.message_id:03X}: "
|
|
91
|
+
f"{self.baseline_hz:.1f}Hz -> {self.stimulus_hz:.1f}Hz, "
|
|
92
|
+
f"ratio={self.change_ratio:.2f}, {self.change_type})"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@dataclass
|
|
97
|
+
class StimulusResponseReport:
|
|
98
|
+
"""Complete stimulus-response analysis report.
|
|
99
|
+
|
|
100
|
+
Attributes:
|
|
101
|
+
changed_messages: Message IDs with detected changes.
|
|
102
|
+
new_messages: Message IDs only in stimulus.
|
|
103
|
+
disappeared_messages: Message IDs only in baseline.
|
|
104
|
+
frequency_changes: Frequency changes by message ID.
|
|
105
|
+
byte_changes: Byte-level changes by message ID.
|
|
106
|
+
duration_baseline: Duration of baseline session (seconds).
|
|
107
|
+
duration_stimulus: Duration of stimulus session (seconds).
|
|
108
|
+
confidence_threshold: Minimum confidence used for detection.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
changed_messages: list[int]
|
|
112
|
+
new_messages: list[int]
|
|
113
|
+
disappeared_messages: list[int]
|
|
114
|
+
frequency_changes: dict[int, FrequencyChange]
|
|
115
|
+
byte_changes: dict[int, list[ByteChange]]
|
|
116
|
+
duration_baseline: float
|
|
117
|
+
duration_stimulus: float
|
|
118
|
+
confidence_threshold: float
|
|
119
|
+
|
|
120
|
+
def summary(self) -> str:
|
|
121
|
+
"""Generate human-readable summary.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
Multi-line summary string.
|
|
125
|
+
"""
|
|
126
|
+
lines = [
|
|
127
|
+
"=== Stimulus-Response Analysis ===",
|
|
128
|
+
f"Baseline duration: {self.duration_baseline:.2f}s",
|
|
129
|
+
f"Stimulus duration: {self.duration_stimulus:.2f}s",
|
|
130
|
+
f"Confidence threshold: {self.confidence_threshold:.2f}",
|
|
131
|
+
"",
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
if self.new_messages:
|
|
135
|
+
lines.append(f"New Messages ({len(self.new_messages)}):")
|
|
136
|
+
for msg_id in sorted(self.new_messages):
|
|
137
|
+
lines.append(f" 0x{msg_id:03X}")
|
|
138
|
+
lines.append("")
|
|
139
|
+
|
|
140
|
+
if self.disappeared_messages:
|
|
141
|
+
lines.append(f"Disappeared Messages ({len(self.disappeared_messages)}):")
|
|
142
|
+
for msg_id in sorted(self.disappeared_messages):
|
|
143
|
+
lines.append(f" 0x{msg_id:03X}")
|
|
144
|
+
lines.append("")
|
|
145
|
+
|
|
146
|
+
if self.frequency_changes:
|
|
147
|
+
lines.append(f"Frequency Changes ({len(self.frequency_changes)}):")
|
|
148
|
+
for msg_id in sorted(self.frequency_changes.keys()):
|
|
149
|
+
fc = self.frequency_changes[msg_id]
|
|
150
|
+
lines.append(
|
|
151
|
+
f" 0x{msg_id:03X}: {fc.baseline_hz:.1f}Hz -> {fc.stimulus_hz:.1f}Hz "
|
|
152
|
+
f"({fc.change_type}, sig={fc.significance:.2f})"
|
|
153
|
+
)
|
|
154
|
+
lines.append("")
|
|
155
|
+
|
|
156
|
+
if self.byte_changes:
|
|
157
|
+
lines.append(f"Byte-Level Changes ({len(self.byte_changes)} messages):")
|
|
158
|
+
for msg_id in sorted(self.byte_changes.keys()):
|
|
159
|
+
changes = self.byte_changes[msg_id]
|
|
160
|
+
lines.append(f" 0x{msg_id:03X}: {len(changes)} bytes changed")
|
|
161
|
+
for bc in changes:
|
|
162
|
+
lines.append(
|
|
163
|
+
f" Byte {bc.byte_position}: "
|
|
164
|
+
f"magnitude={bc.change_magnitude:.2f}, "
|
|
165
|
+
f"mean_change={bc.mean_change:.1f}"
|
|
166
|
+
)
|
|
167
|
+
lines.append("")
|
|
168
|
+
|
|
169
|
+
if not any([self.new_messages, self.disappeared_messages, self.changed_messages]):
|
|
170
|
+
lines.append("No significant changes detected.")
|
|
171
|
+
|
|
172
|
+
return "\n".join(lines)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class StimulusResponseAnalyzer:
|
|
176
|
+
"""Analyzer for detecting CAN message changes between sessions.
|
|
177
|
+
|
|
178
|
+
This class compares a baseline session (no user action) against a stimulus
|
|
179
|
+
session (with user action) to identify which messages and signals respond
|
|
180
|
+
to the stimulus.
|
|
181
|
+
"""
|
|
182
|
+
|
|
183
|
+
def detect_responses(
|
|
184
|
+
self,
|
|
185
|
+
baseline_session: CANSession,
|
|
186
|
+
stimulus_session: CANSession,
|
|
187
|
+
time_window_ms: float = 100,
|
|
188
|
+
change_threshold: float = 0.1,
|
|
189
|
+
) -> StimulusResponseReport:
|
|
190
|
+
"""Detect which messages changed between sessions.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
baseline_session: Baseline capture (no action).
|
|
194
|
+
stimulus_session: Stimulus capture (with action).
|
|
195
|
+
time_window_ms: Time window for aligning messages (milliseconds).
|
|
196
|
+
change_threshold: Minimum normalized change to report (0.0-1.0).
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
StimulusResponseReport with detected changes.
|
|
200
|
+
"""
|
|
201
|
+
# Get message IDs from both sessions
|
|
202
|
+
baseline_ids = baseline_session.unique_ids()
|
|
203
|
+
stimulus_ids = stimulus_session.unique_ids()
|
|
204
|
+
|
|
205
|
+
# Detect new and disappeared messages
|
|
206
|
+
new_messages = sorted(stimulus_ids - baseline_ids)
|
|
207
|
+
disappeared_messages = sorted(baseline_ids - stimulus_ids)
|
|
208
|
+
|
|
209
|
+
# Messages present in both sessions
|
|
210
|
+
common_ids = baseline_ids & stimulus_ids
|
|
211
|
+
|
|
212
|
+
# Detect frequency changes
|
|
213
|
+
frequency_changes = {}
|
|
214
|
+
for msg_id in common_ids:
|
|
215
|
+
freq_change = self._detect_frequency_change(baseline_session, stimulus_session, msg_id)
|
|
216
|
+
if freq_change and freq_change.significance >= change_threshold:
|
|
217
|
+
frequency_changes[msg_id] = freq_change
|
|
218
|
+
|
|
219
|
+
# Detect byte-level changes
|
|
220
|
+
byte_changes = {}
|
|
221
|
+
changed_messages = []
|
|
222
|
+
for msg_id in common_ids:
|
|
223
|
+
changes = self.analyze_signal_changes(
|
|
224
|
+
baseline_session, stimulus_session, msg_id, byte_threshold=1
|
|
225
|
+
)
|
|
226
|
+
# Filter by change threshold
|
|
227
|
+
significant_changes = [c for c in changes if c.change_magnitude >= change_threshold]
|
|
228
|
+
if significant_changes:
|
|
229
|
+
byte_changes[msg_id] = significant_changes
|
|
230
|
+
changed_messages.append(msg_id)
|
|
231
|
+
|
|
232
|
+
# Get durations
|
|
233
|
+
baseline_start, baseline_end = baseline_session.time_range()
|
|
234
|
+
stimulus_start, stimulus_end = stimulus_session.time_range()
|
|
235
|
+
|
|
236
|
+
return StimulusResponseReport(
|
|
237
|
+
changed_messages=sorted(set(changed_messages) | set(frequency_changes.keys())),
|
|
238
|
+
new_messages=new_messages,
|
|
239
|
+
disappeared_messages=disappeared_messages,
|
|
240
|
+
frequency_changes=frequency_changes,
|
|
241
|
+
byte_changes=byte_changes,
|
|
242
|
+
duration_baseline=baseline_end - baseline_start,
|
|
243
|
+
duration_stimulus=stimulus_end - stimulus_start,
|
|
244
|
+
confidence_threshold=change_threshold,
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
def analyze_signal_changes(
|
|
248
|
+
self,
|
|
249
|
+
baseline_session: CANSession,
|
|
250
|
+
stimulus_session: CANSession,
|
|
251
|
+
message_id: int,
|
|
252
|
+
byte_threshold: int = 1,
|
|
253
|
+
) -> list[ByteChange]:
|
|
254
|
+
"""Analyze byte-level changes in a specific message.
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
baseline_session: Baseline capture.
|
|
258
|
+
stimulus_session: Stimulus capture.
|
|
259
|
+
message_id: CAN arbitration ID to analyze.
|
|
260
|
+
byte_threshold: Minimum number of unique values to consider changing.
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
List of ByteChange objects for changed bytes.
|
|
264
|
+
"""
|
|
265
|
+
# Get messages for this ID from both sessions
|
|
266
|
+
baseline_msgs = baseline_session._messages.filter_by_id(message_id)
|
|
267
|
+
stimulus_msgs = stimulus_session._messages.filter_by_id(message_id)
|
|
268
|
+
|
|
269
|
+
if not baseline_msgs.messages or not stimulus_msgs.messages:
|
|
270
|
+
return []
|
|
271
|
+
|
|
272
|
+
# Determine max DLC
|
|
273
|
+
max_dlc = max(
|
|
274
|
+
max(msg.dlc for msg in baseline_msgs.messages),
|
|
275
|
+
max(msg.dlc for msg in stimulus_msgs.messages),
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
changes = []
|
|
279
|
+
for byte_pos in range(max_dlc):
|
|
280
|
+
# Extract byte values from both sessions
|
|
281
|
+
baseline_values = [
|
|
282
|
+
msg.data[byte_pos] for msg in baseline_msgs.messages if len(msg.data) > byte_pos
|
|
283
|
+
]
|
|
284
|
+
stimulus_values = [
|
|
285
|
+
msg.data[byte_pos] for msg in stimulus_msgs.messages if len(msg.data) > byte_pos
|
|
286
|
+
]
|
|
287
|
+
|
|
288
|
+
if not baseline_values or not stimulus_values:
|
|
289
|
+
continue
|
|
290
|
+
|
|
291
|
+
# Analyze changes
|
|
292
|
+
baseline_set = set(baseline_values)
|
|
293
|
+
stimulus_set = set(stimulus_values)
|
|
294
|
+
|
|
295
|
+
# Skip if not enough unique values
|
|
296
|
+
if len(baseline_set) < byte_threshold and len(stimulus_set) < byte_threshold:
|
|
297
|
+
continue
|
|
298
|
+
|
|
299
|
+
# Calculate statistics
|
|
300
|
+
baseline_arr = np.array(baseline_values)
|
|
301
|
+
stimulus_arr = np.array(stimulus_values)
|
|
302
|
+
|
|
303
|
+
baseline_mean = float(np.mean(baseline_arr))
|
|
304
|
+
stimulus_mean = float(np.mean(stimulus_arr))
|
|
305
|
+
mean_change = stimulus_mean - baseline_mean
|
|
306
|
+
|
|
307
|
+
baseline_range = float(np.max(baseline_arr) - np.min(baseline_arr))
|
|
308
|
+
stimulus_range = float(np.max(stimulus_arr) - np.min(stimulus_arr))
|
|
309
|
+
value_range_change = stimulus_range - baseline_range
|
|
310
|
+
|
|
311
|
+
# Calculate normalized change magnitude using multiple factors
|
|
312
|
+
# 1. Mean change (normalized by full byte range)
|
|
313
|
+
mean_change_norm = abs(mean_change) / 255.0
|
|
314
|
+
|
|
315
|
+
# 2. Range change (normalized by full byte range)
|
|
316
|
+
range_change_norm = abs(value_range_change) / 255.0
|
|
317
|
+
|
|
318
|
+
# 3. Set difference (Jaccard distance)
|
|
319
|
+
union_size = len(baseline_set | stimulus_set)
|
|
320
|
+
intersection_size = len(baseline_set & stimulus_set)
|
|
321
|
+
if union_size > 0:
|
|
322
|
+
jaccard_dist = 1.0 - (intersection_size / union_size)
|
|
323
|
+
else:
|
|
324
|
+
jaccard_dist = 0.0
|
|
325
|
+
|
|
326
|
+
# 4. Distribution change (Kolmogorov-Smirnov test)
|
|
327
|
+
try:
|
|
328
|
+
ks_stat, _ = stats.ks_2samp(baseline_arr, stimulus_arr)
|
|
329
|
+
ks_change_norm = float(ks_stat)
|
|
330
|
+
except Exception:
|
|
331
|
+
ks_change_norm = 0.0
|
|
332
|
+
|
|
333
|
+
# Combine factors (weighted average)
|
|
334
|
+
change_magnitude = (
|
|
335
|
+
0.3 * mean_change_norm
|
|
336
|
+
+ 0.2 * range_change_norm
|
|
337
|
+
+ 0.3 * jaccard_dist
|
|
338
|
+
+ 0.2 * ks_change_norm
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
# Only report if there's a meaningful change
|
|
342
|
+
if change_magnitude > 0.0:
|
|
343
|
+
changes.append(
|
|
344
|
+
ByteChange(
|
|
345
|
+
byte_position=byte_pos,
|
|
346
|
+
baseline_values=baseline_set,
|
|
347
|
+
stimulus_values=stimulus_set,
|
|
348
|
+
change_magnitude=change_magnitude,
|
|
349
|
+
value_range_change=value_range_change,
|
|
350
|
+
mean_change=mean_change,
|
|
351
|
+
)
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
return changes
|
|
355
|
+
|
|
356
|
+
def find_responsive_messages(
|
|
357
|
+
self,
|
|
358
|
+
baseline_session: CANSession,
|
|
359
|
+
stimulus_session: CANSession,
|
|
360
|
+
) -> list[int]:
|
|
361
|
+
"""Find message IDs that changed.
|
|
362
|
+
|
|
363
|
+
This is a convenience method that returns just the list of message IDs
|
|
364
|
+
that showed any type of change.
|
|
365
|
+
|
|
366
|
+
Args:
|
|
367
|
+
baseline_session: Baseline capture.
|
|
368
|
+
stimulus_session: Stimulus capture.
|
|
369
|
+
|
|
370
|
+
Returns:
|
|
371
|
+
Sorted list of message IDs that changed.
|
|
372
|
+
"""
|
|
373
|
+
report = self.detect_responses(baseline_session, stimulus_session)
|
|
374
|
+
return report.changed_messages + report.new_messages
|
|
375
|
+
|
|
376
|
+
def _detect_frequency_change(
|
|
377
|
+
self,
|
|
378
|
+
baseline_session: CANSession,
|
|
379
|
+
stimulus_session: CANSession,
|
|
380
|
+
message_id: int,
|
|
381
|
+
) -> FrequencyChange | None:
|
|
382
|
+
"""Detect frequency change for a specific message ID.
|
|
383
|
+
|
|
384
|
+
Args:
|
|
385
|
+
baseline_session: Baseline capture.
|
|
386
|
+
stimulus_session: Stimulus capture.
|
|
387
|
+
message_id: CAN arbitration ID.
|
|
388
|
+
|
|
389
|
+
Returns:
|
|
390
|
+
FrequencyChange if detected, None otherwise.
|
|
391
|
+
"""
|
|
392
|
+
# Get messages
|
|
393
|
+
baseline_msgs = baseline_session._messages.filter_by_id(message_id)
|
|
394
|
+
stimulus_msgs = stimulus_session._messages.filter_by_id(message_id)
|
|
395
|
+
|
|
396
|
+
# Calculate frequencies
|
|
397
|
+
baseline_hz = self._calculate_frequency(
|
|
398
|
+
baseline_msgs.messages, baseline_session.time_range()
|
|
399
|
+
)
|
|
400
|
+
stimulus_hz = self._calculate_frequency(
|
|
401
|
+
stimulus_msgs.messages, stimulus_session.time_range()
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
# Determine change type and ratio
|
|
405
|
+
if baseline_hz == 0.0 and stimulus_hz > 0.0:
|
|
406
|
+
change_type = "appeared"
|
|
407
|
+
change_ratio = float("inf")
|
|
408
|
+
significance = 1.0
|
|
409
|
+
elif stimulus_hz == 0.0 and baseline_hz > 0.0:
|
|
410
|
+
change_type = "disappeared"
|
|
411
|
+
change_ratio = 0.0
|
|
412
|
+
significance = 1.0
|
|
413
|
+
elif baseline_hz > 0.0:
|
|
414
|
+
change_ratio = stimulus_hz / baseline_hz
|
|
415
|
+
|
|
416
|
+
# Determine if change is significant
|
|
417
|
+
# Use a threshold of 20% change
|
|
418
|
+
if change_ratio > 1.2:
|
|
419
|
+
change_type = "increased"
|
|
420
|
+
significance = min(1.0, (change_ratio - 1.0) / 1.0)
|
|
421
|
+
elif change_ratio < 0.8:
|
|
422
|
+
change_type = "decreased"
|
|
423
|
+
significance = min(1.0, (1.0 - change_ratio) / 1.0)
|
|
424
|
+
else:
|
|
425
|
+
# No significant change
|
|
426
|
+
return None
|
|
427
|
+
else:
|
|
428
|
+
# Both frequencies are zero
|
|
429
|
+
return None
|
|
430
|
+
|
|
431
|
+
return FrequencyChange(
|
|
432
|
+
message_id=message_id,
|
|
433
|
+
baseline_hz=baseline_hz,
|
|
434
|
+
stimulus_hz=stimulus_hz,
|
|
435
|
+
change_ratio=change_ratio,
|
|
436
|
+
change_type=change_type,
|
|
437
|
+
significance=significance,
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
@staticmethod
|
|
441
|
+
def _calculate_frequency(messages: list[CANMessage], time_range: tuple[float, float]) -> float:
|
|
442
|
+
"""Calculate message frequency.
|
|
443
|
+
|
|
444
|
+
Args:
|
|
445
|
+
messages: List of CAN messages.
|
|
446
|
+
time_range: Tuple of (start_time, end_time).
|
|
447
|
+
|
|
448
|
+
Returns:
|
|
449
|
+
Frequency in Hz.
|
|
450
|
+
"""
|
|
451
|
+
if not messages or len(messages) < 2:
|
|
452
|
+
return 0.0
|
|
453
|
+
|
|
454
|
+
start_time, end_time = time_range
|
|
455
|
+
duration = end_time - start_time
|
|
456
|
+
|
|
457
|
+
if duration <= 0.0:
|
|
458
|
+
return 0.0
|
|
459
|
+
|
|
460
|
+
# Use message count divided by duration
|
|
461
|
+
return (len(messages) - 1) / duration
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""DBC database support for CAN signal definitions.
|
|
2
|
+
|
|
3
|
+
This module provides DBC file parsing and generation capabilities.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
__all__ = ["DBCGenerator", "DBCParser", "load_dbc"]
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
from oscura.automotive.dbc.generator import DBCGenerator
|
|
12
|
+
from oscura.automotive.dbc.parser import DBCParser, load_dbc
|
|
13
|
+
except ImportError:
|
|
14
|
+
# Optional dependencies not installed
|
|
15
|
+
pass
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""DBC file generator from discovery documents.
|
|
2
|
+
|
|
3
|
+
This module generates standard DBC files from TraceKit discovery documents,
|
|
4
|
+
enabling export of reverse-engineered protocols to industry-standard format.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from oscura.automotive.can.discovery import (
|
|
14
|
+
DiscoveryDocument,
|
|
15
|
+
)
|
|
16
|
+
from oscura.automotive.can.session import CANSession
|
|
17
|
+
|
|
18
|
+
__all__ = ["DBCGenerator"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class DBCGenerator:
|
|
22
|
+
"""Generate DBC files from discovery documents."""
|
|
23
|
+
|
|
24
|
+
@staticmethod
|
|
25
|
+
def generate(
|
|
26
|
+
discovery: DiscoveryDocument,
|
|
27
|
+
output_path: Path | str,
|
|
28
|
+
min_confidence: float = 0.0,
|
|
29
|
+
include_comments: bool = True,
|
|
30
|
+
) -> None:
|
|
31
|
+
"""Generate DBC file from discovery document.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
discovery: DiscoveryDocument with discovered signals.
|
|
35
|
+
output_path: Output DBC file path.
|
|
36
|
+
min_confidence: Minimum confidence threshold for including signals.
|
|
37
|
+
include_comments: Include evidence as comments in DBC.
|
|
38
|
+
"""
|
|
39
|
+
path = Path(output_path)
|
|
40
|
+
|
|
41
|
+
lines = []
|
|
42
|
+
|
|
43
|
+
# DBC header
|
|
44
|
+
lines.append('VERSION ""')
|
|
45
|
+
lines.append("")
|
|
46
|
+
lines.append("NS_ :")
|
|
47
|
+
lines.append("")
|
|
48
|
+
lines.append("BS_:")
|
|
49
|
+
lines.append("")
|
|
50
|
+
lines.append("BU_:") # No nodes defined
|
|
51
|
+
lines.append("")
|
|
52
|
+
|
|
53
|
+
# Generate messages
|
|
54
|
+
for msg in sorted(discovery.messages.values(), key=lambda m: m.id):
|
|
55
|
+
# Filter signals by confidence
|
|
56
|
+
signals = [s for s in msg.signals if s.confidence >= min_confidence]
|
|
57
|
+
|
|
58
|
+
if not signals:
|
|
59
|
+
continue # Skip messages with no high-confidence signals
|
|
60
|
+
|
|
61
|
+
# Message definition
|
|
62
|
+
# Format: BO_ <ID> <Name>: <DLC> <Transmitter>
|
|
63
|
+
transmitter = msg.transmitter if msg.transmitter else "Vector__XXX"
|
|
64
|
+
lines.append(f"BO_ {msg.id} {msg.name}: {msg.length} {transmitter}")
|
|
65
|
+
|
|
66
|
+
# Signals
|
|
67
|
+
for sig in signals:
|
|
68
|
+
# Format: SG_ <Name> : <StartBit>|<Length>@<ByteOrder><ValueType> (<Scale>,<Offset>) [<Min>|<Max>] "<Unit>" <Receiver>
|
|
69
|
+
byte_order = "0" if sig.byte_order == "big_endian" else "1"
|
|
70
|
+
value_type = "+" if sig.value_type == "unsigned" else "-"
|
|
71
|
+
|
|
72
|
+
min_val = sig.min_value if sig.min_value is not None else 0
|
|
73
|
+
max_val = sig.max_value if sig.max_value is not None else 0
|
|
74
|
+
|
|
75
|
+
signal_line = (
|
|
76
|
+
f" SG_ {sig.name} : {sig.start_bit}|{sig.length}@{byte_order}{value_type} "
|
|
77
|
+
f"({sig.scale},{sig.offset}) [{min_val}|{max_val}] "
|
|
78
|
+
f'"{sig.unit}" Vector__XXX'
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
lines.append(signal_line)
|
|
82
|
+
|
|
83
|
+
# Add comment if evidence exists
|
|
84
|
+
if include_comments and sig.evidence:
|
|
85
|
+
evidence_str = "; ".join(sig.evidence)
|
|
86
|
+
# DBC comments format: CM_ SG_ <ID> <SignalName> "<Comment>";
|
|
87
|
+
comment_line = f'CM_ SG_ {msg.id} {sig.name} "{evidence_str}";'
|
|
88
|
+
lines.append("")
|
|
89
|
+
lines.append(comment_line)
|
|
90
|
+
|
|
91
|
+
lines.append("")
|
|
92
|
+
|
|
93
|
+
# Write file with latin-1 encoding (DBC standard)
|
|
94
|
+
with open(path, "w", encoding="latin-1") as f:
|
|
95
|
+
f.write("\n".join(lines))
|
|
96
|
+
|
|
97
|
+
@staticmethod
|
|
98
|
+
def generate_from_session(
|
|
99
|
+
session: CANSession,
|
|
100
|
+
output_path: Path | str,
|
|
101
|
+
min_confidence: float = 0.8, # noqa: ARG004 - API compatibility parameter
|
|
102
|
+
) -> None:
|
|
103
|
+
"""Generate DBC file from CANSession with documented signals.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
session: CANSession with documented signals.
|
|
107
|
+
output_path: Output DBC file path.
|
|
108
|
+
min_confidence: Minimum confidence threshold (reserved for future use).
|
|
109
|
+
"""
|
|
110
|
+
from oscura.automotive.can.discovery import (
|
|
111
|
+
DiscoveryDocument,
|
|
112
|
+
MessageDiscovery,
|
|
113
|
+
SignalDiscovery,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# Build discovery document from session
|
|
117
|
+
doc = DiscoveryDocument()
|
|
118
|
+
|
|
119
|
+
# Get all unique IDs that have documented signals
|
|
120
|
+
for arb_id in session.unique_ids():
|
|
121
|
+
try:
|
|
122
|
+
msg_wrapper = session.message(arb_id)
|
|
123
|
+
documented = msg_wrapper.get_documented_signals()
|
|
124
|
+
|
|
125
|
+
if documented:
|
|
126
|
+
# Create message discovery
|
|
127
|
+
analysis = session.analyze_message(arb_id)
|
|
128
|
+
|
|
129
|
+
signal_discoveries = []
|
|
130
|
+
for sig_def in documented.values():
|
|
131
|
+
sig_disc = SignalDiscovery.from_definition(
|
|
132
|
+
sig_def,
|
|
133
|
+
confidence=1.0,
|
|
134
|
+
evidence=[],
|
|
135
|
+
)
|
|
136
|
+
signal_discoveries.append(sig_disc)
|
|
137
|
+
|
|
138
|
+
msg_disc = MessageDiscovery(
|
|
139
|
+
id=arb_id,
|
|
140
|
+
name=f"Message_{arb_id:03X}",
|
|
141
|
+
length=max(
|
|
142
|
+
msg.dlc for msg in session._messages.filter_by_id(arb_id).messages
|
|
143
|
+
),
|
|
144
|
+
cycle_time_ms=analysis.period_ms,
|
|
145
|
+
confidence=1.0,
|
|
146
|
+
signals=signal_discoveries,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
doc.add_message(msg_disc)
|
|
150
|
+
|
|
151
|
+
except Exception:
|
|
152
|
+
# Skip messages without documented signals
|
|
153
|
+
pass
|
|
154
|
+
|
|
155
|
+
# Generate DBC
|
|
156
|
+
DBCGenerator.generate(doc, output_path, min_confidence=0.0)
|