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
|
@@ -17,6 +17,142 @@ from oscura.automotive.can.models import CANMessage, CANMessageList
|
|
|
17
17
|
__all__ = ["load_csv_can"]
|
|
18
18
|
|
|
19
19
|
|
|
20
|
+
def _detect_csv_columns(fieldnames: list[str] | None) -> dict[str, str]:
|
|
21
|
+
"""Detect required column names from CSV header.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
fieldnames: List of column names from CSV header.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
Dictionary mapping logical names to actual column names.
|
|
28
|
+
|
|
29
|
+
Raises:
|
|
30
|
+
ValueError: If CSV has no header or missing required columns.
|
|
31
|
+
"""
|
|
32
|
+
if not fieldnames:
|
|
33
|
+
raise ValueError("CSV file has no header row")
|
|
34
|
+
|
|
35
|
+
# Normalize column names to lowercase
|
|
36
|
+
fieldnames_lower = [name.lower().strip() for name in fieldnames]
|
|
37
|
+
|
|
38
|
+
# Find required columns
|
|
39
|
+
timestamp_col = None
|
|
40
|
+
id_col = None
|
|
41
|
+
data_col = None
|
|
42
|
+
|
|
43
|
+
for col in fieldnames_lower:
|
|
44
|
+
if "timestamp" in col or col == "time" or col == "t":
|
|
45
|
+
timestamp_col = col
|
|
46
|
+
elif "id" in col or col == "can_id" or col == "arbitration_id":
|
|
47
|
+
id_col = col
|
|
48
|
+
elif "data" in col or col == "payload" or col == "bytes":
|
|
49
|
+
data_col = col
|
|
50
|
+
|
|
51
|
+
if not all([timestamp_col is not None, id_col is not None, data_col is not None]):
|
|
52
|
+
raise ValueError(
|
|
53
|
+
f"CSV file missing required columns. "
|
|
54
|
+
f"Found: {fieldnames_lower}. "
|
|
55
|
+
f"Need: timestamp, id, data"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# All columns are guaranteed non-None here due to validation above
|
|
59
|
+
return {
|
|
60
|
+
"timestamp": str(timestamp_col),
|
|
61
|
+
"id": str(id_col),
|
|
62
|
+
"data": str(data_col),
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _parse_csv_row(
|
|
67
|
+
row_dict: dict[str, str],
|
|
68
|
+
column_mapping: dict[str, str],
|
|
69
|
+
messages: CANMessageList,
|
|
70
|
+
) -> None:
|
|
71
|
+
"""Parse a single CSV row into a CAN message.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
row_dict: Dictionary of row data from CSV.
|
|
75
|
+
column_mapping: Mapping of logical names to column names.
|
|
76
|
+
messages: Message list to append to.
|
|
77
|
+
"""
|
|
78
|
+
# Create lowercase dict for case-insensitive access
|
|
79
|
+
row = {k.lower().strip(): v for k, v in row_dict.items()}
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
# Parse timestamp
|
|
83
|
+
timestamp = float(row[column_mapping["timestamp"]])
|
|
84
|
+
|
|
85
|
+
# Parse ID (handle hex or decimal)
|
|
86
|
+
arb_id = _parse_can_id(row[column_mapping["id"]])
|
|
87
|
+
|
|
88
|
+
# Parse data bytes
|
|
89
|
+
data_bytes = _parse_data_bytes(row[column_mapping["data"]])
|
|
90
|
+
|
|
91
|
+
# Determine if extended (>11 bits = 0x7FF)
|
|
92
|
+
is_extended = arb_id > 0x7FF
|
|
93
|
+
|
|
94
|
+
# Create and append message
|
|
95
|
+
can_msg = CANMessage(
|
|
96
|
+
arbitration_id=arb_id,
|
|
97
|
+
timestamp=timestamp,
|
|
98
|
+
data=data_bytes,
|
|
99
|
+
is_extended=is_extended,
|
|
100
|
+
is_fd=False,
|
|
101
|
+
channel=0,
|
|
102
|
+
)
|
|
103
|
+
messages.append(can_msg)
|
|
104
|
+
|
|
105
|
+
except (ValueError, KeyError):
|
|
106
|
+
# Skip malformed rows silently
|
|
107
|
+
pass
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _parse_can_id(id_str: str) -> int:
|
|
111
|
+
"""Parse CAN ID from string (hex or decimal).
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
id_str: CAN ID string.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
CAN ID as integer.
|
|
118
|
+
|
|
119
|
+
Raises:
|
|
120
|
+
ValueError: If ID cannot be parsed.
|
|
121
|
+
"""
|
|
122
|
+
id_str = id_str.strip()
|
|
123
|
+
|
|
124
|
+
if id_str.startswith("0x") or id_str.startswith("0X"):
|
|
125
|
+
return int(id_str, 16)
|
|
126
|
+
|
|
127
|
+
# Try as int first, then hex
|
|
128
|
+
try:
|
|
129
|
+
return int(id_str)
|
|
130
|
+
except ValueError:
|
|
131
|
+
return int(id_str, 16)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _parse_data_bytes(data_str: str) -> bytes:
|
|
135
|
+
"""Parse data bytes from hex string.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
data_str: Data bytes as hex string.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
Parsed bytes.
|
|
142
|
+
|
|
143
|
+
Raises:
|
|
144
|
+
ValueError: If data cannot be parsed.
|
|
145
|
+
"""
|
|
146
|
+
# Remove common separators and spaces
|
|
147
|
+
data_str = data_str.strip().replace(" ", "").replace(":", "").replace("-", "")
|
|
148
|
+
|
|
149
|
+
# Remove 0x prefix if present
|
|
150
|
+
if data_str.startswith("0x") or data_str.startswith("0X"):
|
|
151
|
+
data_str = data_str[2:]
|
|
152
|
+
|
|
153
|
+
return bytes.fromhex(data_str)
|
|
154
|
+
|
|
155
|
+
|
|
20
156
|
def load_csv_can(file_path: Path | str, delimiter: str = ",") -> CANMessageList:
|
|
21
157
|
"""Load CAN messages from a CSV file.
|
|
22
158
|
|
|
@@ -54,79 +190,14 @@ def load_csv_can(file_path: Path | str, delimiter: str = ",") -> CANMessageList:
|
|
|
54
190
|
with open(path, encoding="utf-8") as f:
|
|
55
191
|
reader = csv.DictReader(f, delimiter=delimiter)
|
|
56
192
|
|
|
57
|
-
#
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
# Find required columns
|
|
65
|
-
timestamp_col = None
|
|
66
|
-
id_col = None
|
|
67
|
-
data_col = None
|
|
68
|
-
|
|
69
|
-
for col in fieldnames:
|
|
70
|
-
if "timestamp" in col or col == "time" or col == "t":
|
|
71
|
-
timestamp_col = col
|
|
72
|
-
elif "id" in col or col == "can_id" or col == "arbitration_id":
|
|
73
|
-
id_col = col
|
|
74
|
-
elif "data" in col or col == "payload" or col == "bytes":
|
|
75
|
-
data_col = col
|
|
76
|
-
|
|
77
|
-
if not all([timestamp_col, id_col, data_col]):
|
|
78
|
-
raise ValueError(
|
|
79
|
-
f"CSV file missing required columns. "
|
|
80
|
-
f"Found: {fieldnames}. "
|
|
81
|
-
f"Need: timestamp, id, data"
|
|
82
|
-
)
|
|
83
|
-
|
|
84
|
-
# Parse messages
|
|
193
|
+
# Detect and validate column layout
|
|
194
|
+
column_mapping = _detect_csv_columns(
|
|
195
|
+
list(reader.fieldnames) if reader.fieldnames else None
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
# Parse all message rows
|
|
85
199
|
for row_dict in reader:
|
|
86
|
-
|
|
87
|
-
row = {k.lower().strip(): v for k, v in row_dict.items()}
|
|
88
|
-
|
|
89
|
-
try:
|
|
90
|
-
# Parse timestamp
|
|
91
|
-
timestamp = float(row[timestamp_col])
|
|
92
|
-
|
|
93
|
-
# Parse ID (handle hex or decimal)
|
|
94
|
-
id_str = row[id_col].strip()
|
|
95
|
-
if id_str.startswith("0x") or id_str.startswith("0X"):
|
|
96
|
-
arb_id = int(id_str, 16)
|
|
97
|
-
else:
|
|
98
|
-
# Try as int first, then hex
|
|
99
|
-
try:
|
|
100
|
-
arb_id = int(id_str)
|
|
101
|
-
except ValueError:
|
|
102
|
-
arb_id = int(id_str, 16)
|
|
103
|
-
|
|
104
|
-
# Parse data bytes
|
|
105
|
-
data_str = row[data_col].strip()
|
|
106
|
-
# Remove common separators and spaces
|
|
107
|
-
data_str = data_str.replace(" ", "").replace(":", "").replace("-", "")
|
|
108
|
-
# Remove 0x prefix if present
|
|
109
|
-
if data_str.startswith("0x") or data_str.startswith("0X"):
|
|
110
|
-
data_str = data_str[2:]
|
|
111
|
-
data_bytes = bytes.fromhex(data_str)
|
|
112
|
-
|
|
113
|
-
# Determine if extended (>11 bits = 0x7FF)
|
|
114
|
-
is_extended = arb_id > 0x7FF
|
|
115
|
-
|
|
116
|
-
# Create message
|
|
117
|
-
can_msg = CANMessage(
|
|
118
|
-
arbitration_id=arb_id,
|
|
119
|
-
timestamp=timestamp,
|
|
120
|
-
data=data_bytes,
|
|
121
|
-
is_extended=is_extended,
|
|
122
|
-
is_fd=False,
|
|
123
|
-
channel=0,
|
|
124
|
-
)
|
|
125
|
-
messages.append(can_msg)
|
|
126
|
-
|
|
127
|
-
except (ValueError, KeyError):
|
|
128
|
-
# Skip malformed rows
|
|
129
|
-
continue
|
|
200
|
+
_parse_csv_row(row_dict, column_mapping, messages)
|
|
130
201
|
|
|
131
202
|
except Exception as e:
|
|
132
203
|
raise ValueError(f"Failed to parse CSV file {path}: {e}") from e
|
|
@@ -27,6 +27,32 @@ def detect_format(file_path: Path | str) -> str:
|
|
|
27
27
|
path = Path(file_path)
|
|
28
28
|
|
|
29
29
|
# Check extension first
|
|
30
|
+
ext_format = _detect_by_extension(path)
|
|
31
|
+
if ext_format != "unknown":
|
|
32
|
+
return ext_format
|
|
33
|
+
|
|
34
|
+
# Check binary file contents
|
|
35
|
+
binary_format = _detect_by_binary_header(path)
|
|
36
|
+
if binary_format != "unknown":
|
|
37
|
+
return binary_format
|
|
38
|
+
|
|
39
|
+
# Check text file contents
|
|
40
|
+
text_format = _detect_by_text_content(path)
|
|
41
|
+
if text_format != "unknown":
|
|
42
|
+
return text_format
|
|
43
|
+
|
|
44
|
+
return "unknown"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _detect_by_extension(path: Path) -> str:
|
|
48
|
+
"""Detect format by file extension.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
path: Path to the file.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
Format name or 'unknown'.
|
|
55
|
+
"""
|
|
30
56
|
ext = path.suffix.lower()
|
|
31
57
|
|
|
32
58
|
if ext == ".blf":
|
|
@@ -40,7 +66,18 @@ def detect_format(file_path: Path | str) -> str:
|
|
|
40
66
|
elif ext in [".pcap", ".pcapng"]:
|
|
41
67
|
return "pcap"
|
|
42
68
|
|
|
43
|
-
|
|
69
|
+
return "unknown"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _detect_by_binary_header(path: Path) -> str:
|
|
73
|
+
"""Detect format by binary file header.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
path: Path to the file.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Format name or 'unknown'.
|
|
80
|
+
"""
|
|
44
81
|
try:
|
|
45
82
|
with open(path, "rb") as f:
|
|
46
83
|
header = f.read(16)
|
|
@@ -60,7 +97,18 @@ def detect_format(file_path: Path | str) -> str:
|
|
|
60
97
|
except Exception:
|
|
61
98
|
pass
|
|
62
99
|
|
|
63
|
-
|
|
100
|
+
return "unknown"
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _detect_by_text_content(path: Path) -> str:
|
|
104
|
+
"""Detect format by text file content.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
path: Path to the file.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
Format name or 'unknown'.
|
|
111
|
+
"""
|
|
64
112
|
try:
|
|
65
113
|
with open(path, encoding="utf-8") as f:
|
|
66
114
|
first_line = f.readline().strip()
|
oscura/automotive/loaders/mdf.py
CHANGED
|
@@ -47,7 +47,7 @@ def load_mdf(file_path: Path | str) -> CANMessageList:
|
|
|
47
47
|
>>> print(f"Unique IDs: {len(messages.unique_ids())}")
|
|
48
48
|
"""
|
|
49
49
|
try:
|
|
50
|
-
from asammdf import MDF
|
|
50
|
+
from asammdf import MDF
|
|
51
51
|
except ImportError as e:
|
|
52
52
|
raise ImportError(
|
|
53
53
|
"asammdf is required for MDF/MF4 file support. "
|
|
@@ -142,63 +142,104 @@ def _extract_structured_can_frames(
|
|
|
142
142
|
timestamps: Array of timestamps.
|
|
143
143
|
messages: CANMessageList to append to.
|
|
144
144
|
"""
|
|
145
|
-
# Common field names in structured CAN logging
|
|
146
|
-
id_fields = ["ID", "id", "BusID", "Identifier", "ArbitrationID"]
|
|
147
|
-
data_fields = ["Data", "data", "DataBytes", "Payload"]
|
|
148
|
-
dlc_fields = ["DLC", "dlc", "DataLength"]
|
|
149
|
-
|
|
150
|
-
# Find which fields are present
|
|
151
145
|
field_names = samples.dtype.names
|
|
152
146
|
if not field_names:
|
|
153
147
|
return
|
|
154
148
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
dlc_field = next((f for f in dlc_fields if f in field_names), None)
|
|
158
|
-
|
|
159
|
-
if not id_field:
|
|
149
|
+
field_map = _find_can_field_mapping(field_names)
|
|
150
|
+
if not field_map["id_field"]:
|
|
160
151
|
return
|
|
161
152
|
|
|
162
153
|
for i in range(len(samples)):
|
|
163
154
|
try:
|
|
164
|
-
|
|
165
|
-
arb_id = int(samples[id_field][i])
|
|
166
|
-
|
|
167
|
-
# Extract data bytes
|
|
168
|
-
if data_field:
|
|
169
|
-
data_bytes = samples[data_field][i]
|
|
170
|
-
if isinstance(data_bytes, bytes):
|
|
171
|
-
data = data_bytes
|
|
172
|
-
else:
|
|
173
|
-
# Convert array to bytes
|
|
174
|
-
data = bytes(data_bytes)
|
|
175
|
-
else:
|
|
176
|
-
data = b""
|
|
177
|
-
|
|
178
|
-
# Extract DLC if available
|
|
179
|
-
if dlc_field:
|
|
180
|
-
dlc = int(samples[dlc_field][i])
|
|
181
|
-
data = data[:dlc]
|
|
182
|
-
|
|
183
|
-
# Determine if extended ID
|
|
184
|
-
is_extended = arb_id > 0x7FF
|
|
185
|
-
|
|
186
|
-
# Create message
|
|
187
|
-
can_msg = CANMessage(
|
|
188
|
-
arbitration_id=arb_id,
|
|
189
|
-
timestamp=float(timestamps[i]),
|
|
190
|
-
data=data,
|
|
191
|
-
is_extended=is_extended,
|
|
192
|
-
is_fd=False, # MDF doesn't typically indicate CAN-FD
|
|
193
|
-
channel=0, # Channel info not always available in MDF
|
|
194
|
-
)
|
|
155
|
+
can_msg = _extract_can_message_from_sample(samples, timestamps, i, field_map)
|
|
195
156
|
messages.append(can_msg)
|
|
196
|
-
|
|
197
157
|
except (IndexError, ValueError, TypeError):
|
|
198
|
-
# Skip malformed frames
|
|
199
158
|
continue
|
|
200
159
|
|
|
201
160
|
|
|
161
|
+
def _find_can_field_mapping(field_names: tuple[str, ...]) -> dict[str, str | None]:
|
|
162
|
+
"""Find mapping of CAN field names in structured array.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
field_names: Field names from structured array.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
Dictionary mapping field types to actual field names.
|
|
169
|
+
"""
|
|
170
|
+
id_fields = ["ID", "id", "BusID", "Identifier", "ArbitrationID"]
|
|
171
|
+
data_fields = ["Data", "data", "DataBytes", "Payload"]
|
|
172
|
+
dlc_fields = ["DLC", "dlc", "DataLength"]
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
"id_field": next((f for f in id_fields if f in field_names), None),
|
|
176
|
+
"data_field": next((f for f in data_fields if f in field_names), None),
|
|
177
|
+
"dlc_field": next((f for f in dlc_fields if f in field_names), None),
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _extract_can_message_from_sample(
|
|
182
|
+
samples: npt.NDArray[Any],
|
|
183
|
+
timestamps: npt.NDArray[Any],
|
|
184
|
+
index: int,
|
|
185
|
+
field_map: dict[str, str | None],
|
|
186
|
+
) -> CANMessage:
|
|
187
|
+
"""Extract single CAN message from sample.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
samples: Structured array.
|
|
191
|
+
timestamps: Timestamp array.
|
|
192
|
+
index: Sample index.
|
|
193
|
+
field_map: Field name mapping.
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
CANMessage instance.
|
|
197
|
+
"""
|
|
198
|
+
arb_id = int(samples[field_map["id_field"]][index])
|
|
199
|
+
data = _extract_can_data_field(samples, index, field_map)
|
|
200
|
+
is_extended = arb_id > 0x7FF
|
|
201
|
+
|
|
202
|
+
return CANMessage(
|
|
203
|
+
arbitration_id=arb_id,
|
|
204
|
+
timestamp=float(timestamps[index]),
|
|
205
|
+
data=data,
|
|
206
|
+
is_extended=is_extended,
|
|
207
|
+
is_fd=False,
|
|
208
|
+
channel=0,
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _extract_can_data_field(
|
|
213
|
+
samples: npt.NDArray[Any], index: int, field_map: dict[str, str | None]
|
|
214
|
+
) -> bytes:
|
|
215
|
+
"""Extract data bytes from sample.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
samples: Structured array.
|
|
219
|
+
index: Sample index.
|
|
220
|
+
field_map: Field name mapping.
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
Data bytes.
|
|
224
|
+
"""
|
|
225
|
+
data_field = field_map["data_field"]
|
|
226
|
+
if not data_field:
|
|
227
|
+
return b""
|
|
228
|
+
|
|
229
|
+
data_bytes = samples[data_field][index]
|
|
230
|
+
if isinstance(data_bytes, bytes):
|
|
231
|
+
data = data_bytes
|
|
232
|
+
else:
|
|
233
|
+
data = bytes(data_bytes)
|
|
234
|
+
|
|
235
|
+
dlc_field = field_map["dlc_field"]
|
|
236
|
+
if dlc_field:
|
|
237
|
+
dlc = int(samples[dlc_field][index])
|
|
238
|
+
data = data[:dlc]
|
|
239
|
+
|
|
240
|
+
return data
|
|
241
|
+
|
|
242
|
+
|
|
202
243
|
def _extract_raw_can_frames(
|
|
203
244
|
samples: npt.NDArray[Any], timestamps: npt.NDArray[Any], messages: CANMessageList
|
|
204
245
|
) -> None:
|
|
@@ -15,13 +15,9 @@ Requirements:
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
17
|
from pathlib import Path
|
|
18
|
-
from typing import TYPE_CHECKING
|
|
19
18
|
|
|
20
19
|
from oscura.automotive.can.models import CANMessage, CANMessageList
|
|
21
20
|
|
|
22
|
-
if TYPE_CHECKING:
|
|
23
|
-
from scapy.packet import Packet # type: ignore[import-not-found]
|
|
24
|
-
|
|
25
21
|
__all__ = ["load_pcap"]
|
|
26
22
|
|
|
27
23
|
|
|
@@ -58,67 +54,12 @@ def load_pcap(file_path: Path | str) -> CANMessageList:
|
|
|
58
54
|
if not path.exists():
|
|
59
55
|
raise FileNotFoundError(f"PCAP file not found: {path}")
|
|
60
56
|
|
|
61
|
-
|
|
62
|
-
from scapy.all import rdpcap # type: ignore[import-not-found]
|
|
63
|
-
from scapy.layers.can import CAN # type: ignore[import-not-found]
|
|
64
|
-
except ImportError as e:
|
|
65
|
-
msg = "scapy library is required for PCAP loading. Install with: uv pip install scapy"
|
|
66
|
-
raise ImportError(msg) from e
|
|
67
|
-
|
|
57
|
+
rdpcap, CAN = _import_scapy_modules()
|
|
68
58
|
messages = CANMessageList()
|
|
69
59
|
|
|
70
60
|
try:
|
|
71
|
-
# Read PCAP file
|
|
72
61
|
packets = rdpcap(str(path))
|
|
73
|
-
|
|
74
|
-
# Extract CAN frames
|
|
75
|
-
first_timestamp: float | None = None
|
|
76
|
-
for packet in packets:
|
|
77
|
-
# Check if packet contains CAN layer
|
|
78
|
-
if CAN in packet:
|
|
79
|
-
can_frame: Packet = packet[CAN]
|
|
80
|
-
|
|
81
|
-
# Get timestamp
|
|
82
|
-
if hasattr(packet, "time"):
|
|
83
|
-
if first_timestamp is None:
|
|
84
|
-
first_timestamp = float(packet.time)
|
|
85
|
-
timestamp = float(packet.time) - first_timestamp
|
|
86
|
-
else:
|
|
87
|
-
timestamp = 0.0
|
|
88
|
-
|
|
89
|
-
# Extract CAN ID and data
|
|
90
|
-
arb_id = int(can_frame.identifier)
|
|
91
|
-
|
|
92
|
-
# Get data bytes (scapy stores CAN data as bytes)
|
|
93
|
-
if hasattr(can_frame, "data"):
|
|
94
|
-
data = bytes(can_frame.data)
|
|
95
|
-
else:
|
|
96
|
-
data = b""
|
|
97
|
-
|
|
98
|
-
# Determine if extended ID (bit 31 indicates extended format)
|
|
99
|
-
# SocketCAN uses bit 31 for extended frame flag
|
|
100
|
-
is_extended = bool(arb_id & 0x80000000)
|
|
101
|
-
if is_extended:
|
|
102
|
-
arb_id = arb_id & 0x1FFFFFFF # Mask to get 29-bit ID
|
|
103
|
-
|
|
104
|
-
# Determine if CAN-FD (scapy may have an FD flag)
|
|
105
|
-
is_fd = hasattr(can_frame, "flags") and (can_frame.flags & 0x01)
|
|
106
|
-
|
|
107
|
-
# Extract channel if available
|
|
108
|
-
channel = 0
|
|
109
|
-
if hasattr(can_frame, "channel"):
|
|
110
|
-
channel = int(can_frame.channel)
|
|
111
|
-
|
|
112
|
-
# Create CANMessage
|
|
113
|
-
can_msg = CANMessage(
|
|
114
|
-
arbitration_id=arb_id,
|
|
115
|
-
timestamp=timestamp,
|
|
116
|
-
data=data,
|
|
117
|
-
is_extended=is_extended,
|
|
118
|
-
is_fd=is_fd,
|
|
119
|
-
channel=channel,
|
|
120
|
-
)
|
|
121
|
-
messages.append(can_msg)
|
|
62
|
+
_extract_can_messages(packets, CAN, messages)
|
|
122
63
|
|
|
123
64
|
except Exception as e:
|
|
124
65
|
raise ValueError(f"Failed to parse PCAP file {path}: {e}") from e
|
|
@@ -130,3 +71,112 @@ def load_pcap(file_path: Path | str) -> CANMessageList:
|
|
|
130
71
|
)
|
|
131
72
|
|
|
132
73
|
return messages
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _import_scapy_modules() -> tuple[type, type]:
|
|
77
|
+
"""Import and return required scapy modules.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
Tuple of (rdpcap function, CAN class).
|
|
81
|
+
|
|
82
|
+
Raises:
|
|
83
|
+
ImportError: If scapy is not installed.
|
|
84
|
+
"""
|
|
85
|
+
try:
|
|
86
|
+
from scapy.all import rdpcap
|
|
87
|
+
from scapy.layers.can import CAN
|
|
88
|
+
|
|
89
|
+
return rdpcap, CAN # type: ignore[return-value]
|
|
90
|
+
except ImportError as e:
|
|
91
|
+
msg = "scapy library is required for PCAP loading. Install with: uv pip install scapy"
|
|
92
|
+
raise ImportError(msg) from e
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _extract_can_messages(
|
|
96
|
+
packets: list[object], CAN: type, messages: CANMessageList
|
|
97
|
+
) -> float | None:
|
|
98
|
+
"""Extract CAN messages from PCAP packets.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
packets: List of scapy packets.
|
|
102
|
+
CAN: CAN layer class from scapy.
|
|
103
|
+
messages: CANMessageList to populate.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
First timestamp value for reference.
|
|
107
|
+
"""
|
|
108
|
+
first_timestamp: float | None = None
|
|
109
|
+
|
|
110
|
+
for packet in packets:
|
|
111
|
+
if CAN in packet: # type: ignore[operator]
|
|
112
|
+
can_frame = packet[CAN] # type: ignore[index]
|
|
113
|
+
timestamp = _extract_timestamp(packet, first_timestamp)
|
|
114
|
+
|
|
115
|
+
if first_timestamp is None and hasattr(packet, "time"):
|
|
116
|
+
first_timestamp = float(packet.time)
|
|
117
|
+
|
|
118
|
+
can_msg = _create_can_message(can_frame, timestamp)
|
|
119
|
+
messages.append(can_msg)
|
|
120
|
+
|
|
121
|
+
return first_timestamp
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _extract_timestamp(packet: object, first_timestamp: float | None) -> float:
|
|
125
|
+
"""Extract and normalize timestamp from packet.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
packet: Scapy packet with time attribute.
|
|
129
|
+
first_timestamp: Reference timestamp for normalization.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
Normalized timestamp in seconds.
|
|
133
|
+
"""
|
|
134
|
+
if hasattr(packet, "time"):
|
|
135
|
+
if first_timestamp is None:
|
|
136
|
+
return 0.0
|
|
137
|
+
return float(packet.time) - first_timestamp
|
|
138
|
+
return 0.0
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _create_can_message(can_frame: object, timestamp: float) -> CANMessage:
|
|
142
|
+
"""Create CANMessage from scapy CAN frame.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
can_frame: Scapy CAN frame object.
|
|
146
|
+
timestamp: Message timestamp.
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
CANMessage object.
|
|
150
|
+
"""
|
|
151
|
+
arb_id: int = int(can_frame.identifier) # type: ignore[attr-defined]
|
|
152
|
+
# Check if data attribute exists before accessing
|
|
153
|
+
if hasattr(can_frame, "data"):
|
|
154
|
+
data: bytes = bytes(can_frame.data)
|
|
155
|
+
else:
|
|
156
|
+
data = b""
|
|
157
|
+
|
|
158
|
+
# Determine if extended ID (bit 31 indicates extended format)
|
|
159
|
+
is_extended = bool(arb_id & 0x80000000)
|
|
160
|
+
if is_extended:
|
|
161
|
+
arb_id = arb_id & 0x1FFFFFFF # Mask to get 29-bit ID
|
|
162
|
+
|
|
163
|
+
# Determine if CAN-FD
|
|
164
|
+
if hasattr(can_frame, "flags"):
|
|
165
|
+
is_fd: bool = bool(can_frame.flags & 0x01)
|
|
166
|
+
else:
|
|
167
|
+
is_fd = False
|
|
168
|
+
|
|
169
|
+
# Extract channel if available
|
|
170
|
+
if hasattr(can_frame, "channel"):
|
|
171
|
+
channel: int = int(can_frame.channel)
|
|
172
|
+
else:
|
|
173
|
+
channel = 0
|
|
174
|
+
|
|
175
|
+
return CANMessage(
|
|
176
|
+
arbitration_id=arb_id,
|
|
177
|
+
timestamp=timestamp,
|
|
178
|
+
data=data,
|
|
179
|
+
is_extended=is_extended,
|
|
180
|
+
is_fd=is_fd,
|
|
181
|
+
channel=channel,
|
|
182
|
+
)
|
|
@@ -37,10 +37,14 @@ Example:
|
|
|
37
37
|
"""
|
|
38
38
|
|
|
39
39
|
__all__ = [
|
|
40
|
+
"UDSECU",
|
|
41
|
+
"UDSAnalyzer",
|
|
40
42
|
"UDSDecoder",
|
|
43
|
+
"UDSMessage",
|
|
41
44
|
"UDSNegativeResponse",
|
|
42
45
|
"UDSService",
|
|
43
46
|
]
|
|
44
47
|
|
|
48
|
+
from oscura.automotive.uds.analyzer import UDSECU, UDSAnalyzer, UDSMessage
|
|
45
49
|
from oscura.automotive.uds.decoder import UDSDecoder
|
|
46
50
|
from oscura.automotive.uds.models import UDSNegativeResponse, UDSService
|