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,605 @@
|
|
|
1
|
+
"""J1939 protocol analyzer with transport protocol and SPN support.
|
|
2
|
+
|
|
3
|
+
This module provides comprehensive J1939 (SAE J1939) protocol analysis for heavy-duty
|
|
4
|
+
vehicles and industrial equipment, including:
|
|
5
|
+
- J1939 CAN identifier decoding (29-bit extended IDs)
|
|
6
|
+
- PGN (Parameter Group Number) extraction
|
|
7
|
+
- Priority and address parsing
|
|
8
|
+
- Transport protocol (TP.CM, TP.DT, BAM) multi-packet reassembly
|
|
9
|
+
- SPN (Suspect Parameter Number) decoding
|
|
10
|
+
- Message and topology export
|
|
11
|
+
|
|
12
|
+
Example:
|
|
13
|
+
>>> from oscura.automotive.j1939.analyzer import J1939Analyzer
|
|
14
|
+
>>> analyzer = J1939Analyzer()
|
|
15
|
+
>>> msg = analyzer.parse_message(0x18FEF100, b'\\x00\\x01\\x02\\x03\\x04\\x05\\x06\\x07', 1.0)
|
|
16
|
+
>>> print(msg.identifier.pgn)
|
|
17
|
+
65265
|
|
18
|
+
>>> print(msg.pgn_name)
|
|
19
|
+
Cruise Control/Vehicle Speed
|
|
20
|
+
|
|
21
|
+
References:
|
|
22
|
+
SAE J1939/21 - Data Link Layer
|
|
23
|
+
SAE J1939/71 - Vehicle Application Layer
|
|
24
|
+
SAE J1939/73 - Application Layer - Diagnostics
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import json
|
|
30
|
+
from dataclasses import dataclass, field
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
from typing import Any, ClassVar
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"J1939SPN",
|
|
36
|
+
"J1939Analyzer",
|
|
37
|
+
"J1939Identifier",
|
|
38
|
+
"J1939Message",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class J1939Identifier:
|
|
44
|
+
"""J1939 CAN identifier breakdown.
|
|
45
|
+
|
|
46
|
+
Attributes:
|
|
47
|
+
priority: Message priority (0-7, lower is higher priority).
|
|
48
|
+
reserved: Reserved bit (typically 0).
|
|
49
|
+
data_page: Data page bit (0 or 1).
|
|
50
|
+
pdu_format: PDU Format (8 bits).
|
|
51
|
+
pdu_specific: PDU Specific (8 bits, destination or group extension).
|
|
52
|
+
source_address: Source address (8 bits).
|
|
53
|
+
pgn: Calculated Parameter Group Number.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
priority: int
|
|
57
|
+
reserved: int
|
|
58
|
+
data_page: int
|
|
59
|
+
pdu_format: int
|
|
60
|
+
pdu_specific: int
|
|
61
|
+
source_address: int
|
|
62
|
+
pgn: int
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class J1939Message:
|
|
67
|
+
"""J1939 message representation.
|
|
68
|
+
|
|
69
|
+
Attributes:
|
|
70
|
+
timestamp: Message timestamp in seconds.
|
|
71
|
+
can_id: 29-bit extended CAN identifier.
|
|
72
|
+
identifier: Decoded J1939 identifier components.
|
|
73
|
+
data: Message data payload (up to 8 bytes for single frame).
|
|
74
|
+
pgn_name: Human-readable PGN name (if known).
|
|
75
|
+
decoded_spns: Decoded Suspect Parameter Numbers.
|
|
76
|
+
is_transport_protocol: True if transport protocol message (TP.CM/TP.DT/BAM).
|
|
77
|
+
transport_info: Transport protocol metadata.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
timestamp: float
|
|
81
|
+
can_id: int
|
|
82
|
+
identifier: J1939Identifier
|
|
83
|
+
data: bytes
|
|
84
|
+
pgn_name: str | None = None
|
|
85
|
+
decoded_spns: dict[str, Any] = field(default_factory=dict)
|
|
86
|
+
is_transport_protocol: bool = False
|
|
87
|
+
transport_info: dict[str, Any] | None = None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@dataclass
|
|
91
|
+
class J1939SPN:
|
|
92
|
+
"""Suspect Parameter Number definition.
|
|
93
|
+
|
|
94
|
+
Attributes:
|
|
95
|
+
spn: SPN number.
|
|
96
|
+
name: Parameter name.
|
|
97
|
+
start_bit: Bit position in data (0-based).
|
|
98
|
+
bit_length: Number of bits.
|
|
99
|
+
resolution: Scaling factor.
|
|
100
|
+
offset: Offset to add after scaling.
|
|
101
|
+
unit: Engineering unit.
|
|
102
|
+
data_range: Valid data range (min, max).
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
spn: int
|
|
106
|
+
name: str
|
|
107
|
+
start_bit: int
|
|
108
|
+
bit_length: int
|
|
109
|
+
resolution: float = 1.0
|
|
110
|
+
offset: float = 0.0
|
|
111
|
+
unit: str = ""
|
|
112
|
+
data_range: tuple[float, float] = (0.0, 0.0)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class J1939Analyzer:
|
|
116
|
+
"""J1939 protocol analyzer for heavy-duty vehicles.
|
|
117
|
+
|
|
118
|
+
Supports comprehensive J1939 protocol analysis including:
|
|
119
|
+
- 29-bit extended CAN ID decoding
|
|
120
|
+
- PGN extraction and naming
|
|
121
|
+
- Transport protocol (TP.CM, TP.DT, BAM) multi-packet reassembly
|
|
122
|
+
- SPN decoding with user-defined parameters
|
|
123
|
+
- Message history and export
|
|
124
|
+
|
|
125
|
+
Example:
|
|
126
|
+
>>> analyzer = J1939Analyzer()
|
|
127
|
+
>>> # Parse a single-frame message
|
|
128
|
+
>>> msg = analyzer.parse_message(0x0CF00400, b'\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff')
|
|
129
|
+
>>> print(f"PGN: {msg.identifier.pgn}, Priority: {msg.identifier.priority}")
|
|
130
|
+
PGN: 61444, Priority: 3
|
|
131
|
+
>>> # Add SPN definition
|
|
132
|
+
>>> spn = J1939SPN(
|
|
133
|
+
... spn=190,
|
|
134
|
+
... name="Engine Speed",
|
|
135
|
+
... start_bit=24,
|
|
136
|
+
... bit_length=16,
|
|
137
|
+
... resolution=0.125,
|
|
138
|
+
... unit="rpm"
|
|
139
|
+
... )
|
|
140
|
+
>>> analyzer.add_spn_definition(61444, spn)
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
# Well-known PGNs per SAE J1939/71
|
|
144
|
+
PGNS: ClassVar[dict[int, str]] = {
|
|
145
|
+
0: "Torque/Speed Control 1",
|
|
146
|
+
59392: "Acknowledgment",
|
|
147
|
+
59904: "Request",
|
|
148
|
+
60160: "Transport Protocol - Connection Management (TP.CM)",
|
|
149
|
+
60416: "Transport Protocol - Data Transfer (TP.DT)",
|
|
150
|
+
60928: "Address Claimed",
|
|
151
|
+
61440: "Electronic Retarder Controller 1",
|
|
152
|
+
61441: "Electronic Brake Controller 1",
|
|
153
|
+
61442: "Electronic Transmission Controller 1",
|
|
154
|
+
61443: "Electronic Engine Controller 2",
|
|
155
|
+
61444: "Electronic Engine Controller 1",
|
|
156
|
+
65226: "Active Diagnostic Trouble Codes",
|
|
157
|
+
65227: "Previously Active Diagnostic Trouble Codes",
|
|
158
|
+
65265: "Cruise Control/Vehicle Speed",
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
# Transport Protocol Connection Management control bytes
|
|
162
|
+
TP_CM_RTS: ClassVar[int] = 16 # Request To Send
|
|
163
|
+
TP_CM_CTS: ClassVar[int] = 17 # Clear To Send
|
|
164
|
+
TP_CM_EOM_ACK: ClassVar[int] = 19 # End of Message Acknowledgment
|
|
165
|
+
TP_CM_BAM: ClassVar[int] = 32 # Broadcast Announce Message
|
|
166
|
+
TP_CM_ABORT: ClassVar[int] = 255 # Connection Abort
|
|
167
|
+
|
|
168
|
+
def __init__(self) -> None:
|
|
169
|
+
"""Initialize J1939 analyzer."""
|
|
170
|
+
self.messages: list[J1939Message] = []
|
|
171
|
+
# Transport sessions: (source_addr, dest_addr) -> session dict
|
|
172
|
+
self.transport_sessions: dict[tuple[int, int], dict[str, Any]] = {}
|
|
173
|
+
# SPN definitions: PGN -> [SPNs]
|
|
174
|
+
self.spn_definitions: dict[int, list[J1939SPN]] = {}
|
|
175
|
+
|
|
176
|
+
def parse_message(self, can_id: int, data: bytes, timestamp: float = 0.0) -> J1939Message:
|
|
177
|
+
"""Parse J1939 message from CAN frame.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
can_id: 29-bit extended CAN identifier.
|
|
181
|
+
data: Message data payload (up to 8 bytes).
|
|
182
|
+
timestamp: Message timestamp in seconds.
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
Parsed J1939 message.
|
|
186
|
+
|
|
187
|
+
Raises:
|
|
188
|
+
ValueError: If CAN ID is invalid or data is too long.
|
|
189
|
+
|
|
190
|
+
Example:
|
|
191
|
+
>>> analyzer = J1939Analyzer()
|
|
192
|
+
>>> msg = analyzer.parse_message(0x18FEF100, b'\\x00\\x01\\x02\\x03')
|
|
193
|
+
>>> msg.identifier.pgn
|
|
194
|
+
65265
|
|
195
|
+
"""
|
|
196
|
+
if can_id > 0x1FFFFFFF:
|
|
197
|
+
raise ValueError(f"Invalid 29-bit CAN ID: 0x{can_id:08X}")
|
|
198
|
+
if len(data) > 8:
|
|
199
|
+
raise ValueError(f"Data too long: {len(data)} bytes (max 8)")
|
|
200
|
+
|
|
201
|
+
# Decode identifier
|
|
202
|
+
identifier = self._decode_identifier(can_id)
|
|
203
|
+
|
|
204
|
+
# Get PGN name
|
|
205
|
+
pgn_name = self.PGNS.get(identifier.pgn)
|
|
206
|
+
|
|
207
|
+
# Parse transport protocol
|
|
208
|
+
transport_info = self._parse_transport_protocol(identifier.pgn, data)
|
|
209
|
+
is_transport = transport_info is not None
|
|
210
|
+
|
|
211
|
+
# Handle multi-packet transport data
|
|
212
|
+
if is_transport and transport_info is not None:
|
|
213
|
+
tp_type = transport_info.get("type")
|
|
214
|
+
if tp_type == "TP.DT":
|
|
215
|
+
# Data transfer packet - might complete a session
|
|
216
|
+
self._handle_transport_data(
|
|
217
|
+
identifier.source_address,
|
|
218
|
+
identifier.pdu_specific
|
|
219
|
+
if self._is_pdu1_format(identifier.pdu_format)
|
|
220
|
+
else 0xFF,
|
|
221
|
+
data,
|
|
222
|
+
timestamp,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
# Decode SPNs if not transport protocol
|
|
226
|
+
decoded_spns: dict[str, Any] = {}
|
|
227
|
+
if not is_transport:
|
|
228
|
+
decoded_spns = self._decode_spns(identifier.pgn, data)
|
|
229
|
+
|
|
230
|
+
# Create message
|
|
231
|
+
msg = J1939Message(
|
|
232
|
+
timestamp=timestamp,
|
|
233
|
+
can_id=can_id,
|
|
234
|
+
identifier=identifier,
|
|
235
|
+
data=data,
|
|
236
|
+
pgn_name=pgn_name,
|
|
237
|
+
decoded_spns=decoded_spns,
|
|
238
|
+
is_transport_protocol=is_transport,
|
|
239
|
+
transport_info=transport_info,
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
self.messages.append(msg)
|
|
243
|
+
return msg
|
|
244
|
+
|
|
245
|
+
def _decode_identifier(self, can_id: int) -> J1939Identifier:
|
|
246
|
+
"""Decode 29-bit J1939 identifier into components.
|
|
247
|
+
|
|
248
|
+
J1939 Identifier Format (bits 28-0):
|
|
249
|
+
- Priority (bits 26-28): 3 bits
|
|
250
|
+
- Reserved (bit 25): 1 bit
|
|
251
|
+
- Data Page (bit 24): 1 bit
|
|
252
|
+
- PDU Format (bits 16-23): 8 bits
|
|
253
|
+
- PDU Specific (bits 8-15): 8 bits (destination or group extension)
|
|
254
|
+
- Source Address (bits 0-7): 8 bits
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
can_id: 29-bit extended CAN identifier.
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
Decoded identifier components.
|
|
261
|
+
|
|
262
|
+
Example:
|
|
263
|
+
>>> analyzer = J1939Analyzer()
|
|
264
|
+
>>> ident = analyzer._decode_identifier(0x18FEF100)
|
|
265
|
+
>>> ident.priority
|
|
266
|
+
6
|
|
267
|
+
>>> ident.pgn
|
|
268
|
+
65265
|
|
269
|
+
"""
|
|
270
|
+
priority = (can_id >> 26) & 0x07
|
|
271
|
+
reserved = (can_id >> 25) & 0x01
|
|
272
|
+
data_page = (can_id >> 24) & 0x01
|
|
273
|
+
pdu_format = (can_id >> 16) & 0xFF
|
|
274
|
+
pdu_specific = (can_id >> 8) & 0xFF
|
|
275
|
+
source_address = can_id & 0xFF
|
|
276
|
+
|
|
277
|
+
# Calculate PGN
|
|
278
|
+
pgn = self._calculate_pgn(pdu_format, pdu_specific, data_page)
|
|
279
|
+
|
|
280
|
+
return J1939Identifier(
|
|
281
|
+
priority=priority,
|
|
282
|
+
reserved=reserved,
|
|
283
|
+
data_page=data_page,
|
|
284
|
+
pdu_format=pdu_format,
|
|
285
|
+
pdu_specific=pdu_specific,
|
|
286
|
+
source_address=source_address,
|
|
287
|
+
pgn=pgn,
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
def _calculate_pgn(self, pdu_format: int, pdu_specific: int, data_page: int) -> int:
|
|
291
|
+
"""Calculate PGN (Parameter Group Number).
|
|
292
|
+
|
|
293
|
+
PGN Calculation:
|
|
294
|
+
- If PDU1 format (PDU Format < 240): PGN = DP | PF | 00
|
|
295
|
+
- If PDU2 format (PDU Format >= 240): PGN = DP | PF | PS
|
|
296
|
+
|
|
297
|
+
Where:
|
|
298
|
+
- DP = Data Page (bit 24)
|
|
299
|
+
- PF = PDU Format (bits 16-23)
|
|
300
|
+
- PS = PDU Specific (bits 8-15)
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
pdu_format: PDU Format byte.
|
|
304
|
+
pdu_specific: PDU Specific byte.
|
|
305
|
+
data_page: Data Page bit.
|
|
306
|
+
|
|
307
|
+
Returns:
|
|
308
|
+
Calculated PGN.
|
|
309
|
+
|
|
310
|
+
Example:
|
|
311
|
+
>>> analyzer = J1939Analyzer()
|
|
312
|
+
>>> analyzer._calculate_pgn(0xFE, 0xF1, 0)
|
|
313
|
+
65265
|
|
314
|
+
"""
|
|
315
|
+
if self._is_pdu1_format(pdu_format):
|
|
316
|
+
# PDU1: PDU Specific is destination address, set to 0 for PGN
|
|
317
|
+
pgn = (data_page << 16) | (pdu_format << 8) | 0x00
|
|
318
|
+
else:
|
|
319
|
+
# PDU2: PDU Specific is group extension
|
|
320
|
+
pgn = (data_page << 16) | (pdu_format << 8) | pdu_specific
|
|
321
|
+
|
|
322
|
+
return pgn
|
|
323
|
+
|
|
324
|
+
def _is_pdu1_format(self, pdu_format: int) -> bool:
|
|
325
|
+
"""Check if PDU1 format (destination-specific).
|
|
326
|
+
|
|
327
|
+
PDU1 format if PDU Format < 240 (0xF0), meaning PDU Specific
|
|
328
|
+
field contains destination address.
|
|
329
|
+
|
|
330
|
+
Args:
|
|
331
|
+
pdu_format: PDU Format byte.
|
|
332
|
+
|
|
333
|
+
Returns:
|
|
334
|
+
True if PDU1 format.
|
|
335
|
+
|
|
336
|
+
Example:
|
|
337
|
+
>>> analyzer = J1939Analyzer()
|
|
338
|
+
>>> analyzer._is_pdu1_format(0xEF)
|
|
339
|
+
True
|
|
340
|
+
>>> analyzer._is_pdu1_format(0xF0)
|
|
341
|
+
False
|
|
342
|
+
"""
|
|
343
|
+
return pdu_format < 240
|
|
344
|
+
|
|
345
|
+
def _parse_transport_protocol(self, pgn: int, data: bytes) -> dict[str, Any] | None:
|
|
346
|
+
"""Parse transport protocol (TP.CM, TP.DT, BAM).
|
|
347
|
+
|
|
348
|
+
TP.CM (PGN 60160) format:
|
|
349
|
+
- Byte 0: Control byte (RTS=16, CTS=17, EOM_ACK=19, BAM=32, ABORT=255)
|
|
350
|
+
- Byte 1-2: Total message size (little-endian)
|
|
351
|
+
- Byte 3: Total packets
|
|
352
|
+
- Byte 4: Max packets (CTS) or Reserved (RTS/BAM)
|
|
353
|
+
- Byte 5-7: PGN of data (little-endian, 3 bytes)
|
|
354
|
+
|
|
355
|
+
TP.DT (PGN 60416) format:
|
|
356
|
+
- Byte 0: Sequence number (1-255)
|
|
357
|
+
- Byte 1-7: Data (7 bytes per packet)
|
|
358
|
+
|
|
359
|
+
Args:
|
|
360
|
+
pgn: Parameter Group Number.
|
|
361
|
+
data: Message data.
|
|
362
|
+
|
|
363
|
+
Returns:
|
|
364
|
+
Transport protocol metadata or None if not transport.
|
|
365
|
+
|
|
366
|
+
Example:
|
|
367
|
+
>>> analyzer = J1939Analyzer()
|
|
368
|
+
>>> # TP.CM RTS
|
|
369
|
+
>>> info = analyzer._parse_transport_protocol(
|
|
370
|
+
... 60160,
|
|
371
|
+
... b'\\x10\\x20\\x00\\x05\\xff\\xf0\\x04\\x00'
|
|
372
|
+
... )
|
|
373
|
+
>>> info['control']
|
|
374
|
+
'RTS'
|
|
375
|
+
>>> info['total_size']
|
|
376
|
+
32
|
|
377
|
+
"""
|
|
378
|
+
if pgn == 60160: # TP.CM
|
|
379
|
+
if len(data) < 8:
|
|
380
|
+
return None
|
|
381
|
+
|
|
382
|
+
control = data[0]
|
|
383
|
+
total_size = int.from_bytes(data[1:3], "little")
|
|
384
|
+
total_packets = data[3]
|
|
385
|
+
max_packets = data[4] if control == self.TP_CM_CTS else 0xFF
|
|
386
|
+
data_pgn = int.from_bytes(data[5:8], "little")
|
|
387
|
+
|
|
388
|
+
cm_types = {
|
|
389
|
+
16: "RTS",
|
|
390
|
+
17: "CTS",
|
|
391
|
+
19: "EOM_ACK",
|
|
392
|
+
32: "BAM",
|
|
393
|
+
255: "ABORT",
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return {
|
|
397
|
+
"type": "TP.CM",
|
|
398
|
+
"control": cm_types.get(control, f"Unknown ({control})"),
|
|
399
|
+
"total_size": total_size,
|
|
400
|
+
"total_packets": total_packets,
|
|
401
|
+
"max_packets": max_packets if control == self.TP_CM_CTS else None,
|
|
402
|
+
"data_pgn": data_pgn,
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
elif pgn == 60416: # TP.DT
|
|
406
|
+
if len(data) < 1:
|
|
407
|
+
return None
|
|
408
|
+
|
|
409
|
+
sequence = data[0]
|
|
410
|
+
packet_data = data[1:]
|
|
411
|
+
|
|
412
|
+
return {
|
|
413
|
+
"type": "TP.DT",
|
|
414
|
+
"sequence": sequence,
|
|
415
|
+
"data": packet_data.hex(),
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return None
|
|
419
|
+
|
|
420
|
+
def _handle_transport_data(
|
|
421
|
+
self, source_address: int, dest_address: int, data: bytes, timestamp: float
|
|
422
|
+
) -> bytes | None:
|
|
423
|
+
"""Handle multi-packet transport protocol data transfer.
|
|
424
|
+
|
|
425
|
+
Reassembles TP.DT packets into complete messages based on TP.CM
|
|
426
|
+
connection management.
|
|
427
|
+
|
|
428
|
+
Args:
|
|
429
|
+
source_address: Source address.
|
|
430
|
+
dest_address: Destination address.
|
|
431
|
+
data: TP.DT packet data.
|
|
432
|
+
timestamp: Packet timestamp.
|
|
433
|
+
|
|
434
|
+
Returns:
|
|
435
|
+
Complete reassembled message if session complete, None otherwise.
|
|
436
|
+
|
|
437
|
+
Example:
|
|
438
|
+
>>> analyzer = J1939Analyzer()
|
|
439
|
+
>>> # Setup session first with TP.CM
|
|
440
|
+
>>> analyzer.parse_message(
|
|
441
|
+
... 0x18ECF100,
|
|
442
|
+
... b'\\x10\\x0e\\x00\\x02\\xff\\xf0\\x04\\x00',
|
|
443
|
+
... 1.0
|
|
444
|
+
... )
|
|
445
|
+
<...>
|
|
446
|
+
>>> # Send TP.DT packets
|
|
447
|
+
>>> analyzer.parse_message(
|
|
448
|
+
... 0x18EBF100,
|
|
449
|
+
... b'\\x01\\x00\\x01\\x02\\x03\\x04\\x05\\x06',
|
|
450
|
+
... 1.1
|
|
451
|
+
... )
|
|
452
|
+
<...>
|
|
453
|
+
"""
|
|
454
|
+
session_key = (source_address, dest_address)
|
|
455
|
+
|
|
456
|
+
# Extract sequence number and packet data
|
|
457
|
+
if len(data) < 1:
|
|
458
|
+
return None
|
|
459
|
+
|
|
460
|
+
sequence = data[0]
|
|
461
|
+
packet_data = data[1:]
|
|
462
|
+
|
|
463
|
+
# Check if session exists
|
|
464
|
+
if session_key not in self.transport_sessions:
|
|
465
|
+
# No active session - ignore
|
|
466
|
+
return None
|
|
467
|
+
|
|
468
|
+
session = self.transport_sessions[session_key]
|
|
469
|
+
|
|
470
|
+
# Append packet data
|
|
471
|
+
if "packets" not in session:
|
|
472
|
+
session["packets"] = {}
|
|
473
|
+
|
|
474
|
+
session["packets"][sequence] = packet_data
|
|
475
|
+
session["last_timestamp"] = timestamp
|
|
476
|
+
|
|
477
|
+
# Check if complete
|
|
478
|
+
expected_packets = session.get("total_packets", 0)
|
|
479
|
+
if len(session["packets"]) == expected_packets:
|
|
480
|
+
# Reassemble in sequence order
|
|
481
|
+
complete_data = b"".join(session["packets"][i] for i in range(1, expected_packets + 1))
|
|
482
|
+
|
|
483
|
+
# Trim to actual size
|
|
484
|
+
total_size = session.get("total_size", len(complete_data))
|
|
485
|
+
complete_data = complete_data[:total_size]
|
|
486
|
+
|
|
487
|
+
# Clean up session
|
|
488
|
+
del self.transport_sessions[session_key]
|
|
489
|
+
|
|
490
|
+
return complete_data
|
|
491
|
+
|
|
492
|
+
return None
|
|
493
|
+
|
|
494
|
+
def _decode_spns(self, pgn: int, data: bytes) -> dict[str, Any]:
|
|
495
|
+
"""Decode Suspect Parameter Numbers from PGN data.
|
|
496
|
+
|
|
497
|
+
Args:
|
|
498
|
+
pgn: Parameter Group Number.
|
|
499
|
+
data: Message data.
|
|
500
|
+
|
|
501
|
+
Returns:
|
|
502
|
+
Dictionary of decoded SPN values.
|
|
503
|
+
|
|
504
|
+
Example:
|
|
505
|
+
>>> analyzer = J1939Analyzer()
|
|
506
|
+
>>> spn = J1939SPN(
|
|
507
|
+
... spn=190,
|
|
508
|
+
... name="Engine Speed",
|
|
509
|
+
... start_bit=24,
|
|
510
|
+
... bit_length=16,
|
|
511
|
+
... resolution=0.125,
|
|
512
|
+
... unit="rpm"
|
|
513
|
+
... )
|
|
514
|
+
>>> analyzer.add_spn_definition(61444, spn)
|
|
515
|
+
>>> # Data with engine speed value
|
|
516
|
+
>>> decoded = analyzer._decode_spns(61444, b'\\x00\\x00\\x00\\x00\\x10\\x00\\x00\\x00')
|
|
517
|
+
>>> decoded.get('Engine Speed')
|
|
518
|
+
2.0
|
|
519
|
+
"""
|
|
520
|
+
decoded: dict[str, Any] = {}
|
|
521
|
+
|
|
522
|
+
if pgn not in self.spn_definitions:
|
|
523
|
+
return decoded
|
|
524
|
+
|
|
525
|
+
# Convert data to bit array
|
|
526
|
+
data_bits = int.from_bytes(data, "little")
|
|
527
|
+
|
|
528
|
+
for spn in self.spn_definitions[pgn]:
|
|
529
|
+
# Extract bits
|
|
530
|
+
mask = (1 << spn.bit_length) - 1
|
|
531
|
+
raw_value = (data_bits >> spn.start_bit) & mask
|
|
532
|
+
|
|
533
|
+
# Apply scaling
|
|
534
|
+
scaled_value = (raw_value * spn.resolution) + spn.offset
|
|
535
|
+
|
|
536
|
+
# Store with name
|
|
537
|
+
decoded[spn.name] = scaled_value
|
|
538
|
+
|
|
539
|
+
return decoded
|
|
540
|
+
|
|
541
|
+
def add_spn_definition(self, pgn: int, spn: J1939SPN) -> None:
|
|
542
|
+
"""Add SPN definition for decoding.
|
|
543
|
+
|
|
544
|
+
Args:
|
|
545
|
+
pgn: Parameter Group Number.
|
|
546
|
+
spn: SPN definition.
|
|
547
|
+
|
|
548
|
+
Example:
|
|
549
|
+
>>> analyzer = J1939Analyzer()
|
|
550
|
+
>>> spn = J1939SPN(
|
|
551
|
+
... spn=190,
|
|
552
|
+
... name="Engine Speed",
|
|
553
|
+
... start_bit=24,
|
|
554
|
+
... bit_length=16,
|
|
555
|
+
... resolution=0.125,
|
|
556
|
+
... offset=0.0,
|
|
557
|
+
... unit="rpm",
|
|
558
|
+
... data_range=(0.0, 8031.875)
|
|
559
|
+
... )
|
|
560
|
+
>>> analyzer.add_spn_definition(61444, spn)
|
|
561
|
+
"""
|
|
562
|
+
if pgn not in self.spn_definitions:
|
|
563
|
+
self.spn_definitions[pgn] = []
|
|
564
|
+
|
|
565
|
+
self.spn_definitions[pgn].append(spn)
|
|
566
|
+
|
|
567
|
+
def export_messages(self, output_path: Path) -> None:
|
|
568
|
+
"""Export parsed messages as JSON.
|
|
569
|
+
|
|
570
|
+
Args:
|
|
571
|
+
output_path: Path to output JSON file.
|
|
572
|
+
|
|
573
|
+
Example:
|
|
574
|
+
>>> analyzer = J1939Analyzer()
|
|
575
|
+
>>> analyzer.parse_message(0x18FEF100, b'\\x00\\x01\\x02\\x03')
|
|
576
|
+
<...>
|
|
577
|
+
>>> analyzer.export_messages(Path("j1939_messages.json"))
|
|
578
|
+
"""
|
|
579
|
+
messages_data = []
|
|
580
|
+
for msg in self.messages:
|
|
581
|
+
msg_dict = {
|
|
582
|
+
"timestamp": msg.timestamp,
|
|
583
|
+
"can_id": f"0x{msg.can_id:08X}",
|
|
584
|
+
"pgn": msg.identifier.pgn,
|
|
585
|
+
"pgn_name": msg.pgn_name,
|
|
586
|
+
"priority": msg.identifier.priority,
|
|
587
|
+
"source_address": msg.identifier.source_address,
|
|
588
|
+
"destination_address": (
|
|
589
|
+
msg.identifier.pdu_specific
|
|
590
|
+
if self._is_pdu1_format(msg.identifier.pdu_format)
|
|
591
|
+
else 0xFF
|
|
592
|
+
),
|
|
593
|
+
"data": msg.data.hex(),
|
|
594
|
+
"decoded_spns": msg.decoded_spns,
|
|
595
|
+
"is_transport_protocol": msg.is_transport_protocol,
|
|
596
|
+
"transport_info": msg.transport_info,
|
|
597
|
+
}
|
|
598
|
+
messages_data.append(msg_dict)
|
|
599
|
+
|
|
600
|
+
with output_path.open("w") as f:
|
|
601
|
+
json.dump(
|
|
602
|
+
{"messages": messages_data, "total_messages": len(messages_data)},
|
|
603
|
+
f,
|
|
604
|
+
indent=2,
|
|
605
|
+
)
|