oscura 0.5.0__py3-none-any.whl → 0.6.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/__init__.py +0 -48
- oscura/analyzers/digital/edges.py +325 -65
- oscura/analyzers/digital/extraction.py +0 -195
- 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/__init__.py +1 -22
- 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 +2763 -0
- 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/core/schemas/bus_configuration.json +322 -0
- oscura/core/schemas/device_mapping.json +182 -0
- oscura/core/schemas/packet_format.json +418 -0
- oscura/core/schemas/protocol_definition.json +363 -0
- 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 -20
- oscura/export/kaitai_struct.py +513 -0
- oscura/export/scapy_layer.py +801 -0
- oscura/export/wireshark/README.md +15 -15
- 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/README.md +7 -7
- 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 +171 -63
- 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/vcd.py +215 -117
- oscura/loaders/wav.py +155 -68
- oscura/reporting/__init__.py +9 -7
- 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/reporting/templates/index.md +13 -13
- 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/autodetect.py +1 -5
- 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 +11 -3
- 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.6.0.dist-info/METADATA +643 -0
- oscura-0.6.0.dist-info/RECORD +590 -0
- oscura/analyzers/digital/ic_database.py +0 -498
- oscura/analyzers/digital/timing_paths.py +0 -339
- oscura/analyzers/digital/vintage.py +0 -377
- oscura/analyzers/digital/vintage_result.py +0 -148
- oscura/analyzers/protocols/parallel_bus.py +0 -449
- 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/export/wavedrom.py +0 -430
- 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 -338
- 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/exporters/vintage_logic_csv.py +0 -247
- oscura/reporting/vintage_logic_report.py +0 -523
- 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/visualization/digital_advanced.py +0 -718
- oscura/visualization/figure_manager.py +0 -156
- oscura/workflow/__init__.py +0 -13
- oscura-0.5.0.dist-info/METADATA +0 -407
- oscura-0.5.0.dist-info/RECORD +0 -486
- /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/{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.0.dist-info → oscura-0.6.0.dist-info}/WHEEL +0 -0
- {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/entry_points.txt +0 -0
- {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,612 @@
|
|
|
1
|
+
"""LIN protocol analyzer with enhanced checksum and LDF generation.
|
|
2
|
+
|
|
3
|
+
This module provides comprehensive LIN 2.x protocol analysis including
|
|
4
|
+
protected ID calculation, enhanced checksum validation, diagnostic frame
|
|
5
|
+
parsing, and LDF (LIN Description File) generation from captured traffic.
|
|
6
|
+
|
|
7
|
+
References:
|
|
8
|
+
LIN Specification 2.2A
|
|
9
|
+
ISO 17987 (LIN standard)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any, ClassVar
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"LINAnalyzer",
|
|
20
|
+
"LINFrame",
|
|
21
|
+
"LINScheduleEntry",
|
|
22
|
+
"LINSignal",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class LINFrame:
|
|
28
|
+
"""LIN frame representation.
|
|
29
|
+
|
|
30
|
+
Attributes:
|
|
31
|
+
timestamp: Frame timestamp in seconds.
|
|
32
|
+
frame_id: Frame ID (0-63, 6-bit).
|
|
33
|
+
data: Frame data bytes (1-8 bytes).
|
|
34
|
+
checksum: Received checksum byte.
|
|
35
|
+
checksum_valid: True if checksum is valid.
|
|
36
|
+
checksum_type: Checksum type ("classic" or "enhanced").
|
|
37
|
+
parity_bits: 2-bit parity from protected ID.
|
|
38
|
+
is_diagnostic: True if diagnostic frame (0x3C or 0x3D).
|
|
39
|
+
decoded_signals: Decoded signal values.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
timestamp: float
|
|
43
|
+
frame_id: int
|
|
44
|
+
data: bytes
|
|
45
|
+
checksum: int
|
|
46
|
+
checksum_valid: bool
|
|
47
|
+
checksum_type: str = "enhanced"
|
|
48
|
+
parity_bits: int = 0
|
|
49
|
+
is_diagnostic: bool = False
|
|
50
|
+
decoded_signals: dict[str, Any] = field(default_factory=dict)
|
|
51
|
+
|
|
52
|
+
def __repr__(self) -> str:
|
|
53
|
+
"""Human-readable representation."""
|
|
54
|
+
status = "✓" if self.checksum_valid else "✗"
|
|
55
|
+
return (
|
|
56
|
+
f"LINFrame(ID=0x{self.frame_id:02X}, t={self.timestamp:.6f}s, "
|
|
57
|
+
f"data={self.data.hex().upper()}, checksum={status} {self.checksum_type})"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class LINSignal:
|
|
63
|
+
"""LIN signal definition.
|
|
64
|
+
|
|
65
|
+
Attributes:
|
|
66
|
+
name: Signal name.
|
|
67
|
+
frame_id: Frame ID containing this signal (0-63).
|
|
68
|
+
start_bit: Starting bit position (0-63 within 8 bytes).
|
|
69
|
+
bit_length: Signal length in bits (1-64).
|
|
70
|
+
init_value: Initial/default value.
|
|
71
|
+
publisher: Node publishing this signal ("Master" or slave name).
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
name: str
|
|
75
|
+
frame_id: int
|
|
76
|
+
start_bit: int
|
|
77
|
+
bit_length: int
|
|
78
|
+
init_value: int = 0
|
|
79
|
+
publisher: str = "Master"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass
|
|
83
|
+
class LINScheduleEntry:
|
|
84
|
+
"""LIN schedule table entry.
|
|
85
|
+
|
|
86
|
+
Attributes:
|
|
87
|
+
frame_id: Frame ID to transmit.
|
|
88
|
+
delay_ms: Delay before next frame in milliseconds.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
frame_id: int
|
|
92
|
+
delay_ms: float
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class LINAnalyzer:
|
|
96
|
+
"""Enhanced LIN protocol analyzer with LDF generation.
|
|
97
|
+
|
|
98
|
+
Supports comprehensive LIN 2.x protocol analysis including:
|
|
99
|
+
- Protected ID calculation with parity bits
|
|
100
|
+
- Classic and enhanced checksum validation
|
|
101
|
+
- Diagnostic frame parsing (0x3C master request, 0x3D slave response)
|
|
102
|
+
- Signal decoding with bit-level extraction
|
|
103
|
+
- Schedule table inference from frame timing
|
|
104
|
+
- LDF (LIN Description File) generation
|
|
105
|
+
|
|
106
|
+
Example:
|
|
107
|
+
>>> analyzer = LINAnalyzer()
|
|
108
|
+
>>> # Parse LIN frame with enhanced checksum
|
|
109
|
+
>>> frame = analyzer.parse_frame(
|
|
110
|
+
... data=b'\\x55\\x80\\x01\\x02\\x03\\xFA',
|
|
111
|
+
... timestamp=1.0
|
|
112
|
+
... )
|
|
113
|
+
>>> print(f"Frame ID: {frame.frame_id}")
|
|
114
|
+
Frame ID: 0
|
|
115
|
+
>>> # Add signal definition
|
|
116
|
+
>>> analyzer.add_signal(LINSignal(
|
|
117
|
+
... name="Speed",
|
|
118
|
+
... frame_id=0,
|
|
119
|
+
... start_bit=0,
|
|
120
|
+
... bit_length=16,
|
|
121
|
+
... publisher="Master"
|
|
122
|
+
... ))
|
|
123
|
+
>>> # Generate LDF from captured traffic
|
|
124
|
+
>>> analyzer.generate_ldf(Path("output.ldf"), baudrate=19200)
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
# Diagnostic frame IDs per LIN 2.x specification
|
|
128
|
+
MASTER_REQUEST_FRAME: ClassVar[int] = 0x3C # 60
|
|
129
|
+
SLAVE_RESPONSE_FRAME: ClassVar[int] = 0x3D # 61
|
|
130
|
+
|
|
131
|
+
# Diagnostic services (subset of UDS adapted for LIN)
|
|
132
|
+
DIAGNOSTIC_SERVICES: ClassVar[dict[int, str]] = {
|
|
133
|
+
0xB0: "AssignFrameIdRange",
|
|
134
|
+
0xB1: "AssignNAD",
|
|
135
|
+
0xB2: "ConditionalChangeNAD",
|
|
136
|
+
0xB3: "DataDump",
|
|
137
|
+
0xB4: "SaveConfiguration",
|
|
138
|
+
0xB5: "AssignFrameId",
|
|
139
|
+
0xB6: "ReadById",
|
|
140
|
+
0xB7: "TargetedReset",
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
def __init__(self) -> None:
|
|
144
|
+
"""Initialize LIN analyzer."""
|
|
145
|
+
self.frames: list[LINFrame] = []
|
|
146
|
+
self.signals: list[LINSignal] = []
|
|
147
|
+
self.schedule: list[LINScheduleEntry] = []
|
|
148
|
+
self.detected_frame_ids: set[int] = set()
|
|
149
|
+
|
|
150
|
+
def parse_frame(
|
|
151
|
+
self,
|
|
152
|
+
data: bytes,
|
|
153
|
+
timestamp: float = 0.0,
|
|
154
|
+
checksum_type: str = "enhanced",
|
|
155
|
+
) -> LINFrame:
|
|
156
|
+
"""Parse LIN frame including sync, protected ID, data, and checksum.
|
|
157
|
+
|
|
158
|
+
LIN Frame Format:
|
|
159
|
+
- Break field (dominant, >= 13 bit times) - not in data
|
|
160
|
+
- Sync field (0x55) - first byte
|
|
161
|
+
- Protected ID (frame ID + parity bits) - second byte
|
|
162
|
+
- Data field (1-8 bytes) - variable
|
|
163
|
+
- Checksum field (1 byte) - last byte
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
data: Raw frame bytes including sync, protected ID, data, checksum.
|
|
167
|
+
timestamp: Frame timestamp in seconds.
|
|
168
|
+
checksum_type: Checksum type ("classic" or "enhanced").
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
Parsed LINFrame object.
|
|
172
|
+
|
|
173
|
+
Raises:
|
|
174
|
+
ValueError: If frame is invalid or too short.
|
|
175
|
+
|
|
176
|
+
Example:
|
|
177
|
+
>>> analyzer = LINAnalyzer()
|
|
178
|
+
>>> # Frame: sync=0x55, protected_id=0x80 (ID=0), data=0x01,0x02, checksum=0xFA
|
|
179
|
+
>>> frame = analyzer.parse_frame(
|
|
180
|
+
... data=b'\\x55\\x80\\x01\\x02\\xFA',
|
|
181
|
+
... timestamp=1.0,
|
|
182
|
+
... checksum_type="enhanced"
|
|
183
|
+
... )
|
|
184
|
+
>>> print(f"Frame ID: {frame.frame_id}, Valid: {frame.checksum_valid}")
|
|
185
|
+
Frame ID: 0, Valid: True
|
|
186
|
+
"""
|
|
187
|
+
if len(data) < 4: # Minimum: sync + protected_id + 1 data byte + checksum
|
|
188
|
+
raise ValueError(f"LIN frame too short: {len(data)} bytes (minimum 4)")
|
|
189
|
+
|
|
190
|
+
# Parse sync byte (should be 0x55)
|
|
191
|
+
sync_byte = data[0]
|
|
192
|
+
if sync_byte != 0x55:
|
|
193
|
+
raise ValueError(f"Invalid sync byte: 0x{sync_byte:02X} (expected 0x55)")
|
|
194
|
+
|
|
195
|
+
# Parse protected ID
|
|
196
|
+
protected_id = data[1]
|
|
197
|
+
frame_id = protected_id & 0x3F # Lower 6 bits
|
|
198
|
+
parity_bits = (protected_id >> 6) & 0x03 # Upper 2 bits
|
|
199
|
+
|
|
200
|
+
# Validate protected ID parity
|
|
201
|
+
expected_protected_id = self._calculate_protected_id(frame_id)
|
|
202
|
+
if protected_id != expected_protected_id:
|
|
203
|
+
raise ValueError(
|
|
204
|
+
f"Invalid protected ID parity: 0x{protected_id:02X} "
|
|
205
|
+
f"(expected 0x{expected_protected_id:02X} for frame ID {frame_id})"
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
# Parse data and checksum
|
|
209
|
+
frame_data = data[2:-1]
|
|
210
|
+
received_checksum = data[-1]
|
|
211
|
+
|
|
212
|
+
# Calculate and validate checksum
|
|
213
|
+
if checksum_type == "classic":
|
|
214
|
+
expected_checksum = self._calculate_classic_checksum(frame_data)
|
|
215
|
+
else: # enhanced
|
|
216
|
+
expected_checksum = self._calculate_enhanced_checksum(protected_id, frame_data)
|
|
217
|
+
|
|
218
|
+
checksum_valid = received_checksum == expected_checksum
|
|
219
|
+
|
|
220
|
+
# Check if diagnostic frame
|
|
221
|
+
is_diagnostic = frame_id in (self.MASTER_REQUEST_FRAME, self.SLAVE_RESPONSE_FRAME)
|
|
222
|
+
|
|
223
|
+
# Parse diagnostic frame if applicable
|
|
224
|
+
decoded_signals: dict[str, Any] = {}
|
|
225
|
+
if is_diagnostic:
|
|
226
|
+
decoded_signals = self._parse_diagnostic_frame(frame_id, frame_data)
|
|
227
|
+
else:
|
|
228
|
+
# Decode regular signals
|
|
229
|
+
decoded_signals = self.decode_signals(frame_id, frame_data)
|
|
230
|
+
|
|
231
|
+
frame = LINFrame(
|
|
232
|
+
timestamp=timestamp,
|
|
233
|
+
frame_id=frame_id,
|
|
234
|
+
data=frame_data,
|
|
235
|
+
checksum=received_checksum,
|
|
236
|
+
checksum_valid=checksum_valid,
|
|
237
|
+
checksum_type=checksum_type,
|
|
238
|
+
parity_bits=parity_bits,
|
|
239
|
+
is_diagnostic=is_diagnostic,
|
|
240
|
+
decoded_signals=decoded_signals,
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
self.frames.append(frame)
|
|
244
|
+
self.detected_frame_ids.add(frame_id)
|
|
245
|
+
|
|
246
|
+
return frame
|
|
247
|
+
|
|
248
|
+
def _calculate_protected_id(self, frame_id: int) -> int:
|
|
249
|
+
"""Calculate protected ID with parity bits.
|
|
250
|
+
|
|
251
|
+
Protected ID Format (8 bits):
|
|
252
|
+
- Bits 0-5: Frame ID (6 bits, range 0-63)
|
|
253
|
+
- Bit 6 (P0): ID0 XOR ID1 XOR ID2 XOR ID4
|
|
254
|
+
- Bit 7 (P1): NOT(ID1 XOR ID3 XOR ID4 XOR ID5)
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
frame_id: Frame ID (0-63).
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
Protected ID with parity bits (8 bits).
|
|
261
|
+
|
|
262
|
+
Raises:
|
|
263
|
+
ValueError: If frame ID exceeds 63.
|
|
264
|
+
|
|
265
|
+
Example:
|
|
266
|
+
>>> analyzer = LINAnalyzer()
|
|
267
|
+
>>> protected_id = analyzer._calculate_protected_id(0)
|
|
268
|
+
>>> print(f"Protected ID: 0x{protected_id:02X}")
|
|
269
|
+
Protected ID: 0x80
|
|
270
|
+
"""
|
|
271
|
+
if frame_id > 0x3F:
|
|
272
|
+
raise ValueError(f"Frame ID {frame_id} exceeds 6 bits (max 63)")
|
|
273
|
+
|
|
274
|
+
# Extract individual ID bits
|
|
275
|
+
id0 = (frame_id >> 0) & 1
|
|
276
|
+
id1 = (frame_id >> 1) & 1
|
|
277
|
+
id2 = (frame_id >> 2) & 1
|
|
278
|
+
id3 = (frame_id >> 3) & 1
|
|
279
|
+
id4 = (frame_id >> 4) & 1
|
|
280
|
+
id5 = (frame_id >> 5) & 1
|
|
281
|
+
|
|
282
|
+
# Calculate parity bits
|
|
283
|
+
p0 = id0 ^ id1 ^ id2 ^ id4
|
|
284
|
+
p1 = (id1 ^ id3 ^ id4 ^ id5) ^ 1 # Inverted
|
|
285
|
+
|
|
286
|
+
# Construct protected ID
|
|
287
|
+
protected_id = frame_id | (p0 << 6) | (p1 << 7)
|
|
288
|
+
|
|
289
|
+
return protected_id
|
|
290
|
+
|
|
291
|
+
def _calculate_classic_checksum(self, data: bytes) -> int:
|
|
292
|
+
"""Calculate classic checksum (LIN 1.x).
|
|
293
|
+
|
|
294
|
+
Classic checksum = inverted modulo-256 sum of data bytes only.
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
data: Frame data bytes.
|
|
298
|
+
|
|
299
|
+
Returns:
|
|
300
|
+
Classic checksum byte.
|
|
301
|
+
|
|
302
|
+
Example:
|
|
303
|
+
>>> analyzer = LINAnalyzer()
|
|
304
|
+
>>> checksum = analyzer._calculate_classic_checksum(b'\\x01\\x02\\x03')
|
|
305
|
+
>>> print(f"Checksum: 0x{checksum:02X}")
|
|
306
|
+
Checksum: 0xF9
|
|
307
|
+
"""
|
|
308
|
+
checksum_sum = sum(data)
|
|
309
|
+
|
|
310
|
+
# Handle carry (modulo-256 with carry propagation)
|
|
311
|
+
while checksum_sum > 255:
|
|
312
|
+
checksum_sum = (checksum_sum & 0xFF) + (checksum_sum >> 8)
|
|
313
|
+
|
|
314
|
+
checksum = (~checksum_sum) & 0xFF
|
|
315
|
+
|
|
316
|
+
return checksum
|
|
317
|
+
|
|
318
|
+
def _calculate_enhanced_checksum(self, protected_id: int, data: bytes) -> int:
|
|
319
|
+
"""Calculate enhanced checksum (LIN 2.x).
|
|
320
|
+
|
|
321
|
+
Enhanced checksum = inverted modulo-256 sum of protected ID + data bytes.
|
|
322
|
+
|
|
323
|
+
Args:
|
|
324
|
+
protected_id: Protected ID byte (frame ID with parity).
|
|
325
|
+
data: Frame data bytes.
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
Enhanced checksum byte.
|
|
329
|
+
|
|
330
|
+
Example:
|
|
331
|
+
>>> analyzer = LINAnalyzer()
|
|
332
|
+
>>> checksum = analyzer._calculate_enhanced_checksum(0x80, b'\\x01\\x02\\x03')
|
|
333
|
+
>>> print(f"Checksum: 0x{checksum:02X}")
|
|
334
|
+
Checksum: 0x79
|
|
335
|
+
"""
|
|
336
|
+
checksum_sum = protected_id
|
|
337
|
+
|
|
338
|
+
for byte in data:
|
|
339
|
+
checksum_sum += byte
|
|
340
|
+
# Handle carry (modulo-256 with carry propagation)
|
|
341
|
+
if checksum_sum > 255:
|
|
342
|
+
checksum_sum = (checksum_sum & 0xFF) + 1
|
|
343
|
+
|
|
344
|
+
checksum = (~checksum_sum) & 0xFF
|
|
345
|
+
|
|
346
|
+
return checksum
|
|
347
|
+
|
|
348
|
+
def _parse_diagnostic_frame(self, frame_id: int, data: bytes) -> dict[str, Any]:
|
|
349
|
+
"""Parse diagnostic frame (Master Request 0x3C or Slave Response 0x3D).
|
|
350
|
+
|
|
351
|
+
Diagnostic Frame Format:
|
|
352
|
+
- NAD (Node Address for Diagnostics) - 1 byte
|
|
353
|
+
- PCI (Protocol Control Information) - 1 byte
|
|
354
|
+
- SID (Service Identifier) - 1 byte
|
|
355
|
+
- Service data - variable (up to 5 bytes)
|
|
356
|
+
|
|
357
|
+
Args:
|
|
358
|
+
frame_id: Frame ID (0x3C or 0x3D).
|
|
359
|
+
data: Frame data bytes.
|
|
360
|
+
|
|
361
|
+
Returns:
|
|
362
|
+
Dictionary of decoded diagnostic fields.
|
|
363
|
+
|
|
364
|
+
Example:
|
|
365
|
+
>>> analyzer = LINAnalyzer()
|
|
366
|
+
>>> decoded = analyzer._parse_diagnostic_frame(
|
|
367
|
+
... 0x3C,
|
|
368
|
+
... b'\\x01\\x06\\xB6\\x00\\x01\\x00\\x00\\x00'
|
|
369
|
+
... )
|
|
370
|
+
>>> print(decoded["service_name"])
|
|
371
|
+
ReadById
|
|
372
|
+
"""
|
|
373
|
+
if len(data) < 3:
|
|
374
|
+
return {"error": "Diagnostic frame too short"}
|
|
375
|
+
|
|
376
|
+
nad = data[0] # Node address
|
|
377
|
+
pci = data[1] # Protocol control info (single frame = 0x06)
|
|
378
|
+
sid = data[2] # Service ID
|
|
379
|
+
|
|
380
|
+
service_name = self.DIAGNOSTIC_SERVICES.get(sid, f"Unknown (0x{sid:02X})")
|
|
381
|
+
service_data = data[3:]
|
|
382
|
+
|
|
383
|
+
result: dict[str, Any] = {
|
|
384
|
+
"nad": nad,
|
|
385
|
+
"pci": pci,
|
|
386
|
+
"service_id": sid,
|
|
387
|
+
"service_name": service_name,
|
|
388
|
+
"service_data": service_data.hex().upper(),
|
|
389
|
+
"frame_type": "MasterRequest"
|
|
390
|
+
if frame_id == self.MASTER_REQUEST_FRAME
|
|
391
|
+
else "SlaveResponse",
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
# Decode specific services
|
|
395
|
+
if sid == 0xB6 and len(service_data) >= 2: # ReadById
|
|
396
|
+
identifier = int.from_bytes(service_data[0:2], "big")
|
|
397
|
+
result["identifier"] = identifier
|
|
398
|
+
|
|
399
|
+
return result
|
|
400
|
+
|
|
401
|
+
def add_signal(self, signal: LINSignal) -> None:
|
|
402
|
+
"""Add signal definition for decoding.
|
|
403
|
+
|
|
404
|
+
Args:
|
|
405
|
+
signal: Signal definition to add.
|
|
406
|
+
|
|
407
|
+
Example:
|
|
408
|
+
>>> analyzer = LINAnalyzer()
|
|
409
|
+
>>> analyzer.add_signal(LINSignal(
|
|
410
|
+
... name="EngineSpeed",
|
|
411
|
+
... frame_id=0x10,
|
|
412
|
+
... start_bit=0,
|
|
413
|
+
... bit_length=16,
|
|
414
|
+
... publisher="Master"
|
|
415
|
+
... ))
|
|
416
|
+
"""
|
|
417
|
+
self.signals.append(signal)
|
|
418
|
+
|
|
419
|
+
def decode_signals(self, frame_id: int, data: bytes) -> dict[str, Any]:
|
|
420
|
+
"""Decode signals from frame data.
|
|
421
|
+
|
|
422
|
+
Args:
|
|
423
|
+
frame_id: Frame ID.
|
|
424
|
+
data: Frame data bytes.
|
|
425
|
+
|
|
426
|
+
Returns:
|
|
427
|
+
Dictionary mapping signal names to decoded values.
|
|
428
|
+
|
|
429
|
+
Example:
|
|
430
|
+
>>> analyzer = LINAnalyzer()
|
|
431
|
+
>>> analyzer.add_signal(LINSignal("Speed", frame_id=0, start_bit=0, bit_length=16))
|
|
432
|
+
>>> decoded = analyzer.decode_signals(0, b'\\x10\\x27')
|
|
433
|
+
>>> print(f"Speed: {decoded['Speed']}")
|
|
434
|
+
Speed: 10000
|
|
435
|
+
"""
|
|
436
|
+
decoded: dict[str, Any] = {}
|
|
437
|
+
|
|
438
|
+
# Find signals for this frame
|
|
439
|
+
frame_signals = [s for s in self.signals if s.frame_id == frame_id]
|
|
440
|
+
|
|
441
|
+
# Convert data to bit array
|
|
442
|
+
if len(data) == 0:
|
|
443
|
+
return decoded
|
|
444
|
+
|
|
445
|
+
data_bits = int.from_bytes(data, "little")
|
|
446
|
+
|
|
447
|
+
for signal in frame_signals:
|
|
448
|
+
# Extract signal bits
|
|
449
|
+
mask = (1 << signal.bit_length) - 1
|
|
450
|
+
value = (data_bits >> signal.start_bit) & mask
|
|
451
|
+
decoded[signal.name] = value
|
|
452
|
+
|
|
453
|
+
return decoded
|
|
454
|
+
|
|
455
|
+
def infer_schedule_table(self) -> list[LINScheduleEntry]:
|
|
456
|
+
"""Infer schedule table from captured frame timing.
|
|
457
|
+
|
|
458
|
+
Analyzes frame timestamps to determine typical transmission schedule.
|
|
459
|
+
|
|
460
|
+
Returns:
|
|
461
|
+
List of schedule entries ordered by typical transmission sequence.
|
|
462
|
+
|
|
463
|
+
Example:
|
|
464
|
+
>>> analyzer = LINAnalyzer()
|
|
465
|
+
>>> # ... parse frames ...
|
|
466
|
+
>>> schedule = analyzer.infer_schedule_table()
|
|
467
|
+
>>> for entry in schedule:
|
|
468
|
+
... print(f"Frame 0x{entry.frame_id:02X} delay {entry.delay_ms:.1f}ms")
|
|
469
|
+
"""
|
|
470
|
+
if len(self.frames) < 2:
|
|
471
|
+
return []
|
|
472
|
+
|
|
473
|
+
# Group frames by ID and calculate average inter-frame delays
|
|
474
|
+
frame_delays: dict[int, list[float]] = {}
|
|
475
|
+
|
|
476
|
+
for i in range(1, len(self.frames)):
|
|
477
|
+
prev_frame = self.frames[i - 1]
|
|
478
|
+
curr_frame = self.frames[i]
|
|
479
|
+
|
|
480
|
+
# Calculate delay in milliseconds
|
|
481
|
+
delay_ms = (curr_frame.timestamp - prev_frame.timestamp) * 1000.0
|
|
482
|
+
|
|
483
|
+
# Group by current frame ID
|
|
484
|
+
if curr_frame.frame_id not in frame_delays:
|
|
485
|
+
frame_delays[curr_frame.frame_id] = []
|
|
486
|
+
frame_delays[curr_frame.frame_id].append(delay_ms)
|
|
487
|
+
|
|
488
|
+
# Calculate average delay for each frame ID
|
|
489
|
+
schedule_entries = []
|
|
490
|
+
for frame_id in sorted(self.detected_frame_ids):
|
|
491
|
+
if frame_id in frame_delays and len(frame_delays[frame_id]) > 0:
|
|
492
|
+
avg_delay = sum(frame_delays[frame_id]) / len(frame_delays[frame_id])
|
|
493
|
+
schedule_entries.append(LINScheduleEntry(frame_id=frame_id, delay_ms=avg_delay))
|
|
494
|
+
|
|
495
|
+
self.schedule = schedule_entries
|
|
496
|
+
return schedule_entries
|
|
497
|
+
|
|
498
|
+
def generate_ldf(self, output_path: Path, baudrate: int = 19200) -> None:
|
|
499
|
+
"""Generate LDF (LIN Description File) from captured traffic.
|
|
500
|
+
|
|
501
|
+
LDF Format per LIN 2.x specification:
|
|
502
|
+
- Header (protocol version, language version, speed)
|
|
503
|
+
- Nodes (master and slaves)
|
|
504
|
+
- Signals (name, size, init value, publisher)
|
|
505
|
+
- Frames (ID, publisher, size, signals)
|
|
506
|
+
- Schedule tables (frame sequence and timing)
|
|
507
|
+
|
|
508
|
+
Args:
|
|
509
|
+
output_path: Path to output LDF file.
|
|
510
|
+
baudrate: LIN bus baudrate in bps (default 19200).
|
|
511
|
+
|
|
512
|
+
Raises:
|
|
513
|
+
ValueError: If no frames have been captured.
|
|
514
|
+
|
|
515
|
+
Example:
|
|
516
|
+
>>> analyzer = LINAnalyzer()
|
|
517
|
+
>>> # ... parse frames and add signals ...
|
|
518
|
+
>>> analyzer.generate_ldf(Path("vehicle.ldf"), baudrate=19200)
|
|
519
|
+
"""
|
|
520
|
+
if len(self.frames) == 0:
|
|
521
|
+
raise ValueError("No frames captured - cannot generate LDF")
|
|
522
|
+
|
|
523
|
+
ldf_lines: list[str] = []
|
|
524
|
+
self._add_ldf_header(ldf_lines, baudrate)
|
|
525
|
+
self._add_ldf_nodes(ldf_lines)
|
|
526
|
+
self._add_ldf_signals(ldf_lines)
|
|
527
|
+
self._add_ldf_frames(ldf_lines)
|
|
528
|
+
self._add_ldf_schedule(ldf_lines)
|
|
529
|
+
|
|
530
|
+
output_path.write_text("\n".join(ldf_lines) + "\n", encoding="utf-8")
|
|
531
|
+
|
|
532
|
+
def _add_ldf_header(self, ldf_lines: list[str], baudrate: int) -> None:
|
|
533
|
+
"""Add LDF header section."""
|
|
534
|
+
ldf_lines.append("LIN_description_file;")
|
|
535
|
+
ldf_lines.append('LIN_protocol_version = "2.1";')
|
|
536
|
+
ldf_lines.append('LIN_language_version = "2.1";')
|
|
537
|
+
ldf_lines.append(f"LIN_speed = {baudrate / 1000:.1f} kbps;")
|
|
538
|
+
ldf_lines.append("")
|
|
539
|
+
|
|
540
|
+
def _add_ldf_nodes(self, ldf_lines: list[str]) -> None:
|
|
541
|
+
"""Add LDF nodes section (master and slaves)."""
|
|
542
|
+
ldf_lines.append("Nodes {")
|
|
543
|
+
ldf_lines.append(" Master: Master, 5 ms, 0.1 ms;")
|
|
544
|
+
|
|
545
|
+
slaves = sorted({s.publisher for s in self.signals if s.publisher != "Master"})
|
|
546
|
+
if len(slaves) > 0:
|
|
547
|
+
ldf_lines.append(f" Slaves: {', '.join(slaves)};")
|
|
548
|
+
ldf_lines.append("}")
|
|
549
|
+
ldf_lines.append("")
|
|
550
|
+
|
|
551
|
+
def _add_ldf_signals(self, ldf_lines: list[str]) -> None:
|
|
552
|
+
"""Add LDF signals section."""
|
|
553
|
+
if len(self.signals) == 0:
|
|
554
|
+
return
|
|
555
|
+
|
|
556
|
+
ldf_lines.append("Signals {")
|
|
557
|
+
for signal in self.signals:
|
|
558
|
+
ldf_lines.append(
|
|
559
|
+
f" {signal.name}: {signal.bit_length}, {signal.init_value}, {signal.publisher};"
|
|
560
|
+
)
|
|
561
|
+
ldf_lines.append("}")
|
|
562
|
+
ldf_lines.append("")
|
|
563
|
+
|
|
564
|
+
def _add_ldf_frames(self, ldf_lines: list[str]) -> None:
|
|
565
|
+
"""Add LDF frames section."""
|
|
566
|
+
ldf_lines.append("Frames {")
|
|
567
|
+
for frame_id in sorted(self.detected_frame_ids):
|
|
568
|
+
if frame_id in (self.MASTER_REQUEST_FRAME, self.SLAVE_RESPONSE_FRAME):
|
|
569
|
+
continue
|
|
570
|
+
|
|
571
|
+
frame_signals = [s for s in self.signals if s.frame_id == frame_id]
|
|
572
|
+
publisher, dlc = self._determine_frame_metadata(frame_id, frame_signals)
|
|
573
|
+
|
|
574
|
+
ldf_lines.append(f" Frame_{frame_id:02X}: {frame_id}, {publisher}, {dlc} {{")
|
|
575
|
+
if len(frame_signals) > 0:
|
|
576
|
+
for signal in frame_signals:
|
|
577
|
+
ldf_lines.append(f" {signal.name}, {signal.start_bit};")
|
|
578
|
+
ldf_lines.append(" }")
|
|
579
|
+
|
|
580
|
+
ldf_lines.append("}")
|
|
581
|
+
ldf_lines.append("")
|
|
582
|
+
|
|
583
|
+
def _determine_frame_metadata(
|
|
584
|
+
self, frame_id: int, frame_signals: list[LINSignal]
|
|
585
|
+
) -> tuple[str, int]:
|
|
586
|
+
"""Determine frame publisher and data length."""
|
|
587
|
+
if len(frame_signals) > 0:
|
|
588
|
+
publisher = frame_signals[0].publisher
|
|
589
|
+
max_bit = max((s.start_bit + s.bit_length) for s in frame_signals)
|
|
590
|
+
dlc = (max_bit + 7) // 8
|
|
591
|
+
else:
|
|
592
|
+
frame_data_lengths = [len(f.data) for f in self.frames if f.frame_id == frame_id]
|
|
593
|
+
dlc = max(frame_data_lengths) if frame_data_lengths else 1
|
|
594
|
+
publisher = "Master"
|
|
595
|
+
return publisher, dlc
|
|
596
|
+
|
|
597
|
+
def _add_ldf_schedule(self, ldf_lines: list[str]) -> None:
|
|
598
|
+
"""Add LDF schedule tables section."""
|
|
599
|
+
if len(self.schedule) == 0:
|
|
600
|
+
self.infer_schedule_table()
|
|
601
|
+
|
|
602
|
+
if len(self.schedule) == 0:
|
|
603
|
+
return
|
|
604
|
+
|
|
605
|
+
ldf_lines.append("Schedule_tables {")
|
|
606
|
+
ldf_lines.append(" NormalTable {")
|
|
607
|
+
for entry in self.schedule:
|
|
608
|
+
if entry.frame_id in (self.MASTER_REQUEST_FRAME, self.SLAVE_RESPONSE_FRAME):
|
|
609
|
+
continue
|
|
610
|
+
ldf_lines.append(f" Frame_{entry.frame_id:02X} delay {entry.delay_ms:.1f} ms;")
|
|
611
|
+
ldf_lines.append(" }")
|
|
612
|
+
ldf_lines.append("}")
|
oscura/automotive/loaders/blf.py
CHANGED
|
@@ -32,7 +32,7 @@ def load_blf(file_path: Path | str) -> CANMessageList:
|
|
|
32
32
|
>>> print(f"Unique IDs: {len(messages.unique_ids())}")
|
|
33
33
|
"""
|
|
34
34
|
try:
|
|
35
|
-
import can
|
|
35
|
+
import can
|
|
36
36
|
except ImportError as e:
|
|
37
37
|
raise ImportError(
|
|
38
38
|
"python-can is required for BLF file support. "
|
|
@@ -56,8 +56,19 @@ def load_blf(file_path: Path | str) -> CANMessageList:
|
|
|
56
56
|
ch = msg.channel
|
|
57
57
|
if isinstance(ch, int):
|
|
58
58
|
channel = ch
|
|
59
|
+
elif isinstance(ch, str):
|
|
60
|
+
channel = int(ch)
|
|
59
61
|
elif ch is not None:
|
|
60
|
-
|
|
62
|
+
# Handle other types (e.g., sequences), default to 0 if conversion fails
|
|
63
|
+
try:
|
|
64
|
+
# Convert sequences to single value
|
|
65
|
+
from collections.abc import Sequence as ABCSequence
|
|
66
|
+
|
|
67
|
+
if isinstance(ch, ABCSequence):
|
|
68
|
+
channel = int(ch[0]) if ch else 0
|
|
69
|
+
# Note: other cases caught by exception handler
|
|
70
|
+
except (TypeError, ValueError, IndexError):
|
|
71
|
+
channel = 0
|
|
61
72
|
|
|
62
73
|
can_msg = CANMessage(
|
|
63
74
|
arbitration_id=msg.arbitration_id,
|