oscura 0.5.1__py3-none-any.whl → 0.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- oscura/__init__.py +169 -167
- oscura/analyzers/__init__.py +3 -0
- oscura/analyzers/classification.py +659 -0
- oscura/analyzers/digital/edges.py +325 -65
- oscura/analyzers/digital/quality.py +293 -166
- oscura/analyzers/digital/timing.py +260 -115
- oscura/analyzers/digital/timing_numba.py +334 -0
- oscura/analyzers/entropy.py +605 -0
- oscura/analyzers/eye/diagram.py +176 -109
- oscura/analyzers/eye/metrics.py +5 -5
- oscura/analyzers/jitter/__init__.py +6 -4
- oscura/analyzers/jitter/ber.py +52 -52
- oscura/analyzers/jitter/classification.py +156 -0
- oscura/analyzers/jitter/decomposition.py +163 -113
- oscura/analyzers/jitter/spectrum.py +80 -64
- oscura/analyzers/ml/__init__.py +39 -0
- oscura/analyzers/ml/features.py +600 -0
- oscura/analyzers/ml/signal_classifier.py +604 -0
- oscura/analyzers/packet/daq.py +246 -158
- oscura/analyzers/packet/parser.py +12 -1
- oscura/analyzers/packet/payload.py +50 -2110
- oscura/analyzers/packet/payload_analysis.py +361 -181
- oscura/analyzers/packet/payload_patterns.py +133 -70
- oscura/analyzers/packet/stream.py +84 -23
- oscura/analyzers/patterns/__init__.py +26 -5
- oscura/analyzers/patterns/anomaly_detection.py +908 -0
- oscura/analyzers/patterns/clustering.py +169 -108
- oscura/analyzers/patterns/clustering_optimized.py +227 -0
- oscura/analyzers/patterns/discovery.py +1 -1
- oscura/analyzers/patterns/matching.py +581 -197
- oscura/analyzers/patterns/pattern_mining.py +778 -0
- oscura/analyzers/patterns/periodic.py +121 -38
- oscura/analyzers/patterns/sequences.py +175 -78
- oscura/analyzers/power/conduction.py +1 -1
- oscura/analyzers/power/soa.py +6 -6
- oscura/analyzers/power/switching.py +250 -110
- oscura/analyzers/protocol/__init__.py +17 -1
- oscura/analyzers/protocols/base.py +6 -6
- oscura/analyzers/protocols/ble/__init__.py +38 -0
- oscura/analyzers/protocols/ble/analyzer.py +809 -0
- oscura/analyzers/protocols/ble/uuids.py +288 -0
- oscura/analyzers/protocols/can.py +257 -127
- oscura/analyzers/protocols/can_fd.py +107 -80
- oscura/analyzers/protocols/flexray.py +139 -80
- oscura/analyzers/protocols/hdlc.py +93 -58
- oscura/analyzers/protocols/i2c.py +247 -106
- oscura/analyzers/protocols/i2s.py +138 -86
- oscura/analyzers/protocols/industrial/__init__.py +40 -0
- oscura/analyzers/protocols/industrial/bacnet/__init__.py +33 -0
- oscura/analyzers/protocols/industrial/bacnet/analyzer.py +708 -0
- oscura/analyzers/protocols/industrial/bacnet/encoding.py +412 -0
- oscura/analyzers/protocols/industrial/bacnet/services.py +622 -0
- oscura/analyzers/protocols/industrial/ethercat/__init__.py +30 -0
- oscura/analyzers/protocols/industrial/ethercat/analyzer.py +474 -0
- oscura/analyzers/protocols/industrial/ethercat/mailbox.py +339 -0
- oscura/analyzers/protocols/industrial/ethercat/topology.py +166 -0
- oscura/analyzers/protocols/industrial/modbus/__init__.py +31 -0
- oscura/analyzers/protocols/industrial/modbus/analyzer.py +525 -0
- oscura/analyzers/protocols/industrial/modbus/crc.py +79 -0
- oscura/analyzers/protocols/industrial/modbus/functions.py +436 -0
- oscura/analyzers/protocols/industrial/opcua/__init__.py +21 -0
- oscura/analyzers/protocols/industrial/opcua/analyzer.py +552 -0
- oscura/analyzers/protocols/industrial/opcua/datatypes.py +446 -0
- oscura/analyzers/protocols/industrial/opcua/services.py +264 -0
- oscura/analyzers/protocols/industrial/profinet/__init__.py +23 -0
- oscura/analyzers/protocols/industrial/profinet/analyzer.py +441 -0
- oscura/analyzers/protocols/industrial/profinet/dcp.py +263 -0
- oscura/analyzers/protocols/industrial/profinet/ptcp.py +200 -0
- oscura/analyzers/protocols/jtag.py +180 -98
- oscura/analyzers/protocols/lin.py +219 -114
- oscura/analyzers/protocols/manchester.py +4 -4
- oscura/analyzers/protocols/onewire.py +253 -149
- oscura/analyzers/protocols/parallel_bus/__init__.py +20 -0
- oscura/analyzers/protocols/parallel_bus/centronics.py +92 -0
- oscura/analyzers/protocols/parallel_bus/gpib.py +137 -0
- oscura/analyzers/protocols/spi.py +192 -95
- oscura/analyzers/protocols/swd.py +321 -167
- oscura/analyzers/protocols/uart.py +267 -125
- oscura/analyzers/protocols/usb.py +235 -131
- oscura/analyzers/side_channel/power.py +17 -12
- oscura/analyzers/signal/__init__.py +15 -0
- oscura/analyzers/signal/timing_analysis.py +1086 -0
- oscura/analyzers/signal_integrity/__init__.py +4 -1
- oscura/analyzers/signal_integrity/sparams.py +2 -19
- oscura/analyzers/spectral/chunked.py +129 -60
- oscura/analyzers/spectral/chunked_fft.py +300 -94
- oscura/analyzers/spectral/chunked_wavelet.py +100 -80
- oscura/analyzers/statistical/checksum.py +376 -217
- oscura/analyzers/statistical/classification.py +229 -107
- oscura/analyzers/statistical/entropy.py +78 -53
- oscura/analyzers/statistics/correlation.py +407 -211
- oscura/analyzers/statistics/outliers.py +2 -2
- oscura/analyzers/statistics/streaming.py +30 -5
- oscura/analyzers/validation.py +216 -101
- oscura/analyzers/waveform/measurements.py +9 -0
- oscura/analyzers/waveform/measurements_with_uncertainty.py +31 -15
- oscura/analyzers/waveform/spectral.py +500 -228
- oscura/api/__init__.py +31 -5
- oscura/api/dsl/__init__.py +582 -0
- oscura/{dsl → api/dsl}/commands.py +43 -76
- oscura/{dsl → api/dsl}/interpreter.py +26 -51
- oscura/{dsl → api/dsl}/parser.py +107 -77
- oscura/{dsl → api/dsl}/repl.py +2 -2
- oscura/api/dsl.py +1 -1
- oscura/{integrations → api/integrations}/__init__.py +1 -1
- oscura/{integrations → api/integrations}/llm.py +201 -102
- oscura/api/operators.py +3 -3
- oscura/api/optimization.py +144 -30
- oscura/api/rest_server.py +921 -0
- oscura/api/server/__init__.py +17 -0
- oscura/api/server/dashboard.py +850 -0
- oscura/api/server/static/README.md +34 -0
- oscura/api/server/templates/base.html +181 -0
- oscura/api/server/templates/export.html +120 -0
- oscura/api/server/templates/home.html +284 -0
- oscura/api/server/templates/protocols.html +58 -0
- oscura/api/server/templates/reports.html +43 -0
- oscura/api/server/templates/session_detail.html +89 -0
- oscura/api/server/templates/sessions.html +83 -0
- oscura/api/server/templates/waveforms.html +73 -0
- oscura/automotive/__init__.py +8 -1
- oscura/automotive/can/__init__.py +10 -0
- oscura/automotive/can/checksum.py +3 -1
- oscura/automotive/can/dbc_generator.py +590 -0
- oscura/automotive/can/message_wrapper.py +121 -74
- oscura/automotive/can/patterns.py +98 -21
- oscura/automotive/can/session.py +292 -56
- oscura/automotive/can/state_machine.py +6 -3
- oscura/automotive/can/stimulus_response.py +97 -75
- oscura/automotive/dbc/__init__.py +10 -2
- oscura/automotive/dbc/generator.py +84 -56
- oscura/automotive/dbc/parser.py +6 -6
- oscura/automotive/dtc/data.json +17 -102
- oscura/automotive/dtc/database.py +2 -2
- oscura/automotive/flexray/__init__.py +31 -0
- oscura/automotive/flexray/analyzer.py +504 -0
- oscura/automotive/flexray/crc.py +185 -0
- oscura/automotive/flexray/fibex.py +449 -0
- oscura/automotive/j1939/__init__.py +45 -8
- oscura/automotive/j1939/analyzer.py +605 -0
- oscura/automotive/j1939/spns.py +326 -0
- oscura/automotive/j1939/transport.py +306 -0
- oscura/automotive/lin/__init__.py +47 -0
- oscura/automotive/lin/analyzer.py +612 -0
- oscura/automotive/loaders/blf.py +13 -2
- oscura/automotive/loaders/csv_can.py +143 -72
- oscura/automotive/loaders/dispatcher.py +50 -2
- oscura/automotive/loaders/mdf.py +86 -45
- oscura/automotive/loaders/pcap.py +111 -61
- oscura/automotive/uds/__init__.py +4 -0
- oscura/automotive/uds/analyzer.py +725 -0
- oscura/automotive/uds/decoder.py +140 -58
- oscura/automotive/uds/models.py +7 -1
- oscura/automotive/visualization.py +1 -1
- oscura/cli/analyze.py +348 -0
- oscura/cli/batch.py +142 -122
- oscura/cli/benchmark.py +275 -0
- oscura/cli/characterize.py +137 -82
- oscura/cli/compare.py +224 -131
- oscura/cli/completion.py +250 -0
- oscura/cli/config_cmd.py +361 -0
- oscura/cli/decode.py +164 -87
- oscura/cli/export.py +286 -0
- oscura/cli/main.py +115 -31
- oscura/{onboarding → cli/onboarding}/__init__.py +3 -3
- oscura/{onboarding → cli/onboarding}/help.py +80 -58
- oscura/{onboarding → cli/onboarding}/tutorials.py +97 -72
- oscura/{onboarding → cli/onboarding}/wizard.py +55 -36
- oscura/cli/progress.py +147 -0
- oscura/cli/shell.py +157 -135
- oscura/cli/validate_cmd.py +204 -0
- oscura/cli/visualize.py +158 -0
- oscura/convenience.py +125 -79
- oscura/core/__init__.py +4 -2
- oscura/core/backend_selector.py +3 -3
- oscura/core/cache.py +126 -15
- oscura/core/cancellation.py +1 -1
- oscura/{config → core/config}/__init__.py +20 -11
- oscura/{config → core/config}/defaults.py +1 -1
- oscura/{config → core/config}/loader.py +7 -5
- oscura/{config → core/config}/memory.py +5 -5
- oscura/{config → core/config}/migration.py +1 -1
- oscura/{config → core/config}/pipeline.py +99 -23
- oscura/{config → core/config}/preferences.py +1 -1
- oscura/{config → core/config}/protocol.py +3 -3
- oscura/{config → core/config}/schema.py +426 -272
- oscura/{config → core/config}/settings.py +1 -1
- oscura/{config → core/config}/thresholds.py +195 -153
- oscura/core/correlation.py +5 -6
- oscura/core/cross_domain.py +0 -2
- oscura/core/debug.py +9 -5
- oscura/{extensibility → core/extensibility}/docs.py +158 -70
- oscura/{extensibility → core/extensibility}/extensions.py +160 -76
- oscura/{extensibility → core/extensibility}/logging.py +1 -1
- oscura/{extensibility → core/extensibility}/measurements.py +1 -1
- oscura/{extensibility → core/extensibility}/plugins.py +1 -1
- oscura/{extensibility → core/extensibility}/templates.py +73 -3
- oscura/{extensibility → core/extensibility}/validation.py +1 -1
- oscura/core/gpu_backend.py +11 -7
- oscura/core/log_query.py +101 -11
- oscura/core/logging.py +126 -54
- oscura/core/logging_advanced.py +5 -5
- oscura/core/memory_limits.py +108 -70
- oscura/core/memory_monitor.py +2 -2
- oscura/core/memory_progress.py +7 -7
- oscura/core/memory_warnings.py +1 -1
- oscura/core/numba_backend.py +13 -13
- oscura/{plugins → core/plugins}/__init__.py +9 -9
- oscura/{plugins → core/plugins}/base.py +7 -7
- oscura/{plugins → core/plugins}/cli.py +3 -3
- oscura/{plugins → core/plugins}/discovery.py +186 -106
- oscura/{plugins → core/plugins}/lifecycle.py +1 -1
- oscura/{plugins → core/plugins}/manager.py +7 -7
- oscura/{plugins → core/plugins}/registry.py +3 -3
- oscura/{plugins → core/plugins}/versioning.py +1 -1
- oscura/core/progress.py +16 -1
- oscura/core/provenance.py +8 -2
- oscura/{schemas → core/schemas}/__init__.py +2 -2
- oscura/{schemas → core/schemas}/device_mapping.json +2 -8
- oscura/{schemas → core/schemas}/packet_format.json +4 -24
- oscura/{schemas → core/schemas}/protocol_definition.json +2 -12
- oscura/core/types.py +4 -0
- oscura/core/uncertainty.py +3 -3
- oscura/correlation/__init__.py +52 -0
- oscura/correlation/multi_protocol.py +811 -0
- oscura/discovery/auto_decoder.py +117 -35
- oscura/discovery/comparison.py +191 -86
- oscura/discovery/quality_validator.py +155 -68
- oscura/discovery/signal_detector.py +196 -79
- oscura/export/__init__.py +18 -8
- oscura/export/kaitai_struct.py +513 -0
- oscura/export/scapy_layer.py +801 -0
- oscura/export/wireshark/generator.py +1 -1
- oscura/export/wireshark/templates/dissector.lua.j2 +2 -2
- oscura/export/wireshark_dissector.py +746 -0
- oscura/guidance/wizard.py +207 -111
- oscura/hardware/__init__.py +19 -0
- oscura/{acquisition → hardware/acquisition}/__init__.py +4 -4
- oscura/{acquisition → hardware/acquisition}/file.py +2 -2
- oscura/{acquisition → hardware/acquisition}/hardware.py +7 -7
- oscura/{acquisition → hardware/acquisition}/saleae.py +15 -12
- oscura/{acquisition → hardware/acquisition}/socketcan.py +1 -1
- oscura/{acquisition → hardware/acquisition}/streaming.py +2 -2
- oscura/{acquisition → hardware/acquisition}/synthetic.py +3 -3
- oscura/{acquisition → hardware/acquisition}/visa.py +33 -11
- oscura/hardware/firmware/__init__.py +29 -0
- oscura/hardware/firmware/pattern_recognition.py +874 -0
- oscura/hardware/hal_detector.py +736 -0
- oscura/hardware/security/__init__.py +37 -0
- oscura/hardware/security/side_channel_detector.py +1126 -0
- oscura/inference/__init__.py +4 -0
- oscura/inference/active_learning/observation_table.py +4 -1
- oscura/inference/alignment.py +216 -123
- oscura/inference/bayesian.py +113 -33
- oscura/inference/crc_reverse.py +101 -55
- oscura/inference/logic.py +6 -2
- oscura/inference/message_format.py +342 -183
- oscura/inference/protocol.py +95 -44
- oscura/inference/protocol_dsl.py +180 -82
- oscura/inference/signal_intelligence.py +1439 -706
- oscura/inference/spectral.py +99 -57
- oscura/inference/state_machine.py +810 -158
- oscura/inference/stream.py +270 -110
- oscura/iot/__init__.py +34 -0
- oscura/iot/coap/__init__.py +32 -0
- oscura/iot/coap/analyzer.py +668 -0
- oscura/iot/coap/options.py +212 -0
- oscura/iot/lorawan/__init__.py +21 -0
- oscura/iot/lorawan/crypto.py +206 -0
- oscura/iot/lorawan/decoder.py +801 -0
- oscura/iot/lorawan/mac_commands.py +341 -0
- oscura/iot/mqtt/__init__.py +27 -0
- oscura/iot/mqtt/analyzer.py +999 -0
- oscura/iot/mqtt/properties.py +315 -0
- oscura/iot/zigbee/__init__.py +31 -0
- oscura/iot/zigbee/analyzer.py +615 -0
- oscura/iot/zigbee/security.py +153 -0
- oscura/iot/zigbee/zcl.py +349 -0
- oscura/jupyter/display.py +125 -45
- oscura/{exploratory → jupyter/exploratory}/__init__.py +8 -8
- oscura/{exploratory → jupyter/exploratory}/error_recovery.py +298 -141
- oscura/jupyter/exploratory/fuzzy.py +746 -0
- oscura/{exploratory → jupyter/exploratory}/fuzzy_advanced.py +258 -100
- oscura/{exploratory → jupyter/exploratory}/legacy.py +464 -242
- oscura/{exploratory → jupyter/exploratory}/parse.py +167 -145
- oscura/{exploratory → jupyter/exploratory}/recovery.py +119 -87
- oscura/jupyter/exploratory/sync.py +612 -0
- oscura/{exploratory → jupyter/exploratory}/unknown.py +299 -176
- oscura/jupyter/magic.py +4 -4
- oscura/{ui → jupyter/ui}/__init__.py +2 -2
- oscura/{ui → jupyter/ui}/formatters.py +3 -3
- oscura/{ui → jupyter/ui}/progressive_display.py +153 -82
- oscura/loaders/__init__.py +183 -67
- oscura/loaders/binary.py +88 -1
- oscura/loaders/chipwhisperer.py +153 -137
- oscura/loaders/configurable.py +208 -86
- oscura/loaders/csv_loader.py +458 -215
- oscura/loaders/hdf5_loader.py +278 -119
- oscura/loaders/lazy.py +87 -54
- oscura/loaders/mmap_loader.py +1 -1
- oscura/loaders/numpy_loader.py +253 -116
- oscura/loaders/pcap.py +226 -151
- oscura/loaders/rigol.py +110 -49
- oscura/loaders/sigrok.py +201 -78
- oscura/loaders/tdms.py +81 -58
- oscura/loaders/tektronix.py +291 -174
- oscura/loaders/touchstone.py +182 -87
- oscura/loaders/tss.py +456 -0
- oscura/loaders/vcd.py +215 -117
- oscura/loaders/wav.py +155 -68
- oscura/reporting/__init__.py +9 -0
- oscura/reporting/analyze.py +352 -146
- oscura/reporting/argument_preparer.py +69 -14
- oscura/reporting/auto_report.py +97 -61
- oscura/reporting/batch.py +131 -58
- oscura/reporting/chart_selection.py +57 -45
- oscura/reporting/comparison.py +63 -17
- oscura/reporting/content/executive.py +76 -24
- oscura/reporting/core_formats/multi_format.py +11 -8
- oscura/reporting/engine.py +312 -158
- oscura/reporting/enhanced_reports.py +949 -0
- oscura/reporting/export.py +86 -43
- oscura/reporting/formatting/numbers.py +69 -42
- oscura/reporting/html.py +139 -58
- oscura/reporting/index.py +137 -65
- oscura/reporting/output.py +158 -67
- oscura/reporting/pdf.py +67 -102
- oscura/reporting/plots.py +191 -112
- oscura/reporting/sections.py +88 -47
- oscura/reporting/standards.py +104 -61
- oscura/reporting/summary_generator.py +75 -55
- oscura/reporting/tables.py +138 -54
- oscura/reporting/templates/enhanced/protocol_re.html +525 -0
- oscura/sessions/__init__.py +14 -23
- oscura/sessions/base.py +3 -3
- oscura/sessions/blackbox.py +106 -10
- oscura/sessions/generic.py +2 -2
- oscura/sessions/legacy.py +783 -0
- oscura/side_channel/__init__.py +63 -0
- oscura/side_channel/dpa.py +1025 -0
- oscura/utils/__init__.py +15 -1
- oscura/utils/bitwise.py +118 -0
- oscura/{builders → utils/builders}/__init__.py +1 -1
- oscura/{comparison → utils/comparison}/__init__.py +6 -6
- oscura/{comparison → utils/comparison}/compare.py +202 -101
- oscura/{comparison → utils/comparison}/golden.py +83 -63
- oscura/{comparison → utils/comparison}/limits.py +313 -89
- oscura/{comparison → utils/comparison}/mask.py +151 -45
- oscura/{comparison → utils/comparison}/trace_diff.py +1 -1
- oscura/{comparison → utils/comparison}/visualization.py +147 -89
- oscura/{component → utils/component}/__init__.py +3 -3
- oscura/{component → utils/component}/impedance.py +122 -58
- oscura/{component → utils/component}/reactive.py +165 -168
- oscura/{component → utils/component}/transmission_line.py +3 -3
- oscura/{filtering → utils/filtering}/__init__.py +6 -6
- oscura/{filtering → utils/filtering}/base.py +1 -1
- oscura/{filtering → utils/filtering}/convenience.py +2 -2
- oscura/{filtering → utils/filtering}/design.py +169 -93
- oscura/{filtering → utils/filtering}/filters.py +2 -2
- oscura/{filtering → utils/filtering}/introspection.py +2 -2
- oscura/utils/geometry.py +31 -0
- oscura/utils/imports.py +184 -0
- oscura/utils/lazy.py +1 -1
- oscura/{math → utils/math}/__init__.py +2 -2
- oscura/{math → utils/math}/arithmetic.py +114 -48
- oscura/{math → utils/math}/interpolation.py +139 -106
- oscura/utils/memory.py +129 -66
- oscura/utils/memory_advanced.py +92 -9
- oscura/utils/memory_extensions.py +10 -8
- oscura/{optimization → utils/optimization}/__init__.py +1 -1
- oscura/{optimization → utils/optimization}/search.py +2 -2
- oscura/utils/performance/__init__.py +58 -0
- oscura/utils/performance/caching.py +889 -0
- oscura/utils/performance/lsh_clustering.py +333 -0
- oscura/utils/performance/memory_optimizer.py +699 -0
- oscura/utils/performance/optimizations.py +675 -0
- oscura/utils/performance/parallel.py +654 -0
- oscura/utils/performance/profiling.py +661 -0
- oscura/{pipeline → utils/pipeline}/base.py +1 -1
- oscura/{pipeline → utils/pipeline}/composition.py +1 -1
- oscura/{pipeline → utils/pipeline}/parallel.py +3 -2
- oscura/{pipeline → utils/pipeline}/pipeline.py +1 -1
- oscura/{pipeline → utils/pipeline}/reverse_engineering.py +412 -221
- oscura/{search → utils/search}/__init__.py +3 -3
- oscura/{search → utils/search}/anomaly.py +188 -58
- oscura/utils/search/context.py +294 -0
- oscura/{search → utils/search}/pattern.py +138 -10
- oscura/utils/serial.py +51 -0
- oscura/utils/storage/__init__.py +61 -0
- oscura/utils/storage/database.py +1166 -0
- oscura/{streaming → utils/streaming}/chunked.py +302 -143
- oscura/{streaming → utils/streaming}/progressive.py +1 -1
- oscura/{streaming → utils/streaming}/realtime.py +3 -2
- oscura/{triggering → utils/triggering}/__init__.py +6 -6
- oscura/{triggering → utils/triggering}/base.py +6 -6
- oscura/{triggering → utils/triggering}/edge.py +2 -2
- oscura/{triggering → utils/triggering}/pattern.py +2 -2
- oscura/{triggering → utils/triggering}/pulse.py +115 -74
- oscura/{triggering → utils/triggering}/window.py +2 -2
- oscura/utils/validation.py +32 -0
- oscura/validation/__init__.py +121 -0
- oscura/{compliance → validation/compliance}/__init__.py +5 -5
- oscura/{compliance → validation/compliance}/advanced.py +5 -5
- oscura/{compliance → validation/compliance}/masks.py +1 -1
- oscura/{compliance → validation/compliance}/reporting.py +127 -53
- oscura/{compliance → validation/compliance}/testing.py +114 -52
- oscura/validation/compliance_tests.py +915 -0
- oscura/validation/fuzzer.py +990 -0
- oscura/validation/grammar_tests.py +596 -0
- oscura/validation/grammar_validator.py +904 -0
- oscura/validation/hil_testing.py +977 -0
- oscura/{quality → validation/quality}/__init__.py +4 -4
- oscura/{quality → validation/quality}/ensemble.py +251 -171
- oscura/{quality → validation/quality}/explainer.py +3 -3
- oscura/{quality → validation/quality}/scoring.py +1 -1
- oscura/{quality → validation/quality}/warnings.py +4 -4
- oscura/validation/regression_suite.py +808 -0
- oscura/validation/replay.py +788 -0
- oscura/{testing → validation/testing}/__init__.py +2 -2
- oscura/{testing → validation/testing}/synthetic.py +5 -5
- oscura/visualization/__init__.py +9 -0
- oscura/visualization/accessibility.py +1 -1
- oscura/visualization/annotations.py +64 -67
- oscura/visualization/colors.py +7 -7
- oscura/visualization/digital.py +180 -81
- oscura/visualization/eye.py +236 -85
- oscura/visualization/interactive.py +320 -143
- oscura/visualization/jitter.py +587 -247
- oscura/visualization/layout.py +169 -134
- oscura/visualization/optimization.py +103 -52
- oscura/visualization/palettes.py +1 -1
- oscura/visualization/power.py +427 -211
- oscura/visualization/power_extended.py +626 -297
- oscura/visualization/presets.py +2 -0
- oscura/visualization/protocols.py +495 -181
- oscura/visualization/render.py +79 -63
- oscura/visualization/reverse_engineering.py +171 -124
- oscura/visualization/signal_integrity.py +460 -279
- oscura/visualization/specialized.py +190 -100
- oscura/visualization/spectral.py +670 -255
- oscura/visualization/thumbnails.py +166 -137
- oscura/visualization/waveform.py +150 -63
- oscura/workflows/__init__.py +3 -0
- oscura/{batch → workflows/batch}/__init__.py +5 -5
- oscura/{batch → workflows/batch}/advanced.py +150 -75
- oscura/workflows/batch/aggregate.py +531 -0
- oscura/workflows/batch/analyze.py +236 -0
- oscura/{batch → workflows/batch}/logging.py +2 -2
- oscura/{batch → workflows/batch}/metrics.py +1 -1
- oscura/workflows/complete_re.py +1144 -0
- oscura/workflows/compliance.py +44 -54
- oscura/workflows/digital.py +197 -51
- oscura/workflows/legacy/__init__.py +12 -0
- oscura/{workflow → workflows/legacy}/dag.py +4 -1
- oscura/workflows/multi_trace.py +9 -9
- oscura/workflows/power.py +42 -62
- oscura/workflows/protocol.py +82 -49
- oscura/workflows/reverse_engineering.py +351 -150
- oscura/workflows/signal_integrity.py +157 -82
- oscura-0.7.0.dist-info/METADATA +661 -0
- oscura-0.7.0.dist-info/RECORD +591 -0
- oscura/batch/aggregate.py +0 -300
- oscura/batch/analyze.py +0 -139
- oscura/dsl/__init__.py +0 -73
- oscura/exceptions.py +0 -59
- oscura/exploratory/fuzzy.py +0 -513
- oscura/exploratory/sync.py +0 -384
- oscura/exporters/__init__.py +0 -94
- oscura/exporters/csv.py +0 -303
- oscura/exporters/exporters.py +0 -44
- oscura/exporters/hdf5.py +0 -217
- oscura/exporters/html_export.py +0 -701
- oscura/exporters/json_export.py +0 -291
- oscura/exporters/markdown_export.py +0 -367
- oscura/exporters/matlab_export.py +0 -354
- oscura/exporters/npz_export.py +0 -219
- oscura/exporters/spice_export.py +0 -210
- oscura/search/context.py +0 -149
- oscura/session/__init__.py +0 -34
- oscura/session/annotations.py +0 -289
- oscura/session/history.py +0 -313
- oscura/session/session.py +0 -520
- oscura/workflow/__init__.py +0 -13
- oscura-0.5.1.dist-info/METADATA +0 -583
- oscura-0.5.1.dist-info/RECORD +0 -481
- /oscura/core/{config.py → config/legacy.py} +0 -0
- /oscura/{extensibility → core/extensibility}/__init__.py +0 -0
- /oscura/{extensibility → core/extensibility}/registry.py +0 -0
- /oscura/{plugins → core/plugins}/isolation.py +0 -0
- /oscura/{schemas → core/schemas}/bus_configuration.json +0 -0
- /oscura/{builders → utils/builders}/signal_builder.py +0 -0
- /oscura/{optimization → utils/optimization}/parallel.py +0 -0
- /oscura/{pipeline → utils/pipeline}/__init__.py +0 -0
- /oscura/{streaming → utils/streaming}/__init__.py +0 -0
- {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/WHEEL +0 -0
- {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/entry_points.txt +0 -0
- {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,612 @@
|
|
|
1
|
+
"""Fuzzy synchronization pattern search for corrupted data.
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
This module provides fuzzy pattern matching for finding sync words and markers
|
|
5
|
+
in noisy or corrupted logic analyzer captures, with configurable bit error
|
|
6
|
+
tolerance using Hamming distance.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from enum import Enum
|
|
13
|
+
from typing import Literal
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
from numpy.typing import NDArray
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class RecoveryStrategy(Enum):
|
|
20
|
+
"""Error recovery strategies for corrupted packets.
|
|
21
|
+
|
|
22
|
+
Attributes:
|
|
23
|
+
NEXT_SYNC: Skip to next sync word when corruption detected
|
|
24
|
+
SKIP_BYTES: Skip fixed byte count and retry parsing
|
|
25
|
+
HEURISTIC: Use statistical packet length model for recovery
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
NEXT_SYNC = "next_sync"
|
|
29
|
+
SKIP_BYTES = "skip_bytes"
|
|
30
|
+
HEURISTIC = "heuristic"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class SyncMatch:
|
|
35
|
+
"""Result from fuzzy sync pattern search.
|
|
36
|
+
|
|
37
|
+
Attributes:
|
|
38
|
+
index: Starting position of match in bits or bytes
|
|
39
|
+
matched_value: The actual value that matched (may differ from pattern)
|
|
40
|
+
hamming_distance: Number of bit errors in the match
|
|
41
|
+
confidence: Match confidence (1.0 - bit_errors/pattern_length)
|
|
42
|
+
pattern_length: Length of pattern in bits
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
index: int
|
|
46
|
+
matched_value: int
|
|
47
|
+
hamming_distance: int
|
|
48
|
+
confidence: float
|
|
49
|
+
pattern_length: int
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class PacketParseResult:
|
|
54
|
+
"""Result from robust packet parsing.
|
|
55
|
+
|
|
56
|
+
Attributes:
|
|
57
|
+
packets: List of successfully parsed packet data
|
|
58
|
+
valid: List of validity flags for each packet
|
|
59
|
+
errors: List of error types ('length_corruption', 'sync_lost', None)
|
|
60
|
+
error_positions: Byte positions where errors occurred
|
|
61
|
+
recovery_count: Number of times recovery was triggered
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
packets: list[bytes]
|
|
65
|
+
valid: list[bool]
|
|
66
|
+
errors: list[str | None]
|
|
67
|
+
error_positions: list[int]
|
|
68
|
+
recovery_count: int
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def hamming_distance(a: int, b: int, pattern_bits: int) -> int:
|
|
72
|
+
"""Calculate Hamming distance between two integers.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
a: First integer
|
|
76
|
+
b: Second integer
|
|
77
|
+
pattern_bits: Number of bits to compare (8, 16, 32, or 64)
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
Number of differing bits
|
|
81
|
+
|
|
82
|
+
Examples:
|
|
83
|
+
>>> hamming_distance(0b10101010, 0b10101011, 8)
|
|
84
|
+
1
|
|
85
|
+
>>> hamming_distance(0xAA55, 0xAA54, 16)
|
|
86
|
+
1
|
|
87
|
+
"""
|
|
88
|
+
# XOR gives 1s where bits differ
|
|
89
|
+
diff = a ^ b
|
|
90
|
+
# Mask to pattern length
|
|
91
|
+
mask = (1 << pattern_bits) - 1
|
|
92
|
+
diff &= mask
|
|
93
|
+
# Count set bits (population count)
|
|
94
|
+
return (diff).bit_count()
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def fuzzy_sync_search(
|
|
98
|
+
data: NDArray[np.uint8],
|
|
99
|
+
pattern: int,
|
|
100
|
+
*,
|
|
101
|
+
pattern_bits: Literal[8, 16, 32, 64] = 8,
|
|
102
|
+
max_errors: int = 2,
|
|
103
|
+
min_confidence: float = 0.85,
|
|
104
|
+
) -> list[SyncMatch]:
|
|
105
|
+
"""Find sync patterns with bit error tolerance using Hamming distance.
|
|
106
|
+
|
|
107
|
+
: Searches for sync words even with bit errors,
|
|
108
|
+
essential for recovering corrupted logic analyzer captures.
|
|
109
|
+
|
|
110
|
+
Performance targets (DAQ-001):
|
|
111
|
+
- ≥10 MB/s for max_errors=2
|
|
112
|
+
- ≥5 MB/s for max_errors=4
|
|
113
|
+
- ≥1 MB/s for max_errors=8
|
|
114
|
+
|
|
115
|
+
Confidence scoring (DAQ-001):
|
|
116
|
+
- ≥0.95 (0-1 bit errors): Highly reliable
|
|
117
|
+
- 0.85-0.95 (2-4 bit errors): Reliable
|
|
118
|
+
- <0.85 (>4 bit errors): Verify manually
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
data: Input byte array to search
|
|
122
|
+
pattern: Sync pattern to find (e.g., 0xAA55F0F0 for 32-bit)
|
|
123
|
+
pattern_bits: Pattern length in bits (8, 16, 32, or 64)
|
|
124
|
+
max_errors: Maximum tolerable bit errors (0-8)
|
|
125
|
+
min_confidence: Minimum confidence threshold (0.0-1.0)
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
List of SyncMatch objects with position, matched value, distance,
|
|
129
|
+
and confidence score
|
|
130
|
+
|
|
131
|
+
Raises:
|
|
132
|
+
ValueError: If pattern_bits not in [8, 16, 32, 64]
|
|
133
|
+
ValueError: If max_errors < 0 or > 8
|
|
134
|
+
ValueError: If min_confidence not in [0.0, 1.0]
|
|
135
|
+
|
|
136
|
+
Examples:
|
|
137
|
+
>>> data = np.array([0xAA, 0x55, 0xF0, 0xF0, 0xFF], dtype=np.uint8)
|
|
138
|
+
>>> # Find exact match
|
|
139
|
+
>>> matches = fuzzy_sync_search(data, 0xAA55, pattern_bits=16)
|
|
140
|
+
>>> print(matches[0].confidence)
|
|
141
|
+
1.0
|
|
142
|
+
|
|
143
|
+
>>> # Find with 1 bit error (0xAA54 instead of 0xAA55)
|
|
144
|
+
>>> data = np.array([0xAA, 0x54, 0x00], dtype=np.uint8)
|
|
145
|
+
>>> matches = fuzzy_sync_search(data, 0xAA55, pattern_bits=16, max_errors=2)
|
|
146
|
+
>>> print(matches[0].hamming_distance)
|
|
147
|
+
1
|
|
148
|
+
|
|
149
|
+
References:
|
|
150
|
+
DAQ-001: Fuzzy Bit Pattern Search with Hamming Distance Tolerance
|
|
151
|
+
"""
|
|
152
|
+
_validate_fuzzy_search_params(pattern_bits, max_errors, min_confidence)
|
|
153
|
+
|
|
154
|
+
pattern_bytes = pattern_bits // 8
|
|
155
|
+
if len(data) < pattern_bytes:
|
|
156
|
+
return []
|
|
157
|
+
|
|
158
|
+
matches: list[SyncMatch] = []
|
|
159
|
+
|
|
160
|
+
for i in range(len(data) - pattern_bytes + 1):
|
|
161
|
+
window = data[i : i + pattern_bytes]
|
|
162
|
+
value = _bytes_to_int(window, pattern_bytes)
|
|
163
|
+
dist = hamming_distance(value, pattern, pattern_bits)
|
|
164
|
+
|
|
165
|
+
if dist <= max_errors:
|
|
166
|
+
confidence = 1.0 - (dist / pattern_bits)
|
|
167
|
+
if confidence >= min_confidence:
|
|
168
|
+
matches.append(
|
|
169
|
+
SyncMatch(
|
|
170
|
+
index=i,
|
|
171
|
+
matched_value=value,
|
|
172
|
+
hamming_distance=dist,
|
|
173
|
+
confidence=confidence,
|
|
174
|
+
pattern_length=pattern_bits,
|
|
175
|
+
)
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
return matches
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _validate_fuzzy_search_params(
|
|
182
|
+
pattern_bits: int, max_errors: int, min_confidence: float
|
|
183
|
+
) -> None:
|
|
184
|
+
"""Validate fuzzy search parameters.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
pattern_bits: Pattern length in bits.
|
|
188
|
+
max_errors: Maximum bit errors.
|
|
189
|
+
min_confidence: Minimum confidence threshold.
|
|
190
|
+
|
|
191
|
+
Raises:
|
|
192
|
+
ValueError: If parameters invalid.
|
|
193
|
+
"""
|
|
194
|
+
if pattern_bits not in (8, 16, 32, 64):
|
|
195
|
+
raise ValueError("pattern_bits must be 8, 16, 32, or 64")
|
|
196
|
+
if max_errors < 0 or max_errors > 8:
|
|
197
|
+
raise ValueError("max_errors must be in range [0, 8]")
|
|
198
|
+
if not 0.0 <= min_confidence <= 1.0:
|
|
199
|
+
raise ValueError("min_confidence must be in range [0.0, 1.0]")
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _bytes_to_int(window: NDArray[np.uint8], pattern_bytes: int) -> int:
|
|
203
|
+
"""Convert byte window to integer (big-endian).
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
window: Byte array window.
|
|
207
|
+
pattern_bytes: Number of bytes (1, 2, 4, or 8).
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
Integer representation.
|
|
211
|
+
"""
|
|
212
|
+
if pattern_bytes == 1:
|
|
213
|
+
return int(window[0])
|
|
214
|
+
elif pattern_bytes == 2:
|
|
215
|
+
return (int(window[0]) << 8) | int(window[1])
|
|
216
|
+
elif pattern_bytes == 4:
|
|
217
|
+
return (
|
|
218
|
+
(int(window[0]) << 24) | (int(window[1]) << 16) | (int(window[2]) << 8) | int(window[3])
|
|
219
|
+
)
|
|
220
|
+
else: # 8 bytes
|
|
221
|
+
value = 0
|
|
222
|
+
for j in range(8):
|
|
223
|
+
value = (value << 8) | int(window[j])
|
|
224
|
+
return value
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _validate_packet_parse_inputs(
|
|
228
|
+
length_size: int,
|
|
229
|
+
recovery_strategy: RecoveryStrategy,
|
|
230
|
+
sync_pattern: int | None,
|
|
231
|
+
) -> None:
|
|
232
|
+
"""Validate packet parsing input parameters.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
length_size: Length field size in bytes
|
|
236
|
+
recovery_strategy: Recovery strategy to use
|
|
237
|
+
sync_pattern: Optional sync pattern
|
|
238
|
+
|
|
239
|
+
Raises:
|
|
240
|
+
ValueError: If length_size not 1 or 2
|
|
241
|
+
ValueError: If NEXT_SYNC strategy without sync_pattern
|
|
242
|
+
"""
|
|
243
|
+
if length_size not in (1, 2):
|
|
244
|
+
raise ValueError("length_size must be 1 or 2")
|
|
245
|
+
|
|
246
|
+
if recovery_strategy == RecoveryStrategy.NEXT_SYNC and sync_pattern is None:
|
|
247
|
+
raise ValueError("NEXT_SYNC strategy requires sync_pattern")
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _has_sufficient_data_for_header(
|
|
251
|
+
pos: int,
|
|
252
|
+
data_length: int,
|
|
253
|
+
length_offset: int,
|
|
254
|
+
length_size: int,
|
|
255
|
+
) -> bool:
|
|
256
|
+
"""Check if sufficient data remains for packet header.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
pos: Current position in data
|
|
260
|
+
data_length: Total data length
|
|
261
|
+
length_offset: Offset to length field
|
|
262
|
+
length_size: Size of length field
|
|
263
|
+
|
|
264
|
+
Returns:
|
|
265
|
+
True if enough data for header, False otherwise
|
|
266
|
+
"""
|
|
267
|
+
return pos + length_offset + length_size <= data_length
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _extract_length_field(
|
|
271
|
+
data: NDArray[np.uint8],
|
|
272
|
+
pos: int,
|
|
273
|
+
length_offset: int,
|
|
274
|
+
length_size: int,
|
|
275
|
+
) -> int:
|
|
276
|
+
"""Extract packet length field from data.
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
data: Input byte array
|
|
280
|
+
pos: Current position
|
|
281
|
+
length_offset: Offset to length field
|
|
282
|
+
length_size: Size of length field (1 or 2 bytes)
|
|
283
|
+
|
|
284
|
+
Returns:
|
|
285
|
+
Extracted packet length value
|
|
286
|
+
"""
|
|
287
|
+
length_pos = pos + length_offset
|
|
288
|
+
if length_size == 1:
|
|
289
|
+
return int(data[length_pos])
|
|
290
|
+
else: # 2 bytes
|
|
291
|
+
return (int(data[length_pos]) << 8) | int(data[length_pos + 1])
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _validate_length_field(
|
|
295
|
+
pkt_length: int,
|
|
296
|
+
length_size: int,
|
|
297
|
+
min_packet_size: int,
|
|
298
|
+
max_packet_size: int,
|
|
299
|
+
packet_lengths: list[int],
|
|
300
|
+
) -> tuple[bool, str | None]:
|
|
301
|
+
"""Validate packet length field for corruption.
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
pkt_length: Extracted packet length
|
|
305
|
+
length_size: Size of length field (1 or 2)
|
|
306
|
+
min_packet_size: Minimum valid packet size
|
|
307
|
+
max_packet_size: Maximum valid packet size
|
|
308
|
+
packet_lengths: History of valid packet lengths
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
Tuple of (is_valid, error_type)
|
|
312
|
+
"""
|
|
313
|
+
# Check for obviously corrupted lengths
|
|
314
|
+
if (
|
|
315
|
+
pkt_length == 0
|
|
316
|
+
or pkt_length > max_packet_size
|
|
317
|
+
or (length_size == 2 and (pkt_length & 0xFF00) == 0xFF00)
|
|
318
|
+
or pkt_length < min_packet_size
|
|
319
|
+
):
|
|
320
|
+
return False, "length_corruption"
|
|
321
|
+
|
|
322
|
+
# Check suspiciously large length (heuristic)
|
|
323
|
+
if len(packet_lengths) >= 10:
|
|
324
|
+
p90 = np.percentile(packet_lengths, 90)
|
|
325
|
+
if pkt_length > p90 * 2:
|
|
326
|
+
return False, "length_corruption"
|
|
327
|
+
|
|
328
|
+
return True, None
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _try_extract_packet(
|
|
332
|
+
data: NDArray[np.uint8],
|
|
333
|
+
pos: int,
|
|
334
|
+
pkt_length: int,
|
|
335
|
+
packets: list[bytes],
|
|
336
|
+
valid: list[bool],
|
|
337
|
+
errors: list[str | None],
|
|
338
|
+
error_positions: list[int],
|
|
339
|
+
packet_lengths: list[int],
|
|
340
|
+
) -> tuple[int | None, bool]:
|
|
341
|
+
"""Try to extract complete packet from current position.
|
|
342
|
+
|
|
343
|
+
Args:
|
|
344
|
+
data: Input byte array
|
|
345
|
+
pos: Current position
|
|
346
|
+
pkt_length: Expected packet length
|
|
347
|
+
packets: List to append packet to
|
|
348
|
+
valid: List to append validity flag
|
|
349
|
+
errors: List to append error info
|
|
350
|
+
error_positions: List to append error positions
|
|
351
|
+
packet_lengths: List to append packet length
|
|
352
|
+
|
|
353
|
+
Returns:
|
|
354
|
+
Tuple of (new_position, continue_parsing)
|
|
355
|
+
- new_position: Next position to parse (None if truncated)
|
|
356
|
+
- continue_parsing: Whether to continue parsing
|
|
357
|
+
"""
|
|
358
|
+
packet_end = pos + pkt_length
|
|
359
|
+
|
|
360
|
+
if packet_end <= len(data):
|
|
361
|
+
# Successfully extract packet
|
|
362
|
+
packet_data = bytes(data[pos:packet_end])
|
|
363
|
+
packets.append(packet_data)
|
|
364
|
+
valid.append(True)
|
|
365
|
+
errors.append(None)
|
|
366
|
+
packet_lengths.append(pkt_length)
|
|
367
|
+
return packet_end, True
|
|
368
|
+
else:
|
|
369
|
+
# Packet extends beyond data
|
|
370
|
+
errors.append("truncated")
|
|
371
|
+
error_positions.append(pos)
|
|
372
|
+
return None, False
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def _apply_recovery_strategy(
|
|
376
|
+
recovery_strategy: RecoveryStrategy,
|
|
377
|
+
pos: int,
|
|
378
|
+
data: NDArray[np.uint8],
|
|
379
|
+
sync_pattern: int | None,
|
|
380
|
+
sync_bits: Literal[8, 16, 32, 64],
|
|
381
|
+
skip_bytes: int,
|
|
382
|
+
packet_lengths: list[int],
|
|
383
|
+
min_packet_size: int,
|
|
384
|
+
errors: list[str | None],
|
|
385
|
+
error_type: str | None,
|
|
386
|
+
) -> tuple[int | None, bool]:
|
|
387
|
+
"""Apply error recovery strategy.
|
|
388
|
+
|
|
389
|
+
Args:
|
|
390
|
+
recovery_strategy: Strategy to apply
|
|
391
|
+
pos: Current position
|
|
392
|
+
data: Input byte array
|
|
393
|
+
sync_pattern: Sync pattern for NEXT_SYNC strategy
|
|
394
|
+
sync_bits: Sync pattern bit length
|
|
395
|
+
skip_bytes: Bytes to skip for SKIP_BYTES strategy
|
|
396
|
+
packet_lengths: History for HEURISTIC strategy
|
|
397
|
+
min_packet_size: Minimum packet size
|
|
398
|
+
errors: List to append error info
|
|
399
|
+
error_type: Error type to append
|
|
400
|
+
|
|
401
|
+
Returns:
|
|
402
|
+
Tuple of (new_position, continue_parsing)
|
|
403
|
+
"""
|
|
404
|
+
if recovery_strategy == RecoveryStrategy.NEXT_SYNC:
|
|
405
|
+
# Search for next sync word
|
|
406
|
+
assert sync_pattern is not None
|
|
407
|
+
search_start = pos + 1
|
|
408
|
+
search_data = data[search_start:]
|
|
409
|
+
|
|
410
|
+
if len(search_data) >= sync_bits // 8:
|
|
411
|
+
matches = fuzzy_sync_search(
|
|
412
|
+
search_data,
|
|
413
|
+
sync_pattern,
|
|
414
|
+
pattern_bits=sync_bits,
|
|
415
|
+
max_errors=0,
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
if matches:
|
|
419
|
+
# Found sync, jump to it
|
|
420
|
+
errors.append("sync_lost")
|
|
421
|
+
return search_start + matches[0].index, True
|
|
422
|
+
else:
|
|
423
|
+
# No more syncs found
|
|
424
|
+
return None, False
|
|
425
|
+
else:
|
|
426
|
+
return None, False
|
|
427
|
+
|
|
428
|
+
elif recovery_strategy == RecoveryStrategy.SKIP_BYTES:
|
|
429
|
+
# Skip fixed bytes and retry
|
|
430
|
+
errors.append(error_type)
|
|
431
|
+
return pos + skip_bytes, True
|
|
432
|
+
|
|
433
|
+
elif recovery_strategy == RecoveryStrategy.HEURISTIC:
|
|
434
|
+
# Use median packet length as guess
|
|
435
|
+
if packet_lengths:
|
|
436
|
+
guess_length = int(np.median(packet_lengths))
|
|
437
|
+
new_pos = pos + guess_length
|
|
438
|
+
else:
|
|
439
|
+
# No history, skip minimal amount
|
|
440
|
+
new_pos = pos + min_packet_size
|
|
441
|
+
errors.append(error_type)
|
|
442
|
+
return new_pos, True
|
|
443
|
+
|
|
444
|
+
raise RuntimeError("Unreachable: must either handle error or return None")
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def _initialize_parse_state() -> tuple[
|
|
448
|
+
list[bytes], list[bool], list[str | None], list[int], list[int]
|
|
449
|
+
]:
|
|
450
|
+
"""Initialize packet parsing state containers.
|
|
451
|
+
|
|
452
|
+
Returns:
|
|
453
|
+
Tuple of (packets, valid, errors, error_positions, packet_lengths).
|
|
454
|
+
"""
|
|
455
|
+
return [], [], [], [], []
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def _handle_packet_extraction(
|
|
459
|
+
data: NDArray[np.uint8],
|
|
460
|
+
pos: int,
|
|
461
|
+
pkt_length: int,
|
|
462
|
+
is_valid_length: bool,
|
|
463
|
+
error_type: str | None,
|
|
464
|
+
packets: list[bytes],
|
|
465
|
+
valid: list[bool],
|
|
466
|
+
errors: list[str | None],
|
|
467
|
+
error_positions: list[int],
|
|
468
|
+
packet_lengths: list[int],
|
|
469
|
+
recovery_strategy: RecoveryStrategy,
|
|
470
|
+
sync_pattern: int | None,
|
|
471
|
+
sync_bits: Literal[8, 16, 32, 64],
|
|
472
|
+
skip_bytes: int,
|
|
473
|
+
min_packet_size: int,
|
|
474
|
+
) -> tuple[int | None, bool, int]:
|
|
475
|
+
"""Handle packet extraction or error recovery.
|
|
476
|
+
|
|
477
|
+
Args:
|
|
478
|
+
data: Input byte array.
|
|
479
|
+
pos: Current position.
|
|
480
|
+
pkt_length: Packet length.
|
|
481
|
+
is_valid_length: Whether length is valid.
|
|
482
|
+
error_type: Error type if invalid.
|
|
483
|
+
packets: Packets list.
|
|
484
|
+
valid: Valid flags list.
|
|
485
|
+
errors: Errors list.
|
|
486
|
+
error_positions: Error positions list.
|
|
487
|
+
packet_lengths: Packet lengths list.
|
|
488
|
+
recovery_strategy: Recovery strategy.
|
|
489
|
+
sync_pattern: Sync pattern.
|
|
490
|
+
sync_bits: Sync pattern bits.
|
|
491
|
+
skip_bytes: Bytes to skip.
|
|
492
|
+
min_packet_size: Minimum packet size.
|
|
493
|
+
|
|
494
|
+
Returns:
|
|
495
|
+
Tuple of (new_position, should_continue, recovery_increment).
|
|
496
|
+
"""
|
|
497
|
+
if is_valid_length:
|
|
498
|
+
new_pos, should_continue = _try_extract_packet(
|
|
499
|
+
data, pos, pkt_length, packets, valid, errors, error_positions, packet_lengths
|
|
500
|
+
)
|
|
501
|
+
return new_pos, should_continue, 0
|
|
502
|
+
else:
|
|
503
|
+
error_positions.append(pos)
|
|
504
|
+
new_pos, should_continue = _apply_recovery_strategy(
|
|
505
|
+
recovery_strategy,
|
|
506
|
+
pos,
|
|
507
|
+
data,
|
|
508
|
+
sync_pattern,
|
|
509
|
+
sync_bits,
|
|
510
|
+
skip_bytes,
|
|
511
|
+
packet_lengths,
|
|
512
|
+
min_packet_size,
|
|
513
|
+
errors,
|
|
514
|
+
error_type,
|
|
515
|
+
)
|
|
516
|
+
return new_pos, should_continue, 1
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
def parse_variable_length_packets(
|
|
520
|
+
data: NDArray[np.uint8],
|
|
521
|
+
*,
|
|
522
|
+
sync_pattern: int | None = None,
|
|
523
|
+
sync_bits: Literal[8, 16, 32, 64] = 16,
|
|
524
|
+
length_offset: int = 2,
|
|
525
|
+
length_size: int = 2,
|
|
526
|
+
min_packet_size: int = 4,
|
|
527
|
+
max_packet_size: int = 1024,
|
|
528
|
+
recovery_strategy: RecoveryStrategy = RecoveryStrategy.NEXT_SYNC,
|
|
529
|
+
skip_bytes: int = 1,
|
|
530
|
+
) -> PacketParseResult:
|
|
531
|
+
"""Parse variable-length packets with error recovery.
|
|
532
|
+
|
|
533
|
+
Robust parsing that continues after corruption, falling back to sync word
|
|
534
|
+
search when length fields are corrupted. Error detection: length=0,
|
|
535
|
+
length>max_packet_size, or length&0xFF00=0xFF00. Recovery strategies:
|
|
536
|
+
next_sync (requires sync_pattern), skip_bytes, or heuristic.
|
|
537
|
+
|
|
538
|
+
Args:
|
|
539
|
+
data: Input byte array containing packets
|
|
540
|
+
sync_pattern: Optional sync word to search for on errors
|
|
541
|
+
sync_bits: Sync pattern length in bits
|
|
542
|
+
length_offset: Byte offset to length field from start
|
|
543
|
+
length_size: Length field size in bytes (1 or 2)
|
|
544
|
+
min_packet_size: Minimum valid packet size in bytes
|
|
545
|
+
max_packet_size: Maximum valid packet size in bytes
|
|
546
|
+
recovery_strategy: Strategy to use when corruption detected
|
|
547
|
+
skip_bytes: Number of bytes to skip for SKIP_BYTES strategy
|
|
548
|
+
|
|
549
|
+
Returns:
|
|
550
|
+
PacketParseResult with parsed packets and error information
|
|
551
|
+
|
|
552
|
+
Raises:
|
|
553
|
+
ValueError: If length_size not 1 or 2
|
|
554
|
+
ValueError: If recovery_strategy is NEXT_SYNC without sync_pattern
|
|
555
|
+
|
|
556
|
+
Examples:
|
|
557
|
+
>>> # Simple TLV parsing with sync word
|
|
558
|
+
>>> data = np.array([0xAA, 0x55, 0x00, 0x04, 0x01, 0x02], dtype=np.uint8)
|
|
559
|
+
>>> result = parse_variable_length_packets(
|
|
560
|
+
... data, sync_pattern=0xAA55, length_offset=2
|
|
561
|
+
... )
|
|
562
|
+
>>> len(result.packets)
|
|
563
|
+
1
|
|
564
|
+
|
|
565
|
+
References:
|
|
566
|
+
DAQ-002: Robust Variable-Length Packet Parsing with Error Recovery
|
|
567
|
+
"""
|
|
568
|
+
_validate_packet_parse_inputs(length_size, recovery_strategy, sync_pattern)
|
|
569
|
+
packets, valid, errors, error_positions, packet_lengths = _initialize_parse_state()
|
|
570
|
+
recovery_count = 0
|
|
571
|
+
pos = 0
|
|
572
|
+
|
|
573
|
+
while pos < len(data):
|
|
574
|
+
if not _has_sufficient_data_for_header(pos, len(data), length_offset, length_size):
|
|
575
|
+
break
|
|
576
|
+
|
|
577
|
+
pkt_length = _extract_length_field(data, pos, length_offset, length_size)
|
|
578
|
+
is_valid_length, error_type = _validate_length_field(
|
|
579
|
+
pkt_length, length_size, min_packet_size, max_packet_size, packet_lengths
|
|
580
|
+
)
|
|
581
|
+
|
|
582
|
+
new_pos, should_continue, recovery_inc = _handle_packet_extraction(
|
|
583
|
+
data,
|
|
584
|
+
pos,
|
|
585
|
+
pkt_length,
|
|
586
|
+
is_valid_length,
|
|
587
|
+
error_type,
|
|
588
|
+
packets,
|
|
589
|
+
valid,
|
|
590
|
+
errors,
|
|
591
|
+
error_positions,
|
|
592
|
+
packet_lengths,
|
|
593
|
+
recovery_strategy,
|
|
594
|
+
sync_pattern,
|
|
595
|
+
sync_bits,
|
|
596
|
+
skip_bytes,
|
|
597
|
+
min_packet_size,
|
|
598
|
+
)
|
|
599
|
+
|
|
600
|
+
recovery_count += recovery_inc
|
|
601
|
+
if not should_continue:
|
|
602
|
+
break
|
|
603
|
+
assert new_pos is not None
|
|
604
|
+
pos = new_pos
|
|
605
|
+
|
|
606
|
+
return PacketParseResult(
|
|
607
|
+
packets=packets,
|
|
608
|
+
valid=valid,
|
|
609
|
+
errors=errors,
|
|
610
|
+
error_positions=error_positions,
|
|
611
|
+
recovery_count=recovery_count,
|
|
612
|
+
)
|