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
oscura/automotive/uds/decoder.py
CHANGED
|
@@ -128,10 +128,13 @@ class UDSDecoder:
|
|
|
128
128
|
# Handle ISO-TP single frame
|
|
129
129
|
if message.data[0] <= 0x07:
|
|
130
130
|
first_byte = message.data[1]
|
|
131
|
+
# Negative response needs: PCI + 0x7F + Requested SID + NRC = 4 bytes minimum
|
|
132
|
+
if first_byte == 0x7F:
|
|
133
|
+
return len(message.data) >= 4
|
|
131
134
|
else:
|
|
132
135
|
first_byte = message.data[0]
|
|
133
136
|
|
|
134
|
-
# Negative response
|
|
137
|
+
# Negative response (non-ISO-TP format)
|
|
135
138
|
if first_byte == 0x7F:
|
|
136
139
|
return len(message.data) >= 3
|
|
137
140
|
|
|
@@ -152,93 +155,172 @@ class UDSDecoder:
|
|
|
152
155
|
|
|
153
156
|
Returns:
|
|
154
157
|
UDSService, UDSNegativeResponse, or None if not a valid UDS message.
|
|
158
|
+
|
|
159
|
+
Example:
|
|
160
|
+
>>> msg = CANMessage(id=0x7E0, data=bytes([0x02, 0x10, 0x01]))
|
|
161
|
+
>>> service = UDSDecoder.decode_service(msg)
|
|
162
|
+
>>> print(service.name if service else "Invalid")
|
|
155
163
|
"""
|
|
156
164
|
if len(message.data) < 2:
|
|
157
165
|
return None
|
|
158
166
|
|
|
159
|
-
#
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
167
|
+
# Extract UDS payload from ISO-TP frame if needed
|
|
168
|
+
data = UDSDecoder._extract_uds_payload(message.data)
|
|
169
|
+
if not data:
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
# Check for negative response
|
|
173
|
+
if data[0] == 0x7F:
|
|
174
|
+
return UDSDecoder._decode_negative_response(data)
|
|
175
|
+
|
|
176
|
+
# Determine SID and request/response type
|
|
177
|
+
sid_info = UDSDecoder._parse_sid_byte(data[0])
|
|
178
|
+
if sid_info is None:
|
|
179
|
+
return None
|
|
180
|
+
|
|
181
|
+
sid, canonical_sid, is_request = sid_info
|
|
182
|
+
service_name = _SERVICE_NAMES[canonical_sid]
|
|
183
|
+
|
|
184
|
+
# Extract sub-function and payload
|
|
185
|
+
sub_function, payload = UDSDecoder._extract_subfunction_and_payload(
|
|
186
|
+
data, canonical_sid, is_request
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
return UDSService(
|
|
190
|
+
sid=sid,
|
|
191
|
+
name=service_name,
|
|
192
|
+
request=is_request,
|
|
193
|
+
sub_function=sub_function,
|
|
194
|
+
data=payload,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
@staticmethod
|
|
198
|
+
def _extract_uds_payload(message_data: bytes) -> bytes:
|
|
199
|
+
"""Extract UDS payload from CAN message data.
|
|
200
|
+
|
|
201
|
+
Handles ISO-TP single frame format (first byte ≤0x07 indicates length).
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
message_data: Raw CAN message data.
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
UDS payload bytes (empty if invalid).
|
|
208
|
+
"""
|
|
209
|
+
if message_data[0] <= 0x07:
|
|
210
|
+
# ISO-TP single frame: [length, ...UDS data...]
|
|
211
|
+
uds_length = message_data[0]
|
|
212
|
+
if len(message_data) < 1 + uds_length:
|
|
213
|
+
return b""
|
|
214
|
+
return message_data[1 : 1 + uds_length]
|
|
172
215
|
else:
|
|
173
|
-
# Direct UDS:
|
|
174
|
-
|
|
216
|
+
# Direct UDS: all bytes are UDS data
|
|
217
|
+
return message_data
|
|
175
218
|
|
|
176
|
-
|
|
219
|
+
@staticmethod
|
|
220
|
+
def _decode_negative_response(data: bytes) -> UDSNegativeResponse | None:
|
|
221
|
+
"""Decode UDS negative response.
|
|
222
|
+
|
|
223
|
+
Format: [0x7F, requested_SID, NRC]
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
data: UDS payload starting with 0x7F.
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
UDSNegativeResponse or None if invalid format.
|
|
230
|
+
"""
|
|
231
|
+
if len(data) < 3:
|
|
177
232
|
return None
|
|
178
233
|
|
|
179
|
-
|
|
234
|
+
requested_sid = data[1]
|
|
235
|
+
nrc = data[2]
|
|
236
|
+
nrc_name = _NRC_NAMES.get(nrc, f"unknownNRC_0x{nrc:02X}")
|
|
180
237
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
238
|
+
return UDSNegativeResponse(
|
|
239
|
+
requested_sid=requested_sid,
|
|
240
|
+
nrc=nrc,
|
|
241
|
+
nrc_name=nrc_name,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
@staticmethod
|
|
245
|
+
def _parse_sid_byte(first_byte: int) -> tuple[int, int, bool] | None:
|
|
246
|
+
"""Parse SID and request/response type from first UDS byte.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
first_byte: First byte of UDS payload.
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
Tuple of (actual_sid, canonical_sid, is_request) or None if unknown service.
|
|
253
|
+
- actual_sid: The SID byte from message (0x50 for responses, 0x10 for requests)
|
|
254
|
+
- canonical_sid: The canonical service ID for name lookup (always 0x10)
|
|
255
|
+
- is_request: True if request, False if response
|
|
256
|
+
|
|
257
|
+
Notes:
|
|
258
|
+
- Response SIDs: 0x40-0x7F (request 0x00-0x3F + 0x40)
|
|
259
|
+
- Response SIDs: 0xC0-0xFF (request 0x80-0xBF + 0x40)
|
|
260
|
+
- Request SIDs: 0x00-0x3F, 0x80-0xBF
|
|
261
|
+
"""
|
|
197
262
|
if 0x40 <= first_byte < 0x80:
|
|
198
263
|
# Positive response to service 0x00-0x3F
|
|
199
|
-
sid = first_byte
|
|
264
|
+
sid = first_byte # Keep actual response SID (e.g., 0x50)
|
|
265
|
+
canonical_sid = first_byte - 0x40 # Request SID for validation (e.g., 0x10)
|
|
200
266
|
is_request = False
|
|
201
267
|
elif first_byte >= 0xC0:
|
|
202
268
|
# Positive response to service 0x80-0xBF
|
|
203
|
-
sid = first_byte
|
|
269
|
+
sid = first_byte # Keep actual response SID
|
|
270
|
+
canonical_sid = first_byte - 0x40 # Request SID for validation
|
|
204
271
|
is_request = False
|
|
205
272
|
else:
|
|
206
273
|
# Request (0x00-0x3F or 0x80-0xBF)
|
|
207
274
|
sid = first_byte
|
|
275
|
+
canonical_sid = first_byte
|
|
208
276
|
is_request = True
|
|
209
277
|
|
|
210
|
-
#
|
|
211
|
-
if
|
|
278
|
+
# Validate service is known (use canonical request SID)
|
|
279
|
+
if canonical_sid not in _SERVICE_NAMES:
|
|
212
280
|
return None
|
|
213
281
|
|
|
214
|
-
|
|
282
|
+
return (sid, canonical_sid, is_request)
|
|
283
|
+
|
|
284
|
+
@staticmethod
|
|
285
|
+
def _extract_subfunction_and_payload(
|
|
286
|
+
data: bytes, sid: int, is_request: bool
|
|
287
|
+
) -> tuple[int | None, bytes]:
|
|
288
|
+
"""Extract sub-function and payload from UDS service data.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
data: UDS payload bytes.
|
|
292
|
+
sid: Service ID.
|
|
293
|
+
is_request: True if request, False if response.
|
|
215
294
|
|
|
216
|
-
|
|
295
|
+
Returns:
|
|
296
|
+
Tuple of (sub_function, payload_bytes).
|
|
297
|
+
|
|
298
|
+
Notes:
|
|
299
|
+
Some services echo sub-function in responses: 0x10, 0x11, 0x27, 0x28, 0x31, 0x3E, 0x85.
|
|
300
|
+
"""
|
|
217
301
|
sub_function = None
|
|
218
302
|
payload_offset = 1
|
|
219
303
|
|
|
220
|
-
if
|
|
304
|
+
if sid not in _SERVICES_WITH_SUBFUNCTIONS:
|
|
305
|
+
# No sub-function for this service
|
|
306
|
+
payload = data[payload_offset:] if len(data) > payload_offset else b""
|
|
307
|
+
return (sub_function, payload)
|
|
308
|
+
|
|
309
|
+
# Extract sub-function if data contains it
|
|
310
|
+
if is_request:
|
|
221
311
|
if len(data) >= 2:
|
|
222
|
-
#
|
|
312
|
+
# Mask off suppress positive response bit (0x80)
|
|
313
|
+
sub_function = data[1] & 0x7F
|
|
314
|
+
payload_offset = 2
|
|
315
|
+
else:
|
|
316
|
+
# Response may echo sub-function for certain services
|
|
317
|
+
_SERVICES_WITH_ECHO = {0x10, 0x11, 0x27, 0x28, 0x31, 0x3E, 0x85}
|
|
318
|
+
if sid in _SERVICES_WITH_ECHO and len(data) >= 2:
|
|
223
319
|
sub_function = data[1] & 0x7F
|
|
224
320
|
payload_offset = 2
|
|
225
|
-
elif not is_request and sid in _SERVICES_WITH_SUBFUNCTIONS:
|
|
226
|
-
# Response may echo sub-function for some services
|
|
227
|
-
if sid in {0x10, 0x11, 0x27, 0x28, 0x31, 0x3E, 0x85}:
|
|
228
|
-
if len(data) >= 2:
|
|
229
|
-
sub_function = data[1] & 0x7F
|
|
230
|
-
payload_offset = 2
|
|
231
|
-
|
|
232
|
-
# Extract remaining data payload
|
|
233
|
-
payload = data[payload_offset:] if len(data) > payload_offset else b""
|
|
234
321
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
name=service_name,
|
|
238
|
-
request=is_request,
|
|
239
|
-
sub_function=sub_function,
|
|
240
|
-
data=payload,
|
|
241
|
-
)
|
|
322
|
+
payload = data[payload_offset:] if len(data) > payload_offset else b""
|
|
323
|
+
return (sub_function, payload)
|
|
242
324
|
|
|
243
325
|
@staticmethod
|
|
244
326
|
def get_service_name(sid: int) -> str:
|
oscura/automotive/uds/models.py
CHANGED
|
@@ -16,7 +16,8 @@ class UDSService:
|
|
|
16
16
|
"""A decoded UDS service.
|
|
17
17
|
|
|
18
18
|
Attributes:
|
|
19
|
-
sid: Service ID (0x10-0xFF).
|
|
19
|
+
sid: Service ID (0x10-0xFF). For responses, this is the response SID (e.g., 0x50),
|
|
20
|
+
not the request SID (0x10).
|
|
20
21
|
name: Human-readable service name.
|
|
21
22
|
request: True if request message, False if positive response.
|
|
22
23
|
sub_function: Sub-function byte (if applicable).
|
|
@@ -29,6 +30,11 @@ class UDSService:
|
|
|
29
30
|
sub_function: int | None = None
|
|
30
31
|
data: bytes = b""
|
|
31
32
|
|
|
33
|
+
@property
|
|
34
|
+
def is_response(self) -> bool:
|
|
35
|
+
"""True if this is a response message."""
|
|
36
|
+
return not self.request
|
|
37
|
+
|
|
32
38
|
def __repr__(self) -> str:
|
|
33
39
|
"""Human-readable representation."""
|
|
34
40
|
msg_type = "Request" if self.request else "Response"
|
|
@@ -63,7 +63,7 @@ def plot_bus_timeline(
|
|
|
63
63
|
msg_list: list[CANMessage] = [plot_messages]
|
|
64
64
|
else:
|
|
65
65
|
# Must be list[CANMessage] from slice
|
|
66
|
-
msg_list = plot_messages
|
|
66
|
+
msg_list = plot_messages
|
|
67
67
|
|
|
68
68
|
# Extract timestamps and IDs
|
|
69
69
|
timestamps = [_get_timestamp(msg) for msg in msg_list]
|
oscura/cli/analyze.py
ADDED
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
"""Oscura Analyze Command - Full Analysis Workflow.
|
|
2
|
+
|
|
3
|
+
Provides CLI for running complete analysis workflows on waveform files with
|
|
4
|
+
automatic protocol detection, signal characterization, and comprehensive reporting.
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
$ oscura analyze signal.wfm
|
|
9
|
+
$ oscura analyze capture.wfm --protocol uart --export-dir output/
|
|
10
|
+
$ oscura analyze data.wfm --interactive
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
import click
|
|
20
|
+
|
|
21
|
+
from oscura.cli.main import format_output
|
|
22
|
+
from oscura.cli.progress import ProgressReporter
|
|
23
|
+
from oscura.sessions.legacy import Session
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger("oscura.cli.analyze")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@click.command()
|
|
29
|
+
@click.argument("file", type=click.Path(exists=True))
|
|
30
|
+
@click.option(
|
|
31
|
+
"--protocol",
|
|
32
|
+
type=str,
|
|
33
|
+
default="auto",
|
|
34
|
+
help="Protocol hint (auto for auto-detection).",
|
|
35
|
+
)
|
|
36
|
+
@click.option(
|
|
37
|
+
"--export-dir",
|
|
38
|
+
type=click.Path(),
|
|
39
|
+
default=None,
|
|
40
|
+
help="Directory to export results (default: ./oscura_output).",
|
|
41
|
+
)
|
|
42
|
+
@click.option(
|
|
43
|
+
"--interactive",
|
|
44
|
+
"-i",
|
|
45
|
+
is_flag=True,
|
|
46
|
+
help="Interactive mode with prompts.",
|
|
47
|
+
)
|
|
48
|
+
@click.option(
|
|
49
|
+
"--output",
|
|
50
|
+
type=click.Choice(["json", "csv", "html", "table"], case_sensitive=False),
|
|
51
|
+
default="table",
|
|
52
|
+
help="Output format (default: table).",
|
|
53
|
+
)
|
|
54
|
+
@click.option(
|
|
55
|
+
"--save-session",
|
|
56
|
+
type=click.Path(),
|
|
57
|
+
default=None,
|
|
58
|
+
help="Save analysis session to file (.tks).",
|
|
59
|
+
)
|
|
60
|
+
@click.pass_context
|
|
61
|
+
def analyze(
|
|
62
|
+
ctx: click.Context,
|
|
63
|
+
file: str,
|
|
64
|
+
protocol: str,
|
|
65
|
+
export_dir: str | None,
|
|
66
|
+
interactive: bool,
|
|
67
|
+
output: str,
|
|
68
|
+
save_session: str | None,
|
|
69
|
+
) -> None:
|
|
70
|
+
"""Run full analysis workflow on waveform file.
|
|
71
|
+
|
|
72
|
+
Performs comprehensive analysis including signal characterization,
|
|
73
|
+
protocol detection and decoding, spectral analysis, and generates
|
|
74
|
+
detailed reports.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
ctx: Click context object.
|
|
78
|
+
file: Path to waveform file to analyze.
|
|
79
|
+
protocol: Protocol hint or 'auto' for detection.
|
|
80
|
+
export_dir: Directory for exported results.
|
|
81
|
+
interactive: Enable interactive prompts.
|
|
82
|
+
output: Output format.
|
|
83
|
+
save_session: Path to save session file.
|
|
84
|
+
|
|
85
|
+
Examples:
|
|
86
|
+
|
|
87
|
+
\b
|
|
88
|
+
# Full auto analysis
|
|
89
|
+
$ oscura analyze capture.wfm
|
|
90
|
+
|
|
91
|
+
\b
|
|
92
|
+
# With protocol hint and export
|
|
93
|
+
$ oscura analyze signal.wfm \\
|
|
94
|
+
--protocol uart \\
|
|
95
|
+
--export-dir analysis_results/
|
|
96
|
+
|
|
97
|
+
\b
|
|
98
|
+
# Interactive mode
|
|
99
|
+
$ oscura analyze data.wfm --interactive
|
|
100
|
+
"""
|
|
101
|
+
verbose = ctx.obj.get("verbose", 0)
|
|
102
|
+
quiet = ctx.obj.get("quiet", False)
|
|
103
|
+
|
|
104
|
+
if verbose:
|
|
105
|
+
logger.info(f"Analyzing: {file}")
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
results = _perform_analysis_workflow(
|
|
109
|
+
file, protocol, export_dir, interactive, quiet, save_session
|
|
110
|
+
)
|
|
111
|
+
formatted = format_output(results, output)
|
|
112
|
+
click.echo(formatted)
|
|
113
|
+
|
|
114
|
+
except Exception as e:
|
|
115
|
+
logger.error(f"Analysis failed: {e}")
|
|
116
|
+
if verbose > 1:
|
|
117
|
+
raise
|
|
118
|
+
click.echo(f"Error: {e}", err=True)
|
|
119
|
+
ctx.exit(1)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _perform_analysis_workflow(
|
|
123
|
+
file: str,
|
|
124
|
+
protocol: str,
|
|
125
|
+
export_dir: str | None,
|
|
126
|
+
interactive: bool,
|
|
127
|
+
quiet: bool,
|
|
128
|
+
save_session: str | None,
|
|
129
|
+
) -> dict[str, Any]:
|
|
130
|
+
"""Perform complete analysis workflow."""
|
|
131
|
+
from oscura.loaders import load
|
|
132
|
+
|
|
133
|
+
progress = ProgressReporter(quiet=quiet, stages=5)
|
|
134
|
+
|
|
135
|
+
# Stage 1: Load file
|
|
136
|
+
progress.start_stage("Loading file")
|
|
137
|
+
trace = load(file)
|
|
138
|
+
progress.complete_stage()
|
|
139
|
+
|
|
140
|
+
# Stage 2: Signal characterization
|
|
141
|
+
progress.start_stage("Characterizing signal")
|
|
142
|
+
signal_char = _characterize_signal(trace)
|
|
143
|
+
progress.complete_stage()
|
|
144
|
+
|
|
145
|
+
# Stage 3: Protocol detection
|
|
146
|
+
progress.start_stage("Detecting protocol")
|
|
147
|
+
protocol_info = _detect_and_prepare_protocol(trace, protocol, interactive)
|
|
148
|
+
progress.complete_stage()
|
|
149
|
+
|
|
150
|
+
# Stage 4: Protocol decoding
|
|
151
|
+
progress.start_stage("Decoding protocol")
|
|
152
|
+
decoded = _decode_protocol(trace, protocol_info["protocol"])
|
|
153
|
+
progress.complete_stage()
|
|
154
|
+
|
|
155
|
+
# Stage 5: Generate report
|
|
156
|
+
progress.start_stage("Generating report")
|
|
157
|
+
results = _build_analysis_results(
|
|
158
|
+
file, signal_char, protocol_info, decoded, export_dir, save_session, trace
|
|
159
|
+
)
|
|
160
|
+
progress.complete_stage()
|
|
161
|
+
progress.finish()
|
|
162
|
+
|
|
163
|
+
return results
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _detect_and_prepare_protocol(trace: Any, protocol: str, interactive: bool) -> dict[str, Any]:
|
|
167
|
+
"""Detect and prepare protocol information."""
|
|
168
|
+
if protocol == "auto":
|
|
169
|
+
detected = _detect_protocol(trace, interactive=interactive)
|
|
170
|
+
protocol = detected["protocol"]
|
|
171
|
+
return {"protocol": protocol, "auto_detected": protocol == "auto"}
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _build_analysis_results(
|
|
175
|
+
file: str,
|
|
176
|
+
signal_char: dict[str, Any],
|
|
177
|
+
protocol_info: dict[str, Any],
|
|
178
|
+
decoded: dict[str, Any],
|
|
179
|
+
export_dir: str | None,
|
|
180
|
+
save_session: str | None,
|
|
181
|
+
trace: Any,
|
|
182
|
+
) -> dict[str, Any]:
|
|
183
|
+
"""Build final analysis results with optional exports."""
|
|
184
|
+
results = {
|
|
185
|
+
"file": str(Path(file).name),
|
|
186
|
+
**signal_char,
|
|
187
|
+
**protocol_info,
|
|
188
|
+
**decoded,
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if export_dir:
|
|
192
|
+
export_path = Path(export_dir)
|
|
193
|
+
export_path.mkdir(parents=True, exist_ok=True)
|
|
194
|
+
_export_results(results, export_path)
|
|
195
|
+
results["export_dir"] = str(export_path)
|
|
196
|
+
|
|
197
|
+
if save_session:
|
|
198
|
+
session = Session(name=Path(file).stem)
|
|
199
|
+
session.add_trace("main", trace)
|
|
200
|
+
session.metadata["analysis_results"] = results
|
|
201
|
+
session.save(save_session)
|
|
202
|
+
results["session_file"] = save_session
|
|
203
|
+
|
|
204
|
+
return results
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _characterize_signal(trace: Any) -> dict[str, Any]:
|
|
208
|
+
"""Characterize signal properties.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
trace: Waveform trace to characterize.
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
Dictionary of signal characteristics.
|
|
215
|
+
"""
|
|
216
|
+
import numpy as np
|
|
217
|
+
|
|
218
|
+
from oscura.analyzers.waveform.measurements import fall_time, rise_time
|
|
219
|
+
|
|
220
|
+
data = trace.data
|
|
221
|
+
sample_rate = trace.metadata.sample_rate
|
|
222
|
+
|
|
223
|
+
rt = rise_time(trace)
|
|
224
|
+
ft = fall_time(trace)
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
"sample_rate": f"{sample_rate / 1e6:.1f} MHz",
|
|
228
|
+
"samples": len(data),
|
|
229
|
+
"duration": f"{len(data) / sample_rate * 1e3:.3f} ms",
|
|
230
|
+
"amplitude": f"{float(data.max() - data.min()):.3f} V",
|
|
231
|
+
"rise_time": f"{rt * 1e9:.2f} ns" if not np.isnan(rt) else "N/A",
|
|
232
|
+
"fall_time": f"{ft * 1e9:.2f} ns" if not np.isnan(ft) else "N/A",
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _detect_protocol(trace: Any, interactive: bool = False) -> dict[str, Any]:
|
|
237
|
+
"""Detect protocol from trace.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
trace: Trace to analyze.
|
|
241
|
+
interactive: If True, prompt for confirmation.
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
Detection results.
|
|
245
|
+
"""
|
|
246
|
+
from oscura.inference.protocol import detect_protocol
|
|
247
|
+
|
|
248
|
+
detection = detect_protocol(trace, min_confidence=0.5, return_candidates=True)
|
|
249
|
+
|
|
250
|
+
if interactive and detection.get("confidence", 0) < 0.9:
|
|
251
|
+
click.echo(f"\nDetected protocol: {detection['protocol']}")
|
|
252
|
+
click.echo(f"Confidence: {detection['confidence']:.1%}")
|
|
253
|
+
|
|
254
|
+
if not click.confirm("Use this protocol?", default=True):
|
|
255
|
+
# Show candidates
|
|
256
|
+
candidates = detection.get("candidates", [])
|
|
257
|
+
if candidates:
|
|
258
|
+
click.echo("\nOther candidates:")
|
|
259
|
+
for i, cand in enumerate(candidates[:5], 1):
|
|
260
|
+
click.echo(f" {i}. {cand['protocol']} ({cand['confidence']:.1%})")
|
|
261
|
+
|
|
262
|
+
choice = click.prompt("Select protocol (1-5, or 0 for manual)", type=int, default=1)
|
|
263
|
+
if 1 <= choice <= len(candidates):
|
|
264
|
+
detection = candidates[choice - 1]
|
|
265
|
+
elif choice == 0:
|
|
266
|
+
manual = click.prompt("Enter protocol name", type=str)
|
|
267
|
+
detection = {"protocol": manual, "confidence": 1.0}
|
|
268
|
+
|
|
269
|
+
return detection
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _decode_protocol(trace: Any, protocol: str) -> dict[str, Any]:
|
|
273
|
+
"""Decode protocol from trace.
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
trace: Trace to decode.
|
|
277
|
+
protocol: Protocol name.
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
Decoding results.
|
|
281
|
+
"""
|
|
282
|
+
from oscura.core.types import DigitalTrace
|
|
283
|
+
|
|
284
|
+
# Convert to digital if needed
|
|
285
|
+
if not isinstance(trace, DigitalTrace):
|
|
286
|
+
import numpy as np
|
|
287
|
+
|
|
288
|
+
threshold = (np.max(trace.data) + np.min(trace.data)) / 2
|
|
289
|
+
digital_data = trace.data > threshold
|
|
290
|
+
digital_trace = DigitalTrace(data=digital_data, metadata=trace.metadata)
|
|
291
|
+
else:
|
|
292
|
+
digital_trace = trace
|
|
293
|
+
|
|
294
|
+
# Decode based on protocol
|
|
295
|
+
packets: list[Any] = []
|
|
296
|
+
if protocol.lower() == "uart":
|
|
297
|
+
from oscura.analyzers.protocols.uart import UARTDecoder
|
|
298
|
+
|
|
299
|
+
uart_decoder = UARTDecoder(baudrate=0) # Auto-detect
|
|
300
|
+
packets = list(uart_decoder.decode(digital_trace))
|
|
301
|
+
elif protocol.lower() == "spi":
|
|
302
|
+
from oscura.analyzers.protocols.spi import SPIDecoder
|
|
303
|
+
|
|
304
|
+
spi_decoder = SPIDecoder()
|
|
305
|
+
packets = list(
|
|
306
|
+
spi_decoder.decode(
|
|
307
|
+
clk=digital_trace.data,
|
|
308
|
+
mosi=digital_trace.data,
|
|
309
|
+
sample_rate=trace.metadata.sample_rate,
|
|
310
|
+
)
|
|
311
|
+
)
|
|
312
|
+
elif protocol.lower() == "i2c":
|
|
313
|
+
from oscura.analyzers.protocols.i2c import I2CDecoder
|
|
314
|
+
|
|
315
|
+
i2c_decoder = I2CDecoder()
|
|
316
|
+
packets = list(
|
|
317
|
+
i2c_decoder.decode(
|
|
318
|
+
scl=digital_trace.data,
|
|
319
|
+
sda=digital_trace.data,
|
|
320
|
+
sample_rate=trace.metadata.sample_rate,
|
|
321
|
+
)
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
return {
|
|
325
|
+
"packets_decoded": len(packets),
|
|
326
|
+
"errors": sum(1 for p in packets if p.errors),
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _export_results(results: dict[str, Any], export_dir: Path) -> None:
|
|
331
|
+
"""Export results to directory.
|
|
332
|
+
|
|
333
|
+
Args:
|
|
334
|
+
results: Results dictionary.
|
|
335
|
+
export_dir: Export directory path.
|
|
336
|
+
"""
|
|
337
|
+
import json
|
|
338
|
+
|
|
339
|
+
# Export JSON
|
|
340
|
+
json_path = export_dir / "analysis_results.json"
|
|
341
|
+
with open(json_path, "w") as f:
|
|
342
|
+
json.dump(results, f, indent=2, default=str)
|
|
343
|
+
|
|
344
|
+
# Export HTML report
|
|
345
|
+
html_path = export_dir / "analysis_report.html"
|
|
346
|
+
html_content = format_output(results, "html")
|
|
347
|
+
with open(html_path, "w") as f:
|
|
348
|
+
f.write(html_content)
|