oscura 0.5.1__py3-none-any.whl → 0.7.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 +169 -167
- oscura/analyzers/__init__.py +3 -0
- oscura/analyzers/classification.py +659 -0
- oscura/analyzers/digital/edges.py +325 -65
- oscura/analyzers/digital/quality.py +293 -166
- oscura/analyzers/digital/timing.py +260 -115
- oscura/analyzers/digital/timing_numba.py +334 -0
- oscura/analyzers/entropy.py +605 -0
- oscura/analyzers/eye/diagram.py +176 -109
- oscura/analyzers/eye/metrics.py +5 -5
- oscura/analyzers/jitter/__init__.py +6 -4
- oscura/analyzers/jitter/ber.py +52 -52
- oscura/analyzers/jitter/classification.py +156 -0
- oscura/analyzers/jitter/decomposition.py +163 -113
- oscura/analyzers/jitter/spectrum.py +80 -64
- oscura/analyzers/ml/__init__.py +39 -0
- oscura/analyzers/ml/features.py +600 -0
- oscura/analyzers/ml/signal_classifier.py +604 -0
- oscura/analyzers/packet/daq.py +246 -158
- oscura/analyzers/packet/parser.py +12 -1
- oscura/analyzers/packet/payload.py +50 -2110
- oscura/analyzers/packet/payload_analysis.py +361 -181
- oscura/analyzers/packet/payload_patterns.py +133 -70
- oscura/analyzers/packet/stream.py +84 -23
- oscura/analyzers/patterns/__init__.py +26 -5
- oscura/analyzers/patterns/anomaly_detection.py +908 -0
- oscura/analyzers/patterns/clustering.py +169 -108
- oscura/analyzers/patterns/clustering_optimized.py +227 -0
- oscura/analyzers/patterns/discovery.py +1 -1
- oscura/analyzers/patterns/matching.py +581 -197
- oscura/analyzers/patterns/pattern_mining.py +778 -0
- oscura/analyzers/patterns/periodic.py +121 -38
- oscura/analyzers/patterns/sequences.py +175 -78
- oscura/analyzers/power/conduction.py +1 -1
- oscura/analyzers/power/soa.py +6 -6
- oscura/analyzers/power/switching.py +250 -110
- oscura/analyzers/protocol/__init__.py +17 -1
- oscura/analyzers/protocols/base.py +6 -6
- oscura/analyzers/protocols/ble/__init__.py +38 -0
- oscura/analyzers/protocols/ble/analyzer.py +809 -0
- oscura/analyzers/protocols/ble/uuids.py +288 -0
- oscura/analyzers/protocols/can.py +257 -127
- oscura/analyzers/protocols/can_fd.py +107 -80
- oscura/analyzers/protocols/flexray.py +139 -80
- oscura/analyzers/protocols/hdlc.py +93 -58
- oscura/analyzers/protocols/i2c.py +247 -106
- oscura/analyzers/protocols/i2s.py +138 -86
- oscura/analyzers/protocols/industrial/__init__.py +40 -0
- oscura/analyzers/protocols/industrial/bacnet/__init__.py +33 -0
- oscura/analyzers/protocols/industrial/bacnet/analyzer.py +708 -0
- oscura/analyzers/protocols/industrial/bacnet/encoding.py +412 -0
- oscura/analyzers/protocols/industrial/bacnet/services.py +622 -0
- oscura/analyzers/protocols/industrial/ethercat/__init__.py +30 -0
- oscura/analyzers/protocols/industrial/ethercat/analyzer.py +474 -0
- oscura/analyzers/protocols/industrial/ethercat/mailbox.py +339 -0
- oscura/analyzers/protocols/industrial/ethercat/topology.py +166 -0
- oscura/analyzers/protocols/industrial/modbus/__init__.py +31 -0
- oscura/analyzers/protocols/industrial/modbus/analyzer.py +525 -0
- oscura/analyzers/protocols/industrial/modbus/crc.py +79 -0
- oscura/analyzers/protocols/industrial/modbus/functions.py +436 -0
- oscura/analyzers/protocols/industrial/opcua/__init__.py +21 -0
- oscura/analyzers/protocols/industrial/opcua/analyzer.py +552 -0
- oscura/analyzers/protocols/industrial/opcua/datatypes.py +446 -0
- oscura/analyzers/protocols/industrial/opcua/services.py +264 -0
- oscura/analyzers/protocols/industrial/profinet/__init__.py +23 -0
- oscura/analyzers/protocols/industrial/profinet/analyzer.py +441 -0
- oscura/analyzers/protocols/industrial/profinet/dcp.py +263 -0
- oscura/analyzers/protocols/industrial/profinet/ptcp.py +200 -0
- oscura/analyzers/protocols/jtag.py +180 -98
- oscura/analyzers/protocols/lin.py +219 -114
- oscura/analyzers/protocols/manchester.py +4 -4
- oscura/analyzers/protocols/onewire.py +253 -149
- oscura/analyzers/protocols/parallel_bus/__init__.py +20 -0
- oscura/analyzers/protocols/parallel_bus/centronics.py +92 -0
- oscura/analyzers/protocols/parallel_bus/gpib.py +137 -0
- oscura/analyzers/protocols/spi.py +192 -95
- oscura/analyzers/protocols/swd.py +321 -167
- oscura/analyzers/protocols/uart.py +267 -125
- oscura/analyzers/protocols/usb.py +235 -131
- oscura/analyzers/side_channel/power.py +17 -12
- oscura/analyzers/signal/__init__.py +15 -0
- oscura/analyzers/signal/timing_analysis.py +1086 -0
- oscura/analyzers/signal_integrity/__init__.py +4 -1
- oscura/analyzers/signal_integrity/sparams.py +2 -19
- oscura/analyzers/spectral/chunked.py +129 -60
- oscura/analyzers/spectral/chunked_fft.py +300 -94
- oscura/analyzers/spectral/chunked_wavelet.py +100 -80
- oscura/analyzers/statistical/checksum.py +376 -217
- oscura/analyzers/statistical/classification.py +229 -107
- oscura/analyzers/statistical/entropy.py +78 -53
- oscura/analyzers/statistics/correlation.py +407 -211
- oscura/analyzers/statistics/outliers.py +2 -2
- oscura/analyzers/statistics/streaming.py +30 -5
- oscura/analyzers/validation.py +216 -101
- oscura/analyzers/waveform/measurements.py +9 -0
- oscura/analyzers/waveform/measurements_with_uncertainty.py +31 -15
- oscura/analyzers/waveform/spectral.py +500 -228
- oscura/api/__init__.py +31 -5
- oscura/api/dsl/__init__.py +582 -0
- oscura/{dsl → api/dsl}/commands.py +43 -76
- oscura/{dsl → api/dsl}/interpreter.py +26 -51
- oscura/{dsl → api/dsl}/parser.py +107 -77
- oscura/{dsl → api/dsl}/repl.py +2 -2
- oscura/api/dsl.py +1 -1
- oscura/{integrations → api/integrations}/__init__.py +1 -1
- oscura/{integrations → api/integrations}/llm.py +201 -102
- oscura/api/operators.py +3 -3
- oscura/api/optimization.py +144 -30
- oscura/api/rest_server.py +921 -0
- oscura/api/server/__init__.py +17 -0
- oscura/api/server/dashboard.py +850 -0
- oscura/api/server/static/README.md +34 -0
- oscura/api/server/templates/base.html +181 -0
- oscura/api/server/templates/export.html +120 -0
- oscura/api/server/templates/home.html +284 -0
- oscura/api/server/templates/protocols.html +58 -0
- oscura/api/server/templates/reports.html +43 -0
- oscura/api/server/templates/session_detail.html +89 -0
- oscura/api/server/templates/sessions.html +83 -0
- oscura/api/server/templates/waveforms.html +73 -0
- oscura/automotive/__init__.py +8 -1
- oscura/automotive/can/__init__.py +10 -0
- oscura/automotive/can/checksum.py +3 -1
- oscura/automotive/can/dbc_generator.py +590 -0
- oscura/automotive/can/message_wrapper.py +121 -74
- oscura/automotive/can/patterns.py +98 -21
- oscura/automotive/can/session.py +292 -56
- oscura/automotive/can/state_machine.py +6 -3
- oscura/automotive/can/stimulus_response.py +97 -75
- oscura/automotive/dbc/__init__.py +10 -2
- oscura/automotive/dbc/generator.py +84 -56
- oscura/automotive/dbc/parser.py +6 -6
- oscura/automotive/dtc/data.json +17 -102
- oscura/automotive/dtc/database.py +2 -2
- oscura/automotive/flexray/__init__.py +31 -0
- oscura/automotive/flexray/analyzer.py +504 -0
- oscura/automotive/flexray/crc.py +185 -0
- oscura/automotive/flexray/fibex.py +449 -0
- oscura/automotive/j1939/__init__.py +45 -8
- oscura/automotive/j1939/analyzer.py +605 -0
- oscura/automotive/j1939/spns.py +326 -0
- oscura/automotive/j1939/transport.py +306 -0
- oscura/automotive/lin/__init__.py +47 -0
- oscura/automotive/lin/analyzer.py +612 -0
- oscura/automotive/loaders/blf.py +13 -2
- oscura/automotive/loaders/csv_can.py +143 -72
- oscura/automotive/loaders/dispatcher.py +50 -2
- oscura/automotive/loaders/mdf.py +86 -45
- oscura/automotive/loaders/pcap.py +111 -61
- oscura/automotive/uds/__init__.py +4 -0
- oscura/automotive/uds/analyzer.py +725 -0
- oscura/automotive/uds/decoder.py +140 -58
- oscura/automotive/uds/models.py +7 -1
- oscura/automotive/visualization.py +1 -1
- oscura/cli/analyze.py +348 -0
- oscura/cli/batch.py +142 -122
- oscura/cli/benchmark.py +275 -0
- oscura/cli/characterize.py +137 -82
- oscura/cli/compare.py +224 -131
- oscura/cli/completion.py +250 -0
- oscura/cli/config_cmd.py +361 -0
- oscura/cli/decode.py +164 -87
- oscura/cli/export.py +286 -0
- oscura/cli/main.py +115 -31
- oscura/{onboarding → cli/onboarding}/__init__.py +3 -3
- oscura/{onboarding → cli/onboarding}/help.py +80 -58
- oscura/{onboarding → cli/onboarding}/tutorials.py +97 -72
- oscura/{onboarding → cli/onboarding}/wizard.py +55 -36
- oscura/cli/progress.py +147 -0
- oscura/cli/shell.py +157 -135
- oscura/cli/validate_cmd.py +204 -0
- oscura/cli/visualize.py +158 -0
- oscura/convenience.py +125 -79
- oscura/core/__init__.py +4 -2
- oscura/core/backend_selector.py +3 -3
- oscura/core/cache.py +126 -15
- oscura/core/cancellation.py +1 -1
- oscura/{config → core/config}/__init__.py +20 -11
- oscura/{config → core/config}/defaults.py +1 -1
- oscura/{config → core/config}/loader.py +7 -5
- oscura/{config → core/config}/memory.py +5 -5
- oscura/{config → core/config}/migration.py +1 -1
- oscura/{config → core/config}/pipeline.py +99 -23
- oscura/{config → core/config}/preferences.py +1 -1
- oscura/{config → core/config}/protocol.py +3 -3
- oscura/{config → core/config}/schema.py +426 -272
- oscura/{config → core/config}/settings.py +1 -1
- oscura/{config → core/config}/thresholds.py +195 -153
- oscura/core/correlation.py +5 -6
- oscura/core/cross_domain.py +0 -2
- oscura/core/debug.py +9 -5
- oscura/{extensibility → core/extensibility}/docs.py +158 -70
- oscura/{extensibility → core/extensibility}/extensions.py +160 -76
- oscura/{extensibility → core/extensibility}/logging.py +1 -1
- oscura/{extensibility → core/extensibility}/measurements.py +1 -1
- oscura/{extensibility → core/extensibility}/plugins.py +1 -1
- oscura/{extensibility → core/extensibility}/templates.py +73 -3
- oscura/{extensibility → core/extensibility}/validation.py +1 -1
- oscura/core/gpu_backend.py +11 -7
- oscura/core/log_query.py +101 -11
- oscura/core/logging.py +126 -54
- oscura/core/logging_advanced.py +5 -5
- oscura/core/memory_limits.py +108 -70
- oscura/core/memory_monitor.py +2 -2
- oscura/core/memory_progress.py +7 -7
- oscura/core/memory_warnings.py +1 -1
- oscura/core/numba_backend.py +13 -13
- oscura/{plugins → core/plugins}/__init__.py +9 -9
- oscura/{plugins → core/plugins}/base.py +7 -7
- oscura/{plugins → core/plugins}/cli.py +3 -3
- oscura/{plugins → core/plugins}/discovery.py +186 -106
- oscura/{plugins → core/plugins}/lifecycle.py +1 -1
- oscura/{plugins → core/plugins}/manager.py +7 -7
- oscura/{plugins → core/plugins}/registry.py +3 -3
- oscura/{plugins → core/plugins}/versioning.py +1 -1
- oscura/core/progress.py +16 -1
- oscura/core/provenance.py +8 -2
- oscura/{schemas → core/schemas}/__init__.py +2 -2
- oscura/{schemas → core/schemas}/device_mapping.json +2 -8
- oscura/{schemas → core/schemas}/packet_format.json +4 -24
- oscura/{schemas → core/schemas}/protocol_definition.json +2 -12
- oscura/core/types.py +4 -0
- oscura/core/uncertainty.py +3 -3
- oscura/correlation/__init__.py +52 -0
- oscura/correlation/multi_protocol.py +811 -0
- oscura/discovery/auto_decoder.py +117 -35
- oscura/discovery/comparison.py +191 -86
- oscura/discovery/quality_validator.py +155 -68
- oscura/discovery/signal_detector.py +196 -79
- oscura/export/__init__.py +18 -8
- oscura/export/kaitai_struct.py +513 -0
- oscura/export/scapy_layer.py +801 -0
- oscura/export/wireshark/generator.py +1 -1
- oscura/export/wireshark/templates/dissector.lua.j2 +2 -2
- oscura/export/wireshark_dissector.py +746 -0
- oscura/guidance/wizard.py +207 -111
- oscura/hardware/__init__.py +19 -0
- oscura/{acquisition → hardware/acquisition}/__init__.py +4 -4
- oscura/{acquisition → hardware/acquisition}/file.py +2 -2
- oscura/{acquisition → hardware/acquisition}/hardware.py +7 -7
- oscura/{acquisition → hardware/acquisition}/saleae.py +15 -12
- oscura/{acquisition → hardware/acquisition}/socketcan.py +1 -1
- oscura/{acquisition → hardware/acquisition}/streaming.py +2 -2
- oscura/{acquisition → hardware/acquisition}/synthetic.py +3 -3
- oscura/{acquisition → hardware/acquisition}/visa.py +33 -11
- oscura/hardware/firmware/__init__.py +29 -0
- oscura/hardware/firmware/pattern_recognition.py +874 -0
- oscura/hardware/hal_detector.py +736 -0
- oscura/hardware/security/__init__.py +37 -0
- oscura/hardware/security/side_channel_detector.py +1126 -0
- oscura/inference/__init__.py +4 -0
- oscura/inference/active_learning/observation_table.py +4 -1
- oscura/inference/alignment.py +216 -123
- oscura/inference/bayesian.py +113 -33
- oscura/inference/crc_reverse.py +101 -55
- oscura/inference/logic.py +6 -2
- oscura/inference/message_format.py +342 -183
- oscura/inference/protocol.py +95 -44
- oscura/inference/protocol_dsl.py +180 -82
- oscura/inference/signal_intelligence.py +1439 -706
- oscura/inference/spectral.py +99 -57
- oscura/inference/state_machine.py +810 -158
- oscura/inference/stream.py +270 -110
- oscura/iot/__init__.py +34 -0
- oscura/iot/coap/__init__.py +32 -0
- oscura/iot/coap/analyzer.py +668 -0
- oscura/iot/coap/options.py +212 -0
- oscura/iot/lorawan/__init__.py +21 -0
- oscura/iot/lorawan/crypto.py +206 -0
- oscura/iot/lorawan/decoder.py +801 -0
- oscura/iot/lorawan/mac_commands.py +341 -0
- oscura/iot/mqtt/__init__.py +27 -0
- oscura/iot/mqtt/analyzer.py +999 -0
- oscura/iot/mqtt/properties.py +315 -0
- oscura/iot/zigbee/__init__.py +31 -0
- oscura/iot/zigbee/analyzer.py +615 -0
- oscura/iot/zigbee/security.py +153 -0
- oscura/iot/zigbee/zcl.py +349 -0
- oscura/jupyter/display.py +125 -45
- oscura/{exploratory → jupyter/exploratory}/__init__.py +8 -8
- oscura/{exploratory → jupyter/exploratory}/error_recovery.py +298 -141
- oscura/jupyter/exploratory/fuzzy.py +746 -0
- oscura/{exploratory → jupyter/exploratory}/fuzzy_advanced.py +258 -100
- oscura/{exploratory → jupyter/exploratory}/legacy.py +464 -242
- oscura/{exploratory → jupyter/exploratory}/parse.py +167 -145
- oscura/{exploratory → jupyter/exploratory}/recovery.py +119 -87
- oscura/jupyter/exploratory/sync.py +612 -0
- oscura/{exploratory → jupyter/exploratory}/unknown.py +299 -176
- oscura/jupyter/magic.py +4 -4
- oscura/{ui → jupyter/ui}/__init__.py +2 -2
- oscura/{ui → jupyter/ui}/formatters.py +3 -3
- oscura/{ui → jupyter/ui}/progressive_display.py +153 -82
- oscura/loaders/__init__.py +183 -67
- oscura/loaders/binary.py +88 -1
- oscura/loaders/chipwhisperer.py +153 -137
- oscura/loaders/configurable.py +208 -86
- oscura/loaders/csv_loader.py +458 -215
- oscura/loaders/hdf5_loader.py +278 -119
- oscura/loaders/lazy.py +87 -54
- oscura/loaders/mmap_loader.py +1 -1
- oscura/loaders/numpy_loader.py +253 -116
- oscura/loaders/pcap.py +226 -151
- oscura/loaders/rigol.py +110 -49
- oscura/loaders/sigrok.py +201 -78
- oscura/loaders/tdms.py +81 -58
- oscura/loaders/tektronix.py +291 -174
- oscura/loaders/touchstone.py +182 -87
- oscura/loaders/tss.py +456 -0
- oscura/loaders/vcd.py +215 -117
- oscura/loaders/wav.py +155 -68
- oscura/reporting/__init__.py +9 -0
- oscura/reporting/analyze.py +352 -146
- oscura/reporting/argument_preparer.py +69 -14
- oscura/reporting/auto_report.py +97 -61
- oscura/reporting/batch.py +131 -58
- oscura/reporting/chart_selection.py +57 -45
- oscura/reporting/comparison.py +63 -17
- oscura/reporting/content/executive.py +76 -24
- oscura/reporting/core_formats/multi_format.py +11 -8
- oscura/reporting/engine.py +312 -158
- oscura/reporting/enhanced_reports.py +949 -0
- oscura/reporting/export.py +86 -43
- oscura/reporting/formatting/numbers.py +69 -42
- oscura/reporting/html.py +139 -58
- oscura/reporting/index.py +137 -65
- oscura/reporting/output.py +158 -67
- oscura/reporting/pdf.py +67 -102
- oscura/reporting/plots.py +191 -112
- oscura/reporting/sections.py +88 -47
- oscura/reporting/standards.py +104 -61
- oscura/reporting/summary_generator.py +75 -55
- oscura/reporting/tables.py +138 -54
- oscura/reporting/templates/enhanced/protocol_re.html +525 -0
- oscura/sessions/__init__.py +14 -23
- oscura/sessions/base.py +3 -3
- oscura/sessions/blackbox.py +106 -10
- oscura/sessions/generic.py +2 -2
- oscura/sessions/legacy.py +783 -0
- oscura/side_channel/__init__.py +63 -0
- oscura/side_channel/dpa.py +1025 -0
- oscura/utils/__init__.py +15 -1
- oscura/utils/bitwise.py +118 -0
- oscura/{builders → utils/builders}/__init__.py +1 -1
- oscura/{comparison → utils/comparison}/__init__.py +6 -6
- oscura/{comparison → utils/comparison}/compare.py +202 -101
- oscura/{comparison → utils/comparison}/golden.py +83 -63
- oscura/{comparison → utils/comparison}/limits.py +313 -89
- oscura/{comparison → utils/comparison}/mask.py +151 -45
- oscura/{comparison → utils/comparison}/trace_diff.py +1 -1
- oscura/{comparison → utils/comparison}/visualization.py +147 -89
- oscura/{component → utils/component}/__init__.py +3 -3
- oscura/{component → utils/component}/impedance.py +122 -58
- oscura/{component → utils/component}/reactive.py +165 -168
- oscura/{component → utils/component}/transmission_line.py +3 -3
- oscura/{filtering → utils/filtering}/__init__.py +6 -6
- oscura/{filtering → utils/filtering}/base.py +1 -1
- oscura/{filtering → utils/filtering}/convenience.py +2 -2
- oscura/{filtering → utils/filtering}/design.py +169 -93
- oscura/{filtering → utils/filtering}/filters.py +2 -2
- oscura/{filtering → utils/filtering}/introspection.py +2 -2
- oscura/utils/geometry.py +31 -0
- oscura/utils/imports.py +184 -0
- oscura/utils/lazy.py +1 -1
- oscura/{math → utils/math}/__init__.py +2 -2
- oscura/{math → utils/math}/arithmetic.py +114 -48
- oscura/{math → utils/math}/interpolation.py +139 -106
- oscura/utils/memory.py +129 -66
- oscura/utils/memory_advanced.py +92 -9
- oscura/utils/memory_extensions.py +10 -8
- oscura/{optimization → utils/optimization}/__init__.py +1 -1
- oscura/{optimization → utils/optimization}/search.py +2 -2
- oscura/utils/performance/__init__.py +58 -0
- oscura/utils/performance/caching.py +889 -0
- oscura/utils/performance/lsh_clustering.py +333 -0
- oscura/utils/performance/memory_optimizer.py +699 -0
- oscura/utils/performance/optimizations.py +675 -0
- oscura/utils/performance/parallel.py +654 -0
- oscura/utils/performance/profiling.py +661 -0
- oscura/{pipeline → utils/pipeline}/base.py +1 -1
- oscura/{pipeline → utils/pipeline}/composition.py +1 -1
- oscura/{pipeline → utils/pipeline}/parallel.py +3 -2
- oscura/{pipeline → utils/pipeline}/pipeline.py +1 -1
- oscura/{pipeline → utils/pipeline}/reverse_engineering.py +412 -221
- oscura/{search → utils/search}/__init__.py +3 -3
- oscura/{search → utils/search}/anomaly.py +188 -58
- oscura/utils/search/context.py +294 -0
- oscura/{search → utils/search}/pattern.py +138 -10
- oscura/utils/serial.py +51 -0
- oscura/utils/storage/__init__.py +61 -0
- oscura/utils/storage/database.py +1166 -0
- oscura/{streaming → utils/streaming}/chunked.py +302 -143
- oscura/{streaming → utils/streaming}/progressive.py +1 -1
- oscura/{streaming → utils/streaming}/realtime.py +3 -2
- oscura/{triggering → utils/triggering}/__init__.py +6 -6
- oscura/{triggering → utils/triggering}/base.py +6 -6
- oscura/{triggering → utils/triggering}/edge.py +2 -2
- oscura/{triggering → utils/triggering}/pattern.py +2 -2
- oscura/{triggering → utils/triggering}/pulse.py +115 -74
- oscura/{triggering → utils/triggering}/window.py +2 -2
- oscura/utils/validation.py +32 -0
- oscura/validation/__init__.py +121 -0
- oscura/{compliance → validation/compliance}/__init__.py +5 -5
- oscura/{compliance → validation/compliance}/advanced.py +5 -5
- oscura/{compliance → validation/compliance}/masks.py +1 -1
- oscura/{compliance → validation/compliance}/reporting.py +127 -53
- oscura/{compliance → validation/compliance}/testing.py +114 -52
- oscura/validation/compliance_tests.py +915 -0
- oscura/validation/fuzzer.py +990 -0
- oscura/validation/grammar_tests.py +596 -0
- oscura/validation/grammar_validator.py +904 -0
- oscura/validation/hil_testing.py +977 -0
- oscura/{quality → validation/quality}/__init__.py +4 -4
- oscura/{quality → validation/quality}/ensemble.py +251 -171
- oscura/{quality → validation/quality}/explainer.py +3 -3
- oscura/{quality → validation/quality}/scoring.py +1 -1
- oscura/{quality → validation/quality}/warnings.py +4 -4
- oscura/validation/regression_suite.py +808 -0
- oscura/validation/replay.py +788 -0
- oscura/{testing → validation/testing}/__init__.py +2 -2
- oscura/{testing → validation/testing}/synthetic.py +5 -5
- oscura/visualization/__init__.py +9 -0
- oscura/visualization/accessibility.py +1 -1
- oscura/visualization/annotations.py +64 -67
- oscura/visualization/colors.py +7 -7
- oscura/visualization/digital.py +180 -81
- oscura/visualization/eye.py +236 -85
- oscura/visualization/interactive.py +320 -143
- oscura/visualization/jitter.py +587 -247
- oscura/visualization/layout.py +169 -134
- oscura/visualization/optimization.py +103 -52
- oscura/visualization/palettes.py +1 -1
- oscura/visualization/power.py +427 -211
- oscura/visualization/power_extended.py +626 -297
- oscura/visualization/presets.py +2 -0
- oscura/visualization/protocols.py +495 -181
- oscura/visualization/render.py +79 -63
- oscura/visualization/reverse_engineering.py +171 -124
- oscura/visualization/signal_integrity.py +460 -279
- oscura/visualization/specialized.py +190 -100
- oscura/visualization/spectral.py +670 -255
- oscura/visualization/thumbnails.py +166 -137
- oscura/visualization/waveform.py +150 -63
- oscura/workflows/__init__.py +3 -0
- oscura/{batch → workflows/batch}/__init__.py +5 -5
- oscura/{batch → workflows/batch}/advanced.py +150 -75
- oscura/workflows/batch/aggregate.py +531 -0
- oscura/workflows/batch/analyze.py +236 -0
- oscura/{batch → workflows/batch}/logging.py +2 -2
- oscura/{batch → workflows/batch}/metrics.py +1 -1
- oscura/workflows/complete_re.py +1144 -0
- oscura/workflows/compliance.py +44 -54
- oscura/workflows/digital.py +197 -51
- oscura/workflows/legacy/__init__.py +12 -0
- oscura/{workflow → workflows/legacy}/dag.py +4 -1
- oscura/workflows/multi_trace.py +9 -9
- oscura/workflows/power.py +42 -62
- oscura/workflows/protocol.py +82 -49
- oscura/workflows/reverse_engineering.py +351 -150
- oscura/workflows/signal_integrity.py +157 -82
- oscura-0.7.0.dist-info/METADATA +661 -0
- oscura-0.7.0.dist-info/RECORD +591 -0
- oscura/batch/aggregate.py +0 -300
- oscura/batch/analyze.py +0 -139
- oscura/dsl/__init__.py +0 -73
- oscura/exceptions.py +0 -59
- oscura/exploratory/fuzzy.py +0 -513
- oscura/exploratory/sync.py +0 -384
- oscura/exporters/__init__.py +0 -94
- oscura/exporters/csv.py +0 -303
- oscura/exporters/exporters.py +0 -44
- oscura/exporters/hdf5.py +0 -217
- oscura/exporters/html_export.py +0 -701
- oscura/exporters/json_export.py +0 -291
- oscura/exporters/markdown_export.py +0 -367
- oscura/exporters/matlab_export.py +0 -354
- oscura/exporters/npz_export.py +0 -219
- oscura/exporters/spice_export.py +0 -210
- oscura/search/context.py +0 -149
- oscura/session/__init__.py +0 -34
- oscura/session/annotations.py +0 -289
- oscura/session/history.py +0 -313
- oscura/session/session.py +0 -520
- oscura/workflow/__init__.py +0 -13
- oscura-0.5.1.dist-info/METADATA +0 -583
- oscura-0.5.1.dist-info/RECORD +0 -481
- /oscura/core/{config.py → config/legacy.py} +0 -0
- /oscura/{extensibility → core/extensibility}/__init__.py +0 -0
- /oscura/{extensibility → core/extensibility}/registry.py +0 -0
- /oscura/{plugins → core/plugins}/isolation.py +0 -0
- /oscura/{schemas → core/schemas}/bus_configuration.json +0 -0
- /oscura/{builders → utils/builders}/signal_builder.py +0 -0
- /oscura/{optimization → utils/optimization}/parallel.py +0 -0
- /oscura/{pipeline → utils/pipeline}/__init__.py +0 -0
- /oscura/{streaming → utils/streaming}/__init__.py +0 -0
- {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/WHEEL +0 -0
- {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/entry_points.txt +0 -0
- {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/licenses/LICENSE +0 -0
oscura/automotive/can/session.py
CHANGED
|
@@ -9,7 +9,14 @@ from __future__ import annotations
|
|
|
9
9
|
|
|
10
10
|
from typing import TYPE_CHECKING, Any
|
|
11
11
|
|
|
12
|
-
import
|
|
12
|
+
# Lazy import for optional dataframe support
|
|
13
|
+
try:
|
|
14
|
+
import pandas as pd
|
|
15
|
+
|
|
16
|
+
_HAS_PANDAS = True
|
|
17
|
+
except ImportError:
|
|
18
|
+
pd = None # type: ignore[assignment]
|
|
19
|
+
_HAS_PANDAS = False
|
|
13
20
|
|
|
14
21
|
from oscura.automotive.can.analysis import MessageAnalyzer
|
|
15
22
|
from oscura.automotive.can.models import (
|
|
@@ -51,7 +58,7 @@ class CANSession(AnalysisSession):
|
|
|
51
58
|
|
|
52
59
|
Example - Basic usage:
|
|
53
60
|
>>> from oscura.sessions import CANSession
|
|
54
|
-
>>> from oscura.acquisition import FileSource
|
|
61
|
+
>>> from oscura.hardware.acquisition import FileSource
|
|
55
62
|
>>> session = CANSession(name="Vehicle Analysis")
|
|
56
63
|
>>> session.add_recording("baseline", FileSource("idle.blf"))
|
|
57
64
|
>>> inventory = session.inventory()
|
|
@@ -80,23 +87,38 @@ class CANSession(AnalysisSession):
|
|
|
80
87
|
>>> print(f"Changed messages: {result.changed_bytes}")
|
|
81
88
|
"""
|
|
82
89
|
|
|
83
|
-
def __init__(
|
|
90
|
+
def __init__(
|
|
91
|
+
self,
|
|
92
|
+
name: str = "CAN Session",
|
|
93
|
+
auto_crc: bool = True,
|
|
94
|
+
crc_validate: bool = True,
|
|
95
|
+
crc_min_messages: int = 10,
|
|
96
|
+
):
|
|
84
97
|
"""Initialize CAN session.
|
|
85
98
|
|
|
86
99
|
Args:
|
|
87
100
|
name: Session name (default: "CAN Session").
|
|
101
|
+
auto_crc: Enable automatic CRC recovery (default: True).
|
|
102
|
+
crc_validate: Validate CRCs on subsequent messages (default: True).
|
|
103
|
+
crc_min_messages: Minimum messages per ID for CRC recovery (default: 10).
|
|
88
104
|
|
|
89
105
|
Example:
|
|
90
106
|
>>> from oscura.sessions import CANSession
|
|
91
|
-
>>> from oscura.acquisition import FileSource
|
|
107
|
+
>>> from oscura.hardware.acquisition import FileSource
|
|
92
108
|
>>> session = CANSession(name="Vehicle Analysis")
|
|
93
109
|
>>> session.add_recording("baseline", FileSource("idle.blf"))
|
|
94
110
|
>>> session.add_recording("active", FileSource("running.blf"))
|
|
95
111
|
>>> results = session.analyze()
|
|
112
|
+
>>> # CRC parameters automatically recovered during analysis
|
|
113
|
+
>>> print(session.crc_info)
|
|
96
114
|
"""
|
|
97
115
|
super().__init__(name=name)
|
|
98
116
|
self._messages = CANMessageList()
|
|
99
117
|
self._analyses_cache: dict[int, MessageAnalysis] = {}
|
|
118
|
+
self.auto_crc = auto_crc
|
|
119
|
+
self.crc_validate = crc_validate
|
|
120
|
+
self.crc_min_messages = crc_min_messages
|
|
121
|
+
self._crc_params: dict[int, Any] = {} # Map CAN ID to CRCParameters
|
|
100
122
|
|
|
101
123
|
def inventory(self) -> pd.DataFrame:
|
|
102
124
|
"""Generate message inventory.
|
|
@@ -112,7 +134,19 @@ class CANSession(AnalysisSession):
|
|
|
112
134
|
|
|
113
135
|
Returns:
|
|
114
136
|
DataFrame with message inventory.
|
|
137
|
+
|
|
138
|
+
Raises:
|
|
139
|
+
ImportError: If pandas is not installed.
|
|
115
140
|
"""
|
|
141
|
+
if not _HAS_PANDAS:
|
|
142
|
+
raise ImportError(
|
|
143
|
+
"Message inventory requires pandas.\n\n"
|
|
144
|
+
"Install with:\n"
|
|
145
|
+
" pip install oscura[dataframes] # DataFrame support\n"
|
|
146
|
+
" pip install oscura[standard] # Recommended\n"
|
|
147
|
+
" pip install oscura[all] # Everything\n"
|
|
148
|
+
)
|
|
149
|
+
|
|
116
150
|
unique_ids = sorted(self._messages.unique_ids())
|
|
117
151
|
|
|
118
152
|
inventory_data = []
|
|
@@ -213,58 +247,139 @@ class CANSession(AnalysisSession):
|
|
|
213
247
|
This creates a new session with filtered messages from the current
|
|
214
248
|
internal message collection. This method is primarily for legacy
|
|
215
249
|
workflows. For new code, use add_recording() with separate files.
|
|
250
|
+
|
|
251
|
+
Example:
|
|
252
|
+
>>> session.filter(min_frequency=10.0, arbitration_ids=[0x123, 0x456])
|
|
216
253
|
"""
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
if start_time <= msg.timestamp <= end_time:
|
|
224
|
-
filtered_messages.append(msg)
|
|
225
|
-
else:
|
|
226
|
-
filtered_messages = list(self._messages)
|
|
227
|
-
|
|
228
|
-
# Filter by CAN IDs if specified
|
|
229
|
-
if arbitration_ids:
|
|
230
|
-
filtered_messages = [
|
|
231
|
-
msg for msg in filtered_messages if msg.arbitration_id in arbitration_ids
|
|
232
|
-
]
|
|
233
|
-
|
|
234
|
-
# Filter by frequency if specified
|
|
235
|
-
if min_frequency or max_frequency:
|
|
236
|
-
# Group by ID and calculate frequencies
|
|
237
|
-
from collections import defaultdict
|
|
238
|
-
|
|
239
|
-
id_messages: dict[int, list[CANMessage]] = defaultdict(list)
|
|
240
|
-
for msg in filtered_messages:
|
|
241
|
-
id_messages[msg.arbitration_id].append(msg)
|
|
242
|
-
|
|
243
|
-
# Filter IDs by frequency
|
|
244
|
-
valid_ids = set()
|
|
245
|
-
for arb_id, msgs in id_messages.items():
|
|
246
|
-
if len(msgs) > 1:
|
|
247
|
-
timestamps = [m.timestamp for m in msgs]
|
|
248
|
-
duration = max(timestamps) - min(timestamps)
|
|
249
|
-
if duration > 0:
|
|
250
|
-
freq = (len(msgs) - 1) / duration
|
|
251
|
-
|
|
252
|
-
if min_frequency and freq < min_frequency:
|
|
253
|
-
continue
|
|
254
|
-
if max_frequency and freq > max_frequency:
|
|
255
|
-
continue
|
|
256
|
-
|
|
257
|
-
valid_ids.add(arb_id)
|
|
258
|
-
|
|
259
|
-
filtered_messages = [
|
|
260
|
-
msg for msg in filtered_messages if msg.arbitration_id in valid_ids
|
|
261
|
-
]
|
|
254
|
+
# Apply filters sequentially
|
|
255
|
+
filtered_messages = self._filter_by_time_range(time_range)
|
|
256
|
+
filtered_messages = self._filter_by_arbitration_ids(filtered_messages, arbitration_ids)
|
|
257
|
+
filtered_messages = self._filter_by_frequency(
|
|
258
|
+
filtered_messages, min_frequency, max_frequency
|
|
259
|
+
)
|
|
262
260
|
|
|
263
261
|
# Create new session with filtered messages
|
|
264
262
|
new_session = CANSession(name=f"{self.name} (filtered)")
|
|
265
263
|
new_session._messages = CANMessageList(messages=filtered_messages)
|
|
266
264
|
return new_session
|
|
267
265
|
|
|
266
|
+
def _filter_by_time_range(self, time_range: tuple[float, float] | None) -> list[CANMessage]:
|
|
267
|
+
"""Filter messages by timestamp range.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
time_range: Optional (start_time, end_time) in seconds.
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
Filtered message list.
|
|
274
|
+
"""
|
|
275
|
+
if time_range is None:
|
|
276
|
+
return list(self._messages)
|
|
277
|
+
|
|
278
|
+
start_time, end_time = time_range
|
|
279
|
+
return [msg for msg in self._messages if start_time <= msg.timestamp <= end_time]
|
|
280
|
+
|
|
281
|
+
def _filter_by_arbitration_ids(
|
|
282
|
+
self, messages: list[CANMessage], arbitration_ids: list[int] | None
|
|
283
|
+
) -> list[CANMessage]:
|
|
284
|
+
"""Filter messages by CAN arbitration IDs.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
messages: Messages to filter.
|
|
288
|
+
arbitration_ids: Optional list of CAN IDs to include.
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
Filtered message list.
|
|
292
|
+
"""
|
|
293
|
+
if arbitration_ids is None:
|
|
294
|
+
return messages
|
|
295
|
+
|
|
296
|
+
id_set = set(arbitration_ids)
|
|
297
|
+
return [msg for msg in messages if msg.arbitration_id in id_set]
|
|
298
|
+
|
|
299
|
+
def _filter_by_frequency(
|
|
300
|
+
self,
|
|
301
|
+
messages: list[CANMessage],
|
|
302
|
+
min_frequency: float | None,
|
|
303
|
+
max_frequency: float | None,
|
|
304
|
+
) -> list[CANMessage]:
|
|
305
|
+
"""Filter messages by transmission frequency.
|
|
306
|
+
|
|
307
|
+
Args:
|
|
308
|
+
messages: Messages to filter.
|
|
309
|
+
min_frequency: Optional minimum frequency in Hz.
|
|
310
|
+
max_frequency: Optional maximum frequency in Hz.
|
|
311
|
+
|
|
312
|
+
Returns:
|
|
313
|
+
Filtered message list (only IDs with frequency in range).
|
|
314
|
+
"""
|
|
315
|
+
if min_frequency is None and max_frequency is None:
|
|
316
|
+
return messages
|
|
317
|
+
|
|
318
|
+
# Calculate frequency for each CAN ID
|
|
319
|
+
valid_ids = self._find_ids_in_frequency_range(messages, min_frequency, max_frequency)
|
|
320
|
+
|
|
321
|
+
return [msg for msg in messages if msg.arbitration_id in valid_ids]
|
|
322
|
+
|
|
323
|
+
def _find_ids_in_frequency_range(
|
|
324
|
+
self,
|
|
325
|
+
messages: list[CANMessage],
|
|
326
|
+
min_frequency: float | None,
|
|
327
|
+
max_frequency: float | None,
|
|
328
|
+
) -> set[int]:
|
|
329
|
+
"""Find CAN IDs with transmission frequency in specified range.
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
messages: Messages to analyze.
|
|
333
|
+
min_frequency: Optional minimum frequency in Hz.
|
|
334
|
+
max_frequency: Optional maximum frequency in Hz.
|
|
335
|
+
|
|
336
|
+
Returns:
|
|
337
|
+
Set of CAN IDs meeting frequency criteria.
|
|
338
|
+
"""
|
|
339
|
+
from collections import defaultdict
|
|
340
|
+
|
|
341
|
+
# Group messages by ID
|
|
342
|
+
id_messages: dict[int, list[CANMessage]] = defaultdict(list)
|
|
343
|
+
for msg in messages:
|
|
344
|
+
id_messages[msg.arbitration_id].append(msg)
|
|
345
|
+
|
|
346
|
+
# Check frequency for each ID
|
|
347
|
+
valid_ids = set()
|
|
348
|
+
for arb_id, msgs in id_messages.items():
|
|
349
|
+
freq = self._calculate_message_frequency(msgs)
|
|
350
|
+
if freq is None:
|
|
351
|
+
continue
|
|
352
|
+
|
|
353
|
+
# Check frequency bounds
|
|
354
|
+
if min_frequency is not None and freq < min_frequency:
|
|
355
|
+
continue
|
|
356
|
+
if max_frequency is not None and freq > max_frequency:
|
|
357
|
+
continue
|
|
358
|
+
|
|
359
|
+
valid_ids.add(arb_id)
|
|
360
|
+
|
|
361
|
+
return valid_ids
|
|
362
|
+
|
|
363
|
+
def _calculate_message_frequency(self, messages: list[CANMessage]) -> float | None:
|
|
364
|
+
"""Calculate transmission frequency for a list of messages.
|
|
365
|
+
|
|
366
|
+
Args:
|
|
367
|
+
messages: Messages with same arbitration ID.
|
|
368
|
+
|
|
369
|
+
Returns:
|
|
370
|
+
Frequency in Hz, or None if cannot be calculated.
|
|
371
|
+
"""
|
|
372
|
+
if len(messages) <= 1:
|
|
373
|
+
return None
|
|
374
|
+
|
|
375
|
+
timestamps = [m.timestamp for m in messages]
|
|
376
|
+
duration = max(timestamps) - min(timestamps)
|
|
377
|
+
|
|
378
|
+
if duration <= 0:
|
|
379
|
+
return None
|
|
380
|
+
|
|
381
|
+
return (len(messages) - 1) / duration
|
|
382
|
+
|
|
268
383
|
def unique_ids(self) -> set[int]:
|
|
269
384
|
"""Get set of unique CAN IDs in this session.
|
|
270
385
|
|
|
@@ -302,7 +417,7 @@ class CANSession(AnalysisSession):
|
|
|
302
417
|
|
|
303
418
|
Example:
|
|
304
419
|
>>> from oscura.sessions import CANSession
|
|
305
|
-
>>> from oscura.acquisition import FileSource
|
|
420
|
+
>>> from oscura.hardware.acquisition import FileSource
|
|
306
421
|
>>> session = CANSession(name="Analysis")
|
|
307
422
|
>>> session.add_recording("data", FileSource("capture.blf"))
|
|
308
423
|
>>> results = session.analyze()
|
|
@@ -316,6 +431,10 @@ class CANSession(AnalysisSession):
|
|
|
316
431
|
# Generate inventory
|
|
317
432
|
inventory = self.inventory()
|
|
318
433
|
|
|
434
|
+
# Automatically recover CRC parameters if enabled
|
|
435
|
+
if self.auto_crc:
|
|
436
|
+
self._auto_recover_crc()
|
|
437
|
+
|
|
319
438
|
# Analyze all unique IDs
|
|
320
439
|
message_analyses = {}
|
|
321
440
|
for arb_id in self.unique_ids():
|
|
@@ -368,7 +487,7 @@ class CANSession(AnalysisSession):
|
|
|
368
487
|
- details: CAN-specific details (changed_ids, byte_changes, etc.)
|
|
369
488
|
|
|
370
489
|
Example:
|
|
371
|
-
>>> from oscura.acquisition import FileSource
|
|
490
|
+
>>> from oscura.hardware.acquisition import FileSource
|
|
372
491
|
>>> session = CANSession(name="Brake Analysis")
|
|
373
492
|
>>> session.add_recording("no_brake", FileSource("idle.blf"))
|
|
374
493
|
>>> session.add_recording("brake_pressed", FileSource("brake.blf"))
|
|
@@ -416,7 +535,7 @@ class CANSession(AnalysisSession):
|
|
|
416
535
|
recording1=name1,
|
|
417
536
|
recording2=name2,
|
|
418
537
|
changed_bytes=total_byte_changes,
|
|
419
|
-
changed_regions=changed_regions,
|
|
538
|
+
changed_regions=changed_regions,
|
|
420
539
|
similarity_score=similarity,
|
|
421
540
|
details={
|
|
422
541
|
"changed_message_ids": changed_message_ids,
|
|
@@ -520,7 +639,7 @@ class CANSession(AnalysisSession):
|
|
|
520
639
|
|
|
521
640
|
Example:
|
|
522
641
|
>>> from oscura.sessions import CANSession
|
|
523
|
-
>>> from oscura.acquisition import FileSource
|
|
642
|
+
>>> from oscura.hardware.acquisition import FileSource
|
|
524
643
|
>>> session = CANSession(name="Pattern Analysis")
|
|
525
644
|
>>> session.add_recording("data", FileSource("capture.blf"))
|
|
526
645
|
>>> pairs = session.find_message_pairs(time_window_ms=50)
|
|
@@ -554,7 +673,7 @@ class CANSession(AnalysisSession):
|
|
|
554
673
|
|
|
555
674
|
Example:
|
|
556
675
|
>>> from oscura.sessions import CANSession
|
|
557
|
-
>>> from oscura.acquisition import FileSource
|
|
676
|
+
>>> from oscura.hardware.acquisition import FileSource
|
|
558
677
|
>>> session = CANSession(name="Sequence Analysis")
|
|
559
678
|
>>> session.add_recording("data", FileSource("startup.blf"))
|
|
560
679
|
>>> sequences = session.find_message_sequences(
|
|
@@ -590,7 +709,7 @@ class CANSession(AnalysisSession):
|
|
|
590
709
|
|
|
591
710
|
Example:
|
|
592
711
|
>>> from oscura.sessions import CANSession
|
|
593
|
-
>>> from oscura.acquisition import FileSource
|
|
712
|
+
>>> from oscura.hardware.acquisition import FileSource
|
|
594
713
|
>>> session = CANSession(name="Correlation Analysis")
|
|
595
714
|
>>> session.add_recording("data", FileSource("capture.blf"))
|
|
596
715
|
>>> correlations = session.find_temporal_correlations(max_delay_ms=50)
|
|
@@ -618,7 +737,7 @@ class CANSession(AnalysisSession):
|
|
|
618
737
|
|
|
619
738
|
Example:
|
|
620
739
|
>>> from oscura.sessions import CANSession
|
|
621
|
-
>>> from oscura.acquisition import FileSource
|
|
740
|
+
>>> from oscura.hardware.acquisition import FileSource
|
|
622
741
|
>>> session = CANSession(name="State Machine Learning")
|
|
623
742
|
>>> session.add_recording("data", FileSource("ignition_cycles.blf"))
|
|
624
743
|
>>> automaton = session.learn_state_machine(
|
|
@@ -633,6 +752,123 @@ class CANSession(AnalysisSession):
|
|
|
633
752
|
session=self, trigger_ids=trigger_ids, context_window_ms=context_window_ms
|
|
634
753
|
)
|
|
635
754
|
|
|
755
|
+
def _auto_recover_crc(self) -> None:
|
|
756
|
+
"""Automatically recover CRC parameters for each message ID.
|
|
757
|
+
|
|
758
|
+
This method attempts to recover CRC parameters for each CAN ID that has
|
|
759
|
+
enough messages. Recovered parameters are stored in self._crc_params.
|
|
760
|
+
|
|
761
|
+
Note:
|
|
762
|
+
Only attempts recovery for IDs with >= crc_min_messages.
|
|
763
|
+
Requires confidence > 0.8 to accept recovered parameters.
|
|
764
|
+
"""
|
|
765
|
+
import logging
|
|
766
|
+
|
|
767
|
+
from oscura.inference.crc_reverse import CRCReverser
|
|
768
|
+
|
|
769
|
+
logger = logging.getLogger(__name__)
|
|
770
|
+
|
|
771
|
+
for arb_id in self.unique_ids():
|
|
772
|
+
filtered = self._messages.filter_by_id(arb_id)
|
|
773
|
+
if len(filtered) < self.crc_min_messages:
|
|
774
|
+
continue
|
|
775
|
+
|
|
776
|
+
try:
|
|
777
|
+
# Prepare message-CRC pairs
|
|
778
|
+
# Heuristic: try last 1-4 bytes as potential CRC
|
|
779
|
+
messages = []
|
|
780
|
+
for msg in filtered.messages:
|
|
781
|
+
if msg.dlc >= 3:
|
|
782
|
+
# Try 1-byte CRC at end
|
|
783
|
+
messages.append((bytes(msg.data[:-1]), bytes(msg.data[-1:])))
|
|
784
|
+
|
|
785
|
+
if len(messages) >= 4:
|
|
786
|
+
reverser = CRCReverser(verbose=False)
|
|
787
|
+
params = reverser.reverse(messages)
|
|
788
|
+
|
|
789
|
+
if params is not None and params.confidence > 0.8:
|
|
790
|
+
self._crc_params[arb_id] = params
|
|
791
|
+
logger.info(
|
|
792
|
+
f"Auto-recovered CRC for 0x{arb_id:03X}: "
|
|
793
|
+
f"poly=0x{params.polynomial:04x}, "
|
|
794
|
+
f"width={params.width}, confidence={params.confidence:.2f}"
|
|
795
|
+
)
|
|
796
|
+
|
|
797
|
+
except Exception:
|
|
798
|
+
# CRC recovery is best-effort
|
|
799
|
+
pass
|
|
800
|
+
|
|
801
|
+
def _validate_crc(self, msg: CANMessage) -> bool:
|
|
802
|
+
"""Validate CRC for a message if parameters are known.
|
|
803
|
+
|
|
804
|
+
Args:
|
|
805
|
+
msg: CAN message to validate.
|
|
806
|
+
|
|
807
|
+
Returns:
|
|
808
|
+
True if CRC is valid or no CRC params available, False if invalid.
|
|
809
|
+
|
|
810
|
+
Note:
|
|
811
|
+
Logs warning if CRC validation fails.
|
|
812
|
+
"""
|
|
813
|
+
if not self.crc_validate or msg.arbitration_id not in self._crc_params:
|
|
814
|
+
return True
|
|
815
|
+
|
|
816
|
+
import logging
|
|
817
|
+
|
|
818
|
+
from oscura.inference.crc_reverse import verify_crc
|
|
819
|
+
|
|
820
|
+
logger = logging.getLogger(__name__)
|
|
821
|
+
params = self._crc_params[msg.arbitration_id]
|
|
822
|
+
|
|
823
|
+
try:
|
|
824
|
+
# Extract message data and CRC based on width
|
|
825
|
+
crc_bytes = params.width // 8
|
|
826
|
+
if msg.dlc < crc_bytes:
|
|
827
|
+
return True # Message too short, skip validation
|
|
828
|
+
|
|
829
|
+
data = bytes(msg.data[:-crc_bytes])
|
|
830
|
+
crc = bytes(msg.data[-crc_bytes:])
|
|
831
|
+
|
|
832
|
+
if not verify_crc(data, crc, params):
|
|
833
|
+
logger.warning(
|
|
834
|
+
f"CRC validation failed for 0x{msg.arbitration_id:03X} "
|
|
835
|
+
f"at timestamp {msg.timestamp:.6f}"
|
|
836
|
+
)
|
|
837
|
+
return False
|
|
838
|
+
|
|
839
|
+
except Exception:
|
|
840
|
+
# Validation errors are non-fatal
|
|
841
|
+
pass
|
|
842
|
+
|
|
843
|
+
return True
|
|
844
|
+
|
|
845
|
+
@property
|
|
846
|
+
def crc_info(self) -> dict[int, dict[str, Any]]:
|
|
847
|
+
"""Get recovered CRC information for all message IDs.
|
|
848
|
+
|
|
849
|
+
Returns:
|
|
850
|
+
Dictionary mapping CAN ID to CRC parameter dict.
|
|
851
|
+
|
|
852
|
+
Example:
|
|
853
|
+
>>> session = CANSession()
|
|
854
|
+
>>> # ... add recordings and analyze ...
|
|
855
|
+
>>> for can_id, params in session.crc_info.items():
|
|
856
|
+
... print(f"0x{can_id:03X}: {params['algorithm_name']}")
|
|
857
|
+
"""
|
|
858
|
+
result = {}
|
|
859
|
+
for arb_id, params in self._crc_params.items():
|
|
860
|
+
result[arb_id] = {
|
|
861
|
+
"polynomial": f"0x{params.polynomial:04x}",
|
|
862
|
+
"width": params.width,
|
|
863
|
+
"init": f"0x{params.init:04x}",
|
|
864
|
+
"xor_out": f"0x{params.xor_out:04x}",
|
|
865
|
+
"reflect_in": params.reflect_in,
|
|
866
|
+
"reflect_out": params.reflect_out,
|
|
867
|
+
"confidence": params.confidence,
|
|
868
|
+
"algorithm_name": params.algorithm_name,
|
|
869
|
+
}
|
|
870
|
+
return result
|
|
871
|
+
|
|
636
872
|
def __repr__(self) -> str:
|
|
637
873
|
"""Human-readable representation."""
|
|
638
874
|
num_messages = len(self._messages)
|
|
@@ -20,7 +20,7 @@ Use cases:
|
|
|
20
20
|
from __future__ import annotations
|
|
21
21
|
|
|
22
22
|
from dataclasses import dataclass
|
|
23
|
-
from typing import TYPE_CHECKING
|
|
23
|
+
from typing import TYPE_CHECKING, cast
|
|
24
24
|
|
|
25
25
|
from oscura.inference.state_machine import FiniteAutomaton, StateMachineInferrer
|
|
26
26
|
|
|
@@ -152,7 +152,8 @@ class CANStateMachine:
|
|
|
152
152
|
)
|
|
153
153
|
|
|
154
154
|
# Learn state machine using RPNI
|
|
155
|
-
|
|
155
|
+
# Cast: list[list[str]] is compatible with list[list[str | int]] at runtime
|
|
156
|
+
automaton = self._inferrer.infer_rpni(positive_traces=cast("list[list[str | int]]", traces))
|
|
156
157
|
|
|
157
158
|
return automaton
|
|
158
159
|
|
|
@@ -275,7 +276,9 @@ class CANStateMachine:
|
|
|
275
276
|
)
|
|
276
277
|
|
|
277
278
|
# Learn state machine
|
|
278
|
-
|
|
279
|
+
# Cast: list[list[str]] is compatible with list[list[str | int]] at runtime
|
|
280
|
+
state_sequences_union: list[list[str | int]] = state_sequences # type: ignore[assignment]
|
|
281
|
+
automaton = self._inferrer.infer_rpni(positive_traces=state_sequences_union)
|
|
279
282
|
|
|
280
283
|
return automaton
|
|
281
284
|
|
|
@@ -17,9 +17,10 @@ This allows answering questions like:
|
|
|
17
17
|
from __future__ import annotations
|
|
18
18
|
|
|
19
19
|
from dataclasses import dataclass, field
|
|
20
|
-
from typing import TYPE_CHECKING
|
|
20
|
+
from typing import TYPE_CHECKING, Any
|
|
21
21
|
|
|
22
22
|
import numpy as np
|
|
23
|
+
from numpy.typing import NDArray
|
|
23
24
|
from scipy import stats
|
|
24
25
|
|
|
25
26
|
if TYPE_CHECKING:
|
|
@@ -262,14 +263,12 @@ class StimulusResponseAnalyzer:
|
|
|
262
263
|
Returns:
|
|
263
264
|
List of ByteChange objects for changed bytes.
|
|
264
265
|
"""
|
|
265
|
-
# Get messages for this ID from both sessions
|
|
266
266
|
baseline_msgs = baseline_session._messages.filter_by_id(message_id)
|
|
267
267
|
stimulus_msgs = stimulus_session._messages.filter_by_id(message_id)
|
|
268
268
|
|
|
269
269
|
if not baseline_msgs.messages or not stimulus_msgs.messages:
|
|
270
270
|
return []
|
|
271
271
|
|
|
272
|
-
# Determine max DLC
|
|
273
272
|
max_dlc = max(
|
|
274
273
|
max(msg.dlc for msg in baseline_msgs.messages),
|
|
275
274
|
max(msg.dlc for msg in stimulus_msgs.messages),
|
|
@@ -277,82 +276,105 @@ class StimulusResponseAnalyzer:
|
|
|
277
276
|
|
|
278
277
|
changes = []
|
|
279
278
|
for byte_pos in range(max_dlc):
|
|
280
|
-
|
|
281
|
-
|
|
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
|
|
279
|
+
change = self._analyze_byte_change(
|
|
280
|
+
baseline_msgs, stimulus_msgs, byte_pos, byte_threshold
|
|
339
281
|
)
|
|
340
|
-
|
|
341
|
-
|
|
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
|
-
)
|
|
282
|
+
if change:
|
|
283
|
+
changes.append(change)
|
|
353
284
|
|
|
354
285
|
return changes
|
|
355
286
|
|
|
287
|
+
def _analyze_byte_change(
|
|
288
|
+
self, baseline_msgs: Any, stimulus_msgs: Any, byte_pos: int, byte_threshold: int
|
|
289
|
+
) -> ByteChange | None:
|
|
290
|
+
"""Analyze change for a single byte position."""
|
|
291
|
+
# Extract byte values
|
|
292
|
+
baseline_values = [
|
|
293
|
+
msg.data[byte_pos] for msg in baseline_msgs.messages if len(msg.data) > byte_pos
|
|
294
|
+
]
|
|
295
|
+
stimulus_values = [
|
|
296
|
+
msg.data[byte_pos] for msg in stimulus_msgs.messages if len(msg.data) > byte_pos
|
|
297
|
+
]
|
|
298
|
+
|
|
299
|
+
if not baseline_values or not stimulus_values:
|
|
300
|
+
return None
|
|
301
|
+
|
|
302
|
+
baseline_set, stimulus_set = set(baseline_values), set(stimulus_values)
|
|
303
|
+
|
|
304
|
+
# Skip if not enough unique values
|
|
305
|
+
if len(baseline_set) < byte_threshold and len(stimulus_set) < byte_threshold:
|
|
306
|
+
return None
|
|
307
|
+
|
|
308
|
+
# Calculate statistics and change magnitude
|
|
309
|
+
baseline_arr, stimulus_arr = np.array(baseline_values), np.array(stimulus_values)
|
|
310
|
+
mean_change, value_range_change = self._compute_byte_stats(baseline_arr, stimulus_arr)
|
|
311
|
+
change_magnitude = self._compute_change_magnitude(
|
|
312
|
+
baseline_arr, stimulus_arr, baseline_set, stimulus_set, mean_change, value_range_change
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
if change_magnitude <= 0.0:
|
|
316
|
+
return None
|
|
317
|
+
|
|
318
|
+
return ByteChange(
|
|
319
|
+
byte_position=byte_pos,
|
|
320
|
+
baseline_values=baseline_set,
|
|
321
|
+
stimulus_values=stimulus_set,
|
|
322
|
+
change_magnitude=change_magnitude,
|
|
323
|
+
value_range_change=value_range_change,
|
|
324
|
+
mean_change=mean_change,
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
def _compute_byte_stats(
|
|
328
|
+
self, baseline_arr: NDArray[np.int_], stimulus_arr: NDArray[np.int_]
|
|
329
|
+
) -> tuple[float, float]:
|
|
330
|
+
"""Compute mean and range changes for byte values."""
|
|
331
|
+
baseline_mean, stimulus_mean = float(np.mean(baseline_arr)), float(np.mean(stimulus_arr))
|
|
332
|
+
mean_change = stimulus_mean - baseline_mean
|
|
333
|
+
|
|
334
|
+
baseline_range = float(np.max(baseline_arr) - np.min(baseline_arr))
|
|
335
|
+
stimulus_range = float(np.max(stimulus_arr) - np.min(stimulus_arr))
|
|
336
|
+
value_range_change = stimulus_range - baseline_range
|
|
337
|
+
|
|
338
|
+
return mean_change, value_range_change
|
|
339
|
+
|
|
340
|
+
def _compute_change_magnitude(
|
|
341
|
+
self,
|
|
342
|
+
baseline_arr: NDArray[np.int_],
|
|
343
|
+
stimulus_arr: NDArray[np.int_],
|
|
344
|
+
baseline_set: set[int],
|
|
345
|
+
stimulus_set: set[int],
|
|
346
|
+
mean_change: float,
|
|
347
|
+
value_range_change: float,
|
|
348
|
+
) -> float:
|
|
349
|
+
"""Compute normalized change magnitude using multiple factors."""
|
|
350
|
+
# 1. Mean change (normalized by full byte range)
|
|
351
|
+
mean_change_norm = abs(mean_change) / 255.0
|
|
352
|
+
|
|
353
|
+
# 2. Range change (normalized by full byte range)
|
|
354
|
+
range_change_norm = abs(value_range_change) / 255.0
|
|
355
|
+
|
|
356
|
+
# 3. Set difference (Jaccard distance)
|
|
357
|
+
union_size, intersection_size = (
|
|
358
|
+
len(baseline_set | stimulus_set),
|
|
359
|
+
len(baseline_set & stimulus_set),
|
|
360
|
+
)
|
|
361
|
+
jaccard_dist = 1.0 - (intersection_size / union_size) if union_size > 0 else 0.0
|
|
362
|
+
|
|
363
|
+
# 4. Distribution change (Kolmogorov-Smirnov test)
|
|
364
|
+
try:
|
|
365
|
+
ks_stat, _ = stats.ks_2samp(baseline_arr, stimulus_arr)
|
|
366
|
+
ks_change_norm = float(ks_stat)
|
|
367
|
+
except Exception:
|
|
368
|
+
ks_change_norm = 0.0
|
|
369
|
+
|
|
370
|
+
# Combine factors (weighted average)
|
|
371
|
+
return (
|
|
372
|
+
0.3 * mean_change_norm
|
|
373
|
+
+ 0.2 * range_change_norm
|
|
374
|
+
+ 0.3 * jaccard_dist
|
|
375
|
+
+ 0.2 * ks_change_norm
|
|
376
|
+
)
|
|
377
|
+
|
|
356
378
|
def find_responsive_messages(
|
|
357
379
|
self,
|
|
358
380
|
baseline_session: CANSession,
|