oscura 0.5.1__py3-none-any.whl → 0.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- oscura/__init__.py +169 -167
- oscura/analyzers/__init__.py +3 -0
- oscura/analyzers/classification.py +659 -0
- oscura/analyzers/digital/edges.py +325 -65
- oscura/analyzers/digital/quality.py +293 -166
- oscura/analyzers/digital/timing.py +260 -115
- oscura/analyzers/digital/timing_numba.py +334 -0
- oscura/analyzers/entropy.py +605 -0
- oscura/analyzers/eye/diagram.py +176 -109
- oscura/analyzers/eye/metrics.py +5 -5
- oscura/analyzers/jitter/__init__.py +6 -4
- oscura/analyzers/jitter/ber.py +52 -52
- oscura/analyzers/jitter/classification.py +156 -0
- oscura/analyzers/jitter/decomposition.py +163 -113
- oscura/analyzers/jitter/spectrum.py +80 -64
- oscura/analyzers/ml/__init__.py +39 -0
- oscura/analyzers/ml/features.py +600 -0
- oscura/analyzers/ml/signal_classifier.py +604 -0
- oscura/analyzers/packet/daq.py +246 -158
- oscura/analyzers/packet/parser.py +12 -1
- oscura/analyzers/packet/payload.py +50 -2110
- oscura/analyzers/packet/payload_analysis.py +361 -181
- oscura/analyzers/packet/payload_patterns.py +133 -70
- oscura/analyzers/packet/stream.py +84 -23
- oscura/analyzers/patterns/__init__.py +26 -5
- oscura/analyzers/patterns/anomaly_detection.py +908 -0
- oscura/analyzers/patterns/clustering.py +169 -108
- oscura/analyzers/patterns/clustering_optimized.py +227 -0
- oscura/analyzers/patterns/discovery.py +1 -1
- oscura/analyzers/patterns/matching.py +581 -197
- oscura/analyzers/patterns/pattern_mining.py +778 -0
- oscura/analyzers/patterns/periodic.py +121 -38
- oscura/analyzers/patterns/sequences.py +175 -78
- oscura/analyzers/power/conduction.py +1 -1
- oscura/analyzers/power/soa.py +6 -6
- oscura/analyzers/power/switching.py +250 -110
- oscura/analyzers/protocol/__init__.py +17 -1
- oscura/analyzers/protocols/base.py +6 -6
- oscura/analyzers/protocols/ble/__init__.py +38 -0
- oscura/analyzers/protocols/ble/analyzer.py +809 -0
- oscura/analyzers/protocols/ble/uuids.py +288 -0
- oscura/analyzers/protocols/can.py +257 -127
- oscura/analyzers/protocols/can_fd.py +107 -80
- oscura/analyzers/protocols/flexray.py +139 -80
- oscura/analyzers/protocols/hdlc.py +93 -58
- oscura/analyzers/protocols/i2c.py +247 -106
- oscura/analyzers/protocols/i2s.py +138 -86
- oscura/analyzers/protocols/industrial/__init__.py +40 -0
- oscura/analyzers/protocols/industrial/bacnet/__init__.py +33 -0
- oscura/analyzers/protocols/industrial/bacnet/analyzer.py +708 -0
- oscura/analyzers/protocols/industrial/bacnet/encoding.py +412 -0
- oscura/analyzers/protocols/industrial/bacnet/services.py +622 -0
- oscura/analyzers/protocols/industrial/ethercat/__init__.py +30 -0
- oscura/analyzers/protocols/industrial/ethercat/analyzer.py +474 -0
- oscura/analyzers/protocols/industrial/ethercat/mailbox.py +339 -0
- oscura/analyzers/protocols/industrial/ethercat/topology.py +166 -0
- oscura/analyzers/protocols/industrial/modbus/__init__.py +31 -0
- oscura/analyzers/protocols/industrial/modbus/analyzer.py +525 -0
- oscura/analyzers/protocols/industrial/modbus/crc.py +79 -0
- oscura/analyzers/protocols/industrial/modbus/functions.py +436 -0
- oscura/analyzers/protocols/industrial/opcua/__init__.py +21 -0
- oscura/analyzers/protocols/industrial/opcua/analyzer.py +552 -0
- oscura/analyzers/protocols/industrial/opcua/datatypes.py +446 -0
- oscura/analyzers/protocols/industrial/opcua/services.py +264 -0
- oscura/analyzers/protocols/industrial/profinet/__init__.py +23 -0
- oscura/analyzers/protocols/industrial/profinet/analyzer.py +441 -0
- oscura/analyzers/protocols/industrial/profinet/dcp.py +263 -0
- oscura/analyzers/protocols/industrial/profinet/ptcp.py +200 -0
- oscura/analyzers/protocols/jtag.py +180 -98
- oscura/analyzers/protocols/lin.py +219 -114
- oscura/analyzers/protocols/manchester.py +4 -4
- oscura/analyzers/protocols/onewire.py +253 -149
- oscura/analyzers/protocols/parallel_bus/__init__.py +20 -0
- oscura/analyzers/protocols/parallel_bus/centronics.py +92 -0
- oscura/analyzers/protocols/parallel_bus/gpib.py +137 -0
- oscura/analyzers/protocols/spi.py +192 -95
- oscura/analyzers/protocols/swd.py +321 -167
- oscura/analyzers/protocols/uart.py +267 -125
- oscura/analyzers/protocols/usb.py +235 -131
- oscura/analyzers/side_channel/power.py +17 -12
- oscura/analyzers/signal/__init__.py +15 -0
- oscura/analyzers/signal/timing_analysis.py +1086 -0
- oscura/analyzers/signal_integrity/__init__.py +4 -1
- oscura/analyzers/signal_integrity/sparams.py +2 -19
- oscura/analyzers/spectral/chunked.py +129 -60
- oscura/analyzers/spectral/chunked_fft.py +300 -94
- oscura/analyzers/spectral/chunked_wavelet.py +100 -80
- oscura/analyzers/statistical/checksum.py +376 -217
- oscura/analyzers/statistical/classification.py +229 -107
- oscura/analyzers/statistical/entropy.py +78 -53
- oscura/analyzers/statistics/correlation.py +407 -211
- oscura/analyzers/statistics/outliers.py +2 -2
- oscura/analyzers/statistics/streaming.py +30 -5
- oscura/analyzers/validation.py +216 -101
- oscura/analyzers/waveform/measurements.py +9 -0
- oscura/analyzers/waveform/measurements_with_uncertainty.py +31 -15
- oscura/analyzers/waveform/spectral.py +500 -228
- oscura/api/__init__.py +31 -5
- oscura/api/dsl/__init__.py +582 -0
- oscura/{dsl → api/dsl}/commands.py +43 -76
- oscura/{dsl → api/dsl}/interpreter.py +26 -51
- oscura/{dsl → api/dsl}/parser.py +107 -77
- oscura/{dsl → api/dsl}/repl.py +2 -2
- oscura/api/dsl.py +1 -1
- oscura/{integrations → api/integrations}/__init__.py +1 -1
- oscura/{integrations → api/integrations}/llm.py +201 -102
- oscura/api/operators.py +3 -3
- oscura/api/optimization.py +144 -30
- oscura/api/rest_server.py +921 -0
- oscura/api/server/__init__.py +17 -0
- oscura/api/server/dashboard.py +850 -0
- oscura/api/server/static/README.md +34 -0
- oscura/api/server/templates/base.html +181 -0
- oscura/api/server/templates/export.html +120 -0
- oscura/api/server/templates/home.html +284 -0
- oscura/api/server/templates/protocols.html +58 -0
- oscura/api/server/templates/reports.html +43 -0
- oscura/api/server/templates/session_detail.html +89 -0
- oscura/api/server/templates/sessions.html +83 -0
- oscura/api/server/templates/waveforms.html +73 -0
- oscura/automotive/__init__.py +8 -1
- oscura/automotive/can/__init__.py +10 -0
- oscura/automotive/can/checksum.py +3 -1
- oscura/automotive/can/dbc_generator.py +590 -0
- oscura/automotive/can/message_wrapper.py +121 -74
- oscura/automotive/can/patterns.py +98 -21
- oscura/automotive/can/session.py +292 -56
- oscura/automotive/can/state_machine.py +6 -3
- oscura/automotive/can/stimulus_response.py +97 -75
- oscura/automotive/dbc/__init__.py +10 -2
- oscura/automotive/dbc/generator.py +84 -56
- oscura/automotive/dbc/parser.py +6 -6
- oscura/automotive/dtc/data.json +17 -102
- oscura/automotive/dtc/database.py +2 -2
- oscura/automotive/flexray/__init__.py +31 -0
- oscura/automotive/flexray/analyzer.py +504 -0
- oscura/automotive/flexray/crc.py +185 -0
- oscura/automotive/flexray/fibex.py +449 -0
- oscura/automotive/j1939/__init__.py +45 -8
- oscura/automotive/j1939/analyzer.py +605 -0
- oscura/automotive/j1939/spns.py +326 -0
- oscura/automotive/j1939/transport.py +306 -0
- oscura/automotive/lin/__init__.py +47 -0
- oscura/automotive/lin/analyzer.py +612 -0
- oscura/automotive/loaders/blf.py +13 -2
- oscura/automotive/loaders/csv_can.py +143 -72
- oscura/automotive/loaders/dispatcher.py +50 -2
- oscura/automotive/loaders/mdf.py +86 -45
- oscura/automotive/loaders/pcap.py +111 -61
- oscura/automotive/uds/__init__.py +4 -0
- oscura/automotive/uds/analyzer.py +725 -0
- oscura/automotive/uds/decoder.py +140 -58
- oscura/automotive/uds/models.py +7 -1
- oscura/automotive/visualization.py +1 -1
- oscura/cli/analyze.py +348 -0
- oscura/cli/batch.py +142 -122
- oscura/cli/benchmark.py +275 -0
- oscura/cli/characterize.py +137 -82
- oscura/cli/compare.py +224 -131
- oscura/cli/completion.py +250 -0
- oscura/cli/config_cmd.py +361 -0
- oscura/cli/decode.py +164 -87
- oscura/cli/export.py +286 -0
- oscura/cli/main.py +115 -31
- oscura/{onboarding → cli/onboarding}/__init__.py +3 -3
- oscura/{onboarding → cli/onboarding}/help.py +80 -58
- oscura/{onboarding → cli/onboarding}/tutorials.py +97 -72
- oscura/{onboarding → cli/onboarding}/wizard.py +55 -36
- oscura/cli/progress.py +147 -0
- oscura/cli/shell.py +157 -135
- oscura/cli/validate_cmd.py +204 -0
- oscura/cli/visualize.py +158 -0
- oscura/convenience.py +125 -79
- oscura/core/__init__.py +4 -2
- oscura/core/backend_selector.py +3 -3
- oscura/core/cache.py +126 -15
- oscura/core/cancellation.py +1 -1
- oscura/{config → core/config}/__init__.py +20 -11
- oscura/{config → core/config}/defaults.py +1 -1
- oscura/{config → core/config}/loader.py +7 -5
- oscura/{config → core/config}/memory.py +5 -5
- oscura/{config → core/config}/migration.py +1 -1
- oscura/{config → core/config}/pipeline.py +99 -23
- oscura/{config → core/config}/preferences.py +1 -1
- oscura/{config → core/config}/protocol.py +3 -3
- oscura/{config → core/config}/schema.py +426 -272
- oscura/{config → core/config}/settings.py +1 -1
- oscura/{config → core/config}/thresholds.py +195 -153
- oscura/core/correlation.py +5 -6
- oscura/core/cross_domain.py +0 -2
- oscura/core/debug.py +9 -5
- oscura/{extensibility → core/extensibility}/docs.py +158 -70
- oscura/{extensibility → core/extensibility}/extensions.py +160 -76
- oscura/{extensibility → core/extensibility}/logging.py +1 -1
- oscura/{extensibility → core/extensibility}/measurements.py +1 -1
- oscura/{extensibility → core/extensibility}/plugins.py +1 -1
- oscura/{extensibility → core/extensibility}/templates.py +73 -3
- oscura/{extensibility → core/extensibility}/validation.py +1 -1
- oscura/core/gpu_backend.py +11 -7
- oscura/core/log_query.py +101 -11
- oscura/core/logging.py +126 -54
- oscura/core/logging_advanced.py +5 -5
- oscura/core/memory_limits.py +108 -70
- oscura/core/memory_monitor.py +2 -2
- oscura/core/memory_progress.py +7 -7
- oscura/core/memory_warnings.py +1 -1
- oscura/core/numba_backend.py +13 -13
- oscura/{plugins → core/plugins}/__init__.py +9 -9
- oscura/{plugins → core/plugins}/base.py +7 -7
- oscura/{plugins → core/plugins}/cli.py +3 -3
- oscura/{plugins → core/plugins}/discovery.py +186 -106
- oscura/{plugins → core/plugins}/lifecycle.py +1 -1
- oscura/{plugins → core/plugins}/manager.py +7 -7
- oscura/{plugins → core/plugins}/registry.py +3 -3
- oscura/{plugins → core/plugins}/versioning.py +1 -1
- oscura/core/progress.py +16 -1
- oscura/core/provenance.py +8 -2
- oscura/{schemas → core/schemas}/__init__.py +2 -2
- oscura/{schemas → core/schemas}/device_mapping.json +2 -8
- oscura/{schemas → core/schemas}/packet_format.json +4 -24
- oscura/{schemas → core/schemas}/protocol_definition.json +2 -12
- oscura/core/types.py +4 -0
- oscura/core/uncertainty.py +3 -3
- oscura/correlation/__init__.py +52 -0
- oscura/correlation/multi_protocol.py +811 -0
- oscura/discovery/auto_decoder.py +117 -35
- oscura/discovery/comparison.py +191 -86
- oscura/discovery/quality_validator.py +155 -68
- oscura/discovery/signal_detector.py +196 -79
- oscura/export/__init__.py +18 -8
- oscura/export/kaitai_struct.py +513 -0
- oscura/export/scapy_layer.py +801 -0
- oscura/export/wireshark/generator.py +1 -1
- oscura/export/wireshark/templates/dissector.lua.j2 +2 -2
- oscura/export/wireshark_dissector.py +746 -0
- oscura/guidance/wizard.py +207 -111
- oscura/hardware/__init__.py +19 -0
- oscura/{acquisition → hardware/acquisition}/__init__.py +4 -4
- oscura/{acquisition → hardware/acquisition}/file.py +2 -2
- oscura/{acquisition → hardware/acquisition}/hardware.py +7 -7
- oscura/{acquisition → hardware/acquisition}/saleae.py +15 -12
- oscura/{acquisition → hardware/acquisition}/socketcan.py +1 -1
- oscura/{acquisition → hardware/acquisition}/streaming.py +2 -2
- oscura/{acquisition → hardware/acquisition}/synthetic.py +3 -3
- oscura/{acquisition → hardware/acquisition}/visa.py +33 -11
- oscura/hardware/firmware/__init__.py +29 -0
- oscura/hardware/firmware/pattern_recognition.py +874 -0
- oscura/hardware/hal_detector.py +736 -0
- oscura/hardware/security/__init__.py +37 -0
- oscura/hardware/security/side_channel_detector.py +1126 -0
- oscura/inference/__init__.py +4 -0
- oscura/inference/active_learning/observation_table.py +4 -1
- oscura/inference/alignment.py +216 -123
- oscura/inference/bayesian.py +113 -33
- oscura/inference/crc_reverse.py +101 -55
- oscura/inference/logic.py +6 -2
- oscura/inference/message_format.py +342 -183
- oscura/inference/protocol.py +95 -44
- oscura/inference/protocol_dsl.py +180 -82
- oscura/inference/signal_intelligence.py +1439 -706
- oscura/inference/spectral.py +99 -57
- oscura/inference/state_machine.py +810 -158
- oscura/inference/stream.py +270 -110
- oscura/iot/__init__.py +34 -0
- oscura/iot/coap/__init__.py +32 -0
- oscura/iot/coap/analyzer.py +668 -0
- oscura/iot/coap/options.py +212 -0
- oscura/iot/lorawan/__init__.py +21 -0
- oscura/iot/lorawan/crypto.py +206 -0
- oscura/iot/lorawan/decoder.py +801 -0
- oscura/iot/lorawan/mac_commands.py +341 -0
- oscura/iot/mqtt/__init__.py +27 -0
- oscura/iot/mqtt/analyzer.py +999 -0
- oscura/iot/mqtt/properties.py +315 -0
- oscura/iot/zigbee/__init__.py +31 -0
- oscura/iot/zigbee/analyzer.py +615 -0
- oscura/iot/zigbee/security.py +153 -0
- oscura/iot/zigbee/zcl.py +349 -0
- oscura/jupyter/display.py +125 -45
- oscura/{exploratory → jupyter/exploratory}/__init__.py +8 -8
- oscura/{exploratory → jupyter/exploratory}/error_recovery.py +298 -141
- oscura/jupyter/exploratory/fuzzy.py +746 -0
- oscura/{exploratory → jupyter/exploratory}/fuzzy_advanced.py +258 -100
- oscura/{exploratory → jupyter/exploratory}/legacy.py +464 -242
- oscura/{exploratory → jupyter/exploratory}/parse.py +167 -145
- oscura/{exploratory → jupyter/exploratory}/recovery.py +119 -87
- oscura/jupyter/exploratory/sync.py +612 -0
- oscura/{exploratory → jupyter/exploratory}/unknown.py +299 -176
- oscura/jupyter/magic.py +4 -4
- oscura/{ui → jupyter/ui}/__init__.py +2 -2
- oscura/{ui → jupyter/ui}/formatters.py +3 -3
- oscura/{ui → jupyter/ui}/progressive_display.py +153 -82
- oscura/loaders/__init__.py +183 -67
- oscura/loaders/binary.py +88 -1
- oscura/loaders/chipwhisperer.py +153 -137
- oscura/loaders/configurable.py +208 -86
- oscura/loaders/csv_loader.py +458 -215
- oscura/loaders/hdf5_loader.py +278 -119
- oscura/loaders/lazy.py +87 -54
- oscura/loaders/mmap_loader.py +1 -1
- oscura/loaders/numpy_loader.py +253 -116
- oscura/loaders/pcap.py +226 -151
- oscura/loaders/rigol.py +110 -49
- oscura/loaders/sigrok.py +201 -78
- oscura/loaders/tdms.py +81 -58
- oscura/loaders/tektronix.py +291 -174
- oscura/loaders/touchstone.py +182 -87
- oscura/loaders/tss.py +456 -0
- oscura/loaders/vcd.py +215 -117
- oscura/loaders/wav.py +155 -68
- oscura/reporting/__init__.py +9 -0
- oscura/reporting/analyze.py +352 -146
- oscura/reporting/argument_preparer.py +69 -14
- oscura/reporting/auto_report.py +97 -61
- oscura/reporting/batch.py +131 -58
- oscura/reporting/chart_selection.py +57 -45
- oscura/reporting/comparison.py +63 -17
- oscura/reporting/content/executive.py +76 -24
- oscura/reporting/core_formats/multi_format.py +11 -8
- oscura/reporting/engine.py +312 -158
- oscura/reporting/enhanced_reports.py +949 -0
- oscura/reporting/export.py +86 -43
- oscura/reporting/formatting/numbers.py +69 -42
- oscura/reporting/html.py +139 -58
- oscura/reporting/index.py +137 -65
- oscura/reporting/output.py +158 -67
- oscura/reporting/pdf.py +67 -102
- oscura/reporting/plots.py +191 -112
- oscura/reporting/sections.py +88 -47
- oscura/reporting/standards.py +104 -61
- oscura/reporting/summary_generator.py +75 -55
- oscura/reporting/tables.py +138 -54
- oscura/reporting/templates/enhanced/protocol_re.html +525 -0
- oscura/sessions/__init__.py +14 -23
- oscura/sessions/base.py +3 -3
- oscura/sessions/blackbox.py +106 -10
- oscura/sessions/generic.py +2 -2
- oscura/sessions/legacy.py +783 -0
- oscura/side_channel/__init__.py +63 -0
- oscura/side_channel/dpa.py +1025 -0
- oscura/utils/__init__.py +15 -1
- oscura/utils/bitwise.py +118 -0
- oscura/{builders → utils/builders}/__init__.py +1 -1
- oscura/{comparison → utils/comparison}/__init__.py +6 -6
- oscura/{comparison → utils/comparison}/compare.py +202 -101
- oscura/{comparison → utils/comparison}/golden.py +83 -63
- oscura/{comparison → utils/comparison}/limits.py +313 -89
- oscura/{comparison → utils/comparison}/mask.py +151 -45
- oscura/{comparison → utils/comparison}/trace_diff.py +1 -1
- oscura/{comparison → utils/comparison}/visualization.py +147 -89
- oscura/{component → utils/component}/__init__.py +3 -3
- oscura/{component → utils/component}/impedance.py +122 -58
- oscura/{component → utils/component}/reactive.py +165 -168
- oscura/{component → utils/component}/transmission_line.py +3 -3
- oscura/{filtering → utils/filtering}/__init__.py +6 -6
- oscura/{filtering → utils/filtering}/base.py +1 -1
- oscura/{filtering → utils/filtering}/convenience.py +2 -2
- oscura/{filtering → utils/filtering}/design.py +169 -93
- oscura/{filtering → utils/filtering}/filters.py +2 -2
- oscura/{filtering → utils/filtering}/introspection.py +2 -2
- oscura/utils/geometry.py +31 -0
- oscura/utils/imports.py +184 -0
- oscura/utils/lazy.py +1 -1
- oscura/{math → utils/math}/__init__.py +2 -2
- oscura/{math → utils/math}/arithmetic.py +114 -48
- oscura/{math → utils/math}/interpolation.py +139 -106
- oscura/utils/memory.py +129 -66
- oscura/utils/memory_advanced.py +92 -9
- oscura/utils/memory_extensions.py +10 -8
- oscura/{optimization → utils/optimization}/__init__.py +1 -1
- oscura/{optimization → utils/optimization}/search.py +2 -2
- oscura/utils/performance/__init__.py +58 -0
- oscura/utils/performance/caching.py +889 -0
- oscura/utils/performance/lsh_clustering.py +333 -0
- oscura/utils/performance/memory_optimizer.py +699 -0
- oscura/utils/performance/optimizations.py +675 -0
- oscura/utils/performance/parallel.py +654 -0
- oscura/utils/performance/profiling.py +661 -0
- oscura/{pipeline → utils/pipeline}/base.py +1 -1
- oscura/{pipeline → utils/pipeline}/composition.py +1 -1
- oscura/{pipeline → utils/pipeline}/parallel.py +3 -2
- oscura/{pipeline → utils/pipeline}/pipeline.py +1 -1
- oscura/{pipeline → utils/pipeline}/reverse_engineering.py +412 -221
- oscura/{search → utils/search}/__init__.py +3 -3
- oscura/{search → utils/search}/anomaly.py +188 -58
- oscura/utils/search/context.py +294 -0
- oscura/{search → utils/search}/pattern.py +138 -10
- oscura/utils/serial.py +51 -0
- oscura/utils/storage/__init__.py +61 -0
- oscura/utils/storage/database.py +1166 -0
- oscura/{streaming → utils/streaming}/chunked.py +302 -143
- oscura/{streaming → utils/streaming}/progressive.py +1 -1
- oscura/{streaming → utils/streaming}/realtime.py +3 -2
- oscura/{triggering → utils/triggering}/__init__.py +6 -6
- oscura/{triggering → utils/triggering}/base.py +6 -6
- oscura/{triggering → utils/triggering}/edge.py +2 -2
- oscura/{triggering → utils/triggering}/pattern.py +2 -2
- oscura/{triggering → utils/triggering}/pulse.py +115 -74
- oscura/{triggering → utils/triggering}/window.py +2 -2
- oscura/utils/validation.py +32 -0
- oscura/validation/__init__.py +121 -0
- oscura/{compliance → validation/compliance}/__init__.py +5 -5
- oscura/{compliance → validation/compliance}/advanced.py +5 -5
- oscura/{compliance → validation/compliance}/masks.py +1 -1
- oscura/{compliance → validation/compliance}/reporting.py +127 -53
- oscura/{compliance → validation/compliance}/testing.py +114 -52
- oscura/validation/compliance_tests.py +915 -0
- oscura/validation/fuzzer.py +990 -0
- oscura/validation/grammar_tests.py +596 -0
- oscura/validation/grammar_validator.py +904 -0
- oscura/validation/hil_testing.py +977 -0
- oscura/{quality → validation/quality}/__init__.py +4 -4
- oscura/{quality → validation/quality}/ensemble.py +251 -171
- oscura/{quality → validation/quality}/explainer.py +3 -3
- oscura/{quality → validation/quality}/scoring.py +1 -1
- oscura/{quality → validation/quality}/warnings.py +4 -4
- oscura/validation/regression_suite.py +808 -0
- oscura/validation/replay.py +788 -0
- oscura/{testing → validation/testing}/__init__.py +2 -2
- oscura/{testing → validation/testing}/synthetic.py +5 -5
- oscura/visualization/__init__.py +9 -0
- oscura/visualization/accessibility.py +1 -1
- oscura/visualization/annotations.py +64 -67
- oscura/visualization/colors.py +7 -7
- oscura/visualization/digital.py +180 -81
- oscura/visualization/eye.py +236 -85
- oscura/visualization/interactive.py +320 -143
- oscura/visualization/jitter.py +587 -247
- oscura/visualization/layout.py +169 -134
- oscura/visualization/optimization.py +103 -52
- oscura/visualization/palettes.py +1 -1
- oscura/visualization/power.py +427 -211
- oscura/visualization/power_extended.py +626 -297
- oscura/visualization/presets.py +2 -0
- oscura/visualization/protocols.py +495 -181
- oscura/visualization/render.py +79 -63
- oscura/visualization/reverse_engineering.py +171 -124
- oscura/visualization/signal_integrity.py +460 -279
- oscura/visualization/specialized.py +190 -100
- oscura/visualization/spectral.py +670 -255
- oscura/visualization/thumbnails.py +166 -137
- oscura/visualization/waveform.py +150 -63
- oscura/workflows/__init__.py +3 -0
- oscura/{batch → workflows/batch}/__init__.py +5 -5
- oscura/{batch → workflows/batch}/advanced.py +150 -75
- oscura/workflows/batch/aggregate.py +531 -0
- oscura/workflows/batch/analyze.py +236 -0
- oscura/{batch → workflows/batch}/logging.py +2 -2
- oscura/{batch → workflows/batch}/metrics.py +1 -1
- oscura/workflows/complete_re.py +1144 -0
- oscura/workflows/compliance.py +44 -54
- oscura/workflows/digital.py +197 -51
- oscura/workflows/legacy/__init__.py +12 -0
- oscura/{workflow → workflows/legacy}/dag.py +4 -1
- oscura/workflows/multi_trace.py +9 -9
- oscura/workflows/power.py +42 -62
- oscura/workflows/protocol.py +82 -49
- oscura/workflows/reverse_engineering.py +351 -150
- oscura/workflows/signal_integrity.py +157 -82
- oscura-0.7.0.dist-info/METADATA +661 -0
- oscura-0.7.0.dist-info/RECORD +591 -0
- oscura/batch/aggregate.py +0 -300
- oscura/batch/analyze.py +0 -139
- oscura/dsl/__init__.py +0 -73
- oscura/exceptions.py +0 -59
- oscura/exploratory/fuzzy.py +0 -513
- oscura/exploratory/sync.py +0 -384
- oscura/exporters/__init__.py +0 -94
- oscura/exporters/csv.py +0 -303
- oscura/exporters/exporters.py +0 -44
- oscura/exporters/hdf5.py +0 -217
- oscura/exporters/html_export.py +0 -701
- oscura/exporters/json_export.py +0 -291
- oscura/exporters/markdown_export.py +0 -367
- oscura/exporters/matlab_export.py +0 -354
- oscura/exporters/npz_export.py +0 -219
- oscura/exporters/spice_export.py +0 -210
- oscura/search/context.py +0 -149
- oscura/session/__init__.py +0 -34
- oscura/session/annotations.py +0 -289
- oscura/session/history.py +0 -313
- oscura/session/session.py +0 -520
- oscura/workflow/__init__.py +0 -13
- oscura-0.5.1.dist-info/METADATA +0 -583
- oscura-0.5.1.dist-info/RECORD +0 -481
- /oscura/core/{config.py → config/legacy.py} +0 -0
- /oscura/{extensibility → core/extensibility}/__init__.py +0 -0
- /oscura/{extensibility → core/extensibility}/registry.py +0 -0
- /oscura/{plugins → core/plugins}/isolation.py +0 -0
- /oscura/{schemas → core/schemas}/bus_configuration.json +0 -0
- /oscura/{builders → utils/builders}/signal_builder.py +0 -0
- /oscura/{optimization → utils/optimization}/parallel.py +0 -0
- /oscura/{pipeline → utils/pipeline}/__init__.py +0 -0
- /oscura/{streaming → utils/streaming}/__init__.py +0 -0
- {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/WHEEL +0 -0
- {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/entry_points.txt +0 -0
- {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/licenses/LICENSE +0 -0
oscura/loaders/pcap.py
CHANGED
|
@@ -27,7 +27,7 @@ if TYPE_CHECKING:
|
|
|
27
27
|
|
|
28
28
|
# Try to import dpkt for full PCAP support
|
|
29
29
|
try:
|
|
30
|
-
import dpkt
|
|
30
|
+
import dpkt
|
|
31
31
|
|
|
32
32
|
DPKT_AVAILABLE = True
|
|
33
33
|
except ImportError:
|
|
@@ -160,6 +160,135 @@ def load_pcap(
|
|
|
160
160
|
)
|
|
161
161
|
|
|
162
162
|
|
|
163
|
+
def _create_pcap_reader(f: Any, path: Path) -> Any:
|
|
164
|
+
"""Create appropriate PCAP reader based on file format.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
f: File handle.
|
|
168
|
+
path: Path to PCAP file.
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
dpkt PCAP or PCAPNG reader.
|
|
172
|
+
|
|
173
|
+
Raises:
|
|
174
|
+
LoaderError: If PCAPNG support is unavailable.
|
|
175
|
+
"""
|
|
176
|
+
magic = f.read(4)
|
|
177
|
+
f.seek(0)
|
|
178
|
+
magic_int = struct.unpack("<I", magic)[0]
|
|
179
|
+
|
|
180
|
+
if magic_int == PCAPNG_MAGIC:
|
|
181
|
+
try:
|
|
182
|
+
return dpkt.pcapng.Reader(f)
|
|
183
|
+
except AttributeError:
|
|
184
|
+
raise LoaderError(
|
|
185
|
+
"PCAPNG support requires newer dpkt version",
|
|
186
|
+
file_path=str(path),
|
|
187
|
+
fix_hint="Install dpkt >= 1.9: pip install dpkt>=1.9",
|
|
188
|
+
)
|
|
189
|
+
else:
|
|
190
|
+
return dpkt.pcap.Reader(f)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _parse_transport_layer(ip: Any, annotations: dict[str, Any]) -> str:
|
|
194
|
+
"""Parse TCP/UDP/ICMP transport layer from IP packet.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
ip: dpkt IP object.
|
|
198
|
+
annotations: Annotations dictionary to populate.
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
Protocol name ("TCP", "UDP", "ICMP", or "IP").
|
|
202
|
+
"""
|
|
203
|
+
if isinstance(ip.data, dpkt.tcp.TCP):
|
|
204
|
+
tcp = ip.data
|
|
205
|
+
annotations["src_port"] = tcp.sport
|
|
206
|
+
annotations["dst_port"] = tcp.dport
|
|
207
|
+
annotations["layer4_protocol"] = "TCP"
|
|
208
|
+
annotations["tcp_flags"] = tcp.flags
|
|
209
|
+
return "TCP"
|
|
210
|
+
|
|
211
|
+
elif isinstance(ip.data, dpkt.udp.UDP):
|
|
212
|
+
udp = ip.data
|
|
213
|
+
annotations["src_port"] = udp.sport
|
|
214
|
+
annotations["dst_port"] = udp.dport
|
|
215
|
+
annotations["layer4_protocol"] = "UDP"
|
|
216
|
+
return "UDP"
|
|
217
|
+
|
|
218
|
+
elif isinstance(ip.data, dpkt.icmp.ICMP):
|
|
219
|
+
annotations["layer4_protocol"] = "ICMP"
|
|
220
|
+
return "ICMP"
|
|
221
|
+
|
|
222
|
+
return "IP"
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _parse_ethernet_frame(raw_data: bytes, link_type: int) -> tuple[str, dict[str, Any]]:
|
|
226
|
+
"""Parse Ethernet frame and extract protocol information.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
raw_data: Raw packet bytes.
|
|
230
|
+
link_type: Link layer type.
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
Tuple of (protocol_name, annotations_dict).
|
|
234
|
+
"""
|
|
235
|
+
annotations: dict[str, Any] = {}
|
|
236
|
+
protocol = "RAW"
|
|
237
|
+
|
|
238
|
+
try:
|
|
239
|
+
if link_type != 1: # Not Ethernet
|
|
240
|
+
return protocol, annotations
|
|
241
|
+
|
|
242
|
+
eth = dpkt.ethernet.Ethernet(raw_data)
|
|
243
|
+
annotations["src_mac"] = _format_mac(eth.src)
|
|
244
|
+
annotations["dst_mac"] = _format_mac(eth.dst)
|
|
245
|
+
|
|
246
|
+
# Parse network layer
|
|
247
|
+
if isinstance(eth.data, dpkt.ip.IP):
|
|
248
|
+
ip = eth.data
|
|
249
|
+
annotations["src_ip"] = _format_ip(ip.src)
|
|
250
|
+
annotations["dst_ip"] = _format_ip(ip.dst)
|
|
251
|
+
annotations["layer3_protocol"] = "IP"
|
|
252
|
+
protocol = _parse_transport_layer(ip, annotations)
|
|
253
|
+
|
|
254
|
+
elif isinstance(eth.data, dpkt.ip6.IP6):
|
|
255
|
+
protocol = "IPv6"
|
|
256
|
+
annotations["layer3_protocol"] = "IPv6"
|
|
257
|
+
|
|
258
|
+
elif isinstance(eth.data, dpkt.arp.ARP):
|
|
259
|
+
protocol = "ARP"
|
|
260
|
+
annotations["layer3_protocol"] = "ARP"
|
|
261
|
+
|
|
262
|
+
except Exception:
|
|
263
|
+
# If parsing fails, return defaults
|
|
264
|
+
pass
|
|
265
|
+
|
|
266
|
+
return protocol, annotations
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _matches_protocol_filter(
|
|
270
|
+
protocol: str, annotations: dict[str, Any], protocol_filter: str | None
|
|
271
|
+
) -> bool:
|
|
272
|
+
"""Check if packet matches protocol filter.
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
protocol: Packet protocol name.
|
|
276
|
+
annotations: Packet annotations.
|
|
277
|
+
protocol_filter: Filter string.
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
True if packet matches filter (or no filter set).
|
|
281
|
+
"""
|
|
282
|
+
if protocol_filter is None:
|
|
283
|
+
return True
|
|
284
|
+
|
|
285
|
+
return (
|
|
286
|
+
annotations.get("layer3_protocol") == protocol_filter
|
|
287
|
+
or annotations.get("layer4_protocol") == protocol_filter
|
|
288
|
+
or protocol == protocol_filter
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
|
|
163
292
|
def _load_with_dpkt(
|
|
164
293
|
path: Path,
|
|
165
294
|
*,
|
|
@@ -180,27 +309,8 @@ def _load_with_dpkt(
|
|
|
180
309
|
LoaderError: If file cannot be read or dpkt version is incompatible.
|
|
181
310
|
"""
|
|
182
311
|
try:
|
|
183
|
-
with open(path, "rb") as f:
|
|
184
|
-
|
|
185
|
-
magic = f.read(4)
|
|
186
|
-
f.seek(0)
|
|
187
|
-
|
|
188
|
-
magic_int = struct.unpack("<I", magic)[0]
|
|
189
|
-
|
|
190
|
-
if magic_int == PCAPNG_MAGIC:
|
|
191
|
-
# PCAPNG format
|
|
192
|
-
try:
|
|
193
|
-
pcap_reader = dpkt.pcapng.Reader(f)
|
|
194
|
-
except AttributeError:
|
|
195
|
-
raise LoaderError( # noqa: B904
|
|
196
|
-
"PCAPNG support requires newer dpkt version",
|
|
197
|
-
file_path=str(path),
|
|
198
|
-
fix_hint="Install dpkt >= 1.9: pip install dpkt>=1.9",
|
|
199
|
-
)
|
|
200
|
-
else:
|
|
201
|
-
# Standard PCAP format
|
|
202
|
-
pcap_reader = dpkt.pcap.Reader(f)
|
|
203
|
-
|
|
312
|
+
with open(path, "rb", buffering=65536) as f:
|
|
313
|
+
pcap_reader = _create_pcap_reader(f, path)
|
|
204
314
|
packets: list[ProtocolPacket] = []
|
|
205
315
|
link_type = getattr(pcap_reader, "datalink", lambda: 1)()
|
|
206
316
|
|
|
@@ -208,62 +318,9 @@ def _load_with_dpkt(
|
|
|
208
318
|
if max_packets is not None and len(packets) >= max_packets:
|
|
209
319
|
break
|
|
210
320
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
protocol
|
|
214
|
-
|
|
215
|
-
try:
|
|
216
|
-
if link_type == 1: # Ethernet
|
|
217
|
-
eth = dpkt.ethernet.Ethernet(raw_data)
|
|
218
|
-
annotations["src_mac"] = _format_mac(eth.src)
|
|
219
|
-
annotations["dst_mac"] = _format_mac(eth.dst)
|
|
220
|
-
|
|
221
|
-
# Parse IP layer
|
|
222
|
-
if isinstance(eth.data, dpkt.ip.IP):
|
|
223
|
-
ip = eth.data
|
|
224
|
-
protocol = "IP"
|
|
225
|
-
annotations["src_ip"] = _format_ip(ip.src)
|
|
226
|
-
annotations["dst_ip"] = _format_ip(ip.dst)
|
|
227
|
-
annotations["layer3_protocol"] = "IP"
|
|
228
|
-
|
|
229
|
-
# Parse transport layer
|
|
230
|
-
if isinstance(ip.data, dpkt.tcp.TCP):
|
|
231
|
-
tcp = ip.data
|
|
232
|
-
protocol = "TCP"
|
|
233
|
-
annotations["src_port"] = tcp.sport
|
|
234
|
-
annotations["dst_port"] = tcp.dport
|
|
235
|
-
annotations["layer4_protocol"] = "TCP"
|
|
236
|
-
annotations["tcp_flags"] = tcp.flags
|
|
237
|
-
|
|
238
|
-
elif isinstance(ip.data, dpkt.udp.UDP):
|
|
239
|
-
udp = ip.data
|
|
240
|
-
protocol = "UDP"
|
|
241
|
-
annotations["src_port"] = udp.sport
|
|
242
|
-
annotations["dst_port"] = udp.dport
|
|
243
|
-
annotations["layer4_protocol"] = "UDP"
|
|
244
|
-
|
|
245
|
-
elif isinstance(ip.data, dpkt.icmp.ICMP):
|
|
246
|
-
protocol = "ICMP"
|
|
247
|
-
annotations["layer4_protocol"] = "ICMP"
|
|
248
|
-
|
|
249
|
-
elif isinstance(eth.data, dpkt.ip6.IP6):
|
|
250
|
-
protocol = "IPv6"
|
|
251
|
-
annotations["layer3_protocol"] = "IPv6"
|
|
252
|
-
|
|
253
|
-
elif isinstance(eth.data, dpkt.arp.ARP):
|
|
254
|
-
protocol = "ARP"
|
|
255
|
-
annotations["layer3_protocol"] = "ARP"
|
|
256
|
-
|
|
257
|
-
except Exception:
|
|
258
|
-
# If parsing fails, store raw data
|
|
259
|
-
pass
|
|
260
|
-
|
|
261
|
-
# Apply protocol filter
|
|
262
|
-
if protocol_filter is not None and (
|
|
263
|
-
annotations.get("layer3_protocol") != protocol_filter
|
|
264
|
-
and annotations.get("layer4_protocol") != protocol_filter
|
|
265
|
-
and protocol != protocol_filter
|
|
266
|
-
):
|
|
321
|
+
protocol, annotations = _parse_ethernet_frame(raw_data, link_type)
|
|
322
|
+
|
|
323
|
+
if not _matches_protocol_filter(protocol, annotations, protocol_filter):
|
|
267
324
|
continue
|
|
268
325
|
|
|
269
326
|
packet = ProtocolPacket(
|
|
@@ -312,75 +369,9 @@ def _load_basic(
|
|
|
312
369
|
LoaderError: If file cannot be read.
|
|
313
370
|
"""
|
|
314
371
|
try:
|
|
315
|
-
with open(path, "rb") as f:
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
if len(header) < 24:
|
|
319
|
-
raise FormatError(
|
|
320
|
-
"File too small to be a valid PCAP",
|
|
321
|
-
file_path=str(path),
|
|
322
|
-
expected="At least 24 bytes",
|
|
323
|
-
got=f"{len(header)} bytes",
|
|
324
|
-
)
|
|
325
|
-
|
|
326
|
-
# Parse magic number
|
|
327
|
-
magic = struct.unpack("<I", header[:4])[0]
|
|
328
|
-
|
|
329
|
-
if magic in (PCAP_MAGIC_LE, PCAP_MAGIC_NS_LE):
|
|
330
|
-
byte_order = "<"
|
|
331
|
-
nanosecond = magic == PCAP_MAGIC_NS_LE
|
|
332
|
-
elif magic in (PCAP_MAGIC_BE, PCAP_MAGIC_NS_BE):
|
|
333
|
-
byte_order = ">"
|
|
334
|
-
nanosecond = magic == PCAP_MAGIC_NS_BE
|
|
335
|
-
elif magic == PCAPNG_MAGIC:
|
|
336
|
-
raise LoaderError(
|
|
337
|
-
"PCAPNG format requires dpkt library",
|
|
338
|
-
file_path=str(path),
|
|
339
|
-
fix_hint="Install dpkt: pip install dpkt",
|
|
340
|
-
)
|
|
341
|
-
else:
|
|
342
|
-
raise FormatError(
|
|
343
|
-
"Invalid PCAP magic number",
|
|
344
|
-
file_path=str(path),
|
|
345
|
-
expected="PCAP magic (0xa1b2c3d4)",
|
|
346
|
-
got=f"0x{magic:08x}",
|
|
347
|
-
)
|
|
348
|
-
|
|
349
|
-
# Parse rest of header (version_major, version_minor, thiszone, sigfigs, snaplen, network)
|
|
350
|
-
_, _, _, _, snaplen, link_type = struct.unpack(f"{byte_order}HHiIII", header[4:])
|
|
351
|
-
|
|
352
|
-
packets: list[ProtocolPacket] = []
|
|
353
|
-
|
|
354
|
-
# Read packets
|
|
355
|
-
while True:
|
|
356
|
-
if max_packets is not None and len(packets) >= max_packets:
|
|
357
|
-
break
|
|
358
|
-
|
|
359
|
-
# Read packet header (16 bytes)
|
|
360
|
-
pkt_header = f.read(16)
|
|
361
|
-
if len(pkt_header) < 16:
|
|
362
|
-
break
|
|
363
|
-
|
|
364
|
-
ts_sec, ts_usec, incl_len, orig_len = struct.unpack(f"{byte_order}IIII", pkt_header)
|
|
365
|
-
|
|
366
|
-
# Calculate timestamp
|
|
367
|
-
if nanosecond:
|
|
368
|
-
timestamp = ts_sec + ts_usec / 1e9
|
|
369
|
-
else:
|
|
370
|
-
timestamp = ts_sec + ts_usec / 1e6
|
|
371
|
-
|
|
372
|
-
# Read packet data
|
|
373
|
-
pkt_data = f.read(incl_len)
|
|
374
|
-
if len(pkt_data) < incl_len:
|
|
375
|
-
break
|
|
376
|
-
|
|
377
|
-
packet = ProtocolPacket(
|
|
378
|
-
timestamp=timestamp,
|
|
379
|
-
protocol="RAW",
|
|
380
|
-
data=bytes(pkt_data),
|
|
381
|
-
annotations={"original_length": orig_len},
|
|
382
|
-
)
|
|
383
|
-
packets.append(packet)
|
|
372
|
+
with open(path, "rb", buffering=65536) as f:
|
|
373
|
+
byte_order, nanosecond, snaplen, link_type = _parse_pcap_header(f, path)
|
|
374
|
+
packets = _read_pcap_packets(f, byte_order, nanosecond, max_packets)
|
|
384
375
|
|
|
385
376
|
return PcapPacketList(
|
|
386
377
|
packets=packets,
|
|
@@ -390,10 +381,7 @@ def _load_basic(
|
|
|
390
381
|
)
|
|
391
382
|
|
|
392
383
|
except struct.error as e:
|
|
393
|
-
raise FormatError(
|
|
394
|
-
"Corrupted PCAP file",
|
|
395
|
-
file_path=str(path),
|
|
396
|
-
) from e
|
|
384
|
+
raise FormatError("Corrupted PCAP file", file_path=str(path)) from e
|
|
397
385
|
except Exception as e:
|
|
398
386
|
if isinstance(e, LoaderError | FormatError):
|
|
399
387
|
raise
|
|
@@ -405,6 +393,93 @@ def _load_basic(
|
|
|
405
393
|
) from e
|
|
406
394
|
|
|
407
395
|
|
|
396
|
+
def _parse_pcap_header(f: Any, path: Path) -> tuple[str, bool, int, int]:
|
|
397
|
+
"""Parse PCAP global header and return format info."""
|
|
398
|
+
header = f.read(24)
|
|
399
|
+
if len(header) < 24:
|
|
400
|
+
raise FormatError(
|
|
401
|
+
"File too small to be a valid PCAP",
|
|
402
|
+
file_path=str(path),
|
|
403
|
+
expected="At least 24 bytes",
|
|
404
|
+
got=f"{len(header)} bytes",
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
# Parse magic number
|
|
408
|
+
magic = struct.unpack("<I", header[:4])[0]
|
|
409
|
+
byte_order, nanosecond = _determine_byte_order(magic, path)
|
|
410
|
+
|
|
411
|
+
# Parse rest of header
|
|
412
|
+
_, _, _, _, snaplen, link_type = struct.unpack(f"{byte_order}HHiIII", header[4:])
|
|
413
|
+
return byte_order, nanosecond, snaplen, link_type
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def _determine_byte_order(magic: int, path: Path) -> tuple[str, bool]:
|
|
417
|
+
"""Determine byte order and timestamp precision from magic number."""
|
|
418
|
+
if magic in (PCAP_MAGIC_LE, PCAP_MAGIC_NS_LE):
|
|
419
|
+
return "<", magic == PCAP_MAGIC_NS_LE
|
|
420
|
+
elif magic in (PCAP_MAGIC_BE, PCAP_MAGIC_NS_BE):
|
|
421
|
+
return ">", magic == PCAP_MAGIC_NS_BE
|
|
422
|
+
elif magic == PCAPNG_MAGIC:
|
|
423
|
+
raise LoaderError(
|
|
424
|
+
"PCAPNG format requires dpkt library",
|
|
425
|
+
file_path=str(path),
|
|
426
|
+
fix_hint="Install dpkt: pip install dpkt",
|
|
427
|
+
)
|
|
428
|
+
else:
|
|
429
|
+
raise FormatError(
|
|
430
|
+
"Invalid PCAP magic number",
|
|
431
|
+
file_path=str(path),
|
|
432
|
+
expected="PCAP magic (0xa1b2c3d4)",
|
|
433
|
+
got=f"0x{magic:08x}",
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def _read_pcap_packets(
|
|
438
|
+
f: Any, byte_order: str, nanosecond: bool, max_packets: int | None
|
|
439
|
+
) -> list[ProtocolPacket]:
|
|
440
|
+
"""Read all packets from PCAP file."""
|
|
441
|
+
DEFAULT_MAX_PACKETS = 1_000_000 # Prevent memory exhaustion on unbounded files
|
|
442
|
+
|
|
443
|
+
packets: list[ProtocolPacket] = []
|
|
444
|
+
effective_max = max_packets if max_packets is not None else DEFAULT_MAX_PACKETS
|
|
445
|
+
|
|
446
|
+
while True:
|
|
447
|
+
if len(packets) >= effective_max:
|
|
448
|
+
break
|
|
449
|
+
|
|
450
|
+
packet = _read_one_packet(f, byte_order, nanosecond)
|
|
451
|
+
if packet is None:
|
|
452
|
+
break
|
|
453
|
+
|
|
454
|
+
packets.append(packet)
|
|
455
|
+
|
|
456
|
+
return packets
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def _read_one_packet(f: Any, byte_order: str, nanosecond: bool) -> ProtocolPacket | None:
|
|
460
|
+
"""Read one packet from PCAP file."""
|
|
461
|
+
pkt_header = f.read(16)
|
|
462
|
+
if len(pkt_header) < 16:
|
|
463
|
+
return None
|
|
464
|
+
|
|
465
|
+
ts_sec, ts_usec, incl_len, orig_len = struct.unpack(f"{byte_order}IIII", pkt_header)
|
|
466
|
+
|
|
467
|
+
# Calculate timestamp
|
|
468
|
+
timestamp = ts_sec + (ts_usec / 1e9 if nanosecond else ts_usec / 1e6)
|
|
469
|
+
|
|
470
|
+
# Read packet data
|
|
471
|
+
pkt_data = f.read(incl_len)
|
|
472
|
+
if len(pkt_data) < incl_len:
|
|
473
|
+
return None
|
|
474
|
+
|
|
475
|
+
return ProtocolPacket(
|
|
476
|
+
timestamp=timestamp,
|
|
477
|
+
protocol="RAW",
|
|
478
|
+
data=bytes(pkt_data),
|
|
479
|
+
annotations={"original_length": orig_len},
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
|
|
408
483
|
def _format_mac(mac_bytes: bytes) -> str:
|
|
409
484
|
"""Format MAC address bytes to string.
|
|
410
485
|
|
oscura/loaders/rigol.py
CHANGED
|
@@ -16,6 +16,7 @@ from pathlib import Path
|
|
|
16
16
|
from typing import TYPE_CHECKING, Any
|
|
17
17
|
|
|
18
18
|
import numpy as np
|
|
19
|
+
from numpy.typing import NDArray
|
|
19
20
|
|
|
20
21
|
from oscura.core.exceptions import FormatError, LoaderError
|
|
21
22
|
from oscura.core.types import TraceMetadata, WaveformTrace
|
|
@@ -25,7 +26,7 @@ if TYPE_CHECKING:
|
|
|
25
26
|
|
|
26
27
|
# Try to import RigolWFM for full Rigol support
|
|
27
28
|
try:
|
|
28
|
-
import RigolWFM.wfm as rigol_wfm # type: ignore[import-
|
|
29
|
+
import RigolWFM.wfm as rigol_wfm # type: ignore[import-untyped] # Optional third-party library
|
|
29
30
|
|
|
30
31
|
RIGOL_WFM_AVAILABLE = True
|
|
31
32
|
except ImportError:
|
|
@@ -100,49 +101,13 @@ def _load_with_rigolwfm(
|
|
|
100
101
|
LoaderError: If the file cannot be loaded.
|
|
101
102
|
"""
|
|
102
103
|
try:
|
|
103
|
-
#
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
model = "E"
|
|
111
|
-
|
|
112
|
-
# Try model detection, fallback to trying both models
|
|
113
|
-
last_error = None
|
|
114
|
-
for try_model in [model] if model else ["Z", "E"]:
|
|
115
|
-
try:
|
|
116
|
-
wfm = rigol_wfm.Wfm.from_file(str(path), model=try_model)
|
|
117
|
-
break
|
|
118
|
-
except Exception as e:
|
|
119
|
-
last_error = e
|
|
120
|
-
continue
|
|
121
|
-
else:
|
|
122
|
-
# None of the models worked
|
|
123
|
-
raise last_error if last_error else RuntimeError("Failed to load WFM file")
|
|
124
|
-
|
|
125
|
-
# Get channel data
|
|
126
|
-
if hasattr(wfm, "channels") and len(wfm.channels) > channel:
|
|
127
|
-
ch = wfm.channels[channel]
|
|
128
|
-
data = np.array(ch.volts, dtype=np.float64)
|
|
129
|
-
sample_rate = wfm.sample_rate if hasattr(wfm, "sample_rate") else 1e6
|
|
130
|
-
vertical_scale = ch.volts_per_div if hasattr(ch, "volts_per_div") else None
|
|
131
|
-
vertical_offset = ch.volt_offset if hasattr(ch, "volt_offset") else None
|
|
132
|
-
channel_name = f"CH{channel + 1}"
|
|
133
|
-
elif hasattr(wfm, "volts"):
|
|
134
|
-
# Single channel format
|
|
135
|
-
data = np.array(wfm.volts, dtype=np.float64)
|
|
136
|
-
sample_rate = wfm.sample_rate if hasattr(wfm, "sample_rate") else 1e6
|
|
137
|
-
vertical_scale = wfm.volts_per_div if hasattr(wfm, "volts_per_div") else None
|
|
138
|
-
vertical_offset = wfm.volt_offset if hasattr(wfm, "volt_offset") else None
|
|
139
|
-
channel_name = "CH1"
|
|
140
|
-
else:
|
|
141
|
-
raise FormatError(
|
|
142
|
-
"No waveform data found in Rigol file",
|
|
143
|
-
file_path=str(path),
|
|
144
|
-
expected="Rigol channel data",
|
|
145
|
-
)
|
|
104
|
+
# Auto-detect model and load waveform
|
|
105
|
+
wfm = _load_rigol_with_model_detection(path)
|
|
106
|
+
|
|
107
|
+
# Extract channel data
|
|
108
|
+
data, sample_rate, vertical_scale, vertical_offset, channel_name = (
|
|
109
|
+
_extract_rigol_channel_data(wfm, channel, str(path))
|
|
110
|
+
)
|
|
146
111
|
|
|
147
112
|
# Build metadata
|
|
148
113
|
metadata = TraceMetadata(
|
|
@@ -157,12 +122,8 @@ def _load_with_rigolwfm(
|
|
|
157
122
|
return WaveformTrace(data=data, metadata=metadata)
|
|
158
123
|
|
|
159
124
|
except Exception as e:
|
|
160
|
-
# Re-raise FormatError as-is for tests that expect it
|
|
161
|
-
# All other exceptions (including kaitaistruct errors) get wrapped
|
|
162
125
|
if isinstance(e, FormatError):
|
|
163
126
|
raise
|
|
164
|
-
# Wrap other exceptions in LoaderError
|
|
165
|
-
# The outer load_rigol_wfm() will catch LoaderError and fall back to basic loader
|
|
166
127
|
raise LoaderError(
|
|
167
128
|
"Failed to load Rigol WFM file with RigolWFM library",
|
|
168
129
|
file_path=str(path),
|
|
@@ -171,6 +132,106 @@ def _load_with_rigolwfm(
|
|
|
171
132
|
) from e
|
|
172
133
|
|
|
173
134
|
|
|
135
|
+
def _detect_rigol_model_from_filename(path: Path) -> str | None:
|
|
136
|
+
"""Auto-detect Rigol oscilloscope model from filename.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
path: Path to WFM file.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Model string ("Z" or "E") or None if detection fails.
|
|
143
|
+
"""
|
|
144
|
+
filename_upper = path.name.upper()
|
|
145
|
+
|
|
146
|
+
# Check for Rigol model indicators in filename
|
|
147
|
+
if "DS1" not in filename_upper and "MSO1" not in filename_upper and "DHO" not in filename_upper:
|
|
148
|
+
return None
|
|
149
|
+
|
|
150
|
+
# Z-series (DS1000Z, MSO1000Z, DHO series)
|
|
151
|
+
if "Z" in filename_upper or "MSO" in filename_upper or "DHO" in filename_upper:
|
|
152
|
+
return "Z"
|
|
153
|
+
|
|
154
|
+
# E-series (DS1000E)
|
|
155
|
+
if "E" in filename_upper:
|
|
156
|
+
return "E"
|
|
157
|
+
|
|
158
|
+
return None
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _load_rigol_with_model_detection(path: Path) -> Any:
|
|
162
|
+
"""Load Rigol WFM file with automatic model detection.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
path: Path to WFM file.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
Loaded waveform object from RigolWFM.
|
|
169
|
+
|
|
170
|
+
Raises:
|
|
171
|
+
RuntimeError: If loading fails with all models.
|
|
172
|
+
"""
|
|
173
|
+
model = _detect_rigol_model_from_filename(path)
|
|
174
|
+
|
|
175
|
+
# Try detected model first, then fallback to both models
|
|
176
|
+
models_to_try = [model] if model else ["Z", "E"]
|
|
177
|
+
|
|
178
|
+
last_error = None
|
|
179
|
+
for try_model in models_to_try:
|
|
180
|
+
try:
|
|
181
|
+
return rigol_wfm.Wfm.from_file(str(path), model=try_model)
|
|
182
|
+
except Exception as e:
|
|
183
|
+
last_error = e
|
|
184
|
+
continue
|
|
185
|
+
|
|
186
|
+
# None of the models worked
|
|
187
|
+
raise last_error if last_error else RuntimeError("Failed to load WFM file")
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _extract_rigol_channel_data(
|
|
191
|
+
wfm: Any,
|
|
192
|
+
channel: int,
|
|
193
|
+
file_path: str,
|
|
194
|
+
) -> tuple[NDArray[np.float64], float, float | None, float | None, str]:
|
|
195
|
+
"""Extract channel data from Rigol waveform object.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
wfm: Rigol waveform object from RigolWFM.
|
|
199
|
+
channel: Channel index.
|
|
200
|
+
file_path: File path for error messages.
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
Tuple of (data, sample_rate, vertical_scale, vertical_offset, channel_name).
|
|
204
|
+
|
|
205
|
+
Raises:
|
|
206
|
+
FormatError: If no waveform data found.
|
|
207
|
+
"""
|
|
208
|
+
# Multi-channel format
|
|
209
|
+
if hasattr(wfm, "channels") and len(wfm.channels) > channel:
|
|
210
|
+
ch = wfm.channels[channel]
|
|
211
|
+
data = np.array(ch.volts, dtype=np.float64)
|
|
212
|
+
sample_rate = wfm.sample_rate if hasattr(wfm, "sample_rate") else 1e6
|
|
213
|
+
vertical_scale = ch.volts_per_div if hasattr(ch, "volts_per_div") else None
|
|
214
|
+
vertical_offset = ch.volt_offset if hasattr(ch, "volt_offset") else None
|
|
215
|
+
channel_name = f"CH{channel + 1}"
|
|
216
|
+
return data, sample_rate, vertical_scale, vertical_offset, channel_name
|
|
217
|
+
|
|
218
|
+
# Single channel format
|
|
219
|
+
if hasattr(wfm, "volts"):
|
|
220
|
+
data = np.array(wfm.volts, dtype=np.float64)
|
|
221
|
+
sample_rate = wfm.sample_rate if hasattr(wfm, "sample_rate") else 1e6
|
|
222
|
+
vertical_scale = wfm.volts_per_div if hasattr(wfm, "volts_per_div") else None
|
|
223
|
+
vertical_offset = wfm.volt_offset if hasattr(wfm, "volt_offset") else None
|
|
224
|
+
channel_name = "CH1"
|
|
225
|
+
return data, sample_rate, vertical_scale, vertical_offset, channel_name
|
|
226
|
+
|
|
227
|
+
# No recognized format
|
|
228
|
+
raise FormatError(
|
|
229
|
+
"No waveform data found in Rigol file",
|
|
230
|
+
file_path=file_path,
|
|
231
|
+
expected="Rigol channel data",
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
|
|
174
235
|
def _load_basic(
|
|
175
236
|
path: Path,
|
|
176
237
|
*,
|
|
@@ -193,7 +254,7 @@ def _load_basic(
|
|
|
193
254
|
LoaderError: If the file cannot be read or parsed.
|
|
194
255
|
"""
|
|
195
256
|
try:
|
|
196
|
-
with open(path, "rb") as f:
|
|
257
|
+
with open(path, "rb", buffering=65536) as f:
|
|
197
258
|
# Read header
|
|
198
259
|
header = f.read(256)
|
|
199
260
|
|