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,788 @@
|
|
|
1
|
+
"""Protocol replay validation framework.
|
|
2
|
+
|
|
3
|
+
This module provides a framework for validating reverse-engineered protocols by
|
|
4
|
+
replaying messages to target devices and comparing expected vs actual responses.
|
|
5
|
+
|
|
6
|
+
Supports multiple interfaces:
|
|
7
|
+
- Serial ports (UART, RS-232, RS-485)
|
|
8
|
+
- SocketCAN (Controller Area Network)
|
|
9
|
+
- UDP/TCP sockets (network protocols)
|
|
10
|
+
|
|
11
|
+
Example:
|
|
12
|
+
>>> from oscura.validation.replay import ReplayConfig, ReplayValidator
|
|
13
|
+
>>> config = ReplayConfig(
|
|
14
|
+
... interface="serial",
|
|
15
|
+
... port="/dev/ttyUSB0",
|
|
16
|
+
... baud_rate=115200,
|
|
17
|
+
... timeout=1.0
|
|
18
|
+
... )
|
|
19
|
+
>>> validator = ReplayValidator(config)
|
|
20
|
+
>>> spec = ProtocolSpec(
|
|
21
|
+
... name="MyProtocol",
|
|
22
|
+
... checksum_algorithm="crc16",
|
|
23
|
+
... expected_response_time=0.1
|
|
24
|
+
... )
|
|
25
|
+
>>> test_messages = [b"\\x01\\x02\\x03\\x04", b"\\x05\\x06\\x07\\x08"]
|
|
26
|
+
>>> result = validator.validate_protocol(spec, test_messages)
|
|
27
|
+
>>> print(f"Success: {result.success}, Messages: {result.messages_sent}")
|
|
28
|
+
Success: True, Messages: 2
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import time
|
|
34
|
+
from dataclasses import dataclass, field
|
|
35
|
+
from typing import TYPE_CHECKING, Any, Literal, Protocol
|
|
36
|
+
|
|
37
|
+
from oscura.utils.serial import connect_serial_port
|
|
38
|
+
|
|
39
|
+
if TYPE_CHECKING:
|
|
40
|
+
import socket
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class CANBusProtocol(Protocol):
|
|
44
|
+
"""Protocol for python-can Bus interface."""
|
|
45
|
+
|
|
46
|
+
def send(self, msg: Any) -> None:
|
|
47
|
+
"""Send a CAN message."""
|
|
48
|
+
...
|
|
49
|
+
|
|
50
|
+
def recv(self, timeout: float | None = None) -> Any:
|
|
51
|
+
"""Receive a CAN message."""
|
|
52
|
+
...
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class ProtocolSpec:
|
|
57
|
+
"""Protocol specification for validation.
|
|
58
|
+
|
|
59
|
+
Attributes:
|
|
60
|
+
name: Protocol name for identification.
|
|
61
|
+
checksum_algorithm: Checksum algorithm ("crc8", "crc16", "crc32", "xor", "sum").
|
|
62
|
+
checksum_position: Byte position of checksum in message (-1 for last byte).
|
|
63
|
+
expected_response_time: Expected response time in seconds.
|
|
64
|
+
timing_tolerance: Timing tolerance as fraction (0.1 = 10% tolerance).
|
|
65
|
+
require_response: Whether response is required for each message.
|
|
66
|
+
message_format: Message format description (optional).
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
name: str
|
|
70
|
+
checksum_algorithm: str = "none"
|
|
71
|
+
checksum_position: int = -1
|
|
72
|
+
expected_response_time: float = 0.1
|
|
73
|
+
timing_tolerance: float = 0.2
|
|
74
|
+
require_response: bool = True
|
|
75
|
+
message_format: str = ""
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass
|
|
79
|
+
class ReplayConfig:
|
|
80
|
+
"""Configuration for replay validation.
|
|
81
|
+
|
|
82
|
+
Attributes:
|
|
83
|
+
interface: Interface type ("serial", "socketcan", "udp", "tcp").
|
|
84
|
+
port: Port identifier ("/dev/ttyUSB0" for serial, "can0" for CAN, port number for UDP/TCP).
|
|
85
|
+
baud_rate: Baud rate for serial interface (default: 115200).
|
|
86
|
+
timeout: Timeout in seconds for receiving responses (default: 1.0).
|
|
87
|
+
validate_checksums: Enable checksum validation (default: True).
|
|
88
|
+
validate_timing: Enable timing validation (default: True).
|
|
89
|
+
max_retries: Maximum number of retries for failed sends (default: 3).
|
|
90
|
+
host: Host address for UDP/TCP (default: "localhost").
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
interface: Literal["serial", "socketcan", "udp", "tcp"]
|
|
94
|
+
port: str | int
|
|
95
|
+
baud_rate: int = 115200
|
|
96
|
+
timeout: float = 1.0
|
|
97
|
+
validate_checksums: bool = True
|
|
98
|
+
validate_timing: bool = True
|
|
99
|
+
max_retries: int = 3
|
|
100
|
+
host: str = "localhost"
|
|
101
|
+
|
|
102
|
+
def __post_init__(self) -> None:
|
|
103
|
+
"""Validate configuration after initialization."""
|
|
104
|
+
if self.interface not in ("serial", "socketcan", "udp", "tcp"):
|
|
105
|
+
raise ValueError(
|
|
106
|
+
f"Invalid interface: {self.interface}. "
|
|
107
|
+
"Must be 'serial', 'socketcan', 'udp', or 'tcp'"
|
|
108
|
+
)
|
|
109
|
+
if self.timeout <= 0:
|
|
110
|
+
raise ValueError(f"timeout must be positive, got {self.timeout}")
|
|
111
|
+
if self.max_retries < 0:
|
|
112
|
+
raise ValueError(f"max_retries must be non-negative, got {self.max_retries}")
|
|
113
|
+
if self.baud_rate <= 0:
|
|
114
|
+
raise ValueError(f"baud_rate must be positive, got {self.baud_rate}")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@dataclass
|
|
118
|
+
class ValidationResult:
|
|
119
|
+
"""Results from protocol validation.
|
|
120
|
+
|
|
121
|
+
Attributes:
|
|
122
|
+
success: Overall validation success status.
|
|
123
|
+
messages_sent: Number of messages successfully sent.
|
|
124
|
+
messages_received: Number of responses received.
|
|
125
|
+
checksum_valid: Number of valid checksums.
|
|
126
|
+
checksum_invalid: Number of invalid checksums.
|
|
127
|
+
timing_valid: Number of responses with valid timing.
|
|
128
|
+
timing_invalid: Number of responses with invalid timing.
|
|
129
|
+
errors: List of error messages encountered.
|
|
130
|
+
response_log: Detailed log of message/response pairs.
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
success: bool
|
|
134
|
+
messages_sent: int
|
|
135
|
+
messages_received: int
|
|
136
|
+
checksum_valid: int
|
|
137
|
+
checksum_invalid: int
|
|
138
|
+
timing_valid: int
|
|
139
|
+
timing_invalid: int
|
|
140
|
+
errors: list[str] = field(default_factory=list)
|
|
141
|
+
response_log: list[dict[str, Any]] = field(default_factory=list)
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def checksum_success_rate(self) -> float:
|
|
145
|
+
"""Calculate checksum success rate.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Fraction of checksums that were valid (0.0-1.0).
|
|
149
|
+
"""
|
|
150
|
+
total = self.checksum_valid + self.checksum_invalid
|
|
151
|
+
return self.checksum_valid / total if total > 0 else 0.0
|
|
152
|
+
|
|
153
|
+
@property
|
|
154
|
+
def timing_success_rate(self) -> float:
|
|
155
|
+
"""Calculate timing success rate.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
Fraction of responses with valid timing (0.0-1.0).
|
|
159
|
+
"""
|
|
160
|
+
total = self.timing_valid + self.timing_invalid
|
|
161
|
+
return self.timing_valid / total if total > 0 else 0.0
|
|
162
|
+
|
|
163
|
+
@property
|
|
164
|
+
def response_rate(self) -> float:
|
|
165
|
+
"""Calculate response rate.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
Fraction of messages that received responses (0.0-1.0).
|
|
169
|
+
"""
|
|
170
|
+
return self.messages_received / self.messages_sent if self.messages_sent > 0 else 0.0
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _checksum_xor(data: bytes) -> int:
|
|
174
|
+
"""Calculate XOR checksum.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
data: Data bytes to checksum.
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
XOR checksum (single byte).
|
|
181
|
+
"""
|
|
182
|
+
result = 0
|
|
183
|
+
for byte in data:
|
|
184
|
+
result ^= byte
|
|
185
|
+
return result
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _checksum_sum(data: bytes) -> int:
|
|
189
|
+
"""Calculate sum checksum.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
data: Data bytes to checksum.
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
Sum checksum (single byte, truncated).
|
|
196
|
+
"""
|
|
197
|
+
return sum(data) & 0xFF
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _checksum_crc8(data: bytes) -> int:
|
|
201
|
+
"""Calculate CRC-8 checksum (SAE J1850).
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
data: Data bytes to checksum.
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
CRC-8 checksum.
|
|
208
|
+
"""
|
|
209
|
+
crc = 0xFF
|
|
210
|
+
poly = 0x1D
|
|
211
|
+
for byte in data:
|
|
212
|
+
crc ^= byte
|
|
213
|
+
for _ in range(8):
|
|
214
|
+
if crc & 0x80:
|
|
215
|
+
crc = (crc << 1) ^ poly
|
|
216
|
+
else:
|
|
217
|
+
crc <<= 1
|
|
218
|
+
crc &= 0xFF
|
|
219
|
+
return crc ^ 0xFF
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _checksum_crc16(data: bytes) -> int:
|
|
223
|
+
"""Calculate CRC-16 checksum (CCITT).
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
data: Data bytes to checksum.
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
CRC-16 checksum low byte.
|
|
230
|
+
"""
|
|
231
|
+
crc = 0xFFFF
|
|
232
|
+
poly = 0x1021
|
|
233
|
+
for byte in data:
|
|
234
|
+
crc ^= byte << 8
|
|
235
|
+
for _ in range(8):
|
|
236
|
+
if crc & 0x8000:
|
|
237
|
+
crc = (crc << 1) ^ poly
|
|
238
|
+
else:
|
|
239
|
+
crc <<= 1
|
|
240
|
+
crc &= 0xFFFF
|
|
241
|
+
return crc & 0xFF
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _init_validation_counters() -> dict[str, int]:
|
|
245
|
+
"""Initialize validation counters.
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
Dictionary with zero-initialized counters.
|
|
249
|
+
"""
|
|
250
|
+
return {
|
|
251
|
+
"messages_sent": 0,
|
|
252
|
+
"messages_received": 0,
|
|
253
|
+
"checksum_valid": 0,
|
|
254
|
+
"checksum_invalid": 0,
|
|
255
|
+
"timing_valid": 0,
|
|
256
|
+
"timing_invalid": 0,
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _create_log_entry(index: int, message: bytes, send_time: float) -> dict[str, Any]:
|
|
261
|
+
"""Create base log entry for message.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
index: Message index.
|
|
265
|
+
message: Message bytes.
|
|
266
|
+
send_time: Send timestamp.
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
Log entry dictionary.
|
|
270
|
+
"""
|
|
271
|
+
return {
|
|
272
|
+
"message_index": index,
|
|
273
|
+
"message": message.hex(),
|
|
274
|
+
"send_time": send_time,
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _update_log_with_response(
|
|
279
|
+
log_entry: dict[str, Any], response: bytes, recv_time: float, send_time: float
|
|
280
|
+
) -> None:
|
|
281
|
+
"""Update log entry with response data.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
log_entry: Log entry to update.
|
|
285
|
+
response: Response bytes.
|
|
286
|
+
recv_time: Receive timestamp.
|
|
287
|
+
send_time: Send timestamp.
|
|
288
|
+
"""
|
|
289
|
+
log_entry["response"] = response.hex()
|
|
290
|
+
log_entry["recv_time"] = recv_time
|
|
291
|
+
log_entry["response_time"] = recv_time - send_time
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _validate_response(
|
|
295
|
+
validator: ReplayValidator,
|
|
296
|
+
response: bytes,
|
|
297
|
+
spec: ProtocolSpec,
|
|
298
|
+
index: int,
|
|
299
|
+
recv_time: float,
|
|
300
|
+
send_time: float,
|
|
301
|
+
counters: dict[str, int],
|
|
302
|
+
errors: list[str],
|
|
303
|
+
log_entry: dict[str, Any],
|
|
304
|
+
) -> None:
|
|
305
|
+
"""Validate response checksum and timing.
|
|
306
|
+
|
|
307
|
+
Args:
|
|
308
|
+
validator: ReplayValidator instance.
|
|
309
|
+
response: Response bytes.
|
|
310
|
+
spec: Protocol specification.
|
|
311
|
+
index: Message index.
|
|
312
|
+
recv_time: Receive timestamp.
|
|
313
|
+
send_time: Send timestamp.
|
|
314
|
+
counters: Validation counters.
|
|
315
|
+
errors: Error list.
|
|
316
|
+
log_entry: Log entry to update.
|
|
317
|
+
"""
|
|
318
|
+
# Validate checksum if enabled
|
|
319
|
+
if validator.config.validate_checksums and spec.checksum_algorithm != "none":
|
|
320
|
+
if validator._validate_checksum(response, spec):
|
|
321
|
+
counters["checksum_valid"] += 1
|
|
322
|
+
log_entry["checksum_valid"] = True
|
|
323
|
+
else:
|
|
324
|
+
counters["checksum_invalid"] += 1
|
|
325
|
+
log_entry["checksum_valid"] = False
|
|
326
|
+
errors.append(f"Message {index}: Invalid checksum in response")
|
|
327
|
+
|
|
328
|
+
# Validate timing if enabled
|
|
329
|
+
if validator.config.validate_timing:
|
|
330
|
+
response_time = recv_time - send_time
|
|
331
|
+
if validator._validate_timing(
|
|
332
|
+
send_time, recv_time, spec.expected_response_time, spec.timing_tolerance
|
|
333
|
+
):
|
|
334
|
+
counters["timing_valid"] += 1
|
|
335
|
+
log_entry["timing_valid"] = True
|
|
336
|
+
else:
|
|
337
|
+
counters["timing_invalid"] += 1
|
|
338
|
+
log_entry["timing_valid"] = False
|
|
339
|
+
errors.append(f"Message {index}: Response time {response_time:.3f}s outside tolerance")
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def _compute_overall_success(
|
|
343
|
+
errors: list[str], counters: dict[str, int], spec: ProtocolSpec
|
|
344
|
+
) -> bool:
|
|
345
|
+
"""Compute overall validation success.
|
|
346
|
+
|
|
347
|
+
Args:
|
|
348
|
+
errors: List of errors.
|
|
349
|
+
counters: Validation counters.
|
|
350
|
+
spec: Protocol specification.
|
|
351
|
+
|
|
352
|
+
Returns:
|
|
353
|
+
True if validation successful.
|
|
354
|
+
"""
|
|
355
|
+
return (
|
|
356
|
+
len(errors) == 0
|
|
357
|
+
and counters["messages_sent"] > 0
|
|
358
|
+
and (
|
|
359
|
+
not spec.require_response or counters["messages_received"] == counters["messages_sent"]
|
|
360
|
+
)
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _checksum_crc32(data: bytes) -> int:
|
|
365
|
+
"""Calculate CRC-32 checksum (IEEE 802.3).
|
|
366
|
+
|
|
367
|
+
Args:
|
|
368
|
+
data: Data bytes to checksum.
|
|
369
|
+
|
|
370
|
+
Returns:
|
|
371
|
+
CRC-32 checksum low byte.
|
|
372
|
+
"""
|
|
373
|
+
crc = 0xFFFFFFFF
|
|
374
|
+
poly = 0xEDB88320
|
|
375
|
+
for byte in data:
|
|
376
|
+
crc ^= byte
|
|
377
|
+
for _ in range(8):
|
|
378
|
+
if crc & 1:
|
|
379
|
+
crc = (crc >> 1) ^ poly
|
|
380
|
+
else:
|
|
381
|
+
crc >>= 1
|
|
382
|
+
return (~crc) & 0xFF
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
class ReplayValidator:
|
|
386
|
+
"""Validator for protocol replay testing.
|
|
387
|
+
|
|
388
|
+
Sends test messages to target devices via serial, CAN, or network interfaces,
|
|
389
|
+
captures responses, and validates checksums, timing, and state transitions.
|
|
390
|
+
|
|
391
|
+
Example:
|
|
392
|
+
>>> config = ReplayConfig(interface="serial", port="/dev/ttyUSB0")
|
|
393
|
+
>>> validator = ReplayValidator(config)
|
|
394
|
+
>>> spec = ProtocolSpec(name="UART", checksum_algorithm="xor")
|
|
395
|
+
>>> result = validator.validate_protocol(spec, [b"\\x01\\x02\\x03"])
|
|
396
|
+
>>> validator.close()
|
|
397
|
+
"""
|
|
398
|
+
|
|
399
|
+
def __init__(self, config: ReplayConfig) -> None:
|
|
400
|
+
"""Initialize validator with interface configuration.
|
|
401
|
+
|
|
402
|
+
Args:
|
|
403
|
+
config: Replay configuration specifying interface and parameters.
|
|
404
|
+
|
|
405
|
+
Raises:
|
|
406
|
+
ImportError: If required library for interface is not installed.
|
|
407
|
+
ValueError: If configuration is invalid.
|
|
408
|
+
"""
|
|
409
|
+
self.config = config
|
|
410
|
+
self._connection: Any = None
|
|
411
|
+
self._is_connected = False
|
|
412
|
+
|
|
413
|
+
def __enter__(self) -> ReplayValidator:
|
|
414
|
+
"""Context manager entry."""
|
|
415
|
+
self.connect()
|
|
416
|
+
return self
|
|
417
|
+
|
|
418
|
+
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
419
|
+
"""Context manager exit."""
|
|
420
|
+
self.close()
|
|
421
|
+
|
|
422
|
+
def connect(self) -> None:
|
|
423
|
+
"""Establish connection to target device.
|
|
424
|
+
|
|
425
|
+
Raises:
|
|
426
|
+
ImportError: If required library is not installed.
|
|
427
|
+
OSError: If connection fails.
|
|
428
|
+
"""
|
|
429
|
+
if self._is_connected:
|
|
430
|
+
return
|
|
431
|
+
|
|
432
|
+
if self.config.interface == "serial":
|
|
433
|
+
self._connect_serial()
|
|
434
|
+
elif self.config.interface == "socketcan":
|
|
435
|
+
self._connect_socketcan()
|
|
436
|
+
elif self.config.interface in ("udp", "tcp"):
|
|
437
|
+
self._connect_network()
|
|
438
|
+
|
|
439
|
+
self._is_connected = True
|
|
440
|
+
|
|
441
|
+
def _connect_serial(self) -> None:
|
|
442
|
+
"""Connect to serial port.
|
|
443
|
+
|
|
444
|
+
Raises:
|
|
445
|
+
ValueError: If port is not a string.
|
|
446
|
+
ImportError: If pyserial is not installed.
|
|
447
|
+
OSError: If serial port cannot be opened.
|
|
448
|
+
"""
|
|
449
|
+
if not isinstance(self.config.port, str):
|
|
450
|
+
raise ValueError("Serial port must be string")
|
|
451
|
+
self._connection = connect_serial_port(
|
|
452
|
+
port=self.config.port,
|
|
453
|
+
baud_rate=self.config.baud_rate,
|
|
454
|
+
timeout=self.config.timeout,
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
def _connect_socketcan(self) -> None:
|
|
458
|
+
"""Connect to SocketCAN interface.
|
|
459
|
+
|
|
460
|
+
Raises:
|
|
461
|
+
ImportError: If python-can is not installed.
|
|
462
|
+
OSError: If CAN interface cannot be opened.
|
|
463
|
+
"""
|
|
464
|
+
try:
|
|
465
|
+
import can
|
|
466
|
+
except ImportError as e:
|
|
467
|
+
raise ImportError(
|
|
468
|
+
"python-can is required for SocketCAN interface. "
|
|
469
|
+
"Install with: pip install python-can"
|
|
470
|
+
) from e
|
|
471
|
+
|
|
472
|
+
if not isinstance(self.config.port, str):
|
|
473
|
+
raise ValueError(f"CAN interface must be string, got {type(self.config.port)}")
|
|
474
|
+
|
|
475
|
+
self._connection = can.interface.Bus(
|
|
476
|
+
channel=self.config.port, interface="socketcan", receive_own_messages=False
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
def _connect_network(self) -> None:
|
|
480
|
+
"""Connect to UDP/TCP socket.
|
|
481
|
+
|
|
482
|
+
Raises:
|
|
483
|
+
OSError: If socket cannot be created or connected.
|
|
484
|
+
"""
|
|
485
|
+
import socket
|
|
486
|
+
|
|
487
|
+
if not isinstance(self.config.port, int):
|
|
488
|
+
raise ValueError(f"Network port must be integer, got {type(self.config.port)}")
|
|
489
|
+
|
|
490
|
+
if self.config.interface == "udp":
|
|
491
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
492
|
+
sock.settimeout(self.config.timeout)
|
|
493
|
+
else: # tcp
|
|
494
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
495
|
+
sock.settimeout(self.config.timeout)
|
|
496
|
+
sock.connect((self.config.host, self.config.port))
|
|
497
|
+
|
|
498
|
+
self._connection = sock
|
|
499
|
+
|
|
500
|
+
def close(self) -> None:
|
|
501
|
+
"""Close connection to target device."""
|
|
502
|
+
if not self._is_connected or self._connection is None:
|
|
503
|
+
return
|
|
504
|
+
|
|
505
|
+
try:
|
|
506
|
+
if hasattr(self._connection, "close"):
|
|
507
|
+
self._connection.close()
|
|
508
|
+
elif hasattr(self._connection, "shutdown"):
|
|
509
|
+
self._connection.shutdown()
|
|
510
|
+
finally:
|
|
511
|
+
self._connection = None
|
|
512
|
+
self._is_connected = False
|
|
513
|
+
|
|
514
|
+
def validate_protocol(self, spec: ProtocolSpec, test_messages: list[bytes]) -> ValidationResult:
|
|
515
|
+
"""Send test messages and validate responses.
|
|
516
|
+
|
|
517
|
+
Args:
|
|
518
|
+
spec: Protocol specification with validation criteria.
|
|
519
|
+
test_messages: List of test messages to send.
|
|
520
|
+
|
|
521
|
+
Returns:
|
|
522
|
+
Validation result with success status and detailed metrics.
|
|
523
|
+
|
|
524
|
+
Raises:
|
|
525
|
+
RuntimeError: If not connected to target device.
|
|
526
|
+
|
|
527
|
+
Example:
|
|
528
|
+
>>> config = ReplayConfig(interface="serial", port="/dev/ttyUSB0")
|
|
529
|
+
>>> validator = ReplayValidator(config)
|
|
530
|
+
>>> validator.connect()
|
|
531
|
+
>>> spec = ProtocolSpec(name="Test", checksum_algorithm="xor")
|
|
532
|
+
>>> result = validator.validate_protocol(spec, [b"\\x01\\x02\\x03"])
|
|
533
|
+
>>> print(f"Success: {result.success}")
|
|
534
|
+
>>> validator.close()
|
|
535
|
+
"""
|
|
536
|
+
if not self._is_connected:
|
|
537
|
+
raise RuntimeError("Not connected. Call connect() first.")
|
|
538
|
+
|
|
539
|
+
counters = _init_validation_counters()
|
|
540
|
+
errors: list[str] = []
|
|
541
|
+
response_log: list[dict[str, Any]] = []
|
|
542
|
+
|
|
543
|
+
for i, message in enumerate(test_messages):
|
|
544
|
+
try:
|
|
545
|
+
send_time, response, recv_time = self._send_and_measure(message)
|
|
546
|
+
counters["messages_sent"] += 1
|
|
547
|
+
|
|
548
|
+
log_entry = _create_log_entry(i, message, send_time)
|
|
549
|
+
|
|
550
|
+
if response is not None:
|
|
551
|
+
counters["messages_received"] += 1
|
|
552
|
+
_update_log_with_response(log_entry, response, recv_time, send_time)
|
|
553
|
+
_validate_response(
|
|
554
|
+
self, response, spec, i, recv_time, send_time, counters, errors, log_entry
|
|
555
|
+
)
|
|
556
|
+
else:
|
|
557
|
+
log_entry["response"] = None
|
|
558
|
+
if spec.require_response:
|
|
559
|
+
errors.append(f"Message {i}: No response received")
|
|
560
|
+
|
|
561
|
+
response_log.append(log_entry)
|
|
562
|
+
|
|
563
|
+
except Exception as e:
|
|
564
|
+
errors.append(f"Message {i}: {type(e).__name__}: {e}")
|
|
565
|
+
response_log.append({"message_index": i, "message": message.hex(), "error": str(e)})
|
|
566
|
+
|
|
567
|
+
success = _compute_overall_success(errors, counters, spec)
|
|
568
|
+
|
|
569
|
+
return ValidationResult(
|
|
570
|
+
success=success,
|
|
571
|
+
messages_sent=counters["messages_sent"],
|
|
572
|
+
messages_received=counters["messages_received"],
|
|
573
|
+
checksum_valid=counters["checksum_valid"],
|
|
574
|
+
checksum_invalid=counters["checksum_invalid"],
|
|
575
|
+
timing_valid=counters["timing_valid"],
|
|
576
|
+
timing_invalid=counters["timing_invalid"],
|
|
577
|
+
errors=errors,
|
|
578
|
+
response_log=response_log,
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
def _send_and_measure(self, message: bytes) -> tuple[float, bytes | None, float]:
|
|
582
|
+
"""Send message and measure timing.
|
|
583
|
+
|
|
584
|
+
Args:
|
|
585
|
+
message: Message bytes to send.
|
|
586
|
+
|
|
587
|
+
Returns:
|
|
588
|
+
Tuple of (send_time, response, recv_time).
|
|
589
|
+
"""
|
|
590
|
+
send_time = time.time()
|
|
591
|
+
response = self._send_message(message)
|
|
592
|
+
recv_time = time.time()
|
|
593
|
+
return send_time, response, recv_time
|
|
594
|
+
|
|
595
|
+
def _send_message(self, message: bytes) -> bytes | None:
|
|
596
|
+
"""Send message and capture response.
|
|
597
|
+
|
|
598
|
+
Args:
|
|
599
|
+
message: Message bytes to send.
|
|
600
|
+
|
|
601
|
+
Returns:
|
|
602
|
+
Response bytes, or None if no response received.
|
|
603
|
+
|
|
604
|
+
Raises:
|
|
605
|
+
OSError: If send/receive fails.
|
|
606
|
+
"""
|
|
607
|
+
if self.config.interface == "serial":
|
|
608
|
+
return self._send_serial(message)
|
|
609
|
+
elif self.config.interface == "socketcan":
|
|
610
|
+
return self._send_socketcan(message)
|
|
611
|
+
elif self.config.interface in ("udp", "tcp"):
|
|
612
|
+
return self._send_network(message)
|
|
613
|
+
|
|
614
|
+
return None
|
|
615
|
+
|
|
616
|
+
def _send_serial(self, message: bytes) -> bytes | None:
|
|
617
|
+
"""Send message via serial port.
|
|
618
|
+
|
|
619
|
+
Args:
|
|
620
|
+
message: Message bytes to send.
|
|
621
|
+
|
|
622
|
+
Returns:
|
|
623
|
+
Response bytes, or None if timeout.
|
|
624
|
+
"""
|
|
625
|
+
import serial # type: ignore[import-untyped]
|
|
626
|
+
|
|
627
|
+
ser: serial.Serial = self._connection
|
|
628
|
+
ser.reset_input_buffer() # Clear any old data
|
|
629
|
+
ser.write(message)
|
|
630
|
+
ser.flush()
|
|
631
|
+
|
|
632
|
+
# Read response with timeout
|
|
633
|
+
response = ser.read(1024) # Read up to 1KB
|
|
634
|
+
return response if response else None
|
|
635
|
+
|
|
636
|
+
def _send_socketcan(self, message: bytes) -> bytes | None:
|
|
637
|
+
"""Send message via SocketCAN.
|
|
638
|
+
|
|
639
|
+
Args:
|
|
640
|
+
message: Message bytes to send (max 8 bytes for CAN 2.0, 64 for CAN-FD).
|
|
641
|
+
|
|
642
|
+
Returns:
|
|
643
|
+
Response bytes, or None if timeout.
|
|
644
|
+
"""
|
|
645
|
+
import can
|
|
646
|
+
|
|
647
|
+
bus: CANBusProtocol = self._connection
|
|
648
|
+
|
|
649
|
+
# Create CAN message with standard ID 0x123 (configurable in future)
|
|
650
|
+
msg = can.Message(arbitration_id=0x123, data=message, is_extended_id=False)
|
|
651
|
+
bus.send(msg)
|
|
652
|
+
|
|
653
|
+
# Wait for response
|
|
654
|
+
response_msg = bus.recv(timeout=self.config.timeout)
|
|
655
|
+
return bytes(response_msg.data) if response_msg else None
|
|
656
|
+
|
|
657
|
+
def _send_network(self, message: bytes) -> bytes | None:
|
|
658
|
+
"""Send message via UDP/TCP socket.
|
|
659
|
+
|
|
660
|
+
Args:
|
|
661
|
+
message: Message bytes to send.
|
|
662
|
+
|
|
663
|
+
Returns:
|
|
664
|
+
Response bytes, or None if timeout.
|
|
665
|
+
"""
|
|
666
|
+
|
|
667
|
+
sock: socket.socket = self._connection
|
|
668
|
+
|
|
669
|
+
if self.config.interface == "udp":
|
|
670
|
+
sock.sendto(message, (self.config.host, self.config.port))
|
|
671
|
+
try:
|
|
672
|
+
response, _ = sock.recvfrom(1024)
|
|
673
|
+
return response
|
|
674
|
+
except TimeoutError:
|
|
675
|
+
return None
|
|
676
|
+
else: # tcp
|
|
677
|
+
sock.sendall(message)
|
|
678
|
+
try:
|
|
679
|
+
response = sock.recv(1024)
|
|
680
|
+
return response if response else None
|
|
681
|
+
except TimeoutError:
|
|
682
|
+
return None
|
|
683
|
+
|
|
684
|
+
def _validate_checksum(self, message: bytes, spec: ProtocolSpec) -> bool:
|
|
685
|
+
"""Verify checksum in received message.
|
|
686
|
+
|
|
687
|
+
Args:
|
|
688
|
+
message: Message bytes to validate.
|
|
689
|
+
spec: Protocol specification with checksum configuration.
|
|
690
|
+
|
|
691
|
+
Returns:
|
|
692
|
+
True if checksum is valid, False otherwise.
|
|
693
|
+
|
|
694
|
+
Example:
|
|
695
|
+
>>> validator = ReplayValidator(ReplayConfig("serial", "/dev/null"))
|
|
696
|
+
>>> spec = ProtocolSpec(name="Test", checksum_algorithm="xor")
|
|
697
|
+
>>> validator._validate_checksum(b"\\x01\\x02\\x03", spec)
|
|
698
|
+
True
|
|
699
|
+
"""
|
|
700
|
+
if len(message) == 0:
|
|
701
|
+
return False
|
|
702
|
+
|
|
703
|
+
# Extract checksum from message
|
|
704
|
+
checksum_pos = spec.checksum_position if spec.checksum_position >= 0 else len(message) - 1
|
|
705
|
+
if checksum_pos >= len(message):
|
|
706
|
+
return False
|
|
707
|
+
|
|
708
|
+
received_checksum = message[checksum_pos]
|
|
709
|
+
|
|
710
|
+
# Calculate expected checksum on data portion
|
|
711
|
+
if checksum_pos == len(message) - 1:
|
|
712
|
+
data = message[:-1]
|
|
713
|
+
else:
|
|
714
|
+
data = message[:checksum_pos] + message[checksum_pos + 1 :]
|
|
715
|
+
|
|
716
|
+
expected_checksum = self._calculate_checksum(data, spec.checksum_algorithm)
|
|
717
|
+
|
|
718
|
+
return received_checksum == expected_checksum
|
|
719
|
+
|
|
720
|
+
def _calculate_checksum(self, data: bytes, algorithm: str) -> int:
|
|
721
|
+
"""Calculate checksum using specified algorithm.
|
|
722
|
+
|
|
723
|
+
Args:
|
|
724
|
+
data: Data bytes to checksum.
|
|
725
|
+
algorithm: Checksum algorithm ("crc8", "crc16", "crc32", "xor", "sum").
|
|
726
|
+
|
|
727
|
+
Returns:
|
|
728
|
+
Calculated checksum value (truncated to single byte for most algorithms).
|
|
729
|
+
|
|
730
|
+
Raises:
|
|
731
|
+
ValueError: If algorithm is not supported.
|
|
732
|
+
|
|
733
|
+
Example:
|
|
734
|
+
>>> validator = ReplayValidator(ReplayConfig("serial", "/dev/null"))
|
|
735
|
+
>>> validator._calculate_checksum(b"\\x01\\x02\\x03", "xor")
|
|
736
|
+
0
|
|
737
|
+
"""
|
|
738
|
+
checksum_functions = {
|
|
739
|
+
"xor": _checksum_xor,
|
|
740
|
+
"sum": _checksum_sum,
|
|
741
|
+
"crc8": _checksum_crc8,
|
|
742
|
+
"crc16": _checksum_crc16,
|
|
743
|
+
"crc32": _checksum_crc32,
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
if algorithm not in checksum_functions:
|
|
747
|
+
raise ValueError(
|
|
748
|
+
f"Unsupported checksum algorithm: {algorithm}. "
|
|
749
|
+
f"Supported: {', '.join(checksum_functions.keys())}"
|
|
750
|
+
)
|
|
751
|
+
|
|
752
|
+
return checksum_functions[algorithm](data)
|
|
753
|
+
|
|
754
|
+
def _validate_timing(
|
|
755
|
+
self, send_time: float, recv_time: float, expected: float, tolerance: float
|
|
756
|
+
) -> bool:
|
|
757
|
+
"""Verify response timing within tolerance.
|
|
758
|
+
|
|
759
|
+
Args:
|
|
760
|
+
send_time: Message send timestamp.
|
|
761
|
+
recv_time: Response receive timestamp.
|
|
762
|
+
expected: Expected response time in seconds.
|
|
763
|
+
tolerance: Timing tolerance as fraction (0.1 = 10%).
|
|
764
|
+
|
|
765
|
+
Returns:
|
|
766
|
+
True if timing is within tolerance, False otherwise.
|
|
767
|
+
|
|
768
|
+
Example:
|
|
769
|
+
>>> validator = ReplayValidator(ReplayConfig("serial", "/dev/null"))
|
|
770
|
+
>>> validator._validate_timing(0.0, 0.1, 0.1, 0.2)
|
|
771
|
+
True
|
|
772
|
+
>>> validator._validate_timing(0.0, 0.2, 0.1, 0.2)
|
|
773
|
+
False
|
|
774
|
+
"""
|
|
775
|
+
actual_time = recv_time - send_time
|
|
776
|
+
lower_bound = expected * (1.0 - tolerance)
|
|
777
|
+
upper_bound = expected * (1.0 + tolerance)
|
|
778
|
+
# Add small epsilon to handle floating point precision errors
|
|
779
|
+
eps = 1e-9
|
|
780
|
+
return (lower_bound - eps) <= actual_time <= (upper_bound + eps)
|
|
781
|
+
|
|
782
|
+
|
|
783
|
+
__all__ = [
|
|
784
|
+
"ProtocolSpec",
|
|
785
|
+
"ReplayConfig",
|
|
786
|
+
"ReplayValidator",
|
|
787
|
+
"ValidationResult",
|
|
788
|
+
]
|