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,639 @@
|
|
|
1
|
+
"""High-level reverse engineering workflow for unknown signals.
|
|
2
|
+
|
|
3
|
+
This module provides a complete workflow for reverse engineering unknown
|
|
4
|
+
digital signals from initial capture to protocol understanding.
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
>>> import oscura as osc
|
|
8
|
+
>>> trace = osc.load("unknown_capture.wfm")
|
|
9
|
+
>>> result = osc.workflows.reverse_engineer_signal(trace)
|
|
10
|
+
>>> print(result.protocol_spec)
|
|
11
|
+
>>> print(f"Detected baud rate: {result.baud_rate}")
|
|
12
|
+
>>> print(f"Frames decoded: {len(result.frames)}")
|
|
13
|
+
|
|
14
|
+
The workflow includes:
|
|
15
|
+
1. Signal characterization (voltage levels, signal type)
|
|
16
|
+
2. Clock recovery / baud rate detection
|
|
17
|
+
3. Bit stream extraction
|
|
18
|
+
4. Frame boundary detection
|
|
19
|
+
5. Sync pattern identification
|
|
20
|
+
6. Field structure inference
|
|
21
|
+
7. Checksum analysis
|
|
22
|
+
8. Protocol specification generation
|
|
23
|
+
|
|
24
|
+
References:
|
|
25
|
+
- sigrok Protocol Analysis
|
|
26
|
+
- UART: TIA-232-F
|
|
27
|
+
- I2C: NXP UM10204
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
from dataclasses import dataclass, field
|
|
33
|
+
from typing import TYPE_CHECKING, Any
|
|
34
|
+
|
|
35
|
+
import numpy as np
|
|
36
|
+
|
|
37
|
+
if TYPE_CHECKING:
|
|
38
|
+
from oscura.core.types import WaveformTrace
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class InferredFrame:
|
|
43
|
+
"""An inferred protocol frame.
|
|
44
|
+
|
|
45
|
+
Attributes:
|
|
46
|
+
start_bit: Starting bit index.
|
|
47
|
+
end_bit: Ending bit index.
|
|
48
|
+
raw_bits: Raw bit string.
|
|
49
|
+
raw_bytes: Raw bytes.
|
|
50
|
+
fields: Identified field mapping.
|
|
51
|
+
checksum_valid: Whether checksum validated (None if unknown).
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
start_bit: int
|
|
55
|
+
end_bit: int
|
|
56
|
+
raw_bits: str
|
|
57
|
+
raw_bytes: bytes
|
|
58
|
+
fields: dict[str, bytes] = field(default_factory=dict)
|
|
59
|
+
checksum_valid: bool | None = None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
class FieldSpec:
|
|
64
|
+
"""Specification for an inferred field.
|
|
65
|
+
|
|
66
|
+
Attributes:
|
|
67
|
+
name: Field name.
|
|
68
|
+
offset: Byte offset in frame.
|
|
69
|
+
size: Size in bytes (or expression for variable).
|
|
70
|
+
field_type: Data type (uint8, bytes, checksum, etc.).
|
|
71
|
+
value: Example or constant value.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
name: str
|
|
75
|
+
offset: int
|
|
76
|
+
size: int | str
|
|
77
|
+
field_type: str
|
|
78
|
+
value: Any = None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass
|
|
82
|
+
class ProtocolSpec:
|
|
83
|
+
"""Inferred protocol specification.
|
|
84
|
+
|
|
85
|
+
Attributes:
|
|
86
|
+
name: Protocol name.
|
|
87
|
+
baud_rate: Detected baud rate.
|
|
88
|
+
frame_format: Frame format (e.g., "8N1").
|
|
89
|
+
sync_pattern: Detected sync pattern (hex string).
|
|
90
|
+
frame_length: Frame length in bytes (or None if variable).
|
|
91
|
+
fields: List of field specifications.
|
|
92
|
+
checksum_type: Detected checksum type (or None).
|
|
93
|
+
checksum_position: Position of checksum in frame.
|
|
94
|
+
confidence: Overall confidence score (0-1).
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
name: str
|
|
98
|
+
baud_rate: float
|
|
99
|
+
frame_format: str
|
|
100
|
+
sync_pattern: str
|
|
101
|
+
frame_length: int | None
|
|
102
|
+
fields: list[FieldSpec]
|
|
103
|
+
checksum_type: str | None
|
|
104
|
+
checksum_position: int | None
|
|
105
|
+
confidence: float
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@dataclass
|
|
109
|
+
class ReverseEngineeringResult:
|
|
110
|
+
"""Complete results from reverse engineering workflow.
|
|
111
|
+
|
|
112
|
+
Attributes:
|
|
113
|
+
protocol_spec: Inferred protocol specification.
|
|
114
|
+
frames: List of decoded frames.
|
|
115
|
+
baud_rate: Detected baud rate.
|
|
116
|
+
bit_stream: Extracted bit stream.
|
|
117
|
+
byte_stream: Extracted byte stream.
|
|
118
|
+
sync_positions: Positions where sync patterns found.
|
|
119
|
+
characterization: Signal characterization results.
|
|
120
|
+
confidence: Overall analysis confidence (0-1).
|
|
121
|
+
warnings: List of analysis warnings.
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
protocol_spec: ProtocolSpec
|
|
125
|
+
frames: list[InferredFrame]
|
|
126
|
+
baud_rate: float
|
|
127
|
+
bit_stream: str
|
|
128
|
+
byte_stream: bytes
|
|
129
|
+
sync_positions: list[int]
|
|
130
|
+
characterization: dict[str, Any]
|
|
131
|
+
confidence: float
|
|
132
|
+
warnings: list[str]
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def reverse_engineer_signal(
|
|
136
|
+
trace: WaveformTrace,
|
|
137
|
+
*,
|
|
138
|
+
expected_baud_rates: list[int] | None = None,
|
|
139
|
+
min_frames: int = 3,
|
|
140
|
+
max_frame_length: int = 256,
|
|
141
|
+
checksum_types: list[str] | None = None,
|
|
142
|
+
) -> ReverseEngineeringResult:
|
|
143
|
+
"""Complete reverse engineering workflow for unknown signals.
|
|
144
|
+
|
|
145
|
+
Analyzes an unknown digital signal to infer protocol parameters,
|
|
146
|
+
frame structure, and decode messages.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
trace: Input waveform trace.
|
|
150
|
+
expected_baud_rates: List of expected baud rates to try.
|
|
151
|
+
Default: [9600, 19200, 38400, 57600, 115200].
|
|
152
|
+
min_frames: Minimum frames required for analysis (default 3).
|
|
153
|
+
max_frame_length: Maximum expected frame length in bytes.
|
|
154
|
+
checksum_types: Checksum types to try.
|
|
155
|
+
Default: ["xor", "sum8", "crc8", "crc16"].
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
ReverseEngineeringResult with protocol specification and decoded frames.
|
|
159
|
+
|
|
160
|
+
Example:
|
|
161
|
+
>>> trace = osc.load("unknown_capture.wfm")
|
|
162
|
+
>>> result = osc.workflows.reverse_engineer_signal(trace)
|
|
163
|
+
>>> print(f"Baud rate: {result.baud_rate}")
|
|
164
|
+
>>> print(f"Sync pattern: {result.protocol_spec.sync_pattern}")
|
|
165
|
+
>>> print(f"Frame length: {result.protocol_spec.frame_length} bytes")
|
|
166
|
+
>>> for frame in result.frames[:5]:
|
|
167
|
+
... print(f" {frame.raw_bytes.hex()}")
|
|
168
|
+
"""
|
|
169
|
+
if expected_baud_rates is None:
|
|
170
|
+
expected_baud_rates = [9600, 19200, 38400, 57600, 115200, 230400, 460800]
|
|
171
|
+
|
|
172
|
+
if checksum_types is None:
|
|
173
|
+
checksum_types = ["xor", "sum8", "crc8", "crc16"]
|
|
174
|
+
|
|
175
|
+
warnings: list[str] = []
|
|
176
|
+
data = trace.data
|
|
177
|
+
sample_rate = trace.metadata.sample_rate
|
|
178
|
+
|
|
179
|
+
# ========== Step 1: Signal Characterization ==========
|
|
180
|
+
characterization = _characterize_signal(data, sample_rate)
|
|
181
|
+
|
|
182
|
+
# ========== Step 2: Clock Recovery ==========
|
|
183
|
+
baud_rate, baud_confidence = _detect_baud_rate(
|
|
184
|
+
data, sample_rate, expected_baud_rates, characterization["threshold"]
|
|
185
|
+
)
|
|
186
|
+
if baud_confidence < 0.7:
|
|
187
|
+
warnings.append(f"Low baud rate confidence: {baud_confidence:.2f}")
|
|
188
|
+
|
|
189
|
+
# ========== Step 3: Bit Stream Extraction ==========
|
|
190
|
+
bit_stream = _extract_bit_stream(data, sample_rate, baud_rate, characterization["threshold"])
|
|
191
|
+
|
|
192
|
+
if len(bit_stream) < 100:
|
|
193
|
+
warnings.append("Short bit stream extracted")
|
|
194
|
+
|
|
195
|
+
# ========== Step 4: Byte Extraction ==========
|
|
196
|
+
byte_positions, byte_stream = _extract_bytes(bit_stream)
|
|
197
|
+
|
|
198
|
+
if len(byte_stream) < 10:
|
|
199
|
+
warnings.append("Few bytes extracted")
|
|
200
|
+
|
|
201
|
+
# ========== Step 5: Sync Pattern Detection ==========
|
|
202
|
+
sync_pattern, sync_positions, sync_confidence = _detect_sync_pattern(
|
|
203
|
+
byte_stream, max_frame_length
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
# ========== Step 6: Frame Extraction ==========
|
|
207
|
+
frames = _extract_frames(bit_stream, byte_stream, byte_positions, sync_positions, sync_pattern)
|
|
208
|
+
|
|
209
|
+
if len(frames) < min_frames:
|
|
210
|
+
warnings.append(f"Only {len(frames)} frames found (minimum {min_frames})")
|
|
211
|
+
|
|
212
|
+
# ========== Step 7: Field Analysis ==========
|
|
213
|
+
field_specs = _infer_fields(frames, sync_pattern)
|
|
214
|
+
|
|
215
|
+
# ========== Step 8: Checksum Analysis ==========
|
|
216
|
+
checksum_type, checksum_pos, checksum_confidence = _detect_checksum(frames, checksum_types)
|
|
217
|
+
|
|
218
|
+
if checksum_type:
|
|
219
|
+
# Validate frames with checksum
|
|
220
|
+
for frame in frames:
|
|
221
|
+
frame.checksum_valid = _verify_checksum(frame.raw_bytes, checksum_type, checksum_pos)
|
|
222
|
+
|
|
223
|
+
# ========== Build Protocol Specification ==========
|
|
224
|
+
frame_lengths = [len(f.raw_bytes) for f in frames]
|
|
225
|
+
frame_length = int(np.median(frame_lengths)) if len(set(frame_lengths)) == 1 else None
|
|
226
|
+
|
|
227
|
+
# Calculate overall confidence
|
|
228
|
+
overall_confidence = (
|
|
229
|
+
baud_confidence * 0.3
|
|
230
|
+
+ sync_confidence * 0.3
|
|
231
|
+
+ checksum_confidence * 0.2
|
|
232
|
+
+ min(len(frames) / 10, 1.0) * 0.2
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
protocol_spec = ProtocolSpec(
|
|
236
|
+
name="Unknown Protocol (Inferred)",
|
|
237
|
+
baud_rate=baud_rate,
|
|
238
|
+
frame_format="8N1", # Most common
|
|
239
|
+
sync_pattern=sync_pattern.hex() if sync_pattern else "",
|
|
240
|
+
frame_length=frame_length,
|
|
241
|
+
fields=field_specs,
|
|
242
|
+
checksum_type=checksum_type,
|
|
243
|
+
checksum_position=checksum_pos,
|
|
244
|
+
confidence=overall_confidence,
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
return ReverseEngineeringResult(
|
|
248
|
+
protocol_spec=protocol_spec,
|
|
249
|
+
frames=frames,
|
|
250
|
+
baud_rate=baud_rate,
|
|
251
|
+
bit_stream=bit_stream,
|
|
252
|
+
byte_stream=byte_stream,
|
|
253
|
+
sync_positions=sync_positions,
|
|
254
|
+
characterization=characterization,
|
|
255
|
+
confidence=overall_confidence,
|
|
256
|
+
warnings=warnings,
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _characterize_signal(
|
|
261
|
+
data: np.ndarray[Any, np.dtype[np.float64]], sample_rate: float
|
|
262
|
+
) -> dict[str, Any]:
|
|
263
|
+
"""Characterize signal voltage levels and type."""
|
|
264
|
+
high_level = float(np.percentile(data, 95))
|
|
265
|
+
low_level = float(np.percentile(data, 5))
|
|
266
|
+
threshold = (high_level + low_level) / 2
|
|
267
|
+
swing = high_level - low_level
|
|
268
|
+
|
|
269
|
+
# Detect if inverted (idle low vs idle high)
|
|
270
|
+
is_inverted = np.mean(data) > threshold
|
|
271
|
+
|
|
272
|
+
# Detect signal type
|
|
273
|
+
signal_type = "digital" # Default assumption
|
|
274
|
+
if swing < 0.5:
|
|
275
|
+
signal_type = "low_swing"
|
|
276
|
+
elif swing > 10:
|
|
277
|
+
signal_type = "high_swing"
|
|
278
|
+
|
|
279
|
+
return {
|
|
280
|
+
"high_level": high_level,
|
|
281
|
+
"low_level": low_level,
|
|
282
|
+
"threshold": threshold,
|
|
283
|
+
"swing": swing,
|
|
284
|
+
"is_inverted": is_inverted,
|
|
285
|
+
"signal_type": signal_type,
|
|
286
|
+
"sample_rate": sample_rate,
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _detect_baud_rate(
|
|
291
|
+
data: np.ndarray[Any, np.dtype[np.float64]],
|
|
292
|
+
sample_rate: float,
|
|
293
|
+
expected_rates: list[int],
|
|
294
|
+
threshold: float,
|
|
295
|
+
) -> tuple[float, float]:
|
|
296
|
+
"""Detect baud rate from edge timing."""
|
|
297
|
+
# Convert to digital
|
|
298
|
+
digital = data > threshold
|
|
299
|
+
edges = np.where(np.diff(digital.astype(int)) != 0)[0]
|
|
300
|
+
|
|
301
|
+
if len(edges) < 20:
|
|
302
|
+
# Default to most common baud rate if not enough edges
|
|
303
|
+
return 115200, 0.3
|
|
304
|
+
|
|
305
|
+
# Measure edge-to-edge intervals
|
|
306
|
+
intervals = np.diff(edges)
|
|
307
|
+
intervals = intervals[intervals > 5] # Filter very short glitches
|
|
308
|
+
|
|
309
|
+
if len(intervals) < 10:
|
|
310
|
+
return 115200, 0.3
|
|
311
|
+
|
|
312
|
+
# Find minimum interval (single bit period)
|
|
313
|
+
min_interval = float(np.percentile(intervals, 5))
|
|
314
|
+
|
|
315
|
+
# Estimate baud rate
|
|
316
|
+
estimated_baud = sample_rate / min_interval
|
|
317
|
+
|
|
318
|
+
# Find closest standard baud rate
|
|
319
|
+
closest_baud = min(expected_rates, key=lambda x: abs(x - estimated_baud))
|
|
320
|
+
|
|
321
|
+
# Calculate confidence based on how close we are
|
|
322
|
+
error_percent = abs(estimated_baud - closest_baud) / closest_baud
|
|
323
|
+
confidence = max(0.0, 1.0 - error_percent * 5)
|
|
324
|
+
|
|
325
|
+
return float(closest_baud), confidence
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _extract_bit_stream(
|
|
329
|
+
data: np.ndarray[Any, np.dtype[np.float64]],
|
|
330
|
+
sample_rate: float,
|
|
331
|
+
baud_rate: float,
|
|
332
|
+
threshold: float,
|
|
333
|
+
) -> str:
|
|
334
|
+
"""Extract bit stream by sampling at bit centers."""
|
|
335
|
+
samples_per_bit = int(sample_rate / baud_rate)
|
|
336
|
+
n_bits = len(data) // samples_per_bit
|
|
337
|
+
|
|
338
|
+
bits: list[str] = []
|
|
339
|
+
for i in range(n_bits):
|
|
340
|
+
sample_idx = i * samples_per_bit + samples_per_bit // 2
|
|
341
|
+
if sample_idx < len(data):
|
|
342
|
+
bit = "1" if data[sample_idx] > threshold else "0"
|
|
343
|
+
bits.append(bit)
|
|
344
|
+
|
|
345
|
+
return "".join(bits)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _extract_bytes(bit_stream: str) -> tuple[list[int], bytes]:
|
|
349
|
+
"""Extract bytes from bit stream (8N1 format)."""
|
|
350
|
+
byte_positions: list[int] = []
|
|
351
|
+
byte_values: list[int] = []
|
|
352
|
+
|
|
353
|
+
bit_pos = 0
|
|
354
|
+
while bit_pos < len(bit_stream) - 10:
|
|
355
|
+
# Look for start bit (0)
|
|
356
|
+
if bit_stream[bit_pos] == "0":
|
|
357
|
+
# Extract 8 data bits (LSB first for UART)
|
|
358
|
+
byte_bits = bit_stream[bit_pos + 1 : bit_pos + 9]
|
|
359
|
+
if len(byte_bits) == 8:
|
|
360
|
+
byte_val = sum(int(byte_bits[i]) << i for i in range(8))
|
|
361
|
+
byte_positions.append(bit_pos)
|
|
362
|
+
byte_values.append(byte_val)
|
|
363
|
+
bit_pos += 10 # Skip to next potential start
|
|
364
|
+
else:
|
|
365
|
+
bit_pos += 1
|
|
366
|
+
|
|
367
|
+
return byte_positions, bytes(byte_values)
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _detect_sync_pattern(
|
|
371
|
+
byte_stream: bytes,
|
|
372
|
+
max_frame_length: int,
|
|
373
|
+
) -> tuple[bytes, list[int], float]:
|
|
374
|
+
"""Detect sync pattern by finding repeated byte sequences."""
|
|
375
|
+
if len(byte_stream) < 20:
|
|
376
|
+
return b"", [], 0.0
|
|
377
|
+
|
|
378
|
+
list(byte_stream)
|
|
379
|
+
|
|
380
|
+
# Try common sync patterns first
|
|
381
|
+
common_patterns = [
|
|
382
|
+
bytes([0xAA, 0x55]),
|
|
383
|
+
bytes([0x55, 0xAA]),
|
|
384
|
+
bytes([0x7E]), # HDLC
|
|
385
|
+
bytes([0xA5]),
|
|
386
|
+
bytes([0x5A]),
|
|
387
|
+
bytes([0xFF, 0x00]),
|
|
388
|
+
]
|
|
389
|
+
|
|
390
|
+
best_pattern = b""
|
|
391
|
+
best_positions: list[int] = []
|
|
392
|
+
best_confidence = 0.0
|
|
393
|
+
|
|
394
|
+
for pattern in common_patterns:
|
|
395
|
+
positions = []
|
|
396
|
+
for i in range(len(byte_stream) - len(pattern)):
|
|
397
|
+
if byte_stream[i : i + len(pattern)] == pattern:
|
|
398
|
+
positions.append(i)
|
|
399
|
+
|
|
400
|
+
if len(positions) >= 3:
|
|
401
|
+
# Check for regular spacing
|
|
402
|
+
spacings = np.diff(positions)
|
|
403
|
+
if len(spacings) > 0:
|
|
404
|
+
median_spacing = np.median(spacings)
|
|
405
|
+
regularity = 1.0 - np.std(spacings) / (median_spacing + 1)
|
|
406
|
+
|
|
407
|
+
confidence = min(len(positions) / 10, 1.0) * max(regularity, 0)
|
|
408
|
+
|
|
409
|
+
if confidence > best_confidence:
|
|
410
|
+
best_pattern = pattern
|
|
411
|
+
best_positions = positions
|
|
412
|
+
best_confidence = confidence
|
|
413
|
+
|
|
414
|
+
# If no common pattern found, try to find repeating sequences
|
|
415
|
+
if best_confidence < 0.5:
|
|
416
|
+
for pattern_len in range(1, 4):
|
|
417
|
+
for start in range(min(50, len(byte_stream) - pattern_len)):
|
|
418
|
+
pattern = byte_stream[start : start + pattern_len]
|
|
419
|
+
positions = []
|
|
420
|
+
for i in range(len(byte_stream) - pattern_len):
|
|
421
|
+
if byte_stream[i : i + pattern_len] == pattern:
|
|
422
|
+
positions.append(i)
|
|
423
|
+
|
|
424
|
+
if len(positions) >= 5:
|
|
425
|
+
spacings = np.diff(positions)
|
|
426
|
+
if len(spacings) > 0 and np.std(spacings) / (np.median(spacings) + 1) < 0.2:
|
|
427
|
+
confidence = min(len(positions) / 10, 1.0)
|
|
428
|
+
if confidence > best_confidence:
|
|
429
|
+
best_pattern = pattern
|
|
430
|
+
best_positions = positions
|
|
431
|
+
best_confidence = confidence
|
|
432
|
+
|
|
433
|
+
return best_pattern, best_positions, best_confidence
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def _extract_frames(
|
|
437
|
+
bit_stream: str,
|
|
438
|
+
byte_stream: bytes,
|
|
439
|
+
byte_positions: list[int],
|
|
440
|
+
sync_positions: list[int],
|
|
441
|
+
sync_pattern: bytes,
|
|
442
|
+
) -> list[InferredFrame]:
|
|
443
|
+
"""Extract frames based on sync positions."""
|
|
444
|
+
frames: list[InferredFrame] = []
|
|
445
|
+
|
|
446
|
+
if not sync_positions or len(sync_positions) < 2:
|
|
447
|
+
return frames
|
|
448
|
+
|
|
449
|
+
# Calculate frame length from sync spacing
|
|
450
|
+
spacings = np.diff(sync_positions)
|
|
451
|
+
frame_length = int(np.median(spacings))
|
|
452
|
+
|
|
453
|
+
for _i, sync_pos in enumerate(sync_positions[:-1]):
|
|
454
|
+
end_pos = min(sync_pos + frame_length, len(byte_stream))
|
|
455
|
+
frame_bytes = byte_stream[sync_pos:end_pos]
|
|
456
|
+
|
|
457
|
+
if len(frame_bytes) < 3:
|
|
458
|
+
continue
|
|
459
|
+
|
|
460
|
+
# Get bit positions
|
|
461
|
+
start_bit = byte_positions[sync_pos] if sync_pos < len(byte_positions) else sync_pos * 10
|
|
462
|
+
end_bit = byte_positions[end_pos - 1] if end_pos - 1 < len(byte_positions) else end_pos * 10
|
|
463
|
+
|
|
464
|
+
frames.append(
|
|
465
|
+
InferredFrame(
|
|
466
|
+
start_bit=start_bit,
|
|
467
|
+
end_bit=end_bit,
|
|
468
|
+
raw_bits=bit_stream[start_bit:end_bit] if end_bit <= len(bit_stream) else "",
|
|
469
|
+
raw_bytes=frame_bytes,
|
|
470
|
+
)
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
return frames
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def _infer_fields(frames: list[InferredFrame], sync_pattern: bytes) -> list[FieldSpec]:
|
|
477
|
+
"""Infer field structure from frames."""
|
|
478
|
+
if not frames:
|
|
479
|
+
return []
|
|
480
|
+
|
|
481
|
+
fields: list[FieldSpec] = []
|
|
482
|
+
sync_len = len(sync_pattern)
|
|
483
|
+
|
|
484
|
+
# Sync field
|
|
485
|
+
if sync_len > 0:
|
|
486
|
+
fields.append(
|
|
487
|
+
FieldSpec(
|
|
488
|
+
name="sync",
|
|
489
|
+
offset=0,
|
|
490
|
+
size=sync_len,
|
|
491
|
+
field_type="constant",
|
|
492
|
+
value=sync_pattern.hex(),
|
|
493
|
+
)
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
# Analyze remaining bytes
|
|
497
|
+
frame_lengths = [len(f.raw_bytes) for f in frames]
|
|
498
|
+
min_len = min(frame_lengths)
|
|
499
|
+
|
|
500
|
+
# Check for length field (common at offset 2 or after sync)
|
|
501
|
+
length_offset = sync_len
|
|
502
|
+
if min_len > length_offset:
|
|
503
|
+
length_values = [
|
|
504
|
+
f.raw_bytes[length_offset] for f in frames if len(f.raw_bytes) > length_offset
|
|
505
|
+
]
|
|
506
|
+
frame_lens = [len(f.raw_bytes) for f in frames]
|
|
507
|
+
|
|
508
|
+
# Check if value correlates with frame length
|
|
509
|
+
if len(length_values) > 2:
|
|
510
|
+
correlation = (
|
|
511
|
+
np.corrcoef(length_values, frame_lens)[0, 1] if len(set(length_values)) > 1 else 0
|
|
512
|
+
)
|
|
513
|
+
if correlation > 0.8 or (
|
|
514
|
+
len(set(length_values)) == 1 and length_values[0] == frame_lens[0]
|
|
515
|
+
):
|
|
516
|
+
fields.append(
|
|
517
|
+
FieldSpec(
|
|
518
|
+
name="length",
|
|
519
|
+
offset=length_offset,
|
|
520
|
+
size=1,
|
|
521
|
+
field_type="uint8",
|
|
522
|
+
)
|
|
523
|
+
)
|
|
524
|
+
length_offset += 1
|
|
525
|
+
|
|
526
|
+
# Assume remaining bytes are data + checksum
|
|
527
|
+
if min_len > length_offset + 1:
|
|
528
|
+
fields.append(
|
|
529
|
+
FieldSpec(
|
|
530
|
+
name="data",
|
|
531
|
+
offset=length_offset,
|
|
532
|
+
size="length - sync_len - 2", # Variable
|
|
533
|
+
field_type="bytes",
|
|
534
|
+
)
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
# Last byte likely checksum
|
|
538
|
+
fields.append(
|
|
539
|
+
FieldSpec(
|
|
540
|
+
name="checksum",
|
|
541
|
+
offset=-1,
|
|
542
|
+
size=1,
|
|
543
|
+
field_type="checksum",
|
|
544
|
+
)
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
return fields
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
def _detect_checksum(
|
|
551
|
+
frames: list[InferredFrame],
|
|
552
|
+
checksum_types: list[str],
|
|
553
|
+
) -> tuple[str | None, int | None, float]:
|
|
554
|
+
"""Detect checksum type by trying different algorithms."""
|
|
555
|
+
if len(frames) < 3:
|
|
556
|
+
return None, None, 0.0
|
|
557
|
+
|
|
558
|
+
best_type: str | None = None
|
|
559
|
+
best_pos: int | None = None
|
|
560
|
+
best_matches = 0
|
|
561
|
+
|
|
562
|
+
for chk_type in checksum_types:
|
|
563
|
+
# Assume checksum is last byte
|
|
564
|
+
pos = -1
|
|
565
|
+
matches = 0
|
|
566
|
+
|
|
567
|
+
for frame in frames:
|
|
568
|
+
if len(frame.raw_bytes) < 3:
|
|
569
|
+
continue
|
|
570
|
+
|
|
571
|
+
data = frame.raw_bytes[:-1] # All but last byte
|
|
572
|
+
expected = frame.raw_bytes[-1]
|
|
573
|
+
|
|
574
|
+
calculated = _calculate_checksum(data, chk_type)
|
|
575
|
+
if calculated == expected:
|
|
576
|
+
matches += 1
|
|
577
|
+
|
|
578
|
+
if matches > best_matches:
|
|
579
|
+
best_matches = matches
|
|
580
|
+
best_type = chk_type
|
|
581
|
+
best_pos = pos
|
|
582
|
+
|
|
583
|
+
confidence = best_matches / len(frames) if frames else 0
|
|
584
|
+
if confidence < 0.5:
|
|
585
|
+
return None, None, confidence
|
|
586
|
+
|
|
587
|
+
return best_type, best_pos, confidence
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
def _calculate_checksum(data: bytes, checksum_type: str) -> int:
|
|
591
|
+
"""Calculate checksum using specified algorithm."""
|
|
592
|
+
if checksum_type == "xor":
|
|
593
|
+
result = 0
|
|
594
|
+
for b in data:
|
|
595
|
+
result ^= b
|
|
596
|
+
return result
|
|
597
|
+
|
|
598
|
+
elif checksum_type == "sum8":
|
|
599
|
+
return sum(data) & 0xFF
|
|
600
|
+
|
|
601
|
+
elif checksum_type == "crc8":
|
|
602
|
+
# Simple CRC-8
|
|
603
|
+
crc = 0
|
|
604
|
+
for b in data:
|
|
605
|
+
crc ^= b
|
|
606
|
+
for _ in range(8):
|
|
607
|
+
if crc & 0x80:
|
|
608
|
+
crc = ((crc << 1) ^ 0x07) & 0xFF
|
|
609
|
+
else:
|
|
610
|
+
crc = (crc << 1) & 0xFF
|
|
611
|
+
return crc
|
|
612
|
+
|
|
613
|
+
elif checksum_type == "crc16":
|
|
614
|
+
# CRC-16-CCITT (return low byte only for single-byte comparison)
|
|
615
|
+
crc = 0xFFFF
|
|
616
|
+
for b in data:
|
|
617
|
+
crc ^= b << 8
|
|
618
|
+
for _ in range(8):
|
|
619
|
+
if crc & 0x8000:
|
|
620
|
+
crc = ((crc << 1) ^ 0x1021) & 0xFFFF
|
|
621
|
+
else:
|
|
622
|
+
crc = (crc << 1) & 0xFFFF
|
|
623
|
+
return crc & 0xFF # Return low byte
|
|
624
|
+
|
|
625
|
+
return 0
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
def _verify_checksum(data: bytes, checksum_type: str, checksum_pos: int | None) -> bool:
|
|
629
|
+
"""Verify checksum in frame."""
|
|
630
|
+
if checksum_pos == -1 or checksum_pos is None:
|
|
631
|
+
# Checksum is last byte
|
|
632
|
+
frame_data = data[:-1]
|
|
633
|
+
expected = data[-1]
|
|
634
|
+
else:
|
|
635
|
+
frame_data = data[:checksum_pos] + data[checksum_pos + 1 :]
|
|
636
|
+
expected = data[checksum_pos]
|
|
637
|
+
|
|
638
|
+
calculated = _calculate_checksum(frame_data, checksum_type)
|
|
639
|
+
return calculated == expected
|