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
|
@@ -0,0 +1,725 @@
|
|
|
1
|
+
"""UDS (Unified Diagnostic Services) protocol analyzer per ISO 14229.
|
|
2
|
+
|
|
3
|
+
This module provides comprehensive UDS protocol analysis for automotive diagnostics,
|
|
4
|
+
supporting all standard UDS services, diagnostic sessions, security access, DTC parsing,
|
|
5
|
+
and ECU capability discovery.
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> from oscura.automotive.uds.analyzer import UDSAnalyzer
|
|
9
|
+
>>> analyzer = UDSAnalyzer()
|
|
10
|
+
>>> msg = analyzer.parse_message(bytes([0x10, 0x03]), timestamp=1.0)
|
|
11
|
+
>>> print(msg.service_name)
|
|
12
|
+
DiagnosticSessionControl
|
|
13
|
+
>>> analyzer.export_session_flows(Path("session_flows.json"))
|
|
14
|
+
|
|
15
|
+
References:
|
|
16
|
+
ISO 14229-1:2020 - UDS specification
|
|
17
|
+
ISO 14229-2:2013 - Session layer services
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import json
|
|
23
|
+
from dataclasses import dataclass, field
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Any, ClassVar
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"UDSECU",
|
|
29
|
+
"UDSAnalyzer",
|
|
30
|
+
"UDSMessage",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class UDSMessage:
|
|
36
|
+
"""UDS message representation.
|
|
37
|
+
|
|
38
|
+
Attributes:
|
|
39
|
+
timestamp: Message timestamp in seconds.
|
|
40
|
+
service_id: Service ID (0x10-0xFF).
|
|
41
|
+
service_name: Human-readable service name.
|
|
42
|
+
is_response: True if response, False if request.
|
|
43
|
+
sub_function: Sub-function byte (if applicable).
|
|
44
|
+
data: Service data payload.
|
|
45
|
+
negative_response_code: NRC for negative responses.
|
|
46
|
+
decoded: Service-specific decoded fields.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
timestamp: float
|
|
50
|
+
service_id: int
|
|
51
|
+
service_name: str
|
|
52
|
+
is_response: bool
|
|
53
|
+
sub_function: int | None = None
|
|
54
|
+
data: bytes = b""
|
|
55
|
+
negative_response_code: int | None = None
|
|
56
|
+
decoded: dict[str, Any] = field(default_factory=dict)
|
|
57
|
+
|
|
58
|
+
def __repr__(self) -> str:
|
|
59
|
+
"""Human-readable representation."""
|
|
60
|
+
msg_type = "Response" if self.is_response else "Request"
|
|
61
|
+
nrc = f" NRC=0x{self.negative_response_code:02X}" if self.negative_response_code else ""
|
|
62
|
+
subfunc = f" sub=0x{self.sub_function:02X}" if self.sub_function is not None else ""
|
|
63
|
+
return f"UDSMessage(0x{self.service_id:02X} {self.service_name} [{msg_type}]{subfunc}{nrc})"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class UDSECU:
|
|
68
|
+
"""UDS ECU information.
|
|
69
|
+
|
|
70
|
+
Attributes:
|
|
71
|
+
ecu_id: ECU identifier.
|
|
72
|
+
supported_services: Set of supported service IDs.
|
|
73
|
+
current_session: Current diagnostic session type.
|
|
74
|
+
security_level: Current security access level (0 = locked).
|
|
75
|
+
dtcs: List of Diagnostic Trouble Codes.
|
|
76
|
+
data_identifiers: Data identifier values.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
ecu_id: str
|
|
80
|
+
supported_services: set[int] = field(default_factory=set)
|
|
81
|
+
current_session: int = 0x01 # Default session
|
|
82
|
+
security_level: int = 0 # Locked
|
|
83
|
+
dtcs: list[dict[str, Any]] = field(default_factory=list)
|
|
84
|
+
data_identifiers: dict[int, bytes] = field(default_factory=dict)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class UDSAnalyzer:
|
|
88
|
+
"""UDS (Unified Diagnostic Services) protocol analyzer.
|
|
89
|
+
|
|
90
|
+
Supports comprehensive UDS protocol analysis including:
|
|
91
|
+
- All standard UDS services (0x10-0x3E, 0x83-0x87)
|
|
92
|
+
- Positive and negative responses
|
|
93
|
+
- Diagnostic session management
|
|
94
|
+
- Security access (seed/key exchange)
|
|
95
|
+
- DTC parsing and analysis
|
|
96
|
+
- Data identifier read/write
|
|
97
|
+
- Routine control
|
|
98
|
+
- Session flow export
|
|
99
|
+
|
|
100
|
+
Example:
|
|
101
|
+
>>> analyzer = UDSAnalyzer()
|
|
102
|
+
>>> # Parse a diagnostic session control request
|
|
103
|
+
>>> msg = analyzer.parse_message(
|
|
104
|
+
... bytes([0x10, 0x03]),
|
|
105
|
+
... timestamp=1.0,
|
|
106
|
+
... ecu_id="ECU1"
|
|
107
|
+
... )
|
|
108
|
+
>>> print(msg.service_name)
|
|
109
|
+
DiagnosticSessionControl
|
|
110
|
+
>>> print(msg.decoded["session_type"])
|
|
111
|
+
ExtendedDiagnosticSession
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
# Service IDs per ISO 14229-1
|
|
115
|
+
SERVICES: ClassVar[dict[int, str]] = {
|
|
116
|
+
0x10: "DiagnosticSessionControl",
|
|
117
|
+
0x11: "ECUReset",
|
|
118
|
+
0x14: "ClearDiagnosticInformation",
|
|
119
|
+
0x19: "ReadDTCInformation",
|
|
120
|
+
0x22: "ReadDataByIdentifier",
|
|
121
|
+
0x23: "ReadMemoryByAddress",
|
|
122
|
+
0x24: "ReadScalingDataByIdentifier",
|
|
123
|
+
0x27: "SecurityAccess",
|
|
124
|
+
0x28: "CommunicationControl",
|
|
125
|
+
0x2A: "ReadDataByPeriodicIdentifier",
|
|
126
|
+
0x2C: "DynamicallyDefineDataIdentifier",
|
|
127
|
+
0x2E: "WriteDataByIdentifier",
|
|
128
|
+
0x2F: "InputOutputControlByIdentifier",
|
|
129
|
+
0x31: "RoutineControl",
|
|
130
|
+
0x34: "RequestDownload",
|
|
131
|
+
0x35: "RequestUpload",
|
|
132
|
+
0x36: "TransferData",
|
|
133
|
+
0x37: "RequestTransferExit",
|
|
134
|
+
0x38: "RequestFileTransfer",
|
|
135
|
+
0x3D: "WriteMemoryByAddress",
|
|
136
|
+
0x3E: "TesterPresent",
|
|
137
|
+
0x83: "AccessTimingParameter",
|
|
138
|
+
0x84: "SecuredDataTransmission",
|
|
139
|
+
0x85: "ControlDTCSetting",
|
|
140
|
+
0x86: "ResponseOnEvent",
|
|
141
|
+
0x87: "LinkControl",
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
# Diagnostic sessions
|
|
145
|
+
DIAGNOSTIC_SESSIONS: ClassVar[dict[int, str]] = {
|
|
146
|
+
0x01: "DefaultSession",
|
|
147
|
+
0x02: "ProgrammingSession",
|
|
148
|
+
0x03: "ExtendedDiagnosticSession",
|
|
149
|
+
0x04: "SafetySystemDiagnosticSession",
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
# Negative response codes per ISO 14229-1
|
|
153
|
+
NEGATIVE_RESPONSE_CODES: ClassVar[dict[int, str]] = {
|
|
154
|
+
0x10: "GeneralReject",
|
|
155
|
+
0x11: "ServiceNotSupported",
|
|
156
|
+
0x12: "SubFunctionNotSupported",
|
|
157
|
+
0x13: "IncorrectMessageLengthOrInvalidFormat",
|
|
158
|
+
0x14: "ResponseTooLong",
|
|
159
|
+
0x21: "BusyRepeatRequest",
|
|
160
|
+
0x22: "ConditionsNotCorrect",
|
|
161
|
+
0x24: "RequestSequenceError",
|
|
162
|
+
0x25: "NoResponseFromSubnetComponent",
|
|
163
|
+
0x26: "FailurePreventsExecutionOfRequestedAction",
|
|
164
|
+
0x31: "RequestOutOfRange",
|
|
165
|
+
0x33: "SecurityAccessDenied",
|
|
166
|
+
0x35: "InvalidKey",
|
|
167
|
+
0x36: "ExceedNumberOfAttempts",
|
|
168
|
+
0x37: "RequiredTimeDelayNotExpired",
|
|
169
|
+
0x70: "UploadDownloadNotAccepted",
|
|
170
|
+
0x71: "TransferDataSuspended",
|
|
171
|
+
0x72: "GeneralProgrammingFailure",
|
|
172
|
+
0x73: "WrongBlockSequenceCounter",
|
|
173
|
+
0x78: "RequestCorrectlyReceived-ResponsePending",
|
|
174
|
+
0x7E: "SubFunctionNotSupportedInActiveSession",
|
|
175
|
+
0x7F: "ServiceNotSupportedInActiveSession",
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
def __init__(self) -> None:
|
|
179
|
+
"""Initialize UDS analyzer."""
|
|
180
|
+
self.messages: list[UDSMessage] = []
|
|
181
|
+
self.ecus: dict[str, UDSECU] = {}
|
|
182
|
+
|
|
183
|
+
def parse_message(
|
|
184
|
+
self, data: bytes, timestamp: float = 0.0, ecu_id: str = "ECU1"
|
|
185
|
+
) -> UDSMessage:
|
|
186
|
+
"""Parse UDS message (CAN/DoIP payload).
|
|
187
|
+
|
|
188
|
+
UDS Message Format:
|
|
189
|
+
- Service ID (1 byte) - or 0x7F for negative response
|
|
190
|
+
- For negative response:
|
|
191
|
+
- Failed Service ID (1 byte)
|
|
192
|
+
- Negative Response Code (1 byte)
|
|
193
|
+
- For positive response:
|
|
194
|
+
- Service ID + 0x40
|
|
195
|
+
- Service-specific data
|
|
196
|
+
- For request:
|
|
197
|
+
- Service ID
|
|
198
|
+
- Sub-function (optional, 1 byte)
|
|
199
|
+
- Service-specific data
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
data: Raw UDS message data.
|
|
203
|
+
timestamp: Message timestamp in seconds.
|
|
204
|
+
ecu_id: ECU identifier.
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
Parsed UDSMessage object.
|
|
208
|
+
|
|
209
|
+
Raises:
|
|
210
|
+
ValueError: If message is invalid or empty.
|
|
211
|
+
|
|
212
|
+
Example:
|
|
213
|
+
>>> analyzer = UDSAnalyzer()
|
|
214
|
+
>>> msg = analyzer.parse_message(bytes([0x10, 0x03]), timestamp=1.0)
|
|
215
|
+
>>> print(msg.service_name)
|
|
216
|
+
DiagnosticSessionControl
|
|
217
|
+
"""
|
|
218
|
+
if len(data) == 0:
|
|
219
|
+
raise ValueError("UDS message is empty")
|
|
220
|
+
|
|
221
|
+
# Ensure ECU exists
|
|
222
|
+
if ecu_id not in self.ecus:
|
|
223
|
+
self.ecus[ecu_id] = UDSECU(ecu_id=ecu_id)
|
|
224
|
+
|
|
225
|
+
sid = data[0]
|
|
226
|
+
|
|
227
|
+
# Check for negative response
|
|
228
|
+
if sid == 0x7F:
|
|
229
|
+
if len(data) < 3:
|
|
230
|
+
raise ValueError("Negative response too short")
|
|
231
|
+
failed_sid = data[1]
|
|
232
|
+
nrc = data[2]
|
|
233
|
+
|
|
234
|
+
decoded = {"nrc_name": self.NEGATIVE_RESPONSE_CODES.get(nrc, "Unknown")}
|
|
235
|
+
|
|
236
|
+
msg = UDSMessage(
|
|
237
|
+
timestamp=timestamp,
|
|
238
|
+
service_id=failed_sid,
|
|
239
|
+
service_name=self.SERVICES.get(failed_sid, f"Unknown (0x{failed_sid:02X})"),
|
|
240
|
+
is_response=True,
|
|
241
|
+
negative_response_code=nrc,
|
|
242
|
+
data=data[3:],
|
|
243
|
+
decoded=decoded,
|
|
244
|
+
)
|
|
245
|
+
else:
|
|
246
|
+
# Check for positive response (SID + 0x40)
|
|
247
|
+
is_response = bool(sid & 0x40)
|
|
248
|
+
actual_sid = sid & 0xBF if is_response else sid
|
|
249
|
+
|
|
250
|
+
# Parse sub-function and data
|
|
251
|
+
service_data = data[1:]
|
|
252
|
+
|
|
253
|
+
decoded = self._decode_service(actual_sid, service_data, is_response)
|
|
254
|
+
sub_function_val = decoded.get("sub_function")
|
|
255
|
+
# Ensure sub_function is int or None (mypy strict)
|
|
256
|
+
sub_function: int | None = (
|
|
257
|
+
int(sub_function_val) if isinstance(sub_function_val, int) else None
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
msg = UDSMessage(
|
|
261
|
+
timestamp=timestamp,
|
|
262
|
+
service_id=actual_sid,
|
|
263
|
+
service_name=self.SERVICES.get(actual_sid, f"Unknown (0x{actual_sid:02X})"),
|
|
264
|
+
is_response=is_response,
|
|
265
|
+
sub_function=sub_function,
|
|
266
|
+
data=service_data,
|
|
267
|
+
decoded=decoded,
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
# Update ECU state
|
|
271
|
+
self._update_ecu_state(ecu_id, msg)
|
|
272
|
+
|
|
273
|
+
self.messages.append(msg)
|
|
274
|
+
return msg
|
|
275
|
+
|
|
276
|
+
def _decode_service(self, service_id: int, data: bytes, is_response: bool) -> dict[str, Any]:
|
|
277
|
+
"""Decode service-specific data.
|
|
278
|
+
|
|
279
|
+
Args:
|
|
280
|
+
service_id: Service ID.
|
|
281
|
+
data: Service payload data.
|
|
282
|
+
is_response: True if response message.
|
|
283
|
+
|
|
284
|
+
Returns:
|
|
285
|
+
Dictionary of decoded fields.
|
|
286
|
+
"""
|
|
287
|
+
decoders = {
|
|
288
|
+
0x10: self._decode_diagnostic_session_control,
|
|
289
|
+
0x11: self._decode_ecu_reset,
|
|
290
|
+
0x19: self._decode_read_dtc,
|
|
291
|
+
0x22: self._decode_read_data_by_id,
|
|
292
|
+
0x27: self._decode_security_access,
|
|
293
|
+
0x2E: self._decode_write_data_by_id,
|
|
294
|
+
0x31: self._decode_routine_control,
|
|
295
|
+
0x3E: self._decode_tester_present,
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
decoder = decoders.get(service_id)
|
|
299
|
+
if decoder:
|
|
300
|
+
return decoder(data, is_response)
|
|
301
|
+
|
|
302
|
+
return {}
|
|
303
|
+
|
|
304
|
+
def _decode_diagnostic_session_control(self, data: bytes, is_response: bool) -> dict[str, Any]:
|
|
305
|
+
"""Decode DiagnosticSessionControl (0x10).
|
|
306
|
+
|
|
307
|
+
Request: [sub-function]
|
|
308
|
+
Response: [sub-function, P2_server_max (2 bytes), P2*_server_max (2 bytes)]
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
data: Service payload.
|
|
312
|
+
is_response: True if response.
|
|
313
|
+
|
|
314
|
+
Returns:
|
|
315
|
+
Decoded fields dictionary.
|
|
316
|
+
"""
|
|
317
|
+
if len(data) == 0:
|
|
318
|
+
return {}
|
|
319
|
+
|
|
320
|
+
sub_function = data[0] & 0x7F
|
|
321
|
+
suppress_response = bool(data[0] & 0x80)
|
|
322
|
+
|
|
323
|
+
result = {
|
|
324
|
+
"sub_function": sub_function,
|
|
325
|
+
"suppress_positive_response": suppress_response,
|
|
326
|
+
"session_type": self.DIAGNOSTIC_SESSIONS.get(sub_function, f"0x{sub_function:02X}"),
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if is_response and len(data) >= 5:
|
|
330
|
+
# P2_server_max and P2*_server_max in milliseconds
|
|
331
|
+
p2_server_max = int.from_bytes(data[1:3], "big")
|
|
332
|
+
p2_star_server_max = int.from_bytes(data[3:5], "big")
|
|
333
|
+
result["p2_server_max_ms"] = p2_server_max
|
|
334
|
+
result["p2_star_server_max_ms"] = p2_star_server_max
|
|
335
|
+
|
|
336
|
+
return result
|
|
337
|
+
|
|
338
|
+
def _decode_ecu_reset(self, data: bytes, is_response: bool) -> dict[str, Any]:
|
|
339
|
+
"""Decode ECUReset (0x11).
|
|
340
|
+
|
|
341
|
+
Request: [sub-function]
|
|
342
|
+
Response: [sub-function, power_down_time? (1 byte)]
|
|
343
|
+
|
|
344
|
+
Sub-functions:
|
|
345
|
+
- 0x01: hardReset
|
|
346
|
+
- 0x02: keyOffOnReset
|
|
347
|
+
- 0x03: softReset
|
|
348
|
+
- 0x04: enableRapidPowerShutDown
|
|
349
|
+
- 0x05: disableRapidPowerShutDown
|
|
350
|
+
|
|
351
|
+
Args:
|
|
352
|
+
data: Service payload.
|
|
353
|
+
is_response: True if response.
|
|
354
|
+
|
|
355
|
+
Returns:
|
|
356
|
+
Decoded fields dictionary.
|
|
357
|
+
"""
|
|
358
|
+
if len(data) == 0:
|
|
359
|
+
return {}
|
|
360
|
+
|
|
361
|
+
sub_function = data[0] & 0x7F
|
|
362
|
+
suppress_response = bool(data[0] & 0x80)
|
|
363
|
+
|
|
364
|
+
reset_types = {
|
|
365
|
+
0x01: "hardReset",
|
|
366
|
+
0x02: "keyOffOnReset",
|
|
367
|
+
0x03: "softReset",
|
|
368
|
+
0x04: "enableRapidPowerShutDown",
|
|
369
|
+
0x05: "disableRapidPowerShutDown",
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
result = {
|
|
373
|
+
"sub_function": sub_function,
|
|
374
|
+
"suppress_positive_response": suppress_response,
|
|
375
|
+
"reset_type": reset_types.get(sub_function, f"0x{sub_function:02X}"),
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if is_response and len(data) >= 2:
|
|
379
|
+
power_down_time = data[1]
|
|
380
|
+
result["power_down_time_s"] = power_down_time
|
|
381
|
+
|
|
382
|
+
return result
|
|
383
|
+
|
|
384
|
+
def _decode_read_dtc(self, data: bytes, is_response: bool) -> dict[str, Any]:
|
|
385
|
+
"""Decode ReadDTCInformation (0x19).
|
|
386
|
+
|
|
387
|
+
Sub-functions:
|
|
388
|
+
- 0x01: reportNumberOfDTCByStatusMask
|
|
389
|
+
- 0x02: reportDTCByStatusMask
|
|
390
|
+
- 0x04: reportDTCSnapshotIdentification
|
|
391
|
+
- 0x06: reportDTCExtDataRecordByDTCNumber
|
|
392
|
+
|
|
393
|
+
Response format for 0x02:
|
|
394
|
+
- [sub-function, availability_mask, dtc1_high, dtc1_mid, dtc1_low, status1, ...]
|
|
395
|
+
|
|
396
|
+
Args:
|
|
397
|
+
data: Service payload.
|
|
398
|
+
is_response: True if response.
|
|
399
|
+
|
|
400
|
+
Returns:
|
|
401
|
+
Decoded fields dictionary.
|
|
402
|
+
"""
|
|
403
|
+
if len(data) == 0:
|
|
404
|
+
return {}
|
|
405
|
+
|
|
406
|
+
sub_function = data[0]
|
|
407
|
+
result = {"sub_function": sub_function}
|
|
408
|
+
|
|
409
|
+
if sub_function == 0x02 and is_response and len(data) >= 2:
|
|
410
|
+
# Parse DTC list
|
|
411
|
+
dtcs = []
|
|
412
|
+
offset = 2 # Skip sub-function echo and availability mask
|
|
413
|
+
|
|
414
|
+
while offset + 4 <= len(data):
|
|
415
|
+
dtc_bytes = data[offset : offset + 3]
|
|
416
|
+
status = data[offset + 3]
|
|
417
|
+
|
|
418
|
+
# DTC format: 3 bytes (6 hex digits)
|
|
419
|
+
dtc_value = int.from_bytes(dtc_bytes, "big")
|
|
420
|
+
dtc_string = f"{dtc_value:06X}"
|
|
421
|
+
|
|
422
|
+
dtcs.append(
|
|
423
|
+
{
|
|
424
|
+
"dtc": dtc_string,
|
|
425
|
+
"status": status,
|
|
426
|
+
"test_failed": bool(status & 0x01),
|
|
427
|
+
"test_failed_this_operation_cycle": bool(status & 0x02),
|
|
428
|
+
"pending": bool(status & 0x04),
|
|
429
|
+
"confirmed": bool(status & 0x08),
|
|
430
|
+
"test_not_completed_since_last_clear": bool(status & 0x10),
|
|
431
|
+
"test_failed_since_last_clear": bool(status & 0x20),
|
|
432
|
+
"test_not_completed_this_operation_cycle": bool(status & 0x40),
|
|
433
|
+
"warning_indicator_requested": bool(status & 0x80),
|
|
434
|
+
}
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
offset += 4
|
|
438
|
+
|
|
439
|
+
result["dtcs"] = dtcs # type: ignore[assignment]
|
|
440
|
+
result["dtc_count"] = len(dtcs)
|
|
441
|
+
if len(data) >= 2:
|
|
442
|
+
result["availability_mask"] = data[1]
|
|
443
|
+
|
|
444
|
+
return result
|
|
445
|
+
|
|
446
|
+
def _decode_read_data_by_id(self, data: bytes, is_response: bool) -> dict[str, Any]:
|
|
447
|
+
"""Decode ReadDataByIdentifier (0x22).
|
|
448
|
+
|
|
449
|
+
Request: [did1_high, did1_low, did2_high, did2_low, ...]
|
|
450
|
+
Response: [did1_high, did1_low, data1..., did2_high, did2_low, data2..., ...]
|
|
451
|
+
|
|
452
|
+
Args:
|
|
453
|
+
data: Service payload.
|
|
454
|
+
is_response: True if response.
|
|
455
|
+
|
|
456
|
+
Returns:
|
|
457
|
+
Decoded fields dictionary.
|
|
458
|
+
"""
|
|
459
|
+
if len(data) < 2:
|
|
460
|
+
return {}
|
|
461
|
+
|
|
462
|
+
result: dict[str, Any] = {}
|
|
463
|
+
|
|
464
|
+
if not is_response:
|
|
465
|
+
# Parse requested DIDs
|
|
466
|
+
dids = []
|
|
467
|
+
offset = 0
|
|
468
|
+
while offset + 2 <= len(data):
|
|
469
|
+
did = int.from_bytes(data[offset : offset + 2], "big")
|
|
470
|
+
dids.append(did)
|
|
471
|
+
offset += 2
|
|
472
|
+
result["requested_dids"] = dids
|
|
473
|
+
else:
|
|
474
|
+
# Parse response DID and data
|
|
475
|
+
# Note: Without knowing DID data lengths, we can only parse first DID
|
|
476
|
+
did = int.from_bytes(data[0:2], "big")
|
|
477
|
+
did_data = data[2:]
|
|
478
|
+
result["did"] = did
|
|
479
|
+
result["did_data"] = did_data.hex()
|
|
480
|
+
|
|
481
|
+
return result
|
|
482
|
+
|
|
483
|
+
def _decode_security_access(self, data: bytes, is_response: bool) -> dict[str, Any]:
|
|
484
|
+
"""Decode SecurityAccess (0x27) - seed/key exchange.
|
|
485
|
+
|
|
486
|
+
Request:
|
|
487
|
+
- Sub-function (1 byte) - 0x01 requestSeed, 0x02 sendKey, etc.
|
|
488
|
+
- Data (variable) - empty for seed request, key for sendKey
|
|
489
|
+
|
|
490
|
+
Response:
|
|
491
|
+
- Sub-function (1 byte)
|
|
492
|
+
- Seed/Key data (variable)
|
|
493
|
+
|
|
494
|
+
Args:
|
|
495
|
+
data: Service payload.
|
|
496
|
+
is_response: True if response.
|
|
497
|
+
|
|
498
|
+
Returns:
|
|
499
|
+
Decoded fields dictionary.
|
|
500
|
+
|
|
501
|
+
Example:
|
|
502
|
+
>>> analyzer = UDSAnalyzer()
|
|
503
|
+
>>> # Request seed for level 1
|
|
504
|
+
>>> msg = analyzer.parse_message(bytes([0x27, 0x01]), timestamp=1.0)
|
|
505
|
+
>>> print(msg.decoded["access_type"])
|
|
506
|
+
requestSeed
|
|
507
|
+
>>> print(msg.decoded["security_level"])
|
|
508
|
+
1
|
|
509
|
+
"""
|
|
510
|
+
if len(data) == 0:
|
|
511
|
+
return {}
|
|
512
|
+
|
|
513
|
+
sub_function = data[0] & 0x7F # Mask suppress positive response bit
|
|
514
|
+
suppress_response = bool(data[0] & 0x80)
|
|
515
|
+
payload = data[1:]
|
|
516
|
+
|
|
517
|
+
result = {
|
|
518
|
+
"sub_function": sub_function,
|
|
519
|
+
"suppress_positive_response": suppress_response,
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if sub_function % 2 == 1: # Odd = requestSeed
|
|
523
|
+
result["access_type"] = "requestSeed" # type: ignore[assignment]
|
|
524
|
+
result["security_level"] = (sub_function + 1) // 2
|
|
525
|
+
if is_response and len(payload) > 0:
|
|
526
|
+
result["seed"] = payload.hex() # type: ignore[assignment]
|
|
527
|
+
else: # Even = sendKey
|
|
528
|
+
result["access_type"] = "sendKey" # type: ignore[assignment]
|
|
529
|
+
result["security_level"] = sub_function // 2
|
|
530
|
+
if not is_response and len(payload) > 0:
|
|
531
|
+
result["key"] = payload.hex() # type: ignore[assignment]
|
|
532
|
+
|
|
533
|
+
return result
|
|
534
|
+
|
|
535
|
+
def _decode_write_data_by_id(self, data: bytes, is_response: bool) -> dict[str, Any]:
|
|
536
|
+
"""Decode WriteDataByIdentifier (0x2E).
|
|
537
|
+
|
|
538
|
+
Request: [did_high, did_low, data...]
|
|
539
|
+
Response: [did_high, did_low]
|
|
540
|
+
|
|
541
|
+
Args:
|
|
542
|
+
data: Service payload.
|
|
543
|
+
is_response: True if response.
|
|
544
|
+
|
|
545
|
+
Returns:
|
|
546
|
+
Decoded fields dictionary.
|
|
547
|
+
"""
|
|
548
|
+
if len(data) < 2:
|
|
549
|
+
return {}
|
|
550
|
+
|
|
551
|
+
did = int.from_bytes(data[0:2], "big")
|
|
552
|
+
result = {"did": did}
|
|
553
|
+
|
|
554
|
+
if not is_response and len(data) > 2:
|
|
555
|
+
result["did_data"] = data[2:].hex() # type: ignore[assignment]
|
|
556
|
+
|
|
557
|
+
return result
|
|
558
|
+
|
|
559
|
+
def _decode_routine_control(self, data: bytes, is_response: bool) -> dict[str, Any]:
|
|
560
|
+
"""Decode RoutineControl (0x31).
|
|
561
|
+
|
|
562
|
+
Request: [sub-function, routine_id_high, routine_id_low, routine_option...]
|
|
563
|
+
Response: [sub-function, routine_id_high, routine_id_low, status_record...]
|
|
564
|
+
|
|
565
|
+
Sub-functions:
|
|
566
|
+
- 0x01: startRoutine
|
|
567
|
+
- 0x02: stopRoutine
|
|
568
|
+
- 0x03: requestRoutineResults
|
|
569
|
+
|
|
570
|
+
Args:
|
|
571
|
+
data: Service payload.
|
|
572
|
+
is_response: True if response.
|
|
573
|
+
|
|
574
|
+
Returns:
|
|
575
|
+
Decoded fields dictionary.
|
|
576
|
+
"""
|
|
577
|
+
if len(data) < 3:
|
|
578
|
+
return {}
|
|
579
|
+
|
|
580
|
+
sub_function = data[0] & 0x7F
|
|
581
|
+
suppress_response = bool(data[0] & 0x80)
|
|
582
|
+
routine_id = int.from_bytes(data[1:3], "big")
|
|
583
|
+
|
|
584
|
+
routine_types = {
|
|
585
|
+
0x01: "startRoutine",
|
|
586
|
+
0x02: "stopRoutine",
|
|
587
|
+
0x03: "requestRoutineResults",
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
result = {
|
|
591
|
+
"sub_function": sub_function,
|
|
592
|
+
"suppress_positive_response": suppress_response,
|
|
593
|
+
"routine_type": routine_types.get(sub_function, f"0x{sub_function:02X}"),
|
|
594
|
+
"routine_id": routine_id,
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
if len(data) > 3:
|
|
598
|
+
if is_response:
|
|
599
|
+
result["status_record"] = data[3:].hex()
|
|
600
|
+
else:
|
|
601
|
+
result["routine_option"] = data[3:].hex()
|
|
602
|
+
|
|
603
|
+
return result
|
|
604
|
+
|
|
605
|
+
def _decode_tester_present(self, data: bytes, is_response: bool) -> dict[str, Any]:
|
|
606
|
+
"""Decode TesterPresent (0x3E).
|
|
607
|
+
|
|
608
|
+
Request: [sub-function] (typically 0x00 or 0x80)
|
|
609
|
+
Response: [sub-function]
|
|
610
|
+
|
|
611
|
+
Args:
|
|
612
|
+
data: Service payload.
|
|
613
|
+
is_response: True if response.
|
|
614
|
+
|
|
615
|
+
Returns:
|
|
616
|
+
Decoded fields dictionary.
|
|
617
|
+
"""
|
|
618
|
+
if len(data) == 0:
|
|
619
|
+
return {}
|
|
620
|
+
|
|
621
|
+
sub_function = data[0] & 0x7F
|
|
622
|
+
suppress_response = bool(data[0] & 0x80)
|
|
623
|
+
|
|
624
|
+
return {
|
|
625
|
+
"sub_function": sub_function,
|
|
626
|
+
"suppress_positive_response": suppress_response,
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
def _update_ecu_state(self, ecu_id: str, msg: UDSMessage) -> None:
|
|
630
|
+
"""Update ECU state based on message.
|
|
631
|
+
|
|
632
|
+
Args:
|
|
633
|
+
ecu_id: ECU identifier.
|
|
634
|
+
msg: Parsed UDS message.
|
|
635
|
+
"""
|
|
636
|
+
ecu = self.ecus[ecu_id]
|
|
637
|
+
|
|
638
|
+
# Track supported services from requests
|
|
639
|
+
if not msg.is_response and msg.negative_response_code is None:
|
|
640
|
+
ecu.supported_services.add(msg.service_id)
|
|
641
|
+
|
|
642
|
+
# Only process successful responses
|
|
643
|
+
if not msg.is_response or msg.negative_response_code is not None:
|
|
644
|
+
return
|
|
645
|
+
|
|
646
|
+
# Update session state (0x10 DiagnosticSessionControl)
|
|
647
|
+
if msg.service_id == 0x10:
|
|
648
|
+
self._update_session_state(ecu, msg)
|
|
649
|
+
|
|
650
|
+
# Update security level (0x27 SecurityAccess)
|
|
651
|
+
if msg.service_id == 0x27:
|
|
652
|
+
self._update_security_level(ecu, msg)
|
|
653
|
+
|
|
654
|
+
# Store DTCs (0x19 ReadDTCInformation)
|
|
655
|
+
if msg.service_id == 0x19:
|
|
656
|
+
self._store_dtcs(ecu, msg)
|
|
657
|
+
|
|
658
|
+
# Store data identifiers (0x22 ReadDataByIdentifier)
|
|
659
|
+
if msg.service_id == 0x22:
|
|
660
|
+
self._store_data_identifier(ecu, msg)
|
|
661
|
+
|
|
662
|
+
def _update_session_state(self, ecu: UDSECU, msg: UDSMessage) -> None:
|
|
663
|
+
"""Update ECU diagnostic session state."""
|
|
664
|
+
session_type = msg.decoded.get("sub_function")
|
|
665
|
+
if session_type is not None:
|
|
666
|
+
ecu.current_session = session_type
|
|
667
|
+
|
|
668
|
+
def _update_security_level(self, ecu: UDSECU, msg: UDSMessage) -> None:
|
|
669
|
+
"""Update ECU security access level."""
|
|
670
|
+
if msg.decoded.get("access_type") == "sendKey":
|
|
671
|
+
level = msg.decoded.get("security_level", 0)
|
|
672
|
+
ecu.security_level = level
|
|
673
|
+
|
|
674
|
+
def _store_dtcs(self, ecu: UDSECU, msg: UDSMessage) -> None:
|
|
675
|
+
"""Store diagnostic trouble codes."""
|
|
676
|
+
dtcs = msg.decoded.get("dtcs")
|
|
677
|
+
if dtcs:
|
|
678
|
+
ecu.dtcs = dtcs
|
|
679
|
+
|
|
680
|
+
def _store_data_identifier(self, ecu: UDSECU, msg: UDSMessage) -> None:
|
|
681
|
+
"""Store data identifier value."""
|
|
682
|
+
did = msg.decoded.get("did")
|
|
683
|
+
did_data_hex = msg.decoded.get("did_data")
|
|
684
|
+
if did is not None and did_data_hex is not None:
|
|
685
|
+
ecu.data_identifiers[did] = bytes.fromhex(did_data_hex)
|
|
686
|
+
|
|
687
|
+
def export_session_flows(self, output_path: Path) -> None:
|
|
688
|
+
"""Export diagnostic session flows as JSON.
|
|
689
|
+
|
|
690
|
+
Args:
|
|
691
|
+
output_path: Path to output JSON file.
|
|
692
|
+
|
|
693
|
+
Example:
|
|
694
|
+
>>> analyzer = UDSAnalyzer()
|
|
695
|
+
>>> # ... parse messages ...
|
|
696
|
+
>>> analyzer.export_session_flows(Path("flows.json"))
|
|
697
|
+
"""
|
|
698
|
+
flows = {
|
|
699
|
+
"messages": [
|
|
700
|
+
{
|
|
701
|
+
"timestamp": msg.timestamp,
|
|
702
|
+
"service_id": msg.service_id,
|
|
703
|
+
"service_name": msg.service_name,
|
|
704
|
+
"is_response": msg.is_response,
|
|
705
|
+
"sub_function": msg.sub_function,
|
|
706
|
+
"negative_response_code": msg.negative_response_code,
|
|
707
|
+
"decoded": msg.decoded,
|
|
708
|
+
}
|
|
709
|
+
for msg in self.messages
|
|
710
|
+
],
|
|
711
|
+
"ecus": {
|
|
712
|
+
ecu_id: {
|
|
713
|
+
"supported_services": sorted(ecu.supported_services),
|
|
714
|
+
"current_session": ecu.current_session,
|
|
715
|
+
"security_level": ecu.security_level,
|
|
716
|
+
"dtc_count": len(ecu.dtcs),
|
|
717
|
+
"dtcs": ecu.dtcs,
|
|
718
|
+
"data_identifier_count": len(ecu.data_identifiers),
|
|
719
|
+
}
|
|
720
|
+
for ecu_id, ecu in self.ecus.items()
|
|
721
|
+
},
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
with output_path.open("w") as f:
|
|
725
|
+
json.dump(flows, f, indent=2)
|