oscura 0.5.0__py3-none-any.whl → 0.6.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- oscura/__init__.py +169 -167
- oscura/analyzers/__init__.py +3 -0
- oscura/analyzers/classification.py +659 -0
- oscura/analyzers/digital/__init__.py +0 -48
- oscura/analyzers/digital/edges.py +325 -65
- oscura/analyzers/digital/extraction.py +0 -195
- oscura/analyzers/digital/quality.py +293 -166
- oscura/analyzers/digital/timing.py +260 -115
- oscura/analyzers/digital/timing_numba.py +334 -0
- oscura/analyzers/entropy.py +605 -0
- oscura/analyzers/eye/diagram.py +176 -109
- oscura/analyzers/eye/metrics.py +5 -5
- oscura/analyzers/jitter/__init__.py +6 -4
- oscura/analyzers/jitter/ber.py +52 -52
- oscura/analyzers/jitter/classification.py +156 -0
- oscura/analyzers/jitter/decomposition.py +163 -113
- oscura/analyzers/jitter/spectrum.py +80 -64
- oscura/analyzers/ml/__init__.py +39 -0
- oscura/analyzers/ml/features.py +600 -0
- oscura/analyzers/ml/signal_classifier.py +604 -0
- oscura/analyzers/packet/daq.py +246 -158
- oscura/analyzers/packet/parser.py +12 -1
- oscura/analyzers/packet/payload.py +50 -2110
- oscura/analyzers/packet/payload_analysis.py +361 -181
- oscura/analyzers/packet/payload_patterns.py +133 -70
- oscura/analyzers/packet/stream.py +84 -23
- oscura/analyzers/patterns/__init__.py +26 -5
- oscura/analyzers/patterns/anomaly_detection.py +908 -0
- oscura/analyzers/patterns/clustering.py +169 -108
- oscura/analyzers/patterns/clustering_optimized.py +227 -0
- oscura/analyzers/patterns/discovery.py +1 -1
- oscura/analyzers/patterns/matching.py +581 -197
- oscura/analyzers/patterns/pattern_mining.py +778 -0
- oscura/analyzers/patterns/periodic.py +121 -38
- oscura/analyzers/patterns/sequences.py +175 -78
- oscura/analyzers/power/conduction.py +1 -1
- oscura/analyzers/power/soa.py +6 -6
- oscura/analyzers/power/switching.py +250 -110
- oscura/analyzers/protocol/__init__.py +17 -1
- oscura/analyzers/protocols/__init__.py +1 -22
- oscura/analyzers/protocols/base.py +6 -6
- oscura/analyzers/protocols/ble/__init__.py +38 -0
- oscura/analyzers/protocols/ble/analyzer.py +809 -0
- oscura/analyzers/protocols/ble/uuids.py +288 -0
- oscura/analyzers/protocols/can.py +257 -127
- oscura/analyzers/protocols/can_fd.py +107 -80
- oscura/analyzers/protocols/flexray.py +139 -80
- oscura/analyzers/protocols/hdlc.py +93 -58
- oscura/analyzers/protocols/i2c.py +247 -106
- oscura/analyzers/protocols/i2s.py +138 -86
- oscura/analyzers/protocols/industrial/__init__.py +40 -0
- oscura/analyzers/protocols/industrial/bacnet/__init__.py +33 -0
- oscura/analyzers/protocols/industrial/bacnet/analyzer.py +708 -0
- oscura/analyzers/protocols/industrial/bacnet/encoding.py +412 -0
- oscura/analyzers/protocols/industrial/bacnet/services.py +622 -0
- oscura/analyzers/protocols/industrial/ethercat/__init__.py +30 -0
- oscura/analyzers/protocols/industrial/ethercat/analyzer.py +474 -0
- oscura/analyzers/protocols/industrial/ethercat/mailbox.py +339 -0
- oscura/analyzers/protocols/industrial/ethercat/topology.py +166 -0
- oscura/analyzers/protocols/industrial/modbus/__init__.py +31 -0
- oscura/analyzers/protocols/industrial/modbus/analyzer.py +525 -0
- oscura/analyzers/protocols/industrial/modbus/crc.py +79 -0
- oscura/analyzers/protocols/industrial/modbus/functions.py +436 -0
- oscura/analyzers/protocols/industrial/opcua/__init__.py +21 -0
- oscura/analyzers/protocols/industrial/opcua/analyzer.py +552 -0
- oscura/analyzers/protocols/industrial/opcua/datatypes.py +446 -0
- oscura/analyzers/protocols/industrial/opcua/services.py +264 -0
- oscura/analyzers/protocols/industrial/profinet/__init__.py +23 -0
- oscura/analyzers/protocols/industrial/profinet/analyzer.py +441 -0
- oscura/analyzers/protocols/industrial/profinet/dcp.py +263 -0
- oscura/analyzers/protocols/industrial/profinet/ptcp.py +200 -0
- oscura/analyzers/protocols/jtag.py +180 -98
- oscura/analyzers/protocols/lin.py +219 -114
- oscura/analyzers/protocols/manchester.py +4 -4
- oscura/analyzers/protocols/onewire.py +253 -149
- oscura/analyzers/protocols/parallel_bus/__init__.py +20 -0
- oscura/analyzers/protocols/parallel_bus/centronics.py +92 -0
- oscura/analyzers/protocols/parallel_bus/gpib.py +137 -0
- oscura/analyzers/protocols/spi.py +192 -95
- oscura/analyzers/protocols/swd.py +321 -167
- oscura/analyzers/protocols/uart.py +267 -125
- oscura/analyzers/protocols/usb.py +235 -131
- oscura/analyzers/side_channel/power.py +17 -12
- oscura/analyzers/signal/__init__.py +15 -0
- oscura/analyzers/signal/timing_analysis.py +1086 -0
- oscura/analyzers/signal_integrity/__init__.py +4 -1
- oscura/analyzers/signal_integrity/sparams.py +2 -19
- oscura/analyzers/spectral/chunked.py +129 -60
- oscura/analyzers/spectral/chunked_fft.py +300 -94
- oscura/analyzers/spectral/chunked_wavelet.py +100 -80
- oscura/analyzers/statistical/checksum.py +376 -217
- oscura/analyzers/statistical/classification.py +229 -107
- oscura/analyzers/statistical/entropy.py +78 -53
- oscura/analyzers/statistics/correlation.py +407 -211
- oscura/analyzers/statistics/outliers.py +2 -2
- oscura/analyzers/statistics/streaming.py +30 -5
- oscura/analyzers/validation.py +216 -101
- oscura/analyzers/waveform/measurements.py +9 -0
- oscura/analyzers/waveform/measurements_with_uncertainty.py +31 -15
- oscura/analyzers/waveform/spectral.py +500 -228
- oscura/api/__init__.py +31 -5
- oscura/api/dsl/__init__.py +582 -0
- oscura/{dsl → api/dsl}/commands.py +43 -76
- oscura/{dsl → api/dsl}/interpreter.py +26 -51
- oscura/{dsl → api/dsl}/parser.py +107 -77
- oscura/{dsl → api/dsl}/repl.py +2 -2
- oscura/api/dsl.py +1 -1
- oscura/{integrations → api/integrations}/__init__.py +1 -1
- oscura/{integrations → api/integrations}/llm.py +201 -102
- oscura/api/operators.py +3 -3
- oscura/api/optimization.py +144 -30
- oscura/api/rest_server.py +921 -0
- oscura/api/server/__init__.py +17 -0
- oscura/api/server/dashboard.py +850 -0
- oscura/api/server/static/README.md +34 -0
- oscura/api/server/templates/base.html +181 -0
- oscura/api/server/templates/export.html +120 -0
- oscura/api/server/templates/home.html +284 -0
- oscura/api/server/templates/protocols.html +58 -0
- oscura/api/server/templates/reports.html +43 -0
- oscura/api/server/templates/session_detail.html +89 -0
- oscura/api/server/templates/sessions.html +83 -0
- oscura/api/server/templates/waveforms.html +73 -0
- oscura/automotive/__init__.py +8 -1
- oscura/automotive/can/__init__.py +10 -0
- oscura/automotive/can/checksum.py +3 -1
- oscura/automotive/can/dbc_generator.py +590 -0
- oscura/automotive/can/message_wrapper.py +121 -74
- oscura/automotive/can/patterns.py +98 -21
- oscura/automotive/can/session.py +292 -56
- oscura/automotive/can/state_machine.py +6 -3
- oscura/automotive/can/stimulus_response.py +97 -75
- oscura/automotive/dbc/__init__.py +10 -2
- oscura/automotive/dbc/generator.py +84 -56
- oscura/automotive/dbc/parser.py +6 -6
- oscura/automotive/dtc/data.json +2763 -0
- oscura/automotive/dtc/database.py +2 -2
- oscura/automotive/flexray/__init__.py +31 -0
- oscura/automotive/flexray/analyzer.py +504 -0
- oscura/automotive/flexray/crc.py +185 -0
- oscura/automotive/flexray/fibex.py +449 -0
- oscura/automotive/j1939/__init__.py +45 -8
- oscura/automotive/j1939/analyzer.py +605 -0
- oscura/automotive/j1939/spns.py +326 -0
- oscura/automotive/j1939/transport.py +306 -0
- oscura/automotive/lin/__init__.py +47 -0
- oscura/automotive/lin/analyzer.py +612 -0
- oscura/automotive/loaders/blf.py +13 -2
- oscura/automotive/loaders/csv_can.py +143 -72
- oscura/automotive/loaders/dispatcher.py +50 -2
- oscura/automotive/loaders/mdf.py +86 -45
- oscura/automotive/loaders/pcap.py +111 -61
- oscura/automotive/uds/__init__.py +4 -0
- oscura/automotive/uds/analyzer.py +725 -0
- oscura/automotive/uds/decoder.py +140 -58
- oscura/automotive/uds/models.py +7 -1
- oscura/automotive/visualization.py +1 -1
- oscura/cli/analyze.py +348 -0
- oscura/cli/batch.py +142 -122
- oscura/cli/benchmark.py +275 -0
- oscura/cli/characterize.py +137 -82
- oscura/cli/compare.py +224 -131
- oscura/cli/completion.py +250 -0
- oscura/cli/config_cmd.py +361 -0
- oscura/cli/decode.py +164 -87
- oscura/cli/export.py +286 -0
- oscura/cli/main.py +115 -31
- oscura/{onboarding → cli/onboarding}/__init__.py +3 -3
- oscura/{onboarding → cli/onboarding}/help.py +80 -58
- oscura/{onboarding → cli/onboarding}/tutorials.py +97 -72
- oscura/{onboarding → cli/onboarding}/wizard.py +55 -36
- oscura/cli/progress.py +147 -0
- oscura/cli/shell.py +157 -135
- oscura/cli/validate_cmd.py +204 -0
- oscura/cli/visualize.py +158 -0
- oscura/convenience.py +125 -79
- oscura/core/__init__.py +4 -2
- oscura/core/backend_selector.py +3 -3
- oscura/core/cache.py +126 -15
- oscura/core/cancellation.py +1 -1
- oscura/{config → core/config}/__init__.py +20 -11
- oscura/{config → core/config}/defaults.py +1 -1
- oscura/{config → core/config}/loader.py +7 -5
- oscura/{config → core/config}/memory.py +5 -5
- oscura/{config → core/config}/migration.py +1 -1
- oscura/{config → core/config}/pipeline.py +99 -23
- oscura/{config → core/config}/preferences.py +1 -1
- oscura/{config → core/config}/protocol.py +3 -3
- oscura/{config → core/config}/schema.py +426 -272
- oscura/{config → core/config}/settings.py +1 -1
- oscura/{config → core/config}/thresholds.py +195 -153
- oscura/core/correlation.py +5 -6
- oscura/core/cross_domain.py +0 -2
- oscura/core/debug.py +9 -5
- oscura/{extensibility → core/extensibility}/docs.py +158 -70
- oscura/{extensibility → core/extensibility}/extensions.py +160 -76
- oscura/{extensibility → core/extensibility}/logging.py +1 -1
- oscura/{extensibility → core/extensibility}/measurements.py +1 -1
- oscura/{extensibility → core/extensibility}/plugins.py +1 -1
- oscura/{extensibility → core/extensibility}/templates.py +73 -3
- oscura/{extensibility → core/extensibility}/validation.py +1 -1
- oscura/core/gpu_backend.py +11 -7
- oscura/core/log_query.py +101 -11
- oscura/core/logging.py +126 -54
- oscura/core/logging_advanced.py +5 -5
- oscura/core/memory_limits.py +108 -70
- oscura/core/memory_monitor.py +2 -2
- oscura/core/memory_progress.py +7 -7
- oscura/core/memory_warnings.py +1 -1
- oscura/core/numba_backend.py +13 -13
- oscura/{plugins → core/plugins}/__init__.py +9 -9
- oscura/{plugins → core/plugins}/base.py +7 -7
- oscura/{plugins → core/plugins}/cli.py +3 -3
- oscura/{plugins → core/plugins}/discovery.py +186 -106
- oscura/{plugins → core/plugins}/lifecycle.py +1 -1
- oscura/{plugins → core/plugins}/manager.py +7 -7
- oscura/{plugins → core/plugins}/registry.py +3 -3
- oscura/{plugins → core/plugins}/versioning.py +1 -1
- oscura/core/progress.py +16 -1
- oscura/core/provenance.py +8 -2
- oscura/{schemas → core/schemas}/__init__.py +2 -2
- oscura/core/schemas/bus_configuration.json +322 -0
- oscura/core/schemas/device_mapping.json +182 -0
- oscura/core/schemas/packet_format.json +418 -0
- oscura/core/schemas/protocol_definition.json +363 -0
- oscura/core/types.py +4 -0
- oscura/core/uncertainty.py +3 -3
- oscura/correlation/__init__.py +52 -0
- oscura/correlation/multi_protocol.py +811 -0
- oscura/discovery/auto_decoder.py +117 -35
- oscura/discovery/comparison.py +191 -86
- oscura/discovery/quality_validator.py +155 -68
- oscura/discovery/signal_detector.py +196 -79
- oscura/export/__init__.py +18 -20
- oscura/export/kaitai_struct.py +513 -0
- oscura/export/scapy_layer.py +801 -0
- oscura/export/wireshark/README.md +15 -15
- oscura/export/wireshark/generator.py +1 -1
- oscura/export/wireshark/templates/dissector.lua.j2 +2 -2
- oscura/export/wireshark_dissector.py +746 -0
- oscura/guidance/wizard.py +207 -111
- oscura/hardware/__init__.py +19 -0
- oscura/{acquisition → hardware/acquisition}/__init__.py +4 -4
- oscura/{acquisition → hardware/acquisition}/file.py +2 -2
- oscura/{acquisition → hardware/acquisition}/hardware.py +7 -7
- oscura/{acquisition → hardware/acquisition}/saleae.py +15 -12
- oscura/{acquisition → hardware/acquisition}/socketcan.py +1 -1
- oscura/{acquisition → hardware/acquisition}/streaming.py +2 -2
- oscura/{acquisition → hardware/acquisition}/synthetic.py +3 -3
- oscura/{acquisition → hardware/acquisition}/visa.py +33 -11
- oscura/hardware/firmware/__init__.py +29 -0
- oscura/hardware/firmware/pattern_recognition.py +874 -0
- oscura/hardware/hal_detector.py +736 -0
- oscura/hardware/security/__init__.py +37 -0
- oscura/hardware/security/side_channel_detector.py +1126 -0
- oscura/inference/__init__.py +4 -0
- oscura/inference/active_learning/README.md +7 -7
- oscura/inference/active_learning/observation_table.py +4 -1
- oscura/inference/alignment.py +216 -123
- oscura/inference/bayesian.py +113 -33
- oscura/inference/crc_reverse.py +101 -55
- oscura/inference/logic.py +6 -2
- oscura/inference/message_format.py +342 -183
- oscura/inference/protocol.py +95 -44
- oscura/inference/protocol_dsl.py +180 -82
- oscura/inference/signal_intelligence.py +1439 -706
- oscura/inference/spectral.py +99 -57
- oscura/inference/state_machine.py +810 -158
- oscura/inference/stream.py +270 -110
- oscura/iot/__init__.py +34 -0
- oscura/iot/coap/__init__.py +32 -0
- oscura/iot/coap/analyzer.py +668 -0
- oscura/iot/coap/options.py +212 -0
- oscura/iot/lorawan/__init__.py +21 -0
- oscura/iot/lorawan/crypto.py +206 -0
- oscura/iot/lorawan/decoder.py +801 -0
- oscura/iot/lorawan/mac_commands.py +341 -0
- oscura/iot/mqtt/__init__.py +27 -0
- oscura/iot/mqtt/analyzer.py +999 -0
- oscura/iot/mqtt/properties.py +315 -0
- oscura/iot/zigbee/__init__.py +31 -0
- oscura/iot/zigbee/analyzer.py +615 -0
- oscura/iot/zigbee/security.py +153 -0
- oscura/iot/zigbee/zcl.py +349 -0
- oscura/jupyter/display.py +125 -45
- oscura/{exploratory → jupyter/exploratory}/__init__.py +8 -8
- oscura/{exploratory → jupyter/exploratory}/error_recovery.py +298 -141
- oscura/jupyter/exploratory/fuzzy.py +746 -0
- oscura/{exploratory → jupyter/exploratory}/fuzzy_advanced.py +258 -100
- oscura/{exploratory → jupyter/exploratory}/legacy.py +464 -242
- oscura/{exploratory → jupyter/exploratory}/parse.py +167 -145
- oscura/{exploratory → jupyter/exploratory}/recovery.py +119 -87
- oscura/jupyter/exploratory/sync.py +612 -0
- oscura/{exploratory → jupyter/exploratory}/unknown.py +299 -176
- oscura/jupyter/magic.py +4 -4
- oscura/{ui → jupyter/ui}/__init__.py +2 -2
- oscura/{ui → jupyter/ui}/formatters.py +3 -3
- oscura/{ui → jupyter/ui}/progressive_display.py +153 -82
- oscura/loaders/__init__.py +171 -63
- oscura/loaders/binary.py +88 -1
- oscura/loaders/chipwhisperer.py +153 -137
- oscura/loaders/configurable.py +208 -86
- oscura/loaders/csv_loader.py +458 -215
- oscura/loaders/hdf5_loader.py +278 -119
- oscura/loaders/lazy.py +87 -54
- oscura/loaders/mmap_loader.py +1 -1
- oscura/loaders/numpy_loader.py +253 -116
- oscura/loaders/pcap.py +226 -151
- oscura/loaders/rigol.py +110 -49
- oscura/loaders/sigrok.py +201 -78
- oscura/loaders/tdms.py +81 -58
- oscura/loaders/tektronix.py +291 -174
- oscura/loaders/touchstone.py +182 -87
- oscura/loaders/vcd.py +215 -117
- oscura/loaders/wav.py +155 -68
- oscura/reporting/__init__.py +9 -7
- oscura/reporting/analyze.py +352 -146
- oscura/reporting/argument_preparer.py +69 -14
- oscura/reporting/auto_report.py +97 -61
- oscura/reporting/batch.py +131 -58
- oscura/reporting/chart_selection.py +57 -45
- oscura/reporting/comparison.py +63 -17
- oscura/reporting/content/executive.py +76 -24
- oscura/reporting/core_formats/multi_format.py +11 -8
- oscura/reporting/engine.py +312 -158
- oscura/reporting/enhanced_reports.py +949 -0
- oscura/reporting/export.py +86 -43
- oscura/reporting/formatting/numbers.py +69 -42
- oscura/reporting/html.py +139 -58
- oscura/reporting/index.py +137 -65
- oscura/reporting/output.py +158 -67
- oscura/reporting/pdf.py +67 -102
- oscura/reporting/plots.py +191 -112
- oscura/reporting/sections.py +88 -47
- oscura/reporting/standards.py +104 -61
- oscura/reporting/summary_generator.py +75 -55
- oscura/reporting/tables.py +138 -54
- oscura/reporting/templates/enhanced/protocol_re.html +525 -0
- oscura/reporting/templates/index.md +13 -13
- oscura/sessions/__init__.py +14 -23
- oscura/sessions/base.py +3 -3
- oscura/sessions/blackbox.py +106 -10
- oscura/sessions/generic.py +2 -2
- oscura/sessions/legacy.py +783 -0
- oscura/side_channel/__init__.py +63 -0
- oscura/side_channel/dpa.py +1025 -0
- oscura/utils/__init__.py +15 -1
- oscura/utils/autodetect.py +1 -5
- oscura/utils/bitwise.py +118 -0
- oscura/{builders → utils/builders}/__init__.py +1 -1
- oscura/{comparison → utils/comparison}/__init__.py +6 -6
- oscura/{comparison → utils/comparison}/compare.py +202 -101
- oscura/{comparison → utils/comparison}/golden.py +83 -63
- oscura/{comparison → utils/comparison}/limits.py +313 -89
- oscura/{comparison → utils/comparison}/mask.py +151 -45
- oscura/{comparison → utils/comparison}/trace_diff.py +1 -1
- oscura/{comparison → utils/comparison}/visualization.py +147 -89
- oscura/{component → utils/component}/__init__.py +3 -3
- oscura/{component → utils/component}/impedance.py +122 -58
- oscura/{component → utils/component}/reactive.py +165 -168
- oscura/{component → utils/component}/transmission_line.py +3 -3
- oscura/{filtering → utils/filtering}/__init__.py +6 -6
- oscura/{filtering → utils/filtering}/base.py +1 -1
- oscura/{filtering → utils/filtering}/convenience.py +2 -2
- oscura/{filtering → utils/filtering}/design.py +169 -93
- oscura/{filtering → utils/filtering}/filters.py +2 -2
- oscura/{filtering → utils/filtering}/introspection.py +2 -2
- oscura/utils/geometry.py +31 -0
- oscura/utils/imports.py +184 -0
- oscura/utils/lazy.py +1 -1
- oscura/{math → utils/math}/__init__.py +2 -2
- oscura/{math → utils/math}/arithmetic.py +114 -48
- oscura/{math → utils/math}/interpolation.py +139 -106
- oscura/utils/memory.py +129 -66
- oscura/utils/memory_advanced.py +92 -9
- oscura/utils/memory_extensions.py +10 -8
- oscura/{optimization → utils/optimization}/__init__.py +1 -1
- oscura/{optimization → utils/optimization}/search.py +2 -2
- oscura/utils/performance/__init__.py +58 -0
- oscura/utils/performance/caching.py +889 -0
- oscura/utils/performance/lsh_clustering.py +333 -0
- oscura/utils/performance/memory_optimizer.py +699 -0
- oscura/utils/performance/optimizations.py +675 -0
- oscura/utils/performance/parallel.py +654 -0
- oscura/utils/performance/profiling.py +661 -0
- oscura/{pipeline → utils/pipeline}/base.py +1 -1
- oscura/{pipeline → utils/pipeline}/composition.py +11 -3
- oscura/{pipeline → utils/pipeline}/parallel.py +3 -2
- oscura/{pipeline → utils/pipeline}/pipeline.py +1 -1
- oscura/{pipeline → utils/pipeline}/reverse_engineering.py +412 -221
- oscura/{search → utils/search}/__init__.py +3 -3
- oscura/{search → utils/search}/anomaly.py +188 -58
- oscura/utils/search/context.py +294 -0
- oscura/{search → utils/search}/pattern.py +138 -10
- oscura/utils/serial.py +51 -0
- oscura/utils/storage/__init__.py +61 -0
- oscura/utils/storage/database.py +1166 -0
- oscura/{streaming → utils/streaming}/chunked.py +302 -143
- oscura/{streaming → utils/streaming}/progressive.py +1 -1
- oscura/{streaming → utils/streaming}/realtime.py +3 -2
- oscura/{triggering → utils/triggering}/__init__.py +6 -6
- oscura/{triggering → utils/triggering}/base.py +6 -6
- oscura/{triggering → utils/triggering}/edge.py +2 -2
- oscura/{triggering → utils/triggering}/pattern.py +2 -2
- oscura/{triggering → utils/triggering}/pulse.py +115 -74
- oscura/{triggering → utils/triggering}/window.py +2 -2
- oscura/utils/validation.py +32 -0
- oscura/validation/__init__.py +121 -0
- oscura/{compliance → validation/compliance}/__init__.py +5 -5
- oscura/{compliance → validation/compliance}/advanced.py +5 -5
- oscura/{compliance → validation/compliance}/masks.py +1 -1
- oscura/{compliance → validation/compliance}/reporting.py +127 -53
- oscura/{compliance → validation/compliance}/testing.py +114 -52
- oscura/validation/compliance_tests.py +915 -0
- oscura/validation/fuzzer.py +990 -0
- oscura/validation/grammar_tests.py +596 -0
- oscura/validation/grammar_validator.py +904 -0
- oscura/validation/hil_testing.py +977 -0
- oscura/{quality → validation/quality}/__init__.py +4 -4
- oscura/{quality → validation/quality}/ensemble.py +251 -171
- oscura/{quality → validation/quality}/explainer.py +3 -3
- oscura/{quality → validation/quality}/scoring.py +1 -1
- oscura/{quality → validation/quality}/warnings.py +4 -4
- oscura/validation/regression_suite.py +808 -0
- oscura/validation/replay.py +788 -0
- oscura/{testing → validation/testing}/__init__.py +2 -2
- oscura/{testing → validation/testing}/synthetic.py +5 -5
- oscura/visualization/__init__.py +9 -0
- oscura/visualization/accessibility.py +1 -1
- oscura/visualization/annotations.py +64 -67
- oscura/visualization/colors.py +7 -7
- oscura/visualization/digital.py +180 -81
- oscura/visualization/eye.py +236 -85
- oscura/visualization/interactive.py +320 -143
- oscura/visualization/jitter.py +587 -247
- oscura/visualization/layout.py +169 -134
- oscura/visualization/optimization.py +103 -52
- oscura/visualization/palettes.py +1 -1
- oscura/visualization/power.py +427 -211
- oscura/visualization/power_extended.py +626 -297
- oscura/visualization/presets.py +2 -0
- oscura/visualization/protocols.py +495 -181
- oscura/visualization/render.py +79 -63
- oscura/visualization/reverse_engineering.py +171 -124
- oscura/visualization/signal_integrity.py +460 -279
- oscura/visualization/specialized.py +190 -100
- oscura/visualization/spectral.py +670 -255
- oscura/visualization/thumbnails.py +166 -137
- oscura/visualization/waveform.py +150 -63
- oscura/workflows/__init__.py +3 -0
- oscura/{batch → workflows/batch}/__init__.py +5 -5
- oscura/{batch → workflows/batch}/advanced.py +150 -75
- oscura/workflows/batch/aggregate.py +531 -0
- oscura/workflows/batch/analyze.py +236 -0
- oscura/{batch → workflows/batch}/logging.py +2 -2
- oscura/{batch → workflows/batch}/metrics.py +1 -1
- oscura/workflows/complete_re.py +1144 -0
- oscura/workflows/compliance.py +44 -54
- oscura/workflows/digital.py +197 -51
- oscura/workflows/legacy/__init__.py +12 -0
- oscura/{workflow → workflows/legacy}/dag.py +4 -1
- oscura/workflows/multi_trace.py +9 -9
- oscura/workflows/power.py +42 -62
- oscura/workflows/protocol.py +82 -49
- oscura/workflows/reverse_engineering.py +351 -150
- oscura/workflows/signal_integrity.py +157 -82
- oscura-0.6.0.dist-info/METADATA +643 -0
- oscura-0.6.0.dist-info/RECORD +590 -0
- oscura/analyzers/digital/ic_database.py +0 -498
- oscura/analyzers/digital/timing_paths.py +0 -339
- oscura/analyzers/digital/vintage.py +0 -377
- oscura/analyzers/digital/vintage_result.py +0 -148
- oscura/analyzers/protocols/parallel_bus.py +0 -449
- oscura/batch/aggregate.py +0 -300
- oscura/batch/analyze.py +0 -139
- oscura/dsl/__init__.py +0 -73
- oscura/exceptions.py +0 -59
- oscura/exploratory/fuzzy.py +0 -513
- oscura/exploratory/sync.py +0 -384
- oscura/export/wavedrom.py +0 -430
- oscura/exporters/__init__.py +0 -94
- oscura/exporters/csv.py +0 -303
- oscura/exporters/exporters.py +0 -44
- oscura/exporters/hdf5.py +0 -217
- oscura/exporters/html_export.py +0 -701
- oscura/exporters/json_export.py +0 -338
- oscura/exporters/markdown_export.py +0 -367
- oscura/exporters/matlab_export.py +0 -354
- oscura/exporters/npz_export.py +0 -219
- oscura/exporters/spice_export.py +0 -210
- oscura/exporters/vintage_logic_csv.py +0 -247
- oscura/reporting/vintage_logic_report.py +0 -523
- oscura/search/context.py +0 -149
- oscura/session/__init__.py +0 -34
- oscura/session/annotations.py +0 -289
- oscura/session/history.py +0 -313
- oscura/session/session.py +0 -520
- oscura/visualization/digital_advanced.py +0 -718
- oscura/visualization/figure_manager.py +0 -156
- oscura/workflow/__init__.py +0 -13
- oscura-0.5.0.dist-info/METADATA +0 -407
- oscura-0.5.0.dist-info/RECORD +0 -486
- /oscura/core/{config.py → config/legacy.py} +0 -0
- /oscura/{extensibility → core/extensibility}/__init__.py +0 -0
- /oscura/{extensibility → core/extensibility}/registry.py +0 -0
- /oscura/{plugins → core/plugins}/isolation.py +0 -0
- /oscura/{builders → utils/builders}/signal_builder.py +0 -0
- /oscura/{optimization → utils/optimization}/parallel.py +0 -0
- /oscura/{pipeline → utils/pipeline}/__init__.py +0 -0
- /oscura/{streaming → utils/streaming}/__init__.py +0 -0
- {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/WHEEL +0 -0
- {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/entry_points.txt +0 -0
- {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,783 @@
|
|
|
1
|
+
"""Legacy session management for backward compatibility.
|
|
2
|
+
|
|
3
|
+
This module provides backward compatibility with the old Session API,
|
|
4
|
+
which has been superseded by the AnalysisSession hierarchy.
|
|
5
|
+
|
|
6
|
+
For new code, use:
|
|
7
|
+
- GenericSession for general waveform analysis
|
|
8
|
+
- BlackBoxSession for protocol reverse engineering
|
|
9
|
+
- Or extend AnalysisSession for custom workflows
|
|
10
|
+
|
|
11
|
+
This module exists only to support existing code and tests.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import gzip
|
|
17
|
+
import hashlib
|
|
18
|
+
import hmac
|
|
19
|
+
import pickle
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
from datetime import datetime
|
|
22
|
+
from enum import Enum
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
from oscura.core.exceptions import SecurityError
|
|
27
|
+
|
|
28
|
+
# Session file format constants
|
|
29
|
+
_SESSION_MAGIC = b"OSC1" # Magic bytes for new format with signature
|
|
30
|
+
_SESSION_SIGNATURE_SIZE = 32 # SHA256 hash size in bytes
|
|
31
|
+
_SECURITY_KEY = hashlib.sha256(b"oscura-session-v1").digest()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class AnnotationType(Enum):
|
|
35
|
+
"""Types of annotations."""
|
|
36
|
+
|
|
37
|
+
POINT = "point" # Single time point
|
|
38
|
+
RANGE = "range" # Time range
|
|
39
|
+
VERTICAL = "vertical" # Vertical line
|
|
40
|
+
HORIZONTAL = "horizontal" # Horizontal line
|
|
41
|
+
REGION = "region" # 2D region (time + amplitude)
|
|
42
|
+
TEXT = "text" # Free-floating text
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class Annotation:
|
|
47
|
+
"""Single annotation on a trace.
|
|
48
|
+
|
|
49
|
+
Attributes:
|
|
50
|
+
text: Annotation text/label
|
|
51
|
+
time: Time point (for point annotations)
|
|
52
|
+
time_range: (start, end) time range
|
|
53
|
+
amplitude: Amplitude value (for horizontal lines)
|
|
54
|
+
amplitude_range: (min, max) amplitude range
|
|
55
|
+
annotation_type: Type of annotation
|
|
56
|
+
color: Display color (hex or name)
|
|
57
|
+
style: Line style ('solid', 'dashed', 'dotted')
|
|
58
|
+
visible: Whether annotation is visible
|
|
59
|
+
created_at: Creation timestamp
|
|
60
|
+
metadata: Additional metadata
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
text: str
|
|
64
|
+
time: float | None = None
|
|
65
|
+
time_range: tuple[float, float] | None = None
|
|
66
|
+
amplitude: float | None = None
|
|
67
|
+
amplitude_range: tuple[float, float] | None = None
|
|
68
|
+
annotation_type: AnnotationType = AnnotationType.POINT
|
|
69
|
+
color: str = "#FF6B6B"
|
|
70
|
+
style: str = "solid"
|
|
71
|
+
visible: bool = True
|
|
72
|
+
created_at: datetime = field(default_factory=datetime.now)
|
|
73
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
74
|
+
|
|
75
|
+
def __post_init__(self) -> None:
|
|
76
|
+
"""Infer annotation type from provided parameters."""
|
|
77
|
+
if self.annotation_type == AnnotationType.POINT:
|
|
78
|
+
if self.amplitude_range is not None and self.time_range is not None:
|
|
79
|
+
self.annotation_type = AnnotationType.REGION
|
|
80
|
+
elif self.time_range is not None:
|
|
81
|
+
self.annotation_type = AnnotationType.RANGE
|
|
82
|
+
elif self.amplitude is not None and self.time is None:
|
|
83
|
+
self.annotation_type = AnnotationType.HORIZONTAL
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def start_time(self) -> float | None:
|
|
87
|
+
"""Get start time for range annotations."""
|
|
88
|
+
if self.time_range:
|
|
89
|
+
return self.time_range[0]
|
|
90
|
+
return self.time
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def end_time(self) -> float | None:
|
|
94
|
+
"""Get end time for range annotations."""
|
|
95
|
+
if self.time_range:
|
|
96
|
+
return self.time_range[1]
|
|
97
|
+
return self.time
|
|
98
|
+
|
|
99
|
+
def to_dict(self) -> dict[str, Any]:
|
|
100
|
+
"""Convert to dictionary for serialization."""
|
|
101
|
+
return {
|
|
102
|
+
"text": self.text,
|
|
103
|
+
"time": self.time,
|
|
104
|
+
"time_range": self.time_range,
|
|
105
|
+
"amplitude": self.amplitude,
|
|
106
|
+
"amplitude_range": self.amplitude_range,
|
|
107
|
+
"annotation_type": self.annotation_type.value,
|
|
108
|
+
"color": self.color,
|
|
109
|
+
"style": self.style,
|
|
110
|
+
"visible": self.visible,
|
|
111
|
+
"created_at": self.created_at.isoformat(),
|
|
112
|
+
"metadata": self.metadata,
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
@classmethod
|
|
116
|
+
def from_dict(cls, data: dict[str, Any]) -> Annotation:
|
|
117
|
+
"""Create from dictionary."""
|
|
118
|
+
data = data.copy()
|
|
119
|
+
data["annotation_type"] = AnnotationType(data.get("annotation_type", "point"))
|
|
120
|
+
if "created_at" in data and isinstance(data["created_at"], str):
|
|
121
|
+
data["created_at"] = datetime.fromisoformat(data["created_at"])
|
|
122
|
+
return cls(**data)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@dataclass
|
|
126
|
+
class AnnotationLayer:
|
|
127
|
+
"""Collection of related annotations.
|
|
128
|
+
|
|
129
|
+
Attributes:
|
|
130
|
+
name: Layer name
|
|
131
|
+
annotations: List of annotations
|
|
132
|
+
visible: Whether layer is visible
|
|
133
|
+
locked: Whether layer is locked (read-only)
|
|
134
|
+
color: Default color for new annotations
|
|
135
|
+
description: Layer description
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
name: str
|
|
139
|
+
annotations: list[Annotation] = field(default_factory=list)
|
|
140
|
+
visible: bool = True
|
|
141
|
+
locked: bool = False
|
|
142
|
+
color: str = "#FF6B6B"
|
|
143
|
+
description: str = ""
|
|
144
|
+
|
|
145
|
+
def add(
|
|
146
|
+
self,
|
|
147
|
+
annotation: Annotation | None = None,
|
|
148
|
+
*,
|
|
149
|
+
text: str = "",
|
|
150
|
+
time: float | None = None,
|
|
151
|
+
time_range: tuple[float, float] | None = None,
|
|
152
|
+
**kwargs: Any,
|
|
153
|
+
) -> Annotation:
|
|
154
|
+
"""Add annotation to layer.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
annotation: Pre-built Annotation object.
|
|
158
|
+
text: Annotation text (if not using pre-built).
|
|
159
|
+
time: Time point.
|
|
160
|
+
time_range: Time range.
|
|
161
|
+
**kwargs: Additional Annotation parameters.
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
Added annotation.
|
|
165
|
+
|
|
166
|
+
Raises:
|
|
167
|
+
ValueError: If layer is locked.
|
|
168
|
+
"""
|
|
169
|
+
if self.locked:
|
|
170
|
+
raise ValueError(f"Layer '{self.name}' is locked")
|
|
171
|
+
|
|
172
|
+
if annotation is None:
|
|
173
|
+
annotation = Annotation(
|
|
174
|
+
text=text,
|
|
175
|
+
time=time,
|
|
176
|
+
time_range=time_range,
|
|
177
|
+
color=kwargs.pop("color", self.color),
|
|
178
|
+
**kwargs,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
self.annotations.append(annotation)
|
|
182
|
+
return annotation
|
|
183
|
+
|
|
184
|
+
def remove(self, annotation: Annotation) -> bool:
|
|
185
|
+
"""Remove annotation from layer.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
annotation: Annotation to remove.
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
True if removed, False if not found.
|
|
192
|
+
|
|
193
|
+
Raises:
|
|
194
|
+
ValueError: If layer is locked.
|
|
195
|
+
"""
|
|
196
|
+
if self.locked:
|
|
197
|
+
raise ValueError(f"Layer '{self.name}' is locked")
|
|
198
|
+
|
|
199
|
+
try:
|
|
200
|
+
self.annotations.remove(annotation)
|
|
201
|
+
return True
|
|
202
|
+
except ValueError:
|
|
203
|
+
return False
|
|
204
|
+
|
|
205
|
+
def find_at_time(
|
|
206
|
+
self,
|
|
207
|
+
time: float,
|
|
208
|
+
tolerance: float = 0.0,
|
|
209
|
+
) -> list[Annotation]:
|
|
210
|
+
"""Find annotations at or near a specific time.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
time: Time to search.
|
|
214
|
+
tolerance: Time tolerance for matching.
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
List of matching annotations.
|
|
218
|
+
"""
|
|
219
|
+
matches = []
|
|
220
|
+
for ann in self.annotations:
|
|
221
|
+
if ann.time is not None:
|
|
222
|
+
if abs(ann.time - time) <= tolerance:
|
|
223
|
+
matches.append(ann)
|
|
224
|
+
elif ann.time_range is not None and (
|
|
225
|
+
ann.time_range[0] - tolerance <= time <= ann.time_range[1] + tolerance
|
|
226
|
+
):
|
|
227
|
+
matches.append(ann)
|
|
228
|
+
return matches
|
|
229
|
+
|
|
230
|
+
def find_in_range(
|
|
231
|
+
self,
|
|
232
|
+
start_time: float,
|
|
233
|
+
end_time: float,
|
|
234
|
+
) -> list[Annotation]:
|
|
235
|
+
"""Find annotations within a time range.
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
start_time: Range start.
|
|
239
|
+
end_time: Range end.
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
List of annotations within range.
|
|
243
|
+
"""
|
|
244
|
+
matches = []
|
|
245
|
+
for ann in self.annotations:
|
|
246
|
+
ann_start = ann.start_time
|
|
247
|
+
ann_end = ann.end_time
|
|
248
|
+
|
|
249
|
+
if ann_start is not None and (
|
|
250
|
+
start_time <= ann_start <= end_time
|
|
251
|
+
or (ann_end is not None and ann_start <= end_time and ann_end >= start_time)
|
|
252
|
+
):
|
|
253
|
+
matches.append(ann)
|
|
254
|
+
|
|
255
|
+
return matches
|
|
256
|
+
|
|
257
|
+
def clear(self) -> int:
|
|
258
|
+
"""Remove all annotations.
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
Number of annotations removed.
|
|
262
|
+
|
|
263
|
+
Raises:
|
|
264
|
+
ValueError: If layer is locked.
|
|
265
|
+
"""
|
|
266
|
+
if self.locked:
|
|
267
|
+
raise ValueError(f"Layer '{self.name}' is locked")
|
|
268
|
+
|
|
269
|
+
count = len(self.annotations)
|
|
270
|
+
self.annotations.clear()
|
|
271
|
+
return count
|
|
272
|
+
|
|
273
|
+
def to_dict(self) -> dict[str, Any]:
|
|
274
|
+
"""Convert to dictionary for serialization."""
|
|
275
|
+
return {
|
|
276
|
+
"name": self.name,
|
|
277
|
+
"annotations": [ann.to_dict() for ann in self.annotations],
|
|
278
|
+
"visible": self.visible,
|
|
279
|
+
"locked": self.locked,
|
|
280
|
+
"color": self.color,
|
|
281
|
+
"description": self.description,
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
@classmethod
|
|
285
|
+
def from_dict(cls, data: dict[str, Any]) -> AnnotationLayer:
|
|
286
|
+
"""Create from dictionary."""
|
|
287
|
+
data = data.copy()
|
|
288
|
+
annotations_data = data.pop("annotations", [])
|
|
289
|
+
layer = cls(**data)
|
|
290
|
+
layer.annotations = [Annotation.from_dict(ann) for ann in annotations_data]
|
|
291
|
+
return layer
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
@dataclass
|
|
295
|
+
class HistoryEntry:
|
|
296
|
+
"""Single history entry recording an operation.
|
|
297
|
+
|
|
298
|
+
Attributes:
|
|
299
|
+
operation: Operation name (function/method called)
|
|
300
|
+
parameters: Input parameters
|
|
301
|
+
result: Operation result (summary)
|
|
302
|
+
timestamp: When operation was performed
|
|
303
|
+
duration_ms: Operation duration in milliseconds
|
|
304
|
+
success: Whether operation succeeded
|
|
305
|
+
error_message: Error message if failed
|
|
306
|
+
metadata: Additional metadata
|
|
307
|
+
"""
|
|
308
|
+
|
|
309
|
+
operation: str
|
|
310
|
+
parameters: dict[str, Any] = field(default_factory=dict)
|
|
311
|
+
result: Any = None
|
|
312
|
+
timestamp: datetime = field(default_factory=datetime.now)
|
|
313
|
+
duration_ms: float = 0.0
|
|
314
|
+
success: bool = True
|
|
315
|
+
error_message: str | None = None
|
|
316
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
317
|
+
|
|
318
|
+
def to_dict(self) -> dict[str, Any]:
|
|
319
|
+
"""Convert to dictionary for serialization."""
|
|
320
|
+
return {
|
|
321
|
+
"operation": self.operation,
|
|
322
|
+
"parameters": self.parameters,
|
|
323
|
+
"result": self._serialize_result(self.result),
|
|
324
|
+
"timestamp": self.timestamp.isoformat(),
|
|
325
|
+
"duration_ms": self.duration_ms,
|
|
326
|
+
"success": self.success,
|
|
327
|
+
"error_message": self.error_message,
|
|
328
|
+
"metadata": self.metadata,
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
@staticmethod
|
|
332
|
+
def _serialize_result(result: Any) -> Any:
|
|
333
|
+
"""Serialize result for JSON storage."""
|
|
334
|
+
if result is None:
|
|
335
|
+
return None
|
|
336
|
+
if isinstance(result, str | int | float | bool):
|
|
337
|
+
return result
|
|
338
|
+
if isinstance(result, dict):
|
|
339
|
+
return {k: HistoryEntry._serialize_result(v) for k, v in result.items()}
|
|
340
|
+
if isinstance(result, list | tuple):
|
|
341
|
+
return [HistoryEntry._serialize_result(v) for v in result]
|
|
342
|
+
# For complex objects, store string representation
|
|
343
|
+
return str(result)
|
|
344
|
+
|
|
345
|
+
@classmethod
|
|
346
|
+
def from_dict(cls, data: dict[str, Any]) -> HistoryEntry:
|
|
347
|
+
"""Create from dictionary."""
|
|
348
|
+
data = data.copy()
|
|
349
|
+
if "timestamp" in data and isinstance(data["timestamp"], str):
|
|
350
|
+
data["timestamp"] = datetime.fromisoformat(data["timestamp"])
|
|
351
|
+
return cls(**data)
|
|
352
|
+
|
|
353
|
+
def to_code(self) -> str:
|
|
354
|
+
"""Generate Python code to replay this operation.
|
|
355
|
+
|
|
356
|
+
Returns:
|
|
357
|
+
Python code string.
|
|
358
|
+
"""
|
|
359
|
+
# Format parameters
|
|
360
|
+
params = []
|
|
361
|
+
for k, v in self.parameters.items():
|
|
362
|
+
if isinstance(v, str):
|
|
363
|
+
params.append(f'{k}="{v}"')
|
|
364
|
+
else:
|
|
365
|
+
params.append(f"{k}={v!r}")
|
|
366
|
+
|
|
367
|
+
param_str = ", ".join(params)
|
|
368
|
+
return f"osc.{self.operation}({param_str})"
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
@dataclass
|
|
372
|
+
class OperationHistory:
|
|
373
|
+
"""History of analysis operations.
|
|
374
|
+
|
|
375
|
+
Supports recording, replaying, and exporting operation history.
|
|
376
|
+
|
|
377
|
+
Attributes:
|
|
378
|
+
entries: List of history entries
|
|
379
|
+
max_entries: Maximum entries to keep (0 = unlimited)
|
|
380
|
+
auto_record: Whether to automatically record operations
|
|
381
|
+
"""
|
|
382
|
+
|
|
383
|
+
entries: list[HistoryEntry] = field(default_factory=list)
|
|
384
|
+
max_entries: int = 0
|
|
385
|
+
auto_record: bool = True
|
|
386
|
+
_current_session_start: datetime = field(default_factory=datetime.now)
|
|
387
|
+
|
|
388
|
+
def record(
|
|
389
|
+
self,
|
|
390
|
+
operation: str,
|
|
391
|
+
parameters: dict[str, Any] | None = None,
|
|
392
|
+
result: Any = None,
|
|
393
|
+
duration_ms: float = 0.0,
|
|
394
|
+
success: bool = True,
|
|
395
|
+
error_message: str | None = None,
|
|
396
|
+
**metadata: Any,
|
|
397
|
+
) -> HistoryEntry:
|
|
398
|
+
"""Record an operation.
|
|
399
|
+
|
|
400
|
+
Args:
|
|
401
|
+
operation: Operation name.
|
|
402
|
+
parameters: Input parameters.
|
|
403
|
+
result: Operation result.
|
|
404
|
+
duration_ms: Duration in milliseconds.
|
|
405
|
+
success: Whether operation succeeded.
|
|
406
|
+
error_message: Error message if failed.
|
|
407
|
+
**metadata: Additional metadata.
|
|
408
|
+
|
|
409
|
+
Returns:
|
|
410
|
+
Created history entry.
|
|
411
|
+
"""
|
|
412
|
+
entry = HistoryEntry(
|
|
413
|
+
operation=operation,
|
|
414
|
+
parameters=parameters or {},
|
|
415
|
+
result=result,
|
|
416
|
+
duration_ms=duration_ms,
|
|
417
|
+
success=success,
|
|
418
|
+
error_message=error_message,
|
|
419
|
+
metadata=metadata,
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
self.entries.append(entry)
|
|
423
|
+
|
|
424
|
+
# Trim if exceeded max entries
|
|
425
|
+
if self.max_entries > 0 and len(self.entries) > self.max_entries:
|
|
426
|
+
self.entries = self.entries[-self.max_entries :]
|
|
427
|
+
|
|
428
|
+
return entry
|
|
429
|
+
|
|
430
|
+
def clear(self) -> None:
|
|
431
|
+
"""Clear all history entries."""
|
|
432
|
+
self.entries.clear()
|
|
433
|
+
|
|
434
|
+
def to_script(self, include_imports: bool = True) -> str:
|
|
435
|
+
"""Generate Python script to replay operations.
|
|
436
|
+
|
|
437
|
+
Args:
|
|
438
|
+
include_imports: Whether to include import statement.
|
|
439
|
+
|
|
440
|
+
Returns:
|
|
441
|
+
Python script as string.
|
|
442
|
+
"""
|
|
443
|
+
lines = []
|
|
444
|
+
|
|
445
|
+
if include_imports:
|
|
446
|
+
lines.append("import oscura as osc")
|
|
447
|
+
lines.append("")
|
|
448
|
+
|
|
449
|
+
for entry in self.entries:
|
|
450
|
+
if entry.success:
|
|
451
|
+
lines.append(entry.to_code())
|
|
452
|
+
|
|
453
|
+
return "\n".join(lines)
|
|
454
|
+
|
|
455
|
+
def to_dict(self) -> dict[str, Any]:
|
|
456
|
+
"""Convert to dictionary for serialization."""
|
|
457
|
+
return {
|
|
458
|
+
"entries": [entry.to_dict() for entry in self.entries],
|
|
459
|
+
"max_entries": self.max_entries,
|
|
460
|
+
"auto_record": self.auto_record,
|
|
461
|
+
"session_start": self._current_session_start.isoformat(),
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
@classmethod
|
|
465
|
+
def from_dict(cls, data: dict[str, Any]) -> OperationHistory:
|
|
466
|
+
"""Create from dictionary."""
|
|
467
|
+
data = data.copy()
|
|
468
|
+
entries_data = data.pop("entries", [])
|
|
469
|
+
if "session_start" in data:
|
|
470
|
+
data["_current_session_start"] = datetime.fromisoformat(data.pop("session_start"))
|
|
471
|
+
history = cls(**data)
|
|
472
|
+
history.entries = [HistoryEntry.from_dict(entry) for entry in entries_data]
|
|
473
|
+
return history
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
@dataclass
|
|
477
|
+
class Session:
|
|
478
|
+
"""Analysis session container (legacy API).
|
|
479
|
+
|
|
480
|
+
NOTE: This is the legacy Session API for backward compatibility.
|
|
481
|
+
For new code, use:
|
|
482
|
+
- GenericSession for general waveform analysis
|
|
483
|
+
- BlackBoxSession for protocol reverse engineering
|
|
484
|
+
- Or extend AnalysisSession for custom workflows
|
|
485
|
+
|
|
486
|
+
Manages traces, annotations, measurements, and history for a complete
|
|
487
|
+
analysis session. Sessions can be saved and restored.
|
|
488
|
+
|
|
489
|
+
Attributes:
|
|
490
|
+
name: Session name
|
|
491
|
+
traces: Dictionary of loaded traces (name -> trace)
|
|
492
|
+
annotation_layers: Annotation layers
|
|
493
|
+
measurements: Recorded measurements
|
|
494
|
+
history: Operation history
|
|
495
|
+
metadata: Session metadata
|
|
496
|
+
created_at: Creation timestamp
|
|
497
|
+
modified_at: Last modification timestamp
|
|
498
|
+
"""
|
|
499
|
+
|
|
500
|
+
name: str = "Untitled Session"
|
|
501
|
+
traces: dict[str, Any] = field(default_factory=dict)
|
|
502
|
+
annotation_layers: dict[str, AnnotationLayer] = field(default_factory=dict)
|
|
503
|
+
measurements: dict[str, Any] = field(default_factory=dict)
|
|
504
|
+
history: OperationHistory = field(default_factory=OperationHistory)
|
|
505
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
506
|
+
created_at: datetime = field(default_factory=datetime.now)
|
|
507
|
+
modified_at: datetime = field(default_factory=datetime.now)
|
|
508
|
+
_file_path: Path | None = None
|
|
509
|
+
|
|
510
|
+
def __post_init__(self) -> None:
|
|
511
|
+
"""Initialize default annotation layer."""
|
|
512
|
+
if "default" not in self.annotation_layers:
|
|
513
|
+
self.annotation_layers["default"] = AnnotationLayer("Default")
|
|
514
|
+
|
|
515
|
+
def load_trace(
|
|
516
|
+
self,
|
|
517
|
+
path: str | Path,
|
|
518
|
+
name: str | None = None,
|
|
519
|
+
**load_kwargs: Any,
|
|
520
|
+
) -> Any:
|
|
521
|
+
"""Load a trace into the session.
|
|
522
|
+
|
|
523
|
+
Args:
|
|
524
|
+
path: Path to trace file.
|
|
525
|
+
name: Name for trace in session (default: filename).
|
|
526
|
+
**load_kwargs: Additional arguments for load().
|
|
527
|
+
|
|
528
|
+
Returns:
|
|
529
|
+
Loaded trace.
|
|
530
|
+
"""
|
|
531
|
+
from oscura.loaders import load
|
|
532
|
+
|
|
533
|
+
path = Path(path)
|
|
534
|
+
trace = load(str(path), **load_kwargs)
|
|
535
|
+
|
|
536
|
+
if name is None:
|
|
537
|
+
name = path.stem
|
|
538
|
+
|
|
539
|
+
self.traces[name] = trace
|
|
540
|
+
self._mark_modified()
|
|
541
|
+
|
|
542
|
+
self.history.record(
|
|
543
|
+
"load_trace",
|
|
544
|
+
{"path": str(path), "name": name},
|
|
545
|
+
result=f"Loaded {name}",
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
return trace
|
|
549
|
+
|
|
550
|
+
def add_trace(
|
|
551
|
+
self,
|
|
552
|
+
name: str,
|
|
553
|
+
trace: Any,
|
|
554
|
+
) -> None:
|
|
555
|
+
"""Add an in-memory trace to the session.
|
|
556
|
+
|
|
557
|
+
Args:
|
|
558
|
+
name: Name for the trace in the session.
|
|
559
|
+
trace: Trace object (WaveformTrace, DigitalTrace, etc.).
|
|
560
|
+
|
|
561
|
+
Raises:
|
|
562
|
+
ValueError: If name is empty or already exists.
|
|
563
|
+
TypeError: If trace doesn't have expected attributes.
|
|
564
|
+
"""
|
|
565
|
+
if not name:
|
|
566
|
+
raise ValueError("Trace name cannot be empty")
|
|
567
|
+
|
|
568
|
+
if not hasattr(trace, "data"):
|
|
569
|
+
raise TypeError("Trace must have a 'data' attribute")
|
|
570
|
+
|
|
571
|
+
self.traces[name] = trace
|
|
572
|
+
self._mark_modified()
|
|
573
|
+
|
|
574
|
+
self.history.record(
|
|
575
|
+
"add_trace",
|
|
576
|
+
{"name": name, "type": type(trace).__name__},
|
|
577
|
+
result=f"Added {name}",
|
|
578
|
+
)
|
|
579
|
+
|
|
580
|
+
def remove_trace(self, name: str) -> None:
|
|
581
|
+
"""Remove a trace from the session.
|
|
582
|
+
|
|
583
|
+
Args:
|
|
584
|
+
name: Name of the trace to remove.
|
|
585
|
+
|
|
586
|
+
Raises:
|
|
587
|
+
KeyError: If trace not found.
|
|
588
|
+
"""
|
|
589
|
+
if name not in self.traces:
|
|
590
|
+
raise KeyError(f"Trace '{name}' not found in session")
|
|
591
|
+
|
|
592
|
+
del self.traces[name]
|
|
593
|
+
self._mark_modified()
|
|
594
|
+
|
|
595
|
+
self.history.record(
|
|
596
|
+
"remove_trace",
|
|
597
|
+
{"name": name},
|
|
598
|
+
result=f"Removed {name}",
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
def get_trace(self, name: str) -> Any:
|
|
602
|
+
"""Get trace by name.
|
|
603
|
+
|
|
604
|
+
Args:
|
|
605
|
+
name: Trace name.
|
|
606
|
+
|
|
607
|
+
Returns:
|
|
608
|
+
Trace object.
|
|
609
|
+
"""
|
|
610
|
+
return self.traces[name]
|
|
611
|
+
|
|
612
|
+
def list_traces(self) -> list[str]:
|
|
613
|
+
"""List all trace names."""
|
|
614
|
+
return list(self.traces.keys())
|
|
615
|
+
|
|
616
|
+
def annotate(
|
|
617
|
+
self,
|
|
618
|
+
text: str,
|
|
619
|
+
*,
|
|
620
|
+
time: float | None = None,
|
|
621
|
+
time_range: tuple[float, float] | None = None,
|
|
622
|
+
layer: str = "default",
|
|
623
|
+
**kwargs: Any,
|
|
624
|
+
) -> None:
|
|
625
|
+
"""Add annotation to session.
|
|
626
|
+
|
|
627
|
+
Args:
|
|
628
|
+
text: Annotation text.
|
|
629
|
+
time: Time point for annotation.
|
|
630
|
+
time_range: Time range for annotation.
|
|
631
|
+
layer: Annotation layer name.
|
|
632
|
+
**kwargs: Additional annotation parameters.
|
|
633
|
+
"""
|
|
634
|
+
if layer not in self.annotation_layers:
|
|
635
|
+
self.annotation_layers[layer] = AnnotationLayer(layer)
|
|
636
|
+
|
|
637
|
+
self.annotation_layers[layer].add(
|
|
638
|
+
text=text,
|
|
639
|
+
time=time,
|
|
640
|
+
time_range=time_range,
|
|
641
|
+
**kwargs,
|
|
642
|
+
)
|
|
643
|
+
|
|
644
|
+
self._mark_modified()
|
|
645
|
+
|
|
646
|
+
def save(self, path: str | Path, *, compress: bool = True) -> None:
|
|
647
|
+
"""Save session to file.
|
|
648
|
+
|
|
649
|
+
Args:
|
|
650
|
+
path: Output file path (.tks extension).
|
|
651
|
+
compress: Whether to compress with gzip.
|
|
652
|
+
|
|
653
|
+
Raises:
|
|
654
|
+
SecurityError: If session verification fails.
|
|
655
|
+
"""
|
|
656
|
+
path = Path(path)
|
|
657
|
+
self._file_path = path
|
|
658
|
+
|
|
659
|
+
# Prepare session data
|
|
660
|
+
session_data = {
|
|
661
|
+
"name": self.name,
|
|
662
|
+
"traces": self.traces,
|
|
663
|
+
"annotation_layers": {
|
|
664
|
+
name: layer.to_dict() for name, layer in self.annotation_layers.items()
|
|
665
|
+
},
|
|
666
|
+
"measurements": self.measurements,
|
|
667
|
+
"history": self.history.to_dict(),
|
|
668
|
+
"metadata": self.metadata,
|
|
669
|
+
"created_at": self.created_at.isoformat(),
|
|
670
|
+
"modified_at": datetime.now().isoformat(),
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
# Serialize
|
|
674
|
+
pickled = pickle.dumps(session_data)
|
|
675
|
+
|
|
676
|
+
# Sign the data
|
|
677
|
+
signature = hmac.new(_SECURITY_KEY, pickled, hashlib.sha256).digest()
|
|
678
|
+
|
|
679
|
+
# Combine magic + signature + data
|
|
680
|
+
full_data = _SESSION_MAGIC + signature + pickled
|
|
681
|
+
|
|
682
|
+
# Write
|
|
683
|
+
if compress:
|
|
684
|
+
with gzip.open(path, "wb") as f:
|
|
685
|
+
f.write(full_data)
|
|
686
|
+
else:
|
|
687
|
+
path.write_bytes(full_data)
|
|
688
|
+
|
|
689
|
+
def _mark_modified(self) -> None:
|
|
690
|
+
"""Mark session as modified."""
|
|
691
|
+
self.modified_at = datetime.now()
|
|
692
|
+
|
|
693
|
+
def to_dict(self) -> dict[str, Any]:
|
|
694
|
+
"""Convert session to dictionary for export."""
|
|
695
|
+
return {
|
|
696
|
+
"name": self.name,
|
|
697
|
+
"traces": {name: str(type(trace).__name__) for name, trace in self.traces.items()},
|
|
698
|
+
"annotation_layers": {
|
|
699
|
+
name: layer.to_dict() for name, layer in self.annotation_layers.items()
|
|
700
|
+
},
|
|
701
|
+
"measurements": self.measurements,
|
|
702
|
+
"history": self.history.to_dict(),
|
|
703
|
+
"metadata": self.metadata,
|
|
704
|
+
"created_at": self.created_at.isoformat(),
|
|
705
|
+
"modified_at": self.modified_at.isoformat(),
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
def load_session(path: str | Path) -> Session:
|
|
710
|
+
"""Load session from file.
|
|
711
|
+
|
|
712
|
+
Args:
|
|
713
|
+
path: Path to session file (.tks).
|
|
714
|
+
|
|
715
|
+
Returns:
|
|
716
|
+
Loaded Session object.
|
|
717
|
+
|
|
718
|
+
Raises:
|
|
719
|
+
SecurityError: If session verification fails or file format is invalid.
|
|
720
|
+
"""
|
|
721
|
+
path = Path(path)
|
|
722
|
+
|
|
723
|
+
# Read file as raw bytes first
|
|
724
|
+
raw_data = path.read_bytes()
|
|
725
|
+
|
|
726
|
+
# Detect and decompress gzip files (magic bytes: 0x1f 0x8b)
|
|
727
|
+
if raw_data[:2] == b"\x1f\x8b":
|
|
728
|
+
import io
|
|
729
|
+
|
|
730
|
+
with gzip.open(io.BytesIO(raw_data), "rb") as f:
|
|
731
|
+
full_data = f.read()
|
|
732
|
+
else:
|
|
733
|
+
full_data = raw_data
|
|
734
|
+
|
|
735
|
+
# Check magic - missing magic bytes is a security issue (file lacks HMAC signature)
|
|
736
|
+
if not full_data.startswith(_SESSION_MAGIC):
|
|
737
|
+
raise SecurityError(
|
|
738
|
+
"Invalid session file format: missing HMAC signature (magic bytes not found)"
|
|
739
|
+
)
|
|
740
|
+
|
|
741
|
+
# Extract signature and data
|
|
742
|
+
signature = full_data[len(_SESSION_MAGIC) : len(_SESSION_MAGIC) + _SESSION_SIGNATURE_SIZE]
|
|
743
|
+
pickled = full_data[len(_SESSION_MAGIC) + _SESSION_SIGNATURE_SIZE :]
|
|
744
|
+
|
|
745
|
+
# Verify signature
|
|
746
|
+
expected_signature = hmac.new(_SECURITY_KEY, pickled, hashlib.sha256).digest()
|
|
747
|
+
if not hmac.compare_digest(signature, expected_signature):
|
|
748
|
+
raise SecurityError("Session file signature verification failed (data may be tampered)")
|
|
749
|
+
|
|
750
|
+
# Deserialize
|
|
751
|
+
session_data = pickle.loads(pickled)
|
|
752
|
+
|
|
753
|
+
# Reconstruct session
|
|
754
|
+
session = Session(
|
|
755
|
+
name=session_data["name"],
|
|
756
|
+
traces=session_data.get("traces", {}),
|
|
757
|
+
measurements=session_data.get("measurements", {}),
|
|
758
|
+
metadata=session_data.get("metadata", {}),
|
|
759
|
+
created_at=datetime.fromisoformat(session_data["created_at"]),
|
|
760
|
+
modified_at=datetime.fromisoformat(session_data["modified_at"]),
|
|
761
|
+
)
|
|
762
|
+
|
|
763
|
+
# Restore annotation layers
|
|
764
|
+
for name, layer_data in session_data.get("annotation_layers", {}).items():
|
|
765
|
+
session.annotation_layers[name] = AnnotationLayer.from_dict(layer_data)
|
|
766
|
+
|
|
767
|
+
# Restore history
|
|
768
|
+
session.history = OperationHistory.from_dict(session_data["history"])
|
|
769
|
+
|
|
770
|
+
session._file_path = path
|
|
771
|
+
|
|
772
|
+
return session
|
|
773
|
+
|
|
774
|
+
|
|
775
|
+
__all__ = [
|
|
776
|
+
"Annotation",
|
|
777
|
+
"AnnotationLayer",
|
|
778
|
+
"AnnotationType",
|
|
779
|
+
"HistoryEntry",
|
|
780
|
+
"OperationHistory",
|
|
781
|
+
"Session",
|
|
782
|
+
"load_session",
|
|
783
|
+
]
|